Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export default defineConfig({
text: '用户属性扩展',
link: '/advance/user-property'
},
{
text: '@ 人',
link: '/advance/at-mention'
},
{
text: 'Markdown 语法',
link: '/advance/markdown'
Expand Down
86 changes: 86 additions & 0 deletions docs/advance/at-mention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# @ 人

## @ 是怎么生效的

请求里的 `at` 对象带上被 @ 人的手机号(或 `isAtAll`)——它决定**谁收到通知**。

`MARKDOWN` 和 `ACTION_CARD` 除此之外还要求**消息正文里出现对应的 `@手机号` 文本**,
否则 @ 完全不生效、连通知都没有。插件会自动补上:把 `@手机号` 追加到消息末尾;
如果你在自定义消息里**已经写了** `@手机号`,插件不会再追加一遍,@ 会留在你写的位置。

`TEXT` 类型**不需要**正文里有 `@手机号`——钉钉会自己把 @ 渲染出来(正文没写就追加到末尾,
写了就在原位渲染),所以插件对 `TEXT` 类型不做任何追加。

::: tip

只有**在群内的成员**才可被 @。官方说明:非群内成员的手机号**会被脱敏**,此时 @ 完全不生效、也不会有通知。
如果 @ 没有任何反应,先确认这个手机号是不是机器人所在群的成员。

:::

勾选 `通知所有人` 时,插件**不再把手机号追加到正文**:所有人本来就会收到通知,而钉钉在 @所有人 的情况下
不再解析单个手机号,追加上去只会在消息里留下一串没有变成姓名的号码。

## 各消息类型的差异

| 消息类型 | 是否支持 @ | 电脑端可点击 | 手机端可点击 |
|---|:---:|:---:|:---:|
| `TEXT` | ✅ | ✅ | **✅** |
| `MARKDOWN` | ✅ | ✅ | **❌** |
| `ACTION_CARD` | ✅ | ✅ | **❌** |
| `LINK` | ❌ | —— | —— |

「是否支持 @」指的是**通知能否送达**。`MARKDOWN` 和 `ACTION_CARD` 的 @ **通知是正常送达的**,
被 @ 的人会收到提醒;差别只在手机端的**显示与交互**。

## 手机端 @ 不可点击是钉钉的行为

**插件的内置消息使用 `ACTION_CARD` 类型,因此它的 @ 在手机端不会高亮、也点不开。**
这不是插件的问题,钉钉对此有明确说明:

> **@人是否高亮显示**:使用不同类型发送消息,被@后是否高亮显示效果不同。
>
> | @效果 | 是否支持高亮 |
> |---|---|
> | Text类型 | 是 |
> | Markdown类型 | 否 |
> | ActionCard类型 | 否 |
>
> 机器人发送 Markdown 消息类型@人员,不支持高亮显示。
>
> —— [钉钉开放平台《机器人发送消息类型与@人》](https://open.dingtalk.com/document/orgapp/robot-message-type-staff-information-in-an-enterprise)

经实测,`MARKDOWN` / `ACTION_CARD` 里的 @ 在电脑端会被渲染出来并且可以点击,
**手机端则完全不渲染、点不开**;`TEXT` 类型两端都可以点击。
换用 `atUserIds`(钉钉 userId)代替手机号**不会改变这一点**——决定因素是消息类型,不是标识符。

::: tip

@ 的具体样式取决于你是不是被 @ 的那个人(官方对「查看 @ 自己的消息」和「@ 其他人的消息」分别举例),
所以判断 @ 有没有真正生效,看的是**能不能点开**,而不是颜色。

:::

### 需要可点击的 @ 怎么办

在 pipeline 中使用 `TEXT` 类型发送:

```groovy
dingtalk(
robot: 'robot-id',
type: 'TEXT',
text: ['构建完成,请查看'],
at: ['13800000000']
)
```

代价是 `TEXT` 类型**不渲染任何 markdown**:标题、链接、颜色、按钮都不可用,
`#`、`[文字](链接)`、`<font>` 都会原样显示。所以这是一个取舍——
需要卡片样式就接受手机端的 @ 不可点击,需要可点击的 @ 就接受纯文本。

::: warning

不要为了 @ 的交互效果把内置消息改成 `TEXT`——内置消息依赖 markdown 渲染标题、状态颜色和两个按钮,
换成 `TEXT` 后这些都会变成可见的原始标记。

:::
7 changes: 7 additions & 0 deletions docs/advance/user-property.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

为了实现 `执行人` 字段带 `@` 效果,需要为 Jenkins 用户补充相关信息

::: warning

内置消息使用 `ACTION_CARD` 类型,钉钉对这类消息的 @ **在手机端不高亮、也不可点击**(通知会正常送达)。
这是钉钉的既有行为,详见 [@ 人](./at-mention.md)。

:::

1. 打开 **Manage Users**

::: details 查看详细
Expand Down
7 changes: 7 additions & 0 deletions docs/guide/pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ dingtalk(
| at | List\<String\> | 需要 @ 的手机号码 |
| atAll | boolean | 是否 @ 全部 |

::: warning

不同消息类型的 @ 表现不同:`MARKDOWN` 和 `ACTION_CARD` 的 @ 在**手机端不高亮、不可点击**,
只有 `TEXT` 类型两端都可点击。详见 [@ 人](../advance/at-mention.md)。

:::

::: tip

`robot` 参数可以在机器人配置中找到
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/jenkins/plugins/DingTalkRunListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ private void send(Run<?, ?> run, TaskListener listener, NoticeOccasionEnum notic
MessageModel msgModel = item.isRaw()
? MessageModel.builder()
.type(MsgTypeEnum.MARKDOWN)
.atAll(atAll)
.atMobiles(atMobiles)
.title(title)
.text(envVars.expand(rawMessage).replace("\\\\n", "\n"))
.build()
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/jenkins/plugins/model/MessageModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public At getAt() {
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList()));
}
at.setIsAtAll(atAll);
at.setAtAll(atAll);
return at;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static class At {
/** 被 @ 人的手机号 */
private List<String> atMobiles;
/** 是否 @ 所有人 */
private Boolean isAtAll;
private boolean isAtAll;
}

/**
Expand Down
54 changes: 37 additions & 17 deletions src/main/java/io/jenkins/plugins/sdk/DingTalkSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -58,30 +59,34 @@
/**
* 发送 text 类型的消息
*
* <p>Unlike markdown and actionCard, DingTalk renders the mention for this type from the
* {@code at} object on its own — appending one to the content as well gets it rendered twice.
*
* @param msg 消息
* @return 异常信息
*/
public String sendText(MessageModel msg) {
At at = msg.getAt();
Text text = new Text();
text.setAt(at);
text.setContent(addKeyWord(addAtInfo(msg.getText(), at, false)));
text.setAt(msg.getAt());
text.setContent(addKeyWord(msg.getText()));

return call(text);
}

/**
* 发送 link 类型的消息
*
* <p>DingTalk documents the link type as not supporting mentions at all: the {@code at} object is
* ignored, and an appended {@code @<mobile>} is never resolved — it would just leave a bare phone
* number in the message and notify nobody.
*
* @param msg 消息
* @return 异常信息
*/
public String sendLink(MessageModel msg) {
At at = msg.getAt();
Link link = new Link();
link.setAt(at);
link.setTitle(addKeyWord(msg.getTitle()));
link.setText(addAtInfo(msg.getText(), at, false));
link.setText(msg.getText());
link.setMessageUrl(msg.getMessageUrl());
link.setPicUrl(msg.getPicUrl());

Expand All @@ -93,7 +98,7 @@
Markdown markdown = new Markdown();
markdown.setAt(at);
markdown.setTitle(addKeyWord(msg.getTitle()));
markdown.setText(addAtInfo(msg.getText(), at, true));
markdown.setText(addAtInfo(msg.getText(), at));

return call(markdown);
}
Expand All @@ -103,7 +108,7 @@
ActionCard actioncard = new ActionCard();
actioncard.setAt(at);
actioncard.setTitle(addKeyWord(msg.getTitle()));
actioncard.setText(addAtInfo(msg.getText(), at, true));
actioncard.setText(addAtInfo(msg.getText(), at));
String singleTitle = msg.getSingleTitle();
if (StringUtils.isEmpty(singleTitle)) {
actioncard.setBtns(msg.getRobotBtns());
Expand Down Expand Up @@ -185,20 +190,35 @@
/**
* 添加 at 信息
*
* <p>Only markdown and actionCard need this. A mobile never resolves for them unless the body
* carries its {@code @<mobile>} token, and actionCard needs one for {@code isAtAll} as well;
* markdown is the single case that notifies without a token, and even there the token is what
* makes the mention visible. Text messages are the other way round — DingTalk renders their
* mention itself — and the link type supports none.
*
* <p>Tokens the caller already placed in the body are skipped, so a hand-written template keeps
* its mention where the author put it instead of getting a second copy appended.
*
* @param content 原始内容
* @param at at 配置
* @param markdown 是否是 markdown 格式的内容
* @return 包含 at 信息的内容
*/
private String addAtInfo(String content, At at, boolean markdown) {
List<String> atMobiles = at.getAtMobiles();
if (atMobiles == null || atMobiles.isEmpty()) {
return content;
private String addAtInfo(String content, At at) {
String body = StringUtils.defaultString(content);
List<String> tokens = new ArrayList<>();
if (at.isAtAll()) {
// Mentioning everyone stops DingTalk from resolving individual mobiles, so adding them here
// as well would leave bare phone numbers in the message where a name should have appeared —
// and everybody has already been notified anyway.
tokens.add(Constants.AT_ALL);
} else if (at.getAtMobiles() != null) {

Check warning on line 214 in src/main/java/io/jenkins/plugins/sdk/DingTalkSender.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 214 is only partially covered, one branch is missing
at.getAtMobiles().forEach(mobile -> tokens.add("@" + mobile));
}
String atContent = "@" + StringUtils.join(atMobiles, " @");
if (markdown) {
return content + "\n\n" + Utils.dye(atContent, Constants.COLOR_BLUE) + "\n";
tokens.removeIf(body::contains);
if (tokens.isEmpty()) {
return body;
}
return content + atContent;
String atContent = String.join(" ", tokens);
return body + "\n\n" + Utils.dye(atContent, Constants.COLOR_BLUE) + "\n";
}
}
5 changes: 5 additions & 0 deletions src/main/java/io/jenkins/plugins/tools/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public class Constants {
// Content Types
public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";

// Mentions
// The literal DingTalk resolves into a mention of the whole group. Its English spellings are
// delivered but not resolved, so this string cannot be localised.
public static final String AT_ALL = "@所有人";

// API Endpoints
public static final String DINGTALK_WEBHOOK_URL_PREFIX = "https://oapi.dingtalk.com/robot/send?access_token=";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@
</tbody>
</table>
</f:entry>
<f:entry title="${%notifyPeople}" field="at">
<f:checkbox title="${%atAll}" field="atAll"/>
<f:textarea field="atMobile"/>
</f:entry>
<div class="dt-raw-content-custom" style="display: none;">
<f:entry field="message" title="${%customMessage}">
<f:textarea/>
</f:entry>
</div>
<div class="dt-raw-content-builtin">
<f:entry title="${%notifyPeople}" field="at">
<f:checkbox title="${%atAll}" field="atAll"/>
<f:textarea field="atMobile"/>
</f:entry>
<f:entry field="content" title="${%customContent}">
<f:textarea/>
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@
atAll: Is it @ everyone?
<br>
Textarea: The mobile phone number configured in DingTalk, used to @ mention the corresponding person. Separate multiple values with commas or line breaks, supports environment variables.
<br>
<br>
With <strong>Notify everyone</strong> ticked the mobile numbers are no longer added to the message — everyone is notified anyway.
<br>
Only <strong>members of the group</strong> can be mentioned; DingTalk masks the number of anyone who is not.
<br>
DingTalk does not highlight a mention in a Markdown or ActionCard message, and it <strong>is not tappable on the mobile client</strong> — the notification itself is still delivered. This is DingTalk's own behaviour.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@
atAll: 是否 @ 全部。
<br>
输入框: 钉钉里面配置的手机号码,用于 @ 对应的人。多个值使用逗号或者换行分割,支持环境变量。
<br>
<br>
勾选 <strong>通知所有人</strong> 时,手机号列表不再追加到消息里——所有人都会收到通知。
<br>
只有<strong>群内成员</strong>的手机号才能被 @,非群内成员的号码会被钉钉脱敏。
<br>
钉钉对 Markdown / ActionCard 类型消息的 @ <strong>在手机端不高亮、也不可点击</strong>(通知会正常送达),
这是钉钉的既有行为。
</div>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<div>
Disabling the plugin's built-in encapsulation message, only sends the <strong>Custom Message</strong> below, which is equivalent to fully customizing the message using markdown.
<br>
<strong>Notify people</strong> still applies: the mentions configured there are appended to the message, unless it already contains them.
</div>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<div>
禁用插件内部封装的消息仅发送下方的 <strong>自定义消息</strong>,相当于使用 markdown 完全自定义消息
<br>
<strong>通知人</strong> 依然生效:其中配置的 @ 信息会追加到消息里,除非消息里已经写了。
</div>
Loading