| 组件 | 最低版本 |
|---|---|
| Python | 3.14+(仅 3.14 系列) |
| pip | 23.0+ |
| Git | 2.30+ |
| 操作系统 | macOS 13+ / Linux (Ubuntu 20.04+ / Debian 11+) / Windows |
| 磁盘空间 | 至少 5 GB(含模型下载与数据库) |
| 内存 | 至少 4 GB(BGE 模型加载约需 2 GB) |
根据目标 IM 平台安装对应的命令行工具:
| 平台 | CLI 工具 | 安装方式 |
|---|---|---|
| 钉钉 | dws |
curl -fsSL https://dtalkapp.sjtu.edu.cn:443/dwscript/install.sh \| bash |
| 飞书 | lark-cli |
联系飞书管理员获取 |
| 企业微信 | wecom-cli |
联系企业微信管理员获取 |
登录(首次):钉钉 dws auth login;飞书 lark-cli login;企业微信 wecom-cli 扫码登录。三平台安装/登录口径一致:装好对应 CLI 并登录后,在 platforms[].adapter.cli_path 指向该 CLI 即可。
# 1. 克隆仓库
git clone <repo-url> linkora
cd linkora
# 2. 创建虚拟环境
python3 -m venv .venv
source .venv/bin/activate # macOS/Linux
# 3. 安装依赖
pip install -r requirements.txt
# 4. 配置环境变量(可选,LLM 密钥敏感信息推荐用 .env)
cp .env.example .env
# 编辑 .env,填入 LLM_API_KEY 等密钥
# 5. 初始化配置文件
cp config.yaml.example config.yaml
# 编辑 config.yaml,至少配置 platforms[0].adapter.cli_path 和 llm.* 段
依赖全部精确钉版(==),以保证 CI、Docker 与 PyInstaller 打包三处装到同一套版本。
四处声明各司其职,必须保持一致:
| 文件 | 角色 | 谁消费 |
|---|---|---|
requirements.txt |
直接依赖唯一真源(32 条,全 ==) |
Dockerfile、Dockerfile.build |
pyproject.toml [project].dependencies |
逐条镜像 requirements.txt |
打包元数据、uv |
requirements.lock |
完整传递闭包(112 条,跨平台标记) | CI 安装与 pip-audit 扫描 |
uv.lock |
uv 工作流锁文件 |
uv sync / uv run |
核心依赖分组见 requirements.txt 内注释(配置 / LLM / Web / 向量检索与 OCR / 文档解析 / 系统工具)。
前端资源(若修改 web/static)需重新构建:npm run build:frontend;前端单元测试用 npm run test:frontend(vitest)。
Python 下限为 3.14(仅 3.14 系列),
numpy==2.5.2等钉版依赖仅提供 cp314 wheel。 放宽requires-python前请先确认所有钉版依赖都提供对应 wheel。
# 1. 改 requirements.txt(新增/升级,必须写 == 精确版本)
# 2. 同步 pyproject.toml 的 [project].dependencies(逐条镜像)
# 3. 重新生成两个锁文件(需要 uv >= 0.11)
bash scripts/lock_deps.sh
# 4. 校验四处声明一致(CI lint 阶段会跑同一个脚本,不一致直接红)
python scripts/check_deps.py
scripts/check_deps.py 是纯标准库、不联网的离线门禁,会断言:
直接依赖全部钉版、pyproject.toml 与 requirements.txt 逐条一致、
两个锁文件都覆盖全部直接依赖且版本相同、requires-python 与 CI 矩阵 / Docker
基础镜像的 Python 版本相容(加 --strict 可把版本相容告警升级为失败)。
首次运行时,sentence-transformers 会自动从 HuggingFace 下载 BGE 模型(约 1.3 GB),请确保网络通畅。
config.yaml 是灵桥的唯一配置入口。从 config.yaml.example 复制后,按需修改以下关键段落:
platforms:
- id: dingtalk # 平台唯一标识
display_name: 钉钉 # 管理台展示名称
enabled: true # 是否启用
adapter_type: dingtalk # 适配器类型:dingtalk / feishu / wecom
storage:
type: sqlite
path: ./data/linkora.db # 数据库文件路径
backup_enabled: true # 是否启用自动备份
backup_dir: ./data/backups
backup_interval_hours: 24
backup_max_count: 7
backup_on_start: true
decisions_retention_days: 14
messages_retention_days: 90
poller:
interval_seconds: 10 # 轮询间隔
unread_conversation_count: 50
messages_per_conversation: 16
merge_window_seconds: 5 # 同会话消息合并窗口
history_window: 6 # LLM 上下文历史条数
reply_cooldown_seconds: 10 # 回复冷却
max_concurrent_replies: 4 # 并发回复上限
llm:
api_key: "" # 主服务商 API Key(推荐通过 .env/LLM_API_KEY 设置)
base_url: "https://kenari.id/v1"
model: "your-model-name"
fallback_api_key: "" # 备用服务商(触发 429/超时后自动切换)
fallback_base_url: "https://integrate.api.nvidia.com/v1"
fallback_model: "deepseek-ai/deepseek-v4-flash"
embedding:
enabled: true # 默认 false;开启后 RAG 检索生效
model: "BAAI/bge-small-zh-v1.5" # BGE 中文向量模型(local 模式自动下载缓存)
provider: local
offline: true # 强制离线,禁止联网下载
rules:
enabled: true
blacklist: # 黑名单中的用户/群组消息不处理
users: []
groups: []
keywords: # 关键词规则(DB 管理,热更新)
- match: "在吗"
reply: "在的,请问有什么事?"
intent_filter: # 意图过滤:跳过无业务价值的社交消息
enabled: true
logging:
level: DEBUG
file: ./logs/linkora.log
max_size_mb: 50
max_backups: 5
web:
port: 8080
host: "127.0.0.1" # 安全默认仅本机回环;公网暴露须经反代+认证
auth_enabled: true # 默认开启;auth_password 为空且开启会启动报错(fail-closed)
auth_username: admin
auth_password: "" # 通过 .env/WEB_AUTH_PASSWORD 或 config.yaml 显式设置
完整配置项说明参见 config.yaml.example(762 行,含详细注释)。
# 基础启动(仅 worker,不启动 Web 管理台)
python main.py --mode worker
# 启动 Web 管理台(不跟端口默认 8000)
python main.py --mode web
# 同时启动 Web + worker(默认模式)
python main.py --mode both
# 指定 Web 端口
python main.py --mode web --web 8000
# 指定配置文件路径
python main.py --config custom_config.yaml --mode web --web 8000
# 开发模式(开启调试日志等)
python main.py --mode web --web 8000 --dev
# 规则测试模式(不启动服务,仅测试规则命中)
python main.py --test-rule "今天天气怎么样"
| 参数 | 说明 | 示例 |
|---|---|---|
--mode MODE |
运行模式:both / web / worker(默认 both) |
--mode web |
--web [PORT] |
启动 Web 管理台,可选指定端口(不跟端口默认 8000) | --web 8000 |
--data-dir DIR |
覆盖数据目录(DB / 备份 / 模型缓存等可写路径) | --data-dir /var/lib/linkora |
--config PATH |
指定配置文件路径(等效于位置参数) | --config custom_config.yaml |
--test-rule TEXT |
规则测试模式 | --test-rule "你好" |
--dev |
开发模式 | --dev |
[config_path] |
配置文件路径(位置参数) | custom_config.yaml |
# 基础运行
pytest
# 带覆盖率报告
pytest --cov=src --cov-report=term-missing
# 并行运行(需安装 pytest-xdist)
pytest -n auto
# 按文件名
pytest tests/test_rule_engine.py
# 按关键词
pytest -k "blacklist"
# 按标记
pytest -m "integration"
tests/
├── conftest.py # 共享 fixtures(store、config mock 等)
├── test_rule_engine.py # 规则引擎(黑名单/冷却/关键词/意图)
├── test_poller.py # 轮询器核心逻辑
├── test_sqlite_store.py # 数据库 CRUD
├── test_skill_loader.py # 技能加载
├── test_skill_router.py # 技能路由
├── test_tool_wrapper.py # SkillTool 自动包装
├── test_tool_routing.py # 工具路由
├── test_tools_*.py # 各内置工具专项测试
├── test_decision_tracker.py # 决策追踪
├── test_metrics.py # 可观测性指标
├── test_web_api_endpoints.py # Web API 端点
├── test_integration_pipeline.py # 集成测试(端到端管线)
├── test_im_adapter.py # IM 适配器接口
├── test_feishu_adapter.py # 飞书适配器
├── test_wecom_adapter.py # 企微适配器
├── test_llm_*.py # LLM 客户端/限流/异常
├── test_rag_*.py # RAG 检索/注入/门控
├── test_embedding.py # Embedding 客户端
├── test_vector_index.py # FAISS 索引
└── ...(共 241 个测试文件,tests/ 下)
所有测试配置了 60 秒超时(pyproject.toml 中 --timeout=60),防止网络/IO 卡死拖垮流水线。超时方法为 signal(可中断 C 层阻塞调用)。
强制使用类型注解:
from __future__ import annotations
def get_store(db_path: str | None = None) -> SQLiteStore:
...
from __future__ import annotations 启用延迟求值TYPE_CHECKING 避免循环导入def init_schema(conn: sqlite3.Connection, db_path: str) -> None:
"""初始化/迁移数据库 schema。
该函数幂等:所有 DDL 使用 IF NOT EXISTS 或前置列检查。
"""
新增数据访问必须遵循 Repository 模式:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from src.memory.sqlite_store import SQLiteStore
class NewDomainRepo:
"""Repository for new_domain operations."""
def __init__(self, store: "SQLiteStore") -> None:
self.store = store
def do_something(self, ...) -> ...:
cur = self.store.conn.cursor()
cur.execute("SELECT ...")
...
SQLiteStore 实例self.store.conn 获取线程级连接SQLiteStore 主类数据库按「主库 / 按平台会话库」双层隔离,读写位置弄错会导致数据不可见或跨平台串台(曾导致会话图片回填永不生效、测试出现全空/串台假阳性):
self.store.conn:平台无关元数据、知识库(kb_*)、路由质量(routing_quality)、成本统计等。self.store.conv_conn(platform):消息(messages)、会话摘要(conversation_summaries)、去重(dedup_messages)、黑名单(blocked_conversations)等按平台隔离的会话数据。铁律:
messages / conversation_summaries / dedup_messages / blocked_conversations 等会话数据,必须走 store.conv_conn(platform)(经对应 *_repo 的 _cc() / _cc_for(platform)),严禁误用主库 store.conn。platform_scope() 取当前平台再定位会话库;缺省平台按 dingtalk 回落。配置修改后无需重启,灵桥自动检测 config.yaml 的 mtime 变化并重新加载。开发时注意:
src/config.py 的 Pydantic 模型中声明Field(default=...) 中定义.env 环境变量注入,不在 config.yaml 中明文存放import logging
logger = logging.getLogger(__name__)
logger.info("message")
logger.debug("detail")
INFO,开发调试可临时改为 DEBUGdocs/CHANGELOG.md 顶部常驻一个 ## 未发布 (YYYY-MM-DD) 段。对外有影响的变更必须随修复/特性一起写进该段,不要等发版时补记——历史上曾连续数个 bugfix 漏记,等到发版时已无从追溯。
判定标准:
## 未发布 (YYYY-MM-DD) 标题改成 ## vX.Y.Z (YYYY-MM-DD),日期填发版当天。pyproject.toml 中 bump version。bash scripts/release.sh(建议先加 --dry-run 预览 Release notes 再实发)。scripts/release.sh 按 ## vX.Y.Z 抽取该段作为 GitHub Release notes,找不到对应版本段会直接报错中止(fail-closed)。所以「忘记把未发布段转正」只会让发版跑不起来,不会静默丢内容。
改完 CHANGELOG 后,其余站点大多自动同步,不要手工改生成物:
| 落点 | 是否需手工改 | 说明 |
|---|---|---|
docs/ 项目 Pages 站 |
❌ 自动 | Jekyll 从 .md 生成;docs/index.html 链接 ./CHANGELOG.html,改 md 即生效 |
| github.io 主站 | ✅ 仅发版时 | 独立仓 0x0021.github.io,posts/ 下放的是 linkora-vXXX-release.html 版本文章;日常 bugfix 不建页 |
| 项目站 | ❌ 自动 | 同 docs/ Pages 站 |
⚠️
docs/是 Jekyll 源,文件内容不得出现 Liquid 定界符(连续两个左花括号,或左花括号紧跟百分号),否则会被 Jekyll 当作模板解析、导致 Pages 构建失败。下面这条自查命令用字符类书写,因此它本身不会被自己匹配出来:grep -rEn '[{][{]|[{]%' docs/*.md # 无输出 = 合规