Files
MyGoNavi/frontend/src/utils/jvmAiPlan.ts
Syngnat fa4f2a938a 🐛 fix(jvm): 绑定前端变更执行到预览上下文
将 JVM 资源变更执行绑定到最近一次成功预览和连接配置指纹,并遮蔽敏感快照、payload 示例和 AI 上下文中的敏感值。
2026-04-28 09:42:48 +08:00

326 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { JVMActionDefinition, JVMChangeRequest, JVMAIPlanContext, JVMValueSnapshot, TabData } from '../types';
import { JVM_SENSITIVE_VALUE_MASK } from './jvmResourcePresentation';
export type JVMAIChangePlan = {
targetType: 'cacheEntry' | 'managedBean' | 'attribute' | 'operation';
selector: {
namespace?: string;
key?: string;
resourcePath?: string;
};
action: string;
payload?: {
format: 'json' | 'text';
value: unknown;
};
reason: string;
};
export type JVMAIChangeDraft = Pick<JVMChangeRequest, 'resourceId' | 'action' | 'reason' | 'source' | 'payload'>;
type JVMAIPlanPromptContext = {
connectionName: string;
host?: string;
providerMode: 'jmx' | 'endpoint' | 'agent';
resourcePath: string;
readOnly: boolean;
environment?: string;
snapshot?: JVMValueSnapshot | null;
};
const planFencePattern = /```json\s*([\s\S]*?)```/gi;
const allowedTargetTypes = new Set<JVMAIChangePlan['targetType']>(['cacheEntry', 'managedBean', 'attribute', 'operation']);
const allowedPayloadFormats = new Set<NonNullable<JVMAIChangePlan['payload']>['format']>(['json', 'text']);
const asTrimmedString = (value: unknown): string => String(value ?? '').trim();
const isRecord = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === 'object' && !Array.isArray(value);
const normalizeSelector = (value: unknown): JVMAIChangePlan['selector'] | null => {
if (!isRecord(value)) {
return null;
}
const selector: JVMAIChangePlan['selector'] = {};
const namespace = asTrimmedString(value.namespace);
const key = asTrimmedString(value.key);
const resourcePath = asTrimmedString(value.resourcePath);
if (namespace) {
selector.namespace = namespace;
}
if (key) {
selector.key = key;
}
if (resourcePath) {
selector.resourcePath = resourcePath;
}
return selector.namespace || selector.key || selector.resourcePath ? selector : null;
};
const normalizePayload = (value: unknown): JVMAIChangePlan['payload'] | undefined => {
if (value == null) {
return undefined;
}
if (!isRecord(value)) {
return undefined;
}
const format = asTrimmedString(value.format) as NonNullable<JVMAIChangePlan['payload']>['format'];
if (!allowedPayloadFormats.has(format)) {
return undefined;
}
return {
format,
value: value.value,
};
};
const normalizePlan = (value: unknown): JVMAIChangePlan | null => {
if (!isRecord(value)) {
return null;
}
const targetType = asTrimmedString(value.targetType) as JVMAIChangePlan['targetType'];
const action = asTrimmedString(value.action) as JVMAIChangePlan['action'];
const reason = asTrimmedString(value.reason);
const selector = normalizeSelector(value.selector);
const payload = normalizePayload(value.payload);
if (!allowedTargetTypes.has(targetType) || !action || !reason || !selector) {
return null;
}
return {
targetType,
selector,
action,
payload,
reason,
};
};
const formatSnapshotValue = (snapshot?: JVMValueSnapshot | null): string => {
if (!snapshot) {
return '当前资源快照尚未加载成功。';
}
if (snapshot.sensitive) {
return JVM_SENSITIVE_VALUE_MASK;
}
if (typeof snapshot.value === 'string') {
return snapshot.value;
}
try {
return JSON.stringify(snapshot.value ?? null, null, 2);
} catch {
return String(snapshot.value);
}
};
export const extractJVMChangePlan = (content: string): JVMAIChangePlan | null => {
const source = String(content || '');
planFencePattern.lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = planFencePattern.exec(source)) !== null) {
try {
const parsed = JSON.parse(match[1]);
const normalized = normalizePlan(parsed);
if (normalized) {
return normalized;
}
} catch {
// Ignore malformed JSON blocks and continue scanning.
}
}
return null;
};
export const resolveJVMAIPlanResourceId = (plan: JVMAIChangePlan): string => {
const resourcePath = asTrimmedString(plan.selector.resourcePath);
if (resourcePath) {
return resourcePath;
}
const namespace = asTrimmedString(plan.selector.namespace);
const key = asTrimmedString(plan.selector.key);
return [namespace, key].filter(Boolean).join('/');
};
export const matchesJVMAIPlanTargetTab = (
tab: Pick<TabData, 'type' | 'connectionId' | 'providerMode' | 'resourcePath'>,
context?: JVMAIPlanContext,
): boolean => {
if (!context || tab.type !== 'jvm-resource') {
return false;
}
const providerMode = (tab.providerMode || 'jmx') as JVMAIPlanContext['providerMode'];
return (
tab.connectionId === context.connectionId &&
providerMode === context.providerMode &&
asTrimmedString(tab.resourcePath) === asTrimmedString(context.resourcePath)
);
};
export const resolveJVMAIPlanTargetTabId = (tabs: TabData[], context?: JVMAIPlanContext): string => {
if (!context) {
return '';
}
const exactMatch = tabs.find((tab) => tab.id === context.tabId && matchesJVMAIPlanTargetTab(tab, context));
if (exactMatch) {
return exactMatch.id;
}
const fallbackMatch = tabs.find((tab) => matchesJVMAIPlanTargetTab(tab, context));
return fallbackMatch?.id || '';
};
export const buildJVMChangeDraftFromAIPlan = (plan: JVMAIChangePlan): JVMAIChangeDraft => {
const resourceId = resolveJVMAIPlanResourceId(plan);
if (!resourceId) {
throw new Error('AI 计划缺少可用的资源定位信息');
}
const reason = asTrimmedString(plan.reason);
if (!reason) {
throw new Error('AI 计划缺少变更原因');
}
const action = asTrimmedString(plan.action);
if (!action) {
throw new Error('AI 计划缺少可执行 action');
}
if (plan.action === 'updateValue') {
const value = plan.payload?.value;
if (plan.payload?.format !== 'json' || !isRecord(value)) {
throw new Error('当前 JVM 预览要求 payload 仍然是 JSON 对象');
}
return {
resourceId,
action: 'put',
reason,
source: 'ai-plan',
payload: value as Record<string, any>,
};
}
const payloadValue = plan.payload?.value;
if (plan.payload && plan.payload.format === 'json') {
if (!isRecord(payloadValue)) {
throw new Error('当前 JVM 预览要求 payload 仍然是 JSON 对象');
}
return {
resourceId,
action,
reason,
source: 'ai-plan',
payload: payloadValue as Record<string, any>,
};
}
if (plan.payload && plan.payload.format === 'text') {
return {
resourceId,
action,
reason,
source: 'ai-plan',
payload: {
value: payloadValue == null ? '' : String(payloadValue),
},
};
}
return {
resourceId,
action,
reason,
source: 'ai-plan',
payload: {},
};
};
const formatSupportedActions = (actions?: JVMActionDefinition[]): string => {
if (!actions || actions.length === 0) {
return '当前资源未声明支持动作。若要生成计划,请仅在你能从快照内容中明确推断时给出 action并保持 payload 为 JSON 对象。';
}
return actions
.map((item) => {
const payloadFields = Array.isArray(item.payloadFields) && item.payloadFields.length > 0
? `payload 字段:${item.payloadFields.map((field) => `${field.name}${field.required ? '(required)' : ''}`).join('、')}`
: '';
return `- ${item.action}${item.label ? ` (${item.label})` : ''}${item.description ? `${item.description}` : ''}${payloadFields}`;
})
.join('\n');
};
export const buildJVMAIPlanPrompt = ({
connectionName,
host,
providerMode,
resourcePath,
readOnly,
environment,
snapshot,
}: JVMAIPlanPromptContext): string => {
const normalizedPath = asTrimmedString(resourcePath) || '(未提供资源路径)';
const snapshotFormat = asTrimmedString(snapshot?.format) || 'json';
const environmentLabel = asTrimmedString(environment) || 'unknown';
const supportedActionsText = formatSupportedActions(snapshot?.supportedActions);
return [
'请分析下面这个 JVM 资源,并生成一个可用于 GoNavi “预览变更” 的结构化修改计划。',
'',
`连接名称:${connectionName}`,
`目标主机:${asTrimmedString(host) || '-'}`,
`Provider 模式:${providerMode}`,
`运行环境:${environmentLabel}`,
`连接策略:${readOnly ? '只读连接,当前只能生成计划和风险分析,不能假设已执行' : '可写连接,但仍必须先预览再人工确认'}`,
`当前资源路径:${normalizedPath}`,
'',
'当前资源快照:',
`\`\`\`${snapshotFormat}`,
formatSnapshotValue(snapshot),
'```',
'',
'当前资源支持动作:',
supportedActionsText,
'',
'输出要求:',
'1. 可以先给一小段分析,但必须包含且只包含一个 ```json 代码块。',
'2. 代码块里的 JSON 字段必须严格是targetType、selector、action、payload、reason。',
`3. selector.resourcePath 优先使用当前资源路径 ${normalizedPath},不要凭空编造其他路径。`,
'4. action 优先从“当前资源支持动作”里选择;如果当前资源未声明支持动作,才允许基于快照内容推断。',
'5. payload 只能使用 JSON 对象包装,不要输出脚本、命令或原始二进制。若需要纯文本值,也请包装成 {"format":"text","value":"..."}。',
'6. 不要声称已经执行修改,也不要输出脚本或命令。',
'',
'JSON 示例:',
'```json',
JSON.stringify(
{
targetType: 'cacheEntry',
selector: {
resourcePath: normalizedPath,
},
action: 'put',
payload: {
format: 'json',
value: {
status: 'ACTIVE',
},
},
reason: '修复缓存脏值',
},
null,
2,
),
'```',
].join('\n');
};