@lesliechan721/aw 0.1.0-beta.13 → 0.1.0-beta.15
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/src/commands/catalog-dev-source.d.ts +31 -0
- package/dist/src/commands/catalog-dev-source.js +165 -5
- package/dist/src/commands/catalog-dev-source.js.map +1 -1
- package/dist/src/commands/catalog-generation.d.ts +5 -0
- package/dist/src/commands/catalog-generation.js.map +1 -1
- package/dist/src/commands/catalog-publication-journal.d.ts +12 -0
- package/dist/src/commands/catalog-publication-journal.js +77 -0
- package/dist/src/commands/catalog-publication-journal.js.map +1 -1
- package/dist/src/commands/catalog-store.d.ts +5 -0
- package/dist/src/commands/catalog-store.js +19 -1
- package/dist/src/commands/catalog-store.js.map +1 -1
- package/dist/src/commands/dependency-operation-plan.d.ts +7 -0
- package/dist/src/commands/dependency-operation-plan.js +63 -27
- package/dist/src/commands/dependency-operation-plan.js.map +1 -1
- package/dist/src/commands/rebuild-command.d.ts +22 -8
- package/dist/src/commands/rebuild-command.js +562 -250
- package/dist/src/commands/rebuild-command.js.map +1 -1
- package/dist/src/commands/reconcile-operation-plan.d.ts +11 -9
- package/dist/src/commands/reconcile-operation-plan.js +25 -6
- package/dist/src/commands/reconcile-operation-plan.js.map +1 -1
- package/dist/src/commands/status-v6.d.ts +2 -1
- package/dist/src/commands/status-v6.js +7 -1
- package/dist/src/commands/status-v6.js.map +1 -1
- package/dist/src/commands/write-commands.d.ts +2 -1
- package/dist/src/commands/write-commands.js +24 -1
- package/dist/src/commands/write-commands.js.map +1 -1
- package/dist/src/tui/catalog-management-screens.d.ts +45 -0
- package/dist/src/tui/catalog-management-screens.js +25 -0
- package/dist/src/tui/catalog-management-screens.js.map +1 -0
- package/dist/src/tui/catalog-management.d.ts +240 -0
- package/dist/src/tui/catalog-management.js +856 -0
- package/dist/src/tui/catalog-management.js.map +1 -0
- package/dist/src/tui/catalog-sources/session.d.ts +5 -0
- package/dist/src/tui/catalog-sources/session.js +49 -1
- package/dist/src/tui/catalog-sources/session.js.map +1 -1
- package/dist/src/tui/catalog-sources/types.d.ts +11 -0
- package/dist/src/tui/console-app-v4.d.ts +1 -1
- package/dist/src/tui/console-app-v4.js +2597 -587
- package/dist/src/tui/console-app-v4.js.map +1 -1
- package/dist/src/tui/controller-runtime.js +668 -50
- package/dist/src/tui/controller-runtime.js.map +1 -1
- package/dist/src/tui/controller.d.ts +136 -7
- package/dist/src/tui/controller.js +752 -19
- package/dist/src/tui/controller.js.map +1 -1
- package/dist/src/tui/dependency-graph.d.ts +67 -0
- package/dist/src/tui/dependency-graph.js +399 -0
- package/dist/src/tui/dependency-graph.js.map +1 -0
- package/dist/src/tui/lifecycle-action-view.js +4 -2
- package/dist/src/tui/lifecycle-action-view.js.map +1 -1
- package/dist/src/tui/model.d.ts +37 -0
- package/dist/src/tui/model.js +163 -7
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/operation-review-planner.js +266 -9
- package/dist/src/tui/operation-review-planner.js.map +1 -1
- package/dist/src/tui/plugin-write-task.d.ts +67 -0
- package/dist/src/tui/plugin-write-task.js +282 -0
- package/dist/src/tui/plugin-write-task.js.map +1 -0
- package/dist/src/tui/router.d.ts +39 -2
- package/dist/src/tui/router.js +13 -5
- package/dist/src/tui/router.js.map +1 -1
- package/dist/src/tui/screen-registry.d.ts +125 -6
- package/dist/src/tui/screen-registry.js +35 -4
- package/dist/src/tui/screen-registry.js.map +1 -1
- package/dist/src/utils/catalog-generation-service.d.ts +4 -0
- package/dist/src/utils/catalog-generation-service.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import { CatalogManagementError, } from './catalog-management.js';
|
|
2
3
|
import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
|
|
3
4
|
import { readSafeOperationError } from '../utils/safe-operation-diagnostic.js';
|
|
5
|
+
import { statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, } from '../commands/status-v6.js';
|
|
4
6
|
import { buildRebuildReceiptJson, requestedIntentAction } from '../output/command-receipt.js';
|
|
5
7
|
import { buildOperationResultModel } from '../commands/operation-planner.js';
|
|
6
8
|
import { buildOperationReviewProjection } from './operation-review-planner.js';
|
|
@@ -13,6 +15,7 @@ import { TuiRouter, } from './router.js';
|
|
|
13
15
|
import { normalizeTuiRoute } from './screen-registry.js';
|
|
14
16
|
import { CatalogSourcesSession } from './catalog-sources/session.js';
|
|
15
17
|
import { catalogSourceAutoCheckOnlyValue, projectCatalogSourceMutationWarnings, } from './catalog-sources/review.js';
|
|
18
|
+
import { buildPluginWriteTaskCandidates, pluginWriteTaskTargetKey, } from './plugin-write-task.js';
|
|
16
19
|
export const PLUGIN_WRITE_BATCH_ACTIONS = [
|
|
17
20
|
'install',
|
|
18
21
|
'sync',
|
|
@@ -20,6 +23,57 @@ export const PLUGIN_WRITE_BATCH_ACTIONS = [
|
|
|
20
23
|
'uninstall',
|
|
21
24
|
'cleanup',
|
|
22
25
|
];
|
|
26
|
+
export function pluginWriteBatchTargets(action, pluginIds, statusTargets) {
|
|
27
|
+
const selected = new Set(pluginIds);
|
|
28
|
+
const targets = [];
|
|
29
|
+
const add = (target, scope) => {
|
|
30
|
+
targets.push({ pluginId: target.pluginId, platform: target.platform, scope });
|
|
31
|
+
};
|
|
32
|
+
for (const target of statusTargets) {
|
|
33
|
+
if (!selected.has(target.pluginId))
|
|
34
|
+
continue;
|
|
35
|
+
const intent = statusTargetIntent(target);
|
|
36
|
+
const ownership = statusTargetOwnershipState(target);
|
|
37
|
+
const activation = statusTargetActivation(target);
|
|
38
|
+
const hasManagedOutput = ownership !== 'absent';
|
|
39
|
+
const hasCleanupEvidence = ownership === 'present' || ownership === 'partial'
|
|
40
|
+
|| intent.relation === 'managed';
|
|
41
|
+
if (action === 'sync' || action === 'reinstall') {
|
|
42
|
+
intent.scopes.forEach((scope) => add(target, scope));
|
|
43
|
+
if (hasManagedOutput)
|
|
44
|
+
add(target, target.scope);
|
|
45
|
+
if (target.platform === 'codex') {
|
|
46
|
+
if (activation?.user === true)
|
|
47
|
+
add(target, 'user');
|
|
48
|
+
if (activation?.project === true)
|
|
49
|
+
add(target, 'project');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (action === 'uninstall') {
|
|
53
|
+
if (hasManagedOutput)
|
|
54
|
+
add(target, target.scope);
|
|
55
|
+
if (target.platform === 'codex') {
|
|
56
|
+
if (activation?.user === true)
|
|
57
|
+
add(target, 'user');
|
|
58
|
+
if (activation?.project === true)
|
|
59
|
+
add(target, 'project');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (hasCleanupEvidence) {
|
|
63
|
+
add(target, target.scope);
|
|
64
|
+
if (target.platform === 'codex') {
|
|
65
|
+
if (activation?.user === true)
|
|
66
|
+
add(target, 'user');
|
|
67
|
+
if (activation?.project === true)
|
|
68
|
+
add(target, 'project');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...new Map(targets.map((target) => [
|
|
73
|
+
[target.pluginId, target.platform, target.scope].join('\0'), target,
|
|
74
|
+
])).values()].sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
|
|
75
|
+
.localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en'));
|
|
76
|
+
}
|
|
23
77
|
function reviewPreparationStageLabel(stage) {
|
|
24
78
|
return {
|
|
25
79
|
refresh: '刷新相关来源',
|
|
@@ -70,10 +124,25 @@ function canonicalBatchTargets(targets) {
|
|
|
70
124
|
return [...byKey.values()].sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
|
|
71
125
|
.localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en'));
|
|
72
126
|
}
|
|
127
|
+
function canonicalPluginWriteTaskTargets(targets) {
|
|
128
|
+
return [...new Map(targets.map((target) => [pluginWriteTaskTargetKey(target), target])).values()]
|
|
129
|
+
.sort((left, right) => pluginWriteTaskTargetKey(left)
|
|
130
|
+
.localeCompare(pluginWriteTaskTargetKey(right), 'en'));
|
|
131
|
+
}
|
|
73
132
|
function normalizeOperationIntent(intent) {
|
|
74
133
|
if (intent.kind === 'configSave' && intent.retryApply && !intent.apply) {
|
|
75
134
|
return { ...intent, apply: true };
|
|
76
135
|
}
|
|
136
|
+
if (intent.kind === 'pluginWriteTask') {
|
|
137
|
+
return { ...intent, targets: canonicalPluginWriteTaskTargets(intent.targets) };
|
|
138
|
+
}
|
|
139
|
+
if (intent.kind === 'catalogManagement' && intent.action === 'storePrune') {
|
|
140
|
+
const items = [...new Map(intent.items.map((item) => [
|
|
141
|
+
`${item.category}\0${item.storeKey}`,
|
|
142
|
+
item,
|
|
143
|
+
])).values()].sort((left, right) => `${left.category}\0${left.storeKey}`.localeCompare(`${right.category}\0${right.storeKey}`, 'en'));
|
|
144
|
+
return { ...intent, items };
|
|
145
|
+
}
|
|
77
146
|
if (intent.kind !== 'pluginWriteBatch')
|
|
78
147
|
return intent;
|
|
79
148
|
if (intent.selection.kind === 'targets') {
|
|
@@ -92,6 +161,49 @@ function normalizeOperationIntent(intent) {
|
|
|
92
161
|
},
|
|
93
162
|
};
|
|
94
163
|
}
|
|
164
|
+
function assertPluginWriteTaskEvidence(intent, evidence) {
|
|
165
|
+
const candidate = evidence;
|
|
166
|
+
const plan = candidate.taskPlan;
|
|
167
|
+
if (!plan || plan.goal !== intent.goal || !Array.isArray(plan.requestedTargets)
|
|
168
|
+
|| !Array.isArray(plan.executableTargets) || !Array.isArray(plan.blockedTargets)
|
|
169
|
+
|| !Array.isArray(plan.causes) || !Array.isArray(plan.sources)
|
|
170
|
+
|| typeof plan.targetFingerprint !== 'string' || typeof plan.planFingerprint !== 'string'
|
|
171
|
+
|| JSON.stringify(plan.requestedTargets) !== JSON.stringify(intent.targets)
|
|
172
|
+
|| plan.executableTargets.length === 0) {
|
|
173
|
+
throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务确认信息不完整。');
|
|
174
|
+
}
|
|
175
|
+
if (plan.planFingerprint !== operationEvidenceFingerprint(evidence)) {
|
|
176
|
+
throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务确认指纹无效。');
|
|
177
|
+
}
|
|
178
|
+
if (intent.executionPolicy === 'all' && plan.blockedTargets.length > 0) {
|
|
179
|
+
throw new TuiOperationBlockedError('taskHasBlockedTargets', '部分目标需要先处理,不能作为完整任务执行。');
|
|
180
|
+
}
|
|
181
|
+
const requested = new Set(plan.requestedTargets.map(pluginWriteTaskTargetKey));
|
|
182
|
+
const executable = new Set(plan.executableTargets.map(pluginWriteTaskTargetKey));
|
|
183
|
+
const blocked = new Set(plan.blockedTargets.map((target) => pluginWriteTaskTargetKey(target.target)));
|
|
184
|
+
if (executable.size !== plan.executableTargets.length || blocked.size !== plan.blockedTargets.length
|
|
185
|
+
|| [...executable].some((key) => !requested.has(key) || blocked.has(key))
|
|
186
|
+
|| [...blocked].some((key) => !requested.has(key))
|
|
187
|
+
|| [...requested].some((key) => !executable.has(key) && !blocked.has(key))) {
|
|
188
|
+
throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务目标分区无效。');
|
|
189
|
+
}
|
|
190
|
+
const grouped = new Set();
|
|
191
|
+
for (const source of plan.sources) {
|
|
192
|
+
if (!source.sourceId || !source.sourceIdentity || source.requestedTargets.length === 0
|
|
193
|
+
|| source.executable !== Boolean(source.preview.operationPlan)
|
|
194
|
+
|| source.requestedTargets.some((target) => !executable.has(pluginWriteTaskTargetKey(target))
|
|
195
|
+
|| sourceIdFromPluginId(target.pluginId) !== source.sourceId)
|
|
196
|
+
|| source.causes.some((cause) => cause.sourceId !== source.sourceId
|
|
197
|
+
|| cause.sourceIdentity !== source.sourceIdentity
|
|
198
|
+
|| cause.consumers.some((consumer) => !executable.has(pluginWriteTaskTargetKey(consumer))))) {
|
|
199
|
+
throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务来源计划无效。');
|
|
200
|
+
}
|
|
201
|
+
source.requestedTargets.forEach((target) => grouped.add(pluginWriteTaskTargetKey(target)));
|
|
202
|
+
}
|
|
203
|
+
if ([...executable].some((key) => !grouped.has(key))) {
|
|
204
|
+
throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务来源计划缺少可执行目标。');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
95
207
|
function assertPluginWriteBatchEvidence(evidence) {
|
|
96
208
|
const candidate = evidence;
|
|
97
209
|
if (!Array.isArray(candidate.requestedPluginIds) || !Array.isArray(candidate.groups)) {
|
|
@@ -195,7 +307,62 @@ export function pluginWriteBatchFailedRequestedPluginIds(execution) {
|
|
|
195
307
|
return canonicalPluginIds(pluginWriteBatchFailedRequestedTargets(execution)
|
|
196
308
|
.map((target) => target.pluginId));
|
|
197
309
|
}
|
|
310
|
+
export function pluginWriteTaskFailedRequestedTargets(execution) {
|
|
311
|
+
const requested = new Map(execution.taskPlan.requestedTargets.map((target) => [
|
|
312
|
+
pluginWriteTaskTargetKey(target), target,
|
|
313
|
+
]));
|
|
314
|
+
const failed = new Set();
|
|
315
|
+
const succeeded = new Set();
|
|
316
|
+
for (const source of execution.sourceResults) {
|
|
317
|
+
const sourceRequested = new Set(source.requestedTargets.map(pluginWriteTaskTargetKey));
|
|
318
|
+
if (source.result.targetResults.length === 0 && source.result.overallState !== 'succeeded') {
|
|
319
|
+
sourceRequested.forEach((key) => failed.add(key));
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
for (const result of source.result.targetResults) {
|
|
323
|
+
const pluginId = result.target.pluginId;
|
|
324
|
+
if (!pluginId)
|
|
325
|
+
continue;
|
|
326
|
+
const exactKey = pluginWriteTaskTargetKey({
|
|
327
|
+
pluginId,
|
|
328
|
+
platform: result.target.platform,
|
|
329
|
+
scope: result.target.scope,
|
|
330
|
+
});
|
|
331
|
+
if (sourceRequested.has(exactKey)) {
|
|
332
|
+
if (result.state === 'failed' || result.action === 'skipped')
|
|
333
|
+
failed.add(exactKey);
|
|
334
|
+
else
|
|
335
|
+
succeeded.add(exactKey);
|
|
336
|
+
}
|
|
337
|
+
if (result.state !== 'failed' && result.action !== 'skipped')
|
|
338
|
+
continue;
|
|
339
|
+
for (const cause of source.requestedTargets.length > 0
|
|
340
|
+
? execution.taskPlan.causes.filter((cause) => cause.sourceId === source.sourceId) : []) {
|
|
341
|
+
if (pluginWriteTaskTargetKey(cause.repairTarget) !== exactKey)
|
|
342
|
+
continue;
|
|
343
|
+
cause.consumers.forEach((consumer) => failed.add(pluginWriteTaskTargetKey(consumer)));
|
|
344
|
+
}
|
|
345
|
+
const placement = source.result.dependencyPlan?.targets.find((target) => target.pluginId === pluginId && target.platform === result.target.platform
|
|
346
|
+
&& (target.requestedScope ?? target.resolvedScope) === result.target.scope);
|
|
347
|
+
for (const consumer of placement?.requiredByTargets ?? []) {
|
|
348
|
+
failed.add(pluginWriteTaskTargetKey(consumer));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const key of succeeded)
|
|
353
|
+
failed.delete(key);
|
|
354
|
+
return canonicalPluginWriteTaskTargets([...failed].flatMap((key) => {
|
|
355
|
+
const target = requested.get(key);
|
|
356
|
+
return target ? [target] : [];
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
198
359
|
function collectOperationSensitiveValues(intent) {
|
|
360
|
+
if (intent.kind === 'catalogManagement') {
|
|
361
|
+
return [
|
|
362
|
+
...('ownerUrl' in intent && intent.ownerUrl ? [intent.ownerUrl] : []),
|
|
363
|
+
...('remoteUrl' in intent && intent.remoteUrl ? [intent.remoteUrl] : []),
|
|
364
|
+
];
|
|
365
|
+
}
|
|
199
366
|
if (intent.kind !== 'configSave')
|
|
200
367
|
return [];
|
|
201
368
|
const sensitiveKeys = new Set([...(intent.sensitiveKeys ?? []), ...(intent.privateKeys ?? [])]);
|
|
@@ -243,6 +410,53 @@ function cloneFrozen(value) {
|
|
|
243
410
|
return deepFreeze(structuredClone(value));
|
|
244
411
|
}
|
|
245
412
|
export function operationEvidenceFingerprint(evidence) {
|
|
413
|
+
if (evidence.catalogManagement) {
|
|
414
|
+
const management = evidence.catalogManagement;
|
|
415
|
+
return crypto.createHash('sha256')
|
|
416
|
+
.update(JSON.stringify(stableValue(management.action === 'storePrune'
|
|
417
|
+
? {
|
|
418
|
+
action: management.action,
|
|
419
|
+
workspaceBinding: management.workspaceBinding,
|
|
420
|
+
authorizedStoreItems: management.authorizedStoreItems,
|
|
421
|
+
}
|
|
422
|
+
: {
|
|
423
|
+
action: management.action,
|
|
424
|
+
workspaceBinding: management.workspaceBinding,
|
|
425
|
+
evidenceDigest: management.evidenceDigest,
|
|
426
|
+
})))
|
|
427
|
+
.digest('hex');
|
|
428
|
+
}
|
|
429
|
+
if ('taskPlan' in evidence) {
|
|
430
|
+
const task = evidence.taskPlan;
|
|
431
|
+
return crypto.createHash('sha256')
|
|
432
|
+
.update(JSON.stringify(stableValue({
|
|
433
|
+
catalog: evidence.catalog,
|
|
434
|
+
goal: task.goal,
|
|
435
|
+
requestedTargets: task.requestedTargets,
|
|
436
|
+
executableTargets: task.executableTargets,
|
|
437
|
+
causes: task.causes,
|
|
438
|
+
blockedTargets: task.blockedTargets,
|
|
439
|
+
targetFingerprint: task.targetFingerprint,
|
|
440
|
+
sources: task.sources.map((source) => ({
|
|
441
|
+
sourceId: source.sourceId,
|
|
442
|
+
sourceIdentity: source.sourceIdentity,
|
|
443
|
+
requestedTargets: source.requestedTargets,
|
|
444
|
+
causes: source.causes,
|
|
445
|
+
executable: source.executable,
|
|
446
|
+
planFingerprint: source.preview.operationPlan?.planFingerprint,
|
|
447
|
+
targetResults: source.preview.targetResults.map((target) => ({
|
|
448
|
+
pluginId: target.target.pluginId,
|
|
449
|
+
platform: target.target.platform,
|
|
450
|
+
scope: target.target.scope,
|
|
451
|
+
state: target.state,
|
|
452
|
+
action: target.action,
|
|
453
|
+
errorCode: target.errorCode,
|
|
454
|
+
requiredBy: target.requiredBy,
|
|
455
|
+
})),
|
|
456
|
+
})),
|
|
457
|
+
})))
|
|
458
|
+
.digest('hex');
|
|
459
|
+
}
|
|
246
460
|
if ('groups' in evidence && 'requestedPluginIds' in evidence) {
|
|
247
461
|
const batch = evidence;
|
|
248
462
|
return crypto.createHash('sha256')
|
|
@@ -337,6 +551,8 @@ function issueCode(error) {
|
|
|
337
551
|
return safeDiagnosticCode(safeOperationError.code, 'operationBlocked');
|
|
338
552
|
if (error instanceof TuiOperationBlockedError)
|
|
339
553
|
return error.code;
|
|
554
|
+
if (error instanceof CatalogManagementError)
|
|
555
|
+
return error.code;
|
|
340
556
|
if (error instanceof CatalogGenerationError)
|
|
341
557
|
return error.code;
|
|
342
558
|
if (error instanceof Error && error.name === 'CatalogSourcesConfigConflictError') {
|
|
@@ -408,6 +624,7 @@ export class TuiController {
|
|
|
408
624
|
#retiring = false;
|
|
409
625
|
#externalDrafts = [];
|
|
410
626
|
#batchRetryIntent;
|
|
627
|
+
#taskRetryIntent;
|
|
411
628
|
#operationHistory = new OperationHistoryStore();
|
|
412
629
|
#sessionEpoch = 0;
|
|
413
630
|
#lastRecordedResultId;
|
|
@@ -431,6 +648,7 @@ export class TuiController {
|
|
|
431
648
|
onError: (error, code) => {
|
|
432
649
|
void this.#logErrorSafe(error, code);
|
|
433
650
|
},
|
|
651
|
+
now: this.dependencies.now,
|
|
434
652
|
});
|
|
435
653
|
if (progressFeed) {
|
|
436
654
|
this.#progressUnsubscribe = progressFeed.subscribe((progress) => {
|
|
@@ -622,6 +840,7 @@ export class TuiController {
|
|
|
622
840
|
this.#installPlacementDraft = undefined;
|
|
623
841
|
this.#recoverableInstallPlacementDraft = undefined;
|
|
624
842
|
this.#batchRetryIntent = undefined;
|
|
843
|
+
this.#taskRetryIntent = undefined;
|
|
625
844
|
this.#externalDrafts = [];
|
|
626
845
|
this.#result = undefined;
|
|
627
846
|
this.#resultAnchor = undefined;
|
|
@@ -774,6 +993,18 @@ export class TuiController {
|
|
|
774
993
|
}
|
|
775
994
|
#ensureOperationFlow(intent) {
|
|
776
995
|
const active = this.#router.activeFlow();
|
|
996
|
+
if (intent.kind === 'catalogManagement') {
|
|
997
|
+
if (active?.kind === 'catalogManagement')
|
|
998
|
+
return;
|
|
999
|
+
this.#router.beginFlow('catalogManagement', this.#router.snapshot.current.route);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (intent.kind === 'pluginWriteTask') {
|
|
1003
|
+
if (active?.kind === 'pluginWriteTask')
|
|
1004
|
+
return;
|
|
1005
|
+
this.#router.beginFlow('pluginWriteTask', this.#router.snapshot.current.route);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
777
1008
|
if (intent.kind === 'pluginWriteBatch') {
|
|
778
1009
|
if (active?.kind === 'batchWrite')
|
|
779
1010
|
return;
|
|
@@ -818,7 +1049,9 @@ export class TuiController {
|
|
|
818
1049
|
: intent.kind === 'sourceMutation' && intent.action !== 'remove'
|
|
819
1050
|
? { name: 'sourceDetail', sourceId: intent.sourceId }
|
|
820
1051
|
: intent.kind === 'catalogGeneration'
|
|
821
|
-
?
|
|
1052
|
+
? this.#router.snapshot.current.route.name === 'pluginWriteTask'
|
|
1053
|
+
? this.#router.snapshot.current.route
|
|
1054
|
+
: { name: 'sourceDetail', sourceId: intent.sourceId }
|
|
822
1055
|
: { name: 'sourceList' });
|
|
823
1056
|
}
|
|
824
1057
|
#stableResultRoute(intent, result) {
|
|
@@ -836,6 +1069,8 @@ export class TuiController {
|
|
|
836
1069
|
}
|
|
837
1070
|
if (intent.kind === 'pluginWriteBatch')
|
|
838
1071
|
return { name: 'pluginList', mode: 'installed' };
|
|
1072
|
+
if (intent.kind === 'pluginWriteTask')
|
|
1073
|
+
return { name: 'pluginList', mode: 'installed' };
|
|
839
1074
|
if (intent.kind === 'portableInstall' || intent.kind === 'configSave' || intent.kind === 'configReset') {
|
|
840
1075
|
return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
|
|
841
1076
|
}
|
|
@@ -843,8 +1078,22 @@ export class TuiController {
|
|
|
843
1078
|
return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
|
|
844
1079
|
}
|
|
845
1080
|
if (intent.kind === 'catalogGeneration') {
|
|
1081
|
+
if ('returnRoute' in result && result.returnRoute?.name === 'pluginWriteTask') {
|
|
1082
|
+
return this.#normalizeRoute(result.returnRoute);
|
|
1083
|
+
}
|
|
846
1084
|
return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
|
|
847
1085
|
}
|
|
1086
|
+
if (intent.kind === 'catalogManagement') {
|
|
1087
|
+
if (intent.action === 'devActivate' || intent.action === 'devDeactivate') {
|
|
1088
|
+
return { name: 'catalogDev', step: 'overview' };
|
|
1089
|
+
}
|
|
1090
|
+
if (intent.action === 'storePrune')
|
|
1091
|
+
return { name: 'catalogStore' };
|
|
1092
|
+
if (intent.action === 'publishPlan' || intent.action === 'publishApply') {
|
|
1093
|
+
return { name: 'catalogPublish', step: 'overview' };
|
|
1094
|
+
}
|
|
1095
|
+
return { name: 'catalogWorkspace', view: 'overview' };
|
|
1096
|
+
}
|
|
848
1097
|
return { name: 'sourceList' };
|
|
849
1098
|
}
|
|
850
1099
|
#showResult(result, intent) {
|
|
@@ -883,6 +1132,19 @@ export class TuiController {
|
|
|
883
1132
|
},
|
|
884
1133
|
}
|
|
885
1134
|
: undefined;
|
|
1135
|
+
this.#taskRetryIntent = intent.kind === 'pluginWriteTask'
|
|
1136
|
+
&& 'summary' in result
|
|
1137
|
+
&& result.summary?.task
|
|
1138
|
+
&& (result.summary.task.retryTargets?.length ?? 0) > 0
|
|
1139
|
+
? {
|
|
1140
|
+
...structuredClone(intent),
|
|
1141
|
+
targets: result.summary.task.retryTargets.map((target) => {
|
|
1142
|
+
const current = this.listPluginWriteTaskCandidates(intent.goal).find((candidate) => pluginWriteTaskTargetKey(candidate.target) === pluginWriteTaskTargetKey(target));
|
|
1143
|
+
return structuredClone(current?.eligible ? current.target : target);
|
|
1144
|
+
}),
|
|
1145
|
+
executionPolicy: 'all',
|
|
1146
|
+
}
|
|
1147
|
+
: undefined;
|
|
886
1148
|
if (!this.#router.finishFlow(target, terminal))
|
|
887
1149
|
this.#router.replace(terminal);
|
|
888
1150
|
}
|
|
@@ -923,14 +1185,18 @@ export class TuiController {
|
|
|
923
1185
|
this.#catalogSourcesSession.configure(status.payload.catalog.sources.map((source) => {
|
|
924
1186
|
const generation = this.#catalogGenerations.get(source.id);
|
|
925
1187
|
const detail = details.get(source.id);
|
|
1188
|
+
const probeFingerprint = JSON.stringify({
|
|
1189
|
+
sourceIdentity: source.identity,
|
|
1190
|
+
requestedRef: source.requestedRef,
|
|
1191
|
+
});
|
|
926
1192
|
return {
|
|
927
1193
|
sourceId: source.id,
|
|
928
1194
|
primary: source.primary,
|
|
929
1195
|
autoCheck: this.dependencies.probeCatalogFreshness !== undefined
|
|
930
1196
|
&& (detail?.autoCheck ?? true),
|
|
1197
|
+
probeFingerprint,
|
|
931
1198
|
fingerprint: JSON.stringify({
|
|
932
|
-
|
|
933
|
-
requestedRef: source.requestedRef,
|
|
1199
|
+
probeFingerprint,
|
|
934
1200
|
activeStoreKey: generation?.active?.storeKey,
|
|
935
1201
|
generationState: generation?.state,
|
|
936
1202
|
generationErrorCode: generation?.errorCode,
|
|
@@ -1252,8 +1518,11 @@ export class TuiController {
|
|
|
1252
1518
|
return origin;
|
|
1253
1519
|
if (this.#router.snapshot.current.route.name === 'result'
|
|
1254
1520
|
&& this.#resultAnchor
|
|
1255
|
-
&& (this.#resultAnchor.name === 'pluginPreview' || this.#resultAnchor.name === 'pluginDetail'
|
|
1256
|
-
|
|
1521
|
+
&& (this.#resultAnchor.name === 'pluginPreview' || this.#resultAnchor.name === 'pluginDetail'
|
|
1522
|
+
|| this.#resultAnchor.name === 'pluginWriteTask')
|
|
1523
|
+
&& (this.#resultAnchor.pluginId === pluginId
|
|
1524
|
+
|| this.#resultAnchor.name === 'pluginWriteTask'
|
|
1525
|
+
&& this.#resultAnchor.targetKeys?.some((key) => key.startsWith(`${pluginId}\0`)))) {
|
|
1257
1526
|
return structuredClone(this.#resultAnchor);
|
|
1258
1527
|
}
|
|
1259
1528
|
return undefined;
|
|
@@ -1621,16 +1890,30 @@ export class TuiController {
|
|
|
1621
1890
|
return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
|
|
1622
1891
|
&& (options.mode === 'browse' || hasInstalledPlacement(plugin) || plugin.savedConfigScopes.length > 0)
|
|
1623
1892
|
&& (options.installFilter !== 'uninstalled' || !hasInstalledPlacement(plugin))
|
|
1893
|
+
&& (options.installFilter !== 'updateAvailable' || plugin.update?.state === 'available')
|
|
1624
1894
|
&& (!query || `${plugin.displayName}\n${plugin.description}\n${plugin.sourceId}`.toLocaleLowerCase('en').includes(query))));
|
|
1625
1895
|
}
|
|
1626
1896
|
getPluginOverview(pluginId) {
|
|
1627
1897
|
const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
|
|
1628
1898
|
return plugin ? cloneFrozen(plugin) : undefined;
|
|
1629
1899
|
}
|
|
1900
|
+
listCurrentProjectIntentRoots() {
|
|
1901
|
+
return cloneFrozen([...new Set((this.#status?.payload.targets ?? []).flatMap((target) => {
|
|
1902
|
+
const intent = statusTargetIntent(target);
|
|
1903
|
+
return intent.relation === 'root' && intent.scopes.includes('project')
|
|
1904
|
+
? [target.pluginId] : [];
|
|
1905
|
+
}))].sort((left, right) => left.localeCompare(right, 'en')));
|
|
1906
|
+
}
|
|
1907
|
+
listPluginWriteBatchTargets(action, pluginIds) {
|
|
1908
|
+
return cloneFrozen(pluginWriteBatchTargets(action, pluginIds, this.#status?.payload.targets ?? []));
|
|
1909
|
+
}
|
|
1630
1910
|
getPluginDependencyOverview(pluginId) {
|
|
1631
1911
|
const overview = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId);
|
|
1632
1912
|
return overview ? cloneFrozen(overview) : undefined;
|
|
1633
1913
|
}
|
|
1914
|
+
listPluginDependencyOverviews() {
|
|
1915
|
+
return cloneFrozen(this.#viewModel?.dependencyOverviews ?? []);
|
|
1916
|
+
}
|
|
1634
1917
|
getBlockingDependents(pluginId, platform, scope) {
|
|
1635
1918
|
const dependents = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId)?.dependents ?? [];
|
|
1636
1919
|
return cloneFrozen(dependents.flatMap((dependent) => {
|
|
@@ -1756,7 +2039,7 @@ export class TuiController {
|
|
|
1756
2039
|
return this.#catalogSourcesSession.fullValidate(sourceId);
|
|
1757
2040
|
}
|
|
1758
2041
|
setExternalDraftState(states = []) {
|
|
1759
|
-
const priority = { sourceDraft: 0, batchDraft: 1,
|
|
2042
|
+
const priority = { sourceDraft: 0, batchDraft: 1, catalogDraft: 2, configDraft: 3 };
|
|
1760
2043
|
this.#externalDrafts = structuredClone(states).sort((left, right) => priority[left.kind] - priority[right.kind] || left.owner.localeCompare(right.owner, 'en'));
|
|
1761
2044
|
this.#publish();
|
|
1762
2045
|
}
|
|
@@ -1769,6 +2052,26 @@ export class TuiController {
|
|
|
1769
2052
|
startPluginWriteBatchReview(intent) {
|
|
1770
2053
|
return this.startReviewPreparation(intent, { name: 'batchPlacement' });
|
|
1771
2054
|
}
|
|
2055
|
+
startPluginWriteTaskReview(intent, returnRoute) {
|
|
2056
|
+
return this.startReviewPreparation(intent, returnRoute);
|
|
2057
|
+
}
|
|
2058
|
+
beginNestedPluginWriteTask(route) {
|
|
2059
|
+
this.#assertAcceptingActions();
|
|
2060
|
+
const current = this.#router.snapshot.current.route;
|
|
2061
|
+
if (current.name !== 'pluginWriteTask') {
|
|
2062
|
+
throw new TuiOperationBlockedError('taskOriginUnavailable', '嵌套任务缺少父任务。');
|
|
2063
|
+
}
|
|
2064
|
+
this.#router.beginFlow('pluginWriteTask', current);
|
|
2065
|
+
this.#router.push(route);
|
|
2066
|
+
this.#publish();
|
|
2067
|
+
}
|
|
2068
|
+
pluginWriteTaskReturnRoute(route) {
|
|
2069
|
+
const active = this.#router.activeFlow();
|
|
2070
|
+
return structuredClone(active?.kind === 'pluginWriteTask'
|
|
2071
|
+
&& active.returnTarget.name === 'pluginWriteTask'
|
|
2072
|
+
? active.returnTarget
|
|
2073
|
+
: route);
|
|
2074
|
+
}
|
|
1772
2075
|
retryFailedPluginWriteBatch() {
|
|
1773
2076
|
if (!this.#batchRetryIntent)
|
|
1774
2077
|
return undefined;
|
|
@@ -1776,6 +2079,13 @@ export class TuiController {
|
|
|
1776
2079
|
this.#batchRetryIntent = undefined;
|
|
1777
2080
|
return this.startReviewPreparation(intent, { name: 'pluginList', mode: 'installed' });
|
|
1778
2081
|
}
|
|
2082
|
+
retryFailedPluginWriteTask() {
|
|
2083
|
+
if (!this.#taskRetryIntent)
|
|
2084
|
+
return undefined;
|
|
2085
|
+
const intent = structuredClone(this.#taskRetryIntent);
|
|
2086
|
+
this.#taskRetryIntent = undefined;
|
|
2087
|
+
return this.startReviewPreparation(intent, { name: 'pluginList', mode: 'installed' });
|
|
2088
|
+
}
|
|
1779
2089
|
catalogActivationBlockReason() {
|
|
1780
2090
|
if (this.#externalDrafts[0])
|
|
1781
2091
|
return this.#externalDrafts[0].kind;
|
|
@@ -1790,11 +2100,26 @@ export class TuiController {
|
|
|
1790
2100
|
return 'writeProgress';
|
|
1791
2101
|
return undefined;
|
|
1792
2102
|
}
|
|
1793
|
-
catalogSourceRecoveryBlockReason() {
|
|
2103
|
+
catalogSourceRecoveryBlockReason(intent) {
|
|
1794
2104
|
const activeFlow = this.#router.activeFlow();
|
|
2105
|
+
const route = this.#router.snapshot.current.route;
|
|
2106
|
+
const ownedFrozenBatchTask = intent !== undefined
|
|
2107
|
+
&& route.name === 'pluginWriteTask'
|
|
2108
|
+
&& route.targetKeys !== undefined
|
|
2109
|
+
&& route.targetKeys.length > 0
|
|
2110
|
+
&& route.batchOrigin !== undefined
|
|
2111
|
+
&& (activeFlow?.id === route.batchOrigin.flowId
|
|
2112
|
+
|| activeFlow?.parentFlowId === route.batchOrigin.flowId)
|
|
2113
|
+
&& this.listPluginWriteTaskCandidates(route.goal).some((candidate) => candidate.sourceId === intent.sourceId
|
|
2114
|
+
&& candidate.blockReason === 'catalogActivationRequired'
|
|
2115
|
+
&& route.targetKeys.includes(pluginWriteTaskTargetKey(candidate.target)));
|
|
1795
2116
|
const blockingExternalDraft = this.#externalDrafts.find((draft) => !(draft.kind === 'configDraft'
|
|
1796
2117
|
&& activeFlow?.kind === 'sourceRecovery'
|
|
1797
|
-
&& activeFlow.parentFlowId !== undefined)
|
|
2118
|
+
&& activeFlow.parentFlowId !== undefined)
|
|
2119
|
+
&& !(ownedFrozenBatchTask
|
|
2120
|
+
&& draft.kind === 'batchDraft'
|
|
2121
|
+
&& draft.owner === route.batchOrigin?.owner
|
|
2122
|
+
&& draft.flowId === route.batchOrigin.flowId));
|
|
1798
2123
|
if (blockingExternalDraft)
|
|
1799
2124
|
return blockingExternalDraft.kind;
|
|
1800
2125
|
if (this.#portableDraft)
|
|
@@ -1865,6 +2190,61 @@ export class TuiController {
|
|
|
1865
2190
|
: this.#abort.signal;
|
|
1866
2191
|
return cloneFrozen(await this.#trackRead(this.dependencies.getConfigPage(pluginId, structuredClone(layer), combined)));
|
|
1867
2192
|
}
|
|
2193
|
+
async selectCatalogWorkspace(workspaceRoot) {
|
|
2194
|
+
this.#assertAcceptingActions();
|
|
2195
|
+
if (!this.dependencies.catalogManagement) {
|
|
2196
|
+
throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
|
|
2197
|
+
}
|
|
2198
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.selectWorkspace(workspaceRoot)));
|
|
2199
|
+
}
|
|
2200
|
+
async getCatalogWorkspaceManagement() {
|
|
2201
|
+
this.#assertAcceptingActions();
|
|
2202
|
+
if (!this.dependencies.catalogManagement) {
|
|
2203
|
+
throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
|
|
2204
|
+
}
|
|
2205
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getWorkspace()));
|
|
2206
|
+
}
|
|
2207
|
+
async validateCatalogWorkspace() {
|
|
2208
|
+
this.#assertAcceptingActions();
|
|
2209
|
+
if (!this.dependencies.catalogManagement) {
|
|
2210
|
+
throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
|
|
2211
|
+
}
|
|
2212
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.validateWorkspace()));
|
|
2213
|
+
}
|
|
2214
|
+
async getCatalogDevManagement() {
|
|
2215
|
+
this.#assertAcceptingActions();
|
|
2216
|
+
if (!this.dependencies.catalogManagement) {
|
|
2217
|
+
throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
|
|
2218
|
+
}
|
|
2219
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getDev()));
|
|
2220
|
+
}
|
|
2221
|
+
async getCatalogStoreManagement() {
|
|
2222
|
+
this.#assertAcceptingActions();
|
|
2223
|
+
if (!this.dependencies.catalogManagement) {
|
|
2224
|
+
throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
|
|
2225
|
+
}
|
|
2226
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getStore()));
|
|
2227
|
+
}
|
|
2228
|
+
async getCatalogPublicationManagement() {
|
|
2229
|
+
this.#assertAcceptingActions();
|
|
2230
|
+
if (!this.dependencies.catalogManagement) {
|
|
2231
|
+
throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
|
|
2232
|
+
}
|
|
2233
|
+
return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getPublication()));
|
|
2234
|
+
}
|
|
2235
|
+
async previewConfigSaveImpact(intent, signal) {
|
|
2236
|
+
this.#assertAcceptingActions();
|
|
2237
|
+
const normalized = normalizeOperationIntent(intent);
|
|
2238
|
+
this.#validateReviewIntent(normalized);
|
|
2239
|
+
const combined = signal
|
|
2240
|
+
? AbortSignal.any([this.#abort.signal, signal])
|
|
2241
|
+
: this.#abort.signal;
|
|
2242
|
+
const evidence = await this.#trackRead(this.#prepareReviewEvidence(normalized, combined));
|
|
2243
|
+
if (!evidence.config) {
|
|
2244
|
+
throw new TuiOperationBlockedError('configImpactUnavailable', '无法确定设置修改的应用影响。');
|
|
2245
|
+
}
|
|
2246
|
+
return cloneFrozen(evidence.config);
|
|
2247
|
+
}
|
|
1868
2248
|
async getConfigResetAvailability(pluginId, layer) {
|
|
1869
2249
|
this.#assertAcceptingActions();
|
|
1870
2250
|
if (!this.dependencies.getConfigResetAvailability) {
|
|
@@ -1875,6 +2255,11 @@ export class TuiController {
|
|
|
1875
2255
|
listUserIssues() {
|
|
1876
2256
|
return cloneFrozen(this.#viewModel?.issues ?? []);
|
|
1877
2257
|
}
|
|
2258
|
+
listPluginWriteTaskCandidates(goal) {
|
|
2259
|
+
return cloneFrozen(this.#viewModel
|
|
2260
|
+
? buildPluginWriteTaskCandidates(this.#viewModel, goal)
|
|
2261
|
+
: []);
|
|
2262
|
+
}
|
|
1878
2263
|
async buildOperationReview(intent, returnRoute) {
|
|
1879
2264
|
this.#assertAcceptingActions();
|
|
1880
2265
|
intent = normalizeOperationIntent(intent);
|
|
@@ -1895,7 +2280,7 @@ export class TuiController {
|
|
|
1895
2280
|
this.#assertAcceptingActions();
|
|
1896
2281
|
intent = normalizeOperationIntent(intent);
|
|
1897
2282
|
if (intent.kind === 'catalogGeneration' && !intent.sourceMutation) {
|
|
1898
|
-
const blockReason = this.catalogSourceRecoveryBlockReason();
|
|
2283
|
+
const blockReason = this.catalogSourceRecoveryBlockReason(intent);
|
|
1899
2284
|
if (blockReason) {
|
|
1900
2285
|
throw new TuiOperationBlockedError('catalogActivationBlocked', `Catalog Active 切换已阻止:${blockReason}。请先完成或放弃当前草稿或操作。`);
|
|
1901
2286
|
}
|
|
@@ -1958,6 +2343,37 @@ export class TuiController {
|
|
|
1958
2343
|
return this.startReviewPreparation(structuredClone(intent), preparation.returnRoute);
|
|
1959
2344
|
}
|
|
1960
2345
|
#validateReviewIntent(intent) {
|
|
2346
|
+
if (intent.kind === 'catalogManagement') {
|
|
2347
|
+
if (intent.action === 'storePrune' && intent.items.length === 0) {
|
|
2348
|
+
throw new TuiOperationBlockedError('catalogStoreSelectionInvalid', 'Select at least one reclaimable Catalog store item.');
|
|
2349
|
+
}
|
|
2350
|
+
if (intent.action === 'workspaceVersion'
|
|
2351
|
+
&& (intent.version === undefined) === (intent.bump === undefined)) {
|
|
2352
|
+
throw new TuiOperationBlockedError('catalogVersionSelectionInvalid', 'Select exactly one Catalog version or bump level.');
|
|
2353
|
+
}
|
|
2354
|
+
if ((intent.action === 'publishPlan' || intent.action === 'publishApply')
|
|
2355
|
+
&& intent.remote !== undefined && intent.remoteUrl !== undefined) {
|
|
2356
|
+
throw new TuiOperationBlockedError('catalogPublicationTargetInvalid', 'Select a Git remote name or one direct remote URL.');
|
|
2357
|
+
}
|
|
2358
|
+
if (intent.action === 'publishApply'
|
|
2359
|
+
&& !/^sha256:[0-9a-f]{64}$/u.test(intent.expectedPlanDigest)) {
|
|
2360
|
+
throw new TuiOperationBlockedError('catalogPublicationPlanInvalid', 'Select an exact prepared Catalog publication plan.');
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
if (intent.kind === 'pluginWriteTask') {
|
|
2364
|
+
if (intent.targets.length === 0
|
|
2365
|
+
|| JSON.stringify(intent.targets) !== JSON.stringify(canonicalPluginWriteTaskTargets(intent.targets))) {
|
|
2366
|
+
throw new TuiOperationBlockedError('taskSelectionInvalid', '任务目标必须是非空精确目标集合。');
|
|
2367
|
+
}
|
|
2368
|
+
for (const target of intent.targets) {
|
|
2369
|
+
sourceIdFromPluginId(target.pluginId);
|
|
2370
|
+
if ((target.platform !== 'codex' && target.platform !== 'claude')
|
|
2371
|
+
|| (target.scope !== 'user' && target.scope !== 'project')
|
|
2372
|
+
|| (intent.goal === 'update') !== Boolean(target.expectedUpdate)) {
|
|
2373
|
+
throw new TuiOperationBlockedError('taskSelectionInvalid', '任务目标或版本证据无效。');
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
1961
2377
|
if (intent.kind === 'pluginWriteBatch') {
|
|
1962
2378
|
if (intent.selection.kind === 'currentProjectIntentRoots'
|
|
1963
2379
|
&& (intent.action !== 'sync' || intent.scope !== 'project')) {
|
|
@@ -1978,8 +2394,12 @@ export class TuiController {
|
|
|
1978
2394
|
}
|
|
1979
2395
|
}
|
|
1980
2396
|
#reviewPreparationLabel(intent) {
|
|
2397
|
+
if (intent.kind === 'catalogManagement')
|
|
2398
|
+
return 'Catalog 管理';
|
|
1981
2399
|
if (intent.kind === 'catalogSourcesRecovery')
|
|
1982
2400
|
return 'Catalog 来源配置';
|
|
2401
|
+
if (intent.kind === 'pluginWriteTask')
|
|
2402
|
+
return `${intent.targets.length} 个安装位置`;
|
|
1983
2403
|
if (intent.kind === 'pluginWriteBatch') {
|
|
1984
2404
|
return intent.selection.kind === 'selected'
|
|
1985
2405
|
? `${intent.selection.pluginIds.length} 个插件`
|
|
@@ -2033,9 +2453,10 @@ export class TuiController {
|
|
|
2033
2453
|
preparation.snapshot.stages = preparation.snapshot.stages.map((stage) => stage.state === 'running' ? { ...stage, state: 'completed' } : stage);
|
|
2034
2454
|
this.#publish();
|
|
2035
2455
|
const returnRoute = structuredClone(preparation.returnRoute);
|
|
2456
|
+
this.#storeOperationReview(intent, evidence, 'replace', returnRoute);
|
|
2036
2457
|
this.#reviewPreparationIntents.delete(preparation.snapshot.id);
|
|
2037
2458
|
this.#reviewPreparation = undefined;
|
|
2038
|
-
this.#
|
|
2459
|
+
this.#publish();
|
|
2039
2460
|
}
|
|
2040
2461
|
catch (error) {
|
|
2041
2462
|
if (!this.#reviewPreparationIsCurrent(token))
|
|
@@ -2148,6 +2569,8 @@ export class TuiController {
|
|
|
2148
2569
|
this.#ensureOperationFlow(intent);
|
|
2149
2570
|
if (intent.kind === 'pluginWriteBatch')
|
|
2150
2571
|
assertPluginWriteBatchEvidence(evidence);
|
|
2572
|
+
if (intent.kind === 'pluginWriteTask')
|
|
2573
|
+
assertPluginWriteTaskEvidence(intent, evidence);
|
|
2151
2574
|
if (this.#router.snapshot.current.route.name === 'result')
|
|
2152
2575
|
navigation = 'replace';
|
|
2153
2576
|
const projection = buildOperationReviewProjection({
|
|
@@ -2164,10 +2587,12 @@ export class TuiController {
|
|
|
2164
2587
|
const expiresAt = now
|
|
2165
2588
|
+ (this.dependencies.reviewTtlMs ?? 2 * 60_000);
|
|
2166
2589
|
const pluginId = 'pluginId' in intent ? intent.pluginId
|
|
2167
|
-
: 'sourceId' in intent ? intent.sourceId
|
|
2590
|
+
: 'sourceId' in intent ? intent.sourceId ?? 'Catalog 管理'
|
|
2168
2591
|
: intent.kind === 'pluginWriteBatch'
|
|
2169
2592
|
? `${evidence.requestedPluginIds.length} 个插件`
|
|
2170
|
-
:
|
|
2593
|
+
: intent.kind === 'pluginWriteTask'
|
|
2594
|
+
? `${evidence.taskPlan.requestedTargets.length} 个安装位置`
|
|
2595
|
+
: 'Catalog 来源配置';
|
|
2171
2596
|
if ('pluginId' in intent)
|
|
2172
2597
|
sourceIdFromPluginId(intent.pluginId);
|
|
2173
2598
|
const pluginVersion = 'pluginId' in intent
|
|
@@ -2184,6 +2609,7 @@ export class TuiController {
|
|
|
2184
2609
|
? { sourceAction: intent.sourceMutation.action } : {}),
|
|
2185
2610
|
...(intent.kind === 'pluginWrite' ? { pluginAction: intent.action } : {}),
|
|
2186
2611
|
...(intent.kind === 'pluginWriteBatch' ? { pluginAction: intent.action } : {}),
|
|
2612
|
+
...(intent.kind === 'pluginWriteTask' ? { taskGoal: intent.goal } : {}),
|
|
2187
2613
|
...(intent.kind === 'pluginWrite' && intent.originIssue
|
|
2188
2614
|
? { originIssue: intent.originIssue }
|
|
2189
2615
|
: {}),
|
|
@@ -2204,6 +2630,15 @@ export class TuiController {
|
|
|
2204
2630
|
.filter((group) => group.executable).length,
|
|
2205
2631
|
},
|
|
2206
2632
|
} : {}),
|
|
2633
|
+
...(intent.kind === 'pluginWriteTask' ? {
|
|
2634
|
+
task: {
|
|
2635
|
+
goal: intent.goal,
|
|
2636
|
+
requestedTargetCount: evidence.taskPlan.requestedTargets.length,
|
|
2637
|
+
executableTargetCount: evidence.taskPlan.executableTargets.length,
|
|
2638
|
+
blockedTargetCount: evidence.taskPlan.blockedTargets.length,
|
|
2639
|
+
causeCount: evidence.taskPlan.causes.length,
|
|
2640
|
+
},
|
|
2641
|
+
} : {}),
|
|
2207
2642
|
...(evidence.source && 'catalogVersion' in evidence.source
|
|
2208
2643
|
&& typeof evidence.source.catalogVersion === 'string'
|
|
2209
2644
|
&& evidence.source.snapshotDigest
|
|
@@ -2267,7 +2702,9 @@ export class TuiController {
|
|
|
2267
2702
|
this.#progressFeed?.clearCurrent?.();
|
|
2268
2703
|
const dependencyTargets = ticket.intent.kind === 'pluginWriteBatch'
|
|
2269
2704
|
? ticket.evidence.groups.flatMap((group) => group.preview.dependencyPlan?.targets ?? [])
|
|
2270
|
-
: ticket.
|
|
2705
|
+
: ticket.intent.kind === 'pluginWriteTask'
|
|
2706
|
+
? ticket.evidence.taskPlan.sources.flatMap((source) => source.preview.dependencyPlan?.targets ?? [])
|
|
2707
|
+
: ticket.evidence.dependencyPlan?.targets ?? [];
|
|
2271
2708
|
this.#progressTargets = dependencyTargets.map((target) => ({
|
|
2272
2709
|
pluginId: target.pluginId,
|
|
2273
2710
|
platform: target.platform,
|
|
@@ -2285,15 +2722,18 @@ export class TuiController {
|
|
|
2285
2722
|
current: {
|
|
2286
2723
|
target: ticket.intent.kind === 'pluginWriteBatch'
|
|
2287
2724
|
? `${ticket.evidence.requestedPluginIds.length} 个插件`
|
|
2288
|
-
:
|
|
2289
|
-
|
|
2725
|
+
: ticket.intent.kind === 'pluginWriteTask'
|
|
2726
|
+
? `${ticket.evidence.taskPlan.executableTargets.length} 个安装位置`
|
|
2727
|
+
: 'pluginId' in ticket.intent ? ticket.intent.pluginId
|
|
2728
|
+
: 'sourceId' in ticket.intent ? ticket.intent.sourceId ?? 'Catalog 管理' : 'Catalog 来源配置',
|
|
2290
2729
|
phase: '验证当前状态',
|
|
2291
2730
|
},
|
|
2292
2731
|
};
|
|
2293
2732
|
this.#router.replace({ name: 'progress', operationId });
|
|
2294
2733
|
this.#publish();
|
|
2295
2734
|
try {
|
|
2296
|
-
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
|
|
2735
|
+
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
|
|
2736
|
+
&& ticket.intent.kind !== 'pluginWriteTask') {
|
|
2297
2737
|
await this.dependencies.validateIntent?.(structuredClone(ticket.intent), this.#abort.signal);
|
|
2298
2738
|
}
|
|
2299
2739
|
}
|
|
@@ -2325,6 +2765,7 @@ export class TuiController {
|
|
|
2325
2765
|
if (!this.#operationIsCurrent(generation))
|
|
2326
2766
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2327
2767
|
if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
|
|
2768
|
+
&& ticket.intent.kind !== 'pluginWriteTask'
|
|
2328
2769
|
&& ticket.intent.kind !== 'catalogGeneration') {
|
|
2329
2770
|
let current;
|
|
2330
2771
|
try {
|
|
@@ -2358,6 +2799,63 @@ export class TuiController {
|
|
|
2358
2799
|
...executionProjection,
|
|
2359
2800
|
sensitiveValues: collectOperationSensitiveValues(ticket.intent),
|
|
2360
2801
|
});
|
|
2802
|
+
if (ticket.intent.kind === 'catalogManagement') {
|
|
2803
|
+
if (ticket.intent.action === 'devActivate' || ticket.intent.action === 'devDeactivate') {
|
|
2804
|
+
this.dependencies.beginCatalogSession?.();
|
|
2805
|
+
generation = ++this.#generation;
|
|
2806
|
+
try {
|
|
2807
|
+
const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
|
|
2808
|
+
if (!this.#operationIsCurrent(generation)) {
|
|
2809
|
+
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2810
|
+
}
|
|
2811
|
+
this.#applyStatus(refreshed);
|
|
2812
|
+
}
|
|
2813
|
+
catch (reloadError) {
|
|
2814
|
+
if (!this.#operationIsCurrent(generation)) {
|
|
2815
|
+
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2816
|
+
}
|
|
2817
|
+
await this.#logErrorSafe(reloadError, 'tuiStateReloadFailed', {
|
|
2818
|
+
privateConfigOperation: true,
|
|
2819
|
+
});
|
|
2820
|
+
this.#status = undefined;
|
|
2821
|
+
this.#viewModel = undefined;
|
|
2822
|
+
this.#phase = 'error';
|
|
2823
|
+
this.#loadError = {
|
|
2824
|
+
code: 'tuiStateReloadFailed',
|
|
2825
|
+
message: 'Catalog 来源已变更,但插件状态刷新失败。请重新加载后再继续操作。',
|
|
2826
|
+
};
|
|
2827
|
+
return this.#setBlocked(operationId, 'tuiStateReloadFailed', {
|
|
2828
|
+
intent: ticket.intent,
|
|
2829
|
+
navigation: 'result',
|
|
2830
|
+
returnRoute: ticket.returnRoute,
|
|
2831
|
+
writeMayHaveCompleted: true,
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
const baseSummary = operationResultSummary(ticket.intent, execution);
|
|
2836
|
+
const summary = {
|
|
2837
|
+
...baseSummary,
|
|
2838
|
+
elapsedMs: Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs),
|
|
2839
|
+
messages: [...this.#progress.messages, ...(baseSummary.messages ?? [])].slice(-8)
|
|
2840
|
+
.map((message) => sanitizeDiagnosticText(message, [], collectOperationSensitiveValues(ticket.intent))),
|
|
2841
|
+
};
|
|
2842
|
+
const projection = this.#operationDiagnostics.get(operationId);
|
|
2843
|
+
if (projection)
|
|
2844
|
+
projection.effects.finalVerification = 'completed';
|
|
2845
|
+
this.#progress = { ...this.#progress, elapsedMs: summary.elapsedMs };
|
|
2846
|
+
const result = {
|
|
2847
|
+
state: summary.code === 'operationPartial' ? 'partial' : 'succeeded',
|
|
2848
|
+
operationId,
|
|
2849
|
+
summary,
|
|
2850
|
+
operationKind: ticket.intent.kind,
|
|
2851
|
+
returnRoute: structuredClone(this.#normalizeRoute(ticket.returnRoute)),
|
|
2852
|
+
};
|
|
2853
|
+
this.#result = result;
|
|
2854
|
+
this.#activeReview = undefined;
|
|
2855
|
+
this.#showResult(result, ticket.intent);
|
|
2856
|
+
this.#publish();
|
|
2857
|
+
return cloneFrozen(result);
|
|
2858
|
+
}
|
|
2361
2859
|
if (ticket.intent.kind === 'sourceMutation' && ticket.intent.action === 'update') {
|
|
2362
2860
|
const confirmedAutoCheck = catalogSourceAutoCheckOnlyValue(ticket.intent.update);
|
|
2363
2861
|
if (confirmedAutoCheck !== undefined) {
|
|
@@ -2453,6 +2951,17 @@ export class TuiController {
|
|
|
2453
2951
|
if (!this.#operationIsCurrent(generation))
|
|
2454
2952
|
return this.#detachedBlocked(operationId, ticket.intent);
|
|
2455
2953
|
this.#applyStatus(refreshed);
|
|
2954
|
+
if (ticket.intent.kind === 'catalogGeneration') {
|
|
2955
|
+
const completed = typeof execution === 'object' && execution !== null
|
|
2956
|
+
? execution.generation
|
|
2957
|
+
: undefined;
|
|
2958
|
+
const active = this.#catalogGenerations.get(ticket.intent.sourceId)?.active;
|
|
2959
|
+
if (completed && active
|
|
2960
|
+
&& active.catalogVersion === completed.resolution.catalogVersion
|
|
2961
|
+
&& active.resolvedCommit === completed.resolution.resolvedCommit) {
|
|
2962
|
+
this.#catalogSourcesSession.recordVerifiedGeneration(ticket.intent.sourceId, active);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2456
2965
|
const verifiedProjection = this.#operationDiagnostics.get(operationId);
|
|
2457
2966
|
if (verifiedProjection && verifiedProjection.effects.finalVerification !== 'failed') {
|
|
2458
2967
|
verifiedProjection.effects.finalVerification = 'completed';
|
|
@@ -2463,6 +2972,7 @@ export class TuiController {
|
|
|
2463
2972
|
}
|
|
2464
2973
|
const pluginSummary = ticket.intent.kind === 'pluginWrite'
|
|
2465
2974
|
|| ticket.intent.kind === 'pluginWriteBatch'
|
|
2975
|
+
|| ticket.intent.kind === 'pluginWriteTask'
|
|
2466
2976
|
? operationResultSummary(ticket.intent, execution)
|
|
2467
2977
|
: undefined;
|
|
2468
2978
|
const hasFailedDependencyTarget = pluginSummary?.targets?.some((target) => target.relation !== undefined && target.relation !== 'requested'
|
|
@@ -2503,9 +3013,10 @@ export class TuiController {
|
|
|
2503
3013
|
intent: ticket.intent,
|
|
2504
3014
|
navigation: 'result',
|
|
2505
3015
|
returnRoute: ticket.returnRoute,
|
|
2506
|
-
...(ticket.intent.kind === 'pluginWriteBatch' ||
|
|
3016
|
+
...(ticket.intent.kind === 'pluginWriteBatch' || ticket.intent.kind === 'pluginWriteTask'
|
|
3017
|
+
|| dependencyFailure || dependencyStillRequired
|
|
2507
3018
|
? { summary: pluginSummary } : {}),
|
|
2508
|
-
...(ticket.intent.kind !== 'pluginWriteBatch'
|
|
3019
|
+
...(ticket.intent.kind !== 'pluginWriteBatch' && ticket.intent.kind !== 'pluginWriteTask'
|
|
2509
3020
|
&& !dependencyFailure && !dependencyStillRequired && ['started', 'partial', 'completed', 'failed', 'unknown'].includes(this.#operationDiagnostics.get(operationId)?.effects.awWrite ?? 'unknown') ? { writeMayHaveCompleted: true } : {}),
|
|
2510
3021
|
});
|
|
2511
3022
|
}
|
|
@@ -2579,7 +3090,9 @@ export class TuiController {
|
|
|
2579
3090
|
: ticket.intent.kind === 'sourceMutation' && ticket.intent.action === 'update'
|
|
2580
3091
|
? { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
|
|
2581
3092
|
: ticket.intent.kind === 'catalogGeneration'
|
|
2582
|
-
?
|
|
3093
|
+
? ticket.returnRoute.name === 'pluginWriteTask'
|
|
3094
|
+
? ticket.returnRoute
|
|
3095
|
+
: { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
|
|
2583
3096
|
: ticket.returnRoute)),
|
|
2584
3097
|
};
|
|
2585
3098
|
if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
|
|
@@ -2620,6 +3133,14 @@ export class TuiController {
|
|
|
2620
3133
|
this.#operationDiagnostics.delete(operationId);
|
|
2621
3134
|
return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
|
|
2622
3135
|
}
|
|
3136
|
+
if (ticket.intent.kind === 'catalogManagement') {
|
|
3137
|
+
return this.#setBlocked(operationId, code, {
|
|
3138
|
+
intent: ticket.intent,
|
|
3139
|
+
navigation: 'result',
|
|
3140
|
+
returnRoute: ticket.returnRoute,
|
|
3141
|
+
writeMayHaveCompleted: true,
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
2623
3144
|
generation = ++this.#generation;
|
|
2624
3145
|
let finalVerification = 'completed';
|
|
2625
3146
|
try {
|
|
@@ -2723,6 +3244,30 @@ export class TuiController {
|
|
|
2723
3244
|
});
|
|
2724
3245
|
}
|
|
2725
3246
|
async #rebuildStaleReview(ticket, operationId, currentEvidence, generation = this.#generation) {
|
|
3247
|
+
if (ticket.intent.kind === 'catalogManagement') {
|
|
3248
|
+
let evidence = currentEvidence;
|
|
3249
|
+
try {
|
|
3250
|
+
evidence ??= await this.dependencies.captureEvidence(structuredClone(ticket.intent), this.#abort.signal);
|
|
3251
|
+
}
|
|
3252
|
+
catch (error) {
|
|
3253
|
+
const code = issueCode(error);
|
|
3254
|
+
await this.#logOperationError(error, code, ticket.intent);
|
|
3255
|
+
return this.#setBlocked(operationId, code, {
|
|
3256
|
+
intent: ticket.intent,
|
|
3257
|
+
returnRoute: ticket.returnRoute,
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
const review = this.#storeOperationReview(ticket.intent, evidence, 'replace', ticket.returnRoute);
|
|
3261
|
+
const result = {
|
|
3262
|
+
state: 'stale',
|
|
3263
|
+
operationId,
|
|
3264
|
+
review,
|
|
3265
|
+
...this.#operationIdentity(ticket.intent),
|
|
3266
|
+
};
|
|
3267
|
+
this.#result = result;
|
|
3268
|
+
this.#publish();
|
|
3269
|
+
return cloneFrozen(result);
|
|
3270
|
+
}
|
|
2726
3271
|
this.dependencies.beginCatalogSession?.();
|
|
2727
3272
|
generation = ++this.#generation;
|
|
2728
3273
|
try {
|
|
@@ -2874,6 +3419,22 @@ export class TuiController {
|
|
|
2874
3419
|
}
|
|
2875
3420
|
}
|
|
2876
3421
|
function operationResultSummaryUnsafe(intent, execution) {
|
|
3422
|
+
if (intent.kind === 'catalogManagement') {
|
|
3423
|
+
const result = execution;
|
|
3424
|
+
return {
|
|
3425
|
+
code: result.state === 'partial' ? 'operationPartial' : 'operationSucceeded',
|
|
3426
|
+
message: result.message,
|
|
3427
|
+
messages: result.messages,
|
|
3428
|
+
catalogManagement: {
|
|
3429
|
+
action: result.action,
|
|
3430
|
+
state: result.state,
|
|
3431
|
+
...(result.catalogVersion ? { catalogVersion: result.catalogVersion } : {}),
|
|
3432
|
+
...(result.planDigest ? { planDigest: result.planDigest } : {}),
|
|
3433
|
+
...(result.store ? { store: structuredClone(result.store) } : {}),
|
|
3434
|
+
...(result.publication ? { publication: structuredClone(result.publication) } : {}),
|
|
3435
|
+
},
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
2877
3438
|
if (intent.kind === 'catalogSourcesRecovery') {
|
|
2878
3439
|
const result = execution;
|
|
2879
3440
|
return {
|
|
@@ -2950,6 +3511,131 @@ function operationResultSummaryUnsafe(intent, execution) {
|
|
|
2950
3511
|
},
|
|
2951
3512
|
};
|
|
2952
3513
|
}
|
|
3514
|
+
if (intent.kind === 'pluginWriteTask') {
|
|
3515
|
+
const taskExecution = execution;
|
|
3516
|
+
const failedTargets = pluginWriteTaskFailedRequestedTargets(taskExecution);
|
|
3517
|
+
const failedKeys = new Set(failedTargets.map(pluginWriteTaskTargetKey));
|
|
3518
|
+
const technicalTargets = taskExecution.targetResults.map((entry) => {
|
|
3519
|
+
const placement = taskExecution.dependencyPlan?.targets.find((target) => target.pluginId === entry.target.pluginId && target.platform === entry.target.platform
|
|
3520
|
+
&& (target.requestedScope ?? target.resolvedScope) === entry.target.scope);
|
|
3521
|
+
return {
|
|
3522
|
+
pluginId: entry.target.pluginId,
|
|
3523
|
+
platform: entry.target.platform,
|
|
3524
|
+
scope: entry.target.scope,
|
|
3525
|
+
state: entry.action === 'skipped' ? 'skipped'
|
|
3526
|
+
: entry.state === 'failed' ? 'failed'
|
|
3527
|
+
: entry.action === 'keep' ? 'kept' : 'completed',
|
|
3528
|
+
relation: entry.relation === 'requested' ? 'requested'
|
|
3529
|
+
: placement?.depth === 1 ? 'direct' : 'transitive',
|
|
3530
|
+
requiredBy: entry.requiredBy,
|
|
3531
|
+
...(entry.errorCode ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') } : {}),
|
|
3532
|
+
};
|
|
3533
|
+
});
|
|
3534
|
+
const blocked = new Map(taskExecution.taskPlan.blockedTargets.map((entry) => [
|
|
3535
|
+
pluginWriteTaskTargetKey(entry.target), entry,
|
|
3536
|
+
]));
|
|
3537
|
+
const relatedResults = (target) => {
|
|
3538
|
+
const key = pluginWriteTaskTargetKey(target);
|
|
3539
|
+
const source = taskExecution.sourceResults.find((candidate) => candidate.requestedTargets.some((requested) => pluginWriteTaskTargetKey(requested) === key));
|
|
3540
|
+
const related = source?.result.targetResults.filter((entry) => {
|
|
3541
|
+
const pluginId = entry.target.pluginId;
|
|
3542
|
+
if (!pluginId)
|
|
3543
|
+
return false;
|
|
3544
|
+
const executionKey = pluginWriteTaskTargetKey({
|
|
3545
|
+
pluginId,
|
|
3546
|
+
platform: entry.target.platform,
|
|
3547
|
+
scope: entry.target.scope,
|
|
3548
|
+
});
|
|
3549
|
+
if (executionKey === key)
|
|
3550
|
+
return true;
|
|
3551
|
+
if (taskExecution.taskPlan.causes.some((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === executionKey
|
|
3552
|
+
&& cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === key)))
|
|
3553
|
+
return true;
|
|
3554
|
+
const placement = source.result.dependencyPlan?.targets.find((candidate) => candidate.pluginId === pluginId && candidate.platform === entry.target.platform
|
|
3555
|
+
&& (candidate.requestedScope ?? candidate.resolvedScope) === entry.target.scope);
|
|
3556
|
+
return placement?.requiredByTargets?.some((consumer) => pluginWriteTaskTargetKey(consumer) === key) ?? false;
|
|
3557
|
+
}) ?? [];
|
|
3558
|
+
const targetResults = related.map((entry) => {
|
|
3559
|
+
const placement = source?.result.dependencyPlan?.targets.find((candidate) => candidate.pluginId === entry.target.pluginId
|
|
3560
|
+
&& candidate.platform === entry.target.platform
|
|
3561
|
+
&& (candidate.requestedScope ?? candidate.resolvedScope) === entry.target.scope);
|
|
3562
|
+
return {
|
|
3563
|
+
pluginId: entry.target.pluginId,
|
|
3564
|
+
platform: entry.target.platform,
|
|
3565
|
+
scope: entry.target.scope,
|
|
3566
|
+
state: entry.action === 'skipped' ? 'skipped'
|
|
3567
|
+
: entry.state === 'failed' ? 'failed'
|
|
3568
|
+
: entry.action === 'keep' ? 'kept' : 'completed',
|
|
3569
|
+
relation: entry.relation === 'requested' ? 'requested'
|
|
3570
|
+
: placement?.depth === 1 ? 'direct' : 'transitive',
|
|
3571
|
+
...(entry.errorCode ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') } : {}),
|
|
3572
|
+
};
|
|
3573
|
+
});
|
|
3574
|
+
if (blocked.has(key) && !targetResults.some((entry) => entry.pluginId === target.pluginId && entry.platform === target.platform
|
|
3575
|
+
&& entry.scope === target.scope)) {
|
|
3576
|
+
targetResults.unshift({
|
|
3577
|
+
pluginId: target.pluginId,
|
|
3578
|
+
platform: target.platform,
|
|
3579
|
+
scope: target.scope,
|
|
3580
|
+
state: 'skipped',
|
|
3581
|
+
relation: 'requested',
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
const hasFailed = targetResults.some((entry) => entry.state === 'failed' || entry.state === 'skipped');
|
|
3585
|
+
const hasCompleted = targetResults.some((entry) => entry.state === 'completed' || entry.state === 'kept');
|
|
3586
|
+
const state = blocked.has(key) ? 'blocked'
|
|
3587
|
+
: !source ? 'notStarted'
|
|
3588
|
+
: hasFailed && hasCompleted ? 'partial'
|
|
3589
|
+
: hasFailed || failedKeys.has(key) ? 'failed'
|
|
3590
|
+
: targetResults.length > 0 && targetResults.every((entry) => entry.state === 'kept')
|
|
3591
|
+
? 'kept' : targetResults.length > 0 ? 'completed' : 'notStarted';
|
|
3592
|
+
return { state, targets: targetResults };
|
|
3593
|
+
};
|
|
3594
|
+
const roots = [...new Set(taskExecution.taskPlan.requestedTargets
|
|
3595
|
+
.map((target) => target.pluginId))].map((pluginId) => {
|
|
3596
|
+
const requestedTargets = taskExecution.taskPlan.requestedTargets.filter((target) => target.pluginId === pluginId);
|
|
3597
|
+
const positions = requestedTargets.map((target) => relatedResults(target));
|
|
3598
|
+
const successfulPositions = positions.filter((position) => position.state === 'completed' || position.state === 'kept');
|
|
3599
|
+
const state = successfulPositions.length > 0
|
|
3600
|
+
? successfulPositions.length === positions.length
|
|
3601
|
+
? positions.every((position) => position.state === 'kept') ? 'kept' : 'completed'
|
|
3602
|
+
: 'partial'
|
|
3603
|
+
: positions.some((position) => position.state === 'failed' || position.state === 'partial')
|
|
3604
|
+
? 'failed'
|
|
3605
|
+
: positions.some((position) => position.state === 'notStarted') ? 'notStarted' : 'blocked';
|
|
3606
|
+
const targets = [...new Map(positions.flatMap((position) => position.targets).map((target) => [[
|
|
3607
|
+
target.pluginId ?? '', target.platform, target.scope,
|
|
3608
|
+
].join('\0'), target])).values()];
|
|
3609
|
+
const causeKeys = [...new Set(taskExecution.taskPlan.causes.filter((cause) => cause.consumers.some((consumer) => consumer.pluginId === pluginId))
|
|
3610
|
+
.map((cause) => cause.causeKey))];
|
|
3611
|
+
return { pluginId, state, causeKeys, targets };
|
|
3612
|
+
});
|
|
3613
|
+
const retryTargets = canonicalPluginWriteTaskTargets(roots.flatMap((root) => root.state === 'failed' || root.state === 'partial' || root.state === 'notStarted'
|
|
3614
|
+
? failedTargets.filter((target) => target.pluginId === root.pluginId) : []));
|
|
3615
|
+
const completedCount = roots.filter((root) => root.state === 'completed'
|
|
3616
|
+
|| root.state === 'kept' || root.state === 'partial').length;
|
|
3617
|
+
const failedCount = roots.filter((root) => root.state === 'failed' || root.state === 'partial'
|
|
3618
|
+
|| root.state === 'notStarted' || root.state === 'blocked').length;
|
|
3619
|
+
const code = failedCount === 0 ? 'operationSucceeded'
|
|
3620
|
+
: completedCount > 0 ? 'operationPartial' : 'operationFailed';
|
|
3621
|
+
return {
|
|
3622
|
+
code,
|
|
3623
|
+
message: code === 'operationSucceeded' ? '任务已完成。'
|
|
3624
|
+
: code === 'operationPartial' ? '任务只完成了一部分。' : '任务没有完成。',
|
|
3625
|
+
warnings: taskExecution.warnings,
|
|
3626
|
+
recoveryActions: [...new Set(taskExecution.targetResults.flatMap((entry) => entry.recoveryActions ?? []))],
|
|
3627
|
+
...(taskExecution.operationPlan?.planFingerprint
|
|
3628
|
+
? { planFingerprint: taskExecution.operationPlan.planFingerprint } : {}),
|
|
3629
|
+
task: {
|
|
3630
|
+
goal: intent.goal,
|
|
3631
|
+
requestedTargets: taskExecution.taskPlan.requestedTargets,
|
|
3632
|
+
rootResults: roots,
|
|
3633
|
+
...(retryTargets.length > 0 ? { retryTargets } : {}),
|
|
3634
|
+
blockedTargets: taskExecution.taskPlan.blockedTargets,
|
|
3635
|
+
},
|
|
3636
|
+
targets: technicalTargets,
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
2953
3639
|
if (intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch') {
|
|
2954
3640
|
const command = intent.action === 'refreshAndSync' ? 'sync' : intent.action;
|
|
2955
3641
|
const rawExecution = typeof execution === 'object' && execution !== null
|
|
@@ -2995,6 +3681,52 @@ function operationResultSummaryUnsafe(intent, execution) {
|
|
|
2995
3681
|
? pluginWriteBatchFailedRequestedTargets(execution)
|
|
2996
3682
|
: [];
|
|
2997
3683
|
const failedRequestedPluginIds = canonicalPluginIds(retryTargets.map((target) => target.pluginId));
|
|
3684
|
+
const requestedTargetKeys = intent.kind === 'pluginWriteBatch'
|
|
3685
|
+
? new Set(execution.groupResults.flatMap((group) => {
|
|
3686
|
+
const roots = new Set(group.requestedPluginIds);
|
|
3687
|
+
return group.result.targetResults.flatMap((entry) => entry.target.pluginId
|
|
3688
|
+
&& roots.has(entry.target.pluginId)
|
|
3689
|
+
&& entry.target.scope === group.scope
|
|
3690
|
+
&& (group.platform === 'both' || entry.target.platform === group.platform)
|
|
3691
|
+
? [[entry.target.pluginId, entry.target.platform, entry.target.scope].join('\0')]
|
|
3692
|
+
: []);
|
|
3693
|
+
}))
|
|
3694
|
+
: new Set();
|
|
3695
|
+
const rootResults = intent.kind === 'pluginWriteBatch'
|
|
3696
|
+
? requestedPluginIds.map((pluginId) => {
|
|
3697
|
+
const observed = (result.targetResults ?? []).flatMap((entry) => entry.target.pluginId === pluginId
|
|
3698
|
+
&& requestedTargetKeys.has([
|
|
3699
|
+
pluginId, entry.target.platform, entry.target.scope,
|
|
3700
|
+
].join('\0')) ? [{
|
|
3701
|
+
pluginId,
|
|
3702
|
+
platform: entry.target.platform,
|
|
3703
|
+
scope: entry.target.scope,
|
|
3704
|
+
state: entry.action === 'skipped' ? 'skipped'
|
|
3705
|
+
: entry.state === 'failed' ? 'failed'
|
|
3706
|
+
: entry.action === 'keep' ? 'kept' : 'completed',
|
|
3707
|
+
relation: 'requested',
|
|
3708
|
+
...(entry.errorCode
|
|
3709
|
+
? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') }
|
|
3710
|
+
: {}),
|
|
3711
|
+
}] : []);
|
|
3712
|
+
const targets = [...new Map([
|
|
3713
|
+
...retryTargets.filter((target) => target.pluginId === pluginId).map((target) => ({
|
|
3714
|
+
...target,
|
|
3715
|
+
state: 'skipped',
|
|
3716
|
+
relation: 'requested',
|
|
3717
|
+
})),
|
|
3718
|
+
...observed,
|
|
3719
|
+
].map((target) => [[target.pluginId, target.platform, target.scope].join('\0'), target]))
|
|
3720
|
+
.values()];
|
|
3721
|
+
const completed = targets.filter((target) => target.state === 'completed' || target.state === 'kept').length;
|
|
3722
|
+
const failed = targets.length - completed;
|
|
3723
|
+
const state = completed > 0
|
|
3724
|
+
? failed > 0 ? 'partial'
|
|
3725
|
+
: targets.every((target) => target.state === 'kept') ? 'kept' : 'completed'
|
|
3726
|
+
: failed > 0 ? 'failed' : 'notStarted';
|
|
3727
|
+
return { pluginId, state, targets };
|
|
3728
|
+
})
|
|
3729
|
+
: [];
|
|
2998
3730
|
return {
|
|
2999
3731
|
code: result.state === 'succeeded' ? 'operationSucceeded'
|
|
3000
3732
|
: result.state === 'partial' ? 'operationPartial' : 'operationFailed',
|
|
@@ -3017,6 +3749,7 @@ function operationResultSummaryUnsafe(intent, execution) {
|
|
|
3017
3749
|
batch: {
|
|
3018
3750
|
requestedPluginIds,
|
|
3019
3751
|
failedRequestedPluginIds,
|
|
3752
|
+
rootResults,
|
|
3020
3753
|
retryTargets,
|
|
3021
3754
|
groupCount: execution.groupResults.length,
|
|
3022
3755
|
},
|