@lesliechan721/aw 0.1.0-beta.10 → 0.1.0-beta.11
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 +165 -45
- 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 +11 -19
- 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 +7 -0
- package/dist/src/commands/rebuild-command.js +769 -279
- package/dist/src/commands/rebuild-command.js.map +1 -1
- package/dist/src/commands/rebuild-integration.d.ts +24 -47
- package/dist/src/commands/rebuild-integration.js +1074 -1011
- 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 +216 -85
- 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 +6 -6
- 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 +2067 -839
- package/dist/src/tui/console-app-v4.js.map +1 -1
- package/dist/src/tui/controller-runtime.js +585 -33
- package/dist/src/tui/controller-runtime.js.map +1 -1
- package/dist/src/tui/controller.d.ts +152 -6
- package/dist/src/tui/controller.js +1168 -156
- 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 +6 -1
- 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 +306 -86
- 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-renderer.d.ts +2 -1
- package/dist/src/utils/artifact-renderer.js +52 -52
- package/dist/src/utils/artifact-renderer.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-write.d.ts +15 -13
- package/dist/src/utils/plugin-config-write.js +418 -198
- package/dist/src/utils/plugin-config-write.js.map +1 -1
- package/dist/src/utils/plugin-config.d.ts +48 -4
- package/dist/src/utils/plugin-config.js +289 -38
- 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 +376 -109
- 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 +17 -8
- 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 +1 -0
- package/dist/src/utils/sensitive-content.js +15 -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,176 @@ 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 !== 'pluginWriteBatch')
|
|
75
|
+
return intent;
|
|
76
|
+
if (intent.selection.kind === 'targets') {
|
|
77
|
+
return {
|
|
78
|
+
...intent,
|
|
79
|
+
selection: { kind: 'targets', targets: canonicalBatchTargets(intent.selection.targets) },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (intent.selection.kind !== 'selected')
|
|
83
|
+
return intent;
|
|
84
|
+
return {
|
|
85
|
+
...intent,
|
|
86
|
+
selection: {
|
|
87
|
+
kind: 'selected',
|
|
88
|
+
pluginIds: canonicalPluginIds(intent.selection.pluginIds),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function assertPluginWriteBatchEvidence(evidence) {
|
|
93
|
+
const candidate = evidence;
|
|
94
|
+
if (!Array.isArray(candidate.requestedPluginIds) || !Array.isArray(candidate.groups)) {
|
|
95
|
+
throw new TuiOperationBlockedError('batchEvidenceInvalid', '批量确认信息不完整。');
|
|
96
|
+
}
|
|
97
|
+
const requestedPluginIds = canonicalPluginIds(candidate.requestedPluginIds);
|
|
98
|
+
if (requestedPluginIds.length === 0
|
|
99
|
+
|| JSON.stringify(requestedPluginIds) !== JSON.stringify(candidate.requestedPluginIds)) {
|
|
100
|
+
throw new TuiOperationBlockedError('batchEvidenceInvalid', '批量根插件集合无效。');
|
|
101
|
+
}
|
|
102
|
+
const requested = new Set(requestedPluginIds);
|
|
103
|
+
const groupIds = new Set();
|
|
104
|
+
const groupedRoots = new Set();
|
|
105
|
+
for (const group of candidate.groups) {
|
|
106
|
+
if (!group || typeof group.groupId !== 'string' || group.groupId.length === 0
|
|
107
|
+
|| typeof group.sourceId !== 'string' || group.sourceId.length === 0
|
|
108
|
+
|| groupIds.has(group.groupId)
|
|
109
|
+
|| !Array.isArray(group.requestedPluginIds)
|
|
110
|
+
|| group.requestedPluginIds.length === 0
|
|
111
|
+
|| JSON.stringify(canonicalPluginIds(group.requestedPluginIds))
|
|
112
|
+
!== JSON.stringify(group.requestedPluginIds)
|
|
113
|
+
|| group.requestedPluginIds.some((pluginId) => !requested.has(pluginId))
|
|
114
|
+
|| group.executable !== Boolean(group.preview?.operationPlan)) {
|
|
115
|
+
throw new TuiOperationBlockedError('batchEvidenceInvalid', '批量分组确认信息无效。');
|
|
116
|
+
}
|
|
117
|
+
groupIds.add(group.groupId);
|
|
118
|
+
group.requestedPluginIds.forEach((pluginId) => groupedRoots.add(pluginId));
|
|
119
|
+
}
|
|
120
|
+
if (requestedPluginIds.some((pluginId) => !groupedRoots.has(pluginId))) {
|
|
121
|
+
throw new TuiOperationBlockedError('batchEvidenceInvalid', '批量分组缺少请求根插件。');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function pluginWriteBatchFailedRequestedTargets(execution) {
|
|
125
|
+
const failed = [];
|
|
126
|
+
for (const group of execution.groupResults) {
|
|
127
|
+
const roots = new Set(group.requestedPluginIds);
|
|
128
|
+
const targetsByPlugin = new Map();
|
|
129
|
+
for (const target of group.result.targetResults) {
|
|
130
|
+
const pluginId = target.target.pluginId;
|
|
131
|
+
if (!pluginId)
|
|
132
|
+
continue;
|
|
133
|
+
const entries = targetsByPlugin.get(pluginId) ?? [];
|
|
134
|
+
entries.push(target);
|
|
135
|
+
targetsByPlugin.set(pluginId, entries);
|
|
136
|
+
}
|
|
137
|
+
const successfulRootKeys = new Set(group.result.targetResults.flatMap((target) => {
|
|
138
|
+
const pluginId = target.target.pluginId;
|
|
139
|
+
return pluginId && roots.has(pluginId)
|
|
140
|
+
&& target.state !== 'failed' && target.action !== 'skipped'
|
|
141
|
+
? [[pluginId, target.target.platform, target.target.scope].join('\0')]
|
|
142
|
+
: [];
|
|
143
|
+
}));
|
|
144
|
+
const unsuccessful = group.result.targetResults.filter((target) => target.state === 'failed' || target.action === 'skipped');
|
|
145
|
+
for (const target of unsuccessful) {
|
|
146
|
+
const pending = target.target.pluginId
|
|
147
|
+
? [{ pluginId: target.target.pluginId, platform: target.target.platform }]
|
|
148
|
+
: [];
|
|
149
|
+
const visited = new Set();
|
|
150
|
+
while (pending.length > 0) {
|
|
151
|
+
const current = pending.shift();
|
|
152
|
+
const visitKey = [current.pluginId, current.platform].join('\0');
|
|
153
|
+
if (visited.has(visitKey))
|
|
154
|
+
continue;
|
|
155
|
+
visited.add(visitKey);
|
|
156
|
+
const candidates = (targetsByPlugin.get(current.pluginId) ?? [])
|
|
157
|
+
.filter((candidate) => candidate.target.platform === current.platform);
|
|
158
|
+
if (roots.has(current.pluginId)) {
|
|
159
|
+
if (candidates.length === 0) {
|
|
160
|
+
const retryTarget = {
|
|
161
|
+
pluginId: current.pluginId,
|
|
162
|
+
platform: current.platform,
|
|
163
|
+
scope: group.scope,
|
|
164
|
+
};
|
|
165
|
+
const key = [retryTarget.pluginId, retryTarget.platform, retryTarget.scope].join('\0');
|
|
166
|
+
if (!successfulRootKeys.has(key))
|
|
167
|
+
failed.push(retryTarget);
|
|
168
|
+
}
|
|
169
|
+
for (const candidate of candidates) {
|
|
170
|
+
const retryTarget = {
|
|
171
|
+
pluginId: current.pluginId,
|
|
172
|
+
platform: candidate.target.platform,
|
|
173
|
+
scope: candidate.target.scope,
|
|
174
|
+
};
|
|
175
|
+
const key = [current.pluginId, retryTarget.platform, retryTarget.scope].join('\0');
|
|
176
|
+
if (!successfulRootKeys.has(key))
|
|
177
|
+
failed.push(retryTarget);
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
for (const candidate of candidates) {
|
|
182
|
+
pending.push(...(candidate.requiredBy ?? []).map((pluginId) => ({
|
|
183
|
+
pluginId, platform: current.platform,
|
|
184
|
+
})));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return canonicalBatchTargets(failed);
|
|
190
|
+
}
|
|
191
|
+
export function pluginWriteBatchFailedRequestedPluginIds(execution) {
|
|
192
|
+
return canonicalPluginIds(pluginWriteBatchFailedRequestedTargets(execution)
|
|
193
|
+
.map((target) => target.pluginId));
|
|
194
|
+
}
|
|
195
|
+
function collectOperationSensitiveValues(intent) {
|
|
196
|
+
if (intent.kind !== 'configSave')
|
|
197
|
+
return [];
|
|
198
|
+
const sensitiveKeys = new Set([...(intent.sensitiveKeys ?? []), ...(intent.privateKeys ?? [])]);
|
|
199
|
+
const values = [];
|
|
200
|
+
const visit = (value) => {
|
|
201
|
+
if (typeof value === 'string')
|
|
202
|
+
values.push(value);
|
|
203
|
+
else if (Array.isArray(value))
|
|
204
|
+
value.forEach(visit);
|
|
205
|
+
else if (value && typeof value === 'object')
|
|
206
|
+
Object.values(value).forEach(visit);
|
|
207
|
+
};
|
|
208
|
+
for (const [key, value] of Object.entries(intent.updates ?? {})) {
|
|
209
|
+
if (sensitiveKeys.has(key))
|
|
210
|
+
visit(value);
|
|
211
|
+
}
|
|
212
|
+
return values;
|
|
213
|
+
}
|
|
214
|
+
function sanitizeOperationSummary(summary, sensitiveValues) {
|
|
215
|
+
const text = (value) => sanitizeDiagnosticText(value, [], sensitiveValues);
|
|
216
|
+
return {
|
|
217
|
+
...summary,
|
|
218
|
+
message: text(summary.message),
|
|
219
|
+
...(summary.messages ? { messages: summary.messages.map(text) } : {}),
|
|
220
|
+
...(summary.changes ? { changes: summary.changes.map(text) } : {}),
|
|
221
|
+
...(summary.warnings ? { warnings: summary.warnings.map(text) } : {}),
|
|
222
|
+
...(summary.recoveryActions ? { recoveryActions: summary.recoveryActions.map(text) } : {}),
|
|
223
|
+
...(summary.quarantineBasename ? { quarantineBasename: text(summary.quarantineBasename) } : {}),
|
|
224
|
+
...(summary.portable ? {
|
|
225
|
+
portable: {
|
|
226
|
+
...summary.portable,
|
|
227
|
+
...(summary.portable.recovery ? { recovery: text(summary.portable.recovery) } : {}),
|
|
228
|
+
},
|
|
229
|
+
} : {}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
51
232
|
function deepFreeze(value) {
|
|
52
233
|
if (typeof value !== 'object' || value === null || Object.isFrozen(value))
|
|
53
234
|
return value;
|
|
@@ -58,27 +239,33 @@ function deepFreeze(value) {
|
|
|
58
239
|
function cloneFrozen(value) {
|
|
59
240
|
return deepFreeze(structuredClone(value));
|
|
60
241
|
}
|
|
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
242
|
export function operationEvidenceFingerprint(evidence) {
|
|
243
|
+
if ('groups' in evidence && 'requestedPluginIds' in evidence) {
|
|
244
|
+
const batch = evidence;
|
|
245
|
+
return crypto.createHash('sha256')
|
|
246
|
+
.update(JSON.stringify(stableValue({
|
|
247
|
+
requestedPluginIds: batch.requestedPluginIds,
|
|
248
|
+
groups: batch.groups.map((group) => ({
|
|
249
|
+
groupId: group.groupId,
|
|
250
|
+
sourceId: group.sourceId,
|
|
251
|
+
platform: group.platform,
|
|
252
|
+
requestedPluginIds: group.requestedPluginIds,
|
|
253
|
+
executable: group.executable,
|
|
254
|
+
planFingerprint: group.preview.operationPlan?.planFingerprint,
|
|
255
|
+
overallState: group.preview.overallState,
|
|
256
|
+
targetResults: group.preview.targetResults.map((target) => ({
|
|
257
|
+
pluginId: target.target.pluginId,
|
|
258
|
+
platform: target.target.platform,
|
|
259
|
+
scope: target.target.scope,
|
|
260
|
+
state: target.state,
|
|
261
|
+
action: target.action,
|
|
262
|
+
errorCode: target.errorCode,
|
|
263
|
+
requiredBy: target.requiredBy,
|
|
264
|
+
})),
|
|
265
|
+
})),
|
|
266
|
+
})))
|
|
267
|
+
.digest('hex');
|
|
268
|
+
}
|
|
82
269
|
if (evidence.operationPlan)
|
|
83
270
|
return evidence.operationPlan.planFingerprint;
|
|
84
271
|
if (evidence.resetTarget) {
|
|
@@ -90,8 +277,14 @@ export function operationEvidenceFingerprint(evidence) {
|
|
|
90
277
|
.digest('hex');
|
|
91
278
|
}
|
|
92
279
|
const { codexManager: _runtimeSelection, ...staleEvidence } = evidence;
|
|
280
|
+
const config = staleEvidence.config
|
|
281
|
+
? (({ shareability: _shareability, ...identity }) => identity)(staleEvidence.config)
|
|
282
|
+
: undefined;
|
|
93
283
|
return crypto.createHash('sha256')
|
|
94
|
-
.update(JSON.stringify(stableValue(
|
|
284
|
+
.update(JSON.stringify(stableValue({
|
|
285
|
+
...staleEvidence,
|
|
286
|
+
...(config ? { config } : {}),
|
|
287
|
+
})))
|
|
95
288
|
.digest('hex');
|
|
96
289
|
}
|
|
97
290
|
export function operationEvidenceMatches(expected, current) {
|
|
@@ -125,6 +318,9 @@ export function userIssueKey(issue) {
|
|
|
125
318
|
.join('\0');
|
|
126
319
|
}
|
|
127
320
|
function issueCode(error) {
|
|
321
|
+
const safeOperationError = readSafeOperationError(error);
|
|
322
|
+
if (safeOperationError)
|
|
323
|
+
return safeDiagnosticCode(safeOperationError.code, 'operationBlocked');
|
|
128
324
|
if (error instanceof TuiOperationBlockedError)
|
|
129
325
|
return error.code;
|
|
130
326
|
if (error instanceof CatalogGenerationError)
|
|
@@ -160,12 +356,15 @@ export class TuiController {
|
|
|
160
356
|
#listeners = new Set();
|
|
161
357
|
#health = new Map();
|
|
162
358
|
#tickets = new Map();
|
|
359
|
+
#reviewPreparationIntents = new Map();
|
|
163
360
|
#inflightOperations = new Map();
|
|
361
|
+
#pendingReads = new Set();
|
|
164
362
|
#abort = new AbortController();
|
|
165
363
|
#progressUnsubscribe;
|
|
166
364
|
#progressFeed;
|
|
167
365
|
#phase = 'idle';
|
|
168
366
|
#status;
|
|
367
|
+
#statusReadAtMs;
|
|
169
368
|
#catalogGenerations = new Map();
|
|
170
369
|
#catalogFreshness = new Map();
|
|
171
370
|
#catalogSourcesSession;
|
|
@@ -177,18 +376,28 @@ export class TuiController {
|
|
|
177
376
|
#reviewPreparation;
|
|
178
377
|
#reviewPreparationToken = 0;
|
|
179
378
|
#result;
|
|
379
|
+
#resultAnchor;
|
|
380
|
+
#portableResultSkills;
|
|
180
381
|
#loadError;
|
|
181
382
|
#portableDraft;
|
|
383
|
+
#recoverablePortableDraft;
|
|
182
384
|
#portableRepreparePluginId;
|
|
183
385
|
#portableRequestToken = 0;
|
|
184
386
|
#portableRequestAbort;
|
|
185
387
|
#portablePending;
|
|
186
388
|
#installPlacementDraft;
|
|
389
|
+
#recoverableInstallPlacementDraft;
|
|
187
390
|
#installPlacementRequestToken = 0;
|
|
188
391
|
#installPlacementRequestAbort;
|
|
189
392
|
#generation = 0;
|
|
190
393
|
#disposed = false;
|
|
191
|
-
#
|
|
394
|
+
#retiring = false;
|
|
395
|
+
#externalDrafts = [];
|
|
396
|
+
#batchRetryIntent;
|
|
397
|
+
#operationHistory = new OperationHistoryStore();
|
|
398
|
+
#sessionEpoch = 0;
|
|
399
|
+
#lastRecordedResultId;
|
|
400
|
+
#operationDiagnostics = new Map();
|
|
192
401
|
constructor(dependencies, progressFeed) {
|
|
193
402
|
this.dependencies = dependencies;
|
|
194
403
|
this.#progressFeed = progressFeed;
|
|
@@ -211,6 +420,8 @@ export class TuiController {
|
|
|
211
420
|
});
|
|
212
421
|
if (progressFeed) {
|
|
213
422
|
this.#progressUnsubscribe = progressFeed.subscribe((progress) => {
|
|
423
|
+
if (this.#router.snapshot.current.route.name === 'result')
|
|
424
|
+
return;
|
|
214
425
|
this.#progress = {
|
|
215
426
|
...structuredClone(progress),
|
|
216
427
|
messages: progress.messages.slice(-8),
|
|
@@ -239,6 +450,7 @@ export class TuiController {
|
|
|
239
450
|
get snapshot() {
|
|
240
451
|
return cloneFrozen({
|
|
241
452
|
phase: this.#phase,
|
|
453
|
+
...(this.#statusReadAtMs !== undefined ? { statusReadAtMs: this.#statusReadAtMs } : {}),
|
|
242
454
|
...(this.#status?.cliChannel ? { cliChannel: this.#status.cliChannel } : {}),
|
|
243
455
|
router: this.#router.snapshot,
|
|
244
456
|
...(this.#viewModel ? { viewModel: this.#viewModel } : {}),
|
|
@@ -252,9 +464,11 @@ export class TuiController {
|
|
|
252
464
|
progressTargets: this.#progressTargets,
|
|
253
465
|
...(this.#activeReview ? { activeReview: this.#activeReview } : {}),
|
|
254
466
|
...(this.#reviewPreparation
|
|
255
|
-
? { reviewPreparation:
|
|
467
|
+
? { reviewPreparation: this.#reviewPreparation.snapshot }
|
|
256
468
|
: {}),
|
|
257
469
|
...(this.#result ? { result: this.#result } : {}),
|
|
470
|
+
operationHistorySummaries: this.#operationHistory.list(),
|
|
471
|
+
...(this.#portableResultSkills ? { portableResultSkills: this.#portableResultSkills } : {}),
|
|
258
472
|
...(this.#loadError ? { loadError: this.#loadError } : {}),
|
|
259
473
|
...(this.#portableDraft ? { portableDraft: this.#portableDraft } : {}),
|
|
260
474
|
...(this.#installPlacementDraft
|
|
@@ -278,13 +492,222 @@ export class TuiController {
|
|
|
278
492
|
listener(this.snapshot);
|
|
279
493
|
return () => this.#listeners.delete(listener);
|
|
280
494
|
}
|
|
495
|
+
get hasActiveWrite() {
|
|
496
|
+
return this.#inflightOperations.size > 0;
|
|
497
|
+
}
|
|
498
|
+
get retiring() {
|
|
499
|
+
return this.#retiring;
|
|
500
|
+
}
|
|
501
|
+
get hasSessionDraft() {
|
|
502
|
+
return this.#externalDrafts.length > 0
|
|
503
|
+
|| this.#portableDraft !== undefined
|
|
504
|
+
|| this.#installPlacementDraft !== undefined;
|
|
505
|
+
}
|
|
506
|
+
get externalDrafts() {
|
|
507
|
+
return structuredClone(this.#externalDrafts);
|
|
508
|
+
}
|
|
509
|
+
attachOperationHistory(history, epoch) {
|
|
510
|
+
this.#operationHistory = history;
|
|
511
|
+
this.#sessionEpoch = epoch;
|
|
512
|
+
}
|
|
513
|
+
listOperationHistory() {
|
|
514
|
+
return this.#operationHistory.list();
|
|
515
|
+
}
|
|
516
|
+
getOperationDiagnostic(recordId) {
|
|
517
|
+
return this.#operationHistory.get(recordId);
|
|
518
|
+
}
|
|
519
|
+
previewOperationReport(recordId) {
|
|
520
|
+
return this.#operationHistory.preview(recordId);
|
|
521
|
+
}
|
|
522
|
+
saveOperationReport(recordId, filePath) {
|
|
523
|
+
return this.#operationHistory.save(recordId, filePath);
|
|
524
|
+
}
|
|
525
|
+
/** @internal Test-only observability for bounded private diagnostic ownership. */
|
|
526
|
+
operationDiagnosticRetentionCountForTests() {
|
|
527
|
+
return this.#operationDiagnostics.size;
|
|
528
|
+
}
|
|
529
|
+
recordUiFailure(error, context) {
|
|
530
|
+
const recordId = this.#recordStandaloneFailure(context.code, context.stage, error);
|
|
531
|
+
this.#publish();
|
|
532
|
+
return recordId;
|
|
533
|
+
}
|
|
534
|
+
#recordStandaloneFailure(code, stage, error) {
|
|
535
|
+
const operationId = `ui-${crypto.randomUUID()}`;
|
|
536
|
+
const recordId = `${this.#sessionEpoch}:${operationId}`;
|
|
537
|
+
const diagnostic = projectThrownOperationDiagnostic({
|
|
538
|
+
error,
|
|
539
|
+
id: `${recordId}:failure`,
|
|
540
|
+
stage: safeDiagnosticCode(stage, 'ui'),
|
|
541
|
+
fallbackCode: safeDiagnosticCode(code, 'uiOperationFailed'),
|
|
542
|
+
});
|
|
543
|
+
this.#operationHistory.record({
|
|
544
|
+
summary: {
|
|
545
|
+
recordId,
|
|
546
|
+
epoch: this.#sessionEpoch,
|
|
547
|
+
operationId,
|
|
548
|
+
createdAtMs: this.dependencies.now?.() ?? Date.now(),
|
|
549
|
+
state: 'blocked',
|
|
550
|
+
code: diagnostic.code,
|
|
551
|
+
message: diagnostic.safeMessage,
|
|
552
|
+
diagnosticCount: 1,
|
|
553
|
+
effects: {
|
|
554
|
+
configCommit: 'notApplicable', awWrite: 'notStarted',
|
|
555
|
+
preApplyExternalEffect: 'notApplicable', finalVerification: 'notStarted',
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
diagnostics: [diagnostic],
|
|
559
|
+
targets: [],
|
|
560
|
+
});
|
|
561
|
+
return recordId;
|
|
562
|
+
}
|
|
563
|
+
#trackRead(promise) {
|
|
564
|
+
this.#pendingReads.add(promise);
|
|
565
|
+
void promise.finally(() => this.#pendingReads.delete(promise)).catch(() => undefined);
|
|
566
|
+
return promise;
|
|
567
|
+
}
|
|
568
|
+
beginRetirement() {
|
|
569
|
+
this.#retiring = true;
|
|
570
|
+
}
|
|
571
|
+
async waitForWrites() {
|
|
572
|
+
this.beginRetirement();
|
|
573
|
+
while (this.#inflightOperations.size > 0) {
|
|
574
|
+
await Promise.allSettled([...this.#inflightOperations.values()].map((operation) => operation.promise));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async cancelAndWaitForReads() {
|
|
578
|
+
if (this.hasActiveWrite)
|
|
579
|
+
return false;
|
|
580
|
+
this.beginRetirement();
|
|
581
|
+
this.#generation += 1;
|
|
582
|
+
this.#cancelReviewPreparation();
|
|
583
|
+
this.#cancelPortableRequest();
|
|
584
|
+
this.#cancelInstallPlacementRequest();
|
|
585
|
+
this.#abort.abort();
|
|
586
|
+
const catalogDrain = this.#catalogSourcesSession.cancelAndWait();
|
|
587
|
+
while (this.#pendingReads.size > 0) {
|
|
588
|
+
await Promise.allSettled([...this.#pendingReads]);
|
|
589
|
+
}
|
|
590
|
+
await catalogDrain;
|
|
591
|
+
return !this.hasActiveWrite;
|
|
592
|
+
}
|
|
593
|
+
#assertAcceptingActions() {
|
|
594
|
+
if (this.#retiring || this.#disposed) {
|
|
595
|
+
throw new DOMException('Controller session is retiring.', 'AbortError');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
goHome(options = {}) {
|
|
599
|
+
if (this.hasSessionDraft && !options.discardDrafts)
|
|
600
|
+
return false;
|
|
601
|
+
this.#cancelReviewPreparation();
|
|
602
|
+
this.#cancelPortableRequest();
|
|
603
|
+
this.#cancelInstallPlacementRequest();
|
|
604
|
+
this.#tickets.clear();
|
|
605
|
+
this.#activeReview = undefined;
|
|
606
|
+
this.#portableDraft = undefined;
|
|
607
|
+
this.#recoverablePortableDraft = undefined;
|
|
608
|
+
this.#installPlacementDraft = undefined;
|
|
609
|
+
this.#recoverableInstallPlacementDraft = undefined;
|
|
610
|
+
this.#batchRetryIntent = undefined;
|
|
611
|
+
this.#externalDrafts = [];
|
|
612
|
+
this.#result = undefined;
|
|
613
|
+
this.#resultAnchor = undefined;
|
|
614
|
+
this.#portableResultSkills = undefined;
|
|
615
|
+
this.#progress = { messages: [] };
|
|
616
|
+
this.#progressTargets = [];
|
|
617
|
+
this.#router.resetToRoot();
|
|
618
|
+
this.#publish();
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
281
621
|
#publish() {
|
|
282
622
|
if (this.#disposed)
|
|
283
623
|
return;
|
|
624
|
+
this.#recordTerminalResult();
|
|
284
625
|
const snapshot = this.snapshot;
|
|
285
626
|
for (const listener of this.#listeners)
|
|
286
627
|
listener(snapshot);
|
|
287
628
|
}
|
|
629
|
+
#recordTerminalResult() {
|
|
630
|
+
const result = this.#result;
|
|
631
|
+
if (!result || result.state === 'stale')
|
|
632
|
+
return;
|
|
633
|
+
const recordId = `${this.#sessionEpoch}:${result.operationId}`;
|
|
634
|
+
if (this.#lastRecordedResultId === recordId)
|
|
635
|
+
return;
|
|
636
|
+
const projected = this.#operationDiagnostics.get(result.operationId) ?? {
|
|
637
|
+
diagnostics: [],
|
|
638
|
+
effects: {
|
|
639
|
+
configCommit: 'notApplicable',
|
|
640
|
+
awWrite: result.state === 'succeeded' ? 'completed' : 'unknown',
|
|
641
|
+
preApplyExternalEffect: 'notApplicable',
|
|
642
|
+
finalVerification: result.state === 'succeeded' ? 'completed' : 'unknown',
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
if ('summary' in result && result.summary) {
|
|
646
|
+
Object.assign(result.summary, sanitizeOperationSummary(result.summary, projected.sensitiveValues ?? []));
|
|
647
|
+
}
|
|
648
|
+
const diagnosticIds = projected.diagnostics.map((diagnostic) => diagnostic.id);
|
|
649
|
+
Object.assign(result, { operationRecordId: recordId, diagnosticIds, effects: projected.effects });
|
|
650
|
+
if ('summary' in result && result.summary) {
|
|
651
|
+
result.summary.diagnosticIds = diagnosticIds;
|
|
652
|
+
result.summary.effects = projected.effects;
|
|
653
|
+
for (const target of result.summary.targets ?? []) {
|
|
654
|
+
const targetDiagnostics = projected.diagnostics.filter((diagnostic) => diagnostic.target
|
|
655
|
+
&& diagnostic.target.platform === target.platform
|
|
656
|
+
&& diagnostic.target.scope === target.scope
|
|
657
|
+
&& (diagnostic.target.pluginId === undefined || diagnostic.target.pluginId === target.pluginId));
|
|
658
|
+
target.diagnosticIds = targetDiagnostics.map((diagnostic) => diagnostic.id);
|
|
659
|
+
target.effects = projected.targetEffects?.get(`${target.pluginId ?? ''}\0${target.platform}\0${target.scope}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const code = result.state === 'succeeded' || result.state === 'partial'
|
|
663
|
+
? result.summary.code : result.summary?.code ?? result.code;
|
|
664
|
+
const message = result.state === 'succeeded' || result.state === 'partial'
|
|
665
|
+
? result.summary.message : result.summary?.message ?? result.message;
|
|
666
|
+
this.#operationHistory.record({
|
|
667
|
+
summary: {
|
|
668
|
+
recordId,
|
|
669
|
+
epoch: this.#sessionEpoch,
|
|
670
|
+
operationId: result.operationId,
|
|
671
|
+
createdAtMs: this.dependencies.now?.() ?? Date.now(),
|
|
672
|
+
state: result.state,
|
|
673
|
+
code,
|
|
674
|
+
message,
|
|
675
|
+
...('operationKind' in result && result.operationKind ? { operationKind: result.operationKind } : {}),
|
|
676
|
+
...('pluginId' in result && result.pluginId ? { pluginId: result.pluginId } : {}),
|
|
677
|
+
diagnosticCount: projected.diagnostics.length,
|
|
678
|
+
effects: projected.effects,
|
|
679
|
+
},
|
|
680
|
+
diagnostics: projected.diagnostics,
|
|
681
|
+
targets: 'summary' in result && result.summary
|
|
682
|
+
? structuredClone((result.summary.targets ?? []).map((target) => ({
|
|
683
|
+
...target,
|
|
684
|
+
diagnosticIds: target.diagnosticIds ?? [],
|
|
685
|
+
})))
|
|
686
|
+
: structuredClone(projected.targets ?? []),
|
|
687
|
+
}, { sensitiveValues: projected.sensitiveValues });
|
|
688
|
+
this.#operationDiagnostics.delete(result.operationId);
|
|
689
|
+
this.#lastRecordedResultId = recordId;
|
|
690
|
+
}
|
|
691
|
+
#captureThrownProjection(operationId, error, code, stage, intent, effects) {
|
|
692
|
+
const sensitiveValues = intent ? collectOperationSensitiveValues(intent) : [];
|
|
693
|
+
this.#operationDiagnostics.set(operationId, {
|
|
694
|
+
diagnostics: [projectThrownOperationDiagnostic({
|
|
695
|
+
error,
|
|
696
|
+
id: `${this.#sessionEpoch}:${operationId}:failure`,
|
|
697
|
+
stage,
|
|
698
|
+
fallbackCode: code,
|
|
699
|
+
sensitiveValues,
|
|
700
|
+
...(intent && 'pluginId' in intent && 'platform' in intent && intent.platform !== 'both'
|
|
701
|
+
? { target: { pluginId: intent.pluginId, platform: intent.platform, scope: intent.scope } }
|
|
702
|
+
: {}),
|
|
703
|
+
})],
|
|
704
|
+
effects: effects ?? {
|
|
705
|
+
configCommit: 'notApplicable', awWrite: 'notStarted',
|
|
706
|
+
preApplyExternalEffect: 'notApplicable', finalVerification: 'notStarted',
|
|
707
|
+
},
|
|
708
|
+
sensitiveValues,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
288
711
|
async #logErrorSafe(error, code, context) {
|
|
289
712
|
try {
|
|
290
713
|
await this.dependencies.logError?.(error, code, context);
|
|
@@ -306,64 +729,152 @@ export class TuiController {
|
|
|
306
729
|
catalogGenerations: this.#catalogGenerations,
|
|
307
730
|
dependencyOverviews: this.#status.dependencyOverviews,
|
|
308
731
|
portablePluginIds: this.#status.portablePluginIds,
|
|
732
|
+
configStoreInventory: this.#status.configStoreInventory,
|
|
309
733
|
});
|
|
310
734
|
}
|
|
311
735
|
#normalizeRoute(route) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
736
|
+
return normalizeTuiRoute(route, this.#routeEntityState());
|
|
737
|
+
}
|
|
738
|
+
#routeEntityState() {
|
|
739
|
+
return {
|
|
740
|
+
hasPlugin: (pluginId) => this.#viewModel?.plugins.some((plugin) => plugin.pluginId === pluginId) ?? false,
|
|
741
|
+
hasSource: (sourceId) => this.#viewModel?.sourceTabs.some((source) => source.id === sourceId) ?? false,
|
|
742
|
+
hasIssue: (issueId) => this.#viewModel?.issues.some((issue) => issue.issueId === issueId) ?? false,
|
|
743
|
+
hasHistoryRecord: (recordId) => this.#operationHistory.get(recordId) !== undefined,
|
|
744
|
+
hasActiveOperation: (candidate) => candidate.name === 'reviewPreparing'
|
|
745
|
+
? this.#reviewPreparation?.snapshot.id === candidate.preparationId
|
|
746
|
+
: candidate.name === 'result'
|
|
747
|
+
? this.#result?.operationId === candidate.operationId
|
|
748
|
+
: this.#activeReview?.operationId === candidate.operationId,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
#normalizeCurrentRoute() {
|
|
752
|
+
this.#router.normalizeRoutes((route) => this.#normalizeRoute(route));
|
|
753
|
+
if (this.#resultAnchor)
|
|
754
|
+
this.#resultAnchor = this.#normalizeRoute(this.#resultAnchor);
|
|
755
|
+
for (const ticket of this.#tickets.values())
|
|
756
|
+
ticket.returnRoute = this.#normalizeRoute(ticket.returnRoute);
|
|
757
|
+
if (this.#reviewPreparation) {
|
|
758
|
+
this.#reviewPreparation.returnRoute = this.#normalizeRoute(this.#reviewPreparation.returnRoute);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
#ensureOperationFlow(intent) {
|
|
762
|
+
const active = this.#router.activeFlow();
|
|
763
|
+
if (intent.kind === 'pluginWriteBatch') {
|
|
764
|
+
if (active?.kind === 'batchWrite')
|
|
765
|
+
return;
|
|
766
|
+
this.#router.beginFlow('batchWrite', { name: 'pluginList', mode: 'installed' });
|
|
767
|
+
return;
|
|
315
768
|
}
|
|
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
|
-
};
|
|
769
|
+
if (intent.kind === 'pluginWrite') {
|
|
770
|
+
const kind = intent.action === 'install' ? 'pluginInstall' : 'pluginMaintenance';
|
|
771
|
+
if (active?.kind === kind)
|
|
772
|
+
return;
|
|
773
|
+
this.#router.beginPluginFlow(kind, intent.pluginId);
|
|
774
|
+
return;
|
|
345
775
|
}
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
776
|
+
if (intent.kind === 'portableInstall') {
|
|
777
|
+
if (active?.kind === 'portableInstall')
|
|
778
|
+
return;
|
|
779
|
+
this.#router.beginPortableFlow(intent.pluginId);
|
|
780
|
+
return;
|
|
349
781
|
}
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
782
|
+
if (intent.kind === 'configSave' || intent.kind === 'configReset') {
|
|
783
|
+
if (active?.kind === 'configEdit')
|
|
784
|
+
return;
|
|
785
|
+
const route = this.#router.snapshot.current.route;
|
|
786
|
+
if ((route.name === 'pluginPreview' || route.name === 'pluginDetail')
|
|
787
|
+
&& route.pluginId === intent.pluginId) {
|
|
788
|
+
this.#router.beginConfigFlow(intent.pluginId);
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
this.#router.beginFlow('configEdit', { name: 'pluginDetail', pluginId: intent.pluginId });
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
356
794
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
795
|
+
const kind = intent.kind === 'catalogSourcesRecovery' ? 'sourceRecovery' : 'sourceEdit';
|
|
796
|
+
if (active?.kind === 'sourceRecovery'
|
|
797
|
+
&& (intent.kind === 'catalogSourcesRecovery'
|
|
798
|
+
|| intent.kind === 'catalogGeneration'
|
|
799
|
+
|| intent.kind === 'sourceMutation'))
|
|
800
|
+
return;
|
|
801
|
+
if (active?.kind === kind)
|
|
802
|
+
return;
|
|
803
|
+
this.#router.beginFlow(kind, active ? this.#router.snapshot.current.route
|
|
804
|
+
: intent.kind === 'sourceMutation' && intent.action !== 'remove'
|
|
805
|
+
? { name: 'sourceDetail', sourceId: intent.sourceId }
|
|
806
|
+
: intent.kind === 'catalogGeneration'
|
|
807
|
+
? { name: 'sourceDetail', sourceId: intent.sourceId }
|
|
808
|
+
: { name: 'sourceList' });
|
|
809
|
+
}
|
|
810
|
+
#stableResultRoute(intent, result) {
|
|
811
|
+
const active = this.#router.activeFlow();
|
|
812
|
+
if (active?.parentFlowId)
|
|
813
|
+
return this.#normalizeRoute(active.returnTarget);
|
|
814
|
+
if (active && (intent.kind === 'portableInstall'
|
|
815
|
+
|| intent.kind === 'configSave' || intent.kind === 'configReset')) {
|
|
816
|
+
return this.#normalizeRoute(active.returnTarget);
|
|
817
|
+
}
|
|
818
|
+
if (intent.kind === 'pluginWrite') {
|
|
819
|
+
if (intent.originIssue && result.state === 'succeeded')
|
|
820
|
+
return { name: 'issueList' };
|
|
821
|
+
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
|
|
822
|
+
}
|
|
823
|
+
if (intent.kind === 'pluginWriteBatch')
|
|
824
|
+
return { name: 'pluginList', mode: 'installed' };
|
|
825
|
+
if (intent.kind === 'portableInstall' || intent.kind === 'configSave' || intent.kind === 'configReset') {
|
|
826
|
+
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
|
|
827
|
+
}
|
|
828
|
+
if (intent.kind === 'sourceMutation' && intent.action === 'update') {
|
|
829
|
+
return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
|
|
830
|
+
}
|
|
831
|
+
if (intent.kind === 'catalogGeneration') {
|
|
832
|
+
return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
|
|
833
|
+
}
|
|
834
|
+
return { name: 'sourceList' };
|
|
835
|
+
}
|
|
836
|
+
#showResult(result, intent) {
|
|
837
|
+
const terminal = { name: 'result', operationId: result.operationId };
|
|
838
|
+
const target = this.#stableResultRoute(intent, result);
|
|
839
|
+
this.#resultAnchor = structuredClone(target);
|
|
840
|
+
this.#cancelReviewPreparation();
|
|
841
|
+
this.#progress = { messages: [] };
|
|
842
|
+
this.#progressTargets = [];
|
|
843
|
+
this.#recoverableInstallPlacementDraft = result.state === 'blocked'
|
|
844
|
+
&& intent.kind === 'pluginWrite' && intent.action === 'install'
|
|
845
|
+
&& this.#installPlacementDraft?.pluginId === intent.pluginId
|
|
846
|
+
? structuredClone(this.#installPlacementDraft)
|
|
847
|
+
: undefined;
|
|
848
|
+
this.#installPlacementDraft = undefined;
|
|
849
|
+
this.#recoverablePortableDraft = result.state !== 'succeeded'
|
|
850
|
+
&& intent.kind === 'portableInstall'
|
|
851
|
+
&& this.#portableDraft?.pluginId === intent.pluginId
|
|
852
|
+
? structuredClone(this.#portableDraft)
|
|
853
|
+
: undefined;
|
|
854
|
+
this.#portableResultSkills = intent.kind === 'portableInstall'
|
|
855
|
+
? this.#portableDraft?.candidates
|
|
856
|
+
.filter((candidate) => candidate.selected)
|
|
857
|
+
.map((candidate) => candidate.name)
|
|
858
|
+
: undefined;
|
|
859
|
+
this.#portableDraft = undefined;
|
|
860
|
+
this.#batchRetryIntent = intent.kind === 'pluginWriteBatch'
|
|
861
|
+
&& 'summary' in result
|
|
862
|
+
&& result.summary?.batch
|
|
863
|
+
&& (result.summary.batch.retryTargets?.length ?? 0) > 0
|
|
864
|
+
? {
|
|
865
|
+
...structuredClone(intent),
|
|
866
|
+
selection: {
|
|
867
|
+
kind: 'targets',
|
|
868
|
+
targets: structuredClone(result.summary.batch.retryTargets),
|
|
869
|
+
},
|
|
870
|
+
}
|
|
871
|
+
: undefined;
|
|
872
|
+
if (!this.#router.finishFlow(target, terminal))
|
|
873
|
+
this.#router.replace(terminal);
|
|
364
874
|
}
|
|
365
875
|
#applyStatus(status) {
|
|
366
876
|
this.#status = structuredClone(status);
|
|
877
|
+
this.#statusReadAtMs = this.dependencies.now?.() ?? Date.now();
|
|
367
878
|
if (status.catalogGenerations) {
|
|
368
879
|
this.#catalogGenerations.clear();
|
|
369
880
|
for (const source of status.catalogGenerations) {
|
|
@@ -421,13 +932,14 @@ export class TuiController {
|
|
|
421
932
|
this.#normalizeCurrentRoute();
|
|
422
933
|
}
|
|
423
934
|
async load() {
|
|
935
|
+
this.#assertAcceptingActions();
|
|
424
936
|
this.dependencies.beginCatalogSession?.();
|
|
425
937
|
const generation = ++this.#generation;
|
|
426
938
|
this.#phase = 'loading';
|
|
427
939
|
this.#publish();
|
|
428
940
|
let status;
|
|
429
941
|
try {
|
|
430
|
-
status = await this.dependencies.loadStatus(this.#abort.signal);
|
|
942
|
+
status = await this.#trackRead(this.dependencies.loadStatus(this.#abort.signal));
|
|
431
943
|
}
|
|
432
944
|
catch (error) {
|
|
433
945
|
if (this.#disposed || generation !== this.#generation)
|
|
@@ -436,6 +948,7 @@ export class TuiController {
|
|
|
436
948
|
await this.#logErrorSafe(error, code);
|
|
437
949
|
this.#phase = 'error';
|
|
438
950
|
this.#loadError = { code, message: '无法安全读取插件状态。请检查诊断后重试或退出。' };
|
|
951
|
+
this.#recordStandaloneFailure(code, 'statusReload', error);
|
|
439
952
|
this.#publish();
|
|
440
953
|
return;
|
|
441
954
|
}
|
|
@@ -451,6 +964,10 @@ export class TuiController {
|
|
|
451
964
|
this.#cancelPortableRequest();
|
|
452
965
|
this.#cancelInstallPlacementRequest();
|
|
453
966
|
this.#discardPortableDraftForRoute(route);
|
|
967
|
+
if ((route.name === 'sourceAdd' || route.name === 'sourceEdit' || route.name === 'sourceSave')
|
|
968
|
+
&& this.#router.activeFlow()?.kind !== 'sourceEdit') {
|
|
969
|
+
this.#router.beginFlow('sourceEdit', this.#router.snapshot.current.route);
|
|
970
|
+
}
|
|
454
971
|
this.#router.push(route, context);
|
|
455
972
|
this.#publish();
|
|
456
973
|
}
|
|
@@ -458,6 +975,7 @@ export class TuiController {
|
|
|
458
975
|
return this.#status?.portablePluginIds?.includes(pluginId) ?? false;
|
|
459
976
|
}
|
|
460
977
|
preparePortableInstall(pluginId, navigation = 'push') {
|
|
978
|
+
this.#assertAcceptingActions();
|
|
461
979
|
if (!this.hasPortableSkills(pluginId)) {
|
|
462
980
|
throw new TuiOperationBlockedError('portableUnsupported', '插件不提供通用 Skills。');
|
|
463
981
|
}
|
|
@@ -473,6 +991,7 @@ export class TuiController {
|
|
|
473
991
|
});
|
|
474
992
|
}
|
|
475
993
|
refreshPortableInstallDraft(pluginId) {
|
|
994
|
+
this.#assertAcceptingActions();
|
|
476
995
|
const expectedRoute = this.#router.snapshot.current.route;
|
|
477
996
|
return this.#runPortableRequest(`refresh:${pluginId}`, async (signal, token) => {
|
|
478
997
|
const draft = await this.#buildPortableDraft(pluginId, true, signal);
|
|
@@ -488,12 +1007,12 @@ export class TuiController {
|
|
|
488
1007
|
: undefined;
|
|
489
1008
|
const prepared = await Promise.allSettled(['project', 'user'].map(async (scope) => ({
|
|
490
1009
|
scope,
|
|
491
|
-
evidence: await this.dependencies.captureEvidence({
|
|
1010
|
+
evidence: await this.#trackRead(this.dependencies.captureEvidence({
|
|
492
1011
|
kind: 'portableInstall',
|
|
493
1012
|
pluginId,
|
|
494
1013
|
scope,
|
|
495
1014
|
selectedSkills: [],
|
|
496
|
-
}, signal),
|
|
1015
|
+
}, signal)),
|
|
497
1016
|
})));
|
|
498
1017
|
const available = prepared.flatMap((entry) => entry.status === 'fulfilled' && entry.value.evidence.portable ? [entry.value] : []);
|
|
499
1018
|
if (available.length === 0) {
|
|
@@ -639,8 +1158,91 @@ export class TuiController {
|
|
|
639
1158
|
});
|
|
640
1159
|
this.#publish();
|
|
641
1160
|
}
|
|
1161
|
+
beginSavedConfigRecovery(pluginId) {
|
|
1162
|
+
this.#assertAcceptingActions();
|
|
1163
|
+
const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
|
|
1164
|
+
if (!plugin?.configOnly || plugin.savedConfigScopes.length === 0) {
|
|
1165
|
+
throw new TuiOperationBlockedError('savedConfigUnavailable', '没有可识别的已保存设置。');
|
|
1166
|
+
}
|
|
1167
|
+
this.#router.beginConfigFlow(pluginId);
|
|
1168
|
+
this.#router.push({
|
|
1169
|
+
name: 'configUnavailable',
|
|
1170
|
+
pluginId,
|
|
1171
|
+
storedScopes: plugin.savedConfigScopes,
|
|
1172
|
+
...(plugin.savedConfigScopes.length === 1
|
|
1173
|
+
? { layer: { scope: plugin.savedConfigScopes[0] } }
|
|
1174
|
+
: {}),
|
|
1175
|
+
diagnosticCode: 'catalogSourceUnavailable',
|
|
1176
|
+
diagnosticStage: 'read-config',
|
|
1177
|
+
});
|
|
1178
|
+
this.#publish();
|
|
1179
|
+
}
|
|
1180
|
+
beginConfigSourceRecovery(sourceId) {
|
|
1181
|
+
this.#assertAcceptingActions();
|
|
1182
|
+
const route = this.#router.snapshot.current.route;
|
|
1183
|
+
const active = this.#router.activeFlow();
|
|
1184
|
+
if (route.name !== 'configUnavailable'
|
|
1185
|
+
|| active?.kind !== 'configEdit'
|
|
1186
|
+
|| route.storedScopes !== undefined
|
|
1187
|
+
|| sourceIdFromPluginId(route.pluginId) !== sourceId
|
|
1188
|
+
|| !this.#viewModel?.sourceTabs.some((source) => source.id === sourceId)) {
|
|
1189
|
+
throw new TuiOperationBlockedError('configSourceRecoveryUnavailable', '来源恢复只能从不可用的设置流程开始。');
|
|
1190
|
+
}
|
|
1191
|
+
const layer = route.layer?.scope ?? route.resumeRoute?.layer.scope ?? 'effective';
|
|
1192
|
+
const returnTarget = route.resumeRoute ?? {
|
|
1193
|
+
name: 'configFields',
|
|
1194
|
+
pluginId: route.pluginId,
|
|
1195
|
+
layer: { scope: layer },
|
|
1196
|
+
};
|
|
1197
|
+
this.#router.beginFlow('sourceRecovery', returnTarget, undefined, route.resumeContext);
|
|
1198
|
+
this.#router.push({ name: 'sourceDetail', sourceId });
|
|
1199
|
+
this.#publish();
|
|
1200
|
+
}
|
|
1201
|
+
async getForceCleanupCandidate(pluginId, platform, scope) {
|
|
1202
|
+
this.#assertAcceptingActions();
|
|
1203
|
+
const intent = {
|
|
1204
|
+
kind: 'pluginWrite', action: 'cleanup', pluginId, platform, scope, force: true,
|
|
1205
|
+
};
|
|
1206
|
+
try {
|
|
1207
|
+
const evidence = await this.#trackRead(this.dependencies.captureEvidence(intent, this.#abort.signal));
|
|
1208
|
+
const candidate = evidence.offlineCleanup;
|
|
1209
|
+
if (candidate?.forceExclusive !== true
|
|
1210
|
+
|| candidate.pluginId !== pluginId
|
|
1211
|
+
|| candidate.platform !== platform
|
|
1212
|
+
|| candidate.scope !== scope
|
|
1213
|
+
|| candidate.plan.actions.length === 0)
|
|
1214
|
+
return undefined;
|
|
1215
|
+
return cloneFrozen({ pluginId, platform, scope, actionCount: candidate.plan.actions.length });
|
|
1216
|
+
}
|
|
1217
|
+
catch (error) {
|
|
1218
|
+
await this.#logOperationError(error, issueCode(error), intent);
|
|
1219
|
+
return undefined;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
startForceCleanupReview(candidate) {
|
|
1223
|
+
if (candidate.actionCount <= 0) {
|
|
1224
|
+
throw new TuiOperationBlockedError('forceCleanupPathUnavailable', '没有可验证的精确清理目标。');
|
|
1225
|
+
}
|
|
1226
|
+
return this.startReviewPreparation({
|
|
1227
|
+
kind: 'pluginWrite',
|
|
1228
|
+
action: 'cleanup',
|
|
1229
|
+
pluginId: candidate.pluginId,
|
|
1230
|
+
platform: candidate.platform,
|
|
1231
|
+
scope: candidate.scope,
|
|
1232
|
+
force: true,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
642
1235
|
configOrigin(pluginId) {
|
|
643
|
-
|
|
1236
|
+
const origin = this.#router.configOrigin(pluginId);
|
|
1237
|
+
if (origin)
|
|
1238
|
+
return origin;
|
|
1239
|
+
if (this.#router.snapshot.current.route.name === 'result'
|
|
1240
|
+
&& this.#resultAnchor
|
|
1241
|
+
&& (this.#resultAnchor.name === 'pluginPreview' || this.#resultAnchor.name === 'pluginDetail')
|
|
1242
|
+
&& this.#resultAnchor.pluginId === pluginId) {
|
|
1243
|
+
return structuredClone(this.#resultAnchor);
|
|
1244
|
+
}
|
|
1245
|
+
return undefined;
|
|
644
1246
|
}
|
|
645
1247
|
updatePageContext(context) {
|
|
646
1248
|
this.#router.updateContext(context);
|
|
@@ -648,6 +1250,7 @@ export class TuiController {
|
|
|
648
1250
|
}
|
|
649
1251
|
back() {
|
|
650
1252
|
const route = this.#router.snapshot.current.route;
|
|
1253
|
+
const activeFlow = this.#router.activeFlow();
|
|
651
1254
|
this.#cancelReviewPreparation();
|
|
652
1255
|
this.#cancelPortableRequest();
|
|
653
1256
|
this.#cancelInstallPlacementRequest();
|
|
@@ -656,6 +1259,14 @@ export class TuiController {
|
|
|
656
1259
|
this.#activeReview = undefined;
|
|
657
1260
|
}
|
|
658
1261
|
this.#router.back();
|
|
1262
|
+
if (activeFlow
|
|
1263
|
+
&& this.#router.snapshot.currentFrameId === activeFlow.originFrameId) {
|
|
1264
|
+
this.#router.abandonFlow(activeFlow.id);
|
|
1265
|
+
if (activeFlow.kind === 'pluginInstall')
|
|
1266
|
+
this.#installPlacementDraft = undefined;
|
|
1267
|
+
if (activeFlow.kind === 'portableInstall')
|
|
1268
|
+
this.#portableDraft = undefined;
|
|
1269
|
+
}
|
|
659
1270
|
this.#normalizeCurrentRoute();
|
|
660
1271
|
this.#discardPortableDraftForRoute(this.#router.snapshot.current.route);
|
|
661
1272
|
this.#publish();
|
|
@@ -666,13 +1277,34 @@ export class TuiController {
|
|
|
666
1277
|
this.#activeReview = undefined;
|
|
667
1278
|
}
|
|
668
1279
|
this.#result = undefined;
|
|
669
|
-
this.#router.
|
|
1280
|
+
const activeFlow = this.#router.activeFlow();
|
|
1281
|
+
if (activeFlow?.kind === 'sourceEdit') {
|
|
1282
|
+
this.#router.abandonFlow(activeFlow.id);
|
|
1283
|
+
if (JSON.stringify(this.#router.snapshot.current.route) !== JSON.stringify(returnRoute)) {
|
|
1284
|
+
this.#router.replace(returnRoute);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
this.#router.discardSourceFlow(owner, returnRoute);
|
|
1289
|
+
}
|
|
670
1290
|
this.#normalizeCurrentRoute();
|
|
671
1291
|
this.#publish();
|
|
672
1292
|
}
|
|
673
1293
|
leaveResult(target) {
|
|
674
1294
|
if (this.#router.snapshot.current.route.name !== 'result')
|
|
675
1295
|
return;
|
|
1296
|
+
if (target?.name === 'home') {
|
|
1297
|
+
this.#router.resetToRoot();
|
|
1298
|
+
this.#portableDraft = undefined;
|
|
1299
|
+
this.#recoverablePortableDraft = undefined;
|
|
1300
|
+
this.#installPlacementDraft = undefined;
|
|
1301
|
+
this.#recoverableInstallPlacementDraft = undefined;
|
|
1302
|
+
this.#result = undefined;
|
|
1303
|
+
this.#resultAnchor = undefined;
|
|
1304
|
+
this.#portableResultSkills = undefined;
|
|
1305
|
+
this.#publish();
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
676
1308
|
if (this.#result?.operationKind === 'portableInstall'
|
|
677
1309
|
&& 'pluginId' in this.#result && this.#result.pluginId) {
|
|
678
1310
|
const preserveDraft = target?.name === 'portableSkillSelect'
|
|
@@ -688,19 +1320,55 @@ export class TuiController {
|
|
|
688
1320
|
this.#publish();
|
|
689
1321
|
return;
|
|
690
1322
|
}
|
|
691
|
-
const route = target ?? (this.#result && 'returnRoute' in this.#result
|
|
692
|
-
? this.#result.returnRoute
|
|
693
|
-
: undefined);
|
|
694
1323
|
this.#router.back();
|
|
695
|
-
if (
|
|
696
|
-
this.#router.replace(this.#normalizeRoute(
|
|
1324
|
+
if (target)
|
|
1325
|
+
this.#router.replace(this.#normalizeRoute(target));
|
|
697
1326
|
else
|
|
698
1327
|
this.#normalizeCurrentRoute();
|
|
699
1328
|
this.#discardPortableDraftForRoute(this.#router.snapshot.current.route);
|
|
700
1329
|
this.#result = undefined;
|
|
1330
|
+
this.#resultAnchor = undefined;
|
|
1331
|
+
this.#portableResultSkills = undefined;
|
|
1332
|
+
this.#recoverableInstallPlacementDraft = undefined;
|
|
1333
|
+
this.#recoverablePortableDraft = undefined;
|
|
1334
|
+
this.#publish();
|
|
1335
|
+
}
|
|
1336
|
+
resumeResultDraft() {
|
|
1337
|
+
const result = this.#result;
|
|
1338
|
+
if (this.#router.snapshot.current.route.name !== 'result' || !result
|
|
1339
|
+
|| !('returnRoute' in result) || !result.returnRoute)
|
|
1340
|
+
return;
|
|
1341
|
+
const returnRoute = this.#normalizeRoute(result.returnRoute);
|
|
1342
|
+
this.#router.back();
|
|
1343
|
+
const kind = result.operationKind === 'configSave' || result.operationKind === 'configReset'
|
|
1344
|
+
? 'configEdit'
|
|
1345
|
+
: result.operationKind === 'portableInstall'
|
|
1346
|
+
? 'portableInstall'
|
|
1347
|
+
: result.operationKind === 'sourceMutation' || result.operationKind === 'catalogGeneration'
|
|
1348
|
+
|| result.operationKind === 'catalogSourcesRecovery'
|
|
1349
|
+
? 'sourceEdit'
|
|
1350
|
+
: 'pluginMaintenance';
|
|
1351
|
+
this.#router.beginFlow(kind, this.#router.snapshot.current.route);
|
|
1352
|
+
if (result.operationKind === 'pluginWrite') {
|
|
1353
|
+
this.#installPlacementDraft = this.#recoverableInstallPlacementDraft;
|
|
1354
|
+
}
|
|
1355
|
+
if (result.operationKind === 'portableInstall') {
|
|
1356
|
+
this.#portableDraft = this.#recoverablePortableDraft;
|
|
1357
|
+
}
|
|
1358
|
+
this.#router.push(returnRoute);
|
|
1359
|
+
this.#recoverableInstallPlacementDraft = undefined;
|
|
1360
|
+
this.#recoverablePortableDraft = undefined;
|
|
1361
|
+
this.#result = undefined;
|
|
1362
|
+
this.#resultAnchor = undefined;
|
|
1363
|
+
this.#portableResultSkills = undefined;
|
|
701
1364
|
this.#publish();
|
|
702
1365
|
}
|
|
703
1366
|
#discardPortableDraftForRoute(route) {
|
|
1367
|
+
const readOnlyHistoryRoute = route.name === 'operationHistory'
|
|
1368
|
+
|| route.name === 'operationDiagnostic'
|
|
1369
|
+
|| route.name === 'operationReportPreview';
|
|
1370
|
+
if (readOnlyHistoryRoute && this.#router.activeFlow()?.kind === 'portableInstall')
|
|
1371
|
+
return;
|
|
704
1372
|
const portableRoute = route.name === 'portableSkillSelect'
|
|
705
1373
|
|| route.name === 'portableDependencies'
|
|
706
1374
|
|| route.name === 'portableAgentMode'
|
|
@@ -709,7 +1377,8 @@ export class TuiController {
|
|
|
709
1377
|
|| route.name === 'portableBlocker';
|
|
710
1378
|
const portableOperationRoute = (route.name === 'reviewPreparing' || route.name === 'review'
|
|
711
1379
|
|| route.name === 'progress' || route.name === 'result')
|
|
712
|
-
&& (this.#reviewPreparation
|
|
1380
|
+
&& (this.#reviewPreparation !== undefined
|
|
1381
|
+
&& this.#reviewPreparationIntents.get(this.#reviewPreparation.snapshot.id)?.kind === 'portableInstall'
|
|
713
1382
|
|| this.#activeReview?.operationKind === 'portableInstall'
|
|
714
1383
|
|| this.#result?.operationKind === 'portableInstall');
|
|
715
1384
|
if (!portableRoute && !portableOperationRoute) {
|
|
@@ -719,15 +1388,24 @@ export class TuiController {
|
|
|
719
1388
|
}
|
|
720
1389
|
resetPortableFlowToPreview(pluginId, target, preserveDraft = false) {
|
|
721
1390
|
this.#cancelPortableRequest();
|
|
1391
|
+
if (preserveDraft && !this.#portableDraft) {
|
|
1392
|
+
this.#portableDraft = this.#recoverablePortableDraft;
|
|
1393
|
+
}
|
|
722
1394
|
if (!preserveDraft)
|
|
723
1395
|
this.#portableDraft = undefined;
|
|
1396
|
+
this.#recoverablePortableDraft = undefined;
|
|
724
1397
|
if (this.#activeReview?.operationKind === 'portableInstall') {
|
|
725
1398
|
this.#tickets.delete(this.#activeReview.reviewToken);
|
|
726
1399
|
this.#activeReview = undefined;
|
|
727
1400
|
}
|
|
728
1401
|
if (this.#result?.operationKind === 'portableInstall')
|
|
729
1402
|
this.#result = undefined;
|
|
730
|
-
this.#
|
|
1403
|
+
this.#resultAnchor = undefined;
|
|
1404
|
+
this.#portableResultSkills = undefined;
|
|
1405
|
+
if (this.#router.snapshot.current.route.name === 'result')
|
|
1406
|
+
this.#router.back();
|
|
1407
|
+
else
|
|
1408
|
+
this.#router.resetPortableFlow(pluginId);
|
|
731
1409
|
if (target && JSON.stringify(this.#router.snapshot.current.route) !== JSON.stringify(target)) {
|
|
732
1410
|
this.#router.beginPortableFlow(pluginId);
|
|
733
1411
|
this.#router.push(this.#normalizeRoute(target));
|
|
@@ -766,6 +1444,9 @@ export class TuiController {
|
|
|
766
1444
|
}
|
|
767
1445
|
beginInstallFlow(pluginId, rootScope, options = {}) {
|
|
768
1446
|
this.#cancelInstallPlacementRequest();
|
|
1447
|
+
const active = this.#router.activeFlow();
|
|
1448
|
+
if (active?.kind !== 'pluginInstall')
|
|
1449
|
+
this.#router.beginPluginFlow('pluginInstall', pluginId);
|
|
769
1450
|
this.#installPlacementDraft = {
|
|
770
1451
|
pluginId,
|
|
771
1452
|
rootScope,
|
|
@@ -838,6 +1519,7 @@ export class TuiController {
|
|
|
838
1519
|
};
|
|
839
1520
|
}
|
|
840
1521
|
async prepareInstallDependencyPlacement() {
|
|
1522
|
+
this.#assertAcceptingActions();
|
|
841
1523
|
const draft = this.#installPlacementDraft;
|
|
842
1524
|
if (!draft)
|
|
843
1525
|
throw new TuiOperationBlockedError('installPlacementMissing', '安装位置草稿不存在。');
|
|
@@ -859,7 +1541,7 @@ export class TuiController {
|
|
|
859
1541
|
throw new DOMException('Dependency placement preview was cancelled.', 'AbortError');
|
|
860
1542
|
}
|
|
861
1543
|
};
|
|
862
|
-
const plan = await preview(this.#installPlacementIntent());
|
|
1544
|
+
const plan = await this.#trackRead(preview(this.#installPlacementIntent()));
|
|
863
1545
|
throwIfCancelled();
|
|
864
1546
|
if (!plan) {
|
|
865
1547
|
draft.state = 'ready';
|
|
@@ -925,7 +1607,7 @@ export class TuiController {
|
|
|
925
1607
|
listPlugins(options) {
|
|
926
1608
|
const query = options.query?.trim().toLocaleLowerCase('en');
|
|
927
1609
|
return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
|
|
928
|
-
&& (options.mode === 'browse' || hasInstalledPlacement(plugin))
|
|
1610
|
+
&& (options.mode === 'browse' || hasInstalledPlacement(plugin) || plugin.savedConfigScopes.length > 0)
|
|
929
1611
|
&& (options.installFilter !== 'uninstalled' || !hasInstalledPlacement(plugin))
|
|
930
1612
|
&& (!query || `${plugin.displayName}\n${plugin.description}\n${plugin.sourceId}`.toLocaleLowerCase('en').includes(query))));
|
|
931
1613
|
}
|
|
@@ -1026,6 +1708,10 @@ export class TuiController {
|
|
|
1026
1708
|
health: this.#health.get(sourceId),
|
|
1027
1709
|
});
|
|
1028
1710
|
}
|
|
1711
|
+
getConfiguredSource(sourceId) {
|
|
1712
|
+
const source = this.#status?.configuredSources?.find((candidate) => candidate.id === sourceId);
|
|
1713
|
+
return source ? cloneFrozen(source) : undefined;
|
|
1714
|
+
}
|
|
1029
1715
|
getCatalogGeneration(sourceId) {
|
|
1030
1716
|
const source = this.#catalogGenerations.get(sourceId);
|
|
1031
1717
|
return source ? cloneFrozen(source) : undefined;
|
|
@@ -1035,17 +1721,70 @@ export class TuiController {
|
|
|
1035
1721
|
return freshness ? cloneFrozen(freshness) : undefined;
|
|
1036
1722
|
}
|
|
1037
1723
|
recheckCatalogSource(sourceId) {
|
|
1724
|
+
this.#assertAcceptingActions();
|
|
1038
1725
|
return this.#catalogSourcesSession.recheck(sourceId);
|
|
1039
1726
|
}
|
|
1727
|
+
async recheckAllCatalogSources() {
|
|
1728
|
+
this.#assertAcceptingActions();
|
|
1729
|
+
const sourceIds = [...this.listSources()]
|
|
1730
|
+
.sort((left, right) => Number(Boolean(right.primary)) - Number(Boolean(left.primary)))
|
|
1731
|
+
.map((source) => source.id);
|
|
1732
|
+
let next = 0;
|
|
1733
|
+
const worker = async () => {
|
|
1734
|
+
while (next < sourceIds.length) {
|
|
1735
|
+
const sourceId = sourceIds[next++];
|
|
1736
|
+
if (sourceId)
|
|
1737
|
+
await this.#catalogSourcesSession.recheck(sourceId);
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
await Promise.all(Array.from({ length: Math.min(2, sourceIds.length) }, () => worker()));
|
|
1741
|
+
}
|
|
1040
1742
|
fullValidateCatalogSource(sourceId) {
|
|
1743
|
+
this.#assertAcceptingActions();
|
|
1041
1744
|
return this.#catalogSourcesSession.fullValidate(sourceId);
|
|
1042
1745
|
}
|
|
1043
|
-
setExternalDraftState(
|
|
1044
|
-
|
|
1746
|
+
setExternalDraftState(states = []) {
|
|
1747
|
+
const priority = { sourceDraft: 0, batchDraft: 1, configDraft: 2 };
|
|
1748
|
+
this.#externalDrafts = structuredClone(states).sort((left, right) => priority[left.kind] - priority[right.kind] || left.owner.localeCompare(right.owner, 'en'));
|
|
1749
|
+
this.#publish();
|
|
1750
|
+
}
|
|
1751
|
+
beginPluginWriteBatch() {
|
|
1752
|
+
this.#assertAcceptingActions();
|
|
1753
|
+
this.#router.beginBatchFlow();
|
|
1754
|
+
this.#router.push({ name: 'batchSelection' });
|
|
1755
|
+
this.#publish();
|
|
1756
|
+
}
|
|
1757
|
+
startPluginWriteBatchReview(intent) {
|
|
1758
|
+
return this.startReviewPreparation(intent, { name: 'batchPlacement' });
|
|
1759
|
+
}
|
|
1760
|
+
retryFailedPluginWriteBatch() {
|
|
1761
|
+
if (!this.#batchRetryIntent)
|
|
1762
|
+
return undefined;
|
|
1763
|
+
const intent = structuredClone(this.#batchRetryIntent);
|
|
1764
|
+
this.#batchRetryIntent = undefined;
|
|
1765
|
+
return this.startReviewPreparation(intent, { name: 'pluginList', mode: 'installed' });
|
|
1045
1766
|
}
|
|
1046
1767
|
catalogActivationBlockReason() {
|
|
1047
|
-
if (this.#
|
|
1048
|
-
return this.#
|
|
1768
|
+
if (this.#externalDrafts[0])
|
|
1769
|
+
return this.#externalDrafts[0].kind;
|
|
1770
|
+
if (this.#portableDraft)
|
|
1771
|
+
return 'portableDraft';
|
|
1772
|
+
if (this.#reviewPreparation)
|
|
1773
|
+
return 'reviewPreparation';
|
|
1774
|
+
if (this.#activeReview)
|
|
1775
|
+
return 'activeReview';
|
|
1776
|
+
if (this.#router.snapshot.current.route.name === 'progress'
|
|
1777
|
+
|| this.#inflightOperations.size > 0)
|
|
1778
|
+
return 'writeProgress';
|
|
1779
|
+
return undefined;
|
|
1780
|
+
}
|
|
1781
|
+
catalogSourceRecoveryBlockReason() {
|
|
1782
|
+
const activeFlow = this.#router.activeFlow();
|
|
1783
|
+
const blockingExternalDraft = this.#externalDrafts.find((draft) => !(draft.kind === 'configDraft'
|
|
1784
|
+
&& activeFlow?.kind === 'sourceRecovery'
|
|
1785
|
+
&& activeFlow.parentFlowId !== undefined));
|
|
1786
|
+
if (blockingExternalDraft)
|
|
1787
|
+
return blockingExternalDraft.kind;
|
|
1049
1788
|
if (this.#portableDraft)
|
|
1050
1789
|
return 'portableDraft';
|
|
1051
1790
|
if (this.#reviewPreparation)
|
|
@@ -1061,6 +1800,7 @@ export class TuiController {
|
|
|
1061
1800
|
return this.#status?.payload.catalog.sources.find((source) => source.id === sourceId)?.catalogVersion;
|
|
1062
1801
|
}
|
|
1063
1802
|
async reloadCatalog(options) {
|
|
1803
|
+
this.#assertAcceptingActions();
|
|
1064
1804
|
if (this.catalogActivationBlockReason())
|
|
1065
1805
|
return false;
|
|
1066
1806
|
this.dependencies.beginCatalogSession?.();
|
|
@@ -1068,7 +1808,7 @@ export class TuiController {
|
|
|
1068
1808
|
this.#phase = 'loading';
|
|
1069
1809
|
this.#publish();
|
|
1070
1810
|
try {
|
|
1071
|
-
const status = await this.dependencies.loadStatus(this.#abort.signal);
|
|
1811
|
+
const status = await this.#trackRead(this.dependencies.loadStatus(this.#abort.signal));
|
|
1072
1812
|
if (this.#disposed || generation !== this.#generation)
|
|
1073
1813
|
return false;
|
|
1074
1814
|
this.#applyStatus(status);
|
|
@@ -1090,39 +1830,45 @@ export class TuiController {
|
|
|
1090
1830
|
}
|
|
1091
1831
|
}
|
|
1092
1832
|
async inspectSourceRemoval(sourceId) {
|
|
1833
|
+
this.#assertAcceptingActions();
|
|
1093
1834
|
if (!this.dependencies.inspectSourceRemoval) {
|
|
1094
1835
|
throw new TuiOperationBlockedError('stateInventoryFailed', '来源引用清单不可用。');
|
|
1095
1836
|
}
|
|
1096
|
-
return cloneFrozen(await this.dependencies.inspectSourceRemoval(sourceId, this.#abort.signal));
|
|
1837
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.inspectSourceRemoval(sourceId, this.#abort.signal)));
|
|
1097
1838
|
}
|
|
1098
1839
|
async getConfigGroups(pluginId, layer) {
|
|
1840
|
+
this.#assertAcceptingActions();
|
|
1099
1841
|
if (!this.dependencies.getConfigGroups) {
|
|
1100
1842
|
throw new TuiOperationBlockedError('configUnavailable', '设置读取入口不可用。');
|
|
1101
1843
|
}
|
|
1102
|
-
return cloneFrozen(await this.dependencies.getConfigGroups(pluginId, structuredClone(layer), this.#abort.signal));
|
|
1844
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.getConfigGroups(pluginId, structuredClone(layer), this.#abort.signal)));
|
|
1103
1845
|
}
|
|
1104
1846
|
async getConfigPage(pluginId, layer, signal) {
|
|
1847
|
+
this.#assertAcceptingActions();
|
|
1105
1848
|
if (!this.dependencies.getConfigPage) {
|
|
1106
1849
|
throw new TuiOperationBlockedError('configUnavailable', '设置读取入口不可用。');
|
|
1107
1850
|
}
|
|
1108
1851
|
const combined = signal
|
|
1109
1852
|
? AbortSignal.any([this.#abort.signal, signal])
|
|
1110
1853
|
: this.#abort.signal;
|
|
1111
|
-
return cloneFrozen(await this.dependencies.getConfigPage(pluginId, structuredClone(layer), combined));
|
|
1854
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.getConfigPage(pluginId, structuredClone(layer), combined)));
|
|
1112
1855
|
}
|
|
1113
1856
|
async getConfigResetAvailability(pluginId, layer) {
|
|
1857
|
+
this.#assertAcceptingActions();
|
|
1114
1858
|
if (!this.dependencies.getConfigResetAvailability) {
|
|
1115
1859
|
throw new TuiOperationBlockedError('configResetVerificationUnavailable', '设置层可信读取入口不可用。');
|
|
1116
1860
|
}
|
|
1117
|
-
return cloneFrozen(await this.dependencies.getConfigResetAvailability(pluginId, structuredClone(layer), this.#abort.signal));
|
|
1861
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.getConfigResetAvailability(pluginId, structuredClone(layer), this.#abort.signal)));
|
|
1118
1862
|
}
|
|
1119
1863
|
listUserIssues() {
|
|
1120
1864
|
return cloneFrozen(this.#viewModel?.issues ?? []);
|
|
1121
1865
|
}
|
|
1122
1866
|
async buildOperationReview(intent, returnRoute) {
|
|
1867
|
+
this.#assertAcceptingActions();
|
|
1868
|
+
intent = normalizeOperationIntent(intent);
|
|
1123
1869
|
try {
|
|
1124
1870
|
this.#validateReviewIntent(intent);
|
|
1125
|
-
const evidence = await this.#prepareReviewEvidence(intent, this.#abort.signal);
|
|
1871
|
+
const evidence = await this.#trackRead(this.#prepareReviewEvidence(intent, this.#abort.signal));
|
|
1126
1872
|
return this.#storeOperationReview(intent, evidence, 'push', returnRoute);
|
|
1127
1873
|
}
|
|
1128
1874
|
catch (error) {
|
|
@@ -1134,12 +1880,15 @@ export class TuiController {
|
|
|
1134
1880
|
}
|
|
1135
1881
|
}
|
|
1136
1882
|
startReviewPreparation(intent, returnRoute) {
|
|
1883
|
+
this.#assertAcceptingActions();
|
|
1884
|
+
intent = normalizeOperationIntent(intent);
|
|
1137
1885
|
if (intent.kind === 'catalogGeneration' && !intent.sourceMutation) {
|
|
1138
|
-
const blockReason = this.
|
|
1886
|
+
const blockReason = this.catalogSourceRecoveryBlockReason();
|
|
1139
1887
|
if (blockReason) {
|
|
1140
1888
|
throw new TuiOperationBlockedError('catalogActivationBlocked', `Catalog Active 切换已阻止:${blockReason}。请先完成或放弃当前草稿或操作。`);
|
|
1141
1889
|
}
|
|
1142
1890
|
}
|
|
1891
|
+
this.#ensureOperationFlow(intent);
|
|
1143
1892
|
const inheritedReturnRoute = this.#reviewPreparation?.returnRoute;
|
|
1144
1893
|
const current = this.#router.snapshot.current.route;
|
|
1145
1894
|
const navigation = current.name === 'result' || current.name === 'reviewPreparing'
|
|
@@ -1151,7 +1900,9 @@ export class TuiController {
|
|
|
1151
1900
|
? this.#result.returnRoute
|
|
1152
1901
|
: current);
|
|
1153
1902
|
const normalizedReturnRoute = this.#normalizeRoute(resolvedReturnRoute);
|
|
1154
|
-
const key =
|
|
1903
|
+
const key = crypto.createHash('sha256')
|
|
1904
|
+
.update(JSON.stringify(stableValue({ intent, returnRoute: normalizedReturnRoute })))
|
|
1905
|
+
.digest('hex');
|
|
1155
1906
|
if (this.#reviewPreparation?.snapshot.state === 'running'
|
|
1156
1907
|
&& this.#reviewPreparation.key === key) {
|
|
1157
1908
|
return cloneFrozen(this.#reviewPreparation.snapshot);
|
|
@@ -1162,14 +1913,16 @@ export class TuiController {
|
|
|
1162
1913
|
const token = ++this.#reviewPreparationToken;
|
|
1163
1914
|
const snapshot = {
|
|
1164
1915
|
id,
|
|
1165
|
-
|
|
1916
|
+
operationKind: intent.kind,
|
|
1917
|
+
subject: this.#reviewPreparationLabel(intent),
|
|
1166
1918
|
state: 'running',
|
|
1167
1919
|
stages: this.dependencies.prepareReviewEvidence ? [] : [
|
|
1168
1920
|
{ id: 'validate', label: '验证当前状态', state: 'running' },
|
|
1169
1921
|
{ id: 'evidence', label: '准备确认信息', state: 'pending' },
|
|
1170
1922
|
],
|
|
1171
|
-
|
|
1923
|
+
...(this.dependencies.prepareReviewEvidence ? {} : { currentStage: 'validate' }),
|
|
1172
1924
|
};
|
|
1925
|
+
this.#reviewPreparationIntents.set(id, structuredClone(intent));
|
|
1173
1926
|
this.#reviewPreparation = {
|
|
1174
1927
|
snapshot,
|
|
1175
1928
|
key,
|
|
@@ -1187,9 +1940,25 @@ export class TuiController {
|
|
|
1187
1940
|
const preparation = this.#reviewPreparation;
|
|
1188
1941
|
if (!preparation || preparation.snapshot.state !== 'failed')
|
|
1189
1942
|
return undefined;
|
|
1190
|
-
|
|
1943
|
+
const intent = this.#reviewPreparationIntents.get(preparation.snapshot.id);
|
|
1944
|
+
if (!intent)
|
|
1945
|
+
return undefined;
|
|
1946
|
+
return this.startReviewPreparation(structuredClone(intent), preparation.returnRoute);
|
|
1191
1947
|
}
|
|
1192
1948
|
#validateReviewIntent(intent) {
|
|
1949
|
+
if (intent.kind === 'pluginWriteBatch') {
|
|
1950
|
+
if (intent.selection.kind === 'currentProjectIntentRoots'
|
|
1951
|
+
&& (intent.action !== 'sync' || intent.scope !== 'project')) {
|
|
1952
|
+
throw new TuiOperationBlockedError('batchSelectionInvalid', '当前项目全部同步只支持 project scope sync。');
|
|
1953
|
+
}
|
|
1954
|
+
if (intent.selection.kind === 'selected') {
|
|
1955
|
+
const canonical = canonicalPluginIds(intent.selection.pluginIds);
|
|
1956
|
+
if (canonical.length === 0) {
|
|
1957
|
+
throw new TuiOperationBlockedError('batchSelectionInvalid', '批量插件选择无效。');
|
|
1958
|
+
}
|
|
1959
|
+
canonical.forEach(sourceIdFromPluginId);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1193
1962
|
const primaryGeneration = intent.kind === 'sourceMutation' && intent.action === 'setPrimary'
|
|
1194
1963
|
? this.#catalogGenerations.get(intent.sourceId) : undefined;
|
|
1195
1964
|
if (primaryGeneration && !primaryGeneration.active && primaryGeneration.state !== 'ready') {
|
|
@@ -1199,6 +1968,11 @@ export class TuiController {
|
|
|
1199
1968
|
#reviewPreparationLabel(intent) {
|
|
1200
1969
|
if (intent.kind === 'catalogSourcesRecovery')
|
|
1201
1970
|
return 'Catalog 来源配置';
|
|
1971
|
+
if (intent.kind === 'pluginWriteBatch') {
|
|
1972
|
+
return intent.selection.kind === 'selected'
|
|
1973
|
+
? `${intent.selection.pluginIds.length} 个插件`
|
|
1974
|
+
: '当前项目全部插件';
|
|
1975
|
+
}
|
|
1202
1976
|
if ('pluginId' in intent) {
|
|
1203
1977
|
return this.#viewModel?.plugins.find((plugin) => plugin.pluginId === intent.pluginId)?.displayName
|
|
1204
1978
|
?? pluginNameFromId(intent.pluginId);
|
|
@@ -1217,13 +1991,17 @@ export class TuiController {
|
|
|
1217
1991
|
const preparation = this.#reviewPreparation;
|
|
1218
1992
|
if (!preparation || preparation.token !== token)
|
|
1219
1993
|
return;
|
|
1220
|
-
const
|
|
1994
|
+
const storedIntent = this.#reviewPreparationIntents.get(preparation.snapshot.id);
|
|
1995
|
+
if (!storedIntent)
|
|
1996
|
+
return;
|
|
1997
|
+
const intent = structuredClone(storedIntent);
|
|
1221
1998
|
const signal = AbortSignal.any([this.#abort.signal, preparation.abort.signal]);
|
|
1222
1999
|
try {
|
|
1223
2000
|
this.#validateReviewIntent(intent);
|
|
1224
|
-
const evidence = await this.#prepareReviewEvidence(intent, signal, (stage) => {
|
|
2001
|
+
const evidence = await this.#trackRead(this.#prepareReviewEvidence(intent, signal, (stage) => {
|
|
1225
2002
|
if (!this.#reviewPreparationIsCurrent(token))
|
|
1226
2003
|
return;
|
|
2004
|
+
preparation.snapshot.currentStage = stage;
|
|
1227
2005
|
const existing = preparation.snapshot.stages.find((entry) => entry.id === stage);
|
|
1228
2006
|
preparation.snapshot.stages = preparation.snapshot.stages.map((entry) => entry.state === 'running' ? { ...entry, state: 'completed' } : entry);
|
|
1229
2007
|
if (existing) {
|
|
@@ -1237,12 +2015,13 @@ export class TuiController {
|
|
|
1237
2015
|
});
|
|
1238
2016
|
}
|
|
1239
2017
|
this.#publish();
|
|
1240
|
-
});
|
|
2018
|
+
}));
|
|
1241
2019
|
if (!this.#reviewPreparationIsCurrent(token))
|
|
1242
2020
|
return;
|
|
1243
2021
|
preparation.snapshot.stages = preparation.snapshot.stages.map((stage) => stage.state === 'running' ? { ...stage, state: 'completed' } : stage);
|
|
1244
2022
|
this.#publish();
|
|
1245
2023
|
const returnRoute = structuredClone(preparation.returnRoute);
|
|
2024
|
+
this.#reviewPreparationIntents.delete(preparation.snapshot.id);
|
|
1246
2025
|
this.#reviewPreparation = undefined;
|
|
1247
2026
|
this.#storeOperationReview(intent, evidence, 'replace', returnRoute);
|
|
1248
2027
|
}
|
|
@@ -1269,7 +2048,10 @@ export class TuiController {
|
|
|
1269
2048
|
: code === 'catalogGenerationActiveComparisonUnavailable'
|
|
1270
2049
|
? '损坏的当前 Active 无法提供可信版本,不能安全比较候选。当前 Active 保持不变。请明确编辑来源,或移除后重新添加。'
|
|
1271
2050
|
: code === 'catalogReplaceDamagedRequired'
|
|
1272
|
-
?
|
|
2051
|
+
? intent.kind === 'catalogGeneration'
|
|
2052
|
+
&& this.#catalogFreshness.get(intent.sourceId)?.state === 'incompatible'
|
|
2053
|
+
? '无法重新取得或精确匹配旧 Active,且当前候选与此 AW 不兼容。当前 Active 保持不变。请升级 AW 或修改来源。'
|
|
2054
|
+
: '无法重新取得或精确匹配旧 Active。当前 Active 保持不变。请返回并选择“使用当前候选替换损坏的 Active”。'
|
|
1273
2055
|
: code === 'catalogVersionConflict'
|
|
1274
2056
|
? '候选 Catalog 与当前 Active 版本相同,但内容摘要不同。普通更新已阻止。'
|
|
1275
2057
|
: code === 'catalogRollbackDetected'
|
|
@@ -1277,6 +2059,34 @@ export class TuiController {
|
|
|
1277
2059
|
: code === 'catalogGenerationUnavailable'
|
|
1278
2060
|
? '本次加载时 Catalog 来源不可用。请刷新来源并重新加载后重试。'
|
|
1279
2061
|
: '无法准备确认信息。请返回检查选择,或重试。';
|
|
2062
|
+
const preparationRecordId = `${this.#sessionEpoch}:${active.snapshot.id}`;
|
|
2063
|
+
const preparationDiagnostic = projectThrownOperationDiagnostic({
|
|
2064
|
+
error,
|
|
2065
|
+
id: `${preparationRecordId}:failure`,
|
|
2066
|
+
stage: active.snapshot.currentStage ?? 'reviewPreparation',
|
|
2067
|
+
fallbackCode: code,
|
|
2068
|
+
sensitiveValues: collectOperationSensitiveValues(intent),
|
|
2069
|
+
});
|
|
2070
|
+
this.#operationHistory.record({
|
|
2071
|
+
summary: {
|
|
2072
|
+
recordId: preparationRecordId,
|
|
2073
|
+
epoch: this.#sessionEpoch,
|
|
2074
|
+
operationId: active.snapshot.id,
|
|
2075
|
+
createdAtMs: this.dependencies.now?.() ?? Date.now(),
|
|
2076
|
+
state: 'blocked',
|
|
2077
|
+
code: preparationDiagnostic.code,
|
|
2078
|
+
message: active.snapshot.message,
|
|
2079
|
+
operationKind: intent.kind,
|
|
2080
|
+
...('pluginId' in intent ? { pluginId: intent.pluginId } : {}),
|
|
2081
|
+
diagnosticCount: 1,
|
|
2082
|
+
effects: {
|
|
2083
|
+
configCommit: 'notApplicable', awWrite: 'notStarted',
|
|
2084
|
+
preApplyExternalEffect: 'notApplicable', finalVerification: 'notStarted',
|
|
2085
|
+
},
|
|
2086
|
+
},
|
|
2087
|
+
diagnostics: [preparationDiagnostic],
|
|
2088
|
+
targets: [],
|
|
2089
|
+
}, { sensitiveValues: collectOperationSensitiveValues(intent) });
|
|
1280
2090
|
this.#publish();
|
|
1281
2091
|
await this.#logOperationError(error, code, intent);
|
|
1282
2092
|
}
|
|
@@ -1300,6 +2110,7 @@ export class TuiController {
|
|
|
1300
2110
|
return;
|
|
1301
2111
|
this.#reviewPreparationToken += 1;
|
|
1302
2112
|
this.#reviewPreparation.abort.abort();
|
|
2113
|
+
this.#reviewPreparationIntents.delete(this.#reviewPreparation.snapshot.id);
|
|
1303
2114
|
this.#reviewPreparation = undefined;
|
|
1304
2115
|
}
|
|
1305
2116
|
async #logOperationError(error, code, intent) {
|
|
@@ -1322,6 +2133,9 @@ export class TuiController {
|
|
|
1322
2133
|
}
|
|
1323
2134
|
}
|
|
1324
2135
|
#storeOperationReview(intent, evidence, navigation = 'push', returnRoute = this.#router.snapshot.current.route) {
|
|
2136
|
+
this.#ensureOperationFlow(intent);
|
|
2137
|
+
if (intent.kind === 'pluginWriteBatch')
|
|
2138
|
+
assertPluginWriteBatchEvidence(evidence);
|
|
1325
2139
|
if (this.#router.snapshot.current.route.name === 'result')
|
|
1326
2140
|
navigation = 'replace';
|
|
1327
2141
|
const projection = buildOperationReviewProjection({
|
|
@@ -1338,7 +2152,10 @@ export class TuiController {
|
|
|
1338
2152
|
const expiresAt = now
|
|
1339
2153
|
+ (this.dependencies.reviewTtlMs ?? 2 * 60_000);
|
|
1340
2154
|
const pluginId = 'pluginId' in intent ? intent.pluginId
|
|
1341
|
-
: 'sourceId' in intent ? intent.sourceId
|
|
2155
|
+
: 'sourceId' in intent ? intent.sourceId
|
|
2156
|
+
: intent.kind === 'pluginWriteBatch'
|
|
2157
|
+
? `${evidence.requestedPluginIds.length} 个插件`
|
|
2158
|
+
: 'Catalog 来源配置';
|
|
1342
2159
|
if ('pluginId' in intent)
|
|
1343
2160
|
sourceIdFromPluginId(intent.pluginId);
|
|
1344
2161
|
const pluginVersion = 'pluginId' in intent
|
|
@@ -1354,6 +2171,7 @@ export class TuiController {
|
|
|
1354
2171
|
: intent.kind === 'catalogGeneration' && intent.sourceMutation
|
|
1355
2172
|
? { sourceAction: intent.sourceMutation.action } : {}),
|
|
1356
2173
|
...(intent.kind === 'pluginWrite' ? { pluginAction: intent.action } : {}),
|
|
2174
|
+
...(intent.kind === 'pluginWriteBatch' ? { pluginAction: intent.action } : {}),
|
|
1357
2175
|
...(intent.kind === 'pluginWrite' && intent.originIssue
|
|
1358
2176
|
? { originIssue: intent.originIssue }
|
|
1359
2177
|
: {}),
|
|
@@ -1366,6 +2184,14 @@ export class TuiController {
|
|
|
1366
2184
|
...('pluginId' in intent ? { sourceLabel: sourceIdFromPluginId(intent.pluginId) } : {}),
|
|
1367
2185
|
...(pluginVersion ? { version: pluginVersion } : {}),
|
|
1368
2186
|
},
|
|
2187
|
+
...(intent.kind === 'pluginWriteBatch' ? {
|
|
2188
|
+
batch: {
|
|
2189
|
+
requestedPluginIds: [...evidence.requestedPluginIds],
|
|
2190
|
+
groupCount: evidence.groups.length,
|
|
2191
|
+
executableGroupCount: evidence.groups
|
|
2192
|
+
.filter((group) => group.executable).length,
|
|
2193
|
+
},
|
|
2194
|
+
} : {}),
|
|
1369
2195
|
...(evidence.source && 'catalogVersion' in evidence.source
|
|
1370
2196
|
&& typeof evidence.source.catalogVersion === 'string'
|
|
1371
2197
|
&& evidence.source.snapshotDigest
|
|
@@ -1389,6 +2215,14 @@ export class TuiController {
|
|
|
1389
2215
|
return cloneFrozen(review);
|
|
1390
2216
|
}
|
|
1391
2217
|
executeOperation(reviewToken) {
|
|
2218
|
+
if (this.#retiring || this.#disposed) {
|
|
2219
|
+
return Promise.resolve(cloneFrozen({
|
|
2220
|
+
state: 'blocked',
|
|
2221
|
+
operationId: this.#tickets.get(reviewToken)?.review.operationId ?? 'unknown',
|
|
2222
|
+
code: 'sessionRetiring',
|
|
2223
|
+
message: '会话正在结束,未启动新操作。',
|
|
2224
|
+
}));
|
|
2225
|
+
}
|
|
1392
2226
|
const inflight = this.#inflightOperations.get(reviewToken);
|
|
1393
2227
|
if (inflight)
|
|
1394
2228
|
return inflight.promise;
|
|
@@ -1409,6 +2243,7 @@ export class TuiController {
|
|
|
1409
2243
|
const promise = this.#executeOperationTicket(ticket, operationId).finally(() => {
|
|
1410
2244
|
if (this.#inflightOperations.get(reviewToken)?.promise === promise) {
|
|
1411
2245
|
this.#inflightOperations.delete(reviewToken);
|
|
2246
|
+
this.#publish();
|
|
1412
2247
|
}
|
|
1413
2248
|
});
|
|
1414
2249
|
this.#inflightOperations.set(reviewToken, { operationId, promise });
|
|
@@ -1418,8 +2253,10 @@ export class TuiController {
|
|
|
1418
2253
|
let generation = this.#generation;
|
|
1419
2254
|
const startedAtMs = this.dependencies.now?.() ?? Date.now();
|
|
1420
2255
|
this.#progressFeed?.clearCurrent?.();
|
|
1421
|
-
const
|
|
1422
|
-
|
|
2256
|
+
const dependencyTargets = ticket.intent.kind === 'pluginWriteBatch'
|
|
2257
|
+
? ticket.evidence.groups.flatMap((group) => group.preview.dependencyPlan?.targets ?? [])
|
|
2258
|
+
: ticket.evidence.dependencyPlan?.targets ?? [];
|
|
2259
|
+
this.#progressTargets = dependencyTargets.map((target) => ({
|
|
1423
2260
|
pluginId: target.pluginId,
|
|
1424
2261
|
platform: target.platform,
|
|
1425
2262
|
scope: target.resolvedScope,
|
|
@@ -1428,21 +2265,23 @@ export class TuiController {
|
|
|
1428
2265
|
: target.depth === 1 ? 'direct' : 'transitive',
|
|
1429
2266
|
requiredBy: target.requiredBy,
|
|
1430
2267
|
state: target.action === 'keep' ? 'kept' : 'pending',
|
|
1431
|
-
}))
|
|
2268
|
+
}));
|
|
1432
2269
|
this.#progress = {
|
|
1433
2270
|
messages: [],
|
|
1434
2271
|
startedAtMs,
|
|
1435
2272
|
elapsedMs: 0,
|
|
1436
2273
|
current: {
|
|
1437
|
-
target:
|
|
1438
|
-
|
|
2274
|
+
target: ticket.intent.kind === 'pluginWriteBatch'
|
|
2275
|
+
? `${ticket.evidence.requestedPluginIds.length} 个插件`
|
|
2276
|
+
: 'pluginId' in ticket.intent ? ticket.intent.pluginId
|
|
2277
|
+
: 'sourceId' in ticket.intent ? ticket.intent.sourceId : 'Catalog 来源配置',
|
|
1439
2278
|
phase: '验证当前状态',
|
|
1440
2279
|
},
|
|
1441
2280
|
};
|
|
1442
2281
|
this.#router.replace({ name: 'progress', operationId });
|
|
1443
2282
|
this.#publish();
|
|
1444
2283
|
try {
|
|
1445
|
-
if (ticket.intent.kind !== 'pluginWrite') {
|
|
2284
|
+
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch') {
|
|
1446
2285
|
await this.dependencies.validateIntent?.(structuredClone(ticket.intent), this.#abort.signal);
|
|
1447
2286
|
}
|
|
1448
2287
|
}
|
|
@@ -1473,7 +2312,8 @@ export class TuiController {
|
|
|
1473
2312
|
}
|
|
1474
2313
|
if (!this.#operationIsCurrent(generation))
|
|
1475
2314
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1476
|
-
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== '
|
|
2315
|
+
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
|
|
2316
|
+
&& ticket.intent.kind !== 'catalogGeneration') {
|
|
1477
2317
|
let current;
|
|
1478
2318
|
try {
|
|
1479
2319
|
current = await this.dependencies.captureEvidence(structuredClone(ticket.intent), this.#abort.signal);
|
|
@@ -1501,6 +2341,11 @@ export class TuiController {
|
|
|
1501
2341
|
const execution = ticket.intent.kind === 'catalogGeneration'
|
|
1502
2342
|
? await this.dependencies.executeIntent(structuredClone(ticket.intent), structuredClone(ticket.evidence), this.#abort.signal)
|
|
1503
2343
|
: await this.dependencies.executeIntent(structuredClone(ticket.intent), structuredClone(ticket.evidence));
|
|
2344
|
+
const executionProjection = projectExecutionDiagnostics(execution, `${this.#sessionEpoch}:${operationId}`, collectOperationSensitiveValues(ticket.intent), ticket.intent.kind);
|
|
2345
|
+
this.#operationDiagnostics.set(operationId, {
|
|
2346
|
+
...executionProjection,
|
|
2347
|
+
sensitiveValues: collectOperationSensitiveValues(ticket.intent),
|
|
2348
|
+
});
|
|
1504
2349
|
if (ticket.intent.kind === 'sourceMutation' && ticket.intent.action === 'update') {
|
|
1505
2350
|
const confirmedAutoCheck = catalogSourceAutoCheckOnlyValue(ticket.intent.update);
|
|
1506
2351
|
if (confirmedAutoCheck !== undefined) {
|
|
@@ -1541,8 +2386,49 @@ export class TuiController {
|
|
|
1541
2386
|
await this.#logErrorSafe(error, verificationCode, {
|
|
1542
2387
|
privateConfigOperation: true,
|
|
1543
2388
|
});
|
|
2389
|
+
const existingProjection = this.#operationDiagnostics.get(operationId);
|
|
2390
|
+
existingProjection.diagnostics.push(projectThrownOperationDiagnostic({
|
|
2391
|
+
error,
|
|
2392
|
+
id: `${this.#sessionEpoch}:${operationId}:status-reload`,
|
|
2393
|
+
stage: 'statusReload',
|
|
2394
|
+
fallbackCode: verificationCode,
|
|
2395
|
+
}));
|
|
2396
|
+
existingProjection.effects.finalVerification = 'failed';
|
|
1544
2397
|
if (!this.#operationIsCurrent(generation))
|
|
1545
2398
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2399
|
+
const configCommitted = typeof execution === 'object'
|
|
2400
|
+
&& execution !== null
|
|
2401
|
+
&& execution.configWrite === 'succeeded';
|
|
2402
|
+
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
|
|
2403
|
+
&& configCommitted) {
|
|
2404
|
+
const baseSummary = operationResultSummary(ticket.intent, execution);
|
|
2405
|
+
const elapsedMs = Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs);
|
|
2406
|
+
const summary = {
|
|
2407
|
+
...baseSummary,
|
|
2408
|
+
code: 'operationPartial',
|
|
2409
|
+
message: '设置已保存,但最终状态无法刷新。',
|
|
2410
|
+
elapsedMs,
|
|
2411
|
+
messages: [
|
|
2412
|
+
...this.#progress.messages,
|
|
2413
|
+
...(baseSummary.messages ?? []),
|
|
2414
|
+
'状态刷新失败。配置写入已经提交;请稍后刷新状态。',
|
|
2415
|
+
].slice(-8).map((message) => sanitizeDiagnosticText(message, [], collectOperationSensitiveValues(ticket.intent))),
|
|
2416
|
+
};
|
|
2417
|
+
this.#progress = { ...this.#progress, elapsedMs };
|
|
2418
|
+
const result = {
|
|
2419
|
+
state: 'partial',
|
|
2420
|
+
operationId,
|
|
2421
|
+
summary,
|
|
2422
|
+
operationKind: ticket.intent.kind,
|
|
2423
|
+
pluginId: ticket.intent.pluginId,
|
|
2424
|
+
returnRoute: structuredClone(this.#normalizeRoute(ticket.returnRoute)),
|
|
2425
|
+
};
|
|
2426
|
+
this.#result = structuredClone(result);
|
|
2427
|
+
this.#activeReview = undefined;
|
|
2428
|
+
this.#showResult(result, ticket.intent);
|
|
2429
|
+
this.#publish();
|
|
2430
|
+
return cloneFrozen(result);
|
|
2431
|
+
}
|
|
1546
2432
|
return this.#setBlocked(operationId, verificationCode, {
|
|
1547
2433
|
intent: ticket.intent,
|
|
1548
2434
|
navigation: 'result',
|
|
@@ -1555,11 +2441,16 @@ export class TuiController {
|
|
|
1555
2441
|
if (!this.#operationIsCurrent(generation))
|
|
1556
2442
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1557
2443
|
this.#applyStatus(refreshed);
|
|
2444
|
+
const verifiedProjection = this.#operationDiagnostics.get(operationId);
|
|
2445
|
+
if (verifiedProjection && verifiedProjection.effects.finalVerification !== 'failed') {
|
|
2446
|
+
verifiedProjection.effects.finalVerification = 'completed';
|
|
2447
|
+
}
|
|
1558
2448
|
if (ticket.intent.kind === 'catalogSourcesRecovery') {
|
|
1559
2449
|
this.#phase = 'ready';
|
|
1560
2450
|
this.#loadError = undefined;
|
|
1561
2451
|
}
|
|
1562
2452
|
const pluginSummary = ticket.intent.kind === 'pluginWrite'
|
|
2453
|
+
|| ticket.intent.kind === 'pluginWriteBatch'
|
|
1563
2454
|
? operationResultSummary(ticket.intent, execution)
|
|
1564
2455
|
: undefined;
|
|
1565
2456
|
const hasFailedDependencyTarget = pluginSummary?.targets?.some((target) => target.relation !== undefined && target.relation !== 'requested'
|
|
@@ -1570,6 +2461,18 @@ export class TuiController {
|
|
|
1570
2461
|
const dependencyFailure = pluginSummary?.code === 'operationFailed'
|
|
1571
2462
|
&& Boolean(hasFailedDependencyTarget);
|
|
1572
2463
|
if (pluginSummary?.code === 'operationFailed') {
|
|
2464
|
+
const projected = this.#operationDiagnostics.get(operationId);
|
|
2465
|
+
if (projected) {
|
|
2466
|
+
projected.targets = (pluginSummary.targets ?? []).map((target) => ({
|
|
2467
|
+
...target,
|
|
2468
|
+
diagnosticIds: projected.diagnostics.filter((diagnostic) => diagnostic.target
|
|
2469
|
+
&& diagnostic.target.platform === target.platform
|
|
2470
|
+
&& diagnostic.target.scope === target.scope
|
|
2471
|
+
&& (diagnostic.target.pluginId === undefined || diagnostic.target.pluginId === target.pluginId))
|
|
2472
|
+
.map((diagnostic) => diagnostic.id),
|
|
2473
|
+
effects: projected.targetEffects?.get(`${target.pluginId ?? ''}\0${target.platform}\0${target.scope}`),
|
|
2474
|
+
}));
|
|
2475
|
+
}
|
|
1573
2476
|
const unsuccessfulTargets = pluginSummary.targets?.filter((target) => target.state === 'failed' || target.state === 'skipped') ?? [];
|
|
1574
2477
|
const managerFailureCode = unsuccessfulTargets.map((target) => target.errorCode).find((code) => code === 'codexSystemCliUnsupported'
|
|
1575
2478
|
|| code === 'managedCodexAuthorizationRequired'
|
|
@@ -1588,8 +2491,10 @@ export class TuiController {
|
|
|
1588
2491
|
intent: ticket.intent,
|
|
1589
2492
|
navigation: 'result',
|
|
1590
2493
|
returnRoute: ticket.returnRoute,
|
|
1591
|
-
...(
|
|
1592
|
-
|
|
2494
|
+
...(ticket.intent.kind === 'pluginWriteBatch' || dependencyFailure || dependencyStillRequired
|
|
2495
|
+
? { summary: pluginSummary } : {}),
|
|
2496
|
+
...(ticket.intent.kind !== 'pluginWriteBatch'
|
|
2497
|
+
&& !dependencyFailure && !dependencyStillRequired && ['started', 'partial', 'completed', 'failed', 'unknown'].includes(this.#operationDiagnostics.get(operationId)?.effects.awWrite ?? 'unknown') ? { writeMayHaveCompleted: true } : {}),
|
|
1593
2498
|
});
|
|
1594
2499
|
}
|
|
1595
2500
|
const originIssue = ticket.intent.kind === 'pluginWrite' ? ticket.intent.originIssue : undefined;
|
|
@@ -1620,7 +2525,8 @@ export class TuiController {
|
|
|
1620
2525
|
const summary = {
|
|
1621
2526
|
...baseSummary,
|
|
1622
2527
|
elapsedMs: Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs),
|
|
1623
|
-
messages: [...this.#progress.messages, ...(baseSummary.messages ?? [])].slice(-8)
|
|
2528
|
+
messages: [...this.#progress.messages, ...(baseSummary.messages ?? [])].slice(-8)
|
|
2529
|
+
.map((message) => sanitizeDiagnosticText(message, [], collectOperationSensitiveValues(ticket.intent))),
|
|
1624
2530
|
};
|
|
1625
2531
|
this.#progress = { ...this.#progress, elapsedMs: summary.elapsedMs };
|
|
1626
2532
|
if (summary.code === 'operationFailed') {
|
|
@@ -1635,7 +2541,7 @@ export class TuiController {
|
|
|
1635
2541
|
};
|
|
1636
2542
|
this.#result = structuredClone(result);
|
|
1637
2543
|
this.#activeReview = undefined;
|
|
1638
|
-
this.#
|
|
2544
|
+
this.#showResult(result, ticket.intent);
|
|
1639
2545
|
this.#publish();
|
|
1640
2546
|
return cloneFrozen(result);
|
|
1641
2547
|
}
|
|
@@ -1666,6 +2572,7 @@ export class TuiController {
|
|
|
1666
2572
|
};
|
|
1667
2573
|
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
|
|
1668
2574
|
&& this.#portableRepreparePluginId === ticket.intent.pluginId) {
|
|
2575
|
+
this.#operationDiagnostics.delete(operationId);
|
|
1669
2576
|
this.#portableRepreparePluginId = undefined;
|
|
1670
2577
|
this.#activeReview = undefined;
|
|
1671
2578
|
this.#result = undefined;
|
|
@@ -1674,7 +2581,7 @@ export class TuiController {
|
|
|
1674
2581
|
}
|
|
1675
2582
|
this.#result = result;
|
|
1676
2583
|
this.#activeReview = undefined;
|
|
1677
|
-
this.#
|
|
2584
|
+
this.#showResult(result, ticket.intent);
|
|
1678
2585
|
this.#publish();
|
|
1679
2586
|
return cloneFrozen(result);
|
|
1680
2587
|
}
|
|
@@ -1682,44 +2589,106 @@ export class TuiController {
|
|
|
1682
2589
|
if (!this.#operationIsCurrent(generation))
|
|
1683
2590
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1684
2591
|
const code = issueCode(error);
|
|
2592
|
+
const metadata = readSafeOperationError(error);
|
|
2593
|
+
const preApply = metadata?.stage === 'preApply';
|
|
2594
|
+
const configWrite = error && typeof error === 'object' && 'configWrite' in error
|
|
2595
|
+
? error.configWrite : undefined;
|
|
2596
|
+
this.#captureThrownProjection(operationId, error, code, 'execute', ticket.intent, {
|
|
2597
|
+
configCommit: ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset'
|
|
2598
|
+
? configWrite === 'succeeded' ? 'completed'
|
|
2599
|
+
: configWrite === 'failed' ? 'failed' : 'notStarted' : 'notApplicable',
|
|
2600
|
+
awWrite: preApply ? 'notStarted' : 'unknown',
|
|
2601
|
+
preApplyExternalEffect: preApply ? 'possible' : 'notApplicable',
|
|
2602
|
+
finalVerification: 'notStarted',
|
|
2603
|
+
});
|
|
1685
2604
|
await this.#logOperationError(error, code, ticket.intent);
|
|
1686
2605
|
if (!this.#operationIsCurrent(generation))
|
|
1687
2606
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1688
2607
|
if (code === 'operationReviewStale' || code === 'catalogGenerationReviewStale') {
|
|
2608
|
+
this.#operationDiagnostics.delete(operationId);
|
|
1689
2609
|
return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
|
|
1690
2610
|
}
|
|
1691
2611
|
generation = ++this.#generation;
|
|
2612
|
+
let finalVerification = 'completed';
|
|
1692
2613
|
try {
|
|
1693
2614
|
const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
|
|
1694
2615
|
if (!this.#operationIsCurrent(generation))
|
|
1695
2616
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
1696
2617
|
this.#applyStatus(refreshed);
|
|
2618
|
+
const projection = this.#operationDiagnostics.get(operationId);
|
|
2619
|
+
if (projection)
|
|
2620
|
+
projection.effects.finalVerification = 'completed';
|
|
1697
2621
|
if (ticket.intent.kind === 'catalogSourcesRecovery'
|
|
1698
2622
|
&& code === 'catalogSourcesConfigRecoveryStale') {
|
|
1699
2623
|
this.#phase = 'ready';
|
|
1700
2624
|
this.#loadError = undefined;
|
|
1701
|
-
this.#
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
state: 'blocked',
|
|
1707
|
-
operationId,
|
|
1708
|
-
code,
|
|
1709
|
-
message: '来源配置已变化,已重新加载当前状态。',
|
|
1710
|
-
operationKind: ticket.intent.kind,
|
|
2625
|
+
return this.#setBlocked(operationId, code, {
|
|
2626
|
+
intent: ticket.intent,
|
|
2627
|
+
navigation: 'result',
|
|
2628
|
+
returnRoute: ticket.returnRoute,
|
|
2629
|
+
writeMayHaveCompleted: false,
|
|
1711
2630
|
});
|
|
1712
2631
|
}
|
|
1713
2632
|
}
|
|
1714
2633
|
catch (reloadError) {
|
|
1715
2634
|
if (!this.#operationIsCurrent(generation))
|
|
1716
2635
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2636
|
+
finalVerification = 'failed';
|
|
2637
|
+
const projection = this.#operationDiagnostics.get(operationId);
|
|
2638
|
+
if (projection)
|
|
2639
|
+
projection.effects.finalVerification = 'failed';
|
|
1717
2640
|
await this.#logErrorSafe(reloadError, 'tuiStateReloadFailed', {
|
|
1718
2641
|
privateConfigOperation: true,
|
|
1719
2642
|
});
|
|
2643
|
+
if (ticket.intent.kind === 'catalogSourcesRecovery'
|
|
2644
|
+
&& code === 'catalogSourcesConfigRecoveryStale') {
|
|
2645
|
+
this.#phase = 'error';
|
|
2646
|
+
this.#loadError = {
|
|
2647
|
+
code: issueCode(reloadError),
|
|
2648
|
+
message: '无法安全读取插件状态。请检查诊断后重试或退出。',
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
1720
2651
|
}
|
|
1721
2652
|
if (!this.#operationIsCurrent(generation))
|
|
1722
2653
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2654
|
+
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
|
|
2655
|
+
&& configWrite === 'succeeded') {
|
|
2656
|
+
const elapsedMs = Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs);
|
|
2657
|
+
const summary = {
|
|
2658
|
+
code: 'operationPartial',
|
|
2659
|
+
message: ticket.intent.kind === 'configReset'
|
|
2660
|
+
? '设置层重置已提交,但后续步骤未完成。'
|
|
2661
|
+
: finalVerification === 'failed'
|
|
2662
|
+
? ticket.intent.apply
|
|
2663
|
+
? '设置已保存,但后续应用或状态复核失败。'
|
|
2664
|
+
: '设置已保存,但状态复核失败。'
|
|
2665
|
+
: ticket.intent.apply
|
|
2666
|
+
? '设置已保存,但后续应用失败。'
|
|
2667
|
+
: '设置已保存,但后续步骤未完成。',
|
|
2668
|
+
pluginId: ticket.intent.pluginId,
|
|
2669
|
+
configScope: ticket.intent.scope,
|
|
2670
|
+
...(ticket.intent.kind === 'configSave' && ticket.intent.apply
|
|
2671
|
+
? { configApplyRetryAvailable: true } : {}),
|
|
2672
|
+
elapsedMs,
|
|
2673
|
+
messages: [ticket.intent.kind === 'configReset'
|
|
2674
|
+
? '设置层重置已经提交;失败的后续步骤没有恢复旧设置。'
|
|
2675
|
+
: '配置写入已经提交;失败的后续步骤没有回滚设置。'],
|
|
2676
|
+
};
|
|
2677
|
+
this.#progress = { ...this.#progress, elapsedMs };
|
|
2678
|
+
const result = {
|
|
2679
|
+
state: 'partial',
|
|
2680
|
+
operationId,
|
|
2681
|
+
summary,
|
|
2682
|
+
operationKind: ticket.intent.kind,
|
|
2683
|
+
pluginId: ticket.intent.pluginId,
|
|
2684
|
+
returnRoute: structuredClone(this.#normalizeRoute(ticket.returnRoute)),
|
|
2685
|
+
};
|
|
2686
|
+
this.#result = structuredClone(result);
|
|
2687
|
+
this.#activeReview = undefined;
|
|
2688
|
+
this.#showResult(result, ticket.intent);
|
|
2689
|
+
this.#publish();
|
|
2690
|
+
return cloneFrozen(result);
|
|
2691
|
+
}
|
|
1723
2692
|
return this.#setBlocked(operationId, code, {
|
|
1724
2693
|
intent: ticket.intent,
|
|
1725
2694
|
navigation: 'result',
|
|
@@ -1732,6 +2701,7 @@ export class TuiController {
|
|
|
1732
2701
|
return !this.#disposed && !this.#abort.signal.aborted && generation === this.#generation;
|
|
1733
2702
|
}
|
|
1734
2703
|
#detachedBlocked(operationId, intent) {
|
|
2704
|
+
this.#operationDiagnostics.delete(operationId);
|
|
1735
2705
|
return cloneFrozen({
|
|
1736
2706
|
state: 'blocked',
|
|
1737
2707
|
operationId,
|
|
@@ -1780,7 +2750,7 @@ export class TuiController {
|
|
|
1780
2750
|
};
|
|
1781
2751
|
this.#result = structuredClone(result);
|
|
1782
2752
|
this.#activeReview = undefined;
|
|
1783
|
-
this.#
|
|
2753
|
+
this.#showResult(result, ticket.intent);
|
|
1784
2754
|
this.#publish();
|
|
1785
2755
|
return cloneFrozen(result);
|
|
1786
2756
|
}
|
|
@@ -1824,6 +2794,9 @@ export class TuiController {
|
|
|
1824
2794
|
return cloneFrozen(result);
|
|
1825
2795
|
}
|
|
1826
2796
|
#setBlocked(operationId, code, options) {
|
|
2797
|
+
if (!this.#operationDiagnostics.has(operationId)) {
|
|
2798
|
+
this.#captureThrownProjection(operationId, undefined, code, 'execute', options?.intent);
|
|
2799
|
+
}
|
|
1827
2800
|
const message = code === 'managedCodexAuthorizationRequired'
|
|
1828
2801
|
? '操作在插件写入前停止。请在 Review 中确认获取 Codex runtime,或在命令行传入 --allow-managed-codex-install。'
|
|
1829
2802
|
: code === 'codexSystemCliUnsupported'
|
|
@@ -1856,6 +2829,8 @@ export class TuiController {
|
|
|
1856
2829
|
this.#router.back();
|
|
1857
2830
|
this.#normalizeCurrentRoute();
|
|
1858
2831
|
}
|
|
2832
|
+
else if (options?.intent)
|
|
2833
|
+
this.#showResult(result, options.intent);
|
|
1859
2834
|
else
|
|
1860
2835
|
this.#router.replace({ name: 'result', operationId });
|
|
1861
2836
|
this.#publish();
|
|
@@ -1886,7 +2861,7 @@ export class TuiController {
|
|
|
1886
2861
|
this.#listeners.clear();
|
|
1887
2862
|
}
|
|
1888
2863
|
}
|
|
1889
|
-
function
|
|
2864
|
+
function operationResultSummaryUnsafe(intent, execution) {
|
|
1890
2865
|
if (intent.kind === 'catalogSourcesRecovery') {
|
|
1891
2866
|
const result = execution;
|
|
1892
2867
|
return {
|
|
@@ -1918,14 +2893,14 @@ function operationResultSummary(intent, execution) {
|
|
|
1918
2893
|
: '来源配置已保存,但 Catalog 尚未启用。可在 TUI 中立即重试。'
|
|
1919
2894
|
: intent.action === 'repair' ? 'Catalog 已重新下载并修复。'
|
|
1920
2895
|
: intent.action === 'replaceDamaged' ? '损坏的 Active Catalog 已由当前候选替换。'
|
|
1921
|
-
: intent.action === 'recoverState' ? '
|
|
2896
|
+
: intent.action === 'recoverState' ? '目录版本状态已恢复,目标来源已启用。'
|
|
1922
2897
|
: updateActivated ? `Catalog 已更新到 ${generation.resolution.catalogVersion}。`
|
|
1923
2898
|
: intent.action === 'update' ? 'Catalog 已验证,Active 未变化。'
|
|
1924
2899
|
: 'Catalog 已验证并启用。',
|
|
1925
2900
|
messages: result.status === 'partial'
|
|
1926
2901
|
? [result.partialReason === 'primary'
|
|
1927
|
-
?
|
|
1928
|
-
:
|
|
2902
|
+
? `新的已启用版本可用;主来源保持不变。错误:${result.errorCode ?? 'catalogSourcePrimaryFailed'}`
|
|
2903
|
+
: `旧的已启用版本保持不变。错误:${result.errorCode ?? 'catalogGenerationRefreshFailed'}`]
|
|
1929
2904
|
: updateActivated
|
|
1930
2905
|
? ['已安装插件不会自动升级。', '已重新计算可用的插件升级。']
|
|
1931
2906
|
: intent.action === 'update'
|
|
@@ -1963,11 +2938,15 @@ function operationResultSummary(intent, execution) {
|
|
|
1963
2938
|
},
|
|
1964
2939
|
};
|
|
1965
2940
|
}
|
|
1966
|
-
if (intent.kind === 'pluginWrite') {
|
|
2941
|
+
if (intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch') {
|
|
1967
2942
|
const command = intent.action === 'refreshAndSync' ? 'sync' : intent.action;
|
|
1968
2943
|
const rawExecution = typeof execution === 'object' && execution !== null
|
|
1969
2944
|
? execution
|
|
1970
2945
|
: {};
|
|
2946
|
+
const requestedPluginIds = intent.kind === 'pluginWriteBatch'
|
|
2947
|
+
? canonicalPluginIds(execution.requestedPluginIds ?? [])
|
|
2948
|
+
: [intent.pluginId];
|
|
2949
|
+
const fallbackPluginId = requestedPluginIds[0] ?? 'unknown/plugin';
|
|
1971
2950
|
const normalizedTargetResults = Array.isArray(rawExecution.targetResults)
|
|
1972
2951
|
? rawExecution.targetResults.map((candidate) => {
|
|
1973
2952
|
if (typeof candidate !== 'object' || candidate === null)
|
|
@@ -1984,7 +2963,7 @@ function operationResultSummary(intent, execution) {
|
|
|
1984
2963
|
...rawTarget,
|
|
1985
2964
|
...(pluginId ? { pluginId } : {}),
|
|
1986
2965
|
pluginName: typeof rawTarget.pluginName === 'string'
|
|
1987
|
-
? rawTarget.pluginName : pluginNameFromId(pluginId ??
|
|
2966
|
+
? rawTarget.pluginName : pluginNameFromId(pluginId ?? fallbackPluginId),
|
|
1988
2967
|
},
|
|
1989
2968
|
action: typeof entry.action === 'string'
|
|
1990
2969
|
? entry.action : requestedIntentAction(command),
|
|
@@ -1995,17 +2974,21 @@ function operationResultSummary(intent, execution) {
|
|
|
1995
2974
|
...rawExecution,
|
|
1996
2975
|
...(normalizedTargetResults ? { targetResults: normalizedTargetResults } : {}),
|
|
1997
2976
|
platforms: Array.isArray(rawExecution.platforms) ? rawExecution.platforms : [],
|
|
1998
|
-
plugins: Array.isArray(rawExecution.plugins) ? rawExecution.plugins :
|
|
2977
|
+
plugins: Array.isArray(rawExecution.plugins) ? rawExecution.plugins : requestedPluginIds,
|
|
1999
2978
|
warnings: Array.isArray(rawExecution.warnings) ? rawExecution.warnings : [],
|
|
2000
2979
|
});
|
|
2001
2980
|
const result = buildOperationResultModel(receipt);
|
|
2002
|
-
const requestedIds = new Set(receipt.requestedPluginIds ??
|
|
2981
|
+
const requestedIds = new Set(receipt.requestedPluginIds ?? requestedPluginIds);
|
|
2982
|
+
const retryTargets = intent.kind === 'pluginWriteBatch'
|
|
2983
|
+
? pluginWriteBatchFailedRequestedTargets(execution)
|
|
2984
|
+
: [];
|
|
2985
|
+
const failedRequestedPluginIds = canonicalPluginIds(retryTargets.map((target) => target.pluginId));
|
|
2003
2986
|
return {
|
|
2004
2987
|
code: result.state === 'succeeded' ? 'operationSucceeded'
|
|
2005
2988
|
: result.state === 'partial' ? 'operationPartial' : 'operationFailed',
|
|
2006
2989
|
message: result.state === 'succeeded' ? '操作已完成。'
|
|
2007
2990
|
: result.state === 'partial' ? '操作只完成了一部分。' : '操作结果无法确认。',
|
|
2008
|
-
pluginId: intent.pluginId,
|
|
2991
|
+
...(intent.kind === 'pluginWrite' ? { pluginId: intent.pluginId } : {}),
|
|
2009
2992
|
action: intent.action,
|
|
2010
2993
|
changes: result.changes,
|
|
2011
2994
|
warnings: result.warnings,
|
|
@@ -2014,10 +2997,18 @@ function operationResultSummary(intent, execution) {
|
|
|
2014
2997
|
messages: [
|
|
2015
2998
|
...result.warnings.map((warning) => `警告:${warning}`),
|
|
2016
2999
|
...result.recoveryActions.map((action) => `恢复:${action}`),
|
|
2017
|
-
...(intent.recoveryDescription
|
|
3000
|
+
...(intent.kind === 'pluginWrite' && intent.recoveryDescription
|
|
2018
3001
|
? [intent.recoveryDescription, `后续:${intent.scope === 'user' ? '当前项目' : '所有项目'} sync`]
|
|
2019
3002
|
: []),
|
|
2020
3003
|
],
|
|
3004
|
+
...(intent.kind === 'pluginWriteBatch' ? {
|
|
3005
|
+
batch: {
|
|
3006
|
+
requestedPluginIds,
|
|
3007
|
+
failedRequestedPluginIds,
|
|
3008
|
+
retryTargets,
|
|
3009
|
+
groupCount: execution.groupResults.length,
|
|
3010
|
+
},
|
|
3011
|
+
} : {}),
|
|
2021
3012
|
targets: result.targetResults.map((entry) => ({
|
|
2022
3013
|
pluginId: entry.target.pluginId,
|
|
2023
3014
|
platform: entry.target.platform,
|
|
@@ -2029,19 +3020,18 @@ function operationResultSummary(intent, execution) {
|
|
|
2029
3020
|
relation: entry.relation === 'requested' ? 'requested'
|
|
2030
3021
|
: entry.requiredBy.some((pluginId) => requestedIds.has(pluginId)) ? 'direct' : 'transitive',
|
|
2031
3022
|
requiredBy: entry.requiredBy,
|
|
2032
|
-
...(entry.errorCode
|
|
2033
|
-
|| entry.errorCode === 'codexSystemCliUnsupported'
|
|
2034
|
-
|| entry.errorCode === 'managedCodexAuthorizationRequired'
|
|
2035
|
-
|| entry.errorCode === 'managedCodexUnavailable'
|
|
2036
|
-
? { errorCode: entry.errorCode }
|
|
2037
|
-
: {}),
|
|
3023
|
+
...(entry.errorCode ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') } : {}),
|
|
2038
3024
|
})),
|
|
2039
3025
|
};
|
|
2040
3026
|
}
|
|
2041
3027
|
if (intent.kind === 'configSave' || intent.kind === 'configReset') {
|
|
2042
|
-
const
|
|
2043
|
-
&&
|
|
2044
|
-
|
|
3028
|
+
const rawApply = typeof execution === 'object' && execution !== null
|
|
3029
|
+
&& typeof execution.apply === 'object'
|
|
3030
|
+
&& execution.apply !== null
|
|
3031
|
+
? execution.apply
|
|
3032
|
+
: {};
|
|
3033
|
+
const rawTargets = Array.isArray(rawApply.targetResults)
|
|
3034
|
+
? rawApply.targetResults
|
|
2045
3035
|
: [];
|
|
2046
3036
|
const targets = rawTargets.flatMap((entry) => {
|
|
2047
3037
|
const target = typeof entry.target === 'object' && entry.target !== null
|
|
@@ -2049,32 +3039,50 @@ function operationResultSummary(intent, execution) {
|
|
|
2049
3039
|
: entry;
|
|
2050
3040
|
if ((target.platform !== 'codex' && target.platform !== 'claude')
|
|
2051
3041
|
|| (target.scope !== 'user' && target.scope !== 'project')
|
|
2052
|
-
|| (entry.state !== 'completed' && entry.state !== 'failed'))
|
|
3042
|
+
|| (entry.state !== 'completed' && entry.state !== 'failed' && entry.state !== 'skipped'))
|
|
2053
3043
|
return [];
|
|
2054
3044
|
return [{
|
|
2055
3045
|
...(typeof target.pluginId === 'string' ? { pluginId: target.pluginId } : {}),
|
|
2056
3046
|
platform: target.platform,
|
|
2057
3047
|
scope: target.scope,
|
|
2058
|
-
state: entry.action === 'skipped' ? 'skipped'
|
|
3048
|
+
state: entry.state === 'skipped' || entry.action === 'skipped' ? 'skipped'
|
|
2059
3049
|
: entry.state === 'failed' ? 'failed'
|
|
2060
3050
|
: entry.action === 'keep' ? 'kept'
|
|
2061
3051
|
: entry.state,
|
|
3052
|
+
...(typeof entry.errorCode === 'string'
|
|
3053
|
+
? { errorCode: safeDiagnosticCode(entry.errorCode, 'configApplyFailed') }
|
|
3054
|
+
: {}),
|
|
2062
3055
|
}];
|
|
2063
3056
|
});
|
|
2064
3057
|
const overallState = typeof execution === 'object' && execution !== null
|
|
2065
3058
|
? execution.overallState
|
|
2066
3059
|
: undefined;
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
const invalidConfig = applySkippedReason === 'invalidConfig';
|
|
3060
|
+
const applyState = rawApply.state;
|
|
3061
|
+
const applyReason = rawApply.reason;
|
|
3062
|
+
const invalidConfig = applyState === 'unavailable' && applyReason === 'invalidConfig';
|
|
2071
3063
|
const resetDisposition = typeof execution === 'object' && execution !== null
|
|
2072
3064
|
? execution.resetDisposition
|
|
2073
3065
|
: undefined;
|
|
2074
3066
|
const quarantineBasename = typeof execution === 'object' && execution !== null
|
|
2075
3067
|
? execution.quarantineBasename
|
|
2076
3068
|
: undefined;
|
|
3069
|
+
const publicWarnings = typeof execution === 'object' && execution !== null
|
|
3070
|
+
&& Array.isArray(execution.warnings)
|
|
3071
|
+
? execution.warnings.filter((warning) => typeof warning === 'string'
|
|
3072
|
+
&& warning.startsWith('projectConfigShareabilityUnverified:'))
|
|
3073
|
+
: [];
|
|
3074
|
+
const otherProjectsMayRequireSync = intent.scope === 'user'
|
|
3075
|
+
&& typeof execution === 'object' && execution !== null
|
|
3076
|
+
&& execution.otherProjectsMayRequireSync === true;
|
|
3077
|
+
const publicMessages = [
|
|
3078
|
+
...publicWarnings.map((warning) => `警告:${warning}`),
|
|
3079
|
+
...(otherProjectsMayRequireSync
|
|
3080
|
+
? ['提示:其它项目没有被扫描,可能需要在对应项目目录中运行 aw sync。']
|
|
3081
|
+
: []),
|
|
3082
|
+
];
|
|
2077
3083
|
const partial = overallState === 'partial'
|
|
3084
|
+
|| applyState === 'partial'
|
|
3085
|
+
|| applyState === 'unavailable'
|
|
2078
3086
|
|| invalidConfig
|
|
2079
3087
|
|| targets.some((target) => target.state === 'failed' || target.state === 'skipped')
|
|
2080
3088
|
&& targets.some((target) => target.state === 'completed' || target.state === 'kept');
|
|
@@ -2088,9 +3096,10 @@ function operationResultSummary(intent, execution) {
|
|
|
2088
3096
|
pluginId: intent.pluginId,
|
|
2089
3097
|
configScope: intent.scope,
|
|
2090
3098
|
configApplyRetryAvailable: intent.kind === 'configSave'
|
|
2091
|
-
&& intent.apply && !invalidConfig && (
|
|
3099
|
+
&& intent.apply && !invalidConfig && (applyState === 'partial' || applyState === 'unavailable'),
|
|
3100
|
+
...(publicMessages.length > 0 ? { messages: publicMessages } : {}),
|
|
2092
3101
|
...(intent.kind === 'configSave'
|
|
2093
|
-
? { configApplied:
|
|
3102
|
+
? { configApplied: applyState === 'succeeded' || applyState === 'partial' }
|
|
2094
3103
|
: invalidConfig ? { configApplied: false } : {}),
|
|
2095
3104
|
...(invalidConfig ? { configApplySkippedReason: 'invalidConfig' } : {}),
|
|
2096
3105
|
...(intent.kind === 'configReset'
|
|
@@ -2117,4 +3126,7 @@ function operationResultSummary(intent, execution) {
|
|
|
2117
3126
|
...(sourceWarnings.length > 0 ? { messages: sourceWarnings } : {}),
|
|
2118
3127
|
};
|
|
2119
3128
|
}
|
|
3129
|
+
function operationResultSummary(intent, execution) {
|
|
3130
|
+
return sanitizeOperationSummary(operationResultSummaryUnsafe(intent, execution), collectOperationSensitiveValues(intent));
|
|
3131
|
+
}
|
|
2120
3132
|
//# sourceMappingURL=controller.js.map
|