feat(files): store files on the host when a channel has no file domain
Chat stays usable when the file service is off or down. Files up to 256 MB are hosted like chat; downloads try the host first and fall back to file-service tickets. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> [CI]
This commit is contained in:
1 parent
7e6ef5e587
commit
4b4ab4ee4d
8 files changed
+47
-16
No files matched your search
@@ -66,21 +66,34 @@ export async function createFileDomain(resource: SavedResource, session: Channel
|
||||
* 保证这个频道有文件域:没有就创建一个并记到频道上(创建频道时没带的、旧频道)。
|
||||
* 只有所有者和管理员能分配;其他人遇到没有文件域的频道只能等管理员分配。
|
||||
*/
|
||||
export async function ensureThreadFileDomain(resource: SavedResource, session: ChannelSession, threadId: string): Promise<void> {
|
||||
if (resource.kind !== 'channel') return;
|
||||
export async function ensureThreadFileDomain(resource: SavedResource, session: ChannelSession, threadId: string, size: number): Promise<boolean> {
|
||||
if (resource.kind !== 'channel') return false;
|
||||
const thread = (await cachedWorkspace(resource.id)).structure.channels.find((channel) => channel.id === threadId);
|
||||
if (!thread) throw new Error('频道不存在');
|
||||
if (thread.fileDomain) return;
|
||||
if (!await filesEnabled()) throw new Error('服务端没有开启文件服务,不能上传文件');
|
||||
if (thread.fileDomain) return true;
|
||||
// 文件服务没开启或没部署好:小文件照样存在主后端(和聊天一样),大文件要等文件服务
|
||||
const hosted = (reason: string): false => {
|
||||
if (size > HOSTED_FILE_MAX_BYTES) throw new Error(`${reason},暂时只能上传不超过 ${HOSTED_FILE_MAX_BYTES / 1024 / 1024} MB 的文件`);
|
||||
return false;
|
||||
};
|
||||
if (!await filesEnabled()) return hosted('服务端没有开启文件服务');
|
||||
let domain: string;
|
||||
try { domain = await createFileDomain(resource, session); }
|
||||
catch (error) {
|
||||
if (hasCode(error, ErrorCode.Forbidden)) throw new Error('这个频道还没有文件空间,请让管理员先为它分配');
|
||||
if (hasCode(error, ErrorCode.Forbidden)) {
|
||||
if (size <= HOSTED_FILE_MAX_BYTES) return false;
|
||||
throw new Error('这个频道还没有文件空间,请让管理员先为它分配');
|
||||
}
|
||||
if (error instanceof Error && error.message === FILE_SERVICE_DOWN) return hosted('文件服务暂时不可用');
|
||||
throw error;
|
||||
}
|
||||
await changeWorkspace(resource.id, `channels/${threadId}/file-domain`, 'PUT', { fileDomain: domain });
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 没有文件域时,文件存在主后端能收的上限 */
|
||||
export const HOSTED_FILE_MAX_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
/** 频道文件域的容量和已用字节 */
|
||||
export async function fileDomainUsage(resourceId: string, threadId: string): Promise<FileDomainUsage | undefined> {
|
||||
const { session, resource } = await getSession(resourceId);
|
||||
@@ -141,6 +154,10 @@ export async function deleteFileBytes(grant: FileGrantBody, itemId: string): Pro
|
||||
*/
|
||||
export async function openItemFrames(resource: SavedResource, session: ChannelSession, itemId: string, from: number, signal?: AbortSignal): Promise<Response> {
|
||||
if (resource.kind === 'channel') {
|
||||
// 先问主后端:没有文件域的频道里,文件和聊天一样存在这里;存在文件服务的文件它会回 use_file_api,再去要票据
|
||||
try {
|
||||
return await apiFetch(`/channels/${resource.id}/items/${itemId}/frames?from=${from}`, { headers: session.authHeaders(tokenRole(session)), ...(signal ? { signal } : {}) });
|
||||
} catch (error) { if (!hasCode(error, ErrorCode.UseFileApi)) throw error; }
|
||||
const grant = await requestGrant(resource, session, { op: 'get', item: itemId, from });
|
||||
return externalFetch(`${base(grant)}/file/v1/domains/${grant.domain}/items/${itemHex(itemId)}/frames?from=${from}`, { 'X-WD-Ticket': grant.ticket }, signal);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user