chore: import upstream snapshot with attribution
Deploy GitBook to 9router.github.io / build-deploy (push) Failing after 2s
Deploy GitBook to 9router.github.io / build-deploy (push) Failing after 2s
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Script: đọc providersDisplay.js + providers.js, inject display+category+uiAlias+extra vào từng registry file.
|
||||
* Chạy: node scripts/injectDisplayToRegistry.mjs
|
||||
*/
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..");
|
||||
const REGISTRY_DIR = path.join(ROOT, "open-sse/providers/registry");
|
||||
|
||||
// ── 1. Build DISPLAY map từ providersDisplay.js (parse thủ công để không cần import) ──
|
||||
// Đọc file, eval trong sandbox đơn giản
|
||||
const displaySrc = fs.readFileSync(path.join(ROOT, "src/shared/constants/providersDisplay.js"), "utf8");
|
||||
const RISK_NOTICE = "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.";
|
||||
// strip export keywords + inject RISK_NOTICE as param so no redeclaration
|
||||
const displayBody = displaySrc
|
||||
.replace(/^export const /gm, "const ")
|
||||
.replace(/^export function /gm, "function ")
|
||||
.replace(/^const RISK_NOTICE\s*=.*$/m, ""); // remove redeclaration
|
||||
// eslint-disable-next-line no-new-func
|
||||
const getDisplay = new Function("RISK_NOTICE", `${displayBody}; return PROVIDER_DISPLAY;`);
|
||||
const DISPLAY = getDisplay(RISK_NOTICE);
|
||||
|
||||
// ── 2. Build CATEGORY + EXTRA map từ providers.js ──
|
||||
// Map: providerId → { category, uiAlias, extra fields }
|
||||
const CATEGORY_MAP = {};
|
||||
|
||||
// Đọc providers.js source để extract thủ công từng dòng
|
||||
const provSrc = fs.readFileSync(path.join(ROOT, "src/shared/constants/providers.js"), "utf8");
|
||||
|
||||
// Detect category blocks
|
||||
const CATEGORIES = {
|
||||
free: /export const FREE_PROVIDERS\s*=\s*\{([\s\S]*?)\n\};/,
|
||||
freeTier: /export const FREE_TIER_PROVIDERS\s*=\s*\{([\s\S]*?)\n\};/,
|
||||
oauth: /export const OAUTH_PROVIDERS\s*=\s*\{([\s\S]*?)\n\};/,
|
||||
apikey: /export const APIKEY_PROVIDERS\s*=\s*\{([\s\S]*?)\n\};/,
|
||||
webCookie: /export const WEB_COOKIE_PROVIDERS\s*=\s*\{([\s\S]*?)\n\};/,
|
||||
};
|
||||
|
||||
// Extract provider ids + uiAlias + extra fields per category
|
||||
// Parse dòng dạng: " openai: { ...D("openai"), id: "openai", alias: "openai", ... }"
|
||||
const ENTRY_RE = /^\s{2}["']?([\w-]+)["']?\s*:\s*\{[^}]*?id:\s*["']([\w-]+)["'][^}]*?alias:\s*["']([\w-]+)["']([\s\S]*?)(?=\n\s{2}["']?[\w-]|\n\};)/gm;
|
||||
|
||||
// Extra fields cần lấy từ providers.js (không lấy display, id, alias vì đã có nguồn khác)
|
||||
const EXTRA_FIELDS = [
|
||||
"thinkingConfig",
|
||||
"regions",
|
||||
"defaultRegion",
|
||||
"hasProviderSpecificData",
|
||||
"authType",
|
||||
"authHint",
|
||||
"passthroughModels",
|
||||
"noAuth",
|
||||
"hiddenKinds",
|
||||
"hasOAuth",
|
||||
"authModes",
|
||||
];
|
||||
|
||||
// THINKING_CONFIG values để inline
|
||||
const THINKING_CONFIG = {
|
||||
extended: { options: ["auto", "on", "off"], defaultMode: "auto", defaultBudgetTokens: 10000 },
|
||||
effort: { options: ["auto", "none", "low", "medium", "high"], defaultMode: "auto" },
|
||||
};
|
||||
|
||||
// Parse thủ công từng category block
|
||||
for (const [cat, re] of Object.entries(CATEGORIES)) {
|
||||
const match = provSrc.match(re);
|
||||
if (!match) continue;
|
||||
const block = match[1];
|
||||
|
||||
// Tìm tất cả entry lines (không comment)
|
||||
const lines = block.split("\n").filter(l => l.trim() && !l.trim().startsWith("//"));
|
||||
for (const line of lines) {
|
||||
// Extract id từ id: "xxx"
|
||||
const idM = line.match(/\bid:\s*["']([\w-]+)["']/);
|
||||
// Extract uiAlias từ alias: "xxx"
|
||||
const aliasM = line.match(/\balias:\s*["']([\w-]+)["']/);
|
||||
if (!idM) continue;
|
||||
const id = idM[1];
|
||||
const uiAlias = aliasM ? aliasM[1] : id;
|
||||
|
||||
const extra = {};
|
||||
|
||||
// thinkingConfig
|
||||
if (line.includes("THINKING_CONFIG.effort")) extra.thinkingConfig = THINKING_CONFIG.effort;
|
||||
else if (line.includes("THINKING_CONFIG.extended")) extra.thinkingConfig = THINKING_CONFIG.extended;
|
||||
|
||||
// hasProviderSpecificData
|
||||
if (line.includes("hasProviderSpecificData: true")) extra.hasProviderSpecificData = true;
|
||||
|
||||
// hasOAuth
|
||||
if (line.includes("hasOAuth: true")) extra.hasOAuth = true;
|
||||
|
||||
// authModes
|
||||
const authModesM = line.match(/authModes:\s*(\[[^\]]+\])/);
|
||||
if (authModesM) {
|
||||
try { extra.authModes = JSON.parse(authModesM[1].replace(/'/g, '"')); } catch {}
|
||||
}
|
||||
|
||||
// authType (webCookie)
|
||||
const authTypeM = line.match(/authType:\s*["']([\w-]+)["']/);
|
||||
if (authTypeM) extra.authType = authTypeM[1];
|
||||
|
||||
// authHint
|
||||
const authHintM = line.match(/authHint:\s*["']([^"']+)["']/);
|
||||
if (authHintM) extra.authHint = authHintM[1];
|
||||
|
||||
// noAuth
|
||||
if (line.includes("noAuth: true")) extra.noAuth = true;
|
||||
|
||||
// passthroughModels
|
||||
if (line.includes("passthroughModels: true")) extra.passthroughModels = true;
|
||||
|
||||
// hiddenKinds
|
||||
const hiddenKindsM = line.match(/hiddenKinds:\s*(\[[^\]]+\])/);
|
||||
if (hiddenKindsM) {
|
||||
try { extra.hiddenKinds = JSON.parse(hiddenKindsM[1].replace(/'/g, '"')); } catch {}
|
||||
}
|
||||
|
||||
// regions (xiaomi-tokenplan)
|
||||
const regionsM = line.match(/regions:\s*(\[[\s\S]*?\])/);
|
||||
if (regionsM) {
|
||||
try { extra.regions = JSON.parse(regionsM[1].replace(/'/g, '"')); } catch {}
|
||||
}
|
||||
const defRegionM = line.match(/defaultRegion:\s*["']([\w-]+)["']/);
|
||||
if (defRegionM) extra.defaultRegion = defRegionM[1];
|
||||
|
||||
CATEGORY_MAP[id] = { category: cat, uiAlias, extra };
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Inject vào từng registry file ──
|
||||
const registryFiles = fs.readdirSync(REGISTRY_DIR)
|
||||
.filter(f => f.endsWith(".js") && f !== "index.js")
|
||||
.map(f => f.replace(".js", ""));
|
||||
|
||||
let injected = 0;
|
||||
let skipped = 0;
|
||||
const results = [];
|
||||
|
||||
for (const id of registryFiles) {
|
||||
const filePath = path.join(REGISTRY_DIR, `${id}.js`);
|
||||
let src = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Bỏ qua nếu đã có display field
|
||||
if (src.includes("display:")) {
|
||||
skipped++;
|
||||
results.push(`⏭️ ${id} (already has display)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const display = DISPLAY[id];
|
||||
const catInfo = CATEGORY_MAP[id];
|
||||
|
||||
if (!display && !catInfo) {
|
||||
skipped++;
|
||||
results.push(`⚠️ ${id} (no display + no category data)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build display block
|
||||
let displayBlock = "";
|
||||
if (display) {
|
||||
const d = { ...display };
|
||||
// Thay RISK_NOTICE string về const reference khi serialize
|
||||
const RISK = RISK_NOTICE;
|
||||
const displayJson = JSON.stringify(d, null, 4)
|
||||
.replace(new RegExp(JSON.stringify(RISK).slice(1, -1), "g"), "RISK_NOTICE");
|
||||
|
||||
displayBlock = ` display: ${displayJson.replace(/^/gm, " ").trimStart()},\n`;
|
||||
}
|
||||
|
||||
// Build category line
|
||||
const categoryLine = catInfo ? ` category: "${catInfo.category}",\n` : "";
|
||||
|
||||
// Build uiAlias line (chỉ khi khác với alias routing)
|
||||
let uiAliasLine = "";
|
||||
if (catInfo && catInfo.uiAlias && catInfo.uiAlias !== id) {
|
||||
uiAliasLine = ` uiAlias: "${catInfo.uiAlias}",\n`;
|
||||
}
|
||||
|
||||
// Build extra fields
|
||||
let extraBlock = "";
|
||||
if (catInfo && Object.keys(catInfo.extra).length > 0) {
|
||||
for (const [k, v] of Object.entries(catInfo.extra)) {
|
||||
extraBlock += ` ${k}: ${JSON.stringify(v)},\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject SAU dòng "alias:" hoặc cuối object (trước closing "};")
|
||||
const insertBlock = displayBlock + categoryLine + uiAliasLine + extraBlock;
|
||||
|
||||
if (!insertBlock.trim()) {
|
||||
skipped++;
|
||||
results.push(`⏭️ ${id} (nothing to inject)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tìm vị trí sau field "alias:" để inject
|
||||
const aliasLineRe = /^(\s+"?alias"?\s*:\s*["'][^"']+["'],?\n)/m;
|
||||
if (aliasLineRe.test(src)) {
|
||||
src = src.replace(aliasLineRe, `$1${insertBlock}`);
|
||||
} else {
|
||||
// Fallback: inject trước closing "};"
|
||||
src = src.replace(/^(\}\s*;\s*)$/m, `${insertBlock}$1`);
|
||||
}
|
||||
|
||||
// Thêm RISK_NOTICE import nếu cần
|
||||
if (insertBlock.includes("RISK_NOTICE") && !src.includes("RISK_NOTICE")) {
|
||||
const riskLine = `const RISK_NOTICE = ${JSON.stringify(RISK_NOTICE)};\n\n`;
|
||||
src = riskLine + src;
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, src);
|
||||
injected++;
|
||||
results.push(`✅ ${id}`);
|
||||
}
|
||||
|
||||
console.log(`\n📦 Inject display+category vào registry files:`);
|
||||
for (const r of results) console.log(` ${r}`);
|
||||
console.log(`\n✅ Injected: ${injected} | ⏭️ Skipped: ${skipped}`);
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* migrate-registry.mjs
|
||||
* Migrates all registry files to Model-A schema:
|
||||
* - models[] = ALL models (chat + media), field `kind` (default "llm")
|
||||
* - media wrapper removed → fields promoted top-level
|
||||
* - *Config.models removed (data merged into models[])
|
||||
* - format: terse, consistent indent
|
||||
*
|
||||
* Run: node --experimental-vm-modules migrate-registry.mjs [--dry]
|
||||
*/
|
||||
import { readFileSync, writeFileSync, readdirSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REGISTRY_DIR = __dirname; // script lives in registry/
|
||||
const DRY = process.argv.includes("--dry");
|
||||
|
||||
// *Config.models field → kind value
|
||||
const CFG_KIND = {
|
||||
ttsConfig: "tts",
|
||||
sttConfig: "stt",
|
||||
embeddingConfig: "embedding",
|
||||
imageConfig: "image",
|
||||
imageToTextConfig: "imageToText",
|
||||
videoConfig: "video",
|
||||
musicConfig: "music",
|
||||
};
|
||||
|
||||
// Fields in *Config that are NOT models (keep on config)
|
||||
const MODEL_ONLY_KEY = "models";
|
||||
|
||||
// Top-level registry fields that are NOT media-config (don't flatten these from media)
|
||||
// serviceKinds + *Config + searchViaChat + mediaConfig + passthroughModels are media fields
|
||||
// Everything else is already top-level
|
||||
const MEDIA_WHITELIST = new Set([
|
||||
"serviceKinds",
|
||||
"ttsConfig", "sttConfig", "embeddingConfig",
|
||||
"imageConfig", "imageToTextConfig", "videoConfig", "musicConfig",
|
||||
"searchViaChat", "searchConfig", "fetchConfig",
|
||||
"modelsFetcher", "hasProviderSpecificData", "passthroughModels",
|
||||
"mediaPriority", "hiddenKinds",
|
||||
]);
|
||||
|
||||
function migrateEntry(entry, filename) {
|
||||
const out = {};
|
||||
|
||||
// 1. Top-level identity/transport fields (preserve order)
|
||||
const TRANSPORT_KEYS = ["id", "alias", "aliases", "uiAlias", "display", "category",
|
||||
"authType", "authHint", "authModes", "hasOAuth", "noAuth",
|
||||
"hasProviderSpecificData", "thinkingConfig", "hiddenKinds",
|
||||
"regions", "defaultRegion", "passthroughModels", "transport"];
|
||||
for (const k of TRANSPORT_KEYS) {
|
||||
if (entry[k] !== undefined) out[k] = entry[k];
|
||||
}
|
||||
|
||||
// 2. Collect existing models[] (convert type→kind, skip if kind already set)
|
||||
const existingModels = (entry.models || []).map(m => {
|
||||
const { type, ...rest } = m;
|
||||
const kind = m.kind ?? (type && type !== "llm" ? type : undefined);
|
||||
return kind ? { ...rest, kind } : rest;
|
||||
});
|
||||
const existingIds = new Set(existingModels.map(m => m.id));
|
||||
|
||||
// 3. Extract models from *Config.models (merge into models[])
|
||||
const mediaModels = [];
|
||||
const media = entry.media || {};
|
||||
for (const [cfgKey, kind] of Object.entries(CFG_KIND)) {
|
||||
const cfg = media[cfgKey];
|
||||
if (!cfg?.models) continue;
|
||||
for (const m of cfg.models) {
|
||||
// Check if same id+kind combo already exists to avoid true duplicates
|
||||
const dup = existingModels.find(x => x.id === m.id && (x.kind ?? "llm") === kind);
|
||||
if (dup) continue;
|
||||
const { ...mClean } = m;
|
||||
mediaModels.push({ ...mClean, kind });
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Merge models (existing first, then media additions)
|
||||
const allModels = [...existingModels, ...mediaModels];
|
||||
// Only include models key if non-empty or explicitly defined
|
||||
if (allModels.length > 0 || entry.models !== undefined) {
|
||||
out.models = allModels;
|
||||
}
|
||||
|
||||
// 5. Flatten media fields (without .models sub-arrays)
|
||||
for (const [k, v] of Object.entries(media)) {
|
||||
if (!MEDIA_WHITELIST.has(k)) continue;
|
||||
if (CFG_KIND[k]) {
|
||||
// Strip .models from config, keep rest
|
||||
const { models: _m, ...cfgRest } = (v || {});
|
||||
if (Object.keys(cfgRest).length > 0) out[k] = cfgRest;
|
||||
} else {
|
||||
out[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Other top-level fields not in TRANSPORT_KEYS and not media (e.g. features, oauth, usage in transport)
|
||||
const SKIP = new Set([...TRANSPORT_KEYS, "models", "media", ...Object.keys(CFG_KIND),
|
||||
"serviceKinds", "searchViaChat", "searchConfig", "fetchConfig",
|
||||
"modelsFetcher", "passthroughModels", "mediaPriority"]);
|
||||
for (const [k, v] of Object.entries(entry)) {
|
||||
if (!SKIP.has(k)) out[k] = v;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Format a registry entry as clean JS (no JSON.stringify — write proper ES module)
|
||||
function formatValue(v, indent = 0) {
|
||||
const pad = " ".repeat(indent);
|
||||
const pad1 = " ".repeat(indent + 1);
|
||||
|
||||
if (v === null || v === undefined) return String(v);
|
||||
if (typeof v === "boolean" || typeof v === "number") return String(v);
|
||||
if (typeof v === "string") return JSON.stringify(v);
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
if (v.length === 0) return "[]";
|
||||
// Model arrays: 1 model per line (compact inline object)
|
||||
const items = v.map(item => {
|
||||
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
||||
return `${pad1}${formatInlineObject(item)}`;
|
||||
}
|
||||
return `${pad1}${formatValue(item, indent + 1)}`;
|
||||
});
|
||||
return `[\n${items.join(",\n")},\n${pad}]`;
|
||||
}
|
||||
|
||||
if (typeof v === "object") {
|
||||
const keys = Object.keys(v);
|
||||
if (keys.length === 0) return "{}";
|
||||
const lines = keys.map(k => {
|
||||
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
|
||||
return `${pad1}${key}: ${formatValue(v[k], indent + 1)}`;
|
||||
});
|
||||
return `{\n${lines.join(",\n")},\n${pad}}`;
|
||||
}
|
||||
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
|
||||
// Inline compact object: { id: "x", name: "y", kind: "tts", dimensions: 1536 }
|
||||
function formatInlineObject(obj) {
|
||||
const parts = Object.entries(obj).map(([k, v]) => {
|
||||
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
|
||||
return `${key}: ${JSON.stringify(v)}`;
|
||||
});
|
||||
return `{ ${parts.join(", ")} }`;
|
||||
}
|
||||
|
||||
// Config objects (ttsConfig etc) — inline single line if short, else multi-line
|
||||
function formatConfig(cfg) {
|
||||
const line = `{ ${Object.entries(cfg).map(([k,v])=>`${k}: ${JSON.stringify(v)}`).join(", ")} }`;
|
||||
if (line.length <= 120) return line;
|
||||
const pad1 = " ".repeat(2);
|
||||
const lines = Object.entries(cfg).map(([k,v]) => `${pad1}${k}: ${JSON.stringify(v)}`);
|
||||
return `{\n${lines.join(",\n")},\n }`;
|
||||
}
|
||||
|
||||
// Top-level registry entry formatter
|
||||
function formatEntry(entry, imports = "") {
|
||||
const lines = [];
|
||||
if (imports) lines.push(imports, "");
|
||||
lines.push("export default {");
|
||||
|
||||
const TOP_ORDER = [
|
||||
"id", "alias", "aliases", "uiAlias", "display", "category",
|
||||
"authType", "authHint", "authModes", "hasOAuth", "noAuth",
|
||||
"hasProviderSpecificData", "thinkingConfig", "hiddenKinds",
|
||||
"regions", "defaultRegion", "transport",
|
||||
"models",
|
||||
// media fields
|
||||
"serviceKinds",
|
||||
"ttsConfig", "sttConfig", "embeddingConfig",
|
||||
"imageConfig", "imageToTextConfig", "videoConfig", "musicConfig",
|
||||
"searchViaChat", "searchConfig", "fetchConfig", "modelsFetcher",
|
||||
"passthroughModels", "mediaPriority",
|
||||
// other
|
||||
"oauth", "features",
|
||||
];
|
||||
|
||||
const emitted = new Set();
|
||||
|
||||
function emitKey(k) {
|
||||
if (!(k in entry) || emitted.has(k)) return;
|
||||
emitted.add(k);
|
||||
const v = entry[k];
|
||||
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
|
||||
|
||||
// Config objects (xConfig) — special inline format
|
||||
if (CFG_KIND[k] || k === "searchViaChat" || k === "searchConfig" || k === "fetchConfig" || k === "modelsFetcher") {
|
||||
lines.push(` ${key}: ${formatConfig(v)},`);
|
||||
return;
|
||||
}
|
||||
|
||||
// models[] — terse per-line
|
||||
if (k === "models" && Array.isArray(v)) {
|
||||
if (v.length === 0) { lines.push(` models: [],`); return; }
|
||||
lines.push(` models: [`);
|
||||
for (const m of v) lines.push(` ${formatInlineObject(m)},`);
|
||||
lines.push(` ],`);
|
||||
return;
|
||||
}
|
||||
|
||||
// serviceKinds — inline array
|
||||
if (k === "serviceKinds") {
|
||||
lines.push(` serviceKinds: ${JSON.stringify(v)},`);
|
||||
return;
|
||||
}
|
||||
|
||||
// display — multi-line
|
||||
if (k === "display") {
|
||||
lines.push(` display: ${formatValue(v, 1)},`);
|
||||
return;
|
||||
}
|
||||
|
||||
// transport — multi-line
|
||||
if (k === "transport") {
|
||||
lines.push(` transport: ${formatValue(v, 1)},`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else
|
||||
lines.push(` ${key}: ${formatValue(v, 1)},`);
|
||||
}
|
||||
|
||||
for (const k of TOP_ORDER) emitKey(k);
|
||||
// Emit any remaining keys not in TOP_ORDER
|
||||
for (const k of Object.keys(entry)) emitKey(k);
|
||||
|
||||
lines.push("};");
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
const files = readdirSync(REGISTRY_DIR).filter(f => f.endsWith(".js") && f !== "index.js");
|
||||
let count = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const path = join(REGISTRY_DIR, file);
|
||||
const src = readFileSync(path, "utf8");
|
||||
|
||||
// Extract import lines (for files that import shared constants)
|
||||
const importLines = src.split("\n").filter(l => l.startsWith("import "));
|
||||
const importSrc = importLines.join("\n");
|
||||
|
||||
// Dynamic import to get entry
|
||||
let entry;
|
||||
try {
|
||||
const mod = await import(`${join(REGISTRY_DIR, file)}?t=${Date.now()}`);
|
||||
entry = mod.default;
|
||||
} catch (e) {
|
||||
console.error(`SKIP ${file}: ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const migrated = migrateEntry(entry, file);
|
||||
const output = formatEntry(migrated, importSrc);
|
||||
|
||||
if (DRY) {
|
||||
console.log(`\n=== ${file} ===\n${output}`);
|
||||
} else {
|
||||
writeFileSync(path, output, "utf8");
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(DRY ? `[DRY] Would migrate ${files.length} files` : `✅ Migrated ${count} files`);
|
||||
@@ -0,0 +1,84 @@
|
||||
// Live test: combo capacity display + auto-switch routing.
|
||||
// Sends text / image / search requests to a combo and reports which member ran.
|
||||
// node scripts/test-combo-autoswitch.mjs
|
||||
const BASE = process.env.BASE_URL || "http://localhost:20127";
|
||||
const KEY = process.env.API_KEY || "sk-6581be4f05a82b6b-uxy6jn-c8190ea8";
|
||||
const COMBO = process.env.COMBO || "haha";
|
||||
|
||||
// 16x16 PNG (valid image so vision providers accept it).
|
||||
const PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFklEQVR4nGO4I2JDEmIY1TCqYfhqAAAeBCwQ8YdREQAAAABJRU5ErkJggg==";
|
||||
|
||||
function memberFromModel(model) {
|
||||
// Response model usually = upstream id; map back to a combo member by substring.
|
||||
return model || "(none)";
|
||||
}
|
||||
|
||||
async function send(label, content, extra = {}) {
|
||||
const body = {
|
||||
model: COMBO,
|
||||
stream: false,
|
||||
max_tokens: 64,
|
||||
messages: [{ role: "user", content }],
|
||||
...extra,
|
||||
};
|
||||
const t0 = Date.now();
|
||||
let res, json, text;
|
||||
try {
|
||||
res = await fetch(`${BASE}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${KEY}` },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
text = await res.text();
|
||||
try { json = JSON.parse(text); } catch { /* keep text */ }
|
||||
} catch (e) {
|
||||
console.log(`\n[${label}] NETWORK ERROR: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
const ms = Date.now() - t0;
|
||||
const model = json?.model || "(no model field)";
|
||||
const ok = res.ok;
|
||||
const snippet = (json?.choices?.[0]?.message?.content || text || "").slice(0, 80).replace(/\n/g, " ");
|
||||
console.log(`\n[${label}] ${ok ? "OK" : "FAIL"} ${res.status} (${ms}ms)`);
|
||||
console.log(` model executed: ${memberFromModel(model)}`);
|
||||
if (!ok) console.log(` error: ${(json?.error?.message || text || "").slice(0, 160)}`);
|
||||
else console.log(` reply: ${snippet}`);
|
||||
}
|
||||
|
||||
async function showCaps() {
|
||||
try {
|
||||
const r = await fetch(`${BASE}/api/models`, { headers: { Authorization: `Bearer ${KEY}` } });
|
||||
if (!r.ok) { console.log("(/api/models needs dashboard auth, skipping caps table)"); return; }
|
||||
const { models } = await r.json();
|
||||
const map = {};
|
||||
for (const m of models || []) if (m.caps) map[m.fullModel] = m.caps;
|
||||
console.log("Capacity of combo members (vision/search):");
|
||||
for (const m of (process.env.MEMBERS || "").split(",").filter(Boolean)) {
|
||||
const c = map[m] || {};
|
||||
console.log(` ${m}: vision=${!!c.vision} search=${!!c.search}`);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log(`Testing combo "${COMBO}" @ ${BASE}\n${"=".repeat(50)}`);
|
||||
await showCaps();
|
||||
|
||||
// 1. Text-only: round-robin order (no capability requirement).
|
||||
await send("text-only #1", "Say hello in one word.");
|
||||
await send("text-only #2", "Say hi in one word.");
|
||||
|
||||
// 2. Image: should auto-switch to a vision-capable member.
|
||||
await send("image (needs vision)", [
|
||||
{ type: "text", text: "What color is this image? One word." },
|
||||
{ type: "image_url", image_url: { url: PNG } },
|
||||
]);
|
||||
|
||||
// 3. Search: should auto-switch to a search-capable member.
|
||||
// Claude built-in web search requires a versioned tool type.
|
||||
await send("search (needs search)", "What is the latest news today?", {
|
||||
tools: [{ type: "web_search_20250305", name: "web_search" }],
|
||||
});
|
||||
|
||||
console.log(`\n${"=".repeat(50)}\nDone. Compare 'model executed' across cases to verify auto-switch.`);
|
||||
})();
|
||||
Executable
+201
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ============ CONFIGURATION ============
|
||||
const API_ENDPOINT = process.env.GLM_API_ENDPOINT || 'https://api.z.ai/api/anthropic/v1/messages';
|
||||
const API_MODEL = process.env.GLM_API_MODEL || 'glm-5';
|
||||
const API_KEY = process.env.GLM_API_KEY;
|
||||
const MAX_TOKENS = parseInt(process.env.GLM_MAX_TOKENS || '32000');
|
||||
const TEMPERATURE = parseFloat(process.env.GLM_TEMPERATURE || '0.3');
|
||||
const BATCH_SIZE = parseInt(process.env.TRANSLATE_BATCH_SIZE || '2'); // Number of languages to translate in parallel
|
||||
|
||||
const SUPPORTED_LANGUAGES = {
|
||||
vi: 'Vietnamese',
|
||||
'zh-CN': 'Simplified Chinese'
|
||||
};
|
||||
|
||||
// ============ VALIDATION ============
|
||||
if (!API_KEY) {
|
||||
console.error('Error: GLM_API_KEY environment variable not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const targetLangs = process.argv.slice(2);
|
||||
if (targetLangs.length === 0) {
|
||||
console.error('Usage: node translate-readme.js <lang1> [lang2] ...');
|
||||
console.error(`Supported languages: ${Object.keys(SUPPORTED_LANGUAGES).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const lang of targetLangs) {
|
||||
if (!SUPPORTED_LANGUAGES[lang]) {
|
||||
console.error(`Unsupported language: ${lang}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ TRANSLATION FUNCTION ============
|
||||
async function translateToLanguage(readmeContent, targetLang) {
|
||||
const langName = SUPPORTED_LANGUAGES[targetLang];
|
||||
console.log(`\n[${targetLang}] Translating to ${langName}...`);
|
||||
console.log(`[${targetLang}] README size: ${readmeContent.length} characters`);
|
||||
|
||||
const prompt = `Translate this entire Markdown document to ${langName}.
|
||||
|
||||
CRITICAL RULES:
|
||||
- Keep ALL markdown syntax EXACTLY as is (##, \`\`\`, -, *, |, tables, etc.)
|
||||
- Do NOT modify code blocks, ASCII diagrams, or code fences
|
||||
- Only translate human-readable text content
|
||||
- Keep all URLs, links, and technical terms unchanged
|
||||
|
||||
${readmeContent}`;
|
||||
|
||||
const response = await fetch(API_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': API_KEY,
|
||||
'anthropic-version': '2023-06-01'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: API_MODEL,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
temperature: TEMPERATURE,
|
||||
max_tokens: MAX_TOKENS,
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`[${targetLang}] API Error: ${response.status} ${error}`);
|
||||
}
|
||||
|
||||
console.log(`[${targetLang}] Receiving translation stream...`);
|
||||
|
||||
let translatedContent = '';
|
||||
let chunkCount = 0;
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6);
|
||||
if (data === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
|
||||
translatedContent += parsed.delta.text;
|
||||
chunkCount++;
|
||||
if (chunkCount % 100 === 0) {
|
||||
process.stdout.write(`\r[${targetLang}] Received ${translatedContent.length} chars...`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write('\n');
|
||||
|
||||
console.log(`\n[${targetLang}] Stream complete, received ${translatedContent.length} characters`);
|
||||
|
||||
if (!translatedContent) {
|
||||
throw new Error(`[${targetLang}] No translation received`);
|
||||
}
|
||||
|
||||
console.log(`[${targetLang}] Fixing image paths...`);
|
||||
|
||||
// Fix image paths
|
||||
translatedContent = translatedContent
|
||||
.replace(/!\[([^\]]*)\]\(\.\/images\//g, '
|
||||
.replace(/!\[([^\]]*)\]\(\.\/public\//g, '
|
||||
.replace(/<img src="\.\/images\//g, '<img src="../images/')
|
||||
.replace(/<img src="\.\/public\//g, '<img src="../public/');
|
||||
|
||||
const i18nDir = path.join(__dirname, '../i18n');
|
||||
if (!fs.existsSync(i18nDir)) {
|
||||
fs.mkdirSync(i18nDir, { recursive: true });
|
||||
}
|
||||
|
||||
const outputPath = path.join(i18nDir, `README.${targetLang}.md`);
|
||||
fs.writeFileSync(outputPath, translatedContent, 'utf8');
|
||||
|
||||
console.log(`[${targetLang}] ✅ Complete: ${outputPath}`);
|
||||
return { lang: targetLang, success: true, path: outputPath };
|
||||
}
|
||||
|
||||
// ============ MAIN ============
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('README Translation Tool (Streaming Mode)');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`API Endpoint: ${API_ENDPOINT}`);
|
||||
console.log(`Model: ${API_MODEL}`);
|
||||
console.log(`Max Tokens: ${MAX_TOKENS}`);
|
||||
console.log(`Batch Size: ${BATCH_SIZE}`);
|
||||
console.log(`Languages: ${targetLangs.join(', ')}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const readmePath = path.join(__dirname, '../README.md');
|
||||
const readmeContent = fs.readFileSync(readmePath, 'utf8');
|
||||
|
||||
// Translate languages in batches (parallel within batch)
|
||||
const results = [];
|
||||
for (let i = 0; i < targetLangs.length; i += BATCH_SIZE) {
|
||||
const batch = targetLangs.slice(i, i + BATCH_SIZE);
|
||||
console.log(`\nBatch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(targetLangs.length / BATCH_SIZE)}: ${batch.join(', ')}`);
|
||||
console.log('Starting translations in parallel...\n');
|
||||
|
||||
// Start all translations in parallel (don't await yet)
|
||||
const batchPromises = batch.map(lang => translateToLanguage(readmeContent, lang));
|
||||
|
||||
// Wait for all to complete
|
||||
const batchResults = await Promise.allSettled(batchPromises);
|
||||
|
||||
results.push(...batchResults);
|
||||
|
||||
// Wait between batches to avoid rate limit
|
||||
if (i + BATCH_SIZE < targetLangs.length) {
|
||||
console.log('\nWaiting 3s before next batch...');
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('SUMMARY');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log(`✅ ${result.value.lang}: ${result.value.path}`);
|
||||
} else {
|
||||
console.log(`❌ ${result.lang}: ${result.reason.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
if (failed > 0) {
|
||||
console.log(`\n⚠️ ${failed} translation(s) failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n✅ All translations completed successfully!');
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user