nexforge-cli 0.2.11__tar.gz → 0.2.12__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.11 → nexforge_cli-0.2.12}/PKG-INFO +1 -1
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/manual/01-/345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md +1 -1
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/manual/04-/345/267/245/345/205/267/345/217/202/350/200/203.md +5 -1
- nexforge_cli-0.2.12/manual/07-/344/274/232/350/257/235/344/270/216/350/256/260/345/277/206.md +100 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/manual/README.md +1 -1
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/pyproject.toml +1 -1
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/__init__.py +1 -1
- nexforge_cli-0.2.12/src/nexforge/core/federation.py +494 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/knowledge_base.py +8 -1
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/summarizer.py +9 -0
- nexforge_cli-0.2.12/src/nexforge/core/system_inject.py +50 -0
- nexforge_cli-0.2.12/src/nexforge/features/federation_feature.py +183 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/kb_feature.py +4 -5
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/map_feature.py +6 -6
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/mode_features.py +62 -1
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/i18n.py +19 -7
- nexforge_cli-0.2.12/src/nexforge/tools/federation.py +175 -0
- nexforge_cli-0.2.12/src/nexforge/tools/kb_tools.py +133 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/registry.py +3 -0
- nexforge_cli-0.2.12/src/nexforge/tools/screenshot_tools.py +320 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/screens/chat.py +29 -5
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/slash_handler.py +59 -6
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/statusbar.py +5 -0
- nexforge_cli-0.2.12/tests/test_federation.py +476 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_manual_consistency.py +1 -1
- nexforge_cli-0.2.12/tests/test_p2_core.py +127 -0
- nexforge_cli-0.2.12/tests/test_screenshot_tools.py +77 -0
- nexforge_cli-0.2.12/tests/test_statusbar.py +53 -0
- nexforge_cli-0.2.12/tests/test_system_inject.py +90 -0
- nexforge_cli-0.2.11/_tmp_whl_check.py +0 -19
- nexforge_cli-0.2.11/manual/07-/344/274/232/350/257/235/344/270/216/350/256/260/345/277/206.md +0 -63
- nexforge_cli-0.2.11/src/nexforge/core/federation.py +0 -202
- nexforge_cli-0.2.11/src/nexforge/features/federation_feature.py +0 -47
- nexforge_cli-0.2.11/src/nexforge/tools/federation.py +0 -86
- nexforge_cli-0.2.11/src/nexforge/tools/kb_tools.py +0 -88
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/.gitignore +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/INSTALL.md +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/README.md +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/STATS_API.md +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/agent.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/cost.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/edit.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/err.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/file.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/model.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/ok.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/read.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/running.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/search.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/shell.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/thinking.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/web.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/assets/icons/write.svg +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/config.example.toml +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/devlog/2026-07-07.md +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/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.11 → nexforge_cli-0.2.12}/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.11 → nexforge_cli-0.2.12}/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.11 → nexforge_cli-0.2.12}/docs/submission/README-/346/212/225/347/250/277/350/257/264/346/230/216.md" +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/docs/submission/nexforge.md +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/docs/submission/nexforge.zh-CN.md +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/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.11 → nexforge_cli-0.2.12}/manual/03-/346/250/241/345/236/213/351/205/215/347/275/256.md" +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/manual/05-/346/225/260/346/215/256/344/270/216/346/220/234/347/264/242.md" +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/manual/06-/346/235/203/351/231/220/344/270/216/346/250/241/345/274/217.md" +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/manual/08-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md" +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/manual/09-/345/270/270/350/247/201/351/227/256/351/242/230.md" +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/manual/10-/346/217/222/344/273/266/345/274/200/345/217/221.md" +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/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.11 → nexforge_cli-0.2.12}/scripts/publish.sh +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/scripts/smoke_provider.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/scripts/smoke_tui.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/__main__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/app.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/cli.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/config.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/agent.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/context.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/goal_manager.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/jobs.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/memory_engine.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/permissions.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/plugin.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/project_mapper.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/provider.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/router.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/session.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/tool_manifest.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/core/types.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/auto_skills.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/base.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/memory_feature.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/plan_feature.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/registry.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/session_feature.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/slash_feature.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/stats_feature.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/features/tool_manifest_feature.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/icons.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/mcp/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/provider/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/provider/_retry.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/provider/anthropic.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/provider/balance.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/provider/deepseek.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/provider/openai.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/provider/qianwen.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/skills/loader.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/stats/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/stats/heartbeat.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/store/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/store/plan_store.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/store/session_store.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/theme.tcss +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/_paths.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/background.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/base.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/data_tools.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/edit_file.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/fs_ops.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/git.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/image_tools.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/list_dir.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/multi_edit.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/ops_tools.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/read_file.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/search.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/search_content.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/search_files.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/shell.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/web.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/tools/write_file.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/clipboard_image.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/screen_api.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/screens/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/screens/chat_actions.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/screens/splash.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/banner.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/diff_modal.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/image_sidebar.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/messages.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/mode_warning.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/model_sidebar.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/ui/widgets/sidebar.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/utils/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/utils/logging.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/web/__init__.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/web/server.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/src/nexforge/web/static/index.html +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/conftest.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_approval.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_config.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_goal_confirm.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_i18n.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_icons.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_lang_cmd.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_m2_tools.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_plugin.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_sanitize.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_session_repair.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_session_store.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_sidebar.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_sidebar_actions.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_splash_modal.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_tools.py +0 -0
- {nexforge_cli-0.2.11 → nexforge_cli-0.2.12}/tests/test_tui.py +0 -0
|
@@ -49,6 +49,8 @@ NexForgeCLI 内置 **35 个工具**供 AI 调用(`git_commit` 需在 config.to
|
|
|
49
49
|
| `image_gen` | 图片生成(阿里云通义万相) |
|
|
50
50
|
| `kb_search` | 知识库检索(全局+项目两级) |
|
|
51
51
|
| `kb_write` | 知识沉淀(写入项目/全局知识库) |
|
|
52
|
+
| `window_screenshot` | 指定窗口截图(Windows, 标题/进程定位, 遮挡也能截) |
|
|
53
|
+
| `web_screenshot` | Web 页面离线渲染截图(无头浏览器, 不弹窗) |
|
|
52
54
|
|
|
53
55
|
详见 [05-数据与搜索](05-数据与搜索.md)。
|
|
54
56
|
|
|
@@ -77,7 +79,9 @@ NexForgeCLI 内置 **35 个工具**供 AI 调用(`git_commit` 需在 config.to
|
|
|
77
79
|
|
|
78
80
|
| 工具 | 功能 |
|
|
79
81
|
|------|------|
|
|
80
|
-
| `federation` | 多项目联结体管理 |
|
|
82
|
+
| `federation` | 多项目联结体管理(init/add/remove/context/status/memory/sessions/handoff/close/task-add/task-list/task-done) |
|
|
83
|
+
| `kb_search` | 知识检索(scope 支持 fed 聚合联结体成员、member:<名> 指定成员) |
|
|
84
|
+
| `kb_write` | 知识沉淀(scope 支持 `member:<成员名>` 写回联结体成员知识库) |
|
|
81
85
|
|
|
82
86
|
## 工具限制
|
|
83
87
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# 会话与记忆
|
|
2
|
+
|
|
3
|
+
## 会话管理
|
|
4
|
+
|
|
5
|
+
### 自动保存
|
|
6
|
+
|
|
7
|
+
每次 AI 回复完成后自动保存当前会话到 `.nexforge/sessions/`,文件为可读 Markdown 格式。
|
|
8
|
+
|
|
9
|
+
### 自动恢复
|
|
10
|
+
|
|
11
|
+
启动时自动恢复当前项目最近一次会话,包括完整对话历史。
|
|
12
|
+
|
|
13
|
+
### 长会话上下文压缩
|
|
14
|
+
|
|
15
|
+
超过 60 轮时自动压缩:保留最近 20 轮全文,更早的对话压缩为**历史摘要**(恢复后可见,避免历史大段丢失);被压缩的原始对话**完整归档**到 `.nexforge/sessions/<id>.archive.md`,随时可查。
|
|
16
|
+
|
|
17
|
+
### 会话侧边栏
|
|
18
|
+
|
|
19
|
+
按 `F5` 打开会话侧边栏:
|
|
20
|
+
|
|
21
|
+
- **新建会话**:点击 `+ 新建会话`
|
|
22
|
+
- **切换会话**:点击会话名,自动保存当前、加载选中
|
|
23
|
+
- **重命名**:`/rename 新名称`
|
|
24
|
+
- **删除**:点击 `✖` 按钮
|
|
25
|
+
|
|
26
|
+
当前活跃会话有绿色圆点标记。
|
|
27
|
+
|
|
28
|
+
### 相关命令
|
|
29
|
+
|
|
30
|
+
| 命令 | 说明 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| `/save [name]` | 保存并命名当前会话 |
|
|
33
|
+
| `/rename <name>` | 重命名当前会话 |
|
|
34
|
+
| `/sessions` | 列出历史会话 |
|
|
35
|
+
| `/clear` | 清空当前会话 |
|
|
36
|
+
| `/cost` | 显示累计 token 和费用 |
|
|
37
|
+
|
|
38
|
+
## 项目记忆
|
|
39
|
+
|
|
40
|
+
NexForgeCLI 支持多层记忆,让 AI 在多次对话中持久记住项目信息。
|
|
41
|
+
|
|
42
|
+
### 记忆类型
|
|
43
|
+
|
|
44
|
+
| 记忆层 | 文件 | 作用 |
|
|
45
|
+
|--------|------|------|
|
|
46
|
+
| **基础记忆** | `.nexforge/memory/base.md` | 所有场景共享的持久信息 |
|
|
47
|
+
| **运维记忆** | `.nexforge/memory/ops.md` | 运维操作记录(服务器配置、常用命令) |
|
|
48
|
+
| **目标记忆** | `.nexforge/memory/goal.md` | 目标模式专用上下文 |
|
|
49
|
+
| **已验证方案** | `.nexforge/ops_known.md` | 运维已验证的成功命令/调用(读取复用,可手动维护) |
|
|
50
|
+
|
|
51
|
+
启动时自动加载基础记忆。运维模式开启时额外加载运维记忆。
|
|
52
|
+
|
|
53
|
+
### 联 结体
|
|
54
|
+
|
|
55
|
+
联结体是**多项目协同工作区**。关联多个相关项目,为每个项目设定角色,AI 在联结体上下文中工作,
|
|
56
|
+
**并把工作写回各成员项目自身结构**,保证成员回到各自的 nf 实例继续开发时零信息割裂。
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# 在 nf 输入框里用 /federation 命令
|
|
60
|
+
/federation init # 初始化联结体
|
|
61
|
+
/federation add <项目路径> [角色] # 添加成员(名称取目录名, 角色可选如 后端)
|
|
62
|
+
/federation remove <名称> # 移除成员
|
|
63
|
+
/federation context # 查看联结体上下文(成员+角色+最近共享记忆)
|
|
64
|
+
/federation status # 查看各成员实时状态(git分支/未提交/最近会话)
|
|
65
|
+
/federation memory # 查看共享记忆
|
|
66
|
+
/federation sessions # 列出各成员项目的会话历史
|
|
67
|
+
/federation handoff # 为所有成员生成/更新交接文档
|
|
68
|
+
/federation close # 结束本次协同: 全成员写回(交接+地图+纪要会话)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### 双向同步机制(写回)
|
|
72
|
+
|
|
73
|
+
联结体是**临时工作区**,各成员项目是**持久家园**。协同期间自动写回:
|
|
74
|
+
|
|
75
|
+
| 写回内容 | 落点(成员项目内) | 触发 |
|
|
76
|
+
|---------|-----------------|------|
|
|
77
|
+
| 每轮操作日志 | `.nexforge/ops_log.md` | 每轮结束自动 |
|
|
78
|
+
| 进展记忆 | `.nexforge/memory/base.md` | 每轮结束自动 |
|
|
79
|
+
| 项目地图刷新 | `.nexforge/map/` | 轮末哈希比对,有变化才 sync |
|
|
80
|
+
| 知识沉淀 | `.nexforge/kb/`(`kb_write scope=member:<名>`) | AI 主动 |
|
|
81
|
+
| 交接文档 | `.nexforge/federation/handoff.md` | `/federation close` 或 `handoff` |
|
|
82
|
+
| 工作纪要会话 | `.nexforge/sessions/session-*.md` | `/federation close` |
|
|
83
|
+
|
|
84
|
+
成员项目回到自身 nf 启动时,若检测到 `handoff.md` 会自动注入交接上下文,AI 延续工作不割裂。
|
|
85
|
+
`kb_write` 支持 `scope=member:<成员名>` 直接把知识写回指定成员的知识库;
|
|
86
|
+
`kb_search` 支持 `scope=fed`(聚合检索所有成员知识库) 和 `scope=member:<名>`(检索指定成员)。
|
|
87
|
+
|
|
88
|
+
#### 任务流转
|
|
89
|
+
|
|
90
|
+
AI 通过 `federation` 工具管理跨成员任务(`.nexforge/federation/tasks.md`):
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
federation task-add name=<成员> content=<任务描述> # 分配任务, 返回 #id
|
|
94
|
+
federation task-list # 列出所有任务及状态
|
|
95
|
+
federation task-done content=<id> # 标记任务完成
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### 双向记忆
|
|
99
|
+
|
|
100
|
+
共享记忆按「用户指令 + AI 决策」成对记录(每轮自动),供联结体上下文读取,协同意图可追溯。
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""联结体引擎 —— 多项目联结,共享记忆与会话,协同工作写回各成员项目。
|
|
2
|
+
|
|
3
|
+
设计原则: 联结体是临时工作区, 各成员项目是持久家园。
|
|
4
|
+
所有可归属到成员项目的产出(日志/进展/地图/交接)必须写回成员项目自身结构,
|
|
5
|
+
保证成员项目回到各自的 nf 实例继续开发时零信息割裂。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import tomllib
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Member:
|
|
21
|
+
name: str # 项目名
|
|
22
|
+
path: Path # 绝对路径
|
|
23
|
+
git_remote: str = "" # git remote URL
|
|
24
|
+
git_branch: str = "" # 当前分支
|
|
25
|
+
role: str = "" # 角色(如 前端/后端/文档/基础设施)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Federation:
|
|
30
|
+
name: str
|
|
31
|
+
root: Path # 主项目目录
|
|
32
|
+
members: list[Member] = field(default_factory=list)
|
|
33
|
+
memory: str = "" # 共享记忆内容
|
|
34
|
+
_config_path: Path | None = None
|
|
35
|
+
|
|
36
|
+
# 写回文件的行数上限
|
|
37
|
+
MAX_LOG_LINES = 500 # ops_log.md
|
|
38
|
+
MAX_MEM_LINES = 800 # memory/base.md
|
|
39
|
+
MAX_FED_MEM_LINES = 400 # federation/memory.md
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def dir(self) -> Path:
|
|
43
|
+
return self.root / ".nexforge" / "federation"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def memory_path(self) -> Path:
|
|
47
|
+
return self.dir / "memory.md"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def sessions_dir(self) -> Path:
|
|
51
|
+
return self.dir / "sessions"
|
|
52
|
+
|
|
53
|
+
def init(self) -> None:
|
|
54
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
self.sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
if not self.memory_path.exists():
|
|
57
|
+
self.memory_path.write_text("# 联结体共享记忆\n\n", encoding="utf-8")
|
|
58
|
+
self._save_config()
|
|
59
|
+
|
|
60
|
+
def add_member(self, name: str, project_path: Path, role: str = "") -> Member:
|
|
61
|
+
p = project_path.resolve()
|
|
62
|
+
# 重名防御: 同名成员已存在则拒绝(修复: 重复 add 导致 get_member 歧义/写回混乱)
|
|
63
|
+
if self.get_member(name) is not None:
|
|
64
|
+
raise ValueError(f"成员已存在: {name}")
|
|
65
|
+
# 获取 git 信息
|
|
66
|
+
remote = ""
|
|
67
|
+
branch = ""
|
|
68
|
+
try:
|
|
69
|
+
import subprocess
|
|
70
|
+
r = subprocess.run(
|
|
71
|
+
["git", "-C", str(p), "remote", "get-url", "origin"],
|
|
72
|
+
capture_output=True, text=True, timeout=5
|
|
73
|
+
)
|
|
74
|
+
if r.returncode == 0:
|
|
75
|
+
remote = r.stdout.strip()
|
|
76
|
+
r = subprocess.run(
|
|
77
|
+
["git", "-C", str(p), "branch", "--show-current"],
|
|
78
|
+
capture_output=True, text=True, timeout=5
|
|
79
|
+
)
|
|
80
|
+
if r.returncode == 0:
|
|
81
|
+
branch = r.stdout.strip()
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
member = Member(name=name, path=p, git_remote=remote, git_branch=branch, role=role)
|
|
85
|
+
self.members.append(member)
|
|
86
|
+
self._save_config()
|
|
87
|
+
return member
|
|
88
|
+
|
|
89
|
+
def remove_member(self, name: str) -> None:
|
|
90
|
+
self.members = [m for m in self.members if m.name != name]
|
|
91
|
+
self._save_config()
|
|
92
|
+
|
|
93
|
+
def get_member(self, name: str) -> Member | None:
|
|
94
|
+
return next((m for m in self.members if m.name == name), None)
|
|
95
|
+
|
|
96
|
+
def member_status(self, member: Member) -> str:
|
|
97
|
+
"""单成员状态: git 分支 / 工作区未提交数 / 最近会话时间(P2-9 状态面板)。"""
|
|
98
|
+
parts: list[str] = []
|
|
99
|
+
git_ok = False
|
|
100
|
+
try:
|
|
101
|
+
import subprocess
|
|
102
|
+
r = subprocess.run(
|
|
103
|
+
["git", "-C", str(member.path), "branch", "--show-current"],
|
|
104
|
+
capture_output=True, text=True, timeout=5
|
|
105
|
+
)
|
|
106
|
+
if r.returncode == 0:
|
|
107
|
+
git_ok = True
|
|
108
|
+
if r.stdout.strip():
|
|
109
|
+
parts.append(f"分支 {r.stdout.strip()}")
|
|
110
|
+
r2 = subprocess.run(
|
|
111
|
+
["git", "-C", str(member.path), "status", "--porcelain"],
|
|
112
|
+
capture_output=True, text=True, timeout=5
|
|
113
|
+
)
|
|
114
|
+
if r2.returncode == 0:
|
|
115
|
+
git_ok = True
|
|
116
|
+
if r2.stdout.strip():
|
|
117
|
+
n = len([l for l in r2.stdout.splitlines() if l.strip()])
|
|
118
|
+
parts.append(f"未提交 {n} 项")
|
|
119
|
+
else:
|
|
120
|
+
parts.append("工作区干净")
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
if not git_ok:
|
|
124
|
+
parts.append("非 git 项目") # 修复: 原把 git 命令失败误报为"工作区干净"
|
|
125
|
+
infos = self.list_member_sessions(member)
|
|
126
|
+
if infos:
|
|
127
|
+
parts.append(f"最近会话 {infos[0].created}")
|
|
128
|
+
return " | ".join(parts)
|
|
129
|
+
|
|
130
|
+
def status_report(self) -> str:
|
|
131
|
+
"""全成员状态报告(/federation status 与 AI 工具用)。"""
|
|
132
|
+
lines = [f"[联结体: {self.name}] 成员状态:"]
|
|
133
|
+
for m in self.members:
|
|
134
|
+
role = f" (角色: {m.role})" if m.role else ""
|
|
135
|
+
lines.append(f" - {m.name}: {m.path}{role}")
|
|
136
|
+
lines.append(f" {self.member_status(m)}")
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
|
|
139
|
+
# ── 共享记忆(治理: 去重 + 上限) ─────────────────────────
|
|
140
|
+
|
|
141
|
+
def read_memory(self) -> str:
|
|
142
|
+
from nexforge.tools._paths import sanitize
|
|
143
|
+
if self.memory_path.exists():
|
|
144
|
+
try:
|
|
145
|
+
text = self.memory_path.read_text(encoding="utf-8")
|
|
146
|
+
except (UnicodeDecodeError, OSError):
|
|
147
|
+
text = "" # 记忆文件损坏时容错(修复: 原直接抛异常)
|
|
148
|
+
return sanitize(text)
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
def append_memory(self, text: str) -> None:
|
|
152
|
+
from nexforge.tools._paths import sanitize
|
|
153
|
+
text = sanitize(text).strip()
|
|
154
|
+
if not text:
|
|
155
|
+
return
|
|
156
|
+
# 单条限长(防超大文本撑爆记忆; 修复: 原无限制)
|
|
157
|
+
if len(text) > 2000:
|
|
158
|
+
text = text[:2000] + "…(截断)"
|
|
159
|
+
current = self.read_memory()
|
|
160
|
+
# 去重: 核心内容(去掉 ## 时间戳行)完整存在则不追加(防每轮重复刷屏;
|
|
161
|
+
# 修复: 原 core[:80] 会导致同指令不同决策时漏记新决策)
|
|
162
|
+
core = "\n".join(
|
|
163
|
+
l.strip() for l in text.splitlines()
|
|
164
|
+
if l.strip() and not l.strip().startswith("## ")
|
|
165
|
+
).strip()
|
|
166
|
+
if core and core in current:
|
|
167
|
+
return
|
|
168
|
+
current += f"\n{text}\n"
|
|
169
|
+
# 上限: 超 400 行保留尾部 300 行(最新记忆优先)
|
|
170
|
+
lines = current.splitlines()
|
|
171
|
+
if len(lines) > self.MAX_FED_MEM_LINES:
|
|
172
|
+
current = "\n".join(lines[-300:]) + "\n"
|
|
173
|
+
self.memory_path.write_text(current, encoding="utf-8")
|
|
174
|
+
|
|
175
|
+
def list_member_sessions(self, member: Member) -> list:
|
|
176
|
+
"""列出成员的会话(从其 .nexforge/sessions 目录)。
|
|
177
|
+
|
|
178
|
+
直接扫描扁平目录 + 旧日期子目录(异步安全,不切换 cwd)。
|
|
179
|
+
"""
|
|
180
|
+
from nexforge.store.session_store import SessionInfo, _BASE_RE
|
|
181
|
+
sessions_dir = member.path / ".nexforge" / "sessions"
|
|
182
|
+
if not sessions_dir.is_dir():
|
|
183
|
+
return []
|
|
184
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
185
|
+
infos: list[SessionInfo] = []
|
|
186
|
+
seen: set[str] = set()
|
|
187
|
+
|
|
188
|
+
def _add(md: Path, created: str) -> None:
|
|
189
|
+
if md.stem in seen:
|
|
190
|
+
return
|
|
191
|
+
# 只收基础会话文件(复用 session_store 的正则,新旧格式统一;
|
|
192
|
+
# 修复:旧正则会把新格式 session-YYYYMMDD-HHMMSS-ffffff.md
|
|
193
|
+
# 误判为分段文件导致联结体列表看不到任何新会话)
|
|
194
|
+
if not _BASE_RE.match(md.name):
|
|
195
|
+
return
|
|
196
|
+
seen.add(md.stem)
|
|
197
|
+
name = self._read_title(md) or md.stem
|
|
198
|
+
infos.append(SessionInfo(
|
|
199
|
+
id=md.stem,
|
|
200
|
+
name=name,
|
|
201
|
+
path=md,
|
|
202
|
+
created=created,
|
|
203
|
+
message_count=self._count_messages(md),
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
# 新格式:扁平目录
|
|
207
|
+
for md in sorted(sessions_dir.glob("*.md"), key=os.path.getmtime, reverse=True):
|
|
208
|
+
created = _dt.fromtimestamp(md.stat().st_mtime, tz=_tz.utc).strftime("%Y-%m-%d")
|
|
209
|
+
_add(md, created)
|
|
210
|
+
|
|
211
|
+
# 旧格式:YYYY-MM-DD/ 子目录
|
|
212
|
+
for day_dir in sorted(sessions_dir.iterdir(), reverse=True):
|
|
213
|
+
if not day_dir.is_dir():
|
|
214
|
+
continue
|
|
215
|
+
for md in sorted(day_dir.glob("*.md"), key=os.path.getmtime, reverse=True):
|
|
216
|
+
_add(md, day_dir.name)
|
|
217
|
+
|
|
218
|
+
return infos
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _read_title(path: Path) -> str | None:
|
|
222
|
+
from nexforge.tools._paths import sanitize
|
|
223
|
+
try:
|
|
224
|
+
first = path.read_text(encoding="utf-8").split("\n", 1)[0]
|
|
225
|
+
title = first.lstrip("# ").strip()
|
|
226
|
+
return sanitize(title) or None
|
|
227
|
+
except Exception:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _count_messages(path: Path) -> int:
|
|
232
|
+
try:
|
|
233
|
+
text = path.read_text(encoding="utf-8")
|
|
234
|
+
return text.count("### \u276f 用户")
|
|
235
|
+
except Exception:
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
# ── 上下文 ──────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
def context_prompt(self) -> str:
|
|
241
|
+
"""构建 AI 上下文:联结体信息 + 共享记忆(读最近, 新记忆优先)。"""
|
|
242
|
+
parts = [f"[联结体: {self.name}]"]
|
|
243
|
+
parts.append(f"项目数: {len(self.members)}")
|
|
244
|
+
for m in self.members:
|
|
245
|
+
parts.append(f" - {m.name}: {m.path}" + (f" (角色: {m.role})" if m.role else ""))
|
|
246
|
+
if m.git_branch:
|
|
247
|
+
parts.append(f" git: {m.git_remote} ({m.git_branch})")
|
|
248
|
+
memory = self.read_memory()
|
|
249
|
+
if memory.strip():
|
|
250
|
+
# 去掉 init 标题行后仍有内容才算有记忆(避免空联结体输出无意义标题)
|
|
251
|
+
mem_body = "\n".join(
|
|
252
|
+
l for l in memory.splitlines()
|
|
253
|
+
if l.strip() and not l.startswith("# 联结体共享记忆")
|
|
254
|
+
)
|
|
255
|
+
if mem_body.strip():
|
|
256
|
+
parts.append(f"\n## 共享记忆(最近)\n{memory[-2000:]}")
|
|
257
|
+
return "\n".join(parts)
|
|
258
|
+
|
|
259
|
+
# ── 写回机制: 联结体工作 → 成员项目自身结构 ─────────────
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def _member_file(member: Member, rel: str) -> Path:
|
|
263
|
+
"""成员项目内文件(自动创建父目录)。"""
|
|
264
|
+
p = member.path / ".nexforge" / rel
|
|
265
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
266
|
+
return p
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _append_limited(path: Path, entry: str, max_lines: int, keep_tail: int) -> None:
|
|
270
|
+
"""追加条目,超行数时保留尾部。"""
|
|
271
|
+
if path.exists():
|
|
272
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
273
|
+
if len(lines) > max_lines:
|
|
274
|
+
path.write_text("\n".join(lines[-keep_tail:]) + "\n", encoding="utf-8")
|
|
275
|
+
with path.open("a", encoding="utf-8") as f:
|
|
276
|
+
f.write(entry)
|
|
277
|
+
|
|
278
|
+
def append_member_log(self, member: Member, text: str) -> Path:
|
|
279
|
+
"""写回: 成员项目 .nexforge/ops_log.md 追加联结体工作条目。"""
|
|
280
|
+
from nexforge.tools._paths import sanitize
|
|
281
|
+
log = self._member_file(member, "ops_log.md")
|
|
282
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
283
|
+
entry = f"\n## {ts} [联结体:{self.name}]\n{sanitize(text).strip()}\n"
|
|
284
|
+
self._append_limited(log, entry, self.MAX_LOG_LINES, self.MAX_LOG_LINES - 100)
|
|
285
|
+
return log
|
|
286
|
+
|
|
287
|
+
def append_member_progress(self, member: Member, text: str) -> Path:
|
|
288
|
+
"""写回: 成员项目 .nexforge/memory/base.md 追加联结体进展条目。"""
|
|
289
|
+
from nexforge.tools._paths import sanitize
|
|
290
|
+
mem = self._member_file(member, "memory/base.md")
|
|
291
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
292
|
+
entry = f"\n## [联结体:{self.name}] {ts}\n{sanitize(text).strip()}\n"
|
|
293
|
+
self._append_limited(mem, entry, self.MAX_MEM_LINES, self.MAX_MEM_LINES - 100)
|
|
294
|
+
return mem
|
|
295
|
+
|
|
296
|
+
def sync_member_map(self, member: Member) -> bool:
|
|
297
|
+
"""写回: 刷新成员项目地图(哈希比对, 有变化才全量 sync)。"""
|
|
298
|
+
try:
|
|
299
|
+
from nexforge.core.project_mapper import ProjectMapper
|
|
300
|
+
mapper = ProjectMapper(member.path)
|
|
301
|
+
report = mapper.check()
|
|
302
|
+
if not report.get("in_sync"):
|
|
303
|
+
mapper.sync()
|
|
304
|
+
return True
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def write_back(self, member: Member, *, summary: str = "",
|
|
310
|
+
progress: str = "", sync_map: bool = True) -> list[Path]:
|
|
311
|
+
"""综合写回一个成员: 操作日志 + 进展记忆 + (可选)地图刷新。
|
|
312
|
+
|
|
313
|
+
返回写出的文件路径列表。单项失败不影响其他(异常保护)。
|
|
314
|
+
成员目录已不存在时跳过(不重建空 .nexforge)。
|
|
315
|
+
"""
|
|
316
|
+
if not member.path.is_dir():
|
|
317
|
+
return []
|
|
318
|
+
out: list[Path] = []
|
|
319
|
+
try:
|
|
320
|
+
if summary.strip():
|
|
321
|
+
out.append(self.append_member_log(member, summary))
|
|
322
|
+
except OSError:
|
|
323
|
+
pass
|
|
324
|
+
try:
|
|
325
|
+
if progress.strip():
|
|
326
|
+
out.append(self.append_member_progress(member, progress))
|
|
327
|
+
except OSError:
|
|
328
|
+
pass
|
|
329
|
+
if sync_map:
|
|
330
|
+
self.sync_member_map(member)
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
def generate_handoff(self, member: Member, *, goal: str = "",
|
|
334
|
+
summary: str = "", next_steps: str = "") -> Path:
|
|
335
|
+
"""生成/更新成员交接文档 .nexforge/federation/handoff.md。
|
|
336
|
+
|
|
337
|
+
成员项目回到自身 nf 实例时, 启动检测到此文件会注入上下文, 零割裂延续。
|
|
338
|
+
"""
|
|
339
|
+
p = self._member_file(member, "federation/handoff.md")
|
|
340
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
341
|
+
content = (
|
|
342
|
+
f"# 联结体交接 — {member.name}\n\n"
|
|
343
|
+
f"- 联结体: {self.name} | 更新时间: {ts}\n"
|
|
344
|
+
f"- 成员路径: {member.path}\n"
|
|
345
|
+
f"- 角色: {member.role or '未指定'}\n\n"
|
|
346
|
+
f"## 协同目标\n{goal.strip() or '(未记录)'}\n\n"
|
|
347
|
+
f"## 本次改动\n{summary.strip() or '(未记录)'}\n\n"
|
|
348
|
+
f"## 下一步建议\n{next_steps.strip() or '(未记录)'}\n"
|
|
349
|
+
)
|
|
350
|
+
p.write_text(content, encoding="utf-8")
|
|
351
|
+
return p
|
|
352
|
+
|
|
353
|
+
def _write_member_summary_session(self, member: Member, summary: str) -> Path | None:
|
|
354
|
+
"""写回: 成员 .nexforge/sessions/ 写入一条'联结体工作纪要'会话(可 /load 查看)。"""
|
|
355
|
+
try:
|
|
356
|
+
from nexforge.store.session_store import _next_id
|
|
357
|
+
sid = _next_id() # 已含 "session-" 前缀(修复: 曾重复拼接导致文件名 session-session-*.md 不被会话列表识别)
|
|
358
|
+
f = member.path / ".nexforge" / "sessions" / f"{sid}.md"
|
|
359
|
+
f.parent.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
361
|
+
body = (
|
|
362
|
+
f"# 联结体工作纪要\n\n> 创建 {ts}\n"
|
|
363
|
+
f"> 项目 {member.name}\n\n"
|
|
364
|
+
f"### ◆ 助手\n\n"
|
|
365
|
+
f"这是联结体 [{self.name}] 工作期间本项目的操作纪要"
|
|
366
|
+
f"(生成于 {ts})。\n\n"
|
|
367
|
+
f"{summary.strip() or '(无记录)'}\n"
|
|
368
|
+
)
|
|
369
|
+
f.write_text(body, encoding="utf-8")
|
|
370
|
+
return f
|
|
371
|
+
except Exception:
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
def close(self, *, goal: str = "", summary: str = "", next_steps: str = "") -> dict[str, list[Path]]:
|
|
375
|
+
"""结束联结体工作: 为所有成员生成交接文档 + 刷新地图 + 写纪要会话。
|
|
376
|
+
|
|
377
|
+
返回 {成员名: [写出的文件...]}。不销毁联结体配置(可继续 add/复用)。
|
|
378
|
+
"""
|
|
379
|
+
result: dict[str, list[Path]] = {}
|
|
380
|
+
# summary 为空时自动取共享记忆尾部, 避免交接文档/纪要会话内容空洞
|
|
381
|
+
if not summary.strip():
|
|
382
|
+
mem = self.read_memory()
|
|
383
|
+
if mem.strip():
|
|
384
|
+
summary = "(自动摘要, 详见联结体 memory.md)\n" + mem.strip()[-500:]
|
|
385
|
+
for m in self.members:
|
|
386
|
+
files: list[Path] = []
|
|
387
|
+
if not m.path.is_dir():
|
|
388
|
+
continue # 成员目录已不存在: 跳过写回(不重建空目录)
|
|
389
|
+
try:
|
|
390
|
+
files.append(self.generate_handoff(m, goal=goal, summary=summary, next_steps=next_steps))
|
|
391
|
+
self.sync_member_map(m)
|
|
392
|
+
sess = self._write_member_summary_session(m, summary)
|
|
393
|
+
if sess:
|
|
394
|
+
files.append(sess)
|
|
395
|
+
except OSError:
|
|
396
|
+
pass # 单成员写回失败不影响其他成员
|
|
397
|
+
result[m.name] = files
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
# ── 任务流转 ────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
def _tasks_path(self) -> Path:
|
|
403
|
+
return self.dir / "tasks.md"
|
|
404
|
+
|
|
405
|
+
def task_add(self, member_name: str, content: str) -> str:
|
|
406
|
+
"""给成员添加任务。返回任务 id。"""
|
|
407
|
+
tasks = self._tasks_path()
|
|
408
|
+
tasks.parent.mkdir(parents=True, exist_ok=True)
|
|
409
|
+
text = tasks.read_text(encoding="utf-8") if tasks.exists() else ""
|
|
410
|
+
ids = [int(m.group(1)) for m in re.finditer(r"#(\d+)", text)]
|
|
411
|
+
tid = max(ids, default=0) + 1
|
|
412
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
413
|
+
# 任务描述换行压平为空格(修复: 多行描述会破坏 task_done 的块定位)
|
|
414
|
+
flat = content.strip().replace("\n", " ").replace("\r", "")
|
|
415
|
+
with tasks.open("a", encoding="utf-8") as f:
|
|
416
|
+
f.write(f"\n## [{ts}] #{tid} [{member_name}] {flat}\n状态: 待办\n")
|
|
417
|
+
return str(tid)
|
|
418
|
+
|
|
419
|
+
def task_list(self) -> str:
|
|
420
|
+
"""列出所有任务(最新在后)。"""
|
|
421
|
+
tasks = self._tasks_path()
|
|
422
|
+
if not tasks.exists():
|
|
423
|
+
return "(无任务)"
|
|
424
|
+
return tasks.read_text(encoding="utf-8").strip() or "(无任务)"
|
|
425
|
+
|
|
426
|
+
def task_done(self, task_id: str) -> bool:
|
|
427
|
+
"""标记任务完成(按 #id 定位块, 只改该任务的状态)。"""
|
|
428
|
+
tasks = self._tasks_path()
|
|
429
|
+
if not tasks.exists():
|
|
430
|
+
return False
|
|
431
|
+
lines = tasks.read_text(encoding="utf-8").splitlines()
|
|
432
|
+
target = f"#{task_id} "
|
|
433
|
+
in_block = False
|
|
434
|
+
done = False
|
|
435
|
+
out: list[str] = []
|
|
436
|
+
for line in lines:
|
|
437
|
+
if line.startswith("## "):
|
|
438
|
+
in_block = target in line
|
|
439
|
+
if in_block and line.strip() == "状态: 待办":
|
|
440
|
+
line = f"状态: ✅ 完成 ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
|
441
|
+
done = True
|
|
442
|
+
out.append(line)
|
|
443
|
+
if done:
|
|
444
|
+
tasks.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
445
|
+
return done
|
|
446
|
+
|
|
447
|
+
# ── 配置持久化 ──────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def _tstr(v: str) -> str:
|
|
451
|
+
"""TOML 基本字符串转义: 反斜杠/双引号/控制字符(防自由文本破坏配置)。"""
|
|
452
|
+
return (v.replace("\\", "\\\\").replace('"', '\\"')
|
|
453
|
+
.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t"))
|
|
454
|
+
|
|
455
|
+
def _save_config(self) -> None:
|
|
456
|
+
self._config_path = self.dir / "federation.toml"
|
|
457
|
+
lines = [f'name = "{self._tstr(self.name)}"']
|
|
458
|
+
for m in self.members:
|
|
459
|
+
lines.append("")
|
|
460
|
+
lines.append("[[member]]")
|
|
461
|
+
lines.append(f'name = "{self._tstr(m.name)}"')
|
|
462
|
+
lines.append(f'path = "{self._tstr(m.path.as_posix())}"')
|
|
463
|
+
if m.git_remote:
|
|
464
|
+
lines.append(f'git_remote = "{self._tstr(m.git_remote)}"')
|
|
465
|
+
if m.git_branch:
|
|
466
|
+
lines.append(f'git_branch = "{self._tstr(m.git_branch)}"')
|
|
467
|
+
if m.role:
|
|
468
|
+
lines.append(f'role = "{self._tstr(m.role)}"')
|
|
469
|
+
self._config_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
470
|
+
|
|
471
|
+
@staticmethod
|
|
472
|
+
def load(root: Path) -> Federation | None:
|
|
473
|
+
config = root / ".nexforge" / "federation" / "federation.toml"
|
|
474
|
+
if not config.exists():
|
|
475
|
+
return None
|
|
476
|
+
try:
|
|
477
|
+
with config.open("rb") as f:
|
|
478
|
+
data = tomllib.load(f)
|
|
479
|
+
fed = Federation(
|
|
480
|
+
name=data.get("name", ""),
|
|
481
|
+
root=root,
|
|
482
|
+
)
|
|
483
|
+
for mdata in data.get("member", []):
|
|
484
|
+
fed.members.append(Member(
|
|
485
|
+
name=mdata.get("name", ""),
|
|
486
|
+
path=Path(mdata.get("path", "")),
|
|
487
|
+
git_remote=mdata.get("git_remote", ""),
|
|
488
|
+
git_branch=mdata.get("git_branch", ""),
|
|
489
|
+
role=mdata.get("role", ""),
|
|
490
|
+
))
|
|
491
|
+
fed._config_path = config
|
|
492
|
+
return fed
|
|
493
|
+
except Exception:
|
|
494
|
+
return None
|
|
@@ -159,11 +159,18 @@ class KnowledgeBase:
|
|
|
159
159
|
def search(self, query: str, scope: str = "all", top_k: int = 5) -> str:
|
|
160
160
|
"""行级多关键词打分检索, 每文件一条最佳命中(含上下文±1行)。"""
|
|
161
161
|
self.ensure()
|
|
162
|
+
return self._search_in_dirs(query, self.dirs(scope), top_k)
|
|
163
|
+
|
|
164
|
+
def _search_in_dirs(self, query: str, dirs: list[Path], top_k: int = 5) -> str:
|
|
165
|
+
"""行级多关键词打分检索(目录列表版)。
|
|
166
|
+
|
|
167
|
+
供跨成员聚合检索复用(kb_search scope=fed / member:<名>)。
|
|
168
|
+
"""
|
|
162
169
|
tokens = self._tokens(query)
|
|
163
170
|
if not tokens:
|
|
164
171
|
return "(空查询)"
|
|
165
172
|
best: dict[Path, tuple[float, int, str]] = {} # file -> (score, line_no, ctx)
|
|
166
|
-
for d in
|
|
173
|
+
for d in dirs:
|
|
167
174
|
for rel, meta in self._refresh_index(d).items():
|
|
168
175
|
f = d / rel
|
|
169
176
|
if not f.is_file():
|