dsh-slack 0.3.4 → 0.3.5
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/CHANGELOG.md +5 -0
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/docs/USAGE.en.md +4 -0
- package/docs/USAGE.md +4 -0
- package/docs/VALIDATION.md +4 -0
- package/docs/validation-0.3.5.md +19 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +5 -3
- package/lib/socket.d.ts +20 -10
- package/lib/socket.js +102 -36
- package/lib/tools.d.ts +2 -0
- package/lib/tools.js +8 -4
- package/lib/types.d.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
[历史英文记录](docs/CHANGELOG.en.md)
|
|
6
6
|
|
|
7
|
+
## 0.3.5(2026-10-26)
|
|
8
|
+
|
|
9
|
+
- Socket Mode 重连改为插件管理的有界退避;捕获启动与重连错误,凭据撤销等永久错误停止重试,卸载取消重连。自检区分凭据已配置、连接中、连接成功与认证失败。
|
|
10
|
+
- Windows 回归 49 项通过、0 项跳过;宿主与未验收范围见[本次记录](docs/validation-0.3.5.md)。
|
|
11
|
+
|
|
7
12
|
## 0.3.4 (2026-10-05)
|
|
8
13
|
|
|
9
14
|
- 标记已读只移除本次展示的消息,保留剩余未读与已读重投去重;发送不隐式重试,缺少消息回执时提示先检查频道。
|
package/README.en.md
CHANGED
|
@@ -8,6 +8,8 @@ Connect Slack for notifications, a Socket Mode inbox and thread replies.
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-slack) [](https://www.npmjs.com/package/dsh-slack)
|
|
10
10
|
|
|
11
|
+
Feedback and contributions are welcome: report [issues](https://github.com/STARDUSTLC666/dsh-slack/issues) or submit [pull requests](https://github.com/STARDUSTLC666/dsh-slack/pulls).
|
|
12
|
+
|
|
11
13
|
## What it does
|
|
12
14
|
|
|
13
15
|
- Send Markdown notifications to channels or threads.
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-slack) [](https://www.npmjs.com/package/dsh-slack)
|
|
10
10
|
|
|
11
|
+
欢迎使用,遇到问题或有改进建议,请提交 [issues](https://github.com/STARDUSTLC666/dsh-slack/issues) 和 [PR](https://github.com/STARDUSTLC666/dsh-slack/pulls)。
|
|
12
|
+
|
|
11
13
|
## 功能
|
|
12
14
|
|
|
13
15
|
- 向频道或线程发送 Markdown 通知。
|
package/docs/USAGE.en.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
|
|
4
4
|
|
|
5
|
+
## Connection diagnostics
|
|
6
|
+
|
|
7
|
+
Configured credentials do not prove an active inbox connection. slack_health reports connection state. Transient failures back off; invalid credentials and permanent authorization errors stop retries. Reload after correcting credentials. Sends are not retried implicitly.
|
|
8
|
+
|
|
5
9
|
## Current improvements
|
|
6
10
|
|
|
7
11
|
slack_inbox with limit=10 and markRead=true returns remaining; call again for unread messages. After a timeout or missing ts, inspect the channel before retrying to avoid duplicate notifications.
|
package/docs/USAGE.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
|
|
4
4
|
|
|
5
|
+
## 连接诊断
|
|
6
|
+
|
|
7
|
+
凭据已填写不等于收件箱已连接。slack_health 显示真实连接状态;临时故障退避重连,凭据失效或权限错误停止重试。修正凭据后重新加载插件。发送操作不会隐式重试。
|
|
8
|
+
|
|
5
9
|
## 本次改进
|
|
6
10
|
|
|
7
11
|
slack_inbox { limit: 10, markRead: true } 返回 remaining,继续调用可查看余下消息。遇到超时或缺少 ts 时先检查频道,确认未发送后再重试,避免重复通知。
|
package/docs/VALIDATION.md
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 0.3.5 验证记录
|
|
2
|
+
|
|
3
|
+
日期:2026-10-26。官方 Windows 宿主:DSH 0.2.1-rc.3。
|
|
4
|
+
|
|
5
|
+
## 行为变化
|
|
6
|
+
|
|
7
|
+
Socket Mode 重连改为插件管理的有界退避;捕获启动与重连错误,凭据撤销等永久错误停止重试,卸载取消重连。自检区分凭据已配置、连接中、连接成功与认证失败。
|
|
8
|
+
|
|
9
|
+
## 验证与限制
|
|
10
|
+
|
|
11
|
+
- Windows 本地:49 项,49 通过、0 跳过;没有把跳过计为通过。
|
|
12
|
+
- 真实 Slack SDK 的 token_revoked 重连故障在严格未处理异常模式下复现并回归;官方 RC.3 检查未配置状态。未配置真实 Slack 账号,线上收发未验收。
|
|
13
|
+
- GitHub 的跨平台 CI 和 npm 发布完整性以该版本工作流及 registry 为准;本记录不把本地测试等同于已发布。
|
|
14
|
+
|
|
15
|
+
## English
|
|
16
|
+
|
|
17
|
+
Socket Mode uses caught, bounded reconnect backoff. Permanent authentication errors stop retries; disposal cancels reconnects. Health distinguishes configured credentials from an active connection.
|
|
18
|
+
|
|
19
|
+
Runtime/account limits are listed above. Synthetic peers and model adapters do not establish live account delivery or model quality.
|
package/lib/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { parseConfig, resolveToken, resolveDefaultChannel, requireToken, resolve
|
|
|
11
11
|
export { createWebSlackClient, mapSlackError, assertChannel, assertText, assertThreadTs, type SlackClient, type PostMessageParams, type ChannelInfo, } from './slack-client.js';
|
|
12
12
|
export { buildNotifyTool, buildChannelsTool, buildInboxTool, buildReplyTool, type ToolDeps } from './tools.js';
|
|
13
13
|
export { parseMessageEvent, InboxQueue, createInboxQueue, INBOX_CAPACITY, type InboxMessage } from './inbox.js';
|
|
14
|
-
export { startSocketModeClient, type SocketMessageHandler } from './socket.js';
|
|
14
|
+
export { startSocketModeClient, type SocketMessageHandler, type SocketStatus, type SocketDisposer, type SocketLifecycleOptions } from './socket.js';
|
|
15
15
|
export type { SlackContext, ToolDefinition, ToolOutputDefinition, ContentBlock } from './types.js';
|
|
16
16
|
/**
|
|
17
17
|
* 插件入口。配置缺失【不失败】(懒加载):只 console.warn;
|
package/lib/index.js
CHANGED
|
@@ -71,10 +71,11 @@ export function apply(ctx, rawConfig) {
|
|
|
71
71
|
return client;
|
|
72
72
|
};
|
|
73
73
|
const inboxProvider = () => inboxQueue;
|
|
74
|
-
|
|
74
|
+
let socket;
|
|
75
|
+
const deps = { clientProvider, configProvider, inboxProvider, socketStatusProvider: () => socket?.status() };
|
|
75
76
|
const appToken = resolveAppToken(config);
|
|
76
77
|
if (appToken !== '' && appToken.startsWith('xapp-')) {
|
|
77
|
-
const startSocket = () => startSocketModeClient(appToken, {
|
|
78
|
+
const startSocket = () => socket = startSocketModeClient(appToken, {
|
|
78
79
|
onMessage(message) {
|
|
79
80
|
inboxQueue.push(message);
|
|
80
81
|
},
|
|
@@ -83,7 +84,8 @@ export function apply(ctx, rawConfig) {
|
|
|
83
84
|
ctx.effect(startSocket);
|
|
84
85
|
}
|
|
85
86
|
else {
|
|
86
|
-
startSocket();
|
|
87
|
+
const dispose = startSocket();
|
|
88
|
+
ctx.on?.('dispose', dispose);
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
ctx.tools.register(buildNotifyTool(deps));
|
package/lib/socket.d.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* 收到 message 事件写入收件箱;网络错误只 warn 不抛崩(SDK 自带自动重连)。
|
|
4
|
-
*/
|
|
1
|
+
/** Socket Mode lifecycle: SDK auto-reconnect is disabled; every attempt is owned and caught here. */
|
|
2
|
+
import { SocketModeClient } from '@slack/socket-mode';
|
|
5
3
|
import { type InboxMessage } from './inbox.js';
|
|
6
|
-
/** Socket 消息回调。 */
|
|
7
4
|
export interface SocketMessageHandler {
|
|
8
5
|
onMessage(message: InboxMessage): void;
|
|
9
6
|
}
|
|
10
7
|
type Warn = (message: string, ...args: unknown[]) => void;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
export interface SocketStatus {
|
|
9
|
+
state: 'connecting' | 'connected' | 'reconnecting' | 'auth_failed' | 'stopped';
|
|
10
|
+
attempts: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SocketClientLike {
|
|
14
|
+
on(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
15
|
+
start(): Promise<unknown>;
|
|
16
|
+
disconnect(): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface SocketLifecycleOptions {
|
|
19
|
+
retryBaseMs?: number;
|
|
20
|
+
createClient?: (options: ConstructorParameters<typeof SocketModeClient>[0]) => SocketClientLike;
|
|
21
|
+
}
|
|
22
|
+
export type SocketDisposer = (() => void) & {
|
|
23
|
+
status(): SocketStatus;
|
|
24
|
+
};
|
|
25
|
+
export declare function startSocketModeClient(appToken: string, handler: SocketMessageHandler, warn: Warn, options?: SocketLifecycleOptions): SocketDisposer;
|
|
16
26
|
export {};
|
package/lib/socket.js
CHANGED
|
@@ -1,16 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Socket Mode 启动/生命周期:用 @slack/socket-mode 建立 WebSocket,
|
|
3
|
-
* 收到 message 事件写入收件箱;网络错误只 warn 不抛崩(SDK 自带自动重连)。
|
|
4
|
-
*/
|
|
1
|
+
/** Socket Mode lifecycle: SDK auto-reconnect is disabled; every attempt is owned and caught here. */
|
|
5
2
|
import { SocketModeClient } from '@slack/socket-mode';
|
|
6
3
|
import { parseMessageEvent } from './inbox.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
const AUTH_ERRORS = new Set(['invalid_auth', 'not_authed', 'account_inactive', 'user_removed_from_team', 'team_disabled', 'token_revoked', 'token_expired', 'missing_scope', 'not_allowed_token_type']);
|
|
5
|
+
function errorCode(error) {
|
|
6
|
+
const value = error;
|
|
7
|
+
const code = value?.data?.error ?? value?.code;
|
|
8
|
+
if (typeof code === 'string' && (AUTH_ERRORS.has(code) || /^(?:E[A-Z0-9_]+|slack_webapi_[a-z_]+)$/.test(code)))
|
|
9
|
+
return code;
|
|
10
|
+
return 'connection_error';
|
|
11
|
+
}
|
|
12
|
+
export function startSocketModeClient(appToken, handler, warn, options = {}) {
|
|
13
|
+
const client = (options.createClient ?? (config => new SocketModeClient(config)))({
|
|
14
|
+
appToken, autoReconnectEnabled: false,
|
|
15
|
+
clientOptions: { timeout: 15000, retryConfig: { retries: 0 }, rejectRateLimitedCalls: true },
|
|
16
|
+
});
|
|
17
|
+
const retryBase = Number.isFinite(options.retryBaseMs) ? Math.max(1, Math.min(30000, options.retryBaseMs)) : 1000;
|
|
18
|
+
let status = { state: 'connecting', attempts: 0 };
|
|
19
|
+
let stopped = false, fatal = false, connecting = false, needsRetry = false, failures = 0;
|
|
20
|
+
let retry;
|
|
21
|
+
const log = (message, error) => {
|
|
22
|
+
try {
|
|
23
|
+
warn('[dsh-slack] ' + message + (error === undefined ? '' : '(' + errorCode(error) + ')'));
|
|
24
|
+
}
|
|
25
|
+
catch { /* Logging must not create an unhandled rejection. */ }
|
|
26
|
+
};
|
|
27
|
+
const disconnect = () => { void Promise.resolve().then(() => client.disconnect()).catch(error => log('Socket Mode 断开失败', error)); };
|
|
28
|
+
const schedule = () => {
|
|
29
|
+
if (stopped || fatal || connecting || retry !== undefined)
|
|
30
|
+
return;
|
|
31
|
+
status = { ...status, state: 'reconnecting' };
|
|
32
|
+
const delay = Math.min(30000, retryBase * 2 ** Math.min(failures, 5));
|
|
33
|
+
retry = setTimeout(() => { retry = undefined; connect(); }, delay);
|
|
34
|
+
retry.unref?.();
|
|
35
|
+
};
|
|
36
|
+
const failure = (error) => {
|
|
37
|
+
const code = errorCode(error);
|
|
38
|
+
fatal = AUTH_ERRORS.has(code);
|
|
39
|
+
status = { ...status, state: fatal ? 'auth_failed' : 'reconnecting', error: code };
|
|
40
|
+
failures++;
|
|
41
|
+
needsRetry = !fatal;
|
|
42
|
+
log(fatal ? 'Socket Mode 令牌或权限无效,已停止重连;更新 App-Level Token 后重启插件' : 'Socket Mode 连接失败,将退避重试', error);
|
|
43
|
+
if (fatal)
|
|
44
|
+
disconnect();
|
|
45
|
+
};
|
|
46
|
+
const connect = () => {
|
|
47
|
+
if (stopped || fatal || connecting)
|
|
48
|
+
return;
|
|
49
|
+
connecting = true;
|
|
50
|
+
needsRetry = false;
|
|
51
|
+
status = { ...status, state: status.attempts === 0 ? 'connecting' : 'reconnecting', attempts: status.attempts + 1 };
|
|
52
|
+
void Promise.resolve().then(() => {
|
|
53
|
+
if (!stopped)
|
|
54
|
+
return client.start();
|
|
55
|
+
}).then(() => {
|
|
56
|
+
if (stopped)
|
|
57
|
+
disconnect();
|
|
58
|
+
}).catch(error => { if (!stopped)
|
|
59
|
+
failure(error); }).finally(() => {
|
|
60
|
+
connecting = false;
|
|
61
|
+
if (needsRetry)
|
|
62
|
+
schedule();
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
client.on('connected', () => {
|
|
66
|
+
if (stopped || fatal) {
|
|
67
|
+
disconnect();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
failures = 0;
|
|
71
|
+
needsRetry = false;
|
|
72
|
+
status = { state: 'connected', attempts: status.attempts };
|
|
73
|
+
});
|
|
74
|
+
client.on('disconnected', () => {
|
|
75
|
+
if (stopped || fatal)
|
|
76
|
+
return;
|
|
77
|
+
needsRetry = true;
|
|
78
|
+
status = { ...status, state: 'reconnecting' };
|
|
79
|
+
schedule();
|
|
80
|
+
});
|
|
81
|
+
client.on('error', error => {
|
|
82
|
+
if (stopped || fatal)
|
|
83
|
+
return;
|
|
84
|
+
if (AUTH_ERRORS.has(errorCode(error)))
|
|
85
|
+
failure(error);
|
|
86
|
+
else
|
|
87
|
+
log('Socket Mode 连接错误', error);
|
|
88
|
+
});
|
|
13
89
|
client.on('slack_event', (...args) => {
|
|
90
|
+
if (stopped || fatal)
|
|
91
|
+
return;
|
|
14
92
|
const raw = args[0];
|
|
15
93
|
try {
|
|
16
94
|
const message = parseMessageEvent(raw);
|
|
@@ -18,34 +96,22 @@ export function startSocketModeClient(appToken, handler, warn) {
|
|
|
18
96
|
handler.onMessage(message);
|
|
19
97
|
}
|
|
20
98
|
catch (error) {
|
|
21
|
-
|
|
99
|
+
log('解析 Socket Mode 事件失败', error);
|
|
22
100
|
}
|
|
23
101
|
finally {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
void raw.ack().catch(() => { });
|
|
27
|
-
}
|
|
102
|
+
if (typeof raw?.ack === 'function')
|
|
103
|
+
void Promise.resolve().then(() => raw.ack()).catch(() => { });
|
|
28
104
|
}
|
|
29
105
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return () => {
|
|
42
|
-
try {
|
|
43
|
-
client.disconnect().catch((error) => {
|
|
44
|
-
warn('[dsh-slack] Socket Mode 断开失败:', error);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
warn('[dsh-slack] Socket Mode 断开异常:', error);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
106
|
+
connect();
|
|
107
|
+
return Object.assign(() => {
|
|
108
|
+
if (stopped)
|
|
109
|
+
return;
|
|
110
|
+
stopped = true;
|
|
111
|
+
if (retry !== undefined)
|
|
112
|
+
clearTimeout(retry);
|
|
113
|
+
retry = undefined;
|
|
114
|
+
status = { ...status, state: 'stopped' };
|
|
115
|
+
disconnect();
|
|
116
|
+
}, { status: () => ({ ...status }) });
|
|
51
117
|
}
|
package/lib/tools.d.ts
CHANGED
|
@@ -2,11 +2,13 @@ import type { SlackConfig } from './config.js';
|
|
|
2
2
|
import type { ToolDefinition } from './types.js';
|
|
3
3
|
import type { SlackClient } from './slack-client.js';
|
|
4
4
|
import type { InboxQueue } from './inbox.js';
|
|
5
|
+
import type { SocketStatus } from './socket.js';
|
|
5
6
|
/** 工具构建的依赖注入点。 */
|
|
6
7
|
export interface ToolDeps {
|
|
7
8
|
clientProvider: () => SlackClient;
|
|
8
9
|
configProvider: () => SlackConfig | undefined;
|
|
9
10
|
inboxProvider: () => InboxQueue;
|
|
11
|
+
socketStatusProvider?: () => SocketStatus | undefined;
|
|
10
12
|
}
|
|
11
13
|
/** 自检工具:检查 token / appToken / 默认频道配置,不发起网络请求。 */
|
|
12
14
|
export declare function buildHealthTool(deps: ToolDeps): ToolDefinition;
|
package/lib/tools.js
CHANGED
|
@@ -63,7 +63,7 @@ function clampLimit(value) {
|
|
|
63
63
|
export function buildHealthTool(deps) {
|
|
64
64
|
return {
|
|
65
65
|
name: 'slack_health',
|
|
66
|
-
description: 'dsh-slack 自检:检查 botToken / appToken /
|
|
66
|
+
description: 'dsh-slack 自检:检查 botToken / appToken / 默认频道配置和当前 Socket Mode 连接状态(不额外发起网络请求)。遇到问题时先运行本工具定位。',
|
|
67
67
|
parameters: compileParameters({}),
|
|
68
68
|
output: {
|
|
69
69
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -84,11 +84,15 @@ export function buildHealthTool(deps) {
|
|
|
84
84
|
const token = resolveToken(config);
|
|
85
85
|
checks.push({ name: 'botToken', ok: token !== '', detail: token !== '' ? '已配置' : '未配置:请填 token 或环境变量(发送/频道工具需要)' });
|
|
86
86
|
const appToken = resolveAppToken(config);
|
|
87
|
-
checks.push({ name: 'appToken', ok:
|
|
87
|
+
checks.push({ name: 'appToken', ok: appToken === '' || appToken.startsWith('xapp-'), detail: appToken !== '' ? '已配置(Socket Mode 连接状态见下项)' : '未配置(收件箱功能不可用,通知不受影响)' });
|
|
88
88
|
const defaultChannel = resolveDefaultChannel(config);
|
|
89
89
|
checks.push({ name: '默认频道', ok: true, detail: defaultChannel !== '' ? defaultChannel : '未配置(发送时必须显式给 channel)' });
|
|
90
|
-
const
|
|
91
|
-
|
|
90
|
+
const socket = deps.socketStatusProvider?.();
|
|
91
|
+
const states = { connecting: '正在连接', connected: '已连接', reconnecting: '已断开,退避重连中', auth_failed: '令牌或权限无效,已停止重连;更新 App-Level Token 后重启插件', stopped: '已停止' };
|
|
92
|
+
if (appToken !== '')
|
|
93
|
+
checks.push({ name: 'Socket Mode', ok: socket?.state === 'connected', detail: socket ? states[socket.state] + (socket.error ? '(' + socket.error + ')' : '') : '仅检查配置,尚未确认连接' });
|
|
94
|
+
const ok = token !== '' && checks.every(check => check.ok === true);
|
|
95
|
+
return { ok, plugin: 'dsh-slack', checks, ...(socket ? { socket } : {}) };
|
|
92
96
|
},
|
|
93
97
|
};
|
|
94
98
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface SlackContext {
|
|
|
28
28
|
};
|
|
29
29
|
/** Cordis 生命周期:传入启动函数,其返回的清理函数在 dispose 时执行。 */
|
|
30
30
|
effect?: (start: () => void | (() => void)) => void;
|
|
31
|
+
on?: (event: 'dispose', listener: () => void) => unknown;
|
|
31
32
|
/** 可选日志器;缺省回退 console.warn。 */
|
|
32
33
|
logger?: {
|
|
33
34
|
warn(message: string, ...args: unknown[]): void;
|