mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix: restore ci after dedupe refactors
This commit is contained in:
56
extensions/acpx/src/acpx-runtime-compat.d.ts
vendored
Normal file
56
extensions/acpx/src/acpx-runtime-compat.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
declare module "acpx/dist/runtime.js" {
|
||||
export const ACPX_BACKEND_ID: string;
|
||||
|
||||
export type AcpRuntimeDoctorReport = import("../runtime-api.js").AcpRuntimeDoctorReport;
|
||||
export type AcpRuntimeEnsureInput = import("../runtime-api.js").AcpRuntimeEnsureInput;
|
||||
export type AcpRuntimeEvent = import("../runtime-api.js").AcpRuntimeEvent;
|
||||
export type AcpRuntimeHandle = import("../runtime-api.js").AcpRuntimeHandle;
|
||||
export type AcpRuntimeCapabilities = import("../runtime-api.js").AcpRuntimeCapabilities;
|
||||
export type AcpRuntimeStatus = import("../runtime-api.js").AcpRuntimeStatus;
|
||||
export type AcpRuntimeTurnInput = import("../runtime-api.js").AcpRuntimeTurnInput;
|
||||
|
||||
export type AcpAgentRegistry = {
|
||||
resolve(agent: string): string | undefined;
|
||||
list(): string[];
|
||||
};
|
||||
|
||||
export type AcpSessionRecord = Record<string, unknown>;
|
||||
|
||||
export type AcpSessionStore = {
|
||||
load(sessionId: string): Promise<AcpSessionRecord | undefined>;
|
||||
save(record: AcpSessionRecord): Promise<void>;
|
||||
};
|
||||
|
||||
export type AcpRuntimeOptions = {
|
||||
cwd: string;
|
||||
sessionStore: AcpSessionStore;
|
||||
agentRegistry: AcpAgentRegistry;
|
||||
mcpServers?: unknown;
|
||||
permissionMode?: unknown;
|
||||
nonInteractivePermissions?: unknown;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export class AcpxRuntime {
|
||||
constructor(options: AcpRuntimeOptions, testOptions?: unknown);
|
||||
isHealthy(): boolean;
|
||||
probeAvailability(): Promise<void>;
|
||||
doctor(): Promise<AcpRuntimeDoctorReport>;
|
||||
ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
|
||||
runTurn(input: AcpRuntimeTurnInput): AsyncIterable<AcpRuntimeEvent>;
|
||||
getCapabilities(input?: {
|
||||
handle?: AcpRuntimeHandle;
|
||||
}): AcpRuntimeCapabilities | Promise<AcpRuntimeCapabilities>;
|
||||
getStatus(input: { handle: AcpRuntimeHandle; signal?: AbortSignal }): Promise<AcpRuntimeStatus>;
|
||||
setMode(input: { handle: AcpRuntimeHandle; mode: string }): Promise<void>;
|
||||
setConfigOption(input: { handle: AcpRuntimeHandle; key: string; value: string }): Promise<void>;
|
||||
cancel(input: { handle: AcpRuntimeHandle; reason?: string }): Promise<void>;
|
||||
close(input: { handle: AcpRuntimeHandle; reason?: string }): Promise<void>;
|
||||
}
|
||||
|
||||
export function createAcpRuntime(...args: unknown[]): AcpxRuntime;
|
||||
export function createAgentRegistry(params: { overrides?: unknown }): AcpAgentRegistry;
|
||||
export function createFileSessionStore(params: { stateDir: string }): AcpSessionStore;
|
||||
export function decodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
|
||||
export function encodeAcpxRuntimeHandleState(...args: unknown[]): unknown;
|
||||
}
|
||||
@@ -301,8 +301,7 @@ export function createDiffsTool(params: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Diff viewer ready.\n${viewerUrl}\n` + `File rendering failed: ${errorMessage}`,
|
||||
text: `Diff viewer ready.\n${viewerUrl}\nFile rendering failed: ${errorMessage}`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { ChannelDoctorLegacyConfigRule } from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { ELEVENLABS_TALK_PROVIDER_ID, migrateElevenLabsLegacyTalkConfig } from "./config-compat.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { ELEVENLABS_TALK_PROVIDER_ID, migrateElevenLabsLegacyTalkConfig } from "./config-compat.js";
|
||||
|
||||
export function hasLegacyTalkFields(value: unknown): boolean {
|
||||
const talk = isRecord(value) ? value : null;
|
||||
|
||||
@@ -3,7 +3,6 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
formatErrorMessage,
|
||||
formatThreadBindingDurationLabel,
|
||||
registerSessionBindingAdapter,
|
||||
resolveThreadBindingConversationIdFromBindingId,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
type SessionBindingAdapter,
|
||||
type SessionBindingRecord,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getRuntimeConfigSnapshot, type OpenClawConfig } from "@openclaw/plugin-sdk/config-runtime";
|
||||
import { jsonResult, readStringParam } from "@openclaw/plugin-sdk/provider-web-search";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
buildXaiCodeExecutionPayload,
|
||||
requestXaiCodeExecution,
|
||||
@@ -9,14 +9,6 @@ import {
|
||||
} from "./src/code-execution-shared.js";
|
||||
import { isXaiToolEnabled, resolveXaiToolApiKey } from "./src/tool-auth-shared.js";
|
||||
|
||||
type _XaiPluginConfig = NonNullable<
|
||||
NonNullable<OpenClawConfig["plugins"]>["entries"]
|
||||
>["xai"] extends {
|
||||
config?: infer Config;
|
||||
}
|
||||
? Config
|
||||
: undefined;
|
||||
|
||||
type CodeExecutionConfig = {
|
||||
enabled?: boolean;
|
||||
model?: string;
|
||||
@@ -30,12 +22,19 @@ function readCodeExecutionConfigRecord(
|
||||
return config && typeof config === "object" ? (config as Record<string, unknown>) : undefined;
|
||||
}
|
||||
|
||||
function readPluginCodeExecutionConfig(cfg?: OpenClawConfig): CodeExecutionConfig | undefined {
|
||||
const entries = cfg?.plugins?.entries;
|
||||
if (!entries || typeof entries !== "object") {
|
||||
function readPluginCodeExecutionConfig(cfg?: unknown): CodeExecutionConfig | undefined {
|
||||
if (!cfg || typeof cfg !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const xaiEntry = (entries as Record<string, unknown>).xai;
|
||||
const entries = (cfg as Record<string, unknown>).plugins;
|
||||
const pluginEntries =
|
||||
entries && typeof entries === "object"
|
||||
? ((entries as Record<string, unknown>).entries as Record<string, unknown> | undefined)
|
||||
: undefined;
|
||||
if (!pluginEntries) {
|
||||
return undefined;
|
||||
}
|
||||
const xaiEntry = pluginEntries.xai;
|
||||
if (!xaiEntry || typeof xaiEntry !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
@@ -51,20 +50,20 @@ function readPluginCodeExecutionConfig(cfg?: OpenClawConfig): CodeExecutionConfi
|
||||
}
|
||||
|
||||
function resolveCodeExecutionEnabled(params: {
|
||||
sourceConfig?: OpenClawConfig;
|
||||
runtimeConfig?: OpenClawConfig;
|
||||
sourceConfig?: unknown;
|
||||
runtimeConfig?: unknown;
|
||||
config?: CodeExecutionConfig;
|
||||
}): boolean {
|
||||
return isXaiToolEnabled({
|
||||
enabled: readCodeExecutionConfigRecord(params.config)?.enabled as boolean | undefined,
|
||||
runtimeConfig: params.runtimeConfig,
|
||||
sourceConfig: params.sourceConfig,
|
||||
runtimeConfig: params.runtimeConfig as never,
|
||||
sourceConfig: params.sourceConfig as never,
|
||||
});
|
||||
}
|
||||
|
||||
export function createCodeExecutionTool(options?: {
|
||||
config?: OpenClawConfig;
|
||||
runtimeConfig?: OpenClawConfig | null;
|
||||
config?: unknown;
|
||||
runtimeConfig?: Record<string, unknown> | null;
|
||||
}) {
|
||||
const runtimeConfig = options?.runtimeConfig ?? getRuntimeConfigSnapshot();
|
||||
const codeExecutionConfig =
|
||||
@@ -93,8 +92,8 @@ export function createCodeExecutionTool(options?: {
|
||||
}),
|
||||
execute: async (_toolCallId: string, args: Record<string, unknown>) => {
|
||||
const apiKey = resolveXaiToolApiKey({
|
||||
runtimeConfig: runtimeConfig ?? undefined,
|
||||
sourceConfig: options?.config,
|
||||
runtimeConfig: (runtimeConfig ?? undefined) as never,
|
||||
sourceConfig: options?.config as never,
|
||||
});
|
||||
if (!apiKey) {
|
||||
return jsonResult({
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { OpenClawConfig } from "@openclaw/plugin-sdk/config-runtime";
|
||||
import { defineSingleProviderPluginEntry } from "@openclaw/plugin-sdk/provider-entry";
|
||||
import { buildProviderReplayFamilyHooks } from "@openclaw/plugin-sdk/provider-model-shared";
|
||||
import { jsonResult, readProviderEnvValue } from "@openclaw/plugin-sdk/provider-web-search";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
|
||||
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { jsonResult, readProviderEnvValue } from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
applyXaiModelCompat,
|
||||
normalizeXaiModelId,
|
||||
@@ -30,8 +29,7 @@ const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
|
||||
|
||||
function hasResolvableXaiApiKey(config: unknown): boolean {
|
||||
return Boolean(
|
||||
resolveFallbackXaiAuth(config as OpenClawConfig | undefined)?.apiKey ||
|
||||
readProviderEnvValue(["XAI_API_KEY"]),
|
||||
resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ModelDefinitionConfig } from "@openclaw/plugin-sdk/provider-model-shared";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
export const XAI_BASE_URL = "https://api.x.ai/v1";
|
||||
export const XAI_DEFAULT_MODEL_ID = "grok-4";
|
||||
@@ -200,7 +200,7 @@ export function buildXaiCatalogModels(): ModelDefinitionConfig[] {
|
||||
return XAI_MODEL_CATALOG.map((entry) => toModelDefinition(entry));
|
||||
}
|
||||
|
||||
export function resolveXaiCatalogEntry(modelId: string): ModelDefinitionConfig | undefined {
|
||||
export function resolveXaiCatalogEntry(modelId: string) {
|
||||
const lower = modelId.trim().toLowerCase();
|
||||
const exact = XAI_MODEL_CATALOG.find((entry) => entry.id.toLowerCase() === lower);
|
||||
if (exact) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
} from "@openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeModelCompat } from "@openclaw/plugin-sdk/provider-model-shared";
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { applyXaiModelCompat } from "./api.js";
|
||||
import { resolveXaiCatalogEntry, XAI_BASE_URL } from "./model-definitions.js";
|
||||
|
||||
@@ -19,7 +19,7 @@ export function isModernXaiModel(modelId: string): boolean {
|
||||
export function resolveXaiForwardCompatModel(params: {
|
||||
providerId: string;
|
||||
ctx: ProviderResolveDynamicModelContext;
|
||||
}): ProviderRuntimeModel | undefined {
|
||||
}) {
|
||||
const definition = resolveXaiCatalogEntry(params.ctx.modelId);
|
||||
if (!definition) {
|
||||
return undefined;
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { isProviderApiKeyConfigured } from "@openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "@openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
fetchWithTimeout,
|
||||
postJsonRequest,
|
||||
resolveProviderHttpRequestConfig,
|
||||
} from "@openclaw/plugin-sdk/provider-http";
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import type {
|
||||
GeneratedVideoAsset,
|
||||
VideoGenerationProvider,
|
||||
VideoGenerationRequest,
|
||||
VideoGenerationSourceAsset,
|
||||
} from "@openclaw/plugin-sdk/video-generation";
|
||||
} from "openclaw/plugin-sdk/video-generation";
|
||||
|
||||
const DEFAULT_XAI_VIDEO_BASE_URL = "https://api.x.ai/v1";
|
||||
const DEFAULT_XAI_VIDEO_MODEL = "grok-imagine-video";
|
||||
@@ -40,6 +39,12 @@ type XaiVideoStatusResponse = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
type VideoGenerationSourceInput = {
|
||||
url?: string;
|
||||
buffer?: Buffer;
|
||||
mimeType?: string;
|
||||
};
|
||||
|
||||
function resolveXaiVideoBaseUrl(req: VideoGenerationRequest): string {
|
||||
return req.cfg?.models?.providers?.xai?.baseUrl?.trim() || DEFAULT_XAI_VIDEO_BASE_URL;
|
||||
}
|
||||
@@ -48,7 +53,7 @@ function toDataUrl(buffer: Buffer, mimeType: string): string {
|
||||
return `data:${mimeType};base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
|
||||
function resolveImageUrl(input: VideoGenerationSourceAsset | undefined): string | undefined {
|
||||
function resolveImageUrl(input: VideoGenerationSourceInput | undefined): string | undefined {
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -61,7 +66,7 @@ function resolveImageUrl(input: VideoGenerationSourceAsset | undefined): string
|
||||
return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png");
|
||||
}
|
||||
|
||||
function resolveInputVideoUrl(input: VideoGenerationSourceAsset | undefined): string | undefined {
|
||||
function resolveInputVideoUrl(input: VideoGenerationSourceInput | undefined): string | undefined {
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
DEFAULT_CACHE_TTL_MINUTES,
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
@@ -14,12 +15,10 @@ import {
|
||||
resolveWebSearchProviderCredential,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
setScopedCredentialValue,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderSetupContext,
|
||||
type WebSearchProviderPlugin,
|
||||
writeCache,
|
||||
} from "@openclaw/plugin-sdk/provider-web-search";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
buildXaiWebSearchPayload,
|
||||
extractXaiWebSearchContent,
|
||||
@@ -168,15 +167,15 @@ function runXaiWebSearch(params: {
|
||||
function resolveXaiToolSearchConfig(ctx: {
|
||||
config?: Record<string, unknown>;
|
||||
searchConfig?: Record<string, unknown>;
|
||||
}): SearchConfigRecord | undefined {
|
||||
}) {
|
||||
return mergeScopedSearchConfig(
|
||||
ctx.searchConfig as SearchConfigRecord | undefined,
|
||||
ctx.searchConfig,
|
||||
"grok",
|
||||
resolveProviderWebSearchPluginConfig(ctx.config, "xai"),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveXaiWebSearchCredential(searchConfig?: SearchConfigRecord): string | undefined {
|
||||
function resolveXaiWebSearchCredential(searchConfig?: Record<string, unknown>): string | undefined {
|
||||
return resolveWebSearchProviderCredential({
|
||||
credentialValue: getScopedCredentialValue(searchConfig, "grok"),
|
||||
path: "tools.web.search.grok.apiKey",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getRuntimeConfigSnapshot, type OpenClawConfig } from "@openclaw/plugin-sdk/config-runtime";
|
||||
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
jsonResult,
|
||||
readCache,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
resolveCacheTtlMs,
|
||||
resolveTimeoutSeconds,
|
||||
writeCache,
|
||||
} from "@openclaw/plugin-sdk/provider-web-search";
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { isXaiToolEnabled, resolveXaiToolApiKey } from "./src/tool-auth-shared.js";
|
||||
import { resolveEffectiveXSearchConfig } from "./src/x-search-config.js";
|
||||
import {
|
||||
@@ -51,27 +51,27 @@ function getSharedXSearchCache(): Map<string, XSearchCacheEntry> {
|
||||
|
||||
const X_SEARCH_CACHE = getSharedXSearchCache();
|
||||
|
||||
function resolveXSearchConfig(cfg?: OpenClawConfig): Record<string, unknown> | undefined {
|
||||
return resolveEffectiveXSearchConfig(cfg);
|
||||
function resolveXSearchConfig(cfg?: unknown): Record<string, unknown> | undefined {
|
||||
return resolveEffectiveXSearchConfig(cfg as never);
|
||||
}
|
||||
|
||||
function resolveXSearchEnabled(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
cfg?: unknown;
|
||||
config?: Record<string, unknown>;
|
||||
runtimeConfig?: OpenClawConfig;
|
||||
runtimeConfig?: unknown;
|
||||
}): boolean {
|
||||
return isXaiToolEnabled({
|
||||
enabled: params.config?.enabled as boolean | undefined,
|
||||
runtimeConfig: params.runtimeConfig,
|
||||
sourceConfig: params.cfg,
|
||||
runtimeConfig: params.runtimeConfig as never,
|
||||
sourceConfig: params.cfg as never,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveXSearchApiKey(params: {
|
||||
sourceConfig?: OpenClawConfig;
|
||||
runtimeConfig?: OpenClawConfig;
|
||||
sourceConfig?: unknown;
|
||||
runtimeConfig?: unknown;
|
||||
}): string | undefined {
|
||||
return resolveXaiToolApiKey(params);
|
||||
return resolveXaiToolApiKey(params as never);
|
||||
}
|
||||
|
||||
function normalizeOptionalIsoDate(value: string | undefined, label: string): string | undefined {
|
||||
@@ -120,8 +120,8 @@ function buildXSearchCacheKey(params: {
|
||||
}
|
||||
|
||||
export function createXSearchTool(options?: {
|
||||
config?: OpenClawConfig;
|
||||
runtimeConfig?: OpenClawConfig | null;
|
||||
config?: unknown;
|
||||
runtimeConfig?: Record<string, unknown> | null;
|
||||
}) {
|
||||
const xSearchConfig = resolveXSearchConfig(options?.config);
|
||||
const runtimeConfig = options?.runtimeConfig ?? getRuntimeConfigSnapshot();
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
resolveInternalSessionKey,
|
||||
resolveSandboxedSessionToolContext,
|
||||
type SessionListRow,
|
||||
type SessionRunStatus,
|
||||
stripToolMessages,
|
||||
} from "./sessions-helpers.js";
|
||||
|
||||
@@ -37,6 +38,16 @@ const SessionsListToolSchema = Type.Object({
|
||||
|
||||
type GatewayCaller = typeof callGateway;
|
||||
|
||||
function readSessionRunStatus(value: unknown): SessionRunStatus | undefined {
|
||||
return value === "running" ||
|
||||
value === "done" ||
|
||||
value === "failed" ||
|
||||
value === "killed" ||
|
||||
value === "timeout"
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function createSessionsListTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
@@ -247,7 +258,7 @@ export function createSessionsListTool(opts?: {
|
||||
totalTokens: typeof entry.totalTokens === "number" ? entry.totalTokens : undefined,
|
||||
estimatedCostUsd:
|
||||
typeof entry.estimatedCostUsd === "number" ? entry.estimatedCostUsd : undefined,
|
||||
status: readStringValue(entry.status),
|
||||
status: readSessionRunStatus(entry.status),
|
||||
startedAt: typeof entry.startedAt === "number" ? entry.startedAt : undefined,
|
||||
endedAt: typeof entry.endedAt === "number" ? entry.endedAt : undefined,
|
||||
runtimeMs: typeof entry.runtimeMs === "number" ? entry.runtimeMs : undefined,
|
||||
|
||||
Reference in New Issue
Block a user