@koishi-ce/plugin-bind 1.0.0 → 1.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 +93 -0
- package/lib/assets/{en-US-C0a1paCV.yml → en-US-B8YOKuiF.yml} +5 -0
- package/lib/assets/{zh-CN-kAS4medd.yml → zh-CN-DbUUJqH1.yml} +3 -0
- package/lib/index.d.ts +6 -7
- package/lib/index.mjs +2 -2
- package/locales/de-DE.yml +16 -11
- package/locales/en-US.yml +5 -0
- package/locales/fr-FR.yml +16 -11
- package/locales/ja-JP.yml +16 -11
- package/locales/ru-RU.yml +16 -11
- package/locales/zh-CN.yml +3 -0
- package/locales/zh-TW.yml +5 -0
- package/package.json +4 -5
- package/src/extra.test.ts +146 -0
- package/src/index.test.ts +40 -9
- package/src/index.ts +59 -19
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @koishi-ce/plugin-bind
|
|
2
|
+
|
|
3
|
+
**简体中文** | [English](#english)
|
|
4
|
+
|
|
5
|
+
跨平台账号绑定插件,移植自上游 [koishijs/koishi](https://github.com/koishijs/koishi) 的 `plugins/common/bind`。通过一次性令牌把不同平台的账号归入同一个用户,实现用户数据的跨平台互通。
|
|
6
|
+
|
|
7
|
+
## 指令
|
|
8
|
+
|
|
9
|
+
| 指令 | 权限 | 说明 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `bind` | 0 | 绑定到账号,`-r, --remove` 解除当前平台账号的绑定 |
|
|
12
|
+
|
|
13
|
+
绑定流程:
|
|
14
|
+
|
|
15
|
+
- 在目标平台(用户数据将被覆盖的一侧)执行 `bind`,获取一枚一次性令牌,默认形如 `koishi/123456`(前缀 `koishi/` 加 6 位数字),5 分钟内有效。
|
|
16
|
+
- 切换到原始平台(保留用户数据的一侧)向机器人发送令牌:私聊场景一步完成绑定;群聊场景会获得第二枚令牌,需回到目标平台再次发送令牌完成绑定。
|
|
17
|
+
- `bind -r` 解除当前平台账号的绑定。
|
|
18
|
+
|
|
19
|
+
## 配置项
|
|
20
|
+
|
|
21
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `tokenPrefix` | string | `koishi/` | 令牌前缀(未注册进控制台 schema,仅手工配置生效) |
|
|
24
|
+
| `generateToken` | function | 内置随机生成 | 自定义令牌生成函数(控制台中隐藏) |
|
|
25
|
+
|
|
26
|
+
## 用法
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun add @koishi-ce/plugin-bind
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
也可以在控制台的插件市场中直接安装。随后在配置文件中启用:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
plugins:
|
|
36
|
+
bind: {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 备注
|
|
40
|
+
|
|
41
|
+
- 依赖数据库服务,使用内置 `binding` 表与 `user` 表。
|
|
42
|
+
- 通过前置中间件拦截令牌文本驱动绑定流程;令牌一次性使用,在同一账号重复输入会提示前往对侧平台。
|
|
43
|
+
- 消费第一步令牌的账号(原始平台一侧)必须是已注册账号(authority 大于 0),未注册账号不能完成绑定。
|
|
44
|
+
- 解绑时,从其他用户迁移而来的账号会恢复其原初绑定;最后一个原初绑定不允许解绑,避免用户失去账号入口。
|
|
45
|
+
|
|
46
|
+
## 许可证
|
|
47
|
+
|
|
48
|
+
[MIT](https://github.com/Koishi-CE/koishi/blob/main/LICENSE)。本包是上游 [koishijs/koishi](https://github.com/koishijs/koishi) 的社区再分发,版权归属见 [NOTICE](https://github.com/Koishi-CE/koishi/blob/main/NOTICE)。
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## English
|
|
53
|
+
|
|
54
|
+
A cross-platform account binding plugin, ported from `plugins/common/bind` of the upstream [koishijs/koishi](https://github.com/koishijs/koishi) repository. It merges accounts from different platforms into a single user via one-time tokens.
|
|
55
|
+
|
|
56
|
+
## Commands
|
|
57
|
+
|
|
58
|
+
| Command | Authority | Description |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `bind` | 0 | Bind to an account; `-r, --remove` unbinds the current platform account |
|
|
61
|
+
|
|
62
|
+
Binding flow: run `bind` on the target platform (the side whose user data will be overwritten) to get a one-time token (by default `koishi/` plus 6 digits, valid for 5 minutes), then send the token to the bot on the original platform (the side whose data is kept). In direct chat the binding completes in one step; in groups a second token is issued and must be sent back on the target platform.
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
| Field | Type | Default | Description |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `tokenPrefix` | string | `koishi/` | Token prefix (not registered in the console schema; manual configuration only) |
|
|
69
|
+
| `generateToken` | function | built-in generator | Custom token generator (hidden in the console) |
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
bun add @koishi-ce/plugin-bind
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The plugin can also be installed from the console plugin market, then enabled in the config file:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
plugins:
|
|
81
|
+
bind: {}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Notes
|
|
85
|
+
|
|
86
|
+
- Requires the database service (built-in `binding` and `user` tables).
|
|
87
|
+
- A leading middleware intercepts token texts; tokens are single-use, and re-entering one on the same account prompts to use the other platform.
|
|
88
|
+
- The account consuming the first token must be registered (authority greater than 0).
|
|
89
|
+
- Unbinding restores migrated accounts to their original binding; the last original binding cannot be removed.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
[MIT](https://github.com/Koishi-CE/koishi/blob/main/LICENSE). This package is a community redistribution of upstream koishijs/koishi; see [NOTICE](https://github.com/Koishi-CE/koishi/blob/main/NOTICE) for attribution.
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
|
|
1
4
|
commands.bind:
|
|
2
5
|
description: Bind to account
|
|
3
6
|
options:
|
|
@@ -13,6 +16,8 @@ commands.bind:
|
|
|
13
16
|
Please use your account to send the following text to the bot on the target platform within 5 minutes:
|
|
14
17
|
{0}
|
|
15
18
|
Note: The current platform is your original platform, the user data here will overwrite the data from the target platform.
|
|
19
|
+
self-1: Please go to the original platform and send the text.
|
|
20
|
+
self-2: Please go to the target platform and send the text.
|
|
16
21
|
success: Account binding has been successful!
|
|
17
22
|
remove-success: Your account has been successfully unbundled!
|
|
18
23
|
remove-original: 'Cannot disconnect: This is your original account.'
|
package/lib/index.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { Context, Schema } from "@koishi-ce/koishi";
|
|
2
2
|
//#region src/index.d.ts
|
|
3
3
|
/** 配置项 */
|
|
4
|
-
interface Config {
|
|
4
|
+
export interface Config {
|
|
5
5
|
/** 令牌前缀,默认 `koishi/` */
|
|
6
6
|
tokenPrefix?: string;
|
|
7
7
|
/** 自定义令牌生成器(测试中可注入确定性实现) */
|
|
8
8
|
generateToken?: () => string;
|
|
9
9
|
}
|
|
10
|
-
declare const name = "bind";
|
|
11
|
-
declare const inject: string[];
|
|
12
|
-
declare const Config: Schema<Config>;
|
|
13
|
-
declare function apply(ctx: Context, config?: Config): void;
|
|
14
|
-
//#endregion
|
|
15
|
-
export { Config, apply, inject, name };
|
|
10
|
+
export declare const name = "bind";
|
|
11
|
+
export declare const inject: string[];
|
|
12
|
+
export declare const Config: Schema<Config>;
|
|
13
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
14
|
+
//#endregion
|
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema, Time } from "@koishi-ce/koishi";
|
|
2
|
-
import enUS from "./assets/en-US-
|
|
3
|
-
import zhCN from "./assets/zh-CN-
|
|
2
|
+
import enUS from "./assets/en-US-B8YOKuiF.yml";
|
|
3
|
+
import zhCN from "./assets/zh-CN-DbUUJqH1.yml";
|
|
4
4
|
//#region src/index.ts
|
|
5
5
|
const name = "bind";
|
|
6
6
|
const inject = ["database"];
|
package/locales/de-DE.yml
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
|
|
1
4
|
commands.bind:
|
|
2
|
-
description:
|
|
5
|
+
description: An Konto binden
|
|
3
6
|
options:
|
|
4
|
-
remove:
|
|
7
|
+
remove: Bindung aufheben
|
|
5
8
|
messages:
|
|
6
9
|
generated-1: |-
|
|
7
|
-
Bind
|
|
8
|
-
|
|
10
|
+
Der Befehl „Bind“ kann verwendet werden, um Benutzerdaten über mehrere Plattformen hinweg zu binden. Während des Bindungsvorgangs werden die Benutzerdaten der ursprünglichen Plattform vollständig erhalten, während die Benutzerdaten der Ziel-Plattform mit den Daten der ursprünglichen Plattform überschrieben werden.
|
|
11
|
+
Bitte stellen Sie sicher, dass die aktuelle Plattform Ihre Ziel-Plattform ist, und senden Sie innerhalb von 5 Minuten mit Ihrem Konto den folgenden Text an den Bot auf der ursprünglichen Plattform:
|
|
9
12
|
{0}
|
|
10
|
-
|
|
13
|
+
Nach Abschluss der Bindung können Sie den Status jederzeit mit „bind -r“ wieder aufheben.
|
|
11
14
|
generated-2: |-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
Token erfolgreich verifiziert! Nun folgt der zweite Schritt.
|
|
16
|
+
Bitte senden Sie innerhalb von 5 Minuten mit Ihrem Konto den folgenden Text an den Bot auf der Ziel-Plattform:
|
|
14
17
|
{0}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
Hinweis: Die aktuelle Plattform ist Ihre ursprüngliche Plattform. Die Benutzerdaten hier überschreiben die Daten der Ziel-Plattform.
|
|
19
|
+
self-1: Bitte geben Sie den Text auf der ursprünglichen Plattform ein.
|
|
20
|
+
self-2: Bitte geben Sie den Text auf der Ziel-Plattform ein.
|
|
21
|
+
success: Das Konto wurde erfolgreich gebunden!
|
|
22
|
+
remove-success: Die Bindung des Kontos wurde erfolgreich aufgehoben!
|
|
23
|
+
remove-original: 'Bindung kann nicht aufgehoben werden: Dies ist Ihr ursprüngliches Konto.'
|
package/locales/en-US.yml
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
|
|
1
4
|
commands.bind:
|
|
2
5
|
description: Bind to account
|
|
3
6
|
options:
|
|
@@ -13,6 +16,8 @@ commands.bind:
|
|
|
13
16
|
Please use your account to send the following text to the bot on the target platform within 5 minutes:
|
|
14
17
|
{0}
|
|
15
18
|
Note: The current platform is your original platform, the user data here will overwrite the data from the target platform.
|
|
19
|
+
self-1: Please go to the original platform and send the text.
|
|
20
|
+
self-2: Please go to the target platform and send the text.
|
|
16
21
|
success: Account binding has been successful!
|
|
17
22
|
remove-success: Your account has been successfully unbundled!
|
|
18
23
|
remove-original: 'Cannot disconnect: This is your original account.'
|
package/locales/fr-FR.yml
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
|
|
1
4
|
commands.bind:
|
|
2
|
-
description:
|
|
5
|
+
description: Lier au compte
|
|
3
6
|
options:
|
|
4
|
-
remove:
|
|
7
|
+
remove: Délier
|
|
5
8
|
messages:
|
|
6
9
|
generated-1: |-
|
|
7
|
-
Bind
|
|
8
|
-
|
|
10
|
+
La commande Bind permet de lier les données utilisateur entre plusieurs plateformes. Pendant le processus de liaison, les données utilisateur de la plateforme d'origine sont entièrement conservées, tandis que celles de la plateforme cible sont remplacées par celles de la plateforme d'origine.
|
|
11
|
+
Veuillez vérifier que la plateforme actuelle est votre plateforme cible, puis envoyez avec votre compte le texte suivant au bot sur la plateforme d'origine dans les 5 minutes :
|
|
9
12
|
{0}
|
|
10
|
-
|
|
13
|
+
Une fois la liaison terminée, vous pouvez à tout moment utiliser « bind -r » pour la délier.
|
|
11
14
|
generated-2: |-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
Jeton vérifié avec succès ! La deuxième étape va maintenant être effectuée.
|
|
16
|
+
Veuillez envoyer avec votre compte le texte suivant au bot sur la plateforme cible dans les 5 minutes :
|
|
14
17
|
{0}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
Remarque : la plateforme actuelle est votre plateforme d'origine. Les données utilisateur présentes ici remplaceront celles de la plateforme cible.
|
|
19
|
+
self-1: Veuillez saisir le texte sur la plateforme d'origine.
|
|
20
|
+
self-2: Veuillez saisir le texte sur la plateforme cible.
|
|
21
|
+
success: Le compte a été lié avec succès !
|
|
22
|
+
remove-success: Le compte a été délié avec succès !
|
|
23
|
+
remove-original: "Impossible de délier : il s'agit de votre compte d'origine."
|
package/locales/ja-JP.yml
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
|
|
1
4
|
commands.bind:
|
|
2
|
-
description:
|
|
5
|
+
description: アカウントにバインド
|
|
3
6
|
options:
|
|
4
|
-
remove:
|
|
7
|
+
remove: バインド解除
|
|
5
8
|
messages:
|
|
6
9
|
generated-1: |-
|
|
7
|
-
Bind
|
|
8
|
-
|
|
10
|
+
Bind コマンドは、複数のプラットフォーム間でユーザーデータをバインドするために使用できます。バインド中、元のプラットフォームのユーザーデータは完全に保持され、ターゲットプラットフォームのユーザーデータは元のプラットフォームのデータで上書きされます。
|
|
11
|
+
現在のプラットフォームがターゲットプラットフォームであることを確認し、5 分以内に元のプラットフォームでアカウントを使用してボットに以下のテキストを送信してください:
|
|
9
12
|
{0}
|
|
10
|
-
|
|
13
|
+
バインドが完了したら、いつでも「bind -r」でバインドを解除できます。
|
|
11
14
|
generated-2: |-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
トークンの検証に成功しました!次に第 2 ステップの操作を行います。
|
|
16
|
+
5 分以内にターゲットプラットフォームでアカウントを使用して、ボットに以下のテキストを送信してください:
|
|
14
17
|
{0}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
注意:現在のプラットフォームは元のプラットフォームです。ここのユーザーデータはターゲットプラットフォームのデータを上書きします。
|
|
19
|
+
self-1: 元のプラットフォームでテキストを送信してください。
|
|
20
|
+
self-2: ターゲットプラットフォームでテキストを送信してください。
|
|
21
|
+
success: アカウントのバインドに成功しました!
|
|
22
|
+
remove-success: アカウントのバインドを解除しました!
|
|
23
|
+
remove-original: バインドを解除できません:これは元のアカウントです。
|
package/locales/ru-RU.yml
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
|
|
1
4
|
commands.bind:
|
|
2
|
-
description:
|
|
5
|
+
description: Привязать к аккаунту
|
|
3
6
|
options:
|
|
4
|
-
remove:
|
|
7
|
+
remove: Отвязать
|
|
5
8
|
messages:
|
|
6
9
|
generated-1: |-
|
|
7
|
-
Bind
|
|
8
|
-
|
|
10
|
+
Команда Bind позволяет связать данные пользователя между несколькими платформами. Во время привязки данные пользователя исходной платформы полностью сохраняются, а данные пользователя целевой платформы перезаписываются данными исходной платформы.
|
|
11
|
+
Убедитесь, что текущая платформа является целевой, и в течение 5 минут отправьте со своего аккаунта следующий текст боту на исходной платформе:
|
|
9
12
|
{0}
|
|
10
|
-
|
|
13
|
+
После завершения привязки вы можете в любой момент отменить её с помощью «bind -r».
|
|
11
14
|
generated-2: |-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
Токен успешно проверен! Далее будет выполнен второй шаг.
|
|
16
|
+
В течение 5 минут отправьте со своего аккаунта следующий текст боту на целевой платформе:
|
|
14
17
|
{0}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
Обратите внимание: текущая платформа является исходной. Данные пользователя здесь перезапишут данные целевой платформы.
|
|
19
|
+
self-1: Пожалуйста, отправьте текст на исходной платформе.
|
|
20
|
+
self-2: Пожалуйста, отправьте текст на целевой платформе.
|
|
21
|
+
success: Аккаунт успешно привязан!
|
|
22
|
+
remove-success: Аккаунт успешно отвязан!
|
|
23
|
+
remove-original: 'Не удалось отвязать: это ваш исходный аккаунт.'
|
package/locales/zh-CN.yml
CHANGED
package/locales/zh-TW.yml
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
|
|
1
4
|
commands.bind:
|
|
2
5
|
description: 綁定到賬號
|
|
3
6
|
options:
|
|
@@ -13,6 +16,8 @@ commands.bind:
|
|
|
13
16
|
請在 5 分鐘內使用你的賬號在目標平台內向機器人發送以下文本:
|
|
14
17
|
{0}
|
|
15
18
|
注意:當前平台是你的原始平台,這裡的使用者數據將覆蓋目標平台的數據。
|
|
19
|
+
self-1: 請前往原始平台輸入。
|
|
20
|
+
self-2: 請前往目標平台輸入。
|
|
16
21
|
success: 帳號綁定成功!
|
|
17
22
|
remove-success: 賬號解綁成功!
|
|
18
23
|
remove-original: 無法解除綁定:這是你的原始賬號。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koishi-ce/plugin-bind",
|
|
3
3
|
"description": "Bind User Accounts Across Platforms in Koishi",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|
|
@@ -48,13 +48,12 @@
|
|
|
48
48
|
]
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"koishi": "^
|
|
51
|
+
"@koishi-ce/koishi": "^1.0.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@koishi-ce/koishi": "^1.0.8",
|
|
54
55
|
"@koishi-ce/plugin-mock": "^1.0.0",
|
|
55
|
-
"@
|
|
56
|
-
"@koishi-ce/koishi": "^1.0.0",
|
|
57
|
-
"minato": "^3.7.0"
|
|
56
|
+
"@koishijs/plugin-database-memory": "^3.7.0"
|
|
58
57
|
},
|
|
59
58
|
"exports": {
|
|
60
59
|
".": {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* bind 插件补充用例(bun:test 断言):
|
|
6
|
+
* 私聊令牌一步完成绑定、原初绑定不止一个时解绑另建新用户承接、
|
|
7
|
+
* 以及未注入生成器时走默认随机数字令牌路径。
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
afterAll,
|
|
11
|
+
beforeAll,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
it,
|
|
15
|
+
jest,
|
|
16
|
+
} from "bun:test";
|
|
17
|
+
import { Context } from "@koishi-ce/koishi";
|
|
18
|
+
import * as bind from "@koishi-ce/plugin-bind";
|
|
19
|
+
import mock from "@koishi-ce/plugin-mock";
|
|
20
|
+
import * as memoryModule from "@koishijs/plugin-database-memory";
|
|
21
|
+
|
|
22
|
+
// CJS 实现配 ESM 声明,Bun 互操作视图多包一层 default,穿透取真实驱动
|
|
23
|
+
const memory =
|
|
24
|
+
memoryModule.default as unknown as typeof memoryModule.default.default;
|
|
25
|
+
|
|
26
|
+
// 主应用:注入确定性令牌生成器,便于跨客户端传递令牌
|
|
27
|
+
const app = new Context();
|
|
28
|
+
|
|
29
|
+
let counter = 0;
|
|
30
|
+
|
|
31
|
+
app.plugin(bind, {
|
|
32
|
+
generateToken: () =>
|
|
33
|
+
`koishi/${(++counter).toString().padStart(6, "0")}`,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.plugin(mock);
|
|
37
|
+
app.plugin(memory);
|
|
38
|
+
|
|
39
|
+
// 群聊客户端(绑定发起方)与私聊客户端(令牌签发方)
|
|
40
|
+
const group = app.mock.client("123", "321");
|
|
41
|
+
const direct = app.mock.client("456");
|
|
42
|
+
|
|
43
|
+
// 副应用:不注入 generateToken,验证默认随机令牌生成器
|
|
44
|
+
const appPlain = new Context();
|
|
45
|
+
|
|
46
|
+
appPlain.plugin(bind);
|
|
47
|
+
appPlain.plugin(mock);
|
|
48
|
+
appPlain.plugin(memory);
|
|
49
|
+
|
|
50
|
+
const plain = appPlain.mock.client("111");
|
|
51
|
+
|
|
52
|
+
beforeAll(async () => {
|
|
53
|
+
await app.start();
|
|
54
|
+
await app.mock.initUser("123", 1);
|
|
55
|
+
await app.mock.initUser("456", 1);
|
|
56
|
+
await appPlain.start();
|
|
57
|
+
await appPlain.mock.initUser("111", 1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterAll(async () => {
|
|
61
|
+
await app.stop();
|
|
62
|
+
await appPlain.stop();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/** 从机器人文案中提取 koishi/ 前缀的六位数字令牌 */
|
|
66
|
+
function extractToken(message: string | undefined) {
|
|
67
|
+
const token = message?.match(/koishi\/\d{6}/)?.[0];
|
|
68
|
+
expect(token).toBeTypeOf("string");
|
|
69
|
+
return token as string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("@koishi-ce/plugin-bind 补充用例", () => {
|
|
73
|
+
it("私聊签发的令牌可一步完成绑定", async () => {
|
|
74
|
+
const replies = await direct.receive("bind");
|
|
75
|
+
const token = extractToken(replies[0]);
|
|
76
|
+
const [reply] = await group.receive(token);
|
|
77
|
+
expect(reply).toBe("账号绑定成功!");
|
|
78
|
+
// 私聊账号应并入群聊账号所属的内部用户
|
|
79
|
+
const [groupBinding] = await app.database.get(
|
|
80
|
+
"binding",
|
|
81
|
+
{ pid: "123" },
|
|
82
|
+
);
|
|
83
|
+
const [directBinding] = await app.database.get(
|
|
84
|
+
"binding",
|
|
85
|
+
{ pid: "456" },
|
|
86
|
+
);
|
|
87
|
+
// 前置 receive 已保证两条绑定存在,非空断言仅消除索引访问的 undefined 分支
|
|
88
|
+
expect(directBinding!.aid).toBe(groupBinding!.aid);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("原初绑定不止一个时解绑会另建新用户承接", async () => {
|
|
92
|
+
// 预置第二条原初绑定,使当前用户名下有两条 aid === bid 的记录
|
|
93
|
+
const [self] = await app.database.get("binding", {
|
|
94
|
+
pid: "123",
|
|
95
|
+
});
|
|
96
|
+
await app.database.create("binding", {
|
|
97
|
+
platform: "mock",
|
|
98
|
+
pid: "789",
|
|
99
|
+
aid: self!.aid,
|
|
100
|
+
bid: self!.aid,
|
|
101
|
+
});
|
|
102
|
+
const usersBefore = await app.database.get("user", {});
|
|
103
|
+
const [reply] = await group.receive("bind -r");
|
|
104
|
+
expect(reply).toBe("账号解绑成功!");
|
|
105
|
+
// 当前平台账号被划入新建用户名下,而非删除绑定记录
|
|
106
|
+
const usersAfter = await app.database.get("user", {});
|
|
107
|
+
expect(usersAfter.length).toBe(usersBefore.length + 1);
|
|
108
|
+
const [moved] = await app.database.get("binding", {
|
|
109
|
+
pid: "123",
|
|
110
|
+
});
|
|
111
|
+
expect(moved!.aid).not.toBe(self!.aid);
|
|
112
|
+
const [newcomer] = await app.database.get("user", {
|
|
113
|
+
id: moved!.aid,
|
|
114
|
+
});
|
|
115
|
+
expect(newcomer).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("第二步令牌签发者的绑定被删除时静默忽略", async () => {
|
|
119
|
+
// 群聊第一步:签发 phase 1 令牌
|
|
120
|
+
const [first] = await group.receive("bind");
|
|
121
|
+
const token1 = extractToken(first);
|
|
122
|
+
// 私聊第二步:核验成功并签发 phase -1 令牌
|
|
123
|
+
const [second] = await direct.receive(token1);
|
|
124
|
+
const token2 = extractToken(second);
|
|
125
|
+
// 模拟签发者账号的绑定入口被删除,令牌失效后无任何回复
|
|
126
|
+
await app.database.remove("binding", { pid: "456" });
|
|
127
|
+
const replies = await group.receive(token2);
|
|
128
|
+
expect(replies).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("令牌超过 5 分钟后自动过期失效", async () => {
|
|
132
|
+
jest.useFakeTimers();
|
|
133
|
+
const replies = await group.receive("bind");
|
|
134
|
+
const token = extractToken(replies[0]);
|
|
135
|
+
// 推进 5 分钟:过期回调删除令牌,此后该文本不再触发绑定流程
|
|
136
|
+
jest.advanceTimersByTime(5 * 60_000);
|
|
137
|
+
const again = await group.receive(token);
|
|
138
|
+
expect(again).toEqual([]);
|
|
139
|
+
jest.useRealTimers();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("未注入生成器时使用默认随机数字令牌", async () => {
|
|
143
|
+
const replies = await plain.receive("bind");
|
|
144
|
+
expect(replies[0]).toMatch(/koishi\/\d{6}/);
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/index.test.ts
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* bind 插件测试:用两个 mock 客户端模拟跨平台用户,
|
|
3
6
|
* 验证两步令牌绑定流程与解绑(含原初账号保护)。
|
|
4
7
|
*/
|
|
5
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
afterAll,
|
|
10
|
+
beforeAll,
|
|
11
|
+
describe,
|
|
12
|
+
it,
|
|
13
|
+
} from "bun:test";
|
|
6
14
|
import { Context } from "@koishi-ce/koishi";
|
|
7
15
|
import * as bind from "@koishi-ce/plugin-bind";
|
|
8
16
|
import mock from "@koishi-ce/plugin-mock";
|
|
9
|
-
import
|
|
17
|
+
import * as memoryModule from "@koishijs/plugin-database-memory";
|
|
18
|
+
|
|
19
|
+
// CJS 实现配 ESM 声明,Bun 互操作视图多包一层 default,穿透取真实驱动
|
|
20
|
+
const memory =
|
|
21
|
+
memoryModule.default as unknown as typeof memoryModule.default.default;
|
|
10
22
|
|
|
11
23
|
const app = new Context();
|
|
12
24
|
|
|
13
25
|
let counter = 0;
|
|
14
26
|
|
|
15
27
|
app.plugin(bind, {
|
|
16
|
-
generateToken: () =>
|
|
28
|
+
generateToken: () =>
|
|
29
|
+
`koishi/${(++counter).toString().padStart(6, "0")}`,
|
|
17
30
|
});
|
|
18
31
|
|
|
19
32
|
app.plugin(mock);
|
|
@@ -41,20 +54,38 @@ describe("@koishi-ce/plugin-bind", () => {
|
|
|
41
54
|
await client1.shouldReply("name", "foo");
|
|
42
55
|
await client2.shouldReply("name", "bar");
|
|
43
56
|
await client1.shouldReply("bind", /^koishi\/000001$/m);
|
|
44
|
-
await client1.shouldReply(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
await client1.shouldReply(
|
|
58
|
+
"koishi/000001",
|
|
59
|
+
"请前往原始平台输入。",
|
|
60
|
+
);
|
|
61
|
+
await client2.shouldReply(
|
|
62
|
+
"koishi/000001",
|
|
63
|
+
/^koishi\/000002$/m,
|
|
64
|
+
);
|
|
65
|
+
await client2.shouldReply(
|
|
66
|
+
"koishi/000002",
|
|
67
|
+
"请前往目标平台输入。",
|
|
68
|
+
);
|
|
69
|
+
await client1.shouldReply(
|
|
70
|
+
"koishi/000002",
|
|
71
|
+
"账号绑定成功!",
|
|
72
|
+
);
|
|
48
73
|
await client1.shouldReply("name", "bar");
|
|
49
74
|
await client2.shouldReply("name", "bar");
|
|
50
75
|
});
|
|
51
76
|
|
|
52
77
|
// 解绑:原初账号受保护不可解绑;迁移绑定的账号解绑后各回各的昵称
|
|
53
78
|
it("remove binding", async () => {
|
|
54
|
-
await client2.shouldReply(
|
|
79
|
+
await client2.shouldReply(
|
|
80
|
+
"bind -r",
|
|
81
|
+
"无法解除绑定:这是你的原始账号。",
|
|
82
|
+
);
|
|
55
83
|
await client1.shouldReply("bind -r", "账号解绑成功!");
|
|
56
84
|
await client1.shouldReply("name", "foo");
|
|
57
85
|
await client2.shouldReply("name", "bar");
|
|
58
|
-
await client1.shouldReply(
|
|
86
|
+
await client1.shouldReply(
|
|
87
|
+
"bind -r",
|
|
88
|
+
"无法解除绑定:这是你的原始账号。",
|
|
89
|
+
);
|
|
59
90
|
});
|
|
60
91
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* 账号绑定插件(bind)。
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
8
|
+
* 提供跨平台账号绑定:在目标平台(用户数据将被覆盖的一侧)执行 `bind`
|
|
9
|
+
* 获取一次性令牌,前往原始平台(保留用户数据的一侧)发送令牌完成绑定
|
|
10
|
+
* (群聊需要两步互发令牌,私聊一步到位)。
|
|
6
11
|
* 指令:`bind`(`-r` 解除当前平台账号的绑定)。
|
|
7
12
|
* 依赖数据库(binding / user 表),令牌默认 5 分钟过期。
|
|
8
13
|
*/
|
|
9
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
Context,
|
|
16
|
+
Dict,
|
|
17
|
+
Session,
|
|
18
|
+
} from "@koishi-ce/koishi";
|
|
10
19
|
import { Schema, Time } from "@koishi-ce/koishi";
|
|
11
20
|
import enUS from "../locales/en-US.yml";
|
|
12
21
|
import zhCN from "../locales/zh-CN.yml";
|
|
@@ -43,11 +52,17 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
43
52
|
ctx.i18n.define("en-US", enUS);
|
|
44
53
|
|
|
45
54
|
// 令牌 phase 含义:1 = 群聊第一步(源平台签发);0 = 私聊(一步完成);-1 = 群聊第二步(目标平台签发)
|
|
46
|
-
type TokenData = [
|
|
55
|
+
type TokenData = [
|
|
56
|
+
platform: string,
|
|
57
|
+
id: string,
|
|
58
|
+
phase: number,
|
|
59
|
+
];
|
|
47
60
|
const tokens: Dict<TokenData> = Object.create(null);
|
|
48
61
|
|
|
49
62
|
const { tokenPrefix: prefix = "koishi/" } = config;
|
|
50
|
-
const {
|
|
63
|
+
const {
|
|
64
|
+
generateToken = () => `${prefix}${randomDigits(6)}`,
|
|
65
|
+
} = config;
|
|
51
66
|
|
|
52
67
|
/** 为当前会话用户签发一次性令牌并记录 phase,5 分钟后自动过期 */
|
|
53
68
|
function generate(session: Session, phase: number) {
|
|
@@ -55,13 +70,24 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
55
70
|
if (!userId) return;
|
|
56
71
|
const token = generateToken();
|
|
57
72
|
tokens[token] = [session.platform, userId, phase];
|
|
58
|
-
ctx.setTimeout(
|
|
73
|
+
ctx.setTimeout(
|
|
74
|
+
() => delete tokens[token],
|
|
75
|
+
5 * Time.minute,
|
|
76
|
+
);
|
|
59
77
|
return token;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
/** 将平台账号(platform + pid)绑定到指定用户(aid)名下 */
|
|
63
|
-
async function bind(
|
|
64
|
-
|
|
81
|
+
async function bind(
|
|
82
|
+
aid: number,
|
|
83
|
+
platform: string,
|
|
84
|
+
pid: string,
|
|
85
|
+
) {
|
|
86
|
+
await ctx.database.set(
|
|
87
|
+
"binding",
|
|
88
|
+
{ platform, pid },
|
|
89
|
+
{ aid },
|
|
90
|
+
);
|
|
65
91
|
}
|
|
66
92
|
|
|
67
93
|
ctx
|
|
@@ -77,7 +103,8 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
77
103
|
aid: user.id,
|
|
78
104
|
});
|
|
79
105
|
const binding = bindings.find(
|
|
80
|
-
(item) =>
|
|
106
|
+
(item) =>
|
|
107
|
+
item.platform === platform && item.pid === pid,
|
|
81
108
|
);
|
|
82
109
|
if (!binding) return;
|
|
83
110
|
if (binding.aid !== binding.bid) {
|
|
@@ -85,7 +112,8 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
85
112
|
await bind(binding.bid, platform, pid);
|
|
86
113
|
return session.text(".remove-success");
|
|
87
114
|
} else if (
|
|
88
|
-
bindings.filter((item) => item.aid === item.bid)
|
|
115
|
+
bindings.filter((item) => item.aid === item.bid)
|
|
116
|
+
.length === 1
|
|
89
117
|
) {
|
|
90
118
|
// 原初绑定只剩最后一个时不允许解绑,否则用户将失去入口
|
|
91
119
|
return session.text(".remove-original");
|
|
@@ -94,7 +122,9 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
94
122
|
const authority = await session.resolve(
|
|
95
123
|
ctx.root.config.autoAuthorize,
|
|
96
124
|
);
|
|
97
|
-
const user = await ctx.database.create("user", {
|
|
125
|
+
const user = await ctx.database.create("user", {
|
|
126
|
+
authority,
|
|
127
|
+
});
|
|
98
128
|
await bind(user.id, platform, pid);
|
|
99
129
|
return session.text(".remove-success");
|
|
100
130
|
}
|
|
@@ -113,7 +143,10 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
113
143
|
const { userId } = session;
|
|
114
144
|
if (!userId) return next();
|
|
115
145
|
// 令牌在同一账号上被重复使用:提示不要在同一平台输入
|
|
116
|
-
if (
|
|
146
|
+
if (
|
|
147
|
+
data[0] === session.platform &&
|
|
148
|
+
data[1] === session.userId
|
|
149
|
+
) {
|
|
117
150
|
return session.text(
|
|
118
151
|
`commands.bind.messages.self-${data[2] < 0 ? "2" : "1"}`,
|
|
119
152
|
);
|
|
@@ -131,20 +164,27 @@ export function apply(ctx: Context, config: Config = {}) {
|
|
|
131
164
|
return session.text("commands.bind.messages.success");
|
|
132
165
|
} else {
|
|
133
166
|
// 第一步令牌 / 私聊令牌:以当前用户为主体,把令牌签发账号并入自己
|
|
134
|
-
const user = await ctx.database.getUser(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
167
|
+
const user = await ctx.database.getUser(
|
|
168
|
+
session.platform,
|
|
169
|
+
userId,
|
|
170
|
+
["id", "authority"],
|
|
171
|
+
);
|
|
138
172
|
// 未注册(无 authority)的账号无权拉人绑定
|
|
139
|
-
if (!user.authority)
|
|
173
|
+
if (!user.authority)
|
|
174
|
+
return session.text("internal.low-authority");
|
|
140
175
|
if (data[2]) {
|
|
141
176
|
// 群聊第一步:签发第二枚令牌,请对方回原平台输入
|
|
142
177
|
const token = generate(session, -1);
|
|
143
|
-
return session.text(
|
|
178
|
+
return session.text(
|
|
179
|
+
"commands.bind.messages.generated-2",
|
|
180
|
+
[token],
|
|
181
|
+
);
|
|
144
182
|
} else {
|
|
145
183
|
// 私聊一步:直接把令牌签发账号并入当前用户
|
|
146
184
|
await bind(user.id, data[0], data[1]);
|
|
147
|
-
return session.text(
|
|
185
|
+
return session.text(
|
|
186
|
+
"commands.bind.messages.success",
|
|
187
|
+
);
|
|
148
188
|
}
|
|
149
189
|
}
|
|
150
190
|
}, true);
|