@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { template, validateConfig } from "./config.js";
|
|
4
|
+
import { pathsFor, SkillStore, safePath } from "./files.js";
|
|
5
|
+
import { Inference } from "./runtime.js";
|
|
6
|
+
import { Memory, parseHermes } from "./memory.js";
|
|
7
|
+
import { McpManager } from "./mcp.js";
|
|
8
|
+
import { Agent } from "./agent.js";
|
|
9
|
+
import { Gateway } from "./gateway.js";
|
|
10
|
+
import { Bot } from "./bot.js";
|
|
11
|
+
import { Reviews } from "./github.js";
|
|
12
|
+
import { json, body, browserAuthorized } from "./http.js";
|
|
13
|
+
import type { SynapseContext, SynapseConfig, ErrorSink } from "./types.js";
|
|
14
|
+
export const name = "@dsh-plugin/dsh-synapse";
|
|
15
|
+
export const inject = ["dshLoader"];
|
|
16
|
+
export { template };
|
|
17
|
+
export async function createService(
|
|
18
|
+
ctx: SynapseContext,
|
|
19
|
+
c: SynapseConfig,
|
|
20
|
+
onError: ErrorSink,
|
|
21
|
+
) {
|
|
22
|
+
validateConfig(c);
|
|
23
|
+
const paths = pathsFor(ctx, c);
|
|
24
|
+
await safePath(paths.profile, "data", "dsh-synapse");
|
|
25
|
+
await mkdir(paths.data, { recursive: true });
|
|
26
|
+
const config = () => c,
|
|
27
|
+
inference = new Inference(paths.data),
|
|
28
|
+
skills = new SkillStore(paths);
|
|
29
|
+
const memory = new Memory(
|
|
30
|
+
path.join(paths.data, "memory.sqlite"),
|
|
31
|
+
inference,
|
|
32
|
+
config,
|
|
33
|
+
),
|
|
34
|
+
mcp = new McpManager(config);
|
|
35
|
+
const agent = new Agent({ config, paths, memory, skills, mcp, onError }),
|
|
36
|
+
gateway = new Gateway(agent, config, onError);
|
|
37
|
+
const bot = new Bot(paths, config, gateway, onError),
|
|
38
|
+
reviews = new Reviews(memory.db, agent, config, bot, onError);
|
|
39
|
+
const close = async () => {
|
|
40
|
+
await Promise.allSettled([
|
|
41
|
+
reviews.close(),
|
|
42
|
+
bot.stop(),
|
|
43
|
+
gateway.close(),
|
|
44
|
+
mcp.close(),
|
|
45
|
+
inference.close(),
|
|
46
|
+
]);
|
|
47
|
+
memory.close();
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
await gateway.start();
|
|
51
|
+
await bot.start();
|
|
52
|
+
void reviews.drain();
|
|
53
|
+
if (c.enabled && c.ragEnabled)
|
|
54
|
+
void inference.embed("初始化记忆模型").catch(onError);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
await close();
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
paths,
|
|
61
|
+
config,
|
|
62
|
+
inference,
|
|
63
|
+
skills,
|
|
64
|
+
memory,
|
|
65
|
+
mcp,
|
|
66
|
+
agent,
|
|
67
|
+
gateway,
|
|
68
|
+
bot,
|
|
69
|
+
reviews,
|
|
70
|
+
close,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function apply(ctx: SynapseContext) {
|
|
74
|
+
const handle = await ctx.dshLoader.config.register<SynapseConfig>(
|
|
75
|
+
ctx,
|
|
76
|
+
template,
|
|
77
|
+
);
|
|
78
|
+
let service,
|
|
79
|
+
disposed = false,
|
|
80
|
+
lastError = "",
|
|
81
|
+
queue = Promise.resolve();
|
|
82
|
+
const onError = (e) => {
|
|
83
|
+
lastError = e.message;
|
|
84
|
+
ctx.logger?.warn?.(`[synapse] ${e.message}`);
|
|
85
|
+
};
|
|
86
|
+
const enqueue = (fn) => {
|
|
87
|
+
const result = queue.then(fn);
|
|
88
|
+
queue = result.catch(onError);
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
const reconfigure = (value) =>
|
|
92
|
+
enqueue(async () => {
|
|
93
|
+
if (disposed) return;
|
|
94
|
+
validateConfig(value);
|
|
95
|
+
pathsFor(ctx, value);
|
|
96
|
+
if (service) {
|
|
97
|
+
const old = service;
|
|
98
|
+
service = null;
|
|
99
|
+
await old.close();
|
|
100
|
+
}
|
|
101
|
+
service = await createService(ctx, value, onError);
|
|
102
|
+
lastError = "";
|
|
103
|
+
});
|
|
104
|
+
const off = handle.watch(
|
|
105
|
+
({ value }) => void reconfigure(value).catch(() => {}),
|
|
106
|
+
);
|
|
107
|
+
const manage = async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
if (!browserAuthorized(ctx, req, res)) return;
|
|
110
|
+
const url = new URL(req.url, "http://localhost");
|
|
111
|
+
const action = url.pathname.slice("/api/dsh-synapse/".length);
|
|
112
|
+
if (action === "status")
|
|
113
|
+
return json(res, {
|
|
114
|
+
running: Boolean(service),
|
|
115
|
+
bot: service?.bot.state || "stopped",
|
|
116
|
+
lastError,
|
|
117
|
+
});
|
|
118
|
+
if (!service)
|
|
119
|
+
return json(res, { error: lastError || "服务尚未就绪" }, 503);
|
|
120
|
+
// Serialize management with configuration changes; no writes into a closing DB.
|
|
121
|
+
await enqueue(async () => {
|
|
122
|
+
if (!service) throw new Error("服务正在重载");
|
|
123
|
+
if (req.method === "GET" && action === "skills")
|
|
124
|
+
return json(res, await service.skills.list());
|
|
125
|
+
if (req.method === "GET" && action === "reviews")
|
|
126
|
+
return json(res, service.reviews.list());
|
|
127
|
+
if (req.method === "GET" && action === "memory")
|
|
128
|
+
return json(
|
|
129
|
+
res,
|
|
130
|
+
service.memory.list(url.searchParams.get("channel") || "chat"),
|
|
131
|
+
);
|
|
132
|
+
if (req.method === "GET" && action === "features") {
|
|
133
|
+
const live = await service.bot.liveFeatures();
|
|
134
|
+
return json(
|
|
135
|
+
res,
|
|
136
|
+
service.config().features.map((f) => ({
|
|
137
|
+
id: f.id,
|
|
138
|
+
value: {
|
|
139
|
+
...JSON.parse(f.json),
|
|
140
|
+
...(live.find((x) => x.id === f.id)?.value || {}),
|
|
141
|
+
},
|
|
142
|
+
})),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (req.method === "POST") {
|
|
146
|
+
const data = JSON.parse((await body(req)).toString() || "{}");
|
|
147
|
+
if (action === "skills/save")
|
|
148
|
+
return json(
|
|
149
|
+
res,
|
|
150
|
+
await service.skills.save(data.name, data.content, data.revision),
|
|
151
|
+
);
|
|
152
|
+
if (action === "skills/delete") {
|
|
153
|
+
await service.skills.remove(data.name, data.revision);
|
|
154
|
+
return json(res, { ok: true });
|
|
155
|
+
}
|
|
156
|
+
if (action === "mcp/tools")
|
|
157
|
+
return json(res, await service.mcp.list(data.name));
|
|
158
|
+
if (action === "memory/import") {
|
|
159
|
+
if (!service.config().ragEnabled) throw new Error("请先启用 RAG");
|
|
160
|
+
const items = parseHermes(data.markdown, data.channel);
|
|
161
|
+
return json(
|
|
162
|
+
res,
|
|
163
|
+
data.preview ? { items } : await service.memory.addMany(items),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (action === "memory/search")
|
|
167
|
+
return json(
|
|
168
|
+
res,
|
|
169
|
+
await service.memory.search(String(data.query), data.channel),
|
|
170
|
+
);
|
|
171
|
+
if (action === "reviews/retry") {
|
|
172
|
+
service.reviews.retry(data.id);
|
|
173
|
+
return json(res, { ok: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
json(res, { error: "Not found" }, 404);
|
|
177
|
+
});
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (!res.headersSent) json(res, { error: e.message }, e.status || 400);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const routes = [];
|
|
183
|
+
ctx.effect(() => () => {
|
|
184
|
+
disposed = true;
|
|
185
|
+
off();
|
|
186
|
+
for (const release of routes) release();
|
|
187
|
+
void enqueue(async () => {
|
|
188
|
+
await service?.close();
|
|
189
|
+
service = null;
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
routes.push(ctx.dshLoader.web.register("/api/dsh-synapse/", manage));
|
|
193
|
+
routes.push(
|
|
194
|
+
ctx.dshLoader.web.post("/hooks/dsh-synapse/github", (req, res) => {
|
|
195
|
+
void enqueue(async () => {
|
|
196
|
+
if (!service) return json(res, { error: "服务尚未就绪" }, 503);
|
|
197
|
+
await service.reviews.webhook(req, res);
|
|
198
|
+
}).catch((e) => {
|
|
199
|
+
if (!res.headersSent) json(res, { error: e.message }, e.status || 400);
|
|
200
|
+
});
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
// A same-origin standalone panel is also available for older DSH clients.
|
|
204
|
+
routes.push(
|
|
205
|
+
ctx.dshLoader.web.get("/synapse", async (req, res) => {
|
|
206
|
+
if (!browserAuthorized(ctx, req, res)) return;
|
|
207
|
+
const html = await readFile(
|
|
208
|
+
new URL("./panel.html", import.meta.url),
|
|
209
|
+
"utf8",
|
|
210
|
+
);
|
|
211
|
+
res.writeHead(200, {
|
|
212
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
213
|
+
"Content-Security-Policy":
|
|
214
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'",
|
|
215
|
+
});
|
|
216
|
+
res.end(html);
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
routes.push(
|
|
220
|
+
ctx.dshLoader.web.get("/synapse-panel.js", async (req, res) => {
|
|
221
|
+
if (!browserAuthorized(ctx, req, res)) return;
|
|
222
|
+
res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8" });
|
|
223
|
+
res.end(await readFile(new URL("./panel.js", import.meta.url), "utf8"));
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
await reconfigure(handle.get().value).catch(() => {});
|
|
227
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
const require = createRequire(workerData.manifest);
|
|
5
|
+
const runtime = await import(
|
|
6
|
+
pathToFileURL(require.resolve("@huggingface/transformers")).href
|
|
7
|
+
);
|
|
8
|
+
const { pipeline, env, AutoTokenizer, AutoModelForSequenceClassification } =
|
|
9
|
+
runtime.default ?? runtime;
|
|
10
|
+
env.cacheDir = workerData.cache;
|
|
11
|
+
env.allowLocalModels = false;
|
|
12
|
+
let embedder, tokenizer, ranker;
|
|
13
|
+
async function load(factory) {
|
|
14
|
+
try {
|
|
15
|
+
env.remoteHost = "https://huggingface.co/";
|
|
16
|
+
return await factory();
|
|
17
|
+
} catch (direct) {
|
|
18
|
+
env.remoteHost = "https://hf-mirror.com/";
|
|
19
|
+
try {
|
|
20
|
+
return await factory();
|
|
21
|
+
} catch (mirror) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`模型直连和镜像均失败:${direct.message}; ${mirror.message}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let queue = Promise.resolve();
|
|
29
|
+
parentPort.on("message", (message) => {
|
|
30
|
+
queue = queue
|
|
31
|
+
.then(async () => {
|
|
32
|
+
const { id, method, args } = message;
|
|
33
|
+
try {
|
|
34
|
+
let value;
|
|
35
|
+
if (method === "embed") {
|
|
36
|
+
embedder ??= await load(() =>
|
|
37
|
+
pipeline("feature-extraction", "Xenova/bge-small-zh-v1.5", {
|
|
38
|
+
dtype: "q8",
|
|
39
|
+
device: "cpu",
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
const output = await embedder(
|
|
43
|
+
(args.query ? "为这个句子生成表示以用于检索相关文章:" : "") +
|
|
44
|
+
args.text,
|
|
45
|
+
{
|
|
46
|
+
pooling: "cls",
|
|
47
|
+
normalize: true,
|
|
48
|
+
truncation: true,
|
|
49
|
+
max_length: 512,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
value = Array.from(output.data);
|
|
53
|
+
} else if (method === "rerank") {
|
|
54
|
+
tokenizer ??= await load(() =>
|
|
55
|
+
AutoTokenizer.from_pretrained("Xenova/bge-reranker-base"),
|
|
56
|
+
);
|
|
57
|
+
ranker ??= await load(() =>
|
|
58
|
+
AutoModelForSequenceClassification.from_pretrained(
|
|
59
|
+
"Xenova/bge-reranker-base",
|
|
60
|
+
{ dtype: "q8", device: "cpu" },
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
value = [];
|
|
64
|
+
for (let i = 0; i < args.documents.length; i += 4) {
|
|
65
|
+
const docs = args.documents.slice(i, i + 4);
|
|
66
|
+
const inputs = tokenizer(
|
|
67
|
+
docs.map(() => args.query),
|
|
68
|
+
{
|
|
69
|
+
text_pair: docs,
|
|
70
|
+
padding: true,
|
|
71
|
+
truncation: true,
|
|
72
|
+
max_length: 512,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
const { logits } = await ranker(inputs);
|
|
76
|
+
value.push(
|
|
77
|
+
...Array.from(logits.data as Float32Array).map(
|
|
78
|
+
(n) => 1 / (1 + Math.exp(-n)),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} else throw new Error("未知推理方法");
|
|
83
|
+
parentPort.postMessage({ id, value });
|
|
84
|
+
} catch (e) {
|
|
85
|
+
parentPort.postMessage({ id, error: e.message });
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
.catch((e) => parentPort.postMessage({ id: message.id, error: e.message }));
|
|
89
|
+
});
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ConfigSource, AvailableTool } from "./types.js";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
6
|
+
export class McpManager {
|
|
7
|
+
config: ConfigSource;
|
|
8
|
+
connections: Map<string, Promise<Client>>;
|
|
9
|
+
closed: boolean;
|
|
10
|
+
|
|
11
|
+
constructor(config: ConfigSource) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.connections = new Map();
|
|
14
|
+
this.closed = false;
|
|
15
|
+
}
|
|
16
|
+
async connect(name) {
|
|
17
|
+
if (this.closed) throw new Error("MCP 已关闭");
|
|
18
|
+
const spec = this.config().mcp.find((x) => x.name === name && x.enabled);
|
|
19
|
+
if (!spec) throw new Error("MCP 服务未启用");
|
|
20
|
+
if (!this.connections.has(name))
|
|
21
|
+
this.connections.set(
|
|
22
|
+
name,
|
|
23
|
+
(async () => {
|
|
24
|
+
const client = new Client({ name: "dsh-synapse", version: "0.1.0" });
|
|
25
|
+
const transport =
|
|
26
|
+
spec.transport === "stdio"
|
|
27
|
+
? new StdioClientTransport({
|
|
28
|
+
command: spec.command,
|
|
29
|
+
args: JSON.parse(spec.argsJson),
|
|
30
|
+
env: {
|
|
31
|
+
PATH: process.env.PATH ?? "",
|
|
32
|
+
...(process.env.SystemRoot
|
|
33
|
+
? { SystemRoot: process.env.SystemRoot }
|
|
34
|
+
: {}),
|
|
35
|
+
...JSON.parse(spec.envJson),
|
|
36
|
+
},
|
|
37
|
+
stderr: "ignore",
|
|
38
|
+
})
|
|
39
|
+
: spec.transport === "sse"
|
|
40
|
+
? new SSEClientTransport(new URL(spec.url), {
|
|
41
|
+
requestInit: { headers: JSON.parse(spec.headersJson) },
|
|
42
|
+
})
|
|
43
|
+
: new StreamableHTTPClientTransport(new URL(spec.url), {
|
|
44
|
+
requestInit: { headers: JSON.parse(spec.headersJson) },
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
await client.connect(transport, { timeout: 15000 });
|
|
48
|
+
if (this.closed) {
|
|
49
|
+
await client.close();
|
|
50
|
+
throw new Error("MCP 已关闭");
|
|
51
|
+
}
|
|
52
|
+
return client;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
await transport.close().catch(() => {});
|
|
55
|
+
this.connections.delete(name);
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
})(),
|
|
59
|
+
);
|
|
60
|
+
return this.connections.get(name);
|
|
61
|
+
}
|
|
62
|
+
async list(name) {
|
|
63
|
+
const c = await this.connect(name);
|
|
64
|
+
let cursor;
|
|
65
|
+
const tools = [];
|
|
66
|
+
do {
|
|
67
|
+
const result = await c.listTools(cursor ? { cursor } : undefined, {
|
|
68
|
+
timeout: 15000,
|
|
69
|
+
});
|
|
70
|
+
tools.push(...result.tools);
|
|
71
|
+
cursor = result.nextCursor;
|
|
72
|
+
if (tools.length > 2000) throw new Error("MCP 工具数量超限");
|
|
73
|
+
} while (cursor);
|
|
74
|
+
return tools;
|
|
75
|
+
}
|
|
76
|
+
async available(): Promise<AvailableTool[]> {
|
|
77
|
+
const result: AvailableTool[] = [];
|
|
78
|
+
for (const spec of this.config().mcp.filter((s) => s.enabled)) {
|
|
79
|
+
const allowed = new Set(spec.allowedTools.split(/\s+/).filter(Boolean));
|
|
80
|
+
if (!allowed.size) continue;
|
|
81
|
+
for (const tool of await this.list(spec.name))
|
|
82
|
+
if (allowed.has(tool.name))
|
|
83
|
+
result.push({
|
|
84
|
+
server: spec.name,
|
|
85
|
+
rawName: tool.name,
|
|
86
|
+
name: `mcp_${result.length}`,
|
|
87
|
+
description: `${spec.name}/${tool.name}: ${tool.description || ""}`,
|
|
88
|
+
parameters: tool.inputSchema,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
async call(tool, args) {
|
|
94
|
+
const spec = this.config().mcp.find(
|
|
95
|
+
(s) => s.name === tool.server && s.enabled,
|
|
96
|
+
);
|
|
97
|
+
if (!spec?.allowedTools.split(/\s+/).includes(tool.rawName))
|
|
98
|
+
throw new Error("工具不在允许列表");
|
|
99
|
+
return (await this.connect(tool.server)).callTool(
|
|
100
|
+
{ name: tool.rawName, arguments: args },
|
|
101
|
+
undefined,
|
|
102
|
+
{ timeout: 60000 },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
async close() {
|
|
106
|
+
this.closed = true;
|
|
107
|
+
await Promise.allSettled(
|
|
108
|
+
[...this.connections.values()].map(async (p) => (await p).close()),
|
|
109
|
+
);
|
|
110
|
+
this.connections.clear();
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/memory.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfigSource,
|
|
3
|
+
Embeddings,
|
|
4
|
+
MemoryRow,
|
|
5
|
+
MemoryItem,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import * as vec from "sqlite-vec";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
const channelCheck = (c) => {
|
|
11
|
+
if (!["chat", "review"].includes(c)) throw new Error("记忆分类无效");
|
|
12
|
+
return c;
|
|
13
|
+
};
|
|
14
|
+
// Whitespace-separated Han characters let unicode61 index Chinese without an external tokenizer.
|
|
15
|
+
export const tokenize = (text: string): string =>
|
|
16
|
+
(text.toLowerCase().match(/[\p{Script=Han}]|[\p{L}\p{N}_]+/gu) || []).join(
|
|
17
|
+
" ",
|
|
18
|
+
);
|
|
19
|
+
export function parseHermes(markdown, channel = "chat") {
|
|
20
|
+
channelCheck(channel);
|
|
21
|
+
if (typeof markdown !== "string" || markdown.length > 2000000)
|
|
22
|
+
throw new Error("记忆文档无效或超过 2 MB");
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let heading = "Hermes memory",
|
|
25
|
+
lines = [];
|
|
26
|
+
const flush = () => {
|
|
27
|
+
const content = lines.join("\n").trim();
|
|
28
|
+
if (content)
|
|
29
|
+
for (let i = 0; i < content.length; i += 1200)
|
|
30
|
+
chunks.push({
|
|
31
|
+
channel,
|
|
32
|
+
source: "hermes",
|
|
33
|
+
text: `${heading}\n${content.slice(i, i + 1200)}`,
|
|
34
|
+
});
|
|
35
|
+
lines = [];
|
|
36
|
+
};
|
|
37
|
+
const clean = markdown.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, "");
|
|
38
|
+
let fenced = false;
|
|
39
|
+
for (const line of clean.split(/\r?\n/)) {
|
|
40
|
+
if (/^\s*(```|~~~)/.test(line)) fenced = !fenced;
|
|
41
|
+
const match = !fenced && line.match(/^#{1,6}\s+(.+)/);
|
|
42
|
+
if (match) {
|
|
43
|
+
flush();
|
|
44
|
+
heading = match[1];
|
|
45
|
+
} else lines.push(line);
|
|
46
|
+
}
|
|
47
|
+
flush();
|
|
48
|
+
return chunks;
|
|
49
|
+
}
|
|
50
|
+
export class Memory {
|
|
51
|
+
inference: Embeddings;
|
|
52
|
+
config: ConfigSource;
|
|
53
|
+
db: Database.Database;
|
|
54
|
+
|
|
55
|
+
constructor(file: string, inference: Embeddings, config: ConfigSource) {
|
|
56
|
+
this.inference = inference;
|
|
57
|
+
this.config = config;
|
|
58
|
+
this.db = new Database(file);
|
|
59
|
+
this.db.pragma("journal_mode = WAL");
|
|
60
|
+
this.db.pragma("busy_timeout = 5000");
|
|
61
|
+
vec.load(this.db);
|
|
62
|
+
this.db
|
|
63
|
+
.exec(`CREATE TABLE IF NOT EXISTS memories(id INTEGER PRIMARY KEY, channel TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL, hash TEXT NOT NULL UNIQUE, created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);
|
|
64
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(tokens);
|
|
65
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(id INTEGER PRIMARY KEY, embedding float[512], channel TEXT partition key);
|
|
66
|
+
CREATE TABLE IF NOT EXISTS deliveries(id TEXT PRIMARY KEY, event TEXT NOT NULL, payload TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'pending', attempts INTEGER NOT NULL DEFAULT 0, error TEXT, result TEXT, updated TEXT DEFAULT CURRENT_TIMESTAMP);`);
|
|
67
|
+
}
|
|
68
|
+
async addMany(items: MemoryItem[]) {
|
|
69
|
+
const ready = [];
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
channelCheck(item.channel);
|
|
72
|
+
if (!item.text?.trim()) continue;
|
|
73
|
+
const hash = createHash("sha256")
|
|
74
|
+
.update(item.channel + "\0" + item.text)
|
|
75
|
+
.digest("hex");
|
|
76
|
+
if (this.db.prepare("SELECT 1 FROM memories WHERE hash=?").get(hash))
|
|
77
|
+
continue;
|
|
78
|
+
const embedding = await this.inference.embed(item.text);
|
|
79
|
+
if (
|
|
80
|
+
embedding.length !== 512 ||
|
|
81
|
+
embedding.some((n) => !Number.isFinite(n))
|
|
82
|
+
)
|
|
83
|
+
throw new Error("嵌入维度或数值无效");
|
|
84
|
+
ready.push({ ...item, hash, embedding: Float32Array.from(embedding) });
|
|
85
|
+
}
|
|
86
|
+
return this.db.transaction(() => {
|
|
87
|
+
let count = 0;
|
|
88
|
+
for (const item of ready) {
|
|
89
|
+
const result = this.db
|
|
90
|
+
.prepare(
|
|
91
|
+
"INSERT OR IGNORE INTO memories(channel,source,text,hash) VALUES(?,?,?,?)",
|
|
92
|
+
)
|
|
93
|
+
.run(item.channel, item.source, item.text, item.hash);
|
|
94
|
+
if (!result.changes) continue;
|
|
95
|
+
const id = BigInt(result.lastInsertRowid);
|
|
96
|
+
this.db
|
|
97
|
+
.prepare("INSERT INTO memory_fts(rowid,tokens) VALUES(?,?)")
|
|
98
|
+
.run(id, tokenize(item.text));
|
|
99
|
+
this.db
|
|
100
|
+
.prepare("INSERT INTO memory_vec(id,embedding,channel) VALUES(?,?,?)")
|
|
101
|
+
.run(id, item.embedding, item.channel);
|
|
102
|
+
count++;
|
|
103
|
+
}
|
|
104
|
+
const old = this.db
|
|
105
|
+
.prepare("SELECT id FROM memories ORDER BY id DESC LIMIT -1 OFFSET ?")
|
|
106
|
+
.all(this.config().maxMemories) as { id: number }[];
|
|
107
|
+
for (const { id } of old) this.remove(id);
|
|
108
|
+
return { inserted: count, duplicates: items.length - count };
|
|
109
|
+
})();
|
|
110
|
+
}
|
|
111
|
+
remove(id) {
|
|
112
|
+
this.db.prepare("DELETE FROM memory_vec WHERE id=?").run(BigInt(id));
|
|
113
|
+
this.db.prepare("DELETE FROM memory_fts WHERE rowid=?").run(id);
|
|
114
|
+
this.db.prepare("DELETE FROM memories WHERE id=?").run(id);
|
|
115
|
+
}
|
|
116
|
+
async search(query, channel) {
|
|
117
|
+
channelCheck(channel);
|
|
118
|
+
const c = this.config();
|
|
119
|
+
if (!c.ragEnabled) return [];
|
|
120
|
+
const embedding = Float32Array.from(
|
|
121
|
+
await this.inference.embed(query, true),
|
|
122
|
+
);
|
|
123
|
+
const vectors = this.db
|
|
124
|
+
.prepare(
|
|
125
|
+
"SELECT id,distance FROM memory_vec WHERE embedding MATCH ? AND k=? AND channel=? ORDER BY distance",
|
|
126
|
+
)
|
|
127
|
+
.all(embedding, c.recallK, channel) as { id: number }[];
|
|
128
|
+
const terms = [
|
|
129
|
+
...new Set(tokenize(query).split(" ").filter(Boolean)),
|
|
130
|
+
].slice(0, 64);
|
|
131
|
+
const lexical = terms.length
|
|
132
|
+
? this.db
|
|
133
|
+
.prepare(
|
|
134
|
+
"SELECT f.rowid AS id FROM memory_fts f JOIN memories m ON m.id=f.rowid WHERE memory_fts MATCH ? AND m.channel=? ORDER BY rank LIMIT ?",
|
|
135
|
+
)
|
|
136
|
+
.all(
|
|
137
|
+
terms.map((t) => '"' + t.replaceAll('"', '""') + '"').join(" OR "),
|
|
138
|
+
channel,
|
|
139
|
+
c.recallK,
|
|
140
|
+
)
|
|
141
|
+
: [];
|
|
142
|
+
const scores = new Map();
|
|
143
|
+
for (const rows of [vectors, lexical] as { id: number }[][])
|
|
144
|
+
rows.forEach(({ id }, i) =>
|
|
145
|
+
scores.set(id, (scores.get(id) || 0) + 1 / (60 + i + 1)),
|
|
146
|
+
);
|
|
147
|
+
let results = [...scores]
|
|
148
|
+
.sort((a, b) => b[1] - a[1])
|
|
149
|
+
.slice(0, c.recallK)
|
|
150
|
+
.map(([id, score]) => ({
|
|
151
|
+
...(this.db
|
|
152
|
+
.prepare("SELECT * FROM memories WHERE id=?")
|
|
153
|
+
.get(id) as MemoryRow),
|
|
154
|
+
score,
|
|
155
|
+
}));
|
|
156
|
+
if (c.reranker && results.length) {
|
|
157
|
+
const ranks = await this.inference.rerank(
|
|
158
|
+
query,
|
|
159
|
+
results.map((x) => x.text),
|
|
160
|
+
);
|
|
161
|
+
results = results
|
|
162
|
+
.map((x, i) => ({ ...x, score: ranks[i] }))
|
|
163
|
+
.sort((a, b) => b.score - a.score);
|
|
164
|
+
}
|
|
165
|
+
return results.slice(0, c.memoryK);
|
|
166
|
+
}
|
|
167
|
+
list(channel, limit = 100) {
|
|
168
|
+
channelCheck(channel);
|
|
169
|
+
return this.db
|
|
170
|
+
.prepare(
|
|
171
|
+
"SELECT * FROM memories WHERE channel=? ORDER BY id DESC LIMIT ?",
|
|
172
|
+
)
|
|
173
|
+
.all(channel, limit);
|
|
174
|
+
}
|
|
175
|
+
close() {
|
|
176
|
+
this.db.close();
|
|
177
|
+
}
|
|
178
|
+
}
|