From 72d090f18220e1d83afeb1edac4383a38f958fd4 Mon Sep 17 00:00:00 2001 From: Patodo Date: Fri, 9 Oct 2026 17:12:59 +0900 Subject: [PATCH 1/2] feat(agent): add document skills and shared Python environment --- docs/agent-document-skills.md | 48 ++ packages/sdk-agent/src/agent-controls.tsx | 3 +- packages/sdk-agent/src/harness-client.ts | 4 + packages/sdk-agent/src/use-agent.ts | 1 + packages/server/THIRD_PARTY_NOTICES.txt | 7 + .../server/scripts/build-server-package.mjs | 3 +- .../build-server-package.node-test.mjs | 8 + packages/server/skills/docx/LICENSE | 21 + packages/server/skills/docx/SKILL.md | 203 +++++++++ packages/server/skills/docx/SOURCE.json | 11 + .../docx/references/revisions-and-comments.md | 88 ++++ .../skills/docx/scripts/docx_comments.py | 289 ++++++++++++ .../server/skills/docx/scripts/docx_common.py | 94 ++++ .../server/skills/docx/scripts/docx_create.py | 177 ++++++++ .../server/skills/docx/scripts/docx_edit.py | 250 +++++++++++ .../server/skills/docx/scripts/docx_read.py | 149 +++++++ .../skills/docx/scripts/docx_revisions.py | 147 ++++++ .../skills/docx/scripts/docx_template.py | 70 +++ .../skills/docx/scripts/docx_validate.py | 156 +++++++ packages/server/skills/markitdown/LICENSE | 21 + packages/server/skills/markitdown/SKILL.md | 274 ++++++++++++ packages/server/skills/markitdown/SOURCE.json | 11 + .../markitdown/references/api_reference.md | 418 ++++++++++++++++++ .../markitdown/references/cloud_and_ocr.md | 351 +++++++++++++++ .../markitdown/references/file_formats.md | 296 +++++++++++++ .../markitdown/references/mcp_and_plugins.md | 243 ++++++++++ .../skills/markitdown/references/migration.md | 368 +++++++++++++++ .../skills/markitdown/references/security.md | 246 +++++++++++ .../skills/markitdown/references/workflows.md | 309 +++++++++++++ .../markitdown/scripts/batch_convert.py | 354 +++++++++++++++ .../markitdown/scripts/convert_literature.py | 405 +++++++++++++++++ .../scripts/inspect_installation.py | 162 +++++++ packages/server/skills/pdf/LICENSE | 21 + packages/server/skills/pdf/SKILL.md | 132 ++++++ packages/server/skills/pdf/SOURCE.json | 11 + .../server/skills/pdf/references/forms.md | 99 +++++ .../skills/pdf/references/nano-pdf-editing.md | 40 ++ .../skills/pdf/references/ocr-extraction.md | 165 +++++++ packages/server/skills/pdf/scripts/_raster.py | 76 ++++ .../skills/pdf/scripts/extract_marker.py | 88 ++++ .../skills/pdf/scripts/extract_pymupdf.py | 101 +++++ .../server/skills/pdf/scripts/pdf_create.py | 130 ++++++ .../skills/pdf/scripts/pdf_fill_form.py | 97 ++++ .../skills/pdf/scripts/pdf_form_layout.py | 168 +++++++ .../skills/pdf/scripts/pdf_make_form.py | 145 ++++++ .../server/skills/pdf/scripts/pdf_merge.py | 50 +++ .../server/skills/pdf/scripts/pdf_meta.py | 115 +++++ .../skills/pdf/scripts/pdf_page_image.py | 99 +++++ .../server/skills/pdf/scripts/pdf_read.py | 153 +++++++ .../server/skills/pdf/scripts/pdf_secure.py | 71 +++ .../server/skills/pdf/scripts/pdf_split.py | 84 ++++ .../server/skills/pdf/scripts/pdf_stamp.py | 143 ++++++ .../skills/pdf/scripts/pdf_watermark.py | 51 +++ packages/server/skills/xlsx/LICENSE | 21 + packages/server/skills/xlsx/SKILL.md | 203 +++++++++ packages/server/skills/xlsx/SOURCE.json | 11 + .../skills/xlsx/references/restructuring.md | 71 +++ .../server/skills/xlsx/scripts/csv_to_xlsx.py | 104 +++++ .../server/skills/xlsx/scripts/xlsx_create.py | 259 +++++++++++ .../server/skills/xlsx/scripts/xlsx_edit.py | 263 +++++++++++ .../server/skills/xlsx/scripts/xlsx_read.py | 160 +++++++ .../server/skills/xlsx/scripts/xlsx_recalc.py | 111 +++++ .../skills/xlsx/scripts/xlsx_restructure.py | 337 ++++++++++++++ .../server/skills/xlsx/scripts/xlsx_to_csv.py | 69 +++ .../lib/__tests__/agent-environment.test.ts | 19 + .../src/lib/__tests__/agent-settings.test.ts | 18 + .../__tests__/deepseek-capabilities.test.ts | 59 +++ .../lib/__tests__/public-agent-skills.test.ts | 22 + .../lib/__tests__/python-environment.test.ts | 14 + packages/server/src/lib/agent-environment.ts | 40 ++ packages/server/src/lib/agent-settings.ts | 22 +- .../server/src/lib/deepseek-capabilities.mts | 22 +- packages/server/src/lib/deepseek-harness.mts | 34 +- .../server/src/lib/public-agent-skills.ts | 22 + packages/server/src/lib/python-environment.ts | 91 ++++ .../src/lib/system-agent-environment.ts | 27 ++ packages/server/src/routes/agent.ts | 65 ++- packages/server/src/routes/system.ts | 18 + .../integration/agent-app-settings.test.ts | 89 ++++ .../web/app/(dashboard)/my/agent/page.tsx | 8 + .../web/app/(dashboard)/my/models/page.tsx | 11 +- .../app-settings/agent-settings.test.tsx | 30 ++ .../app-settings/agent-settings.tsx | 110 +++++ .../app-settings/app-settings-page.tsx | 12 +- .../web/components/shell/platform-agent.ts | 5 + .../web/components/shell/platform-shell.tsx | 4 +- .../system/agent-environment.test.tsx | 15 + .../components/system/agent-environment.tsx | 35 ++ .../system/python-environment-form.test.tsx | 23 + .../system/python-environment-form.tsx | 44 ++ .../web/components/system/system-page.tsx | 6 +- packages/web/components/ui/badge.tsx | 10 + packages/web/components/ui/switch.tsx | 20 + packages/web/package.json | 7 +- pnpm-lock.yaml | 168 +++++++ scripts/public-source-scan-baseline.json | 2 +- 96 files changed, 9875 insertions(+), 32 deletions(-) create mode 100644 docs/agent-document-skills.md create mode 100644 packages/server/skills/docx/LICENSE create mode 100644 packages/server/skills/docx/SKILL.md create mode 100644 packages/server/skills/docx/SOURCE.json create mode 100644 packages/server/skills/docx/references/revisions-and-comments.md create mode 100644 packages/server/skills/docx/scripts/docx_comments.py create mode 100644 packages/server/skills/docx/scripts/docx_common.py create mode 100644 packages/server/skills/docx/scripts/docx_create.py create mode 100644 packages/server/skills/docx/scripts/docx_edit.py create mode 100644 packages/server/skills/docx/scripts/docx_read.py create mode 100644 packages/server/skills/docx/scripts/docx_revisions.py create mode 100644 packages/server/skills/docx/scripts/docx_template.py create mode 100644 packages/server/skills/docx/scripts/docx_validate.py create mode 100644 packages/server/skills/markitdown/LICENSE create mode 100644 packages/server/skills/markitdown/SKILL.md create mode 100644 packages/server/skills/markitdown/SOURCE.json create mode 100644 packages/server/skills/markitdown/references/api_reference.md create mode 100644 packages/server/skills/markitdown/references/cloud_and_ocr.md create mode 100644 packages/server/skills/markitdown/references/file_formats.md create mode 100644 packages/server/skills/markitdown/references/mcp_and_plugins.md create mode 100644 packages/server/skills/markitdown/references/migration.md create mode 100644 packages/server/skills/markitdown/references/security.md create mode 100644 packages/server/skills/markitdown/references/workflows.md create mode 100644 packages/server/skills/markitdown/scripts/batch_convert.py create mode 100644 packages/server/skills/markitdown/scripts/convert_literature.py create mode 100644 packages/server/skills/markitdown/scripts/inspect_installation.py create mode 100644 packages/server/skills/pdf/LICENSE create mode 100644 packages/server/skills/pdf/SKILL.md create mode 100644 packages/server/skills/pdf/SOURCE.json create mode 100644 packages/server/skills/pdf/references/forms.md create mode 100644 packages/server/skills/pdf/references/nano-pdf-editing.md create mode 100644 packages/server/skills/pdf/references/ocr-extraction.md create mode 100644 packages/server/skills/pdf/scripts/_raster.py create mode 100644 packages/server/skills/pdf/scripts/extract_marker.py create mode 100644 packages/server/skills/pdf/scripts/extract_pymupdf.py create mode 100644 packages/server/skills/pdf/scripts/pdf_create.py create mode 100644 packages/server/skills/pdf/scripts/pdf_fill_form.py create mode 100644 packages/server/skills/pdf/scripts/pdf_form_layout.py create mode 100644 packages/server/skills/pdf/scripts/pdf_make_form.py create mode 100644 packages/server/skills/pdf/scripts/pdf_merge.py create mode 100644 packages/server/skills/pdf/scripts/pdf_meta.py create mode 100644 packages/server/skills/pdf/scripts/pdf_page_image.py create mode 100644 packages/server/skills/pdf/scripts/pdf_read.py create mode 100644 packages/server/skills/pdf/scripts/pdf_secure.py create mode 100644 packages/server/skills/pdf/scripts/pdf_split.py create mode 100644 packages/server/skills/pdf/scripts/pdf_stamp.py create mode 100644 packages/server/skills/pdf/scripts/pdf_watermark.py create mode 100644 packages/server/skills/xlsx/LICENSE create mode 100644 packages/server/skills/xlsx/SKILL.md create mode 100644 packages/server/skills/xlsx/SOURCE.json create mode 100644 packages/server/skills/xlsx/references/restructuring.md create mode 100644 packages/server/skills/xlsx/scripts/csv_to_xlsx.py create mode 100644 packages/server/skills/xlsx/scripts/xlsx_create.py create mode 100644 packages/server/skills/xlsx/scripts/xlsx_edit.py create mode 100644 packages/server/skills/xlsx/scripts/xlsx_read.py create mode 100644 packages/server/skills/xlsx/scripts/xlsx_recalc.py create mode 100644 packages/server/skills/xlsx/scripts/xlsx_restructure.py create mode 100644 packages/server/skills/xlsx/scripts/xlsx_to_csv.py create mode 100644 packages/server/src/lib/__tests__/agent-environment.test.ts create mode 100644 packages/server/src/lib/__tests__/public-agent-skills.test.ts create mode 100644 packages/server/src/lib/__tests__/python-environment.test.ts create mode 100644 packages/server/src/lib/agent-environment.ts create mode 100644 packages/server/src/lib/public-agent-skills.ts create mode 100644 packages/server/src/lib/python-environment.ts create mode 100644 packages/server/src/lib/system-agent-environment.ts create mode 100644 packages/server/tests/integration/agent-app-settings.test.ts create mode 100644 packages/web/app/(dashboard)/my/agent/page.tsx create mode 100644 packages/web/components/app-settings/agent-settings.test.tsx create mode 100644 packages/web/components/app-settings/agent-settings.tsx create mode 100644 packages/web/components/system/agent-environment.test.tsx create mode 100644 packages/web/components/system/agent-environment.tsx create mode 100644 packages/web/components/system/python-environment-form.test.tsx create mode 100644 packages/web/components/system/python-environment-form.tsx create mode 100644 packages/web/components/ui/badge.tsx create mode 100644 packages/web/components/ui/switch.tsx diff --git a/docs/agent-document-skills.md b/docs/agent-document-skills.md new file mode 100644 index 00000000..e0dd9d10 --- /dev/null +++ b/docs/agent-document-skills.md @@ -0,0 +1,48 @@ +# 应用 Agent 的 Skills 和工具设置 + +LocalApp 随 Server 提供四个公共文档 skill:MarkItDown、PDF、DOCX 和 XLSX。来源、固定 commit、许可证和 LocalApp 的改动记录分别保存在 `packages/server/skills//SOURCE.json` 与 `LICENSE` 中;发布包包含完整 instructions、scripts 和 references。 + +应用在 manifest 声明所需能力,例如: + +```json +{ "agent": { "capabilities": ["files", "terminal", "skills"] } } +``` + +用户在应用设置页的 **Agent** 标签,或应用聊天区的 **应用 Agent 设置** 入口,选择自己的能力、公共 skills 和工具。其他用户分别保存自己的选择。公共 skills 默认不启用;能力仍需应用声明并经当前用户启用。已有工作区 `skills/` 中的自定义 skills 保持原有加载方式。 + +关闭单个工具后,Server 从模型可见的工具列表中移除它,并阻止直接调用。应用工具在应用注册工具时同步到当前用户的设置列表;旧版应用首次发起请求后也会同步。选择新能力并保存后,会列出相应 DSH 工具。更改设置会关闭该应用当前用户的 Agent 实例,下一次请求使用新配置;对话仍然保留。 + +模型供应商、凭据和 MCP 连接仍在 **模型与 Agent** 页配置。保存供应商不会覆盖应用的 Skills 和工具选择。 + +## 文档运行环境 + +Skills 的 Python 脚本已包含在包内,Python、第三方 Python 库、LibreOffice 和 Poppler 不随 npm 包提供。启用 skill 不等于这些依赖已安装。需要 Python 3.10+,并且该 Python 的安装目录必须能被现有应用终端沙箱读取。脚本、输入文件、输出文件、缓存和虚拟环境都在当前用户的应用工作区内运行。 + +可在工作区创建虚拟环境后安装需要的库: + +```bash +python3 -m venv .venv +.venv/bin/python -m pip install 'markitdown[pdf,docx,xlsx]==0.1.8' pypdf reportlab pdfplumber python-docx openpyxl +``` + +Windows 的虚拟环境命令路径为 `.venv/Scripts/python.exe`,但当前终端能力仍取决于 Server 是否支持文件读取隔离。不得为了运行文档脚本绕过沙箱。 + +PDF、DOCX、XLSX skill 中的 `scripts/` 路径相对于 skill 目录,例如 `.localapp-public-skills/pdf/scripts/pdf_read.py`。优先提取本地文本和表格;扫描件需要 OCR,文档视觉检查需要页面渲染与可用的图片理解工具。外部 OCR、云转换和额外模型服务不会自动配置。 + +本次接入公共 skills 和设置选择,不增加聊天附件上传或模型图片输入传输。文件需已在 Agent 工作区内;这些传输能力需要另外接入。 + +### 系统文档处理环境 + +文档 Skills 由 LocalApp 提供。管理员在“系统设置 → Python 环境”检查全部文档 Skills 的 Python 3.10+ 和依赖,结果适用于所有用户和应用。普通用户不能访问系统检查接口或查看解释器路径;应用设置只显示 Skill 是否可用。 + +检查使用 Server 的 DSH 工作区执行器,不安装软件或请求模型,不依赖用户的模型配置。结果缓存 15 秒。依赖缺失时,应用内无法开启对应 Skill;后端保存时同样校验,并从 Agent 实际加载的列表中移除依赖失效的 Skill。 + +检查在同一个 Python 中导入依赖,不拼接不同解释器的依赖。PDF 渲染、LibreOffice 等可选依赖单独显示,不阻止基本 Skill 开启。环境就绪不代表附件上传、OCR 或真实文档处理已经通过验证。 + +### 通用 Python 虚拟环境 + +管理员可以选择已有 venv 的 Python 绝对路径,或指定 Python 3.10+ 的基础解释器创建 LocalApp 专用 venv。配置属于 Server,供所有应用的 Python 脚本共用,不限于文档处理。专用环境放在 `dataDir/python-environments/`;配置保存在 `dataDir/python-environment.json`。 + +创建环境默认不安装文档库,可明确勾选安装,或稍后使用“安装文档 Skills 依赖”。安装只修改选定的虚拟环境,不向系统 Python 安装包。新建不覆盖现有目录;创建或安装失败不会替换当前环境。 + +执行器向命令提供虚拟环境的 PATH 和 VIRTUAL_ENV,并向模型说明解释器路径。环境及基础 Python 安装目录仅允许读取;应用工作区是可写目录,其他 Server 数据仍不能读取。配置变更后的后续 Agent 请求会重新创建运行时,使用新环境;环境检查只使用选定的解释器,不回退到其他 Python。 diff --git a/packages/sdk-agent/src/agent-controls.tsx b/packages/sdk-agent/src/agent-controls.tsx index 73080c91..be89c3be 100644 --- a/packages/sdk-agent/src/agent-controls.tsx +++ b/packages/sdk-agent/src/agent-controls.tsx @@ -5,7 +5,7 @@ import type { AgentInteraction, HarnessAgent } from "./harness-client.js"; export function AgentControls({ agent }: { agent: HarnessAgent | null }) { const [providers, setProviders] = useState>([]); const [sessions, setSessions] = useState>([]); - const [settingsUrl, setSettingsUrl] = useState("/my/models/"); + const [settingsUrl, setSettingsUrl] = useState("/my/models"); const [error, setError] = useState(""); const [, render] = useState(0); useEffect(() => { @@ -19,6 +19,7 @@ export function AgentControls({ agent }: { agent: HarnessAgent | null }) { const act = (operation: Promise) => { setError(""); operation.catch((e) => setError(e.message)); }; return
+ {agent && agent.appId !== "platform" && 应用 Agent 设置} 模型与 Agent 设置 setAppId(e.target.value)} />
- {Object.entries(settings.grants).map(([id, enabled]) =>
{id}
{Object.entries(capabilities).map(([key, label]) => )}
)} +

应用 Agent 设置

+

Skills、能力和工具在各应用的设置页选择,仅影响当前用户。

+
setAppId(event.target.value)} />
+ {Object.keys(settings.grants).map((id) => {id})}

MCP 服务

{settings.mcpServers.map((server, index) =>
diff --git a/packages/web/components/app-settings/agent-settings.test.tsx b/packages/web/components/app-settings/agent-settings.test.tsx new file mode 100644 index 00000000..21b990d1 --- /dev/null +++ b/packages/web/components/app-settings/agent-settings.test.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { expect, it, vi } from "vitest"; +import { AppAgentSettings } from "./agent-settings"; +it("saves skill, capability and individual tool choices for this application", async () => { + const value = { appId: "owner/demo", capabilities: ["skills", "files", "terminal"], enabledCapabilities: [], enabledSkills: [], disabledTools: [], skills: [{ id: "pdf", name: "PDF", description: "Read PDFs", dependencies: "Python" }], tools: [{ name: "deleteRecord", description: "Delete a record", source: "app" }] }; + const fetchMock = vi.fn(async (_url: unknown, options?: RequestInit) => new Response(JSON.stringify({ success: true, data: options?.method === "PUT" ? { ...value, ...JSON.parse(String(options.body)) } : value }))); + vi.stubGlobal("fetch", fetchMock); + render(); + fireEvent.click(await screen.findByLabelText("Skills")); + fireEvent.click(screen.getByLabelText("PDF")); + fireEvent.click(screen.getByRole("switch", { name: "deleteRecord", exact: true })); + fireEvent.click(screen.getByRole("button", { name: "DSH", exact: true })); + expect(screen.queryByRole("switch", { name: "deleteRecord", exact: true })).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "保存 Agent 设置" })); + await screen.findByText("Agent 设置已保存"); + const body = JSON.parse(String(fetchMock.mock.calls.find((call) => call[1]?.method === "PUT")?.[1]?.body)); + expect(body).toEqual({ appId: "owner/demo", enabledCapabilities: ["skills"], enabledSkills: ["pdf"], disabledTools: ["deleteRecord"] }); +}); +it("keeps public skills unavailable when the application does not declare skills", async () => { + vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ success: true, data: { appId: "owner/demo", capabilities: [], enabledCapabilities: [], enabledSkills: [], disabledTools: [], skills: [{ id: "pdf", name: "PDF", description: "Read", dependencies: "Python" }], tools: [] } })))); + render(); + await waitFor(() => expect(screen.getByLabelText("PDF")).toBeDisabled()); +}); +it("prevents enabling a skill when LocalApp system dependencies are missing", async () => { + vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ success: true, data: { appId: "owner/demo", capabilities: ["skills", "files", "terminal"], enabledCapabilities: ["skills", "files", "terminal"], enabledSkills: [], disabledTools: [], skills: [{ id: "pdf", name: "PDF", description: "Read", dependencies: "Python", available: false, unavailableReason: "系统文档处理环境尚未就绪,请联系管理员检查依赖。" }], tools: [] } })))); + render(); + await waitFor(() => expect(screen.getByLabelText("PDF")).toBeDisabled()); + expect(screen.getByText("系统文档处理环境尚未就绪,请联系管理员检查依赖。")).toBeInTheDocument(); +}); diff --git a/packages/web/components/app-settings/agent-settings.tsx b/packages/web/components/app-settings/agent-settings.tsx new file mode 100644 index 00000000..8f838322 --- /dev/null +++ b/packages/web/components/app-settings/agent-settings.tsx @@ -0,0 +1,110 @@ +"use client"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Bot, FileText, FileSpreadsheet, FileType, FolderOpen, Terminal, Search, Wrench, Sparkles, Save, Check, ChevronDown, ChevronUp, Settings2 } from "lucide-react"; + +type Settings = { + appId: string; + capabilities: string[]; + enabledCapabilities: string[]; + enabledSkills: string[]; + disabledTools: string[]; + skills: Array<{ id: string; name: string; description: string; dependencies: string; available?: boolean; unavailableReason?: string }>; + toolsNotice?: string; + tools: Array<{ name: string; description: string; source: string }>; +}; +const labels: Record = { files: "工作区文件", terminal: "终端执行", skills: "Skills", mcp: "MCP 服务", subagents: "子 Agent", jobs: "后台任务", web: "网页访问", workflow: "工作流", schedule: "定时任务" }; +export function AppAgentSettings({ appId }: { appId: string }) { + const [settings, setSettings] = useState(); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + const [saved, setSaved] = useState(false); + const [query, setQuery] = useState(""); + const [source, setSource] = useState("all"); + const [showRequirements, setShowRequirements] = useState(false); + const [expandedTools, setExpandedTools] = useState([]); + useEffect(() => { + let live = true; + setSettings(undefined); setError(""); setSaved(false); + fetch(`/api/agent/app-settings?${new URLSearchParams({ appId })}`, { credentials: "include" }).then(async (response) => { + const body = await response.json(); + if (!response.ok || !body.success) throw new Error(body.error || "加载失败"); + if (live) setSettings(body.data); + }).catch((error) => { if (live) setError(error.message); }); + return () => { live = false; }; + }, [appId]); + const toggle = (field: "enabledCapabilities" | "enabledSkills" | "disabledTools", id: string) => { + setSaved(false); + setSettings((current) => current && ({ ...current, [field]: current[field].includes(id) ? current[field].filter((value) => value !== id) : [...current[field], id] })); + }; + const save = async () => { + setBusy(true); setError(""); setSaved(false); + try { + const response = await fetch("/api/agent/app-settings", { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ appId, enabledCapabilities: settings?.enabledCapabilities, enabledSkills: settings?.enabledSkills, disabledTools: settings?.disabledTools }) }); + const body = await response.json(); + if (!response.ok || !body.success) throw new Error(body.error || "保存失败"); + setSettings(body.data); setSaved(true); + } catch (error) { setError((error as Error).message); } + finally { setBusy(false); } + }; + const tools = settings?.tools.filter((tool) => (source === "all" || tool.source === source) && `${tool.name} ${tool.description} ${toolLabels[tool.name] ?? ""}`.toLowerCase().includes(query.toLowerCase())) ?? []; + return
+
+ +

为你在此应用中的 Agent 选择能力和工具。

仅对你的账号生效。管理模型供应商

+
+ {error &&

{error}

} + {!settings ? !error &&
: <> + + 可用能力{settings.enabledCapabilities.length} / {settings.capabilities.length}应用声明的能力,需要你开启后才会使用。 + + {settings.capabilities.length ?
{settings.capabilities.map((id) => { + const Icon = id === "files" ? FolderOpen : id === "terminal" ? Terminal : id === "skills" ? Sparkles : Wrench; + return
toggle("enabledCapabilities", id)} />
; + })}
:

应用尚未声明 Agent 扩展能力。

} +
+
+ + 公共文档 Skills{settings.enabledSkills.length} 已选按需开启文档处理,默认关闭。 + + {!settings.capabilities.includes("skills") &&

此应用尚未开放 Skills 能力。

} +
+ {settings.skills.map((skill) => { + const Icon = skill.id === "xlsx" ? FileSpreadsheet : skill.id === "docx" ? FileType : FileText; + const selected = settings.enabledSkills.includes(skill.id); + return
+
toggle("enabledSkills", skill.id)} />
+

{skill.description}

{skill.available === false &&

{skill.unavailableReason}

} +
; + })} +
+ + {showRequirements &&

运行脚本需要开启 Skills、工作区文件和终端执行,系统依赖由管理员在系统设置中检查。扫描件需要 OCR;图片理解还需要应用和模型支持图片输入。

{settings.skills.map((skill) =>

{skill.name}:{skill.dependencies}

)}
} +
+
+ + 工具{settings.tools.filter((tool) => !settings.disabledTools.includes(tool.name)).length} 已开启关闭后,Agent 无法调用对应工具。 + +
setQuery(event.target.value)} />
{[["all", "全部"], ["app", "应用"], ["dsh", "DSH"]].map(([id, text]) => )}
+ {settings.toolsNotice &&

{settings.toolsNotice}

} +
{tools.map((tool) => { + const key = `${tool.source}:${tool.name}`; + const expanded = expandedTools.includes(key); + return
{tool.source === "app" ? "应用" : "DSH"}
{toolLabels[tool.name] &&

{tool.name}

}
toggle("disabledTools", tool.name)} />
{expanded &&

{tool.description}

}
; + })}
+ {!tools.length &&

{query ? "没有匹配的工具" : "暂无工具"}

} +

打开应用后同步应用工具;开启能力并保存后显示相应 DSH 工具。

+
+
+
{saved ? <>Agent 设置已保存 : "选择仅用于当前用户和应用"}
+ } +
; +} + +const toolLabels: Record = { read: "读取文件", write: "写入文件", edit: "编辑文件", glob: "查找文件", grep: "搜索文件内容", bash: "执行命令", skill: "加载 Skill", ask_user_question: "向用户提问", exit_plan_mode: "确认执行计划", todo_write: "任务清单", get_goal: "查看目标", create_goal: "创建目标", update_goal: "更新目标", getCurrentUser: "当前用户", subagent: "创建子 Agent", subagent_fork: "复制子 Agent", workflow: "运行工作流" }; diff --git a/packages/web/components/app-settings/app-settings-page.tsx b/packages/web/components/app-settings/app-settings-page.tsx index b980c5dd..b49f0559 100644 --- a/packages/web/components/app-settings/app-settings-page.tsx +++ b/packages/web/components/app-settings/app-settings-page.tsx @@ -35,7 +35,9 @@ type DataState = { files: { count: number; size: number }; backups: Backup[]; }; -type Tab = "info" | "general" | "access" | "database" | "notify" | "data" | "manage"; +import { AppAgentSettings } from "./agent-settings"; + +type Tab = "agent" | "info" | "general" | "access" | "database" | "notify" | "data" | "manage"; const tabs: Array<{ id: Tab; label: string }> = [ { id: "info", label: "应用信息" }, @@ -45,6 +47,7 @@ const tabs: Array<{ id: Tab; label: string }> = [ { id: "notify", label: "通知" }, { id: "data", label: "数据管理" }, { id: "manage", label: "应用管理" }, + { id: "agent", label: "Agent" }, ]; const accessLevels = ["public", "authenticated", "owner", "acl"]; @@ -72,6 +75,7 @@ export function AppSettingsPage({ name }: { name: string }) { const [data, setData] = useState(null); const [backupName, setBackupName] = useState(""); const importRef = useRef(null); + const tabsRef = useRef(null); const base = `/api/me/pages/${encodeURIComponent(name)}`; const load = async () => { @@ -86,6 +90,7 @@ export function AppSettingsPage({ name }: { name: string }) { const selected = new URLSearchParams(window.location.search).get("tab") as Tab | null; if (selected && tabs.some((item) => item.id === selected)) setTab(selected); }, []); + useEffect(() => { tabsRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: "nearest", inline: "nearest" }); }, [tab, !!settings]); useEffect(() => { if (tab === "data") void loadData().catch(() => toast.error("加载数据状态失败")); }, [tab]); const manifest = useMemo(() => view === "source" ? settings?.sourceManifest ?? {} : draft, [view, settings, draft]); @@ -160,7 +165,7 @@ export function AppSettingsPage({ name }: { name: string }) { const pageAccess = manifest.pageAccess ?? { level: "public", acl: [] }; const db = manifest.db ?? { mode: "crud", sqlAccess: "owner", defaultAccess: {} }; const notify = manifest.notify ?? { enabled: false }; - const manifestTab = !(["info", "data", "manage"] as Tab[]).includes(tab); + const manifestTab = !(["info", "data", "manage", "agent"] as Tab[]).includes(tab); return
@@ -179,7 +184,7 @@ export function AppSettingsPage({ name }: { name: string }) {
-
+
{tabs.map((item) => )}
@@ -187,6 +192,7 @@ export function AppSettingsPage({ name }: { name: string }) { {readonly ? 只读 : }
} + {tab === "agent" && } {tab === "info" &&

版本历史

{settings.app.versions.slice().reverse().map((version) =>
v{version.version}{new Date(version.createdAt).toLocaleString()} · {version.fileCount} 个文件 · {formatSize(version.totalSize)}
)}
diff --git a/packages/web/components/shell/platform-agent.ts b/packages/web/components/shell/platform-agent.ts index b4845f58..15d4bb8f 100644 --- a/packages/web/components/shell/platform-agent.ts +++ b/packages/web/components/shell/platform-agent.ts @@ -187,3 +187,8 @@ export function usePlatformAgent({ return { harness: agentRef.current, chatMessages, setChatMessages, isRunning, aiError, agentSend, handleToolResult }; } + +/** Keep the settings tool list available before a model request is sent. */ +export function registerAgentToolCatalog(appId: string, tools: Array<{ name: string; description: string }>) { + void fetch("/api/agent/app-tools", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ appId, tools: [...SYSTEM_TOOLS, ...tools.filter((tool) => !SYSTEM_TOOL_NAMES.has(tool.name))].map(({ name, description }) => ({ name, description })) }) }).catch(() => {}); +} diff --git a/packages/web/components/shell/platform-shell.tsx b/packages/web/components/shell/platform-shell.tsx index c6813a7a..637f514a 100644 --- a/packages/web/components/shell/platform-shell.tsx +++ b/packages/web/components/shell/platform-shell.tsx @@ -6,7 +6,7 @@ import { Navbar, type PlatformEditSession, type PresenceSnapshot } from "./navba import { IssuesModal } from "./issues-modal"; import { AiSidebar } from "./ai-sidebar"; import { NotificationBell } from "./notification-bell"; -import { usePlatformAgent } from "./platform-agent"; +import { usePlatformAgent, registerAgentToolCatalog } from "./platform-agent"; import { useAuthModals } from "@/components/auth-modals/auth-provider"; import { Button } from "@/components/ui/button"; import { CircleAlert, CircleOff, RefreshCw, Settings } from "lucide-react"; @@ -288,6 +288,7 @@ export function PlatformShell({ userId, name }: { userId: string; name: string } global[NATIVE_TOOL_REGISTRY_KEY] = { registerTools: (tools, executeFns, systemHint) => { registeredToolsRef.current = tools; + registerAgentToolCatalog(pagePath, tools); executeToolsRef.current = new Map(Object.entries(executeFns)); systemHintRef.current = systemHint || ""; setAiMode("system"); @@ -649,6 +650,7 @@ export function PlatformShell({ userId, name }: { userId: string; name: string } if (isRegisterToolsMessage(event.data)) { setAiMode("system"); registeredToolsRef.current = event.data.tools; + registerAgentToolCatalog(pagePath, event.data.tools); systemHintRef.current = event.data.systemHint || ""; } else if (isAiCustomModeMessage(event.data)) { setAiMode("custom"); diff --git a/packages/web/components/system/agent-environment.test.tsx b/packages/web/components/system/agent-environment.test.tsx new file mode 100644 index 00000000..10cf81ae --- /dev/null +++ b/packages/web/components/system/agent-environment.test.tsx @@ -0,0 +1,15 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { SystemAgentEnvironment } from "./agent-environment"; +afterEach(()=>{cleanup();vi.unstubAllGlobals();}); +it("shows missing dependencies and allows a fresh check",async()=>{ + const fetchMock=vi.fn().mockResolvedValue({ok:true,json:async()=>({data:{checkedAt:new Date().toISOString(),platform:"darwin",ready:false,checks:[{id:"pdfplumber",name:"pdfplumber",status:"missing",detail:"需要安装依赖",required:true}]}})}); + vi.stubGlobal("fetch",fetchMock);render(); + expect(await screen.findByText("尚未就绪")).toBeInTheDocument();expect(screen.getByText("pdfplumber")).toBeInTheDocument(); + expect(fetchMock).toHaveBeenCalledWith("/api/system/agent-environment",{credentials:"include"}); + fireEvent.click(screen.getByRole("button",{name:"重新检查"}));await waitFor(()=>expect(fetchMock.mock.calls.filter(call => call[0] === "/api/system/agent-environment")).toHaveLength(2)); +}); +it("shows a configuration or authorization error without claiming readiness",async()=>{ + vi.stubGlobal("fetch",vi.fn().mockResolvedValue({ok:false,json:async()=>({error:"需要管理员权限"})}));render(); + expect(await screen.findByRole("alert")).toHaveTextContent("需要管理员权限");expect(screen.queryByText("已就绪")).not.toBeInTheDocument(); +}); diff --git a/packages/web/components/system/agent-environment.tsx b/packages/web/components/system/agent-environment.tsx new file mode 100644 index 00000000..64b0e13f --- /dev/null +++ b/packages/web/components/system/agent-environment.tsx @@ -0,0 +1,35 @@ +"use client"; +import { useEffect, useState } from "react"; +import { RefreshCw, CheckCircle2, CircleAlert, Monitor } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { PythonEnvironmentForm } from "./python-environment-form"; +import { Skeleton } from "@/components/ui/skeleton"; +type Report = { checkedAt: string; platform: string; ready: boolean; checks: Array<{id:string;name:string;status:"ready"|"missing"|"blocked";detail:string;required:boolean}> }; +export function SystemAgentEnvironment() { + const [report,setReport]=useState(null); + const [busy,setBusy]=useState(false); + const [error,setError]=useState(""); + const check=async()=>{ + setBusy(true);setError("");setReport(null); + try { + const response=await fetch("/api/system/agent-environment",{credentials:"include"}); + const body=await response.json(); + if(!response.ok)throw new Error(body.error || "环境检查失败"); + setReport(body.data); + }catch(e){setError(e instanceof Error ? e.message : "环境检查失败");}finally{setBusy(false);} + }; + useEffect(()=>{setReport(null);void check();},[]); + return
+ + 环境检查命令在 LocalApp Server 所在电脑上执行。访问远程 Server 时,检查的是远程电脑,而非当前浏览器所在电脑。 +

检查 LocalApp 提供的全部文档 Skills 所需的 Python 和依赖,检查结果适用于所有应用。检查不会安装软件,也不会请求大模型。可选依赖缺失不影响基本就绪状态。

+
{report && {report.ready ? "已就绪":"尚未就绪"}}
+ {error &&

{error}

} + {report &&

Server 系统:{report.platform} · 检查时间:{new Date(report.checkedAt).toLocaleString()}。更新系统依赖后请重新检查;短时间内重复检查会使用缓存。

} +
+ {busy && !report && } + {report && {report.checks.map(check=>
{check.status==="ready" ? :}
{check.name}{check.status==="ready" ? "可用":check.status==="blocked" ? "未开启或无法执行":"缺少依赖"}{!check.required && 可选}

{check.detail}

)}
} +
; +} diff --git a/packages/web/components/system/python-environment-form.test.tsx b/packages/web/components/system/python-environment-form.test.tsx new file mode 100644 index 00000000..b81fd2e1 --- /dev/null +++ b/packages/web/components/system/python-environment-form.test.tsx @@ -0,0 +1,23 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, expect, it, vi } from "vitest"; +import { PythonEnvironmentForm } from "./python-environment-form"; +afterEach(()=>{cleanup();vi.unstubAllGlobals();}); +it("saves an existing virtual environment and refreshes the shared check",async()=>{ + const onUpdated=vi.fn(async()=>{}); + const request=vi.fn(async(_url,options)=>({ok:true,json:async()=>({data:{environment:options?.method ? {executable:"/env/bin/python",version:"3.12",directory:"/env"}:null}})})); + vi.stubGlobal("fetch",request);render(); + await waitFor(()=>expect(request).toHaveBeenCalledTimes(1)); + fireEvent.change(screen.getByLabelText("虚拟环境 Python 路径"),{target:{value:"/env/bin/python"}}); + fireEvent.click(screen.getByRole("button",{name:"保存并使用"})); + await waitFor(()=>expect(onUpdated).toHaveBeenCalledTimes(1)); + expect(request).toHaveBeenLastCalledWith("/api/system/python-environment",expect.objectContaining({method:"PUT",body:JSON.stringify({executable:"/env/bin/python"})})); +}); +it("creates an environment with dependency installation as an explicit choice",async()=>{ + const request=vi.fn(async()=>({ok:true,json:async()=>({data:{environment:null}})}));vi.stubGlobal("fetch",request); + render({}}/>); + fireEvent.click(screen.getByRole("button",{name:"创建专用虚拟环境"})); + expect(screen.getByLabelText("同时安装公共文档 Skills 依赖")).not.toBeChecked(); + fireEvent.change(screen.getByLabelText("基础 Python 路径(3.10+)"),{target:{value:"/base/python"}}); + fireEvent.click(screen.getByRole("button",{name:"创建并使用"})); + await waitFor(()=>expect(request).toHaveBeenCalledWith("/api/system/python-environment/create",expect.objectContaining({body:JSON.stringify({baseExecutable:"/base/python",installDocuments:false})}))); +}); diff --git a/packages/web/components/system/python-environment-form.tsx b/packages/web/components/system/python-environment-form.tsx new file mode 100644 index 00000000..900fd0ac --- /dev/null +++ b/packages/web/components/system/python-environment-form.tsx @@ -0,0 +1,44 @@ +"use client"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +type Environment = { executable:string; directory:string; version:string; managed:boolean }; +export function PythonEnvironmentForm({onUpdated}:{onUpdated:()=>Promise}) { + const [environment,setEnvironment]=useState(null); + const [executable,setExecutable]=useState(""); + const [baseExecutable,setBaseExecutable]=useState(""); + const [installDocuments,setInstallDocuments]=useState(false); + const [mode,setMode]=useState("existing"); + const [busy,setBusy]=useState(false); + const [error,setError]=useState(""); + const [message,setMessage]=useState(""); + useEffect(()=>{ + let live=true; + fetch("/api/system/python-environment",{credentials:"include"}).then(async response=>{ + const body=await response.json(); if(!response.ok)throw new Error(body.error || "加载 Python 配置失败"); + if(live){setEnvironment(body.data.environment ?? null);setExecutable(body.data.environment?.executable ?? "");} + }).catch(e=>{if(live)setError(e.message);}); + return ()=>{live=false;}; + },[]); + const update=async(action:"select"|"create"|"install")=>{ + setBusy(true);setError("");setMessage(""); + try { + const url=action === "select" ? "/api/system/python-environment" : action === "create" ? "/api/system/python-environment/create" : "/api/system/python-environment/install-document-dependencies"; + const response=await fetch(url,{method:action === "select" ? "PUT":"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(action === "select" ? {executable} : action === "create" ? {baseExecutable,installDocuments} : {})}); + const body=await response.json();if(!response.ok)throw new Error(body.error || "更新失败"); + setEnvironment(body.data.environment);setExecutable(body.data.environment.executable);setMessage("Python 环境已更新,后续 Agent 命令将使用此环境。");await onUpdated(); + }catch(e){setError(e instanceof Error ? e.message : "更新失败");}finally{setBusy(false);} + }; + return Python 环境LocalApp Server 的通用 Python 虚拟环境,供所有应用的 Agent 脚本使用。 +

{environment ? `当前环境:Python ${environment.version} · ${environment.executable}` : "尚未配置专用虚拟环境;当前使用 Server 可找到的 Python。"}

+
+ {mode === "existing" ?
setExecutable(e.target.value)} placeholder="/path/to/.venv/bin/python" disabled={busy}/>
:
setBaseExecutable(e.target.value)} placeholder="/path/to/python3" disabled={busy}/>

在 LocalApp 数据目录创建独立虚拟环境,不覆盖已有目录。安装依赖时会从 PyPI 下载包。

} + {environment && } + {busy &&

正在更新 Python 环境,安装依赖可能需要几分钟…

} + {error &&

{error}

} + {message &&

{message}

} +
; +} diff --git a/packages/web/components/system/system-page.tsx b/packages/web/components/system/system-page.tsx index b30853cf..bbeebefa 100644 --- a/packages/web/components/system/system-page.tsx +++ b/packages/web/components/system/system-page.tsx @@ -6,9 +6,13 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { SystemAgentEnvironment } from "./agent-environment"; + type Settings = { listenHost: string; listenPort: number; publicUrl: string; workspaceDir: string; allowInsecureLan: boolean }; export function SystemPage() { + const [tab,setTab]=useState("network"); + useEffect(() => { if (new URLSearchParams(window.location.search).get("tab") === "environment") setTab("environment"); }, []); const [settings, setSettings] = useState(null); const [error, setError] = useState(""); const [message, setMessage] = useState(""); @@ -39,5 +43,5 @@ export function SystemPage() { finally { setSaving(false); } }; if (!settings) return

系统设置

{loading ?

加载中...

: <>

{error || "加载系统设置失败"}

}
; - return

系统设置

网络变更会由 Server 安全地暂存并请求受监督的重启。

网络

setSettings({ ...settings, listenHost: event.target.value })} />
setSettings({ ...settings, listenPort: Number(event.target.value) })} />

工作区目录:{settings.workspaceDir}

{error &&

{error}

}{message &&

{message}

}
; + return

系统设置

网络变更会由 Server 安全地暂存并请求受监督的重启。

{tab === "environment" ? :

网络

setSettings({ ...settings, listenHost: event.target.value })} />
setSettings({ ...settings, listenPort: Number(event.target.value) })} />

工作区目录:{settings.workspaceDir}

{error &&

{error}

}{message &&

{message}

}
}
; } diff --git a/packages/web/components/ui/badge.tsx b/packages/web/components/ui/badge.tsx new file mode 100644 index 00000000..e1ce50ef --- /dev/null +++ b/packages/web/components/ui/badge.tsx @@ -0,0 +1,10 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; +const badgeVariants = cva("inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium", { + variants: { variant: { default: "border-transparent bg-primary text-primary-foreground", secondary: "border-transparent bg-secondary text-secondary-foreground", outline: "text-muted-foreground" } }, + defaultVariants: { variant: "secondary" }, +}); +export function Badge({ className, variant, ...props }: React.HTMLAttributes & VariantProps) { + return ; +} diff --git a/packages/web/components/ui/switch.tsx b/packages/web/components/ui/switch.tsx new file mode 100644 index 00000000..8793f8d5 --- /dev/null +++ b/packages/web/components/ui/switch.tsx @@ -0,0 +1,20 @@ +"use client"; + +import * as React from "react"; +import * as SwitchPrimitive from "@radix-ui/react-switch"; +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitive.Root.displayName; +export { Switch }; diff --git a/packages/web/package.json b/packages/web/package.json index 13844922..3742a8e0 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -11,8 +11,10 @@ "test": "vitest run" }, "dependencies": { + "@localapp/sdk-agent": "workspace:*", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.3.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.556.0", @@ -21,12 +23,11 @@ "react": "^18.3.0", "react-dom": "^18.3.0", "react-markdown": "^10.1.0", - "remark-parse": "^11.0.0", "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", - "unified": "^11.0.5", - "@localapp/sdk-agent": "workspace:*" + "unified": "^11.0.5" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2669920..ebf416da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -593,6 +593,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.4 version: 1.2.4(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.3.8 + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -4045,6 +4048,9 @@ packages: '@radix-ui/primitive@1.1.4': resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} + '@radix-ui/primitive@1.1.7': + resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} + '@radix-ui/react-accessible-icon@1.1.9': resolution: {integrity: sha512-5W9KzJz/3DeYbGJHbZv8Q6AkxMOKUmALfc+PRg9dWwJZMk6zD37Sz8sZrF7UD6CBkiJvn7dNeRzn5G7XiCMyig==} peerDependencies: @@ -4180,6 +4186,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context-menu@2.3.0': resolution: {integrity: sha512-d7CouXhAW+CGmFOqmB+IEvd3E9GcaqfgvfjCc3hfulp2pkaUCEVEGa0SN5nNWYA+IvQ6g1Pt+S5dpNn1AoY9hg==} peerDependencies: @@ -4220,6 +4235,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.2.2': + resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.15': resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: @@ -4551,6 +4575,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.11': + resolution: {integrity: sha512-RzcaSw5aKl0OPhBJ1wbolKSF6nVvN+0o/QLeIVq+09npiPdnCkPggy2k6gI6yetxl4ZyrlBPYFgCB8BQJZwXcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -4708,6 +4745,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.4.0': + resolution: {integrity: sha512-IuMVHZ+Ah5fkctu3X3qeFrV/ZrBcuWuDTFUUPfS/L5hvoTjzhp/HdWGE14CsQombPdEidsa33HVlYZXi49PZ1Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.3.0': resolution: {integrity: sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA==} peerDependencies: @@ -4721,6 +4767,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-switch@1.3.8': + resolution: {integrity: sha512-rr+zuoAeHWK3m6H4HgjOUiu3VsrQ/9JkjwY/uMnTgHJGPe9WNzDP/MOuIV7S+65p42DIxjGL0hAHktqBq82+2A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tabs@1.1.14': resolution: {integrity: sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA==} peerDependencies: @@ -4835,6 +4894,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.2.6': + resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-effect-event@0.0.2': resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} peerDependencies: @@ -4853,6 +4921,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.1': resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} peerDependencies: @@ -4898,6 +4975,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-previous@1.1.2': resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} peerDependencies: @@ -4925,6 +5011,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-size@1.1.5': + resolution: {integrity: sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-visually-hidden@1.2.5': resolution: {integrity: sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg==} peerDependencies: @@ -12407,6 +12502,8 @@ snapshots: '@radix-ui/primitive@1.1.4': {} + '@radix-ui/primitive@1.1.7': {} + '@radix-ui/react-accessible-icon@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12534,6 +12631,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.29 + '@radix-ui/react-compose-refs@1.1.5(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + '@radix-ui/react-context-menu@2.3.0(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -12565,6 +12668,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.29 + '@radix-ui/react-context@1.2.2(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -12935,6 +13044,15 @@ snapshots: '@types/react': 18.3.29 '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@radix-ui/react-primitive@2.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.4.0(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) @@ -13103,6 +13221,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.29 + '@radix-ui/react-slot@1.4.0(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + '@radix-ui/react-switch@1.3.0(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -13118,6 +13243,20 @@ snapshots: '@types/react': 18.3.29 '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@radix-ui/react-switch@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-context': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@radix-ui/react-tabs@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.4 @@ -13243,6 +13382,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.29 + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.29)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) @@ -13257,6 +13405,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.29 + '@radix-ui/react-use-effect-event@0.0.5(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.29)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) @@ -13289,6 +13444,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.29 + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@18.3.29)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + '@radix-ui/react-use-previous@1.1.2(@types/react@18.3.29)(react@18.3.1)': dependencies: react: 18.3.1 @@ -13309,6 +13470,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.29 + '@radix-ui/react-use-size@1.1.5(@types/react@18.3.29)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.29)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + '@radix-ui/react-visually-hidden@1.2.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/scripts/public-source-scan-baseline.json b/scripts/public-source-scan-baseline.json index df3f55b5..1895f46f 100644 --- a/scripts/public-source-scan-baseline.json +++ b/scripts/public-source-scan-baseline.json @@ -141,7 +141,7 @@ { "path": "packages/server/src/lib/deepseek-harness.mts", "rule": "POSSIBLE_CREDENTIAL", - "sha256": "991612c70087197597a119553abd9e83e694f6388e1c7a533ac04bfb3d56f4ab", + "sha256": "169a0d027646a9484299af0207e3ed3a064ab4ebea70fed56b54e4ae97c31854", "reason": "apiKeyEnv names the LOCALAPP_USER_MODEL_KEY environment variable; it contains no credential value." } ] From 81c5687737ba44b99144e4db7dcbe4882d59f876 Mon Sep 17 00:00:00 2001 From: Patodo Date: Fri, 9 Oct 2026 17:23:33 +0900 Subject: [PATCH 2/2] fix(agent): omit model credential from environment-only checks --- packages/server/src/lib/system-agent-environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/lib/system-agent-environment.ts b/packages/server/src/lib/system-agent-environment.ts index 3e795f83..9db2d977 100644 --- a/packages/server/src/lib/system-agent-environment.ts +++ b/packages/server/src/lib/system-agent-environment.ts @@ -10,7 +10,7 @@ export async function systemAgentEnvironment(dataDir: string): Promise{ const { DeepSeekHarness }=await import("./deepseek-harness.mjs"); - const harness=new DeepSeekHarness({llmApiKey:"environment-check",llmBaseUrl:"http://127.0.0.1:9",llmModel:"environment-check",root:path.join(dataDir,"agent","system-environment"),capabilities:["files","terminal","skills"],skills:PUBLIC_AGENT_SKILLS.map(s=>s.id),pythonEnvironment}); + const harness=new DeepSeekHarness({llmApiKey:"",llmBaseUrl:"http://127.0.0.1:9",llmModel:"environment-check",root:path.join(dataDir,"agent","system-environment"),capabilities:["files","terminal","skills"],skills:PUBLIC_AGENT_SKILLS.map(s=>s.id),pythonEnvironment}); try{ await harness.initialize(); const report=await harness.checkEnvironment();