mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-01 15:30:52 +08:00
2887 lines
90 KiB
TypeScript
2887 lines
90 KiB
TypeScript
/* global Buffer, process */
|
|
import { app, shell } from "./electronRuntime";
|
|
import {
|
|
AppServerRequestError,
|
|
ERROR_CODES,
|
|
METHOD_AGENT_APP_INSTALLED_LIST,
|
|
METHOD_AGENT_APP_UI_RUNTIME_START,
|
|
METHOD_AGENT_APP_UI_RUNTIME_STATUS,
|
|
METHOD_AGENT_APP_UI_RUNTIME_STOP,
|
|
METHOD_AGENT_SESSION_LIST,
|
|
METHOD_AGENT_SESSION_ACTION_RESPOND,
|
|
METHOD_AGENT_SESSION_READ,
|
|
METHOD_AGENT_SESSION_START,
|
|
METHOD_AGENT_SESSION_TURN_CANCEL,
|
|
METHOD_AGENT_SESSION_TURN_START,
|
|
METHOD_AGENT_SESSION_UPDATE,
|
|
METHOD_CAPABILITY_LIST,
|
|
METHOD_EVIDENCE_EXPORT,
|
|
METHOD_MODEL_LIST,
|
|
METHOD_MODEL_PREFERENCES_LIST,
|
|
METHOD_MODEL_PROVIDER_ALIAS_LIST,
|
|
METHOD_MODEL_PROVIDER_ALIAS_READ,
|
|
METHOD_MODEL_PROVIDER_CATALOG_LIST,
|
|
METHOD_MODEL_PROVIDER_LIST,
|
|
METHOD_MODEL_SYNC_STATE_READ,
|
|
METHOD_MCP_PROMPT_LIST,
|
|
METHOD_MCP_RESOURCE_LIST,
|
|
METHOD_MCP_SERVER_LIST,
|
|
METHOD_MCP_SERVER_STATUS_LIST,
|
|
METHOD_MCP_TOOL_LIST,
|
|
METHOD_PROJECT_MEMORY_READ,
|
|
METHOD_SKILL_LIST,
|
|
METHOD_USAGE_STATS_DAILY_TRENDS_LIST,
|
|
METHOD_USAGE_STATS_MODEL_RANKING_LIST,
|
|
METHOD_USAGE_STATS_READ,
|
|
METHOD_WORKSPACE_BY_PATH_READ,
|
|
METHOD_WORKSPACE_DEFAULT_ENSURE,
|
|
METHOD_WORKSPACE_DEFAULT_READ,
|
|
METHOD_WORKSPACE_ENSURE_READY,
|
|
METHOD_WORKSPACE_LIST,
|
|
METHOD_WORKSPACE_PROJECTS_ROOT_READ,
|
|
METHOD_WORKSPACE_PROJECT_PATH_RESOLVE,
|
|
METHOD_WORKSPACE_READ,
|
|
METHOD_WORKSPACE_SKILL_BINDINGS_LIST,
|
|
type AgentAttachment,
|
|
type AgentSessionActionRespondResponse,
|
|
type AgentSessionListResponse,
|
|
type AgentSessionOverview,
|
|
type AgentSessionReadResponse,
|
|
type AgentSessionStartResponse,
|
|
type AgentSessionTurnCancelResponse,
|
|
type AgentSessionTurnStartResponse,
|
|
type AgentSessionUpdateResponse,
|
|
type AgentAppInstalledListResponse,
|
|
type AgentAppUiRuntimeStartParams,
|
|
type AgentAppUiRuntimeStatusParams,
|
|
type AgentAppUiRuntimeStatusResponse,
|
|
type AgentAppUiRuntimeStopParams,
|
|
type ArtifactSummary,
|
|
type CapabilityDescriptor,
|
|
type CapabilityListResponse,
|
|
type EvidenceExportResponse,
|
|
type ModelListResponse,
|
|
type ModelPreferencesListResponse,
|
|
type ModelProviderAliasListResponse,
|
|
type ModelProviderAliasReadResponse,
|
|
type ModelProviderCatalogListResponse,
|
|
type ModelProviderListResponse,
|
|
type ModelSyncStateReadResponse,
|
|
type McpPromptListResponse,
|
|
type McpResourceListResponse,
|
|
type McpServerListResponse,
|
|
type McpServerStatusListResponse,
|
|
type McpToolListResponse,
|
|
type ProjectMemoryReadResponse,
|
|
type SkillListResponse,
|
|
type UsageStatsDailyTrendsListResponse,
|
|
type UsageStatsModelRankingListResponse,
|
|
type UsageStatsRangeParams,
|
|
type UsageStatsReadResponse,
|
|
type WorkspaceEnsureReadyResponse,
|
|
type WorkspaceListResponse,
|
|
type WorkspaceProjectPathResolveResponse,
|
|
type WorkspaceProjectsRootReadResponse,
|
|
type WorkspaceReadResponse,
|
|
type WorkspaceSkillBindingsListResponse,
|
|
} from "app-server-client";
|
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
import { createServer, type Server } from "node:http";
|
|
import path from "node:path";
|
|
import type { ElectronAppServerHost } from "./appServerHost";
|
|
|
|
type HostArgs = Record<string, unknown> | null | undefined;
|
|
type AppServerParams = Record<string, unknown>;
|
|
type HostEventEmitter = (event: string, payload?: unknown) => void;
|
|
type FileManagerLocation = {
|
|
id: string;
|
|
label: string;
|
|
path: string;
|
|
kind: string;
|
|
};
|
|
type FileManagerLocationCandidate = Omit<FileManagerLocation, "path"> & {
|
|
path: string | null;
|
|
};
|
|
type ElectronKnownPathName = "home" | "desktop" | "documents" | "downloads";
|
|
type UsageStatsSummaryWire = UsageStatsReadResponse["stats"];
|
|
type UsageStatsModelUsageWire = UsageStatsModelRankingListResponse["ranking"][number];
|
|
type UsageStatsDailyUsageWire = UsageStatsDailyTrendsListResponse["trends"][number];
|
|
|
|
const CONFIG_FILE = "config.json";
|
|
const OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT = "oem-cloud-oauth-callback";
|
|
const OEM_CLOUD_OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
const OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_TTL_MS = 10 * 60 * 1000;
|
|
const OEM_CLOUD_OAUTH_CALLBACK_HTML = `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Lime 登录回调</title>
|
|
<script>
|
|
(function () {
|
|
if (window.location.hash && window.location.hash.length > 1) {
|
|
var params = new URLSearchParams(window.location.hash.slice(1));
|
|
var search = new URLSearchParams(window.location.search);
|
|
params.forEach(function (value, key) {
|
|
if (!search.has(key)) search.set(key, value);
|
|
});
|
|
window.location.replace(window.location.pathname + "?" + search.toString());
|
|
}
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<p>Lime 登录结果已返回,可以关闭此页面。</p>
|
|
</body>
|
|
</html>`;
|
|
|
|
export class ElectronHostCommands {
|
|
readonly #appServerHost: ElectronAppServerHost;
|
|
readonly #userDataDir: string;
|
|
readonly #emit: HostEventEmitter;
|
|
#oauthCallbackBridgeServer: Server | null = null;
|
|
#oauthCallbackBridgeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
constructor(
|
|
appServerHost: ElectronAppServerHost,
|
|
userDataDir = app.getPath("userData"),
|
|
emit: HostEventEmitter = () => undefined,
|
|
) {
|
|
this.#appServerHost = appServerHost;
|
|
this.#userDataDir = userDataDir;
|
|
this.#emit = emit;
|
|
}
|
|
|
|
async invoke(command: string, args?: HostArgs): Promise<unknown> {
|
|
switch (command) {
|
|
case "get_config":
|
|
return await this.#readConfig();
|
|
case "save_config":
|
|
return await this.#saveConfig(args);
|
|
case "get_experimental_config":
|
|
return await this.#getExperimentalConfig();
|
|
case "save_experimental_config":
|
|
return await this.#saveExperimentalConfig(args);
|
|
case "open_external_url":
|
|
return await this.#openExternalUrl(args);
|
|
case "reveal_in_finder":
|
|
return this.#revealInFinder(args);
|
|
case "open_with_default_app":
|
|
return await this.#openWithDefaultApp(args);
|
|
case "get_home_dir":
|
|
return this.#getHomeDir();
|
|
case "get_file_manager_locations":
|
|
return await this.#getFileManagerLocations();
|
|
case "get_file_icon_data_url":
|
|
return await this.#getFileIconDataUrl(args);
|
|
case "start_oem_cloud_oauth_callback_bridge":
|
|
return await this.#startOemCloudOAuthCallbackBridge();
|
|
case "aster_agent_init":
|
|
return await this.#initAgentRuntime();
|
|
case "get_default_provider":
|
|
return await this.#getDefaultProvider();
|
|
case "agent_runtime_list_sessions":
|
|
return await this.#listAgentRuntimeSessions(args);
|
|
case "agent_runtime_get_session":
|
|
return await this.#getAgentRuntimeSession(args);
|
|
case "agent_runtime_create_session":
|
|
return await this.#createAgentRuntimeSession(args);
|
|
case "agent_runtime_submit_turn":
|
|
return await this.#submitAgentRuntimeTurn(args);
|
|
case "agent_runtime_interrupt_turn":
|
|
return await this.#interruptAgentRuntimeTurn(args);
|
|
case "agent_runtime_update_session":
|
|
return await this.#updateAgentRuntimeSession(args);
|
|
case "agent_runtime_respond_action":
|
|
return await this.#respondAgentRuntimeAction(args);
|
|
case "agent_runtime_get_thread_read":
|
|
return await this.#getAgentRuntimeThreadRead(args);
|
|
case "agent_runtime_export_evidence_pack":
|
|
return await this.#exportAgentRuntimeEvidencePack(args);
|
|
case "agent_runtime_get_tool_inventory":
|
|
return await this.#getAgentRuntimeToolInventory(args);
|
|
case "agent_runtime_list_workspace_skill_bindings":
|
|
return await this.#listWorkspaceSkillBindings(args);
|
|
case "get_model_registry":
|
|
return await this.#listModels();
|
|
case "get_model_preferences":
|
|
return await this.#listModelPreferences();
|
|
case "get_model_sync_state":
|
|
return await this.#readModelSyncState();
|
|
case "get_model_registry_provider_ids":
|
|
return await this.#listModelRegistryProviderIds();
|
|
case "get_models_for_provider":
|
|
return await this.#listModelsForProvider(args);
|
|
case "get_models_by_tier":
|
|
return await this.#listModelsByTier(args);
|
|
case "get_provider_alias_config":
|
|
return await this.#readProviderAliasConfig(args);
|
|
case "get_all_alias_configs":
|
|
return await this.#listProviderAliasConfigs();
|
|
case "workspace_list":
|
|
return await this.#listWorkspaces();
|
|
case "workspace_get_default":
|
|
return await this.#readDefaultWorkspace();
|
|
case "get_or_create_default_project":
|
|
return await this.#ensureDefaultWorkspace();
|
|
case "workspace_get":
|
|
return await this.#readWorkspace(args);
|
|
case "workspace_get_by_path":
|
|
return await this.#readWorkspaceByPath(args);
|
|
case "workspace_set_default":
|
|
throw new Error(
|
|
"workspace_set_default is not available in the Electron App Server adapter",
|
|
);
|
|
case "workspace_get_projects_root":
|
|
return await this.#readWorkspaceProjectsRoot();
|
|
case "workspace_resolve_project_path":
|
|
return await this.#resolveWorkspaceProjectPath(args);
|
|
case "workspace_ensure_default_ready":
|
|
return await this.#ensureDefaultWorkspaceReady();
|
|
case "workspace_ensure_ready":
|
|
return await this.#ensureWorkspaceReady(args);
|
|
case "get_local_skills_for_app":
|
|
return await this.#listLocalSkillsForApp(args);
|
|
case "get_usage_stats":
|
|
return await this.#getUsageStats(args);
|
|
case "get_model_usage_ranking":
|
|
return await this.#getModelUsageRanking(args);
|
|
case "get_daily_usage_trends":
|
|
return await this.#getDailyUsageTrends(args);
|
|
case "get_voice_input_config":
|
|
return this.#getVoiceInputConfig();
|
|
case "get_voice_shortcut_runtime_status":
|
|
return this.#getVoiceShortcutRuntimeStatus();
|
|
case "get_asr_credentials":
|
|
return this.#getAsrCredentials();
|
|
case "list_audio_devices":
|
|
return this.#emptyDiagnosticList("list_audio_devices");
|
|
case "get_voice_instructions":
|
|
return this.#getVoiceInstructions();
|
|
case "voice_models_list_catalog":
|
|
return this.#emptyDiagnosticList("voice_models_list_catalog");
|
|
case "voice_models_get_install_state":
|
|
return this.#getVoiceModelInstallState(args);
|
|
case "get_environment_preview":
|
|
return await this.#getEnvironmentPreview();
|
|
case "unified_memory_stats":
|
|
return this.#getUnifiedMemoryStats();
|
|
case "get_mcp_servers":
|
|
return await this.#listMcpServers();
|
|
case "mcp_list_servers_with_status":
|
|
return await this.#listMcpServersWithStatus();
|
|
case "mcp_list_tools":
|
|
return await this.#listMcpTools();
|
|
case "mcp_list_prompts":
|
|
return await this.#listMcpPrompts();
|
|
case "mcp_list_resources":
|
|
return await this.#listMcpResources();
|
|
case "site_get_adapter_catalog_status":
|
|
return this.#getSiteAdapterCatalogStatus();
|
|
case "site_list_adapters":
|
|
return this.#emptyDiagnosticList("site_list_adapters");
|
|
case "get_skill_package_file_association_status":
|
|
return this.#getSkillPackageFileAssociationStatus();
|
|
case "set_skill_package_file_association_default":
|
|
return this.#setSkillPackageFileAssociationDefault();
|
|
case "get_browser_connector_settings_cmd":
|
|
return this.#getBrowserConnectorSettings();
|
|
case "get_browser_connector_install_status_cmd":
|
|
return this.#getBrowserConnectorInstallStatus();
|
|
case "get_chrome_profile_sessions":
|
|
return this.#emptyDiagnosticList("get_chrome_profile_sessions");
|
|
case "get_chrome_bridge_endpoint_info":
|
|
return this.#getChromeBridgeEndpointInfo();
|
|
case "get_chrome_bridge_status":
|
|
return this.#getChromeBridgeStatus();
|
|
case "get_browser_backend_policy":
|
|
return this.#getBrowserBackendPolicy();
|
|
case "get_browser_backends_status":
|
|
return this.#getBrowserBackendsStatus();
|
|
case "project_memory_get":
|
|
return await this.#readProjectMemory(args);
|
|
case "agent_app_list_installed":
|
|
return await this.#listAgentAppInstalled();
|
|
case "agent_app_start_ui_runtime":
|
|
return await this.#startAgentAppUiRuntime(args);
|
|
case "agent_app_get_ui_runtime_status":
|
|
return await this.#getAgentAppUiRuntimeStatus(args);
|
|
case "agent_app_stop_ui_runtime":
|
|
return await this.#stopAgentAppUiRuntime(args);
|
|
case "agent_app_runtime_start_task":
|
|
return await this.#startAgentAppRuntimeTask(args);
|
|
case "agent_app_runtime_get_task":
|
|
return await this.#getAgentAppRuntimeTask(args);
|
|
case "agent_app_runtime_cancel_task":
|
|
return await this.#cancelAgentAppRuntimeTask(args);
|
|
case "agent_app_runtime_submit_host_response":
|
|
return await this.#submitAgentAppRuntimeHostResponse(args);
|
|
case "report_frontend_debug_log":
|
|
this.#reportFrontendDebugLog(args);
|
|
return null;
|
|
case "report_frontend_crash":
|
|
this.#reportFrontendCrash(args);
|
|
return { success: true };
|
|
default:
|
|
throw new Error(`Electron host command is not implemented: ${command}`);
|
|
}
|
|
}
|
|
|
|
async #appServerRequest<T>(
|
|
method: string,
|
|
params: AppServerParams = {},
|
|
): Promise<T> {
|
|
return await this.#appServerHost.request<T>(method, params);
|
|
}
|
|
|
|
async #openExternalUrl(args: HostArgs): Promise<Record<string, never>> {
|
|
const request = readRequest(args);
|
|
const url = readRequiredString(request, "url");
|
|
const normalizedUrl = normalizeExternalUrl(url);
|
|
await shell.openExternal(normalizedUrl);
|
|
return {};
|
|
}
|
|
|
|
#revealInFinder(args: HostArgs): Record<string, never> {
|
|
const request = readRequest(args);
|
|
const targetPath = readRequiredString(request, "path");
|
|
shell.showItemInFolder(targetPath);
|
|
return {};
|
|
}
|
|
|
|
async #openWithDefaultApp(args: HostArgs): Promise<Record<string, never>> {
|
|
const request = readRequest(args);
|
|
const targetPath = readRequiredString(request, "path");
|
|
const errorMessage = await shell.openPath(targetPath);
|
|
if (errorMessage) {
|
|
throw new Error(errorMessage);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
async #getFileIconDataUrl(args: HostArgs): Promise<string | null> {
|
|
const request = readRequest(args);
|
|
const targetPath = readRequiredString(request, "path");
|
|
try {
|
|
const icon = await app.getFileIcon(targetPath, { size: "normal" });
|
|
if (icon.isEmpty()) {
|
|
return null;
|
|
}
|
|
return icon.toDataURL() || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
#getHomeDir(): string {
|
|
const homePath = readElectronPath("home");
|
|
if (!homePath) {
|
|
throw new Error("无法获取主目录");
|
|
}
|
|
return homePath;
|
|
}
|
|
|
|
async #getFileManagerLocations(): Promise<FileManagerLocation[]> {
|
|
const locations: FileManagerLocation[] = [];
|
|
const seenPaths = new Set<string>();
|
|
const homePath = readElectronPath("home");
|
|
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "home",
|
|
label: "个人",
|
|
kind: "home",
|
|
path: homePath,
|
|
});
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "desktop",
|
|
label: "桌面",
|
|
kind: "desktop",
|
|
path: readElectronPath("desktop"),
|
|
});
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "documents",
|
|
label: "文档",
|
|
kind: "documents",
|
|
path: readElectronPath("documents"),
|
|
});
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "downloads",
|
|
label: "下载",
|
|
kind: "downloads",
|
|
path: readElectronPath("downloads"),
|
|
});
|
|
|
|
if (process.platform === "darwin") {
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "applications",
|
|
label: "应用程序",
|
|
kind: "applications",
|
|
path: "/Applications",
|
|
});
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "user-applications",
|
|
label: "用户应用程序",
|
|
kind: "applications",
|
|
path: homePath ? path.join(homePath, "Applications") : null,
|
|
});
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "start-menu-programs",
|
|
label: "应用程序",
|
|
kind: "applications",
|
|
path: process.env.APPDATA
|
|
? path.join(
|
|
process.env.APPDATA,
|
|
"Microsoft",
|
|
"Windows",
|
|
"Start Menu",
|
|
"Programs",
|
|
)
|
|
: null,
|
|
});
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "common-start-menu-programs",
|
|
label: "公共应用程序",
|
|
kind: "applications",
|
|
path: process.env.PROGRAMDATA
|
|
? path.join(
|
|
process.env.PROGRAMDATA,
|
|
"Microsoft",
|
|
"Windows",
|
|
"Start Menu",
|
|
"Programs",
|
|
)
|
|
: null,
|
|
});
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "program-files",
|
|
label: "Program Files",
|
|
kind: "applications",
|
|
path: process.env.ProgramFiles || null,
|
|
});
|
|
await appendFileManagerLocation(locations, seenPaths, {
|
|
id: "program-files-x86",
|
|
label: "Program Files (x86)",
|
|
kind: "applications",
|
|
path: process.env["ProgramFiles(x86)"] || null,
|
|
});
|
|
}
|
|
|
|
return locations;
|
|
}
|
|
|
|
async #startOemCloudOAuthCallbackBridge(): Promise<{
|
|
callbackUrl: string;
|
|
}> {
|
|
await this.#closeOemCloudOAuthCallbackBridge();
|
|
|
|
const server = createServer((request, response) => {
|
|
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
if (requestUrl.pathname !== OEM_CLOUD_OAUTH_CALLBACK_PATH) {
|
|
response.writeHead(404, {
|
|
"Content-Type": "text/plain; charset=utf-8",
|
|
});
|
|
response.end("Not found");
|
|
return;
|
|
}
|
|
|
|
const payload = buildOemCloudOAuthCallbackPayload(requestUrl);
|
|
if (shouldEmitOemCloudOAuthCallback(payload)) {
|
|
this.#emit(OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_EVENT, payload);
|
|
void this.#closeOemCloudOAuthCallbackBridge();
|
|
}
|
|
|
|
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
response.end(OEM_CLOUD_OAUTH_CALLBACK_HTML);
|
|
});
|
|
|
|
server.on("error", (error) => {
|
|
console.warn(
|
|
`[OAuthCallbackBridge] OAuth 本地回调桥运行失败: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
);
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onError = (error: Error) => {
|
|
server.off("listening", onListening);
|
|
reject(error);
|
|
};
|
|
const onListening = () => {
|
|
server.off("error", onError);
|
|
resolve();
|
|
};
|
|
server.once("error", onError);
|
|
server.once("listening", onListening);
|
|
server.listen(0, "127.0.0.1");
|
|
});
|
|
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") {
|
|
server.close();
|
|
throw new Error("无法读取 OAuth 本地回调地址");
|
|
}
|
|
|
|
this.#oauthCallbackBridgeServer = server;
|
|
this.#oauthCallbackBridgeTimer = setTimeout(() => {
|
|
void this.#closeOemCloudOAuthCallbackBridge();
|
|
}, OEM_CLOUD_OAUTH_CALLBACK_BRIDGE_TTL_MS);
|
|
|
|
return {
|
|
callbackUrl: `http://127.0.0.1:${address.port}${OEM_CLOUD_OAUTH_CALLBACK_PATH}`,
|
|
};
|
|
}
|
|
|
|
async #closeOemCloudOAuthCallbackBridge(): Promise<void> {
|
|
if (this.#oauthCallbackBridgeTimer) {
|
|
clearTimeout(this.#oauthCallbackBridgeTimer);
|
|
this.#oauthCallbackBridgeTimer = null;
|
|
}
|
|
const server = this.#oauthCallbackBridgeServer;
|
|
this.#oauthCallbackBridgeServer = null;
|
|
if (!server || !server.listening) {
|
|
return;
|
|
}
|
|
await new Promise<void>((resolve) => {
|
|
server.close(() => resolve());
|
|
});
|
|
}
|
|
|
|
#diagnosticMeta(command: string): Record<string, unknown> {
|
|
return {
|
|
source: "electron-host-diagnostic",
|
|
command,
|
|
status: "degraded",
|
|
appServerCurrent: false,
|
|
};
|
|
}
|
|
|
|
#emptyDiagnosticList(command: string): Array<Record<string, unknown>> {
|
|
const result: Array<Record<string, unknown>> = [];
|
|
Object.defineProperty(result, "__diagnostic", {
|
|
value: this.#diagnosticMeta(command),
|
|
enumerable: false,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
async #startAgentAppUiRuntime(
|
|
args: HostArgs,
|
|
): Promise<AgentAppUiRuntimeStatusResponse> {
|
|
const request = readRequest(args);
|
|
const params: AgentAppUiRuntimeStartParams = {
|
|
appId: readRequiredString(request, "appId"),
|
|
};
|
|
const entryKey = readString(request, "entryKey");
|
|
if (entryKey) {
|
|
params.entryKey = entryKey;
|
|
}
|
|
return await this.#appServerRequest<AgentAppUiRuntimeStatusResponse>(
|
|
METHOD_AGENT_APP_UI_RUNTIME_START,
|
|
params,
|
|
);
|
|
}
|
|
|
|
async #getAgentAppUiRuntimeStatus(
|
|
args: HostArgs,
|
|
): Promise<AgentAppUiRuntimeStatusResponse> {
|
|
const request = readRequest(args);
|
|
const params: AgentAppUiRuntimeStatusParams = {
|
|
appId: readRequiredString(request, "appId"),
|
|
};
|
|
return await this.#appServerRequest<AgentAppUiRuntimeStatusResponse>(
|
|
METHOD_AGENT_APP_UI_RUNTIME_STATUS,
|
|
params,
|
|
);
|
|
}
|
|
|
|
async #stopAgentAppUiRuntime(
|
|
args: HostArgs,
|
|
): Promise<AgentAppUiRuntimeStatusResponse> {
|
|
const request = readRequest(args);
|
|
const params: AgentAppUiRuntimeStopParams = {
|
|
appId: readRequiredString(request, "appId"),
|
|
};
|
|
return await this.#appServerRequest<AgentAppUiRuntimeStatusResponse>(
|
|
METHOD_AGENT_APP_UI_RUNTIME_STOP,
|
|
params,
|
|
);
|
|
}
|
|
|
|
async #startAgentAppRuntimeTask(
|
|
args: HostArgs,
|
|
): Promise<Record<string, unknown>> {
|
|
const request = readRequest(args);
|
|
const appId = readRequiredString(request, "appId");
|
|
const taskKind = readRequiredString(request, "taskKind");
|
|
const workspaceId = readRequiredString(request, "workspaceId");
|
|
const nowMs = Date.now();
|
|
const taskId = readString(request, "taskId") ?? `agent-app-task-${nowMs}`;
|
|
const traceId = `agent-app-trace-${nowMs}`;
|
|
const sessionId =
|
|
readString(request, "sessionId") ?? `agent-app-runtime-${nowMs}`;
|
|
const turnId = readString(request, "turnId") ?? `agent-app-turn-${nowMs}`;
|
|
const eventName =
|
|
readString(request, "eventName") ??
|
|
`agent_app_runtime:${appId}:${taskId}`;
|
|
const queueIfBusy = readBoolean(request, "queueIfBusy") ?? true;
|
|
const skipPreSubmitResume =
|
|
readBoolean(request, "skipPreSubmitResume") ?? false;
|
|
const turnConfig =
|
|
readRecord(request, "turnConfig") ??
|
|
readRecord(request, "turn_config") ??
|
|
{};
|
|
const metadata = {
|
|
...(readRecord(request, "metadata") ?? {}),
|
|
...(readRecord(turnConfig, "metadata") ?? {}),
|
|
};
|
|
const message = buildAgentAppRuntimeTaskMessage(request);
|
|
const providerPreference =
|
|
readString(request, "providerPreference") ??
|
|
readString(request, "provider_preference") ??
|
|
readString(turnConfig, "providerPreference") ??
|
|
readString(turnConfig, "provider_preference");
|
|
const modelPreference =
|
|
readString(request, "modelPreference") ??
|
|
readString(request, "model_preference") ??
|
|
readString(turnConfig, "modelPreference") ??
|
|
readString(turnConfig, "model_preference");
|
|
const queuedTurnId = `agent-app-queued-${taskId}`;
|
|
const hostOptions = {
|
|
asterChatRequest: {
|
|
message,
|
|
session_id: sessionId,
|
|
event_name: eventName,
|
|
images: null,
|
|
provider_config:
|
|
turnConfig.providerConfig ?? turnConfig.provider_config ?? null,
|
|
provider_preference: providerPreference,
|
|
model_preference: modelPreference,
|
|
reasoning_effort:
|
|
turnConfig.reasoningEffort ?? turnConfig.reasoning_effort ?? null,
|
|
thinking_enabled:
|
|
turnConfig.thinkingEnabled ?? turnConfig.thinking_enabled ?? null,
|
|
approval_policy:
|
|
turnConfig.approvalPolicy ?? turnConfig.approval_policy ?? null,
|
|
sandbox_policy:
|
|
turnConfig.sandboxPolicy ?? turnConfig.sandbox_policy ?? null,
|
|
project_id: null,
|
|
workspace_id: workspaceId,
|
|
web_search: turnConfig.webSearch ?? turnConfig.web_search ?? null,
|
|
search_mode: turnConfig.searchMode ?? turnConfig.search_mode ?? null,
|
|
execution_strategy:
|
|
turnConfig.executionStrategy ?? turnConfig.execution_strategy ?? null,
|
|
auto_continue:
|
|
turnConfig.autoContinue ?? turnConfig.auto_continue ?? null,
|
|
system_prompt:
|
|
turnConfig.systemPrompt ?? turnConfig.system_prompt ?? null,
|
|
metadata,
|
|
turn_id: turnId,
|
|
queue_if_busy: queueIfBusy,
|
|
queued_turn_id: queuedTurnId,
|
|
turn_config: turnConfig,
|
|
},
|
|
};
|
|
|
|
await this.#ensureAgentAppRuntimeSession({ sessionId, appId, workspaceId });
|
|
await this.#appServerRequest<AgentSessionTurnStartResponse>(
|
|
METHOD_AGENT_SESSION_TURN_START,
|
|
{
|
|
sessionId,
|
|
turnId,
|
|
input: {
|
|
text: message,
|
|
attachments: [],
|
|
},
|
|
runtimeOptions: {
|
|
stream: true,
|
|
eventName,
|
|
providerPreference: providerPreference ?? undefined,
|
|
modelPreference: modelPreference ?? undefined,
|
|
metadata,
|
|
queuedTurnId,
|
|
hostOptions,
|
|
},
|
|
queueIfBusy,
|
|
skipPreSubmitResume,
|
|
},
|
|
);
|
|
|
|
return {
|
|
appId,
|
|
entryKey: readString(request, "entryKey") ?? undefined,
|
|
taskId,
|
|
traceId,
|
|
taskKind,
|
|
sessionId,
|
|
turnId,
|
|
eventName,
|
|
status: "accepted",
|
|
submittedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
async #getAgentAppRuntimeTask(
|
|
args: HostArgs,
|
|
): Promise<Record<string, unknown>> {
|
|
const request = readRequest(args);
|
|
const appId = readRequiredString(request, "appId");
|
|
const taskId = readRequiredString(request, "taskId");
|
|
const sessionId = readRequiredString(request, "sessionId");
|
|
const response = await this.#appServerRequest<AgentSessionReadResponse>(
|
|
METHOD_AGENT_SESSION_READ,
|
|
{ sessionId },
|
|
);
|
|
return {
|
|
appId,
|
|
taskId,
|
|
sessionId,
|
|
status: "thread_read_available",
|
|
taskStatus: sessionStatusToAgentAppTaskStatus(response.session.status),
|
|
taskEvents: [],
|
|
threadRead: response.detail ?? sessionReadToLegacy(response),
|
|
};
|
|
}
|
|
|
|
async #cancelAgentAppRuntimeTask(
|
|
args: HostArgs,
|
|
): Promise<Record<string, unknown>> {
|
|
const request = readRequest(args);
|
|
const appId = readRequiredString(request, "appId");
|
|
const taskId = readRequiredString(request, "taskId");
|
|
const sessionId = readRequiredString(request, "sessionId");
|
|
let turnId = readString(request, "turnId");
|
|
if (!turnId) {
|
|
const response = await this.#appServerRequest<AgentSessionReadResponse>(
|
|
METHOD_AGENT_SESSION_READ,
|
|
{ sessionId },
|
|
);
|
|
turnId = activeAgentSessionTurnId(response);
|
|
}
|
|
if (!turnId) {
|
|
return {
|
|
appId,
|
|
taskId,
|
|
sessionId,
|
|
cancelled: false,
|
|
status: "not_running",
|
|
};
|
|
}
|
|
await this.#appServerRequest<AgentSessionTurnCancelResponse>(
|
|
METHOD_AGENT_SESSION_TURN_CANCEL,
|
|
{ sessionId, turnId },
|
|
);
|
|
return {
|
|
appId,
|
|
taskId,
|
|
sessionId,
|
|
cancelled: true,
|
|
status: "cancelled",
|
|
};
|
|
}
|
|
|
|
async #submitAgentAppRuntimeHostResponse(
|
|
args: HostArgs,
|
|
): Promise<Record<string, unknown>> {
|
|
const request = readRequest(args);
|
|
const appId = readRequiredString(request, "appId");
|
|
const taskId = readRequiredString(request, "taskId");
|
|
const runtimeRequest = readRecord(request, "runtimeRequest") ?? {};
|
|
const sessionId =
|
|
readString(runtimeRequest, "sessionId") ??
|
|
readRequiredString(runtimeRequest, "session_id");
|
|
const requestId =
|
|
readString(runtimeRequest, "requestId") ??
|
|
readRequiredString(runtimeRequest, "request_id");
|
|
const actionType =
|
|
readString(runtimeRequest, "actionType") ??
|
|
readString(runtimeRequest, "action_type") ??
|
|
"tool_confirmation";
|
|
await this.#appServerRequest<AgentSessionActionRespondResponse>(
|
|
METHOD_AGENT_SESSION_ACTION_RESPOND,
|
|
{
|
|
sessionId,
|
|
requestId,
|
|
actionType,
|
|
confirmed: readBoolean(runtimeRequest, "confirmed") ?? false,
|
|
...readStringParam(runtimeRequest, "response", "response"),
|
|
userData: runtimeRequest.userData ?? runtimeRequest.user_data,
|
|
metadata: runtimeRequest.metadata,
|
|
...readStringParam(runtimeRequest, "eventName", "eventName"),
|
|
...readStringParam(runtimeRequest, "event_name", "eventName"),
|
|
actionScope: normalizeAgentSessionActionScope(
|
|
runtimeRequest.actionScope ?? runtimeRequest.action_scope,
|
|
),
|
|
},
|
|
);
|
|
return {
|
|
appId,
|
|
taskId,
|
|
status: "submitted",
|
|
};
|
|
}
|
|
|
|
async #ensureAgentAppRuntimeSession(params: {
|
|
sessionId: string;
|
|
appId: string;
|
|
workspaceId: string;
|
|
}): Promise<void> {
|
|
try {
|
|
await this.#appServerRequest<AgentSessionStartResponse>(
|
|
METHOD_AGENT_SESSION_START,
|
|
params,
|
|
);
|
|
} catch (error) {
|
|
if (isAppServerSessionAlreadyExistsError(error)) {
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async #initAgentRuntime(): Promise<{
|
|
initialized: true;
|
|
provider_configured: boolean;
|
|
provider_name?: string;
|
|
provider_selector?: string;
|
|
model_name?: string;
|
|
}> {
|
|
const [config, providers, models] = await Promise.all([
|
|
this.#readConfig().catch(() => buildDefaultConfig()),
|
|
this.#listModelProviders().catch(() => []),
|
|
this.#listModels().catch(() => []),
|
|
]);
|
|
const defaultProvider = resolveCurrentDefaultProvider(
|
|
config.default_provider,
|
|
providers,
|
|
);
|
|
const configuredDefaultProvider = findProvider(providers, defaultProvider);
|
|
const selectedProvider =
|
|
(configuredDefaultProvider &&
|
|
isConfiguredProvider(configuredDefaultProvider)
|
|
? configuredDefaultProvider
|
|
: null) ??
|
|
providers.find(isConfiguredProvider) ??
|
|
configuredDefaultProvider;
|
|
const selectedProviderId =
|
|
readString(selectedProvider, "id")?.trim() ?? defaultProvider.trim();
|
|
const normalizedSelectedProviderId =
|
|
normalizeProviderIdentity(selectedProviderId);
|
|
const selectedModel = selectedProviderId
|
|
? models.find((model) => {
|
|
const providerId = normalizeProviderIdentity(
|
|
readString(model, "provider_id") ?? readString(model, "providerId"),
|
|
);
|
|
return providerId === normalizedSelectedProviderId;
|
|
})
|
|
: undefined;
|
|
const modelName =
|
|
readString(selectedModel, "id") ??
|
|
readString(selectedModel, "model_id") ??
|
|
readString(selectedModel, "modelId");
|
|
|
|
return {
|
|
initialized: true,
|
|
provider_configured: selectedProvider
|
|
? isConfiguredProvider(selectedProvider)
|
|
: false,
|
|
provider_name: readString(selectedProvider, "name") ?? selectedProviderId,
|
|
provider_selector: selectedProviderId || undefined,
|
|
model_name: modelName ?? undefined,
|
|
};
|
|
}
|
|
|
|
async #getDefaultProvider(): Promise<string> {
|
|
const [config, providers] = await Promise.all([
|
|
this.#readConfig(),
|
|
this.#listModelProviders(),
|
|
]);
|
|
return resolveCurrentDefaultProvider(config.default_provider, providers);
|
|
}
|
|
|
|
async #getExperimentalConfig(): Promise<Record<string, unknown>> {
|
|
const config = await this.#readConfig();
|
|
return normalizeExperimentalConfig(config.experimental);
|
|
}
|
|
|
|
async #saveExperimentalConfig(args: HostArgs): Promise<{ success: true }> {
|
|
const request = readRequest(args);
|
|
const experimentalConfig =
|
|
readRecord(request, "experimentalConfig") ??
|
|
readRecord(request, "experimental_config");
|
|
if (!experimentalConfig) {
|
|
throw new Error("save_experimental_config requires experimentalConfig");
|
|
}
|
|
const config = await this.#readConfig();
|
|
return await this.#saveConfig({
|
|
config: {
|
|
...config,
|
|
experimental: normalizeExperimentalConfig(experimentalConfig),
|
|
},
|
|
});
|
|
}
|
|
|
|
async #listAgentRuntimeSessions(args: HostArgs): Promise<unknown[]> {
|
|
const request = readRequest(args);
|
|
const params: AppServerParams = {
|
|
...readBooleanParam(request, "include_archived", "includeArchived"),
|
|
...readBooleanParam(request, "archived_only", "archivedOnly"),
|
|
...readStringParam(request, "workspace_id", "workspaceId"),
|
|
...readNumberParam(request, "limit", "limit"),
|
|
};
|
|
const response = await this.#appServerRequest<AgentSessionListResponse>(
|
|
METHOD_AGENT_SESSION_LIST,
|
|
params,
|
|
);
|
|
return response.sessions.map(sessionOverviewToLegacy);
|
|
}
|
|
|
|
async #getAgentRuntimeSession(args: HostArgs): Promise<unknown> {
|
|
const request = readRequest(args);
|
|
const sessionId =
|
|
readString(request, "sessionId") ?? readString(request, "session_id");
|
|
if (!sessionId) {
|
|
throw new Error("agent_runtime_get_session requires sessionId");
|
|
}
|
|
const response = await this.#appServerRequest<AgentSessionReadResponse>(
|
|
METHOD_AGENT_SESSION_READ,
|
|
{
|
|
sessionId,
|
|
...readNumberParam(request, "historyLimit", "historyLimit"),
|
|
...readNumberParam(request, "history_limit", "historyLimit"),
|
|
...readNumberParam(request, "historyOffset", "historyOffset"),
|
|
...readNumberParam(request, "history_offset", "historyOffset"),
|
|
...readNumberParam(
|
|
request,
|
|
"historyBeforeMessageId",
|
|
"historyBeforeMessageId",
|
|
),
|
|
...readNumberParam(
|
|
request,
|
|
"history_before_message_id",
|
|
"historyBeforeMessageId",
|
|
),
|
|
},
|
|
);
|
|
return response.detail ?? sessionReadToLegacy(response);
|
|
}
|
|
|
|
async #createAgentRuntimeSession(args: HostArgs): Promise<string> {
|
|
const request = readRequest(args);
|
|
const workspaceId =
|
|
readString(request, "workspaceId") ?? readString(request, "workspace_id");
|
|
if (!workspaceId) {
|
|
throw new Error("agent_runtime_create_session requires workspaceId");
|
|
}
|
|
const name = readString(request, "name") ?? "新对话";
|
|
const optionMetadata = readRecord(request, "metadata") ?? {};
|
|
const executionStrategy =
|
|
readString(request, "executionStrategy") ??
|
|
readString(request, "execution_strategy") ??
|
|
undefined;
|
|
const response = await this.#appServerRequest<AgentSessionStartResponse>(
|
|
METHOD_AGENT_SESSION_START,
|
|
{
|
|
appId: "desktop",
|
|
workspaceId,
|
|
businessObjectRef: {
|
|
kind: "agent.session",
|
|
id: `agent-session:${workspaceId}:${Date.now()}`,
|
|
title: name,
|
|
metadata: {
|
|
...optionMetadata,
|
|
title: name,
|
|
executionStrategy,
|
|
...(readBoolean(request, "runStartHooks") === false
|
|
? { runStartHooks: false }
|
|
: {}),
|
|
},
|
|
},
|
|
},
|
|
);
|
|
return response.session.sessionId;
|
|
}
|
|
|
|
async #submitAgentRuntimeTurn(args: HostArgs): Promise<void> {
|
|
const request = readRequest(args);
|
|
const sessionId =
|
|
readString(request, "session_id") ?? readString(request, "sessionId");
|
|
const message = readString(request, "message") ?? "";
|
|
if (!sessionId) {
|
|
throw new Error("agent_runtime_submit_turn requires session_id");
|
|
}
|
|
if (!message.trim()) {
|
|
throw new Error("agent_runtime_submit_turn requires message");
|
|
}
|
|
|
|
const turnConfig =
|
|
readRecord(request, "turn_config") ?? readRecord(request, "turnConfig");
|
|
const metadata = readRecord(turnConfig, "metadata") ?? undefined;
|
|
const providerPreference =
|
|
readString(turnConfig, "provider_preference") ??
|
|
readString(turnConfig, "providerPreference") ??
|
|
undefined;
|
|
const modelPreference =
|
|
readString(turnConfig, "model_preference") ??
|
|
readString(turnConfig, "modelPreference") ??
|
|
undefined;
|
|
const asterChatRequest = buildAgentRuntimeAsterChatRequest({
|
|
request,
|
|
sessionId,
|
|
message,
|
|
turnConfig,
|
|
providerPreference,
|
|
modelPreference,
|
|
});
|
|
await this.#appServerRequest<AgentSessionTurnStartResponse>(
|
|
METHOD_AGENT_SESSION_TURN_START,
|
|
{
|
|
sessionId,
|
|
...readStringParam(request, "turn_id", "turnId"),
|
|
...readStringParam(request, "turnId", "turnId"),
|
|
input: {
|
|
text: message,
|
|
attachments: normalizeAgentAttachments(readArray(request, "images")),
|
|
},
|
|
runtimeOptions: {
|
|
stream: true,
|
|
eventName:
|
|
readString(request, "event_name") ??
|
|
readString(request, "eventName") ??
|
|
`agentSession/event/${sessionId}`,
|
|
providerPreference,
|
|
modelPreference,
|
|
metadata,
|
|
queuedTurnId:
|
|
readString(request, "queued_turn_id") ??
|
|
readString(request, "queuedTurnId") ??
|
|
undefined,
|
|
hostOptions: {
|
|
asterChatRequest,
|
|
agentRuntimeSubmitTurnRequest: request,
|
|
},
|
|
},
|
|
queueIfBusy:
|
|
readBoolean(request, "queue_if_busy") ??
|
|
readBoolean(request, "queueIfBusy") ??
|
|
false,
|
|
skipPreSubmitResume:
|
|
readBoolean(request, "skip_pre_submit_resume") ??
|
|
readBoolean(request, "skipPreSubmitResume") ??
|
|
false,
|
|
},
|
|
);
|
|
}
|
|
|
|
async #interruptAgentRuntimeTurn(args: HostArgs): Promise<boolean> {
|
|
const request = readRequest(args);
|
|
const sessionId =
|
|
readString(request, "session_id") ?? readString(request, "sessionId");
|
|
const turnId =
|
|
readString(request, "turn_id") ?? readString(request, "turnId");
|
|
if (!sessionId || !turnId) {
|
|
throw new Error(
|
|
"agent_runtime_interrupt_turn requires session_id and turn_id",
|
|
);
|
|
}
|
|
await this.#appServerRequest<AgentSessionTurnCancelResponse>(
|
|
METHOD_AGENT_SESSION_TURN_CANCEL,
|
|
{ sessionId, turnId },
|
|
);
|
|
return true;
|
|
}
|
|
|
|
async #updateAgentRuntimeSession(args: HostArgs): Promise<void> {
|
|
const request = readRequest(args);
|
|
const sessionId =
|
|
readString(request, "session_id") ?? readString(request, "sessionId");
|
|
if (!sessionId) {
|
|
throw new Error("agent_runtime_update_session requires session_id");
|
|
}
|
|
await this.#appServerRequest<AgentSessionUpdateResponse>(
|
|
METHOD_AGENT_SESSION_UPDATE,
|
|
{
|
|
sessionId,
|
|
...readStringParam(request, "name", "title"),
|
|
...readStringParam(request, "title", "title"),
|
|
...readBooleanParam(request, "archived", "archived"),
|
|
...readBooleanParam(request, "isArchived", "archived"),
|
|
...readStringParam(request, "provider_selector", "providerSelector"),
|
|
...readStringParam(request, "providerSelector", "providerSelector"),
|
|
...readStringParam(request, "provider_name", "providerName"),
|
|
...readStringParam(request, "providerName", "providerName"),
|
|
...readStringParam(request, "model_name", "modelName"),
|
|
...readStringParam(request, "modelName", "modelName"),
|
|
...readStringParam(request, "execution_strategy", "executionStrategy"),
|
|
...readStringParam(request, "executionStrategy", "executionStrategy"),
|
|
...readStringParam(request, "recent_access_mode", "recentAccessMode"),
|
|
...readStringParam(request, "recentAccessMode", "recentAccessMode"),
|
|
...readValueParam(request, "recent_preferences", "recentPreferences"),
|
|
...readValueParam(request, "recentPreferences", "recentPreferences"),
|
|
...readValueParam(
|
|
request,
|
|
"recent_team_selection",
|
|
"recentTeamSelection",
|
|
),
|
|
...readValueParam(
|
|
request,
|
|
"recentTeamSelection",
|
|
"recentTeamSelection",
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
async #respondAgentRuntimeAction(args: HostArgs): Promise<void> {
|
|
const request = readRequest(args);
|
|
const sessionId =
|
|
readString(request, "session_id") ?? readString(request, "sessionId");
|
|
const requestId =
|
|
readString(request, "request_id") ?? readString(request, "requestId");
|
|
const actionType =
|
|
readString(request, "action_type") ??
|
|
readString(request, "actionType") ??
|
|
"tool_confirmation";
|
|
if (!sessionId || !requestId) {
|
|
throw new Error(
|
|
"agent_runtime_respond_action requires session_id and request_id",
|
|
);
|
|
}
|
|
await this.#appServerRequest(METHOD_AGENT_SESSION_ACTION_RESPOND, {
|
|
sessionId,
|
|
requestId,
|
|
actionType,
|
|
confirmed: readBoolean(request, "confirmed") ?? false,
|
|
...readStringParam(request, "response", "response"),
|
|
userData: request.user_data ?? request.userData,
|
|
metadata: request.metadata,
|
|
...readStringParam(request, "event_name", "eventName"),
|
|
...readStringParam(request, "eventName", "eventName"),
|
|
actionScope: request.action_scope ?? request.actionScope,
|
|
});
|
|
}
|
|
|
|
async #getAgentRuntimeThreadRead(
|
|
args: HostArgs,
|
|
): Promise<Record<string, unknown>> {
|
|
const request = readRequest(args);
|
|
const sessionId =
|
|
readString(request, "sessionId") ?? readString(request, "session_id");
|
|
if (!sessionId) {
|
|
throw new Error("agent_runtime_get_thread_read requires sessionId");
|
|
}
|
|
const response = await this.#appServerRequest<AgentSessionReadResponse>(
|
|
METHOD_AGENT_SESSION_READ,
|
|
{ sessionId },
|
|
);
|
|
const threadRead = threadReadFromAgentSessionRead(response);
|
|
if (threadRead) {
|
|
return threadRead;
|
|
}
|
|
return {
|
|
session_id: response.session.sessionId,
|
|
thread_id: response.session.threadId,
|
|
turns: response.turns,
|
|
pending_requests: [],
|
|
queued_turns: [],
|
|
diagnostics: null,
|
|
};
|
|
}
|
|
|
|
async #exportAgentRuntimeEvidencePack(
|
|
args: HostArgs,
|
|
): Promise<Record<string, unknown>> {
|
|
const request = readRequest(args);
|
|
const sessionId =
|
|
readString(request, "sessionId") ?? readString(request, "session_id");
|
|
if (!sessionId) {
|
|
throw new Error("agent_runtime_export_evidence_pack requires sessionId");
|
|
}
|
|
const turnId =
|
|
readString(request, "turnId") ?? readString(request, "turn_id");
|
|
const response = await this.#appServerRequest<EvidenceExportResponse>(
|
|
METHOD_EVIDENCE_EXPORT,
|
|
{
|
|
sessionId,
|
|
...(turnId ? { turnId } : {}),
|
|
includeEvents: true,
|
|
includeArtifacts: true,
|
|
includeEvidencePack: true,
|
|
},
|
|
);
|
|
return evidenceExportToLegacy(response);
|
|
}
|
|
|
|
async #listModelProviders(): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<ModelProviderListResponse>(
|
|
METHOD_MODEL_PROVIDER_LIST,
|
|
);
|
|
return response.providers;
|
|
}
|
|
|
|
async #listModels(params: AppServerParams = {}): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<ModelListResponse>(
|
|
METHOD_MODEL_LIST,
|
|
params,
|
|
);
|
|
return response.models;
|
|
}
|
|
|
|
async #listModelPreferences(): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<ModelPreferencesListResponse>(
|
|
METHOD_MODEL_PREFERENCES_LIST,
|
|
);
|
|
return response.preferences;
|
|
}
|
|
|
|
async #readModelSyncState(): Promise<unknown> {
|
|
const response = await this.#appServerRequest<ModelSyncStateReadResponse>(
|
|
METHOD_MODEL_SYNC_STATE_READ,
|
|
);
|
|
return response.syncState;
|
|
}
|
|
|
|
async #listMcpServers(): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<McpServerListResponse>(
|
|
METHOD_MCP_SERVER_LIST,
|
|
);
|
|
return response.servers;
|
|
}
|
|
|
|
async #listMcpServersWithStatus(): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<McpServerStatusListResponse>(
|
|
METHOD_MCP_SERVER_STATUS_LIST,
|
|
);
|
|
return response.servers;
|
|
}
|
|
|
|
async #listMcpTools(): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<McpToolListResponse>(
|
|
METHOD_MCP_TOOL_LIST,
|
|
);
|
|
return response.tools;
|
|
}
|
|
|
|
async #listMcpPrompts(): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<McpPromptListResponse>(
|
|
METHOD_MCP_PROMPT_LIST,
|
|
);
|
|
return response.prompts;
|
|
}
|
|
|
|
async #listMcpResources(): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<McpResourceListResponse>(
|
|
METHOD_MCP_RESOURCE_LIST,
|
|
);
|
|
return response.resources;
|
|
}
|
|
|
|
async #listModelRegistryProviderIds(): Promise<string[]> {
|
|
await this.#appServerRequest<ModelProviderCatalogListResponse>(
|
|
METHOD_MODEL_PROVIDER_CATALOG_LIST,
|
|
);
|
|
return [];
|
|
}
|
|
|
|
async #listModelsForProvider(args: HostArgs): Promise<unknown[]> {
|
|
const request = readRequest(args);
|
|
const providerId =
|
|
readString(request, "providerId") ?? readString(request, "provider_id");
|
|
if (!providerId) {
|
|
return [];
|
|
}
|
|
return await this.#listModels({ providerId });
|
|
}
|
|
|
|
async #listModelsByTier(args: HostArgs): Promise<unknown[]> {
|
|
const request = readRequest(args);
|
|
const tier = readString(request, "tier");
|
|
if (!tier) {
|
|
return [];
|
|
}
|
|
return await this.#listModels({ tier });
|
|
}
|
|
|
|
async #readProviderAliasConfig(args: HostArgs): Promise<unknown | null> {
|
|
const request = readRequest(args);
|
|
const provider = readString(request, "provider");
|
|
if (!provider) {
|
|
return null;
|
|
}
|
|
const response =
|
|
await this.#appServerRequest<ModelProviderAliasReadResponse>(
|
|
METHOD_MODEL_PROVIDER_ALIAS_READ,
|
|
{ provider },
|
|
);
|
|
return response.config ?? null;
|
|
}
|
|
|
|
async #listProviderAliasConfigs(): Promise<Record<string, unknown>> {
|
|
const response =
|
|
await this.#appServerRequest<ModelProviderAliasListResponse>(
|
|
METHOD_MODEL_PROVIDER_ALIAS_LIST,
|
|
);
|
|
return response.configs;
|
|
}
|
|
|
|
async #getAgentRuntimeToolInventory(
|
|
args: HostArgs,
|
|
): Promise<Record<string, unknown>> {
|
|
const request = readRequest(args);
|
|
const caller = readString(request, "caller") ?? "assistant";
|
|
const surface = {
|
|
workbench: readBoolean(request, "workbench") ?? false,
|
|
browser_assist:
|
|
readBoolean(request, "browserAssist") ??
|
|
readBoolean(request, "browser_assist") ??
|
|
false,
|
|
};
|
|
const response = await this.#appServerRequest<CapabilityListResponse>(
|
|
METHOD_CAPABILITY_LIST,
|
|
{
|
|
...readStringParam(request, "appId", "appId"),
|
|
...readStringParam(request, "app_id", "appId"),
|
|
...readStringParam(request, "workspaceId", "workspaceId"),
|
|
...readStringParam(request, "workspace_id", "workspaceId"),
|
|
...readStringParam(request, "sessionId", "sessionId"),
|
|
...readStringParam(request, "session_id", "sessionId"),
|
|
},
|
|
);
|
|
return capabilitiesToToolInventory(response.capabilities, caller, surface);
|
|
}
|
|
|
|
async #listWorkspaces(): Promise<unknown[]> {
|
|
const response = await this.#appServerRequest<WorkspaceListResponse>(
|
|
METHOD_WORKSPACE_LIST,
|
|
);
|
|
return response.workspaces;
|
|
}
|
|
|
|
async #readWorkspace(args: HostArgs): Promise<unknown | null> {
|
|
const request = readRequest(args);
|
|
const id = readString(request, "id") ?? readString(args, "id");
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
const response = await this.#appServerRequest<WorkspaceReadResponse>(
|
|
METHOD_WORKSPACE_READ,
|
|
{ id },
|
|
);
|
|
return response.workspace ?? null;
|
|
}
|
|
|
|
async #readWorkspaceByPath(args: HostArgs): Promise<unknown | null> {
|
|
const request = readRequest(args);
|
|
const rootPath =
|
|
readString(request, "rootPath") ??
|
|
readString(request, "root_path") ??
|
|
readString(args, "rootPath") ??
|
|
readString(args, "root_path");
|
|
if (!rootPath) {
|
|
return null;
|
|
}
|
|
const response = await this.#appServerRequest<WorkspaceReadResponse>(
|
|
METHOD_WORKSPACE_BY_PATH_READ,
|
|
{ rootPath },
|
|
);
|
|
return response.workspace ?? null;
|
|
}
|
|
|
|
async #readDefaultWorkspace(): Promise<unknown | null> {
|
|
const response = await this.#appServerRequest<WorkspaceReadResponse>(
|
|
METHOD_WORKSPACE_DEFAULT_READ,
|
|
);
|
|
return response.workspace ?? null;
|
|
}
|
|
|
|
async #ensureDefaultWorkspace(): Promise<unknown> {
|
|
const response = await this.#appServerRequest<WorkspaceReadResponse>(
|
|
METHOD_WORKSPACE_DEFAULT_ENSURE,
|
|
);
|
|
if (!response.workspace) {
|
|
throw new Error("workspace/default/ensure returned no workspace");
|
|
}
|
|
return response.workspace;
|
|
}
|
|
|
|
async #readWorkspaceProjectsRoot(): Promise<string> {
|
|
const response =
|
|
await this.#appServerRequest<WorkspaceProjectsRootReadResponse>(
|
|
METHOD_WORKSPACE_PROJECTS_ROOT_READ,
|
|
);
|
|
return response.rootPath;
|
|
}
|
|
|
|
async #resolveWorkspaceProjectPath(args: HostArgs): Promise<string> {
|
|
const request = readRequest(args);
|
|
const name =
|
|
readString(request, "name") ?? readString(args, "name") ?? "untitled";
|
|
const parentRootPath =
|
|
readString(request, "parentRootPath") ??
|
|
readString(request, "parent_root_path") ??
|
|
readString(args, "parentRootPath") ??
|
|
readString(args, "parent_root_path");
|
|
const response =
|
|
await this.#appServerRequest<WorkspaceProjectPathResolveResponse>(
|
|
METHOD_WORKSPACE_PROJECT_PATH_RESOLVE,
|
|
{
|
|
name,
|
|
...(parentRootPath ? { parentRootPath } : {}),
|
|
},
|
|
);
|
|
return response.rootPath;
|
|
}
|
|
|
|
async #ensureDefaultWorkspaceReady(): Promise<unknown | null> {
|
|
const workspace = await this.#ensureDefaultWorkspace();
|
|
const id = readString(workspace, "id");
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
const response = await this.#appServerRequest<WorkspaceEnsureReadyResponse>(
|
|
METHOD_WORKSPACE_ENSURE_READY,
|
|
{ id },
|
|
);
|
|
return response.result;
|
|
}
|
|
|
|
async #ensureWorkspaceReady(args: HostArgs): Promise<unknown> {
|
|
const request = readRequest(args);
|
|
const id = readString(request, "id") ?? readString(args, "id");
|
|
if (!id) {
|
|
throw new Error("workspace_ensure_ready requires id");
|
|
}
|
|
const response = await this.#appServerRequest<WorkspaceEnsureReadyResponse>(
|
|
METHOD_WORKSPACE_ENSURE_READY,
|
|
{ id },
|
|
);
|
|
return response.result;
|
|
}
|
|
|
|
async #listLocalSkillsForApp(args: HostArgs): Promise<unknown[]> {
|
|
const request = readRequest(args);
|
|
const appName = readString(request, "app") ?? "lime";
|
|
if (appName !== "lime") {
|
|
return [];
|
|
}
|
|
const response =
|
|
await this.#appServerRequest<SkillListResponse>(METHOD_SKILL_LIST);
|
|
return response.skills.map(skillToLocalSkill);
|
|
}
|
|
|
|
async #listWorkspaceSkillBindings(args: HostArgs): Promise<unknown> {
|
|
const request = readRequest(args);
|
|
const workspaceRoot =
|
|
readString(request, "workspaceRoot") ??
|
|
readString(request, "workspace_root") ??
|
|
readString(args, "workspaceRoot") ??
|
|
readString(args, "workspace_root");
|
|
if (!workspaceRoot) {
|
|
throw new Error(
|
|
"agent_runtime_list_workspace_skill_bindings requires workspaceRoot",
|
|
);
|
|
}
|
|
const response =
|
|
await this.#appServerRequest<WorkspaceSkillBindingsListResponse>(
|
|
METHOD_WORKSPACE_SKILL_BINDINGS_LIST,
|
|
{
|
|
workspaceRoot,
|
|
...readStringParam(request, "caller", "caller"),
|
|
...readBooleanParam(request, "workbench", "workbench"),
|
|
...readBooleanParam(request, "browserAssist", "browserAssist"),
|
|
...readBooleanParam(request, "browser_assist", "browserAssist"),
|
|
},
|
|
);
|
|
return response.bindings;
|
|
}
|
|
|
|
async #listAgentAppInstalled(): Promise<unknown> {
|
|
return await this.#appServerRequest<AgentAppInstalledListResponse>(
|
|
METHOD_AGENT_APP_INSTALLED_LIST,
|
|
);
|
|
}
|
|
|
|
async #getUsageStats(args: HostArgs): Promise<Record<string, unknown>> {
|
|
const response = await this.#appServerRequest<UsageStatsReadResponse>(
|
|
METHOD_USAGE_STATS_READ,
|
|
readUsageStatsParams(args),
|
|
);
|
|
return toLegacyUsageStats(response.stats);
|
|
}
|
|
|
|
async #getModelUsageRanking(args: HostArgs): Promise<unknown[]> {
|
|
const response =
|
|
await this.#appServerRequest<UsageStatsModelRankingListResponse>(
|
|
METHOD_USAGE_STATS_MODEL_RANKING_LIST,
|
|
readUsageStatsParams(args),
|
|
);
|
|
return response.ranking.map(toLegacyModelUsage);
|
|
}
|
|
|
|
async #getDailyUsageTrends(args: HostArgs): Promise<unknown[]> {
|
|
const response =
|
|
await this.#appServerRequest<UsageStatsDailyTrendsListResponse>(
|
|
METHOD_USAGE_STATS_DAILY_TRENDS_LIST,
|
|
readUsageStatsParams(args),
|
|
);
|
|
return response.trends.map(toLegacyDailyUsage);
|
|
}
|
|
|
|
#getVoiceInputConfig(): Record<string, unknown> {
|
|
const instructions = this.#getVoiceInstructions();
|
|
return {
|
|
enabled: false,
|
|
shortcut: "CommandOrControl+Shift+V",
|
|
processor: {
|
|
polish_enabled: true,
|
|
polish_provider: "openai",
|
|
polish_model: "gpt-4.1-mini",
|
|
default_instruction_id: "default",
|
|
},
|
|
output: {
|
|
mode: "type",
|
|
type_delay_ms: 10,
|
|
},
|
|
instructions,
|
|
selected_device_id: undefined,
|
|
sound_enabled: true,
|
|
translate_instruction_id: "translate_en",
|
|
diagnostic: this.#diagnosticMeta("get_voice_input_config"),
|
|
};
|
|
}
|
|
|
|
#getVoiceShortcutRuntimeStatus(): Record<string, unknown> {
|
|
return {
|
|
shortcut_registered: false,
|
|
registered_shortcut: null,
|
|
fn_supported: process.platform === "darwin",
|
|
fn_registered: false,
|
|
fn_fallback_shortcut: "CommandOrControl+Shift+V",
|
|
fn_note:
|
|
"Electron current 语音快捷键运行时尚未接入;当前使用普通语音快捷键回退。",
|
|
diagnostic: this.#diagnosticMeta("get_voice_shortcut_runtime_status"),
|
|
};
|
|
}
|
|
|
|
#getAsrCredentials(): Array<Record<string, unknown>> {
|
|
return this.#emptyDiagnosticList("get_asr_credentials");
|
|
}
|
|
|
|
#getVoiceInstructions(): Array<Record<string, unknown>> {
|
|
return [
|
|
{
|
|
id: "default",
|
|
name: "默认润色",
|
|
prompt: "{{text}}",
|
|
is_preset: true,
|
|
},
|
|
{
|
|
id: "translate_en",
|
|
name: "翻译为英文",
|
|
prompt: "{{text}}",
|
|
is_preset: true,
|
|
},
|
|
{
|
|
id: "raw",
|
|
name: "原始输出",
|
|
prompt: "{{text}}",
|
|
is_preset: true,
|
|
},
|
|
];
|
|
}
|
|
|
|
#getVoiceModelInstallState(args: HostArgs): Record<string, unknown> {
|
|
const request = readRequest(args);
|
|
const modelId =
|
|
readString(request, "modelId") ??
|
|
readString(request, "model_id") ??
|
|
"sensevoice-small-int8-2024-07-17";
|
|
const installDir = path.join(this.#userDataDir, "models", "voice", modelId);
|
|
return {
|
|
model_id: modelId,
|
|
installed: false,
|
|
installing: false,
|
|
install_dir: installDir,
|
|
model_file: null,
|
|
tokens_file: null,
|
|
vad_file: null,
|
|
installed_bytes: 0,
|
|
last_verified_at: Math.floor(Date.now() / 1000),
|
|
missing_files: ["model.int8.onnx", "tokens.txt", "silero_vad.onnx"],
|
|
default_credential_id: null,
|
|
diagnostic: this.#diagnosticMeta("voice_models_get_install_state"),
|
|
};
|
|
}
|
|
|
|
async #getEnvironmentPreview(): Promise<Record<string, unknown>> {
|
|
const config = await this.#readConfig();
|
|
const serverConfig = toRecord(config.server);
|
|
const apiKey = readString(serverConfig, "api_key") ?? "";
|
|
const apiBase =
|
|
`http://${readString(serverConfig, "host") ?? "127.0.0.1"}:` +
|
|
`${readNumber(serverConfig, "port") ?? 8787}`;
|
|
const entries = [
|
|
{
|
|
key: "LIME_API_BASE",
|
|
value: apiBase,
|
|
maskedValue: apiBase,
|
|
source: "config",
|
|
sourceLabel: "Electron Desktop Host",
|
|
sensitive: false,
|
|
overriddenSources: [],
|
|
},
|
|
{
|
|
key: "LIME_API_KEY",
|
|
value: apiKey,
|
|
maskedValue: apiKey ? "********" : "",
|
|
source: "config",
|
|
sourceLabel: "Electron Desktop Host",
|
|
sensitive: true,
|
|
overriddenSources: [],
|
|
},
|
|
];
|
|
return {
|
|
shellImport: {
|
|
enabled: false,
|
|
status: "disabled",
|
|
message: "Electron current 暂未接入 shell 环境导入预览。",
|
|
importedCount: 0,
|
|
durationMs: null,
|
|
diagnostic: this.#diagnosticMeta("get_environment_preview"),
|
|
},
|
|
entries,
|
|
diagnostic: this.#diagnosticMeta("get_environment_preview"),
|
|
};
|
|
}
|
|
|
|
#getUnifiedMemoryStats(): Record<string, unknown> {
|
|
return {
|
|
total_entries: 0,
|
|
storage_used: 0,
|
|
memory_count: 0,
|
|
categories: [
|
|
{ category: "identity", count: 0 },
|
|
{ category: "context", count: 0 },
|
|
{ category: "preference", count: 0 },
|
|
{ category: "experience", count: 0 },
|
|
{ category: "activity", count: 0 },
|
|
],
|
|
diagnostic: this.#diagnosticMeta("unified_memory_stats"),
|
|
};
|
|
}
|
|
|
|
#getSiteAdapterCatalogStatus(): Record<string, unknown> {
|
|
return {
|
|
exists: false,
|
|
source_kind: "bundled",
|
|
registry_version: 1,
|
|
directory: path.join(this.#userDataDir, "site-adapters", "server-synced"),
|
|
adapter_count: 0,
|
|
diagnostic: this.#diagnosticMeta("site_get_adapter_catalog_status"),
|
|
};
|
|
}
|
|
|
|
#getSkillPackageFileAssociationStatus(): Record<string, unknown> {
|
|
return {
|
|
platform: process.platform,
|
|
extension: "skill",
|
|
extensions: ["skill", "skills"],
|
|
mimeType: "application/vnd.lime.skill+zip",
|
|
appIdentifier: app.getName(),
|
|
isDefault: false,
|
|
canSetDefault: true,
|
|
requiresUserConfirmation: true,
|
|
currentHandler: null,
|
|
settingsUrl: null,
|
|
detail:
|
|
"Electron Desktop Host 当前只能检查 .skill / .skills 文件关联状态;设置默认打开方式需要系统确认。",
|
|
diagnostic: this.#diagnosticMeta(
|
|
"get_skill_package_file_association_status",
|
|
),
|
|
};
|
|
}
|
|
|
|
#setSkillPackageFileAssociationDefault(): Record<string, unknown> {
|
|
const status = this.#getSkillPackageFileAssociationStatus();
|
|
return {
|
|
changed: false,
|
|
message:
|
|
"Electron Desktop Host 当前不能静默修改系统文件关联,请在系统设置中确认 .skill / .skills 默认打开方式。",
|
|
status,
|
|
diagnostic: this.#diagnosticMeta(
|
|
"set_skill_package_file_association_default",
|
|
),
|
|
};
|
|
}
|
|
|
|
#getBrowserConnectorSettings(): Record<string, unknown> {
|
|
const installRoot = path.join(this.#userDataDir, "browser-connectors");
|
|
return {
|
|
enabled: true,
|
|
install_root_dir: installRoot,
|
|
install_dir: path.join(installRoot, "Lime Browser Connector"),
|
|
browser_action_capabilities: [
|
|
{ key: "read_page", label: "读取页面", enabled: true },
|
|
{ key: "click", label: "点击", enabled: true },
|
|
{ key: "type", label: "输入", enabled: true },
|
|
{ key: "scroll_page", label: "滚动页面", enabled: true },
|
|
],
|
|
system_connectors: [],
|
|
diagnostic: this.#diagnosticMeta("get_browser_connector_settings_cmd"),
|
|
};
|
|
}
|
|
|
|
#getBrowserConnectorInstallStatus(): Record<string, unknown> {
|
|
const installRoot = path.join(this.#userDataDir, "browser-connectors");
|
|
return {
|
|
status: "not_installed",
|
|
install_root_dir: installRoot,
|
|
install_dir: path.join(installRoot, "Lime Browser Connector"),
|
|
bundled_name: "Lime Browser Connector",
|
|
bundled_version: app.getVersion(),
|
|
installed_name: null,
|
|
installed_version: null,
|
|
message: "尚未导出浏览器连接器",
|
|
diagnostic: this.#diagnosticMeta(
|
|
"get_browser_connector_install_status_cmd",
|
|
),
|
|
};
|
|
}
|
|
|
|
#getChromeBridgeEndpointInfo(): Record<string, unknown> {
|
|
return {
|
|
server_running: false,
|
|
host: "127.0.0.1",
|
|
port: 8999,
|
|
observer_ws_url: "ws://127.0.0.1:8999/lime-chrome-observer",
|
|
control_ws_url: "ws://127.0.0.1:8999/lime-chrome-control",
|
|
bridge_key: "proxy_cast",
|
|
diagnostic: this.#diagnosticMeta("get_chrome_bridge_endpoint_info"),
|
|
};
|
|
}
|
|
|
|
#getChromeBridgeStatus(): Record<string, unknown> {
|
|
return {
|
|
observer_count: 0,
|
|
control_count: 0,
|
|
pending_command_count: 0,
|
|
observers: [],
|
|
controls: [],
|
|
pending_commands: [],
|
|
diagnostic: this.#diagnosticMeta("get_chrome_bridge_status"),
|
|
};
|
|
}
|
|
|
|
#getBrowserBackendPolicy(): Record<string, unknown> {
|
|
return {
|
|
priority: ["lime_extension_bridge", "cdp_direct"],
|
|
auto_fallback: false,
|
|
diagnostic: this.#diagnosticMeta("get_browser_backend_policy"),
|
|
};
|
|
}
|
|
|
|
#getBrowserBackendsStatus(): Record<string, unknown> {
|
|
const policy = this.#getBrowserBackendPolicy();
|
|
return {
|
|
policy,
|
|
bridge_observer_count: 0,
|
|
bridge_control_count: 0,
|
|
running_profile_count: 0,
|
|
cdp_alive_profile_count: 0,
|
|
aster_native_host_supported: false,
|
|
aster_native_host_configured: false,
|
|
backends: [
|
|
{
|
|
backend: "lime_extension_bridge",
|
|
available: false,
|
|
reason: "浏览器连接器尚未连接",
|
|
capabilities: [],
|
|
},
|
|
{
|
|
backend: "cdp_direct",
|
|
available: false,
|
|
reason: "当前没有运行中的 Chrome Profile 会话",
|
|
capabilities: [],
|
|
},
|
|
],
|
|
diagnostic: this.#diagnosticMeta("get_browser_backends_status"),
|
|
};
|
|
}
|
|
|
|
async #readProjectMemory(args: HostArgs): Promise<unknown> {
|
|
const request = readRequest(args);
|
|
const projectId =
|
|
readString(request, "projectId") ??
|
|
readString(request, "project_id") ??
|
|
readString(args, "projectId") ??
|
|
readString(args, "project_id");
|
|
if (!projectId) {
|
|
throw new Error("project_memory_get requires projectId");
|
|
}
|
|
const response = await this.#appServerRequest<ProjectMemoryReadResponse>(
|
|
METHOD_PROJECT_MEMORY_READ,
|
|
{ projectId },
|
|
);
|
|
return response.memory;
|
|
}
|
|
|
|
async #readConfig(): Promise<Record<string, unknown>> {
|
|
const fallback = buildDefaultConfig();
|
|
try {
|
|
const text = await readFile(this.#configPath(), "utf8");
|
|
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
return { ...fallback, ...parsed };
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
async #saveConfig(args: HostArgs): Promise<{ success: true }> {
|
|
const config = readRecord(args, "config") ?? args ?? {};
|
|
await mkdir(this.#userDataDir, { recursive: true });
|
|
await writeFile(
|
|
this.#configPath(),
|
|
JSON.stringify(config, null, 2),
|
|
"utf8",
|
|
);
|
|
return { success: true };
|
|
}
|
|
|
|
#reportFrontendDebugLog(args: HostArgs): void {
|
|
const report = readRecord(args, "report");
|
|
const level = readString(report, "level") ?? "info";
|
|
const message = readString(report, "message") ?? "";
|
|
console.log(`[electron-renderer:${level}] ${message}`);
|
|
}
|
|
|
|
#reportFrontendCrash(args: HostArgs): void {
|
|
const report = readRecord(args, "report") ?? {};
|
|
const message = readString(report, "message") ?? "renderer crash report";
|
|
console.error("[electron-renderer:crash]", message, report);
|
|
}
|
|
|
|
#configPath(): string {
|
|
return path.join(this.#userDataDir, CONFIG_FILE);
|
|
}
|
|
}
|
|
|
|
function readRecord(
|
|
value: unknown,
|
|
key: string,
|
|
): Record<string, unknown> | null {
|
|
const record = toRecord(value);
|
|
if (!record) {
|
|
return null;
|
|
}
|
|
const next = record[key];
|
|
return next && typeof next === "object" && !Array.isArray(next)
|
|
? (next as Record<string, unknown>)
|
|
: null;
|
|
}
|
|
|
|
function readRequest(value: unknown): Record<string, unknown> {
|
|
return readRecord(value, "request") ?? toRecord(value) ?? {};
|
|
}
|
|
|
|
function readUsageStatsParams(args: HostArgs): UsageStatsRangeParams {
|
|
const request = readRequest(args);
|
|
const timeRange =
|
|
readString(request, "timeRange") ??
|
|
readString(request, "time_range") ??
|
|
readString(args, "timeRange") ??
|
|
readString(args, "time_range") ??
|
|
"month";
|
|
return { timeRange };
|
|
}
|
|
|
|
function toLegacyUsageStats(
|
|
stats: UsageStatsSummaryWire,
|
|
): Record<string, unknown> {
|
|
return {
|
|
total_conversations: stats.totalConversations,
|
|
total_messages: stats.totalMessages,
|
|
total_tokens: stats.totalTokens,
|
|
total_time_minutes: stats.totalTimeMinutes,
|
|
monthly_conversations: stats.monthlyConversations,
|
|
monthly_messages: stats.monthlyMessages,
|
|
monthly_tokens: stats.monthlyTokens,
|
|
today_conversations: stats.todayConversations,
|
|
today_messages: stats.todayMessages,
|
|
today_tokens: stats.todayTokens,
|
|
};
|
|
}
|
|
|
|
function toLegacyModelUsage(
|
|
item: UsageStatsModelUsageWire,
|
|
): Record<string, unknown> {
|
|
return {
|
|
model: item.model,
|
|
conversations: item.conversations,
|
|
tokens: item.tokens,
|
|
percentage: item.percentage,
|
|
};
|
|
}
|
|
|
|
function toLegacyDailyUsage(
|
|
item: UsageStatsDailyUsageWire,
|
|
): Record<string, unknown> {
|
|
return {
|
|
date: item.date,
|
|
conversations: item.conversations,
|
|
tokens: item.tokens,
|
|
};
|
|
}
|
|
|
|
function readArray(value: unknown, key: string): unknown[] | undefined {
|
|
const record = toRecord(value);
|
|
const next = record?.[key];
|
|
return Array.isArray(next) ? next : undefined;
|
|
}
|
|
|
|
function readString(value: unknown, key: string): string | null {
|
|
if (!value || typeof value !== "object") {
|
|
return null;
|
|
}
|
|
const next = (value as Record<string, unknown>)[key];
|
|
return typeof next === "string" && next.trim() ? next.trim() : null;
|
|
}
|
|
|
|
function readRequiredString(value: unknown, key: string): string {
|
|
const next = readString(value, key);
|
|
if (!next) {
|
|
throw new Error(`Missing required string field: ${key}`);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function readElectronPath(name: ElectronKnownPathName): string | null {
|
|
try {
|
|
const next = app.getPath(name);
|
|
return next.trim() ? next : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function appendFileManagerLocation(
|
|
locations: FileManagerLocation[],
|
|
seenPaths: Set<string>,
|
|
location: FileManagerLocationCandidate,
|
|
): Promise<void> {
|
|
const normalizedPath = location.path?.trim() ?? "";
|
|
if (!normalizedPath || seenPaths.has(normalizedPath)) {
|
|
return;
|
|
}
|
|
try {
|
|
const metadata = await stat(normalizedPath);
|
|
if (!metadata.isDirectory()) {
|
|
return;
|
|
}
|
|
} catch {
|
|
return;
|
|
}
|
|
seenPaths.add(normalizedPath);
|
|
locations.push({ ...location, path: normalizedPath });
|
|
}
|
|
|
|
function readBoolean(value: unknown, key: string): boolean | null {
|
|
const record = toRecord(value);
|
|
if (!record || typeof record[key] !== "boolean") {
|
|
return null;
|
|
}
|
|
return record[key];
|
|
}
|
|
|
|
function readNumber(value: unknown, key: string): number | null {
|
|
const record = toRecord(value);
|
|
const next = record?.[key];
|
|
return typeof next === "number" && Number.isFinite(next) ? next : null;
|
|
}
|
|
|
|
function readBooleanParam(
|
|
value: unknown,
|
|
inputKey: string,
|
|
outputKey: string,
|
|
): AppServerParams {
|
|
const record = toRecord(value);
|
|
if (!record || typeof record[inputKey] !== "boolean") {
|
|
return {};
|
|
}
|
|
return { [outputKey]: record[inputKey] };
|
|
}
|
|
|
|
function readStringParam(
|
|
value: unknown,
|
|
inputKey: string,
|
|
outputKey: string,
|
|
): AppServerParams {
|
|
const next = readString(value, inputKey);
|
|
return next ? { [outputKey]: next } : {};
|
|
}
|
|
|
|
function readNumberParam(
|
|
value: unknown,
|
|
inputKey: string,
|
|
outputKey: string,
|
|
): AppServerParams {
|
|
const record = toRecord(value);
|
|
if (!record) {
|
|
return {};
|
|
}
|
|
const next = record[inputKey];
|
|
if (typeof next !== "number" || !Number.isFinite(next) || next < 0) {
|
|
return {};
|
|
}
|
|
return { [outputKey]: Math.trunc(next) };
|
|
}
|
|
|
|
function readValueParam(
|
|
value: unknown,
|
|
inputKey: string,
|
|
outputKey: string,
|
|
): AppServerParams {
|
|
const record = toRecord(value);
|
|
if (!record || record[inputKey] === undefined) {
|
|
return {};
|
|
}
|
|
return { [outputKey]: record[inputKey] };
|
|
}
|
|
|
|
function buildAgentRuntimeAsterChatRequest(params: {
|
|
request: Record<string, unknown>;
|
|
sessionId: string;
|
|
message: string;
|
|
turnConfig: Record<string, unknown> | null;
|
|
providerPreference?: string;
|
|
modelPreference?: string;
|
|
}): Record<string, unknown> {
|
|
const { request, sessionId, message, turnConfig } = params;
|
|
const eventName =
|
|
readString(request, "event_name") ??
|
|
readString(request, "eventName") ??
|
|
`agentSession/event/${sessionId}`;
|
|
return {
|
|
message,
|
|
session_id: sessionId,
|
|
event_name: eventName,
|
|
images: readArray(request, "images") ?? null,
|
|
provider_config:
|
|
turnConfig?.provider_config ?? turnConfig?.providerConfig ?? null,
|
|
provider_preference: params.providerPreference,
|
|
model_preference: params.modelPreference,
|
|
reasoning_effort:
|
|
turnConfig?.reasoning_effort ?? turnConfig?.reasoningEffort ?? null,
|
|
thinking_enabled:
|
|
turnConfig?.thinking_enabled ?? turnConfig?.thinkingEnabled ?? null,
|
|
approval_policy:
|
|
turnConfig?.approval_policy ?? turnConfig?.approvalPolicy ?? null,
|
|
sandbox_policy:
|
|
turnConfig?.sandbox_policy ?? turnConfig?.sandboxPolicy ?? null,
|
|
workspace_id:
|
|
readString(request, "workspace_id") ??
|
|
readString(request, "workspaceId") ??
|
|
"",
|
|
web_search: turnConfig?.web_search ?? turnConfig?.webSearch ?? null,
|
|
search_mode: turnConfig?.search_mode ?? turnConfig?.searchMode ?? null,
|
|
execution_strategy:
|
|
turnConfig?.execution_strategy ??
|
|
turnConfig?.executionStrategy ??
|
|
null,
|
|
auto_continue:
|
|
turnConfig?.auto_continue ?? turnConfig?.autoContinue ?? null,
|
|
system_prompt:
|
|
turnConfig?.system_prompt ?? turnConfig?.systemPrompt ?? null,
|
|
metadata: turnConfig?.metadata ?? null,
|
|
turn_id: readString(request, "turn_id") ?? readString(request, "turnId"),
|
|
queue_if_busy:
|
|
readBoolean(request, "queue_if_busy") ??
|
|
readBoolean(request, "queueIfBusy") ??
|
|
false,
|
|
queued_turn_id:
|
|
readString(request, "queued_turn_id") ??
|
|
readString(request, "queuedTurnId") ??
|
|
null,
|
|
turn_config: turnConfig,
|
|
};
|
|
}
|
|
|
|
function buildAgentAppRuntimeTaskMessage(
|
|
request: Record<string, unknown>,
|
|
): string {
|
|
const prompt =
|
|
readString(request, "prompt") ??
|
|
readString(request, "title") ??
|
|
readRequiredString(request, "taskKind");
|
|
return [
|
|
"【Agent App Runtime Task】",
|
|
`App: ${readRequiredString(request, "appId")}`,
|
|
`Entry: ${readString(request, "entryKey") ?? "default"}`,
|
|
`TaskKind: ${readRequiredString(request, "taskKind")}`,
|
|
"",
|
|
"Business Prompt:",
|
|
prompt,
|
|
"",
|
|
"Runtime Boundary:",
|
|
"- 请在 Lime AgentRuntime 主链中完成这个 App 业务任务。",
|
|
"- 不要要求用户跳回通用 Chat;如需补充上下文,请通过可审计的 action / request 机制表达。",
|
|
"",
|
|
"Input JSON:",
|
|
stringifyJsonField(request, "input"),
|
|
"",
|
|
"Expected Output JSON:",
|
|
stringifyJsonField(request, "expectedOutput"),
|
|
].join("\n");
|
|
}
|
|
|
|
function stringifyJsonField(
|
|
record: Record<string, unknown>,
|
|
key: string,
|
|
): string {
|
|
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
return "{}";
|
|
}
|
|
try {
|
|
return JSON.stringify(record[key], null, 2) ?? "{}";
|
|
} catch {
|
|
return String(record[key]);
|
|
}
|
|
}
|
|
|
|
function sessionStatusToAgentAppTaskStatus(status: string): string {
|
|
switch (status) {
|
|
case "completed":
|
|
return "completed";
|
|
case "failed":
|
|
return "failed";
|
|
case "canceled":
|
|
case "cancelled":
|
|
return "cancelled";
|
|
case "waitingAction":
|
|
return "blocked";
|
|
case "idle":
|
|
return "idle";
|
|
case "running":
|
|
return "running";
|
|
default:
|
|
return "thread_read_available";
|
|
}
|
|
}
|
|
|
|
function activeAgentSessionTurnId(
|
|
response: AgentSessionReadResponse,
|
|
): string | null {
|
|
for (let index = response.turns.length - 1; index >= 0; index -= 1) {
|
|
const turn = response.turns[index];
|
|
if (
|
|
turn &&
|
|
(turn.status === "accepted" ||
|
|
turn.status === "queued" ||
|
|
turn.status === "running" ||
|
|
turn.status === "waitingAction")
|
|
) {
|
|
return turn.turnId;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function normalizeAgentSessionActionScope(
|
|
value: unknown,
|
|
): Record<string, string> | undefined {
|
|
const record = toRecord(value);
|
|
if (!record) {
|
|
return undefined;
|
|
}
|
|
const scope = {
|
|
...readStringParam(record, "sessionId", "sessionId"),
|
|
...readStringParam(record, "session_id", "sessionId"),
|
|
...readStringParam(record, "threadId", "threadId"),
|
|
...readStringParam(record, "thread_id", "threadId"),
|
|
...readStringParam(record, "turnId", "turnId"),
|
|
...readStringParam(record, "turn_id", "turnId"),
|
|
};
|
|
return Object.keys(scope).length > 0
|
|
? (scope as Record<string, string>)
|
|
: undefined;
|
|
}
|
|
|
|
function isAppServerSessionAlreadyExistsError(error: unknown): boolean {
|
|
return (
|
|
error instanceof AppServerRequestError &&
|
|
error.response.error.code === ERROR_CODES.sessionAlreadyExists
|
|
);
|
|
}
|
|
|
|
function sessionOverviewToLegacy(
|
|
session: AgentSessionOverview,
|
|
): Record<string, unknown> {
|
|
return {
|
|
id: session.sessionId,
|
|
thread_id: session.threadId ?? session.sessionId,
|
|
name: session.title ?? undefined,
|
|
created_at: timestampMillis(session.createdAt),
|
|
updated_at: timestampMillis(session.updatedAt),
|
|
archived_at: session.archivedAt
|
|
? timestampMillis(session.archivedAt)
|
|
: null,
|
|
model: session.model,
|
|
workspace_id: session.workspaceId,
|
|
working_dir: session.workingDir,
|
|
execution_strategy: session.executionStrategy,
|
|
messages_count: session.messagesCount,
|
|
};
|
|
}
|
|
|
|
function sessionReadToLegacy(
|
|
response: AgentSessionReadResponse,
|
|
): Record<string, unknown> {
|
|
const threadRead = threadReadFromAgentSessionRead(response);
|
|
return {
|
|
id: response.session.sessionId,
|
|
thread_id: response.session.threadId,
|
|
name: response.session.sessionId,
|
|
created_at: timestampMillis(response.session.createdAt),
|
|
updated_at: timestampMillis(response.session.updatedAt),
|
|
model: undefined,
|
|
workspace_id: response.session.workspaceId,
|
|
messages: [],
|
|
turns: response.turns,
|
|
items: [],
|
|
queued_turns: [],
|
|
thread_read: threadRead,
|
|
todo_items: [],
|
|
child_subagent_sessions: [],
|
|
};
|
|
}
|
|
|
|
function threadReadFromAgentSessionRead(
|
|
response: AgentSessionReadResponse,
|
|
): Record<string, unknown> | null {
|
|
const detail = toRecord(response.detail);
|
|
const threadRead =
|
|
toRecord(detail?.thread_read) ?? toRecord(detail?.threadRead);
|
|
return threadRead;
|
|
}
|
|
|
|
function timestampMillis(value: string | undefined): number {
|
|
if (!value) {
|
|
return Date.now();
|
|
}
|
|
const parsed = Date.parse(value);
|
|
if (Number.isFinite(parsed)) {
|
|
return parsed;
|
|
}
|
|
const numeric = Number(value);
|
|
if (Number.isFinite(numeric)) {
|
|
return Math.abs(numeric) < 10_000_000_000 ? numeric * 1000 : numeric;
|
|
}
|
|
return Date.now();
|
|
}
|
|
|
|
function normalizeAgentAttachments(
|
|
images?: unknown[],
|
|
): AgentAttachment[] | undefined {
|
|
if (!images?.length) {
|
|
return undefined;
|
|
}
|
|
|
|
const attachments = images
|
|
.map((image, index): AgentAttachment | null => {
|
|
const record = toRecord(image);
|
|
if (!record) {
|
|
return null;
|
|
}
|
|
const uri = readString(record, "data") ?? readString(record, "uri");
|
|
if (!uri) {
|
|
return null;
|
|
}
|
|
return {
|
|
kind: "image",
|
|
uri,
|
|
metadata: {
|
|
mediaType:
|
|
readString(record, "media_type") ?? readString(record, "mediaType"),
|
|
index,
|
|
},
|
|
};
|
|
})
|
|
.filter((attachment): attachment is AgentAttachment => attachment !== null);
|
|
|
|
return attachments.length > 0 ? attachments : undefined;
|
|
}
|
|
|
|
function evidenceExportToLegacy(
|
|
response: EvidenceExportResponse,
|
|
): Record<string, unknown> {
|
|
const pack = response.evidencePack;
|
|
const latestTurn =
|
|
response.turns.length > 0
|
|
? response.turns[response.turns.length - 1]
|
|
: undefined;
|
|
const observabilitySummary =
|
|
pack?.observabilitySummary ??
|
|
observabilitySummaryFromEvidenceEvents(response.events);
|
|
const workspaceRoot =
|
|
readString(
|
|
toRecord(response.session.businessObjectRef?.metadata),
|
|
"workspaceRoot",
|
|
) ??
|
|
readString(
|
|
toRecord(response.session.businessObjectRef?.metadata),
|
|
"workspace_root",
|
|
) ??
|
|
"";
|
|
|
|
return {
|
|
sessionId: response.session.sessionId,
|
|
threadId: response.session.threadId,
|
|
workspaceId: response.session.workspaceId,
|
|
workspaceRoot,
|
|
packRelativeRoot: pack?.packRelativeRoot ?? "",
|
|
packAbsoluteRoot: pack?.packAbsoluteRoot ?? "",
|
|
exportedAt: pack?.exportedAt ?? response.exportedAt,
|
|
threadStatus: pack?.threadStatus ?? response.session.status,
|
|
latestTurnStatus: pack?.latestTurnStatus ?? latestTurn?.status,
|
|
turnCount: pack?.turnCount ?? response.turns.length,
|
|
itemCount: pack?.itemCount ?? response.events.length,
|
|
pendingRequestCount: pack?.pendingRequestCount ?? 0,
|
|
queuedTurnCount:
|
|
pack?.queuedTurnCount ??
|
|
response.turns.filter((turn) => turn.status === "queued").length,
|
|
recentArtifactCount: pack?.recentArtifactCount ?? response.artifacts.length,
|
|
knownGaps: pack?.knownGaps ?? [],
|
|
observabilitySummary,
|
|
completionAuditSummary: pack?.completionAuditSummary,
|
|
artifacts:
|
|
pack?.artifacts ??
|
|
response.artifacts.map(artifactSummaryToEvidenceArtifact),
|
|
};
|
|
}
|
|
|
|
function observabilitySummaryFromEvidenceEvents(
|
|
events: EvidenceExportResponse["events"],
|
|
): Record<string, unknown> | undefined {
|
|
const toolCalls = toolCallsFromEvidenceEvents(events);
|
|
if (toolCalls.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
schemaVersion: "runtime-evidence-observability.v1",
|
|
toolCalls,
|
|
};
|
|
}
|
|
|
|
function toolCallsFromEvidenceEvents(
|
|
events: EvidenceExportResponse["events"],
|
|
): Array<Record<string, unknown>> {
|
|
const calls: Array<Record<string, unknown>> = [];
|
|
for (const event of events) {
|
|
for (const next of toolCallProjectionsFromEvidenceEvent(event)) {
|
|
mergeToolCallProjection(calls, next);
|
|
}
|
|
}
|
|
return calls;
|
|
}
|
|
|
|
function toolCallProjectionsFromEvidenceEvent(
|
|
event: EvidenceExportResponse["events"][number],
|
|
): Array<Record<string, unknown>> {
|
|
const type = String(event.type || "");
|
|
const payload = toRecord(event.payload) ?? {};
|
|
const runtimeEvent = toRecord(payload.runtimeEvent);
|
|
const item = toRecord(payload.item) ?? toRecord(runtimeEvent?.item);
|
|
const result = toRecord(payload.result) ?? toRecord(runtimeEvent?.result);
|
|
const projections: Array<Record<string, unknown>> = [];
|
|
const status = toolCallStatusFromEvidenceEvent(type, item);
|
|
|
|
if (status) {
|
|
const success =
|
|
readBoolean(payload, "success") ??
|
|
readBoolean(runtimeEvent, "success") ??
|
|
readBoolean(item, "success") ??
|
|
readBoolean(result, "success") ??
|
|
(status === "failed" ? false : undefined);
|
|
projections.push(
|
|
omitNullishRecord({
|
|
id:
|
|
readToolCallId(payload) ??
|
|
readToolCallId(runtimeEvent) ??
|
|
readToolCallId(item) ??
|
|
readToolCallId(result),
|
|
toolName:
|
|
readToolName(payload) ??
|
|
readToolName(runtimeEvent) ??
|
|
readToolName(item) ??
|
|
readToolName(result),
|
|
status,
|
|
success,
|
|
output:
|
|
readToolOutput(item) ??
|
|
readToolOutput(result) ??
|
|
readToolOutput(payload) ??
|
|
readToolOutput(runtimeEvent),
|
|
error: payload.error ?? runtimeEvent?.error ?? item?.error ?? result?.error,
|
|
eventId: event.eventId,
|
|
turnId: event.turnId,
|
|
timestamp: event.timestamp,
|
|
}),
|
|
);
|
|
}
|
|
|
|
for (const content of toolMessageContentRecords(payload, runtimeEvent)) {
|
|
const contentType = readString(content, "type");
|
|
if (contentType !== "tool_request" && contentType !== "tool_response") {
|
|
continue;
|
|
}
|
|
projections.push(
|
|
omitNullishRecord({
|
|
id: readToolCallId(content),
|
|
toolName: readToolName(content),
|
|
status: contentType === "tool_response" ? "completed" : "running",
|
|
success: readBoolean(content, "success"),
|
|
output: readToolOutput(content),
|
|
error: content.error,
|
|
eventId: event.eventId,
|
|
turnId: event.turnId,
|
|
timestamp: event.timestamp,
|
|
}),
|
|
);
|
|
}
|
|
|
|
return projections;
|
|
}
|
|
|
|
function toolCallStatusFromEvidenceEvent(
|
|
type: string,
|
|
item: Record<string, unknown> | null,
|
|
): string | null {
|
|
if (type === "tool.started") {
|
|
return "running";
|
|
}
|
|
if (type === "tool.result") {
|
|
return "completed";
|
|
}
|
|
if (type === "tool.failed") {
|
|
return "failed";
|
|
}
|
|
if (item && readString(item, "type") === "tool_call") {
|
|
if (type === "item.started") {
|
|
return "running";
|
|
}
|
|
if (type === "item.completed") {
|
|
return readString(item, "status") ?? "completed";
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function mergeToolCallProjection(
|
|
calls: Array<Record<string, unknown>>,
|
|
next: Record<string, unknown>,
|
|
): void {
|
|
const id = readString(next, "id");
|
|
const toolName = readString(next, "toolName");
|
|
const existing = calls.find((call) => {
|
|
if (id && readString(call, "id") === id) {
|
|
return true;
|
|
}
|
|
return (
|
|
!id &&
|
|
Boolean(toolName) &&
|
|
readString(call, "toolName") === toolName &&
|
|
readString(call, "turnId") === readString(next, "turnId")
|
|
);
|
|
});
|
|
const normalizedNext = omitNullishRecord({
|
|
id: id ?? existing?.id ?? readString(next, "eventId"),
|
|
...next,
|
|
});
|
|
if (existing) {
|
|
Object.assign(existing, normalizedNext);
|
|
} else {
|
|
calls.push(normalizedNext);
|
|
}
|
|
}
|
|
|
|
function readToolCallId(record: Record<string, unknown> | null): string | null {
|
|
return (
|
|
readString(record, "id") ??
|
|
readString(record, "tool_call_id") ??
|
|
readString(record, "toolCallId") ??
|
|
readString(record, "toolId") ??
|
|
readString(record, "tool_id")
|
|
);
|
|
}
|
|
|
|
function readToolName(record: Record<string, unknown> | null): string | null {
|
|
return (
|
|
readString(record, "tool_name") ??
|
|
readString(record, "toolName") ??
|
|
readString(record, "name")
|
|
);
|
|
}
|
|
|
|
function readToolOutput(record: Record<string, unknown> | null): unknown {
|
|
if (!record) {
|
|
return undefined;
|
|
}
|
|
return (
|
|
readString(record, "output") ??
|
|
readString(record, "output_preview") ??
|
|
readString(record, "outputPreview") ??
|
|
readString(record, "text") ??
|
|
readString(record, "content") ??
|
|
readString(record, "result") ??
|
|
record.output ??
|
|
record.output_preview ??
|
|
record.outputPreview ??
|
|
record.text ??
|
|
record.content ??
|
|
record.result
|
|
);
|
|
}
|
|
|
|
function toolMessageContentRecords(
|
|
payload: Record<string, unknown>,
|
|
runtimeEvent: Record<string, unknown> | null,
|
|
): Array<Record<string, unknown>> {
|
|
const content =
|
|
readArray(readRecord(payload, "message"), "content") ??
|
|
readArray(readRecord(runtimeEvent, "message"), "content") ??
|
|
[];
|
|
return content
|
|
.map((entry) => toRecord(entry))
|
|
.filter((entry): entry is Record<string, unknown> => entry !== null);
|
|
}
|
|
|
|
function omitNullishRecord(
|
|
record: Record<string, unknown>,
|
|
): Record<string, unknown> {
|
|
return Object.fromEntries(
|
|
Object.entries(record).filter(
|
|
([, value]) => value !== undefined && value !== null,
|
|
),
|
|
);
|
|
}
|
|
|
|
function artifactSummaryToEvidenceArtifact(
|
|
artifact: ArtifactSummary,
|
|
): Record<string, unknown> {
|
|
const relativePath = artifact.path ?? artifact.artifactRef;
|
|
return {
|
|
kind: artifact.kind ?? "artifacts",
|
|
title: artifact.title ?? artifact.artifactId ?? artifact.artifactRef,
|
|
relativePath,
|
|
absolutePath: "",
|
|
bytes:
|
|
typeof artifact.content === "string"
|
|
? Buffer.byteLength(artifact.content)
|
|
: 0,
|
|
};
|
|
}
|
|
|
|
function findProvider(
|
|
providers: unknown[],
|
|
providerId: string,
|
|
): Record<string, unknown> | null {
|
|
const normalizedProviderId = normalizeProviderIdentity(providerId);
|
|
if (!normalizedProviderId) {
|
|
return null;
|
|
}
|
|
return (
|
|
(providers.find((provider) => {
|
|
const record = toRecord(provider);
|
|
const id = normalizeProviderIdentity(readString(record, "id"));
|
|
const name = normalizeProviderIdentity(readString(record, "name"));
|
|
return (
|
|
record && (id === normalizedProviderId || name === normalizedProviderId)
|
|
);
|
|
}) as Record<string, unknown> | undefined) ?? null
|
|
);
|
|
}
|
|
|
|
function normalizeProviderIdentity(value: string | null | undefined): string {
|
|
return (value ?? "").trim().toLowerCase();
|
|
}
|
|
|
|
function resolveCurrentDefaultProvider(
|
|
value: unknown,
|
|
providers: unknown[],
|
|
): string {
|
|
if (typeof value !== "string") {
|
|
return readString(providers.find(isConfiguredProvider), "id") ?? "";
|
|
}
|
|
const provider = value.trim();
|
|
const configuredProvider = findProvider(providers, provider);
|
|
if (configuredProvider && isConfiguredProvider(configuredProvider)) {
|
|
return readString(configuredProvider, "id") ?? provider;
|
|
}
|
|
return readString(providers.find(isConfiguredProvider), "id") ?? "";
|
|
}
|
|
|
|
function isConfiguredProvider(
|
|
provider: unknown,
|
|
): provider is Record<string, unknown> {
|
|
const record = toRecord(provider);
|
|
if (!record) {
|
|
return false;
|
|
}
|
|
const enabled = record.enabled !== false;
|
|
const apiKeyCount = record.api_key_count;
|
|
return enabled && typeof apiKeyCount === "number" && apiKeyCount > 0;
|
|
}
|
|
|
|
function skillToLocalSkill(skill: unknown): Record<string, unknown> {
|
|
const record = toRecord(skill) ?? {};
|
|
const directory =
|
|
readString(record, "directory") ??
|
|
readString(record, "name") ??
|
|
readString(record, "skill_name") ??
|
|
"app-server-skill";
|
|
const name =
|
|
readString(record, "display_name") ??
|
|
readString(record, "displayName") ??
|
|
readString(record, "name") ??
|
|
readString(record, "skill_name") ??
|
|
directory;
|
|
return {
|
|
key: directory,
|
|
name,
|
|
description: readString(record, "description") ?? "",
|
|
directory,
|
|
installed: true,
|
|
sourceKind: "builtin",
|
|
catalogSource: "project",
|
|
metadata: {},
|
|
allowedTools: [],
|
|
resourceSummary: {
|
|
hasScripts: false,
|
|
hasReferences: false,
|
|
hasAssets: false,
|
|
},
|
|
standardCompliance: {
|
|
isStandard: true,
|
|
validationErrors: [],
|
|
deprecatedFields: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function toRecord(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: null;
|
|
}
|
|
|
|
function normalizeExperimentalConfig(value: unknown): Record<string, unknown> {
|
|
const record = toRecord(value) ?? {};
|
|
const webmcp = toRecord(record.webmcp);
|
|
return {
|
|
...record,
|
|
webmcp: {
|
|
...webmcp,
|
|
enabled: readBoolean(webmcp, "enabled") ?? false,
|
|
},
|
|
};
|
|
}
|
|
|
|
function normalizeExternalUrl(input: string): string {
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(input.trim());
|
|
} catch (error) {
|
|
throw new Error(
|
|
`外部链接格式无效: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
throw new Error("外部链接只支持 http/https 地址");
|
|
}
|
|
return parsed.toString();
|
|
}
|
|
|
|
function normalizeCallbackBridgeValue(value: string | null): string | null {
|
|
const normalized = value?.trim() ?? "";
|
|
return normalized ? normalized : null;
|
|
}
|
|
|
|
function buildOemCloudOAuthCallbackPayload(
|
|
requestUrl: URL,
|
|
): Record<string, string | null> {
|
|
const params = requestUrl.searchParams;
|
|
return {
|
|
sourcePath: requestUrl.pathname,
|
|
tenantId: normalizeCallbackBridgeValue(
|
|
params.get("tenantId") ?? params.get("tenant_id"),
|
|
),
|
|
token: normalizeCallbackBridgeValue(params.get("token")),
|
|
next: normalizeCallbackBridgeValue(params.get("next")),
|
|
error: normalizeCallbackBridgeValue(params.get("error")),
|
|
deviceCode: normalizeCallbackBridgeValue(
|
|
params.get("deviceCode") ?? params.get("device_code"),
|
|
),
|
|
status: normalizeCallbackBridgeValue(params.get("status")),
|
|
};
|
|
}
|
|
|
|
function shouldEmitOemCloudOAuthCallback(
|
|
payload: Record<string, string | null>,
|
|
): boolean {
|
|
return Boolean(
|
|
payload.tenantId ||
|
|
payload.token ||
|
|
payload.error ||
|
|
payload.deviceCode ||
|
|
payload.status,
|
|
);
|
|
}
|
|
|
|
function capabilitiesToToolInventory(
|
|
capabilities: CapabilityDescriptor[],
|
|
caller: string,
|
|
surface: { workbench: boolean; browser_assist: boolean },
|
|
): Record<string, unknown> {
|
|
const runtimeTools = capabilities.flatMap((capability) =>
|
|
capabilityRuntimeToolNames(capability).map((name) => ({
|
|
name,
|
|
description: capability.description ?? capability.title,
|
|
source_kind: "current_surface",
|
|
source_label: capability.id,
|
|
status: "available",
|
|
deferred_loading: false,
|
|
always_visible: true,
|
|
allowed_callers: [caller],
|
|
tags: [capability.id],
|
|
input_examples_count: 0,
|
|
caller_allowed: true,
|
|
visible_in_context: true,
|
|
})),
|
|
);
|
|
const defaultAllowedTools = runtimeTools.map((entry) => entry.name).sort();
|
|
|
|
return {
|
|
request: {
|
|
caller,
|
|
surface,
|
|
},
|
|
agent_initialized: true,
|
|
warnings: [
|
|
"当前工具库存来自 App Server capability/list;细粒度工具权限 catalog 尚未迁入 App Server protocol。",
|
|
],
|
|
mcp_servers: [],
|
|
default_allowed_tools: defaultAllowedTools,
|
|
counts: {
|
|
catalog_total: 0,
|
|
catalog_current_total: 0,
|
|
catalog_compat_total: 0,
|
|
catalog_deprecated_total: 0,
|
|
default_allowed_total: defaultAllowedTools.length,
|
|
runtime_total: runtimeTools.length,
|
|
runtime_visible_total: runtimeTools.length,
|
|
registry_total: 0,
|
|
registry_visible_total: 0,
|
|
registry_catalog_unmapped_total: 0,
|
|
extension_surface_total: 0,
|
|
extension_mcp_bridge_total: 0,
|
|
extension_runtime_total: 0,
|
|
extension_tool_total: 0,
|
|
extension_tool_visible_total: 0,
|
|
mcp_server_total: 0,
|
|
mcp_tool_total: 0,
|
|
mcp_tool_visible_total: 0,
|
|
},
|
|
catalog_tools: [],
|
|
registry_tools: [],
|
|
runtime_tools: runtimeTools,
|
|
extension_surfaces: [],
|
|
extension_tools: [],
|
|
mcp_tools: [],
|
|
};
|
|
}
|
|
|
|
function capabilityRuntimeToolNames(capability: CapabilityDescriptor): string[] {
|
|
const toolName = capabilityToolName(capability);
|
|
if (toolName) {
|
|
return [toolName];
|
|
}
|
|
return capability.methods;
|
|
}
|
|
|
|
function capabilityToolName(capability: CapabilityDescriptor): string | null {
|
|
const id = capability.id.trim();
|
|
if (!id.startsWith("tool.")) {
|
|
return null;
|
|
}
|
|
const title = capability.title.trim();
|
|
if (title) {
|
|
return title;
|
|
}
|
|
return id.slice("tool.".length).trim() || null;
|
|
}
|
|
|
|
function buildDefaultConfig(): Record<string, unknown> {
|
|
return {
|
|
server: {
|
|
host: "127.0.0.1",
|
|
port: 8787,
|
|
api_key: "",
|
|
response_cache: {
|
|
enabled: true,
|
|
ttl_secs: 600,
|
|
max_entries: 200,
|
|
max_body_bytes: 1048576,
|
|
cacheable_status_codes: [200],
|
|
},
|
|
tls: { enable: false, cert_path: null, key_path: null },
|
|
},
|
|
default_provider: "openai",
|
|
remote_management: {
|
|
allow_remote: false,
|
|
secret_key: null,
|
|
disable_control_panel: false,
|
|
},
|
|
quota_exceeded: {
|
|
switch_project: true,
|
|
switch_preview_model: false,
|
|
cooldown_seconds: 60,
|
|
},
|
|
ampcode: {
|
|
upstream_url: null,
|
|
model_mappings: [],
|
|
restrict_management_to_localhost: true,
|
|
},
|
|
proxy_url: null,
|
|
minimize_to_tray: false,
|
|
language: "zh-CN",
|
|
experimental: { webmcp: { enabled: false } },
|
|
tool_calling: {
|
|
enabled: true,
|
|
dynamic_filtering: true,
|
|
native_input_examples: false,
|
|
},
|
|
automation: {
|
|
enabled: false,
|
|
poll_interval_secs: 30,
|
|
enable_history: true,
|
|
},
|
|
workspace_preferences: {
|
|
schema_version: 3,
|
|
media_defaults: {},
|
|
companion_defaults: {},
|
|
service_models: {},
|
|
},
|
|
navigation: { schema_version: 3, enabled_items: [] },
|
|
crash_reporting: {
|
|
enabled: true,
|
|
dsn: null,
|
|
environment: "development",
|
|
sample_rate: 1,
|
|
send_pii: false,
|
|
},
|
|
};
|
|
}
|