Immich Partner Sharing(伙伴共享)详解:从图库共享到时间线整合的完整实现
Immich 的 Partner Sharing(伙伴共享)允许两名注册用户互相开放自己的照片与视频库,接收方可以浏览并下载对方的资产,还可以选择将对方的照片直接并入自己的主时间线。本篇基于仓库文档 partner-sharing.md,结合服务端 PartnerController、PartnerService、partner 表结构 与 TimelineService 的源码,完整讲解该功能的能力边界、API 设计、Web 与移动端操作流程,以及时间线整合背后的底层调用链。读完后你将能够:说清伙伴共享包含与不包含哪些数据、通过 API 管理共享关系、并理解 inTimeline 开关如何影响主时间线的查询行为。
伙伴共享的能力边界
伙伴共享是用户级的整库共享,而不是针对单个资产的选择性分享。根据官方文档与源码约束,其能力范围如下。
共享包含的内容:
- 对方所有未归档(non-archived)且未进回收站(non-trashed) 的照片和视频;
- 全部元数据,包括 GPS 地理位置信息;
- 通过共享链接(shared link)、相册(album)等渠道继续分享这些资产的能力。
共享不包含的内容:
- 已经存在的“伙伴相册”(partner albums)不会自动合并;
- 某资产在对方那里是否被收藏(favorite)这一状态;
- People 与人脸识别(facial recognition)数据。
文档中还有两条使用须知,源码中都能找到对应实现:
- 共享是单向的。 要查看对方的资产,对方也必须执行一次共享操作。从 PartnerService 可以看到,
create只写入一条sharedById -> sharedWithId方向的记录,系统不会自动反向插入。 - 主时间线可能出现重复资产。 因为重复检测是按用户(per-user)进行的,当对方的资产通过时间线整合显示在你的主时间线中时,如果双方拍摄了相似照片,就可能同时出现。
底层数据模型:partner 表如何表达“双向关系”
伙伴关系在数据库中以一张 partner 表 表达,核心设计值得展开:
// server/src/schema/tables/partner.table.ts
@Table('partner')
export class PartnerTable {
@ForeignKeyColumn(() => UserTable, {
onDelete: 'CASCADE',
primary: true,
// [sharedById, sharedWithId] is the PK constraint
})
sharedById!: string;
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true })
sharedWithId!: string;
@Column({ type: 'boolean', default: false })
inTimeline!: Generated<boolean>;
// ...
}
几个关键点:
- 复合主键
(sharedById, sharedWithId):一条记录即“谁共享给了谁”。A 与 B 互相共享时是两条记录(A→B 与 B→A),这从结构上直接解释了为什么共享是单向的; inTimeline布尔字段,默认false:控制“B 的照片是否显示在 A 的主时间线里”,这正是文档中 Show in timeline 开关背后落库的字段;- 外键
onDelete: 'CASCADE':任一用户被删除时,其所有伙伴关系自动级联删除,不会留下悬空记录; - 表上注册了
partner_delete_audit删除审计触发器(见 schema/functions.ts),伙伴关系的移除会写入审计表 partner-audit。
PartnerRepository 还定义了 PartnerDirection 枚举,用来区分当前查询的视角:
export enum PartnerDirection {
SharedBy = 'shared-by', // “我共享给了谁”
SharedWith = 'shared-with', // “谁共享给了我”
}
API 设计:/partners 端点一览
PartnerController 挂在 partners 路由下,端点及权限要求如下:
| 方法 | 路径 | 说明 | 所需权限 |
|---|---|---|---|
| GET | /partners?direction=shared-by|shared-with |
按方向列出伙伴 | Permission.PartnerRead |
| POST | /partners(body: { sharedWithId }) |
创建共享关系 | Permission.PartnerCreate |
| PUT | /partners/:id(body: { inTimeline }) |
更新对方资产是否进入我的时间线 | Permission.PartnerUpdate |
| DELETE | /partners/:id |
移除伙伴,停止共享 | Permission.PartnerDelete |
请求/响应结构定义在 partner.dto.ts:
PartnerCreateDto:sharedWithId为必填 UUID(接收方用户 ID);PartnerUpdateDto:仅含inTimeline: boolean一个字段,可见 API 层面的“更新”语义被刻意收敛为时间线开关;PartnerResponseDto:在UserResponseSchema基础上扩展了inTimeline字段,即响应本质上是“伙伴用户信息 + 时间线开关状态”。
服务层 PartnerService 的关键行为:
- 创建前双重校验:先
get检查该方向是否已存在关系,存在则抛BadRequestException('Partner already exists');再校验目标用户真实存在,否则抛'Invalid user'; - 搜索是“合并后过滤”:
search先取该用户参与的全部关系(getAll查sharedById = me OR sharedWithId = me),再按direction参数过滤,并从源码结构看会过滤掉已被软删除的用户(partner.sharedBy && partner.sharedWith非空); - 更新时强制访问控制:
update会调用requireAccess校验当前用户确实持有PartnerUpdate权限,且只更新inTimeline。
操作指南
以下操作路径以文档 User Settings 页面为基础,适用于 Web 端与移动端。
添加伙伴
在 Web 端进入 User > Account Settings > Sharing,搜索并选择目标用户发起共享。文档中的三张截图依次展示了添加伙伴流程:
查看伙伴的资产
共享建立后,双方都可以在 Sharing(共享)页面(前端路由见 sharing/+page.svelte/sharing/+page.svelte))看到对方的条目,点击进入对方的共享图库。该页面在 Web 端由 partners/[userId] 路由渲染,页面代码中固定以 visibility: AssetVisibility.Timeline(即只看时间线可见资产)加载对方图库,这与“不包含归档/回收站资产”的文档描述一一对应。
在对方的图库里,页面注册了与主时间线一致的多选操作栏,包含创建共享链接(CreateSharedLink)、加入相册(AddToAlbum)、下载(DownloadAction) 三个动作——这正是文档所说“接收方可以浏览并下载资产、并可通过共享链接/相册继续分享”的前端实现依据。
时间线整合(Timeline Integration)
伙伴共享的照片可以显示在主时间线中。该开关按伙伴逐个设置(per-partner),Web 与移动端均可操作:
- Web:
Account Settings > Partner Sharing > Show in timeline,对应 SharingSettings.svelte/user-settings/SharingSettings.svelte) 中的设置项; - 移动端:在伙伴的视图页(partner's view)直接切换对应的 toggle 按钮(文档配图为 partner-sharing-6 / partner-sharing-8 两张截图)。
这个开关最终调用 PUT /partners/:id 把 inTimeline 写入 partner 表。主时间线的查询链路则在 TimelineService 中完成:
// server/src/services/timeline.service.ts
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
const { userId, ...options } = dto;
let userIds: string[] | undefined;
if (userId) {
userIds = [userId];
if (dto.withPartners) {
const partnerIds = await getMyPartnerIds({
userId: auth.user.id,
repository: this.partnerRepository,
timelineEnabled: true, // 只取 inTimeline = true 的伙伴
});
userIds.push(...partnerIds);
}
}
return { ...options, userIds };
}
从源码结构看,主时间线带 withPartners 参数时,服务会把“我”的 ID 与所有 inTimeline = true 的伙伴 ID 合并进 userIds 查询条件,从而在一条时间线查询中同时取回双方资产。同时 timeBucketChecks 明确限制了该模式的适用范围:
if (isRequestedLocked || isRequestedArchived || isRequestedFavorite || isRequestedTrash) {
throw new BadRequestException(
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
}
即 withPartners 仅对“非归档、非回收站、非收藏、非锁定”的资产生效——这与文档“不包含收藏状态、People 数据”的能力边界在服务端被硬性强制,而不是仅靠前端隐藏。
移除伙伴
在 User > Account Settings > Sharing 页面点击对应伙伴条目上的 X 按钮即可终止共享,对应 DELETE /partners/:id。服务端 remove 会先按 (sharedById=me, sharedWithId=partner) 精确查找关系记录,不存在则抛 'Partner not found',存在则删除该记录;删除动作由表级触发器写入 partner_audit 审计表,便于事后追溯。移除是单方向的——如果对方也共享给了你,那条反向记录仍然存在。
适用前提与限制小结
- 前提条件:双方都是同一 Immich 实例下的注册用户(
sharedWithId必须是真实存在的用户 UUID,否则创建会失败); - 共享为单向、按方向独立记录,双向可见需双方各自操作;
- 共享范围固定为对方时间线可见资产及其元数据,归档、回收站、收藏、People/人脸识别数据均不包含;
withPartners时间线整合在服务端仅限非归档/非回收站/非收藏/非锁定资产;- 重复检测按用户独立进行,时间线整合后主时间线可能出现重复照片。
整体来看,Immich 的伙伴共享用一张复合主键的 partner 表 + 四个权限收敛的 REST 端点,实现了“整库共享 + 独立时间线开关”的组合:数据模型保证关系的方向性与级联清理,inTimeline 字段与 getMyPartnerIds({ timelineEnabled: true }) 的查询链路则把 UI 上的一个开关精确翻译成了主时间线查询条件的扩展。
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 StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00





