mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 21:32:10 +03:00
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -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": [
|
||||
|
||||
@@ -205,17 +205,21 @@ export default function MemoryPage() {
|
||||
{health !== null && (
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded-full ${health.working ? "bg-green-500" : "bg-red-500"}`}
|
||||
title={health.working ? `Pipeline OK (${health.latencyMs}ms)` : "Pipeline error"}
|
||||
title={
|
||||
health.working
|
||||
? t("pipelineOk", { latencyMs: health.latencyMs })
|
||||
: t("pipelineError")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{health === null && !checkingHealth && (
|
||||
<span
|
||||
className="inline-block w-3 h-3 rounded-full bg-gray-400"
|
||||
title="Health unknown"
|
||||
title={t("healthUnknown")}
|
||||
/>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={checkHealth} disabled={checkingHealth}>
|
||||
{checkingHealth ? "Checking..." : "Check Health"}
|
||||
{checkingHealth ? t("checkingHealth") : t("checkHealth")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,7 +325,7 @@ export default function MemoryPage() {
|
||||
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
Page {page} of {totalPages} ({total} total)
|
||||
{t("pageInfo", { page, totalPages, total })}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -330,7 +334,7 @@ export default function MemoryPage() {
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
Previous
|
||||
{t("previous")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -338,7 +342,7 @@ export default function MemoryPage() {
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
>
|
||||
Next
|
||||
{t("next")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,47 +360,47 @@ export default function MemoryPage() {
|
||||
onClick={() => setAddDialogOpen(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddMemory}
|
||||
loading={isSubmitting}
|
||||
disabled={!newMemory.key || !newMemory.content}
|
||||
>
|
||||
Save
|
||||
{t("save")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Type</label>
|
||||
<label className="block text-sm font-medium mb-1">{t("type")}</label>
|
||||
<Select
|
||||
value={newMemory.type}
|
||||
onChange={(e) => setNewMemory({ ...newMemory, type: e.target.value as any })}
|
||||
className="w-full"
|
||||
>
|
||||
<option value="factual">Factual</option>
|
||||
<option value="episodic">Episodic</option>
|
||||
<option value="procedural">Procedural</option>
|
||||
<option value="semantic">Semantic</option>
|
||||
<option value="factual">{t("factual")}</option>
|
||||
<option value="episodic">{t("episodic")}</option>
|
||||
<option value="procedural">{t("procedural")}</option>
|
||||
<option value="semantic">{t("semantic")}</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Key</label>
|
||||
<label className="block text-sm font-medium mb-1">{t("key")}</label>
|
||||
<Input
|
||||
value={newMemory.key}
|
||||
onChange={(e) => setNewMemory({ ...newMemory, key: e.target.value })}
|
||||
placeholder="e.g., user_preference_theme"
|
||||
placeholder={t("keyPlaceholder")}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Content</label>
|
||||
<label className="block text-sm font-medium mb-1">{t("content")}</label>
|
||||
<Input
|
||||
value={newMemory.content}
|
||||
onChange={(e) => setNewMemory({ ...newMemory, content: e.target.value })}
|
||||
placeholder="e.g., Prefers dark mode"
|
||||
placeholder={t("contentPlaceholder")}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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": "技能",
|
||||
|
||||
@@ -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<typeof getDbInstance>,
|
||||
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<Memory, "id" | "createdAt" | "updatedAt">
|
||||
): Promise<Memory> {
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user