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:
Xubin Ren
2026-07-13 13:11:46 +08:00
committed by GitHub
parent 791c7fd505
commit fe0717b385
92 changed files with 15058 additions and 1311 deletions
+159
View File
@@ -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",
+15
View File
@@ -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
View File
@@ -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;