Files
openmaic/OpenMAIC/lib/web-search/minimax.ts
2026-08-16 14:58:47 +08:00

142 lines
4.2 KiB
TypeScript

/**
* MiniMax Web Search integration.
*
* Uses the Token Plan HTTP endpoint behind the MiniMax MCP web_search tool:
* POST https://api.minimaxi.com/v1/coding_plan/search
*/
import { proxyFetch } from '@/lib/server/proxy-fetch';
import type { WebSearchResult, WebSearchSource } from '@/lib/types/web-search';
const MINIMAX_DEFAULT_BASE_URL = 'https://api.minimaxi.com';
function buildMiniMaxWebSearchUrl(baseUrl?: string): string {
const trimmed = (baseUrl || MINIMAX_DEFAULT_BASE_URL).replace(/\/$/, '');
if (trimmed.endsWith('/v1/coding_plan/search')) return trimmed;
if (trimmed.endsWith('/v1/coding_plan')) return `${trimmed}/search`;
if (trimmed.endsWith('/v1')) return `${trimmed}/coding_plan/search`;
return `${trimmed}/v1/coding_plan/search`;
}
function getMiniMaxBaseResp(raw: unknown):
| {
status_code?: string | number;
status_msg?: string;
}
| undefined {
if (!raw || typeof raw !== 'object') return undefined;
const value =
(raw as { base_resp?: unknown; baseResp?: unknown }).base_resp ||
(raw as { base_resp?: unknown; baseResp?: unknown }).baseResp;
if (!value || typeof value !== 'object') return undefined;
return value as { status_code?: string | number; status_msg?: string };
}
function formatMiniMaxError(status: number, statusText: string, errorText: string): string {
if (!errorText) return `MiniMax Web Search API error (${status}): ${statusText}`;
try {
const parsed = JSON.parse(errorText) as {
code?: string | number;
message?: string;
error?: { message?: string };
base_resp?: { status_code?: string | number; status_msg?: string };
};
const baseResp = getMiniMaxBaseResp(parsed);
const code = baseResp?.status_code ?? parsed.code ?? status;
const message = baseResp?.status_msg || parsed.message || parsed.error?.message || statusText;
return `MiniMax Web Search API error (${code}): ${message}`;
} catch {
return `MiniMax Web Search API error (${status}): ${errorText}`;
}
}
function getOrganicResults(raw: MiniMaxSearchResponse): MiniMaxOrganicResult[] {
return raw.organic || raw.data?.organic || raw.results || [];
}
/**
* Search the web using MiniMax Web Search API and return structured results.
*/
export async function searchWithMiniMax(params: {
query: string;
apiKey: string;
maxResults?: number;
baseUrl?: string;
}): Promise<WebSearchResult> {
const { query, apiKey, maxResults = 10, baseUrl } = params;
const startedAt = Date.now();
const res = await proxyFetch(buildMiniMaxWebSearchUrl(baseUrl), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
'MM-API-Source': 'OpenMAIC',
},
body: JSON.stringify({ q: query }),
});
if (!res.ok) {
const errorText = await res.text().catch(() => '');
throw new Error(formatMiniMaxError(res.status, res.statusText, errorText));
}
const raw = (await res.json()) as MiniMaxSearchResponse;
const baseResp = getMiniMaxBaseResp(raw);
if (baseResp?.status_code !== undefined && String(baseResp.status_code) !== '0') {
throw new Error(
`MiniMax Web Search API error (${baseResp.status_code}): ${
baseResp.status_msg || 'Request failed'
}`,
);
}
const limit = Math.max(Math.floor(maxResults), 1);
const sources: WebSearchSource[] = getOrganicResults(raw)
.map((item) => {
const url = item.link || item.url || '';
return {
title: item.title || url,
url,
content: item.snippet || item.summary || item.content || item.date || '',
score: 0,
};
})
.filter((source) => source.url)
.slice(0, limit);
return {
answer: '',
sources,
query,
responseTime: (Date.now() - startedAt) / 1000,
};
}
interface MiniMaxSearchResponse {
organic?: MiniMaxOrganicResult[];
results?: MiniMaxOrganicResult[];
data?: {
organic?: MiniMaxOrganicResult[];
};
base_resp?: {
status_code?: string | number;
status_msg?: string;
};
baseResp?: {
status_code?: string | number;
status_msg?: string;
};
}
interface MiniMaxOrganicResult {
title?: string;
link?: string;
url?: string;
snippet?: string;
summary?: string;
content?: string;
date?: string;
}