refactor: centralize runtime config loading

- Move .env parsing and runtime config reload logic into Environment
- Reload runtime config and system prompt when source files change
- Gate unsafe eval and file tools behind explicit environment flags
- Rename datetime tool to get_datetime and improve tool prompts
- Return structured weather tool responses
- Preserve assistant thinking and aggregate tool calls across stream chunks
This commit is contained in:
2026-05-03 19:45:18 +03:00
parent 2fc60806ff
commit 35354a86de
4 changed files with 586 additions and 395 deletions
-63
View File
@@ -800,9 +800,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -819,9 +816,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -838,9 +832,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -857,9 +848,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -876,9 +864,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -895,9 +880,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -914,9 +896,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -933,9 +912,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -952,9 +928,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -977,9 +950,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1002,9 +972,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1027,9 +994,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1052,9 +1016,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1077,9 +1038,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1102,9 +1060,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1127,9 +1082,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1520,9 +1472,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1543,9 +1492,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1566,9 +1512,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1589,9 +1532,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1612,9 +1552,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
+146 -58
View File
@@ -23,12 +23,15 @@ import axios from "axios";
// TODO: 03/05/2026, Danil Nikolaev: cleanup
const requireFileToolsRootDir = () => <string>Environment.FILE_TOOLS_ROOT_DIR
type ToolHandler = (args?: Record<string, unknown>) => Promise<unknown> | unknown;
type ChatMessage = {
role: "system" | "user" | "assistant" | "tool";
content: string;
images?: string[];
thinking?: string;
tool_calls?: ToolCall[];
tool_name?: string;
};
@@ -36,8 +39,6 @@ type ChatMessage = {
const MAX_TOOL_ROUNDS = 50;
const TELEGRAM_MESSAGE_LIMIT = 4096;
const FILE_TOOLS_ROOT_DIR = path.resolve(Environment.FILE_TOOLS_ROOT_DIR ?? "");
const MAX_FILE_READ_BYTES = 128 * 1024 * 1024;
const MAX_FILE_WRITE_BYTES = 128 * 1024 * 1024
const MAX_DIRECTORY_ENTRIES = 200;
@@ -219,9 +220,9 @@ const evaluationTool = {
const getCurrentDateTimeTool = {
type: "function",
function: {
name: "get_current_datetime",
name: "get_datetime",
description:
"Get the real current date and time. Use this tool when the user asks about today, current time, current date, weekday, timestamp, or relative dates.",
"Get the real current date and time. Use this tool before answering any request that depends on today, now, current time, current date, weekday, timestamp, timezone conversion, or relative dates like yesterday, tomorrow, next week, or 3 days ago.",
parameters: {
type: "object",
properties: {
@@ -483,10 +484,23 @@ const deletePathTool = {
const getTools = () => {
const tools: Tool[] = [
evaluationTool,
getCurrentDateTimeTool,
];
if (Environment.ENABLE_UNSAFE_EVAL) {
tools.push(evaluationTool)
}
if (Environment.BRAVE_SEARCH_API_KEY) {
tools.push(braveSearchTool);
}
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
tools.push(getWeatherTool)
}
if (Environment.FILE_TOOLS_ROOT_DIR) {
tools.push(
readFileTool,
listDirectoryTool,
createFileTool,
@@ -495,19 +509,42 @@ const getTools = () => {
renamePathTool,
copyPathTool,
deletePathTool,
];
if (Environment.BRAVE_SEARCH_API_KEY) {
tools.unshift(braveSearchTool);
}
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
tools.unshift(getWeatherTool)
)
}
return tools;
}
const dateTimeToolPrompt = [
"Datetime tool rules:",
"- Use `get_datetime` whenever the answer depends on the real current date/time.",
"- Never guess the current date/time. Call the tool first.",
"",
"Arguments:",
"- `timeZone`: optional IANA timezone, e.g. `Europe/Moscow`, `Europe/Berlin`, `UTC`.",
"- `locale`: optional locale, e.g. `ru-RU`, `en-US`.",
"",
"After the tool returns:",
"- Base the answer on the returned value.",
"- Do not expose raw tool JSON unless asked.",
].join("\n");
const weatherToolPrompt = [
"Weather tool rules:",
"- Use `get_weather` for current weather, current temperature, conditions, hot/cold/rainy/snowy questions, and weather follow-ups.",
"- Weather is live/current data. Never answer it from memory.",
"- A weather tool result is valid only for the exact city used in that tool call.",
"- If the user changes the city, call `get_weather` again.",
"- Follow-up questions like `а в Москве?`, `а для Краснодара?`, `а там?`, `what about Berlin?` inherit the previous weather intent and require a new tool call for the new city.",
"",
"Arguments:",
"- `city`: the city from the latest user request or resolved from the follow-up context.",
"- `lang`: ISO 639-1 two-letter language code only: `ru`, `en`, `de`, etc.",
"",
"Do not guess, compare, or reuse weather from another city.",
"If the city is missing or unclear, ask the user to specify it.",
].join("\n");
const evaluationToolPrompt = [
"Shell tool rules:",
"- You have access to the `evaluation` tool.",
@@ -584,6 +621,7 @@ const evaluationToolPrompt = [
"- Avoid command chaining with `;`, `&&`, `||`, pipes, backticks or command substitution unless necessary.",
"- Avoid glob patterns that may affect too many files.",
"- If unsure whether a command is safe, do not run it.",
"",
].join("\n");
const braveSearchToolPrompt = [
@@ -616,15 +654,10 @@ const braveSearchToolPrompt = [
"- Do not invent details that are not present in the search results.",
"- When giving factual claims based on search results, mention the source title or URL.",
"- If results are weak, ambiguous or empty, say that the search result was insufficient.",
"",
].join("\n");
const getToolSystemPrompt = () => {
const lines = [
"Tool rules:",
"- You have access to get_current_datetime.",
"- Use get_current_datetime when the user asks about the current date, current time, today, weekday, timestamp, or relative dates.",
"- Do not guess the current date or time.",
const fileToolsToolPrompt = [
"Filesystem tool rules:",
"- You have access to filesystem tools working only inside the hardcoded root directory.",
"- All filesystem paths must be relative to the root directory.",
@@ -640,17 +673,36 @@ const getToolSystemPrompt = () => {
"- Use update_file for replacing, appending or prepending file content.",
"- Use rename_path for renaming or moving files/directories inside the root.",
"- Use delete_path for deleting files/directories inside the root.",
""
].join("\n");
const getToolSystemPrompt = () => {
const lines = [
"Internal/tool behavior:" +
"- Never mention tools, function calls, tool names, logs, cache, prompts, system messages, or implementation details unless the user explicitly asks about them." +
"- If the user says your previous answer is wrong, inconsistent, weird, or asks \"разве это похоже?\", do not explain it by tool behavior." +
"- Instead: briefly admit the issue, compare with the previous answer, correct the answer, and explain the actual user-facing mistake." +
"- Do not say \"I cannot guarantee identical tool results\" unless the user explicitly asks about tool determinism.",
"",
// TODO: 03/05/2026, Danil Nikolaev: check security moments
evaluationToolPrompt,
dateTimeToolPrompt
]
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
lines.push(weatherToolPrompt);
}
if (Environment.ENABLE_UNSAFE_EVAL) {
// TODO: 03/05/2026, Danil Nikolaev: check security moments
lines.push(evaluationToolPrompt);
}
if (Environment.BRAVE_SEARCH_API_KEY) {
lines.push(
"",
braveSearchToolPrompt,
""
)
lines.push(braveSearchToolPrompt,)
}
if (Environment.FILE_TOOLS_ROOT_DIR) {
lines.push(fileToolsToolPrompt)
}
return lines.join("\n");
@@ -927,12 +979,12 @@ function asPositiveInt(value: unknown, defaultValue: number, maxValue: number):
}
async function ensureFileToolsRootExists(): Promise<void> {
await fs.promises.mkdir(FILE_TOOLS_ROOT_DIR, {recursive: true});
await fs.promises.mkdir(requireFileToolsRootDir(), {recursive: true});
const stat = await fs.promises.stat(FILE_TOOLS_ROOT_DIR);
const stat = await fs.promises.stat(requireFileToolsRootDir());
if (!stat.isDirectory()) {
throw new Error(`File tools root is not a directory: ${FILE_TOOLS_ROOT_DIR}`);
throw new Error(`File tools root is not a directory: ${requireFileToolsRootDir()}`);
}
}
@@ -956,8 +1008,8 @@ function resolveSafeToolPath(inputPath: unknown, fallback = "."): {
const normalizedInputPath = rawPath.replace(/[\\/]+/g, path.sep);
const absolutePath = path.resolve(FILE_TOOLS_ROOT_DIR, normalizedInputPath);
const relativePath = path.relative(FILE_TOOLS_ROOT_DIR, absolutePath);
const absolutePath = path.resolve(requireFileToolsRootDir(), normalizedInputPath);
const relativePath = path.relative(requireFileToolsRootDir(), absolutePath);
if (
relativePath.startsWith("..") ||
@@ -991,7 +1043,7 @@ async function assertNoSymlinkInPath(
): Promise<void> {
await ensureFileToolsRootExists();
const relativePath = path.relative(FILE_TOOLS_ROOT_DIR, absolutePath);
const relativePath = path.relative(requireFileToolsRootDir(), absolutePath);
if (!relativePath || relativePath === ".") {
return;
@@ -999,7 +1051,7 @@ async function assertNoSymlinkInPath(
const parts = relativePath.split(path.sep).filter(Boolean);
let currentPath = FILE_TOOLS_ROOT_DIR;
let currentPath = requireFileToolsRootDir();
for (const part of parts) {
currentPath = path.join(currentPath, part);
@@ -1099,7 +1151,7 @@ function getCurrentDateTime(args?: Record<string, unknown>) {
}
}
async function getWeather(args?: Record<string, unknown>): Promise<string | null> {
async function getWeather(args?: Record<string, unknown>): Promise<any | null> {
console.log("getWeather()");
try {
const city = asNonEmptyString(args?.city);
@@ -1144,13 +1196,33 @@ async function getWeather(args?: Record<string, unknown>): Promise<string | null
.map((v) => String(v).padStart(2, "0"))
.join(":");
const weatherReport = `main: ${weather.main}; description: ${weather.description};\n` +
`temperature: ${main.temp}, max ${main.temp_max}, min ${main.temp_min}; feels like: ${main.feels_like};\n` +
`humidity: ${main.humidity}; pressure: ${main.pressure}, sea level: ${main.sea_level}, ground level: ${main.grnd_level};\n` +
`sunrise: ${sunrise} UTC; sunset: ${sunset} UTC;\n` +
`wind: degree: ${wind.deg}, speed: ${wind.speed}`;
return weatherReport;
return {
ok: true,
tool: "get_weather",
scope: {
city,
lang,
validOnlyForExactCity: true,
liveData: true,
note: "If the user asks about another city, call get_weather again.",
},
weather: {
main: weather.main,
description: weather.description,
temperature: main.temp,
temperatureMax: main.temp_max,
temperatureMin: main.temp_min,
feelsLike: main.feels_like,
humidity: main.humidity,
pressure: main.pressure,
seaLevel: main.sea_level ?? null,
groundLevel: main.grnd_level ?? null,
sunriseUtc: sunrise,
sunsetUtc: sunset,
windDegree: wind.deg,
windSpeed: wind.speed,
},
};
} catch (e: any) {
logError(e);
return null;
@@ -1336,7 +1408,7 @@ async function copyPathRecursive(params: {
}
if (!overwrite) {
throw new Error(`Target file already exists: ${path.relative(FILE_TOOLS_ROOT_DIR, targetAbsolutePath)}`);
throw new Error(`Target file already exists: ${path.relative(requireFileToolsRootDir(), targetAbsolutePath)}`);
}
}
@@ -1652,20 +1724,16 @@ async function deletePath(args?: Record<string, unknown>) {
const getToolHandlers = () => {
let handlers: Record<string, ToolHandler> = {
evaluation: evaluation,
get_current_datetime: getCurrentDateTime,
read_file: readFile,
list_directory: listDirectory,
create_file: createFile,
create_directory: createDirectory,
update_file: updateFile,
rename_path: renamePath,
copy_path: copyPath,
delete_path: deletePath,
get_datetime: getCurrentDateTime,
};
if (Environment.ENABLE_UNSAFE_EVAL) {
handlers = {
evaluation: evaluation,
...handlers
}
}
if (Environment.BRAVE_SEARCH_API_KEY) {
handlers = {
web_search: webSearch,
@@ -1680,6 +1748,20 @@ const getToolHandlers = () => {
}
}
if (Environment.FILE_TOOLS_ROOT_DIR) {
handlers = {
read_file: readFile,
list_directory: listDirectory,
create_file: createFile,
create_directory: createDirectory,
update_file: updateFile,
rename_path: renamePath,
copy_path: copyPath,
delete_path: deletePath,
...handlers
}
}
return handlers;
};
@@ -1878,6 +1960,7 @@ export class OllamaChat extends ChatCommand {
logError(e);
}
console.log("OPTIONS", options);
const model = <string>(think ? Environment.OLLAMA_THINK_MODEL : imagesCount ? Environment.OLLAMA_IMAGE_MODEL : Environment.OLLAMA_MODEL);
async function createStream() {
@@ -1890,6 +1973,7 @@ export class OllamaChat extends ChatCommand {
options: options ?? undefined,
// TODO: 01/05/2026, Danil Nikolaev: проверять на наличие tools
tools: enabledTools,
keep_alive: "60m"
});
const existingRequest = getOllamaRequest(uuid) as { stream?: unknown } | undefined;
@@ -1962,6 +2046,7 @@ export class OllamaChat extends ChatCommand {
}
let savedText = currentText;
let roundThinking = "";
let roundText = "";
let roundToolCalls: ToolCall[] = [];
let isThinking = false;
@@ -1973,6 +2058,8 @@ export class OllamaChat extends ChatCommand {
console.log("CHUNK", chunk);
if (message.thinking) {
roundThinking += message.thinking;
if (!isThinking) {
await bot.editMessageText({
chat_id: chatId,
@@ -2008,8 +2095,8 @@ export class OllamaChat extends ChatCommand {
}
}
if (message.tool_calls && message.tool_calls.length > 0) {
roundToolCalls = message.tool_calls;
if (message.tool_calls?.length) {
roundToolCalls.push(...message.tool_calls);
}
if (currentText.length > TELEGRAM_MESSAGE_LIMIT) {
@@ -2057,6 +2144,7 @@ export class OllamaChat extends ChatCommand {
chatMessages.push({
role: "assistant",
content: roundText,
thinking: roundThinking || undefined,
tool_calls: roundToolCalls
});
+418 -118
View File
@@ -1,36 +1,224 @@
import fs from "node:fs";
import path from "node:path";
import {parse as parseDotEnv} from "dotenv";
import {z} from "zod";
import {saveData} from "../db/database";
import {Answers} from "../model/answers";
import {ifTrue} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {ImageHandleFallbackPolicy, ImageHandlePolicy, RateLimitFallbackPolicy} from "./policies";
type EnvRecord = Record<string, string>;
type StringEnumLike = Record<string, string>;
type StringEnumValue<T extends StringEnumLike> = T[keyof T];
function normalizeString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
const optionalStringSchema = z
.preprocess(normalizeString, z.string().optional())
.optional()
.catch(undefined);
function stringWithDefaultSchema(defaultValue: string) {
return z
.preprocess(value => {
const normalized = normalizeString(value);
return normalized ?? defaultValue;
}, z.string())
.default(defaultValue)
.catch(defaultValue);
}
function booleanWithDefaultSchema(defaultValue: boolean) {
return z
.preprocess(value => {
const normalized = normalizeString(value);
if (normalized === undefined) {
return defaultValue;
}
return ifTrue(normalized);
}, z.boolean())
.default(defaultValue)
.catch(defaultValue);
}
const optionalBooleanSchema = z
.preprocess(value => {
const normalized = normalizeString(value);
return normalized === undefined ? undefined : ifTrue(normalized);
}, z.boolean().optional())
.optional()
.catch(undefined);
function numberWithDefaultSchema(defaultValue: number) {
return z
.preprocess(value => {
const normalized = normalizeString(value);
if (normalized === undefined) {
return defaultValue;
}
const number = Number(normalized);
return Number.isFinite(number) ? number : defaultValue;
}, z.number())
.catch(defaultValue);
}
function positiveIntWithDefaultSchema(defaultValue: number) {
return z
.preprocess(value => {
const normalized = normalizeString(value);
if (normalized === undefined) {
return defaultValue;
}
const number = Number(normalized);
if (!Number.isSafeInteger(number) || number <= 0) {
return defaultValue;
}
return number;
}, z.number().int().positive())
.default(defaultValue)
.catch(defaultValue);
}
function enumWithDefaultSchema<T extends StringEnumLike>(
enumObject: T,
defaultValue: StringEnumValue<T>,
) {
const values = Object.values(enumObject) as StringEnumValue<T>[];
return z
.preprocess(value => {
const normalized = normalizeString(value);
if (normalized === undefined) {
return defaultValue;
}
return values.includes(normalized as StringEnumValue<T>)
? normalized
: defaultValue;
}, z.custom<StringEnumValue<T>>((value): value is StringEnumValue<T> => {
return typeof value === "string"
&& values.includes(value as StringEnumValue<T>);
}))
.default(defaultValue)
.catch(defaultValue);
}
const StartupEnvSchema = z.object({
BOT_TOKEN: stringWithDefaultSchema(""),
TEST_ENVIRONMENT: booleanWithDefaultSchema(false),
IS_DOCKER: optionalBooleanSchema,
});
const RuntimeEnvSchema = z.object({
CREATOR_ID: numberWithDefaultSchema(0),
BOT_PREFIX: stringWithDefaultSchema(""),
CHAT_IDS_WHITELIST: optionalStringSchema,
ONLY_FOR_CREATOR_MODE: booleanWithDefaultSchema(false),
ENABLE_UNSAFE_EVAL: booleanWithDefaultSchema(false),
MAX_PHOTO_SIZE: positiveIntWithDefaultSchema(1280),
PROCESS_LINKS: booleanWithDefaultSchema(false),
RATE_LIMIT_FALLBACK_POLICY: enumWithDefaultSchema(
RateLimitFallbackPolicy,
RateLimitFallbackPolicy.NOTIFY_USER,
),
IMAGE_HANDLE_POLICY: enumWithDefaultSchema(
ImageHandlePolicy,
ImageHandlePolicy.HANDLE_IF_CAPABLE,
),
IMAGE_HANDLE_FALLBACK_POLICY: enumWithDefaultSchema(
ImageHandleFallbackPolicy,
ImageHandleFallbackPolicy.NOTIFY_USER,
),
BRAVE_SEARCH_API_KEY: optionalStringSchema,
OPEN_WEATHER_MAP_API_KEY: optionalStringSchema,
FILE_TOOLS_ROOT_DIR: optionalStringSchema,
DEFAULT_AI_PROVIDER: enumWithDefaultSchema(
AiProvider,
AiProvider.OLLAMA,
),
USE_NAMES_IN_PROMPT: booleanWithDefaultSchema(false),
USE_SYSTEM_PROMPT: booleanWithDefaultSchema(true),
SEND_TIME_TOOK: optionalBooleanSchema,
OLLAMA_API_KEY: optionalStringSchema,
OLLAMA_ADDRESS: optionalStringSchema,
OLLAMA_MODEL: stringWithDefaultSchema("gemma3:4b"),
OLLAMA_IMAGE_MODEL: optionalStringSchema,
OLLAMA_THINK_MODEL: optionalStringSchema,
GEMINI_API_KEY: optionalStringSchema,
GEMINI_MODEL: stringWithDefaultSchema("gemini-2.5-flash-lite"),
GEMINI_IMAGE_MODEL: stringWithDefaultSchema("gemini-2.5-flash-image"),
MISTRAL_API_KEY: optionalStringSchema,
MISTRAL_MODEL: stringWithDefaultSchema("mistral-tiny-latest"),
OPENAI_BASE_URL: optionalStringSchema,
OPENAI_API_KEY: optionalStringSchema,
OPENAI_MODEL: stringWithDefaultSchema("gpt-4.1-nano"),
OPENAI_IMAGE_MODEL: stringWithDefaultSchema("gpt-image-1-mini"),
});
type StartupEnv = z.infer<typeof StartupEnvSchema>;
type RuntimeEnv = z.infer<typeof RuntimeEnvSchema>;
export class Environment {
static BOT_TOKEN: string;
static TEST_ENVIRONMENT: boolean;
private static readonly ENV_FILE_PATH = path.resolve(".env");
private static lastEnvMtimeMs: number | undefined;
private static lastSystemPromptMtimeMs: number | undefined;
static BOT_TOKEN: string = "";
static TEST_ENVIRONMENT: boolean = false;
static ADMIN_IDS: Set<number> = new Set<number>();
static MUTED_IDS: Set<number> = new Set<number>();
static CHAT_IDS_WHITELIST: Set<number> = new Set<number>();
static BOT_PREFIX: string;
static CREATOR_ID: number;
static IS_DOCKER: boolean;
static DATA_PATH: string;
static BOT_PREFIX: string = "";
static CREATOR_ID: number = 0;
static IS_DOCKER: boolean = false;
static DATA_PATH: string = "data";
static DB_FILE_NAME: string = "database.db";
static DB_PATH: string;
static DB_PATH: string = "file:" + path.join(Environment.DATA_PATH, Environment.DB_FILE_NAME);
static ONLY_FOR_CREATOR_MODE: boolean;
static ONLY_FOR_CREATOR_MODE: boolean = false;
static ENABLE_UNSAFE_EVAL: boolean;
static ENABLE_UNSAFE_EVAL: boolean = false;
static ANSWERS: Answers;
static MAX_PHOTO_SIZE: number;
static MAX_PHOTO_SIZE: number = 1280;
static PROCESS_LINKS: boolean;
static PROCESS_LINKS: boolean = false;
static RATE_LIMIT_FALLBACK_POLICY: RateLimitFallbackPolicy;
static IMAGE_HANDLE_POLICY: ImageHandlePolicy;
static IMAGE_HANDLE_FALLBACK_POLICY: ImageHandleFallbackPolicy;
static RATE_LIMIT_FALLBACK_POLICY: RateLimitFallbackPolicy = RateLimitFallbackPolicy.NOTIFY_USER;
static IMAGE_HANDLE_POLICY: ImageHandlePolicy = ImageHandlePolicy.HANDLE_IF_CAPABLE;
static IMAGE_HANDLE_FALLBACK_POLICY: ImageHandleFallbackPolicy = ImageHandleFallbackPolicy.NOTIFY_USER;
static BRAVE_SEARCH_API_KEY?: string;
static OPEN_WEATHER_MAP_API_KEY?: string;
@@ -38,30 +226,30 @@ export class Environment {
static FILE_TOOLS_ROOT_DIR?: string;
// AI Stuff
static DEFAULT_AI_PROVIDER: AiProvider;
static DEFAULT_AI_PROVIDER: AiProvider = AiProvider.OLLAMA;
static SYSTEM_PROMPT?: string;
static USE_NAMES_IN_PROMPT: boolean;
static USE_SYSTEM_PROMPT: boolean;
static SEND_TIME_TOOK: boolean;
static USE_NAMES_IN_PROMPT: boolean = false;
static USE_SYSTEM_PROMPT: boolean = true;
static SEND_TIME_TOOK: boolean = false;
static OLLAMA_API_KEY?: string;
static OLLAMA_ADDRESS?: string;
static OLLAMA_MODEL?: string;
static OLLAMA_IMAGE_MODEL?: string;
static OLLAMA_THINK_MODEL?: string;
static OLLAMA_MODEL: string = "gemma3:4b";
static OLLAMA_IMAGE_MODEL: string = Environment.OLLAMA_MODEL;
static OLLAMA_THINK_MODEL: string = Environment.OLLAMA_MODEL;
static GEMINI_API_KEY?: string;
static GEMINI_MODEL: string;
static GEMINI_IMAGE_MODEL: string;
static GEMINI_MODEL: string = "gemini-2.5-flash-lite";
static GEMINI_IMAGE_MODEL: string = "gemini-2.5-flash-image";
static MISTRAL_API_KEY?: string;
static MISTRAL_MODEL: string;
static MISTRAL_MODEL: string = "mistral-tiny-latest";
static OPENAI_BASE_URL?: string;
static OPENAI_API_KEY?: string;
static OPENAI_MODEL: string;
static OPENAI_IMAGE_MODEL: string;
static OPENAI_MODEL: string = "gpt-4.1-nano";
static OPENAI_IMAGE_MODEL: string = "gpt-image-1-mini";
static errorText = "⚠️ Произошла ошибка.";
static waitText = "⏳ Секунду...";
@@ -72,106 +260,212 @@ export class Environment {
static genImageText = "👨‍🎨 Генерирую изображение...";
static ollamaCancelledText = "```Ollama\n❌ Отменено```";
static load() {
Environment.BOT_TOKEN = <string>process.env.BOT_TOKEN;
Environment.TEST_ENVIRONMENT = ifTrue(process.env.TEST_ENVIRONMENT);
Environment.CHAT_IDS_WHITELIST = new Set(process.env.CHAT_IDS_WHITELIST?.split(",")?.map(e => parseInt(e.trim(), 10)) || []);
Environment.BOT_PREFIX = process.env.BOT_PREFIX || "";
Environment.CREATOR_ID = parseInt(process.env.CREATOR_ID || "");
Environment.IS_DOCKER = ifTrue(process.env.IS_DOCKER);
Environment.DATA_PATH = Environment.IS_DOCKER ? "/" + path.join("config", "data") : "data";
Environment.DB_PATH = "file:" + path.join(Environment.DATA_PATH, Environment.DB_FILE_NAME);
Environment.ONLY_FOR_CREATOR_MODE = ifTrue(process.env.ONLY_FOR_CREATOR_MODE);
Environment.ENABLE_UNSAFE_EVAL = ifTrue(process.env.ENABLE_UNSAFE_EVAL);
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280");
Environment.PROCESS_LINKS = ifTrue(process.env.PROCESS_LINKS);
const rateLimitFallbackPolicy = process.env.RATE_LIMIT_FALLBACK_POLICY || "NOTIFY_USER";
if (Object.values(RateLimitFallbackPolicy).includes(rateLimitFallbackPolicy as RateLimitFallbackPolicy)) {
Environment.RATE_LIMIT_FALLBACK_POLICY = rateLimitFallbackPolicy as RateLimitFallbackPolicy;
} else {
Environment.RATE_LIMIT_FALLBACK_POLICY = RateLimitFallbackPolicy.NOTIFY_USER;
private static processEnvAsRecord(): EnvRecord {
return Object.fromEntries(
Object.entries(process.env)
.filter((entry): entry is [string, string] => typeof entry[1] === "string"),
);
}
const imageHandlePolicy = process.env.IMAGE_HANDLE_POLICY || "HANDLE_IF_CAPABLE";
if (Object.values(ImageHandlePolicy).includes(imageHandlePolicy as ImageHandlePolicy)) {
Environment.IMAGE_HANDLE_POLICY = imageHandlePolicy as ImageHandlePolicy;
} else {
Environment.IMAGE_HANDLE_POLICY = ImageHandlePolicy.HANDLE_IF_CAPABLE;
private static parseNumberSet(value: string | undefined): Set<number> {
if (!value) {
return new Set<number>();
}
const imageHandleFallbackPolicy = process.env.IMAGE_HANDLE_FALLBACK_POLICY || "NOTIFY_USER";
if (Object.values(ImageHandleFallbackPolicy).includes(imageHandleFallbackPolicy as ImageHandleFallbackPolicy)) {
Environment.IMAGE_HANDLE_FALLBACK_POLICY = imageHandleFallbackPolicy as ImageHandleFallbackPolicy;
} else {
Environment.IMAGE_HANDLE_FALLBACK_POLICY = ImageHandleFallbackPolicy.NOTIFY_USER;
const numbers = value
.split(",")
.map(e => Number.parseInt(e.trim(), 10))
.filter(Number.isSafeInteger);
return new Set<number>(numbers);
}
Environment.BRAVE_SEARCH_API_KEY = process.env.BRAVE_SEARCH_API_KEY;
Environment.OPEN_WEATHER_MAP_API_KEY = process.env.OPEN_WEATHER_MAP_API_KEY;
Environment.FILE_TOOLS_ROOT_DIR = process.env.FILE_TOOLS_ROOT_DIR;
const aiProvider = process.env.DEFAULT_AI_PROVIDER || "OLLAMA";
if (Object.values(AiProvider).includes(aiProvider as AiProvider)) {
Environment.DEFAULT_AI_PROVIDER = aiProvider as AiProvider;
} else {
Environment.DEFAULT_AI_PROVIDER = AiProvider.OLLAMA;
private static getFileMtimeMs(filePath: string): number | undefined {
try {
return fs.statSync(filePath).mtimeMs;
} catch (e: any) {
if (e?.code === "ENOENT") {
return undefined;
}
Environment.USE_NAMES_IN_PROMPT = ifTrue(process.env.USE_NAMES_IN_PROMPT);
Environment.USE_SYSTEM_PROMPT = ifTrue(process.env.USE_SYSTEM_PROMPT || "true");
Environment.SEND_TIME_TOOK = ifTrue(process.env.SEND_TOOK_TIME || "false");
Environment.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY;
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS;
Environment.OLLAMA_MODEL = process.env.OLLAMA_MODEL || "gemma3:4b";
Environment.OLLAMA_IMAGE_MODEL = process.env.OLLAMA_IMAGE_MODEL || Environment.OLLAMA_MODEL;
Environment.OLLAMA_THINK_MODEL = process.env.OLLAMA_THINK_MODEL || Environment.OLLAMA_MODEL;
Environment.GEMINI_API_KEY = process.env.GEMINI_API_KEY;
Environment.GEMINI_MODEL = process.env.GEMINI_MODEL || "gemini-2.5-flash-lite";
Environment.GEMINI_IMAGE_MODEL = process.env.GEMINI_IMAGE_MODEL || "gemini-2.5-flash-image";
Environment.MISTRAL_API_KEY = process.env.MISTRAL_API_KEY;
Environment.MISTRAL_MODEL = process.env.MISTRAL_MODEL || "mistral-tiny-latest";
Environment.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL;
Environment.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
Environment.OPENAI_MODEL = process.env.OPENAI_MODEL || "gpt-4.1-nano";
Environment.OPENAI_IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || "gpt-image-1-mini";
throw e;
}
}
static setOnlyForCreatorMode(enable: boolean) {
private static readEnvFile(): EnvRecord {
if (!fs.existsSync(Environment.ENV_FILE_PATH)) {
return {};
}
const envFile = fs.readFileSync(Environment.ENV_FILE_PATH, "utf8");
return parseDotEnv(envFile);
}
private static readConfigSource(): EnvRecord {
return {
...Environment.processEnvAsRecord(),
...Environment.readEnvFile(),
};
}
private static getSystemPromptPath(): string {
return path.join(Environment.DATA_PATH, "system_prompt.txt");
}
private static readSystemPrompt(): string | undefined {
const promptPath = Environment.getSystemPromptPath();
if (!fs.existsSync(promptPath)) {
return undefined;
}
const prompt = fs.readFileSync(promptPath, "utf8").trim();
return prompt.length > 0 ? prompt : undefined;
}
private static applyStartupEnv(env: StartupEnv): void {
Environment.BOT_TOKEN = env.BOT_TOKEN;
Environment.TEST_ENVIRONMENT = env.TEST_ENVIRONMENT;
Environment.IS_DOCKER = env.IS_DOCKER ?? false;
Environment.DATA_PATH = Environment.IS_DOCKER
? "/" + path.join("config", "data")
: "data";
Environment.DB_PATH = "file:" + path.join(
Environment.DATA_PATH,
Environment.DB_FILE_NAME,
);
}
private static applyRuntimeEnv(env: RuntimeEnv): void {
Environment.CHAT_IDS_WHITELIST = Environment.parseNumberSet(env.CHAT_IDS_WHITELIST);
Environment.BOT_PREFIX = env.BOT_PREFIX;
Environment.CREATOR_ID = env.CREATOR_ID;
Environment.ONLY_FOR_CREATOR_MODE = env.ONLY_FOR_CREATOR_MODE;
Environment.ENABLE_UNSAFE_EVAL = env.ENABLE_UNSAFE_EVAL;
Environment.MAX_PHOTO_SIZE = env.MAX_PHOTO_SIZE;
Environment.PROCESS_LINKS = env.PROCESS_LINKS;
Environment.RATE_LIMIT_FALLBACK_POLICY = env.RATE_LIMIT_FALLBACK_POLICY;
Environment.IMAGE_HANDLE_POLICY = env.IMAGE_HANDLE_POLICY;
Environment.IMAGE_HANDLE_FALLBACK_POLICY = env.IMAGE_HANDLE_FALLBACK_POLICY;
Environment.BRAVE_SEARCH_API_KEY = env.BRAVE_SEARCH_API_KEY;
Environment.OPEN_WEATHER_MAP_API_KEY = env.OPEN_WEATHER_MAP_API_KEY;
Environment.FILE_TOOLS_ROOT_DIR = env.FILE_TOOLS_ROOT_DIR
? path.resolve(env.FILE_TOOLS_ROOT_DIR)
: undefined;
Environment.DEFAULT_AI_PROVIDER = env.DEFAULT_AI_PROVIDER;
Environment.USE_NAMES_IN_PROMPT = env.USE_NAMES_IN_PROMPT;
Environment.USE_SYSTEM_PROMPT = env.USE_SYSTEM_PROMPT;
Environment.SEND_TIME_TOOK = env.SEND_TIME_TOOK ?? false;
Environment.OLLAMA_API_KEY = env.OLLAMA_API_KEY;
Environment.OLLAMA_ADDRESS = env.OLLAMA_ADDRESS;
Environment.OLLAMA_MODEL = env.OLLAMA_MODEL;
Environment.OLLAMA_IMAGE_MODEL = env.OLLAMA_IMAGE_MODEL ?? env.OLLAMA_MODEL;
Environment.OLLAMA_THINK_MODEL = env.OLLAMA_THINK_MODEL ?? env.OLLAMA_MODEL;
Environment.GEMINI_API_KEY = env.GEMINI_API_KEY;
Environment.GEMINI_MODEL = env.GEMINI_MODEL;
Environment.GEMINI_IMAGE_MODEL = env.GEMINI_IMAGE_MODEL;
Environment.MISTRAL_API_KEY = env.MISTRAL_API_KEY;
Environment.MISTRAL_MODEL = env.MISTRAL_MODEL;
Environment.OPENAI_BASE_URL = env.OPENAI_BASE_URL;
Environment.OPENAI_API_KEY = env.OPENAI_API_KEY;
Environment.OPENAI_MODEL = env.OPENAI_MODEL;
Environment.OPENAI_IMAGE_MODEL = env.OPENAI_IMAGE_MODEL;
}
static load(): void {
const rawEnv = Environment.readConfigSource();
const startupEnv = StartupEnvSchema.parse(rawEnv);
const runtimeEnv = RuntimeEnvSchema.parse(rawEnv);
Environment.applyStartupEnv(startupEnv);
Environment.applyRuntimeEnv(runtimeEnv);
Environment.SYSTEM_PROMPT = Environment.readSystemPrompt();
Environment.lastEnvMtimeMs = Environment.getFileMtimeMs(Environment.ENV_FILE_PATH);
Environment.lastSystemPromptMtimeMs = Environment.getFileMtimeMs(Environment.getSystemPromptPath());
}
static reloadRuntimeConfigIfChanged(): void {
try {
const envMtimeMs = Environment.getFileMtimeMs(Environment.ENV_FILE_PATH);
const systemPromptMtimeMs = Environment.getFileMtimeMs(Environment.getSystemPromptPath());
const envChanged = envMtimeMs !== Environment.lastEnvMtimeMs;
const systemPromptChanged = systemPromptMtimeMs !== Environment.lastSystemPromptMtimeMs;
if (!envChanged && !systemPromptChanged) {
return;
}
if (envChanged) {
const rawEnv = Environment.readConfigSource();
const runtimeEnv = RuntimeEnvSchema.parse(rawEnv);
Environment.applyRuntimeEnv(runtimeEnv);
Environment.lastEnvMtimeMs = envMtimeMs;
}
if (systemPromptChanged) {
Environment.SYSTEM_PROMPT = Environment.readSystemPrompt();
Environment.lastSystemPromptMtimeMs = systemPromptMtimeMs;
}
} catch (e) {
console.error("Failed to reload runtime environment config", e);
}
}
static setOnlyForCreatorMode(enable: boolean): void {
this.ONLY_FOR_CREATOR_MODE = enable;
}
static setSystemPrompt(prompt: string | undefined) {
static setBraveSearchApiKey(apiKey: string | undefined): void {
this.BRAVE_SEARCH_API_KEY = apiKey;
}
static setOpenWeatherMapApiKey(openWeatherMapApiKey: string | undefined): void {
this.OPEN_WEATHER_MAP_API_KEY = openWeatherMapApiKey;
}
static setFileToolsRootDir(rootDir: string | undefined): void {
this.FILE_TOOLS_ROOT_DIR = rootDir ? path.resolve(rootDir) : undefined;
}
static setSystemPrompt(prompt: string | undefined): void {
this.SYSTEM_PROMPT = prompt;
}
static setUseNamesInPrompt(use: boolean) {
static setUseNamesInPrompt(use: boolean): void {
this.USE_NAMES_IN_PROMPT = use;
}
static setUseSystemPrompt(use: boolean) {
static setUseSystemPrompt(use: boolean): void {
this.USE_SYSTEM_PROMPT = use;
}
static setSendTimeTook(send: boolean) {
static setSendTimeTook(send: boolean): void {
this.SEND_TIME_TOOK = send;
}
static setAdmins(admins: Set<number>) {
static setAdmins(admins: Set<number>): void {
this.ADMIN_IDS = admins;
}
static async addAdmin(id: number): Promise<boolean> {
const has = this.ADMIN_IDS.has(id);
if (!has) {
this.ADMIN_IDS.add(id);
await saveData();
@@ -182,6 +476,7 @@ export class Environment {
static async removeAdmin(id: number): Promise<boolean> {
const has = this.ADMIN_IDS.has(id);
if (has) {
this.ADMIN_IDS.delete(id);
await saveData();
@@ -190,82 +485,87 @@ export class Environment {
return has;
}
static setMuted(muted: Set<number>) {
static setMuted(muted: Set<number>): void {
this.MUTED_IDS = muted;
}
static async addMute(id: number): Promise<boolean> {
if (this.MUTED_IDS.has(id)) return Promise.resolve(false);
if (this.MUTED_IDS.has(id)) {
return false;
}
this.MUTED_IDS.add(id);
await saveData();
return Promise.resolve(true);
return true;
}
static async removeMute(id: number): Promise<boolean> {
if (!this.MUTED_IDS.has(id)) return Promise.resolve(false);
this.MUTED_IDS.delete(id);
await saveData();
return Promise.resolve(true);
if (!this.MUTED_IDS.has(id)) {
return false;
}
static setAnswers(answers: Answers) {
this.MUTED_IDS.delete(id);
await saveData();
return true;
}
static setAnswers(answers: Answers): void {
this.ANSWERS = answers;
}
static setOllamaApiKey(key: string) {
static setOllamaApiKey(key: string | undefined): void {
this.OLLAMA_API_KEY = key;
}
static setOllamaAddress(address: string) {
static setOllamaAddress(address: string | undefined): void {
this.OLLAMA_ADDRESS = address;
}
static setOllamaModel(ollamaModel: string) {
static setOllamaModel(ollamaModel: string): void {
this.OLLAMA_MODEL = ollamaModel;
}
static setOllamaThinkModel(ollamaThinkModel: string) {
static setOllamaThinkModel(ollamaThinkModel: string): void {
this.OLLAMA_THINK_MODEL = ollamaThinkModel;
}
static setOllamaImageModel(ollamaImageModel: string) {
static setOllamaImageModel(ollamaImageModel: string): void {
this.OLLAMA_IMAGE_MODEL = ollamaImageModel;
}
static setGeminiApiKey(geminiApiKey: string) {
static setGeminiApiKey(geminiApiKey: string | undefined): void {
this.GEMINI_API_KEY = geminiApiKey;
}
static setGeminiModel(newModel: string) {
static setGeminiModel(newModel: string): void {
this.GEMINI_MODEL = newModel;
}
static setGeminiImageModel(newImageModel: string) {
static setGeminiImageModel(newImageModel: string): void {
this.GEMINI_IMAGE_MODEL = newImageModel;
}
static setMistralApiKey(newMistralApiKey: string) {
static setMistralApiKey(newMistralApiKey: string | undefined): void {
this.MISTRAL_API_KEY = newMistralApiKey;
}
static setMistralModel(newModel: string) {
static setMistralModel(newModel: string): void {
this.MISTRAL_MODEL = newModel;
}
static setOpenAIBaseUrl(newAIBaseUrl: string) {
static setOpenAIBaseUrl(newAIBaseUrl: string | undefined): void {
this.OPENAI_BASE_URL = newAIBaseUrl;
}
static setOpenAIApiKey(newAIApiKey: string) {
static setOpenAIApiKey(newAIApiKey: string | undefined): void {
this.OPENAI_API_KEY = newAIApiKey;
}
static setOpenAIModel(newModel: string) {
static setOpenAIModel(newModel: string): void {
this.OPENAI_MODEL = newModel;
}
static setOpenAIImageModel(newImageModel: string) {
static setOpenAIImageModel(newImageModel: string): void {
this.OPENAI_IMAGE_MODEL = newImageModel;
}
}
+1 -135
View File
@@ -1831,141 +1831,7 @@ export async function processNewMessage(msg: Message): Promise<void> {
console.log("New Message", msg);
if (!msg.from) return;
const envFile: string = fs.readFileSync(".env").toString();
const env = new Map(
envFile
.split(/\r?\n/)
.filter(line => line.trim())
.map(line => {
const [key, value = ""] = line.split("=");
return [key.trim(), value] as const;
})
);
const getEnv = (key: string): string | undefined => {
return env.get(key)?.trim();
};
const onlyForCreatorMode = getEnv("ONLY_FOR_CREATOR_MODE");
const defaultAiProvider = getEnv("DEFAULT_AI_PROVIDER");
let systemPrompt: string | null = null;
try {
const promptPath = path.join(Environment.DATA_PATH, "system_prompt.txt");
if (fs.existsSync(promptPath)) {
systemPrompt = fs.readFileSync(promptPath).toString().trim();
} else {
Environment.setSystemPrompt(undefined);
}
} catch (e) {
logError(e);
}
const useNamesInPrompt = getEnv("USE_NAMES_IN_PROMPT");
const useSystemPrompt = getEnv("USE_SYSTEM_PROMPT");
const sendTimeTook = getEnv("SEND_TIME_TOOK");
const ollamaApiKey = getEnv("OLLAMA_API_KEY");
const ollamaAddress = getEnv("OLLAMA_ADDRESS");
const ollamaModel = getEnv("OLLAMA_MODEL");
const ollamaImageModel = getEnv("OLLAMA_IMAGE_MODEL");
const ollamaThinkModel = getEnv("OLLAMA_THINK_MODEL");
const geminiApiKey = getEnv("GEMINI_API_KEY");
const geminiModel = getEnv("GEMINI_MODEL");
const geminiImageModel = getEnv("GEMINI_IMAGE_MODEL");
const mistralApiKey = getEnv("MISTRAL_API_KEY");
const mistralModel = getEnv("MISTRAL_MODEL");
const openAiBaseUrl = getEnv("OPENAI_BASE_URL");
const openAiApiKey = getEnv("OPENAI_API_KEY");
const openAiModel = getEnv("OPENAI_MODEL");
const openAiImageModel = getEnv("OPENAI_IMAGE_MODEL");
if (onlyForCreatorMode) {
Environment.setOnlyForCreatorMode(ifTrue(onlyForCreatorMode));
}
if (defaultAiProvider) {
if (Object.values(AiProvider).includes(defaultAiProvider as AiProvider)) {
Environment.DEFAULT_AI_PROVIDER = defaultAiProvider as AiProvider;
} else {
Environment.DEFAULT_AI_PROVIDER = AiProvider.OLLAMA;
}
}
if (systemPrompt) {
Environment.setSystemPrompt(systemPrompt);
}
if (useNamesInPrompt) {
Environment.setUseNamesInPrompt(ifTrue(useNamesInPrompt))
}
if (useSystemPrompt) {
Environment.setUseSystemPrompt(ifTrue(useSystemPrompt));
}
if (sendTimeTook) {
Environment.setSendTimeTook(ifTrue(sendTimeTook));
}
if (ollamaApiKey) {
Environment.setOllamaApiKey(ollamaApiKey);
}
if (ollamaAddress) {
Environment.setOllamaAddress(ollamaAddress);
}
if (ollamaModel) {
Environment.setOllamaModel(ollamaModel);
}
if (ollamaImageModel) {
Environment.setOllamaImageModel(ollamaImageModel);
}
if (ollamaThinkModel) {
Environment.setOllamaThinkModel(ollamaThinkModel);
}
if (geminiApiKey) {
Environment.setGeminiApiKey(geminiApiKey);
}
if (geminiModel) {
Environment.setGeminiModel(geminiModel);
}
if (geminiImageModel) {
Environment.setGeminiImageModel(geminiImageModel);
}
if (mistralApiKey) {
Environment.setMistralApiKey(mistralApiKey);
}
if (mistralModel) {
Environment.setMistralModel(mistralModel);
}
if (openAiBaseUrl) {
Environment.setOpenAIBaseUrl(openAiBaseUrl);
}
if (openAiApiKey) {
Environment.setOpenAIApiKey(openAiApiKey);
}
if (openAiModel) {
Environment.setOpenAIModel(openAiModel);
}
if (openAiImageModel) {
Environment.setOpenAIImageModel(openAiImageModel);
}
Environment.reloadRuntimeConfigIfChanged();
let storedMsg: StoredMessage | null = null;