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.
- jfox_cli-0.4.0/.claude/settings.local.json +9 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/PKG-INFO +1 -1
- jfox_cli-0.4.0/docs/superpowers/plans/2026-04-15-session-summary-confirmation.md +135 -0
- jfox_cli-0.4.0/docs/superpowers/specs/2026-04-15-session-summary-confirmation-design.md +52 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/__init__.py +1 -1
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/cli.py +111 -4
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/config.py +6 -5
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/process.py +28 -2
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/server.py +9 -2
- jfox_cli-0.4.0/jfox/embedding_backend.py +174 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/pyproject.toml +1 -1
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-common/SKILL.md +1 -1
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-session-summary/SKILL.md +32 -9
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/conftest.py +11 -1
- jfox_cli-0.4.0/tests/test_config_set_unit.py +78 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_config_unit.py +4 -4
- jfox_cli-0.4.0/tests/test_embedding_device.py +107 -0
- jfox_cli-0.4.0/tests/unit/test_daemon_process.py +86 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/uv.lock +1 -1
- jfox_cli-0.3.2/jfox/embedding_backend.py +0 -112
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.githooks/pre-push +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.github/workflows/integration-test.yml +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.github/workflows/publish.yml +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.gitignore +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/.python-version +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/AGENTS.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/CHANGELOG.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/CLAUDE.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/DEVELOPMENT_PLAN.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/README.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/SESSION.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/SESSION_SUMMARY.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/installation.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-11-bulk-import-bm25-fix.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-11-edit-command.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-11-unify-format-option.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-edit-content-file.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md +0 -0
- {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
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-index-kb-param.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-lazy-import-perf.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-12-skill-redesign.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/plans/2026-04-14-sync-docs-daemon-show.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/specs/2026-04-03-bugfixes-design.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/superpowers/specs/2026-04-14-show-command-design.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/docs/troubleshooting.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jessica-jones-static-cable.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/__main__.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/bm25_index.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/__init__.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/__main__.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/daemon/client.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/formatters.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/git_extractor.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/global_config.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/graph.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/indexer.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/kb_manager.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/models.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/note.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/performance.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/search_engine.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/template.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/template_cli.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/jfox/vector_store.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/pytest.ini +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/run_full_test.ps1 +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/README.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-ingest/SKILL.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-organize/SKILL.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/skills-recommend/claude-code/jfox-search/SKILL.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/COVERAGE_PLAN.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/MIGRATION.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/TESTS.md +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/integration/__init__.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/integration/test_backlinks.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/performance/__init__.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/performance/test_performance.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_advanced_features.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_cli_format.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_core_workflow.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_hybrid_search.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_integration.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_kb_current.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/test_suggest_links.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/__init__.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_bm25_batch.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_edit.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_format_unify.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_formatters.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_git_extractor.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_global_config.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_index_kb_param.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_indexer_clear_before_rebuild.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_indexer_verify.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_kb_manager.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_lazy_import.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_logging_config.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_show.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_template.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_template_cli.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/unit/test_vector_store_clear.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/__init__.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/assertions.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/jfox_cli.py +0 -0
- {jfox_cli-0.3.2 → jfox_cli-0.4.0}/tests/utils/note_generator.py +0 -0
- {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
|
+
}
|
|
@@ -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
|
+
- 不做自动检测笔记类型的智能逻辑
|
|
@@ -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":
|
|
591
|
-
"model": backend.model_name
|
|
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",
|
|
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
|
-
#
|
|
32
|
-
embedding_model: str = "
|
|
33
|
-
embedding_dimension: int =
|
|
34
|
-
device: str = "auto" # auto /
|
|
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 = [
|
|
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
|
-
|
|
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(
|
|
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
|