mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-01 10:59:34 +08:00
🐛 fix(jvm): 修正 AI 计划映射与页签定向应用
- 为 JVM AI 计划补充显式草稿映射,避免 payload 包装层直接透传到现有变更契约 - 将 updateValue 映射为当前 JVM 写入链路的 put,并限制为 JSON 对象 payload - 为 AI 聊天消息绑定 JVM 来源上下文,按 tab/connection/provider/resource 定向应用计划 - 补充 JVM AI 计划解析、契约映射和目标页签解析单测 - 更新需求追踪并回填 go test、前端测试、构建与 wails build 验证结果
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { extractJVMChangePlan } from './jvmAiPlan';
|
||||
import { buildJVMChangeDraftFromAIPlan, extractJVMChangePlan, resolveJVMAIPlanResourceId, resolveJVMAIPlanTargetTabId } from './jvmAiPlan';
|
||||
|
||||
describe('extractJVMChangePlan', () => {
|
||||
it('parses fenced json plan with namespace and key selector', () => {
|
||||
@@ -15,6 +15,7 @@ describe('extractJVMChangePlan', () => {
|
||||
expect(plan?.action).toBe('updateValue');
|
||||
expect(plan?.selector.namespace).toBe('orders');
|
||||
expect(plan?.selector.key).toBe('user:1');
|
||||
expect(plan ? resolveJVMAIPlanResourceId(plan) : '').toBe('orders/user:1');
|
||||
});
|
||||
|
||||
it('parses fenced json plan with explicit resource path', () => {
|
||||
@@ -40,3 +41,115 @@ describe('extractJVMChangePlan', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildJVMChangeDraftFromAIPlan', () => {
|
||||
it('maps updateValue plan to current JVM change contract', () => {
|
||||
const plan = extractJVMChangePlan(
|
||||
'```json\n{"targetType":"cacheEntry","selector":{"namespace":"orders","key":"user:1"},"action":"updateValue","payload":{"format":"json","value":{"status":"ACTIVE"}},"reason":"修复缓存脏值"}\n```',
|
||||
);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
expect(buildJVMChangeDraftFromAIPlan(plan!)).toEqual({
|
||||
resourceId: 'orders/user:1',
|
||||
action: 'put',
|
||||
reason: '修复缓存脏值',
|
||||
payload: {
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('maps clear plan without leaking wrapper payload fields', () => {
|
||||
const plan = extractJVMChangePlan(
|
||||
'```json\n{"targetType":"managedBean","selector":{"resourcePath":"/cache/orders"},"action":"clear","reason":"受控清理"}\n```',
|
||||
);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
expect(buildJVMChangeDraftFromAIPlan(plan!)).toEqual({
|
||||
resourceId: '/cache/orders',
|
||||
action: 'clear',
|
||||
reason: '受控清理',
|
||||
payload: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects non-object update payload values for current preview contract', () => {
|
||||
const plan = extractJVMChangePlan(
|
||||
'```json\n{"targetType":"cacheEntry","selector":{"resourcePath":"/cache/orders"},"action":"updateValue","payload":{"format":"text","value":"ACTIVE"},"reason":"修复缓存脏值"}\n```',
|
||||
);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
expect(() => buildJVMChangeDraftFromAIPlan(plan!)).toThrow('当前 JVM 预览仅支持 JSON 对象作为变更 payload');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveJVMAIPlanTargetTabId', () => {
|
||||
it('prefers the original tab when message context still matches', () => {
|
||||
expect(
|
||||
resolveJVMAIPlanTargetTabId(
|
||||
[
|
||||
{
|
||||
id: 'tab-orders',
|
||||
title: 'orders',
|
||||
type: 'jvm-resource',
|
||||
connectionId: 'conn-orders',
|
||||
providerMode: 'endpoint',
|
||||
resourcePath: '/cache/orders/user:1',
|
||||
},
|
||||
],
|
||||
{
|
||||
tabId: 'tab-orders',
|
||||
connectionId: 'conn-orders',
|
||||
providerMode: 'endpoint',
|
||||
resourcePath: '/cache/orders/user:1',
|
||||
},
|
||||
),
|
||||
).toBe('tab-orders');
|
||||
});
|
||||
|
||||
it('falls back to a reopened tab with the same JVM context', () => {
|
||||
expect(
|
||||
resolveJVMAIPlanTargetTabId(
|
||||
[
|
||||
{
|
||||
id: 'tab-orders-reopened',
|
||||
title: 'orders',
|
||||
type: 'jvm-resource',
|
||||
connectionId: 'conn-orders',
|
||||
providerMode: 'endpoint',
|
||||
resourcePath: '/cache/orders/user:1',
|
||||
},
|
||||
],
|
||||
{
|
||||
tabId: 'tab-orders-old',
|
||||
connectionId: 'conn-orders',
|
||||
providerMode: 'endpoint',
|
||||
resourcePath: '/cache/orders/user:1',
|
||||
},
|
||||
),
|
||||
).toBe('tab-orders-reopened');
|
||||
});
|
||||
|
||||
it('rejects tabs that only match the current session but not the original JVM context', () => {
|
||||
expect(
|
||||
resolveJVMAIPlanTargetTabId(
|
||||
[
|
||||
{
|
||||
id: 'tab-other-resource',
|
||||
title: 'orders-other',
|
||||
type: 'jvm-resource',
|
||||
connectionId: 'conn-orders',
|
||||
providerMode: 'endpoint',
|
||||
resourcePath: '/cache/orders/user:2',
|
||||
},
|
||||
],
|
||||
{
|
||||
tabId: 'tab-orders',
|
||||
connectionId: 'conn-orders',
|
||||
providerMode: 'endpoint',
|
||||
resourcePath: '/cache/orders/user:1',
|
||||
},
|
||||
),
|
||||
).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { JVMValueSnapshot } from '../types';
|
||||
import type { JVMChangeRequest, JVMAIPlanContext, JVMValueSnapshot, TabData } from '../types';
|
||||
|
||||
export type JVMAIChangePlan = {
|
||||
targetType: 'cacheEntry' | 'managedBean';
|
||||
@@ -15,6 +15,8 @@ export type JVMAIChangePlan = {
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type JVMAIChangeDraft = Pick<JVMChangeRequest, 'resourceId' | 'action' | 'reason' | 'payload'>;
|
||||
|
||||
type JVMAIPlanPromptContext = {
|
||||
connectionName: string;
|
||||
host?: string;
|
||||
@@ -135,6 +137,79 @@ export const extractJVMChangePlan = (content: string): JVMAIChangePlan | null =>
|
||||
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 计划缺少变更原因');
|
||||
}
|
||||
|
||||
if (plan.action === 'updateValue') {
|
||||
const value = plan.payload?.value;
|
||||
if (plan.payload?.format !== 'json' || !isRecord(value)) {
|
||||
throw new Error('当前 JVM 预览仅支持 JSON 对象作为变更 payload');
|
||||
}
|
||||
return {
|
||||
resourceId,
|
||||
action: 'put',
|
||||
reason,
|
||||
payload: value as Record<string, any>,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
resourceId,
|
||||
action: plan.action,
|
||||
reason,
|
||||
payload: {},
|
||||
};
|
||||
};
|
||||
|
||||
export const buildJVMAIPlanPrompt = ({
|
||||
connectionName,
|
||||
host,
|
||||
@@ -168,7 +243,7 @@ export const buildJVMAIPlanPrompt = ({
|
||||
'2. 代码块里的 JSON 字段必须严格是:targetType、selector、action、payload、reason。',
|
||||
`3. selector.resourcePath 优先使用当前资源路径 ${normalizedPath},不要凭空编造其他路径。`,
|
||||
'4. action 只能使用 updateValue、evict、clear 之一。',
|
||||
'5. payload 必须保持对象结构,例如 {"format":"json","value":{"status":"ACTIVE"}}。',
|
||||
'5. 当前 MVP 只支持 JSON 对象变更:如果 action=updateValue,则 payload 必须是 {"format":"json","value":{...}},且 value 必须是 JSON 对象;evict/clear 时 payload 可以省略。',
|
||||
'6. 不要声称已经执行修改,也不要输出脚本或命令。',
|
||||
'',
|
||||
'JSON 示例:',
|
||||
|
||||
Reference in New Issue
Block a user