feat: enhance session sync and add voice input feature

- 增强会话同步:确保所有会话完全同步到新账号
  - 强制更新 model_provider 字段
  - 添加 session_index.jsonl 重建功能
  - 确保数据库 UPSERT 逻辑正确处理所有情况
- 新增语音输入功能(voice_cdp.js, voice_inject.js)
- 测试脚本和增强模块

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
mac
2026-07-16 19:16:45 +08:00
parent a6207fdefd
commit 02fb553c9e
19 changed files with 1884 additions and 95 deletions
+184 -10
View File
@@ -2,6 +2,12 @@
// 统一在主进程发起 image/video/tts 请求并把成品下载转 base64 data URL 返回渲染层。
// key 只在主进程流转;渲染层被 CSP 拦截的远程 URL 一律由这里下载转 base64。
const router = require("./router");
const fs = require("fs");
const http = require("http");
const https = require("https");
const os = require("os");
const path = require("path");
const { execFileSync } = require("child_process");
// —— 从 baseUrl 解析出主机名(失败则原样返回)——
function hostOf(baseUrl) {
@@ -31,12 +37,116 @@ function headers(key) {
};
}
function grokDebugLog(entry) {
try {
const dir = path.join(os.homedir(), "Library", "Application Support", "codex-market");
fs.mkdirSync(dir, { recursive: true });
fs.appendFileSync(
path.join(dir, "grok-video-debug.jsonl"),
JSON.stringify({ time: new Date().toISOString(), ...entry }) + "\n"
);
} catch {}
}
function systemProxyUrl() {
const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || "";
if (envProxy) return envProxy;
if (process.platform !== "darwin") return "";
try {
const out = execFileSync("scutil", ["--proxy"], { encoding: "utf8", timeout: 1000 });
const get = (name) => {
const m = out.match(new RegExp("^\\s*" + name + "\\s*:\\s*(.+)$", "m"));
return m ? m[1].trim() : "";
};
const httpsOn = get("HTTPSEnable") === "1";
const httpOn = get("HTTPEnable") === "1";
const host = httpsOn ? get("HTTPSProxy") : httpOn ? get("HTTPProxy") : "";
const port = httpsOn ? get("HTTPSPort") : httpOn ? get("HTTPPort") : "";
return host && port ? `http://${host}:${port}` : "";
} catch {
return "";
}
}
function proxyAgentFor(targetUrl, proxyUrl) {
try {
if (/^https:/i.test(targetUrl)) {
const { HttpsProxyAgent } = require("https-proxy-agent");
return new HttpsProxyAgent(proxyUrl);
}
const { HttpProxyAgent } = require("http-proxy-agent");
return new HttpProxyAgent(proxyUrl);
} catch {
return null;
}
}
function requestBuffer(url, agent) {
return new Promise((resolve, reject) => {
let u;
try {
u = new URL(url);
} catch (e) {
reject(e);
return;
}
const mod = u.protocol === "https:" ? https : http;
const req = mod.get(
url,
{
agent,
headers: { "User-Agent": "codex-market" },
},
(res) => {
const loc = res.headers.location;
if (res.statusCode >= 300 && res.statusCode < 400 && loc) {
res.resume();
requestBuffer(new URL(loc, url).toString(), agent).then(resolve, reject);
return;
}
if (res.statusCode < 200 || res.statusCode >= 300) {
res.resume();
reject(new Error("HTTP " + res.statusCode));
return;
}
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => resolve(Buffer.concat(chunks)));
}
);
req.setTimeout(120000, () => req.destroy(new Error("下载超时")));
req.on("error", reject);
});
}
// —— 下载任意远程资源 → base64 字符串 ——
async function downloadAsBase64(url) {
const res = await fetch(url);
if (!res.ok) throw new Error("下载失败 HTTP " + res.status);
const buf = Buffer.from(await res.arrayBuffer());
return buf.toString("base64");
const proxyUrl = systemProxyUrl();
let proxyErr = "";
if (proxyUrl) {
const agent = proxyAgentFor(url, proxyUrl);
if (agent) {
try {
return (await requestBuffer(url, agent)).toString("base64");
} catch (e) {
proxyErr = String((e && e.message) || e);
// 代理下载失败时再直连兜底,保留原行为。
}
}
}
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 120000);
try {
const res = await fetch(url, { headers: { "User-Agent": "codex-market" }, signal: ctrl.signal });
if (!res.ok) throw new Error("下载失败 HTTP " + res.status);
const buf = Buffer.from(await res.arrayBuffer());
return buf.toString("base64");
} catch (e) {
const directErr = String((e && e.message) || e);
throw new Error("下载失败:" + directErr + (proxyErr ? ";代理下载也失败:" + proxyErr : ""));
} finally {
clearTimeout(timer);
}
}
// —— 组装 data URL ——
@@ -290,7 +400,8 @@ async function runTts(base, llm, params) {
// —— 递归深挖视频地址(覆盖 result_url、data.content.video_url、data.data.content.video_url 等嵌套)——
// port 自 提示词提取/server.js
function findVideoUrl(o) {
if (!o || typeof o === "string") return "";
if (!o) return "";
if (typeof o === "string") return /^https?:\/\//.test(o) ? o : "";
if (Array.isArray(o)) {
for (const v of o) {
const u = findVideoUrl(v);
@@ -324,6 +435,27 @@ function joinUrl(base, p) {
return trimSlash(base) + (String(p || "").startsWith("/") ? p : "/" + p);
}
function pickTaskId(o) {
if (!o || typeof o !== "object") return "";
for (const k of ["request_id", "requestId", "task_id", "taskId", "id"]) {
if (typeof o[k] === "string" && o[k]) return o[k];
}
for (const k of ["data", "result", "task"]) {
const v = o[k];
if (v && typeof v === "object" && !Array.isArray(v)) {
const id = pickTaskId(v);
if (id) return id;
}
}
return "";
}
function pushRecentPoll(arr, value) {
if (!value) return;
arr.push(String(value).slice(0, 500));
if (arr.length > 3) arr.shift();
}
// —— Seedance/TokenDun 视频:异步任务制(建任务 → 轮询 → 下载成品转 base64)——
// mode: "text"(文生视频,只用 prompt) | "image"(图生视频)。model 须用户填写。
// 图生视频素材两条来路:params.imageUrl(手填公网URL,优先) 或 params.image(上游 base64)。
@@ -472,6 +604,21 @@ async function runGrokVideo(base, llm, params) {
}
const createUrl = joinUrl(base, params.endpointPath || "/v1/videos/generations");
const debugBase = {
base: hostOf(base),
create_url: createUrl,
mode,
request: {
model: body.model,
prompt: body.prompt,
resolution: body.resolution || "",
duration: body.duration || "",
aspect_ratio: body.aspect_ratio || "",
has_image: !!body.image,
reference_image_count: Array.isArray(body.reference_images) ? body.reference_images.length : 0,
},
};
grokDebugLog({ type: "grok_video_create", ...debugBase });
const cres = await fetch(createUrl, {
method: "POST",
headers: headers(llm.key),
@@ -479,15 +626,25 @@ async function runGrokVideo(base, llm, params) {
});
if (!cres.ok) {
const detail = await cres.text().catch(() => "");
return { ok: false, error: `Grok 视频建任务失败(${cres.status}):${detail.slice(0, 300)}` };
grokDebugLog({ type: "grok_video_create_failed", ...debugBase, status: cres.status, detail: detail.slice(0, 300) });
return { ok: false, error: `Grok 视频建任务失败(${cres.status}):POST ${createUrl} → ${detail.slice(0, 300)}` };
}
const data = await cres.json();
const requestId = data.request_id || data.id || (data.data && (data.data.request_id || data.data.id));
let data;
let rawCreate = "";
try {
rawCreate = await cres.text();
data = JSON.parse(rawCreate);
} catch {
return { ok: false, error: `Grok 视频建任务返回非 JSON:${rawCreate.slice(0, 300)}` };
}
const requestId = pickTaskId(data);
if (!requestId) return { ok: false, error: `Grok 视频建任务未返回 request_id:${JSON.stringify(data).slice(0, 300)}` };
grokDebugLog({ type: "grok_video_accepted", ...debugBase, request_id: requestId });
const pollUrl = joinUrl(base, "/v1/videos/" + encodeURIComponent(requestId));
const pollHeaders = { Authorization: "Bearer " + (llm.key || ""), Accept: "application/json", "User-Agent": "codex-market" };
let lastPoll = "";
const recentPolls = [];
for (let i = 0; i < 120; i++) {
await sleep(5000);
let pd;
@@ -502,21 +659,38 @@ async function runGrokVideo(base, llm, params) {
lastPoll = "请求异常:" + String((e && e.message) || e);
continue;
}
pushRecentPoll(recentPolls, JSON.stringify(pd));
const d = pd && pd.data && typeof pd.data === "object" && !Array.isArray(pd.data) ? pd.data : pd;
const st = normalizeVideoStatus((d && (d.status || d.state)) || (pd && pd.status));
lastPoll = "status=" + st;
if (st === "succeeded") {
const url = findVideoUrl(pd);
if (!url) return { ok: false, error: `Grok 视频已完成但未找到视频地址:${JSON.stringify(pd).slice(0, 500)}` };
if (!url) {
lastPoll = "status=succeeded,但暂未返回视频地址";
continue;
}
const b64 = await downloadAsBase64(url);
grokDebugLog({ type: "grok_video_done", ...debugBase, request_id: requestId, status: st, url });
return { ok: true, mime: "video/mp4", dataUrl: toDataUrl("video/mp4", b64), url, _model: model };
}
if (st === "failed") {
const err = (d && d.error && (d.error.message || d.error)) || (d && (d.fail_reason || d.message || d.reason)) || (pd && pd.message) || st;
grokDebugLog({ type: "grok_video_failed", ...debugBase, request_id: requestId, status: st, error: typeof err === "string" ? err : JSON.stringify(err) });
return { ok: false, error: "Grok 视频任务失败:" + (typeof err === "string" ? err : JSON.stringify(err)) };
}
}
return { ok: false, error: "Grok 视频轮询 10 分钟未完成(request_id=" + requestId + ",最近一次轮询:" + (lastPoll || "无") + ")" };
return {
ok: false,
error:
"Grok 视频轮询 10 分钟未完成(GET " +
pollUrl +
",request_id=" +
requestId +
",最近一次轮询:" +
(lastPoll || "无") +
(recentPolls.length ? ",最近原始返回:" + recentPolls.join(" | ") : "") +
")",
};
} catch (e) {
return { ok: false, error: "Grok 视频生成异常:" + String((e && e.message) || e) };
}