nexforge-cli 0.2.12__tar.gz → 0.2.13__tar.gz

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.
Files changed (187) hide show
  1. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/PKG-INFO +1 -1
  2. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/01-/345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md +1 -1
  3. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/02-/347/225/214/351/235/242/344/270/216/345/277/253/346/215/267/351/224/256.md +1 -1
  4. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/03-/346/250/241/345/236/213/351/205/215/347/275/256.md +2 -0
  5. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/04-/345/267/245/345/205/267/345/217/202/350/200/203.md +1 -1
  6. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/05-/346/225/260/346/215/256/344/270/216/346/220/234/347/264/242.md +3 -1
  7. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/08-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +1 -1
  8. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/README.md +1 -1
  9. nexforge_cli-0.2.13/plugin_gui_auto/README.md +86 -0
  10. nexforge_cli-0.2.13/plugin_gui_auto/guiauto/__init__.py +80 -0
  11. nexforge_cli-0.2.13/plugin_gui_auto/guiauto/action.py +194 -0
  12. nexforge_cli-0.2.13/plugin_gui_auto/guiauto/base.py +161 -0
  13. nexforge_cli-0.2.13/plugin_gui_auto/guiauto/tools.py +429 -0
  14. nexforge_cli-0.2.13/plugin_gui_auto/guiauto/vision.py +243 -0
  15. nexforge_cli-0.2.13/plugin_gui_auto/guiauto/window.py +207 -0
  16. nexforge_cli-0.2.13/plugin_gui_auto/install_plugin.py +148 -0
  17. nexforge_cli-0.2.13/plugin_gui_auto/requirements.txt +6 -0
  18. nexforge_cli-0.2.13/plugin_gui_auto/smoke_safe.py +43 -0
  19. nexforge_cli-0.2.13/plugin_gui_auto/verify_final.py +61 -0
  20. nexforge_cli-0.2.13/plugin_gui_auto/verify_host.py +33 -0
  21. nexforge_cli-0.2.13/plugin_gui_auto/verify_plugin.py +94 -0
  22. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/pyproject.toml +1 -1
  23. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/__init__.py +1 -1
  24. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/app.py +4 -1
  25. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/cli.py +1 -1
  26. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/config.py +7 -2
  27. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/agent.py +5 -4
  28. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/federation.py +3 -0
  29. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/provider/deepseek.py +82 -1
  30. nexforge_cli-0.2.13/src/nexforge/tools/image_tools.py +320 -0
  31. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/screenshot_tools.py +53 -24
  32. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/clipboard_image.py +54 -33
  33. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/screens/chat.py +6 -3
  34. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/slash_handler.py +1 -1
  35. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/model_sidebar.py +2 -2
  36. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/statusbar.py +1 -1
  37. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/web/server.py +1 -1
  38. nexforge_cli-0.2.13/tests/test_cross_platform.py +354 -0
  39. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_federation.py +18 -0
  40. nexforge_cli-0.2.13/tests/test_vision_model.py +65 -0
  41. nexforge_cli-0.2.13/tests/test_vision_tool_retention.py +98 -0
  42. nexforge_cli-0.2.12/src/nexforge/tools/image_tools.py +0 -173
  43. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/.gitignore +0 -0
  44. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/INSTALL.md +0 -0
  45. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/README.md +0 -0
  46. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/STATS_API.md +0 -0
  47. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/agent.svg +0 -0
  48. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/cost.svg +0 -0
  49. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/edit.svg +0 -0
  50. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/err.svg +0 -0
  51. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/file.svg +0 -0
  52. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/model.svg +0 -0
  53. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/ok.svg +0 -0
  54. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/read.svg +0 -0
  55. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/running.svg +0 -0
  56. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/search.svg +0 -0
  57. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/shell.svg +0 -0
  58. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/thinking.svg +0 -0
  59. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/web.svg +0 -0
  60. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/assets/icons/write.svg +0 -0
  61. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/config.example.toml +0 -0
  62. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/devlog/2026-07-07.md +0 -0
  63. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/doc//344/273/243/347/240/201/344/274/230/345/214/226/350/256/241/345/210/222.md" +0 -0
  64. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/doc//347/211/271/346/200/247/346/250/241/345/235/227/346/263/250/345/206/214/345/210/266-/345/256/236/346/226/275/350/256/241/345/210/222.md" +0 -0
  65. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/doc//351/241/271/347/233/256/346/200/273/347/233/221/345/276/252/347/216/257-/345/274/200/345/217/221/350/256/241/345/210/222.md" +0 -0
  66. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/docs/submission/README-/346/212/225/347/250/277/350/257/264/346/230/216.md" +0 -0
  67. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/docs/submission/nexforge.md +0 -0
  68. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/docs/submission/nexforge.zh-CN.md +0 -0
  69. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/06-/346/235/203/351/231/220/344/270/216/346/250/241/345/274/217.md" +0 -0
  70. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/07-/344/274/232/350/257/235/344/270/216/350/256/260/345/277/206.md" +0 -0
  71. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/09-/345/270/270/350/247/201/351/227/256/351/242/230.md" +0 -0
  72. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual/10-/346/217/222/344/273/266/345/274/200/345/217/221.md" +0 -0
  73. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/manual//351/231/204/345/275/225-/345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -0
  74. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/scripts/publish.sh +0 -0
  75. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/scripts/smoke_provider.py +0 -0
  76. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/scripts/smoke_tui.py +0 -0
  77. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/__main__.py +0 -0
  78. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/__init__.py +0 -0
  79. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/context.py +0 -0
  80. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/goal_manager.py +0 -0
  81. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/jobs.py +0 -0
  82. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/knowledge_base.py +0 -0
  83. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/memory_engine.py +0 -0
  84. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/permissions.py +0 -0
  85. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/plugin.py +0 -0
  86. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/project_mapper.py +0 -0
  87. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/provider.py +0 -0
  88. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/router.py +0 -0
  89. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/session.py +0 -0
  90. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/summarizer.py +0 -0
  91. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/system_inject.py +0 -0
  92. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/tool_manifest.py +0 -0
  93. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/core/types.py +0 -0
  94. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/__init__.py +0 -0
  95. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/auto_skills.py +0 -0
  96. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/base.py +0 -0
  97. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/federation_feature.py +0 -0
  98. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/kb_feature.py +0 -0
  99. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/map_feature.py +0 -0
  100. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/memory_feature.py +0 -0
  101. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/mode_features.py +0 -0
  102. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/plan_feature.py +0 -0
  103. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/registry.py +0 -0
  104. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/session_feature.py +0 -0
  105. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/slash_feature.py +0 -0
  106. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/stats_feature.py +0 -0
  107. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/features/tool_manifest_feature.py +0 -0
  108. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/i18n.py +0 -0
  109. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/icons.py +0 -0
  110. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/mcp/__init__.py +0 -0
  111. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/provider/__init__.py +0 -0
  112. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/provider/_retry.py +0 -0
  113. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/provider/anthropic.py +0 -0
  114. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/provider/balance.py +0 -0
  115. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/provider/openai.py +0 -0
  116. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/provider/qianwen.py +0 -0
  117. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/skills/loader.py +0 -0
  118. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/stats/__init__.py +0 -0
  119. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/stats/heartbeat.py +0 -0
  120. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/store/__init__.py +0 -0
  121. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/store/plan_store.py +0 -0
  122. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/store/session_store.py +0 -0
  123. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/theme.tcss +0 -0
  124. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/__init__.py +0 -0
  125. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/_paths.py +0 -0
  126. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/background.py +0 -0
  127. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/base.py +0 -0
  128. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/data_tools.py +0 -0
  129. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/edit_file.py +0 -0
  130. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/federation.py +0 -0
  131. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/fs_ops.py +0 -0
  132. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/git.py +0 -0
  133. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/kb_tools.py +0 -0
  134. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/list_dir.py +0 -0
  135. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/multi_edit.py +0 -0
  136. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/ops_tools.py +0 -0
  137. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/read_file.py +0 -0
  138. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/registry.py +0 -0
  139. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/search.py +0 -0
  140. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/search_content.py +0 -0
  141. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/search_files.py +0 -0
  142. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/shell.py +0 -0
  143. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/web.py +0 -0
  144. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/tools/write_file.py +0 -0
  145. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/__init__.py +0 -0
  146. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/screen_api.py +0 -0
  147. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/screens/__init__.py +0 -0
  148. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/screens/chat_actions.py +0 -0
  149. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/screens/splash.py +0 -0
  150. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/__init__.py +0 -0
  151. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
  152. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/banner.py +0 -0
  153. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
  154. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/diff_modal.py +0 -0
  155. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/image_sidebar.py +0 -0
  156. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/messages.py +0 -0
  157. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/mode_warning.py +0 -0
  158. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
  159. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
  160. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
  161. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/ui/widgets/sidebar.py +0 -0
  162. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/utils/__init__.py +0 -0
  163. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/utils/logging.py +0 -0
  164. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/web/__init__.py +0 -0
  165. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/src/nexforge/web/static/index.html +0 -0
  166. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/conftest.py +0 -0
  167. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_approval.py +0 -0
  168. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_config.py +0 -0
  169. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_goal_confirm.py +0 -0
  170. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_i18n.py +0 -0
  171. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_icons.py +0 -0
  172. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_lang_cmd.py +0 -0
  173. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_m2_tools.py +0 -0
  174. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_manual_consistency.py +0 -0
  175. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_p2_core.py +0 -0
  176. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_plugin.py +0 -0
  177. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_sanitize.py +0 -0
  178. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_screenshot_tools.py +0 -0
  179. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_session_repair.py +0 -0
  180. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_session_store.py +0 -0
  181. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_sidebar.py +0 -0
  182. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_sidebar_actions.py +0 -0
  183. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_splash_modal.py +0 -0
  184. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_statusbar.py +0 -0
  185. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_system_inject.py +0 -0
  186. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_tools.py +0 -0
  187. {nexforge_cli-0.2.12 → nexforge_cli-0.2.13}/tests/test_tui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nexforge-cli
3
- Version: 0.2.12
3
+ Version: 0.2.13
4
4
  Summary: NexForgeCLI — 一个面向 DeepSeek V4 的终端智能体(TUI + CLI 双模式)
5
5
  Project-URL: Homepage, https://gitee.com/Lighthorn/nexforgecli
6
6
  Author: NexForge
@@ -1,6 +1,6 @@
1
1
  # 安装与配置
2
2
 
3
- > NexForgeCLI v0.2.12
3
+ > NexForgeCLI v0.2.13
4
4
 
5
5
  ## 环境要求
6
6
 
@@ -64,7 +64,7 @@
64
64
  |------|------|
65
65
  | `/key <key>` | 设置 API Key |
66
66
  | `/provider [<name>]` | 无参数列出提供者;带参数切换 |
67
- | `/model flash|pro|auto` | 切换模型档位(仅 DeepSeek) |
67
+ | `/model flash|pro|auto|vision` | 切换模型档位(仅 DeepSeek, vision=视觉模型) |
68
68
  | `/think on|off` | 开关思考模式(仅 DeepSeek) |
69
69
  | `/balance` | 查询账户余额(仅 DeepSeek) |
70
70
  | `/cost` | 显示累计费用 |
@@ -20,8 +20,10 @@ DeepSeek 是内置的默认模型,首次运行只需提供 API Key 即可使
20
20
  | flash | 快速模型 | 常规编码、文件操作、日常问答 |
21
21
  | pro | 推理模型 | 复杂算法、架构设计、调试 |
22
22
  | auto | 自动路由 | 默认。小任务 flash,大任务自动升级 pro |
23
+ | vision | 视觉模型 | 图片识别/理解(deepseek-v4-flash-vision-exp, 发图即用) |
23
24
 
24
25
  **默认 flash**:deepseek 涨价后默认固定 flash 省成本;需要深度推理时用 `/model pro`,或 `/model auto` 恢复自动路由。
26
+ **视觉模型**:`/model vision` 或侧边栏切"视觉"档位后,粘贴图片直接分析(原生多模态,不走识图工具);vision 档位自动关闭思考模式(exp 模型兼容性)。
25
27
 
26
28
  auto 模式下的升级触发条件:
27
29
  - 关键词(重构/架构/算法/证明/调试/为什么/设计)
@@ -45,7 +45,7 @@ NexForgeCLI 内置 **35 个工具**供 AI 调用(`git_commit` 需在 config.to
45
45
  |------|------|
46
46
  | `web_fetch` | HTTP GET 抓取 URL(带 User-Agent, 5MB 限制) |
47
47
  | `search_web` | 网络搜索(默认 Bing, 可选 Google/DuckDuckGo/SearXNG) |
48
- | `image_vision` | 图片识别(阿里云千问 VL) |
48
+ | `image_vision` | 图片识别(deepseek-vision + 千问 VL 双引擎, 支持 cross 交叉验证) |
49
49
  | `image_gen` | 图片生成(阿里云通义万相) |
50
50
  | `kb_search` | 知识库检索(全局+项目两级) |
51
51
  | `kb_write` | 知识沉淀(写入项目/全局知识库) |
@@ -50,7 +50,9 @@ HTTP GET 抓取指定 URL,返回正文(截断至 20000 字符)。带浏览
50
50
 
51
51
  ### image_vision(图片识别)
52
52
 
53
- 传入图片路径和问题,调用阿里云千问 VL 视觉模型分析图片内容。
53
+ 传入图片路径和问题,分析图片内容。**双引擎识别**:
54
+ - `mode=auto`(默认):优先用当前对话 provider 的视觉模型(如 deepseek-vision,共用对话 key),失败回退阿里云千问 qwen-vl
55
+ - `mode=cross`:**双模型交叉验证**——deepseek-vision 与 qwen-vl 并行识别同一张图,返回双答案供交叉印证,减少单一模型的视觉误差(适合 GUI 验收/关键判断)
54
56
 
55
57
  ### image_gen(图片生成)
56
58
 
@@ -25,7 +25,7 @@
25
25
 
26
26
  ```toml
27
27
  [models]
28
- default_tier = "flash" # flash | pro | auto(默认 flash 省成本; auto 会按需升级 pro)
28
+ default_tier = "flash" # flash | pro | auto | vision(默认 flash 省成本; auto 会按需升级 pro; vision=视觉模型)
29
29
  thinking = true # 思考模式
30
30
  reasoning_effort = "high" # high | max
31
31
  max_output_tokens = 32768 # 输出上限(含思考 tokens), 长思考会吃配额, 不够可调大
@@ -1,4 +1,4 @@
1
- # NexForgeCLI 用户手册 · v0.2.12
1
+ # NexForgeCLI 用户手册 · v0.2.13
2
2
 
3
3
  ## 这是什么
4
4
 
@@ -0,0 +1,86 @@
1
+ # NF-GUI —— 给 AI 的跨平台 GUI 自动化插件
2
+
3
+ 让 NexForgeCLI 的 AI 获得操作计算机鼠标、键盘、屏幕的能力:能看界面、能精准定位元素、
4
+ 能点击/拖拽/输入,用于 GUI 应用的自动化测试、界面验收、交互验证。
5
+
6
+ ## 特性
7
+
8
+ - **跨平台**:Windows / macOS / Linux(pyautogui 底层,统一逻辑像素坐标)
9
+ - **三层定位**:模板匹配(快准)· 视觉模型(通用)· 像素特征(零依赖)
10
+ - **复杂拖拽**:直线/曲线路径、分段控制点、按住停顿——滑块、调整大小、排序全支持
11
+ - **中文输入**:自动走剪贴板,不受键盘布局影响
12
+ - **操作留痕**:每次操作前自动存档 before 截图到 `.nexforge/gui_logs/`
13
+ - **安全**:鼠标甩到左上角即中断(FAILSAFE)+ 写操作默认需审批
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ cd plugin_gui_auto
19
+ python install_plugin.py # 装依赖 + 安装到用户级 + 自检
20
+ python install_plugin.py --project # 或装到当前项目
21
+ # 重启 nf 或执行 /plugins reload
22
+ ```
23
+
24
+ 可选依赖:`opencv-python`(模板匹配)、`pyperclip`(中文输入,通常已随 pyautogui 带)。
25
+
26
+ ## 工具清单
27
+
28
+ | 工具 | 功能 | 审批 |
29
+ |------|------|------|
30
+ | `gui_screen` | 截取屏幕/窗口/区域 | 免 |
31
+ | `gui_find` | 定位元素中心坐标(template/prompt/color 三选一) | 免 |
32
+ | `gui_click` | 点击(左/右/中,单/双击,窗口内偏移) | 需 |
33
+ | `gui_drag` | 拖拽(直线/曲线,分段控制,按住停顿) | 需 |
34
+ | `gui_type` | 输入文本(中文走剪贴板) | 需 |
35
+ | `gui_key` | 按键/组合键(enter/ctrl+c/alt+f4) | 需 |
36
+ | `gui_scroll` | 滚动 | 需 |
37
+ | `gui_windows` | 窗口管理(枚举/激活/几何/最小化/最大化/关闭) | 免 |
38
+
39
+ ## 典型工作流
40
+
41
+ ```
42
+ AI 收到 GUI 测试任务后:
43
+
44
+ 1. gui_windows list # 找目标窗口(或 gui_windows activate handle 激活)
45
+ 2. gui_screen # 截图看当前界面
46
+ 3. gui_find prompt="登录按钮的中心" # 定位(视觉模型)
47
+ # 或 gui_find template="btn.png" # 定位(模板匹配, 更快)
48
+ 4. gui_click x=960 y=540 # 点击
49
+ 5. gui_type text="admin" # 输入
50
+ 6. gui_drag x1=100 y1=200 x2=300 y2=200 curve=true # 拖滑块
51
+ 7. gui_screen # 再截图验证结果
52
+ ```
53
+
54
+ ## 坐标体系
55
+
56
+ - 所有坐标是**屏幕逻辑像素**(与 DPI 无关,Windows 高 DPI 自动处理)
57
+ - `gui_windows geometry` 可拿窗口矩形;`client` 动作转客户区坐标
58
+ - 多显示器:pyautogui 只可靠操作主显示器(官方限制),次屏请先拖到主屏
59
+
60
+ ## 跨平台注意
61
+
62
+ | 平台 | 说明 |
63
+ |------|------|
64
+ | Windows | 功能最全(窗口控制/DPI 感知/PrintWindow 截图) |
65
+ | Linux | 需 X11;窗口枚举依赖 `xdotool`(`sudo apt install xdotool`);Wayland 需 XWayland |
66
+ | macOS | pyautogui 需辅助功能权限(系统设置→隐私→辅助功能);窗口控制走 AppleScript |
67
+
68
+ ## 开发
69
+
70
+ ```
71
+ guiauto/
72
+ __init__.py register(api) 插件入口 + 技能提示
73
+ base.py 核心底座(平台/DPI/坐标/安全/截图)
74
+ vision.py 视觉定位(模板匹配/视觉模型/像素特征)
75
+ action.py 鼠标键盘操作(点击/拖拽/输入/按键/滚动)
76
+ window.py 窗口控制(枚举/激活/几何/最小化/关闭)
77
+ tools.py 8 个 BaseTool 工具层(暴露给 AI)
78
+ ```
79
+
80
+ ## 设计决策
81
+
82
+ - **视觉模型定位为主力**:Qt/WPF/Web 等自绘控件 UIAutomation 支持有限,视觉定位通用;
83
+ 模板匹配作为"常用元素"的加速通道;像素特征零依赖兜底
84
+ - **操作与定位分离**:`gui_find` 只回答"在哪",`gui_click/drag` 只负责"执行",
85
+ AI 可组合出任意流程
86
+ - **轻量**:核心依赖仅 pyautogui;模板匹配/视觉模型均为可选增强
@@ -0,0 +1,80 @@
1
+ """NF-GUI 自动化插件 —— 让 AI 获得操作计算机鼠标/键盘/屏幕的能力。
2
+
3
+ 安装方式(二选一):
4
+ 1) 用户级(所有项目可用): 把整个 guiauto/ 目录复制到
5
+ ~/.nexforge/plugins/guiauto/
6
+ 2) 项目级(仅当前项目): 复制到
7
+ <项目根>/.nexforge/plugins/guiauto/
8
+
9
+ 复制后重启 nf(或 /plugins reload)即生效。验证: 运行 gui_screen 应成功截图。
10
+
11
+ 提供 8 个 AI 工具(均以 gui_ 前缀, 不与内置工具冲突):
12
+ gui_screen 截取屏幕/窗口/区域(GUI 自动化第一步)
13
+ gui_find 定位元素中心坐标(模板匹配/视觉模型/像素特征 三选一)
14
+ gui_click 点击(左/右/中, 单击/双击, 窗口内偏移)
15
+ gui_drag 拖拽(直线/曲线, 分段控制点, 按住停顿)
16
+ gui_type 输入文本(中文自动走剪贴板)
17
+ gui_key 按键/组合键(enter/ctrl+c/alt+f4)
18
+ gui_scroll 滚动
19
+ gui_windows 窗口管理(枚举/激活/几何/最小化/最大化/关闭)
20
+
21
+ 典型工作流: gui_screen 看界面 → gui_find 定位 → gui_click/gui_drag 操作
22
+ → gui_screen 验证结果。
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ __version__ = "0.1.0"
28
+
29
+ from . import base as B
30
+
31
+
32
+ def register(api):
33
+ """插件宿主入口: 注册 GUI 自动化工具 + 技能提示。"""
34
+ from .tools import (
35
+ GuiClickTool,
36
+ GuiDragTool,
37
+ GuiFindTool,
38
+ GuiKeyTool,
39
+ GuiScreenTool,
40
+ GuiScrollTool,
41
+ GuiTypeTool,
42
+ GuiWindowsTool,
43
+ )
44
+
45
+ for tool in (
46
+ GuiScreenTool(),
47
+ GuiFindTool(),
48
+ GuiClickTool(),
49
+ GuiDragTool(),
50
+ GuiTypeTool(),
51
+ GuiKeyTool(),
52
+ GuiScrollTool(),
53
+ GuiWindowsTool(),
54
+ ):
55
+ try:
56
+ api.register_tool(tool)
57
+ except Exception:
58
+ pass # 单个工具注册失败不阻塞其他
59
+
60
+ api.register_skill("gui-auto", _skill_hint)
61
+
62
+
63
+ def _skill_hint(screen=None) -> str | None:
64
+ """技能提示: 每轮注入 system, 引导 AI 在 GUI 任务时使用工具链。"""
65
+ try:
66
+ err = B.ensure_ready()
67
+ if err:
68
+ return None # pyautogui 未装, 不注入
69
+ return (
70
+ "你有 GUI 自动化工具(gui_*): 需要操作/测试图形界面时, 按此流程:\n"
71
+ "1. gui_windows list 找目标窗口(或用 handle 激活)\n"
72
+ "2. gui_screen 截图看当前界面\n"
73
+ "3. gui_find 定位元素中心坐标(template=图标模板 / prompt=中文描述 / color=颜色)\n"
74
+ "4. gui_click / gui_drag / gui_type / gui_key 执行操作\n"
75
+ "5. 再 gui_screen 截图验证结果\n"
76
+ "坐标均为屏幕逻辑像素; 拖拽滑块/调整大小用 gui_drag(curve=true 更真实); "
77
+ "中文输入用 gui_type; 操作前会自动存档 before 截图到 .nexforge/gui_logs/。"
78
+ )
79
+ except Exception:
80
+ return None
@@ -0,0 +1,194 @@
1
+ """NF-GUI 操作执行 —— 「手」。
2
+
3
+ 基于 pyautogui 的鼠标/键盘操作, 统一坐标(逻辑像素):
4
+ - click: 点击(左/右/双击/三击)
5
+ - move: 移动(瞬时/平滑/指定步数曲线)
6
+ - drag: 拖拽(直线/分段曲线, 支持中间停顿)
7
+ - type: 输入文本(ASCII/Unicode, 中文自动走剪贴板)
8
+ - key: 按/组合键
9
+ - scroll: 滚动
10
+
11
+ 所有操作前可 `pre_action` 回调(如在 .nexforge/temp 存操作前后截图)。
12
+ 鼠标动作间有 PAUSE 间隔 + FAILSAFE 保护。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import time
18
+ from typing import Callable
19
+
20
+ from . import base as B
21
+
22
+ # 操作日志(供 AI 回顾)
23
+ _OP_LOG: list[dict] = []
24
+
25
+
26
+ def op_log() -> list[dict]:
27
+ """返回本会话操作日志(最近 N 条)。"""
28
+ return _OP_LOG[-100:]
29
+
30
+
31
+ def _log(kind: str, **info) -> None:
32
+ info["kind"] = kind
33
+ info["t"] = time.strftime("%H:%M:%S")
34
+ _OP_LOG.append(info)
35
+ if len(_OP_LOG) > 200:
36
+ _OP_LOG.pop(0)
37
+
38
+
39
+ def _clamp(x: int, y: int) -> tuple[int, int]:
40
+ """坐标钳制到屏幕范围内(防误点出界)。"""
41
+ s = B.screen_size()
42
+ x = max(0, min(int(x), max(0, s["width"] - 1)))
43
+ y = max(0, min(int(y), max(0, s["height"] - 1)))
44
+ return x, y
45
+
46
+
47
+ def _pg():
48
+ if not B.PY_AUTOGUI_OK:
49
+ raise RuntimeError(B._dependency_error())
50
+ return B._pg
51
+
52
+
53
+ # ── 鼠标基础 ──────────────────────────────────────────────
54
+ def click(x: int, y: int, button: str = "left", clicks: int = 1,
55
+ interval: float = 0.2, **kw) -> dict:
56
+ """点击。button=left/right/middle; clicks=次数(双击=2)。"""
57
+ x, y = _clamp(x, y)
58
+ pg = _pg()
59
+ pg.click(x, y, clicks=max(1, min(int(clicks), 3)),
60
+ button=button, interval=max(0.0, float(interval)))
61
+ _log("click", x=x, y=y, button=button, clicks=clicks)
62
+ return {"ok": True, "x": x, "y": y, "button": button, "clicks": clicks}
63
+
64
+
65
+ def move(x: int, y: int, duration: float = 0.3, steps: int = 20, **kw) -> dict:
66
+ """移动鼠标。duration=总时长, steps=控制点数量(越大越平滑, 可模拟曲线)。"""
67
+ x, y = _clamp(x, y)
68
+ pg = _pg()
69
+ pg.moveTo(x, y, duration=max(0.05, float(duration)), tween=_tween(steps))
70
+ _log("move", x=x, y=y, duration=duration)
71
+ return {"ok": True, "x": x, "y": y}
72
+
73
+
74
+ def _tween(steps: int):
75
+ """生成平滑 tween(步数控制曲线平滑度)。"""
76
+ import pytweening
77
+ steps = max(1, min(int(steps), 100))
78
+ # 用 easeOutQuad 抛物线平滑(近似人类拖动, 避免瞬移)
79
+ return pytweening.easeOutQuad
80
+
81
+
82
+ def drag(x1: int, y1: int, x2: int, y2: int, duration: float = 0.8,
83
+ steps: int = 30, hold_ms: int = 200, button: str = "left",
84
+ curve: bool = False, **kw) -> dict:
85
+ """拖拽: 按住从 (x1,y1) 移动到 (x2,y2) 释放。
86
+
87
+ - steps: 中间控制点数量(越多越平滑, 支持精确路径)
88
+ - hold_ms: 按住后停顿(某些控件需要先稳定再拖)
89
+ - curve: 用曲线路径(模拟真实拖拽, 遍历中间插值点)
90
+ """
91
+ x1, y1 = _clamp(x1, y1)
92
+ x2, y2 = _clamp(x2, y2)
93
+ pg = _pg()
94
+ pg.moveTo(x1, y1)
95
+ pg.mouseDown(button=button)
96
+ if hold_ms > 0:
97
+ time.sleep(hold_ms / 1000.0)
98
+ steps = max(2, min(int(steps), 200))
99
+ if curve:
100
+ # 曲线路径: 用 tween 在 start/end 间插值
101
+ import pytweening
102
+ dur = max(0.2, float(duration))
103
+ start = time.monotonic()
104
+ while True:
105
+ t = (time.monotonic() - start) / dur
106
+ if t >= 1:
107
+ break
108
+ e = pytweening.easeInOutQuad(min(1.0, t))
109
+ cx = int(x1 + (x2 - x1) * e)
110
+ cy = int(y1 + (y2 - y1) * e)
111
+ pg.moveTo(cx, cy)
112
+ time.sleep(0.01)
113
+ else:
114
+ pg.moveTo(x2, y2, duration=max(0.1, float(duration)))
115
+ time.sleep(0.05)
116
+ pg.mouseUp(button=button)
117
+ _log("drag", x1=x1, y1=y1, x2=x2, y2=y2, steps=steps, button=button, curve=curve)
118
+ return {"ok": True, "from": [x1, y1], "to": [x2, y2],
119
+ "steps": steps, "button": button, "curve": curve}
120
+
121
+
122
+ def scroll(x: int, y: int, amount: int = -3, **kw) -> dict:
123
+ """滚动。amount>0 上滚, <0 下滚; x,y 为滚动时鼠标位置。"""
124
+ x, y = _clamp(x, y)
125
+ pg = _pg()
126
+ pg.moveTo(x, y)
127
+ pg.scroll(int(amount), x=x, y=y)
128
+ _log("scroll", x=x, y=y, amount=amount)
129
+ return {"ok": True, "x": x, "y": y, "amount": amount}
130
+
131
+
132
+ # ── 键盘 ──────────────────────────────────────────────────
133
+ def type_text(text: str, interval: float = 0.02, **kw) -> dict:
134
+ """输入文本。
135
+
136
+ 全 ASCII/常见符号 → 逐字符 typewrite(快)。
137
+ 中文/Unicode → 走剪贴板+Ctrl+V(准确, 不受键盘布局影响)。
138
+ """
139
+ pg = _pg()
140
+ try:
141
+ # 判断是否需要剪贴板(含非 ASCII 即走剪贴板)
142
+ if text and any(ord(c) > 127 for c in text):
143
+ import pyperclip
144
+ pyperclip.copy(text)
145
+ pg.hotkey("ctrl", "v")
146
+ else:
147
+ pg.typewrite(text, interval=max(0.0, float(interval)))
148
+ except Exception as e:
149
+ return {"ok": False, "error": f"键盘输入失败: {e}"}
150
+ _log("type", text=text[:40])
151
+ return {"ok": True, "chars": len(text)}
152
+
153
+
154
+ def key(keys, **kw) -> dict:
155
+ """按键/组合键。keys 可为 "enter" 或 ["ctrl", "c"] 组合。"""
156
+ pg = _pg()
157
+ try:
158
+ if isinstance(keys, str):
159
+ if "+" in keys:
160
+ parts = [p.strip() for p in keys.split("+")]
161
+ pg.hotkey(*parts)
162
+ else:
163
+ pg.press(keys)
164
+ elif isinstance(keys, (list, tuple)):
165
+ pg.hotkey(*keys)
166
+ else:
167
+ return {"ok": False, "error": f"keys 类型错误: {type(keys)}"}
168
+ except Exception as e:
169
+ return {"ok": False, "error": f"按键失败: {e}"}
170
+ _log("key", keys=keys)
171
+ return {"ok": True, "keys": keys}
172
+
173
+
174
+ def hotkey(*keys) -> dict:
175
+ """组合键(如 ctrl, shift, s)。"""
176
+ return key(list(keys))
177
+
178
+
179
+ # ── 前置操作钩子(截图存档) ────────────────────────────────
180
+ _pre_hook: Callable[[str], None] | None = None
181
+
182
+
183
+ def set_pre_hook(fn: Callable[[str], None] | None) -> None:
184
+ """设置每个动作前的回调(如存档 before 截图)。"""
185
+ global _pre_hook
186
+ _pre_hook = fn
187
+
188
+
189
+ def _notify(kind: str) -> None:
190
+ if _pre_hook:
191
+ try:
192
+ _pre_hook(kind)
193
+ except Exception:
194
+ pass
@@ -0,0 +1,161 @@
1
+ """NF-GUI 核心底座 —— 跨平台封装(Windows/macOS/Linux) + 安全 + 坐标系统。
2
+
3
+ 设计原则:
4
+ - 所有鼠标/键盘操作基于 pyautogui(纯 Python, 跨 Win/macOS/Linux)。
5
+ - 全平台统一坐标: 逻辑像素坐标(与 DPI 无关的屏幕逻辑坐标)。
6
+ - 安全: 全局 FAILSAFE(鼠标到左上角即中断) + 操作间隔 + 可配置。
7
+ - DPI: 自动感知系统缩放, 保证坐标精确(Windows 高 DPI 尤其关键)。
8
+ - 平台差异: Windows 用 ctypes 富窗口控制, Linux 尝试 xdotool 增强。
9
+
10
+ 本模块只做底层封装, 不暴露给 AI; AI 通过工具层(gui_tools)使用。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import platform
17
+ import sys
18
+ import time
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Any, Callable
22
+
23
+ # ── 第三方依赖(缺则降级提示) ─────────────────────────────
24
+ try:
25
+ import pyautogui as _pg
26
+ _pg.PAUSE = 0.05 # 每个动作间隔(防过快)
27
+ _pg.FAILSAFE = True # 鼠标甩到左上角中断
28
+ PY_AUTOGUI_OK = True
29
+ except ImportError:
30
+ _pg = None
31
+ PY_AUTOGUI_OK = False
32
+
33
+
34
+ # ── 平台常量 ─────────────────────────────────────────────
35
+ def detect_platform() -> str:
36
+ """返回 'windows' / 'macos' / 'linux'(小写)。"""
37
+ s = platform.system().lower()
38
+ if s == "darwin":
39
+ return "macos"
40
+ if s.startswith("win"):
41
+ return "windows"
42
+ return "linux"
43
+
44
+
45
+ PLATFORM = detect_platform()
46
+ IS_WINDOWS = PLATFORM == "windows"
47
+ IS_MACOS = PLATFORM == "macos"
48
+ IS_LINUX = PLATFORM == "linux"
49
+
50
+
51
+ # ── 依赖缺失诊断 ──────────────────────────────────────────
52
+ def _dependency_error() -> str:
53
+ return (
54
+ "pyautogui 未安装。GUI 自动化需要它:\n"
55
+ " pip install pyautogui\n"
56
+ "(跨平台支持 Windows/macOS/Linux; Linux 下还需系统 X11/输入权限)"
57
+ )
58
+
59
+
60
+ def ensure_ready() -> str | None:
61
+ """检查环境, 返回错误信息或 None。"""
62
+ if not PY_AUTOGUI_OK:
63
+ return _dependency_error()
64
+ return None
65
+
66
+
67
+ # ── DPI 感知 ──────────────────────────────────────────────
68
+ _dpi_aware_done = False
69
+
70
+
71
+ def set_dpi_aware() -> None:
72
+ """Windows 高 DPI 感知(禁系统缩放, 保证坐标精确)。macOS/Linux 无需处理。"""
73
+ global _dpi_aware_done, _dpi_scale
74
+ if _dpi_aware_done:
75
+ return
76
+ _dpi_aware_done = True
77
+ if IS_WINDOWS:
78
+ try:
79
+ import ctypes
80
+ # PROCESS_SYSTEM_DPI_AWARE=1: 以物理像素为准, 坐标更精确
81
+ ctypes.windll.shcore.SetProcessDpiAwareness(1)
82
+ except Exception:
83
+ try:
84
+ import ctypes
85
+ ctypes.windll.user32.SetProcessDPIAware()
86
+ except Exception:
87
+ pass
88
+ try:
89
+ import ctypes
90
+ _dpi_scale = _pg.screenshot().size[0] / _pg.size().width
91
+ except Exception:
92
+ _dpi_scale = 1.0
93
+ else:
94
+ _dpi_scale = 1.0
95
+
96
+
97
+ _dpi_scale = 1.0
98
+
99
+
100
+ def dpi_scale() -> float:
101
+ """当前 DPI 缩放系数(截图像素 / 逻辑坐标)。用于截图坐标换算。"""
102
+ set_dpi_aware()
103
+ return _dpi_scale
104
+
105
+
106
+ # ── 屏幕信息 ──────────────────────────────────────────────
107
+ def screen_size() -> dict:
108
+ """屏幕逻辑坐标尺寸。"""
109
+ if not PY_AUTOGUI_OK:
110
+ return {"width": 0, "height": 0}
111
+ w, h = _pg.size()
112
+ return {"width": int(w), "height": int(h)}
113
+
114
+
115
+ def mouse_position() -> dict:
116
+ """当前鼠标位置(逻辑坐标)。"""
117
+ if not PY_AUTOGUI_OK:
118
+ return {"x": 0, "y": 0}
119
+ x, y = _pg.position()
120
+ return {"x": int(x), "y": int(y)}
121
+
122
+
123
+ # ── 截图(统一入口) ────────────────────────────────────────
124
+ def screenshot(path: str | Path | None = None, region: tuple[int, int, int, int] | None = None):
125
+ """截屏(可指定区域)。返回 PIL Image。region=(左, 上, 宽, 高) 逻辑坐标。"""
126
+ if not PY_AUTOGUI_OK:
127
+ return None
128
+ set_dpi_aware()
129
+ img = _pg.screenshot(region=region)
130
+ if path:
131
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
132
+ img.save(str(path), "PNG")
133
+ return img
134
+
135
+
136
+ # ── 安全轨道(fail-safe 区域外操作) ───────────────────────
137
+ def ensure_failsafe_on() -> bool:
138
+ """开启 fail-safe(鼠标甩到屏幕左上角三格即中断)。"""
139
+ if not PY_AUTOGUI_OK:
140
+ return False
141
+ _pg.FAILSAFE = True
142
+ return True
143
+
144
+
145
+ # ── 平台特有命令探测(Linux 增强) ──────────────────────────
146
+ def which(cmd: str) -> str | None:
147
+ """找系统命令路径。"""
148
+ import shutil
149
+ return shutil.which(cmd)
150
+
151
+
152
+ def linux_tools() -> dict:
153
+ """探测 Linux 上可用的 X11 增强工具。"""
154
+ if not IS_LINUX:
155
+ return {}
156
+ found = {}
157
+ for name in ("xdotool", "wmctrl", "import", "scrot"):
158
+ p = which(name)
159
+ if p:
160
+ found[name] = p
161
+ return found