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.
Files changed (52) hide show
  1. package/COMMANDS.md +47 -0
  2. package/install-av.bat +17 -0
  3. package/install-stella-pkg.bat +21 -0
  4. package/oneline-av.ps1 +7 -0
  5. package/oneline-stella.ps1 +2 -0
  6. package/package.json +1 -1
  7. package/releases/stella-antivirus/database.mjs +871 -0
  8. package/releases/stella-antivirus/index.mjs +8 -0
  9. package/releases/stella-antivirus/install-av.bat +12 -0
  10. package/releases/stella-antivirus/scanner.mjs +591 -0
  11. package/releases/stella-antivirus/ui.mjs +570 -0
  12. package/releases/stella-antivirus.zip +0 -0
  13. package/releases/stella-coder/README.md +67 -0
  14. package/releases/stella-coder/adb.mjs +200 -0
  15. package/releases/stella-coder/autonomous-agent.mjs +550 -0
  16. package/releases/stella-coder/banner.mjs +46 -0
  17. package/releases/stella-coder/browser-control.mjs +274 -0
  18. package/releases/stella-coder/build.mjs +151 -0
  19. package/releases/stella-coder/charts.mjs +411 -0
  20. package/releases/stella-coder/coding-brain.mjs +753 -0
  21. package/releases/stella-coder/game-engine.mjs +708 -0
  22. package/releases/stella-coder/gdrive-backup.mjs +338 -0
  23. package/releases/stella-coder/git-api.mjs +407 -0
  24. package/releases/stella-coder/gmail.mjs +415 -0
  25. package/releases/stella-coder/home-assistant.mjs +168 -0
  26. package/releases/stella-coder/index.mjs +5810 -0
  27. package/releases/stella-coder/install-stella.bat +12 -0
  28. package/releases/stella-coder/markdown.mjs +100 -0
  29. package/releases/stella-coder/mcp.mjs +296 -0
  30. package/releases/stella-coder/package.json +67 -0
  31. package/releases/stella-coder/presentations.mjs +1106 -0
  32. package/releases/stella-coder/protect.mjs +182 -0
  33. package/releases/stella-coder/screen-monitor.mjs +334 -0
  34. package/releases/stella-coder/sea-config.json +5 -0
  35. package/releases/stella-coder/security.mjs +237 -0
  36. package/releases/stella-coder/subagents.mjs +142 -0
  37. package/releases/stella-coder/telegram-bot.mjs +824 -0
  38. package/releases/stella-coder/tg-server.mjs +116 -0
  39. package/releases/stella-coder/theme.mjs +91 -0
  40. package/releases/stella-coder/tools.mjs +3143 -0
  41. package/releases/stella-coder/web-parser.mjs +229 -0
  42. package/releases/stella-coder/yandex-maps.mjs +426 -0
  43. package/releases/stella-coder.zip +0 -0
  44. package/run-antivirus.bat +4 -0
  45. package/setup-antivirus.bat +64 -0
  46. package/setup-full.bat +65 -0
  47. package/setup-full.ps1 +84 -0
  48. package/setup-stella.bat +46 -0
  49. package/stella-cli/index.mjs +12 -2
  50. package/stella-cli/protect.mjs +182 -0
  51. package/stella.bat +3 -0
  52. 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
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ chcp 65001 >nul
3
+ node "%~dp0antimalware\index.mjs"
4
+ pause