mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 15:04:50 +00:00
feat(backend): switch AI provider from Anthropic to GigaChat (Sber) — OAuth token caching, callAI dispatch
This commit is contained in:
parent
5ecea8fd82
commit
01aa47773e
113
backend/Code.gs
113
backend/Code.gs
@ -11,8 +11,9 @@
|
|||||||
*
|
*
|
||||||
* Script Properties (Project Settings → Script Properties):
|
* Script Properties (Project Settings → Script Properties):
|
||||||
* BOT_TOKEN — токен Telegram-бота (для проверки initData и отправки уведомлений)
|
* BOT_TOKEN — токен Telegram-бота (для проверки initData и отправки уведомлений)
|
||||||
* ANTHROPIC_API_KEY — ключ Claude API
|
* GIGACHAT_AUTH_KEY — Authorization key из кабинета developers.sber.ru (Base64 client_id:client_secret)
|
||||||
* ANTHROPIC_MODEL — опционально, модель (default: claude-haiku-4-5-20251001)
|
* GIGACHAT_MODEL — опционально, модель (default: GigaChat-Pro)
|
||||||
|
* GIGACHAT_SCOPE — опционально, scope (default: GIGACHAT_API_PERS)
|
||||||
* ADMIN_TG_ID — tg_id куратора, кому слать алерты об ошибках
|
* ADMIN_TG_ID — tg_id куратора, кому слать алерты об ошибках
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -259,7 +260,7 @@ function handlePodbor(body) {
|
|||||||
// Build prompt and call Claude
|
// Build prompt and call Claude
|
||||||
const measurement = measurementId ? getMeasurement(measurementId) : null;
|
const measurement = measurementId ? getMeasurement(measurementId) : null;
|
||||||
const prompt = buildPickerPrompt(checklist, measurement, clientName);
|
const prompt = buildPickerPrompt(checklist, measurement, clientName);
|
||||||
const ai = callClaude(prompt);
|
const ai = callAI(prompt);
|
||||||
|
|
||||||
// Update lead row with AI response
|
// Update lead row with AI response
|
||||||
updateLeadAI(id, ai);
|
updateLeadAI(id, ai);
|
||||||
@ -459,7 +460,7 @@ function updateLeadAI(leadId, ai) {
|
|||||||
for (let i = 1; i < data.length; i++) {
|
for (let i = 1; i < data.length; i++) {
|
||||||
if (data[i][0] === leadId) {
|
if (data[i][0] === leadId) {
|
||||||
const props = PropertiesService.getScriptProperties();
|
const props = PropertiesService.getScriptProperties();
|
||||||
const model = props.getProperty("ANTHROPIC_MODEL") || "claude-haiku-4-5-20251001";
|
const model = props.getProperty("GIGACHAT_MODEL") || "GigaChat-Pro";
|
||||||
s.getRange(i + 1, 8).setValue(JSON.stringify(ai.json || ai.text || ""));
|
s.getRange(i + 1, 8).setValue(JSON.stringify(ai.json || ai.text || ""));
|
||||||
s.getRange(i + 1, 9).setValue(model);
|
s.getRange(i + 1, 9).setValue(model);
|
||||||
s.getRange(i + 1, 10).setValue(ai.tokens || 0);
|
s.getRange(i + 1, 10).setValue(ai.tokens || 0);
|
||||||
@ -544,26 +545,82 @@ function buildPickerPrompt(checklist, measurement, clientName) {
|
|||||||
return "Подбери технику для следующего клиента:\n\n" + JSON.stringify(payload, null, 2);
|
return "Подбери технику для следующего клиента:\n\n" + JSON.stringify(payload, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function callClaude(userPrompt) {
|
// =================================================================
|
||||||
|
// GigaChat (Sber) — primary AI provider
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает access_token GigaChat. Кеширует в CacheService на 25 минут
|
||||||
|
* (токен живёт 30 минут — оставляем 5 минут запаса).
|
||||||
|
*/
|
||||||
|
function getGigaChatToken() {
|
||||||
|
const cache = CacheService.getScriptCache();
|
||||||
|
const cached = cache.get("gigachat_token");
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
const props = PropertiesService.getScriptProperties();
|
const props = PropertiesService.getScriptProperties();
|
||||||
const apiKey = props.getProperty("ANTHROPIC_API_KEY");
|
const authKey = props.getProperty("GIGACHAT_AUTH_KEY");
|
||||||
if (!apiKey) throw new Error("ANTHROPIC_API_KEY not in Script Properties");
|
if (!authKey) throw new Error("GIGACHAT_AUTH_KEY not in Script Properties");
|
||||||
// Стабильное имя без даты — Anthropic сам резолвит до свежего снапшота
|
const scope = props.getProperty("GIGACHAT_SCOPE") || "GIGACHAT_API_PERS";
|
||||||
const model = props.getProperty("ANTHROPIC_MODEL") || "claude-haiku-4-5";
|
|
||||||
|
const res = UrlFetchApp.fetch("https://ngw.devices.sberbank.ru:9443/api/v2/oauth", {
|
||||||
|
method: "post",
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Basic " + authKey,
|
||||||
|
"RqUID": Utilities.getUuid(),
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
contentType: "application/x-www-form-urlencoded",
|
||||||
|
payload: "scope=" + encodeURIComponent(scope),
|
||||||
|
muteHttpExceptions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = res.getResponseCode();
|
||||||
|
const text = res.getContentText();
|
||||||
|
if (status >= 400) {
|
||||||
|
log("gigachat_auth_error", null, { status, text: text.slice(0, 400) });
|
||||||
|
throw new Error("GigaChat auth HTTP " + status + ": " + text.slice(0, 300));
|
||||||
|
}
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
const token = data.access_token || data.tok;
|
||||||
|
if (!token) throw new Error("No access_token in GigaChat response: " + text.slice(0, 300));
|
||||||
|
|
||||||
|
cache.put("gigachat_token", token, 1500); // 25 минут
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вызов AI (GigaChat Chat Completions). Возвращает { json, text, tokens, error? }.
|
||||||
|
*/
|
||||||
|
function callAI(userPrompt) {
|
||||||
|
const props = PropertiesService.getScriptProperties();
|
||||||
|
const model = props.getProperty("GIGACHAT_MODEL") || "GigaChat-Pro";
|
||||||
const temperature = parseFloat(getSetting("AI_TEMPERATURE") || "0.3");
|
const temperature = parseFloat(getSetting("AI_TEMPERATURE") || "0.3");
|
||||||
|
|
||||||
|
let token;
|
||||||
|
try {
|
||||||
|
token = getGigaChatToken();
|
||||||
|
} catch (e) {
|
||||||
|
return { json: null, text: "AI auth: " + e.message, tokens: 0, error: true };
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
model,
|
model,
|
||||||
max_tokens: 4000,
|
|
||||||
temperature,
|
temperature,
|
||||||
system: SYSTEM_PROMPT_PICKER,
|
max_tokens: 4000,
|
||||||
messages: [{ role: "user", content: userPrompt }],
|
messages: [
|
||||||
|
{ role: "system", content: SYSTEM_PROMPT_PICKER },
|
||||||
|
{ role: "user", content: userPrompt },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
|
const res = UrlFetchApp.fetch("https://gigachat.devices.sberbank.ru/api/v1/chat/completions", {
|
||||||
method: "post",
|
method: "post",
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
||||||
payload: JSON.stringify(payload),
|
payload: JSON.stringify(payload),
|
||||||
muteHttpExceptions: true,
|
muteHttpExceptions: true,
|
||||||
});
|
});
|
||||||
@ -571,20 +628,27 @@ function callClaude(userPrompt) {
|
|||||||
const status = res.getResponseCode();
|
const status = res.getResponseCode();
|
||||||
const text = res.getContentText();
|
const text = res.getContentText();
|
||||||
if (status >= 400) {
|
if (status >= 400) {
|
||||||
log("claude_error", null, { status, model, text: text.slice(0, 500) });
|
log("gigachat_error", null, { status, model, text: text.slice(0, 500) });
|
||||||
// Surface the actual Anthropic error so we can debug from /api/test_claude
|
|
||||||
let errMsg = text.slice(0, 300);
|
let errMsg = text.slice(0, 300);
|
||||||
try {
|
try {
|
||||||
const j = JSON.parse(text);
|
const j = JSON.parse(text);
|
||||||
errMsg = (j.error && j.error.message) || errMsg;
|
errMsg = (j.message) || (j.error && j.error.message) || errMsg;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return { json: null, text: "AI ошибка HTTP " + status + " (" + model + "): " + errMsg, tokens: 0, error: true };
|
return { json: null, text: "AI ошибка HTTP " + status + " (" + model + "): " + errMsg, tokens: 0, error: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
const responseText = (data.content || []).map(c => c.text || "").join("");
|
const choice = (data.choices && data.choices[0]) || {};
|
||||||
const tokens = (data.usage && (data.usage.input_tokens + data.usage.output_tokens)) || 0;
|
const responseText = (choice.message && choice.message.content) || "";
|
||||||
|
const tokens = (data.usage && data.usage.total_tokens) || 0;
|
||||||
|
|
||||||
|
// Иногда модель оборачивает JSON в ```json ... ```
|
||||||
let json = null;
|
let json = null;
|
||||||
try { json = JSON.parse(responseText); } catch (e) {}
|
try { json = JSON.parse(responseText); } catch (e) {
|
||||||
|
const stripped = responseText.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
||||||
|
try { json = JSON.parse(stripped); } catch (e2) {}
|
||||||
|
}
|
||||||
|
|
||||||
return { json, text: responseText, tokens };
|
return { json, text: responseText, tokens };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -702,9 +766,12 @@ function seedAdminAsManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Тестовый прогон Claude API: проверяет что ключ работает. */
|
/** Тестовый прогон Claude API: проверяет что ключ работает. */
|
||||||
function testClaude() {
|
function testClaude() { return testAI(); } // legacy alias
|
||||||
const ai = callClaude("Скажи одной фразой: что за фабрика ЗОВ?");
|
|
||||||
return { ok: !ai.error, response_text: (ai.text || "").slice(0, 500), tokens: ai.tokens, model: PropertiesService.getScriptProperties().getProperty("ANTHROPIC_MODEL") || "claude-haiku-4-5-20251001" };
|
function testAI() {
|
||||||
|
const ai = callAI("Скажи одной фразой: что за фабрика ЗОВ?");
|
||||||
|
const model = PropertiesService.getScriptProperties().getProperty("GIGACHAT_MODEL") || "GigaChat-Pro";
|
||||||
|
return { ok: !ai.error, provider: "GigaChat", model, response_text: (ai.text || "").slice(0, 500), tokens: ai.tokens };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Тест отправки сообщения через бота — пришлёт админу «привет». */
|
/** Тест отправки сообщения через бота — пришлёт админу «привет». */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user