Răsfoiți Sursa

feat(web-modules): manage WeCom robot key via {\} macro; adopt v2 markdown alerts + recovery

FlyingRome 4 zile în urmă
părinte
comite
a8c902401c

+ 13 - 4
web-modules/README.md

@@ -71,12 +71,21 @@ WecomCenter/
 - 关键 API 注意:`mediatype.get` **不支持 `sortfield=name`**(会整体返回 false);`configuration.import` 的规则键是复数 `templates`(不是 `template`);`trigger.update` **不接受 `triggers` 数组**,只能逐条用 `triggerid` 更新。
 - 告警“开关”落在用户媒介的 `active` 字段:**0=启用、1=停用**;严重度为位掩码(未分类=1…灾难=32)。触发器“是否告警”落在 `trigger.status`:**0=启用、1=停用**。
 
-## 告警媒介说明
+## 告警媒介说明(key 已迁到全局宏)
 
-页面基于“Webhook 类型(type=4)的媒介 + 一个机器人账号(默认用户名 `wecomrobot`)的媒介记录”工作:
+页面基于“Webhook 类型(type=4)的媒介 + 一个机器人账号(默认用户名 `wecomrobot`)的媒介记录”工作。
 
+- **机器人 key 存于全局宏 `{$WECOM_KEY}`**(Administration → Macros),页面“机器人 key”字段直接**读取/写入该宏**——key 不再散落在一长串 `webhook_url` 里。换机器人只改这一格。
+- 媒介 102 的固定参数:`key = {$WECOM_KEY}`、`message = {ALERT.MESSAGE}`。
+- Webhook 脚本按 `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key= + params.key` 拼 URL,用 **markdown** 发送(失败自动回退 text)。
 - 一键方案 / 勾选保存 → 修改该账号对应媒介的 `severity` / `active`。
-- 保存 key → 修改该 Webhook 媒介的 `webhook_url` 参数(保留其它参数)。
-- 发送测试 → 直接 POST 到该 `webhook_url`,检查返回 `errcode:0`。
+- 发送测试 → 用 base + `{$WECOM_KEY}` 拼 URL 直接 POST,检查返回 `errcode:0`。
+
+## 配套告警链路(告警方案 v2,已按预期配置)
+
+- **动作 7「Send alarm by Wecom Robot」**:问题消息改为 markdown 红字(`<font color="warning">`),并**新增恢复操作**发绿字(`<font color="info">`),两者均带前端直达链接 `{$ZBX_URL}/tr_events.php?triggerid={TRIGGER.ID}&eventid={EVENT.ID}`。
+- **全局宏**:`{$WECOM_KEY}`(机器人 key)、`{$ZBX_URL}`(前端地址,默认 `http://192.168.87.20:8080`)。
+- **收件人**:统一为机器人账号 `wecomrobot`(uid 6)的媒介 102,不再指向真人账号。
+- **备份与回滚**:改动前已导出 `/opt/zabbix/backup/wecom-config-backup-*.json`(含媒介 102、动作 7、全局宏),可用对应 API 还原。
 
 若环境中没有 Webhook 媒介类型,页面会提示“未找到 webhook 媒介类型”。

+ 1 - 1
web-modules/WecomCenter/actions/WecomCenterSave.php

@@ -118,7 +118,7 @@ class WecomCenterSave extends CController {
 	private function saveRobot(string $mediatypeid): void {
 		$url = trim((string) ($_REQUEST['webhook_url'] ?? ''));
 		if ($url !== '') {
-			Helper::setWebhookUrl($mediatypeid, $url);
+			Helper::setWebhookKey($url);
 		}
 		$user = Helper::ensureRobotUser($this->userName());
 		$cur = Helper::currentMedia($user, $mediatypeid);

+ 1 - 1
web-modules/WecomCenter/actions/WecomCenterView.php

@@ -52,7 +52,7 @@ class WecomCenterView extends CController {
 		if ($mediatypeid !== '') {
 			$mediatype = Helper::getMediatype($mediatypeid);
 			if ($mediatype) {
-				$webhook_url = Helper::webhookUrl($mediatype);
+				$webhook_url = Helper::webhookKey($mediatype);
 			}
 			$robot = Helper::robotUser('wecomrobot');
 			$media = Helper::currentMedia($robot, $mediatypeid);

+ 65 - 27
web-modules/WecomCenter/lib/Helper.php

@@ -59,42 +59,80 @@ class Helper {
 		}
 	}
 
-	/**
-	 * Read the webhook URL / key from a media type parameters list.
-	 */
-	public static function webhookUrl(array $mediatype): string {
-		foreach (($mediatype['parameters'] ?? []) as $p) {
-			$name = strtolower($p['name']);
-			if (in_array($name, ['webhook_url', 'url', 'webhook'], true)) {
-				return (string) $p['value'];
-			}
+	// Fixed WeCom group-robot endpoint; the per-bot secret lives in {$WECOM_KEY}.
+	public const WECOM_BASE = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=';
+	public const KEY_MACRO  = '{$WECOM_KEY}';
+
+	/** Read a global macro value; null when missing. */
+	public static function getGlobalMacro(string $name): ?string {
+		try {
+			$res = API::UserMacro()->get([
+				'globalmacro' => true,
+				'output'      => ['globalmacroid', 'macro', 'value'],
+				'filter'      => ['macro' => $name]
+			]);
+			return ($res && isset($res[0])) ? (string) $res[0]['value'] : null;
+		}
+		catch (\Throwable $e) {
+			return null;
+		}
+	}
+
+	/** Create or update a global macro. */
+	public static function setGlobalMacro(string $name, string $value): void {
+		$res = API::UserMacro()->get([
+			'globalmacro' => true,
+			'output'      => ['globalmacroid', 'macro'],
+			'filter'      => ['macro' => $name]
+		]);
+		if ($res && isset($res[0])) {
+			API::UserMacro()->updateGlobal([
+				['globalmacroid' => $res[0]['globalmacroid'], 'value' => $value]
+			]);
+		}
+		else {
+			API::UserMacro()->createGlobal([
+				['macro' => $name, 'value' => $value]
+			]);
 		}
-		return '';
 	}
 
 	/**
-	 * Update (or add) the webhook_url parameter of a media type, preserving others.
+	 * Current robot key: {$WECOM_KEY} global macro, falling back to any legacy
+	 * value still stored in the media type parameters.
 	 */
-	public static function setWebhookUrl(string $mediatypeid, string $url): void {
-		$mt = self::getMediatype($mediatypeid);
-		if (!$mt) {
-			throw new Exception('Media type not found: '.$mediatypeid);
+	public static function webhookKey(?array $mediatype = null): string {
+		$v = self::getGlobalMacro(self::KEY_MACRO);
+		if ($v !== null && $v !== '') {
+			return $v;
 		}
-		$params = [];
-		$found = false;
-		foreach (($mt['parameters'] ?? []) as $p) {
-			if (in_array(strtolower($p['name']), ['webhook_url', 'url', 'webhook'], true)) {
-				$params[] = ['name' => $p['name'], 'value' => $url];
-				$found = true;
+		foreach (($mediatype['parameters'] ?? []) as $p) {
+			$n = strtolower($p['name']);
+			$val = (string) $p['value'];
+			if ($n === 'key' && strpos($val, '{') === false) {
+				return $val;
 			}
-			else {
-				$params[] = ['name' => $p['name'], 'value' => $p['value']];
+			if (in_array($n, ['webhook_url', 'url', 'webhook'], true)) {
+				if (strpos($val, 'key=') !== false) {
+					return substr($val, strpos($val, 'key=') + 4);
+				}
+				if (strpos($val, 'http') !== 0) {
+					return $val;
+				}
 			}
 		}
-		if (!$found) {
-			$params[] = ['name' => 'webhook_url', 'value' => $url];
-		}
-		API::Mediatype()->update(['mediatypeid' => $mediatypeid, 'parameters' => $params]);
+		return '';
+	}
+
+	/** Persist the robot key into the {$WECOM_KEY} global macro (managed here, not in the media type). */
+	public static function setWebhookKey(string $key): void {
+		self::setGlobalMacro(self::KEY_MACRO, $key);
+	}
+
+	/** Full webhook URL for this bot = fixed base + {$WECOM_KEY}. */
+	public static function webhookUrl(?array $mediatype = null): string {
+		$key = self::webhookKey($mediatype);
+		return $key === '' ? '' : self::WECOM_BASE.$key;
 	}
 
 	/* ------------------------------------------------------------------ */

+ 2 - 2
web-modules/WecomCenter/locale/en.php

@@ -12,7 +12,7 @@ return [
 	'lbl.severity'    => 'Severities that will notify',
 	'lbl.sendto'      => 'Recipient (send to)',
 	'lbl.scheme_hint' => 'One-click schemes (applied immediately):',
-	'lbl.key'         => 'Webhook URL / robot key',
+	'lbl.key'         => 'Robot key (global macro {$WECOM_KEY})',
 	'lbl.account'     => 'Robot account (username)',
 	'btn.save'        => 'Save',
 	'btn.test'        => 'Send test',
@@ -42,7 +42,7 @@ return [
 	'msg.map_saved'   => 'Map coordinates saved.',
 	'msg.scheme_applied' => 'Scheme applied: {1}',
 	'msg.no_mediatype' => 'No webhook media type found.',
-	'msg.no_url'      => 'Webhook URL is empty.',
+	'msg.no_url'      => 'Robot key is empty (set {$WECOM_KEY}).',
 	'msg.no_template' => 'Please choose a template.',
 	'msg.no_host'     => 'Please choose a host.',
 	'msg.bad_scheme'  => 'Unknown scheme.',

+ 2 - 2
web-modules/WecomCenter/locale/zh_CN.php

@@ -12,7 +12,7 @@ return [
 	'lbl.severity'    => '触发通知的严重度',
 	'lbl.sendto'      => '接收目标(sendto)',
 	'lbl.scheme_hint' => '一键方案(立即生效):',
-	'lbl.key'         => 'Webhook 地址 / 机器人 key',
+	'lbl.key'         => '机器人 key(全局宏 {$WECOM_KEY})',
 	'lbl.account'     => '机器人账号(用户名)',
 	'btn.save'        => '保存',
 	'btn.test'        => '发送测试',
@@ -42,7 +42,7 @@ return [
 	'msg.map_saved'   => '地图坐标已保存。',
 	'msg.scheme_applied' => '已应用方案:{1}',
 	'msg.no_mediatype' => '未找到 webhook 媒介类型。',
-	'msg.no_url'      => 'Webhook 地址为空。',
+	'msg.no_url'      => '机器人 key 为空(请设置 {$WECOM_KEY})。',
 	'msg.no_template' => '请选择一个模板。',
 	'msg.no_host'     => '请选择一台主机。',
 	'msg.bad_scheme'  => '未知方案。',