面向内网的自托管工具 / Skill 共享平台 —— 把团队里散落各处的脚本、工具包、Agent Skill、Prompt 模板收进一个可检索、可审批、可版本管理的门户。
一百来人的研发团队里,工具通常是怎么流转的?某个同事写了个好用的批量改配置脚本,扔进群文件;另一个人做了个 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 自动文档 + 批量导入导出 |
截图由
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 24.03 LTS SP1 ~ SP4,x86_64 或 aarch64,systemd 原生部署。
openEuler 官方源自带 python3.11 rpm,目标机不需要编译 Python,整个安装过程可以完全离线。
逐步细讲、验证清单与常见坑见 docs/13-新手部署指南。
方式 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:自己造包 —— 只有「纯内网、连下载都做不到」时才需要,就是下面这一节。
本节命令在全新克隆上实测过(命令、参数、产物名都对得上)。
构建机只需要能联网、有 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







