@lesliechan721/aw 0.1.0-beta.10 → 0.1.0-beta.12
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/dist/authoring/assets/aw-config.d.mts +1 -50
- package/dist/authoring/assets/aw-config.mjs +58 -928
- package/dist/authoring/assets/aw-config.mjs.map +1 -1
- package/dist/authoring/scripts/init-plugin.mjs +28 -33
- package/dist/authoring/scripts/init-plugin.mjs.map +1 -1
- package/dist/authoring/scripts/lib/plugin-version-check.d.mts +1 -0
- package/dist/authoring/scripts/lib/plugin-version-check.mjs +229 -8
- package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -1
- package/dist/authoring/scripts/validate-plugin.mjs +159 -48
- package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
- package/dist/scripts/bundle-plugins.js +18 -4
- package/dist/scripts/bundle-plugins.js.map +1 -1
- package/dist/scripts/test-plugin-scripts.d.ts +2 -0
- package/dist/scripts/test-plugin-scripts.js +21 -5
- package/dist/scripts/test-plugin-scripts.js.map +1 -1
- package/dist/src/commands/aw-capabilities.d.ts +1 -1
- package/dist/src/commands/aw-capabilities.js +1 -1
- package/dist/src/commands/aw-capabilities.js.map +1 -1
- package/dist/src/commands/catalog-dev-source.d.ts +6 -3
- package/dist/src/commands/catalog-dev-source.js +96 -35
- package/dist/src/commands/catalog-dev-source.js.map +1 -1
- package/dist/src/commands/catalog-publication-inspection.d.ts +1 -1
- package/dist/src/commands/catalog-publication-inspection.js +54 -8
- package/dist/src/commands/catalog-publication-inspection.js.map +1 -1
- package/dist/src/commands/catalog-publication-journal.js +1 -1
- package/dist/src/commands/catalog-publication-journal.js.map +1 -1
- package/dist/src/commands/catalog-source.d.ts +19 -4
- package/dist/src/commands/catalog-source.js +121 -12
- package/dist/src/commands/catalog-source.js.map +1 -1
- package/dist/src/commands/catalog-store.d.ts +26 -7
- package/dist/src/commands/catalog-store.js +361 -42
- package/dist/src/commands/catalog-store.js.map +1 -1
- package/dist/src/commands/console.js +28 -17
- package/dist/src/commands/console.js.map +1 -1
- package/dist/src/commands/operation-plan.d.ts +2 -1
- package/dist/src/commands/operation-plan.js +24 -0
- package/dist/src/commands/operation-plan.js.map +1 -1
- package/dist/src/commands/operation-planner.d.ts +2 -2
- package/dist/src/commands/operation-planner.js +22 -20
- package/dist/src/commands/operation-planner.js.map +1 -1
- package/dist/src/commands/options.d.ts +2 -2
- package/dist/src/commands/options.js +18 -4
- package/dist/src/commands/options.js.map +1 -1
- package/dist/src/commands/plugin-capabilities.js +2 -2
- package/dist/src/commands/plugin-capabilities.js.map +1 -1
- package/dist/src/commands/rebuild-command.d.ts +19 -0
- package/dist/src/commands/rebuild-command.js +828 -283
- package/dist/src/commands/rebuild-command.js.map +1 -1
- package/dist/src/commands/rebuild-integration.d.ts +25 -47
- package/dist/src/commands/rebuild-integration.js +1176 -1016
- package/dist/src/commands/rebuild-integration.js.map +1 -1
- package/dist/src/commands/rebuild-operations.d.ts +50 -12
- package/dist/src/commands/rebuild-operations.js +207 -101
- package/dist/src/commands/rebuild-operations.js.map +1 -1
- package/dist/src/commands/status-v6.d.ts +21 -2
- package/dist/src/commands/status-v6.js +163 -31
- package/dist/src/commands/status-v6.js.map +1 -1
- package/dist/src/commands/sync-command.d.ts +17 -0
- package/dist/src/commands/sync-command.js +45 -37
- package/dist/src/commands/sync-command.js.map +1 -1
- package/dist/src/commands/write-commands.d.ts +5 -0
- package/dist/src/commands/write-commands.js +50 -3
- package/dist/src/commands/write-commands.js.map +1 -1
- package/dist/src/generators/local-plugin.js +14 -4
- package/dist/src/generators/local-plugin.js.map +1 -1
- package/dist/src/index.js +260 -91
- package/dist/src/index.js.map +1 -1
- package/dist/src/output/command-receipt.js +1 -3
- package/dist/src/output/command-receipt.js.map +1 -1
- package/dist/src/output/rebuild-progress.js +1 -3
- package/dist/src/output/rebuild-progress.js.map +1 -1
- package/dist/src/portable/materializer.js +10 -8
- package/dist/src/portable/materializer.js.map +1 -1
- package/dist/src/tui/catalog-generation.js +41 -1
- package/dist/src/tui/catalog-generation.js.map +1 -1
- package/dist/src/tui/catalog-sources/session.d.ts +1 -0
- package/dist/src/tui/catalog-sources/session.js +16 -3
- package/dist/src/tui/catalog-sources/session.js.map +1 -1
- package/dist/src/tui/catalog-sources/types.d.ts +3 -2
- package/dist/src/tui/components.d.ts +4 -0
- package/dist/src/tui/components.js +36 -24
- package/dist/src/tui/components.js.map +1 -1
- package/dist/src/tui/console-app-v4.d.ts +13 -4
- package/dist/src/tui/console-app-v4.js +2065 -857
- package/dist/src/tui/console-app-v4.js.map +1 -1
- package/dist/src/tui/controller-runtime.js +731 -62
- package/dist/src/tui/controller-runtime.js.map +1 -1
- package/dist/src/tui/controller.d.ts +156 -10
- package/dist/src/tui/controller.js +1187 -163
- package/dist/src/tui/controller.js.map +1 -1
- package/dist/src/tui/diagnostic-projection.d.ts +22 -0
- package/dist/src/tui/diagnostic-projection.js +437 -0
- package/dist/src/tui/diagnostic-projection.js.map +1 -0
- package/dist/src/tui/diagnostics.d.ts +1 -1
- package/dist/src/tui/diagnostics.js +7 -2
- package/dist/src/tui/diagnostics.js.map +1 -1
- package/dist/src/tui/glossary.d.ts +23 -0
- package/dist/src/tui/glossary.js +42 -0
- package/dist/src/tui/glossary.js.map +1 -0
- package/dist/src/tui/index.d.ts +4 -1
- package/dist/src/tui/index.js +4 -1
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/keys.d.ts +44 -0
- package/dist/src/tui/keys.js +43 -15
- package/dist/src/tui/keys.js.map +1 -1
- package/dist/src/tui/lifecycle-action-view.d.ts +6 -0
- package/dist/src/tui/lifecycle-action-view.js +34 -0
- package/dist/src/tui/lifecycle-action-view.js.map +1 -0
- package/dist/src/tui/model.d.ts +12 -2
- package/dist/src/tui/model.js +128 -52
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/operation-diagnostic-screens.d.ts +21 -0
- package/dist/src/tui/operation-diagnostic-screens.js +101 -0
- package/dist/src/tui/operation-diagnostic-screens.js.map +1 -0
- package/dist/src/tui/operation-history.d.ts +80 -0
- package/dist/src/tui/operation-history.js +130 -0
- package/dist/src/tui/operation-history.js.map +1 -0
- package/dist/src/tui/operation-review-planner.js +366 -99
- package/dist/src/tui/operation-review-planner.js.map +1 -1
- package/dist/src/tui/router.d.ts +82 -17
- package/dist/src/tui/router.js +186 -38
- package/dist/src/tui/router.js.map +1 -1
- package/dist/src/tui/screen-registry.d.ts +546 -0
- package/dist/src/tui/screen-registry.js +179 -0
- package/dist/src/tui/screen-registry.js.map +1 -0
- package/dist/src/tui/session-console-app.d.ts +6 -0
- package/dist/src/tui/session-console-app.js +13 -0
- package/dist/src/tui/session-console-app.js.map +1 -0
- package/dist/src/tui/session-host.d.ts +44 -0
- package/dist/src/tui/session-host.js +197 -0
- package/dist/src/tui/session-host.js.map +1 -0
- package/dist/src/types.d.ts +64 -3
- package/dist/src/types.js +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/agent-version.js +1 -1
- package/dist/src/utils/agent-version.js.map +1 -1
- package/dist/src/utils/artifact-plan.d.ts +2 -2
- package/dist/src/utils/artifact-plan.js +8 -6
- package/dist/src/utils/artifact-plan.js.map +1 -1
- package/dist/src/utils/artifact-renderer.d.ts +2 -1
- package/dist/src/utils/artifact-renderer.js +139 -120
- package/dist/src/utils/artifact-renderer.js.map +1 -1
- package/dist/src/utils/artifact-request.d.ts +5 -0
- package/dist/src/utils/artifact-request.js +40 -0
- package/dist/src/utils/artifact-request.js.map +1 -1
- package/dist/src/utils/bundle-layout.js +4 -4
- package/dist/src/utils/bundle-layout.js.map +1 -1
- package/dist/src/utils/catalog-capability-projection.d.ts +8 -0
- package/dist/src/utils/catalog-capability-projection.js +14 -0
- package/dist/src/utils/catalog-capability-projection.js.map +1 -0
- package/dist/src/utils/catalog-generation-resolver.d.ts +14 -2
- package/dist/src/utils/catalog-generation-resolver.js +49 -27
- package/dist/src/utils/catalog-generation-resolver.js.map +1 -1
- package/dist/src/utils/catalog-generation-service.d.ts +8 -1
- package/dist/src/utils/catalog-generation-service.js +81 -47
- package/dist/src/utils/catalog-generation-service.js.map +1 -1
- package/dist/src/utils/catalog-generation-store.d.ts +1 -0
- package/dist/src/utils/catalog-generation-store.js +60 -5
- package/dist/src/utils/catalog-generation-store.js.map +1 -1
- package/dist/src/utils/catalog-operation-context.js +1 -0
- package/dist/src/utils/catalog-operation-context.js.map +1 -1
- package/dist/src/utils/catalog-public-error.js +4 -0
- package/dist/src/utils/catalog-public-error.js.map +1 -1
- package/dist/src/utils/catalog-source-impact.d.ts +3 -2
- package/dist/src/utils/catalog-source-impact.js +142 -20
- package/dist/src/utils/catalog-source-impact.js.map +1 -1
- package/dist/src/utils/catalog-source-references.js +95 -9
- package/dist/src/utils/catalog-source-references.js.map +1 -1
- package/dist/src/utils/catalog-sources.d.ts +1 -1
- package/dist/src/utils/catalog-sources.js +19 -3
- package/dist/src/utils/catalog-sources.js.map +1 -1
- package/dist/src/utils/codex-manager.js +8 -0
- package/dist/src/utils/codex-manager.js.map +1 -1
- package/dist/src/utils/codex-native-observation.d.ts +34 -1
- package/dist/src/utils/codex-native-observation.js +132 -0
- package/dist/src/utils/codex-native-observation.js.map +1 -1
- package/dist/src/utils/codex-reconciler.d.ts +0 -2
- package/dist/src/utils/codex-reconciler.js.map +1 -1
- package/dist/src/utils/codex-shared-native.d.ts +2 -26
- package/dist/src/utils/codex-shared-native.js +8 -82
- package/dist/src/utils/codex-shared-native.js.map +1 -1
- package/dist/src/utils/git-catalog-provider.d.ts +3 -0
- package/dist/src/utils/git-catalog-provider.js +40 -1
- package/dist/src/utils/git-catalog-provider.js.map +1 -1
- package/dist/src/utils/local-marketplace.d.ts +1 -0
- package/dist/src/utils/local-marketplace.js +69 -73
- package/dist/src/utils/local-marketplace.js.map +1 -1
- package/dist/src/utils/multi-source-catalog.js +1 -0
- package/dist/src/utils/multi-source-catalog.js.map +1 -1
- package/dist/src/utils/native-cli.d.ts +9 -4
- package/dist/src/utils/native-cli.js +280 -34
- package/dist/src/utils/native-cli.js.map +1 -1
- package/dist/src/utils/path-catalog-provider.js +2 -0
- package/dist/src/utils/path-catalog-provider.js.map +1 -1
- package/dist/src/utils/plugin-config-impact.d.ts +33 -0
- package/dist/src/utils/plugin-config-impact.js +298 -0
- package/dist/src/utils/plugin-config-impact.js.map +1 -0
- package/dist/src/utils/plugin-config-write.d.ts +36 -14
- package/dist/src/utils/plugin-config-write.js +709 -225
- package/dist/src/utils/plugin-config-write.js.map +1 -1
- package/dist/src/utils/plugin-config.d.ts +58 -4
- package/dist/src/utils/plugin-config.js +328 -60
- package/dist/src/utils/plugin-config.js.map +1 -1
- package/dist/src/utils/plugin-scripts.d.ts +20 -12
- package/dist/src/utils/plugin-scripts.js +389 -114
- package/dist/src/utils/plugin-scripts.js.map +1 -1
- package/dist/src/utils/process-tree.d.ts +2 -2
- package/dist/src/utils/process-tree.js +1 -1
- package/dist/src/utils/process-tree.js.map +1 -1
- package/dist/src/utils/project-boundary.d.ts +11 -1
- package/dist/src/utils/project-boundary.js +33 -11
- package/dist/src/utils/project-boundary.js.map +1 -1
- package/dist/src/utils/safe-operation-diagnostic.d.ts +15 -0
- package/dist/src/utils/safe-operation-diagnostic.js +33 -0
- package/dist/src/utils/safe-operation-diagnostic.js.map +1 -0
- package/dist/src/utils/sensitive-content.d.ts +4 -0
- package/dist/src/utils/sensitive-content.js +36 -0
- package/dist/src/utils/sensitive-content.js.map +1 -0
- package/dist/src/utils/windows-claude-shim.d.ts +9 -0
- package/dist/src/utils/windows-claude-shim.js +141 -0
- package/dist/src/utils/windows-claude-shim.js.map +1 -0
- package/dist/vitest.config.js +91 -10
- package/dist/vitest.config.js.map +1 -1
- package/package.json +9 -9
- package/dist/authoring/templates/aw-config-runtime.d.mts +0 -2
- package/dist/authoring/templates/aw-config-runtime.mjs +0 -407
- package/dist/authoring/templates/aw-config-runtime.mjs.map +0 -1
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
|
|
3
|
+
import { readSafeOperationError } from '../utils/safe-operation-diagnostic.js';
|
|
3
4
|
import { buildRebuildReceiptJson, requestedIntentAction } from '../output/command-receipt.js';
|
|
4
5
|
import { buildOperationResultModel } from '../commands/operation-planner.js';
|
|
5
6
|
import { buildOperationReviewProjection } from './operation-review-planner.js';
|
|
6
7
|
import { buildTuiViewModel, dependencyPlacementTargets, hasInstalledPlacement, } from './model.js';
|
|
7
|
-
import { safeDiagnosticCode } from './diagnostics.js';
|
|
8
|
+
import { safeDiagnosticCode, sanitizeDiagnosticText } from './diagnostics.js';
|
|
9
|
+
import { projectExecutionDiagnostics, projectThrownOperationDiagnostic } from './diagnostic-projection.js';
|
|
10
|
+
import { OperationHistoryStore, } from './operation-history.js';
|
|
8
11
|
import { targetHasArtifactCollision } from '../utils/target-eligibility.js';
|
|
9
12
|
import { TuiRouter, } from './router.js';
|
|
13
|
+
import { normalizeTuiRoute } from './screen-registry.js';
|
|
10
14
|
import { CatalogSourcesSession } from './catalog-sources/session.js';
|
|
11
15
|
import { catalogSourceAutoCheckOnlyValue, projectCatalogSourceMutationWarnings, } from './catalog-sources/review.js';
|
|
16
|
+
export const PLUGIN_WRITE_BATCH_ACTIONS = [
|
|
17
|
+
'install',
|
|
18
|
+
'sync',
|
|
19
|
+
'reinstall',
|
|
20
|
+
'uninstall',
|
|
21
|
+
'cleanup',
|
|
22
|
+
];
|
|
12
23
|
function reviewPreparationStageLabel(stage) {
|
|
13
24
|
return {
|
|
14
25
|
refresh: '刷新相关来源',
|
|
@@ -48,6 +59,179 @@ function stableValue(value) {
|
|
|
48
59
|
.sort(([left], [right]) => left.localeCompare(right, 'en'))
|
|
49
60
|
.map(([key, entry]) => [key, stableValue(entry)]));
|
|
50
61
|
}
|
|
62
|
+
function canonicalPluginIds(pluginIds) {
|
|
63
|
+
return [...new Set(pluginIds)].sort((left, right) => left.localeCompare(right, 'en'));
|
|
64
|
+
}
|
|
65
|
+
function canonicalBatchTargets(targets) {
|
|
66
|
+
const byKey = new Map();
|
|
67
|
+
for (const target of targets) {
|
|
68
|
+
byKey.set([target.pluginId, target.platform, target.scope].join('\0'), target);
|
|
69
|
+
}
|
|
70
|
+
return [...byKey.values()].sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
|
|
71
|
+
.localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en'));
|
|
72
|
+
}
|
|
73
|
+
function normalizeOperationIntent(intent) {
|
|
74
|
+
if (intent.kind === 'configSave' && intent.retryApply && !intent.apply) {
|
|
75
|
+
return { ...intent, apply: true };
|
|
76
|
+
}
|
|
77
|
+
if (intent.kind !== 'pluginWriteBatch')
|
|
78
|
+
return intent;
|
|
79
|
+
if (intent.selection.kind === 'targets') {
|
|
80
|
+
return {
|
|
81
|
+
...intent,
|
|
82
|
+
selection: { kind: 'targets', targets: canonicalBatchTargets(intent.selection.targets) },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (intent.selection.kind !== 'selected')
|
|
86
|
+
return intent;
|
|
87
|
+
return {
|
|
88
|
+
...intent,
|
|
89
|
+
selection: {
|
|
90
|
+
kind: 'selected',
|
|
91
|
+
pluginIds: canonicalPluginIds(intent.selection.pluginIds),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function assertPluginWriteBatchEvidence(evidence) {
|
|
96
|
+
const candidate = evidence;
|
|
97
|
+
if (!Array.isArray(candidate.requestedPluginIds) || !Array.isArray(candidate.groups)) {
|
|
98
|
+
throw new TuiOperationBlockedError('batchEvidenceInvalid', '批量确认信息不完整。');
|
|
99
|
+
}
|
|
100
|
+
const requestedPluginIds = canonicalPluginIds(candidate.requestedPluginIds);
|
|
101
|
+
if (requestedPluginIds.length === 0
|
|
102
|
+
|| JSON.stringify(requestedPluginIds) !== JSON.stringify(candidate.requestedPluginIds)) {
|
|
103
|
+
throw new TuiOperationBlockedError('batchEvidenceInvalid', '批量根插件集合无效。');
|
|
104
|
+
}
|
|
105
|
+
const requested = new Set(requestedPluginIds);
|
|
106
|
+
const groupIds = new Set();
|
|
107
|
+
const groupedRoots = new Set();
|
|
108
|
+
for (const group of candidate.groups) {
|
|
109
|
+
if (!group || typeof group.groupId !== 'string' || group.groupId.length === 0
|
|
110
|
+
|| typeof group.sourceId !== 'string' || group.sourceId.length === 0
|
|
111
|
+
|| groupIds.has(group.groupId)
|
|
112
|
+
|| !Array.isArray(group.requestedPluginIds)
|
|
113
|
+
|| group.requestedPluginIds.length === 0
|
|
114
|
+
|| JSON.stringify(canonicalPluginIds(group.requestedPluginIds))
|
|
115
|
+
!== JSON.stringify(group.requestedPluginIds)
|
|
116
|
+
|| group.requestedPluginIds.some((pluginId) => !requested.has(pluginId))
|
|
117
|
+
|| group.executable !== Boolean(group.preview?.operationPlan)) {
|
|
118
|
+
throw new TuiOperationBlockedError('batchEvidenceInvalid', '批量分组确认信息无效。');
|
|
119
|
+
}
|
|
120
|
+
groupIds.add(group.groupId);
|
|
121
|
+
group.requestedPluginIds.forEach((pluginId) => groupedRoots.add(pluginId));
|
|
122
|
+
}
|
|
123
|
+
if (requestedPluginIds.some((pluginId) => !groupedRoots.has(pluginId))) {
|
|
124
|
+
throw new TuiOperationBlockedError('batchEvidenceInvalid', '批量分组缺少请求根插件。');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function pluginWriteBatchFailedRequestedTargets(execution) {
|
|
128
|
+
const failed = [];
|
|
129
|
+
for (const group of execution.groupResults) {
|
|
130
|
+
const roots = new Set(group.requestedPluginIds);
|
|
131
|
+
const targetsByPlugin = new Map();
|
|
132
|
+
for (const target of group.result.targetResults) {
|
|
133
|
+
const pluginId = target.target.pluginId;
|
|
134
|
+
if (!pluginId)
|
|
135
|
+
continue;
|
|
136
|
+
const entries = targetsByPlugin.get(pluginId) ?? [];
|
|
137
|
+
entries.push(target);
|
|
138
|
+
targetsByPlugin.set(pluginId, entries);
|
|
139
|
+
}
|
|
140
|
+
const successfulRootKeys = new Set(group.result.targetResults.flatMap((target) => {
|
|
141
|
+
const pluginId = target.target.pluginId;
|
|
142
|
+
return pluginId && roots.has(pluginId)
|
|
143
|
+
&& target.state !== 'failed' && target.action !== 'skipped'
|
|
144
|
+
? [[pluginId, target.target.platform, target.target.scope].join('\0')]
|
|
145
|
+
: [];
|
|
146
|
+
}));
|
|
147
|
+
const unsuccessful = group.result.targetResults.filter((target) => target.state === 'failed' || target.action === 'skipped');
|
|
148
|
+
for (const target of unsuccessful) {
|
|
149
|
+
const pending = target.target.pluginId
|
|
150
|
+
? [{ pluginId: target.target.pluginId, platform: target.target.platform }]
|
|
151
|
+
: [];
|
|
152
|
+
const visited = new Set();
|
|
153
|
+
while (pending.length > 0) {
|
|
154
|
+
const current = pending.shift();
|
|
155
|
+
const visitKey = [current.pluginId, current.platform].join('\0');
|
|
156
|
+
if (visited.has(visitKey))
|
|
157
|
+
continue;
|
|
158
|
+
visited.add(visitKey);
|
|
159
|
+
const candidates = (targetsByPlugin.get(current.pluginId) ?? [])
|
|
160
|
+
.filter((candidate) => candidate.target.platform === current.platform);
|
|
161
|
+
if (roots.has(current.pluginId)) {
|
|
162
|
+
if (candidates.length === 0) {
|
|
163
|
+
const retryTarget = {
|
|
164
|
+
pluginId: current.pluginId,
|
|
165
|
+
platform: current.platform,
|
|
166
|
+
scope: group.scope,
|
|
167
|
+
};
|
|
168
|
+
const key = [retryTarget.pluginId, retryTarget.platform, retryTarget.scope].join('\0');
|
|
169
|
+
if (!successfulRootKeys.has(key))
|
|
170
|
+
failed.push(retryTarget);
|
|
171
|
+
}
|
|
172
|
+
for (const candidate of candidates) {
|
|
173
|
+
const retryTarget = {
|
|
174
|
+
pluginId: current.pluginId,
|
|
175
|
+
platform: candidate.target.platform,
|
|
176
|
+
scope: candidate.target.scope,
|
|
177
|
+
};
|
|
178
|
+
const key = [current.pluginId, retryTarget.platform, retryTarget.scope].join('\0');
|
|
179
|
+
if (!successfulRootKeys.has(key))
|
|
180
|
+
failed.push(retryTarget);
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (const candidate of candidates) {
|
|
185
|
+
pending.push(...(candidate.requiredBy ?? []).map((pluginId) => ({
|
|
186
|
+
pluginId, platform: current.platform,
|
|
187
|
+
})));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return canonicalBatchTargets(failed);
|
|
193
|
+
}
|
|
194
|
+
export function pluginWriteBatchFailedRequestedPluginIds(execution) {
|
|
195
|
+
return canonicalPluginIds(pluginWriteBatchFailedRequestedTargets(execution)
|
|
196
|
+
.map((target) => target.pluginId));
|
|
197
|
+
}
|
|
198
|
+
function collectOperationSensitiveValues(intent) {
|
|
199
|
+
if (intent.kind !== 'configSave')
|
|
200
|
+
return [];
|
|
201
|
+
const sensitiveKeys = new Set([...(intent.sensitiveKeys ?? []), ...(intent.privateKeys ?? [])]);
|
|
202
|
+
const values = [];
|
|
203
|
+
const visit = (value) => {
|
|
204
|
+
if (typeof value === 'string')
|
|
205
|
+
values.push(value);
|
|
206
|
+
else if (Array.isArray(value))
|
|
207
|
+
value.forEach(visit);
|
|
208
|
+
else if (value && typeof value === 'object')
|
|
209
|
+
Object.values(value).forEach(visit);
|
|
210
|
+
};
|
|
211
|
+
for (const [key, value] of Object.entries(intent.updates ?? {})) {
|
|
212
|
+
if (sensitiveKeys.has(key))
|
|
213
|
+
visit(value);
|
|
214
|
+
}
|
|
215
|
+
return values;
|
|
216
|
+
}
|
|
217
|
+
function sanitizeOperationSummary(summary, sensitiveValues) {
|
|
218
|
+
const text = (value) => sanitizeDiagnosticText(value, [], sensitiveValues);
|
|
219
|
+
return {
|
|
220
|
+
...summary,
|
|
221
|
+
message: text(summary.message),
|
|
222
|
+
...(summary.messages ? { messages: summary.messages.map(text) } : {}),
|
|
223
|
+
...(summary.changes ? { changes: summary.changes.map(text) } : {}),
|
|
224
|
+
...(summary.warnings ? { warnings: summary.warnings.map(text) } : {}),
|
|
225
|
+
...(summary.recoveryActions ? { recoveryActions: summary.recoveryActions.map(text) } : {}),
|
|
226
|
+
...(summary.quarantineBasename ? { quarantineBasename: text(summary.quarantineBasename) } : {}),
|
|
227
|
+
...(summary.portable ? {
|
|
228
|
+
portable: {
|
|
229
|
+
...summary.portable,
|
|
230
|
+
...(summary.portable.recovery ? { recovery: text(summary.portable.recovery) } : {}),
|
|
231
|
+
},
|
|
232
|
+
} : {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
51
235
|
function deepFreeze(value) {
|
|
52
236
|
if (typeof value !== 'object' || value === null || Object.isFrozen(value))
|
|
53
237
|
return value;
|
|
@@ -58,27 +242,33 @@ function deepFreeze(value) {
|
|
|
58
242
|
function cloneFrozen(value) {
|
|
59
243
|
return deepFreeze(structuredClone(value));
|
|
60
244
|
}
|
|
61
|
-
function publicReviewPreparation(snapshot) {
|
|
62
|
-
if (snapshot.intent.kind !== 'catalogGeneration' || !snapshot.intent.sourceMutation)
|
|
63
|
-
return snapshot;
|
|
64
|
-
const mutation = snapshot.intent.sourceMutation;
|
|
65
|
-
const sourceMutation = mutation.action === 'add'
|
|
66
|
-
? {
|
|
67
|
-
...mutation,
|
|
68
|
-
source: mutation.source.type === 'path'
|
|
69
|
-
? { ...mutation.source, path: '<private>' }
|
|
70
|
-
: { ...mutation.source, url: '<private>' },
|
|
71
|
-
}
|
|
72
|
-
: {
|
|
73
|
-
...mutation,
|
|
74
|
-
update: 'url' in mutation.update
|
|
75
|
-
? { ...mutation.update, ...(mutation.update.url ? { url: '<private>' } : {}) }
|
|
76
|
-
: { ...mutation.update, ...('path' in mutation.update && mutation.update.path
|
|
77
|
-
? { path: '<private>' } : {}) },
|
|
78
|
-
};
|
|
79
|
-
return { ...snapshot, intent: { ...snapshot.intent, sourceMutation } };
|
|
80
|
-
}
|
|
81
245
|
export function operationEvidenceFingerprint(evidence) {
|
|
246
|
+
if ('groups' in evidence && 'requestedPluginIds' in evidence) {
|
|
247
|
+
const batch = evidence;
|
|
248
|
+
return crypto.createHash('sha256')
|
|
249
|
+
.update(JSON.stringify(stableValue({
|
|
250
|
+
requestedPluginIds: batch.requestedPluginIds,
|
|
251
|
+
groups: batch.groups.map((group) => ({
|
|
252
|
+
groupId: group.groupId,
|
|
253
|
+
sourceId: group.sourceId,
|
|
254
|
+
platform: group.platform,
|
|
255
|
+
requestedPluginIds: group.requestedPluginIds,
|
|
256
|
+
executable: group.executable,
|
|
257
|
+
planFingerprint: group.preview.operationPlan?.planFingerprint,
|
|
258
|
+
overallState: group.preview.overallState,
|
|
259
|
+
targetResults: group.preview.targetResults.map((target) => ({
|
|
260
|
+
pluginId: target.target.pluginId,
|
|
261
|
+
platform: target.target.platform,
|
|
262
|
+
scope: target.target.scope,
|
|
263
|
+
state: target.state,
|
|
264
|
+
action: target.action,
|
|
265
|
+
errorCode: target.errorCode,
|
|
266
|
+
requiredBy: target.requiredBy,
|
|
267
|
+
})),
|
|
268
|
+
})),
|
|
269
|
+
})))
|
|
270
|
+
.digest('hex');
|
|
271
|
+
}
|
|
82
272
|
if (evidence.operationPlan)
|
|
83
273
|
return evidence.operationPlan.planFingerprint;
|
|
84
274
|
if (evidence.resetTarget) {
|
|
@@ -90,8 +280,14 @@ export function operationEvidenceFingerprint(evidence) {
|
|
|
90
280
|
.digest('hex');
|
|
91
281
|
}
|
|
92
282
|
const { codexManager: _runtimeSelection, ...staleEvidence } = evidence;
|
|
283
|
+
const config = staleEvidence.config
|
|
284
|
+
? (({ shareability: _shareability, ...identity }) => identity)(staleEvidence.config)
|
|
285
|
+
: undefined;
|
|
93
286
|
return crypto.createHash('sha256')
|
|
94
|
-
.update(JSON.stringify(stableValue(
|
|
287
|
+
.update(JSON.stringify(stableValue({
|
|
288
|
+
...staleEvidence,
|
|
289
|
+
...(config ? { config } : {}),
|
|
290
|
+
})))
|
|
95
291
|
.digest('hex');
|
|
96
292
|
}
|
|
97
293
|
export function operationEvidenceMatches(expected, current) {
|
|
@@ -100,11 +296,22 @@ export function operationEvidenceMatches(expected, current) {
|
|
|
100
296
|
!== JSON.stringify(stableValue(current.resetTarget))) {
|
|
101
297
|
return false;
|
|
102
298
|
}
|
|
103
|
-
if (
|
|
299
|
+
if (expected.source && (current.source === undefined
|
|
300
|
+
|| JSON.stringify(stableValue(expected.source))
|
|
301
|
+
!== JSON.stringify(stableValue(current.source)))) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
if (expected.targetFingerprint !== undefined
|
|
305
|
+
&& expected.targetFingerprint !== current.targetFingerprint) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (expected.config?.impactState !== 'known')
|
|
104
309
|
return true;
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
|
|
310
|
+
return JSON.stringify(stableValue({
|
|
311
|
+
config: expected.config,
|
|
312
|
+
})) === JSON.stringify(stableValue({
|
|
313
|
+
config: current.config,
|
|
314
|
+
}));
|
|
108
315
|
}
|
|
109
316
|
return operationEvidenceFingerprint(expected) === operationEvidenceFingerprint(current);
|
|
110
317
|
}
|
|
@@ -125,6 +332,9 @@ export function userIssueKey(issue) {
|
|
|
125
332
|
.join('\0');
|
|
126
333
|
}
|
|
127
334
|
function issueCode(error) {
|
|
335
|
+
const safeOperationError = readSafeOperationError(error);
|
|
336
|
+
if (safeOperationError)
|
|
337
|
+
return safeDiagnosticCode(safeOperationError.code, 'operationBlocked');
|
|
128
338
|
if (error instanceof TuiOperationBlockedError)
|
|
129
339
|
return error.code;
|
|
130
340
|
if (error instanceof CatalogGenerationError)
|
|
@@ -160,12 +370,15 @@ export class TuiController {
|
|
|
160
370
|
#listeners = new Set();
|
|
161
371
|
#health = new Map();
|
|
162
372
|
#tickets = new Map();
|
|
373
|
+
#reviewPreparationIntents = new Map();
|
|
163
374
|
#inflightOperations = new Map();
|
|
375
|
+
#pendingReads = new Set();
|
|
164
376
|
#abort = new AbortController();
|
|
165
377
|
#progressUnsubscribe;
|
|
166
378
|
#progressFeed;
|
|
167
379
|
#phase = 'idle';
|
|
168
380
|
#status;
|
|
381
|
+
#statusReadAtMs;
|
|
169
382
|
#catalogGenerations = new Map();
|
|
170
383
|
#catalogFreshness = new Map();
|
|
171
384
|
#catalogSourcesSession;
|
|
@@ -177,18 +390,28 @@ export class TuiController {
|
|
|
177
390
|
#reviewPreparation;
|
|
178
391
|
#reviewPreparationToken = 0;
|
|
179
392
|
#result;
|
|
393
|
+
#resultAnchor;
|
|
394
|
+
#portableResultSkills;
|
|
180
395
|
#loadError;
|
|
181
396
|
#portableDraft;
|
|
397
|
+
#recoverablePortableDraft;
|
|
182
398
|
#portableRepreparePluginId;
|
|
183
399
|
#portableRequestToken = 0;
|
|
184
400
|
#portableRequestAbort;
|
|
185
401
|
#portablePending;
|
|
186
402
|
#installPlacementDraft;
|
|
403
|
+
#recoverableInstallPlacementDraft;
|
|
187
404
|
#installPlacementRequestToken = 0;
|
|
188
405
|
#installPlacementRequestAbort;
|
|
189
406
|
#generation = 0;
|
|
190
407
|
#disposed = false;
|
|
191
|
-
#
|
|
408
|
+
#retiring = false;
|
|
409
|
+
#externalDrafts = [];
|
|
410
|
+
#batchRetryIntent;
|
|
411
|
+
#operationHistory = new OperationHistoryStore();
|
|
412
|
+
#sessionEpoch = 0;
|
|
413
|
+
#lastRecordedResultId;
|
|
414
|
+
#operationDiagnostics = new Map();
|
|
192
415
|
constructor(dependencies, progressFeed) {
|
|
193
416
|
this.dependencies = dependencies;
|
|
194
417
|
this.#progressFeed = progressFeed;
|
|
@@ -211,6 +434,8 @@ export class TuiController {
|
|
|
211
434
|
});
|
|
212
435
|
if (progressFeed) {
|
|
213
436
|
this.#progressUnsubscribe = progressFeed.subscribe((progress) => {
|
|
437
|
+
if (this.#router.snapshot.current.route.name === 'result')
|
|
438
|
+
return;
|
|
214
439
|
this.#progress = {
|
|
215
440
|
...structuredClone(progress),
|
|
216
441
|
messages: progress.messages.slice(-8),
|
|
@@ -239,6 +464,7 @@ export class TuiController {
|
|
|
239
464
|
get snapshot() {
|
|
240
465
|
return cloneFrozen({
|
|
241
466
|
phase: this.#phase,
|
|
467
|
+
...(this.#statusReadAtMs !== undefined ? { statusReadAtMs: this.#statusReadAtMs } : {}),
|
|
242
468
|
...(this.#status?.cliChannel ? { cliChannel: this.#status.cliChannel } : {}),
|
|
243
469
|
router: this.#router.snapshot,
|
|
244
470
|
...(this.#viewModel ? { viewModel: this.#viewModel } : {}),
|
|
@@ -252,9 +478,11 @@ export class TuiController {
|
|
|
252
478
|
progressTargets: this.#progressTargets,
|
|
253
479
|
...(this.#activeReview ? { activeReview: this.#activeReview } : {}),
|
|
254
480
|
...(this.#reviewPreparation
|
|
255
|
-
? { reviewPreparation:
|
|
481
|
+
? { reviewPreparation: this.#reviewPreparation.snapshot }
|
|
256
482
|
: {}),
|
|
257
483
|
...(this.#result ? { result: this.#result } : {}),
|
|
484
|
+
operationHistorySummaries: this.#operationHistory.list(),
|
|
485
|
+
...(this.#portableResultSkills ? { portableResultSkills: this.#portableResultSkills } : {}),
|
|
258
486
|
...(this.#loadError ? { loadError: this.#loadError } : {}),
|
|
259
487
|
...(this.#portableDraft ? { portableDraft: this.#portableDraft } : {}),
|
|
260
488
|
...(this.#installPlacementDraft
|
|
@@ -278,13 +506,222 @@ export class TuiController {
|
|
|
278
506
|
listener(this.snapshot);
|
|
279
507
|
return () => this.#listeners.delete(listener);
|
|
280
508
|
}
|
|
509
|
+
get hasActiveWrite() {
|
|
510
|
+
return this.#inflightOperations.size > 0;
|
|
511
|
+
}
|
|
512
|
+
get retiring() {
|
|
513
|
+
return this.#retiring;
|
|
514
|
+
}
|
|
515
|
+
get hasSessionDraft() {
|
|
516
|
+
return this.#externalDrafts.length > 0
|
|
517
|
+
|| this.#portableDraft !== undefined
|
|
518
|
+
|| this.#installPlacementDraft !== undefined;
|
|
519
|
+
}
|
|
520
|
+
get externalDrafts() {
|
|
521
|
+
return structuredClone(this.#externalDrafts);
|
|
522
|
+
}
|
|
523
|
+
attachOperationHistory(history, epoch) {
|
|
524
|
+
this.#operationHistory = history;
|
|
525
|
+
this.#sessionEpoch = epoch;
|
|
526
|
+
}
|
|
527
|
+
listOperationHistory() {
|
|
528
|
+
return this.#operationHistory.list();
|
|
529
|
+
}
|
|
530
|
+
getOperationDiagnostic(recordId) {
|
|
531
|
+
return this.#operationHistory.get(recordId);
|
|
532
|
+
}
|
|
533
|
+
previewOperationReport(recordId) {
|
|
534
|
+
return this.#operationHistory.preview(recordId);
|
|
535
|
+
}
|
|
536
|
+
saveOperationReport(recordId, filePath) {
|
|
537
|
+
return this.#operationHistory.save(recordId, filePath);
|
|
538
|
+
}
|
|
539
|
+
/** @internal Test-only observability for bounded private diagnostic ownership. */
|
|
540
|
+
operationDiagnosticRetentionCountForTests() {
|
|
541
|
+
return this.#operationDiagnostics.size;
|
|
542
|
+
}
|
|
543
|
+
recordUiFailure(error, context) {
|
|
544
|
+
const recordId = this.#recordStandaloneFailure(context.code, context.stage, error);
|
|
545
|
+
this.#publish();
|
|
546
|
+
return recordId;
|
|
547
|
+
}
|
|
548
|
+
#recordStandaloneFailure(code, stage, error) {
|
|
549
|
+
const operationId = `ui-${crypto.randomUUID()}`;
|
|
550
|
+
const recordId = `${this.#sessionEpoch}:${operationId}`;
|
|
551
|
+
const diagnostic = projectThrownOperationDiagnostic({
|
|
552
|
+
error,
|
|
553
|
+
id: `${recordId}:failure`,
|
|
554
|
+
stage: safeDiagnosticCode(stage, 'ui'),
|
|
555
|
+
fallbackCode: safeDiagnosticCode(code, 'uiOperationFailed'),
|
|
556
|
+
});
|
|
557
|
+
this.#operationHistory.record({
|
|
558
|
+
summary: {
|
|
559
|
+
recordId,
|
|
560
|
+
epoch: this.#sessionEpoch,
|
|
561
|
+
operationId,
|
|
562
|
+
createdAtMs: this.dependencies.now?.() ?? Date.now(),
|
|
563
|
+
state: 'blocked',
|
|
564
|
+
code: diagnostic.code,
|
|
565
|
+
message: diagnostic.safeMessage,
|
|
566
|
+
diagnosticCount: 1,
|
|
567
|
+
effects: {
|
|
568
|
+
configCommit: 'notApplicable', awWrite: 'notStarted',
|
|
569
|
+
preApplyExternalEffect: 'notApplicable', finalVerification: 'notStarted',
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
diagnostics: [diagnostic],
|
|
573
|
+
targets: [],
|
|
574
|
+
});
|
|
575
|
+
return recordId;
|
|
576
|
+
}
|
|
577
|
+
#trackRead(promise) {
|
|
578
|
+
this.#pendingReads.add(promise);
|
|
579
|
+
void promise.finally(() => this.#pendingReads.delete(promise)).catch(() => undefined);
|
|
580
|
+
return promise;
|
|
581
|
+
}
|
|
582
|
+
beginRetirement() {
|
|
583
|
+
this.#retiring = true;
|
|
584
|
+
}
|
|
585
|
+
async waitForWrites() {
|
|
586
|
+
this.beginRetirement();
|
|
587
|
+
while (this.#inflightOperations.size > 0) {
|
|
588
|
+
await Promise.allSettled([...this.#inflightOperations.values()].map((operation) => operation.promise));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async cancelAndWaitForReads() {
|
|
592
|
+
if (this.hasActiveWrite)
|
|
593
|
+
return false;
|
|
594
|
+
this.beginRetirement();
|
|
595
|
+
this.#generation += 1;
|
|
596
|
+
this.#cancelReviewPreparation();
|
|
597
|
+
this.#cancelPortableRequest();
|
|
598
|
+
this.#cancelInstallPlacementRequest();
|
|
599
|
+
this.#abort.abort();
|
|
600
|
+
const catalogDrain = this.#catalogSourcesSession.cancelAndWait();
|
|
601
|
+
while (this.#pendingReads.size > 0) {
|
|
602
|
+
await Promise.allSettled([...this.#pendingReads]);
|
|
603
|
+
}
|
|
604
|
+
await catalogDrain;
|
|
605
|
+
return !this.hasActiveWrite;
|
|
606
|
+
}
|
|
607
|
+
#assertAcceptingActions() {
|
|
608
|
+
if (this.#retiring || this.#disposed) {
|
|
609
|
+
throw new DOMException('Controller session is retiring.', 'AbortError');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
goHome(options = {}) {
|
|
613
|
+
if (this.hasSessionDraft && !options.discardDrafts)
|
|
614
|
+
return false;
|
|
615
|
+
this.#cancelReviewPreparation();
|
|
616
|
+
this.#cancelPortableRequest();
|
|
617
|
+
this.#cancelInstallPlacementRequest();
|
|
618
|
+
this.#tickets.clear();
|
|
619
|
+
this.#activeReview = undefined;
|
|
620
|
+
this.#portableDraft = undefined;
|
|
621
|
+
this.#recoverablePortableDraft = undefined;
|
|
622
|
+
this.#installPlacementDraft = undefined;
|
|
623
|
+
this.#recoverableInstallPlacementDraft = undefined;
|
|
624
|
+
this.#batchRetryIntent = undefined;
|
|
625
|
+
this.#externalDrafts = [];
|
|
626
|
+
this.#result = undefined;
|
|
627
|
+
this.#resultAnchor = undefined;
|
|
628
|
+
this.#portableResultSkills = undefined;
|
|
629
|
+
this.#progress = { messages: [] };
|
|
630
|
+
this.#progressTargets = [];
|
|
631
|
+
this.#router.resetToRoot();
|
|
632
|
+
this.#publish();
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
281
635
|
#publish() {
|
|
282
636
|
if (this.#disposed)
|
|
283
637
|
return;
|
|
638
|
+
this.#recordTerminalResult();
|
|
284
639
|
const snapshot = this.snapshot;
|
|
285
640
|
for (const listener of this.#listeners)
|
|
286
641
|
listener(snapshot);
|
|
287
642
|
}
|
|
643
|
+
#recordTerminalResult() {
|
|
644
|
+
const result = this.#result;
|
|
645
|
+
if (!result || result.state === 'stale')
|
|
646
|
+
return;
|
|
647
|
+
const recordId = `${this.#sessionEpoch}:${result.operationId}`;
|
|
648
|
+
if (this.#lastRecordedResultId === recordId)
|
|
649
|
+
return;
|
|
650
|
+
const projected = this.#operationDiagnostics.get(result.operationId) ?? {
|
|
651
|
+
diagnostics: [],
|
|
652
|
+
effects: {
|
|
653
|
+
configCommit: 'notApplicable',
|
|
654
|
+
awWrite: result.state === 'succeeded' ? 'completed' : 'unknown',
|
|
655
|
+
preApplyExternalEffect: 'notApplicable',
|
|
656
|
+
finalVerification: result.state === 'succeeded' ? 'completed' : 'unknown',
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
if ('summary' in result && result.summary) {
|
|
660
|
+
Object.assign(result.summary, sanitizeOperationSummary(result.summary, projected.sensitiveValues ?? []));
|
|
661
|
+
}
|
|
662
|
+
const diagnosticIds = projected.diagnostics.map((diagnostic) => diagnostic.id);
|
|
663
|
+
Object.assign(result, { operationRecordId: recordId, diagnosticIds, effects: projected.effects });
|
|
664
|
+
if ('summary' in result && result.summary) {
|
|
665
|
+
result.summary.diagnosticIds = diagnosticIds;
|
|
666
|
+
result.summary.effects = projected.effects;
|
|
667
|
+
for (const target of result.summary.targets ?? []) {
|
|
668
|
+
const targetDiagnostics = projected.diagnostics.filter((diagnostic) => diagnostic.target
|
|
669
|
+
&& diagnostic.target.platform === target.platform
|
|
670
|
+
&& diagnostic.target.scope === target.scope
|
|
671
|
+
&& (diagnostic.target.pluginId === undefined || diagnostic.target.pluginId === target.pluginId));
|
|
672
|
+
target.diagnosticIds = targetDiagnostics.map((diagnostic) => diagnostic.id);
|
|
673
|
+
target.effects = projected.targetEffects?.get(`${target.pluginId ?? ''}\0${target.platform}\0${target.scope}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const code = result.state === 'succeeded' || result.state === 'partial'
|
|
677
|
+
? result.summary.code : result.summary?.code ?? result.code;
|
|
678
|
+
const message = result.state === 'succeeded' || result.state === 'partial'
|
|
679
|
+
? result.summary.message : result.summary?.message ?? result.message;
|
|
680
|
+
this.#operationHistory.record({
|
|
681
|
+
summary: {
|
|
682
|
+
recordId,
|
|
683
|
+
epoch: this.#sessionEpoch,
|
|
684
|
+
operationId: result.operationId,
|
|
685
|
+
createdAtMs: this.dependencies.now?.() ?? Date.now(),
|
|
686
|
+
state: result.state,
|
|
687
|
+
code,
|
|
688
|
+
message,
|
|
689
|
+
...('operationKind' in result && result.operationKind ? { operationKind: result.operationKind } : {}),
|
|
690
|
+
...('pluginId' in result && result.pluginId ? { pluginId: result.pluginId } : {}),
|
|
691
|
+
diagnosticCount: projected.diagnostics.length,
|
|
692
|
+
effects: projected.effects,
|
|
693
|
+
},
|
|
694
|
+
diagnostics: projected.diagnostics,
|
|
695
|
+
targets: 'summary' in result && result.summary
|
|
696
|
+
? structuredClone((result.summary.targets ?? []).map((target) => ({
|
|
697
|
+
...target,
|
|
698
|
+
diagnosticIds: target.diagnosticIds ?? [],
|
|
699
|
+
})))
|
|
700
|
+
: structuredClone(projected.targets ?? []),
|
|
701
|
+
}, { sensitiveValues: projected.sensitiveValues });
|
|
702
|
+
this.#operationDiagnostics.delete(result.operationId);
|
|
703
|
+
this.#lastRecordedResultId = recordId;
|
|
704
|
+
}
|
|
705
|
+
#captureThrownProjection(operationId, error, code, stage, intent, effects) {
|
|
706
|
+
const sensitiveValues = intent ? collectOperationSensitiveValues(intent) : [];
|
|
707
|
+
this.#operationDiagnostics.set(operationId, {
|
|
708
|
+
diagnostics: [projectThrownOperationDiagnostic({
|
|
709
|
+
error,
|
|
710
|
+
id: `${this.#sessionEpoch}:${operationId}:failure`,
|
|
711
|
+
stage,
|
|
712
|
+
fallbackCode: code,
|
|
713
|
+
sensitiveValues,
|
|
714
|
+
...(intent && 'pluginId' in intent && 'platform' in intent && intent.platform !== 'both'
|
|
715
|
+
? { target: { pluginId: intent.pluginId, platform: intent.platform, scope: intent.scope } }
|
|
716
|
+
: {}),
|
|
717
|
+
})],
|
|
718
|
+
effects: effects ?? {
|
|
719
|
+
configCommit: 'notApplicable', awWrite: 'notStarted',
|
|
720
|
+
preApplyExternalEffect: 'notApplicable', finalVerification: 'notStarted',
|
|
721
|
+
},
|
|
722
|
+
sensitiveValues,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
288
725
|
async #logErrorSafe(error, code, context) {
|
|
289
726
|
try {
|
|
290
727
|
await this.dependencies.logError?.(error, code, context);
|
|
@@ -306,64 +743,152 @@ export class TuiController {
|
|
|
306
743
|
catalogGenerations: this.#catalogGenerations,
|
|
307
744
|
dependencyOverviews: this.#status.dependencyOverviews,
|
|
308
745
|
portablePluginIds: this.#status.portablePluginIds,
|
|
746
|
+
configStoreInventory: this.#status.configStoreInventory,
|
|
309
747
|
});
|
|
310
748
|
}
|
|
311
749
|
#normalizeRoute(route) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
750
|
+
return normalizeTuiRoute(route, this.#routeEntityState());
|
|
751
|
+
}
|
|
752
|
+
#routeEntityState() {
|
|
753
|
+
return {
|
|
754
|
+
hasPlugin: (pluginId) => this.#viewModel?.plugins.some((plugin) => plugin.pluginId === pluginId) ?? false,
|
|
755
|
+
hasSource: (sourceId) => this.#viewModel?.sourceTabs.some((source) => source.id === sourceId) ?? false,
|
|
756
|
+
hasIssue: (issueId) => this.#viewModel?.issues.some((issue) => issue.issueId === issueId) ?? false,
|
|
757
|
+
hasHistoryRecord: (recordId) => this.#operationHistory.get(recordId) !== undefined,
|
|
758
|
+
hasActiveOperation: (candidate) => candidate.name === 'reviewPreparing'
|
|
759
|
+
? this.#reviewPreparation?.snapshot.id === candidate.preparationId
|
|
760
|
+
: candidate.name === 'result'
|
|
761
|
+
? this.#result?.operationId === candidate.operationId
|
|
762
|
+
: this.#activeReview?.operationId === candidate.operationId,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
#normalizeCurrentRoute() {
|
|
766
|
+
this.#router.normalizeRoutes((route) => this.#normalizeRoute(route));
|
|
767
|
+
if (this.#resultAnchor)
|
|
768
|
+
this.#resultAnchor = this.#normalizeRoute(this.#resultAnchor);
|
|
769
|
+
for (const ticket of this.#tickets.values())
|
|
770
|
+
ticket.returnRoute = this.#normalizeRoute(ticket.returnRoute);
|
|
771
|
+
if (this.#reviewPreparation) {
|
|
772
|
+
this.#reviewPreparation.returnRoute = this.#normalizeRoute(this.#reviewPreparation.returnRoute);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
#ensureOperationFlow(intent) {
|
|
776
|
+
const active = this.#router.activeFlow();
|
|
777
|
+
if (intent.kind === 'pluginWriteBatch') {
|
|
778
|
+
if (active?.kind === 'batchWrite')
|
|
779
|
+
return;
|
|
780
|
+
this.#router.beginFlow('batchWrite', { name: 'pluginList', mode: 'installed' });
|
|
781
|
+
return;
|
|
315
782
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|| route.name === 'portableBlocker'
|
|
323
|
-
|| route.name === 'pluginDependencies'
|
|
324
|
-
|| route.name === 'installPlacement'
|
|
325
|
-
|| route.name === 'installTargets'
|
|
326
|
-
|| route.name === 'dependencyPlacement'
|
|
327
|
-
|| route.name === 'dependencyPlacementEditor'
|
|
328
|
-
|| route.name === 'pluginDetail'
|
|
329
|
-
|| route.name === 'platform'
|
|
330
|
-
|| route.name === 'pluginAbout'
|
|
331
|
-
|| route.name === 'pluginTechnical'
|
|
332
|
-
|| route.name === 'configLayer'
|
|
333
|
-
|| route.name === 'configFields'
|
|
334
|
-
|| route.name === 'configFieldEditor'
|
|
335
|
-
|| route.name === 'configUnavailable';
|
|
336
|
-
if (pluginRoute && !this.#viewModel?.plugins.some((plugin) => plugin.pluginId === route.pluginId)) {
|
|
337
|
-
return {
|
|
338
|
-
name: 'pluginList',
|
|
339
|
-
mode: route.name === 'pluginPreview' || route.name === 'pluginDependencies'
|
|
340
|
-
|| route.name === 'installPlacement' || route.name === 'installTargets'
|
|
341
|
-
|| route.name === 'dependencyPlacement' || route.name === 'dependencyPlacementEditor'
|
|
342
|
-
? 'browse'
|
|
343
|
-
: 'installed',
|
|
344
|
-
};
|
|
783
|
+
if (intent.kind === 'pluginWrite') {
|
|
784
|
+
const kind = intent.action === 'install' ? 'pluginInstall' : 'pluginMaintenance';
|
|
785
|
+
if (active?.kind === kind)
|
|
786
|
+
return;
|
|
787
|
+
this.#router.beginPluginFlow(kind, intent.pluginId);
|
|
788
|
+
return;
|
|
345
789
|
}
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
790
|
+
if (intent.kind === 'portableInstall') {
|
|
791
|
+
if (active?.kind === 'portableInstall')
|
|
792
|
+
return;
|
|
793
|
+
this.#router.beginPortableFlow(intent.pluginId);
|
|
794
|
+
return;
|
|
349
795
|
}
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
796
|
+
if (intent.kind === 'configSave' || intent.kind === 'configReset') {
|
|
797
|
+
if (active?.kind === 'configEdit')
|
|
798
|
+
return;
|
|
799
|
+
const route = this.#router.snapshot.current.route;
|
|
800
|
+
if ((route.name === 'pluginPreview' || route.name === 'pluginDetail')
|
|
801
|
+
&& route.pluginId === intent.pluginId) {
|
|
802
|
+
this.#router.beginConfigFlow(intent.pluginId);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
this.#router.beginFlow('configEdit', { name: 'pluginDetail', pluginId: intent.pluginId });
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
356
808
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
809
|
+
const kind = intent.kind === 'catalogSourcesRecovery' ? 'sourceRecovery' : 'sourceEdit';
|
|
810
|
+
if (active?.kind === 'sourceRecovery'
|
|
811
|
+
&& (intent.kind === 'catalogSourcesRecovery'
|
|
812
|
+
|| intent.kind === 'catalogGeneration'
|
|
813
|
+
|| intent.kind === 'sourceMutation'))
|
|
814
|
+
return;
|
|
815
|
+
if (active?.kind === kind)
|
|
816
|
+
return;
|
|
817
|
+
this.#router.beginFlow(kind, active ? this.#router.snapshot.current.route
|
|
818
|
+
: intent.kind === 'sourceMutation' && intent.action !== 'remove'
|
|
819
|
+
? { name: 'sourceDetail', sourceId: intent.sourceId }
|
|
820
|
+
: intent.kind === 'catalogGeneration'
|
|
821
|
+
? { name: 'sourceDetail', sourceId: intent.sourceId }
|
|
822
|
+
: { name: 'sourceList' });
|
|
823
|
+
}
|
|
824
|
+
#stableResultRoute(intent, result) {
|
|
825
|
+
const active = this.#router.activeFlow();
|
|
826
|
+
if (active?.parentFlowId)
|
|
827
|
+
return this.#normalizeRoute(active.returnTarget);
|
|
828
|
+
if (active && (intent.kind === 'portableInstall'
|
|
829
|
+
|| intent.kind === 'configSave' || intent.kind === 'configReset')) {
|
|
830
|
+
return this.#normalizeRoute(active.returnTarget);
|
|
831
|
+
}
|
|
832
|
+
if (intent.kind === 'pluginWrite') {
|
|
833
|
+
if (intent.originIssue && result.state === 'succeeded')
|
|
834
|
+
return { name: 'issueList' };
|
|
835
|
+
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
|
|
836
|
+
}
|
|
837
|
+
if (intent.kind === 'pluginWriteBatch')
|
|
838
|
+
return { name: 'pluginList', mode: 'installed' };
|
|
839
|
+
if (intent.kind === 'portableInstall' || intent.kind === 'configSave' || intent.kind === 'configReset') {
|
|
840
|
+
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
|
|
841
|
+
}
|
|
842
|
+
if (intent.kind === 'sourceMutation' && intent.action === 'update') {
|
|
843
|
+
return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
|
|
844
|
+
}
|
|
845
|
+
if (intent.kind === 'catalogGeneration') {
|
|
846
|
+
return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
|
|
847
|
+
}
|
|
848
|
+
return { name: 'sourceList' };
|
|
849
|
+
}
|
|
850
|
+
#showResult(result, intent) {
|
|
851
|
+
const terminal = { name: 'result', operationId: result.operationId };
|
|
852
|
+
const target = this.#stableResultRoute(intent, result);
|
|
853
|
+
this.#resultAnchor = structuredClone(target);
|
|
854
|
+
this.#cancelReviewPreparation();
|
|
855
|
+
this.#progress = { messages: [] };
|
|
856
|
+
this.#progressTargets = [];
|
|
857
|
+
this.#recoverableInstallPlacementDraft = result.state === 'blocked'
|
|
858
|
+
&& intent.kind === 'pluginWrite' && intent.action === 'install'
|
|
859
|
+
&& this.#installPlacementDraft?.pluginId === intent.pluginId
|
|
860
|
+
? structuredClone(this.#installPlacementDraft)
|
|
861
|
+
: undefined;
|
|
862
|
+
this.#installPlacementDraft = undefined;
|
|
863
|
+
this.#recoverablePortableDraft = result.state !== 'succeeded'
|
|
864
|
+
&& intent.kind === 'portableInstall'
|
|
865
|
+
&& this.#portableDraft?.pluginId === intent.pluginId
|
|
866
|
+
? structuredClone(this.#portableDraft)
|
|
867
|
+
: undefined;
|
|
868
|
+
this.#portableResultSkills = intent.kind === 'portableInstall'
|
|
869
|
+
? this.#portableDraft?.candidates
|
|
870
|
+
.filter((candidate) => candidate.selected)
|
|
871
|
+
.map((candidate) => candidate.name)
|
|
872
|
+
: undefined;
|
|
873
|
+
this.#portableDraft = undefined;
|
|
874
|
+
this.#batchRetryIntent = intent.kind === 'pluginWriteBatch'
|
|
875
|
+
&& 'summary' in result
|
|
876
|
+
&& result.summary?.batch
|
|
877
|
+
&& (result.summary.batch.retryTargets?.length ?? 0) > 0
|
|
878
|
+
? {
|
|
879
|
+
...structuredClone(intent),
|
|
880
|
+
selection: {
|
|
881
|
+
kind: 'targets',
|
|
882
|
+
targets: structuredClone(result.summary.batch.retryTargets),
|
|
883
|
+
},
|
|
884
|
+
}
|
|
885
|
+
: undefined;
|
|
886
|
+
if (!this.#router.finishFlow(target, terminal))
|
|
887
|
+
this.#router.replace(terminal);
|
|
364
888
|
}
|
|
365
889
|
#applyStatus(status) {
|
|
366
890
|
this.#status = structuredClone(status);
|
|
891
|
+
this.#statusReadAtMs = this.dependencies.now?.() ?? Date.now();
|
|
367
892
|
if (status.catalogGenerations) {
|
|
368
893
|
this.#catalogGenerations.clear();
|
|
369
894
|
for (const source of status.catalogGenerations) {
|
|
@@ -421,13 +946,14 @@ export class TuiController {
|
|
|
421
946
|
this.#normalizeCurrentRoute();
|
|
422
947
|
}
|
|
423
948
|
async load() {
|
|
949
|
+
this.#assertAcceptingActions();
|
|
424
950
|
this.dependencies.beginCatalogSession?.();
|
|
425
951
|
const generation = ++this.#generation;
|
|
426
952
|
this.#phase = 'loading';
|
|
427
953
|
this.#publish();
|
|
428
954
|
let status;
|
|
429
955
|
try {
|
|
430
|
-
status = await this.dependencies.loadStatus(this.#abort.signal);
|
|
956
|
+
status = await this.#trackRead(this.dependencies.loadStatus(this.#abort.signal));
|
|
431
957
|
}
|
|
432
958
|
catch (error) {
|
|
433
959
|
if (this.#disposed || generation !== this.#generation)
|
|
@@ -436,6 +962,7 @@ export class TuiController {
|
|
|
436
962
|
await this.#logErrorSafe(error, code);
|
|
437
963
|
this.#phase = 'error';
|
|
438
964
|
this.#loadError = { code, message: '无法安全读取插件状态。请检查诊断后重试或退出。' };
|
|
965
|
+
this.#recordStandaloneFailure(code, 'statusReload', error);
|
|
439
966
|
this.#publish();
|
|
440
967
|
return;
|
|
441
968
|
}
|
|
@@ -451,6 +978,10 @@ export class TuiController {
|
|
|
451
978
|
this.#cancelPortableRequest();
|
|
452
979
|
this.#cancelInstallPlacementRequest();
|
|
453
980
|
this.#discardPortableDraftForRoute(route);
|
|
981
|
+
if ((route.name === 'sourceAdd' || route.name === 'sourceEdit' || route.name === 'sourceSave')
|
|
982
|
+
&& this.#router.activeFlow()?.kind !== 'sourceEdit') {
|
|
983
|
+
this.#router.beginFlow('sourceEdit', this.#router.snapshot.current.route);
|
|
984
|
+
}
|
|
454
985
|
this.#router.push(route, context);
|
|
455
986
|
this.#publish();
|
|
456
987
|
}
|
|
@@ -458,6 +989,7 @@ export class TuiController {
|
|
|
458
989
|
return this.#status?.portablePluginIds?.includes(pluginId) ?? false;
|
|
459
990
|
}
|
|
460
991
|
preparePortableInstall(pluginId, navigation = 'push') {
|
|
992
|
+
this.#assertAcceptingActions();
|
|
461
993
|
if (!this.hasPortableSkills(pluginId)) {
|
|
462
994
|
throw new TuiOperationBlockedError('portableUnsupported', '插件不提供通用 Skills。');
|
|
463
995
|
}
|
|
@@ -473,6 +1005,7 @@ export class TuiController {
|
|
|
473
1005
|
});
|
|
474
1006
|
}
|
|
475
1007
|
refreshPortableInstallDraft(pluginId) {
|
|
1008
|
+
this.#assertAcceptingActions();
|
|
476
1009
|
const expectedRoute = this.#router.snapshot.current.route;
|
|
477
1010
|
return this.#runPortableRequest(`refresh:${pluginId}`, async (signal, token) => {
|
|
478
1011
|
const draft = await this.#buildPortableDraft(pluginId, true, signal);
|
|
@@ -488,12 +1021,12 @@ export class TuiController {
|
|
|
488
1021
|
: undefined;
|
|
489
1022
|
const prepared = await Promise.allSettled(['project', 'user'].map(async (scope) => ({
|
|
490
1023
|
scope,
|
|
491
|
-
evidence: await this.dependencies.captureEvidence({
|
|
1024
|
+
evidence: await this.#trackRead(this.dependencies.captureEvidence({
|
|
492
1025
|
kind: 'portableInstall',
|
|
493
1026
|
pluginId,
|
|
494
1027
|
scope,
|
|
495
1028
|
selectedSkills: [],
|
|
496
|
-
}, signal),
|
|
1029
|
+
}, signal)),
|
|
497
1030
|
})));
|
|
498
1031
|
const available = prepared.flatMap((entry) => entry.status === 'fulfilled' && entry.value.evidence.portable ? [entry.value] : []);
|
|
499
1032
|
if (available.length === 0) {
|
|
@@ -639,8 +1172,91 @@ export class TuiController {
|
|
|
639
1172
|
});
|
|
640
1173
|
this.#publish();
|
|
641
1174
|
}
|
|
1175
|
+
beginSavedConfigRecovery(pluginId) {
|
|
1176
|
+
this.#assertAcceptingActions();
|
|
1177
|
+
const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
|
|
1178
|
+
if (!plugin?.configOnly || plugin.savedConfigScopes.length === 0) {
|
|
1179
|
+
throw new TuiOperationBlockedError('savedConfigUnavailable', '没有可识别的已保存设置。');
|
|
1180
|
+
}
|
|
1181
|
+
this.#router.beginConfigFlow(pluginId);
|
|
1182
|
+
this.#router.push({
|
|
1183
|
+
name: 'configUnavailable',
|
|
1184
|
+
pluginId,
|
|
1185
|
+
storedScopes: plugin.savedConfigScopes,
|
|
1186
|
+
...(plugin.savedConfigScopes.length === 1
|
|
1187
|
+
? { layer: { scope: plugin.savedConfigScopes[0] } }
|
|
1188
|
+
: {}),
|
|
1189
|
+
diagnosticCode: 'catalogSourceUnavailable',
|
|
1190
|
+
diagnosticStage: 'read-config',
|
|
1191
|
+
});
|
|
1192
|
+
this.#publish();
|
|
1193
|
+
}
|
|
1194
|
+
beginConfigSourceRecovery(sourceId) {
|
|
1195
|
+
this.#assertAcceptingActions();
|
|
1196
|
+
const route = this.#router.snapshot.current.route;
|
|
1197
|
+
const active = this.#router.activeFlow();
|
|
1198
|
+
if (route.name !== 'configUnavailable'
|
|
1199
|
+
|| active?.kind !== 'configEdit'
|
|
1200
|
+
|| route.storedScopes !== undefined
|
|
1201
|
+
|| sourceIdFromPluginId(route.pluginId) !== sourceId
|
|
1202
|
+
|| !this.#viewModel?.sourceTabs.some((source) => source.id === sourceId)) {
|
|
1203
|
+
throw new TuiOperationBlockedError('configSourceRecoveryUnavailable', '来源恢复只能从不可用的设置流程开始。');
|
|
1204
|
+
}
|
|
1205
|
+
const layer = route.layer?.scope ?? route.resumeRoute?.layer.scope ?? 'effective';
|
|
1206
|
+
const returnTarget = route.resumeRoute ?? {
|
|
1207
|
+
name: 'configFields',
|
|
1208
|
+
pluginId: route.pluginId,
|
|
1209
|
+
layer: { scope: layer },
|
|
1210
|
+
};
|
|
1211
|
+
this.#router.beginFlow('sourceRecovery', returnTarget, undefined, route.resumeContext);
|
|
1212
|
+
this.#router.push({ name: 'sourceDetail', sourceId });
|
|
1213
|
+
this.#publish();
|
|
1214
|
+
}
|
|
1215
|
+
async getForceCleanupCandidate(pluginId, platform, scope) {
|
|
1216
|
+
this.#assertAcceptingActions();
|
|
1217
|
+
const intent = {
|
|
1218
|
+
kind: 'pluginWrite', action: 'cleanup', pluginId, platform, scope, force: true,
|
|
1219
|
+
};
|
|
1220
|
+
try {
|
|
1221
|
+
const evidence = await this.#trackRead(this.dependencies.captureEvidence(intent, this.#abort.signal));
|
|
1222
|
+
const candidate = evidence.offlineCleanup;
|
|
1223
|
+
if (candidate?.forceExclusive !== true
|
|
1224
|
+
|| candidate.pluginId !== pluginId
|
|
1225
|
+
|| candidate.platform !== platform
|
|
1226
|
+
|| candidate.scope !== scope
|
|
1227
|
+
|| candidate.plan.actions.length === 0)
|
|
1228
|
+
return undefined;
|
|
1229
|
+
return cloneFrozen({ pluginId, platform, scope, actionCount: candidate.plan.actions.length });
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
await this.#logOperationError(error, issueCode(error), intent);
|
|
1233
|
+
return undefined;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
startForceCleanupReview(candidate) {
|
|
1237
|
+
if (candidate.actionCount <= 0) {
|
|
1238
|
+
throw new TuiOperationBlockedError('forceCleanupPathUnavailable', '没有可验证的精确清理目标。');
|
|
1239
|
+
}
|
|
1240
|
+
return this.startReviewPreparation({
|
|
1241
|
+
kind: 'pluginWrite',
|
|
1242
|
+
action: 'cleanup',
|
|
1243
|
+
pluginId: candidate.pluginId,
|
|
1244
|
+
platform: candidate.platform,
|
|
1245
|
+
scope: candidate.scope,
|
|
1246
|
+
force: true,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
642
1249
|
configOrigin(pluginId) {
|
|
643
|
-
|
|
1250
|
+
const origin = this.#router.configOrigin(pluginId);
|
|
1251
|
+
if (origin)
|
|
1252
|
+
return origin;
|
|
1253
|
+
if (this.#router.snapshot.current.route.name === 'result'
|
|
1254
|
+
&& this.#resultAnchor
|
|
1255
|
+
&& (this.#resultAnchor.name === 'pluginPreview' || this.#resultAnchor.name === 'pluginDetail')
|
|
1256
|
+
&& this.#resultAnchor.pluginId === pluginId) {
|
|
1257
|
+
return structuredClone(this.#resultAnchor);
|
|
1258
|
+
}
|
|
1259
|
+
return undefined;
|
|
644
1260
|
}
|
|
645
1261
|
updatePageContext(context) {
|
|
646
1262
|
this.#router.updateContext(context);
|
|
@@ -648,6 +1264,7 @@ export class TuiController {
|
|
|
648
1264
|
}
|
|
649
1265
|
back() {
|
|
650
1266
|
const route = this.#router.snapshot.current.route;
|
|
1267
|
+
const activeFlow = this.#router.activeFlow();
|
|
651
1268
|
this.#cancelReviewPreparation();
|
|
652
1269
|
this.#cancelPortableRequest();
|
|
653
1270
|
this.#cancelInstallPlacementRequest();
|
|
@@ -656,6 +1273,14 @@ export class TuiController {
|
|
|
656
1273
|
this.#activeReview = undefined;
|
|
657
1274
|
}
|
|
658
1275
|
this.#router.back();
|
|
1276
|
+
if (activeFlow
|
|
1277
|
+
&& this.#router.snapshot.currentFrameId === activeFlow.originFrameId) {
|
|
1278
|
+
this.#router.abandonFlow(activeFlow.id);
|
|
1279
|
+
if (activeFlow.kind === 'pluginInstall')
|
|
1280
|
+
this.#installPlacementDraft = undefined;
|
|
1281
|
+
if (activeFlow.kind === 'portableInstall')
|
|
1282
|
+
this.#portableDraft = undefined;
|
|
1283
|
+
}
|
|
659
1284
|
this.#normalizeCurrentRoute();
|
|
660
1285
|
this.#discardPortableDraftForRoute(this.#router.snapshot.current.route);
|
|
661
1286
|
this.#publish();
|
|
@@ -666,13 +1291,34 @@ export class TuiController {
|
|
|
666
1291
|
this.#activeReview = undefined;
|
|
667
1292
|
}
|
|
668
1293
|
this.#result = undefined;
|
|
669
|
-
this.#router.
|
|
1294
|
+
const activeFlow = this.#router.activeFlow();
|
|
1295
|
+
if (activeFlow?.kind === 'sourceEdit') {
|
|
1296
|
+
this.#router.abandonFlow(activeFlow.id);
|
|
1297
|
+
if (JSON.stringify(this.#router.snapshot.current.route) !== JSON.stringify(returnRoute)) {
|
|
1298
|
+
this.#router.replace(returnRoute);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
this.#router.discardSourceFlow(owner, returnRoute);
|
|
1303
|
+
}
|
|
670
1304
|
this.#normalizeCurrentRoute();
|
|
671
1305
|
this.#publish();
|
|
672
1306
|
}
|
|
673
1307
|
leaveResult(target) {
|
|
674
1308
|
if (this.#router.snapshot.current.route.name !== 'result')
|
|
675
1309
|
return;
|
|
1310
|
+
if (target?.name === 'home') {
|
|
1311
|
+
this.#router.resetToRoot();
|
|
1312
|
+
this.#portableDraft = undefined;
|
|
1313
|
+
this.#recoverablePortableDraft = undefined;
|
|
1314
|
+
this.#installPlacementDraft = undefined;
|
|
1315
|
+
this.#recoverableInstallPlacementDraft = undefined;
|
|
1316
|
+
this.#result = undefined;
|
|
1317
|
+
this.#resultAnchor = undefined;
|
|
1318
|
+
this.#portableResultSkills = undefined;
|
|
1319
|
+
this.#publish();
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
676
1322
|
if (this.#result?.operationKind === 'portableInstall'
|
|
677
1323
|
&& 'pluginId' in this.#result && this.#result.pluginId) {
|
|
678
1324
|
const preserveDraft = target?.name === 'portableSkillSelect'
|
|
@@ -688,19 +1334,55 @@ export class TuiController {
|
|
|
688
1334
|
this.#publish();
|
|
689
1335
|
return;
|
|
690
1336
|
}
|
|
691
|
-
const route = target ?? (this.#result && 'returnRoute' in this.#result
|
|
692
|
-
? this.#result.returnRoute
|
|
693
|
-
: undefined);
|
|
694
1337
|
this.#router.back();
|
|
695
|
-
if (
|
|
696
|
-
this.#router.replace(this.#normalizeRoute(
|
|
1338
|
+
if (target)
|
|
1339
|
+
this.#router.replace(this.#normalizeRoute(target));
|
|
697
1340
|
else
|
|
698
1341
|
this.#normalizeCurrentRoute();
|
|
699
1342
|
this.#discardPortableDraftForRoute(this.#router.snapshot.current.route);
|
|
700
1343
|
this.#result = undefined;
|
|
1344
|
+
this.#resultAnchor = undefined;
|
|
1345
|
+
this.#portableResultSkills = undefined;
|
|
1346
|
+
this.#recoverableInstallPlacementDraft = undefined;
|
|
1347
|
+
this.#recoverablePortableDraft = undefined;
|
|
1348
|
+
this.#publish();
|
|
1349
|
+
}
|
|
1350
|
+
resumeResultDraft() {
|
|
1351
|
+
const result = this.#result;
|
|
1352
|
+
if (this.#router.snapshot.current.route.name !== 'result' || !result
|
|
1353
|
+
|| !('returnRoute' in result) || !result.returnRoute)
|
|
1354
|
+
return;
|
|
1355
|
+
const returnRoute = this.#normalizeRoute(result.returnRoute);
|
|
1356
|
+
this.#router.back();
|
|
1357
|
+
const kind = result.operationKind === 'configSave' || result.operationKind === 'configReset'
|
|
1358
|
+
? 'configEdit'
|
|
1359
|
+
: result.operationKind === 'portableInstall'
|
|
1360
|
+
? 'portableInstall'
|
|
1361
|
+
: result.operationKind === 'sourceMutation' || result.operationKind === 'catalogGeneration'
|
|
1362
|
+
|| result.operationKind === 'catalogSourcesRecovery'
|
|
1363
|
+
? 'sourceEdit'
|
|
1364
|
+
: 'pluginMaintenance';
|
|
1365
|
+
this.#router.beginFlow(kind, this.#router.snapshot.current.route);
|
|
1366
|
+
if (result.operationKind === 'pluginWrite') {
|
|
1367
|
+
this.#installPlacementDraft = this.#recoverableInstallPlacementDraft;
|
|
1368
|
+
}
|
|
1369
|
+
if (result.operationKind === 'portableInstall') {
|
|
1370
|
+
this.#portableDraft = this.#recoverablePortableDraft;
|
|
1371
|
+
}
|
|
1372
|
+
this.#router.push(returnRoute);
|
|
1373
|
+
this.#recoverableInstallPlacementDraft = undefined;
|
|
1374
|
+
this.#recoverablePortableDraft = undefined;
|
|
1375
|
+
this.#result = undefined;
|
|
1376
|
+
this.#resultAnchor = undefined;
|
|
1377
|
+
this.#portableResultSkills = undefined;
|
|
701
1378
|
this.#publish();
|
|
702
1379
|
}
|
|
703
1380
|
#discardPortableDraftForRoute(route) {
|
|
1381
|
+
const readOnlyHistoryRoute = route.name === 'operationHistory'
|
|
1382
|
+
|| route.name === 'operationDiagnostic'
|
|
1383
|
+
|| route.name === 'operationReportPreview';
|
|
1384
|
+
if (readOnlyHistoryRoute && this.#router.activeFlow()?.kind === 'portableInstall')
|
|
1385
|
+
return;
|
|
704
1386
|
const portableRoute = route.name === 'portableSkillSelect'
|
|
705
1387
|
|| route.name === 'portableDependencies'
|
|
706
1388
|
|| route.name === 'portableAgentMode'
|
|
@@ -709,7 +1391,8 @@ export class TuiController {
|
|
|
709
1391
|
|| route.name === 'portableBlocker';
|
|
710
1392
|
const portableOperationRoute = (route.name === 'reviewPreparing' || route.name === 'review'
|
|
711
1393
|
|| route.name === 'progress' || route.name === 'result')
|
|
712
|
-
&& (this.#reviewPreparation
|
|
1394
|
+
&& (this.#reviewPreparation !== undefined
|
|
1395
|
+
&& this.#reviewPreparationIntents.get(this.#reviewPreparation.snapshot.id)?.kind === 'portableInstall'
|
|
713
1396
|
|| this.#activeReview?.operationKind === 'portableInstall'
|
|
714
1397
|
|| this.#result?.operationKind === 'portableInstall');
|
|
715
1398
|
if (!portableRoute && !portableOperationRoute) {
|
|
@@ -719,15 +1402,24 @@ export class TuiController {
|
|
|
719
1402
|
}
|
|
720
1403
|
resetPortableFlowToPreview(pluginId, target, preserveDraft = false) {
|
|
721
1404
|
this.#cancelPortableRequest();
|
|
1405
|
+
if (preserveDraft && !this.#portableDraft) {
|
|
1406
|
+
this.#portableDraft = this.#recoverablePortableDraft;
|
|
1407
|
+
}
|
|
722
1408
|
if (!preserveDraft)
|
|
723
1409
|
this.#portableDraft = undefined;
|
|
1410
|
+
this.#recoverablePortableDraft = undefined;
|
|
724
1411
|
if (this.#activeReview?.operationKind === 'portableInstall') {
|
|
725
1412
|
this.#tickets.delete(this.#activeReview.reviewToken);
|
|
726
1413
|
this.#activeReview = undefined;
|
|
727
1414
|
}
|
|
728
1415
|
if (this.#result?.operationKind === 'portableInstall')
|
|
729
1416
|
this.#result = undefined;
|
|
730
|
-
this.#
|
|
1417
|
+
this.#resultAnchor = undefined;
|
|
1418
|
+
this.#portableResultSkills = undefined;
|
|
1419
|
+
if (this.#router.snapshot.current.route.name === 'result')
|
|
1420
|
+
this.#router.back();
|
|
1421
|
+
else
|
|
1422
|
+
this.#router.resetPortableFlow(pluginId);
|
|
731
1423
|
if (target && JSON.stringify(this.#router.snapshot.current.route) !== JSON.stringify(target)) {
|
|
732
1424
|
this.#router.beginPortableFlow(pluginId);
|
|
733
1425
|
this.#router.push(this.#normalizeRoute(target));
|
|
@@ -764,8 +1456,11 @@ export class TuiController {
|
|
|
764
1456
|
throw new DOMException('Portable request was cancelled.', 'AbortError');
|
|
765
1457
|
}
|
|
766
1458
|
}
|
|
767
|
-
beginInstallFlow(pluginId, rootScope
|
|
1459
|
+
beginInstallFlow(pluginId, rootScope) {
|
|
768
1460
|
this.#cancelInstallPlacementRequest();
|
|
1461
|
+
const active = this.#router.activeFlow();
|
|
1462
|
+
if (active?.kind !== 'pluginInstall')
|
|
1463
|
+
this.#router.beginPluginFlow('pluginInstall', pluginId);
|
|
769
1464
|
this.#installPlacementDraft = {
|
|
770
1465
|
pluginId,
|
|
771
1466
|
rootScope,
|
|
@@ -774,7 +1469,6 @@ export class TuiController {
|
|
|
774
1469
|
state: 'selecting',
|
|
775
1470
|
targets: [],
|
|
776
1471
|
canContinue: false,
|
|
777
|
-
adjustingExisting: options.adjustingExisting === true,
|
|
778
1472
|
};
|
|
779
1473
|
this.#publish();
|
|
780
1474
|
}
|
|
@@ -834,10 +1528,10 @@ export class TuiController {
|
|
|
834
1528
|
...(draft.dependencyScopeOverrides.length > 0
|
|
835
1529
|
? { dependencyScopeOverrides: draft.dependencyScopeOverrides }
|
|
836
1530
|
: {}),
|
|
837
|
-
...(draft.adjustingExisting ? { dependencyPlacementAdjustment: true } : {}),
|
|
838
1531
|
};
|
|
839
1532
|
}
|
|
840
1533
|
async prepareInstallDependencyPlacement() {
|
|
1534
|
+
this.#assertAcceptingActions();
|
|
841
1535
|
const draft = this.#installPlacementDraft;
|
|
842
1536
|
if (!draft)
|
|
843
1537
|
throw new TuiOperationBlockedError('installPlacementMissing', '安装位置草稿不存在。');
|
|
@@ -859,7 +1553,7 @@ export class TuiController {
|
|
|
859
1553
|
throw new DOMException('Dependency placement preview was cancelled.', 'AbortError');
|
|
860
1554
|
}
|
|
861
1555
|
};
|
|
862
|
-
const plan = await preview(this.#installPlacementIntent());
|
|
1556
|
+
const plan = await this.#trackRead(preview(this.#installPlacementIntent()));
|
|
863
1557
|
throwIfCancelled();
|
|
864
1558
|
if (!plan) {
|
|
865
1559
|
draft.state = 'ready';
|
|
@@ -925,7 +1619,7 @@ export class TuiController {
|
|
|
925
1619
|
listPlugins(options) {
|
|
926
1620
|
const query = options.query?.trim().toLocaleLowerCase('en');
|
|
927
1621
|
return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
|
|
928
|
-
&& (options.mode === 'browse' || hasInstalledPlacement(plugin))
|
|
1622
|
+
&& (options.mode === 'browse' || hasInstalledPlacement(plugin) || plugin.savedConfigScopes.length > 0)
|
|
929
1623
|
&& (options.installFilter !== 'uninstalled' || !hasInstalledPlacement(plugin))
|
|
930
1624
|
&& (!query || `${plugin.displayName}\n${plugin.description}\n${plugin.sourceId}`.toLocaleLowerCase('en').includes(query))));
|
|
931
1625
|
}
|
|
@@ -1026,6 +1720,10 @@ export class TuiController {
|
|
|
1026
1720
|
health: this.#health.get(sourceId),
|
|
1027
1721
|
});
|
|
1028
1722
|
}
|
|
1723
|
+
getConfiguredSource(sourceId) {
|
|
1724
|
+
const source = this.#status?.configuredSources?.find((candidate) => candidate.id === sourceId);
|
|
1725
|
+
return source ? cloneFrozen(source) : undefined;
|
|
1726
|
+
}
|
|
1029
1727
|
getCatalogGeneration(sourceId) {
|
|
1030
1728
|
const source = this.#catalogGenerations.get(sourceId);
|
|
1031
1729
|
return source ? cloneFrozen(source) : undefined;
|
|
@@ -1035,17 +1733,70 @@ export class TuiController {
|
|
|
1035
1733
|
return freshness ? cloneFrozen(freshness) : undefined;
|
|
1036
1734
|
}
|
|
1037
1735
|
recheckCatalogSource(sourceId) {
|
|
1736
|
+
this.#assertAcceptingActions();
|
|
1038
1737
|
return this.#catalogSourcesSession.recheck(sourceId);
|
|
1039
1738
|
}
|
|
1739
|
+
async recheckAllCatalogSources() {
|
|
1740
|
+
this.#assertAcceptingActions();
|
|
1741
|
+
const sourceIds = [...this.listSources()]
|
|
1742
|
+
.sort((left, right) => Number(Boolean(right.primary)) - Number(Boolean(left.primary)))
|
|
1743
|
+
.map((source) => source.id);
|
|
1744
|
+
let next = 0;
|
|
1745
|
+
const worker = async () => {
|
|
1746
|
+
while (next < sourceIds.length) {
|
|
1747
|
+
const sourceId = sourceIds[next++];
|
|
1748
|
+
if (sourceId)
|
|
1749
|
+
await this.#catalogSourcesSession.recheck(sourceId);
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
await Promise.all(Array.from({ length: Math.min(2, sourceIds.length) }, () => worker()));
|
|
1753
|
+
}
|
|
1040
1754
|
fullValidateCatalogSource(sourceId) {
|
|
1755
|
+
this.#assertAcceptingActions();
|
|
1041
1756
|
return this.#catalogSourcesSession.fullValidate(sourceId);
|
|
1042
1757
|
}
|
|
1043
|
-
setExternalDraftState(
|
|
1044
|
-
|
|
1758
|
+
setExternalDraftState(states = []) {
|
|
1759
|
+
const priority = { sourceDraft: 0, batchDraft: 1, configDraft: 2 };
|
|
1760
|
+
this.#externalDrafts = structuredClone(states).sort((left, right) => priority[left.kind] - priority[right.kind] || left.owner.localeCompare(right.owner, 'en'));
|
|
1761
|
+
this.#publish();
|
|
1762
|
+
}
|
|
1763
|
+
beginPluginWriteBatch() {
|
|
1764
|
+
this.#assertAcceptingActions();
|
|
1765
|
+
this.#router.beginBatchFlow();
|
|
1766
|
+
this.#router.push({ name: 'batchSelection' });
|
|
1767
|
+
this.#publish();
|
|
1768
|
+
}
|
|
1769
|
+
startPluginWriteBatchReview(intent) {
|
|
1770
|
+
return this.startReviewPreparation(intent, { name: 'batchPlacement' });
|
|
1771
|
+
}
|
|
1772
|
+
retryFailedPluginWriteBatch() {
|
|
1773
|
+
if (!this.#batchRetryIntent)
|
|
1774
|
+
return undefined;
|
|
1775
|
+
const intent = structuredClone(this.#batchRetryIntent);
|
|
1776
|
+
this.#batchRetryIntent = undefined;
|
|
1777
|
+
return this.startReviewPreparation(intent, { name: 'pluginList', mode: 'installed' });
|
|
1045
1778
|
}
|
|
1046
1779
|
catalogActivationBlockReason() {
|
|
1047
|
-
if (this.#
|
|
1048
|
-
return this.#
|
|
1780
|
+
if (this.#externalDrafts[0])
|
|
1781
|
+
return this.#externalDrafts[0].kind;
|
|
1782
|
+
if (this.#portableDraft)
|
|
1783
|
+
return 'portableDraft';
|
|
1784
|
+
if (this.#reviewPreparation)
|
|
1785
|
+
return 'reviewPreparation';
|
|
1786
|
+
if (this.#activeReview)
|
|
1787
|
+
return 'activeReview';
|
|
1788
|
+
if (this.#router.snapshot.current.route.name === 'progress'
|
|
1789
|
+
|| this.#inflightOperations.size > 0)
|
|
1790
|
+
return 'writeProgress';
|
|
1791
|
+
return undefined;
|
|
1792
|
+
}
|
|
1793
|
+
catalogSourceRecoveryBlockReason() {
|
|
1794
|
+
const activeFlow = this.#router.activeFlow();
|
|
1795
|
+
const blockingExternalDraft = this.#externalDrafts.find((draft) => !(draft.kind === 'configDraft'
|
|
1796
|
+
&& activeFlow?.kind === 'sourceRecovery'
|
|
1797
|
+
&& activeFlow.parentFlowId !== undefined));
|
|
1798
|
+
if (blockingExternalDraft)
|
|
1799
|
+
return blockingExternalDraft.kind;
|
|
1049
1800
|
if (this.#portableDraft)
|
|
1050
1801
|
return 'portableDraft';
|
|
1051
1802
|
if (this.#reviewPreparation)
|
|
@@ -1061,6 +1812,7 @@ export class TuiController {
|
|
|
1061
1812
|
return this.#status?.payload.catalog.sources.find((source) => source.id === sourceId)?.catalogVersion;
|
|
1062
1813
|
}
|
|
1063
1814
|
async reloadCatalog(options) {
|
|
1815
|
+
this.#assertAcceptingActions();
|
|
1064
1816
|
if (this.catalogActivationBlockReason())
|
|
1065
1817
|
return false;
|
|
1066
1818
|
this.dependencies.beginCatalogSession?.();
|
|
@@ -1068,7 +1820,7 @@ export class TuiController {
|
|
|
1068
1820
|
this.#phase = 'loading';
|
|
1069
1821
|
this.#publish();
|
|
1070
1822
|
try {
|
|
1071
|
-
const status = await this.dependencies.loadStatus(this.#abort.signal);
|
|
1823
|
+
const status = await this.#trackRead(this.dependencies.loadStatus(this.#abort.signal));
|
|
1072
1824
|
if (this.#disposed || generation !== this.#generation)
|
|
1073
1825
|
return false;
|
|
1074
1826
|
this.#applyStatus(status);
|
|
@@ -1090,39 +1842,45 @@ export class TuiController {
|
|
|
1090
1842
|
}
|
|
1091
1843
|
}
|
|
1092
1844
|
async inspectSourceRemoval(sourceId) {
|
|
1845
|
+
this.#assertAcceptingActions();
|
|
1093
1846
|
if (!this.dependencies.inspectSourceRemoval) {
|
|
1094
1847
|
throw new TuiOperationBlockedError('stateInventoryFailed', '来源引用清单不可用。');
|
|
1095
1848
|
}
|
|
1096
|
-
return cloneFrozen(await this.dependencies.inspectSourceRemoval(sourceId, this.#abort.signal));
|
|
1849
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.inspectSourceRemoval(sourceId, this.#abort.signal)));
|
|
1097
1850
|
}
|
|
1098
1851
|
async getConfigGroups(pluginId, layer) {
|
|
1852
|
+
this.#assertAcceptingActions();
|
|
1099
1853
|
if (!this.dependencies.getConfigGroups) {
|
|
1100
1854
|
throw new TuiOperationBlockedError('configUnavailable', '设置读取入口不可用。');
|
|
1101
1855
|
}
|
|
1102
|
-
return cloneFrozen(await this.dependencies.getConfigGroups(pluginId, structuredClone(layer), this.#abort.signal));
|
|
1856
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.getConfigGroups(pluginId, structuredClone(layer), this.#abort.signal)));
|
|
1103
1857
|
}
|
|
1104
1858
|
async getConfigPage(pluginId, layer, signal) {
|
|
1859
|
+
this.#assertAcceptingActions();
|
|
1105
1860
|
if (!this.dependencies.getConfigPage) {
|
|
1106
1861
|
throw new TuiOperationBlockedError('configUnavailable', '设置读取入口不可用。');
|
|
1107
1862
|
}
|
|
1108
1863
|
const combined = signal
|
|
1109
1864
|
? AbortSignal.any([this.#abort.signal, signal])
|
|
1110
1865
|
: this.#abort.signal;
|
|
1111
|
-
return cloneFrozen(await this.dependencies.getConfigPage(pluginId, structuredClone(layer), combined));
|
|
1866
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.getConfigPage(pluginId, structuredClone(layer), combined)));
|
|
1112
1867
|
}
|
|
1113
1868
|
async getConfigResetAvailability(pluginId, layer) {
|
|
1869
|
+
this.#assertAcceptingActions();
|
|
1114
1870
|
if (!this.dependencies.getConfigResetAvailability) {
|
|
1115
1871
|
throw new TuiOperationBlockedError('configResetVerificationUnavailable', '设置层可信读取入口不可用。');
|
|
1116
1872
|
}
|
|
1117
|
-
return cloneFrozen(await this.dependencies.getConfigResetAvailability(pluginId, structuredClone(layer), this.#abort.signal));
|
|
1873
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.getConfigResetAvailability(pluginId, structuredClone(layer), this.#abort.signal)));
|
|
1118
1874
|
}
|
|
1119
1875
|
listUserIssues() {
|
|
1120
1876
|
return cloneFrozen(this.#viewModel?.issues ?? []);
|
|
1121
1877
|
}
|
|
1122
1878
|
async buildOperationReview(intent, returnRoute) {
|
|
1879
|
+
this.#assertAcceptingActions();
|
|
1880
|
+
intent = normalizeOperationIntent(intent);
|
|
1123
1881
|
try {
|
|
1124
1882
|
this.#validateReviewIntent(intent);
|
|
1125
|
-
const evidence = await this.#prepareReviewEvidence(intent, this.#abort.signal);
|
|
1883
|
+
const evidence = await this.#trackRead(this.#prepareReviewEvidence(intent, this.#abort.signal));
|
|
1126
1884
|
return this.#storeOperationReview(intent, evidence, 'push', returnRoute);
|
|
1127
1885
|
}
|
|
1128
1886
|
catch (error) {
|
|
@@ -1134,12 +1892,15 @@ export class TuiController {
|
|
|
1134
1892
|
}
|
|
1135
1893
|
}
|
|
1136
1894
|
startReviewPreparation(intent, returnRoute) {
|
|
1895
|
+
this.#assertAcceptingActions();
|
|
1896
|
+
intent = normalizeOperationIntent(intent);
|
|
1137
1897
|
if (intent.kind === 'catalogGeneration' && !intent.sourceMutation) {
|
|
1138
|
-
const blockReason = this.
|
|
1898
|
+
const blockReason = this.catalogSourceRecoveryBlockReason();
|
|
1139
1899
|
if (blockReason) {
|
|
1140
1900
|
throw new TuiOperationBlockedError('catalogActivationBlocked', `Catalog Active 切换已阻止:${blockReason}。请先完成或放弃当前草稿或操作。`);
|
|
1141
1901
|
}
|
|
1142
1902
|
}
|
|
1903
|
+
this.#ensureOperationFlow(intent);
|
|
1143
1904
|
const inheritedReturnRoute = this.#reviewPreparation?.returnRoute;
|
|
1144
1905
|
const current = this.#router.snapshot.current.route;
|
|
1145
1906
|
const navigation = current.name === 'result' || current.name === 'reviewPreparing'
|
|
@@ -1151,7 +1912,9 @@ export class TuiController {
|
|
|
1151
1912
|
? this.#result.returnRoute
|
|
1152
1913
|
: current);
|
|
1153
1914
|
const normalizedReturnRoute = this.#normalizeRoute(resolvedReturnRoute);
|
|
1154
|
-
const key =
|
|
1915
|
+
const key = crypto.createHash('sha256')
|
|
1916
|
+
.update(JSON.stringify(stableValue({ intent, returnRoute: normalizedReturnRoute })))
|
|
1917
|
+
.digest('hex');
|
|
1155
1918
|
if (this.#reviewPreparation?.snapshot.state === 'running'
|
|
1156
1919
|
&& this.#reviewPreparation.key === key) {
|
|
1157
1920
|
return cloneFrozen(this.#reviewPreparation.snapshot);
|
|
@@ -1162,14 +1925,16 @@ export class TuiController {
|
|
|
1162
1925
|
const token = ++this.#reviewPreparationToken;
|
|
1163
1926
|
const snapshot = {
|
|
1164
1927
|
id,
|
|
1165
|
-
|
|
1928
|
+
operationKind: intent.kind,
|
|
1929
|
+
subject: this.#reviewPreparationLabel(intent),
|
|
1166
1930
|
state: 'running',
|
|
1167
1931
|
stages: this.dependencies.prepareReviewEvidence ? [] : [
|
|
1168
1932
|
{ id: 'validate', label: '验证当前状态', state: 'running' },
|
|
1169
1933
|
{ id: 'evidence', label: '准备确认信息', state: 'pending' },
|
|
1170
1934
|
],
|
|
1171
|
-
|
|
1935
|
+
...(this.dependencies.prepareReviewEvidence ? {} : { currentStage: 'validate' }),
|
|
1172
1936
|
};
|
|
1937
|
+
this.#reviewPreparationIntents.set(id, structuredClone(intent));
|
|
1173
1938
|
this.#reviewPreparation = {
|
|
1174
1939
|
snapshot,
|
|
1175
1940
|
key,
|
|
@@ -1187,9 +1952,25 @@ export class TuiController {
|
|
|
1187
1952
|
const preparation = this.#reviewPreparation;
|
|
1188
1953
|
if (!preparation || preparation.snapshot.state !== 'failed')
|
|
1189
1954
|
return undefined;
|
|
1190
|
-
|
|
1955
|
+
const intent = this.#reviewPreparationIntents.get(preparation.snapshot.id);
|
|
1956
|
+
if (!intent)
|
|
1957
|
+
return undefined;
|
|
1958
|
+
return this.startReviewPreparation(structuredClone(intent), preparation.returnRoute);
|
|
1191
1959
|
}
|
|
1192
1960
|
#validateReviewIntent(intent) {
|
|
1961
|
+
if (intent.kind === 'pluginWriteBatch') {
|
|
1962
|
+
if (intent.selection.kind === 'currentProjectIntentRoots'
|
|
1963
|
+
&& (intent.action !== 'sync' || intent.scope !== 'project')) {
|
|
1964
|
+
throw new TuiOperationBlockedError('batchSelectionInvalid', '当前项目全部同步只支持 project scope sync。');
|
|
1965
|
+
}
|
|
1966
|
+
if (intent.selection.kind === 'selected') {
|
|
1967
|
+
const canonical = canonicalPluginIds(intent.selection.pluginIds);
|
|
1968
|
+
if (canonical.length === 0) {
|
|
1969
|
+
throw new TuiOperationBlockedError('batchSelectionInvalid', '批量插件选择无效。');
|
|
1970
|
+
}
|
|
1971
|
+
canonical.forEach(sourceIdFromPluginId);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1193
1974
|
const primaryGeneration = intent.kind === 'sourceMutation' && intent.action === 'setPrimary'
|
|
1194
1975
|
? this.#catalogGenerations.get(intent.sourceId) : undefined;
|
|
1195
1976
|
if (primaryGeneration && !primaryGeneration.active && primaryGeneration.state !== 'ready') {
|
|
@@ -1199,6 +1980,11 @@ export class TuiController {
|
|
|
1199
1980
|
#reviewPreparationLabel(intent) {
|
|
1200
1981
|
if (intent.kind === 'catalogSourcesRecovery')
|
|
1201
1982
|
return 'Catalog 来源配置';
|
|
1983
|
+
if (intent.kind === 'pluginWriteBatch') {
|
|
1984
|
+
return intent.selection.kind === 'selected'
|
|
1985
|
+
? `${intent.selection.pluginIds.length} 个插件`
|
|
1986
|
+
: '当前项目全部插件';
|
|
1987
|
+
}
|
|
1202
1988
|
if ('pluginId' in intent) {
|
|
1203
1989
|
return this.#viewModel?.plugins.find((plugin) => plugin.pluginId === intent.pluginId)?.displayName
|
|
1204
1990
|
?? pluginNameFromId(intent.pluginId);
|
|
@@ -1217,13 +2003,17 @@ export class TuiController {
|
|
|
1217
2003
|
const preparation = this.#reviewPreparation;
|
|
1218
2004
|
if (!preparation || preparation.token !== token)
|
|
1219
2005
|
return;
|
|
1220
|
-
const
|
|
2006
|
+
const storedIntent = this.#reviewPreparationIntents.get(preparation.snapshot.id);
|
|
2007
|
+
if (!storedIntent)
|
|
2008
|
+
return;
|
|
2009
|
+
const intent = structuredClone(storedIntent);
|
|
1221
2010
|
const signal = AbortSignal.any([this.#abort.signal, preparation.abort.signal]);
|
|
1222
2011
|
try {
|
|
1223
2012
|
this.#validateReviewIntent(intent);
|
|
1224
|
-
const evidence = await this.#prepareReviewEvidence(intent, signal, (stage) => {
|
|
2013
|
+
const evidence = await this.#trackRead(this.#prepareReviewEvidence(intent, signal, (stage) => {
|
|
1225
2014
|
if (!this.#reviewPreparationIsCurrent(token))
|
|
1226
2015
|
return;
|
|
2016
|
+
preparation.snapshot.currentStage = stage;
|
|
1227
2017
|
const existing = preparation.snapshot.stages.find((entry) => entry.id === stage);
|
|
1228
2018
|
preparation.snapshot.stages = preparation.snapshot.stages.map((entry) => entry.state === 'running' ? { ...entry, state: 'completed' } : entry);
|
|
1229
2019
|
if (existing) {
|
|
@@ -1237,12 +2027,13 @@ export class TuiController {
|
|
|
1237
2027
|
});
|
|
1238
2028
|
}
|
|
1239
2029
|
this.#publish();
|
|
1240
|
-
});
|
|
2030
|
+
}));
|
|
1241
2031
|
if (!this.#reviewPreparationIsCurrent(token))
|
|
1242
2032
|
return;
|
|
1243
2033
|
preparation.snapshot.stages = preparation.snapshot.stages.map((stage) => stage.state === 'running' ? { ...stage, state: 'completed' } : stage);
|
|
1244
2034
|
this.#publish();
|
|
1245
2035
|
const returnRoute = structuredClone(preparation.returnRoute);
|
|
2036
|
+
this.#reviewPreparationIntents.delete(preparation.snapshot.id);
|
|
1246
2037
|
this.#reviewPreparation = undefined;
|
|
1247
2038
|
this.#storeOperationReview(intent, evidence, 'replace', returnRoute);
|
|
1248
2039
|
}
|
|
@@ -1269,7 +2060,10 @@ export class TuiController {
|
|
|
1269
2060
|
: code === 'catalogGenerationActiveComparisonUnavailable'
|
|
1270
2061
|
? '损坏的当前 Active 无法提供可信版本,不能安全比较候选。当前 Active 保持不变。请明确编辑来源,或移除后重新添加。'
|
|
1271
2062
|
: code === 'catalogReplaceDamagedRequired'
|
|
1272
|
-
?
|
|
2063
|
+
? intent.kind === 'catalogGeneration'
|
|
2064
|
+
&& this.#catalogFreshness.get(intent.sourceId)?.state === 'incompatible'
|
|
2065
|
+
? '无法重新取得或精确匹配旧 Active,且当前候选与此 AW 不兼容。当前 Active 保持不变。请升级 AW 或修改来源。'
|
|
2066
|
+
: '无法重新取得或精确匹配旧 Active。当前 Active 保持不变。请返回并选择“使用当前候选替换损坏的 Active”。'
|
|
1273
2067
|
: code === 'catalogVersionConflict'
|
|
1274
2068
|
? '候选 Catalog 与当前 Active 版本相同,但内容摘要不同。普通更新已阻止。'
|
|
1275
2069
|
: code === 'catalogRollbackDetected'
|
|
@@ -1277,6 +2071,34 @@ export class TuiController {
|
|
|
1277
2071
|
: code === 'catalogGenerationUnavailable'
|
|
1278
2072
|
? '本次加载时 Catalog 来源不可用。请刷新来源并重新加载后重试。'
|
|
1279
2073
|
: '无法准备确认信息。请返回检查选择,或重试。';
|
|
2074
|
+
const preparationRecordId = `${this.#sessionEpoch}:${active.snapshot.id}`;
|
|
2075
|
+
const preparationDiagnostic = projectThrownOperationDiagnostic({
|
|
2076
|
+
error,
|
|
2077
|
+
id: `${preparationRecordId}:failure`,
|
|
2078
|
+
stage: active.snapshot.currentStage ?? 'reviewPreparation',
|
|
2079
|
+
fallbackCode: code,
|
|
2080
|
+
sensitiveValues: collectOperationSensitiveValues(intent),
|
|
2081
|
+
});
|
|
2082
|
+
this.#operationHistory.record({
|
|
2083
|
+
summary: {
|
|
2084
|
+
recordId: preparationRecordId,
|
|
2085
|
+
epoch: this.#sessionEpoch,
|
|
2086
|
+
operationId: active.snapshot.id,
|
|
2087
|
+
createdAtMs: this.dependencies.now?.() ?? Date.now(),
|
|
2088
|
+
state: 'blocked',
|
|
2089
|
+
code: preparationDiagnostic.code,
|
|
2090
|
+
message: active.snapshot.message,
|
|
2091
|
+
operationKind: intent.kind,
|
|
2092
|
+
...('pluginId' in intent ? { pluginId: intent.pluginId } : {}),
|
|
2093
|
+
diagnosticCount: 1,
|
|
2094
|
+
effects: {
|
|
2095
|
+
configCommit: 'notApplicable', awWrite: 'notStarted',
|
|
2096
|
+
preApplyExternalEffect: 'notApplicable', finalVerification: 'notStarted',
|
|
2097
|
+
},
|
|
2098
|
+
},
|
|
2099
|
+
diagnostics: [preparationDiagnostic],
|
|
2100
|
+
targets: [],
|
|
2101
|
+
}, { sensitiveValues: collectOperationSensitiveValues(intent) });
|
|
1280
2102
|
this.#publish();
|
|
1281
2103
|
await this.#logOperationError(error, code, intent);
|
|
1282
2104
|
}
|
|
@@ -1300,6 +2122,7 @@ export class TuiController {
|
|
|
1300
2122
|
return;
|
|
1301
2123
|
this.#reviewPreparationToken += 1;
|
|
1302
2124
|
this.#reviewPreparation.abort.abort();
|
|
2125
|
+
this.#reviewPreparationIntents.delete(this.#reviewPreparation.snapshot.id);
|
|
1303
2126
|
this.#reviewPreparation = undefined;
|
|
1304
2127
|
}
|
|
1305
2128
|
async #logOperationError(error, code, intent) {
|
|
@@ -1322,6 +2145,9 @@ export class TuiController {
|
|
|
1322
2145
|
}
|
|
1323
2146
|
}
|
|
1324
2147
|
#storeOperationReview(intent, evidence, navigation = 'push', returnRoute = this.#router.snapshot.current.route) {
|
|
2148
|
+
this.#ensureOperationFlow(intent);
|
|
2149
|
+
if (intent.kind === 'pluginWriteBatch')
|
|
2150
|
+
assertPluginWriteBatchEvidence(evidence);
|
|
1325
2151
|
if (this.#router.snapshot.current.route.name === 'result')
|
|
1326
2152
|
navigation = 'replace';
|
|
1327
2153
|
const projection = buildOperationReviewProjection({
|
|
@@ -1338,7 +2164,10 @@ export class TuiController {
|
|
|
1338
2164
|
const expiresAt = now
|
|
1339
2165
|
+ (this.dependencies.reviewTtlMs ?? 2 * 60_000);
|
|
1340
2166
|
const pluginId = 'pluginId' in intent ? intent.pluginId
|
|
1341
|
-
: 'sourceId' in intent ? intent.sourceId
|
|
2167
|
+
: 'sourceId' in intent ? intent.sourceId
|
|
2168
|
+
: intent.kind === 'pluginWriteBatch'
|
|
2169
|
+
? `${evidence.requestedPluginIds.length} 个插件`
|
|
2170
|
+
: 'Catalog 来源配置';
|
|
1342
2171
|
if ('pluginId' in intent)
|
|
1343
2172
|
sourceIdFromPluginId(intent.pluginId);
|
|
1344
2173
|
const pluginVersion = 'pluginId' in intent
|
|
@@ -1354,6 +2183,7 @@ export class TuiController {
|
|
|
1354
2183
|
: intent.kind === 'catalogGeneration' && intent.sourceMutation
|
|
1355
2184
|
? { sourceAction: intent.sourceMutation.action } : {}),
|
|
1356
2185
|
...(intent.kind === 'pluginWrite' ? { pluginAction: intent.action } : {}),
|
|
2186
|
+
...(intent.kind === 'pluginWriteBatch' ? { pluginAction: intent.action } : {}),
|
|
1357
2187
|
...(intent.kind === 'pluginWrite' && intent.originIssue
|
|
1358
2188
|
? { originIssue: intent.originIssue }
|
|
1359
2189
|
: {}),
|
|
@@ -1366,6 +2196,14 @@ export class TuiController {
|
|
|
1366
2196
|
...('pluginId' in intent ? { sourceLabel: sourceIdFromPluginId(intent.pluginId) } : {}),
|
|
1367
2197
|
...(pluginVersion ? { version: pluginVersion } : {}),
|
|
1368
2198
|
},
|
|
2199
|
+
...(intent.kind === 'pluginWriteBatch' ? {
|
|
2200
|
+
batch: {
|
|
2201
|
+
requestedPluginIds: [...evidence.requestedPluginIds],
|
|
2202
|
+
groupCount: evidence.groups.length,
|
|
2203
|
+
executableGroupCount: evidence.groups
|
|
2204
|
+
.filter((group) => group.executable).length,
|
|
2205
|
+
},
|
|
2206
|
+
} : {}),
|
|
1369
2207
|
...(evidence.source && 'catalogVersion' in evidence.source
|
|
1370
2208
|
&& typeof evidence.source.catalogVersion === 'string'
|
|
1371
2209
|
&& evidence.source.snapshotDigest
|
|
@@ -1389,6 +2227,14 @@ export class TuiController {
|
|
|
1389
2227
|
return cloneFrozen(review);
|
|
1390
2228
|
}
|
|
1391
2229
|
executeOperation(reviewToken) {
|
|
2230
|
+
if (this.#retiring || this.#disposed) {
|
|
2231
|
+
return Promise.resolve(cloneFrozen({
|
|
2232
|
+
state: 'blocked',
|
|
2233
|
+
operationId: this.#tickets.get(reviewToken)?.review.operationId ?? 'unknown',
|
|
2234
|
+
code: 'sessionRetiring',
|
|
2235
|
+
message: '会话正在结束,未启动新操作。',
|
|
2236
|
+
}));
|
|
2237
|
+
}
|
|
1392
2238
|
const inflight = this.#inflightOperations.get(reviewToken);
|
|
1393
2239
|
if (inflight)
|
|
1394
2240
|
return inflight.promise;
|
|
@@ -1409,6 +2255,7 @@ export class TuiController {
|
|
|
1409
2255
|
const promise = this.#executeOperationTicket(ticket, operationId).finally(() => {
|
|
1410
2256
|
if (this.#inflightOperations.get(reviewToken)?.promise === promise) {
|
|
1411
2257
|
this.#inflightOperations.delete(reviewToken);
|
|
2258
|
+
this.#publish();
|
|
1412
2259
|
}
|
|
1413
2260
|
});
|
|
1414
2261
|
this.#inflightOperations.set(reviewToken, { operationId, promise });
|
|
@@ -1418,8 +2265,10 @@ export class TuiController {
|
|
|
1418
2265
|
let generation = this.#generation;
|
|
1419
2266
|
const startedAtMs = this.dependencies.now?.() ?? Date.now();
|
|
1420
2267
|
this.#progressFeed?.clearCurrent?.();
|
|
1421
|
-
const
|
|
1422
|
-
|
|
2268
|
+
const dependencyTargets = ticket.intent.kind === 'pluginWriteBatch'
|
|
2269
|
+
? ticket.evidence.groups.flatMap((group) => group.preview.dependencyPlan?.targets ?? [])
|
|
2270
|
+
: ticket.evidence.dependencyPlan?.targets ?? [];
|
|
2271
|
+
this.#progressTargets = dependencyTargets.map((target) => ({
|
|
1423
2272
|
pluginId: target.pluginId,
|
|
1424
2273
|
platform: target.platform,
|
|
1425
2274
|
scope: target.resolvedScope,
|
|
@@ -1428,21 +2277,23 @@ export class TuiController {
|
|
|
1428
2277
|
: target.depth === 1 ? 'direct' : 'transitive',
|
|
1429
2278
|
requiredBy: target.requiredBy,
|
|
1430
2279
|
state: target.action === 'keep' ? 'kept' : 'pending',
|
|
1431
|
-
}))
|
|
2280
|
+
}));
|
|
1432
2281
|
this.#progress = {
|
|
1433
2282
|
messages: [],
|
|
1434
2283
|
startedAtMs,
|
|
1435
2284
|
elapsedMs: 0,
|
|
1436
2285
|
current: {
|
|
1437
|
-
target:
|
|
1438
|
-
|
|
2286
|
+
target: ticket.intent.kind === 'pluginWriteBatch'
|
|
2287
|
+
? `${ticket.evidence.requestedPluginIds.length} 个插件`
|
|
2288
|
+
: 'pluginId' in ticket.intent ? ticket.intent.pluginId
|
|
2289
|
+
: 'sourceId' in ticket.intent ? ticket.intent.sourceId : 'Catalog 来源配置',
|
|
1439
2290
|
phase: '验证当前状态',
|
|
1440
2291
|
},
|
|
1441
2292
|
};
|
|
1442
2293
|
this.#router.replace({ name: 'progress', operationId });
|
|
1443
2294
|
this.#publish();
|
|
1444
2295
|
try {
|
|
1445
|
-
if (ticket.intent.kind !== 'pluginWrite') {
|
|
2296
|
+
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch') {
|
|
1446
2297
|
await this.dependencies.validateIntent?.(structuredClone(ticket.intent), this.#abort.signal);
|
|
1447
2298
|
}
|
|
1448
2299
|
}
|
|
@@ -1473,7 +2324,8 @@ export class TuiController {
|
|
|
1473
2324
|
}
|
|
1474
2325
|
if (!this.#operationIsCurrent(generation))
|
|
1475
2326
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1476
|
-
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== '
|
|
2327
|
+
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
|
|
2328
|
+
&& ticket.intent.kind !== 'catalogGeneration') {
|
|
1477
2329
|
let current;
|
|
1478
2330
|
try {
|
|
1479
2331
|
current = await this.dependencies.captureEvidence(structuredClone(ticket.intent), this.#abort.signal);
|
|
@@ -1501,6 +2353,11 @@ export class TuiController {
|
|
|
1501
2353
|
const execution = ticket.intent.kind === 'catalogGeneration'
|
|
1502
2354
|
? await this.dependencies.executeIntent(structuredClone(ticket.intent), structuredClone(ticket.evidence), this.#abort.signal)
|
|
1503
2355
|
: await this.dependencies.executeIntent(structuredClone(ticket.intent), structuredClone(ticket.evidence));
|
|
2356
|
+
const executionProjection = projectExecutionDiagnostics(execution, `${this.#sessionEpoch}:${operationId}`, collectOperationSensitiveValues(ticket.intent), ticket.intent.kind);
|
|
2357
|
+
this.#operationDiagnostics.set(operationId, {
|
|
2358
|
+
...executionProjection,
|
|
2359
|
+
sensitiveValues: collectOperationSensitiveValues(ticket.intent),
|
|
2360
|
+
});
|
|
1504
2361
|
if (ticket.intent.kind === 'sourceMutation' && ticket.intent.action === 'update') {
|
|
1505
2362
|
const confirmedAutoCheck = catalogSourceAutoCheckOnlyValue(ticket.intent.update);
|
|
1506
2363
|
if (confirmedAutoCheck !== undefined) {
|
|
@@ -1541,8 +2398,49 @@ export class TuiController {
|
|
|
1541
2398
|
await this.#logErrorSafe(error, verificationCode, {
|
|
1542
2399
|
privateConfigOperation: true,
|
|
1543
2400
|
});
|
|
2401
|
+
const existingProjection = this.#operationDiagnostics.get(operationId);
|
|
2402
|
+
existingProjection.diagnostics.push(projectThrownOperationDiagnostic({
|
|
2403
|
+
error,
|
|
2404
|
+
id: `${this.#sessionEpoch}:${operationId}:status-reload`,
|
|
2405
|
+
stage: 'statusReload',
|
|
2406
|
+
fallbackCode: verificationCode,
|
|
2407
|
+
}));
|
|
2408
|
+
existingProjection.effects.finalVerification = 'failed';
|
|
1544
2409
|
if (!this.#operationIsCurrent(generation))
|
|
1545
2410
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2411
|
+
const configCommitted = typeof execution === 'object'
|
|
2412
|
+
&& execution !== null
|
|
2413
|
+
&& execution.configWrite === 'succeeded';
|
|
2414
|
+
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
|
|
2415
|
+
&& configCommitted) {
|
|
2416
|
+
const baseSummary = operationResultSummary(ticket.intent, execution);
|
|
2417
|
+
const elapsedMs = Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs);
|
|
2418
|
+
const summary = {
|
|
2419
|
+
...baseSummary,
|
|
2420
|
+
code: 'operationPartial',
|
|
2421
|
+
message: '设置已保存,但最终状态无法刷新。',
|
|
2422
|
+
elapsedMs,
|
|
2423
|
+
messages: [
|
|
2424
|
+
...this.#progress.messages,
|
|
2425
|
+
...(baseSummary.messages ?? []),
|
|
2426
|
+
'状态刷新失败。配置写入已经提交;请稍后刷新状态。',
|
|
2427
|
+
].slice(-8).map((message) => sanitizeDiagnosticText(message, [], collectOperationSensitiveValues(ticket.intent))),
|
|
2428
|
+
};
|
|
2429
|
+
this.#progress = { ...this.#progress, elapsedMs };
|
|
2430
|
+
const result = {
|
|
2431
|
+
state: 'partial',
|
|
2432
|
+
operationId,
|
|
2433
|
+
summary,
|
|
2434
|
+
operationKind: ticket.intent.kind,
|
|
2435
|
+
pluginId: ticket.intent.pluginId,
|
|
2436
|
+
returnRoute: structuredClone(this.#normalizeRoute(ticket.returnRoute)),
|
|
2437
|
+
};
|
|
2438
|
+
this.#result = structuredClone(result);
|
|
2439
|
+
this.#activeReview = undefined;
|
|
2440
|
+
this.#showResult(result, ticket.intent);
|
|
2441
|
+
this.#publish();
|
|
2442
|
+
return cloneFrozen(result);
|
|
2443
|
+
}
|
|
1546
2444
|
return this.#setBlocked(operationId, verificationCode, {
|
|
1547
2445
|
intent: ticket.intent,
|
|
1548
2446
|
navigation: 'result',
|
|
@@ -1555,11 +2453,16 @@ export class TuiController {
|
|
|
1555
2453
|
if (!this.#operationIsCurrent(generation))
|
|
1556
2454
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1557
2455
|
this.#applyStatus(refreshed);
|
|
2456
|
+
const verifiedProjection = this.#operationDiagnostics.get(operationId);
|
|
2457
|
+
if (verifiedProjection && verifiedProjection.effects.finalVerification !== 'failed') {
|
|
2458
|
+
verifiedProjection.effects.finalVerification = 'completed';
|
|
2459
|
+
}
|
|
1558
2460
|
if (ticket.intent.kind === 'catalogSourcesRecovery') {
|
|
1559
2461
|
this.#phase = 'ready';
|
|
1560
2462
|
this.#loadError = undefined;
|
|
1561
2463
|
}
|
|
1562
2464
|
const pluginSummary = ticket.intent.kind === 'pluginWrite'
|
|
2465
|
+
|| ticket.intent.kind === 'pluginWriteBatch'
|
|
1563
2466
|
? operationResultSummary(ticket.intent, execution)
|
|
1564
2467
|
: undefined;
|
|
1565
2468
|
const hasFailedDependencyTarget = pluginSummary?.targets?.some((target) => target.relation !== undefined && target.relation !== 'requested'
|
|
@@ -1570,6 +2473,18 @@ export class TuiController {
|
|
|
1570
2473
|
const dependencyFailure = pluginSummary?.code === 'operationFailed'
|
|
1571
2474
|
&& Boolean(hasFailedDependencyTarget);
|
|
1572
2475
|
if (pluginSummary?.code === 'operationFailed') {
|
|
2476
|
+
const projected = this.#operationDiagnostics.get(operationId);
|
|
2477
|
+
if (projected) {
|
|
2478
|
+
projected.targets = (pluginSummary.targets ?? []).map((target) => ({
|
|
2479
|
+
...target,
|
|
2480
|
+
diagnosticIds: projected.diagnostics.filter((diagnostic) => diagnostic.target
|
|
2481
|
+
&& diagnostic.target.platform === target.platform
|
|
2482
|
+
&& diagnostic.target.scope === target.scope
|
|
2483
|
+
&& (diagnostic.target.pluginId === undefined || diagnostic.target.pluginId === target.pluginId))
|
|
2484
|
+
.map((diagnostic) => diagnostic.id),
|
|
2485
|
+
effects: projected.targetEffects?.get(`${target.pluginId ?? ''}\0${target.platform}\0${target.scope}`),
|
|
2486
|
+
}));
|
|
2487
|
+
}
|
|
1573
2488
|
const unsuccessfulTargets = pluginSummary.targets?.filter((target) => target.state === 'failed' || target.state === 'skipped') ?? [];
|
|
1574
2489
|
const managerFailureCode = unsuccessfulTargets.map((target) => target.errorCode).find((code) => code === 'codexSystemCliUnsupported'
|
|
1575
2490
|
|| code === 'managedCodexAuthorizationRequired'
|
|
@@ -1588,8 +2503,10 @@ export class TuiController {
|
|
|
1588
2503
|
intent: ticket.intent,
|
|
1589
2504
|
navigation: 'result',
|
|
1590
2505
|
returnRoute: ticket.returnRoute,
|
|
1591
|
-
...(
|
|
1592
|
-
|
|
2506
|
+
...(ticket.intent.kind === 'pluginWriteBatch' || dependencyFailure || dependencyStillRequired
|
|
2507
|
+
? { summary: pluginSummary } : {}),
|
|
2508
|
+
...(ticket.intent.kind !== 'pluginWriteBatch'
|
|
2509
|
+
&& !dependencyFailure && !dependencyStillRequired && ['started', 'partial', 'completed', 'failed', 'unknown'].includes(this.#operationDiagnostics.get(operationId)?.effects.awWrite ?? 'unknown') ? { writeMayHaveCompleted: true } : {}),
|
|
1593
2510
|
});
|
|
1594
2511
|
}
|
|
1595
2512
|
const originIssue = ticket.intent.kind === 'pluginWrite' ? ticket.intent.originIssue : undefined;
|
|
@@ -1620,7 +2537,8 @@ export class TuiController {
|
|
|
1620
2537
|
const summary = {
|
|
1621
2538
|
...baseSummary,
|
|
1622
2539
|
elapsedMs: Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs),
|
|
1623
|
-
messages: [...this.#progress.messages, ...(baseSummary.messages ?? [])].slice(-8)
|
|
2540
|
+
messages: [...this.#progress.messages, ...(baseSummary.messages ?? [])].slice(-8)
|
|
2541
|
+
.map((message) => sanitizeDiagnosticText(message, [], collectOperationSensitiveValues(ticket.intent))),
|
|
1624
2542
|
};
|
|
1625
2543
|
this.#progress = { ...this.#progress, elapsedMs: summary.elapsedMs };
|
|
1626
2544
|
if (summary.code === 'operationFailed') {
|
|
@@ -1635,7 +2553,7 @@ export class TuiController {
|
|
|
1635
2553
|
};
|
|
1636
2554
|
this.#result = structuredClone(result);
|
|
1637
2555
|
this.#activeReview = undefined;
|
|
1638
|
-
this.#
|
|
2556
|
+
this.#showResult(result, ticket.intent);
|
|
1639
2557
|
this.#publish();
|
|
1640
2558
|
return cloneFrozen(result);
|
|
1641
2559
|
}
|
|
@@ -1666,6 +2584,7 @@ export class TuiController {
|
|
|
1666
2584
|
};
|
|
1667
2585
|
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
|
|
1668
2586
|
&& this.#portableRepreparePluginId === ticket.intent.pluginId) {
|
|
2587
|
+
this.#operationDiagnostics.delete(operationId);
|
|
1669
2588
|
this.#portableRepreparePluginId = undefined;
|
|
1670
2589
|
this.#activeReview = undefined;
|
|
1671
2590
|
this.#result = undefined;
|
|
@@ -1674,7 +2593,7 @@ export class TuiController {
|
|
|
1674
2593
|
}
|
|
1675
2594
|
this.#result = result;
|
|
1676
2595
|
this.#activeReview = undefined;
|
|
1677
|
-
this.#
|
|
2596
|
+
this.#showResult(result, ticket.intent);
|
|
1678
2597
|
this.#publish();
|
|
1679
2598
|
return cloneFrozen(result);
|
|
1680
2599
|
}
|
|
@@ -1682,44 +2601,106 @@ export class TuiController {
|
|
|
1682
2601
|
if (!this.#operationIsCurrent(generation))
|
|
1683
2602
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1684
2603
|
const code = issueCode(error);
|
|
2604
|
+
const metadata = readSafeOperationError(error);
|
|
2605
|
+
const preApply = metadata?.stage === 'preApply';
|
|
2606
|
+
const configWrite = error && typeof error === 'object' && 'configWrite' in error
|
|
2607
|
+
? error.configWrite : undefined;
|
|
2608
|
+
this.#captureThrownProjection(operationId, error, code, 'execute', ticket.intent, {
|
|
2609
|
+
configCommit: ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset'
|
|
2610
|
+
? configWrite === 'succeeded' ? 'completed'
|
|
2611
|
+
: configWrite === 'failed' ? 'failed' : 'notStarted' : 'notApplicable',
|
|
2612
|
+
awWrite: preApply ? 'notStarted' : 'unknown',
|
|
2613
|
+
preApplyExternalEffect: preApply ? 'possible' : 'notApplicable',
|
|
2614
|
+
finalVerification: 'notStarted',
|
|
2615
|
+
});
|
|
1685
2616
|
await this.#logOperationError(error, code, ticket.intent);
|
|
1686
2617
|
if (!this.#operationIsCurrent(generation))
|
|
1687
2618
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1688
2619
|
if (code === 'operationReviewStale' || code === 'catalogGenerationReviewStale') {
|
|
2620
|
+
this.#operationDiagnostics.delete(operationId);
|
|
1689
2621
|
return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
|
|
1690
2622
|
}
|
|
1691
2623
|
generation = ++this.#generation;
|
|
2624
|
+
let finalVerification = 'completed';
|
|
1692
2625
|
try {
|
|
1693
2626
|
const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
|
|
1694
2627
|
if (!this.#operationIsCurrent(generation))
|
|
1695
2628
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1696
2629
|
this.#applyStatus(refreshed);
|
|
2630
|
+
const projection = this.#operationDiagnostics.get(operationId);
|
|
2631
|
+
if (projection)
|
|
2632
|
+
projection.effects.finalVerification = 'completed';
|
|
1697
2633
|
if (ticket.intent.kind === 'catalogSourcesRecovery'
|
|
1698
2634
|
&& code === 'catalogSourcesConfigRecoveryStale') {
|
|
1699
2635
|
this.#phase = 'ready';
|
|
1700
2636
|
this.#loadError = undefined;
|
|
1701
|
-
this.#
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
state: 'blocked',
|
|
1707
|
-
operationId,
|
|
1708
|
-
code,
|
|
1709
|
-
message: '来源配置已变化,已重新加载当前状态。',
|
|
1710
|
-
operationKind: ticket.intent.kind,
|
|
2637
|
+
return this.#setBlocked(operationId, code, {
|
|
2638
|
+
intent: ticket.intent,
|
|
2639
|
+
navigation: 'result',
|
|
2640
|
+
returnRoute: ticket.returnRoute,
|
|
2641
|
+
writeMayHaveCompleted: false,
|
|
1711
2642
|
});
|
|
1712
2643
|
}
|
|
1713
2644
|
}
|
|
1714
2645
|
catch (reloadError) {
|
|
1715
2646
|
if (!this.#operationIsCurrent(generation))
|
|
1716
2647
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2648
|
+
finalVerification = 'failed';
|
|
2649
|
+
const projection = this.#operationDiagnostics.get(operationId);
|
|
2650
|
+
if (projection)
|
|
2651
|
+
projection.effects.finalVerification = 'failed';
|
|
1717
2652
|
await this.#logErrorSafe(reloadError, 'tuiStateReloadFailed', {
|
|
1718
2653
|
privateConfigOperation: true,
|
|
1719
2654
|
});
|
|
2655
|
+
if (ticket.intent.kind === 'catalogSourcesRecovery'
|
|
2656
|
+
&& code === 'catalogSourcesConfigRecoveryStale') {
|
|
2657
|
+
this.#phase = 'error';
|
|
2658
|
+
this.#loadError = {
|
|
2659
|
+
code: issueCode(reloadError),
|
|
2660
|
+
message: '无法安全读取插件状态。请检查诊断后重试或退出。',
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
1720
2663
|
}
|
|
1721
2664
|
if (!this.#operationIsCurrent(generation))
|
|
1722
2665
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2666
|
+
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
|
|
2667
|
+
&& configWrite === 'succeeded') {
|
|
2668
|
+
const elapsedMs = Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs);
|
|
2669
|
+
const summary = {
|
|
2670
|
+
code: 'operationPartial',
|
|
2671
|
+
message: ticket.intent.kind === 'configReset'
|
|
2672
|
+
? '设置层重置已提交,但后续步骤未完成。'
|
|
2673
|
+
: finalVerification === 'failed'
|
|
2674
|
+
? ticket.intent.apply
|
|
2675
|
+
? '设置已保存,但后续应用或状态复核失败。'
|
|
2676
|
+
: '设置已保存,但状态复核失败。'
|
|
2677
|
+
: ticket.intent.apply
|
|
2678
|
+
? '设置已保存,但后续应用失败。'
|
|
2679
|
+
: '设置已保存,但后续步骤未完成。',
|
|
2680
|
+
pluginId: ticket.intent.pluginId,
|
|
2681
|
+
configScope: ticket.intent.scope,
|
|
2682
|
+
...(ticket.intent.kind === 'configSave' && ticket.intent.apply
|
|
2683
|
+
? { configApplyRetryAvailable: true } : {}),
|
|
2684
|
+
elapsedMs,
|
|
2685
|
+
messages: [ticket.intent.kind === 'configReset'
|
|
2686
|
+
? '设置层重置已经提交;失败的后续步骤没有恢复旧设置。'
|
|
2687
|
+
: '配置写入已经提交;失败的后续步骤没有回滚设置。'],
|
|
2688
|
+
};
|
|
2689
|
+
this.#progress = { ...this.#progress, elapsedMs };
|
|
2690
|
+
const result = {
|
|
2691
|
+
state: 'partial',
|
|
2692
|
+
operationId,
|
|
2693
|
+
summary,
|
|
2694
|
+
operationKind: ticket.intent.kind,
|
|
2695
|
+
pluginId: ticket.intent.pluginId,
|
|
2696
|
+
returnRoute: structuredClone(this.#normalizeRoute(ticket.returnRoute)),
|
|
2697
|
+
};
|
|
2698
|
+
this.#result = structuredClone(result);
|
|
2699
|
+
this.#activeReview = undefined;
|
|
2700
|
+
this.#showResult(result, ticket.intent);
|
|
2701
|
+
this.#publish();
|
|
2702
|
+
return cloneFrozen(result);
|
|
2703
|
+
}
|
|
1723
2704
|
return this.#setBlocked(operationId, code, {
|
|
1724
2705
|
intent: ticket.intent,
|
|
1725
2706
|
navigation: 'result',
|
|
@@ -1732,6 +2713,7 @@ export class TuiController {
|
|
|
1732
2713
|
return !this.#disposed && !this.#abort.signal.aborted && generation === this.#generation;
|
|
1733
2714
|
}
|
|
1734
2715
|
#detachedBlocked(operationId, intent) {
|
|
2716
|
+
this.#operationDiagnostics.delete(operationId);
|
|
1735
2717
|
return cloneFrozen({
|
|
1736
2718
|
state: 'blocked',
|
|
1737
2719
|
operationId,
|
|
@@ -1780,7 +2762,7 @@ export class TuiController {
|
|
|
1780
2762
|
};
|
|
1781
2763
|
this.#result = structuredClone(result);
|
|
1782
2764
|
this.#activeReview = undefined;
|
|
1783
|
-
this.#
|
|
2765
|
+
this.#showResult(result, ticket.intent);
|
|
1784
2766
|
this.#publish();
|
|
1785
2767
|
return cloneFrozen(result);
|
|
1786
2768
|
}
|
|
@@ -1824,6 +2806,9 @@ export class TuiController {
|
|
|
1824
2806
|
return cloneFrozen(result);
|
|
1825
2807
|
}
|
|
1826
2808
|
#setBlocked(operationId, code, options) {
|
|
2809
|
+
if (!this.#operationDiagnostics.has(operationId)) {
|
|
2810
|
+
this.#captureThrownProjection(operationId, undefined, code, 'execute', options?.intent);
|
|
2811
|
+
}
|
|
1827
2812
|
const message = code === 'managedCodexAuthorizationRequired'
|
|
1828
2813
|
? '操作在插件写入前停止。请在 Review 中确认获取 Codex runtime,或在命令行传入 --allow-managed-codex-install。'
|
|
1829
2814
|
: code === 'codexSystemCliUnsupported'
|
|
@@ -1856,6 +2841,8 @@ export class TuiController {
|
|
|
1856
2841
|
this.#router.back();
|
|
1857
2842
|
this.#normalizeCurrentRoute();
|
|
1858
2843
|
}
|
|
2844
|
+
else if (options?.intent)
|
|
2845
|
+
this.#showResult(result, options.intent);
|
|
1859
2846
|
else
|
|
1860
2847
|
this.#router.replace({ name: 'result', operationId });
|
|
1861
2848
|
this.#publish();
|
|
@@ -1886,7 +2873,7 @@ export class TuiController {
|
|
|
1886
2873
|
this.#listeners.clear();
|
|
1887
2874
|
}
|
|
1888
2875
|
}
|
|
1889
|
-
function
|
|
2876
|
+
function operationResultSummaryUnsafe(intent, execution) {
|
|
1890
2877
|
if (intent.kind === 'catalogSourcesRecovery') {
|
|
1891
2878
|
const result = execution;
|
|
1892
2879
|
return {
|
|
@@ -1918,14 +2905,14 @@ function operationResultSummary(intent, execution) {
|
|
|
1918
2905
|
: '来源配置已保存,但 Catalog 尚未启用。可在 TUI 中立即重试。'
|
|
1919
2906
|
: intent.action === 'repair' ? 'Catalog 已重新下载并修复。'
|
|
1920
2907
|
: intent.action === 'replaceDamaged' ? '损坏的 Active Catalog 已由当前候选替换。'
|
|
1921
|
-
: intent.action === 'recoverState' ? '
|
|
2908
|
+
: intent.action === 'recoverState' ? '目录版本状态已恢复,目标来源已启用。'
|
|
1922
2909
|
: updateActivated ? `Catalog 已更新到 ${generation.resolution.catalogVersion}。`
|
|
1923
2910
|
: intent.action === 'update' ? 'Catalog 已验证,Active 未变化。'
|
|
1924
2911
|
: 'Catalog 已验证并启用。',
|
|
1925
2912
|
messages: result.status === 'partial'
|
|
1926
2913
|
? [result.partialReason === 'primary'
|
|
1927
|
-
?
|
|
1928
|
-
:
|
|
2914
|
+
? `新的已启用版本可用;主来源保持不变。错误:${result.errorCode ?? 'catalogSourcePrimaryFailed'}`
|
|
2915
|
+
: `旧的已启用版本保持不变。错误:${result.errorCode ?? 'catalogGenerationRefreshFailed'}`]
|
|
1929
2916
|
: updateActivated
|
|
1930
2917
|
? ['已安装插件不会自动升级。', '已重新计算可用的插件升级。']
|
|
1931
2918
|
: intent.action === 'update'
|
|
@@ -1963,11 +2950,15 @@ function operationResultSummary(intent, execution) {
|
|
|
1963
2950
|
},
|
|
1964
2951
|
};
|
|
1965
2952
|
}
|
|
1966
|
-
if (intent.kind === 'pluginWrite') {
|
|
2953
|
+
if (intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch') {
|
|
1967
2954
|
const command = intent.action === 'refreshAndSync' ? 'sync' : intent.action;
|
|
1968
2955
|
const rawExecution = typeof execution === 'object' && execution !== null
|
|
1969
2956
|
? execution
|
|
1970
2957
|
: {};
|
|
2958
|
+
const requestedPluginIds = intent.kind === 'pluginWriteBatch'
|
|
2959
|
+
? canonicalPluginIds(execution.requestedPluginIds ?? [])
|
|
2960
|
+
: [intent.pluginId];
|
|
2961
|
+
const fallbackPluginId = requestedPluginIds[0] ?? 'unknown/plugin';
|
|
1971
2962
|
const normalizedTargetResults = Array.isArray(rawExecution.targetResults)
|
|
1972
2963
|
? rawExecution.targetResults.map((candidate) => {
|
|
1973
2964
|
if (typeof candidate !== 'object' || candidate === null)
|
|
@@ -1984,7 +2975,7 @@ function operationResultSummary(intent, execution) {
|
|
|
1984
2975
|
...rawTarget,
|
|
1985
2976
|
...(pluginId ? { pluginId } : {}),
|
|
1986
2977
|
pluginName: typeof rawTarget.pluginName === 'string'
|
|
1987
|
-
? rawTarget.pluginName : pluginNameFromId(pluginId ??
|
|
2978
|
+
? rawTarget.pluginName : pluginNameFromId(pluginId ?? fallbackPluginId),
|
|
1988
2979
|
},
|
|
1989
2980
|
action: typeof entry.action === 'string'
|
|
1990
2981
|
? entry.action : requestedIntentAction(command),
|
|
@@ -1995,17 +2986,21 @@ function operationResultSummary(intent, execution) {
|
|
|
1995
2986
|
...rawExecution,
|
|
1996
2987
|
...(normalizedTargetResults ? { targetResults: normalizedTargetResults } : {}),
|
|
1997
2988
|
platforms: Array.isArray(rawExecution.platforms) ? rawExecution.platforms : [],
|
|
1998
|
-
plugins: Array.isArray(rawExecution.plugins) ? rawExecution.plugins :
|
|
2989
|
+
plugins: Array.isArray(rawExecution.plugins) ? rawExecution.plugins : requestedPluginIds,
|
|
1999
2990
|
warnings: Array.isArray(rawExecution.warnings) ? rawExecution.warnings : [],
|
|
2000
2991
|
});
|
|
2001
2992
|
const result = buildOperationResultModel(receipt);
|
|
2002
|
-
const requestedIds = new Set(receipt.requestedPluginIds ??
|
|
2993
|
+
const requestedIds = new Set(receipt.requestedPluginIds ?? requestedPluginIds);
|
|
2994
|
+
const retryTargets = intent.kind === 'pluginWriteBatch'
|
|
2995
|
+
? pluginWriteBatchFailedRequestedTargets(execution)
|
|
2996
|
+
: [];
|
|
2997
|
+
const failedRequestedPluginIds = canonicalPluginIds(retryTargets.map((target) => target.pluginId));
|
|
2003
2998
|
return {
|
|
2004
2999
|
code: result.state === 'succeeded' ? 'operationSucceeded'
|
|
2005
3000
|
: result.state === 'partial' ? 'operationPartial' : 'operationFailed',
|
|
2006
3001
|
message: result.state === 'succeeded' ? '操作已完成。'
|
|
2007
3002
|
: result.state === 'partial' ? '操作只完成了一部分。' : '操作结果无法确认。',
|
|
2008
|
-
pluginId: intent.pluginId,
|
|
3003
|
+
...(intent.kind === 'pluginWrite' ? { pluginId: intent.pluginId } : {}),
|
|
2009
3004
|
action: intent.action,
|
|
2010
3005
|
changes: result.changes,
|
|
2011
3006
|
warnings: result.warnings,
|
|
@@ -2014,10 +3009,18 @@ function operationResultSummary(intent, execution) {
|
|
|
2014
3009
|
messages: [
|
|
2015
3010
|
...result.warnings.map((warning) => `警告:${warning}`),
|
|
2016
3011
|
...result.recoveryActions.map((action) => `恢复:${action}`),
|
|
2017
|
-
...(intent.recoveryDescription
|
|
3012
|
+
...(intent.kind === 'pluginWrite' && intent.recoveryDescription
|
|
2018
3013
|
? [intent.recoveryDescription, `后续:${intent.scope === 'user' ? '当前项目' : '所有项目'} sync`]
|
|
2019
3014
|
: []),
|
|
2020
3015
|
],
|
|
3016
|
+
...(intent.kind === 'pluginWriteBatch' ? {
|
|
3017
|
+
batch: {
|
|
3018
|
+
requestedPluginIds,
|
|
3019
|
+
failedRequestedPluginIds,
|
|
3020
|
+
retryTargets,
|
|
3021
|
+
groupCount: execution.groupResults.length,
|
|
3022
|
+
},
|
|
3023
|
+
} : {}),
|
|
2021
3024
|
targets: result.targetResults.map((entry) => ({
|
|
2022
3025
|
pluginId: entry.target.pluginId,
|
|
2023
3026
|
platform: entry.target.platform,
|
|
@@ -2029,19 +3032,18 @@ function operationResultSummary(intent, execution) {
|
|
|
2029
3032
|
relation: entry.relation === 'requested' ? 'requested'
|
|
2030
3033
|
: entry.requiredBy.some((pluginId) => requestedIds.has(pluginId)) ? 'direct' : 'transitive',
|
|
2031
3034
|
requiredBy: entry.requiredBy,
|
|
2032
|
-
...(entry.errorCode
|
|
2033
|
-
|| entry.errorCode === 'codexSystemCliUnsupported'
|
|
2034
|
-
|| entry.errorCode === 'managedCodexAuthorizationRequired'
|
|
2035
|
-
|| entry.errorCode === 'managedCodexUnavailable'
|
|
2036
|
-
? { errorCode: entry.errorCode }
|
|
2037
|
-
: {}),
|
|
3035
|
+
...(entry.errorCode ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') } : {}),
|
|
2038
3036
|
})),
|
|
2039
3037
|
};
|
|
2040
3038
|
}
|
|
2041
3039
|
if (intent.kind === 'configSave' || intent.kind === 'configReset') {
|
|
2042
|
-
const
|
|
2043
|
-
&&
|
|
2044
|
-
|
|
3040
|
+
const rawApply = typeof execution === 'object' && execution !== null
|
|
3041
|
+
&& typeof execution.apply === 'object'
|
|
3042
|
+
&& execution.apply !== null
|
|
3043
|
+
? execution.apply
|
|
3044
|
+
: {};
|
|
3045
|
+
const rawTargets = Array.isArray(rawApply.targetResults)
|
|
3046
|
+
? rawApply.targetResults
|
|
2045
3047
|
: [];
|
|
2046
3048
|
const targets = rawTargets.flatMap((entry) => {
|
|
2047
3049
|
const target = typeof entry.target === 'object' && entry.target !== null
|
|
@@ -2049,32 +3051,50 @@ function operationResultSummary(intent, execution) {
|
|
|
2049
3051
|
: entry;
|
|
2050
3052
|
if ((target.platform !== 'codex' && target.platform !== 'claude')
|
|
2051
3053
|
|| (target.scope !== 'user' && target.scope !== 'project')
|
|
2052
|
-
|| (entry.state !== 'completed' && entry.state !== 'failed'))
|
|
3054
|
+
|| (entry.state !== 'completed' && entry.state !== 'failed' && entry.state !== 'skipped'))
|
|
2053
3055
|
return [];
|
|
2054
3056
|
return [{
|
|
2055
3057
|
...(typeof target.pluginId === 'string' ? { pluginId: target.pluginId } : {}),
|
|
2056
3058
|
platform: target.platform,
|
|
2057
3059
|
scope: target.scope,
|
|
2058
|
-
state: entry.action === 'skipped' ? 'skipped'
|
|
3060
|
+
state: entry.state === 'skipped' || entry.action === 'skipped' ? 'skipped'
|
|
2059
3061
|
: entry.state === 'failed' ? 'failed'
|
|
2060
3062
|
: entry.action === 'keep' ? 'kept'
|
|
2061
3063
|
: entry.state,
|
|
3064
|
+
...(typeof entry.errorCode === 'string'
|
|
3065
|
+
? { errorCode: safeDiagnosticCode(entry.errorCode, 'configApplyFailed') }
|
|
3066
|
+
: {}),
|
|
2062
3067
|
}];
|
|
2063
3068
|
});
|
|
2064
3069
|
const overallState = typeof execution === 'object' && execution !== null
|
|
2065
3070
|
? execution.overallState
|
|
2066
3071
|
: undefined;
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
const invalidConfig = applySkippedReason === 'invalidConfig';
|
|
3072
|
+
const applyState = rawApply.state;
|
|
3073
|
+
const applyReason = rawApply.reason;
|
|
3074
|
+
const invalidConfig = applyState === 'unavailable' && applyReason === 'invalidConfig';
|
|
2071
3075
|
const resetDisposition = typeof execution === 'object' && execution !== null
|
|
2072
3076
|
? execution.resetDisposition
|
|
2073
3077
|
: undefined;
|
|
2074
3078
|
const quarantineBasename = typeof execution === 'object' && execution !== null
|
|
2075
3079
|
? execution.quarantineBasename
|
|
2076
3080
|
: undefined;
|
|
3081
|
+
const publicWarnings = typeof execution === 'object' && execution !== null
|
|
3082
|
+
&& Array.isArray(execution.warnings)
|
|
3083
|
+
? execution.warnings.filter((warning) => typeof warning === 'string'
|
|
3084
|
+
&& warning.startsWith('projectConfigShareabilityUnverified:'))
|
|
3085
|
+
: [];
|
|
3086
|
+
const otherProjectsMayRequireSync = intent.scope === 'user'
|
|
3087
|
+
&& typeof execution === 'object' && execution !== null
|
|
3088
|
+
&& execution.otherProjectsMayRequireSync === true;
|
|
3089
|
+
const publicMessages = [
|
|
3090
|
+
...publicWarnings.map((warning) => `警告:${warning}`),
|
|
3091
|
+
...(otherProjectsMayRequireSync
|
|
3092
|
+
? ['提示:其它项目没有被扫描,可能需要在对应项目目录中运行 aw sync。']
|
|
3093
|
+
: []),
|
|
3094
|
+
];
|
|
2077
3095
|
const partial = overallState === 'partial'
|
|
3096
|
+
|| applyState === 'partial'
|
|
3097
|
+
|| applyState === 'unavailable'
|
|
2078
3098
|
|| invalidConfig
|
|
2079
3099
|
|| targets.some((target) => target.state === 'failed' || target.state === 'skipped')
|
|
2080
3100
|
&& targets.some((target) => target.state === 'completed' || target.state === 'kept');
|
|
@@ -2088,9 +3108,10 @@ function operationResultSummary(intent, execution) {
|
|
|
2088
3108
|
pluginId: intent.pluginId,
|
|
2089
3109
|
configScope: intent.scope,
|
|
2090
3110
|
configApplyRetryAvailable: intent.kind === 'configSave'
|
|
2091
|
-
&& intent.apply && !invalidConfig && (
|
|
3111
|
+
&& intent.apply && !invalidConfig && (applyState === 'partial' || applyState === 'unavailable'),
|
|
3112
|
+
...(publicMessages.length > 0 ? { messages: publicMessages } : {}),
|
|
2092
3113
|
...(intent.kind === 'configSave'
|
|
2093
|
-
? { configApplied:
|
|
3114
|
+
? { configApplied: applyState === 'succeeded' || applyState === 'partial' }
|
|
2094
3115
|
: invalidConfig ? { configApplied: false } : {}),
|
|
2095
3116
|
...(invalidConfig ? { configApplySkippedReason: 'invalidConfig' } : {}),
|
|
2096
3117
|
...(intent.kind === 'configReset'
|
|
@@ -2117,4 +3138,7 @@ function operationResultSummary(intent, execution) {
|
|
|
2117
3138
|
...(sourceWarnings.length > 0 ? { messages: sourceWarnings } : {}),
|
|
2118
3139
|
};
|
|
2119
3140
|
}
|
|
3141
|
+
function operationResultSummary(intent, execution) {
|
|
3142
|
+
return sanitizeOperationSummary(operationResultSummaryUnsafe(intent, execution), collectOperationSensitiveValues(intent));
|
|
3143
|
+
}
|
|
2120
3144
|
//# sourceMappingURL=controller.js.map
|