mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
♻️ 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:
@@ -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",
|
||||
|
||||
@@ -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个字符",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './keyVaults';
|
||||
export * from './marketSDK';
|
||||
export * from './marketUserInfo';
|
||||
export * from './serverDatabase';
|
||||
export * from './telemetry';
|
||||
|
||||
68
src/libs/trpc/lambda/middleware/marketSDK.ts
Normal file
68
src/libs/trpc/lambda/middleware/marketSDK.ts
Normal 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();
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
504
src/server/routers/lambda/market/agent.ts
Normal file
504
src/server/routers/lambda/market/agent.ts
Normal 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;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
169
src/server/routers/lambda/market/oidc.ts
Normal file
169
src/server/routers/lambda/market/oidc.ts
Normal 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;
|
||||
532
src/server/routers/lambda/market/social.ts
Normal file
532
src/server/routers/lambda/market/social.ts
Normal 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;
|
||||
123
src/server/routers/lambda/market/user.ts
Normal file
123
src/server/routers/lambda/market/user.ts
Normal 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;
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user