dsh-mobile-plugin 0.5.27 → 0.5.29
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/extensions/catalog.json +10 -1
- package/extensions/magpie/agentcompact.js +104 -0
- package/extensions/magpie/app.css +4902 -0
- package/extensions/magpie/app.js +24474 -0
- package/extensions/magpie/autobackup.js +185 -0
- package/extensions/magpie/autoresetown.js +151 -0
- package/extensions/magpie/balfound.js +38 -0
- package/extensions/magpie/boot.js +23 -0
- package/extensions/magpie/cfgblocked.js +39 -0
- package/extensions/magpie/checkinnow.js +111 -0
- package/extensions/magpie/claudemode.js +92 -0
- package/extensions/magpie/compat.js +41 -0
- package/extensions/magpie/context.css +433 -0
- package/extensions/magpie/context.js +1576 -0
- package/extensions/magpie/ctxtext.js +223 -0
- package/extensions/magpie/fonts.js +42 -0
- package/extensions/magpie/i18n.js +18942 -0
- package/extensions/magpie/icons/ai21.svg +1 -0
- package/extensions/magpie/icons/ai302-color.svg +1 -0
- package/extensions/magpie/icons/ai360-color.svg +1 -0
- package/extensions/magpie/icons/aihubmix-color.svg +1 -0
- package/extensions/magpie/icons/aionui.svg +1 -0
- package/extensions/magpie/icons/air.svg +4 -0
- package/extensions/magpie/icons/alibaba-color.svg +1 -0
- package/extensions/magpie/icons/alibabacloud-color.svg +1 -0
- package/extensions/magpie/icons/alma.png +0 -0
- package/extensions/magpie/icons/anthropic.svg +1 -0
- package/extensions/magpie/icons/antigravity-color.svg +1 -0
- package/extensions/magpie/icons/anyscale-color.svg +1 -0
- package/extensions/magpie/icons/app-alma.png +0 -0
- package/extensions/magpie/icons/app-cc-switch.png +0 -0
- package/extensions/magpie/icons/apple-touch-icon.png +0 -0
- package/extensions/magpie/icons/aside.svg +28 -0
- package/extensions/magpie/icons/astrbot.svg +1 -0
- package/extensions/magpie/icons/atomcode.png +0 -0
- package/extensions/magpie/icons/aws.svg +1 -0
- package/extensions/magpie/icons/azure-color.svg +1 -0
- package/extensions/magpie/icons/azureai-color.svg +1 -0
- package/extensions/magpie/icons/baichuan-color.svg +1 -0
- package/extensions/magpie/icons/baidu-color.svg +1 -0
- package/extensions/magpie/icons/baiducloud-color.svg +1 -0
- package/extensions/magpie/icons/bailian-color.svg +1 -0
- package/extensions/magpie/icons/baseten.svg +1 -0
- package/extensions/magpie/icons/bedrock-color.svg +1 -0
- package/extensions/magpie/icons/burncloud-color.svg +1 -0
- package/extensions/magpie/icons/bytedance-color.svg +1 -0
- package/extensions/magpie/icons/cerebras.svg +1 -0
- package/extensions/magpie/icons/chatglm-color.svg +1 -0
- package/extensions/magpie/icons/cherryin-color.svg +1 -0
- package/extensions/magpie/icons/chutes.svg +1 -0
- package/extensions/magpie/icons/cindy.png +0 -0
- package/extensions/magpie/icons/claude-color.svg +1 -0
- package/extensions/magpie/icons/claudecode-color.svg +1 -0
- package/extensions/magpie/icons/cline.svg +1 -0
- package/extensions/magpie/icons/cloudflare-color.svg +1 -0
- package/extensions/magpie/icons/codebuddy-color.svg +1 -0
- package/extensions/magpie/icons/codex-color.svg +1 -0
- package/extensions/magpie/icons/cohere-color.svg +1 -0
- package/extensions/magpie/icons/cometapi-color.svg +1 -0
- package/extensions/magpie/icons/commandcode.svg +1 -0
- package/extensions/magpie/icons/copilot-color.svg +1 -0
- package/extensions/magpie/icons/coze.svg +1 -0
- package/extensions/magpie/icons/crush.png +0 -0
- package/extensions/magpie/icons/curl.svg +1 -0
- package/extensions/magpie/icons/cursor.svg +1 -0
- package/extensions/magpie/icons/deepinfra-color.svg +1 -0
- package/extensions/magpie/icons/deepmind-color.svg +1 -0
- package/extensions/magpie/icons/deepseek-color.svg +1 -0
- package/extensions/magpie/icons/deveco-color.svg +12 -0
- package/extensions/magpie/icons/devin.svg +1 -0
- package/extensions/magpie/icons/doubao-color.svg +1 -0
- package/extensions/magpie/icons/empryo-color.svg +1 -0
- package/extensions/magpie/icons/factory.svg +1 -0
- package/extensions/magpie/icons/favicon.svg +1 -0
- package/extensions/magpie/icons/featherless-color.svg +1 -0
- package/extensions/magpie/icons/fireworks-color.svg +1 -0
- package/extensions/magpie/icons/friendli.svg +1 -0
- package/extensions/magpie/icons/fx.svg +1 -0
- package/extensions/magpie/icons/gemini-color.svg +1 -0
- package/extensions/magpie/icons/geminicli-color.svg +1 -0
- package/extensions/magpie/icons/gemma-color.svg +1 -0
- package/extensions/magpie/icons/giteeai.svg +1 -0
- package/extensions/magpie/icons/github.svg +1 -0
- package/extensions/magpie/icons/githubcopilot.svg +1 -0
- package/extensions/magpie/icons/glama.svg +1 -0
- package/extensions/magpie/icons/gmicloud.svg +1 -0
- package/extensions/magpie/icons/google-color.svg +1 -0
- package/extensions/magpie/icons/googlecloud-color.svg +1 -0
- package/extensions/magpie/icons/goose.svg +1 -0
- package/extensions/magpie/icons/grok.svg +1 -0
- package/extensions/magpie/icons/groq.svg +1 -0
- package/extensions/magpie/icons/hanako.png +0 -0
- package/extensions/magpie/icons/hermes.svg +1 -0
- package/extensions/magpie/icons/huawei-color.svg +1 -0
- package/extensions/magpie/icons/huaweicloud-color.svg +1 -0
- package/extensions/magpie/icons/huggingface-color.svg +1 -0
- package/extensions/magpie/icons/hunyuan-color.svg +1 -0
- package/extensions/magpie/icons/hyperbolic-color.svg +1 -0
- package/extensions/magpie/icons/icon-512.png +0 -0
- package/extensions/magpie/icons/iflytekcloud-color.svg +1 -0
- package/extensions/magpie/icons/inference.svg +1 -0
- package/extensions/magpie/icons/infinigence-color.svg +1 -0
- package/extensions/magpie/icons/internlm-color.svg +1 -0
- package/extensions/magpie/icons/jcode.svg +1 -0
- package/extensions/magpie/icons/jina.svg +1 -0
- package/extensions/magpie/icons/kilo.svg +1 -0
- package/extensions/magpie/icons/kilocode.svg +1 -0
- package/extensions/magpie/icons/kimi-code-desktop.png +0 -0
- package/extensions/magpie/icons/kimi.svg +1 -0
- package/extensions/magpie/icons/kiro-color.svg +1 -0
- package/extensions/magpie/icons/kling-color.svg +1 -0
- package/extensions/magpie/icons/lambda.svg +1 -0
- package/extensions/magpie/icons/leptonai-color.svg +1 -0
- package/extensions/magpie/icons/liquid.svg +1 -0
- package/extensions/magpie/icons/liveagent-color.svg +1 -0
- package/extensions/magpie/icons/llmapi.svg +1 -0
- package/extensions/magpie/icons/lmstudio.svg +1 -0
- package/extensions/magpie/icons/lody.png +0 -0
- package/extensions/magpie/icons/longcat.svg +1 -0
- package/extensions/magpie/icons/magpie.svg +1 -0
- package/extensions/magpie/icons/meta-color.svg +1 -0
- package/extensions/magpie/icons/metaai-color.svg +1 -0
- package/extensions/magpie/icons/microsoft-color.svg +1 -0
- package/extensions/magpie/icons/mimocode.svg +1 -0
- package/extensions/magpie/icons/minimax-color.svg +1 -0
- package/extensions/magpie/icons/mistermorph-color.svg +1 -0
- package/extensions/magpie/icons/mistral-color.svg +1 -0
- package/extensions/magpie/icons/mlx-serve.png +0 -0
- package/extensions/magpie/icons/modelscope-color.svg +1 -0
- package/extensions/magpie/icons/monica-color.svg +1 -0
- package/extensions/magpie/icons/moonshot.svg +1 -0
- package/extensions/magpie/icons/morph-color.svg +1 -0
- package/extensions/magpie/icons/mothx.png +0 -0
- package/extensions/magpie/icons/nebius.svg +1 -0
- package/extensions/magpie/icons/newapi-color.svg +1 -0
- package/extensions/magpie/icons/nousresearch.svg +1 -0
- package/extensions/magpie/icons/nova-color.svg +1 -0
- package/extensions/magpie/icons/novita-color.svg +1 -0
- package/extensions/magpie/icons/nvidia-color.svg +1 -0
- package/extensions/magpie/icons/ollama.svg +1 -0
- package/extensions/magpie/icons/omlx.svg +1 -0
- package/extensions/magpie/icons/omo.svg +1 -0
- package/extensions/magpie/icons/omp.svg +1 -0
- package/extensions/magpie/icons/openai.svg +1 -0
- package/extensions/magpie/icons/openchamber.svg +11 -0
- package/extensions/magpie/icons/openclaw.svg +1 -0
- package/extensions/magpie/icons/opencode.svg +1 -0
- package/extensions/magpie/icons/openmausbot.svg +1 -0
- package/extensions/magpie/icons/openrouter.svg +1 -0
- package/extensions/magpie/icons/openwebui.svg +1 -0
- package/extensions/magpie/icons/parasail.svg +1 -0
- package/extensions/magpie/icons/penguin-color.svg +1 -0
- package/extensions/magpie/icons/perplexity-color.svg +1 -0
- package/extensions/magpie/icons/phala-color.svg +1 -0
- package/extensions/magpie/icons/pi.svg +1 -0
- package/extensions/magpie/icons/pipellm-color.svg +5 -0
- package/extensions/magpie/icons/poe.svg +1 -0
- package/extensions/magpie/icons/ppio-color.svg +1 -0
- package/extensions/magpie/icons/prime-agent.svg +1 -0
- package/extensions/magpie/icons/qiniu-color.svg +1 -0
- package/extensions/magpie/icons/qoder.svg +1 -0
- package/extensions/magpie/icons/qwen-color.svg +1 -0
- package/extensions/magpie/icons/qwenpaw.svg +1 -0
- package/extensions/magpie/icons/raycast.svg +1 -0
- package/extensions/magpie/icons/reasonix-color.svg +48 -0
- package/extensions/magpie/icons/reka.svg +1 -0
- package/extensions/magpie/icons/replicate.svg +1 -0
- package/extensions/magpie/icons/roocode.svg +1 -0
- package/extensions/magpie/icons/rtk.png +0 -0
- package/extensions/magpie/icons/sambanova-color.svg +1 -0
- package/extensions/magpie/icons/sensenova-color.svg +1 -0
- package/extensions/magpie/icons/siliconcloud-color.svg +1 -0
- package/extensions/magpie/icons/snow.png +0 -0
- package/extensions/magpie/icons/snowflake-color.svg +1 -0
- package/extensions/magpie/icons/sophnet-color.svg +1 -0
- package/extensions/magpie/icons/spark-color.svg +1 -0
- package/extensions/magpie/icons/stepfun-color.svg +1 -0
- package/extensions/magpie/icons/straico-color.svg +1 -0
- package/extensions/magpie/icons/submodel-color.svg +1 -0
- package/extensions/magpie/icons/t3code.svg +4 -0
- package/extensions/magpie/icons/targon-color.svg +1 -0
- package/extensions/magpie/icons/tencent-color.svg +1 -0
- package/extensions/magpie/icons/tencentcloud-color.svg +1 -0
- package/extensions/magpie/icons/tiangong-color.svg +1 -0
- package/extensions/magpie/icons/together-color.svg +1 -0
- package/extensions/magpie/icons/trae.svg +1 -0
- package/extensions/magpie/icons/typesafe.png +0 -0
- package/extensions/magpie/icons/upstage-color.svg +1 -0
- package/extensions/magpie/icons/venice-color.svg +1 -0
- package/extensions/magpie/icons/vercel.svg +1 -0
- package/extensions/magpie/icons/vertexai-color.svg +1 -0
- package/extensions/magpie/icons/vllm-color.svg +1 -0
- package/extensions/magpie/icons/volcengine-color.svg +1 -0
- package/extensions/magpie/icons/voyage-color.svg +1 -0
- package/extensions/magpie/icons/vscode.svg +1 -0
- package/extensions/magpie/icons/wenxin-color.svg +1 -0
- package/extensions/magpie/icons/windsurf.svg +1 -0
- package/extensions/magpie/icons/workbuddy-color.svg +1 -0
- package/extensions/magpie/icons/workersai-color.svg +1 -0
- package/extensions/magpie/icons/xai.svg +1 -0
- package/extensions/magpie/icons/xiaomimimo.svg +1 -0
- package/extensions/magpie/icons/xinference-color.svg +1 -0
- package/extensions/magpie/icons/yi.svg +1 -0
- package/extensions/magpie/icons/yuanbao-color.svg +1 -0
- package/extensions/magpie/icons/yylx.svg +5 -0
- package/extensions/magpie/icons/zai.svg +1 -0
- package/extensions/magpie/icons/zcode.png +0 -0
- package/extensions/magpie/icons/zed.svg +1 -0
- package/extensions/magpie/icons/zenmux.svg +1 -0
- package/extensions/magpie/icons/zeroone.svg +1 -0
- package/extensions/magpie/icons/zhipu-color.svg +1 -0
- package/extensions/magpie/icons/zoo-code.png +0 -0
- package/extensions/magpie/index.html +572 -0
- package/extensions/magpie/kernel.js +274 -0
- package/extensions/magpie/keyspend.js +63 -0
- package/extensions/magpie/layout.js +26 -0
- package/extensions/magpie/manifest.json +37 -0
- package/extensions/magpie/navlamp.js +47 -0
- package/extensions/magpie/omarchy.css +72 -0
- package/extensions/magpie/omarchy.js +46 -0
- package/extensions/magpie/passerrors.js +21 -0
- package/extensions/magpie/pluginpages.js +405 -0
- package/extensions/magpie/plugins/gateway/client.js +1194 -0
- package/extensions/magpie/plugins/gateway/keys.js +972 -0
- package/extensions/magpie/plugins/gateway/settings.js +459 -0
- package/extensions/magpie/plugins/library/client.js +6788 -0
- package/extensions/magpie/plugins/library/i18n.js +3228 -0
- package/extensions/magpie/plugins/library/library.css +848 -0
- package/extensions/magpie/plugins/otel/i18n.js +48 -0
- package/extensions/magpie/plugins/otel/page.js +189 -0
- package/extensions/magpie/plugins/redact/client.js +46 -0
- package/extensions/magpie/plugins/redact/demo.js +461 -0
- package/extensions/magpie/plugins/redact/i18n.js +432 -0
- package/extensions/magpie/plugins/redact/page.js +985 -0
- package/extensions/magpie/plugins/redact/redact.css +385 -0
- package/extensions/magpie/plugins/redact/settings.js +248 -0
- package/extensions/magpie/plugins/redact/xq_dark.jpg +0 -0
- package/extensions/magpie/plugins/redact/xq_dark.webm +0 -0
- package/extensions/magpie/plugins/redact/xq_light.jpg +0 -0
- package/extensions/magpie/plugins/redact/xq_light.webm +0 -0
- package/extensions/magpie/plugins/routing/client.js +8593 -0
- package/extensions/magpie/plugins/routing/i18n.js +2696 -0
- package/extensions/magpie/plugins/routing/routing.css +1111 -0
- package/extensions/magpie/plugins/search/settings.js +460 -0
- package/extensions/magpie/plugins/sessions/client.js +1622 -0
- package/extensions/magpie/plugins/sessions/i18n.js +360 -0
- package/extensions/magpie/plugins/sessions/sessions.css +181 -0
- package/extensions/magpie/plugins/sessions/settings.js +100 -0
- package/extensions/magpie/plugins/usage/client.js +150 -0
- package/extensions/magpie/plugins/usage/i18n.js +984 -0
- package/extensions/magpie/plugins/usage/page.js +4307 -0
- package/extensions/magpie/plugins.css +319 -0
- package/extensions/magpie/plugins.js +1926 -0
- package/extensions/magpie/profilerename.js +27 -0
- package/extensions/magpie/purposes.js +2 -0
- package/extensions/magpie/quitask.js +79 -0
- package/extensions/magpie/quotaall.js +142 -0
- package/extensions/magpie/ratewindows.js +123 -0
- package/extensions/magpie/registry.js +87 -0
- package/extensions/magpie/resetask.js +137 -0
- package/extensions/magpie/ruleedit.js +53 -0
- package/extensions/magpie/scrollends.js +91 -0
- package/extensions/magpie/settingshints.js +75 -0
- package/extensions/magpie/signsheets.js +86 -0
- package/extensions/magpie/start.js +26 -0
- package/extensions/magpie/syncfiles.js +216 -0
- package/extensions/magpie/theme.js +29 -0
- package/extensions/magpie/traywindows.js +41 -0
- package/extensions/magpie/webfonts.js +130 -0
- package/extensions/magpie/whatsnew.js +252 -0
- package/extensions/magpie.zip +0 -0
- package/knowledge/apps/baijiahao.md +30 -0
- package/knowledge/apps/bilibili.md +1 -0
- package/knowledge/apps/dedao.md +33 -0
- package/knowledge/apps/feishu.md +35 -0
- package/knowledge/apps/gmail.md +33 -0
- package/knowledge/apps/jike.md +31 -0
- package/knowledge/apps/kuaishou.md +34 -0
- package/knowledge/apps/linkedin.md +33 -0
- package/knowledge/apps/qqmail.md +30 -0
- package/knowledge/apps/telegram.md +27 -0
- package/knowledge/apps/toutiao.md +32 -0
- package/knowledge/apps/uber-eats.md +26 -0
- package/knowledge/apps/uber.md +27 -0
- package/knowledge/apps/whatsapp.md +27 -0
- package/knowledge/apps/x.md +32 -0
- package/knowledge/apps/xiaohongshu.md +1 -0
- package/knowledge/apps/zhihu.md +1 -0
- package/knowledge/sites/bilibili.com/SITE.md +50 -0
- package/knowledge/sites/bilibili.com/references/view-fields.md +17 -0
- package/knowledge/sites/github.com/SITE.md +51 -0
- package/knowledge/sites/github.com/references/repo-fields.md +13 -0
- package/knowledge/sites/x.com/SITE.md +47 -0
- package/knowledge/sites/xiaohongshu.com/SITE.md +48 -0
- package/knowledge/sites/xiaohongshu.com/references/note-fields.md +18 -0
- package/knowledge/sites/youtube.com/SITE.md +49 -0
- package/knowledge/sites/youtube.com/references/innertube-fields.md +14 -0
- package/knowledge/sites/zhihu.com/SITE.md +47 -0
- package/knowledge/sites/zhihu.com/references/api-fields.md +21 -0
- package/lib/mobile/extension-catalog.js +41 -1
- package/lib/mobile/extension-data.js +224 -0
- package/lib/mobile/extension-dev.js +3 -1
- package/lib/mobile/file-ops.js +252 -0
- package/lib/mobile/history-derive.js +80 -4
- package/lib/mobile/host-bridge/endpoints.js +48 -6
- package/lib/mobile/host-bridge/event-synthesis.js +33 -6
- package/lib/mobile/jobs-control.js +118 -0
- package/lib/mobile/memory.js +635 -0
- package/lib/mobile/model-badges.js +77 -0
- package/lib/mobile/pet-tools.js +50 -4
- package/lib/mobile/pets.js +214 -18
- package/lib/mobile/phone/access.js +85 -0
- package/lib/mobile/phone/evidence.js +228 -0
- package/lib/mobile/phone/gate.js +290 -0
- package/lib/mobile/phone/grounding.js +314 -0
- package/lib/mobile/phone/guard.js +465 -0
- package/lib/mobile/phone/irreversible.js +88 -8
- package/lib/mobile/phone/knowledge-tools.js +665 -0
- package/lib/mobile/phone/learn.js +329 -0
- package/lib/mobile/phone/redact.js +237 -0
- package/lib/mobile/phone/risk.js +95 -0
- package/lib/mobile/phone/routes.js +28 -1
- package/lib/mobile/phone/service.js +23 -0
- package/lib/mobile/phone/site-commands.js +271 -0
- package/lib/mobile/phone/step-summary.js +201 -0
- package/lib/mobile/phone/stop-summary.js +235 -0
- package/lib/mobile/phone/tool-groups.js +125 -0
- package/lib/mobile/phone/tools.js +589 -0
- package/lib/mobile/phone/untrusted.js +65 -0
- package/lib/mobile/phone-knowledge.js +20 -4
- package/lib/mobile/schedule-edit.js +500 -0
- package/lib/mobile/schedule.js +5 -0
- package/lib/mobile/send-continuation.js +52 -0
- package/lib/mobile/session-models.js +27 -31
- package/lib/mobile/session-send.js +12 -12
- package/lib/mobile/skill-import.js +338 -0
- package/lib/mobile/skill-store.js +524 -0
- package/lib/routes/mobile-api.js +42 -0
- package/package.json +7 -6
- package/skills/skill-authoring/SKILL.md +46 -0
- package/skills/skill-authoring/references/examples.md +82 -0
- package/skills/skill-authoring/references/frontmatter-and-fields.md +46 -0
- package/skills/skill-authoring/scripts/check_skill.py +167 -0
- package/skills/skill-authoring/scripts/manifest.json +3 -0
- package/knowledge/sites/bilibili.com.md +0 -27
- package/knowledge/sites/github.com.md +0 -27
- package/knowledge/sites/x.com.md +0 -28
- package/knowledge/sites/xiaohongshu.com.md +0 -27
- package/knowledge/sites/youtube.com.md +0 -28
- package/knowledge/sites/zhihu.com.md +0 -27
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 操作停滞守卫:手机每一步结束之后记一笔,判断模型是不是「原地打转」,并在合适的级别提醒
|
|
5
|
+
* 或直接叫停。纯逻辑、无 IO、无时钟、可单测 —— 所有计数都在这一份实例里,外部每步喂一条事实。
|
|
6
|
+
*
|
|
7
|
+
* 分级照 Mika 的 GuardRules / OperationGuards:同一动作+同一目标重复 3/6/12;动作没有效果
|
|
8
|
+
* 3/6/9/12;页面没变却重读 5/8/12;连续失败 2/4/8(不可恢复 1/3);在两三个状态之间兜圈
|
|
9
|
+
* 2/4/...(安全网按步数上限),回到同一状态、连续没到过新状态也各有阶梯。每一级先「提醒」,
|
|
10
|
+
* 最后一级才是「叫停」—— 叫停是安全网,门槛远在提醒之后。提醒里附三条出路:换一条明显不同
|
|
11
|
+
* 的路 / 问用户 / 把已经拿到的交出来并说明还缺什么。
|
|
12
|
+
*
|
|
13
|
+
* 界面指纹用元素表文本(去掉会变的 ref)的 64 位 FNV-1a,照 Mika 的 Fingerprints.kt:纯文本
|
|
14
|
+
* 模型没有截图,也能判断「画面没变」。
|
|
15
|
+
*
|
|
16
|
+
* 用法:
|
|
17
|
+
* const guard = createGuard({ budget: 60 });
|
|
18
|
+
* const v = guard.noteStep({ tool:'phone_tap', args:{ref:'e3'}, kind:'action',
|
|
19
|
+
* effect:'no_effect', elements });
|
|
20
|
+
* if (v) { value.note = (value.note ? value.note + ' ' : '') + v.advice; } // warn 才带 advice
|
|
21
|
+
* if (guard.shouldStop()) { /* 把这一轮收掉 *\/ }
|
|
22
|
+
*
|
|
23
|
+
* noteStep 接受的 step 字段(除 tool 外都可缺,缺了只是少一维判定):
|
|
24
|
+
* tool 工具名,参与「同一动作同一目标」的签名
|
|
25
|
+
* args 参数,参与签名(忽略 reason,坐标按 50 取整,滑动只记方向)
|
|
26
|
+
* kind 'action'(默认)| 'read' | 'diagnostic'
|
|
27
|
+
* effect 'changed' | 'small_change' | 'place_changed' | 'no_effect'(动作实际效果)
|
|
28
|
+
* fingerprint 已经算好的界面指纹(字符串/数字);也可直接给 elements
|
|
29
|
+
* elements 这一屏的元素表,本模块自己算指纹
|
|
30
|
+
* fresh 读动作这次是否读到了之前没读到的新内容(read/diagnostic 用)
|
|
31
|
+
* ok false 记一次失败,true 清零失败计数,缺省表示这份结果不表态
|
|
32
|
+
* recoverable false 记为不可恢复失败
|
|
33
|
+
* treeBlind 这次动作是在读不出控件树的情况下点的(无效果时更早提醒)
|
|
34
|
+
* diagnostic true 时按「诊断类工具」的更高门槛计重复
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const SEVERITY = { NONE: 'none', WARN: 'warn', STOP: 'stop' };
|
|
38
|
+
|
|
39
|
+
/** 不知道任务步数上限时按这么多步算(与 Mika 一致)。 */
|
|
40
|
+
const DEFAULT_BUDGET = 60;
|
|
41
|
+
/** 「同一动作同一目标」看最近多少个动作。 */
|
|
42
|
+
const LOOP_WINDOW = 16;
|
|
43
|
+
/** 兜圈最多看几个状态一圈。 */
|
|
44
|
+
const MAX_CYCLE = 3;
|
|
45
|
+
/** 兜圈 trail 最多记多少(够看出最长的安全网)。 */
|
|
46
|
+
const TRAIL_KEEP = 64;
|
|
47
|
+
/** 元素里这些字段每次都变,算指纹前先去掉。 */
|
|
48
|
+
const VOLATILE_ELEMENT_KEYS = ['ref'];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 阈值表(导出给交付报告与单测真值)。每条的最后一级是「叫停」,其余是「提醒」。
|
|
52
|
+
* 带 divisor/floor 的是「按步数上限的安全网」:lateStop(budget, divisor, floor)。
|
|
53
|
+
*/
|
|
54
|
+
const THRESHOLDS = {
|
|
55
|
+
repeat: [3, 6, 12],
|
|
56
|
+
repeatDiagnostic: [6, 9, 15],
|
|
57
|
+
noEffect: [3, 6, 9, 12],
|
|
58
|
+
reread: [5, 8, 12],
|
|
59
|
+
failure: [2, 4, 8],
|
|
60
|
+
unrecoverable: [1, 3],
|
|
61
|
+
failureWaysOut: 4,
|
|
62
|
+
sameState: { base: [3, 6], divisor: 4, floor: 10 },
|
|
63
|
+
cycle: { base: [2, 4], divisor: 5, floor: 6 },
|
|
64
|
+
noNewState: { base: [8, 16], divisor: 2, floor: 24 },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** 提醒里给的三条出路。 */
|
|
68
|
+
const WAYS_OUT =
|
|
69
|
+
'给你三条路:换一条明显不同的做法(换工具、换入口、换说法);还是不行就直接问用户;' +
|
|
70
|
+
'或者把已经拿到的结果交出来,并说清还缺哪一步。';
|
|
71
|
+
|
|
72
|
+
/** 各条提醒开头「卡在哪」那句。 */
|
|
73
|
+
const LEADS = {
|
|
74
|
+
repeating_action: (n) => `同一个动作(同一目标)已经重复 ${n} 次,屏幕上没有给你新东西。`,
|
|
75
|
+
no_progress: (n) => `连续 ${n} 个动作都没有效果,屏幕没有变化。`,
|
|
76
|
+
read_loop: (n) => `页面没有变,你已经重读了 ${n} 次,读不出新内容。`,
|
|
77
|
+
too_many_failures: (n) => `工具已经连续失败 ${n} 次,原样重试大概率还是失败。`,
|
|
78
|
+
same_state: (n) => `这是第 ${n} 次回到同一个界面状态。`,
|
|
79
|
+
state_cycle: (n, p) => `在 ${p} 个状态之间兜圈子,已经转了 ${n} 圈。`,
|
|
80
|
+
no_new_state: (n) => `连续 ${n} 个动作都没有走到新状态。`,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function lateStop(budget, divisor, floor) {
|
|
84
|
+
const b = Number.isFinite(budget) && budget > 0 ? budget : DEFAULT_BUDGET;
|
|
85
|
+
return Math.max(floor, Math.floor(b / divisor));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sameStateLadder(budget) {
|
|
89
|
+
return THRESHOLDS.sameState.base.concat(lateStop(budget, THRESHOLDS.sameState.divisor, THRESHOLDS.sameState.floor));
|
|
90
|
+
}
|
|
91
|
+
function cycleLadder(budget) {
|
|
92
|
+
return THRESHOLDS.cycle.base.concat(lateStop(budget, THRESHOLDS.cycle.divisor, THRESHOLDS.cycle.floor));
|
|
93
|
+
}
|
|
94
|
+
function noNewStateLadder(budget) {
|
|
95
|
+
return THRESHOLDS.noNewState.base.concat(lateStop(budget, THRESHOLDS.noNewState.divisor, THRESHOLDS.noNewState.floor));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* count 在升序阶梯 ladder 上到了哪一级。没到第一级、或这一级(warnedAt 是已提醒到的门槛)已提醒过
|
|
100
|
+
* 返回 null;到最高一级且 canStop 时叫停,否则提醒。照 Mika GuardRules.climb。
|
|
101
|
+
*/
|
|
102
|
+
function climb(count, ladder, warnedAt, canStop) {
|
|
103
|
+
let threshold = null;
|
|
104
|
+
for (const t of ladder) if (count >= t) threshold = t;
|
|
105
|
+
if (threshold === null) return null;
|
|
106
|
+
if (threshold <= warnedAt) return null;
|
|
107
|
+
const last = ladder[ladder.length - 1];
|
|
108
|
+
return { threshold, severity: threshold === last && canStop ? SEVERITY.STOP : SEVERITY.WARN };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 64 位 FNV-1a(照 Mika Fingerprints.kt 的初值与素数)。返回 16 位十六进制串。 */
|
|
112
|
+
function fnv1a64(str) {
|
|
113
|
+
let h = 0xcbf29ce484222325n;
|
|
114
|
+
const bytes = Buffer.from(String(str), 'utf8');
|
|
115
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
116
|
+
h ^= BigInt(bytes[i]);
|
|
117
|
+
h = BigInt.asUintN(64, h * 0x100000001b3n);
|
|
118
|
+
}
|
|
119
|
+
return h.toString(16).padStart(16, '0');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 稳定序列化:对象键排序,数组保序。让同样的元素表每次得到同样的指纹。 */
|
|
123
|
+
function stableStringify(value) {
|
|
124
|
+
if (value === undefined) return 'null';
|
|
125
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
126
|
+
if (Array.isArray(value)) return '[' + value.map(stableStringify).join(',') + ']';
|
|
127
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
|
|
128
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(value[k])).join(',') + '}';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 去掉元素表里每次都变的字段(ref),留下真正的界面内容。 */
|
|
132
|
+
function stripVolatile(el) {
|
|
133
|
+
if (!el || typeof el !== 'object' || Array.isArray(el)) return el;
|
|
134
|
+
const copy = {};
|
|
135
|
+
for (const k of Object.keys(el)) {
|
|
136
|
+
if (VOLATILE_ELEMENT_KEYS.indexOf(k) === -1) copy[k] = el[k];
|
|
137
|
+
}
|
|
138
|
+
return copy;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 画面指纹:元素表文本(去掉 ref)的 64 位 FNV。elements 不是数组返回 null。
|
|
143
|
+
* @param {Array} elements
|
|
144
|
+
* @returns {string|null}
|
|
145
|
+
*/
|
|
146
|
+
function elementsFingerprint(elements) {
|
|
147
|
+
if (!Array.isArray(elements)) return null;
|
|
148
|
+
return fnv1a64(stableStringify(elements.map(stripVolatile)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function num(v) {
|
|
152
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 动作签名:tool + 参数(忽略 reason;坐标按 50 取整;滑动只记方向)。参数顺序不影响结果。
|
|
157
|
+
* @param {string} tool
|
|
158
|
+
* @param {object} args
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
function actionSignature(tool, args) {
|
|
162
|
+
const a = args && typeof args === 'object' && !Array.isArray(args) ? Object.assign({}, args) : {};
|
|
163
|
+
delete a.reason;
|
|
164
|
+
const x = num(a.x);
|
|
165
|
+
const y = num(a.y);
|
|
166
|
+
if (x !== null && y !== null) {
|
|
167
|
+
a.x = Math.floor(x / 50);
|
|
168
|
+
a.y = Math.floor(y / 50);
|
|
169
|
+
}
|
|
170
|
+
const swipeKeys = ['startX', 'startY', 'endX', 'endY'];
|
|
171
|
+
const s = swipeKeys.map((k) => num(a[k]));
|
|
172
|
+
if (s.every((v) => v !== null)) {
|
|
173
|
+
const dx = s[2] - s[0];
|
|
174
|
+
const dy = s[3] - s[1];
|
|
175
|
+
const vertical = Math.abs(dy) >= Math.abs(dx);
|
|
176
|
+
const major = vertical ? dy : dx;
|
|
177
|
+
a.swipeDirection = (vertical ? 'V' : 'H') + (major < 0 ? '-' : '+');
|
|
178
|
+
for (const k of swipeKeys) delete a[k];
|
|
179
|
+
}
|
|
180
|
+
return String(tool == null ? '' : tool) + '|' + stableStringify(a);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 在 trail(相邻不同的状态序列)末尾找「在 2–3 个状态间兜圈」。照 Mika GuardRules.cycle。 */
|
|
184
|
+
function detectCycle(trail) {
|
|
185
|
+
let best = null;
|
|
186
|
+
for (let p = 2; p <= MAX_CYCLE; p++) {
|
|
187
|
+
if (trail.length < p + 1) continue;
|
|
188
|
+
const tail = trail.slice(-p);
|
|
189
|
+
if (new Set(tail).size !== p) continue;
|
|
190
|
+
let len = p;
|
|
191
|
+
let i = trail.length - 1;
|
|
192
|
+
while (i - p >= 0 && trail[i] === trail[i - p]) {
|
|
193
|
+
len++;
|
|
194
|
+
i--;
|
|
195
|
+
}
|
|
196
|
+
if (len > p && (best === null || len > best.length)) {
|
|
197
|
+
best = { period: p, length: len, rounds: Math.floor(len / p) };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return best;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function fingerprintOf(step) {
|
|
204
|
+
if (step.fingerprint !== undefined && step.fingerprint !== null) return String(step.fingerprint);
|
|
205
|
+
if (Array.isArray(step.elements)) return elementsFingerprint(step.elements);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 建一份守卫。options.budget 是这一轮允许的步数上限(影响兜圈/回同状态/无新状态的安全网)。
|
|
211
|
+
* @param {{budget?: number}} [options]
|
|
212
|
+
*/
|
|
213
|
+
function createGuard(options) {
|
|
214
|
+
const opts = options || {};
|
|
215
|
+
const budget = Number.isFinite(opts.budget) ? opts.budget : null;
|
|
216
|
+
|
|
217
|
+
let state;
|
|
218
|
+
|
|
219
|
+
function reset() {
|
|
220
|
+
state = {
|
|
221
|
+
lastFingerprint: null,
|
|
222
|
+
seenStates: new Set(),
|
|
223
|
+
visits: new Map(),
|
|
224
|
+
visitsWarnedAt: new Map(),
|
|
225
|
+
trail: [],
|
|
226
|
+
cycleWarnedAt: 0,
|
|
227
|
+
sinceNewState: 0,
|
|
228
|
+
sinceNewWarnedAt: 0,
|
|
229
|
+
unchanged: 0,
|
|
230
|
+
unchangedWarnedAt: 0,
|
|
231
|
+
treeBlindActions: 0,
|
|
232
|
+
window: [],
|
|
233
|
+
repeatWarned: new Map(),
|
|
234
|
+
rereads: 0,
|
|
235
|
+
rereadWarnedAt: 0,
|
|
236
|
+
consecutiveFailures: 0,
|
|
237
|
+
unrecoverableFailures: 0,
|
|
238
|
+
failureWarnedAt: 0,
|
|
239
|
+
unrecoverableWarnedAt: 0,
|
|
240
|
+
stopped: null,
|
|
241
|
+
level: SEVERITY.NONE,
|
|
242
|
+
lastAdvice: null,
|
|
243
|
+
lastVerdict: null,
|
|
244
|
+
emitted: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
reset();
|
|
248
|
+
|
|
249
|
+
function buildAdvice(code, count, period) {
|
|
250
|
+
const lead = LEADS[code] ? LEADS[code](count, period) : `卡住了(${code},${count} 次)。`;
|
|
251
|
+
return `${lead}${WAYS_OUT}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function report(code, severity, count, period) {
|
|
255
|
+
const advice = severity === SEVERITY.WARN ? buildAdvice(code, count, period) : null;
|
|
256
|
+
const verdict = { code, severity, count, advice };
|
|
257
|
+
state.lastVerdict = verdict;
|
|
258
|
+
state.emitted.push(verdict);
|
|
259
|
+
if (severity === SEVERITY.STOP) {
|
|
260
|
+
state.stopped = verdict;
|
|
261
|
+
state.level = SEVERITY.STOP;
|
|
262
|
+
} else if (state.level !== SEVERITY.STOP) {
|
|
263
|
+
state.level = SEVERITY.WARN;
|
|
264
|
+
}
|
|
265
|
+
if (advice) state.lastAdvice = advice;
|
|
266
|
+
return verdict;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resetNoEffect() {
|
|
270
|
+
state.unchanged = 0;
|
|
271
|
+
state.unchangedWarnedAt = 0;
|
|
272
|
+
state.treeBlindActions = 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function progress() {
|
|
276
|
+
resetNoEffect();
|
|
277
|
+
state.sinceNewState = 0;
|
|
278
|
+
state.sinceNewWarnedAt = 0;
|
|
279
|
+
if (state.stopped && (state.stopped.code === 'no_progress' || state.stopped.code === 'read_loop')) {
|
|
280
|
+
state.stopped = null;
|
|
281
|
+
state.level = SEVERITY.NONE;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function noProgress() {
|
|
286
|
+
if (state.treeBlindActions >= 2) {
|
|
287
|
+
const severity = state.unchanged >= 3 ? SEVERITY.STOP : SEVERITY.WARN;
|
|
288
|
+
return report('no_progress', severity, state.unchanged);
|
|
289
|
+
}
|
|
290
|
+
const rung = climb(state.unchanged, THRESHOLDS.noEffect, state.unchangedWarnedAt, true);
|
|
291
|
+
if (!rung) return null;
|
|
292
|
+
state.unchangedWarnedAt = rung.threshold;
|
|
293
|
+
return report('no_progress', rung.severity, state.unchanged);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function sameState(fp, n) {
|
|
297
|
+
const rung = climb(n, sameStateLadder(budget), state.visitsWarnedAt.get(fp) || 0, true);
|
|
298
|
+
if (!rung) return null;
|
|
299
|
+
state.visitsWarnedAt.set(fp, rung.threshold);
|
|
300
|
+
return report('same_state', rung.severity, n);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function cycleCheck(cyc) {
|
|
304
|
+
if (!cyc) {
|
|
305
|
+
state.cycleWarnedAt = 0;
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const rung = climb(cyc.rounds, cycleLadder(budget), state.cycleWarnedAt, true);
|
|
309
|
+
if (!rung) return null;
|
|
310
|
+
state.cycleWarnedAt = rung.threshold;
|
|
311
|
+
return report('state_cycle', rung.severity, cyc.rounds, cyc.period);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function noNewState() {
|
|
315
|
+
const rung = climb(state.sinceNewState, noNewStateLadder(budget), state.sinceNewWarnedAt, false);
|
|
316
|
+
if (!rung) return null;
|
|
317
|
+
state.sinceNewWarnedAt = rung.threshold;
|
|
318
|
+
return report('no_new_state', rung.severity, state.sinceNewState);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function noteRepeat(step) {
|
|
322
|
+
const diag = step.diagnostic === true;
|
|
323
|
+
const sig = diag ? String(step.tool == null ? '' : step.tool) : actionSignature(step.tool, step.args);
|
|
324
|
+
state.window.push(sig);
|
|
325
|
+
if (state.window.length > LOOP_WINDOW) state.window.shift();
|
|
326
|
+
let count = 0;
|
|
327
|
+
for (const s of state.window) if (s === sig) count++;
|
|
328
|
+
const ladder = diag ? THRESHOLDS.repeatDiagnostic : THRESHOLDS.repeat;
|
|
329
|
+
const rung = climb(count, ladder, state.repeatWarned.get(sig) || 0, true);
|
|
330
|
+
if (!rung) return null;
|
|
331
|
+
state.repeatWarned.set(sig, rung.threshold);
|
|
332
|
+
return report('repeating_action', rung.severity, count);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function noteFailure(step) {
|
|
336
|
+
const recoverable = step.recoverable !== false;
|
|
337
|
+
state.consecutiveFailures += 1;
|
|
338
|
+
if (!recoverable) state.unrecoverableFailures += 1;
|
|
339
|
+
const r1 = climb(state.consecutiveFailures, THRESHOLDS.failure, state.failureWarnedAt, true);
|
|
340
|
+
if (r1) {
|
|
341
|
+
state.failureWarnedAt = r1.threshold;
|
|
342
|
+
report('too_many_failures', r1.severity, state.consecutiveFailures);
|
|
343
|
+
}
|
|
344
|
+
const r2 = climb(state.unrecoverableFailures, THRESHOLDS.unrecoverable, state.unrecoverableWarnedAt, true);
|
|
345
|
+
if (r2) {
|
|
346
|
+
state.unrecoverableWarnedAt = r2.threshold;
|
|
347
|
+
report('too_many_failures', r2.severity, state.consecutiveFailures);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function resetFailures() {
|
|
352
|
+
state.consecutiveFailures = 0;
|
|
353
|
+
state.unrecoverableFailures = 0;
|
|
354
|
+
state.failureWarnedAt = 0;
|
|
355
|
+
state.unrecoverableWarnedAt = 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 收到一屏。[didAct] 表示这屏是不是一个动作之后拿到的;[effectNoted] 表示这个动作的「有没有效果」
|
|
360
|
+
* 已经按 effect 字段记过,别再用「指纹没变」重复记一次。
|
|
361
|
+
*/
|
|
362
|
+
function handleFrame(fp, didAct, effectNoted) {
|
|
363
|
+
if (fp === state.lastFingerprint) {
|
|
364
|
+
if (didAct && !effectNoted) {
|
|
365
|
+
state.unchanged += 1;
|
|
366
|
+
noProgress();
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
state.lastFingerprint = fp;
|
|
371
|
+
state.rereads = 0;
|
|
372
|
+
state.rereadWarnedAt = 0;
|
|
373
|
+
if (state.trail.length === 0 || state.trail[state.trail.length - 1] !== fp) state.trail.push(fp);
|
|
374
|
+
if (state.trail.length > TRAIL_KEEP) state.trail.shift();
|
|
375
|
+
const seenBefore = state.seenStates.has(fp);
|
|
376
|
+
let cyc = null;
|
|
377
|
+
if (!seenBefore) {
|
|
378
|
+
state.seenStates.add(fp);
|
|
379
|
+
state.visits.set(fp, 1);
|
|
380
|
+
progress();
|
|
381
|
+
} else {
|
|
382
|
+
const n = (state.visits.get(fp) || 1) + 1;
|
|
383
|
+
state.visits.set(fp, n);
|
|
384
|
+
cyc = detectCycle(state.trail);
|
|
385
|
+
if (!cyc) sameState(fp, n);
|
|
386
|
+
}
|
|
387
|
+
if (cyc === null) cyc = detectCycle(state.trail);
|
|
388
|
+
cycleCheck(cyc);
|
|
389
|
+
if (didAct) noNewState();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 喂一步。返回这一步给出的最重要一条结论(stop 优先,否则最后一条 warn),没有则 null。
|
|
394
|
+
* 返回对象形如 { code, severity, count, advice };advice 只有 warn 才有(三出路那段)。
|
|
395
|
+
* @param {object} step
|
|
396
|
+
*/
|
|
397
|
+
function noteStep(step) {
|
|
398
|
+
if (state.stopped) return state.stopped;
|
|
399
|
+
const s = step && typeof step === 'object' ? step : {};
|
|
400
|
+
state.emitted = [];
|
|
401
|
+
|
|
402
|
+
if (s.ok === false) noteFailure(s);
|
|
403
|
+
else if (s.ok === true) resetFailures();
|
|
404
|
+
|
|
405
|
+
const fp = fingerprintOf(s);
|
|
406
|
+
const kind = s.kind || (s.tool === 'phone_screen' ? 'read' : 'action');
|
|
407
|
+
|
|
408
|
+
if (kind === 'read' || kind === 'diagnostic') {
|
|
409
|
+
if (s.fresh === true) progress();
|
|
410
|
+
if (fp !== null) {
|
|
411
|
+
if (fp === state.lastFingerprint && s.fresh !== true) {
|
|
412
|
+
state.rereads += 1;
|
|
413
|
+
const rung = climb(state.rereads, THRESHOLDS.reread, state.rereadWarnedAt, true);
|
|
414
|
+
if (rung) {
|
|
415
|
+
state.rereadWarnedAt = rung.threshold;
|
|
416
|
+
report('read_loop', rung.severity, state.rereads);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
handleFrame(fp, false, true);
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
state.sinceNewState += 1;
|
|
423
|
+
noteRepeat(s);
|
|
424
|
+
const effect = typeof s.effect === 'string' ? s.effect : null;
|
|
425
|
+
if (effect === 'no_effect') {
|
|
426
|
+
state.unchanged += 1;
|
|
427
|
+
if (s.treeBlind === true) state.treeBlindActions += 1;
|
|
428
|
+
noProgress();
|
|
429
|
+
} else if (effect !== null) {
|
|
430
|
+
resetNoEffect();
|
|
431
|
+
}
|
|
432
|
+
if (fp !== null) handleFrame(fp, true, effect !== null);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const made = state.emitted;
|
|
436
|
+
if (state.stopped && state.stopped.severity === SEVERITY.STOP) return state.stopped;
|
|
437
|
+
if (made.length === 0) return null;
|
|
438
|
+
const stop = made.find((v) => v.severity === SEVERITY.STOP);
|
|
439
|
+
return stop || made[made.length - 1];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
noteStep,
|
|
444
|
+
level: () => state.level,
|
|
445
|
+
shouldStop: () => state.level === SEVERITY.STOP || state.stopped !== null,
|
|
446
|
+
advice: () => state.lastAdvice,
|
|
447
|
+
reset,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
module.exports = {
|
|
452
|
+
createGuard,
|
|
453
|
+
actionSignature,
|
|
454
|
+
elementsFingerprint,
|
|
455
|
+
fnv1a64,
|
|
456
|
+
stableStringify,
|
|
457
|
+
detectCycle,
|
|
458
|
+
climb,
|
|
459
|
+
lateStop,
|
|
460
|
+
SEVERITY,
|
|
461
|
+
THRESHOLDS,
|
|
462
|
+
DEFAULT_BUDGET,
|
|
463
|
+
LOOP_WINDOW,
|
|
464
|
+
MAX_CYCLE,
|
|
465
|
+
};
|
|
@@ -8,12 +8,26 @@
|
|
|
8
8
|
* 误判 —— 把整屏文字当成这一下的目标,会把「聊天页里恰好有『删除』两个字」判成删除,
|
|
9
9
|
* 那比漏判更烦人(每次点都停下来问)。
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* ## 从 Mika 借来的三条判据(词表与阈值按我们自己的口径重定,没有照抄)
|
|
12
|
+
*
|
|
13
|
+
* 1. **订单状态不算动作。** 词前面紧挨着「待」「已」、或后面跟着状态/入口一类的尾巴
|
|
14
|
+
* (成功、记录、明细、方式、列表、中心、入口、码、售后),这条词就不是一个可点的动作,
|
|
15
|
+
* 而是状态或入口:`待付款`、`付款成功`、`订单记录`、`付款方式` 一律不算。我们只认
|
|
16
|
+
* 「这一下点下去会改世界」的那种写法,宁可漏判一次也不把每个列表页都拦下来。
|
|
17
|
+
* 2. **优惠金额不算应付金额。** 紧挨着 优惠 / 减 / 抵扣 / 运费 / 券 / 红包 / 已省(或前面带
|
|
18
|
+
* 负号)的金额不参与金额核对;否则「满25减3」会被当成「应付 25」。
|
|
19
|
+
* 3. **金额与合计识别单列出来。** `payableAmounts` / `hasTotalLabel` / `TOTAL_LABELS` 给
|
|
20
|
+
* gate 核对「申报的金额在屏上」用;只有合计/实付/应付/总计/总价/小计/总额这类字样,或带
|
|
21
|
+
* 币种/元的数,才算金额。单独一个价格、待遇、区间不是应付金额。
|
|
22
|
+
*
|
|
23
|
+
* 与 Mika 的差别:Mika 的 `IrreversibleDetector.kt` 六百多行,认回车/组合键/横滑/资源 ID/
|
|
24
|
+
* 下拉/拖拽/看图找点等十几种信号;我们只有 tap 的那一条路,其余信号在手机执行器里由
|
|
25
|
+
* `ScreenTree` 压平后本来就拿不到。所以这里只搬规则、不搬那六百行:状态前缀/后缀、优惠排除、
|
|
26
|
+
* 合计识别这三条最容易被误判的补进来,词表仍用我们自己那份(下单、购买、付款、发送、提交、
|
|
27
|
+
* 删除等),阈值(状态尾巴、优惠窗口、英文整词)都按上面重定。
|
|
14
28
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
29
|
+
* 输出的 kind 仍只有 payment / submit / send / delete / none —— 下单、购买、付款这类收钱的
|
|
30
|
+
* 都归 payment(对熔断来说它们是一回事:都花钱)。
|
|
17
31
|
*/
|
|
18
32
|
|
|
19
33
|
/** 只有点屏幕才有「点的是哪个按钮」这回事。 */
|
|
@@ -52,10 +66,39 @@ const LABEL_FIELDS = [
|
|
|
52
66
|
'contentDescription', 'title', 'content', 'value',
|
|
53
67
|
];
|
|
54
68
|
|
|
69
|
+
/** 紧挨在词前面时,说明它是一个状态而不是动作:「待付款」「已付款」。 */
|
|
70
|
+
const STATUS_PREFIXES = new Set(['待', '已']);
|
|
71
|
+
|
|
72
|
+
/** 紧跟在词后面(跳过标点后)时,说明它是状态或入口:「付款成功」「订单记录」「付款方式」。 */
|
|
73
|
+
const STATUS_SUFFIXES = ['成功', '记录', '明细', '方式', '列表', '中心', '入口', '码', '售后'];
|
|
74
|
+
|
|
75
|
+
/** 合计一类的字样:这后面的数才是应付金额。 */
|
|
76
|
+
const TOTAL_LABELS = ['合计', '实付', '应付', '总计', '总价', '小计', '总额', '待付款金额'];
|
|
77
|
+
|
|
78
|
+
/** 紧挨在金额前面的优惠一类字样:这类金额不是应付金额。 */
|
|
79
|
+
const DISCOUNTS = ['优惠', '减', '抵扣', '运费', '券', '红包', '已省'];
|
|
80
|
+
/** 往前看几个字判断是不是优惠金额。 */
|
|
81
|
+
const DISCOUNT_WINDOW = 4;
|
|
82
|
+
|
|
83
|
+
/** 带币种(¥/¥/$/$)或「元」的金额;小数允许 . 或 , 作分隔。 */
|
|
84
|
+
const CURRENCY = /[¥¥$$]\s*(\d+(?:[.,]\d+)?)|(\d+(?:[.,]\d+)?)\s*元/g;
|
|
85
|
+
|
|
55
86
|
function isEnglishWord(word) {
|
|
56
87
|
return /^[a-z]+$/.test(word);
|
|
57
88
|
}
|
|
58
89
|
|
|
90
|
+
/** 去掉标点后的后续文字,用来判状态尾巴。 */
|
|
91
|
+
function afterWord(text, end) {
|
|
92
|
+
return text.slice(end).replace(/^[^\p{L}\p{N}]+/u, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 这一处词是不是一个「动作」,而不是状态/入口(见文件头第 1 条)。 */
|
|
96
|
+
function isActionOccurrence(label, word, index) {
|
|
97
|
+
if (index > 0 && STATUS_PREFIXES.has(label[index - 1])) return false;
|
|
98
|
+
const rest = afterWord(label, index + word.length);
|
|
99
|
+
return !STATUS_SUFFIXES.some((suffix) => rest.startsWith(suffix));
|
|
100
|
+
}
|
|
101
|
+
|
|
59
102
|
/** 在一个文字里找第一个命中的词,返回 {kind, word} 或 null。 */
|
|
60
103
|
function matchLabel(label) {
|
|
61
104
|
const lower = label.toLowerCase();
|
|
@@ -64,8 +107,15 @@ function matchLabel(label) {
|
|
|
64
107
|
if (isEnglishWord(word)) {
|
|
65
108
|
// 英文按整词匹配,避免 order 命中 reorder / preorder。
|
|
66
109
|
if (new RegExp(`\\b${word}\\b`, 'i').test(lower)) return { kind: group.kind, word };
|
|
67
|
-
|
|
68
|
-
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// 中文可能有多个出现位置,逐个判是不是动作(跳过「待付款」「付款成功」这类)。
|
|
113
|
+
let from = 0;
|
|
114
|
+
let index = label.indexOf(word, from);
|
|
115
|
+
while (index !== -1) {
|
|
116
|
+
if (isActionOccurrence(label, word, index)) return { kind: group.kind, word };
|
|
117
|
+
from = index + 1;
|
|
118
|
+
index = label.indexOf(word, from);
|
|
69
119
|
}
|
|
70
120
|
}
|
|
71
121
|
}
|
|
@@ -96,6 +146,33 @@ function none(reason) {
|
|
|
96
146
|
return { irreversible: false, kind: 'none', reason };
|
|
97
147
|
}
|
|
98
148
|
|
|
149
|
+
/**
|
|
150
|
+
* 从一段文字里认出带币种/元的金额(去掉优惠一类、带负号的)。给 gate 核对金额用。
|
|
151
|
+
* @returns {number[]} 数字数组
|
|
152
|
+
*/
|
|
153
|
+
function payableAmounts(text) {
|
|
154
|
+
if (typeof text !== 'string' || !text) return [];
|
|
155
|
+
const out = [];
|
|
156
|
+
CURRENCY.lastIndex = 0;
|
|
157
|
+
let m;
|
|
158
|
+
while ((m = CURRENCY.exec(text)) !== null) {
|
|
159
|
+
const raw = (m[1] || m[2] || '').replace(/,/g, '');
|
|
160
|
+
const value = Number(raw);
|
|
161
|
+
if (!Number.isFinite(value)) continue;
|
|
162
|
+
const before = text.slice(Math.max(0, m.index - DISCOUNT_WINDOW), m.index);
|
|
163
|
+
if (DISCOUNTS.some((w) => before.includes(w))) continue;
|
|
164
|
+
if (before.trimEnd().endsWith('-')) continue;
|
|
165
|
+
out.push(value);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 这段文字里有没有「合计/实付/应付/总计…」这类字样(金额核对是否绑定用它)。 */
|
|
171
|
+
function hasTotalLabel(text) {
|
|
172
|
+
if (typeof text !== 'string' || !text) return false;
|
|
173
|
+
return TOTAL_LABELS.some((label) => text.includes(label));
|
|
174
|
+
}
|
|
175
|
+
|
|
99
176
|
/**
|
|
100
177
|
* judge({ action, args, screenText }) → { irreversible, kind, reason }。
|
|
101
178
|
*
|
|
@@ -140,7 +217,10 @@ function judge(input) {
|
|
|
140
217
|
|
|
141
218
|
module.exports = {
|
|
142
219
|
judge,
|
|
143
|
-
//
|
|
220
|
+
// 下面几个给守卫和 gate 直接用真值表,不重复实现匹配逻辑。
|
|
144
221
|
WORD_TABLE,
|
|
145
222
|
matchLabel,
|
|
223
|
+
payableAmounts,
|
|
224
|
+
hasTotalLabel,
|
|
225
|
+
TOTAL_LABELS,
|
|
146
226
|
};
|