115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
|
|
import "server-only";
|
|||
|
|
|
|||
|
|
import type { ProviderId } from "@/lib/ai/keys";
|
|||
|
|
import { parseProviderApiKeys, resolveApiKey } from "@/lib/ai/keys";
|
|||
|
|
import { getActiveAccountConnectionSettings } from "@/lib/account-connection-settings";
|
|||
|
|
import { getActiveAccountProfile } from "@/lib/account-context";
|
|||
|
|
import { getOrCreateSettingsForUser } from "@/lib/user-settings";
|
|||
|
|
import { accountHasThreadsToken, isThreadsAppConfigured } from "@/lib/threads-api";
|
|||
|
|
import { requireSessionUser } from "@/lib/auth/session";
|
|||
|
|
import type { FeatureCapability, WorkspaceCapabilities } from "@/lib/capabilities/types";
|
|||
|
|
|
|||
|
|
export type { CapabilityFeature, FeatureCapability, WorkspaceCapabilities } from "@/lib/capabilities/types";
|
|||
|
|
|
|||
|
|
function cap(
|
|||
|
|
ready: boolean,
|
|||
|
|
label: string,
|
|||
|
|
options?: { reason?: string; setupHref?: string; setupLabel?: string }
|
|||
|
|
): FeatureCapability {
|
|||
|
|
return { ready, label, ...options };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function aiCapability(
|
|||
|
|
provider: string,
|
|||
|
|
keys: ReturnType<typeof parseProviderApiKeys>,
|
|||
|
|
label: string
|
|||
|
|
): FeatureCapability {
|
|||
|
|
const id = provider as ProviderId;
|
|||
|
|
const ready = !!resolveApiKey(id, keys);
|
|||
|
|
return cap(ready, label, ready
|
|||
|
|
? undefined
|
|||
|
|
: {
|
|||
|
|
reason: `請先在設定頁填入 ${label} API key`,
|
|||
|
|
setupHref: "/settings",
|
|||
|
|
setupLabel: "前往 AI 設定",
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function getWorkspaceCapabilities(): Promise<WorkspaceCapabilities> {
|
|||
|
|
const user = await requireSessionUser();
|
|||
|
|
const [account, connection, settings] = await Promise.all([
|
|||
|
|
getActiveAccountProfile(),
|
|||
|
|
getActiveAccountConnectionSettings(),
|
|||
|
|
getOrCreateSettingsForUser(user.id),
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
const keys = parseProviderApiKeys(settings.providerApiKeys);
|
|||
|
|
const ai = aiCapability(settings.aiProvider, keys, "AI 模型");
|
|||
|
|
const researchProvider = settings.researchAiProvider ?? settings.aiProvider;
|
|||
|
|
const research = aiCapability(researchProvider, keys, "研究地圖 AI");
|
|||
|
|
|
|||
|
|
const hasToken = accountHasThreadsToken(account);
|
|||
|
|
const appConfigured = isThreadsAppConfigured();
|
|||
|
|
const threadsApi = cap(hasToken && appConfigured, "Threads 官方 API", {
|
|||
|
|
reason: !appConfigured
|
|||
|
|
? "伺服器尚未設定 Threads App(THREADS_APP_ID / SECRET)"
|
|||
|
|
: !hasToken
|
|||
|
|
? "請到連線設定綁定此經營帳號的 Threads OAuth"
|
|||
|
|
: undefined,
|
|||
|
|
setupHref: "/connections",
|
|||
|
|
setupLabel: "前往連線設定",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const hasBrowserState = !!(account?.storageState?.trim());
|
|||
|
|
const browserSession = cap(hasBrowserState, "Chrome 瀏覽器同步", {
|
|||
|
|
reason: "請到連線設定用 Chrome 擴充同步 Threads session",
|
|||
|
|
setupHref: "/connections",
|
|||
|
|
setupLabel: "前往連線設定",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const canScanViaApi = connection.searchViaApi && threadsApi.ready;
|
|||
|
|
const canScanViaBrowser = connection.devMode && browserSession.ready;
|
|||
|
|
const scan = cap(canScanViaApi || canScanViaBrowser, "海巡搜尋", {
|
|||
|
|
reason: connection.searchViaApi
|
|||
|
|
? !threadsApi.ready
|
|||
|
|
? "已選 API 海巡,但帳號尚未綁定 Threads"
|
|||
|
|
: !connection.devMode
|
|||
|
|
? "官方 API 未就緒,且未開啟瀏覽器備援"
|
|||
|
|
: "官方 API 與瀏覽器模式都未就緒"
|
|||
|
|
: connection.devMode
|
|||
|
|
? "已開瀏覽器模式,但尚未同步 Chrome session"
|
|||
|
|
: "請到連線設定選「API 優先」或「Chrome 同步」",
|
|||
|
|
setupHref: "/connections",
|
|||
|
|
setupLabel: "設定海巡方式",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const generate = { ...ai, label: "AI 生成" };
|
|||
|
|
const analyze = { ...research, label: "主題分析" };
|
|||
|
|
const viralAnalysis = { ...ai, label: "爆款分析" };
|
|||
|
|
const outreach = { ...ai, label: "獲客留言" };
|
|||
|
|
|
|||
|
|
const canPublishViaApi = connection.publishViaApi && threadsApi.ready;
|
|||
|
|
const canPublishViaBrowser = connection.devMode && browserSession.ready;
|
|||
|
|
const publish = cap(canPublishViaApi || canPublishViaBrowser, "發布貼文", {
|
|||
|
|
reason: connection.publishViaApi
|
|||
|
|
? "已選 API 發文,但帳號尚未綁定 Threads"
|
|||
|
|
: connection.devMode
|
|||
|
|
? "已開瀏覽器發文,但尚未同步 Chrome session"
|
|||
|
|
: "請到連線設定設定發文方式",
|
|||
|
|
setupHref: "/connections",
|
|||
|
|
setupLabel: "設定發文方式",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
ai,
|
|||
|
|
research,
|
|||
|
|
threadsApi,
|
|||
|
|
browserSession,
|
|||
|
|
scan,
|
|||
|
|
analyze,
|
|||
|
|
generate,
|
|||
|
|
viralAnalysis,
|
|||
|
|
outreach,
|
|||
|
|
publish,
|
|||
|
|
};
|
|||
|
|
}
|