用途:登记 Hub/Server 扩展点、消费者、上游改动成本和已知缺口。 适用版本:CloudFile
14.0.0-cf.0,基于 Seafile CE 14 源码重构。 状态:当前技术清单;各项按正文标记区分已验证、已实现待验与未实现。 边界:CE 保留原生调用链;CloudFile 新增注册与终判接缝;SeaSearch、Metadata Server 等外部实现仅通过契约接入。
CloudFile 的基础扩展能力(基线 dev 提供的全部机制),以及每一个扩展点被
哪些后续特性依赖。配套:扩展能力矩阵(特性状态)、
BRANCHES.md(当前分支规则)、BRANCHING.md(分支模型)。
这份文档回答两个问题:
- 一个新特性要落地,需要基线提供什么?(查下面的关联矩阵)
- 基线里的某个扩展点能不能删/改?(查谁依赖它)
判断一个扩展点是否"到位"的唯一标准:依赖它的特性能否做到零新增上游改动。 只要某个特性还得去改上游文件,那就是扩展点没铺好,正确做法是先给基线补扩展点, 而不是在能力分支上改上游。
这个区别决定了新能力该用哪个注册函数,写错会导致语义错误而不只是风格问题。
| 形状 | 语义 | 注册 | 冲突处理 |
|---|---|---|---|
| 链(chain) | 每个注册者都会跑。问的是"有谁想参与?" | register_*() |
多个能力可共存,按注册顺序串联 |
| 实现(provider) | 同一件事的可互换实现,同时只有一个生效 | register_provider(kind, name, ...) |
由 CF_PROVIDER_<KIND> 选中,重名直接拒绝 |
为什么 search 是 provider 而不是链:一次查询只能有一个后端给出结果集, 两个后端各返回一半没有意义。meilisearch 是 search 的一种实现,不是 search 本身—— seasearch、Elasticsearch、企业自有检索服务都可以是同一个 kind 下的另一个名字。
为什么索引器(search_indexer)反而是链:同一份文档流可以同时喂给全文索引和 审计留痕,互不冲突。
provider 未选中时一律回落到原生 CE 行为;选中了一个没人注册的名字则显式失败,
不静默回落——把 CF_PROVIDER_SEARCH=meilisearh(拼错)当成"用 Elasticsearch"是
更坏的结果。
| 扩展点 | 形状 | 上游调用点 | 基线行为 |
|---|---|---|---|
register_urls |
链 | seahub/utils/rooturl.py |
无路由 |
register_menu |
链 | 前端读 /api/v2.1/cloudfile/features/ |
无菜单项 |
register_permission_check |
链 | seahub/views/__init__.py 的 check_folder_permission(覆盖 255 处调用点) |
原样返回权限 |
register_search_provider |
provider | seahub/search/utils.py 的 search_files + seahub/utils/__init__.py 的 HAS_FILE_SEARCH |
未选中 → Elasticsearch 原生路径 |
register_search_indexer |
链 | register_periodic_task 自驱动 |
无索引器 |
register_file_op_hook |
链 | 钩子永不触发 | |
register_external_source_provider |
按类型 keyed | 无上游调用点(刻意):经自有路由暴露,阶段 3 再影子原生端点,见缺口 3 | 无外部源 |
register_periodic_task |
链 | cf_worker 管理命令(自有进程) |
无任务时为空转;cf-worker 自 2026-09-22 起进默认栈 |
register_provider(kind, …) |
provider | 由声明该 kind 的一方分发 | 未选中 → 原生行为 |
通用机制:
| 机制 | 位置 | 用途 |
|---|---|---|
| 功能开关 | features.py,10 个 CF_ENABLE_* |
默认全关;未知开关名直接抛异常而非回落 false |
| provider 选择 | providers.py,CF_PROVIDER_<KIND> |
设置名由 kind 推导,新增 kind 不需要改基线 |
| 结构化过滤词汇 | search_query.py |
让元数据簇与检索簇能各自独立开发验收,见第六节 |
| 身份解析 | identity.py |
Seafile 14 之后身份 ≠ 邮箱,每个存或比用户名的能力都撞上这一条。放基线而不是放某个能力里:ACL 存规则主体、SSO 把目录成员变成组成员,用的是同一个答案,留在 acl/ 会让 SSO 在运行时 import ACL、并让 ACL 的开关决定 SSO 能不能解析用户 |
| 外部服务回调 | external_service.py,CF_SERVICE_<NAME>_* |
超时 / JWT 签名 / 重试 / fail-closed 或 fail-open |
cf_* 数据层 |
db_router.py + scripts/sql/*/cloudfile.sql |
表落在 seafile-db,因为 seaf-server(C) 与 Go fileserver 只连 ccnet-db / seafile-db |
| 注册密封 | registry.seal() |
启动后再注册直接报错,避免"半装上"的状态 |
| 扩展点 | 上游调用点 | 基线行为 |
|---|---|---|
CfPermFunc |
rpc-service.c 的 check_permission_by_path |
透传原生权限 |
CfDirentFilterFunc |
rpc-service.c 的目录列举 RPC 出口 |
不过滤 |
CfRestrictedFunc |
cf_find_restricted_path RPC → 同步 / 打包下载 |
返回 NULL(子树全可达) |
CfFileOpPrepareFunc |
repo-op.c 全部 19 个写入口;Go 写入口经 cf_fileop_prepare RPC |
放行 |
CfFileOpCommittedFunc |
同上,提交成功后 | 不发事件 |
CfFileOpAbortedFunc |
同上,PREPARE 通过但操作失败后 | 不发事件 |
上面三个是读侧,下面三个是写侧(common/cf-fileop.{c,h},规格见
fileop-lifecycle.md)。两组的形状不同:读侧的
CfPermFunc 是串联(每个 provider 拿到上一个的结果继续收紧),写侧的
CfFileOpPrepareFunc 是一票否决(第一个拒绝就终止,后面的不再执行)。
差别是有意的——权限是一个可以逐步收紧的值,而"这次写入能不能发生"是个布尔,
让第二个 provider 观察一次不会发生的写入只会制造副作用。
规则:扩展只能收紧,不能放宽。 两端各自穷举整个权限格验证过。 写侧对应的规则是只能拒绝,不能改写:provider 拿到的上下文是只读的, 否则 provider 的注册顺序就会决定结果,而那个顺序不是任何人设计过的。
● 依赖 ○ 可选用 空 = 不涉及
簇列即分支归属——同一簇的特性共享表/规格,必须同一条分支。 簇的划分依据见 BRANCHES.md 第一之二节。
| 簇 | 特性 | 开关 | urls | menu | perm_check | search_provider | indexer | file_op | ext_source | periodic | provider(kind) | cf_* 表 | server 钩子 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A | 目录 ACL | DIR_ACL |
● | ● | ● | ● | ● 规则来源 | ● | ● 全三个 | ||||
| B | SSO 登录 | SSO |
|||||||||||
| B | 组织映射 | SSO |
● | ● | ● | ● sso_directory |
● | ||||||
| C | 操作日志 / 审计 | AUDIT |
● | ● | ○ | ○ | ○ | ○ | Server repo-update → Activity |
||||
| D | 文件属性 | METADATA |
● | ● | ● | ● | ● | ● | |||||
| D | 标签 | TAGS |
● | ● | ● | ● | ● | ||||||
| D | 移动/重命名元数据跟随 | METADATA |
● | ● | ● | ● | |||||||
| E | 检索后端 | SEARCH |
● | ● | ● | ● | ● search |
● | |||||
| D×E | 组合检索 | SEARCH |
● | ● | ○ | ● | |||||||
| F | OnlyOffice | ONLYOFFICE;编辑依赖 FILE_LOCK |
● | ● | ○ | ● | ○ 编辑写回 | ||||||
| F | 文件锁基础 | FILE_LOCK(规划新增) |
● | ● | ● | ● | ● | ● 自有锁真值 | ● C/Go/WebDAV 终判 | ||||
| F | 签入签出 | CHECKOUT |
● | ● | ● | ● | ● | ● | ● | ||||
| F | iTeam 流程接口 | CHECKOUT |
● | ● | ○ | ● | |||||||
| G | 外部源(v1 只读本地目录) | EXTERNAL_SOURCES |
● | ● | ● | ○ | ○ | ● | ● | ● 源类型 | ● | ||
| G | 外部源增量扫描 | EXTERNAL_SOURCES |
● | ● | ● | ● | |||||||
| G | 虚拟目录挂载 | EXTERNAL_SOURCES |
● | ● | ● | ● | ● | ○ | |||||
| G×D | Overlay 标签属性 | EXTERNAL_SOURCES |
● | ● | ● | ● | |||||||
| H | S3 / 多存储 | S3_STORAGE |
🔴 见缺口 4:核心文件服务缺 S3 驱动 |
两行标了跨簇(D×E 组合检索、G×D Overlay),它们是仅有的两处会把两条
分支绑死的特性。处理方式:前者经第六节的结构化过滤契约解耦(已实现),
后者待 D 落地后再定归属。
簇 B 拆成两行是探针的结果,不是排版。SSO 登录整行为空——CE 14.0 自带
OAuth2/SAML/CAS/LDAP 且无 Pro 门控,打开它只是往配置块里写标量,一个扩展点
都不需要。真正用到扩展点的是组织映射,上游对通用目录没有。
当前结论见 Authentik 与企业认证;探针过程已归档。
它同时是"先查上游再登记扩展点"的一个实例:按原计划,这一行会占掉一个
provider(认证后端),而那个扩展点根本不需要存在。
读法:periodic 列被 9 个特性依赖——cf-worker 是仅次于 permission_check
的第二关键投资。file_op 列被 8 个特性依赖,这是第二关键的扩展点;它现在有
生产者了(server 侧的写入生命周期,见缺口 1),但矩阵里这一列指的仍是 Hub
的 register_file_op_hook,那个钩子依旧只补 HTTP 上下文、没有上游触发点。
需要文件事实的特性应当消费 server 侧的 COMMITTED。
按影响范围排。每一条都写明"谁被卡住"和"补法"。
原文(已作废):「
register_file_op_hook()可以注册,但上游没有任何地方 调用run_file_op_hooks()。Hub 改的 5 个上游文件里,没有一个会触发它。」
Server 侧已按 P0.5 补上 common/cf-fileop.{c,h}:PREPARE(一票否决)、
COMMITTED(成功一次的不可变事实)、ABORTED(尽力而为)。规格
fileop-lifecycle.md,共享用例集
fileop-cases.json。
覆盖面:
- C:
server/repo-op.c的 19 个写入口,含批量删除、跨库复制/移动的异步 分支、以及SEAF_ERR_CONCURRENT_UPLOAD重试循环(重试不会让事实翻倍)。 - Go:
fileserver/cf_fileop.go经 RPC 问 C,接进上传、更新、分块提交、 裸块上传、逐级建目录和同步分支更新。不做第二份判断。 - WebDAV:不重复校验——seafdav 的写全部走
seafile_api.*→ RPC →repo-op.c,C 的 seam 天然覆盖它。再写一份 Python 校验只会得到第二个真值。 只补patches/seafdav/0002的状态码翻译,把 C 的CF_ERR_FILE_LOCKED拒绝 从上游默认的 500 翻成 423 Locked。 - Hub:
register_file_op_hook保持原状,只补 HTTP 上下文,不作事实主路径。
代价:上游改动 33 → 35(server/repo-op.c、fileserver/fileop.go)。
为什么不放在已经登记过的 rpc-service.c——见 fileop-lifecycle.md 第五节。
整机门禁已补齐但还没跑过:verify-local.sh cap fileop、
两阶段矩阵 tests/e2e/fileop_matrix.py,以及它们要的假 provider
common/cf-fileop-test.c(默认关闭,刻意不进 CF_ENABLE_* 清单)。
单元级证据是 159 项 C 用例、6 项 Go 契约测试、50 个调用点的类型检查、
11 个变异全部被捕获;这些都不能证明运行时真的在每个入口被调用到。
所以这条缺口标 🟡 而不是 ✅——门禁写好了不等于门禁跑过了,而这正是 ACL
那轮的形状:acl_matrix.py 存在很久却没有任何东西调用它,第一次真跑起来就
抓到了第 71 项那个发布过两次的缺陷。
现有 CfRestrictedFunc 的路径规范化与子树包含语义已被复用(下沉为
common/cf-path.c,ACL 侧留薄转发)。没有把锁冲突注册成 ACL restricted
结果:那个 RPC 还服务同步和打包下载,混用会把只读访问误判为不可达。
即便按缺口 1 的建议下沉到 server 侧,"谁的 IP、用哪个客户端"这类信息只有 Hub 有。 需要决定:是接受审计记录缺少这些字段,还是在 Hub 侧补一个轻量的请求上下文透传。 建议第一版接受缺失,等有明确合规要求再补。
原文(已作废):「要让它出现在原生文件浏览器的库列表中,需要改上游的库 列举逻辑。代价为 0 是因为另起了一个入口。」
这条写在 search 能力之前,结论是错的。
seahub/utils/rooturl.py 把 cloudfile_ext.urls 前置到 Seahub 自己的
patterns 之前,注释原文是「CloudFile patterns come first so an extension can
shadow a native endpoint when it has to」。第 40 项已经用这个机制覆盖了两个 Pro
门控端点(cloudfile_ext/search/views.py 的影子子类),零上游改动。
所以真实取舍不是「两套界面 vs 改上游」,而是「两套界面 vs 影子约 8 个只读 端点」——当前能力边界见 外部资料源与虚拟目录挂载(v1 只读本地目录)。
但真正的硬边界不是库列表,是数据面,而它原先根本没被计价:
# seahub/api2/endpoints/file.py
token = seafile_api.get_fileserver_access_token(repo_id, obj_id, 'download', user)obj_id 是内容寻址的 fs 对象 ID。外部源文件没有 obj_id、没有 block,这条链
从第一个参数就断了。所以文件内容必须由 Hub 自己吐出(已实现:
cloudfile_ext/external_sources/apis.py),而桌面同步、WebDAV、目录打包下载
对外部源永久不可用——它们全部以 commit/fs/block 表达。这不是"首版限制"。
已决定:先自有入口,再叠影子层。阶段 1 的核心(表、provider 契约、路径包含、 授权、读 API)与呈现方式无关,两种呈现共用同一套核心,所以这个产品决定推迟的 成本是零。
完整核对见 多存储与 S3。Go fileserver 已实现 S3 与按库路由并经 MinIO 对象读写验证;C 主服务生命周期仍只有 FS:
common/obj-store.c:28写死obj_backend_fs_new,只有obj-backend-fs.c+ 遗留riak——没有 S3。C 侧的 seaf-server / GC(server/gc/gc-core.c)/ FSCK(fsck.c)都经这个obj_store。- Go fileserver 的
backend_s3.go已覆盖read/write/exists/stat,newBackend()可选 FS/S3/multiple,RepoStorageId可路由到 FS+S3;尚缺真实 MariaDB 的服务级路由 E2E。 - seafobj(Python)已带 S3/OSS/Swift/Ceph,但那只是 Python 读侧(seahub 缩略图、 seafevents 索引读对象),不在核心写入/服务路径上。
所以要补的是C 核心生命周期的存储驱动:obj-backend-s3.c + C 侧后端选择与多存储
storage_id 路由,覆盖上传/下载/同步/历史/GC/FSCK/迁移。
接口本身干净(Go 四方法、C 照 fs 后端形状),但这是实现驱动,不是登记一行——
是 roadmap 里最重的构建项之一。
源码复核结果:
include/seafile-rpc.h只有seafile_mark_file_locked/seafile_mark_file_unlocked声明;common/rpc-service.c没有实现,server/seaf-server.c和lib/rpc_table.py没有注册。python/seaserv/api.py的check_file_lock()注释明确写着 CE 不支持锁并恒返回 0;lock_file、unlock_file、get_lock_info不存在。- Go fileserver 没有锁逻辑,
lib/dirent.vala的is_locked也没有 C 侧生产者。 - MySQL/MariaDB 虽有
FileLocksDDL,但没有代码读写,不能把“有表”当成锁基础设施。
因此 #44 不是解除 Hub 的 Pro 门控,而是新增 C lock manager、RPC 实现/注册和
Vala/searpc/Python 绑定,再经缺口 1 覆盖 C、Go、WebDAV 的每条写路径。权威数据必须是
CloudFile 自有 cf_lock_lease(repo_id, normalized_path, generation, lease_until, …);
generation 每次获取都生成独立 UUID。FileLocks.id 不作 fencing:项目固定的
MariaDB 11.4 已持久化 InnoDB 自增计数器,普通重启复用的反馈不适用于本部署;但兼容
迁移目标表的主键仍不是安全协议,不能让 fencing 依赖其备份恢复、重建/重置和迁移行为。
桌面客户端探针已经证明它不读服务端 FileLocks,而是消费 locked-files HTTP、
lock/unlock HTTP、通知并维护本地 filelocks.db。因此 CE 运行期不双写 FileLocks;
该表只作 CE→Pro 停写迁移目标。租约终判只读 cf_lock_lease.lease_until,轮询 revision
来自自有 cf_lock_repo_revision。OnlyOffice 依赖 CF_ENABLE_FILE_LOCK 基础设施,
不依赖 CF_ENABLE_CHECKOUT 产品流程。
Pro 冲突隔离:内部 RPC 必须命名为 cf_lock_*,Hub 经 LockBackend adapter
选择 CE 或 Pro,不能猴子补丁 seafile_api.lock_file,也不能同时注册两套 backend。
公开 REST/fileserver 协议保持 Pro 兼容,但数据表和内部符号隔离。进程检测到
CF_LOCK_BACKEND 与版本不匹配、重复 RPC 或双 provider 时必须启动失败。
开源桌面客户端还揭示了一个部署陷阱:它只在 server property is_pro=true 时轮询
/repo/locked-files、自动锁 Office 文件和处理 file-lock-changed 通知。CE 不能为此
全局伪装 Pro,否则会连带启用目录权限等其他 Pro 假设;应新增 file-lock-v1
capability 并发布 CloudFile 客户端补丁。未修改客户端只能得到服务端写入保护,不能承诺
锁图标或本地只读状态。
行为基线必须对齐 Pro:默认锁期限 12 小时,保留 expire=0/正数/负数冻结、特殊 owner
OnlineOffice、四态 check_file_lock、虚拟库映射、目录锁字段、每库 revision 和通知。
父目录同库移动/重命名默认允许并原子迁移后代锁,父目录删除允许并撤销后代锁;原方案
默认 strict 与官方 Pro 语义冲突,已改为显式增强选项。完整矩阵见
文件协作与本地应用。
需求:目录权限要能从本地 DB 配置取,也要能回调外部服务取。
诱人的做法是"每次判权限就问一次客户的权限系统"。不要这么做:
check_folder_permission在 Hub 被调用 255 次;seaf-server 每个 RPC 都要判一次。 在这条路径上加一次网络往返,一个慢接口就能拖垮整台服务器。- 更糟的是它会把 HTTP 客户端塞进 C 的终判路径。那一层不应该做网络调用—— 超时、重试、TLS、连接池,每一样都是在最不该出问题的地方引入新的失败模式。
- 外部服务不可达时怎么办?fail-open 是安全漏洞,fail-closed 是全员被锁在门外。 这个两难本身就说明位置放错了。
┌──────────────┐ 拉取(cf-worker 周期任务) ┌─────────────┐
│ 外部权限系统 │ ←──────────────────────────── │ external- │
│ (LDAP/OA/ │ 推送(webhook → 自有路由) │ service │ ← provider
│ 自研) │ ────────────────────────────→ │ provider │
└──────────────┘ └──────┬──────┘
│ 写入
┌──────────────┐ ▼
│ 管理员在 Web │ ─────────────────────────────→ ┌─────────────┐
│ 上配置 │ local-db provider │ cf_dir_acl │
└──────────────┘ └──────┬──────┘
│ 只读
┌─────────────────────────────┼─────────────────┐
▼ ▼ ▼
Hub 求解器(Python) seaf-server(C) Go fileserver
友好错误 / UI **终判** 同步前子树校验
选择由 CF_PROVIDER_ACL_RULE_SOURCE 决定:
| provider | 规则来自 | 写入方式 |
|---|---|---|
local-db |
库主 / 管理员在 Web 上配置 | REST API 直接写 cf_dir_acl |
external-service |
客户已有的权限系统 | cf-worker 周期拉取 + webhook 推送,写同一张表 |
这个设计的四个好处:
- 求解器、C 侧、Go 侧一行都不用改。 它们只知道读表,不知道规则从哪来。
- 判权限的延迟不变,外部服务再慢也影响不到在线请求。
- 外部服务挂了,权限仍然按最后一次同步的规则执行——既不放开也不锁死, 而且这个行为是可解释、可监控的(同步时间戳可见)。
- 复用了已经铺好的基线:
register_periodic_task(cf-worker)、register_urls(webhook 入口)、cf_dir_acl表、external_service.py, 四样全是现成的,新增上游改动为 0。
代价是最终一致:外部系统改了权限,到 CloudFile 生效有同步延迟。 webhook 推送可以把延迟压到秒级;纯周期拉取则取决于间隔。这个取舍必须写进 客户对接文档——它是这套设计里唯一真正的妥协。
需要实时决策的场景(例如按请求上下文动态判权)不在第一版范围内。真要做,
正确的位置是 Hub 侧的 permission_check 链里加一个带超时和缓存的 provider,
且明确只影响 Hub 入口,不改变 server 的终判——但那时要接受"两层结论可能不一致",
需要单独的规格来定义。
规则来源 provider 属于目录 ACL 能力,实现在 feature/dir-acl:
cloudfile_ext/acl/sources/local_db.py ← register_provider('acl_rule_source', 'local-db', …)
cloudfile_ext/acl/sources/external_service.py ← register_provider('acl_rule_source', 'external-service', …)
基线只提供机制(providers.py、external_service.py、周期任务、webhook 路由能力),
不认识 acl_rule_source 这个 kind——设置名由 kind 推导,所以新增 kind 不需要改基线。
需求:search 要可扩展,meilisearch 只是其中一种方式。
seahub/utils/__init__.py HAS_FILE_SEARCH ← 或上 provider 是否已配置
│ (不接这里,CE 上六个入口根本不会路由到搜索)
▼
seahub/api2/… 搜索入口(6 处,全部读 HAS_FILE_SEARCH)
▼
seahub/search/utils.py search_files()
│
├─ CloudFile provider 应答 ──→ 返回 (hits, total)
└─ 未选中 → es_search()(原生 Elasticsearch 路径,逐字节不变)
│
▼
Seahub 原有后处理:库归属解析、虚拟根重写、dirent 查询、**库级可见性收敛**
为什么切在 search_files 而不是六个入口:一是把上游改动压到一个函数;
二是 provider 自动继承 Seahub 的库范围收敛逻辑——如果让 provider 自己实现,
一旦写错就是跨库泄露文件。让后端只负责"匹配文档",不负责"能看见哪些库"。
provider 契约:
search_files(repos_map, search_path, keyword, obj_desc,
start, size, org_id, search_filename_only,
filters=None) -> (hits, total)
# hits: [{'repo_id': ..., 'fullpath': ..., ...}, ...]filters 是属性/标签谓词(cloudfile_ext/search_query.py),与上游的
obj_desc(类型、后缀、时间、大小等文件固有属性)正交,两者同时传入。
它存在的理由是分支切分,不是功能。 组合检索——"标了『合同』、部门是 『法务』、正文含『违约金』的文件"——同时需要元数据簇(拥有字段)和检索簇 (拥有索引)。没有共享词汇,这个特性只能落在其中一条分支上,那条分支就再也 无法脱离另一条开发和验收,两个簇塌成一条大分支、一个长验收周期。
有了词汇表之后:
| 簇 | 职责 | 独立验收方式 |
|---|---|---|
| D 元数据 | 声明字段、喂索引、用这套词汇发起查询 | 假 provider 记录"被要求了什么" |
| E 检索 | 把词汇翻译成后端自己的过滤语法 | 合成文档验"过滤正确" |
组合检索的端到端验收需要两者同时在场,那属于 dev 上的集成门禁,
不归任一分支。
算子刻意保持精简(eq ne in contains gt gte lt lte exists)——
词汇表一旦超出最弱后端的表达能力,不兼容就只是被推迟到运行时。
provider 必须声明自己支持哪些算子:
class MeilisearchProvider:
supported_filter_ops = frozenset({search_query.EQ, search_query.IN})未声明的算子在调用 provider 之前就被拒绝。不声明 = 不支持任何算子, 于是只会被以无过滤的形式调用——所以老 provider 不受影响。
为什么是拒绝而不是忽略:被丢掉的谓词返回比请求更大的结果集, 而调用方无法区分"没有文件带这个标签"和"标签条件被忽略了"——两种情况看起来 一模一样。同样的道理适用于
parse()拒绝无法识别的键:一个拼错的键 被丢掉,查询就悄悄变宽了。
已规划的实现:
| name | 归属 | 说明 |
|---|---|---|
meilisearch |
簇 E,✅ 已实现 | cloudfile_ext/search/backends/meilisearch.py,CF_PROVIDER_SEARCH=meilisearch 选中 |
(无 seasearch provider) |
— | 确认不需要注册:上游 CE 14.0 的 SeaSearch 走 seahub/api2/views.py 里独立的 elif HAS_FILE_SEASEARCH 分支(ai_search_files),完全不经过这个 provider 机制——CF_PROVIDER_SEARCH 留空就是默认,见 检索 |
| 企业自有检索 | 客户对接,⬜ 未实现 | 经 external_service.py 调用,契约同上 |
两个上游改动的取舍:
search/utils.py(查询分发)与utils/__init__.py(HAS_FILE_SEARCH或上)。后者不接的话,CE 部署里六个入口根本不会走到搜索, provider 永远不被调用。两处都是"或上/回落"式的加法改动,不改变原生行为, 但确实让 Hub 的上游文件从 3 个变成 5 个,已登记在 upstream-patches/cloudfile-hub.txt。
新增扩展点时:
- 在第二节登记(形状、上游调用点、基线行为)
- 在第三节矩阵里补一列,标出哪些特性依赖
- 如果它没有上游调用点,写进第四节缺口,并说明谁被卡住
tools/preflight-checks.py 会检查 registry.py 里声明的每个钩子是否都在这份
文档的矩阵里出现——防止扩展点加了却没人知道。