From 2aaba386ec7700002d9d717e737eb08c3e9b97a2 Mon Sep 17 00:00:00 2001 From: clousky2020 <33016567+clousky2020@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:53:02 +0800 Subject: [PATCH] fix: resolve build issues and implement memory UPSERT logic (#1763) * fix: resolve build issues for v3.7.5 and apply memory/translation fixes 1. antigravityHeaders.ts: restore ANTIGRAVITY_LOAD_CODE_ASSIST_* exports for oauth.ts compatibility 2. next.config.mjs: add @ngrok/ngrok to serverExternalPackages and webpack externals to handle native .node modules 3. Memory system: UPSERT logic to prevent duplicate entries with same apiKeyId + key 4. Chinese translations: complete CLI tools and memory dashboard localizations 5. Test fixes: unique keys for pagination tests to comply with unique constraint Co-Authored-By: Claude Opus 4.6 * fix: address Gemini Code Assist review feedback 1. store.ts: add expires_at to UPDATE statement in UPSERT logic - Previously, expires_at was not being persisted to database on update - This caused state mismatch between returned Memory object and actual DB row 2. package-lock.json: revert react-markdown registry to official npmjs.org - Mirror-specific registry URL (npmmirror.com) should not be in lockfile Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- next.config.mjs | 7 ++ open-sse/services/antigravityHeaders.ts | 7 +- package-lock.json | 12 ---- src/app/(dashboard)/dashboard/memory/page.tsx | 38 ++++++----- src/i18n/messages/zh-CN.json | 31 ++++++--- src/lib/memory/store.ts | 67 ++++++++++++++++++- tests/unit/memory-store.test.ts | 12 ++++ 7 files changed, 131 insertions(+), 43 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index b1e835a4ae..79348ec773 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -67,6 +67,7 @@ const nextConfig = { "tls-client-node", "koffi", "tough-cookie", + "@ngrok/ngrok", "child_process", "fs", "path", @@ -100,6 +101,12 @@ const nextConfig = { contextRegExp: /thread-stream/, }) ); + + // Mark @ngrok/ngrok as external to prevent webpack from trying to bundle its .node binaries + config.externals = config.externals || []; + config.externals.push({ + "@ngrok/ngrok": "commonjs @ngrok/ngrok", + }); // ── Turbopack / Next.js 16 module-hash patch (#394, #396, #398) ──────── // // Next.js 16 (with or without Turbopack) compiles the instrumentation hook diff --git a/open-sse/services/antigravityHeaders.ts b/open-sse/services/antigravityHeaders.ts index 3e6b632337..4b38e90cf2 100644 --- a/open-sse/services/antigravityHeaders.ts +++ b/open-sse/services/antigravityHeaders.ts @@ -20,7 +20,8 @@ export const GEMINI_CLI_VERSION = "0.39.1"; export const GEMINI_SDK_VERSION = "1.30.0"; export const NODE_VERSION = "v22.21.1"; export const ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT = "google-api-nodejs-client/10.3.0"; -export const ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1"; +export const ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT = + "google-cloud-sdk vscode_cloudshelleditor/0.1"; const LOAD_CODE_ASSIST_METADATA = Object.freeze({ ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", @@ -133,6 +134,4 @@ export function googApiClientHeader(): string { return `google-genai-sdk/${GEMINI_SDK_VERSION} gl-node/${NODE_VERSION}`; } -export { - ANTIGRAVITY_VERSION, -}; +export { ANTIGRAVITY_VERSION }; diff --git a/package-lock.json b/package-lock.json index a22ea08b6e..17ae67cb5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2447,9 +2447,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2466,9 +2463,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2485,9 +2479,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2504,9 +2495,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/src/app/(dashboard)/dashboard/memory/page.tsx b/src/app/(dashboard)/dashboard/memory/page.tsx index 526cc9debe..885727721a 100644 --- a/src/app/(dashboard)/dashboard/memory/page.tsx +++ b/src/app/(dashboard)/dashboard/memory/page.tsx @@ -205,17 +205,21 @@ export default function MemoryPage() { {health !== null && ( )} {health === null && !checkingHealth && ( )} @@ -321,7 +325,7 @@ export default function MemoryPage() {
- Page {page} of {totalPages} ({total} total) + {t("pageInfo", { page, totalPages, total })}
@@ -356,47 +360,47 @@ export default function MemoryPage() { onClick={() => setAddDialogOpen(false)} disabled={isSubmitting} > - Cancel + {t("cancel")} } >
- +
- + setNewMemory({ ...newMemory, key: e.target.value })} - placeholder="e.g., user_preference_theme" + placeholder={t("keyPlaceholder")} className="w-full" />
- + setNewMemory({ ...newMemory, content: e.target.value })} - placeholder="e.g., Prefers dark mode" + placeholder={t("contentPlaceholder")} className="w-full" />
diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 3f7f512e60..43ffa88793 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1335,13 +1335,17 @@ } } }, - "autoConfiguredTab": "Auto Configured Tab", - "toolCategoriesDesc": "Tool Categories Desc", - "allToolsTab": "All Tools Tab", - "guidedClientsTab": "Guided Clients Tab", - "mitmClientsTab": "Mitm Clients Tab", - "toolCategories": "Tool Categories", - "visibleToolsCount": "Visible Tools Count" + "autoConfiguredTab": "自动配置", + "toolCategoriesDesc": "配置 AI 编程助手通过 OmniRoute 路由", + "allToolsTab": "所有工具", + "guidedClientsTab": "引导客户端", + "mitmClientsTab": "MITM 客户端", + "customCliTab": "自定义 CLI", + "toolCategories": "工具分类", + "visibleToolsCount": "{count} 个工具可用", + "installationGuide": "安装指南", + "whenToUseLabel": "何时使用", + "openToolDocs": "打开工具文档" }, "combos": { "title": "组合", @@ -4324,7 +4328,9 @@ "addMemory": "添加记忆", "type": "类型", "key": "键", + "keyPlaceholder": "例如:user_preference_theme", "content": "内容", + "contentPlaceholder": "例如:偏好深色模式", "created": "创建时间", "actions": "操作", "delete": "删除", @@ -4332,7 +4338,16 @@ "episodic": "情景型", "procedural": "程序型", "semantic": "语义型", - "a": "A" + "previous": "上一页", + "next": "下一页", + "pageInfo": "第 {page} 页,共 {totalPages} 页(共 {total} 条)", + "checkingHealth": "检查中...", + "checkHealth": "检查健康", + "pipelineOk": "Pipeline 正常 ({latencyMs}ms)", + "pipelineError": "Pipeline 错误", + "healthUnknown": "健康状态未知", + "cancel": "取消", + "save": "保存" }, "skills": { "title": "技能", diff --git a/src/lib/memory/store.ts b/src/lib/memory/store.ts index df9f554f55..e724a0f2cf 100644 --- a/src/lib/memory/store.ts +++ b/src/lib/memory/store.ts @@ -77,15 +77,78 @@ function rowToMemory(row: MemoryRow): Memory { } /** - * Create a new memory entry + * Find existing memory by apiKeyId and key (for UPSERT logic) + */ +function findExistingMemory( + db: ReturnType, + apiKeyId: string, + key: string +): MemoryRow | undefined { + if (!key) return undefined; + const stmt = db.prepare( + "SELECT * FROM memories WHERE api_key_id = ? AND key = ? ORDER BY created_at DESC LIMIT 1" + ); + return stmt.get(apiKeyId, key) as MemoryRow | undefined; +} + +/** + * Create a new memory entry (UPSERT: updates existing if same apiKeyId + key) */ export async function createMemory( memory: Omit ): Promise { const db = getDbInstance(); - const id = crypto.randomUUID(); const now = new Date().toISOString(); + // Check for existing memory with same apiKeyId + key (UPSERT logic) + const existing = memory.key ? findExistingMemory(db, memory.apiKeyId, memory.key) : undefined; + + if (existing) { + // UPDATE existing record + const updatedMetadata = { ...parseJSON(existing.metadata), ...memory.metadata }; + const stmt = db.prepare( + "UPDATE memories SET content = ?, metadata = ?, updated_at = ?, session_id = ?, type = ?, expires_at = ? WHERE id = ?" + ); + stmt.run( + memory.content, + JSON.stringify(updatedMetadata), + now, + memory.sessionId, + memory.type, + memory.expiresAt ?? null, + existing.id + ); + + const updatedMemory: Memory = { + id: String(existing.id), + apiKeyId: memory.apiKeyId, + sessionId: memory.sessionId, + type: memory.type, + key: memory.key, + content: memory.content, + metadata: updatedMetadata, + createdAt: new Date(String(existing.created_at)), + updatedAt: new Date(now), + expiresAt: memory.expiresAt ?? null, + }; + + // Invalidate and update cache + invalidateMemoryCache(existing.id); + evictIfNeeded(_memoryCache); + _memoryCache.set(existing.id, { value: updatedMemory, timestamp: Date.now() }); + + log.info("memory.updated", { + apiKeyId: memory.apiKeyId, + type: memory.type, + id: existing.id, + key: memory.key, + }); + + return updatedMemory; + } + + // INSERT new record if not exists + const id = crypto.randomUUID(); const stmt = db.prepare( "INSERT INTO memories (id, api_key_id, session_id, type, key, content, metadata, created_at, updated_at, expires_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" diff --git a/tests/unit/memory-store.test.ts b/tests/unit/memory-store.test.ts index de7009b5be..c86de71d37 100644 --- a/tests/unit/memory-store.test.ts +++ b/tests/unit/memory-store.test.ts @@ -177,18 +177,21 @@ test("listMemories filters by api key, type and session while preserving newest- test("listMemories supports limit and offset pagination even when only offset is provided", async () => { insertMemoryRow({ id: "page-1", + key: "pagination:1", content: "oldest", createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", }); insertMemoryRow({ id: "page-2", + key: "pagination:2", content: "middle", createdAt: "2026-04-02T00:00:00.000Z", updatedAt: "2026-04-02T00:00:00.000Z", }); insertMemoryRow({ id: "page-3", + key: "pagination:3", content: "newest", createdAt: "2026-04-03T00:00:00.000Z", updatedAt: "2026-04-03T00:00:00.000Z", @@ -251,18 +254,21 @@ test("listMemories applies query filtering before pagination and type stats", as test("listMemories supports page-based pagination (page 1)", async () => { insertMemoryRow({ id: "pg-1", + key: "page:test:1", content: "first", createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", }); insertMemoryRow({ id: "pg-2", + key: "page:test:2", content: "second", createdAt: "2026-04-02T00:00:00.000Z", updatedAt: "2026-04-02T00:00:00.000Z", }); insertMemoryRow({ id: "pg-3", + key: "page:test:3", content: "third", createdAt: "2026-04-03T00:00:00.000Z", updatedAt: "2026-04-03T00:00:00.000Z", @@ -279,18 +285,21 @@ test("listMemories supports page-based pagination (page 1)", async () => { test("listMemories supports page-based pagination (page 2 returns remainder)", async () => { insertMemoryRow({ id: "pg-1", + key: "page:test:1", content: "first", createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", }); insertMemoryRow({ id: "pg-2", + key: "page:test:2", content: "second", createdAt: "2026-04-02T00:00:00.000Z", updatedAt: "2026-04-02T00:00:00.000Z", }); insertMemoryRow({ id: "pg-3", + key: "page:test:3", content: "third", createdAt: "2026-04-03T00:00:00.000Z", updatedAt: "2026-04-03T00:00:00.000Z", @@ -307,6 +316,7 @@ test("listMemories supports page-based pagination (page 2 returns remainder)", a test("listMemories returns empty data for a page beyond the result set", async () => { insertMemoryRow({ id: "pg-1", + key: "page:test:1", content: "only entry", createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", @@ -320,12 +330,14 @@ test("listMemories returns empty data for a page beyond the result set", async ( test("listMemories page parameter defaults to page 1 when omitted with limit", async () => { insertMemoryRow({ id: "pg-1", + key: "page:test:1", content: "first", createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", }); insertMemoryRow({ id: "pg-2", + key: "page:test:2", content: "second", createdAt: "2026-04-02T00:00:00.000Z", updatedAt: "2026-04-02T00:00:00.000Z",