neko-harness 0.1.1 → 0.2.1
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/dist/client/assets/index-CxnbrvVk.css +2 -0
- package/dist/client/assets/index-D6NpJLXy.js +88 -0
- package/dist/client/index.html +2 -2
- package/dist/gateway.js +210 -130
- package/dist/neko.js +269 -77
- package/package.json +1 -1
- package/dist/client/assets/index-B1I59uXv.js +0 -88
- package/dist/client/assets/index-U3kl-2BJ.css +0 -2
package/dist/client/index.html
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='26' fill='%23ffb7c5'/%3E%3Ctext x='50' y='70' font-size='58' text-anchor='middle' fill='%23ffffff' font-family='sans-serif' font-weight='bold'%3En%3C/text%3E%3C/svg%3E"
|
|
10
10
|
/>
|
|
11
11
|
<title>neko harness</title>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-D6NpJLXy.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CxnbrvVk.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
package/dist/gateway.js
CHANGED
|
@@ -1,10 +1,113 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
+
// packages/gateway/src/cli-args.ts
|
|
5
|
+
var GATEWAY_USAGE = `\u7528\u6CD5: main.ts [\u9009\u9879]
|
|
6
|
+
|
|
7
|
+
\u9009\u9879:
|
|
8
|
+
--host <ip> WebSocket \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF0C\u4EC5\u672C\u673A\uFF09
|
|
9
|
+
--port <n> WebSocket \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 8321\uFF0CWindows \u9ED8\u8BA4\u968F\u673A\uFF1B0/null \u7981\u7528 WS\uFF09
|
|
10
|
+
--socket-path <path> Unix socket \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <\u6570\u636E\u76EE\u5F55>/<serverId>.sock\uFF1BWindows \u9ED8\u8BA4\u7981\u7528\uFF09
|
|
11
|
+
--no-socket \u7981\u7528 Unix socket \u4F20\u8F93\uFF08\u4EC5 WebSocket\uFF09
|
|
12
|
+
--server-id <id> \u903B\u8F91\u670D\u52A1\u5668\u8EAB\u4EFD\uFF08\u9ED8\u8BA4\u8BFB\u53D6/\u751F\u6210 <\u6570\u636E\u76EE\u5F55>/gateway-server-id\uFF09
|
|
13
|
+
--cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
|
|
14
|
+
--model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
|
|
15
|
+
--storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
|
|
16
|
+
--credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
|
|
17
|
+
--config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
|
|
18
|
+
-h, --help \u663E\u793A\u6B64\u5E2E\u52A9
|
|
19
|
+
|
|
20
|
+
\u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
|
|
21
|
+
function parseGatewayArgs(argv) {
|
|
22
|
+
let port;
|
|
23
|
+
const options = {};
|
|
24
|
+
for (let i = 0;i < argv.length; i++) {
|
|
25
|
+
const arg = argv[i];
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
switch (arg) {
|
|
28
|
+
case "--host":
|
|
29
|
+
if (next === undefined)
|
|
30
|
+
throw new Error("--host \u9700\u8981\u53C2\u6570");
|
|
31
|
+
options.host = next;
|
|
32
|
+
i++;
|
|
33
|
+
break;
|
|
34
|
+
case "--port": {
|
|
35
|
+
if (next === undefined)
|
|
36
|
+
throw new Error("--port \u9700\u8981\u53C2\u6570");
|
|
37
|
+
if (next === "0" || next === "null") {
|
|
38
|
+
port = null;
|
|
39
|
+
} else {
|
|
40
|
+
const parsed = Number(next);
|
|
41
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
42
|
+
throw new Error(`--port \u9700\u8981 0-65535 \u7684\u6574\u6570: ${next}`);
|
|
43
|
+
}
|
|
44
|
+
port = parsed;
|
|
45
|
+
}
|
|
46
|
+
i++;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case "--socket-path":
|
|
50
|
+
if (next === undefined)
|
|
51
|
+
throw new Error("--socket-path \u9700\u8981\u53C2\u6570");
|
|
52
|
+
options.socketPath = next;
|
|
53
|
+
i++;
|
|
54
|
+
break;
|
|
55
|
+
case "--no-socket":
|
|
56
|
+
options.socketPath = null;
|
|
57
|
+
break;
|
|
58
|
+
case "--server-id":
|
|
59
|
+
if (next === undefined)
|
|
60
|
+
throw new Error("--server-id \u9700\u8981\u53C2\u6570");
|
|
61
|
+
options.serverId = next;
|
|
62
|
+
i++;
|
|
63
|
+
break;
|
|
64
|
+
case "--cwd":
|
|
65
|
+
if (next === undefined)
|
|
66
|
+
throw new Error("--cwd \u9700\u8981\u53C2\u6570");
|
|
67
|
+
options.cwd = next;
|
|
68
|
+
i++;
|
|
69
|
+
break;
|
|
70
|
+
case "--model": {
|
|
71
|
+
if (next === undefined)
|
|
72
|
+
throw new Error("--model \u9700\u8981\u53C2\u6570");
|
|
73
|
+
const slash = next.indexOf("/");
|
|
74
|
+
if (slash <= 0 || slash === next.length - 1)
|
|
75
|
+
throw new Error(`--model \u683C\u5F0F\u4E3A provider/modelId: ${next}`);
|
|
76
|
+
options.model = { provider: next.slice(0, slash), modelId: next.slice(slash + 1) };
|
|
77
|
+
i++;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "--credentials":
|
|
81
|
+
if (next === undefined)
|
|
82
|
+
throw new Error("--credentials \u9700\u8981\u53C2\u6570");
|
|
83
|
+
options.credentialsPath = next;
|
|
84
|
+
i++;
|
|
85
|
+
break;
|
|
86
|
+
case "--storage":
|
|
87
|
+
if (next === undefined)
|
|
88
|
+
throw new Error("--storage \u9700\u8981\u53C2\u6570");
|
|
89
|
+
options.storagePath = next;
|
|
90
|
+
i++;
|
|
91
|
+
break;
|
|
92
|
+
case "--config":
|
|
93
|
+
if (next === undefined)
|
|
94
|
+
throw new Error("--config \u9700\u8981\u53C2\u6570");
|
|
95
|
+
options.configDir = next;
|
|
96
|
+
i++;
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (port !== undefined)
|
|
103
|
+
options.port = port;
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
|
|
4
107
|
// packages/gateway/src/gateway.ts
|
|
5
108
|
import { randomUUID } from "crypto";
|
|
6
|
-
import { mkdir as mkdir2, readFile as
|
|
7
|
-
import { basename as basename4, dirname as dirname6, join as
|
|
109
|
+
import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
110
|
+
import { basename as basename4, dirname as dirname6, join as join9 } from "path";
|
|
8
111
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
9
112
|
import { isServerId } from "@earendil-works/pi-protocol";
|
|
10
113
|
import { Server, SessionNotFoundError } from "@earendil-works/pi-server";
|
|
@@ -217,8 +320,8 @@ function validate(config, source) {
|
|
|
217
320
|
if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) {
|
|
218
321
|
throw new Error(`neko config: ${source}: "gateway.port" must be an integer between 0 and 65535.`);
|
|
219
322
|
}
|
|
220
|
-
if (socketPath !== undefined && typeof socketPath !== "string") {
|
|
221
|
-
throw new Error(`neko config: ${source}: "gateway.socketPath" must be a string.`);
|
|
323
|
+
if (socketPath !== undefined && socketPath !== null && typeof socketPath !== "string") {
|
|
324
|
+
throw new Error(`neko config: ${source}: "gateway.socketPath" must be a string or null.`);
|
|
222
325
|
}
|
|
223
326
|
}
|
|
224
327
|
const theme = config.theme;
|
|
@@ -1897,7 +2000,10 @@ async function createNekoRuntime(options = {}) {
|
|
|
1897
2000
|
conversationCreated: async (tx, record) => {
|
|
1898
2001
|
(await tx.doc(SessionMetadataDoc, record.id)).createdAt = Date.now();
|
|
1899
2002
|
},
|
|
1900
|
-
env: ({ cwd }) => new NodeExecutionEnv({
|
|
2003
|
+
env: ({ cwd }) => new NodeExecutionEnv({
|
|
2004
|
+
cwd: executionWorkspace(cwd),
|
|
2005
|
+
...process.platform === "win32" ? { shellEnv: { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" } } : {}
|
|
2006
|
+
})
|
|
1901
2007
|
}, context);
|
|
1902
2008
|
const scanConversations = () => harness.commit(async (tx) => {
|
|
1903
2009
|
const records = [];
|
|
@@ -2323,9 +2429,24 @@ async function createNekoRuntime(options = {}) {
|
|
|
2323
2429
|
}
|
|
2324
2430
|
};
|
|
2325
2431
|
}
|
|
2432
|
+
// packages/gateway/src/port-file.ts
|
|
2433
|
+
import { readFile as readFile4, rm, writeFile } from "fs/promises";
|
|
2434
|
+
import { join as join6 } from "path";
|
|
2435
|
+
function gatewayPortFilePath(nekoDir, serverId) {
|
|
2436
|
+
return join6(nekoDir, `${serverId}.port`);
|
|
2437
|
+
}
|
|
2438
|
+
async function writeGatewayPortFile(nekoDir, serverId, wsUrl) {
|
|
2439
|
+
await writeFile(gatewayPortFilePath(nekoDir, serverId), `${wsUrl}
|
|
2440
|
+
`, { mode: 384 });
|
|
2441
|
+
}
|
|
2442
|
+
async function removeGatewayPortFile(nekoDir, serverId) {
|
|
2443
|
+
await rm(gatewayPortFilePath(nekoDir, serverId), { force: true });
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2326
2446
|
// packages/gateway/src/server-services.ts
|
|
2327
2447
|
import {
|
|
2328
2448
|
createRemoteServiceEndpoint,
|
|
2449
|
+
RemoteServiceError,
|
|
2329
2450
|
RemoteServiceProvider,
|
|
2330
2451
|
replicatedState
|
|
2331
2452
|
} from "@earendil-works/chord";
|
|
@@ -2334,13 +2455,20 @@ import {
|
|
|
2334
2455
|
import { spawn } from "child_process";
|
|
2335
2456
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
2336
2457
|
import { realpath, stat } from "fs/promises";
|
|
2337
|
-
import { basename as basename2, delimiter, isAbsolute as isAbsolute2, join as
|
|
2458
|
+
import { basename as basename2, delimiter, isAbsolute as isAbsolute2, join as join7, relative, resolve as resolve4, sep } from "path";
|
|
2459
|
+
|
|
2460
|
+
class MissingFileSearchDependencyError extends Error {
|
|
2461
|
+
constructor() {
|
|
2462
|
+
super("Install fd (or fdfind) to search workspace files.");
|
|
2463
|
+
this.name = "MissingFileSearchDependencyError";
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2338
2466
|
function findFd() {
|
|
2339
2467
|
for (const name of process.platform === "win32" ? ["fd.exe", "fdfind.exe"] : ["fd", "fdfind"]) {
|
|
2340
2468
|
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
2341
2469
|
if (!directory)
|
|
2342
2470
|
continue;
|
|
2343
|
-
const path =
|
|
2471
|
+
const path = join7(directory, name);
|
|
2344
2472
|
try {
|
|
2345
2473
|
accessSync2(path, constants2.X_OK);
|
|
2346
2474
|
return path;
|
|
@@ -2364,7 +2492,7 @@ async function workspaceTarget(root, path) {
|
|
|
2364
2492
|
}
|
|
2365
2493
|
async function searchWorkspaceFiles(root, query, signal, fd = findFd()) {
|
|
2366
2494
|
if (!fd)
|
|
2367
|
-
throw new
|
|
2495
|
+
throw new MissingFileSearchDependencyError;
|
|
2368
2496
|
query = relativeQuery(query);
|
|
2369
2497
|
const slash = query.lastIndexOf("/");
|
|
2370
2498
|
const prefix = slash < 0 ? "" : query.slice(0, slash + 1);
|
|
@@ -2388,6 +2516,8 @@ async function searchWorkspaceFiles(root, query, signal, fd = findFd()) {
|
|
|
2388
2516
|
"--max-results",
|
|
2389
2517
|
"101",
|
|
2390
2518
|
"--print0",
|
|
2519
|
+
"--path-separator",
|
|
2520
|
+
"/",
|
|
2391
2521
|
"--color",
|
|
2392
2522
|
"never"
|
|
2393
2523
|
];
|
|
@@ -2456,14 +2586,38 @@ var NekoWorkspaces = defineService("neko.workspaces");
|
|
|
2456
2586
|
var NekoCredentials = defineService("neko.credentials");
|
|
2457
2587
|
var NekoWorkspaceFiles = defineService("neko.workspace-files");
|
|
2458
2588
|
|
|
2589
|
+
// packages/gateway/src/file-search-help.ts
|
|
2590
|
+
function fileSearchInstallHelp(platform = process.platform) {
|
|
2591
|
+
const docs = "https://github.com/sharkdp/fd#installation";
|
|
2592
|
+
let install;
|
|
2593
|
+
switch (platform) {
|
|
2594
|
+
case "win32":
|
|
2595
|
+
install = "\u8BF7\u6267\u884C\uFF1Awinget install sharkdp.fd\u3002";
|
|
2596
|
+
break;
|
|
2597
|
+
case "darwin":
|
|
2598
|
+
install = "\u8BF7\u6267\u884C\uFF1Abrew install fd\u3002";
|
|
2599
|
+
break;
|
|
2600
|
+
case "linux":
|
|
2601
|
+
install = `Debian/Ubuntu \u8BF7\u6267\u884C\uFF1Asudo apt install fd-find\u3002\u5176\u4ED6\u53D1\u884C\u7248\u8BF7\u53C2\u8003\uFF1A${docs} \u3002`;
|
|
2602
|
+
break;
|
|
2603
|
+
default:
|
|
2604
|
+
install = `\u5B89\u88C5\u65B9\u6CD5\u8BF7\u53C2\u8003\uFF1A${docs} \u3002`;
|
|
2605
|
+
}
|
|
2606
|
+
return `\u672A\u627E\u5230 fd \u6216 fdfind\uFF0C\u6682\u65F6\u65E0\u6CD5\u4F7F\u7528 @ \u6587\u4EF6\u8865\u5168\u3002\u8BF7\u5728\u8FD0\u884C neko \u7684\u7535\u8111\u4E0A\u5B89\u88C5\u3002${install}\u5B89\u88C5\u540E\u91CD\u65B0\u6253\u5F00\u7EC8\u7AEF\u5E76\u91CD\u542F neko\u3002`;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2459
2609
|
// packages/gateway/src/workspaces.ts
|
|
2460
2610
|
import { constants as constants3 } from "fs";
|
|
2461
2611
|
import { access, readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
2462
|
-
import { dirname as dirname5, isAbsolute as isAbsolute3, join as
|
|
2612
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join8 } from "path";
|
|
2463
2613
|
async function browseWorkspaces(options) {
|
|
2464
|
-
|
|
2614
|
+
const requested = options.path.replace(/^\/+([A-Za-z]:[\\/])/, "$1");
|
|
2615
|
+
if (process.platform === "win32" && (requested === "/" || requested === "\\")) {
|
|
2616
|
+
return { path: "/", parent: null, entries: await listDrives(), next: null };
|
|
2617
|
+
}
|
|
2618
|
+
if (!isAbsolute3(requested))
|
|
2465
2619
|
throw new Error("\u8BF7\u9009\u62E9\u5B8C\u6574\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84");
|
|
2466
|
-
const path = accessibleWorkspace(
|
|
2620
|
+
const path = accessibleWorkspace(requested);
|
|
2467
2621
|
const query = options.query?.toLowerCase() ?? "";
|
|
2468
2622
|
const key = JSON.stringify([path, query, options.showHidden === true]);
|
|
2469
2623
|
let offset = 0;
|
|
@@ -2482,7 +2636,7 @@ async function browseWorkspaces(options) {
|
|
|
2482
2636
|
let next = offset;
|
|
2483
2637
|
for (;next < entries.length && directories.length < 100; next++) {
|
|
2484
2638
|
const entry = entries[next];
|
|
2485
|
-
const target =
|
|
2639
|
+
const target = join8(path, entry.name);
|
|
2486
2640
|
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
2487
2641
|
continue;
|
|
2488
2642
|
try {
|
|
@@ -2501,6 +2655,17 @@ async function browseWorkspaces(options) {
|
|
|
2501
2655
|
next: next < entries.length ? JSON.stringify({ key, offset: next }) : null
|
|
2502
2656
|
};
|
|
2503
2657
|
}
|
|
2658
|
+
async function listDrives() {
|
|
2659
|
+
const drives = [];
|
|
2660
|
+
for (const letter of "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
|
2661
|
+
const root = `${letter}:\\`;
|
|
2662
|
+
try {
|
|
2663
|
+
await access(root);
|
|
2664
|
+
drives.push({ name: `${letter}:`, path: root });
|
|
2665
|
+
} catch {}
|
|
2666
|
+
}
|
|
2667
|
+
return drives;
|
|
2668
|
+
}
|
|
2504
2669
|
|
|
2505
2670
|
// packages/gateway/src/server-services.ts
|
|
2506
2671
|
async function createNekoServerServices(options) {
|
|
@@ -2598,6 +2763,10 @@ async function createNekoServerServices(options) {
|
|
|
2598
2763
|
if (!roots.some((w) => w.path === input.workspaceRoot))
|
|
2599
2764
|
throw new Error("Unknown workspace.");
|
|
2600
2765
|
return await searchWorkspaceFiles(input.workspaceRoot, input.query, AbortSignal.any([controller.signal, ...ctx.abortSignal ? [ctx.abortSignal] : []]));
|
|
2766
|
+
} catch (error) {
|
|
2767
|
+
if (error instanceof MissingFileSearchDependencyError)
|
|
2768
|
+
throw new RemoteServiceError("service_not_allowed", fileSearchInstallHelp());
|
|
2769
|
+
throw error;
|
|
2601
2770
|
} finally {
|
|
2602
2771
|
fileQueries--;
|
|
2603
2772
|
if (fileQuery === controller)
|
|
@@ -2688,7 +2857,7 @@ import { open as open2 } from "fs/promises";
|
|
|
2688
2857
|
import { basename as basename3, extname } from "path";
|
|
2689
2858
|
import {
|
|
2690
2859
|
createRemoteServiceEndpoint as createRemoteServiceEndpoint2,
|
|
2691
|
-
RemoteServiceError,
|
|
2860
|
+
RemoteServiceError as RemoteServiceError2,
|
|
2692
2861
|
RemoteServiceProvider as RemoteServiceProvider2,
|
|
2693
2862
|
replicatedState as replicatedState2
|
|
2694
2863
|
} from "@earendil-works/chord";
|
|
@@ -2856,7 +3025,7 @@ async function openNekoSessionHandle(options) {
|
|
|
2856
3025
|
try {
|
|
2857
3026
|
return action();
|
|
2858
3027
|
} catch (error) {
|
|
2859
|
-
throw new
|
|
3028
|
+
throw new RemoteServiceError2("service_invalid_value", error instanceof Error ? error.message : "Image upload failed");
|
|
2860
3029
|
}
|
|
2861
3030
|
};
|
|
2862
3031
|
const createProvider = () => {
|
|
@@ -2913,7 +3082,7 @@ async function openNekoSessionHandle(options) {
|
|
|
2913
3082
|
try {
|
|
2914
3083
|
await session.compact(instructions);
|
|
2915
3084
|
} catch (error) {
|
|
2916
|
-
throw new
|
|
3085
|
+
throw new RemoteServiceError2("service_invalid_value", error instanceof Error ? error.message : "Cannot compact");
|
|
2917
3086
|
}
|
|
2918
3087
|
},
|
|
2919
3088
|
beginImage: async (input) => safeUpload(() => uploads.begin(input)),
|
|
@@ -2922,15 +3091,15 @@ async function openNekoSessionHandle(options) {
|
|
|
2922
3091
|
releaseImage: async (id) => uploads.release(id),
|
|
2923
3092
|
getImage: async (input) => {
|
|
2924
3093
|
if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
|
|
2925
|
-
throw new
|
|
3094
|
+
throw new RemoteServiceError2("service_invalid_value", "Invalid image range");
|
|
2926
3095
|
const entry = history.get(input.entryId);
|
|
2927
3096
|
const content = entry?.model?.[0]?.content;
|
|
2928
3097
|
const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
|
|
2929
3098
|
if (image?.type !== "image")
|
|
2930
|
-
throw new
|
|
3099
|
+
throw new RemoteServiceError2("service_invalid_value", "Image is not part of this conversation");
|
|
2931
3100
|
const bytes = Buffer.from(image.data, "base64");
|
|
2932
3101
|
if (input.offset > bytes.length)
|
|
2933
|
-
throw new
|
|
3102
|
+
throw new RemoteServiceError2("service_invalid_value", "Invalid image range");
|
|
2934
3103
|
return {
|
|
2935
3104
|
data: bytes.subarray(input.offset, input.offset + IMAGE_CHUNK_BYTES).toString("base64"),
|
|
2936
3105
|
total: bytes.length,
|
|
@@ -2950,8 +3119,8 @@ async function openNekoSessionHandle(options) {
|
|
|
2950
3119
|
uploads.release(id);
|
|
2951
3120
|
} catch (error) {
|
|
2952
3121
|
if (error instanceof Error && error.message.startsWith("DEEPSEEK_KEY_REQUIRED:"))
|
|
2953
|
-
throw new
|
|
2954
|
-
throw new
|
|
3122
|
+
throw new RemoteServiceError2("service_invalid_value", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
|
|
3123
|
+
throw new RemoteServiceError2("service_invalid_value", error instanceof Error ? error.message : "Cannot submit message");
|
|
2955
3124
|
}
|
|
2956
3125
|
await onActivity();
|
|
2957
3126
|
},
|
|
@@ -2970,7 +3139,7 @@ async function openNekoSessionHandle(options) {
|
|
|
2970
3139
|
configure: async (change) => {
|
|
2971
3140
|
const applied = await runtime.configureConversation(session.id, change);
|
|
2972
3141
|
if (!applied) {
|
|
2973
|
-
throw new
|
|
3142
|
+
throw new RemoteServiceError2("service_invalid_value", `Session ${String(session.id)} is unavailable or archived`);
|
|
2974
3143
|
}
|
|
2975
3144
|
}
|
|
2976
3145
|
});
|
|
@@ -3123,10 +3292,10 @@ async function startGateway(options = {}) {
|
|
|
3123
3292
|
const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
|
|
3124
3293
|
const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
|
|
3125
3294
|
const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
|
|
3126
|
-
const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? DEFAULT_PORT;
|
|
3295
|
+
const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
|
|
3127
3296
|
const nekoDir = dirname6(storagePath);
|
|
3128
3297
|
const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
|
|
3129
|
-
const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath
|
|
3298
|
+
const socketPath = options.socketPath !== undefined ? options.socketPath : fileConfig.gateway?.socketPath !== undefined ? fileConfig.gateway.socketPath : process.platform === "win32" ? null : join9(nekoDir, `${serverId}.sock`);
|
|
3130
3299
|
const runtime = await createNekoRuntime({
|
|
3131
3300
|
context,
|
|
3132
3301
|
storage: options.storage ?? "sqlite",
|
|
@@ -3147,6 +3316,7 @@ async function startGateway(options = {}) {
|
|
|
3147
3316
|
port,
|
|
3148
3317
|
socketPath,
|
|
3149
3318
|
serverId,
|
|
3319
|
+
nekoDir,
|
|
3150
3320
|
model,
|
|
3151
3321
|
onError: options.onError
|
|
3152
3322
|
});
|
|
@@ -3156,7 +3326,7 @@ async function startGateway(options = {}) {
|
|
|
3156
3326
|
}
|
|
3157
3327
|
}
|
|
3158
3328
|
async function bindGateway(runtime, options) {
|
|
3159
|
-
const { context, host, port, socketPath, serverId, model } = options;
|
|
3329
|
+
const { context, host, port, socketPath, serverId, nekoDir, model } = options;
|
|
3160
3330
|
const modelLabel = `${model.provider}/${model.modelId}`;
|
|
3161
3331
|
const serverServices = await createNekoServerServices({ runtime, context });
|
|
3162
3332
|
let wsListener;
|
|
@@ -3189,6 +3359,10 @@ async function bindGateway(runtime, options) {
|
|
|
3189
3359
|
await serverServices.dispose().catch(() => {});
|
|
3190
3360
|
throw toStartupError(error, { socketPath, port, host, serverId });
|
|
3191
3361
|
}
|
|
3362
|
+
const portFile = process.platform === "win32" && wsListener !== undefined ? { nekoDir, serverId } : undefined;
|
|
3363
|
+
if (portFile !== undefined && wsListener !== undefined) {
|
|
3364
|
+
await writeGatewayPortFile(portFile.nekoDir, portFile.serverId, `ws://${host}:${wsListener.port}`);
|
|
3365
|
+
}
|
|
3192
3366
|
let closed = false;
|
|
3193
3367
|
return {
|
|
3194
3368
|
serverId,
|
|
@@ -3200,6 +3374,8 @@ async function bindGateway(runtime, options) {
|
|
|
3200
3374
|
if (closed)
|
|
3201
3375
|
return;
|
|
3202
3376
|
closed = true;
|
|
3377
|
+
if (portFile !== undefined)
|
|
3378
|
+
await removeGatewayPortFile(portFile.nekoDir, portFile.serverId).catch(() => {});
|
|
3203
3379
|
await server.close();
|
|
3204
3380
|
await serverServices.dispose();
|
|
3205
3381
|
await runtime.close();
|
|
@@ -3209,7 +3385,8 @@ async function bindGateway(runtime, options) {
|
|
|
3209
3385
|
function toStartupError(error, info) {
|
|
3210
3386
|
const message = error instanceof Error ? error.message : String(error);
|
|
3211
3387
|
const existingServerId = info.socketPath === null ? undefined : serverIdFromSocketPath(info.socketPath);
|
|
3212
|
-
const
|
|
3388
|
+
const existingAddress = info.socketPath !== null ? `Unix socket ${info.socketPath}${existingServerId !== undefined ? `\uFF08serverId: ${existingServerId}\uFF09` : ""}` : process.platform === "win32" ? `\u7AEF\u53E3\u6587\u4EF6 <\u6570\u636E\u76EE\u5F55>/${info.serverId}.port \u4E2D\u7684 ws \u5730\u5740` : "Unix socket\uFF08\u672A\u542F\u7528\uFF09";
|
|
3389
|
+
const connectHint = `\u5982\u9700\u8FDE\u63A5\u5DF2\u6709\u5B9E\u4F8B\uFF1A${existingAddress}\uFF0C\u6216\u505C\u6389\u5360\u7528\u65B9\u540E\u91CD\u8BD5\u3002`;
|
|
3213
3390
|
if (message.includes("already running")) {
|
|
3214
3391
|
return new Error(`neko gateway \u542F\u52A8\u5931\u8D25\uFF1A${message}
|
|
3215
3392
|
\u8BE5\u5730\u5740\u5DF2\u88AB\u4E00\u4E2A\u6D3B\u8DC3\u7F51\u5173\u5360\u7528\uFF0C\u4E0D\u80FD\u542F\u52A8\u7B2C\u4E8C\u4E2A\u5B9E\u4F8B\u3002
|
|
@@ -3229,123 +3406,26 @@ function serverIdFromSocketPath(socketPath) {
|
|
|
3229
3406
|
return isServerId(candidate) ? candidate : undefined;
|
|
3230
3407
|
}
|
|
3231
3408
|
async function loadOrCreateServerId(nekoDir) {
|
|
3232
|
-
const file =
|
|
3409
|
+
const file = join9(nekoDir, SERVER_ID_FILENAME);
|
|
3233
3410
|
try {
|
|
3234
|
-
const existing = (await
|
|
3411
|
+
const existing = (await readFile5(file, "utf8")).trim();
|
|
3235
3412
|
if (existing !== "")
|
|
3236
3413
|
return existing;
|
|
3237
3414
|
} catch {}
|
|
3238
3415
|
const created = randomUUID();
|
|
3239
3416
|
await mkdir2(nekoDir, { recursive: true, mode: 448 });
|
|
3240
|
-
await
|
|
3417
|
+
await writeFile2(file, `${created}
|
|
3241
3418
|
`, { mode: 384 });
|
|
3242
3419
|
return created;
|
|
3243
3420
|
}
|
|
3244
3421
|
|
|
3245
3422
|
// packages/gateway/src/main.ts
|
|
3246
|
-
var
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
--port <n> WebSocket \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 8321\uFF1B0 = \u968F\u673A\u5206\u914D\uFF09
|
|
3251
|
-
--socket-path <path> Unix socket \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <\u6570\u636E\u76EE\u5F55>/<serverId>.sock\uFF09
|
|
3252
|
-
--server-id <id> \u903B\u8F91\u670D\u52A1\u5668\u8EAB\u4EFD\uFF08\u9ED8\u8BA4\u8BFB\u53D6/\u751F\u6210 <\u6570\u636E\u76EE\u5F55>/gateway-server-id\uFF09
|
|
3253
|
-
--cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
|
|
3254
|
-
--model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
|
|
3255
|
-
--storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
|
|
3256
|
-
--credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
|
|
3257
|
-
--config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
|
|
3258
|
-
-h, --help \u663E\u793A\u6B64\u5E2E\u52A9
|
|
3259
|
-
|
|
3260
|
-
\u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
|
|
3261
|
-
function parseArgs(argv) {
|
|
3262
|
-
let port;
|
|
3263
|
-
const options = {};
|
|
3264
|
-
for (let i = 0;i < argv.length; i++) {
|
|
3265
|
-
const arg = argv[i];
|
|
3266
|
-
const next = argv[i + 1];
|
|
3267
|
-
switch (arg) {
|
|
3268
|
-
case "--host":
|
|
3269
|
-
if (next === undefined)
|
|
3270
|
-
throw new Error("--host \u9700\u8981\u53C2\u6570");
|
|
3271
|
-
options.host = next;
|
|
3272
|
-
i++;
|
|
3273
|
-
break;
|
|
3274
|
-
case "--port": {
|
|
3275
|
-
if (next === undefined)
|
|
3276
|
-
throw new Error("--port \u9700\u8981\u53C2\u6570");
|
|
3277
|
-
if (next === "0" || next === "null") {
|
|
3278
|
-
port = null;
|
|
3279
|
-
} else {
|
|
3280
|
-
const parsed = Number(next);
|
|
3281
|
-
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
3282
|
-
throw new Error(`--port \u9700\u8981 0-65535 \u7684\u6574\u6570: ${next}`);
|
|
3283
|
-
}
|
|
3284
|
-
port = parsed;
|
|
3285
|
-
}
|
|
3286
|
-
i++;
|
|
3287
|
-
break;
|
|
3288
|
-
}
|
|
3289
|
-
case "--socket-path":
|
|
3290
|
-
if (next === undefined)
|
|
3291
|
-
throw new Error("--socket-path \u9700\u8981\u53C2\u6570");
|
|
3292
|
-
options.socketPath = next;
|
|
3293
|
-
i++;
|
|
3294
|
-
break;
|
|
3295
|
-
case "--server-id":
|
|
3296
|
-
if (next === undefined)
|
|
3297
|
-
throw new Error("--server-id \u9700\u8981\u53C2\u6570");
|
|
3298
|
-
options.serverId = next;
|
|
3299
|
-
i++;
|
|
3300
|
-
break;
|
|
3301
|
-
case "--cwd":
|
|
3302
|
-
if (next === undefined)
|
|
3303
|
-
throw new Error("--cwd \u9700\u8981\u53C2\u6570");
|
|
3304
|
-
options.cwd = next;
|
|
3305
|
-
i++;
|
|
3306
|
-
break;
|
|
3307
|
-
case "--model": {
|
|
3308
|
-
if (next === undefined)
|
|
3309
|
-
throw new Error("--model \u9700\u8981\u53C2\u6570");
|
|
3310
|
-
const slash = next.indexOf("/");
|
|
3311
|
-
if (slash <= 0 || slash === next.length - 1)
|
|
3312
|
-
throw new Error(`--model \u683C\u5F0F\u4E3A provider/modelId: ${next}`);
|
|
3313
|
-
options.model = { provider: next.slice(0, slash), modelId: next.slice(slash + 1) };
|
|
3314
|
-
i++;
|
|
3315
|
-
break;
|
|
3316
|
-
}
|
|
3317
|
-
case "--credentials":
|
|
3318
|
-
if (next === undefined)
|
|
3319
|
-
throw new Error("--credentials \u9700\u8981\u53C2\u6570");
|
|
3320
|
-
options.credentialsPath = next;
|
|
3321
|
-
i++;
|
|
3322
|
-
break;
|
|
3323
|
-
case "--storage":
|
|
3324
|
-
if (next === undefined)
|
|
3325
|
-
throw new Error("--storage \u9700\u8981\u53C2\u6570");
|
|
3326
|
-
options.storagePath = next;
|
|
3327
|
-
i++;
|
|
3328
|
-
break;
|
|
3329
|
-
case "--config":
|
|
3330
|
-
if (next === undefined)
|
|
3331
|
-
throw new Error("--config \u9700\u8981\u53C2\u6570");
|
|
3332
|
-
options.configDir = next;
|
|
3333
|
-
i++;
|
|
3334
|
-
break;
|
|
3335
|
-
case "-h":
|
|
3336
|
-
case "--help":
|
|
3337
|
-
console.log(USAGE);
|
|
3338
|
-
process.exit(0);
|
|
3339
|
-
break;
|
|
3340
|
-
default:
|
|
3341
|
-
throw new Error(`\u672A\u77E5\u9009\u9879: ${arg}`);
|
|
3342
|
-
}
|
|
3343
|
-
}
|
|
3344
|
-
if (port !== undefined)
|
|
3345
|
-
options.port = port;
|
|
3346
|
-
return options;
|
|
3423
|
+
var argv = process.argv.slice(2);
|
|
3424
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
3425
|
+
console.log(GATEWAY_USAGE);
|
|
3426
|
+
process.exit(0);
|
|
3347
3427
|
}
|
|
3348
|
-
var options =
|
|
3428
|
+
var options = parseGatewayArgs(argv);
|
|
3349
3429
|
var gateway = await startGateway({
|
|
3350
3430
|
...options,
|
|
3351
3431
|
onError: (error) => console.error("gateway error:", error.message)
|