dsh-config-manager 0.1.78 → 0.1.80
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/lib/adapters/credentials.d.ts +6 -2
- package/lib/adapters/credentials.js +19 -3
- package/lib/adapters/file-collection.js +2 -1
- package/lib/adapters/mcp.d.ts +48 -2
- package/lib/adapters/mcp.js +265 -3
- package/lib/adapters/messages.d.ts +3 -0
- package/lib/adapters/messages.js +7 -0
- package/lib/adapters/plugin-files.js +2 -1
- package/lib/adapters/plugins.js +105 -20
- package/lib/adapters/pnpm-workspace.d.ts +32 -0
- package/lib/adapters/pnpm-workspace.js +113 -2
- package/lib/adapters/prompts.d.ts +6 -2
- package/lib/adapters/prompts.js +17 -2
- package/lib/adapters/providers.d.ts +19 -2
- package/lib/adapters/providers.js +59 -3
- package/lib/adapters/settings.d.ts +6 -2
- package/lib/adapters/settings.js +13 -2
- package/lib/adapters/skills.d.ts +25 -1
- package/lib/adapters/skills.js +60 -0
- package/lib/adapters/ui.d.ts +6 -2
- package/lib/adapters/ui.js +13 -2
- package/lib/adapters/units.d.ts +18 -0
- package/lib/adapters/units.js +39 -0
- package/lib/adapters/workspaces.js +2 -1
- package/lib/client.d.ts +401 -112
- package/lib/client.js +2899 -1525
- package/lib/core/backup.js +20 -0
- package/lib/core/mcp-connector-store.d.ts +159 -0
- package/lib/core/mcp-connector-store.js +445 -0
- package/lib/core/messages-core.d.ts +2 -0
- package/lib/core/messages-core.js +4 -0
- package/lib/core/messages.d.ts +20 -0
- package/lib/core/messages.js +44 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +22 -2
- package/lib/core/plugin-cli.js +22 -3
- package/lib/core/recovery-orchestrator.d.ts +3 -1
- package/lib/core/recovery-orchestrator.js +75 -16
- package/lib/core/rollback-activity.d.ts +89 -0
- package/lib/core/rollback-activity.js +350 -0
- package/lib/core/rollback-ledger.d.ts +0 -9
- package/lib/core/rollback-ledger.js +37 -0
- package/lib/core/rollback-merge.d.ts +22 -2
- package/lib/core/rollback-merge.js +63 -13
- package/lib/core/rollback-point.d.ts +93 -0
- package/lib/core/rollback-point.js +420 -101
- package/lib/core/rollback-undo-journal.d.ts +123 -0
- package/lib/core/rollback-undo-journal.js +517 -0
- package/lib/core/self-update.js +2 -4
- package/lib/core/text-fidelity.d.ts +89 -0
- package/lib/core/text-fidelity.js +176 -0
- package/lib/core/types.d.ts +12 -3
- package/lib/core/update-check.js +2 -4
- package/lib/core/validator.d.ts +9 -0
- package/lib/core/validator.js +12 -0
- package/lib/foreign/bundle.js +42 -3
- package/lib/foreign/claude-code.js +358 -26
- package/lib/foreign/claude-sessions.d.ts +51 -8
- package/lib/foreign/claude-sessions.js +111 -13
- package/lib/foreign/read-claude-code.d.ts +33 -4
- package/lib/foreign/read-claude-code.js +446 -65
- package/lib/foreign/read-claude-plugins.d.ts +36 -0
- package/lib/foreign/read-claude-plugins.js +403 -0
- package/lib/foreign/registry.js +25 -2
- package/lib/foreign/truth-table.js +6 -4
- package/lib/foreign/types.d.ts +170 -2
- package/lib/index.d.ts +14 -5
- package/lib/index.js +60 -16
- package/lib/market/git-file-writer.js +7 -1
- package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
- package/lib/profiles/dsh-profile-launcher.js +58 -1
- package/lib/profiles/dsh-profile-shared.d.ts +8 -2
- package/lib/profiles/dsh-profile-shared.js +8 -2
- package/lib/routes/foreign.js +8 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/sync.js +173 -9
- package/lib/schema/manifest.d.ts +6 -1
- package/lib/schema/manifest.js +55 -0
- package/lib/schema/types.d.ts +47 -2
- package/lib/sync/ancestor.d.ts +22 -0
- package/lib/sync/ancestor.js +66 -2
- package/lib/sync/autosync-config.d.ts +73 -0
- package/lib/sync/autosync-config.js +89 -0
- package/lib/sync/autosync-scheduler.d.ts +80 -6
- package/lib/sync/autosync-scheduler.js +263 -40
- package/lib/sync/gist/gist-transport.d.ts +18 -1
- package/lib/sync/gist/gist-transport.js +42 -3
- package/lib/sync/gist/messages.d.ts +2 -0
- package/lib/sync/gist/messages.js +5 -0
- package/lib/sync/git/git-transport.d.ts +42 -3
- package/lib/sync/git/git-transport.js +146 -3
- package/lib/sync/merge.d.ts +47 -14
- package/lib/sync/merge.js +242 -222
- package/lib/sync/messages.d.ts +8 -0
- package/lib/sync/messages.js +19 -0
- package/lib/sync/risk.d.ts +19 -2
- package/lib/sync/risk.js +10 -3
- package/lib/sync/s3/messages.d.ts +6 -0
- package/lib/sync/s3/messages.js +13 -0
- package/lib/sync/s3/s3-transport.d.ts +9 -1
- package/lib/sync/s3/s3-transport.js +78 -6
- package/lib/sync/s3/sigv4.js +11 -2
- package/lib/sync/sync-engine.d.ts +212 -8
- package/lib/sync/sync-engine.js +514 -58
- package/lib/sync/sync-history.d.ts +11 -0
- package/lib/sync/sync-history.js +20 -0
- package/lib/sync/sync-state.d.ts +43 -0
- package/lib/sync/sync-state.js +44 -0
- package/lib/sync/sync-watcher.d.ts +76 -0
- package/lib/sync/sync-watcher.js +202 -0
- package/lib/sync/text-merge.d.ts +85 -0
- package/lib/sync/text-merge.js +513 -0
- package/lib/sync/transport.d.ts +48 -0
- package/lib/sync/transport.js +12 -0
- package/lib/sync/watch-scope.d.ts +38 -0
- package/lib/sync/watch-scope.js +95 -0
- package/lib/sync/webdav/webdav-transport.d.ts +16 -2
- package/lib/sync/webdav/webdav-transport.js +128 -11
- package/lib/ui/foreign-view.js +10 -0
- package/lib/ui/i18n.d.ts +9 -0
- package/lib/ui/i18n.js +22 -0
- package/lib/ui/rollback-view.d.ts +12 -0
- package/lib/ui/rollback-view.js +2 -0
- package/lib/ui/sync-settings-view.d.ts +47 -0
- package/lib/ui/sync-settings-view.js +24 -0
- package/lib/ui/types.d.ts +21 -0
- package/lib/utils/env-lock.d.ts +1 -0
- package/lib/utils/env-lock.js +7 -4
- package/package.json +6 -6
- package/src/adapters/credentials.ts +22 -4
- package/src/adapters/file-collection.ts +2 -1
- package/src/adapters/mcp.test.ts +235 -1
- package/src/adapters/mcp.ts +276 -5
- package/src/adapters/messages.ts +7 -0
- package/src/adapters/plugin-files.ts +2 -1
- package/src/adapters/plugins.test.ts +176 -2
- package/src/adapters/plugins.ts +107 -21
- package/src/adapters/pnpm-workspace.test.ts +89 -1
- package/src/adapters/pnpm-workspace.ts +127 -2
- package/src/adapters/prompts.ts +20 -3
- package/src/adapters/providers.ts +62 -4
- package/src/adapters/settings.ts +16 -3
- package/src/adapters/skills.test.ts +141 -0
- package/src/adapters/skills.ts +58 -2
- package/src/adapters/ui.ts +16 -3
- package/src/adapters/units-entries.test.ts +190 -0
- package/src/adapters/units.ts +50 -0
- package/src/adapters/workspaces.ts +2 -1
- package/src/client/about/PluginUpdateDialog.tsx +4 -2
- package/src/client/about/ReleaseNotesDialog.tsx +4 -2
- package/src/client/chat/RollbackReviewDialog.tsx +3 -2
- package/src/client/common/Skeleton.tsx +32 -0
- package/src/client/common/api-base.test.ts +36 -0
- package/src/client/common/http.test.ts +12 -1
- package/src/client/common/http.ts +8 -2
- package/src/client/common/routes.ts +46 -3
- package/src/client/config-manager.module.css +321 -11
- package/src/client/home/HomePanel.tsx +14 -6
- package/src/client/import/ForeignImportView.tsx +3 -1
- package/src/client/import/import-wizard-steps.tsx +2 -1
- package/src/client/library/ArtifactRow.tsx +12 -2
- package/src/client/library/LibraryActions.tsx +6 -3
- package/src/client/library/LibraryPanel.tsx +21 -3
- package/src/client/library/RestorePlanView.tsx +4 -2
- package/src/client/locales.ts +3 -0
- package/src/client/market/market-api.test.ts +3 -2
- package/src/client/recovery/RecoveryPanel.tsx +54 -7
- package/src/client/recovery/RollbackCard.tsx +7 -2
- package/src/client/recovery/RollbackDiffPane.tsx +3 -2
- package/src/client/recovery/recovery-locales.ts +20 -2
- package/src/client/recovery/recovery-view.test.ts +39 -1
- package/src/client/recovery/recovery-view.ts +23 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/run-store.ts +4 -0
- package/src/client/sync/AutosyncCard.tsx +49 -6
- package/src/client/sync/MergeModal.tsx +220 -0
- package/src/client/sync/MergePreviewView.tsx +306 -0
- package/src/client/sync/SyncActivityBar.tsx +31 -0
- package/src/client/sync/SyncConfirmView.tsx +2 -1
- package/src/client/sync/SyncPanel.tsx +130 -87
- package/src/client/sync/SyncProbeRow.tsx +162 -0
- package/src/client/sync/SyncSettingsView.test.ts +27 -2
- package/src/client/sync/WorkDirCard.tsx +44 -6
- package/src/client/sync/history-model.ts +4 -1
- package/src/client/sync/merge-view.test.ts +248 -0
- package/src/client/sync/merge-view.ts +398 -0
- package/src/client/sync/probe-view.test.ts +90 -0
- package/src/client/sync/probe-view.ts +63 -0
- package/src/client/sync/remote-device-view.test.ts +35 -0
- package/src/client/sync/remote-device-view.ts +25 -0
- package/src/client/sync/sync-api.test.ts +5 -3
- package/src/client/sync/sync-api.ts +212 -2
- package/src/client/sync/sync-entry.test.ts +78 -0
- package/src/client/sync/sync-entry.ts +53 -0
- package/src/client/sync/sync-locales.ts +234 -2
- package/src/client/sync/sync-view.test.ts +18 -1
- package/src/client/sync/sync-view.ts +20 -0
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +20 -0
- package/src/core/mcp-connector-store.test.ts +265 -0
- package/src/core/mcp-connector-store.ts +516 -0
- package/src/core/messages-core.ts +4 -0
- package/src/core/messages.ts +46 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +30 -4
- package/src/core/recovery-orchestrator.test.ts +104 -5
- package/src/core/recovery-orchestrator.ts +74 -17
- package/src/core/rollback-activity.test.ts +311 -0
- package/src/core/rollback-activity.ts +367 -0
- package/src/core/rollback-cross-session.test.ts +694 -0
- package/src/core/rollback-interrupted.test.ts +137 -0
- package/src/core/rollback-ledger.ts +32 -0
- package/src/core/rollback-merge.test.ts +130 -0
- package/src/core/rollback-merge.ts +78 -14
- package/src/core/rollback-point.ts +468 -91
- package/src/core/rollback-undo-journal.test.ts +508 -0
- package/src/core/rollback-undo-journal.ts +571 -0
- package/src/core/self-update.ts +2 -5
- package/src/core/text-fidelity.test.ts +189 -0
- package/src/core/text-fidelity.ts +197 -0
- package/src/core/types.ts +11 -3
- package/src/core/update-check.ts +2 -5
- package/src/core/validator.ts +14 -0
- package/src/foreign/bundle-origin.test.ts +265 -0
- package/src/foreign/bundle.ts +45 -5
- package/src/foreign/claude-code.test.ts +268 -0
- package/src/foreign/claude-code.ts +406 -25
- package/src/foreign/claude-plugins.test.ts +234 -0
- package/src/foreign/claude-sessions.test.ts +141 -0
- package/src/foreign/claude-sessions.ts +179 -23
- package/src/foreign/file-budget.test.ts +4 -1
- package/src/foreign/read-claude-code.test.ts +320 -0
- package/src/foreign/read-claude-code.ts +490 -71
- package/src/foreign/read-claude-plugins.ts +465 -0
- package/src/foreign/registry.ts +25 -2
- package/src/foreign/session-ir.test.ts +65 -0
- package/src/foreign/truth-table.test.ts +81 -0
- package/src/foreign/truth-table.ts +6 -3
- package/src/foreign/types.ts +162 -2
- package/src/index.facade.test.ts +26 -4
- package/src/index.ts +76 -19
- package/src/market/git-file-writer.test.ts +4 -0
- package/src/market/git-file-writer.ts +7 -1
- package/src/profiles/dsh-profile-launcher.test.ts +68 -3
- package/src/profiles/dsh-profile-launcher.ts +56 -1
- package/src/profiles/dsh-profile-shared.ts +8 -2
- package/src/routes/foreign.ts +8 -1
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/sync.ts +176 -9
- package/src/schema/manifest-schema.test.ts +87 -2
- package/src/schema/manifest.ts +57 -1
- package/src/schema/types.ts +45 -2
- package/src/sync/ancestor.ts +76 -2
- package/src/sync/autosync-config.test.ts +73 -1
- package/src/sync/autosync-config.ts +124 -0
- package/src/sync/autosync-risk-tier.test.ts +38 -2
- package/src/sync/autosync-scheduler.test.ts +264 -12
- package/src/sync/autosync-scheduler.ts +296 -44
- package/src/sync/gist/gist-conditional-write.test.ts +203 -0
- package/src/sync/gist/gist-transport.ts +53 -5
- package/src/sync/gist/messages.ts +5 -0
- package/src/sync/git/git-probe.test.ts +80 -0
- package/src/sync/git/git-transport.test.ts +185 -0
- package/src/sync/git/git-transport.ts +178 -5
- package/src/sync/merge.test.ts +188 -55
- package/src/sync/merge.ts +282 -224
- package/src/sync/messages.ts +19 -0
- package/src/sync/risk.ts +29 -5
- package/src/sync/s3/messages.ts +13 -0
- package/src/sync/s3/s3-conditional-write.test.ts +330 -0
- package/src/sync/s3/s3-transport.test.ts +2 -1
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/s3/sigv4.test.ts +4 -1
- package/src/sync/s3/sigv4.ts +11 -2
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +410 -19
- package/src/sync/sync-engine.ts +602 -57
- package/src/sync/sync-history.test.ts +16 -1
- package/src/sync/sync-history.ts +26 -0
- package/src/sync/sync-state.test.ts +64 -0
- package/src/sync/sync-state.ts +79 -0
- package/src/sync/sync-watcher.test.ts +197 -0
- package/src/sync/sync-watcher.ts +246 -0
- package/src/sync/text-merge.test.ts +358 -0
- package/src/sync/text-merge.ts +554 -0
- package/src/sync/transport.ts +57 -0
- package/src/sync/watch-scope.test.ts +122 -0
- package/src/sync/watch-scope.ts +120 -0
- package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
- package/src/sync/webdav/webdav-transport.ts +137 -14
- package/src/ui/foreign-view.test.ts +34 -0
- package/src/ui/foreign-view.ts +10 -0
- package/src/ui/i18n.ts +22 -0
- package/src/ui/rollback-view.ts +14 -0
- package/src/ui/sync-settings-view.test.ts +28 -1
- package/src/ui/sync-settings-view.ts +43 -0
- package/src/ui/types.ts +22 -0
- package/src/utils/env-lock.ts +8 -5
package/lib/core/backup.js
CHANGED
|
@@ -16,6 +16,7 @@ import { normalizePath } from '../utils/paths.js';
|
|
|
16
16
|
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
17
17
|
import { SECTION_IDS, isFileSection } from '../schema/config.js';
|
|
18
18
|
import { isPatchLayerRel, PROFILE_PATCH_FILENAME, USER_PATCH_FILE } from './patch-layers.js';
|
|
19
|
+
import { MCP_CONNECTOR_STORE_REL } from './mcp-connector-store.js';
|
|
19
20
|
/** 见 PlanItemExecContext 与上方说明:会写目标的项 = 必须进快照范围的项。 */
|
|
20
21
|
export function planItemWritesTarget(item, exec = {}) {
|
|
21
22
|
if (item.kind === 'Skip' || item.kind === 'Warning'
|
|
@@ -287,6 +288,25 @@ async function engineSnapshotEntry(ctx, target) {
|
|
|
287
288
|
copiedTo: `blobs/${crypto.randomUUID()}`,
|
|
288
289
|
};
|
|
289
290
|
}
|
|
291
|
+
// mcp 分区的第二个数据源(连接器插件的 storage domain 文件):整文件快照。
|
|
292
|
+
// 没有这条分支时,连接器导入的写入撤不回来(该文件不在 patch 层里,走 patchLine 语义会
|
|
293
|
+
// 变成「读过一行 patch」→ 回滚时不还原文件 → 用户的 4 个 MCP 连接被永久改掉)。
|
|
294
|
+
if (target.adapter === 'mcp' && target.ref === MCP_CONNECTOR_STORE_REL) {
|
|
295
|
+
const fsFacade = fsForSection(ctx, target.adapter);
|
|
296
|
+
const abs = resolveFileTarget(ctx, target.adapter, target.ref);
|
|
297
|
+
if (!(await fsFacade.exists(abs))) {
|
|
298
|
+
return { kind: 'file', adapter: target.adapter, ref: target.ref, before: null, existed: false };
|
|
299
|
+
}
|
|
300
|
+
const data = await fsFacade.readFile(abs);
|
|
301
|
+
return {
|
|
302
|
+
kind: 'file',
|
|
303
|
+
adapter: target.adapter,
|
|
304
|
+
ref: target.ref,
|
|
305
|
+
before: { contentHash: sha256Hex(data) },
|
|
306
|
+
existed: true,
|
|
307
|
+
copiedTo: `blobs/${crypto.randomUUID()}`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
290
310
|
// patchLine:从**该行所在的 patch 层**读取原行(issue #71)。
|
|
291
311
|
// target.file 缺省 = 用户层(旧调用方 / 旧计划项),只接受合法层路径,绝不把任意字符串
|
|
292
312
|
// 当文件名传给宿主门面(那会抛「仅支持管理 …」)。
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** 连接器存储文件($DSH_HOME 相对路径;也是快照/回滚的文件类 ref) */
|
|
2
|
+
export declare const MCP_CONNECTOR_STORE_REL = "storages/mcp_connector.json";
|
|
3
|
+
/** 需要用户重填的凭据占位符(与连接器插件同名同形,便于用户/文档互认) */
|
|
4
|
+
export declare const REDACTED_VALUE = "<REDACTED:REENTER>";
|
|
5
|
+
/** 本机专属值占位符(绝对命令、cwd、userinfo) */
|
|
6
|
+
export declare const REDACTED_LOCAL_VALUE = "<REDACTED:LOCAL_VALUE>";
|
|
7
|
+
/** 本项目导出的连接器块标识(与连接器插件自己的 dsh-mcp-connector.redacted 区分:我们的还带 key/connectorId) */
|
|
8
|
+
export declare const MCP_CONNECTOR_EXPORT_FORMAT = "dsh-config-manager.mcp-connector";
|
|
9
|
+
export declare const MCP_CONNECTOR_EXPORT_VERSION = 1;
|
|
10
|
+
/** storage domain 里的一条连接记录(只保留本模块用到的字段,其余字段原样透传) */
|
|
11
|
+
export interface ConnectorRecord {
|
|
12
|
+
key: string;
|
|
13
|
+
connectorId: string;
|
|
14
|
+
kind: 'oauth' | 'manual' | 'json';
|
|
15
|
+
name: string;
|
|
16
|
+
serverKey?: string;
|
|
17
|
+
serverName: string;
|
|
18
|
+
transport: 'streamable-http' | 'sse' | 'stdio';
|
|
19
|
+
command?: string;
|
|
20
|
+
args?: string[];
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
allowInsecurePrivateNetwork?: boolean;
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
auth?: {
|
|
27
|
+
mode: 'oauth' | 'bearer' | 'api-key';
|
|
28
|
+
bearerToken?: string;
|
|
29
|
+
apiKeyHeader?: string;
|
|
30
|
+
apiKeyValue?: string;
|
|
31
|
+
grantKey?: string;
|
|
32
|
+
};
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
injectionMode?: 'always' | 'session';
|
|
35
|
+
createdAt?: number;
|
|
36
|
+
updatedAt?: number;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
/** 导出的连接(脱敏后;redactedFields 列出被替换的字段,导入侧据此判「需重填」) */
|
|
40
|
+
export interface ConnectorExportEntry {
|
|
41
|
+
key: string;
|
|
42
|
+
connectorId: string;
|
|
43
|
+
kind: 'oauth' | 'manual' | 'json';
|
|
44
|
+
name: string;
|
|
45
|
+
serverKey?: string;
|
|
46
|
+
serverName: string;
|
|
47
|
+
transport: 'streamable-http' | 'sse' | 'stdio';
|
|
48
|
+
command?: string;
|
|
49
|
+
args?: string[];
|
|
50
|
+
env?: Record<string, string>;
|
|
51
|
+
cwd?: string;
|
|
52
|
+
url?: string;
|
|
53
|
+
allowInsecurePrivateNetwork?: boolean;
|
|
54
|
+
headers?: Record<string, string>;
|
|
55
|
+
authMode?: 'oauth' | 'bearer' | 'api-key' | 'none';
|
|
56
|
+
bearerToken?: string;
|
|
57
|
+
apiKeyHeader?: string;
|
|
58
|
+
apiKeyValue?: string;
|
|
59
|
+
injectionMode?: 'always' | 'session';
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
redactedFields?: string[];
|
|
62
|
+
}
|
|
63
|
+
/** OAuth 连接只导出「引用」:Token 与动态客户端密钥永不进备份,导入侧提示从市场重新授权 */
|
|
64
|
+
export interface ConnectorOauthRef {
|
|
65
|
+
key: string;
|
|
66
|
+
connectorId: string;
|
|
67
|
+
serverKey?: string;
|
|
68
|
+
name: string;
|
|
69
|
+
serverName: string;
|
|
70
|
+
enabled: boolean;
|
|
71
|
+
injectionMode?: 'always' | 'session';
|
|
72
|
+
action: 'reconnect-oauth';
|
|
73
|
+
}
|
|
74
|
+
/** mcp 分区里连接器来源的整块载荷 */
|
|
75
|
+
export interface McpConnectorExport {
|
|
76
|
+
format: typeof MCP_CONNECTOR_EXPORT_FORMAT;
|
|
77
|
+
version: typeof MCP_CONNECTOR_EXPORT_VERSION;
|
|
78
|
+
exportedAt: string;
|
|
79
|
+
connections: ConnectorExportEntry[];
|
|
80
|
+
oauthConnections: ConnectorOauthRef[];
|
|
81
|
+
}
|
|
82
|
+
/** stdio 参数:只脱掉看起来是密钥的参数(含 `--token=xxx` 与「flag 后紧跟值」两种形态),其余保留 */
|
|
83
|
+
export declare function redactArgs(args: readonly string[] | undefined): {
|
|
84
|
+
args?: string[];
|
|
85
|
+
redacted: boolean;
|
|
86
|
+
};
|
|
87
|
+
/** 命令:裸命令名(npx / uvx / node)保留;带路径的(绝对路径、盘符、相对目录)替换为本机值占位符 */
|
|
88
|
+
export declare function redactCommand(command: string | undefined): {
|
|
89
|
+
command?: string;
|
|
90
|
+
redacted: boolean;
|
|
91
|
+
};
|
|
92
|
+
/** URL:userinfo 整条替换;查询参数逐项脱敏(端点本体必须保住,否则备份无法还原) */
|
|
93
|
+
export declare function redactUrl(url: string | undefined): {
|
|
94
|
+
url?: string;
|
|
95
|
+
redacted: boolean;
|
|
96
|
+
};
|
|
97
|
+
/** 一条连接记录 → 可携带的脱敏条目(永不返回 Token / 密钥明文) */
|
|
98
|
+
export declare function exportConnectorRecord(record: ConnectorRecord): ConnectorExportEntry;
|
|
99
|
+
/** 全部连接 → 导出块(OAuth 只留引用;按 key 排序,便于 diff 与稳定哈希) */
|
|
100
|
+
export declare function exportConnectorBlock(records: readonly ConnectorRecord[], exportedAt?: string): McpConnectorExport;
|
|
101
|
+
/** 列出「还有占位符」的字段路径(导入前判「需重填凭据」) */
|
|
102
|
+
export declare function unresolvedRedactions(value: unknown, path?: string, output?: string[]): string[];
|
|
103
|
+
/** 导出条目里「需要用户重填」的字段(占位符路径 + redactedFields 并集,供计划项 detail 用) */
|
|
104
|
+
export declare function refillFieldsOf(entry: ConnectorExportEntry): string[];
|
|
105
|
+
/** 连接器存储文件的解析结果 */
|
|
106
|
+
export interface ConnectorStoreRead {
|
|
107
|
+
/** 文件存在且解析成功 */
|
|
108
|
+
parsed: boolean;
|
|
109
|
+
/** 文件是否存在(ENOENT → false;其余错误 → true + 解析失败) */
|
|
110
|
+
exists: boolean;
|
|
111
|
+
connections: ConnectorRecord[];
|
|
112
|
+
/** 解析失败原因(读到了但形态不对 → 绝不当作空表覆盖) */
|
|
113
|
+
error?: string;
|
|
114
|
+
/** 原始 JSON(保留其它表,写回时只替换 connections) */
|
|
115
|
+
raw?: Record<string, unknown>;
|
|
116
|
+
}
|
|
117
|
+
/** 解析连接器存储文本(绝不对「读到了但解析不了」当空表) */
|
|
118
|
+
export declare function parseConnectorStore(text: string): ConnectorStoreRead;
|
|
119
|
+
/** 读连接器存储(ENOENT → 空表 + exists:false;其余错误原样上报) */
|
|
120
|
+
export declare function readConnectorStore(fs: {
|
|
121
|
+
readFile(rel: string): Promise<Uint8Array>;
|
|
122
|
+
exists(rel: string): Promise<boolean>;
|
|
123
|
+
}): Promise<ConnectorStoreRead>;
|
|
124
|
+
/** 连接身份:优先 key,其次 (connectorId, serverName) */
|
|
125
|
+
export declare function connectorIdentity(entry: {
|
|
126
|
+
key?: string;
|
|
127
|
+
connectorId?: string;
|
|
128
|
+
serverName: string;
|
|
129
|
+
}): string;
|
|
130
|
+
/**
|
|
131
|
+
* 在目标连接里找同一身份的记录:**只认 (connectorId, serverName)**。
|
|
132
|
+
*
|
|
133
|
+
* 为什么不先按 key 认:key 是存储主键,但「同 key 不同连接」在手工改过/旧版本里是可能的;
|
|
134
|
+
* 按 key 命中就当成同一条会**覆盖掉一条无关连接**。身份相同 → 复用既有 key(幂等);
|
|
135
|
+
* 身份不同 → 交给 pickConnectorKey 挑一个不冲突的新 key。
|
|
136
|
+
*/
|
|
137
|
+
export declare function findConnectorRecord(records: readonly ConnectorRecord[], entry: ConnectorExportEntry): ConnectorRecord | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* 比较用形态:**两侧都过一遍脱敏**再去掉易变字段。
|
|
140
|
+
*
|
|
141
|
+
* 为什么导出条目也要重新脱敏:条目里可能已被用户重填成真值(明文),而目标机记录里的
|
|
142
|
+
* 同名字段也是明文——但如果只脱一侧,明文与占位符一比就永远 Conflict(导入一次就再也 Skip 不了)。
|
|
143
|
+
* 两侧统一过 exportConnectorRecord ⇒ 明文都被换成同一个占位符,同一条连接天然相等。
|
|
144
|
+
*/
|
|
145
|
+
export declare function comparableConnector(record: ConnectorRecord | ConnectorExportEntry): Record<string, unknown>;
|
|
146
|
+
/** 把脱敏条目还原成可写入 storage domain 的记录(占位符必须为空才不会破坏目标机) */
|
|
147
|
+
export declare function buildConnectorRecord(entry: ConnectorExportEntry, options: {
|
|
148
|
+
existing?: ConnectorRecord;
|
|
149
|
+
key: string;
|
|
150
|
+
now: number;
|
|
151
|
+
}): ConnectorRecord;
|
|
152
|
+
/** 目标机里生成一个不与既有记录冲突的 key(同身份 → 复用既有 key) */
|
|
153
|
+
export declare function pickConnectorKey(records: readonly ConnectorRecord[], entry: ConnectorExportEntry): string;
|
|
154
|
+
/**
|
|
155
|
+
* 把一条(已脱敏、无占位符的)连接合并进存储 JSON 文本,返回新文本。
|
|
156
|
+
*
|
|
157
|
+
* 只替换 `tables.connections[`key`]`,其它表与未知字段逐字保留(grants/tool_catalog 等绝不重写)。
|
|
158
|
+
*/
|
|
159
|
+
export declare function mergeConnectorIntoStoreText(read: ConnectorStoreRead, record: ConnectorRecord): string;
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 连接器存储(`storages/mcp_connector.json`)的读写与脱敏(mcp 分区的第二个数据源)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:DSH 的 MCP 有两条完全不同的落地路径 ——
|
|
5
|
+
* ① 组合 patch 里的 `dsh-mcp-client` 行(官方 `dsh-mcp-client` 配置面,mcp 分区一直只认它);
|
|
6
|
+
* ② `dsh-mcp-connector` 插件的连接器(用户实际用的是这条,存在 storage domain
|
|
7
|
+
* `mcp_connector` 的 `connections` 表里,落盘 = `$DSH_HOME/storages/mcp_connector.json`)。
|
|
8
|
+
* 只认 ① 的结果是:用户配了 4 个 MCP 服务器,导出选择器里 MCP 分区恒为「0 个 / 26 B」。
|
|
9
|
+
*
|
|
10
|
+
* 安全边界(与连接器插件 docs/CONFIG-BACKUP.md 同精神,但按「备份要能还原」调整粒度):
|
|
11
|
+
* - **绝不导出** grants / tool_catalog / snapshots 等其它表(含 OAuth Token、动态客户端密钥);
|
|
12
|
+
* - Bearer Token / API Key 值、名字像凭据的 Header/env 值、命中强 secret 形状的值 → `<REDACTED:REENTER>`;
|
|
13
|
+
* - 绝对命令与本地 cwd → `<REDACTED:LOCAL_VALUE>`;
|
|
14
|
+
* - URL 只脱掉 userinfo 与「名字/值像凭据」的查询参数,端点本身保留(整条替换会让备份无法还原);
|
|
15
|
+
* - 非敏感值原样保留(`Accept: application/json`、`-y`、包名这类),否则备份回来的连接全都不能用。
|
|
16
|
+
*
|
|
17
|
+
* 导入纪律:只要还有占位符,就**不写**这条连接(写进去等于给用户一个起不来的 Server),
|
|
18
|
+
* 而是产出「需重填凭据」计划项。
|
|
19
|
+
*/
|
|
20
|
+
import { matchSecretValuePattern } from '../security/secret-scanner.js';
|
|
21
|
+
/** 连接器存储文件($DSH_HOME 相对路径;也是快照/回滚的文件类 ref) */
|
|
22
|
+
export const MCP_CONNECTOR_STORE_REL = 'storages/mcp_connector.json';
|
|
23
|
+
/** 需要用户重填的凭据占位符(与连接器插件同名同形,便于用户/文档互认) */
|
|
24
|
+
export const REDACTED_VALUE = '<REDACTED:REENTER>';
|
|
25
|
+
/** 本机专属值占位符(绝对命令、cwd、userinfo) */
|
|
26
|
+
export const REDACTED_LOCAL_VALUE = '<REDACTED:LOCAL_VALUE>';
|
|
27
|
+
/** 本项目导出的连接器块标识(与连接器插件自己的 dsh-mcp-connector.redacted 区分:我们的还带 key/connectorId) */
|
|
28
|
+
export const MCP_CONNECTOR_EXPORT_FORMAT = 'dsh-config-manager.mcp-connector';
|
|
29
|
+
export const MCP_CONNECTOR_EXPORT_VERSION = 1;
|
|
30
|
+
/** 名字像凭据的键(Header / env / 查询参数 / CLI flag)——命中即脱敏,不看值形状 */
|
|
31
|
+
const SECRETISH_NAME = /(authorization|auth-token|token|api[-_]?key|apikey|secret|password|passwd|pwd|cookie|credential|private[-_]?key|access[-_]?key|session[-_]?key|bearer|signature|sig)/i;
|
|
32
|
+
function isSecretishName(name) {
|
|
33
|
+
return SECRETISH_NAME.test(name);
|
|
34
|
+
}
|
|
35
|
+
function isPlaceholder(value) {
|
|
36
|
+
return value === REDACTED_VALUE || value === REDACTED_LOCAL_VALUE;
|
|
37
|
+
}
|
|
38
|
+
/** 单个值是否需要脱敏(名字启发 + 强形状命中) */
|
|
39
|
+
function needsRedaction(value, name) {
|
|
40
|
+
if (value === '')
|
|
41
|
+
return false;
|
|
42
|
+
if (name !== undefined && isSecretishName(name))
|
|
43
|
+
return true;
|
|
44
|
+
return matchSecretValuePattern(value) !== null;
|
|
45
|
+
}
|
|
46
|
+
/** Header / env 一类的键值表:保留键名(用户重填时要知道填哪个),逐个决定值 */
|
|
47
|
+
function redactValueMap(input) {
|
|
48
|
+
if (input === undefined)
|
|
49
|
+
return { redacted: false };
|
|
50
|
+
const keys = Object.keys(input).sort();
|
|
51
|
+
if (keys.length === 0)
|
|
52
|
+
return { map: {}, redacted: false };
|
|
53
|
+
let redacted = false;
|
|
54
|
+
const map = {};
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const raw = input[key];
|
|
57
|
+
const value = typeof raw === 'string' ? raw : String(raw ?? '');
|
|
58
|
+
if (needsRedaction(value, key)) {
|
|
59
|
+
map[key] = REDACTED_VALUE;
|
|
60
|
+
redacted = true;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
map[key] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { map, redacted };
|
|
67
|
+
}
|
|
68
|
+
/** stdio 参数:只脱掉看起来是密钥的参数(含 `--token=xxx` 与「flag 后紧跟值」两种形态),其余保留 */
|
|
69
|
+
export function redactArgs(args) {
|
|
70
|
+
if (args === undefined)
|
|
71
|
+
return { redacted: false };
|
|
72
|
+
let redacted = false;
|
|
73
|
+
const out = [];
|
|
74
|
+
let prevSecretFlag = false;
|
|
75
|
+
for (const raw of args) {
|
|
76
|
+
const arg = typeof raw === 'string' ? raw : String(raw ?? '');
|
|
77
|
+
const eq = arg.indexOf('=');
|
|
78
|
+
if (arg.startsWith('-') && eq > 0) {
|
|
79
|
+
const flag = arg.slice(0, eq);
|
|
80
|
+
const value = arg.slice(eq + 1);
|
|
81
|
+
if (isSecretishName(flag) || needsRedaction(value)) {
|
|
82
|
+
out.push(`${flag}=${REDACTED_VALUE}`);
|
|
83
|
+
redacted = true;
|
|
84
|
+
prevSecretFlag = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
prevSecretFlag = isSecretishName(flag);
|
|
88
|
+
out.push(arg);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (prevSecretFlag || needsRedaction(arg)) {
|
|
92
|
+
out.push(REDACTED_VALUE);
|
|
93
|
+
redacted = true;
|
|
94
|
+
prevSecretFlag = false;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
prevSecretFlag = arg.startsWith('-') && isSecretishName(arg);
|
|
98
|
+
out.push(arg);
|
|
99
|
+
}
|
|
100
|
+
return { args: out, redacted };
|
|
101
|
+
}
|
|
102
|
+
/** 命令:裸命令名(npx / uvx / node)保留;带路径的(绝对路径、盘符、相对目录)替换为本机值占位符 */
|
|
103
|
+
export function redactCommand(command) {
|
|
104
|
+
if (command === undefined || command === '')
|
|
105
|
+
return { redacted: false };
|
|
106
|
+
if (/^[A-Za-z0-9_.@+-]+$/.test(command))
|
|
107
|
+
return { command, redacted: false };
|
|
108
|
+
return { command: REDACTED_LOCAL_VALUE, redacted: true };
|
|
109
|
+
}
|
|
110
|
+
/** URL:userinfo 整条替换;查询参数逐项脱敏(端点本体必须保住,否则备份无法还原) */
|
|
111
|
+
export function redactUrl(url) {
|
|
112
|
+
if (url === undefined || url === '')
|
|
113
|
+
return { redacted: false };
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = new URL(url);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { url: REDACTED_VALUE, redacted: true };
|
|
120
|
+
}
|
|
121
|
+
if (parsed.username !== '' || parsed.password !== '')
|
|
122
|
+
return { url: REDACTED_VALUE, redacted: true };
|
|
123
|
+
let redacted = false;
|
|
124
|
+
if (parsed.search !== '') {
|
|
125
|
+
const params = [...parsed.searchParams.entries()];
|
|
126
|
+
parsed.search = '';
|
|
127
|
+
for (const [name, value] of params) {
|
|
128
|
+
if (needsRedaction(value, name)) {
|
|
129
|
+
parsed.searchParams.append(name, REDACTED_VALUE);
|
|
130
|
+
redacted = true;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
parsed.searchParams.append(name, value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (parsed.hash !== '' && needsRedaction(parsed.hash.replace(/^#/, ''))) {
|
|
138
|
+
parsed.hash = '';
|
|
139
|
+
return { url: parsed.toString(), redacted: true };
|
|
140
|
+
}
|
|
141
|
+
return { url: redacted ? parsed.toString() : url, redacted };
|
|
142
|
+
}
|
|
143
|
+
/** 一条连接记录 → 可携带的脱敏条目(永不返回 Token / 密钥明文) */
|
|
144
|
+
export function exportConnectorRecord(record) {
|
|
145
|
+
const redactedFields = [];
|
|
146
|
+
const entry = {
|
|
147
|
+
key: record.key,
|
|
148
|
+
connectorId: record.connectorId,
|
|
149
|
+
kind: record.kind,
|
|
150
|
+
name: record.name,
|
|
151
|
+
serverName: record.serverName,
|
|
152
|
+
transport: record.transport,
|
|
153
|
+
enabled: record.enabled !== false,
|
|
154
|
+
};
|
|
155
|
+
if (record.serverKey !== undefined)
|
|
156
|
+
entry.serverKey = record.serverKey;
|
|
157
|
+
if (record.injectionMode !== undefined)
|
|
158
|
+
entry.injectionMode = record.injectionMode;
|
|
159
|
+
if (record.transport === 'stdio') {
|
|
160
|
+
const command = redactCommand(record.command);
|
|
161
|
+
if (command.command !== undefined)
|
|
162
|
+
entry.command = command.command;
|
|
163
|
+
if (command.redacted)
|
|
164
|
+
redactedFields.push('command');
|
|
165
|
+
const args = redactArgs(record.args);
|
|
166
|
+
if (args.args !== undefined)
|
|
167
|
+
entry.args = args.args;
|
|
168
|
+
if (args.redacted)
|
|
169
|
+
redactedFields.push('args');
|
|
170
|
+
const env = redactValueMap(record.env);
|
|
171
|
+
if (env.map !== undefined)
|
|
172
|
+
entry.env = env.map;
|
|
173
|
+
if (env.redacted)
|
|
174
|
+
redactedFields.push('env');
|
|
175
|
+
if (record.cwd !== undefined && record.cwd !== '') {
|
|
176
|
+
entry.cwd = REDACTED_LOCAL_VALUE;
|
|
177
|
+
redactedFields.push('cwd');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const url = redactUrl(record.url);
|
|
182
|
+
if (url.url !== undefined)
|
|
183
|
+
entry.url = url.url;
|
|
184
|
+
if (url.redacted)
|
|
185
|
+
redactedFields.push('url');
|
|
186
|
+
if (record.allowInsecurePrivateNetwork === true)
|
|
187
|
+
entry.allowInsecurePrivateNetwork = true;
|
|
188
|
+
const headers = redactValueMap(record.headers);
|
|
189
|
+
if (headers.map !== undefined)
|
|
190
|
+
entry.headers = headers.map;
|
|
191
|
+
if (headers.redacted)
|
|
192
|
+
redactedFields.push('headers');
|
|
193
|
+
}
|
|
194
|
+
const mode = record.auth?.mode;
|
|
195
|
+
if (mode === 'bearer') {
|
|
196
|
+
entry.authMode = 'bearer';
|
|
197
|
+
entry.bearerToken = REDACTED_VALUE;
|
|
198
|
+
redactedFields.push('bearerToken');
|
|
199
|
+
}
|
|
200
|
+
else if (mode === 'api-key') {
|
|
201
|
+
entry.authMode = 'api-key';
|
|
202
|
+
entry.apiKeyHeader = record.auth?.apiKeyHeader ?? 'X-Api-Key';
|
|
203
|
+
entry.apiKeyValue = REDACTED_VALUE;
|
|
204
|
+
redactedFields.push('apiKeyValue');
|
|
205
|
+
}
|
|
206
|
+
else if (mode === 'oauth') {
|
|
207
|
+
entry.authMode = 'oauth';
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
entry.authMode = 'none';
|
|
211
|
+
}
|
|
212
|
+
if (redactedFields.length > 0)
|
|
213
|
+
entry.redactedFields = redactedFields;
|
|
214
|
+
return entry;
|
|
215
|
+
}
|
|
216
|
+
/** 全部连接 → 导出块(OAuth 只留引用;按 key 排序,便于 diff 与稳定哈希) */
|
|
217
|
+
export function exportConnectorBlock(records, exportedAt = new Date().toISOString()) {
|
|
218
|
+
const sorted = [...records].sort((a, b) => a.key.localeCompare(b.key));
|
|
219
|
+
return {
|
|
220
|
+
format: MCP_CONNECTOR_EXPORT_FORMAT,
|
|
221
|
+
version: MCP_CONNECTOR_EXPORT_VERSION,
|
|
222
|
+
exportedAt,
|
|
223
|
+
connections: sorted.filter((r) => r.auth?.mode !== 'oauth').map((r) => exportConnectorRecord(r)),
|
|
224
|
+
oauthConnections: sorted
|
|
225
|
+
.filter((r) => r.auth?.mode === 'oauth')
|
|
226
|
+
.map((r) => ({
|
|
227
|
+
key: r.key,
|
|
228
|
+
connectorId: r.connectorId,
|
|
229
|
+
...(r.serverKey !== undefined ? { serverKey: r.serverKey } : {}),
|
|
230
|
+
name: r.name,
|
|
231
|
+
serverName: r.serverName,
|
|
232
|
+
enabled: r.enabled !== false,
|
|
233
|
+
...(r.injectionMode !== undefined ? { injectionMode: r.injectionMode } : {}),
|
|
234
|
+
action: 'reconnect-oauth',
|
|
235
|
+
})),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/** 列出「还有占位符」的字段路径(导入前判「需重填凭据」) */
|
|
239
|
+
export function unresolvedRedactions(value, path = '$', output = []) {
|
|
240
|
+
if (typeof value === 'string') {
|
|
241
|
+
if (isPlaceholder(value))
|
|
242
|
+
output.push(path);
|
|
243
|
+
return output;
|
|
244
|
+
}
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
value.forEach((item, index) => unresolvedRedactions(item, `${path}[${index}]`, output));
|
|
247
|
+
return output;
|
|
248
|
+
}
|
|
249
|
+
if (value !== null && typeof value === 'object') {
|
|
250
|
+
for (const [key, item] of Object.entries(value)) {
|
|
251
|
+
unresolvedRedactions(item, `${path}.${key}`, output);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return output;
|
|
255
|
+
}
|
|
256
|
+
/** 导出条目里「需要用户重填」的字段(占位符路径 + redactedFields 并集,供计划项 detail 用) */
|
|
257
|
+
export function refillFieldsOf(entry) {
|
|
258
|
+
const fields = new Set(entry.redactedFields ?? []);
|
|
259
|
+
for (const p of unresolvedRedactions(entry)) {
|
|
260
|
+
const trimmed = p.startsWith('$.') ? p.slice(2) : p;
|
|
261
|
+
if (trimmed !== '')
|
|
262
|
+
fields.add(trimmed);
|
|
263
|
+
}
|
|
264
|
+
return [...fields].sort();
|
|
265
|
+
}
|
|
266
|
+
function asRecordArray(value) {
|
|
267
|
+
if (value === null || typeof value !== 'object')
|
|
268
|
+
return [];
|
|
269
|
+
const out = [];
|
|
270
|
+
for (const [key, item] of Object.entries(value)) {
|
|
271
|
+
if (item === null || typeof item !== 'object')
|
|
272
|
+
continue;
|
|
273
|
+
const rec = item;
|
|
274
|
+
const serverName = typeof rec['serverName'] === 'string' ? rec['serverName'] : '';
|
|
275
|
+
if (serverName === '')
|
|
276
|
+
continue;
|
|
277
|
+
out.push({
|
|
278
|
+
...rec,
|
|
279
|
+
key: typeof rec['key'] === 'string' && rec['key'] !== '' ? rec['key'] : key,
|
|
280
|
+
serverName,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
/** 解析连接器存储文本(绝不对「读到了但解析不了」当空表) */
|
|
286
|
+
export function parseConnectorStore(text) {
|
|
287
|
+
let raw;
|
|
288
|
+
try {
|
|
289
|
+
raw = JSON.parse(text);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
return { parsed: false, exists: true, connections: [], error: err instanceof Error ? err.message : String(err) };
|
|
293
|
+
}
|
|
294
|
+
if (raw === null || typeof raw !== 'object') {
|
|
295
|
+
return { parsed: false, exists: true, connections: [], error: 'root is not an object' };
|
|
296
|
+
}
|
|
297
|
+
const root = raw;
|
|
298
|
+
const tables = root['tables'];
|
|
299
|
+
if (tables === null || typeof tables !== 'object') {
|
|
300
|
+
return { parsed: false, exists: true, connections: [], error: 'tables is not an object' };
|
|
301
|
+
}
|
|
302
|
+
const connections = asRecordArray(tables['connections']);
|
|
303
|
+
return { parsed: true, exists: true, connections, raw: root };
|
|
304
|
+
}
|
|
305
|
+
/** 读连接器存储(ENOENT → 空表 + exists:false;其余错误原样上报) */
|
|
306
|
+
export async function readConnectorStore(fs) {
|
|
307
|
+
if (!(await fs.exists(MCP_CONNECTOR_STORE_REL))) {
|
|
308
|
+
// 没装连接器是正常形态:空表但**可写**(导入时要在目标机新建这个文件)
|
|
309
|
+
return { parsed: true, exists: false, connections: [] };
|
|
310
|
+
}
|
|
311
|
+
const bytes = await fs.readFile(MCP_CONNECTOR_STORE_REL);
|
|
312
|
+
const text = new TextDecoder().decode(bytes);
|
|
313
|
+
return parseConnectorStore(text);
|
|
314
|
+
}
|
|
315
|
+
/** 连接身份:优先 key,其次 (connectorId, serverName) */
|
|
316
|
+
export function connectorIdentity(entry) {
|
|
317
|
+
if (typeof entry.key === 'string' && entry.key !== '')
|
|
318
|
+
return `key:${entry.key}`;
|
|
319
|
+
return `name:${entry.connectorId ?? ''}/${entry.serverName}`;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* 在目标连接里找同一身份的记录:**只认 (connectorId, serverName)**。
|
|
323
|
+
*
|
|
324
|
+
* 为什么不先按 key 认:key 是存储主键,但「同 key 不同连接」在手工改过/旧版本里是可能的;
|
|
325
|
+
* 按 key 命中就当成同一条会**覆盖掉一条无关连接**。身份相同 → 复用既有 key(幂等);
|
|
326
|
+
* 身份不同 → 交给 pickConnectorKey 挑一个不冲突的新 key。
|
|
327
|
+
*/
|
|
328
|
+
export function findConnectorRecord(records, entry) {
|
|
329
|
+
return records.find((r) => r.connectorId === entry.connectorId && r.serverName === entry.serverName);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 比较用形态:**两侧都过一遍脱敏**再去掉易变字段。
|
|
333
|
+
*
|
|
334
|
+
* 为什么导出条目也要重新脱敏:条目里可能已被用户重填成真值(明文),而目标机记录里的
|
|
335
|
+
* 同名字段也是明文——但如果只脱一侧,明文与占位符一比就永远 Conflict(导入一次就再也 Skip 不了)。
|
|
336
|
+
* 两侧统一过 exportConnectorRecord ⇒ 明文都被换成同一个占位符,同一条连接天然相等。
|
|
337
|
+
*/
|
|
338
|
+
export function comparableConnector(record) {
|
|
339
|
+
const isEntry = record.authMode !== undefined;
|
|
340
|
+
const asRecord = isEntry
|
|
341
|
+
? buildConnectorRecord(record, { key: record.key, now: 0 })
|
|
342
|
+
: record;
|
|
343
|
+
const { key: _key, redactedFields: _rf, ...rest } = exportConnectorRecord(asRecord);
|
|
344
|
+
return rest;
|
|
345
|
+
}
|
|
346
|
+
/** 把脱敏条目还原成可写入 storage domain 的记录(占位符必须为空才不会破坏目标机) */
|
|
347
|
+
export function buildConnectorRecord(entry, options) {
|
|
348
|
+
const { existing, key, now } = options;
|
|
349
|
+
const record = {
|
|
350
|
+
...(existing ?? {}),
|
|
351
|
+
key,
|
|
352
|
+
connectorId: entry.connectorId,
|
|
353
|
+
kind: entry.kind,
|
|
354
|
+
name: entry.name,
|
|
355
|
+
serverName: entry.serverName,
|
|
356
|
+
transport: entry.transport,
|
|
357
|
+
enabled: entry.enabled !== false,
|
|
358
|
+
createdAt: existing?.createdAt ?? now,
|
|
359
|
+
updatedAt: now,
|
|
360
|
+
};
|
|
361
|
+
if (entry.serverKey !== undefined)
|
|
362
|
+
record.serverKey = entry.serverKey;
|
|
363
|
+
if (entry.injectionMode !== undefined)
|
|
364
|
+
record.injectionMode = entry.injectionMode;
|
|
365
|
+
if (entry.transport === 'stdio') {
|
|
366
|
+
if (entry.command !== undefined)
|
|
367
|
+
record.command = entry.command;
|
|
368
|
+
if (entry.args !== undefined)
|
|
369
|
+
record.args = [...entry.args];
|
|
370
|
+
if (entry.env !== undefined)
|
|
371
|
+
record.env = { ...entry.env };
|
|
372
|
+
if (entry.cwd !== undefined)
|
|
373
|
+
record.cwd = entry.cwd;
|
|
374
|
+
delete record.url;
|
|
375
|
+
delete record.headers;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
if (entry.url !== undefined)
|
|
379
|
+
record.url = entry.url;
|
|
380
|
+
if (entry.headers !== undefined)
|
|
381
|
+
record.headers = { ...entry.headers };
|
|
382
|
+
if (entry.allowInsecurePrivateNetwork === true)
|
|
383
|
+
record.allowInsecurePrivateNetwork = true;
|
|
384
|
+
else
|
|
385
|
+
delete record.allowInsecurePrivateNetwork;
|
|
386
|
+
delete record.command;
|
|
387
|
+
delete record.args;
|
|
388
|
+
delete record.env;
|
|
389
|
+
delete record.cwd;
|
|
390
|
+
}
|
|
391
|
+
if (entry.authMode === 'bearer') {
|
|
392
|
+
record.auth = { mode: 'bearer', ...(entry.bearerToken !== undefined ? { bearerToken: entry.bearerToken } : {}) };
|
|
393
|
+
}
|
|
394
|
+
else if (entry.authMode === 'api-key') {
|
|
395
|
+
record.auth = {
|
|
396
|
+
mode: 'api-key',
|
|
397
|
+
apiKeyHeader: entry.apiKeyHeader ?? 'X-Api-Key',
|
|
398
|
+
...(entry.apiKeyValue !== undefined ? { apiKeyValue: entry.apiKeyValue } : {}),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
else if (entry.authMode === 'oauth') {
|
|
402
|
+
record.auth = { ...(existing?.auth ?? { mode: 'oauth' }) };
|
|
403
|
+
}
|
|
404
|
+
else if (existing?.auth !== undefined) {
|
|
405
|
+
record.auth = { ...existing.auth };
|
|
406
|
+
}
|
|
407
|
+
return record;
|
|
408
|
+
}
|
|
409
|
+
/** 目标机里生成一个不与既有记录冲突的 key(同身份 → 复用既有 key) */
|
|
410
|
+
export function pickConnectorKey(records, entry) {
|
|
411
|
+
const existing = findConnectorRecord(records, entry);
|
|
412
|
+
if (existing !== undefined)
|
|
413
|
+
return existing.key;
|
|
414
|
+
// key 被一条**无关**连接占着 → 加后缀,绝不覆盖别人的连接
|
|
415
|
+
if (!records.some((r) => r.key === entry.key))
|
|
416
|
+
return entry.key;
|
|
417
|
+
let suffix = 2;
|
|
418
|
+
while (records.some((r) => r.key === `${entry.key}-${suffix}`))
|
|
419
|
+
suffix += 1;
|
|
420
|
+
return `${entry.key}-${suffix}`;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* 把一条(已脱敏、无占位符的)连接合并进存储 JSON 文本,返回新文本。
|
|
424
|
+
*
|
|
425
|
+
* 只替换 `tables.connections[`key`]`,其它表与未知字段逐字保留(grants/tool_catalog 等绝不重写)。
|
|
426
|
+
*/
|
|
427
|
+
export function mergeConnectorIntoStoreText(read, record) {
|
|
428
|
+
const root = read.raw !== undefined ? { ...read.raw } : {};
|
|
429
|
+
if (root['unit'] === undefined || root['unit'] === null || typeof root['unit'] !== 'object') {
|
|
430
|
+
root['unit'] = { name: 'mcp_connector', version: 1 };
|
|
431
|
+
}
|
|
432
|
+
if (!('global' in root))
|
|
433
|
+
root['global'] = null;
|
|
434
|
+
const tables = root['tables'] !== undefined && root['tables'] !== null && typeof root['tables'] === 'object'
|
|
435
|
+
? { ...root['tables'] }
|
|
436
|
+
: {};
|
|
437
|
+
const connections = tables['connections'] !== undefined && tables['connections'] !== null && typeof tables['connections'] === 'object'
|
|
438
|
+
? { ...tables['connections'] }
|
|
439
|
+
: {};
|
|
440
|
+
connections[record.key] = record;
|
|
441
|
+
tables['connections'] = connections;
|
|
442
|
+
root['tables'] = tables;
|
|
443
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
444
|
+
}
|
|
445
|
+
//# sourceMappingURL=mcp-connector-store.js.map
|
|
@@ -80,6 +80,8 @@ export declare const zh: {
|
|
|
80
80
|
'core.recovery.needsAttentionNoRetry': string;
|
|
81
81
|
'core.recovery.dismissFailed': string;
|
|
82
82
|
'core.recovery.lockNotStale': string;
|
|
83
|
+
'core.recovery.opInFlight': string;
|
|
84
|
+
'core.recovery.liveHint': string;
|
|
83
85
|
'core.restore.pathEscaped': string;
|
|
84
86
|
'core.restore.symlinkCriticalFile': string;
|
|
85
87
|
'core.restore.invalidSnapshotId': string;
|
|
@@ -94,6 +94,8 @@ export const zh = {
|
|
|
94
94
|
'core.recovery.needsAttentionNoRetry': 'recovery decision 为 needs-attention,无法 retry',
|
|
95
95
|
'core.recovery.dismissFailed': 'dismiss 失败',
|
|
96
96
|
'core.recovery.lockNotStale': '未判定为 stale,已拒绝回收',
|
|
97
|
+
'core.recovery.opInFlight': '该操作仍在进行中(state={state}),请等它结束后再处理',
|
|
98
|
+
'core.recovery.liveHint': '有 {count} 个操作正在正常执行中',
|
|
97
99
|
// ---------- restore ----------
|
|
98
100
|
'core.restore.pathEscaped': '路径越界: {rel}',
|
|
99
101
|
'core.restore.symlinkCriticalFile': 'symlink 化关键文件: {rel}',
|
|
@@ -388,6 +390,8 @@ export const en = {
|
|
|
388
390
|
'core.recovery.needsAttentionNoRetry': 'the recovery decision is needs-attention; cannot retry',
|
|
389
391
|
'core.recovery.dismissFailed': 'dismiss failed',
|
|
390
392
|
'core.recovery.lockNotStale': 'Not determined to be stale; recovery was refused',
|
|
393
|
+
'core.recovery.opInFlight': 'This operation is still running (state={state}); wait for it to finish',
|
|
394
|
+
'core.recovery.liveHint': '{count} operation(s) are currently running normally',
|
|
391
395
|
// ---------- restore ----------
|
|
392
396
|
'core.restore.pathEscaped': 'Path escapes the allowed root: {rel}',
|
|
393
397
|
'core.restore.symlinkCriticalFile': 'Critical file is a symlink: {rel}',
|