feat: 添加 Native Messaging 桥接,支持 CLI 和 AI Agent 控制扩展#1143
feat: 添加 Native Messaging 桥接,支持 CLI 和 AI Agent 控制扩展#1143LeiShi1313 wants to merge 21 commits intopt-plugins:masterfrom
Conversation
- New nativeMessaging.ts bridge in background script that connects to a native host via chrome.runtime.connectNative(), sends a hello handshake with instance identity, and proxies an allowlisted set of methods through the existing sendMessage() system - Add nativeMessaging permission to manifest - Fix null-safety in search handler for uninitialized config storage - Prevent reconnect loop when native host is not installed
Reviewer's GuideAdds a Chrome Native Messaging bridge from the background script to a local native host, enabling CLI/AI Agent control of the extension while keeping behavior unchanged when the host is absent, and includes a small search null-safety fix and a manifest permission update. Sequence diagram for CLI/AI Agent request via Native Messaging bridgesequenceDiagram
actor User
participant AIAgent
participant CLI as ptd_cli
participant Host as NativeMessagingHost
participant BG as BackgroundScript
participant Offscreen as OffscreenSearch
participant Site as PT_Site
User->>AIAgent: Natural language request
AIAgent->>CLI: Invoke ptd command
CLI->>Host: JSON request {id, method, params}
Host->>BG: Native message request
Note over BG: On startup, BG connectsNative and sends hello {instanceId, browser, extensionId, version, capabilities}
BG->>BG: Validate method in ALLOWED_METHODS
alt Method allowed
BG->>Offscreen: setupOffscreenDocument
Offscreen->>Site: Perform search/download/user_info etc
Site-->>Offscreen: Result
Offscreen-->>BG: Result
BG->>Host: Response {type: response, id, result}
Host->>CLI: Response
CLI->>AIAgent: Parsed result
AIAgent->>User: Answer and follow up actions
else Method not allowed
BG->>Host: Response {error: METHOD_NOT_ALLOWED}
Host->>CLI: Error
end
Class diagram for Native Messaging bridge moduleclassDiagram
class NativeMessagingBridge {
<<module>>
+string NATIVE_HOST_NAME
+string INSTANCE_ID_KEY
+number RECONNECT_BASE_MS
+number RECONNECT_MAX_MS
+Set~string~ ALLOWED_METHODS
+string[] FATAL_ERRORS
+number reconnectAttempt
+Promise~string~ getOrCreateInstanceId()
+void connect()
}
class BackgroundMain {
<<module>>
+NativeMessagingBridge import
}
class OffscreenSearch {
<<module>>
+onMessage_getSiteSearchResult()
+configStorage_optional_access
}
NativeMessagingBridge --> BackgroundMain : used_by
BackgroundMain --> OffscreenSearch : sendMessage_calls
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
很有意思的玩法,也符合目前 AI+CLI 的大趋势。。 几点想法,供mr前参考:
|
Replace auto-connect-on-import with a full lifecycle manager: - Checks chrome.permissions.contains before connecting - Adds ptd_native_bridge_enabled storage flag (default true) for user control - Explicit state machine: no-permission → disabled → connecting → connected → retrying → error - Differentiates intentional vs unexpected disconnects (no retry vs backoff) - Listens for chrome.permissions.onAdded/onRemoved for reactive updates - Exposes nativeBridgeGetStatus, nativeBridgeSetEnabled, nativeBridgeReconnect message handlers - Treats "host not found" / "forbidden" as fatal; "host exited" as recoverable with backoff
State should remain "connecting" after port creation until the first valid message exchange confirms the host is responsive. The previous code immediately set state to "connected" before any handshake completed, giving a false positive in the UI.
…result - Add MAX_RECONNECT_ATTEMPTS (10) to prevent infinite reconnect loops on unclassified permanent failures. After exhausting retries, state transitions to "error" requiring explicit user action to retry. - Test Connection now polls for up to 5s after triggering reconnect, waiting for the async connection to settle before showing the result.
The native host daemon does not send an acknowledgment after receiving the hello. Transition to connected after successful postMessage of the hello handshake — if the host is absent, onDisconnect will correct the state to error.
Firefox uses 'ptd install --browser firefox' without extension ID. Chrome-family uses '--browser chrome --extension-id <id>'.
Lightweight endpoints for CLI discovery — return only IDs and names instead of the full metadata blob which exceeds native messaging limits.
|
|
我蠢了,同时装了2个扩展所以通讯出问题了。起来再测下Edge,FireFox就差不多了 |
|
感谢你的 PR,但是我对该 PR 的部分描述有一些疑惑:
我对 “可被 AI 驱动” 这个说辞持保留意见,不过我可以理解为此 PR 的核心意图是让 ptd 能通过 CLI 被外部程序(包括 AI Agent)调用,从而实现一定程度的自动化。但是社区内已有成熟的自动化抓取程序(*arr 生态),它们无需 相比之下,我认为一个需要下载额外 cli、自己编写脚本,甚至需要借助 AI Agent 的自动化方案的
感谢你愿意把整个项目贡献出来,但是 ptd-cli 作为一个技术栈不同的外部程序,现有维护者对其的维护能力有限,我认为并不适合将其立即作为官方 CLI,目前这个 PR 把该 CLI 和扩展的功能绑定得比较紧,间接增加了扩展的维护成本。 以上仅为我个人看法,如有考虑不周的地方,欢迎指正和继续讨论。 |
|
@socketcat AI Agent对于个人用户来说,最大的便利不是自动化,是人机交互的革新,这次变革肯定不会亚于智能手机对人生活的改变。看看下面几个例子就能明白了 即使从自动化角度来说这种程度的智能和以前那种靠字符串拼接和靠豆瓣/imdb id做的匹配也完全不是一回事。配合OpenClaw的定时功能,用户可以很方便地追剧,追热剧,发两条消息就全搞定了。
|
|
不知道怎么说,但我觉得你说的可能是有道理。PT 对每个人来说,意义不同。我不会尝试为某个人的用法做任何辩解,因为那是徒劳。我在乎的是,我在开发者之前,首先是用户,而我相信大多数用户在乎的不是潮流,而是安逸。而可控性是安全感的关键一环。如果你不认同,你可以选择无视,我不会阻挠。如果你认同,你可以试着重新思考一下。 差不多就这些 |
| if (__BROWSER__ === "firefox") { | ||
| return "ptd install --browser firefox"; | ||
| } | ||
| return `ptd install --browser chrome --extension-id ${extensionId}`; |
There was a problem hiding this comment.
提示命令只有 chrome ,如果 Chromium / Edge 的注册方法不同,需要额外增加浏览器类型判断(比如根据UA信息)
| type BridgeState = "no-permission" | "disabled" | "connecting" | "connected" | "retrying" | "error"; | ||
|
|
||
| interface BridgeStatus { | ||
| permissionGranted: boolean; | ||
| enabled: boolean; | ||
| state: BridgeState; | ||
| connected: boolean; | ||
| lastError?: string; | ||
| } |
There was a problem hiding this comment.
这块类型定义再 background/utils/nativeMessaging.ts , messages.ts 和 vue 模板等地方中重复声明了,都抽象出来,放在 shared/common 目录下,并在 shared/types.ts 中增加代理转发
src/entries/messages.ts
Outdated
| getDownloaderList(): Array<{ id: string; name: string; type: string; enabled: boolean; address: string }>; | ||
|
|
||
| // 2.9 Native messaging bridge control | ||
| nativeBridgeGetStatus(): { |
| ]; | ||
|
|
||
| /** Methods the bridge will proxy to sendMessage(). Everything else is rejected. */ | ||
| const ALLOWED_METHODS = new Set([ |
There was a problem hiding this comment.
CLI 能够使用的 sendMessage methods 有必要进一步细分吗?比如按 site, search, download, userInfo, keepUpload 这种大类?
There was a problem hiding this comment.
这现阶段应该问题不大,还没有那么多方法到让人困惑的地步
src/entries/offscreen/utils/site.ts
Outdated
| id, | ||
| name: nameMap[id] ?? "", | ||
| url: config.url ?? "", | ||
| offline: config.isOffline ?? false, |
There was a problem hiding this comment.
isOffline 并不一定能完全表示站点无法使用。 ISiteMetadata 定义中还声明了一个 isDead 的参数,在 offscreen 中可以通过 getDefinedSiteMetadata 方法获取到
const siteMetaData = await getDefinedSiteMetadata(siteId);
const isDeadSite = siteMetaData.isDead ?? false;
const siteOffline = (isDeadSite || config.isOffline) ?? false;
src/entries/offscreen/utils/site.ts
Outdated
| const nameMap = metadata?.siteNameMap ?? {}; | ||
| return Object.entries(sites).map(([id, config]) => ({ | ||
| id, | ||
| name: nameMap[id] ?? "", |
There was a problem hiding this comment.
如果nameMap中没有,先回落到 config.merge?.name 如果这里还没有,建议使用 id,或者和下面一样从 siteMetaData 中拿,而不是取空字符串。即
{
name: nameMap[id] ?? config.merge?.name ?? id
}|
|
||
| function getStatus() { | ||
| return { | ||
| permissionGranted, |
There was a problem hiding this comment.
此处的 permissionGranted 建议动态获取,因为下面的 onAdded 以及 onRemoved 在重复授权的情况下不会触发,在极端条件下会导致已授权的在设置页面仍然显示未授权(需要reload插件解决)
| return id; | ||
| } | ||
|
|
||
| function getStatus() { |
There was a problem hiding this comment.
建议再增加一个 port 的状态,检查是不是为 null
There was a problem hiding this comment.
Pull request overview
Adds a Chrome/Firefox Native Messaging bridge so an external CLI/agent can communicate with the extension’s background APIs (reusing existing login state, site definitions, and downloader config), plus a small offscreen null-guard fix and an options UI entry to manage the bridge permission/status.
Changes:
- Add a background Native Messaging bridge with reconnect logic and a method whitelist.
- Add options UI + i18n strings for granting/revoking
nativeMessagingpermission and viewing bridge status. - Add lightweight list endpoints (
getSiteList,getDownloaderList) and fix a null access in offscreen search.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Declares nativeMessaging as an optional permission for both Chrome/Firefox builds. |
| src/locales/zh_CN.json | Adds Settings tab label + Native Bridge UI translations (zh-CN). |
| src/locales/en.json | Adds Settings tab label + Native Bridge UI translations (EN). |
| src/entries/options/views/Settings/SetBase/NativeBridgeWindow.vue | New settings panel to request/revoke permission, toggle bridge, and test connection. |
| src/entries/options/views/Settings/SetBase/Index.vue | Hides the global “Save” button for tabs that opt out (used by Native Bridge page). |
| src/entries/options/plugins/router.ts | Adds the native-bridge settings route and redirects /settings → /set-base. |
| src/entries/offscreen/utils/site.ts | Adds getSiteList message handler for CLI discovery. |
| src/entries/offscreen/utils/download.ts | Adds getDownloaderList message handler for CLI discovery. |
| src/entries/offscreen/utils/search.ts | Guards configStorage access to prevent null/undefined crashes. |
| src/entries/messages.ts | Extends ProtocolMap with list endpoints + native bridge control endpoints. |
| src/entries/background/utils/nativeMessaging.ts | New Native Messaging bridge implementation (connect/handshake, whitelist proxying, reconnect, status/control handlers). |
| src/entries/background/main.ts | Loads the new native messaging module in background startup. |
| package.json | Bumps extension version to 0.0.6. |
- Extract BridgeState/BridgeStatus to shared/types/common/nativeBridge.ts - Dynamic permission check via chrome.permissions.contains() in getStatus() - Port race safety: capture currentPort in connect(), guard all listeners - Hello handshake error handling with try/catch and .catch() - Validate boolean input in nativeBridgeSetEnabled - Remove getExtStorage/getLogger from bridge allowlist (security) - getSiteList: include isDead from site metadata, fallback name to id - getDownloaderList: remove address field (may contain credentials) - Browser detection via UA for setup command (chrome/chromium/edge/firefox)
…statement - Restore address field in getDownloaderList per reviewer request - Add Native Messaging privacy disclosure to privacy-statement.md (section 四) - Add privacy notice in NativeBridgeWindow.vue info section - i18n keys for privacy text in both en.json and zh_CN.json






为 PT-Depiler 添加 Chrome Native Messaging 支持,使终端工具和 AI Agent
能够通过命令行与浏览器扩展通信,复用扩展已有的登录态、站点定义和下载器配置。
改动内容
配套 CLI 工具
配合本 PR,我开发了 https://github.com/LeiShi1313/ptd-cli (Rust 编写,跨平台支持
Linux/macOS/Windows),可以在终端中完成以下操作:
跨站搜索种子
ptd search "阿凡达"
搜索指定站点
ptd search "avatar" --site chdbits --site btschool
将搜索结果推送到下载器
ptd download 0 --downloader <下载器ID>
查看站点用户数据(做种数、上传量、魔力值等)
ptd user-info current chdbits
查看所有站点的历史数据
ptd user-info history chdbits
管理下载器
ptd downloader status <下载器ID>
管理辅种任务
ptd keep-upload list
AI Agent 集成
本 PR 最大的意义在于:任何 AI Agent 都可以通过 ptd-cli 用自然语言控制 PT-Depiler 扩展。
例如,用户可以在 OpenClaw/Claude Code/Codex 或其他 AI 编码助手中直接说:
▎ "帮我在所有站点搜索 4K 版本的《流浪地球2》,找到体积最小的那个,推送到 qBittorrent"
AI Agent 会自动调用 ptd search、分析结果、选择最优种子、调用 ptd download 完成整个流程。
这使得 PT-Depiler 从一个浏览器扩展进化为一个可编程的、可被 AI 驱动的 PT 站点管理平台。
安全性
对现有功能的影响
目前 https://github.com/LeiShi1313/ptd-cli 在我的个人仓库下开发。如果本 PR 被接受,我很乐意将 ptd-cli 项目转移到
https://github.com/pt-plugins 组织下,作为 PT-Depiler 的官方 CLI 工具统一维护。
Summary by Sourcery
Add a Chrome Native Messaging bridge to allow external CLI or agents to call a safe subset of PT-Depiler background APIs while keeping existing extension behavior unchanged when the native host is absent.
New Features:
Bug Fixes:
Build: