nexforge-cli 0.2.15__tar.gz → 0.2.17__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.15 → nexforge_cli-0.2.17}/PKG-INFO +2 -2
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/README.md +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/01-/345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/03-/346/250/241/345/236/213/351/205/215/347/275/256.md +18 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/05-/346/225/260/346/215/256/344/270/216/346/220/234/347/264/242.md +6 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/08-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +4 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/README.md +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/pyproject.toml +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/__init__.py +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/config.py +4 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/goal_manager.py +7 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/knowledge_base.py +5 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/project_mapper.py +39 -8
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/provider.py +17 -0
- nexforge_cli-0.2.17/src/nexforge/features/map_feature.py +91 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/mode_features.py +47 -11
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/session_feature.py +22 -3
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/provider/deepseek.py +37 -6
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/provider/openai.py +7 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/store/session_store.py +11 -3
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/image_tools.py +9 -2
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/screens/chat.py +11 -5
- nexforge_cli-0.2.17/tests/test_deepseek_provider.py +54 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_director_loop.py +22 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_goal_confirm.py +74 -1
- nexforge_cli-0.2.17/tests/test_project_map_prune.py +105 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_session_store.py +82 -0
- nexforge_cli-0.2.17/tests/test_vision_model.py +130 -0
- nexforge_cli-0.2.15/src/nexforge/features/map_feature.py +0 -54
- nexforge_cli-0.2.15/tests/test_vision_model.py +0 -65
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/.gitignore +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/INSTALL.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/STATS_API.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/agent.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/cost.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/edit.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/err.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/file.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/model.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/ok.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/read.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/running.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/search.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/shell.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/thinking.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/web.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/assets/icons/write.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/config.example.toml +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/devlog/2026-07-07.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/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.15 → nexforge_cli-0.2.17}/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.15 → nexforge_cli-0.2.17}/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.15 → nexforge_cli-0.2.17}/docs/submission/README-/346/212/225/347/250/277/350/257/264/346/230/216.md" +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/docs/submission/nexforge.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/docs/submission/nexforge.zh-CN.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/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.15 → nexforge_cli-0.2.17}/manual/04-/345/267/245/345/205/267/345/217/202/350/200/203.md" +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/06-/346/235/203/351/231/220/344/270/216/346/250/241/345/274/217.md" +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/07-/344/274/232/350/257/235/344/270/216/350/256/260/345/277/206.md" +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/09-/345/270/270/350/247/201/351/227/256/351/242/230.md" +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/manual/10-/346/217/222/344/273/266/345/274/200/345/217/221.md" +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/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.15 → nexforge_cli-0.2.17}/plugin_gui_auto/README.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/guiauto/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/guiauto/action.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/guiauto/base.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/guiauto/tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/guiauto/vision.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/guiauto/window.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/install_plugin.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/requirements.txt +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/smoke_safe.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/verify_final.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/verify_host.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/plugin_gui_auto/verify_plugin.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/scripts/publish.sh +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/scripts/smoke_provider.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/scripts/smoke_tui.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/__main__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/app.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/cli.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/agent.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/context.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/federation.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/jobs.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/memory_engine.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/model_catalog.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/permissions.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/plugin.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/router.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/session.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/summarizer.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/system_inject.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/tool_manifest.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/types.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/core/update_check.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/auto_skills.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/base.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/federation_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/kb_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/memory_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/plan_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/registry.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/slash_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/stats_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/features/tool_manifest_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/i18n.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/icons.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/mcp/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/provider/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/provider/_retry.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/provider/anthropic.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/provider/balance.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/provider/qianwen.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/skills/loader.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/stats/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/stats/heartbeat.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/store/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/store/director_store.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/store/plan_store.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/theme.tcss +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/_paths.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/background.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/base.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/data_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/edit_file.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/federation.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/fs_ops.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/git.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/kb_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/list_dir.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/multi_edit.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/ops_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/read_file.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/registry.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/screenshot_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/search.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/search_content.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/search_files.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/shell.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/web.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/tools/write_file.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/clipboard_image.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/screen_api.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/screens/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/screens/chat_actions.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/screens/splash.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/slash_handler.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/banner.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/diff_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/file_browser.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/file_menu.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/image_sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/messages.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/mode_warning.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/model_sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/ui/widgets/statusbar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/utils/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/utils/logging.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/web/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/web/server.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/src/nexforge/web/static/index.html +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/conftest.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_app_mouse.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_approval.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_config.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_cross_platform.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_director_store.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_dual_panel.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_federation.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_file_browser.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_i18n.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_icons.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_lang_cmd.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_m2_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_manual_consistency.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_model_catalog.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_p2_core.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_plugin.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_sanitize.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_screenshot_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_session_repair.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_sidebar_actions.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_splash_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_statusbar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_system_inject.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_tool_iters.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_tui.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/tests/test_update_check.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.17}/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.17
|
|
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.17** · PyPI: `pip install nexforge-cli`
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
@@ -4,7 +4,7 @@ NexForgeCLI 支持多模型:
|
|
|
4
4
|
|
|
5
5
|
| 模型 | 支持方式 | 说明 |
|
|
6
6
|
|------|----------|------|
|
|
7
|
-
| **DeepSeek V4** | 内置 | flash/pro
|
|
7
|
+
| **DeepSeek V4** | 内置(models.toml 可扩展) | flash/pro/auto 档位开箱可用;也可在 models.toml 挂独立模型(如实验版) |
|
|
8
8
|
| **千问(qianwen)** | 专用底座 | 思考链流式显示 / enable_thinking 开关 / 多模态贴图直发(与 DeepSeek 平级) |
|
|
9
9
|
| **OpenAI 兼容** | models.toml | Kimi/GPT/通义千问/智谱等一切 OpenAI 兼容接口 |
|
|
10
10
|
| **Anthropic** | models.toml | Claude 系列(需安装额外依赖) |
|
|
@@ -34,6 +34,19 @@ auto 模式下的升级触发条件:
|
|
|
34
34
|
- 默认开启,实时显示 AI 的推理过程(可折叠面板)
|
|
35
35
|
- `/think off` 关闭思考(速度更快、费用更低)
|
|
36
36
|
|
|
37
|
+
**挂独立模型**:DeepSeek 的 flash/pro 是内置档位,若想使用内置档位之外的模型(如限时实验版 `deepseek-v4.1-flash-expires-on-0910`),可在 `~/.nexforge/models.toml` 加一个 `type = "deepseek"` 的 provider:
|
|
38
|
+
|
|
39
|
+
```toml
|
|
40
|
+
[[provider]]
|
|
41
|
+
name = "deepseek-exp"
|
|
42
|
+
type = "deepseek"
|
|
43
|
+
base_url = "https://api.deepseek.com"
|
|
44
|
+
models = ["deepseek-v4.1-flash-expires-on-0910"]
|
|
45
|
+
default_model = "deepseek-v4.1-flash-expires-on-0910"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
provider 的模型解析优先级:显式 `/model <名>` > `default_model` > `models` 第一个 > 内置 flash/pro。未配置 api_key 时自动回退全局 Key(`auth.toml`)。重启后在 F6 侧边栏点选即可切换。
|
|
49
|
+
|
|
37
50
|
## 千问(qianwen 专用底座)
|
|
38
51
|
|
|
39
52
|
千问AI平台(platform.qianwenai.com / 阿里云百炼)有专用 provider,深度匹配千问特性:
|
|
@@ -96,6 +109,10 @@ models = ["claude-sonnet-4-20250514"]
|
|
|
96
109
|
- `api_key` 和 `api_key_env` 都未配置 → 回退到全局保存的 Key(`~/.nexforge/auth.toml`)
|
|
97
110
|
- `models`:可用模型 ID 列表
|
|
98
111
|
- `default_model`:可选,指定默认模型,不填取 models 第一个
|
|
112
|
+
- `multimodal`:可选(默认 `false`)。该 provider 挂载的模型**原生支持图片输入**时设为 `true`:
|
|
113
|
+
用户贴图会直接发给模型(更快、更准,且可保留思考模式),而非降级走识图工具。
|
|
114
|
+
适用于名字不含 "vision" 但实际能读图的模型(如 `deepseek-v4.1` 系列)。
|
|
115
|
+
内置 vision 档位无需设置(按模型名自动识别);声明只对该 provider 列出的模型生效。
|
|
99
116
|
|
|
100
117
|
软件只读不写此文件,不用担心 Key 被覆盖。
|
|
101
118
|
|
|
@@ -54,6 +54,12 @@ HTTP GET 抓取指定 URL,返回正文(截断至 20000 字符)。带浏览
|
|
|
54
54
|
- `mode=auto`(默认):优先用当前对话 provider 的视觉模型(如 deepseek-vision,共用对话 key),失败回退阿里云千问 qwen-vl
|
|
55
55
|
- `mode=cross`:**双模型交叉验证**——deepseek-vision 与 qwen-vl 并行识别同一张图,返回双答案供交叉印证,减少单一模型的视觉误差(适合 GUI 验收/关键判断)
|
|
56
56
|
|
|
57
|
+
**何时直接贴图、何时用本工具**(两条通道互补,不是二选一):
|
|
58
|
+
- **用户贴图**(剪贴板截图 / 拖入)→ 当前模型原生多模态时(vision 档位 / 千问 / `models.toml` 里声明 `multimodal = true` 的模型),图片直接发给模型理解,更快更准且可保留思考模式,此时不需要本工具;
|
|
59
|
+
- **AI 自己产出的图片**(`window_screenshot` / `web_screenshot` 等截图工具落盘后)→ **必须用本工具**:
|
|
60
|
+
AI 不能把本地文件"贴"进对话,本工具是它读取本地图片的唯一通道,GUI 验收闭环依赖这一点;
|
|
61
|
+
- 当前模型**不支持**图片输入时(如纯文本 flash/pro),用户贴图也会自动降级走本工具。
|
|
62
|
+
|
|
57
63
|
### image_gen(图片生成)
|
|
58
64
|
|
|
59
65
|
提供文字描述,调用阿里云通义万相生成图片并保存到本地。
|
|
@@ -125,3 +125,7 @@ engine = "bing" # bing | duckduckgo | google | searxng
|
|
|
125
125
|
AI 收到系统提示后会优先查阅地图来快速定位代码,而不是反复 list_dir/search_content 全量扫描。
|
|
126
126
|
|
|
127
127
|
每轮对话结束自动同步地图。
|
|
128
|
+
|
|
129
|
+
**大项目友好**:地图扫描自动跳过 `node_modules`/`.git`/`.venv`/`dist`/`build` 等噪音目录(整棵子树不下钻),
|
|
130
|
+
且生成与同步都在**后台线程**执行——面对超大工程(如前端 monorepo 的数十万文件)也不会卡住启动界面。
|
|
131
|
+
待分析代码超过 2000 个文件时按路径排序截断,保证界面快速可用。
|
|
@@ -51,6 +51,10 @@ class ProviderDef(BaseModel):
|
|
|
51
51
|
api_key: str | None = None
|
|
52
52
|
models: list[str] = [] # 可用模型 ID 列表
|
|
53
53
|
default_model: str = "" # 默认模型,为空则取 models[0]
|
|
54
|
+
# 该 provider 挂的模型是否支持图片输入(原生多模态)。
|
|
55
|
+
# 用于名字不含 "vision" 但实际支持读图的模型(如 deepseek-v4.1 系列),
|
|
56
|
+
# 显式声明后: 用户贴图直达模型(展开为 image_url), 无需走识图工具。
|
|
57
|
+
multimodal: bool = False
|
|
54
58
|
pricing: dict[str, PricingTier] = {} # model_id → PricingTier
|
|
55
59
|
timeout: float = 120.0
|
|
56
60
|
max_retries: int = 2
|
|
@@ -9,6 +9,13 @@ from datetime import datetime, timezone
|
|
|
9
9
|
|
|
10
10
|
DEFAULT_MAX_ROUNDS = 100
|
|
11
11
|
|
|
12
|
+
# 目标/计划注入总监与开发者上下文的截断上限(字符)。
|
|
13
|
+
# 目标必须完整传递: 长目标下截断会丢失后半部分(任务边界/次要目标/灵活处理等),
|
|
14
|
+
# 导致总监/开发者做出错误判断(历史 bug: [:200]/[:300] 截断致目标信息丢失)。
|
|
15
|
+
# 计划是中间产物(总监会重新生成), 可适当截断以控制 token。
|
|
16
|
+
GOAL_CTX_LIMIT = 4000
|
|
17
|
+
PLAN_CTX_LIMIT = 2000
|
|
18
|
+
|
|
12
19
|
DIRECTOR_PROMPT = """[总监模式]
|
|
13
20
|
你是项目总监,负责对整个开发周期做全局把控、目标分解和质量评审。
|
|
14
21
|
|
|
@@ -105,8 +105,12 @@ class KnowledgeBase:
|
|
|
105
105
|
try:
|
|
106
106
|
st = f.stat()
|
|
107
107
|
rel = f.relative_to(d).as_posix()
|
|
108
|
+
# 只读文件头(取标题用), 避免大 md 全量读入:
|
|
109
|
+
# read_text()[:N] 是"先全读再切片", 此处按字符读前 N 个
|
|
110
|
+
with f.open("r", encoding="utf-8", errors="replace") as fh:
|
|
111
|
+
head = fh.read(4000)
|
|
108
112
|
result[rel] = {
|
|
109
|
-
"title": self._title_of(
|
|
113
|
+
"title": self._title_of(head),
|
|
110
114
|
"size": st.st_size,
|
|
111
115
|
"mtime": st.st_mtime,
|
|
112
116
|
}
|
|
@@ -24,6 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
import ast
|
|
25
25
|
import hashlib
|
|
26
26
|
import json
|
|
27
|
+
import os
|
|
27
28
|
from collections import defaultdict
|
|
28
29
|
from datetime import datetime
|
|
29
30
|
from pathlib import Path
|
|
@@ -49,6 +50,33 @@ _EXCLUDED_DIRS: set[str] = {
|
|
|
49
50
|
|
|
50
51
|
_EXCLUDED_PATTERNS: set[str] = {"test_", "_test", "conftest"}
|
|
51
52
|
|
|
53
|
+
# 单次分析的文件数上限: 超大项目(数千模块)AST 解析会拖慢启动,
|
|
54
|
+
# 按路径排序截断并在地图中注明, 保证 UI 快速可用。
|
|
55
|
+
_MAX_FILES = 2000
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _iter_files_pruned(base: Path, suffix: str = "") -> list[Path]:
|
|
59
|
+
"""剪枝遍历: 排除目录直接不下钻(替代 Path.rglob)。
|
|
60
|
+
|
|
61
|
+
rglob 的目录过滤发生在遍历**之后**——它必须先走完每个子目录
|
|
62
|
+
(包括 node_modules/.git/.venv 这类噪音目录)才能产出结果。
|
|
63
|
+
超大前端项目 node_modules 可达数十万文件, Windows 上单次遍历
|
|
64
|
+
耗时数分钟(实测某项目 rglob 超 120s, 剪枝 0.17s), 而该遍历原本
|
|
65
|
+
同步发生在启动的 on_mount 里, 直接冻结 UI → 表现为"卡在启动黑屏"。
|
|
66
|
+
|
|
67
|
+
改用 os.walk(topdown=True) 原地改写 dirnames 剪枝, 排除目录的
|
|
68
|
+
子树完全不进入, 成本只与真实需分析的文件数相关。
|
|
69
|
+
"""
|
|
70
|
+
out: list[Path] = []
|
|
71
|
+
for dirpath, dirnames, filenames in os.walk(base, topdown=True):
|
|
72
|
+
# 原地剪枝: 排除目录不再下降(必须在遍历 filenames 前改写)
|
|
73
|
+
dirnames[:] = [d for d in dirnames if d not in _EXCLUDED_DIRS]
|
|
74
|
+
for fn in filenames:
|
|
75
|
+
if suffix and not fn.endswith(suffix):
|
|
76
|
+
continue
|
|
77
|
+
out.append(Path(dirpath) / fn)
|
|
78
|
+
return out
|
|
79
|
+
|
|
52
80
|
|
|
53
81
|
def _file_hash(path: Path) -> str | None:
|
|
54
82
|
"""快速文件指纹: mtime+size(仅 stat, 不读字节)。
|
|
@@ -215,15 +243,14 @@ class ProjectMapper:
|
|
|
215
243
|
if not src.is_dir():
|
|
216
244
|
src = self.root
|
|
217
245
|
files: list[Path] = []
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
continue
|
|
246
|
+
# 剪枝遍历(排除目录不下钻), 替代原 rglob——见 _iter_files_pruned 说明
|
|
247
|
+
for f in sorted(_iter_files_pruned(src, ".py")):
|
|
221
248
|
if any(p in f.name for p in _EXCLUDED_PATTERNS):
|
|
222
249
|
continue
|
|
223
250
|
if f.name.startswith("_") and f.name not in ("__init__.py", "__main__.py"):
|
|
224
251
|
continue
|
|
225
252
|
files.append(f)
|
|
226
|
-
return files
|
|
253
|
+
return files[:_MAX_FILES]
|
|
227
254
|
|
|
228
255
|
def _analyze_all(self) -> list[_Analyzer]:
|
|
229
256
|
files = self._walk()
|
|
@@ -269,7 +296,13 @@ class ProjectMapper:
|
|
|
269
296
|
if fp in old_f and old_f[fp] != h]
|
|
270
297
|
added = [fp for fp in now_f if fp not in old_f]
|
|
271
298
|
removed = [fp for fp in old_f if fp not in now_f]
|
|
272
|
-
|
|
299
|
+
in_sync = not changed and not added and not removed
|
|
300
|
+
# 边界: 地图输出被删除(.nexforge/map/ 或 map.md 没了)而哈希缓存还在时,
|
|
301
|
+
# 只比哈希会误判"已同步"→ 永不重建, AI 从此看不到地图。
|
|
302
|
+
# 故代码未变但输出缺失时, 也视为需要重建。
|
|
303
|
+
if in_sync and not self.index_content():
|
|
304
|
+
in_sync = False
|
|
305
|
+
return {"in_sync": in_sync,
|
|
273
306
|
"changed": changed, "added": added, "removed": removed,
|
|
274
307
|
"n_files": now["n_files"], "generated_at": old.get("generated_at", "(无)")}
|
|
275
308
|
|
|
@@ -474,9 +507,7 @@ class ProjectMapper:
|
|
|
474
507
|
continue
|
|
475
508
|
lines.append(f"## {d}/")
|
|
476
509
|
found = False
|
|
477
|
-
for md in sorted(base.
|
|
478
|
-
if any(part in _EXCLUDED_DIRS for part in md.parts):
|
|
479
|
-
continue
|
|
510
|
+
for md in sorted(_iter_files_pruned(base, ".md")):
|
|
480
511
|
rel = md.relative_to(self.root).as_posix()
|
|
481
512
|
size_kb = max(1, md.stat().st_size // 1024)
|
|
482
513
|
heads = self._extract_headings(md, max_heads=6)
|
|
@@ -36,6 +36,23 @@ class BaseProvider(ABC):
|
|
|
36
36
|
"""
|
|
37
37
|
return self.supports_multimodal
|
|
38
38
|
|
|
39
|
+
def _pdef_declares_multimodal(self, model: str) -> bool:
|
|
40
|
+
"""models.toml 显式声明的多模态是否适用于该模型。
|
|
41
|
+
|
|
42
|
+
语义: ``pdef.multimodal = true`` 声明的是"**该 provider 挂载的**
|
|
43
|
+
模型支持图片输入"。若传入的模型名不在该 provider 的 models/
|
|
44
|
+
default_model 列表里(例如同一个 provider 实例被要求跑别的模型),
|
|
45
|
+
声明不适用, 交由调用方的名字启发式判断——否则会把纯文本模型
|
|
46
|
+
也误判为多模态, 导致贴图发给看不懂图的模型。
|
|
47
|
+
"""
|
|
48
|
+
p = getattr(self, "pdef", None)
|
|
49
|
+
if p is None or not getattr(p, "multimodal", False):
|
|
50
|
+
return False
|
|
51
|
+
declared = [x.lower() for x in (getattr(p, "models", None) or [])]
|
|
52
|
+
if getattr(p, "default_model", ""):
|
|
53
|
+
declared.append(p.default_model.lower())
|
|
54
|
+
return (not declared) or (model.lower() in declared)
|
|
55
|
+
|
|
39
56
|
async def list_models(self) -> list[str]:
|
|
40
57
|
"""拉取当前 API key 下可用的模型 ID 列表。
|
|
41
58
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""项目地图特性——每轮结束自动同步 + 注入地图提示。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from nexforge.core.project_mapper import ProjectMapper
|
|
8
|
+
from nexforge.core.system_inject import MAP_MARKER, inject_system
|
|
9
|
+
from nexforge.i18n import t
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from nexforge.ui.screens.chat import ChatScreen
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_MAP_MARKER = MAP_MARKER # 区分旧 [项目地图] 单文件格式,避免旧会话去重误判
|
|
16
|
+
_MAP_PROMPT = (
|
|
17
|
+
f"{_MAP_MARKER} 项目结构分析存于 .nexforge/map/ 目录(分层索引体系)。\n"
|
|
18
|
+
"总索引见下方(已注入本会话): 各层模块清单 + 详细文件导航。\n"
|
|
19
|
+
"定位函数/追踪调用/查依赖/查文档时:\n"
|
|
20
|
+
"1. 先看总索引判断属于哪一层/哪个文件\n"
|
|
21
|
+
"2. 再 read_file 读对应详细文件(.nexforge/map/symbols-<层>.md 等)\n"
|
|
22
|
+
"3. 避免大范围 list_dir/search_content 扫描全项目。"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MapFeature:
|
|
27
|
+
name = "map"
|
|
28
|
+
|
|
29
|
+
def register(self, screen: ChatScreen) -> None:
|
|
30
|
+
self.screen = screen
|
|
31
|
+
from nexforge.core.context import get_project_root
|
|
32
|
+
self._mapper = ProjectMapper(get_project_root())
|
|
33
|
+
self._syncing = False # 防重入: 后台同步进行中不重复触发
|
|
34
|
+
|
|
35
|
+
def on_mount(self) -> None:
|
|
36
|
+
"""首次启动自动生成地图,并注入总索引到会话(先传索引, AI 按需读详细文件)。
|
|
37
|
+
|
|
38
|
+
关键: sync() 必须走后台线程。它要遍历项目树 + AST 解析全部模块,
|
|
39
|
+
超大项目(如前端工程 node_modules 数十万文件)同步执行会冻结事件循环,
|
|
40
|
+
表现为"卡在启动黑屏"(实测某项目单次遍历 >120s)。
|
|
41
|
+
故先用已有地图注入(保证 AI 立即可用), 同步完成后再刷新注入。
|
|
42
|
+
"""
|
|
43
|
+
s = self.screen
|
|
44
|
+
self._inject_index()
|
|
45
|
+
s._spawn(self._sync_and_inject())
|
|
46
|
+
|
|
47
|
+
def _inject_index(self) -> None:
|
|
48
|
+
"""注入当前地图总索引(inject_system 按 marker 去重, 重复调用即替换)。"""
|
|
49
|
+
s = self.screen
|
|
50
|
+
idx = self._mapper.index_content()
|
|
51
|
+
content = _MAP_PROMPT if not idx else _MAP_MARKER + "\n" + idx
|
|
52
|
+
inject_system(s.session.messages, content, _MAP_MARKER)
|
|
53
|
+
|
|
54
|
+
async def _sync_and_inject(self) -> None:
|
|
55
|
+
"""后台线程同步地图, 完成后回主线程刷新注入与提示。"""
|
|
56
|
+
import asyncio
|
|
57
|
+
if self._syncing:
|
|
58
|
+
return # 已有同步在跑(避免并发写地图文件)
|
|
59
|
+
self._syncing = True
|
|
60
|
+
try:
|
|
61
|
+
await asyncio.to_thread(self._mapper.sync)
|
|
62
|
+
except Exception:
|
|
63
|
+
return # 地图同步失败不影响主流程
|
|
64
|
+
finally:
|
|
65
|
+
self._syncing = False
|
|
66
|
+
self._inject_index()
|
|
67
|
+
self.screen._note(t("note.map_generated"), scroll=True)
|
|
68
|
+
|
|
69
|
+
def on_turn_done(self, content: str) -> None:
|
|
70
|
+
"""每轮结束快速哈希比对,有变化则自动同步(后台线程, 不阻塞回合)。"""
|
|
71
|
+
if self._syncing:
|
|
72
|
+
return # 上一轮同步未完成, 跳过本轮(变更下轮再比对)
|
|
73
|
+
self._syncing = True
|
|
74
|
+
self.screen._spawn(self._check_and_sync())
|
|
75
|
+
|
|
76
|
+
async def _check_and_sync(self) -> None:
|
|
77
|
+
import asyncio
|
|
78
|
+
try:
|
|
79
|
+
report = await asyncio.to_thread(self._mapper.check)
|
|
80
|
+
if report["in_sync"]:
|
|
81
|
+
return
|
|
82
|
+
n = len(report["changed"]) + len(report["added"]) + len(report["removed"])
|
|
83
|
+
await asyncio.to_thread(self._mapper.sync)
|
|
84
|
+
self.screen._note(t("note.map_autosynced", n=str(n)), scroll=True)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass # 自动同步失败不影响主流程
|
|
87
|
+
finally:
|
|
88
|
+
self._syncing = False
|
|
89
|
+
|
|
90
|
+
def bindings(self) -> list[tuple[str, str, str]]:
|
|
91
|
+
return []
|
|
@@ -9,6 +9,7 @@ from nexforge.features.base import Feature
|
|
|
9
9
|
from nexforge.config import save_config
|
|
10
10
|
from nexforge.i18n import t
|
|
11
11
|
from nexforge.core.context import get_project_root
|
|
12
|
+
from nexforge.core.goal_manager import GOAL_CTX_LIMIT, PLAN_CTX_LIMIT
|
|
12
13
|
from nexforge.core.memory_engine import MemoryEngine, _MARKER_OPS, _MARKER_GOAL, clear_messages_by_marker, has_marker
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
@@ -106,6 +107,7 @@ class GoalModeFeature:
|
|
|
106
107
|
if gm.rounds_dir.exists():
|
|
107
108
|
shutil.rmtree(gm.rounds_dir, ignore_errors=True)
|
|
108
109
|
gm.rounds_dir.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
self._stop_director = False # 复位停止标志, 防残留导致恢复的循环立即退出
|
|
109
111
|
self._turn_count = 1
|
|
110
112
|
# 清理残留状态(防上次崩溃遗留 dev_done_flag 导致重复验收)
|
|
111
113
|
self._dev_done_flag = False
|
|
@@ -246,14 +248,19 @@ class GoalModeFeature:
|
|
|
246
248
|
from nexforge.core.goal_manager import GoalManager
|
|
247
249
|
gm = GoalManager(get_project_root())
|
|
248
250
|
goal = gm.read_goal()
|
|
249
|
-
gm.set_goal(goal + "\n\n[用户补充意见]\n" + feedback[:
|
|
251
|
+
gm.set_goal(goal + "\n\n[用户补充意见]\n" + feedback[:2000])
|
|
250
252
|
self._awaiting_plan_confirm = False
|
|
251
253
|
self._turn_count = 0 # 让重新规划回复再次作为 turn=1(方案轮)
|
|
252
254
|
s._note(t("note.goal_plan_amended"), scroll=True)
|
|
255
|
+
# 修复: 用户反馈必须完整传给总监, 不能依赖 read_goal()[:300] 截断。
|
|
256
|
+
# 旧实现把反馈追加到 goal 文件末尾后, 仅取前 300 字符作为 prompt,
|
|
257
|
+
# 而反馈永远位于 300 字符之后 → 总监收不到修改意见
|
|
258
|
+
# (表现为"传输截断/未检测到新的修改意见正文")。
|
|
253
259
|
s._run_agent(
|
|
254
260
|
"[项目总监] 用户对初步方案提出了修改意见, 请结合意见重新规划方案。"
|
|
255
261
|
"注意: 先输出规划方案, 不要发布执行指令, 等待用户确认后再动工。\n"
|
|
256
|
-
"
|
|
262
|
+
"【原始目标】\n" + goal +
|
|
263
|
+
"\n\n【用户本次修改意见】\n" + feedback[:2000]
|
|
257
264
|
)
|
|
258
265
|
|
|
259
266
|
def _fed_members_prompt(self) -> str:
|
|
@@ -348,8 +355,8 @@ class GoalModeFeature:
|
|
|
348
355
|
from nexforge.core.goal_manager import GoalManager
|
|
349
356
|
s = self.screen
|
|
350
357
|
gm = GoalManager(get_project_root())
|
|
351
|
-
goal = gm.read_goal()[:
|
|
352
|
-
plan = gm.read_plan()[:
|
|
358
|
+
goal = gm.read_goal()[:GOAL_CTX_LIMIT]
|
|
359
|
+
plan = gm.read_plan()[:PLAN_CTX_LIMIT]
|
|
353
360
|
fed_members = self._fed_members_prompt() # 联结体成员(跨项目编排)
|
|
354
361
|
ctx = (
|
|
355
362
|
"[项目总监] 首次目标分解。\n"
|
|
@@ -387,13 +394,13 @@ class GoalModeFeature:
|
|
|
387
394
|
max_r = s.config.ui.max_rounds
|
|
388
395
|
cycle_n = self._turn_count - 1
|
|
389
396
|
report = (report or self._dev_report)[:2000]
|
|
390
|
-
goal = gm.read_goal()[:
|
|
391
|
-
plan = gm.read_plan()[:
|
|
397
|
+
goal = gm.read_goal()[:GOAL_CTX_LIMIT]
|
|
398
|
+
plan = gm.read_plan()[:PLAN_CTX_LIMIT]
|
|
392
399
|
fed_members = self._fed_members_prompt()
|
|
393
400
|
ctx = (
|
|
394
401
|
"[项目总监复盘] 第" + str(cycle_n) + "/" + str(max_r) + "轮。\n"
|
|
395
402
|
"目标:" + goal + "\n"
|
|
396
|
-
"计划:" + plan
|
|
403
|
+
"计划:" + plan + "\n"
|
|
397
404
|
"开发者报告:" + report + "\n\n"
|
|
398
405
|
"请按审核框架复盘:\n"
|
|
399
406
|
"1. 本轮完成了多少?(核心/重要/次要)\n"
|
|
@@ -435,7 +442,7 @@ class GoalModeFeature:
|
|
|
435
442
|
from nexforge.core.goal_manager import GoalManager
|
|
436
443
|
s = self.screen
|
|
437
444
|
gm = GoalManager(get_project_root())
|
|
438
|
-
goal = gm.read_goal()[:
|
|
445
|
+
goal = gm.read_goal()[:GOAL_CTX_LIMIT]
|
|
439
446
|
dev_status = "开发者正在执行中" if self._dev_busy else "开发者空闲, 等待下一任务"
|
|
440
447
|
ctx = (
|
|
441
448
|
"[项目总监] 用户在工作进行中与你沟通(下栏沟通区)。\n"
|
|
@@ -461,15 +468,15 @@ class GoalModeFeature:
|
|
|
461
468
|
s = self.screen
|
|
462
469
|
gm = GoalManager(get_project_root())
|
|
463
470
|
max_r = s.config.ui.max_rounds
|
|
464
|
-
goal = gm.read_goal()[:
|
|
465
|
-
plan = gm.read_plan()[:
|
|
471
|
+
goal = gm.read_goal()[:GOAL_CTX_LIMIT]
|
|
472
|
+
plan = gm.read_plan()[:PLAN_CTX_LIMIT]
|
|
466
473
|
s.call_later(s._mount, DirectorNote(
|
|
467
474
|
t("note.dev_round", n=str(round_n), mx=str(max_r))
|
|
468
475
|
), dev=True)
|
|
469
476
|
msg = (
|
|
470
477
|
"[开发者] 第" + str(round_n) + "/" + str(max_r) + "轮。\n"
|
|
471
478
|
"目标:" + goal + "\n"
|
|
472
|
-
"计划:" + plan
|
|
479
|
+
"计划:" + plan + "\n"
|
|
473
480
|
"总监指令:" + instruction[:2000] + "\n\n"
|
|
474
481
|
"请按指令执行。完成后提交执行报告。"
|
|
475
482
|
"若遇到不可逾越的障碍,在报告开头标注 [BLOCKED]。"
|
|
@@ -502,6 +509,35 @@ class GoalModeFeature:
|
|
|
502
509
|
self._resume_director_loop()
|
|
503
510
|
self._wake_event.set()
|
|
504
511
|
|
|
512
|
+
def reset_for_session_change(self) -> None:
|
|
513
|
+
"""会话切换/新建时完整重置目标模式运行状态(不关闭 goal_mode 开关)。
|
|
514
|
+
|
|
515
|
+
目标文件(goal/plan/rounds)是项目级跨会话共享,但总监循环是内存态。
|
|
516
|
+
切换会话时旧循环被 cancel, 若运行状态(_turn_count/_dev_done_flag/
|
|
517
|
+
_stop_director 等)残留, 新会话输入会走错分支:
|
|
518
|
+
- _turn_count 残留 → 方案生成后 on_turn_done 跳过 turn=1 分支,
|
|
519
|
+
方案不落盘、不进入待确认, 总监循环无法启动
|
|
520
|
+
- 旧目标文件残留 → 新会话输入被误判为"旧目标插话"入队, 循环又没恢复
|
|
521
|
+
结果都表现为"AI 回复一段就停, 必须反复输入才能继续"。
|
|
522
|
+
这里清零运行状态 + 清理目标文件, 让切换后输入作为全新目标进入正常流程。
|
|
523
|
+
"""
|
|
524
|
+
self._stop_director = False # 关键: 复位停止标志, 否则恢复的循环立即退出
|
|
525
|
+
self._awaiting_plan_confirm = False
|
|
526
|
+
self._director_busy = False
|
|
527
|
+
self._turn_count = 0 # 关键: 复位轮次, 否则方案生成后 on_turn_done 走错分支
|
|
528
|
+
self._user_inbox.clear()
|
|
529
|
+
self._dev_busy = False
|
|
530
|
+
self._dev_done_flag = False
|
|
531
|
+
self._dev_report = ""
|
|
532
|
+
self._director_loop_task = None
|
|
533
|
+
self._wake_event.clear()
|
|
534
|
+
# 清理目标文件, 防旧目标残留被误判为插话
|
|
535
|
+
try:
|
|
536
|
+
from nexforge.core.goal_manager import GoalManager
|
|
537
|
+
GoalManager(get_project_root()).cleanup()
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
|
|
505
541
|
def _stop_director_loop(self) -> None:
|
|
506
542
|
"""停止总监循环。"""
|
|
507
543
|
self._stop_director = True
|
|
@@ -109,7 +109,17 @@ class SessionFeature:
|
|
|
109
109
|
s._director_session_id = ""
|
|
110
110
|
s.session = Session()
|
|
111
111
|
s.session.messages = list(msgs)
|
|
112
|
-
|
|
112
|
+
# 系统提示判断: 摘要 system 消息(历史压缩)不算完整系统提示,
|
|
113
|
+
# 必须注入 DEFAULT_SYSTEM(否则 AI 缺失行为规范, 只回复一段就停)。
|
|
114
|
+
# 与 _auto_resume 保持一致 —— 原判断只看首条 role, 而压缩过的会话
|
|
115
|
+
# 首条是 "[历史摘要" system 消息(save_session 只回显摘要 system),
|
|
116
|
+
# 会误判为已有 system 而漏注入 DEFAULT_SYSTEM。
|
|
117
|
+
has_full_sys = any(
|
|
118
|
+
m.get("role") == "system"
|
|
119
|
+
and str(m.get("content", "")).startswith("【推理要求】")
|
|
120
|
+
for m in s.session.messages
|
|
121
|
+
)
|
|
122
|
+
if not has_full_sys:
|
|
113
123
|
s.session.messages.insert(0, {"role": "system", "content": DEFAULT_SYSTEM})
|
|
114
124
|
s._session_id = info.id
|
|
115
125
|
s._session_name = info.name
|
|
@@ -145,11 +155,20 @@ class SessionFeature:
|
|
|
145
155
|
# ---- 自动保存 ----
|
|
146
156
|
|
|
147
157
|
def _reset_goal_confirm(self) -> None:
|
|
148
|
-
"""
|
|
158
|
+
"""切换/新建会话时完整重置目标模式运行状态(防残留误路由)。
|
|
159
|
+
|
|
160
|
+
修复: 之前只重置 _awaiting_plan_confirm, 而 _turn_count/_dev_done_flag/
|
|
161
|
+
_stop_director 等运行状态与旧目标文件(项目级)残留, 导致切换会话后
|
|
162
|
+
总监循环无法正确恢复, 表现为"AI 回复一段就停, 必须反复输入才能继续"。
|
|
163
|
+
"""
|
|
149
164
|
try:
|
|
150
165
|
gf = next((f for f in self.screen._features._features
|
|
151
166
|
if getattr(f, 'name', '') == 'goal_mode'), None)
|
|
152
|
-
if gf is
|
|
167
|
+
if gf is None:
|
|
168
|
+
return
|
|
169
|
+
if hasattr(gf, 'reset_for_session_change'):
|
|
170
|
+
gf.reset_for_session_change()
|
|
171
|
+
else:
|
|
153
172
|
gf._awaiting_plan_confirm = False
|
|
154
173
|
except Exception:
|
|
155
174
|
pass
|
|
@@ -50,6 +50,7 @@ class DeepSeekProvider(BaseProvider):
|
|
|
50
50
|
api_key: str | None = None,
|
|
51
51
|
):
|
|
52
52
|
self.config = config
|
|
53
|
+
self.pdef = pdef
|
|
53
54
|
key = api_key or config.resolve_api_key()
|
|
54
55
|
if not key:
|
|
55
56
|
raise ProviderError(
|
|
@@ -68,6 +69,7 @@ class DeepSeekProvider(BaseProvider):
|
|
|
68
69
|
"""用明确 key 创建 Provider(跳过 resolve_api_key 链)。"""
|
|
69
70
|
inst = cls.__new__(cls)
|
|
70
71
|
inst.config = config
|
|
72
|
+
inst.pdef = None
|
|
71
73
|
inst._client = AsyncOpenAI(
|
|
72
74
|
api_key=api_key,
|
|
73
75
|
base_url=config.provider.base_url,
|
|
@@ -81,17 +83,45 @@ class DeepSeekProvider(BaseProvider):
|
|
|
81
83
|
|
|
82
84
|
# ── 多模态(视觉模型) ────────────────────────────────────
|
|
83
85
|
|
|
86
|
+
def _resolve_model(self, tier: Tier | None, model: str | None = None) -> str:
|
|
87
|
+
"""解析实际使用的模型名。
|
|
88
|
+
|
|
89
|
+
优先级: 显式传入 model > pdef.default_model > pdef.models[0] > 内置 tier 映射。
|
|
90
|
+
支持 models.toml 里以 type="deepseek" 注册独立模型(如实验版 flash 变体),
|
|
91
|
+
不再被内置 config.models.flash/pro 写死。
|
|
92
|
+
"""
|
|
93
|
+
if model:
|
|
94
|
+
return model
|
|
95
|
+
if self.pdef and self.pdef.default_model:
|
|
96
|
+
return self.pdef.default_model
|
|
97
|
+
if self.pdef and self.pdef.models:
|
|
98
|
+
return self.pdef.models[0]
|
|
99
|
+
return self.config.model_id(tier)
|
|
100
|
+
|
|
84
101
|
@property
|
|
85
102
|
def current_model(self) -> str:
|
|
86
103
|
"""当前使用的模型名(最近一次调用或默认档位)。"""
|
|
87
104
|
if getattr(self, "_last_model", ""):
|
|
88
105
|
return self._last_model
|
|
89
|
-
return self.
|
|
106
|
+
return self._resolve_model(self.config.models.default_tier)
|
|
90
107
|
|
|
91
108
|
def supports_model_multimodal(self, model: str = "") -> bool:
|
|
92
|
-
"""
|
|
109
|
+
"""该模型是否支持图片输入(原生多模态)。
|
|
110
|
+
|
|
111
|
+
判定顺序:
|
|
112
|
+
1. 模型名含 "vision" → 支持(内置 vision 档位, 如 deepseek-v4-flash-vision-exp);
|
|
113
|
+
2. models.toml 里该 provider 显式声明 ``multimodal = true`` → 支持。
|
|
114
|
+
用于名字不含 vision 但实际能读图的模型(如 deepseek-v4.1 系列,
|
|
115
|
+
实测: 对已知内容的图能准确回答圆/三角颜色与文字, 且不编造图中没有的元素);
|
|
116
|
+
3. 否则不支持——纯文本模型(如 deepseek-v4-flash)收到图片会返回空。
|
|
117
|
+
|
|
118
|
+
判为支持后, 用户贴图走 ``[图片附件: 路径]`` 通道直发达模型;
|
|
119
|
+
判为不支持则降级走识图工具(image_vision)。
|
|
120
|
+
"""
|
|
93
121
|
m = (model or self.current_model).lower()
|
|
94
|
-
|
|
122
|
+
if "vision" in m:
|
|
123
|
+
return True
|
|
124
|
+
return self._pdef_declares_multimodal(m)
|
|
95
125
|
|
|
96
126
|
def _expand_image_attachments(
|
|
97
127
|
self, messages: list[dict], model: str | None
|
|
@@ -157,6 +187,7 @@ class DeepSeekProvider(BaseProvider):
|
|
|
157
187
|
tier: Tier | None,
|
|
158
188
|
thinking: bool | None,
|
|
159
189
|
effort: Effort | None,
|
|
190
|
+
model: str | None = None,
|
|
160
191
|
) -> dict:
|
|
161
192
|
cfg = self.config
|
|
162
193
|
think = cfg.models.thinking if thinking is None else thinking
|
|
@@ -169,7 +200,7 @@ class DeepSeekProvider(BaseProvider):
|
|
|
169
200
|
extra_body["reasoning_effort"] = eff
|
|
170
201
|
|
|
171
202
|
kwargs: dict[str, Any] = {
|
|
172
|
-
"model":
|
|
203
|
+
"model": self._resolve_model(tier, model),
|
|
173
204
|
"messages": messages,
|
|
174
205
|
"stream": True,
|
|
175
206
|
"stream_options": {"include_usage": True},
|
|
@@ -215,14 +246,14 @@ class DeepSeekProvider(BaseProvider):
|
|
|
215
246
|
thinking: bool | None = kwargs.get("thinking")
|
|
216
247
|
effort: Effort | None = kwargs.get("effort")
|
|
217
248
|
resolved_tier: Tier = tier or self.config.models.default_tier
|
|
218
|
-
model_id =
|
|
249
|
+
model_id = self._resolve_model(tier, model)
|
|
219
250
|
self._last_model = model_id # 供 current_model / 多模态判断
|
|
220
251
|
# 视觉模型: 强制 thinking off(exp 模型未确认支持思考模式, 避免 400)
|
|
221
252
|
if "vision" in model_id.lower() and thinking is not False:
|
|
222
253
|
thinking = False
|
|
223
254
|
# 多模态: 把 [图片附件: 路径] 展开为 base64 image_url
|
|
224
255
|
messages = self._expand_image_attachments(messages, model_id)
|
|
225
|
-
bkwargs = self._build_kwargs(messages, tools, tier, thinking, effort)
|
|
256
|
+
bkwargs = self._build_kwargs(messages, tools, tier, thinking, effort, model_id)
|
|
226
257
|
|
|
227
258
|
content = ""
|
|
228
259
|
reasoning = ""
|