nolo-cli 0.1.38 → 0.1.39
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/agent-runtime/agentRecordConfig.ts +0 -14
- package/agent-runtime/hostAdapter.ts +0 -1
- package/agent-runtime/index.ts +24 -18
- package/agent-runtime/localDialogRead.ts +23 -0
- package/agent-runtime/localToolPolicy.ts +2 -7
- package/agent-runtime/localWorkspaceTools.ts +9 -0
- package/agent-runtime/noloWorkspaceTools.ts +371 -0
- package/agent-runtime/runtimeToolPolicy.ts +1 -3
- package/agent-runtime/types.ts +1 -1
- package/agentAliases.ts +6 -0
- package/agentRunCommand.ts +57 -65
- package/agentRuntimeLocal.ts +0 -7
- package/chat/messages/fetchMessages.ts +84 -0
- package/chat/messages/types.ts +185 -0
- package/cli/agentAliases.ts +6 -0
- package/cli/agentRunCommand.ts +57 -65
- package/cli/agentRuntimeLocal.ts +0 -7
- package/cli/cliEnvHelpers.ts +11 -7
- package/cli/client/agentRun.ts +55 -7
- package/cli/client/localRuntimeAdapter.ts +87 -59
- package/cli/connectorRunArtifact.ts +0 -1
- package/cli/dialogCommands.ts +572 -0
- package/cli/dialogInternalCommandEntries.ts +4 -0
- package/cli/machineWsRunDispatch.ts +29 -37
- package/cli/scriptCommandEntries.ts +0 -2
- package/cli/tui/readlineWorkspace.ts +69 -0
- package/cli/tui/session.ts +176 -1
- package/cliEnvHelpers.ts +11 -7
- package/client/agentConfigResolver.test.ts +2 -3
- package/client/agentRun.test.ts +185 -1
- package/client/agentRun.ts +55 -7
- package/client/localRuntimeAdapter.test.ts +90 -25
- package/client/localRuntimeAdapter.ts +87 -59
- package/connectorRunArtifact.ts +0 -1
- package/database/server/db.ts +99 -0
- package/database/server/ensureDbOpen.ts +12 -0
- package/database/server/memoryAuthorityStore.ts +133 -0
- package/database/server/serverStoreFactory.ts +118 -0
- package/dialogCommands.ts +572 -0
- package/dialogInternalCommandEntries.ts +4 -0
- package/machineWsRunDispatch.ts +29 -37
- package/package.json +10 -9
- package/scriptCommandEntries.ts +0 -2
- package/tui/readlineWorkspace.ts +69 -0
- package/tui/session.ts +176 -1
- package/agent-runtime/taskWorkspace.ts +0 -193
- package/agent-runtime/workspaceSession.ts +0 -76
- package/cli/client/taskWorktree.ts +0 -8
- package/cli/client/workspaceSession.ts +0 -11
- package/client/taskWorktree.ts +0 -8
- package/client/workspaceSession.test.ts +0 -57
- package/client/workspaceSession.ts +0 -11
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
} from "../agentRuntimeLocal";
|
|
6
6
|
import { request as httpRequest } from "node:http";
|
|
7
7
|
import { request as httpsRequest } from "node:https";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
import {
|
|
9
11
|
buildLocalWorkspacePolicyToolNames,
|
|
10
12
|
buildLocalWorkspaceToolset,
|
|
@@ -40,19 +42,28 @@ import {
|
|
|
40
42
|
} from "./hybridRecordStore";
|
|
41
43
|
import { executeLocalToolWithPolicy } from "./localToolPolicy";
|
|
42
44
|
import { taskRunFunctionSchema } from "../ai/tools/agent/taskRunTool";
|
|
43
|
-
import { prepareTaskWorktree } from "./taskWorktree";
|
|
44
45
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
NOLO_DEFAULT_AGENT_ID,
|
|
47
|
+
NOLO_DEFAULT_AGENT_KEY,
|
|
48
|
+
} from "../agentAliases";
|
|
49
|
+
import {
|
|
50
|
+
buildNoloWorkspaceCliToolExecutors,
|
|
51
|
+
buildNoloWorkspaceOpenAiTools,
|
|
52
|
+
filterNoloWorkspaceToolNames,
|
|
53
|
+
NOLO_WORKSPACE_TOOL_NAMES,
|
|
54
|
+
NOLO_WORKSPACE_TOOL_PROMPT,
|
|
55
|
+
parseNoloWorkspaceToolArguments,
|
|
56
|
+
} from "../agent-runtime/noloWorkspaceTools";
|
|
50
57
|
|
|
51
58
|
type EnvLike = Record<string, string | undefined>;
|
|
52
59
|
type FetchInput = Parameters<typeof fetch>[0];
|
|
53
60
|
type FetchInit = Parameters<typeof fetch>[1];
|
|
54
61
|
const TRANSIENT_FETCH_MAX_ATTEMPTS = 8;
|
|
55
62
|
const TRANSIENT_FETCH_RETRY_BASE_DELAY_MS = 250;
|
|
63
|
+
export const BUILTIN_NOLO_AGENT_ID = NOLO_DEFAULT_AGENT_ID;
|
|
64
|
+
export const BUILTIN_NOLO_AGENT_KEY = NOLO_DEFAULT_AGENT_KEY;
|
|
65
|
+
const CLI_DIR = dirname(fileURLToPath(import.meta.url));
|
|
66
|
+
const CLI_ENTRYPOINT = join(CLI_DIR, "..", "index.ts");
|
|
56
67
|
|
|
57
68
|
export type CliLocalRuntimeDb = CliKvDb;
|
|
58
69
|
|
|
@@ -64,9 +75,7 @@ export type CliLocalRuntimeAdapterDeps = {
|
|
|
64
75
|
createId?: () => string;
|
|
65
76
|
fetchImpl?: typeof fetch;
|
|
66
77
|
cwd?: string;
|
|
67
|
-
useCwdAsTaskWorkspaceBase?: boolean;
|
|
68
78
|
output?: { write(chunk: string): unknown };
|
|
69
|
-
prepareTaskWorktree?: typeof prepareTaskWorktree;
|
|
70
79
|
localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
|
|
71
80
|
sleep?: (ms: number) => Promise<void>;
|
|
72
81
|
loopbackRequest?: (input: FetchInput, init?: FetchInit) => Promise<Response>;
|
|
@@ -85,7 +94,7 @@ function resolveLocalUserId(env: EnvLike) {
|
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
function shellModeEnabled(env: EnvLike) {
|
|
88
|
-
return
|
|
97
|
+
return env.NOLO_LOCAL_SHELL_MODE === "worktree";
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
function parseLocalToolBudgets(env: EnvLike) {
|
|
@@ -246,6 +255,17 @@ function logLocalRuntimeDiagnostic(event: string, fields: Record<string, unknown
|
|
|
246
255
|
console.error(`[nolo-local-runtime] ${event} ${JSON.stringify(fields)}`);
|
|
247
256
|
}
|
|
248
257
|
|
|
258
|
+
function summarizeOpenAiToolNames(tools: Array<Record<string, unknown>>) {
|
|
259
|
+
return tools
|
|
260
|
+
.map((tool) => {
|
|
261
|
+
const fn = tool.function;
|
|
262
|
+
return fn && typeof fn === "object" && "name" in fn && typeof fn.name === "string"
|
|
263
|
+
? fn.name
|
|
264
|
+
: null;
|
|
265
|
+
})
|
|
266
|
+
.filter((name): name is string => Boolean(name));
|
|
267
|
+
}
|
|
268
|
+
|
|
249
269
|
function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
|
|
250
270
|
const toolset = buildLocalWorkspaceToolsetForEnv(args);
|
|
251
271
|
return [
|
|
@@ -254,6 +274,7 @@ function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
|
|
|
254
274
|
exposeShellTools: toolset.exposeShellTools,
|
|
255
275
|
}),
|
|
256
276
|
...buildServerPlatformOpenAiTools({ toolNames: args.toolNames }),
|
|
277
|
+
...buildNoloWorkspaceOpenAiTools({ toolNames: args.toolNames }),
|
|
257
278
|
];
|
|
258
279
|
}
|
|
259
280
|
|
|
@@ -272,6 +293,7 @@ function buildLocalPolicyToolNames(args: { toolNames?: string[]; env: EnvLike })
|
|
|
272
293
|
exposeShellTools: shellModeEnabled(args.env),
|
|
273
294
|
}),
|
|
274
295
|
...((args.toolNames ?? []).includes("taskRun") ? ["taskRun"] : []),
|
|
296
|
+
...filterNoloWorkspaceToolNames(args.toolNames),
|
|
275
297
|
];
|
|
276
298
|
}
|
|
277
299
|
|
|
@@ -287,13 +309,44 @@ function buildServerPlatformOpenAiTools(args: { toolNames?: string[] }) {
|
|
|
287
309
|
];
|
|
288
310
|
}
|
|
289
311
|
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
312
|
+
function buildCliWorkspaceToolExecutors(args: {
|
|
313
|
+
env: EnvLike;
|
|
314
|
+
cliEntrypoint?: string;
|
|
315
|
+
}) {
|
|
316
|
+
return buildNoloWorkspaceCliToolExecutors({
|
|
317
|
+
cliEntrypoint: args.cliEntrypoint ?? CLI_ENTRYPOINT,
|
|
318
|
+
env: args.env,
|
|
319
|
+
metadataKind: "cliWorkspaceTool",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function isBuiltinNoloAgentRef(ref: unknown) {
|
|
324
|
+
if (typeof ref !== "string") return false;
|
|
325
|
+
const normalized = ref.trim();
|
|
326
|
+
return (
|
|
327
|
+
normalized === BUILTIN_NOLO_AGENT_KEY ||
|
|
328
|
+
normalized === BUILTIN_NOLO_AGENT_ID ||
|
|
329
|
+
normalized.endsWith(`-${BUILTIN_NOLO_AGENT_ID}`)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isBuiltinNoloAgentConfig(agentConfig: AgentRuntimeAgentConfig | null | undefined) {
|
|
334
|
+
const key = agentConfig?.key || agentConfig?.rawRecord?.dbKey || agentConfig?.rawRecord?.agentKey;
|
|
335
|
+
const id = agentConfig?.rawRecord?.id;
|
|
336
|
+
return isBuiltinNoloAgentRef(key) || isBuiltinNoloAgentRef(id);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function withBuiltinNoloCliTools(agentConfig: AgentRuntimeAgentConfig | null) {
|
|
340
|
+
if (!agentConfig || !isBuiltinNoloAgentConfig(agentConfig)) return agentConfig;
|
|
341
|
+
const toolNames = [...new Set([...(agentConfig.toolNames ?? []), ...NOLO_WORKSPACE_TOOL_NAMES])];
|
|
342
|
+
return {
|
|
343
|
+
...agentConfig,
|
|
344
|
+
toolNames,
|
|
345
|
+
prompt: [
|
|
346
|
+
agentConfig.prompt?.trim() ?? "",
|
|
347
|
+
NOLO_WORKSPACE_TOOL_PROMPT,
|
|
348
|
+
].filter(Boolean).join("\n\n"),
|
|
349
|
+
};
|
|
297
350
|
}
|
|
298
351
|
|
|
299
352
|
function resolveRuntimeServerUrl(env: EnvLike) {
|
|
@@ -320,7 +373,7 @@ function buildServerPlatformToolExecutors(args: {
|
|
|
320
373
|
"Content-Type": "application/json",
|
|
321
374
|
Authorization: `Bearer ${authToken}`,
|
|
322
375
|
},
|
|
323
|
-
body: JSON.stringify(
|
|
376
|
+
body: JSON.stringify(parseNoloWorkspaceToolArguments(call.arguments)),
|
|
324
377
|
});
|
|
325
378
|
const text = await response.text().catch(() => "");
|
|
326
379
|
if (!response.ok) {
|
|
@@ -352,17 +405,13 @@ function buildLocalToolExecutors(args: {
|
|
|
352
405
|
env: args.env,
|
|
353
406
|
fetchImpl: args.fetchImpl,
|
|
354
407
|
}),
|
|
408
|
+
...buildCliWorkspaceToolExecutors({
|
|
409
|
+
env: args.env,
|
|
410
|
+
}),
|
|
355
411
|
...(args.localToolExecutors ?? {}),
|
|
356
412
|
};
|
|
357
413
|
}
|
|
358
414
|
|
|
359
|
-
function readLocalWorkspaceMode(agentConfig: AgentRuntimeAgentConfig | null) {
|
|
360
|
-
if (agentConfig?.localWorkspaceMode === "task-worktree") return "task-worktree";
|
|
361
|
-
const runtimeBinding = agentConfig?.runtimeBinding;
|
|
362
|
-
const value = runtimeBinding?.localWorkspaceMode ?? runtimeBinding?.workspaceMode;
|
|
363
|
-
return value === "task-worktree" ? "task-worktree" : "current";
|
|
364
|
-
}
|
|
365
|
-
|
|
366
415
|
async function resolveStore(deps: CliLocalRuntimeAdapterDeps) {
|
|
367
416
|
if (deps.store) return deps.store;
|
|
368
417
|
return createCliHybridRecordStore({
|
|
@@ -437,51 +486,25 @@ export function createCliLocalRuntimeAdapter(
|
|
|
437
486
|
const localToolBudgets = parseLocalToolBudgets(deps.env);
|
|
438
487
|
const localToolUsage = new Map<string, number>();
|
|
439
488
|
let activeAgentToolNames: string[] = [];
|
|
440
|
-
|
|
441
|
-
deps.useCwdAsTaskWorkspaceBase
|
|
442
|
-
? { defaultCwd: deps.cwd }
|
|
443
|
-
: { cwd: deps.cwd }
|
|
444
|
-
);
|
|
489
|
+
const workspaceRoot = deps.cwd ?? process.cwd();
|
|
445
490
|
let runtimeToolExecutionLimits: ReturnType<typeof resolveLocalWorkspaceExecutorOptionsFromPolicy> = {};
|
|
446
491
|
let localToolExecutors = buildLocalToolExecutors({
|
|
447
|
-
workspaceRoot
|
|
492
|
+
workspaceRoot,
|
|
448
493
|
env: deps.env,
|
|
449
494
|
fetchImpl,
|
|
450
495
|
localToolExecutors: deps.localToolExecutors,
|
|
451
496
|
...runtimeToolExecutionLimits,
|
|
452
497
|
});
|
|
453
498
|
|
|
454
|
-
async function activateAgentWorkspaceSession(args: {
|
|
455
|
-
agentRef: string;
|
|
456
|
-
agentConfig: AgentRuntimeAgentConfig | null;
|
|
457
|
-
}) {
|
|
458
|
-
const previousWorkspaceRoot = workspaceSession.workspaceRoot;
|
|
459
|
-
workspaceSession = await activateWorkspaceSession(workspaceSession, {
|
|
460
|
-
agentKey: args.agentConfig?.key ?? args.agentRef,
|
|
461
|
-
mode: readLocalWorkspaceMode(args.agentConfig),
|
|
462
|
-
env: deps.env,
|
|
463
|
-
prepareTaskWorkspace: deps.prepareTaskWorktree,
|
|
464
|
-
});
|
|
465
|
-
if (workspaceSession.workspaceRoot === previousWorkspaceRoot) return;
|
|
466
|
-
localToolExecutors = buildLocalToolExecutors({
|
|
467
|
-
workspaceRoot: workspaceSession.workspaceRoot,
|
|
468
|
-
env: deps.env,
|
|
469
|
-
fetchImpl,
|
|
470
|
-
localToolExecutors: deps.localToolExecutors,
|
|
471
|
-
...runtimeToolExecutionLimits,
|
|
472
|
-
});
|
|
473
|
-
deps.output?.write(formatWorkspaceSessionActivation(workspaceSession));
|
|
474
|
-
}
|
|
475
|
-
|
|
476
499
|
return {
|
|
477
500
|
host: "cli",
|
|
478
501
|
capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence", "local-tools"],
|
|
479
502
|
loadAgentConfig: async (agentRef) => {
|
|
480
|
-
const agentConfig = await readAgentFromStore({
|
|
503
|
+
const agentConfig = withBuiltinNoloCliTools(await readAgentFromStore({
|
|
481
504
|
agentRef,
|
|
482
505
|
store: await resolveStore(deps),
|
|
483
506
|
userId,
|
|
484
|
-
});
|
|
507
|
+
}));
|
|
485
508
|
const requestedToolNames = agentConfig
|
|
486
509
|
? resolveRequestedRuntimeToolNames({ agentConfig })
|
|
487
510
|
: [];
|
|
@@ -492,9 +515,8 @@ export function createCliLocalRuntimeAdapter(
|
|
|
492
515
|
runtimeToolExecutionLimits = resolveLocalWorkspaceExecutorOptionsFromPolicy(
|
|
493
516
|
resolveCurrentRunRuntimeToolPolicy(agentConfig),
|
|
494
517
|
);
|
|
495
|
-
await activateAgentWorkspaceSession({ agentRef, agentConfig });
|
|
496
518
|
localToolExecutors = buildLocalToolExecutors({
|
|
497
|
-
workspaceRoot
|
|
519
|
+
workspaceRoot,
|
|
498
520
|
env: deps.env,
|
|
499
521
|
fetchImpl,
|
|
500
522
|
localToolExecutors: deps.localToolExecutors,
|
|
@@ -512,7 +534,7 @@ export function createCliLocalRuntimeAdapter(
|
|
|
512
534
|
userId,
|
|
513
535
|
now,
|
|
514
536
|
createId,
|
|
515
|
-
cwd:
|
|
537
|
+
cwd: workspaceRoot,
|
|
516
538
|
}),
|
|
517
539
|
resolveProvider: async (agentConfig) => {
|
|
518
540
|
if (shouldUsePlatformChatProvider(deps.env, agentConfig)) {
|
|
@@ -558,6 +580,9 @@ export function createCliLocalRuntimeAdapter(
|
|
|
558
580
|
model: providerConfig.model,
|
|
559
581
|
messageCount: messages.length,
|
|
560
582
|
toolCount: tools.length,
|
|
583
|
+
requestedToolNames,
|
|
584
|
+
openAiToolNames: summarizeOpenAiToolNames(tools),
|
|
585
|
+
shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
|
|
561
586
|
timeoutMs: options?.timeoutMs ?? null,
|
|
562
587
|
});
|
|
563
588
|
const res = await fetchWithTransientRetry(fetchImpl, request.url, {
|
|
@@ -627,6 +652,9 @@ export function createCliLocalRuntimeAdapter(
|
|
|
627
652
|
model: providerConfig.model,
|
|
628
653
|
messageCount: messages.length,
|
|
629
654
|
toolCount: tools.length,
|
|
655
|
+
requestedToolNames,
|
|
656
|
+
openAiToolNames: summarizeOpenAiToolNames(tools),
|
|
657
|
+
shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
|
|
630
658
|
timeoutMs: options?.timeoutMs ?? null,
|
|
631
659
|
});
|
|
632
660
|
const res = await fetchWithTransientRetry(fetchImpl, request.url, {
|
|
@@ -672,8 +700,8 @@ export function createCliLocalRuntimeAdapter(
|
|
|
672
700
|
...result,
|
|
673
701
|
metadata: {
|
|
674
702
|
...(result.metadata ?? {}),
|
|
675
|
-
workspaceRoot
|
|
676
|
-
workspaceKind:
|
|
703
|
+
workspaceRoot,
|
|
704
|
+
workspaceKind: "current",
|
|
677
705
|
},
|
|
678
706
|
};
|
|
679
707
|
},
|
package/connectorRunArtifact.ts
CHANGED
|
@@ -32,7 +32,6 @@ export function resolveConnectorRunCwd(args: {
|
|
|
32
32
|
const explicit =
|
|
33
33
|
args.env?.NOLO_CONNECTOR_WORKDIR ||
|
|
34
34
|
args.env?.NOLO_AGENT_WORKDIR ||
|
|
35
|
-
args.env?.NOLO_LOCAL_WORKTREE ||
|
|
36
35
|
"";
|
|
37
36
|
const candidate = explicit || args.policy.writableRoots[0] || args.fallbackCwd || process.cwd();
|
|
38
37
|
const cwd = normalizePath(candidate);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// 文件路径: packages/database/server/db.ts
|
|
2
|
+
|
|
3
|
+
import { ensureDbOpen } from "./ensureDbOpen";
|
|
4
|
+
import { resolveServerDbPath } from "./dbPath";
|
|
5
|
+
import type { LegacyServerDb } from "./legacyServerDb";
|
|
6
|
+
import type { AuthorityStore } from "./authorityStoreTypes";
|
|
7
|
+
import { getOrCreateServerStoreRuntime } from "./serverStoreFactory";
|
|
8
|
+
|
|
9
|
+
const DB_PATH = resolveServerDbPath();
|
|
10
|
+
|
|
11
|
+
console.log("数据库配置:");
|
|
12
|
+
console.log("- 当前工作目录:", process.cwd());
|
|
13
|
+
console.log("- 数据库路径:", DB_PATH);
|
|
14
|
+
|
|
15
|
+
export function isServerDbLockError(error: unknown): boolean {
|
|
16
|
+
const queue: unknown[] = [error];
|
|
17
|
+
const seen = new Set<unknown>();
|
|
18
|
+
|
|
19
|
+
while (queue.length > 0) {
|
|
20
|
+
const current = queue.shift();
|
|
21
|
+
if (!current || seen.has(current)) continue;
|
|
22
|
+
seen.add(current);
|
|
23
|
+
|
|
24
|
+
const text = `${(current as any)?.code ?? ""} ${(current as any)?.message ?? current}`;
|
|
25
|
+
if (/LEVEL_LOCKED|Resource temporarily unavailable|\/LOCK|LOCK:/.test(text)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ((current as any)?.cause) queue.push((current as any).cause);
|
|
30
|
+
if (Array.isArray((current as any)?.errors)) {
|
|
31
|
+
queue.push(...(current as any).errors);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
authorityStore,
|
|
40
|
+
serverDb,
|
|
41
|
+
}: { authorityStore: AuthorityStore; serverDb: LegacyServerDb } =
|
|
42
|
+
getOrCreateServerStoreRuntime(DB_PATH);
|
|
43
|
+
|
|
44
|
+
console.log("- LevelDB实际路径:", serverDb.location);
|
|
45
|
+
|
|
46
|
+
// ✅ 保留默认导出:兼容现有的 import serverDb from "./db"
|
|
47
|
+
export default serverDb;
|
|
48
|
+
|
|
49
|
+
// 具名导出:需要的时候可以引用
|
|
50
|
+
export { serverDb };
|
|
51
|
+
export function getServerAuthorityStore() {
|
|
52
|
+
return authorityStore;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 确保数据库处于 open 状态。
|
|
57
|
+
* - 已 open / opening:直接返回
|
|
58
|
+
* - 其他状态:尝试 open,一旦失败抛错
|
|
59
|
+
*/
|
|
60
|
+
export async function ensureServerDbOpen() {
|
|
61
|
+
const status = authorityStore.status;
|
|
62
|
+
if (status === "open") return;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await ensureDbOpen(authorityStore);
|
|
66
|
+
if (status !== "opening") {
|
|
67
|
+
console.log("✅ LevelDB 已打开");
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (isServerDbLockError(err)) {
|
|
71
|
+
console.error("❌ 打开 LevelDB 失败: 数据库已被其他进程占用");
|
|
72
|
+
} else {
|
|
73
|
+
console.error("❌ 打开 LevelDB 失败:", err);
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 兼容入口调用:启动时显式 open 一次
|
|
81
|
+
*/
|
|
82
|
+
export async function openServerDb() {
|
|
83
|
+
return ensureServerDbOpen();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 优雅关机时关闭 DB
|
|
88
|
+
*/
|
|
89
|
+
export async function closeServerDb() {
|
|
90
|
+
const status = authorityStore.status;
|
|
91
|
+
if (status !== "open") return;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await authorityStore.close();
|
|
95
|
+
console.log("✅ LevelDB 已关闭");
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error("❌ 关闭 LevelDB 失败:", err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthorityBatchOperation,
|
|
3
|
+
AuthorityBatchWriter,
|
|
4
|
+
AuthorityIteratorOptions,
|
|
5
|
+
AuthorityStore,
|
|
6
|
+
} from "./authorityStoreTypes";
|
|
7
|
+
|
|
8
|
+
type MemoryEntry = {
|
|
9
|
+
key: string;
|
|
10
|
+
value: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const createNotFoundError = () => {
|
|
14
|
+
const error = new Error("NotFound") as Error & { notFound: true };
|
|
15
|
+
error.notFound = true;
|
|
16
|
+
return error;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
class MemoryAuthorityBacking {
|
|
20
|
+
readonly location: string;
|
|
21
|
+
status: "closed" | "open" = "closed";
|
|
22
|
+
private readonly data = new Map<string, unknown>();
|
|
23
|
+
|
|
24
|
+
constructor(location: string) {
|
|
25
|
+
this.location = location;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async open() {
|
|
29
|
+
this.status = "open";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async close() {
|
|
33
|
+
this.status = "closed";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async get<T = any>(key: string): Promise<T> {
|
|
37
|
+
if (!this.data.has(key)) {
|
|
38
|
+
throw createNotFoundError();
|
|
39
|
+
}
|
|
40
|
+
return this.data.get(key) as T;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async put(key: string, value: unknown) {
|
|
44
|
+
this.data.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async del(key: string) {
|
|
48
|
+
this.data.delete(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async batchWrite(ops: AuthorityBatchOperation[]) {
|
|
52
|
+
for (const op of ops) {
|
|
53
|
+
if (op.type === "put") {
|
|
54
|
+
this.data.set(op.key, op.value);
|
|
55
|
+
} else {
|
|
56
|
+
this.data.delete(op.key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createBatch(): AuthorityBatchWriter {
|
|
62
|
+
const buffered: AuthorityBatchOperation[] = [];
|
|
63
|
+
return {
|
|
64
|
+
put(key: string, value: unknown) {
|
|
65
|
+
buffered.push({ type: "put", key, value });
|
|
66
|
+
},
|
|
67
|
+
del(key: string) {
|
|
68
|
+
buffered.push({ type: "del", key });
|
|
69
|
+
},
|
|
70
|
+
write: async () => {
|
|
71
|
+
await this.batchWrite(buffered);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async *iterator(
|
|
77
|
+
options: AuthorityIteratorOptions = {}
|
|
78
|
+
): AsyncIterableIterator<[string, unknown]> {
|
|
79
|
+
const entries: MemoryEntry[] = Array.from(this.data.entries())
|
|
80
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
81
|
+
.map(([key, value]) => ({ key, value }));
|
|
82
|
+
|
|
83
|
+
if (options.reverse) {
|
|
84
|
+
entries.reverse();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (options.gte && entry.key < options.gte) continue;
|
|
89
|
+
if (options.lte && entry.key > options.lte) continue;
|
|
90
|
+
if (options.lt && entry.key >= options.lt) continue;
|
|
91
|
+
yield [entry.key, entry.value];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createMemoryAuthorityStore(
|
|
97
|
+
location = ":memory:"
|
|
98
|
+
): AuthorityStore {
|
|
99
|
+
const backing = new MemoryAuthorityBacking(location);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
get location() {
|
|
103
|
+
return backing.location;
|
|
104
|
+
},
|
|
105
|
+
get status() {
|
|
106
|
+
return backing.status;
|
|
107
|
+
},
|
|
108
|
+
async open() {
|
|
109
|
+
await backing.open();
|
|
110
|
+
},
|
|
111
|
+
async close() {
|
|
112
|
+
await backing.close();
|
|
113
|
+
},
|
|
114
|
+
async get<T = any>(key: string) {
|
|
115
|
+
return backing.get<T>(key);
|
|
116
|
+
},
|
|
117
|
+
async put(key: string, value: unknown) {
|
|
118
|
+
await backing.put(key, value);
|
|
119
|
+
},
|
|
120
|
+
async del(key: string) {
|
|
121
|
+
await backing.del(key);
|
|
122
|
+
},
|
|
123
|
+
async batchWrite(ops: AuthorityBatchOperation[]) {
|
|
124
|
+
await backing.batchWrite(ops);
|
|
125
|
+
},
|
|
126
|
+
createBatch() {
|
|
127
|
+
return backing.createBatch();
|
|
128
|
+
},
|
|
129
|
+
iterator(options: AuthorityIteratorOptions = {}) {
|
|
130
|
+
return backing.iterator(options);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Level } from "level";
|
|
4
|
+
|
|
5
|
+
import type { AuthorityStore } from "./authorityStoreTypes";
|
|
6
|
+
import { createLegacyServerDb, type LegacyServerDb } from "./legacyServerDb";
|
|
7
|
+
import { createLevelAuthorityStore } from "./levelAuthorityStore";
|
|
8
|
+
import { createMemoryAuthorityStore } from "./memoryAuthorityStore";
|
|
9
|
+
|
|
10
|
+
export type ServerStoreDriver = "level" | "memory";
|
|
11
|
+
|
|
12
|
+
export type ServerStoreFactoryEnv = {
|
|
13
|
+
NOLO_SERVER_AUTHORITY_DRIVER?: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const safeJsonEncoding = {
|
|
17
|
+
name: "safe-json",
|
|
18
|
+
format: "utf8" as const,
|
|
19
|
+
encode: (data: any) => JSON.stringify(data),
|
|
20
|
+
decode: (data: string) => {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(data);
|
|
23
|
+
} catch {
|
|
24
|
+
console.warn("[LevelDB] Corrupted value:", data?.substring?.(0, 60));
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ServerStoreRuntime = {
|
|
31
|
+
authorityStore: AuthorityStore;
|
|
32
|
+
serverDb: LegacyServerDb;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ServerStoreFactoryOptions = {
|
|
36
|
+
driver?: ServerStoreDriver;
|
|
37
|
+
env?: ServerStoreFactoryEnv;
|
|
38
|
+
globalScope?: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function ensureServerDbDirectory(dbPath: string) {
|
|
42
|
+
const dir = path.dirname(dbPath);
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createLevelBackedAuthorityStore(dbPath: string): AuthorityStore {
|
|
49
|
+
const levelDb = new Level<string, any>(dbPath, {
|
|
50
|
+
valueEncoding: safeJsonEncoding,
|
|
51
|
+
});
|
|
52
|
+
return createLevelAuthorityStore(levelDb);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveServerStoreDriver(
|
|
56
|
+
env: ServerStoreFactoryEnv = process.env
|
|
57
|
+
): ServerStoreDriver {
|
|
58
|
+
const rawDriver = env.NOLO_SERVER_AUTHORITY_DRIVER?.trim().toLowerCase();
|
|
59
|
+
if (!rawDriver) return "level";
|
|
60
|
+
if (rawDriver === "level") return "level";
|
|
61
|
+
if (rawDriver === "memory") return "memory";
|
|
62
|
+
throw new Error(`Unsupported server authority store driver: ${rawDriver}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createAuthorityStoreForDriver(
|
|
66
|
+
driver: ServerStoreDriver,
|
|
67
|
+
dbPath: string
|
|
68
|
+
): AuthorityStore {
|
|
69
|
+
switch (driver) {
|
|
70
|
+
case "level":
|
|
71
|
+
return createLevelBackedAuthorityStore(dbPath);
|
|
72
|
+
case "memory":
|
|
73
|
+
return createMemoryAuthorityStore(dbPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getOrCreateServerStoreRuntime(
|
|
78
|
+
dbPath: string,
|
|
79
|
+
options: ServerStoreFactoryOptions = {}
|
|
80
|
+
): ServerStoreRuntime {
|
|
81
|
+
const globalScope =
|
|
82
|
+
options.globalScope ?? (globalThis as Record<string, unknown>);
|
|
83
|
+
const driver = options.driver ?? resolveServerStoreDriver(options.env);
|
|
84
|
+
const cachedDriver = globalScope.__serverAuthorityStoreDriver;
|
|
85
|
+
|
|
86
|
+
ensureServerDbDirectory(dbPath);
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
globalScope.__serverAuthorityStore &&
|
|
90
|
+
typeof cachedDriver === "string" &&
|
|
91
|
+
cachedDriver !== driver
|
|
92
|
+
) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Server authority store already initialized with driver ${cachedDriver}, cannot reuse it for ${driver}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!globalScope.__serverAuthorityStore) {
|
|
99
|
+
globalScope.__serverAuthorityStore = createAuthorityStoreForDriver(
|
|
100
|
+
driver,
|
|
101
|
+
dbPath
|
|
102
|
+
);
|
|
103
|
+
globalScope.__serverAuthorityStoreDriver = driver;
|
|
104
|
+
} else if (!cachedDriver) {
|
|
105
|
+
globalScope.__serverAuthorityStoreDriver = driver;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const authorityStore = globalScope.__serverAuthorityStore as AuthorityStore;
|
|
109
|
+
|
|
110
|
+
if (!globalScope.__serverDb) {
|
|
111
|
+
globalScope.__serverDb = createLegacyServerDb(authorityStore);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
authorityStore,
|
|
116
|
+
serverDb: globalScope.__serverDb as LegacyServerDb,
|
|
117
|
+
};
|
|
118
|
+
}
|