Skip to content

About

内网工具 / Skill 共享平台:上传 → 审批 → 发布 → 下载,systemd 原生部署,x86_64/aarch64 离线安装

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

localcraft

面向内网的自托管工具 / Skill 共享平台 —— 把团队里散落各处的脚本、工具包、Agent Skill、Prompt 模板收进一个可检索、可审批、可版本管理的门户。

CI License: MIT Python 3.11+ React 18


这是什么

一百来人的研发团队里,工具通常是怎么流转的?某个同事写了个好用的批量改配置脚本,扔进群文件;另一个人做了个 SQL 优化 Prompt,发在聊天记录里;再有人把几个可执行文件打包丢到共享盘。三个月后没人记得哪个是最新版、谁维护、能不能对外用。

localcraft 把这件事收敛成一个内网站点:

  • 门户大屏 —— 卡片墙 + 分类侧栏 + 全文检索,按分类和标签归档整齐
  • 个人主页 —— 授权用户上传自己的工具,填简介详情、传封面图、发新版本
  • 审批闭环 —— 管理员审批上架;可切「一键全放行」,也可只放行白名单用户
  • 管理 API —— 签发长效 Token,脚本可以一键批量导入导出、管理用户与权限

它从设计之初就是给离线内网用的:不依赖任何外部服务,不需要容器,一次装好就长期跑着。

核心特性

能力 说明
四种工具形态 文件包(zip / tar.gz / whl / exe)、Agent Skill 包(zip 内含 SKILL.md,服务端解压出文件树并渲染预览)、在线网页工具(内网 URL 直跳)、Prompt 模板(纯文本在线预览)
免登录浏览 门户首页与公开工具详情默认免登录可看(portal.allow_anonymous_view,管理员可关);下载、上传、审批等仍需登录 —— 未登录只可能看到 public
三级可见性 public 全员 / restricted 指定用户或用户组 / private 仅自己;超管可开关式查看 private 内容
四角色 RBAC 超级管理员 / 审批管理员 / 普通用户 / 只读访客;管理员建号,无自助注册,首次登录强制改密
版本管理 SemVer 字符串手填(不强制格式);同一工具可反复发新版本,历史版本最多保留 10 份,超出按最旧淘汰
审批流 全局开关「需审批 / 全放行」+ 用户白名单免审;驳回必填理由,用户可改后重提;完整审批历史可查;支持下架与重新上架
检索 SQLite FTS5 全文索引(切 PostgreSQL 后走 tsvector),支持分类、标签、形态、可见性多维筛选
统计 下载量与浏览量走内存聚合 + 定时批量落库,不做逐请求自增写入
管理 API 长效 API Token(st_ 前缀,SHA256 落库,可吊销、可限 Scope)+ OpenAPI 自动文档 + 批量导入导出

界面

工具门户
工具门户 —— 卡片墙 + 分类侧栏 + 全文检索,26 个演示工具
工具详情
工具详情 —— 版本时间线、Skill 包文件树、下载票据
我的工具
个人主页 —— 上传私有工具 / Skill,查看审批状态与历史版本
审批台
审批台 —— 全局开关「需审批 / 全放行」+ 用户白名单免审
用户与权限
用户与权限 —— 四角色 RBAC、用户组、账号锁定与强制改密
批量导入导出
批量导入导出 —— 管理 API 的可视化对应,也可纯脚本调用
暗色模式
暗色模式 —— 亮/暗/跟随系统三态;文本对比度复测浅色与深色各 0/812 不达标
登录页
登录 —— 无自助注册,管理员建号,首次登录强制改密

截图由 web/scripts/capture-screenshots.mjs 在真实后端 + 真实种子上生成, 可随时重跑:先 PORT=8099 bash preview.sh,再 cd web && PORT=8099 node scripts/capture-screenshots.mjs。 脚本会对每张图做自检(正文长度、卡片数、暗色主题是否生效),避免提交空白页或错误页。

技术栈

后端 Python 3.11 · FastAPI · SQLAlchemy 2.0(async)· Alembic · Pydantic v2 · uvicorn 前端 React 18 · Vite 5 · TypeScript(strict)· Tailwind CSS v4 · shadcn/ui · TanStack Query v5 · react-router-dom 6 数据库 SQLite(WAL)为默认;通过 SQLAlchemy + Alembic 抽象,切 PostgreSQL 只改连接串 部署 systemd 原生部署 · nginx 反向代理 · 不使用 Docker / Podman / 任何容器方案 架构 x86_64 与 aarch64 双架构,各交付一套离线 wheelhouse

架构

                        ┌──────────────────────────────┐
   浏览器 ──────────────▶│  nginx                       │
                        │  TLS 终止 / 静态资源 / 限流    │
                        └───────────────┬──────────────┘
                                        │  proxy_pass
                        ┌───────────────▼──────────────┐
                        │  uvicorn(单 worker)         │
                        │  ├─ /api/v1/*   REST 接口     │
                        │  ├─ /docs       OpenAPI       │
                        │  └─ /*          前端 SPA 产物  │
                        └───────────────┬──────────────┘
                                        │
              ┌─────────────────────────┼─────────────────────────┐
              ▼                         ▼                         ▼
    ┌──────────────────┐   ┌──────────────────────┐   ┌──────────────────┐
    │ SQLite (WAL)     │   │ /var/lib/localcraft  │   │ 内存计数聚合       │
    │ 21 张表 + FTS5   │   │ 上传文件 / 封面图     │   │ 定时批量落库       │
    │ 可换 PostgreSQL  │   │ 版本归档 / 备份       │   │ tool_stats_daily │
    └──────────────────┘   └──────────────────────┘   └──────────────────┘

为什么是单 worker:SQLite 在 WAL 下仍是单写者。生产刻意只跑一个 uvicorn worker,靠 asyncio 扛 I/O 并发 —— 加 worker 不会提升写吞吐,只会加剧锁竞争。真要横向扩,先换 PostgreSQL 再谈。

快速开始

本机一键预览(推荐先试这个)

需要 Python 3.11+ 和 Node.js 18+:

git clone https://github.com/jacob-bytes/localcraft.git
cd localcraft

# 建 venv、装依赖、建库、播种演示数据、构建前端、起服务 —— 全部自动
bash preview.sh

跑完打开 http://127.0.0.1:8000。首次会自动播种 26 个演示工具 / 6 个账号(含 8 个边界样本:无封面、超长名称、空文件树等)。

不用先登录:门户首页默认免登录可看,下载时才需要登录(右上角有入口)。

bash preview.sh --reset     # 丢弃预览数据重新来
PORT=8090 bash preview.sh   # 换端口

预览数据落在 .preview/(已 gitignore),不碰 backend/var/。预览是生产拓扑:后端直接托管前端构建产物,单一 URL,没有独立前端进程也没有代理。

演示账号(全部为虚构数据):

角色 账号 密码 用途
超级管理员 admin Admin@12345 全部页面
审批员 wangwu Author@12345 user + approver,验证审批台与角色边界
普通用户 zhangsan / lisi Author@12345 上传与个人主页
只读访客 viewer Viewer@12345 能看详情但下载被拒
需改密 newbie Newbie@12345 登录后被强制跳转改密页

分别起前后端(开发模式)

# 终端 1 —— 后端
cd backend
python3.11 -m venv .venv && .venv/bin/pip install -r requirements.lock
.venv/bin/python -m alembic upgrade head       # 建库 + 迁移
.venv/bin/python -m app.cli seed-demo          # 播种演示数据
.venv/bin/python -m uvicorn app.main:app --reload --port 8000

# 终端 2 —— 前端(Vite 代理 /api 到 8000)
cd web
npm install
npm run dev                                    # http://127.0.0.1:5173

开发模式下前端默认启用 MSW mock(VITE_ENABLE_MOCKS=true),不连后端也能点通全部页面。

部署到 openEuler

目标环境:openEuler 24.03 LTS SP1 ~ SP4,x86_64 或 aarch64,systemd 原生部署。 openEuler 官方源自带 python3.11 rpm,目标机不需要编译 Python,整个安装过程可以完全离线。

逐步细讲、验证清单与常见坑见 docs/13-新手部署指南。

拿到发布包(两条路,先用方式 1)

方式 1(推荐):下载 GitHub 上构建好的包 —— 不需要构建机,也不需要 Node / Python

打 tag 时 GitHub Actions 会自动构建发布包并挂到 Releases: 里面已经含编好的前端产物(web/dist/,91 个文件)与 x86_64 / aarch64 两套 wheelhouse。 在一台联网机上:

# 文件名以 Releases 页上**显示的实际名字**为准(资产名含构建日期,从版本号推不出来)
BASE=https://github.com/jacob-bytes/localcraft/releases/download/v1.2.0
F=localcraft-1.2.0-offline-20260929.tar.gz
curl -fSLO "$BASE/$F" && curl -fSLO "$BASE/$F.sha256"
sha256sum -c "$F.sha256"          # 必须输出 OK

方式 2:自己造包 —— 只有「纯内网、连下载都做不到」时才需要,就是下面这一节。

方式 2:在构建机上产出发布包

本节命令在全新克隆上实测过(命令、参数、产物名都对得上)。

构建机只需要能联网、有 python3.11 与 Node(不必是 openEuler,macOS / 普通 Linux 都行):

cd web && npm ci && npm run build && cd ..   # 1) 必须先构建前端,make-release 需要 web/dist
bash scripts/build-wheelhouse.sh all          # 2) 两套离线 wheel(也可写 x86_64 / aarch64)
bash scripts/make-release.sh 1.0.0            # 3) 产出 backend/dist/localcraft-1.0.0-offline-<日期>.tar.gz

注意三个容易踩的点:

  • build-wheelhouse.sh 的参数是位置参数,写成 --arch x86_64 会报用法错误。
  • 忘了第 1 步时 make-release.sh 会明确报错并告诉你怎么办,不会产出一个残缺的包。
  • 发布包两个架构的 wheelhouse 都带(这样同一个包能在两种机器上装),所以体积约 84 MB。

在目标机上安装

# 把 tar 包传到目标机(文件名用你实际拿到的那个:方式 1 下载的 / 方式 2 产出的,同名同结构)
scp localcraft-1.2.0-offline-20260929.tar.gz root@目标机:/tmp/
# 目标机上(root)
cd /tmp && tar xzf localcraft-1.2.0-offline-20260929.tar.gz && cd localcraft-1.2.0
sudo ./install.sh

不需要传 LOCALCRAFT_WHEELHOUSE —— install.sh 会按本机架构自己挑 wheelhouse-x86_64 / wheelhouse-aarch64,并校验其中的 MANIFEST.sha256。 (传一个不存在的路径会让它静默退化成在线安装,离线环境下报错很难懂 —— 别画蛇添足。)

install.sh 会依次完成十步:环境自检 → 校验发布包 → 建 localcraft 系统用户与目录 → 建代码软链 → 建 venv → 离线装依赖 → 写环境变量文件 → 跑 Alembic 迁移 → 装 systemd unit / slice / tmpfiles / logrotate → 启动并做健康检查。

★ 但 install.sh 跑完只是「服务起来了」,此刻你还登不进去。 它末尾会打印剩余步骤,这里先给全 (第 1 步不做就没有账号;第 2 步不做就永远不会备份、探活也不会跑):

# 1) 建超级管理员(密码走交互输入,不要用明文参数 —— 会进 shell 历史)
sudo -u localcraft bash -c '
  set -a; . /etc/localcraft/localcraft.env; set +a
  cd /opt/localcraft/app/current
  /opt/localcraft/venv/bin/python -m app.cli create-superadmin --username admin
'

# 2) 启用两个定时器(install.sh 只 enable 主服务,这两个刻意留给你决定)
systemctl enable --now localcraft-backup.timer       # 每日 03:00 备份
systemctl enable --now localcraft-maintenance.timer  # 每小时维护(在线工具探活在其中)

# 3) 要让**别的机器**访问:默认只听 127.0.0.1
sed -i 's/^LOCALCRAFT_HOST=.*/LOCALCRAFT_HOST=0.0.0.0/' /etc/localcraft/localcraft.env
systemctl restart localcraft.service
firewall-cmd --add-port=8000/tcp --permanent && firewall-cmd --reload

逐项解释、验证命令与「为什么」见 docs/13-新手部署指南 的第 3~6 步; 备份/探活的运维细节见 docs/08-运维手册。

不需要 nginx:后端自己托管前端产物并下发安全响应头,IP:PORT 直连是一等部署方式。 要用 nginx 反代(TLS / 限流)也能,见 docs/13 与 deploy/nginx-*.conf。

只想先在本机看看?

不必走上面这一套:

git clone https://github.com/jacob-bytes/localcraft.git && cd localcraft
bash preview.sh          # 建 venv、装依赖、建库、播种、构建前端、起服务,全自动

配套脚本

install.sh / uninstall.sh|backup.sh / restore.sh / verify-backup.sh(一致性备份与恢复)| upgrade.sh / rollback.sh(升级与回滚)|security-check.sh / disk-alert.sh / metrics-snapshot.sh

配置

所有配置走环境变量,生产放 /etc/localcraft/localcraft.env(0640 root:localcraft)。完整示例见 deploy/localcraft.env.example,逐项说明见 docs/05 §14.2。

变量 默认 说明
LOCALCRAFT_HOST / LOCALCRAFT_PORT 127.0.0.1 / 8000 监听地址。nginx 反代保持回环;直连 IP:PORT 必须改 0.0.0.0
LOCALCRAFT_WEB_MODE nginx nginx / direct。应用不读,只决定安装自检检查哪些前置条件
DATABASE_URL sqlite+aiosqlite:///…/localcraft.db 换 PostgreSQL 时改这里(注意绝对路径要四个斜杠)
DATA_DIR ./var 数据根目录;files/、backups/ 都由它派生
SECRET_KEY —— 必改。openssl rand -hex 32 生成
ACCESS_TOKEN_MINUTES / REFRESH_TOKEN_DAYS 30 / 7 访问令牌与刷新令牌有效期
COOKIE_SECURE false 上 TLS 时置 true;直连明文 HTTP 保持 false,否则刷新 cookie 不会被带上
API_DOCS_ENABLED false 生产默认关闭 /docs
portal.allow_anonymous_view true 是否允许未登录浏览门户。在数据库 system_settings 里,不是环境变量
LOGIN_MAX_FAILURES / LOCKOUT_MINUTES 5 / 15 登录失败锁定策略
DB_BUSY_TIMEOUT 5000 SQLite 忙等待毫秒数
DB_POOL_SIZE / DB_MAX_OVERFLOW 5 / 5 连接池;仅 PostgreSQL 生效
LOCALCRAFT_LOG_LEVEL / LOG_LEVEL info / INFO 前者给 uvicorn 的 --log-level,后者是应用自身的 JSON 日志级别 —— 两个都要设

环境变量名不做统一前缀:只有 5 个字段带 LOCALCRAFT_(host / port / debug / version / allow_insecure_secret),其余沿用直觉命名(DATABASE_URL、SECRET_KEY…)。其中 localcraft_allow_insecure_secret 是 v1.2.2 的逃生阀:本地开发 / CI 需要占位 SECRET_KEY 时显式放行,生产不要设。 写错名字会被静默忽略,表现为「改了配置没反应」。

配额、版本保留数、上传白名单这类运行期可调的设置不在环境变量里,而在数据库 system_settings 表(管理员界面或管理 API 修改)。完整名单见 docs/05 §14.2 末两张表。

项目结构

localcraft/
├── backend/                 FastAPI 应用
│   ├── app/
│   │   ├── api/v1/          路由层(79 路径 / 99 操作)
│   │   ├── core/            配置、安全、依赖注入、日志
│   │   ├── models/          21 张表的 ORM 模型(+ FTS5 虚拟表)
│   │   ├── repositories/    数据访问
│   │   ├── services/        业务逻辑
│   │   └── cli.py           命令行(create-superadmin / seed-demo / maintenance / …)
│   ├── migrations/          Alembic 迁移(0001~0010)
│   ├── tests/               676 个测试
│   └── openapi.json         OpenAPI 快照 —— 接口形状的**权威**,测试守卫其时效性
├── web/                     React SPA
│   └── src/{pages,components,api,lib}
├── deploy/                  systemd unit / slice / tmpfiles / nginx / logrotate
├── scripts/                 安装、备份、升级、演练、发布等 26 个运维脚本
├── docs/                    需求、数据模型、API、前端、部署、手册共 14 份
├── contracts/               接口契约与验收脚本
├── prompts/                 每个开发里程碑的任务书
└── preview.sh               本机一键预览

文档

docs/ 里的 14 份文档是这个项目从需求澄清到交付的完整轨迹,全部保留:

文档 内容
00-关键决策 立项阶段逐条确认的 34 项决策(D1–D34),以及明确不做的事——设计与实现的最高上游依据
01-需求规格说明书 角色权限、编号功能需求、业务规则与状态机、非功能需求、验收标准
02-数据模型设计 ER 关系、21 张表的字段级设计、索引、约束、迁移策略
03-API接口清单 REST 接口全集、请求/响应示例、错误码、鉴权与 Scope 矩阵
04-前端页面与交互清单 路由表、每页组件拆解、shadcn/ui 选型、空态与异常态
05-部署与运维方案 离线双架构打包、systemd、nginx、备份恢复、升级回滚、故障排查
06-用户手册 / 07-管理员手册 / 08-运维手册 面向三类使用者的操作手册
09-勘误与已知限制 文档与实现不一致处的勘误表(优先级高于它所修正的文档)
10-交付前置检查清单 交付检查项与真实状态,含未完成项
11-优化点分析 性能剖析与优化结论
12-前端UI-UX审查 可访问性与视觉审查,含审查者自身误报的复盘
13-新手部署指南 第一次部署从这里开始:两条路径的逐步命令、验证清单、常见坑,以及「哪些实测过、哪些没有」的诚实边界

contracts/CONTRACT.md 是并行开发时的唯一事实来源,记录了接口冻结、边界裁定与交付台账。

测试与质量

# 后端:lint + 676 个测试 / 约 90% 覆盖率(截至 v1.2.2)
cd backend && .venv/bin/python -m ruff check . && .venv/bin/python -m pytest

# 前端:typecheck + lint + 组件约束 + 构建产物检查
cd web && npm run verify

# 端到端:MSW mock(默认)或真实后端
cd web && npm run e2e              # mock
cd web && npm run e2e:real         # 需要 preview.sh 已在跑

# 交付前自动化检查(36 项)
bash contracts/acceptance/pre-delivery-checks.sh

几条刻意为之的工程约束:

  • backend/openapi.json 被纳入版本控制,并有测试守卫它必须与实现同步 —— 它是接口形状的权威,前端类型从这个文件生成。
  • 前端有 check:api-types / check:primitives / check:dist / check:lazy-retry 四道结构检查,防止手写类型漂移、绕过设计原语、mock 代码混进生产产物、以及懒加载 chunk 失效导致白屏。
  • ruff 的 target-version = "py311" 是硬防线:本机可以用更高版本 Python 开发,但语法必须停在 3.11,否则部署到 openEuler 会直接 SyntaxError。
  • 图片用签名 URL(?sig= HMAC,7 天 TTL),因为 <img> 带不了 Authorization 头;下载用 60 秒一次性 ticket 并绑定用户。

已知限制

诚实地说清楚哪些验过、哪些没验过:

项 状态
后端 676 测试 / 约 90% 覆盖率(截至 v1.2.2) ✅ 通过
前端 npm run verify、mock E2E、真实后端 E2E ✅ 通过
双架构 wheelhouse 文件级校验(各 58 wheel + MANIFEST.sha256、0 sdist、平台标签匹配) ✅ 通过
aarch64 真机运行 ❌ 未验证 —— 本机无 aarch64 设备,文件级检查不等于架构验证
真实 systemd 上的 unit / slice / tmpfiles ❌ 未验证 —— 本机无 systemd,安装演练走的是 LOCALCRAFT_SKIP_SYSTEMD=1 路径
用发布包的 linux wheelhouse 真正启动服务 ❌ 未验证 —— macOS 无法加载 ELF
PostgreSQL 迁移复跑 ✅ 已在 CI 的 backend-postgres(UTC + Asia/Shanghai)复跑
生产级性能数据 ⚠️ 仅本机数据,非目标硬件

已知的文档—实现不一致集中在 docs/09-勘误与已知限制.md(locate 类环境变量名、SHA256SUMS vs MANIFEST.sha256 等)。

参与贡献

见 CONTRIBUTING.md。简言之:开 issue 说明场景,改动请带上测试,backend/ 与 web/ 各自跑通对应验证命令再提 PR。

许可证

MIT © 2026 jacob-bytes

About

内网工具 / Skill 共享平台:上传 → 审批 → 发布 → 下载,systemd 原生部署,x86_64/aarch64 离线安装

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages