在 Backstage 中集成 Confluence 搜索:Collator 与结果项组件的完整接入指南
[!NOTE] 本文内容基于仓库
contrib/search/confluence目录下的原始实现整理。该目录文档已标注为弃用(deprecated),官方推荐改用社区维护的@backstage-community/plugin-search-backend-module-confluence-collator插件。本文仅用于帮助读者理解 Backstage 搜索插件接入第三方文档源(Collator)的完整技术原理与历史实现路径,若用于生产环境请优先评估社区插件方案。
导读
Backstage 搜索插件通过「Collator(文档收集器)」从各类数据源抓取文档并构建全文索引。本篇文章聚焦仓库 contrib/search/confluence 提供的 Confluence 搜索集成方案,完整讲解如何把 Atlassian Confluence 空间与页面作为搜索源接入 Backstage:包括后端 ConfluenceCollator 的 REST API 调用逻辑、前端 ConfluenceResultListItem 结果项组件,以及前后端注册代码的落地位置。读完本文,你将掌握 Backstage 搜索插件中「数据收集器 + 结果展示组件」双端协作的标准接入范式,并能据此扩展出任意第三方文档源的搜索集成。
集成概述:把 Confluence 变成 Backstage 的搜索源
Backstage 的搜索插件采用「索引构建器(IndexBuilder)+ 调度器(Scheduler)+ 搜索引擎」的架构:Collator 负责从数据源收集文档并归一化为可索引结构,Decorator 负责对文档做加工,搜索引擎负责存储与检索。本目录提供的两个文件正是这条链路中的两端:
| 文件 | 角色 | 部署位置 |
|---|---|---|
ConfluenceCollator(ConfluenceCollator.md 中的参考实现) |
后端文档收集器,调用 Confluence REST API 拉取空间与页面 | packages/backend/src/plugins/search/ |
ConfluenceResultListItem(ConfluenceResultListItem.md 中的参考实现) |
前端搜索结果项组件,渲染标题、摘要并跳转原页面 | packages/app/src/components/search/ |
接入流程分为三步:先把两个文件复制到 Backstage 应用的对应目录,再在前端 SearchPage.tsx 中为 confluence 类型注册渲染组件,最后在后端 search.ts 中把 ConfluenceCollator 注册进 indexBuilder。
弃用说明与社区替代方案
目录 README.md 开头的 [!NOTE] 明确指出:本文档已弃用,将在未来移除,官方建议使用社区插件 @backstage-community/plugin-search-backend-module-confluence-collator。这意味着本文讲解的 ConfluenceCollator.ts 与 ConfluenceResultListItem.tsx 属于早期的参考实现,其主要价值在于揭示搜索插件扩展机制的原理。生产环境部署时,应优先查看社区插件仓库的安装与配置文档。
后端接入:复制 Collator 文件到搜索插件目录
按照 README.md 的指引,首先把本目录下的两个文件复制到 Backstage 应用的 packages/backend/src/plugins/search/ 路径下。该路径是旧版(非新后端系统)Backstage 应用后端插件默认的代码组织位置,后端搜索插件的路由与 Collator 注册通常都集中在这里:
packages/backend/src/plugins/search/
├── ConfluenceCollator.ts # 新增:Confluence 文档收集器
└── ConfluenceResultListItem.tsx # 注意:该组件实际应置于前端(见下文)
需要说明的是:
ConfluenceResultListItem是 React 组件,实际使用位置是前端packages/app/src/components/search/(README 中的代码示例也印证了这一点)。原 README 要求"将本目录两个文件都放到后端 search 路径下"的表述更像一份宽松的放置说明,实操时建议把 Collator 放后端、结果组件放前端,保持前后端职责清晰。
前端接入:在 SearchPage 中渲染 Confluence 结果
在 Backstage 应用前端的 SearchPage.tsx 中,搜索页通过 useSearch 拿到查询结果后,会根据文档的 type 字段选择对应的结果项组件渲染。接入 Confluence 需要在两处改动:
1. 引入结果项组件
import { ConfluenceResultListItem } from './ConfluenceResultListItem';
2. 在类型分发中注册 confluence 分支
搜索结果组件通常维护一个从 document.type 到渲染组件的映射,在 SearchPage.tsx 中补上 confluence 分支即可:
case 'confluence':
return (
<ConfluenceResultListItem
key={document.location}
result={document}
/>
);
这里的关键关联点是 document.type:Collator 通过 public readonly type: string = 'confluence' 声明了自己的文档类型(详见后文),前端正是靠这个 type 值把搜索结果路由到 ConfluenceResultListItem。result 属性接收的是 IndexableDocument,即搜索插件定义的统一文档结构。
IndexableDocument 的字段契约
ConfluenceResultListItem 的 Props 类型定义如下:
type Props = {
result: IndexableDocument;
};
它依赖 result 的 text、title、location 三个字段:
title:文档标题,显示为结果项的主标题;text:文档正文,用于生成摘要(该组件会先截取前 500 字符做 HTML 标签剥离,再取前 80 字符展示);location:文档的跳转链接,指向 Confluence 页面 web UI。
这恰好对应后端 Collator 返回的文档结构 { title, text, location },前后端通过 IndexableDocument 这一契约完成对接。IndexableDocument 接口定义在 plugins/search-common/src/types.ts,是搜索插件所有 Collator 输出与搜索结果的通用数据形状。
结果项组件解析:剥离 HTML 与展示摘要
ConfluenceResultListItem.md 提供了 ConfluenceResultListItem.tsx 的完整参考实现,它展示了三个值得关注的处理点:
1. 手工剥离 HTML 标签
Confluence REST API 返回的 body.storage.value 是富文本 HTML。组件在渲染摘要前,用一个简单的字符级状态机把 <...> 标签剥离掉,只保留纯文本:
const chars = [];
let isTag = false;
for (const c of result.text.substring(0, 500)) {
if (c === '<') {
isTag = true;
continue;
}
if (c === '>') {
isTag = false;
chars.push(' ');
continue;
}
if (!isTag) {
chars.push(c);
}
}
const excerpt =
chars.join('').substring(0, 80) + (result.text.length > 80 ? '...' : '');
处理策略总结:先截取原文前 500 字符(控制计算量),剔除标签后取前 80 字符,若原文超过 80 字符则追加 ...。这是早期参考实现的做法,实际生产中可以替换为更健壮的 DOM 解析方案。
2. 结果项布局
<Link to={result.location}>
<ListItem alignItems="center">
<ListItemIcon>
<img
width="20"
height="20"
src="https://cdn.worldvectorlogo.com/logos/confluence-1.svg"
/>
</ListItemIcon>
<ListItemText
primaryTypographyProps={{ variant: 'h6' }}
primary={result.title}
secondary={excerpt}
/>
</ListItem>
<Divider />
</Link>
Link来自@backstage/core-components,to指向result.location,点击即可跳转到原始 Confluence 页面;ListItem/ListItemIcon/ListItemText/Divider来自 Material-UI(当前仓库该历史实现使用的版本为@material-ui/core),用于组织列表布局;- 图标使用 Confluence 的 SVG Logo,作为结果项的视觉标识。
3. 组件导出方式
该组件以命名导出(export const ConfluenceResultListItem = ...)方式暴露,这符合 Backstage 仓库自身的编码约定——ADRs 中明确提倡避免默认导出(参见 adr003-avoid-default-exports.md),因此在 SearchPage.tsx 中需要用 import { ConfluenceResultListItem } from './ConfluenceResultListItem' 的形式引入。
后端核心:ConfluenceCollator 的 REST API 调用链
ConfluenceCollator.md 给出了 ConfluenceCollator.ts 的完整参考实现,它实现了 @backstage/plugin-search-common 的 DocumentCollator 接口。整个收集过程分为三个阶段,对应三次对 Confluence REST API 的调用:
阶段一:获取全部全局空间(getSpaces)
async function getSpaces(): Promise<string[]> {
const data = await getConfluenceData(
`${ConfluenceUrlBase}/space?&limit=1000&type=global&status=current`,
);
// 从 data['results'] 中逐个取出 result['key'] 作为空间 key
}
调用 Confluence REST API 的 /wiki/rest/api/space 端点,带 limit=1000、type=global、status=current 参数,拉取所有处于 current 状态的全局空间,仅保留每个空间的 key 字段。
阶段二:按空间分页拉取页面(getDocumentsFromSpaces)
let requestUrl = `${ConfluenceUrlBase}/content?limit=1000&status=current&spaceKey=${space}`;
while (next) {
const data = await getConfluenceData(requestUrl);
// 收集 data['results'][]._links.self(页面详情 URL)
if (data['_links']['next']) {
requestUrl = data['_links']['base'] + data['_links']['next'];
} else {
next = false;
}
}
对每个空间调用 /wiki/rest/api/content 端点,按 spaceKey 过滤、limit=1000 分页,从 results 中收集每个页面的 _links.self(即页面详情资源的 URL)。分页通过 _links.next 与 _links.base 拼接下一页 URL,直到没有下一页为止。
阶段三:逐页拉取正文并归一化(getDocumentInfo)
const data = await getConfluenceData(documentUrl + '?expand=body.storage');
if (data['status'] && data['status'] == 'current') {
const documentMetaData = {
title: data['title'],
text: data['body']['storage']['value'],
location: data['_links']['base'] + data['_links']['webui'],
};
documentInfo.push(documentMetaData);
}
对每个页面 URL 追加 ?expand=body.storage 参数,一次性取回标题、HTML 正文与页面 Web 地址,组装成 { title, text, location } 结构——这正是前端 ConfluenceResultListItem 消费的数据形态。status === 'current' 的判断确保已归档或已删除的页面不会被收录。
统一出口:execute() 与 type 声明
export class ConfluenceCollator implements DocumentCollator {
public readonly type: string = 'confluence';
async execute() {
const spacesList = await getSpaces();
const documentsList = await getDocumentsFromSpaces(spacesList);
const documentMetaDataList = await getDocumentInfo(documentsList);
return documentMetaDataList;
}
}
type = 'confluence'是文档类型标识,后端IndexBuilder会以它为索引名/类型名(见下文源码),前端也用它做结果项路由;execute()串联三个阶段,返回文档元数据数组。
鉴权方式:Basic Auth 与 CONFLUENCE_TOKEN 环境变量
所有 API 请求都通过统一封装的 getConfluenceData 发起:
const res = await fetch(requestUrl, {
method: 'get',
headers: {
Authorization: `Basic ${process.env.CONFLUENCE_TOKEN}`,
},
});
- 使用 HTTP Basic 鉴权,
process.env.CONFLUENCE_TOKEN存放 base64 编码后的用户名:API Token凭据,需要提前在 Backstage 后端进程的环境变量中配置; - 网络请求使用
cross-fetch(历史实现;当前仓库的核心依赖已迁移到 Node 原生fetch,可参见 ADR adr014-use-fetch.md); - 失败兜底:请求失败或响应非
ok时返回空对象{},调用方对results做空值判断后安全退出,保证单个空间或页面失败不会拖垮整个收集任务。
后端注册:把 Collator 挂进 indexBuilder
在 packages/backend/src/plugins/search.ts 中完成两步注册:
1. 引入 ConfluenceCollator
import { ConfluenceCollator } from './search/ConfluenceCollator';
2. 注册到索引构建器
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
collator: new ConfluenceCollator(),
});
defaultRefreshIntervalSeconds: 600 表示每 600 秒(10 分钟)重新执行一次收集任务,让新发布的 Confluence 页面能被周期性收录进索引。注册完成后,indexBuilder.build() 会把所有已注册 Collator 编译成调度任务交给 Scheduler 周期性执行。
源码印证:addCollator 的调度与类型注册机制
通过阅读当前仓库源码,可以确认 indexBuilder.addCollator 的底层行为。IndexBuilder 类定义在 plugins/search-backend-node/src/IndexBuilder.ts,其 addCollator 实现如下:
addCollator(options: RegisterCollatorParameters): void {
const { factory, schedule } = options;
this.logger.info(
`Added ${factory.constructor.name} collator factory for type ${factory.type}`,
);
this.collators[factory.type] = {
factory,
schedule,
};
this.documentTypes[factory.type] = {
visibilityPermission: factory.visibilityPermission,
};
}
- 以
factory.type为键存储 collator,并同步登记文档类型信息documentTypes; RegisterCollatorParameters接口(见 plugins/search-backend-node/src/types.ts)要求提供schedule(调度器任务运行器)与factory(返回文档收集器的工厂)。README 示例中defaultRefreshIntervalSeconds这种简写形式属于旧版IndexBuilder.addCollator的便捷参数,最终都会被编译为对应的调度任务;- 注册完成后,
build()会将 Collator 包装进Scheduler(见 plugins/search-backend-node/src/Scheduler.ts),由调度器在后台按间隔周期触发execute(),把结果送入搜索引擎索引。
DocumentCollator 接口本身定义在 plugins/search-common/src/types.ts,对每个 Collator 的要求正是 type(文档类型/索引名)与 getCollator()(旧接口为 execute())两要素,ConfluenceCollator 的实现完全符合这一契约。
常见问题与排查要点
- 搜索不到 Confluence 内容:检查后端进程是否正确设置了
CONFLUENCE_TOKEN环境变量;确认indexBuilder.addCollator的注册代码已生效,且build()被调用;首次索引建立后需等待一个刷新周期或触发一次收集。 - 部分页面缺失:确认页面状态为
current(API 会过滤非 current 页面);检查空间是否属于global类型且为current状态;分页是否被_links.next完整遍历。 - 前端点开结果无跳转:确认
ConfluenceResultListItem收到的result.location是完整的 Confluence Web 地址(由_links.base + _links.webui拼接),且SearchPage.tsx的case 'confluence'分支已正确注册。 - 类型不匹配:前端
case分支的字符串必须与 Collator 的type属性('confluence')完全一致,否则搜索结果无法路由到对应组件。 - 生产环境选型:本实现已弃用,若要在生产环境接入 Confluence 搜索,请改用官方推荐的社区插件
@backstage-community/plugin-search-backend-module-confluence-collator,其功能与配置方式以该插件仓库文档为准。
小结
contrib/search/confluence 目录完整演示了 Backstage 搜索插件接入第三方文档源的端到端路径:后端用 ConfluenceCollator 通过 REST API 拉取空间、分页抓取页面、归一化为 { title, text, location } 文档并周期性送入索引;前端用 ConfluenceResultListItem 按类型路由渲染搜索结果并跳转原文。虽然这份参考实现已被社区插件取代,但其揭示的「Collator 类型契约 + IndexBuilder 注册 + 前端类型分发渲染」的扩展模式,适用于任何文档源的搜索集成,是理解 Backstage 搜索插件架构的一份高质量样例。
相关仓库资源
- 集成说明与弃用声明:contrib/search/confluence/README.md
- 后端收集器参考实现:contrib/search/confluence/ConfluenceCollator.md
- 前端结果项参考实现:contrib/search/confluence/ConfluenceResultListItem.md
- 搜索文档契约接口:plugins/search-common/src/types.ts
- 索引构建器与调度机制:plugins/search-backend-node/src/IndexBuilder.ts、plugins/search-backend-node/src/Scheduler.ts
- 搜索后端插件注册入口:plugins/search-backend/src/plugin.ts
- 相关架构决策记录:adr014-use-fetch.md、adr003-avoid-default-exports.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00