haixunMaster/lib/capabilities.ts

115 lines
4.2 KiB
TypeScript
Raw Permalink Normal View History

2026-06-21 12:50:31 +00:00
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 AppTHREADS_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,
};
}