mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-26 00:00:51 +00:00
- 桌面端:解析 db.<集合>.aggregate([...]) shell 语法,新增 mongoAggregateDocuments
接口(Tauri + HTTP);mongo 入口统一补 ensureConnected;ObjectId(...) 自动
转换为 {"\$oid":"..."} 扩展 JSON 表示。
- dbx-core:新增 aggregate_documents、insert/update/delete_documents bulk
操作;扩展 JSON 解析识别 \$oid;schema list_tables_core 补 MongoDB/Elasticsearch
分支返回 COLLECTION/INDEX。
- dbx-web / Tauri 命令 / MCP bridge:注册 4 个新 mongo 路由
(aggregate/insert/update/delete documents);MCP bridge 新增 mongo HTTP 入口
和 lazy 连接复用。
- node-core (MCP 后端):mongodb 走专属 executeQuery / listTables /
describeTable;read-only 默认 + DBX_MCP_ALLOW_WRITES /
DBX_MCP_ALLOW_DANGEROUS_SQL 两级开关;aggregate 含 \$out/\$merge 走同样开关;
describeTable 抽样 20 条文档推断字段类型,_id 自动标记主键。
- mcp-server:mongodb 跳过 SQL safety evaluator(mongo shell 与 SQL 语法
不兼容),改由 backend 做命令级 gating;附注释。
- AiAssistant 同时 emit replaceSql + executeSql 让编辑器和结果保持一致;
App.vue ensureQueryTab 在无上下文时回退到连接默认库;aiMessageRender
识别 mongodb / mongo 代码块。
- Review 修复:合并两份重复的 evaluateMongo*Safety 实现(同时修复 web-backend
用字符串比较导致空 filter 检查可被空格绕过的 bug);ObjectId 正则兼容
单/双引号;mongo_ops 错误文案 "MCP" 改为 "bulk";补 aggregate 解析测试用例。
测试:pnpm test 911 通过;vue-tsc 通过;cargo check -p dbx-core 通过。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
136 lines
3.7 KiB
TypeScript
136 lines
3.7 KiB
TypeScript
export interface AiMessageTextSegment {
|
|
type: "text";
|
|
content: string;
|
|
html: string;
|
|
}
|
|
|
|
export interface AiMessageCodeSegment {
|
|
type: "code";
|
|
content: string;
|
|
lang: string;
|
|
html: string;
|
|
isSql: boolean;
|
|
}
|
|
|
|
export type AiMessageRenderSegment = AiMessageTextSegment | AiMessageCodeSegment;
|
|
|
|
interface MessageSegment {
|
|
type: "text" | "code";
|
|
content: string;
|
|
lang?: string;
|
|
}
|
|
|
|
export interface AiMessageRendererOptions {
|
|
maxEntries?: number;
|
|
markdown: (text: string) => string;
|
|
highlightCode?: (content: string, lang: string) => string;
|
|
}
|
|
|
|
const DEFAULT_MAX_ENTRIES = 100;
|
|
const SQL_LANGUAGES = new Map([
|
|
["sql", "SQL"],
|
|
["mysql", "MYSQL"],
|
|
["postgres", "POSTGRESQL"],
|
|
["postgresql", "POSTGRESQL"],
|
|
["sqlite", "SQLITE"],
|
|
["tsql", "TSQL"],
|
|
["clickhouse", "CLICKHOUSE"],
|
|
["mongodb", "MONGODB"],
|
|
["mongo", "MONGODB"],
|
|
]);
|
|
const SHELL_LANGUAGES = new Map([
|
|
["bash", "BASH"],
|
|
["sh", "SHELL"],
|
|
["shell", "SHELL"],
|
|
["zsh", "ZSH"],
|
|
]);
|
|
const SQL_LANGUAGE_LABELS = new Set(SQL_LANGUAGES.values());
|
|
|
|
export function createAiMessageRenderer(options: AiMessageRendererOptions) {
|
|
const maxEntries = Math.max(1, Math.floor(options.maxEntries ?? DEFAULT_MAX_ENTRIES));
|
|
const cache = new Map<string, AiMessageRenderSegment[]>();
|
|
|
|
function render(content: string): AiMessageRenderSegment[] {
|
|
const cached = cache.get(content);
|
|
if (cached) {
|
|
cache.delete(content);
|
|
cache.set(content, cached);
|
|
return cached;
|
|
}
|
|
|
|
const rendered = parseAiMessage(content).map((segment): AiMessageRenderSegment => {
|
|
if (segment.type === "text") {
|
|
return { type: "text", content: segment.content, html: options.markdown(segment.content) };
|
|
}
|
|
const lang = normalizeAiCodeLanguage(segment.lang);
|
|
return {
|
|
type: "code",
|
|
content: segment.content,
|
|
html: options.highlightCode?.(segment.content, lang) ?? escapeHtml(segment.content),
|
|
lang,
|
|
isSql: isSqlAiCodeLanguage(lang),
|
|
};
|
|
});
|
|
|
|
cache.set(content, rendered);
|
|
while (cache.size > maxEntries) {
|
|
const oldestKey = cache.keys().next().value;
|
|
if (oldestKey === undefined) break;
|
|
cache.delete(oldestKey);
|
|
}
|
|
return rendered;
|
|
}
|
|
|
|
function clear() {
|
|
cache.clear();
|
|
}
|
|
|
|
return { render, clear };
|
|
}
|
|
|
|
export function parseAiMessage(text: string): MessageSegment[] {
|
|
const segments: MessageSegment[] = [];
|
|
const lines = text.split("\n");
|
|
let i = 0;
|
|
|
|
while (i < lines.length) {
|
|
const fenceMatch = lines[i].match(/^```([a-zA-Z0-9_+.-]*)\s*$/);
|
|
if (fenceMatch) {
|
|
const lang = fenceMatch[1] || "sql";
|
|
const codeLines: string[] = [];
|
|
i++;
|
|
while (i < lines.length && !/^```\s*$/.test(lines[i])) {
|
|
codeLines.push(lines[i]);
|
|
i++;
|
|
}
|
|
if (i < lines.length) i++;
|
|
const content = codeLines.join("\n").trim();
|
|
if (content) segments.push({ type: "code", lang, content });
|
|
} else {
|
|
const textLines: string[] = [];
|
|
while (i < lines.length && !/^```([a-zA-Z0-9_+.-]*)\s*$/.test(lines[i])) {
|
|
textLines.push(lines[i]);
|
|
i++;
|
|
}
|
|
const content = textLines.join("\n");
|
|
if (content.trim()) segments.push({ type: "text", content });
|
|
}
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
export function normalizeAiCodeLanguage(lang?: string): string {
|
|
const key = (lang || "sql").trim().toLowerCase();
|
|
if (!key) return "SQL";
|
|
return SQL_LANGUAGES.get(key) || SHELL_LANGUAGES.get(key) || (key === "json" ? "JSON" : key.toUpperCase());
|
|
}
|
|
|
|
export function isSqlAiCodeLanguage(lang: string): boolean {
|
|
return SQL_LANGUAGE_LABELS.has(lang);
|
|
}
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
}
|