diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b398497de..27b330aa9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,8 +20,8 @@ jobs: close-pr-message: 'This PR has been automatically closed due to inactivity.' days-before-stale: 30 # Days of inactivity before marking as stale days-before-close: 7 # Days of inactivity before closing stale issues/PRs - stale-issue-label: 'stale' - stale-pr-label: 'stale' - exempt-issue-labels: 'do not close' - exempt-pr-labels: 'do not close' + stale-issue-label: 'status:stale' + stale-pr-label: 'status:stale' + exempt-issue-labels: 'status:do not close' + exempt-pr-labels: 'status:do not close' remove-stale-when-updated: true diff --git a/apps/memos-local-openclaw/src/embedding/providers/gemini.ts b/apps/memos-local-openclaw/src/embedding/providers/gemini.ts index 6156b8a7b..70eeaa1df 100644 --- a/apps/memos-local-openclaw/src/embedding/providers/gemini.ts +++ b/apps/memos-local-openclaw/src/embedding/providers/gemini.ts @@ -5,7 +5,7 @@ export async function embedGemini( cfg: EmbeddingConfig, log: Logger, ): Promise { - const model = cfg.model ?? "text-embedding-004"; + const model = cfg.model ?? "gemini-embedding-001"; const endpoint = cfg.endpoint ?? `https://generativelanguage.googleapis.com/v1beta/models/${model}:batchEmbedContents`; diff --git a/apps/memos-local-openclaw/src/viewer/html.ts b/apps/memos-local-openclaw/src/viewer/html.ts index 8c48f7017..d091e06cd 100644 --- a/apps/memos-local-openclaw/src/viewer/html.ts +++ b/apps/memos-local-openclaw/src/viewer/html.ts @@ -7234,7 +7234,7 @@ var _providerDefaults={ cohere:{endpoint:'https://api.cohere.com/v2',embModel:'embed-english-v3.0'}, mistral:{endpoint:'https://api.mistral.ai/v1',embModel:'mistral-embed'}, voyage:{endpoint:'https://api.voyageai.com/v1',embModel:'voyage-3'}, - gemini:{endpoint:'',embModel:'text-embedding-004',chatModel:'gemini-2.0-flash'}, + gemini:{endpoint:'',embModel:'gemini-embedding-001',chatModel:'gemini-2.0-flash'}, zhipu:{endpoint:'https://open.bigmodel.cn/api/paas/v4',embModel:'embedding-3',chatModel:'glm-4-flash'}, deepseek:{endpoint:'https://api.deepseek.com/v1',chatModel:'deepseek-chat'}, bailian:{endpoint:'https://coding.dashscope.aliyuncs.com/v1',embModel:'text-embedding-v3',chatModel:'qwen-max'}, diff --git a/apps/memos-local-openclaw/src/viewer/server.ts b/apps/memos-local-openclaw/src/viewer/server.ts index e3474cc70..6ee8184fe 100644 --- a/apps/memos-local-openclaw/src/viewer/server.ts +++ b/apps/memos-local-openclaw/src/viewer/server.ts @@ -3874,11 +3874,31 @@ export class ViewerServer { return vecs[0].length; } if (provider === "gemini") { - const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`; + const geminiModel = model || "gemini-embedding-001"; + const geminiEndpoint = ( + endpoint || + `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:batchEmbedContents` + ).replace(/\/+$/, ""); + const separator = geminiEndpoint.includes("?") ? "&" : "?"; + // Only append the API key for the default Gemini endpoint; for custom + // endpoints the caller is responsible for authentication and we must + // never leak the Gemini API key to a user-controlled server. + const url = endpoint + ? geminiEndpoint + : `${geminiEndpoint}${separator}key=${apiKey}`; + // When the caller supplies a custom endpoint, don't hardcode the + // default model name in the request body either — the proxy may + // route based on this field. + const bodyModel = endpoint ? undefined : `models/${geminiModel}`; const resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ content: { parts: [{ text: "test embedding vector" }] } }), + body: JSON.stringify({ + requests: [{ + ...(bodyModel ? { model: bodyModel } : {}), + content: { parts: [{ text: "test embedding vector" }] }, + }], + }), signal: AbortSignal.timeout(15_000), }); if (!resp.ok) { @@ -3886,7 +3906,7 @@ export class ViewerServer { throw new Error(`Gemini embed ${resp.status}: ${txt}`); } const json = await resp.json() as any; - const vec = json?.embedding?.values; + const vec = json?.embeddings?.[0]?.values; if (!Array.isArray(vec) || vec.length === 0) { throw new Error("Gemini returned empty embedding vector"); } diff --git a/apps/memos-local-openclaw/tests/gemini-viewer-test.test.ts b/apps/memos-local-openclaw/tests/gemini-viewer-test.test.ts new file mode 100644 index 000000000..d87340989 --- /dev/null +++ b/apps/memos-local-openclaw/tests/gemini-viewer-test.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); + +describe("Gemini viewer embedding test", () => { + it("uses the batch embedding API and current default model", () => { + const server = fs.readFileSync(path.join(root, "src/viewer/server.ts"), "utf-8"); + const provider = fs.readFileSync( + path.join(root, "src/embedding/providers/gemini.ts"), + "utf-8", + ); + const html = fs.readFileSync(path.join(root, "src/viewer/html.ts"), "utf-8"); + + expect(server).toContain("v1beta/models/${geminiModel}:batchEmbedContents"); + expect(server).toContain("const geminiEndpoint = ("); + expect(server).toContain("requests: [{"); + expect(server).toContain("json?.embeddings?.[0]?.values"); + expect(server).not.toContain(":embedContent?key=${apiKey}"); + expect(provider).toContain('cfg.model ?? "gemini-embedding-001"'); + expect(html).toContain("embModel:'gemini-embedding-001'"); + }); + + it("does not append the Gemini API key when a custom endpoint is supplied", () => { + const server = fs.readFileSync(path.join(root, "src/viewer/server.ts"), "utf-8"); + // The url-construction block must branch on `endpoint` so that user- + // supplied endpoints never receive ?key=. + expect(server).toMatch(/const url = endpoint\s*\?\s*geminiEndpoint\s*:/); + // And the model field must be omitted (or fall back to undefined) when + // a custom endpoint is supplied, so a proxy can route correctly. + expect(server).toMatch(/const bodyModel = endpoint\s*\?\s*undefined\s*:/); + }); +}); diff --git a/docs/cn/open_source/modules/dream.md b/docs/cn/open_source/modules/dream.md index a83beab42..55cd9e2cf 100644 --- a/docs/cn/open_source/modules/dream.md +++ b/docs/cn/open_source/modules/dream.md @@ -79,7 +79,7 @@ Dream 模拟这一点:从*未完成的内在动机*出发,而不是从原始 ```json { "motive_id": "motive:dream_memory_strategy_alignment", - "description": "Several conversations failed for the same hidden reason: weekly reporting, future planning, and filter design were treated as separate tasks, while the user needed a shared strategic narrative.", + "description": "几次对话失败,背后是同一个隐藏原因:周报、未来规划和 filter 设计被当成三个独立任务,而用户需要的是一条统一的战略叙事。", "memory_ids": ["weekly_report_thread", "future_planning_thread", "filter_design_thread"] } ```