feat(val-blog): add 2026-04-30 dream journey post
This commit is contained in:
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# download-model.sh - 下载适合 8GB 内存的 Qwen 模型
|
||||
|
||||
MODEL_DIR="${HOME}/.llama/models"
|
||||
mkdir -p "$MODEL_DIR"
|
||||
|
||||
echo "=== 下载 Qwen 模型 (适合 8GB 内存) ==="
|
||||
echo ""
|
||||
echo "可选模型:"
|
||||
echo "1) Qwen3-1.8B-Q4_K_M (推荐, ~1.2GB, 速度最快)"
|
||||
echo "2) Qwen3-4B-Q4_K_M (~2.5GB, 质量更好)"
|
||||
echo "3) Qwen2.5-1.5B-Q4_K_M (备用, ~1GB)"
|
||||
echo ""
|
||||
|
||||
# 默认下载 1.8B 版本
|
||||
MODEL_URL="https://huggingface.co/Qwen/Qwen3-1.8B-GGUF/resolve/main/qwen3-1.8b-q4_k_m.gguf"
|
||||
MODEL_NAME="qwen3-1.8b-q4_k_m.gguf"
|
||||
|
||||
cd "$MODEL_DIR"
|
||||
|
||||
if [ -f "$MODEL_NAME" ]; then
|
||||
echo "✅ 模型已存在: $MODEL_NAME"
|
||||
else
|
||||
echo "⬇️ 正在下载 $MODEL_NAME..."
|
||||
echo "(如下载慢可用浏览器访问 https://huggingface.co/Qwen/Qwen3-1.8B-GGUF)"
|
||||
|
||||
# 使用 curl 下载,支持断点续传
|
||||
curl -L -C - -o "$MODEL_NAME" "$MODEL_URL" || {
|
||||
echo "❌ 下载失败,请手动下载到: $MODEL_DIR/$MODEL_NAME"
|
||||
echo "下载链接: $MODEL_URL"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ 模型已准备: $MODEL_DIR/$MODEL_NAME"
|
||||
echo ""
|
||||
echo "=== 下一步 ==="
|
||||
echo "启动服务: ./start-server.sh"
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# install-llama-cpp.sh - 在 Intel Mac 上安装优化版 llama.cpp
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== 安装 llama.cpp (Intel Mac 优化版) ==="
|
||||
|
||||
# 方式1: 通过 Homebrew 安装(最简单)
|
||||
if command -v brew &> /dev/null; then
|
||||
echo "通过 Homebrew 安装..."
|
||||
brew install llama.cpp
|
||||
echo "✅ Homebrew 安装完成"
|
||||
else
|
||||
echo "⚠️ 未检测到 Homebrew,建议先安装: https://brew.sh"
|
||||
echo "或使用方式2手动编译"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== 安装后验证 ==="
|
||||
which llama-cli || echo "llama-cli 未找到"
|
||||
which llama-server || echo "llama-server 未找到"
|
||||
|
||||
echo ""
|
||||
echo "=== 下一步 ==="
|
||||
echo "1. 下载模型: 运行 download-model.sh"
|
||||
echo "2. 启动服务: 运行 start-server.sh"
|
||||
Executable
+263
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FTS5 记忆索引系统
|
||||
用于全文搜索 memory/*.md 文件
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 配置
|
||||
MEMORY_DIR = Path.home() / ".openclaw" / "workspace" / "memory"
|
||||
DB_PATH = MEMORY_DIR / ".fts5" / "memory.db"
|
||||
|
||||
|
||||
def init_db():
|
||||
"""初始化 FTS5 数据库"""
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建文件表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY,
|
||||
path TEXT UNIQUE NOT NULL,
|
||||
modified_at TIMESTAMP,
|
||||
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建 FTS5 虚拟表用于全文搜索
|
||||
cursor.execute("""
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS memory_index USING fts5(
|
||||
content,
|
||||
file_id,
|
||||
chunk_index,
|
||||
tokenize='porter unicode61'
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建 chunks 表存储分块信息
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
file_id INTEGER,
|
||||
chunk_index INTEGER,
|
||||
content TEXT,
|
||||
start_line INTEGER,
|
||||
end_line INTEGER,
|
||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"✓ 数据库初始化完成: {DB_PATH}")
|
||||
|
||||
|
||||
def chunk_content(content, chunk_size=500, overlap=100):
|
||||
"""
|
||||
将内容分块,带重叠以保持上下文
|
||||
|
||||
Args:
|
||||
content: 文件内容
|
||||
chunk_size: 每块字符数
|
||||
overlap: 重叠字符数
|
||||
|
||||
Returns:
|
||||
list of (chunk_text, start_line, end_line)
|
||||
"""
|
||||
lines = content.split('\n')
|
||||
chunks = []
|
||||
|
||||
current_chunk = []
|
||||
current_size = 0
|
||||
start_line = 0
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
line_with_newline = line + '\n'
|
||||
|
||||
if current_size + len(line_with_newline) > chunk_size and current_chunk:
|
||||
# 保存当前块
|
||||
chunk_text = ''.join(current_chunk)
|
||||
chunks.append((chunk_text, start_line + 1, i))
|
||||
|
||||
# 重叠部分
|
||||
overlap_text = chunk_text[-overlap:] if len(chunk_text) > overlap else chunk_text
|
||||
current_chunk = [overlap_text]
|
||||
current_size = len(overlap_text)
|
||||
start_line = i - len(overlap_text.split('\n')) + 1
|
||||
|
||||
current_chunk.append(line_with_newline)
|
||||
current_size += len(line_with_newline)
|
||||
|
||||
# 最后一块
|
||||
if current_chunk:
|
||||
chunk_text = ''.join(current_chunk)
|
||||
chunks.append((chunk_text, start_line + 1, len(lines)))
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
def index_file(file_path, conn=None):
|
||||
"""索引单个文件"""
|
||||
close_conn = False
|
||||
if conn is None:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
close_conn = True
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 读取文件
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
print(f"✗ 读取失败 {file_path}: {e}")
|
||||
return
|
||||
|
||||
# 获取文件信息
|
||||
stat = os.stat(file_path)
|
||||
modified_at = datetime.fromtimestamp(stat.st_mtime)
|
||||
rel_path = str(Path(file_path).relative_to(MEMORY_DIR.parent))
|
||||
|
||||
# 插入或更新文件记录
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO files (path, modified_at, indexed_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
""", (rel_path, modified_at))
|
||||
|
||||
file_id = cursor.lastrowid
|
||||
|
||||
# 删除旧索引
|
||||
cursor.execute("DELETE FROM memory_index WHERE file_id = ?", (file_id,))
|
||||
cursor.execute("DELETE FROM chunks WHERE file_id = ?", (file_id,))
|
||||
|
||||
# 分块并索引
|
||||
chunks = chunk_content(content)
|
||||
|
||||
for chunk_idx, (chunk_text, start_line, end_line) in enumerate(chunks):
|
||||
# 插入 chunks 表
|
||||
cursor.execute("""
|
||||
INSERT INTO chunks (file_id, chunk_index, content, start_line, end_line)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (file_id, chunk_idx, chunk_text, start_line, end_line))
|
||||
|
||||
# 插入 FTS5 索引
|
||||
cursor.execute("""
|
||||
INSERT INTO memory_index (content, file_id, chunk_index)
|
||||
VALUES (?, ?, ?)
|
||||
""", (chunk_text, file_id, chunk_idx))
|
||||
|
||||
conn.commit()
|
||||
|
||||
if close_conn:
|
||||
conn.close()
|
||||
|
||||
print(f"✓ 已索引: {rel_path} ({len(chunks)} 块)")
|
||||
|
||||
|
||||
def index_all():
|
||||
"""索引所有 memory 文件"""
|
||||
init_db()
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
|
||||
# 查找所有 .md 文件
|
||||
md_files = list(MEMORY_DIR.glob("*.md"))
|
||||
|
||||
print(f"\n开始索引 {len(md_files)} 个文件...")
|
||||
|
||||
for file_path in md_files:
|
||||
if file_path.name.startswith('.'):
|
||||
continue
|
||||
index_file(file_path, conn)
|
||||
|
||||
conn.close()
|
||||
print(f"\n✓ 索引完成")
|
||||
|
||||
|
||||
def search(query, limit=10):
|
||||
"""
|
||||
全文搜索
|
||||
|
||||
Args:
|
||||
query: 搜索关键词
|
||||
limit: 返回结果数量
|
||||
|
||||
Returns:
|
||||
list of dict with path, snippet, rank
|
||||
"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 使用 FTS5 的 snippet 功能生成摘要
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
f.path,
|
||||
snippet(memory_index, 0, '[', ']', '...', 32) as snippet,
|
||||
c.start_line,
|
||||
c.end_line,
|
||||
rank
|
||||
FROM memory_index
|
||||
JOIN files f ON memory_index.file_id = f.id
|
||||
JOIN chunks c ON memory_index.file_id = c.file_id
|
||||
AND memory_index.chunk_index = c.chunk_index
|
||||
WHERE memory_index MATCH ?
|
||||
ORDER BY rank
|
||||
LIMIT ?
|
||||
""", (query, limit))
|
||||
|
||||
results = []
|
||||
for row in cursor.fetchall():
|
||||
results.append({
|
||||
'path': row[0],
|
||||
'snippet': row[1],
|
||||
'start_line': row[2],
|
||||
'end_line': row[3],
|
||||
'rank': row[4]
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return results
|
||||
|
||||
|
||||
def print_search_results(query, results):
|
||||
"""打印搜索结果"""
|
||||
print(f"\n🔍 搜索: '{query}'")
|
||||
print(f"找到 {len(results)} 条结果:\n")
|
||||
|
||||
for i, r in enumerate(results, 1):
|
||||
print(f"{i}. {r['path']} (行 {r['start_line']}-{r['end_line']})")
|
||||
print(f" {r['snippet']}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("用法:")
|
||||
print(f" {sys.argv[0]} index # 索引所有文件")
|
||||
print(f" {sys.argv[0]} search <query> # 搜索")
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "index":
|
||||
index_all()
|
||||
elif command == "search":
|
||||
if len(sys.argv) < 3:
|
||||
print("错误: 需要提供搜索关键词")
|
||||
sys.exit(1)
|
||||
query = sys.argv[2]
|
||||
results = search(query)
|
||||
print_search_results(query, results)
|
||||
else:
|
||||
print(f"未知命令: {command}")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,33 @@
|
||||
# OpenClaw 本地模型集成配置
|
||||
# 将此配置添加到 ~/.openclaw/openclaw.json 的 models.providers 部分
|
||||
|
||||
{
|
||||
"local": {
|
||||
"baseUrl": "http://127.0.0.1:8080/v1",
|
||||
"apiKey": "local-llm-key",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "qwen3-1.8b",
|
||||
"name": "Qwen3-1.8B (Local)",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 4096,
|
||||
"maxTokens": 2048
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# 然后修改 agents.defaults.model.fallbacks 将本地模型加入列表
|
||||
# "fallbacks": [
|
||||
# "lkeap/kimi-k2.5",
|
||||
# "local/qwen3-1.8b", <-- 添加这行
|
||||
# ...
|
||||
# ]
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# start-server.sh - 启动 llama.cpp 推理服务 (Intel Mac 8GB 内存优化)
|
||||
|
||||
MODEL_DIR="${HOME}/.llama/models"
|
||||
MODEL_NAME="qwen3-1.8b-q4_k_m.gguf"
|
||||
MODEL_PATH="$MODEL_DIR/$MODEL_NAME"
|
||||
|
||||
# 检查模型是否存在
|
||||
if [ ! -f "$MODEL_PATH" ]; then
|
||||
echo "❌ 模型未找到: $MODEL_PATH"
|
||||
echo "请先运行: ./download-model.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== 启动 llama.cpp 推理服务 ==="
|
||||
echo "模型: $MODEL_NAME"
|
||||
echo "内存优化: 针对 Intel Mac 8GB 优化"
|
||||
echo ""
|
||||
|
||||
# Intel CPU + 8GB 内存优化参数
|
||||
# -t 4: 使用4线程 (你的CPU是4核8线程,留一些给系统)
|
||||
# -c 4096: 上下文长度 4K (省内存)
|
||||
# -np 1: 单并发 (避免内存爆炸)
|
||||
# --mlock: 锁定内存防止 swap (可选,如果你的内存够)
|
||||
# --host 0.0.0.0: 允许局域网访问 (可选)
|
||||
|
||||
llama-server \
|
||||
--model "$MODEL_PATH" \
|
||||
--threads 4 \
|
||||
--ctx-size 4096 \
|
||||
--parallel 1 \
|
||||
--batch-size 512 \
|
||||
--timeout 300 \
|
||||
--host 127.0.0.1 \
|
||||
--port 8080 \
|
||||
--api-key "local-llm-key" \
|
||||
--verbose \
|
||||
"$@"
|
||||
|
||||
# 如果 llama-server 找不到,尝试 llama-cli
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "⚠️ llama-server 未找到,尝试 llama-cli 交互模式..."
|
||||
echo "(注意: llama-cli 不提供 API 服务,仅用于测试)"
|
||||
llama-cli \
|
||||
--model "$MODEL_PATH" \
|
||||
--threads 4 \
|
||||
--ctx-size 4096 \
|
||||
--interactive \
|
||||
--verbose-prompt
|
||||
fi
|
||||
Reference in New Issue
Block a user