@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/runtime.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const VERSION = "3.8.1";
|
|
7
|
+
export async function installRuntime(root: string, signal?: AbortSignal) {
|
|
8
|
+
await mkdir(root, { recursive: true });
|
|
9
|
+
const file = path.join(root, "package.json");
|
|
10
|
+
const manifest = {
|
|
11
|
+
name: "dsh-synapse-inference-runtime",
|
|
12
|
+
private: true,
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
dependencies: { "@huggingface/transformers": VERSION },
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
const req = createRequire(file);
|
|
18
|
+
req.resolve("@huggingface/transformers");
|
|
19
|
+
const pkg = JSON.parse(
|
|
20
|
+
await readFile(
|
|
21
|
+
path.join(root, "node_modules/@huggingface/transformers/package.json"),
|
|
22
|
+
"utf8",
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
if (pkg.version === VERSION) return file;
|
|
26
|
+
} catch {}
|
|
27
|
+
await writeFile(file, JSON.stringify(manifest, null, 2));
|
|
28
|
+
// npm is launched without a command shell, including on Windows.
|
|
29
|
+
const candidates = [
|
|
30
|
+
process.env.npm_execpath,
|
|
31
|
+
path.join(
|
|
32
|
+
path.dirname(process.execPath),
|
|
33
|
+
"node_modules/npm/bin/npm-cli.js",
|
|
34
|
+
),
|
|
35
|
+
path.resolve(
|
|
36
|
+
path.dirname(process.execPath),
|
|
37
|
+
"../lib/node_modules/npm/bin/npm-cli.js",
|
|
38
|
+
),
|
|
39
|
+
...(process.env.PATH || "")
|
|
40
|
+
.split(path.delimiter)
|
|
41
|
+
.flatMap((dir) => [
|
|
42
|
+
path.join(dir, "node_modules/npm/bin/npm-cli.js"),
|
|
43
|
+
path.resolve(dir, "../lib/node_modules/npm/bin/npm-cli.js"),
|
|
44
|
+
]),
|
|
45
|
+
...(process.platform === "win32"
|
|
46
|
+
? []
|
|
47
|
+
: ["/usr/share/nodejs/npm/bin/npm-cli.js"]),
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
let npm;
|
|
50
|
+
for (const c of candidates) {
|
|
51
|
+
try {
|
|
52
|
+
await readFile(c);
|
|
53
|
+
npm = c;
|
|
54
|
+
break;
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
if (!npm) throw new Error("找不到 npm-cli.js;请安装 Node.js 的 npm 组件");
|
|
58
|
+
await new Promise<void>((resolve, reject) => {
|
|
59
|
+
const child = spawn(
|
|
60
|
+
process.execPath,
|
|
61
|
+
[npm, "install", "--omit=dev", "--no-audit", "--no-fund"],
|
|
62
|
+
{ cwd: root, windowsHide: true, stdio: "ignore", signal },
|
|
63
|
+
);
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
child.kill();
|
|
66
|
+
reject(new Error("推理运行时安装超时"));
|
|
67
|
+
}, 600000);
|
|
68
|
+
child.on("error", (e) => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
reject(e);
|
|
71
|
+
});
|
|
72
|
+
child.on("exit", (code) => {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
code === 0
|
|
75
|
+
? resolve()
|
|
76
|
+
: reject(new Error(`推理运行时安装失败 (${code})`));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
return file;
|
|
80
|
+
}
|
|
81
|
+
export class Inference {
|
|
82
|
+
data: string;
|
|
83
|
+
pending: Map<
|
|
84
|
+
number,
|
|
85
|
+
{
|
|
86
|
+
resolve: (value: number[]) => void;
|
|
87
|
+
reject: (error: Error) => void;
|
|
88
|
+
timer: NodeJS.Timeout;
|
|
89
|
+
}
|
|
90
|
+
>;
|
|
91
|
+
counter: number;
|
|
92
|
+
closed: boolean;
|
|
93
|
+
starting: Promise<void>;
|
|
94
|
+
worker: Worker;
|
|
95
|
+
installer = new AbortController();
|
|
96
|
+
|
|
97
|
+
constructor(data: string) {
|
|
98
|
+
this.data = data;
|
|
99
|
+
this.pending = new Map();
|
|
100
|
+
this.counter = 0;
|
|
101
|
+
this.closed = false;
|
|
102
|
+
}
|
|
103
|
+
async start() {
|
|
104
|
+
if (this.closed) throw new Error("推理 Worker 已关闭");
|
|
105
|
+
if (!this.starting)
|
|
106
|
+
this.starting = (async () => {
|
|
107
|
+
const manifest = await installRuntime(
|
|
108
|
+
path.join(this.data, "runtime"),
|
|
109
|
+
this.installer.signal,
|
|
110
|
+
);
|
|
111
|
+
if (this.closed) throw new Error("推理 Worker 已关闭");
|
|
112
|
+
this.worker = new Worker(
|
|
113
|
+
new URL("./inference-worker.js", import.meta.url),
|
|
114
|
+
{ workerData: { manifest, cache: path.join(this.data, "models") } },
|
|
115
|
+
);
|
|
116
|
+
this.worker.on("message", ({ id, value, error }) => {
|
|
117
|
+
const p = this.pending.get(id);
|
|
118
|
+
if (!p) return;
|
|
119
|
+
clearTimeout(p.timer);
|
|
120
|
+
this.pending.delete(id);
|
|
121
|
+
error ? p.reject(new Error(error)) : p.resolve(value);
|
|
122
|
+
});
|
|
123
|
+
const fail = (e) => {
|
|
124
|
+
for (const p of this.pending.values()) {
|
|
125
|
+
clearTimeout(p.timer);
|
|
126
|
+
p.reject(e);
|
|
127
|
+
}
|
|
128
|
+
this.pending.clear();
|
|
129
|
+
};
|
|
130
|
+
this.worker.on("error", fail);
|
|
131
|
+
this.worker.on("exit", (code) => {
|
|
132
|
+
this.closed = true;
|
|
133
|
+
fail(new Error(`推理 Worker 退出 (${code})`));
|
|
134
|
+
});
|
|
135
|
+
})();
|
|
136
|
+
return this.starting;
|
|
137
|
+
}
|
|
138
|
+
async call(method, args) {
|
|
139
|
+
await this.start();
|
|
140
|
+
if (this.closed) throw new Error("推理 Worker 已关闭");
|
|
141
|
+
if (this.pending.size >= 100) throw new Error("推理队列已满");
|
|
142
|
+
return new Promise<number[]>((resolve, reject) => {
|
|
143
|
+
const id = ++this.counter;
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
this.pending.delete(id);
|
|
146
|
+
reject(new Error("模型下载或推理超时"));
|
|
147
|
+
}, 600000);
|
|
148
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
149
|
+
this.worker.postMessage({ id, method, args });
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
embed(text, query = false) {
|
|
153
|
+
return this.call("embed", { text, query });
|
|
154
|
+
}
|
|
155
|
+
rerank(query, documents) {
|
|
156
|
+
return this.call("rerank", { query, documents });
|
|
157
|
+
}
|
|
158
|
+
async close() {
|
|
159
|
+
this.closed = true;
|
|
160
|
+
this.installer.abort();
|
|
161
|
+
await this.starting?.catch(() => {});
|
|
162
|
+
await this.worker?.terminate();
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { HostAPI, CordisContext } from "@dsh-plugin/dsh-loader";
|
|
2
|
+
export type Channel = "chat" | "review";
|
|
3
|
+
export interface PluginPaths {
|
|
4
|
+
profile: string;
|
|
5
|
+
home: string;
|
|
6
|
+
data: string;
|
|
7
|
+
skills: string;
|
|
8
|
+
}
|
|
9
|
+
export interface McpSpec {
|
|
10
|
+
name: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
transport: "stdio" | "http" | "sse";
|
|
13
|
+
command: string;
|
|
14
|
+
argsJson: string;
|
|
15
|
+
envJson: string;
|
|
16
|
+
url: string;
|
|
17
|
+
headersJson: string;
|
|
18
|
+
allowedTools: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SynapseConfig {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
profileDir: string;
|
|
23
|
+
homeDir: string;
|
|
24
|
+
httpUrl: string;
|
|
25
|
+
wsUrl: string;
|
|
26
|
+
httpToken: string;
|
|
27
|
+
wsToken: string;
|
|
28
|
+
chromePath: string;
|
|
29
|
+
botJson: string;
|
|
30
|
+
llmUrl: string;
|
|
31
|
+
llmKey: string;
|
|
32
|
+
model: string;
|
|
33
|
+
timeoutSec: number;
|
|
34
|
+
maxToolRounds: number;
|
|
35
|
+
externalHermes: boolean;
|
|
36
|
+
hermesUrl: string;
|
|
37
|
+
hermesKey: string;
|
|
38
|
+
githubEnabled: boolean;
|
|
39
|
+
githubToken: string;
|
|
40
|
+
webhookSecret: string;
|
|
41
|
+
repositories: string;
|
|
42
|
+
publishReviews: boolean;
|
|
43
|
+
maxDiffChars: number;
|
|
44
|
+
ragEnabled: boolean;
|
|
45
|
+
recallK: number;
|
|
46
|
+
memoryK: number;
|
|
47
|
+
reranker: boolean;
|
|
48
|
+
learnMemory: boolean;
|
|
49
|
+
learnSkills: boolean;
|
|
50
|
+
maxMemories: number;
|
|
51
|
+
features: { id: string; json: string }[];
|
|
52
|
+
mcp: McpSpec[];
|
|
53
|
+
}
|
|
54
|
+
export type ConfigSource = () => SynapseConfig;
|
|
55
|
+
export type ErrorSink = (error: Error) => void;
|
|
56
|
+
export interface SynapseContext extends CordisContext {
|
|
57
|
+
dshLoader: HostAPI;
|
|
58
|
+
baseUrl?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface MemoryItem {
|
|
61
|
+
channel: Channel;
|
|
62
|
+
source: string;
|
|
63
|
+
text: string;
|
|
64
|
+
}
|
|
65
|
+
export interface MemoryRow extends MemoryItem {
|
|
66
|
+
id: number;
|
|
67
|
+
hash: string;
|
|
68
|
+
created: string;
|
|
69
|
+
score?: number;
|
|
70
|
+
}
|
|
71
|
+
export interface Embeddings {
|
|
72
|
+
embed(text: string, query?: boolean): Promise<number[]>;
|
|
73
|
+
rerank(query: string, documents: string[]): Promise<number[]>;
|
|
74
|
+
}
|
|
75
|
+
export interface Skill {
|
|
76
|
+
name: string;
|
|
77
|
+
content: string;
|
|
78
|
+
revision: string;
|
|
79
|
+
inherited: boolean;
|
|
80
|
+
}
|
|
81
|
+
export interface AvailableTool {
|
|
82
|
+
server: string;
|
|
83
|
+
rawName: string;
|
|
84
|
+
name: string;
|
|
85
|
+
description: string;
|
|
86
|
+
parameters: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
export interface ChatMessage {
|
|
89
|
+
role: string;
|
|
90
|
+
content?: any;
|
|
91
|
+
tool_calls?: { id: string; function: { name: string; arguments: string } }[];
|
|
92
|
+
tool_call_id?: string;
|
|
93
|
+
}
|
|
94
|
+
export type AgentEvent = (
|
|
95
|
+
event: string,
|
|
96
|
+
payload: Record<string, unknown>,
|
|
97
|
+
) => void;
|
|
98
|
+
export interface Delivery {
|
|
99
|
+
id: string;
|
|
100
|
+
event: string;
|
|
101
|
+
payload: string;
|
|
102
|
+
state: string;
|
|
103
|
+
attempts: number;
|
|
104
|
+
error?: string;
|
|
105
|
+
result?: string;
|
|
106
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, readFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createHmac } from "node:crypto";
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { defaultsFor } from "@dsh-plugin/dsh-loader/config";
|
|
9
|
+
import { template, validateConfig } from "../lib/host/config.js";
|
|
10
|
+
import { SkillStore, promptFor, pathsFor } from "../lib/host/files.js";
|
|
11
|
+
import { Memory, parseHermes } from "../lib/host/memory.js";
|
|
12
|
+
import {
|
|
13
|
+
verifySignature,
|
|
14
|
+
repositoryAllowed,
|
|
15
|
+
Reviews,
|
|
16
|
+
} from "../lib/host/github.js";
|
|
17
|
+
import { Agent } from "../lib/host/agent.js";
|
|
18
|
+
import { Gateway } from "../lib/host/gateway.js";
|
|
19
|
+
import { browserAuthorized } from "../lib/host/http.js";
|
|
20
|
+
const config = () => defaultsFor(template.fields);
|
|
21
|
+
const fixture = async () => {
|
|
22
|
+
const home = await mkdtemp(path.join(os.tmpdir(), "synapse-"));
|
|
23
|
+
const profile = path.join(home, "profiles", "test");
|
|
24
|
+
await mkdir(profile, { recursive: true });
|
|
25
|
+
return {
|
|
26
|
+
home,
|
|
27
|
+
profile,
|
|
28
|
+
skills: path.join(profile, "skills"),
|
|
29
|
+
data: path.join(profile, "data", "dsh-synapse"),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const inference = {
|
|
33
|
+
async embed(text) {
|
|
34
|
+
const v = Array(512).fill(0);
|
|
35
|
+
v[text.includes("猫") ? 0 : 1] = 1;
|
|
36
|
+
return v;
|
|
37
|
+
},
|
|
38
|
+
async rerank(q, docs) {
|
|
39
|
+
return docs.map((t) => (t.includes("重排") ? 1 : 0));
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
test("defaults form valid config and reject malformed JSON / duplicate MCP", () => {
|
|
43
|
+
const c = config();
|
|
44
|
+
assert.equal(validateConfig(c), c);
|
|
45
|
+
assert.throws(() => validateConfig({ ...c, botJson: "[]" }));
|
|
46
|
+
assert.throws(() => validateConfig({ ...c, githubEnabled: true }));
|
|
47
|
+
assert.throws(() => validateConfig({ ...c, recallK: 1.5 }));
|
|
48
|
+
});
|
|
49
|
+
test("profile prompt overrides home; chat and review use distinct files", async () => {
|
|
50
|
+
const p = await fixture();
|
|
51
|
+
await writeFile(path.join(p.home, "SOUL.md"), "home soul");
|
|
52
|
+
await writeFile(path.join(p.home, "AGENTS.md"), "home review");
|
|
53
|
+
assert.equal(await promptFor(p, "chat"), "home soul");
|
|
54
|
+
await writeFile(path.join(p.profile, "SOUL.md"), "profile soul");
|
|
55
|
+
assert.equal(await promptFor(p, "chat"), "profile soul");
|
|
56
|
+
assert.equal(await promptFor(p, "review"), "home review");
|
|
57
|
+
assert.equal(pathsFor({}, { profileDir: p.profile }).home, p.home);
|
|
58
|
+
assert.throws(() => pathsFor({}, {}));
|
|
59
|
+
});
|
|
60
|
+
test("skills import, edit conflicts, inheritance, retrieval, deletion and traversal", async () => {
|
|
61
|
+
const p = await fixture();
|
|
62
|
+
const skills = new SkillStore(p);
|
|
63
|
+
await mkdir(path.join(p.home, "skills"), { recursive: true });
|
|
64
|
+
await writeFile(path.join(p.home, "skills", "global.md"), "global reference");
|
|
65
|
+
const saved = await skills.save(
|
|
66
|
+
"testing",
|
|
67
|
+
"# Testing\nUse assert for tests.",
|
|
68
|
+
);
|
|
69
|
+
assert.equal((await skills.retrieve("assert", "chat")).length, 1);
|
|
70
|
+
await assert.rejects(skills.save("testing", "changed", null), /已变更/);
|
|
71
|
+
await assert.rejects(skills.save("../escape", "oops"), /名称无效/);
|
|
72
|
+
await skills.save("testing", "Updated procedure", saved.revision);
|
|
73
|
+
const now = (await skills.list()).find((s) => s.name === "testing");
|
|
74
|
+
await skills.remove("testing", now.revision);
|
|
75
|
+
assert.equal((await skills.list()).length, 1);
|
|
76
|
+
await assert.rejects(skills.remove("global", "anything"), /当前 profile/);
|
|
77
|
+
});
|
|
78
|
+
test("Hermes headings, frontmatter, fenced text, CRLF, and categories", () => {
|
|
79
|
+
const entries = parseHermes(
|
|
80
|
+
"---\r\nname: memory\r\n---\r\n# 用户\r\n喜欢猫\r\n## 项目\r\n```md\r\n# this is code\r\n```",
|
|
81
|
+
"review",
|
|
82
|
+
);
|
|
83
|
+
assert.equal(entries.length, 2);
|
|
84
|
+
assert.equal(entries[0].channel, "review");
|
|
85
|
+
assert.match(entries[1].text, /# this is code/);
|
|
86
|
+
assert.throws(() => parseHermes("test", "unknown"));
|
|
87
|
+
assert.equal(parseHermes("").length, 0);
|
|
88
|
+
});
|
|
89
|
+
test("actual sqlite-vec + FTS5 hybrid retrieval isolates channels and deduplicates", async () => {
|
|
90
|
+
const c = config();
|
|
91
|
+
const memory = new Memory(":memory:", inference, () => c);
|
|
92
|
+
try {
|
|
93
|
+
await memory.addMany([
|
|
94
|
+
{ channel: "chat", source: "test", text: "猫喜欢鱼" },
|
|
95
|
+
{ channel: "review", source: "test", text: "猫审查代码" },
|
|
96
|
+
{ channel: "chat", source: "test", text: "代码重排" },
|
|
97
|
+
]);
|
|
98
|
+
const result = await memory.addMany([
|
|
99
|
+
{ channel: "chat", source: "hermes", text: "猫喜欢鱼" },
|
|
100
|
+
]);
|
|
101
|
+
assert.equal(result.inserted, 0);
|
|
102
|
+
const found = await memory.search("猫", "chat");
|
|
103
|
+
assert.equal(found[0].text, "猫喜欢鱼");
|
|
104
|
+
assert(found.every((x) => x.channel === "chat"));
|
|
105
|
+
c.reranker = true;
|
|
106
|
+
assert.equal((await memory.search("代码", "chat"))[0].text, "代码重排");
|
|
107
|
+
assert.equal(memory.list("review").length, 1);
|
|
108
|
+
} finally {
|
|
109
|
+
memory.close();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
test("embedding failure does not partially import a batch", async () => {
|
|
113
|
+
let i = 0;
|
|
114
|
+
const memory = new Memory(
|
|
115
|
+
":memory:",
|
|
116
|
+
{
|
|
117
|
+
async embed() {
|
|
118
|
+
if (i++) throw new Error("download failed");
|
|
119
|
+
return Array(512).fill(0);
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
config,
|
|
123
|
+
);
|
|
124
|
+
try {
|
|
125
|
+
await assert.rejects(
|
|
126
|
+
memory.addMany(parseHermes("# A\none\n# B\ntwo")),
|
|
127
|
+
/download failed/,
|
|
128
|
+
);
|
|
129
|
+
assert.equal(memory.list("chat").length, 0);
|
|
130
|
+
} finally {
|
|
131
|
+
memory.close();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
test("signature validation uses raw bytes and repository allowlists", () => {
|
|
135
|
+
const raw = Buffer.from('{ "a": 1 }'),
|
|
136
|
+
secret = "secret",
|
|
137
|
+
signature =
|
|
138
|
+
"sha256=" + createHmac("sha256", secret).update(raw).digest("hex");
|
|
139
|
+
assert(verifySignature(raw, secret, signature));
|
|
140
|
+
assert(!verifySignature(Buffer.from('{"a":1}'), secret, signature));
|
|
141
|
+
assert(!verifySignature(raw, "", signature));
|
|
142
|
+
assert(repositoryAllowed("Owner/Repo", "owner/*"));
|
|
143
|
+
assert(!repositoryAllowed("owner-evil/repo", "owner/*"));
|
|
144
|
+
assert(!repositoryAllowed("../repo", "owner/*"));
|
|
145
|
+
});
|
|
146
|
+
test("cross-site and unavailable browser authentication are denied", () => {
|
|
147
|
+
let status;
|
|
148
|
+
const res = {
|
|
149
|
+
writeHead(s) {
|
|
150
|
+
status = s;
|
|
151
|
+
},
|
|
152
|
+
end() {},
|
|
153
|
+
};
|
|
154
|
+
assert.equal(
|
|
155
|
+
browserAuthorized(
|
|
156
|
+
{ dshLoader: { services: { get: () => undefined } } },
|
|
157
|
+
{ headers: {} },
|
|
158
|
+
res,
|
|
159
|
+
),
|
|
160
|
+
false,
|
|
161
|
+
);
|
|
162
|
+
assert.equal(status, 503);
|
|
163
|
+
assert.equal(
|
|
164
|
+
browserAuthorized({}, { headers: { "sec-fetch-site": "cross-site" } }, res),
|
|
165
|
+
false,
|
|
166
|
+
);
|
|
167
|
+
assert.equal(status, 403);
|
|
168
|
+
});
|
|
169
|
+
test("agent injects SOUL, retrieved skill and memory; tools execute only in chat", async () => {
|
|
170
|
+
const p = await fixture();
|
|
171
|
+
await writeFile(path.join(p.profile, "SOUL.md"), "shared soul");
|
|
172
|
+
const c = { ...config(), learnSkills: false, learnMemory: false };
|
|
173
|
+
let calls = 0;
|
|
174
|
+
const a = new Agent({
|
|
175
|
+
paths: p,
|
|
176
|
+
config: () => c,
|
|
177
|
+
memory: { search: async () => [{ text: "durable", source: "test" }] },
|
|
178
|
+
skills: { retrieve: async () => [{ content: "skill procedure" }] },
|
|
179
|
+
mcp: {
|
|
180
|
+
available: async () => [
|
|
181
|
+
{
|
|
182
|
+
name: "mcp_0",
|
|
183
|
+
server: "test",
|
|
184
|
+
rawName: "read",
|
|
185
|
+
parameters: { type: "object" },
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
call: async () => {
|
|
189
|
+
calls++;
|
|
190
|
+
return { content: "ok" };
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
let rounds = 0;
|
|
195
|
+
a.completion = async (messages) => {
|
|
196
|
+
assert.match(messages[0].content, /shared soul|Review changes/);
|
|
197
|
+
assert.match(messages[0].content, /durable/);
|
|
198
|
+
return rounds++ === 0
|
|
199
|
+
? {
|
|
200
|
+
role: "assistant",
|
|
201
|
+
tool_calls: [
|
|
202
|
+
{ id: "a", function: { name: "mcp_0", arguments: "{}" } },
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
: { role: "assistant", content: "done" };
|
|
206
|
+
};
|
|
207
|
+
assert.equal(await a.run("question"), "done");
|
|
208
|
+
assert.equal(calls, 1);
|
|
209
|
+
rounds = 1;
|
|
210
|
+
assert.equal(await a.run("question", "review"), "done");
|
|
211
|
+
assert.equal(calls, 1);
|
|
212
|
+
});
|
|
213
|
+
test("Gateway authenticates, serves replayable completion events, and stops runs", async () => {
|
|
214
|
+
const g = new Gateway({ run: async () => "hello" }, config, () => {});
|
|
215
|
+
await g.start();
|
|
216
|
+
try {
|
|
217
|
+
assert.equal((await fetch(g.url + "/v1/runs")).status, 401);
|
|
218
|
+
const headers = {
|
|
219
|
+
Authorization: `Bearer ${g.token}`,
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
};
|
|
222
|
+
const response = await fetch(g.url + "/v1/runs", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers,
|
|
225
|
+
body: JSON.stringify({ input: "hi" }),
|
|
226
|
+
});
|
|
227
|
+
const { run_id } = await response.json();
|
|
228
|
+
const events = await (
|
|
229
|
+
await fetch(g.url + `/v1/runs/${run_id}/events`, { headers })
|
|
230
|
+
).text();
|
|
231
|
+
assert.match(events, /run.completed/);
|
|
232
|
+
assert.match(events, /hello/);
|
|
233
|
+
} finally {
|
|
234
|
+
await g.close();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
test("durable GitHub queue creates head-bound review, deduplicates publication and skips stale heads", async () => {
|
|
238
|
+
const memory = new Memory(":memory:", inference, config);
|
|
239
|
+
const c = {
|
|
240
|
+
...config(),
|
|
241
|
+
githubEnabled: true,
|
|
242
|
+
publishReviews: true,
|
|
243
|
+
repositories: "owner/repo",
|
|
244
|
+
};
|
|
245
|
+
const r = new Reviews(
|
|
246
|
+
memory.db,
|
|
247
|
+
{ run: async () => "Finding" },
|
|
248
|
+
() => c,
|
|
249
|
+
{ notify() {} },
|
|
250
|
+
() => {},
|
|
251
|
+
);
|
|
252
|
+
const posted = [];
|
|
253
|
+
r.api = async (route, options) => {
|
|
254
|
+
if (options) {
|
|
255
|
+
posted.push(JSON.parse(options.body));
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
if (route.includes("/reviews?")) return [];
|
|
259
|
+
if (route.includes("/files?"))
|
|
260
|
+
return [{ filename: "a.js", patch: "+ bad()" }];
|
|
261
|
+
return { title: "PR", body: "body", head: { sha: "abc" } };
|
|
262
|
+
};
|
|
263
|
+
const job = {
|
|
264
|
+
id: "delivery",
|
|
265
|
+
event: "pull_request",
|
|
266
|
+
payload: JSON.stringify({
|
|
267
|
+
number: 1,
|
|
268
|
+
repository: { full_name: "owner/repo" },
|
|
269
|
+
pull_request: { head: { sha: "abc" } },
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
assert.equal((await r.review(job)).published, true);
|
|
273
|
+
assert.equal(posted[0].commit_id, "abc");
|
|
274
|
+
assert.match(posted[0].body, /dsh-synapse:delivery/);
|
|
275
|
+
job.payload = job.payload.replace("abc", "old");
|
|
276
|
+
assert.equal((await r.review(job)).skipped, "superseded-head");
|
|
277
|
+
await r.close();
|
|
278
|
+
memory.close();
|
|
279
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
const server = new McpServer({ name: "synapse-test", version: "1.0.0" });
|
|
4
|
+
server.registerTool(
|
|
5
|
+
"read",
|
|
6
|
+
{ description: "Test read", inputSchema: {} },
|
|
7
|
+
async () => ({ content: [{ type: "text", text: "fixture result" }] }),
|
|
8
|
+
);
|
|
9
|
+
server.registerTool(
|
|
10
|
+
"blocked",
|
|
11
|
+
{ description: "Must not run", inputSchema: {} },
|
|
12
|
+
async () => ({ content: [{ type: "text", text: "blocked" }] }),
|
|
13
|
+
);
|
|
14
|
+
await server.connect(new StdioServerTransport());
|