♻️ refactor: change all market routes & api call into lambda trpc client call (#11256)

* feat: add market auth middleware & create market lamdar trpc endpoint

* feat: add user、social、oidc trpc endpoint

* feat: change the MARKET_ENDPOINTS call change to trpc

* refactor: add the fork double check modal

* fix: lint fixed

* feat: update the market sdk version

* feat: upadte the market sdk & fixed types
This commit is contained in:
Shinji-Li
2026-01-06 20:01:49 +08:00
committed by GitHub
parent 1456adc812
commit 8f7e37872f
20 changed files with 1799 additions and 550 deletions

View File

@@ -127,6 +127,10 @@
"llm.proxyUrl.title": "API proxy URL",
"llm.waitingForMore": "More models are <1>planned to be added</1>, stay tuned",
"llm.waitingForMoreLinkAriaLabel": "Open the Provider request form",
"marketPublish.forkConfirm.by": "by {{author}}",
"marketPublish.forkConfirm.confirm": "Confirm Publish",
"marketPublish.forkConfirm.description": "You are about to publish a derivative version based on an existing agent from the community. Your new agent will be created as a separate entry in the marketplace.",
"marketPublish.forkConfirm.title": "Publish Derivative Agent",
"marketPublish.modal.changelog.extra": "Describe the key changes and improvements in this version",
"marketPublish.modal.changelog.label": "Changelog",
"marketPublish.modal.changelog.maxLengthError": "Changelog must not exceed 500 characters",

View File

@@ -127,6 +127,10 @@
"llm.proxyUrl.title": "API 代理地址",
"llm.waitingForMore": "更多模型正在 <1>计划接入</1> 中,敬请期待",
"llm.waitingForMoreLinkAriaLabel": "打开模型服务商接入需求表单",
"marketPublish.forkConfirm.by": "作者:{{author}}",
"marketPublish.forkConfirm.confirm": "确认发布",
"marketPublish.forkConfirm.description": "你即将基于社区中已存在的助理发布一个二次创作版本。你的新助理将作为独立条目发布到市场。",
"marketPublish.forkConfirm.title": "发布二次创作助理",
"marketPublish.modal.changelog.extra": "描述此版本的主要变更和改进",
"marketPublish.modal.changelog.label": "变更日志",
"marketPublish.modal.changelog.maxLengthError": "变更日志不能超过500个字符",

View File

@@ -204,7 +204,7 @@
"@lobehub/desktop-ipc-typings": "workspace:*",
"@lobehub/editor": "^3.4.1",
"@lobehub/icons": "^4.0.2",
"@lobehub/market-sdk": "^0.25.1",
"@lobehub/market-sdk": "^0.27.1",
"@lobehub/tts": "^4.0.2",
"@lobehub/ui": "^4.9.3",
"@modelcontextprotocol/sdk": "^1.25.1",
@@ -454,4 +454,4 @@
"access": "public",
"registry": "https://registry.npmjs.org"
}
}
}

View File

@@ -0,0 +1,67 @@
'use client';
import { Avatar, Flexbox, Modal } from '@lobehub/ui';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
interface OriginalAgentInfo {
author?: {
avatar?: string;
name?: string;
userName?: string;
};
avatar?: string;
identifier: string;
name: string;
}
interface ForkConfirmModalProps {
loading?: boolean;
onCancel: () => void;
onConfirm: () => void;
open: boolean;
originalAgent: OriginalAgentInfo | null;
}
const ForkConfirmModal = memo<ForkConfirmModalProps>(
({ open, onCancel, onConfirm, originalAgent, loading }) => {
const { t } = useTranslation('setting');
if (!originalAgent) return null;
const authorName = originalAgent.author?.name || originalAgent.author?.userName || 'Unknown';
return (
<Modal
cancelText={t('cancel', { ns: 'common' })}
centered
closable
confirmLoading={loading}
okText={t('marketPublish.forkConfirm.confirm')}
onCancel={onCancel}
onOk={onConfirm}
open={open}
title={t('marketPublish.forkConfirm.title')}
width={480}
>
<Flexbox gap={16} style={{ marginTop: 16 }}>
<Flexbox align="center" gap={12} horizontal>
<Avatar avatar={originalAgent.avatar} size={48} style={{ flex: 'none' }} />
<Flexbox gap={4}>
<div style={{ fontWeight: 500 }}>{originalAgent.name}</div>
<div style={{ fontSize: 12, opacity: 0.6 }}>
{t('marketPublish.forkConfirm.by', { author: authorName })}
</div>
</Flexbox>
</Flexbox>
<p style={{ lineHeight: 1.6, margin: 0 }}>{t('marketPublish.forkConfirm.description')}</p>
</Flexbox>
</Modal>
);
},
);
ForkConfirmModal.displayName = 'ForkConfirmModal';
export default ForkConfirmModal;

View File

@@ -1,127 +1,107 @@
import { ActionIcon } from '@lobehub/ui';
import { ShapesUploadIcon } from '@lobehub/ui/icons';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { message } from '@/components/AntdStaticMethods';
import { HEADER_ICON_SIZE } from '@/const/layoutTokens';
import { checkOwnership } from '@/hooks/useAgentOwnershipCheck';
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
import { resolveMarketAuthError } from '@/layout/AuthProvider/MarketAuth/errors';
import { useServerConfigStore } from '@/store/serverConfig';
import ForkConfirmModal from './ForkConfirmModal';
import type { MarketPublishAction } from './types';
import { useMarketPublish } from './useMarketPublish';
import { type OriginalAgentInfo, useMarketPublish } from './useMarketPublish';
interface MarketPublishButtonProps {
action: MarketPublishAction;
marketIdentifier?: string;
onPublishSuccess?: (identifier: string) => void;
}
const PublishButton = memo<MarketPublishButtonProps>(
({ action, marketIdentifier, onPublishSuccess }) => {
const { t } = useTranslation(['setting', 'marketAuth']);
const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess }) => {
const { t } = useTranslation(['setting', 'marketAuth']);
const mobile = useServerConfigStore((s) => s.isMobile);
const mobile = useServerConfigStore((s) => s.isMobile);
const { isAuthenticated, isLoading, session, signIn } = useMarketAuth();
const { isPublishing, publish } = useMarketPublish({
action,
onSuccess: onPublishSuccess,
});
const { isAuthenticated, isLoading, signIn } = useMarketAuth();
const { checkOwnership, isCheckingOwnership, isPublishing, publish } = useMarketPublish({
action,
onSuccess: onPublishSuccess,
});
const buttonConfig = useMemo(() => {
if (action === 'upload') {
return {
authSuccessMessage: t('messages.success.upload', { ns: 'marketAuth' }),
authenticated: t('marketPublish.upload.tooltip'),
unauthenticated: t('marketPublish.upload.tooltip'),
} as const;
}
const submitText = t('submitAgentModal.tooltips');
// Fork confirmation modal state
const [showForkModal, setShowForkModal] = useState(false);
const [originalAgentInfo, setOriginalAgentInfo] = useState<OriginalAgentInfo | null>(null);
const buttonConfig = useMemo(() => {
if (action === 'upload') {
return {
authSuccessMessage: t('messages.success.submit', { ns: 'marketAuth' }),
authenticated: submitText,
unauthenticated: t('marketPublish.submit.tooltip'),
authenticated: t('marketPublish.upload.tooltip'),
unauthenticated: t('marketPublish.upload.tooltip'),
} as const;
}, [action, t]);
}
const handleButtonClick = useCallback(async () => {
if (!isAuthenticated) {
try {
const accountId = await signIn();
// Check ownership after authentication if marketIdentifier exists
if (marketIdentifier && accountId !== null) {
let accessToken = session?.accessToken;
const submitText = t('submitAgentModal.tooltips');
if (!accessToken && typeof window !== 'undefined') {
const storedSession = sessionStorage.getItem('market_auth_session');
if (storedSession) {
try {
const parsed = JSON.parse(storedSession) as { accessToken?: string };
accessToken = parsed.accessToken;
} catch (parseError) {
console.error(
'[MarketPublishButton] Failed to parse stored session:',
parseError,
);
}
}
}
return {
authenticated: submitText,
unauthenticated: t('marketPublish.submit.tooltip'),
} as const;
}, [action, t]);
if (accessToken) {
try {
const isOwner = await checkOwnership({
accessToken,
accountId,
marketIdentifier,
skipCache: true,
});
const doPublish = useCallback(async () => {
// Check ownership before publishing
const { needsForkConfirm, originalAgent } = await checkOwnership();
// If user is not the owner and trying to upload, just return
// The parent component should handle the action switch
if (!isOwner && action === 'upload') {
return;
}
} catch (ownershipError) {
console.error('[MarketPublishButton] Failed to confirm ownership:', ownershipError);
}
}
}
if (needsForkConfirm && originalAgent) {
// Show fork confirmation modal
setOriginalAgentInfo(originalAgent);
setShowForkModal(true);
return;
}
// After authentication, proceed with publish
await publish();
} catch (error) {
console.error(`[MarketPublishButton][${action}] Authorization failed:`, error);
const normalizedError = resolveMarketAuthError(error);
message.error({
content: t(`errors.${normalizedError.code}`, { ns: 'marketAuth' }),
key: 'market-auth',
});
}
return;
// No confirmation needed, proceed with publish
await publish();
}, [checkOwnership, publish]);
const handleButtonClick = useCallback(async () => {
if (!isAuthenticated) {
try {
await signIn();
// After authentication, proceed with ownership check and publish
await doPublish();
} catch (error) {
console.error(`[MarketPublishButton][${action}] Authorization failed:`, error);
const normalizedError = resolveMarketAuthError(error);
message.error({
content: t(`errors.${normalizedError.code}`, { ns: 'marketAuth' }),
key: 'market-auth',
});
}
return;
}
// User is authenticated, directly publish
await publish();
}, [
action,
buttonConfig.authSuccessMessage,
isAuthenticated,
marketIdentifier,
publish,
session?.accessToken,
signIn,
t,
]);
// User is authenticated, check ownership and publish
await doPublish();
}, [action, doPublish, isAuthenticated, signIn, t]);
const buttonTitle = isAuthenticated ? buttonConfig.authenticated : buttonConfig.unauthenticated;
const loading = isLoading || isPublishing;
const handleForkConfirm = useCallback(async () => {
setShowForkModal(false);
setOriginalAgentInfo(null);
// User confirmed, proceed with publish
await publish();
}, [publish]);
return (
const handleForkCancel = useCallback(() => {
setShowForkModal(false);
setOriginalAgentInfo(null);
}, []);
const buttonTitle = isAuthenticated ? buttonConfig.authenticated : buttonConfig.unauthenticated;
const loading = isLoading || isCheckingOwnership || isPublishing;
return (
<>
<ActionIcon
icon={ShapesUploadIcon}
loading={loading}
@@ -129,9 +109,16 @@ const PublishButton = memo<MarketPublishButtonProps>(
size={HEADER_ICON_SIZE(mobile)}
title={buttonTitle}
/>
);
},
);
<ForkConfirmModal
loading={isPublishing}
onCancel={handleForkCancel}
onConfirm={handleForkConfirm}
open={showForkModal}
originalAgent={originalAgentInfo}
/>
</>
);
});
PublishButton.displayName = 'MarketPublishButton';

View File

@@ -1,20 +1,22 @@
import { ActionIcon } from '@lobehub/ui';
import isEqual from 'fast-deep-equal';
import { Loader2 } from 'lucide-react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { memo, useCallback, useState } from 'react';
import { useAgentOwnershipCheck } from '@/hooks/useAgentOwnershipCheck';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import PublishButton from './PublishButton';
import PublishResultModal from './PublishResultModal';
/**
* Agent Publish Button Component
*
* Simplified version - backend now handles ownership check automatically.
* The action type (submit vs upload) is determined by backend based on:
* 1. Whether the identifier exists
* 2. Whether the current user is the owner
*/
const AgentPublishButton = memo(() => {
const { t } = useTranslation('setting');
const meta = useAgentStore(agentSelectors.currentAgentMeta, isEqual);
const { isOwnAgent } = useAgentOwnershipCheck(meta?.marketIdentifier);
const [showResultModal, setShowResultModal] = useState(false);
const [publishedIdentifier, setPublishedIdentifier] = useState<string>();
@@ -24,50 +26,13 @@ const AgentPublishButton = memo(() => {
setShowResultModal(true);
}, []);
const buttonType = useMemo(() => {
if (!meta?.marketIdentifier) {
return 'submit';
}
if (isOwnAgent === null) {
return 'loading';
}
if (isOwnAgent === true) {
return 'upload';
}
return 'submit';
}, [meta?.marketIdentifier, isOwnAgent]);
const content = useMemo(() => {
switch (buttonType) {
case 'upload': {
return (
<PublishButton
action="upload"
marketIdentifier={meta?.marketIdentifier}
onPublishSuccess={handlePublishSuccess}
/>
);
}
case 'submit': {
return (
<PublishButton
action="submit"
marketIdentifier={meta?.marketIdentifier}
onPublishSuccess={handlePublishSuccess}
/>
);
}
default: {
return <ActionIcon disabled icon={Loader2} loading title={t('checkingPermissions')} />;
}
}
}, [buttonType, meta?.marketIdentifier]);
// Determine action based on whether we have an existing marketIdentifier
// Backend will verify ownership and decide to create new or update
const action = meta?.marketIdentifier ? 'upload' : 'submit';
return (
<>
{content}
<PublishButton action={action} onPublishSuccess={handlePublishSuccess} />
<PublishResultModal
identifier={publishedIdentifier}
onCancel={() => setShowResultModal(false)}

View File

@@ -3,32 +3,45 @@ import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { message } from '@/components/AntdStaticMethods';
import { checkOwnership } from '@/hooks/useAgentOwnershipCheck';
import { useTokenCount } from '@/hooks/useTokenCount';
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
import { marketApiService } from '@/services/marketApi';
import { lambdaClient } from '@/libs/trpc/client';
import { useAgentStore } from '@/store/agent';
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
import { useGlobalStore } from '@/store/global';
import { globalGeneralSelectors } from '@/store/global/selectors';
import { useServerConfigStore } from '@/store/serverConfig';
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
import type { MarketPublishAction } from './types';
import { generateDefaultChangelog, generateMarketIdentifier } from './utils';
import { generateDefaultChangelog } from './utils';
export interface OriginalAgentInfo {
author?: {
avatar?: string;
name?: string;
userName?: string;
};
avatar?: string;
identifier: string;
name: string;
}
interface UseMarketPublishOptions {
action: MarketPublishAction;
onSuccess?: (identifier: string) => void;
}
export interface CheckOwnershipResult {
needsForkConfirm: boolean;
originalAgent: OriginalAgentInfo | null;
}
export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions) => {
const { t } = useTranslation('setting');
const [isPublishing, setIsPublishing] = useState(false);
const [isCheckingOwnership, setIsCheckingOwnership] = useState(false);
// 使用 ref 来同步跟踪发布状态,避免闭包导致的竞态问题
const isPublishingRef = useRef(false);
const { isAuthenticated, session, getCurrentUserInfo } = useMarketAuth();
const enableMarketTrustedClient = useServerConfigStore(serverConfigSelectors.enableMarketTrustedClient);
const { isAuthenticated } = useMarketAuth();
// Agent data from store
const meta = useAgentStore(agentSelectors.currentAgentMeta, isEqual);
@@ -46,15 +59,49 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
const isSubmit = action === 'submit';
/**
* Check ownership before publishing
* Returns whether fork confirmation is needed and original agent info
*/
const checkOwnership = useCallback(async (): Promise<CheckOwnershipResult> => {
const identifier = meta?.marketIdentifier;
// No identifier means new agent, no need to check
if (!identifier) {
return { needsForkConfirm: false, originalAgent: null };
}
try {
setIsCheckingOwnership(true);
const result = await lambdaClient.market.agent.checkOwnership.query({ identifier });
// If agent doesn't exist or user is owner, no confirmation needed
if (!result.exists || result.isOwner) {
return { needsForkConfirm: false, originalAgent: null };
}
// User is not owner, need fork confirmation
return {
needsForkConfirm: true,
originalAgent: result.originalAgent as OriginalAgentInfo,
};
} catch (error) {
console.error('[useMarketPublish] Failed to check ownership:', error);
// On error, proceed without confirmation
return { needsForkConfirm: false, originalAgent: null };
} finally {
setIsCheckingOwnership(false);
}
}, [meta?.marketIdentifier]);
const publish = useCallback(async () => {
// 防止重复发布:使用 ref 同步检查,避免闭包导致的竞态问题
if (isPublishingRef.current) {
return { success: false };
}
// 如果启用了 trustedClient只需要检查 isAuthenticated
// 因为后端会自动注入 trustedClientToken
if (!isAuthenticated || (!enableMarketTrustedClient && !session?.accessToken)) {
// 检查认证状态 - tRPC 会自动处理 trustedClient
if (!isAuthenticated) {
return { success: false };
}
@@ -63,8 +110,6 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
? t('marketPublish.modal.loading.submit')
: t('marketPublish.modal.loading.upload');
let identifier = meta?.marketIdentifier;
const changelog = generateDefaultChangelog();
try {
@@ -72,61 +117,9 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
isPublishingRef.current = true;
setIsPublishing(true);
message.loading({ content: loadingMessage, key: messageKey });
// 只有在非 trustedClient 模式下才需要设置 accessToken
if (session?.accessToken) {
marketApiService.setAccessToken(session.accessToken);
}
// 判断是否需要创建新 agent
let needsCreateAgent = false;
if (!identifier) {
// 没有 marketIdentifier需要创建新 agent
needsCreateAgent = true;
} else if (isSubmit) {
// 有 marketIdentifier 且是 submit 操作,需要检查是否是自己的 agent
const userInfo = getCurrentUserInfo?.() ?? session?.userInfo;
const accountId = userInfo?.accountId;
if (accountId) {
const isOwner = await checkOwnership({
accessToken: session?.accessToken,
accountId,
enableMarketTrustedClient,
marketIdentifier: identifier,
});
if (!isOwner) {
// 不是自己的 agent需要创建新的
needsCreateAgent = true;
}
} else {
// 无法获取用户 ID为安全起见创建新 agent
needsCreateAgent = true;
}
}
if (needsCreateAgent) {
identifier = generateMarketIdentifier();
try {
await marketApiService.getAgentDetail(identifier);
} catch {
const createPayload: Record<string, unknown> = {
identifier,
name: meta?.title || '',
};
await marketApiService.createAgent(createPayload as any);
}
} else if (!identifier) {
message.error({
content: t('marketPublish.modal.messages.missingIdentifier'),
key: messageKey,
});
return { success: false };
}
const versionPayload = {
// 使用 tRPC publishOrCreate - 后端会自动处理 ownership 检查
const result = await lambdaClient.market.agent.publishOrCreate.mutate({
avatar: meta?.avatar,
changelog,
config: {
@@ -157,31 +150,16 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
},
description: meta?.description || '',
editorData: editorData,
identifier: identifier,
// 传递现有的 identifier后端会检查 ownership
identifier: meta?.marketIdentifier,
name: meta?.title || '',
tags: meta?.tags,
tokenUsage: tokenUsage,
};
});
try {
await marketApiService.createAgentVersion(versionPayload);
} catch (versionError) {
const errorMessage =
versionError instanceof Error
? versionError.message
: t('unknownError', { ns: 'common' });
message.error({
content: t('marketPublish.modal.messages.createVersionFailed', {
message: errorMessage,
}),
key: messageKey,
});
return { success: false };
}
// 只有在首次创建 agent 时才需要更新 meta
if (needsCreateAgent) {
updateAgentMeta({ marketIdentifier: identifier });
// 如果是新创建的 agent需要更新 meta
if (result.isNewAgent) {
updateAgentMeta({ marketIdentifier: result.identifier });
}
message.success({
@@ -189,8 +167,8 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
key: messageKey,
});
onSuccess?.(identifier!);
return { identifier, success: true };
onSuccess?.(result.identifier);
return { identifier: result.identifier, success: true };
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : t('unknownError', { ns: 'common' });
@@ -211,8 +189,6 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
chatConfig?.historyCount,
chatConfig?.searchMode,
editorData,
enableMarketTrustedClient,
getCurrentUserInfo,
isAuthenticated,
isSubmit,
language,
@@ -225,8 +201,6 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
onSuccess,
plugins,
provider,
session?.accessToken,
session?.userInfo,
systemRole,
tokenUsage,
t,
@@ -234,6 +208,8 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
]);
return {
checkOwnership,
isCheckingOwnership,
isPublishing,
publish,
};

View File

@@ -5,7 +5,8 @@ import { type ReactNode, createContext, useCallback, useContext, useEffect, useS
import { useTranslation } from 'react-i18next';
import { mutate as globalMutate } from 'swr';
import { MARKET_ENDPOINTS, MARKET_OIDC_ENDPOINTS } from '@/services/_url';
import { lambdaClient } from '@/libs/trpc/client';
import { MARKET_OIDC_ENDPOINTS } from '@/services/_url';
import { useServerConfigStore } from '@/store/serverConfig';
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
import { useUserStore } from '@/store/user';
@@ -32,31 +33,16 @@ interface MarketAuthProviderProps {
}
/**
* 获取用户信息(从 OIDC userinfo endpoint
* 获取用户信息(通过 tRPC OIDC endpoint
* @param accessToken - 可选的 access token如果不传则后端会尝试使用 trustedClientToken
*/
const fetchUserInfo = async (accessToken?: string): Promise<MarketUserInfo | null> => {
try {
const response = await fetch(MARKET_OIDC_ENDPOINTS.userinfo, {
body: JSON.stringify({ token: accessToken }),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
const userInfo = await lambdaClient.market.oidc.getUserInfo.mutate({
token: accessToken,
});
if (!response.ok) {
console.error(
'[MarketAuth] Failed to fetch user info:',
response.status,
response.statusText,
);
return null;
}
const userInfo = (await response.json()) as MarketUserInfo;
return userInfo;
return userInfo as MarketUserInfo;
} catch (error) {
console.error('[MarketAuth] Error fetching user info:', error);
return null;
@@ -136,16 +122,11 @@ const refreshToken = async (): Promise<boolean> => {
*/
const checkNeedsProfileSetup = async (username: string): Promise<boolean> => {
try {
const response = await fetch(MARKET_ENDPOINTS.getUserProfile(username));
if (!response.ok) {
// User profile not found, needs setup
return true;
}
const profile = (await response.json()) as MarketUserProfile;
const profile = await lambdaClient.market.user.getUserByUsername.query({ username });
// If userName is not set, user needs to complete profile setup
return !profile.userName;
} catch {
// Error fetching profile, assume needs setup
// Error fetching profile (e.g., NOT_FOUND), assume needs setup
return true;
}
};
@@ -177,7 +158,9 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
const isUserStateInit = useUserStore((s) => s.isUserStateInit);
// 检查是否启用了 Market Trusted Client 认证
const enableMarketTrustedClient = useServerConfigStore(serverConfigSelectors.enableMarketTrustedClient);
const enableMarketTrustedClient = useServerConfigStore(
serverConfigSelectors.enableMarketTrustedClient,
);
// 初始化 OIDC 客户端(仅在客户端)
useEffect(() => {

View File

@@ -9,7 +9,7 @@ import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import EmojiPicker from '@/components/EmojiPicker';
import { MARKET_ENDPOINTS } from '@/services/_url';
import { lambdaClient } from '@/libs/trpc/client';
import { useFileStore } from '@/store/file';
import { useGlobalStore } from '@/store/global';
import { globalGeneralSelectors } from '@/store/global/selectors';
@@ -211,37 +211,42 @@ const ProfileSetupModal = memo<ProfileSetupModalProps>(
if (bannerUrl) meta.bannerUrl = bannerUrl;
if (Object.keys(socialLinks).length > 0) meta.socialLinks = socialLinks;
const response = await fetch(MARKET_ENDPOINTS.updateUserProfile, {
body: JSON.stringify({
avatarUrl: avatarUrl || undefined,
displayName: values.displayName,
meta: Object.keys(meta).length > 0 ? meta : undefined,
userName: values.userName,
}),
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
method: 'PUT',
const result = await lambdaClient.market.user.updateUserProfile.mutate({
avatarUrl: avatarUrl || undefined,
displayName: values.displayName,
meta: Object.keys(meta).length > 0 ? meta : undefined,
userName: values.userName,
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.error === 'username_taken') {
message.error(t('profileSetup.errors.usernameTaken'));
return;
}
throw new Error(errorData.message || 'Update failed');
}
const data = await response.json();
message.success(t('profileSetup.success'));
onSuccess?.(data.user);
// Cast result.user to MarketUserProfile with required fields
const userProfile: MarketUserProfile = {
avatarUrl: result.user?.avatarUrl || avatarUrl || null,
bannerUrl: bannerUrl || null,
createdAt: result.user?.createdAt || new Date().toISOString(),
description: values.description || null,
displayName: values.displayName || null,
id: result.user?.id || 0,
namespace: result.user?.namespace || '',
socialLinks: Object.keys(socialLinks).length > 0 ? socialLinks : null,
type: result.user?.type || null,
userName: values.userName || null,
};
onSuccess?.(userProfile);
onClose();
} catch (error) {
console.error('[ProfileSetupModal] Update failed:', error);
if (error instanceof Error && error.message !== 'Validation failed') {
message.error(t('profileSetup.errors.updateFailed'));
// Check for username taken error (tRPC CONFLICT code)
const errorMessage = error.message || '';
if (
errorMessage.toLowerCase().includes('already taken') ||
errorMessage.includes('CONFLICT')
) {
message.error(t('profileSetup.errors.usernameTaken'));
} else {
message.error(t('profileSetup.errors.updateFailed'));
}
}
} finally {
setLoading(false);

View File

@@ -1,20 +1,15 @@
import useSWR from 'swr';
import { MARKET_ENDPOINTS } from '@/services/_url';
import { lambdaClient } from '@/libs/trpc/client';
import { type MarketUserProfile } from './types';
/**
* Fetcher function for user profile
* Fetcher function for user profile using tRPC
*/
const fetchUserProfile = async (username: string): Promise<MarketUserProfile | null> => {
const response = await fetch(MARKET_ENDPOINTS.getUserProfile(username));
if (!response.ok) {
throw new Error(`Failed to fetch user profile: ${response.status}`);
}
return response.json();
const result = await lambdaClient.market.user.getUserByUsername.query({ username });
return result as MarketUserProfile;
};
/**

View File

@@ -1,4 +1,5 @@
export * from './keyVaults';
export * from './marketSDK';
export * from './marketUserInfo';
export * from './serverDatabase';
export * from './telemetry';

View File

@@ -0,0 +1,68 @@
import { MarketSDK } from '@lobehub/market-sdk';
import { generateTrustedClientToken, type TrustedClientUserInfo } from '@/libs/trusted-client';
import { trpc } from '../init';
interface ContextWithMarketUserInfo {
marketAccessToken?: string;
marketUserInfo?: TrustedClientUserInfo;
}
/**
* Middleware that initializes MarketSDK with proper authentication.
* This requires marketUserInfo middleware to be applied first.
*
* Provides:
* - ctx.marketSDK: Initialized MarketSDK instance with trustedClientToken and optional accessToken
* - ctx.trustedClientToken: The generated trusted client token (if available)
*/
export const marketSDK = trpc.middleware(async (opts) => {
const ctx = opts.ctx as ContextWithMarketUserInfo;
// Generate trusted client token if user info is available
const trustedClientToken = ctx.marketUserInfo
? generateTrustedClientToken(ctx.marketUserInfo)
: undefined;
// Initialize MarketSDK with both authentication methods
const market = new MarketSDK({
accessToken: ctx.marketAccessToken,
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
trustedClientToken,
});
return opts.next({
ctx: {
marketSDK: market,
trustedClientToken,
},
});
});
/**
* Middleware that requires authentication for Market API access.
* This middleware ensures that either accessToken or trustedClientToken is available.
* It should be used after marketUserInfo and marketSDK middlewares.
*
* Throws UNAUTHORIZED error if neither authentication method is available.
*/
export const requireMarketAuth = trpc.middleware(async (opts) => {
const ctx = opts.ctx as ContextWithMarketUserInfo & {
trustedClientToken?: string;
};
// Check if any authentication is available
const hasAccessToken = !!ctx.marketAccessToken;
const hasTrustedToken = !!ctx.trustedClientToken;
if (!hasAccessToken && !hasTrustedToken) {
const { TRPCError } = await import('@trpc/server');
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Authentication required. Please sign in.',
});
}
return opts.next();
});

View File

@@ -140,6 +140,11 @@ export default {
'llm.proxyUrl.title': 'API proxy URL',
'llm.waitingForMore': 'More models are <1>planned to be added</1>, stay tuned',
'llm.waitingForMoreLinkAriaLabel': 'Open the Provider request form',
'marketPublish.forkConfirm.by': 'by {{author}}',
'marketPublish.forkConfirm.confirm': 'Confirm Publish',
'marketPublish.forkConfirm.description':
'You are about to publish a derivative version based on an existing agent from the community. Your new agent will be created as a separate entry in the marketplace.',
'marketPublish.forkConfirm.title': 'Publish Derivative Agent',
'marketPublish.modal.changelog.extra':
'Describe the key changes and improvements in this version',
'marketPublish.modal.changelog.label': 'Changelog',

View File

@@ -0,0 +1,504 @@
import { TRPCError } from '@trpc/server';
import debug from 'debug';
import { customAlphabet } from 'nanoid/non-secure';
import { z } from 'zod';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { marketSDK, marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
import { type TrustedClientUserInfo, generateTrustedClientToken } from '@/libs/trusted-client';
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
interface MarketUserInfo {
accountId: number;
sub: string;
}
const log = debug('lambda-router:market:agent');
/**
* Generate a market identifier (8-character lowercase alphanumeric string)
* Format: [a-z0-9]{8}
*/
const generateMarketIdentifier = () => {
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
const generate = customAlphabet(alphabet, 8);
return generate();
};
interface FetchMarketUserInfoOptions {
accessToken?: string;
userInfo?: TrustedClientUserInfo;
}
/**
* Fetch Market user info using either trustedClientToken or accessToken
* Returns the Market accountId which is different from LobeChat userId
*
* Priority:
* 1. trustedClientToken (if userInfo is provided and TRUSTED_CLIENT_SECRET is configured)
* 2. accessToken (if provided, from OIDC flow)
*/
const fetchMarketUserInfo = async (
options: FetchMarketUserInfoOptions,
): Promise<MarketUserInfo | null> => {
const { userInfo, accessToken } = options;
try {
const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// Try trustedClientToken first (if userInfo is available)
if (userInfo) {
const trustedClientToken = generateTrustedClientToken(userInfo);
if (trustedClientToken) {
headers['x-lobe-trust-token'] = trustedClientToken;
log('Using trustedClientToken for user info fetch');
}
}
// Fall back to accessToken if no trustedClientToken
if (!headers['x-lobe-trust-token'] && accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
log('Using accessToken for user info fetch');
}
// If neither authentication method is available, return null
if (!headers['x-lobe-trust-token'] && !headers['Authorization']) {
log('No authentication method available for fetching user info');
return null;
}
const response = await fetch(userInfoUrl, {
headers,
method: 'GET',
});
if (!response.ok) {
log('Failed to fetch Market user info: %s %s', response.status, response.statusText);
return null;
}
return (await response.json()) as MarketUserInfo;
} catch (error) {
log('Error fetching Market user info: %O', error);
return null;
}
};
// Authenticated procedure for agent management
// Requires user to be logged in and has MarketSDK initialized
// Also fetches user's market accessToken from database for OIDC authentication
const agentProcedure = authedProcedure
.use(serverDatabase)
.use(marketUserInfo)
.use(marketSDK)
.use(async ({ ctx, next }) => {
// Import UserModel dynamically to avoid circular dependencies
const { UserModel } = await import('@/database/models/user');
const userModel = new UserModel(ctx.serverDB, ctx.userId);
// Get user's market accessToken from database (stored by MarketAuthProvider after OIDC login)
let marketOidcAccessToken: string | undefined;
try {
const userState = await userModel.getUserState(async () => ({}));
marketOidcAccessToken = userState.settings?.market?.accessToken;
log('marketOidcAccessToken from DB exists=%s', !!marketOidcAccessToken);
} catch (error) {
log('Failed to get marketOidcAccessToken from DB: %O', error);
}
return next({
ctx: {
marketOidcAccessToken,
},
});
});
// Schema definitions
const createAgentSchema = z.object({
homepage: z.string().optional(),
identifier: z.string(),
isFeatured: z.boolean().optional(),
name: z.string(),
status: z.enum(['published', 'unpublished', 'archived', 'deprecated']).optional(),
tokenUsage: z.number().optional(),
visibility: z.enum(['public', 'private', 'internal']).optional(),
});
const createAgentVersionSchema = z.object({
a2aProtocolVersion: z.string().optional(),
avatar: z.string().optional(),
category: z.string().optional(),
changelog: z.string().optional(),
config: z.record(z.any()).optional(),
defaultInputModes: z.array(z.string()).optional(),
defaultOutputModes: z.array(z.string()).optional(),
description: z.string().optional(),
documentationUrl: z.string().optional(),
extensions: z.array(z.record(z.any())).optional(),
hasPushNotifications: z.boolean().optional(),
hasStateTransitionHistory: z.boolean().optional(),
hasStreaming: z.boolean().optional(),
identifier: z.string(),
interfaces: z.array(z.record(z.any())).optional(),
name: z.string().optional(),
preferredTransport: z.string().optional(),
providerId: z.number().optional(),
securityRequirements: z.array(z.record(z.any())).optional(),
securitySchemes: z.record(z.any()).optional(),
setAsCurrent: z.boolean().optional(),
summary: z.string().optional(),
supportsAuthenticatedExtendedCard: z.boolean().optional(),
tokenUsage: z.number().optional(),
url: z.string().optional(),
});
const paginationSchema = z.object({
page: z.number().optional(),
pageSize: z.number().optional(),
});
// Schema for the unified publish/create flow
const publishOrCreateSchema = z.object({
// Version data
avatar: z.string().optional(),
category: z.string().optional(),
changelog: z.string().optional(),
config: z.record(z.any()).optional(),
description: z.string().optional(),
editorData: z.record(z.any()).optional(),
// Agent basic info
identifier: z.string().optional(),
// Optional - if not provided or not owned, will create new
name: z.string(),
tags: z.array(z.string()).optional(),
tokenUsage: z.number().optional(),
});
export const agentRouter = router({
/**
* Check if current user owns the specified agent
* Returns ownership status and original agent info for fork scenario
*/
checkOwnership: agentProcedure
.input(z.object({ identifier: z.string() }))
.query(async ({ input, ctx }) => {
log('checkOwnership input: %O', input);
try {
// Get agent detail
const agentDetail = await ctx.marketSDK.agents.getAgentDetail(input.identifier);
if (!agentDetail) {
return {
exists: false,
isOwner: false,
originalAgent: null,
};
}
// Get Market user info to get accountId
// Support both trustedClientToken and OIDC accessToken authentication
const userInfo = ctx.marketUserInfo as TrustedClientUserInfo | undefined;
const accessToken = (ctx as { marketOidcAccessToken?: string }).marketOidcAccessToken;
let currentAccountId: number | null = null;
const marketUserInfoResult = await fetchMarketUserInfo({ accessToken, userInfo });
currentAccountId = marketUserInfoResult?.accountId ?? null;
const ownerId = agentDetail.ownerId;
const isOwner = currentAccountId !== null && `${ownerId}` === `${currentAccountId}`;
log(
'checkOwnership result: isOwner=%s, currentAccountId=%s, ownerId=%s',
isOwner,
currentAccountId,
ownerId,
);
return {
exists: true,
isOwner,
originalAgent: isOwner
? null
: {
author: agentDetail.author,
avatar: agentDetail.avatar,
identifier: agentDetail.identifier,
name: agentDetail.name,
},
};
} catch (error) {
log('Error checking ownership: %O', error);
// If agent not found or error, treat as not existing
return {
exists: false,
isOwner: false,
originalAgent: null,
};
}
}),
/**
* Create a new agent in the marketplace
* POST /market/agent/create
*/
createAgent: agentProcedure.input(createAgentSchema).mutation(async ({ input, ctx }) => {
log('createAgent input: %O', input);
try {
const response = await ctx.marketSDK.agents.createAgent(input);
return response;
} catch (error) {
log('Error creating agent: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to create agent',
});
}
}),
/**
* Create a new version for an existing agent
* POST /market/agent/versions/create
*/
createAgentVersion: agentProcedure
.input(createAgentVersionSchema)
.mutation(async ({ input, ctx }) => {
log('createAgentVersion input: %O', input);
try {
const response = await ctx.marketSDK.agents.createAgentVersion(input);
return response;
} catch (error) {
log('Error creating agent version: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to create agent version',
});
}
}),
/**
* Deprecate an agent (permanently hide, cannot be republished)
* POST /market/agent/:identifier/deprecate
*/
deprecateAgent: agentProcedure
.input(z.object({ identifier: z.string() }))
.mutation(async ({ input, ctx }) => {
log('deprecateAgent input: %O', input);
try {
const response = await ctx.marketSDK.agents.deprecate(input.identifier);
return response ?? { success: true };
} catch (error) {
log('Error deprecating agent: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to deprecate agent',
});
}
}),
/**
* Get agent detail by identifier
* GET /market/agent/:identifier
*/
getAgentDetail: agentProcedure
.input(z.object({ identifier: z.string() }))
.query(async ({ input, ctx }) => {
log('getAgentDetail input: %O', input);
try {
const response = await ctx.marketSDK.agents.getAgentDetail(input.identifier);
return response;
} catch (error) {
log('Error getting agent detail: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get agent detail',
});
}
}),
/**
* Get own agents (requires authentication)
* GET /market/agent/own
*/
getOwnAgents: agentProcedure.input(paginationSchema.optional()).query(async ({ input, ctx }) => {
log('getOwnAgents input: %O', input);
try {
const response = await ctx.marketSDK.agents.getOwnAgents({
page: input?.page,
pageSize: input?.pageSize,
});
return response;
} catch (error) {
log('Error getting own agents: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get own agents',
});
}
}),
/**
* Publish an agent (make it visible in marketplace)
* POST /market/agent/:identifier/publish
*/
publishAgent: agentProcedure
.input(z.object({ identifier: z.string() }))
.mutation(async ({ input, ctx }) => {
log('publishAgent input: %O', input);
try {
const response = await ctx.marketSDK.agents.publish(input.identifier);
return response ?? { success: true };
} catch (error) {
log('Error publishing agent: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to publish agent',
});
}
}),
/**
* Unified publish or create agent flow
* This procedure handles the complete publish logic:
* 1. Check if identifier exists and if current user is owner
* 2. If not owner or no identifier, create new agent
* 3. Create new version for the agent
*
* Returns: { identifier, isNewAgent, success }
*/
publishOrCreate: agentProcedure.input(publishOrCreateSchema).mutation(async ({ input, ctx }) => {
log('publishOrCreate input: %O', input);
const { identifier: inputIdentifier, name, ...versionData } = input;
let finalIdentifier = inputIdentifier;
let isNewAgent = false;
try {
// Step 1: Check ownership if identifier is provided
if (inputIdentifier) {
try {
const agentDetail = await ctx.marketSDK.agents.getAgentDetail(inputIdentifier);
log('Agent detail for ownership check: ownerId=%s', agentDetail?.ownerId);
// Get Market user info to get accountId (Market's user ID)
// Support both trustedClientToken and OIDC accessToken authentication
const userInfo = ctx.marketUserInfo as TrustedClientUserInfo | undefined;
const accessToken = (ctx as { marketOidcAccessToken?: string }).marketOidcAccessToken;
let currentAccountId: number | null = null;
const marketUserInfoResult = await fetchMarketUserInfo({ accessToken, userInfo });
currentAccountId = marketUserInfoResult?.accountId ?? null;
log('Market user info: accountId=%s', currentAccountId);
const ownerId = agentDetail?.ownerId;
log('Ownership check: currentAccountId=%s, ownerId=%s', currentAccountId, ownerId);
if (!currentAccountId || `${ownerId}` !== `${currentAccountId}`) {
// Not the owner, need to create a new agent
log('User is not owner, will create new agent');
finalIdentifier = undefined;
isNewAgent = true;
}
} catch (detailError) {
// Agent not found or error, create new
log('Agent not found or error, will create new: %O', detailError);
finalIdentifier = undefined;
isNewAgent = true;
}
} else {
isNewAgent = true;
}
// Step 2: Create new agent if needed
if (!finalIdentifier) {
// Generate a unique 8-character identifier
finalIdentifier = generateMarketIdentifier();
isNewAgent = true;
log('Creating new agent with identifier: %s', finalIdentifier);
await ctx.marketSDK.agents.createAgent({
identifier: finalIdentifier,
name,
});
}
// Step 3: Create version for the agent
log('Creating version for agent: %s', finalIdentifier);
await ctx.marketSDK.agents.createAgentVersion({
...versionData,
identifier: finalIdentifier,
name,
});
return {
identifier: finalIdentifier,
isNewAgent,
success: true,
};
} catch (error) {
log('Error in publishOrCreate: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to publish agent',
});
}
}),
/**
* Unpublish an agent (hide from marketplace, can be republished)
* POST /market/agent/:identifier/unpublish
*/
unpublishAgent: agentProcedure
.input(z.object({ identifier: z.string() }))
.mutation(async ({ input, ctx }) => {
log('unpublishAgent input: %O', input);
try {
const response = await ctx.marketSDK.agents.unpublish(input.identifier);
return response ?? { success: true };
} catch (error) {
log('Error unpublishing agent: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to unpublish agent',
});
}
}),
});
export type AgentRouter = typeof agentRouter;

View File

@@ -16,6 +16,11 @@ import {
ProviderSorts,
} from '@/types/discover';
import { agentRouter } from './agent';
import { oidcRouter } from './oidc';
import { socialRouter } from './social';
import { userRouter } from './user';
const log = debug('lambda-router:market');
const marketSourceSchema = z.enum(['legacy', 'new']);
@@ -36,6 +41,9 @@ const marketProcedure = publicProcedure
});
export const marketRouter = router({
// ============================== Agent Management (authenticated) ==============================
agent: agentRouter,
// ============================== Assistant Market ==============================
getAssistantCategories: marketProcedure
.input(
@@ -516,6 +524,9 @@ export const marketRouter = router({
}
}),
// ============================== OIDC Authentication ==============================
oidc: oidcRouter,
registerClientInMarketplace: marketProcedure.input(z.object({})).mutation(async ({ ctx }) => {
return ctx.discoverService.registerClient({
userAgent: ctx.userAgent,
@@ -671,4 +682,10 @@ export const marketRouter = router({
return { success: false };
}
}),
// ============================== Social Features ==============================
social: socialRouter,
// ============================== User Profile ==============================
user: userRouter,
});

View File

@@ -0,0 +1,169 @@
import { MarketSDK } from '@lobehub/market-sdk';
import { TRPCError } from '@trpc/server';
import debug from 'debug';
import { z } from 'zod';
import { publicProcedure, router } from '@/libs/trpc/lambda';
import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
import { generateTrustedClientToken } from '@/libs/trusted-client';
const log = debug('lambda-router:market:oidc');
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
// OIDC procedures are public (used during authentication flow)
const oidcProcedure = publicProcedure.use(serverDatabase).use(marketUserInfo);
export const oidcRouter = router({
/**
* Exchange OAuth code for tokens
* POST /market/oidc/token (with grant_type=authorization_code)
*/
exchangeAuthorizationCode: oidcProcedure
.input(
z.object({
clientId: z.string(),
code: z.string(),
codeVerifier: z.string(),
redirectUri: z.string(),
}),
)
.mutation(async ({ input }) => {
log('exchangeAuthorizationCode input: %O', { ...input, code: '[REDACTED]' });
const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
try {
const response = await market.auth.exchangeOAuthToken({
clientId: input.clientId,
code: input.code,
codeVerifier: input.codeVerifier,
grantType: 'authorization_code',
redirectUri: input.redirectUri,
});
return response;
} catch (error) {
log('Error exchanging authorization code: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to exchange authorization code',
});
}
}),
/**
* Get OAuth handoff information
* GET /market/oidc/handoff?id=xxx
*/
getOAuthHandoff: oidcProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
log('getOAuthHandoff input: %O', input);
const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
try {
const handoff = await market.auth.getOAuthHandoff(input.id);
return handoff;
} catch (error) {
log('Error getting OAuth handoff: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get OAuth handoff',
});
}
}),
/**
* Get user info from token or trusted client
* POST /market/oidc/userinfo
*/
getUserInfo: oidcProcedure
.input(z.object({ token: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
log('getUserInfo input: token=%s', input.token ? '[REDACTED]' : 'undefined');
const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
try {
// If token is provided, use it
if (input.token) {
const response = await market.auth.getUserInfo(input.token);
return response;
}
// Otherwise, try to use trustedClientToken
if (ctx.marketUserInfo) {
const trustedClientToken = generateTrustedClientToken(ctx.marketUserInfo);
if (trustedClientToken) {
const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
const response = await fetch(userInfoUrl, {
headers: {
'Content-Type': 'application/json',
'x-lobe-trust-token': trustedClientToken,
},
method: 'GET',
});
if (!response.ok) {
throw new Error(
`Failed to fetch user info: ${response.status} ${response.statusText}`,
);
}
return await response.json();
}
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Token is required for userinfo',
});
} catch (error) {
if (error instanceof TRPCError) throw error;
log('Error getting user info: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get user info',
});
}
}),
/**
* Refresh access token
* POST /market/oidc/token (with grant_type=refresh_token)
*/
refreshToken: oidcProcedure
.input(
z.object({
clientId: z.string().optional(),
refreshToken: z.string(),
}),
)
.mutation(async ({ input }) => {
log('refreshToken input: %O', { ...input, refreshToken: '[REDACTED]' });
const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
try {
const response = await market.auth.exchangeOAuthToken({
clientId: input.clientId,
grantType: 'refresh_token',
refreshToken: input.refreshToken,
});
return response;
} catch (error) {
log('Error refreshing token: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to refresh token',
});
}
}),
});
export type OidcRouter = typeof oidcRouter;

View File

@@ -0,0 +1,532 @@
import { TRPCError } from '@trpc/server';
import debug from 'debug';
import { z } from 'zod';
import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
import { marketSDK, marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
const log = debug('lambda-router:market:social');
// Authenticated procedure for social actions that require login
const socialAuthProcedure = authedProcedure.use(serverDatabase).use(marketUserInfo).use(marketSDK);
// Public procedure with optional auth for status checks
const socialPublicProcedure = publicProcedure
.use(serverDatabase)
.use(marketUserInfo)
.use(marketSDK);
// Schema definitions
const targetTypeSchema = z.enum(['agent', 'plugin']);
const paginationSchema = z.object({
limit: z.number().optional(),
offset: z.number().optional(),
});
export const socialRouter = router({
// ============================== Favorite Actions ==============================
/**
* Add to favorites
* POST /market/social/favorite
*/
addFavorite: socialAuthProcedure
.input(
z.object({
identifier: z.string().optional(),
targetId: z.number().optional(),
targetType: targetTypeSchema,
}),
)
.mutation(async ({ input, ctx }) => {
log('addFavorite input: %O', input);
try {
const targetValue = input.identifier ?? input.targetId;
if (!targetValue) {
throw new Error('Either identifier or targetId is required');
}
await ctx.marketSDK.favorites.addFavorite(input.targetType, targetValue as any);
return { success: true };
} catch (error) {
log('Error adding favorite: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to add favorite',
});
}
}),
/**
* Check if item is favorited
* GET /market/social/favorite-status/[targetType]/[targetId]
*/
checkFavorite: socialPublicProcedure
.input(
z.object({
targetIdOrIdentifier: z.union([z.number(), z.string()]),
targetType: targetTypeSchema,
}),
)
.query(async ({ input, ctx }) => {
log('checkFavorite input: %O', input);
if (!ctx.marketSDK) {
return { isFavorited: false };
}
try {
const result = await ctx.marketSDK.favorites.checkFavorite(
input.targetType,
input.targetIdOrIdentifier as any,
);
return result;
} catch (error) {
log('Error checking favorite: %O', error);
return { isFavorited: false };
}
}),
// ============================== Follow Actions ==============================
/**
* Check follow status between current user and target user
* GET /market/social/follow-status/[userId]
*/
checkFollowStatus: socialPublicProcedure
.input(z.object({ targetUserId: z.number() }))
.query(async ({ input, ctx }) => {
log('checkFollowStatus input: %O', input);
// If no auth, return default status
if (!ctx.marketSDK) {
return { isFollowing: false, isMutual: false };
}
try {
const result = await ctx.marketSDK.follows.checkFollowStatus(input.targetUserId);
return result;
} catch (error) {
log('Error checking follow status: %O', error);
// Return default on error (user might not be authenticated)
return { isFollowing: false, isMutual: false };
}
}),
// ============================== Like Actions ==============================
/**
* Check if item is liked
* GET /market/social/like-status/[targetType]/[targetId]
*/
checkLike: socialPublicProcedure
.input(
z.object({
targetIdOrIdentifier: z.union([z.number(), z.string()]),
targetType: targetTypeSchema,
}),
)
.query(async ({ input, ctx }) => {
log('checkLike input: %O', input);
if (!ctx.marketSDK) {
return { isLiked: false };
}
try {
const result = await ctx.marketSDK.likes.checkLike(
input.targetType,
input.targetIdOrIdentifier as any,
);
return result;
} catch (error) {
log('Error checking like: %O', error);
return { isLiked: false };
}
}),
/**
* Follow a user
* POST /market/social/follow
*/
follow: socialAuthProcedure
.input(z.object({ followingId: z.number() }))
.mutation(async ({ input, ctx }) => {
log('follow input: %O', input);
try {
await ctx.marketSDK.follows.follow(input.followingId);
return { success: true };
} catch (error) {
log('Error following user: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to follow user',
});
}
}),
/**
* Get follow counts for a user
* GET /market/social/follow-counts/[userId]
*/
getFollowCounts: socialPublicProcedure
.input(z.object({ userId: z.number() }))
.query(async ({ input, ctx }) => {
log('getFollowCounts input: %O', input);
try {
const [following, followers] = await Promise.all([
ctx.marketSDK.follows.getFollowing(input.userId, { limit: 1 }),
ctx.marketSDK.follows.getFollowers(input.userId, { limit: 1 }),
]);
return {
followersCount: (followers as any).totalCount || (followers as any).total || 0,
followingCount: (following as any).totalCount || (following as any).total || 0,
};
} catch (error) {
log('Error getting follow counts: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get follow counts',
});
}
}),
/**
* Get followers of a user
* GET /market/social/followers/[userId]
*/
getFollowers: socialPublicProcedure
.input(z.object({ userId: z.number() }).merge(paginationSchema))
.query(async ({ input, ctx }) => {
log('getFollowers input: %O', input);
try {
const { userId, ...params } = input;
const result = await ctx.marketSDK.follows.getFollowers(userId, params);
return result;
} catch (error) {
log('Error getting followers: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get followers',
});
}
}),
/**
* Get users that a user is following
* GET /market/social/following/[userId]
*/
getFollowing: socialPublicProcedure
.input(z.object({ userId: z.number() }).merge(paginationSchema))
.query(async ({ input, ctx }) => {
log('getFollowing input: %O', input);
try {
const { userId, ...params } = input;
const result = await ctx.marketSDK.follows.getFollowing(userId, params);
return result;
} catch (error) {
log('Error getting following: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get following',
});
}
}),
/**
* Get current user's favorites
* GET /market/social/favorites
*/
getMyFavorites: socialAuthProcedure
.input(paginationSchema.optional())
.query(async ({ input, ctx }) => {
log('getMyFavorites input: %O', input);
try {
const result = await ctx.marketSDK.favorites.getMyFavorites(input);
return result;
} catch (error) {
log('Error getting my favorites: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get favorites',
});
}
}),
/**
* Get user's favorite agents
* GET /market/social/favorite-agents/[userId]
*/
getUserFavoriteAgents: socialPublicProcedure
.input(z.object({ userId: z.number() }).merge(paginationSchema))
.query(async ({ input, ctx }) => {
log('getUserFavoriteAgents input: %O', input);
try {
const { userId, ...params } = input;
const result = await ctx.marketSDK.favorites.getUserFavoriteAgents(userId, params);
return result;
} catch (error) {
log('Error getting user favorite agents: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get favorite agents',
});
}
}),
/**
* Get user's favorite plugins
* GET /market/social/favorite-plugins/[userId]
*/
getUserFavoritePlugins: socialPublicProcedure
.input(z.object({ userId: z.number() }).merge(paginationSchema))
.query(async ({ input, ctx }) => {
log('getUserFavoritePlugins input: %O', input);
try {
const { userId, ...params } = input;
const result = await ctx.marketSDK.favorites.getUserFavoritePlugins(userId, params);
return result;
} catch (error) {
log('Error getting user favorite plugins: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get favorite plugins',
});
}
}),
/**
* Get user's all favorites
* GET /market/social/user-favorites/[userId]
*/
getUserFavorites: socialPublicProcedure
.input(z.object({ userId: z.number() }).merge(paginationSchema))
.query(async ({ input, ctx }) => {
log('getUserFavorites input: %O', input);
try {
const { userId, ...params } = input;
const result = await ctx.marketSDK.favorites.getUserFavorites(userId, params);
return result;
} catch (error) {
log('Error getting user favorites: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get user favorites',
});
}
}),
/**
* Get user's liked agents
* GET /market/social/liked-agents/[userId]
*/
getUserLikedAgents: socialPublicProcedure
.input(z.object({ userId: z.number() }).merge(paginationSchema))
.query(async ({ input, ctx }) => {
log('getUserLikedAgents input: %O', input);
try {
const { userId, ...params } = input;
const result = await ctx.marketSDK.likes.getUserLikedAgents(userId, params);
return result;
} catch (error) {
log('Error getting user liked agents: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get liked agents',
});
}
}),
/**
* Get user's liked plugins
* GET /market/social/liked-plugins/[userId]
*/
getUserLikedPlugins: socialPublicProcedure
.input(z.object({ userId: z.number() }).merge(paginationSchema))
.query(async ({ input, ctx }) => {
log('getUserLikedPlugins input: %O', input);
try {
const { userId, ...params } = input;
const result = await ctx.marketSDK.likes.getUserLikedPlugins(userId, params);
return result;
} catch (error) {
log('Error getting user liked plugins: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get liked plugins',
});
}
}),
/**
* Like an item
* POST /market/social/like
*/
like: socialAuthProcedure
.input(
z.object({
identifier: z.string().optional(),
targetId: z.number().optional(),
targetType: targetTypeSchema,
}),
)
.mutation(async ({ input, ctx }) => {
log('like input: %O', input);
try {
const targetValue = input.identifier ?? input.targetId;
if (!targetValue) {
throw new Error('Either identifier or targetId is required');
}
await ctx.marketSDK.likes.like(input.targetType, targetValue as any);
return { success: true };
} catch (error) {
log('Error liking item: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to like item',
});
}
}),
/**
* Remove from favorites
* POST /market/social/unfavorite
*/
removeFavorite: socialAuthProcedure
.input(
z.object({
identifier: z.string().optional(),
targetId: z.number().optional(),
targetType: targetTypeSchema,
}),
)
.mutation(async ({ input, ctx }) => {
log('removeFavorite input: %O', input);
try {
const targetValue = input.identifier ?? input.targetId;
if (!targetValue) {
throw new Error('Either identifier or targetId is required');
}
await ctx.marketSDK.favorites.removeFavorite(input.targetType, targetValue as any);
return { success: true };
} catch (error) {
log('Error removing favorite: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to remove favorite',
});
}
}),
/**
* Toggle like on an item
* POST /market/social/toggle-like
*/
toggleLike: socialAuthProcedure
.input(
z.object({
identifier: z.string().optional(),
targetId: z.number().optional(),
targetType: targetTypeSchema,
}),
)
.mutation(async ({ input, ctx }) => {
log('toggleLike input: %O', input);
try {
const targetValue = input.identifier ?? input.targetId;
if (!targetValue) {
throw new Error('Either identifier or targetId is required');
}
const result = await ctx.marketSDK.likes.toggleLike(input.targetType, targetValue as any);
return result;
} catch (error) {
log('Error toggling like: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to toggle like',
});
}
}),
/**
* Unfollow a user
* POST /market/social/unfollow
*/
unfollow: socialAuthProcedure
.input(z.object({ followingId: z.number() }))
.mutation(async ({ input, ctx }) => {
log('unfollow input: %O', input);
try {
await ctx.marketSDK.follows.unfollow(input.followingId);
return { success: true };
} catch (error) {
log('Error unfollowing user: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to unfollow user',
});
}
}),
/**
* Unlike an item
* POST /market/social/unlike
*/
unlike: socialAuthProcedure
.input(
z.object({
identifier: z.string().optional(),
targetId: z.number().optional(),
targetType: targetTypeSchema,
}),
)
.mutation(async ({ input, ctx }) => {
log('unlike input: %O', input);
try {
const targetValue = input.identifier ?? input.targetId;
if (!targetValue) {
throw new Error('Either identifier or targetId is required');
}
await ctx.marketSDK.likes.unlike(input.targetType, targetValue as any);
return { success: true };
} catch (error) {
log('Error unliking item: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to unlike item',
});
}
}),
});
export type SocialRouter = typeof socialRouter;

View File

@@ -0,0 +1,123 @@
import { TRPCError } from '@trpc/server';
import debug from 'debug';
import { z } from 'zod';
import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
import { marketSDK, marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
const log = debug('lambda-router:market:user');
// Authenticated procedure for user profile updates
const userAuthProcedure = authedProcedure.use(serverDatabase).use(marketUserInfo).use(marketSDK);
// Public procedure for viewing user profiles
const userPublicProcedure = publicProcedure.use(serverDatabase).use(marketUserInfo).use(marketSDK);
// Schema definitions
const socialLinksSchema = z.object({
github: z.string().optional(),
twitter: z.string().optional(),
website: z.string().optional(),
});
const userMetaSchema = z.object({
bannerUrl: z.string().optional(),
description: z.string().optional(),
socialLinks: socialLinksSchema.optional(),
});
const updateUserProfileSchema = z.object({
avatarUrl: z.string().optional(),
displayName: z.string().optional(),
meta: userMetaSchema.optional(),
userName: z.string().optional(),
});
export const userRouter = router({
/**
* Get user profile by username
* GET /market/user/[username]
*/
getUserByUsername: userPublicProcedure
.input(z.object({ username: z.string() }))
.query(async ({ input, ctx }) => {
log('getUserByUsername input: %O', input);
try {
const response = await ctx.marketSDK.user.getUserInfo(input.username);
if (!response?.user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `User not found: ${input.username}`,
});
}
const { user } = response;
return {
avatarUrl: user.avatarUrl || null,
bannerUrl: user.meta?.bannerUrl || null,
createdAt: user.createdAt,
description: user.meta?.description || null,
displayName: user.displayName || null,
id: user.id,
namespace: user.namespace,
socialLinks: user.meta?.socialLinks || null,
type: user.type || null,
userName: user.userName || null,
};
} catch (error) {
if (error instanceof TRPCError) throw error;
log('Error getting user profile: %O', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: error instanceof Error ? error.message : 'Failed to get user profile',
});
}
}),
/**
* Update current user's profile
* PUT /market/user/me
*/
updateUserProfile: userAuthProcedure
.input(updateUserProfileSchema)
.mutation(async ({ input, ctx }) => {
log('updateUserProfile input: %O', input);
try {
// Ensure meta is at least an empty object
const normalizedPayload = {
...input,
meta: input.meta ?? {},
};
const response = await ctx.marketSDK.user.updateUserInfo(normalizedPayload);
return response;
} catch (error) {
log('Error updating user profile: %O', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const isUserNameTaken = errorMessage.toLowerCase().includes('already taken');
if (isUserNameTaken) {
throw new TRPCError({
cause: error,
code: 'CONFLICT',
message: 'Username is already taken',
});
}
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: errorMessage,
});
}
}),
});
export type UserRouter = typeof userRouter;

View File

@@ -1,6 +1,10 @@
import { type AgentItemDetail, type AgentListResponse } from '@lobehub/market-sdk';
import {
type AgentCreateResponse,
type AgentItemDetail,
type AgentListResponse,
} from '@lobehub/market-sdk';
import { MARKET_ENDPOINTS } from '@/services/_url';
import { lambdaClient } from '@/libs/trpc/client';
interface GetOwnAgentsParams {
page?: number;
@@ -8,49 +12,15 @@ interface GetOwnAgentsParams {
}
export class MarketApiService {
private accessToken?: string;
// eslint-disable-next-line no-undef
private async request<T>(endpoint: string, init?: RequestInit): Promise<T> {
const headers = new Headers(init?.headers);
if (init?.body && !headers.has('content-type')) {
headers.set('content-type', 'application/json');
}
if (this.accessToken && !headers.has('authorization')) {
headers.set('authorization', `Bearer ${this.accessToken}`);
}
const response = await fetch(endpoint, {
...init,
credentials: init?.credentials ?? 'same-origin',
headers,
});
if (!response.ok) {
let message = 'Unknown error';
try {
const errorBody = await response.json();
message = errorBody?.message ?? message;
} catch {
message = await response.text();
}
throw new Error(message || 'Market request failed');
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
/**
* @deprecated This method is no longer needed as authentication is now handled
* automatically through tRPC middleware. Keeping for backward compatibility.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setAccessToken(_token: string) {
// No-op: Authentication is now handled through tRPC authedProcedure middleware
}
setAccessToken(token: string) {
this.accessToken = token;
}
// Create new agent
async createAgent(agentData: {
homepage?: string;
@@ -60,18 +30,15 @@ export class MarketApiService {
status?: 'published' | 'unpublished' | 'archived' | 'deprecated';
tokenUsage?: number;
visibility?: 'public' | 'private' | 'internal';
}): Promise<AgentItemDetail> {
return this.request(MARKET_ENDPOINTS.createAgent, {
body: JSON.stringify(agentData),
method: 'POST',
});
}): Promise<AgentCreateResponse> {
return lambdaClient.market.agent.createAgent.mutate(agentData);
}
// Get agent detail by identifier
async getAgentDetail(identifier: string): Promise<AgentItemDetail> {
return this.request(MARKET_ENDPOINTS.getAgentDetail(identifier), {
method: 'GET',
});
return lambdaClient.market.agent.getAgentDetail.query({
identifier,
}) as Promise<AgentItemDetail>;
}
// Check if agent exists (returns true if exists, false if not)
@@ -111,55 +78,28 @@ export class MarketApiService {
supportsAuthenticatedExtendedCard?: boolean;
tokenUsage?: number;
url?: string;
}): Promise<AgentItemDetail> {
const { identifier, ...rest } = versionData;
const targetIdentifier = identifier;
if (!targetIdentifier) throw new Error('Identifier is required');
return this.request(MARKET_ENDPOINTS.createAgentVersion, {
body: JSON.stringify({
identifier: targetIdentifier,
...rest,
}),
method: 'POST',
});
}) {
return lambdaClient.market.agent.createAgentVersion.mutate(versionData);
}
// Publish agent (make it visible in marketplace)
async publishAgent(identifier: string): Promise<void> {
return this.request(MARKET_ENDPOINTS.publishAgent(identifier), {
method: 'POST',
});
await lambdaClient.market.agent.publishAgent.mutate({ identifier });
}
// Unpublish agent (hide from marketplace, can be republished)
async unpublishAgent(identifier: string): Promise<void> {
return this.request(MARKET_ENDPOINTS.unpublishAgent(identifier), {
method: 'POST',
});
await lambdaClient.market.agent.unpublishAgent.mutate({ identifier });
}
// Deprecate agent (permanently hide, cannot be republished)
async deprecateAgent(identifier: string): Promise<void> {
return this.request(MARKET_ENDPOINTS.deprecateAgent(identifier), {
method: 'POST',
});
await lambdaClient.market.agent.deprecateAgent.mutate({ identifier });
}
// Get own agents (requires authentication)
async getOwnAgents(params?: GetOwnAgentsParams): Promise<AgentListResponse> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
const queryString = searchParams.toString();
const url = queryString
? `${MARKET_ENDPOINTS.getOwnAgents}?${queryString}`
: MARKET_ENDPOINTS.getOwnAgents;
return this.request(url, {
method: 'GET',
});
return lambdaClient.market.agent.getOwnAgents.query(params) as Promise<AgentListResponse>;
}
}

View File

@@ -1,4 +1,4 @@
import { MARKET_ENDPOINTS } from '@/services/_url';
import { lambdaClient } from '@/libs/trpc/client';
export type SocialTargetType = 'agent' | 'plugin';
@@ -74,108 +74,57 @@ export interface FavoritePluginItem {
}
class SocialService {
private accessToken?: string;
// eslint-disable-next-line no-undef
private async request<T>(endpoint: string, init?: RequestInit): Promise<T> {
const headers = new Headers(init?.headers);
if (init?.body && !headers.has('content-type')) {
headers.set('content-type', 'application/json');
}
if (this.accessToken && !headers.has('authorization')) {
headers.set('authorization', `Bearer ${this.accessToken}`);
}
const response = await fetch(endpoint, {
...init,
credentials: init?.credentials ?? 'same-origin',
headers,
});
if (!response.ok) {
let message = 'Unknown error';
try {
const errorBody = await response.json();
message = errorBody?.message ?? message;
} catch {
message = await response.text();
}
throw new Error(message || 'Social request failed');
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
setAccessToken(token: string | undefined) {
this.accessToken = token;
/**
* @deprecated This method is no longer needed as authentication is now handled
* automatically through tRPC middleware. Keeping for backward compatibility.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setAccessToken(_token: string | undefined) {
// No-op: Authentication is now handled through tRPC authedProcedure middleware
}
// ==================== Follow ====================
async follow(followingId: number): Promise<void> {
await this.request(MARKET_ENDPOINTS.follow, {
body: JSON.stringify({ followingId }),
method: 'POST',
});
await lambdaClient.market.social.follow.mutate({ followingId });
}
async unfollow(followingId: number): Promise<void> {
await this.request(MARKET_ENDPOINTS.unfollow, {
body: JSON.stringify({ followingId }),
method: 'POST',
});
await lambdaClient.market.social.unfollow.mutate({ followingId });
}
async checkFollowStatus(userId: number): Promise<FollowStatus> {
return this.request(MARKET_ENDPOINTS.followStatus(userId), {
method: 'GET',
});
return lambdaClient.market.social.checkFollowStatus.query({
targetUserId: userId,
}) as Promise<FollowStatus>;
}
async getFollowCounts(userId: number): Promise<FollowCounts> {
return this.request(MARKET_ENDPOINTS.followCounts(userId), {
method: 'GET',
});
return lambdaClient.market.social.getFollowCounts.query({
userId,
}) as Promise<FollowCounts>;
}
async getFollowing(
userId: number,
params?: PaginationParams,
): Promise<PaginatedResponse<FollowUserItem>> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
const queryString = searchParams.toString();
const url = queryString
? `${MARKET_ENDPOINTS.following(userId)}?${queryString}`
: MARKET_ENDPOINTS.following(userId);
return this.request(url, { method: 'GET' });
return lambdaClient.market.social.getFollowing.query({
limit: params?.pageSize,
offset: params?.page ? (params.page - 1) * (params.pageSize || 10) : undefined,
userId,
}) as unknown as Promise<PaginatedResponse<FollowUserItem>>;
}
async getFollowers(
userId: number,
params?: PaginationParams,
): Promise<PaginatedResponse<FollowUserItem>> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
const queryString = searchParams.toString();
const url = queryString
? `${MARKET_ENDPOINTS.followers(userId)}?${queryString}`
: MARKET_ENDPOINTS.followers(userId);
return this.request(url, { method: 'GET' });
return lambdaClient.market.social.getFollowers.query({
limit: params?.pageSize,
offset: params?.page ? (params.page - 1) * (params.pageSize || 10) : undefined,
userId,
}) as unknown as Promise<PaginatedResponse<FollowUserItem>>;
}
// ==================== Favorite ====================
@@ -184,172 +133,127 @@ class SocialService {
targetType: SocialTargetType,
targetIdOrIdentifier: number | string,
): Promise<void> {
const body =
const input =
typeof targetIdOrIdentifier === 'string'
? { identifier: targetIdOrIdentifier, targetType }
: { targetId: targetIdOrIdentifier, targetType };
await this.request(MARKET_ENDPOINTS.favorite, {
body: JSON.stringify(body),
method: 'POST',
});
await lambdaClient.market.social.addFavorite.mutate(input);
}
async removeFavorite(
targetType: SocialTargetType,
targetIdOrIdentifier: number | string,
): Promise<void> {
const body =
const input =
typeof targetIdOrIdentifier === 'string'
? { identifier: targetIdOrIdentifier, targetType }
: { targetId: targetIdOrIdentifier, targetType };
await this.request(MARKET_ENDPOINTS.unfavorite, {
body: JSON.stringify(body),
method: 'POST',
});
await lambdaClient.market.social.removeFavorite.mutate(input);
}
async checkFavoriteStatus(
targetType: SocialTargetType,
targetIdOrIdentifier: number | string,
): Promise<FavoriteStatus> {
return this.request(MARKET_ENDPOINTS.favoriteStatus(targetType, targetIdOrIdentifier), {
method: 'GET',
});
return lambdaClient.market.social.checkFavorite.query({
targetIdOrIdentifier,
targetType,
}) as Promise<FavoriteStatus>;
}
async getMyFavorites(params?: PaginationParams): Promise<PaginatedResponse<FavoriteItem>> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
const queryString = searchParams.toString();
const url = queryString
? `${MARKET_ENDPOINTS.myFavorites}?${queryString}`
: MARKET_ENDPOINTS.myFavorites;
return this.request(url, { method: 'GET' });
return lambdaClient.market.social.getMyFavorites.query({
limit: params?.pageSize,
offset: params?.page ? (params.page - 1) * (params.pageSize || 10) : undefined,
}) as unknown as Promise<PaginatedResponse<FavoriteItem>>;
}
async getUserFavoriteAgents(
userId: number,
params?: PaginationParams,
): Promise<PaginatedResponse<FavoriteAgentItem>> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
const queryString = searchParams.toString();
const url = queryString
? `${MARKET_ENDPOINTS.favoriteAgents(userId)}?${queryString}`
: MARKET_ENDPOINTS.favoriteAgents(userId);
return this.request(url, { method: 'GET' });
return lambdaClient.market.social.getUserFavoriteAgents.query({
limit: params?.pageSize,
offset: params?.page ? (params.page - 1) * (params.pageSize || 10) : undefined,
userId,
}) as unknown as Promise<PaginatedResponse<FavoriteAgentItem>>;
}
async getUserFavoritePlugins(
userId: number,
params?: PaginationParams,
): Promise<PaginatedResponse<FavoritePluginItem>> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
const queryString = searchParams.toString();
const url = queryString
? `${MARKET_ENDPOINTS.favoritePlugins(userId)}?${queryString}`
: MARKET_ENDPOINTS.favoritePlugins(userId);
return this.request(url, { method: 'GET' });
return lambdaClient.market.social.getUserFavoritePlugins.query({
limit: params?.pageSize,
offset: params?.page ? (params.page - 1) * (params.pageSize || 10) : undefined,
userId,
}) as unknown as Promise<PaginatedResponse<FavoritePluginItem>>;
}
// ==================== Like ====================
async like(
targetType: SocialTargetType,
targetIdOrIdentifier: number | string,
): Promise<void> {
const body =
async like(targetType: SocialTargetType, targetIdOrIdentifier: number | string): Promise<void> {
const input =
typeof targetIdOrIdentifier === 'string'
? { identifier: targetIdOrIdentifier, targetType }
: { targetId: targetIdOrIdentifier, targetType };
await this.request(MARKET_ENDPOINTS.like, {
body: JSON.stringify(body),
method: 'POST',
});
await lambdaClient.market.social.like.mutate(input);
}
async unlike(
targetType: SocialTargetType,
targetIdOrIdentifier: number | string,
): Promise<void> {
const body =
async unlike(targetType: SocialTargetType, targetIdOrIdentifier: number | string): Promise<void> {
const input =
typeof targetIdOrIdentifier === 'string'
? { identifier: targetIdOrIdentifier, targetType }
: { targetId: targetIdOrIdentifier, targetType };
await this.request(MARKET_ENDPOINTS.unlike, {
body: JSON.stringify(body),
method: 'POST',
});
await lambdaClient.market.social.unlike.mutate(input);
}
async checkLikeStatus(
targetType: SocialTargetType,
targetIdOrIdentifier: number | string,
): Promise<LikeStatus> {
return this.request(MARKET_ENDPOINTS.likeStatus(targetType, targetIdOrIdentifier), {
method: 'GET',
});
return lambdaClient.market.social.checkLike.query({
targetIdOrIdentifier,
targetType,
}) as Promise<LikeStatus>;
}
async toggleLike(
targetType: SocialTargetType,
targetIdOrIdentifier: number | string,
): Promise<ToggleLikeResult> {
const body =
const input =
typeof targetIdOrIdentifier === 'string'
? { identifier: targetIdOrIdentifier, targetType }
: { targetId: targetIdOrIdentifier, targetType };
return this.request(MARKET_ENDPOINTS.toggleLike, {
body: JSON.stringify(body),
method: 'POST',
});
return lambdaClient.market.social.toggleLike.mutate(input) as Promise<ToggleLikeResult>;
}
async getUserLikedAgents(
userId: number,
params?: PaginationParams,
): Promise<PaginatedResponse<FavoriteAgentItem>> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
const queryString = searchParams.toString();
const url = queryString
? `${MARKET_ENDPOINTS.likedAgents(userId)}?${queryString}`
: MARKET_ENDPOINTS.likedAgents(userId);
return this.request(url, { method: 'GET' });
return lambdaClient.market.social.getUserLikedAgents.query({
limit: params?.pageSize,
offset: params?.page ? (params.page - 1) * (params.pageSize || 10) : undefined,
userId,
}) as unknown as Promise<PaginatedResponse<FavoriteAgentItem>>;
}
async getUserLikedPlugins(
userId: number,
params?: PaginationParams,
): Promise<PaginatedResponse<FavoritePluginItem>> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.pageSize) searchParams.set('pageSize', String(params.pageSize));
const queryString = searchParams.toString();
const url = queryString
? `${MARKET_ENDPOINTS.likedPlugins(userId)}?${queryString}`
: MARKET_ENDPOINTS.likedPlugins(userId);
return this.request(url, { method: 'GET' });
return lambdaClient.market.social.getUserLikedPlugins.query({
limit: params?.pageSize,
offset: params?.page ? (params.page - 1) * (params.pageSize || 10) : undefined,
userId,
}) as unknown as Promise<PaginatedResponse<FavoritePluginItem>>;
}
}