Linkora · 灵桥

灵桥 (Linkora) — 开发指南

环境要求

组件 最低版本
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)

平台 CLI 工具

根据目标 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 是灵桥的唯一配置入口。从 config.yaml.example 复制后,按需修改以下关键段落:

platforms(多平台隔离)

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(轮询器)

    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(大语言模型)

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

embedding:
  enabled: true                         # 默认 false;开启后 RAG 检索生效
  model: "BAAI/bge-small-zh-v1.5"      # BGE 中文向量模型(local 模式自动下载缓存)
  provider: local
  offline: true                         # 强制离线,禁止联网下载

rules(规则引擎)

rules:
  enabled: true
  blacklist:                            # 黑名单中的用户/群组消息不处理
    users: []
    groups: []
  keywords:                             # 关键词规则(DB 管理,热更新)
    - match: "在吗"
      reply: "在的,请问有什么事?"
  intent_filter:                       # 意图过滤:跳过无业务价值的社交消息
    enabled: true

logging

logging:
  level: DEBUG
  file: ./logs/linkora.log
  max_size_mb: 50
  max_backups: 5

web

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 层阻塞调用)。


代码风格约定

Type Hints

强制使用类型注解:

from __future__ import annotations

def get_store(db_path: str | None = None) -> SQLiteStore:
    ...

Docstring

def init_schema(conn: sqlite3.Connection, db_path: str) -> None:
    """初始化/迁移数据库 schema。

    该函数幂等:所有 DDL 使用 IF NOT EXISTS 或前置列检查。
    """

Repository 模式

新增数据访问必须遵循 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 ...")
        ...

会话数据隔离(conv_conn 铁律)

数据库按「主库 / 按平台会话库」双层隔离,读写位置弄错会导致数据不可见或跨平台串台(曾导致会话图片回填永不生效、测试出现全空/串台假阳性):

铁律:

配置热重载

配置修改后无需重启,灵桥自动检测 config.yaml 的 mtime 变化并重新加载。开发时注意:

日志

import logging
logger = logging.getLogger(__name__)

logger.info("message")
logger.debug("detail")

CHANGELOG 维护与发版

日常:随改动写入「未发布」段

docs/CHANGELOG.md 顶部常驻一个 ## 未发布 (YYYY-MM-DD) 段。对外有影响的变更必须随修复/特性一起写进该段,不要等发版时补记——历史上曾连续数个 bugfix 漏记,等到发版时已无从追溯。

判定标准:

发版:未发布段转正

  1. 把 ## 未发布 (YYYY-MM-DD) 标题改成 ## vX.Y.Z (YYYY-MM-DD),日期填发版当天。
  2. 若本次发版没有对外变更,直接删掉空的未发布段。
  3. 在 pyproject.toml 中 bump version。
  4. 跑 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   # 无输出 = 合规