sync: 鍚屾灞€鍩熺綉鏈€鏂颁唬鐮?(source master @ b69568e4)
This commit is contained in:
1 parent
4f7f07504a
commit
8bc8b0e082
1651 files changed
+278599
-11416
No files matched your search
@@ -0,0 +1,26 @@
|
||||
# 商业情报监测接口模拟服务
|
||||
|
||||
该服务只使用 Python 标准库,用于在真实监测接口尚未部署时联调自定义技能。
|
||||
|
||||
启动:
|
||||
|
||||
```powershell
|
||||
./start-mock.ps1
|
||||
```
|
||||
|
||||
为当前 PowerShell 会话切换技能到模拟服务:
|
||||
|
||||
```powershell
|
||||
$env:MONITOR_API_BASE_URL = "http://127.0.0.1:18080/consumer"
|
||||
$env:MONITOR_AUTHORIZATION = "admin"
|
||||
```
|
||||
|
||||
模拟路由:
|
||||
|
||||
- `GET /consumer/monitor/task/{id}`
|
||||
- `GET /consumer/monitor/task/calendar`
|
||||
- `GET /consumer/monitor/task/eventMonitorByTime`
|
||||
- `POST /consumer/monitor/task/entites`
|
||||
- `GET /consumer/monitor/task/realNetwork/list/{taskId}`
|
||||
|
||||
所有请求必须携带 `Authorization: admin`。任务 `3` 默认返回 T 方向数据。完整任务名作为关键词时会先返回空数组;技能移除关键词后返回数据,可用来验证逐步放宽逻辑。
|
||||
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Zero-dependency mock for the commercial-intelligence monitor APIs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from datetime import datetime
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
AUTHORIZATION = "Bearer admin"
|
||||
PREFIX = "/consumer/monitor/task"
|
||||
|
||||
CALENDAR_ROWS = [
|
||||
{
|
||||
"createdTime": "2026-09-28 09:10:00",
|
||||
"description": "重点会议进入准备窗口,需持续跟踪相关议题和参与方动态。",
|
||||
"direction": "T",
|
||||
"endTime": "2026-10-01 18:00:00",
|
||||
"nodeName": "重点会议",
|
||||
"recUuid": "calendar-t-001",
|
||||
"startTime": "2026-09-29 10:00:00",
|
||||
"title": "台海方向重点会议准备节点",
|
||||
"updateTime": "2026-09-28 09:20:00",
|
||||
},
|
||||
{
|
||||
"createdTime": "2026-09-28 09:30:00",
|
||||
"description": "公开活动开始前的关键观察窗口。",
|
||||
"direction": "T",
|
||||
"endTime": "2026-10-05 20:00:00",
|
||||
"nodeName": "公开活动",
|
||||
"recUuid": "calendar-t-002",
|
||||
"startTime": "2026-10-03 08:30:00",
|
||||
"title": "台海方向公开活动观察节点",
|
||||
"updateTime": "2026-09-28 09:35:00",
|
||||
},
|
||||
]
|
||||
|
||||
EVENT_ROWS = {
|
||||
"热点": [
|
||||
{
|
||||
"id": 101,
|
||||
"m_title": "台海方向热点活动升温",
|
||||
"m_publish_time_date": "2026-09-29 08:15:00",
|
||||
"source": "热点",
|
||||
"m_content": "相关活动密度上升,公开信息关注度持续增加。",
|
||||
"import_num": 9,
|
||||
"direction": "台",
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"m_title": "重点区域公开讨论增多",
|
||||
"m_publish_time_date": "2026-09-29 09:20:00",
|
||||
"source": "热点",
|
||||
"m_content": "多个公开渠道围绕重点区域展开讨论。",
|
||||
"import_num": 6,
|
||||
"direction": "台",
|
||||
},
|
||||
],
|
||||
"威胁": [
|
||||
{
|
||||
"id": 201,
|
||||
"m_title": "外部干扰因素增加",
|
||||
"m_publish_time_date": "2026-09-29 07:40:00",
|
||||
"source": "威胁",
|
||||
"m_content": "外部干扰活动可能对既定安排形成压力。",
|
||||
"import_num": 8,
|
||||
"direction": "台",
|
||||
}
|
||||
],
|
||||
"战机": [
|
||||
{
|
||||
"id": 301,
|
||||
"m_title": "信息窗口出现",
|
||||
"m_publish_time_date": "2026-09-29 06:50:00",
|
||||
"source": "战机",
|
||||
"m_content": "公开信息集中释放,形成进一步研判和行动衔接窗口。",
|
||||
"import_num": 7,
|
||||
"direction": "台",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
ENTITY_ROWS = {
|
||||
101: {
|
||||
"eid": "Q22686",
|
||||
"name": "示例关键人物",
|
||||
"etype": "human",
|
||||
"text": "与热点活动相关的关键人物。",
|
||||
"imagePath": "/mock/images/q22686.png",
|
||||
"num": 12,
|
||||
"label": "重点关注",
|
||||
"reason": "在多个相关事件中被重复提及。",
|
||||
"score": 92,
|
||||
},
|
||||
201: {
|
||||
"eid": "ORG-1001",
|
||||
"name": "示例关联机构",
|
||||
"etype": "organization",
|
||||
"text": "与威胁事件有关联的机构。",
|
||||
"imagePath": "/mock/images/org-1001.png",
|
||||
"num": 8,
|
||||
"label": "风险关联",
|
||||
"reason": "与高重要度威胁事件存在关联。",
|
||||
"score": 86,
|
||||
},
|
||||
301: {
|
||||
"eid": "LOC-3001",
|
||||
"name": "示例重点区域",
|
||||
"etype": "location",
|
||||
"text": "战机事件涉及的重点区域。",
|
||||
"imagePath": "/mock/images/loc-3001.png",
|
||||
"num": 6,
|
||||
"label": "机会窗口",
|
||||
"reason": "在战机信息中具有较高关联度。",
|
||||
"score": 81,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _contains(row: dict, keyword: str, fields: tuple[str, ...]) -> bool:
|
||||
if not keyword:
|
||||
return True
|
||||
return any(keyword.lower() in str(row.get(field, "")).lower() for field in fields)
|
||||
|
||||
|
||||
def _in_range(row_time: str, start: str, end: str) -> bool:
|
||||
try:
|
||||
value = datetime.strptime(row_time, "%Y-%m-%d %H:%M:%S")
|
||||
if start and value < datetime.strptime(start, "%Y-%m-%d %H:%M:%S"):
|
||||
return False
|
||||
if end and value > datetime.strptime(end, "%Y-%m-%d %H:%M:%S"):
|
||||
return False
|
||||
except ValueError:
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "MonitorMock/1.0"
|
||||
|
||||
def log_message(self, fmt: str, *args) -> None:
|
||||
print(f"[mock-monitor] {self.address_string()} {fmt % args}")
|
||||
|
||||
def _json(self, status: int, payload: dict) -> None:
|
||||
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def _authorized(self) -> bool:
|
||||
if self.headers.get("Authorization", "") == AUTHORIZATION:
|
||||
return True
|
||||
self._json(403, {"data": None, "msg": "Authorization 必须为 Bearer admin", "state": 403})
|
||||
return False
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
if not self._authorized():
|
||||
return
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path.rstrip("/")
|
||||
query = {key: values[-1] for key, values in parse_qs(parsed.query, keep_blank_values=True).items()}
|
||||
if path == f"{PREFIX}/calendar":
|
||||
self._calendar(query)
|
||||
return
|
||||
if path == f"{PREFIX}/eventMonitorByTime":
|
||||
self._events(query)
|
||||
return
|
||||
if path.startswith(f"{PREFIX}/realNetwork/list/"):
|
||||
self._real_network(unquote(path.rsplit("/", 1)[-1]))
|
||||
return
|
||||
if path.startswith(PREFIX + "/"):
|
||||
self._task_detail(unquote(path.rsplit("/", 1)[-1]))
|
||||
return
|
||||
self._json(404, {"data": None, "msg": "接口不存在", "state": 404})
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
if not self._authorized():
|
||||
return
|
||||
path = urlparse(self.path).path.rstrip("/")
|
||||
if path != f"{PREFIX}/entites":
|
||||
self._json(404, {"data": None, "msg": "接口不存在", "state": 404})
|
||||
return
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
body = json.loads(self.rfile.read(length) or b"[]")
|
||||
if not isinstance(body, list):
|
||||
raise ValueError("请求体必须是事件 id 数组")
|
||||
ids = [int(value) for value in body]
|
||||
except (ValueError, json.JSONDecodeError) as error:
|
||||
self._json(400, {"data": [], "msg": str(error), "state": 400})
|
||||
return
|
||||
rows = [ENTITY_ROWS[event_id] for event_id in ids if event_id in ENTITY_ROWS]
|
||||
self._json(200, {"data": rows, "msg": "操作成功", "state": 200})
|
||||
|
||||
def _task_detail(self, task_id: str) -> None:
|
||||
numeric_id = int(task_id) if task_id.isdigit() else task_id
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"data": {
|
||||
"id": numeric_id,
|
||||
"taskName": "台海商业情报分析",
|
||||
"taskDesc": "收集台海方向重要节点、热点、威胁、战机、目标及衔接任务。",
|
||||
"direction": "T",
|
||||
"taskType": "商业情报",
|
||||
"createTime": "2026-09-28 10:00:00",
|
||||
"updateTime": "2026-09-29 09:00:00",
|
||||
},
|
||||
"msg": "操作成功",
|
||||
"state": 200,
|
||||
},
|
||||
)
|
||||
|
||||
def _calendar(self, query: dict[str, str]) -> None:
|
||||
direction = query.get("direction", "")
|
||||
keyword = query.get("searchText", "")
|
||||
start = query.get("startDate", "")
|
||||
end = query.get("endDate", "")
|
||||
rows = [
|
||||
row
|
||||
for row in CALENDAR_ROWS
|
||||
if (not direction or row["direction"] == direction)
|
||||
and _contains(row, keyword, ("title", "description", "nodeName"))
|
||||
and _in_range(row["startTime"], start, end)
|
||||
]
|
||||
page_num = max(1, int(query.get("pageNum", "1") or 1))
|
||||
page_size = max(1, int(query.get("pageSize", "10") or 10))
|
||||
offset = (page_num - 1) * page_size
|
||||
self._json(200, {"data": rows[offset : offset + page_size], "msg": "操作成功", "state": 200, "sumCount": len(rows)})
|
||||
|
||||
def _events(self, query: dict[str, str]) -> None:
|
||||
discriminant = query.get("discriminant", "热点")
|
||||
keyword = query.get("keyword", "")
|
||||
start = query.get("startTime", "")
|
||||
end = query.get("endTime", "")
|
||||
direction = query.get("direction", "")
|
||||
direction_label = {"U": "美", "T": "台", "J": "日", "F": "菲"}.get(direction, direction)
|
||||
rows = [
|
||||
row
|
||||
for row in EVENT_ROWS.get(discriminant, [])
|
||||
if (not direction_label or row["direction"] == direction_label)
|
||||
and _contains(row, keyword, ("m_title", "m_content"))
|
||||
and _in_range(row["m_publish_time_date"], start, end)
|
||||
]
|
||||
size = max(1, int(query.get("size", "50") or 50))
|
||||
self._json(200, {"data": rows[:size], "msg": "操作成功", "state": 200})
|
||||
|
||||
def _real_network(self, task_id: str) -> None:
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"taskId": f"{task_id}-A01",
|
||||
"connectionType": "打击",
|
||||
"taskName": "重点目标关联处置",
|
||||
"taskDirection": "T",
|
||||
"taskLevel": 1,
|
||||
"taskStatus": 20,
|
||||
"taskContent": "围绕重点目标形成处置任务。",
|
||||
"sourceType": 0,
|
||||
"createTime": "2026-09-29 09:10:00",
|
||||
},
|
||||
{
|
||||
"taskId": f"{task_id}-S01",
|
||||
"connectionType": "塑势",
|
||||
"taskName": "公开信息正面塑势",
|
||||
"taskDirection": "T",
|
||||
"taskLevel": 2,
|
||||
"taskStatus": 10,
|
||||
"taskContent": "结合热点事件形成公开信息塑势任务。",
|
||||
"sourceType": 1,
|
||||
"createTime": "2026-09-29 09:30:00",
|
||||
},
|
||||
],
|
||||
"msg": "操作成功",
|
||||
"state": 200,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="商业情报监测接口本地模拟服务")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=18080)
|
||||
args = parser.parse_args()
|
||||
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
||||
print(f"mock monitor API listening on http://{args.host}:{args.port}/consumer", flush=True)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
server.server_close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,7 @@
|
||||
param(
|
||||
[string]$HostAddress = "127.0.0.1",
|
||||
[int]$Port = 18080
|
||||
)
|
||||
|
||||
$scriptPath = Join-Path $PSScriptRoot "mock_server.py"
|
||||
python $scriptPath --host $HostAddress --port $Port
|
||||
Reference in new issue
Block a user