nexforge-cli 0.2.15__tar.gz → 0.2.16__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.16}/PKG-INFO +2 -2
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/README.md +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/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.16}/manual/03-/346/250/241/345/236/213/351/205/215/347/275/256.md +14 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/manual/README.md +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/pyproject.toml +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/__init__.py +1 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/goal_manager.py +7 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/mode_features.py +47 -11
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/session_feature.py +22 -3
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/provider/deepseek.py +22 -4
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/screens/chat.py +11 -5
- nexforge_cli-0.2.16/tests/test_deepseek_provider.py +54 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_director_loop.py +22 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_goal_confirm.py +74 -1
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_session_store.py +82 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/.gitignore +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/INSTALL.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/STATS_API.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/agent.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/cost.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/edit.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/err.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/file.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/model.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/ok.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/read.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/running.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/search.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/shell.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/thinking.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/web.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/assets/icons/write.svg +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/config.example.toml +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/devlog/2026-07-07.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/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.16}/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.16}/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.16}/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.16}/docs/submission/nexforge.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/docs/submission/nexforge.zh-CN.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/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.16}/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.16}/manual/05-/346/225/260/346/215/256/344/270/216/346/220/234/347/264/242.md" +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/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.16}/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.16}/manual/08-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md" +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/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.16}/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.16}/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.16}/plugin_gui_auto/README.md +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/guiauto/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/guiauto/action.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/guiauto/base.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/guiauto/tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/guiauto/vision.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/guiauto/window.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/install_plugin.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/requirements.txt +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/smoke_safe.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/verify_final.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/verify_host.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/plugin_gui_auto/verify_plugin.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/scripts/publish.sh +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/scripts/smoke_provider.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/scripts/smoke_tui.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/__main__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/app.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/cli.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/config.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/agent.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/context.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/federation.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/jobs.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/knowledge_base.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/memory_engine.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/model_catalog.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/permissions.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/plugin.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/project_mapper.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/provider.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/router.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/session.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/summarizer.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/system_inject.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/tool_manifest.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/types.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/core/update_check.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/auto_skills.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/base.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/federation_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/kb_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/map_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/memory_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/plan_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/registry.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/slash_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/stats_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/features/tool_manifest_feature.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/i18n.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/icons.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/mcp/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/provider/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/provider/_retry.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/provider/anthropic.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/provider/balance.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/provider/openai.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/provider/qianwen.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/skills/loader.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/stats/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/stats/heartbeat.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/store/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/store/director_store.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/store/plan_store.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/store/session_store.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/theme.tcss +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/_paths.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/background.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/base.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/data_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/edit_file.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/federation.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/fs_ops.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/git.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/image_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/kb_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/list_dir.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/multi_edit.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/ops_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/read_file.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/registry.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/screenshot_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/search.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/search_content.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/search_files.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/shell.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/web.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/tools/write_file.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/clipboard_image.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/screen_api.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/screens/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/screens/chat_actions.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/screens/splash.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/slash_handler.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/banner.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/diff_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/file_browser.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/file_menu.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/image_sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/messages.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/mode_warning.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/model_sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/ui/widgets/statusbar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/utils/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/utils/logging.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/web/__init__.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/web/server.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/src/nexforge/web/static/index.html +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/conftest.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_app_mouse.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_approval.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_config.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_cross_platform.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_director_store.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_dual_panel.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_federation.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_file_browser.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_i18n.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_icons.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_lang_cmd.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_m2_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_manual_consistency.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_model_catalog.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_p2_core.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_plugin.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_sanitize.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_screenshot_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_session_repair.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_sidebar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_sidebar_actions.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_splash_modal.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_statusbar.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_system_inject.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_tool_iters.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_tools.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_tui.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_update_check.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/tests/test_vision_model.py +0 -0
- {nexforge_cli-0.2.15 → nexforge_cli-0.2.16}/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.16
|
|
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.16** · 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,深度匹配千问特性:
|
|
@@ -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
|
|
|
@@ -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,12 +83,27 @@ 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
|
"""视觉模型(模型名含 vision)支持图片输入。"""
|
|
@@ -157,6 +174,7 @@ class DeepSeekProvider(BaseProvider):
|
|
|
157
174
|
tier: Tier | None,
|
|
158
175
|
thinking: bool | None,
|
|
159
176
|
effort: Effort | None,
|
|
177
|
+
model: str | None = None,
|
|
160
178
|
) -> dict:
|
|
161
179
|
cfg = self.config
|
|
162
180
|
think = cfg.models.thinking if thinking is None else thinking
|
|
@@ -169,7 +187,7 @@ class DeepSeekProvider(BaseProvider):
|
|
|
169
187
|
extra_body["reasoning_effort"] = eff
|
|
170
188
|
|
|
171
189
|
kwargs: dict[str, Any] = {
|
|
172
|
-
"model":
|
|
190
|
+
"model": self._resolve_model(tier, model),
|
|
173
191
|
"messages": messages,
|
|
174
192
|
"stream": True,
|
|
175
193
|
"stream_options": {"include_usage": True},
|
|
@@ -215,14 +233,14 @@ class DeepSeekProvider(BaseProvider):
|
|
|
215
233
|
thinking: bool | None = kwargs.get("thinking")
|
|
216
234
|
effort: Effort | None = kwargs.get("effort")
|
|
217
235
|
resolved_tier: Tier = tier or self.config.models.default_tier
|
|
218
|
-
model_id =
|
|
236
|
+
model_id = self._resolve_model(tier, model)
|
|
219
237
|
self._last_model = model_id # 供 current_model / 多模态判断
|
|
220
238
|
# 视觉模型: 强制 thinking off(exp 模型未确认支持思考模式, 避免 400)
|
|
221
239
|
if "vision" in model_id.lower() and thinking is not False:
|
|
222
240
|
thinking = False
|
|
223
241
|
# 多模态: 把 [图片附件: 路径] 展开为 base64 image_url
|
|
224
242
|
messages = self._expand_image_attachments(messages, model_id)
|
|
225
|
-
bkwargs = self._build_kwargs(messages, tools, tier, thinking, effort)
|
|
243
|
+
bkwargs = self._build_kwargs(messages, tools, tier, thinking, effort, model_id)
|
|
226
244
|
|
|
227
245
|
content = ""
|
|
228
246
|
reasoning = ""
|
|
@@ -313,7 +313,7 @@ class ChatScreen(ChatActionsMixin, Screen):
|
|
|
313
313
|
# 注入目标场景记忆
|
|
314
314
|
self._get_feature("goal_mode")._inject_goal_memory()
|
|
315
315
|
self._note(t("note.goal_started"), scroll=True)
|
|
316
|
-
from nexforge.core.goal_manager import GoalManager
|
|
316
|
+
from nexforge.core.goal_manager import GoalManager, GOAL_CTX_LIMIT
|
|
317
317
|
from pathlib import Path
|
|
318
318
|
gm = GoalManager(Path.cwd())
|
|
319
319
|
gm.set_goal(goal)
|
|
@@ -321,7 +321,7 @@ class ChatScreen(ChatActionsMixin, Screen):
|
|
|
321
321
|
self._run_agent(
|
|
322
322
|
"[项目总监] 请针对以下目标规划初步方案(技术路线/任务拆解/实施顺序)。"
|
|
323
323
|
"注意: 先输出规划方案, 不要发布执行指令, 等待用户确认后再动工。\n"
|
|
324
|
-
"目标: " + goal[:
|
|
324
|
+
"目标: " + goal[:GOAL_CTX_LIMIT]
|
|
325
325
|
)
|
|
326
326
|
return
|
|
327
327
|
if self.agent is None:
|
|
@@ -340,7 +340,7 @@ class ChatScreen(ChatActionsMixin, Screen):
|
|
|
340
340
|
# (修复: F8 开启目标模式后用户直接输入, 原先走普通对话导致上栏工作区空;
|
|
341
341
|
# 现在自动把输入当作目标, 触发总监编排, 让上栏工作区真正运转起来)
|
|
342
342
|
if self.config.ui.goal_mode:
|
|
343
|
-
from nexforge.core.goal_manager import GoalManager
|
|
343
|
+
from nexforge.core.goal_manager import GoalManager, GOAL_CTX_LIMIT
|
|
344
344
|
from nexforge.core.context import get_project_root
|
|
345
345
|
gm = GoalManager(get_project_root())
|
|
346
346
|
if not gm.read_goal().strip():
|
|
@@ -353,7 +353,7 @@ class ChatScreen(ChatActionsMixin, Screen):
|
|
|
353
353
|
self._run_agent(
|
|
354
354
|
"[项目总监] 请针对以下目标规划初步方案(技术路线/任务拆解/实施顺序)。"
|
|
355
355
|
"注意: 先输出规划方案, 不要发布执行指令, 等待用户确认后再动工。\n"
|
|
356
|
-
"目标: " + text[:
|
|
356
|
+
"目标: " + text[:GOAL_CTX_LIMIT]
|
|
357
357
|
)
|
|
358
358
|
return
|
|
359
359
|
# 已有目标 → 用户插话 → 入队(软打断: 总监响应用户, 不打断开发者)
|
|
@@ -410,8 +410,14 @@ class ChatScreen(ChatActionsMixin, Screen):
|
|
|
410
410
|
|
|
411
411
|
@staticmethod
|
|
412
412
|
def _is_goal_confirm(text: str) -> bool:
|
|
413
|
-
"""判断用户输入是否为方案确认(短文本 + 确认关键词)。
|
|
413
|
+
"""判断用户输入是否为方案确认(短文本 + 确认关键词)。
|
|
414
|
+
|
|
415
|
+
疑问句(含"吗/?"等)不可能是确认——用户讨论方案时输入"可以改吗"等
|
|
416
|
+
短疑问句应走修改意见(amend_plan), 不能被误判为确认而忽略意见。
|
|
417
|
+
"""
|
|
414
418
|
t = text.strip().lower().rstrip("。!!.,, ")
|
|
419
|
+
if any(q in t for q in ("吗", "?", "?", "是不是", "能否", "可不可以", "行不行")):
|
|
420
|
+
return False
|
|
415
421
|
exact = {"确认", "确定", "开始", "执行", "没问题", "可以", "同意", "ok", "yes", "go", "start", "继续"}
|
|
416
422
|
if t in exact:
|
|
417
423
|
return True
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DeepSeek provider 独立模型解析测试(不触网)。
|
|
3
|
+
|
|
4
|
+
验证 models.toml 里 type="deepseek" 注册独立模型时,
|
|
5
|
+
模型名解析优先级: 显式 model > pdef.default_model > pdef.models[0] > 内置 tier。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from nexforge.config import NexForgeConfig, ProviderDef
|
|
10
|
+
from nexforge.provider.deepseek import DeepSeekProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _bare(cfg, pdef=None):
|
|
14
|
+
"""绕过 __init__ 的网络客户端, 只构造被测对象。"""
|
|
15
|
+
p = DeepSeekProvider.__new__(DeepSeekProvider)
|
|
16
|
+
p.config = cfg
|
|
17
|
+
p.pdef = pdef
|
|
18
|
+
return p
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_resolve_model_builtin_fallback():
|
|
22
|
+
"""无 pdef(内置兼容模式) → 用内置 tier 映射, 行为不变。"""
|
|
23
|
+
cfg = NexForgeConfig()
|
|
24
|
+
p = _bare(cfg, None)
|
|
25
|
+
assert p._resolve_model("flash") == cfg.model_id("flash") == "deepseek-v4-flash"
|
|
26
|
+
assert p._resolve_model("pro") == cfg.model_id("pro") == "deepseek-v4-pro"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_resolve_model_uses_pdef_default_model():
|
|
30
|
+
"""有 pdef.default_model → 优先用独立模型(核心修复)。"""
|
|
31
|
+
cfg = NexForgeConfig()
|
|
32
|
+
pdef = ProviderDef(
|
|
33
|
+
type="deepseek",
|
|
34
|
+
models=["deepseek-v4.1-flash-expires-on-0910"],
|
|
35
|
+
default_model="deepseek-v4.1-flash-expires-on-0910",
|
|
36
|
+
)
|
|
37
|
+
p = _bare(cfg, pdef)
|
|
38
|
+
assert p._resolve_model("flash") == "deepseek-v4.1-flash-expires-on-0910"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_resolve_model_falls_back_to_models_first():
|
|
42
|
+
"""有 pdef 但无 default_model → 用 models[0]。"""
|
|
43
|
+
cfg = NexForgeConfig()
|
|
44
|
+
pdef = ProviderDef(type="deepseek", models=["custom-flash"])
|
|
45
|
+
p = _bare(cfg, pdef)
|
|
46
|
+
assert p._resolve_model("flash") == "custom-flash"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_resolve_model_explicit_model_wins():
|
|
50
|
+
"""显式传入 model 优先级最高。"""
|
|
51
|
+
cfg = NexForgeConfig()
|
|
52
|
+
pdef = ProviderDef(type="deepseek", models=["m1"], default_model="m1")
|
|
53
|
+
p = _bare(cfg, pdef)
|
|
54
|
+
assert p._resolve_model("flash", model="explicit-model") == "explicit-model"
|
|
@@ -78,6 +78,28 @@ def loop_env(tmp_path, monkeypatch):
|
|
|
78
78
|
return tmp_path, mf, gm
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def test_dispatch_dev_keeps_long_goal(loop_env):
|
|
82
|
+
"""长目标下, 开发者指令中的目标/计划必须完整传递, 不能被 [:300] 截断。
|
|
83
|
+
|
|
84
|
+
回归: 旧代码 goal = read_goal()[:300] / plan = read_plan()[:300],
|
|
85
|
+
长目标(>300字符)的后半部分(任务边界/次要目标等)在总监分解/开发者执行阶段丢失。
|
|
86
|
+
"""
|
|
87
|
+
tmp_path, mf, gm = loop_env
|
|
88
|
+
long_goal = "构建Unity角色生成系统: " + "详细需求描述" * 60 # > 300 字符
|
|
89
|
+
long_plan = "计划步骤: " + "分阶段实施说明" * 60 # > 300 字符
|
|
90
|
+
gm.set_goal(long_goal)
|
|
91
|
+
gm.set_plan(long_plan)
|
|
92
|
+
screen = LoopScreen([])
|
|
93
|
+
feat = mf.GoalModeFeature()
|
|
94
|
+
feat.register(screen)
|
|
95
|
+
screen._feat = feat
|
|
96
|
+
feat._dispatch_dev("开始编码", 1)
|
|
97
|
+
assert screen.agents, "应派发开发者任务"
|
|
98
|
+
text = screen.agents[0][0]
|
|
99
|
+
assert long_goal in text, "开发者指令应包含完整目标, 不能被截断"
|
|
100
|
+
assert long_plan in text, "开发者指令应包含完整计划, 不能被截断"
|
|
101
|
+
|
|
102
|
+
|
|
81
103
|
@pytest.mark.asyncio
|
|
82
104
|
async def test_director_loop_full_flow(loop_env):
|
|
83
105
|
"""完整循环: 初始分解 → 派发开发者 → 完成 → 验收 → GOAL_ACHIEVED 停止。"""
|
|
@@ -116,6 +116,22 @@ def test_amend_replans(screen):
|
|
|
116
116
|
assert "用户补充意见" in gm.read_goal(), "意见应追加到目标"
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
def test_amend_plan_keeps_feedback_not_truncated(screen):
|
|
120
|
+
"""长目标下, 用户修改意见必须完整传给总监, 不能被 read_goal()[:300] 截断。
|
|
121
|
+
|
|
122
|
+
回归: 旧实现把反馈追加到 goal 文件末尾后, 仅取前 300 字符作为 prompt,
|
|
123
|
+
而反馈位于 300 字符之后 → 总监收不到意见(表现为"截断/未检测到意见")。
|
|
124
|
+
"""
|
|
125
|
+
s, feat, gm = screen
|
|
126
|
+
long_goal = "构建一个Unity角色生成系统, 包含以下详细需求: " + "需求细节" * 80 # >300 字符
|
|
127
|
+
gm.set_goal(long_goal)
|
|
128
|
+
feat.on_turn_done("方案: 编译优化")
|
|
129
|
+
feedback = "角色默认角度改为3个: 正面/侧面/表情特写; 场景设定图按剧本级主要场景各一张"
|
|
130
|
+
feat.amend_plan(feedback)
|
|
131
|
+
assert s.agents, "应触发重新规划"
|
|
132
|
+
assert feedback in s.agents[0], "用户反馈必须完整出现在总监 prompt 中, 不能被截断"
|
|
133
|
+
|
|
134
|
+
|
|
119
135
|
def test_toggle_resets_confirm(screen):
|
|
120
136
|
s, feat, _ = screen
|
|
121
137
|
feat.on_turn_done("方案: v1")
|
|
@@ -140,7 +156,8 @@ def test_session_switch_resets_confirm(screen):
|
|
|
140
156
|
def test_confirm_word_detection():
|
|
141
157
|
true_cases = ["确认", "确定", "开始", "没问题", "方案可以", "ok", "OK", "yes",
|
|
142
158
|
"确认,开始吧", "继续", "可以"]
|
|
143
|
-
false_cases = ["不要微服务", "换个思路", "这个方案不行", "补充一点: 用缓存"
|
|
159
|
+
false_cases = ["不要微服务", "换个思路", "这个方案不行", "补充一点: 用缓存",
|
|
160
|
+
"可以改吗", "确定吗", "可以吗", "开始了吗", "是不是要加缓存?"]
|
|
144
161
|
for t in true_cases:
|
|
145
162
|
assert ChatScreen._is_goal_confirm(t), f"{t!r} 应判为确认"
|
|
146
163
|
for t in false_cases:
|
|
@@ -155,3 +172,59 @@ def test_empty_plan_no_director(screen):
|
|
|
155
172
|
feat.confirm_plan()
|
|
156
173
|
assert len(s.director_started) == n, "空方案不应启动总监"
|
|
157
174
|
assert not feat._awaiting_plan_confirm
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_reset_for_session_change_full(screen):
|
|
178
|
+
"""会话切换重置应清零所有运行状态 + 清理目标文件。"""
|
|
179
|
+
s, feat, gm = screen
|
|
180
|
+
# 模拟切换前"运行中"的脏状态
|
|
181
|
+
feat._turn_count = 5
|
|
182
|
+
feat._stop_director = True
|
|
183
|
+
feat._dev_done_flag = True
|
|
184
|
+
feat._dev_busy = True
|
|
185
|
+
feat._dev_report = "旧报告"
|
|
186
|
+
feat._user_inbox.append("插话")
|
|
187
|
+
feat._awaiting_plan_confirm = True
|
|
188
|
+
feat._director_busy = True
|
|
189
|
+
feat.reset_for_session_change()
|
|
190
|
+
assert feat._turn_count == 0
|
|
191
|
+
assert feat._stop_director is False, "停止标志必须复位,否则恢复的循环立即退出"
|
|
192
|
+
assert feat._dev_done_flag is False
|
|
193
|
+
assert feat._dev_busy is False
|
|
194
|
+
assert feat._dev_report == ""
|
|
195
|
+
assert feat._user_inbox == []
|
|
196
|
+
assert feat._awaiting_plan_confirm is False
|
|
197
|
+
assert feat._director_busy is False
|
|
198
|
+
# 旧目标文件被清理
|
|
199
|
+
assert not gm.goal_path.exists(), "旧目标应清理,防被误判为插话"
|
|
200
|
+
assert not gm.plan_path.exists()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_session_switch_full_reset(screen):
|
|
204
|
+
"""SessionFeature._reset_goal_confirm 应走完整重置(非只清 confirm 标志)。"""
|
|
205
|
+
s, feat, gm = screen
|
|
206
|
+
from nexforge.features.session_feature import SessionFeature
|
|
207
|
+
sf = SessionFeature()
|
|
208
|
+
sf.register(s)
|
|
209
|
+
feat._turn_count = 3
|
|
210
|
+
feat._stop_director = True
|
|
211
|
+
gm.set_plan("旧方案")
|
|
212
|
+
sf._reset_goal_confirm()
|
|
213
|
+
assert feat._turn_count == 0
|
|
214
|
+
assert feat._stop_director is False
|
|
215
|
+
assert not gm.goal_path.exists()
|
|
216
|
+
assert not gm.plan_path.exists()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_reset_then_plan_flows_to_confirm(screen):
|
|
220
|
+
"""修复核心: 残留 _turn_count 会导致方案生成后跳过 turn=1,方案不落盘。
|
|
221
|
+
|
|
222
|
+
复位后 on_turn_done 应正确走 turn=1 分支(落盘方案 + 进入待确认)。
|
|
223
|
+
"""
|
|
224
|
+
s, feat, gm = screen
|
|
225
|
+
feat._turn_count = 5 # 模拟切换会话残留
|
|
226
|
+
feat.reset_for_session_change()
|
|
227
|
+
feat.on_turn_done("新方案: 1. 缓存 2. 并行")
|
|
228
|
+
assert feat._awaiting_plan_confirm, "复位后新方案应进入待确认(而非卡死)"
|
|
229
|
+
assert gm.read_plan(), "方案应落盘"
|
|
230
|
+
assert not s.director_started, "方案阶段不应启动总监"
|