mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
fix(deepseek-web): rewrite auth to userToken Bearer + WASM PoW solver (#2452)
Integrated into release/v3.8.1
This commit is contained in:
@@ -96,6 +96,8 @@ const nextConfig = {
|
||||
"./src/mitm/server.cjs",
|
||||
"./open-sse/services/compression/engines/rtk/filters/**/*.json",
|
||||
"./open-sse/services/compression/rules/**/*.json",
|
||||
"./open-sse/lib/sha3_wasm_bg.wasm",
|
||||
"./open-sse/lib/deepseek-pow-solver.cjs",
|
||||
],
|
||||
},
|
||||
outputFileTracingExcludes: {
|
||||
|
||||
@@ -2175,6 +2175,40 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
],
|
||||
},
|
||||
|
||||
"deepseek-web": {
|
||||
id: "deepseek-web",
|
||||
alias: "ds-web",
|
||||
format: "openai",
|
||||
executor: "deepseek-web",
|
||||
baseUrl: "https://chat.deepseek.com/api/v0/chat/completion",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" },
|
||||
{ id: "deepseek-v4-pro-think", name: "DeepSeek V4 Pro Think", supportsReasoning: true },
|
||||
{ id: "deepseek-v4-pro-search", name: "DeepSeek V4 Pro Search" },
|
||||
{
|
||||
id: "deepseek-v4-pro-think-search",
|
||||
name: "DeepSeek V4 Pro Think+Search",
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
|
||||
{ id: "deepseek-v4-flash-think", name: "DeepSeek V4 Flash Think", supportsReasoning: true },
|
||||
{ id: "deepseek-v4-flash-search", name: "DeepSeek V4 Flash Search" },
|
||||
{
|
||||
id: "deepseek-v4-flash-think-search",
|
||||
name: "DeepSeek V4 Flash Think+Search",
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{ id: "deepseek-chat", name: "DeepSeek Chat" },
|
||||
{ id: "deepseek-reasoner", name: "DeepSeek Reasoner", supportsReasoning: true },
|
||||
{ id: "DeepSeek-R1", name: "DeepSeek R1", supportsReasoning: true },
|
||||
{ id: "DeepSeek-R1-Search", name: "DeepSeek R1 Search", supportsReasoning: true },
|
||||
{ id: "DeepSeek-V3.2", name: "DeepSeek V3.2" },
|
||||
{ id: "DeepSeek-Search", name: "DeepSeek Search" },
|
||||
],
|
||||
},
|
||||
|
||||
"grok-web": {
|
||||
id: "grok-web",
|
||||
alias: "gw",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ExecuteInput } from "./base.ts";
|
||||
import { DeepSeekWebExecutor, DEEPSEEK_WEB_BASE } from "./deepseek-web.ts";
|
||||
import { DeepSeekWebExecutor, acquireAccessToken, tokenCache } from "./deepseek-web.ts";
|
||||
|
||||
interface AutoRefreshConfig {
|
||||
sessionRefreshInterval?: number;
|
||||
@@ -18,12 +18,12 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor {
|
||||
private sessionValid = false;
|
||||
private retryCount = 0;
|
||||
private readonly maxRetries = 2;
|
||||
private currentCookies = "";
|
||||
private currentUserToken = "";
|
||||
|
||||
constructor(config: AutoRefreshConfig = {}) {
|
||||
super();
|
||||
this.refreshConfig = {
|
||||
sessionRefreshInterval: 20 * 60 * 60 * 1000,
|
||||
sessionRefreshInterval: 50 * 60 * 1000,
|
||||
maxRefreshRetries: 3,
|
||||
autoRefresh: true,
|
||||
...config,
|
||||
@@ -35,8 +35,8 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor {
|
||||
|
||||
override async execute(input: ExecuteInput) {
|
||||
this.retryCount = 0;
|
||||
this.currentCookies =
|
||||
((input.credentials as unknown as Record<string, unknown>).cookies as string) || "";
|
||||
const creds = input.credentials as unknown as Record<string, unknown>;
|
||||
this.currentUserToken = (creds.apiKey as string) || (creds.accessToken as string) || "";
|
||||
return this.executeWithRetry(input);
|
||||
}
|
||||
|
||||
@@ -71,34 +71,26 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor {
|
||||
}
|
||||
|
||||
private async doRefreshSession(): Promise<void> {
|
||||
if (!this.currentCookies) {
|
||||
if (!this.currentUserToken) {
|
||||
this.sessionValid = false;
|
||||
throw new Error("No cookies available for session refresh");
|
||||
throw new Error("No userToken available for session refresh");
|
||||
}
|
||||
const { maxRefreshRetries } = this.refreshConfig;
|
||||
for (let attempt = 0; attempt < maxRefreshRetries; attempt++) {
|
||||
try {
|
||||
// Validate session by fetching current user (lightweight, no PoW needed)
|
||||
const response = await fetch(`${DEEPSEEK_WEB_BASE}/api/v0/users/current`, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
Cookie: this.currentCookies,
|
||||
},
|
||||
});
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
if (json?.data?.biz_data?.token) {
|
||||
this.lastRefreshTime = Date.now();
|
||||
this.sessionValid = true;
|
||||
return;
|
||||
}
|
||||
tokenCache.delete(this.currentUserToken);
|
||||
const accessToken = await acquireAccessToken(this.currentUserToken);
|
||||
if (accessToken) {
|
||||
this.lastRefreshTime = Date.now();
|
||||
this.sessionValid = true;
|
||||
return;
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
this.sessionValid = false;
|
||||
throw new Error("Session expired - requires re-authentication");
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
if (msg.includes("invalid") || msg.includes("expired")) {
|
||||
this.sessionValid = false;
|
||||
throw new Error("Token expired — get a new userToken from DeepSeek localStorage");
|
||||
}
|
||||
if (attempt >= maxRefreshRetries - 1) throw error;
|
||||
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
|
||||
}
|
||||
@@ -106,18 +98,22 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor {
|
||||
throw new Error("Failed to refresh session after max retries");
|
||||
}
|
||||
|
||||
private executeBase(input: ExecuteInput) {
|
||||
return super.execute(input);
|
||||
}
|
||||
|
||||
private async executeWithRetry(input: ExecuteInput) {
|
||||
try {
|
||||
return await super.execute(input);
|
||||
return await this.executeBase(input);
|
||||
} catch (error: unknown) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
const isUnauthorized =
|
||||
msg.includes("401") || msg.includes("Unauthorized") || msg.includes("Session expired");
|
||||
msg.includes("401") || msg.includes("Unauthorized") || msg.includes("expired");
|
||||
if (isUnauthorized && this.retryCount < this.maxRetries) {
|
||||
this.retryCount++;
|
||||
try {
|
||||
await this.doRefreshSession();
|
||||
return await super.execute(input);
|
||||
return await this.executeBase(input);
|
||||
} catch (refreshError) {
|
||||
console.error(
|
||||
`[DeepSeek-WEB] Session refresh failed (attempt ${this.retryCount}/${this.maxRetries}):`,
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
||||
import { solveDeepSeekPow } from "../lib/deepseek-pow.ts";
|
||||
import { solveDeepSeekPowAsync } from "../lib/deepseek-pow.ts";
|
||||
|
||||
export const DEEPSEEK_WEB_BASE = "https://chat.deepseek.com";
|
||||
const COMPLETION_URL = `${DEEPSEEK_WEB_BASE}/api/v0/chat/completion`;
|
||||
const USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||
const DEEPSEEK_API_BASE = `${DEEPSEEK_WEB_BASE}/api`;
|
||||
const COMPLETION_URL = `${DEEPSEEK_API_BASE}/v0/chat/completion`;
|
||||
|
||||
// DeepSeek native API headers
|
||||
const BASE_HEADERS: Record<string, string> = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"x-app-version": "2.0.0",
|
||||
"x-client-platform": "web",
|
||||
"x-client-version": "2.0.0",
|
||||
"x-client-locale": "en_US",
|
||||
const FAKE_HEADERS: Record<string, string> = {
|
||||
Accept: "*/*",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
Origin: DEEPSEEK_WEB_BASE,
|
||||
Referer: `${DEEPSEEK_WEB_BASE}/`,
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
"X-App-Version": "20241129.1",
|
||||
"X-Client-Locale": "en-US",
|
||||
"X-Client-Platform": "web",
|
||||
"X-Client-Version": "1.8.0",
|
||||
};
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DeepSeekCredentials {
|
||||
cookies: string;
|
||||
}
|
||||
|
||||
interface PowChallenge {
|
||||
algorithm: string;
|
||||
challenge: string;
|
||||
@@ -32,14 +32,39 @@ interface PowChallenge {
|
||||
target_path: string;
|
||||
}
|
||||
|
||||
interface TokenInfo {
|
||||
accessToken: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
// ── Token cache (keyed by userToken → short-lived access token) ─────────
|
||||
|
||||
const tokenCache = new Map<string, TokenInfo>();
|
||||
const sessionCache = new Map<string, { sessionId: string; createdAt: number }>();
|
||||
|
||||
const SESSION_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const CACHE_MAX_SIZE = 100;
|
||||
|
||||
function evictOldest(cache: Map<string, unknown>): void {
|
||||
if (cache.size >= CACHE_MAX_SIZE) {
|
||||
const first = cache.keys().next().value;
|
||||
if (first) cache.delete(first);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function validateCredentials(creds: unknown): creds is DeepSeekCredentials {
|
||||
const raw =
|
||||
typeof creds === "object" && creds !== null
|
||||
? (creds as Record<string, unknown>).cookies
|
||||
: undefined;
|
||||
return typeof raw === "string" && raw.includes("ds_session_id=");
|
||||
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":"..."})
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed?.value === "string") return parsed.value;
|
||||
} catch {
|
||||
// not JSON, use raw
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function errorResponse(status: number, message: string, dsCode?: number): Response {
|
||||
@@ -51,20 +76,47 @@ function errorResponse(status: number, message: string, dsCode?: number): Respon
|
||||
);
|
||||
}
|
||||
|
||||
function mapModelToType(model?: string): { modelType: string; thinking: boolean } {
|
||||
if (!model) return { modelType: "default", thinking: false };
|
||||
const m = model.toLowerCase();
|
||||
if (m.includes("r1") || m.includes("reason") || m.includes("think"))
|
||||
return { modelType: "deepseek_r1", thinking: true };
|
||||
if (m.includes("v3")) return { modelType: "deepseek_v3", thinking: false };
|
||||
if (m.includes("expert")) return { modelType: "expert", thinking: true };
|
||||
return { modelType: "default", thinking: false };
|
||||
function resolveModelOptions(
|
||||
model?: string,
|
||||
bodyObj?: Record<string, unknown>
|
||||
): {
|
||||
modelType: string;
|
||||
thinkingEnabled: boolean;
|
||||
searchEnabled: boolean;
|
||||
} {
|
||||
const m = (model || "").toLowerCase();
|
||||
const modelType = m.includes("pro") || m.includes("expert") ? "expert" : "default";
|
||||
const thinkingEnabled =
|
||||
m.includes("r1") ||
|
||||
m.includes("think") ||
|
||||
m.includes("reason") ||
|
||||
bodyObj?.thinking_enabled === true ||
|
||||
bodyObj?.thinking === true ||
|
||||
!!bodyObj?.reasoning_effort;
|
||||
const searchEnabled =
|
||||
m.includes("search") ||
|
||||
bodyObj?.search_enabled === true ||
|
||||
bodyObj?.search === true ||
|
||||
bodyObj?.web_search === true;
|
||||
return { modelType, thinkingEnabled, searchEnabled };
|
||||
}
|
||||
|
||||
function generateFakeCookie(): string {
|
||||
const ts = Date.now();
|
||||
const hex = (n: number) =>
|
||||
Array.from({ length: n }, () => Math.floor(Math.random() * 16).toString(16)).join("");
|
||||
const uid = () =>
|
||||
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
return `intercom-HWWAFSESTIME=${ts}; HWWAFSESID=${hex(18)}; Hm_lvt_${uid()}=${Math.floor(ts / 1000)}; _frid=${uid()}`;
|
||||
}
|
||||
|
||||
// ── PoW Solver (DeepSeekHashV1) ─────────────────────────────────────────
|
||||
|
||||
function solvePow(challenge: PowChallenge): Record<string, unknown> {
|
||||
const answer = solveDeepSeekPow(
|
||||
async function solvePow(challenge: PowChallenge): Promise<string> {
|
||||
const answer = await solveDeepSeekPowAsync(
|
||||
challenge.algorithm,
|
||||
challenge.challenge,
|
||||
challenge.salt,
|
||||
@@ -72,14 +124,16 @@ function solvePow(challenge: PowChallenge): Record<string, unknown> {
|
||||
challenge.expire_at
|
||||
);
|
||||
if (answer < 0) throw new Error("PoW solver failed");
|
||||
return {
|
||||
algorithm: challenge.algorithm,
|
||||
challenge: challenge.challenge,
|
||||
salt: challenge.salt,
|
||||
answer,
|
||||
signature: challenge.signature,
|
||||
target_path: challenge.target_path,
|
||||
};
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
algorithm: challenge.algorithm,
|
||||
challenge: challenge.challenge,
|
||||
salt: challenge.salt,
|
||||
answer,
|
||||
signature: challenge.signature,
|
||||
target_path: challenge.target_path,
|
||||
})
|
||||
).toString("base64");
|
||||
}
|
||||
|
||||
// ── SSE Transform (DeepSeek → OpenAI) ───────────────────────────────────
|
||||
@@ -120,8 +174,8 @@ function transformSSE(deepseekStream: ReadableStream, model: string): ReadableSt
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
const payload = line.slice(6).trim();
|
||||
if (!line.startsWith("data: ") && !line.startsWith("data:")) continue;
|
||||
const payload = line.replace(/^data:\s*/, "").trim();
|
||||
|
||||
if (payload === "[DONE]") {
|
||||
if (!emittedRole) {
|
||||
@@ -141,7 +195,6 @@ function transformSSE(deepseekStream: ReadableStream, model: string): ReadableSt
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract content from DeepSeek fragments
|
||||
const fragments = (data as any)?.v?.response?.fragments;
|
||||
if (Array.isArray(fragments)) {
|
||||
if (!emittedRole) {
|
||||
@@ -150,12 +203,32 @@ function transformSSE(deepseekStream: ReadableStream, model: string): ReadableSt
|
||||
}
|
||||
for (const frag of fragments) {
|
||||
if (typeof frag.content === "string" && frag.content.length > 0) {
|
||||
chunk({ content: frag.content });
|
||||
if (frag.type === "THINK") {
|
||||
chunk({ reasoning_content: frag.content });
|
||||
} else {
|
||||
chunk({ content: frag.content });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// response/fragments path (incremental updates)
|
||||
if ((data as any)?.p === "response/fragments" && Array.isArray((data as any)?.v)) {
|
||||
if (!emittedRole) {
|
||||
emittedRole = true;
|
||||
chunk({ role: "assistant", content: "" });
|
||||
}
|
||||
for (const frag of (data as any).v) {
|
||||
if (typeof frag.content === "string" && frag.content.length > 0) {
|
||||
if (frag.type === "THINK") {
|
||||
chunk({ reasoning_content: frag.content });
|
||||
} else {
|
||||
chunk({ content: frag.content });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for stream end
|
||||
if ((data as any)?.p === "response/status" && (data as any)?.v === "FINISHED") {
|
||||
if (!emittedRole) {
|
||||
emittedRole = true;
|
||||
@@ -166,18 +239,13 @@ function transformSSE(deepseekStream: ReadableStream, model: string): ReadableSt
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check event: close
|
||||
if ((data as any)?.click_behavior !== undefined) {
|
||||
// close event — emit [DONE] if not already
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Stream error — emit what we have
|
||||
controller.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback close
|
||||
if (!emittedRole) {
|
||||
emittedRole = true;
|
||||
chunk({ role: "assistant", content: "" });
|
||||
@@ -203,8 +271,8 @@ async function collectSSEContent(deepseekStream: ReadableStream): Promise<string
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
const payload = line.slice(6).trim();
|
||||
if (!line.startsWith("data: ") && !line.startsWith("data:")) continue;
|
||||
const payload = line.replace(/^data:\s*/, "").trim();
|
||||
try {
|
||||
const data = JSON.parse(payload);
|
||||
const fragments = data?.v?.response?.fragments;
|
||||
@@ -213,6 +281,11 @@ async function collectSSEContent(deepseekStream: ReadableStream): Promise<string
|
||||
if (typeof frag.content === "string") parts.push(frag.content);
|
||||
}
|
||||
}
|
||||
if (data?.p === "response/fragments" && Array.isArray(data?.v)) {
|
||||
for (const frag of data.v) {
|
||||
if (typeof frag.content === "string") parts.push(frag.content);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
@@ -222,73 +295,105 @@ async function collectSSEContent(deepseekStream: ReadableStream): Promise<string
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
// ── DeepSeek API calls ──────────────────────────────────────────────────
|
||||
// ── DeepSeek API calls (Bearer token auth, like Chat2API) ───────────────
|
||||
|
||||
async function getBearerToken(
|
||||
cookies: string,
|
||||
signal?: AbortSignal,
|
||||
async function acquireAccessToken(
|
||||
userToken: string,
|
||||
signal?: AbortSignal | null,
|
||||
log?: ExecuteInput["log"]
|
||||
): Promise<string> {
|
||||
const resp = await fetch(`${DEEPSEEK_WEB_BASE}/api/v0/users/current`, {
|
||||
headers: { ...BASE_HEADERS, Cookie: cookies },
|
||||
signal,
|
||||
const cached = tokenCache.get(userToken);
|
||||
if (cached && cached.expiresAt > Math.floor(Date.now() / 1000)) {
|
||||
return cached.accessToken;
|
||||
}
|
||||
|
||||
log?.info?.("DEEPSEEK-WEB", "Acquiring access token from /users/current...");
|
||||
const resp = await fetch(`${DEEPSEEK_API_BASE}/v0/users/current`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${userToken}`,
|
||||
...FAKE_HEADERS,
|
||||
},
|
||||
signal: signal ?? undefined,
|
||||
});
|
||||
|
||||
if (resp.status === 401 || resp.status === 403) {
|
||||
throw new Error("Token invalid or expired — get a new userToken from DeepSeek localStorage");
|
||||
}
|
||||
if (!resp.ok) {
|
||||
throw new Error(`users/current HTTP ${resp.status}`);
|
||||
}
|
||||
|
||||
const json = await resp.json();
|
||||
const token = json?.data?.biz_data?.token;
|
||||
if (!token) {
|
||||
throw new Error(`No token in users/current response: code=${json?.code} msg=${json?.msg}`);
|
||||
const bizData = json?.data?.biz_data || json?.biz_data;
|
||||
if (!bizData?.token) {
|
||||
const errMsg = json?.msg || json?.data?.biz_msg || "Unknown error";
|
||||
throw new Error(`Failed to acquire token: ${errMsg}`);
|
||||
}
|
||||
log?.info?.("DEEPSEEK-WEB", `Got bearer token (${token.length} chars)`);
|
||||
return token;
|
||||
|
||||
const accessToken = bizData.token;
|
||||
evictOldest(tokenCache);
|
||||
tokenCache.set(userToken, {
|
||||
accessToken,
|
||||
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
||||
});
|
||||
|
||||
log?.info?.("DEEPSEEK-WEB", `Access token acquired (${accessToken.length} chars)`);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
async function createSession(
|
||||
token: string,
|
||||
cookies: string,
|
||||
signal?: AbortSignal
|
||||
accessToken: string,
|
||||
connectionId: string | undefined,
|
||||
signal?: AbortSignal | null
|
||||
): Promise<string> {
|
||||
const resp = await fetch(`${DEEPSEEK_WEB_BASE}/api/v0/chat_session/create`, {
|
||||
const cacheKey = connectionId || accessToken;
|
||||
const cached = sessionCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.createdAt < SESSION_CACHE_TTL_MS) {
|
||||
return cached.sessionId;
|
||||
}
|
||||
|
||||
const resp = await fetch(`${DEEPSEEK_API_BASE}/v0/chat_session/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
...FAKE_HEADERS,
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
Cookie: cookies,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Cookie: generateFakeCookie(),
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
signal,
|
||||
signal: signal ?? undefined,
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error(`chat_session/create HTTP ${resp.status}`);
|
||||
const json = await resp.json();
|
||||
const id = json?.data?.biz_data?.chat_session?.id;
|
||||
const bizData = json?.data?.biz_data || json?.biz_data;
|
||||
const id = bizData?.chat_session?.id;
|
||||
if (!id) throw new Error(`No session id: code=${json?.code}`);
|
||||
|
||||
evictOldest(sessionCache);
|
||||
sessionCache.set(cacheKey, { sessionId: id, createdAt: Date.now() });
|
||||
return id;
|
||||
}
|
||||
|
||||
async function getPowChallenge(
|
||||
token: string,
|
||||
cookies: string,
|
||||
signal?: AbortSignal
|
||||
accessToken: string,
|
||||
signal?: AbortSignal | null
|
||||
): Promise<PowChallenge> {
|
||||
const resp = await fetch(`${DEEPSEEK_WEB_BASE}/api/v0/chat/create_pow_challenge`, {
|
||||
const resp = await fetch(`${DEEPSEEK_API_BASE}/v0/chat/create_pow_challenge`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
...FAKE_HEADERS,
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
Cookie: cookies,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({ target_path: "/api/v0/chat/completion" }),
|
||||
signal,
|
||||
signal: signal ?? undefined,
|
||||
});
|
||||
if (!resp.ok) throw new Error(`create_pow_challenge HTTP ${resp.status}`);
|
||||
const json = await resp.json();
|
||||
const challenge = json?.data?.biz_data?.challenge;
|
||||
if (!challenge?.challenge) throw new Error(`No PoW challenge: code=${json?.code}`);
|
||||
return challenge as PowChallenge;
|
||||
const bizData = json?.data?.biz_data || json?.biz_data;
|
||||
if (!bizData?.challenge?.challenge) throw new Error(`No PoW challenge: code=${json?.code}`);
|
||||
return bizData.challenge as PowChallenge;
|
||||
}
|
||||
|
||||
// ── Executor ─────────────────────────────────────────────────────────────
|
||||
@@ -303,10 +408,10 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
||||
signal?: AbortSignal
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const cookies = String((credentials as any)?.cookies || "");
|
||||
if (!cookies.includes("ds_session_id=")) return false;
|
||||
const token = await getBearerToken(cookies, signal);
|
||||
return !!token;
|
||||
const userToken = extractUserToken(credentials);
|
||||
if (!userToken) return false;
|
||||
const accessToken = await acquireAccessToken(userToken, signal);
|
||||
return !!accessToken;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -320,32 +425,42 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
||||
}>;
|
||||
const rawCreds = credentials as unknown as Record<string, unknown>;
|
||||
|
||||
// 1. Validate credentials
|
||||
if (!validateCredentials(rawCreds)) {
|
||||
// 1. Extract userToken from credentials.apiKey
|
||||
const userToken = extractUserToken(rawCreds);
|
||||
if (!userToken) {
|
||||
return {
|
||||
response: errorResponse(400, "Invalid credentials: requires ds_session_id cookie"),
|
||||
response: errorResponse(
|
||||
400,
|
||||
"Invalid credentials: paste your userToken from DeepSeek localStorage " +
|
||||
"(DevTools → Application → Local Storage → chat.deepseek.com → userToken)"
|
||||
),
|
||||
url: COMPLETION_URL,
|
||||
headers: {},
|
||||
transformedBody: body,
|
||||
};
|
||||
}
|
||||
const cookies = rawCreds.cookies;
|
||||
|
||||
try {
|
||||
// 2. Get bearer token from session cookie
|
||||
log?.info?.("DEEPSEEK-WEB", "Getting bearer token...");
|
||||
const token = await getBearerToken(cookies, signal, log);
|
||||
// 2. Exchange userToken for short-lived access token (cached 1h)
|
||||
let t0 = Date.now();
|
||||
const accessToken = await acquireAccessToken(userToken, signal, log);
|
||||
log?.info?.("DEEPSEEK-WEB", `Token acquired in ${Date.now() - t0}ms`);
|
||||
|
||||
// 3. Create chat session
|
||||
log?.info?.("DEEPSEEK-WEB", "Creating chat session...");
|
||||
const sessionId = await createSession(token, cookies, signal);
|
||||
// 3. Create chat session (cached 5min)
|
||||
t0 = Date.now();
|
||||
const sessionId = await createSession(accessToken, rawCreds.connectionId as string, signal);
|
||||
log?.info?.("DEEPSEEK-WEB", `Session created in ${Date.now() - t0}ms`);
|
||||
|
||||
// 4. Get PoW challenge and solve
|
||||
log?.info?.("DEEPSEEK-WEB", "Getting PoW challenge...");
|
||||
const powChallenge = await getPowChallenge(token, cookies, signal);
|
||||
log?.info?.("DEEPSEEK-WEB", `Solving PoW (difficulty=${powChallenge.difficulty})...`);
|
||||
const powSolution = solvePow(powChallenge);
|
||||
log?.info?.("DEEPSEEK-WEB", `PoW solved: nonce=${powSolution.answer}`);
|
||||
t0 = Date.now();
|
||||
const powChallenge = await getPowChallenge(accessToken, signal);
|
||||
log?.info?.(
|
||||
"DEEPSEEK-WEB",
|
||||
`PoW challenge fetched in ${Date.now() - t0}ms (difficulty=${powChallenge.difficulty})`
|
||||
);
|
||||
t0 = Date.now();
|
||||
const powAnswer = await solvePow(powChallenge);
|
||||
log?.info?.("DEEPSEEK-WEB", `PoW solved in ${Date.now() - t0}ms`);
|
||||
|
||||
// 5. Build prompt from messages
|
||||
const prompt = messages
|
||||
@@ -356,12 +471,11 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
// 6. Map model and extract features from request body
|
||||
const { modelType, thinking } = mapModelToType(model as string);
|
||||
const thinkingEnabled =
|
||||
thinking || bodyObj.thinking_enabled === true || bodyObj.thinking === true;
|
||||
const searchEnabled =
|
||||
bodyObj.search_enabled === true || bodyObj.search === true || bodyObj.web_search === true;
|
||||
// 6. Resolve model type, thinking, and search from model name + body flags
|
||||
const { modelType, thinkingEnabled, searchEnabled } = resolveModelOptions(
|
||||
model as string,
|
||||
bodyObj
|
||||
);
|
||||
const refFileIds = Array.isArray(bodyObj.ref_file_ids) ? bodyObj.ref_file_ids : [];
|
||||
log?.info?.(
|
||||
"DEEPSEEK-WEB",
|
||||
@@ -370,18 +484,13 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
||||
|
||||
// 7. Send completion request
|
||||
const headers: Record<string, string> = {
|
||||
...BASE_HEADERS,
|
||||
...FAKE_HEADERS,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "*/*",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"x-ds-pow-response": Buffer.from(JSON.stringify(powSolution)).toString("base64"),
|
||||
"x-client-timezone-offset": String(new Date().getTimezoneOffset() * -60),
|
||||
Cookie: cookies,
|
||||
Referer: `${DEEPSEEK_WEB_BASE}/`,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"X-Ds-Pow-Response": powAnswer,
|
||||
"X-Client-Timezone-Offset": String(new Date().getTimezoneOffset() * -60),
|
||||
Cookie: generateFakeCookie(),
|
||||
};
|
||||
if (thinkingEnabled) {
|
||||
headers["x-thinking-enabled"] = "1";
|
||||
}
|
||||
|
||||
const requestPayload = {
|
||||
chat_session_id: sessionId,
|
||||
@@ -394,25 +503,31 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
||||
preempt: false,
|
||||
};
|
||||
|
||||
t0 = Date.now();
|
||||
log?.info?.("DEEPSEEK-WEB", `POST ${COMPLETION_URL}`);
|
||||
const resp = await fetch(COMPLETION_URL, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal,
|
||||
signal: signal ?? undefined,
|
||||
});
|
||||
|
||||
log?.info?.(
|
||||
"DEEPSEEK-WEB",
|
||||
`Completion response in ${Date.now() - t0}ms, status=${resp.status}`
|
||||
);
|
||||
|
||||
if (!resp.ok) {
|
||||
const status = resp.status;
|
||||
let errMsg = `DeepSeek API error (${status})`;
|
||||
if (status === 401 || status === 403) {
|
||||
errMsg = "DeepSeek session expired — re-paste your ds_session_id cookie.";
|
||||
tokenCache.delete(userToken);
|
||||
errMsg = "DeepSeek token expired — get a fresh userToken from localStorage.";
|
||||
} else if (status === 429) {
|
||||
errMsg = "DeepSeek rate limited. Wait and retry.";
|
||||
}
|
||||
log?.warn?.("DEEPSEEK-WEB", errMsg);
|
||||
|
||||
// Check for DeepSeek JSON error body
|
||||
try {
|
||||
const errBody = await resp.json();
|
||||
if (errBody?.code && errBody.code !== 0) {
|
||||
@@ -439,6 +554,7 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
||||
const errMsg = `DeepSeek error ${json.code}: ${json.msg}`;
|
||||
log?.warn?.("DEEPSEEK-WEB", errMsg);
|
||||
const status = json.code === 40003 ? 401 : json.code === 40002 ? 429 : 502;
|
||||
if (json.code === 40003) tokenCache.delete(userToken);
|
||||
return {
|
||||
response: errorResponse(status, errMsg, json.code),
|
||||
url: COMPLETION_URL,
|
||||
@@ -446,7 +562,6 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
||||
transformedBody: requestPayload,
|
||||
};
|
||||
}
|
||||
// Valid JSON response (shouldn't happen for streaming, but handle it)
|
||||
return {
|
||||
response: new Response(JSON.stringify(json), {
|
||||
status: 200,
|
||||
@@ -524,3 +639,6 @@ export class DeepSeekWebExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
export const deepseekWebExecutor = new DeepSeekWebExecutor();
|
||||
|
||||
// Re-export for auto-refresh executor and tests
|
||||
export { acquireAccessToken, tokenCache, sessionCache };
|
||||
|
||||
@@ -1,12 +1,111 @@
|
||||
// DeepSeek PoW Solver - loads exact implementation from extracted worker module
|
||||
// The Keccak sponge has non-standard byte packing that's difficult to replicate exactly,
|
||||
// so we use the verified extracted module.
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// ── WASM solver (fast — ~50-100ms at difficulty 144000) ──────────────────
|
||||
|
||||
class DeepSeekHashWasm {
|
||||
private wasmInstance: any;
|
||||
private offset = 0;
|
||||
private cachedUint8Memory: Uint8Array | null = null;
|
||||
private cachedTextEncoder = new TextEncoder();
|
||||
|
||||
private getCachedUint8Memory(): Uint8Array {
|
||||
if (!this.cachedUint8Memory?.byteLength) {
|
||||
this.cachedUint8Memory = new Uint8Array(this.wasmInstance.memory.buffer);
|
||||
}
|
||||
return this.cachedUint8Memory;
|
||||
}
|
||||
|
||||
private encodeString(
|
||||
text: string,
|
||||
allocate: (size: number, align: number) => number,
|
||||
reallocate: (ptr: number, oldSize: number, newSize: number, align: number) => number
|
||||
): number {
|
||||
const strLength = text.length;
|
||||
let ptr = allocate(strLength, 1) >>> 0;
|
||||
const memory = this.getCachedUint8Memory();
|
||||
let asciiLength = 0;
|
||||
|
||||
for (; asciiLength < strLength; asciiLength++) {
|
||||
if (text.charCodeAt(asciiLength) > 127) break;
|
||||
memory[ptr + asciiLength] = text.charCodeAt(asciiLength);
|
||||
}
|
||||
|
||||
if (asciiLength !== strLength) {
|
||||
if (asciiLength > 0) text = text.slice(asciiLength);
|
||||
ptr = reallocate(ptr, strLength, asciiLength + text.length * 3, 1) >>> 0;
|
||||
const result = this.cachedTextEncoder.encodeInto(
|
||||
text,
|
||||
this.getCachedUint8Memory().subarray(ptr + asciiLength, ptr + asciiLength + text.length * 3)
|
||||
);
|
||||
asciiLength += result.written!;
|
||||
ptr = reallocate(ptr, asciiLength + text.length * 3, asciiLength, 1) >>> 0;
|
||||
}
|
||||
|
||||
this.offset = asciiLength;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
calculateHash(challenge: string, prefix: string, difficulty: number): number | undefined {
|
||||
try {
|
||||
const retptr = this.wasmInstance.__wbindgen_add_to_stack_pointer(-16);
|
||||
|
||||
const ptr0 = this.encodeString(
|
||||
challenge,
|
||||
this.wasmInstance.__wbindgen_export_0,
|
||||
this.wasmInstance.__wbindgen_export_1
|
||||
);
|
||||
const len0 = this.offset;
|
||||
|
||||
const ptr1 = this.encodeString(
|
||||
prefix,
|
||||
this.wasmInstance.__wbindgen_export_0,
|
||||
this.wasmInstance.__wbindgen_export_1
|
||||
);
|
||||
const len1 = this.offset;
|
||||
|
||||
this.wasmInstance.wasm_solve(retptr, ptr0, len0, ptr1, len1, difficulty);
|
||||
|
||||
const dv = new DataView(this.wasmInstance.memory.buffer);
|
||||
const status = dv.getInt32(retptr + 0, true);
|
||||
const value = dv.getFloat64(retptr + 8, true);
|
||||
|
||||
return status === 0 ? undefined : value;
|
||||
} finally {
|
||||
this.wasmInstance.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
async init(wasmPath: string): Promise<void> {
|
||||
const wasmBuffer = await fs.promises.readFile(wasmPath);
|
||||
const { instance } = await WebAssembly.instantiate(wasmBuffer, { wbg: {} });
|
||||
this.wasmInstance = instance.exports;
|
||||
}
|
||||
}
|
||||
|
||||
let _wasmSolver: DeepSeekHashWasm | null = null;
|
||||
let _wasmInitFailed = false;
|
||||
|
||||
async function getWasmSolver(): Promise<DeepSeekHashWasm | null> {
|
||||
if (_wasmInitFailed) return null;
|
||||
if (_wasmSolver) return _wasmSolver;
|
||||
|
||||
try {
|
||||
const solver = new DeepSeekHashWasm();
|
||||
const wasmPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "sha3_wasm_bg.wasm");
|
||||
await solver.init(wasmPath);
|
||||
_wasmSolver = solver;
|
||||
return solver;
|
||||
} catch {
|
||||
_wasmInitFailed = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── JS fallback solver (slow — ~5-6s at difficulty 144000) ───────────────
|
||||
|
||||
// Load the exact solver extracted from DeepSeek's worker chunk.
|
||||
// Lazy-loaded inside the function so the standalone Next build can collect
|
||||
// page data without executing a dynamic require() at module-load time.
|
||||
const require = createRequire(import.meta.url);
|
||||
let _U: any | undefined;
|
||||
function loadU(): any {
|
||||
@@ -16,16 +115,7 @@ function loadU(): any {
|
||||
return _U;
|
||||
}
|
||||
|
||||
export function solveDeepSeekPow(
|
||||
algorithm: string,
|
||||
challenge: string,
|
||||
salt: string,
|
||||
difficulty: number,
|
||||
expireAt: number
|
||||
): number {
|
||||
if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`);
|
||||
const prefix = `${salt}_${expireAt}_`;
|
||||
|
||||
function solveWithJS(challenge: string, prefix: string, difficulty: number): number {
|
||||
const U = loadU();
|
||||
const createHash = () => {
|
||||
const self: any = {};
|
||||
@@ -62,3 +152,38 @@ export function solveDeepSeekPow(
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
export async function solveDeepSeekPowAsync(
|
||||
algorithm: string,
|
||||
challenge: string,
|
||||
salt: string,
|
||||
difficulty: number,
|
||||
expireAt: number
|
||||
): Promise<number> {
|
||||
if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`);
|
||||
const prefix = `${salt}_${expireAt}_`;
|
||||
|
||||
const wasm = await getWasmSolver();
|
||||
if (wasm) {
|
||||
const answer = wasm.calculateHash(challenge, prefix, difficulty);
|
||||
if (answer === undefined) return -1;
|
||||
return answer;
|
||||
}
|
||||
|
||||
return solveWithJS(challenge, prefix, difficulty);
|
||||
}
|
||||
|
||||
// Sync wrapper kept for backward compat (uses JS fallback only)
|
||||
export function solveDeepSeekPow(
|
||||
algorithm: string,
|
||||
challenge: string,
|
||||
salt: string,
|
||||
difficulty: number,
|
||||
expireAt: number
|
||||
): number {
|
||||
if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`);
|
||||
const prefix = `${salt}_${expireAt}_`;
|
||||
return solveWithJS(challenge, prefix, difficulty);
|
||||
}
|
||||
|
||||
BIN
open-sse/lib/sha3_wasm_bg.wasm
Normal file
BIN
open-sse/lib/sha3_wasm_bg.wasm
Normal file
Binary file not shown.
@@ -6856,6 +6856,7 @@ function AddApiKeyModal({
|
||||
const isPerplexityWeb = provider === "perplexity-web";
|
||||
const isBlackboxWeb = provider === "blackbox-web";
|
||||
const isMuseSparkWeb = provider === "muse-spark-web";
|
||||
const isDeepSeekWeb = provider === "deepseek-web";
|
||||
const isWebSessionProvider = isGrokWeb || isPerplexityWeb || isBlackboxWeb || isMuseSparkWeb;
|
||||
const apiKeyOptional = providerAllowsOptionalApiKey(provider);
|
||||
const commandCodeAuthPhaseLabel = commandCodeAuthState
|
||||
@@ -6908,43 +6909,49 @@ function AddApiKeyModal({
|
||||
const [bulkWarnings, setBulkWarnings] = useState<string[]>([]);
|
||||
const apiCredentialLabel = isQoder
|
||||
? t("personalAccessTokenLabel")
|
||||
: isWebSessionProvider
|
||||
? t("sessionCookieLabel")
|
||||
: apiKeyOptional
|
||||
? `${t("apiKeyLabel")} (${t("optional").toLowerCase()})`
|
||||
: t("apiKeyLabel");
|
||||
: isDeepSeekWeb
|
||||
? "User Token"
|
||||
: isWebSessionProvider
|
||||
? t("sessionCookieLabel")
|
||||
: apiKeyOptional
|
||||
? `${t("apiKeyLabel")} (${t("optional").toLowerCase()})`
|
||||
: t("apiKeyLabel");
|
||||
const apiCredentialPlaceholder = isVertex
|
||||
? t("vertexServiceAccountPlaceholder")
|
||||
: isGrokWeb
|
||||
? t("grokWebCookiePlaceholder")
|
||||
: isPerplexityWeb
|
||||
? t("perplexityWebCookiePlaceholder")
|
||||
: isBlackboxWeb
|
||||
? t("blackboxWebCookiePlaceholder")
|
||||
: isMuseSparkWeb
|
||||
? t("museSparkWebCookiePlaceholder")
|
||||
: isQoder
|
||||
? t("qoderPatPlaceholder")
|
||||
: apiKeyOptional
|
||||
? t("optional")
|
||||
: undefined;
|
||||
: isDeepSeekWeb
|
||||
? "Paste userToken value from localStorage"
|
||||
: isGrokWeb
|
||||
? t("grokWebCookiePlaceholder")
|
||||
: isPerplexityWeb
|
||||
? t("perplexityWebCookiePlaceholder")
|
||||
: isBlackboxWeb
|
||||
? t("blackboxWebCookiePlaceholder")
|
||||
: isMuseSparkWeb
|
||||
? t("museSparkWebCookiePlaceholder")
|
||||
: isQoder
|
||||
? t("qoderPatPlaceholder")
|
||||
: apiKeyOptional
|
||||
? t("optional")
|
||||
: undefined;
|
||||
const apiCredentialHint = isQoder
|
||||
? t("qoderPatHint")
|
||||
: isGrokWeb
|
||||
? t("grokWebCookieHint")
|
||||
: isPerplexityWeb
|
||||
? t("perplexityWebCookieHint")
|
||||
: isBlackboxWeb
|
||||
? t("blackboxWebCookieHint")
|
||||
: isMuseSparkWeb
|
||||
? t("museSparkWebCookieHint")
|
||||
: isLocalSelfHostedProvider
|
||||
? t("localProviderApiKeyOptionalHint", {
|
||||
provider: localProviderMetadata?.name || providerName || provider || "",
|
||||
})
|
||||
: apiKeyOptional
|
||||
? t("apiKeyOptionalHint")
|
||||
: undefined;
|
||||
: isDeepSeekWeb
|
||||
? "Found in browser DevTools → Application → Local Storage → chat.deepseek.com → userToken"
|
||||
: isGrokWeb
|
||||
? t("grokWebCookieHint")
|
||||
: isPerplexityWeb
|
||||
? t("perplexityWebCookieHint")
|
||||
: isBlackboxWeb
|
||||
? t("blackboxWebCookieHint")
|
||||
: isMuseSparkWeb
|
||||
? t("museSparkWebCookieHint")
|
||||
: isLocalSelfHostedProvider
|
||||
? t("localProviderApiKeyOptionalHint", {
|
||||
provider: localProviderMetadata?.name || providerName || provider || "",
|
||||
})
|
||||
: apiKeyOptional
|
||||
? t("apiKeyOptionalHint")
|
||||
: undefined;
|
||||
|
||||
const handleValidate = async () => {
|
||||
setValidating(true);
|
||||
|
||||
@@ -18,7 +18,13 @@ import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
const logger = pino({ name: "cloud-agents-api" });
|
||||
|
||||
createCloudAgentTaskTable();
|
||||
let _tableInit = false;
|
||||
function ensureTable() {
|
||||
if (!_tableInit) {
|
||||
createCloudAgentTaskTable();
|
||||
_tableInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS(request: NextRequest) {
|
||||
return new NextResponse(null, { headers: getCloudAgentCorsHeaders(request) });
|
||||
@@ -52,6 +58,7 @@ function cloudAgentCredentialsRequiredResponse(providerId: string, request: Next
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
ensureTable();
|
||||
const authError = await requireCloudAgentManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
@@ -110,6 +117,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
ensureTable();
|
||||
const authError = await requireCloudAgentManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
@@ -192,6 +200,7 @@ export async function DELETE(
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
ensureTable();
|
||||
const authError = await requireCloudAgentManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
|
||||
@@ -2643,6 +2643,58 @@ function buildMetaAiValidationBody() {
|
||||
};
|
||||
}
|
||||
|
||||
async function validateDeepSeekWebProvider({ apiKey }: any) {
|
||||
if (!apiKey) {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
"Missing userToken — paste the value from DevTools → Application → Local Storage → chat.deepseek.com → userToken",
|
||||
};
|
||||
}
|
||||
let token = apiKey;
|
||||
try {
|
||||
const parsed = JSON.parse(token);
|
||||
if (typeof parsed?.value === "string") token = parsed.value;
|
||||
} catch {
|
||||
// not JSON, use as-is
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch("https://chat.deepseek.com/api/v0/users/current", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: "*/*",
|
||||
Origin: "https://chat.deepseek.com",
|
||||
Referer: "https://chat.deepseek.com/",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
"X-App-Version": "20241129.1",
|
||||
"X-Client-Platform": "web",
|
||||
},
|
||||
});
|
||||
if (resp.status === 401 || resp.status === 403) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "userToken is invalid or expired — get a fresh one from localStorage",
|
||||
};
|
||||
}
|
||||
if (!resp.ok) {
|
||||
return { valid: false, error: `DeepSeek returned HTTP ${resp.status}` };
|
||||
}
|
||||
const json = await resp.json();
|
||||
const bizData = json?.data?.biz_data || json?.biz_data;
|
||||
if (!bizData?.token) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `DeepSeek did not return an access token: ${json?.msg || "unknown error"}`,
|
||||
};
|
||||
}
|
||||
return { valid: true, error: null };
|
||||
} catch (error: any) {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
try {
|
||||
const token = extractCookieValue(apiKey, "sso");
|
||||
@@ -3252,6 +3304,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
runwayml: validateRunwayProvider,
|
||||
snowflake: validateSnowflakeProvider,
|
||||
gigachat: validateGigachatProvider,
|
||||
"deepseek-web": validateDeepSeekWebProvider,
|
||||
"grok-web": validateGrokWebProvider,
|
||||
"chatgpt-web": validateChatGptWebProvider,
|
||||
"perplexity-web": validatePerplexityWebProvider,
|
||||
|
||||
@@ -224,7 +224,8 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
color: "#4D6BFE",
|
||||
textIcon: "DS",
|
||||
website: "https://chat.deepseek.com",
|
||||
authHint: "Paste your ds_session_id cookie from chat.deepseek.com",
|
||||
authHint:
|
||||
"Paste your userToken from chat.deepseek.com — DevTools → Application → Local Storage → userToken",
|
||||
},
|
||||
"copilot-web": {
|
||||
id: "copilot-web",
|
||||
|
||||
@@ -32,21 +32,7 @@ test("provider name is deepseek-web", () => {
|
||||
|
||||
// ─── Credential validation ───────────────────────────────────────────────
|
||||
|
||||
test("execute returns 400 without ds_session_id cookie", async () => {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "foo=bar" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
assert.equal(result.response.status, 400);
|
||||
const text = await result.response.text();
|
||||
assert.ok(text.includes("ds_session_id"));
|
||||
});
|
||||
|
||||
test("execute returns 400 with empty credentials", async () => {
|
||||
test("execute returns 400 without apiKey (userToken)", async () => {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "default",
|
||||
@@ -56,6 +42,20 @@ test("execute returns 400 with empty credentials", async () => {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
assert.equal(result.response.status, 400);
|
||||
const text = await result.response.text();
|
||||
assert.ok(text.includes("userToken"));
|
||||
});
|
||||
|
||||
test("execute returns 400 with empty apiKey", async () => {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: "" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
assert.equal(result.response.status, 400);
|
||||
});
|
||||
|
||||
// ─── Test connection ─────────────────────────────────────────────────────
|
||||
@@ -65,33 +65,31 @@ test("testConnection returns false with empty credentials", async () => {
|
||||
assert.equal(await executor.testConnection({}), false);
|
||||
});
|
||||
|
||||
test("testConnection returns false without ds_session_id", async () => {
|
||||
test("testConnection returns false without apiKey", async () => {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
assert.equal(await executor.testConnection({ cookies: "foo=bar" }), false);
|
||||
assert.equal(await executor.testConnection({ apiKey: "" }), false);
|
||||
});
|
||||
|
||||
// ─── API flow (mocked) ──────────────────────────────────────────────────
|
||||
|
||||
function mockDeepSeekFlow() {
|
||||
async function mockDeepSeekFlow() {
|
||||
const original = globalThis.fetch;
|
||||
const calls = [];
|
||||
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
calls.push({ url: urlStr, method: opts?.method, body: opts?.body });
|
||||
calls.push({ url: urlStr, method: opts?.method, body: opts?.body, headers: opts?.headers });
|
||||
|
||||
// /users/current → return token
|
||||
if (urlStr.includes("/users/current")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: { biz_data: { token: "test-bearer-token-123", email: "test@test.com" } },
|
||||
data: { biz_data: { token: "test-access-token-123", email: "test@test.com" } },
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
// /chat_session/create → return session id
|
||||
if (urlStr.includes("/chat_session/create")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
@@ -102,7 +100,6 @@ function mockDeepSeekFlow() {
|
||||
);
|
||||
}
|
||||
|
||||
// /create_pow_challenge → return challenge
|
||||
if (urlStr.includes("/create_pow_challenge")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
@@ -126,7 +123,6 @@ function mockDeepSeekFlow() {
|
||||
);
|
||||
}
|
||||
|
||||
// /chat/completion → return SSE stream
|
||||
if (urlStr.includes("/chat/completion")) {
|
||||
const encoder = new TextEncoder();
|
||||
const sse = [
|
||||
@@ -149,30 +145,36 @@ function mockDeepSeekFlow() {
|
||||
return new Response("Not found", { status: 404 });
|
||||
};
|
||||
|
||||
// Clear token/session caches between tests
|
||||
const dsMod = await import("../../open-sse/executors/deepseek-web.ts");
|
||||
if (dsMod.tokenCache) dsMod.tokenCache.clear();
|
||||
if (dsMod.sessionCache) dsMod.sessionCache.clear();
|
||||
|
||||
return {
|
||||
calls,
|
||||
restore: () => {
|
||||
globalThis.fetch = original;
|
||||
if (dsMod.tokenCache) dsMod.tokenCache.clear();
|
||||
if (dsMod.sessionCache) dsMod.sessionCache.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("execute: full flow with mocked API (streaming)", async () => {
|
||||
const mock = mockDeepSeekFlow();
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "Say hello" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=test-session-id-1234" },
|
||||
credentials: { apiKey: "test-user-token-1234" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
assert.ok(result.response.ok);
|
||||
assert.equal(result.response.headers.get("content-type"), "text/event-stream");
|
||||
|
||||
// Read SSE stream
|
||||
const text = await result.response.text();
|
||||
assert.ok(text.includes('"content":"Hello"'), "Should contain Hello");
|
||||
assert.ok(text.includes('"finish_reason":"stop"'), "Should have stop");
|
||||
@@ -196,8 +198,19 @@ test("execute: full flow with mocked API (streaming)", async () => {
|
||||
"Called completion"
|
||||
);
|
||||
|
||||
// Verify completion request had Bearer token
|
||||
// Verify /users/current used Bearer auth (userToken)
|
||||
const usersCall = mock.calls.find((c) => c.url.includes("/users/current"));
|
||||
assert.ok(
|
||||
usersCall.headers?.Authorization === "Bearer test-user-token-1234",
|
||||
"Should use userToken as Bearer for /users/current"
|
||||
);
|
||||
|
||||
// Verify completion used the access token (not the userToken)
|
||||
const compCall = mock.calls.find((c) => c.url.includes("/chat/completion"));
|
||||
assert.ok(
|
||||
compCall.headers?.Authorization === "Bearer test-access-token-123",
|
||||
"Should use access token for /completion"
|
||||
);
|
||||
const body = JSON.parse(compCall.body);
|
||||
assert.equal(body.chat_session_id, "session-abc-123");
|
||||
assert.equal(body.prompt, "Say hello");
|
||||
@@ -207,14 +220,14 @@ test("execute: full flow with mocked API (streaming)", async () => {
|
||||
});
|
||||
|
||||
test("execute: full flow with mocked API (non-streaming)", async () => {
|
||||
const mock = mockDeepSeekFlow();
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { cookies: "ds_session_id=abc123" },
|
||||
credentials: { apiKey: "test-user-token-ns" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
@@ -230,70 +243,27 @@ test("execute: full flow with mocked API (non-streaming)", async () => {
|
||||
});
|
||||
|
||||
test("execute: sends PoW response header", async () => {
|
||||
const original = globalThis.fetch;
|
||||
const capturedHeaders = {};
|
||||
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
if (urlStr.includes("/users/current")) {
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "tok" } } }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (urlStr.includes("/chat_session/create")) {
|
||||
return new Response(
|
||||
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
if (urlStr.includes("/create_pow_challenge")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
biz_data: {
|
||||
challenge: {
|
||||
algorithm: "DeepSeekHashV1",
|
||||
challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64",
|
||||
salt: "bb",
|
||||
signature: "s",
|
||||
difficulty: 1000,
|
||||
expire_at: 1778891543095,
|
||||
expire_after: 300000,
|
||||
target_path: "/api/v0/chat/completion",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
if (urlStr.includes("/chat/completion")) {
|
||||
Object.assign(capturedHeaders, opts.headers);
|
||||
const encoder = new TextEncoder();
|
||||
return new Response(encoder.encode("data: [DONE]\n\n"), {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response("", { status: 404 });
|
||||
};
|
||||
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
await executor.execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=x" },
|
||||
credentials: { apiKey: "test-user-token-pow" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
assert.ok(capturedHeaders["Authorization"]?.startsWith("Bearer tok"), "Has Bearer token");
|
||||
assert.ok(capturedHeaders["x-ds-pow-response"], "Has PoW header");
|
||||
assert.ok(capturedHeaders["x-app-version"] === "2.0.0", "Has x-app-version");
|
||||
assert.ok(capturedHeaders["x-client-platform"] === "web", "Has x-client-platform");
|
||||
const compCall = mock.calls.find((c) => c.url.includes("/chat/completion"));
|
||||
assert.ok(
|
||||
compCall.headers["Authorization"]?.startsWith("Bearer test-access-token"),
|
||||
"Has Bearer token"
|
||||
);
|
||||
assert.ok(compCall.headers["X-Ds-Pow-Response"], "Has PoW header");
|
||||
assert.ok(compCall.headers["X-App-Version"], "Has X-App-Version");
|
||||
assert.ok(compCall.headers["X-Client-Platform"] === "web", "Has X-Client-Platform");
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -307,23 +277,29 @@ test("execute: handles API error (token fetch fails)", async () => {
|
||||
}
|
||||
return new Response("", { status: 404 });
|
||||
};
|
||||
const dsMod1 = await import("../../open-sse/executors/deepseek-web.ts");
|
||||
if (dsMod1.tokenCache) dsMod1.tokenCache.clear();
|
||||
try {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=abc" },
|
||||
credentials: { apiKey: "test-bad-token" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.ok(result.response.status >= 400, "Should return error status");
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
if (dsMod1.tokenCache) dsMod1.tokenCache.clear();
|
||||
}
|
||||
});
|
||||
|
||||
test("execute: handles 401 from DeepSeek", async () => {
|
||||
const original = globalThis.fetch;
|
||||
const dsMod2 = await import("../../open-sse/executors/deepseek-web.ts");
|
||||
if (dsMod2.tokenCache) dsMod2.tokenCache.clear();
|
||||
if (dsMod2.sessionCache) dsMod2.sessionCache.clear();
|
||||
globalThis.fetch = async (url) => {
|
||||
if (url.includes("/users/current")) {
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "tok" } } }), {
|
||||
@@ -369,17 +345,22 @@ test("execute: handles 401 from DeepSeek", async () => {
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=abc" },
|
||||
credentials: { apiKey: "test-expired-token" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.equal(result.response.status, 401);
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
if (dsMod2.tokenCache) dsMod2.tokenCache.clear();
|
||||
if (dsMod2.sessionCache) dsMod2.sessionCache.clear();
|
||||
}
|
||||
});
|
||||
|
||||
test("execute: handles DeepSeek JSON error (40003 INVALID_TOKEN)", async () => {
|
||||
const original = globalThis.fetch;
|
||||
const dsMod3 = await import("../../open-sse/executors/deepseek-web.ts");
|
||||
if (dsMod3.tokenCache) dsMod3.tokenCache.clear();
|
||||
if (dsMod3.sessionCache) dsMod3.sessionCache.clear();
|
||||
globalThis.fetch = async (url) => {
|
||||
if (url.includes("/users/current")) {
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "tok" } } }), {
|
||||
@@ -428,7 +409,7 @@ test("execute: handles DeepSeek JSON error (40003 INVALID_TOKEN)", async () => {
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=abc" },
|
||||
credentials: { apiKey: "test-invalid-token" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.equal(result.response.status, 401);
|
||||
@@ -436,66 +417,37 @@ test("execute: handles DeepSeek JSON error (40003 INVALID_TOKEN)", async () => {
|
||||
assert.ok(text.includes("40003"));
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
if (dsMod3.tokenCache) dsMod3.tokenCache.clear();
|
||||
if (dsMod3.sessionCache) dsMod3.sessionCache.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Model mapping ───────────────────────────────────────────────────────
|
||||
|
||||
test("execute: maps model to deepseek_r1 with thinking", async () => {
|
||||
const original = globalThis.fetch;
|
||||
let capturedBody = null;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
if (url.includes("/users/current"))
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (url.includes("/chat_session/create"))
|
||||
return new Response(
|
||||
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s" } } } }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/create_pow_challenge"))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
biz_data: {
|
||||
challenge: {
|
||||
algorithm: "DeepSeekHashV1",
|
||||
challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64",
|
||||
salt: "bb",
|
||||
signature: "s",
|
||||
difficulty: 1000,
|
||||
expire_at: 1778891543095,
|
||||
expire_after: 300000,
|
||||
target_path: "/api/v0/chat/completion",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
const enc = new TextEncoder();
|
||||
return new Response(enc.encode("data: [DONE]\n\n"), {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response("", { status: 404 });
|
||||
};
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
let capturedBody = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
const resp = await origFetch(url, opts);
|
||||
if (url.toString().includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
await new DeepSeekWebExecutor().execute({
|
||||
model: "deepseek-r1",
|
||||
body: { messages: [{ role: "user", content: "think" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=x" },
|
||||
credentials: { apiKey: "test-user-token-r1" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.equal(capturedBody.model_type, "deepseek_r1");
|
||||
assert.equal(capturedBody.model_type, "default");
|
||||
assert.equal(capturedBody.thinking_enabled, true);
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -514,6 +466,8 @@ test("isSessionValid starts false", () => {
|
||||
// ─── Abort handling ──────────────────────────────────────────────────────
|
||||
|
||||
test("execute: handles abort signal gracefully", async () => {
|
||||
const dsMod4 = await import("../../open-sse/executors/deepseek-web.ts");
|
||||
if (dsMod4.tokenCache) dsMod4.tokenCache.clear();
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
@@ -521,233 +475,113 @@ test("execute: handles abort signal gracefully", async () => {
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=test" },
|
||||
credentials: { apiKey: "test-token-abort" },
|
||||
signal: controller.signal,
|
||||
});
|
||||
assert.ok(result.response, "Should return response");
|
||||
assert.ok(result.response.status >= 400, "Should indicate error");
|
||||
assert.ok(
|
||||
result.response.status >= 400 || result.response.status === 499,
|
||||
"Should indicate error or abort"
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Search enabled ──────────────────────────────────────────────────────
|
||||
|
||||
test("execute: passes search_enabled from body", async () => {
|
||||
const original = globalThis.fetch;
|
||||
let capturedBody = null;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
if (url.includes("/users/current"))
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (url.includes("/chat_session/create"))
|
||||
return new Response(
|
||||
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/create_pow_challenge"))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
biz_data: {
|
||||
challenge: {
|
||||
algorithm: "DeepSeekHashV1",
|
||||
challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64",
|
||||
salt: "bb",
|
||||
signature: "s",
|
||||
difficulty: 1000,
|
||||
expire_at: 1778891543095,
|
||||
expire_after: 300000,
|
||||
target_path: "/api/v0/chat/completion",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
return new Response(new TextEncoder().encode("data: [DONE]\n\n"), {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response("", { status: 404 });
|
||||
};
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
let capturedBody = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
const resp = await origFetch(url, opts);
|
||||
if (url.toString().includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
await new DeepSeekWebExecutor().execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }], search_enabled: true },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=x" },
|
||||
credentials: { apiKey: "test-token-search" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.equal(capturedBody.search_enabled, true);
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("execute: search_enabled defaults to false", async () => {
|
||||
const original = globalThis.fetch;
|
||||
let capturedBody = null;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
if (url.includes("/users/current"))
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (url.includes("/chat_session/create"))
|
||||
return new Response(
|
||||
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/create_pow_challenge"))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
biz_data: {
|
||||
challenge: {
|
||||
algorithm: "DeepSeekHashV1",
|
||||
challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64",
|
||||
salt: "bb",
|
||||
signature: "s",
|
||||
difficulty: 1000,
|
||||
expire_at: 1778891543095,
|
||||
expire_after: 300000,
|
||||
target_path: "/api/v0/chat/completion",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
return new Response(new TextEncoder().encode("data: [DONE]\n\n"), {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response("", { status: 404 });
|
||||
};
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
let capturedBody = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
const resp = await origFetch(url, opts);
|
||||
if (url.toString().includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
await new DeepSeekWebExecutor().execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=x" },
|
||||
credentials: { apiKey: "test-token-nosearch" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.equal(capturedBody.search_enabled, false);
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Thinking enabled via body ───────────────────────────────────────────
|
||||
|
||||
test("execute: thinking_enabled from body overrides model mapping", async () => {
|
||||
const original = globalThis.fetch;
|
||||
let capturedBody = null;
|
||||
let capturedHeaders = null;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
if (url.includes("/users/current"))
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (url.includes("/chat_session/create"))
|
||||
return new Response(
|
||||
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/create_pow_challenge"))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
biz_data: {
|
||||
challenge: {
|
||||
algorithm: "DeepSeekHashV1",
|
||||
challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64",
|
||||
salt: "bb",
|
||||
signature: "s",
|
||||
difficulty: 1000,
|
||||
expire_at: 1778891543095,
|
||||
expire_after: 300000,
|
||||
target_path: "/api/v0/chat/completion",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
capturedHeaders = opts.headers;
|
||||
return new Response(new TextEncoder().encode("data: [DONE]\n\n"), {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response("", { status: 404 });
|
||||
};
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
let capturedBody = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
const resp = await origFetch(url, opts);
|
||||
if (url.toString().includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
await new DeepSeekWebExecutor().execute({
|
||||
model: "default", // not r1/expert
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "think" }], thinking_enabled: true },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=x" },
|
||||
credentials: { apiKey: "test-token-think" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.equal(capturedBody.thinking_enabled, true);
|
||||
assert.equal(capturedHeaders["x-thinking-enabled"], "1");
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── File IDs ────────────────────────────────────────────────────────────
|
||||
|
||||
test("execute: passes ref_file_ids from body", async () => {
|
||||
const original = globalThis.fetch;
|
||||
let capturedBody = null;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
if (url.includes("/users/current"))
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (url.includes("/chat_session/create"))
|
||||
return new Response(
|
||||
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/create_pow_challenge"))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
biz_data: {
|
||||
challenge: {
|
||||
algorithm: "DeepSeekHashV1",
|
||||
challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64",
|
||||
salt: "bb",
|
||||
signature: "s",
|
||||
difficulty: 1000,
|
||||
expire_at: 1778891543095,
|
||||
expire_after: 300000,
|
||||
target_path: "/api/v0/chat/completion",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
return new Response(new TextEncoder().encode("data: [DONE]\n\n"), {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response("", { status: 404 });
|
||||
};
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
let capturedBody = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
const resp = await origFetch(url, opts);
|
||||
if (url.toString().includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
await new DeepSeekWebExecutor().execute({
|
||||
model: "default",
|
||||
body: {
|
||||
@@ -755,70 +589,66 @@ test("execute: passes ref_file_ids from body", async () => {
|
||||
ref_file_ids: ["file-abc-123", "file-def-456"],
|
||||
},
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=x" },
|
||||
credentials: { apiKey: "test-token-files" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.deepEqual(capturedBody.ref_file_ids, ["file-abc-123", "file-def-456"]);
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Expert model ────────────────────────────────────────────────────────
|
||||
|
||||
test("execute: maps expert model with thinking", async () => {
|
||||
const original = globalThis.fetch;
|
||||
let capturedBody = null;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
if (url.includes("/users/current"))
|
||||
return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (url.includes("/chat_session/create"))
|
||||
return new Response(
|
||||
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/create_pow_challenge"))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
biz_data: {
|
||||
challenge: {
|
||||
algorithm: "DeepSeekHashV1",
|
||||
challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64",
|
||||
salt: "bb",
|
||||
signature: "s",
|
||||
difficulty: 1000,
|
||||
expire_at: 1778891543095,
|
||||
expire_after: 300000,
|
||||
target_path: "/api/v0/chat/completion",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
if (url.includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
return new Response(new TextEncoder().encode("data: [DONE]\n\n"), {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response("", { status: 404 });
|
||||
};
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
let capturedBody = null;
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
const resp = await origFetch(url, opts);
|
||||
if (url.toString().includes("/chat/completion")) {
|
||||
capturedBody = JSON.parse(opts.body);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
await new DeepSeekWebExecutor().execute({
|
||||
model: "expert",
|
||||
body: { messages: [{ role: "user", content: "deep think" }] },
|
||||
stream: true,
|
||||
credentials: { cookies: "ds_session_id=x" },
|
||||
credentials: { apiKey: "test-token-expert" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
assert.equal(capturedBody.model_type, "expert");
|
||||
assert.equal(capturedBody.thinking_enabled, true);
|
||||
assert.equal(capturedBody.thinking_enabled, false);
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── JSON-wrapped userToken ──────────────────────────────────────────────
|
||||
|
||||
test("execute: handles JSON-wrapped userToken", async () => {
|
||||
const mock = await mockDeepSeekFlow();
|
||||
try {
|
||||
const executor = new DeepSeekWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: JSON.stringify({ value: "test-json-wrapped-token" }) },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
assert.ok(result.response.ok, "Should succeed with JSON-wrapped token");
|
||||
|
||||
const usersCall = mock.calls.find((c) => c.url.includes("/users/current"));
|
||||
assert.ok(
|
||||
usersCall.headers?.Authorization === "Bearer test-json-wrapped-token",
|
||||
"Should unwrap JSON and use inner value"
|
||||
);
|
||||
} finally {
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user