28f9a54ba9
把 Obsidian vault 通过 MCP 暴露给 Claude.ai,OAuth 走 nas-auth。 设计文档见 vault Coding/obsidian-mcp/obsidian-mcp 设计.md。 代码层落地参考 vault Coding/obsidian-mcp/MCP 实现指南.md。 V1+V2 同时实现(用户要求跳过分阶段直接全部): 读 Tools(需 scope=read:obsidian): - list_vault_tree(一次性 vault 地图,限制深度) - list_files / read_file(含 offset/limit 大文件分页) - search(子串匹配 + glob 过滤,最多 50 hits) - get_metadata(size / modified_at / has_frontmatter) 写 Tools(需 scope=write:obsidian): - write_file / append_file - 多重门禁:scope 校验 + 路径黑名单 + 写入白名单 + 永禁文件 - 永禁写:任意目录的 AGENTS.md / PROFILE.md / README.md / CLAUDE.md / 01-Secret/** - 白名单:02-ShengquGames/logs/ + Coding/ + NAS/NAS 待办清单.md - 写入审计日志按天 rotate(JSON line) 安全: - VaultPathResolver chroot:path traversal + symlink 双拒绝 - JwtBearer (HS256, Current+Previous fallback, MapInboundClaims=false) - aud=obsidian, iss=https://auth.zhengchentao.win - 黑名单:01-Secret / .obsidian / .trash / .git 技术栈: - .NET 10 + ModelContextProtocol SDK 1.0 - Streamable HTTP transport (POST /mcp) - JwtBearer 10.0 + IdentityModel.Tokens 8.x 部署: - Dockerfile multi-stage,runtime 装 ripgrep(V3 备用),non-root user - .gitea/workflows/build-image.yml:build + deploy 双 job,buildkit v0.13.2 - 容器内 :8080,宿主端口 9090 - 子域名 obs.zhengchentao.win - vault 挂载 /volume1/docker/webdav/data/Zhengchen:/vault:rw(V2 写入需要 rw) 测试:35/35 单测过(VaultPathResolver path traversal/blacklist/symlink + VaultWriteGuard whitelist/forbidden) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
132 lines
4.5 KiB
YAML
132 lines
4.5 KiB
YAML
name: Build Docker Image
|
||
|
||
on:
|
||
# 自动触发:push 到 main 分支(纯文档改动跳过)
|
||
# ⚠️ quirk:git commit --allow-empty 不触发 paths-ignore 过滤的 workflow,
|
||
# 要强制重新触发必须改一个非 ignore 路径的真实文件(改 build-image.yml 自己最稳)
|
||
push:
|
||
branches: [main]
|
||
paths-ignore:
|
||
- '**.md'
|
||
- 'LICENSE'
|
||
- '.gitignore'
|
||
- '.dockerignore'
|
||
# 手动触发:应急通道(重新打包 / 指定自定义 tag)
|
||
workflow_dispatch:
|
||
inputs:
|
||
branch:
|
||
description: '要打包的分支(仅手动触发生效)'
|
||
required: true
|
||
default: 'main'
|
||
tag:
|
||
description: '镜像 tag(留空则用 commit short hash)'
|
||
required: false
|
||
default: ''
|
||
|
||
# 同分支连续 push 只跑最新的 run,旧 in-progress run 被取消(build + deploy 一起停)
|
||
concurrency:
|
||
group: ${{ github.workflow }}-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
build:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout target branch
|
||
uses: actions/checkout@v4
|
||
with:
|
||
ref: ${{ inputs.branch || github.ref_name }}
|
||
fetch-depth: 0
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@v3
|
||
with:
|
||
# 钉 v0.13.2(runc 1.1.x)避免 DSM 4.4.x 内核不支持 runc 1.2+ 的
|
||
# openat2/fsmount syscall 导致 build 失败
|
||
driver-opts: |
|
||
image=moby/buildkit:v0.13.2
|
||
|
||
- name: Login to Gitea Container Registry
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: git.zhengchentao.win
|
||
username: ${{ gitea.actor }}
|
||
password: ${{ secrets.PACKAGES_TOKEN }}
|
||
|
||
- name: Determine image tag and revision
|
||
id: meta
|
||
run: |
|
||
if [ -n "${{ inputs.tag }}" ]; then
|
||
IMAGE_TAG="${{ inputs.tag }}"
|
||
else
|
||
IMAGE_TAG="$(git rev-parse --short HEAD)"
|
||
fi
|
||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||
echo "full_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||
echo "==> Image tag: $IMAGE_TAG"
|
||
|
||
- name: Build and push
|
||
uses: docker/build-push-action@v5
|
||
with:
|
||
context: .
|
||
push: true
|
||
labels: |
|
||
org.opencontainers.image.source=https://git.zhengchentao.win/zhengchen.tao/obsidian-mcp
|
||
org.opencontainers.image.revision=${{ steps.meta.outputs.full_sha }}
|
||
tags: |
|
||
git.zhengchentao.win/zhengchen.tao/obsidian-mcp:${{ steps.meta.outputs.image_tag }}
|
||
git.zhengchentao.win/zhengchen.tao/obsidian-mcp:latest
|
||
|
||
- name: Build summary
|
||
if: always()
|
||
run: |
|
||
{
|
||
echo "## Build Summary"
|
||
echo ""
|
||
echo "| 项 | 值 |"
|
||
echo "|---|---|"
|
||
echo "| 触发方式 | \`${{ github.event_name }}\` |"
|
||
echo "| 源分支 | \`${{ inputs.branch || github.ref_name }}\` |"
|
||
echo "| 源 commit (full) | \`${{ steps.meta.outputs.full_sha }}\` |"
|
||
echo "| 源 commit (short) | \`${{ steps.meta.outputs.image_tag }}\` |"
|
||
echo "| 镜像 | \`git.zhengchentao.win/zhengchen.tao/obsidian-mcp:${{ steps.meta.outputs.image_tag }}\` + \`:latest\` |"
|
||
} >> "$GITHUB_STEP_SUMMARY"
|
||
|
||
deploy:
|
||
# needs: build 串起来 —— build 失败则 deploy 自动 skip,无需 if 条件
|
||
needs: build
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
# deploy job 跑在独立 runner 容器上,凭据不从 build job 继承,必须再登一次
|
||
- name: Login to Gitea Container Registry
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: git.zhengchentao.win
|
||
username: ${{ gitea.actor }}
|
||
password: ${{ secrets.PACKAGES_TOKEN }}
|
||
|
||
- name: Pull and restart obsidian-mcp
|
||
env:
|
||
NAS_INFRA_TOKEN: ${{ secrets.NAS_INFRA_TOKEN }}
|
||
run: |
|
||
set -e
|
||
|
||
TMPDIR=$(mktemp -d)
|
||
trap 'rm -rf "$TMPDIR"' EXIT
|
||
|
||
if [ -n "$NAS_INFRA_TOKEN" ]; then
|
||
CLONE_URL="https://x-access-token:${NAS_INFRA_TOKEN}@git.zhengchentao.win/dev/nas-infra.git"
|
||
else
|
||
CLONE_URL="https://git.zhengchentao.win/dev/nas-infra.git"
|
||
fi
|
||
|
||
git clone --depth 1 "$CLONE_URL" "$TMPDIR/nas-infra"
|
||
cd "$TMPDIR/nas-infra/obsidian-mcp"
|
||
|
||
docker compose pull
|
||
docker compose up -d
|
||
|
||
sleep 3
|
||
docker compose ps
|
||
docker compose logs --tail=30 obsidian-mcp
|