feat(extension): 接入 stdio MCP Bridge 与 Plugin Host #8

Merged
Kronecker merged 5 commits from feat/mcp-plugin-host into main 2026-09-01 22:10:57 +08:00
15 changed files with 138 additions and 24 deletions
Showing only changes of commit 1132a4cece - Show all commits
+1 -1
View File
@@ -118,7 +118,7 @@ cd frontend
pnpm test pnpm test
``` ```
当前回归基线为后端 87 项测试、前端 27 项测试,且生产构建通过。测试数量会随功能增长,以本地实际输出和 CI 为准。 当前回归基线为后端 91 项测试、前端 27 项测试,且生产构建通过。测试数量会随功能增长,以本地实际输出和 CI 为准。
构建产物位于 `frontend/dist`,该目录不提交到 Git。 构建产物位于 `frontend/dist`,该目录不提交到 Git。
+1 -1
View File
@@ -23,7 +23,7 @@ uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
uv run pytest uv run pytest
``` ```
当前基线为 87 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY``DEEPSEEK_API_KEY``AINOTE_CREDENTIAL_<ID>` 注入;不要把真实密钥写入仓库。 当前基线为 91 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY``DEEPSEEK_API_KEY``AINOTE_CREDENTIAL_<ID>` 注入;不要把真实密钥写入仓库。
团队接口清单见 `../docs/contracts/后端接口契约-开发版.md`,机器可读契约以运行时的 `/openapi.json` 为准。 团队接口清单见 `../docs/contracts/后端接口契约-开发版.md`,机器可读契约以运行时的 `/openapi.json` 为准。
+33 -6
View File
@@ -188,7 +188,7 @@ class McpStdioClient:
result = response.get("result") result = response.get("result")
if not isinstance(result, dict): if not isinstance(result, dict):
raise McpBridgeError( raise McpBridgeError(
"MCP_TOOL_CALL_FAILED", "MCP response result must be an object." response_error_code, "MCP response result must be an object."
) )
return result return result
@@ -207,9 +207,18 @@ class McpStdioClient:
except McpBridgeError: except McpBridgeError:
pass pass
def abandon(self, request_id: int) -> None: def abandon(
self, request_id: int, wake_error: BaseException | None = None
) -> None:
with self._pending_lock: with self._pending_lock:
self._pending.pop(request_id, None) pending = self._pending.pop(request_id, None)
# asyncio.to_thread 被取消时不会停止底层线程;主动唤醒 Queue,避免线程
# 一直占用默认线程池直至远端超时。
if pending is not None and wake_error is not None:
try:
pending.response.put_nowait(wake_error)
except queue.Full:
pass
def stop(self) -> None: def stop(self) -> None:
process = self.process process = self.process
@@ -258,7 +267,15 @@ class McpStdioClient:
assert process is not None and process.stdout is not None assert process is not None and process.stdout is not None
failure: str | None = None failure: str | None = None
try: try:
for raw_line in process.stdout: while True:
# readline(size) 在换行缺失时仍有硬上限,不能先把任意大的
# 第三方 stdout 行完整读入宿主内存再检查。
raw_line = process.stdout.readline(MAX_MCP_MESSAGE_BYTES + 1)
if raw_line == "":
break
if not raw_line.endswith("\n"):
failure = "MCP server emitted an oversized or unterminated message."
break
if len(raw_line.encode("utf-8")) > MAX_MCP_MESSAGE_BYTES: if len(raw_line.encode("utf-8")) > MAX_MCP_MESSAGE_BYTES:
failure = "MCP server emitted an oversized protocol message." failure = "MCP server emitted an oversized protocol message."
break break
@@ -313,7 +330,12 @@ class McpStdioClient:
process = self.process process = self.process
assert process is not None and process.stderr is not None assert process is not None and process.stderr is not None
try: try:
for line in process.stderr: while True:
# stderr 不是协议通道,但同样按块读取,避免无换行日志造成
# 宿主侧的无界字符串分配。
line = process.stderr.readline(1025)
if line == "":
break
self._stderr_tail.append(line.rstrip()[:1024]) self._stderr_tail.append(line.rstrip()[:1024])
except (OSError, ValueError): except (OSError, ValueError):
return return
@@ -489,7 +511,12 @@ class McpBridge:
) )
except asyncio.CancelledError: except asyncio.CancelledError:
host.client.cancel(rpc_id) host.client.cancel(rpc_id)
host.client.abandon(rpc_id) host.client.abandon(
rpc_id,
McpBridgeError(
"MCP_TOOL_CALL_FAILED", "MCP request was cancelled."
),
)
raise raise
except McpBridgeError as exc: except McpBridgeError as exc:
raise ToolExecutionError(exc.code, exc.message) from exc raise ToolExecutionError(exc.code, exc.message) from exc
+14 -1
View File
@@ -479,6 +479,17 @@ class PluginRuntime:
status_code=409, status_code=409,
details={"plugin_id": plugin_id}, details={"plugin_id": plugin_id},
) )
if record.plugin.status in {
PluginStatus.installed,
PluginStatus.disabled,
PluginStatus.permission_required,
}:
raise ExtensionError(
"PLUGIN_HOST_UNAVAILABLE",
"Disabled or inactive MCP Plugins must be started with Enable.",
status_code=409,
details={"plugin_id": plugin_id, "status": record.plugin.status.value},
)
for name in record.registered_tools: for name in record.registered_tools:
self.registry.unregister(name) self.registry.unregister(name)
record.registered_tools.clear() record.registered_tools.clear()
@@ -664,7 +675,9 @@ def _arguments_model_from_schema(
annotation = types.get(field_schema.get("type"), Any) annotation = types.get(field_schema.get("type"), Any)
fields[name] = (annotation, ... if name in required else None) fields[name] = (annotation, ... if name in required else None)
model_name = "PluginArgs_" + re.sub(r"\W+", "_", tool_name) model_name = "PluginArgs_" + re.sub(r"\W+", "_", tool_name)
return create_model(model_name, __config__=ConfigDict(extra="forbid"), **fields) # 完整 JSON Schema 已在 ToolRegistry 中先行校验。这里允许额外字段,避免
# Pydantic 再次拒绝 additionalProperties/patternProperties 接受的合法参数。
return create_model(model_name, __config__=ConfigDict(extra="allow"), **fields)
def _validate_tool_schema(spec: DeclarativeToolSpec) -> None: def _validate_tool_schema(spec: DeclarativeToolSpec) -> None:
@@ -135,6 +135,15 @@ def main() -> None:
request_id = message.get("id") request_id = message.get("id")
params = message.get("params") or {} params = message.get("params") or {}
if method == "initialize" and isinstance(request_id, int): if method == "initialize" and isinstance(request_id, int):
if MODE == "invalid-result":
send({"jsonrpc": "2.0", "id": request_id, "result": None})
continue
if MODE == "oversized-stdout":
# 不带换行,验证 Host 在读取完整内容前执行硬上限。
sys.stdout.write("x" * (2 * 1024 * 1024 + 1))
sys.stdout.flush()
time.sleep(10)
return
respond( respond(
request_id, request_id,
{ {
+61 -1
View File
@@ -1,5 +1,6 @@
import asyncio import asyncio
import shutil import shutil
import threading
import time import time
import pytest import pytest
@@ -14,6 +15,8 @@ from app.contracts import (
ToolCall, ToolCall,
) )
from app.extensions import ExtensionError from app.extensions import ExtensionError
from app.extensions.mcp import McpStdioClient
from app.extensions.runtime import _arguments_model_from_schema
from app.services import note_service from app.services import note_service
from app.config import BACKEND_DIR, get_settings from app.config import BACKEND_DIR, get_settings
@@ -362,6 +365,11 @@ def test_mcp_stdio_host_discovers_namespaced_tools_and_maps_results(
assert disabled.status == "disabled" assert disabled.status == "disabled"
assert mcp_container.plugins.get_host_status("mcp-fixture").status == "stopped" assert mcp_container.plugins.get_host_status("mcp-fixture").status == "stopped"
assert not mcp_container.tools.contains("mcp-fixture.echo") assert not mcp_container.tools.contains("mcp-fixture.echo")
with pytest.raises(ExtensionError) as exc:
mcp_container.plugins.restart_host("mcp-fixture")
assert exc.value.code == "PLUGIN_HOST_UNAVAILABLE"
assert mcp_container.plugins.get("mcp-fixture").status == "disabled"
assert not mcp_container.tools.contains("mcp-fixture.echo")
run(scenario()) run(scenario())
@@ -438,6 +446,56 @@ def test_mcp_business_error_size_limit_and_timeout_are_structured(mcp_container)
run(scenario()) run(scenario())
def test_mcp_cancel_releases_blocking_response_thread(
mcp_container, monkeypatch
) -> None:
async def scenario() -> None:
mcp_container.plugins.enable("mcp-fixture")
released = threading.Event()
original_wait = McpStdioClient.wait_response
def tracked_wait(self, *args, **kwargs):
try:
return original_wait(self, *args, **kwargs)
finally:
released.set()
monkeypatch.setattr(McpStdioClient, "wait_response", tracked_wait)
task = asyncio.create_task(
mcp_container.plugins.mcp.call_tool(
"mcp-fixture",
"sleep",
{"seconds": 5},
request_id="call_cancel_release",
)
)
await asyncio.sleep(0.05)
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
deadline = time.monotonic() + 0.5
while not released.is_set() and time.monotonic() < deadline:
await asyncio.sleep(0.01)
assert released.is_set(), "cancelled MCP wait must not occupy a worker until timeout"
run(scenario())
def test_mcp_argument_model_preserves_json_schema_additional_properties() -> None:
arguments_model = _arguments_model_from_schema(
"mcp-fixture.dynamic",
{
"type": "object",
"additionalProperties": {"type": "string"},
},
)
arguments = arguments_model.model_validate({"dynamic_key": "value"})
assert arguments.model_dump() == {"dynamic_key": "value"}
def test_mcp_abnormal_exit_unregisters_tools_and_restart_recovers(mcp_container) -> None: def test_mcp_abnormal_exit_unregisters_tools_and_restart_recovers(mcp_container) -> None:
async def scenario() -> None: async def scenario() -> None:
mcp_container.plugins.enable("mcp-fixture") mcp_container.plugins.enable("mcp-fixture")
@@ -471,9 +529,11 @@ def test_mcp_abnormal_exit_unregisters_tools_and_restart_recovers(mcp_container)
[ [
("no-tools", "[]", "MCP_CAPABILITY_UNSUPPORTED"), ("no-tools", "[]", "MCP_CAPABILITY_UNSUPPORTED"),
("invalid-schema", "[mcp-invalid.broken]", "MCP_TOOL_SCHEMA_INVALID"), ("invalid-schema", "[mcp-invalid.broken]", "MCP_TOOL_SCHEMA_INVALID"),
("invalid-result", "[]", "MCP_INITIALIZE_FAILED"),
("oversized-stdout", "[]", "PLUGIN_HOST_UNAVAILABLE"),
], ],
) )
def test_mcp_rejects_missing_capability_and_invalid_discovery( def test_mcp_rejects_invalid_initialization_and_discovery(
tmp_path, mode, contributions, expected_code tmp_path, mode, contributions, expected_code
) -> None: ) -> None:
package = tmp_path / f"mcp-{mode}" package = tmp_path / f"mcp-{mode}"
@@ -2323,7 +2323,7 @@ Markdown Workspace
第一阶段 Plugin Runtime 已完成安装、启用、停用、权限和声明式 Tool 注册,建立 Skill 调用 Plugin Tool 的基础链路。Command、Settings 和 MCP 执行不计入第一阶段完成项。 第一阶段 Plugin Runtime 已完成安装、启用、停用、权限和声明式 Tool 注册,建立 Skill 调用 Plugin Tool 的基础链路。Command、Settings 和 MCP 执行不计入第一阶段完成项。
截至 2026-09-01,上述第一阶段后端链路和 Web 联调前端均已完成;第二阶段前置的 Workspace 去 Mock 联调、Agent Trace 持久化/恢复接口以及 stdio MCP Bridge / Plugin Host 也已完成。当前验证基线为后端 87 项测试、前端 27 项测试及生产构建通过。向量链路当前使用 `HashEmbeddingProvider` 验证工程正确性,真实 Embedding 召回质量不属于该测试结论。 截至 2026-09-01,上述第一阶段后端链路和 Web 联调前端均已完成;第二阶段前置的 Workspace 去 Mock 联调、Agent Trace 持久化/恢复接口以及 stdio MCP Bridge / Plugin Host 也已完成。当前验证基线为后端 91 项测试、前端 27 项测试及生产构建通过。向量链路当前使用 `HashEmbeddingProvider` 验证工程正确性,真实 Embedding 召回质量不属于该测试结论。
第二阶段在既有 Contract 上接入: 第二阶段在既有 Contract 上接入:
@@ -176,7 +176,7 @@ RunCancelled
## 当前实现状态 ## 当前实现状态
更新至 2026-09-01:后端 87 项回归测试通过。 更新至 2026-09-01:后端 91 项回归测试通过。
- Chat、Agent Run、Agent Events、Tool 列表、Provider 配置生命周期、模型列表和连接测试已经接入 AI Core。 - Chat、Agent Run、Agent Events、Tool 列表、Provider 配置生命周期、模型列表和连接测试已经接入 AI Core。
- Agent Run/Event 已持久化到 SQLiteSSE 帧携带 sequence `id`,断线后可以回放缺失事件。Trace API 与 Benchmark 共用同一事件事实,并在入库前执行 Secret 脱敏和结果限长。 - Agent Run/Event 已持久化到 SQLiteSSE 帧携带 sequence `id`,断线后可以回放缺失事件。Trace API 与 Benchmark 共用同一事件事实,并在入库前执行 Secret 脱敏和结果限长。
@@ -427,7 +427,7 @@ class McpBridge(Protocol):
async def stop(self, plugin_id: str) -> None: ... async def stop(self, plugin_id: str) -> None: ...
``` ```
首个实现已支持本地 `stdio`,按 MCP `2025-11-25` 发起 initialize,并兼容 `2025-06-18``2025-03-26``2024-11-05` 协商结果。Host 负责 capability negotiation、分页 `tools/list`、进程生命周期、超时取消、stderr 隔离和异常退出后的 Tool 注销。stdio 消息使用 UTF-8 单行 JSON-RPC;当前不实现 Streamable HTTP。 首个实现已支持本地 `stdio`,按 MCP `2025-11-25` 发起 initialize,并兼容 `2025-06-18``2025-03-26``2024-11-05` 协商结果。Host 负责 capability negotiation、分页 `tools/list`、进程生命周期、超时取消、stderr 隔离和异常退出后的 Tool 注销。stdio 消息使用 UTF-8 单行 JSON-RPC,并在完整行进入内存前执行有界读取;当前不实现 Streamable HTTP。
MCP Plugin 的 `backend` 增加: MCP Plugin 的 `backend` 增加:
@@ -482,7 +482,7 @@ error
`POST /api/plugins/{plugin_id}/host/restart` 返回 `202 OperationResponse`。重启期间先注销旧 Tool,发现和校验全部成功后再一次性发布新 Tool 集合,避免半注册状态。 `POST /api/plugins/{plugin_id}/host/restart` 返回 `202 OperationResponse`。重启期间先注销旧 Tool,发现和校验全部成功后再一次性发布新 Tool 集合,避免半注册状态。
当前实现还返回协商后的 `protocol_version``server_name``server_version`。单条协议消息上限为 2 MiB,单次 Tool Result 上限为 256 KiB;超限分别按 Host/Result 错误处理。Server 异常退出或发送无效 stdout 消息时,Host 进入 `unhealthy`Plugin 进入 `error`,相关 Tool 立即注销。 当前实现还返回协商后的 `protocol_version``server_name``server_version`。单条协议消息上限为 2 MiB,单次 Tool Result 上限为 256 KiB;超限分别按 Host/Result 错误处理。Server 异常退出或发送无效 stdout 消息时,Host 进入 `unhealthy`Plugin 进入 `error`,相关 Tool 立即注销。取消会同时通知 Server、移除 pending request 并唤醒本地等待线程。Restart 不得把 `installed``disabled``permission_required` Plugin 隐式启用,这些状态必须走 Enable。
### 7.3 Command Contribution 列表 ### 7.3 Command Contribution 列表
@@ -2,7 +2,7 @@
> 本文档用于团队开发和模块联调,记录当前已经落地的核心边界与使用方式。 > 本文档用于团队开发和模块联调,记录当前已经落地的核心边界与使用方式。
> 更新日期:2026-09-01。第一阶段 AI Core、Agent Core、Extension Core 和 Model Core 主链路已经完成;第二阶段 Agent Trace 持久化、可恢复 SSE、stdio MCP Bridge 与隔离 Plugin Host 已落地,后端当前回归基线为 87 项测试通过。 > 更新日期:2026-09-01。第一阶段 AI Core、Agent Core、Extension Core 和 Model Core 主链路已经完成;第二阶段 Agent Trace 持久化、可恢复 SSE、stdio MCP Bridge 与隔离 Plugin Host 已落地,后端当前回归基线为 91 项测试通过。
## 当前实现 ## 当前实现
@@ -3,7 +3,7 @@
> 本文档用于团队开发和模块联调,记录 Knowledge Core / Retrieval Core 已经落地的 > 本文档用于团队开发和模块联调,记录 Knowledge Core / Retrieval Core 已经落地的
> 模块边界、数据模型、接口与使用方式,对应分工表中的杨星萱。 > 模块边界、数据模型、接口与使用方式,对应分工表中的杨星萱。
> 更新日期:2026-09-01。第一阶段 Knowledge/Retrieval 主链路已经完成,并已接入 Agent Tool Registry;完整后端回归基线为 87 项测试通过。 > 更新日期:2026-09-01。第一阶段 Knowledge/Retrieval 主链路已经完成,并已接入 Agent Tool Registry;完整后端回归基线为 91 项测试通过。
## 当前实现 ## 当前实现
@@ -146,6 +146,7 @@ MCP Tool
- 远端名称必须能转换为合法且稳定的项目 Tool ID; - 远端名称必须能转换为合法且稳定的项目 Tool ID;
- `inputSchema` 必须是有效的 object JSON Schema - `inputSchema` 必须是有效的 object JSON Schema
- `additionalProperties``patternProperties` 等动态字段先由完整 JSON Schema 校验,Pydantic 参数载体不会再次误拒绝合法字段;
- `_meta.notesagent/permission` 必须属于项目已知权限; - `_meta.notesagent/permission` 必须属于项目已知权限;
- Tool 权限必须同时出现在 Plugin Manifest 中; - Tool 权限必须同时出现在 Plugin Manifest 中;
- Agent 仍通过 Tool Registry 执行参数校验、Permission、超时和 Trace - Agent 仍通过 Tool Registry 执行参数校验、Permission、超时和 Trace
@@ -165,9 +166,10 @@ MCP Tool
- 不把 Provider API Key、`APP_DB_PATH`、Vault 路径和其他宿主环境变量传入子进程; - 不把 Provider API Key、`APP_DB_PATH`、Vault 路径和其他宿主环境变量传入子进程;
- stderr 与 JSON-RPC stdout 分离,stderr 不进入 API 和 Agent Trace - stderr 与 JSON-RPC stdout 分离,stderr 不进入 API 和 Agent Trace
- stdout 只能发送合法 MCP JSON-RPC - stdout 只能发送合法 MCP JSON-RPC
- 单条协议消息上限 2 MiB - stdout 在读取完整行前即应用有界读取,单条协议消息上限 2 MiB;stderr 也按固定大小分块读取
- 单次 Tool Result 上限 256 KiB - 单次 Tool Result 上限 256 KiB
- MCP Tool 不绕过 Permission Manager 和 Agent Tool Timeout。 - MCP Tool 不绕过 Permission Manager 和 Agent Tool Timeout。
- 调用被 Agent 取消时,同时通知 Server 并唤醒本地 pending Queue,阻塞线程不会继续占用线程池直至远端超时。
当前尚未提供容器、受限系统账户、seccomp、Windows AppContainer 或 macOS Sandbox,因此 Plugin 进程仍具有当前操作系统用户授予的一般文件访问能力。正式社区插件分发前必须继续增加包签名、来源验证和平台级沙箱;不得把当前进程隔离描述为完全安全执行任意不可信代码。 当前尚未提供容器、受限系统账户、seccomp、Windows AppContainer 或 macOS Sandbox,因此 Plugin 进程仍具有当前操作系统用户授予的一般文件访问能力。正式社区插件分发前必须继续增加包签名、来源验证和平台级沙箱;不得把当前进程隔离描述为完全安全执行任意不可信代码。
@@ -201,7 +203,7 @@ unhealthy
error error
``` ```
Restart 返回 `202 OperationResponse`。接口返回前已完成本地 Host 重启和 Tool 重新发现;`message` 中给出最终 Host 状态。 Restart 返回 `202 OperationResponse`。接口返回前已完成本地 Host 重启和 Tool 重新发现;`message` 中给出最终 Host 状态。Restart 只用于运行中或异常 Host;用户主动停用、尚未启用或等待授权的 Plugin 返回 `409 PLUGIN_HOST_UNAVAILABLE`,必须通过 Enable 明确启动。
## 8. 离线 Fixture ## 8. 离线 Fixture
@@ -220,7 +222,7 @@ backend/extensions/fixtures/mcp-echo
- `mcp-fixture.environment`:验证宿主 Secret/路径没有进入子进程; - `mcp-fixture.environment`:验证宿主 Secret/路径没有进入子进程;
- `mcp-fixture.exit`:验证异常退出、Tool 注销和 Restart。 - `mcp-fixture.exit`:验证异常退出、Tool 注销和 Restart。
Fixture 的 `tools/list` 使用两页响应,用于覆盖分页发现。测试还会启动缺少 tools capability返回无效 Schema 的变体。 Fixture 的 `tools/list` 使用两页响应,用于覆盖分页发现。测试还会启动缺少 tools capability返回无效 Schema/initialize result,以及输出超长无换行 stdout 的变体。
## 9. 验证 ## 9. 验证
@@ -241,10 +243,13 @@ pnpm build
- 分页 `tools/list` 与命名空间映射; - 分页 `tools/list` 与命名空间映射;
- Permission、JSON Schema 与 Contribution 集合; - Permission、JSON Schema 与 Contribution 集合;
- Tool 成功、业务错误、结果过大和超时; - Tool 成功、业务错误、结果过大和超时;
- Agent 取消后 pending 等待线程及时释放;
- `additionalProperties` 动态参数保持 JSON Schema 语义;
- Agent Runtime 调用 MCP Tool 并写入正式 Trace - Agent Runtime 调用 MCP Tool 并写入正式 Trace
- Secret/Vault 环境隔离; - Secret/Vault 环境隔离;
- Server 异常退出、Tool 注销和 Host Restart - Server 异常退出、Tool 注销和 Host Restart
- 缺少 capability 与无效 MCP Schema - 缺少 capability、无效 initialize result、无效 MCP Schema 和超长无换行 stdout
- disabled Plugin 不会被 Host Restart 隐式重新启用;
- OpenAPI 发布 Host 状态和重启路径。 - OpenAPI 发布 Host 状态和重启路径。
## 10. 当前边界与后续阶段 ## 10. 当前边界与后续阶段
@@ -187,12 +187,12 @@ pnpm build
```text ```text
pnpm build passed pnpm build passed
pnpm test 27 passed pnpm test 27 passed
uv run pytest 87 passed uv run pytest 91 passed
preview smoke HTTP 200 preview smoke HTTP 200
git diff --check passed git diff --check passed
``` ```
当前前端使用 Vitest 执行 Store、Workspace API Adapter、SSE 恢复游标、文件树、编辑器组件、智能体标签、轻量动效约束、Markdown 对比度 Token、scoped CSS 选择器约束和 Shiki GitHub 双主题测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 87 项测试结果,也不涉及产品代码。 当前前端使用 Vitest 执行 Store、Workspace API Adapter、SSE 恢复游标、文件树、编辑器组件、智能体标签、轻量动效约束、Markdown 对比度 Token、scoped CSS 选择器约束和 Shiki GitHub 双主题测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 91 项测试结果,也不涉及产品代码。
Vite 当前会提示 Chat 与 Workspace 的部分异步 Chunk 超过 500 kB,这是 Milkdown、CodeMirror、KaTeX 和 Shiki 等编辑/渲染依赖带来的性能优化项,不影响构建成功或功能正确性;进入桌面打包前应通过手动分包或更细粒度动态加载继续优化。 Vite 当前会提示 Chat 与 Workspace 的部分异步 Chunk 超过 500 kB,这是 Milkdown、CodeMirror、KaTeX 和 Shiki 等编辑/渲染依赖带来的性能优化项,不影响构建成功或功能正确性;进入桌面打包前应通过手动分包或更细粒度动态加载继续优化。
@@ -104,4 +104,4 @@ pnpm build
自动化验证覆盖 Provider 预设、OpenAI-Compatible `/models` 请求与鉴权头、模型映射、前端自动刷新、排序去重及按 Provider 隔离错误。生产构建同时执行 Vue 和 TypeScript 类型检查。 自动化验证覆盖 Provider 预设、OpenAI-Compatible `/models` 请求与鉴权头、模型映射、前端自动刷新、排序去重及按 Provider 隔离错误。生产构建同时执行 Vue 和 TypeScript 类型检查。
当前完整回归基线:后端 87 项测试、前端 27 项测试通过,前端类型检查和生产构建通过。Provider 配置目前仍保存在内存 Registry,AI Core 重启后需要重新创建;凭据密文会保留。OpenAI Responses 与 Anthropic Messages Adapter 尚未实现,设置页正式预设不会使用这两种协议。 当前完整回归基线:后端 91 项测试、前端 27 项测试通过,前端类型检查和生产构建通过。Provider 配置目前仍保存在内存 Registry,AI Core 重启后需要重新创建;凭据密文会保留。OpenAI Responses 与 Anthropic Messages Adapter 尚未实现,设置页正式预设不会使用这两种协议。
@@ -4,7 +4,7 @@
> 审阅范围:FastAPI、Knowledge / Retrieval Core、Agent Core、Extension Core、Provider Adapter、公共接口和后端开发文档。 > 审阅范围:FastAPI、Knowledge / Retrieval Core、Agent Core、Extension Core、Provider Adapter、公共接口和后端开发文档。
> 文档用途:记录问题形成原因、实际影响、修复判断和落地方案,供后续开发文档、比赛材料与技术博客使用。 > 文档用途:记录问题形成原因、实际影响、修复判断和落地方案,供后续开发文档、比赛材料与技术博客使用。
> 2026-09-01 状态补充:本文记录的缺陷均保持修复。此后又加入 Provider 预设、模型发现、DeepSeek/OpenAI 凭据解析、Fernet 加密存储、Agent Trace 持久化和 stdio MCP Plugin Host,当前完整后端回归基线为 87 项测试通过。 > 2026-09-01 状态补充:本文记录的缺陷均保持修复。此后又加入 Provider 预设、模型发现、DeepSeek/OpenAI 凭据解析、Fernet 加密存储、Agent Trace 持久化和 stdio MCP Plugin Host,当前完整后端回归基线为 91 项测试通过。
## 1. 审阅结论 ## 1. 审阅结论