@zhin.js/adapter-github 3.0.3 → 4.0.1

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
@@ -1,5 +1,53 @@
1
1
  # @zhin.js/adapter-github
2
2
 
3
+ ## 4.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 74b035c: endpoint 管理命令扩展至 18/20 适配器:kook / discord / github(private_key 支持内联文件路径)/ icqq(bindFlow 登记式 add + `icqq login` 引导)/ dingtalk / lark / line / satori / wechat-mp / wecom / weixin-ilink 接入 `endpoint list/add/remove`(字段对齐各自 schema,凭据写 `.env`)。email(smtp/imap 嵌套对象)与 sandbox(无凭据)暂不接。
8
+ - Updated dependencies [cdf64e7]
9
+ - Updated dependencies [2d0a159]
10
+ - Updated dependencies [5691aba]
11
+ - Updated dependencies [078e3f7]
12
+ - Updated dependencies [50497a5]
13
+ - Updated dependencies [9c997b2]
14
+ - Updated dependencies [09d4f25]
15
+ - Updated dependencies [43485a9]
16
+ - Updated dependencies [f0ec5ab]
17
+ - Updated dependencies [3e925d0]
18
+ - Updated dependencies [fa66c4c]
19
+ - Updated dependencies [fa66c4c]
20
+ - Updated dependencies [6cb6152]
21
+ - @zhin.js/command@1.0.3
22
+ - @zhin.js/agent@1.0.6
23
+ - @zhin.js/plugin-runtime@1.1.1
24
+ - @zhin.js/host-http@1.0.3
25
+ - zhin.js@5.0.1
26
+ - @zhin.js/adapter@1.1.1
27
+ - @zhin.js/core@1.4.1
28
+
29
+ ## 4.0.0
30
+
31
+ ### Patch Changes
32
+
33
+ - 7db69c1: 命令前缀改为适配器配置项:`MessageDispatcher` 不再硬编码 `/`,默认按消息所属适配器实例 config 的 `commandPrefix` 解析(默认 `''` 无前缀,任意文本按命令匹配),`endpoints[i].commandPrefix` 逐项覆盖;`ImRuntime({ commandPrefix })` 仍可设全局静态前缀。全部 20 个平台适配器 schema 新增 `commandPrefix` 属性。
34
+
35
+ BREAKING(行为变化):未配置时命令不再需要 `/` 前缀——原 `/zt` 写法不再命中,直接发 `zt` 即可;需要斜杠风格的适配器请在配置里显式设 `commandPrefix: '/'`。
36
+
37
+ - 713445c: 适配器配置格式定稿(不兼容旧格式):`plugins.<adapter>` 顶层仅共享字段 + `commandPrefix`,`endpoints[i]` 携带 endpoint 级字段(`name` + 凭据,各 schema 已类型化),`endpoints` 为必填(icqq 另需顶层 `master`);icqq 新增 `trusted` 列表(顶层/逐项均可)。scaffold-wizard 全部字段式与自定义 configure() 产出改为新格式,examples(full-bot / qq-games-bot)与 20 个适配器 README 同步迁移。
38
+ - f32c424: 适配器配置统一为 endpoints 数组格式:`plugins.<adapter>` 为该 adapter 所有 endpoint 的通用配置,`plugins.<adapter>.endpoints[i]` 为单个 endpoint 的特殊配置(逐项覆盖,`name` 必填)。slack / github schema 新增 `endpoints` 属性;icqq / qq / slack 顶层必填改为 `anyOf`(单 endpoint 字段或 `endpoints` 数组二选一,兼容 Ajv strictRequired)。
39
+ - Updated dependencies [7db69c1]
40
+ - Updated dependencies [e5c84ed]
41
+ - Updated dependencies [3ea84a0]
42
+ - Updated dependencies [1ddcd70]
43
+ - Updated dependencies [ac9da66]
44
+ - @zhin.js/core@1.4.0
45
+ - @zhin.js/adapter@1.1.0
46
+ - @zhin.js/plugin-runtime@1.1.0
47
+ - @zhin.js/agent@1.0.5
48
+ - @zhin.js/host-http@1.0.2
49
+ - zhin.js@5.0.0
50
+
3
51
  ## 3.0.3
4
52
 
5
53
  ### Patch Changes
package/README.md CHANGED
@@ -24,14 +24,15 @@ Webhook 需要 Root 提供 `@zhin.js/host-http`(`zhin runtime start` 默认装
24
24
  # zhin.config.yml
25
25
  plugins:
26
26
  github:
27
- name: my-github-bot
28
- app_id: 123456
29
- private_key: ./data/github-app.pem
30
- webhook_secret: your-secret
31
27
  webhook_path: /github/webhook
32
28
  auto_reply_repos:
33
29
  - zhinjs/zhin
34
30
  workspace_root: ./data/github-workspaces
31
+ endpoints:
32
+ - name: my-github-bot
33
+ app_id: 123456
34
+ private_key: ./data/github-app.pem
35
+ webhook_secret: your-secret
35
36
  ```
36
37
 
37
38
  ```env
@@ -41,6 +42,20 @@ GITHUB_WEBHOOK_SECRET=your-secret
41
42
 
42
43
  `private_key` 支持文件路径或 PEM 内容。未配置 `webhook_secret` 时仅 API 出站 / agent 工具可用(无入站)。
43
44
 
45
+ 多 App:一个插件实例挂多个 endpoint(`endpoints` 数组逐项覆盖顶层字段,`name` 必填):
46
+
47
+ ```yaml
48
+ plugins:
49
+ github:
50
+ endpoints:
51
+ - name: app-a
52
+ app_id: 123456
53
+ private_key: ./data/app-a.pem
54
+ - name: app-b
55
+ app_id: 234567
56
+ private_key: ./data/app-b.pem
57
+ ```
58
+
44
59
  ## 已移除的配置
45
60
 
46
61
  - **`ai.githubMcp.enabled` / `ai.githubMcp.token`**:Plugin Runtime 迁移后 `register-github-mcp`(stdio `@modelcontextprotocol/server-github`,PAT 人身份)已移除,该配置不再生效。如需 MCP 工具,请按新运行时 `mcp/<name>.ts`(`@zhin.js/mcp-feature`)约定自行装配。
@@ -10,6 +10,7 @@ import {
10
10
  type DatabaseHost,
11
11
  } from '@zhin.js/plugin-runtime';
12
12
  import { GithubEndpoint } from '../src/endpoint.js';
13
+ import { githubRuntimeStateToken } from '../src/github-runtime-state.js';
13
14
  import {
14
15
  resolveGithubConfig,
15
16
  type GithubAdapterConfig,
@@ -28,9 +29,19 @@ function optionalDatabase(context: AdapterContext): DatabaseHost | undefined {
28
29
 
29
30
  export default defineAdapter<GithubAdapterConfig>({
30
31
  capabilities: ['inbound', 'outbound'],
32
+ // Issue/PR 评论以 markdown 图片链接消费远程 URL;无交互面,交互段降级纯文本。
33
+ segments: {
34
+ outboundMedia: ['url'],
35
+ interactive: 'text',
36
+ },
31
37
  create(context) {
32
38
  const config = resolveGithubConfig(context.config);
33
39
  const database = optionalDatabase(context);
40
+ // 注册到插件运行时状态(github endpoint list 的"运行中"数据源)
41
+ context.use(githubRuntimeStateToken).endpoints.set(config.name, {
42
+ name: config.name,
43
+ mode: config.webhookSecret ? 'webhook' : 'api',
44
+ });
34
45
  if (config.webhookSecret) {
35
46
  return new GithubEndpoint({
36
47
  id: context.id,
@@ -0,0 +1,3 @@
1
+ import { githubEndpointCommands } from '../../../src/github-endpoint-commands.js';
2
+
3
+ export default githubEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { githubEndpointCommands } from '../../src/github-endpoint-commands.js';
2
+
3
+ export default githubEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { githubEndpointCommands } from '../../../src/github-endpoint-commands.js';
2
+
3
+ export default githubEndpointCommands.remove;
package/lib/endpoint.d.ts CHANGED
@@ -13,6 +13,10 @@ export interface GithubEndpointOptions {
13
13
  readonly config: ResolvedGithubConfig;
14
14
  readonly createClient?: (config: ResolvedGithubConfig) => GhClient;
15
15
  }
16
+ /**
17
+ * GitHub 是代码协作面(issue/PR),无好友/群/频道等 IM 社交概念,
18
+ * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
19
+ */
16
20
  export declare class GithubEndpoint implements EndpointInstance {
17
21
  #private;
18
22
  readonly gh: GhClient;
package/lib/endpoint.js CHANGED
@@ -10,6 +10,10 @@ import { enrichInboundContent, formatInboundContent, formatOutboundBody, parseCh
10
10
  import { registerGithubWebhookRoutes } from './webhook.js';
11
11
  import { WorkspaceManager } from './workspace-manager.js';
12
12
  const logger = getLogger('github');
13
+ /**
14
+ * GitHub 是代码协作面(issue/PR),无好友/群/频道等 IM 社交概念,
15
+ * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
16
+ */
13
17
  export class GithubEndpoint {
14
18
  #options;
15
19
  gh;
@@ -0,0 +1 @@
1
+ export declare const githubEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `github endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
+ *
5
+ * 字段说明:private_key 内容长且含换行,不适合 kv 直传——运行时(gh-client
6
+ * resolvePrivateKey)本就支持 PEM 内容或文件路径,故 add 采用**内联路径**形式:
7
+ * `github endpoint add mybot app_id=123456 private_key=./data/mybot.pem`。
8
+ */
9
+ import { createEndpointCommands } from '@zhin.js/adapter';
10
+ import { defineCommand } from '@zhin.js/command';
11
+ import { githubRuntimeStateToken } from './github-runtime-state.js';
12
+ export const githubEndpointCommands = createEndpointCommands({
13
+ adapterKey: 'github',
14
+ adapterDisplayName: 'GitHub',
15
+ fields: [
16
+ { key: 'app_id', required: true, env: true, description: 'GitHub App ID' },
17
+ { key: 'private_key', required: true, description: 'PEM 文件路径(推荐,如 ./data/xxx.pem)或 PEM 内容' },
18
+ { key: 'webhook_secret', env: true, description: 'webhook 签名密钥(不配则 API-only)' },
19
+ ],
20
+ running: (use) => use(githubRuntimeStateToken).endpoints.values(),
21
+ describeEntry: (entry) => `app_id: ${String(entry.app_id ?? entry.appId)}`,
22
+ }, defineCommand);
@@ -0,0 +1 @@
1
+ export declare const githubRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * GitHub 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `github endpoint` 命令共享(同一 owner generation)。
4
+ */
5
+ import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
+ export const githubRuntimeStateToken = defineEndpointRuntimeStateToken('github');
@@ -5,7 +5,16 @@ import { spawn } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  const REPO_FULL_NAME_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
8
- const GIT_REF_ILLEGAL_RE = /[\s\x00-\x1f\x7f~^:?*[\\`;]/;
8
+ const GIT_REF_ILLEGAL_CHARS = new Set('~^:?*[\\`;');
9
+ function containsIllegalGitRefCharacter(ref) {
10
+ return [...ref].some((character) => {
11
+ const code = character.charCodeAt(0);
12
+ return code <= 31
13
+ || code === 127
14
+ || /\s/u.test(character)
15
+ || GIT_REF_ILLEGAL_CHARS.has(character);
16
+ });
17
+ }
9
18
  /**
10
19
  * 校验 GitHub "owner/name" 全名,防止路径穿越与 git 选项注入。
11
20
  * 返回通过校验的原值,便于调用方直接使用「已消毒」的变量。
@@ -27,7 +36,7 @@ export function assertGitRefName(ref) {
27
36
  }
28
37
  if (ref.startsWith('-') ||
29
38
  ref.includes('..') ||
30
- GIT_REF_ILLEGAL_RE.test(ref) ||
39
+ containsIllegalGitRefCharacter(ref) ||
31
40
  ref.endsWith('/') ||
32
41
  ref.endsWith('.')) {
33
42
  throw new TypeError(`非法的 git ref: ${JSON.stringify(ref)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-github",
3
- "version": "3.0.3",
3
+ "version": "4.0.1",
4
4
  "description": "Zhin.js GitHub adapter for Plugin Runtime (App auth + webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -41,20 +41,21 @@
41
41
  "directory": "plugins/adapters/github"
42
42
  },
43
43
  "dependencies": {
44
- "@zhin.js/adapter": "1.0.1",
45
- "@zhin.js/core": "1.3.5",
46
- "@zhin.js/host-http": "1.0.1",
44
+ "@zhin.js/adapter": "1.1.1",
45
+ "@zhin.js/command": "1.0.3",
46
+ "@zhin.js/core": "1.4.1",
47
+ "@zhin.js/host-http": "1.0.3",
47
48
  "@zhin.js/logger": "1.0.75",
48
- "@zhin.js/plugin-runtime": "1.0.1"
49
+ "@zhin.js/plugin-runtime": "1.1.1"
49
50
  },
50
51
  "peerDependencies": {
51
52
  "zod": "^4.0.0",
52
- "@zhin.js/adapter": "1.0.1",
53
- "@zhin.js/agent": "1.0.4",
54
- "@zhin.js/core": "1.3.5",
55
- "@zhin.js/host-http": "1.0.1",
56
- "@zhin.js/plugin-runtime": "1.0.1",
57
- "zhin.js": "4.1.3"
53
+ "@zhin.js/adapter": "1.1.1",
54
+ "@zhin.js/agent": "1.0.6",
55
+ "@zhin.js/core": "1.4.1",
56
+ "@zhin.js/host-http": "1.0.3",
57
+ "@zhin.js/plugin-runtime": "1.1.1",
58
+ "zhin.js": "5.0.1"
58
59
  },
59
60
  "peerDependenciesMeta": {
60
61
  "zhin.js": {
@@ -72,11 +73,12 @@
72
73
  "typescript": "^6.0.3",
73
74
  "vitest": "^4.1.10",
74
75
  "zod": "^4.4.3",
75
- "@zhin.js/agent": "1.0.4",
76
- "zhin.js": "4.1.3"
76
+ "@zhin.js/agent": "1.0.6",
77
+ "zhin.js": "5.0.1"
77
78
  },
78
79
  "files": [
79
80
  "adapters",
81
+ "commands",
80
82
  "plugin.ts",
81
83
  "schema.json",
82
84
  "src",
@@ -102,6 +104,10 @@
102
104
  {
103
105
  "package": "@zhin.js/adapter",
104
106
  "api": "^1.0.0"
107
+ },
108
+ {
109
+ "package": "@zhin.js/command",
110
+ "api": "^1.0.0"
105
111
  }
106
112
  ],
107
113
  "plugins": []
package/plugin.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { createEndpointRuntimeState } from '@zhin.js/adapter';
1
2
  import { definePlugin, databaseHostToken } from '@zhin.js/plugin-runtime';
3
+ import { githubRuntimeStateToken } from './src/github-runtime-state.js';
2
4
  import { defineGithubOauthUsersTable } from './src/oauth-users.js';
3
5
 
4
6
  /**
@@ -26,6 +28,8 @@ export default definePlugin({
26
28
  displayName: 'GitHub Adapter',
27
29
  },
28
30
  setup(context) {
31
+ // 运行中 endpoint 注册表(github endpoint list 的"运行中"数据源)
32
+ context.resources.provide(githubRuntimeStateToken, createEndpointRuntimeState());
29
33
  if (context.resources.has(databaseHostToken)) {
30
34
  const host = context.resources.use(databaseHostToken);
31
35
  defineGithubOauthUsersTable(host);
package/schema.json CHANGED
@@ -3,38 +3,10 @@
3
3
  "type": "object",
4
4
  "additionalProperties": false,
5
5
  "properties": {
6
- "name": {
7
- "type": "string",
8
- "default": "github-bot"
9
- },
10
6
  "host": {
11
7
  "type": "string",
12
8
  "description": "GitHub Enterprise hostname (default github.com)"
13
9
  },
14
- "app_id": {
15
- "type": ["string", "number"],
16
- "description": "GitHub App ID"
17
- },
18
- "appId": {
19
- "type": ["string", "number"],
20
- "description": "GitHub App ID (camelCase alias)"
21
- },
22
- "private_key": {
23
- "type": "string",
24
- "description": "GitHub App private key (PEM content or file path)"
25
- },
26
- "privateKey": {
27
- "type": "string",
28
- "description": "GitHub App private key (camelCase alias)"
29
- },
30
- "webhook_secret": {
31
- "type": "string",
32
- "description": "Webhook HMAC secret; enables httpHostToken POST route"
33
- },
34
- "webhookSecret": {
35
- "type": "string",
36
- "description": "Webhook HMAC secret (camelCase alias)"
37
- },
38
10
  "webhook_path": {
39
11
  "type": "string",
40
12
  "default": "/github/webhook"
@@ -50,7 +22,9 @@
50
22
  },
51
23
  "auto_reply_repos": {
52
24
  "type": "array",
53
- "items": { "type": "string" },
25
+ "items": {
26
+ "type": "string"
27
+ },
54
28
  "description": "Repos whose Issue/PR comments auto-trigger AI without @bot"
55
29
  },
56
30
  "bot_login": {
@@ -60,6 +34,61 @@
60
34
  "workspace_root": {
61
35
  "type": "string",
62
36
  "description": "Managed git workspace root"
37
+ },
38
+ "endpoints": {
39
+ "type": "array",
40
+ "description": "多账号:一个插件实例挂多个 endpoint",
41
+ "items": {
42
+ "type": "object",
43
+ "additionalProperties": true,
44
+ "properties": {
45
+ "name": {
46
+ "type": "string",
47
+ "description": "GitHub App bot name"
48
+ },
49
+ "app_id": {
50
+ "type": [
51
+ "string",
52
+ "number"
53
+ ],
54
+ "description": "GitHub App ID"
55
+ },
56
+ "appId": {
57
+ "type": [
58
+ "string",
59
+ "number"
60
+ ],
61
+ "description": "GitHub App ID (camelCase alias)"
62
+ },
63
+ "private_key": {
64
+ "type": "string",
65
+ "description": "GitHub App private key (PEM content or file path)"
66
+ },
67
+ "privateKey": {
68
+ "type": "string",
69
+ "description": "GitHub App private key (camelCase alias)"
70
+ },
71
+ "webhook_secret": {
72
+ "type": "string",
73
+ "description": "Webhook HMAC secret; enables httpHostToken POST route"
74
+ },
75
+ "webhookSecret": {
76
+ "type": "string",
77
+ "description": "Webhook HMAC secret (camelCase alias)"
78
+ }
79
+ },
80
+ "required": [
81
+ "name"
82
+ ]
83
+ }
84
+ },
85
+ "commandPrefix": {
86
+ "type": "string",
87
+ "default": "",
88
+ "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
63
89
  }
64
- }
90
+ },
91
+ "required": [
92
+ "endpoints"
93
+ ]
65
94
  }
package/src/endpoint.ts CHANGED
@@ -32,6 +32,10 @@ export interface GithubEndpointOptions {
32
32
  readonly createClient?: (config: ResolvedGithubConfig) => GhClient;
33
33
  }
34
34
 
35
+ /**
36
+ * GitHub 是代码协作面(issue/PR),无好友/群/频道等 IM 社交概念,
37
+ * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
38
+ */
35
39
  export class GithubEndpoint implements EndpointInstance {
36
40
  readonly #options: GithubEndpointOptions;
37
41
  readonly gh: GhClient;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `github endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
+ *
5
+ * 字段说明:private_key 内容长且含换行,不适合 kv 直传——运行时(gh-client
6
+ * resolvePrivateKey)本就支持 PEM 内容或文件路径,故 add 采用**内联路径**形式:
7
+ * `github endpoint add mybot app_id=123456 private_key=./data/mybot.pem`。
8
+ */
9
+ import { createEndpointCommands } from '@zhin.js/adapter';
10
+ import { defineCommand } from '@zhin.js/command';
11
+ import { githubRuntimeStateToken } from './github-runtime-state.js';
12
+
13
+ export const githubEndpointCommands = createEndpointCommands({
14
+ adapterKey: 'github',
15
+ adapterDisplayName: 'GitHub',
16
+ fields: [
17
+ { key: 'app_id', required: true, env: true, description: 'GitHub App ID' },
18
+ { key: 'private_key', required: true, description: 'PEM 文件路径(推荐,如 ./data/xxx.pem)或 PEM 内容' },
19
+ { key: 'webhook_secret', env: true, description: 'webhook 签名密钥(不配则 API-only)' },
20
+ ],
21
+ running: (use) => use(githubRuntimeStateToken).endpoints.values(),
22
+ describeEntry: (entry) => `app_id: ${String(entry.app_id ?? entry.appId)}`,
23
+ }, defineCommand);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * GitHub 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `github endpoint` 命令共享(同一 owner generation)。
4
+ */
5
+ import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
+
7
+ export const githubRuntimeStateToken = defineEndpointRuntimeStateToken('github');
@@ -7,7 +7,17 @@ import path from 'node:path';
7
7
  import type { GhClient, GitHubBotIdentity } from './gh-client.js';
8
8
 
9
9
  const REPO_FULL_NAME_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
10
- const GIT_REF_ILLEGAL_RE = /[\s\x00-\x1f\x7f~^:?*[\\`;]/;
10
+ const GIT_REF_ILLEGAL_CHARS = new Set('~^:?*[\\`;');
11
+
12
+ function containsIllegalGitRefCharacter(ref: string): boolean {
13
+ return [...ref].some((character) => {
14
+ const code = character.charCodeAt(0);
15
+ return code <= 31
16
+ || code === 127
17
+ || /\s/u.test(character)
18
+ || GIT_REF_ILLEGAL_CHARS.has(character);
19
+ });
20
+ }
11
21
 
12
22
  /**
13
23
  * 校验 GitHub "owner/name" 全名,防止路径穿越与 git 选项注入。
@@ -34,7 +44,7 @@ export function assertGitRefName(ref: string): string {
34
44
  if (
35
45
  ref.startsWith('-') ||
36
46
  ref.includes('..') ||
37
- GIT_REF_ILLEGAL_RE.test(ref) ||
47
+ containsIllegalGitRefCharacter(ref) ||
38
48
  ref.endsWith('/') ||
39
49
  ref.endsWith('.')
40
50
  ) {