diff --git a/.gitignore b/.gitignore index 232f07953..c059ea1a3 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,9 @@ frontends/conductor_im_plugins/* # Beta 渠道:订阅门户插件(仅 beta 分发携带,不入库) plugins/subscription_portal.py + +# GAnet 由设备互联 SOP 从官方发布服务安装,不进入主仓。 +/plugins/ganet/ + +# Tracked device-interconnect SOP. +!memory/ganet_pc_setup_sop.md diff --git a/assets/global_mem_insight_template.txt b/assets/global_mem_insight_template.txt index ebb88c552..c6a2c8c0e 100644 --- a/assets/global_mem_insight_template.txt +++ b/assets/global_mem_insight_template.txt @@ -11,6 +11,7 @@ L4: L4_raw_sessions/ 历史会话 UI操作/截图/视觉: computer_use.md | vision_sop | 窗口截图必用ljqCtrl 定时:scheduled_task_sop | 自主:autonomous_operation_sop | watchdog/反射:agentmain --reflect 手机:adb_ui.py +手机直连电脑/组网/让手机 GA 连接这台电脑: ganet_pc_setup_sop [RULES] 1. 搜索先行: 搜文件名禁长时间递归, 网络搜索一定优先使用web工具的google, 优先看cwd,禁猜路径 diff --git a/assets/global_mem_insight_template_en.txt b/assets/global_mem_insight_template_en.txt index 04018121c..f70b2572d 100644 --- a/assets/global_mem_insight_template_en.txt +++ b/assets/global_mem_insight_template_en.txt @@ -11,6 +11,7 @@ Keyboard/Mouse: ljqCtrl_sop(no pyautogui/activate first) UI/Screenshot/Vision: computer_use.md | vision_sop | window screenshot use ljqCtrl Scheduling: scheduled_task_sop | Autonomous: autonomous_operation_sop | watchdog/reflect: agentmain --reflect Mobile: adb_ui.py +Connect a phone to this computer / configure GAnet: ganet_pc_setup_sop [RULES] 1. Search first: no huge PS recursion/no dir traversal for file search, prefer Google for web search, check cwd first, no guessing paths diff --git a/frontends/desktop_bridge.py b/frontends/desktop_bridge.py index a42388d8f..7a1c41ecb 100644 --- a/frontends/desktop_bridge.py +++ b/frontends/desktop_bridge.py @@ -740,6 +740,17 @@ def submit_prompt(self, sid: str, prompt: Any, images: Optional[list] = None, di raise web.HTTPNotFound(text=json.dumps({"error": f"session not found: {sid}"}, ensure_ascii=False), content_type="application/json") if sess.status == "running": raise web.HTTPConflict(text=json.dumps({"error": "session is already running"}, ensure_ascii=False), content_type="application/json") + if prompt.strip() == "/user": + user_msg = self.add_message(sess, "user", prompt) + try: + from plugins.ganet import open_user_center + except ImportError: + result = "设备互联尚未安装,请先发送“阅读 ../memory/ganet_pc_setup_sop.md,帮我配置设备互联。”" + else: + result = open_user_center() + self.add_message(sess, "assistant", result) + return {"ok": True, "sessionId": sid, "accepted": False, + "userMessageId": user_msg["id"], "seq": sess.msg_seq} extra = {} if image_ids: extra["image_ids"] = image_ids diff --git a/frontends/qtapp.py b/frontends/qtapp.py index 5e40a9c3f..89bad0918 100644 --- a/frontends/qtapp.py +++ b/frontends/qtapp.py @@ -2032,7 +2032,14 @@ def _handle_command(self, cmd: str): parts = cmd.split() op = parts[0].lower() if parts else "" if op == "/help": - self._add_system_notice(HELP_TEXT) + self._add_system_notice(HELP_TEXT + "\n/user - 打开账号与设备管理") + elif op == "/user": + try: + from plugins.ganet import open_user_center + except ImportError: + self._add_system_notice("设备互联尚未安装,请先发送“阅读 ../memory/ganet_pc_setup_sop.md,帮我配置设备互联。”") + else: + self._add_system_notice(open_user_center()) elif op == "/stop": self._do_stop() self._add_system_notice("⏹️ 已停止") diff --git a/frontends/stapp.py b/frontends/stapp.py index 62659bab4..c5d5a0089 100644 --- a/frontends/stapp.py +++ b/frontends/stapp.py @@ -368,6 +368,18 @@ def _slash_missing(name): {"role": "assistant", "content": f"❌ `{name}` 模块未安装", "time": ts}, ]) _reset_and_rerun() + if cmd == "/user": + try: + from plugins.ganet import open_user_center + except ImportError: + result = "设备互联尚未安装,请先发送“阅读 ../memory/ganet_pc_setup_sop.md,帮我配置设备互联。”" + else: + result = open_user_center() + st.session_state.messages.extend([ + {"role": "user", "content": cmd, "time": ts}, + {"role": "assistant", "content": result, "time": ts}, + ]) + _reset_and_rerun() if cmd == "/new": if not _SLASH: _slash_missing('continue_cmd') st.session_state.messages[:] = [{"role": "assistant", "content": reset_conversation(agent), "time": ts}] diff --git a/frontends/stapp2.py b/frontends/stapp2.py index 1d7968f5d..7eca3e3fc 100644 --- a/frontends/stapp2.py +++ b/frontends/stapp2.py @@ -1043,7 +1043,17 @@ def render_streaming_area(): for msg in st.session_state.messages: render_message(msg["role"], msg["content"], ts=msg.get("time", ""), unsafe_allow_html=True) if st.session_state.streaming: render_streaming_area() if prompt := st.chat_input("请输入指令", disabled=st.session_state.streaming): - st.session_state.messages.append({"role": "user", "content": prompt, "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) - start_agent_task(prompt) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + st.session_state.messages.append({"role": "user", "content": prompt, "time": now}) + if prompt.strip() == "/user": + try: + from plugins.ganet import open_user_center + except ImportError: + result = "设备互联尚未安装,请先发送“阅读 ../memory/ganet_pc_setup_sop.md,帮我配置设备互联。”" + else: + result = open_user_center() + st.session_state.messages.append({"role": "assistant", "content": result, "time": now}) + else: + start_agent_task(prompt) st.rerun() diff --git a/frontends/tui_v3.py b/frontends/tui_v3.py index 414ec558a..88b669706 100644 --- a/frontends/tui_v3.py +++ b/frontends/tui_v3.py @@ -1893,6 +1893,7 @@ def _cmds() -> list[tuple[str, str, str]]: return [ ('/help', '', _t('cmd.help.desc')), ('/status', '', _t('cmd.status.desc')), + ('/user', '', _t('cmd.user.desc', default='打开账号与设备管理')), ('/llm', _t('cmd.llm.arg'), _t('cmd.llm.desc')), ('/btw', _t('cmd.btw.arg'), _t('cmd.btw.desc')), ('/review', _t('cmd.review.arg'), _t('cmd.review.desc')), @@ -4504,6 +4505,13 @@ def _cmd(self, raw: str) -> None: rows.append(f' {_DIM}⚠ {self._bridge._init_error}{_RST}') rows.append('') self.commit(rows) + elif name == 'user': + try: + from plugins.ganet import open_user_center + except ImportError: + self.commit(['设备互联尚未安装,请先发送“阅读 ../memory/ganet_pc_setup_sop.md,帮我配置设备互联。”']) + else: + self.commit([open_user_center()]) elif name == 'new': # New session = wipe the current conversation and start fresh. # Keeping prior sessions around (multi-session) is a separate diff --git a/frontends/tuiapp.py b/frontends/tuiapp.py index 176334a23..60a0c14b1 100644 --- a/frontends/tuiapp.py +++ b/frontends/tuiapp.py @@ -151,7 +151,7 @@ def parse_local_command(raw: str) -> tuple[str, list[str]] | None: name, *rest = text.split(maxsplit=1) cmd = name[1:].lower() args = rest[0].split() if rest else [] - if cmd in {"help", "status", "new", "switch", "sessions", "stop", "llm", "branch", "rewind", "clear", "close", "quit", "exit"}: + if cmd in {"help", "status", "user", "new", "switch", "sessions", "stop", "llm", "branch", "rewind", "clear", "close", "quit", "exit"}: return cmd, args return None @@ -238,7 +238,7 @@ def compose(self) -> ComposeResult: with Vertical(id="main"): yield Static("", id="status") yield RichLog(id="log", wrap=True, highlight=True, markup=True) - yield PromptInput(placeholder="Message, or /help /new /branch /rewind /switch /clear /close /stop /llm /resume", id="prompt") + yield PromptInput(placeholder="Message, or /help /user /new /branch /rewind /switch /clear /close /stop /llm /resume", id="prompt") yield Footer() def on_mount(self) -> None: @@ -321,6 +321,7 @@ def _dispatch_command(self, cmd: str, args: list[str]) -> None: handlers = { "help": self._cmd_help, "status": self._cmd_status, + "user": self._cmd_user, "new": self._cmd_new, "switch": self._cmd_switch, "sessions": self._cmd_sessions, @@ -407,10 +408,19 @@ def _set_assistant_message(self, agent_id: int, task_id: int, text: str, *, done else: self._refresh_sidebar() + def _cmd_user(self, args: list[str]) -> None: + try: + from plugins.ganet import open_user_center + except ImportError: + self._system("设备互联尚未安装,请先发送“阅读 ../memory/ganet_pc_setup_sop.md,帮我配置设备互联。”") + return + self._system(open_user_center()) + def _cmd_help(self, args: list[str]) -> None: self._system( "Commands:\n" "/help - show this help\n" + "/user - open account and device management\n" "/new [name] - create and switch to a new agent session\n" "/branch [name] - fork current session (copies LLM history + display)\n" "/rewind - list rewindable turns; /rewind to truncate history\n" diff --git a/frontends/tuiapp_v2.py b/frontends/tuiapp_v2.py index 8e37ec7f5..b3ed2ce5c 100644 --- a/frontends/tuiapp_v2.py +++ b/frontends/tuiapp_v2.py @@ -2067,6 +2067,7 @@ def default_agent_factory() -> Any: ("/help", "", "显示帮助"), ("/status", "", "查看会话状态"), ("/sessions", "", "列出所有会话"), + ("/user", "", "打开账号与设备管理"), ("/new", "[name]", "新建并切换到新会话"), ("/switch", "", "切换到指定会话"), ("/close", "", "关闭当前会话"), @@ -3605,6 +3606,7 @@ def __init__(self, agent_factory: Optional[AgentFactory] = None) -> None: self._spinner_timer = None self._handlers: dict = { "help": self._cmd_help, "status": self._cmd_status, "sessions": self._cmd_status, + "user": self._cmd_user, "new": self._cmd_new, "switch": self._cmd_switch, "close": self._cmd_close, "rename": self._cmd_rename, "branch": self._cmd_branch, "rewind": self._cmd_rewind, "clear": self._cmd_clear, @@ -4998,6 +5000,14 @@ def _cmd_help(self, args, raw): lines = [f"{c:<11} {a:<18} {d}" for c, a, d in COMMANDS] self._system("命令列表:\n" + "\n".join(lines)) + def _cmd_user(self, args, raw): + try: + from plugins.ganet import open_user_center + except ImportError: + self._system("设备互联尚未安装,请先发送“阅读 ../memory/ganet_pc_setup_sop.md,帮我配置设备互联。”") + return + self._system(open_user_center()) + def _cmd_status(self, args, raw): lines = [] for sid, s in self.sessions.items(): diff --git a/memory/ganet_pc_setup_sop.md b/memory/ganet_pc_setup_sop.md new file mode 100644 index 000000000..d7d03e810 --- /dev/null +++ b/memory/ganet_pc_setup_sop.md @@ -0,0 +1,233 @@ +# 配置设备互联环境 + +触发词:`帮我配置设备互联`。 + +目标:把当前电脑准备为可与手机 GA 安全配对和连接的设备。 + +> 当前仅适配 Windows 系统;macOS 和 Linux 暂不支持本配置流程,发现操作系统不适配时停止配置并提醒用户,并告诉用户后续测试开发完善后会上线。 + +实际配置顺序: + +```text +安装 GAnet 插件 +→ 确认 GA 登录 +→ 准备 GAnet 网络组件与 OpenSSH +→ 配置 SSH 与独立授权文件 +→ 登记电脑并加入 GAnet +→ 复检 +→ 在用户中心配对手机 +``` + +完成标准:插件可导入,且基础环境、GAnet 控制面、SSH 服务、设备访问四项检查均通过。 + +## 1. 安装 GAnet 插件 + +GA 根目录是本 SOP 所在 `memory/` 目录的上一级,即包含 `agent_loop.py`、`frontends/` 和 `plugins/` 的目录。先把 GA 根目录加入 `sys.path`,尝试导入: + +```python +from plugins import ganet +``` + +导入成功则保留当前插件,直接进入第 2 节。插件不存在时,从以下固定地址读取发布清单: + +```text +https://ganet.gaagent.ai/releases/plugin/manifest.json +``` + +核对清单中的文件大小和 SHA-256。验证通过后解压得到 ganet ,并放到到目录 `plugins` 下。 + +安装完成后重新导入并确认: + +```python +from plugins.ganet import open_user_center +``` + +然后请用户在当前本地 UI 输入 `/user`。`/user` 会打开仅监听本机回环地址的 GA 用户中心;若当前 UI 提示组件尚未安装,则重新检查安装目录和导入错误,不要把插件源码放回 `frontends/`。插件目录是下载产物,不加入 Git。 + +## 2. 确认登录并检查环境 + +正式 GA 登录为后续电脑登记和 GAnet 入网提供身份。登录接口位于 `plugins/ganet/device_connection/auth.py`。 + +```python +from plugins.ganet.device_connection import auth +identity = auth.current_identity() +logged_in = bool(auth.get_token() and identity and identity.get("valid")) +``` + +未登录时,请用户在当前电脑 GA 执行 `/user` 并完成登录;已登录时继续。 + +环境检查由 `plugins/ganet/device_connection/network.py` 的 `check_env()` 提供。 + +GA 根目录就是本 SOP 所在 `memory/` 目录的上一级,即包含 `agent_loop.py`、`frontends/` 和 `plugins/` 的目录。`code_run` 的 python 脚本默认导入不到该目录下的项目模块,导入 `plugins.ganet` 前先自行把 GA 根目录加入 `sys.path`。 + +这一步只读,检查: + +- GAnet 网络组件是否已安装、运行、加入 GAnet 并监听设备连接; +- GAnet 网络组件版本状态:`current`、`available`、`required` 或 `unknown`; +- OpenSSH 服务、SFTP、配置端口和监听状态; +- GAnet 独立授权文件与 ACL; +- 二维码和电脑截图所需的当前 GA 组件。 + +版本状态只影响“基础环境”节点的呈现:`available` 为黄色提示,表示当前连接仍可用;`required` 与组件缺失、无法响应一样进入修复流程;`unknown` 不影响当前可用链路。其他状态从 `chain` 和 `checks` 汇总实际缺失项。 + +组件本机状态和版本状态由: + +```python +from plugins.ganet.device_connection import sidecar_manager +component = sidecar_manager.inspect() +``` + +读取。`inspect()` 返回 `installed`、`running`、`online`、`listening`、`version_state` 和脱敏 `reason`;`version_state` 取 `current`、`available`、`required` 或 `unknown`。 + +系统级变更前,用一句话向用户说明将自动安装或修复的项目并取得一次确认。后续技术操作由 GA 完成,用户只需接受 Windows 管理员提示。 + +在同一次确认里提醒用户:配置需要修改系统 SSH 配置并重启服务,这类操作常被安全软件误判拦截,建议先退出 360、火绒一类杀毒软件,配置完成后再恢复。同时 ssh 服务的安装需要一定时间,需要请用户耐心等待。 + +如需用当前 GA 解释器安装 Python 依赖,官方 PyPI 下载缓慢或出现读取超时时,使用镜像源重试,例如:`<当前 GA Python> -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple <包名>`;不要因单次下载超时误判 Python 或虚拟环境损坏。 + +## 3. 准备缺失组件 + +GAnet 网络组件提供电脑与手机之间的私有连接,OpenSSH 提供终端和文件访问。组件准备完成不代表已经完成组网。 + +### Windows:GAnet 网络组件 + +网络组件接口位于 `plugins.ganet.device_connection.sidecar_manager`。安装、替换和入网是不同阶段:本节只处理组件文件与本机进程;第 4 节才使用正式 GA 登录态让组件加入 GAnet。 + +#### 读取发布列表并选择文件 + +```python +from plugins.ganet.device_connection import sidecar_manager + +releases = sidecar_manager.list_releases() +``` + +`list_releases()` 读取 `https://ganet.gaagent.ai/releases/sidecar/`,返回发布页实际列出的可用条目及其签名验证结果。每个有效条目至少包含: + +```python +{ + "platform": "windows", + "architecture": "amd64", # 以发布页实际值为准 + "version": "…", + "url": "…", + "sha256": "…", + "size": 12345678, + "update_level": "available", +} +``` + +先确认当前电脑的实际 Windows 架构,再从 `releases` 中选出平台、架构匹配的条目。选择必须依据本机检查结果和发布页返回内容;不得从 SOP、记忆或固定文件名推断版本或架构映射。若发布页不可读但当前组件可用,保留当前组件并继续设备互联,不把版本检查失败报告为连接故障。 + +#### 下载、验证与安装 + +```python +artifact = sidecar_manager.download_release(release) +verified = sidecar_manager.verify_release(artifact, release) +result = sidecar_manager.install_release(verified) +``` + +- `download_release(release)` 仅下载选定条目到临时目录,返回本地 artifact;不替换已安装组件。 +- `verify_release(artifact, release)` 验证发布签名、SHA-256、PE 文件、平台和架构;任一检查失败立即停止,不返回可安装 artifact。 +- `install_release(verified)` 安装缺失组件,或安全替换已安装组件:停止旧进程、保留可回滚副本、原子替换、恢复当前用户登录启动项并验证二进制版本;已有入网配置时启动组件并确认可响应,首次安装尚未入网时允许保持未运行;失败恢复旧版。 + +安装成功后重新运行第 2 节检查。组件缺失、无法响应或版本 `required` 时,完成本节后才进入第 4 节;版本 `available` 时当前连接仍可用,但用户本次要求配置或修复设备互联则按本节完成替换后再复检。 + +### Windows:OpenSSH Server + +安装前先阅读 Microsoft 官方文档,再按其中适用于当前 Windows 版本的方式安装 OpenSSH Server:。OpenSSH 安装耗时较长(有时达 20 多分钟),长时间未响应不一定是安装出问题,必要可让用户看看有没有出现进度条,帮忙检查进度。 + +只安装,不要启动 sshd 服务,也不要设置服务启动类型:官方文档在安装之后还给出了启动服务的步骤,这一步由第 4 节统一负责。原因是 sshd 首次启动会生成默认配置并开始监听默认端口 22,一旦提前启动,第 4 节就无法分辨 22 端口上的服务是用户原有的还是本轮刚装出来的,只能保守地把 22 一起保留下来。 + +如果本轮由 GA 安装了 OpenSSH Server,请记住这一点,第 4 节调用时需要显式说明。 + +安装失败时,保留完整错误和检查结果,向用户报告系统组件存储或 Windows Update 的实际问题;不要套用其他 Windows 版本的固定安装命令。 + +系统组件安装完成后,重新运行第 2 节检查。 + +## 4. 配置电脑并加入 GAnet + +完整编排入口是 `plugins/ganet/device_connection/pairing.py` 的 `configure_environment()`。它先检查正式登录态,再依次调用 `plugins/ganet/device_connection/network.py` 中的系统配置和入网实现,最后返回权威环境检查结果。 + +GAnet 网络组件就绪后: + +```python +from plugins.ganet.device_connection import pairing +result = pairing.configure_environment(approved=True) +``` + +若第 3 节由 GA 安装了 OpenSSH Server,则加上: + +```python +from plugins.ganet.device_connection import pairing +result = pairing.configure_environment( + approved=True, + sshd_installed_by_ganet=True, +) +``` + +不确定时不要传:漏传的后果是保守而安全的。sshd 只要写入任何显式端口,原本隐式生效的默认端口 22 就会失效,因此在无法确认这台电脑的 sshd 是本轮新装的情况下,受管配置会把 22 一并显式保留,避免静默切断用户已经在用的 SSH 访问。本轮新装的 sshd 不存在这类既有访问,只有明确传了这一项,配置才会只监听 GAnet 端口。 + +一次调用按以下阶段执行: + +### 4.1 配置电脑访问能力 + +`network.apply_confirmed()` 通过 Windows 管理员权限完成: + +1. 在 OpenSSH 配置中写入 GAnet 管理块,使用配置端口,同时保留用户原有 SSH 密钥与认证策略;用户原有的显式端口一律保留,原本依赖默认端口 22 的电脑也会把 22 显式保留,除非已说明 OpenSSH Server 由本轮 GA 安装;Windows 管理员账户还会在原有管理员授权文件之外,显式读取该账户独立的 GAnet 授权文件; +2. 准备独立的 `.ssh/authorized_keys_ganet`,并设置 Windows OpenSSH 可接受且当前用户可维护的 ACL; +3. 缓存 SSH Ed25519 主机公钥并重启 sshd; +4. 对嵌入式网络组件,验证 OpenSSH 仅作为本机连接后端可用;不再为旧系统级网络客户端创建入站防火墙规则。 + +这些操作使用受管标记块和独立授权文件,重复执行应修复现状而不是累加配置。 + +UAC 提权后的受管配置跑在独立进程,原终端看不到逐步输出。排障时读取: + +```text +~/.genericagent/ganet/setup-elevated.log +``` + +失败返回的 `message` 也会带上该路径。 + +### 4.2 登记电脑并加入 GAnet + +电脑访问配置完成后,`network.enroll()`: + +1. 使用正式 GA 登录态向设备登记服务提交电脑身份和 SSH 连接信息; +2. 获取一次性入网授权; +3. 将授权仅交给网络 provider 的受管 `join()` 调用,使已安装组件加入 GAnet; +4. 保存本机入网回执。 + +一次性授权只在本次入网过程中传递,不得显示、记录或长期保存。嵌入式网络组件不修改 Windows 系统 DNS、路由或用户已有网络客户端。 + +### 4.3 处理结果 + +- `needs_project_setup`:说明 `message` 中缺失的当前 GA 组件,补齐后从第 1 节重新开始。 +- `needs_system_setup`:说明 `message` 中缺失的系统组件,按第 3 节安装后重新开始。 +- `needs_login`:当前正式登录态无效,请用户执行 `/user`,登录后重新开始。 +- `blocked`:根据 `stage`、`message` 和环境检查报告失败阶段、真实原因及已经完成的状态,不把 SSH、防火墙失败解释为登录或入网失败。 +- `ok`:进入验收。若基础环境仅有 `available` 版本提示,仍可进入验收;在本轮按第 3 节完成组件替换后再复检。 + +每轮只调用一次 `configure_environment()`。若 `blocked` 信息不足或返回状态与上述阶段职责不一致,可读取 `pairing.py` 中的 `configure_environment()` 以及 `network.py` 中对应的 `apply_confirmed()` 或 `enroll()` 定位接口故障;不要用临时脚本或手工命令绕开受管接口修改 sshd 或网络组件状态。 + +## 5. 复检与手机配对 + +`configure_environment()` 返回的 `environment` 是本轮最终检查结果。必要时只再运行一次第 2 节的只读检查,确认: + +```text +基础环境 → GAnet 控制面 → SSH 服务 → 设备访问 +``` + +四项均通过后,向用户说明: + +```text +设备互联环境已配置完成。 +请回到已打开的“GA 用户中心”页面,在“我的设备”中点击“添加设备”,再使用手机 GA 扫描电脑显示的二维码。 +``` + +完成扫码、电脑确认和一次真实连接验证后,才能确认端到端设备互联可用(这些由用户完成)。 + +## 6. 安全边界 + +- 导入 `plugins.ganet` 前先把 GA 根目录(本 SOP 所在 `memory/` 的上一级)加入 `sys.path`;Python 依赖用当前 GA 解释器管理。 +- 保留用户原有 SSH 密钥、`authorized_keys`、密码登录策略、监听端口、DNS、hosts 与已有网络客户端状态。 +- SSH 默认端口为 `48222`;设备连接仅使用 GAnet 的受管网络组件和本机 OpenSSH 后端。 +- token、一次性入网授权、配对短消息和 SSH 私钥不写入日志或聊天记录。 diff --git a/pyproject.toml b/pyproject.toml index cf243449c..12b027ea5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,14 @@ all-frontends = [ "wecom-aibot-sdk>=1.0", "dingtalk-stream>=0.20", ] +# phone <-> computer device interconnect (GAnet). Optional and Windows-leaning: +# install only to pair a phone with this computer. Keeping the list here rather +# than hardcoded in the repair path is what lets `/user` install what it checks. +device = [ + "qrcode>=7.4", # pairing QR + "pillow>=9.0", # computer screenshot capture + "cryptography>=42.0", # signed GAnet component releases +] [build-system] requires = ["setuptools>=68.0"]