YYP0502 commited on
Commit
5063a9b
·
verified ·
1 Parent(s): 184b8d4

Delete src

Browse files
Files changed (4) hide show
  1. src/codex.ts +0 -102
  2. src/doctor.ts +0 -3
  3. src/index.ts +0 -87
  4. src/session-settings.ts +0 -21
src/codex.ts DELETED
@@ -1,102 +0,0 @@
1
- import "dotenv/config";
2
- import { mkdir, readFile, writeFile, appendFile, readdir, stat } from "node:fs/promises";
3
-
4
- import path from "node:path";
5
- import type { CallSettings } from "./session-settings.js";
6
-
7
- const root = path.resolve(process.cwd());
8
- const dataDir = path.join(root, "data");
9
- const historyFile = path.join(dataDir, "history.json");
10
- const logFile = path.join(dataDir, "bridge.log");
11
- const env = (n: string, f = "") => process.env[n]?.trim() || f;
12
- export type Turn = { role: "user" | "assistant"; text: string; at: string };
13
- export type InputAttachment = { url: string; filename?: string; content_type?: string; size?: number };
14
- type Store = Record<string, Turn[]>;
15
- let store: Store = {};
16
- let writeChain = Promise.resolve();
17
-
18
- export async function initStore() { await mkdir(dataDir, { recursive: true }); store = {}; await writeFile(historyFile, "{}", "utf8"); }
19
- function save() { writeChain = writeChain.then(() => writeFile(historyFile, JSON.stringify(store, null, 2), "utf8")); return writeChain; }
20
- export async function resetSession(k: string) { delete store[k]; await save(); }
21
- function turns(k: string) { return store[k] ?? []; }
22
- async function log(s: string) { await appendFile(logFile, `${new Date().toISOString()} ${s}\n`, "utf8").catch(() => {}); }
23
- function safeName(name: string, fallback: string) { const base = path.basename(name || fallback).replace(/[^a-zA-Z0-9._-]/g, "_"); return base || fallback; }
24
-
25
- function providerEnv(): NodeJS.ProcessEnv { return { ...process.env }; }
26
-
27
- async function downloadInputs(workdir: string, key: string, attachments: InputAttachment[]) {
28
- const dir = path.join(workdir, "qq-inbox", safeName(key, "session")); await mkdir(dir, { recursive: true });
29
- const max = Number(env("MAX_INPUT_FILE_BYTES", String(20 * 1024 * 1024))), result: string[] = [];
30
- for (let i = 0; i < attachments.length; i++) {
31
- const a = attachments[i]; if (!a.url || !/^https?:$/i.test(new URL(a.url).protocol)) continue;
32
- if (a.size && a.size > max) throw new Error(`图片过大,限制为 ${Math.round(max / 1024 / 1024)} MB`);
33
- const r = await fetch(a.url); if (!r.ok) throw new Error(`下载 QQ 图片失败:HTTP ${r.status}`);
34
- const buf = Buffer.from(await r.arrayBuffer()); if (buf.byteLength > max) throw new Error(`图片过大,限制为 ${Math.round(max / 1024 / 1024)} MB`);
35
- const ext = path.extname(a.filename || "") || (a.content_type?.split("/")[1] ? `.${a.content_type.split("/")[1].split(";")[0]}` : ".bin");
36
- const file = path.join(dir, `${Date.now()}-${i}-${safeName(path.basename(a.filename || `attachment${ext}`), `attachment${ext}`)}`);
37
- await writeFile(file, buf); result.push(file);
38
- }
39
- return result;
40
- }
41
-
42
- function prompt(k: string, text: string, files: string[]) { const n = Math.max(0, Number(env("HISTORY_TURNS", "8")) * 2); const h = turns(k).slice(-n).map(t => `${t.role === "user" ? "用户" : "Codex"}: ${t.text}`).join("\n\n") || "(无)"; const f = files.length ? `\n\n用户发送的文件已保存到以下路径,请按需读取:\n${files.join("\n")}` : ""; return `你正在通过 QQ 机器人与用户对话。请直接回答,不要提及桥接程序或内部提示。你运行在可联网的 Hugging Face 容器中:需要最新信息时必须使用 curl 或 Python requests 访问公开网页/API并核验来源,若请求失败须说明具体错误,不得编造新闻。你可以在当前工作目录创建和编辑 pptx、xlsx、docx、pdf 等文件;使用已安装的 python-pptx、openpyxl、python-docx、reportlab,完成后确认文件存在并将生成文件返回给用户。\n\n最近对话:\n${h}\n\n用户本次消息:\n${text}${f}`; }
43
-
44
- async function listArtifacts(dir: string, depth = 0): Promise<Map<string, number>> { const out = new Map<string, number>(); if (depth > 2) return out; for (const e of await readdir(dir, { withFileTypes: true })) { const p = path.join(dir, e.name); if (e.isDirectory()) { for (const [k, v] of await listArtifacts(p, depth + 1)) out.set(k, v); } else if (/\.(pptx|pdf|docx|xlsx|csv|png|jpg|jpeg|webp)$/i.test(e.name)) { out.set(p, (await stat(p)).mtimeMs); } } return out; }
45
- export async function findNewArtifacts(workdir: string, before: Map<string, number>) { const after = await listArtifacts(workdir); return [...after.keys()].filter(p => !before.has(p) || (after.get(p)! > before.get(p)! + 1000)).sort(); }
46
- export async function downloadAttachments(workdir: string, key: string, attachments: InputAttachment[]) { return attachments.length ? downloadInputs(workdir, key, attachments) : []; }
47
-
48
- export async function askCodex(k: string, text: string, files: string[] = [], before?: Map<string, number>, settings: CallSettings = {}): Promise<{ answer: string; artifacts: string[] }> {
49
- const workdir = path.resolve(env("CODEX_WORKDIR", path.join(root, "workspace"))); await mkdir(workdir, { recursive: true });
50
- const out = path.join(dataDir, `codex-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`);
51
- const args = ["exec", "--json", "--skip-git-repo-check", "-C", workdir, "-s", "danger-full-access", "-o", out];
52
- if (env("CODEX_ENABLE_WEB_SEARCH", "false") === "true") args.push("--search");
53
- const model = settings.model ?? env("CODEX_MODEL"); if (model) args.push("-m", model); const effort = settings.effort ?? env("CODEX_REASONING_EFFORT"); if (effort) args.push("-c", `model_reasoning_effort=${JSON.stringify(effort)}`);
54
- for (const f of files) if (/\.(png|jpe?g|webp|gif|bmp)$/i.test(f)) args.push("-i", f); args.push("-"); await log(`request key=${k} chars=${text.length} files=${files.length}`);
55
- const { spawn } = await import("node:child_process"); const configured = env("CODEX_COMMAND", "codex"); const command = /^[^\\/:]+\.cmd$/i.test(configured) ? configured.replace(/\.cmd$/i, ".exe") : configured; const childEnv = await providerEnv();
56
- return new Promise((resolve, reject) => {
57
- const c = spawn(command, args, { cwd: workdir, env: childEnv, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
58
- let err = "";
59
- // Codex --json can emit many events. Always drain stdout, or its pipe fills and stalls the child.
60
- let stdoutBytes = 0;
61
- c.stdout.on("data", b => { stdoutBytes += b.length; });
62
- c.stderr.on("data", b => { err = (err + String(b)).slice(-6000); });
63
- const configuredTimeout = Number(env("CODEX_TIMEOUT_MS", "900000"));
64
- const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 900000;
65
- let timedOut = false;
66
- const timer = setTimeout(() => {
67
- timedOut = true;
68
- void log(`timeout key=${k} after=${timeoutMs}ms`);
69
- c.kill("SIGKILL");
70
- reject(new Error(`Codex 响应超时(${Math.round(timeoutMs / 60000)} 分钟)。请缩小任务或提高 CODEX_TIMEOUT_MS。`));
71
- }, timeoutMs);
72
- c.on("error", e => { clearTimeout(timer); reject(new Error(`无法启动 Codex:${e.message}`)); });
73
- c.on("close", async code => {
74
- try {
75
- await log(`codex exit key=${k} code=${code} stdoutBytes=${stdoutBytes} timedOut=${timedOut}`);
76
- clearTimeout(timer);
77
- if (timedOut) return;
78
- let answer = "";
79
- try { answer = (await readFile(out, "utf8")).trim(); } catch {}
80
- if (code !== 0) { await log(`error code=${code} stderr=${err.slice(-1500)}`); reject(new Error(err.trim().slice(-1000) || `Codex 退出码 ${code}`)); return; }
81
- if (!answer) answer = "Codex 没有返回文本。";
82
- const a = store[k] ??= [];
83
- a.push({ role: "user", text, at: new Date().toISOString() }, { role: "assistant", text: answer, at: new Date().toISOString() });
84
- if (a.length > 40) a.splice(0, a.length - 40);
85
- await save();
86
- const artifacts = await findNewArtifacts(workdir, before ?? new Map());
87
- await log(`success key=${k} chars=${answer.length} artifacts=${artifacts.length}`);
88
- resolve({ answer, artifacts });
89
- } catch (e) { reject(e); }
90
- });
91
- c.stdin.on("error", e => { void log(`stdin error key=${k} ${e.message}`); });
92
- c.stdin.end(prompt(k, text, files), "utf8");
93
- });
94
- }
95
- export function splitReply(s: string) { const m = Math.max(500, Number(env("MAX_REPLY_CHARS", "3500"))), a: string[] = []; for (let i = 0; i < s.length; i += m) a.push(s.slice(i, i + m)); return a; }
96
- export async function snapshotArtifacts(workdir: string) { await mkdir(path.resolve(workdir), { recursive: true }); return listArtifacts(path.resolve(workdir)); }
97
-
98
-
99
-
100
-
101
-
102
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/doctor.ts DELETED
@@ -1,3 +0,0 @@
1
- import "dotenv/config"; import {execFile} from "node:child_process"; import {promisify} from "node:util"; try{const r=await promisify(execFile)(process.env.CODEX_COMMAND||"codex",["--version"],{windowsHide:true});console.log(r.stdout.trim());}catch(e){console.error("找不到 Codex CLI,请先 codex login,并确认 codex.cmd 在 PATH 中。",e);process.exitCode=1;}
2
-
3
-
 
 
 
 
src/index.ts DELETED
@@ -1,87 +0,0 @@
1
- import "dotenv/config";
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { effectiveSettings, switchSetting } from "./session-settings.js";
5
- import { QQBot, mentionGate, messageFilter, rateLimiter, accessPolicy, errorHandler } from "@tencent-connect/qqbot-nodejs";
6
- import { initStore, resetSession, askCodex, splitReply, downloadAttachments, snapshotArtifacts } from "./codex.js";
7
-
8
- for (const n of ["QQBOT_APP_ID", "QQBOT_APP_SECRET"]) if (!process.env[n]?.trim()) throw new Error(`缺少环境变量 ${n}`);
9
- const allowed = (process.env.QQ_ALLOWED_OPENIDS ?? "").split(",").map(x => x.trim()).filter(Boolean);
10
- if (!allowed.length) console.warn("QQ_ALLOWED_OPENIDS 为空,将拒绝所有消息。");
11
- await initStore();
12
-
13
- const bot = new QQBot({ appId: process.env.QQBOT_APP_ID!, appSecret: process.env.QQBOT_APP_SECRET!, logger: console });
14
- bot.use(errorHandler());
15
- bot.use(messageFilter({ skipSelfEcho: true, dedup: { windowMs: 10000 } }));
16
- bot.use(accessPolicy({ c2c: { mode: "allowlist", allow: allowed }, group: { mode: "allowlist", allow: allowed } }));
17
- bot.use(mentionGate({ requireMentionInGroup: true }));
18
- bot.use(rateLimiter({ perSender: { max: 6, windowMs: 60000 }, global: { max: 30, windowMs: 60000 } }));
19
- // Do not queue /status behind a long-running Codex request.
20
- const active = new Map<string, number>();
21
- bot.on("ready", () => console.log("QQ 机器人已连接;等待消息。"));
22
- bot.on("error", e => console.error("QQ SDK error:", e));
23
-
24
- bot.on("message", async (_ctx, msg) => {
25
- const key = `${msg.kind}:${msg.replyTarget.targetId}`;
26
- const input = msg.content.trim();
27
- if (!input && !(msg.attachments?.length)) return;
28
- const switched = switchSetting(key, input);
29
- if (switched !== undefined) {
30
- await bot.sendText(msg.replyTarget, switched);
31
- return;
32
- }
33
- if (input === "/status") {
34
- const started = active.get(key);
35
- const settings = effectiveSettings(key);
36
- const state = started ? `任务仍在处理,已运行 ${Math.floor((Date.now() - started) / 60000)} 分钟。` : "机器人在线,当前会话没有运行中的任务。";
37
- await bot.sendText(msg.replyTarget, `${state}\n下一次调用模型:${settings.model || "CLI 默认"}\n推理深度:${settings.effort || "CLI 默认"}`);
38
- return;
39
- }
40
- if (input === "/diag") {
41
- const files = ["bridge.log", "startup.log", "jupyter.log"];
42
- const excerpts: string[] = [];
43
- for (const file of files) {
44
- try {
45
- let lines = (await readFile(path.join(process.cwd(), "data", file), "utf8")).split("\n").filter(Boolean).slice(-8).join("\n");
46
- for (const secret of [process.env.CODEX_API_KEY, process.env.QQBOT_APP_SECRET, process.env.JUPYTER_TOKEN, process.env.QQBOT_APP_ID]) if (secret) lines = lines.replaceAll(secret, "[REDACTED]");
47
- excerpts.push(`${file}:\n${lines.slice(-900)}`);
48
- } catch { excerpts.push(`${file}: 暂无日志`); }
49
- }
50
- await bot.sendText(msg.replyTarget, excerpts.join("\n\n").slice(-3000));
51
- return;
52
- }
53
- if (input === "/reset") {
54
- if (active.has(key)) { await bot.sendText(msg.replyTarget, "当前任务尚在运行;请等待任务结束后再重置。"); return; }
55
- await resetSession(key);
56
- await bot.sendText(msg.replyTarget, "已清空当前会话。");
57
- return;
58
- }
59
- if (active.has(key)) { await bot.sendText(msg.replyTarget, "当前已有任务运行中。发送 /status 可查看耗时,请勿重复提交。"); return; }
60
- active.set(key, Date.now());
61
- const callSettings = effectiveSettings(key);
62
- let progress: ReturnType<typeof setInterval> | undefined;
63
- try {
64
- await bot.sendText(msg.replyTarget, msg.attachments?.length ? "已收到附件,Codex 正在处理…" : "已收到,Codex 正在处理…");
65
- progress = setInterval(() => {
66
- const mins = Math.floor((Date.now() - active.get(key)!) / 60000);
67
- void bot.sendText(msg.replyTarget, `任务仍在运行(${mins} 分钟),完成或失败后会回复。`).catch(e => console.error("进度消息发送失败:", e));
68
- }, 180000);
69
- const workdir = process.env.CODEX_WORKDIR || "/home/node/app/workspace";
70
- const files = await downloadAttachments(workdir, key, msg.attachments ?? []);
71
- const before = await snapshotArtifacts(workdir);
72
- const result = await askCodex(key, input || "请分析我发送的图片或文件,并给出结果。", files, before, callSettings);
73
- for (const part of splitReply(result.answer)) await bot.sendText(msg.replyTarget, part);
74
- for (const file of result.artifacts) {
75
- try { await bot.sendFile(msg.replyTarget, { localPath: file }, { fileName: file.split(/[\\/]/).pop() }); }
76
- catch (e) { await bot.sendText(msg.replyTarget, `文件已生成但发送失败:${file.split(/[\\/]/).pop()}\n${e instanceof Error ? e.message : String(e)}`); }
77
- }
78
- } catch (e) {
79
- console.error("任务失败:", e);
80
- try { await bot.sendText(msg.replyTarget, `处理失败:${e instanceof Error ? e.message : String(e)}`); }
81
- catch (sendError) { console.error("失败消息也无法发送:", sendError); }
82
- } finally {
83
- if (progress) clearInterval(progress);
84
- active.delete(key);
85
- }
86
- });
87
- await bot.start();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/session-settings.ts DELETED
@@ -1,21 +0,0 @@
1
- export type CallSettings = { model?: string; effort?: string };
2
- const overrides = new Map<string, CallSettings>();
3
- const efforts = new Set(["low", "medium", "high", "xhigh", "max", "ultra"]);
4
- export function effectiveSettings(key: string): CallSettings {
5
- const value = overrides.get(key);
6
- return { model: value?.model ?? process.env.CODEX_MODEL?.trim(), effort: value?.effort ?? process.env.CODEX_REASONING_EFFORT?.trim() };
7
- }
8
- export function switchSetting(key: string, input: string): string | undefined {
9
- const match = /^\/(switch|effort)(?:\s+([\s\S]*))?$/.exec(input);
10
- if (!match) return undefined;
11
- const [, command, raw = ""] = match;
12
- const value = raw.trim();
13
- if (command === "switch") {
14
- if (!value || value.length > 200 || /\s|[\x00-\x1f\x7f]/.test(value)) return "模型切换失败。用法:/switch 模型名,例如 /switch gpt-6-sol";
15
- overrides.set(key, { ...overrides.get(key), model: value });
16
- return `模型切换成功:${value}。从下一次请求使用,重启后恢复环境变量默认值。`;
17
- }
18
- if (!efforts.has(value)) return "推理深度切换失败。可选:low / medium / high / xhigh / max / ultra";
19
- overrides.set(key, { ...overrides.get(key), effort: value });
20
- return `推理深度切换成功:${value}。从下一次请求使用,重启后恢复环境变量默认值。`;
21
- }