jfox-cli 0.3.2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. jfox_cli-0.4.0/.claude/settings.local.json +9 -0
  2. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/PKG-INFO +1 -1
  3. jfox_cli-0.4.0/docs/superpowers/plans/2026-04-15-session-summary-confirmation.md +135 -0
  4. jfox_cli-0.4.0/docs/superpowers/specs/2026-04-15-session-summary-confirmation-design.md +52 -0
  5. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/__init__.py +1 -1
  6. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/cli.py +111 -4
  7. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/config.py +6 -5
  8. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/process.py +28 -2
  9. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/server.py +9 -2
  10. jfox_cli-0.4.0/jfox/embedding_backend.py +174 -0
  11. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/pyproject.toml +1 -1
  12. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-common/SKILL.md +1 -1
  13. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-session-summary/SKILL.md +32 -9
  14. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/conftest.py +11 -1
  15. jfox_cli-0.4.0/tests/test_config_set_unit.py +78 -0
  16. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_config_unit.py +4 -4
  17. jfox_cli-0.4.0/tests/test_embedding_device.py +107 -0
  18. jfox_cli-0.4.0/tests/unit/test_daemon_process.py +86 -0
  19. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/uv.lock +1 -1
  20. jfox_cli-0.3.2/jfox/embedding_backend.py +0 -112
  21. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.githooks/pre-push +0 -0
  22. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.github/workflows/integration-test.yml +0 -0
  23. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.github/workflows/publish.yml +0 -0
  24. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.gitignore +0 -0
  25. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.python-version +0 -0
  26. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/AGENTS.md +0 -0
  27. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/CHANGELOG.md +0 -0
  28. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/CLAUDE.md +0 -0
  29. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/DEVELOPMENT_PLAN.md +0 -0
  30. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/README.md +0 -0
  31. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/SESSION.md +0 -0
  32. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/SESSION_SUMMARY.md +0 -0
  33. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/installation.md +0 -0
  34. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-11-bulk-import-bm25-fix.md +0 -0
  35. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-11-edit-command.md +0 -0
  36. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-11-unify-format-option.md +0 -0
  37. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md +0 -0
  38. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-edit-content-file.md +0 -0
  39. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md +0 -0
  40. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md +0 -0
  41. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-fix-jfox-health-skill-kb-param.md +0 -0
  42. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-index-kb-param.md +0 -0
  43. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-lazy-import-perf.md +0 -0
  44. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-skill-redesign.md +0 -0
  45. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-14-sync-docs-daemon-show.md +0 -0
  46. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/specs/2026-04-03-bugfixes-design.md +0 -0
  47. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md +0 -0
  48. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md +0 -0
  49. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/specs/2026-04-14-show-command-design.md +0 -0
  50. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/troubleshooting.md +0 -0
  51. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jessica-jones-static-cable.md +0 -0
  52. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/__main__.py +0 -0
  53. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/bm25_index.py +0 -0
  54. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/__init__.py +0 -0
  55. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/__main__.py +0 -0
  56. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/client.py +0 -0
  57. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/formatters.py +0 -0
  58. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/git_extractor.py +0 -0
  59. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/global_config.py +0 -0
  60. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/graph.py +0 -0
  61. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/indexer.py +0 -0
  62. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/kb_manager.py +0 -0
  63. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/models.py +0 -0
  64. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/note.py +0 -0
  65. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/performance.py +0 -0
  66. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/search_engine.py +0 -0
  67. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/template.py +0 -0
  68. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/template_cli.py +0 -0
  69. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/vector_store.py +0 -0
  70. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/pytest.ini +0 -0
  71. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/run_full_test.ps1 +0 -0
  72. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/README.md +0 -0
  73. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-ingest/SKILL.md +0 -0
  74. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-organize/SKILL.md +0 -0
  75. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-search/SKILL.md +0 -0
  76. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/COVERAGE_PLAN.md +0 -0
  77. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/MIGRATION.md +0 -0
  78. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/TESTS.md +0 -0
  79. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/integration/__init__.py +0 -0
  80. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/integration/test_backlinks.py +0 -0
  81. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/performance/__init__.py +0 -0
  82. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/performance/test_performance.py +0 -0
  83. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_advanced_features.py +0 -0
  84. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_cli_format.py +0 -0
  85. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_core_workflow.py +0 -0
  86. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_hybrid_search.py +0 -0
  87. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_integration.py +0 -0
  88. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_kb_current.py +0 -0
  89. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_suggest_links.py +0 -0
  90. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/__init__.py +0 -0
  91. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_bm25_batch.py +0 -0
  92. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_edit.py +0 -0
  93. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_format_unify.py +0 -0
  94. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_formatters.py +0 -0
  95. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_git_extractor.py +0 -0
  96. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_global_config.py +0 -0
  97. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_index_kb_param.py +0 -0
  98. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_indexer_clear_before_rebuild.py +0 -0
  99. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_indexer_verify.py +0 -0
  100. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_kb_manager.py +0 -0
  101. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_lazy_import.py +0 -0
  102. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_logging_config.py +0 -0
  103. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_show.py +0 -0
  104. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_template.py +0 -0
  105. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_template_cli.py +0 -0
  106. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_vector_store_clear.py +0 -0
  107. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/__init__.py +0 -0
  108. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/assertions.py +0 -0
  109. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/jfox_cli.py +0 -0
  110. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/note_generator.py +0 -0
  111. {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/temp_kb.py +0 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(cp \"C:/Users/zhuzh/.claude/skills/jfox-ingest/SKILL.md\" \"skills-recommend/claude-code/jfox-ingest/SKILL.md\")",
5
+ "Bash(cp \"C:/Users/zhuzh/.claude/skills/jfox-organize/SKILL.md\" \"skills-recommend/claude-code/jfox-organize/SKILL.md\")",
6
+ "Bash(cp \"C:/Users/zhuzh/.claude/skills/jfox-common/SKILL.md\" \"skills-recommend/claude-code/jfox-common/SKILL.md\")"
7
+ ]
8
+ }
9
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jfox-cli
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: JFox - Zettelkasten 知识管理 CLI 工具
5
5
  Project-URL: Homepage, https://github.com/zhuxixi/jfox
6
6
  Project-URL: Repository, https://github.com/zhuxixi/jfox
@@ -0,0 +1,135 @@
1
+ # Session Summary Confirmation Flow Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add user confirmation and note type selection steps to the jfox-session-summary skill before writing to the knowledge base.
6
+
7
+ **Architecture:** Insert two new steps (confirm content + select type) between summary generation and `jfox add`. The modification is purely in the skill Markdown document — no Python code changes.
8
+
9
+ **Tech Stack:** Skill document (Markdown), AskUserQuestion tool
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-04-15-session-summary-confirmation-design.md`
12
+
13
+ ---
14
+
15
+ ### Task 1: Rewrite SKILL.md workflow section
16
+
17
+ **Files:**
18
+ - Modify: `skills-recommend/claude-code/jfox-session-summary/SKILL.md`
19
+
20
+ This is the only task. The file is a single skill document — no decomposition needed.
21
+
22
+ - [ ] **Step 1: Read current SKILL.md**
23
+
24
+ Read `skills-recommend/claude-code/jfox-session-summary/SKILL.md` to confirm current content matches expectations (3-step workflow, Step 2 hardcodes `--type fleeting`).
25
+
26
+ - [ ] **Step 2: Replace Step 2 and add new Steps 2–3**
27
+
28
+ Replace the entire `### Step 2: 写入知识库` section and everything after it (through end of file) with the new 5-step workflow. The exact replacement content:
29
+
30
+ Replace the old `### Step 2` and `### Step 3` sections (lines 42–96) with:
31
+
32
+ ```markdown
33
+ ### Step 2: 用户确认
34
+
35
+ 将生成的总结用普通文本输出,供用户阅读。然后使用 `AskUserQuestion` 询问:
36
+
37
+ - 问题:`笔记内容是否 OK?`
38
+ - 选项:
39
+ - `内容没问题` → 继续 Step 3
40
+ - `需要修改` → 用户在 "Other" 中输入修改意见,根据意见调整总结后回到 Step 2 重新展示和确认
41
+
42
+ 循环直到用户满意为止。
43
+
44
+ ### Step 3: 选择笔记类型
45
+
46
+ 用户确认内容后,使用 `AskUserQuestion` 询问笔记类型:
47
+
48
+ - 问题:`选择笔记类型`
49
+ - 选项:
50
+ - `fleeting`(推荐)— 会话记录是临时性笔记,后续可提炼为 permanent
51
+ - `literature` — 如果会话有明确的参考资料来源
52
+ - `permanent` — 如果总结已经是成熟的知识
53
+
54
+ ### Step 4: 写入知识库
55
+
56
+ 使用 Step 3 选定的笔记类型执行写入:
57
+
58
+ ```bash
59
+ jfox add "<markdown-escaped-summary>" \
60
+ --title "Session: <topic>" \
61
+ --type <Step 3 选定的类型> \
62
+ --tag session \
63
+ --kb <kb-name> \
64
+ --format json
65
+ ```
66
+
67
+ **注意**:
68
+ - 标题格式统一为 `Session: <简短主题>`
69
+ - 类型使用 Step 3 的选择结果,不再硬编码 `fleeting`
70
+ - 标签统一使用 `session`
71
+ - 内容中的双引号需要转义,或使用 `--content-file` 从临时文件读取
72
+
73
+ ### Step 5: 处理长内容
74
+
75
+ 如果总结超过 500 字或包含特殊字符,优先使用 `--content-file`:
76
+
77
+ ```bash
78
+ # 写入临时文件
79
+ cat > /tmp/session-summary.md << 'EOF'
80
+ <总结内容>
81
+ EOF
82
+
83
+ # 从文件导入
84
+ jfox add --content-file /tmp/session-summary.md \
85
+ --title "Session: <topic>" \
86
+ --type <Step 3 选定的类型> \
87
+ --tag session \
88
+ --kb <kb-name> \
89
+ --format json
90
+ ```
91
+
92
+ ## 命令参考
93
+
94
+ ```bash
95
+ # 直接添加(短内容)
96
+ jfox add "<summary>" --title "Session: <topic>" --type <type> --tag session --kb <name>
97
+
98
+ # 从文件添加(长内容或含特殊字符)
99
+ jfox add --content-file <path> --title "Session: <topic>" --type <type> --tag session --kb <name>
100
+
101
+ # 验证写入
102
+ jfox show <note_id> --format json
103
+ ```
104
+
105
+ ## 错误处理
106
+
107
+ - **"Knowledge base not found"**: 提示用户先运行 `/jfox-common` 创建知识库
108
+ - **内容过长导致 shell 解析失败**: 切换到 `--content-file` 方式
109
+ - **特殊字符转义问题**: 使用单引号包裹内容,或写入临时文件
110
+ ```
111
+
112
+ - [ ] **Step 3: Verify the file reads correctly**
113
+
114
+ Read the full updated `SKILL.md` and confirm:
115
+ 1. Step 1 (生成会话总结) is unchanged
116
+ 2. Step 2 (用户确认) has AskUserQuestion with "内容没问题" and "需要修改" options, plus loop description
117
+ 3. Step 3 (选择笔记类型) has AskUserQuestion with fleeting/literature/permanent options
118
+ 4. Step 4 (写入知识库) uses `<Step 3 选定的类型>` not hardcoded `fleeting`
119
+ 5. Step 5 (处理长内容) also uses `<Step 3 选定的类型>`
120
+ 6. 命令参考 section shows `--type <type>` not `--type fleeting`
121
+ 7. No leftover hardcoded `fleeting` in any `jfox add` example (except the Step 3 option description)
122
+
123
+ - [ ] **Step 4: Commit**
124
+
125
+ ```bash
126
+ git add skills-recommend/claude-code/jfox-session-summary/SKILL.md
127
+ git commit -m "feat(skill): add user confirmation and note type selection to session-summary
128
+
129
+ Inserts Step 2 (content confirmation via AskUserQuestion) and Step 3
130
+ (note type selection) before writing to knowledge base. Users can now
131
+ review the generated summary, request modifications, and choose between
132
+ fleeting/literature/permanent note types.
133
+
134
+ Closes #154"
135
+ ```
@@ -0,0 +1,52 @@
1
+ # jfox-session-summary 用户确认流程设计
2
+
3
+ **Date**: 2026-04-15
4
+ **Issue**: #154
5
+ **Status**: Draft
6
+
7
+ ## 背景
8
+
9
+ 当前 `jfox-session-summary` skill 生成总结后直接写入知识库,硬编码 `--type fleeting`。用户无法审查内容或选择笔记类型。
10
+
11
+ ## 改动范围
12
+
13
+ 仅修改 `skills-recommend/claude-code/jfox-session-summary/SKILL.md`。不涉及 CLI 命令或 Python 代码变更。
14
+
15
+ ## 新流程
16
+
17
+ 当前 3 步扩展为 5 步:
18
+
19
+ | Step | 内容 | 状态 |
20
+ |------|------|------|
21
+ | Step 1 | 生成会话总结 | 不变 |
22
+ | Step 2 | 展示总结 + 用户确认 | **新增** |
23
+ | Step 3 | 选择笔记类型 | **新增** |
24
+ | Step 4 | `jfox add --type <选择>` | 原 Step 2 |
25
+ | Step 5 | 处理长内容 | 原 Step 3,不变 |
26
+
27
+ ### Step 2:展示总结 + 用户确认
28
+
29
+ 1. 用普通文本输出完整总结内容,供用户阅读
30
+ 2. 使用 `AskUserQuestion` 询问「笔记内容是否 OK?」
31
+ - **内容没问题** → 继续 Step 3
32
+ - **需要修改** → 用户在 Other 中输入修改意见 → 根据意见调整总结 → 回到 Step 2 重新展示和确认
33
+ 3. 循环直到用户满意为止
34
+
35
+ ### Step 3:选择笔记类型
36
+
37
+ 使用 `AskUserQuestion` 询问「选择笔记类型」:
38
+
39
+ - **fleeting**(推荐)— 会话记录是临时性笔记,后续可提炼为 permanent
40
+ - **literature** — 会话有明确的参考资料来源
41
+ - **permanent** — 总结已经是成熟的知识
42
+
43
+ ### Step 4:写入知识库
44
+
45
+ `jfox add` 的 `--type` 参数使用 Step 3 的选择结果,不再硬编码 `fleeting`。其余参数(`--title`、`--tag session`、`--kb`)不变。
46
+
47
+ ## 不在范围内
48
+
49
+ - 不修改 `jfox add` CLI 命令本身
50
+ - 不增加新的笔记类型
51
+ - 不改变总结模板格式(Step 1 不变)
52
+ - 不做自动检测笔记类型的智能逻辑
@@ -1,5 +1,5 @@
1
1
  """JFox - Zettelkasten 知识管理工具"""
2
2
 
3
- __version__ = "0.3.2"
3
+ __version__ = "0.4.0"
4
4
  __author__ = "User"
5
5
  __email__ = "user@example.com"
@@ -8,6 +8,8 @@ from datetime import datetime
8
8
  from pathlib import Path
9
9
  from typing import List, Optional
10
10
 
11
+ import yaml
12
+
11
13
  # Windows 下强制 UTF-8 输出,避免中文乱码
12
14
  if sys.platform == "win32":
13
15
  if hasattr(sys.stdout, "reconfigure"):
@@ -52,6 +54,9 @@ for _lib in (
52
54
 
53
55
  logger = logging.getLogger(__name__)
54
56
 
57
+ # config set 支持的配置项
58
+ _VALID_CONFIG_KEYS = {"device", "embedding_model", "batch_size"}
59
+
55
60
  # 创建应用
56
61
  app = typer.Typer(
57
62
  name="jfox",
@@ -569,6 +574,104 @@ def search(
569
574
  raise typer.Exit(1)
570
575
 
571
576
 
577
+ def _warn_dimension_change(new_model: str):
578
+ """切换 embedding 模型时警告用户重建索引"""
579
+ if new_model == "auto":
580
+ return # auto 模式不需要警告
581
+ try:
582
+ import chromadb
583
+
584
+ chroma_path = config.chroma_dir
585
+ if not chroma_path.exists():
586
+ return
587
+ client = chromadb.PersistentClient(path=str(chroma_path))
588
+ collection = client.get_collection("notes")
589
+ if collection.count() == 0:
590
+ return # 空索引,无需警告
591
+ except Exception:
592
+ return
593
+
594
+ # 有已有索引 + 换了模型 = 需要重建
595
+ console.print(
596
+ f"\n[yellow]⚠ 模型已更改为 {new_model}[/yellow]\n"
597
+ f"[yellow] 如果检索结果异常,请重建索引:[/yellow]\n"
598
+ f" jfox index rebuild\n"
599
+ )
600
+
601
+
602
+ def _config_set_impl(key: str, value: str):
603
+ """设置知识库配置项"""
604
+ if key not in _VALID_CONFIG_KEYS:
605
+ raise ValueError(f"不支持的配置项: {key},可选值: {', '.join(sorted(_VALID_CONFIG_KEYS))}")
606
+
607
+ config_path = config.zk_dir / "config.yaml"
608
+
609
+ # 读取现有配置
610
+ if config_path.exists():
611
+ with open(config_path, "r", encoding="utf-8") as f:
612
+ data = yaml.safe_load(f) or {}
613
+ else:
614
+ data = {}
615
+
616
+ # 类型转换
617
+ if key == "batch_size":
618
+ value = int(value)
619
+
620
+ # 写入配置
621
+ data[key] = value
622
+ config_path.parent.mkdir(parents=True, exist_ok=True)
623
+ with open(config_path, "w", encoding="utf-8") as f:
624
+ yaml.dump(data, f, allow_unicode=True, sort_keys=False)
625
+
626
+ # 同步更新内存中的 config 对象
627
+ setattr(config, key, value if key != "batch_size" else int(value))
628
+
629
+ # 重置 backend 单例,让新配置在下次使用时生效
630
+ from .embedding_backend import reset_backend
631
+
632
+ reset_backend()
633
+
634
+ console.print(f"[green]✓ {key} = {value}[/green]")
635
+
636
+ # 切换模型时检查维度
637
+ if key == "embedding_model":
638
+ _warn_dimension_change(value)
639
+
640
+
641
+ @app.command(name="config")
642
+ def config_cmd(
643
+ action: str = typer.Argument(..., help="操作: set"),
644
+ key: str = typer.Argument(None, help="配置项名称"),
645
+ value: str = typer.Argument(None, help="配置值"),
646
+ ):
647
+ """
648
+ 查看/修改知识库配置
649
+
650
+ 示例:
651
+
652
+ jfox config set device cuda
653
+ jfox config set device auto
654
+ jfox config set embedding_model BAAI/bge-m3
655
+ jfox config set embedding_model auto
656
+ """
657
+ try:
658
+ if action == "set":
659
+ if key is None:
660
+ console.print("[red]✗ 缺少配置项名称[/red]")
661
+ raise typer.Exit(1)
662
+ if value is None:
663
+ console.print("[red]✗ 缺少配置值[/red]")
664
+ raise typer.Exit(1)
665
+ _config_set_impl(key, value)
666
+ else:
667
+ console.print(f"[red]✗ 未知操作: {action}[/red]")
668
+ console.print(" 可用操作: set")
669
+ raise typer.Exit(1)
670
+ except ValueError as e:
671
+ console.print(f"[red]✗ {e}[/red]")
672
+ raise typer.Exit(1)
673
+
674
+
572
675
  def _status_impl(output_format: str, json_output: bool):
573
676
  """查看知识库状态的内部实现"""
574
677
  from .formatters import OutputFormatter
@@ -587,8 +690,9 @@ def _status_impl(output_format: str, json_output: bool):
587
690
  },
588
691
  "stats": stats,
589
692
  "backend": {
590
- "type": "CPU",
591
- "model": backend.model_name if backend.model else "not loaded",
693
+ "type": backend.resolved_device,
694
+ "model": backend.model_name or "auto (未加载)",
695
+ "dimension": backend.dimension,
592
696
  },
593
697
  }
594
698
 
@@ -612,8 +716,9 @@ def _status_impl(output_format: str, json_output: bool):
612
716
  table.add_row("Fleeting", str(stats["by_type"].get("fleeting", 0)))
613
717
  table.add_row("Literature", str(stats["by_type"].get("literature", 0)))
614
718
  table.add_row("Permanent", str(stats["by_type"].get("permanent", 0)))
615
- table.add_row("Backend", "CPU")
616
- table.add_row("Model", backend.model_name)
719
+ table.add_row("Backend", backend.resolved_device)
720
+ table.add_row("Model", backend.model_name or "auto (未加载)")
721
+ table.add_row("Dimension", str(backend.dimension))
617
722
 
618
723
  console.print(table)
619
724
  else:
@@ -2520,6 +2625,7 @@ def daemon(
2520
2625
  table.add_row("端口", str(info["port"]))
2521
2626
  table.add_row("模型", info["model"])
2522
2627
  table.add_row("维度", str(info["dimension"]))
2628
+ table.add_row("设备", info.get("device", "unknown"))
2523
2629
  console.print(table)
2524
2630
  else:
2525
2631
  console.print("[green]✓ Daemon 已启动[/green]")
@@ -2546,6 +2652,7 @@ def daemon(
2546
2652
  table.add_row("端口", str(info["port"]))
2547
2653
  table.add_row("模型", info.get("model", "unknown"))
2548
2654
  table.add_row("维度", str(info.get("dimension", "unknown")))
2655
+ table.add_row("设备", info.get("device", "unknown"))
2549
2656
  console.print(table)
2550
2657
  else:
2551
2658
  console.print("[dim]Daemon 未运行[/dim]")
@@ -28,10 +28,10 @@ class ZKConfig:
28
28
  zk_dir: Path = field(init=False)
29
29
  chroma_dir: Path = field(init=False)
30
30
 
31
- # NPU 配置
32
- embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2"
33
- embedding_dimension: int = 384
34
- device: str = "auto" # auto / npu / gpu / cpu
31
+ # Embedding 配置
32
+ embedding_model: str = "auto" # auto = 根据 device 自动选择模型
33
+ embedding_dimension: int = 0 # 0 = 动态,由模型决定
34
+ device: str = "auto" # auto / cuda / cpu
35
35
  batch_size: int = 32
36
36
 
37
37
  # 检索配置
@@ -122,13 +122,14 @@ config = get_config()
122
122
 
123
123
 
124
124
  def _reset_singletons():
125
- """重置所有缓存的单例(搜索引擎、向量存储、BM25 索引)"""
125
+ """重置所有缓存的单例(搜索引擎、向量存储、BM25 索引、embedding 后端)"""
126
126
  import importlib
127
127
 
128
128
  for module_name, fn_name in [
129
129
  (".bm25_index", "reset_bm25_index"),
130
130
  (".search_engine", "reset_search_engine"),
131
131
  (".vector_store", "reset_vector_store"),
132
+ (".embedding_backend", "reset_backend"),
132
133
  ]:
133
134
  try:
134
135
  module = importlib.import_module(module_name, package="jfox")
@@ -16,6 +16,23 @@ from typing import Optional
16
16
 
17
17
  from . import DEFAULT_HOST, DEFAULT_PORT
18
18
 
19
+
20
+ def _get_pythonw_executable() -> str:
21
+ """获取 pythonw.exe 路径(Windows 无控制台入口)
22
+
23
+ Windows 上优先使用 pythonw.exe 避免 daemon 子进程弹出控制台窗口。
24
+ 如果 pythonw.exe 不存在则回退到 sys.executable。
25
+ 非 Windows 平台直接返回 sys.executable。
26
+ """
27
+ if sys.platform != "win32":
28
+ return sys.executable
29
+
30
+ pythonw = sys.executable.replace("python.exe", "pythonw.exe")
31
+ if pythonw != sys.executable and Path(pythonw).exists():
32
+ return pythonw
33
+ return sys.executable
34
+
35
+
19
36
  logger = logging.getLogger(__name__)
20
37
 
21
38
  STARTUP_TIMEOUT = 60 # 模型加载可能较慢
@@ -107,8 +124,16 @@ def start_daemon(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> bool:
107
124
  )
108
125
  return True
109
126
 
110
- # 构建启动命令
111
- cmd = [sys.executable, "-m", "jfox.daemon.server", "--host", host, "--port", str(port)]
127
+ # 构建启动命令(Windows 使用 pythonw.exe 避免控制台窗口)
128
+ cmd = [
129
+ _get_pythonw_executable(),
130
+ "-m",
131
+ "jfox.daemon.server",
132
+ "--host",
133
+ host,
134
+ "--port",
135
+ str(port),
136
+ ]
112
137
 
113
138
  kwargs = {}
114
139
  if sys.platform == "win32":
@@ -222,5 +247,6 @@ def get_daemon_status() -> Optional[dict]:
222
247
  "port": port,
223
248
  "model": health.get("model", "unknown"),
224
249
  "dimension": health.get("dimension", 384),
250
+ "device": health.get("device", "unknown"),
225
251
  "started_at": data.get("started_at") if data else None,
226
252
  }
@@ -26,12 +26,17 @@ def _load_model():
26
26
  """启动时加载模型(标记为 daemon 进程,防止自引用)"""
27
27
  global _backend
28
28
  os.environ["JFOX_DAEMON_PROCESS"] = "1"
29
+ from ..config import config
29
30
  from ..embedding_backend import EmbeddingBackend
30
31
 
31
- _backend = EmbeddingBackend()
32
+ model_name = config.embedding_model if config.embedding_model != "auto" else None
33
+ _backend = EmbeddingBackend(device=config.device, model_name=model_name)
32
34
  try:
33
35
  _backend.load()
34
- logger.info(f"Daemon: 模型已加载 ({_backend.model_name})")
36
+ logger.info(
37
+ f"Daemon: 模型已加载 {_backend.model_name} "
38
+ f"(device={_backend._resolved_device}, dimension={_backend._resolved_dim})"
39
+ )
35
40
  except Exception as e:
36
41
  logger.error(f"Daemon: 模型加载失败,进程退出: {e}")
37
42
  os._exit(1)
@@ -46,6 +51,7 @@ class HealthResponse(BaseModel):
46
51
  status: str
47
52
  model: str
48
53
  dimension: int
54
+ device: str # 实际使用的设备
49
55
  pid: int
50
56
 
51
57
 
@@ -80,6 +86,7 @@ def health():
80
86
  status="ok",
81
87
  model=_backend.model_name,
82
88
  dimension=_backend.dimension,
89
+ device=_backend.resolved_device,
83
90
  pid=os.getpid(),
84
91
  )
85
92
 
@@ -0,0 +1,174 @@
1
+ """Embedding Backend - 支持 daemon 加速 + GPU (CUDA)"""
2
+
3
+ import logging
4
+ import os
5
+ from typing import List, Optional
6
+
7
+ import numpy as np
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # 默认模型
12
+ _GPU_DEFAULT_MODEL = "BAAI/bge-m3"
13
+ _CPU_DEFAULT_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
14
+
15
+
16
+ class EmbeddingBackend:
17
+ """嵌入模型后端(支持 daemon 代理 + GPU 加速)"""
18
+
19
+ def __init__(self, model_name: Optional[str] = None, device: str = "auto"):
20
+ self.model_name = model_name # None/"auto" 表示由 device 自动决定
21
+ self.device = device
22
+ self.model = None
23
+ self._daemon_client = None # 延迟初始化
24
+ self._use_daemon: Optional[bool] = None # None=未检测
25
+ self._resolved_device: Optional[str] = None # 实际解析后的设备
26
+ self._resolved_dim: Optional[int] = None # 实际嵌入维度
27
+
28
+ def _resolve_device(self) -> str:
29
+ """解析 device 字符串为实际设备名"""
30
+ if self.device != "auto":
31
+ return self.device
32
+ try:
33
+ import torch
34
+
35
+ if torch.cuda.is_available():
36
+ device_name = torch.cuda.get_device_name(0)
37
+ logger.info(f"检测到 CUDA 可用 ({device_name}), 使用 GPU")
38
+ return "cuda"
39
+ except Exception:
40
+ # ImportError: torch 未安装; 其他: CUDA 驱动异常等
41
+ pass
42
+ logger.info("CUDA 不可用, 使用 CPU")
43
+ return "cpu"
44
+
45
+ def _resolve_model_name(self, resolved_device: str) -> str:
46
+ """根据 device 和用户配置决定使用哪个模型"""
47
+ if self.model_name is not None and self.model_name != "auto":
48
+ return self.model_name
49
+ return _GPU_DEFAULT_MODEL if resolved_device == "cuda" else _CPU_DEFAULT_MODEL
50
+
51
+ def _check_daemon(self) -> bool:
52
+ """检测是否使用 daemon(仅检测一次,缓存结果)"""
53
+ if self._use_daemon is not None:
54
+ return self._use_daemon
55
+
56
+ # daemon 进程内不应连接自己
57
+ if os.environ.get("JFOX_DAEMON_PROCESS"):
58
+ self._use_daemon = False
59
+ return False
60
+
61
+ try:
62
+ from .daemon.process import _get_daemon_url, is_daemon_running
63
+
64
+ if is_daemon_running():
65
+ from .daemon.client import DaemonClient
66
+
67
+ url = _get_daemon_url()
68
+ client = DaemonClient(url)
69
+ if client.available:
70
+ self._daemon_client = client
71
+ self._use_daemon = True
72
+ logger.info("使用远程 embedding daemon")
73
+ return True
74
+ except Exception:
75
+ pass
76
+
77
+ self._use_daemon = False
78
+ return False
79
+
80
+ def load(self):
81
+ """加载模型(支持 device 自动检测和 GPU 加速)"""
82
+ if self.model is not None:
83
+ return
84
+
85
+ # 解析 device 和 model(即使 daemon 模式也需要,用于 status 显示)
86
+ if self._resolved_device is None:
87
+ self._resolved_device = self._resolve_device()
88
+ if self.model_name is None or self.model_name == "auto":
89
+ self.model_name = self._resolve_model_name(self._resolved_device)
90
+
91
+ if self._check_daemon():
92
+ return # daemon 已持有模型,无需本地加载
93
+
94
+ try:
95
+ from sentence_transformers import SentenceTransformer
96
+
97
+ self.model = SentenceTransformer(self.model_name, device=self._resolved_device)
98
+ self._resolved_dim = self.model.get_sentence_embedding_dimension()
99
+ logger.info(
100
+ f"模型已加载: {self.model_name} "
101
+ f"(device={self._resolved_device}, dimension={self._resolved_dim})"
102
+ )
103
+ except Exception as e:
104
+ logger.error(f"加载模型失败: {e}")
105
+ raise
106
+
107
+ def encode(self, texts: List[str], batch_size: int = 32) -> np.ndarray:
108
+ """文本编码(优先使用 daemon)"""
109
+ # 优先使用 daemon
110
+ if self._check_daemon() and self._daemon_client is not None:
111
+ try:
112
+ return self._daemon_client.encode(texts, batch_size=batch_size)
113
+ except Exception as e:
114
+ logger.warning(f"Daemon 编码失败,回退到本地: {e}")
115
+ self._use_daemon = False
116
+
117
+ if self.model is None:
118
+ self.load()
119
+
120
+ try:
121
+ return self.model.encode(
122
+ texts, batch_size=batch_size, show_progress_bar=False, convert_to_numpy=True
123
+ )
124
+ except Exception as e:
125
+ logger.error(f"编码失败: {e}")
126
+ raise
127
+
128
+ def encode_single(self, text: str) -> np.ndarray:
129
+ """单文本编码"""
130
+ return self.encode([text])[0]
131
+
132
+ @property
133
+ def dimension(self) -> int:
134
+ """返回嵌入维度(动态读取)"""
135
+ if self._resolved_dim is not None:
136
+ return self._resolved_dim
137
+ if self.model is not None:
138
+ return self.model.get_sentence_embedding_dimension()
139
+ # daemon 模式下从 daemon client 获取维度
140
+ if self._daemon_client is not None:
141
+ return self._daemon_client.dimension
142
+ # 未加载时:如果 model_name 已确定,估算维度
143
+ if self.model_name and self.model_name != "auto":
144
+ if "bge-m3" in self.model_name or "bge-large" in self.model_name:
145
+ return 1024
146
+ return 384 # 默认 MiniLM 维度
147
+
148
+ @property
149
+ def resolved_device(self) -> str:
150
+ """实际使用的设备(auto 解析后)"""
151
+ return self._resolved_device or "unknown"
152
+
153
+
154
+ # Global backend instance
155
+ _backend: Optional[EmbeddingBackend] = None
156
+
157
+
158
+ def get_backend() -> EmbeddingBackend:
159
+ """获取全局 embedding 后端实例(从 config 读取配置)"""
160
+ global _backend
161
+ if _backend is None:
162
+ from .config import config
163
+
164
+ model_name = config.embedding_model
165
+ if model_name == "auto":
166
+ model_name = None # 交给 EmbeddingBackend 根据 device 自动决定
167
+ _backend = EmbeddingBackend(model_name=model_name, device=config.device)
168
+ return _backend
169
+
170
+
171
+ def reset_backend():
172
+ """重置全局 embedding 后端(用于测试或特殊场景)"""
173
+ global _backend
174
+ _backend = None
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jfox-cli"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "JFox - Zettelkasten 知识管理 CLI 工具"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}