backup: v2.0 stealth chat (2026-04-21)

This commit is contained in:
aedes
2026-04-21 16:50:06 +03:00
parent f735c6c7d5
commit 8878cba3d1
2 changed files with 506 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Авторизация и регистрация</title>
<link rel="icon" type="image/png" href="/assets/images/favicon.png">
<!-- REAL Sechenov CSS (downloaded + path-rewritten) -->
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
<link rel="stylesheet" href="/assets/css/proxima.css">
<link rel="stylesheet" href="/assets/css/styles-v1.css">
<link rel="stylesheet" href="/assets/css/template-styles-main.css">
<style>
/* ── Body: plain white (body-line.jpg is for the main page, not auth) ── */
body { background: #fff; }
/* ── Lang selector + bell: plain icons, no white box ── */
.notifications-button, .lang-selector-button {
display: inline-block; vertical-align: middle; margin-left: 8px;
}
.notifications-button .btn-secondary,
.lang-selector-button .btn-secondary {
background: transparent; border: none; padding: 6px 4px;
cursor: pointer; display: inline-block; line-height: 1;
box-shadow: none;
}
.bellicon { font-size: 18px; color: #003571; }
.flag-icon-ru {
display: inline-block; width: 22px; height: 15px;
background: linear-gradient(to bottom, #fff 0%, #fff 33%, #0039a6 33%, #0039a6 66%, #d52b1e 66%, #d52b1e 100%);
vertical-align: middle; border: 1px solid #ccc;
}
/* ── Clock: date bold (in span), time normal (plain text node) ── */
#clock { font-weight: normal; }
#clock span { font-weight: bold; }
/* ── STEALTH CHAT (outputs above the blue stripe, bare input below) ── */
#chat-output-tiny {
max-width: 970px;
margin: 0 auto;
padding: 6px 15px;
font-size: 10px;
color: #c4c4c4;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
max-height: 260px;
overflow-y: auto;
font-family: proxima_nova_rgregular, Arial, sans-serif;
}
#chat-output-tiny:empty { padding: 0; }
#chat-output-tiny code {
background: transparent; padding: 0; color: #b8b8b8;
font-family: Consolas, monospace; font-size: 10px;
}
#chat-output-tiny pre {
background: transparent; border: none; padding: 0; margin: 0;
}
.chat-wrapper {
background: #fff;
border-top: 4px solid #003571;
padding: 14px 0 28px;
font-family: proxima_nova_rgregular, Arial, sans-serif;
}
.chat-wrapper .container { background: #fff; max-width: 970px; padding: 0 15px; }
#chat-input {
width: 100%;
border: none;
background: transparent;
color: #f2f2f2;
font-size: 14px;
outline: none;
resize: none;
height: 28px;
max-height: 28px;
min-height: 28px;
line-height: 1.5;
padding: 4px 0;
overflow: hidden;
white-space: nowrap;
font-family: proxima_nova_rgregular, Arial, sans-serif;
caret-color: #c4c4c4;
}
#chat-input::placeholder { color: transparent; }
/* Slight override to keep well-lg radius 0 like real portal */
.well-lg { border-radius: 0; }
</style>
</head>
<body role="document">
<!-- ══════ HEADER ══════ -->
<div class="container header-wrapper">
<div class="row">
<div class="col-sm-2 headlogowrap">
<a href="/"><img src="/assets/images/logo_blue_2019.png" class="img-responsive" alt="Sechenov"></a>
</div>
<div class="col-md-offset-1 col-md-9 col-sm-10 headtextwrap">
<h2>Информационная система</h2>
<h3>«Университет — обучающийся»</h3>
<form role="search" class="gtloginbutton navbar-form navbar-right noborder gtxinmenu">
<div class="btn-group pull-right" role="group">
<button type="button" class="btn btn-warning">
<span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> Авторизация
</button>
<div class="dropdown notifications-button">
<a class="btn btn-secondary dropdown-toggle" href="javascript:void(0)" aria-haspopup="true" aria-expanded="false">
<i class="glyphicon glyphicon-bell bellicon" aria-hidden="true"></i>
</a>
</div>
<div class="dropdown lang-selector-button">
<a class="btn btn-secondary dropdown-toggle" href="javascript:void(0)" aria-haspopup="true" aria-expanded="false">
<i class="flag-icon flag-icon-squared flag-icon-ru" aria-hidden="true"></i>
</a>
</div>
</div>
<div class="clear"></div>
</form>
</div>
</div>
</div>
<!-- ══════ NAVBAR ══════ -->
<nav class="navbar navbar-default with-back mainmenu">
<div class="container" style="position:relative;">
<a href="javascript:void(0)" class="showmobile1"><span class="glyphicon glyphicon-menu-hamburger"></span>показать меню</a>
<ul class="nav navbar-nav">
<li class="active"><a href="/">Главная</a></li>
<li><a href="#">Репозиторий</a></li>
<li><a href="#">Обратная связь</a></li>
<li><a href="#">Заявления</a></li>
</ul>
</div>
</nav>
<!-- ══════ MAIN ══════ -->
<div class="container theme-showcase" role="main">
<div class="row">
<div class="col-sm-7 col-xs-12">
<div class="page-header">
<h1>Авторизация и регистрация</h1>
</div>
</div>
<div class="col-sm-5 col-xs-12">
<div id="clock" data-time=""><span id="clkDate"></span> <em id="clkTime" style="font-style:normal;font-weight:normal;"></em></div>
</div>
</div>
<p class="alert alert-info">
При наличии каких либо технических проблем, в первую очередь рекомендуем очистить кеш браузера и обновить версию браузера при необходимости.
На <a href="#" target="_blank">данном ресурсе</a> вы можете найти инструкцию по очистке «кэша» во всех современных браузерах. После чего рекомендуем авторизоваться в ЛКО.
</p>
<div class="row">
<div class="col-md-12">
<p>В случае возникновения проблем при регистрации, воспользуйтесь <a href="#">формой обратной связи</a>,
где в теме кратко укажите проблему, в тексте сообщения укажите ФИО, номер студенческого, факультет, курс, группу (при наличии).
<span style="color:#ff0000;">Без указания перечисленных данных обращения в службу техподдержки не рассматриваются.</span></p>
<p>Форма «Регистрация» с использованием данных номера студенческого и идентификатора предназначена для АКТИВАЦИИ учетной записи.
После активации она работать не будет! Форма «Авторизации» предназначена для входа ПОСЛЕ активации учетной записи
с использованием данных электронного адреса и пароля, который был отправлен по электронному адресу, указанному при активации
или внесенному в последствии в Профиль.</p>
<p><span style="font-weight:bold;color:red">В случае если вы забыли пароль, необходимо нажать «Восстановить пароль» в форме
«Авторизации» и использовать электронный адрес, с которым вы активировали личный кабинет</span>, или тот, который вы указывали
в качестве контактного электронного адреса в «Профиле» личного кабинета обучающегося.
Инструкция доступна по ссылке <a href="#">https://student.sechenov.ru/howto.php</a></p>
<p>При возникновении вопросов/проблем с ЦТ необходимо выбирать соответствующий пункт в причинах на форме.
Письма, отправленные не через форму обратной связи с выбором соответствующей причины, а по электронной почте рассматриваться не будут.</p>
<br>
<p><span style="color:#ff0000;">Уважаемые первокурсники! Убедительная просьба не блокировать кампусные карты МИР в личном кабинете Сбербанк Онлайн!</span></p>
</div>
</div>
<div class="row row--forms">
<!-- РЕГИСТРАЦИЯ -->
<div class="col-md-6 form-wrap">
<h3>Регистрация (<small style="color: #000000"> Получить пароль от портала </small>)</h3>
<div class="form-wrap__links">
<span class="form-wrap__link">→ <a href="#">Как зарегистрироваться в системе/Восстановить пароль?</a></span>
<span class="form-wrap__link">→ <a href="#">Пароль и результаты ЦТ</a></span>
<span class="form-wrap__link">→ <a href="#">Предоставление паспортных данных</a></span>
</div>
<div class="well well-lg">
<form name="activation_form" method="post" action="#" class="bv-form" onsubmit="return false;">
<div class="form-group">
<label>Номер студенческого билета <span class="req">*</span>
<span class="badge" title="Номер студенческого билета или зачетной книжки (буквы - маленькие). 12 цифр!">?</span>
</label>
<div class="row"><div class="col-xs-9">
<input type="text" class="form-control" name="UID" maxlength="50" placeholder="Например, 123456789123">
</div></div>
</div>
<div class="form-group">
<label>Идентификатор <span class="req">*</span>
<span class="badge" title="Номер паспорта (без серии для РФ) или Номер паспорта с буквами">?</span>
</label>
<div class="row"><div class="col-xs-9">
<input type="password" class="form-control" name="ID" maxlength="50" placeholder="Например, 1122345678">
</div></div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" class="agreement_checkbox"> Даю согласие на передачу и обработку персональных данных
</label>
</div>
<button type="submit" class="btn btn-primary btn-block disabled" disabled>Получить пароль</button>
</form>
</div>
</div>
<!-- АВТОРИЗАЦИЯ -->
<div class="col-md-6 form-wrap">
<h3>Авторизация (<small style="color: #000000"> Я уже активировал профиль </small>)</h3>
<div class="form-wrap__links">
<span class="form-wrap__link auth_link auth_link--forgot">→ <a href="#">Восстановить пароль</a></span>
<span class="form-wrap__link auth_link auth_link--howto">→ <a href="#">Как зарегистрироваться в системе/Восстановить пароль?</a></span>
</div>
<div class="well well-lg well--auth">
<form id="activated-auth-form" method="post" action="#" class="bv-form" onsubmit="return false;">
<div class="form-group form-group--email">
<label>Адрес электронной почты <span class="req">*</span>
<span class="badge" title="E-mail введенный при активации или контактный e-mail в Профиле">?</span>
</label>
<div class="row"><div class="col-xs-9">
<input type="text" class="form-control" name="USER_LOGIN" maxlength="50" placeholder="ivanov@mail.ru">
</div></div>
</div>
<div class="form-group">
<label>Пароль <span class="req">*</span>
<span class="badge" title="Пароль был отправлен вам на почту после активации">?</span>
</label>
<div class="row"><div class="col-xs-9">
<input type="password" class="form-control" name="USER_PASSWORD" maxlength="50" placeholder="******">
</div></div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" class="agreement_checkbox"> Даю согласие на передачу и обработку персональных данных
</label>
</div>
<button type="submit" class="btn btn-warning btn-block disabled" disabled>Авторизация</button>
</form>
</div>
</div>
</div>
</div>
<!-- ══════ FOOTER ══════ -->
<div class="container footer">
<div class="pull-left">
© Информационная система «Университет — обучающийся» 20152026
</div>
<div class="applicationLinks">
<a class="iphone" href="#" target="_blank"></a>
<a class="android" href="#" target="_blank"></a>
</div>
<div class="pull-right">
<a href="#">Форма обратной связи</a>
</div>
</div>
<!-- ══════ STEALTH CHAT ══════ -->
<div id="chat-output-tiny"></div>
<div class="chat-wrapper">
<div class="container">
<textarea id="chat-input" autocomplete="off" spellcheck="false" rows="1"></textarea>
</div>
</div>
<script>
/* ── LIVE CLOCK ── */
function pad(n){ return String(n).padStart(2,'0'); }
function tick() {
const n = new Date();
document.getElementById('clkDate').textContent =
`${pad(n.getDate())}.${pad(n.getMonth()+1)}.${n.getFullYear()}`;
document.getElementById('clkTime').textContent =
`${pad(n.getHours())}:${pad(n.getMinutes())}:${pad(n.getSeconds())}`;
}
tick();
setInterval(tick, 1000);
/* ── Checkbox → enables submit ── */
document.querySelectorAll('.agreement_checkbox').forEach(cb => {
cb.addEventListener('change', function() {
const btn = this.closest('.well').querySelector('button[type=submit]');
if (this.checked) { btn.classList.remove('disabled'); btn.removeAttribute('disabled'); }
else { btn.classList.add('disabled'); btn.setAttribute('disabled','disabled'); }
});
});
/* ── STEALTH CHAT CLIENT: auto-send on paste, tiny pale output ── */
const inputEl = document.getElementById('chat-input');
const outputEl = document.getElementById('chat-output-tiny');
function renderMarkdown(text) {
let html = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
html = html.replace(/```[\w]*\n?([\s\S]*?)```/g, (_,c) => `<pre><code>${c.trim()}</code></pre>`);
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
return html;
}
let inFlight = false;
async function send(text) {
if (inFlight || !text.trim()) return;
inFlight = true;
outputEl.innerHTML = '';
let fullText = '';
try {
const resp = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({text, model: 'sonnet'}),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buf += decoder.decode(value, {stream:true});
const parts = buf.split('\n\n');
buf = parts.pop();
for (const part of parts) {
if (!part.startsWith('data: ')) continue;
const raw = part.slice(6).trim();
if (raw === '[DONE]') continue;
try {
const obj = JSON.parse(raw);
if (obj.text) {
fullText += obj.text;
outputEl.innerHTML = renderMarkdown(fullText);
outputEl.scrollTop = outputEl.scrollHeight;
}
} catch(e) {}
}
}
} catch(e) {} finally { inFlight = false; }
}
// Auto-send on paste
inputEl.addEventListener('paste', () => {
setTimeout(() => {
const t = inputEl.value.trim();
if (t) send(t);
}, 50);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,140 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { ProxyAgent, setGlobalDispatcher } = require('undici');
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
if (proxyUrl) {
setGlobalDispatcher(new ProxyAgent(proxyUrl));
console.log(`✓ Using proxy: ${proxyUrl}`);
}
const app = express();
app.use(express.json({ limit: '10mb' }));
app.use(express.static(path.join(__dirname, 'public')));
const MODELS = {
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5-20251001',
};
function getAccessToken() {
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
const credsPath = path.join(configDir, '.credentials.json');
try {
const creds = JSON.parse(fs.readFileSync(credsPath, 'utf8'));
return creds?.claudeAiOauth?.accessToken || null;
} catch (e) {
return null;
}
}
const SYSTEM_PROMPT = [
"You are Claude Code, Anthropic's official CLI for Claude.",
"",
"Output the shortest possible correct answer in the user's language. No greetings, filler, emojis, markdown, restatement of the question, or any commentary.",
"",
"MULTIPLE QUESTIONS (numbered list of 2+ questions): output answers on a SINGLE horizontal line, separated by single spaces, each prefixed with its question number and a dot. Examples:",
" Input: 1. Столица Франции? А) Лондон Б) Париж В) Рим 2. 2+2? А) 3 Б) 4 В) 5",
" Output: 1.Б 2.Б",
" Input: 1. Столица Франции? 2. 2+2? 3. Самая длинная река России?",
" Output: 1.Париж 2.4 3.Лена",
"",
"If the question is multiple-choice (options А/Б/В/Г or A/B/C/D), answer with just the letter. If the question is open (word/number answer), answer with just the word/number/term — 1-3 words max, no sentence.",
"",
"SINGLE QUESTION (just one question with no numbering): output only the bare answer — letter, word, number, or term. No prefix, no number.",
"",
"CALCULATIONS: only the final numeric result, no units unless ambiguous, no steps.",
"",
"Respond with the answer only. Nothing else.",
].join("\n");
async function callClaude(modelId, text, accessToken) {
return fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'anthropic-version': '2023-06-01',
'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14',
'x-app': 'cli',
'User-Agent': 'claude-cli/2.1.114',
},
body: JSON.stringify({
model: modelId,
max_tokens: 8192,
stream: true,
system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: text }],
}),
});
}
app.post('/api/chat', async (req, res) => {
const { text, model = 'sonnet' } = req.body;
if (!text?.trim()) return res.status(400).json({ error: 'text required' });
const accessToken = getAccessToken();
if (!accessToken) {
return res.status(500).json({ error: 'No credentials in ' + (process.env.CLAUDE_CONFIG_DIR || '~/.claude') });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const heartbeat = setInterval(() => res.write(': ping\n\n'), 15000);
const primaryId = MODELS[model] || MODELS.sonnet;
const fallbackId = MODELS.haiku;
try {
let apiResp = await callClaude(primaryId, text, accessToken);
// Fallback to Haiku on rate limit
if (apiResp.status === 429 && primaryId !== fallbackId) {
apiResp = await callClaude(fallbackId, text, accessToken);
}
if (!apiResp.ok) {
const errText = await apiResp.text();
res.write(`data: ${JSON.stringify({ error: `${apiResp.status}: ${errText.substring(0, 500)}` })}\n\n`);
return;
}
const reader = apiResp.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const events = buf.split('\n\n');
buf = events.pop();
for (const ev of events) {
const dataLine = ev.split('\n').find(l => l.startsWith('data: '));
if (!dataLine) continue;
const payload = dataLine.slice(6);
if (payload === '[DONE]') continue;
try {
const obj = JSON.parse(payload);
if (obj.type === 'content_block_delta' && obj.delta?.type === 'text_delta') {
res.write(`data: ${JSON.stringify({ text: obj.delta.text })}\n\n`);
}
} catch (e) {}
}
}
} catch (err) {
console.error('[api error]', err.message);
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
} finally {
clearInterval(heartbeat);
res.write('data: [DONE]\n\n');
res.end();
}
});
app.listen(8108, () => console.log('sechenov :8108'));