@zhin.js/core 1.4.2 → 1.5.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.
@@ -20,34 +20,33 @@ export interface OutboundSegment {
20
20
  }
21
21
  /**
22
22
  * 出站媒体能力策略(html→image 渲染产物与 base64/path 媒体段的投递方式):
23
- * - `base64`:平台接受 base64 直发(qq / icqq / slack / weixin-ilink);
23
+ * - `base64`:平台接受 base64 直发;
24
24
  * - `url-or-text`:平台仅接受 URL 媒体;本层无上传通道,base64/path → 文本降级;
25
- * - `passthrough`:adapter 自行物化媒体(napcat / onebot11 / onebot12),本层不动。
25
+ * - `passthrough`:adapter 自行物化媒体,本层不动。
26
26
  */
27
27
  export type OutboundMediaPolicy = 'base64' | 'url-or-text' | 'passthrough';
28
28
  /**
29
- * 任务 C `defineAdapter` segments policy 挂载形状(duck-typed 读取)。
30
- * 契约形状为 `outboundMedia: readonly ('url'|'path'|'base64'|'upload')[]`
31
- * (端点可消费的媒体来源形式,见 `@zhin.js/adapter` AdapterSegmentPolicy);
32
- * 兼容过渡期的单值策略字符串。Adapter definition 上声明
33
- * `segments: { outboundMedia }` 即覆盖内置表。
29
+ * adapter definition segments policy 声明(必选能力面)。
30
+ * `outboundMedia` 为端点可消费的媒体来源形式数组
31
+ * ('url' | 'path' | 'base64' | 'upload'),映射到投递策略:
32
+ * base64 可直发;无 base64 但端点可自行物化(upload/path)→ passthrough;
33
+ * url URL 媒体文本降级。未声明的 adapter 按 ['url'] 兜底。
34
34
  */
35
35
  export interface OutboundSegmentsPolicy {
36
- readonly outboundMedia?: OutboundMediaPolicy | readonly ('url' | 'path' | 'base64' | 'upload')[];
36
+ readonly outboundMedia: readonly ('url' | 'path' | 'base64' | 'upload')[];
37
37
  readonly interactive?: InteractivePolicy;
38
38
  }
39
39
  export interface NormalizeOutboundOptions {
40
- /** 缺省 `base64`(保持历史行为:html→image base64 直发)。 */
40
+ /** 缺省 `url-or-text`(未声明 adapter 的保守兜底:仅 URL 媒体可达)。 */
41
41
  readonly mediaPolicy?: OutboundMediaPolicy;
42
42
  }
43
- export declare function isOutboundSegment(value: unknown): value is OutboundSegment;
44
43
  /**
45
- * 解析端点的出站媒体策略:优先读 adapter definition 上声明的
46
- * `segments.outboundMedia`(任务 C 挂载点;多 endpoint 展开的 `slot~entry`
47
- * id 回退到 slot id 查声明),否则按平台名查内置表,
48
- * 未知平台回退 `base64`(历史行为)。
44
+ * 解析端点的出站媒体策略:读 adapter definition 声明的 `segments.outboundMedia`
45
+ * (多 endpoint 展开的 `slot~entry` id 回退到 slot id 查声明);
46
+ * 未声明回退 `url-or-text`(仅 URL 媒体可达,其余文本降级)。
49
47
  */
50
48
  export declare function resolveOutboundMediaPolicy(adapter: CapabilityId, snapshot: RuntimeSnapshot): OutboundMediaPolicy;
49
+ export declare function isOutboundSegment(value: unknown): value is OutboundSegment;
51
50
  /**
52
51
  * 解析端点的出站 interactive 策略:优先读 adapter definition 上声明的
53
52
  * `segments.interactive`,否则按平台名查内置表,未知平台回退 'text'。
@@ -5,48 +5,16 @@ import { DEFAULT_INTERACTIVE_POLICY, isKeyboardSegment, } from '../../built/inte
5
5
  import { isMediaRef } from '../../built/segment-contract/index.js';
6
6
  const DEFAULT_CARD_WIDTH = 540;
7
7
  const DEFAULT_CARD_FILENAME = 'card.png';
8
- const DEFAULT_MEDIA_POLICY = 'base64';
9
- /** 平台名 → 出站媒体策略(任务 C 声明式 policy 落地前的内置来源)。 */
10
- const OUTBOUND_MEDIA_POLICY_BY_ADAPTER = { qq: 'base64',
11
- icqq: 'base64',
12
- slack: 'base64',
13
- 'weixin-ilink': 'base64',
14
- telegram: 'url-or-text',
15
- line: 'url-or-text',
16
- lark: 'url-or-text',
17
- kook: 'url-or-text',
18
- dingtalk: 'url-or-text',
19
- 'wechat-mp': 'url-or-text',
20
- wecom: 'url-or-text',
21
- email: 'url-or-text',
22
- github: 'url-or-text',
23
- milky: 'url-or-text',
24
- napcat: 'passthrough',
25
- onebot11: 'passthrough',
26
- onebot12: 'passthrough',
27
- };
28
- export function isOutboundSegment(value) {
29
- return typeof value === 'object'
30
- && value !== null
31
- && !Array.isArray(value)
32
- && typeof value.type === 'string';
33
- }
8
+ const DEFAULT_MEDIA_POLICY = 'url-or-text';
34
9
  /**
35
- * 解析端点的出站媒体策略:优先读 adapter definition 上声明的
36
- * `segments.outboundMedia`(任务 C 挂载点;多 endpoint 展开的 `slot~entry`
37
- * id 回退到 slot id 查声明),否则按平台名查内置表,
38
- * 未知平台回退 `base64`(历史行为)。
10
+ * 解析端点的出站媒体策略:读 adapter definition 声明的 `segments.outboundMedia`
11
+ * (多 endpoint 展开的 `slot~entry` id 回退到 slot id 查声明);
12
+ * 未声明回退 `url-or-text`(仅 URL 媒体可达,其余文本降级)。
39
13
  */
40
14
  export function resolveOutboundMediaPolicy(adapter, snapshot) {
41
15
  const slot = snapshot.capabilities.get(adapter)
42
16
  ?? snapshot.capabilities.get(baseSlotCapabilityId(adapter));
43
- const declared = readDeclaredMediaPolicy(slot?.definition);
44
- if (declared)
45
- return declared;
46
- const packageName = slot ? snapshot.tree.get(slot.owner)?.packageName : undefined;
47
- const adapterType = adapterTypeName(packageName);
48
- return (adapterType ? OUTBOUND_MEDIA_POLICY_BY_ADAPTER[adapterType] : undefined)
49
- ?? DEFAULT_MEDIA_POLICY;
17
+ return readDeclaredMediaPolicy(slot?.definition) ?? DEFAULT_MEDIA_POLICY;
50
18
  }
51
19
  function readDeclaredMediaPolicy(definition) {
52
20
  if (!definition || typeof definition !== 'object')
@@ -55,23 +23,22 @@ function readDeclaredMediaPolicy(definition) {
55
23
  if (!segments || typeof segments !== 'object')
56
24
  return undefined;
57
25
  const declared = segments.outboundMedia;
58
- // 过渡期单值策略字符串
59
- if (declared === 'base64' || declared === 'url-or-text' || declared === 'passthrough') {
60
- return declared;
61
- }
62
- // 任务 C 契约:媒体来源形式数组 → 投递策略。
63
- // base64 可直发;无 base64 但端点可自行物化(upload/path)→ passthrough
64
- // url → 非 URL 媒体文本降级。
65
- if (Array.isArray(declared)) {
66
- if (declared.includes('base64'))
67
- return 'base64';
68
- if (declared.includes('upload') || declared.includes('path'))
69
- return 'passthrough';
70
- if (declared.includes('url'))
71
- return 'url-or-text';
72
- }
26
+ if (!Array.isArray(declared))
27
+ return undefined;
28
+ if (declared.includes('base64'))
29
+ return 'base64';
30
+ if (declared.includes('upload') || declared.includes('path'))
31
+ return 'passthrough';
32
+ if (declared.includes('url'))
33
+ return 'url-or-text';
73
34
  return undefined;
74
35
  }
36
+ export function isOutboundSegment(value) {
37
+ return typeof value === 'object'
38
+ && value !== null
39
+ && !Array.isArray(value)
40
+ && typeof value.type === 'string';
41
+ }
75
42
  /**
76
43
  * 平台名 → 出站 interactive 策略(adapter 未声明 `segments.interactive` 时
77
44
  * 的内置来源):telegram / discord 端点自行把 keyboard 编码为原生按钮;
@@ -181,11 +148,12 @@ async function renderHtmlSegment(segment, renderer, mediaPolicy) {
181
148
  return {
182
149
  type: 'image',
183
150
  data: {
184
- // canonical MediaRef 为主,legacy `base64`/`name` 双写保持旧 adapter 可读
185
- // (Wave 2 适配器迁移完成后移除 legacy 字段)。
186
- media: { kind: 'base64', value: base64, mime_type: 'image/png' },
187
- base64,
188
- name: typeof data.fileName === 'string' ? data.fileName : DEFAULT_CARD_FILENAME,
151
+ media: {
152
+ kind: 'base64',
153
+ value: base64,
154
+ mime_type: 'image/png',
155
+ file_name: typeof data.fileName === 'string' ? data.fileName : DEFAULT_CARD_FILENAME,
156
+ },
189
157
  },
190
158
  };
191
159
  }
@@ -197,22 +165,26 @@ async function renderHtmlSegment(segment, renderer, mediaPolicy) {
197
165
  return { type: 'text', data: { text: htmlSegmentFallbackText(data, html) } };
198
166
  }
199
167
  /**
200
- * 媒体协商:仅 `url-or-text` 需要在本层动作——image 段的非 URL MediaRef
201
- * (base64 / 本地路径)无法投递,降级为文本(alt 优先)。
168
+ * 媒体协商:仅 `url-or-text` 需要在本层动作——媒体段的非 URL MediaRef
169
+ * (base64 / 本地路径)无法投递,降级为文本(alt / file_name 优先)。
202
170
  */
171
+ const MEDIA_SEGMENT_TYPES = new Set(['image', 'audio', 'video', 'file']);
203
172
  function applyOutboundMediaPolicy(segments, mediaPolicy) {
204
173
  if (mediaPolicy !== 'url-or-text')
205
174
  return segments;
206
175
  return segments.map((segment) => {
207
- if (segment.type !== 'image')
176
+ if (!MEDIA_SEGMENT_TYPES.has(segment.type))
208
177
  return segment;
209
- const media = segment.data.media;
178
+ const data = segment.data;
179
+ const media = data.media;
210
180
  if (!isMediaRef(media) || media.kind === 'url')
211
181
  return segment;
212
- const alt = typeof segment.data.alt === 'string' && segment.data.alt
213
- ? segment.data.alt
214
- : '[image]';
215
- return { type: 'text', data: { text: alt } };
182
+ const label = typeof data.alt === 'string' && data.alt
183
+ ? data.alt
184
+ : typeof data.name === 'string' && data.name
185
+ ? data.name
186
+ : media.file_name ?? `[${segment.type}]`;
187
+ return { type: 'text', data: { text: label } };
216
188
  });
217
189
  }
218
190
  function htmlSegmentFallbackText(data, html) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/core",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Zhin机器人核心框架",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -68,12 +68,13 @@
68
68
  "dependencies": {
69
69
  "qrcode": "^1.5.4",
70
70
  "segment-matcher": "^1.0.5",
71
- "smol-toml": "^1.7.0",
71
+ "smol-toml": "^1.7.1",
72
72
  "yaml": "^2.9.0",
73
- "@zhin.js/adapter": "1.1.2",
74
- "@zhin.js/command": "1.0.4",
73
+ "@zhin.js/adapter": "1.1.3",
74
+ "@zhin.js/command": "1.0.5",
75
75
  "@zhin.js/component": "1.0.4",
76
76
  "@zhin.js/database": "1.0.78",
77
+ "@zhin.js/im-contract": "1.0.0",
77
78
  "@zhin.js/kernel": "1.0.5",
78
79
  "@zhin.js/logger": "1.0.75",
79
80
  "@zhin.js/middleware": "1.0.4",
@@ -89,11 +90,11 @@
89
90
  }
90
91
  },
91
92
  "devDependencies": {
92
- "@types/node": "^26.1.0",
93
- "@types/qrcode": "^1.5.5",
93
+ "@types/node": "^26.1.2",
94
+ "@types/qrcode": "^1.5.6",
94
95
  "ajv": "8.18.0",
95
96
  "typescript": "^6.0.3",
96
- "@zhin.js/ai": "1.4.6"
97
+ "@zhin.js/ai": "1.5.0"
97
98
  },
98
99
  "repository": {
99
100
  "type": "git",