micro-models-agent 2.11.0 → 2.13.1
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/CHANGELOG.md +39 -0
- package/dist/assets/app.js +22 -2
- package/dist/assets/index.html +39 -9
- package/dist/assets/js/dom.js +15 -0
- package/dist/assets/js/feed.js +129 -10
- package/dist/assets/js/sidebar.js +512 -0
- package/dist/assets/js/tabs.js +30 -5
- package/dist/assets/setup.js +267 -8
- package/dist/assets/styles.css +168 -3
- package/dist/cli/cache-line.js +1 -0
- package/dist/cli/command-suggest.js +5 -4
- package/dist/cli/commands.js +231 -122
- package/dist/cli/completer.js +132 -31
- package/dist/cli/index.js +1 -0
- package/dist/cli/json-payload.js +4 -4
- package/dist/cli/main.js +166 -61
- package/dist/cli/onboarding-commands.js +126 -0
- package/dist/cli/plugin-commands.js +2 -1
- package/dist/cli/relaunch.js +16 -13
- package/dist/cli/repl-commands-lsp.js +75 -0
- package/dist/cli/repl-commands-skills.js +105 -0
- package/dist/cli/repl-commands.js +589 -266
- package/dist/cli/repl.js +436 -177
- package/dist/cli/run-result.js +5 -6
- package/dist/cli/security-commands.js +37 -28
- package/dist/cli/setup-order.js +4 -55
- package/dist/cli/setup-prompt.js +3 -2
- package/dist/cli/setup.js +27 -107
- package/dist/cli/web-command.js +98 -0
- package/dist/config/budget.js +5 -5
- package/dist/config/config.js +103 -96
- package/dist/config/defaults.js +38 -8
- package/dist/config/domains.js +24 -27
- package/dist/config/env.js +46 -0
- package/dist/config/experts.js +2 -0
- package/dist/config/index.js +1 -0
- package/dist/config/paths.js +56 -0
- package/dist/config/redact.js +44 -0
- package/dist/config/registry-extra.js +334 -0
- package/dist/config/registry.js +584 -0
- package/dist/config/security.js +35 -36
- package/dist/config/timeouts.js +74 -0
- package/dist/config/utils.js +16 -15
- package/dist/core/agent/artifact-progress.js +59 -0
- package/dist/core/agent/artifact-resolve.js +79 -0
- package/dist/core/agent/audit-gate.js +7 -3
- package/dist/core/agent/compaction.js +2 -2
- package/dist/core/agent/constants.js +33 -4
- package/dist/core/agent/context-log.js +454 -0
- package/dist/core/agent/context-renderer.js +5 -0
- package/dist/core/agent/critic-gate.js +132 -0
- package/dist/core/agent/hallucination-gate.js +148 -38
- package/dist/core/agent/loop-state.js +15 -0
- package/dist/core/agent/prefix-monitor.js +1 -1
- package/dist/core/agent/reasoning-resolver.js +10 -1
- package/dist/core/agent/token-tracker.js +7 -4
- package/dist/core/agent/tool-batch.js +93 -18
- package/dist/core/agent/tool-images.js +18 -0
- package/dist/core/agent/tool-output.js +65 -11
- package/dist/core/agent/tool-pairing.js +60 -0
- package/dist/core/agent-moe.js +25 -24
- package/dist/core/agent.js +429 -108
- package/dist/core/bootstrap/agent-phase.js +66 -0
- package/dist/core/bootstrap/config-phase.js +86 -0
- package/dist/core/bootstrap/modules-phase.js +269 -0
- package/dist/core/bootstrap/provider-phase.js +183 -0
- package/dist/core/bootstrap/session-phase.js +204 -0
- package/dist/core/bootstrap/state.js +94 -0
- package/dist/core/bootstrap/workspace-phase.js +115 -0
- package/dist/core/bootstrap.js +92 -660
- package/dist/core/crash-handler.js +17 -7
- package/dist/core/environment.js +37 -12
- package/dist/core/host-plan.js +43 -0
- package/dist/core/host.js +23 -0
- package/dist/core/index.js +1 -0
- package/dist/core/prompt-builder.js +17 -1
- package/dist/core/prompt-overflow.js +42 -43
- package/dist/core/session-logger.js +122 -30
- package/dist/core/system-prompt.js +117 -0
- package/dist/core/version.js +6 -4
- package/dist/core/web-host.js +34 -0
- package/dist/core/workspace.js +19 -15
- package/dist/i18n/en.json +89 -3
- package/dist/i18n/index.js +37 -3
- package/dist/i18n/ru.json +89 -3
- package/dist/index.js +7 -3
- package/dist/llm/image-utils.js +55 -64
- package/dist/llm/llm-errors.js +9 -0
- package/dist/llm/model-loader.js +10 -32
- package/dist/llm/models-dev.js +121 -0
- package/dist/llm/openai-compat.js +146 -73
- package/dist/llm/orchestrator.js +33 -11
- package/dist/llm/provider-budget.js +2 -2
- package/dist/llm/provider-context.js +233 -0
- package/dist/llm/provider.js +1 -1
- package/dist/llm/response.js +5 -0
- package/dist/llm/token-counter.js +27 -11
- package/dist/logger/app-logger.js +48 -22
- package/dist/logger/file-log.js +28 -19
- package/dist/logger/index.js +1 -0
- package/dist/main.js +888 -139
- package/dist/migration/backup.js +14 -3
- package/dist/migration/detect.js +12 -3
- package/dist/migration/index.js +1 -0
- package/dist/modules/artifacts/store.js +9 -8
- package/dist/modules/browser/actions.js +6 -0
- package/dist/modules/browser/bridge-client.js +14 -12
- package/dist/modules/browser/bridge-path.js +4 -4
- package/dist/modules/browser/console-buffer.js +37 -0
- package/dist/modules/browser/cookie-store.js +5 -0
- package/dist/modules/browser/driver-types.js +1 -0
- package/dist/modules/browser/driver.js +19 -11
- package/dist/modules/browser/index.js +3 -1
- package/dist/modules/browser/module.js +2 -1
- package/dist/modules/browser/session.js +10 -30
- package/dist/modules/browser/snapshot.js +19 -13
- package/dist/modules/browser/types.js +1 -0
- package/dist/modules/certification/cli.js +28 -20
- package/dist/modules/certification/fact-checker.js +9 -1
- package/dist/modules/certification/loader.js +9 -2
- package/dist/modules/certification/manifest.js +83 -21
- package/dist/modules/certification/runner.js +23 -13
- package/dist/modules/certification/scenarios.js +20 -20
- package/dist/modules/certification/syntax-scenarios.js +1 -2
- package/dist/modules/context/chunk-query.js +11 -9
- package/dist/modules/context/fact-extractor.js +37 -39
- package/dist/modules/context/index.js +1 -0
- package/dist/modules/context/manager.js +106 -80
- package/dist/modules/critic/client.js +176 -0
- package/dist/modules/critic/evidence.js +81 -0
- package/dist/modules/critic/provider.js +45 -0
- package/dist/modules/critic/tools.js +78 -0
- package/dist/modules/critic/types.js +1 -0
- package/dist/modules/execution/audit-runners.js +28 -34
- package/dist/modules/execution/auditor.js +53 -49
- package/dist/modules/execution/execution-plugin.js +142 -102
- package/dist/modules/execution/index.js +1 -1
- package/dist/modules/execution/input-from.js +37 -13
- package/dist/modules/execution/module.js +368 -191
- package/dist/modules/execution/moe-executor.js +53 -26
- package/dist/modules/execution/plan-actions.js +320 -202
- package/dist/modules/execution/plan-checks.js +35 -0
- package/dist/modules/execution/plan-coverage.js +36 -33
- package/dist/modules/execution/plan-steps.js +11 -3
- package/dist/modules/execution/plan-store.js +83 -17
- package/dist/modules/execution/plan-tool.js +89 -35
- package/dist/modules/execution/plan-validator.js +30 -10
- package/dist/modules/execution/planner.js +22 -13
- package/dist/modules/execution/stuck-detector.js +196 -93
- package/dist/modules/execution/tracker.js +22 -8
- package/dist/modules/execution/verifier.js +69 -48
- package/dist/modules/execution/windows-commands.js +12 -12
- package/dist/modules/hallucination/artifact-claim.js +93 -0
- package/dist/modules/hallucination/confidence.js +13 -11
- package/dist/modules/hallucination/consistency.js +10 -2
- package/dist/modules/hallucination/detector.js +10 -5
- package/dist/modules/hallucination/factual.js +43 -26
- package/dist/modules/hallucination/index.js +1 -0
- package/dist/modules/hallucination/js-identifiers.js +44 -45
- package/dist/modules/hallucination/llm-judge.js +17 -12
- package/dist/modules/hallucination/success-claim.js +59 -0
- package/dist/modules/indexer/cache.js +52 -4
- package/dist/modules/indexer/index.js +1 -0
- package/dist/modules/indexer/map-command.js +2 -4
- package/dist/modules/indexer/map-select.js +16 -16
- package/dist/modules/indexer/module.js +47 -12
- package/dist/modules/indexer/project-profile.js +8 -7
- package/dist/modules/indexer/symbols.js +15 -15
- package/dist/modules/indexer/walker.js +159 -29
- package/dist/modules/lsp/check-tool.js +7 -5
- package/dist/modules/lsp/client.js +39 -39
- package/dist/modules/lsp/command.js +17 -24
- package/dist/modules/lsp/config.js +12 -11
- package/dist/modules/lsp/index.js +1 -0
- package/dist/modules/lsp/module.js +48 -46
- package/dist/modules/lsp/probe.js +13 -14
- package/dist/modules/lsp/project-root.js +14 -17
- package/dist/modules/lsp/startup-check.js +18 -16
- package/dist/modules/mcp/client.js +59 -62
- package/dist/modules/mcp/index.js +1 -0
- package/dist/modules/mcp/module.js +66 -37
- package/dist/modules/mcp/registry.js +5 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/module.js +12 -8
- package/dist/modules/memory/search.js +4 -1
- package/dist/modules/memory/store.js +40 -2
- package/dist/modules/onboarding/init.js +47 -0
- package/dist/modules/onboarding/meet.js +30 -0
- package/dist/modules/onboarding/mode.js +30 -0
- package/dist/modules/onboarding/prompts.js +43 -0
- package/dist/modules/pipelines/engine.js +10 -0
- package/dist/modules/pipelines/index.js +1 -0
- package/dist/modules/pipelines/parser.js +4 -1
- package/dist/modules/pipelines/template.js +4 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
- package/dist/modules/plugins/builtin/notify.js +2 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +18 -3
- package/dist/modules/plugins/manager.js +37 -17
- package/dist/modules/pricing/index.js +10 -11
- package/dist/modules/pricing/prices.js +12 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +27 -31
- package/dist/modules/processes/registry.js +44 -25
- package/dist/modules/processes/runner.js +4 -4
- package/dist/modules/providers/capabilities.js +28 -0
- package/dist/modules/providers/create.js +3 -4
- package/dist/modules/providers/factory.js +7 -7
- package/dist/modules/providers/fallback.js +12 -12
- package/dist/modules/providers/health.js +4 -4
- package/dist/modules/providers/manager.js +50 -19
- package/dist/modules/providers/naming.js +15 -0
- package/dist/modules/providers/preflight.js +71 -0
- package/dist/modules/providers/presets.js +39 -8
- package/dist/modules/providers/registry.js +7 -2
- package/dist/modules/reasoning/policy.js +8 -6
- package/dist/modules/reasoning/probe.js +30 -25
- package/dist/modules/security/audit-log.js +19 -36
- package/dist/modules/security/audit-notifier.js +35 -75
- package/dist/modules/security/command-validator.js +99 -51
- package/dist/modules/security/content-scanner.js +8 -8
- package/dist/modules/security/data-sanitizer.js +9 -16
- package/dist/modules/security/encryption.js +32 -52
- package/dist/modules/security/index.js +2 -1
- package/dist/modules/security/network-validator.js +120 -43
- package/dist/modules/security/path-validator.js +61 -46
- package/dist/modules/security/protected-paths.js +73 -0
- package/dist/modules/security/rate-limiter.js +17 -49
- package/dist/modules/security/security-policies.js +25 -57
- package/dist/modules/security/session-encryption.js +32 -71
- package/dist/modules/security/session-isolation.js +15 -28
- package/dist/modules/session/auto-name.js +63 -0
- package/dist/modules/session/export.js +98 -0
- package/dist/modules/session/index.js +1 -0
- package/dist/modules/session/manager.js +79 -28
- package/dist/modules/session/module.js +4 -3
- package/dist/modules/session/store.js +57 -20
- package/dist/modules/setup/answers.js +35 -0
- package/dist/modules/setup/apply.js +79 -0
- package/dist/modules/setup/discovery.js +72 -0
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/loader.js +9 -1
- package/dist/modules/skills/module.js +11 -0
- package/dist/modules/updater/changelog-reader.js +15 -16
- package/dist/modules/updater/checker.js +9 -4
- package/dist/modules/updater/dev-detect.js +7 -6
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +17 -15
- package/dist/modules/user-profile/compressor.js +2 -0
- package/dist/modules/user-profile/index.js +1 -0
- package/dist/modules/user-profile/profile.js +10 -1
- package/dist/modules/webui/answer-api.js +29 -0
- package/dist/modules/webui/assets.js +50 -0
- package/dist/modules/webui/body.js +21 -0
- package/dist/modules/webui/commands-settings.js +209 -0
- package/dist/modules/webui/commands-shared.js +60 -0
- package/dist/modules/webui/commands.js +174 -0
- package/dist/modules/webui/config-api.js +134 -0
- package/dist/modules/webui/file-api.js +202 -0
- package/dist/modules/webui/forward.js +159 -0
- package/dist/modules/webui/history-api.js +112 -0
- package/dist/modules/webui/host-state.js +60 -0
- package/dist/modules/webui/i18n-api.js +9 -0
- package/dist/modules/webui/limits.js +6 -0
- package/dist/modules/webui/log-forward.js +20 -0
- package/dist/modules/webui/memory-commands.js +64 -0
- package/dist/modules/webui/model-cache.js +18 -0
- package/dist/modules/webui/module.js +148 -0
- package/dist/modules/webui/persist.js +24 -0
- package/dist/modules/webui/prompt-io.js +97 -0
- package/dist/modules/webui/providers-api.js +337 -0
- package/dist/modules/webui/providers-view.js +56 -0
- package/dist/modules/webui/reasoning-view.js +15 -0
- package/dist/modules/webui/run-agent.js +81 -0
- package/dist/modules/webui/server.js +284 -0
- package/dist/modules/webui/sessions-api.js +178 -0
- package/dist/modules/webui/settings-api.js +561 -0
- package/dist/modules/webui/setup-api.js +201 -0
- package/dist/modules/webui/state.js +74 -0
- package/dist/modules/webui/util.js +261 -0
- package/dist/output/bus.js +7 -2
- package/dist/output/channel.js +80 -28
- package/dist/output/format.js +2 -1
- package/dist/output/index.js +1 -0
- package/dist/output/json-sink.js +5 -3
- package/dist/output/machine.js +4 -3
- package/dist/output/session-sink.js +4 -4
- package/dist/tools/approve.js +4 -0
- package/dist/tools/attach-image.js +5 -4
- package/dist/tools/auto-fixer.js +26 -44
- package/dist/tools/auto-tags.js +83 -0
- package/dist/tools/bash.js +193 -116
- package/dist/tools/browser.js +76 -31
- package/dist/tools/chunk-query.js +14 -6
- package/dist/tools/create-dir.js +9 -5
- package/dist/tools/delete-file.js +9 -5
- package/dist/tools/download-file.js +32 -7
- package/dist/tools/edit-file.js +52 -14
- package/dist/tools/enable-tools.js +6 -6
- package/dist/tools/executor.js +132 -27
- package/dist/tools/file-info.js +3 -1
- package/dist/tools/filter-tools.js +3 -3
- package/dist/tools/glob-tool.js +4 -1
- package/dist/tools/grep-tool.js +13 -10
- package/dist/tools/hidden-tools-block.js +6 -6
- package/dist/tools/index.js +16 -8
- package/dist/tools/list-dir.js +4 -2
- package/dist/tools/load-skill.js +7 -0
- package/dist/tools/mcp-call.js +15 -4
- package/dist/tools/meet-done.js +14 -0
- package/dist/tools/move-file.js +14 -7
- package/dist/tools/path-utils.js +29 -29
- package/dist/tools/pipeline-run.js +29 -7
- package/dist/tools/preview.js +1 -1
- package/dist/tools/process-ids.js +31 -0
- package/dist/tools/process-kill.js +9 -16
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +11 -16
- package/dist/tools/process-wait.js +63 -0
- package/dist/tools/prompt-io.js +4 -3
- package/dist/tools/question.js +172 -25
- package/dist/tools/read-file.js +55 -34
- package/dist/tools/recall.js +14 -8
- package/dist/tools/registry.js +6 -5
- package/dist/tools/remember.js +8 -4
- package/dist/tools/scope-request.js +11 -6
- package/dist/tools/search-history.js +5 -2
- package/dist/tools/session-info.js +2 -0
- package/dist/tools/set-thinking.js +8 -4
- package/dist/tools/subagent.js +59 -39
- package/dist/tools/syntax-validator.js +292 -90
- package/dist/tools/user-input.js +22 -18
- package/dist/tools/web-browse.js +9 -3
- package/dist/tools/web-fetch.js +12 -23
- package/dist/tools/web-search.js +7 -5
- package/dist/tools/write-file.js +45 -15
- package/dist/ui/action-log.js +13 -0
- package/dist/ui/box.js +4 -3
- package/dist/ui/colors.js +2 -0
- package/dist/ui/diff.js +6 -5
- package/dist/ui/index.js +1 -0
- package/dist/ui/line-editor.js +230 -122
- package/dist/ui/line-math.js +10 -2
- package/dist/ui/md-formatter.js +57 -37
- package/dist/ui/output.js +8 -3
- package/dist/ui/plan-view.js +21 -10
- package/dist/ui/renderer.js +393 -67
- package/dist/ui/spinner.js +16 -5
- package/dist/ui/table.js +15 -17
- package/dist/ui/theme.js +143 -0
- package/dist/utils/child-run.js +20 -0
- package/dist/utils/error.js +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/retry.js +2 -2
- package/dist/utils/truncate.js +1 -1
- package/package.json +1 -1
package/dist/migration/backup.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
// Бэкапы перед миграцией: config.json.bak и копия всего configDir.
|
|
1
2
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
|
2
3
|
import { join } from "path";
|
|
3
4
|
import { t } from "../i18n/index";
|
|
5
|
+
import { defaultOutputBus } from "../output";
|
|
6
|
+
/** Делает бэкапы конфига перед миграцией v1 → v2. */
|
|
4
7
|
export class BackupManager {
|
|
5
8
|
configDir;
|
|
6
9
|
constructor(configDir) {
|
|
7
10
|
this.configDir = configDir;
|
|
8
11
|
}
|
|
12
|
+
/** Копия config.json в config.json.bak. Нет файла — ничего не делает. */
|
|
9
13
|
backupConfig() {
|
|
10
14
|
const configPath = join(this.configDir, "config.json");
|
|
11
15
|
if (!existsSync(configPath))
|
|
@@ -13,24 +17,31 @@ export class BackupManager {
|
|
|
13
17
|
const bakPath = join(this.configDir, "config.json.bak");
|
|
14
18
|
copyFileSync(configPath, bakPath);
|
|
15
19
|
}
|
|
20
|
+
/** Рекурсивная копия всего configDir в `<dir>.bak` (один раз, не перезаписывает). */
|
|
16
21
|
backupAll() {
|
|
17
|
-
const bakDir = this.configDir
|
|
18
|
-
if (existsSync(bakDir))
|
|
22
|
+
const bakDir = `${this.configDir}.bak`;
|
|
23
|
+
if (existsSync(bakDir)) {
|
|
24
|
+
// Повторная миграция не перезаписывает бэкап — фиксируем в логе, чтобы
|
|
25
|
+
// «нет бэкапа за этот запуск» не выглядело как потеря данных.
|
|
26
|
+
defaultOutputBus.log("debug", "migration", `Backup already exists at ${bakDir}; keeping the previous state (not overwritten).`);
|
|
19
27
|
return;
|
|
28
|
+
}
|
|
20
29
|
if (existsSync(this.configDir)) {
|
|
21
30
|
cpSync(this.configDir, bakDir, { recursive: true });
|
|
22
31
|
}
|
|
23
32
|
}
|
|
33
|
+
/** Пишет свежий конфиг v2 в config.json (создаёт каталог при нужде). */
|
|
24
34
|
writeFreshConfig(config) {
|
|
25
35
|
if (!existsSync(this.configDir)) {
|
|
26
36
|
mkdirSync(this.configDir, { recursive: true });
|
|
27
37
|
}
|
|
28
38
|
writeFileSync(join(this.configDir, "config.json"), JSON.stringify(config, null, 2), "utf-8");
|
|
29
39
|
}
|
|
40
|
+
/** Человекочитаемая сводка: какие бэкапы есть, или что миграция не нужна. */
|
|
30
41
|
getBackupSummary() {
|
|
31
42
|
const lines = [];
|
|
32
43
|
const bakConfig = join(this.configDir, "config.json.bak");
|
|
33
|
-
const bakDir = this.configDir
|
|
44
|
+
const bakDir = `${this.configDir}.bak`;
|
|
34
45
|
if (existsSync(bakConfig)) {
|
|
35
46
|
lines.push(t("migration.config_bak"));
|
|
36
47
|
}
|
package/dist/migration/detect.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
// Детект необходимости миграции: отличает конфиг v1 от v2 и битого файла.
|
|
1
2
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
3
|
import { join } from "path";
|
|
3
4
|
const V1_ONLY_FIELDS = ["apiBase", "apiKey", "chatEndpoint", "temperature", "maxTokens"];
|
|
5
|
+
/** Проверяет, лежит ли в configDir конфиг формата v1. Битый файл — не v1. */
|
|
4
6
|
export class MigrationDetector {
|
|
5
7
|
configDir;
|
|
6
8
|
constructor(configDir) {
|
|
7
9
|
this.configDir = configDir;
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Нужна ли миграция: есть v1-поля или нет всех обязательных v2-полей.
|
|
13
|
+
* Битый/вручную правленный конфиг — не повод: loadConfig предупредит
|
|
14
|
+
* и откатится на дефолты, файл не трогаем.
|
|
15
|
+
*/
|
|
9
16
|
needsMigration() {
|
|
10
17
|
const configPath = join(this.configDir, "config.json");
|
|
11
18
|
if (!existsSync(configPath))
|
|
@@ -13,7 +20,7 @@ export class MigrationDetector {
|
|
|
13
20
|
try {
|
|
14
21
|
const content = readFileSync(configPath, "utf-8");
|
|
15
22
|
const config = JSON.parse(content);
|
|
16
|
-
if (config.version
|
|
23
|
+
if (config.version?.startsWith("2."))
|
|
17
24
|
return false;
|
|
18
25
|
for (const field of V1_ONLY_FIELDS) {
|
|
19
26
|
if (field in config)
|
|
@@ -26,17 +33,19 @@ export class MigrationDetector {
|
|
|
26
33
|
return false;
|
|
27
34
|
}
|
|
28
35
|
catch {
|
|
29
|
-
//
|
|
30
|
-
// loadConfig
|
|
36
|
+
// Битый или вручную правленный конфиг — НЕ v1, удалять нельзя.
|
|
37
|
+
// loadConfig предупредит и откатится на дефолты.
|
|
31
38
|
return false;
|
|
32
39
|
}
|
|
33
40
|
}
|
|
41
|
+
/** Есть ли старые сессии в каталоге sessions. */
|
|
34
42
|
hasOldSessions() {
|
|
35
43
|
const sessionsDir = join(this.configDir, "sessions");
|
|
36
44
|
if (!existsSync(sessionsDir))
|
|
37
45
|
return false;
|
|
38
46
|
return readdirSync(sessionsDir).length > 0;
|
|
39
47
|
}
|
|
48
|
+
/** Профиль старого формата: есть profile.json без полей platform/os. */
|
|
40
49
|
hasOldProfile() {
|
|
41
50
|
const profilePath = join(this.configDir, "profile.json");
|
|
42
51
|
if (!existsSync(profilePath))
|
package/dist/migration/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
// Хранилище артефактов сессии: запись/чтение файлов в <baseDir>/.mma/artifacts/<sessionId>.
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { resolve, relative, isAbsolute } from "node:path";
|
|
4
|
+
/** Файловое хранилище артефактов сессии (jail внутри root). */
|
|
3
5
|
export class ArtifactStore {
|
|
4
6
|
root;
|
|
5
7
|
constructor(opts) {
|
|
6
8
|
const artifactsDir = opts.artifactsDir || ".mma/artifacts";
|
|
7
9
|
this.root = resolve(opts.baseDir, artifactsDir, opts.sessionId);
|
|
8
10
|
}
|
|
9
|
-
/**
|
|
11
|
+
/** Нормализует имя к [a-zA-Z0-9._-]; никогда не пустое, не "." и не "..". */
|
|
10
12
|
static sanitizeName(name) {
|
|
11
|
-
//
|
|
12
|
-
// never ".._.._etc_passwd".
|
|
13
|
+
// Срезаем префиксы обхода: "../../etc/passwd" станет "etc_passwd", а не ".._.._etc_passwd".
|
|
13
14
|
const stripped = name.replace(/^(\.\.?[\\/])+/g, "").replace(/^\.{1,2}$/, "");
|
|
14
15
|
const cleaned = stripped
|
|
15
16
|
.replace(/[^a-zA-Z0-9._-]/g, "_")
|
|
@@ -20,7 +21,7 @@ export class ArtifactStore {
|
|
|
20
21
|
}
|
|
21
22
|
return cleaned;
|
|
22
23
|
}
|
|
23
|
-
/** True
|
|
24
|
+
/** True, если `candidate` строго внутри `root` (или равен ему). */
|
|
24
25
|
static isInside(root, candidate) {
|
|
25
26
|
const rel = relative(root, candidate);
|
|
26
27
|
if (rel === "")
|
|
@@ -28,8 +29,8 @@ export class ArtifactStore {
|
|
|
28
29
|
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
32
|
+
* Сохраняет контент в <root>/<name>.<ext>, возвращает абсолютный путь.
|
|
33
|
+
* Бросает, если путь выходит за root (defense in depth — sanitizeName уже убрал разделители).
|
|
33
34
|
*/
|
|
34
35
|
save(name, content, ext = "md") {
|
|
35
36
|
const safe = ArtifactStore.sanitizeName(name);
|
|
@@ -42,7 +43,7 @@ export class ArtifactStore {
|
|
|
42
43
|
writeFileSync(abs, content, "utf8");
|
|
43
44
|
return abs;
|
|
44
45
|
}
|
|
45
|
-
/**
|
|
46
|
+
/** Читает артефакт по имени файла (относительно root); null, если вне root или нет файла. */
|
|
46
47
|
read(path) {
|
|
47
48
|
const abs = resolve(this.root, path);
|
|
48
49
|
if (!ArtifactStore.isInside(this.root, abs))
|
|
@@ -51,7 +52,7 @@ export class ArtifactStore {
|
|
|
51
52
|
return null;
|
|
52
53
|
return readFileSync(abs, "utf8");
|
|
53
54
|
}
|
|
54
|
-
/**
|
|
55
|
+
/** Первые maxChars символов (обрезанные справа) + маркер усечения. */
|
|
55
56
|
summary(content, maxChars) {
|
|
56
57
|
if (content.length <= maxChars)
|
|
57
58
|
return content;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// JS-сниппеты для действий в странице: клик, ввод, скролл, разметка, извлечение текста.
|
|
2
|
+
/** JS для клика по элементу с индексом из снапшота. */
|
|
1
3
|
export function buildClickScript(elementIndex) {
|
|
2
4
|
return `
|
|
3
5
|
const el = document.querySelector('[data-browser-index="${elementIndex}"]');
|
|
@@ -5,6 +7,7 @@ export function buildClickScript(elementIndex) {
|
|
|
5
7
|
el.click();
|
|
6
8
|
`;
|
|
7
9
|
}
|
|
10
|
+
/** JS для ввода текста в поле: фокус, установка значения, события input/change. */
|
|
8
11
|
export function buildTypeScript(elementIndex, text) {
|
|
9
12
|
const escaped = JSON.stringify(text);
|
|
10
13
|
return `
|
|
@@ -17,6 +20,7 @@ export function buildTypeScript(elementIndex, text) {
|
|
|
17
20
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
18
21
|
`;
|
|
19
22
|
}
|
|
23
|
+
/** JS для прокрутки страницы в заданном направлении. */
|
|
20
24
|
export function buildScrollScript(direction) {
|
|
21
25
|
switch (direction) {
|
|
22
26
|
case "down":
|
|
@@ -29,6 +33,7 @@ export function buildScrollScript(direction) {
|
|
|
29
33
|
return `window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })`;
|
|
30
34
|
}
|
|
31
35
|
}
|
|
36
|
+
/** JS для простановки интерактивным элементам атрибутов data-browser-index. */
|
|
32
37
|
export function buildIndexInjectionScript() {
|
|
33
38
|
return `
|
|
34
39
|
const selectors = 'a[href], button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [contenteditable="true"]';
|
|
@@ -41,6 +46,7 @@ export function buildIndexInjectionScript() {
|
|
|
41
46
|
});
|
|
42
47
|
`;
|
|
43
48
|
}
|
|
49
|
+
/** JS для извлечения видимого текстового содержимого страницы построчно. */
|
|
44
50
|
export function buildTextExtractionScript() {
|
|
45
51
|
return `
|
|
46
52
|
const contentTags = new Set(['h1','h2','h3','h4','h5','h6','p','li','article','main','section','blockquote','pre','td','th','dt','dd','figcaption']);
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
// Драйвер через node-мост (дочерний процесс bridge-server.mjs по JSON-RPC поверх stdio).
|
|
1
2
|
import { spawn } from "child_process";
|
|
2
3
|
import { createInterface } from "readline";
|
|
3
4
|
import { dirname, join } from "path";
|
|
4
5
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { ConsoleBuffer } from "./
|
|
6
|
+
import { ConsoleBuffer } from "./console-buffer";
|
|
6
7
|
import { pickExistingPath } from "./bridge-path";
|
|
7
|
-
|
|
8
|
+
import { BROWSER_BRIDGE_REQUEST_TIMEOUT_MS as BRIDGE_REQUEST_TIMEOUT_MS } from "../../config/timeouts";
|
|
8
9
|
function bridgeScriptPath() {
|
|
9
|
-
//
|
|
10
|
+
// Резолв скрипта моста для ОБЕИХ раскладок (пути читаются с диска):
|
|
10
11
|
// src/modules/browser/bridge-server.mjs (bun run src/…)
|
|
11
|
-
// dist/modules/browser/bridge-server.mjs (
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// dist/modules/browser/bridge-server.mjs (бандл dist/main.js +
|
|
13
|
+
// ассеты от copy-assets.ts)
|
|
14
|
+
// Версия с одним кандидатом искала рядом с бандлом (dist/bridge-server.mjs),
|
|
15
|
+
// чего copy-assets.ts не создаёт, — и тул браузера падал с ENOENT
|
|
16
|
+
// в опубликованном npm-пакете (наблюдалось: v0.37.0).
|
|
16
17
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const candidates = [
|
|
18
19
|
join(dir, "bridge-server.mjs"),
|
|
@@ -20,6 +21,7 @@ function bridgeScriptPath() {
|
|
|
20
21
|
];
|
|
21
22
|
return pickExistingPath(candidates);
|
|
22
23
|
}
|
|
24
|
+
/** Драйвер поверх node-моста: команды шлются JSON-строками в stdin дочернего процесса. */
|
|
23
25
|
export class BridgeDriver {
|
|
24
26
|
proc = null;
|
|
25
27
|
rl = null;
|
|
@@ -50,7 +52,7 @@ export class BridgeDriver {
|
|
|
50
52
|
clearTimeout(timer);
|
|
51
53
|
resolve(resp);
|
|
52
54
|
});
|
|
53
|
-
this.proc.stdin.write(JSON.stringify({ id, cmd, params })
|
|
55
|
+
this.proc.stdin.write(`${JSON.stringify({ id, cmd, params })}\n`);
|
|
54
56
|
});
|
|
55
57
|
this.drainEvents(response);
|
|
56
58
|
if (!response.ok)
|
|
@@ -108,14 +110,14 @@ export class BridgeDriver {
|
|
|
108
110
|
});
|
|
109
111
|
proc.on("error", (err) => {
|
|
110
112
|
if (this.proc !== proc)
|
|
111
|
-
return; //
|
|
113
|
+
return; // протухший потомок — жив уже новый мост
|
|
112
114
|
this.failPending(`Bridge process failed to start: ${err.message}`);
|
|
113
115
|
this.proc = null;
|
|
114
116
|
this.rl = null;
|
|
115
117
|
});
|
|
116
118
|
proc.on("exit", (code) => {
|
|
117
119
|
if (this.proc !== proc)
|
|
118
|
-
return; //
|
|
120
|
+
return; // протухший потомок — жив уже новый мост
|
|
119
121
|
const stderrTail = this.lastStderr.join("").trim();
|
|
120
122
|
this.failPending(stderrTail
|
|
121
123
|
? `Bridge process exited (code ${code}): ${stderrTail.slice(0, 300)}`
|
|
@@ -184,7 +186,7 @@ export class BridgeDriver {
|
|
|
184
186
|
await this.send("close");
|
|
185
187
|
}
|
|
186
188
|
catch {
|
|
187
|
-
//
|
|
189
|
+
// игнорируем — идёт завершение работы
|
|
188
190
|
}
|
|
189
191
|
this.dispose();
|
|
190
192
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
// Поиск скрипта node-моста: раскладки src/ и dist/ (тестируемо без запуска моста).
|
|
1
2
|
import { existsSync } from "fs";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* are resolved from disk at runtime, never inlined).
|
|
4
|
+
* Первый существующий путь из кандидатов, иначе — первый кандидат.
|
|
5
|
+
* Вынесено из bridge-client.ts, чтобы резолв раскладки src-vs-dist
|
|
6
|
+
* тестировался без спавна node-моста (пути читаются с диска в рантайме, не инлайнятся).
|
|
7
7
|
*/
|
|
8
8
|
export function pickExistingPath(candidates, fallback = candidates[0]) {
|
|
9
9
|
return candidates.find((p) => existsSync(p)) ?? fallback;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const CONSOLE_MAX_LINE = 400;
|
|
2
|
+
/**
|
|
3
|
+
* Ограниченный буфер записей консоли браузера.
|
|
4
|
+
* Вынесен из `session.ts`, чтобы `driver.ts` и `bridge-client.ts` делили его
|
|
5
|
+
* БЕЗ импорта `session.ts` — это ребро давало два цикла импортов
|
|
6
|
+
* (`driver ↔ session`, `driver → bridge-client → session`).
|
|
7
|
+
*/
|
|
8
|
+
export class ConsoleBuffer {
|
|
9
|
+
entries = [];
|
|
10
|
+
maxEntries;
|
|
11
|
+
constructor(maxEntries = 20) {
|
|
12
|
+
this.maxEntries = maxEntries;
|
|
13
|
+
}
|
|
14
|
+
/** Добавить запись; пустые отбрасываются, длинные строки режутся. */
|
|
15
|
+
add(type, text) {
|
|
16
|
+
const clean = String(text).replace(/\s+/g, " ").trim();
|
|
17
|
+
if (!clean)
|
|
18
|
+
return;
|
|
19
|
+
const line = clean.length > CONSOLE_MAX_LINE ? `${clean.slice(0, CONSOLE_MAX_LINE)}…` : clean;
|
|
20
|
+
this.entries.push({ type, text: line });
|
|
21
|
+
if (this.entries.length > this.maxEntries) {
|
|
22
|
+
this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Очистить буфер. */
|
|
26
|
+
clear() {
|
|
27
|
+
this.entries = [];
|
|
28
|
+
}
|
|
29
|
+
/** Все записи копией. */
|
|
30
|
+
getAll() {
|
|
31
|
+
return [...this.entries];
|
|
32
|
+
}
|
|
33
|
+
/** Текущее число записей. */
|
|
34
|
+
get size() {
|
|
35
|
+
return this.entries.length;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
// Персистентное хранилище cookies браузера (cookies.json в cookieDir).
|
|
1
2
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
2
3
|
import { join } from "path";
|
|
4
|
+
/** Файловое хранилище кук (cookies.json). */
|
|
3
5
|
export class CookieStore {
|
|
4
6
|
filePath;
|
|
5
7
|
constructor(cookieDir) {
|
|
6
8
|
this.filePath = join(cookieDir, "cookies.json");
|
|
7
9
|
}
|
|
10
|
+
/** Сохранить куки в файл. */
|
|
8
11
|
async save(cookies) {
|
|
9
12
|
await mkdir(join(this.filePath, ".."), { recursive: true });
|
|
10
13
|
await writeFile(this.filePath, JSON.stringify(cookies, null, 2), "utf-8");
|
|
11
14
|
}
|
|
15
|
+
/** Загрузить куки; при отсутствии/битом файле — пустой список. */
|
|
12
16
|
async load() {
|
|
13
17
|
try {
|
|
14
18
|
const data = await readFile(this.filePath, "utf-8");
|
|
@@ -18,6 +22,7 @@ export class CookieStore {
|
|
|
18
22
|
return [];
|
|
19
23
|
}
|
|
20
24
|
}
|
|
25
|
+
/** Удалить все куки. */
|
|
21
26
|
async clear() {
|
|
22
27
|
await this.save([]);
|
|
23
28
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
// Playwright-драйвер: запуск Chromium, навигация, снапшоты, выбор транспорта.
|
|
1
2
|
import { chromium } from "playwright";
|
|
2
|
-
import { ConsoleBuffer } from "./
|
|
3
|
-
|
|
3
|
+
import { ConsoleBuffer } from "./console-buffer";
|
|
4
|
+
import { MMA_ENV } from "../../config/env";
|
|
5
|
+
import { BROWSER_DIRECT_LAUNCH_TIMEOUT_MS as CANONICAL_DIRECT_LAUNCH_TIMEOUT_MS } from "../../config/timeouts";
|
|
6
|
+
/** Таймаут прямого запуска Playwright до отката на node-мост. */
|
|
7
|
+
/** Публичный экспорт-алиас: то же значение из реестра. */
|
|
8
|
+
export const DIRECT_LAUNCH_TIMEOUT_MS = CANONICAL_DIRECT_LAUNCH_TIMEOUT_MS;
|
|
9
|
+
/** Оверрайды запуска из env (MMA_BROWSER_EXECUTABLE/MMA_BROWSER_CHANNEL). */
|
|
4
10
|
export function resolveBrowserLaunchOverrides(env = process.env) {
|
|
5
|
-
const executablePath = env.
|
|
6
|
-
const channel = env.
|
|
11
|
+
const executablePath = env[MMA_ENV.BROWSER_EXECUTABLE]?.trim();
|
|
12
|
+
const channel = env[MMA_ENV.BROWSER_CHANNEL]?.trim();
|
|
7
13
|
const out = {};
|
|
8
14
|
if (executablePath)
|
|
9
15
|
out.executablePath = executablePath;
|
|
@@ -11,15 +17,15 @@ export function resolveBrowserLaunchOverrides(env = process.env) {
|
|
|
11
17
|
out.channel = channel;
|
|
12
18
|
return out;
|
|
13
19
|
}
|
|
14
|
-
/**
|
|
20
|
+
/** Системные браузеры, если bundled-ревизия Chromium отсутствует. */
|
|
15
21
|
const SYSTEM_CHANNEL_FALLBACKS = ["chrome", "msedge"];
|
|
16
22
|
function isMissingBrowserError(err) {
|
|
17
23
|
return /Executable doesn't exist|is not found|looks like Playwright was just installed/i.test(String(err?.message ?? err));
|
|
18
24
|
}
|
|
19
25
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
26
|
+
* Запуск Chromium с фолбэком на системный Chrome/Edge, если bundled-ревизии
|
|
27
|
+
* нет (CDN недоступен). Явный оверрайд всегда побеждает и подавляет фолбэк —
|
|
28
|
+
* выбор пользователя должен падать громко.
|
|
23
29
|
*/
|
|
24
30
|
export async function launchChromiumWithFallback(launch, cfg, overrides = {}) {
|
|
25
31
|
const base = { headless: cfg.headless, timeout: cfg.timeoutMs, ...overrides };
|
|
@@ -45,6 +51,7 @@ export async function launchChromiumWithFallback(launch, cfg, overrides = {}) {
|
|
|
45
51
|
function launchChromium(cfg, overrides) {
|
|
46
52
|
return launchChromiumWithFallback((opts) => chromium.launch(opts), cfg, overrides);
|
|
47
53
|
}
|
|
54
|
+
/** Драйвер поверх Playwright: страница, консоль, сетевые ошибки. */
|
|
48
55
|
export class PlaywrightDriver {
|
|
49
56
|
browser = null;
|
|
50
57
|
context = null;
|
|
@@ -145,6 +152,7 @@ export class PlaywrightDriver {
|
|
|
145
152
|
return this.page;
|
|
146
153
|
}
|
|
147
154
|
}
|
|
155
|
+
/** Создать драйвер: прямой Playwright, при неудаче — node-мост. */
|
|
148
156
|
export function createBrowserDriver(config) {
|
|
149
157
|
const common = {
|
|
150
158
|
headless: config.headless,
|
|
@@ -153,9 +161,9 @@ export function createBrowserDriver(config) {
|
|
|
153
161
|
overrides: resolveBrowserLaunchOverrides(),
|
|
154
162
|
};
|
|
155
163
|
return (async () => {
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
164
|
+
// Транспорт pipe/websocket у Playwright сломан под Bun на части платформ
|
|
165
|
+
// (см. oven-sh/bun#31105, #9911). Под Bun всегда идём через node-мост,
|
|
166
|
+
// под Node сначала пробуем прямой путь.
|
|
159
167
|
if (process.versions.bun) {
|
|
160
168
|
return launchBridge(config, common);
|
|
161
169
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
// Публичный фасад модуля браузера: сессия, драйверы, снапшоты, типы.
|
|
1
2
|
export { BrowserModule } from "./module";
|
|
2
|
-
export { BrowserSession, BrowserActionTracker
|
|
3
|
+
export { BrowserSession, BrowserActionTracker } from "./session";
|
|
4
|
+
export { ConsoleBuffer } from "./console-buffer";
|
|
3
5
|
export { CookieStore } from "./cookie-store";
|
|
4
6
|
export { createBrowserDriver, PlaywrightDriver } from "./driver";
|
|
5
7
|
export { BridgeDriver } from "./bridge-client";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createBrowserTool, closeBrowserSession } from "../../tools/browser";
|
|
2
|
+
/** Модуль headless-браузера: промпт workflow, тул, плагин закрытия сессии. */
|
|
2
3
|
export class BrowserModule {
|
|
3
4
|
name = "browser";
|
|
4
5
|
getSystemPromptBlock() {
|
|
@@ -8,7 +9,7 @@ export class BrowserModule {
|
|
|
8
9
|
"Workflow: open(url) → read snapshot → click/type on element numbers → read updated snapshot.",
|
|
9
10
|
'Each snapshot includes a "Content" section with the visible page text, a "Console" section with browser messages, and network errors. Use these to understand the page, detect JS errors, and verify the result of your actions.',
|
|
10
11
|
'Always start with "open" action. Use element numbers from snapshot for click/type targets.',
|
|
11
|
-
'
|
|
12
|
+
'The "screenshot" action is delivered automatically: a vision model receives the image, a text-only model receives the text snapshot instead. You do not need to decide — just call it and read what you get.',
|
|
12
13
|
].join("\n"),
|
|
13
14
|
priority: "normal",
|
|
14
15
|
essential: false,
|
|
@@ -3,39 +3,14 @@ import { extractInteractiveElements, formatSnapshot } from "./snapshot";
|
|
|
3
3
|
import { buildClickScript, buildTypeScript, buildScrollScript, buildIndexInjectionScript, buildTextExtractionScript, } from "./actions";
|
|
4
4
|
import { CookieStore } from "./cookie-store";
|
|
5
5
|
import { t } from "../../i18n/index";
|
|
6
|
-
|
|
7
|
-
export class ConsoleBuffer {
|
|
8
|
-
entries = [];
|
|
9
|
-
maxEntries;
|
|
10
|
-
constructor(maxEntries = 20) {
|
|
11
|
-
this.maxEntries = maxEntries;
|
|
12
|
-
}
|
|
13
|
-
add(type, text) {
|
|
14
|
-
const clean = String(text).replace(/\s+/g, " ").trim();
|
|
15
|
-
if (!clean)
|
|
16
|
-
return;
|
|
17
|
-
const line = clean.length > CONSOLE_MAX_LINE ? clean.slice(0, CONSOLE_MAX_LINE) + "…" : clean;
|
|
18
|
-
this.entries.push({ type, text: line });
|
|
19
|
-
if (this.entries.length > this.maxEntries) {
|
|
20
|
-
this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
clear() {
|
|
24
|
-
this.entries = [];
|
|
25
|
-
}
|
|
26
|
-
getAll() {
|
|
27
|
-
return [...this.entries];
|
|
28
|
-
}
|
|
29
|
-
get size() {
|
|
30
|
-
return this.entries.length;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
6
|
+
/** Ловит N одинаковых действий подряд на одном URL и возвращает предупреждение. */
|
|
33
7
|
export class BrowserActionTracker {
|
|
34
8
|
history = [];
|
|
35
9
|
threshold;
|
|
36
10
|
constructor(threshold = 3) {
|
|
37
11
|
this.threshold = threshold;
|
|
38
12
|
}
|
|
13
|
+
/** Записать действие; при серии повторов — текст предупреждения, иначе null. */
|
|
39
14
|
record(action, args, url) {
|
|
40
15
|
this.history.push({ action, args, url });
|
|
41
16
|
if (this.history.length < this.threshold)
|
|
@@ -52,10 +27,12 @@ export class BrowserActionTracker {
|
|
|
52
27
|
}
|
|
53
28
|
return null;
|
|
54
29
|
}
|
|
30
|
+
/** Сбросить историю (вызывается при open). */
|
|
55
31
|
reset() {
|
|
56
32
|
this.history = [];
|
|
57
33
|
}
|
|
58
34
|
}
|
|
35
|
+
/** Сессия: ленивый драйвер, куки, выполнение действий модели, снапшоты. */
|
|
59
36
|
export class BrowserSession {
|
|
60
37
|
driver = null;
|
|
61
38
|
config;
|
|
@@ -71,9 +48,11 @@ export class BrowserSession {
|
|
|
71
48
|
this.config = config;
|
|
72
49
|
this.cookieStore = new CookieStore(config.cookieDir);
|
|
73
50
|
}
|
|
51
|
+
/** Копия текущего состояния (открыта/URL/заголовок/счётчик элементов). */
|
|
74
52
|
getState() {
|
|
75
53
|
return { ...this.state };
|
|
76
54
|
}
|
|
55
|
+
/** Выполнить действие модели (open/click/type/…); ошибки — текстом в ActionResult. */
|
|
77
56
|
async execute(action, args) {
|
|
78
57
|
try {
|
|
79
58
|
let result;
|
|
@@ -117,7 +96,7 @@ export class BrowserSession {
|
|
|
117
96
|
if (action !== "open") {
|
|
118
97
|
const warning = this.actionTracker.record(action, args, this.driver?.url() || "");
|
|
119
98
|
if (warning && result.success) {
|
|
120
|
-
result.output = result.output
|
|
99
|
+
result.output = `${result.output}\n\n⚠️ ${warning}`;
|
|
121
100
|
}
|
|
122
101
|
}
|
|
123
102
|
return result;
|
|
@@ -186,13 +165,13 @@ export class BrowserSession {
|
|
|
186
165
|
async open(url) {
|
|
187
166
|
if (!url)
|
|
188
167
|
return { success: false, output: t("browser.url_required") };
|
|
189
|
-
//
|
|
168
|
+
// Блок опасных схем (file:, chrome:, about:, data:) — обход запретов исключён.
|
|
190
169
|
if (/^(file|chrome|about|data|javascript|view-source):/i.test(url)) {
|
|
191
170
|
return { success: false, output: `[SECURITY BLOCKED] scheme not allowed: ${url}` };
|
|
192
171
|
}
|
|
193
172
|
if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("file://")) {
|
|
194
173
|
const isLocalhost = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?$/i.test(url);
|
|
195
|
-
url = isLocalhost ?
|
|
174
|
+
url = isLocalhost ? `http://${url}` : `https://${url}`;
|
|
196
175
|
}
|
|
197
176
|
await this.launch();
|
|
198
177
|
if (!this.driver)
|
|
@@ -330,6 +309,7 @@ export class BrowserSession {
|
|
|
330
309
|
const snapshot = await this.takeSnapshot();
|
|
331
310
|
return { success: true, output: snapshot };
|
|
332
311
|
}
|
|
312
|
+
/** Закрыть сессию: сохранить куки, убить драйвер, сбросить состояние. */
|
|
333
313
|
async close() {
|
|
334
314
|
await this.saveCookies();
|
|
335
315
|
if (this.driver) {
|
|
@@ -24,11 +24,11 @@ function inferRole(tag, type) {
|
|
|
24
24
|
}
|
|
25
25
|
function getName(tag, attrs, html, matchIndex) {
|
|
26
26
|
const fromAttr = attrs["aria-label"] ||
|
|
27
|
-
attrs
|
|
28
|
-
attrs
|
|
29
|
-
attrs
|
|
30
|
-
attrs
|
|
31
|
-
attrs
|
|
27
|
+
attrs.placeholder ||
|
|
28
|
+
attrs.title ||
|
|
29
|
+
attrs.alt ||
|
|
30
|
+
attrs.value ||
|
|
31
|
+
attrs.name ||
|
|
32
32
|
"";
|
|
33
33
|
if (fromAttr)
|
|
34
34
|
return fromAttr;
|
|
@@ -47,6 +47,10 @@ function getName(tag, attrs, html, matchIndex) {
|
|
|
47
47
|
.trim();
|
|
48
48
|
return text.slice(0, 50);
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Вытащить интерактивные элементы из HTML (ссылки, кнопки, поля).
|
|
52
|
+
* @param html сырой HTML страницы. @param maxElements лимит элементов.
|
|
53
|
+
*/
|
|
50
54
|
export function extractInteractiveElements(html, maxElements = 30) {
|
|
51
55
|
const elements = [];
|
|
52
56
|
const tagRegex = /<(a|button|input|textarea|select|div|span)([^>]*)>/gi;
|
|
@@ -62,19 +66,19 @@ export function extractInteractiveElements(html, maxElements = 30) {
|
|
|
62
66
|
while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
|
|
63
67
|
attrs[attrMatch[1].toLowerCase()] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
64
68
|
}
|
|
65
|
-
if (attrs
|
|
69
|
+
if (attrs.style?.includes("display:none") || attrs.style?.includes("display: none"))
|
|
66
70
|
continue;
|
|
67
|
-
if (attrs
|
|
71
|
+
if (attrs.hidden !== undefined)
|
|
68
72
|
continue;
|
|
69
73
|
if (attrs["aria-hidden"] === "true")
|
|
70
74
|
continue;
|
|
71
|
-
if (tag === "input" && attrs
|
|
75
|
+
if (tag === "input" && attrs.type === "hidden")
|
|
72
76
|
continue;
|
|
73
|
-
const role = attrs
|
|
74
|
-
const isInteractive = ["a", "button", "input", "textarea", "select"].includes(tag) || attrs
|
|
77
|
+
const role = attrs.role || inferRole(tag, attrs.type);
|
|
78
|
+
const isInteractive = ["a", "button", "input", "textarea", "select"].includes(tag) || attrs.role !== undefined;
|
|
75
79
|
if (!isInteractive)
|
|
76
80
|
continue;
|
|
77
|
-
if (tag === "a" && !attrs
|
|
81
|
+
if (tag === "a" && !attrs.href)
|
|
78
82
|
continue;
|
|
79
83
|
const name = getName(tag, attrs, html, match.index);
|
|
80
84
|
elements.push({
|
|
@@ -82,13 +86,14 @@ export function extractInteractiveElements(html, maxElements = 30) {
|
|
|
82
86
|
tag,
|
|
83
87
|
role,
|
|
84
88
|
name,
|
|
85
|
-
value: attrs
|
|
86
|
-
href: attrs
|
|
89
|
+
value: attrs.value,
|
|
90
|
+
href: attrs.href,
|
|
87
91
|
visible: true,
|
|
88
92
|
});
|
|
89
93
|
}
|
|
90
94
|
return elements;
|
|
91
95
|
}
|
|
96
|
+
/** Обрезать текст по лимиту, предпочтительно по границе строки. */
|
|
92
97
|
export function truncateText(text, maxChars) {
|
|
93
98
|
if (maxChars <= 0)
|
|
94
99
|
return "";
|
|
@@ -99,6 +104,7 @@ export function truncateText(text, maxChars) {
|
|
|
99
104
|
const safe = lastNewline > maxChars * 0.6 ? cut.slice(0, lastNewline) : cut;
|
|
100
105
|
return `${safe}\n${t("browser.truncated")}`;
|
|
101
106
|
}
|
|
107
|
+
/** Собрать текстовый снапшот: элементы, контент, консоль, сетевые ошибки. */
|
|
102
108
|
export function formatSnapshot(snapshot, maxContentChars = DEFAULT_BROWSER_CONFIG.maxContentChars) {
|
|
103
109
|
const lines = [];
|
|
104
110
|
lines.push(`Page: "${snapshot.title}"`);
|