dsh-config-manager 0.1.78 → 0.1.79
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 +400 -112
- package/lib/client.js +2845 -1524
- 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/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 +15 -1
- package/lib/sync/gist/gist-transport.js +66 -5
- package/lib/sync/gist/messages.d.ts +3 -0
- package/lib/sync/gist/messages.js +7 -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/sync-engine.d.ts +193 -4
- package/lib/sync/sync-engine.js +450 -37
- 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 +8 -0
- package/lib/ui/i18n.js +20 -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/routes.ts +9 -0
- 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/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 +219 -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 +234 -0
- package/src/client/sync/merge-view.ts +371 -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.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/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 +223 -0
- package/src/sync/gist/gist-transport.ts +73 -7
- package/src/sync/gist/messages.ts +7 -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.ts +87 -9
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +379 -0
- package/src/sync/sync-engine.ts +535 -36
- 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 +20 -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/messages.d.ts
CHANGED
|
@@ -237,6 +237,17 @@ export declare const zh: {
|
|
|
237
237
|
readonly 'adapter.mcpCreate': '创建 MCP server {serverName}';
|
|
238
238
|
readonly 'adapter.mcpSame': 'MCP server {serverName} 已一致';
|
|
239
239
|
readonly 'adapter.mcpDiff': 'MCP server {serverName} 与目标不同';
|
|
240
|
+
readonly 'adapter.mcpConnectorReadFailed': '无法读取 MCP 连接器存储({reason}),本次备份不含连接器配置';
|
|
241
|
+
readonly 'adapter.mcpConnectorTargetUnreadable': '目标机的 MCP 连接器存储无法解析({reason}),已跳过连接器导入,原文件未改动';
|
|
242
|
+
readonly 'adapter.mcpConnectorNeedsRefill': 'MCP 连接器「{name}」含需重填的凭据,未导入';
|
|
243
|
+
readonly 'adapter.mcpConnectorCreate': '新增 MCP 连接器「{name}」';
|
|
244
|
+
readonly 'adapter.mcpConnectorSame': 'MCP 连接器「{name}」已存在且一致';
|
|
245
|
+
readonly 'adapter.mcpConnectorDiff': 'MCP 连接器「{name}」与目标机不同';
|
|
246
|
+
readonly 'adapter.mcpConnectorDuplicateName': 'server「{serverName}」同时来自 patch 行与连接器,两份都会保留(不合并)';
|
|
247
|
+
readonly 'adapter.mcpConnectorOauth': 'OAuth 连接器「{name}」需在市场中重新授权';
|
|
248
|
+
readonly 'adapter.mcpConnectorMissing': '备份中找不到 MCP 连接器「{name}」';
|
|
249
|
+
readonly 'adapter.mcpConnectorWritten': '已写入 MCP 连接器「{name}」,重启 DSH 后生效';
|
|
250
|
+
readonly 'adapter.mcpConnectorInvalid': 'MCP 连接器条目缺少 key 或 serverName';
|
|
240
251
|
readonly 'adapter.pwCreate': '写入 pnpm-workspace.yaml(插件构建白名单/冷静期配置)';
|
|
241
252
|
readonly 'adapter.pwUpdate': '更新 pnpm-workspace.yaml(插件构建白名单/冷静期配置)';
|
|
242
253
|
readonly 'adapter.pluginInstall': '安装插件 {name}@{version}';
|
|
@@ -246,6 +257,9 @@ export declare const zh: {
|
|
|
246
257
|
readonly 'adapter.pluginBundleMember': '(bundle 成员,经聚合包安装)';
|
|
247
258
|
readonly 'adapter.pluginInstallOk': '插件 {name} 已安装{suffix}';
|
|
248
259
|
readonly 'adapter.pluginUpdateTriggered': '插件 {name} 已触发更新(npm 最新版){suffix}';
|
|
260
|
+
readonly 'adapter.pluginInstalledAtVersion': '插件 {name} 已安装到 {version}{suffix}';
|
|
261
|
+
readonly 'adapter.pluginVersionMismatch': '插件 {name} 安装后版本为 {actual},与备份记录的 {expected} 不一致(该版本可能已从 npm 撤版);可手动执行:{command}';
|
|
262
|
+
readonly 'adapter.skillBundledSkipped': '技能 {name} 由插件内置提供(bundled),备份里的副本不完整,已跳过;升级对应插件即可获得最新版本';
|
|
249
263
|
readonly 'adapter.pluginRestartSuffix': ',重启 dsh 后生效';
|
|
250
264
|
readonly 'adapter.pluginHostBundleDeclare': '声明插件层 {name}(由 DSH 自带提供,不安装)';
|
|
251
265
|
readonly 'adapter.pluginHostBundleSame': '插件层 {name} 已在目标机声明(由 DSH 自带提供)';
|
|
@@ -384,6 +398,12 @@ export declare const zh: {
|
|
|
384
398
|
readonly 'sync.workdirNotConfigured': '尚未配置 git 仓库地址,无法重建工作副本。';
|
|
385
399
|
readonly 'sync.workdirRelinked': '工作副本的 origin 已重指向当前仓库地址(工作树与本地提交未改动)。';
|
|
386
400
|
readonly 'sync.workdirRecloned': '工作副本已重新克隆(原有本地提交已丢弃,远端快照已同步回本地副本)。';
|
|
401
|
+
readonly 'sync.git.adoptDirty': '工作副本 {dir} 里有未提交的改动:请先提交或撤销这些改动,再采纳远端历史(否则冲突会把你的改动卡住)。';
|
|
402
|
+
readonly 'sync.git.adoptDetached': '工作副本 {dir} 当前不在任何分支上(detached HEAD),无法采纳远端历史。请先切回一个分支(git switch <分支名>)后重试。';
|
|
403
|
+
readonly 'sync.git.adoptNoRemoteBranch': '远端没有名为 {branch} 的分支(远端现有:{heads}),未做任何合并。请先把仓库地址/分支改对,或改用「重新克隆工作副本」。';
|
|
404
|
+
readonly 'sync.git.adoptNotSyncRepo': '远端仓库顶层出现了快照布局以外的内容({entries}):它看起来不是本插件的同步仓库,已拒绝合并(不会把别的仓库内容搬进工作副本)。';
|
|
405
|
+
readonly 'sync.git.adoptMergeConflict': '合并远端 {branch} 时发生冲突:已撤销这次合并(merge --abort),工作副本回到操作前的状态。请手工解决冲突后重试,或改用「重新克隆工作副本」。';
|
|
406
|
+
readonly 'sync.workdirModeInvalidFull': '重建工作副本的模式非法: {mode}(只允许 relink(重指向地址)、adopt(采纳远端历史)或 reclone(重新克隆))';
|
|
387
407
|
readonly 'sync.git.cmdFailed': 'git {args} 失败 (exit {code}): {err}';
|
|
388
408
|
readonly 'sync.git.pushTokenRequired': '推送需要访问令牌:远端是 http(s) 仓库,但本机没有可用的令牌(拉取公开仓库可以匿名完成,推送必须认证)。请在「同步 → 通道配置」里登录 GitHub 或填入访问令牌后重试。';
|
|
389
409
|
readonly 'sync.git.pushAuthFailed': '推送认证失败:{err}。请确认令牌有效且对该仓库有写权限(账号密码推送已被远端禁用);拉取成功不代表令牌可用 —— 公开仓库匿名即可拉取。可在「同步 → 通道配置」里重新登录或更换令牌。';
|
package/lib/core/messages.js
CHANGED
|
@@ -228,6 +228,17 @@ export const zh = {
|
|
|
228
228
|
'adapter.mcpCreate': '创建 MCP server {serverName}',
|
|
229
229
|
'adapter.mcpSame': 'MCP server {serverName} 已一致',
|
|
230
230
|
'adapter.mcpDiff': 'MCP server {serverName} 与目标不同',
|
|
231
|
+
'adapter.mcpConnectorReadFailed': '无法读取 MCP 连接器存储({reason}),本次备份不含连接器配置',
|
|
232
|
+
'adapter.mcpConnectorTargetUnreadable': '目标机的 MCP 连接器存储无法解析({reason}),已跳过连接器导入,原文件未改动',
|
|
233
|
+
'adapter.mcpConnectorNeedsRefill': 'MCP 连接器「{name}」含需重填的凭据,未导入',
|
|
234
|
+
'adapter.mcpConnectorCreate': '新增 MCP 连接器「{name}」',
|
|
235
|
+
'adapter.mcpConnectorSame': 'MCP 连接器「{name}」已存在且一致',
|
|
236
|
+
'adapter.mcpConnectorDiff': 'MCP 连接器「{name}」与目标机不同',
|
|
237
|
+
'adapter.mcpConnectorDuplicateName': 'server「{serverName}」同时来自 patch 行与连接器,两份都会保留(不合并)',
|
|
238
|
+
'adapter.mcpConnectorOauth': 'OAuth 连接器「{name}」需在市场中重新授权',
|
|
239
|
+
'adapter.mcpConnectorMissing': '备份中找不到 MCP 连接器「{name}」',
|
|
240
|
+
'adapter.mcpConnectorWritten': '已写入 MCP 连接器「{name}」,重启 DSH 后生效',
|
|
241
|
+
'adapter.mcpConnectorInvalid': 'MCP 连接器条目缺少 key 或 serverName',
|
|
231
242
|
'adapter.pwCreate': '写入 pnpm-workspace.yaml(插件构建白名单/冷静期配置)',
|
|
232
243
|
'adapter.pwUpdate': '更新 pnpm-workspace.yaml(插件构建白名单/冷静期配置)',
|
|
233
244
|
'adapter.pluginInstall': '安装插件 {name}@{version}',
|
|
@@ -237,6 +248,9 @@ export const zh = {
|
|
|
237
248
|
'adapter.pluginBundleMember': '(bundle 成员,经聚合包安装)',
|
|
238
249
|
'adapter.pluginInstallOk': '插件 {name} 已安装{suffix}',
|
|
239
250
|
'adapter.pluginUpdateTriggered': '插件 {name} 已触发更新(npm 最新版){suffix}',
|
|
251
|
+
'adapter.pluginInstalledAtVersion': '插件 {name} 已安装到 {version}{suffix}',
|
|
252
|
+
'adapter.pluginVersionMismatch': '插件 {name} 安装后版本为 {actual},与备份记录的 {expected} 不一致(该版本可能已从 npm 撤版);可手动执行:{command}',
|
|
253
|
+
'adapter.skillBundledSkipped': '技能 {name} 由插件内置提供(bundled),备份里的副本不完整,已跳过;升级对应插件即可获得最新版本',
|
|
240
254
|
'adapter.pluginRestartSuffix': ',重启 dsh 后生效',
|
|
241
255
|
'adapter.pluginHostBundleDeclare': '声明插件层 {name}(由 DSH 自带提供,不安装)',
|
|
242
256
|
'adapter.pluginHostBundleSame': '插件层 {name} 已在目标机声明(由 DSH 自带提供)',
|
|
@@ -379,6 +393,14 @@ export const zh = {
|
|
|
379
393
|
'sync.workdirNotConfigured': '尚未配置 git 仓库地址,无法重建工作副本。',
|
|
380
394
|
'sync.workdirRelinked': '工作副本的 origin 已重指向当前仓库地址(工作树与本地提交未改动)。',
|
|
381
395
|
'sync.workdirRecloned': '工作副本已重新克隆(原有本地提交已丢弃,远端快照已同步回本地副本)。',
|
|
396
|
+
// issue #104:换仓库/远端历史被重写后「采纳远端历史」(adopt)的可操作文案。
|
|
397
|
+
// 注意:既有键(含上面的 historyDiverged / workdirModeInvalid)有冻结指纹,只许追加、不许改值。
|
|
398
|
+
'sync.git.adoptDirty': '工作副本 {dir} 里有未提交的改动:请先提交或撤销这些改动,再采纳远端历史(否则冲突会把你的改动卡住)。',
|
|
399
|
+
'sync.git.adoptDetached': '工作副本 {dir} 当前不在任何分支上(detached HEAD),无法采纳远端历史。请先切回一个分支(git switch <分支名>)后重试。',
|
|
400
|
+
'sync.git.adoptNoRemoteBranch': '远端没有名为 {branch} 的分支(远端现有:{heads}),未做任何合并。请先把仓库地址/分支改对,或改用「重新克隆工作副本」。',
|
|
401
|
+
'sync.git.adoptNotSyncRepo': '远端仓库顶层出现了快照布局以外的内容({entries}):它看起来不是本插件的同步仓库,已拒绝合并(不会把别的仓库内容搬进工作副本)。',
|
|
402
|
+
'sync.git.adoptMergeConflict': '合并远端 {branch} 时发生冲突:已撤销这次合并(merge --abort),工作副本回到操作前的状态。请手工解决冲突后重试,或改用「重新克隆工作副本」。',
|
|
403
|
+
'sync.workdirModeInvalidFull': '重建工作副本的模式非法: {mode}(只允许 relink(重指向地址)、adopt(采纳远端历史)或 reclone(重新克隆))',
|
|
382
404
|
'sync.git.cmdFailed': 'git {args} 失败 (exit {code}): {err}',
|
|
383
405
|
// 推送认证(真机:公开仓库拉取可匿名成功、推送必失败,git 原文只说「Invalid username or token」)
|
|
384
406
|
'sync.git.pushTokenRequired': '推送需要访问令牌:远端是 http(s) 仓库,但本机没有可用的令牌(拉取公开仓库可以匿名完成,推送必须认证)。请在「同步 → 通道配置」里登录 GitHub 或填入访问令牌后重试。',
|
|
@@ -676,6 +698,17 @@ export const en = {
|
|
|
676
698
|
'adapter.mcpCreate': 'Create MCP server {serverName}',
|
|
677
699
|
'adapter.mcpSame': 'MCP server {serverName} is already identical',
|
|
678
700
|
'adapter.mcpDiff': 'MCP server {serverName} differs from the target',
|
|
701
|
+
'adapter.mcpConnectorReadFailed': 'Could not read the MCP connector store ({reason}); this backup has no connector config',
|
|
702
|
+
'adapter.mcpConnectorTargetUnreadable': 'The target MCP connector store cannot be parsed ({reason}); connector import skipped and the file left untouched',
|
|
703
|
+
'adapter.mcpConnectorNeedsRefill': 'MCP connector "{name}" contains credentials that must be re-entered; not imported',
|
|
704
|
+
'adapter.mcpConnectorCreate': 'Add MCP connector "{name}"',
|
|
705
|
+
'adapter.mcpConnectorSame': 'MCP connector "{name}" already exists and matches',
|
|
706
|
+
'adapter.mcpConnectorDiff': 'MCP connector "{name}" differs from the target',
|
|
707
|
+
'adapter.mcpConnectorDuplicateName': 'Server "{serverName}" comes from both a patch line and a connector; both are kept (never merged)',
|
|
708
|
+
'adapter.mcpConnectorOauth': 'OAuth connector "{name}" must be re-authorized in the marketplace',
|
|
709
|
+
'adapter.mcpConnectorMissing': 'MCP connector "{name}" not found in the backup',
|
|
710
|
+
'adapter.mcpConnectorWritten': 'MCP connector "{name}" written; takes effect after restarting DSH',
|
|
711
|
+
'adapter.mcpConnectorInvalid': 'MCP connector entry is missing key or serverName',
|
|
679
712
|
'adapter.pwCreate': 'Write pnpm-workspace.yaml (plugin build whitelist / release-age settings)',
|
|
680
713
|
'adapter.pwUpdate': 'Update pnpm-workspace.yaml (plugin build whitelist / release-age settings)',
|
|
681
714
|
'adapter.pluginInstall': 'Install plugin {name}@{version}',
|
|
@@ -685,6 +718,9 @@ export const en = {
|
|
|
685
718
|
'adapter.pluginBundleMember': ' (bundle member, installed via the aggregate package)',
|
|
686
719
|
'adapter.pluginInstallOk': 'Plugin {name} installed{suffix}',
|
|
687
720
|
'adapter.pluginUpdateTriggered': 'Plugin {name} update triggered (npm latest){suffix}',
|
|
721
|
+
'adapter.pluginInstalledAtVersion': 'Plugin {name} installed at {version}{suffix}',
|
|
722
|
+
'adapter.pluginVersionMismatch': 'Plugin {name} is at {actual} after install, not the backed-up {expected} (that version may have been unpublished); run manually: {command}',
|
|
723
|
+
'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',
|
|
688
724
|
'adapter.pluginRestartSuffix': '; takes effect after restarting dsh',
|
|
689
725
|
'adapter.pluginHostBundleDeclare': 'Declare plugin layer {name} (provided by DSH itself; not installed)',
|
|
690
726
|
'adapter.pluginHostBundleSame': 'Plugin layer {name} is already declared on the target (provided by DSH itself)',
|
|
@@ -827,6 +863,14 @@ export const en = {
|
|
|
827
863
|
'sync.workdirNotConfigured': 'No git repository address is configured, so the working copy cannot be rebuilt.',
|
|
828
864
|
'sync.workdirRelinked': 'The working copy origin now points at the configured repository address (working tree and local commits untouched).',
|
|
829
865
|
'sync.workdirRecloned': 'The working copy was cloned again (local commits were discarded; remote snapshots are back in the local copy).',
|
|
866
|
+
// issue #104: actionable text for adopting remote history (adopt) after a repository swap / rewritten history.
|
|
867
|
+
// Existing keys (including historyDiverged / workdirModeInvalid above) have frozen fingerprints: append only.
|
|
868
|
+
'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).',
|
|
869
|
+
'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.',
|
|
870
|
+
'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".',
|
|
871
|
+
'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).',
|
|
872
|
+
'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".',
|
|
873
|
+
'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).',
|
|
830
874
|
'sync.git.cmdFailed': 'git {args} failed (exit {code}): {err}',
|
|
831
875
|
'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.',
|
|
832
876
|
'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/lib/core/phase3-host.js
CHANGED
|
@@ -501,6 +501,15 @@ export class Phase3Recovery {
|
|
|
501
501
|
}
|
|
502
502
|
await this.store.update(opId, (j) => {
|
|
503
503
|
let next = j;
|
|
504
|
+
// ext step 必须随 op 成功一起结算:此前 :556 写入的 `status:'planned'` 永远不会被更新,
|
|
505
|
+
// ⇒ 一条 COMMITTED 的 journal 里躺着「计划做、没做过」的步骤。后果有两个:
|
|
506
|
+
// ① 审计不可信(recovery 面板把 external step 的 planned 当「未执行的意图」转发给用户);
|
|
507
|
+
// ② issue #87 的 `selfProvableResidual` 用「所有 step 都是 planned 且 appliedAt 为空」来
|
|
508
|
+
// 证明「副作用从未开始」—— 成功 op 落在 completed/ 不参与判定,但同一形状一旦被别的
|
|
509
|
+
// 路径读到就会被误读成「残留」。结算成 done + appliedAt 后形状语义唯一。
|
|
510
|
+
if (next.steps.ext !== undefined) {
|
|
511
|
+
next = { ...next, steps: { ...next.steps, ext: { ...next.steps.ext, status: 'done', appliedAt: new Date().toISOString() } } };
|
|
512
|
+
}
|
|
504
513
|
if (next.state === 'SNAPSHOT_CREATED')
|
|
505
514
|
next = transitionJournalState(next, 'APPLYING');
|
|
506
515
|
if (next.state === 'APPLYING')
|
|
@@ -524,7 +533,15 @@ export class Phase3Recovery {
|
|
|
524
533
|
else {
|
|
525
534
|
const j = await this.store.loadActive(opId);
|
|
526
535
|
if (j !== null && !isTerminalState(j.state)) {
|
|
527
|
-
await this.store.update(opId, (cur) =>
|
|
536
|
+
await this.store.update(opId, (cur) => {
|
|
537
|
+
// 失败时 ext step 不能停在 planned:planned 的含义是「还没开始」,而外部操作已经
|
|
538
|
+
// 尝试过了(可能半途炸掉)。落成 attention 让 journal 自己说明「外部步骤结论未知」,
|
|
539
|
+
// 而不是留给读取方去猜。appliedAt 保持 null(没有可证明的完成时刻)。
|
|
540
|
+
const withStep = cur.steps.ext !== undefined && cur.steps.ext.status === 'planned'
|
|
541
|
+
? { ...cur, steps: { ...cur.steps, ext: { ...cur.steps.ext, status: 'attention' } } }
|
|
542
|
+
: cur;
|
|
543
|
+
return transitionJournalState(withStep, 'NEEDS_ATTENTION');
|
|
544
|
+
}).catch(() => undefined);
|
|
528
545
|
}
|
|
529
546
|
}
|
|
530
547
|
throw err;
|
package/lib/core/plugin-cli.d.ts
CHANGED
|
@@ -144,8 +144,28 @@ export declare function mergeBundleDeclaration(pkgText: string, name: string): {
|
|
|
144
144
|
text: string;
|
|
145
145
|
changed: boolean;
|
|
146
146
|
} | null;
|
|
147
|
-
/**
|
|
148
|
-
export
|
|
147
|
+
/** `pluginInstallTarget()` 的输入:全部来自备份里的 `PluginEntry`(都可缺省 = 旧备份)。 */
|
|
148
|
+
export interface PluginInstallTargetInput {
|
|
149
|
+
/** 备份记录的实际安装版本(`PluginEntry.version`):具体 semver 时精确安装到它(issue #101)。 */
|
|
150
|
+
version?: string | undefined;
|
|
151
|
+
/** 备份记录的声明依赖 spec(`PluginEntry.spec`):非 registry 来源原样,registry 区间带包名。 */
|
|
152
|
+
spec?: string | undefined;
|
|
153
|
+
/** 备份内自带 tarball 解包后的绝对路径:优先于 spec(T1 本地源插件迁移)。 */
|
|
154
|
+
tarballPath?: string | undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 安装目标:可直接交给 `dsh plugin add` 的**完整参数**(issue #101)。
|
|
158
|
+
*
|
|
159
|
+
* 为什么不能只传裸包名:pnpm add 会尊重目标机 `package.json` 里既有的 spec —— 固定版不动、
|
|
160
|
+
* 已经满足的区间也不动,于是「备份里记着 0.9.5、目标机装着 0.9.3」的升级会**空转却报成功**。
|
|
161
|
+
* 规则按优先级排列(`src/core/plugin-cli.test.ts` 表驱动钉住):
|
|
162
|
+
* ① 备份内 tarball → `file:<abs>`(换机后原绝对路径必然失效);
|
|
163
|
+
* ② 非 registry 来源(github:/git+/file:/link:)→ spec 原样(裸包名在 registry 上查不到);
|
|
164
|
+
* ③ `version` 是具体 semver → `<pkg>@<version>`(精确版本,pnpm 无法被既有 spec 挡住);
|
|
165
|
+
* ④ 其余 spec(`^1.2.3` / dist-tag)→ `<pkg>@<spec>`(交给 pnpm 解析,至少不会退回裸包名);
|
|
166
|
+
* ⑤ 什么都没有(旧备份 / 无版本记录)→ 裸包名(npm 最新版,改造前行为)。
|
|
167
|
+
*/
|
|
168
|
+
export declare function pluginInstallTarget(pkg: string, input?: PluginInstallTargetInput): string;
|
|
149
169
|
/** 一次 CLI 调用的启动描述(env 只在需要覆写子进程环境时出现)。 */
|
|
150
170
|
export interface DshCliArgv {
|
|
151
171
|
file: string;
|
package/lib/core/plugin-cli.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 只依赖 node 内置模块,零 DSH 运行时依赖(M5 的 TDD 可直接单测本模块)。
|
|
16
16
|
*/
|
|
17
17
|
import { engineMsg } from './messages-core.js';
|
|
18
|
+
import { isConcreteVersion } from './validator.js';
|
|
18
19
|
import { spawn } from 'node:child_process';
|
|
19
20
|
import { readFileSync, rmSync } from 'node:fs';
|
|
20
21
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
@@ -405,13 +406,31 @@ const INSTALL_TIMEOUT_MS = Number(process.env.DSH_CONFIG_MANAGER_INSTALL_TIMEOUT
|
|
|
405
406
|
/**
|
|
406
407
|
* 非 registry 来源的依赖 spec 前缀:这些来源必须把 spec 原样交给 pnpm add,
|
|
407
408
|
* 裸包名在 registry 上查不到(fetch-404 → 幽灵依赖拖垮整个 profile 的安装)。
|
|
408
|
-
*
|
|
409
|
+
* registry 包由 `pluginInstallTarget()` 拼精确版本或「包名@spec」(issue #101)。
|
|
409
410
|
*/
|
|
410
411
|
const NON_REGISTRY_SPEC = /^(github:|gitlab:|bitbucket:|git\+|file:|link:|workspace:|https?:)/i;
|
|
411
|
-
/**
|
|
412
|
-
|
|
412
|
+
/**
|
|
413
|
+
* 安装目标:可直接交给 `dsh plugin add` 的**完整参数**(issue #101)。
|
|
414
|
+
*
|
|
415
|
+
* 为什么不能只传裸包名:pnpm add 会尊重目标机 `package.json` 里既有的 spec —— 固定版不动、
|
|
416
|
+
* 已经满足的区间也不动,于是「备份里记着 0.9.5、目标机装着 0.9.3」的升级会**空转却报成功**。
|
|
417
|
+
* 规则按优先级排列(`src/core/plugin-cli.test.ts` 表驱动钉住):
|
|
418
|
+
* ① 备份内 tarball → `file:<abs>`(换机后原绝对路径必然失效);
|
|
419
|
+
* ② 非 registry 来源(github:/git+/file:/link:)→ spec 原样(裸包名在 registry 上查不到);
|
|
420
|
+
* ③ `version` 是具体 semver → `<pkg>@<version>`(精确版本,pnpm 无法被既有 spec 挡住);
|
|
421
|
+
* ④ 其余 spec(`^1.2.3` / dist-tag)→ `<pkg>@<spec>`(交给 pnpm 解析,至少不会退回裸包名);
|
|
422
|
+
* ⑤ 什么都没有(旧备份 / 无版本记录)→ 裸包名(npm 最新版,改造前行为)。
|
|
423
|
+
*/
|
|
424
|
+
export function pluginInstallTarget(pkg, input = {}) {
|
|
425
|
+
const { version, spec, tarballPath } = input;
|
|
426
|
+
if (tarballPath !== undefined && tarballPath !== '')
|
|
427
|
+
return `file:${tarballPath}`;
|
|
413
428
|
if (spec !== undefined && spec !== '' && NON_REGISTRY_SPEC.test(spec))
|
|
414
429
|
return spec;
|
|
430
|
+
if (version !== undefined && isConcreteVersion(version))
|
|
431
|
+
return `${pkg}@${version}`;
|
|
432
|
+
if (spec !== undefined && spec !== '')
|
|
433
|
+
return `${pkg}@${spec}`;
|
|
415
434
|
return pkg;
|
|
416
435
|
}
|
|
417
436
|
/** Windows 的 npm/corepack/pnpm/dsh 都是 .cmd 垫片:Node 直接 spawn 无法启动(ENOENT/EINVAL)。 */
|
|
@@ -56,11 +56,13 @@ export interface RecoveryOrchestratorDeps {
|
|
|
56
56
|
/**
|
|
57
57
|
* 只读环境锁状态(issue #31)。**由宿主注入而非本模块 import**:本模块不依赖 env-lock,
|
|
58
58
|
* 无锁环境(测试 mock 端口)可返回保守的 UNKNOWN_STATE。
|
|
59
|
-
* detail 含 owner pid/op
|
|
59
|
+
* detail / ownerInstanceId 含 owner pid/op/实例标识等内部诊断 → 只用于日志与
|
|
60
|
+
* **本模块内部**的 journal↔锁归属匹配,绝不进响应体。
|
|
60
61
|
*/
|
|
61
62
|
inspectLockState: () => Promise<{
|
|
62
63
|
state: string;
|
|
63
64
|
detail?: string;
|
|
65
|
+
ownerInstanceId?: string;
|
|
64
66
|
}>;
|
|
65
67
|
/**
|
|
66
68
|
* 显式回收 stale 残留锁(EnvironmentLockManager.recoverStaleLock)。
|
|
@@ -27,6 +27,37 @@ import { engineMsg } from './messages-core.js';
|
|
|
27
27
|
export function createRecoveryOrchestrator(deps) {
|
|
28
28
|
const { store, runs, snapshotsDir, host, snapshotExists, getEnvironmentFingerprint, clearSafeMode, inspectLockState, recoverStaleLock } = deps;
|
|
29
29
|
const safeModeBlocked = deps.safeModeBlocked ?? (() => false);
|
|
30
|
+
/**
|
|
31
|
+
* 该 active journal 是否属于「正在正常执行中的外部操作」(in-flight),而不是
|
|
32
|
+
* 「一次未正常完成的操作」(incident)。
|
|
33
|
+
*
|
|
34
|
+
* 为什么必须区分:autosync push 的 journal 在整个执行期间存在于 `transactions/active/`
|
|
35
|
+
* (state=CREATED→APPLYING,成功后直接 moveToCompleted)。旧 status() 把所有非 terminal
|
|
36
|
+
* 的 active journal 一律当 incident 上报 ⇒ 正在跑的 push 被显示成「需要人工处理」的事故,
|
|
37
|
+
* 顶部冒出「配置修改已被保护」+「已进入安全模式」,而这类 journal 没有 snapshotId、
|
|
38
|
+
* decision 恒为 needs-attention ⇒ 「执行恢复」永久灰;唯一出口 dismiss 又要 GLOBAL 锁,
|
|
39
|
+
* 而锁正被同一个操作持着 ⇒ 点一次 423 一次。用户界面上是个纯粹的死胡同。
|
|
40
|
+
*
|
|
41
|
+
* 判据三合一,任一不成立就保守地仍按 incident 上报:
|
|
42
|
+
* 1. state 非 terminal 且非 NEEDS_ATTENTION(后者是明确的「需人工处理」);
|
|
43
|
+
* 2. 环境锁 state === 'LOCKED'(另一任务活跃持有;STALE/UNKNOWN 都不可当作「有人在跑」);
|
|
44
|
+
* 3. journal.ownerInstanceId === 锁持有者的 owner.instanceId —— 证明持锁的实例**就是**
|
|
45
|
+
* 写这条 journal 的实例,即它正在跑,而非上次崩溃的残骸。
|
|
46
|
+
*
|
|
47
|
+
* 不用时间窗启发式:op 执行期间 journal 本来就可能长时间不写(外部步骤无中途落盘),
|
|
48
|
+
* 任何「多久没更新算卡住」的阈值都会误伤慢速 push。
|
|
49
|
+
*/
|
|
50
|
+
const isLiveInFlight = (j, lock) => {
|
|
51
|
+
if (lock === undefined || lock.state !== 'LOCKED')
|
|
52
|
+
return false;
|
|
53
|
+
if (isTerminalState(j.state) || j.state === 'NEEDS_ATTENTION')
|
|
54
|
+
return false;
|
|
55
|
+
const holder = lock.ownerInstanceId;
|
|
56
|
+
if (holder === undefined || holder === '')
|
|
57
|
+
return false;
|
|
58
|
+
const owner = j.ownerInstanceId;
|
|
59
|
+
return owner !== null && owner !== '' && owner === holder;
|
|
60
|
+
};
|
|
30
61
|
/**
|
|
31
62
|
* 只读 recovery decision(不修改 journal)。**不用 reconcileActive**:其 §6.5 硬门控会把
|
|
32
63
|
* RECOVERING journal 迁移到 NEEDS_ATTENTION,若 status 轮询时触发会破坏 execute→verify 流程。
|
|
@@ -128,14 +159,44 @@ export function createRecoveryOrchestrator(deps) {
|
|
|
128
159
|
// 「解除安全模式」入口,而不是显示「暂无需要处理的恢复事项」然后继续 423。
|
|
129
160
|
const blocked = safeModeBlocked();
|
|
130
161
|
const clearable = blocked ? await resolveSafeMode(true) : false;
|
|
162
|
+
// issue #31:附带环境锁分类。**只暴露分类,不暴露 owner pid/op/hostname**(内部诊断)。
|
|
163
|
+
// attention 只对「需用户显式处理」的状态为 true:STALE_LOCK_DETECTED(残留锁,重试不会自愈)
|
|
164
|
+
// 与 UNKNOWN_STATE(无法判定)→ 引导用户回收;LOCKED(另一任务活跃持有)会自行释放,
|
|
165
|
+
// 不该催用户去「回收」,否则又是一个误导。探测失败 → 保守 UNKNOWN_STATE + attention。
|
|
166
|
+
//
|
|
167
|
+
// 探测必须**先于** incident 分类:LOCKED 且持有者 == journal.ownerInstanceId 时该 journal
|
|
168
|
+
// 是「正在跑」,不该进 incidents(解释见 isLiveInFlight)。
|
|
169
|
+
let lock;
|
|
170
|
+
let lockInsp;
|
|
171
|
+
try {
|
|
172
|
+
const insp = await inspectLockState();
|
|
173
|
+
lockInsp = insp.ownerInstanceId !== undefined
|
|
174
|
+
? { state: insp.state, ownerInstanceId: insp.ownerInstanceId }
|
|
175
|
+
: { state: insp.state };
|
|
176
|
+
lock = {
|
|
177
|
+
state: insp.state,
|
|
178
|
+
attention: insp.state === 'STALE_LOCK_DETECTED' || insp.state === 'UNKNOWN_STATE',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
lock = { state: 'UNKNOWN_STATE', attention: true };
|
|
183
|
+
}
|
|
131
184
|
const activeIds = await store.scanActive();
|
|
132
185
|
const incidents = [];
|
|
186
|
+
/** 正在正常执行的操作(不是 incident):面板据此解释「为什么现在什么都点不动」。 */
|
|
187
|
+
const live = [];
|
|
133
188
|
for (const opId of activeIds) {
|
|
134
189
|
const j = await store.loadActive(opId);
|
|
135
190
|
if (j === null)
|
|
136
191
|
continue;
|
|
137
192
|
if (isTerminalState(j.state) && j.state !== 'NEEDS_ATTENTION')
|
|
138
193
|
continue; // COMMITTED/ROLLED_BACK/RECOVERED → 非 incident
|
|
194
|
+
// 正在跑的操作:既不进 incidents(否则伪装成事故 + 假 SAFE MODE 横幅),
|
|
195
|
+
// 也不进 running(那是 recovery run),单列进 live 供界面如实说明。
|
|
196
|
+
if (isLiveInFlight(j, lockInsp)) {
|
|
197
|
+
live.push({ operationId: opId, operationType: j.operationType, state: j.state, startedAt: j.createdAt });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
139
200
|
const d = await decision(j);
|
|
140
201
|
incidents.push({
|
|
141
202
|
operationId: opId,
|
|
@@ -148,24 +209,10 @@ export function createRecoveryOrchestrator(deps) {
|
|
|
148
209
|
});
|
|
149
210
|
}
|
|
150
211
|
const running = runs.listActive().filter((r) => r.kind === 'recovery').map((r) => ({ runId: r.runId, status: r.status }));
|
|
151
|
-
// issue #31:附带环境锁分类。**只暴露分类,不暴露 owner pid/op/hostname**(内部诊断)。
|
|
152
|
-
// attention 只对「需用户显式处理」的状态为 true:STALE_LOCK_DETECTED(残留锁,重试不会自愈)
|
|
153
|
-
// 与 UNKNOWN_STATE(无法判定)→ 引导用户回收;LOCKED(另一任务活跃持有)会自行释放,
|
|
154
|
-
// 不该催用户去「回收」,否则又是一个误导。探测失败 → 保守 UNKNOWN_STATE + attention。
|
|
155
|
-
let lock;
|
|
156
|
-
try {
|
|
157
|
-
const insp = await inspectLockState();
|
|
158
|
-
lock = {
|
|
159
|
-
state: insp.state,
|
|
160
|
-
attention: insp.state === 'STALE_LOCK_DETECTED' || insp.state === 'UNKNOWN_STATE',
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
lock = { state: 'UNKNOWN_STATE', attention: true };
|
|
165
|
-
}
|
|
166
212
|
// issue #56:SAFE MODE 阻断态如实回传。`clearable` = 「结案但保护仍开着」
|
|
167
213
|
// (durable 标记在、没有任何未解决 incident)—— 界面据此渲染显式解除入口。
|
|
168
|
-
|
|
214
|
+
// live 是**加法字段**:旧客户端读不到就按「无进行中操作」处理,不影响既有渲染。
|
|
215
|
+
return { status: 200, body: { incidents, running, live, lock, safeMode: { blocked, clearable } } };
|
|
169
216
|
},
|
|
170
217
|
async preview(operationId) {
|
|
171
218
|
if (!isValidOperationId(operationId))
|
|
@@ -308,6 +355,18 @@ export function createRecoveryOrchestrator(deps) {
|
|
|
308
355
|
const j = await store.loadActive(operationId);
|
|
309
356
|
if (j === null)
|
|
310
357
|
return { status: 404, body: { error: 'operation not found' } };
|
|
358
|
+
// 正在执行的操作不能「放弃」:它不是事故,quarantine 会把在跑的那条 journal 从 active/
|
|
359
|
+
// 抽走,让执行方后续 transition 失败。正常情况下这条路径走不到 —— 路由的 GLOBAL 锁
|
|
360
|
+
// 会先以 423 挡下;这里是防御性判据(锁端口未接线/跨进程判定差异时仍能给出准确原因,
|
|
361
|
+
// 而不是笼统的「另一个任务正在运行」)。
|
|
362
|
+
let lockForDismiss;
|
|
363
|
+
try {
|
|
364
|
+
lockForDismiss = await inspectLockState();
|
|
365
|
+
}
|
|
366
|
+
catch { /* 探测失败 → 不拦(保持旧行为) */ }
|
|
367
|
+
if (isLiveInFlight(j, lockForDismiss)) {
|
|
368
|
+
return { status: 409, body: { error: engineMsg('core.recovery.opInFlight', { state: j.state }), code: 'operation-in-flight' } };
|
|
369
|
+
}
|
|
311
370
|
// dismiss → quarantine(用户放弃;不删除 snapshot/journal evidence,不强行 RECOVERED)
|
|
312
371
|
const result = await executeRecovery(store, { operationId, action: 'dismiss', snapshotId: j.snapshotId }, true);
|
|
313
372
|
if (result === 'failed')
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** 台账文件名(与回滚点库同目录:dataDir/rollback-points/activity.json) */
|
|
2
|
+
export declare const ROLLBACK_ACTIVITY_FILE_NAME = "activity.json";
|
|
3
|
+
/** 同一会话两条活动的间隔 <= 该值就并成一个窗口(缺省 10 分钟) */
|
|
4
|
+
export declare const ROLLBACK_ACTIVITY_DEFAULT_GAP_MS: number;
|
|
5
|
+
/** 窗口条目上限(超出按 to 淘汰最旧)—— 无界增长会把宿主拖垮(见 issue #89 的教训) */
|
|
6
|
+
export declare const ROLLBACK_ACTIVITY_MAX_INTERVALS = 20000;
|
|
7
|
+
/** 一条会话活跃窗口:会话 sessionId 在 [from, to] 之间有过活动 */
|
|
8
|
+
export interface RollbackActivityInterval {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
from: number;
|
|
11
|
+
to: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RollbackActivityOptions {
|
|
14
|
+
/** 台账文件绝对路径(引擎传 dataDir/rollback-points/activity.json) */
|
|
15
|
+
file: string;
|
|
16
|
+
/** 缺省时钟;`noteActivity` 不传 at 时用它(事件时间优先,取数时刻只是兜底) */
|
|
17
|
+
now?: () => number;
|
|
18
|
+
/** 同一会话两条活动间隔 <= gapMs 就合并(缺省 ROLLBACK_ACTIVITY_DEFAULT_GAP_MS) */
|
|
19
|
+
gapMs?: number;
|
|
20
|
+
/** 窗口总条数上限(缺省 ROLLBACK_ACTIVITY_MAX_INTERVALS),超出淘汰最旧的 */
|
|
21
|
+
maxIntervals?: number;
|
|
22
|
+
/** 落盘延迟(毫秒);<= 0 表示不自动落盘(测试用) */
|
|
23
|
+
flushDelayMs?: number;
|
|
24
|
+
/** 读写失败只 warn,绝不抛 */
|
|
25
|
+
warn?: (line: string) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 内存窗口表 + best-effort 落盘。
|
|
29
|
+
*
|
|
30
|
+
* 落盘的作用只有一个:**进程重启后**给旧会话回退时,活跃窗口还在(否则新进程只有本次启动后看到的窗口)。
|
|
31
|
+
* 因此写入是延迟合并的(一次会话事件一行,立即落盘会把 I/O 打成筛子)。
|
|
32
|
+
*/
|
|
33
|
+
export declare class RollbackSessionActivity {
|
|
34
|
+
private readonly table;
|
|
35
|
+
private readonly file;
|
|
36
|
+
private readonly now;
|
|
37
|
+
private readonly gapMs;
|
|
38
|
+
private readonly maxIntervals;
|
|
39
|
+
private readonly flushDelayMs;
|
|
40
|
+
private readonly warn;
|
|
41
|
+
private loadPromise;
|
|
42
|
+
private flushTimer;
|
|
43
|
+
private writing;
|
|
44
|
+
/** 内存变更计数:与 lastFlushedRevision 相等 ⇒ 没有未落盘数据(flush 幂等靠它,不靠时间戳) */
|
|
45
|
+
private revision;
|
|
46
|
+
private lastFlushedRevision;
|
|
47
|
+
constructor(options: RollbackActivityOptions);
|
|
48
|
+
/** 窗口总条数(诊断与测试用;上限见 maxIntervals。等价于「条目」,不是会话数) */
|
|
49
|
+
get size(): number;
|
|
50
|
+
/** 惰性读盘(幂等;读失败 = 空表,只 warn) */
|
|
51
|
+
load(): Promise<void>;
|
|
52
|
+
private read;
|
|
53
|
+
/**
|
|
54
|
+
* 记一次活动。窗口只增不减:与上一条间隔 <= gapMs 就并入同一条窗口,否则新开一条。
|
|
55
|
+
*
|
|
56
|
+
* `at` 缺省取注入时钟(`options.now`):引擎把**事件时间**显式传进来时以它为准
|
|
57
|
+
* (事件时间才是那次活动的真实时刻,取数时刻可能晚很多)。
|
|
58
|
+
* 空会话 id、非有限数字、负数一律**忽略**(fail-closed:宁可少一个窗口,绝不记错归属)。
|
|
59
|
+
*/
|
|
60
|
+
noteActivity(sessionId: string, at?: number): void;
|
|
61
|
+
/** 缺省时钟:注入的 now 抛错或给出非有限值时 fail-closed(返回 NaN,由调用方校验丢弃) */
|
|
62
|
+
private clockNow;
|
|
63
|
+
/** 该会话最后一次活动时刻;没记过 = null(**不是 0**,0 会被误当成有效时间戳) */
|
|
64
|
+
lastActivityAt(sessionId: string): number | null;
|
|
65
|
+
/** 该会话是否有任何窗口(用于区分「确知没活动」与「根本没采到」) */
|
|
66
|
+
hasDataFor(sessionId: string): boolean;
|
|
67
|
+
/** 活跃窗口包含 at(可容忍 toleranceMs,缺省 0)的会话 id,按 id 升序、去重 */
|
|
68
|
+
sessionsActiveAt(at: number, toleranceMs?: number): string[];
|
|
69
|
+
/** 单个会话是否活跃(与 sessionsActiveAt 同口径,不另写一套判定) */
|
|
70
|
+
isActiveAt(sessionId: string, at: number, toleranceMs?: number): boolean;
|
|
71
|
+
/** 延迟落盘(多次调用只落一次) */
|
|
72
|
+
scheduleFlush(): void;
|
|
73
|
+
/** 内存快照(深拷一层:落盘期间的新活动不得改写正在序列化的数据) */
|
|
74
|
+
private snapshot;
|
|
75
|
+
/** 淘汰最旧的窗口(按 to 升序删到上限内;条目 = 窗口,不是会话) */
|
|
76
|
+
private evictOldest;
|
|
77
|
+
/**
|
|
78
|
+
* 立刻落盘(原子:同目录临时文件 + rename;失败只 warn,绝不上抛)。
|
|
79
|
+
*
|
|
80
|
+
* 幂等:没有未落盘数据(revision == lastFlushedRevision)时**直接返回、不碰磁盘**;
|
|
81
|
+
* 并发调用共用同一个在飞写入。
|
|
82
|
+
*/
|
|
83
|
+
flush(): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* 关闭:取消待落盘的定时器,并把还没落盘的活动写下去。
|
|
86
|
+
* 没有未落盘数据时不会重复写(flush 幂等);写入失败只 warn,不透出(关停路径不得被台账拖垮)。
|
|
87
|
+
*/
|
|
88
|
+
dispose(): Promise<void>;
|
|
89
|
+
}
|