Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/web-ui/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,41 @@ export default tseslint.config(
'src/shared/context-menu-system/examples/**',
],
},
{
// Adapter-layer fence: business Tauri commands must reach the platform
// only through ApiClient (api.invoke). Direct `invoke` from
// '@tauri-apps/api/core' is reserved for the adapter implementations in
// adapters/** (and the peer-device host bridge, which intentionally runs
// outside the routed transport — see PeerHostInvokeBridge). This is the
// executable form of "front end calls go through the adapter layer";
// reintroducing a direct invoke elsewhere fails the build.
files: ['src/**/*.{ts,tsx}'],
ignores: [
'src/infrastructure/api/adapters/**',
// PeerHostInvokeBridge runs on the HOST side of Peer-Device Mode: it
// executes *dynamic* command names forwarded from the peer device via a
// raw Tauri invoke. ApiClient is already routed to the peer adapter at
// this point, so routing through it would be wrong. This is the one
// intentional exception to the adapter-layer fence.
'src/infrastructure/peer-device/PeerHostInvokeBridge.tsx',
],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@tauri-apps/api/core'],
importNames: ['invoke'],
message:
'业务命令必须经 api.invoke(ApiClient) 统一适配层,不可直接 import invoke。' +
'如需直连平台 invoke,放到 adapters/ 内并经 api 暴露。',
},
],
},
],
},
},
{
files: ['src/**/*.{ts,tsx}'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
Expand Down
6 changes: 0 additions & 6 deletions src/web-ui/src/flow_chat/services/FlowChatManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ vi.mock('../store/FlowChatStore', () => ({
},
}));

vi.mock('../../shared/services/agent-service', () => ({
AgentService: {
getInstance: vi.fn(() => ({})),
},
}));

vi.mock('@/infrastructure/api/service-api/ACPClientAPI', () => ({
ACPClientAPI: {},
}));
Expand Down
9 changes: 1 addition & 8 deletions src/web-ui/src/flow_chat/services/FlowChatManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import { processingStatusManager } from './ProcessingStatusManager';
import { FlowChatStore } from '../store/FlowChatStore';
import { useModernFlowChatStore } from '../store/modernFlowChatStore';
import { AgentService } from '../../shared/services/agent-service';
import { ACPClientAPI } from '@/infrastructure/api/service-api/ACPClientAPI';
import { stateMachineManager } from '../state-machine';
import { EventBatcher } from './EventBatcher';
Expand Down Expand Up @@ -79,7 +78,6 @@ const EVENT_LISTENER_RETRY_MS = 2000;
export class FlowChatManager {
private static instance: FlowChatManager | null = null;
private context: FlowChatContext;
private agentService: AgentService;
private eventListenerInitialized = false;
private eventListenerInitializationPromise: Promise<void> | null = null;
private eventListenerCleanup: (() => void) | null = null;
Expand Down Expand Up @@ -117,8 +115,7 @@ export class FlowChatManager {
currentWorkspacePath: null,
ensureLiveSubscription: () => this.ensureEventListeners(),
};

this.agentService = AgentService.getInstance();

registerDriverSessionLookup(
sessionId => this.context.flowChatStore.getState().sessions.get(sessionId),
);
Expand Down Expand Up @@ -917,10 +914,6 @@ export class FlowChatManager {
updateImageAnalysisItemModule(this.context, sessionId, dialogTurnId, imageId, updates);
}

async getAvailableAgents(): Promise<string[]> {
return this.agentService.getAvailableAgents();
}

getCurrentSession() {
return this.context.flowChatStore.getActiveSession();
}
Expand Down
12 changes: 6 additions & 6 deletions src/web-ui/src/infrastructure/api/insightsApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core';
import { api } from './service-api/ApiClient';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import { openPath } from '@tauri-apps/plugin-opener';

Expand Down Expand Up @@ -176,29 +176,29 @@ export interface InsightsProgressEvent {

export const insightsApi = {
async generateInsights(days?: number, modelId?: string): Promise<InsightsReport> {
return invoke('generate_insights', {
return api.invoke('generate_insights', {
request: { days: days ?? 30, modelId: modelId || 'auto' },
});
},

async getLatestInsights(): Promise<InsightsReportMeta[]> {
return invoke('get_latest_insights');
return api.invoke('get_latest_insights');
},

async loadReport(path: string): Promise<InsightsReport> {
return invoke('load_insights_report', {
return api.invoke('load_insights_report', {
request: { path },
});
},

async hasInsightsData(days?: number): Promise<boolean> {
return invoke('has_insights_data', {
return api.invoke('has_insights_data', {
request: { days: days ?? 30 },
});
},

async cancelGeneration(): Promise<void> {
return invoke('cancel_insights_generation');
return api.invoke('cancel_insights_generation');
},

async listenProgress(
Expand Down
12 changes: 6 additions & 6 deletions src/web-ui/src/infrastructure/api/service-api/I18nAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@


import { invoke } from '@tauri-apps/api/core';
import { api } from './ApiClient';
import type { LocaleId, LocaleMetadata, I18nConfig } from '@/infrastructure/i18n/types';
import { getLocaleMetadata } from '@/infrastructure/i18n/presets';
import { createLogger } from '@/shared/utils/logger';
Expand All @@ -21,7 +21,7 @@ class I18nAPIClass {

async getCurrentLanguage(): Promise<LocaleId> {
try {
const language = await invoke<string>('i18n_get_current_language');
const language = await api.invoke<string>('i18n_get_current_language');
return language as LocaleId;
} catch (error) {
log.warn('Failed to get current language, using default', error);
Expand All @@ -31,14 +31,14 @@ class I18nAPIClass {


async setLanguage(language: LocaleId): Promise<string> {
return invoke<string>('i18n_set_language', {
return api.invoke<string>('i18n_set_language', {
request: { language }
});
}


async getSupportedLanguages(): Promise<LocaleMetadata[]> {
const response = await invoke<LocaleMetadataResponse[]>('i18n_get_supported_languages');
const response = await api.invoke<LocaleMetadataResponse[]>('i18n_get_supported_languages');

return response.map(item => {
const id = item.id as LocaleId;
Expand Down Expand Up @@ -67,7 +67,7 @@ class I18nAPIClass {

async getConfig(): Promise<I18nConfig> {
try {
const config = await invoke<any>('i18n_get_config');
const config = await api.invoke<any>('i18n_get_config');
return {
currentLanguage: config.currentLanguage || 'zh-CN',
fallbackLanguage: config.fallbackLanguage || 'en-US',
Expand All @@ -87,7 +87,7 @@ class I18nAPIClass {


async setConfig(config: Partial<I18nConfig>): Promise<string> {
return invoke<string>('i18n_set_config', { config });
return api.invoke<string>('i18n_set_config', { config });
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import { readFile } from '@tauri-apps/plugin-fs';
import type { AgentCompanionPetSelection } from './AIExperienceConfigService';
import { isTauriRuntime } from '@/infrastructure/runtime';
Expand Down Expand Up @@ -172,7 +172,7 @@ export async function listAgentCompanionPets(): Promise<AgentCompanionPetPackage
const builtinPets = await Promise.all(BUILTIN_PETS.map(withPreviewSrc));
if (!isTauriRuntime()) return builtinPets;
try {
const response = await invoke<ListAgentCompanionPetsResponse>('list_agent_companion_pets');
const response = await api.invoke<ListAgentCompanionPetsResponse>('list_agent_companion_pets');
const userPets = await Promise.all(response.pets.map(withPreviewSrc));
return [...builtinPets, ...userPets];
} catch (error) {
Expand All @@ -182,14 +182,14 @@ export async function listAgentCompanionPets(): Promise<AgentCompanionPetPackage
}

export async function importAgentCompanionPetPackage(path: string): Promise<AgentCompanionPetPackage> {
const pet = await invoke<AgentCompanionPetSelection>('import_agent_companion_pet_package', {
const pet = await api.invoke<AgentCompanionPetSelection>('import_agent_companion_pet_package', {
request: { path },
});
return withPreviewSrc(pet);
}

export async function deleteAgentCompanionPetPackage(packagePath: string): Promise<void> {
await invoke('delete_agent_companion_pet_package', {
await api.invoke('delete_agent_companion_pet_package', {
request: { packagePath },
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Wraps all Tauri `invoke` calls for the announcement system so that the
* rest of the frontend never touches `invoke` directly.
*/
import { invoke } from '@tauri-apps/api/core';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import type { AnnouncementCard } from '../types';
import { createLogger } from '@/shared/utils/logger';

Expand All @@ -18,7 +18,7 @@ export const announcementService = {
*/
async getPendingAnnouncements(): Promise<AnnouncementCard[]> {
try {
return await invoke<AnnouncementCard[]>('get_pending_announcements');
return await api.invoke<AnnouncementCard[]>('get_pending_announcements');
} catch (e) {
log.error('Failed to get pending announcements', e);
return [];
Expand All @@ -28,7 +28,7 @@ export const announcementService = {
/** Mark a card as seen (modal was opened or action button was clicked). */
async markSeen(id: string): Promise<void> {
try {
await invoke('mark_announcement_seen', { request: { id } });
await api.invoke('mark_announcement_seen', { request: { id } });
} catch (e) {
log.error('Failed to mark announcement seen', { id, error: e });
}
Expand All @@ -37,7 +37,7 @@ export const announcementService = {
/** Dismiss a card for the current version cycle. */
async dismiss(id: string): Promise<void> {
try {
await invoke('dismiss_announcement', { request: { id } });
await api.invoke('dismiss_announcement', { request: { id } });
} catch (e) {
log.error('Failed to dismiss announcement', { id, error: e });
}
Expand All @@ -46,7 +46,7 @@ export const announcementService = {
/** Permanently suppress a card. */
async neverShow(id: string): Promise<void> {
try {
await invoke('never_show_announcement', { request: { id } });
await api.invoke('never_show_announcement', { request: { id } });
} catch (e) {
log.error('Failed to suppress announcement', { id, error: e });
}
Expand All @@ -58,7 +58,7 @@ export const announcementService = {
*/
async triggerCard(id: string): Promise<AnnouncementCard | null> {
try {
return await invoke<AnnouncementCard | null>('trigger_announcement', { request: { id } });
return await api.invoke<AnnouncementCard | null>('trigger_announcement', { request: { id } });
} catch (e) {
log.error('Failed to trigger announcement', { id, error: e });
return null;
Expand All @@ -68,7 +68,7 @@ export const announcementService = {
/** Fetch all currently eligible tip cards (for a tips browser). */
async getTips(): Promise<AnnouncementCard[]> {
try {
return await invoke<AnnouncementCard[]>('get_announcement_tips');
return await api.invoke<AnnouncementCard[]>('get_announcement_tips');
} catch (e) {
log.error('Failed to get announcement tips', e);
return [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@


import React from 'react';
import { invoke } from '@tauri-apps/api/core';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import { FileIcon, CheckCircle } from 'lucide-react';
import type { FileContext, ValidationResult, RenderOptions } from '../../../types/context';
import type {
ContextTransformer,
ContextValidator,
ContextCardRenderer
import type {
ContextTransformer,
ContextValidator,
ContextCardRenderer
} from '../../../services/ContextRegistry';
import { i18nService } from '@/infrastructure/i18n';

Expand Down Expand Up @@ -42,7 +42,7 @@ export class FileContextValidator implements ContextValidator<'file'> {
async validate(context: FileContext): Promise<ValidationResult> {
try {

const exists = await invoke<boolean>('fs_exists', { path: context.filePath });
const exists = await api.invoke<boolean>('fs_exists', { path: context.filePath });

if (!exists) {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@


import React from 'react';
import { invoke } from '@tauri-apps/api/core';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import { Image as ImageIcon, Eye } from 'lucide-react';
import { Modal, Button } from '@/component-library';
import type { ImageContext, ValidationResult, RenderOptions } from '../../../types/context';
Expand Down Expand Up @@ -86,7 +86,7 @@ export class ImageContextValidator implements ContextValidator<'image'> {

if (context.isLocal && context.imagePath) {
try {
const exists = await invoke<boolean>('check_path_exists', {
const exists = await api.invoke<boolean>('check_path_exists', {
request: {
path: context.imagePath
}
Expand Down
Loading