@dsh-plugin/dsh-synapse 0.0.0-stage → 0.1.0-dev.41044818627
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/LICENSE +661 -0
- package/NOTICE.md +18 -0
- package/README.md +136 -2
- package/cordis.patch.yml +3 -0
- package/lib/bot.cjs +8314 -0
- package/lib/bot.cjs.map +7 -0
- package/lib/build.json +1 -0
- package/lib/client.js +46 -0
- package/lib/host/agent.d.ts +21 -0
- package/lib/host/agent.js +151 -0
- package/lib/host/agent.js.map +1 -0
- package/lib/host/bot.d.ts +17 -0
- package/lib/host/bot.js +158 -0
- package/lib/host/bot.js.map +1 -0
- package/lib/host/client.d.ts +2 -0
- package/lib/host/client.js +19 -0
- package/lib/host/client.js.map +1 -0
- package/lib/host/config.d.ts +24 -0
- package/lib/host/config.js +192 -0
- package/lib/host/config.js.map +1 -0
- package/lib/host/files.d.ts +29 -0
- package/lib/host/files.js +188 -0
- package/lib/host/files.js.map +1 -0
- package/lib/host/gateway.d.ts +33 -0
- package/lib/host/gateway.js +237 -0
- package/lib/host/gateway.js.map +1 -0
- package/lib/host/github.d.ts +47 -0
- package/lib/host/github.js +219 -0
- package/lib/host/github.js.map +1 -0
- package/lib/host/http.d.ts +4 -0
- package/lib/host/http.js +41 -0
- package/lib/host/http.js.map +1 -0
- package/lib/host/index.d.ts +32 -0
- package/lib/host/index.js +192 -0
- package/lib/host/index.js.map +1 -0
- package/lib/host/inference-worker.d.ts +1 -0
- package/lib/host/inference-worker.js +72 -0
- package/lib/host/inference-worker.js.map +1 -0
- package/lib/host/mcp.d.ts +138 -0
- package/lib/host/mcp.js +104 -0
- package/lib/host/mcp.js.map +1 -0
- package/lib/host/memory.d.ts +26 -0
- package/lib/host/memory.js +155 -0
- package/lib/host/memory.js.map +1 -0
- package/lib/host/panel.d.ts +1 -0
- package/lib/host/panel.html +174 -0
- package/lib/host/panel.js +388 -0
- package/lib/host/panel.js.map +1 -0
- package/lib/host/runtime.d.ts +21 -0
- package/lib/host/runtime.js +145 -0
- package/lib/host/runtime.js.map +1 -0
- package/lib/host/types.d.ts +112 -0
- package/lib/host/types.js +2 -0
- package/lib/host/types.js.map +1 -0
- package/package.json +94 -4
- package/scripts/build.mjs +90 -0
- package/scripts/check.mjs +13 -0
- package/scripts/dump-npm-latest.mjs +49 -0
- package/scripts/model-smoke.mjs +25 -0
- package/src/agent.ts +203 -0
- package/src/bot.ts +173 -0
- package/src/client.ts +20 -0
- package/src/config.ts +217 -0
- package/src/files.ts +206 -0
- package/src/gateway.ts +276 -0
- package/src/github.ts +263 -0
- package/src/http.ts +41 -0
- package/src/index.ts +227 -0
- package/src/inference-worker.ts +89 -0
- package/src/mcp.ts +112 -0
- package/src/memory.ts +178 -0
- package/src/panel.html +174 -0
- package/src/panel.ts +440 -0
- package/src/runtime.ts +164 -0
- package/src/types.ts +106 -0
- package/test/core.test.mjs +279 -0
- package/test/fixtures/mcp-server.mjs +14 -0
- package/test/integration.test.mjs +253 -0
- package/test/panel.test.mjs +73 -0
- package/tsconfig.json +18 -0
- package/vendor/gugle-chat-bot/LICENSE +661 -0
- package/vendor/gugle-chat-bot/package.json +57 -0
- package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -0
- package/vendor/gugle-chat-bot/src/api.json +4285 -0
- package/vendor/gugle-chat-bot/src/command/index.ts +141 -0
- package/vendor/gugle-chat-bot/src/config/features.ts +548 -0
- package/vendor/gugle-chat-bot/src/config/index.ts +184 -0
- package/vendor/gugle-chat-bot/src/config/manager.ts +357 -0
- package/vendor/gugle-chat-bot/src/constants/index.ts +13 -0
- package/vendor/gugle-chat-bot/src/custom.ts +548 -0
- package/vendor/gugle-chat-bot/src/event/index.ts +118 -0
- package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -0
- package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -0
- package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -0
- package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -0
- package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -0
- package/vendor/gugle-chat-bot/src/features/bili/template/video.html +57 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -0
- package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -0
- package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -0
- package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -0
- package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -0
- package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -0
- package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -0
- package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +469 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -0
- package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -0
- package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -0
- package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -0
- package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -0
- package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -0
- package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -0
- package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -0
- package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -0
- package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -0
- package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -0
- package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -0
- package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -0
- package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -0
- package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -0
- package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -0
- package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -0
- package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +452 -0
- package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -0
- package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +78 -0
- package/vendor/gugle-chat-bot/src/features/welcome/index.ts +58 -0
- package/vendor/gugle-chat-bot/src/github-ip.json +5814 -0
- package/vendor/gugle-chat-bot/src/image/index.ts +278 -0
- package/vendor/gugle-chat-bot/src/index.ts +695 -0
- package/vendor/gugle-chat-bot/src/logger/index.ts +54 -0
- package/vendor/gugle-chat-bot/src/result.json +359 -0
- package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -0
- package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -0
- package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -0
- package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -0
- package/vendor/gugle-chat-bot/tsconfig.json +46 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { CommandManager, CommandSource } from 'gugle-command';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 平台无关的命令源抽象。
|
|
5
|
+
*
|
|
6
|
+
* 背景:`gugle-command` 的 `CommandSource` 只有 success/fail/getName/hasPermission 四个方法,
|
|
7
|
+
* 命令实现若直接 `instanceof GroupMsgCommandSource` 判定,就只能服务 QQ 群消息。
|
|
8
|
+
* 这里扩展出跨平台需要的最小能力集,让同一套命令实现既能在 QQ 群、也能在 Discord 频道执行。
|
|
9
|
+
*/
|
|
10
|
+
export interface BotCommandSource extends CommandSource {
|
|
11
|
+
/**
|
|
12
|
+
* 调用者平台内唯一标识:
|
|
13
|
+
* - QQ 群消息:纯数字 QQ 号字符串(如 `2308465862`)
|
|
14
|
+
* - Discord 消息:`dc:<用户ID>`(如 `dc:123456789012345678`)
|
|
15
|
+
*
|
|
16
|
+
* 用于 GitHub 绑定等需要持久化用户身份的场景;`dc:` 前缀保证与纯数字 QQ 号不会冲突。
|
|
17
|
+
*/
|
|
18
|
+
getUserId(): string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 消息所在群/频道在当前平台上对应的互通 QQ 群号。
|
|
22
|
+
* QQ 群消息返回自身群号;Discord 频道返回该频道桥接配置里的 QQ 群号;
|
|
23
|
+
* 无归属群(未配置互通的 Discord 频道等)返回 undefined。
|
|
24
|
+
*/
|
|
25
|
+
getGroupId(): number | undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 是否为管理员:QQ 侧复用 management operators 白名单;
|
|
29
|
+
* Discord 侧为服务器拥有者或拥有管理服务器/管理员权限者。
|
|
30
|
+
*/
|
|
31
|
+
isAdmin(): boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 判断命令源是否具备 BotCommandSource 能力(鸭子类型,跨模块实例也成立) */
|
|
35
|
+
export function isBotCommandSource(source: CommandSource | undefined): source is BotCommandSource {
|
|
36
|
+
if (!source) return false;
|
|
37
|
+
const candidate = source as Partial<BotCommandSource>;
|
|
38
|
+
return (
|
|
39
|
+
typeof candidate.getUserId === 'function' &&
|
|
40
|
+
typeof candidate.getGroupId === 'function' &&
|
|
41
|
+
typeof candidate.isAdmin === 'function'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 由命令源推导「操作者标识」,供需要区分平台的管理逻辑使用。
|
|
47
|
+
*
|
|
48
|
+
* 背景:`executeCommand` 会把命令源包进 `FailOnceCommandSource` 代理,
|
|
49
|
+
* 命令实现里 `source instanceof GroupMsgCommandSource` 恒为 false——曾因此让
|
|
50
|
+
* QQ 管理员执行 /pardon 被误判成 Discord 用户而失败。这里改为按 BotCommandSource
|
|
51
|
+
* 的 id 约定判定,不依赖实例类型:
|
|
52
|
+
* - 纯数字 → QQ 号,返回 number(Management 内部再校验 operators 白名单);
|
|
53
|
+
* - `dc:<用户ID>` → Discord 用户,原样返回字符串(服务器管理员身份已在命令源层校验)。
|
|
54
|
+
*/
|
|
55
|
+
export function resolveOperatorId(source: BotCommandSource): number | string {
|
|
56
|
+
const userId = source.getUserId();
|
|
57
|
+
return /^\d+$/.test(userId) ? Number(userId) : userId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 把命令文本规范化为 gugle-command 可执行的形式。
|
|
62
|
+
*
|
|
63
|
+
* gugle-command 固定使用 `/` 前缀;Discord 客户端会把以 `/` 开头的内容当作斜杠命令输入,
|
|
64
|
+
* 因此 Discord 侧额外支持 `!` 前缀作为兜底,统一在这里转换。
|
|
65
|
+
*/
|
|
66
|
+
export function normalizeCommandText(text: string): string {
|
|
67
|
+
return text.startsWith('!') ? `/${text.slice(1)}` : text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 失败去重代理:同一命令执行周期内,`fail` 只向用户上报第一次。
|
|
72
|
+
*
|
|
73
|
+
* 背景:gugle-command 的 `CommandNode.parse()` 在深层匹配失败时已调用一次
|
|
74
|
+
* `source.fail('Invalid command')`,`CommandManager.execute()` 遍历完所有命名空间后
|
|
75
|
+
* 又兜底调用一次——`/help abc` 这类「命令存在但参数多余」的输入会向用户回复两条
|
|
76
|
+
* Invalid command。代理只放行第一次 fail(success 不受影响),修复重复回复。
|
|
77
|
+
*
|
|
78
|
+
* 注意:getUserId/getGroupId/isAdmin 按内部源是否有该方法条件绑定,
|
|
79
|
+
* 保证 isBotCommandSource 鸭子类型检查对代理前后结果一致。
|
|
80
|
+
*/
|
|
81
|
+
class FailOnceCommandSource implements CommandSource {
|
|
82
|
+
private readonly inner: CommandSource;
|
|
83
|
+
private failed = false;
|
|
84
|
+
|
|
85
|
+
public getUserId?: () => string;
|
|
86
|
+
public getGroupId?: () => number | undefined;
|
|
87
|
+
public isAdmin?: () => boolean;
|
|
88
|
+
|
|
89
|
+
public constructor(inner: CommandSource) {
|
|
90
|
+
this.inner = inner;
|
|
91
|
+
if (isBotCommandSource(inner)) {
|
|
92
|
+
this.getUserId = () => inner.getUserId();
|
|
93
|
+
this.getGroupId = () => inner.getGroupId();
|
|
94
|
+
this.isAdmin = () => inner.isAdmin();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public success(message: string): void {
|
|
99
|
+
this.inner.success(message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public fail(message: string): void {
|
|
103
|
+
if (this.failed) return;
|
|
104
|
+
this.failed = true;
|
|
105
|
+
this.inner.fail(message);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public getName(): string {
|
|
109
|
+
return this.inner.getName();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public hasPermission(permission: string): boolean {
|
|
113
|
+
return this.inner.hasPermission(permission);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 执行命令文本(经 FailOnce 代理,重复失败提示只上报一次)。
|
|
119
|
+
* 所有命令分发入口(QQ 群、Discord 文本、Discord 交互)统一走这里。
|
|
120
|
+
*/
|
|
121
|
+
export function executeCommand(manager: CommandManager, source: CommandSource, command: string): void {
|
|
122
|
+
manager.execute(new FailOnceCommandSource(source), command);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 以精确 token 序列执行命令(不经字符串切分)。
|
|
127
|
+
*
|
|
128
|
+
* 用途:Discord 原生斜杠命令交互——参数值由 Discord 客户端按 option 给出,
|
|
129
|
+
* 可能包含空格(如 /wiki 的查询词),若拼回字符串再经 gugle-command 按空格切分
|
|
130
|
+
* 会被打散。这里把 token 逆序后直接喂给各命名空间根节点的 parse:
|
|
131
|
+
* parse 逐 token 弹出匹配,字符串参数保持完整。
|
|
132
|
+
*
|
|
133
|
+
* @param tokens 完整 token 序列,如 ['github', 'bind', 'Gugle'] 或 ['wiki', '下界 合金']
|
|
134
|
+
*/
|
|
135
|
+
export function executeCommandTokens(manager: CommandManager, source: CommandSource, tokens: string[]): void {
|
|
136
|
+
const wrapped = new FailOnceCommandSource(source);
|
|
137
|
+
for (const root of manager.roots.values()) {
|
|
138
|
+
if (root.parse([...tokens].reverse(), wrapped)) return;
|
|
139
|
+
}
|
|
140
|
+
wrapped.fail('Invalid command');
|
|
141
|
+
}
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import Constants from '@/constants';
|
|
3
|
+
import { readConfigFile, writeConfigFile, ConfigStore, MigrationStep } from '@/config/manager';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// 功能模块独立配置文件注册表
|
|
7
|
+
//
|
|
8
|
+
// 历史背景:所有功能的群聊/操作人白名单原先挤在 bot-config.json 的
|
|
9
|
+
// `function*` 字段里。现已拆分为各功能自己的配置文件(configs/features/*.json),
|
|
10
|
+
// 每个文件带 version 字段,未来的结构变更通过 versions/migrations 升级。
|
|
11
|
+
//
|
|
12
|
+
// 本模块职责:
|
|
13
|
+
// 1. 声明每个功能的独立配置文件路径、默认值、版本与迁移步骤
|
|
14
|
+
// 2. 提供 getFeatureConfig / getFeatureGroups 访问生效配置(ConfigStore 热重载)
|
|
15
|
+
// 3. 提供 migrateLegacyFunctionFields() 把 bot-config.json 里的旧 function*
|
|
16
|
+
// 字段一次性拆进各功能配置文件,并从 bot-config.json 移除
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface FeatureConfig {
|
|
20
|
+
version: number;
|
|
21
|
+
/** 生效群聊 */
|
|
22
|
+
groups: number[];
|
|
23
|
+
/** 操作人白名单(如管理功能的管理员 QQ) */
|
|
24
|
+
operators?: number[];
|
|
25
|
+
/**
|
|
26
|
+
* Discord 管理员白名单(Discord 用户 ID,可带 `dc:` 前缀)。
|
|
27
|
+
*
|
|
28
|
+
* 用途:桥接频道里执行管理员命令时,除「服务器拥有者/管理员权限」外,
|
|
29
|
+
* 额外允许这些用户。Discord 用户 ID 为 snowflake 字符串,不能塞进 operators(number[])。
|
|
30
|
+
*
|
|
31
|
+
* 示例(configs/features/management.json):
|
|
32
|
+
* ```jsonc
|
|
33
|
+
* {
|
|
34
|
+
* "version": 1,
|
|
35
|
+
* "groups": [703453948],
|
|
36
|
+
* "operators": [2308465862], // QQ 管理员
|
|
37
|
+
* "discordOperators": ["123456789012345678"] // Discord 管理员(可写 dc: 前缀)
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
discordOperators?: string[];
|
|
42
|
+
/** 其他自定义字段(如 Bili 关注列表) */
|
|
43
|
+
extra?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FeatureDefinition {
|
|
47
|
+
/** 功能 id,对应 configs/features/<id>.json */
|
|
48
|
+
id: string;
|
|
49
|
+
/** 默认群聊 */
|
|
50
|
+
defaultGroups: number[];
|
|
51
|
+
/** 默认操作人白名单 */
|
|
52
|
+
defaultOperators?: number[];
|
|
53
|
+
/** bot-config.json 中的旧字段名(迁移用) */
|
|
54
|
+
legacyGroupsFields: string[];
|
|
55
|
+
/** bot-config.json 中旧操作人字段名(迁移用) */
|
|
56
|
+
legacyOperatorsField?: string;
|
|
57
|
+
/** 其他旧字段名 → 迁移到 extra 的键 */
|
|
58
|
+
|
|
59
|
+
legacyExtraFields?: Record<string, string>;
|
|
60
|
+
/** 当前配置版本 */
|
|
61
|
+
version?: number;
|
|
62
|
+
/** 迁移步骤 */
|
|
63
|
+
migrations?: import('@/config/manager').MigrationStep[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* GitHub 功能配置(v2)。
|
|
68
|
+
*
|
|
69
|
+
* groups 从 v1 的 `number[]`(全仓库白名单群)升级为
|
|
70
|
+
* `Record<仓库 full_name, 订阅该仓库的群号[]>`——webhook 事件只推送到
|
|
71
|
+
* 对应仓库订阅的群,实现"仓库级订阅"。
|
|
72
|
+
*/
|
|
73
|
+
export interface GithubFeatureConfig {
|
|
74
|
+
version: number;
|
|
75
|
+
/** 仓库 full_name(如 Anvil-Dev/AnvilCraft)→ 订阅群号列表 */
|
|
76
|
+
groups: Record<string, number[]>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 新人欢迎配置(v1)。
|
|
81
|
+
*
|
|
82
|
+
* configs/features/welcome.json:
|
|
83
|
+
* ```json
|
|
84
|
+
* {
|
|
85
|
+
* "version": 1,
|
|
86
|
+
* "welcomes": [
|
|
87
|
+
* {
|
|
88
|
+
* "group": [123456, 234567],
|
|
89
|
+
* "msg": ["欢迎 ${at} 加入群聊", "请查看群公告"]
|
|
90
|
+
* }
|
|
91
|
+
* ]
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
* `msg` 可以是字符串或字符串数组(数组每项一行,发送时按换行拼接)。
|
|
95
|
+
*/
|
|
96
|
+
export interface WelcomeFeatureConfig {
|
|
97
|
+
version: number;
|
|
98
|
+
welcomes: WelcomeEntry[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 单条欢迎配置:适用的群列表 + 欢迎语模板(${at} 替换为 @新成员;支持多行数组) */
|
|
102
|
+
export interface WelcomeEntry {
|
|
103
|
+
group: number[];
|
|
104
|
+
msg: string | string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface LegacyFunctionValues {
|
|
108
|
+
featureId: string;
|
|
109
|
+
groups?: number[];
|
|
110
|
+
operators?: number[];
|
|
111
|
+
extra?: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const FEATURES_DIR = path.resolve(process.cwd(), 'configs', 'features');
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// 功能定义表
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export const FEATURE_DEFINITIONS: FeatureDefinition[] = [
|
|
121
|
+
{
|
|
122
|
+
id: 'github',
|
|
123
|
+
defaultGroups: [],
|
|
124
|
+
legacyGroupsFields: ['functionGithubGroup']
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'management',
|
|
128
|
+
defaultGroups: [],
|
|
129
|
+
defaultOperators: [],
|
|
130
|
+
legacyGroupsFields: ['functionManagementGroup'],
|
|
131
|
+
legacyOperatorsField: 'functionManagementOperator'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'parentheses',
|
|
135
|
+
defaultGroups: [],
|
|
136
|
+
legacyGroupsFields: ['functionParenthesesGroup']
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'poke',
|
|
140
|
+
defaultGroups: [],
|
|
141
|
+
legacyGroupsFields: ['functionPokeGroup']
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'minecraft',
|
|
145
|
+
defaultGroups: [],
|
|
146
|
+
legacyGroupsFields: ['functionMinecraftGroup']
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'modrinth',
|
|
150
|
+
defaultGroups: [],
|
|
151
|
+
legacyGroupsFields: ['functionModrinthGroup']
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'bili',
|
|
155
|
+
defaultGroups: [],
|
|
156
|
+
legacyGroupsFields: ['functionBiliGroup'],
|
|
157
|
+
legacyExtraFields: { functionBiliFollow: 'follow' }
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'command',
|
|
161
|
+
defaultGroups: [],
|
|
162
|
+
legacyGroupsFields: ['functionCommandGroup']
|
|
163
|
+
}
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// 配置仓库(ConfigStore:独立文件 + 热重载)
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const stores = new Map<string, ConfigStore<FeatureConfig>>();
|
|
171
|
+
|
|
172
|
+
function isNumberArray(value: unknown): value is number[] {
|
|
173
|
+
return Array.isArray(value) && value.every(item => typeof item === 'number' && Number.isSafeInteger(item));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isGithubGroups(
|
|
177
|
+
value: unknown
|
|
178
|
+
): value is Record<string, number[]> {
|
|
179
|
+
return (
|
|
180
|
+
!!value &&
|
|
181
|
+
typeof value === 'object' &&
|
|
182
|
+
!Array.isArray(value) &&
|
|
183
|
+
Object.entries(value as Record<string, unknown>).every(
|
|
184
|
+
([repo, groups]) =>
|
|
185
|
+
typeof repo === 'string' && repo.includes('/') && isNumberArray(groups)
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** github.json 的默认工厂:v2 空订阅(无仓库订阅) */
|
|
191
|
+
function githubFactory(): GithubFeatureConfig {
|
|
192
|
+
return { version: 2, groups: {} };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** github.json 的 v1 → v2 迁移:旧 groups 数组(全仓库白名单群)拆给默认仓库 */
|
|
196
|
+
function migrateGithubV1ToV2(data: Record<string, unknown>): Record<string, unknown> {
|
|
197
|
+
const legacyGroups = isNumberArray(data.groups) ? data.groups : [];
|
|
198
|
+
return {
|
|
199
|
+
groups: legacyGroups.length > 0 ? { 'Anvil-Dev/AnvilCraft': legacyGroups } : {}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** github.json 规范化:接受 v2 Record;非法/损坏返回 null 触发备份重建 */
|
|
204
|
+
function normalizeGithubConfig(raw: unknown): GithubFeatureConfig | null {
|
|
205
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
206
|
+
const record = raw as Record<string, unknown>;
|
|
207
|
+
if (isGithubGroups(record.groups)) {
|
|
208
|
+
return { version: 2, groups: record.groups };
|
|
209
|
+
}
|
|
210
|
+
// v1 遗留(groups 为数组):转为 v2 结构(不落盘,读取时动态转换)
|
|
211
|
+
if (isNumberArray(record.groups)) {
|
|
212
|
+
const groups: Record<string, number[]> =
|
|
213
|
+
record.groups.length > 0 ? { 'Anvil-Dev/AnvilCraft': record.groups } : {};
|
|
214
|
+
return { version: 2, groups };
|
|
215
|
+
}
|
|
216
|
+
return { version: 2, groups: {} };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeFeatureConfig(def: FeatureDefinition, raw: unknown): FeatureConfig | null {
|
|
220
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
221
|
+
const record = raw as Record<string, unknown>;
|
|
222
|
+
const groups = record.groups !== undefined && isNumberArray(record.groups) ? record.groups : def.defaultGroups;
|
|
223
|
+
const operators =
|
|
224
|
+
record.operators !== undefined
|
|
225
|
+
? isNumberArray(record.operators)
|
|
226
|
+
? record.operators
|
|
227
|
+
: def.defaultOperators
|
|
228
|
+
: def.defaultOperators;
|
|
229
|
+
const extra =
|
|
230
|
+
record.extra !== undefined && record.extra && typeof record.extra === 'object' && !Array.isArray(record.extra)
|
|
231
|
+
? (record.extra as Record<string, unknown>)
|
|
232
|
+
: undefined;
|
|
233
|
+
// Discord 用户 ID:snowflake 字符串,允许带 dc: 前缀;非法项剔除
|
|
234
|
+
const discordOperators = Array.isArray(record.discordOperators)
|
|
235
|
+
? record.discordOperators
|
|
236
|
+
.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
|
|
237
|
+
.map(item => normalizeDiscordUserId(item))
|
|
238
|
+
: undefined;
|
|
239
|
+
const config: FeatureConfig = {
|
|
240
|
+
version: def.version ?? 1,
|
|
241
|
+
groups,
|
|
242
|
+
...(operators ? { operators } : {}),
|
|
243
|
+
...(discordOperators ? { discordOperators } : {}),
|
|
244
|
+
...(extra ? { extra } : {})
|
|
245
|
+
};
|
|
246
|
+
return config;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 归一化 Discord 用户 ID:去掉 dc: 前缀与首尾空白,统一存裸 snowflake */
|
|
250
|
+
export function normalizeDiscordUserId(raw: string): string {
|
|
251
|
+
return raw.trim().replace(/^dc:/i, '');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function createStore(def: FeatureDefinition): ConfigStore<FeatureConfig> {
|
|
255
|
+
const filePath = path.join('configs', 'features', `${def.id}.json`);
|
|
256
|
+
return new ConfigStore<FeatureConfig>({
|
|
257
|
+
path: filePath,
|
|
258
|
+
factory: () => ({
|
|
259
|
+
version: def.version ?? 1,
|
|
260
|
+
groups: def.defaultGroups,
|
|
261
|
+
...(def.defaultOperators ? { operators: def.defaultOperators } : {}),
|
|
262
|
+
...(def.legacyExtraFields ? { extra: {} } : {})
|
|
263
|
+
}),
|
|
264
|
+
version: def.version ?? 1,
|
|
265
|
+
...(def.migrations ? { migrations: def.migrations } : {}),
|
|
266
|
+
normalize: raw => normalizeFeatureConfig(def, raw)
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getStore(def: FeatureDefinition): ConfigStore<FeatureConfig> {
|
|
271
|
+
let store = stores.get(def.id);
|
|
272
|
+
if (!store) {
|
|
273
|
+
store = createStore(def);
|
|
274
|
+
stores.set(def.id, store);
|
|
275
|
+
}
|
|
276
|
+
return store;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 读取功能的生效配置(自动创建默认文件 + 热重载) */
|
|
280
|
+
export function getFeatureConfig(defId: string): FeatureConfig {
|
|
281
|
+
const def = FEATURE_DEFINITIONS.find(item => item.id === defId);
|
|
282
|
+
if (!def) throw new Error(`未注册的功能配置: ${defId}`);
|
|
283
|
+
return getStore(def).get();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** 读取功能的生效群聊白名单 */
|
|
287
|
+
export function getFeatureGroups(defId: string): number[] {
|
|
288
|
+
return getFeatureConfig(defId).groups;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** 订阅功能配置文件变化(外部改文件自动热重载) */
|
|
292
|
+
export function watchFeatureConfig(defId: string, listener: (config: FeatureConfig, source: import('@/config/manager').ConfigChangeSource) => void): () => void {
|
|
293
|
+
const def = FEATURE_DEFINITIONS.find(item => item.id === defId);
|
|
294
|
+
if (!def) throw new Error(`未注册的功能配置: ${defId}`);
|
|
295
|
+
return getStore(def).onChange(listener);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// GitHub 配置(v2:仓库级订阅)
|
|
300
|
+
// 与通用 FeatureConfig 不同:groups 是 仓库 full_name → 订阅群号[]。
|
|
301
|
+
// 独立 store 管理(configs/features/github.json,v1 → v2 自动迁移)。
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
const githubStore = new ConfigStore<GithubFeatureConfig>({
|
|
305
|
+
path: 'configs/features/github.json',
|
|
306
|
+
version: 2,
|
|
307
|
+
migrations: [{ from: 1, to: 2, migrate: migrateGithubV1ToV2 }],
|
|
308
|
+
factory: githubFactory,
|
|
309
|
+
normalize: normalizeGithubConfig
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/** 读取 github 生效配置(v2:仓库 → 订阅群映射) */
|
|
313
|
+
export function getGithubConfig(): GithubFeatureConfig {
|
|
314
|
+
return githubStore.get();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 读取某仓库的订阅群列表(无订阅返回空数组) */
|
|
318
|
+
export function getGithubSubscribers(repository: string): number[] {
|
|
319
|
+
return getGithubConfig().groups[repository] || [];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** 判断某群是否订阅了任意仓库(用于 processMessage 的启用判断) */
|
|
323
|
+
export function isGithubEnabledGroup(groupId: number): boolean {
|
|
324
|
+
return Object.values(getGithubConfig().groups).some(list => list.includes(groupId));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 订阅:把群加入仓库的订阅列表(幂等;写入后热生效)。
|
|
329
|
+
* @returns 该仓库订阅后的完整群列表
|
|
330
|
+
*/
|
|
331
|
+
export function subscribeGithubRepository(repository: string, groupId: number): number[] {
|
|
332
|
+
const current = getGithubConfig();
|
|
333
|
+
const subscribers = Array.from(new Set([...(current.groups[repository] || []), groupId]));
|
|
334
|
+
const next: GithubFeatureConfig = {
|
|
335
|
+
version: 2,
|
|
336
|
+
groups: { ...current.groups, [repository]: subscribers }
|
|
337
|
+
};
|
|
338
|
+
githubStore.update(next);
|
|
339
|
+
return subscribers;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 取消订阅:把群从仓库的订阅列表移除。
|
|
344
|
+
* @returns 该仓库订阅后的完整群列表(群号不存在则不变)
|
|
345
|
+
*/
|
|
346
|
+
export function unsubscribeGithubRepository(repository: string, groupId: number): number[] {
|
|
347
|
+
const current = getGithubConfig();
|
|
348
|
+
const subscribers = (current.groups[repository] || []).filter(id => id !== groupId);
|
|
349
|
+
const nextGroups: Record<string, number[]> = { ...current.groups };
|
|
350
|
+
if (subscribers.length > 0) nextGroups[repository] = subscribers;
|
|
351
|
+
else delete nextGroups[repository];
|
|
352
|
+
const next: GithubFeatureConfig = { version: 2, groups: nextGroups };
|
|
353
|
+
githubStore.update(next);
|
|
354
|
+
return subscribers;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** 仓库名格式校验(owner/name,均允许字母数字 . _ -) */
|
|
358
|
+
export function isValidRepositoryName(repository: string): boolean {
|
|
359
|
+
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** GitHub owner 格式校验(字母数字 . _ -,不含斜杠) */
|
|
363
|
+
export function isValidRepositoryOwner(owner: string): boolean {
|
|
364
|
+
return /^[A-Za-z0-9_.-]+$/.test(owner);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 判断某 QQ 用户是否为管理员(操作人白名单)。
|
|
369
|
+
*
|
|
370
|
+
* 白名单复用 management 功能的 operators 配置,使「谁是管理员」在全项目只有一个定义,
|
|
371
|
+
* 与 /pardon 等管理命令保持一致。
|
|
372
|
+
*
|
|
373
|
+
* @param qqUserId 发送者 QQ 号
|
|
374
|
+
*/
|
|
375
|
+
export function isOperator(qqUserId: number): boolean {
|
|
376
|
+
return getFeatureConfig('management').operators?.includes(qqUserId) ?? false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 判断某 Discord 用户是否为管理员(配置白名单)。
|
|
381
|
+
*
|
|
382
|
+
* 与「服务器拥有者/管理员权限」互补:Discord 侧命令源用 isDiscordAdmin() 判定服务器权限,
|
|
383
|
+
* 这里额外允许 management.json 的 `discordOperators` 名单成员(便于不加服务器权限也能管理)。
|
|
384
|
+
*
|
|
385
|
+
* @param discordUserId Discord 用户 ID(裸 snowflake 或带 `dc:` 前缀)
|
|
386
|
+
*/
|
|
387
|
+
export function isDiscordOperator(discordUserId: string): boolean {
|
|
388
|
+
const list = getFeatureConfig('management').discordOperators;
|
|
389
|
+
if (!list || list.length === 0) return false;
|
|
390
|
+
const target = normalizeDiscordUserId(discordUserId);
|
|
391
|
+
return list.some(item => normalizeDiscordUserId(item) === target);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// 新人欢迎配置(v1:welcomes 数组)
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
function welcomeFactory(): WelcomeFeatureConfig {
|
|
399
|
+
return { version: 1, welcomes: [] };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function normalizeWelcomeConfig(raw: unknown): WelcomeFeatureConfig | null {
|
|
403
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
404
|
+
const record = raw as Record<string, unknown>;
|
|
405
|
+
const welcomes = Array.isArray(record.welcomes) ? record.welcomes : [];
|
|
406
|
+
const normalized: WelcomeEntry[] = [];
|
|
407
|
+
for (const item of welcomes) {
|
|
408
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
|
|
409
|
+
const entry = item as Record<string, unknown>;
|
|
410
|
+
if (!Array.isArray(entry.group)) continue;
|
|
411
|
+
const msg = normalizeWelcomeMsg(entry.msg);
|
|
412
|
+
if (msg === null) continue;
|
|
413
|
+
const group = entry.group.filter(g => typeof g === 'number' && Number.isSafeInteger(g));
|
|
414
|
+
normalized.push({ group, msg });
|
|
415
|
+
}
|
|
416
|
+
return { version: 1, welcomes: normalized };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** 规范化 msg 字段:字符串或字符串数组(数组元素须全为字符串),否则返回 null */
|
|
420
|
+
function normalizeWelcomeMsg(raw: unknown): string | string[] | null {
|
|
421
|
+
if (typeof raw === 'string') return raw;
|
|
422
|
+
if (Array.isArray(raw)) {
|
|
423
|
+
if (raw.every(item => typeof item === 'string')) return raw;
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const welcomeStore = new ConfigStore<WelcomeFeatureConfig>({
|
|
430
|
+
path: 'configs/features/welcome.json',
|
|
431
|
+
version: 1,
|
|
432
|
+
factory: welcomeFactory,
|
|
433
|
+
normalize: normalizeWelcomeConfig
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
/** 读取新人欢迎生效配置(v1:welcomes 数组) */
|
|
437
|
+
export function getWelcomeConfig(): WelcomeFeatureConfig {
|
|
438
|
+
return welcomeStore.get();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// 旧字段迁移:bot-config.json 的 function* → 各功能配置文件
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
const BOT_CONFIG_PATH = path.resolve(process.cwd(), 'configs', 'bot-config.json');
|
|
446
|
+
|
|
447
|
+
function readLegacyValues(def: FeatureDefinition, legacyRaw: Record<string, unknown>): LegacyFunctionValues | undefined {
|
|
448
|
+
const groupsValue = def.legacyGroupsFields
|
|
449
|
+
.map(field => legacyRaw[field])
|
|
450
|
+
.find(value => value !== undefined);
|
|
451
|
+
const groups = groupsValue !== undefined && isNumberArray(groupsValue) ? groupsValue : undefined;
|
|
452
|
+
|
|
453
|
+
let operators: number[] | undefined = undefined;
|
|
454
|
+
if (def.legacyOperatorsField) {
|
|
455
|
+
const operatorValue = legacyRaw[def.legacyOperatorsField];
|
|
456
|
+
if (operatorValue !== undefined && isNumberArray(operatorValue)) operators = operatorValue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let extra: Record<string, unknown> | undefined = undefined;
|
|
460
|
+
if (def.legacyExtraFields) {
|
|
461
|
+
const extras: Record<string, unknown> = {};
|
|
462
|
+
let extrasAny = false;
|
|
463
|
+
for (const [legacyField, extraKey] of Object.entries(def.legacyExtraFields)) {
|
|
464
|
+
const value = legacyRaw[legacyField];
|
|
465
|
+
if (value !== undefined) {
|
|
466
|
+
extras[extraKey] = value;
|
|
467
|
+
extrasAny = true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (extrasAny) extra = extras;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (groups === undefined && operators === undefined && extra === undefined) return undefined;
|
|
474
|
+
return {
|
|
475
|
+
featureId: def.id,
|
|
476
|
+
...(groups !== undefined ? { groups } : {}),
|
|
477
|
+
...(operators !== undefined ? { operators } : {}),
|
|
478
|
+
...(extra !== undefined ? { extra } : {})
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function applyLegacyToStore(def: FeatureDefinition, legacy: LegacyFunctionValues): void {
|
|
483
|
+
// github 使用专用 store(v2 仓库级订阅),旧 groups 数组拆给默认仓库
|
|
484
|
+
if (def.id === 'github') {
|
|
485
|
+
const legacyGroups = legacy.groups || [];
|
|
486
|
+
const current = githubStore.get();
|
|
487
|
+
const nextGroups: Record<string, number[]> = { ...current.groups };
|
|
488
|
+
if (legacyGroups.length > 0 && Object.keys(nextGroups).length === 0) {
|
|
489
|
+
nextGroups['Anvil-Dev/AnvilCraft'] = legacyGroups;
|
|
490
|
+
}
|
|
491
|
+
githubStore.update({ version: 2, groups: nextGroups });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const store = getStore(def);
|
|
495
|
+
const current = store.get();
|
|
496
|
+
const next: FeatureConfig = {
|
|
497
|
+
version: def.version ?? 1,
|
|
498
|
+
groups: legacy.groups ?? current.groups,
|
|
499
|
+
...(legacy.operators || current.operators ? { operators: legacy.operators ?? current.operators } : {}),
|
|
500
|
+
...(legacy.extra || current.extra ? { extra: legacy.extra ?? current.extra } : {})
|
|
501
|
+
};
|
|
502
|
+
store.update(next);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 把 bot-config.json 里的旧 `function*` 字段一次性拆进各功能配置文件。
|
|
507
|
+
* 迁移完成后从 bot-config.json 删除这些字段。幂等:无旧字段时直接返回。
|
|
508
|
+
*/
|
|
509
|
+
export function migrateLegacyFunctionFields(): boolean {
|
|
510
|
+
const legacyRaw = readConfigFile<Record<string, unknown>>(BOT_CONFIG_PATH);
|
|
511
|
+
if (!legacyRaw) return false;
|
|
512
|
+
|
|
513
|
+
let anyMigrated = false;
|
|
514
|
+
for (const def of FEATURE_DEFINITIONS) {
|
|
515
|
+
const legacy = readLegacyValues(def, legacyRaw);
|
|
516
|
+
if (legacy) {
|
|
517
|
+
applyLegacyToStore(def, legacy);
|
|
518
|
+
anyMigrated = true;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (!anyMigrated) return false;
|
|
523
|
+
|
|
524
|
+
// 从 bot-config.json 删除已迁移的 function* 字段并写回
|
|
525
|
+
const next = { ...legacyRaw };
|
|
526
|
+
for (const def of FEATURE_DEFINITIONS) {
|
|
527
|
+
for (const field of def.legacyGroupsFields) delete next[field];
|
|
528
|
+
if (def.legacyOperatorsField) delete next[def.legacyOperatorsField];
|
|
529
|
+
if (def.legacyExtraFields) {
|
|
530
|
+
for (const legacyField of Object.keys(def.legacyExtraFields)) delete next[legacyField];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
writeConfigFile(BOT_CONFIG_PATH, next);
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** 供 ConfigStore 观察者使用的内部导出(测试用) */
|
|
538
|
+
export function __featureStores(): Map<string, ConfigStore<FeatureConfig>> {
|
|
539
|
+
return stores;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export type { MigrationStep };
|
|
543
|
+
export { FEATURES_DIR };
|
|
544
|
+
|
|
545
|
+
// 模块加载时立即执行一次旧字段迁移:
|
|
546
|
+
// 保证任何功能模块首次 getFeatureConfig() 之前,bot-config.json 的
|
|
547
|
+
// function* 旧字段已拆入各自的 features/*.json 配置文件。
|
|
548
|
+
migrateLegacyFunctionFields();
|