Files
LineWalker 294a0f35b1 feat(linsight): 内置技能随部署自带,启动时按内容幂等 seed
目标是「装完就有」: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>
2026-08-11 15:55:52 +08:00
..