diff --git a/backend/app/contracts.py b/backend/app/contracts.py index 22b47e8..2cebba3 100644 --- a/backend/app/contracts.py +++ b/backend/app/contracts.py @@ -993,6 +993,8 @@ class TranscriptionJob(Contract): class IndexStatus(Contract): + total_notes: int = 0 + total_blocks: int = 0 status: Literal["idle", "queued", "running", "failed"] = "idle" pending_jobs: int = 0 active_job_id: str | None = None diff --git a/backend/app/routes.py b/backend/app/routes.py index 38d319b..ef9f8d3 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -120,6 +120,13 @@ from app.services.attachment_service import attachment_path router = APIRouter(prefix="/api") +@router.get("/permissions/policy", tags=["Permissions"]) +async def get_permission_policy() -> dict[str, str]: + from app.agent.permissions import KNOWN_PERMISSIONS + return {permission: container.permissions.policy.mode_for(permission).value + for permission in sorted(KNOWN_PERMISSIONS)} + + async def mcp_call_async(operation): """Even registry reads can wait on lifecycle locks; keep all MCP work off the event loop.""" try: diff --git a/backend/app/services/index_service.py b/backend/app/services/index_service.py index c501f57..e635577 100644 --- a/backend/app/services/index_service.py +++ b/backend/app/services/index_service.py @@ -126,9 +126,12 @@ async def rebuild(request: IndexRebuildRequest) -> IndexJob: def get_status() -> IndexStatus: + counts = repository.stats() if _active_job_id is not None: - return IndexStatus(status="running", pending_jobs=0, active_job_id=_active_job_id) + return IndexStatus(status="running", pending_jobs=0, active_job_id=_active_job_id, + total_notes=counts["notes"], total_blocks=counts["blocks"]) return IndexStatus( + total_notes=counts["notes"], total_blocks=counts["blocks"], status="failed" if _last_error else "idle", pending_jobs=0, last_completed_at=_last_completed_at, diff --git a/backend/tests/test_live_frontend_metadata.py b/backend/tests/test_live_frontend_metadata.py new file mode 100644 index 0000000..96c6d4e --- /dev/null +++ b/backend/tests/test_live_frontend_metadata.py @@ -0,0 +1,31 @@ +import asyncio + +from fastapi.testclient import TestClient + +from app.main import app +from app.container import container +from app.agent.permissions import PermissionMode +from app.services.note_service import create_note + + +def test_index_status_returns_real_counts(): + with TestClient(app) as client: + initial = client.get('/api/index/status').json() + assert (initial['total_notes'], initial['total_blocks']) == (0, 0) + note = asyncio.run(create_note(title='Real note', markdown='# Real note\n\ncontent', folder=None, tags=[])) + result = client.get('/api/index/status').json() + assert result['total_notes'] == 1 + assert result['total_blocks'] == len(note.blocks) + + +def test_permissions_endpoint_reads_effective_backend_policy(): + policy = container.permissions.policy + original = policy.mode_for('attachments.read') + try: + policy.set_rule('attachments.read', PermissionMode.deny) + with TestClient(app) as client: + response = client.get('/api/permissions/policy') + assert response.status_code == 200 + assert response.json()['attachments.read'] == 'deny' + finally: + policy.set_rule('attachments.read', original) diff --git a/frontend/src/components/common/StatusBar.vue b/frontend/src/components/common/StatusBar.vue index 992191e..db815a2 100644 --- a/frontend/src/components/common/StatusBar.vue +++ b/frontend/src/components/common/StatusBar.vue @@ -39,11 +39,12 @@ const saveStatusColor = computed(() => { const indexStatusText = computed(() => { const s = settingsStore.indexStatus.status - return s === 'idle' ? '索引就绪' : s === 'indexing' ? `索引中 (${settingsStore.indexStatus.pending_jobs})` : '索引错误' + return s === 'unknown' ? '索引状态未获取' : s === 'idle' ? '索引就绪' : s === 'indexing' ? `索引中 (${settingsStore.indexStatus.pending_jobs})` : '索引错误' }) const aiCoreStatusText = computed(() => { const map: Record = { + unknown: 'AI Core 状态未获取', starting: 'AI Core 启动中', running: 'AI Core 运行中', stopped: 'AI Core 已停止', diff --git a/frontend/src/contracts/index.ts b/frontend/src/contracts/index.ts index 8449a6e..d550c0a 100644 --- a/frontend/src/contracts/index.ts +++ b/frontend/src/contracts/index.ts @@ -207,8 +207,8 @@ export interface PermissionRequest { } export interface TokenUsage { - input_tokens: number - output_tokens: number + input_tokens?: number + output_tokens?: number total_tokens: number } @@ -462,11 +462,11 @@ export interface TaskItem { title: string description?: string status: TaskStatus - priority: TaskPriority + priority?: TaskPriority due_date?: string note_id?: string note_title?: string - source: TaskSource + source?: TaskSource created_at: string updated_at: string } @@ -487,12 +487,12 @@ export interface ThemeConfig { // ============ Index ============ export interface IndexStatus { - status: 'idle' | 'indexing' | 'error' + status: 'unknown' | 'idle' | 'indexing' | 'error' pending_jobs: number - total_notes: number - total_blocks: number - fts_enabled: boolean - vector_enabled: boolean + total_notes: number | null + total_blocks: number | null + fts_enabled?: boolean + vector_enabled?: boolean embedding_model?: string reranker_model?: string last_indexed_at?: string @@ -527,7 +527,7 @@ export type SaveStatus = | 'external_changed' | 'conflict' -export type AiCoreStatus = 'starting' | 'running' | 'stopped' | 'error' +export type AiCoreStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error' // ============ FastAPI wire contracts ============ // UI view models above may contain presentation-only fields. Services must use @@ -777,6 +777,8 @@ export interface ApiTask { } export interface ApiIndexStatus { + total_notes: number + total_blocks: number status: 'idle' | 'queued' | 'running' | 'failed' pending_jobs: number active_job_id?: string | null diff --git a/frontend/src/features/agent/AgentView.vue b/frontend/src/features/agent/AgentView.vue index a7f41fd..11ed477 100644 --- a/frontend/src/features/agent/AgentView.vue +++ b/frontend/src/features/agent/AgentView.vue @@ -14,7 +14,7 @@ const providerStore = useProviderStore() const skillStore = useSkillStore() const pageError = ref('') const form = reactive({ - input: '', provider_id: 'mock', model: 'mock-1', skill_id: '', max_steps: 10, + input: '', provider_id: '', model: '', skill_id: '', max_steps: 10, tool_timeout_seconds: 30, run_timeout_seconds: 300, token_budget: 8000, allow_network: false, max_concurrent_tools: 1, allowed_tools: [] as string[], }) @@ -25,7 +25,7 @@ const isNewRun = computed(() => !route.params.runId) onMounted(async () => { try { await Promise.all([providerStore.loadProviders(), skillStore.loadSkills(), agentStore.loadTools()]) - await providerStore.loadModels(form.provider_id) + form.provider_id = providerStore.defaultProviderId } catch (error) { pageError.value = error instanceof Error ? error.message : '智能体配置加载失败' } }) @@ -35,7 +35,10 @@ watch(() => route.params.runId, async (runId) => { }, { immediate: true }) watch(() => form.provider_id, async (providerId) => { - try { await providerStore.loadModels(providerId); form.model = models.value[0]?.model_id ?? '' } catch { /* page keeps current selection */ } + form.model = providerStore.providers.find(p => p.provider_id === providerId)?.default_model ?? '' + if (!providerId) return + try { await providerStore.loadModels(providerId) } + catch (error) { if (form.provider_id === providerId) pageError.value = error instanceof Error ? error.message : '模型列表加载失败,请手动填写模型 ID。' } }) function toggleTool(name: string) { @@ -47,6 +50,7 @@ function toggleTool(name: string) { async function createRun() { pageError.value = '' try { + if (!form.provider_id || !form.model.trim()) throw new Error('请选择提供商并填写模型 ID。') const run = await agentStore.createRun({ input: form.input, provider_id: form.provider_id, model: form.model, skill_id: form.skill_id || undefined, allowed_tools: form.allowed_tools, @@ -71,12 +75,12 @@ function eventText(event: AgentEvent) {

{{ isNewRun ? '创建智能体运行' : '智能体执行轨迹' }}

配置执行边界,并实时查看模型、工具和权限事件。

-
{{ pageError || agentStore.error }}
+
{{ pageError || agentStore.error || providerStore.error }}