@zleap-ai/dsh-sag 0.1.0
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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/README.zh.md +58 -0
- package/THIRD_PARTY_NOTICES +671 -0
- package/cordis.patch.yml +5 -0
- package/docs/embedded.md +61 -0
- package/lib/brand.d.ts +13 -0
- package/lib/brand.js +15 -0
- package/lib/cli/runtime.d.ts +40 -0
- package/lib/cli/runtime.js +121 -0
- package/lib/cli.d.ts +21 -0
- package/lib/cli.js +265 -0
- package/lib/config.d.ts +60 -0
- package/lib/config.js +120 -0
- package/lib/connection/descriptor.d.ts +4 -0
- package/lib/connection/descriptor.js +66 -0
- package/lib/connection/discovery.d.ts +40 -0
- package/lib/connection/discovery.js +139 -0
- package/lib/connection/guidance.d.ts +7 -0
- package/lib/connection/guidance.js +7 -0
- package/lib/connection/manager.d.ts +67 -0
- package/lib/connection/manager.js +273 -0
- package/lib/connection/store.d.ts +42 -0
- package/lib/connection/store.js +164 -0
- package/lib/connection/types.d.ts +35 -0
- package/lib/connection/types.js +2 -0
- package/lib/dsh-sag-cli.js +32202 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +68 -0
- package/lib/local/api-client.d.ts +157 -0
- package/lib/local/api-client.js +338 -0
- package/lib/local/gateway.d.ts +43 -0
- package/lib/local/gateway.js +34 -0
- package/lib/local/mcp-probe.d.ts +27 -0
- package/lib/local/mcp-probe.js +92 -0
- package/lib/presentation.d.ts +8 -0
- package/lib/presentation.js +7 -0
- package/lib/runtime/client.d.ts +22 -0
- package/lib/runtime/client.js +117 -0
- package/lib/runtime/protocol.d.ts +113 -0
- package/lib/runtime/protocol.js +118 -0
- package/lib/runtime/supervisor.d.ts +30 -0
- package/lib/runtime/supervisor.js +107 -0
- package/lib/tools/documents.d.ts +8 -0
- package/lib/tools/documents.js +134 -0
- package/lib/tools/ingest.d.ts +5 -0
- package/lib/tools/ingest.js +35 -0
- package/lib/tools/local.d.ts +40 -0
- package/lib/tools/local.js +111 -0
- package/lib/tools/output.d.ts +96 -0
- package/lib/tools/output.js +67 -0
- package/lib/tools/read.d.ts +6 -0
- package/lib/tools/read.js +84 -0
- package/lib/tools/search.d.ts +6 -0
- package/lib/tools/search.js +107 -0
- package/lib/tools/sources.d.ts +5 -0
- package/lib/tools/sources.js +61 -0
- package/lib/tools/status.d.ts +5 -0
- package/lib/tools/status.js +28 -0
- package/lib/tools/upload.d.ts +6 -0
- package/lib/tools/upload.js +68 -0
- package/package.json +96 -0
- package/runtime/pyproject.toml +29 -0
- package/runtime/src/dsh_sag_runtime/__init__.py +3 -0
- package/runtime/src/dsh_sag_runtime/__main__.py +80 -0
- package/runtime/src/dsh_sag_runtime/engines.py +139 -0
- package/runtime/src/dsh_sag_runtime/errors.py +47 -0
- package/runtime/src/dsh_sag_runtime/evidence.py +53 -0
- package/runtime/src/dsh_sag_runtime/protocol.py +161 -0
- package/runtime/src/dsh_sag_runtime/read.py +61 -0
- package/runtime/src/dsh_sag_runtime/search.py +76 -0
- package/runtime/src/dsh_sag_runtime/server.py +136 -0
- package/runtime/uv.lock +2310 -0
- package/scripts/setup-runtime.mjs +47 -0
package/docs/embedded.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# dsh-sag embedded 模式
|
|
2
|
+
|
|
3
|
+
embedded 模式面向需要由 DeepSeek Harness 直接启动 `zleap-sag==0.10.0` 引擎的高级部署。普通本机 SAG 用户不需要此模式。
|
|
4
|
+
|
|
5
|
+
## 准备运行时
|
|
6
|
+
|
|
7
|
+
需要 Python 3.11+ 和 `uv`。先把插件安装到目标 profile:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
dsh plugin --profile web add @zleap-ai/dsh-sag
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`dsh plugin ... exec` 会在该 profile 的安装目录中运行命令,因此不依赖当前目录存在 `node_modules`:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
dsh plugin --profile web exec node node_modules/@zleap-ai/dsh-sag/scripts/setup-runtime.mjs --python python3 --target "$HOME/.local/share/dsh-sag"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
命令会创建固定版本的 Python 环境,并输出解释器路径。
|
|
20
|
+
|
|
21
|
+
## 配置
|
|
22
|
+
|
|
23
|
+
准备 SAG Engine 环境文件,例如 `/etc/dsh-sag/sag.env`。以下三个环境变量分别表示 Python 解释器、SAG 环境文件和允许使用的 namespace:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
export DSH_SAG_PYTHON="$HOME/.local/share/dsh-sag/dsh-sag-runtime-0.1.0/bin/python"
|
|
27
|
+
export DSH_SAG_ENV_FILE=/etc/dsh-sag/sag.env
|
|
28
|
+
export DSH_SAG_NAMESPACES='[{"id":"product-docs","label":"产品文档"}]'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Harness home 优先使用 `$DSH_HOME`,未设置时是 `~/.dsh`。将下列 patch 写入 `$DSH_HOME/profiles/web/cordis.patch.yml`;如果未设置 `$DSH_HOME`,对应路径是 `~/.dsh/profiles/web/cordis.patch.yml`:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
- id: dsh-sag
|
|
35
|
+
name: '@zleap-ai/dsh-sag'
|
|
36
|
+
config:
|
|
37
|
+
mode: embedded
|
|
38
|
+
pythonCommand: !!js process.env.DSH_SAG_PYTHON
|
|
39
|
+
envFile: !!js process.env.DSH_SAG_ENV_FILE
|
|
40
|
+
namespaces: !!js JSON.parse(process.env.DSH_SAG_NAMESPACES)
|
|
41
|
+
defaultMode: fast
|
|
42
|
+
maxResults: 20
|
|
43
|
+
maxReadEngines: 4
|
|
44
|
+
requestTimeoutMs: 30000
|
|
45
|
+
shutdownGraceMs: 5000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
每个 namespace 的 `id` 必须与 SAG 数据源一致,不能为空、不能重复,最长 36 个 URL-safe 字符。`label` 只用于向用户和模型说明其内容。
|
|
49
|
+
|
|
50
|
+
## 启动与诊断
|
|
51
|
+
|
|
52
|
+
插件加载时会检查 Python 可执行文件、环境文件、协议版本、`zleap-sag` 版本、引擎健康状态、证据读取能力和 namespace 列表。任一检查失败都会停止加载,并在 dsh 日志中给出具体原因。
|
|
53
|
+
|
|
54
|
+
先在不启动服务的情况下检查最终配置,再启动 `web` profile 并观察启动日志:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
dsh --profile web --dump-config
|
|
58
|
+
dsh web
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
卸载插件或停止 dsh 时,插件先向 sidecar 发送 `shutdown` 并等待引擎释放资源;超过 `shutdownGraceMs` 后会终止受管进程树。运行中检索超时由 `requestTimeoutMs` 控制。
|
package/lib/brand.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A string whose meaning is a configured SAG namespace. */
|
|
2
|
+
export type SagNamespaceId = string & {
|
|
3
|
+
readonly __brand: 'SagNamespaceId';
|
|
4
|
+
};
|
|
5
|
+
/** An opaque reference returned by `sag_search` and consumed by `sag_read`. */
|
|
6
|
+
export type SagEvidenceRef = string & {
|
|
7
|
+
readonly __brand: 'SagEvidenceRef';
|
|
8
|
+
};
|
|
9
|
+
/** Validate and brand a namespace at a wire or configuration boundary. */
|
|
10
|
+
export declare function sagNamespaceId(value: string): SagNamespaceId;
|
|
11
|
+
/** Validate and brand an opaque evidence reference at a wire boundary. */
|
|
12
|
+
export declare function sagEvidenceRef(value: string): SagEvidenceRef;
|
|
13
|
+
//# sourceMappingURL=brand.d.ts.map
|
package/lib/brand.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Validate and brand a namespace at a wire or configuration boundary. */
|
|
2
|
+
export function sagNamespaceId(value) {
|
|
3
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/.test(value)) {
|
|
4
|
+
throw new TypeError(`invalid SAG namespace id: ${JSON.stringify(value)}`);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
/** Validate and brand an opaque evidence reference at a wire boundary. */
|
|
9
|
+
export function sagEvidenceRef(value) {
|
|
10
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length > 4096) {
|
|
11
|
+
throw new TypeError('invalid SAG evidence reference');
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { discoverConnection } from '../connection/discovery.js';
|
|
2
|
+
import type { SagConnectionManagerDeps } from '../connection/manager.js';
|
|
3
|
+
import { SagConnectionStore } from '../connection/store.js';
|
|
4
|
+
/** Candidate locations used by one non-interactive connection discovery attempt. */
|
|
5
|
+
export interface CliDiscoveryOptions {
|
|
6
|
+
readonly paths?: readonly string[];
|
|
7
|
+
readonly urls?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/** The live local services consumed by the command-line interface. */
|
|
10
|
+
export interface CliRuntime {
|
|
11
|
+
readonly store: SagConnectionStore;
|
|
12
|
+
readonly discovery: {
|
|
13
|
+
discover(signal: AbortSignal, options?: CliDiscoveryOptions): ReturnType<typeof discoverConnection>;
|
|
14
|
+
};
|
|
15
|
+
readonly inspect?: SagConnectionManagerDeps['inspect'];
|
|
16
|
+
dispose(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/** One initialized Cordis layer that can release its owned resources. */
|
|
19
|
+
export interface CliRuntimeLayer {
|
|
20
|
+
dispose(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** Layer startup operations for the short-lived CLI runtime. */
|
|
23
|
+
export interface CliRuntimeLayerFactory {
|
|
24
|
+
startSettings(): Promise<CliRuntimeLayer>;
|
|
25
|
+
startCredentials(): Promise<CliRuntimeLayer>;
|
|
26
|
+
startRegistration(): Promise<{
|
|
27
|
+
readonly layer: CliRuntimeLayer;
|
|
28
|
+
readonly store: SagConnectionStore;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
/** Settings store and reverse-order disposer created from initialized runtime layers. */
|
|
32
|
+
export interface CliRuntimeLifecycle {
|
|
33
|
+
readonly store: SagConnectionStore;
|
|
34
|
+
dispose(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Start nested runtime layers and guarantee every initialized layer is released in reverse order. */
|
|
37
|
+
export declare function startCliRuntime(factory: CliRuntimeLayerFactory): Promise<CliRuntimeLifecycle>;
|
|
38
|
+
/** Start short-lived local settings and credential providers under the shared DSH home. */
|
|
39
|
+
export declare function createCliRuntime(): Promise<CliRuntime>;
|
|
40
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
5
|
+
import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local';
|
|
6
|
+
import { FileSettingsProvider } from '@deepseek-ai/dsh-settings-file';
|
|
7
|
+
import { resolveConfig } from '../config.js';
|
|
8
|
+
import { discoverConnection, platformConnectionPaths } from '../connection/discovery.js';
|
|
9
|
+
import { SagConnectionStore, registerSagSettings } from '../connection/store.js';
|
|
10
|
+
function runtimeFileSystem() {
|
|
11
|
+
return {
|
|
12
|
+
async resolve(path) {
|
|
13
|
+
const filename = resolve(path);
|
|
14
|
+
return { targetKey: filename, displayPath: filename };
|
|
15
|
+
},
|
|
16
|
+
async readText(target, signal) {
|
|
17
|
+
if (signal?.aborted)
|
|
18
|
+
throw signal.reason;
|
|
19
|
+
return readFile(target.displayPath, 'utf8');
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function disposalFailures(layers) {
|
|
24
|
+
const failures = [];
|
|
25
|
+
for (const layer of [...layers].reverse()) {
|
|
26
|
+
try {
|
|
27
|
+
await layer.dispose();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
failures.push(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return failures;
|
|
34
|
+
}
|
|
35
|
+
function throwDisposalFailures(failures) {
|
|
36
|
+
if (failures.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
if (failures.length === 1)
|
|
39
|
+
throw failures[0];
|
|
40
|
+
throw new AggregateError(failures, 'dsh-sag: multiple CLI runtime disposal failures');
|
|
41
|
+
}
|
|
42
|
+
/** Start nested runtime layers and guarantee every initialized layer is released in reverse order. */
|
|
43
|
+
export async function startCliRuntime(factory) {
|
|
44
|
+
const layers = [];
|
|
45
|
+
let store;
|
|
46
|
+
try {
|
|
47
|
+
layers.push(await factory.startSettings());
|
|
48
|
+
layers.push(await factory.startCredentials());
|
|
49
|
+
const registration = await factory.startRegistration();
|
|
50
|
+
layers.push(registration.layer);
|
|
51
|
+
store = registration.store;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const failures = await disposalFailures(layers);
|
|
55
|
+
if (failures.length === 0)
|
|
56
|
+
throw error;
|
|
57
|
+
throw new AggregateError([error, ...failures], 'dsh-sag: CLI runtime startup and disposal failed');
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
store: store,
|
|
61
|
+
async dispose() {
|
|
62
|
+
throwDisposalFailures(await disposalFailures(layers));
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function activatedLayer(factory) {
|
|
67
|
+
const layer = factory();
|
|
68
|
+
try {
|
|
69
|
+
await layer;
|
|
70
|
+
return layer;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const failures = await disposalFailures([layer]);
|
|
74
|
+
if (failures.length === 0)
|
|
75
|
+
throw error;
|
|
76
|
+
throw new AggregateError([error, ...failures], 'dsh-sag: CLI runtime layer startup and disposal failed');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Start short-lived local settings and credential providers under the shared DSH home. */
|
|
80
|
+
export async function createCliRuntime() {
|
|
81
|
+
const ctx = new Context();
|
|
82
|
+
const dshHome = process.env.DSH_HOME;
|
|
83
|
+
const providerConfig = { watch: false, ...(dshHome === undefined ? {} : { dshHome }) };
|
|
84
|
+
const lifecycle = await startCliRuntime({
|
|
85
|
+
startSettings: () => activatedLayer(() => ctx.plugin(FileSettingsProvider, providerConfig)),
|
|
86
|
+
startCredentials: () => activatedLayer(() => ctx.plugin(LocalCredentialProvider, providerConfig)),
|
|
87
|
+
async startRegistration() {
|
|
88
|
+
let store;
|
|
89
|
+
const layer = await activatedLayer(() => ctx.plugin({
|
|
90
|
+
inject: ['settings', 'credentials'],
|
|
91
|
+
apply(child) {
|
|
92
|
+
store = new SagConnectionStore({ credentials: child.credentials, settings: registerSagSettings(child) });
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
if (store === undefined) {
|
|
96
|
+
await layer.dispose();
|
|
97
|
+
throw new Error('dsh-sag: local settings runtime did not start');
|
|
98
|
+
}
|
|
99
|
+
return { layer, store };
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const fs = runtimeFileSystem();
|
|
103
|
+
const defaults = resolveConfig({});
|
|
104
|
+
if (defaults.mode !== 'local')
|
|
105
|
+
throw new Error('dsh-sag: local discovery defaults are unavailable');
|
|
106
|
+
return {
|
|
107
|
+
store: lifecycle.store,
|
|
108
|
+
discovery: {
|
|
109
|
+
discover(signal, options = {}) {
|
|
110
|
+
return discoverConnection({
|
|
111
|
+
fs,
|
|
112
|
+
paths: options.paths ?? platformConnectionPaths(process.env, process.platform, homedir()),
|
|
113
|
+
urls: options.urls ?? defaults.discoveryUrls,
|
|
114
|
+
fetch: (url, init) => fetch(url, init),
|
|
115
|
+
}, signal);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
dispose: lifecycle.dispose,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=runtime.js.map
|
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type CliRuntime } from './cli/runtime.js';
|
|
3
|
+
export type { CliRuntime } from './cli/runtime.js';
|
|
4
|
+
/** Minimal output ports for the reusable command-line entry point. */
|
|
5
|
+
export interface CliIo {
|
|
6
|
+
stdout(line: string): void;
|
|
7
|
+
stderr(line: string): void;
|
|
8
|
+
}
|
|
9
|
+
/** Injectable process boundaries for focused command-line tests. */
|
|
10
|
+
export interface CliDependencies {
|
|
11
|
+
createRuntime(): Promise<CliRuntime>;
|
|
12
|
+
readFile?(path: string, encoding: 'utf8'): Promise<string>;
|
|
13
|
+
homedir?(): string;
|
|
14
|
+
/** Testable hard deadline for one complete connection check. */
|
|
15
|
+
requestTimeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Run setup, doctor, or help with no prompts and an explicit process exit code. */
|
|
18
|
+
export declare function runCli(argv: readonly string[], io: CliIo, dependencies?: CliDependencies): Promise<number>;
|
|
19
|
+
/** Detect direct execution after resolving npm/pnpm bin symlinks without firing when this module is imported. */
|
|
20
|
+
export declare function isDirectCliEntry(moduleUrl: string, argvEntry: string | undefined, resolveRealPath?: (path: string) => string): boolean;
|
|
21
|
+
//# sourceMappingURL=cli.d.ts.map
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { parseConnectionDescriptor } from './connection/descriptor.js';
|
|
8
|
+
import { SAG_SETUP_COMMAND } from './connection/guidance.js';
|
|
9
|
+
import { SagConnectionManager, } from './connection/manager.js';
|
|
10
|
+
import { createCliRuntime } from './cli/runtime.js';
|
|
11
|
+
const HELP = `用法:
|
|
12
|
+
dsh-sag setup
|
|
13
|
+
dsh-sag setup --url <SAG 本机地址>
|
|
14
|
+
dsh-sag setup <SAG 导出文件>
|
|
15
|
+
dsh-sag doctor
|
|
16
|
+
`;
|
|
17
|
+
const RECOVERY = `请启动 SAG 后重试;也可运行 ${SAG_SETUP_COMMAND} 重新发现。\n`;
|
|
18
|
+
function usage(io, message) {
|
|
19
|
+
if (message !== undefined)
|
|
20
|
+
io.stderr(`${message}\n`);
|
|
21
|
+
io.stderr(HELP);
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
24
|
+
function parse(argv, io) {
|
|
25
|
+
let result;
|
|
26
|
+
try {
|
|
27
|
+
result = parseArgs({
|
|
28
|
+
args: argv,
|
|
29
|
+
options: {
|
|
30
|
+
help: { type: 'boolean', short: 'h' },
|
|
31
|
+
url: { type: 'string' },
|
|
32
|
+
},
|
|
33
|
+
allowPositionals: true,
|
|
34
|
+
strict: false,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return usage(io, '参数无效。');
|
|
39
|
+
}
|
|
40
|
+
const keys = Object.keys(result.values);
|
|
41
|
+
if (keys.some(key => key !== 'help' && key !== 'url'))
|
|
42
|
+
return usage(io, '参数无效。');
|
|
43
|
+
if (result.values.help === true)
|
|
44
|
+
return result.positionals.length === 0 && keys.length === 1 ? { kind: 'help' } : usage(io, '参数无效。');
|
|
45
|
+
const [command, ...positionals] = result.positionals;
|
|
46
|
+
if (command === 'doctor' && positionals.length === 0 && keys.length === 0)
|
|
47
|
+
return { kind: 'doctor' };
|
|
48
|
+
if (command !== 'setup')
|
|
49
|
+
return usage(io, '参数无效。');
|
|
50
|
+
const url = result.values.url;
|
|
51
|
+
if (url !== undefined && (typeof url !== 'string' || positionals.length !== 0))
|
|
52
|
+
return usage(io, '参数无效。');
|
|
53
|
+
if (url === undefined && positionals.length > 1)
|
|
54
|
+
return usage(io, '参数无效。');
|
|
55
|
+
if (url !== undefined) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = new URL(url);
|
|
58
|
+
if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || parsed.username || parsed.password)
|
|
59
|
+
throw new Error('invalid URL');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return usage(io, 'SAG 地址必须是没有用户名或密码的 HTTP(S) 地址。');
|
|
63
|
+
}
|
|
64
|
+
return { kind: 'setup', url };
|
|
65
|
+
}
|
|
66
|
+
return { kind: 'setup', ...(positionals[0] === undefined ? {} : { path: positionals[0] }) };
|
|
67
|
+
}
|
|
68
|
+
function expandHome(path, getHome) {
|
|
69
|
+
if (path === '~')
|
|
70
|
+
return getHome();
|
|
71
|
+
if (path.startsWith('~/') || path.startsWith('~\\'))
|
|
72
|
+
return `${getHome()}${path.slice(1)}`;
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
function redactedMessage(error, secrets = []) {
|
|
76
|
+
const message = typeof error === 'string' ? error : error instanceof Error ? error.message : '未知错误';
|
|
77
|
+
return secrets.reduce((value, secret) => secret ? value.split(secret).join('<redacted>') : value, message)
|
|
78
|
+
.replace(/Bearer\s+[^\s,;]+/giu, 'Bearer <redacted>')
|
|
79
|
+
.replace(/([?&](?:access_)?token=)[^&#\s]+/giu, '$1<redacted>')
|
|
80
|
+
.replace(/(\/\/)[^/@\s]+@/gu, '$1<redacted>@');
|
|
81
|
+
}
|
|
82
|
+
function diagnostic(report, io) {
|
|
83
|
+
if (report.status !== 'ready') {
|
|
84
|
+
io.stderr(`SAG 未就绪:${report.status}\n`);
|
|
85
|
+
for (const error of report.errors ?? [])
|
|
86
|
+
io.stderr(`- ${redactedMessage(error)}\n`);
|
|
87
|
+
for (const item of report.discovery?.diagnostics ?? [])
|
|
88
|
+
io.stderr(`- ${redactedMessage(item.action)}\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
io.stdout('SAG 已连接。\n');
|
|
92
|
+
io.stdout(`健康检查:${report.health ? '正常' : '失败'};就绪检查:${report.ready ? '通过' : '失败'}。\n`);
|
|
93
|
+
io.stdout(`知识源:${report.sourceCount} 个。\n`);
|
|
94
|
+
if (report.capabilities !== undefined) {
|
|
95
|
+
if (report.capabilities.upload !== undefined) {
|
|
96
|
+
io.stdout(`文件上传:最多 ${report.capabilities.upload.maxMb} MiB;支持 ${report.capabilities.upload.extensions.join('、')}。\n`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
io.stdout('文件上传:当前 SAG 未提供。\n');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function setupDiscovery(descriptor) {
|
|
104
|
+
return { source: 'file', descriptor, diagnostics: [] };
|
|
105
|
+
}
|
|
106
|
+
function setupManager(runtime, discover, requestTimeoutMs) {
|
|
107
|
+
const deps = {
|
|
108
|
+
store: {
|
|
109
|
+
load: async () => undefined,
|
|
110
|
+
},
|
|
111
|
+
discover,
|
|
112
|
+
...(runtime.inspect === undefined ? {} : { inspect: runtime.inspect }),
|
|
113
|
+
...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
|
|
114
|
+
};
|
|
115
|
+
return new SagConnectionManager(deps);
|
|
116
|
+
}
|
|
117
|
+
async function runSetup(command, runtime, io, dependencies, rememberSecret) {
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
let discover;
|
|
120
|
+
if (command.path !== undefined) {
|
|
121
|
+
const filename = expandHome(command.path, dependencies.homedir ?? homedir);
|
|
122
|
+
let descriptor;
|
|
123
|
+
try {
|
|
124
|
+
descriptor = parseConnectionDescriptor(JSON.parse(await (dependencies.readFile ?? readFile)(filename, 'utf8')));
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
io.stderr(`无法读取 SAG 导出文件:${redactedMessage(error)}\n`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
discover = async () => setupDiscovery(descriptor);
|
|
131
|
+
}
|
|
132
|
+
else if (command.url !== undefined) {
|
|
133
|
+
discover = signal => runtime.discovery.discover(signal, { paths: [], urls: [command.url] });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
discover = signal => runtime.discovery.discover(signal);
|
|
137
|
+
}
|
|
138
|
+
let report;
|
|
139
|
+
try {
|
|
140
|
+
report = await setupManager(runtime, discover, dependencies.requestTimeoutMs).ensureConnected(controller.signal);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
io.stderr(`连接 SAG 失败:${redactedMessage(error)}\n`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
if (report.status !== 'ready') {
|
|
147
|
+
diagnostic(report, io);
|
|
148
|
+
io.stderr(RECOVERY);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
if (report.descriptor === undefined) {
|
|
152
|
+
io.stderr('SAG 连接缺少可保存的连接描述。\n');
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
rememberSecret(report.descriptor.accessToken);
|
|
156
|
+
try {
|
|
157
|
+
await runtime.store.save(report.descriptor);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
io.stderr(`保存 SAG 配置失败:${redactedMessage(error, [report.descriptor.accessToken])}\n`);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
diagnostic(report, io);
|
|
164
|
+
return runDoctor(runtime, io, dependencies.requestTimeoutMs);
|
|
165
|
+
}
|
|
166
|
+
async function runDoctor(runtime, io, requestTimeoutMs) {
|
|
167
|
+
let report;
|
|
168
|
+
try {
|
|
169
|
+
report = await new SagConnectionManager({
|
|
170
|
+
store: runtime.store,
|
|
171
|
+
discover: signal => runtime.discovery.discover(signal),
|
|
172
|
+
...(runtime.inspect === undefined ? {} : { inspect: runtime.inspect }),
|
|
173
|
+
...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
|
|
174
|
+
}).doctor(new AbortController().signal);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
let secrets = [];
|
|
178
|
+
try {
|
|
179
|
+
const saved = await runtime.store.load();
|
|
180
|
+
if (saved !== undefined)
|
|
181
|
+
secrets = [saved.accessToken];
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// The original diagnostic remains the actionable failure.
|
|
185
|
+
}
|
|
186
|
+
io.stderr(`SAG 检查失败:${redactedMessage(error, secrets)}\n`);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
diagnostic(report, io);
|
|
190
|
+
if (report.status === 'ready')
|
|
191
|
+
return 0;
|
|
192
|
+
if (report.status === 'not-found')
|
|
193
|
+
io.stderr(RECOVERY);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
/** Run setup, doctor, or help with no prompts and an explicit process exit code. */
|
|
197
|
+
export async function runCli(argv, io, dependencies = { createRuntime: createCliRuntime }) {
|
|
198
|
+
const command = parse(argv, io);
|
|
199
|
+
if (typeof command === 'number')
|
|
200
|
+
return command;
|
|
201
|
+
if (command.kind === 'help') {
|
|
202
|
+
io.stdout(HELP);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
let runtime;
|
|
206
|
+
let code = 1;
|
|
207
|
+
let secrets = [];
|
|
208
|
+
try {
|
|
209
|
+
runtime = await dependencies.createRuntime();
|
|
210
|
+
try {
|
|
211
|
+
const saved = await runtime.store.load();
|
|
212
|
+
if (saved !== undefined)
|
|
213
|
+
secrets = [saved.accessToken];
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// The command reports saved configuration failures through its connection manager.
|
|
217
|
+
}
|
|
218
|
+
code = command.kind === 'setup'
|
|
219
|
+
? await runSetup(command, runtime, io, dependencies, secret => { secrets = [...secrets, secret]; })
|
|
220
|
+
: await runDoctor(runtime, io, dependencies.requestTimeoutMs);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
io.stderr(`dsh-sag 失败:${redactedMessage(error, secrets)}\n`);
|
|
224
|
+
code = 1;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
if (runtime !== undefined) {
|
|
228
|
+
try {
|
|
229
|
+
await runtime.dispose();
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
try {
|
|
233
|
+
const saved = await runtime.store.load();
|
|
234
|
+
if (saved !== undefined)
|
|
235
|
+
secrets = [...secrets, saved.accessToken];
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Cleanup reporting must continue even when the backing store is unavailable.
|
|
239
|
+
}
|
|
240
|
+
io.stderr(`清理 dsh-sag 运行环境失败:${redactedMessage(error, secrets)}\n`);
|
|
241
|
+
code = 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return code;
|
|
246
|
+
}
|
|
247
|
+
/** Detect direct execution after resolving npm/pnpm bin symlinks without firing when this module is imported. */
|
|
248
|
+
export function isDirectCliEntry(moduleUrl, argvEntry, resolveRealPath = realpathSync) {
|
|
249
|
+
if (argvEntry === undefined)
|
|
250
|
+
return false;
|
|
251
|
+
try {
|
|
252
|
+
return resolveRealPath(argvEntry) === fileURLToPath(moduleUrl);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (isDirectCliEntry(import.meta.url, process.argv[1])) {
|
|
259
|
+
const code = await runCli(process.argv.slice(2), {
|
|
260
|
+
stdout: line => process.stdout.write(line),
|
|
261
|
+
stderr: line => process.stderr.write(line),
|
|
262
|
+
});
|
|
263
|
+
process.exitCode = code;
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=cli.js.map
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import { type SagNamespaceId } from './brand.js';
|
|
3
|
+
import type { SupervisorConfig } from './runtime/supervisor.js';
|
|
4
|
+
export interface NamespaceConfig {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
}
|
|
8
|
+
interface ExistingSidecarConfig {
|
|
9
|
+
readonly pythonCommand: string;
|
|
10
|
+
readonly envFile: string;
|
|
11
|
+
readonly cwd?: string;
|
|
12
|
+
readonly namespaces: readonly NamespaceConfig[];
|
|
13
|
+
readonly defaultMode?: 'fast' | 'precise';
|
|
14
|
+
readonly maxResults?: number;
|
|
15
|
+
readonly maxSnippetChars?: number;
|
|
16
|
+
readonly maxReadChars?: number;
|
|
17
|
+
readonly maxReadEngines?: number;
|
|
18
|
+
readonly requestTimeoutMs?: number;
|
|
19
|
+
readonly shutdownGraceMs?: number;
|
|
20
|
+
readonly allowDegraded?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Connect to an already-running local SAG server. */
|
|
23
|
+
export interface LocalConfig {
|
|
24
|
+
readonly mode?: 'local';
|
|
25
|
+
readonly discoveryUrls?: readonly string[];
|
|
26
|
+
readonly requestTimeoutMs?: number;
|
|
27
|
+
readonly maxUploadBytes?: number;
|
|
28
|
+
readonly maxReadChars?: number;
|
|
29
|
+
readonly connectionCacheTtlMs?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Run the legacy Python sidecar directly. */
|
|
32
|
+
export interface EmbeddedConfig extends ExistingSidecarConfig {
|
|
33
|
+
readonly mode: 'embedded';
|
|
34
|
+
}
|
|
35
|
+
/** Loader-facing plugin configuration. Local mode is the default. */
|
|
36
|
+
export type Config = LocalConfig | EmbeddedConfig;
|
|
37
|
+
export declare const Config: z<Config>;
|
|
38
|
+
export interface ResolvedNamespaceConfig {
|
|
39
|
+
readonly id: SagNamespaceId;
|
|
40
|
+
readonly label: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ResolvedLocalConfig {
|
|
43
|
+
readonly mode: 'local';
|
|
44
|
+
readonly discoveryUrls: readonly string[];
|
|
45
|
+
readonly requestTimeoutMs: number;
|
|
46
|
+
readonly maxUploadBytes: number;
|
|
47
|
+
readonly maxReadChars: number;
|
|
48
|
+
readonly connectionCacheTtlMs: number;
|
|
49
|
+
}
|
|
50
|
+
export interface ResolvedEmbeddedConfig extends SupervisorConfig {
|
|
51
|
+
readonly mode: 'embedded';
|
|
52
|
+
readonly namespaces: readonly ResolvedNamespaceConfig[];
|
|
53
|
+
readonly defaultMode: 'fast' | 'precise';
|
|
54
|
+
readonly requestTimeoutMs: number;
|
|
55
|
+
}
|
|
56
|
+
export type ResolvedConfig = ResolvedLocalConfig | ResolvedEmbeddedConfig;
|
|
57
|
+
/** Apply and validate every default before a local client or sidecar observes config. */
|
|
58
|
+
export declare function resolveConfig(config: Config): ResolvedConfig;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=config.d.ts.map
|