294a0f35b1
目标是「装完就有」:docker compose up / 升级镜像重启后,官方技能直接出现在 任务模式的技能选择器里,不需要运维执行任何脚本。 落点被两件事定死: - 技能包必须在 src/backend 之内。Dockerfile 是 `COPY ./ ./`、构建上下文就是 src/backend,仓库根的 linsight-skills/ 根本不进镜像。放进 bisheng/linsight/builtin_skills/ 后,docker、裸机 rsync、pip 安装都自动带上。 - seed 不能写进 Alembic。项目铁律是 revision 只做 DDL,数据 seed 一律走独立 流程;挂在 API lifespan 上既保住了「一条命令部署」,又不进迁移链。位置紧挨 既有的两个同形态 backfill。 seed 的落点与用户上传的技能完全一致(磁盘 bundle + linsight_skill 行, source='builtin'),因此选择器、启停开关、详情页、租户隔离、 materialize_session_skills 全都不需要为内置技能加特例。 三条关键策略: - 幂等按内容:磁盘已装 bundle 与镜像内的逐字节比对,不同才重写。升级镜像重启 即更新,没变则只花几次文件读取,不引入版本号列。 - 用户改过的永不覆盖:管理端编辑内置技能会把 source 翻成 manual (SkillService._mark_forked),该副本从此退出 seed。升级时静默回滚客户的 修改,比让副本漂移糟糕得多。 - 新租户补种:启动 seed 只覆盖当时存在的租户,故 TenantService.acreate_tenant 末尾也为新租户 seed 一次;失败只 warn,不连累建租户。 并发与容错:多副本同时启动由 uq_linsight_skill_tenant_name 兜底,输的一方 INSERT 失败而赢的一方已写入同样的字节;单个 bundle 失败不影响其它;整个 seed 异常只 log,绝不阻塞启动。单租户部署(租户表为空)回落到 DEFAULT_TENANT_ID。 同时把 linsight-skills/README.md 提升为 docs/linsight-skill-authoring.md (技能编写指南 + 内置技能分发机制),打包脚本仍保留,供客户定制类技能走手工 导入。 test/linsight/test_builtin_skill_seeder.py 13 条:发现与校验(目录名须等于 frontmatter name、缺 SKILL.md、忽略 __pycache__)、首次创建、内容未变不重写、 内容变更刷新、已分叉不覆盖、多租户各一份、单租户回落、单包失败不影响其它、 租户上下文不泄漏,以及一条守卫仓库内实际 bundle 合法性的用例。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>