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 0000000..6460b01 Binary files /dev/null and b/packages/pi.agents-anywhere-1.0.0.piplug differ 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); +});