mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 08:21:55 +00:00
add mobile version for pro user
This commit is contained in:
parent
1483b3b8b9
commit
669656e138
@ -1,4 +1,7 @@
|
||||
releases:
|
||||
- version: "0.8.5"
|
||||
features:
|
||||
- "add mobile version for pro user, fix update error"
|
||||
- version: "0.8.4"
|
||||
features:
|
||||
- "test mobile version"
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
{
|
||||
"id": "infio-copilot",
|
||||
"name": "Infio Copilot",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.5",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes",
|
||||
"author": "Felix.D",
|
||||
"authorUrl": "https://github.com/infiolab",
|
||||
"isDesktopOnly": false
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-infio-copilot",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.5",
|
||||
"description": "A Cursor-inspired AI assistant that offers smart autocomplete and interactive chat with your selected notes",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@ -1,29 +1,9 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
|
||||
import JSZip from "jszip";
|
||||
import { machineId } from 'node-machine-id';
|
||||
import { Notice, Platform, Plugin, requestUrl } from "obsidian";
|
||||
import { Notice, Plugin, requestUrl } from "obsidian";
|
||||
|
||||
import { INFIO_BASE_URL } from "../constants";
|
||||
|
||||
function getOperatingSystem(): string {
|
||||
if (Platform.isWin) {
|
||||
return 'windows';
|
||||
}
|
||||
if (Platform.isMacOS) {
|
||||
return 'macos';
|
||||
}
|
||||
if (Platform.isLinux) {
|
||||
return 'linux';
|
||||
}
|
||||
if (Platform.isAndroidApp) {
|
||||
return 'android';
|
||||
}
|
||||
if (Platform.isIosApp) {
|
||||
return 'ios';
|
||||
}
|
||||
// 如果所有已知的平台都不是,则返回未知
|
||||
return 'unknown';
|
||||
}
|
||||
import { getDeviceId, getOperatingSystem } from "../utils/device-id";
|
||||
|
||||
// API响应类型定义
|
||||
export type UserPlanResponse = {
|
||||
@ -76,7 +56,7 @@ export const checkGeneral = async (
|
||||
if (!apiKey) {
|
||||
throw new Error('API密钥不能为空');
|
||||
}
|
||||
const deviceId = await machineId();
|
||||
const deviceId = await getDeviceId();
|
||||
const deviceName = getOperatingSystem();
|
||||
if (!deviceId || !deviceName) {
|
||||
throw new Error('设备ID和设备名称不能为空');
|
||||
@ -336,12 +316,12 @@ export const upgradeToProVersion = async (
|
||||
console.log(`重载插件: ${plugin.manifest.id}`);
|
||||
try {
|
||||
// 禁用插件
|
||||
// @ts-ignore
|
||||
// @ts-expect-error obsidian typings do not expose this internal API
|
||||
await plugin.app.plugins.disablePlugin(plugin.manifest.id);
|
||||
console.log(`插件已禁用: ${plugin.manifest.id}`);
|
||||
|
||||
// 启用插件
|
||||
// @ts-ignore
|
||||
// @ts-expect-error obsidian typings do not expose this internal API
|
||||
await plugin.app.plugins.enablePlugin(plugin.manifest.id);
|
||||
console.log(`插件已重新启用: ${plugin.manifest.id}`);
|
||||
|
||||
|
||||
568
src/main.desktop.ts
Normal file
568
src/main.desktop.ts
Normal file
@ -0,0 +1,568 @@
|
||||
// @ts-nocheck
|
||||
import { EditorView } from '@codemirror/view'
|
||||
// import { PGlite } from '@electric-sql/pglite'
|
||||
import { Editor, MarkdownView, Modal, Notice, Plugin, TFile } from 'obsidian'
|
||||
|
||||
import { ApplyView } from './ApplyView'
|
||||
import { ChatView } from './ChatView'
|
||||
import { ChatProps } from './components/chat-view/ChatView'
|
||||
import { APPLY_VIEW_TYPE, CHAT_VIEW_TYPE, JSON_VIEW_TYPE, PREVIEW_VIEW_TYPE } from './constants'
|
||||
import { getDiffStrategy } from "./core/diff/DiffStrategy"
|
||||
import { InlineEdit } from './core/edit/inline-edit-processor'
|
||||
import { McpHub } from './core/mcp/McpHub'
|
||||
import { RAGEngine } from './core/rag/rag-engine'
|
||||
import { TransEngine } from './core/transformations/trans-engine'
|
||||
import { DBManager } from './database/database-manager'
|
||||
import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase'
|
||||
import { EmbeddingManager } from './embedworker/EmbeddingManager'
|
||||
import EventListener from "./event-listener"
|
||||
import JsonView from './JsonFileView'
|
||||
import { t } from './lang/helpers'
|
||||
import { PreviewView } from './PreviewView'
|
||||
import CompletionKeyWatcher from "./render-plugin/completion-key-watcher"
|
||||
import DocumentChangesListener, {
|
||||
DocumentChanges,
|
||||
getPrefix, getSuffix,
|
||||
hasMultipleCursors,
|
||||
hasSelection
|
||||
} from "./render-plugin/document-changes-listener"
|
||||
import RenderSuggestionPlugin from "./render-plugin/render-surgestion-plugin"
|
||||
import { InlineSuggestionState } from "./render-plugin/states"
|
||||
import { InfioSettingTab } from './settings/SettingTab'
|
||||
import StatusBar from "./status-bar"
|
||||
import {
|
||||
InfioSettings,
|
||||
parseInfioSettings,
|
||||
} from './types/settings'
|
||||
import { createDataviewManager, DataviewManager } from './utils/dataview'
|
||||
import { getMentionableBlockData } from './utils/obsidian'
|
||||
import './utils/path'
|
||||
import { onEnt } from './utils/web-search'
|
||||
|
||||
type DesktopAugmented = Plugin & {
|
||||
metadataCacheUnloadFn: (() => void) | null
|
||||
activeLeafChangeUnloadFn: (() => void) | null
|
||||
dbManagerInitPromise: Promise<DBManager> | null
|
||||
ragEngineInitPromise: Promise<RAGEngine> | null
|
||||
transEngineInitPromise: Promise<TransEngine> | null
|
||||
mcpHubInitPromise: Promise<McpHub> | null
|
||||
settings: InfioSettings
|
||||
settingTab: InfioSettingTab
|
||||
settingsListeners: ((newSettings: InfioSettings) => void)[]
|
||||
initChatProps?: ChatProps
|
||||
dbManager: DBManager | null
|
||||
mcpHub: McpHub | null
|
||||
ragEngine: RAGEngine | null
|
||||
transEngine: TransEngine | null
|
||||
embeddingManager: EmbeddingManager | null
|
||||
inlineEdit: InlineEdit | null
|
||||
diffStrategy?: DiffStrategy
|
||||
dataviewManager: DataviewManager | null
|
||||
|
||||
// methods (attached below)
|
||||
loadSettings: () => Promise<void>
|
||||
setSettings: (newSettings: InfioSettings) => Promise<void>
|
||||
addSettingsListener: (listener: (newSettings: InfioSettings) => void) => () => void
|
||||
openChatView: (openNewChat?: boolean) => Promise<void>
|
||||
activateChatView: (chatProps?: ChatProps, openNewChat?: boolean) => Promise<void>
|
||||
addSelectionToChat: (editor: Editor, view: MarkdownView) => Promise<void>
|
||||
getDbManager: () => Promise<DBManager>
|
||||
getMcpHub: () => Promise<McpHub | null>
|
||||
getRAGEngine: () => Promise<RAGEngine>
|
||||
getTransEngine: () => Promise<TransEngine>
|
||||
getEmbeddingManager: () => EmbeddingManager | null
|
||||
migrateToJsonStorage: () => Promise<void>
|
||||
reloadChatView: () => Promise<void>
|
||||
}
|
||||
|
||||
export async function loadDesktop(base: Plugin) {
|
||||
const plugin = base as DesktopAugmented
|
||||
// initialize fields
|
||||
plugin.metadataCacheUnloadFn = null
|
||||
plugin.activeLeafChangeUnloadFn = null
|
||||
plugin.dbManagerInitPromise = null
|
||||
plugin.ragEngineInitPromise = null
|
||||
plugin.transEngineInitPromise = null
|
||||
plugin.mcpHubInitPromise = null
|
||||
plugin.initChatProps = undefined
|
||||
plugin.dbManager = null
|
||||
plugin.mcpHub = null
|
||||
plugin.ragEngine = null
|
||||
plugin.transEngine = null
|
||||
plugin.embeddingManager = null
|
||||
plugin.inlineEdit = null
|
||||
plugin.diffStrategy = undefined
|
||||
plugin.dataviewManager = null
|
||||
plugin.settingsListeners = []
|
||||
|
||||
// attach methods migrated from original class
|
||||
plugin.loadSettings = async function () {
|
||||
this.settings = parseInfioSettings(await this.loadData())
|
||||
await this.saveData(this.settings)
|
||||
}
|
||||
plugin.setSettings = async function (newSettings: InfioSettings) {
|
||||
this.settings = newSettings
|
||||
await this.saveData(newSettings)
|
||||
this.ragEngine?.setSettings(newSettings)
|
||||
this.transEngine?.setSettings(newSettings)
|
||||
this.settingsListeners.forEach((listener) => listener(newSettings))
|
||||
}
|
||||
plugin.addSettingsListener = function (listener: (ns: InfioSettings) => void) {
|
||||
this.settingsListeners.push(listener)
|
||||
return () => {
|
||||
this.settingsListeners = this.settingsListeners.filter((l) => l !== listener)
|
||||
}
|
||||
}
|
||||
plugin.openChatView = async function (openNewChat = false) {
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView)
|
||||
const editor = view?.editor
|
||||
if (!view || !editor) {
|
||||
await this.activateChatView(undefined, openNewChat)
|
||||
return
|
||||
}
|
||||
const selectedBlockData = await getMentionableBlockData(editor, view)
|
||||
await this.activateChatView({ selectedBlock: selectedBlockData ?? undefined }, openNewChat)
|
||||
}
|
||||
plugin.activateChatView = async function (chatProps?: ChatProps, openNewChat = false) {
|
||||
this.initChatProps = chatProps
|
||||
const leaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0]
|
||||
await (leaf ?? this.app.workspace.getRightLeaf(false))?.setViewState({ type: CHAT_VIEW_TYPE, active: true })
|
||||
if (openNewChat && leaf && leaf.view instanceof ChatView) {
|
||||
leaf.view.openNewChat(chatProps?.selectedBlock)
|
||||
}
|
||||
this.app.workspace.revealLeaf(this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0])
|
||||
}
|
||||
plugin.addSelectionToChat = async function (editor: Editor, view: MarkdownView) {
|
||||
const data = await getMentionableBlockData(editor, view)
|
||||
if (!data) return
|
||||
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
||||
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) {
|
||||
await this.activateChatView({ selectedBlock: data })
|
||||
return
|
||||
}
|
||||
await this.app.workspace.revealLeaf(leaves[0])
|
||||
const chatView = leaves[0].view
|
||||
chatView.addSelectionToChat(data)
|
||||
chatView.focusMessage()
|
||||
}
|
||||
plugin.getDbManager = async function (): Promise<DBManager> {
|
||||
if (this.dbManager) return this.dbManager
|
||||
if (!this.dbManagerInitPromise) {
|
||||
this.dbManagerInitPromise = (async () => {
|
||||
this.dbManager = await DBManager.create(this.app, this.settings.ragOptions.filesystem)
|
||||
return this.dbManager
|
||||
})()
|
||||
}
|
||||
return this.dbManagerInitPromise
|
||||
}
|
||||
plugin.getMcpHub = async function (): Promise<McpHub | null> {
|
||||
if (!this.settings.mcpEnabled) return null
|
||||
if (this.mcpHub) return this.mcpHub
|
||||
if (!this.mcpHubInitPromise) {
|
||||
this.mcpHubInitPromise = (async () => {
|
||||
this.mcpHub = new McpHub(this.app, this as unknown as Plugin)
|
||||
await this.mcpHub.onload()
|
||||
return this.mcpHub
|
||||
})()
|
||||
}
|
||||
return this.mcpHubInitPromise
|
||||
}
|
||||
plugin.getRAGEngine = async function (): Promise<RAGEngine> {
|
||||
if (this.ragEngine) return this.ragEngine
|
||||
if (!this.ragEngineInitPromise) {
|
||||
this.ragEngineInitPromise = (async () => {
|
||||
const dbManager = await this.getDbManager()
|
||||
this.ragEngine = new RAGEngine(this.app, this.settings, dbManager, this.embeddingManager)
|
||||
return this.ragEngine
|
||||
})()
|
||||
}
|
||||
return this.ragEngineInitPromise
|
||||
}
|
||||
plugin.getTransEngine = async function (): Promise<TransEngine> {
|
||||
if (this.transEngine) return this.transEngine
|
||||
if (!this.transEngineInitPromise) {
|
||||
this.transEngineInitPromise = (async () => {
|
||||
const dbManager = await this.getDbManager()
|
||||
this.transEngine = new TransEngine(this.app, this.settings, dbManager, this.embeddingManager)
|
||||
return this.transEngine
|
||||
})()
|
||||
}
|
||||
return this.transEngineInitPromise
|
||||
}
|
||||
plugin.getEmbeddingManager = function (): EmbeddingManager | null {
|
||||
return this.embeddingManager
|
||||
}
|
||||
plugin.migrateToJsonStorage = async function () {
|
||||
try {
|
||||
const dbManager = await this.getDbManager()
|
||||
await migrateToJsonDatabase(this.app, dbManager, async () => {
|
||||
await this.reloadChatView()
|
||||
console.log('Migration to JSON storage completed successfully')
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to migrate to JSON storage:', error)
|
||||
new Notice(t('notifications.migrationFailed'))
|
||||
}
|
||||
}
|
||||
plugin.reloadChatView = async function () {
|
||||
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
||||
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) return
|
||||
new Notice(t('notifications.reloadingInfio'), 1000)
|
||||
leaves[0].detach()
|
||||
await this.activateChatView()
|
||||
}
|
||||
|
||||
// ==== Original onload body starts here (adapted) ====
|
||||
await plugin.loadSettings()
|
||||
|
||||
setTimeout(() => {
|
||||
void plugin.migrateToJsonStorage().then(() => { })
|
||||
void onEnt('loaded')
|
||||
}, 100)
|
||||
|
||||
plugin.settingTab = new InfioSettingTab(plugin.app, plugin as unknown as any)
|
||||
plugin.addSettingTab(plugin.settingTab)
|
||||
|
||||
plugin.dataviewManager = createDataviewManager(plugin.app)
|
||||
|
||||
plugin.embeddingManager = new EmbeddingManager()
|
||||
console.log('EmbeddingManager initialized')
|
||||
|
||||
plugin.addRibbonIcon('wand-sparkles', t('main.openInfioCopilot'), () => plugin.openChatView())
|
||||
|
||||
plugin.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, plugin as unknown as any))
|
||||
plugin.registerView(APPLY_VIEW_TYPE, (leaf) => new ApplyView(leaf))
|
||||
plugin.registerView(PREVIEW_VIEW_TYPE, (leaf) => new PreviewView(leaf))
|
||||
plugin.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf, plugin as unknown as any))
|
||||
|
||||
plugin.inlineEdit = new InlineEdit(plugin as unknown as any, plugin.settings);
|
||||
plugin.registerMarkdownCodeBlockProcessor("infioedit", (source, el, ctx) => {
|
||||
plugin.inlineEdit?.Processor(source, el, ctx);
|
||||
});
|
||||
|
||||
const statusBar = StatusBar.fromApp(plugin as unknown as any);
|
||||
const eventListener = EventListener.fromSettings(
|
||||
plugin.settings,
|
||||
statusBar,
|
||||
plugin.app
|
||||
);
|
||||
|
||||
plugin.diffStrategy = getDiffStrategy(
|
||||
plugin.settings.chatModelId || "",
|
||||
plugin.app,
|
||||
plugin.settings.fuzzyMatchThreshold,
|
||||
plugin.settings.experimentalDiffStrategy,
|
||||
plugin.settings.multiSearchReplaceDiffStrategy,
|
||||
)
|
||||
|
||||
plugin.addSettingsListener((newSettings) => {
|
||||
plugin.inlineEdit = new InlineEdit(plugin as unknown as any, newSettings);
|
||||
eventListener.handleSettingChanged(newSettings)
|
||||
plugin.diffStrategy = getDiffStrategy(
|
||||
plugin.settings.chatModelId || "",
|
||||
plugin.app,
|
||||
plugin.settings.fuzzyMatchThreshold,
|
||||
plugin.settings.experimentalDiffStrategy,
|
||||
plugin.settings.multiSearchReplaceDiffStrategy,
|
||||
)
|
||||
if (plugin.settings.mcpEnabled && !plugin.mcpHub) {
|
||||
void plugin.getMcpHub()
|
||||
} else if (!plugin.settings.mcpEnabled && plugin.mcpHub) {
|
||||
plugin.mcpHub.dispose()
|
||||
plugin.mcpHub = null
|
||||
plugin.mcpHubInitPromise = null
|
||||
}
|
||||
});
|
||||
|
||||
plugin.registerEditorExtension([
|
||||
InlineSuggestionState,
|
||||
CompletionKeyWatcher(
|
||||
eventListener.handleAcceptKeyPressed.bind(eventListener) as () => boolean,
|
||||
eventListener.handlePartialAcceptKeyPressed.bind(eventListener) as () => boolean,
|
||||
eventListener.handleCancelKeyPressed.bind(eventListener) as () => boolean,
|
||||
),
|
||||
DocumentChangesListener(
|
||||
eventListener.handleDocumentChange.bind(eventListener) as (documentChange: DocumentChanges) => Promise<void>
|
||||
),
|
||||
RenderSuggestionPlugin(),
|
||||
]);
|
||||
|
||||
plugin.app.workspace.onLayoutReady(() => {
|
||||
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (view) {
|
||||
// @ts-expect-error, not typed
|
||||
const editorView = view.editor.cm as EditorView;
|
||||
eventListener.onViewUpdate(editorView);
|
||||
}
|
||||
});
|
||||
|
||||
plugin.registerEvent(
|
||||
plugin.app.workspace.on("active-leaf-change", (leaf) => {
|
||||
if (leaf?.view instanceof MarkdownView) {
|
||||
// @ts-expect-error, not typed
|
||||
const editorView = leaf.view.editor.cm as EditorView;
|
||||
eventListener.onViewUpdate(editorView);
|
||||
if (leaf.view.file) {
|
||||
eventListener.handleFileChange(leaf.view.file);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
plugin.registerEvent(
|
||||
plugin.app.metadataCache.on("changed", (file: TFile) => {
|
||||
if (file) {
|
||||
eventListener.handleFileChange(file);
|
||||
// is not worth it to update the file index on every file change
|
||||
// plugin.ragEngine?.updateFileIndex(file);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
plugin.registerEvent(
|
||||
plugin.app.metadataCache.on("deleted", (file: TFile) => {
|
||||
if (file) {
|
||||
plugin.ragEngine?.deleteFileIndex(file);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
plugin.addCommand({
|
||||
id: 'open-new-chat',
|
||||
name: t('main.openNewChat'),
|
||||
callback: () => plugin.openChatView(true),
|
||||
})
|
||||
|
||||
plugin.addCommand({
|
||||
id: 'add-selection-to-chat',
|
||||
name: t('main.addSelectionToChat'),
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
plugin.addSelectionToChat(editor, view)
|
||||
},
|
||||
})
|
||||
|
||||
plugin.addCommand({
|
||||
id: 'rebuild-vault-index',
|
||||
name: t('main.rebuildVaultIndex'),
|
||||
callback: async () => {
|
||||
const notice = new Notice(t('notifications.rebuildingIndex'), 0)
|
||||
try {
|
||||
const ragEngine = await plugin.getRAGEngine()
|
||||
await ragEngine.updateVaultIndex(
|
||||
{ reindexAll: true },
|
||||
(queryProgress) => {
|
||||
if (queryProgress.type === 'indexing') {
|
||||
const { completedChunks, totalChunks } =
|
||||
queryProgress.indexProgress
|
||||
notice.setMessage(
|
||||
t('notifications.indexingChunks', { completedChunks, totalChunks }),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
notice.setMessage(t('notifications.rebuildComplete'))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notice.setMessage(t('notifications.rebuildFailed'))
|
||||
} finally {
|
||||
setTimeout(() => { notice.hide() }, 1000)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
plugin.addCommand({
|
||||
id: 'update-vault-index',
|
||||
name: t('main.updateVaultIndex'),
|
||||
callback: async () => {
|
||||
const notice = new Notice(t('notifications.updatingIndex'), 0)
|
||||
try {
|
||||
const ragEngine = await plugin.getRAGEngine()
|
||||
await ragEngine.updateVaultIndex(
|
||||
{ reindexAll: false },
|
||||
(queryProgress) => {
|
||||
if (queryProgress.type === 'indexing') {
|
||||
const { completedChunks, totalChunks } =
|
||||
queryProgress.indexProgress
|
||||
notice.setMessage(
|
||||
t('notifications.indexingChunks', { completedChunks, totalChunks }),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
notice.setMessage(t('notifications.updateComplete'))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notice.setMessage(t('notifications.updateFailed'))
|
||||
} finally {
|
||||
setTimeout(() => { notice.hide() }, 1000)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
plugin.addCommand({
|
||||
id: 'autocomplete-accept',
|
||||
name: t('main.autocompleteAccept'),
|
||||
editorCheckCallback: (
|
||||
checking: boolean,
|
||||
editor: Editor,
|
||||
view: MarkdownView
|
||||
) => {
|
||||
if (checking) {
|
||||
return (
|
||||
eventListener.isSuggesting()
|
||||
);
|
||||
}
|
||||
eventListener.handleAcceptCommand();
|
||||
return true;
|
||||
},
|
||||
})
|
||||
|
||||
plugin.addCommand({
|
||||
id: 'autocomplete-predict',
|
||||
name: t('main.autocompletePredict'),
|
||||
editorCheckCallback: (
|
||||
checking: boolean,
|
||||
editor: Editor,
|
||||
view: MarkdownView
|
||||
) => {
|
||||
// @ts-expect-error, not typed
|
||||
const editorView = editor.cm as EditorView;
|
||||
const state = editorView.state;
|
||||
if (checking) {
|
||||
return eventListener.isIdle() && !hasMultipleCursors(state) && !hasSelection(state);
|
||||
}
|
||||
const prefix = getPrefix(state)
|
||||
const suffix = getSuffix(state)
|
||||
eventListener.handlePredictCommand(prefix, suffix);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
id: "autocomplete-toggle",
|
||||
name: t('main.autocompleteToggle'),
|
||||
callback: () => {
|
||||
const newValue = !plugin.settings.autocompleteEnabled;
|
||||
plugin.setSettings({
|
||||
...plugin.settings,
|
||||
autocompleteEnabled: newValue,
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
id: "autocomplete-enable",
|
||||
name: t('main.autocompleteEnable'),
|
||||
checkCallback: (checking) => {
|
||||
if (checking) {
|
||||
return !plugin.settings.autocompleteEnabled;
|
||||
}
|
||||
plugin.setSettings({
|
||||
...plugin.settings,
|
||||
autocompleteEnabled: true,
|
||||
})
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
id: "autocomplete-disable",
|
||||
name: t('main.autocompleteDisable'),
|
||||
checkCallback: (checking) => {
|
||||
if (checking) {
|
||||
return plugin.settings.autocompleteEnabled;
|
||||
}
|
||||
plugin.setSettings({
|
||||
...plugin.settings,
|
||||
autocompleteEnabled: false,
|
||||
})
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
id: "ai-inline-edit",
|
||||
name: t('main.inlineEditCommand'),
|
||||
editorCallback: (editor: Editor) => {
|
||||
const selection = editor.getSelection();
|
||||
if (!selection) {
|
||||
new Notice(t('notifications.selectTextFirst'));
|
||||
return;
|
||||
}
|
||||
const from = editor.getCursor("from");
|
||||
const insertPos = { line: from.line, ch: 0 };
|
||||
const customBlock = "```infioedit\n```\n";
|
||||
editor.replaceRange(customBlock, insertPos);
|
||||
},
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
id: 'test-dataview-simple',
|
||||
name: '测试 Dataview(简单查询)',
|
||||
callback: async () => {
|
||||
console.log('开始测试 Dataview...');
|
||||
if (!plugin.dataviewManager) { new Notice('DataviewManager 未初始化'); return; }
|
||||
if (!plugin.dataviewManager.isDataviewAvailable()) {
|
||||
new Notice('Dataview 插件未安装或未启用');
|
||||
console.log('Dataview API 不可用');
|
||||
return;
|
||||
}
|
||||
console.log('Dataview API 可用,执行简单查询...');
|
||||
try {
|
||||
const result = await plugin.dataviewManager.executeQuery('LIST FROM ""');
|
||||
if (result.success) {
|
||||
new Notice('Dataview 查询成功!结果已在控制台输出');
|
||||
} else {
|
||||
new Notice(`查询失败: ${result.error}`);
|
||||
console.error('查询错误:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('执行测试查询失败:', error);
|
||||
new Notice('执行测试查询时发生错误');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
id: 'test-local-embed',
|
||||
name: '测试本地嵌入模型',
|
||||
callback: async () => {
|
||||
try {
|
||||
if (!plugin.embeddingManager) { new Notice('EmbeddingManager 未初始化', 5000); return; }
|
||||
await plugin.embeddingManager.loadModel("Xenova/all-MiniLM-L6-v2", true);
|
||||
const testText = "hello world";
|
||||
const result = await plugin.embeddingManager.embed(testText);
|
||||
const resultMessage = `\n\t嵌入测试完成!\n\t文本: "${testText}"\n\tToken 数量: ${result.tokens}\n\t向量维度: ${result.vec.length}\n\t向量前4个值: [${result.vec.slice(0, 4).map(v => v.toFixed(4)).join(', ')}...]\n\t\t\t\t\t\t`.trim();
|
||||
console.log('本地嵌入测试结果:', result);
|
||||
const modal = new Modal(plugin.app);
|
||||
modal.titleEl.setText('本地嵌入测试结果');
|
||||
modal.contentEl.createEl('pre', { text: resultMessage });
|
||||
modal.open();
|
||||
} catch (error) {
|
||||
console.error('嵌入测试失败:', error);
|
||||
new Notice(`嵌入测试失败: ${error.message}`, 5000);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function unloadDesktop(base: Plugin) {
|
||||
const plugin = base as DesktopAugmented
|
||||
plugin.dbManagerInitPromise = null
|
||||
plugin.ragEngineInitPromise = null
|
||||
plugin.transEngineInitPromise = null
|
||||
plugin.mcpHubInitPromise = null
|
||||
plugin.ragEngine?.cleanup()
|
||||
plugin.ragEngine = null
|
||||
plugin.transEngine?.cleanup()
|
||||
plugin.transEngine = null
|
||||
plugin.dbManager?.cleanup()
|
||||
plugin.dbManager = null
|
||||
plugin.mcpHub?.dispose()
|
||||
plugin.mcpHub = null
|
||||
plugin.embeddingManager?.terminate()
|
||||
plugin.embeddingManager = null
|
||||
plugin.dataviewManager = null
|
||||
}
|
||||
|
||||
|
||||
491
src/main.mobile.ts
Normal file
491
src/main.mobile.ts
Normal file
@ -0,0 +1,491 @@
|
||||
// @ts-check
|
||||
|
||||
import JSZip from "jszip";
|
||||
import { App, Notice, Platform, Plugin, PluginSettingTab, Setting, requestUrl } from 'obsidian';
|
||||
|
||||
import { ApiKeyModal } from './components/modals/ApiKeyModal';
|
||||
import { ProUpgradeModal } from './components/modals/ProUpgradeModal';
|
||||
// import { checkGeneral, fetchUserPlan, upgradeToProVersion } from './hooks/use-infio';
|
||||
import { InfioSettings, parseInfioSettings } from './types/settings-mobile';
|
||||
import { getDeviceId, getOperatingSystem } from './utils/device-id';
|
||||
|
||||
INFIO_BASE_URL = 'https://api.infio.app'
|
||||
|
||||
// API响应类型定义
|
||||
export type CheckGeneralResponse = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
dl_zip?: string;
|
||||
};
|
||||
|
||||
export type CheckGeneralParams = {
|
||||
device_id: string;
|
||||
device_name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查设备一般状态
|
||||
* @param apiKey API密钥
|
||||
* @param deviceId 设备ID
|
||||
* @param deviceName 设备名称
|
||||
* @returns Promise<CheckGeneralResponse>
|
||||
*/
|
||||
export const checkGeneral = async (
|
||||
apiKey: string
|
||||
): Promise<CheckGeneralResponse> => {
|
||||
try {
|
||||
if (!apiKey) {
|
||||
throw new Error('API密钥不能为空');
|
||||
}
|
||||
const deviceId = await getDeviceId();
|
||||
const deviceName = getOperatingSystem();
|
||||
if (!deviceId || !deviceName) {
|
||||
throw new Error('设备ID和设备名称不能为空');
|
||||
}
|
||||
|
||||
const response = await requestUrl({
|
||||
url: `${INFIO_BASE_URL}/subscription/check_general`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_id: deviceId,
|
||||
device_name: deviceName,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.json.success) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response.json;
|
||||
} else {
|
||||
console.error('检查 gerenal 会员失败:', response.json.message);
|
||||
return {
|
||||
success: false,
|
||||
message: response.json.message || '检查设备一般状态失败',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查 gerenal 会员失败:', error);
|
||||
|
||||
// 返回错误响应格式
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '检查设备状态时出现未知错误'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// API响应类型定义
|
||||
export type UserPlanResponse = {
|
||||
plan: string;
|
||||
status: string;
|
||||
dl_zip?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type UpgradeResult = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const fetchUserPlan = async (apiKey: string): Promise<UserPlanResponse> => {
|
||||
const response = await requestUrl({
|
||||
url: `${INFIO_BASE_URL}/subscription/status`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response.json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时目录
|
||||
*/
|
||||
const cleanupTempDirectory = async (adapter: Plugin['app']['vault']['adapter'], tempDir: string): Promise<void> => {
|
||||
try {
|
||||
// 检查目录是否存在
|
||||
if (await adapter.exists(tempDir)) {
|
||||
console.log(`清理临时目录: ${tempDir}`);
|
||||
// 删除临时目录及其所有内容
|
||||
await adapter.rmdir(tempDir, true);
|
||||
console.log(`临时目录清理完成: ${tempDir}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("清理临时目录失败:", error);
|
||||
// 不抛出错误,因为这不是关键操作
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 下载并解压ZIP文件到临时目录
|
||||
*/
|
||||
const downloadAndExtractToTemp = async (
|
||||
adapter: Plugin['app']['vault']['adapter'],
|
||||
tempDir: string,
|
||||
downloadUrl: string
|
||||
): Promise<void> => {
|
||||
console.log(`开始下载文件: ${downloadUrl}`);
|
||||
|
||||
// 下载ZIP文件
|
||||
let zipResponse;
|
||||
try {
|
||||
zipResponse = await requestUrl({
|
||||
url: downloadUrl,
|
||||
method: "GET",
|
||||
});
|
||||
console.log("文件下载完成,状态:", zipResponse.status);
|
||||
} catch (error) {
|
||||
console.log("下载失败:", error);
|
||||
throw new Error("网络连接失败,无法下载Pro版本文件");
|
||||
}
|
||||
|
||||
if (!zipResponse.arrayBuffer) {
|
||||
console.log("响应格式无效,缺少arrayBuffer");
|
||||
throw new Error("下载的文件格式无效");
|
||||
}
|
||||
|
||||
console.log("正在解压文件到临时目录...");
|
||||
console.log(`开始解压文件到临时目录: ${tempDir}`);
|
||||
|
||||
// 解压ZIP文件
|
||||
let zipData: JSZip;
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
zipData = await zip.loadAsync(zipResponse.arrayBuffer);
|
||||
console.log("ZIP文件解析成功");
|
||||
} catch (error) {
|
||||
console.log("ZIP文件解析失败:", error);
|
||||
throw new Error("文件解压失败,可能文件已损坏");
|
||||
}
|
||||
|
||||
// 创建临时目录
|
||||
try {
|
||||
if (!(await adapter.exists(tempDir))) {
|
||||
await adapter.mkdir(tempDir);
|
||||
console.log(`临时目录创建成功: ${tempDir}`);
|
||||
} else {
|
||||
console.log(`临时目录已存在: ${tempDir}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("创建临时目录失败:", error);
|
||||
throw new Error("无法创建临时目录");
|
||||
}
|
||||
|
||||
// 解压所有文件到临时目录
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const files = Object.keys(zipData.files);
|
||||
console.log(files);
|
||||
console.log(`ZIP文件中包含 ${files.length} 个条目`);
|
||||
|
||||
let extractedCount = 0;
|
||||
for (const filename of files) {
|
||||
const file = zipData.files[filename];
|
||||
|
||||
// 跳过目录
|
||||
if (file?.dir) {
|
||||
console.log(`跳过目录: ${filename}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`正在解压文件: ${filename}`);
|
||||
|
||||
// 获取文件内容
|
||||
const content = await file?.async("text");
|
||||
|
||||
if (!content) {
|
||||
console.log(`跳过空文件: ${filename}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 提取文件名(去掉路径前缀)
|
||||
const pathParts = filename.split('/');
|
||||
const actualFileName = pathParts[pathParts.length - 1];
|
||||
|
||||
// 直接写入到临时目录根目录,不创建子目录
|
||||
const tempFilePath = `${tempDir}/${actualFileName}`;
|
||||
|
||||
// 写入文件到临时目录
|
||||
await adapter.write(tempFilePath, content);
|
||||
extractedCount++;
|
||||
console.log(`文件解压完成: ${actualFileName} (${extractedCount}/${files.filter(f => !zipData.files[f].dir).length})`);
|
||||
}
|
||||
|
||||
console.log(`所有文件解压完成,共解压 ${extractedCount} 个文件`);
|
||||
} catch (error) {
|
||||
console.log("文件解压过程中出错:", error);
|
||||
throw new Error("文件解压过程中出现错误");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从临时目录复制文件到插件目录
|
||||
*/
|
||||
const copyFilesFromTemp = async (
|
||||
adapter: Plugin['app']['vault']['adapter'],
|
||||
tempDir: string,
|
||||
pluginDir: string
|
||||
): Promise<void> => {
|
||||
console.log("正在更新插件文件...");
|
||||
console.log(`开始从临时目录复制文件到插件目录: ${tempDir} -> ${pluginDir}`);
|
||||
|
||||
// 需要复制的关键文件
|
||||
const filesToCopy = ['main.js', 'styles.css', 'manifest.json'];
|
||||
|
||||
// 检查必需文件是否存在
|
||||
const mainJsPath = `${tempDir}/main.js`;
|
||||
if (!(await adapter.exists(mainJsPath))) {
|
||||
console.log("关键文件缺失: main.js");
|
||||
throw new Error("升级文件不完整,缺少关键组件");
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
let copiedCount = 0;
|
||||
for (const filename of filesToCopy) {
|
||||
const tempFilePath = `${tempDir}/${filename}`;
|
||||
const pluginFilePath = `${pluginDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (await adapter.exists(tempFilePath)) {
|
||||
const content = await adapter.read(tempFilePath);
|
||||
await adapter.write(pluginFilePath, content);
|
||||
copiedCount++;
|
||||
} else if (filename !== 'main.js') {
|
||||
console.log(`可选文件不存在,跳过: ${filename}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`文件更新失败: ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`文件复制完成,共复制 ${copiedCount} 个文件`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载并安装Pro版本
|
||||
*/
|
||||
export const upgradeToProVersion = async (
|
||||
plugin: Plugin,
|
||||
dl_zip: string
|
||||
): Promise<UpgradeResult> => {
|
||||
const tempDir = '.infio_download_cache';
|
||||
const adapter = plugin.app.vault.adapter;
|
||||
|
||||
try {
|
||||
// 获取插件目录
|
||||
const pluginDir = plugin.manifest.dir;
|
||||
if (!pluginDir) {
|
||||
console.log("插件目录未找到");
|
||||
throw new Error("无法找到插件目录");
|
||||
}
|
||||
new Notice("正在加载...");
|
||||
|
||||
await cleanupTempDirectory(adapter, tempDir);
|
||||
|
||||
await downloadAndExtractToTemp(
|
||||
adapter,
|
||||
tempDir,
|
||||
dl_zip
|
||||
);
|
||||
|
||||
await copyFilesFromTemp(adapter, tempDir, pluginDir);
|
||||
|
||||
new Notice("加载完成,成功升级");
|
||||
|
||||
await cleanupTempDirectory(adapter, tempDir);
|
||||
|
||||
setTimeout(async () => {
|
||||
console.log(`重载插件: ${plugin.manifest.id}`);
|
||||
try {
|
||||
// 禁用插件
|
||||
// @ts-expect-error obsidian typings do not expose this internal API
|
||||
await plugin.app.plugins.disablePlugin(plugin.manifest.id);
|
||||
console.log(`插件已禁用: ${plugin.manifest.id}`);
|
||||
|
||||
// 启用插件
|
||||
// @ts-expect-error obsidian typings do not expose this internal API
|
||||
await plugin.app.plugins.enablePlugin(plugin.manifest.id);
|
||||
console.log(`插件已重新启用: ${plugin.manifest.id}`);
|
||||
|
||||
new Notice("插件重载完成");
|
||||
} catch (error) {
|
||||
console.error("插件重载失败:", error);
|
||||
new Notice("插件重载失败,请手动重启插件");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "加载完成"
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.log("错误详情:", error);
|
||||
|
||||
// 发生错误时也要清理临时目录
|
||||
await cleanupTempDirectory(adapter, tempDir);
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "升级过程中出现未知错误";
|
||||
console.log(`最终错误信息: ${errorMessage}`);
|
||||
new Notice(`加载失败: ${errorMessage}`);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class MobileSettingTab extends PluginSettingTab {
|
||||
plugin: Plugin & { settings: InfioSettings; setSettings: (s: InfioSettings) => Promise<void> }
|
||||
|
||||
constructor(app: App, plugin: Plugin & { settings: InfioSettings; setSettings: (s: InfioSet·tings) => Promise<void> }) {
|
||||
super(app, plugin)
|
||||
this.plugin = plugin
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this
|
||||
containerEl.empty()
|
||||
|
||||
// Title
|
||||
const title = containerEl.createEl('h2', { text: 'Infio Mobile' })
|
||||
title.style.marginBottom = '8px'
|
||||
|
||||
// Description
|
||||
containerEl.createEl('div', { text: '仅用于在移动端填写 API Key 以下载正式移动版本。' })
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Infio API Key')
|
||||
.setDesc('用于验证并下载移动端正式版本')
|
||||
.addText((text) => {
|
||||
text
|
||||
.setPlaceholder('sk-...')
|
||||
.setValue(this.plugin.settings?.infioProvider?.apiKey || '')
|
||||
.onChange(async (value) => {
|
||||
await this.plugin.setSettings({
|
||||
...this.plugin.settings,
|
||||
infioProvider: {
|
||||
...(this.plugin.settings?.infioProvider || { name: 'Infio', apiKey: '', baseUrl: '', useCustomUrl: false, models: [] }),
|
||||
apiKey: value,
|
||||
},
|
||||
// 兼容字段
|
||||
infioApiKey: value,
|
||||
})
|
||||
new Notice('已保存 API Key')
|
||||
})
|
||||
})
|
||||
|
||||
// 升级到 Pro 按钮
|
||||
new Setting(containerEl)
|
||||
.setName('升级到Pro')
|
||||
.setDesc('填写 API Key 后点击下载并升级到正式移动版')
|
||||
.addButton((button) => {
|
||||
button.setButtonText('升级到Pro').onClick(async () => {
|
||||
const originalText = button.buttonEl.textContent || '升级到Pro'
|
||||
button.setDisabled(true)
|
||||
button.setButtonText('加载中...')
|
||||
try {
|
||||
const apiKey = this.plugin.settings?.infioProvider?.apiKey || this.plugin.settings?.infioApiKey || ''
|
||||
if (!apiKey) {
|
||||
if (this.app) {
|
||||
new ApiKeyModal(this.app).open()
|
||||
} else {
|
||||
new Notice('请先在Infio Provider设置中配置 Infio API Key')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const userPlan = await fetchUserPlan(apiKey)
|
||||
const plan = (userPlan.plan || '').toLowerCase()
|
||||
const isProUser = plan.startsWith('pro')
|
||||
const isGeneralUser = plan.startsWith('general')
|
||||
let dl_zip = userPlan.dl_zip || ''
|
||||
|
||||
if (!isProUser && !isGeneralUser) {
|
||||
if (this.app) {
|
||||
new ProUpgradeModal(this.app).open()
|
||||
} else {
|
||||
new Notice('您的账户不是会员用户, 请先购买会员')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (isGeneralUser) {
|
||||
const result = await checkGeneral(apiKey)
|
||||
if (!result.success) {
|
||||
if (this.app) {
|
||||
new ProUpgradeModal(this.app).open()
|
||||
} else {
|
||||
new Notice('您的账户不是会员用户, 请先购买会员')
|
||||
}
|
||||
return
|
||||
}
|
||||
dl_zip = result.dl_zip || dl_zip
|
||||
}
|
||||
|
||||
if (Platform.isMobile) {
|
||||
dl_zip = dl_zip.replace('.zip', '.mobile.zip')
|
||||
}
|
||||
|
||||
if (!dl_zip) {
|
||||
new Notice('无法获取下载地址,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await upgradeToProVersion(this.plugin, dl_zip)
|
||||
if (!result.success) {
|
||||
new Notice(`加载失败: ${result.message}`)
|
||||
}
|
||||
} catch (_error) {
|
||||
new Notice('升级过程中发生错误')
|
||||
console.error(_error)
|
||||
} finally {
|
||||
button.setDisabled(false)
|
||||
button.setButtonText(originalText)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Hint for users
|
||||
const hint = containerEl.createEl('div', { text: Platform.isMobile ? '已检测到移动端环境。填写 API Key 后,将在插件中验证并引导下载正式版本。' : '非移动端环境。' })
|
||||
hint.style.marginTop = '8px'
|
||||
; // keep style
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadMobile(base: Plugin) {
|
||||
const plugin = base as Plugin & {
|
||||
settings: InfioSettings
|
||||
loadSettings: () => Promise<void>
|
||||
setSettings: (s: InfioSettings) => Promise<void>
|
||||
}
|
||||
|
||||
plugin.loadSettings = async function () {
|
||||
this.settings = parseInfioSettings(await this.loadData())
|
||||
await this.saveData(this.settings)
|
||||
}
|
||||
|
||||
plugin.setSettings = async function (newSettings: InfioSettings) {
|
||||
this.settings = newSettings
|
||||
await this.saveData(newSettings)
|
||||
}
|
||||
|
||||
await plugin.loadSettings()
|
||||
|
||||
// Only settings tab
|
||||
plugin.addSettingTab(new MobileSettingTab(plugin.app, plugin))
|
||||
}
|
||||
|
||||
export function unloadMobile(_base: Plugin) {
|
||||
// nothing to cleanup in lite mobile
|
||||
}
|
||||
|
||||
|
||||
683
src/main.ts
683
src/main.ts
@ -1,685 +1,26 @@
|
||||
// @ts-nocheck
|
||||
import { EditorView } from '@codemirror/view'
|
||||
// import { PGlite } from '@electric-sql/pglite'
|
||||
import { Editor, MarkdownView, Modal, Notice, Plugin, TFile } from 'obsidian'
|
||||
|
||||
import { ApplyView } from './ApplyView'
|
||||
import { ChatView } from './ChatView'
|
||||
import { ChatProps } from './components/chat-view/ChatView'
|
||||
import { APPLY_VIEW_TYPE, CHAT_VIEW_TYPE, JSON_VIEW_TYPE, PREVIEW_VIEW_TYPE } from './constants'
|
||||
import { getDiffStrategy } from "./core/diff/DiffStrategy"
|
||||
import { InlineEdit } from './core/edit/inline-edit-processor'
|
||||
import { McpHub } from './core/mcp/McpHub'
|
||||
import { RAGEngine } from './core/rag/rag-engine'
|
||||
import { TransEngine } from './core/transformations/trans-engine'
|
||||
import { DBManager } from './database/database-manager'
|
||||
import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase'
|
||||
import { EmbeddingManager } from './embedworker/EmbeddingManager'
|
||||
import EventListener from "./event-listener"
|
||||
import JsonView from './JsonFileView'
|
||||
import { t } from './lang/helpers'
|
||||
import { PreviewView } from './PreviewView'
|
||||
import CompletionKeyWatcher from "./render-plugin/completion-key-watcher"
|
||||
import DocumentChangesListener, {
|
||||
DocumentChanges,
|
||||
getPrefix, getSuffix,
|
||||
hasMultipleCursors,
|
||||
hasSelection
|
||||
} from "./render-plugin/document-changes-listener"
|
||||
import RenderSuggestionPlugin from "./render-plugin/render-surgestion-plugin"
|
||||
import { InlineSuggestionState } from "./render-plugin/states"
|
||||
import { InfioSettingTab } from './settings/SettingTab'
|
||||
import StatusBar from "./status-bar"
|
||||
import {
|
||||
InfioSettings,
|
||||
parseInfioSettings,
|
||||
} from './types/settings'
|
||||
import { createDataviewManager, DataviewManager } from './utils/dataview'
|
||||
import { getMentionableBlockData } from './utils/obsidian'
|
||||
import './utils/path'
|
||||
import { onEnt } from './utils/web-search'
|
||||
import { Platform, Plugin } from 'obsidian'
|
||||
|
||||
export default class InfioPlugin extends Plugin {
|
||||
private metadataCacheUnloadFn: (() => void) | null = null
|
||||
private activeLeafChangeUnloadFn: (() => void) | null = null
|
||||
private dbManagerInitPromise: Promise<DBManager> | null = null
|
||||
private ragEngineInitPromise: Promise<RAGEngine> | null = null
|
||||
private transEngineInitPromise: Promise<TransEngine> | null = null
|
||||
private mcpHubInitPromise: Promise<McpHub> | null = null
|
||||
settings: InfioSettings
|
||||
settingTab: InfioSettingTab
|
||||
settingsListeners: ((newSettings: InfioSettings) => void)[] = []
|
||||
initChatProps?: ChatProps
|
||||
dbManager: DBManager | null = null
|
||||
mcpHub: McpHub | null = null
|
||||
ragEngine: RAGEngine | null = null
|
||||
transEngine: TransEngine | null = null
|
||||
embeddingManager: EmbeddingManager | null = null
|
||||
inlineEdit: InlineEdit | null = null
|
||||
diffStrategy?: DiffStrategy
|
||||
dataviewManager: DataviewManager | null = null
|
||||
|
||||
async onload() {
|
||||
// load settings
|
||||
await this.loadSettings()
|
||||
|
||||
// migrate to json database
|
||||
setTimeout(() => {
|
||||
void this.migrateToJsonStorage().then(() => { })
|
||||
void onEnt('loaded')
|
||||
}, 100)
|
||||
|
||||
// add settings tab
|
||||
this.settingTab = new InfioSettingTab(this.app, this)
|
||||
this.addSettingTab(this.settingTab)
|
||||
|
||||
// initialize dataview manager
|
||||
this.dataviewManager = createDataviewManager(this.app)
|
||||
|
||||
// initialize embedding manager
|
||||
this.embeddingManager = new EmbeddingManager()
|
||||
console.log('EmbeddingManager initialized')
|
||||
|
||||
// add icon to ribbon
|
||||
this.addRibbonIcon('wand-sparkles', t('main.openInfioCopilot'), () =>
|
||||
this.openChatView(),
|
||||
)
|
||||
|
||||
// register views
|
||||
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this))
|
||||
this.registerView(APPLY_VIEW_TYPE, (leaf) => new ApplyView(leaf))
|
||||
this.registerView(PREVIEW_VIEW_TYPE, (leaf) => new PreviewView(leaf))
|
||||
this.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf, this))
|
||||
|
||||
// register markdown processor for Inline Edit
|
||||
this.inlineEdit = new InlineEdit(this, this.settings);
|
||||
this.registerMarkdownCodeBlockProcessor("infioedit", (source, el, ctx) => {
|
||||
this.inlineEdit?.Processor(source, el, ctx);
|
||||
});
|
||||
|
||||
// setup autocomplete event listener
|
||||
const statusBar = StatusBar.fromApp(this);
|
||||
const eventListener = EventListener.fromSettings(
|
||||
this.settings,
|
||||
statusBar,
|
||||
this.app
|
||||
);
|
||||
|
||||
// initialize diff strategy
|
||||
this.diffStrategy = getDiffStrategy(
|
||||
this.settings.chatModelId || "",
|
||||
this.app,
|
||||
this.settings.fuzzyMatchThreshold,
|
||||
this.settings.experimentalDiffStrategy,
|
||||
this.settings.multiSearchReplaceDiffStrategy,
|
||||
)
|
||||
|
||||
// add settings change listener
|
||||
this.addSettingsListener((newSettings) => {
|
||||
// Update inlineEdit when settings change
|
||||
this.inlineEdit = new InlineEdit(this, newSettings);
|
||||
// Update autocomplete event listener when settings change
|
||||
eventListener.handleSettingChanged(newSettings)
|
||||
// Update diff strategy when settings change
|
||||
this.diffStrategy = getDiffStrategy(
|
||||
this.settings.chatModelId || "",
|
||||
this.app,
|
||||
this.settings.fuzzyMatchThreshold,
|
||||
this.settings.experimentalDiffStrategy,
|
||||
this.settings.multiSearchReplaceDiffStrategy,
|
||||
)
|
||||
// Update MCP Hub when settings change
|
||||
if (this.settings.mcpEnabled && !this.mcpHub) {
|
||||
void this.getMcpHub()
|
||||
} else if (!this.settings.mcpEnabled && this.mcpHub) {
|
||||
this.mcpHub.dispose()
|
||||
this.mcpHub = null
|
||||
this.mcpHubInitPromise = null
|
||||
}
|
||||
});
|
||||
|
||||
// setup autocomplete render plugin
|
||||
this.registerEditorExtension([
|
||||
InlineSuggestionState,
|
||||
CompletionKeyWatcher(
|
||||
eventListener.handleAcceptKeyPressed.bind(eventListener) as () => boolean,
|
||||
eventListener.handlePartialAcceptKeyPressed.bind(eventListener) as () => boolean,
|
||||
eventListener.handleCancelKeyPressed.bind(eventListener) as () => boolean,
|
||||
),
|
||||
DocumentChangesListener(
|
||||
eventListener.handleDocumentChange.bind(eventListener) as (documentChange: DocumentChanges) => Promise<void>
|
||||
),
|
||||
RenderSuggestionPlugin(),
|
||||
]);
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
|
||||
if (view) {
|
||||
// @ts-expect-error, not typed
|
||||
const editorView = view.editor.cm as EditorView;
|
||||
eventListener.onViewUpdate(editorView);
|
||||
}
|
||||
});
|
||||
|
||||
/// *** Event Listeners ***
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("active-leaf-change", (leaf) => {
|
||||
if (leaf?.view instanceof MarkdownView) {
|
||||
// @ts-expect-error, not typed
|
||||
const editorView = leaf.view.editor.cm as EditorView;
|
||||
eventListener.onViewUpdate(editorView);
|
||||
if (leaf.view.file) {
|
||||
eventListener.handleFileChange(leaf.view.file);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.metadataCache.on("changed", (file: TFile) => {
|
||||
if (file) {
|
||||
eventListener.handleFileChange(file);
|
||||
// is not worth it to update the file index on every file change
|
||||
// this.ragEngine?.updateFileIndex(file);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.metadataCache.on("deleted", (file: TFile) => {
|
||||
if (file) {
|
||||
this.ragEngine?.deleteFileIndex(file);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// *** Commands ***
|
||||
this.addCommand({
|
||||
id: 'open-new-chat',
|
||||
name: t('main.openNewChat'),
|
||||
callback: () => this.openChatView(true),
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: 'add-selection-to-chat',
|
||||
name: t('main.addSelectionToChat'),
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
this.addSelectionToChat(editor, view)
|
||||
},
|
||||
// hotkeys: [
|
||||
// {
|
||||
// modifiers: ['Mod', 'Shift'],
|
||||
// key: 'l',
|
||||
// },
|
||||
// ],
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: 'rebuild-vault-index',
|
||||
name: t('main.rebuildVaultIndex'),
|
||||
callback: async () => {
|
||||
const notice = new Notice(t('notifications.rebuildingIndex'), 0)
|
||||
try {
|
||||
const ragEngine = await this.getRAGEngine()
|
||||
await ragEngine.updateVaultIndex(
|
||||
{ reindexAll: true },
|
||||
(queryProgress) => {
|
||||
if (queryProgress.type === 'indexing') {
|
||||
const { completedChunks, totalChunks } =
|
||||
queryProgress.indexProgress
|
||||
notice.setMessage(
|
||||
t('notifications.indexingChunks', { completedChunks, totalChunks }),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
notice.setMessage(t('notifications.rebuildComplete'))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notice.setMessage(t('notifications.rebuildFailed'))
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
notice.hide()
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: 'update-vault-index',
|
||||
name: t('main.updateVaultIndex'),
|
||||
callback: async () => {
|
||||
const notice = new Notice(t('notifications.updatingIndex'), 0)
|
||||
try {
|
||||
const ragEngine = await this.getRAGEngine()
|
||||
await ragEngine.updateVaultIndex(
|
||||
{ reindexAll: false },
|
||||
(queryProgress) => {
|
||||
if (queryProgress.type === 'indexing') {
|
||||
const { completedChunks, totalChunks } =
|
||||
queryProgress.indexProgress
|
||||
notice.setMessage(
|
||||
t('notifications.indexingChunks', { completedChunks, totalChunks }),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
notice.setMessage(t('notifications.updateComplete'))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notice.setMessage(t('notifications.updateFailed'))
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
notice.hide()
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: 'autocomplete-accept',
|
||||
name: t('main.autocompleteAccept'),
|
||||
editorCheckCallback: (
|
||||
checking: boolean,
|
||||
editor: Editor,
|
||||
view: MarkdownView
|
||||
) => {
|
||||
if (checking) {
|
||||
return (
|
||||
eventListener.isSuggesting()
|
||||
);
|
||||
}
|
||||
|
||||
eventListener.handleAcceptCommand();
|
||||
|
||||
return true;
|
||||
},
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: 'autocomplete-predict',
|
||||
name: t('main.autocompletePredict'),
|
||||
editorCheckCallback: (
|
||||
checking: boolean,
|
||||
editor: Editor,
|
||||
view: MarkdownView
|
||||
) => {
|
||||
// @ts-expect-error, not typed
|
||||
const editorView = editor.cm as EditorView;
|
||||
const state = editorView.state;
|
||||
if (checking) {
|
||||
return eventListener.isIdle() && !hasMultipleCursors(state) && !hasSelection(state);
|
||||
}
|
||||
|
||||
const prefix = getPrefix(state)
|
||||
const suffix = getSuffix(state)
|
||||
|
||||
eventListener.handlePredictCommand(prefix, suffix);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "autocomplete-toggle",
|
||||
name: t('main.autocompleteToggle'),
|
||||
callback: () => {
|
||||
const newValue = !this.settings.autocompleteEnabled;
|
||||
this.setSettings({
|
||||
...this.settings,
|
||||
autocompleteEnabled: newValue,
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "autocomplete-enable",
|
||||
name: t('main.autocompleteEnable'),
|
||||
checkCallback: (checking) => {
|
||||
if (checking) {
|
||||
return !this.settings.autocompleteEnabled;
|
||||
}
|
||||
|
||||
this.setSettings({
|
||||
...this.settings,
|
||||
autocompleteEnabled: true,
|
||||
})
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "autocomplete-disable",
|
||||
name: t('main.autocompleteDisable'),
|
||||
checkCallback: (checking) => {
|
||||
if (checking) {
|
||||
return this.settings.autocompleteEnabled;
|
||||
}
|
||||
|
||||
this.setSettings({
|
||||
...this.settings,
|
||||
autocompleteEnabled: false,
|
||||
})
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "ai-inline-edit",
|
||||
name: t('main.inlineEditCommand'),
|
||||
// hotkeys: [
|
||||
// {
|
||||
// modifiers: ['Mod', 'Shift'],
|
||||
// key: "k",
|
||||
// },
|
||||
// ],
|
||||
editorCallback: (editor: Editor) => {
|
||||
const selection = editor.getSelection();
|
||||
if (!selection) {
|
||||
new Notice(t('notifications.selectTextFirst'));
|
||||
return;
|
||||
}
|
||||
// Get the selection start position
|
||||
const from = editor.getCursor("from");
|
||||
// Create the position for inserting the block
|
||||
const insertPos = { line: from.line, ch: 0 };
|
||||
// Create the AI block with the selected text
|
||||
const customBlock = "```infioedit\n```\n";
|
||||
// Insert the block above the selection
|
||||
editor.replaceRange(customBlock, insertPos);
|
||||
},
|
||||
});
|
||||
|
||||
// 添加简单测试命令
|
||||
this.addCommand({
|
||||
id: 'test-dataview-simple',
|
||||
name: '测试 Dataview(简单查询)',
|
||||
callback: async () => {
|
||||
console.log('开始测试 Dataview...');
|
||||
|
||||
if (!this.dataviewManager) {
|
||||
new Notice('DataviewManager 未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dataviewManager.isDataviewAvailable()) {
|
||||
new Notice('Dataview 插件未安装或未启用');
|
||||
console.log('Dataview API 不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Dataview API 可用,执行简单查询...');
|
||||
|
||||
try {
|
||||
// 执行一个最简单的查询
|
||||
const result = await this.dataviewManager.executeQuery('LIST FROM ""');
|
||||
|
||||
if (result.success) {
|
||||
new Notice('Dataview 查询成功!结果已在控制台输出');
|
||||
// console.log('查询结果:', result.data);
|
||||
if (Platform.isMobile) {
|
||||
console.log('Infio Copilot: Mobile platform detected, skipping desktop-only features.')
|
||||
const mod = await import('./main.mobile')
|
||||
await mod.loadMobile(this)
|
||||
} else {
|
||||
new Notice(`查询失败: ${result.error}`);
|
||||
console.error('查询错误:', result.error);
|
||||
console.log('Infio Copilot: Desktop platform detected, loading desktop features.')
|
||||
const mod = await import('./main.desktop')
|
||||
await mod.loadDesktop(this)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('执行测试查询失败:', error);
|
||||
new Notice('执行测试查询时发生错误');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 添加本地嵌入测试命令
|
||||
this.addCommand({
|
||||
id: 'test-local-embed',
|
||||
name: '测试本地嵌入模型',
|
||||
callback: async () => {
|
||||
try {
|
||||
if (!this.embeddingManager) {
|
||||
new Notice('EmbeddingManager 未初始化', 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载模型
|
||||
await this.embeddingManager.loadModel("Xenova/all-MiniLM-L6-v2", true);
|
||||
|
||||
// 测试嵌入 "hello world"
|
||||
const testText = "hello world";
|
||||
|
||||
const result = await this.embeddingManager.embed(testText);
|
||||
|
||||
// 显示结果
|
||||
const resultMessage = `
|
||||
嵌入测试完成!
|
||||
文本: "${testText}"
|
||||
Token 数量: ${result.tokens}
|
||||
向量维度: ${result.vec.length}
|
||||
向量前4个值: [${result.vec.slice(0, 4).map(v => v.toFixed(4)).join(', ')}...]
|
||||
`.trim();
|
||||
|
||||
console.log('本地嵌入测试结果:', result);
|
||||
|
||||
// 创建模态框显示结果
|
||||
const modal = new Modal(this.app);
|
||||
modal.titleEl.setText('本地嵌入测试结果');
|
||||
modal.contentEl.createEl('pre', { text: resultMessage });
|
||||
modal.open();
|
||||
|
||||
} catch (error) {
|
||||
console.error('嵌入测试失败:', error);
|
||||
new Notice(`嵌入测试失败: ${error.message}`, 5000);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// Promise cleanup
|
||||
this.dbManagerInitPromise = null
|
||||
this.ragEngineInitPromise = null
|
||||
this.transEngineInitPromise = null
|
||||
this.mcpHubInitPromise = null
|
||||
// RagEngine cleanup
|
||||
this.ragEngine?.cleanup()
|
||||
this.ragEngine = null
|
||||
// TransEngine cleanup
|
||||
this.transEngine?.cleanup()
|
||||
this.transEngine = null
|
||||
// Database cleanup
|
||||
this.dbManager?.cleanup()
|
||||
this.dbManager = null
|
||||
// MCP Hub cleanup
|
||||
this.mcpHub?.dispose()
|
||||
this.mcpHub = null
|
||||
// EmbeddingManager cleanup
|
||||
this.embeddingManager?.terminate()
|
||||
this.embeddingManager = null
|
||||
// Dataview cleanup
|
||||
this.dataviewManager = null
|
||||
if (Platform.isMobile) {
|
||||
void import('./main.mobile').then((m) => m.unloadMobile?.(this)).catch(() => {})
|
||||
} else {
|
||||
void import('./main.desktop').then((m) => m.unloadDesktop?.(this)).catch(() => {})
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = parseInfioSettings(await this.loadData())
|
||||
await this.saveData(this.settings) // Save updated settings
|
||||
}
|
||||
|
||||
async setSettings(newSettings: InfioSettings) {
|
||||
this.settings = newSettings
|
||||
await this.saveData(newSettings)
|
||||
this.ragEngine?.setSettings(newSettings)
|
||||
this.transEngine?.setSettings(newSettings)
|
||||
this.settingsListeners.forEach((listener) => listener(newSettings))
|
||||
}
|
||||
|
||||
addSettingsListener(
|
||||
listener: (newSettings: InfioSettings) => void,
|
||||
) {
|
||||
this.settingsListeners.push(listener)
|
||||
return () => {
|
||||
this.settingsListeners = this.settingsListeners.filter(
|
||||
(l) => l !== listener,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async openChatView(openNewChat = false) {
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView)
|
||||
const editor = view?.editor
|
||||
if (!view || !editor) {
|
||||
this.activateChatView(undefined, openNewChat)
|
||||
return
|
||||
}
|
||||
const selectedBlockData = await getMentionableBlockData(editor, view)
|
||||
this.activateChatView(
|
||||
{
|
||||
selectedBlock: selectedBlockData ?? undefined,
|
||||
},
|
||||
openNewChat,
|
||||
)
|
||||
}
|
||||
|
||||
async activateChatView(chatProps?: ChatProps, openNewChat = false) {
|
||||
// chatProps is consumed in ChatView.tsx
|
||||
this.initChatProps = chatProps
|
||||
|
||||
const leaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0]
|
||||
|
||||
await (leaf ?? this.app.workspace.getRightLeaf(false))?.setViewState({
|
||||
type: CHAT_VIEW_TYPE,
|
||||
active: true,
|
||||
})
|
||||
|
||||
if (openNewChat && leaf && leaf.view instanceof ChatView) {
|
||||
leaf.view.openNewChat(chatProps?.selectedBlock)
|
||||
}
|
||||
|
||||
this.app.workspace.revealLeaf(
|
||||
this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0],
|
||||
)
|
||||
}
|
||||
|
||||
async addSelectionToChat(editor: Editor, view: MarkdownView) {
|
||||
const data = await getMentionableBlockData(editor, view)
|
||||
if (!data) return
|
||||
|
||||
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
||||
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) {
|
||||
await this.activateChatView({
|
||||
selectedBlock: data,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// bring leaf to foreground (uncollapse sidebar if it's collapsed)
|
||||
await this.app.workspace.revealLeaf(leaves[0])
|
||||
|
||||
const chatView = leaves[0].view
|
||||
chatView.addSelectionToChat(data)
|
||||
chatView.focusMessage()
|
||||
}
|
||||
|
||||
async getDbManager(): Promise<DBManager> {
|
||||
if (this.dbManager) {
|
||||
return this.dbManager
|
||||
}
|
||||
|
||||
if (!this.dbManagerInitPromise) {
|
||||
this.dbManagerInitPromise = (async () => {
|
||||
this.dbManager = await DBManager.create(
|
||||
this.app,
|
||||
this.settings.ragOptions.filesystem,
|
||||
)
|
||||
return this.dbManager
|
||||
})()
|
||||
}
|
||||
|
||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
||||
return this.dbManagerInitPromise
|
||||
}
|
||||
|
||||
async getMcpHub(): Promise<McpHub | null> {
|
||||
// MCP is not enabled
|
||||
if (!this.settings.mcpEnabled) {
|
||||
// new Notice('MCP is not enabled')
|
||||
return null
|
||||
}
|
||||
|
||||
// if we already have an instance, return it
|
||||
if (this.mcpHub) {
|
||||
return this.mcpHub
|
||||
}
|
||||
|
||||
if (!this.mcpHubInitPromise) {
|
||||
this.mcpHubInitPromise = (async () => {
|
||||
this.mcpHub = new McpHub(this.app, this)
|
||||
await this.mcpHub.onload()
|
||||
return this.mcpHub
|
||||
})()
|
||||
}
|
||||
|
||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
||||
return this.mcpHubInitPromise
|
||||
}
|
||||
|
||||
async getRAGEngine(): Promise<RAGEngine> {
|
||||
if (this.ragEngine) {
|
||||
return this.ragEngine
|
||||
}
|
||||
|
||||
if (!this.ragEngineInitPromise) {
|
||||
this.ragEngineInitPromise = (async () => {
|
||||
const dbManager = await this.getDbManager()
|
||||
this.ragEngine = new RAGEngine(this.app, this.settings, dbManager, this.embeddingManager)
|
||||
return this.ragEngine
|
||||
})()
|
||||
}
|
||||
|
||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
||||
return this.ragEngineInitPromise
|
||||
}
|
||||
|
||||
async getTransEngine(): Promise<TransEngine> {
|
||||
if (this.transEngine) {
|
||||
return this.transEngine
|
||||
}
|
||||
|
||||
if (!this.transEngineInitPromise) {
|
||||
this.transEngineInitPromise = (async () => {
|
||||
const dbManager = await this.getDbManager()
|
||||
this.transEngine = new TransEngine(this.app, this.settings, dbManager, this.embeddingManager)
|
||||
return this.transEngine
|
||||
})()
|
||||
}
|
||||
|
||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
||||
return this.transEngineInitPromise
|
||||
}
|
||||
|
||||
getEmbeddingManager(): EmbeddingManager | null {
|
||||
return this.embeddingManager
|
||||
}
|
||||
|
||||
private async migrateToJsonStorage() {
|
||||
try {
|
||||
const dbManager = await this.getDbManager()
|
||||
await migrateToJsonDatabase(this.app, dbManager, async () => {
|
||||
await this.reloadChatView()
|
||||
console.log('Migration to JSON storage completed successfully')
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to migrate to JSON storage:', error)
|
||||
new Notice(
|
||||
t('notifications.migrationFailed'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async reloadChatView() {
|
||||
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
||||
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) {
|
||||
return
|
||||
}
|
||||
new Notice(t('notifications.reloadingInfio'), 1000)
|
||||
leaves[0].detach()
|
||||
await this.activateChatView()
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import * as React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import { t } from '../lang/helpers';
|
||||
import InfioPlugin from '../main';
|
||||
import { InfioSettings } from '../types/settings';
|
||||
import { findFilesMatchingPatterns } from '../utils/glob-utils';
|
||||
|
||||
@ -24,13 +23,18 @@ import PreprocessingSettings from './components/PreprocessingSettings';
|
||||
import PrivacySettings from './components/PrivacySettings';
|
||||
import TriggerSettingsSection from './components/TriggerSettingsSection';
|
||||
|
||||
type InfioPluginLike = Plugin & {
|
||||
settings: InfioSettings;
|
||||
setSettings: (s: InfioSettings) => Promise<void>;
|
||||
}
|
||||
|
||||
export class InfioSettingTab extends PluginSettingTab {
|
||||
plugin: InfioPlugin;
|
||||
plugin: InfioPluginLike;
|
||||
private autoCompleteContainer: HTMLElement | null = null;
|
||||
private modelsContainer: HTMLElement | null = null;
|
||||
private pluginInfoContainer: HTMLElement | null = null;
|
||||
|
||||
constructor(app: App, plugin: InfioPlugin) {
|
||||
constructor(app: App, plugin: InfioPluginLike) {
|
||||
super(app, plugin)
|
||||
this.plugin = plugin
|
||||
}
|
||||
|
||||
659
src/types/settings-mobile.ts
Normal file
659
src/types/settings-mobile.ts
Normal file
@ -0,0 +1,659 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DEFAULT_MODELS } from '../constants';
|
||||
import {
|
||||
MAX_DELAY,
|
||||
MAX_MAX_CHAR_LIMIT,
|
||||
MIN_DELAY,
|
||||
MIN_MAX_CHAR_LIMIT,
|
||||
MIN_MAX_TOKENS,
|
||||
fewShotExampleSchema,
|
||||
modelOptionsSchema
|
||||
} from '../settings/versions/shared';
|
||||
export const DEFAULT_SETTINGS = {
|
||||
// version: "1",
|
||||
|
||||
// General settings
|
||||
autocompleteEnabled: true,
|
||||
advancedMode: false,
|
||||
apiProvider: "openai",
|
||||
// API settings
|
||||
azureOAIApiSettings: {
|
||||
key: "",
|
||||
url: "https://YOUR_AOI_SERVICE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions",
|
||||
},
|
||||
openAIApiSettings: {
|
||||
key: "",
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
model: "gpt-3.5-turbo",
|
||||
},
|
||||
ollamaApiSettings: {
|
||||
url: "http://localhost:11434/api/chat",
|
||||
model: "",
|
||||
},
|
||||
|
||||
// Trigger settings
|
||||
triggers: [
|
||||
{ type: "string", value: "# " },
|
||||
{ type: "string", value: ". " },
|
||||
{ type: "string", value: ": " },
|
||||
{ type: "string", value: ", " },
|
||||
{ type: "string", value: "! " },
|
||||
{ type: "string", value: "? " },
|
||||
{ type: "string", value: "`" },
|
||||
{ type: "string", value: "' " },
|
||||
{ type: "string", value: "= " },
|
||||
{ type: "string", value: "$ " },
|
||||
{ type: "string", value: "> " },
|
||||
{ type: "string", value: "\n" },
|
||||
|
||||
// bullet list
|
||||
{ type: "regex", value: "[\\t ]*(\\-|\\*)[\\t ]+$" },
|
||||
// numbered list
|
||||
{ type: "regex", value: "[\\t ]*[0-9A-Za-z]+\\.[\\t ]+$" },
|
||||
// new line with spaces
|
||||
{ type: "regex", value: "\\$\\$\\n[\\t ]*$" },
|
||||
// markdown multiline code block
|
||||
{ type: "regex", value: "```[a-zA-Z0-9]*(\\n\\s*)?$" },
|
||||
// task list normal, sub or numbered.
|
||||
{ type: "regex", value: "\\s*(-|[0-9]+\\.) \\[.\\]\\s+$" },
|
||||
],
|
||||
|
||||
delay: 500,
|
||||
// Request settings
|
||||
modelOptions: {
|
||||
temperature: 1,
|
||||
top_p: 0.1,
|
||||
frequency_penalty: 0.25,
|
||||
presence_penalty: 0,
|
||||
max_tokens: MIN_MAX_TOKENS,
|
||||
},
|
||||
// Prompt settings
|
||||
systemMessage: `Your job is to predict the most logical text that should be written at the location of the <mask/>.
|
||||
Your answer can be either code, a single word, or multiple sentences.
|
||||
If the <mask/> is in the middle of a partial sentence, your answer should only be the 1 or 2 words fixes the sentence and not the entire sentence.
|
||||
You are not allowed to have any overlapping text directly surrounding the <mask/>.
|
||||
Your answer must be in the same language as the text directly surrounding the <mask/>.
|
||||
Your response must have the following format:
|
||||
THOUGHT: here, you reason about the answer; use the 80/20 principle to be brief.
|
||||
LANGUAGE: here, you write the language of your answer, e.g. English, Python, Dutch, etc.
|
||||
ANSWER: here, you write the text that should be at the location of <mask/>
|
||||
`,
|
||||
fewShotExamples: [
|
||||
],
|
||||
userMessageTemplate: "{{prefix}}<mask/>{{suffix}}",
|
||||
chainOfThoughRemovalRegex: `(.|\\n)*ANSWER:`,
|
||||
// Preprocessing settings
|
||||
dontIncludeDataviews: true,
|
||||
maxPrefixCharLimit: 4000,
|
||||
maxSuffixCharLimit: 4000,
|
||||
// Postprocessing settings
|
||||
removeDuplicateMathBlockIndicator: true,
|
||||
removeDuplicateCodeBlockIndicator: true,
|
||||
ignoredFilePatterns: "**/secret/**\n",
|
||||
ignoredTags: "",
|
||||
cacheSuggestions: true,
|
||||
debugMode: false,
|
||||
};
|
||||
import { ApiProvider } from './llm/model';
|
||||
|
||||
export function isRegexValid(value: string): boolean {
|
||||
try {
|
||||
const regex = new RegExp(value);
|
||||
regex.test("");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidIgnorePattern(value: string): boolean {
|
||||
if (typeof value !== "string" || value.length === 0) return false;
|
||||
// 不允许以单个反斜杠结尾
|
||||
if (/\\$/.test(value)) return false;
|
||||
|
||||
const openerToCloser: Record<string, string> = { "[": "]", "{": "}", "(": ")" };
|
||||
const validExtglobLeaders = new Set(["!", "?", "+", "*", "@"]);
|
||||
const stack: string[] = [];
|
||||
|
||||
const isEscaped = (s: string, i: number): boolean => {
|
||||
let backslashes = 0;
|
||||
for (let k = i - 1; k >= 0 && s[k] === "\\"; k--) backslashes++;
|
||||
return backslashes % 2 === 1;
|
||||
};
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const ch = value[i];
|
||||
if (isEscaped(value, i)) continue;
|
||||
|
||||
if (ch === "[" || ch === "{" || ch === "(") {
|
||||
// 括号需作为 extglob 的一部分,如 !(...), ?(...), +(...), *(...), @(...)
|
||||
if (ch === "(") {
|
||||
const prev = value[i - 1];
|
||||
if (!validExtglobLeaders.has(prev ?? "")) return false;
|
||||
}
|
||||
stack.push(openerToCloser[ch]);
|
||||
} else if (ch === "]" || ch === "}" || ch === ")") {
|
||||
const expected = stack.pop();
|
||||
if (expected !== ch) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return stack.length === 0;
|
||||
}
|
||||
export const SETTINGS_SCHEMA_VERSION = 0.5
|
||||
|
||||
const InfioProviderSchema = z.object({
|
||||
name: z.literal('Infio'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'Infio',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const OpenRouterProviderSchema = z.object({
|
||||
name: z.literal('OpenRouter'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'OpenRouter',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const SiliconFlowProviderSchema = z.object({
|
||||
name: z.literal('SiliconFlow'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'SiliconFlow',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const AlibabaQwenProviderSchema = z.object({
|
||||
name: z.literal('AlibabaQwen'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'AlibabaQwen',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const AnthropicProviderSchema = z.object({
|
||||
name: z.literal('Anthropic'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().optional(),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'Anthropic',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const DeepSeekProviderSchema = z.object({
|
||||
name: z.literal('DeepSeek'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'DeepSeek',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const GoogleProviderSchema = z.object({
|
||||
name: z.literal('Google'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'Google',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const OpenAIProviderSchema = z.object({
|
||||
name: z.literal('OpenAI'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().optional(),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'OpenAI',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const OpenAICompatibleProviderSchema = z.object({
|
||||
name: z.literal('OpenAICompatible'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().optional(),
|
||||
useCustomUrl: z.boolean().catch(true),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'OpenAICompatible',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: true,
|
||||
models: []
|
||||
})
|
||||
|
||||
const OllamaProviderSchema = z.object({
|
||||
name: z.literal('Ollama'),
|
||||
apiKey: z.string().catch('ollama'),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'Ollama',
|
||||
apiKey: 'ollama',
|
||||
baseUrl: '',
|
||||
useCustomUrl: true,
|
||||
models: []
|
||||
})
|
||||
|
||||
const GroqProviderSchema = z.object({
|
||||
name: z.literal('Groq'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'Groq',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const GrokProviderSchema = z.object({
|
||||
name: z.literal('Grok'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'Grok',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const MoonshotProviderSchema = z.object({
|
||||
name: z.literal('Moonshot'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'Moonshot',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const LocalProviderSchema = z.object({
|
||||
name: z.literal('LocalProvider'),
|
||||
apiKey: z.string().catch(''),
|
||||
baseUrl: z.string().catch(''),
|
||||
useCustomUrl: z.boolean().catch(false),
|
||||
models: z.array(z.string()).catch([])
|
||||
}).catch({
|
||||
name: 'LocalProvider',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
useCustomUrl: false,
|
||||
models: []
|
||||
})
|
||||
|
||||
const ollamaModelSchema = z.object({
|
||||
baseUrl: z.string().catch(''),
|
||||
model: z.string().catch(''),
|
||||
})
|
||||
|
||||
const openAICompatibleModelSchema = z.object({
|
||||
baseUrl: z.string().catch(''),
|
||||
apiKey: z.string().catch(''),
|
||||
model: z.string().catch(''),
|
||||
})
|
||||
|
||||
const ragOptionsSchema = z.object({
|
||||
filesystem: z.enum(['idb', 'opfs']).catch('opfs'),
|
||||
chunkSize: z.number().catch(500),
|
||||
batchSize: z.number().catch(32),
|
||||
thresholdTokens: z.number().catch(8192),
|
||||
minSimilarity: z.number().catch(0.0),
|
||||
limit: z.number().catch(10),
|
||||
excludePatterns: z.array(z.string()).catch([]),
|
||||
includePatterns: z.array(z.string()).catch([]),
|
||||
})
|
||||
|
||||
export const triggerSchema = z.object({
|
||||
type: z.enum(['string', 'regex']),
|
||||
value: z.string().min(1, { message: "Trigger value must be at least 1 character long" })
|
||||
}).strict().superRefine((trigger, ctx) => {
|
||||
if (trigger.type === "regex") {
|
||||
if (!trigger.value.endsWith("$")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Regex triggers must end with a $.",
|
||||
path: ["value"],
|
||||
});
|
||||
}
|
||||
if (!isRegexValid(trigger.value)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid regex: "${trigger.value}"`,
|
||||
path: ["value"],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const FilesSearchSettingsSchema = z.object({
|
||||
method: z.enum(['match', 'regex', 'semantic', 'auto']).catch('auto'),
|
||||
regexBackend: z.enum(['coreplugin', 'ripgrep']).catch('coreplugin'),
|
||||
matchBackend: z.enum(['omnisearch', 'coreplugin']).catch('coreplugin'),
|
||||
ripgrepPath: z.string().catch(''),
|
||||
}).catch({
|
||||
method: 'auto',
|
||||
regexBackend: 'coreplugin',
|
||||
matchBackend: 'coreplugin',
|
||||
ripgrepPath: '',
|
||||
});
|
||||
|
||||
export const InfioSettingsSchema = z.object({
|
||||
// Version
|
||||
version: z.literal(SETTINGS_SCHEMA_VERSION).catch(SETTINGS_SCHEMA_VERSION),
|
||||
|
||||
// Provider
|
||||
defaultProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
infioProvider: InfioProviderSchema,
|
||||
openrouterProvider: OpenRouterProviderSchema,
|
||||
siliconflowProvider: SiliconFlowProviderSchema,
|
||||
alibabaQwenProvider: AlibabaQwenProviderSchema,
|
||||
anthropicProvider: AnthropicProviderSchema,
|
||||
deepseekProvider: DeepSeekProviderSchema,
|
||||
openaiProvider: OpenAIProviderSchema,
|
||||
googleProvider: GoogleProviderSchema,
|
||||
ollamaProvider: OllamaProviderSchema,
|
||||
groqProvider: GroqProviderSchema,
|
||||
grokProvider: GrokProviderSchema,
|
||||
moonshotProvider: MoonshotProviderSchema,
|
||||
openaicompatibleProvider: OpenAICompatibleProviderSchema,
|
||||
localproviderProvider: LocalProviderSchema,
|
||||
|
||||
// MCP Servers
|
||||
mcpEnabled: z.boolean().catch(false),
|
||||
|
||||
// Chat Model start list
|
||||
collectedChatModels: z.array(z.object({
|
||||
provider: z.nativeEnum(ApiProvider),
|
||||
modelId: z.string(),
|
||||
})).catch([]),
|
||||
|
||||
// Insight Model start list
|
||||
collectedInsightModels: z.array(z.object({
|
||||
provider: z.nativeEnum(ApiProvider),
|
||||
modelId: z.string(),
|
||||
})).catch([]),
|
||||
|
||||
// Apply Model start list
|
||||
collectedApplyModels: z.array(z.object({
|
||||
provider: z.nativeEnum(ApiProvider),
|
||||
modelId: z.string(),
|
||||
})).catch([]),
|
||||
|
||||
// Embedding Model start list
|
||||
collectedEmbeddingModels: z.array(z.object({
|
||||
provider: z.nativeEnum(ApiProvider),
|
||||
modelId: z.string(),
|
||||
})).catch([]),
|
||||
|
||||
// Active Provider Tab (for UI state)
|
||||
activeProviderTab: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
|
||||
// Chat Model
|
||||
chatModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
chatModelId: z.string().catch(''),
|
||||
|
||||
// Insight Model
|
||||
insightModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
insightModelId: z.string().catch(''),
|
||||
|
||||
// Apply Model
|
||||
applyModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
applyModelId: z.string().catch(''),
|
||||
|
||||
// Embedding Model
|
||||
embeddingModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||
embeddingModelId: z.string().catch(''),
|
||||
|
||||
// fuzzyMatchThreshold
|
||||
fuzzyMatchThreshold: z.number().catch(0.85),
|
||||
|
||||
// experimentalDiffStrategy
|
||||
experimentalDiffStrategy: z.boolean().catch(false),
|
||||
|
||||
// multiSearchReplaceDiffStrategy
|
||||
multiSearchReplaceDiffStrategy: z.boolean().catch(true),
|
||||
|
||||
// Workspace
|
||||
workspace: z.string().catch(''),
|
||||
// Mode
|
||||
mode: z.string().catch('ask'),
|
||||
defaultMention: z.enum(['none', 'current-file', 'vault']).catch('none'),
|
||||
|
||||
// web search
|
||||
serperApiKey: z.string().catch(''),
|
||||
serperSearchEngine: z.enum(['google', 'duckduckgo', 'bing']).catch('google'),
|
||||
jinaApiKey: z.string().catch(''),
|
||||
|
||||
// Files Search
|
||||
filesSearchSettings: FilesSearchSettingsSchema,
|
||||
|
||||
/// [compatible]
|
||||
// activeModels [compatible]
|
||||
activeModels: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
provider: z.string(),
|
||||
enabled: z.boolean(),
|
||||
isEmbeddingModel: z.boolean(),
|
||||
isBuiltIn: z.boolean(),
|
||||
apiKey: z.string().optional(),
|
||||
baseUrl: z.string().optional(),
|
||||
dimension: z.number().optional(),
|
||||
})
|
||||
).catch(DEFAULT_MODELS),
|
||||
// API Keys [compatible]
|
||||
infioApiKey: z.string().catch(''),
|
||||
openAIApiKey: z.string().catch(''),
|
||||
anthropicApiKey: z.string().catch(''),
|
||||
geminiApiKey: z.string().catch(''),
|
||||
groqApiKey: z.string().catch(''),
|
||||
deepseekApiKey: z.string().catch(''),
|
||||
ollamaEmbeddingModel: ollamaModelSchema.catch({
|
||||
baseUrl: '',
|
||||
model: '',
|
||||
}),
|
||||
ollamaChatModel: ollamaModelSchema.catch({
|
||||
baseUrl: '',
|
||||
model: '',
|
||||
}),
|
||||
openAICompatibleChatModel: openAICompatibleModelSchema.catch({
|
||||
baseUrl: '',
|
||||
apiKey: '',
|
||||
model: '',
|
||||
}),
|
||||
ollamaApplyModel: ollamaModelSchema.catch({
|
||||
baseUrl: '',
|
||||
model: '',
|
||||
}),
|
||||
openAICompatibleApplyModel: openAICompatibleModelSchema.catch({
|
||||
baseUrl: '',
|
||||
apiKey: '',
|
||||
model: '',
|
||||
}),
|
||||
|
||||
// System Prompt
|
||||
systemPrompt: z.string().catch(''),
|
||||
|
||||
// RAG Options
|
||||
ragOptions: ragOptionsSchema.catch({
|
||||
filesystem: 'opfs',
|
||||
batchSize: 32,
|
||||
chunkSize: 500,
|
||||
thresholdTokens: 8192,
|
||||
minSimilarity: 0.0,
|
||||
limit: 10,
|
||||
excludePatterns: [],
|
||||
includePatterns: [],
|
||||
}),
|
||||
|
||||
// autocomplete options
|
||||
autocompleteEnabled: z.boolean(),
|
||||
advancedMode: z.boolean(),
|
||||
|
||||
// [compatible]
|
||||
apiProvider: z.enum(['azure', 'openai', "ollama"]),
|
||||
azureOAIApiSettings: z.string().catch(''),
|
||||
openAIApiSettings: z.string().catch(''),
|
||||
ollamaApiSettings: z.string().catch(''),
|
||||
|
||||
triggers: z.array(triggerSchema),
|
||||
delay: z.number().int().min(MIN_DELAY, { message: "Delay must be between 0ms and 2000ms" }).max(MAX_DELAY, { message: "Delay must be between 0ms and 2000ms" }),
|
||||
modelOptions: modelOptionsSchema,
|
||||
systemMessage: z.string().min(3, { message: "System message must be at least 3 characters long" }),
|
||||
fewShotExamples: z.array(fewShotExampleSchema),
|
||||
userMessageTemplate: z.string().min(3, { message: "User message template must be at least 3 characters long" }),
|
||||
chainOfThoughRemovalRegex: z.string().refine((regex) => isRegexValid(regex), { message: "Invalid regex" }),
|
||||
dontIncludeDataviews: z.boolean(),
|
||||
maxPrefixCharLimit: z.number().int().min(MIN_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at least ${MIN_MAX_CHAR_LIMIT}` }).max(MAX_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at most ${MAX_MAX_CHAR_LIMIT}` }),
|
||||
maxSuffixCharLimit: z.number().int().min(MIN_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at least ${MIN_MAX_CHAR_LIMIT}` }).max(MAX_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at most ${MAX_MAX_CHAR_LIMIT}` }),
|
||||
removeDuplicateMathBlockIndicator: z.boolean(),
|
||||
removeDuplicateCodeBlockIndicator: z.boolean(),
|
||||
ignoredFilePatterns: z.string().refine((value) => value
|
||||
.split("\n")
|
||||
.filter(s => s.trim().length > 0)
|
||||
.filter(s => !isValidIgnorePattern(s)).length === 0,
|
||||
{ message: "Invalid ignore pattern" }
|
||||
),
|
||||
ignoredTags: z.string().refine((value) => value
|
||||
.split("\n")
|
||||
.filter(s => s.includes(" ")).length === 0, { message: "Tags cannot contain spaces" }
|
||||
).refine((value) => value
|
||||
.split("\n")
|
||||
.filter(s => s.includes("#")).length === 0, { message: "Enter tags without the # symbol" }
|
||||
).refine((value) => value
|
||||
.split("\n")
|
||||
.filter(s => s.includes(",")).length === 0, { message: "Enter each tag on a new line without commas" }
|
||||
),
|
||||
cacheSuggestions: z.boolean(),
|
||||
debugMode: z.boolean(),
|
||||
})
|
||||
|
||||
export type InfioSettings = z.infer<typeof InfioSettingsSchema>
|
||||
export type FilesSearchSettings = z.infer<typeof FilesSearchSettingsSchema>
|
||||
|
||||
type Migration = {
|
||||
fromVersion: number
|
||||
toVersion: number
|
||||
migrate: (data: Record<string, unknown>) => Record<string, unknown>
|
||||
}
|
||||
|
||||
const MIGRATIONS: Migration[] = [
|
||||
{
|
||||
fromVersion: 0.1,
|
||||
toVersion: 0.4,
|
||||
migrate: (data) => {
|
||||
const newData = { ...data }
|
||||
newData.version = 0.4
|
||||
return newData
|
||||
},
|
||||
},
|
||||
{
|
||||
fromVersion: 0.4,
|
||||
toVersion: 0.5,
|
||||
migrate: (data) => {
|
||||
const newData = { ...data }
|
||||
newData.version = SETTINGS_SCHEMA_VERSION
|
||||
|
||||
// Handle max_tokens minimum value increase from 800 to 4096
|
||||
if (newData.modelOptions && typeof newData.modelOptions === 'object') {
|
||||
const modelOptions = newData.modelOptions as Record<string, any>
|
||||
if (typeof modelOptions.max_tokens === 'number' && modelOptions.max_tokens < MIN_MAX_TOKENS) {
|
||||
console.log(`Updating max_tokens from ${modelOptions.max_tokens} to ${MIN_MAX_TOKENS} due to minimum value change`)
|
||||
modelOptions.max_tokens = MIN_MAX_TOKENS
|
||||
}
|
||||
}
|
||||
|
||||
return newData
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function migrateSettings(
|
||||
data: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
let currentData = { ...data }
|
||||
const currentVersion = (currentData.version as number) ?? 0
|
||||
|
||||
for (const migration of MIGRATIONS) {
|
||||
if (
|
||||
currentVersion >= migration.fromVersion &&
|
||||
currentVersion < migration.toVersion &&
|
||||
migration.toVersion <= SETTINGS_SCHEMA_VERSION
|
||||
) {
|
||||
console.debug(
|
||||
`Migrating settings from ${migration.fromVersion} to ${migration.toVersion}`,
|
||||
)
|
||||
currentData = migration.migrate(currentData)
|
||||
}
|
||||
}
|
||||
|
||||
return currentData
|
||||
}
|
||||
|
||||
export function parseInfioSettings(data: unknown): InfioSettings {
|
||||
try {
|
||||
const migratedData = migrateSettings(data as Record<string, unknown>)
|
||||
return InfioSettingsSchema.parse(migratedData)
|
||||
} catch (error) {
|
||||
console.error("Failed to parse settings with migrated data, using default settings instead: ", error);
|
||||
return InfioSettingsSchema.parse({ ...DEFAULT_SETTINGS })
|
||||
}
|
||||
}
|
||||
84
src/utils/device-id.ts
Normal file
84
src/utils/device-id.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { Platform } from 'obsidian'
|
||||
|
||||
const DEVICE_ID_STORAGE_KEY = 'infio_device_id'
|
||||
|
||||
function generatePseudoId(): string {
|
||||
// RFC4122-ish v4 UUID (non-crypto), sufficient for stable device identifier when persisted
|
||||
let timeSeed = Date.now()
|
||||
let perfSeed = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
||||
? Math.floor(performance.now() * 1000)
|
||||
: 0
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {
|
||||
let rand = Math.random() * 16
|
||||
if (timeSeed > 0) {
|
||||
rand = (timeSeed + rand) % 16
|
||||
timeSeed = Math.floor(timeSeed / 16)
|
||||
} else {
|
||||
rand = (perfSeed + rand) % 16
|
||||
perfSeed = Math.floor(perfSeed / 16)
|
||||
}
|
||||
const value = ch === 'x' ? rand : (rand & 0x3) | 0x8
|
||||
return Math.floor(value).toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
function safeGetLocalStorage(key: string): string | null {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
return window.localStorage.getItem(key)
|
||||
}
|
||||
} catch { /* noop */ }
|
||||
return null
|
||||
}
|
||||
|
||||
function safeSetLocalStorage(key: string, value: string): void {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
window.localStorage.setItem(key, value)
|
||||
}
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
|
||||
export async function getDeviceId(): Promise<string> {
|
||||
// On mobile, generate and persist a stable pseudo ID
|
||||
if (Platform.isMobile) {
|
||||
const existing = safeGetLocalStorage(DEVICE_ID_STORAGE_KEY)
|
||||
if (existing) return existing
|
||||
const generated = generatePseudoId()
|
||||
safeSetLocalStorage(DEVICE_ID_STORAGE_KEY, generated)
|
||||
return generated
|
||||
}
|
||||
|
||||
// Desktop: try node-machine-id; fall back to persisted pseudo ID
|
||||
try {
|
||||
const moduleName = 'node-machine-id'
|
||||
// Use dynamic import via variable to avoid bundlers pulling it into mobile builds
|
||||
const mod: unknown = await import(/* @vite-ignore */ moduleName)
|
||||
if (
|
||||
typeof mod === 'object' && mod !== null &&
|
||||
'machineId' in mod && typeof (mod as Record<string, unknown>).machineId === 'function'
|
||||
) {
|
||||
const id = await (mod as { machineId: () => Promise<string> }).machineId()
|
||||
if (id) return id
|
||||
}
|
||||
} catch {
|
||||
// ignore and fall back
|
||||
}
|
||||
|
||||
const existing = safeGetLocalStorage(DEVICE_ID_STORAGE_KEY)
|
||||
if (existing) return existing
|
||||
const generated = generatePseudoId()
|
||||
safeSetLocalStorage(DEVICE_ID_STORAGE_KEY, generated)
|
||||
return generated
|
||||
}
|
||||
|
||||
export function getOperatingSystem(): string {
|
||||
if (Platform.isWin) return 'windows'
|
||||
if (Platform.isMacOS) return 'macos'
|
||||
if (Platform.isLinux) return 'linux'
|
||||
if (Platform.isAndroidApp) return 'android'
|
||||
if (Platform.isIosApp) return 'ios'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
|
||||
475
styles.css
475
styles.css
@ -73,7 +73,8 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: calc(var(--size-4-1) * -1);
|
||||
margin-top: calc(var(--size-2-1) * -1);
|
||||
padding: 0 var(--size-2-2);
|
||||
}
|
||||
|
||||
.infio-chat-header-title {
|
||||
@ -89,8 +90,8 @@
|
||||
.infio-chat-current-workspace {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: var(--size-4-2);
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
margin-left: var(--size-2-2);
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
@ -102,11 +103,12 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.infio-chat-header-buttons {
|
||||
display: flex;
|
||||
gap: var(--size-2-1);
|
||||
gap: var(--size-1-1);
|
||||
}
|
||||
|
||||
.infio-chat-container {
|
||||
@ -118,12 +120,21 @@
|
||||
.infio-stop-gen-btn {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
bottom: 160px;
|
||||
bottom: 120px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-1);
|
||||
gap: var(--size-2-2);
|
||||
padding: var(--size-2-2) var(--size-4-3);
|
||||
background-color: var(--background-modifier-error);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
border-radius: var(--radius-l);
|
||||
font-size: var(--font-ui-medium);
|
||||
min-height: 44px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,8 +150,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-1);
|
||||
padding: 0 var(--size-4-3) var(--size-4-5) var(--size-4-3);
|
||||
margin: var(--size-4-2) calc(var(--size-4-3) * -1) 0;
|
||||
padding: 0 var(--size-2-2) var(--size-4-3) var(--size-2-2);
|
||||
margin: var(--size-2-2) calc(var(--size-2-2) * -1) 0;
|
||||
/* 确保内容不会超出容器 */
|
||||
min-width: 0;
|
||||
|
||||
@ -158,7 +169,7 @@
|
||||
.infio-chat-messages-assistant {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--size-2-2);
|
||||
/* 防止助手消息超出容器 */
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
@ -170,7 +181,8 @@
|
||||
*/
|
||||
.infio-starred-commands {
|
||||
border: none !important;
|
||||
margin-bottom: var(--size-4-2);
|
||||
margin-bottom: var(--size-2-2);
|
||||
padding: 0 var(--size-2-2);
|
||||
}
|
||||
|
||||
.infio-starred-commands-title {
|
||||
@ -193,17 +205,18 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 1px 8px;
|
||||
gap: var(--size-2-1);
|
||||
padding: var(--size-2-1) var(--size-2-3);
|
||||
background-color: rgba(86, 72, 29, 0.3) !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: var(--radius-l);
|
||||
font-size: var(--font-ui-small);
|
||||
font-size: var(--font-ui-medium);
|
||||
color: #f1c43f;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
max-width: 200px;
|
||||
min-height: 40px;
|
||||
max-width: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@ -581,16 +594,20 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
color: var(--text-muted);
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
}
|
||||
|
||||
@ -618,10 +635,11 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
||||
border: var(--input-border-width) solid var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
font-family: inherit;
|
||||
padding: calc(var(--size-2-2) + 1px);
|
||||
padding: var(--size-2-1);
|
||||
font-size: var(--font-ui-medium);
|
||||
border-radius: var(--radius-s);
|
||||
outline: none;
|
||||
margin: 0 var(--size-2-2) var(--size-2-2) var(--size-2-2);
|
||||
|
||||
&:focus-within,
|
||||
&:focus,
|
||||
@ -635,18 +653,18 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
||||
.infio-chat-user-input-files {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--size-4-1);
|
||||
gap: var(--size-2-1);
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: var(--size-4-1);
|
||||
padding-bottom: var(--size-2-1);
|
||||
}
|
||||
|
||||
.infio-chat-user-input-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--size-4-1);
|
||||
gap: var(--size-2-1);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: var(--size-4-4);
|
||||
height: 36px;
|
||||
|
||||
.infio-chat-user-input-controls__model-select-container {
|
||||
display: flex;
|
||||
@ -658,7 +676,7 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
||||
.infio-chat-user-input-controls__buttons {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: var(--size-4-3);
|
||||
gap: var(--size-2-1);
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@ -666,20 +684,23 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
||||
.infio-chat-user-input-controls .infio-chat-user-input-submit-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-1);
|
||||
font-size: var(--font-smallest);
|
||||
gap: var(--size-2-1);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0 var(--size-2-1);
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
border-radius: var(--radius-s);
|
||||
height: var(--size-4-4);
|
||||
height: 32px;
|
||||
min-width: 36px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease-in-out;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:active {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-chat-user-input-submit-button-icons {
|
||||
@ -691,20 +712,23 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
||||
.infio-chat-user-input-controls .infio-chat-user-input-vault-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-1);
|
||||
font-size: var(--font-smallest);
|
||||
gap: var(--size-2-1);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
background-color: var(--background-secondary-alt) !important;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0 var(--size-2-1);
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
border-radius: var(--radius-s);
|
||||
height: var(--size-4-4);
|
||||
height: 32px;
|
||||
min-width: 36px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease-in-out;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:active {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover) !important;
|
||||
}
|
||||
|
||||
.infio-chat-user-input-vault-button-icons {
|
||||
@ -719,12 +743,13 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: var(--font-smallest);
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
font-size: var(--font-ui-smaller);
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
gap: var(--size-2-1);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-height: 28px;
|
||||
|
||||
&.infio-chat-user-input-file-badge-focused {
|
||||
border: 1px solid var(--interactive-accent);
|
||||
@ -791,23 +816,27 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
||||
|
||||
.infio-chat-edit-cancel-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
top: var(--size-2-1);
|
||||
right: var(--size-2-1);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-on-accent-hover);
|
||||
background-color: var(--interactive-accent-hover);
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--text-muted);
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
width: 44px !important;
|
||||
height: 44px !important;
|
||||
border-radius: var(--radius-m) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--interactive-accent-hover) !important;
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--background-modifier-hover) !important;
|
||||
color: var(--text-normal) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -991,15 +1020,11 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
gap: var(--size-4-1);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
代码块样式 - 统一管理所有代码块相关样式
|
||||
=========================================== */
|
||||
|
||||
/* 基础代码块容器 */
|
||||
.infio-chat-code-block {
|
||||
position: relative;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
/* 防止代码块内容溢出 */
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@ -1008,36 +1033,63 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* RawMarkdownBlock 专用代码块容器 */
|
||||
/* 删除冲突的样式定义 */
|
||||
|
||||
/* RawMarkdownBlock 代码块容器样式 */
|
||||
.infio-raw-markdown-code-block {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
position: relative !important;
|
||||
z-index: 100 !important; /* 提高z-index优先级 */
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
/* 确保代码块不会被其他元素遮挡 */
|
||||
clear: both;
|
||||
box-sizing: border-box;
|
||||
/* 创建新的层叠上下文,避免z-index冲突 */
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* RawMarkdownBlock 内的 pre 元素 */
|
||||
.infio-raw-markdown-code-block pre {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* RawMarkdownBlock 内的 code 元素 */
|
||||
.infio-raw-markdown-code-block code {
|
||||
position: relative;
|
||||
display: block;
|
||||
/* 添加背景色以确保可见性 */
|
||||
background: transparent;
|
||||
/* 确保不被截断 */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 确保代码块内容正确显示 */
|
||||
.infio-raw-markdown-code-block pre {
|
||||
position: relative !important;
|
||||
z-index: 101 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
/* 确保完整显示 */
|
||||
width: 100% !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.infio-raw-markdown-code-block code {
|
||||
position: relative !important;
|
||||
z-index: 102 !important;
|
||||
/* 确保代码内容完整显示 */
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.infio-reasoning-content-wrapper {
|
||||
/* Height restrictions removed to show full content */
|
||||
/* 防止推理内容溢出 */
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
/* 折叠/展开功能相关样式 */
|
||||
.infio-reasoning-content-wrapper.collapsed {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.infio-reasoning-content-wrapper.expanded {
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* 代码块头部样式 */
|
||||
.infio-chat-code-block-header {
|
||||
display: none;
|
||||
justify-content: space-between;
|
||||
@ -1068,7 +1120,6 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* 代码块头部文件名样式 */
|
||||
.infio-chat-code-block-header-filename {
|
||||
padding-left: var(--size-4-2);
|
||||
font-size: var(--font-medium);
|
||||
@ -1085,7 +1136,6 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 代码块头部按钮样式 */
|
||||
.infio-chat-code-block-header-button {
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
@ -1105,7 +1155,9 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
padding-right: var(--size-4-1);
|
||||
}
|
||||
|
||||
.infio-chat-code-block.has-filename .infio-chat-code-block-header-button button {
|
||||
.infio-chat-code-block.has-filename
|
||||
.infio-chat-code-block-header-button
|
||||
button {
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
padding: 0 var(--size-4-2);
|
||||
@ -1114,11 +1166,11 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
font-size: var(--font-medium);
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infio-chat-code-block.has-filename .infio-chat-code-block-header-button button:hover {
|
||||
&:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.infio-chat-code-block-header-button button {
|
||||
display: flex;
|
||||
@ -1126,36 +1178,12 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
/* 代码块内容样式 */
|
||||
.infio-chat-code-block-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 特殊按钮样式 */
|
||||
.infio-dataview-query-button {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
推理内容包装器样式
|
||||
=========================================== */
|
||||
|
||||
.infio-reasoning-content-wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
/* 折叠/展开功能相关样式 */
|
||||
.infio-reasoning-content-wrapper.collapsed {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.infio-reasoning-content-wrapper.expanded {
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
.infio-chat-code-block-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Read File Block - Minimal styling for better integration */
|
||||
@ -1536,11 +1564,11 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
*/
|
||||
|
||||
.infio-chat-lexical-content-editable-root {
|
||||
min-height: 62px;
|
||||
max-height: 800px;
|
||||
min-height: 36px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
padding: var(--size-2-1);
|
||||
}
|
||||
|
||||
.infio-chat-lexical-content-editable-root .mention {
|
||||
@ -1556,10 +1584,11 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
}
|
||||
|
||||
.infio-search-lexical-content-editable-root {
|
||||
min-height: 36px;
|
||||
max-height: 120px;
|
||||
min-height: 40px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: var(--size-2-1);
|
||||
/* 确保编辑器不会影响外部滚动 */
|
||||
contain: size style;
|
||||
}
|
||||
@ -1713,22 +1742,27 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
gap: var(--size-2-1);
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-m);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2281,6 +2315,49 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
/* 移动端专用优化 */
|
||||
|
||||
/* 增加触摸目标的最小尺寸 */
|
||||
button, .clickable-icon, [role="button"] {
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* 输入框按钮使用较小的尺寸以节省空间 */
|
||||
.infio-chat-user-input-controls button,
|
||||
.infio-chat-user-input-controls .clickable-icon {
|
||||
min-height: 32px;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
/* 聊天头部按钮保持36px尺寸 */
|
||||
.infio-chat-header-buttons button {
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
/* 优化滚动性能 */
|
||||
.infio-chat-messages,
|
||||
.infio-chat-lexical-content-editable-root,
|
||||
.infio-search-lexical-content-editable-root {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 优化文本选择 */
|
||||
.infio-chat-messages {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* 防止双击缩放 */
|
||||
button, .clickable-icon {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* control pc and mobile view */
|
||||
|
||||
.desktop-only {
|
||||
@ -2686,20 +2763,21 @@ button.infio-chat-input-model-select {
|
||||
box-shadow: none;
|
||||
border: 1;
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
font-size: var(--font-smallest);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: var(--size-4-4);
|
||||
height: 32px;
|
||||
max-width: 100%;
|
||||
gap: var(--size-2-2);
|
||||
gap: var(--size-2-1);
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.15s ease-in-out;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:active {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
@ -2799,6 +2877,7 @@ button.infio-chat-input-model-select {
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: var(--size-4-3);
|
||||
}
|
||||
|
||||
.infio-chat-loading-container {
|
||||
@ -2998,8 +3077,10 @@ button.infio-chat-input-model-select {
|
||||
}
|
||||
|
||||
.infio-commands-header-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: var(--text-normal);
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
margin: 0 0 var(--size-4-3) 0;
|
||||
}
|
||||
|
||||
.infio-commands-label {
|
||||
@ -3655,18 +3736,18 @@ button.infio-chat-input-model-select {
|
||||
|
||||
.infio-markdown-actions {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
right: 1px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0.7;
|
||||
visibility: visible;
|
||||
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
|
||||
padding: 0px;
|
||||
z-index: 10; /* 降低z-index,确保不会遮挡代码块 */
|
||||
z-index: 50; /* 降低z-index,确保不会遮挡代码块 */
|
||||
/* 确保不会遮挡代码块 */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.infio-markdown-actions button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -3677,18 +3758,17 @@ button.infio-chat-input-model-select {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
color: var(--text-muted);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
/* 恢复按钮的点击事件 */
|
||||
pointer-events: auto;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.infio-chat-message-actions-icon--copied {
|
||||
.infio-markdown-actions button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-markdown-actions .infio-chat-message-actions-icon--copied {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@ -3957,136 +4037,3 @@ button.infio-chat-input-model-select {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Structured Progress Styles */
|
||||
.infio-structured-progress {
|
||||
padding: 16px;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.infio-progress-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.infio-progress-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.infio-progress-step-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infio-progress-step-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.infio-progress-step-line {
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
background-color: var(--background-modifier-border);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.infio-progress-step-line.completed {
|
||||
background-color: var(--color-green, #28a745);
|
||||
}
|
||||
|
||||
.infio-progress-step-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.infio-progress-step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.infio-progress-step-title {
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.infio-progress-step.running .infio-progress-step-title {
|
||||
color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.infio-progress-step-counter {
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-monospace);
|
||||
background-color: var(--background-modifier-border);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.infio-progress-step-description {
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
margin: 4px 0 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.infio-progress-step-details {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-faint);
|
||||
margin: 2px 0 0 0;
|
||||
line-height: 1.3;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.infio-progress-step-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: 2px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.infio-progress-step-bar-fill {
|
||||
height: 100%;
|
||||
background-color: var(--interactive-accent);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Tool result content display */
|
||||
.infio-tool-result-content {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user