@lesliechan721/aw 0.1.0-beta.52 → 0.1.0-beta.53

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.
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { configReferenceKey, configReferenceValues, configReferenceScalars, configReferenceSelection, configReferenceMessage, configCrossLayerDependencies } from './config-references.js';
2
3
  import { arrayDraftChanged, arrayDraftValue, createConfigArrayDraft, editConfigArrayDraft } from './config-array-draft.js';
3
4
  import { PLUGIN_CONFIG_ARRAY_MAX_ITEMS } from '@lesliechan721/aw-plugin-core';
4
5
  import { presentConfigValue, safeConfigText } from './config-value-presentation.js';
@@ -1013,6 +1014,8 @@ export function ConsoleAppV4(props) {
1013
1014
  const [configLayerCounts, setConfigLayerCounts] = useState({});
1014
1015
  const [configLayerPages, setConfigLayerPages] = useState({});
1015
1016
  const [configDrafts, setConfigDrafts] = useState({});
1017
+ const [referenceContexts, setReferenceContexts] = useState({});
1018
+ const [referenceContextError, setReferenceContextError] = useState();
1016
1019
  const [configCandidate, setConfigCandidate] = useState();
1017
1020
  const [arrayDrafts, setArrayDrafts] = useState({});
1018
1021
  const [arrayToolsOpen, setArrayToolsOpen] = useState(false);
@@ -1092,7 +1095,8 @@ export function ConsoleAppV4(props) {
1092
1095
  const draft = configDrafts[draftScope];
1093
1096
  return count + Object.keys(draft?.updates ?? {}).length + (draft?.removes.length ?? 0);
1094
1097
  }, 0);
1095
- const draftUpdates = {
1098
+ const editingScope = 'layer' in route && route.layer && route.layer.scope !== 'effective' ? route.layer.scope : undefined;
1099
+ const draftUpdates = editingScope ? configDrafts[editingScope]?.updates ?? {} : {
1096
1100
  ...(configDrafts.user?.updates ?? {}),
1097
1101
  ...(configDrafts.project?.updates ?? {}),
1098
1102
  };
@@ -1380,7 +1384,7 @@ export function ConsoleAppV4(props) {
1380
1384
  if (currentFlowId && sourceDraft)
1381
1385
  externalDraftFlowIds.current.set(sourceDraft.owner, currentFlowId);
1382
1386
  const drafts = [];
1383
- if ((dirty || configCandidate || nestedPropertyRevision || objectVariantPreview || Object.values(arrayDrafts).some((draft) => draft.initial === undefined ? draft.rows.length > 0 : arrayDraftChanged(draft)) || changedObjectDraftKeys.length > 0) && draftOwner) {
1387
+ if ((dirty || configCandidate && !configCandidate.selectingScope || nestedPropertyRevision || objectVariantPreview || Object.values(arrayDrafts).some((draft) => draft.initial === undefined ? draft.rows.length > 0 : arrayDraftChanged(draft)) || changedObjectDraftKeys.length > 0) && draftOwner) {
1384
1388
  const flowId = externalDraftFlowIds.current.get(draftOwner);
1385
1389
  drafts.push({
1386
1390
  kind: 'configDraft', owner: draftOwner, revision: externalDraftRevision.current.config,
@@ -1406,7 +1410,30 @@ export function ConsoleAppV4(props) {
1406
1410
  configDrafts, configCandidate, arrayDrafts, objectDrafts, nestedPropertyRevision, objectVariantPreview, route.name, snapshot.router.currentFlowId, sourceDraft,
1407
1411
  ]);
1408
1412
  useEffect(() => () => props.controller.setExternalDraftState(), [props.controller]);
1409
- const currentConfigPage = configPageMatches(configPage, configRoute) ? configPage : undefined;
1413
+ const currentConfigPage = configPageMatches(configPage, configRoute) ? configPage
1414
+ : configRoute && configRoute.layer.scope !== 'effective' ? configLayerPages[configRoute.layer.scope] : undefined;
1415
+ const hasReferenceFields = (configPage ?? configLayerPages.user ?? configLayerPages.project)?.groups
1416
+ .some((group) => group.fields.some((field) => configReferenceScalars(field.definition).length > 0)) ?? false;
1417
+ useEffect(() => {
1418
+ if (!configFlowPluginId || !hasReferenceFields)
1419
+ return;
1420
+ const abort = new AbortController();
1421
+ setReferenceContexts({});
1422
+ setReferenceContextError(undefined);
1423
+ void Promise.allSettled(['user', 'project'].map(async (scope) => ({ scope,
1424
+ context: await props.controller.getConfigReferenceContext(configFlowPluginId, scope, configDrafts[scope] ?? { updates: {}, removes: [] }, abort.signal),
1425
+ }))).then((results) => {
1426
+ if (abort.signal.aborted)
1427
+ return;
1428
+ setReferenceContexts(Object.fromEntries(results.flatMap((result) => result.status === 'fulfilled'
1429
+ ? [[result.value.scope, result.value.context]] : [])));
1430
+ if (results.some((result) => result.status === 'rejected'))
1431
+ setReferenceContextError('引用来源读取失败。请返回修复来源配置后重新打开设置。');
1432
+ });
1433
+ return () => abort.abort();
1434
+ }, [props.controller, configFlowPluginId, configDrafts, editingScope, hasReferenceFields]);
1435
+ const crossLayerDependencies = configCrossLayerDependencies(Object.fromEntries((configPage?.groups ?? []).flatMap((group) => group.fields.map((field) => [field.key, field.definition]))), configDrafts, referenceContexts);
1436
+ const referenceContext = editingScope ? referenceContexts[editingScope] : undefined;
1410
1437
  const configDraftIssues = (() => {
1411
1438
  const issues = [];
1412
1439
  for (const draftScope of ['project', 'user']) {
@@ -1744,9 +1771,11 @@ export function ConsoleAppV4(props) {
1744
1771
  useEffect(() => {
1745
1772
  if (!activeArrayKey || activeArrayDraft || !activeTopField || route.name !== 'configFieldEditor')
1746
1773
  return;
1774
+ if (route.layer.scope === 'effective' && configReferenceScalars(activeTopField.definition).length)
1775
+ return;
1747
1776
  const value = draftUpdates[activeTopField.key] ?? activeTopField.draftValue ?? activeTopField.value;
1748
1777
  setArrayDrafts((current) => ({ ...current, [activeArrayKey]: { ...createConfigArrayDraft(Array.isArray(value) ? value : undefined), evidence: currentConfigPage?.editEvidence } }));
1749
- }, [activeArrayKey, activeArrayDraft, activeTopField, route.name]);
1778
+ }, [activeArrayKey, activeArrayDraft, activeTopField, route.name, editingScope]);
1750
1779
  useEffect(() => {
1751
1780
  if (route.name !== 'configFieldEditor' || route.editor !== 'array' || !activeArrayKey || !activeArrayDraft)
1752
1781
  return;
@@ -1811,6 +1840,15 @@ export function ConsoleAppV4(props) {
1811
1840
  || activePropertyDefinition?.sensitive === true;
1812
1841
  const activeFieldIsConst = activeField && 'const' in activeField.definition && activeField.definition.const !== undefined;
1813
1842
  const activePropertyIsConst = activePropertyDefinition && 'const' in activePropertyDefinition && activePropertyDefinition.const !== undefined;
1843
+ const referenceNotice = (scalar, value) => {
1844
+ const selection = referenceContext?.selections[configReferenceKey(scalar)];
1845
+ const message = referenceContextError && !selection ? referenceContextError : configReferenceMessage(scalar, selection, value);
1846
+ const source = 'valueRef' in scalar ? scalar.valueRef.field : '';
1847
+ const pendingSource = editingScope === 'project' && selection?.source !== 'project'
1848
+ && (Object.hasOwn(configDrafts.user?.updates ?? {}, source) || configDrafts.user?.removes.includes(source));
1849
+ return [message, pendingSource ? `来源 ${source} 在个人默认有未保存修改。请先保存来源配置,再编辑或保存引用配置。` : undefined]
1850
+ .filter(Boolean).join(' ') || undefined;
1851
+ };
1814
1852
  useEffect(() => {
1815
1853
  if (route.name !== 'configFieldEditor' || !activeField)
1816
1854
  return;
@@ -2973,8 +3011,8 @@ export function ConsoleAppV4(props) {
2973
3011
  const fieldEditor = (field) => {
2974
3012
  return field.type;
2975
3013
  };
2976
- const fieldEditorMode = (field) => (field.choices?.length ?? 0) > 0 ? { mode: 'choices' } : {};
2977
- const openConfigTarget = (candidate) => {
3014
+ const fieldEditorMode = (field) => (field.choices?.length ?? 0) > 0 || 'valueRef' in field.definition && field.definition.valueRef ? { mode: 'choices' } : {};
3015
+ const openConfigTarget = (candidate, replace = false) => {
2978
3016
  const currentFlowId = snapshot.router.currentFlowId;
2979
3017
  const storedAnchor = configOverviewAnchors.current.get(candidate.pluginId);
2980
3018
  const storedOverviewFrameId = storedAnchor && storedAnchor.flowId === currentFlowId
@@ -2986,18 +3024,32 @@ export function ConsoleAppV4(props) {
2986
3024
  ...(currentFlowId ? { flowId: currentFlowId } : {}),
2987
3025
  ...(overviewFrameId ? { overviewFrameId } : {}),
2988
3026
  });
2989
- go({
3027
+ const targetRoute = {
2990
3028
  name: 'configLayer',
2991
3029
  pluginId: candidate.pluginId,
2992
3030
  ...(candidate.fieldKey ? { fieldKey: candidate.fieldKey } : {}),
2993
3031
  ...(candidate.groupId ? { groupId: candidate.groupId } : {}),
2994
3032
  ...(candidate.presetId ? { presetId: candidate.presetId } : {}),
2995
- });
3033
+ };
3034
+ if (replace)
3035
+ props.controller.replace(targetRoute);
3036
+ else
3037
+ go(targetRoute);
2996
3038
  };
2997
3039
  const updateDraft = (key, value) => {
2998
3040
  if (!configRoute || !currentConfigPage)
2999
3041
  return;
3000
3042
  setConfigActionError(undefined);
3043
+ if (activeField && editingScope) {
3044
+ for (const { scalar, value: referencedValue } of configReferenceValues(activeField.definition, value)) {
3045
+ const selected = referenceContext?.selections[configReferenceKey(scalar)];
3046
+ if (!selected || selected.issues.length || !selected.choices.some((choice) => choice.value === referencedValue)) {
3047
+ setObjectError(referenceNotice(scalar, referencedValue));
3048
+ setInputError(referenceNotice(scalar, referencedValue));
3049
+ return;
3050
+ }
3051
+ }
3052
+ }
3001
3053
  if (arrayItem && activeArrayKey && activeArrayDraft && activeField) {
3002
3054
  const issues = validatePluginConfigValue(activeField.definition, value, key);
3003
3055
  if (issues.length) {
@@ -3021,13 +3073,18 @@ export function ConsoleAppV4(props) {
3021
3073
  back();
3022
3074
  return;
3023
3075
  }
3024
- openConfigTarget({
3076
+ const candidate = {
3025
3077
  pluginId: configRoute.pluginId,
3026
3078
  fieldKey: key,
3027
3079
  groupId: snapshot.router.current.context.configGroupId,
3028
3080
  mutations: { [key]: { kind: 'set', value } },
3029
3081
  ...(activeArrayDraft?.evidence ? { editEvidence: activeArrayDraft.evidence } : {}),
3030
- });
3082
+ };
3083
+ if (editingScope && activeTopField && configReferenceScalars(activeTopField.definition).length) {
3084
+ openConfigLayerTarget({ scope: editingScope }, candidate);
3085
+ }
3086
+ else
3087
+ openConfigTarget(candidate);
3031
3088
  };
3032
3089
  const editArray = (edit) => {
3033
3090
  if (!activeArrayKey || !activeArrayDraft)
@@ -3046,7 +3103,7 @@ export function ConsoleAppV4(props) {
3046
3103
  go({
3047
3104
  name: 'configFieldEditor', pluginId: configRoute.pluginId, layer: configRoute.layer,
3048
3105
  fieldKey: activeField.key, editor: activeField.definition.items.type,
3049
- ...(activeField.definition.items.choices ? { mode: 'choices' } : {}),
3106
+ ...(activeField.definition.items.choices || 'valueRef' in activeField.definition.items && activeField.definition.items.valueRef ? { mode: 'choices' } : {}),
3050
3107
  arrayItem: { rowId },
3051
3108
  });
3052
3109
  };
@@ -3070,12 +3127,16 @@ export function ConsoleAppV4(props) {
3070
3127
  if (!configRoute)
3071
3128
  return;
3072
3129
  setConfigActionError(undefined);
3073
- openConfigTarget({
3130
+ const candidate = {
3074
3131
  pluginId: configRoute.pluginId,
3075
3132
  fieldKey: key,
3076
3133
  groupId: snapshot.router.current.context.configGroupId,
3077
3134
  mutations: { [key]: { kind: 'unset' } },
3078
- });
3135
+ };
3136
+ if (editingScope && activeTopField && configReferenceScalars(activeTopField.definition).length)
3137
+ openConfigLayerTarget({ scope: editingScope }, candidate);
3138
+ else
3139
+ openConfigTarget(candidate);
3079
3140
  };
3080
3141
  const updateObjectDraft = (value) => {
3081
3142
  if (!activeObjectKey)
@@ -3110,7 +3171,7 @@ export function ConsoleAppV4(props) {
3110
3171
  setChangedObjectDraftKeys((current) => current.filter((key) => key !== activeObjectKey));
3111
3172
  }
3112
3173
  };
3113
- const objectPropertyStage = (definition) => definition.type === 'boolean' || definition.choices ? 'choices' : 'custom';
3174
+ const objectPropertyStage = (definition) => definition.type === 'boolean' || definition.choices || 'valueRef' in definition && definition.valueRef ? 'choices' : 'custom';
3114
3175
  const openObjectProperty = (propertyKey, dynamic) => {
3115
3176
  if (!activeField || activeField.definition.type !== 'object')
3116
3177
  return;
@@ -3118,13 +3179,13 @@ export function ConsoleAppV4(props) {
3118
3179
  ?? (activeField.definition.additionalProperties === false
3119
3180
  ? undefined
3120
3181
  : activeField.definition.additionalProperties);
3121
- if (!definition || 'const' in definition && definition.const !== undefined)
3182
+ if (!definition || 'const' in definition && definition.const !== undefined && !('valueRef' in definition && definition.valueRef))
3122
3183
  return;
3123
3184
  const exists = activeObjectDraft && Object.hasOwn(activeObjectDraft, propertyKey);
3124
3185
  go({
3125
3186
  name: 'configObjectPropertyEditor',
3126
3187
  pluginId: configRoute.pluginId,
3127
- layer: { scope: 'effective' },
3188
+ layer: configRoute.layer,
3128
3189
  fieldKey: activeField.key,
3129
3190
  propertyKey,
3130
3191
  dynamic,
@@ -3207,17 +3268,45 @@ export function ConsoleAppV4(props) {
3207
3268
  mutations,
3208
3269
  });
3209
3270
  };
3210
- const openConfigLayerTarget = (layer) => {
3211
- if (route.name !== 'configLayer')
3271
+ const openConfigLayerTarget = (layer, explicitCandidate) => {
3272
+ const candidate = explicitCandidate ?? configCandidate;
3273
+ if (!candidate)
3212
3274
  return;
3213
- if (!configCandidate || configCandidate.pluginId !== route.pluginId)
3275
+ const selectedCandidate = { ...candidate,
3276
+ flowId: candidate.flowId ?? snapshot.router.currentFlowId,
3277
+ overviewFrameId: candidate.overviewFrameId ?? configOverviewAnchors.current.get(candidate.pluginId)?.overviewFrameId,
3278
+ };
3279
+ if (selectedCandidate.selectingScope) {
3280
+ const field = configLayerPages[layer.scope]?.groups.flatMap((group) => group.fields).find((field) => field.key === selectedCandidate.fieldKey);
3281
+ if (!field || !configFieldCanSet(field))
3282
+ return;
3283
+ if (field.definition.type === 'array') {
3284
+ const owner = `${candidate.pluginId}\0${field.key}`;
3285
+ const working = arrayDrafts[owner];
3286
+ if (working) {
3287
+ const stored = configDrafts[layer.scope]?.updates[field.key] ?? field.draftValue ?? field.value;
3288
+ setArrayDrafts((current) => ({ ...current, [owner]: { ...working,
3289
+ initial: Array.isArray(stored) ? structuredClone(stored) : undefined,
3290
+ evidence: configLayerPages[layer.scope]?.editEvidence,
3291
+ } }));
3292
+ }
3293
+ }
3294
+ const available = ['user', 'project'].filter((scope) => configLayerPages[scope]?.groups
3295
+ .flatMap((group) => group.fields).some((entry) => entry.key === field.key && configFieldCanSet(entry)));
3296
+ const next = { name: 'configFieldEditor', pluginId: candidate.pluginId, layer, fieldKey: field.key,
3297
+ editor: fieldEditor(field), ...fieldEditorMode(field) };
3298
+ if (available.length === 1)
3299
+ props.controller.replace(next);
3300
+ else
3301
+ go(next);
3214
3302
  return;
3303
+ }
3215
3304
  const page = configLayerPages[layer.scope];
3216
3305
  if (!page)
3217
3306
  return;
3218
3307
  const fields = new Map(page.groups.flatMap((group) => group.fields)
3219
3308
  .map((field) => [field.key, field]));
3220
- const valid = Object.entries(configCandidate.mutations).every(([key, mutation]) => {
3309
+ const valid = Object.entries(selectedCandidate.mutations).every(([key, mutation]) => {
3221
3310
  const field = fields.get(key);
3222
3311
  return Boolean(field && (mutation.kind === 'set' ? configFieldCanSet(field) : configFieldCanUnset(field)));
3223
3312
  });
@@ -3229,10 +3318,10 @@ export function ConsoleAppV4(props) {
3229
3318
  const updates = { ...previous.updates };
3230
3319
  const removes = new Set(previous.removes);
3231
3320
  const editEvidence = { ...previous.editEvidence };
3232
- for (const [key, mutation] of Object.entries(configCandidate.mutations)) {
3321
+ for (const [key, mutation] of Object.entries(selectedCandidate.mutations)) {
3233
3322
  const field = fields.get(key);
3234
- if (configCandidate.editEvidence)
3235
- editEvidence[key] = configCandidate.editEvidence;
3323
+ if (selectedCandidate.editEvidence)
3324
+ editEvidence[key] = selectedCandidate.editEvidence;
3236
3325
  else
3237
3326
  delete editEvidence[key];
3238
3327
  if (mutation.kind === 'set') {
@@ -3255,20 +3344,23 @@ export function ConsoleAppV4(props) {
3255
3344
  const next = { ...configDrafts, [layer.scope]: { updates, removes: [...removes], editEvidence } };
3256
3345
  if (Object.keys(updates).length === 0 && removes.size === 0)
3257
3346
  delete next[layer.scope];
3258
- const target = configCandidate.fieldKey
3259
- ? configFieldTarget(configPage ?? page, configCandidate.fieldKey)
3260
- : configCandidate.groupId ? { groupId: configCandidate.groupId, focusIndex: 0 } : undefined;
3261
- const returned = props.controller.returnToConfigOverview(configCandidate.flowId, configCandidate.overviewFrameId, route.pluginId, target ? { configGroupId: target.groupId, focusIndex: target.focusIndex, scrollOffset: 0 } : undefined);
3347
+ const target = selectedCandidate.fieldKey
3348
+ ? configFieldTarget(configPage ?? page, selectedCandidate.fieldKey)
3349
+ : selectedCandidate.groupId ? { groupId: selectedCandidate.groupId, focusIndex: 0 } : undefined;
3350
+ const returned = props.controller.returnToConfigOverview(selectedCandidate.flowId, selectedCandidate.overviewFrameId, selectedCandidate.pluginId, target ? { configGroupId: target.groupId, focusIndex: target.focusIndex, scrollOffset: 0 } : undefined);
3262
3351
  if (!returned) {
3263
3352
  setConfigActionError('设置总览已变化。请返回插件详情后重新打开设置。');
3264
3353
  return;
3265
3354
  }
3266
- if (!configCandidate.flowId) {
3267
- props.controller.replace({ name: 'configFields', pluginId: route.pluginId, layer: { scope: 'effective' } }, target ? { configGroupId: target.groupId, focusIndex: target.focusIndex, scrollOffset: 0 } : undefined);
3355
+ if (!selectedCandidate.flowId) {
3356
+ props.controller.replace({ name: 'configFields', pluginId: selectedCandidate.pluginId, layer: { scope: 'effective' } }, target ? { configGroupId: target.groupId, focusIndex: target.focusIndex, scrollOffset: 0 } : undefined);
3268
3357
  }
3269
- if (configCandidate.fieldKey) {
3270
- const owner = `${configCandidate.pluginId}\0${configCandidate.fieldKey}`;
3358
+ if (selectedCandidate.fieldKey) {
3359
+ const owner = `${selectedCandidate.pluginId}\0${selectedCandidate.fieldKey}`;
3360
+ const belongsToField = (key) => key === owner || key.startsWith(`${owner}\0`);
3271
3361
  setArrayDrafts((current) => Object.fromEntries(Object.entries(current).filter(([key]) => key !== owner)));
3362
+ setObjectDrafts((current) => Object.fromEntries(Object.entries(current).filter(([key]) => !belongsToField(key))));
3363
+ setChangedObjectDraftKeys((current) => current.filter((key) => !belongsToField(key)));
3272
3364
  }
3273
3365
  setConfigDrafts(next);
3274
3366
  setDirty(Object.keys(next).length > 0);
@@ -3284,6 +3376,8 @@ export function ConsoleAppV4(props) {
3284
3376
  if (!page)
3285
3377
  return false;
3286
3378
  const fields = page.groups.flatMap((group) => group.fields);
3379
+ if (configCandidate.selectingScope)
3380
+ return fields.some((field) => field.key === configCandidate.fieldKey && configFieldCanSet(field));
3287
3381
  return Object.entries(configCandidate.mutations).every(([key, mutation]) => {
3288
3382
  const field = fields.find((entry) => entry.key === key);
3289
3383
  return field && (mutation.kind === 'set' ? configFieldCanSet(field) : configFieldCanUnset(field));
@@ -3291,9 +3385,16 @@ export function ConsoleAppV4(props) {
3291
3385
  });
3292
3386
  if (scopes.length === 1) {
3293
3387
  openConfigLayerTarget({ scope: scopes[0] });
3294
- setConfigActionError(`仅可保存到${configLayerLabel({ scope: scopes[0] })};已加入待保存修改。`);
3388
+ if (!configCandidate.selectingScope)
3389
+ setConfigActionError(`仅可保存到${configLayerLabel({ scope: scopes[0] })};已加入待保存修改。`);
3295
3390
  }
3296
3391
  }, [route.name, configCandidate, configLayerPages]);
3392
+ useEffect(() => {
3393
+ if (route.name !== 'configFieldEditor' || route.layer.scope !== 'effective' || arrayItem || !activeTopField
3394
+ || !configReferenceScalars(activeTopField.definition).length)
3395
+ return;
3396
+ openConfigTarget({ pluginId: route.pluginId, fieldKey: activeTopField.key, selectingScope: true, mutations: {} }, true);
3397
+ }, [route.name, route.name === 'configFieldEditor' ? route.layer.scope : '', activeTopField?.key]);
3297
3398
  const configChanges = (page, draft) => {
3298
3399
  const fields = page.groups.flatMap((group) => group.fields);
3299
3400
  return [...Object.keys(draft.updates), ...draft.removes.filter((key) => !Object.hasOwn(draft.updates, key))]
@@ -3427,6 +3528,16 @@ export function ConsoleAppV4(props) {
3427
3528
  };
3428
3529
  };
3429
3530
  const beginConfigSave = async (apply) => {
3531
+ if (crossLayerDependencies.length) {
3532
+ setConfigActionError(`请先保存来源配置,再编辑或保存引用配置:${[...new Set(crossLayerDependencies.map((entry) => entry.source))].join('、')}(个人默认)。请取消相关引用草稿后保存来源。`);
3533
+ return;
3534
+ }
3535
+ if (['user', 'project'].some((scope) => !referenceContexts[scope]
3536
+ && Object.keys(configDrafts[scope]?.updates ?? {}).some((key) => configPage?.groups.flatMap((group) => group.fields)
3537
+ .some((field) => field.key === key && configReferenceScalars(field.definition).length)))) {
3538
+ setConfigActionError(referenceContextError ?? '正在读取引用来源,请稍后保存。');
3539
+ return;
3540
+ }
3430
3541
  if (configDraftIssues.length > 0) {
3431
3542
  setConfigActionError('字段已变化。请先放弃失效草稿,或返回继续检查来源。');
3432
3543
  return;
@@ -4494,7 +4605,7 @@ export function ConsoleAppV4(props) {
4494
4605
  { label: '自动选择可用 Agent', description: '按当前环境确定可用 Agent,不承诺全部 Agent 可用', meta: '默认', selected: snapshot.portableDraft.agentMode === 'automatic', run: () => { props.controller.setPortableAgentMode('automatic'); go({ name: 'portableScope', pluginId: plugin.pluginId }); } },
4495
4606
  { label: '指定 Agent', description: '输入一个或多个 Agent ID', selected: snapshot.portableDraft.agentMode === 'explicit', run: () => go({ name: 'portableAgentInput', pluginId: plugin.pluginId }) },
4496
4607
  ];
4497
- content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
4608
+ content = _jsx(SemanticRows, { actions: [...actions], focus: focus, kind: "choice", compact: policy.mode === 'compact' });
4498
4609
  }
4499
4610
  else if (route.name === 'portableAgentInput' && plugin) {
4500
4611
  breadcrumb = `${plugin.displayName} / 通用 Skills / Agent`;
@@ -5748,7 +5859,7 @@ export function ConsoleAppV4(props) {
5748
5859
  : candidateSummaryLines[0];
5749
5860
  title = selectedField ? `“${selectedField.title}”保存到哪里?`
5750
5861
  : selectedPreset ? `“${selectedPreset.title}”保存到哪里?` : '这个值保存到哪里?';
5751
- description = selectedField
5862
+ description = configCandidate?.selectingScope ? '先选择保存位置,再按该层配置选择引用值。' : selectedField
5752
5863
  ? `${selectedField.sensitive ? '敏感值只能保存到个人设置。' : '选择后回到分组总览;磁盘尚未改变。'}`
5753
5864
  : selectedPreset ? '选择后回到分组总览;磁盘尚未改变。' : '请返回重新选择值。';
5754
5865
  if (configActionError)
@@ -5759,6 +5870,8 @@ export function ConsoleAppV4(props) {
5759
5870
  if (!page || !configCandidate)
5760
5871
  return false;
5761
5872
  const fields = page.groups.flatMap((group) => group.fields);
5873
+ if (configCandidate.selectingScope)
5874
+ return fields.some((field) => field.key === configCandidate.fieldKey && configFieldCanSet(field));
5762
5875
  return Object.entries(configCandidate.mutations).every(([key, mutation]) => {
5763
5876
  const field = fields.find((candidate) => candidate.key === key);
5764
5877
  return Boolean(field && (mutation.kind === 'set' ? configFieldCanSet(field) : configFieldCanUnset(field)));
@@ -5834,12 +5947,31 @@ export function ConsoleAppV4(props) {
5834
5947
  };
5835
5948
  })(),
5836
5949
  configKey: field.key,
5837
- run: () => go({
5838
- name: 'configFieldEditor', pluginId: plugin.pluginId, layer: mutableLayer,
5839
- fieldKey: field.key, editor: fieldEditor(field), ...fieldEditorMode(field),
5840
- }),
5950
+ run: () => configReferenceScalars(field.definition).length
5951
+ ? openConfigTarget({ pluginId: plugin.pluginId, fieldKey: field.key, selectingScope: true, mutations: {} })
5952
+ : go({ name: 'configFieldEditor', pluginId: plugin.pluginId, layer: mutableLayer,
5953
+ fieldKey: field.key, editor: fieldEditor(field), ...fieldEditorMode(field) }),
5841
5954
  }));
5842
5955
  const operationActions = [
5956
+ ...(crossLayerDependencies.length ? [{
5957
+ label: '取消跨层引用草稿,保留来源修改',
5958
+ description: '先保存个人默认来源,再重新编辑当前项目引用。',
5959
+ run: () => {
5960
+ const keys = new Set(crossLayerDependencies.map((entry) => entry.key));
5961
+ const next = { ...configDrafts };
5962
+ const project = next.project;
5963
+ const updates = Object.fromEntries(Object.entries(project.updates).filter(([key]) => !keys.has(key)));
5964
+ if (Object.keys(updates).length || project.removes.length)
5965
+ next.project = { ...project, updates };
5966
+ else
5967
+ delete next.project;
5968
+ setConfigDrafts(next);
5969
+ setObjectDrafts({});
5970
+ setArrayDrafts({});
5971
+ setChangedObjectDraftKeys([]);
5972
+ setConfigActionError('已取消相关引用草稿。请先保存来源,再重新编辑引用。');
5973
+ },
5974
+ }] : []),
5843
5975
  ...(configDraftIssues.length > 0 ? [{
5844
5976
  label: '放弃无法保存的修改',
5845
5977
  description: `${configDraftIssues.length} 项未保存修改已不再适用于当前插件设置;放弃后不会修改已保存设置。`,
@@ -5882,7 +6014,7 @@ export function ConsoleAppV4(props) {
5882
6014
  beginConfigSave(true);
5883
6015
  },
5884
6016
  };
5885
- bottomActions = [configSaveAction, {
6017
+ bottomActions = [configSaveAction, ...(crossLayerDependencies.length ? operationActions.slice(0, 1) : []), {
5886
6018
  label: '设置选项', run: () => go({ ...route, view: 'options' }),
5887
6019
  }];
5888
6020
  actions = [...tabActions, ...bottomActions];
@@ -5993,7 +6125,8 @@ export function ConsoleAppV4(props) {
5993
6125
  }
5994
6126
  else if (route.name === 'configFieldEditor' && plugin && activeField) {
5995
6127
  breadcrumb = `${plugin.displayName} / 设置 / ${activeField.title}`;
5996
- const showChoices = (activeField.choices?.length ?? 0) > 0 && route.mode !== 'custom';
6128
+ const scalarReference = 'valueRef' in activeField.definition && activeField.definition.valueRef ? activeField.definition : undefined;
6129
+ const showChoices = Boolean(scalarReference) || (activeField.choices?.length ?? 0) > 0 && route.mode !== 'custom';
5997
6130
  const targetFields = ['project', 'user'].flatMap((scope) => configLayerPages[scope]?.groups.flatMap((group) => group.fields).filter((field) => field.key === activeField.key) ?? []);
5998
6131
  const canSet = targetFields.some(configFieldCanSet);
5999
6132
  const canUnset = !arrayItem && targetFields.some(configFieldCanUnset);
@@ -6003,6 +6136,16 @@ export function ConsoleAppV4(props) {
6003
6136
  actions = canUnset ? [{ label: '删除此覆盖并恢复继承', run: () => removeDraft(activeField.key) }] : [];
6004
6137
  content = _jsxs(Box, { flexDirection: "column", children: [_jsx(StatusCard, { status: "warning", title: "\u4EC5\u53EF\u5220\u9664\u65E7\u503C", description: description }), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
6005
6138
  }
6139
+ else if (scalarReference) {
6140
+ const selection = configReferenceSelection(referenceContext, scalarReference);
6141
+ const current = arrayItem ? itemValue : draftUpdates[activeField.key] ?? activeField.draftValue ?? activeField.value;
6142
+ title = `选择${activeField.title}`;
6143
+ description = referenceNotice(scalarReference, current) ?? `选项来自 ${scalarReference.valueRef.field};保存到${editingScope === 'user' ? '个人默认' : '当前项目'}。`;
6144
+ help = '↑↓ 移动 · Enter 使用 · Esc 返回修复来源';
6145
+ actions = selection.choices.map((choice) => ({ label: safeConfigText(choice.title), selected: choice.value === current,
6146
+ run: () => updateDraft(activeField.key, choice.value) }));
6147
+ content = _jsx(SemanticRows, { actions: [...actions], focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6148
+ }
6006
6149
  else if ('const' in activeField.definition && activeField.definition.const !== undefined) {
6007
6150
  title = activeField.title;
6008
6151
  description = '这个值由插件固定,不能编辑。';
@@ -6047,7 +6190,7 @@ export function ConsoleAppV4(props) {
6047
6190
  },
6048
6191
  }] : []),
6049
6192
  ];
6050
- content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6193
+ content = _jsx(SemanticRows, { actions: [...actions], focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6051
6194
  }
6052
6195
  else if (route.editor === 'array' && activeField.definition.type === 'array') {
6053
6196
  title = activeField.title;
@@ -6079,7 +6222,7 @@ export function ConsoleAppV4(props) {
6079
6222
  ? activeCandidateMutation.value
6080
6223
  : (arrayItem ? itemValue : draftUpdates[activeField.key]) ?? activeField.value;
6081
6224
  actions = [{ label: '开启', selected: !activeField.sensitive && current === true, run: () => updateDraft(activeField.key, true) }, { label: '关闭', selected: !activeField.sensitive && current === false, run: () => updateDraft(activeField.key, false) }];
6082
- content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6225
+ content = _jsx(SemanticRows, { actions: [...actions], focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6083
6226
  }
6084
6227
  else if (route.editor === 'object' && activeField.definition.type === 'object' && activeObjectDraft) {
6085
6228
  const objectDefinition = activeField.definition;
@@ -6112,10 +6255,10 @@ export function ConsoleAppV4(props) {
6112
6255
  label: definition.title ?? property,
6113
6256
  description: definition.description,
6114
6257
  meta: propertySummary(property, definition),
6115
- disabled: 'const' in definition && definition.const !== undefined
6258
+ disabled: 'const' in definition && definition.const !== undefined && !('valueRef' in definition && definition.valueRef)
6116
6259
  && !(definition.required && !Object.hasOwn(activeObjectDraft, property)) ? '插件固定值,不能编辑' : undefined,
6117
6260
  run: () => {
6118
- if ('const' in definition && definition.const !== undefined) {
6261
+ if ('const' in definition && definition.const !== undefined && !('valueRef' in definition && definition.valueRef)) {
6119
6262
  updateObjectDraft({ ...activeObjectDraft, [property]: definition.const });
6120
6263
  }
6121
6264
  else
@@ -6182,6 +6325,10 @@ export function ConsoleAppV4(props) {
6182
6325
  : activeField.sensitive ? '当前值不会回显;留空不会自动删除旧值。' : activeField.description ?? '';
6183
6326
  content = _jsx(InputCard, { focused: true, title: _jsx(InputDisplay, { value: inputValue, sensitive: activeField.sensitive, placeholder: activeField.placeholder, columns: Math.max(8, size.columns - 8) }), description: inputError ?? (activeField.sensitive ? '输入已隐藏。' : activeField.placeholder ?? '按 Enter 使用输入值。'), status: inputError ? 'error' : undefined });
6184
6327
  }
6328
+ if (editingScope === 'user' && activeTopField && configReferenceScalars(activeTopField.definition).length
6329
+ && configLayerPages.project?.groups.flatMap((group) => group.fields).some((field) => field.key === activeTopField.key && field.configured)) {
6330
+ description = `${description ?? ''} 个人默认在当前项目被覆盖。`;
6331
+ }
6185
6332
  if (canUnset) {
6186
6333
  const unset = { label: '恢复继承', description: '选择要删除覆盖值的位置', run: () => removeDraft(activeField.key) };
6187
6334
  const inputEditor = canSet && !showChoices && (route.editor === 'number' || route.editor === 'string');
@@ -6218,6 +6365,18 @@ export function ConsoleAppV4(props) {
6218
6365
  } }, { label: '取消切换', run: () => { setObjectVariantPreview(undefined); back(); } }];
6219
6366
  content = _jsxs(Box, { flexDirection: "column", children: [readingPage(readingText, size.columns - 2, Math.max(1, pageActionBudget(4))), _jsx(SemanticRows, { actions: actions, focus: focus, kind: "action", compact: true })] });
6220
6367
  }
6368
+ else if (property !== undefined && definition && 'valueRef' in definition && definition.valueRef
6369
+ && route.stage !== 'action' && route.stage !== 'key') {
6370
+ const selection = configReferenceSelection(referenceContext, definition);
6371
+ const current = activeObjectDraft[property];
6372
+ title = `选择${propertyTitle}`;
6373
+ description = referenceNotice(definition, current) ?? `选项来自 ${definition.valueRef.field}。`;
6374
+ help = '↑↓ 移动 · Enter 使用 · Esc 返回修复来源';
6375
+ actions = selection.choices.map((choice) => ({ label: safeConfigText(choice.title), selected: choice.value === current,
6376
+ run: () => { if (updateObjectProperty(property, choice.value))
6377
+ back(); } }));
6378
+ content = _jsx(SemanticRows, { actions: [...actions], focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6379
+ }
6221
6380
  else if (property !== undefined && definition && 'const' in definition && definition.const !== undefined) {
6222
6381
  title = propertyTitle;
6223
6382
  description = '这个值由插件固定,不能编辑。';
@@ -6312,13 +6471,13 @@ export function ConsoleAppV4(props) {
6312
6471
  },
6313
6472
  }] : []),
6314
6473
  ];
6315
- content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6474
+ content = _jsx(SemanticRows, { actions: [...actions], focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6316
6475
  }
6317
6476
  else if (property !== undefined && definition && (definition.type === 'array' || definition.type === 'object')) {
6318
6477
  title = `编辑${propertyTitle}`;
6319
6478
  description = '确认后更新当前对象草稿;Esc 取消此属性的全部修改。';
6320
6479
  help = '↑↓ 移动 · Enter 编辑 · a 操作 · c 使用 · Esc 取消';
6321
- content = _jsx(ConfigLeafEditor, { definition: definition, value: activeObjectDraft[property], propertyKey: property, columns: size.columns, rowBudget: pageActionBudget(1), active: !overlay && !tooSmall && !busy, error: inputError, onDraftChange: setNestedPropertyRevision, onCancel: back, onConfirm: (value) => { if (!updateObjectProperty(property, value))
6480
+ content = _jsx(ConfigLeafEditor, { definition: definition, value: activeObjectDraft[property], propertyKey: property, referenceContext: referenceContext, referenceNotice: referenceNotice, columns: size.columns, rowBudget: pageActionBudget(1), active: !overlay && !tooSmall && !busy, error: inputError, onDraftChange: setNestedPropertyRevision, onCancel: back, onConfirm: (value) => { if (!updateObjectProperty(property, value))
6322
6481
  return false; back(); return true; } }, `${snapshot.router.currentFrameId}:${property}`);
6323
6482
  }
6324
6483
  else if (property !== undefined && definition) {
@@ -6886,7 +7045,7 @@ export function ConsoleAppV4(props) {
6886
7045
  { label: 'Git 仓库', description: '使用现有自动检查默认值;可在修改汇总前单独更改', run: () => selectType('git') },
6887
7046
  { label: '本地目录', description: '使用现有自动检查默认值;可在修改汇总前单独更改', run: () => selectType('path') },
6888
7047
  ];
6889
- content = _jsx(SemanticRows, { actions: actions, focus: focus, kind: "choice", compact: policy.mode === 'compact' });
7048
+ content = _jsx(SemanticRows, { actions: [...actions], focus: focus, kind: "choice", compact: policy.mode === 'compact' });
6890
7049
  }
6891
7050
  else if (route.step === 'refPolicy') {
6892
7051
  title = '选择版本来源';
@@ -9976,6 +10135,7 @@ export function ConsoleAppV4(props) {
9976
10135
  }
9977
10136
  if (route.name === 'configFieldEditor'
9978
10137
  && !activeFieldIsConst
10138
+ && !(activeField && 'valueRef' in activeField.definition && activeField.definition.valueRef)
9979
10139
  && (route.editor === 'string' || route.editor === 'number')
9980
10140
  && ((activeField?.choices?.length ?? 0) === 0 || route.mode === 'custom')
9981
10141
  && ['project', 'user'].some((scope) => configLayerPages[scope]?.groups.flatMap((group) => group.fields)
@@ -10024,6 +10184,7 @@ export function ConsoleAppV4(props) {
10024
10184
  && (route.stage === 'key' || route.stage === 'custom')
10025
10185
  && !activeVariantPreview
10026
10186
  && !activePropertyIsConst
10187
+ && !(activePropertyDefinition && 'valueRef' in activePropertyDefinition && activePropertyDefinition.valueRef && route.stage !== 'key')
10027
10188
  && activeField?.definition.type === 'object'
10028
10189
  && activeObjectDraft) {
10029
10190
  if (key.escape) {
@@ -10302,6 +10463,13 @@ export function ConsoleAppV4(props) {
10302
10463
  back();
10303
10464
  }
10304
10465
  else if (globalAction === 'back' && route.name === 'configLayer') {
10466
+ if (configCandidate?.selectingScope) {
10467
+ setConfigCandidate(undefined);
10468
+ const owner = `${route.pluginId}\0${route.fieldKey}`;
10469
+ setObjectDrafts((current) => Object.fromEntries(Object.entries(current).filter(([key]) => !key.startsWith(owner))));
10470
+ setArrayDrafts((current) => Object.fromEntries(Object.entries(current).filter(([key]) => key !== owner)));
10471
+ setChangedObjectDraftKeys((current) => current.filter((key) => !key.startsWith(owner)));
10472
+ }
10305
10473
  back();
10306
10474
  }
10307
10475
  else if (globalAction === 'back'
@@ -10310,12 +10478,17 @@ export function ConsoleAppV4(props) {
10310
10478
  }
10311
10479
  else if (globalAction === 'back'
10312
10480
  && (route.name === 'configFieldEditor' || route.name === 'configFieldAction')) {
10313
- setConfigCandidate(undefined);
10314
- if (activeArrayKey && !arrayItem) {
10481
+ const returnToReferenceScope = !arrayItem && editingScope && activeTopField
10482
+ && configReferenceScalars(activeTopField.definition).length > 0
10483
+ && ['user', 'project'].filter((scope) => configLayerPages[scope]?.groups.flatMap((group) => group.fields)
10484
+ .some((field) => field.key === activeTopField.key && configFieldCanSet(field))).length > 1;
10485
+ if (!returnToReferenceScope)
10486
+ setConfigCandidate(undefined);
10487
+ if (activeArrayKey && !arrayItem && !returnToReferenceScope) {
10315
10488
  setArrayDrafts((current) => Object.fromEntries(Object.entries(current).filter(([key]) => key !== activeArrayKey)));
10316
10489
  setObjectError(undefined);
10317
10490
  }
10318
- if (activeObjectKey) {
10491
+ if (activeObjectKey && !returnToReferenceScope) {
10319
10492
  setObjectDrafts((current) => Object.fromEntries(Object.entries(current)
10320
10493
  .filter(([key]) => key !== activeObjectKey)));
10321
10494
  setChangedObjectDraftKeys((current) => current.filter((key) => key !== activeObjectKey));