diff --git a/src/components/AppSidebarContent.tsx b/src/components/AppSidebarContent.tsx index 37f34ce..d3f3e51 100644 --- a/src/components/AppSidebarContent.tsx +++ b/src/components/AppSidebarContent.tsx @@ -9,7 +9,7 @@ import { Rocket, Database, Key, - History, + Activity, Settings, Plus, } from 'lucide-react' @@ -20,7 +20,7 @@ const navItems = [ { href: '/deployments', label: 'Deployments', icon: Rocket }, { href: '/resources', label: 'Resources', icon: Database }, { href: '/api-keys', label: 'API Keys', icon: Key }, - { href: '/logs', label: 'Logs', icon: History }, + { href: '/panel/observability', label: 'Observability', icon: Activity }, { href: '/settings', label: 'Settings', icon: Settings }, ] diff --git a/src/components/openclaw/OpenClawAssistantPane.tsx b/src/components/openclaw/OpenClawAssistantPane.tsx index fd624d3..6a095e4 100644 --- a/src/components/openclaw/OpenClawAssistantPane.tsx +++ b/src/components/openclaw/OpenClawAssistantPane.tsx @@ -50,6 +50,9 @@ type OpenClawAssistantPaneProps = { initialQuestion?: string; initialQuestionKey?: number; variant?: "page" | "sidebar"; + showConversation?: boolean; + emptyConversationHint?: string; + onStateChange?: (state: OpenClawAssistantViewState) => void; }; type ComposerAttachment = GatewayChatAttachmentPayload & { @@ -60,6 +63,23 @@ type ComposerAttachment = GatewayChatAttachmentPayload & { type ConnectionState = "idle" | "connecting" | "ready" | "error"; +export type OpenClawAssistantViewState = { + connectionState: ConnectionState; + healthBadge: string; + errorMessage: string; + hasGateway: boolean; + selectedSessionLabel: string; + streamingText: string; + streamingHtml: string; + messages: Array<{ + id: string; + role: string; + text: string; + html: string; + timestampMs?: number; + }>; +}; + function pickCopy(isChinese: boolean, zh: string, en: string): string { return isChinese ? zh : en; } @@ -195,6 +215,9 @@ export function OpenClawAssistantPane({ initialQuestion, initialQuestionKey, variant = "page", + showConversation = true, + emptyConversationHint, + onStateChange, }: OpenClawAssistantPaneProps) { const router = useRouter(); const { language } = useLanguage(); @@ -432,6 +455,34 @@ export function OpenClawAssistantPane({ [messages, minimalPage], ); + useEffect(() => { + onStateChange?.({ + connectionState, + healthBadge, + errorMessage, + hasGateway: Boolean(openclawUrl.trim()), + selectedSessionLabel: + activeSession?.derivedTitle || + activeSession?.displayName || + selectedSessionKey || + copy.mainSession, + streamingText, + streamingHtml: streamingText ? renderMarkdown(streamingText) : "", + messages: renderedMessages, + }); + }, [ + activeSession, + connectionState, + copy.mainSession, + errorMessage, + healthBadge, + onStateChange, + openclawUrl, + renderedMessages, + selectedSessionKey, + streamingText, + ]); + const connectGateway = useCallback( async (nextSessionKey?: string, nextAgentId?: string): Promise => { if (!openclawUrl.trim()) { @@ -904,7 +955,26 @@ export function OpenClawAssistantPane({ ) : null}
- {!openclawUrl.trim() ? ( + {!showConversation ? ( +
+
+ +
+
+

+ {copy.assistantTitle} +

+

+ {emptyConversationHint ?? + pickCopy( + isChinese, + "在右侧发起任务,中间区域会同步展示助手结果。", + "Start tasks from the right panel. Results will be mirrored in the center workspace.", + )} +

+
+
+ ) : !openclawUrl.trim() ? (
diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 153eb1e..d3c1c15 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -998,7 +998,7 @@ export const translations: Record<'en' | 'zh', Translation> = { deployments: 'Deployments', resources: 'Resources', apiKeys: 'API Keys', - logs: 'Logs', + logs: 'Observability', settings: 'Settings', }, overview: { @@ -1168,10 +1168,28 @@ export const translations: Record<'en' | 'zh', Translation> = { { title: 'Full-link SaaS Hosting', description: 'Provide one-stop hosting services from development and deployment to maintenance, simplifying architectural complexity and helping applications quickly achieve SaaS transformation.', + guide: { + title: 'Deployments Console Guide', + dismiss: 'Exit Guide', + steps: [ + { text: 'Use the deployments console as the operational entry for release progress, blockers, and execution history.' }, + { text: 'Open the Deployments workspace and let X Assistant generate the rollout checklist or summarize current status.', link: { url: '/panel/deployments', label: 'Open Deployments Console' } }, + { text: 'Once connected, ask for deployment diagnosis, release steps, or rollback suggestions, and the result stream will appear in the center workspace.' }, + ], + }, }, { title: 'AI-Driven Observability', description: 'Utilize AI to intelligently analyze full-link logs and performance metrics, identifying potential anomalies in real-time and providing predictive insights to ensure smooth system operation.', + guide: { + title: 'Observability Console Guide', + dismiss: 'Exit Guide', + steps: [ + { text: 'Use the observability console as the AI entry for logs, metrics, and anomaly investigation.' }, + { text: 'Open the Observability workspace and start from log analysis, timeline summaries, or incident review.', link: { url: '/panel/observability', label: 'Open Observability Console' } }, + { text: 'X Assistant stays open on the right while the center pane continuously mirrors analysis results, remediation ideas, and summaries.' }, + ], + }, }, ], nextSteps: { @@ -1778,7 +1796,7 @@ export const translations: Record<'en' | 'zh', Translation> = { deployments: '部署管理', resources: '资源列表', apiKeys: '接口密钥', - logs: '运行日志', + logs: '可观测性', settings: '系统设置', }, overview: { @@ -1944,10 +1962,28 @@ export const translations: Record<'en' | 'zh', Translation> = { { title: '全链路 SaaS 托管', description: '提供从开发、部署到维护的一站式托管服务,简化架构复杂度,助力应用快速实现 SaaS 化转型。', + guide: { + title: '部署控制台向导', + dismiss: '退出向导', + steps: [ + { text: '把部署控制台作为发布进度、阻塞项和执行记录的统一操作入口。' }, + { text: '打开 Deployments 工作区,让 X 助手为你生成部署检查清单,或者总结当前发布状态。', link: { url: '/panel/deployments', label: '打开 Deployments 控制台' } }, + { text: '接入完成后,可以继续让助手分析失败部署、补充回滚步骤,并把结果同步展示在中间工作区。' }, + ], + }, }, { title: 'AI 驱动的可观测性', description: '利用 AI 智能分析全链路日志与性能指标,实时识别潜在异常并提供预测性洞察,保障系统平稳运行。', + guide: { + title: '可观测性控制台向导', + dismiss: '退出向导', + steps: [ + { text: '把可观测性控制台作为日志、指标与异常诊断的 AI 工作入口。' }, + { text: '打开 Observability 工作区,从日志分析、时间线梳理或事件复盘开始。', link: { url: '/panel/observability', label: '打开 Observability 控制台' } }, + { text: '右侧 X 助手保持打开,中间区域持续展示分析结果、修复建议与总结。' }, + ], + }, }, ], nextSteps: { diff --git a/src/modules/extensions/builtin/infra/index.ts b/src/modules/extensions/builtin/infra/index.ts index 48b50e3..38360c1 100644 --- a/src/modules/extensions/builtin/infra/index.ts +++ b/src/modules/extensions/builtin/infra/index.ts @@ -1,4 +1,4 @@ -import { Database, History, Key, Rocket, Settings } from 'lucide-react' +import { Activity, Database, Key, Rocket, Settings } from 'lucide-react' import type { DashboardExtension } from '../../types' @@ -6,10 +6,10 @@ export const infraExtension: DashboardExtension = { id: 'builtin.infra', meta: { title: '基础设施管理', - description: '云基础设施、部署、资源与日志管理。', + description: '云基础设施、部署、资源与可观测性管理。', version: '1.0.0', author: 'Cloud-Neutral', - keywords: ['infrastructure', 'deployments', 'resources', 'logs'], + keywords: ['infrastructure', 'deployments', 'resources', 'observability'], }, routes: [ { @@ -44,10 +44,10 @@ export const infraExtension: DashboardExtension = { }, { id: 'logs', - path: '/panel/logs', - label: 'Logs', - description: '系统流水与审计日志', - icon: History, + path: '/panel/observability', + label: 'Observability', + description: '监控、日志与 AI 分析', + icon: Activity, loader: () => import('./routes/placeholder'), guard: { requireLogin: true }, sidebar: { section: 'infra', order: 3 }, diff --git a/src/modules/extensions/builtin/infra/routes/placeholder.tsx b/src/modules/extensions/builtin/infra/routes/placeholder.tsx index d183f10..f72daed 100644 --- a/src/modules/extensions/builtin/infra/routes/placeholder.tsx +++ b/src/modules/extensions/builtin/infra/routes/placeholder.tsx @@ -1,22 +1,368 @@ -'use client' +"use client"; -import React from 'react' -import { usePathname } from 'next/navigation' +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import type { LucideIcon } from "lucide-react"; +import { + Activity, + ArrowRight, + Database, + KeyRound, + Rocket, + ShieldCheck, + Sparkles, +} from "lucide-react"; + +import { + OpenClawAssistantPane, + type OpenClawAssistantViewState, +} from "@/components/openclaw/OpenClawAssistantPane"; +import type { IntegrationDefaults } from "@/lib/openclaw/types"; +import { cn } from "@/lib/utils"; + +type WorkspaceKind = "deployments" | "resources" | "logs" | "api-keys"; + +type WorkspaceConfig = { + title: string; + subtitle: string; + icon: LucideIcon; + accent: string; + prompts: string[]; + suggestions: string[]; +}; + +const EMPTY_DEFAULTS: IntegrationDefaults = { + openclawUrl: "", + openclawOrigin: "", + openclawTokenConfigured: false, + vaultUrl: "", + vaultNamespace: "", + vaultTokenConfigured: false, + vaultSecretPath: "", + vaultSecretKey: "", + apisixUrl: "", + apisixTokenConfigured: false, +}; + +function getWorkspaceKind(pathname: string): WorkspaceKind { + if (pathname.includes("/resources")) { + return "resources"; + } + if (pathname.includes("/observability")) { + return "logs"; + } + if (pathname.includes("/api-keys")) { + return "api-keys"; + } + return "deployments"; +} + +function getWorkspaceConfig(kind: WorkspaceKind): WorkspaceConfig { + switch (kind) { + case "resources": + return { + title: "Resources", + subtitle: "让 X 助手整理资源盘点、实例状态和依赖关系,结果在这里持续展开。", + icon: Database, + accent: "R", + prompts: [ + "盘点当前资源并按环境分组", + "列出数据库实例与风险项", + "整理资源依赖关系和下一步动作", + ], + suggestions: [ + "资源清单", + "实例状态", + "风险摘要", + ], + }; + case "logs": + return { + title: "Observability", + subtitle: "把监控、日志与 AI 分析集中到同一个中间结果区,不再停留在空白页。", + icon: Activity, + accent: "O", + prompts: [ + "分析最近异常日志并归类", + "总结监控异常和修复建议", + "按时间线梳理今天的可观测性事件", + ], + suggestions: [ + "指标概览", + "日志分析", + "修复建议", + ], + }; + case "api-keys": + return { + title: "API Keys", + subtitle: "把接口密钥、访问凭证和安全引用整理成可交互的工作区结果。", + icon: KeyRound, + accent: "K", + prompts: [ + "梳理当前密钥用途与系统归属", + "列出需要轮换的访问凭证", + "生成密钥治理检查清单", + ], + suggestions: [ + "凭证盘点", + "轮换建议", + "治理清单", + ], + }; + default: + return { + title: "Deployments", + subtitle: "部署任务、发布状态和后续动作由 X 助手驱动,中间区直接展示结果流。", + icon: Rocket, + accent: "D", + prompts: [ + "总结当前部署状态和阻塞项", + "生成一次部署检查清单", + "分析失败部署并给出修复步骤", + ], + suggestions: [ + "部署状态", + "阻塞项", + "执行步骤", + ], + }; + } +} + +function StatusPill({ + label, + tone = "neutral", +}: { + label: string; + tone?: "neutral" | "success" | "danger"; +}) { + return ( +
+ + {label} +
+ ); +} export default function PlaceholderPage() { - const pathname = usePathname() - const title = pathname.split('/').pop()?.replace(/-/g, ' ') || 'Page' + const pathname = usePathname(); + const kind = useMemo(() => getWorkspaceKind(pathname), [pathname]); + const config = useMemo(() => getWorkspaceConfig(kind), [kind]); + const Icon = config.icon; - return ( -
-
- {title.charAt(0)} + const [defaults, setDefaults] = useState(EMPTY_DEFAULTS); + const [promptSeed, setPromptSeed] = useState(""); + const [promptKey, setPromptKey] = useState(0); + const [assistantState, setAssistantState] = + useState({ + connectionState: "idle", + healthBadge: "offline", + errorMessage: "", + hasGateway: false, + selectedSessionLabel: "main", + streamingText: "", + streamingHtml: "", + messages: [], + }); + + useEffect(() => { + let cancelled = false; + + void (async () => { + try { + const response = await fetch("/api/integrations/defaults", { + cache: "no-store", + }); + if (!response.ok) { + return; + } + const payload = (await response.json()) as IntegrationDefaults; + if (!cancelled) { + setDefaults(payload); + } + } catch { + // Keep empty defaults so the assistant can still render setup guidance. + } + })(); + + return () => { + cancelled = true; + }; + }, []); + + const assistantMessages = assistantState.messages.filter( + (message) => message.role === "user" || message.role === "assistant", + ); + const statusTone = + assistantState.connectionState === "ready" + ? "success" + : assistantState.connectionState === "error" + ? "danger" + : "neutral"; + + return ( +
+
+
+
+
+
+
+ +
+
+ {config.accent} +
+

+ {config.title} +

+

+ {config.subtitle} +

+
+
+ + +
-

{title}

-

- This feature is currently under active development. - It will soon provide a powerful interface for managing your {title.toLowerCase()}. -

-
- ) + +
+ {config.prompts.map((prompt) => ( + + ))} +
+
+ +
+ {!assistantState.hasGateway ? ( +
+ +

+ 先连接 X 助手 +

+

+ 右侧助手已经默认打开。完成 Gateway 接入后,这里的中间结果区会直接显示部署、资源或日志分析结果。 +

+ + 打开接口集成 + + +
+ ) : assistantMessages.length === 0 && !assistantState.streamingText ? ( +
+
+ +
+

+ 等待 X 助手输出结果 +

+

+ 在右侧直接提问,或者使用上面的快捷任务。助手的分析、步骤和流式结果会实时出现在这个中间工作区。 +

+
+ {config.suggestions.map((item) => ( +
+ {item} +
+ ))} +
+
+ ) : ( +
+ {assistantMessages.map((message) => { + const isUser = message.role === "user"; + + return ( +
+
+
+
+
+ ); + })} + + {assistantState.streamingText ? ( +
+
+
+
+
+ ) : null} +
+ )} +
+ + + +
+
+ ); } diff --git a/src/server/consoleIntegrations.ts b/src/server/consoleIntegrations.ts index caad589..f3190bb 100644 --- a/src/server/consoleIntegrations.ts +++ b/src/server/consoleIntegrations.ts @@ -18,7 +18,7 @@ const APISIX_URL_KEYS = [ 'API_GATEWAY_URL', ] as const -const APISIX_TOKEN_KEYS = ['AI_GATEWAY_ACCESS_TOKEN'] as const +const APISIX_TOKEN_KEYS = ['AI_GATEWAY_ACCESS_TOKEN', 'AI_GATEWAY_API_KEY'] as const const VAULT_URL_KEYS = ['VAULT_SERVER_URL', 'VAULT_ADDR', 'vault_addr'] as const const VAULT_NAMESPACE_KEYS = ['VAULT_NAMESPACE'] as const @@ -257,7 +257,7 @@ export async function resolveApisixProbeConfig(overrides?: { vaultToken: overrides?.vaultToken ?? readEnvValue(...VAULT_TOKEN_KEYS) ?? '', vaultNamespace: overrides?.vaultNamespace ?? readEnvValue(...VAULT_NAMESPACE_KEYS) ?? '', vaultSecretPath: overrides?.vaultSecretPath, - fallbackKeys: ['AI_GATEWAY_ACCESS_TOKEN', 'APISIX_AI_GATEWAY_TOKEN'], + fallbackKeys: ['AI_GATEWAY_ACCESS_TOKEN', 'AI_GATEWAY_API_KEY', 'APISIX_AI_GATEWAY_TOKEN'], }) return {