@dsh-plugin/dsh-synapse 0.1.0-dev.41044228975 → 0.1.0-dev.41051739681
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 +12 -24
- package/lib/host/agent.d.ts +8 -4
- package/lib/host/agent.js +16 -99
- package/lib/host/agent.js.map +1 -1
- package/lib/host/bot.js +9 -7
- package/lib/host/bot.js.map +1 -1
- package/lib/host/config-migration.d.ts +3 -0
- package/lib/host/config-migration.js +103 -0
- package/lib/host/config-migration.js.map +1 -0
- package/lib/host/config.d.ts +4 -18
- package/lib/host/config.js +77 -130
- package/lib/host/config.js.map +1 -1
- package/lib/host/feature-config.d.ts +20 -0
- package/lib/host/feature-config.js +264 -0
- package/lib/host/feature-config.js.map +1 -0
- package/lib/host/gateway.d.ts +4 -6
- package/lib/host/gateway.js +42 -104
- package/lib/host/gateway.js.map +1 -1
- package/lib/host/github.d.ts +2 -2
- package/lib/host/harness.d.ts +23 -0
- package/lib/host/harness.js +209 -0
- package/lib/host/harness.js.map +1 -0
- package/lib/host/index.js +22 -12
- package/lib/host/index.js.map +1 -1
- package/lib/host/mcp.d.ts +1 -1
- package/lib/host/mcp.js +15 -11
- package/lib/host/mcp.js.map +1 -1
- package/lib/host/panel.html +2 -1
- package/lib/host/panel.js +40 -24
- package/lib/host/panel.js.map +1 -1
- package/lib/host/types.d.ts +104 -16
- package/package.json +1 -1
- package/scripts/dump-npm-latest.mjs +29 -10
- package/scripts/host-smoke.mjs +139 -0
- package/src/agent.ts +34 -114
- package/src/bot.ts +9 -7
- package/src/config-migration.ts +148 -0
- package/src/config.ts +116 -152
- package/src/feature-config.ts +351 -0
- package/src/gateway.ts +48 -115
- package/src/harness.ts +253 -0
- package/src/index.ts +25 -18
- package/src/mcp.ts +23 -11
- package/src/panel.html +2 -1
- package/src/panel.ts +53 -24
- package/src/types.ts +75 -13
- package/test/config-migration.test.mjs +137 -0
- package/test/core.test.mjs +82 -16
- package/test/fixtures/host.mjs +133 -0
- package/test/harness.test.mjs +90 -0
- package/test/integration.test.mjs +10 -12
- package/test/panel.test.mjs +71 -3
package/src/config.ts
CHANGED
|
@@ -1,43 +1,31 @@
|
|
|
1
|
-
import { defineConfig,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"parentheses",
|
|
24
|
-
"poke",
|
|
25
|
-
"minecraft",
|
|
26
|
-
"modrinth",
|
|
27
|
-
"bili",
|
|
28
|
-
"command",
|
|
29
|
-
"github",
|
|
30
|
-
"welcome",
|
|
31
|
-
"discord-bridge",
|
|
32
|
-
"hermes",
|
|
33
|
-
"peak-valley-timer",
|
|
34
|
-
];
|
|
1
|
+
import { defineConfig, validateValues } from "@dsh-plugin/dsh-loader/config";
|
|
2
|
+
import type { ConfigField } from "@dsh-plugin/dsh-loader/config";
|
|
3
|
+
import type { SynapseConfig } from "./types.js";
|
|
4
|
+
import {
|
|
5
|
+
text,
|
|
6
|
+
flag,
|
|
7
|
+
num,
|
|
8
|
+
object,
|
|
9
|
+
array,
|
|
10
|
+
strings,
|
|
11
|
+
select,
|
|
12
|
+
moduleFields,
|
|
13
|
+
} from "./feature-config.js";
|
|
14
|
+
const pairs = (key: string, label: string) =>
|
|
15
|
+
array(
|
|
16
|
+
key,
|
|
17
|
+
label,
|
|
18
|
+
object("item", "条目", [
|
|
19
|
+
text("name", "名称"),
|
|
20
|
+
text("value", "值", "", "password"),
|
|
21
|
+
]),
|
|
22
|
+
);
|
|
35
23
|
export const template = defineConfig({
|
|
36
24
|
id: "dsh-synapse",
|
|
37
25
|
packageName: "@dsh-plugin/dsh-synapse",
|
|
38
26
|
title: "Synapse · 通道与记忆",
|
|
39
27
|
description:
|
|
40
|
-
"
|
|
28
|
+
"聊天、审查和自总结使用当前 DSH 的默认模型与 Agent 运行时。各功能在下方直接配置;Skill、MCP 工具预览与记忆导入位于 Synapse 管理面板。",
|
|
41
29
|
fields: [
|
|
42
30
|
flag("enabled", "启用机器人"),
|
|
43
31
|
text("profileDir", "当前 profile 绝对路径(自动定位失败时填写)"),
|
|
@@ -50,27 +38,28 @@ export const template = defineConfig({
|
|
|
50
38
|
text("wsUrl", "WebSocket 地址", "ws://127.0.0.1:3001"),
|
|
51
39
|
text("httpToken", "HTTP Token", "", "password"),
|
|
52
40
|
text("wsToken", "WS Token", "", "password"),
|
|
41
|
+
text("tokenParams", "WebSocket Token 查询参数前缀", "?access_token="),
|
|
53
42
|
text("chromePath", "Chrome / Chromium 路径"),
|
|
54
|
-
text("
|
|
43
|
+
text("userAgent", "HTTP User-Agent", "Mozilla/5.0"),
|
|
44
|
+
select("logLevel", "日志级别", [
|
|
45
|
+
["info", "信息"],
|
|
46
|
+
["warn", "警告"],
|
|
47
|
+
["error", "错误"],
|
|
48
|
+
["debug", "调试"],
|
|
49
|
+
]),
|
|
55
50
|
],
|
|
56
51
|
},
|
|
57
52
|
{
|
|
58
53
|
type: "group",
|
|
59
|
-
label: "
|
|
54
|
+
label: "任务运行限制",
|
|
60
55
|
fields: [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
text("model", "模型", "deepseek-chat"),
|
|
64
|
-
num("timeoutSec", "请求超时(秒)", 120, 5, 600),
|
|
65
|
-
num("maxToolRounds", "工具轮数上限", 6, 1, 20),
|
|
66
|
-
flag("externalHermes", "使用外部 Hermes(保留其工具及审批)"),
|
|
67
|
-
text("hermesUrl", "Hermes API 地址", "http://127.0.0.1:8642"),
|
|
68
|
-
text("hermesKey", "Hermes Key", "", "password"),
|
|
56
|
+
num("timeoutSec", "单次任务超时(秒)", 120, 5, 600),
|
|
57
|
+
num("maxToolRounds", "单次任务最大步骤数", 6, 1, 20),
|
|
69
58
|
],
|
|
70
59
|
},
|
|
71
60
|
{
|
|
72
61
|
type: "group",
|
|
73
|
-
label: "GitHub",
|
|
62
|
+
label: "GitHub Webhook",
|
|
74
63
|
fields: [
|
|
75
64
|
flag("githubEnabled", "启用 Webhook 审查"),
|
|
76
65
|
text("githubToken", "GitHub Token", "", "password"),
|
|
@@ -98,120 +87,95 @@ export const template = defineConfig({
|
|
|
98
87
|
num("maxMemories", "最大记忆条数", 20000, 100, 1000000),
|
|
99
88
|
],
|
|
100
89
|
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
},
|
|
129
|
-
text("json", "配置 JSON(可能含密钥)", "{}", "password"),
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
type: "array",
|
|
135
|
-
key: "mcp",
|
|
136
|
-
label: "MCP 服务",
|
|
137
|
-
item: {
|
|
138
|
-
type: "object",
|
|
139
|
-
label: "服务",
|
|
140
|
-
fields: [
|
|
141
|
-
text("name", "名称"),
|
|
142
|
-
flag("enabled", "启用", true),
|
|
143
|
-
{
|
|
144
|
-
type: "select",
|
|
145
|
-
key: "transport",
|
|
146
|
-
label: "传输",
|
|
147
|
-
options: [
|
|
148
|
-
{ label: "stdio", value: "stdio" },
|
|
149
|
-
{ label: "Streamable HTTP", value: "http" },
|
|
150
|
-
{ label: "SSE", value: "sse" },
|
|
151
|
-
],
|
|
152
|
-
},
|
|
153
|
-
text("command", "命令"),
|
|
154
|
-
text("argsJson", "参数 JSON", "[]", "textarea"),
|
|
155
|
-
text("envJson", "环境变量 JSON", "{}", "password"),
|
|
156
|
-
text("url", "URL"),
|
|
157
|
-
text("headersJson", "HTTP 请求头 JSON", "{}", "password"),
|
|
158
|
-
text(
|
|
159
|
-
"allowedTools",
|
|
160
|
-
"允许自动调用的工具名称(每行一个)",
|
|
161
|
-
"",
|
|
162
|
-
"textarea",
|
|
163
|
-
),
|
|
164
|
-
],
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
],
|
|
90
|
+
object("modules", "聊天与通知功能", moduleFields),
|
|
91
|
+
array(
|
|
92
|
+
"mcpServers",
|
|
93
|
+
"MCP 服务",
|
|
94
|
+
object("item", "服务", [
|
|
95
|
+
{
|
|
96
|
+
type: "text",
|
|
97
|
+
key: "name",
|
|
98
|
+
label: "名称",
|
|
99
|
+
required: true,
|
|
100
|
+
pattern: "^[a-zA-Z0-9_-]{1,32}$",
|
|
101
|
+
},
|
|
102
|
+
flag("enabled", "启用", true),
|
|
103
|
+
select("transport", "传输", [
|
|
104
|
+
["stdio", "stdio"],
|
|
105
|
+
["http", "Streamable HTTP"],
|
|
106
|
+
["sse", "SSE"],
|
|
107
|
+
]),
|
|
108
|
+
text("command", "启动命令"),
|
|
109
|
+
strings("args", "命令参数(每项一个参数)"),
|
|
110
|
+
pairs("env", "环境变量"),
|
|
111
|
+
text("url", "服务 URL"),
|
|
112
|
+
pairs("headers", "HTTP 请求头"),
|
|
113
|
+
strings("allowedTools", "允许自动调用的工具名称"),
|
|
114
|
+
]),
|
|
115
|
+
),
|
|
116
|
+
] satisfies ConfigField[],
|
|
168
117
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
throw new Error("MCP 环境变量和请求头必须是字符串");
|
|
118
|
+
|
|
119
|
+
export function validateConfig(c: SynapseConfig) {
|
|
120
|
+
validateValues(template.fields, c as any);
|
|
121
|
+
const unique = (values: string[], label: string) => {
|
|
122
|
+
if (new Set(values).size !== values.length)
|
|
123
|
+
throw new Error(`${label}不能重复`);
|
|
124
|
+
};
|
|
125
|
+
unique(
|
|
126
|
+
c.mcpServers.map((s) => s.name),
|
|
127
|
+
"MCP 名称",
|
|
128
|
+
);
|
|
129
|
+
for (const s of c.mcpServers) {
|
|
130
|
+
for (const [entries, label] of [
|
|
131
|
+
[s.env, "环境变量"],
|
|
132
|
+
[s.headers, "HTTP 请求头"],
|
|
133
|
+
] as const) {
|
|
134
|
+
unique(
|
|
135
|
+
entries.map((e) => e.name.toLowerCase()),
|
|
136
|
+
label,
|
|
137
|
+
);
|
|
138
|
+
if (entries.some((e) => !e.name || /[\r\n=]/.test(e.name)))
|
|
139
|
+
throw new Error(`${label}名称无效`);
|
|
140
|
+
}
|
|
141
|
+
if (s.enabled && s.transport === "stdio" && !s.command.trim())
|
|
142
|
+
throw new Error("MCP 启动命令不能为空");
|
|
195
143
|
if (s.transport !== "stdio" && !/^https?:\/\//.test(s.url))
|
|
196
144
|
throw new Error("MCP URL 无效");
|
|
197
145
|
}
|
|
146
|
+
unique(
|
|
147
|
+
c.modules.discord.bridges.map((b) => `${b.guildId}#${b.channelName}`),
|
|
148
|
+
"Discord 频道映射",
|
|
149
|
+
);
|
|
150
|
+
for (const b of c.modules.discord.bridges)
|
|
151
|
+
if (
|
|
152
|
+
!/^\d+$/.test(b.guildId) ||
|
|
153
|
+
!/^\d+$/.test(b.group) ||
|
|
154
|
+
!b.channelName.trim()
|
|
155
|
+
)
|
|
156
|
+
throw new Error("Discord 桥接需要服务器 ID、频道名称和 QQ 群号");
|
|
157
|
+
unique(
|
|
158
|
+
c.modules.github.subscriptions.map((s) => s.repository.toLowerCase()),
|
|
159
|
+
"GitHub 订阅仓库",
|
|
160
|
+
);
|
|
161
|
+
for (const s of c.modules.github.subscriptions)
|
|
162
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(s.repository))
|
|
163
|
+
throw new Error("GitHub 订阅仓库格式应为 owner/repo");
|
|
198
164
|
if (
|
|
199
165
|
c.githubEnabled &&
|
|
200
166
|
(!c.webhookSecret || !c.githubToken || !c.repositories.trim())
|
|
201
167
|
)
|
|
202
168
|
throw new Error("启用 GitHub 需要 Secret、Token 和仓库允许列表");
|
|
203
|
-
|
|
204
|
-
"
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
throw new Error(`${key} 必须是 HTTP(S) URL`);
|
|
215
|
-
if (!/^wss?:\/\//.test(c.wsUrl)) throw new Error("wsUrl 必须是 WS(S) URL");
|
|
169
|
+
const integers = (value: unknown) => {
|
|
170
|
+
if (typeof value === "number" && !Number.isSafeInteger(value))
|
|
171
|
+
throw new Error("数量和号码必须是安全整数");
|
|
172
|
+
if (value && typeof value === "object")
|
|
173
|
+
Object.values(value).forEach(integers);
|
|
174
|
+
};
|
|
175
|
+
integers(c);
|
|
176
|
+
if (!/^https?:\/\//.test(c.httpUrl))
|
|
177
|
+
throw new Error("HTTP 地址必须是 HTTP(S) URL");
|
|
178
|
+
if (!/^wss?:\/\//.test(c.wsUrl))
|
|
179
|
+
throw new Error("WebSocket 地址必须是 WS(S) URL");
|
|
216
180
|
return c;
|
|
217
181
|
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { ConfigField } from "@dsh-plugin/dsh-loader/config";
|
|
2
|
+
import type { SynapseConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const text = (
|
|
5
|
+
key: string,
|
|
6
|
+
label: string,
|
|
7
|
+
value = "",
|
|
8
|
+
type: ConfigField["type"] = "text",
|
|
9
|
+
): ConfigField => ({ type, key, label, default: value });
|
|
10
|
+
export const flag = (
|
|
11
|
+
key: string,
|
|
12
|
+
label: string,
|
|
13
|
+
value = false,
|
|
14
|
+
): ConfigField => ({ type: "boolean", key, label, default: value });
|
|
15
|
+
export const num = (
|
|
16
|
+
key: string,
|
|
17
|
+
label: string,
|
|
18
|
+
value: number,
|
|
19
|
+
min = 0,
|
|
20
|
+
max = Number.MAX_SAFE_INTEGER,
|
|
21
|
+
): ConfigField => ({ type: "number", key, label, default: value, min, max });
|
|
22
|
+
export const object = (
|
|
23
|
+
key: string,
|
|
24
|
+
label: string,
|
|
25
|
+
fields: ConfigField[],
|
|
26
|
+
): ConfigField => ({ type: "object", key, label, fields });
|
|
27
|
+
export const array = (
|
|
28
|
+
key: string,
|
|
29
|
+
label: string,
|
|
30
|
+
item: ConfigField,
|
|
31
|
+
): ConfigField => ({ type: "array", key, label, item });
|
|
32
|
+
export const strings = (key: string, label: string): ConfigField =>
|
|
33
|
+
array(key, label, text("item", "值"));
|
|
34
|
+
export const ids = (key = "groups", label = "启用的 QQ 群"): ConfigField =>
|
|
35
|
+
array(key, label, num("item", "号码", 1, 1));
|
|
36
|
+
export const select = (
|
|
37
|
+
key: string,
|
|
38
|
+
label: string,
|
|
39
|
+
options: [string, string][],
|
|
40
|
+
value = options[0][0],
|
|
41
|
+
): ConfigField => ({
|
|
42
|
+
type: "select",
|
|
43
|
+
key,
|
|
44
|
+
label,
|
|
45
|
+
options: options.map(([value, label]) => ({ value, label })),
|
|
46
|
+
default: value,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
50
|
+
export const moduleFields: ConfigField[] = [
|
|
51
|
+
object("management", "群管理与管理员", [
|
|
52
|
+
ids(),
|
|
53
|
+
ids("operators", "QQ 管理员"),
|
|
54
|
+
strings("discordOperators", "Discord 管理员 ID"),
|
|
55
|
+
]),
|
|
56
|
+
...[
|
|
57
|
+
["parentheses", "括号匹配"],
|
|
58
|
+
["poke", "戳一戳"],
|
|
59
|
+
["minecraft", "Minecraft 查询"],
|
|
60
|
+
["modrinth", "Modrinth 查询"],
|
|
61
|
+
["command", "命令入口"],
|
|
62
|
+
].map(([id, label]) => object(id, label, [ids()])),
|
|
63
|
+
object("bili", "B 站视频与订阅", [ids(), ids("follow", "关注的 B 站 UID")]),
|
|
64
|
+
object("github", "GitHub 仓库通知", [
|
|
65
|
+
array(
|
|
66
|
+
"subscriptions",
|
|
67
|
+
"仓库订阅",
|
|
68
|
+
object("item", "订阅", [text("repository", "owner/repo"), ids()]),
|
|
69
|
+
),
|
|
70
|
+
]),
|
|
71
|
+
object("welcome", "新人欢迎", [
|
|
72
|
+
array(
|
|
73
|
+
"welcomes",
|
|
74
|
+
"欢迎规则",
|
|
75
|
+
object("item", "规则", [
|
|
76
|
+
ids("group", "适用 QQ 群"),
|
|
77
|
+
text(
|
|
78
|
+
"msg",
|
|
79
|
+
"欢迎语(${at} 表示新成员)",
|
|
80
|
+
"欢迎 ${at} 加入群聊",
|
|
81
|
+
"textarea",
|
|
82
|
+
),
|
|
83
|
+
]),
|
|
84
|
+
),
|
|
85
|
+
]),
|
|
86
|
+
object("discord", "Discord 跨平台桥接", [
|
|
87
|
+
text("token", "Discord Bot Token", "", "password"),
|
|
88
|
+
array(
|
|
89
|
+
"bridges",
|
|
90
|
+
"频道与 QQ 群映射",
|
|
91
|
+
object("item", "桥接", [
|
|
92
|
+
text("guildId", "Discord 服务器 ID"),
|
|
93
|
+
text("channelName", "Discord 频道名称"),
|
|
94
|
+
text("group", "QQ 群号"),
|
|
95
|
+
flag("needReply", "QQ → Discord 仅转发对桥消息的回复"),
|
|
96
|
+
flag("needCmd", "双向仅转发 /send 与桥消息回复"),
|
|
97
|
+
]),
|
|
98
|
+
),
|
|
99
|
+
]),
|
|
100
|
+
object("chat", "QQ / Discord 消息行为", [
|
|
101
|
+
text("botName", "机器人展示名称", "小喵"),
|
|
102
|
+
ids(),
|
|
103
|
+
ids("admins", "对话管理员 QQ"),
|
|
104
|
+
ids("allowedUsers", "允许对话的 QQ 用户(空为全部)"),
|
|
105
|
+
ids("blockedUsers", "屏蔽的 QQ 用户"),
|
|
106
|
+
flag("requireMention", "群聊需要 @机器人", true),
|
|
107
|
+
strings("keywordTriggers", "触发关键词"),
|
|
108
|
+
num("progressRateLimitSec", "进度更新最小间隔(秒)", 15),
|
|
109
|
+
flag("progressAsImage", "进度使用图片卡片", true),
|
|
110
|
+
num("progressMaxTools", "进度卡片最多工具数", 12),
|
|
111
|
+
num("maxMessageLength", "单条消息最大字符数", 1200, 1),
|
|
112
|
+
flag("forwardImages", "转发图片到 DSH", true),
|
|
113
|
+
flag("replyPerSentence", "逐句发送回复", true),
|
|
114
|
+
num("compactMaxChars", "合并转发字符阈值(0 关闭)", 300),
|
|
115
|
+
num("compactMaxLines", "合并转发行数阈值(0 关闭)", 5),
|
|
116
|
+
num("localHistoryMaxMessages", "会话上下文消息数", 24),
|
|
117
|
+
flag("persistHistoryEnabled", "保存会话历史", true),
|
|
118
|
+
num("persistHistoryMax", "历史保留消息数", 100),
|
|
119
|
+
flag("approvalEnabled", "在聊天通道显示 DSH 审批", true),
|
|
120
|
+
num("approvalTimeoutSec", "审批超时自动拒绝(秒,0 不超时)", 300),
|
|
121
|
+
flag("reactionEnabled", "处理期间使用表情回应", true),
|
|
122
|
+
text("reactionEmojiQq", "QQ 回应表情 ID", "76"),
|
|
123
|
+
text("reactionEmojiDiscord", "Discord 回应表情", "👀"),
|
|
124
|
+
flag("privateReactionFallbackText", "私聊表情失败时发送文字回执", true),
|
|
125
|
+
text("privateReceiptText", "私聊文字回执", "🤔 收到,正在思考…"),
|
|
126
|
+
]),
|
|
127
|
+
object("peakValley", "峰谷时段提醒", [
|
|
128
|
+
ids(),
|
|
129
|
+
select("mode", "默认时段类型", [
|
|
130
|
+
["peak", "峰"],
|
|
131
|
+
["valley", "谷"],
|
|
132
|
+
]),
|
|
133
|
+
{
|
|
134
|
+
...array(
|
|
135
|
+
"time",
|
|
136
|
+
"时段",
|
|
137
|
+
object("item", "时段", [
|
|
138
|
+
{
|
|
139
|
+
...text("start", "开始时间", "09:00"),
|
|
140
|
+
pattern: "^([01]?\\d|2[0-3]):[0-5]\\d$",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
...text("end", "结束时间", "12:00"),
|
|
144
|
+
pattern: "^([01]?\\d|2[0-3]):[0-5]\\d$",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "multiselect",
|
|
148
|
+
key: "days",
|
|
149
|
+
label: "重复日期",
|
|
150
|
+
default: days.slice(0, 5),
|
|
151
|
+
options: days.map((value, i) => ({
|
|
152
|
+
value,
|
|
153
|
+
label: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][
|
|
154
|
+
i
|
|
155
|
+
],
|
|
156
|
+
})),
|
|
157
|
+
},
|
|
158
|
+
select("mode", "时段类型", [
|
|
159
|
+
["", "使用默认类型"],
|
|
160
|
+
["peak", "峰"],
|
|
161
|
+
["valley", "谷"],
|
|
162
|
+
]),
|
|
163
|
+
text("msg", "此时段提醒(留空使用默认)", "", "textarea"),
|
|
164
|
+
]),
|
|
165
|
+
),
|
|
166
|
+
default: [
|
|
167
|
+
{
|
|
168
|
+
start: "09:00",
|
|
169
|
+
end: "12:00",
|
|
170
|
+
days: days.slice(0, 5),
|
|
171
|
+
mode: "",
|
|
172
|
+
msg: "",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
start: "14:00",
|
|
176
|
+
end: "18:00",
|
|
177
|
+
days: days.slice(0, 5),
|
|
178
|
+
mode: "",
|
|
179
|
+
msg: "",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
text(
|
|
184
|
+
"peak_msg",
|
|
185
|
+
"峰时段提醒",
|
|
186
|
+
"梁文峰时间到!\n当前时间是${time}",
|
|
187
|
+
"textarea",
|
|
188
|
+
),
|
|
189
|
+
text(
|
|
190
|
+
"valley_msg",
|
|
191
|
+
"谷时段提醒",
|
|
192
|
+
"梁文谷时间到!\n当前时间是${time}",
|
|
193
|
+
"textarea",
|
|
194
|
+
),
|
|
195
|
+
text(
|
|
196
|
+
"cmd_peak_msg",
|
|
197
|
+
"峰时段查询回复",
|
|
198
|
+
"当前时间是${time},梁文峰时间!",
|
|
199
|
+
"textarea",
|
|
200
|
+
),
|
|
201
|
+
text(
|
|
202
|
+
"cmd_valley_msg",
|
|
203
|
+
"谷时段查询回复",
|
|
204
|
+
"当前时间是${time},梁文谷时间!",
|
|
205
|
+
"textarea",
|
|
206
|
+
),
|
|
207
|
+
]),
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
/** Only this boundary translates the visual form to the upstream file formats. */
|
|
211
|
+
export function featureFiles(c: SynapseConfig): { id: string; value: any }[] {
|
|
212
|
+
const m = c.modules;
|
|
213
|
+
return [
|
|
214
|
+
...[
|
|
215
|
+
"management",
|
|
216
|
+
"parentheses",
|
|
217
|
+
"poke",
|
|
218
|
+
"minecraft",
|
|
219
|
+
"modrinth",
|
|
220
|
+
"command",
|
|
221
|
+
].map((id) => ({ id, value: { version: 1, ...m[id] } })),
|
|
222
|
+
{
|
|
223
|
+
id: "bili",
|
|
224
|
+
value: {
|
|
225
|
+
version: 1,
|
|
226
|
+
groups: m.bili.groups,
|
|
227
|
+
extra: { follow: m.bili.follow },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "github",
|
|
232
|
+
value: {
|
|
233
|
+
version: 2,
|
|
234
|
+
groups: Object.fromEntries(
|
|
235
|
+
m.github.subscriptions.map((s) => [s.repository, s.groups]),
|
|
236
|
+
),
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{ id: "welcome", value: { version: 1, welcomes: m.welcome.welcomes } },
|
|
240
|
+
{
|
|
241
|
+
id: "discord-bridge",
|
|
242
|
+
value: {
|
|
243
|
+
version: 1,
|
|
244
|
+
token: m.discord.token,
|
|
245
|
+
bridges: Object.fromEntries(
|
|
246
|
+
m.discord.bridges.map((b) => [
|
|
247
|
+
`${b.guildId}#${b.channelName}`,
|
|
248
|
+
{
|
|
249
|
+
group: b.group,
|
|
250
|
+
need_reply: String(b.needReply),
|
|
251
|
+
need_cmd: String(b.needCmd),
|
|
252
|
+
},
|
|
253
|
+
]),
|
|
254
|
+
),
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{ id: "hermes", value: { version: 1, ...m.chat, systemPrompt: "" } },
|
|
258
|
+
{
|
|
259
|
+
id: "peak-valley-timer",
|
|
260
|
+
value: {
|
|
261
|
+
...m.peakValley,
|
|
262
|
+
time: m.peakValley.time.map((t) => ({
|
|
263
|
+
...t,
|
|
264
|
+
...(t.mode ? {} : { mode: undefined }),
|
|
265
|
+
})),
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function modulesFromFiles(
|
|
272
|
+
base: SynapseConfig["modules"],
|
|
273
|
+
files: { id: string; value: any }[],
|
|
274
|
+
): SynapseConfig["modules"] {
|
|
275
|
+
const m = structuredClone(base);
|
|
276
|
+
for (const { id, value: v } of files) {
|
|
277
|
+
if (
|
|
278
|
+
[
|
|
279
|
+
"management",
|
|
280
|
+
"parentheses",
|
|
281
|
+
"poke",
|
|
282
|
+
"minecraft",
|
|
283
|
+
"modrinth",
|
|
284
|
+
"command",
|
|
285
|
+
].includes(id)
|
|
286
|
+
)
|
|
287
|
+
m[id] = { ...m[id], ...v };
|
|
288
|
+
else if (id === "bili")
|
|
289
|
+
m.bili = { groups: v.groups ?? [], follow: v.extra?.follow ?? [] };
|
|
290
|
+
else if (id === "github")
|
|
291
|
+
m.github.subscriptions = Object.entries(
|
|
292
|
+
Array.isArray(v.groups)
|
|
293
|
+
? { "Anvil-Dev/AnvilCraft": v.groups }
|
|
294
|
+
: (v.groups ?? {}),
|
|
295
|
+
).map(([repository, groups]) => ({
|
|
296
|
+
repository,
|
|
297
|
+
groups: groups as number[],
|
|
298
|
+
}));
|
|
299
|
+
else if (id === "welcome")
|
|
300
|
+
m.welcome.welcomes = (v.welcomes ?? []).map((w) => ({
|
|
301
|
+
group: w.group,
|
|
302
|
+
msg: Array.isArray(w.msg) ? w.msg.join("\n") : w.msg,
|
|
303
|
+
}));
|
|
304
|
+
else if (id === "discord-bridge")
|
|
305
|
+
m.discord = {
|
|
306
|
+
token: v.token ?? "",
|
|
307
|
+
bridges: Object.entries(v.bridges ?? {}).map(
|
|
308
|
+
([key, entry]: [string, any]) => ({
|
|
309
|
+
guildId: key.slice(0, key.indexOf("#")),
|
|
310
|
+
channelName: key.slice(key.indexOf("#") + 1),
|
|
311
|
+
group: String(entry.group),
|
|
312
|
+
needReply: String(entry.need_reply) === "true",
|
|
313
|
+
needCmd: String(entry.need_cmd) === "true",
|
|
314
|
+
}),
|
|
315
|
+
),
|
|
316
|
+
};
|
|
317
|
+
else if (id === "hermes") {
|
|
318
|
+
const { apiUrl, apiKey, systemPrompt, version, ...behavior } = v;
|
|
319
|
+
m.chat = { ...m.chat, ...behavior };
|
|
320
|
+
} else if (id === "peak-valley-timer")
|
|
321
|
+
m.peakValley = {
|
|
322
|
+
...m.peakValley,
|
|
323
|
+
...v,
|
|
324
|
+
time: (v.time ?? m.peakValley.time).map(timeRange),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return m;
|
|
328
|
+
}
|
|
329
|
+
function timeRange(raw: any) {
|
|
330
|
+
if (typeof raw !== "string")
|
|
331
|
+
return { ...raw, mode: raw.mode ?? "", msg: raw.msg ?? "" };
|
|
332
|
+
const match = raw.match(/^(\d{1,2}:\d{2})-(\d{1,2}:\d{2})(?:\s+(.+))?$/);
|
|
333
|
+
if (!match) throw new Error(`无法迁移时段:${raw}`);
|
|
334
|
+
const selected = new Set<string>();
|
|
335
|
+
for (const token of (match[3] ?? "Mon-Sun").split(/[,\s]+/)) {
|
|
336
|
+
const [start, end = start] = token.split("-");
|
|
337
|
+
let i = days.indexOf(start);
|
|
338
|
+
if (i < 0 || !days.includes(end)) throw new Error(`无法迁移日期:${token}`);
|
|
339
|
+
for (let n = 0; n < 7; n++, i = (i + 1) % 7) {
|
|
340
|
+
selected.add(days[i]);
|
|
341
|
+
if (days[i] === end) break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
start: match[1],
|
|
346
|
+
end: match[2],
|
|
347
|
+
days: [...selected],
|
|
348
|
+
mode: "",
|
|
349
|
+
msg: "",
|
|
350
|
+
};
|
|
351
|
+
}
|