mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 07:01:40 +00:00
test: narrow telegram sticker cache imports
This commit is contained in:
@@ -102,6 +102,18 @@ beforeAll(async () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
for (const key of [
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
]) {
|
||||
vi.stubEnv(key, "");
|
||||
}
|
||||
loggerInfo.mockReset();
|
||||
loggerDebug.mockReset();
|
||||
});
|
||||
@@ -320,7 +332,7 @@ describe("resolveTelegramFetch", () => {
|
||||
});
|
||||
|
||||
it("uses EnvHttpProxyAgent dispatcher when proxy env is configured", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
|
||||
undiciFetch.mockResolvedValue({ ok: true } as Response);
|
||||
|
||||
const resolved = resolveTelegramFetchOrThrow(undefined, {
|
||||
@@ -351,7 +363,7 @@ describe("resolveTelegramFetch", () => {
|
||||
});
|
||||
|
||||
it("pins env-proxy transport policy onto proxyTls for proxied HTTPS requests", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
|
||||
undiciFetch.mockResolvedValue({ ok: true } as Response);
|
||||
|
||||
const resolved = resolveTelegramFetchOrThrow(undefined, {
|
||||
@@ -482,7 +494,7 @@ describe("resolveTelegramFetch", () => {
|
||||
});
|
||||
|
||||
it("does not blind-retry when sticky IPv4 fallback is disallowed for env proxy paths", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
|
||||
primeStickyFallbackRetry("EHOSTUNREACH", 1);
|
||||
|
||||
const resolved = resolveTelegramFetchOrThrow(undefined, {
|
||||
@@ -533,7 +545,7 @@ describe("resolveTelegramFetch", () => {
|
||||
});
|
||||
|
||||
it("arms sticky IPv4 fallback when env proxy init falls back to direct Agent", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
|
||||
EnvHttpProxyAgentCtor.mockImplementationOnce(function ThrowingEnvProxyAgent() {
|
||||
throw new Error("invalid proxy config");
|
||||
});
|
||||
@@ -551,8 +563,8 @@ describe("resolveTelegramFetch", () => {
|
||||
});
|
||||
|
||||
it("arms sticky IPv4 fallback when NO_PROXY bypasses telegram under env proxy", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("NO_PROXY", "api.telegram.org");
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("no_proxy", "api.telegram.org");
|
||||
await runDefaultStickyIpv4FallbackProbe();
|
||||
|
||||
expect(undiciFetch).toHaveBeenCalledTimes(3);
|
||||
@@ -567,7 +579,7 @@ describe("resolveTelegramFetch", () => {
|
||||
});
|
||||
|
||||
it("uses no_proxy over NO_PROXY when deciding env-proxy bypass", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("NO_PROXY", "");
|
||||
vi.stubEnv("no_proxy", "api.telegram.org");
|
||||
await runDefaultStickyIpv4FallbackProbe();
|
||||
@@ -577,7 +589,7 @@ describe("resolveTelegramFetch", () => {
|
||||
});
|
||||
|
||||
it("matches whitespace and wildcard no_proxy entries like EnvHttpProxyAgent", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("no_proxy", "localhost *.telegram.org");
|
||||
await runDefaultStickyIpv4FallbackProbe();
|
||||
|
||||
@@ -604,7 +616,7 @@ describe("resolveTelegramFetch", () => {
|
||||
});
|
||||
|
||||
it("falls back to Agent when env proxy dispatcher initialization fails", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7890");
|
||||
EnvHttpProxyAgentCtor.mockImplementationOnce(function ThrowingEnvProxyAgent() {
|
||||
throw new Error("invalid proxy config");
|
||||
});
|
||||
|
||||
133
extensions/telegram/src/sticker-cache-store.ts
Normal file
133
extensions/telegram/src/sticker-cache-store.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import path from "node:path";
|
||||
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
const CACHE_VERSION = 1;
|
||||
|
||||
export interface CachedSticker {
|
||||
fileId: string;
|
||||
fileUniqueId: string;
|
||||
emoji?: string;
|
||||
setName?: string;
|
||||
description: string;
|
||||
cachedAt: string;
|
||||
receivedFrom?: string;
|
||||
}
|
||||
|
||||
interface StickerCache {
|
||||
version: number;
|
||||
stickers: Record<string, CachedSticker>;
|
||||
}
|
||||
|
||||
function getCacheFile(): string {
|
||||
return path.join(resolveStateDir(), "telegram", "sticker-cache.json");
|
||||
}
|
||||
|
||||
function loadCache(): StickerCache {
|
||||
const data = loadJsonFile(getCacheFile());
|
||||
if (!data || typeof data !== "object") {
|
||||
return { version: CACHE_VERSION, stickers: {} };
|
||||
}
|
||||
const cache = data as StickerCache;
|
||||
if (cache.version !== CACHE_VERSION) {
|
||||
// Future: handle migration if needed
|
||||
return { version: CACHE_VERSION, stickers: {} };
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
function saveCache(cache: StickerCache): void {
|
||||
saveJsonFile(getCacheFile(), cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cached sticker by its unique ID.
|
||||
*/
|
||||
export function getCachedSticker(fileUniqueId: string): CachedSticker | null {
|
||||
const cache = loadCache();
|
||||
return cache.stickers[fileUniqueId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a sticker in the cache.
|
||||
*/
|
||||
export function cacheSticker(sticker: CachedSticker): void {
|
||||
const cache = loadCache();
|
||||
cache.stickers[sticker.fileUniqueId] = sticker;
|
||||
saveCache(cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search cached stickers by text query (fuzzy match on description + emoji + setName).
|
||||
*/
|
||||
export function searchStickers(query: string, limit = 10): CachedSticker[] {
|
||||
const cache = loadCache();
|
||||
const queryLower = normalizeLowercaseStringOrEmpty(query);
|
||||
const results: Array<{ sticker: CachedSticker; score: number }> = [];
|
||||
|
||||
for (const sticker of Object.values(cache.stickers)) {
|
||||
let score = 0;
|
||||
const descLower = normalizeLowercaseStringOrEmpty(sticker.description);
|
||||
|
||||
// Exact substring match in description
|
||||
if (descLower.includes(queryLower)) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// Word-level matching
|
||||
const queryWords = queryLower.split(/\s+/).filter(Boolean);
|
||||
const descWords = descLower.split(/\s+/);
|
||||
for (const qWord of queryWords) {
|
||||
if (descWords.some((dWord) => dWord.includes(qWord))) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji match
|
||||
if (sticker.emoji && query.includes(sticker.emoji)) {
|
||||
score += 8;
|
||||
}
|
||||
|
||||
// Set name match
|
||||
if (normalizeLowercaseStringOrEmpty(sticker.setName).includes(queryLower)) {
|
||||
score += 3;
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
results.push({ sticker, score });
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
.toSorted((a, b) => b.score - a.score)
|
||||
.slice(0, limit)
|
||||
.map((r) => r.sticker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached stickers (for debugging/listing).
|
||||
*/
|
||||
export function getAllCachedStickers(): CachedSticker[] {
|
||||
const cache = loadCache();
|
||||
return Object.values(cache.stickers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics.
|
||||
*/
|
||||
export function getCacheStats(): { count: number; oldestAt?: string; newestAt?: string } {
|
||||
const cache = loadCache();
|
||||
const stickers = Object.values(cache.stickers);
|
||||
if (stickers.length === 0) {
|
||||
return { count: 0 };
|
||||
}
|
||||
const sorted = [...stickers].toSorted(
|
||||
(a, b) => new Date(a.cachedAt).getTime() - new Date(b.cachedAt).getTime(),
|
||||
);
|
||||
return {
|
||||
count: stickers.length,
|
||||
oldestAt: sorted[0]?.cachedAt,
|
||||
newestAt: sorted[sorted.length - 1]?.cachedAt,
|
||||
};
|
||||
}
|
||||
@@ -1,43 +1,16 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({
|
||||
resolveApiKeyForProvider: vi.fn(),
|
||||
findModelInCatalog: vi.fn(),
|
||||
loadModelCatalog: vi.fn(async () => []),
|
||||
modelSupportsVision: vi.fn(() => false),
|
||||
resolveDefaultModelForAgent: vi.fn(() => ({ provider: "openai", model: "gpt-5.2" })),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/media-runtime", () => ({
|
||||
resolveAutoImageModel: vi.fn(async () => null),
|
||||
resolveAutoMediaKeyProviders: vi.fn(() => ["openai"]),
|
||||
resolveDefaultMediaModel: vi.fn(() => "gpt-4.1-mini"),
|
||||
}));
|
||||
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getTelegramRuntime: () => ({
|
||||
mediaUnderstanding: {
|
||||
describeImageFileWithModel: vi.fn(),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import * as stickerCache from "./sticker-cache-store.js";
|
||||
|
||||
const TEST_CACHE_DIR = "/tmp/openclaw-test-sticker-cache/telegram";
|
||||
const TEST_CACHE_FILE = path.join(TEST_CACHE_DIR, "sticker-cache.json");
|
||||
|
||||
type StickerCacheModule = typeof import("./sticker-cache.js");
|
||||
|
||||
let stickerCache: StickerCacheModule;
|
||||
|
||||
describe("sticker-cache", () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-test-sticker-cache";
|
||||
fs.rmSync("/tmp/openclaw-test-sticker-cache", { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_CACHE_DIR, { recursive: true });
|
||||
vi.resetModules();
|
||||
stickerCache = await import("./sticker-cache.js");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import path from "node:path";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { ModelCatalogEntry } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import {
|
||||
@@ -8,142 +7,22 @@ import {
|
||||
} from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveAutoImageModel } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
resolveAutoMediaKeyProviders,
|
||||
resolveDefaultMediaModel,
|
||||
} from "openclaw/plugin-sdk/media-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { STATE_DIR } from "openclaw/plugin-sdk/state-paths";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { getTelegramRuntime } from "./runtime.js";
|
||||
|
||||
const CACHE_FILE = path.join(STATE_DIR, "telegram", "sticker-cache.json");
|
||||
const CACHE_VERSION = 1;
|
||||
|
||||
export interface CachedSticker {
|
||||
fileId: string;
|
||||
fileUniqueId: string;
|
||||
emoji?: string;
|
||||
setName?: string;
|
||||
description: string;
|
||||
cachedAt: string;
|
||||
receivedFrom?: string;
|
||||
}
|
||||
|
||||
interface StickerCache {
|
||||
version: number;
|
||||
stickers: Record<string, CachedSticker>;
|
||||
}
|
||||
|
||||
function loadCache(): StickerCache {
|
||||
const data = loadJsonFile(CACHE_FILE);
|
||||
if (!data || typeof data !== "object") {
|
||||
return { version: CACHE_VERSION, stickers: {} };
|
||||
}
|
||||
const cache = data as StickerCache;
|
||||
if (cache.version !== CACHE_VERSION) {
|
||||
// Future: handle migration if needed
|
||||
return { version: CACHE_VERSION, stickers: {} };
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
function saveCache(cache: StickerCache): void {
|
||||
saveJsonFile(CACHE_FILE, cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cached sticker by its unique ID.
|
||||
*/
|
||||
export function getCachedSticker(fileUniqueId: string): CachedSticker | null {
|
||||
const cache = loadCache();
|
||||
return cache.stickers[fileUniqueId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a sticker in the cache.
|
||||
*/
|
||||
export function cacheSticker(sticker: CachedSticker): void {
|
||||
const cache = loadCache();
|
||||
cache.stickers[sticker.fileUniqueId] = sticker;
|
||||
saveCache(cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search cached stickers by text query (fuzzy match on description + emoji + setName).
|
||||
*/
|
||||
export function searchStickers(query: string, limit = 10): CachedSticker[] {
|
||||
const cache = loadCache();
|
||||
const queryLower = normalizeLowercaseStringOrEmpty(query);
|
||||
const results: Array<{ sticker: CachedSticker; score: number }> = [];
|
||||
|
||||
for (const sticker of Object.values(cache.stickers)) {
|
||||
let score = 0;
|
||||
const descLower = normalizeLowercaseStringOrEmpty(sticker.description);
|
||||
|
||||
// Exact substring match in description
|
||||
if (descLower.includes(queryLower)) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// Word-level matching
|
||||
const queryWords = queryLower.split(/\s+/).filter(Boolean);
|
||||
const descWords = descLower.split(/\s+/);
|
||||
for (const qWord of queryWords) {
|
||||
if (descWords.some((dWord) => dWord.includes(qWord))) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji match
|
||||
if (sticker.emoji && query.includes(sticker.emoji)) {
|
||||
score += 8;
|
||||
}
|
||||
|
||||
// Set name match
|
||||
if (normalizeLowercaseStringOrEmpty(sticker.setName).includes(queryLower)) {
|
||||
score += 3;
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
results.push({ sticker, score });
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
.toSorted((a, b) => b.score - a.score)
|
||||
.slice(0, limit)
|
||||
.map((r) => r.sticker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached stickers (for debugging/listing).
|
||||
*/
|
||||
export function getAllCachedStickers(): CachedSticker[] {
|
||||
const cache = loadCache();
|
||||
return Object.values(cache.stickers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics.
|
||||
*/
|
||||
export function getCacheStats(): { count: number; oldestAt?: string; newestAt?: string } {
|
||||
const cache = loadCache();
|
||||
const stickers = Object.values(cache.stickers);
|
||||
if (stickers.length === 0) {
|
||||
return { count: 0 };
|
||||
}
|
||||
const sorted = [...stickers].toSorted(
|
||||
(a, b) => new Date(a.cachedAt).getTime() - new Date(b.cachedAt).getTime(),
|
||||
);
|
||||
return {
|
||||
count: stickers.length,
|
||||
oldestAt: sorted[0]?.cachedAt,
|
||||
newestAt: sorted[sorted.length - 1]?.cachedAt,
|
||||
};
|
||||
}
|
||||
export {
|
||||
cacheSticker,
|
||||
getAllCachedStickers,
|
||||
getCachedSticker,
|
||||
getCacheStats,
|
||||
searchStickers,
|
||||
type CachedSticker,
|
||||
} from "./sticker-cache-store.js";
|
||||
|
||||
const STICKER_DESCRIPTION_PROMPT =
|
||||
"Describe this sticker image in 1-2 sentences. Focus on what the sticker depicts (character, object, action, emotion). Be concise and objective.";
|
||||
|
||||
Reference in New Issue
Block a user