fix(extension): 按Schema资源作用域校验引用

This commit is contained in:
2026-09-02 15:47:54 +08:00
parent 022c3226c7
commit c06b962743
20 changed files with 128 additions and 58 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ cd frontend
pnpm test pnpm test
``` ```
当前回归基线为后端 121 项测试、前端 29 项测试,且 TypeScript 类型检查和生产构建通过。测试数量会随功能增长,以本地实际输出和 CI 为准。 当前回归基线为后端 126 项测试、前端 29 项测试,且 TypeScript 类型检查和生产构建通过。测试数量会随功能增长,以本地实际输出和 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
``` ```
当前基线为 121 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY``DEEPSEEK_API_KEY``AINOTE_CREDENTIAL_<ID>` 注入;不要把真实密钥写入仓库。`plugin.*` 是 Plugin Settings 的保留凭据命名空间,通用 Provider 凭据接口不能读写。 当前基线为 126 项测试通过。Provider API Key 可通过前端设置页写入,也可用 `OPENAI_API_KEY``DEEPSEEK_API_KEY``AINOTE_CREDENTIAL_<ID>` 注入;不要把真实密钥写入仓库。`plugin.*` 是 Plugin Settings 的保留凭据命名空间,通用 Provider 凭据接口不能读写。
团队接口清单见 `../docs/contracts/后端接口契约-开发版.md`,机器可读契约以运行时的 `/openapi.json` 为准。 团队接口清单见 `../docs/contracts/后端接口契约-开发版.md`,机器可读契约以运行时的 `/openapi.json` 为准。
+1
View File
@@ -55,6 +55,7 @@ class ToolRegistry:
arguments_model: type[BaseModel], arguments_model: type[BaseModel],
executor: ToolExecutor, executor: ToolExecutor,
) -> None: ) -> None:
Draft202012Validator.check_schema(definition.parameters)
reject_external_schema_references(definition.parameters) reject_external_schema_references(definition.parameters)
with self._lock: with self._lock:
if definition.name in self._tools: if definition.name in self._tools:
+8 -1
View File
@@ -682,6 +682,13 @@ def validate_settings_definition(
field.minimum is not None or field.maximum is not None field.minimum is not None or field.maximum is not None
): ):
raise _settings_schema_error(plugin_id, f"Only number settings accept bounds: {field.key}") raise _settings_schema_error(plugin_id, f"Only number settings accept bounds: {field.key}")
if any(
bound is not None and not math.isfinite(bound)
for bound in (field.minimum, field.maximum)
):
raise _settings_schema_error(
plugin_id, f"Number setting bounds must be finite: {field.key}"
)
if field.minimum is not None and field.maximum is not None and field.minimum > field.maximum: if field.minimum is not None and field.maximum is not None and field.minimum > field.maximum:
raise _settings_schema_error(plugin_id, f"Setting bounds are reversed: {field.key}") raise _settings_schema_error(plugin_id, f"Setting bounds are reversed: {field.key}")
if field.type == PluginSettingType.secret and field.default is not None: if field.type == PluginSettingType.secret and field.default is not None:
@@ -746,8 +753,8 @@ def validate_command_spec(plugin_id: str, spec: PluginCommandSpec) -> None:
if spec.parameters.get("type", "object") != "object": if spec.parameters.get("type", "object") != "object":
raise ExtensionError("PLUGIN_COMMAND_INVALID", "Command parameters must be an object schema.") raise ExtensionError("PLUGIN_COMMAND_INVALID", "Command parameters must be an object schema.")
try: try:
reject_external_schema_references(spec.parameters)
Draft202012Validator.check_schema(spec.parameters) Draft202012Validator.check_schema(spec.parameters)
reject_external_schema_references(spec.parameters)
except (SchemaReferenceError, SchemaError) as exc: except (SchemaReferenceError, SchemaError) as exc:
message = exc.message if isinstance(exc, SchemaError) else str(exc) message = exc.message if isinstance(exc, SchemaError) else str(exc)
raise ExtensionError( raise ExtensionError(
+1 -1
View File
@@ -680,8 +680,8 @@ class McpBridge:
f"MCP tool inputSchema must be an object schema: {remote_name}", f"MCP tool inputSchema must be an object schema: {remote_name}",
) )
try: try:
reject_external_schema_references(schema)
Draft202012Validator.check_schema(schema) Draft202012Validator.check_schema(schema)
reject_external_schema_references(schema)
except (SchemaReferenceError, SchemaError) as exc: except (SchemaReferenceError, SchemaError) as exc:
message = exc.message if isinstance(exc, SchemaError) else str(exc) message = exc.message if isinstance(exc, SchemaError) else str(exc)
raise McpBridgeError( raise McpBridgeError(
+1 -1
View File
@@ -1043,8 +1043,8 @@ def _arguments_model_from_schema(
def _validate_tool_schema(spec: DeclarativeToolSpec) -> None: def _validate_tool_schema(spec: DeclarativeToolSpec) -> None:
schema = spec.parameters or {"type": "object", "properties": {}} schema = spec.parameters or {"type": "object", "properties": {}}
try: try:
reject_external_schema_references(schema)
Draft202012Validator.check_schema(schema) Draft202012Validator.check_schema(schema)
reject_external_schema_references(schema)
except (SchemaReferenceError, SchemaError) as exc: except (SchemaReferenceError, SchemaError) as exc:
message = exc.message if isinstance(exc, SchemaError) else str(exc) message = exc.message if isinstance(exc, SchemaError) else str(exc)
raise ExtensionError( raise ExtensionError(
+31 -41
View File
@@ -3,7 +3,13 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from urllib.parse import unquote from urllib.parse import urljoin
from referencing import Registry
from referencing.exceptions import Unresolvable
from referencing.jsonschema import DRAFT202012
_SCHEMA_BASE_URI = "https://notesagent.invalid/local-schema"
class SchemaReferenceError(ValueError): class SchemaReferenceError(ValueError):
@@ -24,47 +30,31 @@ class UnresolvableLocalSchemaReferenceError(SchemaReferenceError):
def reject_external_schema_references(schema: Any) -> None: def reject_external_schema_references(schema: Any) -> None:
"""只允许可解析的文档内 Fragment,禁止文件和网络检索""" """只允许可解析的文档内 Fragment,并按 JSON Schema Resource 作用域解析"""
pending = [schema] root = DRAFT202012.create_resource(schema)
local_references: list[str] = [] root_uri = urljoin(_SCHEMA_BASE_URI, root.id() or "")
anchors: set[str] = set() registry = Registry().with_resource(_SCHEMA_BASE_URI, root).crawl()
while pending: resolver = registry.resolver(root_uri)
value = pending.pop() _validate_resource_references(root, resolver)
if isinstance(value, dict):
for key, child in value.items():
if key in {"$ref", "$dynamicRef"}:
if not isinstance(child, str) or not child.startswith("#"):
raise ExternalSchemaReferenceError(key, child)
local_references.append(child)
elif key in {"$anchor", "$dynamicAnchor"} and isinstance(child, str):
anchors.add(child)
pending.append(child)
elif isinstance(value, list):
pending.extend(value)
for reference in local_references:
if not _local_reference_exists(schema, reference, anchors):
raise UnresolvableLocalSchemaReferenceError(reference)
def _local_reference_exists(schema: Any, reference: str, anchors: set[str]) -> bool: def _validate_resource_references(resource, resolver: Any) -> None:
fragment = unquote(reference[1:]) contents = resource.contents
if not fragment: if isinstance(contents, dict):
return True for keyword in ("$ref", "$dynamicRef"):
if not fragment.startswith("/"): if keyword not in contents:
return fragment in anchors continue
reference = contents[keyword]
if not isinstance(reference, str) or not reference.startswith("#"):
raise ExternalSchemaReferenceError(keyword, reference)
try:
resolver.lookup(reference)
except Unresolvable as exc:
raise UnresolvableLocalSchemaReferenceError(reference) from exc
current = schema for subresource in resource.subresources():
for encoded_segment in fragment[1:].split("/"): _validate_resource_references(
segment = encoded_segment.replace("~1", "/").replace("~0", "~") subresource,
if isinstance(current, dict) and segment in current: resolver.in_subresource(subresource),
current = current[segment] )
elif isinstance(current, list) and segment.isdecimal():
index = int(segment)
if index >= len(current):
return False
current = current[index]
else:
return False
return True
+1
View File
@@ -10,6 +10,7 @@ dependencies = [
"httpx>=0.28,<1.0", "httpx>=0.28,<1.0",
"jsonschema>=4.25,<5.0", "jsonschema>=4.25,<5.0",
"pyyaml>=6.0,<7.0", "pyyaml>=6.0,<7.0",
"referencing>=0.36,<1.0",
"sqlite-vec>=0.1.9", "sqlite-vec>=0.1.9",
"uvicorn[standard]>=0.35,<1.0", "uvicorn[standard]>=0.35,<1.0",
] ]
@@ -430,6 +430,43 @@ fields:
assert settings_error.value.code == "PLUGIN_SETTINGS_SCHEMA_INVALID" assert settings_error.value.code == "PLUGIN_SETTINGS_SCHEMA_INVALID"
@pytest.mark.parametrize("bound", [".nan", ".inf", "-.inf"])
def test_non_finite_setting_bounds_are_rejected(tmp_path: Path, bound: str) -> None:
package = tmp_path / f"invalid-bound-{bound.replace('.', 'dot').replace('-', 'neg')}"
package.mkdir()
(package / "plugin.yaml").write_text(
"""
id: invalid-bound
name: Invalid Bound
version: 1.0.0
contributes:
settings_sections: [invalid-bound.general]
backend:
type: none
transport: none
""".strip(),
encoding="utf-8",
)
(package / "settings.yaml").write_text(
f"""
section_id: invalid-bound.general
schema_version: 1
fields:
- key: limit
label: Limit
type: number
minimum: {bound}
""".strip(),
encoding="utf-8",
)
with pytest.raises(ExtensionError) as exc:
PluginRuntime(ToolRegistry()).install(package)
assert exc.value.code == "PLUGIN_SETTINGS_SCHEMA_INVALID"
assert "must be finite" in exc.value.message
def test_null_command_list_returns_stable_manifest_error(tmp_path: Path) -> None: def test_null_command_list_returns_stable_manifest_error(tmp_path: Path) -> None:
package = tmp_path / "null-commands" package = tmp_path / "null-commands"
package.mkdir() package.mkdir()
+30
View File
@@ -33,3 +33,33 @@ def test_local_json_schema_fragment_reference_is_allowed() -> None:
def test_unresolvable_local_schema_reference_is_rejected(reference: str) -> None: def test_unresolvable_local_schema_reference_is_rejected(reference: str) -> None:
with pytest.raises(UnresolvableLocalSchemaReferenceError): with pytest.raises(UnresolvableLocalSchemaReferenceError):
reject_external_schema_references({"type": "object", "$ref": reference}) reject_external_schema_references({"type": "object", "$ref": reference})
def test_root_reference_cannot_use_anchor_from_nested_schema_resource() -> None:
schema = {
"$defs": {
"nested": {
"$id": "nested",
"$anchor": "inside",
"type": "string",
}
},
"properties": {"value": {"$ref": "#inside"}},
}
with pytest.raises(UnresolvableLocalSchemaReferenceError):
reject_external_schema_references(schema)
def test_nested_schema_resource_can_resolve_its_own_anchor() -> None:
schema = {
"$defs": {
"nested": {
"$id": "nested",
"$anchor": "inside",
"allOf": [{"$ref": "#inside"}],
}
}
}
reject_external_schema_references(schema)
+2
View File
@@ -374,6 +374,7 @@ dependencies = [
{ name = "httpx" }, { name = "httpx" },
{ name = "jsonschema" }, { name = "jsonschema" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "referencing" },
{ name = "sqlite-vec" }, { name = "sqlite-vec" },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
] ]
@@ -390,6 +391,7 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.28,<1.0" }, { name = "httpx", specifier = ">=0.28,<1.0" },
{ name = "jsonschema", specifier = ">=4.25,<5.0" }, { name = "jsonschema", specifier = ">=4.25,<5.0" },
{ name = "pyyaml", specifier = ">=6.0,<7.0" }, { name = "pyyaml", specifier = ">=6.0,<7.0" },
{ name = "referencing", specifier = ">=0.36,<1.0" },
{ name = "sqlite-vec", specifier = ">=0.1.9" }, { name = "sqlite-vec", specifier = ">=0.1.9" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35,<1.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.35,<1.0" },
] ]
@@ -2329,7 +2329,7 @@ Markdown Workspace
第一阶段 Plugin Runtime 已完成安装、启用、停用、权限和声明式 Tool 注册,建立 Skill 调用 Plugin Tool 的基础链路。Command、Settings 和 MCP 执行不计入第一阶段完成项。 第一阶段 Plugin Runtime 已完成安装、启用、停用、权限和声明式 Tool 注册,建立 Skill 调用 Plugin Tool 的基础链路。Command、Settings 和 MCP 执行不计入第一阶段完成项。
截至 2026-09-02,上述第一阶段后端链路和 Web 联调前端均已完成;第二阶段前置的 Workspace 去 Mock 联调、Agent Trace 持久化/恢复接口、stdio MCP Bridge / Plugin Host 以及 Plugin Command/Settings 后端 Contract 也已完成。当前验证基线为后端 121 项测试、前端 29 项测试、TypeScript 类型检查及生产构建通过。向量链路当前使用 `HashEmbeddingProvider` 验证工程正确性,真实 Embedding 召回质量不属于该测试结论。 截至 2026-09-02,上述第一阶段后端链路和 Web 联调前端均已完成;第二阶段前置的 Workspace 去 Mock 联调、Agent Trace 持久化/恢复接口、stdio MCP Bridge / Plugin Host 以及 Plugin Command/Settings 后端 Contract 也已完成。当前验证基线为后端 126 项测试、前端 29 项测试、TypeScript 类型检查及生产构建通过。向量链路当前使用 `HashEmbeddingProvider` 验证工程正确性,真实 Embedding 召回质量不属于该测试结论。
第二阶段在既有 Contract 上接入: 第二阶段在既有 Contract 上接入:
@@ -176,7 +176,7 @@ RunCancelled
## 当前实现状态 ## 当前实现状态
更新至 2026-09-02:后端 121 项回归测试通过;第二阶段 Plugin Command 与 Plugin Settings/Secret 接口已实现,详细 DTO 和边界见《第二阶段接口契约-开发版》第 7 节。 更新至 2026-09-02:后端 126 项回归测试通过;第二阶段 Plugin Command 与 Plugin Settings/Secret 接口已实现,详细 DTO 和边界见《第二阶段接口契约-开发版》第 7 节。
- 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 脱敏和结果限长。
@@ -546,7 +546,7 @@ error
需要 Secret 的 Command 必须在 `commands.yaml` 的内部 `secrets` 数组中声明对应 Setting Key,并在 Plugin Manifest 声明 `secrets.use` 权限。安装时宿主校验该字段确实属于当前 Plugin Settings Schema 的 `secret` 类型;只有权限已授予并启用后,运行时才向受控 handler 或 MCP Command Target 提供声明过的 Secret。未声明字段返回 `PLUGIN_SECRET_ACCESS_DENIED`,必填 Secret 未配置返回 `PLUGIN_SECRET_REQUIRED``secrets` 不属于前端 `PluginCommand` DTOSecret 明文也不会并入普通 Settings 字典。 需要 Secret 的 Command 必须在 `commands.yaml` 的内部 `secrets` 数组中声明对应 Setting Key,并在 Plugin Manifest 声明 `secrets.use` 权限。安装时宿主校验该字段确实属于当前 Plugin Settings Schema 的 `secret` 类型;只有权限已授予并启用后,运行时才向受控 handler 或 MCP Command Target 提供声明过的 Secret。未声明字段返回 `PLUGIN_SECRET_ACCESS_DENIED`,必填 Secret 未配置返回 `PLUGIN_SECRET_REQUIRED``secrets` 不属于前端 `PluginCommand` DTOSecret 明文也不会并入普通 Settings 字典。
`commands.yaml` 中的执行目标必须在宿主白名单 `handler` 与当前 Plugin 命名空间的 `mcp_tool` 之间二选一。MCP Command Target 不注册为 Agent Tool;宿主用 `_notesagent` 保留包装传入 Command ID、参数、裁剪后的 Context 和声明过的 Secret,并将 MCP structured result 再校验为白名单 effect。Command 与 Tool Schema 仅允许 `#...` 文档内引用,任何通过 `$ref``$dynamicRef` 指向文件、HTTP 或其他外部资源的 Schema 都会在注册前被拒绝。 `commands.yaml` 中的执行目标必须在宿主白名单 `handler` 与当前 Plugin 命名空间的 `mcp_tool` 之间二选一。MCP Command Target 不注册为 Agent Tool;宿主用 `_notesagent` 保留包装传入 Command ID、参数、裁剪后的 Context 和声明过的 Secret,并将 MCP structured result 再校验为白名单 effect。Command 与 Tool Schema 仅允许 `#...` 文档内引用,任何通过 `$ref``$dynamicRef` 指向文件、HTTP 或其他外部资源的 Schema 都会在注册前被拒绝。文档内引用遵循 Draft 2020-12 的嵌套 `$id` 与 Anchor 资源作用域,不能解析的引用不得进入运行时。
### 7.5 Settings Schema ### 7.5 Settings Schema
@@ -591,6 +591,8 @@ select
secret secret
``` ```
Number 字段的 `minimum``maximum` 必须是有限数值;`NaN`、正无穷和负无穷均视为无效 Settings Schema。
### 7.6 更新 Settings 和 Secret ### 7.6 更新 Settings 和 Secret
`PUT /api/plugins/{plugin_id}/settings` `PUT /api/plugins/{plugin_id}/settings`
@@ -2,7 +2,7 @@
> 本文档用于团队开发和模块联调,记录当前已经落地的核心边界与使用方式。 > 本文档用于团队开发和模块联调,记录当前已经落地的核心边界与使用方式。
> 更新日期:2026-09-02。第一阶段 AI Core、Agent Core、Extension Core 和 Model Core 主链路已经完成;第二阶段 Agent Trace 持久化、可恢复 SSE、stdio MCP Bridge、隔离 Plugin Host 以及 Plugin Command/Settings 已落地,后端当前回归基线为 121 项测试通过。 > 更新日期:2026-09-02。第一阶段 AI Core、Agent Core、Extension Core 和 Model Core 主链路已经完成;第二阶段 Agent Trace 持久化、可恢复 SSE、stdio MCP Bridge、隔离 Plugin Host 以及 Plugin Command/Settings 已落地,后端当前回归基线为 126 项测试通过。
## 当前实现 ## 当前实现
@@ -3,7 +3,7 @@
> 本文档用于团队开发和模块联调,记录 Knowledge Core / Retrieval Core 已经落地的 > 本文档用于团队开发和模块联调,记录 Knowledge Core / Retrieval Core 已经落地的
> 模块边界、数据模型、接口与使用方式,对应分工表中的杨星萱。 > 模块边界、数据模型、接口与使用方式,对应分工表中的杨星萱。
> 更新日期:2026-09-02。第一阶段 Knowledge/Retrieval 主链路已经完成,并已接入 Agent Tool Registry;完整后端回归基线为 121 项测试通过。 > 更新日期:2026-09-02。第一阶段 Knowledge/Retrieval 主链路已经完成,并已接入 Agent Tool Registry;完整后端回归基线为 126 项测试通过。
## 当前实现 ## 当前实现
@@ -1,6 +1,6 @@
# Plugin Command 与 Settings 开发说明 # Plugin Command 与 Settings 开发说明
> 更新日期:2026-09-02。本文记录第二阶段阶段 D 已实现的 Plugin Command Contribution、Plugin Settings Contribution、Secret 边界和前端 Service Contract。当前回归基线为后端 121 项测试、前端 29 项测试,TypeScript 类型检查和生产构建通过。 > 更新日期:2026-09-02。本文记录第二阶段阶段 D 已实现的 Plugin Command Contribution、Plugin Settings Contribution、Secret 边界和前端 Service Contract。当前回归基线为后端 126 项测试、前端 29 项测试,TypeScript 类型检查和生产构建通过。
## 1. 阶段目标 ## 1. 阶段目标
@@ -25,7 +25,7 @@ Plugin 在 `plugin.yaml` 的 `contributes.commands` 与 `contributes.settings_se
- 执行目标必须在宿主白名单 `handler` 与当前插件命名空间的 `mcp_tool` 之间二选一。 - 执行目标必须在宿主白名单 `handler` 与当前插件命名空间的 `mcp_tool` 之间二选一。
- 可选 `secrets` 字段:只声明当前 Command 允许按需读取的 Secret Setting Key,不暴露给前端 DTO。 - 可选 `secrets` 字段:只声明当前 Command 允许按需读取的 Secret Setting Key,不暴露给前端 DTO。
`settings.yaml` 采用递增 `schema_version`,首批字段类型固定为 `string``number``boolean``select``secret`。宿主会校验默认值、必填项、数值边界、Select 选项,以及 Secret 不得携带默认明文。 `settings.yaml` 采用递增 `schema_version`,首批字段类型固定为 `string``number``boolean``select``secret`。宿主会校验默认值、必填项、有限数值边界、Select 选项,以及 Secret 不得携带默认明文`NaN` 与正负无穷不能用作上下界
仓库内 `text-tools` 是联调 Fixture,覆盖 Command 和五种 Settings 字段类型。 仓库内 `text-tools` 是联调 Fixture,覆盖 Command 和五种 Settings 字段类型。
@@ -49,7 +49,7 @@ Command 执行器通过受控 Resolver 按需读取 `commands.yaml` 已声明且
MCP Command Target 是专用执行目标,不注册进 Agent `ToolRegistry`,因此模型无法绕过 Command 权限与 Context 裁剪直接调用。宿主通过 `_notesagent` 保留包装传入 `command_id`、已校验 arguments、已裁剪 Context 和声明过的 SecretMCP Server 必须返回结构化的白名单 effect。远程原始错误不直接透传给 HTTP 调用方。插件仍不能把模块路径或 Shell 字符串作为执行器。 MCP Command Target 是专用执行目标,不注册进 Agent `ToolRegistry`,因此模型无法绕过 Command 权限与 Context 裁剪直接调用。宿主通过 `_notesagent` 保留包装传入 `command_id`、已校验 arguments、已裁剪 Context 和声明过的 SecretMCP Server 必须返回结构化的白名单 effect。远程原始错误不直接透传给 HTTP 调用方。插件仍不能把模块路径或 Shell 字符串作为执行器。
Command 与 Tool 的 JSON Schema 只允许当前文档内的 Fragment 引用(`#...`);宿主在注册前递归拒绝 `$ref` / `$dynamicRef` 指向的文件、HTTP 或其他外部资源,避免 Schema 校验触发未授权 I/O。 Command 与 Tool 的 JSON Schema 只允许当前文档内的 Fragment 引用(`#...`);宿主在注册前递归拒绝 `$ref` / `$dynamicRef` 指向的文件、HTTP 或其他外部资源,避免 Schema 校验触发未授权 I/O。文档内引用使用 Draft 2020-12 Resource Resolver 预检,嵌套 `$id` 创建的新资源及其 Anchor 按各自作用域解析,无法解析的引用在注册阶段返回稳定错误。
## 4. Settings 与 Secret 边界 ## 4. Settings 与 Secret 边界
@@ -187,12 +187,12 @@ pnpm build
```text ```text
pnpm build passed pnpm build passed
pnpm test 29 passed pnpm test 29 passed
uv run pytest 121 passed uv run pytest 126 passed
preview smoke HTTP 200 preview smoke HTTP 200
git diff --check passed git diff --check passed
``` ```
当前前端使用 Vitest 执行 Store、Workspace API Adapter、SSE 恢复游标、Plugin Command/Settings Service、文件树、编辑器组件、智能体标签、轻量动效约束、Markdown 对比度 Token、scoped CSS 选择器约束和 Shiki GitHub 双主题测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 121 项测试结果,也不涉及产品代码。 当前前端使用 Vitest 执行 Store、Workspace API Adapter、SSE 恢复游标、Plugin Command/Settings Service、文件树、编辑器组件、智能体标签、轻量动效约束、Markdown 对比度 Token、scoped CSS 选择器约束和 Shiki GitHub 双主题测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 126 项测试结果,也不涉及产品代码。
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 类型检查。
当前完整回归基线:后端 121 项测试、前端 29 项测试通过,前端类型检查和生产构建通过。Provider 配置目前仍保存在内存 Registry,AI Core 重启后需要重新创建;凭据密文会保留。`plugin.*` 为 Plugin Secret 保留命名空间,Provider 配置、临时测试凭据和通用凭据 API 均拒绝该前缀。OpenAI Responses 与 Anthropic Messages Adapter 尚未实现,设置页正式预设不会使用这两种协议。 当前完整回归基线:后端 126 项测试、前端 29 项测试通过,前端类型检查和生产构建通过。Provider 配置目前仍保存在内存 Registry,AI Core 重启后需要重新创建;凭据密文会保留。`plugin.*` 为 Plugin Secret 保留命名空间,Provider 配置、临时测试凭据和通用凭据 API 均拒绝该前缀。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-02 状态补充:本文记录的缺陷均保持修复。此后又加入 Provider 预设、模型发现、DeepSeek/OpenAI 凭据解析、Fernet 加密存储、Agent Trace 持久化、stdio MCP Plugin Host 和 Plugin Command/Settings,当前完整后端回归基线为 121 项测试通过。 > 2026-09-02 状态补充:本文记录的缺陷均保持修复。此后又加入 Provider 预设、模型发现、DeepSeek/OpenAI 凭据解析、Fernet 加密存储、Agent Trace 持久化、stdio MCP Plugin Host 和 Plugin Command/Settings,当前完整后端回归基线为 126 项测试通过。
## 1. 审阅结论 ## 1. 审阅结论