From b53000ef0e88a4b49e64599fa0d8ce9c35bff8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morning=C2=B7Galaxy=20Dawn?= <3184876@qq.com> Date: Mon, 21 Sep 2026 10:42:03 +0800 Subject: [PATCH] feat(pi.agents-anywhere): add Agents Anywhere remote control plugin - Add pi.agents-anywhere plugin source and views - Add packaged pi.agents-anywhere-1.0.0.piplug - Register plugin in marketplace catalog.json - Add automated verification tests --- catalog.json | 47 ++- packages/pi.agents-anywhere-1.0.0.piplug | Bin 0 -> 21903 bytes plugins/pi.agents-anywhere/README.md | 48 +++ plugins/pi.agents-anywhere/main.js | 381 ++++++++++++++++++++ plugins/pi.agents-anywhere/manifest.json | 53 +++ plugins/pi.agents-anywhere/views/panel.html | 187 ++++++++++ tests/agents-anywhere.test.mjs | 44 +++ 7 files changed, 759 insertions(+), 1 deletion(-) create mode 100644 packages/pi.agents-anywhere-1.0.0.piplug create mode 100644 plugins/pi.agents-anywhere/README.md create mode 100644 plugins/pi.agents-anywhere/main.js create mode 100644 plugins/pi.agents-anywhere/manifest.json create mode 100644 plugins/pi.agents-anywhere/views/panel.html create mode 100644 tests/agents-anywhere.test.mjs diff --git a/catalog.json b/catalog.json index 79dfc14..5a0469a 100644 --- a/catalog.json +++ b/catalog.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "providerId": "official", "name": "PI-Desktop Official Plugins", - "updatedAt": "2026-09-20T17:49:14Z", + "updatedAt": "2026-09-21T02:39:43Z", "homepage": "https://github.com/vastsa/pi-desktop-plugins", "plugins": [ { @@ -403,6 +403,51 @@ } ] }, + { + "id": "pi.agents-anywhere", + "name": "Agents Anywhere \u8fdc\u7a0b\u52a9\u624b", + "description": "\u57fa\u4e8e Agents Anywhere \u5e73\u53f0\u5b9e\u73b0\u624b\u673a\u7aef\u5bf9 Pi Desktop \u7684\u8fdc\u7a0b\u67e5\u770b\u3001\u53d1\u95ee\u4e0e\u63a7\u5236\u3002", + "i18n": { + "en": { + "name": "Agents Anywhere Remote", + "description": "Remote control and sync for Pi Desktop from mobile via Agents Anywhere.", + "safetyNotes": "Connector credentials stay on this local machine. Network communication connects strictly to the official/configured Agents Anywhere relay servers over encrypted WSS/HTTPS. Remote actions reuse local session permissions and never bypass host boundaries." + }, + "zh-CN": { + "name": "Agents Anywhere \u8fdc\u7a0b\u52a9\u624b", + "description": "\u57fa\u4e8e Agents Anywhere \u5e73\u53f0\u5b9e\u73b0\u624b\u673a\u7aef\u5bf9 Pi Desktop \u7684\u8fdc\u7a0b\u67e5\u770b\u3001\u53d1\u95ee\u4e0e\u63a7\u5236\u3002", + "safetyNotes": "\u8fde\u63a5\u51ed\u636e\u4ec5\u4fdd\u5b58\u5728\u672c\u673a\uff1b\u7f51\u7edc\u901a\u4fe1\u4e25\u683c\u9650\u5236\u5728\u5b98\u65b9\u6216\u7528\u6237\u914d\u7f6e\u7684 Agents Anywhere \u4e2d\u7ee7\u670d\u52a1\u5668\uff08\u901a\u8fc7\u52a0\u5bc6 WSS/HTTPS \u4f20\u8f93\uff09\uff1b\u8fdc\u7a0b\u6267\u884c\u7684\u6307\u4ee4\u5b8c\u5168\u590d\u7528\u672c\u5730\u4f1a\u8bdd\u6743\u9650\u4f53\u7cfb\uff0c\u7edd\u4e0d\u7ed5\u8fc7\u5b89\u5168\u6c99\u7bb1\u3002" + } + }, + "author": "Anywhere-Labs & PI-Desktop", + "categories": [ + "community" + ], + "verified": true, + "downloads": 0, + "homepage": "https://github.com/vastsa/pi-desktop-plugins/tree/main/plugins/pi.agents-anywhere", + "repository": "https://github.com/vastsa/pi-desktop-plugins", + "readmeMarkdown": "# Agents Anywhere Remote Plugin for Pi-Desktop\n\n\u672c\u63d2\u4ef6\u57fa\u4e8e\u5f00\u6e90\u8de8\u8bbe\u5907 Agent \u5de5\u4f5c\u53f0 **[Agents Anywhere](https://www.agents-anywhere.com/)**\uff08[GitHub \u4ed3\u5e93](https://github.com/anywhere-labs/Agents-Anywhere)\uff09\uff0c\u4e3a **Pi-Desktop** \u63d0\u4f9b\u624b\u673a\u7aef\uff08iOS / Android / Web\uff09\u8fdc\u7a0b\u67e5\u770b\u3001\u5b9e\u65f6\u4f1a\u8bdd\u63a7\u5236\u4e0e\u4ea4\u4e92\u5ba1\u6279\u80fd\u529b\u3002\n\n---\n\n## \u4e00\u3001\u529f\u80fd\u7279\u6027\n\n- **\u8de8\u8bbe\u5907\u53cd\u5411\u96a7\u9053**\uff1a\u57fa\u4e8e WSS \u957f\u8fde\u63a5\u5b9e\u73b0\u5185\u7f51\u7a7f\u900f\uff0c\u65e0\u9700\u914d\u7f6e\u516c\u7f51 IP \u6216\u6253\u6d1e\u7aef\u53e3\u3002\n- **\u53cc\u5411\u4f1a\u8bdd\u6d41\u540c\u6b65**\uff1a\u624b\u673a\u7aef\u4e0e PC \u684c\u9762\u7aef\u53cc\u5411\u540c\u6b65\u4f1a\u8bdd\u5386\u53f2\u3001\u8fd0\u884c\u4e2d\u72b6\u6001\u4e0e\u5b9e\u65f6\u54cd\u5e94\u3002\n- **\u601d\u8003\u6d41\u5f0f\u5c55\u5f00\u4e0e\u6298\u53e0**\uff1a\u624b\u673a\u7aef\u5b8c\u6574\u5c55\u793a Agent \u6df1\u5ea6\u601d\u8003\u94fe\uff08Chain of Thought\uff09\uff0c\u652f\u6301\u6253\u5b57\u65f6\u5c55\u5f00\u3001\u751f\u6210\u7b54\u6848\u540e\u81ea\u52a8\u6298\u53e0\u3002\n- **\u8fdc\u7a0b\u64cd\u4f5c\u63a7\u5236**\uff1a\u5728\u624b\u673a\u4e0a\u968f\u65f6\u53d1\u8d77\u65b0\u4efb\u52a1\u3001\u53d1\u9001\u6307\u4ee4\u3001\u4e2d\u6b62\u8fd0\u884c\u4e2d\u7684\u5f02\u5e38\u4efb\u52a1\u3002\n- **\u5b89\u5168\u6c99\u7bb1\u4e0e\u6700\u5c0f\u6743\u9650**\uff1a\u53d7 Pi-Desktop \u63d2\u4ef6\u6743\u9650\u4f53\u7cfb\u76d1\u7ba1\uff0c\u4ec5\u5f00\u653e\u7ecf\u7528\u6237\u663e\u5f0f\u6388\u6743\u7684 `desktop.control` \u4e0e\u7f51\u7edc\u767d\u540d\u5355\u901a\u9053\u3002\n\n---\n\n## \u4e8c\u3001\u76ee\u5f55\u7ed3\u6784\n\n```text\npi.agents-anywhere/\n\u251c\u2500\u2500 manifest.json # \u63d2\u4ef6\u6e05\u5355\uff08\u58f0\u660e\u6743\u9650\u3001\u9762\u677f\u89c6\u56fe\u4e0e\u7f51\u7edc\u767d\u540d\u5355\uff09\n\u251c\u2500\u2500 main.js # \u63d2\u4ef6\u6838\u5fc3\u670d\u52a1\uff08WebSocket \u901a\u4fe1\u3001RPC \u8def\u7531\u4e0e\u4e8b\u4ef6\u76d1\u542c\uff09\n\u251c\u2500\u2500 views/\n\u2502 \u2514\u2500\u2500 panel.html # Pi-Desktop \u53f3\u4fa7\u5de5\u4f5c\u533a\u539f\u751f\u4e92\u8054\u7ba1\u7406\u9762\u677f\n\u2514\u2500\u2500 README.md # \u672c\u8bf4\u660e\u6587\u6863\n```\n\n---\n\n## \u4e09\u3001\u5feb\u901f\u5f00\u59cb\n\n### 1. \u83b7\u53d6\u79fb\u52a8\u7aef\u914d\u5bf9\u51ed\u8bc1\n1. \u5728\u624b\u673a\u4e0a\u5b89\u88c5 **Agents Anywhere** \u5ba2\u6237\u7aef\uff08iOS TestFlight / Android APK / \u6253\u5f00 [web.agents-anywhere.com](https://web.agents-anywhere.com)\uff09\u3002\n2. \u767b\u5f55\u8d26\u53f7\u540e\uff0c\u5728\u5e95\u90e8\u70b9\u51fb **\u300c\u8bbe\u5907\u300d** $\\rightarrow$ **\u300c\u6dfb\u52a0\u8bbe\u5907 (Add Connector)\u300d**\u3002\n3. \u590d\u5236\u751f\u6210\u7684\u4e13\u5c5e\u51ed\u8bc1\uff1a\n - `Connector ID`\uff08\u5982 `conn_xxx`\uff09\n - `Connector Token`\uff08\u5982 `cxt_xxx`\uff09\n\n### 2. \u5728 Pi-Desktop \u4e2d\u52a0\u8f7d\u63d2\u4ef6\n1. \u6253\u5f00 Pi-Desktop \u684c\u9762\u7aef\uff0c\u8fdb\u5165\u300c\u8bbe\u7f6e $\\rightarrow$ \u63d2\u4ef6 / Extensions $\\rightarrow$ \u5f00\u53d1\u4e2d\u63d2\u4ef6\u300d\u3002\n2. \u70b9\u51fb\u300c\u52a0\u8f7d\u672a\u6253\u5305\u63d2\u4ef6\u300d\uff0c\u9009\u62e9\u672c\u63d2\u4ef6\u76ee\u5f55 `examples/plugins/pi.agents-anywhere`\u3002\n3. \u5bbf\u4e3b\u5f39\u51fa\u6743\u9650\u7533\u8bf7\u63d0\u793a\uff08\u7533\u8bf7 `net.websocket` \u548c `desktop.control`\uff09\uff0c\u70b9\u51fb **\u300c\u5141\u8bb8\u5e76\u542f\u7528\u300d**\u3002\n\n### 3. \u8fde\u63a5\u4e0e\u8054\u8c03\n1. \u70b9\u51fb\u684c\u9762\u53f3\u4fa7\u5de5\u4f5c\u9762\u677f\u4e2d\u7684 **\u300c\u8fdc\u7a0b\u52a9\u624b\u300d**\uff08\u56fe\u6807\u4e3a \ud83c\udf10/\ud83d\udd17\uff09\u3002\n2. \u5c06\u624b\u673a\u7aef\u751f\u6210\u7684 `Connector ID` \u548c `Connector Token` \u586b\u5165\u8f93\u5165\u6846\uff0c\u70b9\u51fb **\u300c\u4fdd\u5b58\u5e76\u7acb\u5373\u8fde\u63a5\u300d**\u3002\n3. \u72b6\u6001\u5fbd\u6807\u53d8\u4e3a\u7eff\u8272 **\u300c\u5728\u7ebf (\u5df2\u8fde\u63a5)\u300d**\u3002\n4. \u6253\u5f00\u624b\u673a\u7aef Agents Anywhere\uff0c\u5373\u53ef\u5728\u8bbe\u5907\u5217\u8868\u4e2d\u770b\u5230\u672c\u53f0\u7535\u8111\uff0c\u53ef\u968f\u65f6\u53d1\u9001 Prompt \u8fdc\u7a0b\u6307\u6325\uff01\n", + "safetyNotes": "\u8fde\u63a5\u51ed\u636e\u4ec5\u4fdd\u5b58\u5728\u672c\u673a\uff1b\u7f51\u7edc\u901a\u4fe1\u4e25\u683c\u9650\u5236\u5728\u5b98\u65b9\u6216\u7528\u6237\u914d\u7f6e\u7684 Agents Anywhere \u4e2d\u7ee7\u670d\u52a1\u5668\uff1b\u8fdc\u7a0b\u6267\u884c\u6307\u4ee4\u5b8c\u5168\u9075\u5faa\u672c\u5730\u6743\u9650\u5ba1\u6279\u7b56\u7565\u3002", + "versions": [ + { + "version": "1.0.0", + "publishedAt": "2026-09-21T02:37:42Z", + "changelog": "Release 1.0.0", + "minPiDesktop": ">=0.9.0", + "shasum": "5a0bdcda1cb0de47d1c9ae85d2a3be40c6ed486025cd7205710e15d3cc55c8fe", + "url": "packages/pi.agents-anywhere-1.0.0.piplug", + "sizeBytes": 21903, + "permissions": [ + "ui.view", + "net.fetch", + "net.websocket", + "desktop.control" + ], + "fs": {} + } + ] + }, { "id": "pi.bianqian", "name": "\u4fbf\u7b7e", diff --git a/packages/pi.agents-anywhere-1.0.0.piplug b/packages/pi.agents-anywhere-1.0.0.piplug new file mode 100644 index 0000000000000000000000000000000000000000..6460b01ce06131f7fb5baa47459ce8e4abd48269 GIT binary patch literal 21903 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 literal 0 HcmV?d00001 diff --git a/plugins/pi.agents-anywhere/README.md b/plugins/pi.agents-anywhere/README.md new file mode 100644 index 0000000..2f1621c --- /dev/null +++ b/plugins/pi.agents-anywhere/README.md @@ -0,0 +1,48 @@ +# Agents Anywhere Remote Plugin for Pi-Desktop + +本插件基于开源跨设备 Agent 工作台 **[Agents Anywhere](https://www.agents-anywhere.com/)**([GitHub 仓库](https://github.com/anywhere-labs/Agents-Anywhere)),为 **Pi-Desktop** 提供手机端(iOS / Android / Web)远程查看、实时会话控制与交互审批能力。 + +--- + +## 一、功能特性 + +- **跨设备反向隧道**:基于 WSS 长连接实现内网穿透,无需配置公网 IP 或打洞端口。 +- **双向会话流同步**:手机端与 PC 桌面端双向同步会话历史、运行中状态与实时响应。 +- **思考流式展开与折叠**:手机端完整展示 Agent 深度思考链(Chain of Thought),支持打字时展开、生成答案后自动折叠。 +- **远程操作控制**:在手机上随时发起新任务、发送指令、中止运行中的异常任务。 +- **安全沙箱与最小权限**:受 Pi-Desktop 插件权限体系监管,仅开放经用户显式授权的 `desktop.control` 与网络白名单通道。 + +--- + +## 二、目录结构 + +```text +pi.agents-anywhere/ +├── manifest.json # 插件清单(声明权限、面板视图与网络白名单) +├── main.js # 插件核心服务(WebSocket 通信、RPC 路由与事件监听) +├── views/ +│ └── panel.html # Pi-Desktop 右侧工作区原生互联管理面板 +└── README.md # 本说明文档 +``` + +--- + +## 三、快速开始 + +### 1. 获取移动端配对凭证 +1. 在手机上安装 **Agents Anywhere** 客户端(iOS TestFlight / Android APK / 打开 [web.agents-anywhere.com](https://web.agents-anywhere.com))。 +2. 登录账号后,在底部点击 **「设备」** $\rightarrow$ **「添加设备 (Add Connector)」**。 +3. 复制生成的专属凭证: + - `Connector ID`(如 `conn_xxx`) + - `Connector Token`(如 `cxt_xxx`) + +### 2. 在 Pi-Desktop 中加载插件 +1. 打开 Pi-Desktop 桌面端,进入「设置 $\rightarrow$ 插件 / Extensions $\rightarrow$ 开发中插件」。 +2. 点击「加载未打包插件」,选择本插件目录 `examples/plugins/pi.agents-anywhere`。 +3. 宿主弹出权限申请提示(申请 `net.websocket` 和 `desktop.control`),点击 **「允许并启用」**。 + +### 3. 连接与联调 +1. 点击桌面右侧工作面板中的 **「远程助手」**(图标为 🌐/🔗)。 +2. 将手机端生成的 `Connector ID` 和 `Connector Token` 填入输入框,点击 **「保存并立即连接」**。 +3. 状态徽标变为绿色 **「在线 (已连接)」**。 +4. 打开手机端 Agents Anywhere,即可在设备列表中看到本台电脑,可随时发送 Prompt 远程指挥! diff --git a/plugins/pi.agents-anywhere/main.js b/plugins/pi.agents-anywhere/main.js new file mode 100644 index 0000000..07840b1 --- /dev/null +++ b/plugins/pi.agents-anywhere/main.js @@ -0,0 +1,381 @@ +/** + * Agents Anywhere Remote Bridge Plugin for Pi-Desktop + * Copyright (c) 2026 Anywhere-Labs & PI-Desktop Contributors + */ + +// 1. 桌面能力转译桥接 +class DesktopBridge { + async getWorkspace() { + try { + if (typeof pi.workspace?.get === "function") { + const ws = await pi.workspace.get(); + return ws ? { path: ws.path, name: ws.name } : null; + } + return null; + } catch { + return null; + } + } + + async listSessions() { + try { + const result = await pi.desktop.invoke({ + operation: "session/collaboration/list", + args: [{}] + }); + return Array.isArray(result) ? result : []; + } catch (e) { + console.error("[Anywhere:Bridge] listSessions error:", e); + return []; + } + } + + async createSession(title, task) { + try { + const result = await pi.desktop.invoke({ + operation: "session/collaboration/spawn", + args: [{ + title: title || "远程移动会话", + task: task || "通过 Agents Anywhere 移动端初始化任务", + notifyOnCompletion: true + }] + }); + return result?.sessionId || ""; + } catch (e) { + console.error("[Anywhere:Bridge] createSession error:", e); + return ""; + } + } + + async sendMessage(sessionId, content) { + try { + await pi.desktop.invoke({ + operation: "session/collaboration/send", + args: [{ + sessionId, + content, + kind: "message", + notifyOnCompletion: true + }] + }); + return true; + } catch (e) { + console.error("[Anywhere:Bridge] sendMessage error:", e); + return false; + } + } + + async cancelTask(sessionId) { + try { + await pi.desktop.invoke({ + operation: "session/collaboration/cancel", + args: [{ sessionId }] + }); + return true; + } catch (e) { + console.error("[Anywhere:Bridge] cancelTask error:", e); + return false; + } + } +} + +// 2. Agents Anywhere WebSocket 客户端与 RPC 路由器 +class AnywhereClient { + constructor(config, bridge) { + this.config = config; + this.bridge = bridge; + this.socketId = null; + this.isConnected = false; + this.reconnectTimer = null; + this.heartbeatTimer = null; + this.logHistory = []; + } + + log(msg) { + const time = new Date().toLocaleTimeString(); + const entry = `[${time}] ${msg}`; + console.log(`[AgentsAnywhere] ${msg}`); + this.logHistory.unshift(entry); + if (this.logHistory.length > 50) this.logHistory.pop(); + } + + getStatus() { + return { + connected: this.isConnected, + serverUrl: this.config.serverUrl, + deviceName: this.config.deviceName, + connectorId: this.config.connectorId, + logs: this.logHistory + }; + } + + async connect() { + if (this.isConnected) return; + if (!this.config.connectorId || !this.config.connectorToken) { + this.log("等待配对配置:未提供 Connector ID 或 Token"); + return; + } + + const wsUrl = `${this.config.serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/connector/v2/rpc`; + this.log(`正在连接到 Agents Anywhere 服务端: ${wsUrl}`); + if (typeof pi.net?.fetch === "function") { + try { + await pi.net.fetch({ + url: `${this.config.serverUrl.replace(/\/+$/, "")}/healthz`, + timeoutMs: 3000 + }); + } catch {} + } + + try { + const res = await pi.net.websocket.connect({ + url: wsUrl, + headers: { + "Authorization": `Connector ${this.config.connectorId}:${this.config.connectorToken}`, + "X-Connector-Version": "2.0.0", + "X-Device-OS": process.platform || "desktop", + "X-Device-Name": encodeURIComponent(this.config.deviceName || "Pi-Desktop") + }, + timeoutMs: 10000 + }); + + this.socketId = res.socketId; + this.isConnected = true; + this.log("WebSocket 隧道建立成功,开始注册设备能力..."); + + this.setupEventListeners(); + this.startHeartbeat(); + await this.registerRuntime(); + } catch (err) { + this.log(`连接失败: ${err?.message || err},5秒后自动重试`); + this.scheduleReconnect(); + } + } + + async registerRuntime() { + const ws = await this.bridge.getWorkspace(); + await this.sendNotification("runtime.discover", { + runtimes: [ + { + id: "pi-desktop", + name: "Pi Desktop", + version: "1.0.0", + capabilities: [ + "chat", + "thinking_stream", + "tool_approvals", + "workspace_inspection" + ], + workspace: ws + } + ] + }); + this.log("已成功向云端注册 Pi Desktop 运行时,设备已就绪"); + } + + setupEventListeners() { + this._onMessage = async (event) => { + if (event.socketId !== this.socketId) return; + try { + const msg = JSON.parse(event.data); + await this.handleIncomingRpc(msg); + } catch (err) { + console.error("[AgentsAnywhere] Parse JSON error:", err); + } + }; + + this._onClose = (event) => { + if (event.socketId !== this.socketId) return; + this.log("WebSocket 连接已关闭"); + this.cleanup(); + this.scheduleReconnect(); + }; + + this._onError = (event) => { + if (event.socketId !== this.socketId) return; + this.log(`WebSocket 发生异常: ${JSON.stringify(event.error)}`); + }; + + pi.events.on("net:websocket:message", this._onMessage); + pi.events.on("net:websocket:close", this._onClose); + pi.events.on("net:websocket:error", this._onError); + } + + async handleIncomingRpc(msg) { + if (!msg || !msg.method) return; + const { id, method, params } = msg; + let result = null; + let error = null; + + this.log(`收到移动端 RPC 请求: ${method}`); + + try { + switch (method) { + case "runtime.discover": + result = { + runtimes: [ + { + id: "pi-desktop", + name: "Pi Desktop", + capabilities: ["chat", "thinking_stream", "tool_approvals"] + } + ] + }; + break; + + case "session.discover": + result = { sessions: await this.bridge.listSessions() }; + break; + + case "session.create": + const newSessionId = await this.bridge.createSession(params?.title, params?.task); + result = { sessionId: newSessionId }; + this.log(`远程创建新会话: ${newSessionId}`); + break; + + case "session.send_message": + const ok = await this.bridge.sendMessage(params.sessionId, params.content); + result = { success: ok }; + this.log(`向会话 ${params.sessionId} 派发远程指令`); + break; + + case "session.interrupt": + const cancelled = await this.bridge.cancelTask(params.sessionId); + result = { success: cancelled }; + this.log(`远程中止会话 ${params.sessionId}`); + break; + + case "ping": + result = { pong: Date.now() }; + break; + + default: + error = { code: -32601, message: `Method '${method}' is not implemented on Pi Desktop` }; + } + } catch (e) { + error = { code: -32000, message: e?.message || "Internal error" }; + } + + if (id !== undefined) { + await this.sendResponse(id, result, error); + } + } + + async syncTimeline(sessionId, update) { + await this.sendNotification("timeline.sync", { + sessionId, + timestamp: Date.now(), + ...update + }); + } + + async sendResponse(id, result, error) { + if (!this.socketId) return; + const payload = JSON.stringify({ jsonrpc: "2.0", id, result, error }); + await pi.net.websocket.send({ socketId: this.socketId, data: payload }); + } + + async sendNotification(method, params) { + if (!this.socketId || !this.isConnected) return; + const payload = JSON.stringify({ jsonrpc: "2.0", method, params }); + await pi.net.websocket.send({ socketId: this.socketId, data: payload }); + } + + startHeartbeat() { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = setInterval(() => { + this.sendNotification("heartbeat", { clientTime: Date.now() }).catch(() => {}); + }, 25000); + } + + scheduleReconnect() { + if (this.reconnectTimer) return; + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + this.connect(); + }, 5000); + } + + async disconnect() { + this.cleanup(); + if (this.socketId) { + try { + await pi.net.websocket.close({ socketId: this.socketId }); + } catch {} + this.socketId = null; + } + this.log("已断开远程连接"); + } + + cleanup() { + this.isConnected = false; + clearInterval(this.heartbeatTimer); + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + if (this._onMessage) pi.events.off("net:websocket:message", this._onMessage); + if (this._onClose) pi.events.off("net:websocket:close", this._onClose); + if (this._onError) pi.events.off("net:websocket:error", this._onError); + } +} + +// 3. 插件生命周期绑定 +const bridge = new DesktopBridge(); +let client = null; + +async function activate() { + console.log("[pi.agents-anywhere] Plugin starting up..."); + + // 从 storage 读取用户配置 + const storedConfig = (await pi.storage.get("connector_config")) || { + serverUrl: "https://api.agents-anywhere.com", + connectorId: "", + connectorToken: "", + deviceName: "My-Pi-Desktop" + }; + + client = new AnywhereClient(storedConfig, bridge); + client.log("Agents Anywhere 插件就绪"); + + // 自动连接 + if (storedConfig.connectorId && storedConfig.connectorToken) { + client.connect(); + } + + // 监听桌面端轮次结束事件 + pi.events.on("session:turnEnded", async (event) => { + if (client && client.isConnected) { + await client.syncTimeline(event.sessionId, { + phase: "turn_ended", + reason: event.reason + }); + } + }); + + // 监听桌面端工作区切换 + pi.events.on("workspace:changed", async (workspace) => { + if (client && client.isConnected) { + await client.syncTimeline("global", { + type: "workspace_changed", + workspace: workspace ? { path: workspace.path, name: workspace.name } : null + }); + } + }); + + // 与面板(Webview)通信 + pi.bus.subscribe("anywhere:get_status", async () => { + return client ? client.getStatus() : { connected: false, logs: [] }; + }); + + pi.bus.subscribe("anywhere:save_config", async (newConfig) => { + await pi.storage.set("connector_config", newConfig); + if (client) await client.disconnect(); + client = new AnywhereClient(newConfig, bridge); + await client.connect(); + return { success: true }; + }); +} + +// 启动插件 +activate().catch((err) => { + console.error("[pi.agents-anywhere] Failed to activate:", err); +}); diff --git a/plugins/pi.agents-anywhere/manifest.json b/plugins/pi.agents-anywhere/manifest.json new file mode 100644 index 0000000..adbdaec --- /dev/null +++ b/plugins/pi.agents-anywhere/manifest.json @@ -0,0 +1,53 @@ +{ + "schemaVersion": 1, + "id": "pi.agents-anywhere", + "name": "Agents Anywhere 远程助手", + "version": "1.0.0", + "description": "基于 Agents Anywhere 平台实现手机端对 Pi Desktop 的远程查看、发问与控制。", + "i18n": { + "en": { + "name": "Agents Anywhere Remote", + "description": "Remote control and sync for Pi Desktop from mobile via Agents Anywhere.", + "safetyNotes": "Connector credentials stay on this local machine. Network communication connects strictly to the official/configured Agents Anywhere relay servers over encrypted WSS/HTTPS. Remote actions reuse local session permissions and never bypass host boundaries." + }, + "zh-CN": { + "name": "Agents Anywhere 远程助手", + "description": "基于 Agents Anywhere 平台实现手机端对 Pi Desktop 的远程查看、发问与控制。", + "safetyNotes": "连接凭据仅保存在本机;网络通信严格限制在官方或用户配置的 Agents Anywhere 中继服务器(通过加密 WSS/HTTPS 传输);远程执行的指令完全复用本地会话权限体系,绝不绕过安全沙箱。" + } + }, + "safetyNotes": "连接凭据仅保存在本机;网络通信严格限制在官方或用户配置的 Agents Anywhere 中继服务器;远程执行指令完全遵循本地权限审批策略。", + "author": "Anywhere-Labs & PI-Desktop", + "license": "MIT", + "main": "main.js", + "contributes": { + "views": [ + { + "id": "anywhere-panel", + "title": { + "en": "Agents Anywhere", + "zh-CN": "远程助手" + }, + "icon": "link", + "entry": "views/panel.html", + "order": 20 + } + ] + }, + "permissions": [ + "ui.view", + "net.fetch", + "net.websocket", + "desktop.control" + ], + "net": { + "domains": [ + "api.agents-anywhere.com", + "web.agents-anywhere.com" + ] + }, + "engines": { + "piDesktop": ">=0.9.0" + }, + "activationEvents": ["onStartup"] +} diff --git a/plugins/pi.agents-anywhere/views/panel.html b/plugins/pi.agents-anywhere/views/panel.html new file mode 100644 index 0000000..f274864 --- /dev/null +++ b/plugins/pi.agents-anywhere/views/panel.html @@ -0,0 +1,187 @@ + + + + + Agents Anywhere 远程助手 + + + +
+
🌐 远程互联
+
+ + 离线 +
+
+ +
+ 配对与连接设置 +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ 💡 手机端操作:打开 Agents Anywhere App,在「设备」页面点击添加,将生成的 ID 与 Token 粘贴到上方即可完成配对。 +
+
+ +
+ 实时运行日志 +
[等待连接...]
+
+ + + + diff --git a/tests/agents-anywhere.test.mjs b/tests/agents-anywhere.test.mjs new file mode 100644 index 0000000..cda2018 --- /dev/null +++ b/tests/agents-anywhere.test.mjs @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(fileURLToPath(new URL("..", import.meta.url))); +const pluginRoot = join(root, "plugins", "pi.agents-anywhere"); +const manifest = JSON.parse(readFileSync(join(pluginRoot, "manifest.json"), "utf8")); +const catalog = JSON.parse(readFileSync(join(root, "catalog.json"), "utf8")); + +test("Agents Anywhere plugin declares the expected identity and permissions", () => { + assert.equal(manifest.schemaVersion, 1); + assert.equal(manifest.id, "pi.agents-anywhere"); + assert.equal(manifest.version, "1.0.0"); + assert.equal(manifest.main, "main.js"); + assert.deepEqual(manifest.permissions, [ + "ui.view", + "net.fetch", + "net.websocket", + "desktop.control", + ]); + assert.deepEqual(manifest.net?.domains, [ + "api.agents-anywhere.com", + "web.agents-anywhere.com", + ]); + assert.ok(manifest.i18n?.en?.safetyNotes); + assert.ok(manifest.i18n?.["zh-CN"]?.safetyNotes); +}); + +test("Agents Anywhere plugin package exists and is registered in marketplace catalog", () => { + const packagePath = join(root, "packages", `pi.agents-anywhere-${manifest.version}.piplug`); + assert.ok(existsSync(packagePath), "piplug package file must exist"); + + const entry = catalog.plugins.find((p) => p.id === "pi.agents-anywhere"); + assert.ok(entry, "plugin must be listed in catalog.json"); + assert.equal(entry.name, manifest.name); + assert.equal(entry.author, manifest.author); + + const versionEntry = entry.versions.find((v) => v.version === manifest.version); + assert.ok(versionEntry, "matching version entry must exist in catalog"); + assert.equal(versionEntry.url, `packages/pi.agents-anywhere-${manifest.version}.piplug`); + assert.ok(versionEntry.shasum && versionEntry.shasum.length === 64); +});