dsh-slack 0.1.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/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # dsh-slack
2
+
3
+ DSH(DeepSeek Harness)社区插件:让 agent 向 Slack 发通知/桥接消息。
4
+
5
+ > **v0.1 范围(单向)**:本版本只做「agent → Slack」的单向通知(发消息、列频道)。
6
+ > 双向对话(Slack 消息实时流入 agent)、Socket Mode、RTM、交互组件都不在 v0.1,见下方
7
+ > [已知限制与路线图](#已知限制与路线图)。
8
+
9
+ ## 功能
10
+
11
+ - `slack_notify`:向指定频道(或线程)发送一条 Markdown 文本消息,返回消息 `ts` 供后续引用。
12
+ - `slack_channels`:列出机器人当前可见的频道(`conversations.list`)。
13
+ - 配置走 `cordis.patch.yml`,令牌支持环境变量回退 `DSH_SLACK_TOKEN`。
14
+
15
+ ## 安装
16
+
17
+ 插件运行在宿主进程内,通过 `dsh plugin` 安装进 profile,重启后生效:
18
+
19
+ ```sh
20
+ dsh plugin --profile web add dsh-slack
21
+ ```
22
+
23
+ 安装后重启你的 dsh Web 服务,`slack_notify` / `slack_channels` 两个工具即对模型可见。
24
+
25
+ ## 配置
26
+
27
+ 配置在 profile 的 `cordis.patch.yml` 里按 `id: slack` 覆盖本插件的行(覆盖会整体替换该行的
28
+ `config`,不会合并)。可用配置项:
29
+
30
+ | 键 | 类型 | 必填 | 说明 |
31
+ | --- | --- | --- | --- |
32
+ | `token` | string | 是* | Slack 令牌:机器人令牌(`xoxb-`)或用户令牌(`xoxp-`)。留空时回退到环境变量 `DSH_SLACK_TOKEN`。 |
33
+ | `defaultChannel` | string | 否 | 默认频道(如 `#general`)。模型未特别指定频道时的默认目标,写进 `channel` 参数说明里。 |
34
+
35
+ `*` `token` 在「配置层」可留空,此时回退环境变量;两者都为空时插件照常加载,但调用工具时会
36
+ 返回中文报错(懒加载,见[错误处理](#错误处理))。
37
+
38
+ **方式一:环境变量(推荐,不写死令牌)**
39
+
40
+ ```sh
41
+ # 在启动 dsh 的进程里设置
42
+ export DSH_SLACK_TOKEN=xoxb-你的机器人令牌
43
+ ```
44
+
45
+ **方式二:profile 的 cordis.patch.yml 直接覆盖**
46
+
47
+ 在你的 profile 目录(`$DSH_HOME/profiles/web/cordis.patch.yml`)追加:
48
+
49
+ ```yaml
50
+ # 覆盖 dsh-slack 的 slack 行配置(整体替换)
51
+ - id: slack
52
+ config:
53
+ token: 'xoxb-你的机器人令牌'
54
+ defaultChannel: '#general'
55
+ ```
56
+
57
+ > 优先级:`config.token` > 环境变量 `DSH_SLACK_TOKEN`。
58
+
59
+ ### 创建 Slack App 并拿令牌
60
+
61
+ 1. 打开 <https://api.slack.com/apps>,点 **Create New App**(选 `From scratch`,给 App 起名、选工作区)。
62
+ 2. 进入 **OAuth & Permissions** 页,在 **Scopes → Bot Token Scopes** 下勾选:
63
+ - `chat:write`(发消息必需)
64
+ - `channels:read`(列频道必需)
65
+ 3. 回到顶部点 **Install to Workspace**(授权)。
66
+ 4. 拿到 **Bot User OAuth Token**(以 `xoxb-` 开头)。
67
+ 5. 把机器人(App)加进它要发消息的频道:在频道里 `/invite @你的机器人`(私有频道必需)。
68
+
69
+ ## 工具清单
70
+
71
+ ### `slack_notify`
72
+
73
+ 向指定频道/线程发送 Markdown 文本(底层 `chat.postMessage`)。
74
+
75
+ | 参数 | 类型 | 必填 | 说明 |
76
+ | --- | --- | --- | --- |
77
+ | `channel` | string | 是 | 频道名(如 `#general`)或频道 ID。 |
78
+ | `text` | string | 是 | 要发送的 Markdown 文本。 |
79
+ | `thread_ts` | string | 否 | 回复某条消息所在线程的 `ts`(即 `slack_notify` 返回的 `ts`)。 |
80
+
81
+ 返回:`{ "ts": "...", "channel": "#general" }`(`ts` 供后续 `thread_ts` 引用)。
82
+
83
+ ### `slack_channels`
84
+
85
+ 列出机器人可见频道(底层 `conversations.list`)。
86
+
87
+ 无参数。返回:`{ "channels": [{ "id": "...", "name": "..." }, ...] }`。
88
+
89
+ ## 错误处理
90
+
91
+ 所有错误信息均为中文,模型与用户都能直接读懂:
92
+
93
+ - 未配置令牌:`token 未配置:缺少 Slack 机器人令牌(xoxb-)…请在 profile 的 cordis.patch.yml 覆盖 slack 行的 config.token 并重启,或设置环境变量 DSH_SLACK_TOKEN。`
94
+ - `invalid_auth`:提示检查/重新生成 token。
95
+ - `channel_not_found`:提示频道名或频道 ID 错误。
96
+ - `not_in_channel`:提示先把机器人 App 邀请进频道。
97
+ - `token_revoked` / `account_inactive` / `missing_scope`:提示权限或令牌失效,需重新安装 App。
98
+
99
+ ## 开发与测试
100
+
101
+ ```sh
102
+ pnpm install # 安装依赖并触发 prepare(tsc 构建)
103
+ pnpm build # tsc 编译 src → lib
104
+ pnpm test # 先 build,再用 node:test 跑 test/*.test.mjs
105
+ ```
106
+
107
+ 测试无需真实 token:参数编译、配置解析(含 env 回退)、工具注册(2 个工具 + 缺配置中文报错)、
108
+ 注入 fake client 断言 `postMessage` 参数、输出 schema 纯 JSON 校验。
109
+
110
+ ## 已知限制与路线图
111
+
112
+ - **v0.1 是单向通知(agent→Slack)**:不能从 Slack 收消息反向驱动 agent。
113
+ - **不支持** Socket Mode 实时接收、RTM、交互组件(按钮/弹窗/slash command 回复)。
114
+ - `channel` 为必填参数,`defaultChannel` 目前只作为说明提示写入 `channel` 参数描述,不替代
115
+ 必填的 `channel`。
116
+ - 令牌缺失时插件仍会加载(懒加载),错误在工具被调用时才抛出。
117
+
118
+ 路线图:v0.2 计划引入 Socket Mode(`@slack/socket-mode`)实现「Slack 消息 → agent」双向桥接与
119
+ 交互组件。
120
+
121
+ ## 依赖
122
+
123
+ - 运行时:`@slack/web-api`(官方 WebClient)
124
+ - peer(由宿主提供,插件运行时不直接 import):`@deepseek-ai/cordis`、`@deepseek-ai/dsh-tools`
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,11 @@
1
+ # dsh-slack 的 bundle patch:向 profile 根插入 slack 插件行。
2
+ # 用户可在 profile 的 cordis.patch.yml 里按 id 覆盖本行 config(整体替换)。
3
+ - insert:
4
+ - id: slack
5
+ name: dsh-slack
6
+ config:
7
+ # Slack 令牌:机器人令牌(xoxb-)或用户令牌(xoxp-)。
8
+ # 留空时回退到环境变量 DSH_SLACK_TOKEN。
9
+ token: ''
10
+ # 默认频道(可选):模型未特别指定频道时的默认目标(如 #general)。
11
+ defaultChannel: ''
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 工具参数 DSL → 已编译 JSON Schema。
3
+ *
4
+ * 关键约定(坑 1/2):注册给宿主的 parameters 必须是【编译好的 JSON Schema 对象】
5
+ * ({ type: 'object', properties: {...}, required?: [...] }),绝不能把原始 DSL
6
+ * 直接塞进去,否则标准模式请求会被 DeepSeek API 拒绝。
7
+ */
8
+ /** 单个参数的作者友好 DSL。 */
9
+ export interface ParameterDsl {
10
+ type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
11
+ required?: boolean;
12
+ description?: string;
13
+ enum?: readonly (string | number | boolean)[];
14
+ items?: ParameterDsl;
15
+ properties?: Record<string, ParameterDsl>;
16
+ }
17
+ /** 参数 DSL 表:键为参数名,值为参数描述。 */
18
+ export type ParameterDslMap = Record<string, ParameterDsl>;
19
+ /** 编译后的 JSON Schema 节点(lossless JSON)。 */
20
+ export interface JsonSchemaNode {
21
+ type?: string;
22
+ properties?: Record<string, JsonSchemaNode>;
23
+ required?: string[];
24
+ description?: string;
25
+ enum?: (string | number | boolean)[];
26
+ items?: JsonSchemaNode;
27
+ additionalProperties?: boolean;
28
+ }
29
+ /**
30
+ * 把参数 DSL 编译为【已编译好的 JSON Schema 对象】。
31
+ * 结果形如 { type: 'object', properties: {...}, required?: [...] },
32
+ * 可直接作为 ToolDefinition.parameters 注册。
33
+ */
34
+ export declare function compileParameters(dsl: ParameterDslMap): JsonSchemaNode;
package/lib/compile.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 工具参数 DSL → 已编译 JSON Schema。
3
+ *
4
+ * 关键约定(坑 1/2):注册给宿主的 parameters 必须是【编译好的 JSON Schema 对象】
5
+ * ({ type: 'object', properties: {...}, required?: [...] }),绝不能把原始 DSL
6
+ * 直接塞进去,否则标准模式请求会被 DeepSeek API 拒绝。
7
+ */
8
+ /** 把单个属性 DSL 编译成 JSON Schema 节点。 */
9
+ function compileProperty(spec, path) {
10
+ const type = spec.type;
11
+ if (type === undefined) {
12
+ throw new Error(`[dsh-slack] 参数 ${path} 缺少 type`);
13
+ }
14
+ const node = { type };
15
+ if (spec.description !== undefined)
16
+ node.description = spec.description;
17
+ if (spec.enum !== undefined)
18
+ node.enum = [...spec.enum];
19
+ if (type === 'array') {
20
+ if (spec.items !== undefined)
21
+ node.items = compileProperty(spec.items, `${path}[]`);
22
+ }
23
+ else if (type === 'object') {
24
+ node.additionalProperties = true;
25
+ if (spec.properties !== undefined) {
26
+ const properties = {};
27
+ const required = [];
28
+ for (const [key, value] of Object.entries(spec.properties)) {
29
+ properties[key] = compileProperty(value, `${path}.${key}`);
30
+ if (value.required === true)
31
+ required.push(key);
32
+ }
33
+ node.properties = properties;
34
+ if (required.length > 0)
35
+ node.required = required;
36
+ }
37
+ }
38
+ return node;
39
+ }
40
+ /**
41
+ * 把参数 DSL 编译为【已编译好的 JSON Schema 对象】。
42
+ * 结果形如 { type: 'object', properties: {...}, required?: [...] },
43
+ * 可直接作为 ToolDefinition.parameters 注册。
44
+ */
45
+ export function compileParameters(dsl) {
46
+ const properties = {};
47
+ const required = [];
48
+ for (const [key, spec] of Object.entries(dsl)) {
49
+ properties[key] = compileProperty(spec, key);
50
+ if (spec.required === true)
51
+ required.push(key);
52
+ }
53
+ const schema = { type: 'object', properties };
54
+ if (required.length > 0)
55
+ schema.required = required;
56
+ return schema;
57
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Slack 插件配置解析(懒加载、全中文错误)。
3
+ *
4
+ * 坑 5:配置缺失时插件【不失败】——apply 里 try/catch,失败只 console.warn;
5
+ * 每个工具的 execute 再解析,缺失时抛出带中文指引的错误。
6
+ */
7
+ /** Slack 插件配置。 */
8
+ export interface SlackConfig {
9
+ token: string;
10
+ defaultChannel: string;
11
+ }
12
+ /** token 的环境变量名。 */
13
+ export declare const ENV_TOKEN = "DSH_SLACK_TOKEN";
14
+ /**
15
+ * 解析原始配置。永不抛错:非法形状返回 undefined,让插件懒加载不失败。
16
+ */
17
+ export declare function parseConfig(raw: unknown): SlackConfig | undefined;
18
+ /**
19
+ * 解析 token:config.token 优先,其次环境变量 DSH_SLACK_TOKEN。
20
+ */
21
+ export declare function resolveToken(config: SlackConfig | undefined): string;
22
+ /**
23
+ * 解析默认频道。
24
+ */
25
+ export declare function resolveDefaultChannel(config: SlackConfig | undefined): string;
26
+ /**
27
+ * 要求 token:缺失时抛出带中文指引的错误(供 execute 使用)。
28
+ */
29
+ export declare function requireToken(config: SlackConfig | undefined): string;
package/lib/config.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Slack 插件配置解析(懒加载、全中文错误)。
3
+ *
4
+ * 坑 5:配置缺失时插件【不失败】——apply 里 try/catch,失败只 console.warn;
5
+ * 每个工具的 execute 再解析,缺失时抛出带中文指引的错误。
6
+ */
7
+ /** token 的环境变量名。 */
8
+ export const ENV_TOKEN = 'DSH_SLACK_TOKEN';
9
+ /**
10
+ * 解析原始配置。永不抛错:非法形状返回 undefined,让插件懒加载不失败。
11
+ */
12
+ export function parseConfig(raw) {
13
+ if (raw === undefined || raw === null)
14
+ return { token: '', defaultChannel: '' };
15
+ if (typeof raw !== 'object' || Array.isArray(raw))
16
+ return undefined;
17
+ const obj = raw;
18
+ const token = typeof obj.token === 'string' ? obj.token.trim() : '';
19
+ const defaultChannel = typeof obj.defaultChannel === 'string' ? obj.defaultChannel.trim() : '';
20
+ return { token, defaultChannel };
21
+ }
22
+ /**
23
+ * 解析 token:config.token 优先,其次环境变量 DSH_SLACK_TOKEN。
24
+ */
25
+ export function resolveToken(config) {
26
+ const fromConfig = config?.token.trim() ?? '';
27
+ if (fromConfig)
28
+ return fromConfig;
29
+ return process.env[ENV_TOKEN]?.trim() ?? '';
30
+ }
31
+ /**
32
+ * 解析默认频道。
33
+ */
34
+ export function resolveDefaultChannel(config) {
35
+ return config?.defaultChannel ?? '';
36
+ }
37
+ /**
38
+ * 要求 token:缺失时抛出带中文指引的错误(供 execute 使用)。
39
+ */
40
+ export function requireToken(config) {
41
+ const token = resolveToken(config);
42
+ if (!token) {
43
+ throw new Error('token 未配置:缺少 Slack 机器人令牌(xoxb-)或用户令牌(xoxp-)。'
44
+ + '请在 profile 的 cordis.patch.yml 覆盖 slack 行的 config.token 并重启,或设置环境变量 DSH_SLACK_TOKEN。');
45
+ }
46
+ return token;
47
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * dsh-slack:DSH 社区 Slack 通知/桥接插件。
3
+ * v0.1 只做单向通知(agent→Slack),无 Socket Mode / RTM / 交互组件。
4
+ * @module dsh-slack
5
+ */
6
+ import type { SlackContext } from './types.js';
7
+ export declare const name = "dsh-slack";
8
+ export declare const inject: string[];
9
+ export { compileParameters, type ParameterDsl, type ParameterDslMap, type JsonSchemaNode } from './compile.js';
10
+ export { parseConfig, resolveToken, resolveDefaultChannel, requireToken, ENV_TOKEN, type SlackConfig, } from './config.js';
11
+ export { createWebSlackClient, mapSlackError, assertChannel, assertText, type SlackClient, type PostMessageParams, type ChannelInfo, } from './slack-client.js';
12
+ export { buildNotifyTool, buildChannelsTool, type ToolDeps } from './tools.js';
13
+ export type { SlackContext, ToolDefinition, ToolOutputDefinition, ContentBlock } from './types.js';
14
+ /**
15
+ * 插件入口。配置缺失【不失败】(懒加载):只 console.warn;
16
+ * token 缺失在每个工具的 execute 时抛出带中文指引的错误。
17
+ */
18
+ export declare function apply(ctx: SlackContext, rawConfig: unknown): void;
package/lib/index.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * dsh-slack:DSH 社区 Slack 通知/桥接插件。
3
+ * v0.1 只做单向通知(agent→Slack),无 Socket Mode / RTM / 交互组件。
4
+ * @module dsh-slack
5
+ */
6
+ import { parseConfig, requireToken } from './config.js';
7
+ import { createWebSlackClient } from './slack-client.js';
8
+ import { buildChannelsTool, buildNotifyTool } from './tools.js';
9
+ export const name = 'dsh-slack';
10
+ export const inject = ['tools'];
11
+ export { compileParameters } from './compile.js';
12
+ export { parseConfig, resolveToken, resolveDefaultChannel, requireToken, ENV_TOKEN, } from './config.js';
13
+ export { createWebSlackClient, mapSlackError, assertChannel, assertText, } from './slack-client.js';
14
+ export { buildNotifyTool, buildChannelsTool } from './tools.js';
15
+ /**
16
+ * 插件入口。配置缺失【不失败】(懒加载):只 console.warn;
17
+ * token 缺失在每个工具的 execute 时抛出带中文指引的错误。
18
+ */
19
+ export function apply(ctx, rawConfig) {
20
+ let config;
21
+ try {
22
+ config = parseConfig(rawConfig);
23
+ }
24
+ catch (error) {
25
+ console.warn('[dsh-slack] 配置解析失败,工具将在调用时报错:', error);
26
+ config = undefined;
27
+ }
28
+ if (config === undefined) {
29
+ console.warn('[dsh-slack] 配置缺失或格式错误:slack_notify / slack_channels 已注册,但调用时会报错。请在 profile 的 cordis.patch.yml 配置 slack 行并重启。');
30
+ }
31
+ else {
32
+ const token = config.token.trim() || process.env.DSH_SLACK_TOKEN?.trim();
33
+ if (!token) {
34
+ console.warn('[dsh-slack] 未检测到 Slack 令牌(config.token 与 DSH_SLACK_TOKEN 均为空):slack_notify / slack_channels 调用时会报错。');
35
+ }
36
+ }
37
+ const configProvider = () => config;
38
+ const clientProvider = () => {
39
+ // 每次调用时重新解析配置(config.token 优先,环境变量回退),并懒创建客户端。
40
+ const current = parseConfig(rawConfig);
41
+ const token = requireToken(current);
42
+ return createWebSlackClient(token);
43
+ };
44
+ const deps = { clientProvider, configProvider };
45
+ ctx.tools.register(buildNotifyTool(deps));
46
+ ctx.tools.register(buildChannelsTool(deps));
47
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Slack 客户端抽象:生产用官方 @slack/web-api,测试注入 fake。
3
+ * 同时把 Slack 错误码映射为全中文提示,并抽出频道/文本校验纯函数。
4
+ */
5
+ /** chat.postMessage 参数。 */
6
+ export interface PostMessageParams {
7
+ channel: string;
8
+ text: string;
9
+ thread_ts?: string;
10
+ }
11
+ /** conversations.list 可见频道的最小信息。 */
12
+ export interface ChannelInfo {
13
+ id: string;
14
+ name: string;
15
+ }
16
+ /** 可注入的 Slack 客户端抽象。 */
17
+ export interface SlackClient {
18
+ /** 发消息,返回消息 ts(用于线程后续引用)。 */
19
+ postMessage(params: PostMessageParams): Promise<{
20
+ ts: string;
21
+ }>;
22
+ /** 列出机器人可见频道。 */
23
+ listChannels(): Promise<ChannelInfo[]>;
24
+ }
25
+ /** 用官方 WebClient 实现。 */
26
+ export declare function createWebSlackClient(token: string): SlackClient;
27
+ /**
28
+ * 把 Slack API 错误映射为全中文提示。
29
+ */
30
+ export declare function mapSlackError(error: unknown): string;
31
+ /** 校验 channel 参数:必须是非空字符串(频道名或频道 ID)。 */
32
+ export declare function assertChannel(channel: unknown): string;
33
+ /** 校验 text 参数:必须是非空字符串。 */
34
+ export declare function assertText(text: unknown): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Slack 客户端抽象:生产用官方 @slack/web-api,测试注入 fake。
3
+ * 同时把 Slack 错误码映射为全中文提示,并抽出频道/文本校验纯函数。
4
+ */
5
+ import { WebClient } from '@slack/web-api';
6
+ /** 用官方 WebClient 实现。 */
7
+ export function createWebSlackClient(token) {
8
+ const client = new WebClient(token);
9
+ return {
10
+ async postMessage(params) {
11
+ const result = await client.chat.postMessage({
12
+ channel: params.channel,
13
+ text: params.text,
14
+ ...(params.thread_ts !== undefined ? { thread_ts: params.thread_ts } : {}),
15
+ });
16
+ const ts = typeof result.ts === 'string' ? result.ts : '';
17
+ return { ts };
18
+ },
19
+ async listChannels() {
20
+ const result = await client.conversations.list({ types: 'public_channel,private_channel' });
21
+ const channels = [];
22
+ for (const ch of result.channels ?? []) {
23
+ if (typeof ch.id === 'string' && typeof ch.name === 'string') {
24
+ channels.push({ id: ch.id, name: ch.name });
25
+ }
26
+ }
27
+ return channels;
28
+ },
29
+ };
30
+ }
31
+ /**
32
+ * 把 Slack API 错误映射为全中文提示。
33
+ */
34
+ export function mapSlackError(error) {
35
+ const data = error?.data;
36
+ const code = data?.error;
37
+ if (code === 'invalid_auth') {
38
+ return 'Slack 认证失败(invalid_auth):token 无效、已撤销或过期。请检查并重新生成令牌,再在 profile 的 cordis.patch.yml 覆盖 slack 行的 config.token 并重启。';
39
+ }
40
+ if (code === 'channel_not_found') {
41
+ return '频道未找到(channel_not_found):请检查频道名或频道 ID 是否正确(如 #general)。';
42
+ }
43
+ if (code === 'not_in_channel') {
44
+ return '机器人不在该频道中(not_in_channel):请先把机器人 App 添加(invite)到该频道。';
45
+ }
46
+ if (code === 'token_revoked' || code === 'account_inactive' || code === 'missing_scope') {
47
+ return `Slack 令牌或权限不足(${code}):请确认已勾选 chat:write 与 channels:read,并重新安装 App 拿新令牌。`;
48
+ }
49
+ const message = error?.message;
50
+ return message ? `Slack API 调用失败:${message}` : 'Slack API 调用失败:未知错误。';
51
+ }
52
+ /** 校验 channel 参数:必须是非空字符串(频道名或频道 ID)。 */
53
+ export function assertChannel(channel) {
54
+ if (typeof channel !== 'string' || channel.trim() === '') {
55
+ throw new Error('channel 参数错误:必须是频道名(如 #general)或频道 ID 的非空字符串。');
56
+ }
57
+ return channel.trim();
58
+ }
59
+ /** 校验 text 参数:必须是非空字符串。 */
60
+ export function assertText(text) {
61
+ if (typeof text !== 'string' || text.trim() === '') {
62
+ throw new Error('text 参数错误:必须是待发送的 Markdown 文本(非空字符串)。');
63
+ }
64
+ return text;
65
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 构建两个面向模型的工具:slack_notify、slack_channels。
3
+ * 依赖通过 ToolDeps 注入(clientProvider / configProvider),测试可注入 fake client。
4
+ */
5
+ import type { SlackConfig } from './config.js';
6
+ import type { ToolDefinition } from './types.js';
7
+ import type { SlackClient } from './slack-client.js';
8
+ /** 工具构建的依赖注入点。 */
9
+ export interface ToolDeps {
10
+ /** 返回一个 SlackClient;token 缺失时抛中文错误。 */
11
+ clientProvider: () => SlackClient;
12
+ /** 返回当前配置(供默认频道等解析)。 */
13
+ configProvider: () => SlackConfig | undefined;
14
+ }
15
+ /** 构建 slack_notify:向指定频道/线程发送 Markdown 文本,返回 ts。 */
16
+ export declare function buildNotifyTool(deps: ToolDeps): ToolDefinition;
17
+ /** 构建 slack_channels:列出机器人可见频道(conversations.list)。 */
18
+ export declare function buildChannelsTool(deps: ToolDeps): ToolDefinition;
package/lib/tools.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 构建两个面向模型的工具:slack_notify、slack_channels。
3
+ * 依赖通过 ToolDeps 注入(clientProvider / configProvider),测试可注入 fake client。
4
+ */
5
+ import { compileParameters } from './compile.js';
6
+ import { resolveDefaultChannel } from './config.js';
7
+ import { assertChannel, assertText, mapSlackError } from './slack-client.js';
8
+ /** slack_notify 的输出 schema(原始 JSON Schema,纯 JSON 无 undefined)。 */
9
+ const NOTIFY_OUTPUT_SCHEMA = {
10
+ type: 'object',
11
+ properties: {
12
+ ts: { type: 'string' },
13
+ channel: { type: 'string' },
14
+ },
15
+ required: ['ts', 'channel'],
16
+ additionalProperties: true,
17
+ };
18
+ /** slack_channels 的输出 schema。 */
19
+ const CHANNELS_OUTPUT_SCHEMA = {
20
+ type: 'object',
21
+ properties: {
22
+ channels: {
23
+ type: 'array',
24
+ items: {
25
+ type: 'object',
26
+ properties: {
27
+ id: { type: 'string' },
28
+ name: { type: 'string' },
29
+ },
30
+ required: ['id', 'name'],
31
+ additionalProperties: true,
32
+ },
33
+ },
34
+ },
35
+ required: ['channels'],
36
+ additionalProperties: true,
37
+ };
38
+ function textBlock(text) {
39
+ return { type: 'text', text };
40
+ }
41
+ /** 构建 slack_notify:向指定频道/线程发送 Markdown 文本,返回 ts。 */
42
+ export function buildNotifyTool(deps) {
43
+ const defaultChannel = resolveDefaultChannel(deps.configProvider());
44
+ const channelDescription = defaultChannel
45
+ ? `目标频道:频道名(如 #general)或频道 ID。未特别指定时可使用默认频道 ${defaultChannel}。`
46
+ : '目标频道:频道名(如 #general)或频道 ID。';
47
+ const parameters = compileParameters({
48
+ channel: { type: 'string', required: true, description: channelDescription },
49
+ text: { type: 'string', required: true, description: '要发送的 Markdown 文本内容。' },
50
+ thread_ts: { type: 'string', description: '可选:回复某条消息所在线程的 ts(即 slack_notify 返回的 ts)。' },
51
+ });
52
+ const output = {
53
+ schema: NOTIFY_OUTPUT_SCHEMA,
54
+ render: (_args, value) => {
55
+ const v = value;
56
+ return [textBlock(`已发送到 ${v.channel}(消息 ts:${v.ts})。`)];
57
+ },
58
+ };
59
+ return {
60
+ name: 'slack_notify',
61
+ description: '向指定 Slack 频道(或线程)发送一条 Markdown 文本消息,返回消息 ts 供后续引用。',
62
+ parameters,
63
+ output,
64
+ timeoutMs: 30000,
65
+ async execute(args) {
66
+ const raw = args;
67
+ const channel = assertChannel(raw.channel);
68
+ const text = assertText(raw.text);
69
+ const thread_ts = typeof raw.thread_ts === 'string' && raw.thread_ts.trim() !== ''
70
+ ? raw.thread_ts.trim()
71
+ : undefined;
72
+ const client = deps.clientProvider();
73
+ try {
74
+ const result = await client.postMessage({
75
+ channel,
76
+ text,
77
+ ...(thread_ts !== undefined ? { thread_ts } : {}),
78
+ });
79
+ return { ts: result.ts, channel };
80
+ }
81
+ catch (error) {
82
+ throw new Error(mapSlackError(error));
83
+ }
84
+ },
85
+ };
86
+ }
87
+ /** 构建 slack_channels:列出机器人可见频道(conversations.list)。 */
88
+ export function buildChannelsTool(deps) {
89
+ const parameters = compileParameters({});
90
+ const output = {
91
+ schema: CHANNELS_OUTPUT_SCHEMA,
92
+ render: (_args, value) => {
93
+ const v = value;
94
+ if (v.channels.length === 0) {
95
+ return [textBlock('机器人当前看不到任何频道(可能需要先把 App 加入频道)。')];
96
+ }
97
+ const names = v.channels.map((c) => `#${c.name}`).join('、');
98
+ return [textBlock(`机器人可见频道(${v.channels.length} 个):${names}。`)];
99
+ },
100
+ };
101
+ return {
102
+ name: 'slack_channels',
103
+ description: '列出当前 Slack 机器人可见的频道(conversations.list)。',
104
+ parameters,
105
+ output,
106
+ timeoutMs: 30000,
107
+ async execute() {
108
+ const client = deps.clientProvider();
109
+ try {
110
+ const channels = await client.listChannels();
111
+ return { channels: channels.map((c) => ({ id: c.id, name: c.name })) };
112
+ }
113
+ catch (error) {
114
+ throw new Error(mapSlackError(error));
115
+ }
116
+ },
117
+ };
118
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * dsh-slack 自包含的最小结构类型,与宿主(@deepseek-ai/dsh-tools)运行时 API 兼容。
3
+ * 插件只做结构化调用,运行时不 import 任何 @deepseek-ai/* 包。
4
+ */
5
+ /** 面向模型/UI 的文本内容块。 */
6
+ export type ContentBlock = {
7
+ type: 'text';
8
+ text: string;
9
+ };
10
+ /** 工具的输出声明:schema 必须是原始 JSON Schema,render 是纯投影。 */
11
+ export interface ToolOutputDefinition {
12
+ readonly schema: unknown;
13
+ render(args: unknown, value: unknown): ContentBlock[];
14
+ }
15
+ /** 注册给 ctx.tools.register 的工具定义(与宿主 ToolDefinition 结构兼容)。 */
16
+ export interface ToolDefinition {
17
+ name: string;
18
+ description: string;
19
+ parameters: unknown;
20
+ readonly output: ToolOutputDefinition;
21
+ execute(args: unknown, exec?: unknown): Promise<unknown>;
22
+ timeoutMs?: number;
23
+ }
24
+ /** 插件所需的最小 Context 形状(宿主传入真实的 Cordis Context)。 */
25
+ export interface SlackContext {
26
+ tools: {
27
+ register(definition: ToolDefinition): () => void;
28
+ };
29
+ }
package/lib/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * dsh-slack 自包含的最小结构类型,与宿主(@deepseek-ai/dsh-tools)运行时 API 兼容。
3
+ * 插件只做结构化调用,运行时不 import 任何 @deepseek-ai/* 包。
4
+ */
5
+ export {};
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "dsh-slack",
3
+ "version": "0.1.0",
4
+ "description": "DSH 社区插件:Slack 通知/桥接(v0.1 单向 agent→Slack,配置走 cordis.patch.yml)",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./package.json": "./package.json",
14
+ "./cordis.patch.yml": "./cordis.patch.yml"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md"
20
+ ],
21
+ "license": "MIT",
22
+ "dsh": {
23
+ "bundle": {
24
+ "patch": "./cordis.patch.yml"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "prepare": "tsc -p tsconfig.json",
30
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
31
+ "prepublishOnly": "pnpm run build"
32
+ },
33
+ "dependencies": {
34
+ "@slack/web-api": "^8.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^24.0.0",
38
+ "typescript": "^5.7.3"
39
+ },
40
+ "repository": {
41
+ "url": "https://github.com/STARDUSTLC666/dsh-slack",
42
+ "type": "git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/STARDUSTLC666/dsh-slack/issues"
46
+ },
47
+ "homepage": "https://github.com/STARDUSTLC666/dsh-slack#readme",
48
+ "author": "stardustlc",
49
+ "keywords": [
50
+ "dsh-plugin",
51
+ "deepseek-harness",
52
+ "dsh"
53
+ ]
54
+ }