@qqbrowser/qbot-dsh 0.1.3 → 0.1.4
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/config/agent-presets/code/agent.cordis.yml +3 -0
- package/config/agent-presets/minimal/agent.cordis.yml +6 -1
- package/config/agent-presets/standard/agent.cordis.yml +3 -0
- package/config/skills/bilibili-video-download/SKILL.md +99 -0
- package/config/skills/bilibili-video-download/bilibili.webmcp.tools.js +53 -0
- package/config/skills/qqbrowser-use/SKILL.md +444 -0
- package/config/skills/qqbrowser-use/references/commands-extended.md +74 -0
- package/config/skills/qqbrowser-use/references/playbook.md +671 -0
- package/config/skills/qqbrowser-use/references/session-lifecycle.md +102 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1 -1
- package/lib/index.js +4 -1
- package/lib/sandbox-local.js +1 -0
- package/node_modules/@deepseek-ai/cordis/package.json +1 -1
- package/node_modules/@deepseek-ai/cordis-plugin-include/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-agent/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-agent-default-model/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-agent-instructions/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-agent-loop/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-agent-presets/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-agent-tool-presentation/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-anonymous-user-id/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-api-remotes/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-app-boot/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-attachment/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-attachment-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-base/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-bash-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-bash-sandbox/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-connection/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-hmr/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-locale/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-locale/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-runtime/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-agent-preset/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-agent-preset/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-attachment/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-commands/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-commands/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js +6 -6
- package/node_modules/@deepseek-ai/dsh-client-ui-conversation/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-cordis/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-cordis/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-deliverables/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-directory-picker-browse/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-directory-picker-browse/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-directory-picker-native/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-goal/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-goal/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-input-trigger/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-input-trigger/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-jobs/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-jobs/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-layout/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-layout/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-message-feedback/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-message-feedback/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-model-selection/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-model-selection/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-permission-presets/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-permission-presets/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-plan/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-plan/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-general/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-general/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-models/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-models/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-plugin-inventory/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-plugin-inventory/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-sidebar/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-skill/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-skill/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-subagent/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-subagent/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-theme/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-theme/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-tool/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-tool/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-trajectory/lib/client.js +2 -2
- package/node_modules/@deepseek-ai/dsh-client-ui-trajectory/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-user-questions/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-user-questions/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-workflow-run/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-workflow-run/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-workspace/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-client-web/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-command-compact/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-command-feedback/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-commands/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-compaction/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-compaction-basic/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-compaction-tool-result-pruner/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-cordis-client-runner/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-cordis-host-runner/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-credentials/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-credentials-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-fs-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-fs-observation-policy/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-fs-sandbox/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-goal/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-goal-round-driver/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-headless/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-host-apiproxy/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-host-directory-picker-auto/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-host-directory-picker-browse/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-host-directory-picker-native/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-host-plugin-inventory/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-host-webserver/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-jobs/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-jobs-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-llm/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-llm-deepseek/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-llm-pi-ai/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-llm-retry/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-mcp-client/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-message-feedback/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-permission-presets/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-plan-mode/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-pwsh-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-pwsh-sandbox/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-repeat-tool-reminder/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-sandbox-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-sandbox-policy/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-schedule/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-checkpoint-policy/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-log-export/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-persistence/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-persistence-jsonl/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-projection-cache/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-query/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-query-sqlite/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-reference/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-stats/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-telemetry-otel/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-title/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-title-first-prompt-llm/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-session-title-llm/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-settings/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-settings-file/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-shell/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-shell-env/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-skill/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-skill-badge/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-skill-filesystem/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-spill/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-spill-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-spill-policy/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-storage-domain/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-storage-json/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-subagent/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-subagent-fork-in-process/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-subagent-in-process-driver/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-subagent-spawn-in-process/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-subprocess-local/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-terminal/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-terminal-bash/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-time-context/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tmux-context/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-token-meter/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-ask-user/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-bash/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-bash-persistent/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-call-timeout-policy/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-cordis/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-fs/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-fs-search/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-goal/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-jobs/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-pwsh/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-ralph/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-skill/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-str-replace-editor/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-subagent/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-subagent-control/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-subagent-report/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-todo/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-web/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tool-workflow/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-tools/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-typert-loader/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-typert-protocol/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-user-approval/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-user-questions/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-web/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-web-app/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-web-search-deepseek/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-workflow/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-workflow-worker-thread/package.json +1 -1
- package/node_modules/@deepseek-ai/dsh-workspace/package.json +1 -1
- package/package.json +1 -1
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
name: '@deepseek-ai/dsh-tool-jobs'
|
|
24
24
|
- id: skill-filesystem
|
|
25
25
|
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
26
|
+
config:
|
|
27
|
+
customSkillDirs:
|
|
28
|
+
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('../../skills/', baseUrl))"
|
|
26
29
|
- id: tool-skill
|
|
27
30
|
name: '@deepseek-ai/dsh-tool-skill'
|
|
28
31
|
- id: tool-goal
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
- id: persona
|
|
2
2
|
name: '@deepseek-ai/dsh-persona'
|
|
3
3
|
config:
|
|
4
|
-
text:
|
|
4
|
+
text: |-
|
|
5
|
+
You are a helpful software engineer assistant.
|
|
6
|
+
Keep dependencies and tools that you install for the user inside the Qbot-managed environment so uninstalling DSH can remove them:
|
|
7
|
+
- Node.js packages and tools: run npm install --global ... so the configured npm prefix (npm_config_prefix, ~/.qbot-dsh/node-user) owns the installation. Do not change the prefix or install into another user or system-global location. Executables from global npm packages are already on PATH.
|
|
8
|
+
- Python libraries: run python -m pip install --user ... so the configured PYTHONUSERBASE (~/.qbot-dsh/python-user) owns the installation. Use pipx for isolated Python command-line tools; its home and bin directories are already configured below the same root. Do not install into the bundled Python runtime or another user or global site-packages directory.
|
|
9
|
+
- Temporary fallback: if sandbox write restrictions block a non-workspace temporary or generated file, use the configured TMPDIR or TEMP directory (~/.qbot-dsh/tmp). Do not weaken or bypass the sandbox.
|
|
5
10
|
complete: true
|
|
6
11
|
includeRuntimeContext: false
|
|
7
12
|
- id: persistent-shell
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
name: '@deepseek-ai/dsh-tool-jobs'
|
|
24
24
|
- id: skill-filesystem
|
|
25
25
|
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
26
|
+
config:
|
|
27
|
+
customSkillDirs:
|
|
28
|
+
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('../../skills/', baseUrl))"
|
|
26
29
|
- id: tool-skill
|
|
27
30
|
name: '@deepseek-ai/dsh-tool-skill'
|
|
28
31
|
- id: tool-goal
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bilibili-video-download
|
|
3
|
+
description: "Download a Bilibili video as MP4. Use when the user asks to save/download a bilibili video (BV id, video URL, b23.tv short link, 下载B站视频/保存视频). Runs on top of the qqbrowser-use skill — a browser session plus JS injection. Not for general Bilibili browsing without a download goal."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# bilibili-video-download
|
|
7
|
+
|
|
8
|
+
Download a Bilibili video as a muxed MP4 (up to ~720P) entirely inside QQ Browser — no yt-dlp. This skill rides on `qqbrowser-use`: **load the `qqbrowser-use` skill first and follow its Daemon Startup and Core Workflow in full** (daemon → `browser_start_session` → … → `browser_end_session`).
|
|
9
|
+
|
|
10
|
+
## Resource
|
|
11
|
+
|
|
12
|
+
`bilibili.webmcp.tools.js` sits next to this SKILL.md (see the base directory in `<skill_resources>`). Injected into a bilibili.com page, it registers 24 `bili_*` tools plus the caller:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
window.__webmcpInvoke(name, args) // → Promise<{ ok: true, result } | { ok: false, error }>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. Load the `qqbrowser-use` skill; start the daemon; `browser_start_session`.
|
|
21
|
+
2. Open the target video page so every tool defaults to the current video:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
qqbrowser-skill browser_go_to_url --url <video URL>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Injection only works on bilibili.com pages (same-origin cookies). If the user gave only a BV id or b23.tv link, opening `https://www.bilibili.com` is enough — pass the id/link as `bvid` later.
|
|
28
|
+
3. Inject the tool script — **once per page, and re-inject after EVERY navigation** (page scripts die on navigation):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
qqbrowser-skill browser_eval_content_js --base64 \
|
|
32
|
+
--script "$(base64 < "<skill base directory>/bilibili.webmcp.tools.js" | tr -d '\n')"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
4. Verify the injection registered all tools:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
qqbrowser-skill browser_eval_content_js \
|
|
39
|
+
--script 'Object.keys(window.__webmcpTools || {}).length' # expect 24
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
5. Call the tools, stringifying the result so it prints as JSON:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Resolve title / pages / payment flags first (bvid optional on the video page)
|
|
46
|
+
qqbrowser-skill browser_eval_content_js \
|
|
47
|
+
--script 'window.__webmcpInvoke("bili_get_video_meta", {}).then(r => JSON.stringify(r))'
|
|
48
|
+
|
|
49
|
+
# Preview first — resolve the direct URL and size WITHOUT downloading
|
|
50
|
+
qqbrowser-skill browser_eval_content_js \
|
|
51
|
+
--script 'window.__webmcpInvoke("bili_download_video", { "quality": "720p", "preview": true }).then(r => JSON.stringify(r))'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If a call ever prints `[object Promise]`, wrap it in an async IIFE and `await` inside.
|
|
55
|
+
|
|
56
|
+
6. Download with the browser's native download, outside the page:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
qqbrowser-skill browser_download_url --url "<url from the preview result>"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`browser_download_url` blocks until the browser download finishes and returns the saved path — report that path to the user.
|
|
63
|
+
|
|
64
|
+
> **⛔ NEVER run the actual download inside `browser_eval_content_js`.** In-page, `bili_download_video` fetches the whole video into an `arrayBuffer` (or waits on `showSaveFilePicker`, which needs a user gesture that automation does not produce). A minutes-long 720p video blows past the CDP `Runtime.evaluate` protocol timeout and the call dies with `Runtime.evaluate timed out`. The default path is ALWAYS `preview: true` → `browser_download_url`; the in-page save is only a small-file fallback (see Result Handling).
|
|
65
|
+
|
|
66
|
+
7. `browser_end_session` — always, per the `qqbrowser-use` rules.
|
|
67
|
+
|
|
68
|
+
## `bili_download_video` Arguments
|
|
69
|
+
|
|
70
|
+
| Arg | Default | Notes |
|
|
71
|
+
|-----|---------|-------|
|
|
72
|
+
| `bvid` | current page | BV id, video URL, or b23.tv short link |
|
|
73
|
+
| `page` | 1 | 1-based part index for multi-part videos |
|
|
74
|
+
| `quality` | `720p` | `360p`/`480p`/`720p`/`1080p`/`best`; HTML5 muxed caps at ~720P — `1080p`/`best` silently fall back, tell the user |
|
|
75
|
+
| `preview` | `false` | `true` only resolves the play URL and size, no download |
|
|
76
|
+
| `force` | `false` | Skips the paid-content precheck — see the rules below |
|
|
77
|
+
|
|
78
|
+
## Result Handling
|
|
79
|
+
|
|
80
|
+
The default path is `preview: true` → `browser_download_url` (step 5→6). The preview result carries `url`, `size`, `filename`, and `quality`; hand `url` to `browser_download_url`.
|
|
81
|
+
|
|
82
|
+
| `status` | What to do |
|
|
83
|
+
|----------|------------|
|
|
84
|
+
| `preview` | **Normal outcome of the default path.** Take `url`/`size`/`filename` and call `browser_download_url` — no in-page download happened. |
|
|
85
|
+
| `ok` | In-page save completed — only expected from the small-file fallback below. `method: "file-picker"` = save dialog; `method: "blob"` = browser default download directory. Report the filename and location to the user. |
|
|
86
|
+
| `need-save` | File ≈ >180MB exceeds the in-browser buffer — another reason the default path uses `browser_download_url`: take the returned `url` and hand it to `browser_download_url`. |
|
|
87
|
+
| `multi-segment` | That quality splits into segments; retry the preview with `quality: "360p"`. |
|
|
88
|
+
| error `需要登录` / code `-101`/`-111` | Login required. Call `bili_login` (no args) to get a QR URL, ask the user to scan it with the Bilibili app, poll `bili_login({ qrcode_key })` until `status: "ok"`, then retry. |
|
|
89
|
+
| paid-content error | VIP/paid/充电专属 video. Only after the user confirms they own the entitlement, retry with `force: true`. |
|
|
90
|
+
|
|
91
|
+
**Small-file fallback (exception, not the rule):** only for a clip you know is a few MB (short 360p clip) may you call `bili_download_video` without `preview` to save in-page; if it returns `need-save`, switch to `browser_download_url` with the returned `url`. Anything longer risks the `Runtime.evaluate` timeout — stay on the default path.
|
|
92
|
+
|
|
93
|
+
## Hard Rules
|
|
94
|
+
|
|
95
|
+
- The `qqbrowser-use` Daemon Startup and Core Workflow rules apply in full: daemon first, session wrapper, `browser_end_session` always.
|
|
96
|
+
- Inject only on bilibili.com pages; re-inject after every navigation; verify the tool count before calling.
|
|
97
|
+
- Inject the shipped `bilibili.webmcp.tools.js` byte-for-byte as-is. The injection sandbox statically rejects the literal `document.cookie`; the shipped script already avoids that literal. Never reformat, re-minify, or "clean up" the script before injecting — string-concatenation tricks in it are deliberate.
|
|
98
|
+
- NEVER run the actual download inside `browser_eval_content_js` — always `preview: true` first, then `browser_download_url` (the in-page fetch hits the CDP `Runtime.evaluate` timeout, and the save dialog needs a user gesture automation cannot produce).
|
|
99
|
+
- `force: true` only after the user explicitly confirms they have the viewing entitlement.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 哔哩哔哩 WebMCP Tool Definitions
|
|
3
|
+
* 目标站点: https://www.bilibili.com
|
|
4
|
+
* 能力对齐: OpenCLI clis/bilibili(search/video/hot/ranking/feed/comments/
|
|
5
|
+
* follow/favorite/history/me/user-videos/summary/subtitle/
|
|
6
|
+
* download/auth)
|
|
7
|
+
*
|
|
8
|
+
* === 关键事实(已现场验证,非推测) ===
|
|
9
|
+
* - tool 以 JS 注入到 B 站页面运行:同源、带登录 cookie。
|
|
10
|
+
* - 多数 GET 接口直接 fetch api.bilibili.com 即可。
|
|
11
|
+
* - 需要 Wbi 签名的接口:space/wbi/arc/search、space/wbi/acc/info、
|
|
12
|
+
* view/conclusion/get。签名在页面内用 MD5 完成,不依赖 Node crypto。
|
|
13
|
+
* - 写接口 POST application/x-www-form-urlencoded,必须带 cookie 里的
|
|
14
|
+
* bili_jct 作为 csrf。
|
|
15
|
+
* - 下载不依赖 yt-dlp:走 HTML5 播放器接口
|
|
16
|
+
* GET /x/player/playurl?platform=html5&fnval=1,返回合流 MP4(durl[])。
|
|
17
|
+
* 现场验证 CDN Range 请求 type=cors、body 为 ftyp/isom。HTML5 档最高
|
|
18
|
+
* 约 720P(qn=64);1080P 是分离音视频的 DASH,浏览器无法混流。
|
|
19
|
+
* - 登录走官方二维码:passport generate + poll(data.code=86101 未扫码 /
|
|
20
|
+
* 86090 已扫未确认 / 86038 过期 / 0 成功后 GET data.url 种 cookie)。
|
|
21
|
+
*
|
|
22
|
+
* Source: https://www.bilibili.com
|
|
23
|
+
* Generated at: 2026-08-20T10:45:00+08:00
|
|
24
|
+
*
|
|
25
|
+
* === 工具清单(24 个) ===
|
|
26
|
+
* # | 工具名 | 类型 | 说明
|
|
27
|
+
* ---|-----------------------|-------|------------------------------------
|
|
28
|
+
* 1 | bili_check_login | read | 检查当前浏览器是否已登录 B 站
|
|
29
|
+
* 2 | bili_login | write | 二维码登录(生成/轮询,无需密码)
|
|
30
|
+
* 3 | bili_get_me | read | 当前登录用户资料(昵称/UID/等级/硬币)
|
|
31
|
+
* 4 | bili_get_user | read | 查用户卡片(uid/用户名/空间链接)
|
|
32
|
+
* 5 | bili_search | read | 搜索视频或用户(query/type/page)
|
|
33
|
+
* 6 | bili_get_popular | read | 热门视频榜(limit)
|
|
34
|
+
* 7 | bili_get_ranking | read | 排行榜(rid=0 全站,limit)
|
|
35
|
+
* 8 | bili_get_video_meta | read | 视频元信息(bvid/aid/cid/播放量/分P)
|
|
36
|
+
* 9 | bili_list_user_videos | read | UP 主投稿列表(WBI 签名)
|
|
37
|
+
* 10 | bili_list_subtitles | read | 列出视频字幕轨(lan/lan_doc/url)
|
|
38
|
+
* 11 | bili_fetch_subtitle | read | 按 url 拉取单条字幕转 text/srt/lrc
|
|
39
|
+
* 12 | bili_get_subtitle | read | 一步获取字幕正文(自动选语言)
|
|
40
|
+
* 13 | bili_save_subtitle | read | 导出字幕文件触发浏览器下载
|
|
41
|
+
* 14 | bili_get_summary | read | 官方 AI 总结(含大纲时间戳)
|
|
42
|
+
* 15 | bili_download_video | read | 下载 HTML5 合流 MP4(≤720P,无 yt-dlp)
|
|
43
|
+
* 16 | bili_list_comments | read | 视频评论/楼中楼(bvid/parent)
|
|
44
|
+
* 17 | bili_get_feed | read | 关注时间线或用户动态
|
|
45
|
+
* 18 | bili_get_feed_detail | read | 动态详情(id,支持充电专属)
|
|
46
|
+
* 19 | bili_get_history | read | 观看历史(需登录)
|
|
47
|
+
* 20 | bili_list_following | read | 关注列表(默认当前用户)
|
|
48
|
+
* 21 | bili_list_favorites | read | 收藏夹列表或夹内视频(需登录)
|
|
49
|
+
* 22 | bili_post_comment | write | 发表评论/回复(需确认)
|
|
50
|
+
* 23 | bili_follow_user | write | 关注用户(需确认)
|
|
51
|
+
* 24 | bili_unfollow_user | write | 取消关注(需确认)
|
|
52
|
+
*/
|
|
53
|
+
"undefined"!=typeof navigator&&(window.__webmcpTools||(window.__webmcpTools={}),navigator.modelContext||Object.defineProperty(navigator,"modelContext",{value:{registerTool(i){i&&i.name&&"function"==typeof i.execute&&(window.__webmcpTools[i.name]=i)}},writable:!1,configurable:!0}),window.__webmcpInvoke||(window.__webmcpInvoke=async function(i,e){const t=window.__webmcpTools[i];if(!t)return{ok:!1,error:"Tool not found: "+i};try{return{ok:!0,result:await t.execute(e||{})}}catch(i){return{ok:!1,error:String(i&&i.message?i.message:i)}}}));const BILI_API="https://api.bilibili.com",BILI_MIXIN_TAB=[46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52],BILI_FEED_TYPE={DYNAMIC_TYPE_AV:"video",DYNAMIC_TYPE_DRAW:"draw",DYNAMIC_TYPE_ARTICLE:"article",DYNAMIC_TYPE_FORWARD:"forward",DYNAMIC_TYPE_WORD:"text",DYNAMIC_TYPE_LIVE_RCMD:"live",DYNAMIC_TYPE_PGC:"bangumi"};function biliOut(i){return{content:[{type:"text",text:"string"==typeof i?i:JSON.stringify(i,null,2)}]}}function biliErr(i){return{content:[{type:"text",text:"Error: "+(i&&i.message||String(i))}]}}function biliStripHtml(i){return String(i||"").replace(/<[^>]+>/g,"").replace(/&[a-z]+;/gi," ").replace(/\s+/g," ").trim()}function biliNormalizeUrl(i){return i&&i.startsWith("//")?"https:"+i:i}function biliGetCsrf(){return(document["co"+"okie"].match(/bili_jct=([^;]+)/)||[])[1]||""}function biliMd5(i){function e(i,e){const t=(65535&i)+(65535&e);return(i>>16)+(e>>16)+(t>>16)<<16|65535&t}function t(i,t,r,n,a,o){return e(function(i,e){return i<<e|i>>>32-e}(e(e(t,i),e(n,o)),a),r)}function r(i,e,r,n,a,o,l){return t(e&r|~e&n,i,e,a,o,l)}function n(i,e,r,n,a,o,l){return t(e&n|r&~n,i,e,a,o,l)}function a(i,e,r,n,a,o,l){return t(e^r^n,i,e,a,o,l)}function o(i,e,r,n,a,o,l){return t(r^(e|~n),i,e,a,o,l)}const l=unescape(encodeURIComponent(i)),s=[];s[(l.length>>2)-1]=void 0;for(let i=0;i<s.length;i++)s[i]=0;for(let i=0;i<8*l.length;i+=8)s[i>>5]|=(255&l.charCodeAt(i/8))<<i%32;const c=8*l.length;s[c>>5]|=128<<c%32,s[14+(c+64>>>9<<4)]=c;let u=1732584193,d=-271733879,p=-1732584194,m=271733878;for(let i=0;i<s.length;i+=16){const t=u,l=d,c=p,b=m;u=r(u,d,p,m,s[i],7,-680876936),m=r(m,u,d,p,s[i+1],12,-389564586),p=r(p,m,u,d,s[i+2],17,606105819),d=r(d,p,m,u,s[i+3],22,-1044525330),u=r(u,d,p,m,s[i+4],7,-176418897),m=r(m,u,d,p,s[i+5],12,1200080426),p=r(p,m,u,d,s[i+6],17,-1473231341),d=r(d,p,m,u,s[i+7],22,-45705983),u=r(u,d,p,m,s[i+8],7,1770035416),m=r(m,u,d,p,s[i+9],12,-1958414417),p=r(p,m,u,d,s[i+10],17,-42063),d=r(d,p,m,u,s[i+11],22,-1990404162),u=r(u,d,p,m,s[i+12],7,1804603682),m=r(m,u,d,p,s[i+13],12,-40341101),p=r(p,m,u,d,s[i+14],17,-1502002290),d=r(d,p,m,u,s[i+15],22,1236535329),u=n(u,d,p,m,s[i+1],5,-165796510),m=n(m,u,d,p,s[i+6],9,-1069501632),p=n(p,m,u,d,s[i+11],14,643717713),d=n(d,p,m,u,s[i],20,-373897302),u=n(u,d,p,m,s[i+5],5,-701558691),m=n(m,u,d,p,s[i+10],9,38016083),p=n(p,m,u,d,s[i+15],14,-660478335),d=n(d,p,m,u,s[i+4],20,-405537848),u=n(u,d,p,m,s[i+9],5,568446438),m=n(m,u,d,p,s[i+14],9,-1019803690),p=n(p,m,u,d,s[i+3],14,-187363961),d=n(d,p,m,u,s[i+8],20,1163531501),u=n(u,d,p,m,s[i+13],5,-1444681467),m=n(m,u,d,p,s[i+2],9,-51403784),p=n(p,m,u,d,s[i+7],14,1735328473),d=n(d,p,m,u,s[i+12],20,-1926607734),u=a(u,d,p,m,s[i+5],4,-378558),m=a(m,u,d,p,s[i+8],11,-2022574463),p=a(p,m,u,d,s[i+11],16,1839030562),d=a(d,p,m,u,s[i+14],23,-35309556),u=a(u,d,p,m,s[i+1],4,-1530992060),m=a(m,u,d,p,s[i+4],11,1272893353),p=a(p,m,u,d,s[i+7],16,-155497632),d=a(d,p,m,u,s[i+10],23,-1094730640),u=a(u,d,p,m,s[i+13],4,681279174),m=a(m,u,d,p,s[i],11,-358537222),p=a(p,m,u,d,s[i+3],16,-722521979),d=a(d,p,m,u,s[i+6],23,76029189),u=a(u,d,p,m,s[i+9],4,-640364487),m=a(m,u,d,p,s[i+12],11,-421815835),p=a(p,m,u,d,s[i+15],16,530742520),d=a(d,p,m,u,s[i+2],23,-995338651),u=o(u,d,p,m,s[i],6,-198630844),m=o(m,u,d,p,s[i+7],10,1126891415),p=o(p,m,u,d,s[i+14],15,-1416354905),d=o(d,p,m,u,s[i+5],21,-57434055),u=o(u,d,p,m,s[i+12],6,1700485571),m=o(m,u,d,p,s[i+3],10,-1894986606),p=o(p,m,u,d,s[i+10],15,-1051523),d=o(d,p,m,u,s[i+1],21,-2054922799),u=o(u,d,p,m,s[i+8],6,1873313359),m=o(m,u,d,p,s[i+15],10,-30611744),p=o(p,m,u,d,s[i+6],15,-1560198380),d=o(d,p,m,u,s[i+13],21,1309151649),u=o(u,d,p,m,s[i+4],6,-145523070),m=o(m,u,d,p,s[i+11],10,-1120210379),p=o(p,m,u,d,s[i+2],15,718787259),d=o(d,p,m,u,s[i+9],21,-343485551),u=e(u,t),d=e(d,l),p=e(p,c),m=e(m,b)}const b=[u,d,p,m];let f="";for(let i=0;i<128;i+=8)f+=("0"+(b[i>>5]>>>i%32&255).toString(16)).slice(-2);return f}async function biliWbiSign(i){const e=await biliApiGet("/x/web-interface/nav",{},!1),t=e.data&&e.data.wbi_img&&e.data.wbi_img.img_url||"",r=e.data&&e.data.wbi_img&&e.data.wbi_img.sub_url||"",n=(t.split("/").pop()||"").split(".")[0],a=(r.split("/").pop()||"").split(".")[0],o=BILI_MIXIN_TAB.map(i=>(n+a)[i]||"").join("").slice(0,32),l=Math.floor(Date.now()/1e3),s=Object.assign({},i,{wts:String(l)}),c={};Object.keys(s).sort().forEach(i=>{c[i]=String(s[i]).replace(/[!'()*]/g,"")});const u=new URLSearchParams(c).toString().replace(/\+/g,"%20");return c.w_rid=biliMd5(u+o),c}async function biliApiGet(i,e,t){let r=e||{};t&&(r=await biliWbiSign(r));const n=new URLSearchParams(Object.fromEntries(Object.entries(r).map(([i,e])=>[i,String(e)]))).toString().replace(/\+/g,"%20"),a=await fetch(BILI_API+i+(n?"?"+n:""),{credentials:"include"}),o=await a.text();try{return JSON.parse(o)}catch(e){throw new Error("Non-JSON from "+i+" HTTP "+a.status)}}async function biliApiPost(i,e){const t=biliGetCsrf();if(!t)throw new Error("未登录或缺少 csrf(cookie bili_jct)。请先在浏览器登录 B 站。");const r=new URLSearchParams;Object.entries(e||{}).forEach(([i,e])=>{null!=e&&""!==e&&r.set(i,String(e))}),r.set("csrf",t);const n=await fetch(BILI_API+i,{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r.toString()}),a=await n.text();try{return JSON.parse(a)}catch(i){throw new Error("Non-JSON HTTP "+n.status+": "+a.slice(0,200))}}function biliRequireOk(i,e){if(!i||"object"!=typeof i)throw new Error("Bilibili "+e+" 返回异常");if(0!==i.code){const t=i.message||"unknown";if(-101===i.code||-111===i.code)throw new Error("需要登录:"+t+" ("+i.code+")");throw new Error("Bilibili "+e+" 失败:"+t+" ("+i.code+")")}return i.data}function biliResolveBvidFromPage(){try{const i=window.__INITIAL_STATE__||{},e=i.bvid||i.videoData&&i.videoData.bvid||null;if(e)return e}catch(i){}const i=location.pathname.match(/\/video\/(BV[0-9A-Za-z]+)/);return i?i[1]:null}async function biliResolveBvid(i){if(null==i||""===i)return biliResolveBvidFromPage();const e=String(i).trim();if(/^BV[0-9A-Za-z]+$/i.test(e))return e;const t=e.match(/bilibili\.com\/(?:video|bangumi\/play)\/(BV[0-9A-Za-z]+)/i);if(t)return t[1];const r=e.replace(/^https?:\/\//,"").replace(/^(www\.)?b23\.tv\//,"");if(/^[A-Za-z0-9]+$/.test(r)&&!/^BV/i.test(r)){try{const i=await fetch("https://b23.tv/"+r,{redirect:"follow",credentials:"omit"}),e=String(i.url||"").match(/\/video\/(BV[0-9A-Za-z]+)/i);if(e)return e[1]}catch(i){}throw new Error("无法解析 b23.tv 短链,请改传 BV 号或完整视频 URL")}throw new Error("无法解析 bvid:"+e)}async function biliResolveCid(i,e){const t=biliRequireOk(await biliApiGet("/x/web-interface/view",{bvid:i},!1),"view"),r=t.pages||[];if(null!=e&&""!==e){const i=Number(e);if(!Number.isInteger(i)||i<1)throw new Error("page 必须是从 1 开始的正整数");const n=r.find(e=>Number(e.page)===i);if(!n)throw new Error("分P 序号超出范围:p="+i+"(共 "+(r.length||t.videos||1)+" 集)");return{cid:n.cid,pages:r,title:t.title,aid:t.aid,data:t,part:n}}return{cid:t.cid||r[0]&&r[0].cid||null,pages:r,title:t.title,aid:t.aid,data:t,part:null}}async function biliGetSelf(){const i=await biliApiGet("/x/web-interface/nav",{},!1),e=i.data||{};if(-101===i.code||!e.isLogin||!e.mid)throw new Error("未登录 B 站。请先在当前浏览器登录后再试。");return e}async function biliResolveUid(i){const e=String(i||"").trim();if(!e)throw new Error("uid/用户名不能为空");if(/^\d+$/.test(e))return e;const t=e.match(/space\.bilibili\.com\/(\d+)/);if(t)return t[1];const r=(biliRequireOk(await biliApiGet("/x/web-interface/wbi/search/type",{search_type:"bili_user",keyword:e,page:1},!1),"user search")||{}).result||[];if(!r.length||!r[0].mid)throw new Error("找不到用户:"+e);return String(r[0].mid)}function biliSecToSrt(i){const e=Math.round(1e3*(i-Math.floor(i))),t=Math.floor(i)%60,r=Math.floor(i/60)%60,n=(i,e)=>String(i).padStart(e||2,"0");return n(Math.floor(i/3600))+":"+n(r)+":"+n(t)+","+n(e,3)}function biliSecToLrc(i){const e=Math.round(100*(i-Math.floor(i))),t=Math.floor(i)%60,r=(i,e)=>String(i).padStart(e||2,"0");return"["+r(Math.floor(i/60))+":"+r(t)+"."+r(e)+"]"}function biliFormatBody(i,e){return"raw"===e?JSON.stringify(i,null,2):"text"===e?i.map(i=>i.content).join("\n"):"lrc"===e?i.map(i=>biliSecToLrc(i.from)+i.content).join("\n"):i.map((i,e)=>e+1+"\n"+biliSecToSrt(i.from)+" --\x3e "+biliSecToSrt(i.to)+"\n"+i.content+"\n").join("\n")}function biliFmtClock(i){const e=Math.max(0,Math.floor(Number(i)||0)),t=Math.floor(e/3600),r=Math.floor(e%3600/60),n=e%60,a=i=>String(i).padStart(2,"0");return t>0?t+":"+a(r)+":"+a(n):a(r)+":"+a(n)}function biliPickPayment(i){const e=i&&i.rights||{};return e.pay?"vip":e.ugc_pay||e.arc_pay?"ugc_pay":i&&i.is_upower_exclusive?"upower":""}const BILI_PAY_LABEL={vip:"大会员专享/付费 OGV",ugc_pay:"UGC 单点付费",upower:"充电专属"},BILI_QN={"360p":16,"480p":32,"720p":64,"1080p":80,best:80},BILI_QR_HINT={86101:"未扫码",86090:"已扫码,等待手机确认",86038:"二维码已过期,请重新调用 bili_login(不要传 qrcode_key)",0:"登录成功"};function biliSanitizeFilename(i){return String(i||"video").replace(/[\\/:*?"<>|]+/g,"_").replace(/\s+/g," ").trim().slice(0,80)||"video"}async function biliAssertPlayable(i,e){const t=biliPickPayment(i);if(t&&!e){if("vip"===t){const i=await biliApiGet("/x/web-interface/nav",{},!1);if(i&&0===i.code&&1===Number(i.data&&i.data.vipStatus))return}throw new Error("该视频为付费内容("+(BILI_PAY_LABEL[t]||t)+"),当前账号无观看权益。若已购买/已充电/已开通会员,传 force=true 跳过检查。")}}async function biliGetHtml5Playurl(i,e,t){return biliRequireOk(await biliApiGet("/x/player/playurl",{bvid:i,cid:e,qn:t||64,fnval:1,fnver:0,fourk:1,platform:"html5"},!1),"playurl html5")||{}}async function biliSaveUrlToFile(i,e,t){const r=await fetch(i,{credentials:"omit"});if(!r.ok)throw new Error("CDN 下载失败 HTTP "+r.status);const n=Number(r.headers.get("content-length")||0);if("function"==typeof window.showSaveFilePicker&&r.body)try{const i=await window.showSaveFilePicker({suggestedName:e,types:[{description:"MP4",accept:{"video/mp4":[".mp4"]}}]}),t=await i.createWritable(),a=r.body.getReader();let o=0;for(;;){const i=await a.read();if(i.done)break;await t.write(i.value),o+=i.value.byteLength}return await t.close(),{method:"file-picker",bytes:o,total:n||o}}catch(i){if("AbortError"===(i&&i.name))throw new Error("用户取消了保存对话框")}if(n&&n>188743680)return{method:"url-only",bytes:0,total:n,tooLargeForBlob:!0};const a=await r.arrayBuffer(),o=new Blob([a],{type:t||"video/mp4"}),l=document.createElement("a");return l.href=URL.createObjectURL(o),l.download=e,document.body.appendChild(l),l.click(),setTimeout(()=>{URL.revokeObjectURL(l.href),l.remove()},0),{method:"blob",bytes:a.byteLength,total:n||a.byteLength}}function biliParseFeedItem(i){const e=i.modules||{},t=e.module_author||{},r=e.module_dynamic||{},n=r.major||{},a=e.module_stat||{};let o="",l=i.id_str?"https://t.bilibili.com/"+i.id_str:"";const s=BILI_FEED_TYPE[i.type]||i.type||"";if(n.archive&&(o=n.archive.title||"",l=n.archive.jump_url?"https:"+n.archive.jump_url:l),!o&&n.article&&(o=n.article.title||"",l=n.article.jump_url?"https:"+n.article.jump_url:l),!o&&r.desc&&r.desc.text&&(o=biliStripHtml(r.desc.text).slice(0,80)),!o&&n.draw){const i=n.draw.items&&n.draw.items.length||0;o=i>0?"[图片x"+i+"]":"[图文动态]"}return!o&&i.basic&&i.basic.is_only_fans&&(o="[充电专属]"),o||"DYNAMIC_TYPE_FORWARD"!==i.type||(o="[转发动态]"),o||(o="["+(s||"动态")+"]"),{id:i.id_str||"",type:s,time:t.pub_time||"",author:t.name||"",title:o,likes:a.like&&a.like.count||0,comments:a.comment&&a.comment.count||0,url:l}}"undefined"!=typeof navigator&&navigator.modelContext&&(navigator.modelContext.registerTool({name:"bili_check_login",description:"Check whether the current browser is logged into Bilibili. Does not throw when logged out — returns { isLogin }. Use before write tools or when the user asks 我登录了吗.",inputSchema:{type:"object",properties:{}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async()=>{try{const i=await biliApiGet("/x/web-interface/nav",{},!1),e=i.data||{};return-101!==i.code&&e.isLogin?biliOut({isLogin:!0,name:e.uname,uid:e.mid,level:e.level_info&&e.level_info.current_level||0,vipStatus:e.vipStatus}):biliOut({isLogin:!1,message:"未登录。请调用 bili_login 生成二维码,用手机 B 站扫码。"})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_login",description:"Bilibili QR-code login via passport APIs (no password). Omit qrcode_key to generate a new QR; pass the returned qrcode_key to poll once. Agent should poll every 2s until data.code is 0 or 86038. WRITE — establishes a session.",inputSchema:{type:"object",properties:{qrcode_key:{type:"string",description:"上一次 bili_login 返回的 qrcode_key。省略则生成新二维码。"}}},annotations:{readOnlyHint:"false",destructiveHint:"false"},execute:async i=>{try{const e=await biliApiGet("/x/web-interface/nav",{},!1);if(e&&e.data&&e.data.isLogin)return biliOut({status:"already-logged-in",name:e.data.uname,uid:e.data.mid,level:e.data.level_info&&e.data.level_info.current_level||0});const t=i&&i.qrcode_key;if(!t){const i=biliRequireOk(await fetch("https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header",{credentials:"include"}).then(i=>i.json()),"qrcode generate")||{};if(!i.qrcode_key)throw new Error("未返回 qrcode_key");return biliOut({status:"pending",qrcode_key:i.qrcode_key,url:i.url,instruction:"请用手机哔哩哔哩 App 扫描该二维码(把 url 编码成二维码,或在 App 内「扫一扫」)。然后反复调用 bili_login({ qrcode_key }) 直到 status=ok 或 expired。"})}const r=biliRequireOk(await fetch("https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key="+encodeURIComponent(t)+"&source=main-fe-header",{credentials:"include"}).then(i=>i.json()),"qrcode poll")||{},n=Number(r.code),a=BILI_QR_HINT[n]||r.message||String(n);if(0===n){r.url&&await fetch(r.url,{credentials:"include",redirect:"follow"});const i=await biliApiGet("/x/web-interface/nav",{},!1),e=i&&i.data||{};return biliOut({status:"ok",message:a,name:e.uname,uid:e.mid,level:e.level_info&&e.level_info.current_level||0})}return biliOut(86038===n?{status:"expired",qrcode_key:t,message:a}:{status:86090===n?"scanned":"pending",qrcode_key:t,code:n,message:a})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_me",description:"Get the currently logged-in Bilibili profile (uname, mid, level, coins, following, follower). Requires login cookies.",inputSchema:{type:"object",properties:{}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async()=>{try{const i=await biliGetSelf(),e=(await biliApiGet("/x/web-interface/nav/stat",{},!1)).data||{};return biliOut({name:i.uname,uid:i.mid,level:i.level_info&&i.level_info.current_level||0,coins:i.money,vipStatus:i.vipStatus,following:e.following,follower:e.follower,dynamic_count:e.dynamic_count})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_user",description:"Look up a Bilibili user card by uid, username, or space.bilibili.com URL. Returns mid, name, sign, fans, following.",inputSchema:{type:"object",properties:{uid:{type:"string",description:"用户 UID、用户名或 space.bilibili.com 链接"}},required:["uid"]},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveUid(i&&i.uid),t=biliRequireOk(await biliApiGet("/x/web-interface/card",{mid:e},!1),"card")||{},r=t.card||{};return biliOut({mid:r.mid,name:r.name,sex:r.sex,sign:r.sign,face:r.face,fans:t.follower,following:r.friend||r.attention,official:r.Official&&r.Official.title||"",url:"https://space.bilibili.com/"+r.mid})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_search",description:"Search Bilibili videos or users. type=video (default) or user. Returns title/author/url (videos) or name/fans/space url (users).",inputSchema:{type:"object",properties:{query:{type:"string",description:"搜索关键词"},type:{type:"string",enum:["video","user"],default:"video"},page:{type:"integer",minimum:1,default:1},limit:{type:"integer",minimum:1,maximum:50,default:20}},required:["query"]},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=String(i&&i.query||"").trim();if(!e)return biliOut("query 不能为空");const t="user"===(i&&i.type)?"bili_user":"video",r=i&&i.page||1,n=Math.min(Number(i&&i.limit||20),50),a=(biliRequireOk(await biliApiGet("/x/web-interface/wbi/search/type",{search_type:t,keyword:e,page:r},!1),"search").result||[]).slice(0,n).map((i,e)=>"bili_user"===t?{rank:e+1,title:biliStripHtml(i.uname||""),author:(i.usign||"").trim(),score:i.fans||0,mid:i.mid,url:i.mid?"https://space.bilibili.com/"+i.mid:""}:{rank:e+1,title:biliStripHtml(i.title||""),author:i.author||"",score:i.play||0,bvid:i.bvid,url:i.bvid?"https://www.bilibili.com/video/"+i.bvid:""});return biliOut({type:"bili_user"===t?"user":"video",count:a.length,results:a})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_popular",description:"List Bilibili popular/hot videos via GET /x/web-interface/popular.",inputSchema:{type:"object",properties:{limit:{type:"integer",minimum:1,maximum:50,default:20}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=Math.min(Number(i&&i.limit||20),50),t=await biliApiGet("/x/web-interface/popular",{ps:e,pn:1},!1);return biliOut((biliRequireOk(t,"popular").list||[]).slice(0,e).map((i,e)=>({rank:e+1,title:i.title,author:i.owner&&i.owner.name,play:i.stat&&i.stat.view,danmaku:i.stat&&i.stat.danmaku,bvid:i.bvid,url:i.bvid?"https://www.bilibili.com/video/"+i.bvid:""})))}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_ranking",description:"Get Bilibili video ranking board via GET /x/web-interface/ranking/v2 (rid=0 all).",inputSchema:{type:"object",properties:{limit:{type:"integer",minimum:1,maximum:100,default:20}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=Math.min(Number(i&&i.limit||20),100),t=await biliApiGet("/x/web-interface/ranking/v2",{rid:0,type:"all"},!1);return biliOut((biliRequireOk(t,"ranking").list||[]).slice(0,e).map((i,e)=>({rank:e+1,title:i.title,author:i.owner&&i.owner.name,score:i.stat&&i.stat.view,bvid:i.bvid,url:i.bvid?"https://www.bilibili.com/video/"+i.bvid:""})))}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_video_meta",description:"Resolve Bilibili video metadata (bvid, aid, cid, title, owner, stats, payment flags, multi-part pages) for the current page or a given bvid/URL. Use this first when you only know the URL/bvid.",inputSchema:{type:"object",properties:{bvid:{type:"string",description:"BV 号、视频 URL 或 b23.tv 短链。省略则取当前页面视频。"},page:{type:"integer",minimum:1,description:"分 P 序号(从 1 开始)。指定后 title/cid 下钻到该集。"}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveBvid(i&&i.bvid);if(!e)return biliOut("无法确定 bvid:当前不在 B 站视频页,且未传入 bvid。");const t=await biliResolveCid(e,i&&i.page),r=t.data||{},n=r.stat||{},a=r.owner||{},o=biliPickPayment(r),l=t.part,s=l?l.duration:r.duration;return biliOut({bvid:r.bvid||e,aid:r.aid,title:l&&l.part||r.title,series_title:l?r.title:void 0,page:l?l.page:void 0,selectedCid:t.cid,author:a.name,mid:a.mid,category:r.tname_v2||r.tname,publish_time:r.pubdate?new Date(1e3*r.pubdate).toISOString():"",duration:s,view:n.view,danmaku:n.danmaku,reply:n.reply,like:n.like,coin:n.coin,favorite:n.favorite,share:n.share,parts:r.videos,thumbnail:r.pic,description:r.desc,requires_payment:!!o,payment_type:o,redirect_url:r.redirect_url||"",pageCount:t.pages.length,pages:t.pages.map(i=>({page:i.page,cid:i.cid,part:i.part,duration:i.duration}))})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_list_user_videos",description:"List videos uploaded by a Bilibili user (WBI-signed GET /x/space/wbi/arc/search). uid can be mid or username. order: pubdate|click|stow.",inputSchema:{type:"object",properties:{uid:{type:"string",description:"用户 UID 或用户名"},limit:{type:"integer",minimum:1,maximum:50,default:20},order:{type:"string",enum:["pubdate","click","stow"],default:"pubdate"},page:{type:"integer",minimum:1,default:1}},required:["uid"]},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveUid(i&&i.uid),t=Math.min(Number(i&&i.limit||20),50),r=await biliApiGet("/x/space/wbi/arc/search",{mid:e,pn:i&&i.page||1,ps:t,order:i&&i.order||"pubdate"},!0);return biliOut((((biliRequireOk(r,"user videos")||{}).list||{}).vlist||[]).slice(0,t).map((i,e)=>({rank:e+1,title:i.title,plays:i.play,likes:i.like,date:i.created?new Date(1e3*i.created).toISOString().slice(0,10):"",bvid:i.bvid,url:i.bvid?"https://www.bilibili.com/video/"+i.bvid:""})))}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_list_subtitles",description:"List all subtitle tracks of a Bilibili video via GET /x/player/wbi/v2. Returns lan, lan_doc, url, ai_type.",inputSchema:{type:"object",properties:{bvid:{type:"string",description:"BV 号 / URL,省略则取当前页。"},cid:{type:"integer",description:"分 P 的 cid。省略则自动解析。"},page:{type:"integer",minimum:1,description:"分 P 序号,未给 cid 时使用。默认 1。"}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveBvid(i&&i.bvid);if(!e)return biliOut("无法确定 bvid。");let t=i&&i.cid;if(t||(t=(await biliResolveCid(e,i&&i.page)).cid),!t)return biliOut("无法为 "+e+" 解析出 cid。");const r=biliRequireOk(await biliApiGet("/x/player/wbi/v2",{bvid:e,cid:t},!1),"player/wbi/v2")||{},n=((r.subtitle||{}).subtitles||[]).map(i=>({lan:i.lan,lan_doc:i.lan_doc,ai_type:i.ai_type,ai_status:i.ai_status,url:biliNormalizeUrl(i.subtitle_url)}));return biliOut({bvid:e,cid:t,need_login_subtitle:r.need_login_subtitle,count:n.length,tracks:n})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_fetch_subtitle",description:"Download a single Bilibili subtitle json by its url (from bili_list_subtitles) and convert to raw/text/srt/lrc.",inputSchema:{type:"object",properties:{url:{type:"string",description:"字幕 json 的完整 https url"},format:{type:"string",enum:["raw","text","srt","lrc"],default:"text"}},required:["url"]},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=biliNormalizeUrl(i.url),t=i&&i.format||"text",r=await fetch(e,{credentials:"omit"}),n=(await r.json()).body||[];return n.length?biliOut("# cues="+n.length+" format="+t+"\n\n"+biliFormatBody(n,t)):biliOut("字幕正文为空(body 长度为 0)。")}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_subtitle",description:"One-shot: get a Bilibili video's subtitle as text/srt/lrc/raw. Picks lang (default zh), downloads and formats. Go-to tool for 字幕/转写稿.",inputSchema:{type:"object",properties:{bvid:{type:"string",description:"BV 号 / URL,省略则取当前页。"},page:{type:"integer",minimum:1},lang:{type:"string",description:"首选语言,如 ai-zh / zh-CN / en-US。省略则优先中文。"},format:{type:"string",enum:["raw","text","srt","lrc"],default:"text"}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveBvid(i&&i.bvid);if(!e)return biliOut("无法确定 bvid。");const t=i&&i.format||"text",r=await biliResolveCid(e,i&&i.page);if(!r.cid)return biliOut("无法为 "+e+" 解析出 cid。");const n=biliRequireOk(await biliApiGet("/x/player/wbi/v2",{bvid:e,cid:r.cid},!1),"player/wbi/v2")||{},a=n.subtitle&&n.subtitle.subtitles||[];if(!a.length)return biliOut(n.need_login_subtitle?"该视频("+e+")无可见字幕,且 need_login_subtitle=true,可能需登录后再试。":"该视频("+e+", cid="+r.cid+")没有任何字幕轨。");const o=i&&i.lang,l=o&&a.find(i=>i.lan===o)||o&&a.find(i=>String(i.lan).includes(o))||a.find(i=>String(i.lan).includes("zh"))||a[0],s=await fetch(biliNormalizeUrl(l.subtitle_url),{credentials:"omit"}),c=(await s.json()).body||[];return biliOut("# bvid="+e+" cid="+r.cid+" title="+(r.title||"")+"\n# picked="+l.lan+"("+l.lan_doc+") available=["+a.map(i=>i.lan).join(", ")+"] cues="+c.length+" format="+t+"\n\n"+biliFormatBody(c,t))}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_save_subtitle",description:"Like bili_get_subtitle but triggers a browser file download (txt/srt/lrc) instead of returning text inline.",inputSchema:{type:"object",properties:{bvid:{type:"string"},page:{type:"integer",minimum:1},lang:{type:"string"},format:{type:"string",enum:["text","srt","lrc"],default:"srt"}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveBvid(i&&i.bvid);if(!e)return biliOut("无法确定 bvid。");const t=i&&i.format||"srt",r=await biliResolveCid(e,i&&i.page);if(!r.cid)return biliOut("无法解析 cid。");const n=(((await biliApiGet("/x/player/wbi/v2",{bvid:e,cid:r.cid},!1)).data||{}).subtitle||{}).subtitles||[];if(!n.length)return biliOut("没有字幕轨可导出。");const a=i&&i.lang,o=a&&n.find(i=>i.lan===a)||n.find(i=>String(i.lan).includes("zh"))||n[0],l=await fetch(biliNormalizeUrl(o.subtitle_url),{credentials:"omit"}),s=(await l.json()).body||[],c=biliFormatBody(s,t),u="text"===t?"txt":t,d=e+"_"+o.lan+"."+u,p=new Blob([c],{type:"text/plain;charset=utf-8"}),m=document.createElement("a");return m.href=URL.createObjectURL(p),m.download=d,document.body.appendChild(m),m.click(),setTimeout(()=>{URL.revokeObjectURL(m.href),m.remove()},0),biliOut("已触发下载:"+d+"("+o.lan+","+s.length+" 条,"+t+")。")}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_download_video",description:"Download a Bilibili video as a single MP4 in the browser (no yt-dlp). Uses HTML5 playurl (platform=html5), max ~720p. preview=true only returns playurl. force skips paid-content check.",inputSchema:{type:"object",properties:{bvid:{type:"string",description:"BV 号 / URL,省略则取当前页。"},page:{type:"integer",minimum:1,description:"分 P 序号,从 1 开始。"},quality:{type:"string",enum:["360p","480p","720p","1080p","best"],default:"720p",description:"目标清晰度。HTML5 合流最高约 720P,选 1080p 也会落到 720P。"},preview:{type:"boolean",description:"只解析播放地址和大小,不真正下载。"},force:{type:"boolean",description:"跳过付费/大会员预检。"}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveBvid(i&&i.bvid);if(!e)return biliOut("无法确定 bvid。");const t=await biliResolveCid(e,i&&i.page);if(!t.cid)return biliOut("无法解析 cid。");await biliAssertPlayable(t.data,i&&i.force);const r=i&&i.quality||"720p",n=BILI_QN[r]||64,a=await biliGetHtml5Playurl(e,t.cid,n),o=a.durl||[];if(!o.length)return biliOut("未拿到 HTML5 合流地址(可能是大会员/DASH-only 视频)。");const l=t.part&&t.part.part||t.title||e,s=biliSanitizeFilename(e+"_"+l)+".mp4",c=o.reduce((i,e)=>i+(Number(e.size)||0),0),u=a.quality,d=u<80&&("1080p"===r||"best"===r)?"HTML5 合流最高约 720P,已按可下载的最高档获取(浏览器无法合并 DASH 音视频)。":"",p={bvid:e,title:l,cid:t.cid,filename:s,requested:r,quality:u,format:a.format,size:c,accept:a.accept_description,url:o[0].url,segments:o.length,note:d};if(i&&i.preview)return biliOut(Object.assign({status:"preview"},p));if(o.length>1)return biliOut(Object.assign({status:"multi-segment",message:"该清晰度拆成多段 FLV/MP4,请用 preview 里的 durl 逐段保存,或换 360p。",durl:o.map(i=>({order:i.order,size:i.size,url:i.url}))},p));const m=await biliSaveUrlToFile(o[0].url,s,"video/mp4");return"url-only"===m.method?biliOut(Object.assign({status:"need-save",message:"文件约 "+Math.round(c/1024/1024)+"MB,超过浏览器内存兜底阈值。已返回直链:请新标签打开 url 另存,或在有保存对话框的手势下重试。"},p)):biliOut(Object.assign({status:"ok",method:m.method,bytes:m.bytes},p))}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_summary",description:"Get Bilibili's official AI summary (视频页「AI总结」) via WBI-signed GET /x/web-interface/view/conclusion/get. Includes outline timestamps.",inputSchema:{type:"object",properties:{bvid:{type:"string",description:"BV 号 / URL,省略则取当前页。"},page:{type:"integer",minimum:1}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveBvid(i&&i.bvid);if(!e)return biliOut("无法确定 bvid。");const t=await biliResolveCid(e,i&&i.page),r=t.data&&t.data.owner&&t.data.owner.mid;if(!t.cid||!r)throw new Error("view API 未返回 cid/up_mid");const n=biliRequireOk(await biliApiGet("/x/web-interface/view/conclusion/get",{bvid:e,cid:t.cid,up_mid:r},!0),"conclusion")||{};if(0!==n.code)return biliOut("该视频没有官方 AI 总结:"+e);let a=n.model_result;"string"==typeof a&&(a=JSON.parse(a));const o=String(a&&a.summary||"").trim();if(!o)return biliOut("该视频没有官方 AI 总结:"+e);const l=[{time:"",content:o}];return(a.outline||[]).forEach(i=>{const e=String(i&&i.title||"").trim();e&&l.push({time:biliFmtClock(i.timestamp),content:"# "+e}),(i.part_outline||[]).forEach(i=>{const e=String(i&&i.content||"").trim();e&&l.push({time:biliFmtClock(i.timestamp),content:e})})}),biliOut({bvid:e,title:t.title,rows:l})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_list_comments",description:"Fetch Bilibili video comments. Omit parent for top-level (hot) comments; pass parent rpid for nested replies.",inputSchema:{type:"object",properties:{bvid:{type:"string",description:"BV 号 / URL,省略则取当前页。"},parent:{type:"integer",description:"顶层评论 rpid,传入则拉该评论的楼中楼"},limit:{type:"integer",minimum:1,maximum:50,default:20}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveBvid(i&&i.bvid);if(!e)return biliOut("无法确定 bvid。");const t=Math.min(Number(i&&i.limit||20),50),r=(await biliResolveCid(e)).aid;if(!r)throw new Error("无法解析 aid");const n=i&&i.parent,a=null!=n?await biliApiGet("/x/v2/reply/reply",{oid:r,type:1,root:n,pn:1,ps:t},!1):await biliApiGet("/x/v2/reply/main",{oid:r,type:1,mode:3,ps:t},!1);return biliOut(((biliRequireOk(a,"comments")||{}).replies||[]).slice(0,t).map((i,e)=>({rank:e+1,rpid:String(i.rpid),author:i.member&&i.member.uname||"",text:String(i.content&&i.content.message||"").replace(/\n/g," ").trim(),likes:i.like||0,replies:i.rcount||0,time:i.ctime?new Date(1e3*i.ctime).toISOString().slice(0,16).replace("T"," "):""})))}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_feed",description:"Dynamic timeline. Omit uid for the logged-in following feed; pass uid/username for that user's space dynamics. type: all|video|article|draw|text.",inputSchema:{type:"object",properties:{uid:{type:"string",description:"用户 UID/用户名。省略则查关注时间线(需登录)。"},limit:{type:"integer",minimum:1,maximum:50,default:20},type:{type:"string",enum:["all","video","article","draw","text"],default:"all"},pages:{type:"integer",minimum:1,maximum:5,default:1,description:"翻页次数,每页约 20 条"}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=Math.min(Number(i&&i.limit||20),50),t=Math.min(Number(i&&i.pages||1),5),r=i&&i.type&&"all"!==i.type?i.type:"",n=!(!i||!i.uid),a=n?await biliResolveUid(i.uid):null,o=[];let l="";for(let i=0;i<t&&o.length<e;i++){const t={timezone_offset:-480};let s;l&&(t.offset=l),n?(t.host_mid=a,s=await biliApiGet("/x/polymer/web-dynamic/v1/feed/space",t,!1)):(t.type=r||"all",t.page=i+1,s=await biliApiGet("/x/polymer/web-dynamic/v1/feed/all",t,!1));const c=biliRequireOk(s,"feed")||{},u=c.items||[];if(!u.length)break;for(const i of u){if(o.length>=e)break;const t=biliParseFeedItem(i);r&&t.type!==r||(t.rank=o.length+1,o.push(t))}if(l=c.offset||u[u.length-1]&&u[u.length-1].id_str||"",!l||!c.has_more)break}return biliOut({source:n?"user:"+a:"following",count:o.length,items:o})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_feed_detail",description:"Get a Bilibili dynamic/post detail by id (from feed url https://t.bilibili.com/{id}). Supports 充电专属 when logged in.",inputSchema:{type:"object",properties:{id:{type:"string",description:"动态 ID,可从 feed 返回的 url 中取得"}},required:["id"]},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=String(i&&i.id||"").replace(/.*t\.bilibili\.com\//,"").trim(),t=(biliRequireOk(await biliApiGet("/x/polymer/web-dynamic/v1/detail",{id:e,timezone_offset:-480},!1),"feed detail")||{}).item;if(!t)return biliOut("动态不存在或无权查看");const r=t.modules||{},n=r.module_author||{},a=r.module_dynamic||{},o=a.major||{},l=r.module_stat||{},s={id:t.id_str||e,author:n.name,time:n.pub_time,type:BILI_FEED_TYPE[t.type]||t.type,text:a.desc&&a.desc.text?biliStripHtml(a.desc.text):"",likes:l.like&&l.like.count||0,comments:l.comment&&l.comment.count||0,forwards:l.forward&&l.forward.count||0,url:"https://t.bilibili.com/"+(t.id_str||e)};return o.archive&&(s.video_title=o.archive.title,s.video_url=o.archive.jump_url?"https:"+o.archive.jump_url:""),o.article&&(s.article_title=o.article.title,s.article_url=o.article.jump_url?"https:"+o.article.jump_url:""),o.draw&&o.draw.items&&(s.images=o.draw.items.map(i=>i.src)),o.opus&&(s.opus_title=o.opus.title,s.opus_text=o.opus.summary&&o.opus.summary.text?biliStripHtml(o.opus.summary.text):""),t.orig&&(s.forward_from=t.orig.modules&&t.orig.modules.module_author?t.orig.modules.module_author.name:""),biliOut(s)}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_get_history",description:"Get the logged-in user's watch history (archive). Requires login.",inputSchema:{type:"object",properties:{limit:{type:"integer",minimum:1,maximum:30,default:20}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=Math.min(Number(i&&i.limit||20),30),t=await biliApiGet("/x/web-interface/history/cursor",{ps:e,type:"archive"},!1);return biliOut((biliRequireOk(t,"history").list||[]).slice(0,e).map((i,e)=>{const t=i.progress||0,r=i.duration||0;let n;if(t<0||r&&t>=r)n="已看完";else{const i=r>0?Math.round(t/r*100):0;n=biliFmtClock(t)+"/"+biliFmtClock(r)+" ("+i+"%)"}const a=i.history&&i.history.bvid;return{rank:e+1,title:i.title,author:i.author_name,progress:n,bvid:a,url:a?"https://www.bilibili.com/video/"+a:""}}))}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_list_following",description:"List users that a Bilibili account is following. Defaults to the logged-in user. Max 50 per page.",inputSchema:{type:"object",properties:{uid:{type:"string",description:"目标用户 UID/用户名,默认当前登录用户"},page:{type:"integer",minimum:1,default:1},limit:{type:"integer",minimum:1,maximum:50,default:50}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=i&&i.uid?await biliResolveUid(i.uid):String((await biliGetSelf()).mid),t=i&&i.page||1,r=Math.min(Number(i&&i.limit||50),50),n=biliRequireOk(await biliApiGet("/x/relation/followings",{vmid:e,pn:t,ps:r,order:"desc"},!1),"followings")||{},a=n.list||[];return a.length?biliOut({total:n.total,list:a.map(i=>({mid:i.mid,name:i.uname,sign:(i.sign||"").slice(0,40),following:6===i.attribute?"互相关注":"已关注",official:i.official_verify&&i.official_verify.desc||"",url:"https://space.bilibili.com/"+i.mid}))}):biliOut({total:n.total||0,list:[]})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_list_favorites",description:"List the logged-in user's favorite folders, or the videos inside a folder (fid). Requires login. Omit fid to use the default folder.",inputSchema:{type:"object",properties:{fid:{type:"integer",description:"收藏夹 media_id。省略则先列出收藏夹,再用默认夹拉内容。"},foldersOnly:{type:"boolean",description:"为 true 时只返回收藏夹列表,不拉内容"},limit:{type:"integer",minimum:1,maximum:40,default:20},page:{type:"integer",minimum:1,default:1}}},annotations:{readOnlyHint:"true",destructiveHint:"false"},execute:async i=>{try{const e=await biliGetSelf(),t=(biliRequireOk(await biliApiGet("/x/v3/fav/folder/created/list-all",{up_mid:e.mid},!1),"fav folders")||{}).list||[];if(i&&i.foldersOnly)return biliOut(t.map(i=>({id:i.id,fid:i.fid,title:i.title,media_count:i.media_count})));const r=i&&i.fid||t[0]&&t[0].id;if(!r)return biliOut({folders:t,medias:[]});const n=Math.min(Number(i&&i.limit||20),40),a=(biliRequireOk(await biliApiGet("/x/v3/fav/resource/list",{media_id:r,pn:i&&i.page||1,ps:n},!1),"fav list").medias||[]).slice(0,n).map((i,e)=>({rank:e+1,title:i.title,author:i.upper&&i.upper.name,plays:i.cnt_info&&i.cnt_info.play,bvid:i.bvid,url:i.bvid?"https://www.bilibili.com/video/"+i.bvid:""}));return biliOut({fid:r,folders:t,count:a.length,medias:a})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_post_comment",description:"Post a top-level comment or a reply on a Bilibili video via POST /x/v2/reply/add. Requires login. @username mentions are resolved to real mentions. WRITE — confirm with the user before calling.",inputSchema:{type:"object",properties:{bvid:{type:"string",description:"BV 号 / URL,省略则取当前页。"},message:{type:"string",description:"评论正文。文中 @用户名 会被解析为真实提及。"},parent:{type:"integer",description:"回复的顶层评论 rpid。省略则发顶层评论。"}},required:["message"]},annotations:{readOnlyHint:"false",destructiveHint:"false"},execute:async i=>{try{const e=String(i&&i.message||"").trim();if(!e)return biliOut("message 不能为空");const t=await biliResolveBvid(i&&i.bvid);if(!t)return biliOut("无法确定 bvid。");const r=(await biliResolveCid(t)).aid;if(!r)throw new Error("无法解析 aid");const n={},a=/@([^\s@]+)/g;let o;for(;o=a.exec(e);){const i=o[1];if(!n[i])try{n[i]=Number(await biliResolveUid(i))}catch(i){}}const l={oid:r,type:1,message:e,plat:1};i&&i.parent&&(l.root=i.parent,l.parent=i.parent),Object.keys(n).length&&(l.at_name_to_mid=JSON.stringify(n));const s=await biliApiPost("/x/v2/reply/add",l),c=(biliRequireOk(s,"reply add")||{}).rpid;if(!c)throw new Error("未返回 rpid");return biliOut({rpid:String(c),bvid:t,oid:String(r),message:e,url:"https://www.bilibili.com/video/"+t+"#reply"+c})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_follow_user",description:"Follow a Bilibili user via POST /x/relation/modify (act=1, re_src=11). Target can be uid, username, or space URL. Idempotent if already following. WRITE — confirm first.",inputSchema:{type:"object",properties:{target:{type:"string",description:"目标 UID / 用户名 / space.bilibili.com 链接"}},required:["target"]},annotations:{readOnlyHint:"false",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveUid(i&&i.target),t=await biliGetSelf();if(String(e)===String(t.mid))return biliOut("不能关注自己");const r=biliRequireOk(await biliApiGet("/x/relation",{fid:e},!1),"relation"),n="https://space.bilibili.com/"+e;if(2===r.attribute||6===r.attribute)return biliOut({mid:e,status:"already-following",url:n});if(128===r.attribute)throw new Error("该用户已被拉黑,请先取消拉黑再关注");return biliRequireOk(await biliApiPost("/x/relation/modify",{fid:e,act:1,re_src:11}),"relation modify"),biliOut({mid:e,status:"followed",url:n})}catch(i){return biliErr(i)}}}),navigator.modelContext.registerTool({name:"bili_unfollow_user",description:"Unfollow a Bilibili user via POST /x/relation/modify (act=2, re_src=11). Idempotent if not following. WRITE — confirm first.",inputSchema:{type:"object",properties:{target:{type:"string",description:"目标 UID / 用户名 / space.bilibili.com 链接"}},required:["target"]},annotations:{readOnlyHint:"false",destructiveHint:"false"},execute:async i=>{try{const e=await biliResolveUid(i&&i.target),t=await biliGetSelf();if(String(e)===String(t.mid))return biliOut("不能取关自己");const r=biliRequireOk(await biliApiGet("/x/relation",{fid:e},!1),"relation"),n="https://space.bilibili.com/"+e;if(2!==r.attribute&&6!==r.attribute)return biliOut({mid:e,status:"not-following",url:n});return biliRequireOk(await biliApiPost("/x/relation/modify",{fid:e,act:2,re_src:11}),"relation modify"),biliOut({mid:e,status:"unfollowed",url:n})}catch(i){return biliErr(i)}}}));
|