@zhin.js/adapter-slack 5.0.6 → 6.0.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/CHANGELOG.md +25 -0
- package/adapters/slack.js +5 -4
- package/adapters/slack.ts +5 -4
- package/lib/protocol.js +54 -20
- package/lib/slack-endpoint-commands.js +1 -1
- package/lib/slack-runtime-state.js +1 -1
- package/package.json +10 -10
- package/plugin.js +1 -1
- package/src/protocol.ts +54 -20
- package/src/slack-endpoint-commands.ts +1 -1
- package/src/slack-runtime-state.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4fbff5d: feat!: 多模态双向 Segment 一贯制(BREAKING,无兼容层)
|
|
8
|
+
|
|
9
|
+
全框架唯一媒体表达统一为 canonical `Segment` + `MediaRef{kind: url|path|base64|file, value, mime_type?, file_name?, size?}`,新增 audio/video/file 段类型;所有第二形状(legacy `data.url/file/base64` 字段、`mediaRefFromLegacyData`/`mediaRefToLegacyFields` 桥、双写)全部删除。
|
|
10
|
+
|
|
11
|
+
- **core**:`SendContent` 一等支持 `Segment[]`;endpoint 出站载荷只含 canonical 段;`resolveOutboundMediaPolicy` 改为纯声明驱动(adapter definition `segments.outboundMedia`),内置策略表删除,未声明回退 `url-or-text`;`ImageContent` 旧桥删除。
|
|
12
|
+
- **ai**:新增 `MediaContentBlock`/`MediaBlockRef`(Segment 同构)与 `UserMessage.media`(当前 turn 媒体,**不持久化**——存储层自动剥离);`createUserMessage(text, media?)` 签名变更(`ImageContent` 删除);provider 边界序列化器 `filterMediaBlocksForProvider` + 能力表(缺省 image-only,不支持类型降级占位文本);ai-sdk 桥媒体块 → SDK image/file parts。
|
|
13
|
+
- **agent**:入站 turn 注入(`turn/inbound-media.ts`)——commMessage 媒体段 → 当前 turn `UserMessage.media`;图片 path 物化、音频默认 STT(`@zhin.js/speech` 可选,失败降级占位)、视频/文件占位;`publishOutboundElements` 产出 canonical Segment;`transcribeAudioPayload` 导出。
|
|
14
|
+
- **cli**:`bridgeRuntimeMessage` 回复链路媒体段透传,不再压平为文本(`$reply` 直达 normalize → adapter)。
|
|
15
|
+
- **全部 20 个平台适配器**:出站媒体只消费 `data.media`(url 直发 / base64 直发 / 平台上传 / 读盘),入站媒体产出 canonical `data.media`;`segments.outboundMedia` 声明与实际消费逐一核对修正;QQ 入站新增 canonical segments(image/audio/video/file/mention/face/reply),图片/语音/视频不再丢失。
|
|
16
|
+
|
|
17
|
+
迁移:适配器/插件产媒体一律用 `{ type, data: { media: MediaRef } }`;发送 legacy `data.url/file/base64` 形状的段会被 warn 丢弃。
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [7c1e63a]
|
|
20
|
+
- Updated dependencies [4fbff5d]
|
|
21
|
+
- Updated dependencies [5b94d9c]
|
|
22
|
+
- @zhin.js/command@1.0.5
|
|
23
|
+
- @zhin.js/adapter@1.1.3
|
|
24
|
+
- @zhin.js/core@1.5.0
|
|
25
|
+
- @zhin.js/agent@1.1.0
|
|
26
|
+
- zhin.js@6.0.0
|
|
27
|
+
|
|
3
28
|
## 5.0.6
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/adapters/slack.js
CHANGED
|
@@ -11,15 +11,16 @@ import { slackRuntimeStateToken } from "../lib/slack-runtime-state.js";
|
|
|
11
11
|
export { SlackEndpoint } from "../lib/endpoint.js";
|
|
12
12
|
export default defineAdapter({
|
|
13
13
|
capabilities: ['inbound', 'outbound'],
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// 媒体段(canonical MediaRef):kind=url 直发(image 走 attachment image_url,
|
|
15
|
+
// 其余拉取后上传);kind=base64 解码后经 files.uploadV2 上传;kind=path 读盘上传;
|
|
16
|
+
// kind=file 无 Slack 对应物,丢弃留痕。Block Kit 原生按钮承载交互段。
|
|
16
17
|
segments: {
|
|
17
|
-
outboundMedia: ['url', 'upload'],
|
|
18
|
+
outboundMedia: ['url', 'upload', 'path'],
|
|
18
19
|
interactive: 'native',
|
|
19
20
|
},
|
|
20
21
|
create(context) {
|
|
21
22
|
const config = resolveSlackConfig(context.config);
|
|
22
|
-
// 注册到插件运行时状态(slack
|
|
23
|
+
// 注册到插件运行时状态(slack.endpoint list 的"运行中"数据源)
|
|
23
24
|
context.use(slackRuntimeStateToken).endpoints.set(config.name, {
|
|
24
25
|
name: config.name,
|
|
25
26
|
mode: config.mode,
|
package/adapters/slack.ts
CHANGED
|
@@ -16,15 +16,16 @@ export type { SlackEndpointOptions, SlackSocketLike, SlackWebClientLike } from '
|
|
|
16
16
|
|
|
17
17
|
export default defineAdapter<SlackAdapterConfig>({
|
|
18
18
|
capabilities: ['inbound', 'outbound'],
|
|
19
|
-
//
|
|
20
|
-
//
|
|
19
|
+
// 媒体段(canonical MediaRef):kind=url 直发(image 走 attachment image_url,
|
|
20
|
+
// 其余拉取后上传);kind=base64 解码后经 files.uploadV2 上传;kind=path 读盘上传;
|
|
21
|
+
// kind=file 无 Slack 对应物,丢弃留痕。Block Kit 原生按钮承载交互段。
|
|
21
22
|
segments: {
|
|
22
|
-
outboundMedia: ['url', 'upload'],
|
|
23
|
+
outboundMedia: ['url', 'upload', 'path'],
|
|
23
24
|
interactive: 'native',
|
|
24
25
|
},
|
|
25
26
|
create(context) {
|
|
26
27
|
const config = resolveSlackConfig(context.config);
|
|
27
|
-
// 注册到插件运行时状态(slack
|
|
28
|
+
// 注册到插件运行时状态(slack.endpoint list 的"运行中"数据源)
|
|
28
29
|
context.use(slackRuntimeStateToken).endpoints.set(config.name, {
|
|
29
30
|
name: config.name,
|
|
30
31
|
mode: config.mode,
|
package/lib/protocol.js
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
*/
|
|
5
5
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
6
|
+
import { isMediaRef } from '@zhin.js/core';
|
|
7
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
8
|
import { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
|
|
9
|
+
const logger = getLogger('slack');
|
|
10
|
+
/** 无 canonical MediaRef 或不可投递时留痕(对齐 QQ `*_outbound_media_dropped` 风格)。 */
|
|
11
|
+
function dropOutboundMedia(type, reason) {
|
|
12
|
+
logger.warn(formatCompact({ op: 'slack_outbound_media_dropped', type, reason }));
|
|
13
|
+
}
|
|
7
14
|
const SLACK_SIG_VERSION = 'v0';
|
|
8
15
|
const MAX_TIMESTAMP_DRIFT_SECONDS = 300;
|
|
9
16
|
export function resolveSlackConfig(config = {}) {
|
|
@@ -129,31 +136,42 @@ export function formatOutboundWire(payload) {
|
|
|
129
136
|
text += `<${String(data.url ?? '')}>`;
|
|
130
137
|
}
|
|
131
138
|
break;
|
|
132
|
-
case 'image':
|
|
133
|
-
|
|
139
|
+
case 'image': {
|
|
140
|
+
// canonical MediaRef 唯一来源;kind=url 走 attachment 直发,其余物化后 files.uploadV2 上传
|
|
141
|
+
const media = data.media;
|
|
142
|
+
if (!isMediaRef(media)) {
|
|
143
|
+
dropOutboundMedia(item.type, 'missing_media_ref');
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
if (media.kind === 'url') {
|
|
134
147
|
attachments.push({
|
|
135
|
-
image_url:
|
|
136
|
-
title: String(data.
|
|
148
|
+
image_url: media.value,
|
|
149
|
+
title: String(data.alt ?? data.title ?? media.file_name ?? ''),
|
|
137
150
|
});
|
|
151
|
+
break;
|
|
138
152
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
const imageFile = resolveMediaToFile(media, String(data.alt ?? media.file_name ?? 'image'));
|
|
154
|
+
if (imageFile)
|
|
155
|
+
files.push(imageFile);
|
|
156
|
+
else
|
|
157
|
+
dropOutboundMedia(item.type, 'unsupported_media_kind');
|
|
142
158
|
break;
|
|
159
|
+
}
|
|
143
160
|
case 'audio':
|
|
144
161
|
case 'video':
|
|
145
|
-
case 'file':
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
files.push({
|
|
151
|
-
path: typeof data.file === 'string' ? data.file : undefined,
|
|
152
|
-
url: typeof data.url === 'string' ? data.url : undefined,
|
|
153
|
-
name: String(data.name ?? item.type),
|
|
154
|
-
});
|
|
162
|
+
case 'file': {
|
|
163
|
+
const media = data.media;
|
|
164
|
+
if (!isMediaRef(media)) {
|
|
165
|
+
dropOutboundMedia(item.type, 'missing_media_ref');
|
|
166
|
+
break;
|
|
155
167
|
}
|
|
168
|
+
const mediaFile = resolveMediaToFile(media, String(data.name ?? media.file_name ?? item.type));
|
|
169
|
+
if (mediaFile)
|
|
170
|
+
files.push(mediaFile);
|
|
171
|
+
else
|
|
172
|
+
dropOutboundMedia(item.type, 'unsupported_media_kind');
|
|
156
173
|
break;
|
|
174
|
+
}
|
|
157
175
|
case 'keyboard':
|
|
158
176
|
blocks.push(...keyboardToBlockKitBlocks(data));
|
|
159
177
|
break;
|
|
@@ -183,11 +201,27 @@ export function keyboardToBlockKitBlocks(data) {
|
|
|
183
201
|
}
|
|
184
202
|
return blocks;
|
|
185
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* MediaRef → 待上传文件描述:
|
|
206
|
+
* - kind=url → 记录 URL,由 files.uploadV2 前拉取物化;
|
|
207
|
+
* - kind=base64 → 解码为 buffer 直传;
|
|
208
|
+
* - kind=path → 记录本地路径,上传前读盘;
|
|
209
|
+
* - kind=file → Slack 无平台不透明文件引用可直发,返回 undefined 由调用方丢弃留痕。
|
|
210
|
+
*/
|
|
186
211
|
function resolveMediaToFile(media, name) {
|
|
187
|
-
|
|
188
|
-
|
|
212
|
+
switch (media.kind) {
|
|
213
|
+
case 'base64':
|
|
214
|
+
return {
|
|
215
|
+
buffer: Buffer.from(media.value.replace(/^base64:\/\//, ''), 'base64'),
|
|
216
|
+
name,
|
|
217
|
+
};
|
|
218
|
+
case 'url':
|
|
219
|
+
return { url: media.value, name };
|
|
220
|
+
case 'path':
|
|
221
|
+
return { path: media.value, name };
|
|
222
|
+
default:
|
|
223
|
+
return undefined;
|
|
189
224
|
}
|
|
190
|
-
return { url: media.value, name };
|
|
191
225
|
}
|
|
192
226
|
export function verifySlackSignature(signingSecret, rawBody, timestamp, signature) {
|
|
193
227
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `slack
|
|
2
|
+
* `slack.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
3
|
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
4
|
*/
|
|
5
5
|
import { createEndpointCommands } from '@zhin.js/adapter';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slack 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `slack
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `slack.endpoint` 命令共享(同一 owner generation)。
|
|
4
4
|
*/
|
|
5
5
|
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
6
|
export const slackRuntimeStateToken = defineEndpointRuntimeStateToken('slack');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-slack",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Zhin.js Slack adapter for Plugin Runtime (Socket Mode / HTTP Events)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -34,21 +34,21 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@slack/socket-mode": "^2.0.7",
|
|
36
36
|
"@slack/web-api": "^7.19.0",
|
|
37
|
-
"@zhin.js/adapter": "1.1.
|
|
38
|
-
"@zhin.js/command": "1.0.
|
|
39
|
-
"@zhin.js/core": "1.
|
|
37
|
+
"@zhin.js/adapter": "1.1.3",
|
|
38
|
+
"@zhin.js/command": "1.0.5",
|
|
39
|
+
"@zhin.js/core": "1.5.0",
|
|
40
40
|
"@zhin.js/host-http": "1.0.4",
|
|
41
41
|
"@zhin.js/logger": "1.0.75",
|
|
42
42
|
"@zhin.js/plugin-runtime": "1.1.1"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"zod": "^4.0.0",
|
|
46
|
-
"@zhin.js/adapter": "1.1.
|
|
47
|
-
"@zhin.js/agent": "1.0
|
|
48
|
-
"@zhin.js/core": "1.
|
|
46
|
+
"@zhin.js/adapter": "1.1.3",
|
|
47
|
+
"@zhin.js/agent": "1.1.0",
|
|
48
|
+
"@zhin.js/core": "1.5.0",
|
|
49
49
|
"@zhin.js/host-http": "1.0.4",
|
|
50
50
|
"@zhin.js/plugin-runtime": "1.1.1",
|
|
51
|
-
"zhin.js": "
|
|
51
|
+
"zhin.js": "6.0.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"zhin.js": {
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
"typescript": "^6.0.3",
|
|
67
67
|
"vitest": "^4.1.10",
|
|
68
68
|
"zod": "^4.4.3",
|
|
69
|
-
"@zhin.js/agent": "1.0
|
|
70
|
-
"zhin.js": "
|
|
69
|
+
"@zhin.js/agent": "1.1.0",
|
|
70
|
+
"zhin.js": "6.0.0"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"adapters",
|
package/plugin.js
CHANGED
|
@@ -9,7 +9,7 @@ export default definePlugin({
|
|
|
9
9
|
displayName: 'Slack Adapter',
|
|
10
10
|
},
|
|
11
11
|
setup(context) {
|
|
12
|
-
// 运行中 endpoint 注册表(slack
|
|
12
|
+
// 运行中 endpoint 注册表(slack.endpoint list 的"运行中"数据源)
|
|
13
13
|
context.resources.provide(slackRuntimeStateToken, createEndpointRuntimeState());
|
|
14
14
|
// 平台权限门禁:workspace_owner / channel_manager 等(agent 工具 platformPermit)
|
|
15
15
|
return registerSlackPlatformPermitChecker();
|
package/src/protocol.ts
CHANGED
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
6
6
|
import type { IncomingMessage } from 'node:http';
|
|
7
|
+
import { isMediaRef, type MediaRef } from '@zhin.js/core';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
9
|
import { mrkdwnToMarkdown } from './mrkdwn-to-markdown.js';
|
|
8
10
|
|
|
11
|
+
const logger = getLogger('slack');
|
|
12
|
+
|
|
13
|
+
/** 无 canonical MediaRef 或不可投递时留痕(对齐 QQ `*_outbound_media_dropped` 风格)。 */
|
|
14
|
+
function dropOutboundMedia(type: string, reason: string): void {
|
|
15
|
+
logger.warn(formatCompact({ op: 'slack_outbound_media_dropped', type, reason }));
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
const SLACK_SIG_VERSION = 'v0';
|
|
10
19
|
const MAX_TIMESTAMP_DRIFT_SECONDS = 300;
|
|
11
20
|
|
|
@@ -266,29 +275,38 @@ export function formatOutboundWire(payload: unknown): {
|
|
|
266
275
|
text += `<${String(data.url ?? '')}>`;
|
|
267
276
|
}
|
|
268
277
|
break;
|
|
269
|
-
case 'image':
|
|
270
|
-
|
|
278
|
+
case 'image': {
|
|
279
|
+
// canonical MediaRef 唯一来源;kind=url 走 attachment 直发,其余物化后 files.uploadV2 上传
|
|
280
|
+
const media = data.media;
|
|
281
|
+
if (!isMediaRef(media)) {
|
|
282
|
+
dropOutboundMedia(item.type, 'missing_media_ref');
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
if (media.kind === 'url') {
|
|
271
286
|
attachments.push({
|
|
272
|
-
image_url:
|
|
273
|
-
title: String(data.
|
|
287
|
+
image_url: media.value,
|
|
288
|
+
title: String(data.alt ?? data.title ?? media.file_name ?? ''),
|
|
274
289
|
});
|
|
275
|
-
|
|
276
|
-
files.push(resolveMediaToFile(data.media as { kind: string; value: string }, String(data.alt ?? 'image')));
|
|
290
|
+
break;
|
|
277
291
|
}
|
|
292
|
+
const imageFile = resolveMediaToFile(media, String(data.alt ?? media.file_name ?? 'image'));
|
|
293
|
+
if (imageFile) files.push(imageFile);
|
|
294
|
+
else dropOutboundMedia(item.type, 'unsupported_media_kind');
|
|
278
295
|
break;
|
|
296
|
+
}
|
|
279
297
|
case 'audio':
|
|
280
298
|
case 'video':
|
|
281
|
-
case 'file':
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
path: typeof data.file === 'string' ? data.file : undefined,
|
|
287
|
-
url: typeof data.url === 'string' ? data.url : undefined,
|
|
288
|
-
name: String(data.name ?? item.type),
|
|
289
|
-
});
|
|
299
|
+
case 'file': {
|
|
300
|
+
const media = data.media;
|
|
301
|
+
if (!isMediaRef(media)) {
|
|
302
|
+
dropOutboundMedia(item.type, 'missing_media_ref');
|
|
303
|
+
break;
|
|
290
304
|
}
|
|
305
|
+
const mediaFile = resolveMediaToFile(media, String(data.name ?? media.file_name ?? item.type));
|
|
306
|
+
if (mediaFile) files.push(mediaFile);
|
|
307
|
+
else dropOutboundMedia(item.type, 'unsupported_media_kind');
|
|
291
308
|
break;
|
|
309
|
+
}
|
|
292
310
|
case 'keyboard':
|
|
293
311
|
blocks.push(...keyboardToBlockKitBlocks(data));
|
|
294
312
|
break;
|
|
@@ -321,14 +339,30 @@ export function keyboardToBlockKitBlocks(data: Record<string, unknown>): Record<
|
|
|
321
339
|
return blocks;
|
|
322
340
|
}
|
|
323
341
|
|
|
342
|
+
/**
|
|
343
|
+
* MediaRef → 待上传文件描述:
|
|
344
|
+
* - kind=url → 记录 URL,由 files.uploadV2 前拉取物化;
|
|
345
|
+
* - kind=base64 → 解码为 buffer 直传;
|
|
346
|
+
* - kind=path → 记录本地路径,上传前读盘;
|
|
347
|
+
* - kind=file → Slack 无平台不透明文件引用可直发,返回 undefined 由调用方丢弃留痕。
|
|
348
|
+
*/
|
|
324
349
|
function resolveMediaToFile(
|
|
325
|
-
media:
|
|
350
|
+
media: MediaRef,
|
|
326
351
|
name: string,
|
|
327
|
-
): { buffer?: Buffer; url?: string; name: string } {
|
|
328
|
-
|
|
329
|
-
|
|
352
|
+
): { buffer?: Buffer; url?: string; path?: string; name: string } | undefined {
|
|
353
|
+
switch (media.kind) {
|
|
354
|
+
case 'base64':
|
|
355
|
+
return {
|
|
356
|
+
buffer: Buffer.from(media.value.replace(/^base64:\/\//, ''), 'base64'),
|
|
357
|
+
name,
|
|
358
|
+
};
|
|
359
|
+
case 'url':
|
|
360
|
+
return { url: media.value, name };
|
|
361
|
+
case 'path':
|
|
362
|
+
return { path: media.value, name };
|
|
363
|
+
default:
|
|
364
|
+
return undefined;
|
|
330
365
|
}
|
|
331
|
-
return { url: media.value, name };
|
|
332
366
|
}
|
|
333
367
|
|
|
334
368
|
export function verifySlackSignature(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `slack
|
|
2
|
+
* `slack.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
3
|
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
4
|
*/
|
|
5
5
|
import { createEndpointCommands } from '@zhin.js/adapter';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slack 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `slack
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `slack.endpoint` 命令共享(同一 owner generation)。
|
|
4
4
|
*/
|
|
5
5
|
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
6
|
|