nexforge-cli 0.2.13__tar.gz → 0.2.14__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.
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/PKG-INFO +2 -2
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/README.md +1 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/01-/345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md +3 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/03-/346/250/241/345/236/213/351/205/215/347/275/256.md +21 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/04-/345/267/245/345/205/267/345/217/202/350/200/203.md +1 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/08-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +1 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/10-/346/217/222/344/273/266/345/274/200/345/217/221.md +2 -2
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/README.md +1 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/pyproject.toml +1 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/__init__.py +1 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/app.py +31 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/config.py +10 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/agent.py +3 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/goal_manager.py +14 -2
- nexforge_cli-0.2.14/src/nexforge/core/model_catalog.py +114 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/provider.py +8 -0
- nexforge_cli-0.2.14/src/nexforge/core/update_check.py +99 -0
- nexforge_cli-0.2.14/src/nexforge/features/mode_features.py +686 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/session_feature.py +2 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/i18n.py +15 -1
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/provider/openai.py +8 -3
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/provider/qianwen.py +8 -0
- nexforge_cli-0.2.14/src/nexforge/store/director_store.py +113 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/theme.tcss +13 -4
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/screens/chat.py +190 -15
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/screens/chat_actions.py +29 -5
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/slash_handler.py +1 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/model_sidebar.py +48 -1
- nexforge_cli-0.2.14/tests/test_app_mouse.py +72 -0
- nexforge_cli-0.2.14/tests/test_director_loop.py +278 -0
- nexforge_cli-0.2.14/tests/test_director_store.py +65 -0
- nexforge_cli-0.2.14/tests/test_dual_panel.py +203 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_goal_confirm.py +3 -0
- nexforge_cli-0.2.14/tests/test_model_catalog.py +191 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_p2_core.py +21 -6
- nexforge_cli-0.2.14/tests/test_tool_iters.py +55 -0
- nexforge_cli-0.2.14/tests/test_update_check.py +122 -0
- nexforge_cli-0.2.13/src/nexforge/features/mode_features.py +0 -456
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/.gitignore +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/INSTALL.md +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/STATS_API.md +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/agent.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/cost.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/edit.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/err.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/file.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/model.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/ok.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/read.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/running.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/search.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/shell.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/thinking.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/web.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/assets/icons/write.svg +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/config.example.toml +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/devlog/2026-07-07.md +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/doc//344/273/243/347/240/201/344/274/230/345/214/226/350/256/241/345/210/222.md" +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/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
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/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
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/docs/submission/README-/346/212/225/347/250/277/350/257/264/346/230/216.md" +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/docs/submission/nexforge.md +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/docs/submission/nexforge.zh-CN.md +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/02-/347/225/214/351/235/242/344/270/216/345/277/253/346/215/267/351/224/256.md" +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/05-/346/225/260/346/215/256/344/270/216/346/220/234/347/264/242.md" +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/06-/346/235/203/351/231/220/344/270/216/346/250/241/345/274/217.md" +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/07-/344/274/232/350/257/235/344/270/216/350/256/260/345/277/206.md" +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual/09-/345/270/270/350/247/201/351/227/256/351/242/230.md" +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/manual//351/231/204/345/275/225-/345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/README.md +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/guiauto/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/guiauto/action.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/guiauto/base.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/guiauto/tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/guiauto/vision.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/guiauto/window.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/install_plugin.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/requirements.txt +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/smoke_safe.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/verify_final.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/verify_host.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/plugin_gui_auto/verify_plugin.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/scripts/publish.sh +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/scripts/smoke_provider.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/scripts/smoke_tui.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/__main__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/cli.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/context.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/federation.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/jobs.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/knowledge_base.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/memory_engine.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/permissions.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/plugin.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/project_mapper.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/router.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/session.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/summarizer.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/system_inject.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/tool_manifest.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/core/types.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/auto_skills.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/base.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/federation_feature.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/kb_feature.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/map_feature.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/memory_feature.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/plan_feature.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/registry.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/slash_feature.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/stats_feature.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/features/tool_manifest_feature.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/icons.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/mcp/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/provider/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/provider/_retry.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/provider/anthropic.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/provider/balance.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/provider/deepseek.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/skills/loader.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/stats/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/stats/heartbeat.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/store/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/store/plan_store.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/store/session_store.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/_paths.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/background.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/base.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/data_tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/edit_file.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/federation.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/fs_ops.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/git.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/image_tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/kb_tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/list_dir.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/multi_edit.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/ops_tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/read_file.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/registry.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/screenshot_tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/search.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/search_content.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/search_files.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/shell.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/web.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/tools/write_file.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/clipboard_image.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/screen_api.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/screens/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/screens/splash.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/banner.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/diff_modal.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/image_sidebar.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/messages.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/mode_warning.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/sidebar.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/ui/widgets/statusbar.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/utils/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/utils/logging.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/web/__init__.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/web/server.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/src/nexforge/web/static/index.html +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/conftest.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_approval.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_config.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_cross_platform.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_federation.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_i18n.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_icons.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_lang_cmd.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_m2_tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_manual_consistency.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_plugin.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_sanitize.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_screenshot_tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_session_repair.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_session_store.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_sidebar.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_sidebar_actions.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_splash_modal.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_statusbar.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_system_inject.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_tools.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_tui.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_vision_model.py +0 -0
- {nexforge_cli-0.2.13 → nexforge_cli-0.2.14}/tests/test_vision_tool_retention.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nexforge-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
4
4
|
Summary: NexForgeCLI — 一个面向 DeepSeek V4 的终端智能体(TUI + CLI 双模式)
|
|
5
5
|
Project-URL: Homepage, https://gitee.com/Lighthorn/nexforgecli
|
|
6
6
|
Author: NexForge
|
|
@@ -51,7 +51,7 @@ Description-Content-Type: text/markdown
|
|
|
51
51
|
|
|
52
52
|
NexForgeCLI 是一个运行在终端里的智能体助手,既支持 DeepSeek/千问等多模型,也是**插件宿主**(plugins 目录即插即用)。既能作为全屏 TUI 交互式对话/编码,也能作为 one-shot 命令行工具嵌进脚本和管道。
|
|
53
53
|
|
|
54
|
-
> 当前版本:**v0.2.
|
|
54
|
+
> 当前版本:**v0.2.14** · PyPI: `pip install nexforge-cli`
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 安装与配置
|
|
2
2
|
|
|
3
|
-
> NexForgeCLI v0.2.
|
|
3
|
+
> NexForgeCLI v0.2.14
|
|
4
4
|
|
|
5
5
|
## 环境要求
|
|
6
6
|
|
|
@@ -72,6 +72,8 @@ API Key 保存在 `~/.nexforge/auth.toml`,不会被 Git 追踪。
|
|
|
72
72
|
pip install --upgrade nexforge-cli
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
启动 nf 时会自动检查 PyPI 最新版本(24 小时缓存一次,失败静默),若有新版本会在界面提示升级命令。
|
|
76
|
+
|
|
75
77
|
## 卸载
|
|
76
78
|
|
|
77
79
|
```bash
|
|
@@ -125,6 +125,27 @@ models = ["claude-sonnet-4-20250514"]
|
|
|
125
125
|
|
|
126
126
|
切换后自动持久化,重启 nf 保持选中。
|
|
127
127
|
|
|
128
|
+
### 动态模型列表(千问 / OpenAI 兼容)
|
|
129
|
+
|
|
130
|
+
大模型更新太快,`models.toml` 里手写的模型名可能过时。对 **千问** 和 **OpenAI 兼容**(`type = "openai"`)provider,F6 侧边栏会:
|
|
131
|
+
|
|
132
|
+
- 自动调用该平台的 `/models` 接口,拉取当前 API Key 下**实际可用**的模型列表
|
|
133
|
+
- 拉取结果缓存 24 小时,失败时回退 `models.toml` 里写的静态列表
|
|
134
|
+
- 每个模型名都是一个可点击按钮,直接点选切换具体模型(当前使用的模型标绿点 ●)
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
│ ● qianwen ✓ │
|
|
138
|
+
│ 类型: qianwen │
|
|
139
|
+
│ ● qwen3.8-max │ ← 当前使用
|
|
140
|
+
│ qwen-vision-plus │ ← 动态拉取到的新模型, 点选即切换
|
|
141
|
+
│ qwen-long │
|
|
142
|
+
│ [🔄 刷新模型] │ ← 强制重新拉取最新列表
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
点「🔄 刷新模型」可强制清缓存重新拉取(比如平台刚上线了新模型)。
|
|
146
|
+
|
|
147
|
+
**选择持久化**:点选的具体模型记录到 `~/.nexforge/model_selection.json`(独立于只读的 `models.toml`,不污染配置),重启 nf 保持选中。
|
|
148
|
+
|
|
128
149
|
## 自动路由 vs 多模型切换
|
|
129
150
|
|
|
130
151
|
- **自动路由**(DeepSeek auto 档):同一个模型内,根据任务复杂度自动选择 flash 或 pro
|
|
@@ -87,7 +87,7 @@ NexForgeCLI 内置 **35 个工具**供 AI 调用(`git_commit` 需在 config.to
|
|
|
87
87
|
|
|
88
88
|
| 限制 | 值 | 说明 |
|
|
89
89
|
|------|-----|------|
|
|
90
|
-
| 工具调用轮数 |
|
|
90
|
+
| 工具调用轮数 | 300(目标模式 500) | 防死循环; 单轮内连续调工具上限, 目标模式自动放大 |
|
|
91
91
|
| 单轮总超时 | 0 秒(不限额) | 默认不限额; 可 /timeout <秒> 或 config.toml 的 `turn_timeout` 设置 |
|
|
92
92
|
| 单工具超时 | 60 秒 | |
|
|
93
93
|
|
|
@@ -59,6 +59,7 @@ model = "qwen-audio-3.0-tts-flash" # 千问 TTS 模型
|
|
|
59
59
|
voice = "loongmary" # 音色(flash 用 loong 系列: loongmary/loongeva/loongJohn 等)
|
|
60
60
|
engine = "auto" # auto | qianwen | local
|
|
61
61
|
max_chars = 100 # 简报最大字符数
|
|
62
|
+
enabled = true # 语音播报总开关
|
|
62
63
|
|
|
63
64
|
[search]
|
|
64
65
|
engine = "bing" # bing | duckduckgo | google | searxng
|
|
@@ -70,9 +70,9 @@ def register(api):
|
|
|
70
70
|
|
|
71
71
|
`~/.nexforge/plugins/speech_summary.py`(已随本机安装)——每轮任务结束用 TTS 播报简报:
|
|
72
72
|
|
|
73
|
-
- **引擎**:千问语音合成(qwen-audio-3.0-tts-
|
|
73
|
+
- **引擎**:千问语音合成(qwen-audio-3.0-tts-flash,官方 dashscope SDK WebSocket 方式)优先,失败自动回退 Windows 本地语音(SAPI)
|
|
74
74
|
- **配置**(config.toml `[tts]` 段):`engine=auto|qianwen|local`、`voice`、`api_key`(留空用 [image] 按量 key)、`ws_url`、`max_chars`、`enabled`
|
|
75
75
|
- **不阻塞**:合成+播放在后台线程,回合结束流程零延迟
|
|
76
76
|
- **简报内容**:项目名 + 工具调用次数 + AI 最终总结摘要(自动清理 Markdown 符号)
|
|
77
77
|
|
|
78
|
-
> 千问 TTS 使用条件:① `pip install dashscope`;② 平台控制台**开通 qwen-audio-3.0-tts-
|
|
78
|
+
> 千问 TTS 使用条件:① `pip install dashscope`;② 平台控制台**开通 qwen-audio-3.0-tts-flash 按量服务**(¥1.4/万字符,Token Plan 套餐不含)。未满足时自动回退本地语音,不打断工作。
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
@@ -53,10 +54,39 @@ class NexForgeApp(App):
|
|
|
53
54
|
return ChatScreen(config=self._config, resume_session_id=None)
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
def _mouse_enabled() -> bool:
|
|
58
|
+
"""决定是否启用鼠标追踪(Textual 的 app.run(mouse=...))。
|
|
59
|
+
|
|
60
|
+
Windows 老式控制台(conhost)下, Textual 会开启鼠标追踪
|
|
61
|
+
(ESC[?1003h 任意事件 + ESC[?1006h SGR 模式), 鼠标一移动就产生海量转义序列。
|
|
62
|
+
长时间搁置/窗口失焦恢复后, 积压的鼠标事件会淹没输入处理导致假死,
|
|
63
|
+
且若退出时终端状态未正确恢复, 这些序列会泄漏到命令行(表现为
|
|
64
|
+
动鼠标就刷出一堆 [555;78;27M 之类的字符)。
|
|
65
|
+
|
|
66
|
+
判定规则(优先级从高到低):
|
|
67
|
+
1. 环境变量 NEXFORGE_MOUSE=1/0 跨平台强制覆盖(逃生舱);
|
|
68
|
+
2. 非 Windows → 始终开鼠标;
|
|
69
|
+
3. Windows 下仅"纯老式 conhost"(无 ConPTY 标记)禁用鼠标。
|
|
70
|
+
Windows Terminal 设 WT_SESSION, VS Code 集成终端设 TERM_PROGRAM=vscode,
|
|
71
|
+
二者基于 ConPTY(VT 支持完善), 保留鼠标;
|
|
72
|
+
老式 cmd/PowerShell 窗口两者皆无 → 禁用, 避免搁置后鼠标序列泄漏。
|
|
73
|
+
"""
|
|
74
|
+
env = os.environ.get("NEXFORGE_MOUSE")
|
|
75
|
+
if env is not None:
|
|
76
|
+
return env.strip().lower() in ("1", "true", "yes", "on")
|
|
77
|
+
if sys.platform != "win32":
|
|
78
|
+
return True
|
|
79
|
+
if os.environ.get("WT_SESSION"):
|
|
80
|
+
return True
|
|
81
|
+
if os.environ.get("TERM_PROGRAM", "").lower() == "vscode":
|
|
82
|
+
return True
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
56
86
|
def run_tui(config: "NexForgeConfig", *, resume: bool = False) -> int:
|
|
57
87
|
app = NexForgeApp(config, resume=resume)
|
|
58
88
|
try:
|
|
59
|
-
app.run()
|
|
89
|
+
app.run(mouse=_mouse_enabled())
|
|
60
90
|
except KeyboardInterrupt:
|
|
61
91
|
return 0
|
|
62
92
|
return 0
|
|
@@ -114,7 +114,7 @@ class UIConfig(BaseModel):
|
|
|
114
114
|
|
|
115
115
|
class ToolsConfig(BaseModel):
|
|
116
116
|
enable_git_commit: bool = False
|
|
117
|
-
max_tool_iters: int =
|
|
117
|
+
max_tool_iters: int = 300 # 工具轮数安全上限(防死循环); 目标模式自动放大到 500
|
|
118
118
|
turn_timeout: int = 0 # 单轮总超时(秒); 0=不限额(用户按需设置)
|
|
119
119
|
tool_timeout: int = 60 # 单个工具超时(秒)
|
|
120
120
|
|
|
@@ -275,6 +275,15 @@ def load_config(path: Path | None = None) -> NexForgeConfig:
|
|
|
275
275
|
cfg.providers[name] = ProviderDef(**entry)
|
|
276
276
|
except Exception:
|
|
277
277
|
pass
|
|
278
|
+
# 读用户选择的模型(独立于只读 models.toml, 见 core/model_catalog.py)
|
|
279
|
+
try:
|
|
280
|
+
from nexforge.core.model_catalog import load_selection
|
|
281
|
+
selection = load_selection()
|
|
282
|
+
for sel_name, sel_model in selection.items():
|
|
283
|
+
if sel_name in cfg.providers and sel_model:
|
|
284
|
+
cfg.providers[sel_name].default_model = sel_model
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
278
287
|
# 恢复已验证的 provider 列表(从项目配置的 provider.verified 字段)
|
|
279
288
|
verified_list: list[str] = []
|
|
280
289
|
if path is None:
|
|
@@ -89,7 +89,10 @@ class AgentLoop:
|
|
|
89
89
|
last_failed: tuple[str, str] = ("", "")
|
|
90
90
|
consecutive_fails = 0
|
|
91
91
|
try:
|
|
92
|
+
# 工具轮数上限: 目标模式放开到 500(多轮开发), 普通会话 300
|
|
92
93
|
limit = self.config.tools.max_tool_iters
|
|
94
|
+
if getattr(self.config.ui, "goal_mode", False):
|
|
95
|
+
limit = max(limit, 500)
|
|
93
96
|
for _ in range(limit):
|
|
94
97
|
decision = self.router.choose(self.session, user_text, tool_failures)
|
|
95
98
|
yield AgentEvent(AgentEventKind.ROUTE, route=decision)
|
|
@@ -113,7 +113,7 @@ class GoalManager:
|
|
|
113
113
|
def read_goal(self) -> str:
|
|
114
114
|
from nexforge.tools._paths import sanitize
|
|
115
115
|
if self.goal_path.exists():
|
|
116
|
-
return sanitize(self.goal_path.read_text(encoding="utf-8"))
|
|
116
|
+
return self._strip_md_title(sanitize(self.goal_path.read_text(encoding="utf-8")))
|
|
117
117
|
return ""
|
|
118
118
|
|
|
119
119
|
def set_plan(self, plan: str) -> None:
|
|
@@ -123,9 +123,21 @@ class GoalManager:
|
|
|
123
123
|
def read_plan(self) -> str:
|
|
124
124
|
from nexforge.tools._paths import sanitize
|
|
125
125
|
if self.plan_path.exists():
|
|
126
|
-
return sanitize(self.plan_path.read_text(encoding="utf-8"))
|
|
126
|
+
return self._strip_md_title(sanitize(self.plan_path.read_text(encoding="utf-8")))
|
|
127
127
|
return ""
|
|
128
128
|
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _strip_md_title(content: str) -> str:
|
|
131
|
+
"""去掉 Markdown '# 标题' 首行, 返回纯净正文。
|
|
132
|
+
|
|
133
|
+
修复: 之前 read_goal/read_plan 返回含标题前缀的完整文件内容,
|
|
134
|
+
amend_plan 再 set_goal 会重复累加 '# 目标' 前缀污染文件。
|
|
135
|
+
"""
|
|
136
|
+
lines = content.split("\n", 1)
|
|
137
|
+
if len(lines) > 1 and lines[0].startswith("#"):
|
|
138
|
+
return lines[1].strip()
|
|
139
|
+
return content.strip()
|
|
140
|
+
|
|
129
141
|
def save_round(self, num: int, dev_report: str, director_review: str) -> None:
|
|
130
142
|
from nexforge.tools._paths import sanitize
|
|
131
143
|
dev_report = sanitize(dev_report)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""动态模型目录 —— 拉取 provider 可用模型列表并缓存。
|
|
2
|
+
|
|
3
|
+
解决"大模型更新太快, 每次都要升级软件才能跟上"的问题:
|
|
4
|
+
- 对 OpenAI 兼容 provider(openai/qianwen) 调用 /models 接口拉取可用模型列表
|
|
5
|
+
- 缓存 24 小时, 失败回退 models.toml 静态列表
|
|
6
|
+
- 用户点选的具体模型持久化到 model_selection.json
|
|
7
|
+
(独立于只读的 models.toml, 不污染该文件)
|
|
8
|
+
|
|
9
|
+
数据文件:
|
|
10
|
+
- ``~/.nexforge/model_catalog.json`` — 动态拉取的模型列表缓存
|
|
11
|
+
- ``~/.nexforge/model_selection.json`` — 用户对每个 provider 选择的模型
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from nexforge.config import CONFIG_DIR
|
|
22
|
+
|
|
23
|
+
CATALOG_PATH = CONFIG_DIR / "model_catalog.json"
|
|
24
|
+
SELECTION_PATH = CONFIG_DIR / "model_selection.json"
|
|
25
|
+
CACHE_TTL_SECONDS = 24 * 3600 # 24 小时
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_json(path: Path) -> dict:
|
|
29
|
+
try:
|
|
30
|
+
if path.exists():
|
|
31
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _write_json(path: Path, data: dict) -> None:
|
|
38
|
+
try:
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
path.write_text(
|
|
41
|
+
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
42
|
+
)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_catalog() -> dict:
|
|
48
|
+
"""读动态模型缓存目录。"""
|
|
49
|
+
return _read_json(CATALOG_PATH)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_selection() -> dict[str, str]:
|
|
53
|
+
"""读用户模型选择 {provider_name: model_id}。"""
|
|
54
|
+
return _read_json(SELECTION_PATH)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def save_selection(name: str, model: str) -> None:
|
|
58
|
+
"""记录用户对某 provider 选择的模型。"""
|
|
59
|
+
sel = load_selection()
|
|
60
|
+
sel[name] = model
|
|
61
|
+
_write_json(SELECTION_PATH, sel)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_cached_models(name: str) -> list[str] | None:
|
|
65
|
+
"""返回缓存模型列表; 无缓存或已过期返回 None。"""
|
|
66
|
+
entry = load_catalog().get(name)
|
|
67
|
+
if not isinstance(entry, dict):
|
|
68
|
+
return None
|
|
69
|
+
fetched_at = entry.get("fetched_at", 0)
|
|
70
|
+
if time.time() - float(fetched_at) > CACHE_TTL_SECONDS:
|
|
71
|
+
return None
|
|
72
|
+
models = entry.get("models")
|
|
73
|
+
return models if isinstance(models, list) else None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_cached_models(name: str, models: list[str]) -> None:
|
|
77
|
+
cat = load_catalog()
|
|
78
|
+
cat[name] = {"fetched_at": time.time(), "models": list(models)}
|
|
79
|
+
_write_json(CATALOG_PATH, cat)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def clear_cache(name: str | None = None) -> None:
|
|
83
|
+
"""清除动态模型缓存。name 为 None 时清空全部。"""
|
|
84
|
+
if name is None:
|
|
85
|
+
_write_json(CATALOG_PATH, {})
|
|
86
|
+
else:
|
|
87
|
+
cat = load_catalog()
|
|
88
|
+
cat.pop(name, None)
|
|
89
|
+
_write_json(CATALOG_PATH, cat)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def fetch_models(config: Any, name: str, pdef: Any) -> list[str]:
|
|
93
|
+
"""拉取 provider 可用模型列表(带缓存)。
|
|
94
|
+
|
|
95
|
+
成功且非空 → 返回动态列表并写缓存;
|
|
96
|
+
失败或为空 → 回退 pdef.models 静态列表(不写缓存, 允许下次重试)。
|
|
97
|
+
"""
|
|
98
|
+
cached = get_cached_models(name)
|
|
99
|
+
if cached:
|
|
100
|
+
return cached
|
|
101
|
+
|
|
102
|
+
dynamic: list[str] = []
|
|
103
|
+
try:
|
|
104
|
+
from nexforge.provider import create_provider
|
|
105
|
+
provider = create_provider(config, name=name)
|
|
106
|
+
dynamic = await provider.list_models()
|
|
107
|
+
await provider.aclose()
|
|
108
|
+
except Exception:
|
|
109
|
+
dynamic = []
|
|
110
|
+
|
|
111
|
+
if dynamic:
|
|
112
|
+
set_cached_models(name, dynamic)
|
|
113
|
+
return dynamic
|
|
114
|
+
return list(pdef.models) if pdef else []
|
|
@@ -36,6 +36,14 @@ class BaseProvider(ABC):
|
|
|
36
36
|
"""
|
|
37
37
|
return self.supports_multimodal
|
|
38
38
|
|
|
39
|
+
async def list_models(self) -> list[str]:
|
|
40
|
+
"""拉取当前 API key 下可用的模型 ID 列表。
|
|
41
|
+
|
|
42
|
+
默认返回空(不支持动态列举的 provider 保持空)。
|
|
43
|
+
OpenAI 兼容 provider(openai/qianwen)覆写此方法调用 /models 接口。
|
|
44
|
+
"""
|
|
45
|
+
return []
|
|
46
|
+
|
|
39
47
|
@abstractmethod
|
|
40
48
|
async def stream(
|
|
41
49
|
self,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""PyPI 自升级检查。
|
|
2
|
+
|
|
3
|
+
启动时异步检查 PyPI 上 nexforge-cli 的最新版本, 若有更新则提示用户。
|
|
4
|
+
设计要点:
|
|
5
|
+
- 24 小时缓存(避免每次启动都请求 PyPI, 且网络失败不阻断启动)
|
|
6
|
+
- 版本比较用标准库实现(点分数字, 忽略 v 前缀与预发布后缀), 不引入 packaging
|
|
7
|
+
- 检查失败静默返回 None(无更新提示)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from nexforge.config import CONFIG_DIR
|
|
16
|
+
|
|
17
|
+
PYPI_URL = "https://pypi.org/pypi/nexforge-cli/json"
|
|
18
|
+
CHECK_INTERVAL_SECONDS = 24 * 3600
|
|
19
|
+
_CACHE_PATH = CONFIG_DIR / ".update_check"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def compare_versions(a: str, b: str) -> int:
|
|
23
|
+
"""比较两个点分版本号。返回 a>b:1, a<b:-1, a==b:0。
|
|
24
|
+
|
|
25
|
+
仅比较数字段(忽略 v 前缀、-rc1/-dev1 等预发布后缀),
|
|
26
|
+
段数不足按 0 补齐, 避免字符串比较 "0.2.9" > "0.2.13" 的经典错误。
|
|
27
|
+
"""
|
|
28
|
+
def _parts(v: str) -> list[int]:
|
|
29
|
+
v = v.strip().lstrip("vV")
|
|
30
|
+
v = v.split("-")[0].split("+")[0]
|
|
31
|
+
out: list[int] = []
|
|
32
|
+
for part in v.split("."):
|
|
33
|
+
try:
|
|
34
|
+
out.append(int(part))
|
|
35
|
+
except ValueError:
|
|
36
|
+
out.append(0)
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
pa, pb = _parts(a), _parts(b)
|
|
40
|
+
n = max(len(pa), len(pb))
|
|
41
|
+
pa += [0] * (n - len(pa))
|
|
42
|
+
pb += [0] * (n - len(pb))
|
|
43
|
+
for x, y in zip(pa, pb):
|
|
44
|
+
if x != y:
|
|
45
|
+
return 1 if x > y else -1
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_cache() -> tuple[float, str] | None:
|
|
50
|
+
"""读缓存, 返回 (上次检查时间戳, 最新版本) 或 None。"""
|
|
51
|
+
try:
|
|
52
|
+
if _CACHE_PATH.exists():
|
|
53
|
+
text = _CACHE_PATH.read_text(encoding="utf-8").strip()
|
|
54
|
+
ts_str, _, version = text.partition(" ")
|
|
55
|
+
return float(ts_str), version.strip()
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _write_cache(version: str) -> None:
|
|
62
|
+
"""写缓存(时间戳 + 最新版本)。失败静默。"""
|
|
63
|
+
try:
|
|
64
|
+
_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
ts = datetime.now(timezone.utc).timestamp()
|
|
66
|
+
_CACHE_PATH.write_text(f"{ts} {version}\n", encoding="utf-8")
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def check_update(current_version: str) -> str | None:
|
|
72
|
+
"""检查是否有新版本。
|
|
73
|
+
|
|
74
|
+
返回最新版本号(若有更新)或 None(无更新 / 网络失败 / 缓存未过期)。
|
|
75
|
+
"""
|
|
76
|
+
import time
|
|
77
|
+
|
|
78
|
+
# 24 小时内已检查 → 用缓存结果
|
|
79
|
+
cached = _read_cache()
|
|
80
|
+
if cached:
|
|
81
|
+
ts, version = cached
|
|
82
|
+
if time.time() - ts < CHECK_INTERVAL_SECONDS:
|
|
83
|
+
return version if compare_versions(version, current_version) > 0 else None
|
|
84
|
+
|
|
85
|
+
# 请求 PyPI JSON API
|
|
86
|
+
try:
|
|
87
|
+
import httpx
|
|
88
|
+
async with httpx.AsyncClient(timeout=8) as client:
|
|
89
|
+
resp = await client.get(PYPI_URL)
|
|
90
|
+
resp.raise_for_status()
|
|
91
|
+
data = resp.json()
|
|
92
|
+
latest = str(data.get("info", {}).get("version", "")).strip()
|
|
93
|
+
except Exception:
|
|
94
|
+
return None # 网络失败静默
|
|
95
|
+
|
|
96
|
+
if not latest:
|
|
97
|
+
return None
|
|
98
|
+
_write_cache(latest)
|
|
99
|
+
return latest if compare_versions(latest, current_version) > 0 else None
|