feat: 添加飞书集成和改进安全配置

- 集成 lark-oapi 库以支持飞书功能
- 改进 CORS 配置验证器以支持 JSON 格式输入
- 添加安全凭证检查逻辑以防止跨域安全问题
- 在 DirectLLMAdapter 中增加响应解析异常处理

fix: 增强查询参数验证和分页限制

- 为多个路由添加 Query 参数验证器
- 实现 bounded_limit 和 bounded_offset 辅助函数
- 设置查询限制范围为 1-500 之间
- 使用 secrets.compare_digest 提升令牌验证安全性

refactor: 调整文档忽略规则和测试配置

- 更新 .gitignore 文件中的文档路径配置
- 在 smoke 测试中添加必要的环境变量配置
- 重构配置验证器以提高类型兼容性
```
This commit is contained in:
2026-07-06 10:27:17 +08:00
parent e3a4a6d426
commit 9b87c6a7a3
22 changed files with 155 additions and 35 deletions

View File

@@ -9,6 +9,7 @@ from app.modules.ai_agent.constants import (
OPENCLAW_HERMES_PIPELINE,
AIChatRole,
AIContextKey,
AIErrorKey,
AIHttpHeader,
AIHttpPath,
AIHttpPayloadKey,
@@ -237,3 +238,30 @@ def test_openclaw_hermes_adapter_fails_when_requested_tool_is_blocked(monkeypatc
"http://openclaw.local/healthz",
"http://openclaw.local/readyz",
]
def test_direct_llm_adapter_wraps_unexpected_chat_response(monkeypatch) -> None:
class BadChatClient(DummyClient):
def post(
self,
url: str,
json: dict[str, Any],
headers: dict[str, str],
) -> DummyResponse:
self.calls.append({"method": "POST", "url": url, "json": json, "headers": headers})
return DummyResponse({AIHttpPayloadKey.CHOICES: []})
DummyClient.calls = []
monkeypatch.setattr(adapters.httpx, "Client", BadChatClient)
settings = Settings(
direct_llm_base_url="http://llm.local/v1",
direct_llm_api_key="direct-key",
direct_llm_model="company-model",
)
with pytest.raises(HTTPException) as exc_info:
adapters.DirectLLMAdapter(settings).ask("summarize")
assert exc_info.value.status_code == 502
assert exc_info.value.detail[AIErrorKey.DIRECT_LLM] == "Unexpected chat completion response"
assert exc_info.value.detail[AIResponseKey.RAW] == {AIHttpPayloadKey.CHOICES: []}

View File

@@ -20,15 +20,19 @@ os.environ["APPROVAL_API_ACTOR"] = "approval-manager"
os.environ["FEISHU_APP_ID"] = ""
os.environ["FEISHU_APP_SECRET"] = ""
os.environ["FEISHU_VERIFICATION_TOKEN"] = "test-feishu-token"
os.environ["LEGACY_ALLOWED_QUERIES"] = "{}"
os.environ["LEGACY_DATABASE_URL"] = ""
os.environ["LEGACY_PROJECT_QUERY"] = ""
os.environ["MODEL_PROVIDER"] = AIProviderName.NOOP
os.environ["SCHEDULER_ENABLED"] = "false"
from fastapi.testclient import TestClient
from app.core.config import get_settings
from app.core.config import Settings, get_settings
from app.core.database import Base, engine
from app.core.pagination import bounded_limit, bounded_offset
from app.core.security import require_api_key, require_approval_api_key
from app.main import app
from app.main import _allow_cors_credentials, app
from app.modules.legacy_mysql.service import LegacyMySQLService
from app.modules.reports.constants import (
LifecycleAttentionKey,
@@ -133,6 +137,26 @@ def test_api_key_and_feishu_webhook_fail_closed(monkeypatch) -> None:
get_settings.cache_clear()
def test_config_and_pagination_guardrails() -> None:
settings = Settings(cors_origins='["https://app.example.com", "https://admin.example.com"]')
assert settings.cors_origins == ["https://app.example.com", "https://admin.example.com"]
assert _allow_cors_credentials(["*"]) is False
assert _allow_cors_credentials(["https://app.example.com"]) is True
assert bounded_limit(-1) == 1
assert bounded_limit(1000) == 500
assert bounded_offset(-10) == 0
negative_limit_response = client.get("/api/v1/business/projects?limit=-1", headers=headers)
assert negative_limit_response.status_code == 422
oversized_limit_response = client.get("/api/v1/risks/events?limit=501", headers=headers)
assert oversized_limit_response.status_code == 422
negative_offset_response = client.get("/api/v1/business/projects?offset=-1", headers=headers)
assert negative_offset_response.status_code == 422
def test_approval_gate_for_high_risk_update() -> None:
create_payload = {
"code": "FUND-SMOKE-001",