/** * # AI 聊天服务(对话式操作入口) * 让用户用自然语言驱动整个 DevOS,而不是手动填表单。 * * 双引擎: * 1. LLM 引擎(配置 AI_PROVIDER 后自动启用):OpenAI function-calling 多轮循环, * 模型自主选择 ToolRegistry 工具并填参(工作区快照注入上下文用于名称→ID 解析); * 2. 规则引擎(无 LLM 时兜底,保证开箱可用):意图匹配核心操作。 * * 安全:所有执行走 ToolRegistry(与 UI/MCP 同一授权与审计); * HIGH/CRITICAL 工具不直接执行,而是创建 AWAITING_APPROVAL 的 AiRun 返回审批卡片。 */ import { Injectable } from "@nestjs/common"; import { zodToJsonSchema } from "zod-to-json-schema"; import type { ResourceStatus } from "@prisma/client"; import { PrismaService } from "../../prisma/prisma.service"; import { ConfigService } from "../../common/config/config.service"; import { AiProviderService } from "../../common/ai/ai-provider.service"; import { ToolRegistryService, type ToolContext } from "./tool-registry.service"; import { LogService, StructuredLoggerInstance } from "../../common/logger/logger.module"; import { maskSecrets } from "../../common/utils/mask"; /** # 聊天允许使用的工具白名单(删除类操作刻意不开放,防误删) **/ const CHAT_TOOLS = [ "devos.server.list", "devos.server.find", "devos.server.status", "devos.server.exec", "devos.server.getDockerContainers", "devos.website.list", "devos.website.create", "devos.website.deploy", "devos.website.logs", "devos.website.health", "devos.deployment.list", "devos.deployment.get", "devos.domain.list", "devos.domain.find", "devos.domain.records", "devos.domain.upsertRecord", "devos.monitor.list", "devos.automation.list", "devos.automation.run", "devos.task.list", "devos.task.create", "devos.task.update", "devos.task.delete", "devos.note.list", "devos.note.create", "devos.note.update", "devos.note.delete", "devos.web.fetch", "devos.repository.createFromUrl", ] as const; /** # 聊天消息(前端回传的简史) **/ export interface ChatTurn { role: "user" | "assistant"; content: string; } /** # 已执行的工具步骤(回显) **/ export interface ChatToolCall { tool: string; args: Record; ok: boolean; summary: string; } /** # 聊天响应 **/ export interface ChatResponse { reply: string; engine: "llm" | "rules"; toolCalls: ChatToolCall[]; /** # HIGH/CRITICAL 操作待审批卡片 **/ approval?: { runId: string; steps: { tool: string; args: Record; risk: string }[] }; /** # 自动执行产生的引用(如新任务 ID) **/ refs?: { type: string; id: string; name: string }[]; } /** # 单条快照行 **/ interface SnapshotItem { id: string; name: string; status?: string; extra?: string; } @Injectable() export class AiChatService { constructor( private readonly prisma: PrismaService, private readonly config: ConfigService, private readonly ai: AiProviderService, private readonly tools: ToolRegistryService, loggers: LogService, ) { this.logger = loggers.child("AiChat"); } /** # 日志器 **/ private readonly logger: StructuredLoggerInstance; /** # 主入口:处理一条用户消息 * @param onEvent 可选进度回调(工具开始/完成实时上报,供流式 UI) */ async chat( workspaceId: string, userId: string, message: string, history: ChatTurn[], onEvent?: (event: { type: "status" | "tool_start" | "tool"; text?: string; call?: ChatToolCall }) => void, ): Promise { const snapshot = await this.buildSnapshot(workspaceId); const ctx: ToolContext = { workspaceId, actorType: "USER", actorId: userId }; if (this.ai.enabled) { try { return await this.llmChat(workspaceId, ctx, message, history, snapshot, onEvent); } catch (error) { this.logger.warn( `LLM chat failed, falling back to rules: ${error instanceof Error ? error.message : ""}`, ); } } return this.rulesChat(workspaceId, ctx, message, snapshot, onEvent); } // ─────────────────────────── LLM 引擎 ─────────────────────────── /** # LLM function-calling 多轮循环 */ private async llmChat( workspaceId: string, ctx: ToolContext, message: string, history: ChatTurn[], snapshot: string, onEvent?: (event: { type: "status" | "tool_start" | "tool"; text?: string; call?: ChatToolCall }) => void, ): Promise { const chatTools = CHAT_TOOLS.map((name) => this.tools.get(name)).filter(Boolean); const toolsParam = chatTools.map((t) => ({ type: "function" as const, function: { name: t!.name, description: `${t!.description}(风险等级 ${t!.risk})`, parameters: zodToJsonSchema(t!.schema as never, { $refStrategy: "none" }), }, })); const messages: Record[] = [ { role: "system", content: [ "你是 DevOS 的运维助手,能通过工具完成平台上的几乎所有操作。今天是 " + new Date().toISOString().slice(0, 10) + "。", "", "## 能力地图", "- 查询:server/website/domain/container/task/note/deployment/monitor/automation 列表与详情", "- 创建:任务、笔记、网站(website.create)、仓库登记(repository.createFromUrl)、DNS 记录(domain.upsertRecord)", "- 操作:部署(website.deploy)、重启容器(container.restart)、服务器命令(server.exec)、运行自动化", "- 编辑/删除:task/note 的 update 与 delete;website/server/domain 的 delete", "- 网页:web.fetch 抓取 URL 正文(供总结/摘录)", "", "## 复合任务编排(重要)", "用户常一次提出多步需求,请按依赖顺序组合工具完成:", "例1「总结 存到笔记」:web.fetch → 自己提炼总结 → note.create(content 用简洁 HTML:h3+p/ul,开头附原文链接)", "例2「把 安装到 <服务器名> 并用 <域名> 解析(CF 代理)」:", " 1. repository.createFromUrl {url}", " 2. server.find {name:<服务器名>} → 取 id", " 3. domain.find {name:<域名根>} → 取 id(若无域名记录,用 domain.upsertRecord 直接建)", " 4. website.create {name, serverId, repositoryId, primaryDomain:<域名>}", " 5. domain.upsertRecord {domainId, type:'A', name:<域名或子域>, value:<服务器公网 IP=host>, proxied:true}", " 6. website.deploy {websiteId}(HIGH,系统会转用户审批)", " 7. website.health 验证", "", "## 规则", "- 只能使用提供的工具;ID 必须来自工具返回或快照,禁止编造。", "- 名称解析:优先 server.find/domain.find,其次快照。", "- HIGH/CRITICAL 工具(部署/重启容器/删除/删除服务器)也直接调用——系统会自动转为用户审批卡片,你只需说明已提交审批。", "- 删除前必须与用户确认过上下文;不确定时先查询再行动。", "- 回答简洁、用用户的语言(默认中文);汇报做了什么、结果如何;不要复读工具原始 JSON。", "", "## 当前工作区快照", snapshot, ].join("\n"), }, ...history.slice(-8).map((h) => ({ role: h.role, content: h.content })), { role: "user", content: message }, ]; const toolCalls: ChatToolCall[] = []; const pendingApproval: { tool: string; args: Record; risk: string }[] = []; let iterations = 0; let finalText = ""; while (iterations < 6) { iterations += 1; const res = await this.rawChatCompletion(messages, toolsParam); const choice = res.choices?.[0]; if (!choice) throw new Error("empty completion"); const msg = choice.message as { content?: string | null; tool_calls?: { id: string; function: { name: string; arguments: string } }[]; }; if (!msg.tool_calls || msg.tool_calls.length === 0) { finalText = msg.content ?? "(无回复)"; break; } messages.push({ role: "assistant", content: msg.content ?? null, tool_calls: msg.tool_calls }); for (const call of msg.tool_calls) { const tool = this.tools.get(call.function.name); if (!tool) { messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ error: "unknown tool" }) }); continue; } let args: Record = {}; try { args = JSON.parse(call.function.arguments || "{}") as Record; } catch { args = {}; } // HIGH/CRITICAL → 转审批,不直接执行 if (tool.risk === "HIGH" || tool.risk === "CRITICAL") { pendingApproval.push({ tool: tool.name, args, risk: tool.risk }); messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ ok: true, note: "submitted for user approval" }), }); continue; } onEvent?.({ type: "tool_start", text: tool.name }); const rec: ChatToolCall = { tool: tool.name, args: maskSecrets(args) as Record, ok: false, summary: "" }; try { const result = await tool.execute(ctx, args as never); rec.ok = true; rec.summary = summarize(result); messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(maskSecrets(result)).slice(0, 6000), }); } catch (error) { const err = error instanceof Error ? error.message : String(error); rec.summary = err.slice(0, 200); messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ error: err.slice(0, 500) }) }); } toolCalls.push(rec); onEvent?.({ type: "tool", call: rec }); } if (pendingApproval.length > 0) break; // 有审批待确认,先返回 } if (pendingApproval.length > 0) { const run = await this.createApprovalRun(workspaceId, ctx.actorId ?? "", message, pendingApproval); return { reply: `以下操作风险较高,需要你确认后执行:\n${pendingApproval .map((s) => `• ${s.tool}(${s.risk})`) .join("\n")}\n确认后我会立即执行。`, engine: "llm", toolCalls, approval: { runId: run.id, steps: pendingApproval }, }; } if (!finalText) finalText = summarizeDone(toolCalls); return { reply: finalText, engine: "llm", toolCalls }; } /** # 调用 OpenAI 兼容 chat/completions(带工具) */ private async rawChatCompletion( messages: Record[], tools: unknown[], ): Promise<{ choices?: { message?: unknown }[] }> { const res = await fetch(`${this.config.aiBaseUrl.replace(/\/$/, "")}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.config.aiApiKey}` }, body: JSON.stringify({ model: this.config.aiModel, messages, tools, temperature: 0.2, }), signal: AbortSignal.timeout(90_000), }); if (!res.ok) throw new Error(`AI provider HTTP ${res.status}`); return (await res.json()) as { choices?: { message?: unknown }[] }; } /** # 创建待审批运行(复用 AI Gateway 的审批执行管线) */ private async createApprovalRun( workspaceId: string, _userId: string, objective: string, steps: { tool: string; args: Record; risk: string }[], ): Promise<{ id: string }> { const plan = steps.map((s) => ({ tool: s.tool, args: s.args, reason: "chat request", risk: s.risk })); const risk = steps.some((s) => s.risk === "CRITICAL") ? "CRITICAL" : "HIGH"; return this.prisma.$transaction(async (tx) => { const resource = await tx.resource.create({ data: { workspaceId, type: "AI_RUN", name: objective.slice(0, 80), provider: "chat", status: "PENDING" }, }); return tx.aiRun.create({ data: { workspaceId, resourceId: resource.id, objective: objective.slice(0, 2000), status: "AWAITING_APPROVAL", source: "CHAT", riskLevel: risk, plan: plan as unknown as object, }, select: { id: true }, }); }); } // ─────────────────────────── 规则引擎(离线兜底) ─────────────────────────── /** # 规则引擎:意图匹配核心操作 */ private async rulesChat( workspaceId: string, ctx: ToolContext, message: string, snapshot: string, onEvent?: (event: { type: "status" | "tool_start" | "tool"; text?: string; call?: ChatToolCall }) => void, ): Promise { const text = message.trim(); const toolCalls: ChatToolCall[] = []; const run = async (toolName: string, args: Record): Promise => { const tool = this.tools.get(toolName); if (!tool) return null; onEvent?.({ type: "tool_start", text: toolName }); try { const result = await tool.execute(ctx, args as never); const call: ChatToolCall = { tool: toolName, args, ok: true, summary: summarize(result) }; toolCalls.push(call); onEvent?.({ type: "tool", call }); return result; } catch (error) { const err = error instanceof Error ? error.message : String(error); const call: ChatToolCall = { tool: toolName, args, ok: false, summary: err.slice(0, 200) }; toolCalls.push(call); onEvent?.({ type: "tool", call }); return { __error: err }; } }; // 帮助 if (/帮助|help|你能做什么|能干什么/i.test(text)) { return { reply: HELP_TEXT, engine: "rules", toolCalls: [] }; } // URL 总结 → 存笔记(规则引擎用抽取式摘要兜底;LLM 模式下由模型深度总结) const urlInText = text.match(/https?:\/\/[^\s,。;))]+/); if (urlInText && /总结|摘要|summary|保存到?笔记|记到笔记/i.test(text)) { const url = urlInText[0]; const fetchTool = this.tools.get("devos.web.fetch")!; try { const page = (await fetchTool.execute(ctx, { url })) as { title?: string; description?: string; text?: string }; const title = (page.title ?? url).slice(0, 120); // 抽取式摘要:meta 描述 + 前几段 const paras = (page.text ?? "").split("\n\n").filter((p) => p && !p.startsWith("# ")).slice(0, 5); const summaryHtml = [ `

📄 来源:${url}

`, page.description ? `

摘要:${page.description}

` : "", "
    " + paras.map((p) => `
  • ${p.slice(0, 260)}
  • `).join("") + "
", "

配置 LLM 后可获得更高质量的深度总结。

", ].filter(Boolean).join(""); const noteTool = this.tools.get("devos.note.create")!; const note = (await noteTool.execute(ctx, { title: `总结:${title}`, content: summaryHtml, tags: ["summary"], })) as { id: string; title: string }; return { reply: `✅ 已抓取并总结「${title}」,保存为笔记「${note.title}」。可在「笔记」页面查看与编辑(配置 LLM 后总结质量更高)。`, engine: "rules", toolCalls: [ { tool: "devos.web.fetch", args: { url }, ok: true, summary: title }, { tool: "devos.note.create", args: { title: note.title }, ok: true, summary: note.id }, ], refs: [{ type: "NOTE", id: note.id, name: note.title }], }; } catch (error) { return { reply: `抓取页面失败:${error instanceof Error ? error.message : "unknown"}。请确认 URL 可公网访问。`, engine: "rules", toolCalls, }; } } // 创建任务 const taskM = text.match(/(?:创建|新建|添加|记录)个?任务[::,,\s]*(.+)/); if (taskM) { const title = taskM[1].slice(0, 120).trim(); const result = await run("devos.task.create", { title }); if (result && !(result as { __error?: string }).__error) { const t = result as { id: string; title: string }; return { reply: `✅ 已创建任务「${t.title}」`, engine: "rules", toolCalls, refs: [{ type: "TASK", id: t.id, name: t.title }], }; } } // 创建笔记 const noteM = text.match(/(?:创建|新建|添加|记)个?笔记[::,,\s]*(.+)/); if (noteM) { const title = noteM[1].slice(0, 120).trim(); const result = await run("devos.note.create", { title, content: "" }); if (result && !(result as { __error?: string }).__error) { const n = result as { id: string; title: string }; return { reply: `✅ 已创建笔记「${n.title}」,可到「笔记」页面编辑内容。`, engine: "rules", toolCalls, refs: [{ type: "NOTE", id: n.id, name: n.title }], }; } } // 部署网站(HIGH → 审批) const deployM = text.match(/部署\s*网站?\s*[::]?\s*(\S+)|deploy\s+(\S+)/i); if (deployM) { const name = (deployM[1] ?? deployM[2] ?? "").replace(/网站|到\S+$/g, "").trim(); const site = await this.findWebsite(workspaceId, name); if (!site) { const sites = await this.prisma.website.findMany({ where: { workspaceId }, take: 10, select: { name: true } }); return { reply: `没有找到名称包含「${name}」的网站。${sites.length ? `当前的网站有:${sites.map((s) => s.name).join("、")}` : "当前还没有网站,可到「网站」页面创建。"}`, engine: "rules", toolCalls, }; } const run0 = await this.createApprovalRun(workspaceId, ctx.actorId ?? "", text, [ { tool: "devos.website.deploy", args: { websiteId: site.id }, risk: "HIGH" }, ]); return { reply: `⚠️ 部署是 HIGH 风险操作:将部署网站「${site.name}」。确认后执行(含健康验证)。`, engine: "rules", toolCalls, approval: { runId: run0.id, steps: [{ tool: "devos.website.deploy", args: { websiteId: site.id }, risk: "HIGH" }], }, }; } // 重启容器(HIGH → 审批) const restartM = text.match(/(?:重启|restart)\s*容器?\s*[::]?\s*(\S+)/i); if (restartM) { const name = restartM[1]; const container = await this.findContainer(workspaceId, name); if (!container) { const rows = await this.prisma.container.findMany({ where: { workspaceId }, take: 12, orderBy: { updatedAt: "desc" }, select: { name: true, state: true } }); return { reply: `没有找到名称包含「${name}」的容器。${rows.length ? `运行中的容器如:${rows.map((c) => c.name).slice(0, 8).join("、")}` : "还没有发现过容器,先到服务器详情页执行「发现容器」。"}`, engine: "rules", toolCalls, }; } const run0 = await this.createApprovalRun(workspaceId, ctx.actorId ?? "", text, [ { tool: "devos.container.restart", args: { id: container.id }, risk: "HIGH" }, ]); return { reply: `⚠️ 重启容器是 HIGH 风险操作:将重启「${container.name}」(${container.image.slice(0, 30)})。确认后执行。`, engine: "rules", toolCalls, approval: { runId: run0.id, steps: [{ tool: "devos.container.restart", args: { id: container.id }, risk: "HIGH" }], }, }; } // 列出/查看类 const listTarget = text.match(/(?:列出|查看|显示|有哪些|list)\s*(全部|所有|一下)?\s*(服务器|网站|域名|容器|任务|笔记|部署|监控|自动化|仓库|凭据)?/i); if (listTarget) { const target = listTarget[2] ?? inferTargetFromText(text); if (target) { const map: Record = { 服务器: "devos.server.list", 网站: "devos.website.list", 域名: "devos.domain.list", 容器: "devos.server.getDockerContainers", 任务: "devos.task.list", 笔记: "devos.note.list", 部署: "devos.deployment.list", 监控: "devos.monitor.list", 自动化: "devos.automation.list", }; const toolName = map[target]; if (toolName) { // 容器列表需要 serverId:取第一台在线服务器 if (target === "容器") { const firstServer = (await this.tools.get("devos.server.list")!.execute(ctx, {})) as { id: string }[] | undefined; const sid = Array.isArray(firstServer) ? firstServer[0]?.id : undefined; if (!sid) { return { reply: "还没有登记服务器,无法发现容器。", engine: "rules", toolCalls }; } const containers = await run("devos.server.getDockerContainers", { serverId: sid }); return { reply: renderList(target, containers), engine: "rules", toolCalls }; } const result = await run(toolName, target === "任务" || target === "笔记" ? {} : {}); return { reply: renderList(target, result), engine: "rules", toolCalls }; } } } // 网站健康/状态 const healthM = text.match(/(健康|health|状态|status|在线吗|正常吗)/i); if (healthM) { const site = await this.findAnyWebsite(workspaceId); if (site) { const result = await run("devos.website.health", { id: site.id }); const r = result as { ok?: boolean; status?: number; latencyMs?: number; error?: string }; return { reply: r?.ok ? `✅ 「${site.name}」健康:HTTP ${r.status}(${r.latencyMs ?? "?"}ms)` : `❌ 「${site.name}」异常:${r?.error ?? `HTTP ${r?.status}`}`, engine: "rules", toolCalls, }; } } return { reply: `我暂时没理解这句话。我可以(离线模式,配置 LLM 后理解任意表达):\n\n• 列出服务器 / 网站 / 域名 / 容器 / 任务 / 部署\n• 创建任务:<标题>\n• 创建笔记:<标题>\n• 部署网站:<名称>(需确认)\n• 重启容器:<名称>(需确认)\n• 网站健康检查\n• 帮助`, engine: "rules", toolCalls, }; } // ─────────────────────────── 上下文与解析 ─────────────────────────── /** # 构建工作区快照(名称→ID 解析用) */ private async buildSnapshot(workspaceId: string): Promise { const [servers, websites, domains, containers, tasks] = await this.prisma.$transaction([ this.prisma.server.findMany({ where: { workspaceId }, take: 15, include: { resource: { select: { status: true } } } }), this.prisma.website.findMany({ where: { workspaceId }, take: 15, include: { resource: { select: { status: true } } } }), this.prisma.domain.findMany({ where: { workspaceId }, take: 15 }), this.prisma.container.findMany({ where: { workspaceId }, take: 20, orderBy: { updatedAt: "desc" } }), this.prisma.task.findMany({ where: { workspaceId }, take: 10, orderBy: { updatedAt: "desc" } }), ]); const lines: string[] = []; lines.push(`服务器(${servers.length}): ${servers.map((s) => `${s.name}(id=${s.id}, ${s.resource.status})`).join(", ") || "无"}`); lines.push(`网站(${websites.length}): ${websites.map((w) => `${w.name}(id=${w.id}, ${w.resource.status})`).join(", ") || "无"}`); lines.push(`域名(${domains.length}): ${domains.map((d) => `${d.name}(id=${d.id})`).join(", ") || "无"}`); lines.push(`容器(最近): ${containers.map((c) => `${c.name}(id=${c.id}, ${c.state})`).join(", ") || "无"}`); lines.push(`任务(最近): ${tasks.map((t) => `${t.title}(status=${t.status})`).join(", ") || "无"}`); return lines.join("\n"); } /** # 模糊查找网站 */ private async findWebsite(workspaceId: string, name: string) { const rows = await this.prisma.website.findMany({ where: { workspaceId } }); const lower = name.toLowerCase(); return ( rows.find((r) => r.name.toLowerCase() === lower) ?? rows.find((r) => r.name.toLowerCase().includes(lower)) ?? rows.find((r) => r.primaryDomain?.toLowerCase().includes(lower)) ?? null ); } /** # 任意网站(健康检查默认目标) */ private async findAnyWebsite(workspaceId: string) { return this.prisma.website.findFirst({ where: { workspaceId }, orderBy: { updatedAt: "desc" } }); } /** # 模糊查找容器 */ private async findContainer(workspaceId: string, name: string) { const rows = await this.prisma.container.findMany({ where: { workspaceId } }); const lower = name.toLowerCase(); return rows.find((r) => r.name.toLowerCase() === lower) ?? rows.find((r) => r.name.toLowerCase().includes(lower)) ?? null; } } /** # 从文本推断列表目标(规则引擎兜底) */ function inferTargetFromText(text: string): string | null { const map: [RegExp, string][] = [ [/服务器|server/i, "服务器"], [/网站|website/i, "网站"], [/域名|domain/i, "域名"], [/容器|container/i, "容器"], [/任务|task/i, "任务"], [/笔记|note/i, "笔记"], [/部署|deployment/i, "部署"], [/监控|monitor/i, "监控"], [/自动化|automation/i, "自动化"], ]; for (const [re, label] of map) if (re.test(text)) return label; return null; } /** # 渲染列表结果(人性化的简短文本,不吐 JSON) */ function renderList(target: string, result: unknown): string { const rows = Array.isArray(result) ? (result as Record[]) : []; if (rows.length === 0) return `当前没有${target}。`; const lines = rows.slice(0, 15).map((r) => { const name = (r.name ?? r.title ?? r.fullName ?? "?") as string; const status = (r.status ?? r.state ?? "") as string; const extra = r.primaryDomain ?? r.host ?? ""; const image = r.image ? `(${String(r.image).slice(0, 32)})` : ""; return `- ${name}${status ? `(${status})` : ""}${extra ? ` — ${String(extra)}` : ""}${target === "容器" ? image : ""}`; }); const more = rows.length > 15 ? `\n…及其它 ${rows.length - 15} 项` : ""; return `**${target}** 共 ${rows.length} 项:\n\n${lines.join("\n")}${more}`; } /** # 工具结果摘要(回显用,避免超长 JSON) */ function summarize(result: unknown): string { if (Array.isArray(result)) { const first = result[0] as Record | undefined; return `${result.length} 项${first ? `,如 ${String(first.name ?? first.title ?? first.id ?? "")}` : ""}`; } const r = result as Record | null; if (!r) return "完成"; if (r.title) return String(r.title); if (r.name) return String(r.name); if (r.ok !== undefined) return `ok=${r.ok}`; return Object.keys(r).slice(0, 4).join(","); } /** # 无文本时的兜底总结 */ function summarizeDone(calls: ChatToolCall[]): string { const okCount = calls.filter((c) => c.ok).length; return `已完成 ${okCount}/${calls.length} 项操作。`; } /** # 能力说明(离线模式) */ const HELP_TEXT = [ "我是 DevOS 运维助手,可以直接帮我完成这些事:", "", "📋 任务与笔记", " • 创建任务:修复登录页超时", " • 创建笔记:部署排障手册", " • 列出任务 / 列出笔记", "", "🖥️ 基础设施", " • 列出服务器 / 列出容器 / 列出监控", " • 服务器连通状态(到详情页看完整指标)", "", "🌐 网站与部署", " • 列出网站 / 网站健康检查", " • 部署网站:<名称>(需要你确认后执行)", " • 重启容器:<名称>(需要你确认后执行)", "", "⚙️ 其它", " • 列出域名 / 部署 / 自动化;运行自动化:<名称>", "", "配置 LLM(AI_PROVIDER_*)后,可以用任意自然语言表达,我会自动选择并组合工具完成操作。", ].join("\n");