@zhin.js/plugin-group-suite 1.1.9 → 1.1.11

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 CHANGED
@@ -9,10 +9,12 @@ Plugin Runtime 群运营套件,提供签到积分、消息统计、群问答
9
9
  pnpm exec zhin install @zhin.js/plugin-group-suite
10
10
  ```
11
11
 
12
- 安装会将实例挂在父插件的 `package.json#zhin.plugins`。手工挂载:
12
+ 安装会将实例声明写入主配置 `plugins` 映射。手工声明:
13
13
 
14
- ```json
15
- { "package": "@zhin.js/plugin-group-suite", "instanceKey": "group-suite" }
14
+ ```yaml
15
+ configVersion: 2
16
+ plugins:
17
+ group-suite: '@zhin.js/plugin-group-suite'
16
18
  ```
17
19
 
18
20
  应用 Host 必须共享同一物理实例的 `@zhin.js/command`、`@zhin.js/middleware`、
@@ -20,20 +22,22 @@ pnpm exec zhin install @zhin.js/plugin-group-suite
20
22
  Feature provider,并装配 Schedule Host。正常 CLI 负责 Host 装配;手写测试 Host 也需要显式安装这些资源。
21
23
 
22
24
  ```yaml
25
+ configVersion: 2
23
26
  plugins:
24
- group-suite:
25
- keywordReply: true
26
- ephemeralKeywords: false
27
- welcomeMessage: "" # 默认关闭;可设为 欢迎 {name} ({id})
28
- basePointsMin: 10
29
- basePointsMax: 30
30
- streakBonus: 5
31
- streakCap: 50
32
- rankSize: 10
33
- teachMaxPerGroup: 200
34
- teachCooldownMs: 3000
35
- teachAllowRegex: true
36
- teachPageSize: 10
27
+ group-suite: "@zhin.js/plugin-group-suite"
28
+ group-suite:
29
+ keywordReply: true
30
+ ephemeralKeywords: false
31
+ welcomeMessage: "" # 默认关闭;可设为 欢迎 {name} ({id})
32
+ basePointsMin: 10
33
+ basePointsMax: 30
34
+ streakBonus: 5
35
+ streakCap: 50
36
+ rankSize: 10
37
+ teachMaxPerGroup: 200
38
+ teachCooldownMs: 3000
39
+ teachAllowRegex: true
40
+ teachPageSize: 10
37
41
  ```
38
42
 
39
43
  约束以 [schema.json](./schema.json) 为准。启用关键词回复时默认要求 Database Host 的原子
@@ -73,6 +77,35 @@ plugins:
73
77
  欢迎通过 `outboundMessageToken` 走统一发送链路,投递失败/未知会报错,不盲目重试。
74
78
  `stats-card` 是 HTML 图片/文本展示,不是平台原生 `card` 载荷。
75
79
 
80
+ ### 限定群欢迎范围
81
+
82
+ `welcomeConversations` 是当前插件实例的精确会话白名单,最多 100 项。省略此字段保留原行为:
83
+ 设置非空 `welcomeMessage` 后,所有符合条件的群成员加入事件都可触发欢迎。显式配置 `[]`
84
+ 则不欢迎任何会话;`welcomeMessage` 为空时仍始终关闭欢迎。
85
+
86
+ ```yaml
87
+ group-suite:
88
+ welcomeMessage: "欢迎 {name} ({id})"
89
+ welcomeConversations:
90
+ - endpoint:
91
+ adapter: "<复制实际 conversation.endpoint.adapter>"
92
+ id: "root/qq\u0000zhin.adapter\u0000icqq~main"
93
+ kind: group
94
+ id: "123456"
95
+ ```
96
+
97
+ 示例 Endpoint ID 仅说明格式,实际请复制当前 canonical `conversation.endpoint`,不要按昵称、
98
+ 账号、Endpoint 简称或实例名拼接。完整 Endpoint ID 中的 NUL 在双引号 YAML 中写为 `\u0000`。
99
+ `endpoint.adapter` 也须与事件完全一致。
100
+
101
+ 每项同时匹配完整 Endpoint 身份、`kind`、会话 `id`、`parent.kind/id` 和 `threadId`,没有通配符。
102
+ 省略 `parent` 或 `threadId` 只匹配同样没有该字段的群根会话;需要父容器或线程时须完整填写。
103
+ `null`、空 ID、残缺父容器和额外字段会被拒绝。重复条目不会重复发送欢迎。
104
+ 白名单只筛选入站群成员加入事件,不改写原会话投递目标,也不限制签到、词库、统计等能力。
105
+
106
+ 这不补充平台本身缺少的入站事件或目标:未提供 canonical 群会话的成员加入事件不会发送欢迎,
107
+ 也不会推测频道。配置可热更新,校验失败保留旧配置与 generation;没有单独的范围数据库。
108
+
76
109
  所有新数据按插件实例与完整会话隔离:平台、Endpoint 身份、群/频道/私聊类型与 ID,以及父会话、
77
110
  线程信息共同参与键。相同群号的两个 Bot 不共享词库、签到、教学或统计。群专用 Tool 只接受 IM
78
111
  群上下文,并通过当前 AdapterIndex 精确解析 Endpoint,无法确定唯一来源时拒绝执行。当前群的查询保留当前线程,只查该完整会话,不聚合其他线程。
@@ -2,18 +2,22 @@
2
2
  import { defineHandler } from 'zhin.js/handler';
3
3
  import { outboundMessageToken } from '@zhin.js/core/runtime';
4
4
  import { resolveGroupSuiteConfig } from "../../lib/config.js";
5
+ import { welcomeConversationAllowed } from "../../lib/welcome-scope.js";
5
6
  /** Canonical notice input only; no SDK-specific callbacks or direct platform sends. */
6
7
  export default defineHandler({
7
8
  event: 'notice.receive',
8
9
  async handle({ payload: notice }) {
9
- const text = resolveGroupSuiteConfig(this.config).welcomeMessage;
10
+ const config = resolveGroupSuiteConfig(this.config);
11
+ const text = config.welcomeMessage;
10
12
  if (!text || notice.name !== 'notice.group.member_increase'
11
13
  || notice.conversation?.kind !== 'group' || !notice.target?.id)
12
14
  return;
15
+ if (!welcomeConversationAllowed(config.welcomeConversations, notice.conversation))
16
+ return;
13
17
  const content = text.replaceAll('{name}', notice.target.name ?? notice.target.id)
14
18
  .replaceAll('{id}', notice.target.id);
15
19
  const result = await this.use(outboundMessageToken).send({
16
- requester: this.owner,
20
+ requester: this.owner.id,
17
21
  conversation: notice.conversation,
18
22
  content: { type: 'text', data: { text: content } },
19
23
  });
@@ -1,18 +1,21 @@
1
1
  import { defineHandler } from 'zhin.js/handler';
2
2
  import { outboundMessageToken } from '@zhin.js/core/runtime';
3
3
  import { resolveGroupSuiteConfig, type GroupSuiteConfig } from '../../src/config.js';
4
+ import { welcomeConversationAllowed } from '../../src/welcome-scope.js';
4
5
 
5
6
  /** Canonical notice input only; no SDK-specific callbacks or direct platform sends. */
6
7
  export default defineHandler({
7
8
  event: 'notice.receive',
8
9
  async handle({ payload: notice }) {
9
- const text = resolveGroupSuiteConfig(this.config as Partial<GroupSuiteConfig>).welcomeMessage;
10
+ const config = resolveGroupSuiteConfig(this.config as Partial<GroupSuiteConfig>);
11
+ const text = config.welcomeMessage;
10
12
  if (!text || notice.name !== 'notice.group.member_increase'
11
13
  || notice.conversation?.kind !== 'group' || !notice.target?.id) return;
14
+ if (!welcomeConversationAllowed(config.welcomeConversations, notice.conversation)) return;
12
15
  const content = text.replaceAll('{name}', notice.target.name ?? notice.target.id)
13
16
  .replaceAll('{id}', notice.target.id);
14
17
  const result = await this.use(outboundMessageToken).send({
15
- requester: this.owner,
18
+ requester: this.owner.id,
16
19
  conversation: notice.conversation,
17
20
  content: { type: 'text', data: { text: content } },
18
21
  });
package/lib/config.d.ts CHANGED
@@ -1,9 +1,12 @@
1
+ import type { DeliveryTarget } from 'zhin.js';
1
2
  export interface GroupSuiteConfig {
2
3
  keywordReply: boolean;
3
4
  /** Explicitly volatile keywords for demos without a persistent Database Host. */
4
5
  ephemeralKeywords: boolean;
5
6
  /** Empty disables group member welcomes. {name} and {id} are literal placeholders. */
6
7
  welcomeMessage: string;
8
+ /** Omitted preserves all groups; [] disables welcomes. Each address matches exactly. */
9
+ welcomeConversations?: readonly DeliveryTarget[];
7
10
  basePointsMin: number;
8
11
  basePointsMax: number;
9
12
  streakBonus: number;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,OAAO,CAAC;IACtB,iFAAiF;IACjF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sFAAsF;IACtF,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,0BAA0B,EAAE,gBAaxC,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,SAAS,GACzC,gBAAgB,CAKlB"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,OAAO,CAAC;IACtB,iFAAiF;IACjF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sFAAsF;IACtF,cAAc,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,0BAA0B,EAAE,gBAaxC,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,SAAS,GACzC,gBAAgB,CAQlB"}
package/lib/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { normalizeWelcomeConversations } from './welcome-scope.js';
1
2
  export const DEFAULT_GROUP_SUITE_CONFIG = {
2
3
  keywordReply: false,
3
4
  ephemeralKeywords: false,
@@ -16,6 +17,9 @@ export function resolveGroupSuiteConfig(raw) {
16
17
  return {
17
18
  ...DEFAULT_GROUP_SUITE_CONFIG,
18
19
  ...raw,
20
+ ...(raw?.welcomeConversations === undefined ? {} : {
21
+ welcomeConversations: normalizeWelcomeConversations(raw.welcomeConversations),
22
+ }),
19
23
  };
20
24
  }
21
25
  //# sourceMappingURL=config.js.map
package/lib/config.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAiBA,MAAM,CAAC,MAAM,0BAA0B,GAAqB;IAC1D,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,cAAc,EAAE,EAAE;IAClB,aAAa,EAAE,EAAE;IACjB,aAAa,EAAE,EAAE;IACjB,WAAW,EAAE,CAAC;IACd,SAAS,EAAE,EAAE;IACb,QAAQ,EAAE,EAAE;IACZ,gBAAgB,EAAE,GAAG;IACrB,eAAe,EAAE,IAAI;IACrB,eAAe,EAAE,IAAI;IACrB,aAAa,EAAE,EAAE;CAClB,CAAC;AAEF,MAAM,UAAU,uBAAuB,CACrC,GAA0C;IAE1C,OAAO;QACL,GAAG,0BAA0B;QAC7B,GAAG,GAAG;KACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,6BAA6B,EAAE,MAAM,oBAAoB,CAAC;AAqBnE,MAAM,CAAC,MAAM,0BAA0B,GAAqB;IAC1D,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,cAAc,EAAE,EAAE;IAClB,aAAa,EAAE,EAAE;IACjB,aAAa,EAAE,EAAE;IACjB,WAAW,EAAE,CAAC;IACd,SAAS,EAAE,EAAE;IACb,QAAQ,EAAE,EAAE;IACZ,gBAAgB,EAAE,GAAG;IACrB,eAAe,EAAE,IAAI;IACrB,eAAe,EAAE,IAAI;IACrB,aAAa,EAAE,EAAE;CAClB,CAAC;AAEF,MAAM,UAAU,uBAAuB,CACrC,GAA0C;IAE1C,OAAO;QACL,GAAG,0BAA0B;QAC7B,GAAG,GAAG;QACN,GAAG,CAAC,GAAG,EAAE,oBAAoB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACjD,oBAAoB,EAAE,6BAA6B,CAAC,GAAG,CAAC,oBAAoB,CAAC;SAC9E,CAAC;KACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { type DeliveryTarget } from 'zhin.js';
2
+ /** Validate without resolving aliases, retaining clients or introducing another scope store. */
3
+ export declare function normalizeWelcomeConversations(value: unknown): readonly DeliveryTarget[];
4
+ /** Missing parent/thread matches absence, never all descendants or threads. */
5
+ export declare function welcomeConversationAllowed(scopes: readonly DeliveryTarget[] | undefined, target: DeliveryTarget): boolean;
6
+ //# sourceMappingURL=welcome-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"welcome-scope.d.ts","sourceRoot":"","sources":["../src/welcome-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAGpE,gGAAgG;AAChG,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,cAAc,EAAE,CAOvF;AAED,+EAA+E;AAC/E,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAIzH"}
@@ -0,0 +1,21 @@
1
+ import { createDeliveryTarget } from 'zhin.js';
2
+ import { conversationKey } from './conversation-scope.js';
3
+ /** Validate without resolving aliases, retaining clients or introducing another scope store. */
4
+ export function normalizeWelcomeConversations(value) {
5
+ if (!Array.isArray(value) || value.length > 100)
6
+ throw new TypeError('welcomeConversations must contain at most 100 exact group conversations');
7
+ return Object.freeze(value.map(entry => {
8
+ const target = createDeliveryTarget({ conversation: entry });
9
+ if (target.kind !== 'group')
10
+ throw new TypeError('welcomeConversations only supports group conversations');
11
+ return target;
12
+ }));
13
+ }
14
+ /** Missing parent/thread matches absence, never all descendants or threads. */
15
+ export function welcomeConversationAllowed(scopes, target) {
16
+ if (scopes === undefined)
17
+ return true;
18
+ const key = conversationKey(target);
19
+ return scopes.some(scope => conversationKey(scope) === key);
20
+ }
21
+ //# sourceMappingURL=welcome-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"welcome-scope.js","sourceRoot":"","sources":["../src/welcome-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAuB,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,gGAAgG;AAChG,MAAM,UAAU,6BAA6B,CAAC,KAAc;IAC1D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,yEAAyE,CAAC,CAAC;IAChJ,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;QACrC,MAAM,MAAM,GAAG,oBAAoB,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;YAAE,MAAM,IAAI,SAAS,CAAC,wDAAwD,CAAC,CAAC;QAC3G,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC,CAAC;AACN,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,0BAA0B,CAAC,MAA6C,EAAE,MAAsB;IAC9G,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,GAAG,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACpC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;AAC9D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-group-suite",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "Zhin.js 群运营套件:签到、教学、统计、持久词库与群欢迎",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -42,24 +42,24 @@
42
42
  "license": "MIT",
43
43
  "dependencies": {
44
44
  "@zhin.js/components": "1.1.3",
45
- "@zhin.js/core": "1.1.44",
45
+ "@zhin.js/core": "1.1.46",
46
46
  "@zhin.js/jsx": "1.1.3",
47
- "@zhin.js/skill": "1.1.6",
48
- "@zhin.js/tool": "1.1.6"
47
+ "@zhin.js/skill": "1.1.7",
48
+ "@zhin.js/tool": "1.1.7"
49
49
  },
50
50
  "devDependencies": {
51
51
  "typescript": "^6.0.3",
52
52
  "vitest": "^4.1.10",
53
53
  "zod": "^4.4.3",
54
- "zhin.js": "1.1.9"
54
+ "zhin.js": "1.1.11"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "zod": "^4.0.0",
58
- "@zhin.js/command": "^1.1.7",
59
- "@zhin.js/middleware": "^1.1.6",
60
- "zhin.js": "^1.1.9",
61
- "@zhin.js/handler": "^1.1.5",
62
- "@zhin.js/schedule-feature": "^1.1.5"
58
+ "@zhin.js/command": "^1.1.8",
59
+ "@zhin.js/middleware": "^1.1.7",
60
+ "zhin.js": "^1.1.11",
61
+ "@zhin.js/handler": "^1.1.6",
62
+ "@zhin.js/schedule-feature": "^1.1.6"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "@zhin.js/command": {
package/schema.json CHANGED
@@ -49,6 +49,40 @@
49
49
  "default": "",
50
50
  "description": "Group welcome text. Empty disables; {name}/{id} refer to the joined member."
51
51
  },
52
+ "welcomeConversations": {
53
+ "type": "array",
54
+ "maxItems": 100,
55
+ "description": "Exact welcome conversations. Omitted welcomes all groups; [] welcomes none. Missing parent/thread requires their absence; no wildcards.",
56
+ "x-zhin-config": { "component": "conversation-scope" },
57
+ "items": {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "required": ["endpoint", "kind", "id"],
61
+ "properties": {
62
+ "endpoint": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "required": ["adapter", "id"],
66
+ "properties": {
67
+ "adapter": { "type": "string", "minLength": 1 },
68
+ "id": { "type": "string", "minLength": 1 }
69
+ }
70
+ },
71
+ "kind": { "type": "string", "enum": ["group"] },
72
+ "id": { "type": "string", "minLength": 1 },
73
+ "parent": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": ["kind", "id"],
77
+ "properties": {
78
+ "kind": { "type": "string", "enum": ["private", "group", "channel"] },
79
+ "id": { "type": "string", "minLength": 1 }
80
+ }
81
+ },
82
+ "threadId": { "type": "string", "minLength": 1 }
83
+ }
84
+ }
85
+ },
52
86
  "ephemeralKeywords": {
53
87
  "type": "boolean",
54
88
  "default": false,
package/src/config.ts CHANGED
@@ -1,9 +1,14 @@
1
+ import type { DeliveryTarget } from 'zhin.js';
2
+ import { normalizeWelcomeConversations } from './welcome-scope.js';
3
+
1
4
  export interface GroupSuiteConfig {
2
5
  keywordReply: boolean;
3
6
  /** Explicitly volatile keywords for demos without a persistent Database Host. */
4
7
  ephemeralKeywords: boolean;
5
8
  /** Empty disables group member welcomes. {name} and {id} are literal placeholders. */
6
9
  welcomeMessage: string;
10
+ /** Omitted preserves all groups; [] disables welcomes. Each address matches exactly. */
11
+ welcomeConversations?: readonly DeliveryTarget[];
7
12
  basePointsMin: number;
8
13
  basePointsMax: number;
9
14
  streakBonus: number;
@@ -36,5 +41,8 @@ export function resolveGroupSuiteConfig(
36
41
  return {
37
42
  ...DEFAULT_GROUP_SUITE_CONFIG,
38
43
  ...raw,
44
+ ...(raw?.welcomeConversations === undefined ? {} : {
45
+ welcomeConversations: normalizeWelcomeConversations(raw.welcomeConversations),
46
+ }),
39
47
  };
40
48
  }
@@ -0,0 +1,19 @@
1
+ import { createDeliveryTarget, type DeliveryTarget } from 'zhin.js';
2
+ import { conversationKey } from './conversation-scope.js';
3
+
4
+ /** Validate without resolving aliases, retaining clients or introducing another scope store. */
5
+ export function normalizeWelcomeConversations(value: unknown): readonly DeliveryTarget[] {
6
+ if (!Array.isArray(value) || value.length > 100) throw new TypeError('welcomeConversations must contain at most 100 exact group conversations');
7
+ return Object.freeze(value.map(entry => {
8
+ const target = createDeliveryTarget({ conversation: entry });
9
+ if (target.kind !== 'group') throw new TypeError('welcomeConversations only supports group conversations');
10
+ return target;
11
+ }));
12
+ }
13
+
14
+ /** Missing parent/thread matches absence, never all descendants or threads. */
15
+ export function welcomeConversationAllowed(scopes: readonly DeliveryTarget[] | undefined, target: DeliveryTarget): boolean {
16
+ if (scopes === undefined) return true;
17
+ const key = conversationKey(target);
18
+ return scopes.some(scope => conversationKey(scope) === key);
19
+ }