dsh-dingtalk 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.d.ts CHANGED
@@ -27,8 +27,8 @@ export declare class DingTalkClient {
27
27
  private readonly fetchImpl;
28
28
  constructor(options: DingTalkClientOptions);
29
29
  /** 发送 Markdown 消息:{ msgtype: 'markdown', markdown: { title, text } } */
30
- sendMarkdown(title: string, text: string): Promise<DingTalkSendResult>;
30
+ sendMarkdown(title: string, text: string, signal?: AbortSignal): Promise<DingTalkSendResult>;
31
31
  /** 发送纯文本消息:{ msgtype: 'text', text: { content } } */
32
- sendText(content: string): Promise<DingTalkSendResult>;
32
+ sendText(content: string, signal?: AbortSignal): Promise<DingTalkSendResult>;
33
33
  private post;
34
34
  }
package/lib/client.js CHANGED
@@ -31,52 +31,64 @@ export class DingTalkClient {
31
31
  this.fetchImpl = options.fetchImpl ?? defaultFetch;
32
32
  }
33
33
  /** 发送 Markdown 消息:{ msgtype: 'markdown', markdown: { title, text } } */
34
- async sendMarkdown(title, text) {
35
- return this.post({ msgtype: 'markdown', markdown: { title, text } });
34
+ async sendMarkdown(title, text, signal) {
35
+ return this.post({ msgtype: 'markdown', markdown: { title, text } }, signal);
36
36
  }
37
37
  /** 发送纯文本消息:{ msgtype: 'text', text: { content } } */
38
- async sendText(content) {
39
- return this.post({ msgtype: 'text', text: { content } });
38
+ async sendText(content, signal) {
39
+ return this.post({ msgtype: 'text', text: { content } }, signal);
40
40
  }
41
- async post(payload) {
41
+ async post(payload, signal) {
42
+ signal?.throwIfAborted();
42
43
  const url = signedUrl(this.webhook, this.secret || undefined);
44
+ const parsedUrl = new URL(url);
45
+ const redact = (text) => [url, this.webhook, this.secret, parsedUrl.searchParams.get('access_token'), parsedUrl.searchParams.get('sign')]
46
+ .filter((value) => typeof value === 'string' && value !== '')
47
+ .reduce((safe, value) => safe.replaceAll(value, '[已隐藏]').replaceAll(encodeURIComponent(value), '[已隐藏]'), text);
43
48
  const controller = new AbortController();
44
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
45
- let response;
49
+ const requestSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
50
+ const timer = setTimeout(() => controller.abort(new Error('请求超时')), this.timeoutMs);
46
51
  try {
47
- response = await this.fetchImpl(url, {
52
+ const response = await this.fetchImpl(url, {
48
53
  method: 'POST',
49
54
  headers: { 'Content-Type': 'application/json' },
50
55
  body: JSON.stringify(payload),
51
- signal: controller.signal,
56
+ signal: requestSignal,
52
57
  });
58
+ requestSignal.throwIfAborted();
59
+ if (!response.ok) {
60
+ const body = await response.text().catch(() => '');
61
+ requestSignal.throwIfAborted();
62
+ throw new DingTalkError('钉钉机器人返回 HTTP ' + response.status + ':' + body.slice(0, 200));
63
+ }
64
+ let body;
65
+ try {
66
+ body = await response.json();
67
+ }
68
+ catch (error) {
69
+ requestSignal.throwIfAborted();
70
+ throw new DingTalkError('钉钉机器人返回非 JSON 响应(HTTP ' + response.status + ')');
71
+ }
72
+ requestSignal.throwIfAborted();
73
+ const result = body;
74
+ if (!Number.isSafeInteger(result?.errcode))
75
+ throw new DingTalkError('钉钉机器人返回格式异常:缺少 errcode');
76
+ if (result.errcode !== 0)
77
+ throw new DingTalkError(dingTalkErrorMessage(result.errcode, result.errmsg ?? ''), result.errcode);
78
+ return result;
53
79
  }
54
80
  catch (error) {
81
+ signal?.throwIfAborted();
82
+ if (controller.signal.aborted)
83
+ throw new DingTalkError('钉钉机器人请求超时,尚未确认远端是否收到;请先核对群消息,再决定是否重发。');
84
+ if (error instanceof DingTalkError)
85
+ throw new DingTalkError(redact(error.message), error.errcode);
55
86
  const message = error instanceof Error ? error.message : String(error);
56
- const hint = /abort/i.test(message) ? '(请求超时)' : '';
57
- throw new DingTalkError('钉钉机器人请求失败' + hint + ':' + message);
87
+ const safe = redact(message);
88
+ throw new DingTalkError('钉钉机器人请求失败:' + safe);
58
89
  }
59
90
  finally {
60
91
  clearTimeout(timer);
61
92
  }
62
- if (!response.ok) {
63
- const body = await response.text().catch(() => '');
64
- throw new DingTalkError('钉钉机器人返回 HTTP ' + response.status + ':' + body.slice(0, 200));
65
- }
66
- let body;
67
- try {
68
- body = await response.json();
69
- }
70
- catch {
71
- throw new DingTalkError('钉钉机器人返回非 JSON 响应(HTTP ' + response.status + ')');
72
- }
73
- const result = body;
74
- if (typeof result?.errcode !== 'number') {
75
- throw new DingTalkError('钉钉机器人返回格式异常:缺少 errcode');
76
- }
77
- if (result.errcode !== 0) {
78
- throw new DingTalkError(dingTalkErrorMessage(result.errcode, result.errmsg ?? ''), result.errcode);
79
- }
80
- return result;
81
93
  }
82
94
  }
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { registerUpdates } from '@stardustlc/dsh-updates';
1
2
  import { resolveDingTalkConfig } from './config.js';
2
3
  import { DingTalkClient } from './client.js';
3
4
  export const name = 'tool-dingtalk';
@@ -36,6 +37,10 @@ const notifySchema = {
36
37
  function oneText(text) {
37
38
  return [{ type: 'text', text }];
38
39
  }
40
+ function executionSignal(exec) {
41
+ const signal = exec?.signal;
42
+ return signal instanceof AbortSignal ? signal : undefined;
43
+ }
39
44
  function renderNotify(value) {
40
45
  if (value.msgtype === 'markdown') {
41
46
  return oneText('已通过钉钉机器人发送 Markdown 消息(标题「' + value.title + '」),钉钉返回 errcode=' + value.errcode + ',errmsg=' + value.errmsg);
@@ -43,6 +48,7 @@ function renderNotify(value) {
43
48
  return oneText('已通过钉钉机器人发送文本消息,钉钉返回 errcode=' + value.errcode + ',errmsg=' + value.errmsg);
44
49
  }
45
50
  export function apply(ctx, config = {}) {
51
+ registerUpdates(ctx, { name: "dsh-dingtalk" });
46
52
  let client = null;
47
53
  let fingerprint = '';
48
54
  const getClient = () => {
@@ -72,13 +78,14 @@ export function apply(ctx, config = {}) {
72
78
  schema: notifySchema,
73
79
  render: (_args, value) => renderNotify(value),
74
80
  },
75
- async execute(rawArgs) {
81
+ timeoutMs: 65000,
82
+ async execute(rawArgs, exec) {
76
83
  const args = rawArgs;
77
84
  if (typeof args.title !== 'string' || args.title.trim() === '')
78
85
  throw new Error('title 不能为空');
79
86
  if (typeof args.text !== 'string' || args.text.trim() === '')
80
87
  throw new Error('text 不能为空');
81
- const result = await getClient().sendMarkdown(args.title, args.text);
88
+ const result = await getClient().sendMarkdown(args.title, args.text, executionSignal(exec));
82
89
  return { ok: true, msgtype: 'markdown', title: args.title, errcode: result.errcode, errmsg: result.errmsg };
83
90
  },
84
91
  });
@@ -92,11 +99,12 @@ export function apply(ctx, config = {}) {
92
99
  schema: notifySchema,
93
100
  render: (_args, value) => renderNotify(value),
94
101
  },
95
- async execute(rawArgs) {
102
+ timeoutMs: 65000,
103
+ async execute(rawArgs, exec) {
96
104
  const args = rawArgs;
97
105
  if (typeof args.content !== 'string' || args.content.trim() === '')
98
106
  throw new Error('content 不能为空');
99
- const result = await getClient().sendText(args.content);
107
+ const result = await getClient().sendText(args.content, executionSignal(exec));
100
108
  return { ok: true, msgtype: 'text', title: '', errcode: result.errcode, errmsg: result.errmsg };
101
109
  },
102
110
  });