feat(webui): add guided setup flows
* feat(channels): add guided setup flows * test(channels): preserve setup config values * fix(channels): reflect saved setup state * refactor(channels): simplify setup state metadata * fix(channels): harden setup lifecycle * refactor(channels): centralize setup contracts * fix(channels): route setup actions through webui shim * fix(channels): adapt settings for compact screens * fix(models): preserve default preset display * feat(models): add curated Codex catalog * fix(webui): stop attached gateway on interrupt * fix(webui): simplify apps catalog * docs(webui): clarify apps and runtime features * feat(settings): add guided capability setup * fix(webui): harden setup and managed services * test: keep managed runtime checks portable * test: scope POSIX runtime coverage * fix(webui): simplify file settings * feat(files): bundle document reading * fix(webui): harden setup request boundaries * fix(webui): prevent channel setup status squeeze * fix(settings): group provider compatibility aliases * refactor(settings): remove redundant setup surfaces * fix(webui): harden guided setup lifecycle * fix(webui): preserve channel setup compatibility
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import type {
|
||||
ApiServicePayload,
|
||||
AutomationsPayload,
|
||||
AutomationUpdatePayload,
|
||||
ChannelConfigurePayload,
|
||||
ChannelConnectPayload,
|
||||
ChannelValidationPayload,
|
||||
ChatSummary,
|
||||
CliAppsPayload,
|
||||
FilePreviewPayload,
|
||||
@@ -10,6 +14,7 @@ import type {
|
||||
ModelConfigurationCreate,
|
||||
ModelConfigurationUpdate,
|
||||
NetworkSafetySettingsUpdate,
|
||||
PairingPayload,
|
||||
ProviderModelsPayload,
|
||||
ProviderSettingsUpdate,
|
||||
SessionDeleteResult,
|
||||
@@ -44,6 +49,8 @@ function isSlashCommandLifecycle(value: unknown): value is SlashCommandLifecycle
|
||||
&& SLASH_COMMAND_LIFECYCLES.has(value as SlashCommandLifecycle)
|
||||
);
|
||||
}
|
||||
const CHANNEL_VALUES_HEADER = "X-Nanobot-Channel-Values";
|
||||
const API_SERVICE_VALUES_HEADER = "X-Nanobot-API-Service-Values";
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
@@ -386,13 +393,43 @@ export async function fetchNanobotFeatures(
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchApiService(token: string, base: string = ""): Promise<ApiServicePayload> {
|
||||
return request<ApiServicePayload>(`${base}/api/settings/api-service`, token);
|
||||
}
|
||||
|
||||
export async function startApiService(
|
||||
token: string,
|
||||
values: { host: string; port: number; timeout: number; apiKey?: string },
|
||||
base: string = "",
|
||||
): Promise<ApiServicePayload> {
|
||||
const query = new URLSearchParams({
|
||||
host: values.host,
|
||||
port: String(values.port),
|
||||
timeout: String(values.timeout),
|
||||
});
|
||||
const headers = values.apiKey === undefined
|
||||
? undefined
|
||||
: { [API_SERVICE_VALUES_HEADER]: JSON.stringify({ api_key: values.apiKey }) };
|
||||
return request<ApiServicePayload>(
|
||||
`${base}/api/settings/api-service/start?${query}`,
|
||||
token,
|
||||
{ headers },
|
||||
);
|
||||
}
|
||||
|
||||
export async function stopApiService(token: string, base: string = ""): Promise<ApiServicePayload> {
|
||||
return request<ApiServicePayload>(`${base}/api/settings/api-service/stop`, token);
|
||||
}
|
||||
|
||||
export async function enableNanobotFeature(
|
||||
token: string,
|
||||
name: string,
|
||||
options: { instanceId?: string } = {},
|
||||
base: string = "",
|
||||
): Promise<NanobotFeaturesPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("name", name);
|
||||
if (options.instanceId) query.set("instance_id", options.instanceId);
|
||||
return request<NanobotFeaturesPayload>(
|
||||
`${base}/api/settings/nanobot-features/enable?${query}`,
|
||||
token,
|
||||
@@ -402,16 +439,138 @@ export async function enableNanobotFeature(
|
||||
export async function disableNanobotFeature(
|
||||
token: string,
|
||||
name: string,
|
||||
options: { instanceId?: string } = {},
|
||||
base: string = "",
|
||||
): Promise<NanobotFeaturesPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("name", name);
|
||||
if (options.instanceId) query.set("instance_id", options.instanceId);
|
||||
return request<NanobotFeaturesPayload>(
|
||||
`${base}/api/settings/nanobot-features/disable?${query}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchPairingRequests(
|
||||
token: string,
|
||||
base: string = "",
|
||||
): Promise<PairingPayload> {
|
||||
return request<PairingPayload>(
|
||||
`${base}/api/settings/pairing`,
|
||||
token,
|
||||
undefined,
|
||||
API_READ_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
export async function runPairingAction(
|
||||
token: string,
|
||||
action: "approve" | "deny",
|
||||
code: string,
|
||||
base: string = "",
|
||||
): Promise<PairingPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("code", code);
|
||||
return request<PairingPayload>(
|
||||
`${base}/api/settings/pairing/${action}?${query}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
export async function startChannelConnect(
|
||||
token: string,
|
||||
channel: "feishu" | "weixin",
|
||||
options: {
|
||||
domain?: "feishu" | "lark";
|
||||
instanceId?: string;
|
||||
mode?: "replace" | "create";
|
||||
force?: boolean;
|
||||
} = {},
|
||||
base: string = "",
|
||||
): Promise<ChannelConnectPayload> {
|
||||
const query = new URLSearchParams();
|
||||
if (options.domain) query.set("domain", options.domain);
|
||||
if (options.instanceId) query.set("instance_id", options.instanceId);
|
||||
if (options.mode) query.set("mode", options.mode);
|
||||
if (options.force) query.set("force", "true");
|
||||
const suffix = query.toString();
|
||||
return request<ChannelConnectPayload>(
|
||||
`${base}/api/settings/channels/${channel}/connect/start${suffix ? `?${suffix}` : ""}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
export async function pollChannelConnect(
|
||||
token: string,
|
||||
channel: "feishu" | "weixin",
|
||||
sessionId: string,
|
||||
base: string = "",
|
||||
): Promise<ChannelConnectPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("session_id", sessionId);
|
||||
return request<ChannelConnectPayload>(
|
||||
`${base}/api/settings/channels/${channel}/connect/poll?${query}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
export async function cancelChannelConnect(
|
||||
token: string,
|
||||
channel: "feishu" | "weixin",
|
||||
sessionId: string,
|
||||
base: string = "",
|
||||
): Promise<ChannelConnectPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("session_id", sessionId);
|
||||
return request<ChannelConnectPayload>(
|
||||
`${base}/api/settings/channels/${channel}/connect/cancel?${query}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
export async function configureChannel(
|
||||
token: string,
|
||||
name: string,
|
||||
values: Record<string, string>,
|
||||
options: { enable?: boolean; instanceId?: string } = {},
|
||||
base: string = "",
|
||||
): Promise<ChannelConfigurePayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("name", name);
|
||||
if (options.enable !== undefined) query.set("enable", String(options.enable));
|
||||
if (options.instanceId) query.set("instance_id", options.instanceId);
|
||||
return request<ChannelConfigurePayload>(
|
||||
`${base}/api/settings/channels/configure?${query}`,
|
||||
token,
|
||||
{
|
||||
headers: {
|
||||
[CHANNEL_VALUES_HEADER]: JSON.stringify(values),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateChannel(
|
||||
token: string,
|
||||
name: string,
|
||||
values: Record<string, string> = {},
|
||||
options: { instanceId?: string } = {},
|
||||
base: string = "",
|
||||
): Promise<ChannelValidationPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("name", name);
|
||||
if (options.instanceId) query.set("instance_id", options.instanceId);
|
||||
return request<ChannelValidationPayload>(
|
||||
`${base}/api/settings/channels/validate?${query}`,
|
||||
token,
|
||||
{
|
||||
headers: {
|
||||
[CHANNEL_VALUES_HEADER]: JSON.stringify(values),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function runCliAppAction(
|
||||
token: string,
|
||||
action: "install" | "update" | "uninstall" | "test",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function isLoopbackHost(host: string): boolean {
|
||||
let normalized = host.trim().toLowerCase();
|
||||
if (normalized.endsWith(".")) normalized = normalized.slice(0, -1);
|
||||
if (normalized.startsWith("[") && normalized.endsWith("]")) {
|
||||
normalized = normalized.slice(1, -1);
|
||||
}
|
||||
if (normalized === "localhost" || normalized === "::1") return true;
|
||||
|
||||
const octets = normalized.split(".");
|
||||
return (
|
||||
octets.length === 4 &&
|
||||
octets[0] === "127" &&
|
||||
octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255)
|
||||
);
|
||||
}
|
||||
+132
-1
@@ -332,6 +332,7 @@ export interface RuntimeCapabilities {
|
||||
export interface ProviderModelInfo {
|
||||
id: string;
|
||||
label?: string | null;
|
||||
description?: string | null;
|
||||
owned_by?: string | null;
|
||||
context_window?: number | null;
|
||||
}
|
||||
@@ -345,7 +346,7 @@ export interface ProviderModelsPayload {
|
||||
| "not_configured"
|
||||
| "missing_api_base"
|
||||
| "error";
|
||||
catalog_kind: "official" | "catalog" | "local" | "custom" | "unsupported";
|
||||
catalog_kind: "builtin" | "official" | "catalog" | "local" | "custom" | "unsupported";
|
||||
models: ProviderModelInfo[];
|
||||
model_count: number;
|
||||
message?: string | null;
|
||||
@@ -399,6 +400,7 @@ export interface SettingsPayload {
|
||||
api_base?: string | null;
|
||||
default_api_base?: string | null;
|
||||
model_selectable?: boolean;
|
||||
model_catalog?: ProviderModelsPayload["catalog_kind"];
|
||||
api_type?: "auto" | "chat_completions" | "responses";
|
||||
oauth_account?: string | null;
|
||||
oauth_expires_at?: number | null;
|
||||
@@ -428,6 +430,17 @@ export interface SettingsPayload {
|
||||
use_jina_reader: boolean;
|
||||
};
|
||||
};
|
||||
api?: {
|
||||
host: string;
|
||||
port: number;
|
||||
timeout: number;
|
||||
api_key_hint?: string | null;
|
||||
};
|
||||
observability?: {
|
||||
provider: "langfuse" | string;
|
||||
configured: boolean;
|
||||
base_url: string;
|
||||
};
|
||||
image_generation: {
|
||||
enabled: boolean;
|
||||
provider: string;
|
||||
@@ -543,6 +556,26 @@ export interface SettingsPayload {
|
||||
version?: {
|
||||
current: string;
|
||||
};
|
||||
docs?: {
|
||||
version: string;
|
||||
base_url: string;
|
||||
chat_apps_url: string;
|
||||
latest_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApiServicePayload {
|
||||
installed: boolean;
|
||||
running: boolean;
|
||||
managed: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
timeout: number;
|
||||
api_key_hint?: string | null;
|
||||
endpoint: string;
|
||||
command: string;
|
||||
log_path?: string | null;
|
||||
last_action?: "started" | "stopped" | string;
|
||||
}
|
||||
|
||||
export interface AppPackageRef {
|
||||
@@ -638,6 +671,10 @@ export interface NanobotFeatureInfo {
|
||||
display_name: string;
|
||||
type: "channel" | "feature" | string;
|
||||
enabled: boolean;
|
||||
configured?: boolean;
|
||||
config_values?: Record<string, string>;
|
||||
configured_fields?: string[];
|
||||
instances?: NanobotChannelInstanceInfo[];
|
||||
installed: boolean;
|
||||
ready: boolean;
|
||||
status: "enabled" | "missing_dependency" | "not_enabled" | string;
|
||||
@@ -645,6 +682,19 @@ export interface NanobotFeatureInfo {
|
||||
requires_restart: boolean;
|
||||
}
|
||||
|
||||
export interface NanobotChannelInstanceInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name?: string;
|
||||
avatar_url?: string;
|
||||
domain?: "feishu" | "lark" | string;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
app_id?: string;
|
||||
group_policy?: string;
|
||||
allow_from?: string[];
|
||||
}
|
||||
|
||||
export interface NanobotFeaturesPayload {
|
||||
features: NanobotFeatureInfo[];
|
||||
enabled_count: number;
|
||||
@@ -656,6 +706,64 @@ export interface NanobotFeaturesPayload {
|
||||
};
|
||||
}
|
||||
|
||||
export type ChannelSetupStatus =
|
||||
| "connected"
|
||||
| "configured"
|
||||
| "needs_setup"
|
||||
| "invalid"
|
||||
| "unsupported"
|
||||
| string;
|
||||
|
||||
export type ChannelValidationCheckStatus = "pass" | "warn" | "fail" | "skipped" | string;
|
||||
|
||||
export interface ChannelValidationCheck {
|
||||
id: string;
|
||||
label: string;
|
||||
status: ChannelValidationCheckStatus;
|
||||
message?: string;
|
||||
action_url?: string;
|
||||
}
|
||||
|
||||
export interface ChannelIdentity {
|
||||
name?: string;
|
||||
workspace?: string;
|
||||
account?: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
export interface ChannelValidationPayload {
|
||||
name: string;
|
||||
status: ChannelSetupStatus;
|
||||
checks: ChannelValidationCheck[];
|
||||
identity?: ChannelIdentity;
|
||||
missing_fields: string[];
|
||||
can_enable: boolean;
|
||||
requires_restart: boolean;
|
||||
checked_at?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PairingRequestInfo {
|
||||
code: string;
|
||||
channel: string;
|
||||
sender_id: string;
|
||||
created_at_ms?: number | null;
|
||||
expires_at_ms?: number | null;
|
||||
expires_in_seconds?: number | null;
|
||||
}
|
||||
|
||||
export interface PairingPayload {
|
||||
requests: PairingRequestInfo[];
|
||||
last_action?: {
|
||||
ok: boolean;
|
||||
action: "approve" | "deny" | string;
|
||||
message: string;
|
||||
code?: string;
|
||||
channel?: string;
|
||||
sender_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface McpPresetField {
|
||||
name: string;
|
||||
label: string;
|
||||
@@ -725,6 +833,29 @@ export interface McpPresetsPayload {
|
||||
};
|
||||
}
|
||||
|
||||
export type ChannelConnectStatus = "pending" | "succeeded" | "expired" | "cancelled" | "failed";
|
||||
|
||||
export interface ChannelConnectPayload {
|
||||
session_id: string;
|
||||
instance_id?: string;
|
||||
status: ChannelConnectStatus;
|
||||
message?: string;
|
||||
qr_url?: string;
|
||||
domain?: string;
|
||||
interval_ms?: number;
|
||||
expires_at_ms?: number;
|
||||
app_id?: string;
|
||||
account?: string;
|
||||
nanobot_features?: NanobotFeaturesPayload;
|
||||
}
|
||||
|
||||
export interface ChannelConfigurePayload {
|
||||
name: string;
|
||||
saved: boolean;
|
||||
saved_keys?: string[];
|
||||
nanobot_features?: NanobotFeaturesPayload;
|
||||
}
|
||||
|
||||
export interface SettingsUpdate {
|
||||
model?: string;
|
||||
provider?: string;
|
||||
|
||||
Reference in New Issue
Block a user