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 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
  [![npm](https://img.shields.io/npm/v/dsh-slack)](https://www.npmjs.com/package/dsh-slack) [![downloads](https://raw.githubusercontent.com/STARDUSTLC666/dsh-suite/npm-downloads/assets/dsh-slack-downloads.svg)](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
  [![npm](https://img.shields.io/npm/v/dsh-slack)](https://www.npmjs.com/package/dsh-slack) [![downloads](https://raw.githubusercontent.com/STARDUSTLC666/dsh-suite/npm-downloads/assets/dsh-slack-downloads.svg)](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 时先检查频道,确认未发送后再重试,避免重复通知。
@@ -1,5 +1,9 @@
1
1
  # dsh-slack 验证记录
2
2
 
3
+ ## 2026-10-26:0.3.5
4
+
5
+ [本次修复、回归与未验收范围](validation-0.3.5.md)。
6
+
3
7
  本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
4
8
 
5
9
  ## 原中文记录
@@ -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
- const deps = { clientProvider, configProvider, inboxProvider };
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
- * 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. */
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
- * 启动 Socket Mode 客户端,返回 dispose 函数(断开连接)。
13
- * 网络错误 / 解析错误均 try/catch + warn,绝不抛出。
14
- */
15
- export declare function startSocketModeClient(appToken: string, handler: SocketMessageHandler, warn: Warn): () => void;
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
- * 启动 Socket Mode 客户端,返回 dispose 函数(断开连接)。
9
- * 网络错误 / 解析错误均 try/catch + warn,绝不抛出。
10
- */
11
- export function startSocketModeClient(appToken, handler, warn) {
12
- const client = new SocketModeClient({ appToken });
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
- warn('[dsh-slack] 解析 Socket Mode 事件失败:', error);
99
+ log('解析 Socket Mode 事件失败', error);
22
100
  }
23
101
  finally {
24
- // events_api 事件必须 ack,否则 Slack 会重投;重复投递由收件箱 seen 去重兜底。
25
- if (typeof raw.ack === 'function') {
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
- client.on('error', (...args) => {
31
- warn('[dsh-slack] Socket Mode 连接错误(SDK 会自动重连):', args[0]);
32
- });
33
- try {
34
- client.start().catch((error) => {
35
- warn('[dsh-slack] Socket Mode 启动失败:', error);
36
- });
37
- }
38
- catch (error) {
39
- warn('[dsh-slack] Socket Mode 启动异常:', error);
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: true, detail: appToken !== '' ? '已配置(Socket Mode 收件箱可用)' : '未配置(收件箱功能不可用,通知不受影响)' });
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 ok = token !== '';
91
- return { ok, plugin: 'dsh-slack', checks };
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-slack",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "连接 Slack,发送通知并处理 Socket Mode 收件箱与线程回复。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",