minimal-agent 0.10.0 → 0.12.0
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/README.md +11 -3
- package/package.json +52 -5
- package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
- package/plugins/computer_use/README.md +94 -0
- package/plugins/computer_use/app/_template.md +142 -0
- package/plugins/computer_use/app/chrome.md +165 -0
- package/plugins/computer_use/app/finder.md +76 -0
- package/plugins/computer_use/app/macos-system-settings.md +81 -0
- package/plugins/computer_use/app/microsoft-excel.md +80 -0
- package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
- package/plugins/computer_use/app/microsoft-word.md +79 -0
- package/plugins/computer_use/app/wechat.md +413 -0
- package/plugins/computer_use/app/weread.md +319 -0
- package/plugins/computer_use/commands/computeruse.md +25 -0
- package/plugins/computer_use/commands/cu.md +9 -0
- package/plugins/computer_use/plugin.js +26 -0
- package/plugins/computer_use/plugin.ts +30 -0
- package/plugins/computer_use/src/capture.js +90 -0
- package/plugins/computer_use/src/capture.ts +104 -0
- package/plugins/computer_use/src/commands.js +91 -0
- package/plugins/computer_use/src/commands.ts +103 -0
- package/plugins/computer_use/src/daemon.js +134 -0
- package/plugins/computer_use/src/daemon.ts +161 -0
- package/plugins/computer_use/src/manuals.js +135 -0
- package/plugins/computer_use/src/manuals.ts +159 -0
- package/plugins/computer_use/src/verify.js +168 -0
- package/plugins/computer_use/src/verify.ts +219 -0
- package/plugins/computer_use/test/commands.test.ts +73 -0
- package/plugins/cron-runner/plugin.ts +38 -0
- package/plugins/cron-runner/src/commands.ts +373 -0
- package/plugins/cron-runner/src/cronTime.ts +155 -0
- package/plugins/cron-runner/src/lock.ts +79 -0
- package/plugins/cron-runner/src/runs.ts +99 -0
- package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
- package/plugins/cron-runner/src/scheduler.ts +376 -0
- package/plugins/cron-runner/src/schema.ts +139 -0
- package/plugins/cron-runner/src/service.ts +193 -0
- package/plugins/cron-runner/src/store.ts +333 -0
- package/plugins/cron-runner/src/types.ts +141 -0
- package/plugins/cron-runner/test/cronTime.test.ts +41 -0
- package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
- package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
- package/plugins/cron-runner/test/service.test.ts +76 -0
- package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
- package/plugins/workflow-runner/plugin.js +56 -2
- package/plugins/workflow-runner/plugin.ts +113 -0
- package/plugins/workflow-runner/src/agentCommon.js +43 -4
- package/plugins/workflow-runner/src/agentCommon.ts +435 -0
- package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
- package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
- package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
- package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
- package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
- package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
- package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
- package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
- package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
- package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
- package/plugins/workflow-runner/src/dslManifest.ts +127 -0
- package/plugins/workflow-runner/src/expressions.ts +622 -0
- package/plugins/workflow-runner/src/index.js +13 -4
- package/plugins/workflow-runner/src/index.ts +338 -0
- package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
- package/plugins/workflow-runner/src/loader.js +23 -0
- package/plugins/workflow-runner/src/loader.ts +721 -0
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +64 -0
- package/plugins/workflow-runner/src/recovery.ts +149 -0
- package/plugins/workflow-runner/src/runner.js +75 -659
- package/plugins/workflow-runner/src/runner.ts +604 -0
- package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
- package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
- package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
- package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
- package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
- package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
- package/plugins/workflow-runner/src/stepPipeline.js +277 -0
- package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
- package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +37 -1
- package/plugins/workflow-runner/src/types.ts +602 -0
- package/plugins/workflow-runner/src/workflowState.ts +733 -0
- package/plugins/workflow-runner/test/agent.test.ts +587 -0
- package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
- package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
- package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
- package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
- package/plugins/workflow-runner/test/context-files.test.ts +320 -0
- package/plugins/workflow-runner/test/e2e.test.ts +370 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
- package/plugins/workflow-runner/test/expressions.test.ts +397 -0
- package/plugins/workflow-runner/test/foundation.test.ts +126 -0
- package/plugins/workflow-runner/test/graceful.test.ts +139 -0
- package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
- package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
- package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
- package/plugins/workflow-runner/test/loader.test.ts +617 -0
- package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
- package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
- package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
- package/plugins/workflow-runner/test/recovery.test.ts +265 -0
- package/plugins/workflow-runner/test/runner.test.ts +911 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
- package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
- package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
- package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
- package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
- package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
- package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
- package/plugins/workflow-runner/test/word.load.test.ts +208 -0
- package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
- package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
- package/skills/browser-automation/SKILL.md +9 -19
- package/skills/web-crawler/SKILL.md +9 -11
- package/src/askBridge.ts +249 -0
- package/src/bootstrap/cwdArg.js +10 -3
- package/src/bootstrap/cwdArg.ts +31 -0
- package/src/bootstrap/workingDir.ts +48 -0
- package/src/cli/args.js +70 -7
- package/src/cli/args.ts +146 -0
- package/src/cli/computerUse.js +185 -0
- package/src/cli/computerUse.ts +221 -0
- package/src/cli/configMenu.js +274 -202
- package/src/cli/configMenu.tsx +521 -0
- package/src/cli/memoryAccess.js +45 -0
- package/src/cli/memoryAccess.ts +53 -0
- package/src/cli/print.js +119 -5
- package/src/cli/print.ts +597 -0
- package/src/cli/quickModelSetup.js +60 -0
- package/src/cli/quickModelSetup.tsx +96 -0
- package/src/cli/streamJson.js +80 -4
- package/src/cli/streamJson.ts +433 -0
- package/src/cli/wizards/advisorWizard.js +1 -0
- package/src/cli/wizards/advisorWizard.tsx +195 -0
- package/src/cli/wizards/agentRuntimeWizard.js +84 -0
- package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
- package/src/cli/wizards/browserWizard.js +55 -0
- package/src/cli/wizards/browserWizard.tsx +77 -0
- package/src/cli/wizards/contextAdvisorWizard.js +84 -0
- package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
- package/src/cli/wizards/fieldForm.js +7 -4
- package/src/cli/wizards/fieldForm.tsx +250 -0
- package/src/cli/wizards/imageWizard.js +4 -5
- package/src/cli/wizards/imageWizard.tsx +96 -0
- package/src/cli/wizards/openRouterWizard.tsx +48 -0
- package/src/cli/wizards/providerPresets.js +1 -8
- package/src/cli/wizards/providerPresets.ts +106 -0
- package/src/cli/wizards/providerWizard.js +152 -205
- package/src/cli/wizards/providerWizard.tsx +266 -0
- package/src/cli/wizards/searchWizard.tsx +47 -0
- package/src/cli/wizards/ttsWizard.js +4 -3
- package/src/cli/wizards/ttsWizard.tsx +61 -0
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configDiagnostics.js +117 -0
- package/src/config/configDiagnostics.ts +166 -0
- package/src/config/configFile.js +52 -32
- package/src/config/configFile.ts +633 -0
- package/src/config/configManagement.js +126 -0
- package/src/config/configManagement.ts +159 -0
- package/src/config/configService.js +66 -11
- package/src/config/configService.ts +554 -0
- package/src/config/numericLimits.js +61 -0
- package/src/config/numericLimits.ts +80 -0
- package/src/config.ts +109 -0
- package/src/context/agentHome.ts +11 -0
- package/src/context/archive.ts +144 -0
- package/src/context/budgetPlan.js +54 -0
- package/src/context/budgetPlan.ts +88 -0
- package/src/context/compact.js +179 -106
- package/src/context/compact.ts +584 -0
- package/src/context/contextBundle.js +119 -0
- package/src/context/contextBundle.ts +176 -0
- package/src/context/contextLock.ts +84 -0
- package/src/context/microCompactLite.ts +218 -0
- package/src/context/persistContext.ts +177 -0
- package/src/context/reactiveCompact.js +19 -4
- package/src/context/reactiveCompact.ts +240 -0
- package/src/context/recentDirs.ts +42 -0
- package/src/context/sessionPath.ts +206 -0
- package/src/context/sessionRegistry.js +8 -2
- package/src/context/sessionRegistry.ts +414 -0
- package/src/context/snipCompact.ts +163 -0
- package/src/context/tokenCounter.ts +106 -0
- package/src/context/workspaceIndex.js +2 -2
- package/src/context/workspaceIndex.ts +751 -0
- package/src/entry.compiled.tsx +20 -0
- package/src/harness/claimValidation.js +9 -0
- package/src/harness/claimValidation.ts +22 -0
- package/src/harness/evidenceLedger.js +92 -0
- package/src/harness/evidenceLedger.ts +96 -0
- package/src/harness/stallGuard.js +60 -0
- package/src/harness/stallGuard.ts +81 -0
- package/src/humanIntervention.js +129 -0
- package/src/humanIntervention.ts +216 -0
- package/src/infra/verification.ts +295 -0
- package/src/irreversible/rules.js +216 -0
- package/src/irreversible/rules.ts +261 -0
- package/src/llm/client.js +40 -3
- package/src/llm/client.ts +621 -0
- package/src/llm/promptCache.js +27 -0
- package/src/llm/promptCache.ts +41 -0
- package/src/llm/providers/anthropicFormat.js +41 -5
- package/src/llm/providers/anthropicFormat.ts +290 -0
- package/src/llm/providers/openaiFormat.js +186 -8
- package/src/llm/providers/openaiFormat.ts +428 -0
- package/src/loop.js +614 -63
- package/src/loop.ts +1454 -0
- package/src/main.js +67 -10
- package/src/main.tsx +344 -0
- package/src/memory/codec.js +204 -0
- package/src/memory/codec.ts +249 -0
- package/src/memory/constants.js +34 -0
- package/src/memory/constants.ts +50 -0
- package/src/memory/episodicMemory.js +684 -0
- package/src/memory/episodicMemory.ts +815 -0
- package/src/memory/fileStore.js +148 -0
- package/src/memory/fileStore.ts +184 -0
- package/src/memory/memoryService.js +570 -0
- package/src/memory/memoryService.ts +859 -0
- package/src/memory/migration.js +128 -0
- package/src/memory/migration.ts +160 -0
- package/src/memory/repository.js +789 -0
- package/src/memory/repository.ts +893 -0
- package/src/memory/retrieval.js +108 -0
- package/src/memory/retrieval.ts +145 -0
- package/src/memory/searchIndex.js +107 -0
- package/src/memory/searchIndex.ts +153 -0
- package/src/memory/topicView.js +82 -0
- package/src/memory/topicView.ts +95 -0
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.js +2 -0
- package/src/memory/types.ts +186 -0
- package/src/messageQueue.ts +123 -0
- package/src/multimodal.js +128 -46
- package/src/multimodal.ts +349 -0
- package/src/plugin-sdk.js +21 -1
- package/src/plugin-sdk.ts +134 -0
- package/src/plugins/builtinPluginIds.js +47 -0
- package/src/plugins/builtinPluginIds.ts +51 -0
- package/src/plugins/builtinRegistry.ts +43 -0
- package/src/plugins/commandRouter.js +40 -26
- package/src/plugins/commandRouter.ts +261 -0
- package/src/plugins/hookEngine.ts +341 -0
- package/src/plugins/pluginApi.ts +133 -0
- package/src/plugins/pluginLifecycle.ts +34 -0
- package/src/plugins/pluginLoader.ts +147 -0
- package/src/plugins/pluginManifest.js +127 -0
- package/src/plugins/pluginManifest.ts +152 -0
- package/src/plugins/pluginRunner.js +47 -2
- package/src/plugins/pluginRunner.ts +207 -0
- package/src/plugins/transcript.ts +208 -0
- package/src/prompts/lint.js +169 -0
- package/src/prompts/lint.ts +220 -0
- package/src/prompts/pluginList.js +32 -16
- package/src/prompts/pluginList.ts +86 -0
- package/src/prompts/policyRegistry.js +318 -0
- package/src/prompts/policyRegistry.ts +358 -0
- package/src/prompts/projectInstructions.js +24 -0
- package/src/prompts/projectInstructions.ts +81 -0
- package/src/prompts/promptAssembly.js +311 -202
- package/src/prompts/promptAssembly.ts +510 -0
- package/src/prompts/skillList.js +106 -45
- package/src/prompts/skillList.ts +198 -0
- package/src/prompts/system.js +31 -6
- package/src/prompts/system.ts +81 -0
- package/src/runtime/agentRuntimeContext.js +53 -0
- package/src/runtime/agentRuntimeContext.ts +112 -0
- package/src/session/runAgentTurn.js +71 -12
- package/src/session/runAgentTurn.ts +181 -0
- package/src/session/runTurn.js +29 -0
- package/src/session/runTurn.ts +160 -0
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +79 -3
- package/src/session/sessionState.ts +134 -0
- package/src/toolGate.ts +50 -0
- package/src/tools/advisor/advisor.js +76 -15
- package/src/tools/advisor/advisor.ts +512 -0
- package/src/tools/agent/agent.js +245 -29
- package/src/tools/agent/agent.ts +837 -0
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +20 -8
- package/src/tools/agent/agentTypes.ts +94 -0
- package/src/tools/agent/coordinator.ts +111 -0
- package/src/tools/agent/worktree.ts +296 -0
- package/src/tools/ask/ask.js +17 -0
- package/src/tools/ask/ask.ts +122 -0
- package/src/tools/bash/attachImage.js +223 -0
- package/src/tools/bash/attachImage.ts +281 -0
- package/src/tools/bash/bash.js +132 -14
- package/src/tools/bash/bash.ts +642 -0
- package/src/tools/bash/semantics.ts +103 -0
- package/src/tools/bash/warnings.js +17 -11
- package/src/tools/bash/warnings.ts +117 -0
- package/src/tools/capabilities.js +42 -0
- package/src/tools/capabilities.ts +69 -0
- package/src/tools/computeruse/accessibilityPermission.js +261 -0
- package/src/tools/computeruse/accessibilityPermission.ts +294 -0
- package/src/tools/computeruse/axSearch.js +96 -0
- package/src/tools/computeruse/axSearch.ts +120 -0
- package/src/tools/computeruse/axSnap.js +272 -0
- package/src/tools/computeruse/axSnap.ts +349 -0
- package/src/tools/computeruse/capabilityLabels.js +95 -0
- package/src/tools/computeruse/capabilityLabels.ts +115 -0
- package/src/tools/computeruse/computerUseService.js +2235 -0
- package/src/tools/computeruse/computerUseService.ts +2686 -0
- package/src/tools/computeruse/computeruse.js +2742 -0
- package/src/tools/computeruse/computeruse.ts +3265 -0
- package/src/tools/computeruse/diagnosticTrace.js +154 -0
- package/src/tools/computeruse/diagnosticTrace.ts +195 -0
- package/src/tools/computeruse/diagnostics.js +369 -0
- package/src/tools/computeruse/diagnostics.ts +453 -0
- package/src/tools/computeruse/grants.js +53 -0
- package/src/tools/computeruse/grants.ts +70 -0
- package/src/tools/computeruse/humanBoundary.js +159 -0
- package/src/tools/computeruse/humanBoundary.ts +199 -0
- package/src/tools/computeruse/intervention.js +77 -0
- package/src/tools/computeruse/intervention.ts +110 -0
- package/src/tools/computeruse/manualSchema.js +709 -0
- package/src/tools/computeruse/manualSchema.ts +916 -0
- package/src/tools/computeruse/observationImages.js +705 -0
- package/src/tools/computeruse/observationImages.ts +889 -0
- package/src/tools/computeruse/protocol.js +60 -0
- package/src/tools/computeruse/protocol.ts +515 -0
- package/src/tools/computeruse/telemetryLog.js +57 -0
- package/src/tools/computeruse/telemetryLog.ts +67 -0
- package/src/tools/computeruse/vision/appGuide.js +325 -0
- package/src/tools/computeruse/vision/appGuide.ts +482 -0
- package/src/tools/computeruse/vision/focus.js +291 -0
- package/src/tools/computeruse/vision/focus.ts +382 -0
- package/src/tools/computeruse/vision/frames.js +248 -0
- package/src/tools/computeruse/vision/frames.ts +329 -0
- package/src/tools/computeruse/vision/geometry.js +131 -0
- package/src/tools/computeruse/vision/geometry.ts +163 -0
- package/src/tools/computeruse/vision/imaging.js +201 -0
- package/src/tools/computeruse/vision/imaging.ts +281 -0
- package/src/tools/computeruse/vision/png.js +274 -0
- package/src/tools/computeruse/vision/png.ts +317 -0
- package/src/tools/computeruse/vision/raster.js +320 -0
- package/src/tools/computeruse/vision/raster.ts +408 -0
- package/src/tools/computeruse/vision/settle.js +131 -0
- package/src/tools/computeruse/vision/settle.ts +220 -0
- package/src/tools/computeruse/vision/telemetry.js +196 -0
- package/src/tools/computeruse/vision/telemetry.ts +241 -0
- package/src/tools/computeruse/visionOcr.js +115 -0
- package/src/tools/computeruse/visionOcr.ts +128 -0
- package/src/tools/computeruse/waitFor.js +237 -0
- package/src/tools/computeruse/waitFor.ts +291 -0
- package/src/tools/download/download.js +10 -6
- package/src/tools/download/download.ts +187 -0
- package/src/tools/edit/edit.js +30 -18
- package/src/tools/edit/edit.ts +349 -0
- package/src/tools/edit/multi-edit.js +25 -14
- package/src/tools/edit/multi-edit.ts +223 -0
- package/src/tools/effectiveToolSet.js +73 -0
- package/src/tools/effectiveToolSet.ts +91 -0
- package/src/tools/genimage/genimage.ts +257 -0
- package/src/tools/genspeech/genspeech.ts +83 -0
- package/src/tools/glob/glob.js +176 -29
- package/src/tools/glob/glob.ts +283 -0
- package/src/tools/grep/grep.js +61 -19
- package/src/tools/grep/grep.ts +239 -0
- package/src/tools/grep/rgPath.ts +206 -0
- package/src/tools/index.js +99 -5
- package/src/tools/index.ts +293 -0
- package/src/tools/lsp/config.js +113 -0
- package/src/tools/lsp/config.ts +113 -0
- package/src/tools/lsp/lsp.js +256 -0
- package/src/tools/lsp/lsp.ts +274 -0
- package/src/tools/lsp/service.js +303 -0
- package/src/tools/lsp/service.ts +296 -0
- package/src/tools/memory/memory.js +520 -0
- package/src/tools/memory/memory.ts +625 -0
- package/src/tools/memory/policy.js +53 -0
- package/src/tools/memory/policy.ts +72 -0
- package/src/tools/notebook/notebookEdit.js +114 -0
- package/src/tools/notebook/notebookEdit.ts +126 -0
- package/src/tools/powershell/powershell.js +210 -0
- package/src/tools/powershell/powershell.ts +239 -0
- package/src/tools/presentresult/presentresult.js +145 -0
- package/src/tools/presentresult/presentresult.ts +156 -0
- package/src/tools/read/read.js +371 -163
- package/src/tools/read/read.ts +558 -0
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +51 -0
- package/src/tools/shared/fileActivity.ts +66 -0
- package/src/tools/shared/fileMutationBus.js +13 -0
- package/src/tools/shared/fileMutationBus.ts +14 -0
- package/src/tools/shared/fileState.js +73 -65
- package/src/tools/shared/fileState.ts +145 -0
- package/src/tools/shared/fileUtils.js +81 -4
- package/src/tools/shared/fileUtils.ts +504 -0
- package/src/tools/shared/killTree.ts +50 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/schemas.ts +20 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +275 -0
- package/src/tools/tasks/backgroundTaskService.ts +369 -0
- package/src/tools/tasks/processEvidence.js +24 -0
- package/src/tools/tasks/processEvidence.ts +34 -0
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +113 -0
- package/src/tools/tasks/taskOutput.ts +145 -0
- package/src/tools/tasks/taskStop.js +74 -0
- package/src/tools/tasks/taskStop.ts +99 -0
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/types.js +1 -1
- package/src/tools/types.ts +24 -0
- package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
- package/src/tools/webbrowser/browser.js +214 -11
- package/src/tools/webbrowser/browser.ts +293 -0
- package/src/tools/webbrowser/browserControl.js +153 -48
- package/src/tools/webbrowser/browserControl.ts +262 -0
- package/src/tools/webbrowser/browserDaemon.js +147 -39
- package/src/tools/webbrowser/browserDaemon.ts +270 -0
- package/src/tools/webbrowser/browserOutcome.js +273 -0
- package/src/tools/webbrowser/browserOutcome.ts +306 -0
- package/src/tools/webbrowser/browserRunRegistry.js +150 -0
- package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
- package/src/tools/webbrowser/browserService.js +20 -6
- package/src/tools/webbrowser/browserService.ts +56 -0
- package/src/tools/webbrowser/browserStream.js +30 -1
- package/src/tools/webbrowser/browserStream.ts +216 -0
- package/src/tools/webbrowser/browserTabs.js +143 -0
- package/src/tools/webbrowser/browserTabs.ts +210 -0
- package/src/tools/webbrowser/webbrowser.js +831 -150
- package/src/tools/webbrowser/webbrowser.ts +1537 -0
- package/src/tools/webfetch/preapproved.ts +274 -0
- package/src/tools/webfetch/webfetch.js +24 -23
- package/src/tools/webfetch/webfetch.ts +383 -0
- package/src/tools/websearch/websearch.js +12 -11
- package/src/tools/websearch/websearch.ts +292 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +27 -44
- package/src/tools/write/write.ts +136 -0
- package/src/types.ts +960 -0
- package/src/ui/App.js +216 -12
- package/src/ui/App.tsx +879 -0
- package/src/ui/AskQuestion.tsx +300 -0
- package/src/ui/InputBox.js +77 -1
- package/src/ui/InputBox.tsx +567 -0
- package/src/ui/LiveArea.tsx +344 -0
- package/src/ui/MessageList.js +27 -6
- package/src/ui/MessageList.tsx +559 -0
- package/src/ui/ModelPicker.tsx +107 -0
- package/src/ui/QueuedMessagesBanner.tsx +51 -0
- package/src/ui/Root.js +6 -8
- package/src/ui/Root.tsx +100 -0
- package/src/ui/SessionPicker.tsx +169 -0
- package/src/ui/StatusLine.js +22 -1
- package/src/ui/StatusLine.tsx +145 -0
- package/src/ui/WorkspacePicker.tsx +84 -0
- package/src/ui/advisorCommand.js +54 -0
- package/src/ui/advisorCommand.ts +63 -0
- package/src/ui/computerCommand.js +19 -0
- package/src/ui/computerCommand.ts +35 -0
- package/src/ui/figures.ts +45 -0
- package/src/ui/hooks/askSlot.ts +112 -0
- package/src/ui/hooks/useChat.js +271 -7
- package/src/ui/hooks/useChat.ts +1578 -0
- package/src/ui/hooks/useInputHistory.ts +235 -0
- package/src/ui/hooks/usePasteHandler.ts +220 -0
- package/src/ui/hooks/useTerminalSize.ts +41 -0
- package/src/ui/hooks/useTextBuffer.ts +108 -0
- package/src/ui/hooks/useTokenUsage.ts +44 -0
- package/src/ui/liveViewport.ts +189 -0
- package/src/ui/markdown.ts +230 -0
- package/src/ui/textBuffer.ts +239 -0
- package/src/ui/theme.ts +35 -0
- package/src/utils/clipboardImage.js +358 -0
- package/src/utils/clipboardImage.ts +420 -0
- package/src/utils/debugLog.ts +50 -0
- package/src/utils/greenRoot.ts +35 -0
- package/src/utils/packageRoot.ts +40 -0
- package/src/utils/pathContains.js +28 -0
- package/src/utils/pathContains.ts +31 -0
- package/src/utils/resourcePaths.ts +71 -0
- package/src/utils/stripThink.ts +49 -0
- package/src/utils/zodToJson.ts +73 -0
- package/src/web/browserBridge.js +365 -66
- package/src/web/browserBridge.ts +672 -0
- package/src/web/protocol.ts +1804 -0
- package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
- package/workflows/ppt.yaml +13 -2
- package/workflows/wechat-reading-shots.yaml +315 -0
- package/workflows/word.yaml +11 -1
- package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
- /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
- /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* observationImages.ts —— 把原生观测变成模型看得懂的一组图
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 职责边界(架构总纲「Rust 出像素,TS 做判断」的落点):
|
|
6
|
+
* - 原生侧只出**裸像素**(低频彩色环里的 RGB 帧),不编码、不组装、不判断
|
|
7
|
+
* - 本模块负责差分、注意力遮罩、编码、落盘、配说明
|
|
8
|
+
*
|
|
9
|
+
* 为什么帧要落盘而不是直接塞 data URL:`ImageInputSpec.source` 走的是既有的
|
|
10
|
+
* 多模态管线(`multimodal.ts`),它接受路径、读盘、base64 内联进请求体。
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ **那条管线不做任何缩放。** `ImageInputSpec.maxLongSidePixel` 只是透传给
|
|
13
|
+
* provider 的提示字段(`image_url.max_long_side_pixel`),发出去的仍是原文件
|
|
14
|
+
* 字节。所以**尺寸必须在本模块内定死** —— 交给管线的每一张图都得是已经缩过的。
|
|
15
|
+
* 这一条曾被违反过一次,代价是一次 observe 打爆上下文(见 downscaleForModel)。
|
|
16
|
+
*
|
|
17
|
+
* 文件写在系统临时目录并带 TTL 清理 —— 屏幕帧是隐私敏感数据,不进工作目录、
|
|
18
|
+
* 不长留磁盘。
|
|
19
|
+
* ============================================================
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
mkdir,
|
|
24
|
+
readFile,
|
|
25
|
+
readdir,
|
|
26
|
+
rm,
|
|
27
|
+
stat,
|
|
28
|
+
writeFile,
|
|
29
|
+
} from 'node:fs/promises';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
import type { ImageInputSpec } from '../../types.js';
|
|
35
|
+
import {
|
|
36
|
+
REPLAY_LONG_SIDE,
|
|
37
|
+
buildFrameSequence,
|
|
38
|
+
estimateImageTokens,
|
|
39
|
+
} from './vision/imaging.js';
|
|
40
|
+
import { buildTimeline, pickEventAnchors } from './vision/frames.js';
|
|
41
|
+
import {
|
|
42
|
+
cloneImage,
|
|
43
|
+
crop,
|
|
44
|
+
drawActionMarker,
|
|
45
|
+
drawBadge,
|
|
46
|
+
drawRect,
|
|
47
|
+
fitLongSide,
|
|
48
|
+
resizeRgba,
|
|
49
|
+
} from './vision/raster.js';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 元素框与编号徽标的颜色,以及徽标字号。
|
|
53
|
+
*
|
|
54
|
+
* 刻意**不同于**动作十字的颜色:一个是"这些是可点的控件",一个是"我刚才点了这里",
|
|
55
|
+
* 混成一种颜色模型会把两件事读成一件。
|
|
56
|
+
*/
|
|
57
|
+
const ELEMENT_BOX_COLOR: [number, number, number] = [0, 132, 255];
|
|
58
|
+
const ELEMENT_BADGE_SCALE = 2;
|
|
59
|
+
/** `drawBadge` 的位图字形几何,用来算徽标占多少像素(与 raster.ts 保持一致)。 */
|
|
60
|
+
const BADGE_GLYPH_W = 5;
|
|
61
|
+
const BADGE_GLYPH_H = 7;
|
|
62
|
+
const BADGE_PADDING = 2;
|
|
63
|
+
/**
|
|
64
|
+
* 算标注是否放得下时假定的图片尺寸。
|
|
65
|
+
*
|
|
66
|
+
* 真实尺寸要解码完才知道,而筛选发生在解码之前。用预算上限对应的典型尺寸做保守
|
|
67
|
+
* 估计即可:估小了只会少画几个(安全),估大了才会画出读不清的(危险)。
|
|
68
|
+
*/
|
|
69
|
+
const DEFAULT_OVERLAY_IMAGE_SIZE = { width: 1371, height: 771 };
|
|
70
|
+
import { decodePng, encodePng, type RgbaImage } from './vision/png.js';
|
|
71
|
+
import {
|
|
72
|
+
generateAxElementSummary,
|
|
73
|
+
type AxInteractiveElement,
|
|
74
|
+
} from './axSnap.js';
|
|
75
|
+
import type {
|
|
76
|
+
ComputerUseActionMarker,
|
|
77
|
+
ComputerState,
|
|
78
|
+
ComputerUseImageReference,
|
|
79
|
+
ComputerUseRect,
|
|
80
|
+
ComputerUseSequenceFrame,
|
|
81
|
+
} from './protocol.js';
|
|
82
|
+
|
|
83
|
+
/** 帧文件目录。与原生侧的截图目录并列,同样只放临时数据。 */
|
|
84
|
+
export const FRAME_DIR = join(tmpdir(), 'kakadeai-computer-use-frames');
|
|
85
|
+
|
|
86
|
+
/** 帧文件保留时长。SPEC §4.3 的截图磁盘 TTL(10分钟)。 */
|
|
87
|
+
const FRAME_TTL_MS = 600_000;
|
|
88
|
+
|
|
89
|
+
/** 帧文件最大数量(额外保护,超过后删除最旧的)。 */
|
|
90
|
+
const FRAME_MAX_COUNT = 200;
|
|
91
|
+
|
|
92
|
+
/** Compatibility export; runtime defaults now come from ConfigService. */
|
|
93
|
+
export const DEFAULT_OBSERVE_FRAMES = 1;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Ordinary observations always expose one current frame. Replay remains the
|
|
97
|
+
* explicit API for inspecting a timeline without inflating every model turn.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveObservationFrames(
|
|
100
|
+
_requested: number | undefined,
|
|
101
|
+
_configured: number,
|
|
102
|
+
): number {
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ObservationImages {
|
|
107
|
+
images: ImageInputSpec[];
|
|
108
|
+
/** 随图给模型的说明。帧数是变量,文案按实际帧数生成。 */
|
|
109
|
+
note: string;
|
|
110
|
+
/** 文本摘要,进 tool result 的正文(比图便宜 20 倍)。 */
|
|
111
|
+
summary: string[];
|
|
112
|
+
estimatedTokens: number;
|
|
113
|
+
/** AX元素短id→真实id映射,用于处理LLM用@1/@2引用元素 */
|
|
114
|
+
axIdMap?: Map<string, string>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 组装一次观测要发给模型的图。
|
|
119
|
+
*
|
|
120
|
+
* ★ 对齐 UI-TARS:永远只给全屏截图,不给裁剪图。
|
|
121
|
+
* 裁剪图会造成"坐标错觉"——模型在裁剪图内定位,却被要求输出整屏坐标,容易算错偏移。
|
|
122
|
+
* 截图统一按 MAX_PIXELS (~100万像素) 收进预算(`downscaleForModel`),坐标直接对应。
|
|
123
|
+
*/
|
|
124
|
+
export async function buildObservationImages(
|
|
125
|
+
state: ComputerState,
|
|
126
|
+
actionMarker?: ComputerUseActionMarker,
|
|
127
|
+
magnify?: ComputerUseRect,
|
|
128
|
+
): Promise<ObservationImages | null> {
|
|
129
|
+
const sequence = state.sequence ?? [];
|
|
130
|
+
if (sequence.length >= 2) {
|
|
131
|
+
return assembleSequence(sequence);
|
|
132
|
+
}
|
|
133
|
+
if (state.screenshot) {
|
|
134
|
+
const hasMarker = actionMarker !== undefined;
|
|
135
|
+
// ★ 编号必须来自**同一个** summary:图上画的 ③ 和文字清单里的 [@3] 必须永远是
|
|
136
|
+
// 同一个元素。所以先算 summary,再把它选中的那批元素交给绘图,而不是让绘图
|
|
137
|
+
// 自己再筛一遍——两处各筛各的,迟早会错开,而错开的表现是模型点了别的控件。
|
|
138
|
+
const axSummary = generateAxElementSummary(state);
|
|
139
|
+
const { image, scaledSize } = await downscaleForModel(
|
|
140
|
+
state.screenshot.path,
|
|
141
|
+
state.screenshot.mime,
|
|
142
|
+
actionMarker,
|
|
143
|
+
buildElementOverlays(axSummary?.shown, state),
|
|
144
|
+
);
|
|
145
|
+
const origW = state.screenshot.width;
|
|
146
|
+
const origH = state.screenshot.height;
|
|
147
|
+
const imgW = scaledSize?.width ?? origW;
|
|
148
|
+
const imgH = scaledSize?.height ?? origH;
|
|
149
|
+
// ⚠️ **如实告诉模型这张图的精度极限。**
|
|
150
|
+
//
|
|
151
|
+
// 此前只说「图片尺寸 1371×771」,没说这意味着什么 —— 模型于是不知道自己看不清,
|
|
152
|
+
// 看到一团模糊就按**最像的那个**猜,而且猜得毫不犹豫。
|
|
153
|
+
//
|
|
154
|
+
// 实测事故(真机 trace + 截图为证):任务是「给王磊发消息」,微信当时停在**王鹏**
|
|
155
|
+
// 的会话上。5120×2880 缩到 1371×771 是 3.7 倍,聊天标题那 30 设备像素的字在图上
|
|
156
|
+
// 只剩约 8 像素 —— 把那块裁出来放大 6 倍看,第二个字完全糊成一团,「鹏」和「磊」
|
|
157
|
+
// 无法区分。模型据此认定「已经在王磊的会话上了」,跳过整个搜索步骤,直接打字发送,
|
|
158
|
+
// **消息发给了王鹏**,然后报告「已成功给王磊发送消息」。
|
|
159
|
+
//
|
|
160
|
+
// 这不是模型能力问题:那张图里**物理上就不含**区分这两个字所需的信息。
|
|
161
|
+
// 能修的是让它知道这一点,并知道 magnify 能拿到原生分辨率。
|
|
162
|
+
const shrink = origW > 0 && imgW > 0 ? origW / imgW : 1;
|
|
163
|
+
// 可用区(扣掉菜单栏与 Dock)换算成 0-1000 比例坐标。拿不到就整段不说 ——
|
|
164
|
+
// 说不出准确数字时保持沉默,好过给一个编的。
|
|
165
|
+
const display = state.displays?.find((d) => d.primary) ?? state.displays?.[0];
|
|
166
|
+
const visible = display?.visibleBounds;
|
|
167
|
+
const bounds = display?.logicalBounds;
|
|
168
|
+
const usable =
|
|
169
|
+
visible && bounds && bounds.height > 0
|
|
170
|
+
? {
|
|
171
|
+
top: Math.round(((visible.y - bounds.y) / bounds.height) * 1000),
|
|
172
|
+
bottom: Math.round(
|
|
173
|
+
((visible.y - bounds.y + visible.height) / bounds.height) * 1000,
|
|
174
|
+
),
|
|
175
|
+
centerY: Math.round(
|
|
176
|
+
((visible.y - bounds.y + visible.height / 2) / bounds.height) * 1000,
|
|
177
|
+
),
|
|
178
|
+
}
|
|
179
|
+
: undefined;
|
|
180
|
+
const noteLines: string[] = [
|
|
181
|
+
'【截图说明】',
|
|
182
|
+
shrink >= 1.05
|
|
183
|
+
? `图片尺寸:${imgW}×${imgH}(原生 ${origW}×${origH} 等比缩小到像素预算内)`
|
|
184
|
+
: `图片尺寸:${imgW}×${imgH}(**与屏幕 1:1,没有缩放**,界面上多大的字这里就是多大)`,
|
|
185
|
+
...(shrink >= 1.5
|
|
186
|
+
? [
|
|
187
|
+
`⚠️ 这张图比原生屏幕**缩小了 ${shrink.toFixed(1)} 倍**:界面上正常大小的文字,`
|
|
188
|
+
+ `在这张图上只有约 ${Math.max(1, Math.round(30 / shrink))} 像素高。`,
|
|
189
|
+
' **这个尺寸下汉字无法逐字分辨** —— 只差一笔的两个字看起来会完全一样。',
|
|
190
|
+
' 凡是需要**逐字确认**的内容 —— 人名、联系人、金额、验证码、文件名、账号 ——',
|
|
191
|
+
' 一律先用 `magnify`(返回该区域的原生分辨率裁剪)看清楚再下结论。',
|
|
192
|
+
' ⛔ 看着模糊的字「觉得像」就当成确认 —— 这是本工具造成过真实事故的错误。',
|
|
193
|
+
]
|
|
194
|
+
: []),
|
|
195
|
+
'',
|
|
196
|
+
// ⚠️ **可用区 ≠ 整屏。** 顶部菜单栏和底部 Dock 永远盖在应用上面,
|
|
197
|
+
// 可点区域比屏幕小一圈,而且**中心是偏上的**。
|
|
198
|
+
// 本机实测(1920×1080):菜单栏 30pt、Dock 94pt → 可用区 1920×956,
|
|
199
|
+
// 中心在 y=470‰ 而不是 500‰。按整屏中心摆窗口,底部 62pt 会塞进 Dock 底下——
|
|
200
|
+
// 被压住的往往正是输入框或提交按钮,坐标点得到、实际点不着。
|
|
201
|
+
...(usable
|
|
202
|
+
? [
|
|
203
|
+
`⚠️ 可用区(能放窗口、能点到的范围)= y 从 ${usable.top} 到 ${usable.bottom}(0-1000 比例):`,
|
|
204
|
+
` 顶部 0~${usable.top} 是菜单栏、底部 ${usable.bottom}~1000 是 Dock,都盖在应用上面。`,
|
|
205
|
+
` **可用区中心是 (500, ${usable.centerY}),不是 (500, 500)。**`,
|
|
206
|
+
' 窗口有一部分在这个范围外时,用 `window` 的 `center` 操作把它摆回来。',
|
|
207
|
+
'',
|
|
208
|
+
]
|
|
209
|
+
: []),
|
|
210
|
+
'⛔⛔⛔ 坐标系统(非常重要,不要搞错!):',
|
|
211
|
+
'- 坐标是 0-1000 的**全屏比例坐标**,不是像素坐标!',
|
|
212
|
+
'- x=0 是最左边,x=1000 是最右边;y=0 是最上边,y=1000 是最下边',
|
|
213
|
+
`- 图片宽度 ${imgW} 像素对应 x=1000,图片高度 ${imgH} 像素对应 y=1000`,
|
|
214
|
+
'',
|
|
215
|
+
'★ 强制要求:所有点击必须用 bbox 框选(x/y 左上角 + x2/y2 右下角)',
|
|
216
|
+
'- 不需要精确框!大致框住目标按钮/条目即可,系统自动点中心点',
|
|
217
|
+
'- 换算方法:目测目标左/右/上/下边缘分别在屏幕宽度/高度的百分之多少,×1000 就是坐标',
|
|
218
|
+
'',
|
|
219
|
+
// ⚠️ hover **不属于**这张清单。它是一次「验证」,不是一种「定位方式」。
|
|
220
|
+
// 早先它被排在这份「必须按顺序尝试」的第 2 位,模型于是对**每一次**
|
|
221
|
+
// click 都先 hover 一轮 —— 实测一次任务里第 4/5 步是同一个 bbox,
|
|
222
|
+
// 白烧 20.1s 的 LLM 往返(工具本身只用 1.4s)。
|
|
223
|
+
// 清单只列定位方式;hover 单独说,并且写清「什么时候才值得多花一轮」。
|
|
224
|
+
'定位方式优先级(按可用性从高到低选一种):',
|
|
225
|
+
' 1. textTarget 文字定位(最准,完全绕过坐标误差)',
|
|
226
|
+
' 2. elementId 精准点击(从下方AX元素列表用 @id 引用)',
|
|
227
|
+
' 3. bbox 坐标点击(自绘 / Electron 类应用无障碍树为空,只能用这个;坐标越靠近0/500/1000越稳)',
|
|
228
|
+
'',
|
|
229
|
+
'★ 什么时候先 hover 再 click:',
|
|
230
|
+
' hover 会多花一整轮往返(通常 10~20 秒),**只在下面两种情况下才值得**:',
|
|
231
|
+
' · 这一步不可逆或代价高(发送、提交、删除、支付、关窗);',
|
|
232
|
+
' · 上一次点击的红色十字明显偏了,需要先确认位置再点。',
|
|
233
|
+
' 其余情况(点列表条目、点输入框、点标签页…)**直接 click**,',
|
|
234
|
+
' 点错了从截图看得出来,重点一次比每次都 hover 便宜得多。',
|
|
235
|
+
];
|
|
236
|
+
if (hasMarker) {
|
|
237
|
+
noteLines.push('');
|
|
238
|
+
noteLines.push('🎯 红色十字标记你上次点击的位置。如果偏了,请根据偏移调整本次坐标。');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ★ AX元素摘要:原生应用可交互元素列表,给LLM用elementId精准点击
|
|
242
|
+
if (axSummary) {
|
|
243
|
+
noteLines.push(axSummary.noteText);
|
|
244
|
+
if (axSummary.shown.length > 0) {
|
|
245
|
+
noteLines.push(
|
|
246
|
+
`🔢 上面这些元素已经**在截图上画了同号的方框和编号徽标**(图上的 N = 清单里的 @N)。`
|
|
247
|
+
+ `看图找到目标的编号,直接 elementId:"@N" 点击,不用估坐标。`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── 放大镜:从**原始分辨率**帧上裁一块放大,专供读小字 ──────────────
|
|
253
|
+
const glass = magnify
|
|
254
|
+
? await buildMagnifier(state.screenshot, magnify)
|
|
255
|
+
: null;
|
|
256
|
+
if (glass) {
|
|
257
|
+
noteLines.push('');
|
|
258
|
+
noteLines.push(
|
|
259
|
+
`🔍 第 2 张是**放大镜**:全屏比例区域 (${Math.round(magnify!.x)},${Math.round(magnify!.y)})`
|
|
260
|
+
+ `-(${Math.round(magnify!.x + magnify!.width)},${Math.round(magnify!.y + magnify!.height)})`
|
|
261
|
+
+ ` 从原始 ${origW}×${origH} 帧上裁出后放大到 ${glass.width}×${glass.height}。`,
|
|
262
|
+
);
|
|
263
|
+
noteLines.push(
|
|
264
|
+
'⛔ 放大镜**只能用来看清文字/数字**。所有坐标一律以第 1 张全屏图为准 ——'
|
|
265
|
+
+ '放大镜里的位置换算回全屏是错的,不要用它定位点击目标。',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
images: glass ? [image, glass.image] : [image],
|
|
271
|
+
note: noteLines.join('\n'),
|
|
272
|
+
summary: [
|
|
273
|
+
`屏幕 ${origW}×${origH} → 模型输入 ${imgW}×${imgH},坐标 0-1000 直接对应`,
|
|
274
|
+
...(glass ? [`🔍 放大镜已附(${glass.width}×${glass.height}),仅供读字不供定位`] : []),
|
|
275
|
+
'优先 activate_app / textTarget / elementId,坐标最后考虑',
|
|
276
|
+
],
|
|
277
|
+
estimatedTokens:
|
|
278
|
+
estimateImageTokens(state.screenshot) + (glass?.estimatedTokens ?? 0),
|
|
279
|
+
axIdMap: axSummary?.idMap,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 把原生截图**在本地缩放**后再交给模型。
|
|
287
|
+
*
|
|
288
|
+
* ★ 这里曾经直接把 `screencapture` 的原始文件路径交出去,只在
|
|
289
|
+
* `ImageInputSpec` 上挂一个 `maxLongSidePixel: 1400`。那个字段**不做任何本地
|
|
290
|
+
* 缩放** —— 它只是透传给 provider 的一个提示(`image_url.max_long_side_pixel`),
|
|
291
|
+
* 而我们仍然把原文件整个 base64 塞进请求体。
|
|
292
|
+
*
|
|
293
|
+
* 后果实测:这台机器的原生截图是 3840×2160 / 3.25MB,base64 后 **4.33MB 字符**,
|
|
294
|
+
* 一次 observe 就把请求打成 `context window exceeds limit`。而本地缩到 720p 之后
|
|
295
|
+
* 是 525KB(**6 倍差距**)—— 帧序列那条路一直是这么做的,只有这条单张兜底路
|
|
296
|
+
* 是裸的。又一次「一条路封装完整、另一条路是空的」。
|
|
297
|
+
*
|
|
298
|
+
* `maxLongSidePixel` 因此不再用于这里:一个看起来像在缩放、实际不缩放的参数,
|
|
299
|
+
* 比没有更危险。
|
|
300
|
+
*/
|
|
301
|
+
function delay(ms: number): Promise<void> {
|
|
302
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function readFileWithRetry(
|
|
306
|
+
sourcePath: string,
|
|
307
|
+
maxRetries: number = 2,
|
|
308
|
+
retryDelayMs: number = 100,
|
|
309
|
+
): Promise<Buffer | null> {
|
|
310
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
311
|
+
const raw = await readFile(sourcePath).catch(() => null);
|
|
312
|
+
if (raw && raw.length > 0) return raw;
|
|
313
|
+
if (attempt < maxRetries) {
|
|
314
|
+
await delay(retryDelayMs);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function decodePngWithRetry(
|
|
321
|
+
raw: Buffer,
|
|
322
|
+
maxRetries: number = 2,
|
|
323
|
+
): Promise<ReturnType<typeof decodePng> | null> {
|
|
324
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
325
|
+
try {
|
|
326
|
+
return decodePng(raw);
|
|
327
|
+
} catch {
|
|
328
|
+
if (attempt < maxRetries) {
|
|
329
|
+
await delay(50);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 解码截图为 RgbaImage。
|
|
338
|
+
*
|
|
339
|
+
* **只认 PNG,且这是有意的**:唯一的像素来源是 Rust 的 cu-helperd,它的
|
|
340
|
+
* `capture-full` 出 PNG、快循环出裸灰度帧,不产 JPEG。此处曾经有一条 Jimp 的
|
|
341
|
+
* JPEG/WebP 分支,那是为已移除的 nut-js transport(输出 q78 JPEG)配的;
|
|
342
|
+
* 随它一起删掉,`vision/png.ts` 的零依赖编解码重新自足——这也是绿色版
|
|
343
|
+
* 「零原生 node 模块」得以成立的前提之一。
|
|
344
|
+
*
|
|
345
|
+
* 若将来真需要 JPEG(省磁盘/IO;注意模型侧按**像素**计费,token 成本不变),
|
|
346
|
+
* 应由 Rust 侧编码后连同 mime 一起送来,**不要**再引入 JS 图像库。
|
|
347
|
+
*/
|
|
348
|
+
async function decodeImageWithRetry(
|
|
349
|
+
raw: Buffer,
|
|
350
|
+
mime: ComputerUseImageReference['mime'] | undefined,
|
|
351
|
+
maxRetries: number = 2,
|
|
352
|
+
): Promise<RgbaImage | null> {
|
|
353
|
+
if (mime !== undefined && mime !== 'image/png') return null;
|
|
354
|
+
if (mime === undefined && !isPngBuffer(raw)) return null;
|
|
355
|
+
return decodePngWithRetry(raw, maxRetries);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function isPngBuffer(buf: Buffer): boolean {
|
|
359
|
+
return (
|
|
360
|
+
buf.length >= 8 &&
|
|
361
|
+
buf[0] === 0x89 &&
|
|
362
|
+
buf[1] === 0x50 &&
|
|
363
|
+
buf[2] === 0x4e &&
|
|
364
|
+
buf[3] === 0x47
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** 只读 IHDR 拿尺寸,不解码像素(PNG 的宽高就在固定偏移 16/20)。 */
|
|
369
|
+
function readPngDimensionsQuick(buf: Buffer): { width: number; height: number } | null {
|
|
370
|
+
if (!isPngBuffer(buf) || buf.length < 24) return null;
|
|
371
|
+
const width = buf.readUInt32BE(16);
|
|
372
|
+
const height = buf.readUInt32BE(20);
|
|
373
|
+
return width > 0 && height > 0 ? { width, height } : null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 送给模型的截图像素预算。
|
|
378
|
+
*
|
|
379
|
+
* 值取 1920×1080 —— 这**不是**随手放大,是与
|
|
380
|
+
* `macos.rs::ensure_model_friendly_resolution` 配套的一半:
|
|
381
|
+
* 那边把桌面切成 1920×1080 的 1:1 模式,这边的预算正好装得下,于是
|
|
382
|
+
* **整条链上零缩放**,界面上多大的字送到模型眼里就是多大。
|
|
383
|
+
*
|
|
384
|
+
* 旧值是 1350*28*28 ≈ 106 万(对齐 UI-TARS)。在 5120×2880 的 Retina 桌面上
|
|
385
|
+
* 它意味着缩 3.7 倍:24 像素的字只剩 6~8 像素,**汉字无法逐字分辨**。
|
|
386
|
+
* 实测因此发生过发错人事故(把「王鹏」的会话当成「王磊」的,见那个函数的注释)。
|
|
387
|
+
*
|
|
388
|
+
* 代价诚实说:单张图约 2.8k token,比旧值的 1.4k 翻倍。但它换掉的是
|
|
389
|
+
* 「看不清 → 放大镜再来一轮」这种整轮 LLM 往返(实测 10~20 秒、外加一张图),
|
|
390
|
+
* 以及看不清时直接猜错的风险。净赚。
|
|
391
|
+
*
|
|
392
|
+
* ⚠️ 仍然是**防线**而不是目标尺寸:桌面若因为任何原因不是 1080p(外接屏、
|
|
393
|
+
* 用户手动改回去、切换失败),这里照旧把图压进预算,不让大图打爆上下文。
|
|
394
|
+
*/
|
|
395
|
+
const MAX_PIXELS = 1920 * 1080;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 模型**实际看到**的那张图有多大。
|
|
399
|
+
*
|
|
400
|
+
* ⚠️ 这是一条必须单一出处的规则。任何「把模型给的像素坐标换算回屏幕」的地方,分母都
|
|
401
|
+
* 只能是这个值 —— 不能是 `state.screenshot.width`(那是 daemon 的原始截图尺寸)。
|
|
402
|
+
*
|
|
403
|
+
* 两者在 Retina 上差得很远:5K 屏原始截图 5120×2880,压进预算后模型看到的是 1371×771,
|
|
404
|
+
* **相差 3.73 倍**。用错分母的后果不是「稍微偏一点」,是每一次点击都朝左上角偏出去
|
|
405
|
+
* 近四倍距离 —— 在多窗口叠放的桌面上,这通常意味着点到了目标窗口后面的另一个应用。
|
|
406
|
+
*
|
|
407
|
+
* 缩放规则与 `downscaleForModel` 里的实际缩放**必须逐字一致**,所以那里也改成调它。
|
|
408
|
+
*/
|
|
409
|
+
export function modelFacingImageSize(
|
|
410
|
+
sourceWidth: number,
|
|
411
|
+
sourceHeight: number,
|
|
412
|
+
): { width: number; height: number } {
|
|
413
|
+
if (sourceWidth <= 0 || sourceHeight <= 0) {
|
|
414
|
+
return { width: sourceWidth, height: sourceHeight };
|
|
415
|
+
}
|
|
416
|
+
if (sourceWidth * sourceHeight <= MAX_PIXELS) {
|
|
417
|
+
return { width: sourceWidth, height: sourceHeight };
|
|
418
|
+
}
|
|
419
|
+
const factor = Math.sqrt(MAX_PIXELS / (sourceWidth * sourceHeight));
|
|
420
|
+
return {
|
|
421
|
+
width: Math.floor(sourceWidth * factor),
|
|
422
|
+
height: Math.floor(sourceHeight * factor),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** 一个要画到图上的元素标注:矩形按屏幕比例 0–1 表示,与图片实际像素解耦。 */
|
|
427
|
+
interface ElementOverlay {
|
|
428
|
+
/** 徽标数字,等于文字清单里的 @N。 */
|
|
429
|
+
index: number;
|
|
430
|
+
/** 0–1 的屏幕比例矩形。 */
|
|
431
|
+
left: number;
|
|
432
|
+
top: number;
|
|
433
|
+
right: number;
|
|
434
|
+
bottom: number;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 面积超过整屏这个比例的元素不画。
|
|
439
|
+
*
|
|
440
|
+
* AX 树里 `AXGroup` / `AXScrollArea` 这类容器也满足"可交互"的判据,但给整个窗口
|
|
441
|
+
* 画一个框加一个编号没有任何定位价值,只会盖住真正的控件。
|
|
442
|
+
*/
|
|
443
|
+
const MAX_OVERLAY_AREA_RATIO = 0.25;
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 把 AX 元素(逻辑点)换算成屏幕比例矩形。
|
|
447
|
+
*
|
|
448
|
+
* `frame` 与 `display.logicalBounds` 是同一坐标空间(都是逻辑点),所以先减去
|
|
449
|
+
* 显示器原点再除以显示器尺寸就得到比例;比例再乘图片像素尺寸即可落到图上——
|
|
450
|
+
* 这样不管图被缩放到多大都不用改。
|
|
451
|
+
*/
|
|
452
|
+
function buildElementOverlays(
|
|
453
|
+
shown: AxInteractiveElement[] | undefined,
|
|
454
|
+
state: ComputerState,
|
|
455
|
+
imageSize: { width: number; height: number } = DEFAULT_OVERLAY_IMAGE_SIZE,
|
|
456
|
+
): ElementOverlay[] {
|
|
457
|
+
if (!shown?.length) return [];
|
|
458
|
+
const display = state.displays.find((d) => d.primary) ?? state.displays[0];
|
|
459
|
+
if (!display || display.logicalBounds.width <= 0 || display.logicalBounds.height <= 0) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
const { x: ox, y: oy, width: sw, height: sh } = display.logicalBounds;
|
|
463
|
+
// 徽标在图上占多大(比例)。约束是**徽标之间不许叠**,不是「元素必须装得下徽标」——
|
|
464
|
+
// 后者试过,是错的:元素排序是面积从小到大(小按钮更可能是目标),而"装得下"恰好
|
|
465
|
+
// 把小的全筛掉,18 个里只剩 1 个能画。两条规则互相打架。
|
|
466
|
+
//
|
|
467
|
+
// 真正要防的是**读不出来**:实拍时 Finder 文件列表里十几个单元格上下只差几像素,
|
|
468
|
+
// 徽标层层叠住,数字互相盖住谁也认不出,还糊住了内容。所以徽标允许溢出元素框
|
|
469
|
+
// (SoM 的常规做法),但一旦会和已放置的徽标重叠就跳过——密集列表里只标第一个,
|
|
470
|
+
// 稀疏界面上全都能标。
|
|
471
|
+
const badgeW = BADGE_GLYPH_W * ELEMENT_BADGE_SCALE + BADGE_PADDING * 2 * ELEMENT_BADGE_SCALE;
|
|
472
|
+
const badgeH = BADGE_GLYPH_H * ELEMENT_BADGE_SCALE + BADGE_PADDING * 2 * ELEMENT_BADGE_SCALE;
|
|
473
|
+
const minW = badgeW / Math.max(1, imageSize.width);
|
|
474
|
+
const minH = badgeH / Math.max(1, imageSize.height);
|
|
475
|
+
|
|
476
|
+
const overlays: ElementOverlay[] = [];
|
|
477
|
+
for (let i = 0; i < shown.length; i += 1) {
|
|
478
|
+
const frame = shown[i].frame;
|
|
479
|
+
const left = (frame.x - ox) / sw;
|
|
480
|
+
const top = (frame.y - oy) / sh;
|
|
481
|
+
const right = (frame.x + frame.width - ox) / sw;
|
|
482
|
+
const bottom = (frame.y + frame.height - oy) / sh;
|
|
483
|
+
// 完全在屏幕外的不画;容器类的不画;装不下徽标的不画。
|
|
484
|
+
// 编号仍然按原顺序,**不重排** —— 编号必须和文字清单一一对应,跳过一个就让
|
|
485
|
+
// 后面全体错位。图上缺号是安全的(模型仍可用清单里的 @N),编号错位不是。
|
|
486
|
+
if (right <= 0 || bottom <= 0 || left >= 1 || top >= 1) continue;
|
|
487
|
+
if ((right - left) * (bottom - top) > MAX_OVERLAY_AREA_RATIO) continue;
|
|
488
|
+
// 徽标之间不许重叠:两个数字叠在一起等于两个都读不出来。先来先得,
|
|
489
|
+
// 顺序即优先级(extractInteractiveElements 已按「聚焦 > 启用 > 面积小」排过)。
|
|
490
|
+
const collides = overlays.some(
|
|
491
|
+
(placed) =>
|
|
492
|
+
left < placed.left + minW
|
|
493
|
+
&& left + minW > placed.left
|
|
494
|
+
&& top < placed.top + minH
|
|
495
|
+
&& top + minH > placed.top,
|
|
496
|
+
);
|
|
497
|
+
if (collides) continue;
|
|
498
|
+
overlays.push({ index: i + 1, left, top, right, bottom });
|
|
499
|
+
}
|
|
500
|
+
return overlays;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* 只给测试用的内部导出。
|
|
505
|
+
*
|
|
506
|
+
* `buildElementOverlays` 是纯函数(输入 AX 元素 + 显示器几何,输出比例矩形),
|
|
507
|
+
* 编号一致性这条不变量只能在这一层断言 —— 画到图上之后就只剩像素了。
|
|
508
|
+
*/
|
|
509
|
+
export const __testing = {
|
|
510
|
+
buildElementOverlays,
|
|
511
|
+
MAX_OVERLAY_AREA_RATIO,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 放大镜:从**原始分辨率**截图上裁一块,放大后单独作为一张图给模型。
|
|
516
|
+
*
|
|
517
|
+
* ⚠️ 为什么必须从原始帧裁,而不是从模型那张图上裁 —— 这是实测逼出来的:
|
|
518
|
+
* 微信读书的页码("35 | 116")在 5120×2880 原图上约 20px 高,压进模型预算
|
|
519
|
+
* (1371×771)后只剩约 5px。把那张图放大 8 倍看到的是一团马赛克 ——
|
|
520
|
+
* **信息在降采样那一步就被物理销毁了,任何提示词都救不回来。**
|
|
521
|
+
* 此前模型报的页码全是猜的(报「85/116」时恰好蒙对,也报错过)。
|
|
522
|
+
*
|
|
523
|
+
* ⚠️ 为什么不直接把全屏图给得更大:模型侧按像素计费,全屏提分辨率是每次观测都付
|
|
524
|
+
* 的固定成本;而小字只在少数步骤需要读。放大镜是按需付费的那一档。
|
|
525
|
+
*
|
|
526
|
+
* ⚠️ 坐标错觉的防线:放大镜**始终是第 2 张**,全屏图永远是第 1 张且不受影响,
|
|
527
|
+
* note 里明写「放大镜只许读字、不许定位」。这与「永不返回裁剪图」那条铁律
|
|
528
|
+
* 不冲突 —— 那条禁的是**用裁剪图替代全屏图**,这里是叠加,不是替代。
|
|
529
|
+
*/
|
|
530
|
+
async function buildMagnifier(
|
|
531
|
+
screenshot: ComputerUseImageReference,
|
|
532
|
+
rect: ComputerUseRect,
|
|
533
|
+
): Promise<
|
|
534
|
+
{ image: ImageInputSpec; width: number; height: number; estimatedTokens: number } | null
|
|
535
|
+
> {
|
|
536
|
+
const raw = await readFileWithRetry(screenshot.path, 2, 100);
|
|
537
|
+
if (!raw) return null;
|
|
538
|
+
const decoded = await decodeImageWithRetry(raw, screenshot.mime, 2);
|
|
539
|
+
if (!decoded) return null;
|
|
540
|
+
|
|
541
|
+
// 0–1000 相对坐标 → 原始帧像素。分母是**原始尺寸**,不是模型输入尺寸:
|
|
542
|
+
// 这里裁的就是原始帧,用错分母会裁到别处(且不会报错,只会安静地裁错)。
|
|
543
|
+
const px = {
|
|
544
|
+
x: (rect.x / 1000) * decoded.width,
|
|
545
|
+
y: (rect.y / 1000) * decoded.height,
|
|
546
|
+
width: Math.max(1, (rect.width / 1000) * decoded.width),
|
|
547
|
+
height: Math.max(1, (rect.height / 1000) * decoded.height),
|
|
548
|
+
};
|
|
549
|
+
const patch = crop(decoded, px);
|
|
550
|
+
if (patch.width < 2 || patch.height < 2) return null;
|
|
551
|
+
|
|
552
|
+
// 放大到长边 ~900px(够读清 5px 的字),同时受 MAX_PIXELS 兜底。
|
|
553
|
+
const factor = Math.min(
|
|
554
|
+
MAGNIFIER_LONG_SIDE / Math.max(patch.width, patch.height),
|
|
555
|
+
Math.sqrt(MAX_PIXELS / (patch.width * patch.height)),
|
|
556
|
+
);
|
|
557
|
+
const scaled =
|
|
558
|
+
factor > 1.01
|
|
559
|
+
? resizeRgba(
|
|
560
|
+
patch.data,
|
|
561
|
+
patch.width,
|
|
562
|
+
patch.height,
|
|
563
|
+
Math.round(patch.width * factor),
|
|
564
|
+
Math.round(patch.height * factor),
|
|
565
|
+
)
|
|
566
|
+
: patch;
|
|
567
|
+
|
|
568
|
+
await mkdir(FRAME_DIR, { recursive: true, mode: 0o700 }).catch(() => {});
|
|
569
|
+
const encoded = encodePng(scaled, { channels: 3, level: 6 });
|
|
570
|
+
const path = join(FRAME_DIR, `glass-${Date.now()}-${process.pid}.png`);
|
|
571
|
+
await writeFile(path, encoded, { mode: 0o600 });
|
|
572
|
+
return {
|
|
573
|
+
image: { source: path, detail: 'high' },
|
|
574
|
+
width: scaled.width,
|
|
575
|
+
height: scaled.height,
|
|
576
|
+
// 与全屏图同一套估法:按像素数折算(~28×28 一个 token)。
|
|
577
|
+
estimatedTokens: Math.ceil((scaled.width * scaled.height) / (28 * 28)),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** 放大镜输出的长边目标像素。够把 5px 的字撑到 ~40px(人眼与模型都能读)。 */
|
|
582
|
+
const MAGNIFIER_LONG_SIDE = 900;
|
|
583
|
+
|
|
584
|
+
async function downscaleForModel(
|
|
585
|
+
sourcePath: string,
|
|
586
|
+
mime: ComputerUseImageReference['mime'] | undefined,
|
|
587
|
+
actionMarker?: ComputerUseActionMarker,
|
|
588
|
+
overlays: ElementOverlay[] = [],
|
|
589
|
+
): Promise<{
|
|
590
|
+
image: ImageInputSpec;
|
|
591
|
+
scaledSize: { width: number; height: number } | null;
|
|
592
|
+
sourceSize: { width: number; height: number };
|
|
593
|
+
}> {
|
|
594
|
+
// 快速路径:不需要画十字标记、且原图尺寸已在预算内时,直接返回原文件,
|
|
595
|
+
// 零拷贝零编码。判据从「是 JPEG」改成「尺寸读得出且在预算内」——
|
|
596
|
+
// 前者绑的是已移除的 nut-js(它输出预缩放好的 JPEG),后者才是真正的条件。
|
|
597
|
+
const raw = await readFileWithRetry(sourcePath, 2, 100);
|
|
598
|
+
if (!raw)
|
|
599
|
+
return {
|
|
600
|
+
image: { source: sourcePath, detail: 'default' },
|
|
601
|
+
scaledSize: null,
|
|
602
|
+
sourceSize: { width: 0, height: 0 },
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// 只读文件头拿尺寸,不解码全图
|
|
606
|
+
let sourceWidth = 0, sourceHeight = 0;
|
|
607
|
+
const dims = readPngDimensionsQuick(raw);
|
|
608
|
+
if (dims) {
|
|
609
|
+
sourceWidth = dims.width;
|
|
610
|
+
sourceHeight = dims.height;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 有标注要画就不能走零拷贝快路径 —— 快路径直接把原文件交出去,画不上任何东西。
|
|
614
|
+
const needsDrawing = actionMarker !== undefined || overlays.length > 0;
|
|
615
|
+
if (!needsDrawing && sourceWidth > 0 && sourceWidth * sourceHeight <= MAX_PIXELS) {
|
|
616
|
+
return {
|
|
617
|
+
image: { source: sourcePath, detail: 'high' },
|
|
618
|
+
scaledSize: { width: sourceWidth, height: sourceHeight },
|
|
619
|
+
sourceSize: { width: sourceWidth, height: sourceHeight },
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 需要处理:解码 → [安全缩放] → 画十字 → 编码 PNG
|
|
624
|
+
const decoded = await decodeImageWithRetry(raw, mime, 2);
|
|
625
|
+
if (!decoded) {
|
|
626
|
+
return {
|
|
627
|
+
image: { source: sourcePath, detail: 'default' },
|
|
628
|
+
scaledSize: sourceWidth > 0 ? { width: sourceWidth, height: sourceHeight } : null,
|
|
629
|
+
sourceSize: { width: sourceWidth, height: sourceHeight },
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
sourceWidth = decoded.width;
|
|
633
|
+
sourceHeight = decoded.height;
|
|
634
|
+
|
|
635
|
+
await mkdir(FRAME_DIR, { recursive: true, mode: 0o700 }).catch(() => {});
|
|
636
|
+
await pruneStaleFrames();
|
|
637
|
+
|
|
638
|
+
// ★ 安全防线:如果图片超过 MAX_PIXELS,先缩放到预算内(防御性编程,防止其他transport输出大图)
|
|
639
|
+
let processed: RgbaImage = decoded;
|
|
640
|
+
// 缩放规则走 modelFacingImageSize(单一出处):像素坐标换算的分母必须与这里
|
|
641
|
+
// 算出来的尺寸逐字一致,两处各写一遍迟早会错开,而错开的表现是每次点击都偏。
|
|
642
|
+
const target = modelFacingImageSize(sourceWidth, sourceHeight);
|
|
643
|
+
if (target.width !== sourceWidth || target.height !== sourceHeight) {
|
|
644
|
+
processed = resizeRgba(decoded.data, decoded.width, decoded.height, target.width, target.height);
|
|
645
|
+
sourceWidth = target.width;
|
|
646
|
+
sourceHeight = target.height;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ★ 元素编号标注(Set-of-Marks)先画,动作十字后画 —— 十字标的是"我刚点了哪",
|
|
650
|
+
// 必须压在最上层,不能被某个方框盖住。
|
|
651
|
+
if (overlays.length > 0) {
|
|
652
|
+
if (processed === decoded) {
|
|
653
|
+
processed = cloneImage(decoded);
|
|
654
|
+
}
|
|
655
|
+
for (const overlay of overlays) {
|
|
656
|
+
const x = Math.round(overlay.left * processed.width);
|
|
657
|
+
const y = Math.round(overlay.top * processed.height);
|
|
658
|
+
const width = Math.max(2, Math.round((overlay.right - overlay.left) * processed.width));
|
|
659
|
+
const height = Math.max(2, Math.round((overlay.bottom - overlay.top) * processed.height));
|
|
660
|
+
drawRect(processed, { x, y, width, height }, ELEMENT_BOX_COLOR, 1);
|
|
661
|
+
// 徽标画在框内左上角:画在框外会跑到相邻控件身上,模型就分不清编号属于谁。
|
|
662
|
+
drawBadge(processed, x + 1, y + 1, overlay.index, ELEMENT_BOX_COLOR, ELEMENT_BADGE_SCALE);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 画标记
|
|
667
|
+
if (actionMarker?.x !== undefined && actionMarker?.y !== undefined) {
|
|
668
|
+
if (processed === decoded) {
|
|
669
|
+
processed = cloneImage(decoded);
|
|
670
|
+
}
|
|
671
|
+
const mx = (actionMarker.x / 1000) * processed.width;
|
|
672
|
+
const my = (actionMarker.y / 1000) * processed.height;
|
|
673
|
+
drawActionMarker(processed, mx, my, 0, 14, 2);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const encoded = encodePng(processed, { channels: 3, level: 6 });
|
|
677
|
+
const path = join(FRAME_DIR, `shot-${Date.now()}-${process.pid}.png`);
|
|
678
|
+
await writeFile(path, encoded, { mode: 0o600 });
|
|
679
|
+
return {
|
|
680
|
+
image: { source: path, detail: 'high' },
|
|
681
|
+
scaledSize: { width: sourceWidth, height: sourceHeight },
|
|
682
|
+
sourceSize: { width: sourceWidth, height: sourceHeight },
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* 把 `replay` 返回的裸灰度帧变成模型能看的少量图。
|
|
688
|
+
*
|
|
689
|
+
* ★ 不接这条路的后果是灾难性的,而且它曾经真的没接:`replay` 已经暴露给模型,
|
|
690
|
+
* 守护进程返回 30 帧 480×270 灰度的 base64,而 TS 侧不解码 —— 于是
|
|
691
|
+
* **约 520 万字符(≈130 万 token)的 base64 直接进上下文**。不是不好用,
|
|
692
|
+
* 是一次调用就把上下文炸穿。
|
|
693
|
+
*
|
|
694
|
+
* 所以这里做三件事:
|
|
695
|
+
* ① 用**事件锚定**从帧序列里挑最有信息量的 4 帧 —— 界面变化是突发的,
|
|
696
|
+
* 等间隔采样会花 30 倍的钱买 4 帧的信息量
|
|
697
|
+
* ② 缩到 `REPLAY_LONG_SIDE`(683)—— 回放回答「发生了什么」而非「写的什么」
|
|
698
|
+
* ③ 编码成灰度 PNG 落盘,走既有的多模态管线
|
|
699
|
+
*/
|
|
700
|
+
export async function buildReplayImages(
|
|
701
|
+
frames: ReplayFrame[],
|
|
702
|
+
): Promise<ObservationImages | null> {
|
|
703
|
+
const decoded = frames.flatMap((frame) => {
|
|
704
|
+
if (!frame.grayBase64 || frame.width <= 0 || frame.height <= 0) return [];
|
|
705
|
+
const gray = Buffer.from(frame.grayBase64, 'base64');
|
|
706
|
+
if (gray.length !== frame.width * frame.height) return [];
|
|
707
|
+
return [
|
|
708
|
+
{
|
|
709
|
+
t: frame.capturedAtMs,
|
|
710
|
+
gray: new Uint8Array(gray),
|
|
711
|
+
width: frame.width,
|
|
712
|
+
height: frame.height,
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
});
|
|
716
|
+
if (decoded.length === 0) return null;
|
|
717
|
+
|
|
718
|
+
const { width, height } = decoded[0]!;
|
|
719
|
+
// 尺寸不一致的帧无法两两差分,直接排除,不拿坏数据算时间线。
|
|
720
|
+
const usable = decoded.filter(
|
|
721
|
+
(f) => f.width === width && f.height === height,
|
|
722
|
+
);
|
|
723
|
+
const timeline = buildTimeline(usable, width, height);
|
|
724
|
+
const anchors = pickEventAnchors(usable, timeline, { max: REPLAY_FRAMES });
|
|
725
|
+
|
|
726
|
+
await mkdir(FRAME_DIR, { recursive: true, mode: 0o700 }).catch(() => {});
|
|
727
|
+
await pruneStaleFrames();
|
|
728
|
+
|
|
729
|
+
const stamp = `${Date.now()}-${process.pid}`;
|
|
730
|
+
const images: ImageInputSpec[] = [];
|
|
731
|
+
const summary: string[] = [
|
|
732
|
+
`回放 ${anchors.length} 帧(从 ${usable.length} 帧里事件锚定挑出)`,
|
|
733
|
+
];
|
|
734
|
+
let estimatedTokens = 0;
|
|
735
|
+
|
|
736
|
+
for (const [index, anchor] of anchors.entries()) {
|
|
737
|
+
const frame = usable[anchor.index]!;
|
|
738
|
+
const scaled = fitLongSide(
|
|
739
|
+
grayToRgba(frame.gray, width, height),
|
|
740
|
+
REPLAY_LONG_SIDE,
|
|
741
|
+
);
|
|
742
|
+
// 回放帧用低压缩级别的 PNG:回放回答「发生了什么」不是「写的什么」,
|
|
743
|
+
// level 3 换编码速度(注释原本写「质量 75 的 JPEG」,与代码不符,已订正)
|
|
744
|
+
const png = encodePng(scaled, { channels: 3, level: 3 });
|
|
745
|
+
const path = join(FRAME_DIR, `replay-${stamp}-${index + 1}.png`);
|
|
746
|
+
await writeFile(path, png, { mode: 0o600 });
|
|
747
|
+
images.push({ source: path, detail: 'low' });
|
|
748
|
+
estimatedTokens += estimateImageTokens(scaled);
|
|
749
|
+
const offset = Math.round((frame.t - usable[0]!.t) / 100) / 10;
|
|
750
|
+
summary.push(`第 ${index + 1} 帧:${anchor.label}(+${offset}s)`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
images,
|
|
755
|
+
note: [
|
|
756
|
+
`以上 ${anchors.length} 张是**灰度回放帧**,按时间正序,从刚才那几秒里挑出信息量最大的时刻`,
|
|
757
|
+
'(动作前 / 首次变化 / 变化峰值 / 稳定后)。它回答「刚才发生了什么」,',
|
|
758
|
+
'**不是**用来读文字的 —— 分辨率和颜色都已为此牺牲。要看清内容请用 observe。',
|
|
759
|
+
].join('\n'),
|
|
760
|
+
summary,
|
|
761
|
+
estimatedTokens,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export interface ReplayFrame {
|
|
766
|
+
capturedAtMs: number;
|
|
767
|
+
width: number;
|
|
768
|
+
height: number;
|
|
769
|
+
grayBase64?: string;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** 回放最多给几帧。事件锚定的四个语义位各一帧。 */
|
|
773
|
+
const REPLAY_FRAMES = 4;
|
|
774
|
+
|
|
775
|
+
function grayToRgba(
|
|
776
|
+
gray: Uint8Array,
|
|
777
|
+
width: number,
|
|
778
|
+
height: number,
|
|
779
|
+
): RgbaImage {
|
|
780
|
+
const data = new Uint8Array(width * height * 4);
|
|
781
|
+
for (let i = 0, p = 0; p < gray.length; p += 1, i += 4) {
|
|
782
|
+
const v = gray[p]!;
|
|
783
|
+
data[i] = v;
|
|
784
|
+
data[i + 1] = v;
|
|
785
|
+
data[i + 2] = v;
|
|
786
|
+
data[i + 3] = 255;
|
|
787
|
+
}
|
|
788
|
+
return { width, height, data };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function assembleSequence(
|
|
792
|
+
frames: ComputerUseSequenceFrame[],
|
|
793
|
+
): Promise<ObservationImages | null> {
|
|
794
|
+
const decoded = frames.flatMap((frame) => {
|
|
795
|
+
const image = rgbToRgba(frame);
|
|
796
|
+
return image ? [{ t: frame.capturedAtMs, image }] : [];
|
|
797
|
+
});
|
|
798
|
+
if (decoded.length === 0) return null;
|
|
799
|
+
|
|
800
|
+
const sequence = buildFrameSequence({ frames: decoded });
|
|
801
|
+
if (sequence.frames.length === 0) return null;
|
|
802
|
+
|
|
803
|
+
await mkdir(FRAME_DIR, { recursive: true, mode: 0o700 }).catch(() => {});
|
|
804
|
+
await pruneStaleFrames();
|
|
805
|
+
|
|
806
|
+
const stamp = `${Date.now()}-${process.pid}`;
|
|
807
|
+
const images: ImageInputSpec[] = [];
|
|
808
|
+
let estimatedTokens = 0;
|
|
809
|
+
|
|
810
|
+
for (const frame of sequence.frames) {
|
|
811
|
+
// 帧序列一次给多张,用低压缩级别 PNG 控制编码耗时
|
|
812
|
+
// (注释原本写「使用 JPEG(质量 80)」,与代码不符,已订正)
|
|
813
|
+
const png = encodePng(frame.image, { channels: 3, level: 3 });
|
|
814
|
+
const path = join(FRAME_DIR, `seq-${stamp}-${frame.index}.png`);
|
|
815
|
+
await writeFile(path, png, { mode: 0o600 });
|
|
816
|
+
images.push({ source: path, detail: 'default' });
|
|
817
|
+
estimatedTokens += estimateImageTokens(frame.image);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
images,
|
|
822
|
+
note: sequence.note,
|
|
823
|
+
summary: sequence.summary,
|
|
824
|
+
estimatedTokens,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** 裸 RGB → RGBA。载荷长度与声明尺寸不符时返回 null,不拿坏数据组图。 */
|
|
829
|
+
function rgbToRgba(frame: ComputerUseSequenceFrame): RgbaImage | null {
|
|
830
|
+
const rgb = Buffer.from(frame.rgbBase64, 'base64');
|
|
831
|
+
const expected = frame.width * frame.height * 3;
|
|
832
|
+
if (rgb.length !== expected) return null;
|
|
833
|
+
|
|
834
|
+
const data = new Uint8Array(frame.width * frame.height * 4);
|
|
835
|
+
for (let src = 0, dst = 0; src < rgb.length; src += 3, dst += 4) {
|
|
836
|
+
data[dst] = rgb[src]!;
|
|
837
|
+
data[dst + 1] = rgb[src + 1]!;
|
|
838
|
+
data[dst + 2] = rgb[src + 2]!;
|
|
839
|
+
data[dst + 3] = 255;
|
|
840
|
+
}
|
|
841
|
+
return { width: frame.width, height: frame.height, data };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* 清掉过期帧文件。
|
|
846
|
+
*
|
|
847
|
+
* 每次组装时顺手做,而不是靠退出时清理:进程可能被 kill、可能崩溃,
|
|
848
|
+
* 而「退出时才清」的策略在这两种情况下都留下满磁盘的屏幕截图。
|
|
849
|
+
*
|
|
850
|
+
* 双重保护:
|
|
851
|
+
* 1. TTL:删除超过10分钟的文件(默认值,对齐内核规范)
|
|
852
|
+
* 2. LRU数量限制:如果剩余文件超过200个,删除最旧的
|
|
853
|
+
*/
|
|
854
|
+
export async function pruneStaleFrames(ttlMs: number = FRAME_TTL_MS, maxCount: number = FRAME_MAX_COUNT): Promise<void> {
|
|
855
|
+
try {
|
|
856
|
+
await mkdir(FRAME_DIR, { recursive: true, mode: 0o700 }).catch(() => {});
|
|
857
|
+
const entries = await readdir(FRAME_DIR).catch(() => [] as string[]);
|
|
858
|
+
const fileStats: Array<{ path: string; mtime: number }> = [];
|
|
859
|
+
|
|
860
|
+
for (const name of entries) {
|
|
861
|
+
const path = join(FRAME_DIR, name);
|
|
862
|
+
const info = await stat(path).catch(() => null);
|
|
863
|
+
if (info?.isFile()) {
|
|
864
|
+
fileStats.push({ path, mtime: info.mtimeMs });
|
|
865
|
+
} else if (!info) {
|
|
866
|
+
// 文件已不存在或无权限,尝试删除
|
|
867
|
+
await rm(path, { force: true }).catch(() => {});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const now = Date.now();
|
|
872
|
+
|
|
873
|
+
// 1. 删除TTL过期文件
|
|
874
|
+
const expired = fileStats.filter((f) => now - f.mtime > ttlMs);
|
|
875
|
+
await Promise.all(expired.map((f) => rm(f.path, { force: true }).catch(() => {})));
|
|
876
|
+
|
|
877
|
+
// 2. LRU:剩余文件按mtime升序,超过maxCount时删除最旧的
|
|
878
|
+
const remaining = fileStats
|
|
879
|
+
.filter((f) => now - f.mtime <= ttlMs)
|
|
880
|
+
.sort((a, b) => a.mtime - b.mtime);
|
|
881
|
+
|
|
882
|
+
if (remaining.length > maxCount) {
|
|
883
|
+
const toDelete = remaining.slice(0, remaining.length - maxCount);
|
|
884
|
+
await Promise.all(toDelete.map((f) => rm(f.path, { force: true }).catch(() => {})));
|
|
885
|
+
}
|
|
886
|
+
} catch {
|
|
887
|
+
// 清理失败不影响主功能
|
|
888
|
+
}
|
|
889
|
+
}
|