stella-coder 5.3.1 → 5.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMMANDS.md +47 -0
- package/install-av.bat +17 -0
- package/install-stella-pkg.bat +21 -0
- package/oneline-av.ps1 +7 -0
- package/oneline-stella.ps1 +2 -0
- package/package.json +1 -1
- package/releases/stella-antivirus/database.mjs +871 -0
- package/releases/stella-antivirus/index.mjs +8 -0
- package/releases/stella-antivirus/install-av.bat +12 -0
- package/releases/stella-antivirus/scanner.mjs +591 -0
- package/releases/stella-antivirus/ui.mjs +570 -0
- package/releases/stella-antivirus.zip +0 -0
- package/releases/stella-coder/README.md +67 -0
- package/releases/stella-coder/adb.mjs +200 -0
- package/releases/stella-coder/autonomous-agent.mjs +550 -0
- package/releases/stella-coder/banner.mjs +46 -0
- package/releases/stella-coder/browser-control.mjs +274 -0
- package/releases/stella-coder/build.mjs +151 -0
- package/releases/stella-coder/charts.mjs +411 -0
- package/releases/stella-coder/coding-brain.mjs +753 -0
- package/releases/stella-coder/game-engine.mjs +708 -0
- package/releases/stella-coder/gdrive-backup.mjs +338 -0
- package/releases/stella-coder/git-api.mjs +407 -0
- package/releases/stella-coder/gmail.mjs +415 -0
- package/releases/stella-coder/home-assistant.mjs +168 -0
- package/releases/stella-coder/index.mjs +5810 -0
- package/releases/stella-coder/install-stella.bat +12 -0
- package/releases/stella-coder/markdown.mjs +100 -0
- package/releases/stella-coder/mcp.mjs +296 -0
- package/releases/stella-coder/package.json +67 -0
- package/releases/stella-coder/presentations.mjs +1106 -0
- package/releases/stella-coder/protect.mjs +182 -0
- package/releases/stella-coder/screen-monitor.mjs +334 -0
- package/releases/stella-coder/sea-config.json +5 -0
- package/releases/stella-coder/security.mjs +237 -0
- package/releases/stella-coder/subagents.mjs +142 -0
- package/releases/stella-coder/telegram-bot.mjs +824 -0
- package/releases/stella-coder/tg-server.mjs +116 -0
- package/releases/stella-coder/theme.mjs +91 -0
- package/releases/stella-coder/tools.mjs +3143 -0
- package/releases/stella-coder/web-parser.mjs +229 -0
- package/releases/stella-coder/yandex-maps.mjs +426 -0
- package/releases/stella-coder.zip +0 -0
- package/run-antivirus.bat +4 -0
- package/setup-antivirus.bat +64 -0
- package/setup-full.bat +65 -0
- package/setup-full.ps1 +84 -0
- package/setup-stella.bat +46 -0
- package/stella-cli/index.mjs +12 -2
- package/stella-cli/protect.mjs +182 -0
- package/stella.bat +3 -0
- package/tests/test-modules.mjs +101 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import http from "http"
|
|
2
|
+
import https from "https"
|
|
3
|
+
import { JSDOM } from "jsdom"
|
|
4
|
+
|
|
5
|
+
function fetchUrl(url, options = {}) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const mod = url.startsWith("https") ? https : http
|
|
8
|
+
const headers = {
|
|
9
|
+
"User-Agent": "StellaBot/5.2 (compatible; like Googlebot)",
|
|
10
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
11
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
12
|
+
...options.headers,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const req = mod.get(url, { headers, timeout: 15000 }, (res) => {
|
|
16
|
+
// Follow redirects
|
|
17
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
18
|
+
const redirectUrl = new URL(res.headers.location, url).href
|
|
19
|
+
return fetchUrl(redirectUrl, options).then(resolve).catch(reject)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let data = ""
|
|
23
|
+
res.on("data", (chunk) => data += chunk)
|
|
24
|
+
res.on("end", () => resolve({
|
|
25
|
+
status: res.statusCode,
|
|
26
|
+
headers: res.headers,
|
|
27
|
+
html: data,
|
|
28
|
+
url,
|
|
29
|
+
}))
|
|
30
|
+
})
|
|
31
|
+
req.on("error", reject)
|
|
32
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")) })
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class WebParser {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.cache = new Map()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async fetchPage(url) {
|
|
42
|
+
if (this.cache.has(url)) {
|
|
43
|
+
return this.cache.get(url)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await fetchUrl(url)
|
|
47
|
+
const dom = new JSDOM(result.html)
|
|
48
|
+
const doc = dom.window.document
|
|
49
|
+
|
|
50
|
+
const parsed = {
|
|
51
|
+
url,
|
|
52
|
+
status: result.status,
|
|
53
|
+
title: doc.querySelector("title")?.textContent?.trim() || "",
|
|
54
|
+
meta: {
|
|
55
|
+
description: doc.querySelector('meta[name="description"]')?.content || "",
|
|
56
|
+
keywords: doc.querySelector('meta[name="keywords"]')?.content || "",
|
|
57
|
+
ogTitle: doc.querySelector('meta[property="og:title"]')?.content || "",
|
|
58
|
+
ogDescription: doc.querySelector('meta[property="og:description"]')?.content || "",
|
|
59
|
+
ogImage: doc.querySelector('meta[property="og:image"]')?.content || "",
|
|
60
|
+
canonical: doc.querySelector('link[rel="canonical"]')?.href || "",
|
|
61
|
+
},
|
|
62
|
+
headings: Array.from(doc.querySelectorAll("h1,h2,h3,h4")).map(h => ({
|
|
63
|
+
level: parseInt(h.tagName[1]),
|
|
64
|
+
text: h.textContent.trim(),
|
|
65
|
+
})),
|
|
66
|
+
links: Array.from(doc.querySelectorAll("a[href]")).map(a => ({
|
|
67
|
+
text: a.textContent.trim().slice(0, 100),
|
|
68
|
+
href: a.href,
|
|
69
|
+
})).filter(l => l.text).slice(0, 100),
|
|
70
|
+
images: Array.from(doc.querySelectorAll("img[src]")).map(img => ({
|
|
71
|
+
src: img.src,
|
|
72
|
+
alt: img.alt || "",
|
|
73
|
+
})).slice(0, 50),
|
|
74
|
+
text: doc.body?.innerText?.trim()?.slice(0, 50000) || "",
|
|
75
|
+
html: result.html,
|
|
76
|
+
linksCount: doc.querySelectorAll("a[href]").length,
|
|
77
|
+
imagesCount: doc.querySelectorAll("img[src]").length,
|
|
78
|
+
formsCount: doc.querySelectorAll("form").length,
|
|
79
|
+
scriptsCount: doc.querySelectorAll("script").length,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.cache.set(url, parsed)
|
|
83
|
+
return parsed
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async extractText(url) {
|
|
87
|
+
const page = await this.fetchPage(url)
|
|
88
|
+
return { success: true, title: page.title, text: page.text }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async extractLinks(url) {
|
|
92
|
+
const page = await this.fetchPage(url)
|
|
93
|
+
return { success: true, links: page.links }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async extractImages(url) {
|
|
97
|
+
const page = await this.fetchPage(url)
|
|
98
|
+
return { success: true, images: page.images }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async extractForms(url) {
|
|
102
|
+
const page = await this.fetchPage(url)
|
|
103
|
+
const dom = new JSDOM(page.html)
|
|
104
|
+
const doc = dom.window.document
|
|
105
|
+
|
|
106
|
+
const forms = Array.from(doc.querySelectorAll("form")).map(f => ({
|
|
107
|
+
action: f.action,
|
|
108
|
+
method: f.method?.toUpperCase() || "GET",
|
|
109
|
+
fields: Array.from(f.querySelectorAll("input,textarea,select,button")).map(el => ({
|
|
110
|
+
tag: el.tagName.toLowerCase(),
|
|
111
|
+
type: el.type || "",
|
|
112
|
+
name: el.name || "",
|
|
113
|
+
placeholder: el.placeholder || "",
|
|
114
|
+
value: el.value || "",
|
|
115
|
+
required: el.required,
|
|
116
|
+
options: el.tagName === "SELECT"
|
|
117
|
+
? Array.from(el.querySelectorAll("option")).map(o => ({ value: o.value, text: o.textContent.trim() }))
|
|
118
|
+
: undefined,
|
|
119
|
+
})),
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
return { success: true, forms }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async search(query, engine = "duckduckgo") {
|
|
126
|
+
if (engine === "duckduckgo") {
|
|
127
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`
|
|
128
|
+
const page = await this.fetchPage(url)
|
|
129
|
+
const dom = new JSDOM(page.html)
|
|
130
|
+
const doc = dom.window.document
|
|
131
|
+
|
|
132
|
+
const results = Array.from(doc.querySelectorAll(".result")).map(r => ({
|
|
133
|
+
title: r.querySelector(".result__title")?.textContent?.trim() || "",
|
|
134
|
+
url: r.querySelector(".result__url")?.textContent?.trim() || "",
|
|
135
|
+
snippet: r.querySelector(".result__snippet")?.textContent?.trim() || "",
|
|
136
|
+
})).slice(0, 10)
|
|
137
|
+
|
|
138
|
+
return { success: true, results }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (engine === "google") {
|
|
142
|
+
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}`
|
|
143
|
+
const page = await this.fetchPage(url)
|
|
144
|
+
const dom = new JSDOM(page.html)
|
|
145
|
+
const doc = dom.window.document
|
|
146
|
+
|
|
147
|
+
const results = Array.from(doc.querySelectorAll("div.g,div[data-sokoban-container]")).map(r => ({
|
|
148
|
+
title: r.querySelector("h3")?.textContent?.trim() || "",
|
|
149
|
+
url: r.querySelector("a")?.href || "",
|
|
150
|
+
snippet: r.querySelector(".VwiC3b,.st")?.textContent?.trim() || "",
|
|
151
|
+
})).filter(r => r.title).slice(0, 10)
|
|
152
|
+
|
|
153
|
+
return { success: true, results }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { success: false, error: `Unknown engine: ${engine}` }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async getSEO(url) {
|
|
160
|
+
const page = await this.fetchPage(url)
|
|
161
|
+
const dom = new JSDOM(page.html)
|
|
162
|
+
const doc = dom.window.document
|
|
163
|
+
|
|
164
|
+
const seo = {
|
|
165
|
+
title: page.title,
|
|
166
|
+
titleLength: page.title.length,
|
|
167
|
+
metaDescription: page.meta.description,
|
|
168
|
+
metaDescLength: page.meta.description.length,
|
|
169
|
+
h1Count: doc.querySelectorAll("h1").length,
|
|
170
|
+
hasCanonical: !!page.meta.canonical,
|
|
171
|
+
hasOgTitle: !!page.meta.ogTitle,
|
|
172
|
+
hasOgDescription: !!page.meta.ogDescription,
|
|
173
|
+
hasOgImage: !!page.meta.ogImage,
|
|
174
|
+
imageAlts: Array.from(doc.querySelectorAll("img")).filter(img => !img.alt).length === 0,
|
|
175
|
+
linksInternal: page.links.filter(l => new URL(l.href, url).hostname === new URL(url).hostname).length,
|
|
176
|
+
linksExternal: page.links.filter(l => new URL(l.href, url).hostname !== new URL(url).hostname).length,
|
|
177
|
+
score: 0,
|
|
178
|
+
issues: [],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Score
|
|
182
|
+
if (seo.titleLength >= 30 && seo.titleLength <= 60) seo.score += 20
|
|
183
|
+
else seo.issues.push(`Title length ${seo.titleLength} (optimal: 30-60)`)
|
|
184
|
+
|
|
185
|
+
if (seo.metaDescLength >= 120 && seo.metaDescLength <= 160) seo.score += 20
|
|
186
|
+
else seo.issues.push(`Meta description length ${seo.metaDescLength} (optimal: 120-160)`)
|
|
187
|
+
|
|
188
|
+
if (seo.h1Count === 1) seo.score += 15
|
|
189
|
+
else seo.issues.push(`H1 count: ${seo.h1Count} (should be 1)`)
|
|
190
|
+
|
|
191
|
+
if (seo.hasCanonical) seo.score += 10
|
|
192
|
+
else seo.issues.push("Missing canonical tag")
|
|
193
|
+
|
|
194
|
+
if (seo.hasOgTitle) seo.score += 10
|
|
195
|
+
else seo.issues.push("Missing og:title")
|
|
196
|
+
|
|
197
|
+
if (seo.hasOgDescription) seo.score += 10
|
|
198
|
+
else seo.issues.push("Missing og:description")
|
|
199
|
+
|
|
200
|
+
if (seo.hasOgImage) seo.score += 5
|
|
201
|
+
else seo.issues.push("Missing og:image")
|
|
202
|
+
|
|
203
|
+
if (seo.imageAlts) seo.score += 10
|
|
204
|
+
else seo.issues.push("Images without alt text")
|
|
205
|
+
|
|
206
|
+
return { success: true, seo }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async batchFetch(urls) {
|
|
210
|
+
const results = await Promise.allSettled(
|
|
211
|
+
urls.map(url => this.fetchPage(url).then(p => ({
|
|
212
|
+
url,
|
|
213
|
+
title: p.title,
|
|
214
|
+
status: p.status,
|
|
215
|
+
textLength: p.text.length,
|
|
216
|
+
})))
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
results: results.map((r, i) => ({
|
|
222
|
+
url: urls[i],
|
|
223
|
+
...(r.status === "fulfilled" ? r.value : { error: r.reason?.message }),
|
|
224
|
+
})),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export default WebParser
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import { execSync } from "node:child_process"
|
|
5
|
+
|
|
6
|
+
const MAPS_DIR = path.join(os.homedir(), ".stella", "yandex-maps")
|
|
7
|
+
|
|
8
|
+
function ensureDir() {
|
|
9
|
+
if (!fs.existsSync(MAPS_DIR)) fs.mkdirSync(MAPS_DIR, { recursive: true })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function openFile(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
if (process.platform === "win32") {
|
|
15
|
+
execSync(`start "" "${filePath}"`, { shell: "cmd.exe", stdio: "ignore" })
|
|
16
|
+
} else if (process.platform === "darwin") {
|
|
17
|
+
execSync(`open "${filePath}"`, { stdio: "ignore" })
|
|
18
|
+
} else {
|
|
19
|
+
execSync(`xdg-open "${filePath}"`, { stdio: "ignore" })
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const YANDEX_MAP_TEMPLATE = `
|
|
25
|
+
<!DOCTYPE html>
|
|
26
|
+
<html lang="ru">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="UTF-8">
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
30
|
+
<title>{{TITLE}}</title>
|
|
31
|
+
<script src="https://api-maps.yandex.ru/2.1/?apikey={{API_KEY}}&lang=ru_RU"></script>
|
|
32
|
+
<style>
|
|
33
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
34
|
+
body { font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
35
|
+
#map { width: 100%; height: 100vh; }
|
|
36
|
+
.info-panel {
|
|
37
|
+
position: absolute; top: 12px; left: 12px; z-index: 100;
|
|
38
|
+
background: rgba(30,30,46,0.95); color: #cdd6f4; padding: 16px;
|
|
39
|
+
border-radius: 12px; max-width: 320px; backdrop-filter: blur(12px);
|
|
40
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
|
41
|
+
}
|
|
42
|
+
.info-panel h3 { margin-bottom: 8px; font-size: 16px; color: #89b4fa; }
|
|
43
|
+
.info-panel p { font-size: 13px; line-height: 1.5; color: #a6adc8; }
|
|
44
|
+
.search-box {
|
|
45
|
+
position: absolute; top: 12px; right: 12px; z-index: 100;
|
|
46
|
+
}
|
|
47
|
+
.search-box input {
|
|
48
|
+
padding: 10px 16px; border: none; border-radius: 8px;
|
|
49
|
+
background: rgba(30,30,46,0.95); color: #cdd6f4; font-size: 14px;
|
|
50
|
+
width: 300px; backdrop-filter: blur(12px); outline: none;
|
|
51
|
+
}
|
|
52
|
+
.search-box input::placeholder { color: #6c7086; }
|
|
53
|
+
.route-info {
|
|
54
|
+
position: absolute; bottom: 12px; left: 12px; z-index: 100;
|
|
55
|
+
background: rgba(30,30,46,0.95); color: #cdd6f4; padding: 16px;
|
|
56
|
+
border-radius: 12px; display: none; backdrop-filter: blur(12px);
|
|
57
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
|
58
|
+
}
|
|
59
|
+
.route-info h4 { color: #a6e3a1; margin-bottom: 8px; }
|
|
60
|
+
.route-info p { font-size: 13px; color: #a6adc8; }
|
|
61
|
+
.marker-label {
|
|
62
|
+
background: rgba(30,30,46,0.95); color: #cdd6f4; padding: 6px 12px;
|
|
63
|
+
border-radius: 8px; font-size: 12px; white-space: nowrap;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
66
|
+
</head>
|
|
67
|
+
<body>
|
|
68
|
+
<div id="map"></div>
|
|
69
|
+
<div class="search-box">
|
|
70
|
+
<input type="text" id="searchInput" placeholder="Поиск места..." />
|
|
71
|
+
</div>
|
|
72
|
+
<div class="info-panel" id="infoPanel" style="display:none">
|
|
73
|
+
<h3 id="infoTitle"></h3>
|
|
74
|
+
<p id="infoBody"></p>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="route-info" id="routeInfo">
|
|
77
|
+
<h4>Маршрут</h4>
|
|
78
|
+
<p id="routeDetails"></p>
|
|
79
|
+
</div>
|
|
80
|
+
<script>
|
|
81
|
+
ymaps.ready(function() {
|
|
82
|
+
const map = new ymaps.Map('map', {
|
|
83
|
+
center: [{{CENTER_LAT}}, {{CENTER_LNG}}],
|
|
84
|
+
zoom: {{ZOOM}},
|
|
85
|
+
controls: ['zoomControl', 'fullscreenControl', 'geolocationControl', 'typeSelector']
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
{{MARKERS_JS}}
|
|
89
|
+
|
|
90
|
+
{{PLACEMARKS_JS}}
|
|
91
|
+
|
|
92
|
+
{{POLYLINE_JS}}
|
|
93
|
+
|
|
94
|
+
{{POLYGON_JS}}
|
|
95
|
+
|
|
96
|
+
{{CIRCLE_JS}}
|
|
97
|
+
|
|
98
|
+
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
|
99
|
+
if (e.key === 'Enter') {
|
|
100
|
+
const query = this.value;
|
|
101
|
+
ymaps.geocode(query).then(function(result) {
|
|
102
|
+
const obj = result.geoObjects.get(0);
|
|
103
|
+
if (obj) {
|
|
104
|
+
const coords = obj.geometry.getCoordinates();
|
|
105
|
+
map.setCenter(coords, 16);
|
|
106
|
+
const pm = new ymaps.Placemark(coords, {
|
|
107
|
+
balloonContent: obj.getAddressLine(),
|
|
108
|
+
hintContent: obj.getAddressLine()
|
|
109
|
+
}, {
|
|
110
|
+
preset: 'islands#redDotIcon'
|
|
111
|
+
});
|
|
112
|
+
map.geoObjects.add(pm);
|
|
113
|
+
showInfo('Найдено', obj.getAddressLine() + '<br>Координаты: ' + coords.join(', '));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
{{GEOLOCATION_JS}}
|
|
120
|
+
|
|
121
|
+
function showInfo(title, body) {
|
|
122
|
+
document.getElementById('infoTitle').textContent = title;
|
|
123
|
+
document.getElementById('infoBody').innerHTML = body;
|
|
124
|
+
document.getElementById('infoPanel').style.display = 'block';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
{{CUSTOM_JS}}
|
|
128
|
+
});
|
|
129
|
+
</script>
|
|
130
|
+
</body>
|
|
131
|
+
</html>`
|
|
132
|
+
|
|
133
|
+
export class YandexMaps {
|
|
134
|
+
constructor(apiKey = "") {
|
|
135
|
+
this.apiKey = apiKey || process.env.YANDEX_MAPS_API_KEY || ""
|
|
136
|
+
ensureDir()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
isConfigured() {
|
|
140
|
+
return !!this.apiKey
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setApiKey(key) {
|
|
144
|
+
this.apiKey = key
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async geocode(query) {
|
|
148
|
+
if (!this.apiKey) return { success: false, error: "API key not set. Use /ymaps-key <key>" }
|
|
149
|
+
const resp = await fetch(
|
|
150
|
+
`https://geocode-maps.yandex.ru/1.x/?apikey=${this.apiKey}&geocode=${encodeURIComponent(query)}&format=json&lang=ru_RU`
|
|
151
|
+
)
|
|
152
|
+
const data = await resp.json()
|
|
153
|
+
const member = data.response?.GeoObjectCollection?.featureMember?.[0]
|
|
154
|
+
if (!member) return { success: false, error: "Not found" }
|
|
155
|
+
|
|
156
|
+
const geo = member.GeoObject
|
|
157
|
+
const pos = geo.Point?.pos?.split(" ") || []
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
name: geo.name,
|
|
161
|
+
description: geo.Description,
|
|
162
|
+
address: geo.metaDataProperty?.GeocoderMetaData?.text,
|
|
163
|
+
coordinates: { lng: parseFloat(pos[0]), lat: parseFloat(pos[1]) },
|
|
164
|
+
kind: geo.metaDataProperty?.GeocoderMetaData?.kind,
|
|
165
|
+
postalCode: geo.metaDataProperty?.GeocoderMetaData?.Address?.postal_code,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async reverseGeocode(lat, lng) {
|
|
170
|
+
return this.geocode(`${lng},${lat}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async searchNearby(lat, lng, query, radius = 1000) {
|
|
174
|
+
if (!this.apiKey) return { success: false, error: "API key not set" }
|
|
175
|
+
const resp = await fetch(
|
|
176
|
+
`https://geocode-maps.yandex.ru/1.x/?apikey=${this.apiKey}&geocode=${lng},${lat}&format=json&lang=ru_RU&results=10&kind=house`
|
|
177
|
+
)
|
|
178
|
+
const data = await resp.json()
|
|
179
|
+
const members = data.response?.GeoObjectCollection?.featureMember || []
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
results: members.map(m => ({
|
|
183
|
+
name: m.GeoObject.name,
|
|
184
|
+
description: m.GeoObject.Description,
|
|
185
|
+
address: m.GeoObject.metaDataProperty?.GeocoderMetaData?.text,
|
|
186
|
+
coordinates: m.GeoObject.Point?.pos?.split(" ").map(Number),
|
|
187
|
+
})),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async getStaticMap({ center = [55.7558, 37.6173], zoom = 12, markers = [], polylines = [], size = "650x450" } = {}) {
|
|
192
|
+
if (!this.apiKey) return { success: false, error: "API key not set" }
|
|
193
|
+
const [lat, lng] = center
|
|
194
|
+
let url = `https://static-maps.yandex.ru/v1?ll=${lng},${lat}&z=${zoom}&size=${size}&apikey=${this.apiKey}`
|
|
195
|
+
|
|
196
|
+
if (markers.length > 0) {
|
|
197
|
+
const pt = markers.map(m => `${m.lng || m[1]},${m.lat || m[0]},pm2${m.color || "rdm"}`).join("~")
|
|
198
|
+
url += `&pt=${pt}`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const resp = await fetch(url)
|
|
203
|
+
const buffer = Buffer.from(await resp.arrayBuffer())
|
|
204
|
+
const filePath = path.join(MAPS_DIR, `map_${Date.now()}.png`)
|
|
205
|
+
fs.writeFileSync(filePath, buffer)
|
|
206
|
+
openFile(filePath)
|
|
207
|
+
return { success: true, path: filePath, size: buffer.length }
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return { success: false, error: e.message }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async createMap({
|
|
214
|
+
center = [55.7558, 37.6173],
|
|
215
|
+
zoom = 12,
|
|
216
|
+
markers = [],
|
|
217
|
+
polylines = [],
|
|
218
|
+
polygons = [],
|
|
219
|
+
circles = [],
|
|
220
|
+
title = "Яндекс Карта",
|
|
221
|
+
showSearch = true,
|
|
222
|
+
showGeolocation = false,
|
|
223
|
+
mapType = "map",
|
|
224
|
+
} = {}) {
|
|
225
|
+
const markersJs = markers.map((m, i) => {
|
|
226
|
+
const [lat, lng] = Array.isArray(m) ? m : [m.lat, m.lng]
|
|
227
|
+
const label = m.label || m.name || `Метка ${i + 1}`
|
|
228
|
+
const content = m.content || m.description || label
|
|
229
|
+
return `
|
|
230
|
+
var pm${i} = new ymaps.Placemark([${lat}, ${lng}], {
|
|
231
|
+
balloonContent: \`${content}\`,
|
|
232
|
+
hintContent: \`${label}\`,
|
|
233
|
+
iconContent: \`${label}\`
|
|
234
|
+
}, {
|
|
235
|
+
preset: 'islands#${m.color || "violet"}DotIcon',
|
|
236
|
+
iconColor: '${m.hex || "#7c3aed"}'
|
|
237
|
+
});
|
|
238
|
+
map.geoObjects.add(pm${i});
|
|
239
|
+
pm${i}.events.add('click', function() {
|
|
240
|
+
showInfo(\`${label}\`, \`${content}<br>Координаты: ${lat}, ${lng}\`);
|
|
241
|
+
});`
|
|
242
|
+
}).join("\n ")
|
|
243
|
+
|
|
244
|
+
const polylineJs = polylines.map((p, i) => {
|
|
245
|
+
const coords = p.coords || p
|
|
246
|
+
const color = p.color || "#7c3aed"
|
|
247
|
+
const width = p.width || 3
|
|
248
|
+
return `
|
|
249
|
+
var pl${i} = new ymaps.Polyline(${JSON.stringify(coords)}, {}, {
|
|
250
|
+
strokeColor: '${color}',
|
|
251
|
+
strokeWidth: ${width}
|
|
252
|
+
});
|
|
253
|
+
map.geoObjects.add(pl${i});`
|
|
254
|
+
}).join("\n ")
|
|
255
|
+
|
|
256
|
+
const polygonJs = polygons.map((p, i) => {
|
|
257
|
+
const coords = p.coords || p
|
|
258
|
+
const fillColor = p.fillColor || "rgba(124,58,237,0.2)"
|
|
259
|
+
const strokeColor = p.strokeColor || "#7c3aed"
|
|
260
|
+
return `
|
|
261
|
+
var pg${i} = new ymaps.Polygon(${JSON.stringify(coords)}, {}, {
|
|
262
|
+
fillColor: '${fillColor}',
|
|
263
|
+
strokeColor: '${strokeColor}',
|
|
264
|
+
strokeWidth: 2
|
|
265
|
+
});
|
|
266
|
+
map.geoObjects.add(pg${i});`
|
|
267
|
+
}).join("\n ")
|
|
268
|
+
|
|
269
|
+
const circleJs = circles.map((c, i) => {
|
|
270
|
+
const [lat, lng] = Array.isArray(c.center) ? c.center : [c.center.lat, c.center.lng]
|
|
271
|
+
const radius = c.radius || 1000
|
|
272
|
+
return `
|
|
273
|
+
var cr${i} = new ymaps.Circle([[${lat}, ${lng}], ${radius}], {}, {
|
|
274
|
+
fillColor: '${c.fillColor || "rgba(124,58,237,0.15)"}',
|
|
275
|
+
strokeColor: '${c.strokeColor || "#7c3aed"}',
|
|
276
|
+
strokeWidth: 2
|
|
277
|
+
});
|
|
278
|
+
map.geoObjects.add(cr${i});`
|
|
279
|
+
}).join("\n ")
|
|
280
|
+
|
|
281
|
+
const geoJs = showGeolocation ? `
|
|
282
|
+
ymaps.geolocation.get({ provider: 'yandex' }).then(function(result) {
|
|
283
|
+
map.setCenter(result.position, 14);
|
|
284
|
+
var pmGeo = new ymaps.Placemark(result.position, {
|
|
285
|
+
balloonContent: 'Вы здесь',
|
|
286
|
+
hintContent: 'Ваше местоположение'
|
|
287
|
+
}, { preset: 'islands#greenDotIcon' });
|
|
288
|
+
map.geoObjects.add(pmGeo);
|
|
289
|
+
});` : ""
|
|
290
|
+
|
|
291
|
+
const customJs = polylines.some(p => p.route) ? `
|
|
292
|
+
ymaps.route([${JSON.stringify(polylines.find(p => p.route)?.coords || [])}]).then(function(route) {
|
|
293
|
+
map.geoObjects.add(route);
|
|
294
|
+
var path = route.getPaths();
|
|
295
|
+
document.getElementById('routeInfo').style.display = 'block';
|
|
296
|
+
document.getElementById('routeDetails').textContent = 'Расстояние: ' + route.getHumanLength();
|
|
297
|
+
});` : ""
|
|
298
|
+
|
|
299
|
+
const html = YANDEX_MAP_TEMPLATE
|
|
300
|
+
.replace("{{TITLE}}", title)
|
|
301
|
+
.replace("{{API_KEY}}", this.apiKey)
|
|
302
|
+
.replace("{{CENTER_LAT}}", center[0])
|
|
303
|
+
.replace("{{CENTER_LNG}}", center[1])
|
|
304
|
+
.replace("{{ZOOM}}", zoom)
|
|
305
|
+
.replace("{{MARKERS_JS}}", markersJs)
|
|
306
|
+
.replace("{{PLACEMARKS_JS}}", "")
|
|
307
|
+
.replace("{{POLYLINE_JS}}", polylineJs)
|
|
308
|
+
.replace("{{POLYGON_JS}}", polygonJs)
|
|
309
|
+
.replace("{{CIRCLE_JS}}", circleJs)
|
|
310
|
+
.replace("{{GEOLOCATION_JS}}", geoJs)
|
|
311
|
+
.replace("{{CUSTOM_JS}}", customJs)
|
|
312
|
+
|
|
313
|
+
const filename = `yamap_${Date.now()}.html`
|
|
314
|
+
const filePath = path.join(MAPS_DIR, filename)
|
|
315
|
+
fs.writeFileSync(filePath, html)
|
|
316
|
+
openFile(filePath)
|
|
317
|
+
return { success: true, path: filePath, filename }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async showRoute(from, to, mode = "auto") {
|
|
321
|
+
const fromGeo = await this.geocode(from)
|
|
322
|
+
const toGeo = await this.geocode(to)
|
|
323
|
+
if (!fromGeo.success || !toGeo.success) {
|
|
324
|
+
return { success: false, error: "Could not geocode addresses" }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return this.createMap({
|
|
328
|
+
center: [
|
|
329
|
+
(fromGeo.coordinates.lat + toGeo.coordinates.lat) / 2,
|
|
330
|
+
(fromGeo.coordinates.lng + toGeo.coordinates.lng) / 2,
|
|
331
|
+
],
|
|
332
|
+
zoom: 11,
|
|
333
|
+
markers: [
|
|
334
|
+
{ lat: fromGeo.coordinates.lat, lng: fromGeo.coordinates.lng, label: "Откуда", color: "green", hex: "#059669" },
|
|
335
|
+
{ lat: toGeo.coordinates.lat, lng: toGeo.coordinates.lng, label: "Куда", color: "red", hex: "#dc2626" },
|
|
336
|
+
],
|
|
337
|
+
polylines: [{
|
|
338
|
+
coords: [
|
|
339
|
+
[fromGeo.coordinates.lat, fromGeo.coordinates.lng],
|
|
340
|
+
[toGeo.coordinates.lat, toGeo.coordinates.lng],
|
|
341
|
+
],
|
|
342
|
+
route: true,
|
|
343
|
+
}],
|
|
344
|
+
title: `Маршрут: ${from} → ${to}`,
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async showLocation(query) {
|
|
349
|
+
const geo = await this.geocode(query)
|
|
350
|
+
if (!geo.success) return { success: false, error: geo.error }
|
|
351
|
+
|
|
352
|
+
return this.createMap({
|
|
353
|
+
center: [geo.coordinates.lat, geo.coordinates.lng],
|
|
354
|
+
zoom: 16,
|
|
355
|
+
markers: [{
|
|
356
|
+
lat: geo.coordinates.lat,
|
|
357
|
+
lng: geo.coordinates.lng,
|
|
358
|
+
label: geo.name,
|
|
359
|
+
content: `${geo.address || geo.description}<br>${geo.coordinates.lat}, ${geo.coordinates.lng}`,
|
|
360
|
+
}],
|
|
361
|
+
title: geo.name,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async showMultiplePlaces(places) {
|
|
366
|
+
const markers = []
|
|
367
|
+
const coords = []
|
|
368
|
+
|
|
369
|
+
for (const place of places) {
|
|
370
|
+
const geo = typeof place === "string" ? await this.geocode(place) : place
|
|
371
|
+
if (geo.success || geo.coordinates) {
|
|
372
|
+
const c = geo.coordinates || { lat: place.lat, lng: place.lng }
|
|
373
|
+
markers.push({
|
|
374
|
+
lat: c.lat,
|
|
375
|
+
lng: c.lng,
|
|
376
|
+
label: geo.name || place.name || place,
|
|
377
|
+
content: geo.address || place.name || place,
|
|
378
|
+
})
|
|
379
|
+
coords.push([c.lat, c.lng])
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (coords.length === 0) return { success: false, error: "No places found" }
|
|
384
|
+
|
|
385
|
+
const avgLat = coords.reduce((s, c) => s + c[0], 0) / coords.length
|
|
386
|
+
const avgLng = coords.reduce((s, c) => s + c[1], 0) / coords.length
|
|
387
|
+
|
|
388
|
+
return this.createMap({
|
|
389
|
+
center: [avgLat, avgLng],
|
|
390
|
+
zoom: coords.length > 5 ? 10 : 12,
|
|
391
|
+
markers,
|
|
392
|
+
title: `Места (${markers.length})`,
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async showRouteFromHere(fromLat, fromLng, toQuery) {
|
|
397
|
+
const toGeo = await this.geocode(toQuery)
|
|
398
|
+
if (!toGeo.success) return { success: false, error: toGeo.error }
|
|
399
|
+
|
|
400
|
+
return this.createMap({
|
|
401
|
+
center: [(fromLat + toGeo.coordinates.lat) / 2, (fromLng + toGeo.coordinates.lng) / 2],
|
|
402
|
+
zoom: 11,
|
|
403
|
+
markers: [
|
|
404
|
+
{ lat: fromLat, lng: fromLng, label: "Отсюда", color: "green", hex: "#059669" },
|
|
405
|
+
{ lat: toGeo.coordinates.lat, lng: toGeo.coordinates.lng, label: toGeo.name, color: "red", hex: "#dc2626" },
|
|
406
|
+
],
|
|
407
|
+
polylines: [{
|
|
408
|
+
coords: [[fromLat, fromLng], [toGeo.coordinates.lat, toGeo.coordinates.lng]],
|
|
409
|
+
route: true,
|
|
410
|
+
}],
|
|
411
|
+
title: `Маршрут до ${toGeo.name}`,
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
listMaps() {
|
|
416
|
+
if (!fs.existsSync(MAPS_DIR)) return []
|
|
417
|
+
return fs.readdirSync(MAPS_DIR)
|
|
418
|
+
.filter(f => f.endsWith(".html") || f.endsWith(".png"))
|
|
419
|
+
.map(f => ({
|
|
420
|
+
name: f,
|
|
421
|
+
path: path.join(MAPS_DIR, f),
|
|
422
|
+
created: fs.statSync(path.join(MAPS_DIR, f)).birthtime,
|
|
423
|
+
}))
|
|
424
|
+
.sort((a, b) => b.created - a.created)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
Binary file
|