dsh-mobile-plugin 0.5.28 → 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
|
@@ -168,6 +168,43 @@ function betweenDelay() {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* 这一批交给手机侧的节奏参数 `{mode, place}`(见协议 §1.2/§3.3)。
|
|
173
|
+
*
|
|
174
|
+
* mode 来自插件设置的三档(pacing.js 的 get(),默认 standard);place 来自 App 知识里那个 App 的
|
|
175
|
+
* `pace`(careful 的 App 手机侧停更久、按字数读)。手机侧按它把「步间停留 / type 分片」放到批循环里跑,
|
|
176
|
+
* 电脑侧插不进 sleep(批是一次下发、手机侧 for 循环)。拿不到就给空,手机侧按 standard / normal 兜底。
|
|
177
|
+
*/
|
|
178
|
+
function pacingModeOf() {
|
|
179
|
+
if (pacing && typeof pacing.get === 'function') {
|
|
180
|
+
try {
|
|
181
|
+
const m = pacing.get();
|
|
182
|
+
if (typeof m === 'string' && m) return m;
|
|
183
|
+
} catch {
|
|
184
|
+
/* 读不到就走默认 */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return 'standard';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function batchPlaceOf(steps) {
|
|
191
|
+
if (!phoneKnowledge || typeof phoneKnowledge.matchApp !== 'function') return '';
|
|
192
|
+
let pkg = '';
|
|
193
|
+
for (const s of steps) {
|
|
194
|
+
if (s && s.action === 'app.open' && s.args && typeof s.args.packageName === 'string' && s.args.packageName) {
|
|
195
|
+
pkg = s.args.packageName;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!pkg) return '';
|
|
200
|
+
try {
|
|
201
|
+
const doc = phoneKnowledge.matchApp(pkg);
|
|
202
|
+
return doc && typeof doc.pace === 'string' ? doc.pace : '';
|
|
203
|
+
} catch {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
171
208
|
/** 这台手机报没报某项能力;拿不到能力接口时按「支持」放过,别凭空白挡一条路径。 */
|
|
172
209
|
function supports(service, deviceId, cap) {
|
|
173
210
|
if (!service || typeof service.hasCapability !== 'function') return true;
|
|
@@ -401,6 +438,7 @@ async function run(service, deviceId, sessionId, action, args, signal) {
|
|
|
401
438
|
if (code === 'offline') throw withCode('那台手机在这期间断开了连接,这一步没有做。');
|
|
402
439
|
if (code === 'timeout') throw withCode(`那台手机 ${message}。可以再试一次,或者先 phone_screen 看看它现在停在哪。`);
|
|
403
440
|
if (code === 'unsupported') throw withCode(`那台手机上装的 App 版本还不支持 ${action},需要把手机上的 App 升级到最新。`);
|
|
441
|
+
if (code === 'not-supported') throw withCode('这台手机上没有一个 App 能接这个动作,退回操作界面去做吧。');
|
|
404
442
|
if (code === 'adb-unavailable') throw withCode('那台手机的无线调试没连上(可能被系统关了),让用户在手机上重新打开无线调试。');
|
|
405
443
|
if (code === 'warming') throw withCode('手机正在准备后台屏,大约还要几秒,稍等再试。');
|
|
406
444
|
if (code === 'aborted') throw withCode('这一步被取消了。');
|
|
@@ -1308,6 +1346,8 @@ function installPhoneTools(ctx, deps) {
|
|
|
1308
1346
|
const batchArgs = {
|
|
1309
1347
|
steps: runnable.map((s) => ({ action: s.action, args: s.args, ...(s.until ? { until: s.until } : {}) })),
|
|
1310
1348
|
quiet: args.quiet !== false,
|
|
1349
|
+
// 节奏跟着批一起下去:mode 来自设置三档,place 来自 App 知识;手机侧在批循环里消费(协议 §1.2 / §3.3)。
|
|
1350
|
+
pacing: { mode: pacingModeOf(), place: batchPlaceOf(runnable) },
|
|
1311
1351
|
};
|
|
1312
1352
|
const data = await run(service, deviceId, sessionId, 'batch', batchArgs, exec.signal);
|
|
1313
1353
|
finalData = data;
|
|
@@ -1640,6 +1680,555 @@ function installPhoneTools(ctx, deps) {
|
|
|
1640
1680
|
},
|
|
1641
1681
|
});
|
|
1642
1682
|
|
|
1683
|
+
// ── 结构化:提取 / 查询 / 查找 / 采集 / 滚轮 ──────────────────────────────
|
|
1684
|
+
|
|
1685
|
+
register({
|
|
1686
|
+
name: 'phone_extract',
|
|
1687
|
+
description: 'Extract structured data from the current screen in one read on the phone. mode=text pulls visible text; mode=list groups a scrollable list/recycler into rows with bounds and parents; mode=form returns only editable fields (secret fields show a boolean, never the value). It reads the accessibility tree on the phone, so list rows beyond the first 160 elements come back with bounds but no tappable ref (best-effort).',
|
|
1688
|
+
parameters: {
|
|
1689
|
+
mode: { type: 'string', enum: ['text', 'list', 'form'], description: 'What to extract: text, list rows, or form fields.' },
|
|
1690
|
+
maxItems: { type: 'integer', description: 'Maximum records, 1-200 (default 100).' },
|
|
1691
|
+
},
|
|
1692
|
+
output: {
|
|
1693
|
+
schema: {
|
|
1694
|
+
type: 'object',
|
|
1695
|
+
additionalProperties: false,
|
|
1696
|
+
properties: {
|
|
1697
|
+
ok: { type: 'boolean' },
|
|
1698
|
+
mode: { type: 'string' },
|
|
1699
|
+
items: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
1700
|
+
count: { type: 'number' },
|
|
1701
|
+
truncated: { type: 'boolean' },
|
|
1702
|
+
packageName: { type: 'string' },
|
|
1703
|
+
revision: { type: 'number' },
|
|
1704
|
+
note: { type: 'string' },
|
|
1705
|
+
},
|
|
1706
|
+
},
|
|
1707
|
+
render: (_args, value) => [{ type: 'text', text: `提取了 ${value.count || 0} 条(${value.mode || ''})${value.truncated ? ',已截断' : ''}。` }],
|
|
1708
|
+
},
|
|
1709
|
+
execute: async (args, exec) => {
|
|
1710
|
+
const sessionId = sessionOf(exec);
|
|
1711
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
1712
|
+
const mode = ['text', 'list', 'form'].includes(args.mode) ? args.mode : 'text';
|
|
1713
|
+
const maxItems = Number.isInteger(args.maxItems) ? Math.min(200, Math.max(1, args.maxItems)) : 100;
|
|
1714
|
+
const data = await run(service, deviceId, sessionId, 'extract', { mode, maxItems }, exec.signal);
|
|
1715
|
+
const notes = [mentionMismatch(sessionId, deviceId)];
|
|
1716
|
+
if (!supports(service, deviceId, 'extract')) notes.push(unsupportedNote('结构化提取(extract)'));
|
|
1717
|
+
const value = {
|
|
1718
|
+
ok: true,
|
|
1719
|
+
mode,
|
|
1720
|
+
items: Array.isArray(data && data.items) ? data.items : [],
|
|
1721
|
+
count: Number.isFinite(data && data.count) ? data.count : (Array.isArray(data && data.items) ? data.items.length : 0),
|
|
1722
|
+
truncated: Boolean(data && data.truncated),
|
|
1723
|
+
};
|
|
1724
|
+
if (data && typeof data.packageName === 'string' && data.packageName) value.packageName = textOf(data.packageName, MAX_TEXT);
|
|
1725
|
+
if (data && Number.isFinite(data.revision)) value.revision = data.revision;
|
|
1726
|
+
const note = notes.filter(Boolean).join(' ');
|
|
1727
|
+
if (note) value.note = note;
|
|
1728
|
+
return value;
|
|
1729
|
+
},
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
register({
|
|
1733
|
+
name: 'phone_query',
|
|
1734
|
+
description: 'Find controls on the current screen by text, resource id, or role without scrolling, returning ref, bounds, parents and whether each is clickable/editable. Use it when the same text appears many places or a control has no visible label. limit up to 100 (default 30).',
|
|
1735
|
+
parameters: {
|
|
1736
|
+
text: { type: 'string', description: 'Case-insensitive text or accessibility description to find.' },
|
|
1737
|
+
resourceId: { type: 'string', description: 'Full or partial Android resource id to find.' },
|
|
1738
|
+
role: { type: 'string', description: 'Full or partial role/class to find.' },
|
|
1739
|
+
limit: { type: 'integer', description: 'Maximum results, 1-100 (default 30).' },
|
|
1740
|
+
},
|
|
1741
|
+
output: {
|
|
1742
|
+
schema: {
|
|
1743
|
+
type: 'object',
|
|
1744
|
+
additionalProperties: false,
|
|
1745
|
+
properties: {
|
|
1746
|
+
ok: { type: 'boolean' },
|
|
1747
|
+
total: { type: 'number' },
|
|
1748
|
+
truncated: { type: 'boolean' },
|
|
1749
|
+
matches: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
1750
|
+
packageName: { type: 'string' },
|
|
1751
|
+
revision: { type: 'number' },
|
|
1752
|
+
note: { type: 'string' },
|
|
1753
|
+
},
|
|
1754
|
+
},
|
|
1755
|
+
render: (_args, value) => [{ type: 'text', text: `找到 ${value.total || 0} 个匹配(返回 ${Array.isArray(value.matches) ? value.matches.length : 0} 条)。` }],
|
|
1756
|
+
},
|
|
1757
|
+
execute: async (args, exec) => {
|
|
1758
|
+
const sessionId = sessionOf(exec);
|
|
1759
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
1760
|
+
const queryArgs = {};
|
|
1761
|
+
if (typeof args.text === 'string' && args.text.trim()) queryArgs.text = textOf(args.text, 200);
|
|
1762
|
+
if (typeof args.resourceId === 'string' && args.resourceId.trim()) queryArgs.resourceId = textOf(args.resourceId, 200);
|
|
1763
|
+
if (typeof args.role === 'string' && args.role.trim()) queryArgs.role = textOf(args.role, 100);
|
|
1764
|
+
if (!queryArgs.text && !queryArgs.resourceId && !queryArgs.role) {
|
|
1765
|
+
throw new Error('要查什么?给 text / resourceId / role 其中至少一个。');
|
|
1766
|
+
}
|
|
1767
|
+
queryArgs.limit = Number.isInteger(args.limit) ? Math.min(100, Math.max(1, args.limit)) : 30;
|
|
1768
|
+
const data = await run(service, deviceId, sessionId, 'query', queryArgs, exec.signal);
|
|
1769
|
+
const notes = [mentionMismatch(sessionId, deviceId)];
|
|
1770
|
+
if (!supports(service, deviceId, 'query')) notes.push(unsupportedNote('按字段查询(query)'));
|
|
1771
|
+
const value = {
|
|
1772
|
+
ok: true,
|
|
1773
|
+
total: Number.isFinite(data && data.total) ? data.total : 0,
|
|
1774
|
+
truncated: Boolean(data && data.truncated),
|
|
1775
|
+
matches: Array.isArray(data && data.matches) ? data.matches : [],
|
|
1776
|
+
};
|
|
1777
|
+
if (data && typeof data.packageName === 'string' && data.packageName) value.packageName = textOf(data.packageName, MAX_TEXT);
|
|
1778
|
+
if (data && Number.isFinite(data.revision)) value.revision = data.revision;
|
|
1779
|
+
const note = notes.filter(Boolean).join(' ');
|
|
1780
|
+
if (note) value.note = note;
|
|
1781
|
+
return value;
|
|
1782
|
+
},
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
register({
|
|
1786
|
+
name: 'phone_find',
|
|
1787
|
+
description: 'Scroll on the phone until an element matching text/resourceId/role is visible, then return its ref. It reads, matches, swipes and waits for the screen to settle in a loop on the phone. Stopped reason is one of found / screen_needs_review / app_switched / content_unchanged / scroll_limit / read_failed. maxScrolls 1-200 (default 30).',
|
|
1788
|
+
parameters: {
|
|
1789
|
+
text: { type: 'string', description: 'Text or accessibility description to find.' },
|
|
1790
|
+
resourceId: { type: 'string', description: 'Full or partial Android resource id.' },
|
|
1791
|
+
role: { type: 'string', description: 'Full or partial role/class.' },
|
|
1792
|
+
direction: { type: 'string', enum: ['up', 'down'], description: 'Where the target is: down means scroll down to see lower content (default down).' },
|
|
1793
|
+
maxScrolls: { type: 'integer', description: 'Maximum scrolls, 1-200 (default 30).' },
|
|
1794
|
+
},
|
|
1795
|
+
output: {
|
|
1796
|
+
schema: {
|
|
1797
|
+
type: 'object',
|
|
1798
|
+
additionalProperties: false,
|
|
1799
|
+
properties: {
|
|
1800
|
+
ok: { type: 'boolean' },
|
|
1801
|
+
found: { type: 'boolean' },
|
|
1802
|
+
stoppedBecause: { type: 'string' },
|
|
1803
|
+
scrolls: { type: 'number' },
|
|
1804
|
+
match: { type: 'object', additionalProperties: true },
|
|
1805
|
+
packageName: { type: 'string' },
|
|
1806
|
+
revision: { type: 'number' },
|
|
1807
|
+
note: { type: 'string' },
|
|
1808
|
+
},
|
|
1809
|
+
},
|
|
1810
|
+
render: (_args, value) => [{ type: 'text', text: value.found ? '找到了。' : `没找到(${value.stoppedBecause || '未知'},滚了 ${value.scrolls || 0} 次)。` }],
|
|
1811
|
+
},
|
|
1812
|
+
execute: async (args, exec) => {
|
|
1813
|
+
const sessionId = sessionOf(exec);
|
|
1814
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
1815
|
+
const findArgs = { direction: args.direction === 'up' ? 'up' : 'down' };
|
|
1816
|
+
if (typeof args.text === 'string' && args.text.trim()) findArgs.text = textOf(args.text, 200);
|
|
1817
|
+
if (typeof args.resourceId === 'string' && args.resourceId.trim()) findArgs.resourceId = textOf(args.resourceId, 200);
|
|
1818
|
+
if (typeof args.role === 'string' && args.role.trim()) findArgs.role = textOf(args.role, 100);
|
|
1819
|
+
if (!findArgs.text && !findArgs.resourceId && !findArgs.role) throw new Error('要找什么?给 text / resourceId / role 其中至少一个。');
|
|
1820
|
+
findArgs.maxScrolls = Number.isInteger(args.maxScrolls) ? Math.min(200, Math.max(1, args.maxScrolls)) : 30;
|
|
1821
|
+
const data = await run(service, deviceId, sessionId, 'find', findArgs, exec.signal);
|
|
1822
|
+
const notes = [mentionMismatch(sessionId, deviceId)];
|
|
1823
|
+
if (!supports(service, deviceId, 'find')) notes.push(unsupportedNote('滚动查找(find)'));
|
|
1824
|
+
const value = {
|
|
1825
|
+
ok: Boolean(data && data.found),
|
|
1826
|
+
found: Boolean(data && data.found),
|
|
1827
|
+
stoppedBecause: textOf(data && data.stoppedBecause, 40) || 'scroll_limit',
|
|
1828
|
+
scrolls: Number.isFinite(data && data.scrolls) ? data.scrolls : 0,
|
|
1829
|
+
};
|
|
1830
|
+
if (data && data.match && typeof data.match === 'object') value.match = data.match;
|
|
1831
|
+
if (data && typeof data.packageName === 'string' && data.packageName) value.packageName = textOf(data.packageName, MAX_TEXT);
|
|
1832
|
+
if (data && Number.isFinite(data.revision)) value.revision = data.revision;
|
|
1833
|
+
if (!value.found) notes.push(`滚了 ${value.scrolls} 次也没找到(${value.stoppedBecause}),如实说没找到,别猜它在哪。`);
|
|
1834
|
+
const note = notes.filter(Boolean).join(' ');
|
|
1835
|
+
if (note) value.note = note;
|
|
1836
|
+
return value;
|
|
1837
|
+
},
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
register({
|
|
1841
|
+
name: 'phone_collect',
|
|
1842
|
+
description: 'Scroll 1-30 screens and merge the text across them with de-duplication on the phone, returning one merged list. Use it to read a long list or chat without one round-trip per screen. Has both a maxItems and a 50KB byte cap.',
|
|
1843
|
+
parameters: {
|
|
1844
|
+
direction: { type: 'string', enum: ['up', 'down'], description: 'Direction of newer content: up or down (default down).' },
|
|
1845
|
+
pages: { type: 'integer', description: 'Maximum screens to inspect, 1-30 (default 10).' },
|
|
1846
|
+
maxItems: { type: 'integer', description: 'Maximum lines to return, 1-1000 (default 300).' },
|
|
1847
|
+
fromY: { type: 'integer', description: 'Top of the content area, normalized 0-999 (default 50).' },
|
|
1848
|
+
toY: { type: 'integer', description: 'Bottom of the content area, normalized 0-999 (default 950).' },
|
|
1849
|
+
},
|
|
1850
|
+
output: {
|
|
1851
|
+
schema: {
|
|
1852
|
+
type: 'object',
|
|
1853
|
+
additionalProperties: false,
|
|
1854
|
+
properties: {
|
|
1855
|
+
ok: { type: 'boolean' },
|
|
1856
|
+
pagesInspected: { type: 'number' },
|
|
1857
|
+
scrolls: { type: 'number' },
|
|
1858
|
+
stoppedBecause: { type: 'string' },
|
|
1859
|
+
count: { type: 'number' },
|
|
1860
|
+
items: { type: 'array', items: { type: 'string' } },
|
|
1861
|
+
text: { type: 'string' },
|
|
1862
|
+
truncated: { type: 'boolean' },
|
|
1863
|
+
currentApp: { type: 'string' },
|
|
1864
|
+
note: { type: 'string' },
|
|
1865
|
+
},
|
|
1866
|
+
},
|
|
1867
|
+
render: (_args, value) => [{ type: 'text', text: `采集了 ${value.pagesInspected || 0} 屏,合并 ${value.count || 0} 行(${value.stoppedBecause || ''})${value.truncated ? ',已截断' : ''}。` }],
|
|
1868
|
+
},
|
|
1869
|
+
execute: async (args, exec) => {
|
|
1870
|
+
const sessionId = sessionOf(exec);
|
|
1871
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
1872
|
+
const collectArgs = {
|
|
1873
|
+
direction: args.direction === 'up' ? 'up' : 'down',
|
|
1874
|
+
pages: Number.isInteger(args.pages) ? Math.min(30, Math.max(1, args.pages)) : 10,
|
|
1875
|
+
maxItems: Number.isInteger(args.maxItems) ? Math.min(1000, Math.max(1, args.maxItems)) : 300,
|
|
1876
|
+
fromY: Number.isInteger(args.fromY) ? Math.min(999, Math.max(0, args.fromY)) : 50,
|
|
1877
|
+
toY: Number.isInteger(args.toY) ? Math.min(999, Math.max(0, args.toY)) : 950,
|
|
1878
|
+
};
|
|
1879
|
+
const data = await run(service, deviceId, sessionId, 'collect', collectArgs, exec.signal);
|
|
1880
|
+
const notes = [mentionMismatch(sessionId, deviceId)];
|
|
1881
|
+
if (!supports(service, deviceId, 'collect')) notes.push(unsupportedNote('多屏采集(collect)'));
|
|
1882
|
+
const items = Array.isArray(data && data.items) ? data.items.map((x) => String(x)) : [];
|
|
1883
|
+
const value = {
|
|
1884
|
+
ok: Boolean(data && data.ok !== false),
|
|
1885
|
+
pagesInspected: Number.isFinite(data && data.pagesInspected) ? data.pagesInspected : 0,
|
|
1886
|
+
scrolls: Number.isFinite(data && data.scrolls) ? data.scrolls : 0,
|
|
1887
|
+
stoppedBecause: textOf(data && data.stoppedBecause, 40) || 'page_limit',
|
|
1888
|
+
count: Number.isFinite(data && data.count) ? data.count : items.length,
|
|
1889
|
+
items,
|
|
1890
|
+
text: textOf(data && data.text, 50_000) || items.join('\n'),
|
|
1891
|
+
truncated: Boolean(data && data.truncated),
|
|
1892
|
+
};
|
|
1893
|
+
if (data && typeof data.currentApp === 'string' && data.currentApp) value.currentApp = textOf(data.currentApp, MAX_TEXT);
|
|
1894
|
+
const note = notes.filter(Boolean).join(' ');
|
|
1895
|
+
if (note) value.note = note;
|
|
1896
|
+
return value;
|
|
1897
|
+
},
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
register({
|
|
1901
|
+
name: 'phone_wheel',
|
|
1902
|
+
description: 'Turn a wheel picker (time / date / region) straight to a value in one call. The phone reads the column, scrolls or drags, and reads back to verify. Give ref (from phone_screen) and value. Error codes: not_a_wheel, wheel_at_end, wheel_unreadable, wheel_not_moving.',
|
|
1903
|
+
parameters: {
|
|
1904
|
+
ref: { type: 'string', required: true, description: 'Element ref of the wheel column (from phone_screen).' },
|
|
1905
|
+
value: { type: 'string', required: true, description: 'The value to reach, e.g. "14", "30", "November".' },
|
|
1906
|
+
},
|
|
1907
|
+
output: {
|
|
1908
|
+
schema: {
|
|
1909
|
+
type: 'object',
|
|
1910
|
+
additionalProperties: false,
|
|
1911
|
+
properties: {
|
|
1912
|
+
ok: { type: 'boolean' },
|
|
1913
|
+
reached: { type: 'boolean' },
|
|
1914
|
+
target: { type: 'string' },
|
|
1915
|
+
value: { type: 'string' },
|
|
1916
|
+
from: { type: 'string' },
|
|
1917
|
+
attempts: { type: 'number' },
|
|
1918
|
+
method: { type: 'string' },
|
|
1919
|
+
error: { type: 'object', additionalProperties: true },
|
|
1920
|
+
effect: { type: 'string' },
|
|
1921
|
+
packageName: { type: 'string' },
|
|
1922
|
+
elements: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
1923
|
+
note: { type: 'string' },
|
|
1924
|
+
},
|
|
1925
|
+
},
|
|
1926
|
+
render: (_args, value) => [{ type: 'text', text: value.reached ? `已转到 ${value.value || value.target}。` : `没转到位:${value.error ? value.error.code : '未知'}。` }],
|
|
1927
|
+
},
|
|
1928
|
+
execute: async (args, exec) => {
|
|
1929
|
+
const sessionId = sessionOf(exec);
|
|
1930
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
1931
|
+
const ref = textOf(args.ref, 40);
|
|
1932
|
+
const target = textOf(args.value, 100);
|
|
1933
|
+
if (!ref) throw new Error('给滚轮那一列的 ref(phone_screen 里的编号)。');
|
|
1934
|
+
if (!target) throw new Error('要转到哪个值?给 value。');
|
|
1935
|
+
const data = await run(service, deviceId, sessionId, 'wheel.set', { ref, value: target }, exec.signal);
|
|
1936
|
+
const notes = [mentionMismatch(sessionId, deviceId)];
|
|
1937
|
+
if (!supports(service, deviceId, 'wheel.set')) notes.push(unsupportedNote('滚轮选择器(wheel.set)'));
|
|
1938
|
+
const value = {
|
|
1939
|
+
ok: Boolean(data && data.reached),
|
|
1940
|
+
reached: Boolean(data && data.reached),
|
|
1941
|
+
target,
|
|
1942
|
+
attempts: Number.isFinite(data && data.attempts) ? data.attempts : 0,
|
|
1943
|
+
method: textOf(data && data.method, 20) || 'none',
|
|
1944
|
+
};
|
|
1945
|
+
if (data && typeof data.value === 'string') value.value = data.value;
|
|
1946
|
+
if (data && typeof data.from === 'string') value.from = data.from;
|
|
1947
|
+
if (data && data.error && typeof data.error === 'object') value.error = data.error;
|
|
1948
|
+
if (data && typeof data.effect === 'string') value.effect = data.effect;
|
|
1949
|
+
if (data && typeof data.packageName === 'string' && data.packageName) value.packageName = textOf(data.packageName, MAX_TEXT);
|
|
1950
|
+
if (data && Array.isArray(data.elements)) value.elements = data.elements.slice(0, 120);
|
|
1951
|
+
const note = notes.filter(Boolean).join(' ');
|
|
1952
|
+
if (note) value.note = note;
|
|
1953
|
+
return value;
|
|
1954
|
+
},
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
// ── 分享文字 / 系统动作 / App 去留 ────────────────────────────────────────
|
|
1958
|
+
|
|
1959
|
+
register({
|
|
1960
|
+
name: 'phone_share_text',
|
|
1961
|
+
description: 'Share a piece of text to an app via ACTION_SEND (text/plain), landing on that app\'s share page. Parameters are only the package and the text.',
|
|
1962
|
+
parameters: {
|
|
1963
|
+
packageName: { type: 'string', required: true, description: 'Android package name of the app to share into.' },
|
|
1964
|
+
text: { type: 'string', required: true, description: 'The text to share.' },
|
|
1965
|
+
},
|
|
1966
|
+
output: {
|
|
1967
|
+
schema: {
|
|
1968
|
+
type: 'object',
|
|
1969
|
+
additionalProperties: false,
|
|
1970
|
+
properties: {
|
|
1971
|
+
ok: { type: 'boolean' },
|
|
1972
|
+
packageName: { type: 'string' },
|
|
1973
|
+
handledBy: { type: 'string' },
|
|
1974
|
+
effect: { type: 'string' },
|
|
1975
|
+
elements: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
1976
|
+
note: { type: 'string' },
|
|
1977
|
+
},
|
|
1978
|
+
},
|
|
1979
|
+
render: (_args, value) => [{ type: 'text', text: `已把文字分享到 ${value.packageName || '那个 App'}。` }],
|
|
1980
|
+
},
|
|
1981
|
+
execute: async (args, exec) => {
|
|
1982
|
+
const sessionId = sessionOf(exec);
|
|
1983
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
1984
|
+
const pkg = textOf(args.packageName, 200);
|
|
1985
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$/.test(pkg)) throw new Error('packageName 看起来不像安卓包名。');
|
|
1986
|
+
const body = textOf(args.text, 20_000);
|
|
1987
|
+
if (!body) throw new Error('要分享的文字是空的。');
|
|
1988
|
+
const data = await run(service, deviceId, sessionId, 'share.text', { packageName: pkg, text: body }, exec.signal);
|
|
1989
|
+
const notes = [mentionMismatch(sessionId, deviceId)];
|
|
1990
|
+
if (!supports(service, deviceId, 'share.text')) notes.push(unsupportedNote('分享文字(share.text)'));
|
|
1991
|
+
const value = {
|
|
1992
|
+
ok: true,
|
|
1993
|
+
packageName: pkg,
|
|
1994
|
+
handledBy: textOf(data && data.share && data.share.handledBy, 200) || pkg,
|
|
1995
|
+
};
|
|
1996
|
+
if (data && typeof data.effect === 'string') value.effect = data.effect;
|
|
1997
|
+
if (data && Array.isArray(data.elements)) value.elements = data.elements.slice(0, 120);
|
|
1998
|
+
const note = notes.filter(Boolean).join(' ');
|
|
1999
|
+
if (note) value.note = note;
|
|
2000
|
+
return value;
|
|
2001
|
+
},
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
const weekdayNames = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
|
|
2005
|
+
const systemNotes = (sessionId, deviceId, cap) => {
|
|
2006
|
+
const notes = [mentionMismatch(sessionId, deviceId)];
|
|
2007
|
+
if (!supports(service, deviceId, cap)) notes.push(unsupportedNote('这类系统动作'));
|
|
2008
|
+
return notes;
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
register({
|
|
2012
|
+
name: 'phone_set_alarm',
|
|
2013
|
+
description: 'Set an alarm through the standard Android clock interface without touching its UI. If no clock app handles it, returns not-supported and you should fall back to operating the UI.',
|
|
2014
|
+
parameters: {
|
|
2015
|
+
hour: { type: 'integer', required: true, description: 'Hour of day, 0-23.' },
|
|
2016
|
+
minute: { type: 'integer', required: true, description: 'Minute, 0-59.' },
|
|
2017
|
+
label: { type: 'string', description: 'Optional label shown with the alarm.' },
|
|
2018
|
+
days: { type: 'array', items: { type: 'string' }, description: 'Optional repeat days: MON..SUN. Omit for a one-time alarm.' },
|
|
2019
|
+
vibrate: { type: 'boolean', description: 'Optional: vibrate when it rings.' },
|
|
2020
|
+
},
|
|
2021
|
+
output: {
|
|
2022
|
+
schema: {
|
|
2023
|
+
type: 'object',
|
|
2024
|
+
additionalProperties: false,
|
|
2025
|
+
properties: {
|
|
2026
|
+
ok: { type: 'boolean' },
|
|
2027
|
+
requested: { type: 'object', additionalProperties: true },
|
|
2028
|
+
handledBy: { type: 'string' },
|
|
2029
|
+
note: { type: 'string' },
|
|
2030
|
+
},
|
|
2031
|
+
},
|
|
2032
|
+
render: (_args, value) => [{ type: 'text', text: `已把闹钟交给 ${value.handledBy || '时钟 App'}。` }],
|
|
2033
|
+
},
|
|
2034
|
+
execute: async (args, exec) => {
|
|
2035
|
+
const sessionId = sessionOf(exec);
|
|
2036
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
2037
|
+
if (!Number.isInteger(args.hour) || args.hour < 0 || args.hour > 23) throw new Error('hour 要是 0-23 的整数。');
|
|
2038
|
+
if (!Number.isInteger(args.minute) || args.minute < 0 || args.minute > 59) throw new Error('minute 要是 0-59 的整数。');
|
|
2039
|
+
const payload = { hour: args.hour, minute: args.minute };
|
|
2040
|
+
if (typeof args.label === 'string' && args.label.trim()) payload.label = textOf(args.label, 100);
|
|
2041
|
+
if (Array.isArray(args.days) && args.days.length > 0) {
|
|
2042
|
+
const days = args.days.map((d) => {
|
|
2043
|
+
const idx = weekdayNames.indexOf(String(d).trim().toUpperCase().slice(0, 3));
|
|
2044
|
+
if (idx < 0) throw new Error(`days 里只能是 ${weekdayNames.join(' / ')}。`);
|
|
2045
|
+
return idx + 1;
|
|
2046
|
+
});
|
|
2047
|
+
payload.days = [...new Set(days)].sort((a, b) => a - b);
|
|
2048
|
+
}
|
|
2049
|
+
if (args.vibrate === true) payload.vibrate = true;
|
|
2050
|
+
const data = await run(service, deviceId, sessionId, 'system.alarm', payload, exec.signal);
|
|
2051
|
+
const value = { ok: true, requested: { time: `${String(args.hour).padStart(2, '0')}:${String(args.minute).padStart(2, '0')}` } };
|
|
2052
|
+
if (payload.label) value.requested.label = payload.label;
|
|
2053
|
+
const handledBy = textOf(data && data.system && data.system.handledBy, 200);
|
|
2054
|
+
if (handledBy) value.handledBy = handledBy;
|
|
2055
|
+
const note = systemNotes(sessionId, deviceId, 'system.alarm').filter(Boolean).join(' ');
|
|
2056
|
+
if (note) value.note = note;
|
|
2057
|
+
return value;
|
|
2058
|
+
},
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
register({
|
|
2062
|
+
name: 'phone_set_timer',
|
|
2063
|
+
description: 'Start a timer through the standard Android clock interface without touching its UI. If no clock app handles it, returns not-supported and you should fall back to operating the UI.',
|
|
2064
|
+
parameters: {
|
|
2065
|
+
seconds: { type: 'integer', required: true, description: 'Length of the timer in seconds, 1-86400.' },
|
|
2066
|
+
label: { type: 'string', description: 'Optional label shown with the timer.' },
|
|
2067
|
+
},
|
|
2068
|
+
output: {
|
|
2069
|
+
schema: {
|
|
2070
|
+
type: 'object',
|
|
2071
|
+
additionalProperties: false,
|
|
2072
|
+
properties: {
|
|
2073
|
+
ok: { type: 'boolean' },
|
|
2074
|
+
requested: { type: 'object', additionalProperties: true },
|
|
2075
|
+
handledBy: { type: 'string' },
|
|
2076
|
+
note: { type: 'string' },
|
|
2077
|
+
},
|
|
2078
|
+
},
|
|
2079
|
+
render: (_args, value) => [{ type: 'text', text: `已把计时器交给 ${value.handledBy || '时钟 App'}。` }],
|
|
2080
|
+
},
|
|
2081
|
+
execute: async (args, exec) => {
|
|
2082
|
+
const sessionId = sessionOf(exec);
|
|
2083
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
2084
|
+
if (!Number.isInteger(args.seconds) || args.seconds < 1 || args.seconds > 86400) throw new Error('seconds 要是 1-86400 的整数。');
|
|
2085
|
+
const payload = { seconds: args.seconds };
|
|
2086
|
+
if (typeof args.label === 'string' && args.label.trim()) payload.label = textOf(args.label, 100);
|
|
2087
|
+
const data = await run(service, deviceId, sessionId, 'system.timer', payload, exec.signal);
|
|
2088
|
+
const value = { ok: true, requested: { seconds: args.seconds } };
|
|
2089
|
+
if (payload.label) value.requested.label = payload.label;
|
|
2090
|
+
const handledBy = textOf(data && data.system && data.system.handledBy, 200);
|
|
2091
|
+
if (handledBy) value.handledBy = handledBy;
|
|
2092
|
+
const note = systemNotes(sessionId, deviceId, 'system.timer').filter(Boolean).join(' ');
|
|
2093
|
+
if (note) value.note = note;
|
|
2094
|
+
return value;
|
|
2095
|
+
},
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
register({
|
|
2099
|
+
name: 'phone_show_alarms',
|
|
2100
|
+
description: 'Open the clock app\'s alarm list on the phone so alarms can be inspected or edited there. If no clock app handles it, returns not-supported.',
|
|
2101
|
+
parameters: {},
|
|
2102
|
+
output: {
|
|
2103
|
+
schema: {
|
|
2104
|
+
type: 'object',
|
|
2105
|
+
additionalProperties: false,
|
|
2106
|
+
properties: {
|
|
2107
|
+
ok: { type: 'boolean' },
|
|
2108
|
+
handledBy: { type: 'string' },
|
|
2109
|
+
effect: { type: 'string' },
|
|
2110
|
+
elements: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
2111
|
+
note: { type: 'string' },
|
|
2112
|
+
},
|
|
2113
|
+
},
|
|
2114
|
+
render: (_args, value) => [{ type: 'text', text: `已打开闹钟列表(${value.handledBy || '时钟 App'})。` }],
|
|
2115
|
+
},
|
|
2116
|
+
execute: async (args, exec) => {
|
|
2117
|
+
const sessionId = sessionOf(exec);
|
|
2118
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
2119
|
+
const data = await run(service, deviceId, sessionId, 'system.show_alarms', {}, exec.signal);
|
|
2120
|
+
const value = { ok: true };
|
|
2121
|
+
const handledBy = textOf(data && data.system && data.system.handledBy, 200);
|
|
2122
|
+
if (handledBy) value.handledBy = handledBy;
|
|
2123
|
+
if (data && typeof data.effect === 'string') value.effect = data.effect;
|
|
2124
|
+
if (data && Array.isArray(data.elements)) value.elements = data.elements.slice(0, 120);
|
|
2125
|
+
const note = systemNotes(sessionId, deviceId, 'system.show_alarms').filter(Boolean).join(' ');
|
|
2126
|
+
if (note) value.note = note;
|
|
2127
|
+
return value;
|
|
2128
|
+
},
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
register({
|
|
2132
|
+
name: 'phone_add_calendar_event',
|
|
2133
|
+
description: 'Open a pre-filled new-event page in the calendar app (nothing is saved until you tap save there). Standard Android Intent, no calendar permission needed. If no calendar app handles it, returns not-supported.',
|
|
2134
|
+
parameters: {
|
|
2135
|
+
title: { type: 'string', required: true, description: 'Event title.' },
|
|
2136
|
+
start: { type: 'string', required: true, description: 'Start as local date-time yyyy-MM-ddTHH:mm, or a date yyyy-MM-dd with allDay=true.' },
|
|
2137
|
+
end: { type: 'string', description: 'Optional end, same format as start. Defaults to one hour after start (one day for all-day).' },
|
|
2138
|
+
allDay: { type: 'boolean', description: 'All-day event (default false).' },
|
|
2139
|
+
location: { type: 'string', description: 'Optional location.' },
|
|
2140
|
+
description: { type: 'string', description: 'Optional notes.' },
|
|
2141
|
+
},
|
|
2142
|
+
output: {
|
|
2143
|
+
schema: {
|
|
2144
|
+
type: 'object',
|
|
2145
|
+
additionalProperties: false,
|
|
2146
|
+
properties: {
|
|
2147
|
+
ok: { type: 'boolean' },
|
|
2148
|
+
requested: { type: 'object', additionalProperties: true },
|
|
2149
|
+
handledBy: { type: 'string' },
|
|
2150
|
+
effect: { type: 'string' },
|
|
2151
|
+
elements: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
2152
|
+
note: { type: 'string' },
|
|
2153
|
+
},
|
|
2154
|
+
},
|
|
2155
|
+
render: (_args, value) => [{ type: 'text', text: `已打开新日程页(${value.handledBy || '日历 App'}),还没保存。` }],
|
|
2156
|
+
},
|
|
2157
|
+
execute: async (args, exec) => {
|
|
2158
|
+
const sessionId = sessionOf(exec);
|
|
2159
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
2160
|
+
const title = textOf(args.title, 200);
|
|
2161
|
+
if (!title) throw new Error('日程要有 title。');
|
|
2162
|
+
const allDay = args.allDay === true;
|
|
2163
|
+
const parseLocal = (raw) => {
|
|
2164
|
+
let s = String(raw || '').trim();
|
|
2165
|
+
if (!s) return null;
|
|
2166
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) s += allDay ? 'T00:00' : 'T09:00';
|
|
2167
|
+
const ms = Date.parse(s);
|
|
2168
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2169
|
+
};
|
|
2170
|
+
const beginMs = parseLocal(args.start);
|
|
2171
|
+
if (beginMs === null) throw new Error('start 要是 yyyy-MM-ddTHH:mm 或 yyyy-MM-dd。');
|
|
2172
|
+
let endMs = parseLocal(args.end);
|
|
2173
|
+
if (endMs === null || endMs <= beginMs) endMs = beginMs + (allDay ? 24 * 3_600_000 : 3_600_000);
|
|
2174
|
+
const payload = { title, beginMs, endMs, allDay };
|
|
2175
|
+
if (typeof args.location === 'string' && args.location.trim()) payload.location = textOf(args.location, 200);
|
|
2176
|
+
if (typeof args.description === 'string' && args.description.trim()) payload.description = textOf(args.description, 2000);
|
|
2177
|
+
const data = await run(service, deviceId, sessionId, 'system.calendar_insert', payload, exec.signal);
|
|
2178
|
+
const value = { ok: true, requested: { title, start: new Date(beginMs).toISOString().slice(0, 16).replace('T', ' ') } };
|
|
2179
|
+
if (allDay) value.requested.allDay = true;
|
|
2180
|
+
const handledBy = textOf(data && data.system && data.system.handledBy, 200);
|
|
2181
|
+
if (handledBy) value.handledBy = handledBy;
|
|
2182
|
+
if (data && typeof data.effect === 'string') value.effect = data.effect;
|
|
2183
|
+
if (data && Array.isArray(data.elements)) value.elements = data.elements.slice(0, 120);
|
|
2184
|
+
const note = systemNotes(sessionId, deviceId, 'system.calendar_insert').filter(Boolean).join(' ');
|
|
2185
|
+
if (note) value.note = note;
|
|
2186
|
+
return value;
|
|
2187
|
+
},
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
register({
|
|
2191
|
+
name: 'phone_app_after_task',
|
|
2192
|
+
description: 'Decide whether an app should be left running or closed when the task ends, with a reason. It only records the mark; nothing is touched now. Default (when unmarked) is to keep a quiet app in the background and close one that is playing sound.',
|
|
2193
|
+
parameters: {
|
|
2194
|
+
packageName: { type: 'string', required: true, description: 'Exact package name of the app.' },
|
|
2195
|
+
action: { type: 'string', required: true, enum: ['keep', 'close'], description: 'keep: leave it running after the task (even if playing sound). close: close it when the task ends.' },
|
|
2196
|
+
reason: { type: 'string', required: true, description: 'Why, in a few words.' },
|
|
2197
|
+
user: { type: 'integer', description: 'Only for another copy of the app (clone / work profile): that copy\'s user value. Leave out for the main app.' },
|
|
2198
|
+
},
|
|
2199
|
+
output: {
|
|
2200
|
+
schema: {
|
|
2201
|
+
type: 'object',
|
|
2202
|
+
additionalProperties: false,
|
|
2203
|
+
properties: {
|
|
2204
|
+
ok: { type: 'boolean' },
|
|
2205
|
+
packageName: { type: 'string' },
|
|
2206
|
+
action: { type: 'string' },
|
|
2207
|
+
replaces: { type: 'string' },
|
|
2208
|
+
note: { type: 'string' },
|
|
2209
|
+
},
|
|
2210
|
+
},
|
|
2211
|
+
render: (_args, value) => [{ type: 'text', text: `${value.packageName || '这个 App'} 任务后${value.action === 'close' ? '关掉' : '留着'}。` }],
|
|
2212
|
+
},
|
|
2213
|
+
execute: async (args, exec) => {
|
|
2214
|
+
const sessionId = sessionOf(exec);
|
|
2215
|
+
const { deviceId, service } = targetOf(ctx, deps, sessionId);
|
|
2216
|
+
const pkg = textOf(args.packageName, 200);
|
|
2217
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$/.test(pkg)) throw new Error('packageName 看起来不像安卓包名。');
|
|
2218
|
+
if (args.action !== 'keep' && args.action !== 'close') throw new Error('action 只能是 keep / close。');
|
|
2219
|
+
const reason = textOf(args.reason, 200);
|
|
2220
|
+
if (!reason) throw new Error('给一句原因(reason)。');
|
|
2221
|
+
const payload = { packageName: pkg, action: args.action, reason };
|
|
2222
|
+
if (Number.isInteger(args.user)) payload.user = args.user;
|
|
2223
|
+
const data = await run(service, deviceId, sessionId, 'app.after_task', payload, exec.signal);
|
|
2224
|
+
const value = { ok: true, packageName: pkg, action: args.action };
|
|
2225
|
+
if (data && typeof data.replaces === 'string') value.replaces = data.replaces;
|
|
2226
|
+
const note = [mentionMismatch(sessionId, deviceId), '只记下了,任务结束时才按它处置,不会当场关掉。'].filter(Boolean).join(' ');
|
|
2227
|
+
if (note) value.note = note;
|
|
2228
|
+
return value;
|
|
2229
|
+
},
|
|
2230
|
+
});
|
|
2231
|
+
|
|
1643
2232
|
register({
|
|
1644
2233
|
name: 'phone_target',
|
|
1645
2234
|
description: 'Show or change which phone this conversation operates. Without arguments it reports the binding and the phones currently connected. With a deviceId it rebinds — only do that when the user asked to switch phones.',
|