UNPKG

@lobehub/market-sdk

Version:

LobeHub Market JavaScript SDK

1 lines 354 kB
{"version":3,"sources":["../src/admin/MarketAdmin.ts","../src/core/BaseSDK.ts","../src/core/MarketAPIError.ts","../src/core/version.ts","../src/admin/services/AgentService.ts","../src/admin/services/AnalysisService.ts","../src/admin/services/PluginService.ts","../src/admin/services/SystemDependencyService.ts","../src/admin/services/SettingsService.ts","../src/admin/services/ReviewService.ts","../src/admin/services/SkillCollectionService.ts","../src/admin/services/PluginEnvService.ts","../src/market/market-sdk.ts","../src/market/services/AgentProfileService.ts","../src/market/services/AgentService.ts","../src/market/services/AgentGroupService.ts","../src/market/services/AuthService.ts","../src/market/services/ConnectService.ts","../src/market/services/CredService.ts","../src/market/services/DiscoveryService.ts","../src/market/services/FeedbackService.ts","../src/market/services/PluginsService.ts","../src/market/services/SkillService.ts","../src/market/services/MarketSkillCollectionService.ts","../src/market/services/MarketSkillService.ts","../src/market/services/UserService.ts","../src/market/services/UserFollowService.ts","../src/market/services/UserFavoriteService.ts","../src/market/services/UserLikeService.ts","../src/types/admin.ts","../src/utils/trustedClient.ts","../src/index.ts"],"sourcesContent":["import debug from 'debug';\n\nimport { BaseSDK } from '@/core/BaseSDK';\nimport { MarketSDKOptions } from '@/types';\n\nimport {\n AgentService,\n AnalysisService,\n PluginEnvService,\n PluginService,\n ReviewService,\n SettingsService,\n SkillCollectionService,\n SystemDependencyService,\n} from './services';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:admin');\n\n/**\n * LobeHub Market Admin SDK Client\n *\n * Client for accessing administrative functionality of the LobeHub Marketplace.\n * This SDK provides privileged operations for managing agents, plugins, reviews,\n * system settings, and dependencies. It requires admin-level authentication.\n */\nexport class MarketAdmin extends BaseSDK {\n /**\n * Agent management service\n * Provides methods for creating, updating, and managing agents\n */\n readonly agents: AgentService;\n\n /**\n * Market analysis service\n * Provides methods for accessing market analytics and statistics\n */\n readonly analysis: AnalysisService;\n\n /**\n * Plugin environment service\n */\n readonly env: PluginEnvService;\n\n /**\n * Plugin management service\n * Provides methods for creating, updating, and managing plugins\n */\n readonly plugins: PluginService;\n\n /**\n * Review management service\n * Provides methods for moderating and managing user reviews\n */\n readonly reviews: ReviewService;\n\n /**\n * System settings management service\n * Provides methods for configuring marketplace settings\n */\n readonly settings: SettingsService;\n\n /**\n * Skill collection management service\n * Provides methods for managing curated skill collection localizations\n */\n readonly skillCollections: SkillCollectionService;\n\n /**\n * System dependency management service\n * Provides methods for managing system dependencies required by plugins\n */\n readonly dependencies: SystemDependencyService;\n\n /**\n * Creates a new MarketAdmin instance\n *\n * @param options - Configuration options for the SDK\n */\n constructor(options: MarketSDKOptions = {}) {\n // Use admin-specific API key if available\n const apiKey = options.apiKey || process.env.MARKET_ADMIN_API_KEY;\n\n // Create shared token state object for all services\n const sharedTokenState = {\n accessToken: undefined,\n tokenExpiry: undefined,\n };\n\n super({ ...options, apiKey }, undefined, sharedTokenState);\n log('MarketAdmin instance created');\n\n // Initialize admin services with shared headers and token state for efficient reuse\n this.agents = new AgentService(options, this.headers, sharedTokenState);\n this.analysis = new AnalysisService(options, this.headers, sharedTokenState);\n this.dependencies = new SystemDependencyService(options, this.headers, sharedTokenState);\n this.env = new PluginEnvService(options, this.headers, sharedTokenState);\n this.plugins = new PluginService(options, this.headers, sharedTokenState);\n this.reviews = new ReviewService(options, this.headers, sharedTokenState);\n this.settings = new SettingsService(options, this.headers, sharedTokenState);\n this.skillCollections = new SkillCollectionService(options, this.headers, sharedTokenState);\n }\n}\n","import debug from 'debug';\nimport { SignJWT } from 'jose';\nimport urlJoin from 'url-join';\n\nimport type { MarketSDKOptions, SharedTokenState } from '../types';\nimport { MarketAPIError } from './MarketAPIError';\nimport { SDK_USER_AGENT } from './version';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:core');\n\n/**\n * Base SDK class\n *\n * Provides shared request handling and authentication functionality that is used\n * by both the Market SDK and Admin SDK. This class handles the common concerns:\n * - API endpoint configuration\n * - Authentication header management\n * - HTTP request handling\n * - Error handling\n * - Query string building\n */\nexport class BaseSDK {\n /** Base API URL */\n protected apiBaseUrl: string;\n /** Base URL */\n protected baseUrl: string;\n /** OAuth URL */\n protected oauthBaseUrl: string;\n\n /** Default locale for requests that require localization */\n protected defaultLocale: string;\n\n /** HTTP headers to include with all requests */\n protected headers: Record<string, string>;\n\n private clientId?: string;\n private clientSecret?: string;\n private readonly initialAccessToken?: string;\n private accessToken?: string;\n private tokenExpiry?: number;\n\n // Shared token state for all instances\n private sharedTokenState?: SharedTokenState;\n\n /**\n * Creates a new BaseSDK instance\n *\n * @param options - Configuration options for the SDK\n * @param sharedHeaders - Optional shared headers object for reuse across services\n * @param sharedTokenState - Optional shared token state object for reuse across services\n */\n constructor(\n options: MarketSDKOptions = {},\n sharedHeaders?: Record<string, string>,\n sharedTokenState?: SharedTokenState,\n ) {\n // Set base URL from options, environment variable, or default to production URL\n this.baseUrl = options.baseURL || process.env.MARKET_BASE_URL || 'https://market.lobehub.com';\n this.apiBaseUrl = urlJoin(this.baseUrl, 'api');\n this.oauthBaseUrl = urlJoin(this.baseUrl, 'oauth');\n\n // Set default locale from options or use English as default\n this.defaultLocale = options.defaultLocale || 'en-US';\n\n this.initialAccessToken = options.accessToken;\n this.clientId = options.clientId;\n this.clientSecret = options.clientSecret;\n\n // Share token state across instances if provided\n this.sharedTokenState = sharedTokenState;\n\n // Get API key from options or environment variable\n const apiKey = options.apiKey || process.env.MARKET_API_KEY;\n\n // Determine User-Agent: custom > default SDK UA\n const userAgent = options.userAgent || SDK_USER_AGENT;\n\n // Either use shared headers or create new headers object\n if (sharedHeaders) {\n this.headers = sharedHeaders;\n log('Using shared headers object');\n } else {\n this.headers = {\n 'Content-Type': 'application/json',\n 'User-Agent': userAgent,\n };\n log('Created new headers object with User-Agent: %s', userAgent);\n }\n\n // If an apiKey is provided, use it for authorization.\n // This will be overridden by M2M auth if credentials are also provided.\n if (apiKey) {\n this.headers.Authorization = `Bearer ${apiKey}`;\n }\n\n // If an accessToken is provided on init, it takes precedence.\n if (this.initialAccessToken) {\n this.headers.Authorization = `Bearer ${this.initialAccessToken}`;\n }\n\n // If a trusted client token is provided, set the x-lobe-trust-token header\n if (options.trustedClientToken) {\n this.headers['x-lobe-trust-token'] = options.trustedClientToken;\n log('Trusted client token configured');\n }\n\n log('BaseSDK instance created: %O', {\n baseUrl: this.baseUrl,\n defaultLocale: this.defaultLocale,\n hasApiKey: !!apiKey,\n hasInitialAccessToken: !!this.initialAccessToken,\n hasM2MCredentials: !!this.clientId,\n hasSharedTokenState: !!this.sharedTokenState,\n hasTrustedClientToken: !!options.trustedClientToken,\n });\n }\n\n /**\n * Sends an HTTP request to the API and handles the response\n *\n * @param url - Request URL path (will be appended to baseUrl)\n * @param options - Fetch API request options\n * @returns Promise resolving to the parsed JSON response\n * @throws Error if the request fails\n */\n // eslint-disable-next-line no-undef\n protected async request<T>(url: string, options: RequestInit = {}): Promise<T> {\n const requestUrl = urlJoin(this.apiBaseUrl, url);\n log('Sending request: %s', requestUrl);\n\n // If no access token was provided on init, and we have M2M creds, run the auth flow.\n if (!this.initialAccessToken && this.clientId && this.clientSecret) {\n await this.setM2MAuthToken();\n }\n\n const mergedHeaders = {\n ...this.headers,\n ...options.headers,\n };\n log('Request headers: %O', mergedHeaders);\n\n const response = await fetch(requestUrl, {\n ...options,\n headers: mergedHeaders,\n });\n\n if (!response.ok) {\n let errorBody: any;\n try {\n errorBody = await response.json();\n console.error('Request error: %s', JSON.stringify(errorBody));\n } catch {\n // Response body is not JSON or empty\n errorBody = undefined;\n }\n\n throw new MarketAPIError(response.status, response.statusText, errorBody);\n }\n\n log('Request successful: %s', url);\n\n // Some endpoints (e.g., plugin events) intentionally return 204 with no body.\n if (response.status === 204) {\n return undefined as T;\n }\n\n return response.json() as Promise<T>;\n }\n\n /**\n * Builds a URL query string from a parameters object\n *\n * @param params - Object containing query parameters\n * @returns Formatted query string (including leading ? if params exist)\n */\n protected buildQueryString(params: Record<string, any>): string {\n const query = Object.entries(params)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n .filter(([_, value]) => value !== undefined && value !== null)\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)\n .join('&');\n\n return query ? `?${query}` : '';\n }\n\n /**\n * Sets an authentication token for API requests\n *\n * @param token - API authentication token\n */\n setAuthToken(token: string): void {\n log('Setting authentication token');\n this.accessToken = undefined;\n this.tokenExpiry = undefined;\n\n // Clear shared token state if it exists\n if (this.sharedTokenState) {\n this.sharedTokenState.accessToken = undefined;\n this.sharedTokenState.tokenExpiry = undefined;\n }\n\n this.headers.Authorization = `Bearer ${token}`;\n }\n\n /**\n * Clears the authentication token\n */\n clearAuthToken(): void {\n log('Clearing authentication token');\n this.accessToken = undefined;\n this.tokenExpiry = undefined;\n\n // Clear shared token state if it exists\n if (this.sharedTokenState) {\n this.sharedTokenState.accessToken = undefined;\n this.sharedTokenState.tokenExpiry = undefined;\n }\n\n delete this.headers.Authorization;\n }\n\n /**\n * Fetches an M2M access token.\n * This method is designed for server-side use cases where you need to manage the token lifecycle\n * (e.g., storing it in a cookie).\n *\n * @returns A promise that resolves to an object containing the access token and its expiry time.\n */\n public async fetchM2MToken(): Promise<{ accessToken: string; expiresIn: number }> {\n if (!this.clientId || !this.clientSecret) {\n throw new Error('clientId and clientSecret are required to fetch an M2M token.');\n }\n\n log('Fetching M2M token for server-side use');\n\n const assertion = await this.createClientAssertion();\n const tokenData = await this.exchangeTokenForServer(assertion);\n\n log('M2M token fetched successfully');\n\n return {\n accessToken: tokenData.access_token,\n expiresIn: tokenData.expires_in || 3600,\n };\n }\n\n private async createClientAssertion(): Promise<string> {\n if (!this.clientId || !this.clientSecret) {\n throw new Error('Missing clientId or clientSecret for M2M authentication.');\n }\n\n const secret = new TextEncoder().encode(this.clientSecret);\n\n const tokenEndpoint = `${this.oauthBaseUrl}/token`;\n\n return await new SignJWT({})\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuer(this.clientId)\n .setSubject(this.clientId)\n .setAudience(tokenEndpoint)\n .setJti(crypto.randomUUID())\n .setIssuedAt()\n .setExpirationTime('5m')\n .sign(secret);\n }\n\n private async exchangeToken(clientAssertion: string): Promise<string> {\n const tokenData = await this.exchangeTokenForServer(clientAssertion);\n\n // Calculate token expiry time (current time + expires_in seconds - 60 seconds buffer)\n const expiresInSeconds = tokenData.expires_in || 3600; // Default to 1 hour if not provided\n this.tokenExpiry = Date.now() + (expiresInSeconds - 60) * 1000; // 60 seconds buffer\n\n return tokenData.access_token;\n }\n\n private async exchangeTokenForServer(clientAssertion: string): Promise<any> {\n const tokenEndpoint = urlJoin(this.oauthBaseUrl, 'token');\n log('Exchanging token at endpoint: %s', tokenEndpoint);\n\n const params = new URLSearchParams();\n params.append('grant_type', 'client_credentials');\n params.append(\n 'client_assertion_type',\n 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',\n );\n params.append('client_assertion', clientAssertion);\n\n const response = await fetch(tokenEndpoint, {\n body: params,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n });\n\n const tokenData = await response.json();\n\n if (!response.ok) {\n throw new Error(`Token exchange failed: ${JSON.stringify(tokenData)}`);\n }\n\n return tokenData;\n }\n\n private async setM2MAuthToken(): Promise<void> {\n // Use shared token state if available\n const currentAccessToken = this.sharedTokenState?.accessToken || this.accessToken;\n const currentTokenExpiry = this.sharedTokenState?.tokenExpiry || this.tokenExpiry;\n\n log(\n 'Token check: hasSharedState=%s, hasCurrentToken=%s, tokenExpiry=%d, currentTime=%d',\n !!this.sharedTokenState,\n !!currentAccessToken,\n currentTokenExpiry || 0,\n Date.now(),\n );\n\n // Check if we have a valid token that hasn't expired\n if (currentAccessToken && currentTokenExpiry && Date.now() < currentTokenExpiry) {\n log(\n 'Using existing M2M access token (expires in %d seconds)',\n Math.floor((currentTokenExpiry - Date.now()) / 1000),\n );\n this.headers.Authorization = `Bearer ${currentAccessToken}`;\n return;\n }\n\n // Check if there's already a token request in progress (for shared state)\n if (this.sharedTokenState?.tokenPromise) {\n log('Token request already in progress, waiting for completion...');\n const token = await this.sharedTokenState.tokenPromise;\n this.headers.Authorization = `Bearer ${token}`;\n log('Using token from concurrent request');\n return;\n }\n\n log('Fetching new M2M access token...');\n\n // Create token request promise and store it in shared state\n const tokenPromise = this.fetchNewToken();\n if (this.sharedTokenState) {\n this.sharedTokenState.tokenPromise = tokenPromise;\n }\n\n try {\n const newAccessToken = await tokenPromise;\n\n // Update both local and shared token state\n this.accessToken = newAccessToken;\n this.headers.Authorization = `Bearer ${newAccessToken}`;\n\n if (this.sharedTokenState) {\n this.sharedTokenState.accessToken = newAccessToken;\n this.sharedTokenState.tokenExpiry = this.tokenExpiry;\n // Clear the promise since we're done\n this.sharedTokenState.tokenPromise = undefined;\n log(\n 'Updated shared token state (expires in %d seconds)',\n Math.floor(((this.tokenExpiry || 0) - Date.now()) / 1000),\n );\n }\n\n log('Successfully set new M2M access token');\n } catch (error) {\n // Clear the promise on error\n if (this.sharedTokenState) {\n this.sharedTokenState.tokenPromise = undefined;\n }\n throw error;\n }\n }\n\n private async fetchNewToken(): Promise<string> {\n const assertion = await this.createClientAssertion();\n return await this.exchangeToken(assertion);\n }\n}\n","/**\n * Market API Error\n *\n * Custom error class for Market API errors that preserves the structured error response.\n */\nconst normalizeErrorDetails = (errorBody?: any) => {\n if (!errorBody) return undefined;\n\n if (typeof errorBody.error === 'object' && errorBody.error !== null) {\n return errorBody.error;\n }\n\n if (typeof errorBody.error === 'string') {\n return {\n ...(errorBody.code ? { code: errorBody.code } : {}),\n message: errorBody.error,\n };\n }\n\n if (typeof errorBody.message === 'string') {\n return {\n ...(errorBody.code ? { code: errorBody.code } : {}),\n message: errorBody.message,\n };\n }\n\n return undefined;\n};\n\nexport class MarketAPIError extends Error {\n /** HTTP status code */\n public readonly status: number;\n\n /** Error code from API response */\n public readonly code?: string;\n\n /** The full error response body */\n public readonly errorBody?: any;\n\n constructor(status: number, statusText: string, errorBody?: any) {\n const errorDetails = normalizeErrorDetails(errorBody);\n const errorCode = errorDetails?.code;\n const errorMessage = errorDetails?.message;\n\n // Use the API's error message if available, otherwise use status text\n const message = errorMessage || statusText;\n\n super(message);\n\n this.name = 'MarketAPIError';\n this.status = status;\n this.code = errorCode;\n this.errorBody = errorBody;\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, MarketAPIError);\n }\n }\n\n /**\n * Check if this is a specific error code\n */\n isErrorCode(code: string): boolean {\n return this.code === code;\n }\n\n /**\n * Get the full error details from the API response\n */\n getErrorDetails(): any {\n return normalizeErrorDetails(this.errorBody);\n }\n\n /**\n * Convert to a plain object for logging or serialization\n */\n toJSON(): any {\n return {\n code: this.code,\n errorBody: this.errorBody,\n message: this.message,\n name: this.name,\n status: this.status,\n };\n }\n}\n","/**\n * SDK Version\n *\n * This version should be kept in sync with package.json version.\n * It is used for User-Agent header to identify SDK requests.\n */\nexport const SDK_VERSION = '0.31.3';\n\n/**\n * SDK User-Agent string\n */\nexport const SDK_USER_AGENT = `LobeHub-Market-SDK/${SDK_VERSION}`;\n","import debug from 'debug';\n\nimport { BaseSDK } from '@/core/BaseSDK';\nimport type {\n AdminListQueryParams,\n AdminListResponse,\n AgentItemDetail,\n AgentStatus,\n} from '@/types';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:admin:agents');\n\n/**\n * Admin Agent Item\n * Agent item with admin-specific fields for management\n */\nexport interface AdminAgentItem {\n /** Author information */\n author?: {\n avatar?: string;\n name?: string;\n userName?: string;\n };\n /** Avatar URL or emoji */\n avatar?: string;\n /** Category name */\n category?: string;\n /** Agent configuration */\n config?: any;\n /** Creation timestamp */\n createdAt: string;\n /** Description */\n description?: string;\n /** Agent ID */\n id: string;\n /** Agent identifier */\n identifier: string;\n /** Install count */\n installCount?: number;\n /** Whether featured */\n isFeatured?: boolean;\n /** Whether officially maintained */\n isOfficial: boolean;\n /** Number of knowledge bases attached */\n knowledgeCount?: number;\n /** Display name */\n name: string;\n /** Owner ID */\n ownerId: number;\n /** Number of plugins attached */\n pluginCount?: number;\n /** Provider ID */\n providerId?: number;\n /** Safety check result */\n safetyCheck?: string | null;\n /** Publication status */\n status?: string;\n /** Tags */\n tags?: string[];\n /** Token usage */\n tokenUsage?: number;\n /** Update timestamp */\n updatedAt: string;\n /** URL */\n url?: string;\n /** Version name */\n versionName?: string;\n /** Version number */\n versionNumber?: number;\n /** Version list */\n versions?: Array<{\n isLatest: boolean;\n isValidated: boolean;\n updatedAt: string;\n version: string;\n versionNumber: number;\n }>;\n}\n\n/**\n * Admin Agent Item Detail\n * Extended agent detail with admin-specific fields\n */\nexport interface AdminAgentItemDetail extends AgentItemDetail {\n /** Safety check result */\n safetyCheck?: string | null;\n}\n\n/**\n * Agent list query parameters for admin\n */\nexport interface AdminAgentListQueryParams extends AdminListQueryParams {\n /** Filter by category */\n category?: string;\n /** Filter by official status */\n isOfficial?: 'true' | 'false';\n /** Filter by namespace */\n namespace?: string;\n /** Filter by status */\n status?: 'published' | 'unpublished' | 'archived' | 'deprecated' | 'all';\n /** Filter by visibility */\n visibility?: 'public' | 'private' | 'internal' | 'all';\n}\n\n/**\n * Agent update parameters\n */\nexport interface AgentUpdateParams {\n /** Official homepage or repository URL for the agent */\n homepage?: string | null;\n /** Unique identifier for the agent */\n identifier?: string;\n /** Whether this agent is featured */\n isFeatured?: boolean;\n /** Whether this agent is officially maintained by LobeHub */\n isOfficial?: boolean;\n /** Default name of the agent */\n name?: string;\n /** Safety check result */\n safetyCheck?: string | null;\n /** Publication status */\n status?: 'published' | 'unpublished' | 'archived' | 'deprecated';\n /** Visibility level */\n visibility?: 'public' | 'private' | 'internal';\n}\n\n/**\n * Agent by status response item\n */\nexport interface AgentByStatusItem {\n id: number;\n identifier: string;\n name: string;\n status: AgentStatus;\n updatedAt: string | null;\n}\n\n/**\n * Agent version by status response item\n */\nexport interface AgentVersionByStatusItem {\n agentId: number;\n agentName: string;\n identifier: string;\n status: AgentStatus;\n updatedAt: string | null;\n version: string;\n versionId: number;\n}\n\n/**\n * Agent Management Service\n *\n * Provides administrative functionality for managing agents in the marketplace.\n * This service handles CRUD operations for agents, agent status, and visibility.\n */\nexport class AgentService extends BaseSDK {\n /**\n * Retrieves a list of agents with admin details\n *\n * Supports filtering, pagination, and sorting of results.\n *\n * @param params - Query parameters for filtering and pagination\n * @returns Promise resolving to the agent list response with admin details\n */\n async getAgents(\n params: AdminAgentListQueryParams = {},\n ): Promise<AdminListResponse<AdminAgentItem>> {\n log('Getting agents with params: %O', params);\n\n const queryString = this.buildQueryString(params);\n const url = `/admin/agents${queryString}`;\n\n const result = await this.request<AdminListResponse<AdminAgentItem>>(url);\n\n log('Retrieved %d agents', result.data.length);\n return result;\n }\n\n /**\n * Retrieves agents filtered by status\n *\n * @param status - The status to filter by\n * @returns Promise resolving to agents matching the status\n */\n async getAgentsByStatus(\n status: AgentStatus,\n ): Promise<{ data: AgentByStatusItem[]; total: number }> {\n log('Getting agents by status: %s', status);\n\n const result = await this.request<{ data: AgentByStatusItem[]; total: number }>(\n `/admin/agents/by-status/${status}`,\n );\n\n log('Retrieved %d agents with status %s', result.total, status);\n return result;\n }\n\n /**\n * Retrieves agent versions filtered by status\n *\n * @param status - The status to filter by\n * @returns Promise resolving to agent versions matching the status\n */\n async getAgentVersionsByStatus(\n status: AgentStatus,\n ): Promise<{ data: AgentVersionByStatusItem[]; total: number }> {\n log('Getting agent versions by status: %s', status);\n\n const result = await this.request<{ data: AgentVersionByStatusItem[]; total: number }>(\n `/admin/agents/versions/by-status/${status}`,\n );\n\n log('Retrieved %d agent versions with status %s', result.total, status);\n return result;\n }\n\n /**\n * Retrieves a single agent with full admin details\n *\n * @param id - Agent ID or identifier\n * @param options - Optional query parameters\n * @returns Promise resolving to the detailed agent information\n */\n async getAgent(\n id: number | string,\n options: { locale?: string; version?: string } = {},\n ): Promise<AdminAgentItemDetail> {\n log('Getting agent details (admin): %s', id);\n\n const queryString = this.buildQueryString(options);\n const result = await this.request<AdminAgentItemDetail>(`/admin/agents/${id}${queryString}`);\n\n log('Retrieved agent: %s', result.identifier);\n return result;\n }\n\n /**\n * Updates agent information\n *\n * @param id - Agent ID or identifier\n * @param data - Agent update data containing fields to update\n * @returns Promise resolving to the updated agent\n */\n async updateAgent(id: number | string, data: AgentUpdateParams): Promise<AdminAgentItem> {\n log('Updating agent: %s, data: %O', id, data);\n\n const result = await this.request<AdminAgentItem>(`/admin/agents/${id}`, {\n body: JSON.stringify(data),\n method: 'PUT',\n });\n\n log('Agent updated successfully');\n return result;\n }\n\n /**\n * Updates agent publication status\n *\n * @param id - Agent ID or identifier\n * @param status - New status to set\n * @returns Promise resolving to success response\n */\n async updateAgentStatus(\n id: number | string,\n status: 'published' | 'unpublished' | 'archived' | 'deprecated',\n ): Promise<{ message: string; success: boolean }> {\n log('Updating agent status: %s to %s', id, status);\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/agents/${id}/status`,\n {\n body: JSON.stringify({ status }),\n method: 'PATCH',\n },\n );\n\n log('Agent status updated successfully');\n return result;\n }\n\n /**\n * Updates agent visibility\n *\n * @param id - Agent ID or identifier\n * @param visibility - New visibility setting\n * @returns Promise resolving to success response\n */\n async updateAgentVisibility(\n id: number | string,\n visibility: 'public' | 'private' | 'internal',\n ): Promise<{ message: string; success: boolean }> {\n log('Updating agent visibility: %s to %s', id, visibility);\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/agents/${id}/visibility`,\n {\n body: JSON.stringify({ visibility }),\n method: 'PATCH',\n },\n );\n\n log('Agent visibility updated successfully');\n return result;\n }\n\n /**\n * Deletes an agent\n *\n * @param id - Agent ID or identifier\n * @returns Promise resolving to success response\n */\n async deleteAgent(id: number | string): Promise<{ message: string; success: boolean }> {\n log('Deleting agent: %s', id);\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/agents/${id}`,\n { method: 'DELETE' },\n );\n\n log('Agent deleted successfully');\n return result;\n }\n\n /**\n * Updates status for multiple agents in a single operation\n *\n * @param ids - Array of agent IDs to update\n * @param status - New status to set for all specified agents\n * @returns Promise resolving to success response\n */\n async batchUpdateAgentStatus(\n ids: number[],\n status: 'published' | 'unpublished' | 'archived' | 'deprecated',\n ): Promise<{ message: string; success: boolean; updatedCount: number }> {\n log('Batch updating agent status: %O to %s', ids, status);\n\n const result = await this.request<{ message: string; success: boolean; updatedCount: number }>(\n '/admin/agents/batch/status',\n {\n body: JSON.stringify({ ids, status }),\n method: 'PATCH',\n },\n );\n\n log('Batch agent status update completed: %d updated', result.updatedCount);\n return result;\n }\n\n /**\n * Deletes multiple agents in a single operation\n *\n * @param ids - Array of agent IDs to delete\n * @returns Promise resolving to success response\n */\n async batchDeleteAgents(\n ids: number[],\n ): Promise<{ deletedCount: number; message: string; success: boolean }> {\n log('Batch deleting agents: %O', ids);\n\n const result = await this.request<{ deletedCount: number; message: string; success: boolean }>(\n '/admin/agents/batch/delete',\n {\n body: JSON.stringify({ ids }),\n method: 'DELETE',\n },\n );\n\n log('Batch agent deletion completed: %d deleted', result.deletedCount);\n return result;\n }\n}\n","import {\n InstallFailureAnalysis,\n InstallFailureAnalysisQuery,\n RangeQuery,\n RangeStats,\n TopPlugin,\n TopPluginsQuery,\n} from '@lobehub/market-types';\nimport debug from 'debug';\n\nimport { BaseSDK } from '@/core/BaseSDK';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:admin:analysis');\n\n/**\n * Market Overview Statistics Interface\n * Defines the structure for market overview data\n */\nexport interface MarketOverviewStats {\n devices: {\n count: number;\n prevCount: number;\n };\n installs: {\n count: number;\n prevCount: number;\n };\n period: string;\n pluginCalls: {\n count: number;\n prevCount: number;\n };\n plugins: {\n count: number;\n prevCount: number;\n };\n}\n\n/**\n * Period type for analysis queries\n */\nexport type AnalysisPeriod = '1d' | '7d' | '30d' | '1mo' | '3mo' | '1y';\n\n/**\n * Market overview query parameters\n */\nexport interface MarketOverviewQuery {\n /** Analysis period: 1d, 7d, 30d (rolling periods) or 1mo, 3mo, 1y (natural periods) */\n period?: AnalysisPeriod;\n}\n\n/**\n * Standard API response wrapper\n */\ninterface ApiResponse<T> {\n data: T;\n success: boolean;\n}\n\n/**\n * Analysis Management Service\n *\n * Provides administrative functionality for accessing market analysis and statistics.\n * This service handles retrieving various analytics reports including market overview,\n * plugin trends, and installation analytics for administrative dashboards.\n */\nexport class AnalysisService extends BaseSDK {\n /**\n * Retrieves market overview statistics\n *\n * Returns comprehensive market statistics including plugin counts,\n * installation metrics, new plugin trends, and rating averages\n * with comparison to previous periods.\n *\n * @param params - Query parameters for the analysis\n * @returns Promise resolving to market overview statistics\n */\n async getMarketOverview(params: MarketOverviewQuery = {}): Promise<MarketOverviewStats> {\n const { period = '30d' } = params;\n\n log('Getting market overview statistics for period: %s', period);\n\n const searchParams = new URLSearchParams();\n if (period) {\n searchParams.append('period', period);\n }\n\n const url = `/admin/analysis/plugin/overview${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;\n\n const result = await this.request<ApiResponse<MarketOverviewStats>>(url);\n\n log('Market overview statistics retrieved successfully: %s', result.data);\n\n return result.data;\n }\n\n /**\n * Retrieves market overview statistics for 1 day period\n *\n * Convenience method for getting daily market statistics.\n *\n * @returns Promise resolving to market overview statistics for 1 day\n */\n async getDailyOverview(): Promise<MarketOverviewStats> {\n log('Getting daily market overview');\n return this.getMarketOverview({ period: '1d' });\n }\n\n /**\n * Retrieves market overview statistics for 7 days period\n *\n * Convenience method for getting weekly market statistics.\n *\n * @returns Promise resolving to market overview statistics for 7 days\n */\n async getWeeklyOverview(): Promise<MarketOverviewStats> {\n log('Getting weekly market overview');\n return this.getMarketOverview({ period: '7d' });\n }\n\n /**\n * Retrieves market overview statistics for 30 days period\n *\n * Convenience method for getting monthly market statistics.\n *\n * @returns Promise resolving to market overview statistics for 30 days\n */\n async getMonthlyOverview(): Promise<MarketOverviewStats> {\n log('Getting monthly market overview');\n return this.getMarketOverview({ period: '30d' });\n }\n\n /**\n * Retrieves market overview statistics for current natural month\n *\n * Convenience method for getting current month vs previous month statistics.\n *\n * @returns Promise resolving to market overview statistics for current month\n */\n async getThisMonthOverview(): Promise<MarketOverviewStats> {\n log('Getting this month market overview');\n return this.getMarketOverview({ period: '1mo' });\n }\n\n /**\n * Retrieves market overview statistics for current quarter\n *\n * Convenience method for getting current quarter vs previous quarter statistics.\n *\n * @returns Promise resolving to market overview statistics for current quarter\n */\n async getQuarterlyOverview(): Promise<MarketOverviewStats> {\n log('Getting quarterly market overview');\n return this.getMarketOverview({ period: '3mo' });\n }\n\n /**\n * Retrieves market overview statistics for current year\n *\n * Convenience method for getting current year vs previous year statistics.\n *\n * @returns Promise resolving to market overview statistics for current year\n */\n async getYearlyOverview(): Promise<MarketOverviewStats> {\n log('Getting yearly market overview');\n return this.getMarketOverview({ period: '1y' });\n }\n\n /**\n * Retrieves install failure analysis for plugins within a date range\n *\n * Returns detailed analysis of plugin installation failures including failure counts,\n * failure rates, and most common error messages for each plugin with failures.\n *\n * @param params - Query parameters for the failure analysis\n * @returns Promise resolving to install failure analysis data\n */\n async getInstallFailureAnalysis(\n params: InstallFailureAnalysisQuery,\n ): Promise<InstallFailureAnalysis[]> {\n const { range, limit = 15 } = params;\n\n log('Getting install failure analysis for range: %o, limit: %d', range, limit);\n\n const searchParams = new URLSearchParams();\n searchParams.append('range', JSON.stringify(range));\n if (limit !== 15) {\n searchParams.append('limit', limit.toString());\n }\n\n const url = `/admin/analysis/plugin/install-failure?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<InstallFailureAnalysis[]>>(url);\n\n log('Install failure analysis retrieved successfully for %d plugins', result.data.length);\n\n return result.data;\n }\n\n /**\n * Calculates growth rate between current and previous values\n *\n * Utility method for calculating percentage growth rates from the statistics.\n *\n * @param current - Current period value\n * @param previous - Previous period value\n * @returns Growth rate as percentage (e.g., 15.5 for 15.5% growth)\n */\n static calculateGrowthRate(current: number, previous: number): number {\n if (previous === 0) return current > 0 ? 100 : 0;\n return Math.round(((current - previous) / previous) * 100 * 10) / 10;\n }\n\n /**\n * Formats market overview statistics with calculated growth rates\n *\n * Utility method that enhances the raw statistics with calculated growth rates\n * for easier consumption in dashboards and reports.\n *\n * @param stats - Raw market overview statistics\n * @returns Enhanced statistics with growth rate calculations\n */\n static formatMarketOverview(stats: MarketOverviewStats) {\n return {\n ...stats,\n growth: {\n devices: this.calculateGrowthRate(stats.devices.count, stats.devices.prevCount),\n installs: this.calculateGrowthRate(stats.installs.count, stats.installs.prevCount),\n pluginCalls: this.calculateGrowthRate(stats.pluginCalls.count, stats.pluginCalls.prevCount),\n plugins: this.calculateGrowthRate(stats.plugins.count, stats.plugins.prevCount),\n },\n };\n }\n\n /**\n * Retrieves installation trend statistics for a specified date range\n *\n * Returns daily installation counts and trends for the specified period\n * with optional comparison to a previous period.\n *\n * @param params - Query parameters including date range and display config\n * @returns Promise resolving to installation trend statistics\n */\n async getRangeInstalls(params: RangeQuery): Promise<RangeStats> {\n const { display, range, prevRange } = params;\n\n log('Getting installation trend statistics for range: %s to %s', range[0], range[1]);\n\n const searchParams = new URLSearchParams();\n searchParams.append('display', display);\n searchParams.append('range', range.join(','));\n if (prevRange) {\n searchParams.append('prevRange', prevRange.join(','));\n }\n\n const url = `/admin/analysis/plugin/range-installs?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<RangeStats>>(url);\n\n log(\n 'Installation trend statistics retrieved successfully: %d data points',\n result.data.data.length,\n );\n\n return result.data;\n }\n\n /**\n * Retrieves plugin growth trend statistics for a specified date range\n *\n * Returns daily plugin creation counts and trends for the specified period\n * with optional comparison to a previous period.\n *\n * @param params - Query parameters including date range and display config\n * @returns Promise resolving to plugin growth trend statistics\n */\n async getRangePlugins(params: RangeQuery): Promise<RangeStats> {\n const { display, range, prevRange } = params;\n\n log('Getting plugin growth trend statistics for range: %s to %s', range[0], range[1]);\n\n const searchParams = new URLSearchParams();\n searchParams.append('display', display);\n searchParams.append('range', range.join(','));\n if (prevRange) {\n searchParams.append('prevRange', prevRange.join(','));\n }\n\n const url = `/admin/analysis/plugin/range-plugins?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<RangeStats>>(url);\n\n log(\n 'Plugin growth trend statistics retrieved successfully: %d data points',\n result.data.data.length,\n );\n\n return result.data;\n }\n\n /**\n * Retrieves device growth trend statistics for a specified date range\n *\n * Note: This is a system-level statistic that tracks device registrations,\n * not plugin-specific metrics. It provides daily device registration counts\n * and trends for the specified period with optional comparison to a previous period.\n *\n * @param params - Query parameters including date range and display config\n * @returns Promise resolving to device growth trend statistics\n */\n async getRangeDevices(params: RangeQuery): Promise<RangeStats> {\n const { display, range, prevRange } = params;\n\n log('Getting device growth trend statistics for range: %s to %s', range[0], range[1]);\n\n const searchParams = new URLSearchParams();\n searchParams.append('display', display);\n searchParams.append('range', range.join(','));\n if (prevRange) {\n searchParams.append('prevRange', prevRange.join(','));\n }\n\n const url = `/admin/analysis/system/range-devices?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<RangeStats>>(url);\n\n log(\n 'Device growth trend statistics retrieved successfully: %d data points',\n result.data.data.length,\n );\n\n return result.data;\n }\n\n /**\n * Retrieves plugin call trend statistics for a specified date range\n *\n * Returns daily plugin call counts and trends for the specified period\n * with optional comparison to a previous period.\n *\n * @param params - Query parameters including date range and display config\n * @returns Promise resolving to plugin call trend statistics\n */\n async getRangeCalls(params: RangeQuery): Promise<RangeStats> {\n const { display, range, prevRange } = params;\n\n log('Getting plugin call trend statistics for range: %s to %s', range[0], range[1]);\n\n const searchParams = new URLSearchParams();\n searchParams.append('display', display);\n searchParams.append('range', range.join(','));\n if (prevRange) {\n searchParams.append('prevRange', prevRange.join(','));\n }\n\n const url = `/admin/analysis/plugin/range-calls?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<RangeStats>>(url);\n\n log(\n 'Plugin call trend statistics retrieved successfully: %d data points',\n result.data.data.length,\n );\n\n return result.data;\n }\n\n /**\n * Calculates trend growth rate between current and previous period totals\n *\n * Utility method for calculating percentage growth rates from range statistics.\n *\n * @param stats - Range statistics with sum and prevSum\n * @returns Growth rate as percentage (e.g., 15.5 for 15.5% growth)\n */\n static calculateTrendGrowthRate(stats: RangeStats): number {\n return this.calculateGrowthRate(stats.sum, stats.prevSum);\n }\n\n /**\n * Retrieves top plugins sorted by specified criteria within a date range\n *\n * Returns list of plugins sorted by the specified criteria (installs or calls)\n * in descending order for the specified date range.\n *\n * @param params - Query parameters including date range, sort criteria, and limit\n * @returns Promise resolving to list of top plugins\n */\n async getTopPlugins(params: TopPluginsQuery): Promise<TopPlugin[]> {\n const { range, sortBy = 'installs', limit = 10 } = params;\n\n const searchParams = new URLSearchParams();\n searchParams.append('range', range.join(','));\n searchParams.append('sortBy', sortBy);\n searchParams.append('limit', limit.toString());\n\n const url = `/admin/analysis/plugin/top-plugins?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<TopPlugin[]>>(url);\n\n return result.data;\n }\n}\n","import type {\n AdminDeploymentOption,\n AdminPluginItem,\n AdminPluginItemDetail,\n IncompleteI18nPlugin,\n InstallationDetails,\n PluginManifest,\n PluginVersion,\n PluginVersionLocalization,\n SystemDependency,\n} from '@lobehub/market-types';\nimport debug from 'debug';\n\nimport { BaseSDK } from '@/core/BaseSDK';\nimport {\n AdminListQueryParams,\n AdminListResponse,\n PluginI18nImportParams,\n PluginI18nImportResponse,\n PluginUpdateParams,\n PluginVersionCreateParams,\n PluginVersionUpdateParams,\n UnclaimedPluginItem,\n} from '@/types';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:admin:plugins');\n\n/**\n * Plugin Management Service\n *\n * Provides administrative functionality for managing plugins in the marketplace.\n * This service handles CRUD operations for plugins, plugin versions, and deployment options.\n */\nexport class PluginService extends BaseSDK {\n /**\n * Batch imports plugin manifests using the dedicated import endpoint\n *\n * This method is intended for use with scripts and bulk import operations.\n *\n * @param manifests - Array of plugin manifests to import\n * @param ownerId - Optional owner ID to associate with the imported plugins\n * @returns Promise resolving to the import results with counts of success, skipped, failed, and a list of failed IDs\n */\n async importPlugins(\n manifests: PluginManifest[],\n ownerId?: number,\n ): Promise<{ failed: number; failedIds: string[]; skipped: number; success: number }> {\n log(`Starting batch plugin import of ${manifests.length} manifests`);\n if (ownerId) {\n log(`Using specified owner ID for import: ${ownerId}`);\n }\n\n const response = await this.request<{\n data: { failed: number; failedIds: string[]; skipped: number; success: number };\n }>('/admin/plugins/import', {\n body: JSON.stringify({\n manifests,\n ownerId,\n }),\n method: 'POST',\n });\n\n log(\n `Plugin import completed: ${response.data.success} succeeded, ${response.data.skipped} skipped, ${response.data.failed} failed`,\n );\n return response.data;\n }\n\n /**\n * Imports plugin internationalization (i18n) data\n *\n * Allows importing localized content for a specific plugin version.\n * This method creates or updates localizations for the specified plugin.\n *\n * @param params - Plugin i18n import parameters containing identifier, version, and localizations\n * @returns Promise resolving to the import results with counts of success and failure\n */\n async importPluginI18n(params: PluginI18nImportParams): Promise<PluginI18nImportResponse> {\n log(\n `Starting i18n import for plugin ${params.identifier} v${params.version} with ${params.localizations.length} localizations`,\n );\n\n const response = await this.request<{\n data: PluginI18nImportResponse;\n message: string;\n }>('/admin/plugins/import/i18n', {\n body: JSON.stringify(params),\n method: 'POST',\n });\n\n log(\n `Plugin i18n import completed: ${response.data.success} succeeded, ${response.data.failed} failed, ${response.data.totalLocalizations} total`,\n );\n return response.data;\n }\n\n /**\n * Retrieves a list of plugins with admin details\n *\n * Supports filtering, pagination, and sorting of results.\n *\n * @param params - Query parameters for filtering and pagination\n * @returns Promise resolving to the plugin list response with admin details\n */\n async getPlugins(params: AdminListQueryParams = {}): Promise<AdminListResponse<AdminPluginItem>> {\n log('Getting plugins with params: %O', params);\n\n const queryString = this.buildQueryString(params);\n const url = `/admin/plugins${queryString}`;\n\n const result = await this.request<AdminListResponse<AdminPluginItem>>(url);\n\n log('Retrieved %d plugins', result.data.length);\n return result;\n }\n\n /**\n * Retrieves all published plugin identifiers\n *\n * Returns a lightweight list of all published plugin identifiers without\n * full plugin metadata. This is useful for admin operations that need to know\n * which plugins are currently published and available to users.\n *\n * @returns Promise resolving to an array containing identifiers array and last modified time\n */\n async getPublishedIdentifiers(): Promise<{ identifier: string; lastModified: string }[]> {\n log('Getting published plugin identifiers (admin)');\n\n const result =\n await this.request<{ identifier: string; lastModified: string }[]>('/v1/plugins/identifiers');\n log('Retrieved %d published plugin identifiers (admin)', result.length);\n return result;\n }\n\n /**\n * Retrieves a single plugin with full admin details\n *\n * @param id - Plugin ID or identifier\n * @returns Promise resolving to the detailed plugin information with version history\n */\n async getPlugin(id: number | string): Promise<AdminPluginItemDetail> {\n log('Getting plugin details (admin): %d', id);\n\n const result = await this.request<AdminPluginItemDetail>(`/admin/plugins/${id}`);\n log('Retrieved plugin with %d versions', result.versions.length);\n return result;\n }\n\n /**\n * Retrieves a plugin by its GitHub repository URL\n *\n * @param githubUrl - The GitHub repository URL to search for\n * @returns Promise resolving to the detailed plugin information with version history\n */\n async getPluginByGithubUrl(githubUrl: string): Promise<AdminPluginItemDetail> {\n log('Getting plugin by GitHub URL: %s', githubUrl);\n\n const queryString = this.buildQueryString({ url: githubUrl });\n const result = await this.request<AdminPluginItemDetail>(\n `/admin/plugins/by-gi