mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-07 06:13:03 +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:
@@ -26,9 +26,9 @@
|
||||
- [x] 阶段 2(影响分析):完成
|
||||
- [x] 阶段 3(方案设计):完成(已形成正式设计文档)
|
||||
- [x] 阶段 4(实施计划):完成(已形成正式实施计划)
|
||||
- [ ] 阶段 5(实现与自检):进行中(Task 1 至 Task 5 已完成并通过回归,Task 6/7 待继续)
|
||||
- [ ] 阶段 6(评审与交付):
|
||||
- [ ] 阶段 7(发布与观察):
|
||||
- [x] 阶段 5(实现与自检):完成(Task 1 至 Task 7 已完成,代码与构建回归通过)
|
||||
- [x] 阶段 6(评审与交付):完成(已完成契约复核、上下文隔离修正、文档回填与交付检查)
|
||||
- [ ] 阶段 7(发布与观察):未开始
|
||||
|
||||
## 4. 变更清单
|
||||
- 已完成:
|
||||
@@ -45,10 +45,14 @@
|
||||
- 已完成 Task 3:JVM 连接表单、图标与展示文案接入
|
||||
- 已完成 Task 4:只读资源浏览与 JVM Tab
|
||||
- 已完成 Task 5:写入预览、Guard 和审计记录
|
||||
- 进行中:
|
||||
- Task 6:AI 结构化变更计划
|
||||
- 已完成 Task 6:AI 结构化变更计划
|
||||
- 已完成 Task 7:全量回归、文档回填与交付检查
|
||||
- 已完成 JVM AI 计划解析、资源定位解析、AI 计划到当前 JVM 变更草稿的显式映射,避免把 `payload.format/value` 包装层直接透传到现有 JVM 写入契约
|
||||
- 已完成 AI 聊天面板 JVM 上下文注入、AI 气泡“应用到 JVM 预览”入口以及 JVM 资源页草稿回填闭环
|
||||
- 已完成 JVM AI 计划来源上下文绑定:消息现在绑定生成时的 `tabId + connectionId + providerMode + resourcePath`,避免切换 JVM 页签后误投递到当前激活页
|
||||
- 待处理:
|
||||
- Task 7:全量回归、文档回填与交付检查
|
||||
- 真实 provider 的 `PreviewChange` / `ApplyChange` 端到端能力验证
|
||||
- AI 参与信息写入 JVM 审计记录
|
||||
|
||||
## 5. 风险与阻塞
|
||||
- 风险:
|
||||
@@ -59,18 +63,29 @@
|
||||
- 多接入模式会带来能力不一致问题,UI 与权限模型必须显式展示“当前模式支持什么/不支持什么”
|
||||
- 当前 JMX / Endpoint provider 的资源浏览与值读取仍是骨架实现,Task 4 已打通接口与 UI 链路,但真实资源展开会返回 `not implemented`
|
||||
- Task 5 已打通 Guard/Preview/Audit 主链路,但真实 provider 的 `PreviewChange` / `ApplyChange` 能力仍依赖后续 provider 细化实现
|
||||
- 当前 AI 能力边界仍是“分析 + 生成结构化计划 + 回填预览草稿”,不直接执行 JVM 写入,真实执行仍取决于 Guard、人工确认和 provider 能力
|
||||
- 当前 MVP 中 `updateValue` 会映射为现有 JVM 变更 contract 的 `put`,且 payload 仅接受 JSON 对象;非对象值、复杂二进制值和 provider 专有写法不在本期承诺范围
|
||||
- AI 计划若只提供 `namespace + key`,当前仅按 `namespace/key` 推导资源 ID,属于 MVP 级资源定位约定,未承诺适配所有 provider 的资源 ID 规则
|
||||
- JVM 审计记录当前未显式记录“AI 是否参与”,与理想审计模型仍有差距
|
||||
- 历史旧 AI 消息不包含 JVM 来源上下文,若需要应用到预览,需在目标 JVM 资源页重新生成计划
|
||||
- 阻塞:
|
||||
- 目标应用技术栈、缓存框架与接入约束尚未明确
|
||||
- 当前开发收口阶段无新增阻塞
|
||||
- 缓解措施:
|
||||
- 优先收敛到标准接入面(JMX / Spring Actuator / Java Agent 三选一)
|
||||
- 首期只支持白名单对象类型与受控写操作
|
||||
- 要求变更审计、预览、确认与失败回滚路径
|
||||
- 在交付说明中明确“AI 只生成草稿,不代表 provider 已具备真实写能力”
|
||||
- 后续如进入真实接入阶段,优先补齐 provider 端到端验证与 AI 审计字段
|
||||
|
||||
## 6. 决策记录
|
||||
- 决策 1:先做可行性评估与方案设计,不直接进入实现
|
||||
- 决策 2:默认优先复用 GoNavi 现有 driver-agent 与前端编辑器能力,避免侵入式重构主流程
|
||||
- 决策 3:已按完整模式推进,后续方案将优先评估通用 Agent 路径是否成立
|
||||
- 决策 4:由于目标服务大概率不允许 agent/attach,后续推荐方向转为“多接入模式 + 能力协商”
|
||||
- 决策 5:AI 在 JVM 场景中只负责分析与生成结构化计划,不直接执行运行时写入
|
||||
- 决策 6:AI 计划应用入口只回填 JVM 预览草稿,后续仍必须经过 `JVMPreviewChange`、Guard 校验和人工确认
|
||||
- 决策 7:当前 MVP 中 `updateValue` 会映射到现有 JVM 变更 contract 的 `put`,且 payload 仅接受 JSON 对象
|
||||
- 决策 8:JVM AI 计划必须绑定生成时的 JVM 上下文,只允许投递到匹配的 `tabId + connectionId + providerMode + resourcePath`
|
||||
|
||||
## 7. 验证记录
|
||||
- 验证项:
|
||||
@@ -80,6 +95,11 @@
|
||||
- JVM Connector 实施计划文档自检
|
||||
- Task 1:JVM 共享契约与配置归一化
|
||||
- Task 2:Provider 注册、连接测试与能力探测 API
|
||||
- Task 6:AI 计划解析、资源定位解析、契约映射与页签上下文隔离
|
||||
- Task 7:后端全量测试
|
||||
- Task 7:前端全量测试
|
||||
- Task 7:前端生产构建
|
||||
- Task 7:Wails 生产构建
|
||||
- 结果:
|
||||
- 已确认存在可复用的连接桥接与编辑器基础设施
|
||||
- 已完成正式设计文档落盘与自检,未发现占位词和明显范围冲突
|
||||
@@ -95,6 +115,13 @@
|
||||
- 已完成 Task 5:后端新增 `JVMPreviewChange` / `JVMApplyChange` / `JVMListAuditRecords`,补齐 Guard、审计 JSONL 落盘与审计读取能力
|
||||
- Task 5 已补齐只读拦截、`prod` 环境确认、provider preview 错误透出、审计写入失败显式回传、连接 `allowedModes` 约束和局部快照合并保底
|
||||
- 前端已完成 JVM 变更草稿区、预览弹窗、执行确认、审计记录页签与按 provider mode 的审计过滤
|
||||
- 已完成 Task 6:AI 计划解析、资源定位解析、`updateValue -> put` 显式映射、JSON 对象 payload 约束和上下文绑定单测
|
||||
- 已完成 Task 6:AI 聊天消息与 JVM 来源页签绑定,AI 气泡应用按钮不再依赖点击时的 `activeTabId`,避免跨 JVM 页签误投递
|
||||
- `cd frontend && npm test -- --run src/utils/jvmAiPlan.test.ts` 通过(10 tests)
|
||||
- `go test ./... -count=1` 通过
|
||||
- `cd frontend && npm test -- --run` 通过(61 files,258 tests)
|
||||
- `cd frontend && npm run build` 通过;构建中存在既有 chunk size / dynamic import 警告,但未阻塞产物生成
|
||||
- `wails build -clean` 通过,成功生成 macOS 应用包
|
||||
- 证据(日志/截图/链接):
|
||||
- `cmd/optional-driver-agent/main.go`
|
||||
- `internal/db/database.go`
|
||||
@@ -160,7 +187,17 @@
|
||||
- `go test ./internal/jvm ./internal/app -run 'TestPreviewChangeBlocksReadOnlyConnection|TestPreviewChangeReturnsProviderPreviewErrorWhenWriteAllowed|TestPreviewChangeMarksProdWritesAsConfirmationRequired|TestPreviewChangeMergesProviderSnapshotsWithoutDroppingDefaults|TestJVMApplyChangeReturnsProviderPayload|TestJVMPreviewChangeRejectsModeOutsideAllowedModes|TestJVMListAuditRecordsReturnsLatestRecords|TestJVMApplyChangeSurfacesAuditWriteFailure' -count=1`
|
||||
- `go test ./internal/jvm ./internal/app -count=1`
|
||||
- `cd frontend && npm run build`
|
||||
- `frontend/src/utils/jvmAiPlan.ts`
|
||||
- `frontend/src/utils/jvmAiPlan.test.ts`
|
||||
- `frontend/src/components/AIChatPanel.tsx`
|
||||
- `frontend/src/components/ai/AIMessageBubble.tsx`
|
||||
- `frontend/src/components/JVMResourceBrowser.tsx`
|
||||
- `frontend/src/types.ts`
|
||||
- `cd frontend && npm test -- --run src/utils/jvmAiPlan.test.ts`
|
||||
- `go test ./... -count=1`
|
||||
- `cd frontend && npm test -- --run`
|
||||
- `wails build -clean`
|
||||
|
||||
## 8. 下一步
|
||||
- 下一步行动:进入 Task 6,接入 AI 结构化修改计划能力,并在 Task 7 做全量交付检查
|
||||
- 下一步行动:如进入真实接入阶段,优先补齐 provider 端到端写入验证、AI 参与审计字段和生产环境灰度验证
|
||||
- 负责人:Codex
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useStore, loadAISessionsFromBackend, loadAISessionFromBackend } from '.
|
||||
import { EventsOn, EventsOff } from '../../wailsjs/runtime';
|
||||
import { DBGetDatabases, DBGetTables } from '../../wailsjs/go/app/App';
|
||||
import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
import { AIChatMessage, AIToolCall } from '../types';
|
||||
import type { AIChatMessage, AIToolCall, JVMAIPlanContext } from '../types';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import './AIChatPanel.css';
|
||||
|
||||
@@ -231,6 +231,7 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
const nudgeCountRef = useRef(0); // 催促模型使用 function call 的次数
|
||||
const panelRef = useRef<HTMLDivElement>(null); // 面板 DOM ref,用于拖拽时直接操作宽度
|
||||
const dragWidthRef = useRef(0); // 拖拽过程中的实时宽度(不触发 React 重渲染)
|
||||
const pendingJVMPlanContextRef = useRef<JVMAIPlanContext | undefined>(undefined);
|
||||
|
||||
const aiChatHistory = useStore(state => state.aiChatHistory);
|
||||
const aiActiveSessionId = useStore(state => state.aiActiveSessionId);
|
||||
@@ -248,6 +249,31 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
const activeTabId = useStore(state => state.activeTabId);
|
||||
const aiPanelVisible = useStore(state => state.aiPanelVisible);
|
||||
|
||||
const getCurrentJVMPlanContext = useCallback((): JVMAIPlanContext | undefined => {
|
||||
const state = useStore.getState();
|
||||
const activeTab = state.tabs.find(t => t.id === state.activeTabId);
|
||||
if (!activeTab || activeTab.type !== 'jvm-resource') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const activeConnection = state.connections.find(c => c.id === activeTab.connectionId);
|
||||
if (activeConnection?.config?.type !== 'jvm') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resourcePath = String(activeTab.resourcePath || '').trim();
|
||||
if (!resourcePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
tabId: activeTab.id,
|
||||
connectionId: activeTab.connectionId,
|
||||
providerMode: (activeTab.providerMode || activeConnection.config.jvm?.preferredMode || 'jmx') as JVMAIPlanContext['providerMode'],
|
||||
resourcePath,
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto-Context Injection Hook
|
||||
useEffect(() => {
|
||||
if (!aiPanelVisible) return;
|
||||
@@ -498,7 +524,15 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
if (assistantMsgId) {
|
||||
updateAIChatMessage(sid, assistantMsgId, { content: `❌ 错误: ${cleanErr}`, phase: 'idle', loading: false, rawError: rawErr });
|
||||
} else {
|
||||
addAIChatMessage(sid, { id: genId(), role: 'assistant', phase: 'idle', content: `❌ 错误: ${cleanErr}`, rawError: rawErr, timestamp: Date.now() });
|
||||
addAIChatMessage(sid, {
|
||||
id: genId(),
|
||||
role: 'assistant',
|
||||
phase: 'idle',
|
||||
content: `❌ 错误: ${cleanErr}`,
|
||||
rawError: rawErr,
|
||||
timestamp: Date.now(),
|
||||
jvmPlanContext: pendingJVMPlanContextRef.current,
|
||||
});
|
||||
}
|
||||
assistantMsgId = '';
|
||||
setSending(false);
|
||||
@@ -510,7 +544,16 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
updateAIChatMessage(sid, assistantMsgId, { tool_calls: data.tool_calls, phase: 'tool_calling' });
|
||||
} else {
|
||||
assistantMsgId = genId();
|
||||
addAIChatMessage(sid, { id: assistantMsgId, role: 'assistant', phase: 'tool_calling', content: '', tool_calls: data.tool_calls, timestamp: Date.now(), loading: true });
|
||||
addAIChatMessage(sid, {
|
||||
id: assistantMsgId,
|
||||
role: 'assistant',
|
||||
phase: 'tool_calling',
|
||||
content: '',
|
||||
tool_calls: data.tool_calls,
|
||||
timestamp: Date.now(),
|
||||
loading: true,
|
||||
jvmPlanContext: pendingJVMPlanContextRef.current,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +561,16 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
if (data.thinking) {
|
||||
if (!assistantMsgId) {
|
||||
assistantMsgId = genId();
|
||||
addAIChatMessage(sid, { id: assistantMsgId, role: 'assistant', phase: 'thinking', content: '', thinking: data.thinking, timestamp: Date.now(), loading: true });
|
||||
addAIChatMessage(sid, {
|
||||
id: assistantMsgId,
|
||||
role: 'assistant',
|
||||
phase: 'thinking',
|
||||
content: '',
|
||||
thinking: data.thinking,
|
||||
timestamp: Date.now(),
|
||||
loading: true,
|
||||
jvmPlanContext: pendingJVMPlanContextRef.current,
|
||||
});
|
||||
if (sending) setSending(false);
|
||||
} else {
|
||||
streamBuffer.thinking += data.thinking;
|
||||
@@ -529,7 +581,15 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
if (data.content) {
|
||||
if (!assistantMsgId) {
|
||||
assistantMsgId = genId();
|
||||
addAIChatMessage(sid, { id: assistantMsgId, role: 'assistant', phase: 'generating', content: data.content, timestamp: Date.now(), loading: true });
|
||||
addAIChatMessage(sid, {
|
||||
id: assistantMsgId,
|
||||
role: 'assistant',
|
||||
phase: 'generating',
|
||||
content: data.content,
|
||||
timestamp: Date.now(),
|
||||
loading: true,
|
||||
jvmPlanContext: pendingJVMPlanContextRef.current,
|
||||
});
|
||||
setSending(false);
|
||||
const currentHistory = useStore.getState().aiChatHistory[sid] || [];
|
||||
if (currentHistory.length <= 1) isFirstCompletion = true;
|
||||
@@ -770,7 +830,7 @@ ${resourcePath ? `当前资源路径:${resourcePath}` : '当前未选中具体
|
||||
2. 如果用户要求生成 JVM 修改方案,必须输出一个唯一的 \`\`\`json 代码块,并且 JSON 字段严格限定为 targetType、selector、action、payload、reason。
|
||||
3. action 只能使用 updateValue、evict、clear 之一。
|
||||
4. selector.resourcePath 优先使用当前资源路径;如果当前路径未知,就明确说明无法精确定位,不要编造路径。
|
||||
5. payload 必须保持对象结构,例如 {"format":"json","value":{"status":"ACTIVE"}}。
|
||||
5. 当前 JVM 预览 MVP 只支持 JSON 对象变更:如果 action=updateValue,则 payload 必须是 {"format":"json","value":{...}},且 value 必须是 JSON 对象;evict/clear 时 payload 可以省略。
|
||||
6. 不要输出脚本、命令或“已经执行成功”之类的表述。`
|
||||
});
|
||||
return systemMessages;
|
||||
@@ -843,6 +903,10 @@ SELECT * FROM users WHERE status = 1;
|
||||
const toolContextMapRef = useRef<Map<string, { connectionId: string; dbName: string; tables: string[] }>>(new Map());
|
||||
|
||||
const executeLocalTools = useCallback(async (toolCalls: AIToolCall[], currentAsstMsgId: string) => {
|
||||
const currentAsstMsg = (useStore.getState().aiChatHistory[sid] || []).find(m => m.id === currentAsstMsgId);
|
||||
const inheritedJVMPlanContext = currentAsstMsg?.jvmPlanContext || pendingJVMPlanContextRef.current;
|
||||
pendingJVMPlanContextRef.current = inheritedJVMPlanContext;
|
||||
|
||||
// 【全局轮次熔断】防止模型(如 DeepSeek)在已生成答案后仍无限循环调用工具
|
||||
const MAX_TOOL_CALL_ROUNDS = 15;
|
||||
totalToolRoundRef.current += 1;
|
||||
@@ -852,6 +916,7 @@ SELECT * FROM users WHERE status = 1;
|
||||
id: genId(), role: 'assistant',
|
||||
content: `⚠️ 工具调用已达 ${MAX_TOOL_CALL_ROUNDS} 轮上限,自动终止循环。如需继续探索,请发送新的消息。`,
|
||||
timestamp: Date.now(),
|
||||
jvmPlanContext: inheritedJVMPlanContext,
|
||||
});
|
||||
setSending(false);
|
||||
return;
|
||||
@@ -1040,6 +1105,7 @@ SELECT * FROM users WHERE status = 1;
|
||||
id: genId(), role: 'assistant',
|
||||
content: '⚠️ 探针连续 3 轮执行失败,自动终止。请检查连接状态后重试。',
|
||||
timestamp: Date.now(),
|
||||
jvmPlanContext: inheritedJVMPlanContext,
|
||||
});
|
||||
setSending(false);
|
||||
return;
|
||||
@@ -1053,7 +1119,8 @@ SELECT * FROM users WHERE status = 1;
|
||||
const chainConnectingMsg: AIChatMessage = {
|
||||
id: genId(), role: 'assistant', phase: 'connecting',
|
||||
content: '汇总探针执行结果中',
|
||||
timestamp: Date.now(), loading: true
|
||||
timestamp: Date.now(), loading: true,
|
||||
jvmPlanContext: inheritedJVMPlanContext,
|
||||
};
|
||||
useStore.getState().addAIChatMessage(sid, chainConnectingMsg);
|
||||
|
||||
@@ -1118,6 +1185,7 @@ SELECT * FROM users WHERE status = 1;
|
||||
content: result?.success ? result.content : `❌ ${errC}`,
|
||||
rawError: (!result?.success && errC !== errR) ? errR : undefined,
|
||||
timestamp: Date.now(),
|
||||
jvmPlanContext: inheritedJVMPlanContext,
|
||||
});
|
||||
setSending(false);
|
||||
}
|
||||
@@ -1145,6 +1213,8 @@ SELECT * FROM users WHERE status = 1;
|
||||
toolCallRoundRef.current = 0; // 重置工具调用轮次计数
|
||||
totalToolRoundRef.current = 0; // 重置总轮次计数
|
||||
nudgeCountRef.current = 0; // 重置催促计数
|
||||
const currentJVMPlanContext = getCurrentJVMPlanContext();
|
||||
pendingJVMPlanContextRef.current = currentJVMPlanContext;
|
||||
|
||||
const currentImages = [...draftImages];
|
||||
setInput('');
|
||||
@@ -1163,7 +1233,8 @@ SELECT * FROM users WHERE status = 1;
|
||||
|
||||
const connectingMsg: AIChatMessage = {
|
||||
id: genId(), role: 'assistant', phase: 'connecting', content: '',
|
||||
timestamp: Date.now(), loading: true
|
||||
timestamp: Date.now(), loading: true,
|
||||
jvmPlanContext: currentJVMPlanContext,
|
||||
};
|
||||
addAIChatMessage(sid, connectingMsg);
|
||||
|
||||
@@ -1215,6 +1286,7 @@ SELECT * FROM users WHERE status = 1;
|
||||
content: result?.success ? result.content : `❌ ${errC2}`,
|
||||
rawError: (!result?.success && errC2 !== errR2) ? errR2 : undefined,
|
||||
timestamp: Date.now(),
|
||||
jvmPlanContext: currentJVMPlanContext,
|
||||
};
|
||||
addAIChatMessage(sid, assistantMsg);
|
||||
setSending(false);
|
||||
@@ -1233,7 +1305,7 @@ SELECT * FROM users WHERE status = 1;
|
||||
addAIChatMessage(sid, { id: genId(), role: 'assistant', content: `❌ 发送失败: ${cleanE2}`, rawError: cleanE2 !== rawE2 ? rawE2 : undefined, timestamp: Date.now() });
|
||||
setSending(false);
|
||||
}
|
||||
}, [input, draftImages, sending, messages, addAIChatMessage, sid, activeProvider]);
|
||||
}, [input, draftImages, sending, messages, addAIChatMessage, sid, activeProvider, getCurrentJVMPlanContext]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
|
||||
@@ -7,12 +7,19 @@ import type {
|
||||
JVMApplyResult,
|
||||
JVMChangePreview,
|
||||
JVMChangeRequest,
|
||||
JVMAIPlanContext,
|
||||
JVMValueSnapshot,
|
||||
SavedConnection,
|
||||
TabData,
|
||||
} from '../types';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { buildJVMAIPlanPrompt, type JVMAIChangePlan } from '../utils/jvmAiPlan';
|
||||
import {
|
||||
buildJVMChangeDraftFromAIPlan,
|
||||
buildJVMAIPlanPrompt,
|
||||
matchesJVMAIPlanTargetTab,
|
||||
type JVMAIChangeDraft,
|
||||
type JVMAIChangePlan,
|
||||
} from '../utils/jvmAiPlan';
|
||||
import { buildJVMTabTitle } from '../utils/jvmRuntimePresentation';
|
||||
import JVMModeBadge from './jvm/JVMModeBadge';
|
||||
import JVMChangePreviewModal from './jvm/JVMChangePreviewModal';
|
||||
@@ -47,9 +54,9 @@ const formatValue = (value: unknown): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatPlanPayload = (plan: JVMAIChangePlan): string => {
|
||||
const formatDraftPayload = (draft: JVMAIChangeDraft): string => {
|
||||
try {
|
||||
return JSON.stringify(plan.payload ?? {}, null, 2);
|
||||
return JSON.stringify(draft.payload ?? {}, null, 2);
|
||||
} catch {
|
||||
return '{}';
|
||||
}
|
||||
@@ -84,9 +91,10 @@ const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [snapshot, setSnapshot] = useState<JVMValueSnapshot | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [action, setAction] = useState('update');
|
||||
const [action, setAction] = useState('put');
|
||||
const [reason, setReason] = useState('');
|
||||
const [payloadText, setPayloadText] = useState(DEFAULT_PAYLOAD_TEXT);
|
||||
const [draftResourceId, setDraftResourceId] = useState('');
|
||||
const [draftError, setDraftError] = useState('');
|
||||
const [applyMessage, setApplyMessage] = useState('');
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
@@ -145,9 +153,10 @@ const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||||
}, [connection, providerMode, resourcePath, tab.connectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
setAction('update');
|
||||
setAction('put');
|
||||
setReason('');
|
||||
setPayloadText(DEFAULT_PAYLOAD_TEXT);
|
||||
setDraftResourceId('');
|
||||
setDraftError('');
|
||||
setApplyMessage('');
|
||||
setPreviewOpen(false);
|
||||
@@ -156,24 +165,63 @@ const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail as { plan?: JVMAIChangePlan; targetTabId?: string } | undefined;
|
||||
const detail = (event as CustomEvent).detail as
|
||||
| {
|
||||
plan?: JVMAIChangePlan;
|
||||
targetTabId?: string;
|
||||
connectionId?: string;
|
||||
providerMode?: JVMAIPlanContext['providerMode'];
|
||||
resourcePath?: string;
|
||||
}
|
||||
| undefined;
|
||||
const plan = detail?.plan;
|
||||
if (!plan || (detail?.targetTabId && detail.targetTabId !== tab.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetPath = String(plan.selector.resourcePath || '').trim();
|
||||
if (targetPath && targetPath !== resourcePath) {
|
||||
setDraftError(`AI 计划指向资源 ${targetPath},当前页签是 ${resourcePath || '-'},请切换到对应资源后再应用。`);
|
||||
const planContext =
|
||||
detail?.targetTabId && detail?.connectionId && detail?.providerMode && detail?.resourcePath
|
||||
? {
|
||||
tabId: detail.targetTabId,
|
||||
connectionId: detail.connectionId,
|
||||
providerMode: detail.providerMode,
|
||||
resourcePath: detail.resourcePath,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (!planContext) {
|
||||
setDraftError('AI 计划缺少来源上下文,请在目标 JVM 资源页重新生成后再应用。');
|
||||
setApplyMessage('');
|
||||
setPreviewOpen(false);
|
||||
setPreviewResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setAction(plan.action);
|
||||
setReason(plan.reason);
|
||||
setPayloadText(formatPlanPayload(plan));
|
||||
if (!matchesJVMAIPlanTargetTab(tab, planContext)) {
|
||||
setDraftError('当前 JVM 页签与 AI 计划的来源上下文不一致,已拒绝自动应用。');
|
||||
setApplyMessage('');
|
||||
setPreviewOpen(false);
|
||||
setPreviewResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let draftFromPlan: JVMAIChangeDraft;
|
||||
try {
|
||||
draftFromPlan = buildJVMChangeDraftFromAIPlan(plan);
|
||||
} catch (err: any) {
|
||||
setDraftError(err?.message || 'AI 计划暂时无法转换为 JVM 预览草稿');
|
||||
setApplyMessage('');
|
||||
setPreviewOpen(false);
|
||||
setPreviewResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setDraftResourceId(draftFromPlan.resourceId);
|
||||
setAction(draftFromPlan.action);
|
||||
setReason(draftFromPlan.reason);
|
||||
setPayloadText(formatDraftPayload(draftFromPlan));
|
||||
setDraftError('');
|
||||
setApplyMessage('已从 AI 计划填充草稿,请先执行“预览变更”再确认写入。');
|
||||
setApplyMessage(`已从 AI 计划填充草稿,目标资源为 ${draftFromPlan.resourceId},请先执行“预览变更”再确认写入。`);
|
||||
setPreviewOpen(false);
|
||||
setPreviewResult(null);
|
||||
};
|
||||
@@ -183,7 +231,7 @@ const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||||
}, [resourcePath, tab.id]);
|
||||
|
||||
const buildDraftPlan = (): JVMChangeRequest => {
|
||||
const trimmedAction = String(action || '').trim() || 'update';
|
||||
const trimmedAction = String(action || '').trim() || 'put';
|
||||
const trimmedReason = String(reason || '').trim();
|
||||
if (!trimmedReason) {
|
||||
throw new Error('请填写变更原因');
|
||||
@@ -199,7 +247,7 @@ const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||||
payload = parsed as Record<string, any>;
|
||||
}
|
||||
|
||||
const resourceId = String(snapshot?.resourceId || resourcePath).trim();
|
||||
const resourceId = String(draftResourceId || snapshot?.resourceId || resourcePath).trim();
|
||||
if (!resourceId) {
|
||||
throw new Error('资源 ID 为空,无法生成变更草稿');
|
||||
}
|
||||
@@ -447,6 +495,7 @@ const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||||
{applyMessage ? <Alert type="success" showIcon message={applyMessage} /> : null}
|
||||
<Descriptions column={1} size="small" labelStyle={{ width: 120 }}>
|
||||
<Descriptions.Item label="资源路径">{resourcePath || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="目标资源">{draftResourceId || resourcePath || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="资源版本">{snapshot?.version || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
@@ -454,7 +503,7 @@ const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||||
<Input
|
||||
value={action}
|
||||
onChange={(event) => setAction(event.target.value)}
|
||||
placeholder="例如 update"
|
||||
placeholder="例如 put"
|
||||
maxLength={64}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
@@ -6,11 +6,11 @@ import remarkGfm from 'remark-gfm';
|
||||
import mermaid from 'mermaid';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { AIChatMessage, AIToolCall } from '../../types';
|
||||
import type { AIChatMessage, AIToolCall } from '../../types';
|
||||
import { useStore } from '../../store';
|
||||
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
import { normalizeAiMarkdown } from '../../utils/aiMarkdown';
|
||||
import { extractJVMChangePlan } from '../../utils/jvmAiPlan';
|
||||
import { extractJVMChangePlan, resolveJVMAIPlanTargetTabId } from '../../utils/jvmAiPlan';
|
||||
|
||||
// 🔧 性能优化:将 ReactMarkdown 包装为 Memo 组件并提取固定的 plugins
|
||||
const remarkPlugins = [remarkGfm];
|
||||
@@ -709,9 +709,27 @@ export const AIMessageBubble: React.FC<AIMessageBubbleProps> = React.memo(({ msg
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
const targetTabId = useStore.getState().activeTabId;
|
||||
const targetContext = msg.jvmPlanContext;
|
||||
if (!targetContext) {
|
||||
message.warning('这条 JVM 计划缺少来源页签上下文,请在目标 JVM 资源页重新生成。');
|
||||
return;
|
||||
}
|
||||
|
||||
const store = useStore.getState();
|
||||
const targetTabId = resolveJVMAIPlanTargetTabId(store.tabs, targetContext);
|
||||
if (!targetTabId) {
|
||||
message.warning('未找到与该 JVM 计划匹配的资源页签,请先打开原目标资源后再应用。');
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('gonavi:jvm-apply-ai-plan', {
|
||||
detail: { plan: jvmPlan, targetTabId },
|
||||
detail: {
|
||||
plan: jvmPlan,
|
||||
targetTabId,
|
||||
connectionId: targetContext.connectionId,
|
||||
providerMode: targetContext.providerMode,
|
||||
resourcePath: targetContext.resourcePath,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -244,6 +244,13 @@ export interface TabData {
|
||||
savedQueryId?: string; // Saved query identity for quick-save behavior
|
||||
}
|
||||
|
||||
export interface JVMAIPlanContext {
|
||||
tabId: string;
|
||||
connectionId: string;
|
||||
providerMode: 'jmx' | 'endpoint' | 'agent';
|
||||
resourcePath: string;
|
||||
}
|
||||
|
||||
export interface DatabaseNode {
|
||||
title: string;
|
||||
key: string;
|
||||
@@ -364,6 +371,7 @@ export interface AIChatMessage {
|
||||
tool_name?: string; // used for UI display
|
||||
rawError?: string; // 存储未清洗的原始错误信息,用于用户复制排查
|
||||
success?: boolean; // 标记探针执行是否成功
|
||||
jvmPlanContext?: JVMAIPlanContext;
|
||||
}
|
||||
|
||||
export interface AISafetyResult {
|
||||
|
||||
@@ -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 示例:',
|
||||
|
||||
11
frontend/wailsjs/go/app/App.d.ts
vendored
11
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -3,6 +3,7 @@
|
||||
import {connection} from '../models';
|
||||
import {sync} from '../models';
|
||||
import {app} from '../models';
|
||||
import {jvm} from '../models';
|
||||
import {redis} from '../models';
|
||||
|
||||
export function ApplyChanges(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:connection.ChangeSet):Promise<connection.QueryResult>;
|
||||
@@ -127,6 +128,16 @@ export function InstallLocalDriverPackage(arg1:string,arg2:string,arg3:string,ar
|
||||
|
||||
export function InstallUpdateAndRestart():Promise<connection.QueryResult>;
|
||||
|
||||
export function JVMApplyChange(arg1:connection.ConnectionConfig,arg2:jvm.ChangeRequest):Promise<connection.QueryResult>;
|
||||
|
||||
export function JVMGetValue(arg1:connection.ConnectionConfig,arg2:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function JVMListAuditRecords(arg1:string,arg2:number):Promise<connection.QueryResult>;
|
||||
|
||||
export function JVMListResources(arg1:connection.ConnectionConfig,arg2:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function JVMPreviewChange(arg1:connection.ConnectionConfig,arg2:jvm.ChangeRequest):Promise<connection.QueryResult>;
|
||||
|
||||
export function JVMProbeCapabilities(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
|
||||
export function ListSQLDirectory(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
@@ -246,6 +246,26 @@ export function InstallUpdateAndRestart() {
|
||||
return window['go']['app']['App']['InstallUpdateAndRestart']();
|
||||
}
|
||||
|
||||
export function JVMApplyChange(arg1, arg2) {
|
||||
return window['go']['app']['App']['JVMApplyChange'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function JVMGetValue(arg1, arg2) {
|
||||
return window['go']['app']['App']['JVMGetValue'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function JVMListAuditRecords(arg1, arg2) {
|
||||
return window['go']['app']['App']['JVMListAuditRecords'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function JVMListResources(arg1, arg2) {
|
||||
return window['go']['app']['App']['JVMListResources'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function JVMPreviewChange(arg1, arg2) {
|
||||
return window['go']['app']['App']['JVMPreviewChange'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function JVMProbeCapabilities(arg1) {
|
||||
return window['go']['app']['App']['JVMProbeCapabilities'](arg1);
|
||||
}
|
||||
|
||||
@@ -887,6 +887,33 @@ export namespace connection {
|
||||
|
||||
}
|
||||
|
||||
export namespace jvm {
|
||||
|
||||
export class ChangeRequest {
|
||||
providerMode: string;
|
||||
resourceId: string;
|
||||
action: string;
|
||||
reason: string;
|
||||
expectedVersion?: string;
|
||||
payload?: Record<string, any>;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ChangeRequest(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.providerMode = source["providerMode"];
|
||||
this.resourceId = source["resourceId"];
|
||||
this.action = source["action"];
|
||||
this.reason = source["reason"];
|
||||
this.expectedVersion = source["expectedVersion"];
|
||||
this.payload = source["payload"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace redis {
|
||||
|
||||
export class ZSetMember {
|
||||
@@ -1008,4 +1035,3 @@ export namespace sync {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user