mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
Enable unsafe member access linting (#2474)
This commit is contained in:
parent
3f12c14444
commit
e03234137d
17 changed files with 118 additions and 141 deletions
|
|
@ -103,10 +103,8 @@ export default [
|
|||
// are candidates to enable in small follow-up PRs.
|
||||
|
||||
// --- Heavy: any-flow through Obsidian/LangChain APIs ---
|
||||
// no-unsafe-member-access: enabled globally; tests and heavy source files
|
||||
// are exempted via per-file overrides below (see "no-unsafe-member-access
|
||||
// exemptions"). Remaining source files (≤5 violations each) were fixed in
|
||||
// this PR.
|
||||
// no-unsafe-member-access: enabled globally; tests are exempted via the
|
||||
// test-file override below.
|
||||
"@typescript-eslint/no-unsafe-assignment": "off", // enabled for tests below; follow-up PR for production
|
||||
"@typescript-eslint/no-unsafe-call": "off", // 107 violations
|
||||
|
||||
|
|
@ -158,58 +156,6 @@ export default [
|
|||
},
|
||||
},
|
||||
|
||||
// no-unsafe-member-access exemptions: heavy source files that flow `any`
|
||||
// through Obsidian / LangChain / Bedrock APIs. Counts are current as of the
|
||||
// PR that enabled the rule; pick these off one at a time in follow-up PRs.
|
||||
{
|
||||
files: [
|
||||
"src/LLMProviders/BedrockChatModel.ts", // 106
|
||||
"src/LLMProviders/ChatOpenRouter.ts", // 28
|
||||
"src/LLMProviders/CustomOpenAIEmbeddings.ts", // 16
|
||||
"src/LLMProviders/brevilabsClient.ts", // 7
|
||||
"src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts", // 13
|
||||
"src/LLMProviders/chainRunner/BaseChainRunner.ts", // 7
|
||||
"src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts", // 55
|
||||
"src/LLMProviders/chainRunner/VaultQAChainRunner.ts", // 9
|
||||
"src/LLMProviders/chainRunner/utils/ActionBlockStreamer.ts", // 8
|
||||
"src/LLMProviders/chainRunner/utils/ThinkBlockStreamer.ts", // 33
|
||||
"src/LLMProviders/chainRunner/utils/chatHistoryUtils.ts", // 17
|
||||
"src/LLMProviders/chainRunner/utils/citationUtils.ts", // 11
|
||||
"src/LLMProviders/chainRunner/utils/finishReasonDetector.ts", // 29
|
||||
"src/LLMProviders/chainRunner/utils/modelAdapter.ts", // 9
|
||||
"src/LLMProviders/chainRunner/utils/promptPayloadRecorder.ts", // 12
|
||||
"src/LLMProviders/chainRunner/utils/searchResultUtils.ts", // 81
|
||||
"src/LLMProviders/chainRunner/utils/toolExecution.ts", // 9
|
||||
"src/LLMProviders/chatModelManager.ts", // 9
|
||||
"src/LLMProviders/selfHostServices.ts", // 9
|
||||
"src/commands/customCommandManager.ts", // 10
|
||||
"src/commands/customCommandUtils.ts", // 10
|
||||
"src/commands/index.ts", // 14
|
||||
"src/components/chat-components/ChatControls.tsx", // 8
|
||||
"src/components/chat-components/ChatInput.tsx", // 14
|
||||
"src/components/modals/SourcesModal.tsx", // 33
|
||||
"src/contextProcessor.ts", // 17
|
||||
"src/core/ChatPersistenceManager.ts", // 10
|
||||
"src/encryptionService.ts", // 6
|
||||
"src/projects/projectUtils.ts", // 38
|
||||
"src/search/chunkedStorage.ts", // 28
|
||||
"src/search/dbOperations.ts", // 27
|
||||
"src/search/hybridRetriever.ts", // 11
|
||||
"src/search/indexOperations.ts", // 15
|
||||
"src/search/v3/TieredLexicalRetriever.ts", // 9
|
||||
"src/settings/providerModels.ts", // 20
|
||||
"src/system-prompts/systemPromptUtils.ts", // 9
|
||||
"src/tools/FileParserManager.ts", // 11
|
||||
"src/tools/SearchTools.ts", // 11
|
||||
"src/tools/ToolResultFormatter.ts", // 106
|
||||
"src/utils.ts", // 49
|
||||
"src/utils/rateLimitUtils.ts", // 10
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
},
|
||||
},
|
||||
|
||||
// Tests have been cleaned of unsafe `any` assignments. Production code
|
||||
// (~499 violations) is a follow-up; keep tests enforced.
|
||||
{
|
||||
|
|
|
|||
|
|
@ -448,13 +448,19 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return toolCalls.map((call) => ({
|
||||
name: call?.function?.name,
|
||||
args: call?.function?.arguments,
|
||||
id: call?.id,
|
||||
index: call?.index,
|
||||
type: "tool_call_chunk" as const,
|
||||
}));
|
||||
return toolCalls.map((rawCall) => {
|
||||
const call = rawCall as
|
||||
| { function?: { name?: string; arguments?: string }; id?: string; index?: number }
|
||||
| null
|
||||
| undefined;
|
||||
return {
|
||||
name: call?.function?.name,
|
||||
args: call?.function?.arguments,
|
||||
id: call?.id,
|
||||
index: call?.index,
|
||||
type: "tool_call_chunk" as const,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -55,13 +55,13 @@ export class CustomOpenAIEmbeddings extends OpenAIEmbeddings {
|
|||
);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const responseData = (await response.json()) as { data?: Array<{ embedding?: unknown }> };
|
||||
|
||||
if (!responseData.data || !Array.isArray(responseData.data)) {
|
||||
throw new Error("Invalid API response format: missing or invalid data array");
|
||||
}
|
||||
|
||||
return (responseData.data as Array<{ embedding?: unknown }>).map((item) => {
|
||||
return responseData.data.map((item) => {
|
||||
if (!item.embedding || !Array.isArray(item.embedding)) {
|
||||
throw new Error("Invalid API response format: missing or invalid embedding array");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1030,9 +1030,15 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner {
|
|||
})
|
||||
);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
for await (const rawChunk of stream) {
|
||||
if (abortController.signal.aborted) break;
|
||||
|
||||
const chunk = rawChunk as {
|
||||
response_metadata?: { finish_reason?: string };
|
||||
tool_call_chunks?: unknown;
|
||||
content?: unknown;
|
||||
};
|
||||
|
||||
// Check for MALFORMED_FUNCTION_CALL error - throw to trigger fallback
|
||||
const finishReason = chunk.response_metadata?.finish_reason;
|
||||
if (finishReason === "MALFORMED_FUNCTION_CALL") {
|
||||
|
|
|
|||
|
|
@ -1224,7 +1224,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
|
|||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(toolResult.result);
|
||||
const parsed = JSON.parse(toolResult.result) as { type?: unknown; documents?: unknown };
|
||||
const searchResults =
|
||||
parsed &&
|
||||
typeof parsed === "object" &&
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ export function normalizeCitations(content: string, map: Map<number, number>): s
|
|||
});
|
||||
|
||||
// Handle multiple citations: [^n, ^m] -> [n, m]
|
||||
result = result.replace(/\[\^(\d+(?:\s*,\s*\^?\d+)*)\]/g, (match, citationList) => {
|
||||
result = result.replace(/\[\^(\d+(?:\s*,\s*\^?\d+)*)\]/g, (match, citationList: string) => {
|
||||
// Split and process each number in the list
|
||||
const processedNumbers = citationList
|
||||
.split(",")
|
||||
|
|
|
|||
|
|
@ -55,10 +55,10 @@ const GOOGLE_SAFETY_SETTINGS_BLOCK_NONE: SafetySetting[] = [
|
|||
// vocabulary from tiktoken.pages.dev, which blocks all LLM calls when the CDN is
|
||||
// unreachable. This char/4 estimation is the same fallback LangChain uses internally
|
||||
// before tiktoken loads. Actual token usage comes from API response metadata.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- patching a private prototype method requires any cast
|
||||
(BaseLanguageModel.prototype as any).getNumTokens = async (
|
||||
content: string | Array<{ type: string; text?: string }>
|
||||
) => {
|
||||
|
||||
(
|
||||
BaseLanguageModel.prototype as { getNumTokens: (...args: unknown[]) => Promise<number> }
|
||||
).getNumTokens = async (content: string | Array<{ type: string; text?: string }>) => {
|
||||
const text =
|
||||
typeof content === "string"
|
||||
? content
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ async function firecrawlSearch(query: string, apiKey: string): Promise<SelfHostW
|
|||
throw new Error(`Firecrawl search failed (${response.status}): ${text}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as {
|
||||
data?: FirecrawlSearchResult[] | { web?: FirecrawlSearchResult[] };
|
||||
};
|
||||
|
||||
// v2 returns { data: { web: [...] } }, older responses return { data: [...] }
|
||||
const rawData = json?.data;
|
||||
|
|
@ -113,9 +115,12 @@ async function perplexitySonarSearch(
|
|||
throw new Error(`Perplexity Sonar search failed (${response.status}): ${text}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as {
|
||||
choices?: Array<{ message?: { content?: string } }>;
|
||||
citations?: unknown;
|
||||
};
|
||||
const content = json?.choices?.[0]?.message?.content ?? "";
|
||||
const citations: string[] = Array.isArray(json?.citations) ? json.citations : [];
|
||||
const citations: string[] = Array.isArray(json?.citations) ? (json.citations as string[]) : [];
|
||||
|
||||
return { content, citations };
|
||||
}
|
||||
|
|
@ -154,7 +159,7 @@ export async function selfHostYoutube4llm(url: string): Promise<Youtube4llmRespo
|
|||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as { content?: string };
|
||||
const elapsed = Date.now() - startTime;
|
||||
logInfo(`[selfHostYoutube4llm] transcript received in ${elapsed}ms`);
|
||||
return {
|
||||
|
|
@ -164,12 +169,12 @@ export async function selfHostYoutube4llm(url: string): Promise<Youtube4llmRespo
|
|||
}
|
||||
|
||||
if (response.status === 201 || response.status === 202) {
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as { job_id?: string };
|
||||
const jobId = json.job_id;
|
||||
if (!jobId) {
|
||||
throw new Error("Supadata returned async status but no job_id");
|
||||
}
|
||||
return await pollSupadataJob(jobId as string, apiKey, startTime);
|
||||
return await pollSupadataJob(jobId, apiKey, startTime);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
|
@ -199,7 +204,7 @@ async function pollSupadataJob(
|
|||
});
|
||||
|
||||
if (pollResponse.status === 200) {
|
||||
const json = await pollResponse.json();
|
||||
const json = (await pollResponse.json()) as { content?: string };
|
||||
const elapsed = Date.now() - startTime;
|
||||
logInfo(`[selfHostYoutube4llm] async transcript completed in ${elapsed}ms`);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -66,13 +66,16 @@ export class CustomCommandManager {
|
|||
commandFile = await app.vault.create(filePath, command.content);
|
||||
}
|
||||
|
||||
await app.fileManager.processFrontMatter(commandFile, (frontmatter) => {
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
|
||||
frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
|
||||
frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
|
||||
frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
|
||||
});
|
||||
await app.fileManager.processFrontMatter(
|
||||
commandFile,
|
||||
(frontmatter: Record<string, unknown>) => {
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
|
||||
frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
|
||||
frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
|
||||
frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
|
||||
}
|
||||
);
|
||||
|
||||
if (!mergedOptions.skipStoreUpdate) {
|
||||
updateCachedCommand(command, command.title);
|
||||
|
|
@ -126,13 +129,16 @@ export class CustomCommandManager {
|
|||
|
||||
if (commandFile instanceof TFile) {
|
||||
await app.vault.modify(commandFile, command.content);
|
||||
await app.fileManager.processFrontMatter(commandFile, (frontmatter) => {
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
|
||||
frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
|
||||
frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
|
||||
frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
|
||||
});
|
||||
await app.fileManager.processFrontMatter(
|
||||
commandFile,
|
||||
(frontmatter: Record<string, unknown>) => {
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
|
||||
frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
|
||||
frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
|
||||
frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
|
||||
}
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
removePendingFileWrite(filePath);
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ export function getNextCustomCommandOrder(): number {
|
|||
export async function ensureCommandFrontmatter(file: TFile, command: CustomCommand) {
|
||||
try {
|
||||
addPendingFileWrite(file.path);
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
|
||||
if (frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] == null) {
|
||||
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ function getSafeStorage(): SafeStorage | null {
|
|||
if (safeStorageInternal) return safeStorageInternal;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
safeStorageInternal = require("electron")?.remote?.safeStorage as SafeStorage | null;
|
||||
const electron = require("electron") as { remote?: { safeStorage?: SafeStorage } } | undefined;
|
||||
safeStorageInternal = electron?.remote?.safeStorage ?? null;
|
||||
return safeStorageInternal;
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export async function writeProjectFrontmatter(
|
|||
const webUrls = splitUrlsStringToArray(project.contextSource?.webUrls || "");
|
||||
const youtubeUrls = splitUrlsStringToArray(project.contextSource?.youtubeUrls || "");
|
||||
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
|
||||
// Reason: project.id is the stable logical identity, always set by createProject/migration.
|
||||
// Do NOT fallback to folderName — with name-based folders, folderName is derived from
|
||||
// project name, not id, so it cannot serve as an id substitute.
|
||||
|
|
@ -423,7 +423,7 @@ export async function ensureProjectFrontmatter(
|
|||
try {
|
||||
if (!alreadyPending) addPendingFileWrite(file.path);
|
||||
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
|
||||
// Reason: do NOT fallback to record.folderName for id — with name-based folders,
|
||||
// folderName is derived from project name, not id.
|
||||
if (frontmatter[COPILOT_PROJECT_ID] == null && record.project.id) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const LEGACY_INDEX_SUFFIX = ".json";
|
|||
|
||||
export interface ChunkMetadata {
|
||||
numPartitions: number;
|
||||
vectorLength: number;
|
||||
schema: Record<string, string>;
|
||||
lastModified: number;
|
||||
documentPartitions: Record<string, number>;
|
||||
|
|
@ -138,7 +137,6 @@ export class ChunkedStorage {
|
|||
if (!rawDocs || rawDocs.length === 0) {
|
||||
const metadata: ChunkMetadata = {
|
||||
numPartitions,
|
||||
vectorLength: db.schema.embedding.match(/\d+/)[0],
|
||||
schema: db.schema,
|
||||
lastModified: Date.now(),
|
||||
documentPartitions: {},
|
||||
|
|
@ -158,7 +156,6 @@ export class ChunkedStorage {
|
|||
|
||||
const metadata: ChunkMetadata = {
|
||||
numPartitions,
|
||||
vectorLength: db.schema.embedding.match(/\d+/)[0],
|
||||
schema: db.schema,
|
||||
lastModified: Date.now(),
|
||||
documentPartitions: Object.fromEntries(
|
||||
|
|
@ -242,7 +239,7 @@ export class ChunkedStorage {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error(`Error saving database:`, error);
|
||||
throw new CustomError(`Failed to save database: ${error.message}`);
|
||||
throw new CustomError(`Failed to save database: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,14 +282,19 @@ export class ChunkedStorage {
|
|||
});
|
||||
|
||||
// Load and merge all partitions
|
||||
let mergedData = null;
|
||||
const allChunks = [];
|
||||
type ChunkData = {
|
||||
internalDocumentIDStore: { internalIdToId: string[] };
|
||||
docs: { docs: Record<string, unknown>; count: number };
|
||||
index: { vectorIndexes: { embedding: { vectors: Record<string, unknown> } } };
|
||||
};
|
||||
let mergedData: ChunkData | null = null;
|
||||
const allChunks: ChunkData[] = [];
|
||||
|
||||
// First, load all chunks
|
||||
for (let i = 0; i < metadata.numPartitions; i++) {
|
||||
const chunkPath = this.getChunkPath(i);
|
||||
if (await this.app.vault.adapter.exists(chunkPath)) {
|
||||
const chunkData = JSON.parse(await this.app.vault.adapter.read(chunkPath));
|
||||
const chunkData = JSON.parse(await this.app.vault.adapter.read(chunkPath)) as ChunkData;
|
||||
allChunks.push(chunkData);
|
||||
|
||||
// First chunk contains global data
|
||||
|
|
@ -313,7 +315,7 @@ export class ChunkedStorage {
|
|||
for (const internalId of mergedData.internalDocumentIDStore.internalIdToId) {
|
||||
// Find document in any chunk
|
||||
const doc = allChunks
|
||||
.flatMap((chunk) => Object.values(chunk.docs.docs as Record<string, unknown>))
|
||||
.flatMap((chunk) => Object.values(chunk.docs.docs))
|
||||
.find((doc) => (doc as Record<string, unknown>).id === internalId);
|
||||
|
||||
if (doc) {
|
||||
|
|
@ -333,16 +335,13 @@ export class ChunkedStorage {
|
|||
// upsert cycles. These "ghost" IDs cause position mismatches after load,
|
||||
// where some user IDs point to wrong doc positions or undefined entries.
|
||||
mergedData.internalDocumentIDStore.internalIdToId = Object.values(orderedDocs).map(
|
||||
(doc: { id: string }): string => doc.id
|
||||
(doc): string => (doc as { id: string }).id
|
||||
);
|
||||
|
||||
// Merge vectors from all chunks
|
||||
mergedData.index.vectorIndexes.embedding.vectors = Object.assign(
|
||||
{},
|
||||
...allChunks.map(
|
||||
(chunk) =>
|
||||
(chunk.index?.vectorIndexes?.embedding?.vectors as Record<string, unknown>) || {}
|
||||
)
|
||||
...allChunks.map((chunk) => chunk.index?.vectorIndexes?.embedding?.vectors || {})
|
||||
);
|
||||
|
||||
// Load merged data into database
|
||||
|
|
@ -350,7 +349,7 @@ export class ChunkedStorage {
|
|||
return newDb;
|
||||
} catch (error) {
|
||||
console.error(`Error loading database:`, error);
|
||||
throw new CustomError(`Failed to load database: ${error.message}`);
|
||||
throw new CustomError(`Failed to load database: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -373,7 +372,7 @@ export class ChunkedStorage {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error(`Error clearing storage:`, error);
|
||||
throw new CustomError(`Failed to clear storage: ${error.message}`);
|
||||
throw new CustomError(`Failed to clear storage: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ export class HybridRetriever extends BaseRetriever {
|
|||
created_at: hit.document.created_at,
|
||||
nchars: hit.document.nchars,
|
||||
// Expose chunkId explicitly for cross-engine deduplication
|
||||
chunkId: hit.document.metadata?.chunkId,
|
||||
chunkId: (hit.document.metadata as { chunkId?: string } | undefined)?.chunkId,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
@ -268,8 +268,8 @@ export class HybridRetriever extends BaseRetriever {
|
|||
// Combine and deduplicate results
|
||||
const combinedResults = [...dailyNoteResultsWithContext, ...timeIntervalDocuments];
|
||||
const uniqueResults = Array.from(
|
||||
new Set(combinedResults.map((doc): string => doc.metadata.id as string))
|
||||
).map((id) => combinedResults.find((doc) => doc.metadata.id === id));
|
||||
new Set(combinedResults.map((doc): string => (doc.metadata as { id: string }).id))
|
||||
).map((id) => combinedResults.find((doc) => (doc.metadata as { id: string }).id === id));
|
||||
|
||||
return uniqueResults.filter((doc): doc is Document => doc !== undefined);
|
||||
}
|
||||
|
|
@ -316,7 +316,7 @@ export class HybridRetriever extends BaseRetriever {
|
|||
created_at: hit.document.created_at,
|
||||
nchars: hit.document.nchars,
|
||||
// Expose chunkId explicitly for cross-engine deduplication
|
||||
chunkId: hit.document.metadata?.chunkId,
|
||||
chunkId: (hit.document.metadata as { chunkId?: string } | undefined)?.chunkId,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ export async function ensurePromptFrontmatter(file: TFile, prompt: UserSystemPro
|
|||
if (!alreadyPending) {
|
||||
addPendingFileWrite(file.path);
|
||||
}
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
|
||||
if (frontmatter[COPILOT_SYSTEM_PROMPT_CREATED] == null) {
|
||||
frontmatter[COPILOT_SYSTEM_PROMPT_CREATED] = createdMs;
|
||||
}
|
||||
|
|
@ -247,7 +247,7 @@ export async function updatePromptDefaultFlag(
|
|||
if (!alreadyPending) {
|
||||
addPendingFileWrite(file.path);
|
||||
}
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
|
||||
if (isDefault) {
|
||||
frontmatter[COPILOT_SYSTEM_PROMPT_DEFAULT] = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -371,13 +371,14 @@ export class Docs4LLMParser implements FileParser {
|
|||
} else if (Array.isArray(docs4llmResponse.response)) {
|
||||
// Handle array of documents from docs4llm
|
||||
const markdownParts: string[] = [];
|
||||
for (const doc of docs4llmResponse.response) {
|
||||
if (doc.content) {
|
||||
for (const rawDoc of docs4llmResponse.response) {
|
||||
const doc = rawDoc as { content?: { md?: string; text?: string } } | null | undefined;
|
||||
if (doc?.content) {
|
||||
// Prioritize markdown content, then fallback to text content
|
||||
if (doc.content.md) {
|
||||
markdownParts.push(doc.content.md as string);
|
||||
markdownParts.push(doc.content.md);
|
||||
} else if (doc.content.text) {
|
||||
markdownParts.push(doc.content.text as string);
|
||||
markdownParts.push(doc.content.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
src/utils.ts
45
src/utils.ts
|
|
@ -411,7 +411,7 @@ export interface ChatHistoryEntry {
|
|||
// TODO: Deprecated, use chatHistoryUtils.processRawChatHistory instead
|
||||
export function extractChatHistory(memoryVariables: MemoryVariables): ChatHistoryEntry[] {
|
||||
const chatHistory: ChatHistoryEntry[] = [];
|
||||
const { history } = memoryVariables;
|
||||
const history = memoryVariables.history as Array<{ content?: string }>;
|
||||
|
||||
for (let i = 0; i < history.length; i += 2) {
|
||||
const userMessage = history[i]?.content || "";
|
||||
|
|
@ -694,12 +694,21 @@ export async function safeFetch(
|
|||
|
||||
// Check if response is error status (only throw if throwOnHttpError is true)
|
||||
if (throwOnHttpError && response.status >= 400) {
|
||||
let errorJson;
|
||||
type ErrorJson = {
|
||||
detail?: { reason?: string; message?: string } | string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
};
|
||||
let errorJson: ErrorJson | null = null;
|
||||
try {
|
||||
errorJson = typeof response.json === "string" ? JSON.parse(response.json) : response.json;
|
||||
errorJson = (
|
||||
typeof response.json === "string" ? JSON.parse(response.json) : response.json
|
||||
) as ErrorJson;
|
||||
} catch {
|
||||
try {
|
||||
errorJson = typeof response.text === "string" ? JSON.parse(response.text) : response.text;
|
||||
errorJson = (
|
||||
typeof response.text === "string" ? JSON.parse(response.text) : response.text
|
||||
) as ErrorJson;
|
||||
} catch {
|
||||
errorJson = null;
|
||||
}
|
||||
|
|
@ -710,15 +719,13 @@ export async function safeFetch(
|
|||
error.json = errorJson;
|
||||
|
||||
// Handle nested error structure
|
||||
if (
|
||||
errorJson?.detail?.reason === "Invalid license key" ||
|
||||
errorJson?.reason === "Invalid license key"
|
||||
) {
|
||||
const detail = errorJson && typeof errorJson.detail === "object" ? errorJson.detail : undefined;
|
||||
if (detail?.reason === "Invalid license key" || errorJson?.reason === "Invalid license key") {
|
||||
error.message = "Invalid license key";
|
||||
} else if (errorJson?.detail?.message || errorJson?.message) {
|
||||
const message = errorJson?.detail?.message || errorJson?.message;
|
||||
const reason = errorJson?.detail?.reason || errorJson?.reason;
|
||||
error.message = reason ? `${message}: ${reason}` : message;
|
||||
} else if (detail?.message || errorJson?.message) {
|
||||
const message = detail?.message || errorJson?.message;
|
||||
const reason = detail?.reason || errorJson?.reason;
|
||||
error.message = reason ? `${message}: ${reason}` : (message ?? "");
|
||||
} else if (errorJson?.detail) {
|
||||
error.message = JSON.stringify(errorJson.detail);
|
||||
} else if (errorJson) {
|
||||
|
|
@ -990,7 +997,7 @@ export async function checkLatestVersion(): Promise<{
|
|||
url: "https://api.github.com/repos/logancyang/obsidian-copilot/releases/latest",
|
||||
method: "GET",
|
||||
});
|
||||
const version = response.json.tag_name.replace("v", "");
|
||||
const version = (response.json as { tag_name: string }).tag_name.replace("v", "");
|
||||
return { version, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to check for updates";
|
||||
|
|
@ -1180,9 +1187,9 @@ export function extractTextFromChunk(content: unknown): string {
|
|||
return content;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
return (content as Array<{ type?: string; text?: string }>)
|
||||
.filter((item) => item.type === "text")
|
||||
.map((item) => item.text as string)
|
||||
.map((item) => item.text ?? "")
|
||||
.join("");
|
||||
}
|
||||
// For any other type, return empty string
|
||||
|
|
@ -1236,12 +1243,12 @@ export async function withSuppressedTokenWarnings<T>(fn: () => Promise<T>): Prom
|
|||
|
||||
try {
|
||||
// Replace with filtered version
|
||||
console.warn = function (...args) {
|
||||
console.warn = function (...args: unknown[]) {
|
||||
// Ignore token counting warnings
|
||||
const first = args[0];
|
||||
if (
|
||||
args[0]?.includes &&
|
||||
(args[0].includes("Failed to calculate number of tokens") ||
|
||||
args[0].includes("Unknown model"))
|
||||
typeof first === "string" &&
|
||||
(first.includes("Failed to calculate number of tokens") || first.includes("Unknown model"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue