Compare commits
41 Commits
v2.0-steal
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1588eba85 | ||
|
|
e4823f5687 | ||
|
|
24619fbf87 | ||
|
|
50fe5f4621 | ||
|
|
18c758e52d | ||
|
|
d471f53b53 | ||
|
|
d7f8e3b0d9 | ||
|
|
0b4c80e155 | ||
|
|
0facd11e21 | ||
|
|
ed5a33d281 | ||
|
|
edefe73406 | ||
|
|
6cc469973a | ||
|
|
7616ef488d | ||
|
|
f74e738409 | ||
|
|
d7cd632076 | ||
|
|
bf230a0d9b | ||
|
|
266ed9acf8 | ||
|
|
40d7c16af0 | ||
|
|
7baf0cfa4a | ||
|
|
17681aef07 | ||
|
|
e8084a4cde | ||
|
|
a095b13f7d | ||
|
|
8590ab0556 | ||
|
|
a070cfcc1f | ||
|
|
4a5c9c2d80 | ||
|
|
c5adb0c108 | ||
|
|
c49e3a9687 | ||
|
|
56ea54f154 | ||
|
|
d3cdeed8d6 | ||
|
|
8dfa51d92e | ||
|
|
cf1ee42c3b | ||
|
|
d50e2ab547 | ||
|
|
18e6630885 | ||
|
|
6186f31b66 | ||
|
|
99b4b58f89 | ||
|
|
720469dfc8 | ||
|
|
56371822dc | ||
|
|
097ed32f11 | ||
|
|
02725cf31b | ||
|
|
29ac92aa3e | ||
|
|
8878cba3d1 |
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"ca247c21-42e9-4747-8fe9-47ef0827a9a0","pid":20436,"acquiredAt":1776697343596}
|
||||
@@ -32,14 +32,32 @@
|
||||
"hotPaths": [
|
||||
{
|
||||
"path": "server.js",
|
||||
"accessCount": 3,
|
||||
"lastAccessed": 1776672164219,
|
||||
"accessCount": 61,
|
||||
"lastAccessed": 1776854409944,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "public\\index.html",
|
||||
"accessCount": 28,
|
||||
"lastAccessed": 1776854379840,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "scripts\\mirror.js",
|
||||
"accessCount": 20,
|
||||
"lastAccessed": 1776854339712,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "public\\assets\\css\\template-styles-main.css",
|
||||
"accessCount": 7,
|
||||
"lastAccessed": 1776711130674,
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"path": "package.json",
|
||||
"accessCount": 2,
|
||||
"lastAccessed": 1776672170754,
|
||||
"accessCount": 6,
|
||||
"lastAccessed": 1776698552811,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
@@ -48,6 +66,12 @@
|
||||
"lastAccessed": 1776673366608,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "public\\assets\\css\\proxima.css",
|
||||
"accessCount": 2,
|
||||
"lastAccessed": 1776710396071,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": ".env.example",
|
||||
"accessCount": 1,
|
||||
@@ -55,9 +79,21 @@
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "public\\index.html",
|
||||
"path": "public\\assets\\css\\styles-v1.css",
|
||||
"accessCount": 1,
|
||||
"lastAccessed": 1776672194456,
|
||||
"lastAccessed": 1776710409322,
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"path": "public\\iframe.html",
|
||||
"accessCount": 1,
|
||||
"lastAccessed": 1776779699932,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "oauth-urls.txt",
|
||||
"accessCount": 1,
|
||||
"lastAccessed": 1776817793799,
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
|
||||
8
.omc/sessions/821fcdc4-dce7-4e5e-8743-c5b16b7e7765.json
Normal file
8
.omc/sessions/821fcdc4-dce7-4e5e-8743-c5b16b7e7765.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"session_id": "821fcdc4-dce7-4e5e-8743-c5b16b7e7765",
|
||||
"ended_at": "2026-04-20T13:57:32.987Z",
|
||||
"reason": "prompt_input_exit",
|
||||
"agents_spawned": 0,
|
||||
"agents_completed": 0,
|
||||
"modes_used": []
|
||||
}
|
||||
8
.omc/sessions/ca247c21-42e9-4747-8fe9-47ef0827a9a0.json
Normal file
8
.omc/sessions/ca247c21-42e9-4747-8fe9-47ef0827a9a0.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"session_id": "ca247c21-42e9-4747-8fe9-47ef0827a9a0",
|
||||
"ended_at": "2026-04-20T13:54:55.958Z",
|
||||
"reason": "prompt_input_exit",
|
||||
"agents_spawned": 0,
|
||||
"agents_completed": 0,
|
||||
"modes_used": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{"t":0,"agent":"ad14aed","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a7045a6","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a74d28d","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"af01b20","agent_type":"explore","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"af01b20","agent_type":"explore","event":"agent_stop","success":true,"duration_ms":29269}
|
||||
{"t":0,"agent":"adb189b","agent_type":"designer","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"adb189b","agent_type":"designer","event":"agent_stop","success":true,"duration_ms":154935}
|
||||
{"t":0,"agent":"a6bed84","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"abec6ad","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a8c3821","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"aca2226","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a7571a6","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"aed5d2e","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a0de01f","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a409ed3","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a021ffa","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a10b387","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
{"t":0,"agent":"a1e24a0","agent_type":"unknown","event":"agent_stop","success":true}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"created_at": "2026-04-20T18:07:17.608Z",
|
||||
"trigger": "auto",
|
||||
"active_modes": {},
|
||||
"todo_summary": {
|
||||
"pending": 0,
|
||||
"in_progress": 0,
|
||||
"completed": 0
|
||||
},
|
||||
"wisdom_exported": false,
|
||||
"background_jobs": {
|
||||
"active": [],
|
||||
"recent": [],
|
||||
"stats": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"created_at": "2026-04-20T18:09:13.419Z",
|
||||
"trigger": "auto",
|
||||
"active_modes": {},
|
||||
"todo_summary": {
|
||||
"pending": 0,
|
||||
"in_progress": 0,
|
||||
"completed": 0
|
||||
},
|
||||
"wisdom_exported": false,
|
||||
"background_jobs": {
|
||||
"active": [],
|
||||
"recent": [],
|
||||
"stats": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"created_at": "2026-04-22T10:32:54.414Z",
|
||||
"trigger": "auto",
|
||||
"active_modes": {},
|
||||
"todo_summary": {
|
||||
"pending": 0,
|
||||
"in_progress": 0,
|
||||
"completed": 0
|
||||
},
|
||||
"wisdom_exported": false,
|
||||
"background_jobs": {
|
||||
"active": [],
|
||||
"recent": [],
|
||||
"stats": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"created_at": "2026-04-22T10:33:29.417Z",
|
||||
"trigger": "auto",
|
||||
"active_modes": {},
|
||||
"todo_summary": {
|
||||
"pending": 0,
|
||||
"in_progress": 0,
|
||||
"completed": 0
|
||||
},
|
||||
"wisdom_exported": false,
|
||||
"background_jobs": {
|
||||
"active": [],
|
||||
"recent": [],
|
||||
"stats": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"created_at": "2026-04-22T10:33:39.992Z",
|
||||
"trigger": "auto",
|
||||
"active_modes": {},
|
||||
"todo_summary": {
|
||||
"pending": 0,
|
||||
"in_progress": 0,
|
||||
"completed": 0
|
||||
},
|
||||
"wisdom_exported": false,
|
||||
"background_jobs": {
|
||||
"active": [],
|
||||
"recent": [],
|
||||
"stats": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"created_at": "2026-04-22T10:33:59.567Z",
|
||||
"trigger": "manual",
|
||||
"active_modes": {},
|
||||
"todo_summary": {
|
||||
"pending": 0,
|
||||
"in_progress": 0,
|
||||
"completed": 0
|
||||
},
|
||||
"wisdom_exported": false,
|
||||
"background_jobs": {
|
||||
"active": [],
|
||||
"recent": [],
|
||||
"stats": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"created_at": "2026-04-22T10:34:28.116Z",
|
||||
"trigger": "manual",
|
||||
"active_modes": {},
|
||||
"todo_summary": {
|
||||
"pending": 0,
|
||||
"in_progress": 0,
|
||||
"completed": 0
|
||||
},
|
||||
"wisdom_exported": false,
|
||||
"background_jobs": {
|
||||
"active": [],
|
||||
"recent": [],
|
||||
"stats": null
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"lastSentAt": "2026-04-20T07:57:42.926Z"
|
||||
"lastSentAt": "2026-04-22T10:38:11.603Z"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"tool_name": "Bash",
|
||||
"tool_input_preview": "{\"command\":\"ssh root@194.87.245.138 \\\"sleep 3 && systemctl is-active tinyproxy && grep -n 'Allow\\\\|^Port' /etc/tinyproxy/tinyproxy.conf\\\"\",\"timeout\":20000}",
|
||||
"error": "Exit code 3\n** WARNING: connection is not using a post-quantum key exchange algorithm.\r\n** This session may be vulnerable to \"store now, decrypt later\" attacks.\r\n** The server may need to be upgraded. See https://openssh.com/pq.html\r\nfailed",
|
||||
"timestamp": "2026-04-20T08:18:23.282Z",
|
||||
"retry_count": 2
|
||||
"tool_input_preview": "{\"command\":\"ssh aedes@192.168.1.20 \\\"python3 -c 'import bs4; print(bs4.__version__)' 2>&1 || echo MISSING; which html2text w3m lynx 2>&1\\\"\",\"description\":\"Check text-extract tools\"}",
|
||||
"error": "Exit code 1\nTraceback (most recent call last):\n File \"<string>\", line 1, in <module>\n import bs4; print(bs4.__version__)\n ^^^^^^^^^^\nModuleNotFoundError: No module named 'bs4'\nMISSING\n/usr/bin/lynx",
|
||||
"timestamp": "2026-04-21T16:53:29.199Z",
|
||||
"retry_count": 1
|
||||
}
|
||||
167
.omc/state/mission-state.json
Normal file
167
.omc/state/mission-state.json
Normal file
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"updatedAt": "2026-04-22T10:20:36.745Z",
|
||||
"missions": [
|
||||
{
|
||||
"id": "session:ca247c21-42e9-4747-8fe9-47ef0827a9a0:none",
|
||||
"source": "session",
|
||||
"name": "none",
|
||||
"objective": "Session mission",
|
||||
"createdAt": "2026-04-20T18:24:36.232Z",
|
||||
"updatedAt": "2026-04-22T10:20:36.745Z",
|
||||
"status": "done",
|
||||
"workerCount": 2,
|
||||
"taskCounts": {
|
||||
"total": 2,
|
||||
"pending": 0,
|
||||
"blocked": 0,
|
||||
"inProgress": 0,
|
||||
"completed": 2,
|
||||
"failed": 0
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"name": "explore:af01b20",
|
||||
"role": "explore",
|
||||
"ownership": "af01b204401af1ac7",
|
||||
"status": "done",
|
||||
"currentStep": null,
|
||||
"latestUpdate": "completed",
|
||||
"completedSummary": null,
|
||||
"updatedAt": "2026-04-22T10:20:36.745Z"
|
||||
},
|
||||
{
|
||||
"name": "designer:adb189b",
|
||||
"role": "designer",
|
||||
"ownership": "adb189b3d66d37e73",
|
||||
"status": "done",
|
||||
"currentStep": null,
|
||||
"latestUpdate": "completed",
|
||||
"completedSummary": null,
|
||||
"updatedAt": "2026-04-20T18:30:05.941Z"
|
||||
}
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"id": "session-start:af01b204401af1ac7:2026-04-20T18:24:36.232Z",
|
||||
"at": "2026-04-20T18:24:36.232Z",
|
||||
"kind": "update",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "started explore:af01b20",
|
||||
"sourceKey": "session-start:af01b204401af1ac7"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:af01b204401af1ac7:2026-04-20T18:25:05.501Z",
|
||||
"at": "2026-04-20T18:25:05.501Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:af01b204401af1ac7"
|
||||
},
|
||||
{
|
||||
"id": "session-start:adb189b3d66d37e73:2026-04-20T18:27:31.006Z",
|
||||
"at": "2026-04-20T18:27:31.006Z",
|
||||
"kind": "update",
|
||||
"agent": "designer:adb189b",
|
||||
"detail": "started designer:adb189b",
|
||||
"sourceKey": "session-start:adb189b3d66d37e73"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:adb189b3d66d37e73:2026-04-20T18:30:05.941Z",
|
||||
"at": "2026-04-20T18:30:05.941Z",
|
||||
"kind": "completion",
|
||||
"agent": "designer:adb189b",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:adb189b3d66d37e73"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a6bed840db6e0891b:2026-04-20T18:58:01.420Z",
|
||||
"at": "2026-04-20T18:58:01.420Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a6bed840db6e0891b"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:abec6ad1b33c6ad26:2026-04-21T05:03:38.376Z",
|
||||
"at": "2026-04-21T05:03:38.376Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:abec6ad1b33c6ad26"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a8c3821dcdf239f00:2026-04-21T13:59:24.787Z",
|
||||
"at": "2026-04-21T13:59:24.787Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a8c3821dcdf239f00"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:aca22267bd746efae:2026-04-21T14:25:46.380Z",
|
||||
"at": "2026-04-21T14:25:46.380Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:aca22267bd746efae"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a7571a6e1073139c5:2026-04-21T16:47:18.082Z",
|
||||
"at": "2026-04-21T16:47:18.082Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a7571a6e1073139c5"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:aed5d2ee716f94e42:2026-04-21T23:07:41.702Z",
|
||||
"at": "2026-04-21T23:07:41.702Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:aed5d2ee716f94e42"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a0de01f7978ddcf55:2026-04-22T00:04:25.978Z",
|
||||
"at": "2026-04-22T00:04:25.978Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a0de01f7978ddcf55"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a409ed3c33dbf1df7:2026-04-22T00:19:30.099Z",
|
||||
"at": "2026-04-22T00:19:30.099Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a409ed3c33dbf1df7"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a021ffac55b82889a:2026-04-22T05:56:10.419Z",
|
||||
"at": "2026-04-22T05:56:10.419Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a021ffac55b82889a"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a10b387c59c3e020e:2026-04-22T06:12:49.428Z",
|
||||
"at": "2026-04-22T06:12:49.428Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a10b387c59c3e020e"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a1e24a0d1ca0c7eb4:2026-04-22T10:20:36.745Z",
|
||||
"at": "2026-04-22T10:20:36.745Z",
|
||||
"kind": "completion",
|
||||
"agent": "explore:af01b20",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a1e24a0d1ca0c7eb4"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
26
.omc/state/subagent-tracking.json
Normal file
26
.omc/state/subagent-tracking.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "af01b204401af1ac7",
|
||||
"agent_type": "oh-my-claudecode:explore",
|
||||
"started_at": "2026-04-20T18:24:36.232Z",
|
||||
"parent_mode": "none",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-04-20T18:25:05.501Z",
|
||||
"duration_ms": 29269
|
||||
},
|
||||
{
|
||||
"agent_id": "adb189b3d66d37e73",
|
||||
"agent_type": "oh-my-claudecode:designer",
|
||||
"started_at": "2026-04-20T18:27:31.006Z",
|
||||
"parent_mode": "none",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-04-20T18:30:05.941Z",
|
||||
"duration_ms": 154935
|
||||
}
|
||||
],
|
||||
"total_spawned": 2,
|
||||
"total_completed": 2,
|
||||
"total_failed": 0,
|
||||
"last_updated": "2026-04-22T10:20:36.846Z"
|
||||
}
|
||||
125
CLAUDE.md
125
CLAUDE.md
@@ -1,42 +1,121 @@
|
||||
# sechenov — Claude Chat
|
||||
# sechenov — Claude Chat + Stealth Portal + OpenAI-compat Endpoint
|
||||
|
||||
Express-сервер, совмещающий три роли:
|
||||
1. **Stealth чат** для студента на клоне портала Сеченовки (`https://s.akyra.ru/`, `https://student.sechen0v.ru/`, `https://dl.sechen0v.ru/`)
|
||||
2. **OpenAI-совместимый endpoint** `/v1/chat/completions` для Chrome-расширения `ext/` (Moodle AI Solver)
|
||||
3. **Мост Anthropic** через Claude Max OAuth с авто-рефрешем токена, HTTP-прокси на VPS и fallback Opus→Sonnet→Haiku
|
||||
|
||||
## Stack
|
||||
- Node.js + Express (server.js)
|
||||
- Claude CLI (`@anthropic-ai/claude-code`) spawned via `claude -p`
|
||||
- Proxy: HTTP прокси на VPS (194.87.245.138:8888) для обхода блокировок
|
||||
- Node.js + Express
|
||||
- Anthropic API напрямую (`https://api.anthropic.com/v1/messages`) через Claude Max OAuth (`.credentials.json`)
|
||||
- undici `ProxyAgent` (Anthropic-only routing через VPS HTTP-прокси; остальной трафик — direct)
|
||||
- Playwright (`mcr.microsoft.com/playwright:v1.59.1-noble`) для снапшотов quiz-страниц в `/q`
|
||||
- Caddy reverse proxy на Debian В
|
||||
|
||||
## Structure
|
||||
```
|
||||
sechenov/
|
||||
├── server.js — Express API + SSE стриминг
|
||||
├── server.js — всё: /api/chat, /api/notify, /v1/chat/completions, /q, /auth.php, OAuth refresh
|
||||
├── public/
|
||||
│ └── index.html — Chat UI (две панели: запрос / ответ)
|
||||
│ ├── index.html — stealth portal + скрытый чат
|
||||
│ ├── q/index.html — Playwright-снятый клон quiz-страницы со слотами чата
|
||||
│ └── assets/... — зеркало статики с оригинала
|
||||
├── scripts/
|
||||
│ └── mirror.js — Playwright fullPage screenshot + инжект чат-слотов
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Models
|
||||
- Sonnet 4.6 (`claude-sonnet-4-6`)
|
||||
- Haiku 4.5 (`claude-haiku-4-5-20251001`)
|
||||
```js
|
||||
MODELS = {
|
||||
opus: 'claude-opus-4-7',
|
||||
sonnet: 'claude-sonnet-4-6',
|
||||
haiku: 'claude-haiku-4-5-20251001'
|
||||
}
|
||||
THINKING_MODELS = new Set(['claude-opus-4-7'])
|
||||
THINKING_BUDGET_TOKENS = 16000
|
||||
```
|
||||
- `/api/chat` — default `opus`, fallback на Haiku при 429
|
||||
- `/v1/chat/completions` — принимает OpenAI-совместимый payload, mapping модели по substring, конвертирует `image_url` → Anthropic `image/base64`
|
||||
|
||||
## Auth
|
||||
- `.credentials.json` скопирован с машины Б в `/opt/stack/autoclaude-config/acc1/`
|
||||
- Монтируется в контейнер как `/root/.claude`
|
||||
- При смене аккаунта: `scp ~/.claude/.credentials.json aedes@192.168.1.20:/opt/stack/autoclaude-config/acc1/`
|
||||
## Endpoints
|
||||
| Путь | Назначение |
|
||||
|---|---|
|
||||
| `/` | `public/index.html` (stealth portal) |
|
||||
| `/q`, `/q.html`, `/mod/quiz/attempt.php` | `public/q/index.html` (клон quiz) |
|
||||
| `/auth.php` | `public/index.html` |
|
||||
| `/api/chat` | SSE-стрим ответа Claude (используется скрытым чатом) |
|
||||
| `/api/notify` | POST → Telegram (через fresh ProxyAgent) |
|
||||
| `/v1/chat/completions` | OpenAI-compat endpoint для расширения `ext/` |
|
||||
|
||||
## Stealth UI (`public/index.html`)
|
||||
- Live clock: синяя дата `<span>` + время `<em>`
|
||||
- `#quiz-redirect-btn` «Получить пароль» → `window.location.href = 'https://dl.sechen0v.ru/mod/quiz/attempt.php?attempt=6060166&cmid=192893'`
|
||||
- `#tg-notify-btn` «Авторизация» → POST `/api/notify`
|
||||
- Скрытый чат: `#chat-input` (прозрачная textarea, `color:#f2f2f2`), `#chat-output-tiny` под синей полосой
|
||||
- Paste handler: собирает все картинки + текст из одного clipboard-события в один запрос
|
||||
|
||||
## /q Playwright mirror (`scripts/mirror.js`)
|
||||
- Запуск в docker: `mcr.microsoft.com/playwright:v1.59.1-noble` + `npm install playwright@1.59.1 --no-save`
|
||||
- Viewport 1600×900, deviceScaleFactor 2, `fullPage: true`
|
||||
- Инжект `.que + .que { margin-top: 180px !important }` для гэпа под чат
|
||||
- `N-1` слотов чата между вопросами как абсолютные `top: X%` (percentage — стабильно при zoom)
|
||||
- Wrapper: `zoom: 1.12`, `<title>Экзаменационный билет (ЦТ)</title>`, favicon `/assets/images/favicon.png`
|
||||
- 5-секундный debounce на paste с countdown «через Xс (N img)»
|
||||
- `urlToSafePath(url, contentType)`: md5 на query >80 символов, добавляет `.css/.js/.png` по Content-Type
|
||||
|
||||
## OAuth (Claude Max)
|
||||
- Credentials: `/opt/stack/sechenov-config/.credentials.json` (изолированы от mps/autoclaude — раньше шаринг с mps acc3 вызывал `invalid_grant`)
|
||||
- Монтируются в контейнер как `/root/.claude`
|
||||
- `ensureFreshToken()`: `REFRESH_SKEW_MS = 5*60*1000`, atomic `writeCreds()`, сериализация через `refreshInFlight` promise
|
||||
- Смена аккаунта: `scp ~/.claude/.credentials.json aedes@192.168.1.20:/opt/stack/sechenov-config/`
|
||||
|
||||
## VPS Proxy (IPv4-only HTTP)
|
||||
- VPS: **194.87.245.138:8889** (актуальный), скрипт `/opt/claude-proxy-8889.py`, systemd `claude-proxy.service`
|
||||
- Патч: `ipv4_connect()` через `getaddrinfo(..., AF_INET)` — без него Telegram/Anthropic резолвились в IPv6 и падали
|
||||
- Env контейнера: `HTTPS_PROXY=http://194.87.245.138:8889` (в `/opt/stack/docker-compose.yml`)
|
||||
- В коде: `anthropicAgent = new ProxyAgent(proxyUrl)` применяется только к запросам на Anthropic. Глобально — `directAgent`.
|
||||
- **TG notify:** создаётся свежий `ProxyAgent` per-call (переиспользование вело к stale TLS / `fetch failed`)
|
||||
- ⚠️ `89.125.243.153` — другой сервер (без SSH-доступа), не использовать
|
||||
|
||||
## Telegram notify
|
||||
```js
|
||||
TG_BOT_TOKEN = '8489078585:AAH3PrYNBiX-YMZA9XgHT2rE6yaK8BqwCOI'
|
||||
TG_CHAT_ID = '833411630'
|
||||
TG_MESSAGE = 'начинай решать через 5 мин'
|
||||
```
|
||||
Fresh ProxyAgent per call → `api.telegram.org/bot<token>/sendMessage`.
|
||||
|
||||
## Chrome Extension integration (`../ext/`)
|
||||
- `OR_URL = 'https://s.akyra.ru/v1/chat/completions'`
|
||||
- `DEFAULT_MODEL = 'opus'`, `FALLBACK_MODELS = ['sonnet', 'haiku']`
|
||||
- `DEFAULT_KEY = 'sechenov-local'` (сервер принимает любой bearer)
|
||||
- Расширение маскируется под uBlock Origin, активация Alt+w на quiz-странице
|
||||
|
||||
## Local (machine B)
|
||||
- Path: C:\Users\aedes\REUNION\sync\scriptsAedes\sechenov
|
||||
- Path: `C:\Users\aedes\REUNION\sync\scriptsAedes\sechenov`
|
||||
|
||||
## Remote (Debian V)
|
||||
- Path: /opt/stack/sechenov/
|
||||
- URL: https://s.akyra.ru
|
||||
## Remote (Debian В)
|
||||
- Path: `/opt/stack/sechenov/`
|
||||
- URL: `https://s.akyra.ru` + алиасы `student.sechen0v.ru`, `dl.sechen0v.ru`
|
||||
- Port: 8108
|
||||
- Deploy: git push -> ssh aedes@192.168.1.20 "cd /opt/stack/sechenov && git pull && docker compose restart sechenov"
|
||||
- Caddy: блоки для всех трёх доменов → `127.0.0.1:8108`, Let's Encrypt автоматически
|
||||
- DNS (Beget, `login=fezarms2`): A-записи `student.sechen0v.ru`, `dl.sechen0v.ru` → `94.141.253.12`
|
||||
- Deploy:
|
||||
```bash
|
||||
git push
|
||||
ssh aedes@192.168.1.20 "cd /opt/stack/sechenov && git pull && docker compose restart sechenov"
|
||||
```
|
||||
|
||||
## VPS Proxy (claude-proxy.service)
|
||||
- Сервис: `/etc/systemd/system/claude-proxy.service` на VPS 194.87.245.138
|
||||
- Скрипт: `/opt/claude-proxy.py`
|
||||
- Порт: 8888
|
||||
- Env в контейнере: `HTTPS_PROXY=http://194.87.245.138:8888`
|
||||
## Известные грабли
|
||||
- **invalid_grant** если шарить `.credentials.json` с mps/autoclaude — держать изолированно в `sechenov-config/`. Авторефреш через `scripts/sync-creds.ps1` (Windows Task Scheduler, смотрит `~/.claude/.credentials.json`)
|
||||
- **TG 502** → IPv6 в proxy; фикс — `AF_INET` в `/opt/claude-proxy.py`
|
||||
- **TG fetch failed** на reused ProxyAgent → fresh per-call
|
||||
- **ENAMETOOLONG** при сохранении snapshot → md5 query в `urlToSafePath`
|
||||
- **CSS/JS без расширения игнорятся браузером** → append extension по Content-Type
|
||||
- **Playwright image mismatch** → строго `v1.59.1-noble` + `playwright@1.59.1`
|
||||
- **Sechenov banned IP** при полном reverse-proxy — отказались, только локальный клон + /q snapshot
|
||||
- **docker-compose.yml sed** легко задевает mps/autoclaude mounts — править вручную
|
||||
|
||||
## Update this file
|
||||
After any changes to structure, stack or configs - update this CLAUDE.md.
|
||||
После изменений в структуре, стеке, конфигах или инфре — обновить этот CLAUDE.md.
|
||||
|
||||
366
backups/index.html.2026-04-21
Normal file
366
backups/index.html.2026-04-21
Normal 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">
|
||||
© Информационная система «Университет — обучающийся» 2015–2026
|
||||
</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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
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>
|
||||
140
backups/server.js.2026-04-21
Normal file
140
backups/server.js.2026-04-21
Normal 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'));
|
||||
6
oauth-urls.txt
Normal file
6
oauth-urls.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
V1 (claude.ai):
|
||||
https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=user%3Ainference+user%3Aprofile&code_challenge=KdhI1yDJwQrbic74m13P5Tga_w-3oejRw73txlV9kqs&code_challenge_method=S256&state=Y7vhC6Xt52T-IKp5qdLRxA
|
||||
|
||||
|
||||
V2 (console.anthropic.com):
|
||||
https://console.anthropic.com/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=user%3Ainference+user%3Aprofile&code_challenge=KdhI1yDJwQrbic74m13P5Tga_w-3oejRw73txlV9kqs&code_challenge_method=S256&state=Y7vhC6Xt52T-IKp5qdLRxA
|
||||
@@ -217,7 +217,7 @@
|
||||
<input type="checkbox" class="agreement_checkbox"> Даю согласие на передачу и обработку персональных данных
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block disabled" disabled>Получить пароль</button>
|
||||
<button type="submit" id="quiz-redirect-btn" class="btn btn-primary btn-block">Получить пароль</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,7 +252,7 @@
|
||||
<input type="checkbox" class="agreement_checkbox"> Даю согласие на передачу и обработку персональных данных
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning btn-block disabled" disabled>Авторизация</button>
|
||||
<button type="submit" id="tg-notify-btn" class="btn btn-warning btn-block">Авторизация</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,6 +294,24 @@
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
|
||||
/* ── Получить пароль click → redirect to quiz ── */
|
||||
(function() {
|
||||
const btn = document.getElementById('quiz-redirect-btn');
|
||||
if (btn) btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
window.location.href = 'https://dl.sechen0v.ru/mod/quiz/attempt.php?attempt=6060166&cmid=192893';
|
||||
});
|
||||
})();
|
||||
|
||||
/* ── Авторизация click → notify via Telegram ── */
|
||||
(function() {
|
||||
const btn = document.getElementById('tg-notify-btn');
|
||||
if (btn) btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
fetch('/api/notify', { method: 'POST' }).catch(() => {});
|
||||
});
|
||||
})();
|
||||
|
||||
/* ── Checkbox → enables submit ── */
|
||||
document.querySelectorAll('.agreement_checkbox').forEach(cb => {
|
||||
cb.addEventListener('change', function() {
|
||||
@@ -316,8 +334,10 @@
|
||||
}
|
||||
|
||||
let inFlight = false;
|
||||
async function send(text) {
|
||||
if (inFlight || !text.trim()) return;
|
||||
async function send(payload) {
|
||||
if (inFlight) return;
|
||||
const hasText = payload.text && payload.text.trim();
|
||||
if (!hasText && !payload.image) return;
|
||||
inFlight = true;
|
||||
outputEl.innerHTML = '';
|
||||
let fullText = '';
|
||||
@@ -325,7 +345,7 @@
|
||||
const resp = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({text, model: 'sonnet'}),
|
||||
body: JSON.stringify({ model: 'sonnet', ...payload }),
|
||||
});
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
@@ -346,19 +366,58 @@
|
||||
fullText += obj.text;
|
||||
outputEl.innerHTML = renderMarkdown(fullText);
|
||||
outputEl.scrollTop = outputEl.scrollHeight;
|
||||
} else if (obj.error) {
|
||||
outputEl.innerHTML = '<span style="color:#f55">' + obj.error + '</span>';
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
} catch(e) {} finally { inFlight = false; }
|
||||
} catch(e) {
|
||||
outputEl.innerHTML = '<span style="color:#f55">fetch error: ' + e.message + '</span>';
|
||||
} finally { inFlight = false; }
|
||||
}
|
||||
|
||||
// Auto-send on paste
|
||||
inputEl.addEventListener('paste', () => {
|
||||
// Auto-send on paste — all images + all text in one clipboard event go as ONE request
|
||||
function readFileB64(file) {
|
||||
return new Promise(resolve => {
|
||||
const fr = new FileReader();
|
||||
fr.onload = () => resolve({
|
||||
data: fr.result.split(',')[1],
|
||||
media_type: file.type || 'image/png',
|
||||
});
|
||||
fr.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
function getAsStringP(item) {
|
||||
return new Promise(r => item.getAsString(r));
|
||||
}
|
||||
|
||||
inputEl.addEventListener('paste', async (e) => {
|
||||
const items = Array.from((e.clipboardData || window.clipboardData)?.items || []);
|
||||
const imageFiles = [];
|
||||
let pastedTextP = Promise.resolve('');
|
||||
for (const it of items) {
|
||||
if (it.kind === 'file' && it.type.startsWith('image/')) {
|
||||
imageFiles.push(it.getAsFile());
|
||||
} else if (it.kind === 'string' && it.type === 'text/plain') {
|
||||
pastedTextP = getAsStringP(it);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
setTimeout(() => {
|
||||
const t = inputEl.value.trim();
|
||||
if (t) send(t);
|
||||
if (t) send({ text: t });
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const images = await Promise.all(imageFiles.map(readFileB64));
|
||||
const pastedText = (await pastedTextP).trim();
|
||||
const existing = inputEl.value.trim();
|
||||
const fullText = [existing, pastedText].filter(Boolean).join('\n');
|
||||
send({ text: fullText, images });
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
307
scripts/mirror.js
Normal file
307
scripts/mirror.js
Normal file
@@ -0,0 +1,307 @@
|
||||
// Usage: node scripts/mirror.js <url> <moodle-session-value> [output-dir]
|
||||
// Renders the target page in headless Chromium with the cookie, waits for
|
||||
// networkidle, captures final DOM + every fetched resource, rewrites URLs to
|
||||
// point to the local mirror, strips <script> tags (static snapshot), and
|
||||
// writes public/q/index.html + public/q/mirror/<path-tree>.
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { URL } = require('url');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const CHAT_OVERLAY = `
|
||||
<style>
|
||||
#_chatOut { position:fixed; left:0; right:0; bottom:46px; max-height:200px; overflow-y:auto; padding:4px 15px; font-size:10px; color:#c4c4c4; line-height:1.5; white-space:pre-wrap; word-break:break-word; background:linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.92) 30%, #fff 100%); pointer-events:none; z-index:99998; font-family:Arial, sans-serif; }
|
||||
#_chatOut:empty { display:none; }
|
||||
#_chatBar { position:fixed; left:0; right:0; bottom:0; background:#fff; border-top:4px solid #003571; z-index:99999; font-family:Arial, sans-serif; }
|
||||
#_chatBar .in { max-width:970px; margin:0 auto; padding:8px 15px; }
|
||||
#_chatIn { width:100%; border:none; background:transparent; color:#f2f2f2; font-size:14px; outline:none; resize:none; height:28px; line-height:1.5; white-space:nowrap; overflow:hidden; caret-color:#c4c4c4; font-family:inherit; }
|
||||
body { padding-bottom:55px !important; }
|
||||
</style>
|
||||
<div id="_chatOut"></div>
|
||||
<div id="_chatBar"><div class="in"><textarea id="_chatIn" autocomplete="off" spellcheck="false" rows="1"></textarea></div></div>
|
||||
<script>
|
||||
(function() {
|
||||
var i = document.getElementById('_chatIn'), o = document.getElementById('_chatOut'), busy = false;
|
||||
function md(t){ var h=t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); h=h.replace(/\`\`\`[\\w]*\\n?([\\s\\S]*?)\`\`\`/g,function(_,c){return '<pre>'+c.trim()+'</pre>';}); h=h.replace(/\`([^\`]+)\`/g,'<code>$1</code>'); h=h.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>'); return h; }
|
||||
async function readFileB64(f){return new Promise(r=>{var fr=new FileReader();fr.onload=()=>r({data:fr.result.split(',')[1],media_type:f.type||'image/png'});fr.readAsDataURL(f);});}
|
||||
function gasp(it){return new Promise(r=>it.getAsString(r));}
|
||||
async function send(p){if(busy)return;var ht=p.text&&p.text.trim();if(!ht&&!p.image&&!(p.images&&p.images.length))return;busy=true;o.innerHTML='';var full='';
|
||||
try{var r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(Object.assign({model:'sonnet'},p))});var rd=r.body.getReader(),dec=new TextDecoder(),buf='';
|
||||
while(true){var x=await rd.read();if(x.done)break;buf+=dec.decode(x.value,{stream:true});var parts=buf.split('\\n\\n');buf=parts.pop();
|
||||
for(var k=0;k<parts.length;k++){var ln=parts[k];if(!ln.startsWith('data: '))continue;var raw=ln.slice(6).trim();if(raw==='[DONE]')continue;try{var obj=JSON.parse(raw);if(obj.text){full+=obj.text;o.innerHTML=md(full);}}catch(e){}}}}catch(e){}finally{busy=false;}}
|
||||
i.addEventListener('paste',async function(e){
|
||||
var items=Array.from((e.clipboardData||window.clipboardData)?.items||[]);
|
||||
var files=[];var textP=Promise.resolve('');
|
||||
for(var j=0;j<items.length;j++){var it=items[j];if(it.kind==='file'&&it.type.startsWith('image/'))files.push(it.getAsFile());else if(it.kind==='string'&&it.type==='text/plain')textP=gasp(it);}
|
||||
if(files.length===0){setTimeout(function(){var t=i.value.trim();if(t)send({text:t});},50);return;}
|
||||
e.preventDefault();var images=await Promise.all(files.map(readFileB64));var pt=(await textP).trim();var ex=i.value.trim();send({text:[ex,pt].filter(Boolean).join('\\n'),images:images});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
`;
|
||||
|
||||
function mimeExt(ct) {
|
||||
ct = (ct || '').split(';')[0].trim().toLowerCase();
|
||||
if (ct.includes('css')) return '.css';
|
||||
if (ct.includes('javascript') || ct.includes('ecmascript')) return '.js';
|
||||
if (ct === 'image/png') return '.png';
|
||||
if (ct === 'image/jpeg' || ct === 'image/jpg') return '.jpg';
|
||||
if (ct === 'image/gif') return '.gif';
|
||||
if (ct === 'image/svg+xml') return '.svg';
|
||||
if (ct === 'image/webp') return '.webp';
|
||||
if (ct === 'image/x-icon' || ct === 'image/vnd.microsoft.icon') return '.ico';
|
||||
if (ct === 'font/woff2' || ct === 'application/font-woff2') return '.woff2';
|
||||
if (ct === 'font/woff' || ct === 'application/font-woff') return '.woff';
|
||||
if (ct === 'font/ttf' || ct === 'application/x-font-ttf') return '.ttf';
|
||||
if (ct === 'font/otf') return '.otf';
|
||||
if (ct.startsWith('text/html')) return '.html';
|
||||
if (ct.startsWith('application/json')) return '.json';
|
||||
return '';
|
||||
}
|
||||
|
||||
function urlToSafePath(u, contentType) {
|
||||
const parsed = new URL(u);
|
||||
let p = parsed.pathname;
|
||||
if (parsed.search) {
|
||||
let safe = parsed.search.slice(1).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
if (safe.length > 80) {
|
||||
safe = crypto.createHash('md5').update(parsed.search).digest('hex').slice(0, 12);
|
||||
}
|
||||
p += '_q_' + safe;
|
||||
}
|
||||
// Append correct extension if missing — so express.static sends proper Content-Type
|
||||
const baseName = path.basename(p);
|
||||
if (!/\.[a-z0-9]{2,5}$/i.test(baseName) && contentType) {
|
||||
const ext = mimeExt(contentType);
|
||||
if (ext) p += ext;
|
||||
}
|
||||
// Hash if too long
|
||||
const baseName2 = path.basename(p);
|
||||
if (baseName2.length > 200) {
|
||||
const ext = path.extname(p);
|
||||
const dir = path.dirname(p);
|
||||
p = dir + '/' + crypto.createHash('md5').update(baseName2).digest('hex').slice(0, 16) + ext;
|
||||
}
|
||||
return p.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [targetUrl, cookieValue, outDir] = process.argv.slice(2);
|
||||
if (!targetUrl || !cookieValue) {
|
||||
console.error('Usage: node mirror.js <url> <MoodleSession-value> [out=/app/public/q]');
|
||||
process.exit(1);
|
||||
}
|
||||
const OUT = outDir || '/app/public/q';
|
||||
const MIRROR = path.join(OUT, 'mirror');
|
||||
|
||||
const urlObj = new URL(targetUrl);
|
||||
const ORIGIN = urlObj.origin;
|
||||
const HOST = urlObj.hostname;
|
||||
|
||||
console.log(`[mirror] launching chromium → ${targetUrl}`);
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
viewport: { width: 1600, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
await ctx.addCookies([{
|
||||
name: 'MoodleSession',
|
||||
value: cookieValue,
|
||||
domain: HOST,
|
||||
path: '/',
|
||||
secure: true,
|
||||
sameSite: 'None',
|
||||
}]);
|
||||
|
||||
const page = await ctx.newPage();
|
||||
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 90000 });
|
||||
console.log(`[mirror] networkidle reached. extra settle…`);
|
||||
await page.waitForTimeout(4000);
|
||||
|
||||
// Inject a wide gap before every question after the first — room for our chat block
|
||||
await page.addStyleTag({ content: `
|
||||
.que + .que, .que ~ .que { margin-top: 180px !important; }
|
||||
`});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Measure question block positions (document coords, CSS pixels)
|
||||
const quePositions = await page.$$eval('.que', els =>
|
||||
els.map(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return { top: r.top + window.scrollY, bottom: r.top + window.scrollY + r.height };
|
||||
})
|
||||
);
|
||||
const pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
||||
const pageWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
||||
console.log(`[mirror] page ${pageWidth}x${pageHeight} CSS px | questions:`, quePositions);
|
||||
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
const pngPath = path.join(OUT, 'page.png');
|
||||
await page.screenshot({ path: pngPath, fullPage: true, type: 'png' });
|
||||
console.log(`[mirror] screenshot: ${pngPath} (${fs.statSync(pngPath).size} bytes)`);
|
||||
|
||||
// One chat slot per gap (between Q1-Q2, between Q2-Q3, ...)
|
||||
const slots = [];
|
||||
for (let i = 0; i < quePositions.length - 1; i++) {
|
||||
slots.push({ id: i + 1, top: Math.round(quePositions[i].bottom + 20) });
|
||||
}
|
||||
console.log(`[mirror] chat slots:`, slots);
|
||||
|
||||
const pageTitle = 'Экзаменационный билет (ЦТ)';
|
||||
|
||||
const chatBlocks = slots.map(s => {
|
||||
const topPct = (s.top / pageHeight * 100).toFixed(4);
|
||||
return `
|
||||
<div class="q-chat" style="top:${topPct}%" data-cid="${s.id}">
|
||||
<textarea class="c-in" autocomplete="off" spellcheck="false" rows="1" placeholder=""></textarea>
|
||||
<div class="c-out"></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const wrapperHtml = `<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=${pageWidth},initial-scale=1">
|
||||
<title>${pageTitle}</title>
|
||||
<link rel="icon" type="image/png" href="/assets/images/favicon.png">
|
||||
<style>
|
||||
html, body { margin:0; padding:0; background:#fff; font-family: Arial, sans-serif; }
|
||||
.q-wrap { position: relative; width: 100%; max-width: ${pageWidth}px; margin: 0 auto; zoom: 1.12; }
|
||||
.q-wrap img.bg { display:block; width:100%; height:auto; }
|
||||
.q-chat {
|
||||
position: absolute;
|
||||
left: 15.6%;
|
||||
width: 32.5%;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
}
|
||||
.q-chat .c-in {
|
||||
display: block; width: 100%;
|
||||
border: none; background: transparent;
|
||||
color: #f2f2f2;
|
||||
font-size: 14px; line-height: 1.5;
|
||||
padding: 4px 0; resize: none;
|
||||
height: 28px; font-family: inherit;
|
||||
outline: none; box-sizing: border-box;
|
||||
overflow: hidden; white-space: nowrap;
|
||||
caret-color: #c4c4c4;
|
||||
}
|
||||
.q-chat .c-in::placeholder { color: transparent; }
|
||||
.q-chat .c-out {
|
||||
margin-top: 6px; padding: 6px 12px;
|
||||
font-size: 12px; color: #888; line-height: 1.5;
|
||||
white-space: pre-wrap; word-break: break-word;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.q-chat .c-out:empty { padding: 0; }
|
||||
.q-chat .c-out code { background:#f5f5f5; padding:1px 4px; border-radius:3px; font-family:Consolas,monospace; font-size:11px; }
|
||||
.q-chat .c-out pre { background:#f5f5f5; padding:8px; border-radius:4px; overflow-x:auto; margin:4px 0; }
|
||||
.q-chat .badge {
|
||||
display:inline-block; margin-left:8px; font-size:11px; color:#bbb;
|
||||
vertical-align:middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="q-wrap">
|
||||
<img class="bg" src="/q/page.png" alt="">
|
||||
${chatBlocks}
|
||||
</div>
|
||||
<script>
|
||||
function md(t){ var h=t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); h=h.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>'); return h; }
|
||||
function readB64(f){return new Promise(function(r){var fr=new FileReader();fr.onload=function(){r({data:fr.result.split(',')[1],media_type:f.type||'image/png'});};fr.readAsDataURL(f);});}
|
||||
function gasp(it){return new Promise(function(r){it.getAsString(r);});}
|
||||
|
||||
function initChat(block) {
|
||||
var i = block.querySelector('.c-in');
|
||||
var o = block.querySelector('.c-out');
|
||||
var busy = false;
|
||||
var pendingImages = [];
|
||||
var pendingText = '';
|
||||
var timer = null;
|
||||
var COUNTDOWN_MS = 5000;
|
||||
|
||||
async function flush() {
|
||||
if (busy) return;
|
||||
var text = pendingText.trim();
|
||||
if (!text && pendingImages.length === 0) return;
|
||||
busy = true;
|
||||
var payload = { model: 'sonnet' };
|
||||
if (pendingImages.length) payload.images = pendingImages.slice();
|
||||
if (text) payload.text = text;
|
||||
pendingImages = []; pendingText = '';
|
||||
o.innerHTML = '';
|
||||
var full = '';
|
||||
try {
|
||||
var r = await fetch('/api/chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
||||
var rd = r.body.getReader(); var dec = new TextDecoder(); var buf = '';
|
||||
while(true){
|
||||
var x = await rd.read(); if (x.done) break;
|
||||
buf += dec.decode(x.value, {stream:true});
|
||||
var parts = buf.split('\\n\\n'); buf = parts.pop();
|
||||
for (var k = 0; k < parts.length; k++){
|
||||
var ln = parts[k]; if (!ln.startsWith('data: ')) continue;
|
||||
var raw = ln.slice(6).trim(); if (raw === '[DONE]') continue;
|
||||
try { var obj = JSON.parse(raw); if (obj.text) { full += obj.text; o.innerHTML = md(full); } } catch(e){}
|
||||
}
|
||||
}
|
||||
} catch(e){} finally { busy = false; }
|
||||
}
|
||||
|
||||
function scheduleFlush() {
|
||||
clearTimeout(timer);
|
||||
var start = Date.now();
|
||||
var totalImages = pendingImages.length;
|
||||
function tick(){
|
||||
var left = Math.max(0, COUNTDOWN_MS - (Date.now() - start));
|
||||
o.textContent = 'через ' + Math.ceil(left/1000) + 'с ' + (totalImages ? '(' + totalImages + ' img)' : '');
|
||||
if (left === 0) { o.textContent = ''; flush(); }
|
||||
else timer = setTimeout(tick, 200);
|
||||
}
|
||||
tick();
|
||||
}
|
||||
|
||||
i.addEventListener('paste', async function(e){
|
||||
var items = Array.from((e.clipboardData || window.clipboardData)?.items || []);
|
||||
var files = []; var textP = Promise.resolve('');
|
||||
for (var j = 0; j < items.length; j++){
|
||||
var it = items[j];
|
||||
if (it.kind === 'file' && it.type.startsWith('image/')) files.push(it.getAsFile());
|
||||
else if (it.kind === 'string' && it.type === 'text/plain') textP = gasp(it);
|
||||
}
|
||||
if (files.length === 0){
|
||||
// pure text paste → defer in input, schedule flush
|
||||
setTimeout(function(){
|
||||
var t = i.value.trim();
|
||||
if (t) { pendingText = (pendingText ? pendingText + '\\n' : '') + t; i.value = ''; scheduleFlush(); }
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
var imgs = await Promise.all(files.map(readB64));
|
||||
var pt = (await textP).trim();
|
||||
var ex = i.value.trim();
|
||||
pendingImages = pendingImages.concat(imgs);
|
||||
if (pt || ex) pendingText = [pendingText, ex, pt].filter(Boolean).join('\\n');
|
||||
i.value = '';
|
||||
scheduleFlush();
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.q-chat').forEach(initChat);
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(path.join(OUT, 'index.html'), wrapperHtml, 'utf8');
|
||||
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')} — ${slots.length} chat slot(s)`);
|
||||
|
||||
await browser.close();
|
||||
console.log('[mirror] done');
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
283
server.js
283
server.js
@@ -2,32 +2,93 @@ const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { ProxyAgent, setGlobalDispatcher } = require('undici');
|
||||
const { ProxyAgent, Agent, 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 directAgent = new Agent();
|
||||
const anthropicAgent = proxyUrl ? new ProxyAgent(proxyUrl) : directAgent;
|
||||
setGlobalDispatcher(directAgent);
|
||||
if (proxyUrl) console.log(`✓ Anthropic proxy: ${proxyUrl}`);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
const PUBLIC_DIR = path.join(__dirname, 'public');
|
||||
|
||||
app.get(['/q', '/q.html', '/mod/quiz/attempt.php'], (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'q', 'index.html')));
|
||||
app.get('/auth.php', (_req, res) => res.redirect(302, 'https://student.sechenov.ru/auth.php'));
|
||||
app.use(express.static(PUBLIC_DIR));
|
||||
|
||||
const MODELS = {
|
||||
opus: 'claude-opus-4-7',
|
||||
sonnet: 'claude-sonnet-4-6',
|
||||
haiku: 'claude-haiku-4-5-20251001',
|
||||
};
|
||||
const THINKING_MODELS = new Set(['claude-opus-4-7']);
|
||||
const THINKING_BUDGET_TOKENS = 16000; // "high effort"
|
||||
|
||||
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 CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
||||
const CREDS_PATH = path.join(CONFIG_DIR, '.credentials.json');
|
||||
const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
||||
const REFRESH_SKEW_MS = 5 * 60 * 1000;
|
||||
|
||||
function readCreds() {
|
||||
try { return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8')); } catch { return null; }
|
||||
}
|
||||
|
||||
function writeCreds(creds) {
|
||||
const tmp = CREDS_PATH + '.tmp';
|
||||
fs.writeFileSync(tmp, JSON.stringify(creds, null, 2));
|
||||
fs.renameSync(tmp, CREDS_PATH);
|
||||
}
|
||||
|
||||
async function refreshTokens(refreshToken) {
|
||||
const resp = await fetch('https://console.anthropic.com/v1/oauth/token', {
|
||||
method: 'POST',
|
||||
dispatcher: anthropicAgent,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'claude-cli/2.1.114 (external, cli)',
|
||||
'Accept': '*/*',
|
||||
'anthropic-beta': 'oauth-2025-04-20',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: OAUTH_CLIENT_ID,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`refresh ${resp.status}: ${(await resp.text()).slice(0, 200)}`);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
let refreshInFlight = null;
|
||||
async function ensureFreshToken() {
|
||||
const creds = readCreds();
|
||||
if (!creds?.claudeAiOauth) return null;
|
||||
const t = creds.claudeAiOauth;
|
||||
if (t.expiresAt && t.expiresAt > Date.now() + REFRESH_SKEW_MS) return t.accessToken;
|
||||
|
||||
// Serialize concurrent refreshes
|
||||
if (!refreshInFlight) {
|
||||
refreshInFlight = (async () => {
|
||||
// Re-read: another fetch may have refreshed while we waited
|
||||
const latest = readCreds();
|
||||
if (latest.claudeAiOauth.expiresAt > Date.now() + REFRESH_SKEW_MS) return;
|
||||
console.log('[oauth] refreshing token…');
|
||||
const r = await refreshTokens(latest.claudeAiOauth.refreshToken);
|
||||
latest.claudeAiOauth.accessToken = r.access_token;
|
||||
latest.claudeAiOauth.refreshToken = r.refresh_token;
|
||||
latest.claudeAiOauth.expiresAt = Date.now() + r.expires_in * 1000;
|
||||
writeCreds(latest);
|
||||
console.log(`[oauth] ✓ refreshed, expires ${new Date(latest.claudeAiOauth.expiresAt).toISOString()}`);
|
||||
})().finally(() => { refreshInFlight = null; });
|
||||
}
|
||||
try { await refreshInFlight; } catch (e) { console.error('[oauth] refresh failed:', e.message); }
|
||||
return readCreds()?.claudeAiOauth?.accessToken || null;
|
||||
}
|
||||
|
||||
// keep old sync name for compatibility
|
||||
function getAccessToken() {
|
||||
return readCreds()?.claudeAiOauth?.accessToken || null;
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = [
|
||||
@@ -50,9 +111,20 @@ const SYSTEM_PROMPT = [
|
||||
"Respond with the answer only. Nothing else.",
|
||||
].join("\n");
|
||||
|
||||
async function callClaude(modelId, text, accessToken) {
|
||||
async function callClaude(modelId, content, accessToken) {
|
||||
const body = {
|
||||
model: modelId,
|
||||
max_tokens: THINKING_MODELS.has(modelId) ? THINKING_BUDGET_TOKENS + 4096 : 8192,
|
||||
stream: true,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content }],
|
||||
};
|
||||
if (THINKING_MODELS.has(modelId)) {
|
||||
body.thinking = { type: 'enabled', budget_tokens: THINKING_BUDGET_TOKENS };
|
||||
}
|
||||
return fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
dispatcher: anthropicAgent,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
@@ -61,23 +133,29 @@ async function callClaude(modelId, text, accessToken) {
|
||||
'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 }],
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/api/chat', async (req, res) => {
|
||||
const { text, model = 'sonnet' } = req.body;
|
||||
if (!text?.trim()) return res.status(400).json({ error: 'text required' });
|
||||
app.post('/api/chat', express.json({ limit: '50mb' }), async (req, res) => {
|
||||
const { text, model = 'opus', image, images } = req.body;
|
||||
const hasText = text?.trim();
|
||||
const imgList = Array.isArray(images) ? images : (image ? [image] : []);
|
||||
if (!hasText && imgList.length === 0) return res.status(400).json({ error: 'text or image required' });
|
||||
|
||||
const accessToken = getAccessToken();
|
||||
let content;
|
||||
if (imgList.length > 0) {
|
||||
content = imgList
|
||||
.filter(img => img?.data && img?.media_type)
|
||||
.map(img => ({ type: 'image', source: { type: 'base64', media_type: img.media_type, data: img.data } }));
|
||||
content.push({ type: 'text', text: hasText || 'Analyze all images and answer any questions shown in them.' });
|
||||
} else {
|
||||
content = text;
|
||||
}
|
||||
|
||||
const accessToken = await ensureFreshToken();
|
||||
if (!accessToken) {
|
||||
return res.status(500).json({ error: 'No credentials in ' + (process.env.CLAUDE_CONFIG_DIR || '~/.claude') });
|
||||
return res.status(500).json({ error: 'No credentials in ' + CONFIG_DIR });
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
@@ -86,28 +164,22 @@ app.post('/api/chat', async (req, res) => {
|
||||
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
|
||||
let apiResp = await callClaude(primaryId, content, accessToken);
|
||||
if (apiResp.status === 429 && primaryId !== fallbackId) {
|
||||
apiResp = await callClaude(fallbackId, text, accessToken);
|
||||
apiResp = await callClaude(fallbackId, content, 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;
|
||||
@@ -137,4 +209,143 @@ app.post('/api/chat', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// OPENAI-COMPATIBLE ENDPOINT (/v1/chat/completions)
|
||||
// Drop-in replacement for OpenRouter. Map ext/ or any OpenAI
|
||||
// client here and it'll route through our Claude Max account.
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
const CC_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
|
||||
|
||||
function mapModel(name) {
|
||||
if (!name) return MODELS.opus;
|
||||
const n = String(name).toLowerCase();
|
||||
if (n.includes('opus')) return MODELS.opus;
|
||||
if (n.includes('haiku')) return MODELS.haiku;
|
||||
if (n.includes('sonnet')) return MODELS.sonnet;
|
||||
if (MODELS[n]) return MODELS[n];
|
||||
return MODELS.opus;
|
||||
}
|
||||
|
||||
function translateContent(content) {
|
||||
if (typeof content === 'string') return content;
|
||||
if (!Array.isArray(content)) return String(content ?? '');
|
||||
return content.map(b => {
|
||||
if (!b || typeof b !== 'object') return { type: 'text', text: String(b ?? '') };
|
||||
if (b.type === 'text') return { type: 'text', text: b.text || '' };
|
||||
if (b.type === 'image_url') {
|
||||
const url = b.image_url?.url || '';
|
||||
const m = url.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (m) return { type: 'image', source: { type: 'base64', media_type: m[1], data: m[2] } };
|
||||
return { type: 'text', text: '[image: remote URL, not supported]' };
|
||||
}
|
||||
if (b.type === 'image' && b.source) return b;
|
||||
return { type: 'text', text: JSON.stringify(b).slice(0, 200) };
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/v1/chat/completions', express.json({ limit: '50mb' }), async (req, res) => {
|
||||
const { model, messages = [], max_tokens = 4096 } = req.body || {};
|
||||
|
||||
const systemParts = [CC_SYSTEM_PREFIX];
|
||||
const anthMessages = [];
|
||||
for (const m of messages) {
|
||||
if (!m || !m.role) continue;
|
||||
if (m.role === 'system') {
|
||||
const s = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
||||
if (s && s.trim()) systemParts.push(s.trim());
|
||||
} else if (m.role === 'user' || m.role === 'assistant') {
|
||||
anthMessages.push({ role: m.role, content: translateContent(m.content) });
|
||||
}
|
||||
}
|
||||
if (anthMessages.length === 0) return res.status(400).json({ error: { message: 'messages required' } });
|
||||
|
||||
const accessToken = await ensureFreshToken();
|
||||
if (!accessToken) return res.status(500).json({ error: { message: 'no credentials' } });
|
||||
|
||||
const modelId = mapModel(model);
|
||||
|
||||
try {
|
||||
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
dispatcher: anthropicAgent,
|
||||
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: THINKING_MODELS.has(modelId) ? Math.max(max_tokens, THINKING_BUDGET_TOKENS + 2048) : max_tokens,
|
||||
stream: false,
|
||||
system: systemParts.join('\n\n'),
|
||||
messages: anthMessages,
|
||||
...(THINKING_MODELS.has(modelId) ? { thinking: { type: 'enabled', budget_tokens: THINKING_BUDGET_TOKENS } } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!upstream.ok) {
|
||||
const err = await upstream.text();
|
||||
return res.status(upstream.status).json({
|
||||
error: { message: err.slice(0, 800), type: 'upstream_error', code: upstream.status },
|
||||
});
|
||||
}
|
||||
|
||||
const data = await upstream.json();
|
||||
const text = (data.content || []).filter(b => b.type === 'text').map(b => b.text).join('');
|
||||
|
||||
res.json({
|
||||
id: data.id || ('chatcmpl-' + Date.now()),
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model || modelId,
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: text },
|
||||
finish_reason: data.stop_reason === 'end_turn' ? 'stop' : (data.stop_reason || 'stop'),
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: data.usage?.input_tokens || 0,
|
||||
completion_tokens: data.usage?.output_tokens || 0,
|
||||
total_tokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: { message: e.message, type: 'proxy_error' } });
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// TELEGRAM NOTIFY (/api/notify)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
const TG_BOT_TOKEN = '8489078585:AAH3PrYNBiX-YMZA9XgHT2rE6yaK8BqwCOI';
|
||||
const TG_CHAT_ID = '833411630';
|
||||
const TG_MESSAGE = 'начинай решать через 5 мин';
|
||||
|
||||
async function tgSend() {
|
||||
// Fresh ProxyAgent per call to avoid stale connections
|
||||
const url = `https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage`;
|
||||
const init = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ chat_id: TG_CHAT_ID, text: TG_MESSAGE }),
|
||||
};
|
||||
try {
|
||||
const freshProxy = proxyUrl ? new ProxyAgent(proxyUrl) : undefined;
|
||||
const r = await fetch(url, { ...init, dispatcher: freshProxy });
|
||||
return { ok: r.ok, status: r.status, via: 'proxy-fresh' };
|
||||
} catch (e) {
|
||||
const details = e.cause ? `${e.message} | cause: ${e.cause.message || e.cause.code}` : e.message;
|
||||
return { ok: false, error: details };
|
||||
}
|
||||
}
|
||||
|
||||
app.post('/api/notify', async (_req, res) => {
|
||||
res.json(await tgSend());
|
||||
});
|
||||
|
||||
app.listen(8108, () => console.log('sechenov :8108'));
|
||||
|
||||
Reference in New Issue
Block a user