Skip to content

Commit ee3e72e

Browse files
committed
Add opinion monitor quickstart
1 parent 5cffb51 commit ee3e72e

16 files changed

Lines changed: 1762 additions & 47 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,7 @@ odp-rs/target/
8181
.gstack/
8282
.repowise/
8383
.understand-anything/
84+
.codex/
85+
.sentrux/agent-sessions/
86+
.sentrux/baseline.json
87+
artifacts/

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,52 @@ docker compose up -d # 启动中心 + agent-1
101101

102102
---
103103

104+
### 舆情监控实战闭环
105+
106+
当前实战链路已经投到真实运行面,而不是只停留在配置说明:
107+
108+
1. **多账号 / 多节点采集** — 通过「节点管理」和站点绑定,把 `opencli` 采集路由到指定 WS agent;验收脚本会证明 `chrome_endpoint``node_url` 都落在绑定节点。
109+
2. **AI 摘要与打标**`collect → normalize → store → ai → notify` 流水线会把模型输出写入 `ai_enrichment`,监控台读取真实记录展示摘要、标签和情绪分布。
110+
3. **飞书推送** — 飞书模板可以直接引用 `{{summary}}``{{tags}}``{{sentiment}}`,把 AI 处理后的内容推到群机器人。
111+
4. **可视化验收** — 「监控台」的舆情监控卡片读取 `/api/v1/dashboard/opinion-monitor`,展示最近热点、AI 处理量、Feishu sent/failed 证据和来源贡献。
112+
113+
一键生成实战配置:
114+
115+
```bash
116+
curl -X POST http://localhost:8000/api/v1/presets/opinion-monitor/apply \
117+
-H "Content-Type: application/json" \
118+
-d '{
119+
"source_prefix": "实战舆情",
120+
"feishu_webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
121+
}'
122+
```
123+
124+
这会创建两条默认 `aibase news` 多账号采集源、对应定时计划,以及一个飞书规则。
125+
如果暂时不填 `feishu_webhook_url`,飞书规则会以 disabled 状态创建,不会伪造推送成功。
126+
127+
关键验收命令:
128+
129+
```powershell
130+
scripts\acceptance\fleet-acceptance.ps1 `
131+
-Site aibase `
132+
-Command news `
133+
-Limit 1 `
134+
-CenterPort 8032 `
135+
-AgentPort 19824 `
136+
-FreshDb
137+
```
138+
139+
如果本机已有旧 API/agent 进程占用端口,可以换一组固定端口,例如
140+
`-CenterPort 8035 -AgentPort 19828`
141+
142+
最终应输出:
143+
144+
```text
145+
ACCEPTANCE: PASS
146+
```
147+
148+
---
149+
104150
## 快速开始
105151

106152
### 方式零:前后端本地开发(推荐)

backend/agent_server.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,17 @@ def _resolve_bin(mode: str) -> str: # noqa: ARG001
9191
_CENTRAL_API_URL = os.environ.get("CENTRAL_API_URL", "").rstrip("/")
9292
_AGENT_ADVERTISE_URL = os.environ.get("AGENT_ADVERTISE_URL", "")
9393
_AGENT_MODE = os.environ.get("AGENT_MODE", "cdp")
94-
# Deployment/startup type reported to center: "docker" (running in container) | "shell" (native process)
94+
# Deployment/startup type reported to center:
95+
# "docker" (container) | "shell" (native process).
9596
_AGENT_DEPLOY_TYPE = os.environ.get("AGENT_DEPLOY_TYPE", "docker")
9697
# True when the image was built with INSTALL_CHROME=true (Chrome bundled inside container).
9798
# False → Chrome runs on the host; localhost must be remapped to host.docker.internal.
9899
_AGENT_HAS_CHROME = os.environ.get("AGENT_HAS_CHROME", "false").lower() == "true"
99100
_AGENT_LABEL = os.environ.get("AGENT_LABEL", socket.gethostname())
100101
# Registration mode:
101102
# http — LAN mode: agent POSTs its URL to center, center calls back via HTTP (default)
102-
# ws — NAT/reverse-channel mode: agent opens WS to center, registration via WS handshake (Phase 2)
103+
# ws — NAT/reverse-channel mode: agent opens WS to center, then
104+
# registers through the WS handshake.
103105
# off — disable auto-registration entirely
104106
_AGENT_REGISTER = os.environ.get("AGENT_REGISTER", "http").lower()
105107
# opencli subprocess execution timeout in seconds
@@ -191,7 +193,12 @@ async def _register_with_center(advertise_url: str) -> None:
191193
return
192194
except Exception as exc:
193195
wait = attempt * 3
194-
logger.warning("Registration attempt %d failed: %s — retrying in %ds", attempt, exc, wait)
196+
logger.warning(
197+
"Registration attempt %d failed: %s — retrying in %ds",
198+
attempt,
199+
exc,
200+
wait,
201+
)
195202
await asyncio.sleep(wait)
196203
logger.error("Could not register with center after 5 attempts")
197204

@@ -281,7 +288,11 @@ async def _send_result(result: dict) -> None:
281288
"event": event,
282289
}))
283290
except Exception as exc:
284-
logger.error("WS: failed to send agent_event for request_id=%s: %s", request_id, exc)
291+
logger.error(
292+
"WS: failed to send agent_event for request_id=%s: %s",
293+
request_id,
294+
exc,
295+
)
285296
if terminal_event is None:
286297
# Contract violation (adapter yielded nothing) — still must resolve
287298
# the center's pending future rather than hang it until timeout.
@@ -293,7 +304,11 @@ async def _send_result(result: dict) -> None:
293304
}
294305
await _send_result(terminal_event)
295306
except RuntimeInvocationError as exc:
296-
logger.exception("WS agent_task request_id=%s: adapter invocation error: %s", request_id, exc)
307+
logger.exception(
308+
"WS agent_task request_id=%s: adapter invocation error: %s",
309+
request_id,
310+
exc,
311+
)
297312
await _send_result({
298313
"type": "error",
299314
"task_id": request_id,
@@ -409,11 +424,19 @@ async def lifespan(app: FastAPI):
409424
_CENTRAL_API_URL or "", _AGENT_REGISTER)
410425
elif _AGENT_REGISTER == "http":
411426
advertise_url = _detect_advertise_url()
412-
logger.info("LAN registration: advertise_url=%s → center=%s", advertise_url, _CENTRAL_API_URL)
427+
logger.info(
428+
"LAN registration: advertise_url=%s → center=%s",
429+
advertise_url,
430+
_CENTRAL_API_URL,
431+
)
413432
asyncio.get_event_loop().create_task(_register_with_center(advertise_url))
414433
elif _AGENT_REGISTER == "ws":
415434
advertise_url = _detect_advertise_url()
416-
logger.info("WS registration: advertise_url=%s → center=%s", advertise_url, _CENTRAL_API_URL)
435+
logger.info(
436+
"WS registration: advertise_url=%s → center=%s",
437+
advertise_url,
438+
_CENTRAL_API_URL,
439+
)
417440
_ws_task = asyncio.get_event_loop().create_task(_register_via_ws(advertise_url))
418441
yield
419442
if _ws_task and not _ws_task.done():
@@ -468,7 +491,11 @@ async def _cleanup_cdp_tabs(cdp_endpoint: str, pre_existing_ids: set[str]) -> No
468491
if tab.get("type") == "page" and tab_id not in pre_existing_ids:
469492
try:
470493
await client.get(f"{cdp_endpoint}/json/close/{tab_id}")
471-
logger.info("cleanup: closed new tab %s url=%s", tab_id, tab.get("url", "")[:80])
494+
logger.info(
495+
"cleanup: closed new tab %s url=%s",
496+
tab_id,
497+
tab.get("url", "")[:80],
498+
)
472499
remaining_pages -= 1
473500
except Exception:
474501
pass
@@ -560,7 +587,7 @@ async def collect(req: CollectRequest) -> dict:
560587
)
561588
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=_OPENCLI_TIMEOUT)
562589
rc = proc.returncode
563-
except asyncio.TimeoutError:
590+
except TimeoutError:
564591
logger.error("timeout | cmd=%s", " ".join(cmd))
565592
if proc:
566593
proc.kill()

0 commit comments

Comments
 (0)