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
|
@@ -0,0 +1,516 @@
|
|
|
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.ts';
|
|
21
|
+
|
|
22
|
+
/** 连接器存储文件($DSH_HOME 相对路径;也是快照/回滚的文件类 ref) */
|
|
23
|
+
export const MCP_CONNECTOR_STORE_REL = 'storages/mcp_connector.json';
|
|
24
|
+
|
|
25
|
+
/** 需要用户重填的凭据占位符(与连接器插件同名同形,便于用户/文档互认) */
|
|
26
|
+
export const REDACTED_VALUE = '<REDACTED:REENTER>';
|
|
27
|
+
|
|
28
|
+
/** 本机专属值占位符(绝对命令、cwd、userinfo) */
|
|
29
|
+
export const REDACTED_LOCAL_VALUE = '<REDACTED:LOCAL_VALUE>';
|
|
30
|
+
|
|
31
|
+
/** 本项目导出的连接器块标识(与连接器插件自己的 dsh-mcp-connector.redacted 区分:我们的还带 key/connectorId) */
|
|
32
|
+
export const MCP_CONNECTOR_EXPORT_FORMAT = 'dsh-config-manager.mcp-connector';
|
|
33
|
+
export const MCP_CONNECTOR_EXPORT_VERSION = 1;
|
|
34
|
+
|
|
35
|
+
/** storage domain 里的一条连接记录(只保留本模块用到的字段,其余字段原样透传) */
|
|
36
|
+
export interface ConnectorRecord {
|
|
37
|
+
key: string;
|
|
38
|
+
connectorId: string;
|
|
39
|
+
kind: 'oauth' | 'manual' | 'json';
|
|
40
|
+
name: string;
|
|
41
|
+
serverKey?: string;
|
|
42
|
+
serverName: string;
|
|
43
|
+
transport: 'streamable-http' | 'sse' | 'stdio';
|
|
44
|
+
command?: string;
|
|
45
|
+
args?: string[];
|
|
46
|
+
env?: Record<string, string>;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
url?: string;
|
|
49
|
+
allowInsecurePrivateNetwork?: boolean;
|
|
50
|
+
headers?: Record<string, string>;
|
|
51
|
+
auth?: {
|
|
52
|
+
mode: 'oauth' | 'bearer' | 'api-key';
|
|
53
|
+
bearerToken?: string;
|
|
54
|
+
apiKeyHeader?: string;
|
|
55
|
+
apiKeyValue?: string;
|
|
56
|
+
grantKey?: string;
|
|
57
|
+
};
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
injectionMode?: 'always' | 'session';
|
|
60
|
+
createdAt?: number;
|
|
61
|
+
updatedAt?: number;
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 导出的连接(脱敏后;redactedFields 列出被替换的字段,导入侧据此判「需重填」) */
|
|
66
|
+
export interface ConnectorExportEntry {
|
|
67
|
+
key: string;
|
|
68
|
+
connectorId: string;
|
|
69
|
+
kind: 'oauth' | 'manual' | 'json';
|
|
70
|
+
name: string;
|
|
71
|
+
serverKey?: string;
|
|
72
|
+
serverName: string;
|
|
73
|
+
transport: 'streamable-http' | 'sse' | 'stdio';
|
|
74
|
+
command?: string;
|
|
75
|
+
args?: string[];
|
|
76
|
+
env?: Record<string, string>;
|
|
77
|
+
cwd?: string;
|
|
78
|
+
url?: string;
|
|
79
|
+
allowInsecurePrivateNetwork?: boolean;
|
|
80
|
+
headers?: Record<string, string>;
|
|
81
|
+
authMode?: 'oauth' | 'bearer' | 'api-key' | 'none';
|
|
82
|
+
bearerToken?: string;
|
|
83
|
+
apiKeyHeader?: string;
|
|
84
|
+
apiKeyValue?: string;
|
|
85
|
+
injectionMode?: 'always' | 'session';
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
redactedFields?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** OAuth 连接只导出「引用」:Token 与动态客户端密钥永不进备份,导入侧提示从市场重新授权 */
|
|
91
|
+
export interface ConnectorOauthRef {
|
|
92
|
+
key: string;
|
|
93
|
+
connectorId: string;
|
|
94
|
+
serverKey?: string;
|
|
95
|
+
name: string;
|
|
96
|
+
serverName: string;
|
|
97
|
+
enabled: boolean;
|
|
98
|
+
injectionMode?: 'always' | 'session';
|
|
99
|
+
action: 'reconnect-oauth';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** mcp 分区里连接器来源的整块载荷 */
|
|
103
|
+
export interface McpConnectorExport {
|
|
104
|
+
format: typeof MCP_CONNECTOR_EXPORT_FORMAT;
|
|
105
|
+
version: typeof MCP_CONNECTOR_EXPORT_VERSION;
|
|
106
|
+
exportedAt: string;
|
|
107
|
+
connections: ConnectorExportEntry[];
|
|
108
|
+
oauthConnections: ConnectorOauthRef[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 名字像凭据的键(Header / env / 查询参数 / CLI flag)——命中即脱敏,不看值形状 */
|
|
112
|
+
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;
|
|
113
|
+
|
|
114
|
+
function isSecretishName(name: string): boolean {
|
|
115
|
+
return SECRETISH_NAME.test(name);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isPlaceholder(value: string): boolean {
|
|
119
|
+
return value === REDACTED_VALUE || value === REDACTED_LOCAL_VALUE;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 单个值是否需要脱敏(名字启发 + 强形状命中) */
|
|
123
|
+
function needsRedaction(value: string, name?: string): boolean {
|
|
124
|
+
if (value === '') return false;
|
|
125
|
+
if (name !== undefined && isSecretishName(name)) return true;
|
|
126
|
+
return matchSecretValuePattern(value) !== null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Header / env 一类的键值表:保留键名(用户重填时要知道填哪个),逐个决定值 */
|
|
130
|
+
function redactValueMap(input: Record<string, string> | undefined): { map?: Record<string, string>; redacted: boolean } {
|
|
131
|
+
if (input === undefined) return { redacted: false };
|
|
132
|
+
const keys = Object.keys(input).sort();
|
|
133
|
+
if (keys.length === 0) return { map: {}, redacted: false };
|
|
134
|
+
let redacted = false;
|
|
135
|
+
const map: Record<string, string> = {};
|
|
136
|
+
for (const key of keys) {
|
|
137
|
+
const raw = input[key];
|
|
138
|
+
const value = typeof raw === 'string' ? raw : String(raw ?? '');
|
|
139
|
+
if (needsRedaction(value, key)) {
|
|
140
|
+
map[key] = REDACTED_VALUE;
|
|
141
|
+
redacted = true;
|
|
142
|
+
} else {
|
|
143
|
+
map[key] = value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { map, redacted };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** stdio 参数:只脱掉看起来是密钥的参数(含 `--token=xxx` 与「flag 后紧跟值」两种形态),其余保留 */
|
|
150
|
+
export function redactArgs(args: readonly string[] | undefined): { args?: string[]; redacted: boolean } {
|
|
151
|
+
if (args === undefined) return { redacted: false };
|
|
152
|
+
let redacted = false;
|
|
153
|
+
const out: string[] = [];
|
|
154
|
+
let prevSecretFlag = false;
|
|
155
|
+
for (const raw of args) {
|
|
156
|
+
const arg = typeof raw === 'string' ? raw : String(raw ?? '');
|
|
157
|
+
const eq = arg.indexOf('=');
|
|
158
|
+
if (arg.startsWith('-') && eq > 0) {
|
|
159
|
+
const flag = arg.slice(0, eq);
|
|
160
|
+
const value = arg.slice(eq + 1);
|
|
161
|
+
if (isSecretishName(flag) || needsRedaction(value)) {
|
|
162
|
+
out.push(`${flag}=${REDACTED_VALUE}`);
|
|
163
|
+
redacted = true;
|
|
164
|
+
prevSecretFlag = false;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
prevSecretFlag = isSecretishName(flag);
|
|
168
|
+
out.push(arg);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (prevSecretFlag || needsRedaction(arg)) {
|
|
172
|
+
out.push(REDACTED_VALUE);
|
|
173
|
+
redacted = true;
|
|
174
|
+
prevSecretFlag = false;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
prevSecretFlag = arg.startsWith('-') && isSecretishName(arg);
|
|
178
|
+
out.push(arg);
|
|
179
|
+
}
|
|
180
|
+
return { args: out, redacted };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 命令:裸命令名(npx / uvx / node)保留;带路径的(绝对路径、盘符、相对目录)替换为本机值占位符 */
|
|
184
|
+
export function redactCommand(command: string | undefined): { command?: string; redacted: boolean } {
|
|
185
|
+
if (command === undefined || command === '') return { redacted: false };
|
|
186
|
+
if (/^[A-Za-z0-9_.@+-]+$/.test(command)) return { command, redacted: false };
|
|
187
|
+
return { command: REDACTED_LOCAL_VALUE, redacted: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** URL:userinfo 整条替换;查询参数逐项脱敏(端点本体必须保住,否则备份无法还原) */
|
|
191
|
+
export function redactUrl(url: string | undefined): { url?: string; redacted: boolean } {
|
|
192
|
+
if (url === undefined || url === '') return { redacted: false };
|
|
193
|
+
let parsed: URL;
|
|
194
|
+
try {
|
|
195
|
+
parsed = new URL(url);
|
|
196
|
+
} catch {
|
|
197
|
+
return { url: REDACTED_VALUE, redacted: true };
|
|
198
|
+
}
|
|
199
|
+
if (parsed.username !== '' || parsed.password !== '') return { url: REDACTED_VALUE, redacted: true };
|
|
200
|
+
let redacted = false;
|
|
201
|
+
if (parsed.search !== '') {
|
|
202
|
+
const params = [...parsed.searchParams.entries()];
|
|
203
|
+
parsed.search = '';
|
|
204
|
+
for (const [name, value] of params) {
|
|
205
|
+
if (needsRedaction(value, name)) {
|
|
206
|
+
parsed.searchParams.append(name, REDACTED_VALUE);
|
|
207
|
+
redacted = true;
|
|
208
|
+
} else {
|
|
209
|
+
parsed.searchParams.append(name, value);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (parsed.hash !== '' && needsRedaction(parsed.hash.replace(/^#/, ''))) {
|
|
214
|
+
parsed.hash = '';
|
|
215
|
+
return { url: parsed.toString(), redacted: true };
|
|
216
|
+
}
|
|
217
|
+
return { url: redacted ? parsed.toString() : url, redacted };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 一条连接记录 → 可携带的脱敏条目(永不返回 Token / 密钥明文) */
|
|
221
|
+
export function exportConnectorRecord(record: ConnectorRecord): ConnectorExportEntry {
|
|
222
|
+
const redactedFields: string[] = [];
|
|
223
|
+
const entry: ConnectorExportEntry = {
|
|
224
|
+
key: record.key,
|
|
225
|
+
connectorId: record.connectorId,
|
|
226
|
+
kind: record.kind,
|
|
227
|
+
name: record.name,
|
|
228
|
+
serverName: record.serverName,
|
|
229
|
+
transport: record.transport,
|
|
230
|
+
enabled: record.enabled !== false,
|
|
231
|
+
};
|
|
232
|
+
if (record.serverKey !== undefined) entry.serverKey = record.serverKey;
|
|
233
|
+
if (record.injectionMode !== undefined) entry.injectionMode = record.injectionMode;
|
|
234
|
+
|
|
235
|
+
if (record.transport === 'stdio') {
|
|
236
|
+
const command = redactCommand(record.command);
|
|
237
|
+
if (command.command !== undefined) entry.command = command.command;
|
|
238
|
+
if (command.redacted) redactedFields.push('command');
|
|
239
|
+
const args = redactArgs(record.args);
|
|
240
|
+
if (args.args !== undefined) entry.args = args.args;
|
|
241
|
+
if (args.redacted) redactedFields.push('args');
|
|
242
|
+
const env = redactValueMap(record.env);
|
|
243
|
+
if (env.map !== undefined) entry.env = env.map;
|
|
244
|
+
if (env.redacted) redactedFields.push('env');
|
|
245
|
+
if (record.cwd !== undefined && record.cwd !== '') {
|
|
246
|
+
entry.cwd = REDACTED_LOCAL_VALUE;
|
|
247
|
+
redactedFields.push('cwd');
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
const url = redactUrl(record.url);
|
|
251
|
+
if (url.url !== undefined) entry.url = url.url;
|
|
252
|
+
if (url.redacted) redactedFields.push('url');
|
|
253
|
+
if (record.allowInsecurePrivateNetwork === true) entry.allowInsecurePrivateNetwork = true;
|
|
254
|
+
const headers = redactValueMap(record.headers);
|
|
255
|
+
if (headers.map !== undefined) entry.headers = headers.map;
|
|
256
|
+
if (headers.redacted) redactedFields.push('headers');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const mode = record.auth?.mode;
|
|
260
|
+
if (mode === 'bearer') {
|
|
261
|
+
entry.authMode = 'bearer';
|
|
262
|
+
entry.bearerToken = REDACTED_VALUE;
|
|
263
|
+
redactedFields.push('bearerToken');
|
|
264
|
+
} else if (mode === 'api-key') {
|
|
265
|
+
entry.authMode = 'api-key';
|
|
266
|
+
entry.apiKeyHeader = record.auth?.apiKeyHeader ?? 'X-Api-Key';
|
|
267
|
+
entry.apiKeyValue = REDACTED_VALUE;
|
|
268
|
+
redactedFields.push('apiKeyValue');
|
|
269
|
+
} else if (mode === 'oauth') {
|
|
270
|
+
entry.authMode = 'oauth';
|
|
271
|
+
} else {
|
|
272
|
+
entry.authMode = 'none';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (redactedFields.length > 0) entry.redactedFields = redactedFields;
|
|
276
|
+
return entry;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 全部连接 → 导出块(OAuth 只留引用;按 key 排序,便于 diff 与稳定哈希) */
|
|
280
|
+
export function exportConnectorBlock(records: readonly ConnectorRecord[], exportedAt = new Date().toISOString()): McpConnectorExport {
|
|
281
|
+
const sorted = [...records].sort((a, b) => a.key.localeCompare(b.key));
|
|
282
|
+
return {
|
|
283
|
+
format: MCP_CONNECTOR_EXPORT_FORMAT,
|
|
284
|
+
version: MCP_CONNECTOR_EXPORT_VERSION,
|
|
285
|
+
exportedAt,
|
|
286
|
+
connections: sorted.filter((r) => r.auth?.mode !== 'oauth').map((r) => exportConnectorRecord(r)),
|
|
287
|
+
oauthConnections: sorted
|
|
288
|
+
.filter((r) => r.auth?.mode === 'oauth')
|
|
289
|
+
.map((r) => ({
|
|
290
|
+
key: r.key,
|
|
291
|
+
connectorId: r.connectorId,
|
|
292
|
+
...(r.serverKey !== undefined ? { serverKey: r.serverKey } : {}),
|
|
293
|
+
name: r.name,
|
|
294
|
+
serverName: r.serverName,
|
|
295
|
+
enabled: r.enabled !== false,
|
|
296
|
+
...(r.injectionMode !== undefined ? { injectionMode: r.injectionMode } : {}),
|
|
297
|
+
action: 'reconnect-oauth' as const,
|
|
298
|
+
})),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 列出「还有占位符」的字段路径(导入前判「需重填凭据」) */
|
|
303
|
+
export function unresolvedRedactions(value: unknown, path = '$', output: string[] = []): string[] {
|
|
304
|
+
if (typeof value === 'string') {
|
|
305
|
+
if (isPlaceholder(value)) output.push(path);
|
|
306
|
+
return output;
|
|
307
|
+
}
|
|
308
|
+
if (Array.isArray(value)) {
|
|
309
|
+
value.forEach((item, index) => unresolvedRedactions(item, `${path}[${index}]`, output));
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
312
|
+
if (value !== null && typeof value === 'object') {
|
|
313
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
314
|
+
unresolvedRedactions(item, `${path}.${key}`, output);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return output;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 导出条目里「需要用户重填」的字段(占位符路径 + redactedFields 并集,供计划项 detail 用) */
|
|
321
|
+
export function refillFieldsOf(entry: ConnectorExportEntry): string[] {
|
|
322
|
+
const fields = new Set<string>(entry.redactedFields ?? []);
|
|
323
|
+
for (const p of unresolvedRedactions(entry)) {
|
|
324
|
+
const trimmed = p.startsWith('$.') ? p.slice(2) : p;
|
|
325
|
+
if (trimmed !== '') fields.add(trimmed);
|
|
326
|
+
}
|
|
327
|
+
return [...fields].sort();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** 连接器存储文件的解析结果 */
|
|
331
|
+
export interface ConnectorStoreRead {
|
|
332
|
+
/** 文件存在且解析成功 */
|
|
333
|
+
parsed: boolean;
|
|
334
|
+
/** 文件是否存在(ENOENT → false;其余错误 → true + 解析失败) */
|
|
335
|
+
exists: boolean;
|
|
336
|
+
connections: ConnectorRecord[];
|
|
337
|
+
/** 解析失败原因(读到了但形态不对 → 绝不当作空表覆盖) */
|
|
338
|
+
error?: string;
|
|
339
|
+
/** 原始 JSON(保留其它表,写回时只替换 connections) */
|
|
340
|
+
raw?: Record<string, unknown>;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function asRecordArray(value: unknown): ConnectorRecord[] {
|
|
344
|
+
if (value === null || typeof value !== 'object') return [];
|
|
345
|
+
const out: ConnectorRecord[] = [];
|
|
346
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
347
|
+
if (item === null || typeof item !== 'object') continue;
|
|
348
|
+
const rec = item as Record<string, unknown>;
|
|
349
|
+
const serverName = typeof rec['serverName'] === 'string' ? rec['serverName'] : '';
|
|
350
|
+
if (serverName === '') continue;
|
|
351
|
+
out.push({
|
|
352
|
+
...(rec as ConnectorRecord),
|
|
353
|
+
key: typeof rec['key'] === 'string' && rec['key'] !== '' ? rec['key'] : key,
|
|
354
|
+
serverName,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 解析连接器存储文本(绝不对「读到了但解析不了」当空表) */
|
|
361
|
+
export function parseConnectorStore(text: string): ConnectorStoreRead {
|
|
362
|
+
let raw: unknown;
|
|
363
|
+
try {
|
|
364
|
+
raw = JSON.parse(text);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
return { parsed: false, exists: true, connections: [], error: err instanceof Error ? err.message : String(err) };
|
|
367
|
+
}
|
|
368
|
+
if (raw === null || typeof raw !== 'object') {
|
|
369
|
+
return { parsed: false, exists: true, connections: [], error: 'root is not an object' };
|
|
370
|
+
}
|
|
371
|
+
const root = raw as Record<string, unknown>;
|
|
372
|
+
const tables = root['tables'];
|
|
373
|
+
if (tables === null || typeof tables !== 'object') {
|
|
374
|
+
return { parsed: false, exists: true, connections: [], error: 'tables is not an object' };
|
|
375
|
+
}
|
|
376
|
+
const connections = asRecordArray((tables as Record<string, unknown>)['connections']);
|
|
377
|
+
return { parsed: true, exists: true, connections, raw: root };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** 读连接器存储(ENOENT → 空表 + exists:false;其余错误原样上报) */
|
|
381
|
+
export async function readConnectorStore(
|
|
382
|
+
fs: { readFile(rel: string): Promise<Uint8Array>; exists(rel: string): Promise<boolean> },
|
|
383
|
+
): Promise<ConnectorStoreRead> {
|
|
384
|
+
if (!(await fs.exists(MCP_CONNECTOR_STORE_REL))) {
|
|
385
|
+
// 没装连接器是正常形态:空表但**可写**(导入时要在目标机新建这个文件)
|
|
386
|
+
return { parsed: true, exists: false, connections: [] };
|
|
387
|
+
}
|
|
388
|
+
const bytes = await fs.readFile(MCP_CONNECTOR_STORE_REL);
|
|
389
|
+
const text = new TextDecoder().decode(bytes);
|
|
390
|
+
return parseConnectorStore(text);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** 连接身份:优先 key,其次 (connectorId, serverName) */
|
|
394
|
+
export function connectorIdentity(entry: { key?: string; connectorId?: string; serverName: string }): string {
|
|
395
|
+
if (typeof entry.key === 'string' && entry.key !== '') return `key:${entry.key}`;
|
|
396
|
+
return `name:${entry.connectorId ?? ''}/${entry.serverName}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 在目标连接里找同一身份的记录:**只认 (connectorId, serverName)**。
|
|
401
|
+
*
|
|
402
|
+
* 为什么不先按 key 认:key 是存储主键,但「同 key 不同连接」在手工改过/旧版本里是可能的;
|
|
403
|
+
* 按 key 命中就当成同一条会**覆盖掉一条无关连接**。身份相同 → 复用既有 key(幂等);
|
|
404
|
+
* 身份不同 → 交给 pickConnectorKey 挑一个不冲突的新 key。
|
|
405
|
+
*/
|
|
406
|
+
export function findConnectorRecord(records: readonly ConnectorRecord[], entry: ConnectorExportEntry): ConnectorRecord | undefined {
|
|
407
|
+
return records.find((r) => r.connectorId === entry.connectorId && r.serverName === entry.serverName);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 比较用形态:**两侧都过一遍脱敏**再去掉易变字段。
|
|
412
|
+
*
|
|
413
|
+
* 为什么导出条目也要重新脱敏:条目里可能已被用户重填成真值(明文),而目标机记录里的
|
|
414
|
+
* 同名字段也是明文——但如果只脱一侧,明文与占位符一比就永远 Conflict(导入一次就再也 Skip 不了)。
|
|
415
|
+
* 两侧统一过 exportConnectorRecord ⇒ 明文都被换成同一个占位符,同一条连接天然相等。
|
|
416
|
+
*/
|
|
417
|
+
export function comparableConnector(record: ConnectorRecord | ConnectorExportEntry): Record<string, unknown> {
|
|
418
|
+
const isEntry = (record as ConnectorExportEntry).authMode !== undefined;
|
|
419
|
+
const asRecord = isEntry
|
|
420
|
+
? buildConnectorRecord(record as ConnectorExportEntry, { key: (record as ConnectorExportEntry).key, now: 0 })
|
|
421
|
+
: (record as ConnectorRecord);
|
|
422
|
+
const { key: _key, redactedFields: _rf, ...rest } = exportConnectorRecord(asRecord);
|
|
423
|
+
return rest as Record<string, unknown>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** 把脱敏条目还原成可写入 storage domain 的记录(占位符必须为空才不会破坏目标机) */
|
|
427
|
+
export function buildConnectorRecord(
|
|
428
|
+
entry: ConnectorExportEntry,
|
|
429
|
+
options: { existing?: ConnectorRecord; key: string; now: number },
|
|
430
|
+
): ConnectorRecord {
|
|
431
|
+
const { existing, key, now } = options;
|
|
432
|
+
const record: ConnectorRecord = {
|
|
433
|
+
...(existing ?? {}),
|
|
434
|
+
key,
|
|
435
|
+
connectorId: entry.connectorId,
|
|
436
|
+
kind: entry.kind,
|
|
437
|
+
name: entry.name,
|
|
438
|
+
serverName: entry.serverName,
|
|
439
|
+
transport: entry.transport,
|
|
440
|
+
enabled: entry.enabled !== false,
|
|
441
|
+
createdAt: existing?.createdAt ?? now,
|
|
442
|
+
updatedAt: now,
|
|
443
|
+
};
|
|
444
|
+
if (entry.serverKey !== undefined) record.serverKey = entry.serverKey;
|
|
445
|
+
if (entry.injectionMode !== undefined) record.injectionMode = entry.injectionMode;
|
|
446
|
+
|
|
447
|
+
if (entry.transport === 'stdio') {
|
|
448
|
+
if (entry.command !== undefined) record.command = entry.command;
|
|
449
|
+
if (entry.args !== undefined) record.args = [...entry.args];
|
|
450
|
+
if (entry.env !== undefined) record.env = { ...entry.env };
|
|
451
|
+
if (entry.cwd !== undefined) record.cwd = entry.cwd;
|
|
452
|
+
delete record.url;
|
|
453
|
+
delete record.headers;
|
|
454
|
+
} else {
|
|
455
|
+
if (entry.url !== undefined) record.url = entry.url;
|
|
456
|
+
if (entry.headers !== undefined) record.headers = { ...entry.headers };
|
|
457
|
+
if (entry.allowInsecurePrivateNetwork === true) record.allowInsecurePrivateNetwork = true;
|
|
458
|
+
else delete record.allowInsecurePrivateNetwork;
|
|
459
|
+
delete record.command;
|
|
460
|
+
delete record.args;
|
|
461
|
+
delete record.env;
|
|
462
|
+
delete record.cwd;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (entry.authMode === 'bearer') {
|
|
466
|
+
record.auth = { mode: 'bearer', ...(entry.bearerToken !== undefined ? { bearerToken: entry.bearerToken } : {}) };
|
|
467
|
+
} else if (entry.authMode === 'api-key') {
|
|
468
|
+
record.auth = {
|
|
469
|
+
mode: 'api-key',
|
|
470
|
+
apiKeyHeader: entry.apiKeyHeader ?? 'X-Api-Key',
|
|
471
|
+
...(entry.apiKeyValue !== undefined ? { apiKeyValue: entry.apiKeyValue } : {}),
|
|
472
|
+
};
|
|
473
|
+
} else if (entry.authMode === 'oauth') {
|
|
474
|
+
record.auth = { ...(existing?.auth ?? { mode: 'oauth' }) };
|
|
475
|
+
} else if (existing?.auth !== undefined) {
|
|
476
|
+
record.auth = { ...existing.auth };
|
|
477
|
+
}
|
|
478
|
+
return record;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** 目标机里生成一个不与既有记录冲突的 key(同身份 → 复用既有 key) */
|
|
482
|
+
export function pickConnectorKey(records: readonly ConnectorRecord[], entry: ConnectorExportEntry): string {
|
|
483
|
+
const existing = findConnectorRecord(records, entry);
|
|
484
|
+
if (existing !== undefined) return existing.key;
|
|
485
|
+
// key 被一条**无关**连接占着 → 加后缀,绝不覆盖别人的连接
|
|
486
|
+
if (!records.some((r) => r.key === entry.key)) return entry.key;
|
|
487
|
+
let suffix = 2;
|
|
488
|
+
while (records.some((r) => r.key === `${entry.key}-${suffix}`)) suffix += 1;
|
|
489
|
+
return `${entry.key}-${suffix}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 把一条(已脱敏、无占位符的)连接合并进存储 JSON 文本,返回新文本。
|
|
494
|
+
*
|
|
495
|
+
* 只替换 `tables.connections[`key`]`,其它表与未知字段逐字保留(grants/tool_catalog 等绝不重写)。
|
|
496
|
+
*/
|
|
497
|
+
export function mergeConnectorIntoStoreText(
|
|
498
|
+
read: ConnectorStoreRead,
|
|
499
|
+
record: ConnectorRecord,
|
|
500
|
+
): string {
|
|
501
|
+
const root: Record<string, unknown> = read.raw !== undefined ? { ...read.raw } : {};
|
|
502
|
+
if (root['unit'] === undefined || root['unit'] === null || typeof root['unit'] !== 'object') {
|
|
503
|
+
root['unit'] = { name: 'mcp_connector', version: 1 };
|
|
504
|
+
}
|
|
505
|
+
if (!('global' in root)) root['global'] = null;
|
|
506
|
+
const tables: Record<string, unknown> = root['tables'] !== undefined && root['tables'] !== null && typeof root['tables'] === 'object'
|
|
507
|
+
? { ...(root['tables'] as Record<string, unknown>) }
|
|
508
|
+
: {};
|
|
509
|
+
const connections: Record<string, unknown> = tables['connections'] !== undefined && tables['connections'] !== null && typeof tables['connections'] === 'object'
|
|
510
|
+
? { ...(tables['connections'] as Record<string, unknown>) }
|
|
511
|
+
: {};
|
|
512
|
+
connections[record.key] = record;
|
|
513
|
+
tables['connections'] = connections;
|
|
514
|
+
root['tables'] = tables;
|
|
515
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
516
|
+
}
|
|
@@ -111,6 +111,8 @@ export const zh = {
|
|
|
111
111
|
'core.recovery.needsAttentionNoRetry': 'recovery decision 为 needs-attention,无法 retry',
|
|
112
112
|
'core.recovery.dismissFailed': 'dismiss 失败',
|
|
113
113
|
'core.recovery.lockNotStale': '未判定为 stale,已拒绝回收',
|
|
114
|
+
'core.recovery.opInFlight': '该操作仍在进行中(state={state}),请等它结束后再处理',
|
|
115
|
+
'core.recovery.liveHint': '有 {count} 个操作正在正常执行中',
|
|
114
116
|
// ---------- restore ----------
|
|
115
117
|
'core.restore.pathEscaped': '路径越界: {rel}',
|
|
116
118
|
'core.restore.symlinkCriticalFile': 'symlink 化关键文件: {rel}',
|
|
@@ -423,6 +425,8 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
423
425
|
'core.recovery.needsAttentionNoRetry': 'the recovery decision is needs-attention; cannot retry',
|
|
424
426
|
'core.recovery.dismissFailed': 'dismiss failed',
|
|
425
427
|
'core.recovery.lockNotStale': 'Not determined to be stale; recovery was refused',
|
|
428
|
+
'core.recovery.opInFlight': 'This operation is still running (state={state}); wait for it to finish',
|
|
429
|
+
'core.recovery.liveHint': '{count} operation(s) are currently running normally',
|
|
426
430
|
// ---------- restore ----------
|
|
427
431
|
'core.restore.pathEscaped': 'Path escapes the allowed root: {rel}',
|
|
428
432
|
'core.restore.symlinkCriticalFile': 'Critical file is a symlink: {rel}',
|
package/src/core/messages.ts
CHANGED
|
@@ -255,6 +255,17 @@ export const zh = {
|
|
|
255
255
|
'adapter.mcpCreate': '创建 MCP server {serverName}',
|
|
256
256
|
'adapter.mcpSame': 'MCP server {serverName} 已一致',
|
|
257
257
|
'adapter.mcpDiff': 'MCP server {serverName} 与目标不同',
|
|
258
|
+
'adapter.mcpConnectorReadFailed': '无法读取 MCP 连接器存储({reason}),本次备份不含连接器配置',
|
|
259
|
+
'adapter.mcpConnectorTargetUnreadable': '目标机的 MCP 连接器存储无法解析({reason}),已跳过连接器导入,原文件未改动',
|
|
260
|
+
'adapter.mcpConnectorNeedsRefill': 'MCP 连接器「{name}」含需重填的凭据,未导入',
|
|
261
|
+
'adapter.mcpConnectorCreate': '新增 MCP 连接器「{name}」',
|
|
262
|
+
'adapter.mcpConnectorSame': 'MCP 连接器「{name}」已存在且一致',
|
|
263
|
+
'adapter.mcpConnectorDiff': 'MCP 连接器「{name}」与目标机不同',
|
|
264
|
+
'adapter.mcpConnectorDuplicateName': 'server「{serverName}」同时来自 patch 行与连接器,两份都会保留(不合并)',
|
|
265
|
+
'adapter.mcpConnectorOauth': 'OAuth 连接器「{name}」需在市场中重新授权',
|
|
266
|
+
'adapter.mcpConnectorMissing': '备份中找不到 MCP 连接器「{name}」',
|
|
267
|
+
'adapter.mcpConnectorWritten': '已写入 MCP 连接器「{name}」,重启 DSH 后生效',
|
|
268
|
+
'adapter.mcpConnectorInvalid': 'MCP 连接器条目缺少 key 或 serverName',
|
|
258
269
|
'adapter.pwCreate': '写入 pnpm-workspace.yaml(插件构建白名单/冷静期配置)',
|
|
259
270
|
'adapter.pwUpdate': '更新 pnpm-workspace.yaml(插件构建白名单/冷静期配置)',
|
|
260
271
|
'adapter.pluginInstall': '安装插件 {name}@{version}',
|
|
@@ -264,6 +275,9 @@ export const zh = {
|
|
|
264
275
|
'adapter.pluginBundleMember': '(bundle 成员,经聚合包安装)',
|
|
265
276
|
'adapter.pluginInstallOk': '插件 {name} 已安装{suffix}',
|
|
266
277
|
'adapter.pluginUpdateTriggered': '插件 {name} 已触发更新(npm 最新版){suffix}',
|
|
278
|
+
'adapter.pluginInstalledAtVersion': '插件 {name} 已安装到 {version}{suffix}',
|
|
279
|
+
'adapter.pluginVersionMismatch': '插件 {name} 安装后版本为 {actual},与备份记录的 {expected} 不一致(该版本可能已从 npm 撤版);可手动执行:{command}',
|
|
280
|
+
'adapter.skillBundledSkipped': '技能 {name} 由插件内置提供(bundled),备份里的副本不完整,已跳过;升级对应插件即可获得最新版本',
|
|
267
281
|
'adapter.pluginRestartSuffix': ',重启 dsh 后生效',
|
|
268
282
|
'adapter.pluginHostBundleDeclare': '声明插件层 {name}(由 DSH 自带提供,不安装)',
|
|
269
283
|
'adapter.pluginHostBundleSame': '插件层 {name} 已在目标机声明(由 DSH 自带提供)',
|
|
@@ -409,6 +423,15 @@ export const zh = {
|
|
|
409
423
|
'sync.workdirNotConfigured': '尚未配置 git 仓库地址,无法重建工作副本。',
|
|
410
424
|
'sync.workdirRelinked': '工作副本的 origin 已重指向当前仓库地址(工作树与本地提交未改动)。',
|
|
411
425
|
'sync.workdirRecloned': '工作副本已重新克隆(原有本地提交已丢弃,远端快照已同步回本地副本)。',
|
|
426
|
+
// issue #104:换仓库/远端历史被重写后「采纳远端历史」(adopt)的可操作文案。
|
|
427
|
+
// 注意:既有键(含上面的 historyDiverged / workdirModeInvalid)有冻结指纹,只许追加、不许改值。
|
|
428
|
+
'sync.git.adoptDirty': '工作副本 {dir} 里有未提交的改动:请先提交或撤销这些改动,再采纳远端历史(否则冲突会把你的改动卡住)。',
|
|
429
|
+
'sync.git.adoptDetached': '工作副本 {dir} 当前不在任何分支上(detached HEAD),无法采纳远端历史。请先切回一个分支(git switch <分支名>)后重试。',
|
|
430
|
+
'sync.git.adoptNoRemoteBranch': '远端没有名为 {branch} 的分支(远端现有:{heads}),未做任何合并。请先把仓库地址/分支改对,或改用「重新克隆工作副本」。',
|
|
431
|
+
'sync.git.adoptNotSyncRepo': '远端仓库顶层出现了快照布局以外的内容({entries}):它看起来不是本插件的同步仓库,已拒绝合并(不会把别的仓库内容搬进工作副本)。',
|
|
432
|
+
'sync.git.adoptMergeConflict': '合并远端 {branch} 时发生冲突:已撤销这次合并(merge --abort),工作副本回到操作前的状态。请手工解决冲突后重试,或改用「重新克隆工作副本」。',
|
|
433
|
+
'sync.workdirModeInvalidFull': '重建工作副本的模式非法: {mode}(只允许 relink(重指向地址)、adopt(采纳远端历史)或 reclone(重新克隆))',
|
|
434
|
+
|
|
412
435
|
'sync.git.cmdFailed': 'git {args} 失败 (exit {code}): {err}',
|
|
413
436
|
// 推送认证(真机:公开仓库拉取可匿名成功、推送必失败,git 原文只说「Invalid username or token」)
|
|
414
437
|
'sync.git.pushTokenRequired': '推送需要访问令牌:远端是 http(s) 仓库,但本机没有可用的令牌(拉取公开仓库可以匿名完成,推送必须认证)。请在「同步 → 通道配置」里登录 GitHub 或填入访问令牌后重试。',
|
|
@@ -715,6 +738,17 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
715
738
|
'adapter.mcpCreate': 'Create MCP server {serverName}',
|
|
716
739
|
'adapter.mcpSame': 'MCP server {serverName} is already identical',
|
|
717
740
|
'adapter.mcpDiff': 'MCP server {serverName} differs from the target',
|
|
741
|
+
'adapter.mcpConnectorReadFailed': 'Could not read the MCP connector store ({reason}); this backup has no connector config',
|
|
742
|
+
'adapter.mcpConnectorTargetUnreadable': 'The target MCP connector store cannot be parsed ({reason}); connector import skipped and the file left untouched',
|
|
743
|
+
'adapter.mcpConnectorNeedsRefill': 'MCP connector "{name}" contains credentials that must be re-entered; not imported',
|
|
744
|
+
'adapter.mcpConnectorCreate': 'Add MCP connector "{name}"',
|
|
745
|
+
'adapter.mcpConnectorSame': 'MCP connector "{name}" already exists and matches',
|
|
746
|
+
'adapter.mcpConnectorDiff': 'MCP connector "{name}" differs from the target',
|
|
747
|
+
'adapter.mcpConnectorDuplicateName': 'Server "{serverName}" comes from both a patch line and a connector; both are kept (never merged)',
|
|
748
|
+
'adapter.mcpConnectorOauth': 'OAuth connector "{name}" must be re-authorized in the marketplace',
|
|
749
|
+
'adapter.mcpConnectorMissing': 'MCP connector "{name}" not found in the backup',
|
|
750
|
+
'adapter.mcpConnectorWritten': 'MCP connector "{name}" written; takes effect after restarting DSH',
|
|
751
|
+
'adapter.mcpConnectorInvalid': 'MCP connector entry is missing key or serverName',
|
|
718
752
|
'adapter.pwCreate': 'Write pnpm-workspace.yaml (plugin build whitelist / release-age settings)',
|
|
719
753
|
'adapter.pwUpdate': 'Update pnpm-workspace.yaml (plugin build whitelist / release-age settings)',
|
|
720
754
|
'adapter.pluginInstall': 'Install plugin {name}@{version}',
|
|
@@ -724,6 +758,9 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
724
758
|
'adapter.pluginBundleMember': ' (bundle member, installed via the aggregate package)',
|
|
725
759
|
'adapter.pluginInstallOk': 'Plugin {name} installed{suffix}',
|
|
726
760
|
'adapter.pluginUpdateTriggered': 'Plugin {name} update triggered (npm latest){suffix}',
|
|
761
|
+
'adapter.pluginInstalledAtVersion': 'Plugin {name} installed at {version}{suffix}',
|
|
762
|
+
'adapter.pluginVersionMismatch': 'Plugin {name} is at {actual} after install, not the backed-up {expected} (that version may have been unpublished); run manually: {command}',
|
|
763
|
+
'adapter.skillBundledSkipped': 'Skill {name} is provided by a plugin (bundled); the copy in the backup is incomplete and was skipped; update that plugin to get the latest version',
|
|
727
764
|
'adapter.pluginRestartSuffix': '; takes effect after restarting dsh',
|
|
728
765
|
'adapter.pluginHostBundleDeclare': 'Declare plugin layer {name} (provided by DSH itself; not installed)',
|
|
729
766
|
'adapter.pluginHostBundleSame': 'Plugin layer {name} is already declared on the target (provided by DSH itself)',
|
|
@@ -869,6 +906,15 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
869
906
|
'sync.workdirNotConfigured': 'No git repository address is configured, so the working copy cannot be rebuilt.',
|
|
870
907
|
'sync.workdirRelinked': 'The working copy origin now points at the configured repository address (working tree and local commits untouched).',
|
|
871
908
|
'sync.workdirRecloned': 'The working copy was cloned again (local commits were discarded; remote snapshots are back in the local copy).',
|
|
909
|
+
// issue #104: actionable text for adopting remote history (adopt) after a repository swap / rewritten history.
|
|
910
|
+
// Existing keys (including historyDiverged / workdirModeInvalid above) have frozen fingerprints: append only.
|
|
911
|
+
'sync.git.adoptDirty': 'The working copy {dir} has uncommitted changes: commit or discard them before adopting the remote history (otherwise a conflict would trap your changes).',
|
|
912
|
+
'sync.git.adoptDetached': 'The working copy {dir} is not on any branch (detached HEAD), so the remote history cannot be adopted. Switch back to a branch (git switch <branch>) and retry.',
|
|
913
|
+
'sync.git.adoptNoRemoteBranch': 'The remote has no branch named {branch} (remote branches: {heads}); nothing was merged. Fix the repository address/branch first, or use "clone the working copy again".',
|
|
914
|
+
'sync.git.adoptNotSyncRepo': 'The remote repository root contains entries outside the snapshot layout ({entries}): it does not look like this plugin\'s sync repository, so the merge was refused (another repository\'s content is never pulled into the working copy).',
|
|
915
|
+
'sync.git.adoptMergeConflict': 'Merging remote {branch} produced conflicts: the merge was aborted (merge --abort) and the working copy is back to its previous state. Resolve the conflicts manually and retry, or use "clone the working copy again".',
|
|
916
|
+
'sync.workdirModeInvalidFull': 'Invalid working-copy rebuild mode: {mode} (only relink (re-point the address), adopt (merge the remote history) or reclone (clone again) are allowed).',
|
|
917
|
+
|
|
872
918
|
'sync.git.cmdFailed': 'git {args} failed (exit {code}): {err}',
|
|
873
919
|
'sync.git.pushTokenRequired': 'Push requires an access token, but none is available on this machine (public repositories can be fetched anonymously; pushing always needs authentication). Sign in to GitHub or paste an access token under Sync → Channel configuration, then retry.',
|
|
874
920
|
'sync.git.pushAuthFailed': 'Push authentication failed: {err}. Check that the token is valid and has write access to this repository (password authentication is disabled by the remote); a successful pull does not prove the token works — public repositories can be fetched anonymously. Sign in again or replace the token under Sync → Channel configuration.',
|
package/src/core/phase3-host.ts
CHANGED
|
@@ -577,6 +577,15 @@ export class Phase3Recovery {
|
|
|
577
577
|
}
|
|
578
578
|
await this.store.update(opId, (j) => {
|
|
579
579
|
let next = j;
|
|
580
|
+
// ext step 必须随 op 成功一起结算:此前 :556 写入的 `status:'planned'` 永远不会被更新,
|
|
581
|
+
// ⇒ 一条 COMMITTED 的 journal 里躺着「计划做、没做过」的步骤。后果有两个:
|
|
582
|
+
// ① 审计不可信(recovery 面板把 external step 的 planned 当「未执行的意图」转发给用户);
|
|
583
|
+
// ② issue #87 的 `selfProvableResidual` 用「所有 step 都是 planned 且 appliedAt 为空」来
|
|
584
|
+
// 证明「副作用从未开始」—— 成功 op 落在 completed/ 不参与判定,但同一形状一旦被别的
|
|
585
|
+
// 路径读到就会被误读成「残留」。结算成 done + appliedAt 后形状语义唯一。
|
|
586
|
+
if (next.steps.ext !== undefined) {
|
|
587
|
+
next = { ...next, steps: { ...next.steps, ext: { ...next.steps.ext, status: 'done', appliedAt: new Date().toISOString() } } };
|
|
588
|
+
}
|
|
580
589
|
if (next.state === 'SNAPSHOT_CREATED') next = transitionJournalState(next, 'APPLYING');
|
|
581
590
|
if (next.state === 'APPLYING') next = transitionJournalState(next, 'VALIDATING');
|
|
582
591
|
next = transitionJournalState(next, 'COMMITTED');
|
|
@@ -596,7 +605,15 @@ export class Phase3Recovery {
|
|
|
596
605
|
} else {
|
|
597
606
|
const j = await this.store.loadActive(opId);
|
|
598
607
|
if (j !== null && !isTerminalState(j.state)) {
|
|
599
|
-
await this.store.update(opId, (cur) =>
|
|
608
|
+
await this.store.update(opId, (cur) => {
|
|
609
|
+
// 失败时 ext step 不能停在 planned:planned 的含义是「还没开始」,而外部操作已经
|
|
610
|
+
// 尝试过了(可能半途炸掉)。落成 attention 让 journal 自己说明「外部步骤结论未知」,
|
|
611
|
+
// 而不是留给读取方去猜。appliedAt 保持 null(没有可证明的完成时刻)。
|
|
612
|
+
const withStep = cur.steps.ext !== undefined && cur.steps.ext.status === 'planned'
|
|
613
|
+
? { ...cur, steps: { ...cur.steps, ext: { ...cur.steps.ext, status: 'attention' as const } } }
|
|
614
|
+
: cur;
|
|
615
|
+
return transitionJournalState(withStep, 'NEEDS_ATTENTION');
|
|
616
|
+
}).catch(() => undefined);
|
|
600
617
|
}
|
|
601
618
|
}
|
|
602
619
|
throw err;
|