mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-22 19:32:55 -05:00
1584 lines
47 KiB
TypeScript
1584 lines
47 KiB
TypeScript
import { APIRequestContext, expect, APIResponse } from "@playwright/test";
|
|
|
|
const E2E_LLM_PROVIDER_API_KEY =
|
|
process.env.E2E_LLM_PROVIDER_API_KEY ||
|
|
process.env.OPENAI_API_KEY ||
|
|
"e2e-placeholder-api-key-not-used";
|
|
|
|
const E2E_WEB_SEARCH_API_KEY =
|
|
process.env.E2E_WEB_SEARCH_API_KEY ||
|
|
process.env.EXA_API_KEY ||
|
|
process.env.BRAVE_SEARCH_API_KEY ||
|
|
process.env.SERPER_API_KEY ||
|
|
"e2e-placeholder-web-search-key";
|
|
|
|
const E2E_IMAGE_GEN_API_KEY =
|
|
process.env.E2E_IMAGE_GEN_API_KEY ||
|
|
process.env.OPENAI_API_KEY ||
|
|
E2E_LLM_PROVIDER_API_KEY;
|
|
|
|
/** Subset of the backend `MCPToolCreateRequest` needed to provision a server. */
|
|
export interface McpServerCreateRequest {
|
|
name: string;
|
|
description?: string;
|
|
server_url: string;
|
|
auth_type: "NONE" | "API_TOKEN" | "OAUTH" | "PT_OAUTH";
|
|
auth_performer: "ADMIN" | "PER_USER";
|
|
api_token?: string;
|
|
oauth_client_id?: string;
|
|
oauth_client_secret?: string;
|
|
transport?: "STREAMABLE_HTTP" | "SSE";
|
|
auth_template?: {
|
|
headers: Record<string, string>;
|
|
required_fields?: string[];
|
|
};
|
|
admin_credentials?: Record<string, string>;
|
|
admin_credentials_changed?: Record<string, boolean>;
|
|
existing_server_id?: number;
|
|
}
|
|
|
|
/** A tool row returned by the `db-tools` endpoint (server prefix stripped). */
|
|
export interface McpDbTool {
|
|
id: number;
|
|
name: string;
|
|
display_name: string;
|
|
description: string;
|
|
}
|
|
|
|
/** Options for creating an agent (persona) directly via the API. */
|
|
export interface CreateAgentOptions {
|
|
instructions?: string;
|
|
description?: string;
|
|
isPublic?: boolean;
|
|
userIds?: string[];
|
|
groupIds?: number[];
|
|
}
|
|
|
|
/**
|
|
* API Client for Onyx backend operations in E2E tests.
|
|
*
|
|
* Provides a type-safe, abstracted interface for interacting with the Onyx backend API.
|
|
* All methods handle authentication via the Playwright page context and include automatic
|
|
* error handling, logging, and polling for asynchronous operations.
|
|
*
|
|
* **Available Endpoints:**
|
|
*
|
|
* **Connectors:**
|
|
* - `createFileConnector(name)` - Creates a file connector with mock credentials
|
|
* - `findCCPairByName(source, name)` - Looks up a connector-credential pair ID by source + name
|
|
* - `deleteCCPair(ccPairId)` - Deletes a connector-credential pair (with polling until complete)
|
|
*
|
|
* **Document Sets:**
|
|
* - `createDocumentSet(name, ccPairIds)` - Creates a document set from connector pairs
|
|
* - `deleteDocumentSet(id)` - Deletes a document set (with polling until complete)
|
|
*
|
|
* **LLM Providers:**
|
|
* - `listLlmProviders()` - Lists LLM providers (admin endpoint, includes is_public)
|
|
* - `ensurePublicProvider(name?)` - Idempotently creates a public default LLM provider
|
|
* - `createRestrictedProvider(name, groupId)` - Creates a restricted LLM provider assigned to a group
|
|
* - `setProviderAsDefault(id)` - Sets an LLM provider as the default for chat
|
|
* - `deleteProvider(id)` - Deletes an LLM provider
|
|
*
|
|
* **User Groups:**
|
|
* - `getUserGroups()` - Lists all user groups (including default system groups)
|
|
* - `createUserGroup(name)` - Creates a user group
|
|
* - `deleteUserGroup(id)` - Deletes a user group
|
|
*
|
|
* **Tool Providers:**
|
|
* - `createWebSearchProvider(type, name)` - Creates and activates a web search provider
|
|
* - `deleteWebSearchProvider(id)` - Deletes a web search provider
|
|
* - `createImageGenerationConfig(id, model, provider, isDefault)` - Creates an image generation config (enables image gen tool)
|
|
* - `deleteImageGenerationConfig(id)` - Deletes an image generation config
|
|
*
|
|
* **Chat Sessions:**
|
|
* - `createChatSession(description, personaId?)` - Creates a chat session with a description
|
|
* - `deleteChatSession(chatId)` - Deletes a chat session
|
|
*
|
|
* **Projects:**
|
|
* - `createProject(name)` - Creates a project with a name
|
|
* - `deleteProject(projectId)` - Deletes a project
|
|
*
|
|
* **Usage Example:**
|
|
* ```typescript
|
|
* // From a test with a Page:
|
|
* const client = new OnyxApiClient(page.request);
|
|
*
|
|
* // From global-setup with a standalone context (pass baseURL explicitly):
|
|
* const ctx = await request.newContext({ baseURL, storageState: "admin_auth.json" });
|
|
* const client = new OnyxApiClient(ctx, baseURL);
|
|
* ```
|
|
*
|
|
* @param request - Playwright APIRequestContext with authenticated session
|
|
* (e.g. `page.request`, `context.request`, or `request.newContext()`)
|
|
* @param baseUrl - Optional base URL override (e.g. `http://localhost:3000`).
|
|
* Defaults to `process.env.BASE_URL` or `http://localhost:3000`.
|
|
* Pass this when the Playwright-configured baseURL differs from
|
|
* the env var (e.g. in `global-setup.ts` where the config value
|
|
* is authoritative).
|
|
*/
|
|
export class OnyxApiClient {
|
|
private baseUrl: string;
|
|
|
|
constructor(
|
|
private request: APIRequestContext,
|
|
baseUrl?: string
|
|
) {
|
|
this.baseUrl = `${
|
|
baseUrl ?? process.env.BASE_URL ?? "http://localhost:3000"
|
|
}/api`;
|
|
}
|
|
|
|
/**
|
|
* Generic GET request to the API.
|
|
*
|
|
* @param endpoint - API endpoint path (e.g., "/manage/document-set/123")
|
|
* @returns The API response
|
|
*/
|
|
private async get(endpoint: string): Promise<APIResponse> {
|
|
return await this.request.get(`${this.baseUrl}${endpoint}`);
|
|
}
|
|
|
|
/**
|
|
* Generic POST request to the API.
|
|
*
|
|
* @param endpoint - API endpoint path (e.g., "/manage/admin/document-set")
|
|
* @param data - Optional request body data
|
|
* @returns The API response
|
|
*/
|
|
private async post(endpoint: string, data?: any): Promise<APIResponse> {
|
|
return await this.request.post(`${this.baseUrl}${endpoint}`, {
|
|
data,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generic DELETE request to the API.
|
|
*
|
|
* @param endpoint - API endpoint path (e.g., "/manage/admin/document-set/123")
|
|
* @returns The API response
|
|
*/
|
|
private async delete(endpoint: string): Promise<APIResponse> {
|
|
return await this.request.delete(`${this.baseUrl}${endpoint}`);
|
|
}
|
|
|
|
/**
|
|
* Generic PUT request to the API.
|
|
*
|
|
* @param endpoint - API endpoint path (e.g., "/manage/admin/cc-pair/123/status")
|
|
* @param data - Optional request body data
|
|
* @returns The API response
|
|
*/
|
|
private async put(endpoint: string, data?: any): Promise<APIResponse> {
|
|
return await this.request.put(`${this.baseUrl}${endpoint}`, {
|
|
data,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generic PATCH request to the API.
|
|
*
|
|
* @param endpoint - API endpoint path (e.g., "/paste-as-tile?paste_as_tile=true")
|
|
* @param data - Optional request body data
|
|
* @returns The API response
|
|
*/
|
|
private async patch(endpoint: string, data?: any): Promise<APIResponse> {
|
|
return await this.request.patch(`${this.baseUrl}${endpoint}`, {
|
|
data,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle API response - parse JSON and handle errors.
|
|
*
|
|
* @param response - The API response to handle
|
|
* @param errorMessage - Error message prefix to use if request failed
|
|
* @returns Parsed JSON response data
|
|
* @throws Error if the response is not ok
|
|
*/
|
|
private async handleResponse<T>(
|
|
response: APIResponse,
|
|
errorMessage: string
|
|
): Promise<T> {
|
|
if (!response.ok()) {
|
|
const errorText = await response.text();
|
|
throw new Error(`${errorMessage}: ${response.status()} - ${errorText}`);
|
|
}
|
|
return await response.json();
|
|
}
|
|
|
|
/**
|
|
* Handle API response with logging on error (non-throwing).
|
|
* Used for cleanup operations where we want to log errors but not fail the test.
|
|
*
|
|
* @param response - The API response to handle
|
|
* @param errorMessage - Error message prefix to use if request failed
|
|
* @returns true if response was ok, false otherwise
|
|
*/
|
|
private async handleResponseSoft(
|
|
response: APIResponse,
|
|
errorMessage: string
|
|
): Promise<boolean> {
|
|
if (!response.ok()) {
|
|
const errorText = await response.text();
|
|
console.error(
|
|
`[OnyxApiClient] ${errorMessage}: ${response.status()} - ${errorText}`
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Wait for a resource to be deleted by polling until 404.
|
|
* Uses Playwright's expect.poll() with automatic retry and exponential backoff.
|
|
* We poll here because the deletion endpoint is asynchronous (kicks off a celery task)
|
|
* and we want to wait for it to complete.
|
|
*
|
|
* @param endpoint - API endpoint to poll (e.g., "/manage/document-set/123")
|
|
* @param resourceType - Human-readable resource type for error messages (e.g., "Document set")
|
|
* @param resourceId - The resource ID for error messages
|
|
* @param timeout - Maximum time to wait in milliseconds (default: 30000)
|
|
* @returns Promise that resolves when resource returns 404, or rejects on timeout
|
|
*/
|
|
private async waitForDeletion(
|
|
endpoint: string,
|
|
resourceType: string,
|
|
resourceId: number | string,
|
|
timeout: number = 30000
|
|
): Promise<void> {
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const checkResponse = await this.get(endpoint);
|
|
return checkResponse.status();
|
|
},
|
|
{
|
|
message: `${resourceType} ${resourceId} was not deleted`,
|
|
timeout,
|
|
}
|
|
)
|
|
.toBe(404);
|
|
}
|
|
|
|
/**
|
|
* Log an action with consistent formatting.
|
|
*
|
|
* @param message - The message to log (will be prefixed with "[OnyxApiClient]")
|
|
*/
|
|
private log(message: string): void {
|
|
console.log(`[OnyxApiClient] ${message}`);
|
|
}
|
|
|
|
/**
|
|
* Checks whether the vector database is enabled in this deployment.
|
|
*
|
|
* @returns true if vector DB is enabled, false if DISABLE_VECTOR_DB is set
|
|
*/
|
|
async isVectorDbEnabled(): Promise<boolean> {
|
|
const response = await this.get("/settings");
|
|
const data = await this.handleResponse<{ vector_db_enabled: boolean }>(
|
|
response,
|
|
"Failed to fetch settings"
|
|
);
|
|
return data.vector_db_enabled;
|
|
}
|
|
|
|
/**
|
|
* Creates a simple file connector with mock credentials.
|
|
* This enables the Knowledge toggle in assistant creation.
|
|
*
|
|
* @param connectorName - Name for the connector (defaults to "Test File Connector")
|
|
* @param accessType - Access type for the connector (defaults to "public")
|
|
* @returns The connector-credential pair ID (ccPairId)
|
|
* @throws Error if the connector creation fails
|
|
*/
|
|
async createFileConnector(
|
|
connectorName: string = "Test File Connector",
|
|
accessType: "public" | "private" = "public"
|
|
): Promise<number> {
|
|
const response = await this.post(
|
|
"/manage/admin/connector-with-mock-credential",
|
|
{
|
|
name: connectorName,
|
|
source: "file",
|
|
input_type: "load_state",
|
|
connector_specific_config: {
|
|
file_locations: [],
|
|
},
|
|
refresh_freq: null,
|
|
prune_freq: null,
|
|
indexing_start: null,
|
|
access_type: accessType,
|
|
groups: [],
|
|
}
|
|
);
|
|
|
|
const responseData = await this.handleResponse<{ data: number }>(
|
|
response,
|
|
"Failed to create connector"
|
|
);
|
|
|
|
const ccPairId = responseData.data;
|
|
this.log(
|
|
`Created file connector: ${connectorName} (CC Pair ID: ${ccPairId})`
|
|
);
|
|
|
|
// Pause the connector immediately to prevent indexing during tests
|
|
await this.pauseConnector(ccPairId);
|
|
|
|
return ccPairId;
|
|
}
|
|
|
|
/**
|
|
* Pauses a connector-credential pair to prevent indexing.
|
|
*
|
|
* @param ccPairId - The connector-credential pair ID to pause
|
|
* @throws Error if the pause operation fails
|
|
*/
|
|
async pauseConnector(ccPairId: number): Promise<void> {
|
|
const response = await this.put(
|
|
`/manage/admin/cc-pair/${ccPairId}/status`,
|
|
{
|
|
status: "PAUSED",
|
|
}
|
|
);
|
|
|
|
await this.handleResponse(response, "Failed to pause connector");
|
|
this.log(`Paused connector CC Pair ID: ${ccPairId}`);
|
|
}
|
|
|
|
/**
|
|
* Finds a connector-credential pair by source and exact connector name.
|
|
* Useful for cleaning up connectors created through the UI, where the test
|
|
* never sees the ccPairId directly.
|
|
*
|
|
* @param source - The connector source (e.g. "web", "file")
|
|
* @param name - The exact connector name to match
|
|
* @returns The ccPairId, or null if no connector with that name exists
|
|
*/
|
|
async findCCPairByName(source: string, name: string): Promise<number | null> {
|
|
const response = await this.post(
|
|
"/manage/admin/connector/indexing-status",
|
|
{
|
|
source,
|
|
name_filter: name,
|
|
get_all_connectors: true,
|
|
}
|
|
);
|
|
|
|
const sourceGroups = await this.handleResponse<
|
|
{ indexing_statuses: { cc_pair_id: number; name: string }[] }[]
|
|
>(response, "Failed to fetch connector indexing status");
|
|
|
|
for (const group of sourceGroups) {
|
|
const match = group.indexing_statuses.find(
|
|
(status) => status.name === name
|
|
);
|
|
if (match) {
|
|
return match.cc_pair_id;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Creates a document set from connector-credential pairs.
|
|
*
|
|
* @param documentSetName - Name for the document set
|
|
* @param ccPairIds - Array of connector-credential pair IDs to include in the set
|
|
* @returns The document set ID
|
|
* @throws Error if the document set creation fails
|
|
*/
|
|
async createDocumentSet(
|
|
documentSetName: string,
|
|
ccPairIds: number[]
|
|
): Promise<number> {
|
|
const response = await this.post("/manage/admin/document-set", {
|
|
name: documentSetName,
|
|
description: `Test document set: ${documentSetName}`,
|
|
cc_pair_ids: ccPairIds,
|
|
is_public: true,
|
|
users: [],
|
|
groups: [],
|
|
federated_connectors: [],
|
|
});
|
|
|
|
const documentSetId = await this.handleResponse<number>(
|
|
response,
|
|
"Failed to create document set"
|
|
);
|
|
|
|
this.log(`Created document set: ${documentSetName} (ID: ${documentSetId})`);
|
|
return documentSetId;
|
|
}
|
|
|
|
/**
|
|
* Deletes a document set and waits for deletion to complete.
|
|
* Uses polling to verify the deletion was successful (waits for 404 response).
|
|
*
|
|
* @param documentSetId - The document set ID to delete
|
|
* @returns Promise that resolves when deletion is confirmed, or rejects on timeout
|
|
*/
|
|
async deleteDocumentSet(documentSetId: number): Promise<void> {
|
|
const response = await this.delete(
|
|
`/manage/admin/document-set/${documentSetId}`
|
|
);
|
|
|
|
if (
|
|
!(await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete document set ${documentSetId}`
|
|
))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.log(`Initiated deletion for document set: ${documentSetId}`);
|
|
await this.waitForDeletion(
|
|
`/manage/document-set/${documentSetId}`,
|
|
"Document set",
|
|
documentSetId
|
|
);
|
|
this.log(`Document set ${documentSetId} deletion confirmed`);
|
|
}
|
|
|
|
/**
|
|
* Deletes a connector-credential pair and waits for deletion to complete.
|
|
* Fetches the CC pair details to get connector/credential IDs, then initiates deletion
|
|
* and polls until the deletion is confirmed (waits for 404 response).
|
|
*
|
|
* @param ccPairId - The connector-credential pair ID to delete
|
|
* @returns Promise that resolves when deletion is confirmed, or rejects on timeout
|
|
*/
|
|
async deleteCCPair(ccPairId: number): Promise<void> {
|
|
// Get CC pair details to extract connector_id and credential_id
|
|
const getResponse = await this.get(`/manage/admin/cc-pair/${ccPairId}`);
|
|
|
|
if (
|
|
!(await this.handleResponseSoft(
|
|
getResponse,
|
|
`Failed to get CC pair ${ccPairId} details`
|
|
))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const ccPairInfo = await getResponse.json();
|
|
const {
|
|
connector: { id: connectorId },
|
|
credential: { id: credentialId },
|
|
} = ccPairInfo;
|
|
|
|
// Delete using the deletion-attempt endpoint
|
|
const deleteResponse = await this.post("/manage/admin/deletion-attempt", {
|
|
connector_id: connectorId,
|
|
credential_id: credentialId,
|
|
});
|
|
|
|
if (
|
|
!(await this.handleResponseSoft(
|
|
deleteResponse,
|
|
`Failed to delete CC pair ${ccPairId}`
|
|
))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.log(
|
|
`Initiated deletion for CC pair: ${ccPairId} (connector: ${connectorId}, credential: ${credentialId})`
|
|
);
|
|
await this.waitForDeletion(
|
|
`/manage/admin/cc-pair/${ccPairId}`,
|
|
"CC pair",
|
|
ccPairId
|
|
);
|
|
this.log(`CC pair ${ccPairId} deletion confirmed`);
|
|
}
|
|
|
|
/**
|
|
* Creates a restricted LLM provider assigned to a specific user group.
|
|
*
|
|
* @param providerName - Name for the provider
|
|
* @param groupId - The user group ID that should have access to this provider
|
|
* @returns The provider ID
|
|
* @throws Error if the provider creation fails
|
|
*/
|
|
async createRestrictedProvider(
|
|
providerName: string,
|
|
groupId: number
|
|
): Promise<number> {
|
|
const response = await this.request.put(
|
|
`${this.baseUrl}/admin/llm/provider?is_creation=true`,
|
|
{
|
|
data: {
|
|
name: providerName,
|
|
provider: "openai",
|
|
api_key: E2E_LLM_PROVIDER_API_KEY,
|
|
default_model_name: "gpt-4o",
|
|
is_public: false,
|
|
groups: [groupId],
|
|
personas: [],
|
|
},
|
|
}
|
|
);
|
|
|
|
const responseData = await this.handleResponse<{ id: number }>(
|
|
response,
|
|
"Failed to create restricted provider"
|
|
);
|
|
|
|
this.log(
|
|
`Created restricted LLM provider: ${providerName} (ID: ${responseData.id}, Group: ${groupId})`
|
|
);
|
|
return responseData.id;
|
|
}
|
|
|
|
/**
|
|
* Lists LLM providers visible to the admin (includes `is_public`).
|
|
*
|
|
* @returns Array of LLM providers with id and is_public fields
|
|
*/
|
|
async listLlmProviders(): Promise<
|
|
Array<{
|
|
id: number;
|
|
is_public?: boolean;
|
|
}>
|
|
> {
|
|
const response = await this.get("/admin/llm/provider");
|
|
const data = await this.handleResponse<{
|
|
providers: Array<{ id: number; is_public?: boolean }>;
|
|
}>(response, "Failed to list LLM providers");
|
|
return data.providers;
|
|
}
|
|
|
|
/**
|
|
* Ensure at least one public LLM provider exists and is set as default.
|
|
*
|
|
* Idempotent — returns `null` if a public provider already exists,
|
|
* or the new provider ID if one was created.
|
|
*
|
|
* @param providerName - Name for the provider (default: "PW Default Provider")
|
|
* @returns The provider ID if one was created, or `null` if already present
|
|
*/
|
|
async ensurePublicProvider(
|
|
providerName: string = "PW Default Provider"
|
|
): Promise<number | null> {
|
|
const providers = await this.listLlmProviders();
|
|
const hasPublic = providers.some((p) => p.is_public);
|
|
|
|
if (hasPublic) {
|
|
return null;
|
|
}
|
|
|
|
const defaultModelName = "gpt-4o";
|
|
const response = await this.request.put(
|
|
`${this.baseUrl}/admin/llm/provider?is_creation=true`,
|
|
{
|
|
data: {
|
|
name: providerName,
|
|
provider: "openai",
|
|
api_key: E2E_LLM_PROVIDER_API_KEY,
|
|
is_public: true,
|
|
groups: [],
|
|
personas: [],
|
|
model_configurations: [{ name: defaultModelName, is_visible: true }],
|
|
},
|
|
}
|
|
);
|
|
|
|
const responseData = await this.handleResponse<{ id: number }>(
|
|
response,
|
|
"Failed to create public provider"
|
|
);
|
|
|
|
// Set as default so get_default_llm() works (needed for tokenization, etc.)
|
|
await this.setProviderAsDefault(responseData.id, defaultModelName);
|
|
|
|
this.log(
|
|
`Created public LLM provider: ${providerName} (ID: ${responseData.id})`
|
|
);
|
|
return responseData.id;
|
|
}
|
|
|
|
/**
|
|
* Sets an LLM provider + model as the default for chat.
|
|
*
|
|
* @param providerId - The provider ID to set as default
|
|
* @param modelName - The model name to set as default
|
|
*/
|
|
async setProviderAsDefault(
|
|
providerId: number,
|
|
modelName: string
|
|
): Promise<void> {
|
|
const response = await this.post("/admin/llm/default", {
|
|
provider_id: providerId,
|
|
model_name: modelName,
|
|
});
|
|
|
|
await this.handleResponseSoft(
|
|
response,
|
|
`Failed to set provider ${providerId} as default`
|
|
);
|
|
|
|
this.log(`Set LLM provider ${providerId} as default`);
|
|
}
|
|
|
|
/**
|
|
* Deletes an LLM provider.
|
|
*
|
|
* @param providerId - The provider ID to delete
|
|
*/
|
|
async deleteProvider(
|
|
providerId: number,
|
|
{ force = false }: { force?: boolean } = {}
|
|
): Promise<void> {
|
|
const query = force ? "?force=true" : "";
|
|
const response = await this.delete(
|
|
`/admin/llm/provider/${providerId}${query}`
|
|
);
|
|
|
|
await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete provider ${providerId}`
|
|
);
|
|
|
|
this.log(`Deleted LLM provider: ${providerId}`);
|
|
}
|
|
|
|
/**
|
|
* Creates a user group.
|
|
*
|
|
* @param groupName - Name for the user group
|
|
* @param userIds - Optional list of user IDs to add to the group
|
|
* @param ccPairIds - Optional list of connector-credential pair IDs to associate
|
|
* @returns The user group ID
|
|
* @throws Error if the user group creation fails
|
|
*/
|
|
async createUserGroup(
|
|
groupName: string,
|
|
userIds: string[] = [],
|
|
ccPairIds: number[] = []
|
|
): Promise<number> {
|
|
const response = await this.post("/manage/admin/user-group", {
|
|
name: groupName,
|
|
user_ids: userIds,
|
|
cc_pair_ids: ccPairIds,
|
|
});
|
|
|
|
const responseData = await this.handleResponse<{ id: number }>(
|
|
response,
|
|
"Failed to create user group"
|
|
);
|
|
|
|
this.log(`Created user group: ${groupName} (ID: ${responseData.id})`);
|
|
return responseData.id;
|
|
}
|
|
|
|
/**
|
|
* Polls until a user group has finished syncing (is_up_to_date === true).
|
|
* Newly created groups start syncing immediately; many mutation endpoints
|
|
* reject requests while the group is still syncing.
|
|
*/
|
|
async waitForGroupSync(
|
|
groupId: number,
|
|
timeout: number = 30000
|
|
): Promise<void> {
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const res = await this.get("/manage/admin/user-group");
|
|
const groups = await res.json();
|
|
const group = groups.find(
|
|
(g: { id: number; is_up_to_date: boolean }) => g.id === groupId
|
|
);
|
|
return group?.is_up_to_date ?? false;
|
|
},
|
|
{
|
|
message: `User group ${groupId} did not finish syncing`,
|
|
timeout,
|
|
}
|
|
)
|
|
.toBe(true);
|
|
this.log(`User group ${groupId} finished syncing`);
|
|
}
|
|
|
|
/**
|
|
* Deletes a user group.
|
|
*
|
|
* @param groupId - The user group ID to delete
|
|
*/
|
|
async deleteUserGroup(groupId: number): Promise<void> {
|
|
const response = await this.delete(`/manage/admin/user-group/${groupId}`);
|
|
|
|
await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete user group ${groupId}`
|
|
);
|
|
|
|
this.log(`Deleted user group: ${groupId}`);
|
|
}
|
|
|
|
/**
|
|
* Lists all user groups.
|
|
*/
|
|
async getUserGroups(): Promise<
|
|
Array<{ id: number; name: string; is_default: boolean }>
|
|
> {
|
|
const response = await this.get(
|
|
"/manage/admin/user-group?include_default=true"
|
|
);
|
|
return response.json();
|
|
}
|
|
|
|
async setUserRole(
|
|
email: string,
|
|
role: "admin" | "curator" | "global_curator" | "basic",
|
|
explicitOverride = false
|
|
): Promise<void> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/manage/set-user-role`,
|
|
{
|
|
data: {
|
|
user_email: email,
|
|
new_role: role,
|
|
explicit_override: explicitOverride,
|
|
},
|
|
}
|
|
);
|
|
await this.handleResponse(response, `Failed to set user role for ${email}`);
|
|
this.log(`Updated role for ${email} to ${role}`);
|
|
}
|
|
|
|
async deleteMcpServer(serverId: number): Promise<boolean> {
|
|
const response = await this.request.delete(
|
|
`${this.baseUrl}/admin/mcp/server/${serverId}`
|
|
);
|
|
const success = await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete MCP server ${serverId}`
|
|
);
|
|
if (success) {
|
|
this.log(`Deleted MCP server ${serverId}`);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
async deleteCustomTool(toolId: number): Promise<boolean> {
|
|
const response = await this.request.delete(
|
|
`${this.baseUrl}/admin/tool/custom/${toolId}`
|
|
);
|
|
const success = await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete custom tool ${toolId}`
|
|
);
|
|
if (success) {
|
|
this.log(`Deleted custom tool ${toolId}`);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
async listOpenApiTools(): Promise<
|
|
Array<{ id: number; name: string; description: string }>
|
|
> {
|
|
const response = await this.get("/tool/openapi");
|
|
return await this.handleResponse(response, "Failed to list OpenAPI tools");
|
|
}
|
|
|
|
async findToolByName(
|
|
name: string
|
|
): Promise<{ id: number; name: string; description: string } | null> {
|
|
const tools = await this.listOpenApiTools();
|
|
return tools.find((tool) => tool.name === name) ?? null;
|
|
}
|
|
|
|
async deleteAgent(agentId: number): Promise<boolean> {
|
|
const response = await this.request.delete(
|
|
`${this.baseUrl}/persona/${agentId}`
|
|
);
|
|
const success = await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete assistant ${agentId}`
|
|
);
|
|
if (success) {
|
|
this.log(`Deleted assistant ${agentId}`);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
async getAssistant(agentId: number): Promise<{
|
|
id: number;
|
|
is_public: boolean;
|
|
users: Array<{ id: string }>;
|
|
groups: number[];
|
|
tools: Array<{
|
|
id: number;
|
|
in_code_tool_id?: string | null;
|
|
mcp_server_id?: number | null;
|
|
}>;
|
|
}> {
|
|
const response = await this.get(`/persona/${agentId}`);
|
|
return await this.handleResponse(
|
|
response,
|
|
`Failed to fetch assistant ${agentId}`
|
|
);
|
|
}
|
|
|
|
async updateAgentSharing(
|
|
agentId: number,
|
|
options: {
|
|
userIds?: string[];
|
|
groupIds?: number[];
|
|
isPublic?: boolean;
|
|
labelIds?: number[];
|
|
}
|
|
): Promise<void> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/persona/${agentId}/share`,
|
|
{
|
|
data: {
|
|
user_ids: options.userIds,
|
|
group_ids: options.groupIds,
|
|
is_public: options.isPublic,
|
|
label_ids: options.labelIds,
|
|
},
|
|
}
|
|
);
|
|
await this.handleResponse(
|
|
response,
|
|
`Failed to update sharing for assistant ${agentId}`
|
|
);
|
|
this.log(
|
|
`Updated assistant sharing: ${agentId} (is_public=${String(
|
|
options.isPublic
|
|
)})`
|
|
);
|
|
}
|
|
|
|
async listMcpServers(): Promise<any[]> {
|
|
const response = await this.get(`/admin/mcp/servers`);
|
|
const data = await this.handleResponse<{ mcp_servers: any[] }>(
|
|
response,
|
|
"Failed to list MCP servers"
|
|
);
|
|
return data.mcp_servers;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MCP server provisioning (API-driven test setup)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Create (or update, via `existing_server_id`) an MCP server with its auth
|
|
* configuration in a single call. Covers admin shared-key, per-user template,
|
|
* and OAuth-stub servers. Mirrors the frontend `upsertMCPServer` payload.
|
|
* Returns the new server id.
|
|
*/
|
|
async createMcpServerWithAuth(
|
|
request: McpServerCreateRequest
|
|
): Promise<number> {
|
|
const response = await this.post("/admin/mcp/servers/create", {
|
|
transport: "STREAMABLE_HTTP",
|
|
...request,
|
|
});
|
|
const data = await this.handleResponse<{ server_id: number }>(
|
|
response,
|
|
`Failed to create MCP server ${request.name}`
|
|
);
|
|
this.log(`Created MCP server ${request.name} (ID: ${data.server_id})`);
|
|
return data.server_id;
|
|
}
|
|
|
|
/**
|
|
* Discover the tools exposed by a live MCP server and persist them to the DB.
|
|
* `source=mcp` triggers discovery, enables the tools, and flips the server
|
|
* status to CONNECTED. Returns the discovered tool snapshots.
|
|
*/
|
|
async discoverMcpTools(serverId: number): Promise<Array<{ id: number }>> {
|
|
const response = await this.get(
|
|
`/admin/mcp/server/${serverId}/tools/snapshots?source=mcp`
|
|
);
|
|
const tools = await this.handleResponse<Array<{ id: number }>>(
|
|
response,
|
|
`Failed to discover tools for MCP server ${serverId}`
|
|
);
|
|
this.log(`Discovered ${tools.length} tool(s) for MCP server ${serverId}`);
|
|
return tools;
|
|
}
|
|
|
|
/** List the DB tool rows for an MCP server (names have the server prefix stripped). */
|
|
async getMcpDbTools(serverId: number): Promise<McpDbTool[]> {
|
|
const response = await this.get(`/admin/mcp/server/${serverId}/db-tools`);
|
|
const data = await this.handleResponse<{ tools: McpDbTool[] }>(
|
|
response,
|
|
`Failed to list DB tools for MCP server ${serverId}`
|
|
);
|
|
return data.tools ?? [];
|
|
}
|
|
|
|
/**
|
|
* Resolve a tool's DB id by its (prefix-stripped) name, polling briefly in
|
|
* case discovery just completed.
|
|
*/
|
|
async findMcpToolId(
|
|
serverId: number,
|
|
toolName: string,
|
|
timeoutMs: number = 15_000
|
|
): Promise<number> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
let lastSeen: string[] = [];
|
|
for (;;) {
|
|
const tools = await this.getMcpDbTools(serverId);
|
|
lastSeen = tools.map((tool) => tool.name);
|
|
const match = tools.find((tool) => tool.name === toolName);
|
|
if (match) {
|
|
return match.id;
|
|
}
|
|
if (Date.now() >= deadline) {
|
|
throw new Error(
|
|
`Tool ${toolName} not found on server ${serverId}. Saw: ${lastSeen.join(
|
|
", "
|
|
)}`
|
|
);
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
}
|
|
}
|
|
|
|
/** Enable exactly the named tools on a server (all others are disabled). */
|
|
async syncMcpTools(
|
|
serverId: number,
|
|
selectedToolNames: string[]
|
|
): Promise<void> {
|
|
const response = await this.post("/admin/mcp/servers/update", {
|
|
server_id: serverId,
|
|
selected_tools: selectedToolNames,
|
|
});
|
|
await this.handleResponse(
|
|
response,
|
|
`Failed to sync tools for MCP server ${serverId}`
|
|
);
|
|
this.log(
|
|
`Synced ${selectedToolNames.length} tool(s) on MCP server ${serverId}`
|
|
);
|
|
}
|
|
|
|
/** Save the calling user's per-user credentials for a template-based server. */
|
|
async saveUserMcpCredentials(
|
|
serverId: number,
|
|
credentials: Record<string, string>,
|
|
transport: "STREAMABLE_HTTP" | "SSE" = "STREAMABLE_HTTP"
|
|
): Promise<void> {
|
|
const response = await this.post("/mcp/user-credentials", {
|
|
server_id: serverId,
|
|
credentials,
|
|
transport,
|
|
});
|
|
await this.handleResponse(
|
|
response,
|
|
`Failed to save user MCP credentials for server ${serverId}`
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Agent / default-assistant provisioning
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Create an agent (persona) with the given tools attached. Returns its id. */
|
|
async createAgentWithMcpTools(
|
|
name: string,
|
|
toolIds: number[],
|
|
options: CreateAgentOptions = {}
|
|
): Promise<number> {
|
|
const response = await this.post("/persona", {
|
|
name,
|
|
description: options.description ?? `${name} (e2e)`,
|
|
system_prompt: options.instructions ?? "",
|
|
task_prompt: "",
|
|
datetime_aware: true,
|
|
document_set_ids: [],
|
|
is_public: options.isPublic ?? false,
|
|
users: options.userIds ?? [],
|
|
groups: options.groupIds ?? [],
|
|
tool_ids: toolIds,
|
|
});
|
|
const data = await this.handleResponse<{ id: number }>(
|
|
response,
|
|
`Failed to create agent ${name}`
|
|
);
|
|
this.log(`Created agent ${name} (ID: ${data.id})`);
|
|
return data.id;
|
|
}
|
|
|
|
/** Read the default assistant's currently enabled tool ids + system prompt. */
|
|
async getDefaultAssistantConfig(): Promise<{
|
|
tool_ids: number[];
|
|
system_prompt: string | null;
|
|
default_system_prompt: string;
|
|
}> {
|
|
const response = await this.get("/admin/default-assistant/configuration");
|
|
return await this.handleResponse(
|
|
response,
|
|
"Failed to fetch default assistant configuration"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add the given tool ids to the default assistant (preserving existing ones).
|
|
* The PATCH endpoint expects the full replacement list, so we read-merge-write.
|
|
*/
|
|
async addToolsToDefaultAssistant(toolIds: number[]): Promise<void> {
|
|
const { tool_ids: current } = await this.getDefaultAssistantConfig();
|
|
const merged = Array.from(new Set([...current, ...toolIds]));
|
|
const response = await this.patch("/admin/default-assistant", {
|
|
tool_ids: merged,
|
|
});
|
|
await this.handleResponse(
|
|
response,
|
|
"Failed to add tools to default assistant"
|
|
);
|
|
this.log(`Added ${toolIds.length} tool(s) to the default assistant`);
|
|
}
|
|
|
|
async listAgents(options?: {
|
|
includeDeleted?: boolean;
|
|
getEditable?: boolean;
|
|
}): Promise<any[]> {
|
|
const params = new URLSearchParams();
|
|
if (options?.includeDeleted) {
|
|
params.set("include_deleted", "true");
|
|
}
|
|
if (options?.getEditable ?? true) {
|
|
params.set("get_editable", "true");
|
|
}
|
|
const query = params.toString();
|
|
const response = await this.get(
|
|
`/admin/persona${query ? `?${query}` : ""}`
|
|
);
|
|
return await this.handleResponse<any[]>(
|
|
response,
|
|
"Failed to list assistants"
|
|
);
|
|
}
|
|
|
|
async findAgentByName(
|
|
name: string,
|
|
options?: { includeDeleted?: boolean; getEditable?: boolean }
|
|
): Promise<any | null> {
|
|
const assistants = await this.listAgents(options);
|
|
return assistants.find((assistant) => assistant.name === name) ?? null;
|
|
}
|
|
|
|
async registerUser(email: string, password: string): Promise<{ id: string }> {
|
|
const response = await this.request.post(`${this.baseUrl}/auth/register`, {
|
|
data: {
|
|
email,
|
|
username: email,
|
|
password,
|
|
},
|
|
});
|
|
const data = await this.handleResponse<{ id: string }>(
|
|
response,
|
|
`Failed to register user ${email}`
|
|
);
|
|
return data;
|
|
}
|
|
|
|
async getUserByEmail(email: string): Promise<{
|
|
id: string;
|
|
email: string;
|
|
role: string;
|
|
} | null> {
|
|
const response = await this.request.get(
|
|
`${this.baseUrl}/manage/users/accepted`,
|
|
{
|
|
params: {
|
|
q: email,
|
|
page_size: 1,
|
|
},
|
|
}
|
|
);
|
|
const data = await this.handleResponse<{ items: any[] }>(
|
|
response,
|
|
`Failed to fetch user ${email}`
|
|
);
|
|
const [user] = data.items;
|
|
return user
|
|
? {
|
|
id: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
}
|
|
: null;
|
|
}
|
|
|
|
async setCuratorStatus(
|
|
userGroupId: string,
|
|
userId: string,
|
|
isCurator: boolean = true
|
|
): Promise<void> {
|
|
const response = await this.request.post(
|
|
`${this.baseUrl}/manage/admin/user-group/${userGroupId}/set-curator`,
|
|
{
|
|
data: {
|
|
user_id: userId,
|
|
is_curator: isCurator,
|
|
},
|
|
}
|
|
);
|
|
await this.handleResponse(
|
|
response,
|
|
`Failed to update curator status for ${userId}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create and activate a web search provider for testing.
|
|
* Uses env-backed keys when available and falls back to a placeholder key.
|
|
*
|
|
* @param providerType - Type of provider: "exa", "brave", "serper", "google_pse", "searxng"
|
|
* @param name - Optional name for the provider (defaults to "Test Provider")
|
|
* @returns The created provider ID
|
|
*/
|
|
async createWebSearchProvider(
|
|
providerType: "exa" | "brave" | "serper" | "google_pse" | "searxng" = "exa",
|
|
name: string = "Test Provider"
|
|
): Promise<number> {
|
|
const config: Record<string, string> = {};
|
|
if (providerType === "google_pse") {
|
|
config.search_engine_id = "test-engine-id";
|
|
}
|
|
if (providerType === "searxng") {
|
|
config.searxng_base_url = "https://test-searxng.example.com";
|
|
}
|
|
|
|
const response = await this.post("/admin/web-search/search-providers", {
|
|
name,
|
|
provider_type: providerType,
|
|
api_key: E2E_WEB_SEARCH_API_KEY,
|
|
api_key_changed: true,
|
|
config: Object.keys(config).length > 0 ? config : undefined,
|
|
activate: true,
|
|
});
|
|
|
|
const data = await this.handleResponse<{ id: number }>(
|
|
response,
|
|
`Failed to create web search provider ${providerType}`
|
|
);
|
|
return data.id;
|
|
}
|
|
|
|
/**
|
|
* Delete a web search provider.
|
|
*
|
|
* @param providerId - ID of the provider to delete
|
|
*/
|
|
async deleteWebSearchProvider(providerId: number): Promise<void> {
|
|
const response = await this.delete(
|
|
`/admin/web-search/search-providers/${providerId}`
|
|
);
|
|
if (!response.ok()) {
|
|
const errorText = await response.text();
|
|
console.warn(
|
|
`Failed to delete web search provider ${providerId}: ${response.status()} - ${errorText}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an image generation configuration for testing.
|
|
* This enables the image generation tool in assistants.
|
|
*
|
|
* API: POST /api/admin/image-generation/config
|
|
* Schema (ImageGenerationConfigCreate):
|
|
* - image_provider_id: string (required) - unique key
|
|
* - model_name: string (required) - e.g., "gpt-image-1"
|
|
* - provider: string - e.g., "openai"
|
|
* - api_key: string
|
|
* - is_default: boolean
|
|
*
|
|
* @param imageProviderId - Unique identifier for the image generation config
|
|
* @param modelName - Model name (defaults to "gpt-image-1")
|
|
* @param provider - Provider name (defaults to "openai")
|
|
* @param isDefault - Whether this should be the default config (defaults to true)
|
|
* @returns The image_provider_id
|
|
*/
|
|
async createImageGenerationConfig(
|
|
imageProviderId: string,
|
|
modelName: string = "gpt-image-1",
|
|
provider: string = "openai",
|
|
isDefault: boolean = true
|
|
): Promise<string> {
|
|
const response = await this.post("/admin/image-generation/config", {
|
|
image_provider_id: imageProviderId,
|
|
model_name: modelName,
|
|
provider: provider,
|
|
api_key: E2E_IMAGE_GEN_API_KEY,
|
|
is_default: isDefault,
|
|
});
|
|
|
|
await this.handleResponse(
|
|
response,
|
|
"Failed to create image generation config"
|
|
);
|
|
|
|
this.log(`Created image generation config: ${imageProviderId}`);
|
|
return imageProviderId;
|
|
}
|
|
|
|
/**
|
|
* Deletes an image generation configuration.
|
|
*
|
|
* @param imageProviderId - The image_provider_id to delete
|
|
*/
|
|
async deleteImageGenerationConfig(imageProviderId: string): Promise<void> {
|
|
const response = await this.delete(
|
|
`/admin/image-generation/config/${imageProviderId}`
|
|
);
|
|
|
|
await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete image generation config ${imageProviderId}`
|
|
);
|
|
|
|
this.log(`Deleted image generation config: ${imageProviderId}`);
|
|
}
|
|
|
|
// === Discord Bot Methods ===
|
|
|
|
/**
|
|
* Creates a Discord guild configuration.
|
|
* Returns the guild config with registration key (shown once).
|
|
*
|
|
* @returns The created guild config with id and registration_key
|
|
*/
|
|
async createDiscordGuild(): Promise<{
|
|
id: number;
|
|
registration_key: string;
|
|
guild_name: string | null;
|
|
}> {
|
|
const response = await this.post("/manage/admin/discord-bot/guilds");
|
|
|
|
const guild = await this.handleResponse<{
|
|
id: number;
|
|
registration_key: string;
|
|
guild_name: string | null;
|
|
}>(response, "Failed to create Discord guild config");
|
|
|
|
this.log(
|
|
`Created Discord guild config: id=${guild.id}, registration_key=${guild.registration_key}`
|
|
);
|
|
return guild;
|
|
}
|
|
|
|
/**
|
|
* Lists all Discord guild configurations.
|
|
*
|
|
* @returns Array of guild configs
|
|
*/
|
|
async listDiscordGuilds(): Promise<
|
|
Array<{
|
|
id: number;
|
|
guild_id: string | null;
|
|
guild_name: string | null;
|
|
enabled: boolean;
|
|
}>
|
|
> {
|
|
const response = await this.get("/manage/admin/discord-bot/guilds");
|
|
return await this.handleResponse(response, "Failed to list Discord guilds");
|
|
}
|
|
|
|
/**
|
|
* Gets a specific Discord guild configuration.
|
|
*
|
|
* @param guildId - The internal guild config ID
|
|
* @returns The guild config or null if not found
|
|
*/
|
|
async getDiscordGuild(guildId: number): Promise<{
|
|
id: number;
|
|
guild_id: string | null;
|
|
guild_name: string | null;
|
|
enabled: boolean;
|
|
default_persona_id: number | null;
|
|
} | null> {
|
|
const response = await this.get(
|
|
`/manage/admin/discord-bot/guilds/${guildId}`
|
|
);
|
|
if (response.status() === 404) {
|
|
return null;
|
|
}
|
|
return await this.handleResponse(
|
|
response,
|
|
`Failed to get Discord guild ${guildId}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates a Discord guild configuration.
|
|
*
|
|
* @param guildId - The internal guild config ID
|
|
* @param updates - The fields to update
|
|
* @returns The updated guild config
|
|
*/
|
|
async updateDiscordGuild(
|
|
guildId: number,
|
|
updates: { enabled?: boolean; default_persona_id?: number | null }
|
|
): Promise<{
|
|
id: number;
|
|
guild_id: string | null;
|
|
guild_name: string | null;
|
|
enabled: boolean;
|
|
}> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/manage/admin/discord-bot/guilds/${guildId}`,
|
|
{ data: updates }
|
|
);
|
|
return await this.handleResponse(
|
|
response,
|
|
`Failed to update Discord guild ${guildId}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Deletes a Discord guild configuration.
|
|
*
|
|
* @param guildId - The internal guild config ID
|
|
*/
|
|
async deleteDiscordGuild(guildId: number): Promise<void> {
|
|
const response = await this.delete(
|
|
`/manage/admin/discord-bot/guilds/${guildId}`
|
|
);
|
|
|
|
await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete Discord guild ${guildId}`
|
|
);
|
|
|
|
this.log(`Deleted Discord guild config: ${guildId}`);
|
|
}
|
|
|
|
/**
|
|
* Lists channels for a Discord guild configuration.
|
|
*
|
|
* @param guildConfigId - The internal guild config ID
|
|
* @returns Array of channel configs
|
|
*/
|
|
async listDiscordChannels(guildConfigId: number): Promise<
|
|
Array<{
|
|
id: number;
|
|
channel_id: string;
|
|
channel_name: string;
|
|
channel_type: string;
|
|
enabled: boolean;
|
|
}>
|
|
> {
|
|
const response = await this.get(
|
|
`/manage/admin/discord-bot/guilds/${guildConfigId}/channels`
|
|
);
|
|
return await this.handleResponse(
|
|
response,
|
|
`Failed to list channels for guild ${guildConfigId}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates a Discord channel configuration.
|
|
*
|
|
* @param guildConfigId - The internal guild config ID
|
|
* @param channelConfigId - The internal channel config ID
|
|
* @param updates - The fields to update
|
|
* @returns The updated channel config
|
|
*/
|
|
async updateDiscordChannel(
|
|
guildConfigId: number,
|
|
channelConfigId: number,
|
|
updates: {
|
|
enabled?: boolean;
|
|
thread_only_mode?: boolean;
|
|
require_bot_invocation?: boolean;
|
|
persona_override_id?: number | null;
|
|
}
|
|
): Promise<{
|
|
id: number;
|
|
channel_id: string;
|
|
channel_name: string;
|
|
enabled: boolean;
|
|
}> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/manage/admin/discord-bot/guilds/${guildConfigId}/channels/${channelConfigId}`,
|
|
{ data: updates }
|
|
);
|
|
return await this.handleResponse(
|
|
response,
|
|
`Failed to update channel ${channelConfigId}`
|
|
);
|
|
}
|
|
|
|
// === User Management Methods ===
|
|
|
|
async deactivateUser(email: string): Promise<void> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/manage/admin/deactivate-user`,
|
|
{ data: { user_email: email } }
|
|
);
|
|
await this.handleResponse(response, `Failed to deactivate user ${email}`);
|
|
this.log(`Deactivated user: ${email}`);
|
|
}
|
|
|
|
async activateUser(email: string): Promise<void> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/manage/admin/activate-user`,
|
|
{ data: { user_email: email } }
|
|
);
|
|
await this.handleResponse(response, `Failed to activate user ${email}`);
|
|
this.log(`Activated user: ${email}`);
|
|
}
|
|
|
|
async deleteUser(email: string): Promise<void> {
|
|
const response = await this.request.delete(
|
|
`${this.baseUrl}/manage/admin/delete-user`,
|
|
{ data: { user_email: email } }
|
|
);
|
|
await this.handleResponse(response, `Failed to delete user ${email}`);
|
|
this.log(`Deleted user: ${email}`);
|
|
}
|
|
|
|
async cancelInvite(email: string): Promise<void> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/manage/admin/remove-invited-user`,
|
|
{ data: { user_email: email } }
|
|
);
|
|
await this.handleResponse(response, `Failed to cancel invite for ${email}`);
|
|
this.log(`Cancelled invite for: ${email}`);
|
|
}
|
|
|
|
async inviteUsers(emails: string[]): Promise<void> {
|
|
const response = await this.put("/manage/admin/users", { emails });
|
|
await this.handleResponse(response, `Failed to invite users`);
|
|
this.log(`Invited users: ${emails.join(", ")}`);
|
|
}
|
|
|
|
async setPersonalName(name: string): Promise<void> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/user/personalization`,
|
|
{ data: { name } }
|
|
);
|
|
await this.handleResponse(
|
|
response,
|
|
`Failed to set personal name to ${name}`
|
|
);
|
|
this.log(`Set personal name: ${name}`);
|
|
}
|
|
|
|
// === Chat Session Methods ===
|
|
|
|
/**
|
|
* Creates a chat session with a specific description.
|
|
*
|
|
* @param description - The description/title for the chat session
|
|
* @param personaId - The persona/assistant ID to use (defaults to 0)
|
|
* @returns The chat session ID
|
|
* @throws Error if the chat session creation fails
|
|
*/
|
|
async createChatSession(
|
|
description: string,
|
|
personaId: number = 0
|
|
): Promise<string> {
|
|
const response = await this.post("/chat/create-chat-session", {
|
|
persona_id: personaId,
|
|
description,
|
|
});
|
|
const data = await this.handleResponse<{ chat_session_id: string }>(
|
|
response,
|
|
"Failed to create chat session"
|
|
);
|
|
this.log(
|
|
`Created chat session: ${description} (ID: ${data.chat_session_id})`
|
|
);
|
|
return data.chat_session_id;
|
|
}
|
|
|
|
/**
|
|
* Deletes a chat session.
|
|
*
|
|
* @param chatId - The chat session ID to delete
|
|
*/
|
|
async deleteChatSession(chatId: string): Promise<void> {
|
|
const response = await this.delete(`/chat/delete-chat-session/${chatId}`);
|
|
await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete chat session ${chatId}`
|
|
);
|
|
this.log(`Deleted chat session: ${chatId}`);
|
|
}
|
|
|
|
// === Project Methods ===
|
|
|
|
/**
|
|
* Creates a project with a specific name.
|
|
*
|
|
* @param name - The name for the project
|
|
* @returns The project ID
|
|
* @throws Error if the project creation fails
|
|
*/
|
|
async createProject(name: string): Promise<number> {
|
|
const response = await this.post(
|
|
`/user/projects/create?name=${encodeURIComponent(name)}`
|
|
);
|
|
const data = await this.handleResponse<{ id: number }>(
|
|
response,
|
|
"Failed to create project"
|
|
);
|
|
this.log(`Created project: ${name} (ID: ${data.id})`);
|
|
return data.id;
|
|
}
|
|
|
|
/**
|
|
* Deletes a project.
|
|
*
|
|
* @param projectId - The project ID to delete
|
|
*/
|
|
async deleteProject(projectId: number): Promise<void> {
|
|
const response = await this.delete(`/user/projects/${projectId}`);
|
|
await this.handleResponseSoft(
|
|
response,
|
|
`Failed to delete project ${projectId}`
|
|
);
|
|
this.log(`Deleted project: ${projectId}`);
|
|
}
|
|
|
|
/**
|
|
* Sets the current user's default app mode preference.
|
|
*
|
|
* @param mode - The default mode to persist ("CHAT" or "SEARCH")
|
|
*/
|
|
async setDefaultAppMode(mode: "CHAT" | "SEARCH"): Promise<void> {
|
|
const response = await this.request.patch(
|
|
`${this.baseUrl}/user/default-app-mode`,
|
|
{
|
|
data: { default_app_mode: mode },
|
|
}
|
|
);
|
|
await this.handleResponse(
|
|
response,
|
|
`Failed to set default app mode to ${mode}`
|
|
);
|
|
this.log(`Set default app mode: ${mode}`);
|
|
}
|
|
|
|
/**
|
|
* Enables or disables the paste-as-tile user preference.
|
|
*
|
|
* @param enabled - Whether paste-as-tile should be enabled
|
|
*/
|
|
async setPasteTileSetting(enabled: boolean): Promise<void> {
|
|
const response = await this.patch(
|
|
`/paste-as-tile?paste_as_tile=${enabled}`
|
|
);
|
|
await this.handleResponse(
|
|
response,
|
|
`Failed to set paste_as_tile to ${enabled}`
|
|
);
|
|
this.log(`Set paste_as_tile: ${enabled}`);
|
|
}
|
|
}
|