@dsh-plugin/dsh-synapse 0.0.0-stage → 0.1.0-dev.41044228975
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 +30 -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
package/src/bot.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { ConfigSource, PluginPaths, ErrorSink } from "./types.js";
|
|
2
|
+
import { fork } from "node:child_process";
|
|
3
|
+
import { mkdir, cp, readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { once } from "node:events";
|
|
7
|
+
import { atomicWrite } from "./files.js";
|
|
8
|
+
export class Bot {
|
|
9
|
+
paths: PluginPaths;
|
|
10
|
+
config: ConfigSource;
|
|
11
|
+
gateway: import("./gateway.js").Gateway;
|
|
12
|
+
onError: ErrorSink;
|
|
13
|
+
state: string;
|
|
14
|
+
stopping: boolean;
|
|
15
|
+
child: import("node:child_process").ChildProcess;
|
|
16
|
+
stderr: string;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
paths: PluginPaths,
|
|
20
|
+
config: ConfigSource,
|
|
21
|
+
gateway: import("./gateway.js").Gateway,
|
|
22
|
+
onError: ErrorSink,
|
|
23
|
+
) {
|
|
24
|
+
Object.assign(this, { paths, config, gateway, onError });
|
|
25
|
+
this.state = "stopped";
|
|
26
|
+
this.stopping = false;
|
|
27
|
+
}
|
|
28
|
+
async start() {
|
|
29
|
+
const c = this.config();
|
|
30
|
+
if (!c.enabled) return;
|
|
31
|
+
const cwd = path.join(this.paths.data, "bot");
|
|
32
|
+
await mkdir(path.join(cwd, "configs", "features"), { recursive: true });
|
|
33
|
+
await cp(
|
|
34
|
+
fileURLToPath(
|
|
35
|
+
new URL("../../vendor/gugle-chat-bot/src", import.meta.url),
|
|
36
|
+
),
|
|
37
|
+
path.join(cwd, "src"),
|
|
38
|
+
{ recursive: true },
|
|
39
|
+
);
|
|
40
|
+
const base = JSON.parse(c.botJson);
|
|
41
|
+
await atomicWrite(
|
|
42
|
+
path.join(cwd, "configs", "bot-config.json"),
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
...base,
|
|
45
|
+
httpUrl: c.httpUrl,
|
|
46
|
+
wsUrl: c.wsUrl,
|
|
47
|
+
httpToken: c.httpToken,
|
|
48
|
+
wsToken: c.wsToken,
|
|
49
|
+
chromePath: c.chromePath,
|
|
50
|
+
githubAllowedRepositories: c.repositories.split(/\s+/).filter(Boolean),
|
|
51
|
+
logLevel: base.logLevel || "info",
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
// Existing feature files contain live /github subscribe and admin changes.
|
|
55
|
+
// Only replace a module when its dashboard configuration has changed.
|
|
56
|
+
let previous = {};
|
|
57
|
+
const marker = path.join(cwd, "applied-config.json");
|
|
58
|
+
try {
|
|
59
|
+
previous = JSON.parse(await readFile(marker, "utf8"));
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e.code !== "ENOENT") throw e;
|
|
62
|
+
}
|
|
63
|
+
const applied = {};
|
|
64
|
+
for (const f of c.features) {
|
|
65
|
+
applied[f.id] = f.json;
|
|
66
|
+
const file = path.join(
|
|
67
|
+
cwd,
|
|
68
|
+
"configs",
|
|
69
|
+
...(f.id === "peak-valley-timer" ? [] : ["features"]),
|
|
70
|
+
`${f.id}.json`,
|
|
71
|
+
);
|
|
72
|
+
let feature = JSON.parse(f.json);
|
|
73
|
+
if (previous[f.id] === f.json)
|
|
74
|
+
try {
|
|
75
|
+
feature = JSON.parse(await readFile(file, "utf8"));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (e.code !== "ENOENT") throw e;
|
|
78
|
+
}
|
|
79
|
+
if (f.id === "hermes")
|
|
80
|
+
feature = {
|
|
81
|
+
...feature,
|
|
82
|
+
apiUrl: this.gateway.url,
|
|
83
|
+
apiKey: this.gateway.token,
|
|
84
|
+
};
|
|
85
|
+
await atomicWrite(file, JSON.stringify(feature, null, 2));
|
|
86
|
+
}
|
|
87
|
+
await atomicWrite(marker, JSON.stringify(applied));
|
|
88
|
+
this.stopping = false;
|
|
89
|
+
this.state = "starting";
|
|
90
|
+
this.child = fork(
|
|
91
|
+
fileURLToPath(new URL("../bot.cjs", import.meta.url)),
|
|
92
|
+
[],
|
|
93
|
+
{
|
|
94
|
+
cwd,
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"],
|
|
97
|
+
env: {
|
|
98
|
+
...process.env,
|
|
99
|
+
SYNAPSE_PROFILE: this.paths.profile,
|
|
100
|
+
SYNAPSE_HOME: this.paths.home,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
this.stderr = "";
|
|
105
|
+
this.child.stderr?.on("data", (chunk) => {
|
|
106
|
+
this.stderr = (this.stderr + chunk.toString()).slice(-6000);
|
|
107
|
+
});
|
|
108
|
+
this.child.on("message", (msg: { type?: string }) => {
|
|
109
|
+
if (msg?.type === "ready") this.state = "running";
|
|
110
|
+
if (msg?.type === "restart" && !this.stopping)
|
|
111
|
+
void this.restart().catch(this.onError);
|
|
112
|
+
});
|
|
113
|
+
this.child.on("error", (e) => {
|
|
114
|
+
this.state = "error";
|
|
115
|
+
this.onError(e);
|
|
116
|
+
});
|
|
117
|
+
this.child.on("exit", (code) => {
|
|
118
|
+
this.state = this.stopping ? "stopped" : `exited (${code})`;
|
|
119
|
+
});
|
|
120
|
+
// Parent disconnect guarantees the upstream child cannot outlive its owner.
|
|
121
|
+
}
|
|
122
|
+
async restart() {
|
|
123
|
+
await this.stop();
|
|
124
|
+
await this.start();
|
|
125
|
+
}
|
|
126
|
+
notify(event, payload) {
|
|
127
|
+
if (this.child?.connected)
|
|
128
|
+
this.child.send({ type: "github", event, payload }, (e) => {
|
|
129
|
+
if (e) this.onError(e);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async stop() {
|
|
133
|
+
this.stopping = true;
|
|
134
|
+
const child = this.child;
|
|
135
|
+
this.child = null;
|
|
136
|
+
if (child && child.exitCode === null) {
|
|
137
|
+
const done = once(child, "exit").catch(() => {});
|
|
138
|
+
child.kill();
|
|
139
|
+
await done;
|
|
140
|
+
}
|
|
141
|
+
this.state = "stopped";
|
|
142
|
+
}
|
|
143
|
+
async liveFeatures() {
|
|
144
|
+
const root = path.join(this.paths.data, "bot", "configs", "features");
|
|
145
|
+
let files;
|
|
146
|
+
try {
|
|
147
|
+
files = await readdir(root);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (e.code === "ENOENT") return [];
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
const result = [];
|
|
153
|
+
for (const file of files.filter((f) => f.endsWith(".json"))) {
|
|
154
|
+
const data = JSON.parse(await readFile(path.join(root, file), "utf8"));
|
|
155
|
+
if (file === "hermes.json") {
|
|
156
|
+
delete data.apiKey;
|
|
157
|
+
delete data.apiUrl;
|
|
158
|
+
}
|
|
159
|
+
result.push({ id: file.slice(0, -5), value: data });
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
result.push({
|
|
163
|
+
id: "peak-valley-timer",
|
|
164
|
+
value: JSON.parse(
|
|
165
|
+
await readFile(path.join(root, "../peak-valley-timer.json"), "utf8"),
|
|
166
|
+
),
|
|
167
|
+
});
|
|
168
|
+
} catch (e) {
|
|
169
|
+
if (e.code !== "ENOENT") throw e;
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const inject = ["dshLoaderUi"];
|
|
2
|
+
export function apply(ctx) {
|
|
3
|
+
const ui = ctx.dshLoaderUi ?? ctx.get?.("dshLoaderUi");
|
|
4
|
+
ctx.effect(() =>
|
|
5
|
+
ui.mount("settings.nav", {
|
|
6
|
+
id: "synapse:management",
|
|
7
|
+
render(mount) {
|
|
8
|
+
const link = document.createElement("a");
|
|
9
|
+
link.href = "/synapse";
|
|
10
|
+
link.target = "_blank";
|
|
11
|
+
link.rel = "noopener";
|
|
12
|
+
link.textContent = "Synapse · 通道与记忆";
|
|
13
|
+
link.style.cssText =
|
|
14
|
+
"display:block;padding:10px;color:inherit;font:inherit";
|
|
15
|
+
mount.append(link);
|
|
16
|
+
return () => link.remove();
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { defineConfig, type ConfigField } from "@dsh-plugin/dsh-loader/config";
|
|
2
|
+
const text = (
|
|
3
|
+
key,
|
|
4
|
+
label,
|
|
5
|
+
value = "",
|
|
6
|
+
type: ConfigField["type"] = "text",
|
|
7
|
+
): ConfigField => ({ type, key, label, default: value });
|
|
8
|
+
const flag = (key, label, value = false): ConfigField => ({
|
|
9
|
+
type: "boolean",
|
|
10
|
+
key,
|
|
11
|
+
label,
|
|
12
|
+
default: value,
|
|
13
|
+
});
|
|
14
|
+
const num = (
|
|
15
|
+
key,
|
|
16
|
+
label,
|
|
17
|
+
value: number,
|
|
18
|
+
min: number,
|
|
19
|
+
max: number,
|
|
20
|
+
): ConfigField => ({ type: "number", key, label, default: value, min, max });
|
|
21
|
+
export const FEATURES = [
|
|
22
|
+
"management",
|
|
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
|
+
];
|
|
35
|
+
export const template = defineConfig({
|
|
36
|
+
id: "dsh-synapse",
|
|
37
|
+
packageName: "@dsh-plugin/dsh-synapse",
|
|
38
|
+
title: "Synapse · 通道与记忆",
|
|
39
|
+
description:
|
|
40
|
+
"保存后重新连接通道。Skill、MCP、Hermes 导入位于 Synapse 管理面板。密码仅保存在当前 profile。",
|
|
41
|
+
fields: [
|
|
42
|
+
flag("enabled", "启用机器人"),
|
|
43
|
+
text("profileDir", "当前 profile 绝对路径(自动定位失败时填写)"),
|
|
44
|
+
text("homeDir", "DSH Home 绝对路径(默认 profile 上两级)"),
|
|
45
|
+
{
|
|
46
|
+
type: "group",
|
|
47
|
+
label: "OneBot · NapCat / Snowluma",
|
|
48
|
+
fields: [
|
|
49
|
+
text("httpUrl", "HTTP 地址", "http://127.0.0.1:3000"),
|
|
50
|
+
text("wsUrl", "WebSocket 地址", "ws://127.0.0.1:3001"),
|
|
51
|
+
text("httpToken", "HTTP Token", "", "password"),
|
|
52
|
+
text("wsToken", "WS Token", "", "password"),
|
|
53
|
+
text("chromePath", "Chrome / Chromium 路径"),
|
|
54
|
+
text("botJson", "上游 bot-config 扩展 JSON", "{}", "textarea"),
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "group",
|
|
59
|
+
label: "AI 对话 / 审查",
|
|
60
|
+
fields: [
|
|
61
|
+
text("llmUrl", "OpenAI 兼容 API 地址", "https://api.deepseek.com/v1"),
|
|
62
|
+
text("llmKey", "API Key", "", "password"),
|
|
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"),
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "group",
|
|
73
|
+
label: "GitHub",
|
|
74
|
+
fields: [
|
|
75
|
+
flag("githubEnabled", "启用 Webhook 审查"),
|
|
76
|
+
text("githubToken", "GitHub Token", "", "password"),
|
|
77
|
+
text("webhookSecret", "Webhook Secret", "", "password"),
|
|
78
|
+
text(
|
|
79
|
+
"repositories",
|
|
80
|
+
"允许的 owner/repo(每行一个,支持 owner/*)",
|
|
81
|
+
"",
|
|
82
|
+
"textarea",
|
|
83
|
+
),
|
|
84
|
+
flag("publishReviews", "自动发布审查评论"),
|
|
85
|
+
num("maxDiffChars", "最大差异字符数", 80000, 1000, 500000),
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: "group",
|
|
90
|
+
label: "RAG / Skill 自总结",
|
|
91
|
+
fields: [
|
|
92
|
+
flag("ragEnabled", "启用本地混合记忆", true),
|
|
93
|
+
num("recallK", "初步召回数量", 50, 1, 200),
|
|
94
|
+
num("memoryK", "上下文记忆条数", 8, 1, 50),
|
|
95
|
+
flag("reranker", "启用本地 bge-reranker-base"),
|
|
96
|
+
flag("learnMemory", "总结重要事项到长期记忆", true),
|
|
97
|
+
flag("learnSkills", "自动总结 Skill", true),
|
|
98
|
+
num("maxMemories", "最大记忆条数", 20000, 100, 1000000),
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: "array",
|
|
103
|
+
key: "features",
|
|
104
|
+
label: "gugle-chat-bot 全量功能配置(JSON)",
|
|
105
|
+
default: FEATURES.map((id) => ({
|
|
106
|
+
id,
|
|
107
|
+
json: JSON.stringify(
|
|
108
|
+
id === "github"
|
|
109
|
+
? { version: 2, groups: {} }
|
|
110
|
+
: id === "welcome"
|
|
111
|
+
? { version: 1, welcomes: [] }
|
|
112
|
+
: id === "discord-bridge"
|
|
113
|
+
? { version: 1, token: "", bridges: {} }
|
|
114
|
+
: { version: 1, groups: [] },
|
|
115
|
+
null,
|
|
116
|
+
2,
|
|
117
|
+
),
|
|
118
|
+
})),
|
|
119
|
+
item: {
|
|
120
|
+
type: "object",
|
|
121
|
+
label: "功能",
|
|
122
|
+
fields: [
|
|
123
|
+
{
|
|
124
|
+
type: "select",
|
|
125
|
+
key: "id",
|
|
126
|
+
label: "模块",
|
|
127
|
+
options: FEATURES.map((value) => ({ label: value, value })),
|
|
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
|
+
],
|
|
168
|
+
});
|
|
169
|
+
export function parseObject(raw) {
|
|
170
|
+
const value = JSON.parse(raw);
|
|
171
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
172
|
+
throw new Error("需要 JSON 对象");
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
export function validateConfig(c) {
|
|
176
|
+
parseObject(c.botJson);
|
|
177
|
+
const ids = new Set();
|
|
178
|
+
for (const f of c.features) {
|
|
179
|
+
if (!FEATURES.includes(f.id) || ids.has(f.id))
|
|
180
|
+
throw new Error("功能 ID 无效或重复");
|
|
181
|
+
ids.add(f.id);
|
|
182
|
+
parseObject(f.json);
|
|
183
|
+
}
|
|
184
|
+
const names = new Set();
|
|
185
|
+
for (const s of c.mcp) {
|
|
186
|
+
if (!/^[a-zA-Z0-9_-]{1,32}$/.test(s.name) || names.has(s.name))
|
|
187
|
+
throw new Error("MCP 名称无效或重复");
|
|
188
|
+
names.add(s.name);
|
|
189
|
+
const args = JSON.parse(s.argsJson);
|
|
190
|
+
if (!Array.isArray(args) || args.some((x) => typeof x !== "string"))
|
|
191
|
+
throw new Error("MCP 参数必须是字符串数组");
|
|
192
|
+
for (const raw of [s.envJson, s.headersJson])
|
|
193
|
+
if (Object.values(parseObject(raw)).some((x) => typeof x !== "string"))
|
|
194
|
+
throw new Error("MCP 环境变量和请求头必须是字符串");
|
|
195
|
+
if (s.transport !== "stdio" && !/^https?:\/\//.test(s.url))
|
|
196
|
+
throw new Error("MCP URL 无效");
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
c.githubEnabled &&
|
|
200
|
+
(!c.webhookSecret || !c.githubToken || !c.repositories.trim())
|
|
201
|
+
)
|
|
202
|
+
throw new Error("启用 GitHub 需要 Secret、Token 和仓库允许列表");
|
|
203
|
+
for (const k of [
|
|
204
|
+
"recallK",
|
|
205
|
+
"memoryK",
|
|
206
|
+
"maxMemories",
|
|
207
|
+
"maxToolRounds",
|
|
208
|
+
"timeoutSec",
|
|
209
|
+
"maxDiffChars",
|
|
210
|
+
])
|
|
211
|
+
if (!Number.isInteger(c[k])) throw new Error(`${k} 必须是整数`);
|
|
212
|
+
for (const key of ["httpUrl", "llmUrl", "hermesUrl"])
|
|
213
|
+
if (!/^https?:\/\//.test(c[key]))
|
|
214
|
+
throw new Error(`${key} 必须是 HTTP(S) URL`);
|
|
215
|
+
if (!/^wss?:\/\//.test(c.wsUrl)) throw new Error("wsUrl 必须是 WS(S) URL");
|
|
216
|
+
return c;
|
|
217
|
+
}
|
package/src/files.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { PluginPaths, Skill } from "./types.js";
|
|
2
|
+
import {
|
|
3
|
+
mkdir,
|
|
4
|
+
readFile,
|
|
5
|
+
writeFile,
|
|
6
|
+
rename,
|
|
7
|
+
lstat,
|
|
8
|
+
readdir,
|
|
9
|
+
unlink,
|
|
10
|
+
} from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
14
|
+
export const ID = "dsh-synapse";
|
|
15
|
+
export function pathsFor(ctx, c) {
|
|
16
|
+
let profile = c.profileDir;
|
|
17
|
+
const url =
|
|
18
|
+
ctx.baseUrl ??
|
|
19
|
+
ctx.get?.("loader")?.ctx?.baseUrl ??
|
|
20
|
+
ctx.get?.("loader")?.config?.baseUrl;
|
|
21
|
+
if (!profile && typeof url === "string" && url.startsWith("file:"))
|
|
22
|
+
profile = fileURLToPath(new URL(".", url));
|
|
23
|
+
if (!profile || !path.isAbsolute(profile))
|
|
24
|
+
throw new Error("无法定位当前 profile;请填写 profileDir 绝对路径");
|
|
25
|
+
const home = c.homeDir || path.resolve(profile, "../..");
|
|
26
|
+
if (!path.isAbsolute(home)) throw new Error("homeDir 必须是绝对路径");
|
|
27
|
+
return {
|
|
28
|
+
profile,
|
|
29
|
+
home,
|
|
30
|
+
data: path.join(profile, "data", ID),
|
|
31
|
+
skills: path.join(profile, "skills"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function safePath(root, ...parts) {
|
|
35
|
+
const target = path.resolve(root, ...parts),
|
|
36
|
+
relative = path.relative(path.resolve(root), target);
|
|
37
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
38
|
+
throw new Error("路径越界");
|
|
39
|
+
// System ancestors may be symlinks (macOS /var -> /private/var).
|
|
40
|
+
// The configured root itself and every path inside it must stay link-free.
|
|
41
|
+
let current = path.resolve(root);
|
|
42
|
+
for (const part of ["", ...relative.split(path.sep).filter(Boolean)]) {
|
|
43
|
+
current = path.join(current, part);
|
|
44
|
+
try {
|
|
45
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
46
|
+
throw new Error("不允许符号链接路径");
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (e.code !== "ENOENT") throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return target;
|
|
52
|
+
}
|
|
53
|
+
export async function atomicWrite(file, content) {
|
|
54
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
55
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
56
|
+
try {
|
|
57
|
+
await writeFile(tmp, content, {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
flag: "wx",
|
|
60
|
+
mode: 0o600,
|
|
61
|
+
});
|
|
62
|
+
await rename(tmp, file);
|
|
63
|
+
} finally {
|
|
64
|
+
await unlink(tmp).catch((e) => {
|
|
65
|
+
if (e.code !== "ENOENT") throw e;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function promptFor(paths, channel) {
|
|
70
|
+
const name = channel === "review" ? "AGENTS.md" : "SOUL.md";
|
|
71
|
+
for (const root of [paths.profile, paths.home]) {
|
|
72
|
+
try {
|
|
73
|
+
return await readFile(await safePath(root, name), "utf8");
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e.code !== "ENOENT") throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return channel === "review"
|
|
79
|
+
? "Review changes for concrete defects. Explain evidence and actionable fixes."
|
|
80
|
+
: "你是一位友善、准确的聊天助手。";
|
|
81
|
+
}
|
|
82
|
+
export function skillName(name) {
|
|
83
|
+
if (
|
|
84
|
+
!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) ||
|
|
85
|
+
/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(name)
|
|
86
|
+
)
|
|
87
|
+
throw new Error("Skill 名称无效");
|
|
88
|
+
return name;
|
|
89
|
+
}
|
|
90
|
+
export const revisionOf = (text) =>
|
|
91
|
+
createHash("sha256").update(text).digest("hex");
|
|
92
|
+
export class SkillStore {
|
|
93
|
+
paths: PluginPaths;
|
|
94
|
+
queue: Promise<unknown>;
|
|
95
|
+
|
|
96
|
+
constructor(paths: PluginPaths) {
|
|
97
|
+
this.paths = paths;
|
|
98
|
+
this.queue = Promise.resolve();
|
|
99
|
+
}
|
|
100
|
+
async list() {
|
|
101
|
+
const found = new Map<string, Skill>();
|
|
102
|
+
for (const root of [
|
|
103
|
+
path.join(this.paths.home, "skills"),
|
|
104
|
+
this.paths.skills,
|
|
105
|
+
]) {
|
|
106
|
+
let names;
|
|
107
|
+
try {
|
|
108
|
+
names = await readdir(await safePath(root));
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (e.code === "ENOENT") continue;
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
for (const entry of names) {
|
|
114
|
+
const name = entry.endsWith(".md") ? entry.slice(0, -3) : entry;
|
|
115
|
+
try {
|
|
116
|
+
skillName(name);
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const file = await safePath(
|
|
121
|
+
root,
|
|
122
|
+
entry,
|
|
123
|
+
...(entry.endsWith(".md") ? [] : ["SKILL.md"]),
|
|
124
|
+
);
|
|
125
|
+
try {
|
|
126
|
+
const content = await readFile(file, "utf8");
|
|
127
|
+
found.set(name, {
|
|
128
|
+
name,
|
|
129
|
+
content,
|
|
130
|
+
revision: revisionOf(content),
|
|
131
|
+
inherited: root !== this.paths.skills,
|
|
132
|
+
});
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (!["ENOENT", "EISDIR", "ENOTDIR"].includes(e.code)) throw e;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [...found.values()];
|
|
139
|
+
}
|
|
140
|
+
mutate(fn) {
|
|
141
|
+
const task = this.queue.then(fn);
|
|
142
|
+
this.queue = task.catch(() => {});
|
|
143
|
+
return task;
|
|
144
|
+
}
|
|
145
|
+
save(name, content, revision = null) {
|
|
146
|
+
return this.mutate(async () => {
|
|
147
|
+
skillName(name);
|
|
148
|
+
if (
|
|
149
|
+
typeof content !== "string" ||
|
|
150
|
+
!content.trim() ||
|
|
151
|
+
content.length > 200000
|
|
152
|
+
)
|
|
153
|
+
throw new Error("Skill 文档为空或过大");
|
|
154
|
+
const current = (await this.list()).find((x) => x.name === name);
|
|
155
|
+
if ((current?.revision ?? null) !== revision)
|
|
156
|
+
throw Object.assign(new Error("Skill 已变更,请重新载入"), {
|
|
157
|
+
status: 409,
|
|
158
|
+
});
|
|
159
|
+
const flat = await safePath(this.paths.skills, `${name}.md`);
|
|
160
|
+
const useFlat = await lstat(flat).then(
|
|
161
|
+
() => true,
|
|
162
|
+
(e) => {
|
|
163
|
+
if (e.code === "ENOENT") return false;
|
|
164
|
+
throw e;
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
await atomicWrite(
|
|
168
|
+
useFlat ? flat : await safePath(this.paths.skills, name, "SKILL.md"),
|
|
169
|
+
content,
|
|
170
|
+
);
|
|
171
|
+
return { name, revision: revisionOf(content) };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
remove(name, revision) {
|
|
175
|
+
return this.mutate(async () => {
|
|
176
|
+
skillName(name);
|
|
177
|
+
const current = (await this.list()).find((x) => x.name === name);
|
|
178
|
+
if (!current || current.inherited)
|
|
179
|
+
throw new Error("只能删除当前 profile 的 Skill");
|
|
180
|
+
if (current.revision !== revision)
|
|
181
|
+
throw Object.assign(new Error("Skill 已变更"), { status: 409 });
|
|
182
|
+
for (const parts of [[name, "SKILL.md"], [`${name}.md`]])
|
|
183
|
+
await unlink(await safePath(this.paths.skills, ...parts)).catch((e) => {
|
|
184
|
+
if (e.code !== "ENOENT") throw e;
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async retrieve(query, channel) {
|
|
189
|
+
const terms = query.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) || [];
|
|
190
|
+
return (await this.list())
|
|
191
|
+
.filter(
|
|
192
|
+
(x) =>
|
|
193
|
+
!x.name.startsWith("auto-") || x.name.startsWith(`auto-${channel}-`),
|
|
194
|
+
)
|
|
195
|
+
.map((x) => ({
|
|
196
|
+
...x,
|
|
197
|
+
score: terms.reduce(
|
|
198
|
+
(n, t) => n + (x.content.toLowerCase().includes(t) ? 1 : 0),
|
|
199
|
+
0,
|
|
200
|
+
),
|
|
201
|
+
}))
|
|
202
|
+
.filter((x) => x.score > 0)
|
|
203
|
+
.sort((a, b) => b.score - a.score)
|
|
204
|
+
.slice(0, 3);
|
|
205
|
+
}
|
|
206
|
+
}
|