mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-14 08:26:07 +00:00
feat(mcp): support mcp resources (#2215)
* feat(mcp): support mcp resources * feat(web): split MCP resources into tab * docs: add MCP resources PR review * feat(mcp): productionize resource support * feat(mcp): scope local agent tools and resources * fix(web): gate space embedding models behind login * fix(web): prevent clipped space model CTA * test: update preproc resource tool expectations * fix(web): expose skill authoring tools in selector --------- Co-authored-by: yang.xiang <yang.xiang@advancegroup.com> Co-authored-by: Hyu <chenhyu@proton.me> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
# MCP Resources PR #2215 Review
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-29
|
||||||
|
> 分支: `mcp_resources`
|
||||||
|
> PR: langbot-app/LangBot#2215
|
||||||
|
> 主题: MCP Resources 在 LangBot 中的产品价值、AgentRunner 集成方式与后续架构方向
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
PR #2215 对 LangBot 有明确价值:它补齐了 MCP 协议中 Resources 这一重要能力,让 MCP server 不再只暴露 tools,也可以暴露文档、代码片段、配置、日志、图片等上下文资源。管理端可以发现和预览资源,Agent 也可以通过当前实现按需列出和读取资源。
|
||||||
|
|
||||||
|
但当前 AgentRunner 层的接入方式更接近一个可用的第一阶段方案,而不是最终架构。现在 MCP Resources 被包装成两个 synthetic tools:
|
||||||
|
|
||||||
|
- `langbot_mcp_list_resources`
|
||||||
|
- `langbot_mcp_read_resource`
|
||||||
|
|
||||||
|
这让模型可以通过 function calling 主动探索资源,落地成本低,也复用了已有 `ToolManager` / `LocalAgentRunner` 的工具调用链路。不过从 MCP 规范和主流实现来看,Resources 更适合作为一种一等上下文来源,而不是长期隐藏在工具列表里。
|
||||||
|
|
||||||
|
建议保留当前 synthetic tools 作为探索能力,同时把后续主线设计调整为:MCP Resources 是 pipeline / conversation / message 级别可选择、可固定、可审计的上下文输入。
|
||||||
|
|
||||||
|
## 当前实现判断
|
||||||
|
|
||||||
|
当前 AgentRunner 集成路径如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Pipeline 绑定 MCP server
|
||||||
|
-> query.variables['_pipeline_bound_mcp_servers']
|
||||||
|
-> Preproc 为 local-agent 加载工具
|
||||||
|
-> ToolManager.get_all_tools()
|
||||||
|
-> MCPLoader 注入 synthetic resource tools
|
||||||
|
-> LocalAgentRunner 将工具 schema 传给模型
|
||||||
|
-> 模型发起 list/read tool call
|
||||||
|
-> ToolManager.execute_func_call()
|
||||||
|
-> MCPLoader 调 MCP session.list_resources/read_resource
|
||||||
|
-> tool result 回灌给模型
|
||||||
|
```
|
||||||
|
|
||||||
|
这个路径的优点是:
|
||||||
|
|
||||||
|
- 复用现有工具调用机制,改动范围小。
|
||||||
|
- Agent 可以按需探索资源,不需要每轮预先读取所有资源。
|
||||||
|
- 可以沿用 pipeline 绑定的 MCP server 范围,避免越权读取未绑定 server。
|
||||||
|
- 对已有 MCP tools 行为影响较小。
|
||||||
|
|
||||||
|
主要问题是:
|
||||||
|
|
||||||
|
- Resources 在语义上被降级成 tools,和 MCP 规范里的 resource primitive 不完全一致。
|
||||||
|
- 模型必须先理解并主动调用 `list/read`,资源不会自然成为上下文。
|
||||||
|
- pipeline 不能配置“默认携带某些资源”或“本轮附加某些资源”。
|
||||||
|
- UI 资源 tab 目前是管理端预览能力,和 Agent 上下文选择没有打通。
|
||||||
|
- 对 blob、图片、大文件、结构化资源的处理还比较粗糙。
|
||||||
|
- 缺少 resource templates、订阅更新、缓存、chunk、token budget、trace 与审计策略。
|
||||||
|
|
||||||
|
## 主流项目做法
|
||||||
|
|
||||||
|
### MCP 官方规范
|
||||||
|
|
||||||
|
MCP Resources 是 server 暴露上下文数据的协议能力。规范没有要求 resources 必须以 tool call 形式给模型使用,而是把如何选择、过滤、读取和纳入上下文交给 Host application。
|
||||||
|
|
||||||
|
这意味着比较正统的集成方式是:LangBot 作为 Host,在 pipeline、会话或消息层决定哪些 resources 进入模型上下文。
|
||||||
|
|
||||||
|
参考: https://modelcontextprotocol.io/specification/2025-06-18/server/resources
|
||||||
|
|
||||||
|
### VS Code Copilot
|
||||||
|
|
||||||
|
VS Code 把 MCP Resources 做成 chat context 的一部分。用户可以通过 `Add Context > MCP Resources` 或命令浏览 MCP resources,并把选中的资源附加到一次 chat request。
|
||||||
|
|
||||||
|
这是目前最值得 LangBot 参考的产品形态:资源不是模型工具,而是用户和 Host 可控的上下文附件。
|
||||||
|
|
||||||
|
参考: https://code.visualstudio.com/docs/agent-customization/mcp-servers
|
||||||
|
|
||||||
|
### Anthropic SDK
|
||||||
|
|
||||||
|
Anthropic 的 client-side MCP helpers 提供资源读取和转换能力,例如把 MCP resource 转为 Claude message content 或 file。也就是说,应用先读取 resource,再显式放进模型消息。
|
||||||
|
|
||||||
|
这同样是 application-owned context injection,而不是把 resource 伪装成模型工具。
|
||||||
|
|
||||||
|
参考: https://platform.claude.com/docs/en/agents-and-tools/mcp-connector
|
||||||
|
|
||||||
|
### LangChain MCP Adapters
|
||||||
|
|
||||||
|
LangChain 把 MCP Resources 更像 data loader / document input 来处理,可以把资源加载成 `Blob`,再进入 LangChain 的文档、检索或上下文处理链路。
|
||||||
|
|
||||||
|
这说明 Resources 很适合作为知识源、文档源或上下文源,而不只是即时工具调用。
|
||||||
|
|
||||||
|
参考: https://docs.langchain.com/oss/python/langchain/mcp
|
||||||
|
|
||||||
|
### OpenAI Agents SDK
|
||||||
|
|
||||||
|
OpenAI Agents SDK 主路径仍偏向 MCP tools,但底层 MCP server API 已经有 `list_resources`、`list_resource_templates`、`read_resource` 等能力。当前形态说明 resources 是 client 能力,但并未默认变成 agent-visible tools。
|
||||||
|
|
||||||
|
参考: https://openai.github.io/openai-agents-python/mcp/
|
||||||
|
|
||||||
|
### Cline
|
||||||
|
|
||||||
|
Cline 会拉取 MCP tools、resources、resourceTemplates、prompts,并通过类似 `access_mcp_resource` 的内置访问方式让模型读取资源。这个方向和 LangBot 当前 synthetic tools 比较接近。
|
||||||
|
|
||||||
|
这种模式适合让 Agent 自主探索,但更像 Host 自定义的模型访问协议,不应成为唯一集成路径。
|
||||||
|
|
||||||
|
参考: https://github.com/cline/cline/blob/main/src/services/mcp/McpHub.ts
|
||||||
|
|
||||||
|
## 建议架构方向
|
||||||
|
|
||||||
|
### 1. 保留探索型工具
|
||||||
|
|
||||||
|
保留当前两个 synthetic tools:
|
||||||
|
|
||||||
|
- `langbot_mcp_list_resources`
|
||||||
|
- `langbot_mcp_read_resource`
|
||||||
|
|
||||||
|
它们适合处理“用户没有显式选择资源,但 Agent 判断需要探索 MCP server 上下文”的场景。后续可以优化工具描述、返回格式、资源大小限制和错误信息。
|
||||||
|
|
||||||
|
### 2. 增加一等 Resource Context
|
||||||
|
|
||||||
|
新增一个 Host 层资源上下文概念,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PipelineResourceBinding
|
||||||
|
ConversationResourceAttachment
|
||||||
|
MessageResourceAttachment
|
||||||
|
```
|
||||||
|
|
||||||
|
Preproc 或独立的 `ResourceContextProvider` 在模型调用前读取这些资源,按 MIME 类型、大小、token budget 转为模型可消费的上下文。
|
||||||
|
|
||||||
|
### 3. 打通 UI 与 Agent 上下文
|
||||||
|
|
||||||
|
当前 MCP 详情页的 Resources tab 可以继续作为资源发现和预览入口。建议增加操作:
|
||||||
|
|
||||||
|
- 添加到本轮上下文
|
||||||
|
- 固定到当前 pipeline
|
||||||
|
- 固定到当前 bot / conversation
|
||||||
|
- 查看资源读取历史和错误
|
||||||
|
|
||||||
|
这样 UI 资源管理能力才能真正影响 Agent 行为。
|
||||||
|
|
||||||
|
### 4. 支持 resource templates
|
||||||
|
|
||||||
|
MCP resource templates 允许 server 暴露参数化资源,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
repo://{owner}/{repo}/file/{path}
|
||||||
|
log://{service}/{date}
|
||||||
|
```
|
||||||
|
|
||||||
|
LangBot 后续应支持模板发现、参数填写、实例化和绑定。否则只能使用静态 resources,覆盖面会受限。
|
||||||
|
|
||||||
|
### 5. 增加资源处理策略
|
||||||
|
|
||||||
|
建议补齐:
|
||||||
|
|
||||||
|
- 文本资源 token budget 与截断策略。
|
||||||
|
- 大文件 chunk 与摘要策略。
|
||||||
|
- 图片/blob 的模型能力判断与 fallback。
|
||||||
|
- MIME 类型白名单与安全限制。
|
||||||
|
- 缓存与过期策略。
|
||||||
|
- `resources/listChanged` 或订阅更新。
|
||||||
|
- resource read trace,便于审计 Agent 读取了什么上下文。
|
||||||
|
|
||||||
|
## 推荐落地顺序
|
||||||
|
|
||||||
|
### Phase 1: 完成当前 PR 可用性
|
||||||
|
|
||||||
|
- 保留 synthetic tools。
|
||||||
|
- 明确文档说明当前 Agent 集成是 tool-mediated。
|
||||||
|
- 完善资源工具描述,降低模型误用概率。
|
||||||
|
- 给 read/list 增加大小限制和更清晰的 MIME 处理。
|
||||||
|
- 前端 Resources tab 与 Tools tab 分离,保持管理端清晰。
|
||||||
|
|
||||||
|
### Phase 2: 做 Host-owned context attachments
|
||||||
|
|
||||||
|
- 在 pipeline 或 conversation 层新增 resource attachment 配置。
|
||||||
|
- Preproc 读取已绑定 resources,注入模型上下文。
|
||||||
|
- UI 支持“添加到上下文 / 固定到 pipeline”。
|
||||||
|
- 记录每轮实际注入的 resource URI 和 token 消耗。
|
||||||
|
|
||||||
|
### Phase 3: 做完整 MCP Resources 能力
|
||||||
|
|
||||||
|
- 支持 resource templates。
|
||||||
|
- 支持资源订阅更新。
|
||||||
|
- 支持 chunk、summary、RAG 化接入。
|
||||||
|
- 为 DifyAgentRunner、LocalAgentRunner 等不同 runner 定义统一资源上下文接口。
|
||||||
|
|
||||||
|
## 最终建议
|
||||||
|
|
||||||
|
PR #2215 可以作为 MCP Resources 的第一阶段实现继续推进。它让 LangBot 快速拥有“资源发现、预览、按需读取”的闭环,也给 Agent 探索资源提供了可运行路径。
|
||||||
|
|
||||||
|
但在正式设计上,不建议把 “Resources == Tools” 固化为长期抽象。LangBot 更应该把 MCP Resources 定位为上下文来源,与 tools、prompts、knowledge base 并列:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tools -> Agent 可以执行的动作
|
||||||
|
Resources -> Host/用户/Agent 可以选择的上下文数据
|
||||||
|
Prompts -> 可复用的任务模板
|
||||||
|
Knowledge -> 可检索、可索引的长期知识
|
||||||
|
```
|
||||||
|
|
||||||
|
这样既尊重 MCP 协议语义,也能让 LangBot 在 Agent 工作流、企业知识接入和多 MCP server 管理上走得更稳。
|
||||||
@@ -86,6 +86,10 @@ class PipelinesRouterGroup(group.RouterGroup):
|
|||||||
'available_plugins': plugins,
|
'available_plugins': plugins,
|
||||||
'bound_mcp_servers': extensions_prefs.get('mcp_servers', []),
|
'bound_mcp_servers': extensions_prefs.get('mcp_servers', []),
|
||||||
'available_mcp_servers': mcp_servers,
|
'available_mcp_servers': mcp_servers,
|
||||||
|
'bound_mcp_resources': extensions_prefs.get('mcp_resources', []),
|
||||||
|
'mcp_resource_agent_read_enabled': extensions_prefs.get(
|
||||||
|
'mcp_resource_agent_read_enabled', True
|
||||||
|
),
|
||||||
'bound_skills': extensions_prefs.get('skills', []),
|
'bound_skills': extensions_prefs.get('skills', []),
|
||||||
'available_skills': available_skills,
|
'available_skills': available_skills,
|
||||||
}
|
}
|
||||||
@@ -99,6 +103,8 @@ class PipelinesRouterGroup(group.RouterGroup):
|
|||||||
bound_plugins = json_data.get('bound_plugins', [])
|
bound_plugins = json_data.get('bound_plugins', [])
|
||||||
bound_mcp_servers = json_data.get('bound_mcp_servers', [])
|
bound_mcp_servers = json_data.get('bound_mcp_servers', [])
|
||||||
bound_skills = json_data.get('bound_skills', [])
|
bound_skills = json_data.get('bound_skills', [])
|
||||||
|
bound_mcp_resources = json_data.get('bound_mcp_resources')
|
||||||
|
mcp_resource_agent_read_enabled = json_data.get('mcp_resource_agent_read_enabled')
|
||||||
|
|
||||||
await self.ap.pipeline_service.update_pipeline_extensions(
|
await self.ap.pipeline_service.update_pipeline_extensions(
|
||||||
pipeline_uuid,
|
pipeline_uuid,
|
||||||
@@ -108,6 +114,8 @@ class PipelinesRouterGroup(group.RouterGroup):
|
|||||||
enable_all_mcp_servers,
|
enable_all_mcp_servers,
|
||||||
bound_skills=bound_skills,
|
bound_skills=bound_skills,
|
||||||
enable_all_skills=enable_all_skills,
|
enable_all_skills=enable_all_skills,
|
||||||
|
bound_mcp_resources=bound_mcp_resources,
|
||||||
|
mcp_resource_agent_read_enabled=mcp_resource_agent_read_enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.success()
|
return self.success()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import quart
|
import quart
|
||||||
import traceback
|
import traceback
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
|
||||||
from ... import group
|
from ... import group
|
||||||
@@ -66,3 +67,50 @@ class MCPRouterGroup(group.RouterGroup):
|
|||||||
server_data = await quart.request.json
|
server_data = await quart.request.json
|
||||||
task_id = await self.ap.mcp_service.test_mcp_server(server_name=server_name, server_data=server_data)
|
task_id = await self.ap.mcp_service.test_mcp_server(server_name=server_name, server_data=server_data)
|
||||||
return self.success(data={'task_id': task_id})
|
return self.success(data={'task_id': task_id})
|
||||||
|
|
||||||
|
@self.route('/servers/<server_name>/resources', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
|
async def _(server_name: str) -> str:
|
||||||
|
"""Get resources from an MCP server"""
|
||||||
|
server_name = unquote(server_name)
|
||||||
|
try:
|
||||||
|
resources = await self.ap.mcp_service.get_mcp_server_resources(server_name)
|
||||||
|
templates = await self.ap.mcp_service.get_mcp_server_resource_templates(server_name)
|
||||||
|
runtime_info = await self.ap.mcp_service.get_runtime_info(server_name)
|
||||||
|
return self.success(
|
||||||
|
data={
|
||||||
|
'resources': resources,
|
||||||
|
'resource_templates': templates,
|
||||||
|
'resource_capabilities': (runtime_info or {}).get('resource_capabilities', {}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return self.http_status(500, -1, f'Failed to get resources: {str(e)}')
|
||||||
|
|
||||||
|
@self.route('/servers/<server_name>/resource-templates', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
|
async def _(server_name: str) -> str:
|
||||||
|
"""Get resource templates from an MCP server"""
|
||||||
|
server_name = unquote(server_name)
|
||||||
|
try:
|
||||||
|
templates = await self.ap.mcp_service.get_mcp_server_resource_templates(server_name)
|
||||||
|
return self.success(data={'resource_templates': templates})
|
||||||
|
except Exception as e:
|
||||||
|
return self.http_status(500, -1, f'Failed to get resource templates: {str(e)}')
|
||||||
|
|
||||||
|
@self.route('/servers/<server_name>/resources/read', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
|
async def _(server_name: str) -> str:
|
||||||
|
"""Read a resource from an MCP server"""
|
||||||
|
server_name = unquote(server_name)
|
||||||
|
data = await quart.request.json
|
||||||
|
uri = data.get('uri')
|
||||||
|
if not uri:
|
||||||
|
return self.http_status(400, -1, 'URI is required')
|
||||||
|
try:
|
||||||
|
envelope = await self.ap.mcp_service.read_mcp_server_resource_envelope(
|
||||||
|
server_name,
|
||||||
|
uri,
|
||||||
|
max_bytes=data.get('max_bytes'),
|
||||||
|
include_blob=bool(data.get('include_blob', False)),
|
||||||
|
)
|
||||||
|
return self.success(data=envelope)
|
||||||
|
except Exception as e:
|
||||||
|
return self.http_status(500, -1, f'Failed to read resource: {str(e)}')
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import quart
|
||||||
|
|
||||||
from ... import group
|
from ... import group
|
||||||
|
|
||||||
|
|
||||||
@@ -9,25 +11,41 @@ class ToolsRouterGroup(group.RouterGroup):
|
|||||||
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
"""获取所有可用工具列表"""
|
"""获取所有可用工具列表"""
|
||||||
tools = await self.ap.tool_mgr.get_all_tools()
|
pipeline_uuid = quart.request.args.get('pipeline_uuid') or quart.request.args.get('pipeline_id')
|
||||||
|
bound_plugins: list[str] | None = None
|
||||||
|
bound_mcp_servers: list[str] | None = None
|
||||||
|
|
||||||
tool_list = []
|
if pipeline_uuid:
|
||||||
for tool in tools:
|
pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_uuid)
|
||||||
tool_list.append(
|
if pipeline is None:
|
||||||
{
|
return self.http_status(404, -1, 'pipeline not found')
|
||||||
'name': tool.name,
|
|
||||||
'description': tool.description,
|
|
||||||
'human_desc': tool.human_desc,
|
|
||||||
'parameters': tool.parameters,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'tools': tool_list})
|
extensions_prefs = pipeline.get('extensions_preferences', {}) or {}
|
||||||
|
if not extensions_prefs.get('enable_all_plugins', True):
|
||||||
|
bound_plugins = [
|
||||||
|
f'{plugin.get("author", "")}/{plugin.get("name", "")}'
|
||||||
|
for plugin in extensions_prefs.get('plugins', [])
|
||||||
|
if isinstance(plugin, dict) and plugin.get('name')
|
||||||
|
]
|
||||||
|
if not extensions_prefs.get('enable_all_mcp_servers', True):
|
||||||
|
bound_mcp_servers = [
|
||||||
|
server for server in (extensions_prefs.get('mcp_servers', []) or []) if isinstance(server, str)
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.success(
|
||||||
|
data={
|
||||||
|
'tools': await self.ap.tool_mgr.get_tool_catalog(
|
||||||
|
bound_plugins,
|
||||||
|
bound_mcp_servers,
|
||||||
|
include_skill_authoring=True,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@self.route('/<tool_name>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
@self.route('/<tool_name>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
async def _(tool_name: str) -> str:
|
async def _(tool_name: str) -> str:
|
||||||
"""获取特定工具详情"""
|
"""获取特定工具详情"""
|
||||||
tools = await self.ap.tool_mgr.get_all_tools()
|
tools = await self.ap.tool_mgr.get_all_tools(include_skill_authoring=True)
|
||||||
|
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
if tool.name == tool_name:
|
if tool.name == tool_name:
|
||||||
|
|||||||
@@ -136,6 +136,32 @@ class MCPService:
|
|||||||
if server_name in self.ap.tool_mgr.mcp_tool_loader.sessions:
|
if server_name in self.ap.tool_mgr.mcp_tool_loader.sessions:
|
||||||
await self.ap.tool_mgr.mcp_tool_loader.remove_mcp_server(server_name)
|
await self.ap.tool_mgr.mcp_tool_loader.remove_mcp_server(server_name)
|
||||||
|
|
||||||
|
async def get_mcp_server_resources(self, server_name: str) -> list[dict]:
|
||||||
|
"""Get resources from a specific MCP server."""
|
||||||
|
return await self.ap.tool_mgr.mcp_tool_loader.get_resources(server_name)
|
||||||
|
|
||||||
|
async def get_mcp_server_resource_templates(self, server_name: str) -> list[dict]:
|
||||||
|
"""Get resource templates from a specific MCP server."""
|
||||||
|
return await self.ap.tool_mgr.mcp_tool_loader.get_resource_templates(server_name)
|
||||||
|
|
||||||
|
async def read_mcp_server_resource_envelope(
|
||||||
|
self,
|
||||||
|
server_name: str,
|
||||||
|
uri: str,
|
||||||
|
*,
|
||||||
|
max_bytes: int | None = None,
|
||||||
|
include_blob: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""Read a resource from a specific MCP server with metadata."""
|
||||||
|
kwargs = {'include_blob': include_blob, 'source': 'ui_preview'}
|
||||||
|
if max_bytes is not None:
|
||||||
|
kwargs['max_bytes'] = max_bytes
|
||||||
|
return await self.ap.tool_mgr.mcp_tool_loader.read_resource_envelope(server_name, uri, **kwargs)
|
||||||
|
|
||||||
|
async def read_mcp_server_resource(self, server_name: str, uri: str) -> list[dict]:
|
||||||
|
"""Read a resource from a specific MCP server."""
|
||||||
|
return await self.ap.tool_mgr.mcp_tool_loader.read_resource(server_name, uri)
|
||||||
|
|
||||||
async def test_mcp_server(self, server_name: str, server_data: dict) -> int:
|
async def test_mcp_server(self, server_name: str, server_data: dict) -> int:
|
||||||
"""测试 MCP 服务器连接并返回任务 ID"""
|
"""测试 MCP 服务器连接并返回任务 ID"""
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ class PipelineService:
|
|||||||
'enable_all_mcp_servers': True,
|
'enable_all_mcp_servers': True,
|
||||||
'plugins': [],
|
'plugins': [],
|
||||||
'mcp_servers': [],
|
'mcp_servers': [],
|
||||||
|
'mcp_resources': [],
|
||||||
|
'mcp_resource_agent_read_enabled': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
@@ -193,6 +195,8 @@ class PipelineService:
|
|||||||
'enable_all_mcp_servers': True,
|
'enable_all_mcp_servers': True,
|
||||||
'plugins': [],
|
'plugins': [],
|
||||||
'mcp_servers': [],
|
'mcp_servers': [],
|
||||||
|
'mcp_resources': [],
|
||||||
|
'mcp_resource_agent_read_enabled': True,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -217,6 +221,8 @@ class PipelineService:
|
|||||||
enable_all_mcp_servers: bool = True,
|
enable_all_mcp_servers: bool = True,
|
||||||
bound_skills: list[str] = None,
|
bound_skills: list[str] = None,
|
||||||
enable_all_skills: bool = True,
|
enable_all_skills: bool = True,
|
||||||
|
bound_mcp_resources: list[dict] = None,
|
||||||
|
mcp_resource_agent_read_enabled: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update the bound plugins and MCP servers for a pipeline"""
|
"""Update the bound plugins and MCP servers for a pipeline"""
|
||||||
# Get current pipeline
|
# Get current pipeline
|
||||||
@@ -236,10 +242,14 @@ class PipelineService:
|
|||||||
extensions_preferences['enable_all_mcp_servers'] = enable_all_mcp_servers
|
extensions_preferences['enable_all_mcp_servers'] = enable_all_mcp_servers
|
||||||
extensions_preferences['enable_all_skills'] = enable_all_skills
|
extensions_preferences['enable_all_skills'] = enable_all_skills
|
||||||
extensions_preferences['plugins'] = bound_plugins
|
extensions_preferences['plugins'] = bound_plugins
|
||||||
|
if mcp_resource_agent_read_enabled is not None:
|
||||||
|
extensions_preferences['mcp_resource_agent_read_enabled'] = mcp_resource_agent_read_enabled
|
||||||
if bound_mcp_servers is not None:
|
if bound_mcp_servers is not None:
|
||||||
extensions_preferences['mcp_servers'] = bound_mcp_servers
|
extensions_preferences['mcp_servers'] = bound_mcp_servers
|
||||||
if bound_skills is not None:
|
if bound_skills is not None:
|
||||||
extensions_preferences['skills'] = bound_skills
|
extensions_preferences['skills'] = bound_skills
|
||||||
|
if bound_mcp_resources is not None:
|
||||||
|
extensions_preferences['mcp_resources'] = bound_mcp_resources
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||||
|
|||||||
@@ -26,7 +26,14 @@ class LegacyPipeline(Base):
|
|||||||
extensions_preferences = sqlalchemy.Column(
|
extensions_preferences = sqlalchemy.Column(
|
||||||
sqlalchemy.JSON,
|
sqlalchemy.JSON,
|
||||||
nullable=False,
|
nullable=False,
|
||||||
default={'enable_all_plugins': True, 'enable_all_mcp_servers': True, 'plugins': [], 'mcp_servers': []},
|
default={
|
||||||
|
'enable_all_plugins': True,
|
||||||
|
'enable_all_mcp_servers': True,
|
||||||
|
'plugins': [],
|
||||||
|
'mcp_servers': [],
|
||||||
|
'mcp_resources': [],
|
||||||
|
'mcp_resource_agent_read_enabled': True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,15 @@ class RuntimePipeline:
|
|||||||
extensions_prefs = pipeline_entity.extensions_preferences or {}
|
extensions_prefs = pipeline_entity.extensions_preferences or {}
|
||||||
self.enable_all_plugins = extensions_prefs.get('enable_all_plugins', True)
|
self.enable_all_plugins = extensions_prefs.get('enable_all_plugins', True)
|
||||||
self.enable_all_mcp_servers = extensions_prefs.get('enable_all_mcp_servers', True)
|
self.enable_all_mcp_servers = extensions_prefs.get('enable_all_mcp_servers', True)
|
||||||
|
local_agent_config = (pipeline_entity.config or {}).get('ai', {}).get('local-agent', {})
|
||||||
|
self.mcp_resource_attachments = local_agent_config.get(
|
||||||
|
'mcp-resources',
|
||||||
|
extensions_prefs.get('mcp_resources', []),
|
||||||
|
)
|
||||||
|
self.mcp_resource_agent_read_enabled = local_agent_config.get(
|
||||||
|
'mcp-resource-agent-read-enabled',
|
||||||
|
extensions_prefs.get('mcp_resource_agent_read_enabled', True),
|
||||||
|
)
|
||||||
|
|
||||||
if self.enable_all_plugins:
|
if self.enable_all_plugins:
|
||||||
# None indicates to use all available plugins
|
# None indicates to use all available plugins
|
||||||
@@ -116,6 +125,8 @@ class RuntimePipeline:
|
|||||||
# Store bound plugins and MCP servers in query for filtering
|
# Store bound plugins and MCP servers in query for filtering
|
||||||
query.variables['_pipeline_bound_plugins'] = self.bound_plugins
|
query.variables['_pipeline_bound_plugins'] = self.bound_plugins
|
||||||
query.variables['_pipeline_bound_mcp_servers'] = self.bound_mcp_servers
|
query.variables['_pipeline_bound_mcp_servers'] = self.bound_mcp_servers
|
||||||
|
query.variables['_pipeline_mcp_resource_attachments'] = self.mcp_resource_attachments
|
||||||
|
query.variables['_pipeline_mcp_resource_agent_read_enabled'] = self.mcp_resource_agent_read_enabled
|
||||||
|
|
||||||
# Record query start for monitoring
|
# Record query start for monitoring
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -25,6 +25,21 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
- use_funcs
|
- use_funcs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_selected_tools(
|
||||||
|
tools: list,
|
||||||
|
local_agent_config: dict,
|
||||||
|
) -> list:
|
||||||
|
if local_agent_config.get('enable-all-tools', True) is not False:
|
||||||
|
return tools
|
||||||
|
|
||||||
|
selected_tools = local_agent_config.get('tools', [])
|
||||||
|
if not isinstance(selected_tools, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
selected_tool_names = {tool for tool in selected_tools if isinstance(tool, str)}
|
||||||
|
return [tool for tool in tools if tool.name in selected_tool_names]
|
||||||
|
|
||||||
async def process(
|
async def process(
|
||||||
self,
|
self,
|
||||||
query: pipeline_query.Query,
|
query: pipeline_query.Query,
|
||||||
@@ -32,6 +47,7 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
) -> entities.StageProcessResult:
|
) -> entities.StageProcessResult:
|
||||||
"""Process"""
|
"""Process"""
|
||||||
selected_runner = query.pipeline_config['ai']['runner']['runner']
|
selected_runner = query.pipeline_config['ai']['runner']['runner']
|
||||||
|
local_agent_config = query.pipeline_config.get('ai', {}).get('local-agent', {})
|
||||||
include_skill_authoring = (
|
include_skill_authoring = (
|
||||||
selected_runner == 'local-agent' and getattr(self.ap, 'skill_service', None) is not None
|
selected_runner == 'local-agent' and getattr(self.ap, 'skill_service', None) is not None
|
||||||
)
|
)
|
||||||
@@ -43,7 +59,7 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
if selected_runner == 'local-agent':
|
if selected_runner == 'local-agent':
|
||||||
# Read model config — new format is { primary: str, fallbacks: [str] },
|
# Read model config — new format is { primary: str, fallbacks: [str] },
|
||||||
# but handle legacy plain string for backward compatibility
|
# but handle legacy plain string for backward compatibility
|
||||||
model_config = query.pipeline_config['ai']['local-agent'].get('model', {})
|
model_config = local_agent_config.get('model', {})
|
||||||
if isinstance(model_config, str):
|
if isinstance(model_config, str):
|
||||||
# Legacy format: plain UUID string
|
# Legacy format: plain UUID string
|
||||||
primary_uuid = model_config
|
primary_uuid = model_config
|
||||||
@@ -113,11 +129,14 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
# Get bound plugins and MCP servers for filtering tools
|
# Get bound plugins and MCP servers for filtering tools
|
||||||
bound_plugins = query.variables.get('_pipeline_bound_plugins', None)
|
bound_plugins = query.variables.get('_pipeline_bound_plugins', None)
|
||||||
bound_mcp_servers = query.variables.get('_pipeline_bound_mcp_servers', None)
|
bound_mcp_servers = query.variables.get('_pipeline_bound_mcp_servers', None)
|
||||||
query.use_funcs = await self.ap.tool_mgr.get_all_tools(
|
include_mcp_resource_tools = query.variables.get('_pipeline_mcp_resource_agent_read_enabled', True)
|
||||||
|
all_tools = await self.ap.tool_mgr.get_all_tools(
|
||||||
bound_plugins,
|
bound_plugins,
|
||||||
bound_mcp_servers,
|
bound_mcp_servers,
|
||||||
include_skill_authoring=include_skill_authoring,
|
include_skill_authoring=include_skill_authoring,
|
||||||
|
include_mcp_resource_tools=include_mcp_resource_tools,
|
||||||
)
|
)
|
||||||
|
query.use_funcs = self._filter_selected_tools(all_tools, local_agent_config)
|
||||||
|
|
||||||
self.ap.logger.debug(f'Bound plugins: {bound_plugins}')
|
self.ap.logger.debug(f'Bound plugins: {bound_plugins}')
|
||||||
self.ap.logger.debug(f'Bound MCP servers: {bound_mcp_servers}')
|
self.ap.logger.debug(f'Bound MCP servers: {bound_mcp_servers}')
|
||||||
@@ -128,11 +147,14 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
if not query.use_funcs and query.variables.get('_fallback_model_uuids'):
|
if not query.use_funcs and query.variables.get('_fallback_model_uuids'):
|
||||||
bound_plugins = query.variables.get('_pipeline_bound_plugins', None)
|
bound_plugins = query.variables.get('_pipeline_bound_plugins', None)
|
||||||
bound_mcp_servers = query.variables.get('_pipeline_bound_mcp_servers', None)
|
bound_mcp_servers = query.variables.get('_pipeline_bound_mcp_servers', None)
|
||||||
query.use_funcs = await self.ap.tool_mgr.get_all_tools(
|
include_mcp_resource_tools = query.variables.get('_pipeline_mcp_resource_agent_read_enabled', True)
|
||||||
|
all_tools = await self.ap.tool_mgr.get_all_tools(
|
||||||
bound_plugins,
|
bound_plugins,
|
||||||
bound_mcp_servers,
|
bound_mcp_servers,
|
||||||
include_skill_authoring=include_skill_authoring,
|
include_skill_authoring=include_skill_authoring,
|
||||||
|
include_mcp_resource_tools=include_mcp_resource_tools,
|
||||||
)
|
)
|
||||||
|
query.use_funcs = self._filter_selected_tools(all_tools, local_agent_config)
|
||||||
|
|
||||||
sender_name = ''
|
sender_name = ''
|
||||||
|
|
||||||
|
|||||||
@@ -417,6 +417,30 @@ class LocalAgentRunner(runner.RequestRunner):
|
|||||||
ce.text = final_user_message_text
|
ce.text = final_user_message_text
|
||||||
break
|
break
|
||||||
|
|
||||||
|
mcp_loader = getattr(getattr(self.ap, 'tool_mgr', None), 'mcp_tool_loader', None)
|
||||||
|
if mcp_loader is not None:
|
||||||
|
resource_context = await mcp_loader.build_resource_context_for_query(query)
|
||||||
|
if resource_context:
|
||||||
|
resource_addition = (
|
||||||
|
'\n\nMCP resource context selected by LangBot host:\n'
|
||||||
|
f'{resource_context}\n\n'
|
||||||
|
'Use this context as read-only reference material. If it conflicts with the user message, '
|
||||||
|
'ask for clarification before taking external actions.'
|
||||||
|
)
|
||||||
|
if isinstance(user_message.content, str):
|
||||||
|
user_message.content += resource_addition
|
||||||
|
elif isinstance(user_message.content, list):
|
||||||
|
appended = False
|
||||||
|
for ce in user_message.content:
|
||||||
|
if ce.type == 'text':
|
||||||
|
ce.text = (ce.text or '') + resource_addition
|
||||||
|
appended = True
|
||||||
|
break
|
||||||
|
if not appended:
|
||||||
|
user_message.content.append(
|
||||||
|
provider_message.ContentElement.from_text(resource_addition.strip())
|
||||||
|
)
|
||||||
|
|
||||||
req_messages = self._build_request_messages(query, user_message)
|
req_messages = self._build_request_messages(query, user_message)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,24 @@ class PluginToolLoader(loader.ToolLoader):
|
|||||||
|
|
||||||
return all_functions
|
return all_functions
|
||||||
|
|
||||||
|
async def get_tool_catalog(self, bound_plugins: list[str] | None = None) -> list[dict[str, typing.Any]]:
|
||||||
|
catalog: list[dict[str, typing.Any]] = []
|
||||||
|
|
||||||
|
for tool in await self.ap.plugin_connector.list_tools(bound_plugins):
|
||||||
|
catalog.append(
|
||||||
|
{
|
||||||
|
'name': tool.metadata.name,
|
||||||
|
'description': tool.spec['llm_prompt'],
|
||||||
|
'human_desc': tool.metadata.description.en_US,
|
||||||
|
'parameters': tool.spec['parameters'],
|
||||||
|
'source': 'plugin',
|
||||||
|
'source_name': tool.owner,
|
||||||
|
'source_id': tool.owner,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return catalog
|
||||||
|
|
||||||
async def has_tool(self, name: str) -> bool:
|
async def has_tool(self, name: str) -> bool:
|
||||||
"""检查工具是否存在"""
|
"""检查工具是否存在"""
|
||||||
for tool in await self.ap.plugin_connector.list_tools():
|
for tool in await self.ap.plugin_connector.list_tools():
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class ToolManager:
|
|||||||
bound_plugins: list[str] | None = None,
|
bound_plugins: list[str] | None = None,
|
||||||
bound_mcp_servers: list[str] | None = None,
|
bound_mcp_servers: list[str] | None = None,
|
||||||
include_skill_authoring: bool = False,
|
include_skill_authoring: bool = False,
|
||||||
|
include_mcp_resource_tools: bool = True,
|
||||||
) -> list[resource_tool.LLMTool]:
|
) -> list[resource_tool.LLMTool]:
|
||||||
all_functions: list[resource_tool.LLMTool] = []
|
all_functions: list[resource_tool.LLMTool] = []
|
||||||
|
|
||||||
@@ -66,10 +67,51 @@ class ToolManager:
|
|||||||
if include_skill_authoring:
|
if include_skill_authoring:
|
||||||
all_functions.extend(await self.skill_tool_loader.get_tools())
|
all_functions.extend(await self.skill_tool_loader.get_tools())
|
||||||
all_functions.extend(await self.plugin_tool_loader.get_tools(bound_plugins))
|
all_functions.extend(await self.plugin_tool_loader.get_tools(bound_plugins))
|
||||||
all_functions.extend(await self.mcp_tool_loader.get_tools(bound_mcp_servers))
|
all_functions.extend(
|
||||||
|
await self.mcp_tool_loader.get_tools(
|
||||||
|
bound_mcp_servers,
|
||||||
|
include_resource_tools=include_mcp_resource_tools,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return all_functions
|
return all_functions
|
||||||
|
|
||||||
|
async def get_tool_catalog(
|
||||||
|
self,
|
||||||
|
bound_plugins: list[str] | None = None,
|
||||||
|
bound_mcp_servers: list[str] | None = None,
|
||||||
|
include_skill_authoring: bool = False,
|
||||||
|
include_mcp_resource_tools: bool = False,
|
||||||
|
) -> list[dict[str, typing.Any]]:
|
||||||
|
catalog: list[dict[str, typing.Any]] = []
|
||||||
|
|
||||||
|
def append_tools(source: str, source_name: str, tools: list[resource_tool.LLMTool]) -> None:
|
||||||
|
for tool in tools:
|
||||||
|
catalog.append(
|
||||||
|
{
|
||||||
|
'name': tool.name,
|
||||||
|
'description': tool.description,
|
||||||
|
'human_desc': tool.human_desc,
|
||||||
|
'parameters': tool.parameters,
|
||||||
|
'source': source,
|
||||||
|
'source_name': source_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
append_tools('builtin', 'LangBot', await self.native_tool_loader.get_tools())
|
||||||
|
if include_skill_authoring:
|
||||||
|
append_tools('skill', 'LangBot', await self.skill_tool_loader.get_tools())
|
||||||
|
catalog.extend(await self.plugin_tool_loader.get_tool_catalog(bound_plugins))
|
||||||
|
|
||||||
|
if self.mcp_tool_loader:
|
||||||
|
for item in await self.mcp_tool_loader.get_tool_catalog(
|
||||||
|
bound_mcp_servers,
|
||||||
|
include_resource_tools=include_mcp_resource_tools,
|
||||||
|
):
|
||||||
|
catalog.append(item)
|
||||||
|
|
||||||
|
return catalog
|
||||||
|
|
||||||
async def get_tool_by_name(self, name: str) -> tool_loader.ToolLookupResult | None:
|
async def get_tool_by_name(self, name: str) -> tool_loader.ToolLookupResult | None:
|
||||||
"""Get tool by name from any active loader."""
|
"""Get tool by name from any active loader."""
|
||||||
for active_loader in (
|
for active_loader in (
|
||||||
|
|||||||
@@ -118,20 +118,6 @@ stages:
|
|||||||
default:
|
default:
|
||||||
- role: system
|
- role: system
|
||||||
content: "You are a helpful assistant."
|
content: "You are a helpful assistant."
|
||||||
- name: knowledge-bases
|
|
||||||
label:
|
|
||||||
en_US: Knowledge Bases
|
|
||||||
zh_Hans: 知识库
|
|
||||||
description:
|
|
||||||
en_US: Configure the knowledge bases to use for the agent, if not selected, the agent will directly use the LLM to reply
|
|
||||||
zh_Hans: 配置用于提升回复质量的知识库,若不选择,则直接使用大模型回复
|
|
||||||
type: knowledge-base-multi-selector
|
|
||||||
required: false
|
|
||||||
default: []
|
|
||||||
show_if:
|
|
||||||
field: __system.is_wizard
|
|
||||||
operator: neq
|
|
||||||
value: true
|
|
||||||
- name: box-session-id-template
|
- name: box-session-id-template
|
||||||
label:
|
label:
|
||||||
en_US: Sandbox Scope
|
en_US: Sandbox Scope
|
||||||
@@ -254,6 +240,34 @@ stages:
|
|||||||
field: rerank-model
|
field: rerank-model
|
||||||
operator: neq
|
operator: neq
|
||||||
value: ''
|
value: ''
|
||||||
|
- name: tools
|
||||||
|
label:
|
||||||
|
en_US: Tools
|
||||||
|
zh_Hans: 工具
|
||||||
|
description:
|
||||||
|
en_US: Select plugin, MCP, skill, and built-in tools available to this Local Agent.
|
||||||
|
zh_Hans: 选择此内置 Agent 可以调用的插件、MCP、技能和内置工具。
|
||||||
|
type: rich-tools-selector
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
show_if:
|
||||||
|
field: __system.is_wizard
|
||||||
|
operator: neq
|
||||||
|
value: true
|
||||||
|
- name: knowledge-bases
|
||||||
|
label:
|
||||||
|
en_US: Resources
|
||||||
|
zh_Hans: 资源
|
||||||
|
description:
|
||||||
|
en_US: Select MCP resources and knowledge bases available to this Local Agent.
|
||||||
|
zh_Hans: 选择此内置 Agent 可以读取的 MCP 资源和知识库。
|
||||||
|
type: resources-selector
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
show_if:
|
||||||
|
field: __system.is_wizard
|
||||||
|
operator: neq
|
||||||
|
value: true
|
||||||
- name: dify-service-api
|
- name: dify-service-api
|
||||||
label:
|
label:
|
||||||
en_US: Dify Service API
|
en_US: Dify Service API
|
||||||
|
|||||||
@@ -90,6 +90,56 @@ class TestMCPServiceGetRuntimeInfo:
|
|||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestMCPServiceResources:
|
||||||
|
"""Tests for MCP resource helpers."""
|
||||||
|
|
||||||
|
async def test_get_resource_templates_delegates_to_loader(self):
|
||||||
|
ap = SimpleNamespace()
|
||||||
|
ap.tool_mgr = SimpleNamespace()
|
||||||
|
ap.tool_mgr.mcp_tool_loader = SimpleNamespace()
|
||||||
|
ap.tool_mgr.mcp_tool_loader.get_resource_templates = AsyncMock(
|
||||||
|
return_value=[{'uri_template': 'file:///{path}', 'name': 'files'}]
|
||||||
|
)
|
||||||
|
|
||||||
|
service = MCPService(ap)
|
||||||
|
|
||||||
|
result = await service.get_mcp_server_resource_templates('docs')
|
||||||
|
|
||||||
|
assert result == [{'uri_template': 'file:///{path}', 'name': 'files'}]
|
||||||
|
ap.tool_mgr.mcp_tool_loader.get_resource_templates.assert_awaited_once_with('docs')
|
||||||
|
|
||||||
|
async def test_read_resource_envelope_uses_ui_preview_source(self):
|
||||||
|
ap = SimpleNamespace()
|
||||||
|
ap.tool_mgr = SimpleNamespace()
|
||||||
|
ap.tool_mgr.mcp_tool_loader = SimpleNamespace()
|
||||||
|
ap.tool_mgr.mcp_tool_loader.read_resource_envelope = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
'server_name': 'docs',
|
||||||
|
'uri': 'file:///README.md',
|
||||||
|
'contents': [],
|
||||||
|
'source': 'ui_preview',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
service = MCPService(ap)
|
||||||
|
|
||||||
|
result = await service.read_mcp_server_resource_envelope(
|
||||||
|
'docs',
|
||||||
|
'file:///README.md',
|
||||||
|
max_bytes=4096,
|
||||||
|
include_blob=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result['source'] == 'ui_preview'
|
||||||
|
ap.tool_mgr.mcp_tool_loader.read_resource_envelope.assert_awaited_once_with(
|
||||||
|
'docs',
|
||||||
|
'file:///README.md',
|
||||||
|
include_blob=True,
|
||||||
|
source='ui_preview',
|
||||||
|
max_bytes=4096,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestMCPServiceGetMCPServers:
|
class TestMCPServiceGetMCPServers:
|
||||||
"""Tests for get_mcp_servers method."""
|
"""Tests for get_mcp_servers method."""
|
||||||
|
|
||||||
|
|||||||
@@ -348,6 +348,8 @@ class TestPipelineServiceCreatePipeline:
|
|||||||
'enable_all_mcp_servers': True,
|
'enable_all_mcp_servers': True,
|
||||||
'plugins': [],
|
'plugins': [],
|
||||||
'mcp_servers': [],
|
'mcp_servers': [],
|
||||||
|
'mcp_resources': [],
|
||||||
|
'mcp_resource_agent_read_enabled': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -814,6 +816,47 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
|||||||
# Verify - persistence was called
|
# Verify - persistence was called
|
||||||
ap.persistence_mgr.execute_async.assert_called()
|
ap.persistence_mgr.execute_async.assert_called()
|
||||||
|
|
||||||
|
async def test_update_extensions_preserves_mcp_resource_agent_read_when_omitted(self):
|
||||||
|
"""Does not reset mcp_resource_agent_read_enabled when omitted by older clients."""
|
||||||
|
ap = SimpleNamespace()
|
||||||
|
ap.persistence_mgr = SimpleNamespace()
|
||||||
|
ap.pipeline_mgr = SimpleNamespace()
|
||||||
|
ap.pipeline_mgr.remove_pipeline = AsyncMock()
|
||||||
|
ap.pipeline_mgr.load_pipeline = AsyncMock()
|
||||||
|
|
||||||
|
original_pipeline = _create_mock_pipeline(
|
||||||
|
extensions_preferences={
|
||||||
|
'enable_all_plugins': True,
|
||||||
|
'enable_all_mcp_servers': True,
|
||||||
|
'plugins': [],
|
||||||
|
'mcp_servers': [],
|
||||||
|
'mcp_resources': [{'server_uuid': 'srv-1', 'uri': 'file:///README.md'}],
|
||||||
|
'mcp_resource_agent_read_enabled': False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
call_count = 0
|
||||||
|
|
||||||
|
async def mock_execute(query):
|
||||||
|
nonlocal call_count
|
||||||
|
call_count += 1
|
||||||
|
if call_count == 1:
|
||||||
|
return _create_mock_result(first_item=original_pipeline)
|
||||||
|
return Mock()
|
||||||
|
|
||||||
|
ap.persistence_mgr.execute_async = AsyncMock(side_effect=mock_execute)
|
||||||
|
ap.persistence_mgr.serialize_model = Mock(return_value={'uuid': 'test-uuid'})
|
||||||
|
|
||||||
|
service = PipelineService(ap)
|
||||||
|
service.get_pipeline = AsyncMock(return_value={'uuid': 'test-uuid'})
|
||||||
|
|
||||||
|
await service.update_pipeline_extensions('test-uuid', bound_plugins=[])
|
||||||
|
|
||||||
|
assert original_pipeline.extensions_preferences['mcp_resource_agent_read_enabled'] is False
|
||||||
|
assert original_pipeline.extensions_preferences['mcp_resources'] == [
|
||||||
|
{'server_uuid': 'srv-1', 'uri': 'file:///README.md'}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestDefaultStageOrder:
|
class TestDefaultStageOrder:
|
||||||
"""Tests for default_stage_order constant."""
|
"""Tests for default_stage_order constant."""
|
||||||
|
|||||||
@@ -162,3 +162,46 @@ async def test_runtime_pipeline_execute(mock_app, sample_query):
|
|||||||
|
|
||||||
# Verify stage was called
|
# Verify stage was called
|
||||||
mock_stage.process.assert_called_once()
|
mock_stage.process.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_pipeline_prefers_local_agent_mcp_resources(mock_app):
|
||||||
|
"""Local Agent resource selection should override legacy extension prefs."""
|
||||||
|
pipelinemgr = get_pipelinemgr_module()
|
||||||
|
persistence_pipeline = get_persistence_pipeline_module()
|
||||||
|
|
||||||
|
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||||
|
pipeline_entity.config = {
|
||||||
|
'ai': {
|
||||||
|
'local-agent': {
|
||||||
|
'mcp-resources': [{'server_uuid': 'srv-new', 'uri': 'file:///new.md'}],
|
||||||
|
'mcp-resource-agent-read-enabled': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pipeline_entity.extensions_preferences = {
|
||||||
|
'mcp_resources': [{'server_uuid': 'srv-old', 'uri': 'file:///old.md'}],
|
||||||
|
'mcp_resource_agent_read_enabled': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||||
|
|
||||||
|
assert runtime_pipeline.mcp_resource_attachments == [{'server_uuid': 'srv-new', 'uri': 'file:///new.md'}]
|
||||||
|
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_pipeline_falls_back_to_extension_mcp_resources(mock_app):
|
||||||
|
"""Existing extension prefs remain compatible until a Local Agent value exists."""
|
||||||
|
pipelinemgr = get_pipelinemgr_module()
|
||||||
|
persistence_pipeline = get_persistence_pipeline_module()
|
||||||
|
|
||||||
|
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||||
|
pipeline_entity.config = {'ai': {'local-agent': {}}}
|
||||||
|
pipeline_entity.extensions_preferences = {
|
||||||
|
'mcp_resources': [{'server_uuid': 'srv-old', 'uri': 'file:///old.md'}],
|
||||||
|
'mcp_resource_agent_read_enabled': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||||
|
|
||||||
|
assert runtime_pipeline.mcp_resource_attachments == [{'server_uuid': 'srv-old', 'uri': 'file:///old.md'}]
|
||||||
|
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, Mock
|
from unittest.mock import AsyncMock, Mock
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
FakeApp,
|
FakeApp,
|
||||||
@@ -431,3 +432,60 @@ class TestPreProcessorVariables:
|
|||||||
variables = result.new_query.variables
|
variables = result.new_query.variables
|
||||||
assert 'group_name' in variables
|
assert 'group_name' in variables
|
||||||
assert 'sender_name' in variables
|
assert 'sender_name' in variables
|
||||||
|
|
||||||
|
|
||||||
|
class TestPreProcessorToolSelection:
|
||||||
|
"""Tests for Local Agent tool selection."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_local_agent_filters_selected_tools(self):
|
||||||
|
"""Only selected tools should be exposed when all-tools mode is off."""
|
||||||
|
preproc = get_preproc_module()
|
||||||
|
|
||||||
|
app = FakeApp()
|
||||||
|
mock_session = Mock()
|
||||||
|
mock_session.launcher_type = Mock(value='person')
|
||||||
|
mock_session.launcher_id = 12345
|
||||||
|
app.sess_mgr.get_session = AsyncMock(return_value=mock_session)
|
||||||
|
|
||||||
|
mock_conversation = Mock()
|
||||||
|
mock_conversation.prompt = Mock(messages=[])
|
||||||
|
mock_conversation.prompt.copy = Mock(return_value=Mock(messages=[]))
|
||||||
|
mock_conversation.messages = []
|
||||||
|
mock_conversation.uuid = None
|
||||||
|
app.sess_mgr.get_conversation = AsyncMock(return_value=mock_conversation)
|
||||||
|
|
||||||
|
mock_model = Mock()
|
||||||
|
mock_model.model_entity = Mock(uuid='primary-model-uuid', abilities=['func_call'])
|
||||||
|
app.model_mgr.get_model_by_uuid = AsyncMock(return_value=mock_model)
|
||||||
|
app.tool_mgr.get_all_tools = AsyncMock(
|
||||||
|
return_value=[
|
||||||
|
SimpleNamespace(name='exec'),
|
||||||
|
SimpleNamespace(name='plugin_tool'),
|
||||||
|
SimpleNamespace(name='mcp_tool'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_event_ctx = Mock()
|
||||||
|
mock_event_ctx.event = Mock(default_prompt=[], prompt=[])
|
||||||
|
app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
|
||||||
|
|
||||||
|
stage = preproc.PreProcessor(app)
|
||||||
|
query = text_query('hello')
|
||||||
|
query.pipeline_config = {
|
||||||
|
'ai': {
|
||||||
|
'runner': {'runner': 'local-agent'},
|
||||||
|
'local-agent': {
|
||||||
|
'model': {'primary': 'primary-model-uuid', 'fallbacks': []},
|
||||||
|
'prompt': 'default',
|
||||||
|
'enable-all-tools': False,
|
||||||
|
'tools': ['plugin_tool'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'output': {'misc': {'at-sender': False}},
|
||||||
|
'trigger': {'misc': {}},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await stage.process(query, 'PreProcessor')
|
||||||
|
|
||||||
|
assert [tool.name for tool in result.new_query.use_funcs] == ['plugin_tool']
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mcp import types as mcp_types
|
||||||
|
|
||||||
|
from langbot.pkg.provider.tools.loaders.mcp import (
|
||||||
|
MCP_RESOURCE_CONTEXT_QUERY_KEY,
|
||||||
|
MCP_RESOURCE_TRACE_QUERY_KEY,
|
||||||
|
MCP_TOOL_LIST_RESOURCES,
|
||||||
|
MCP_TOOL_READ_RESOURCE,
|
||||||
|
MCPLoader,
|
||||||
|
MCPSessionStatus,
|
||||||
|
RuntimeMCPSession,
|
||||||
|
)
|
||||||
|
from langbot.pkg.telemetry import features as telemetry_features
|
||||||
|
|
||||||
|
|
||||||
|
def _app() -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(logger=Mock())
|
||||||
|
|
||||||
|
|
||||||
|
def _connected_session(
|
||||||
|
*,
|
||||||
|
name: str = 'docs',
|
||||||
|
uuid: str = 'srv-1',
|
||||||
|
resources: list[dict] | None = None,
|
||||||
|
templates: list[dict] | None = None,
|
||||||
|
) -> RuntimeMCPSession:
|
||||||
|
session = RuntimeMCPSession(name, {'uuid': uuid, 'mode': 'remote'}, True, _app())
|
||||||
|
session.status = MCPSessionStatus.CONNECTED
|
||||||
|
session.session = SimpleNamespace(read_resource=AsyncMock())
|
||||||
|
session.resources = resources or [
|
||||||
|
{
|
||||||
|
'uri': 'file:///README.md',
|
||||||
|
'name': 'README.md',
|
||||||
|
'title': '',
|
||||||
|
'description': '',
|
||||||
|
'mime_type': 'text/markdown',
|
||||||
|
'size': None,
|
||||||
|
'icons': [],
|
||||||
|
'annotations': {},
|
||||||
|
'_meta': {},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
session.resource_templates = templates or []
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def _query() -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(variables={})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_read_resource_envelope_truncates_caches_and_records_trace():
|
||||||
|
session = _connected_session()
|
||||||
|
session.session.read_resource.return_value = mcp_types.ReadResourceResult(
|
||||||
|
contents=[
|
||||||
|
mcp_types.TextResourceContents(
|
||||||
|
uri='file:///README.md',
|
||||||
|
mimeType='text/markdown',
|
||||||
|
text='abcdef',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
query = _query()
|
||||||
|
|
||||||
|
first = await session.read_resource_envelope(
|
||||||
|
'file:///README.md',
|
||||||
|
max_bytes=4,
|
||||||
|
source='ui_preview',
|
||||||
|
query=query,
|
||||||
|
)
|
||||||
|
second = await session.read_resource_envelope(
|
||||||
|
'file:///README.md',
|
||||||
|
max_bytes=4,
|
||||||
|
source='agent_tool',
|
||||||
|
query=query,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert first['contents'][0]['text'] == 'abcd'
|
||||||
|
assert first['contents'][0]['bytes'] == 6
|
||||||
|
assert first['truncated'] is True
|
||||||
|
assert first['cache_hit'] is False
|
||||||
|
assert second['cache_hit'] is True
|
||||||
|
assert second['source'] == 'agent_tool'
|
||||||
|
assert session.session.read_resource.await_count == 1
|
||||||
|
|
||||||
|
traces = query.variables[MCP_RESOURCE_TRACE_QUERY_KEY]
|
||||||
|
assert [trace['source'] for trace in traces] == ['ui_preview', 'agent_tool']
|
||||||
|
assert traces[1]['cache_hit'] is True
|
||||||
|
assert query.variables[telemetry_features.FEATURES_KEY]['mcp_resource_reads'] == {
|
||||||
|
'ui_preview': 1,
|
||||||
|
'agent_tool': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_read_resource_envelope_shares_byte_budget_across_text_contents():
|
||||||
|
session = _connected_session()
|
||||||
|
session.session.read_resource.return_value = mcp_types.ReadResourceResult(
|
||||||
|
contents=[
|
||||||
|
mcp_types.TextResourceContents(
|
||||||
|
uri='file:///README.md#first',
|
||||||
|
mimeType='text/plain',
|
||||||
|
text='abc',
|
||||||
|
),
|
||||||
|
mcp_types.TextResourceContents(
|
||||||
|
uri='file:///README.md#second',
|
||||||
|
mimeType='text/plain',
|
||||||
|
text='def',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
envelope = await session.read_resource_envelope('file:///README.md', max_bytes=4)
|
||||||
|
|
||||||
|
assert [item['text'] for item in envelope['contents']] == ['abc', 'd']
|
||||||
|
assert envelope['contents'][0]['truncated'] is False
|
||||||
|
assert envelope['contents'][1]['truncated'] is True
|
||||||
|
assert envelope['bytes'] == 6
|
||||||
|
assert envelope['truncated'] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_read_resource_envelope_omits_binary_by_default():
|
||||||
|
session = _connected_session(
|
||||||
|
resources=[
|
||||||
|
{
|
||||||
|
'uri': 'file:///image.png',
|
||||||
|
'name': 'image.png',
|
||||||
|
'title': '',
|
||||||
|
'description': '',
|
||||||
|
'mime_type': 'image/png',
|
||||||
|
'size': 4,
|
||||||
|
'icons': [],
|
||||||
|
'annotations': {},
|
||||||
|
'_meta': {},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
session.session.read_resource.return_value = mcp_types.ReadResourceResult(
|
||||||
|
contents=[
|
||||||
|
mcp_types.BlobResourceContents(
|
||||||
|
uri='file:///image.png',
|
||||||
|
mimeType='image/png',
|
||||||
|
blob=base64.b64encode(b'\x00\x01\x02\x03').decode(),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
envelope = await session.read_resource_envelope('file:///image.png')
|
||||||
|
|
||||||
|
content = envelope['contents'][0]
|
||||||
|
assert content['type'] == 'blob'
|
||||||
|
assert content['blob'] is None
|
||||||
|
assert content['bytes'] == 4
|
||||||
|
assert content['binary_omitted'] is True
|
||||||
|
assert envelope['truncated'] is True
|
||||||
|
assert envelope['warnings'] == ['Binary resource content omitted from response.']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_read_resource_envelope_rejects_unlisted_uri():
|
||||||
|
session = _connected_session()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='Resource URI is not available'):
|
||||||
|
await session.read_resource_envelope('file:///secret.txt')
|
||||||
|
|
||||||
|
session.session.read_resource.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_resource_uri_allowed_supports_listed_templates_conservatively():
|
||||||
|
session = _connected_session(
|
||||||
|
resources=[],
|
||||||
|
templates=[
|
||||||
|
{
|
||||||
|
'uri_template': 'repo://{owner}/{repo}/file/{path}',
|
||||||
|
'name': 'repository file',
|
||||||
|
'title': '',
|
||||||
|
'description': '',
|
||||||
|
'mime_type': 'text/plain',
|
||||||
|
'icons': [],
|
||||||
|
'annotations': {},
|
||||||
|
'_meta': {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert session.resource_uri_allowed('repo://langbot-app/LangBot/file/src/main.py') is True
|
||||||
|
assert session.resource_uri_allowed('repo://langbot-app/LangBot/issues/1') is False
|
||||||
|
assert session.resource_uri_allowed('https://example.com/secret') is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mcp_loader_can_hide_synthetic_resource_tools():
|
||||||
|
loader = MCPLoader(_app())
|
||||||
|
session = _connected_session()
|
||||||
|
loader.sessions = {'docs': session}
|
||||||
|
|
||||||
|
with_resource_tools = await loader.get_tools(['srv-1'], include_resource_tools=True)
|
||||||
|
without_resource_tools = await loader.get_tools(['srv-1'], include_resource_tools=False)
|
||||||
|
|
||||||
|
assert {tool.name for tool in with_resource_tools} == {
|
||||||
|
MCP_TOOL_LIST_RESOURCES,
|
||||||
|
MCP_TOOL_READ_RESOURCE,
|
||||||
|
}
|
||||||
|
assert without_resource_tools == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mcp_loader_refuses_resource_tool_calls_when_agent_read_disabled():
|
||||||
|
loader = MCPLoader(_app())
|
||||||
|
session = _connected_session()
|
||||||
|
loader.sessions = {'docs': session}
|
||||||
|
query = SimpleNamespace(
|
||||||
|
variables={
|
||||||
|
'_pipeline_bound_mcp_servers': ['srv-1'],
|
||||||
|
'_pipeline_mcp_resource_agent_read_enabled': False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await loader.invoke_tool(
|
||||||
|
MCP_TOOL_READ_RESOURCE,
|
||||||
|
{'server_name': 'docs', 'uri': 'file:///README.md'},
|
||||||
|
query,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result[0].text == 'Error: MCP resource agent reads are disabled.'
|
||||||
|
session.session.read_resource.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_build_resource_context_for_query_uses_only_bound_attached_text_resources():
|
||||||
|
loader = MCPLoader(_app())
|
||||||
|
docs = _connected_session(name='docs', uuid='srv-1')
|
||||||
|
docs.session.read_resource.return_value = mcp_types.ReadResourceResult(
|
||||||
|
contents=[
|
||||||
|
mcp_types.TextResourceContents(
|
||||||
|
uri='file:///README.md',
|
||||||
|
mimeType='text/markdown',
|
||||||
|
text='LangBot MCP resource context',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
other = _connected_session(name='other', uuid='srv-2')
|
||||||
|
other.session.read_resource.return_value = mcp_types.ReadResourceResult(
|
||||||
|
contents=[
|
||||||
|
mcp_types.TextResourceContents(
|
||||||
|
uri='file:///README.md',
|
||||||
|
mimeType='text/markdown',
|
||||||
|
text='must not be injected',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
loader.sessions = {'docs': docs, 'other': other}
|
||||||
|
query = SimpleNamespace(
|
||||||
|
variables={
|
||||||
|
'_pipeline_bound_mcp_servers': ['srv-1'],
|
||||||
|
'_pipeline_mcp_resource_attachments': [
|
||||||
|
{'server_uuid': 'srv-1', 'server_name': 'docs', 'uri': 'file:///README.md', 'mode': 'pinned'},
|
||||||
|
{'server_uuid': 'srv-2', 'server_name': 'other', 'uri': 'file:///README.md', 'mode': 'pinned'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context = await loader.build_resource_context_for_query(query)
|
||||||
|
|
||||||
|
assert '<mcp_resource ' in context
|
||||||
|
assert 'server="docs"' in context
|
||||||
|
assert 'LangBot MCP resource context' in context
|
||||||
|
assert 'must not be injected' not in context
|
||||||
|
assert query.variables[MCP_RESOURCE_CONTEXT_QUERY_KEY]['resource_count'] == 1
|
||||||
|
docs.session.read_resource.assert_awaited_once()
|
||||||
|
other.session.read_resource.assert_not_called()
|
||||||
@@ -15,13 +15,35 @@ from langbot.pkg.provider.tools.toolmgr import ToolManager
|
|||||||
|
|
||||||
|
|
||||||
class StubLoader:
|
class StubLoader:
|
||||||
def __init__(self, tools: list[resource_tool.LLMTool] | None = None, invoke_result=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
tools: list[resource_tool.LLMTool] | None = None,
|
||||||
|
invoke_result=None,
|
||||||
|
catalog_source: str = 'mcp',
|
||||||
|
catalog_source_name: str = 'fixture-server',
|
||||||
|
):
|
||||||
self._tools = tools or []
|
self._tools = tools or []
|
||||||
self._invoke_result = invoke_result
|
self._invoke_result = invoke_result
|
||||||
|
self._catalog_source = catalog_source
|
||||||
|
self._catalog_source_name = catalog_source_name
|
||||||
|
|
||||||
async def get_tools(self, *_args, **_kwargs):
|
async def get_tools(self, *_args, **_kwargs):
|
||||||
return self._tools
|
return self._tools
|
||||||
|
|
||||||
|
async def get_tool_catalog(self, *_args, **_kwargs):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'name': tool.name,
|
||||||
|
'description': tool.description,
|
||||||
|
'human_desc': tool.human_desc,
|
||||||
|
'parameters': tool.parameters,
|
||||||
|
'source': self._catalog_source,
|
||||||
|
'source_name': self._catalog_source_name,
|
||||||
|
'source_id': self._catalog_source_name,
|
||||||
|
}
|
||||||
|
for tool in self._tools
|
||||||
|
]
|
||||||
|
|
||||||
async def has_tool(self, name: str) -> bool:
|
async def has_tool(self, name: str) -> bool:
|
||||||
return any(tool.name == name for tool in self._tools)
|
return any(tool.name == name for tool in self._tools)
|
||||||
|
|
||||||
@@ -68,6 +90,28 @@ async def test_tool_manager_includes_skill_authoring_tools_when_requested():
|
|||||||
assert [tool.name for tool in tools] == ['exec', 'activate', 'plugin_tool', 'mcp_tool']
|
assert [tool.name for tool in tools] == ['exec', 'activate', 'plugin_tool', 'mcp_tool']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tool_manager_catalog_labels_tool_sources():
|
||||||
|
manager = ToolManager(SimpleNamespace())
|
||||||
|
manager.native_tool_loader = StubLoader([make_tool('exec')])
|
||||||
|
manager.skill_tool_loader = StubLoader([make_tool('activate')])
|
||||||
|
manager.plugin_tool_loader = StubLoader(
|
||||||
|
[make_tool('plugin_tool')],
|
||||||
|
catalog_source='plugin',
|
||||||
|
catalog_source_name='fixture-plugin',
|
||||||
|
)
|
||||||
|
manager.mcp_tool_loader = StubLoader([make_tool('mcp_tool')])
|
||||||
|
|
||||||
|
catalog = await manager.get_tool_catalog(include_skill_authoring=True)
|
||||||
|
|
||||||
|
assert [(item['name'], item['source'], item['source_name']) for item in catalog] == [
|
||||||
|
('exec', 'builtin', 'LangBot'),
|
||||||
|
('activate', 'skill', 'LangBot'),
|
||||||
|
('plugin_tool', 'plugin', 'fixture-plugin'),
|
||||||
|
('mcp_tool', 'mcp', 'fixture-server'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_tool_manager_routes_native_tool_calls():
|
async def test_tool_manager_routes_native_tool_calls():
|
||||||
app = SimpleNamespace()
|
app = SimpleNamespace()
|
||||||
|
|||||||
@@ -118,7 +118,12 @@ async def test_preproc_enables_skill_authoring_tools_when_skill_service_availabl
|
|||||||
result = await stage.process(_make_query(), 'PreProcessor')
|
result = await stage.process(_make_query(), 'PreProcessor')
|
||||||
|
|
||||||
assert result.result_type == entities_module.ResultType.CONTINUE
|
assert result.result_type == entities_module.ResultType.CONTINUE
|
||||||
app.tool_mgr.get_all_tools.assert_awaited_once_with(None, None, include_skill_authoring=True)
|
app.tool_mgr.get_all_tools.assert_awaited_once_with(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
include_skill_authoring=True,
|
||||||
|
include_mcp_resource_tools=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -131,7 +136,32 @@ async def test_preproc_disables_skill_authoring_tools_when_skill_service_missing
|
|||||||
result = await stage.process(_make_query(), 'PreProcessor')
|
result = await stage.process(_make_query(), 'PreProcessor')
|
||||||
|
|
||||||
assert result.result_type == entities_module.ResultType.CONTINUE
|
assert result.result_type == entities_module.ResultType.CONTINUE
|
||||||
app.tool_mgr.get_all_tools.assert_awaited_once_with(None, None, include_skill_authoring=False)
|
app.tool_mgr.get_all_tools.assert_awaited_once_with(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
include_skill_authoring=False,
|
||||||
|
include_mcp_resource_tools=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_preproc_disables_mcp_resource_tools_when_agent_reading_is_disabled():
|
||||||
|
preproc_module, entities_module = _import_preproc_modules()
|
||||||
|
|
||||||
|
app = _make_app(skill_service=SimpleNamespace())
|
||||||
|
stage = preproc_module.PreProcessor(app)
|
||||||
|
query = _make_query()
|
||||||
|
query.variables['_pipeline_mcp_resource_agent_read_enabled'] = False
|
||||||
|
|
||||||
|
result = await stage.process(query, 'PreProcessor')
|
||||||
|
|
||||||
|
assert result.result_type == entities_module.ResultType.CONTINUE
|
||||||
|
app.tool_mgr.get_all_tools.assert_awaited_once_with(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
include_skill_authoring=True,
|
||||||
|
include_mcp_resource_tools=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
DynamicFormItemType,
|
||||||
IDynamicFormItemSchema,
|
IDynamicFormItemSchema,
|
||||||
SYSTEM_FIELD_PREFIX,
|
SYSTEM_FIELD_PREFIX,
|
||||||
} from '@/app/infra/entities/form/dynamic';
|
} from '@/app/infra/entities/form/dynamic';
|
||||||
@@ -57,6 +58,95 @@ function resolveShowIfValue(
|
|||||||
return externalDependentValues?.[field];
|
return externalDependentValues?.[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DynamicFormValueSpec = Pick<
|
||||||
|
IDynamicFormItemSchema,
|
||||||
|
'default' | 'name' | 'required' | 'type'
|
||||||
|
>;
|
||||||
|
|
||||||
|
function getValueSpecs(item: IDynamicFormItemSchema): DynamicFormValueSpec[] {
|
||||||
|
if (item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR) {
|
||||||
|
return [
|
||||||
|
item,
|
||||||
|
{
|
||||||
|
name: 'enable-all-tools',
|
||||||
|
type: DynamicFormItemType.BOOLEAN,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === DynamicFormItemType.RESOURCES_SELECTOR) {
|
||||||
|
return [
|
||||||
|
item,
|
||||||
|
{
|
||||||
|
name: 'mcp-resources',
|
||||||
|
type: DynamicFormItemType.UNKNOWN,
|
||||||
|
required: false,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mcp-resource-agent-read-enabled',
|
||||||
|
type: DynamicFormItemType.BOOLEAN,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [item];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValueSchema(spec: DynamicFormValueSpec) {
|
||||||
|
if (spec.name === 'mcp-resources') {
|
||||||
|
return z.array(z.any());
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (spec.type) {
|
||||||
|
case DynamicFormItemType.INT:
|
||||||
|
return z.number();
|
||||||
|
case DynamicFormItemType.FLOAT:
|
||||||
|
return z.number();
|
||||||
|
case DynamicFormItemType.BOOLEAN:
|
||||||
|
return z.boolean();
|
||||||
|
case DynamicFormItemType.STRING:
|
||||||
|
return z.string();
|
||||||
|
case DynamicFormItemType.STRING_ARRAY:
|
||||||
|
return z.array(z.string());
|
||||||
|
case DynamicFormItemType.SELECT:
|
||||||
|
return z.string();
|
||||||
|
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
||||||
|
return z.string();
|
||||||
|
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR:
|
||||||
|
return z.string();
|
||||||
|
case DynamicFormItemType.RERANK_MODEL_SELECTOR:
|
||||||
|
return z.string();
|
||||||
|
case DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR:
|
||||||
|
return z.string();
|
||||||
|
case DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR:
|
||||||
|
case DynamicFormItemType.RESOURCES_SELECTOR:
|
||||||
|
case DynamicFormItemType.RICH_TOOLS_SELECTOR:
|
||||||
|
case DynamicFormItemType.TOOLS_SELECTOR:
|
||||||
|
return z.array(z.string());
|
||||||
|
case DynamicFormItemType.BOT_SELECTOR:
|
||||||
|
return z.string();
|
||||||
|
case DynamicFormItemType.MODEL_FALLBACK_SELECTOR:
|
||||||
|
return z.object({
|
||||||
|
primary: z.string(),
|
||||||
|
fallbacks: z.array(z.string()),
|
||||||
|
});
|
||||||
|
case DynamicFormItemType.PROMPT_EDITOR:
|
||||||
|
return z.array(
|
||||||
|
z.object({
|
||||||
|
content: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return z.string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display-only component for embed code fields with copy animation.
|
* Display-only component for embed code fields with copy animation.
|
||||||
*/
|
*/
|
||||||
@@ -322,9 +412,16 @@ export default function DynamicFormComponent({
|
|||||||
// model-fallback-selector) is coerced to the expected shape
|
// model-fallback-selector) is coerced to the expected shape
|
||||||
// so that downstream components never crash.
|
// so that downstream components never crash.
|
||||||
const normalizeFieldValue = (
|
const normalizeFieldValue = (
|
||||||
item: IDynamicFormItemSchema,
|
item: DynamicFormValueSpec,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): unknown => {
|
): unknown => {
|
||||||
|
if (
|
||||||
|
item.name === 'mcp-resources' ||
|
||||||
|
item.type === DynamicFormItemType.RESOURCES_SELECTOR ||
|
||||||
|
item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR
|
||||||
|
) {
|
||||||
|
return Array.isArray(value) ? value : [];
|
||||||
|
}
|
||||||
if (item.type === 'model-fallback-selector') {
|
if (item.type === 'model-fallback-selector') {
|
||||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
const obj = value as Record<string, unknown>;
|
const obj = value as Record<string, unknown>;
|
||||||
@@ -368,68 +465,16 @@ export default function DynamicFormComponent({
|
|||||||
[itemConfigList],
|
[itemConfigList],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const editableValueSpecs = useMemo(
|
||||||
|
() => editableItems.flatMap(getValueSpecs),
|
||||||
|
[editableItems],
|
||||||
|
);
|
||||||
|
|
||||||
// 根据 itemConfigList 动态生成 zod schema
|
// 根据 itemConfigList 动态生成 zod schema
|
||||||
const formSchema = z.object(
|
const formSchema = z.object(
|
||||||
editableItems.reduce(
|
editableValueSpecs.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
let fieldSchema;
|
let fieldSchema = getValueSchema(item);
|
||||||
switch (item.type) {
|
|
||||||
case 'integer':
|
|
||||||
fieldSchema = z.number();
|
|
||||||
break;
|
|
||||||
case 'float':
|
|
||||||
fieldSchema = z.number();
|
|
||||||
break;
|
|
||||||
case 'boolean':
|
|
||||||
fieldSchema = z.boolean();
|
|
||||||
break;
|
|
||||||
case 'string':
|
|
||||||
fieldSchema = z.string();
|
|
||||||
break;
|
|
||||||
case 'array[string]':
|
|
||||||
fieldSchema = z.array(z.string());
|
|
||||||
break;
|
|
||||||
case 'select':
|
|
||||||
fieldSchema = z.string();
|
|
||||||
break;
|
|
||||||
case 'llm-model-selector':
|
|
||||||
fieldSchema = z.string();
|
|
||||||
break;
|
|
||||||
case 'embedding-model-selector':
|
|
||||||
fieldSchema = z.string();
|
|
||||||
break;
|
|
||||||
case 'rerank-model-selector':
|
|
||||||
fieldSchema = z.string();
|
|
||||||
break;
|
|
||||||
case 'knowledge-base-selector':
|
|
||||||
fieldSchema = z.string();
|
|
||||||
break;
|
|
||||||
case 'knowledge-base-multi-selector':
|
|
||||||
fieldSchema = z.array(z.string());
|
|
||||||
break;
|
|
||||||
case 'bot-selector':
|
|
||||||
fieldSchema = z.string();
|
|
||||||
break;
|
|
||||||
case 'tools-selector':
|
|
||||||
fieldSchema = z.array(z.string());
|
|
||||||
break;
|
|
||||||
case 'model-fallback-selector':
|
|
||||||
fieldSchema = z.object({
|
|
||||||
primary: z.string(),
|
|
||||||
fallbacks: z.array(z.string()),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'prompt-editor':
|
|
||||||
fieldSchema = z.array(
|
|
||||||
z.object({
|
|
||||||
content: z.string(),
|
|
||||||
role: z.string(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
fieldSchema = z.string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
item.required &&
|
item.required &&
|
||||||
@@ -454,7 +499,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: editableItems.reduce((acc, item) => {
|
defaultValues: editableValueSpecs.reduce((acc, item) => {
|
||||||
// 优先使用 initialValues,如果没有则使用默认值
|
// 优先使用 initialValues,如果没有则使用默认值
|
||||||
const rawValue = initialValues?.[item.name] ?? item.default;
|
const rawValue = initialValues?.[item.name] ?? item.default;
|
||||||
return {
|
return {
|
||||||
@@ -493,7 +538,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
if (initialValues && hasRealChange) {
|
if (initialValues && hasRealChange) {
|
||||||
// 合并默认值和初始值
|
// 合并默认值和初始值
|
||||||
const mergedValues = editableItems.reduce(
|
const mergedValues = editableValueSpecs.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
const rawValue = initialValues[item.name] ?? item.default;
|
const rawValue = initialValues[item.name] ?? item.default;
|
||||||
acc[item.name] = normalizeFieldValue(item, rawValue) as object;
|
acc[item.name] = normalizeFieldValue(item, rawValue) as object;
|
||||||
@@ -508,10 +553,16 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
previousInitialValues.current = initialValues;
|
previousInitialValues.current = initialValues;
|
||||||
}
|
}
|
||||||
}, [initialValues, form, editableItems]);
|
}, [initialValues, form, editableValueSpecs]);
|
||||||
|
|
||||||
// Get reactive form values for conditional rendering
|
// Get reactive form values for conditional rendering
|
||||||
const watchedValues = form.watch();
|
const watchedValues = form.watch();
|
||||||
|
const setFormValue = (name: string, value: unknown) => {
|
||||||
|
form.setValue(name as keyof FormValues, value as never, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Stable ref for onSubmit to avoid re-triggering the effect when the
|
// Stable ref for onSubmit to avoid re-triggering the effect when the
|
||||||
// parent passes a new closure on every render.
|
// parent passes a new closure on every render.
|
||||||
@@ -524,7 +575,7 @@ export default function DynamicFormComponent({
|
|||||||
// even if the user saves without modifying any field.
|
// even if the user saves without modifying any field.
|
||||||
// form.watch(callback) only fires on subsequent changes, not on mount.
|
// form.watch(callback) only fires on subsequent changes, not on mount.
|
||||||
const formValues = form.getValues();
|
const formValues = form.getValues();
|
||||||
const initialFinalValues = editableItems.reduce(
|
const initialFinalValues = editableValueSpecs.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
acc[item.name] = formValues[item.name] ?? item.default;
|
acc[item.name] = formValues[item.name] ?? item.default;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -544,7 +595,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
const subscription = form.watch(() => {
|
const subscription = form.watch(() => {
|
||||||
const formValues = form.getValues();
|
const formValues = form.getValues();
|
||||||
const finalValues = editableItems.reduce(
|
const finalValues = editableValueSpecs.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
acc[item.name] = formValues[item.name] ?? item.default;
|
acc[item.name] = formValues[item.name] ?? item.default;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -555,7 +606,7 @@ export default function DynamicFormComponent({
|
|||||||
previousInitialValues.current = finalValues as Record<string, object>;
|
previousInitialValues.current = finalValues as Record<string, object>;
|
||||||
});
|
});
|
||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [form, editableItems]);
|
}, [form, editableValueSpecs]);
|
||||||
|
|
||||||
// State for QR code login dialog
|
// State for QR code login dialog
|
||||||
const [qrDialogOpen, setQrDialogOpen] = useState(false);
|
const [qrDialogOpen, setQrDialogOpen] = useState(false);
|
||||||
@@ -786,6 +837,41 @@ export default function DynamicFormComponent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.type === DynamicFormItemType.RICH_TOOLS_SELECTOR ||
|
||||||
|
config.type === DynamicFormItemType.RESOURCES_SELECTOR
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
key={config.id}
|
||||||
|
control={form.control}
|
||||||
|
name={config.name as keyof FormValues}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="min-w-0">
|
||||||
|
<FormControl>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-w-0 max-w-full overflow-x-hidden',
|
||||||
|
isFieldDisabled && 'pointer-events-none opacity-60',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DynamicFormItemComponent
|
||||||
|
config={config}
|
||||||
|
field={field}
|
||||||
|
formValues={watchedValues as Record<string, unknown>}
|
||||||
|
onFileUploaded={onFileUploaded}
|
||||||
|
setFormValue={setFormValue}
|
||||||
|
systemContext={systemContext}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Boolean fields use a special inline layout
|
// Boolean fields use a special inline layout
|
||||||
if (config.type === 'boolean') {
|
if (config.type === 'boolean') {
|
||||||
return (
|
return (
|
||||||
@@ -816,7 +902,10 @@ export default function DynamicFormComponent({
|
|||||||
<DynamicFormItemComponent
|
<DynamicFormItemComponent
|
||||||
config={config}
|
config={config}
|
||||||
field={field}
|
field={field}
|
||||||
|
formValues={watchedValues as Record<string, unknown>}
|
||||||
onFileUploaded={onFileUploaded}
|
onFileUploaded={onFileUploaded}
|
||||||
|
setFormValue={setFormValue}
|
||||||
|
systemContext={systemContext}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
@@ -853,7 +942,10 @@ export default function DynamicFormComponent({
|
|||||||
<DynamicFormItemComponent
|
<DynamicFormItemComponent
|
||||||
config={config}
|
config={config}
|
||||||
field={field}
|
field={field}
|
||||||
|
formValues={watchedValues as Record<string, unknown>}
|
||||||
onFileUploaded={onFileUploaded}
|
onFileUploaded={onFileUploaded}
|
||||||
|
setFormValue={setFormValue}
|
||||||
|
systemContext={systemContext}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -64,15 +64,23 @@ import {
|
|||||||
import SettingsDialog, {
|
import SettingsDialog, {
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from '@/app/home/components/settings-dialog/SettingsDialog';
|
} from '@/app/home/components/settings-dialog/SettingsDialog';
|
||||||
|
import ToolResourceSelectors from '@/app/home/components/dynamic-form/ToolResourceSelectors';
|
||||||
|
import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '@/app/home/components/models-dialog/types';
|
||||||
|
|
||||||
export default function DynamicFormItemComponent({
|
export default function DynamicFormItemComponent({
|
||||||
config,
|
config,
|
||||||
field,
|
field,
|
||||||
|
formValues,
|
||||||
onFileUploaded,
|
onFileUploaded,
|
||||||
|
setFormValue,
|
||||||
|
systemContext,
|
||||||
}: {
|
}: {
|
||||||
config: IDynamicFormItemSchema;
|
config: IDynamicFormItemSchema;
|
||||||
field: ControllerRenderProps<any, any>;
|
field: ControllerRenderProps<any, any>;
|
||||||
|
formValues?: Record<string, unknown>;
|
||||||
onFileUploaded?: (fileKey: string) => void;
|
onFileUploaded?: (fileKey: string) => void;
|
||||||
|
setFormValue?: (name: string, value: unknown) => void;
|
||||||
|
systemContext?: Record<string, unknown>;
|
||||||
}) {
|
}) {
|
||||||
const [llmModels, setLlmModels] = useState<LLMModel[]>([]);
|
const [llmModels, setLlmModels] = useState<LLMModel[]>([]);
|
||||||
const [embeddingModels, setEmbeddingModels] = useState<EmbeddingModel[]>([]);
|
const [embeddingModels, setEmbeddingModels] = useState<EmbeddingModel[]>([]);
|
||||||
@@ -103,10 +111,34 @@ export default function DynamicFormItemComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchEmbeddingModels = () => {
|
||||||
|
httpClient
|
||||||
|
.getProviderEmbeddingModels()
|
||||||
|
.then((resp) => {
|
||||||
|
setEmbeddingModels(resp.models);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(t('embedding.getModelListError') + err.msg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRerankModels = () => {
|
||||||
|
httpClient
|
||||||
|
.getProviderRerankModels()
|
||||||
|
.then((resp) => {
|
||||||
|
setRerankModels(resp.models);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed to load rerank models: ' + err.msg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleModelsDialogChange = (open: boolean) => {
|
const handleModelsDialogChange = (open: boolean) => {
|
||||||
setModelsDialogOpen(open);
|
setModelsDialogOpen(open);
|
||||||
if (!open) {
|
if (!open) {
|
||||||
fetchLlmModels();
|
fetchLlmModels();
|
||||||
|
fetchEmbeddingModels();
|
||||||
|
fetchRerankModels();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -174,27 +206,13 @@ export default function DynamicFormItemComponent({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config.type === DynamicFormItemType.EMBEDDING_MODEL_SELECTOR) {
|
if (config.type === DynamicFormItemType.EMBEDDING_MODEL_SELECTOR) {
|
||||||
httpClient
|
fetchEmbeddingModels();
|
||||||
.getProviderEmbeddingModels()
|
|
||||||
.then((resp) => {
|
|
||||||
setEmbeddingModels(resp.models);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(t('embedding.getModelListError') + err.msg);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [config.type]);
|
}, [config.type]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config.type === DynamicFormItemType.RERANK_MODEL_SELECTOR) {
|
if (config.type === DynamicFormItemType.RERANK_MODEL_SELECTOR) {
|
||||||
httpClient
|
fetchRerankModels();
|
||||||
.getProviderRerankModels()
|
|
||||||
.then((resp) => {
|
|
||||||
setRerankModels(resp.models);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Failed to load rerank models: ' + err.msg);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [config.type]);
|
}, [config.type]);
|
||||||
|
|
||||||
@@ -248,6 +266,16 @@ export default function DynamicFormItemComponent({
|
|||||||
}
|
}
|
||||||
}, [config.type]);
|
}, [config.type]);
|
||||||
|
|
||||||
|
const handleCompositePatch = (patch: Record<string, unknown>) => {
|
||||||
|
for (const [name, value] of Object.entries(patch)) {
|
||||||
|
if (setFormValue) {
|
||||||
|
setFormValue(name, value);
|
||||||
|
} else if (name === field.name) {
|
||||||
|
field.onChange(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
switch (config.type) {
|
switch (config.type) {
|
||||||
case DynamicFormItemType.INT:
|
case DynamicFormItemType.INT:
|
||||||
case DynamicFormItemType.FLOAT:
|
case DynamicFormItemType.FLOAT:
|
||||||
@@ -377,10 +405,10 @@ export default function DynamicFormItemComponent({
|
|||||||
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
||||||
// Separate space models from regular models
|
// Separate space models from regular models
|
||||||
const spaceModels = llmModels.filter(
|
const spaceModels = llmModels.filter(
|
||||||
(m) => m.provider?.requester === 'space-chat-completions',
|
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||||
);
|
);
|
||||||
const regularModels = llmModels.filter(
|
const regularModels = llmModels.filter(
|
||||||
(m) => m.provider?.requester !== 'space-chat-completions',
|
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Group regular models by provider
|
// Group regular models by provider
|
||||||
@@ -481,7 +509,7 @@ export default function DynamicFormItemComponent({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* Blurred remaining models with login overlay */}
|
{/* Blurred remaining models with login overlay */}
|
||||||
<div className="relative">
|
<div className="relative min-h-10">
|
||||||
<div
|
<div
|
||||||
className="select-none overflow-hidden"
|
className="select-none overflow-hidden"
|
||||||
style={{ maxHeight: '3rem' }}
|
style={{ maxHeight: '3rem' }}
|
||||||
@@ -558,7 +586,10 @@ export default function DynamicFormItemComponent({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-9 w-9 shrink-0"
|
className="h-9 w-9 shrink-0"
|
||||||
onClick={() => setModelsDialogOpen(true)}
|
onClick={() => {
|
||||||
|
setSettingsSection('models');
|
||||||
|
setModelsDialogOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -574,9 +605,15 @@ export default function DynamicFormItemComponent({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR:
|
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR: {
|
||||||
// Group embedding models by provider
|
const spaceEmbeddingModels = embeddingModels.filter(
|
||||||
const groupedEmbeddingModels = embeddingModels.reduce(
|
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||||
|
);
|
||||||
|
const regularEmbeddingModels = embeddingModels.filter(
|
||||||
|
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedEmbeddingModels = regularEmbeddingModels.reduce(
|
||||||
(acc, model) => {
|
(acc, model) => {
|
||||||
const providerName = model.provider?.name || 'Unknown';
|
const providerName = model.provider?.name || 'Unknown';
|
||||||
if (!acc[providerName]) acc[providerName] = [];
|
if (!acc[providerName]) acc[providerName] = [];
|
||||||
@@ -586,29 +623,169 @@ export default function DynamicFormItemComponent({
|
|||||||
{} as Record<string, EmbeddingModel[]>,
|
{} as Record<string, EmbeddingModel[]>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groupedSpaceEmbeddingModels = spaceEmbeddingModels.reduce(
|
||||||
|
(acc, model) => {
|
||||||
|
const providerName =
|
||||||
|
model.provider?.name || model.provider?.requester || 'Unknown';
|
||||||
|
if (!acc[providerName]) acc[providerName] = [];
|
||||||
|
acc[providerName].push(model);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, EmbeddingModel[]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const previewEmbeddingModelNames = [
|
||||||
|
'text-embedding-3-large',
|
||||||
|
'text-embedding-3-small',
|
||||||
|
'bge-m3',
|
||||||
|
'jina-embeddings-v3',
|
||||||
|
'qwen3-embedding-8b',
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md min-w-0">
|
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
|
||||||
<Select value={field.value} onValueChange={field.onChange}>
|
<div className="min-w-0 flex-1">
|
||||||
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
<SelectValue placeholder={t('knowledge.selectEmbeddingModel')} />
|
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
</SelectTrigger>
|
<SelectValue
|
||||||
<SelectContent>
|
placeholder={t('knowledge.selectEmbeddingModel')}
|
||||||
{Object.entries(groupedEmbeddingModels).map(
|
/>
|
||||||
([providerName, models]) => (
|
</SelectTrigger>
|
||||||
<SelectGroup key={providerName}>
|
<SelectContent>
|
||||||
<SelectLabel>{providerName}</SelectLabel>
|
{Object.entries(groupedEmbeddingModels).map(
|
||||||
{models.map((model) => (
|
([providerName, models]) => (
|
||||||
<SelectItem key={model.uuid} value={model.uuid}>
|
<SelectGroup key={providerName}>
|
||||||
{model.name}
|
<SelectLabel>{providerName}</SelectLabel>
|
||||||
</SelectItem>
|
{models.map((model) => (
|
||||||
))}
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
|
{model.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{showSpaceLoginCTA ? (
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||||
|
{t('models.langbotModels')}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
asChild
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Info className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-[240px]">
|
||||||
|
{t('models.spaceTrialTooltip')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</SelectLabel>
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{(spaceEmbeddingModels.length > 0
|
||||||
|
? spaceEmbeddingModels.map((m) => m.name)
|
||||||
|
: previewEmbeddingModelNames
|
||||||
|
)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((name) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-muted-foreground/60"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="relative min-h-10">
|
||||||
|
<div
|
||||||
|
className="select-none overflow-hidden"
|
||||||
|
style={{ maxHeight: '3rem' }}
|
||||||
|
>
|
||||||
|
{(spaceEmbeddingModels.length > 0
|
||||||
|
? spaceEmbeddingModels.map((m) => m.name)
|
||||||
|
: previewEmbeddingModelNames
|
||||||
|
)
|
||||||
|
.slice(3)
|
||||||
|
.map((name) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className="flex w-full items-center py-1.5 pl-8 pr-2 text-sm text-muted-foreground/40 blur-[2px]"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-b from-transparent to-background/80">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs px-3 gap-1.5 shadow-sm"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSpaceLogin();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-3 w-3" />
|
||||||
|
{t('models.unlockModels')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
),
|
) : !systemInfo.disable_models_service ? (
|
||||||
)}
|
Object.entries(groupedSpaceEmbeddingModels).map(
|
||||||
</SelectContent>
|
([providerName, models]) => (
|
||||||
</Select>
|
<SelectGroup key={providerName}>
|
||||||
|
<SelectLabel>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||||
|
{providerName}
|
||||||
|
</span>
|
||||||
|
</SelectLabel>
|
||||||
|
{models.map((model) => (
|
||||||
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
|
{model.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 shrink-0"
|
||||||
|
onClick={() => {
|
||||||
|
setSettingsSection('models');
|
||||||
|
setModelsDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">{t('models.title')}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<SettingsDialog
|
||||||
|
open={modelsDialogOpen}
|
||||||
|
onOpenChange={handleModelsDialogChange}
|
||||||
|
section={settingsSection}
|
||||||
|
onSectionChange={setSettingsSection}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case DynamicFormItemType.RERANK_MODEL_SELECTOR:
|
case DynamicFormItemType.RERANK_MODEL_SELECTOR:
|
||||||
const groupedRerankModels = rerankModels.reduce(
|
const groupedRerankModels = rerankModels.reduce(
|
||||||
@@ -652,10 +829,10 @@ export default function DynamicFormItemComponent({
|
|||||||
case DynamicFormItemType.MODEL_FALLBACK_SELECTOR: {
|
case DynamicFormItemType.MODEL_FALLBACK_SELECTOR: {
|
||||||
// Separate space models from regular models
|
// Separate space models from regular models
|
||||||
const fbSpaceModels = llmModels.filter(
|
const fbSpaceModels = llmModels.filter(
|
||||||
(m) => m.provider?.requester === 'space-chat-completions',
|
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||||
);
|
);
|
||||||
const fbRegularModels = llmModels.filter(
|
const fbRegularModels = llmModels.filter(
|
||||||
(m) => m.provider?.requester !== 'space-chat-completions',
|
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Group regular models by provider
|
// Group regular models by provider
|
||||||
@@ -786,7 +963,7 @@ export default function DynamicFormItemComponent({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* Blurred remaining models with login overlay */}
|
{/* Blurred remaining models with login overlay */}
|
||||||
<div className="relative">
|
<div className="relative min-h-10">
|
||||||
<div
|
<div
|
||||||
className="select-none overflow-hidden"
|
className="select-none overflow-hidden"
|
||||||
style={{ maxHeight: '3rem' }}
|
style={{ maxHeight: '3rem' }}
|
||||||
@@ -1383,6 +1560,32 @@ export default function DynamicFormItemComponent({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case DynamicFormItemType.RICH_TOOLS_SELECTOR:
|
||||||
|
return (
|
||||||
|
<ToolResourceSelectors
|
||||||
|
mode="tools"
|
||||||
|
pipelineId={systemContext?.pipeline_id as string | undefined}
|
||||||
|
value={{
|
||||||
|
...(formValues || {}),
|
||||||
|
[field.name]: field.value,
|
||||||
|
}}
|
||||||
|
onChange={handleCompositePatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case DynamicFormItemType.RESOURCES_SELECTOR:
|
||||||
|
return (
|
||||||
|
<ToolResourceSelectors
|
||||||
|
mode="resources"
|
||||||
|
pipelineId={systemContext?.pipeline_id as string | undefined}
|
||||||
|
value={{
|
||||||
|
...(formValues || {}),
|
||||||
|
[field.name]: field.value,
|
||||||
|
}}
|
||||||
|
onChange={handleCompositePatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case DynamicFormItemType.PROMPT_EDITOR: {
|
case DynamicFormItemType.PROMPT_EDITOR: {
|
||||||
// Guard: field.value may be undefined when the form resets or
|
// Guard: field.value may be undefined when the form resets or
|
||||||
// initialValues haven't propagated yet. Fall back to a default
|
// initialValues haven't propagated yet. Fall back to a default
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ export function getDefaultValues(
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
acc[item.name] = item.default;
|
acc[item.name] = item.default;
|
||||||
|
if (item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR) {
|
||||||
|
acc['enable-all-tools'] = true;
|
||||||
|
}
|
||||||
|
if (item.type === DynamicFormItemType.RESOURCES_SELECTOR) {
|
||||||
|
acc['mcp-resources'] = [];
|
||||||
|
acc['mcp-resource-agent-read-enabled'] = true;
|
||||||
|
}
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,8 @@ import MCPReadme from '@/app/home/mcp/components/mcp-form/MCPReadme';
|
|||||||
import {
|
import {
|
||||||
MCPServerRuntimeInfo,
|
MCPServerRuntimeInfo,
|
||||||
MCPTool,
|
MCPTool,
|
||||||
|
MCPResource,
|
||||||
|
MCPResourceContent,
|
||||||
MCPServer,
|
MCPServer,
|
||||||
MCPSessionStatus,
|
MCPSessionStatus,
|
||||||
MCPServerExtraArgsRemote,
|
MCPServerExtraArgsRemote,
|
||||||
@@ -244,13 +246,121 @@ function ToolsList({ tools, t }: { tools: MCPTool[]; t: TFunction }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResourcesList({
|
||||||
|
resources,
|
||||||
|
serverName,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
resources: MCPResource[];
|
||||||
|
serverName: string;
|
||||||
|
t: (key: string) => string;
|
||||||
|
}) {
|
||||||
|
const [expandedUri, setExpandedUri] = React.useState<string | null>(null);
|
||||||
|
const [resourceContent, setResourceContent] =
|
||||||
|
React.useState<MCPResourceContent | null>(null);
|
||||||
|
const [loadingContent, setLoadingContent] = React.useState(false);
|
||||||
|
|
||||||
|
const handleToggleResource = async (uri: string) => {
|
||||||
|
if (expandedUri === uri) {
|
||||||
|
setExpandedUri(null);
|
||||||
|
setResourceContent(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setExpandedUri(uri);
|
||||||
|
setResourceContent(null);
|
||||||
|
setLoadingContent(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await httpClient.readMCPServerResource(
|
||||||
|
serverName,
|
||||||
|
uri,
|
||||||
|
65536,
|
||||||
|
);
|
||||||
|
if (resp.contents && resp.contents.length > 0) {
|
||||||
|
setResourceContent(resp.contents[0]);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setResourceContent(null);
|
||||||
|
} finally {
|
||||||
|
setLoadingContent(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 pb-6">
|
||||||
|
{resources.map((resource, index) => (
|
||||||
|
<Card key={index} className="py-3 shadow-none">
|
||||||
|
<CardHeader
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => handleToggleResource(resource.uri)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-sm">{resource.name}</CardTitle>
|
||||||
|
{resource.mime_type && (
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
|
{resource.mime_type}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{resource.description && (
|
||||||
|
<CardDescription className="text-xs">
|
||||||
|
{resource.description}
|
||||||
|
</CardDescription>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-muted-foreground font-mono break-all">
|
||||||
|
{resource.uri}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
{expandedUri === resource.uri && (
|
||||||
|
<CardContent>
|
||||||
|
{loadingContent ? (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{t('mcp.loading')}
|
||||||
|
</div>
|
||||||
|
) : resourceContent?.type === 'text' && resourceContent.text ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<pre className="text-xs bg-muted p-3 rounded-md overflow-auto max-h-[200px] whitespace-pre-wrap break-all">
|
||||||
|
{resourceContent.text}
|
||||||
|
</pre>
|
||||||
|
{resourceContent.truncated && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{t('mcp.resourceTruncated')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : resourceContent?.type === 'blob' ? (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{resourceContent.binary_omitted
|
||||||
|
? t('mcp.resourceBinaryOmitted')
|
||||||
|
: t('mcp.resourceBinaryContent')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{t('mcp.resourceReadFailed')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuntimePanelContent = 'all' | 'tools' | 'resources';
|
||||||
|
|
||||||
function RuntimePanel({
|
function RuntimePanel({
|
||||||
mcpTesting,
|
mcpTesting,
|
||||||
runtimeInfo,
|
runtimeInfo,
|
||||||
|
serverName,
|
||||||
|
content = 'all',
|
||||||
t,
|
t,
|
||||||
}: {
|
}: {
|
||||||
mcpTesting: boolean;
|
mcpTesting: boolean;
|
||||||
runtimeInfo: MCPServerRuntimeInfo | null;
|
runtimeInfo: MCPServerRuntimeInfo | null;
|
||||||
|
serverName: string;
|
||||||
|
content?: RuntimePanelContent;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
}) {
|
}) {
|
||||||
// Show tools whenever we have runtime info — either an edit-mode server or a
|
// Show tools whenever we have runtime info — either an edit-mode server or a
|
||||||
@@ -259,7 +369,9 @@ function RuntimePanel({
|
|||||||
if (!runtimeInfo) {
|
if (!runtimeInfo) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[280px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
|
<div className="flex min-h-[280px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
|
||||||
{t('mcp.noToolsFound')}
|
{content === 'resources'
|
||||||
|
? t('mcp.noResourcesFound')
|
||||||
|
: t('mcp.noToolsFound')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -267,6 +379,15 @@ function RuntimePanel({
|
|||||||
const isConnected =
|
const isConnected =
|
||||||
!mcpTesting && runtimeInfo.status === MCPSessionStatus.CONNECTED;
|
!mcpTesting && runtimeInfo.status === MCPSessionStatus.CONNECTED;
|
||||||
const tools = runtimeInfo.tools || [];
|
const tools = runtimeInfo.tools || [];
|
||||||
|
const resources = runtimeInfo.resources || [];
|
||||||
|
const showTools = content === 'all' || content === 'tools';
|
||||||
|
const showResources = content === 'all' || content === 'resources';
|
||||||
|
const empty =
|
||||||
|
content === 'tools'
|
||||||
|
? tools.length === 0
|
||||||
|
: content === 'resources'
|
||||||
|
? resources.length === 0
|
||||||
|
: tools.length === 0 && resources.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
@@ -276,11 +397,26 @@ function RuntimePanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isConnected && tools.length > 0 && <ToolsList tools={tools} t={t} />}
|
{isConnected && showTools && tools.length > 0 && (
|
||||||
|
<ToolsList tools={tools} t={t} />
|
||||||
|
)}
|
||||||
|
|
||||||
{isConnected && tools.length === 0 && (
|
{isConnected && showResources && resources.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{content === 'all' && (
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{t('mcp.resourceCount', { count: resources.length })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ResourcesList resources={resources} serverName={serverName} t={t} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isConnected && empty && (
|
||||||
<div className="flex min-h-[220px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
|
<div className="flex min-h-[220px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
|
||||||
{t('mcp.noToolsFound')}
|
{content === 'resources'
|
||||||
|
? t('mcp.noResourcesFound')
|
||||||
|
: t('mcp.noToolsFound')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -732,6 +868,8 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
error_message: errorMsg,
|
error_message: errorMsg,
|
||||||
tool_count: 0,
|
tool_count: 0,
|
||||||
tools: [],
|
tools: [],
|
||||||
|
resource_count: 0,
|
||||||
|
resources: [],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
@@ -1026,20 +1164,29 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const runtimePanel = (
|
const runtimePanel = (
|
||||||
<RuntimePanel mcpTesting={mcpTesting} runtimeInfo={runtimeInfo} t={t} />
|
<RuntimePanel
|
||||||
|
mcpTesting={mcpTesting}
|
||||||
|
runtimeInfo={runtimeInfo}
|
||||||
|
serverName={form.getValues('name')}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// In edit mode the right side shows a tablist switching between the live
|
// In edit mode the right side shows a tablist switching between the live
|
||||||
// Tools list and the Docs (README captured from LangBot Space at install).
|
// Tools/resources lists and the Docs (README captured from LangBot Space at install).
|
||||||
// Create mode has neither, so it falls back to the bare runtime placeholder.
|
// Create mode has neither, so it falls back to the bare runtime placeholder.
|
||||||
// The tool count lives in the tab label (only when connected); the panel
|
// Counts live in the tab labels (only when connected); the panel
|
||||||
// body itself no longer repeats a title/subtitle.
|
// body itself no longer repeats a title/subtitle.
|
||||||
const toolsConnected =
|
const runtimeConnected =
|
||||||
!mcpTesting && runtimeInfo?.status === MCPSessionStatus.CONNECTED;
|
!mcpTesting && runtimeInfo?.status === MCPSessionStatus.CONNECTED;
|
||||||
const toolsCount = runtimeInfo?.tools?.length ?? 0;
|
const toolsCount = runtimeInfo?.tools?.length ?? 0;
|
||||||
const toolsTabLabel = toolsConnected
|
const resourcesCount = runtimeInfo?.resources?.length ?? 0;
|
||||||
|
const toolsTabLabel = runtimeConnected
|
||||||
? `${t('mcp.tabTools')} ${toolsCount}`
|
? `${t('mcp.tabTools')} ${toolsCount}`
|
||||||
: t('mcp.tabTools');
|
: t('mcp.tabTools');
|
||||||
|
const resourcesTabLabel = runtimeConnected
|
||||||
|
? `${t('mcp.tabResources')} ${resourcesCount}`
|
||||||
|
: t('mcp.tabResources');
|
||||||
|
|
||||||
const detailPanel = isEditMode ? (
|
const detailPanel = isEditMode ? (
|
||||||
<Tabs defaultValue="tools" className="flex h-full min-h-0 flex-col">
|
<Tabs defaultValue="tools" className="flex h-full min-h-0 flex-col">
|
||||||
@@ -1050,6 +1197,9 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
<TabsTrigger value="tools" className="flex-none px-4">
|
<TabsTrigger value="tools" className="flex-none px-4">
|
||||||
{toolsTabLabel}
|
{toolsTabLabel}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="resources" className="flex-none px-4">
|
||||||
|
{resourcesTabLabel}
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="docs" className="mt-4 min-h-0 flex-1 overflow-y-auto">
|
<TabsContent value="docs" className="mt-4 min-h-0 flex-1 overflow-y-auto">
|
||||||
<MCPReadme readme={readme} />
|
<MCPReadme readme={readme} />
|
||||||
@@ -1058,7 +1208,25 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
value="tools"
|
value="tools"
|
||||||
className="mt-4 min-h-0 flex-1 overflow-y-auto"
|
className="mt-4 min-h-0 flex-1 overflow-y-auto"
|
||||||
>
|
>
|
||||||
{runtimePanel}
|
<RuntimePanel
|
||||||
|
mcpTesting={mcpTesting}
|
||||||
|
runtimeInfo={runtimeInfo}
|
||||||
|
serverName={form.getValues('name')}
|
||||||
|
content="tools"
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
value="resources"
|
||||||
|
className="mt-4 min-h-0 flex-1 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<RuntimePanel
|
||||||
|
mcpTesting={mcpTesting}
|
||||||
|
runtimeInfo={runtimeInfo}
|
||||||
|
serverName={form.getValues('name')}
|
||||||
|
content="resources"
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -12,16 +12,40 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Plus, X, Server, Wrench, Sparkles } from 'lucide-react';
|
import { CircleHelp, Plus, X, Server, Wrench, Sparkles } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
import { Plugin } from '@/app/infra/entities/plugin';
|
import { Plugin } from '@/app/infra/entities/plugin';
|
||||||
import { MCPServer, Skill } from '@/app/infra/entities/api';
|
import { MCPServer, Skill } from '@/app/infra/entities/api';
|
||||||
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
|
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
|
||||||
import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice';
|
import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice';
|
||||||
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
|
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
|
||||||
|
|
||||||
|
function InfoTooltip({ label }: { label: string }) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
<CircleHelp className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-[280px]">
|
||||||
|
{label}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function PipelineExtension({
|
export default function PipelineExtension({
|
||||||
pipelineId,
|
pipelineId,
|
||||||
}: {
|
}: {
|
||||||
@@ -418,9 +442,14 @@ export default function PipelineExtension({
|
|||||||
{/* MCP Servers Section */}
|
{/* MCP Servers Section */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold text-foreground">
|
<div className="flex items-center gap-1.5">
|
||||||
{t('pipelines.extensions.mcpServersTitle')}
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
</h3>
|
{t('pipelines.extensions.mcpServersTitle')}
|
||||||
|
</h3>
|
||||||
|
<InfoTooltip
|
||||||
|
label={t('pipelines.extensions.mcpServersScopeTooltip')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label
|
<Label
|
||||||
htmlFor="enable-all-mcp-servers"
|
htmlFor="enable-all-mcp-servers"
|
||||||
@@ -428,6 +457,9 @@ export default function PipelineExtension({
|
|||||||
>
|
>
|
||||||
{t('pipelines.extensions.enableAllMCPServers')}
|
{t('pipelines.extensions.enableAllMCPServers')}
|
||||||
</Label>
|
</Label>
|
||||||
|
<InfoTooltip
|
||||||
|
label={t('pipelines.extensions.enableAllMCPServersTooltip')}
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
id="enable-all-mcp-servers"
|
id="enable-all-mcp-servers"
|
||||||
checked={enableAllMCPServers}
|
checked={enableAllMCPServers}
|
||||||
|
|||||||
@@ -427,13 +427,14 @@ export default function PipelineFormComponent({
|
|||||||
const forcedBoxTemplate =
|
const forcedBoxTemplate =
|
||||||
systemInfo.limitation?.force_box_session_id_template || '';
|
systemInfo.limitation?.force_box_session_id_template || '';
|
||||||
const boxScopeForced = !!forcedBoxTemplate;
|
const boxScopeForced = !!forcedBoxTemplate;
|
||||||
const stageSystemContext =
|
const isLocalAgentStage = formName === 'ai' && stage.name === 'local-agent';
|
||||||
stage.name === 'local-agent'
|
const stageSystemContext = isLocalAgentStage
|
||||||
? {
|
? {
|
||||||
box_available: boxAvailable,
|
box_available: boxAvailable,
|
||||||
box_scope_editable: boxAvailable && !boxScopeForced,
|
box_scope_editable: boxAvailable && !boxScopeForced,
|
||||||
}
|
pipeline_id: pipelineId,
|
||||||
: undefined;
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// When the deployment pins every pipeline to a fixed sandbox scope (SaaS
|
// When the deployment pins every pipeline to a fixed sandbox scope (SaaS
|
||||||
// ``force_box_session_id_template``), the Sandbox Scope selector is locked.
|
// ``force_box_session_id_template``), the Sandbox Scope selector is locked.
|
||||||
@@ -446,12 +447,26 @@ export default function PipelineFormComponent({
|
|||||||
const stageInitialValues: Record<string, any> =
|
const stageInitialValues: Record<string, any> =
|
||||||
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
|
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
|
||||||
const effectiveInitialValues =
|
const effectiveInitialValues =
|
||||||
stage.name === 'local-agent' && boxScopeForced
|
isLocalAgentStage && boxScopeForced
|
||||||
? {
|
? {
|
||||||
...stageInitialValues,
|
...stageInitialValues,
|
||||||
'box-session-id-template': forcedBoxTemplate,
|
'box-session-id-template': forcedBoxTemplate,
|
||||||
}
|
}
|
||||||
: stageInitialValues;
|
: stageInitialValues;
|
||||||
|
const emitStageValues = (values: object) => {
|
||||||
|
if (!isLocalAgentStage) {
|
||||||
|
handleDynamicFormEmit(formName, stage.name, values);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestStageValues =
|
||||||
|
((form.getValues(formName) as Record<string, any>) || {})[stage.name] ||
|
||||||
|
{};
|
||||||
|
handleDynamicFormEmit(formName, stage.name, {
|
||||||
|
...latestStageValues,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={stage.name}>
|
<Card key={stage.name}>
|
||||||
@@ -467,9 +482,7 @@ export default function PipelineFormComponent({
|
|||||||
<DynamicFormComponent
|
<DynamicFormComponent
|
||||||
itemConfigList={stage.config}
|
itemConfigList={stage.config}
|
||||||
initialValues={effectiveInitialValues}
|
initialValues={effectiveInitialValues}
|
||||||
onSubmit={(values) => {
|
onSubmit={emitStageValues}
|
||||||
handleDynamicFormEmit(formName, stage.name, values);
|
|
||||||
}}
|
|
||||||
systemContext={stageSystemContext}
|
systemContext={stageSystemContext}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -563,6 +563,11 @@ export interface MCPServerRuntimeInfo {
|
|||||||
* server runs inside Box. Absent when Box is unavailable. */
|
* server runs inside Box. Absent when Box is unavailable. */
|
||||||
box_session_id?: string;
|
box_session_id?: string;
|
||||||
box_enabled?: boolean;
|
box_enabled?: boolean;
|
||||||
|
resource_count: number;
|
||||||
|
resources: MCPResource[];
|
||||||
|
resource_template_count?: number;
|
||||||
|
resource_templates?: MCPResourceTemplate[];
|
||||||
|
resource_capabilities?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MCPServer =
|
export type MCPServer =
|
||||||
@@ -617,11 +622,67 @@ export interface MCPTool {
|
|||||||
parameters?: object;
|
parameters?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MCPResource {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
description: string;
|
||||||
|
mime_type: string;
|
||||||
|
size?: number;
|
||||||
|
icons?: object[];
|
||||||
|
annotations?: Record<string, unknown>;
|
||||||
|
_meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPResourceTemplate {
|
||||||
|
uri_template: string;
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
description: string;
|
||||||
|
mime_type: string;
|
||||||
|
icons?: object[];
|
||||||
|
annotations?: Record<string, unknown>;
|
||||||
|
_meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPResourceContent {
|
||||||
|
uri: string;
|
||||||
|
mime_type: string;
|
||||||
|
type: 'text' | 'blob';
|
||||||
|
text?: string;
|
||||||
|
blob?: string | null;
|
||||||
|
bytes?: number;
|
||||||
|
truncated?: boolean;
|
||||||
|
binary_omitted?: boolean;
|
||||||
|
_meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiRespMCPResources {
|
||||||
|
resources: MCPResource[];
|
||||||
|
resource_templates?: MCPResourceTemplate[];
|
||||||
|
resource_capabilities?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiRespMCPResourceContents {
|
||||||
|
contents: MCPResourceContent[];
|
||||||
|
server_name?: string;
|
||||||
|
server_uuid?: string;
|
||||||
|
uri?: string;
|
||||||
|
source?: string;
|
||||||
|
bytes?: number;
|
||||||
|
truncated?: boolean;
|
||||||
|
cache_hit?: boolean;
|
||||||
|
warnings?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginTool {
|
export interface PluginTool {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
human_desc: string;
|
human_desc: string;
|
||||||
parameters: object;
|
parameters: object;
|
||||||
|
source?: 'builtin' | 'plugin' | 'mcp' | 'skill';
|
||||||
|
source_name?: string;
|
||||||
|
source_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiRespTools {
|
export interface ApiRespTools {
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export enum DynamicFormItemType {
|
|||||||
PLUGIN_SELECTOR = 'plugin-selector',
|
PLUGIN_SELECTOR = 'plugin-selector',
|
||||||
BOT_SELECTOR = 'bot-selector',
|
BOT_SELECTOR = 'bot-selector',
|
||||||
TOOLS_SELECTOR = 'tools-selector',
|
TOOLS_SELECTOR = 'tools-selector',
|
||||||
|
RICH_TOOLS_SELECTOR = 'rich-tools-selector',
|
||||||
|
RESOURCES_SELECTOR = 'resources-selector',
|
||||||
WEBHOOK_URL = 'webhook-url',
|
WEBHOOK_URL = 'webhook-url',
|
||||||
EMBED_CODE = 'embed-code',
|
EMBED_CODE = 'embed-code',
|
||||||
QR_CODE_LOGIN = 'qr-code-login',
|
QR_CODE_LOGIN = 'qr-code-login',
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import {
|
|||||||
BoxSessionInfo,
|
BoxSessionInfo,
|
||||||
ApiRespMCPServers,
|
ApiRespMCPServers,
|
||||||
ApiRespMCPServer,
|
ApiRespMCPServer,
|
||||||
|
ApiRespMCPResources,
|
||||||
|
ApiRespMCPResourceContents,
|
||||||
MCPServer,
|
MCPServer,
|
||||||
ApiRespModelProviders,
|
ApiRespModelProviders,
|
||||||
ApiRespModelProvider,
|
ApiRespModelProvider,
|
||||||
@@ -269,10 +271,20 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
enable_all_plugins: boolean;
|
enable_all_plugins: boolean;
|
||||||
enable_all_mcp_servers: boolean;
|
enable_all_mcp_servers: boolean;
|
||||||
enable_all_skills: boolean;
|
enable_all_skills: boolean;
|
||||||
|
mcp_resource_agent_read_enabled: boolean;
|
||||||
bound_plugins: Array<{ author: string; name: string }>;
|
bound_plugins: Array<{ author: string; name: string }>;
|
||||||
available_plugins: Plugin[];
|
available_plugins: Plugin[];
|
||||||
bound_mcp_servers: string[];
|
bound_mcp_servers: string[];
|
||||||
available_mcp_servers: MCPServer[];
|
available_mcp_servers: MCPServer[];
|
||||||
|
bound_mcp_resources: Array<{
|
||||||
|
server_uuid?: string;
|
||||||
|
server_name?: string;
|
||||||
|
uri: string;
|
||||||
|
mode?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
max_bytes?: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
}>;
|
||||||
bound_skills: string[];
|
bound_skills: string[];
|
||||||
available_skills: Skill[];
|
available_skills: Skill[];
|
||||||
}> {
|
}> {
|
||||||
@@ -287,15 +299,32 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
enable_all_mcp_servers: boolean = true,
|
enable_all_mcp_servers: boolean = true,
|
||||||
bound_skills: string[] = [],
|
bound_skills: string[] = [],
|
||||||
enable_all_skills: boolean = true,
|
enable_all_skills: boolean = true,
|
||||||
|
bound_mcp_resources?: Array<{
|
||||||
|
server_uuid?: string;
|
||||||
|
server_name?: string;
|
||||||
|
uri: string;
|
||||||
|
mode?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
max_bytes?: number;
|
||||||
|
max_tokens?: number;
|
||||||
|
}>,
|
||||||
|
mcp_resource_agent_read_enabled?: boolean,
|
||||||
): Promise<object> {
|
): Promise<object> {
|
||||||
return this.put(`/api/v1/pipelines/${uuid}/extensions`, {
|
const payload: Record<string, unknown> = {
|
||||||
bound_plugins,
|
bound_plugins,
|
||||||
bound_mcp_servers,
|
bound_mcp_servers,
|
||||||
enable_all_plugins,
|
enable_all_plugins,
|
||||||
enable_all_mcp_servers,
|
enable_all_mcp_servers,
|
||||||
bound_skills,
|
bound_skills,
|
||||||
enable_all_skills,
|
enable_all_skills,
|
||||||
});
|
};
|
||||||
|
if (bound_mcp_resources !== undefined) {
|
||||||
|
payload.bound_mcp_resources = bound_mcp_resources;
|
||||||
|
}
|
||||||
|
if (mcp_resource_agent_read_enabled !== undefined) {
|
||||||
|
payload.mcp_resource_agent_read_enabled = mcp_resource_agent_read_enabled;
|
||||||
|
}
|
||||||
|
return this.put(`/api/v1/pipelines/${uuid}/extensions`, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ WebSocket Chat API ============
|
// ============ WebSocket Chat API ============
|
||||||
@@ -865,8 +894,11 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
|
|
||||||
// ========== Tools ==========
|
// ========== Tools ==========
|
||||||
|
|
||||||
public getTools(): Promise<ApiRespTools> {
|
public getTools(pipelineId?: string): Promise<ApiRespTools> {
|
||||||
return this.get('/api/v1/tools');
|
return this.get(
|
||||||
|
'/api/v1/tools',
|
||||||
|
pipelineId ? { pipeline_uuid: pipelineId } : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getToolDetail(toolName: string): Promise<ApiRespToolDetail> {
|
public getToolDetail(toolName: string): Promise<ApiRespToolDetail> {
|
||||||
@@ -926,6 +958,28 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.post('/api/v1/mcp/servers', { source });
|
return this.post('/api/v1/mcp/servers', { source });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getMCPServerResources(
|
||||||
|
serverName: string,
|
||||||
|
): Promise<ApiRespMCPResources> {
|
||||||
|
return this.get(
|
||||||
|
`/api/v1/mcp/servers/${encodeURIComponent(serverName)}/resources`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readMCPServerResource(
|
||||||
|
serverName: string,
|
||||||
|
uri: string,
|
||||||
|
maxBytes?: number,
|
||||||
|
): Promise<ApiRespMCPResourceContents> {
|
||||||
|
return this.post(
|
||||||
|
`/api/v1/mcp/servers/${encodeURIComponent(serverName)}/resources/read`,
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
max_bytes: maxBytes,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ System API ============
|
// ============ System API ============
|
||||||
public getSystemInfo(): Promise<ApiRespSystemInfo> {
|
public getSystemInfo(): Promise<ApiRespSystemInfo> {
|
||||||
return this.get('/api/v1/system/info');
|
return this.get('/api/v1/system/info');
|
||||||
|
|||||||
@@ -824,7 +824,9 @@ const enUS = {
|
|||||||
toolsFound: 'tools',
|
toolsFound: 'tools',
|
||||||
unknownError: 'Unknown error',
|
unknownError: 'Unknown error',
|
||||||
noToolsFound: 'No tools found',
|
noToolsFound: 'No tools found',
|
||||||
|
noResourcesFound: 'No resources found',
|
||||||
tabTools: 'Tools',
|
tabTools: 'Tools',
|
||||||
|
tabResources: 'Resources',
|
||||||
tabDocs: 'Docs',
|
tabDocs: 'Docs',
|
||||||
noReadme: 'No documentation available',
|
noReadme: 'No documentation available',
|
||||||
parseResultFailed: 'Failed to parse test result',
|
parseResultFailed: 'Failed to parse test result',
|
||||||
@@ -844,6 +846,11 @@ const enUS = {
|
|||||||
toolCount: 'Tools: {{count}}',
|
toolCount: 'Tools: {{count}}',
|
||||||
parameterCount: 'Parameters: {{count}}',
|
parameterCount: 'Parameters: {{count}}',
|
||||||
noParameters: 'No parameters',
|
noParameters: 'No parameters',
|
||||||
|
resourceCount: 'Resources: {{count}}',
|
||||||
|
resourceBinaryContent: 'Binary content (cannot be displayed)',
|
||||||
|
resourceBinaryOmitted: 'Binary content omitted by resource safety policy',
|
||||||
|
resourceTruncated: 'Content truncated by byte or token limits',
|
||||||
|
resourceReadFailed: 'Failed to read resource content',
|
||||||
statusConnected: 'Connected',
|
statusConnected: 'Connected',
|
||||||
statusDisconnected: 'Disconnected',
|
statusDisconnected: 'Disconnected',
|
||||||
statusError: 'Connection Error',
|
statusError: 'Connection Error',
|
||||||
@@ -935,9 +942,12 @@ const enUS = {
|
|||||||
selectPlugins: 'Select Plugins',
|
selectPlugins: 'Select Plugins',
|
||||||
pluginsTitle: 'Plugins',
|
pluginsTitle: 'Plugins',
|
||||||
mcpServersTitle: 'MCP Servers',
|
mcpServersTitle: 'MCP Servers',
|
||||||
|
mcpResourcesTitle: 'MCP Resources',
|
||||||
noMCPServersSelected: 'No MCP servers selected',
|
noMCPServersSelected: 'No MCP servers selected',
|
||||||
|
noMCPResourcesAvailable: 'No MCP resources available',
|
||||||
addMCPServer: 'Add MCP Server',
|
addMCPServer: 'Add MCP Server',
|
||||||
selectMCPServers: 'Select MCP Servers',
|
selectMCPServers: 'Select MCP Servers',
|
||||||
|
enableMCPResourceAgentRead: 'Agent read',
|
||||||
toolCount: '{{count}} tools',
|
toolCount: '{{count}} tools',
|
||||||
noPluginsInstalled: 'No installed plugins',
|
noPluginsInstalled: 'No installed plugins',
|
||||||
noMCPServersConfigured: 'No configured MCP servers',
|
noMCPServersConfigured: 'No configured MCP servers',
|
||||||
@@ -953,6 +963,42 @@ const enUS = {
|
|||||||
addSkill: 'Add Skill',
|
addSkill: 'Add Skill',
|
||||||
selectSkills: 'Select Skills',
|
selectSkills: 'Select Skills',
|
||||||
noSkillsAvailable: 'No skills available',
|
noSkillsAvailable: 'No skills available',
|
||||||
|
mcpServersScopeTooltip:
|
||||||
|
'This only controls which MCP servers are bound to the pipeline. Choose exact MCP tools and resources in AI Feature > Local Agent.',
|
||||||
|
enableAllMCPServersTooltip:
|
||||||
|
'When enabled, all configured and enabled MCP servers become candidates for MCP tools and resources in AI Feature.',
|
||||||
|
},
|
||||||
|
localAgent: {
|
||||||
|
toolsTitle: 'Tools',
|
||||||
|
toolsDescription:
|
||||||
|
'Select plugin, MCP, skill, and built-in tools available to this Local Agent.',
|
||||||
|
toolsScopeTooltip:
|
||||||
|
'MCP tools only come from MCP servers bound in Extensions. Bind another MCP server there to make its tools selectable here.',
|
||||||
|
enableAllTools: 'Enable all tools',
|
||||||
|
allToolsEnabled: 'All available tools are enabled',
|
||||||
|
noToolsSelected: 'No tools selected',
|
||||||
|
editTools: 'Edit tools',
|
||||||
|
builtinTools: 'Built-in tools',
|
||||||
|
pluginTools: 'Plugin tools',
|
||||||
|
skillTools: 'Skill tools',
|
||||||
|
mcpTools: 'MCP tools',
|
||||||
|
mcpToolsScopeTooltip:
|
||||||
|
'Only tools from MCP servers currently allowed in Extensions are shown here.',
|
||||||
|
skillToolsScopeTooltip:
|
||||||
|
'Skill tools are available when LangBot skill service and the Box sandbox backend are ready. They let the agent activate or register skills.',
|
||||||
|
selectTools: 'Select tools',
|
||||||
|
resourcesTitle: 'Resources',
|
||||||
|
resourcesDescription:
|
||||||
|
'Select MCP resources and knowledge bases available to this Local Agent.',
|
||||||
|
knowledgeBases: 'Knowledge bases',
|
||||||
|
mcpResources: 'MCP resources',
|
||||||
|
mcpResourcesScopeTooltip:
|
||||||
|
'Only resources exposed by MCP servers currently allowed in Extensions are shown here.',
|
||||||
|
enableMCPResourceRead: 'Allow model to read MCP resources',
|
||||||
|
mcpResourceReadTooltip:
|
||||||
|
'When disabled, selected MCP resources are not injected into model context.',
|
||||||
|
noMCPResourcesAvailable: 'No MCP resources available',
|
||||||
|
selectKnowledgeBases: 'Select knowledge bases',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'Pipeline Chat',
|
title: 'Pipeline Chat',
|
||||||
|
|||||||
@@ -838,7 +838,9 @@ const esES = {
|
|||||||
toolsFound: 'herramientas',
|
toolsFound: 'herramientas',
|
||||||
unknownError: 'Error desconocido',
|
unknownError: 'Error desconocido',
|
||||||
noToolsFound: 'No se encontraron herramientas',
|
noToolsFound: 'No se encontraron herramientas',
|
||||||
|
noResourcesFound: 'No se encontraron recursos',
|
||||||
tabTools: 'Herramientas',
|
tabTools: 'Herramientas',
|
||||||
|
tabResources: 'Recursos',
|
||||||
tabDocs: 'Documentación',
|
tabDocs: 'Documentación',
|
||||||
noReadme: 'No hay documentación disponible',
|
noReadme: 'No hay documentación disponible',
|
||||||
parseResultFailed: 'Error al analizar el resultado de la prueba',
|
parseResultFailed: 'Error al analizar el resultado de la prueba',
|
||||||
@@ -858,6 +860,12 @@ const esES = {
|
|||||||
toolCount: 'Herramientas: {{count}}',
|
toolCount: 'Herramientas: {{count}}',
|
||||||
parameterCount: 'Parámetros: {{count}}',
|
parameterCount: 'Parámetros: {{count}}',
|
||||||
noParameters: 'Sin parámetros',
|
noParameters: 'Sin parámetros',
|
||||||
|
resourceCount: 'Recursos: {{count}}',
|
||||||
|
resourceBinaryContent: 'Contenido binario (no se puede mostrar)',
|
||||||
|
resourceBinaryOmitted:
|
||||||
|
'Contenido binario omitido por la política de seguridad de recursos',
|
||||||
|
resourceTruncated: 'Contenido truncado por límites de bytes o tokens',
|
||||||
|
resourceReadFailed: 'Error al leer el contenido del recurso',
|
||||||
statusConnected: 'Conectado',
|
statusConnected: 'Conectado',
|
||||||
statusDisconnected: 'Desconectado',
|
statusDisconnected: 'Desconectado',
|
||||||
statusError: 'Error de conexión',
|
statusError: 'Error de conexión',
|
||||||
@@ -952,9 +960,12 @@ const esES = {
|
|||||||
selectPlugins: 'Seleccionar plugins',
|
selectPlugins: 'Seleccionar plugins',
|
||||||
pluginsTitle: 'Plugins',
|
pluginsTitle: 'Plugins',
|
||||||
mcpServersTitle: 'Servidores MCP',
|
mcpServersTitle: 'Servidores MCP',
|
||||||
|
mcpResourcesTitle: 'Recursos MCP',
|
||||||
noMCPServersSelected: 'No hay servidores MCP seleccionados',
|
noMCPServersSelected: 'No hay servidores MCP seleccionados',
|
||||||
|
noMCPResourcesAvailable: 'No hay recursos MCP disponibles',
|
||||||
addMCPServer: 'Añadir servidor MCP',
|
addMCPServer: 'Añadir servidor MCP',
|
||||||
selectMCPServers: 'Seleccionar servidores MCP',
|
selectMCPServers: 'Seleccionar servidores MCP',
|
||||||
|
enableMCPResourceAgentRead: 'Permitir lectura del modelo',
|
||||||
toolCount: '{{count}} herramientas',
|
toolCount: '{{count}} herramientas',
|
||||||
noPluginsInstalled: 'No hay plugins instalados',
|
noPluginsInstalled: 'No hay plugins instalados',
|
||||||
noMCPServersConfigured: 'No hay servidores MCP configurados',
|
noMCPServersConfigured: 'No hay servidores MCP configurados',
|
||||||
@@ -970,6 +981,40 @@ const esES = {
|
|||||||
addSkill: 'Añadir skill',
|
addSkill: 'Añadir skill',
|
||||||
selectSkills: 'Seleccionar skills',
|
selectSkills: 'Seleccionar skills',
|
||||||
noSkillsAvailable: 'No hay skills disponibles',
|
noSkillsAvailable: 'No hay skills disponibles',
|
||||||
|
mcpServersScopeTooltip:
|
||||||
|
'Aquí solo se controla qué servidores MCP se vinculan al Pipeline. Las herramientas y recursos MCP concretos se eligen en AI Feature > Local Agent.',
|
||||||
|
enableAllMCPServersTooltip:
|
||||||
|
'Al activarlo, todos los servidores MCP configurados y habilitados serán candidatos para herramientas y recursos MCP en AI Feature.',
|
||||||
|
},
|
||||||
|
localAgent: {
|
||||||
|
toolsTitle: 'Herramientas',
|
||||||
|
toolsDescription:
|
||||||
|
'Selecciona las herramientas de plugins, MCP e integradas disponibles para este Local Agent.',
|
||||||
|
toolsScopeTooltip:
|
||||||
|
'Las herramientas MCP solo provienen de servidores MCP vinculados en Extensiones. Vincula allí otro servidor para poder seleccionarlo aquí.',
|
||||||
|
enableAllTools: 'Activar todas las herramientas',
|
||||||
|
allToolsEnabled: 'Todas las herramientas disponibles están activadas',
|
||||||
|
noToolsSelected: 'No hay herramientas seleccionadas',
|
||||||
|
editTools: 'Editar herramientas',
|
||||||
|
builtinTools: 'Herramientas integradas',
|
||||||
|
pluginTools: 'Herramientas de plugin',
|
||||||
|
skillTools: 'Herramientas de skill',
|
||||||
|
mcpTools: 'Herramientas MCP',
|
||||||
|
mcpToolsScopeTooltip:
|
||||||
|
'Aquí solo se muestran herramientas de servidores MCP permitidos actualmente en Extensiones.',
|
||||||
|
selectTools: 'Seleccionar herramientas',
|
||||||
|
resourcesTitle: 'Recursos',
|
||||||
|
resourcesDescription:
|
||||||
|
'Selecciona los recursos MCP y bases de conocimiento disponibles para este Local Agent.',
|
||||||
|
knowledgeBases: 'Bases de conocimiento',
|
||||||
|
mcpResources: 'Recursos MCP',
|
||||||
|
mcpResourcesScopeTooltip:
|
||||||
|
'Aquí solo se muestran recursos expuestos por servidores MCP permitidos actualmente en Extensiones.',
|
||||||
|
enableMCPResourceRead: 'Permitir que el modelo lea recursos MCP',
|
||||||
|
mcpResourceReadTooltip:
|
||||||
|
'Si se desactiva, los recursos MCP seleccionados no se inyectarán en el contexto del modelo.',
|
||||||
|
noMCPResourcesAvailable: 'No hay recursos MCP disponibles',
|
||||||
|
selectKnowledgeBases: 'Seleccionar bases de conocimiento',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'Chat del Pipeline',
|
title: 'Chat del Pipeline',
|
||||||
|
|||||||
@@ -830,7 +830,9 @@ const jaJP = {
|
|||||||
toolsFound: '個のツール',
|
toolsFound: '個のツール',
|
||||||
unknownError: '不明なエラー',
|
unknownError: '不明なエラー',
|
||||||
noToolsFound: 'ツールが見つかりません',
|
noToolsFound: 'ツールが見つかりません',
|
||||||
|
noResourcesFound: 'リソースが見つかりません',
|
||||||
tabTools: 'ツール',
|
tabTools: 'ツール',
|
||||||
|
tabResources: 'リソース',
|
||||||
tabDocs: 'ドキュメント',
|
tabDocs: 'ドキュメント',
|
||||||
noReadme: 'ドキュメントがありません',
|
noReadme: 'ドキュメントがありません',
|
||||||
parseResultFailed: 'テスト結果の解析に失敗しました',
|
parseResultFailed: 'テスト結果の解析に失敗しました',
|
||||||
@@ -850,6 +852,12 @@ const jaJP = {
|
|||||||
toolCount: 'ツール:{{count}}',
|
toolCount: 'ツール:{{count}}',
|
||||||
parameterCount: 'パラメータ:{{count}}',
|
parameterCount: 'パラメータ:{{count}}',
|
||||||
noParameters: 'パラメータなし',
|
noParameters: 'パラメータなし',
|
||||||
|
resourceCount: 'リソース:{{count}}',
|
||||||
|
resourceBinaryContent: 'バイナリコンテンツ(表示できません)',
|
||||||
|
resourceBinaryOmitted:
|
||||||
|
'リソース安全ポリシーによりバイナリコンテンツを省略しました',
|
||||||
|
resourceTruncated: 'バイトまたはトークンの上限により内容を切り詰めました',
|
||||||
|
resourceReadFailed: 'リソースの読み込みに失敗しました',
|
||||||
statusConnected: '接続済み',
|
statusConnected: '接続済み',
|
||||||
statusDisconnected: '未接続',
|
statusDisconnected: '未接続',
|
||||||
statusError: '接続エラー',
|
statusError: '接続エラー',
|
||||||
@@ -939,9 +947,12 @@ const jaJP = {
|
|||||||
selectPlugins: 'プラグインを選択',
|
selectPlugins: 'プラグインを選択',
|
||||||
pluginsTitle: 'プラグイン',
|
pluginsTitle: 'プラグイン',
|
||||||
mcpServersTitle: 'MCPサーバー',
|
mcpServersTitle: 'MCPサーバー',
|
||||||
|
mcpResourcesTitle: 'MCPリソース',
|
||||||
noMCPServersSelected: 'MCPサーバーが選択されていません',
|
noMCPServersSelected: 'MCPサーバーが選択されていません',
|
||||||
|
noMCPResourcesAvailable: '利用可能なMCPリソースがありません',
|
||||||
addMCPServer: 'MCPサーバーを追加',
|
addMCPServer: 'MCPサーバーを追加',
|
||||||
selectMCPServers: 'MCPサーバーを選択',
|
selectMCPServers: 'MCPサーバーを選択',
|
||||||
|
enableMCPResourceAgentRead: 'モデルの読み取りを許可',
|
||||||
toolCount: '{{count}}個のツール',
|
toolCount: '{{count}}個のツール',
|
||||||
noPluginsInstalled: 'インストールされているプラグインがありません',
|
noPluginsInstalled: 'インストールされているプラグインがありません',
|
||||||
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
||||||
@@ -957,6 +968,40 @@ const jaJP = {
|
|||||||
addSkill: 'スキルを追加',
|
addSkill: 'スキルを追加',
|
||||||
selectSkills: 'スキルを選択',
|
selectSkills: 'スキルを選択',
|
||||||
noSkillsAvailable: '利用可能なスキルがありません',
|
noSkillsAvailable: '利用可能なスキルがありません',
|
||||||
|
mcpServersScopeTooltip:
|
||||||
|
'ここでは、このパイプラインに紐付ける MCP サーバーだけを管理します。個別の MCP ツールとリソースは AI 機能の Local Agent で選択します。',
|
||||||
|
enableAllMCPServersTooltip:
|
||||||
|
'有効にすると、設定済みで有効なすべての MCP サーバーが AI 機能の MCP ツールとリソース候補になります。',
|
||||||
|
},
|
||||||
|
localAgent: {
|
||||||
|
toolsTitle: 'ツール',
|
||||||
|
toolsDescription:
|
||||||
|
'この Local Agent が使用できるプラグイン、MCP、組み込みツールを選択します。',
|
||||||
|
toolsScopeTooltip:
|
||||||
|
'MCP ツールは拡張機能で紐付けられた MCP サーバーからのみ表示されます。追加するには先に拡張機能でサーバーを紐付けてください。',
|
||||||
|
enableAllTools: 'すべてのツールを有効化',
|
||||||
|
allToolsEnabled: '利用可能なすべてのツールが有効です',
|
||||||
|
noToolsSelected: 'ツールが選択されていません',
|
||||||
|
editTools: 'ツールを編集',
|
||||||
|
builtinTools: '組み込みツール',
|
||||||
|
pluginTools: 'プラグインツール',
|
||||||
|
skillTools: 'スキルツール',
|
||||||
|
mcpTools: 'MCP ツール',
|
||||||
|
mcpToolsScopeTooltip:
|
||||||
|
'拡張機能で現在許可されている MCP サーバーのツールだけが表示されます。',
|
||||||
|
selectTools: 'ツールを選択',
|
||||||
|
resourcesTitle: 'リソース',
|
||||||
|
resourcesDescription:
|
||||||
|
'この Local Agent が読み取れる MCP リソースとナレッジベースを選択します。',
|
||||||
|
knowledgeBases: 'ナレッジベース',
|
||||||
|
mcpResources: 'MCP リソース',
|
||||||
|
mcpResourcesScopeTooltip:
|
||||||
|
'拡張機能で現在許可されている MCP サーバーのリソースだけが表示されます。',
|
||||||
|
enableMCPResourceRead: 'モデルによる MCP リソース読み取りを許可',
|
||||||
|
mcpResourceReadTooltip:
|
||||||
|
'無効にすると、選択済みの MCP リソースもモデルコンテキストに注入されません。',
|
||||||
|
noMCPResourcesAvailable: '利用可能な MCP リソースがありません',
|
||||||
|
selectKnowledgeBases: 'ナレッジベースを選択',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'パイプラインのチャット',
|
title: 'パイプラインのチャット',
|
||||||
|
|||||||
@@ -835,7 +835,9 @@ const ruRU = {
|
|||||||
toolsFound: 'инструментов',
|
toolsFound: 'инструментов',
|
||||||
unknownError: 'Неизвестная ошибка',
|
unknownError: 'Неизвестная ошибка',
|
||||||
noToolsFound: 'Инструменты не найдены',
|
noToolsFound: 'Инструменты не найдены',
|
||||||
|
noResourcesFound: 'Ресурсы не найдены',
|
||||||
tabTools: 'Инструменты',
|
tabTools: 'Инструменты',
|
||||||
|
tabResources: 'Ресурсы',
|
||||||
tabDocs: 'Документация',
|
tabDocs: 'Документация',
|
||||||
noReadme: 'Документация отсутствует',
|
noReadme: 'Документация отсутствует',
|
||||||
parseResultFailed: 'Не удалось разобрать результат теста',
|
parseResultFailed: 'Не удалось разобрать результат теста',
|
||||||
@@ -855,6 +857,12 @@ const ruRU = {
|
|||||||
toolCount: 'Инструменты: {{count}}',
|
toolCount: 'Инструменты: {{count}}',
|
||||||
parameterCount: 'Параметры: {{count}}',
|
parameterCount: 'Параметры: {{count}}',
|
||||||
noParameters: 'Нет параметров',
|
noParameters: 'Нет параметров',
|
||||||
|
resourceCount: 'Ресурсы: {{count}}',
|
||||||
|
resourceBinaryContent: 'Двоичное содержимое (невозможно отобразить)',
|
||||||
|
resourceBinaryOmitted:
|
||||||
|
'Двоичное содержимое опущено согласно политике безопасности ресурсов',
|
||||||
|
resourceTruncated: 'Содержимое усечено по лимитам байтов или токенов',
|
||||||
|
resourceReadFailed: 'Не удалось прочитать содержимое ресурса',
|
||||||
statusConnected: 'Подключён',
|
statusConnected: 'Подключён',
|
||||||
statusDisconnected: 'Отключён',
|
statusDisconnected: 'Отключён',
|
||||||
statusError: 'Ошибка подключения',
|
statusError: 'Ошибка подключения',
|
||||||
@@ -945,9 +953,12 @@ const ruRU = {
|
|||||||
selectPlugins: 'Выберите плагины',
|
selectPlugins: 'Выберите плагины',
|
||||||
pluginsTitle: 'Плагины',
|
pluginsTitle: 'Плагины',
|
||||||
mcpServersTitle: 'MCP-серверы',
|
mcpServersTitle: 'MCP-серверы',
|
||||||
|
mcpResourcesTitle: 'MCP-ресурсы',
|
||||||
noMCPServersSelected: 'MCP-серверы не выбраны',
|
noMCPServersSelected: 'MCP-серверы не выбраны',
|
||||||
|
noMCPResourcesAvailable: 'Нет доступных MCP-ресурсов',
|
||||||
addMCPServer: 'Добавить MCP-сервер',
|
addMCPServer: 'Добавить MCP-сервер',
|
||||||
selectMCPServers: 'Выберите MCP-серверы',
|
selectMCPServers: 'Выберите MCP-серверы',
|
||||||
|
enableMCPResourceAgentRead: 'Разрешить модели чтение',
|
||||||
toolCount: '{{count}} инструментов',
|
toolCount: '{{count}} инструментов',
|
||||||
noPluginsInstalled: 'Нет установленных плагинов',
|
noPluginsInstalled: 'Нет установленных плагинов',
|
||||||
noMCPServersConfigured: 'Нет настроенных MCP-серверов',
|
noMCPServersConfigured: 'Нет настроенных MCP-серверов',
|
||||||
@@ -963,6 +974,40 @@ const ruRU = {
|
|||||||
addSkill: 'Добавить навык',
|
addSkill: 'Добавить навык',
|
||||||
selectSkills: 'Выбрать навыки',
|
selectSkills: 'Выбрать навыки',
|
||||||
noSkillsAvailable: 'Нет доступных навыков',
|
noSkillsAvailable: 'Нет доступных навыков',
|
||||||
|
mcpServersScopeTooltip:
|
||||||
|
'Здесь задаётся только привязка MCP-серверов к конвейеру. Конкретные MCP-инструменты и ресурсы выбираются в AI Feature > Local Agent.',
|
||||||
|
enableAllMCPServersTooltip:
|
||||||
|
'Если включено, все настроенные и включённые MCP-серверы станут кандидатами для инструментов и ресурсов MCP в AI Feature.',
|
||||||
|
},
|
||||||
|
localAgent: {
|
||||||
|
toolsTitle: 'Инструменты',
|
||||||
|
toolsDescription:
|
||||||
|
'Выберите инструменты плагинов, MCP и встроенные инструменты для этого Local Agent.',
|
||||||
|
toolsScopeTooltip:
|
||||||
|
'MCP-инструменты берутся только из MCP-серверов, привязанных в Расширениях. Чтобы добавить источник, сначала привяжите там сервер.',
|
||||||
|
enableAllTools: 'Включить все инструменты',
|
||||||
|
allToolsEnabled: 'Все доступные инструменты включены',
|
||||||
|
noToolsSelected: 'Инструменты не выбраны',
|
||||||
|
editTools: 'Редактировать инструменты',
|
||||||
|
builtinTools: 'Встроенные инструменты',
|
||||||
|
pluginTools: 'Инструменты плагинов',
|
||||||
|
skillTools: 'Инструменты навыков',
|
||||||
|
mcpTools: 'Инструменты MCP',
|
||||||
|
mcpToolsScopeTooltip:
|
||||||
|
'Здесь показаны только инструменты MCP-серверов, разрешённых сейчас в Расширениях.',
|
||||||
|
selectTools: 'Выбрать инструменты',
|
||||||
|
resourcesTitle: 'Ресурсы',
|
||||||
|
resourcesDescription:
|
||||||
|
'Выберите MCP-ресурсы и базы знаний для этого Local Agent.',
|
||||||
|
knowledgeBases: 'Базы знаний',
|
||||||
|
mcpResources: 'MCP-ресурсы',
|
||||||
|
mcpResourcesScopeTooltip:
|
||||||
|
'Здесь показаны только ресурсы MCP-серверов, разрешённых сейчас в Расширениях.',
|
||||||
|
enableMCPResourceRead: 'Разрешить модели читать MCP-ресурсы',
|
||||||
|
mcpResourceReadTooltip:
|
||||||
|
'Если выключено, выбранные MCP-ресурсы не будут добавляться в контекст модели.',
|
||||||
|
noMCPResourcesAvailable: 'Нет доступных MCP-ресурсов',
|
||||||
|
selectKnowledgeBases: 'Выбрать базы знаний',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'Чат конвейера',
|
title: 'Чат конвейера',
|
||||||
|
|||||||
@@ -813,7 +813,9 @@ const thTH = {
|
|||||||
toolsFound: 'เครื่องมือ',
|
toolsFound: 'เครื่องมือ',
|
||||||
unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ',
|
unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ',
|
||||||
noToolsFound: 'ไม่พบเครื่องมือ',
|
noToolsFound: 'ไม่พบเครื่องมือ',
|
||||||
|
noResourcesFound: 'ไม่พบทรัพยากร',
|
||||||
tabTools: 'เครื่องมือ',
|
tabTools: 'เครื่องมือ',
|
||||||
|
tabResources: 'ทรัพยากร',
|
||||||
tabDocs: 'เอกสาร',
|
tabDocs: 'เอกสาร',
|
||||||
noReadme: 'ไม่มีเอกสาร',
|
noReadme: 'ไม่มีเอกสาร',
|
||||||
parseResultFailed: 'ไม่สามารถแยกวิเคราะห์ผลการทดสอบได้',
|
parseResultFailed: 'ไม่สามารถแยกวิเคราะห์ผลการทดสอบได้',
|
||||||
@@ -833,6 +835,11 @@ const thTH = {
|
|||||||
toolCount: 'เครื่องมือ: {{count}}',
|
toolCount: 'เครื่องมือ: {{count}}',
|
||||||
parameterCount: 'พารามิเตอร์: {{count}}',
|
parameterCount: 'พารามิเตอร์: {{count}}',
|
||||||
noParameters: 'ไม่มีพารามิเตอร์',
|
noParameters: 'ไม่มีพารามิเตอร์',
|
||||||
|
resourceCount: 'ทรัพยากร: {{count}}',
|
||||||
|
resourceBinaryContent: 'เนื้อหาไบนารี (ไม่สามารถแสดงได้)',
|
||||||
|
resourceBinaryOmitted: 'ละเว้นเนื้อหาไบนารีตามนโยบายความปลอดภัยของทรัพยากร',
|
||||||
|
resourceTruncated: 'ตัดเนื้อหาตามขีดจำกัดไบต์หรือโทเคน',
|
||||||
|
resourceReadFailed: 'ไม่สามารถอ่านเนื้อหาทรัพยากรได้',
|
||||||
statusConnected: 'เชื่อมต่อแล้ว',
|
statusConnected: 'เชื่อมต่อแล้ว',
|
||||||
statusDisconnected: 'ไม่ได้เชื่อมต่อ',
|
statusDisconnected: 'ไม่ได้เชื่อมต่อ',
|
||||||
statusError: 'ข้อผิดพลาดการเชื่อมต่อ',
|
statusError: 'ข้อผิดพลาดการเชื่อมต่อ',
|
||||||
@@ -922,9 +929,12 @@ const thTH = {
|
|||||||
selectPlugins: 'เลือกปลั๊กอิน',
|
selectPlugins: 'เลือกปลั๊กอิน',
|
||||||
pluginsTitle: 'ปลั๊กอิน',
|
pluginsTitle: 'ปลั๊กอิน',
|
||||||
mcpServersTitle: 'เซิร์ฟเวอร์ MCP',
|
mcpServersTitle: 'เซิร์ฟเวอร์ MCP',
|
||||||
|
mcpResourcesTitle: 'ทรัพยากร MCP',
|
||||||
noMCPServersSelected: 'ไม่ได้เลือกเซิร์ฟเวอร์ MCP',
|
noMCPServersSelected: 'ไม่ได้เลือกเซิร์ฟเวอร์ MCP',
|
||||||
|
noMCPResourcesAvailable: 'ไม่มีทรัพยากร MCP ที่พร้อมใช้งาน',
|
||||||
addMCPServer: 'เพิ่มเซิร์ฟเวอร์ MCP',
|
addMCPServer: 'เพิ่มเซิร์ฟเวอร์ MCP',
|
||||||
selectMCPServers: 'เลือกเซิร์ฟเวอร์ MCP',
|
selectMCPServers: 'เลือกเซิร์ฟเวอร์ MCP',
|
||||||
|
enableMCPResourceAgentRead: 'อนุญาตให้โมเดลอ่าน',
|
||||||
toolCount: '{{count}} เครื่องมือ',
|
toolCount: '{{count}} เครื่องมือ',
|
||||||
noPluginsInstalled: 'ไม่มีปลั๊กอินที่ติดตั้ง',
|
noPluginsInstalled: 'ไม่มีปลั๊กอินที่ติดตั้ง',
|
||||||
noMCPServersConfigured: 'ไม่มีเซิร์ฟเวอร์ MCP ที่กำหนดค่า',
|
noMCPServersConfigured: 'ไม่มีเซิร์ฟเวอร์ MCP ที่กำหนดค่า',
|
||||||
@@ -940,6 +950,40 @@ const thTH = {
|
|||||||
addSkill: 'เพิ่มสกิล',
|
addSkill: 'เพิ่มสกิล',
|
||||||
selectSkills: 'เลือกสกิล',
|
selectSkills: 'เลือกสกิล',
|
||||||
noSkillsAvailable: 'ไม่มีสกิลที่พร้อมใช้งาน',
|
noSkillsAvailable: 'ไม่มีสกิลที่พร้อมใช้งาน',
|
||||||
|
mcpServersScopeTooltip:
|
||||||
|
'ส่วนนี้ใช้ควบคุมว่า Pipeline ผูกกับเซิร์ฟเวอร์ MCP ใดเท่านั้น ส่วนเครื่องมือและทรัพยากร MCP รายตัวให้เลือกใน AI Feature > Local Agent',
|
||||||
|
enableAllMCPServersTooltip:
|
||||||
|
'เมื่อเปิดใช้ เซิร์ฟเวอร์ MCP ที่ตั้งค่าและเปิดใช้งานทั้งหมดจะเป็นตัวเลือกสำหรับเครื่องมือและทรัพยากร MCP ใน AI Feature',
|
||||||
|
},
|
||||||
|
localAgent: {
|
||||||
|
toolsTitle: 'เครื่องมือ',
|
||||||
|
toolsDescription:
|
||||||
|
'เลือกเครื่องมือจากปลั๊กอิน MCP และเครื่องมือในตัวสำหรับ Local Agent นี้',
|
||||||
|
toolsScopeTooltip:
|
||||||
|
'เครื่องมือ MCP จะแสดงจากเซิร์ฟเวอร์ MCP ที่ผูกไว้ในส่วนขยายเท่านั้น หากต้องการเพิ่มแหล่งเครื่องมือ ให้ไปผูกเซิร์ฟเวอร์ที่นั่นก่อน',
|
||||||
|
enableAllTools: 'เปิดใช้เครื่องมือทั้งหมด',
|
||||||
|
allToolsEnabled: 'เปิดใช้เครื่องมือที่มีทั้งหมดแล้ว',
|
||||||
|
noToolsSelected: 'ยังไม่ได้เลือกเครื่องมือ',
|
||||||
|
editTools: 'แก้ไขเครื่องมือ',
|
||||||
|
builtinTools: 'เครื่องมือในตัว',
|
||||||
|
pluginTools: 'เครื่องมือปลั๊กอิน',
|
||||||
|
skillTools: 'เครื่องมือสกิล',
|
||||||
|
mcpTools: 'เครื่องมือ MCP',
|
||||||
|
mcpToolsScopeTooltip:
|
||||||
|
'ที่นี่จะแสดงเฉพาะเครื่องมือจากเซิร์ฟเวอร์ MCP ที่อนุญาตอยู่ในส่วนขยาย',
|
||||||
|
selectTools: 'เลือกเครื่องมือ',
|
||||||
|
resourcesTitle: 'ทรัพยากร',
|
||||||
|
resourcesDescription:
|
||||||
|
'เลือกทรัพยากร MCP และคลังความรู้สำหรับ Local Agent นี้',
|
||||||
|
knowledgeBases: 'คลังความรู้',
|
||||||
|
mcpResources: 'ทรัพยากร MCP',
|
||||||
|
mcpResourcesScopeTooltip:
|
||||||
|
'ที่นี่จะแสดงเฉพาะทรัพยากรจากเซิร์ฟเวอร์ MCP ที่อนุญาตอยู่ในส่วนขยาย',
|
||||||
|
enableMCPResourceRead: 'อนุญาตให้โมเดลอ่านทรัพยากร MCP',
|
||||||
|
mcpResourceReadTooltip:
|
||||||
|
'เมื่อปิด ทรัพยากร MCP ที่เลือกไว้จะไม่ถูกใส่เข้าไปในบริบทของโมเดล',
|
||||||
|
noMCPResourcesAvailable: 'ไม่มีทรัพยากร MCP ที่พร้อมใช้งาน',
|
||||||
|
selectKnowledgeBases: 'เลือกคลังความรู้',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'แชท Pipeline',
|
title: 'แชท Pipeline',
|
||||||
|
|||||||
@@ -828,7 +828,9 @@ const viVN = {
|
|||||||
toolsFound: 'công cụ',
|
toolsFound: 'công cụ',
|
||||||
unknownError: 'Lỗi không xác định',
|
unknownError: 'Lỗi không xác định',
|
||||||
noToolsFound: 'Không tìm thấy công cụ nào',
|
noToolsFound: 'Không tìm thấy công cụ nào',
|
||||||
|
noResourcesFound: 'Không tìm thấy tài nguyên nào',
|
||||||
tabTools: 'Công cụ',
|
tabTools: 'Công cụ',
|
||||||
|
tabResources: 'Tài nguyên',
|
||||||
tabDocs: 'Tài liệu',
|
tabDocs: 'Tài liệu',
|
||||||
noReadme: 'Không có tài liệu',
|
noReadme: 'Không có tài liệu',
|
||||||
parseResultFailed: 'Phân tích kết quả kiểm tra thất bại',
|
parseResultFailed: 'Phân tích kết quả kiểm tra thất bại',
|
||||||
@@ -848,6 +850,12 @@ const viVN = {
|
|||||||
toolCount: 'Công cụ: {{count}}',
|
toolCount: 'Công cụ: {{count}}',
|
||||||
parameterCount: 'Tham số: {{count}}',
|
parameterCount: 'Tham số: {{count}}',
|
||||||
noParameters: 'Không có tham số',
|
noParameters: 'Không có tham số',
|
||||||
|
resourceCount: 'Tài nguyên: {{count}}',
|
||||||
|
resourceBinaryContent: 'Nội dung nhị phân (không thể hiển thị)',
|
||||||
|
resourceBinaryOmitted:
|
||||||
|
'Nội dung nhị phân đã bị lược bỏ theo chính sách an toàn tài nguyên',
|
||||||
|
resourceTruncated: 'Nội dung đã bị cắt theo giới hạn byte hoặc token',
|
||||||
|
resourceReadFailed: 'Không thể đọc nội dung tài nguyên',
|
||||||
statusConnected: 'Đã kết nối',
|
statusConnected: 'Đã kết nối',
|
||||||
statusDisconnected: 'Đã ngắt kết nối',
|
statusDisconnected: 'Đã ngắt kết nối',
|
||||||
statusError: 'Lỗi kết nối',
|
statusError: 'Lỗi kết nối',
|
||||||
@@ -937,9 +945,12 @@ const viVN = {
|
|||||||
selectPlugins: 'Chọn Plugin',
|
selectPlugins: 'Chọn Plugin',
|
||||||
pluginsTitle: 'Plugin',
|
pluginsTitle: 'Plugin',
|
||||||
mcpServersTitle: 'Máy chủ MCP',
|
mcpServersTitle: 'Máy chủ MCP',
|
||||||
|
mcpResourcesTitle: 'Tài nguyên MCP',
|
||||||
noMCPServersSelected: 'Chưa chọn máy chủ MCP nào',
|
noMCPServersSelected: 'Chưa chọn máy chủ MCP nào',
|
||||||
|
noMCPResourcesAvailable: 'Không có tài nguyên MCP nào',
|
||||||
addMCPServer: 'Thêm máy chủ MCP',
|
addMCPServer: 'Thêm máy chủ MCP',
|
||||||
selectMCPServers: 'Chọn máy chủ MCP',
|
selectMCPServers: 'Chọn máy chủ MCP',
|
||||||
|
enableMCPResourceAgentRead: 'Cho phép mô hình đọc',
|
||||||
toolCount: '{{count}} công cụ',
|
toolCount: '{{count}} công cụ',
|
||||||
noPluginsInstalled: 'Chưa cài đặt plugin nào',
|
noPluginsInstalled: 'Chưa cài đặt plugin nào',
|
||||||
noMCPServersConfigured: 'Chưa cấu hình máy chủ MCP nào',
|
noMCPServersConfigured: 'Chưa cấu hình máy chủ MCP nào',
|
||||||
@@ -955,6 +966,40 @@ const viVN = {
|
|||||||
addSkill: 'Thêm kỹ năng',
|
addSkill: 'Thêm kỹ năng',
|
||||||
selectSkills: 'Chọn kỹ năng',
|
selectSkills: 'Chọn kỹ năng',
|
||||||
noSkillsAvailable: 'Không có kỹ năng khả dụng',
|
noSkillsAvailable: 'Không có kỹ năng khả dụng',
|
||||||
|
mcpServersScopeTooltip:
|
||||||
|
'Tại đây chỉ kiểm soát máy chủ MCP được liên kết với Pipeline. Công cụ và tài nguyên MCP cụ thể được chọn trong AI Feature > Local Agent.',
|
||||||
|
enableAllMCPServersTooltip:
|
||||||
|
'Khi bật, mọi máy chủ MCP đã cấu hình và bật sẽ trở thành ứng viên cho công cụ và tài nguyên MCP trong AI Feature.',
|
||||||
|
},
|
||||||
|
localAgent: {
|
||||||
|
toolsTitle: 'Công cụ',
|
||||||
|
toolsDescription:
|
||||||
|
'Chọn công cụ plugin, MCP và công cụ tích hợp sẵn cho Local Agent này.',
|
||||||
|
toolsScopeTooltip:
|
||||||
|
'Công cụ MCP chỉ đến từ máy chủ MCP đã liên kết trong Tiện ích mở rộng. Hãy liên kết máy chủ tại đó trước nếu muốn chọn thêm tại đây.',
|
||||||
|
enableAllTools: 'Bật tất cả công cụ',
|
||||||
|
allToolsEnabled: 'Tất cả công cụ khả dụng đã được bật',
|
||||||
|
noToolsSelected: 'Chưa chọn công cụ nào',
|
||||||
|
editTools: 'Sửa công cụ',
|
||||||
|
builtinTools: 'Công cụ tích hợp sẵn',
|
||||||
|
pluginTools: 'Công cụ plugin',
|
||||||
|
skillTools: 'Công cụ kỹ năng',
|
||||||
|
mcpTools: 'Công cụ MCP',
|
||||||
|
mcpToolsScopeTooltip:
|
||||||
|
'Tại đây chỉ hiển thị công cụ từ máy chủ MCP hiện được cho phép trong Tiện ích mở rộng.',
|
||||||
|
selectTools: 'Chọn công cụ',
|
||||||
|
resourcesTitle: 'Tài nguyên',
|
||||||
|
resourcesDescription:
|
||||||
|
'Chọn tài nguyên MCP và kho tri thức cho Local Agent này.',
|
||||||
|
knowledgeBases: 'Kho tri thức',
|
||||||
|
mcpResources: 'Tài nguyên MCP',
|
||||||
|
mcpResourcesScopeTooltip:
|
||||||
|
'Tại đây chỉ hiển thị tài nguyên từ máy chủ MCP hiện được cho phép trong Tiện ích mở rộng.',
|
||||||
|
enableMCPResourceRead: 'Cho phép mô hình đọc tài nguyên MCP',
|
||||||
|
mcpResourceReadTooltip:
|
||||||
|
'Khi tắt, tài nguyên MCP đã chọn sẽ không được đưa vào ngữ cảnh mô hình.',
|
||||||
|
noMCPResourcesAvailable: 'Không có tài nguyên MCP nào',
|
||||||
|
selectKnowledgeBases: 'Chọn kho tri thức',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'Trò chuyện Pipeline',
|
title: 'Trò chuyện Pipeline',
|
||||||
|
|||||||
@@ -790,7 +790,9 @@ const zhHans = {
|
|||||||
toolsFound: '个工具',
|
toolsFound: '个工具',
|
||||||
unknownError: '未知错误',
|
unknownError: '未知错误',
|
||||||
noToolsFound: '未找到任何工具',
|
noToolsFound: '未找到任何工具',
|
||||||
|
noResourcesFound: '未找到任何资源',
|
||||||
tabTools: '工具',
|
tabTools: '工具',
|
||||||
|
tabResources: '资源',
|
||||||
tabDocs: '文档',
|
tabDocs: '文档',
|
||||||
noReadme: '暂无文档',
|
noReadme: '暂无文档',
|
||||||
parseResultFailed: '解析测试结果失败',
|
parseResultFailed: '解析测试结果失败',
|
||||||
@@ -810,6 +812,11 @@ const zhHans = {
|
|||||||
toolCount: '工具:{{count}}',
|
toolCount: '工具:{{count}}',
|
||||||
parameterCount: '参数:{{count}}',
|
parameterCount: '参数:{{count}}',
|
||||||
noParameters: '无参数',
|
noParameters: '无参数',
|
||||||
|
resourceCount: '资源:{{count}}',
|
||||||
|
resourceBinaryContent: '二进制内容(无法显示)',
|
||||||
|
resourceBinaryOmitted: '二进制内容已按资源安全策略省略',
|
||||||
|
resourceTruncated: '内容已按字节或 token 限制截断',
|
||||||
|
resourceReadFailed: '读取资源内容失败',
|
||||||
statusConnected: '已连接',
|
statusConnected: '已连接',
|
||||||
statusDisconnected: '未连接',
|
statusDisconnected: '未连接',
|
||||||
statusError: '连接错误',
|
statusError: '连接错误',
|
||||||
@@ -895,9 +902,12 @@ const zhHans = {
|
|||||||
selectPlugins: '选择插件',
|
selectPlugins: '选择插件',
|
||||||
pluginsTitle: '插件',
|
pluginsTitle: '插件',
|
||||||
mcpServersTitle: 'MCP 服务器',
|
mcpServersTitle: 'MCP 服务器',
|
||||||
|
mcpResourcesTitle: 'MCP 资源',
|
||||||
noMCPServersSelected: '未选择任何 MCP 服务器',
|
noMCPServersSelected: '未选择任何 MCP 服务器',
|
||||||
|
noMCPResourcesAvailable: '暂无可用 MCP 资源',
|
||||||
addMCPServer: '添加 MCP 服务器',
|
addMCPServer: '添加 MCP 服务器',
|
||||||
selectMCPServers: '选择 MCP 服务器',
|
selectMCPServers: '选择 MCP 服务器',
|
||||||
|
enableMCPResourceAgentRead: '允许模型读取',
|
||||||
toolCount: '{{count}} 个工具',
|
toolCount: '{{count}} 个工具',
|
||||||
noPluginsInstalled: '无已安装的插件',
|
noPluginsInstalled: '无已安装的插件',
|
||||||
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
||||||
@@ -913,6 +923,41 @@ const zhHans = {
|
|||||||
addSkill: '添加技能',
|
addSkill: '添加技能',
|
||||||
selectSkills: '选择技能',
|
selectSkills: '选择技能',
|
||||||
noSkillsAvailable: '暂无可用技能',
|
noSkillsAvailable: '暂无可用技能',
|
||||||
|
mcpServersScopeTooltip:
|
||||||
|
'这里仅控制此流水线绑定哪些 MCP 服务器;具体 MCP 工具和资源在 AI 能力的内置 Agent 表单中选择。',
|
||||||
|
enableAllMCPServersTooltip:
|
||||||
|
'开启后,所有已配置且启用的 MCP 服务器都会进入 AI 能力里的 MCP 工具和资源候选范围。',
|
||||||
|
},
|
||||||
|
localAgent: {
|
||||||
|
toolsTitle: '工具',
|
||||||
|
toolsDescription:
|
||||||
|
'选择此内置 Agent 可以调用的插件、MCP、技能和内置工具。',
|
||||||
|
toolsScopeTooltip:
|
||||||
|
'MCP 工具只会从扩展集成中已绑定的 MCP 服务器里出现;如需增加 MCP 工具来源,请先到扩展集成绑定对应服务器。',
|
||||||
|
enableAllTools: '启用所有工具',
|
||||||
|
allToolsEnabled: '已启用所有可用工具',
|
||||||
|
noToolsSelected: '未选择任何工具',
|
||||||
|
editTools: '编辑工具',
|
||||||
|
builtinTools: '内置工具',
|
||||||
|
pluginTools: '插件工具',
|
||||||
|
skillTools: '技能工具',
|
||||||
|
mcpTools: 'MCP 工具',
|
||||||
|
mcpToolsScopeTooltip:
|
||||||
|
'这里仅展示扩展集成当前允许的 MCP 服务器提供的工具。',
|
||||||
|
skillToolsScopeTooltip:
|
||||||
|
'技能工具会在 LangBot 技能服务和 Box 沙箱后端可用时出现,用于让 Agent 激活或注册技能。',
|
||||||
|
selectTools: '选择工具',
|
||||||
|
resourcesTitle: '资源',
|
||||||
|
resourcesDescription: '选择此内置 Agent 可以读取的 MCP 资源和知识库。',
|
||||||
|
knowledgeBases: '知识库',
|
||||||
|
mcpResources: 'MCP 资源',
|
||||||
|
mcpResourcesScopeTooltip:
|
||||||
|
'这里仅展示扩展集成当前允许的 MCP 服务器暴露的资源。',
|
||||||
|
enableMCPResourceRead: '允许模型读取 MCP 资源',
|
||||||
|
mcpResourceReadTooltip:
|
||||||
|
'关闭后,即使已选择资源,也不会把 MCP 资源内容注入给模型。',
|
||||||
|
noMCPResourcesAvailable: '暂无可用 MCP 资源',
|
||||||
|
selectKnowledgeBases: '选择知识库',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流水线对话',
|
title: '流水线对话',
|
||||||
|
|||||||
@@ -789,7 +789,9 @@ const zhHant = {
|
|||||||
toolsFound: '個工具',
|
toolsFound: '個工具',
|
||||||
unknownError: '未知錯誤',
|
unknownError: '未知錯誤',
|
||||||
noToolsFound: '未找到任何工具',
|
noToolsFound: '未找到任何工具',
|
||||||
|
noResourcesFound: '未找到任何資源',
|
||||||
tabTools: '工具',
|
tabTools: '工具',
|
||||||
|
tabResources: '資源',
|
||||||
tabDocs: '文件',
|
tabDocs: '文件',
|
||||||
noReadme: '暫無文件',
|
noReadme: '暫無文件',
|
||||||
parseResultFailed: '解析測試結果失敗',
|
parseResultFailed: '解析測試結果失敗',
|
||||||
@@ -809,6 +811,11 @@ const zhHant = {
|
|||||||
toolCount: '工具:{{count}}',
|
toolCount: '工具:{{count}}',
|
||||||
parameterCount: '參數:{{count}}',
|
parameterCount: '參數:{{count}}',
|
||||||
noParameters: '無參數',
|
noParameters: '無參數',
|
||||||
|
resourceCount: '資源:{{count}}',
|
||||||
|
resourceBinaryContent: '二進位內容(無法顯示)',
|
||||||
|
resourceBinaryOmitted: '二進位內容已依資源安全策略省略',
|
||||||
|
resourceTruncated: '內容已依位元組或 token 限制截斷',
|
||||||
|
resourceReadFailed: '讀取資源內容失敗',
|
||||||
statusConnected: '已連線',
|
statusConnected: '已連線',
|
||||||
statusDisconnected: '未連線',
|
statusDisconnected: '未連線',
|
||||||
statusError: '連接錯誤',
|
statusError: '連接錯誤',
|
||||||
@@ -894,9 +901,12 @@ const zhHant = {
|
|||||||
selectPlugins: '選擇插件',
|
selectPlugins: '選擇插件',
|
||||||
pluginsTitle: '插件',
|
pluginsTitle: '插件',
|
||||||
mcpServersTitle: 'MCP 伺服器',
|
mcpServersTitle: 'MCP 伺服器',
|
||||||
|
mcpResourcesTitle: 'MCP 資源',
|
||||||
noMCPServersSelected: '未選擇任何 MCP 伺服器',
|
noMCPServersSelected: '未選擇任何 MCP 伺服器',
|
||||||
|
noMCPResourcesAvailable: '暫無可用 MCP 資源',
|
||||||
addMCPServer: '新增 MCP 伺服器',
|
addMCPServer: '新增 MCP 伺服器',
|
||||||
selectMCPServers: '選擇 MCP 伺服器',
|
selectMCPServers: '選擇 MCP 伺服器',
|
||||||
|
enableMCPResourceAgentRead: '允許模型讀取',
|
||||||
toolCount: '{{count}} 個工具',
|
toolCount: '{{count}} 個工具',
|
||||||
noPluginsInstalled: '無已安裝的插件',
|
noPluginsInstalled: '無已安裝的插件',
|
||||||
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
||||||
@@ -912,6 +922,38 @@ const zhHant = {
|
|||||||
addSkill: '新增技能',
|
addSkill: '新增技能',
|
||||||
selectSkills: '選擇技能',
|
selectSkills: '選擇技能',
|
||||||
noSkillsAvailable: '暫無可用技能',
|
noSkillsAvailable: '暫無可用技能',
|
||||||
|
mcpServersScopeTooltip:
|
||||||
|
'這裡僅控制此流程線綁定哪些 MCP 伺服器;具體 MCP 工具和資源在 AI 能力的內建 Agent 表單中選擇。',
|
||||||
|
enableAllMCPServersTooltip:
|
||||||
|
'啟用後,所有已配置且啟用的 MCP 伺服器都會進入 AI 能力中的 MCP 工具和資源候選範圍。',
|
||||||
|
},
|
||||||
|
localAgent: {
|
||||||
|
toolsTitle: '工具',
|
||||||
|
toolsDescription: '選擇此內建 Agent 可以調用的插件、MCP 和內建工具。',
|
||||||
|
toolsScopeTooltip:
|
||||||
|
'MCP 工具只會從擴展集成中已綁定的 MCP 伺服器裡出現;如需增加 MCP 工具來源,請先到擴展集成綁定對應伺服器。',
|
||||||
|
enableAllTools: '啟用所有工具',
|
||||||
|
allToolsEnabled: '已啟用所有可用工具',
|
||||||
|
noToolsSelected: '未選擇任何工具',
|
||||||
|
editTools: '編輯工具',
|
||||||
|
builtinTools: '內建工具',
|
||||||
|
pluginTools: '插件工具',
|
||||||
|
skillTools: '技能工具',
|
||||||
|
mcpTools: 'MCP 工具',
|
||||||
|
mcpToolsScopeTooltip:
|
||||||
|
'這裡僅展示擴展集成目前允許的 MCP 伺服器提供的工具。',
|
||||||
|
selectTools: '選擇工具',
|
||||||
|
resourcesTitle: '資源',
|
||||||
|
resourcesDescription: '選擇此內建 Agent 可以讀取的 MCP 資源和知識庫。',
|
||||||
|
knowledgeBases: '知識庫',
|
||||||
|
mcpResources: 'MCP 資源',
|
||||||
|
mcpResourcesScopeTooltip:
|
||||||
|
'這裡僅展示擴展集成目前允許的 MCP 伺服器暴露的資源。',
|
||||||
|
enableMCPResourceRead: '允許模型讀取 MCP 資源',
|
||||||
|
mcpResourceReadTooltip:
|
||||||
|
'關閉後,即使已選擇資源,也不會把 MCP 資源內容注入給模型。',
|
||||||
|
noMCPResourcesAvailable: '暫無可用 MCP 資源',
|
||||||
|
selectKnowledgeBases: '選擇知識庫',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流程線對話',
|
title: '流程線對話',
|
||||||
|
|||||||
Reference in New Issue
Block a user