Compare commits

...
102 changed files with 18027 additions and 215 deletions
+2
View File
@@ -1,5 +1,7 @@
# Notes Agent(暂命名) 团队开发说明
> 第二阶段收尾(开发分支,2026-09-07):标准 Agent/RAG Benchmark 与报告页、函数图预览、三格式快照导出及真实 Provider/MCP 结果见[实现与验收记录](docs/development/第二阶段收尾实现与验收-2026-09-07.md)。当前分支尚未合并,不更改下文历史 main 基线。
> 本文件用于团队开发期间快速配置环境、启动项目并了解当前实现状态,不是正式的项目 README。
NotesAgent 是本地优先的 AI 笔记与知识库项目。当前可运行形态为 Vue/Vite Web 前端与 FastAPI AI CoreMarkdown 和附件保存在本地 Vault,SQLite 管理元数据、全文索引、向量空间、搜索历史、AI 会话、任务、Agent Trace、多模态任务及运行诊断。AI 对话已接入知识库检索,会话与消息由后端持久化并供 Web 和桌面客户端共用。
+2
View File
@@ -1,5 +1,7 @@
# NotesAgent Backend
> 第二阶段收尾:标准 Agent/RAG Benchmark 与报告页、函数图预览、三格式快照导出及真实 Provider/MCP 结果见[实现与验收记录](../docs/development/第二阶段收尾实现与验收-2026-09-07.md)。当前分支尚未合并,不更改下文历史 main 基线。
NotesAgent Backend 是基于 Python 3.11+、FastAPI、Pydantic v2 和 SQLite 的本地 AI Core / Agent Core,使用 uv 管理 API 依赖和虚拟环境。
当前实现包含 Knowledge/Retrieval、Chat、Agent、Tool/Permission、Skill/Plugin、MCP、模型提供商、RAG Benchmark、多模态任务、本地模型调度、Token/音频用量和运行诊断。数据持久化位于后端 SQLite 与 VaultTauri Sidecar 生命周期、Stronghold 和操作系统级 Plugin 沙箱属于后续桌面阶段。
+4 -3
View File
@@ -6,7 +6,7 @@ from pydantic import BaseModel, ConfigDict, Field
from app.contracts import ToolDefinition
from app.services import note_service
Format = Literal['heading', 'paragraph', 'bold', 'italic', 'strikethrough', 'inline-code', 'bullet-list', 'ordered-list', 'task-list', 'blockquote', 'callout', 'code-block', 'mermaid', 'inline-math', 'math-block', 'link', 'image', 'table', 'horizontal-rule', 'hard-break', 'reference-link', 'html', 'metadata']
Format = Literal['heading', 'paragraph', 'bold', 'italic', 'strikethrough', 'inline-code', 'bullet-list', 'ordered-list', 'task-list', 'blockquote', 'callout', 'code-block', 'mermaid', 'function-plot', 'inline-math', 'math-block', 'link', 'image', 'table', 'horizontal-rule', 'hard-break', 'reference-link', 'html', 'metadata']
CALLOUTS = ['note', 'abstract', 'summary', 'tldr', 'info', 'todo', 'tip', 'hint', 'important', 'success', 'check', 'done', 'question', 'help', 'faq', 'warning', 'caution', 'attention', 'failure', 'fail', 'missing', 'danger', 'error', 'bug', 'example', 'quote', 'cite']
@@ -56,7 +56,7 @@ def compose(arguments: ComposeArguments, _):
elif kind == 'inline-code':
marker = '`' * (max([0, *(len(m[0]) for m in re.finditer(r'`+', text))]) + 1)
result = marker + ' ' + text.replace('\n', ' ') + ' ' + marker
elif kind in ('code-block', 'mermaid'): result = fenced(text, 'mermaid' if kind == 'mermaid' else a.language)
elif kind in ('code-block', 'mermaid', 'function-plot'): result = fenced(text, kind if kind != 'code-block' else a.language)
elif kind in ('bullet-list', 'ordered-list', 'task-list'):
result = '\n'.join((f'{i + 1}. ' if kind == 'ordered-list' else '- [ ] ' if kind == 'task-list' else '- ') + item.replace('\n', '\n ') for i, item in enumerate(a.items))
elif kind == 'blockquote': result = '\n'.join('> ' + line for line in text.split('\n'))
@@ -90,7 +90,8 @@ def catalog(_, __):
from typing import get_args
return {'formats': list(get_args(Format)), 'callouts': CALLOUTS,
'workflow': 'Use markdown.compose, then notes.create or notes.patch_markdown to persist. Read notes.read.content_hash before patching. metadata composition replaces the frontmatter only when you explicitly patch it; do not prepend duplicate frontmatter.',
'rendering': 'Math, Mermaid, callouts and auto-links depend on editor preferences. HTML is sanitized; scripts are not supported. Heading folding, font size, undo and redo are UI state, not Markdown document syntax. Callout collapsed=null is static, true is folded, false is expanded.'}
'function_plot': 'Use a function-plot fenced block: domain: -4, 4 followed by y = x^2 and y = sin(x). At most 16 expressions per block, 16 plots and 8000 total AST nodes per exported document. No arbitrary code execution.',
'rendering': 'Function plots, Math, Mermaid, callouts and auto-links depend on editor preferences. HTML is sanitized; scripts are not supported. Heading folding, font size, undo and redo are UI state, not Markdown document syntax. Callout collapsed=null is static, true is folded, false is expanded.'}
async def patch(arguments: PatchArguments, _):
+153
View File
@@ -0,0 +1,153 @@
"""通过真实 AgentRuntime 执行标准任务评测,不使用脚本化替代运行器。"""
import asyncio
from time import perf_counter
from uuid import uuid4
from app.contracts import (AgentBenchmarkRequest, AgentCaseResult, AgentRunCreateRequest,
BenchmarkRun, BenchmarkReport, BenchmarkKind, BenchmarkStatus, BenchmarkEvent, BenchmarkEventType)
from app.benchmarks import datasets, service
from app.errors import ApiError
INVALID = {'TOOL_NOT_FOUND', 'TOOL_NOT_ALLOWED', 'TOOL_ARGUMENT_INVALID', 'TOOL_VALIDATION_ERROR'}
def score(case, run, events, latency, repeat):
"""按工具选择、参数、结果、输出和引用要求评定单个样本。"""
calls = [e.data for e in events if e.event.value == 'ToolCall']
# 使用最大二分匹配,避免宽松的参数子集占用唯一能满足更严格预期的调用;
# 每个实际调用最多匹配一个预期调用。
matched = {}
def assign(expected_index, visited):
expected = case.expected_tools[expected_index]
for call_index, call in enumerate(calls):
if call_index in visited or call.get('name') != expected.name:
continue
arguments = call.get('arguments', {})
if not all(key in arguments and arguments[key] == value for key, value in expected.arguments.items()):
continue
visited.add(call_index)
if call_index not in matched or assign(matched[call_index], visited):
matched[call_index] = expected_index
return True
return False
accurate = sum(assign(index, set()) for index in range(len(case.expected_tools)))
from collections import Counter
actual_names = Counter(call.get('name') for call in calls)
expected_names = Counter(tool.name for tool in case.expected_tools)
selected = sum(min(count, actual_names[name]) for name, count in expected_names.items())
results = run.tool_results
checks = {
'completed': run.status.value == 'completed',
'tools_selected': selected == len(case.expected_tools),
'tool_arguments': accurate == len(case.expected_tools),
'no_extra_calls': len(calls) <= len(case.expected_tools),
'tool_results': all(r.success for r in results),
'output': all(text.casefold() in (run.output or '').casefold() for text in case.output_contains),
'citation': not case.citation_required or bool(run.citations),
'tasks_created': case.tasks_created is None or sum(r.success and r.name == 'tasks.create' for r in results) == case.tasks_created,
}
return AgentCaseResult(case_id=case.case_id, repeat=repeat, agent_run_id=run.run_id,
success=all(checks.values()), tool_calls=len(calls), expected_calls=len(case.expected_tools),
selected_calls=selected, accurate_calls=accurate, invalid_calls=sum(r.error_code in INVALID for r in results),
steps=run.current_step, latency_ms=latency, token_usage=run.token_usage, checks=checks, error_code=run.error_code)
def aggregate(cases, planned_total=None):
"""汇总已执行样本,并让取消后的未执行样本继续计入计划总数。"""
total = len(cases) if planned_total is None else planned_total
calls = sum(c.tool_calls for c in cases)
expected = sum(c.expected_calls for c in cases)
# 微平均同时惩罚遗漏和多余调用;完全没有调用要求时准确率记为不适用。
denominator = max(calls, expected)
return {'total_cases': total, 'evaluated_cases': len(cases), 'task_success_rate': sum(c.success for c in cases)/total if total else 0,
'tool_selection_accuracy': sum(c.selected_calls for c in cases)/denominator if denominator else None,
'tool_argument_accuracy': sum(c.accurate_calls for c in cases)/denominator if denominator else None,
'invalid_tool_call_rate': sum(c.invalid_calls for c in cases)/calls if calls else None,
'average_steps': sum(c.steps for c in cases)/total if total else 0,
'average_latency_ms': sum(c.latency_ms for c in cases)/total if total else 0,
'token_usage': sum(c.token_usage for c in cases), 'tool_calls': calls, 'expected_calls': expected}
async def create_run(request: AgentBenchmarkRequest):
"""冻结数据集与运行配置,并把评测交给后台真实 Agent Runtime。"""
from app.container import container
from app.providers.registry import ProviderNotFoundError
try:
provider = container.providers.get(request.provider_id)
except ProviderNotFoundError as exc:
raise ApiError(404, 'PROVIDER_NOT_FOUND', 'Provider not found or disabled.') from exc
is_mock = provider.config.provider_type.value == 'mock'
if request.offline and not is_mock:
raise ApiError(422, 'BENCHMARK_OFFLINE_PROVIDER_REQUIRED', 'Offline regression only accepts a mock provider.')
if is_mock and not request.offline:
raise ApiError(422, 'BENCHMARK_REAL_PROVIDER_REQUIRED', 'Select a real provider or explicitly mark offline regression.')
dataset = datasets.load_dataset(request.dataset_id, BenchmarkKind.agent)
if not service._evict_terminal():
raise ApiError(429, 'BENCHMARK_CAPACITY_EXCEEDED', 'Benchmark capacity exceeded.')
run_id = 'benchmark_' + uuid4().hex[:12]
snapshot = {**request.model_dump(), 'dataset_hash': dataset.content_hash,
'dataset_version': dataset.version, 'execution': 'offline' if request.offline else 'real_agent_runtime',
'provider_type': provider.config.provider_type, 'scoring_version': '1.0', 'permission_policy': 'runtime_user_decision'}
run = BenchmarkRun(run_id=run_id, kind=BenchmarkKind.agent, dataset_id=dataset.dataset_id,
dataset_hash=dataset.content_hash, status=BenchmarkStatus.queued, created_at=service._now(), config_snapshot=snapshot)
service._runs[run_id] = run
service._events[run_id] = []
service._subscribers[run_id] = []
service._cancel_flags[run_id] = asyncio.Event()
service._tasks[run_id] = asyncio.create_task(execute(run_id, request, dataset, container.agent))
return run
async def execute(run_id, request, dataset, runtime):
"""顺序执行样本,传播取消信号,并持续发布可订阅的运行事件。"""
flag = service._cancel_flags[run_id]
results = []; active = None
def emit(kind, data):
event = BenchmarkEvent(event=kind, run_id=run_id, sequence=len(service._events[run_id]), data=data, timestamp=service._now())
service._events[run_id].append(event)
for queue in service._subscribers.get(run_id, []): queue.put_nowait(event)
status = BenchmarkStatus.completed
error = None
try:
service._runs[run_id] = service._runs[run_id].model_copy(update={'status': BenchmarkStatus.running, 'started_at': service._now()})
emit(BenchmarkEventType.run_started, {'dataset_id': dataset.dataset_id})
for case in dataset.cases:
for repeat in range(request.repeat):
if flag.is_set():
status = BenchmarkStatus.cancelled; break
started = perf_counter()
active = await runtime.create_run(AgentRunCreateRequest(input=case.prompt, provider_id=request.provider_id,
model=request.model, allowed_tools=case.allowed_tools, max_steps=request.max_steps,
token_budget=request.token_budget, run_timeout_seconds=request.timeout_seconds,
tool_timeout_seconds=min(30, request.timeout_seconds), allow_network=request.allow_network,
metadata={'benchmark_run_id': run_id, 'case_id': case.case_id}))
# 样本仍在运行时就暴露真实 Trace 与权限入口,便于界面处理待决授权。
service._runs[run_id].config_snapshot['active_agent_run_id'] = active.run_id
wait = asyncio.create_task(runtime.wait(active.run_id))
cancel = asyncio.create_task(flag.wait())
try:
done, _ = await asyncio.wait([wait, cancel], return_when=asyncio.FIRST_COMPLETED)
if cancel in done:
await runtime.cancel(active.run_id)
status = BenchmarkStatus.cancelled
finished = await wait
finally:
cancel.cancel(); await asyncio.gather(cancel, return_exceptions=True)
events = [event async for event in runtime.events(active.run_id)]
result = score(case, finished, events, (perf_counter()-started)*1000, repeat)
results.append(result); active = None
service._runs[run_id].progress = len(results)/(len(dataset.cases)*request.repeat)
emit(BenchmarkEventType.case_completed, result.model_dump(mode='json'))
if status == BenchmarkStatus.cancelled: break
except asyncio.CancelledError:
status = BenchmarkStatus.cancelled
except Exception:
status = BenchmarkStatus.failed; error = 'BENCHMARK_RUN_FAILED'
finally:
if active:
await runtime.cancel(active.run_id)
await runtime.wait(active.run_id)
metrics = aggregate(results, len(dataset.cases)*request.repeat)
run = service._runs[run_id]
service._runs[run_id] = run.model_copy(update={'status':status, 'metrics':metrics, 'completed_at':service._now(), 'error_code':error})
service._reports[run_id] = BenchmarkReport(run_id=run_id, kind=BenchmarkKind.agent,
dataset_id=dataset.dataset_id, dataset_hash=dataset.content_hash, status=status,
config_snapshot=run.config_snapshot, cases=results, metrics=metrics, error_code=error)
emit({BenchmarkStatus.completed: BenchmarkEventType.run_completed, BenchmarkStatus.failed: BenchmarkEventType.run_failed,
BenchmarkStatus.cancelled: BenchmarkEventType.run_cancelled}[status], {'metrics':metrics, 'error_code':error})
service._cancel_flags.pop(run_id, None); service._subscribers.pop(run_id, None)
+14 -5
View File
@@ -17,20 +17,20 @@ from app.config import get_settings
from app.contracts import (
BenchmarkDatasetInfo,
BenchmarkKind,
RAGDatasetCase,
RAGDatasetCase, AgentDatasetCase,
)
from app.errors import ApiError
@dataclass
class RAGDataset:
"""内存中的 RAG 数据集:元信息 + 已校验的 Case 列表 + 内容哈希。"""
"""内存中的 RAG / Agent 数据集:元信息 + 已校验的 Case 列表 + 内容哈希。"""
dataset_id: str
kind: BenchmarkKind
version: str
description: str
cases: list[RAGDatasetCase] = field(default_factory=list)
cases: list[RAGDatasetCase | AgentDatasetCase] = field(default_factory=list)
content_hash: str = ""
@@ -104,10 +104,10 @@ def _dataset_from_raw(raw: dict, raw_bytes: bytes, kind: BenchmarkKind) -> RAGDa
{"dataset_id": dataset_id},
)
cases: list[RAGDatasetCase] = []
cases: list[RAGDatasetCase | AgentDatasetCase] = []
for index, case in enumerate(raw_cases):
try:
parsed = RAGDatasetCase.model_validate(case)
parsed = (AgentDatasetCase if kind == BenchmarkKind.agent else RAGDatasetCase).model_validate(case)
except ValidationError as exc:
raise ApiError(
422,
@@ -115,6 +115,13 @@ def _dataset_from_raw(raw: dict, raw_bytes: bytes, kind: BenchmarkKind) -> RAGDa
f"Dataset case #{index} is invalid.",
{"dataset_id": dataset_id, "case_index": index, "errors": exc.errors()},
) from exc
if kind == BenchmarkKind.agent:
if not (parsed.expected_tools or parsed.output_contains or parsed.citation_required or parsed.tasks_created is not None):
raise ApiError(422, 'BENCHMARK_DATASET_INVALID', 'Agent case requires objective expectations.')
if any(tool.name not in parsed.allowed_tools for tool in parsed.expected_tools):
raise ApiError(422, 'BENCHMARK_DATASET_INVALID', 'Expected tools must be allowed.')
cases.append(parsed)
continue
# 每个 Case 至少要声明一个期望 id,否则无法计算命中/召回
if not parsed.expected_note_ids and not parsed.expected_block_ids:
raise ApiError(
@@ -133,6 +140,8 @@ def _dataset_from_raw(raw: dict, raw_bytes: bytes, kind: BenchmarkKind) -> RAGDa
)
cases.append(parsed)
if len(cases) > 100 or len({c.case_id for c in cases}) != len(cases):
raise ApiError(422, 'BENCHMARK_DATASET_INVALID', 'Dataset case IDs must be unique; maximum 100 cases.')
return RAGDataset(
dataset_id=dataset_id,
kind=kind,
+1
View File
@@ -86,6 +86,7 @@ async def _evaluate_one(
limit=request.retrieval.top_k,
include_snippet=False,
rrf_k=request.retrieval.rrf_k,
fusion=request.retrieval.fusion,
rerank=request.retrieval.rerank,
rerank_candidates=request.retrieval.rerank_candidates,
score_threshold=request.retrieval.score_threshold,
+9
View File
@@ -352,3 +352,12 @@ async def wait_for_run(run_id: str) -> BenchmarkRun:
if task is not None:
await task
return _runs.get(run_id)
async def shutdown():
loop = asyncio.get_running_loop()
active = {rid: task for rid, task in _tasks.items() if not task.done() and task.get_loop() is loop}
for rid in active:
flag = _cancel_flags.get(rid)
if flag: flag.set()
await asyncio.gather(*active.values(), return_exceptions=True)
+78 -1
View File
@@ -155,6 +155,7 @@ class SearchRequest(Contract):
include_snippet: bool = True
# 检索调优参数(Benchmark 与 Skill 共用):控制 RRF / 精排 / 候选池 / 分数阈值。
# rerank_candidates=None 表示对全部候选精排(保留原有行为),Benchmark 传显式值。
fusion: Literal['rrf', 'weighted'] = 'rrf'
rrf_k: int = Field(default=60, ge=1)
rerank: bool = True
rerank_candidates: int | None = Field(default=None, ge=1)
@@ -1221,6 +1222,7 @@ class RAGRetrievalConfig(Contract):
其余参数透传到 SearchRequest,由检索引擎实际执行。"""
top_k: int = Field(default=10, ge=1, le=100)
fusion: Literal['rrf', 'weighted'] = 'rrf'
rrf_k: int = Field(default=60, ge=1)
rerank: bool = True
rerank_candidates: int = Field(default=20, ge=1)
@@ -1329,6 +1331,51 @@ class RAGCaseResult(Contract):
error_code: str | None = None
class ExpectedToolCall(Contract):
name: str = Field(min_length=1)
arguments: dict[str, Any] = Field(default_factory=dict)
class AgentDatasetCase(Contract):
case_id: str = Field(min_length=1)
prompt: str = Field(min_length=1, max_length=20000)
allowed_tools: list[str] = Field(default_factory=list, max_length=30)
expected_tools: list[ExpectedToolCall] = Field(default_factory=list, max_length=30)
output_contains: list[str] = Field(default_factory=list)
citation_required: bool = False
tasks_created: int | None = Field(default=None, ge=0, le=20)
tags: list[str] = Field(default_factory=list)
class AgentBenchmarkRequest(Contract):
dataset_id: str = Field(min_length=1)
provider_id: str
model: str = Field(min_length=1)
max_steps: int = Field(default=6, ge=1, le=20)
timeout_seconds: int = Field(default=90, ge=1, le=300)
token_budget: int = Field(default=6000, ge=1, le=30000)
repeat: int = Field(default=1, ge=1, le=3)
allow_network: bool = False
offline: bool = False
class AgentCaseResult(Contract):
case_id: str
repeat: int
agent_run_id: str | None = None
success: bool = False
tool_calls: int = 0
expected_calls: int = 0
selected_calls: int = 0
accurate_calls: int = 0
invalid_calls: int = 0
steps: int = 0
latency_ms: float = 0
token_usage: int = 0
checks: dict[str, bool] = Field(default_factory=dict)
error_code: str | None = None
class BenchmarkReport(Contract):
run_id: str
kind: BenchmarkKind
@@ -1337,7 +1384,7 @@ class BenchmarkReport(Contract):
status: BenchmarkStatus
config_snapshot: dict[str, Any] = Field(default_factory=dict)
metrics: dict[str, Any] = Field(default_factory=dict)
cases: list[RAGCaseResult] = Field(default_factory=list)
cases: list[RAGCaseResult | AgentCaseResult] = Field(default_factory=list)
error: str | None = None
error_code: str | None = None
@@ -1366,6 +1413,7 @@ class ExportSource(Contract):
"""导出源:note 引用已索引笔记,markdown 用于未保存预览(不持久化)。"""
type: ExportSourceType
file_path: str | None = Field(default=None, max_length=1024)
note_id: str | None = None
markdown: str | None = None
@@ -1378,7 +1426,18 @@ class ExportSource(Contract):
return self
class ExportPalette(Contract):
page: str = Field(pattern=r'^#[0-9a-fA-F]{6}$')
surface: str = Field(pattern=r'^#[0-9a-fA-F]{6}$')
text: str = Field(pattern=r'^#[0-9a-fA-F]{6}$')
muted: str = Field(pattern=r'^#[0-9a-fA-F]{6}$')
code: str = Field(pattern=r'^#[0-9a-fA-F]{6}$')
border: str = Field(pattern=r'^#[0-9a-fA-F]{6}$')
accent: str = Field(pattern=r'^#[0-9a-fA-F]{6}$')
class ExportOptions(Contract):
palette: ExportPalette | None = None
theme_id: str = "light"
include_title: bool = True
include_metadata: bool = False
@@ -1386,11 +1445,29 @@ class ExportOptions(Contract):
code_theme: str = "github-light"
class ExportAsset(Contract):
kind: Literal['mermaid', 'math_block', 'math_inline', 'image']
source_hash: str = Field(pattern=r'^[a-f0-9]{64}$')
png_base64: str
class ExportRequest(Contract):
print_html: str | None = None
assets: list[ExportAsset] = Field(default_factory=list)
title: str = Field(default="", max_length=200)
source: ExportSource
format: ExportFormat
options: ExportOptions = Field(default_factory=ExportOptions)
@model_validator(mode="after")
def _asset_limits(self) -> "ExportRequest":
if self.print_html is not None and self.format != ExportFormat.pdf:
raise ValueError("print_html is only supported for PDF")
if self.format != ExportFormat.pdf:
if len(self.assets) > 64 or any(len(asset.png_base64) > 2800000 for asset in self.assets):
raise ValueError("export asset count or size limit exceeded")
return self
class ExportProgress(Contract):
phase: str
+145
View File
@@ -0,0 +1,145 @@
"""处理栅格资源;PDF 不受导出配额限制,但仍执行路径和格式校验。"""
import base64
import hashlib
import threading
from io import BytesIO
from PIL import Image
from app.errors import ApiError
_math_lock = threading.Lock()
def enrich_document(document, file_path=None, unlimited=False, options=None, preserve_alpha=False):
"""内嵌 Vault 图片和 MathText,并按导出格式应用配额与主题配色。"""
from app.config import get_settings
from urllib.parse import unquote, urlsplit
vault = get_settings().vault_path.resolve()
base = (vault / (file_path or '')).parent if file_path else vault
from app.export.themes import pdf_palette
palette = pdf_palette(options, []) if unlimited and options else None
warnings = []
count = total = pixels = 0
def visit(node):
nonlocal count, total, pixels
if node.type in {'image','math_block','math_inline'} or node.attributes.get('static_png'):
count += 1
try:
if not unlimited and count > 64: raise ValueError('resource count')
if node.attributes.get('static_png'):
raw = node.attributes['static_png']
elif node.type == 'image':
src = str(node.attributes.get('src',''))
if urlsplit(src).scheme or src.startswith('//'): raise ValueError('remote image')
path = (base / unquote(src)).resolve()
if not path.is_relative_to(vault) or path.suffix.lower() not in {'.png','.jpg','.jpeg','.webp'} or (not unlimited and path.stat().st_size > 2_000_000):
raise ValueError('image path or budget')
raw = path.read_bytes()
else:
source = node.text
depth = 0
for char in source:
depth += (char == '{') - (char == '}')
if not unlimited and depth > 20: raise ValueError('math depth')
if (not unlimited and len(source) > 512) or depth != 0: raise ValueError('math budget')
from matplotlib.mathtext import math_to_image
from matplotlib import rc_context
with _math_lock, rc_context({'savefig.transparent': bool(palette)}):
out = BytesIO()
math_to_image('$'+source+'$', out, dpi=180, format='png', color=palette['text'] if palette else 'black')
raw = out.getvalue()
with Image.open(BytesIO(raw)) as image:
pixels += image.width * image.height
if not unlimited and pixels > 16_000_000: raise ValueError('document pixels')
if not unlimited and image.width * image.height > 4_000_000: raise ValueError('image dimensions')
out = BytesIO()
# 透明像素按 PDF 主题表面色合成;打印 HTML 与 Word 使用白色底色。
rgba=image.convert('RGBA'); background=Image.new('RGBA',rgba.size,palette['surface'] if palette else 'white')
background.alpha_composite(rgba); (rgba if preserve_alpha else background.convert('RGB')).save(out,'PNG')
png=out.getvalue();total += len(png)
if not unlimited and total > 8_000_000: raise ValueError('resource bytes')
node.attributes['static_png']=png
except Exception:
node.attributes.pop('static_png', None)
warnings.append('图片无法内嵌(仅支持 Vault 内 PNG/JPEG/WebP),已保留替代文字' if node.type=='image'
else '公式超出 MathText 语法或资源预算,已保留源码' if node.type.startswith('math')
else '静态图表超过文档资源预算,已保留源码')
for child in node.children: visit(child)
for child in document.children: visit(child)
return warnings
def source_hash(source):
return hashlib.sha256(source.strip().encode()).hexdigest()
def validate_assets(assets, unlimited=False):
"""校验前端静态资源并解码为 PNG;PDF 仅解除容量限制,不放宽格式要求。"""
result = {}
total = pixels = 0
for asset in assets:
try:
raw = base64.b64decode(asset.png_base64, validate=True)
total += len(raw)
if not unlimited and total > 8 * 1024 * 1024:
raise ValueError('asset budget')
with Image.open(BytesIO(raw)) as image:
pixels += image.width * image.height
if not unlimited and pixels > 16_000_000: raise ValueError('document pixel budget')
if image.format != 'PNG' or (not unlimited and image.width * image.height > 4_000_000):
raise ValueError('image budget')
image.load()
out = BytesIO()
rgba = image.convert('RGBA')
background = Image.new('RGBA', rgba.size, 'white')
background.alpha_composite(rgba)
(rgba if unlimited else background.convert('RGB')).save(out, 'PNG')
key = (asset.kind, asset.source_hash)
if key in result:
raise ValueError('duplicate asset')
result[key] = out.getvalue()
except Exception as exc:
raise ApiError(422, 'EXPORT_ASSET_INVALID', 'Invalid PNG or resource budget exceeded.') from exc
return result
def attach_assets(document, assets):
"""按资源类型和源码哈希把已验证图片挂载到对应文档节点。"""
def visit(node):
source = node.attributes.get('src', '') if node.type == 'image' else node.text
key = (node.type, source_hash(source))
if key in assets:
node.attributes['static_png'] = assets[key]
for child in node.children:
visit(child)
for child in document.children:
visit(child)
def plot_png(plot):
"""按 SVG/PDF 共用的裁剪几何,以二倍分辨率生成 DOCX 图像。"""
from app.plot.render import compute_geometry, _sx, _sy, _fmt_num
from PIL import ImageDraw, ImageFont
geo = compute_geometry(plot)
image = Image.new('RGB', (geo.width * 2, (geo.height + ((len(plot.expressions)+1)//2)*24) * 2), 'white')
draw = ImageDraw.Draw(image)
from app.export.fonts import FONT_PATH
font = ImageFont.truetype(str(FONT_PATH), 20) if FONT_PATH else ImageFont.load_default(size=20)
def line(points, color, width=2):
draw.line([(x * 2, y * 2) for x, y in points], fill=color, width=width)
sx = lambda x: _sx(x, geo.xmin, geo.xmax)
sy = lambda y: _sy(y, geo.ymin, geo.ymax)
for x in geo.xticks:
if geo.grid: line([(sx(x),52),(sx(x),428)], '#d0d7de')
draw.text((sx(x)*2, sy(geo.x_axis_y)*2+8), _fmt_num(x), fill='#57606a', font=font)
for y in geo.yticks:
if geo.grid: line([(52,sy(y)),(588,sy(y))], '#d0d7de')
draw.text((max(0,sx(geo.y_axis_x)*2-75),sy(y)*2), _fmt_num(y), fill='#57606a', font=font)
line([(52,sy(geo.x_axis_y)),(588,sy(geo.x_axis_y))], '#57606a')
line([(sx(geo.y_axis_x),52),(sx(geo.y_axis_x),428)], '#57606a')
for segments, color in zip(geo.polylines,geo.colors):
for segment in segments:
if len(segment)>1: line(segment,color,3)
if geo.xlabel:
draw.text((geo.width, (geo.height - 18)*2), geo.xlabel, fill='#1f2328', font=font, anchor='mm')
if geo.ylabel:
# 纵轴标题横排在左上边距,避免 CJK 文本在 Word 中旋转后不可读。
draw.text((24, 24), geo.ylabel, fill='#1f2328', font=font)
for index, expression in enumerate(plot.expressions):
draw.text((48+(index%2)*620,geo.height*2+index//2*48),expression.label or 'y = '+expression.expression,fill=geo.colors[index],font=font)
out=BytesIO(); image.save(out,'PNG')
return out.getvalue(), geo.warnings
+66
View File
@@ -0,0 +1,66 @@
"""使用真实浏览器引擎打印应用生成的自包含主题快照。
子进程隔离 Playwright 在 Windows 上的事件循环与 Uvicorn,并把浏览器生命周期限制在
单次导出内。快照禁止脚本、网络和文件加载,字体与图片必须由客户端提前内嵌。
"""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
from app.export.document import ExportResult
def browser_executable():
"""优先使用显式配置,再查找系统已安装的 Chromium 系浏览器。"""
configured = os.environ.get('APP_PDF_BROWSER')
if configured:
return configured
for root in (os.environ.get('PROGRAMFILES(X86)', ''), os.environ.get('PROGRAMFILES', ''), os.environ.get('LOCALAPPDATA', '')):
if not root:
continue
for suffix in ('Microsoft/Edge/Application/msedge.exe', 'Google/Chrome/Application/chrome.exe'):
candidate = Path(root) / suffix
if candidate.is_file():
return str(candidate)
return next((p for name in ('chromium','chromium-browser','google-chrome','microsoft-edge') if (p := shutil.which(name))), None)
def render_snapshot(snapshot: str, page_size: str) -> ExportResult:
"""在隔离子进程中打印快照,避免阻塞或污染服务进程的事件循环。"""
with tempfile.TemporaryDirectory(prefix='notes-pdf-') as directory:
source = Path(directory) / 'snapshot.html'
output = Path(directory) / 'document.pdf'
source.write_text(snapshot, encoding='utf-8')
process = subprocess.run([sys.executable, '-m', 'app.export.browser_pdf', str(source), str(output), page_size],
capture_output=True, text=True, encoding='utf-8', errors='replace',
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0),
cwd=Path(__file__).resolve().parents[2])
if process.returncode:
raise RuntimeError('PDF browser rendering failed: ' + process.stderr[-2000:])
return ExportResult(content=output.read_bytes(), mime_type='application/pdf', warnings=[])
def print_snapshot(source: Path, output: Path, page_size: str):
"""在离线、禁用 JavaScript 的上下文中将自包含 HTML 打印为 PDF。"""
from playwright.sync_api import sync_playwright
with sync_playwright() as runtime:
browser = runtime.chromium.launch(executable_path=browser_executable(), headless=True)
try:
context = browser.new_context(java_script_enabled=False, offline=True)
context.route('**/*', lambda route: route.abort())
page = context.new_page()
page.set_default_timeout(0)
page.emulate_media(media='screen')
csp = "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'"
page.set_content('<meta http-equiv="Content-Security-Policy" content="'+csp+'">'+source.read_text(encoding='utf-8'), wait_until='load', timeout=0)
page.evaluate('async () => { await document.fonts.ready; await Promise.all([...document.images].map(image => image.decode().catch(() => {}))); }')
page.pdf(path=str(output), format='Letter' if page_size.lower()=='letter' else 'A4',
print_background=True, display_header_footer=False, prefer_css_page_size=False)
finally:
browser.close()
if __name__ == '__main__':
print_snapshot(Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3])
+2 -3
View File
@@ -1,7 +1,6 @@
"""导出器共享工具:URL 协议校验、函数图像预算与占位 warning 文案。
html / pdf / docx 三个导出器共用同一套安全规则与函数图像资源预算,避免各写一份
导致行为漂移。
导出器共享 URL 规则;HTML / DOCX 使用文档资源预算,PDF 不使用这些预算。
"""
from __future__ import annotations
@@ -27,7 +26,7 @@ MAX_TOTAL_PLOT_NODES = 8000
class FunctionPlotBudget:
"""函数图像文档级资源预算:数量上限 + 累计 AST 节点上限。
HTML 与 PDF 导出器在渲染每个 function-plot 图块前先问预算,超限即回退源码占位,
HTML 与 DOCX 导出器在渲染每个 function-plot 图块前先问预算,超限即回退源码占位,
不解析不采样,避免多图块组合复杂度耗尽内存/CPU。
"""
+37 -3
View File
@@ -1,7 +1,7 @@
"""DocxExporterDocument AST → DOCXpython-docx)。
v1 为文本优先:标题/段落/行内强调与链接/列表/引用/表格/代码块/数学文本均可导出;
function_plot 与 mermaid 保留源码占位并记 warning。中文字体通过 Normal 样式挂载
标题段落、列表、表格等使用原生 Word 元素;函数图、已准备的 Mermaid、
受支持的公式与 Vault 图片使用静态图片,无法表示的资源保留源码并记 warning。中文字体通过 Normal 样式挂载
w:eastAsia=宋体,保证 Word 打开时中文正常显示;bold/italic 由 Word 原生渲染。
"""
@@ -50,6 +50,8 @@ class DocxExporter:
def render(self, document: Document, options: ExportOptions) -> ExportResult:
"""同步渲染;CPU 密集,调用方应放入线程执行,避免阻塞事件循环。"""
from app.export.exporters._common import FunctionPlotBudget
self._plot_budget = FunctionPlotBudget()
self._doc = DocxDocument()
self._configure_normal_style()
self._configure_page(options)
@@ -109,6 +111,19 @@ class DocxExporter:
self._render_block(child, warnings)
def _render_block(self, node: DocumentNode, warnings: list[str]) -> None:
if node.attributes.get('static_png'):
from PIL import Image
png = node.attributes['static_png']
with Image.open(BytesIO(png)) as image:
section = self._doc.sections[-1]
available_width = (section.page_width - section.left_margin - section.right_margin) / 914400
# 为 Word 外层段落的行高和间距预留空间,避免图片跨出页面。
available_height = (section.page_height - section.top_margin - section.bottom_margin) / 914400 - 0.25
width = min(5.8, available_width,
image.width / (180 if node.type == 'math_block' else 96),
available_height * image.width / image.height)
self._doc.add_picture(BytesIO(png), width=Inches(width))
return
handler = getattr(self, f"_block_{node.type}", None)
if handler is not None:
handler(node, warnings)
@@ -270,7 +285,20 @@ class DocxExporter:
self._block_code_block(node, warnings)
def _block_function_plot(self, node: DocumentNode, warnings: list[str]) -> None:
warnings.append(PLOT_PLACEHOLDER_WARNING)
from app.plot.parser import parse_source
from app.export.assets import plot_png
over = self._plot_budget.check_count()
if not over:
parsed = parse_source(node.text)
warnings.extend(d.message for d in parsed.diagnostics)
if parsed.plot:
over = self._plot_budget.check_nodes(parsed.plot.node_count)
if not over:
png, messages = plot_png(parsed.plot)
warnings.extend(messages)
self._doc.add_picture(BytesIO(png), width=Inches(5.8))
return
warnings.append(over or '函数图像无法绘制,已保留源码')
self._block_code_block(node, warnings)
def _block_math_block(self, node: DocumentNode, warnings: list[str]) -> None:
@@ -303,6 +331,12 @@ class DocxExporter:
bold: bool = False,
italic: bool = False,
) -> None:
if node.attributes.get('static_png'):
from PIL import Image
with Image.open(BytesIO(node.attributes['static_png'])) as image:
width = min(5.8, image.width / (180 if node.type.startswith('math') else 96))
paragraph.add_run().add_picture(BytesIO(node.attributes['static_png']), width=Inches(width))
return
t = node.type
if t == "text":
self._add_run(paragraph, node.text, bold=bold, italic=italic)
+12
View File
@@ -151,6 +151,16 @@ class HtmlExporter:
return "".join(self._render_node(child, warnings) for child in children)
def _render_node(self, node: DocumentNode, warnings: list[str]) -> str:
if node.attributes.get('static_png'):
import base64
data = base64.b64encode(node.attributes['static_png']).decode()
from PIL import Image
from io import BytesIO
width = ''
if node.type.startswith('math'):
with Image.open(BytesIO(node.attributes['static_png'])) as image:
width = f'width:{image.width*96/180:.1f}px;vertical-align:middle;'
return f'<img alt="{html.escape(node.text or node.type)}" src="data:image/png;base64,{data}" style="{width}max-width:100%">'
handler = getattr(self, f"_render_{node.type}", None)
if handler is not None:
return handler(node, warnings)
@@ -257,6 +267,8 @@ class HtmlExporter:
warnings.append(f"函数图像:解析或渲染失败,已回退占位({exc}")
return f'<pre class="function-plot">{html.escape(node.text)}</pre>'
warnings.extend(rendered.warnings)
from app.plot.render import theme_svg
rendered.content = theme_svg(rendered.content, self._options.theme_id)
return f'<figure class="function-plot">{rendered.content}</figure>'
def _render_math_block(self, node: DocumentNode, warnings: list[str]) -> str:
+59 -41
View File
@@ -20,7 +20,7 @@ from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.platypus import (
Paragraph,
Indenter,
Preformatted,
XPreformatted,
SimpleDocTemplate,
Spacer,
Table,
@@ -29,12 +29,11 @@ from reportlab.platypus import (
from reportlab.platypus.flowables import HRFlowable
from app.contracts import ExportOptions
from app.export.themes import CALLOUTS, print_theme_warning
from app.export.themes import CALLOUTS, pdf_palette
from app.export.document import Document, DocumentNode, ExportResult
from app.export.exporters._common import (
MERMAID_WARNING,
RAW_HTML_WARNING,
FunctionPlotBudget,
format_meta_value,
format_plot_diagnostic,
safe_url,
@@ -42,8 +41,7 @@ from app.export.exporters._common import (
from app.plot.render_reportlab import render_drawing
from app.plot.renderer import FunctionPlotStaticRenderer, StaticRenderRequest
_FONT = "STSong-Light"
pdfmetrics.registerFont(UnicodeCIDFont(_FONT))
from app.export.fonts import FONT as _FONT
_MIME = "application/pdf"
@@ -55,10 +53,11 @@ _HEADING_SIZES = {1: 20, 2: 16, 3: 14, 4: 12, 5: 11, 6: 10.5}
_QUOTE_COLOR = "#57606a"
def _make_styles() -> dict[str, ParagraphStyle]:
def _make_styles(palette) -> dict[str, ParagraphStyle]:
body = ParagraphStyle(
"pdf-body",
fontName=_FONT,
textColor=palette["text"],
fontSize=10.5,
leading=16,
spaceAfter=6,
@@ -68,7 +67,7 @@ def _make_styles() -> dict[str, ParagraphStyle]:
"pdf-quote",
parent=body,
leftIndent=14,
textColor="#57606a",
textColor=palette["muted"],
spaceBefore=4,
spaceAfter=6,
)
@@ -79,8 +78,8 @@ def _make_styles() -> dict[str, ParagraphStyle]:
leading=12,
leftIndent=6,
rightIndent=6,
backColor="#f6f8fa",
borderColor="#d0d7de",
backColor=palette["code"],
borderColor=palette["border"],
borderWidth=0.5,
borderPadding=6,
spaceBefore=4,
@@ -89,9 +88,9 @@ def _make_styles() -> dict[str, ParagraphStyle]:
math = ParagraphStyle("pdf-math", parent=body, alignment=TA_CENTER, spaceBefore=6)
cell = ParagraphStyle("pdf-cell", parent=body, fontSize=10, leading=14, spaceAfter=0)
cell_head = ParagraphStyle(
"pdf-cell-head", parent=cell, textColor="#1f2328", fontSize=10
"pdf-cell-head", parent=cell, textColor=palette["text"], fontSize=10
)
meta = ParagraphStyle("pdf-meta", parent=body, fontSize=8.5, leading=13, textColor="#57606a")
meta = ParagraphStyle("pdf-meta", parent=body, fontSize=8.5, leading=13, textColor=palette["muted"])
styles: dict[str, ParagraphStyle] = {
"body": body,
"title": title,
@@ -110,6 +109,7 @@ def _make_styles() -> dict[str, ParagraphStyle]:
leading=size * 1.4,
spaceBefore=14 if level <= 2 else 10,
spaceAfter=6,
keepWithNext=True,
)
return styles
@@ -119,16 +119,17 @@ class PdfExporter:
def render(self, document: Document, options: ExportOptions) -> ExportResult:
"""同步渲染;CPU 密集,调用方应放入线程执行,避免阻塞事件循环。"""
self._styles = _make_styles()
warnings: list[str] = []
print_theme_warning(options, warnings, "PDF")
self._palette = pdf_palette(options, warnings)
self._styles = _make_styles(self._palette)
if _FONT == "STSong-Light": warnings.append("PDF 使用 CID 字体,阅读器需提供中文字体;可配置 APP_EXPORT_FONT 嵌入 TrueType 字体")
page = _PAGE_SIZES.get((options.page_size or "A4").lower(), A4)
self._options = options
self._plot_budget = FunctionPlotBudget()
self._plot_renderer = FunctionPlotStaticRenderer()
# 内容区宽度(左右各 20mm 边距),供函数图像缩放适配页面
self._plot_width = page[0] - 40 * mm
self._plot_width = page[0] - 40 * mm - 12
self._plot_height = page[1] - 36 * mm - 12
buf = BytesIO()
doc = SimpleDocTemplate(
buf,
@@ -144,7 +145,14 @@ class PdfExporter:
self._render_header(document, options, story)
self._render_children(document.children, story, warnings)
doc.build(story)
def paint_page(canvas, template):
canvas.saveState()
canvas.setFillColor(self._palette['page'])
canvas.rect(0, 0, page[0], page[1], fill=1, stroke=0)
canvas.setFillColor(self._palette['surface'])
canvas.roundRect(12*mm, 10*mm, page[0]-24*mm, page[1]-20*mm, 5*mm, fill=1, stroke=0)
canvas.restoreState()
doc.build(story, onFirstPage=paint_page, onLaterPages=paint_page)
return ExportResult(content=buf.getvalue(), mime_type=_MIME, warnings=warnings)
async def export(self, document: Document, options: ExportOptions) -> ExportResult:
@@ -169,6 +177,14 @@ class PdfExporter:
self._render_block(child, story, warnings)
def _render_block(self, node: DocumentNode, story: list, warnings: list[str]) -> None:
if node.attributes.get('static_png'):
from reportlab.platypus import Image
image = Image(BytesIO(node.attributes['static_png']))
scale = min(1, self._plot_width / image.imageWidth, self._plot_height / image.imageHeight)
image.drawWidth = image.imageWidth * scale
image.drawHeight = image.imageHeight * scale
story.append(image)
return
handler = getattr(self, f"_block_{node.type}", None)
if handler is not None:
handler(node, story, warnings)
@@ -186,9 +202,13 @@ class PdfExporter:
def _block_callout(self, node, story, warnings):
kind = node.attributes['kind']
icon, color = CALLOUTS[kind]
from reportlab.lib.colors import HexColor
background = HexColor(self._palette['code'])
if .2126*background.red + .7152*background.green + .0722*background.blue < .5:
color = {'#0969da':'#a5d6ff','#7041a0':'#d2a8ff','#176f41':'#7ee787','#805400':'#f2cc60','#b42318':'#ffa198','#57606a':self._palette['muted']}[color]
title = self._render_inline(node.children[0].children,warnings)
style = ParagraphStyle('callout-'+kind,parent=self._styles['body'],textColor=color,
backColor='#f6f8fa',borderColor=color,borderWidth=1,borderPadding=6,spaceBefore=8,spaceAfter=8)
backColor=self._palette['code'],borderColor=color,borderWidth=1,borderPadding=6,spaceBefore=8,spaceAfter=8)
story.append(Paragraph(_html.escape(icon)+' '+title,style))
self._render_children(node.children[1:],story,warnings)
@@ -201,7 +221,7 @@ class PdfExporter:
Paragraph(self._render_inline(child.children, warnings), self._styles["quote"])
)
elif child.type == "list":
self._block_list(child, story, warnings, indent=14, color=_QUOTE_COLOR)
self._block_list(child, story, warnings, indent=14, color=self._palette['muted'])
else:
self._render_block(child, story, warnings)
@@ -265,7 +285,7 @@ class PdfExporter:
parts.append(self._render_inline(child.children, warnings))
elif hasattr(self, f"_block_{child.type}"):
flush()
# Keep block content inside the list frame, including tables and callouts.
# 表格、警告框等块级内容也要保持在列表缩进框内。
story.append(Indenter(left=indent))
self._render_block(child, story, warnings)
story.append(Indenter(left=-indent))
@@ -293,7 +313,7 @@ class PdfExporter:
data.append(cells)
table = Table(data, repeatRows=head_row_count)
commands = [
("GRID", (0, 0), (-1, -1), 0.5, "#d0d7de"),
("GRID", (0, 0), (-1, -1), 0.5, self._palette["border"]),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
@@ -301,53 +321,42 @@ class PdfExporter:
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
if head_row_count:
commands.append(("BACKGROUND", (0, 0), (-1, head_row_count - 1), "#f6f8fa"))
commands.append(("BACKGROUND", (0, 0), (-1, head_row_count - 1), self._palette["code"]))
table.setStyle(TableStyle(commands))
story.append(table)
def _block_code_block(self, node: DocumentNode, story: list, warnings: list[str]) -> None:
story.append(Preformatted(node.text, self._styles["code"]))
story.append(XPreformatted(_html.escape(node.text), self._styles["code"]))
def _block_thematic_break(self, node: DocumentNode, story: list, warnings: list[str]) -> None:
story.append(Spacer(1, 4))
story.append(HRFlowable(width="100%", color="#d0d7de", thickness=0.5))
story.append(HRFlowable(width="100%", color=self._palette["border"], thickness=0.5))
story.append(Spacer(1, 6))
def _block_mermaid(self, node: DocumentNode, story: list, warnings: list[str]) -> None:
warnings.append(MERMAID_WARNING)
story.append(Preformatted(node.text, self._styles["code"]))
story.append(XPreformatted(_html.escape(node.text), self._styles["code"]))
def _block_function_plot(self, node: DocumentNode, story: list, warnings: list[str]) -> None:
# 文档级数量上限:超出部分直接回退占位,不解析不采样,防止海量图像耗尽资源
over = self._plot_budget.check_count()
if over is not None:
warnings.append(over)
story.append(Preformatted(node.text, self._styles["code"]))
return
# 解析与渲染共同纳入局部异常回退:单个图像失败只回退占位 + warning,
# 绝不阻断整篇导出(含复杂表达式触发的 RecursionError 等异常)。
try:
request = StaticRenderRequest(
kind="function_plot", source=node.text, theme=self._options.theme_id
)
parsed = self._plot_renderer.parse(request)
from app.plot.parser import parse_source
parsed = parse_source(request.source, unlimited=True)
for diag in parsed.diagnostics:
warnings.append(format_plot_diagnostic(diag))
if parsed.plot is None:
story.append(Preformatted(node.text, self._styles["code"]))
return
# 文档级累计复杂度预算:超出后回退占位,不再采样求值
over = self._plot_budget.check_nodes(parsed.plot.node_count)
if over is not None:
warnings.append(over)
story.append(Preformatted(node.text, self._styles["code"]))
story.append(XPreformatted(_html.escape(node.text), self._styles["code"]))
return
# Drawing 本身即 Flowable,缩放后追加到 story,与 HTML 视觉一致
drawing = render_drawing(parsed.plot, width=self._plot_width)
drawing = render_drawing(parsed.plot, width=self._plot_width, palette=self._palette, unlimited=True, max_height=self._plot_height)
story.append(drawing)
except Exception as exc:
warnings.append(f"函数图像:解析或渲染失败,已回退占位({exc}")
story.append(Preformatted(node.text, self._styles["code"]))
story.append(XPreformatted(_html.escape(node.text), self._styles["code"]))
def _block_math_block(self, node: DocumentNode, story: list, warnings: list[str]) -> None:
story.append(Paragraph(f"$${_html.escape(node.text)}$$", self._styles["math"]))
@@ -362,6 +371,15 @@ class PdfExporter:
return "".join(self._render_inline_node(child, warnings) for child in children)
def _render_inline_node(self, node: DocumentNode, warnings: list[str]) -> str:
if node.attributes.get('static_png'):
import base64
from PIL import Image as PILImage
raw = node.attributes['static_png']
with PILImage.open(BytesIO(raw)) as image:
scale = min(.4 if node.type.startswith('math') else 1, 350/image.width, 160/image.height)
width, height = image.width*scale, image.height*scale
data = base64.b64encode(raw).decode()
return f'<img src="data:image/png;base64,{data}" width="{width}" height="{height}" valign="middle"/>'
t = node.type
if t == "text":
return _html.escape(node.text)
@@ -376,7 +394,7 @@ class PdfExporter:
if safe_href is None:
warnings.append(f"链接协议不安全,已降级为纯文本:{href!r}")
return inner
return f'<a href="{_html.escape(safe_href)}">{inner}</a>'
return f'<a href="{_html.escape(safe_href)}" color="{self._palette["accent"]}">{inner}</a>'
if t == "image":
src = str(node.attributes.get("src") or "")
alt = str(node.attributes.get("alt") or "")
+23
View File
@@ -0,0 +1,23 @@
"""嵌入可用的 CJK TrueType 字体,找不到时保留可移植的 CID 字体回退。"""
import os
from pathlib import Path
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
def register_font():
"""按显式配置、系统字体、Linux 字体的顺序注册 PDF 中文字体。"""
candidates = [os.getenv('APP_EXPORT_FONT',''),
str(Path(os.getenv('WINDIR','C:/Windows'))/'Fonts/simsun.ttc'),
'/usr/share/fonts/truetype/arphic/uming.ttc']
for candidate in candidates:
if candidate and Path(candidate).is_file():
try:
pdfmetrics.registerFont(TTFont('NotesExportCJK',candidate,subfontIndex=0))
return 'NotesExportCJK', Path(candidate)
except Exception:
continue
pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light'))
return 'STSong-Light', None
FONT, FONT_PATH = register_font()
+3
View File
@@ -178,6 +178,9 @@ class _AstMapper:
type="link", node_id=self.next_id(), attributes=attributes,
children=self.map_inline(token.get("children", [])),
)
if kind == "inline_html":
# 保留行内 HTML 的来源标记,仅供 PDF 资源扫描识别 img;最终 HTML 仍由前端净化。
return DocumentNode(type="text", node_id=self.next_id(), text=token.get("raw", ""), attributes={"raw_html": True})
if kind == "codespan":
return DocumentNode(type="codespan", node_id=self.next_id(), text=token.get("raw", ""))
if kind == "image":
+60 -7
View File
@@ -146,7 +146,7 @@ def _evict_terminal() -> bool:
return True
async def _resolve_source(source: ExportSource) -> tuple[str, str, dict | None]:
async def _resolve_source(source: ExportSource, unlimited: bool = False) -> tuple[str, str, dict | None]:
"""把导出源解析为 (markdown, title, metadata)metadata 仅 note 源提供。"""
if source.type == ExportSourceType.note:
note = await note_service.get_note(source.note_id)
@@ -157,7 +157,7 @@ async def _resolve_source(source: ExportSource) -> tuple[str, str, dict | None]:
"note not found",
{"note_id": source.note_id},
)
if len(note.markdown) > MAX_MARKDOWN_CHARS:
if not unlimited and len(note.markdown) > MAX_MARKDOWN_CHARS:
raise ApiError(
400,
"EXPORT_OPTIONS_INVALID",
@@ -175,19 +175,22 @@ async def _resolve_source(source: ExportSource) -> tuple[str, str, dict | None]:
markdown = source.markdown or ""
if not markdown.strip():
raise ApiError(400, "EXPORT_OPTIONS_INVALID", "markdown source must not be empty")
if len(markdown) > MAX_MARKDOWN_CHARS:
if not unlimited and len(markdown) > MAX_MARKDOWN_CHARS:
raise ApiError(
400,
"EXPORT_OPTIONS_INVALID",
f"markdown source exceeds {MAX_MARKDOWN_CHARS} characters",
{"size": len(markdown), "limit": MAX_MARKDOWN_CHARS},
)
return markdown, "", None
return markdown, "", {"file_path": source.file_path} if source.file_path else None
async def create_export(request: ExportRequest) -> ExportJob:
"""创建导出任务,立即返回 queued 的 ExportJob,由后台 Task 渲染。"""
markdown, title, metadata = await _resolve_source(request.source)
markdown, title, metadata = await _resolve_source(request.source, request.format == ExportFormat.pdf)
title = request.title or title
from app.export.assets import validate_assets
assets = await asyncio.to_thread(validate_assets, request.assets, request.format == ExportFormat.pdf)
if not _evict_terminal():
raise ApiError(
@@ -207,7 +210,7 @@ async def create_export(request: ExportRequest) -> ExportJob:
_jobs[job_id] = job
_cancel_flags[job_id] = asyncio.Event()
_tasks[job_id] = asyncio.create_task(
_execute(job_id, request.format, markdown, title, metadata, request.options)
_execute(job_id, request.format, markdown, title, metadata, request.options, assets, request.print_html)
)
return job
@@ -245,6 +248,8 @@ async def _execute(
title: str,
metadata: dict | None,
options: ExportOptions,
assets: dict | None = None,
print_html: str | None = None,
) -> None:
"""后台渲染:排队 → 解析 → 导出 → 写文件 → 挂载产物元信息。"""
cancel_event = _cancel_flags[job_id]
@@ -271,15 +276,24 @@ async def _execute(
# 解析与渲染都是 CPU 密集的同步工作,放入线程执行避免阻塞事件循环,
# 使运行中的取消能在渲染边界生效;写文件前再次检查取消。
if format == ExportFormat.pdf and print_html is not None:
from app.export.browser_pdf import render_snapshot
result = await asyncio.to_thread(render_snapshot, print_html, options.page_size)
else:
document = await asyncio.to_thread(parse_document, markdown)
document.attributes["title"] = title
from app.export.assets import attach_assets
attach_assets(document, assets or {})
if metadata:
document.attributes["metadata"] = metadata
from app.export.assets import enrich_document
resource_warnings = await asyncio.to_thread(enrich_document, document, (metadata or {}).get('file_path'), format == ExportFormat.pdf, options)
result = await asyncio.to_thread(_render_document, document, options, format)
result.warnings[:0] = resource_warnings
if cancel_event.is_set():
raise ExportCancelled()
if len(result.content) > MAX_EXPORT_BYTES:
if format != ExportFormat.pdf and len(result.content) > MAX_EXPORT_BYTES:
raise ExportTooLarge()
ext = _extension_for(format)
@@ -389,3 +403,42 @@ async def wait_for_export(job_id: str) -> ExportJob | None:
if task is not None:
await task
return _jobs.get(job_id)
async def preview_resources(request: ExportRequest):
"""为浏览器渲染器准备通过 Vault 校验的图片和静态函数图。"""
import base64
from app.export.assets import enrich_document
from app.plot.parser import parse_source
from app.plot.render import render_svg
from app.export.document import Document, DocumentNode
from html.parser import HTMLParser
markdown, _, metadata = await _resolve_source(request.source, True)
def prepare():
document = parse_document(markdown)
images, plots = [], []
class HtmlImages(HTMLParser):
# 原始 HTML 只提取 img.src;路径、扩展名和图片格式仍交给 enrich_document 校验。
# 行内代码和代码块在 AST 中不是 HTML 节点,因此不会误当作图片资源。
def handle_starttag(self, tag, attrs):
if tag == 'img':
src = dict(attrs).get('src')
if src:
visit(DocumentNode(type='image', node_id='html-image', attributes={'src':src}))
def visit(node):
if node.type == 'html_block' or node.attributes.get('raw_html'):
parser = HtmlImages(convert_charrefs=True)
parser.feed(node.text)
parser.close()
if node.type == 'image':
warnings = enrich_document(Document(node_id='pdf-resources', children=[node]), (metadata or {}).get('file_path'), True, request.options, preserve_alpha=True)
raw = node.attributes.get('static_png')
images.append({'source': node.attributes.get('src',''), 'data': 'data:image/png;base64,'+base64.b64encode(raw).decode() if raw else None, 'warnings': warnings})
if node.type == 'function_plot':
parsed = parse_source(node.text, unlimited=True)
result = render_svg(parsed.plot, request.options.theme_id, unlimited=True) if parsed.plot else None
plots.append({'source':node.text, 'svg':result.content if result else '', 'warnings':[d.message for d in parsed.diagnostics]+(result.warnings if result else [])})
for child in node.children: visit(child)
for child in document.children: visit(child)
return {'images':images,'plots':plots}
return await asyncio.to_thread(prepare)
+11
View File
@@ -1,5 +1,6 @@
"""Export palettes are fixed data; arbitrary theme CSS is never executed."""
PALETTES = {
'ocean-blue': ('#edf5fa','#ffffff','#183a50','#46667a','#e6f1f8','#a6c5d9','#086b9c'),
'light': ('#f6f7f9','#ffffff','#1f2328','#57606a','#eaeef2','#d0d7de','#0969da'),
'dark': ('#010409','#0d1117','#e6edf3','#b1bac4','#21262d','#57606a','#79c0ff'),
'sepia': ('#eee5d2','#faf4e6','#463b2d','#6b5943','#eae0cd','#b5a58b','#80532a'),
@@ -31,3 +32,13 @@ ALIASES = {'summary':'abstract','tldr':'abstract','hint':'tip',
'check':'success','done':'success','help':'question','faq':'question',
'caution':'warning','attention':'warning','fail':'failure','missing':'failure',
'error':'danger','cite':'quote'}
def pdf_palette(options, warnings):
if options.palette is not None:
return options.palette.model_dump()
theme_id = options.theme_id
if theme_id not in PALETTES:
warnings.append(f'PDF 不支持主题 {theme_id},已使用 light 导出配色')
theme_id = 'light'
return dict(zip(('page','surface','text','muted','code','border','accent'), PALETTES[theme_id]))
+23 -1
View File
@@ -5,6 +5,8 @@ import asyncio
import json
import os
import time
import hashlib
from collections import OrderedDict
from contextlib import closing
from contextvars import ContextVar
from functools import wraps
@@ -239,6 +241,11 @@ class Runtime:
runtime = Runtime()
# 对确定性的单文本本地向量做有界内存复用。键包含模型目录、不可变版本和冻结运行配置;
# 远程 API 响应以及模型不可用时的回退结果都不进入缓存。
_embedding_cache = OrderedDict()
_EMBEDDING_CACHE_TTL = 600
class LocalEmbedding:
dim = 384
@@ -264,9 +271,24 @@ class LocalEmbedding:
async def embed_documents(self, texts):
config = (self._config or configuration()).model_copy(deep=True)
from app.retrieval.provenance import record_embedding
cache_key = None
if len(texts) == 1 and read_state(config.embedding_model)['status'] == 'installed' and interpreter(config).is_file():
cache_key = (str(model_path(config.embedding_model).resolve()), config.model_dump_json(),
hashlib.sha256(texts[0].encode()).hexdigest())
cached = _embedding_cache.get(cache_key)
if cached and time.monotonic() - cached[0] < _EMBEDDING_CACHE_TTL:
_embedding_cache.move_to_end(cache_key)
record_embedding(query_embedding_cache='hit')
return [list(cached[1])]
record_embedding(query_embedding_cache='miss')
token = runtime_context.set(config)
try:
return await runtime.infer(config.embedding_model, "embedding", {"texts": texts}, priority=embedding_priority.get())
vectors = await runtime.infer(config.embedding_model, "embedding", {"texts": texts}, priority=embedding_priority.get())
if cache_key and len(vectors) == 1:
_embedding_cache[cache_key] = (time.monotonic(), tuple(vectors[0]))
while len(_embedding_cache) > 128: _embedding_cache.popitem(last=False)
return vectors
finally:
runtime_context.reset(token)
+4
View File
@@ -35,6 +35,8 @@ async def lifespan(_: FastAPI):
try:
yield
finally:
from app.benchmarks import service as benchmark_service
await benchmark_service.shutdown()
await container.agent.shutdown()
from app.services import index_service
await index_service.shutdown()
@@ -75,6 +77,8 @@ app.include_router(local_model_router)
app.include_router(usage_router)
app.include_router(provider_preview_router)
app.include_router(log_router)
from app.plot_routes import router as plot_router
app.include_router(plot_router)
@app.middleware('http')
+2 -2
View File
@@ -1,7 +1,7 @@
"""Function Plot 内部数据模型。
契约 §12.2 FunctionPlot 结构与 §10.4 StaticRenderResult 只在导出链路的后端内部
流转不进入 HTTP 契约因此与 Document AST 一样放在独立包内不进 contracts.py
FunctionPlot 供预览和导出共享StaticRenderResult 同时是交互预览端点的响应内容
模型保留在独立包内 plot_routes 中的请求与响应类型注册 OpenAPI
"""
from __future__ import annotations
+12 -12
View File
@@ -143,7 +143,7 @@ def _preprocess(expr: str) -> str:
return _insert_implicit_multiplication(expr.replace("^", "**"))
def _check_node(node: ast.AST, depth: int = 0, counter: list[int] | None = None) -> None:
def _check_node(node: ast.AST, depth: int = 0, counter: list[int] | None = None, unlimited: bool = False) -> None:
"""白名单校验:任何越界节点都抛 FUNCTION_PLOT_EXPRESSION_UNSAFE。
同时限制 AST 深度与节点总数避免超长/超深表达式在递归校验或求值时触发
@@ -151,10 +151,10 @@ def _check_node(node: ast.AST, depth: int = 0, counter: list[int] | None = None)
"""
if counter is None:
counter = [0]
if depth > _MAX_AST_DEPTH:
if not unlimited and depth > _MAX_AST_DEPTH:
_unsafe(f"表达式嵌套过深(超过 {_MAX_AST_DEPTH} 层)")
counter[0] += 1
if counter[0] > _MAX_AST_NODES:
if not unlimited and counter[0] > _MAX_AST_NODES:
_unsafe(f"表达式过于复杂(节点数超过 {_MAX_AST_NODES}")
if isinstance(node, ast.Constant):
if isinstance(node.value, bool) or not isinstance(node.value, (int, float)):
@@ -167,13 +167,13 @@ def _check_node(node: ast.AST, depth: int = 0, counter: list[int] | None = None)
if isinstance(node, ast.BinOp):
if not isinstance(node.op, _ALLOWED_BINOPS):
_unsafe(f"不支持的运算符 {type(node.op).__name__}")
_check_node(node.left, depth + 1, counter)
_check_node(node.right, depth + 1, counter)
_check_node(node.left, depth + 1, counter, unlimited)
_check_node(node.right, depth + 1, counter, unlimited)
return
if isinstance(node, ast.UnaryOp):
if not isinstance(node.op, _ALLOWED_UNARY):
_unsafe(f"不支持的运算符 {type(node.op).__name__}")
_check_node(node.operand, depth + 1, counter)
_check_node(node.operand, depth + 1, counter, unlimited)
return
if isinstance(node, ast.Call):
if not isinstance(node.func, ast.Name) or node.func.id not in _FUNCTIONS:
@@ -184,12 +184,12 @@ def _check_node(node: ast.AST, depth: int = 0, counter: list[int] | None = None)
if len(node.args) != 1:
_unsafe(f"{node.func.id} 需要 1 个参数,实际 {len(node.args)}")
for arg in node.args:
_check_node(arg, depth + 1, counter)
_check_node(arg, depth + 1, counter, unlimited)
return
_unsafe(f"不支持的语法 {type(node).__name__}")
def parse_expression(expr: str) -> ast.Expression:
def parse_expression(expr: str, unlimited: bool = False) -> ast.Expression:
"""把数学表达式解析为已通过白名单校验的 AST(可直接交给 evaluate)。"""
preprocessed = _preprocess(expr)
try:
@@ -211,7 +211,7 @@ def parse_expression(expr: str) -> ast.Expression:
message="表达式嵌套过深,无法解析",
)
) from exc
_check_node(tree.body)
_check_node(tree.body, unlimited=unlimited)
return tree
@@ -279,7 +279,7 @@ def _parse_directive(line: str) -> tuple[str, str] | None:
return key, value.strip()
def parse_source(source: str) -> FunctionPlotParseResult:
def parse_source(source: str, unlimited: bool = False) -> FunctionPlotParseResult:
"""把 function-plot fenced block 源码解析为 FunctionPlot + 诊断。"""
diagnostics: list[PlotDiagnostic] = []
expressions: list[FunctionPlotExpression] = []
@@ -371,7 +371,7 @@ def parse_source(source: str) -> FunctionPlotParseResult:
continue
try:
tree = parse_expression(expr_text)
tree = parse_expression(expr_text, unlimited=unlimited)
except PlotParseError as exc:
exc.diagnostic.line = lineno
diagnostics.append(exc.diagnostic)
@@ -380,7 +380,7 @@ def parse_source(source: str) -> FunctionPlotParseResult:
total_nodes += _count_nodes(tree.body)
expressions.append(FunctionPlotExpression(expression=expr_text))
# 表达式数量超限:整块回退并提前终止,避免对海量表达式做采样求值
if len(expressions) > _MAX_EXPRESSIONS:
if not unlimited and len(expressions) > _MAX_EXPRESSIONS:
diagnostics.append(
PlotDiagnostic(
severity="error",
+34 -13
View File
@@ -339,7 +339,7 @@ def _sample_segments(
return clipped
def compute_geometry(plot: FunctionPlot) -> PlotGeometry:
def compute_geometry(plot: FunctionPlot, unlimited: bool = False) -> PlotGeometry:
"""解析并计算几何,供 SVG 与 reportlab 后端复用。"""
warnings: list[str] = []
xmin, xmax = plot.domain
@@ -351,7 +351,7 @@ def compute_geometry(plot: FunctionPlot) -> PlotGeometry:
fns: list[tuple[object, object]] = []
for expr in plot.expressions:
try:
tree = parse_expression(expr.expression)
tree = parse_expression(expr.expression, unlimited=unlimited)
except PlotParseError as exc:
warnings.append(f"表达式无法渲染,已跳过:{expr.expression}{exc.diagnostic.message}")
continue
@@ -413,12 +413,12 @@ def _grid_svg(geo: PlotGeometry) -> str:
for x in geo.xticks:
parts.append(
f'<line x1="{sx(x):.2f}" y1="{sy(geo.ymin):.2f}" x2="{sx(x):.2f}" '
f'y2="{sy(geo.ymax):.2f}" stroke="#eaeef2"/>'
f'y2="{sy(geo.ymax):.2f}" stroke="#eaeef2" class="plot-grid"/>'
)
for y in geo.yticks:
parts.append(
f'<line x1="{sx(geo.xmin):.2f}" y1="{sy(y):.2f}" x2="{sx(geo.xmax):.2f}" '
f'y2="{sy(y):.2f}" stroke="#eaeef2"/>'
f'y2="{sy(y):.2f}" stroke="#eaeef2" class="plot-grid"/>'
)
return "".join(parts)
@@ -430,11 +430,11 @@ def _axes_svg(geo: PlotGeometry) -> str:
# 坐标轴:过原点则画在原点,否则贴边,保证始终有参照系
parts.append(
f'<line x1="{sx(geo.xmin):.2f}" y1="{sy(geo.x_axis_y):.2f}" x2="{sx(geo.xmax):.2f}" '
f'y2="{sy(geo.x_axis_y):.2f}" stroke="#57606a"/>'
f'y2="{sy(geo.x_axis_y):.2f}" stroke="#57606a" class="plot-axis"/>'
)
parts.append(
f'<line x1="{sx(geo.y_axis_x):.2f}" y1="{sy(geo.ymin):.2f}" x2="{sx(geo.y_axis_x):.2f}" '
f'y2="{sy(geo.ymax):.2f}" stroke="#57606a"/>'
f'y2="{sy(geo.ymax):.2f}" stroke="#57606a" class="plot-axis"/>'
)
# x 轴刻度数字(画在轴下方)
for x in geo.xticks:
@@ -453,10 +453,10 @@ def _axes_svg(geo: PlotGeometry) -> str:
def _polylines_svg(geo: PlotGeometry) -> str:
parts: list[str] = []
for segments, color in zip(geo.polylines, geo.colors):
for index, (segments, color) in enumerate(zip(geo.polylines, geo.colors)):
for seg in segments:
points = " ".join(f"{px:.2f},{py:.2f}" for px, py in seg)
parts.append(f'<polyline points="{points}" fill="none" stroke="{color}"/>')
parts.append(f'<polyline points="{points}" fill="none" stroke="{color}" class="plot-curve-{index % 6}"/>')
return "".join(parts)
@@ -476,22 +476,43 @@ def _labels_svg(geo: PlotGeometry) -> str:
return "".join(parts)
def render_svg(plot: FunctionPlot) -> StaticRenderResult:
def render_svg(plot: FunctionPlot, theme_id: str = 'light', unlimited: bool = False) -> StaticRenderResult:
"""把已解析的 FunctionPlot 渲染为内嵌 SVG。"""
geo = compute_geometry(plot)
geo = compute_geometry(plot, unlimited=unlimited)
legend_height = ((len(plot.expressions) + 1) // 2) * 24
height = geo.height + legend_height
parts: list[str] = [
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {geo.width} {geo.height}" role="img">'
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {geo.width} {height}" role="img" class="function-plot-svg">'
]
if geo.grid:
parts.append(_grid_svg(geo))
parts.append(_axes_svg(geo))
parts.append(_polylines_svg(geo))
parts.append(_labels_svg(geo))
for index, expression in enumerate(plot.expressions):
x = 24 + (index % 2) * 310
y = geo.height + 18 + (index // 2) * 24
label = html.escape(expression.label or ('y = ' + expression.expression))
parts.append(f'<text x="{x}" y="{y}" font-size="12" fill="{geo.colors[index]}" class="plot-legend-{index % 6}">{label}</text>')
parts.append("</svg>")
return StaticRenderResult(
content="".join(parts),
content=theme_svg("".join(parts), theme_id),
width=geo.width,
height=geo.height,
height=height,
warnings=geo.warnings,
)
def theme_svg(svg: str, theme_id: str) -> str:
from app.export.themes import PALETTES
palette = PALETTES.get(theme_id, PALETTES['light'])
for source, target in [('#eaeef2', palette[5]), ('#57606a', palette[3]), ('#1f2328', palette[2])]:
svg = svg.replace(source, target)
if theme_id in {'dark', 'midnight-purple'}:
for source, target in zip(_PALETTE, ['#79c0ff','#ff9b9b','#7ee787','#d2a8ff','#f2cc60','#ffa657']):
svg = svg.replace(source, target)
background = '<rect width="100%" height="100%" fill="' + palette[1] + '"/>'
if re.search(r'<rect width="100%" height="100%" fill="[^"]*"/>', svg):
return re.sub(r'<rect width="100%" height="100%" fill="[^"]*"/>', background, svg, count=1)
return svg.replace('role="img" class="function-plot-svg">', 'role="img" class="function-plot-svg">' + background)
+28 -16
View File
@@ -17,9 +17,7 @@ from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from app.plot.model import FunctionPlot
from app.plot.render import PlotGeometry, _fmt_num, _sx, _sy, compute_geometry
_FONT = "STSong-Light"
if _FONT not in pdfmetrics.getRegisteredFontNames():
pdfmetrics.registerFont(UnicodeCIDFont(_FONT))
from app.export.fonts import FONT as _FONT
_GRID_COLOR = HexColor("#eaeef2")
_AXIS_COLOR = HexColor("#57606a")
@@ -28,9 +26,12 @@ _TICK_FONT_SIZE = 10
_LABEL_FONT_SIZE = 12
def _build_drawing(geo: PlotGeometry) -> Drawing:
def _build_drawing(geo: PlotGeometry, palette=None) -> Drawing:
"""由共享几何构建矢量 Drawing(坐标翻转后仍沿用 SVG 的像素布局)。"""
drawing = Drawing(geo.width, geo.height)
grid_color = HexColor(palette['border']) if palette else _GRID_COLOR
axis_color = HexColor(palette['muted']) if palette else _AXIS_COLOR
label_color = HexColor(palette['text']) if palette else _LABEL_COLOR
# SVG y-down → reportlab y-up:翻转像素 y
def sx(x: float) -> float:
@@ -43,19 +44,19 @@ def _build_drawing(geo: PlotGeometry) -> Drawing:
if geo.grid:
for x in geo.xticks:
drawing.add(
Line(sx(x), sy(geo.ymin), sx(x), sy(geo.ymax), strokeColor=_GRID_COLOR, strokeWidth=0.5)
Line(sx(x), sy(geo.ymin), sx(x), sy(geo.ymax), strokeColor=grid_color, strokeWidth=0.5)
)
for y in geo.yticks:
drawing.add(
Line(sx(geo.xmin), sy(y), sx(geo.xmax), sy(y), strokeColor=_GRID_COLOR, strokeWidth=0.5)
Line(sx(geo.xmin), sy(y), sx(geo.xmax), sy(y), strokeColor=grid_color, strokeWidth=0.5)
)
# 坐标轴(过原点画在原点,否则贴边,与 SVG 一致)
drawing.add(
Line(sx(geo.xmin), sy(geo.x_axis_y), sx(geo.xmax), sy(geo.x_axis_y), strokeColor=_AXIS_COLOR, strokeWidth=0.7)
Line(sx(geo.xmin), sy(geo.x_axis_y), sx(geo.xmax), sy(geo.x_axis_y), strokeColor=axis_color, strokeWidth=0.7)
)
drawing.add(
Line(sx(geo.y_axis_x), sy(geo.ymin), sx(geo.y_axis_x), sy(geo.ymax), strokeColor=_AXIS_COLOR, strokeWidth=0.7)
Line(sx(geo.y_axis_x), sy(geo.ymin), sx(geo.y_axis_x), sy(geo.ymax), strokeColor=axis_color, strokeWidth=0.7)
)
# 刻度数字(x 轴下方、y 轴左侧)
@@ -63,14 +64,14 @@ def _build_drawing(geo: PlotGeometry) -> Drawing:
drawing.add(
String(
sx(x), sy(geo.x_axis_y) - 14, _fmt_num(x),
fontName=_FONT, fontSize=_TICK_FONT_SIZE, fillColor=_AXIS_COLOR, textAnchor="middle",
fontName=_FONT, fontSize=_TICK_FONT_SIZE, fillColor=axis_color, textAnchor="middle",
)
)
for y in geo.yticks:
drawing.add(
String(
sx(geo.y_axis_x) - 6, sy(y) - 3, _fmt_num(y),
fontName=_FONT, fontSize=_TICK_FONT_SIZE, fillColor=_AXIS_COLOR, textAnchor="end",
fontName=_FONT, fontSize=_TICK_FONT_SIZE, fillColor=axis_color, textAnchor="end",
)
)
@@ -85,7 +86,7 @@ def _build_drawing(geo: PlotGeometry) -> Drawing:
drawing.add(
String(
geo.width / 2, 10, geo.xlabel,
fontName=_FONT, fontSize=_LABEL_FONT_SIZE, fillColor=_LABEL_COLOR, textAnchor="middle",
fontName=_FONT, fontSize=_LABEL_FONT_SIZE, fillColor=label_color, textAnchor="middle",
)
)
if geo.ylabel:
@@ -97,7 +98,7 @@ def _build_drawing(geo: PlotGeometry) -> Drawing:
label.add(
String(
0, 0, geo.ylabel,
fontName=_FONT, fontSize=_LABEL_FONT_SIZE, fillColor=_LABEL_COLOR, textAnchor="middle",
fontName=_FONT, fontSize=_LABEL_FONT_SIZE, fillColor=label_color, textAnchor="middle",
)
)
label.translate(16, geo.height / 2)
@@ -107,14 +108,25 @@ def _build_drawing(geo: PlotGeometry) -> Drawing:
return drawing
def render_drawing(plot: FunctionPlot, width: float | None = None) -> Drawing:
def render_drawing(plot: FunctionPlot, width: float | None = None, palette=None, unlimited=False, max_height=None) -> Drawing:
"""把已解析的 FunctionPlot 渲染为 reportlab Drawing(可直接追加到 platypus story)。
``width`` 为目标输出宽度用于把 640px 的几何缩放到页面内容宽省略则按
原始尺寸输出缩放只影响 PDF 渲染不改动共享几何
"""
geo = compute_geometry(plot)
drawing = _build_drawing(geo)
geo = compute_geometry(plot, unlimited=unlimited)
if palette:
from reportlab.lib.colors import HexColor as color
bg = color(palette['surface'])
if .2126*bg.red + .7152*bg.green + .0722*bg.blue < .5:
colors = ['#79c0ff','#ff9b9b','#7ee787','#d2a8ff','#f2cc60','#ffa657']
geo.colors = [value if plot.expressions[i].color else colors[i % len(colors)] for i,value in enumerate(geo.colors)]
drawing = _build_drawing(geo, palette)
legend_height = ((len(plot.expressions)+1)//2)*24
drawing.height += legend_height
for index, expression in enumerate(plot.expressions):
drawing.add(String(24+(index%2)*310,geo.height+legend_height-18-(index//2)*24,
expression.label or 'y = '+expression.expression,fontName=_FONT,fontSize=12,fillColor=HexColor(geo.colors[index])))
if width is not None and width > 0:
drawing.renderScale = min(1.0, width / geo.width)
drawing.renderScale = min(1.0, width / geo.width, max_height / drawing.height if max_height else 1.0)
return drawing
+1 -1
View File
@@ -47,7 +47,7 @@ class FunctionPlotStaticRenderer:
parsed = self.parse(request)
if parsed.plot is None:
raise ValueError("function-plot source has no valid plot")
return self.render_plot(parsed.plot)
return render_svg(parsed.plot, request.theme or 'light')
def render_plot(self, plot: FunctionPlot) -> StaticRenderResult:
return render_svg(plot)
+36
View File
@@ -0,0 +1,36 @@
"""交互预览复用导出使用的有界解析器和几何计算。"""
import asyncio
from fastapi import APIRouter
from pydantic import BaseModel, Field
from app.plot.parser import parse_source
from app.plot.render import render_svg
from app.plot.model import PlotDiagnostic, StaticRenderResult
router = APIRouter(prefix='/api/plots', tags=['Function Plot'])
_slots = asyncio.Semaphore(2)
class PlotRequest(BaseModel):
source: str = Field(max_length=20000)
theme_id: str = Field(default='light', max_length=100)
class PlotResponse(BaseModel):
result: StaticRenderResult | None = None
diagnostics: list[PlotDiagnostic] = Field(default_factory=list)
node_count: int = 0
def preview(request):
"""同步解析并渲染函数图,供受并发限制的异步路由在线程中调用。"""
parsed = parse_source(request.source)
if parsed.plot is None:
return PlotResponse(diagnostics=parsed.diagnostics)
if parsed.plot.node_count > 8000:
return PlotResponse(node_count=parsed.plot.node_count, diagnostics=[PlotDiagnostic(
severity='error', code='PLOT_BUDGET_EXCEEDED', message='图表累计表达式节点超过 8000 上限')])
return PlotResponse(result=render_svg(parsed.plot, request.theme_id),
diagnostics=parsed.diagnostics, node_count=parsed.plot.node_count)
@router.post('/function', response_model=PlotResponse)
async def render_function(request: PlotRequest):
# 绘图属于 CPU 密集任务,限制并发并移入线程,避免阻塞事件循环。
async with _slots:
return await asyncio.to_thread(preview, request)
+7
View File
@@ -114,6 +114,13 @@ class RetrievalEngine:
elif request.mode == SearchMode.vector:
candidate_scores = vec_scores
else: # hybridRRF 融合
if request.fusion == 'weighted':
# 两路原始分值量纲不同,先各自归一化再等权融合,避免任一路分值范围支配结果。
fts_normal = dict(normalize_scores(list(fts_scores.items())))
vec_normal = dict(normalize_scores(list(vec_scores.items())))
candidate_scores = {bid: .5 * fts_normal.get(bid, 0) + .5 * vec_normal.get(bid, 0)
for bid in dict.fromkeys(fts_ranked + vec_ranked)}
else:
candidate_scores = rrf_fuse([fts_ranked, vec_ranked], k=request.rrf_k)
if not candidate_scores:
+17 -4
View File
@@ -562,7 +562,7 @@ async def create_agent_run(request: AgentRunCreateRequest) -> AgentRun:
tags=["Agent"],
)
async def get_agent_run(run_id: str) -> AgentRun:
return agent_run_or_404(run_id)
return await asyncio.to_thread(agent_run_or_404, run_id)
@router.post(
@@ -571,7 +571,7 @@ async def get_agent_run(run_id: str) -> AgentRun:
tags=["Agent"],
)
async def cancel_agent_run(run_id: str) -> OperationResponse:
agent_run_or_404(run_id)
await asyncio.to_thread(agent_run_or_404, run_id)
run = await container.agent.cancel(run_id)
return OperationResponse(
status="completed",
@@ -596,7 +596,7 @@ async def agent_events(
after_sequence: int | None = Query(default=None, ge=-1),
last_event_id: str | None = Header(default=None, alias="Last-Event-ID"),
) -> StreamingResponse:
agent_run_or_404(run_id)
await asyncio.to_thread(agent_run_or_404, run_id)
cursor = after_sequence
if cursor is None and last_event_id is not None:
try:
@@ -658,7 +658,7 @@ async def get_agent_trace(
async def decide_agent_permission(
run_id: str, request_id: str, request: PermissionDecisionRequest
) -> OperationResponse:
agent_run_or_404(run_id)
await asyncio.to_thread(agent_run_or_404, run_id)
if not await container.agent.resolve_permission(run_id, request_id, request.decision):
raise ApiError(
404,
@@ -1547,6 +1547,11 @@ async def create_export(request: ExportRequest) -> ExportJob:
return await export_service.create_export(request)
@router.post("/exports/preview-resources", tags=["Export"])
async def export_preview_resources(request: ExportRequest):
return await export_service.preview_resources(request)
@router.get(
"/exports",
response_model=ExportJobListResponse,
@@ -1622,3 +1627,11 @@ async def get_global_persona():
@router.put("/settings/persona", response_model=PersonaSettings, tags=["Settings"])
async def put_global_persona(request: PersonaSettings):
return save_persona(request)
from app.contracts import AgentBenchmarkRequest
from app.benchmarks import agent as agent_benchmark
@router.post('/benchmarks/agent/runs', response_model=BenchmarkRun, status_code=202, tags=['Benchmark'])
async def create_agent_benchmark(request: AgentBenchmarkRequest):
return await agent_benchmark.create_run(request)
@@ -0,0 +1,89 @@
{
"dataset_id": "agent-core-v1",
"kind": "agent",
"version": "1.0.0",
"description": "受限真实 Runtime 工具选择、参数、无需调用和 Markdown 目录基线;不等同于复杂任务验收",
"cases": [
{
"case_id": "arithmetic",
"prompt": "必须调用 math.add 计算 17 + 25,并报告结果。",
"allowed_tools": [
"math.add",
"system.echo"
],
"expected_tools": [
{
"name": "math.add",
"arguments": {
"left": 17,
"right": 25
}
}
],
"output_contains": [
"42"
],
"tags": [
"tool-selection",
"arguments"
]
},
{
"case_id": "echo",
"prompt": "调用 system.echo 原样回显字符串 phase2-check,然后回答原文。",
"allowed_tools": [
"math.add",
"system.echo"
],
"expected_tools": [
{
"name": "system.echo",
"arguments": {
"text": "phase2-check"
}
}
],
"output_contains": [
"phase2-check"
],
"tags": [
"exact-arguments"
]
},
{
"case_id": "no-tool",
"prompt": "不调用任何工具,只回答:验收就绪",
"allowed_tools": [
"math.add",
"system.echo"
],
"expected_tools": [],
"output_contains": [
"验收就绪"
],
"tags": [
"unnecessary-tools"
]
},
{
"case_id": "markdown-catalog",
"prompt": "使用 markdown.catalog 查询支持的 Markdown 语法,指出函数图像围栏的名称。",
"allowed_tools": [
"markdown.catalog"
],
"expected_tools": [
{
"name": "markdown.catalog",
"arguments": {}
}
],
"output_contains": [
"function-plot"
],
"tags": [
"markdown",
"integration"
]
}
]
}
@@ -0,0 +1,18 @@
{
"version": "1.0.0",
"description": "手工编写的中文工程笔记检索集;每篇含关键词与改写问法,混淆主题分别建篇。用于小规模质量对照,不代表生产分布。",
"notes": [
{"id":"deadlock","title":"死锁的必要条件","text":"死锁需要互斥、占有并等待、不可抢占、循环等待四个条件同时成立。规定所有线程按相同顺序申请锁,可以破坏循环等待条件。","queries":["死锁有哪些必要条件","几个线程各占一把锁并等待对方释放,怎样避免一直卡住"]},
{"id":"starvation","title":"饥饿与公平调度","text":"饥饿指某个任务长期得不到资源,即使其他任务仍能运行。优先级老化会逐渐提高等待任务的优先级;公平队列可以减少长期等待。饥饿不等于所有进程互相等待的死锁。","queries":["优先级老化怎样缓解饥饿","系统一直有任务在跑,但一个低优先级任务永远轮不到怎么办"]},
{"id":"rrf","title":"RRF 排名融合","text":"RRF 使用每个候选在各通道中的名次进行融合,单通道贡献为 1/(k+rank)。它避免直接比较全文检索与向量余弦相似度的原始分数。k 越大,头部名次的差距越平缓。","queries":["RRF 的融合公式是什么","全文分数和向量分数尺度不同,如何按排名合并结果"]},
{"id":"rerank","title":"召回后的重排","text":"重排只重新排列已召回的候选,不能找回不在候选池中的相关段落。扩大候选池可能提高质量,但会增加精排成本。LexicalReranker 根据词面重合打分,不是 Cross-Encoder 神经模型。","queries":["重排能否找回未召回的文档","精排前候选池太小会导致什么问题"]},
{"id":"optimistic","title":"笔记的乐观并发控制","text":"保存笔记时携带读取时的内容摘要。服务端比较当前摘要,若已变化则拒绝覆盖并报告冲突。用户应重新加载或合并修改,避免把另一个窗口的新内容静默覆盖。","queries":["保存时为什么比较内容摘要","两个窗口同时修改同一篇笔记,怎样避免后保存者覆盖新内容"]},
{"id":"idempotency","title":"重复提交的幂等键","text":"客户端为一次逻辑上传创建唯一幂等键。网络重试使用相同的键和内容,服务端返回原附件编号;同键不同内容必须拒绝,防止错误复用。新的逻辑上传使用新的键。","queries":["幂等键如何处理重复上传","上传成功但响应丢失,重试怎样不生成两个附件"]},
{"id":"sse","title":"事件流断线续读","text":"SSE 事件携带递增 sequence。客户端保存最后接收的序号,重连后请求后续事件并去重。终止事件只能出现一次;连接中断本身不代表后台任务被取消。","queries":["SSE 重连如何去重","页面断网后任务仍在运行,如何恢复之前错过的进度"]},
{"id":"cancel","title":"后台任务取消边界","text":"取消标志由运行循环和工具边界检查。排队任务可以立即结束;正在同步渲染的工作应在安全边界检查取消,并丢弃产物。取消后不能发布完成事件或允许下载未完成文件。","queries":["导出任务取消后怎样处理产物","用户停止渲染时工作线程还没返回,应当如何收尾"]},
{"id":"embedding","title":"向量空间隔离","text":"不同 Embedding 模型或维度产生的向量属于不同空间,不能直接比较。索引按模型、版本、维度隔离;切换模型后需要重建对应索引。向量不可用时的全文回退必须在报告中明确记录。","queries":["Embedding 模型切换后为什么要重建索引","两个模型生成的向量长度一样就能混着搜索吗"]},
{"id":"citation","title":"引用定位与块标识","text":"引用记录笔记编号、块编号以及起止偏移。点击引用可定位原文。候选搜索结果不等于回答实际引用的来源;引用质量需要核对正文标记对应的支持性内容。","queries":["引用如何定位到原文","搜索返回十段资料,是否都应该算作回答已引用的来源"]},
{"id":"zip","title":"ZIP 安装路径检查","text":"解压前检查每个条目的规范路径,拒绝绝对路径、父目录穿越、符号链接和超出解压大小预算的条目。安装完成保存包摘要,重启时复核,包被修改后重新审查。","queries":["ZIP 安装如何阻止路径穿越","扩展包里有指向安装目录外的文件名,为什么必须拒绝"]},
{"id":"plot","title":"函数图像的安全解析","text":"function-plot 围栏支持 y = x^2 和 y = sin(x),可以设置 domain 和 range。解析器只允许数学语法,不执行任意代码。函数采样应限制表达式节点和求值次数,渐近线处断开曲线。","queries":["函数图像怎样处理渐近线","让用户输入公式绘图时如何避免执行任意程序"]}
]
}
+368
View File
@@ -0,0 +1,368 @@
{
"dataset_id": "rag-phase2-v1",
"kind": "rag",
"version": "1.0.0",
"description": "手工编写的中文工程笔记检索集;每篇含关键词与改写问法,混淆主题分别建篇。用于小规模质量对照,不代表生产分布。",
"cases": [
{
"case_id": "deadlock-0",
"query": "死锁有哪些必要条件",
"expected_note_ids": [
"note_894ec7d0760d0cd6"
],
"expected_block_ids": [
"blk_748b1be4cee7cb9b"
],
"citation_required": true,
"tags": [
"keyword",
"deadlock"
]
},
{
"case_id": "deadlock-1",
"query": "几个线程各占一把锁并等待对方释放,怎样避免一直卡住",
"expected_note_ids": [
"note_894ec7d0760d0cd6"
],
"expected_block_ids": [
"blk_748b1be4cee7cb9b"
],
"citation_required": true,
"tags": [
"paraphrase",
"deadlock"
]
},
{
"case_id": "starvation-0",
"query": "优先级老化怎样缓解饥饿",
"expected_note_ids": [
"note_da790c1c3b905f26"
],
"expected_block_ids": [
"blk_d15c420ab15ba221"
],
"citation_required": true,
"tags": [
"keyword",
"starvation"
]
},
{
"case_id": "starvation-1",
"query": "系统一直有任务在跑,但一个低优先级任务永远轮不到怎么办",
"expected_note_ids": [
"note_da790c1c3b905f26"
],
"expected_block_ids": [
"blk_d15c420ab15ba221"
],
"citation_required": true,
"tags": [
"paraphrase",
"starvation"
]
},
{
"case_id": "rrf-0",
"query": "RRF 的融合公式是什么",
"expected_note_ids": [
"note_1acd666aa1e79f96"
],
"expected_block_ids": [
"blk_abe4534c5c35b694"
],
"citation_required": true,
"tags": [
"keyword",
"rrf"
]
},
{
"case_id": "rrf-1",
"query": "全文分数和向量分数尺度不同,如何按排名合并结果",
"expected_note_ids": [
"note_1acd666aa1e79f96"
],
"expected_block_ids": [
"blk_abe4534c5c35b694"
],
"citation_required": true,
"tags": [
"paraphrase",
"rrf"
]
},
{
"case_id": "rerank-0",
"query": "重排能否找回未召回的文档",
"expected_note_ids": [
"note_df8b1e8216af7f9a"
],
"expected_block_ids": [
"blk_064caf4b9c2e2518"
],
"citation_required": true,
"tags": [
"keyword",
"rerank"
]
},
{
"case_id": "rerank-1",
"query": "精排前候选池太小会导致什么问题",
"expected_note_ids": [
"note_df8b1e8216af7f9a"
],
"expected_block_ids": [
"blk_064caf4b9c2e2518"
],
"citation_required": true,
"tags": [
"paraphrase",
"rerank"
]
},
{
"case_id": "optimistic-0",
"query": "保存时为什么比较内容摘要",
"expected_note_ids": [
"note_04142ad0124ae76d"
],
"expected_block_ids": [
"blk_3f8c19cf88a03805"
],
"citation_required": true,
"tags": [
"keyword",
"optimistic"
]
},
{
"case_id": "optimistic-1",
"query": "两个窗口同时修改同一篇笔记,怎样避免后保存者覆盖新内容",
"expected_note_ids": [
"note_04142ad0124ae76d"
],
"expected_block_ids": [
"blk_3f8c19cf88a03805"
],
"citation_required": true,
"tags": [
"paraphrase",
"optimistic"
]
},
{
"case_id": "idempotency-0",
"query": "幂等键如何处理重复上传",
"expected_note_ids": [
"note_cdc416a180e5099b"
],
"expected_block_ids": [
"blk_b68f6945024f098d"
],
"citation_required": true,
"tags": [
"keyword",
"idempotency"
]
},
{
"case_id": "idempotency-1",
"query": "上传成功但响应丢失,重试怎样不生成两个附件",
"expected_note_ids": [
"note_cdc416a180e5099b"
],
"expected_block_ids": [
"blk_b68f6945024f098d"
],
"citation_required": true,
"tags": [
"paraphrase",
"idempotency"
]
},
{
"case_id": "sse-0",
"query": "SSE 重连如何去重",
"expected_note_ids": [
"note_5cf7aef15e17bd32"
],
"expected_block_ids": [
"blk_f6328254ea8624a1"
],
"citation_required": true,
"tags": [
"keyword",
"sse"
]
},
{
"case_id": "sse-1",
"query": "页面断网后任务仍在运行,如何恢复之前错过的进度",
"expected_note_ids": [
"note_5cf7aef15e17bd32"
],
"expected_block_ids": [
"blk_f6328254ea8624a1"
],
"citation_required": true,
"tags": [
"paraphrase",
"sse"
]
},
{
"case_id": "cancel-0",
"query": "导出任务取消后怎样处理产物",
"expected_note_ids": [
"note_48f96c75ea97d552"
],
"expected_block_ids": [
"blk_efa6b9f6210964c2"
],
"citation_required": true,
"tags": [
"keyword",
"cancel"
]
},
{
"case_id": "cancel-1",
"query": "用户停止渲染时工作线程还没返回,应当如何收尾",
"expected_note_ids": [
"note_48f96c75ea97d552"
],
"expected_block_ids": [
"blk_efa6b9f6210964c2"
],
"citation_required": true,
"tags": [
"paraphrase",
"cancel"
]
},
{
"case_id": "embedding-0",
"query": "Embedding 模型切换后为什么要重建索引",
"expected_note_ids": [
"note_6e13fbe17c9f7a30"
],
"expected_block_ids": [
"blk_dc2b0771ea50c3a4"
],
"citation_required": true,
"tags": [
"keyword",
"embedding"
]
},
{
"case_id": "embedding-1",
"query": "两个模型生成的向量长度一样就能混着搜索吗",
"expected_note_ids": [
"note_6e13fbe17c9f7a30"
],
"expected_block_ids": [
"blk_dc2b0771ea50c3a4"
],
"citation_required": true,
"tags": [
"paraphrase",
"embedding"
]
},
{
"case_id": "citation-0",
"query": "引用如何定位到原文",
"expected_note_ids": [
"note_a0b8289f7f334952"
],
"expected_block_ids": [
"blk_5eac426f6220ca29"
],
"citation_required": true,
"tags": [
"keyword",
"citation"
]
},
{
"case_id": "citation-1",
"query": "搜索返回十段资料,是否都应该算作回答已引用的来源",
"expected_note_ids": [
"note_a0b8289f7f334952"
],
"expected_block_ids": [
"blk_5eac426f6220ca29"
],
"citation_required": true,
"tags": [
"paraphrase",
"citation"
]
},
{
"case_id": "zip-0",
"query": "ZIP 安装如何阻止路径穿越",
"expected_note_ids": [
"note_4a26a95db9b832fc"
],
"expected_block_ids": [
"blk_ce08f371b1dcf2c5"
],
"citation_required": true,
"tags": [
"keyword",
"zip"
]
},
{
"case_id": "zip-1",
"query": "扩展包里有指向安装目录外的文件名,为什么必须拒绝",
"expected_note_ids": [
"note_4a26a95db9b832fc"
],
"expected_block_ids": [
"blk_ce08f371b1dcf2c5"
],
"citation_required": true,
"tags": [
"paraphrase",
"zip"
]
},
{
"case_id": "plot-0",
"query": "函数图像怎样处理渐近线",
"expected_note_ids": [
"note_bfd65995af95a6a6"
],
"expected_block_ids": [
"blk_c4644f51b0853ef2"
],
"citation_required": true,
"tags": [
"keyword",
"plot"
]
},
{
"case_id": "plot-1",
"query": "让用户输入公式绘图时如何避免执行任意程序",
"expected_note_ids": [
"note_bfd65995af95a6a6"
],
"expected_block_ids": [
"blk_c4644f51b0853ef2"
],
"citation_required": true,
"tags": [
"paraphrase",
"plot"
]
}
]
}
@@ -0,0 +1,138 @@
# function-plot 功能演示
这份笔记展示函数图像的写法、编辑刷新、坐标设置和错误反馈。在 NotesAgent 中打开后,切换到「写作」查看图像;「源码」模式可查看和修改下面的代码块。
## 1. 从一条抛物线开始
`domain` 设置横轴范围,`range` 设置纵轴显示范围。`xlabel``ylabel` 设置坐标轴标签。
```function-plot
domain: -4, 4
range: -2, 18
xlabel: 横坐标 x
ylabel: 函数值 y
grid: true
y = x^2
```
试着将 `y = x^2` 改为 `y = (x-1)^2 + 2`,观察顶点从 `(0, 0)` 移到 `(1, 2)`。修改后切回写作模式即可查看结果。
## 2. 多函数同图
一个代码块内每行写一个函数,曲线按顺序分配颜色,并显示对应图例。三角函数的输入单位是弧度。
```function-plot
domain: -6.2832, 6.2832
range: -2.2, 2.2
xlabel: x / 弧度
ylabel: y
y = sin(x)
y = cos(x)
y = 2sin(x)
```
将第三条函数改为 `y = sin(2x)`,比较振幅变化和周期变化。
## 3. 隐式乘法与交点
支持 `2x``2(x+1)``(x+1)(x-1)` 等写法。乘号也可以显式写成 `*`,幂可以使用 `^`
```function-plot
domain: -3, 4
range: -5, 12
xlabel: x
ylabel: y
y = (x+1)(x-1)
y = 2x + 1
```
两条曲线的交点满足 `x^2 - 1 = 2x + 1`,横坐标约为 `-0.732``2.732`
## 4. 指数、对数与参考直线
支持常量 `e``pi`,以及 `exp``ln``log10` 等函数。这里把横轴限定在正数范围,保证对数有定义。
```function-plot
domain: -3, 3
range: -3, 8
xlabel: x
ylabel: y
y = exp(x)
y = ln(x)
y = x
```
超出纵轴显示范围的曲线会被裁切。把 `range` 改为 `-3, 22`,可以查看更完整的指数曲线。
## 5. 绝对值与平方根
```function-plot
domain: -4, 4
range: -0.5, 4.5
xlabel: x
ylabel: y
y = abs(x)
y = sqrt(abs(x))
```
这里用 `sqrt(abs(x))`,所以负半轴也有定义;它与 `sqrt(x)` 的定义域不同。
## 6. 间断点与显示范围
```function-plot
domain: -5, 5
range: -5, 5
xlabel: x
ylabel: y
y = 1/x
```
`x = 0` 处无定义,图像应分成左右两支,而不是跨过间断点连线。可缩放或打开大图查看原点附近;曲线是有限采样的可视化,不代替数学定义。
## 7. 关闭网格
```function-plot
domain: -6, 6
range: -0.2, 1.2
xlabel: x
ylabel: y
grid: false
y = exp(-x^2/2)
```
`grid: false` 改为 `grid: true`,比较有无网格的效果。
## 8. 错误反馈演示(故意写错)
下面的 `sinn` 不是支持的函数名,预期显示错误诊断,不生成曲线。这是本节的演示内容。把它改为 `sin` 即可恢复图像;若希望导出一份没有错误警告的文档,请先修正这一行。
```function-plot
domain: -3.14, 3.14
y = sinn(x)
```
## 交互与导出检查
- 将鼠标移到图表区域,试用缩小、放大、重置和大图查看;只读预览也可切换源码。
- 在窄窗口中横向滚动函数图,检查坐标刻度和右侧图例。
- 依次切换 `light``dark``sepia``paper-moments``ocean-blue``midnight-purple`;社区主题需先安装并启用。检查背景、网格、文字和曲线的对比度。
- 修改第一节函数后不保存,点击编辑器顶部「导出」,分别选择 HTML、PDF、DOCX,验证文件采用点击时的编辑内容。
- HTML 保留支持的主题配色;PDF、DOCX 使用浅色打印样式。HTML 的函数图为 SVG,PDF 为矢量图,DOCX 为静态图片。
## 写法速查
| 项目 | 示例 |
| ----- | ------------------------------------------------- |
| 代码块语言 | `function-plot` |
| 函数表达式 | `y = x^2 + 2x + 1` |
| 横轴范围 | `domain: -5, 5` |
| 纵轴范围 | `range: -2, 10`,省略时自动估计 |
| 坐标标签 | `xlabel: 时间``ylabel: 数值` |
| 网格开关 | `grid: true` / `grid: false` |
| 数学常量 | `pi``e` |
| 常用函数 | `sin``cos``tan``sqrt``abs``exp``ln``log10` |
| 注释 | 单独一行以 `#` 开头 |
范围端点使用数值,例如 `domain: -3.1416, 3.1416`;表达式中可以使用 `pi`。当前只绘制以 `x` 为自变量的二维函数,不支持任意脚本、参数曲线或三维曲面。
每个图块最多 16 条表达式;每份导出文档最多 16 个函数图、累计 8000 个表达式节点。本文件包含 7 个正常示例和 1 个有意保留的错误示例。
+2
View File
@@ -17,6 +17,8 @@ dependencies = [
"referencing>=0.36,<1.0",
"sqlite-vec>=0.1.9",
"uvicorn[standard]>=0.35,<1.0",
"matplotlib>=3.9,<4",
"playwright>=1.55,<2",
]
[dependency-groups]
+4 -2
View File
@@ -96,11 +96,13 @@ async def main(output):
assert all(sequences)
recovered = AgentRuntime(container.providers, container.tools, container.permissions,
trace_repository=runtime.trace_repository)
assert all(recovered.get_run(run_id).status == AgentRunStatus.completed for run_id in ids)
recovery_started = time.perf_counter()
assert await asyncio.to_thread(lambda: all(recovered.get_run(run_id).status == AgentRunStatus.completed for run_id in ids))
recovery_read_ms = (time.perf_counter() - recovery_started) * 1000
assert not any(record.subscribers for record in runtime._records.values())
return {"concurrency": concurrency, "runs": len(ids), "latency": stats(durations),
"completed": statuses.count('completed'), "ordered_events_and_replay": True,
"terminal_recovery": True, "retained_records": len(runtime._records)}
"terminal_recovery": True, "recovery_read_ms": round(recovery_read_ms,2), "retained_records": len(runtime._records)}
save('agent_tool_runs', await measured(batch))
# Hold model calls so all 200 records remain active while testing admission.
+50
View File
@@ -0,0 +1,50 @@
"""执行两次有界真实调用完成上下文摘要与回答,不修改已保存配置。"""
import argparse, asyncio, json, sys
from pathlib import Path
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
async def main(args):
from app.container import container
from app.contracts import ModelRequest, ModelContextPolicy
from app.providers.factory import ProviderFactory
from app.providers.context_budget import prepare_context, estimate
from app.providers.base import ProviderError
from app.services.usage_service import connection
provider=container.providers.get(args.provider)
model=provider.config.default_model
request=ModelRequest(provider_id=args.provider,model=model,max_tokens=1024,messages=[
{'role':'user','content':'项目事实:笔记保存在 Vault,导出使用点击时的快照。'*50},
{'role':'assistant','content':'已记录。'}, {'role':'user','content':'请保持中文。'},
{'role':'assistant','content':'好的。'}, {'role':'user','content':'笔记保存在什么地方?一句话回答。'}])
original=request.model_dump()
config=provider.config.model_copy(deep=True)
config.context_policies=[ModelContextPolicy(model=model,context_window=8192,output_reserve=512,threshold=.1,mode='detect')]
calls=0
async def complete(value):
nonlocal calls
calls+=1
return await provider.adapter.complete(value)
results={'model':model,'configured_test_window':8192,'vendor_max_context_tested':False}
try:
try: await prepare_context(request,config,complete)
except ProviderError as exc: results['detect']={'error_code':exc.code,'network_calls':calls}
config.context_policies[0].mode='compress'
config.context_policies[0].prompt='把以下历史资料压缩成一句中文,只保留笔记存储位置和导出快照规则。'
prepared=await asyncio.wait_for(prepare_context(request,config,complete),60)
turn=await asyncio.wait_for(complete(prepared),60)
results['compression']={'passed':'vault' in (turn.text or '').lower(),'before_estimate':estimate(request),
'after_estimate':estimate(prepared),'archive_unchanged':request.model_dump()==original,'network_calls':calls,
'answer_input_tokens':turn.input_tokens,'answer_output_tokens':turn.output_tokens}
with connection() as conn:
rows=[json.loads(row[0]) for row in conn.execute('SELECT counters_json FROM model_usage WHERE provider_id=?',(args.provider,))]
results['observed_provider_cache']={'requests':len(rows),'reporting_requests':sum(x.get('cache_hit_tokens') is not None for x in rows),
'positive_hit_requests':sum((x.get('cache_hit_tokens') or 0)>0 for x in rows),
'positive_miss_requests':sum((x.get('cache_miss_tokens') or 0)>0 for x in rows),
'scope':'provider reported usage across this isolated acceptance session; not deterministic cache control'}
finally:
args.output.write_text(json.dumps(results,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(results,ensure_ascii=False))
await container.agent.shutdown();container.mcp_servers.shutdown();container.plugins.shutdown()
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--provider',required=True);p.add_argument('--output',type=Path,required=True);p.add_argument('--execute',action='store_true');args=p.parse_args()
if not args.execute:p.error('--execute required; two requests use existing quota')
asyncio.run(main(args))
+56
View File
@@ -0,0 +1,56 @@
"""显式隔离演示:检索、读取、创建三个任务,再调用真实只读 MCP。
只批准本次运行产生的 tasks.write 权限票据需要质量验收用 Vault
脚本仅调用公共 API 契约不把伪造的完成状态写入 SQLite
"""
import argparse, json, time
from pathlib import Path
from urllib.request import Request,urlopen
def main(args):
def api(path,body=None):
req=Request(args.base_url+'/api'+path,data=json.dumps(body).encode() if body is not None else None,
headers={'Content-Type':'application/json'})
with urlopen(req,timeout=60) as response:return json.load(response)
directory=args.data_dir.resolve()
if not (directory/'vault/.phase2-fixture').exists():raise SystemExit('Isolated fixture Vault required')
core=json.loads((directory/'benchmarks/rag-phase2-v1.json').read_text(encoding='utf-8'))
note=next(c for c in core['cases'] if c['case_id']=='deadlock-0')['expected_note_ids'][0]
cases=[{'case_id':'retrieval-tasks','prompt':f'按顺序执行:1. 用 rag.search 搜索“死锁”,明确使用 mode=fts2. 用 notes.read 读取笔记 {note}3. 根据内容用 tasks.create 分别创建且仅创建三个任务,标题严格为“验收-互斥条件”、“验收-循环等待”、“验收-锁顺序”;4. 总结死锁条件并引用搜索来源。请不要调用其他工具,不重复创建。',
'allowed_tools':['rag.search','notes.read','tasks.create'],
'expected_tools':[{'name':'rag.search','arguments':{'query':'死锁','mode':'fts'}},{'name':'notes.read','arguments':{'note_id':note}}]+
[{'name':'tasks.create','arguments':{'title':title}} for title in ['验收-互斥条件','验收-循环等待','验收-锁顺序']],
'citation_required':True,'tasks_created':3,'output_contains':['死锁'],'tags':['rag','notes','tasks','permissions']}]
tools=api('/tools')['items']; mcp=next((t for t in tools if t['name'].endswith('.web_search') and t['name'].startswith('mcp.')),None)
if mcp:
cases.append({'case_id':'mcp-search','prompt':f'调用一次 {mcp["name"]}query 严格使用 Python official documentation tutorial。根据工具真实返回给出一句总结。',
'allowed_tools':[mcp['name']], 'expected_tools':[{'name':mcp['name'],'arguments':{'query':'Python official documentation tutorial'}}], 'tags':['mcp','real-network']})
dataset={'dataset_id':'agent-integration-v1','kind':'agent','version':'1.0.0','description':'Isolated phase2 cross-module Demo; existing MCP binding captured','cases':cases}
(directory/'benchmarks/agent-integration-v1.json').write_text(json.dumps(dataset,ensure_ascii=False,indent=2),encoding='utf-8')
providers=api('/providers')['items']; provider=next(p for p in providers if p['enabled'] and p['provider_type']!='mock')
run=api('/benchmarks/agent/runs',{'dataset_id':dataset['dataset_id'],'provider_id':provider['provider_id'],'model':provider['default_model'],
'max_steps':10,'timeout_seconds':150,'token_budget':10000,'allow_network':True})
approved=set();deadline=time.monotonic()+360
while run['status'] in ['queued','running']:
if time.monotonic()>deadline:
api('/benchmarks/runs/'+run['run_id']+'/cancel',{});raise RuntimeError('Demo deadline')
active=run['config_snapshot'].get('active_agent_run_id')
if active:
trace=api('/agent/runs/'+active+'/trace')
for event in trace['items']:
data=event['data'];ticket=data.get('request_id')
if event['event']=='PermissionRequired' and data.get('permission')=='tasks.write' and ticket not in approved:
api('/agent/runs/'+active+'/permissions/'+ticket,{'decision':'allow_once'});approved.add(ticket)
time.sleep(.3);run=api('/benchmarks/runs/'+run['run_id'])
report=api('/benchmarks/runs/'+run['run_id']+'/report')
report['permission_approvals']=len(approved)
report['mcp_present']=bool(mcp)
report['tasks']= [{'task_id':t['task_id'],'title':t['title']} for t in api('/tasks')['items'] if t['title'].startswith('验收-')]
args.output.write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({'metrics':report['metrics'],'cases':report['cases'],'permission_approvals':len(approved)},ensure_ascii=False))
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--base-url',default='http://127.0.0.1:8017');p.add_argument('--data-dir',type=Path,required=True);p.add_argument('--output',type=Path,required=True);p.add_argument('--execute',action='store_true');args=p.parse_args()
if not args.execute:p.error('--execute required; creates three tasks only in the isolated fixture application')
if args.base_url not in {'http://127.0.0.1:8017','http://localhost:8017'}:p.error('Use the isolated local acceptance server on port 8017')
main(args)
+64
View File
@@ -0,0 +1,64 @@
"""使用现有配置执行有界的真实协议与 Agent 检查,不输出凭据。
必须显式传入 --execute最多发起 5 次直接模型请求和一组 4 样本 Agent 评测
每个样本最多 6 6000 Token不创建配置也不执行外部写入
"""
import argparse, asyncio, json, sys
from pathlib import Path
from time import perf_counter
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
async def main(args):
from app.container import container
from app.contracts import ModelRequest, AgentBenchmarkRequest
from app.providers.base import ProviderError
from app.benchmarks import agent,service
provider=container.providers.get(args.provider)
adapter=provider.adapter; model=provider.config.default_model
results={'provider_id':args.provider,'protocol':provider.config.provider_type.value,'model':model,'checks':{},
'unconfigured_protocols':['openai_responses','anthropic_messages','ollama'],
'not_tested':['provider_context_limit','cache_hit_miss','context_compression'],'max_direct_requests':5}
def request(prompt='Reply OK only.', **changes):
return ModelRequest(provider_id=args.provider,model=model,messages=[{'role':'user','content':prompt}],max_tokens=128,**changes)
async def check(name, fn):
started=perf_counter()
try: results['checks'][name]=await asyncio.wait_for(fn(),60)
except ProviderError as exc: results['checks'][name]={'passed':False,'error_code':exc.code}
except Exception as exc: results['checks'][name]={'passed':False,'error_type':type(exc).__name__}
results['checks'][name]['elapsed_ms']=round((perf_counter()-started)*1000,2)
print(name,results['checks'][name],flush=True)
async def discover():
models=await adapter.list_models();return {'passed':bool(models),'count':len(models)}
async def complete():
turn=await adapter.complete(request());return {'passed':bool(turn.text),'input_tokens':turn.input_tokens,'output_tokens':turn.output_tokens}
async def stream():
events=[e async for e in adapter.stream(request())]
names=[e.event.value for e in events]
return {'passed':names.count('Done')==1 and 'TextDelta' in names,'events':sorted(set(names)),
'usage_reported':'Usage' in names,'reasoning_observed':'ThinkingDelta' in names}
async def cancel():
iterator=adapter.stream(request('List the integers 1 through 1000.'))
first=await anext(iterator);started=perf_counter();await iterator.aclose()
return {'passed':True,'first_event':first.event.value,'close_ms':(perf_counter()-started)*1000,
'scope':'client stream resource close; provider billing cessation not observable'}
async def invalid_model():
try: await adapter.complete(request().model_copy(update={'model':'notesagent-nonexistent-acceptance-model'}))
except ProviderError as exc:return {'passed':True,'error_code':exc.code}
return {'passed':False,'reason':'provider accepted unknown model'}
try:
for name,fn in [('discovery',discover),('normal_chat',complete),('stream_usage_reasoning',stream),('stream_cancel',cancel),('error_mapping',invalid_model)]:await check(name,fn)
created=await agent.create_run(AgentBenchmarkRequest(dataset_id='agent-core-v1',provider_id=args.provider,model=model))
await service.wait_for_run(created.run_id)
results['agent']=service.get_report(created.run_id).model_dump(mode='json')
print('agent',results['agent']['metrics'],flush=True)
servers=container.mcp_servers.list()
results['mcp']=[{'server_id':s.server_id,'name':s.name,'state':s.status.value if hasattr(s.status,'value') else s.status} for s in servers]
finally:
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(results,ensure_ascii=False,indent=2,default=str),encoding='utf-8')
await container.agent.shutdown();container.mcp_servers.shutdown();container.plugins.shutdown()
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--provider',required=True);p.add_argument('--output',type=Path,required=True);p.add_argument('--execute',action='store_true');args=p.parse_args()
if not args.execute:p.error('--execute required; uses existing provider quota')
asyncio.run(main(args))
+69
View File
@@ -0,0 +1,69 @@
"""在显式隔离的 APP_DATA_DIR 中执行可复现的真实模型质量验证。
使用应用自身的索引与评测服务不注入向量或伪造完成记录
运行前必须已有本地权重和运行环境推理过程不会下载模型
"""
import argparse
import asyncio
import hashlib
import json
import os
import sys
from pathlib import Path
from datetime import datetime, timezone
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
async def main(args):
from app.config import get_settings
from app.knowledge.parser import parse_note
from app.services import index_service
from app.benchmarks import service
from app.contracts import IndexRebuildRequest, RAGRunRequest
settings=get_settings()
if not all(os.getenv(name) for name in ['APP_DATA_DIR','APP_DB_PATH','APP_VAULT_PATH']):
raise SystemExit('Explicit isolated APP_DATA_DIR/APP_DB_PATH/APP_VAULT_PATH required')
fixture=Path(__file__).resolve().parents[1]/'data/benchmarks/corpus/phase2-v1.json'
payload=json.loads(fixture.read_text(encoding='utf-8')); cases=[]
settings.vault_path.mkdir(parents=True,exist_ok=True)
if list(settings.vault_path.glob('*.md')) and not (settings.vault_path/'.phase2-fixture').exists():
raise SystemExit('Refusing to overwrite a non-fixture vault')
now=datetime(2026,9,7,tzinfo=timezone.utc)
for note in payload['notes']:
name=note['id']+'.md'; markdown='# '+note['title']+'\n\n'+note['text']+'\n'
(settings.vault_path/name).write_text(markdown,encoding='utf-8')
parsed=parse_note(markdown=markdown,file_path=name,folder='',created_at=now,updated_at=now)
for index,query in enumerate(note['queries']):
cases.append({'case_id':note['id']+'-'+str(index),'query':query,'expected_note_ids':[parsed.note_id],
'expected_block_ids':[parsed.blocks[-1].block_id],'citation_required':True,
'tags':['keyword' if index==0 else 'paraphrase',note['id']]})
(settings.vault_path/'.phase2-fixture').touch()
dataset={'dataset_id':'rag-phase2-v1','kind':'rag','version':payload['version'],'description':payload['description'],'cases':cases}
settings.benchmark_datasets_path.mkdir(parents=True,exist_ok=True)
(settings.benchmark_datasets_path/'rag-phase2-v1.json').write_text(json.dumps(dataset,ensure_ascii=False,indent=2),encoding='utf-8')
if not args.reuse_index:
job=await index_service.rebuild(IndexRebuildRequest())
if job.status != 'completed': raise RuntimeError('Index did not complete: '+str(job.status))
from app import repository
expected_blocks = {bid for case in cases for bid in case['expected_block_ids']}
if {hit.block_id for hit in repository.get_block_hits(list(expected_blocks))} != expected_blocks:
raise RuntimeError('Frozen corpus does not match the index; rerun without --reuse-index')
reports={}
for label, mode, fusion, rerank, k in [('fts','fts','rrf',False,60),('vector','vector','rrf',False,60),
('hybrid-weighted','hybrid','weighted',False,60),('hybrid-rrf','hybrid','rrf',False,60),
('hybrid-rerank','hybrid','rrf',True,60),('rrf-k20','hybrid','rrf',False,20)]:
run=await service.create_rag_run(RAGRunRequest(dataset_id='rag-phase2-v1',modes=[mode],repeat=2,
retrieval={'top_k':5,'fusion':fusion,'rerank':rerank,'rrf_k':k,'rerank_candidates':20},
metadata={'corpus_sha256':hashlib.sha256(fixture.read_bytes()).hexdigest(),'split':'development; no held-out production claim'}))
await service.wait_for_run(run.run_id)
reports[label]=service.get_report(run.run_id).model_dump(mode='json')
print(label, json.dumps(reports[label]['metrics']),flush=True)
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(reports,ensure_ascii=False,indent=2),encoding='utf-8')
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(reports,ensure_ascii=False,indent=2),encoding='utf-8')
from app.container import container
await container.agent.shutdown(); container.mcp_servers.shutdown(); container.plugins.shutdown()
if __name__=='__main__':
parser=argparse.ArgumentParser(); parser.add_argument('--output',type=Path,required=True); parser.add_argument('--reuse-index',action='store_true')
asyncio.run(main(parser.parse_args()))
+13 -10
View File
@@ -321,7 +321,7 @@ def test_pdf_exporter_embeds_function_plot_and_marks_mermaid() -> None:
# function_plot 已内嵌为矢量图,不再产生「函数图像占位」warning
assert not any("函数图像" in w for w in result.warnings)
# 绘图用 STSong-Light 渲染刻度/标签,字体应嵌入 PDF
assert b"STSong-Light" in result.content
assert b"STSong-Light" in result.content or b"/FontFile2" in result.content
def test_pdf_exporter_function_plot_fallback_on_error() -> None:
@@ -334,17 +334,16 @@ def test_pdf_exporter_function_plot_fallback_on_error() -> None:
assert any("函数图像" in w for w in result.warnings)
def test_pdf_exporter_limits_function_plot_count() -> None:
def test_pdf_exporter_has_no_function_plot_count_quota() -> None:
from app.export.exporters.pdf import PdfExporter
blocks = "\n\n".join("```function-plot\ny = x\n```" for _ in range(20))
result = asyncio.run(PdfExporter().export(parse_document(blocks), ExportOptions()))
assert result.content[:4] == b"%PDF"
# 超出数量上限的图块回退占位并记 warning
assert any("数量超过上限" in w for w in result.warnings)
assert not any("函数图像" in w for w in result.warnings)
def test_pdf_exporter_limits_total_plot_nodes(monkeypatch) -> None:
def test_pdf_exporter_has_no_total_plot_node_quota(monkeypatch) -> None:
import app.export.exporters._common as common_mod
from app.export.exporters.pdf import PdfExporter
@@ -352,17 +351,20 @@ def test_pdf_exporter_limits_total_plot_nodes(monkeypatch) -> None:
md = "```function-plot\ny = x\n```\n\n```function-plot\ny = x + x + x + x\n```"
result = asyncio.run(PdfExporter().export(parse_document(md), ExportOptions()))
assert result.content[:4] == b"%PDF"
assert any("累计复杂度" in w for w in result.warnings)
assert not any("函数图像" in w for w in result.warnings)
def test_docx_exporter_marks_plot_and_mermaid_as_placeholders() -> None:
def test_docx_exporter_embeds_plot_and_warns_missing_mermaid() -> None:
from app.export.exporters.docx import DocxExporter
md = "```mermaid\ngraph LR\n```\n\n```function_plot\ny = x\n```"
result = asyncio.run(DocxExporter().export(parse_document(md), ExportOptions()))
assert result.content[:2] == b"PK"
assert any("mermaid" in w for w in result.warnings)
assert any("函数图像" in w for w in result.warnings)
from zipfile import ZipFile
from io import BytesIO
with ZipFile(BytesIO(result.content)) as archive:
assert any(name.startswith('word/media/') for name in archive.namelist())
def test_pdf_exporter_embeds_cjk_font() -> None:
@@ -373,7 +375,7 @@ def test_pdf_exporter_embeds_cjk_font() -> None:
result = asyncio.run(PdfExporter().export(doc, ExportOptions(include_title=True)))
assert result.content[:4] == b"%PDF"
# 中文字体通过 STSong-Light CID 字体嵌入,PDF 内应引用该 BaseFont
assert b"STSong-Light" in result.content
assert b"STSong-Light" in result.content or b"/FontFile2" in result.content
def test_docx_exporter_contains_cjk_text() -> None:
@@ -828,7 +830,8 @@ def test_callout_formats(name):
xml = z.read("word/document.xml").decode()
assert all(word in xml for word in ["Title", "Body", "item", "second", "w:shd"])
result = PdfExporter().render(doc, ExportOptions(theme_id="sepia"))
assert result.content.startswith(b"%PDF") and len(result.warnings) == 1
assert result.content.startswith(b"%PDF")
assert not any("浅色打印" in warning for warning in result.warnings)
@pytest.mark.parametrize("fold", ["", "+", "-"])
+68
View File
@@ -0,0 +1,68 @@
import asyncio
from pathlib import Path
import pytest
from app.contracts import ExportRequest
from app.export import service
from app.export.document import ExportResult
from app.export.browser_pdf import render_snapshot, browser_executable
def test_browser_snapshot_uses_print_pipeline(monkeypatch):
calls=[]
def render(html,size):
calls.append((html,size));return ExportResult(content=b'%PDF-browser',mime_type='application/pdf')
monkeypatch.setattr('app.export.browser_pdf.render_snapshot',render)
monkeypatch.setattr(service,'parse_document',lambda _:pytest.fail('Browser snapshots must not be reparsed by ReportLab'))
async def run():
request=ExportRequest(format='pdf',source={'type':'markdown','markdown':'snapshot'},print_html='<style>h1::before{content:"tape"}</style><h1>Note</h1>')
job=await service.create_export(request);done=await service.wait_for_export(job.job_id)
assert done.status.value=='completed'
assert calls==[(request.print_html,'A4')]
asyncio.run(run())
def test_preview_resources_keeps_vault_boundary_and_plot_quota_removed():
async def run():
source='![outside](../../private.png)\n\n```function-plot\n'+'\n'.join(f'y=x+{i}' for i in range(17))+'\n```'
resources=await service.preview_resources(ExportRequest(format='pdf',source={'type':'markdown','markdown':source}))
assert resources['images'][0]['data'] is None
assert resources['images'][0]['warnings']
assert resources['plots'][0]['svg'].startswith('<svg')
asyncio.run(run())
@pytest.mark.skipif(browser_executable() is None,reason='No installed Chromium browser')
def test_browser_prints_css_without_executing_document_scripts(tmp_path):
# 若脚本被执行会清空正文;测试同时确认 CSS/字体可用且网络、文件资源保持禁用。
html='<style>h1{color:#875343;font-size:37px} h1::before{content:"Theme "}</style><h1>Snapshot</h1><script>document.body.innerHTML="EXECUTED"</script><img src="file:///private.png">'
result=render_snapshot(html,'A4')
assert result.content.startswith(b'%PDF')
assert b'/Subtype /Type0' in result.content or b'/Type /Font' in result.content
import shutil, subprocess
if shutil.which('pdftotext'):
pdf=tmp_path/'snapshot.pdf'; pdf.write_bytes(result.content)
text=subprocess.check_output(['pdftotext',str(pdf),'-']).decode('utf-8')
assert 'Theme Snapshot' in text
assert 'EXECUTED' not in text
@pytest.mark.parametrize('source', ['<div><IMG SRC="assets/a&amp;b.png"></div>', 'inline <img src="assets/a&amp;b.png"/> image'])
def test_preview_embeds_html_images(source):
from app.config import get_settings
from PIL import Image
import base64
folder=get_settings().vault_path/'notes'/'assets'
folder.mkdir(parents=True)
Image.new('RGBA',(2,2),(10,20,30,128)).save(folder/'a&b.png')
resources=asyncio.run(service.preview_resources(ExportRequest(format='pdf',source={'type':'markdown','markdown':source,'file_path':'notes/test.md'})))
image=resources['images'][0]
assert image['source']=='assets/a&b.png'
assert base64.b64decode(image['data'].split(',')[1]).startswith(b'\x89PNG')
assert image['warnings']==[]
def test_html_images_keep_path_validation_and_code_is_not_an_image():
source='<img src="../../private.png">\n\ninline <img src="https://example.com/a.png">\n\n`<img src="code.png">`\n\n```html\n<img src="fenced.png">\n```'
resources=asyncio.run(service.preview_resources(ExportRequest(format='pdf',source={'type':'markdown','markdown':source})))
assert [image['source'] for image in resources['images']]==['../../private.png','https://example.com/a.png']
assert all(image['data'] is None and image['warnings'] for image in resources['images'])
+96
View File
@@ -0,0 +1,96 @@
"""PDF theme and resource policy regressions; no real providers or user files."""
import asyncio
import base64
from io import BytesIO
import pytest
from PIL import Image
from pydantic import ValidationError
from app.contracts import ExportAsset, ExportOptions, ExportRequest
from app.export.assets import validate_assets, enrich_document, source_hash
from app.export.exporters.pdf import PdfExporter
from app.export.markdown import parse_document
from app.export.themes import PALETTES
from app.export import service
def png_asset(size=(40,30), source='graph LR; A-->B'):
out=BytesIO(); Image.new('RGBA',size,(0,0,0,0)).save(out,'PNG')
return ExportAsset(kind='mermaid',source_hash=source_hash(source),png_base64=base64.b64encode(out.getvalue()).decode())
@pytest.mark.parametrize('theme',list(PALETTES))
def test_pdf_theme_colors_are_written_on_every_page(theme):
import re, zlib
palette=PALETTES[theme]
doc=parse_document(('## Section\n\nText body\n\n> Quoted text\n\n```python\nprint(1)\n```\n\n')*30)
result=PdfExporter().render(doc,ExportOptions(theme_id=theme))
streams=[]
for match in re.finditer(rb'stream\r?\n(.*?)endstream',result.content,re.S):
try: streams.append(zlib.decompress(base64.a85decode(match[1].strip().removesuffix(b'~>'))))
except Exception: pass
from reportlab.lib.rl_accel import fp_str
command=(fp_str(*[int(palette[0][i:i+2],16)/255 for i in (1,3,5)])+' rg').encode()
pages=[s for s in streams if b'BT' in s and b'/F' in s]
assert len(pages)>1
assert all(command in s for s in pages)
assert not any('浅色打印' in w for w in result.warnings)
def test_pdf_accepts_asset_contract_beyond_previous_count_and_size():
assets=[png_asset(source=str(i)) for i in range(65)]
assets[0]=assets[0].model_copy(update={'png_base64':'A'*2800004})
values=dict(source={'type':'markdown','markdown':'content'},assets=assets)
ExportRequest(format='pdf',**values)
with pytest.raises(ValidationError): ExportRequest(format='html',**values)
with pytest.raises(ValidationError): ExportRequest(format='docx',**values)
def test_pdf_large_png_still_requires_valid_format():
asset=png_asset((2100,2000))
assert validate_assets([asset],unlimited=True)
with pytest.raises(Exception): validate_assets([asset])
with pytest.raises(Exception): validate_assets([asset.model_copy(update={'png_base64':'invalid'})],unlimited=True)
def test_pdf_embeds_more_than_64_resources_with_theme_background():
doc=parse_document(('```mermaid\ngraph LR; A-->B\n```\n\n')*65)
from app.export.assets import attach_assets
attach_assets(doc,validate_assets([png_asset()],unlimited=True))
assert not enrich_document(doc,unlimited=True,options=ExportOptions(theme_id='dark'))
assert all('static_png' in node.attributes for node in doc.children)
with Image.open(BytesIO(doc.children[-1].attributes['static_png'])) as image:
assert image.getpixel((0,0)) == (13,17,23)
assert PdfExporter().render(doc,ExportOptions(theme_id='dark')).content.startswith(b'%PDF')
def test_pdf_pipeline_ignores_source_and_output_quotas(monkeypatch):
monkeypatch.setattr(service,'MAX_MARKDOWN_CHARS',8)
monkeypatch.setattr(service,'MAX_EXPORT_BYTES',8)
async def run():
job=await service.create_export(ExportRequest(format='pdf',source={'type':'markdown','markdown':'Beyond the previous quota.'}))
done=await service.wait_for_export(job.job_id)
assert done.status.value=='completed'
assert service.get_export_file(job.job_id).stat().st_size>8
with pytest.raises(Exception):
await service.create_export(ExportRequest(format='html',source={'type':'markdown','markdown':'Beyond the previous quota.'}))
asyncio.run(run())
def test_pdf_accepts_more_than_16_curves_and_keeps_expression_safety():
doc=parse_document('```function-plot\n'+'\n'.join(f'y=x+{i}' for i in range(17))+'\n```')
result=PdfExporter().render(doc,ExportOptions(theme_id='dark'))
assert not any('函数图像' in w for w in result.warnings)
unsafe=PdfExporter().render(parse_document('```function-plot\ny=__import__("os")\n```'),ExportOptions())
assert any('函数图像' in w for w in unsafe.warnings)
def test_pdf_custom_palette_and_math_color():
palette=dict(zip(('page','surface','text','muted','code','border','accent'),PALETTES['midnight-purple']))
options=ExportOptions(theme_id='my-theme',palette=palette)
doc=parse_document('Formula $x^2$')
assert not enrich_document(doc,unlimited=True,options=options)
math=next(n for n in doc.children[0].children if n.type=='math_inline')
with Image.open(BytesIO(math.attributes['static_png'])) as image:
assert image.getpixel((0,0))==(25,19,34)
assert not any('主题' in w for w in PdfExporter().render(doc,options).warnings)
with pytest.raises(ValidationError): ExportOptions(palette={**palette,'text':'url(file:///private)'})
+199
View File
@@ -0,0 +1,199 @@
import asyncio
import base64
import json
from io import BytesIO
from zipfile import ZipFile
from pathlib import Path
import pytest
from PIL import Image
from app.contracts import ExportAsset, ExportRequest, AgentBenchmarkRequest, BenchmarkStatus
from app.export import service as exports
from app.export.assets import validate_assets, source_hash
from app.errors import ApiError
def asset(source='flowchart LR\n A --> B'):
buf=BytesIO(); Image.new('RGB',(60,40),'blue').save(buf,'PNG')
return ExportAsset(kind='mermaid', source_hash=source_hash(source), png_base64=base64.b64encode(buf.getvalue()).decode())
@pytest.mark.parametrize('format',['html','pdf','docx'])
def test_static_mermaid_in_export(format):
async def run():
job=await exports.create_export(ExportRequest(source={'type':'markdown','markdown':'```mermaid\nflowchart LR\n A --> B\n```'},format=format,assets=[asset()],title='snapshot'))
finished=await exports.wait_for_export(job.job_id)
assert finished.status.value=='completed'
assert not any('mermaid' in w for w in finished.warnings)
data=exports.get_export_file(job.job_id).read_bytes()
if format=='html': assert b'data:image/png;base64,' in data
elif format=='pdf': assert b'/Subtype /Image' in data
else:
with ZipFile(BytesIO(data)) as archive: assert any(n.startswith('word/media/') for n in archive.namelist())
asyncio.run(run())
def test_asset_invalid_and_duplicate():
with pytest.raises(ApiError): validate_assets([asset().model_copy(update={'png_base64':'not png'})])
with pytest.raises(ApiError): validate_assets([asset(),asset()])
def test_stale_asset_does_not_replace_source():
from app.export.assets import attach_assets
from app.export.markdown import parse_document
document=parse_document('```mermaid\nflowchart LR\n X --> Y\n```')
attach_assets(document,validate_assets([asset()]))
assert 'static_png' not in document.children[0].attributes
@pytest.mark.parametrize('format',['html','pdf','docx'])
def test_math_and_local_image_export(format):
from app.config import get_settings
vault=get_settings().vault_path; vault.mkdir(parents=True)
Image.new('RGB',(100,50),'green').save(vault/'figure.png')
async def run():
job=await exports.create_export(ExportRequest(source={'type':'markdown','file_path':'demo.md',
'markdown':'Formula $\\frac{x^2}{2}$\n\n![figure](figure.png)'},format=format))
done=await exports.wait_for_export(job.job_id)
assert done.status.value=='completed'
assert not any('公式' in w or '图片' in w for w in done.warnings)
data=exports.get_export_file(job.job_id).read_bytes()
if format=='html': assert data.count(b'data:image/png;base64,')==2
if format=='docx':
with ZipFile(BytesIO(data)) as archive: assert len([n for n in archive.namelist() if n.startswith('word/media/')])==2
asyncio.run(run())
def test_local_image_path_escape_and_tex_fallback():
from app.export.assets import enrich_document
from app.export.markdown import parse_document
document=parse_document('![no](../outside.png)\n\n$\\unknownmacro{x}$')
warnings=enrich_document(document,'demo.md')
assert len(warnings)==2
@pytest.mark.parametrize('theme',['light','dark','sepia','paper-moments','ocean-blue','midnight-purple'])
def test_function_preview_theme_and_parser(theme):
from app.plot_routes import PlotRequest, preview
result=preview(PlotRequest(source='y = x^2\ny = sin(x)',theme_id=theme))
assert '<polyline' in result.result.content
assert 'nan' not in result.result.content
assert preview(PlotRequest(source='y = __import__("os")')).result is None
def test_agent_benchmark_real_runtime_offline_lifecycle():
from app.config import get_settings
from app.benchmarks import agent, service
from app.container import container
directory=get_settings().benchmark_datasets_path; directory.mkdir(parents=True,exist_ok=True)
(directory/'agent-test.json').write_text(json.dumps({'dataset_id':'agent-test','kind':'agent','version':'1', 'cases':[
{'case_id':'hello','prompt':'hello','output_contains':['definitely-absent'],'allowed_tools':[]}
]}),encoding='utf-8')
async def run():
request=AgentBenchmarkRequest(dataset_id='agent-test',provider_id='mock',model='mock-model',offline=True)
with pytest.raises(ApiError): await agent.create_run(request.model_copy(update={'offline':False}))
created=await agent.create_run(request)
done=await service.wait_for_run(created.run_id)
assert done.status==BenchmarkStatus.completed
report=service.get_report(created.run_id)
assert report.metrics['task_success_rate']==0
case=report.cases[0]
assert case.agent_run_id and container.agent.get_run(case.agent_run_id)
assert report.config_snapshot['execution']=='offline'
events=service.get_events(created.run_id)
assert [e.sequence for e in events]==list(range(len(events)))
assert sum(e.event.value.startswith('Run') and e.event.value!='RunStarted' for e in events)==1
second=await agent.create_run(request); service.cancel_run(second.run_id)
assert (await service.wait_for_run(second.run_id)).status==BenchmarkStatus.cancelled
asyncio.run(run())
def test_agent_score_counts_duplicate_and_invalid_calls():
from types import SimpleNamespace as NS
from app.contracts import AgentDatasetCase
from app.benchmarks.agent import score,aggregate
case=AgentDatasetCase(case_id='x',prompt='x',allowed_tools=['math.add'],expected_tools=[{'name':'math.add','arguments':{'left':2}}])
events=[NS(event=NS(value='ToolCall'),data={'name':'math.add','arguments':{'left':2}}) for _ in range(2)]
run=NS(status=NS(value='completed'),tool_results=[NS(success=False,name='math.add',error_code='TOOL_ARGUMENT_INVALID')],output='',citations=[],run_id='r',current_step=2,token_usage=10,error_code=None)
result=score(case,run,events,10,0)
assert not result.success
assert aggregate([result])['tool_argument_accuracy']==.5
assert aggregate([result])['invalid_tool_call_rate']==.5
def test_local_embedding_cache_is_config_scoped_and_returns_copies(monkeypatch,tmp_path):
from app.local_models import runtime as local
from app.retrieval.provenance import capture_embedding
monkeypatch.setattr(local,'read_state',lambda key:{'status':'installed'})
monkeypatch.setattr(local,'interpreter',lambda config=None:Path(__file__))
monkeypatch.setattr(local,'model_path',lambda key:tmp_path/key)
calls=[]
async def infer(*args,**kwargs):
calls.append(args);return [[.5]*384]
monkeypatch.setattr(local.runtime,'infer',infer)
async def run():
embedding=local.LocalEmbedding(local.RuntimeConfig())
first=await embedding.embed_documents(['query'])
first[0][0]=999
with capture_embedding() as observation:
second=await embedding.embed_documents(['query'])
assert second[0][0]==.5 and observation['query_embedding_cache']=='hit'
assert len(calls)==1
await local.LocalEmbedding(local.RuntimeConfig(version=2)).embed_documents(['query'])
assert len(calls)==2
asyncio.run(run())
def test_preview_http_and_agent_benchmark_validation():
from fastapi.testclient import TestClient
from app.main import app
with TestClient(app) as client:
response = client.post('/api/plots/function', json={'source':'y = sin(x)', 'theme_id':'dark'})
assert response.status_code == 200 and '<polyline' in response.json()['result']['content']
assert client.post('/api/plots/function', json={'source':'x'*20001}).status_code == 422
bad = client.post('/api/benchmarks/agent/runs', json={
'dataset_id':'missing', 'provider_id':'missing', 'model':'missing'})
assert bad.status_code == 404
assert client.get('/api/benchmarks/runs/missing/report').status_code == 404
schema = client.get('/openapi.json').json()
assert '/api/benchmarks/agent/runs' in schema['paths']
def test_preview_rejects_aggregate_complexity_before_sampling():
from app.plot_routes import PlotRequest, preview
source = '\n'.join('y = '+ '+'.join(['(x+x)']*150) for _ in range(16))
result = preview(PlotRequest(source=source))
assert result.result is None
assert result.diagnostics[0].code == 'PLOT_BUDGET_EXCEEDED'
def test_repeated_static_assets_share_document_resource_budget():
from app.export.assets import attach_assets, enrich_document
from app.export.markdown import parse_document
document = parse_document(('```mermaid\nflowchart LR\n A --> B\n```\n\n')*65)
attach_assets(document, validate_assets([asset()]))
warnings = enrich_document(document)
assert sum(bool(node.attributes.get('static_png')) for node in document.children) == 64
assert any('预算' in warning for warning in warnings)
@pytest.mark.parametrize('order', [(1, 2), (2, 1)])
def test_agent_parameter_matching_is_independent_of_call_order(order):
from types import SimpleNamespace as NS
from app.benchmarks.agent import score
from app.contracts import AgentDatasetCase
case = AgentDatasetCase(case_id='overlap', prompt='test', allowed_tools=['math.add'],
expected_tools=[{'name':'math.add','arguments':{}}, {'name':'math.add','arguments':{'left':1}}])
events = [NS(event=NS(value='ToolCall'), data={'name':'math.add','arguments':{'left':value}}) for value in order]
run = NS(status=NS(value='completed'),tool_results=[],output='',citations=[],run_id='test',current_step=1,token_usage=0,error_code=None)
result = score(case, run, events, 1, 0)
assert result.success and result.accurate_calls == result.selected_calls == 2
# Two expectations cannot reuse one matching call.
result = score(case, run, events[:1], 1, 0)
assert not result.success and result.accurate_calls == 1
@pytest.mark.parametrize('page_size', ['A4', 'Letter'])
@pytest.mark.parametrize('dimensions', [(200, 2000), (2000, 200)])
def test_docx_static_images_fit_both_page_dimensions(page_size, dimensions):
from app.export.markdown import parse_document
from app.export.exporters.docx import DocxExporter
from app.contracts import ExportOptions
from docx import Document
png = BytesIO(); Image.new('RGB', dimensions, 'white').save(png, 'PNG')
document = parse_document('```mermaid\nflowchart TD\n A-->B\n```')
document.children[0].attributes['static_png'] = png.getvalue()
result = DocxExporter().render(document, ExportOptions(page_size=page_size))
word = Document(BytesIO(result.content)); section = word.sections[0]; shape = word.inline_shapes[0]
assert shape.width <= section.page_width - section.left_margin - section.right_margin
assert shape.height < section.page_height - section.top_margin - section.bottom_margin
assert shape.width / shape.height == pytest.approx(dimensions[0] / dimensions[1], rel=1e-5)
+4 -3
View File
@@ -107,7 +107,7 @@ def test_render_svg_contains_polyline_and_axes() -> None:
assert "<line" in svg # 坐标轴/网格
assert "<script" not in svg
assert rendered.width == 640
assert rendered.height == 480
assert rendered.height == 504 # Includes the legend row.
def test_render_svg_multiple_functions() -> None:
@@ -300,7 +300,7 @@ def test_function_plot_static_renderer_renders_svg() -> None:
assert "<polyline" in result.content
assert result.mime_type == "image/svg+xml"
assert result.width == 640
assert result.height == 480
assert result.height == 504 # Includes the legend row.
def test_function_plot_static_renderer_parse_exposes_node_count() -> None:
@@ -359,7 +359,8 @@ def test_render_reportlab_builds_drawing() -> None:
kinds = {type(c).__name__ for c in drawing.contents}
assert {"Line", "PolyLine", "String", "Group"} <= kinds
strings = [c for c in drawing.contents if isinstance(c, String)]
assert any(s.fontName == "STSong-Light" for s in strings)
from app.export.fonts import FONT
assert any(s.fontName == FONT for s in strings)
assert any(s.text == "时间" for s in strings)
# ylabel 在旋转 Group 内
groups = [c for c in drawing.contents if isinstance(c, Group)]
+656
View File
@@ -1,6 +1,10 @@
version = 1
revision = 3
requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version < '3.12'",
]
[[package]]
name = "annotated-doc"
@@ -314,6 +318,89 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "contourpy"
version = "1.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
{ url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
{ url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
{ url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
{ url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
{ url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
{ url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
{ url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
{ url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
{ url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
{ url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
{ url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
{ url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
{ url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
{ url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
{ url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
{ url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
{ url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
{ url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
{ url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
{ url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
{ url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
{ url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
{ url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
{ url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
{ url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
{ url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
]
[[package]]
name = "cryptography"
version = "50.0.1"
@@ -370,6 +457,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" },
]
[[package]]
name = "cycler"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
[[package]]
name = "fastapi"
version = "0.141.1"
@@ -386,6 +482,148 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
]
[[package]]
name = "fonttools"
version = "4.64.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/41/0f072a712dc74496e03710e462a18a4cfd8a258ad055a4e22d28b43a7abd/fonttools-4.64.0.tar.gz", hash = "sha256:ecb2e59a7bc692fee64dda6010deb66222335693b30046f15cccf81233aa715f", size = 3664266, upload-time = "2026-08-31T15:44:33.685Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/f7/a222d1e20d460a09d08fe0b612a6f373235168c6c3228ff6a913cc8ff9ea/fonttools-4.64.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dac25768be4c03a990c359f408cb7e8958ed0e93061e495b3642ce7909761205", size = 3087538, upload-time = "2026-08-31T15:42:26.68Z" },
{ url = "https://files.pythonhosted.org/packages/74/fd/571331ac2b9ea43403ba43f27f5b45427a13b5c559d4379c99f3f9437b59/fonttools-4.64.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d652592c71683941b768306fa1c7c6ce1bb9b072505043feafe86305d71030b7", size = 2582847, upload-time = "2026-08-31T15:42:28.664Z" },
{ url = "https://files.pythonhosted.org/packages/dd/bc/583ad5e4d6fbc600b1d9eb3c2e8b4eac5ca1c204fa29e4ef0ff344d8aa60/fonttools-4.64.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:801fd04899d72eab34f02ab78d0451525621b3bd589da9d2d480dfffe951b643", size = 5493826, upload-time = "2026-08-31T15:42:30.991Z" },
{ url = "https://files.pythonhosted.org/packages/f8/3d/4edf079bdb01791abe87753b7c8fdca4e8ec709276e7b030a1aa84a88a51/fonttools-4.64.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff7aff4637fbf71394df139c63ccfe08a47aa4252d2f91224ddb3335c716c925", size = 5455609, upload-time = "2026-08-31T15:42:33.149Z" },
{ url = "https://files.pythonhosted.org/packages/7e/b6/e36ea109c8cfdb0f174c09d8464b4ddf6b446f97313118d69b6a28cd7189/fonttools-4.64.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f521d79d6acda4923b264805541696f452079db0952a5bb96f9ff742f50629ec", size = 5460840, upload-time = "2026-08-31T15:42:35.423Z" },
{ url = "https://files.pythonhosted.org/packages/6c/bf/0d8bb1fbb96c621c1da5f93e288bec8424802df10265e790dddc66626b1f/fonttools-4.64.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a0afa8bac675445dc0e2ba2891ecbedd9be89cb437afa94c823e0290cc2c4bc5", size = 5591341, upload-time = "2026-08-31T15:42:37.874Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e1/6f8a1a01e5ed4408fdffb934c77ce65d978a2df6fd6eaaec5e6b864b10f5/fonttools-4.64.0-cp311-cp311-win32.whl", hash = "sha256:c3c1fb656063a2f762db5378ea8d38ad5f7836b4f3fb8c4652270ded43df2935", size = 2439647, upload-time = "2026-08-31T15:42:40.196Z" },
{ url = "https://files.pythonhosted.org/packages/21/41/b575f14a653911f33f17ef62bbeaa818c94bc7e694579cc9ddec6935d5b2/fonttools-4.64.0-cp311-cp311-win_amd64.whl", hash = "sha256:e63b63b8b5fdb8e29318dff2b15c5f852be46e972775b466f75b848f6eed4502", size = 2497436, upload-time = "2026-08-31T15:42:42.36Z" },
{ url = "https://files.pythonhosted.org/packages/82/23/4ea251977fef70ed14193785e1b2949355f1f5927dc0ef1dada675c0bbb2/fonttools-4.64.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ecb2b206b5b2386f6968721a0770226b66bdd54adc4279bfff3ddf62873eed8", size = 3095501, upload-time = "2026-08-31T15:42:44.299Z" },
{ url = "https://files.pythonhosted.org/packages/c4/32/943e9034f49797e1a25dbbd60c8047ce0c38c3585c5d1b2db38ce64059c5/fonttools-4.64.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8c631303bb1fd7be3067c47536a30ff1fcb4846d6008c112bc52a03f7cd6965", size = 2583091, upload-time = "2026-08-31T15:42:46.57Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4b/332cb3105d5d550cb5728e669151601c4f7698cbedafd134154eb806ef83/fonttools-4.64.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2763e452b025ee8e990f0462e76052de9bb094ebc21d296f62c6dfe958886b4", size = 5423449, upload-time = "2026-08-31T15:42:48.615Z" },
{ url = "https://files.pythonhosted.org/packages/dc/8f/e5f2906ea833d362916c64a2f6b1922a07b19442744fd3cd89563f429bff/fonttools-4.64.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:66a83f93579fb3493e458c4449d1d566a7b2a1c7b19915cd0fa3c9b8b5a8540b", size = 5400981, upload-time = "2026-08-31T15:42:50.862Z" },
{ url = "https://files.pythonhosted.org/packages/fe/46/1b325ebb20aef8bb05f803052ea65931d73638ae3e0d02a6658e3d14e0f8/fonttools-4.64.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf67f96dc0bfe9607f5f2b734cedfbe2f6f995231adee4ccefa12872044d452d", size = 5359588, upload-time = "2026-08-31T15:42:53.341Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d3/ac56fa880e01339aa6ad0bcb457cc30bb4d6c5ca79922e1bd650e4a3a396/fonttools-4.64.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6786bed88581e19bc4f28ea7a64ad531e8f54acf50327fddca942688824a60bd", size = 5520698, upload-time = "2026-08-31T15:42:55.919Z" },
{ url = "https://files.pythonhosted.org/packages/d7/40/1251fef04c308836a3a7db523703a7ca8ab010dc1e3b9a9bf08e0861c7a8/fonttools-4.64.0-cp312-cp312-win32.whl", hash = "sha256:da4c9bdeaf6b06c12d13d0addfc8ef15aa9695d26574a6dc10751258bef72f30", size = 2430689, upload-time = "2026-08-31T15:42:58.48Z" },
{ url = "https://files.pythonhosted.org/packages/6f/1c/ce0e89183c0235ff6cfbf0603d593023a5afa5333efb35ff569cc0be9ce2/fonttools-4.64.0-cp312-cp312-win_amd64.whl", hash = "sha256:06b6409b868494556a831ae33b2d9a090476c37516b38d70f45a9720b460d423", size = 2482071, upload-time = "2026-08-31T15:43:00.598Z" },
{ url = "https://files.pythonhosted.org/packages/44/7b/49b0054a79b9ed918c018e70e09b56eb5678ee8b44e59e126c79de4d8d73/fonttools-4.64.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9443eefff58aad558608f352092e1be6d278980e8c3b4e8621fcbfda97818500", size = 3092750, upload-time = "2026-08-31T15:43:02.705Z" },
{ url = "https://files.pythonhosted.org/packages/83/e2/08e73bd2f6e6248f071d3e9debe4c2b4cecda3a7dc057c56b44e255421c3/fonttools-4.64.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09657817b75575822bcd6098ef0ebf0386f34430839ee53109e70fd40a7f6539", size = 2583001, upload-time = "2026-08-31T15:43:04.923Z" },
{ url = "https://files.pythonhosted.org/packages/2f/58/6dc0ff0963fc1e12f53bac59175617ec5927342ee7a4ffb871b44d56d81b/fonttools-4.64.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d7995b906666037d7114c20a5566a372902747452af7d5bd4cd6bca8f1a2550", size = 5392765, upload-time = "2026-08-31T15:43:07.096Z" },
{ url = "https://files.pythonhosted.org/packages/7a/dd/4d3911049680da7b3aecf97cb5094e3a05554f5d94829358fc7664640456/fonttools-4.64.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c42237b7e8c6813643e57d3efed3be094d4c06339dc2166b626e2cc5c12ee93", size = 5373969, upload-time = "2026-08-31T15:43:09.617Z" },
{ url = "https://files.pythonhosted.org/packages/48/0a/a2cf94121fd3ca9166bdb4863f71d6db32a5606b223c81e2ba99832a0612/fonttools-4.64.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:498f02ea92c9ca18c0f9c581ea93184a9d56c25b0af14189b0767adaf34235d8", size = 5333854, upload-time = "2026-08-31T15:43:11.985Z" },
{ url = "https://files.pythonhosted.org/packages/44/e2/632f4a8b94e6d7eea41e6d82f0ed337f64f5fb96f9910e2ac6b1689910e2/fonttools-4.64.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8252f20108e557532f91d7d6dd9af87c16ed6fa930f65516aa480fa2cfed3363", size = 5492828, upload-time = "2026-08-31T15:43:14.288Z" },
{ url = "https://files.pythonhosted.org/packages/ff/25/7fee1978fdedc1a2d978b76dddbea88785416f5774e7222327b867712d71/fonttools-4.64.0-cp313-cp313-win32.whl", hash = "sha256:45e3ecc3888f1637094fd75cd8fc727f3a4b06d1ddf89181126c071e244fd2a5", size = 2429011, upload-time = "2026-08-31T15:43:16.463Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ed/bb1c217c9e1fbfa59f42b266af57df937a703882a73e59a309773752228f/fonttools-4.64.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4812f71c39d77ec5041348dafa400532adf7bf8f1fffa9aa6495fce5876d7b8", size = 2480199, upload-time = "2026-08-31T15:43:18.774Z" },
{ url = "https://files.pythonhosted.org/packages/a0/0a/59b9074b8ab165cc28d3e08bcf7a8eff8e1dc5932a5a41c87b15952fa3af/fonttools-4.64.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6f1ce9ef9a1b13098efdc2e43a2ed96d9851bbde7b31c652a87552c4efe9b422", size = 3096790, upload-time = "2026-08-31T15:43:21.125Z" },
{ url = "https://files.pythonhosted.org/packages/17/39/af1077a36feefe79f36d67d75aee637a96673c0a74eeb2c6f24266f5a20b/fonttools-4.64.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83cc48d1411d2ff388dab99973dca81172cc9ceae9c9799da9548d494cfb38cb", size = 2584288, upload-time = "2026-08-31T15:43:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/00/c4/1ea58af0eb78264d28e5871bba0810bf0943cd93d641fdeae92553ad3410/fonttools-4.64.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e412767d1c9765cf1b82f7b00f1686c6ca5809ebb77af363b3f9f2325a465c01", size = 5378061, upload-time = "2026-08-31T15:43:25.193Z" },
{ url = "https://files.pythonhosted.org/packages/93/99/5c7e36d770407b66f7fc8378d2bb47f1530d083d8b9247ff011ec9b4dd70/fonttools-4.64.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b4a7af455ffed980925bc0ebf5b8d6239e6c3e797d9d755b6db192fb3080d614", size = 5319458, upload-time = "2026-08-31T15:43:27.696Z" },
{ url = "https://files.pythonhosted.org/packages/a8/09/89e8d600e92723309d4ddd3944c4ca565197a5df0236ae4115ccbe797375/fonttools-4.64.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:398b14f89ca950b288bd290875f07e4e10685644fa4ac668546fb107b1ada4d4", size = 5316656, upload-time = "2026-08-31T15:43:29.842Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b3/56719ab37e1592ceac89724574146622bedec036a207be80f3c7b1c14cbe/fonttools-4.64.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dc96150f99e05a317cb1f042b92c4cf8bc93cdb1f9f85717322e202ecdf2e505", size = 5448380, upload-time = "2026-08-31T15:43:32.311Z" },
{ url = "https://files.pythonhosted.org/packages/3b/b0/15951b7e3006073f3260da4f8983732d18903905cfe26daa5138a0bbf682/fonttools-4.64.0-cp314-cp314-win32.whl", hash = "sha256:1c3661324f3f0fa4539a32288a3e0711a5f3ccf020036e760bb558ae9811a16f", size = 2432778, upload-time = "2026-08-31T15:43:34.421Z" },
{ url = "https://files.pythonhosted.org/packages/8e/f3/0c97402b29411f4f6d31b32f2af5b4bcd5babe5a93c64558a8fd3654f140/fonttools-4.64.0-cp314-cp314-win_amd64.whl", hash = "sha256:043f6c572bf236f2a76e762c25f841daea11e8fc03e78088d7be66e0c5b4e4c0", size = 2485394, upload-time = "2026-08-31T15:43:36.506Z" },
{ url = "https://files.pythonhosted.org/packages/00/da/82192c7bee5314f04129a068dddae11082a63295d4e3b5ce08657de96608/fonttools-4.64.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4691a122b8c1d0d82d6e7510ce59d5c42146518240274b53e912e255573924f7", size = 3170118, upload-time = "2026-08-31T15:43:38.579Z" },
{ url = "https://files.pythonhosted.org/packages/42/2d/0b4f608d754f625fecc7d94b3b26af6f65f6ab4527b386bb0c6141cdd9ea/fonttools-4.64.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3200180abc69639483cf54a17cca2e13c31ede5f665979ea0a9c829d093f372f", size = 2617333, upload-time = "2026-08-31T15:43:40.934Z" },
{ url = "https://files.pythonhosted.org/packages/5e/df/9f7448c38dea05458acfee81c443b14ef97d6d66c636af053d41cf8a32dd/fonttools-4.64.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53eee22af5b5a305c1ee2652955ed46b148e881456fcec1e7f0eb27f642f6bb4", size = 5542345, upload-time = "2026-08-31T15:43:43.114Z" },
{ url = "https://files.pythonhosted.org/packages/b1/e8/cbf4a81e8be322bc4bee4ba31cd33a7b6336ef8030fa13b53b3e9c09fbf9/fonttools-4.64.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08f172961e11f4eb4f80f2f20049e09b0ea8e044fa6d456fed8346eb8588f360", size = 5349981, upload-time = "2026-08-31T15:43:45.363Z" },
{ url = "https://files.pythonhosted.org/packages/d8/dc/106fd9e93e962dbb60482d30c0913af3d108f5762429dfe93265a850bdc5/fonttools-4.64.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6eae4376adb104c2acfa76fd9ea0cb12b572ca1d70eceac709871f638ff76e93", size = 5409249, upload-time = "2026-08-31T15:43:47.817Z" },
{ url = "https://files.pythonhosted.org/packages/b0/2d/c7be990abf74c9c5c5367ae3dc65953ffce49fe00f62abd129cf725a2397/fonttools-4.64.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2730946ca8f12c356bd98eb9b2b095c8e761ed05bed5afb0d5b380cebe4f6370", size = 5443781, upload-time = "2026-08-31T15:43:49.909Z" },
{ url = "https://files.pythonhosted.org/packages/9b/cc/bffec3dccb9e3f4f6097a2baedaef5a8dec2cb816522ee163e4bb7f54b44/fonttools-4.64.0-cp314-cp314t-win32.whl", hash = "sha256:d30c966bea2deffa19c738c81776f7182da5ccabd97e666bae4f3d6ba87341d9", size = 2466542, upload-time = "2026-08-31T15:43:52.088Z" },
{ url = "https://files.pythonhosted.org/packages/96/49/e31f97dfc94e0648999f04bfccb37a97062d978f87df60654f496942c89f/fonttools-4.64.0-cp314-cp314t-win_amd64.whl", hash = "sha256:917fd520bb60809d83c14d43cfe48d5ad2516abaf2c073d65a431800dade2d29", size = 2516703, upload-time = "2026-08-31T15:43:53.954Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ec/e39b9e56db7dcc859983caa24cf2a36209dd71f6cc8ddbbf00d7342d7988/fonttools-4.64.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:8dd18fdff0ac9759b8d67a714730abee07b2312e3656c20ba5affb0107094762", size = 3091150, upload-time = "2026-08-31T15:43:56.309Z" },
{ url = "https://files.pythonhosted.org/packages/a8/e1/3e6a409d6f99efb66c2f6d0a71986432ff9cc9aca0df9ff4a8bed1d850f6/fonttools-4.64.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:5af87d1a6d247d7467ee082ae977a5443b2c45f8cd4d59375b6daa38d523c2de", size = 2582833, upload-time = "2026-08-31T15:43:58.348Z" },
{ url = "https://files.pythonhosted.org/packages/91/7e/e8aec0e6eaf93267450535c1be9c9ae63e1d9c49659c2ffdb52c19982114/fonttools-4.64.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:769fb64412ca237547ca73f111a64252d9e32c9d938bed51ed537bc9146a8f54", size = 5375327, upload-time = "2026-08-31T15:44:00.417Z" },
{ url = "https://files.pythonhosted.org/packages/61/d1/fa9ce5c1c3c0ef858a2cbe5632e9b6b137deca010b28d5d9a677e4aa89a7/fonttools-4.64.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e662f874ab2c7da9861584db44a13573e0936df087215f63013138f6e5eba083", size = 5338647, upload-time = "2026-08-31T15:44:02.62Z" },
{ url = "https://files.pythonhosted.org/packages/0d/13/e4d4fa3c166f7f5a8f9b6403ad9dcb3887cd05f8b9d2638bd2b43d2889ef/fonttools-4.64.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2524a26f8fdb9051b0d778d052f5d238285ca9f91a7dc004514c7d6cf38d35f4", size = 5313557, upload-time = "2026-08-31T15:44:05.037Z" },
{ url = "https://files.pythonhosted.org/packages/91/c8/5a352d69608ea7606f677080ede971090fc7340b9f143c0b4fa00cfbf63e/fonttools-4.64.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1e4e84b47839d35be24dbf476845a34f2ccf99707b66df125c1c414d3e86d25d", size = 5462363, upload-time = "2026-08-31T15:44:07.13Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f1/b15b845f66b559a24a9c40859391bbd3048af3ac0fd04e45b57dca11c4c7/fonttools-4.64.0-cp315-cp315-win32.whl", hash = "sha256:be084d19a3ac0c8b2aba696680642d703118d3b1f18cf83f5b7dbaf0ffc62ab6", size = 2431682, upload-time = "2026-08-31T15:44:09.293Z" },
{ url = "https://files.pythonhosted.org/packages/c6/b2/909beff0d2e2e1adac896e36cff7f9d76fd154bcd57e303d8f09aee8f72b/fonttools-4.64.0-cp315-cp315-win_amd64.whl", hash = "sha256:de8acaa5f4160f537a3cf41b031171d51004b9f4aebfa6c194f18dffa9533d03", size = 2484393, upload-time = "2026-08-31T15:44:11.117Z" },
{ url = "https://files.pythonhosted.org/packages/40/47/06a51becf651cc071daa88e18ecb9f45ace9e3a570d8191ed8b3cae353fa/fonttools-4.64.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:5b90ad6637237b636d15c9ae8b7c4a7a1c194f33def378677e468c13fd4542f8", size = 3162235, upload-time = "2026-08-31T15:44:13.182Z" },
{ url = "https://files.pythonhosted.org/packages/dd/96/8b3faf58fd7ec3bb11943126658ad15b3f385068b23248deffccf2327ac1/fonttools-4.64.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fa75c7970bc6bca340cc6e20f20f069201bfcb50094c31a536fd99724d1d01ca", size = 2613507, upload-time = "2026-08-31T15:44:15.123Z" },
{ url = "https://files.pythonhosted.org/packages/bb/3a/223d1437e72d79f1549be7bafb3cc08f397e1d9c354a241a8ec8573b7d33/fonttools-4.64.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:236e59bc7e2a63557a4d7b013f9cb9e28d9aebc45bc09f85e545e6bf091db626", size = 5518005, upload-time = "2026-08-31T15:44:17.359Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9f/6f1a4b40ae533b9e907b7e627e397be632d0d9fd6f6be7756cf458870279/fonttools-4.64.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a515f664cad988f2295056833a59f62220bc3e46afdaffe389a29060f6712355", size = 5341654, upload-time = "2026-08-31T15:44:19.869Z" },
{ url = "https://files.pythonhosted.org/packages/52/f6/9ca30ba98730a22b527bcc1e02034f0b13b3be2ede764bb0fe8b8e0408d6/fonttools-4.64.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:5bfdaada437e7730c17d366bd7bb8c4a16639963ddbfc1b2f302a68a17a290e7", size = 5385994, upload-time = "2026-08-31T15:44:21.979Z" },
{ url = "https://files.pythonhosted.org/packages/09/27/baf1b61bff983bfec72cbb7b32162c4b68d76dc824aaaa48a1c26fa10b6b/fonttools-4.64.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c60be0aed97a32c6ba8cee21f0d0477136e495451bd97910f589ac892db120d4", size = 5432407, upload-time = "2026-08-31T15:44:24.469Z" },
{ url = "https://files.pythonhosted.org/packages/21/ff/2eab37d43f2e2ccc0993959d8f2605d9a68470082c592684ac98611ee70a/fonttools-4.64.0-cp315-cp315t-win32.whl", hash = "sha256:f8669ce37851b597d3435b91fefa51139e58d506ca449ca0e5bb68c63b8b6d2b", size = 2463614, upload-time = "2026-08-31T15:44:27.198Z" },
{ url = "https://files.pythonhosted.org/packages/dc/8d/8bdff3ea656592197c8ef5062774221885468ffa01d8b0dc782dae23a83a/fonttools-4.64.0-cp315-cp315t-win_amd64.whl", hash = "sha256:89356c0793b474af7e49ec90d39fb2363e2341516a90460e38231df5ebe8acd5", size = 2512422, upload-time = "2026-08-31T15:44:29.478Z" },
{ url = "https://files.pythonhosted.org/packages/82/f8/7188153c4b265c899cd035de6a062677d51f67118a4ba640902bd9683e90/fonttools-4.64.0-py3-none-any.whl", hash = "sha256:4a05783ff54ce4c7a28f18e5772efdf63c219374bd9ffc55452182e1cef8be60", size = 1195327, upload-time = "2026-08-31T15:44:31.741Z" },
]
[[package]]
name = "greenlet"
version = "3.5.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" },
{ url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" },
{ url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" },
{ url = "https://files.pythonhosted.org/packages/39/82/5c873dbb4fb001d22fbbd50e80d4c1b0181ddae106856132160f84b94e88/greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c", size = 633329, upload-time = "2026-08-10T14:30:06.062Z" },
{ url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" },
{ url = "https://files.pythonhosted.org/packages/70/12/f7df98e72a8eb4a7edfec5a08d6d1a4ab53a52c95ba0b2ea6c10b8dd9bd0/greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f", size = 428145, upload-time = "2026-08-10T14:30:01.071Z" },
{ url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" },
{ url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" },
{ url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" },
{ url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" },
{ url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" },
{ url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" },
{ url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" },
{ url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" },
{ url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" },
{ url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" },
{ url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" },
{ url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" },
{ url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" },
{ url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" },
{ url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" },
{ url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" },
{ url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" },
{ url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" },
{ url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" },
{ url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" },
{ url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" },
{ url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" },
{ url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" },
{ url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" },
{ url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" },
{ url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" },
{ url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" },
{ url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" },
{ url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" },
{ url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" },
{ url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" },
{ url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" },
{ url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" },
{ url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" },
{ url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" },
{ url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" },
{ url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" },
{ url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" },
{ url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" },
{ url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" },
{ url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" },
{ url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" },
{ url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" },
{ url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" },
{ url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" },
{ url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" },
{ url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" },
{ url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" },
{ url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" },
{ url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" },
{ url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" },
{ url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" },
{ url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" },
{ url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" },
{ url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -511,6 +749,136 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
[[package]]
name = "kiwisolver"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/7b/2de6908edc668427c149af5f93112e931f87e1fa4cab80bac32c5844dccc/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6", size = 123364, upload-time = "2026-08-28T10:25:04.359Z" },
{ url = "https://files.pythonhosted.org/packages/8a/24/e70914415c77c97be7e22c80a0740869cb7428768cc380fdcdf6703e7084/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55", size = 66558, upload-time = "2026-08-28T10:25:05.506Z" },
{ url = "https://files.pythonhosted.org/packages/e8/2b/8b08b11833db4d475b8ef1f36174f8d8a7abd31bedd7e794be78e8814b48/kiwisolver-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839", size = 64071, upload-time = "2026-08-28T10:25:06.7Z" },
{ url = "https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720", size = 1438206, upload-time = "2026-08-28T10:25:08.254Z" },
{ url = "https://files.pythonhosted.org/packages/c0/05/c941a139f27438c1910d630fdc3ccfdab7c8407c72052299ead12ece086e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:719a35fa1156db3640555f95ebb94f60a444e64d1c69626b0edef5df78eba225", size = 1248975, upload-time = "2026-08-28T10:25:10.053Z" },
{ url = "https://files.pythonhosted.org/packages/58/a1/2669ee5512e39b9d4de25faacaedf788c957f93730c5f7c63993ec4f5933/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febcce10f2bcdbb80b4ea919238a6a4ac13dbc4c7cadbe8d5d75c3682f8b5404", size = 1266301, upload-time = "2026-08-28T10:25:11.754Z" },
{ url = "https://files.pythonhosted.org/packages/28/b8/353f52f2c7f861a9e90cd2e8f90f85b3ad03060835f823e08298d094c463/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1d852545c4d0e35a72728d072cbaa59e2fa7dd84bdf01e068d670dd0ceb58eb6", size = 1319708, upload-time = "2026-08-28T10:25:13.559Z" },
{ url = "https://files.pythonhosted.org/packages/21/0e/14b83200eadc2c1d63b76bac01c1813bf072aecf567429f303e00b70258e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:2e10ae1bba1899188b33557c10d73affcc12033edd18adddb57d209039976a4c", size = 971720, upload-time = "2026-08-28T10:25:14.934Z" },
{ url = "https://files.pythonhosted.org/packages/86/91/9d43d84d23b1cbff72a142d387ead1ea03db0cba8ff86ed5335addad3cc9/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b69602970994a2ed8bbfa78c2f0394a7435226c6040489702d9f0a0ad0c07052", size = 2200119, upload-time = "2026-08-28T10:25:16.636Z" },
{ url = "https://files.pythonhosted.org/packages/ff/7c/f2bd9616f27ffb5e17cecc0baa5d0bbcee7e55aeddc0ccc871d69e2fc3ee/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d50de98e8d807dc31822fff96f50293163a62418eb65487a21b42713d72ed0b7", size = 2295005, upload-time = "2026-08-28T10:25:18.374Z" },
{ url = "https://files.pythonhosted.org/packages/ba/17/ee671b72bf8f46a08379d4392c65582541759a542428197562f2898294ad/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3221f78211074f561c44ca42eac0619828171bec15a2c4cf6f7747d07df76e8e", size = 1960982, upload-time = "2026-08-28T10:25:19.893Z" },
{ url = "https://files.pythonhosted.org/packages/2e/f7/0e26b4c05bee3bdb0f048dfa305e4fe701999ea17b51e9c616ef91035bbe/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0ba9527afc80ae3d7814ed98b6572d02bf85eaf48065678342c5f0c6dab7a8c7", size = 2464918, upload-time = "2026-08-28T10:25:21.65Z" },
{ url = "https://files.pythonhosted.org/packages/ae/62/6eb431133d30ce656ac1e5ff72fac70dd34d54c3984f4011b9ac8bf77d54/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12dfea7f5fc2a34a9080efbf79c4c44eb380ec5b9c6fea09407e08f0d1e941d", size = 2270967, upload-time = "2026-08-28T10:25:23.643Z" },
{ url = "https://files.pythonhosted.org/packages/bf/9f/6f9e489c188200e6fb3193935501894811e8c97577c8ffe9033589bf3521/kiwisolver-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071", size = 70744, upload-time = "2026-08-28T10:25:25.061Z" },
{ url = "https://files.pythonhosted.org/packages/c6/6d/dfc430d1d43957061599adea3f08ea982bb6f4ab601a8c974bedcf2ba850/kiwisolver-1.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3", size = 68404, upload-time = "2026-08-28T10:25:26.186Z" },
{ url = "https://files.pythonhosted.org/packages/6b/9b/65b302742389c6f96f2956bef5decf26011309feb2fc5d79613af18adea4/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", size = 123876, upload-time = "2026-08-28T10:25:27.44Z" },
{ url = "https://files.pythonhosted.org/packages/71/74/c21f339956f6f691b2ed7e31d5f3ae767304df6c460192739fc830853051/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", size = 66487, upload-time = "2026-08-28T10:25:28.728Z" },
{ url = "https://files.pythonhosted.org/packages/84/e5/bdb34e21523e01dceda064d63713f3bdec91388af24fba1eca7ea5e85864/kiwisolver-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", size = 64660, upload-time = "2026-08-28T10:25:30.071Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", size = 1477929, upload-time = "2026-08-28T10:25:31.495Z" },
{ url = "https://files.pythonhosted.org/packages/6f/35/09c58daac34e6f6ea5c6dee0094b422118e5a7c265586008a95fd135ac5f/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", size = 1278499, upload-time = "2026-08-28T10:25:33.375Z" },
{ url = "https://files.pythonhosted.org/packages/19/32/739765e24fbad29d13f83e546ea4abc215a78cea9d677ca09025b027724d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", size = 1296677, upload-time = "2026-08-28T10:25:35.059Z" },
{ url = "https://files.pythonhosted.org/packages/df/32/03304d1010e2cc45e5b3b52cef7e43fed3a2a5cd6c87a89b4a88e1d85b5d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", size = 1346037, upload-time = "2026-08-28T10:25:36.705Z" },
{ url = "https://files.pythonhosted.org/packages/3e/57/4c49377bfd274450dd72ecaa13eaac32ea804a03363e4d1db0c5aa999ceb/kiwisolver-1.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", size = 988248, upload-time = "2026-08-28T10:25:38.299Z" },
{ url = "https://files.pythonhosted.org/packages/cb/c3/38df144a08b6c5d75ca4504e5cc3141bb3bfef64c04f4ef48204f42711b6/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", size = 2228722, upload-time = "2026-08-28T10:25:40.038Z" },
{ url = "https://files.pythonhosted.org/packages/e7/11/3221838a89cd64d9b386353e000cd8a296069a20fbe3584507fdfd5bebae/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", size = 2325216, upload-time = "2026-08-28T10:25:41.699Z" },
{ url = "https://files.pythonhosted.org/packages/83/d4/075c219230697bb5db910d37262b9bacf880f92b4811a02ab81ed073a253/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", size = 1977689, upload-time = "2026-08-28T10:25:43.559Z" },
{ url = "https://files.pythonhosted.org/packages/bb/08/1d219c3c2dd960983d0d4da623d916e9de6385df2b0bab3d1af0e9b8fccc/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", size = 2491443, upload-time = "2026-08-28T10:25:45.242Z" },
{ url = "https://files.pythonhosted.org/packages/ba/d3/024208ec1079d273f1047468d1bdffbf38bb75b7b268090fd3a0301b9d9a/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", size = 2295200, upload-time = "2026-08-28T10:25:46.984Z" },
{ url = "https://files.pythonhosted.org/packages/6e/7c/7b210498f9f92e1cd7855f260fa69ef056881087b199ee20c208f0e4189a/kiwisolver-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", size = 70748, upload-time = "2026-08-28T10:25:48.444Z" },
{ url = "https://files.pythonhosted.org/packages/94/61/ef0daa157c8bb23672f7423e0d14c39db1dc6ef8ed47e6bc54c9c1bef3bf/kiwisolver-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", size = 68324, upload-time = "2026-08-28T10:25:49.81Z" },
{ url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" },
{ url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" },
{ url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" },
{ url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" },
{ url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" },
{ url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" },
{ url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" },
{ url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" },
{ url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" },
{ url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" },
{ url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" },
{ url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" },
{ url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" },
{ url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" },
{ url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" },
{ url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" },
{ url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" },
{ url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" },
{ url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" },
{ url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" },
{ url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" },
{ url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" },
{ url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" },
{ url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" },
{ url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" },
{ url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" },
{ url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" },
{ url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" },
{ url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" },
{ url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" },
{ url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" },
{ url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" },
{ url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" },
{ url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" },
{ url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" },
{ url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" },
{ url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" },
{ url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" },
{ url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" },
{ url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" },
{ url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" },
{ url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" },
{ url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" },
{ url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" },
{ url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" },
{ url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" },
{ url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" },
{ url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" },
{ url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" },
{ url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" },
{ url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" },
{ url = "https://files.pythonhosted.org/packages/a3/a6/8fbecaf4fc18c02f31f05e47a84c010a80e3ec391ed2f0bdade1d62b5954/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", size = 124031, upload-time = "2026-08-28T10:27:16.193Z" },
{ url = "https://files.pythonhosted.org/packages/7c/c6/bf3090d2983b4204347cbdbe952116e7c3b2abf62b4e33e50167a13e75ee/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", size = 66489, upload-time = "2026-08-28T10:27:17.506Z" },
{ url = "https://files.pythonhosted.org/packages/85/de/562dddef55fdd7c291da8626d6619e72b5fc0870e6ccca0e149a5731e7f3/kiwisolver-1.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", size = 64806, upload-time = "2026-08-28T10:27:18.673Z" },
{ url = "https://files.pythonhosted.org/packages/89/b1/ba7b9c0164ce1cf62bf2872db63b8483289cf0f3110d6f9390eb09e409ed/kiwisolver-1.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", size = 1482211, upload-time = "2026-08-28T10:27:20.039Z" },
{ url = "https://files.pythonhosted.org/packages/13/dc/34da54efb4976616d45c20aae32d70e89d6e7395ed908029154d1609ef22/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", size = 1283739, upload-time = "2026-08-28T10:27:21.82Z" },
{ url = "https://files.pythonhosted.org/packages/5a/3a/30ffb62bee646e266e98a1b5cd276d9c75b6116fbfcb87c1190838c1b6df/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", size = 1301681, upload-time = "2026-08-28T10:27:23.876Z" },
{ url = "https://files.pythonhosted.org/packages/a9/8d/13c70be22a8506880b35fdc38dca36629613bc493405c79f4037f2cd2bb9/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", size = 1349159, upload-time = "2026-08-28T10:27:25.899Z" },
{ url = "https://files.pythonhosted.org/packages/82/0e/993972b8ec6767f47cd69818fb3a5ff14510557d29f7d1a839be7574fa1b/kiwisolver-1.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", size = 997613, upload-time = "2026-08-28T10:27:27.507Z" },
{ url = "https://files.pythonhosted.org/packages/36/82/ca26eddd2eda2420dfc56693449c1f821f78b485da9cbde9904c03af3f93/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", size = 2235109, upload-time = "2026-08-28T10:27:30.113Z" },
{ url = "https://files.pythonhosted.org/packages/c7/84/97d920881e10840b8d7c7185620298e3e4c88820b05514e3a15a258b08a6/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", size = 2331207, upload-time = "2026-08-28T10:27:32.795Z" },
{ url = "https://files.pythonhosted.org/packages/e2/ce/34d74b8f25acc58800f4c09268371e8d6159cf0f1206f1e4dc7835629b48/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", size = 1986696, upload-time = "2026-08-28T10:27:34.628Z" },
{ url = "https://files.pythonhosted.org/packages/0e/01/f892644014612527aef7031d3306a2ffc60b3cb044f802c1561f8e5e14f3/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", size = 2496400, upload-time = "2026-08-28T10:27:36.818Z" },
{ url = "https://files.pythonhosted.org/packages/32/6d/d8284e66e697026536e5f418b9cfe56567bffd3c775e3ecfbae373605854/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", size = 2302967, upload-time = "2026-08-28T10:27:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/46/0a/69a355e27f32ba50d5b6369949b6a1702e122f5277c89bc76d452b81c1c4/kiwisolver-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", size = 72283, upload-time = "2026-08-28T10:27:40.491Z" },
{ url = "https://files.pythonhosted.org/packages/ef/d8/7a95be90c33dcdd52204d4aa6384d731443225b887283bbd8b61e7931f6c/kiwisolver-1.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", size = 69860, upload-time = "2026-08-28T10:27:41.835Z" },
{ url = "https://files.pythonhosted.org/packages/31/f8/9bc493e7f5707788ba7f621902c68f82dc3a7ba03c78fbd337b026cef1ed/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", size = 126336, upload-time = "2026-08-28T10:27:43.379Z" },
{ url = "https://files.pythonhosted.org/packages/d1/82/3aea86b3f99712db825e9ac5631bf99571e818a8b8961ff98cebd798413e/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", size = 67698, upload-time = "2026-08-28T10:27:44.613Z" },
{ url = "https://files.pythonhosted.org/packages/84/7d/8daafc5d2e7f9c47a4f78f8865d86d2a9cf399c2a85f86c44a993594410c/kiwisolver-1.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", size = 65945, upload-time = "2026-08-28T10:27:45.932Z" },
{ url = "https://files.pythonhosted.org/packages/3d/c9/51dc974d9130da70a8c47a96160123443d387ffe1b6b833d6f91d9429339/kiwisolver-1.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", size = 1588030, upload-time = "2026-08-28T10:27:47.678Z" },
{ url = "https://files.pythonhosted.org/packages/e6/86/f3e1a730e7a995149d8d3ff9e313b6d8a17b2cf1d98a8eff139dc30463fb/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", size = 1390760, upload-time = "2026-08-28T10:27:49.676Z" },
{ url = "https://files.pythonhosted.org/packages/4f/3b/8ba25a2b5a0d2375e046f1b72de5179513f0be95aba6e7b094c89303929f/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", size = 1403279, upload-time = "2026-08-28T10:27:51.242Z" },
{ url = "https://files.pythonhosted.org/packages/18/d0/278d5cb8be812740027d5ca0a7eda0c375488a88d6dce0fa60fcc2591ad2/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", size = 1454429, upload-time = "2026-08-28T10:27:53.213Z" },
{ url = "https://files.pythonhosted.org/packages/94/37/bcbab41063ec284c1d200efe5087cf087798c2f8916960aa8a20dd303290/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", size = 1073225, upload-time = "2026-08-28T10:27:55.089Z" },
{ url = "https://files.pythonhosted.org/packages/cb/c5/ab79dcdf5ae28909a51210ae0a1c579e97ff997b3466414f0d04c0994583/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", size = 2334335, upload-time = "2026-08-28T10:27:56.78Z" },
{ url = "https://files.pythonhosted.org/packages/8d/8e/71d047468a189041d9c93f3b76844b924f9793b188c44bd149fa258912da/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", size = 2424982, upload-time = "2026-08-28T10:27:58.639Z" },
{ url = "https://files.pythonhosted.org/packages/7d/37/1347461bbea6d0e1f0580b94ef603b18e72c2be5f667fa1653867361a00b/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", size = 2062857, upload-time = "2026-08-28T10:28:00.407Z" },
{ url = "https://files.pythonhosted.org/packages/1f/87/c7f0976c9cd0d127643351bc0c9929e0b8899d7f49d4ec238cd909e39c42/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", size = 2596022, upload-time = "2026-08-28T10:28:02.183Z" },
{ url = "https://files.pythonhosted.org/packages/e3/9a/59a6f6ae6f938c15076be2c21b6cedea973d71bb1349ec84fa485fab82cf/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", size = 2395634, upload-time = "2026-08-28T10:28:03.977Z" },
{ url = "https://files.pythonhosted.org/packages/e9/2d/8982c1fb7926da5bb7ed60318c3665b5c3f941447271ac982960a11b8637/kiwisolver-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", size = 75379, upload-time = "2026-08-28T10:28:06.307Z" },
{ url = "https://files.pythonhosted.org/packages/07/78/ba7b6dfa1708b82b373ac056928a30c545d5c1a627df9839dcec3c6c1881/kiwisolver-1.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", size = 73011, upload-time = "2026-08-28T10:28:07.578Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c4/1407df7512a5b36cc79840e01710dc575733c461b13ab866cae77eaf87f3/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", size = 134002, upload-time = "2026-08-28T10:28:08.881Z" },
{ url = "https://files.pythonhosted.org/packages/16/45/c37a21ad5c0ab581a93c55ad544721aaa1f0ae94edb29c6a678a23d013e6/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", size = 194292, upload-time = "2026-08-28T10:28:11.06Z" },
{ url = "https://files.pythonhosted.org/packages/a6/c1/69f00d627949580e43d57af0aa465df46868d7c29801c137a55374101294/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", size = 73362, upload-time = "2026-08-28T10:28:12.449Z" },
{ url = "https://files.pythonhosted.org/packages/b5/1d/59ba570b1774e95e97fde3a0981b2e22118a7a495f73bf74cedc538566a0/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1", size = 59450, upload-time = "2026-08-28T10:28:20.383Z" },
{ url = "https://files.pythonhosted.org/packages/22/98/a6849f04dc18b5400e8b98affa2cd8fd86ed583085f036e57b32e571f4fa/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957", size = 57400, upload-time = "2026-08-28T10:28:21.632Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ce/a7dc71353dd06a4cbe02222773f52d4a28c81e5a452a75797f8ed113dc99/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5daa1f19e097050b9c4d9a78fcc9263cb96c9dfae08037ddc1b7c4ad1889f2a2", size = 79891, upload-time = "2026-08-28T10:28:22.936Z" },
{ url = "https://files.pythonhosted.org/packages/10/b1/d61c61a84ff85d1a36a99df2c152b59ffedb1d356c598902aba44abcdb60/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdaeeb6c350106df6bf9d873395973e5f066a9713200b72cd64f55d0a3eafab6", size = 77605, upload-time = "2026-08-28T10:28:24.322Z" },
{ url = "https://files.pythonhosted.org/packages/d3/52/5aef56f21a460a6e43ab3cdfc7697d59d7b87deb0ec97a0f7b91aa4a521b/kiwisolver-1.5.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f", size = 98465, upload-time = "2026-08-28T10:28:25.696Z" },
]
[[package]]
name = "lxml"
version = "6.1.3"
@@ -643,6 +1011,71 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/03/5c/91fe48856f9f8089be3096fa4dbe4b3fb5526f3bf3e852ea9497f399cb9f/lxml-6.1.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bc8dd3d9c93e70c3df974a201ac2958b6d77b465d813c51d1f15fa8e645763ae", size = 3511258, upload-time = "2026-09-02T14:46:49.046Z" },
]
[[package]]
name = "matplotlib"
version = "3.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "contourpy" },
{ name = "cycler" },
{ name = "fonttools" },
{ name = "kiwisolver" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "packaging" },
{ name = "pillow" },
{ name = "pyparsing" },
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" },
{ url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f0/9b4298911303f74e6d83e64a81d996c0616405ec95046fac7f17e4258b9e/matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83", size = 10024922, upload-time = "2026-07-18T03:38:08.236Z" },
{ url = "https://files.pythonhosted.org/packages/84/6f/0bc3c3d05b021db44c14bc379a7c0df7d57302aa15380c16fd4e63fd6a9b/matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099", size = 10832170, upload-time = "2026-07-18T03:38:10.276Z" },
{ url = "https://files.pythonhosted.org/packages/db/4d/e375f39acdb2af5a9342730618608e39790ec842e6f1b392863028781459/matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407", size = 10916701, upload-time = "2026-07-18T03:38:12.512Z" },
{ url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" },
{ url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" },
{ url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" },
{ url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" },
{ url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" },
{ url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" },
{ url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" },
{ url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" },
{ url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" },
{ url = "https://files.pythonhosted.org/packages/24/ea/65cec46fe241390ccea1b1754207ee28eb71c5ab866bd5f22fe47e538fa4/matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3", size = 10035860, upload-time = "2026-07-18T03:38:39.663Z" },
{ url = "https://files.pythonhosted.org/packages/c7/10/63fdccccbabe002fb0960876baabc5e3f24d9c1bb4cfb25651457f74b3a0/matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e", size = 10843594, upload-time = "2026-07-18T03:38:42.144Z" },
{ url = "https://files.pythonhosted.org/packages/98/51/a1155945bff7b91381875022ac1522c5dfdac0d006be8e7df389b3134eae/matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f", size = 10927962, upload-time = "2026-07-18T03:38:44.302Z" },
{ url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" },
{ url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" },
{ url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" },
{ url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7f/36ffe144fc4aacfe0e3ed2318f72b6755d1e73b041d619b4d393e60f5a66/matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464", size = 10049244, upload-time = "2026-07-18T03:38:55.911Z" },
{ url = "https://files.pythonhosted.org/packages/ab/5f/55812d68c0a840d3a463638f48c00ab1fe338518ec49a640cb6473b444af/matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf", size = 10860798, upload-time = "2026-07-18T03:38:58.282Z" },
{ url = "https://files.pythonhosted.org/packages/7a/64/cca444b4eb5e6c768c44fc5e1f0b5211f20ca2b282778051996e996a2bdf/matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f", size = 10943282, upload-time = "2026-07-18T03:39:00.465Z" },
{ url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" },
{ url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" },
{ url = "https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3", size = 9456022, upload-time = "2026-07-18T03:39:07.041Z" },
{ url = "https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319", size = 9285475, upload-time = "2026-07-18T03:39:09.562Z" },
{ url = "https://files.pythonhosted.org/packages/d3/b3/31b15a2ca56d4ddd6aaa1c884c2f51cf9a61cfaf5ca6f6fbd6343d38e6df/matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f", size = 10847102, upload-time = "2026-07-18T03:39:11.532Z" },
{ url = "https://files.pythonhosted.org/packages/64/0d/a17e966e620545c1548125af0b29ac812dd17b197a18a7462ac12fa859ee/matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be", size = 11131087, upload-time = "2026-07-18T03:39:13.764Z" },
{ url = "https://files.pythonhosted.org/packages/97/c5/5e100efdd67abb7de20befaa333612ef9bfc63417fb71398f904f25d083c/matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2", size = 10929036, upload-time = "2026-07-18T03:39:16.888Z" },
{ url = "https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6", size = 9489571, upload-time = "2026-07-18T03:39:19.449Z" },
{ url = "https://files.pythonhosted.org/packages/48/65/facabdc2f1f6caba7e856db64dfedddca25f7608df07d96a1c8fd114fd3b/matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685", size = 9164486, upload-time = "2026-07-18T03:39:21.424Z" },
{ url = "https://files.pythonhosted.org/packages/88/dd/18da6cd01cf96354534f98c468a25380c68ce582a2c9dd0cae12b04af4f2/matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae", size = 9504876, upload-time = "2026-07-18T03:39:23.633Z" },
{ url = "https://files.pythonhosted.org/packages/79/b0/f0b63555a18b79d038c81fd6126f35fc4dfce0eaff48d96103348c7cf935/matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda", size = 9336120, upload-time = "2026-07-18T03:39:25.797Z" },
{ url = "https://files.pythonhosted.org/packages/c6/dd/f210ec7c4a6f198d5567237048a93d0811fb5a1f1691f13320e592f95b41/matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb", size = 10858033, upload-time = "2026-07-18T03:39:27.999Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d2/d6d5324507c5fbb316db48e258c09c2807f3de03d9af47017e120070926f/matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2", size = 11141827, upload-time = "2026-07-18T03:39:30.092Z" },
{ url = "https://files.pythonhosted.org/packages/0f/68/3c22e9320bdce2c4d2f1320643ef706db7a24cb7420eea28b97a2d67f5a8/matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d", size = 10943061, upload-time = "2026-07-18T03:39:32.356Z" },
{ url = "https://files.pythonhosted.org/packages/f6/4a/907ed190ee81a9df581e0ed5456134fc0f7cb55ffcfda2f9e54ca900761c/matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb", size = 9540074, upload-time = "2026-07-18T03:39:34.789Z" },
{ url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" },
{ url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" },
{ url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" },
{ url = "https://files.pythonhosted.org/packages/8a/6d/69552382fcc8e93d1f2763ef2665980a900a48b7f3a4c57ed290726d1cbc/matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99", size = 10019439, upload-time = "2026-07-18T03:39:43.78Z" },
]
[[package]]
name = "mistune"
version = "3.3.4"
@@ -661,8 +1094,10 @@ dependencies = [
{ name = "fastapi" },
{ name = "httpx" },
{ name = "jsonschema" },
{ name = "matplotlib" },
{ name = "mistune" },
{ name = "olefile" },
{ name = "playwright" },
{ name = "python-docx" },
{ name = "pyyaml" },
{ name = "referencing" },
@@ -682,8 +1117,10 @@ requires-dist = [
{ name = "fastapi", specifier = ">=0.116,<1.0" },
{ name = "httpx", specifier = ">=0.28,<1.0" },
{ name = "jsonschema", specifier = ">=4.25,<5.0" },
{ name = "matplotlib", specifier = ">=3.9,<4" },
{ name = "mistune", specifier = ">=3.0,<4.0" },
{ name = "olefile", specifier = ">=0.47" },
{ name = "playwright", specifier = ">=1.55,<2" },
{ name = "python-docx", specifier = ">=1.1,<2.0" },
{ name = "pyyaml", specifier = ">=6.0,<7.0" },
{ name = "referencing", specifier = ">=0.36,<1.0" },
@@ -695,6 +1132,164 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.4,<9.0" }]
[[package]]
name = "numpy"
version = "2.4.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.12'",
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
{ url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
{ url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
{ url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
{ url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
]
[[package]]
name = "numpy"
version = "2.5.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12'",
]
sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" },
{ url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" },
{ url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" },
{ url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" },
{ url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" },
{ url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" },
{ url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" },
{ url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" },
{ url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" },
{ url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" },
{ url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" },
{ url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" },
{ url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" },
{ url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" },
{ url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" },
{ url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" },
{ url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" },
{ url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" },
{ url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" },
{ url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" },
{ url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" },
{ url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" },
{ url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" },
{ url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" },
{ url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" },
{ url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" },
{ url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" },
{ url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" },
{ url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" },
{ url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" },
{ url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" },
{ url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" },
{ url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" },
{ url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" },
{ url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" },
{ url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" },
{ url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" },
{ url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" },
{ url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" },
{ url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" },
{ url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" },
{ url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" },
{ url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" },
{ url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" },
{ url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" },
{ url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" },
{ url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" },
{ url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" },
{ url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" },
{ url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" },
{ url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" },
{ url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" },
{ url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" },
{ url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" },
{ url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" },
]
[[package]]
name = "olefile"
version = "0.47"
@@ -798,6 +1393,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
]
[[package]]
name = "playwright"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet" },
{ name = "pyee" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/5b/ca2abcf3aa69f9fb510215e3064f30b57fe57657c8d04ede45bb966d5606/playwright-1.62.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8da938f3748841a8754f2e1f0216902c1c8f8ae3720de8b32ccf8e6913a7c4f", size = 43732091, upload-time = "2026-07-31T17:00:44.178Z" },
{ url = "https://files.pythonhosted.org/packages/af/1a/0bfbe9904350961f4dbb713f04342e40d548c5fc26c8157bd13617c81492/playwright-1.62.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db755ab27db21a04186f1fe8169888e42356086e439b1059b923ef417f0b6034", size = 42510842, upload-time = "2026-07-31T17:00:48.596Z" },
{ url = "https://files.pythonhosted.org/packages/66/dc/c0486b407ad0699a250f6bbe3066fca95344009a99ca66e88ca175c69dc1/playwright-1.62.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5108bd5b3e87169ddf269feee097da5893af7f8aea4634dfc840518d64c1f1da", size = 43732093, upload-time = "2026-07-31T17:00:52.218Z" },
{ url = "https://files.pythonhosted.org/packages/43/6b/b24aebc2b04bffcb342bccf96e287c78b363e1615bed5cea97500cc0393a/playwright-1.62.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ba33bae6a13b3d9d354c751cb618af357d20fe1d57767cbcce52079bbef17ad3", size = 47748926, upload-time = "2026-07-31T17:00:56.438Z" },
{ url = "https://files.pythonhosted.org/packages/36/43/b4b18bdc87e1949568fffdcde3ff9a0456266b2d0c6d4432cc34d89ea6eb/playwright-1.62.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2d76613a57ad844362ce42f7d0c2fa26b19a4f7a46d4f76b891c631e6e5aff", size = 47441423, upload-time = "2026-07-31T17:01:00.404Z" },
{ url = "https://files.pythonhosted.org/packages/81/22/af5d926fc2c32a339eec00a443644bc40ab9db1dd2dd9017873c59773c0c/playwright-1.62.0-py3-none-win32.whl", hash = "sha256:e5614fa89355d7081457680324bb219f79f69c423c5cb6fa250e30b0d8aebf1c", size = 38164450, upload-time = "2026-07-31T17:01:04.187Z" },
{ url = "https://files.pythonhosted.org/packages/2b/a9/4160c1033c07af98bf841ad079457dd78408a5ee0dd56cbfe50b8b6a1c22/playwright-1.62.0-py3-none-win_amd64.whl", hash = "sha256:92c0d98ed04eb35af557b709875edba415b1f548bdb22ddb5bb3e1e6c835c2f1", size = 38164458, upload-time = "2026-07-31T17:01:08.459Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ec/06b55d619a7082a766aa04f2c6bb31435c87f02930087d8a0517119408fa/playwright-1.62.0-py3-none-win_arm64.whl", hash = "sha256:ea8d3055aa9d5a9f1832ac82517bd8b42c78fac7ebcbebb0107116735c8cb6a1", size = 34208868, upload-time = "2026-07-31T17:01:11.818Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -933,6 +1547,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
]
[[package]]
name = "pyee"
version = "13.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
@@ -942,6 +1568,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pyparsing"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
[[package]]
name = "pytest"
version = "8.4.2"
@@ -958,6 +1593,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-docx"
version = "1.2.0"
@@ -1185,6 +1832,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sqlite-vec"
version = "0.1.9"
+2
View File
@@ -1,5 +1,7 @@
# NotesAgent 文档索引
> 第二阶段收尾:标准 Agent/RAG Benchmark 与报告页、函数图预览、三格式快照导出及真实 Provider/MCP 结果见[实现与验收记录](development/第二阶段收尾实现与验收-2026-09-07.md)。当前分支尚未合并,不更改下文历史 main 基线。
本目录集中保存团队开发期间需要长期维护的架构、接口、实现、协作和问题复盘文档。文档按用途分类,避免设计约束、开发记录与故障复盘混放。
当前文档基线为 2026-09-06:第一阶段和第二阶段 A~F 工程范围已经合并到 `main`,当前可运行形态仍为 Vue/Vite Web 前端与 FastAPI AI Core。Tauri/Rust Host、Stronghold、原生多 Vault 文件系统、生产级 MCP 沙箱和 Sync Server 尚未接入。
@@ -25,9 +25,9 @@
| 同步 | 技术栈中已有目标设计;`server sync/` 当前无实现文件 | 协议、独立服务、客户端队列、冲突、设备身份、部署运维 |
| 原生桌面 | 已有需求文档 | Tauri 工程和 Rust Host 均需建设,不能将 Web 页面当作桌面交付 |
本次社区准备包验证为 70 项相关后端测试通过,并在本地 API 上完成真实 ZIP 导入、启用和命令执行;它不是所有第三阶段功能的验收。开发服务器监听新解压 `.py` 会热重载,当前内存安装记录随之丢失;持久化与开发监听排除规则列为首批问题
本次社区准备包验证为 70 项相关后端测试通过,并在本地 API 上完成真实 ZIP 导入、启用和命令执行;它不是所有第三阶段功能的验收。早期开发监听曾使内存安装记录丢失;第二阶段已补齐持久安装库、摘要复核及重启恢复,不再将其列作未实现。第三阶段继续负责升级事务和桌面生命周期
第二阶段待验收事项单独保留:目标 Provider 真实账号兼容性、声纹阈值校准、带标注音频质量、逐字对齐和重叠语音。已有约 37 分 16 秒录音的 CUDA 功能闭环,无参考标注,不能报告 WER/CER、DER 达标。杨星萱负责的检索调优、Benchmark、导出和函数图像须由对应负责人确认状态,不因本规划自动判为完成或重新归责
第二阶段待验收事项单独保留:目标 Provider 真实账号兼容性、声纹阈值校准、带标注音频质量、逐字对齐和重叠语音。已有约 37 分 16 秒录音的 CUDA 功能闭环,无参考标注,不能报告 WER/CER、DER 达标。检索调优、Benchmark、导出和函数图像的当前交接结果见[第二阶段收尾验收](../development/第二阶段收尾实现与验收-2026-09-07.md),不因本规划扩大为第三阶段任务
## 3. 架构、写入所有权与目录
@@ -212,7 +212,7 @@ base_revision 不匹配返回 409 类冲突与当前 Revision;界面展示本
| --- | --- |
| Web 单 Vault → 桌面多 Vault | 识别旧目录,备份元数据,保持 note_id/file_id 对应关系,校验文件摘要和数量;索引可重建,正文不能覆盖 |
| Fernet → Stronghold | 按 credential_id 迁移、验证、记录版本,失败重试;迁移完成前保留旧存储,不向 UI 返回明文 |
| 内存扩展记录 → 持久化安装库 | 探测用户认可的受管理包、重新校验、不自动继承高权限;重启恢复与包损坏修复必须实测 |
| 既有持久化安装库 → 桌面受管理安装库 | 探测用户认可的受管理包、重新校验、不自动继承高权限;重启恢复与包损坏修复必须实测 |
| 旧主题/Skill/Plugin → 社区版本 | ID/来源/版本/摘要关联,未知来源标本地;配置迁移保留备份,用户修改包不能静默覆盖 |
| 首次绑定同步 | 本地/远端清单对账、显示新增和冲突,不以空 Vault 向另一端下发批量删除;绑定信息可撤销 |
| 升级与降级 | Schema 版本门禁,升级前备份;不支持降级的数据库禁止旧客户端写入,提供恢复路径 |
@@ -1,5 +1,10 @@
# 第二阶段团队分工表
## 2026-09-07 全范围收尾状态
本轮覆盖三位成员的第二阶段模块(音频质量专项除外),以[实现与验收记录](../development/第二阶段收尾实现与验收-2026-09-07.md)及下方更新后的DoD为当前口径。此前“不含杨侧验收”仅描述当日范围。现有DeepSeek/MiniMax真实链路通过,其他未配置协议保留外部验收项;音频使用Qwen3-ASR/ERes2NetV2技术路径,不声称交付原清单指定模型。
## 2026-09-06 复核修复补充(不含杨侧验收)
- Agent 增加断点续读、有界重连和手动恢复;连接中断不再隐藏仍在运行任务的取消入口。
@@ -776,33 +781,33 @@ Markdown
- [x] Plugin Settings Contribution 后端可解析;
- [x] Plugin Command 可以显示并从前端执行;
- [x] Plugin Settings 可以动态生成设置项并独立提交 Secret;
- [ ] Provider Adapter 的 Streaming / Tool Calling / Error Mapping 稳定
- [ ] 完成跨模块接口审阅和第二阶段集成。
- [ ] 全部目标Provider真实矩阵;现有DeepSeek Streaming/Tool Calling/Error Mapping已通过,其他协议缺配置
- [x] 完成非音频范围跨模块接口审阅和第二阶段集成;外部协议/DOCX整页视觉边界见验收记录
### 杨星萱
- [ ] RAG Benchmark Dataset 可以稳定运行;
- [ ] 能输出 Hit@K、Recall@K、MRR、Latency 等指标;
- [ ] Agent Benchmark Framework 可以执行标准 Case
- [ ] Markdown 可以导出 HTML
- [ ] Markdown 可以导出 PDF
- [ ] Markdown 可以导出 DOCX
- [ ] Mermaid 在导出链路中可以保留为静态图;
- [ ] 函数图像能够由结构化表达生成;
- [ ] 函数图像能够进入预览和导出链路;
- [ ] Retrieval 调优结果有 Benchmark 数据支撑。
- [x] RAG Benchmark Dataset 可以稳定运行;
- [x] 能输出 Hit@K、Recall@K、MRR、Latency 等指标;
- [x] Agent Benchmark Framework 可以执行标准 Case
- [x] Markdown 可以导出 HTML
- [x] Markdown 可以导出 PDF
- [x] Markdown 可以导出 DOCX
- [x] Mermaid 在导出链路中可以保留为静态图;
- [x] 函数图像能够由结构化表达生成;
- [x] 函数图像能够进入预览和导出链路;
- [x] Retrieval 调优结果有 Benchmark 数据支撑。
### 吉海燕
- [ ] Theme Package 可以导入;
- [ ] Theme Manifest 可以校验;
- [ ] Theme 可以启用、停用和卸载;
- [ ] Agent Trace 可以展示完整 Tool Call 顺序;
- [ ] Trace Node 可以查看参数、结果、耗时和错误;
- [ ] Markdown Mermaid Code Block 可以渲染;
- [ ] Mermaid 支持主题切换;
- [ ] Mermaid 渲染错误可以明确展示;
- [ ] Mermaid 图能够提供给 Export Service。
- [x] Theme Package 可以导入;
- [x] Theme Manifest 可以校验;
- [x] Theme 可以启用、停用和卸载;
- [x] Agent Trace 可以展示完整 Tool Call 顺序;
- [x] Trace Node 可以查看参数、结果、耗时和错误;
- [x] Markdown Mermaid Code Block 可以渲染;
- [x] Mermaid 支持主题切换;
- [x] Mermaid 渲染错误可以明确展示;
- [x] Mermaid 图能够提供给 Export Service。
---
@@ -878,18 +878,19 @@ Dataset 从仓库或受控导入目录注册。API 不接受调用方提交任
```json
{
"case_id": "agent-os-review-001",
"prompt": "查找死锁内容并创建三个复习任务",
"allowed_tools": ["rag.search", "notes.read", "tasks.create"],
"expected_tools": ["rag.search", "notes.read", "tasks.create"],
"expected_conditions": {
"citation_required": true,
"tasks_created": 3
},
"tags": ["os", "write"]
"case_id": "agent-add-001",
"prompt": "调用 math.add 计算17加25,然后回答42。",
"allowed_tools": ["math.add"],
"expected_tools": [{"name": "math.add", "arguments": {"left": 17, "right": 25}}],
"output_contains": ["42"],
"citation_required": false,
"tasks_created": null,
"tags": ["math"]
}
```
每个Dataset最多100例且case_id唯一;Case至少声明工具、输出子串、引用或任务数量之一作为客观断言。预期工具必须属于allowed_tools。参数按声明键的值匹配,重复/额外调用计入错误,任务创建按成功Tool Result计数。
有写操作的 Agent Case 必须运行在隔离 Vault/数据库中,测试结束后清理 Fixture,禁止作用于用户真实知识库。
### 9.4 创建 RAG Benchmark
@@ -902,6 +903,7 @@ Dataset 从仓库或受控导入目录注册。API 不接受调用方提交任
"modes": ["fts", "vector", "hybrid"],
"retrieval": {
"top_k": 10,
"fusion": "rrf",
"rrf_k": 60,
"rerank": true,
"rerank_candidates": 20,
@@ -914,23 +916,28 @@ Dataset 从仓库或受控导入目录注册。API 不接受调用方提交任
配置快照必须记录 Embedding model ID/version/dimension、Reranker、索引版本、Dataset Hash 和运行环境。
### 9.5 创建 Agent Benchmark暂缓,未暴露接口
### 9.5 创建 Agent Benchmark已实现
`POST /api/benchmarks/agent/runs`
```json
{
"dataset_id": "agent-core-v1",
"provider_id": "mock",
"model": "mock-1",
"skill_id": null,
"max_steps": 10,
"token_budget": 20000,
"concurrency": 1,
"metadata": {}
"provider_id": "已有启用的Provider ID",
"model": "已有模型",
"max_steps": 6,
"timeout_seconds": 90,
"token_budget": 6000,
"repeat": 1,
"allow_network": false,
"offline": false
}
```
`max_steps` 120、`timeout_seconds` 1300、`token_budget` 130000、`repeat` 1–3。顺序执行以限制额度;默认拒绝mock。仅显式offline=true可使用mock,且offline不能选真实Provider。创建返回202;无Provider为404,模式冲突为422,容量超限为429。权限由用户在真实Trace中处理,不自动批准。共用运行查询/SSE/取消/报告端点。
报告包含逐例agent_run_id、success/checks、调用/匹配/无效数量、steps、latency_ms、token_usage。汇总task_success_rate以计划案例总数为分母,total_cases/evaluated_cases区分未完成样本;选择/参数准确率以max(实际调用总数,预期调用总数)为分母,无调用时为null。无效调用率以实际调用数为分母。平均步骤/耗时采用计划数分母,取消/失败报告不可当完整性能测量。配置冻结dataset hash/version、Provider类型/引用、模型、预算、评分版本与权限策略。
Benchmark Runner 通过正式 Agent Runtime 创建 Run,并从 Trace 计算结果,不能直接调用 Tool Executor 绕过权限和步骤控制。
### 9.6 Benchmark Run
@@ -1080,7 +1087,7 @@ VECTOR_INDEX_REBUILD_REQUIRED
## 10. Export Service
> 实现状态:HTML / PDF / DOCX 导出均已实现(`backend/app/export/`),`format` 支持 `html`/`pdf`/`docx` 三格式。`function-plot` 已支持静态 SVG 内嵌(HTML)与矢量图内嵌(PDF,经 `backend/app/plot/render_reportlab.py` 复用共享几何),解析或渲染失败时回退为源码占位并记录 warning;Mermaid 目前仍以占位代码块保留并记 warning。DOCX 为文本优先 v1`function-plot` 与 Mermaid 保留源码占位并记 warning
> 当前实现:三格式已有编辑器快照入口、任务/取消/下载/warning。函数图为HTML SVG、PDF共享几何矢量、DOCX PNG;Mermaid通过前端准备并按源码摘要绑定PNG后进入三格式。未提供静态资源的直接API Mermaid请求仍保留源码并warning,不能假称服务器独立运行Mermaid。受支持MathText公式及Vault图片内嵌,完整限制见本节末补充
### 10.1 创建导出任务
@@ -1616,3 +1623,25 @@ CUDA 组件:`GET /api/local-models/runtime-components/cuda` 返回 status、st
#### 回答版本的上下文快照(2026-09-07)
`ChatMessage.context_captured` 为布尔值,旧记录默认 false。新 assistant 消息保存本次请求的 `workspace_context``attachments`,并设置 context_captured 为 true;此时 null 文件上下文和空附件列表都是明确快照。重新生成不覆盖原 user 消息的快照。客户端恢复旧记录时仅在 context_captured 为 false 时回退到对应父用户消息。
## 2026-09-07 预览与静态资源增量契约
`POST /api/plots/function`:请求`{source, theme_id}`source最多20000字符;响应`{result: {content,mime_type,width,height,warnings} | null, diagnostics: [{severity,code,message,line}], node_count}`。语法错误为200诊断、请求字段违规422。共享plot白名单解释器,不执行eval;每块16表达式、8000累计节点、并发2。主题映射当前六个Theme ID并提供CSS图表Token;未知主题回退light。
`ExportRequest`新增可选title(最多200字符)、assetsHTML/DOCX 最多64PDF 不设数量上限);`source.file_path`为未保存快照中相对图片的基准位置,不能用于任意文件读。每个asset为`{kind: mermaid|math_block|math_inline|image, source_hash: 64位sha256十六进制, png_base64}`;摘要为strip后UTF-8源码(image为src)的SHA256。只接受有效PNG并重编码;HTML/DOCX 每图4百万像素、总16百万像素/8MiB,PDF 不使用这些预算。重复kind/hash或无效PNG返回422 EXPORT_ASSET_INVALID。摘要失配不替换当前节点,不接受客户端SVG/XML/URL执行。
未带资源的公式由 MathText 转换,仅解析 Vault 范围内 PNG/JPEG/WebP,拒绝远端与越界路径。HTML/DOCX 保留 512 字符/20 层/64 资源、单文件 2MB 的预算;PDF 不使用这些预算,也不限制导出源长度、产物字节数、函数图数量、表达式数量及累计复杂度。表达式白名单、有效图片校验、数值采样的收敛控制和队列并发调度仍保留。资源无法表示时保留源码/替代文字和 warning。
PDF 使用当前主题配色。`ExportOptions.palette` 可选,包含 page/surface/text/muted/code/border/accent 七个必填 `#RRGGBB` 值,由客户端在点击导出时冻结,用于自定义主题。未传 palette 时按 theme_id 解析六套内置配色,未知 ID 回退 light 并警告。PDF 页背景、正文、代码、表格、引用、链接、公式、Mermaid 和函数图均主题化;不会执行主题 CSS。DOCX 仍采用浅色打印样式;HTML 保留有限主题调色板。DOCX 图片为静态内容,不提供可编辑公式对象。
关闭导出窗口仅停止 UI 轮询,已发起的导出继续。主动取消在准备阶段停止提交;创建请求期间取消会等待任务 ID,调用后台取消接口并读取实际状态。
RAG `retrieval.fusion`接受rrf(默认)或weighted(归一化FTS/vector各50%),参数写入config_snapshot。真实本地单查询Embedding可命中有界进程缓存,provenance.query_embedding_cache为hit/miss;比较延迟须分别报告冷暖样本。HashEmbedding仍仅为确定性单元测试,不是当前生产检索模型。
## PDF 浏览器快照契约(2026-09-07
`ExportRequest.print_html?: string` 仅允许 format=pdf。应用导出发送包含实际主题 CSS、根属性/CSS 变量、笔记 DOM 和内嵌字体图片的 HTML 快照;后端通过禁用脚本与网络的 Chromium 打印。`options.page_size` 控制 A4/Letter。print_html 存在时不再经过 ReportLab 的 AST 重排;无此字段的旧客户端维持兼容渲染。
`POST /api/exports/preview-resources` 接收 ExportRequest 的 source/options,返回 `{images:[{source,data:string|null,warnings:string[]}],plots:[{source,svg,warnings:string[]}]}`,供浏览器共享 Markdown 渲染器使用。图片仅解析 Vault 相对路径;函数图使用共享白名单解释器,PDF 不使用预览页面配额。客户端准备失败不会创建半成品任务,已提交任务仍走统一取消和下载接口。
+10 -3
View File
@@ -1,6 +1,6 @@
# Benchmark 开发说明
> 所属模块:Knowledge / Retrieval Core(后端,负责人 yxx)。RAG Benchmark 已交付;Agent Benchmark 暂缓,待 Agent Runtime 完成后在同一契约下补齐
> 所属模块:Knowledge / Retrieval Core(后端,负责人 yxx)。RAG 与标准Agent Benchmark均已实现,前端入口`/benchmarks`。真实验收结果与范围见[第二阶段收尾记录](第二阶段收尾实现与验收-2026-09-07.md)
## 定位
@@ -18,7 +18,7 @@ Benchmark Service 用受控 Dataset 对检索引擎做可复现评测:创建
| POST | `/api/benchmarks/runs/{run_id}/cancel` | 取消运行 |
| GET | `/api/benchmarks/runs/{run_id}/report` | 结构化完整报告 |
Agent Benchmark`/api/benchmarks/agent/runs` 未暴露(暂缓),不在 OpenAPI 注册占位接口
Agent Benchmark`POST /api/benchmarks/agent/runs`已实现并注册OpenAPI,使用正式Agent Runtime与Trace;请求/Case见[契约§9](../contracts/第二阶段接口契约-开发版.md)。默认只接受真实Provider,显式offline才允许mock,不自动批准工具权限
## Dataset
@@ -30,7 +30,7 @@ RAG Case 结构:`case_id`、`query`、`expected_note_ids`、`expected_block_id
`queued → running → completed | failed | cancelled`
- 创建时校验索引兼容性:索引非空、Embedding model/dim 与当前引擎一致、vector/hybrid 时向量索引非空;不满足返回 `BENCHMARK_INDEX_INCOMPATIBLE`(409),避免把环境/索引错误误判为检索质量差。
- RAG创建时校验索引兼容性:索引非空、Embedding model/dim 与当前引擎一致、vector/hybrid 时向量索引非空;不满足返回 `BENCHMARK_INDEX_INCOMPATIBLE`(409),避免把环境/索引错误误判为检索质量差。
- 内存注册表上限 `MAX_RUNS=100`,超限只淘汰终态 run;满容量且全为活动 run 时返回 `BENCHMARK_CAPACITY_EXCEEDED`429)。
- 失败/取消只向公开响应暴露项目错误码与安全消息,详细异常进入日志,不通过 HTTP/SSE 返回。
@@ -76,3 +76,10 @@ uv run pytest -q
```
`tests/test_benchmark.py` 覆盖数据集注册与校验、指标纯函数、端到端运行、取消、索引兼容、容量与失败样本聚合;`tests/test_retrieval.py` 覆盖 FTS 阈值与分页 total 一致性。
## 2026-09-07 数据与复现补充
`agent-core-v1.json`为四例标准任务;`rag-phase2-v1.json`对应独立测试语料,不对应任意用户Vault,运行前必须用`phase2-quality.py`准备语料及真实索引。语料源、配置、逐例结果和冷暖缓存说明见收尾验收记录。UI支持RAG/Agent切换、Provider/模型、融合/TopK/RRF/rerank配置、取消、报告JSON与Trace入口。
Agent成功判定包含终态完成、预期工具及声明参数一一匹配、无额外调用、工具结果成功、输出子串、引用和任务数量;并非由另一个LLM主观打分。失败/取消报告保留total_cases与evaluated_cases,不能当成完成的质量测量。报告注册表最多100条且不跨进程恢复,重要结果须下载保存;持久Agent Trace独立保留。工具选择/参数准确率和无效调用率在无分母时返回null。
+22
View File
@@ -156,3 +156,25 @@ PDF、DOCX 保持浅色打印样式;选择其他主题时返回明确 warning
警告框识别与工作区一致:标记与标题之间可不留空格,类型允许数字、下划线和连字符;自定义类型回退 note 配色并保留自定义标题,省略标题时使用类型名称首字母大写。
警告框、普通引用、列表及交叉嵌套中的 Markdown 表格均启用容器内部解析,HTML 输出 table、PDF 输出 Table、DOCX 输出原生表格。测试逐一检查单元格内容和产物结构。每个 HTML 警告框独立初始化颜色变量,避免 NOTE 等类型继承外层 WARNING 的颜色;已在五套内置导出主题中检查嵌套配色及表格显示。
### PDF 主题与资源策略更新(2026-09-07)
当前 PDF 行为以此节为准,覆盖上文早期 PR 的浅色打印和资源预算说明。
PDF 使用导出按钮点击时的主题配色快照,支持六种内置/社区主题与自定义主题的七项颜色。页背景、正文、引用、代码块、表格、链接、语义提示块、Mermaid、公式与函数图均参与主题适配;公式使用透明底再合成主题表面色,深色曲线使用较亮的默认色。标题随下一块分页,代码保留背景与边框。PDF 不执行 CSS 装饰或主题脚本。
PDF 取消导出源/产物大小限制、Mermaid 数量和像素预算、请求资源数量和字节预算、Vault 图片大小/累计预算、MathText 长度/深度预算、函数图数量/表达式数量及 AST 预算。HTML/DOCX、在线预览仍使用原有限制;有效图片、Vault 路径、表达式语法校验和数值求解退出条件仍生效。无限制指移除应用导出配额,实际文件规模仍受浏览器、解析器和可用内存约束。
关闭窗口仅结束 UI 生命周期,导出流程继续;主动取消仍取消提交中的后台任务。回归测试见 `ExportDialog.spec.ts``exportService.spec.ts``test_pdf_theme_resources.py`
### PDF 实际主题样式修正(2026-09-07)
七色调色板加 ReportLab 固定排版不能复现笔记主题。应用界面的 PDF 导出现在提交 `print_html`:复用 Markdown 渲染器、KaTeX、Shiki、编辑器 DOM 容器、主题 CSS、CSS 变量、标题偏好和字体资源,由 Chromium 打印为可选择文本的 PDF。保留背景纹理、伪元素、边框、阴影和语义样式;打印规则只处理页面尺寸、滚动容器、分页、图表缩放和交互按钮。提示块采用编辑器的 blockquote 结构以匹配主题选择器。KaTeX 内部 SVG 不应用图表缩放规则。
`POST /api/exports/preview-resources` 根据同一 Markdown 快照准备 Vault 图片和函数图 SVG,不恢复 PDF 的旧资源配额。图片保留透明通道;不读取 Vault 外文件。客户端将字体和主题图片内嵌为 data URI。浏览器进程关闭文档 JavaScript、网络与文件资源加载,等待字体就绪后打印;不会为静态 CSS 加载用户脚本。
后端依赖 Playwright(已写入 pyproject/uv.lock)。Windows 优先使用本机 Edge/Chrome,可用 `APP_PDF_BROWSER` 指定可执行文件;没有系统浏览器时,在 backend 目录运行 `uv run playwright install chromium`。打印在独立子进程中执行,避免 Windows Uvicorn 事件循环冲突。
不传 print_html 的旧 API 客户端保留 ReportLab 兼容路径,其固定排版不能代表应用主题的实际样式。HTML/DOCX 导出行为不变。主题字体/图片需可从应用资源读取并内嵌,读取失败会报错,避免输出缺失资源却宣称样式一致。
@@ -0,0 +1,22 @@
# PDF 主题与资源限制修复验收
工作目录:`G:/OSProject/NotesAgent`;分支:`feat/phase2-completion`
## 完成内容
1. 修复关闭窗口误取消提交中的导出任务,保留主动取消。
2. PDF 移除前后端导出数量、大小、图片像素及累计资源配额,详见接口契约。
3. PDF 读取点击导出时的主题颜色快照,覆盖六套主题和自定义调色板,移除强制浅色打印提示。
4. 公式透明底、Mermaid 主题变量、矢量函数图、代码背景与边框、表格、提示块和页面背景都使用对应主题颜色。
## 验证
- 全量后端:879 项通过,1 条既有 Starlette/httpx 弃用警告。
- 全量前端:82 文件、448 项通过。
- 分页/代码背景最终调整后:相关后端 108 项通过。
- 前端生产构建通过,仍有既有大分包提示。
- 超旧限制回归:17 个 Mermaid、65 个文档资源、单图超过 400 万像素、超过旧源/产物阈值、17 条函数表达式与20个函数图。
- 真实前端 Mermaid 栅格化 + 后端导出流水线生成 light/dark/sepia/paper-moments/ocean-blue/midnight-purple 六份 PDFPoppler 渲染并检查全部12页,未出现白底公式、不可读深色文字、图表裁切或孤立章节标题。
- 验证数据与截图:本机 `.local-plans/pdf-theme/`;全部接口拦截为测试数据,未访问真实模型或用户笔记。
本次未改动用户已有的三份笔记修改。主题适配使用颜色与语义样式,不将任意主题 CSS/脚本直接用于 PDF。
@@ -0,0 +1,25 @@
# PDF 实际主题样式修复验收
路径:G:/OSProject/NotesAgent。此前七色调色板验收仅覆盖配色,不能代表完整主题样式一致;本次替换应用 PDF 导出的渲染链路。
- 使用真实主题 CSS、编辑器容器、伪元素、字体、标题偏好、KaTeX 与 ShikiChromium 输出 PDF;旧客户端 ReportLab 路径仅用于兼容。
- 纸间时光保留纸张边框、缝线、左侧装订线、顶部胶带、提示块与代码块装饰。修复跨页图表右侧裁切、提示块 DOM 选择器及 KaTeX 根号 SVG 受图表规则影响的问题。
- 无旧 PDF 数量/大小配额;内嵌资源、禁用文档脚本及网络访问、保留用户主动取消行为。
- 六主题各两页,由真实前端构造快照、实际后端导出、Poppler 渲染检查。样例和浏览器参考截图位于本机 .local-plans/pdf-browser。
- 后端全量 882 项、前端全量 451 项通过;最终排版调整后相关前端19项、资源/浏览器后端15项通过;生产构建成功。1 条既有 Starlette 警告和既有大分包提示保留。
- 浏览器烟测验证 CSS 伪元素文本进入 PDF,文档脚本未执行;Vault 越界图片被拒绝,17 条函数表达式可准备。
- 用户已有的三份笔记修改未纳入提交。
## 元数据栏补充验收
PDF 快照使用与可视编辑器相同的 `splitNoteMetadata` 解析当前内容,在正文前输出笔记属性、标题和标签,保留 `.note-metadata` 结构及 Vue scoped 样式属性。标签输入框和移除按钮不进入导出。识别出的 YAML 不再作为 Markdown 正文渲染;不支持的元数据仍按编辑器规则保留原文。不额外读取磁盘,因此导出包含当前未保存的元数据。
验证:PDF 快照与导出服务 15 项测试通过,前端类型检查及生产构建通过(保留已有大分块提示)。六种主题均实际生成 PDF,并检查包含元数据栏的第一页:paper-moments、dark、light、sepia、ocean-blue、midnight-purple。纸张主题的胶带、缝线及标签配色正常,其他主题的元数据背景、边框和文字配色正常。
## 合并审阅问题修复
- 只有标题、标签而正文为空或仅有空白时,跳过资源准备请求,继续生成元数据栏,避免空 Markdown 触发 422。
- PDF 资源准备覆盖块级及行内 HTML 的 img,使用 HTMLParser 处理标签和属性实体,再复用 Vault 图片路径与格式校验。行内 HTML 在 AST 中单独标记;行内代码和代码块不会被作为 HTML 图片收集。
- 回归覆盖纯元数据、空白正文、HTML 图片实体与相对路径、越界及远程资源、代码示例排除。后端相关 114 项、前端相关 18 项通过,vue-tsc 类型检查通过。pytest 仅提示本机缓存目录不可写,测试本身通过。
@@ -0,0 +1,50 @@
[
{
"theme": "paper-moments",
"pages": 2,
"bytes": 187968,
"sha256": "33a28768101307d97f950135bd8910907ab88df82174b87d4350bd02c71e3236",
"renderer": "Chromium / actual CSS snapshot",
"visual_review": "passed"
},
{
"theme": "light",
"pages": 2,
"bytes": 105426,
"sha256": "50bd5cfb9303c17d1b34fd5e7b3ed05915720dd45aba2dfa5623e4c387f273a6",
"renderer": "Chromium / actual CSS snapshot",
"visual_review": "passed"
},
{
"theme": "dark",
"pages": 2,
"bytes": 105445,
"sha256": "586da3acaf3f55ad802da13c188544a50347f1b58d2fbe2c20a22605d9d9e3d9",
"renderer": "Chromium / actual CSS snapshot",
"visual_review": "passed"
},
{
"theme": "sepia",
"pages": 2,
"bytes": 105471,
"sha256": "700bf49901ef23da7f760efba346a69a61f7ce4f3bb26b3b2e7ab673ecbd3809",
"renderer": "Chromium / actual CSS snapshot",
"visual_review": "passed"
},
{
"theme": "ocean-blue",
"pages": 2,
"bytes": 105414,
"sha256": "7501dc0765af583d498ed54c9678631d9e6578dcb68dbc6db9c0e83551cc3479",
"renderer": "Chromium / actual CSS snapshot",
"visual_review": "passed"
},
{
"theme": "midnight-purple",
"pages": 2,
"bytes": 105489,
"sha256": "213f59e4b43e701deda9e6b8589913c2d795d3ef8a642b516ba425aa1bd6da9e",
"renderer": "Chromium / actual CSS snapshot",
"visual_review": "passed"
}
]
@@ -0,0 +1,104 @@
[
{
"theme": "light",
"pages": 2,
"bytes": 67245,
"sha256": "5b6efed909df971f455253958ceadce47a7bd198b52ca1a1c755d9d25c51dc05",
"palette": {
"page": "#ffffff",
"surface": "#ffffff",
"text": "#1f2328",
"muted": "#656d76",
"code": "#f7f8fa",
"border": "#e4e7eb",
"accent": "#5b67f1"
},
"warnings": [],
"visual_review": "passed"
},
{
"theme": "dark",
"pages": 2,
"bytes": 66788,
"sha256": "b09da4dc97246b7444f17d2b5756dea6dda004981cbcbd360bc2de3d26bd00cf",
"palette": {
"page": "#0d1117",
"surface": "#161b22",
"text": "#e6edf3",
"muted": "#8b949e",
"code": "#161b22",
"border": "#30363d",
"accent": "#7d8bff"
},
"warnings": [],
"visual_review": "passed"
},
{
"theme": "sepia",
"pages": 2,
"bytes": 66836,
"sha256": "abc329f9691ba46d8ddd25e4b3621b63fc4cc8baba8823800c47d68feb910e0c",
"palette": {
"page": "#fbf3df",
"surface": "#fff8e8",
"text": "#40372b",
"muted": "#746653",
"code": "#f4e8ca",
"border": "#ddcfad",
"accent": "#8a5b32"
},
"warnings": [],
"visual_review": "passed"
},
{
"theme": "paper-moments",
"pages": 2,
"bytes": 66808,
"sha256": "e1929fbec0486b59abcc808fab557f4103ec102ad70e6fb9f0b26f84414c0fe4",
"palette": {
"page": "#faf7ee",
"surface": "#fffdf5",
"text": "#493f35",
"muted": "#6e6053",
"code": "#f3eee3",
"border": "#b5a693",
"accent": "#875343"
},
"warnings": [],
"visual_review": "passed"
},
{
"theme": "ocean-blue",
"pages": 2,
"bytes": 67260,
"sha256": "1e567daeb2ba8bcb5c13af465f174865a3662ade444c58cd09af66ae99233346",
"palette": {
"page": "#ffffff",
"surface": "#ffffff",
"text": "#1e293b",
"muted": "#64748b",
"code": "#f8fafc",
"border": "#e2e8f0",
"accent": "#0077b6"
},
"warnings": [],
"visual_review": "passed"
},
{
"theme": "midnight-purple",
"pages": 2,
"bytes": 66788,
"sha256": "fd5efcedec0d7f4892fb987e0e4ba9052aa88b7361159cbb2ca3b7e00dde495c",
"palette": {
"page": "#1a1b26",
"surface": "#24283b",
"text": "#c0caf5",
"muted": "#9aa5ce",
"code": "#24283b",
"border": "#3b3f5c",
"accent": "#9d4edd"
},
"warnings": [],
"visual_review": "passed"
}
]
@@ -0,0 +1,234 @@
{
"python": "3.13.9",
"platform": "Windows-11-10.0.26100-SP0",
"provider": "mock with 50 ms injected delay per model turn; no network",
"results": [
{
"scenario": "agent_tool_runs",
"concurrency": 1,
"runs": 20,
"latency": {
"count": 20,
"median_ms": 168.93,
"p95_ms": 172.17,
"max_ms": 173.06
},
"completed": 20,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"recovery_read_ms": 35.15,
"retained_records": 20,
"elapsed_ms": 3446.25,
"event_loop_lag": {
"count": 223,
"median_ms": 5.42,
"p95_ms": 12.94,
"max_ms": 15.02
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 10,
"runs": 20,
"latency": {
"count": 20,
"median_ms": 203.54,
"p95_ms": 205.86,
"max_ms": 206.36
},
"completed": 20,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"recovery_read_ms": 31.0,
"retained_records": 40,
"elapsed_ms": 479.2,
"event_loop_lag": {
"count": 31,
"median_ms": 5.12,
"p95_ms": 13.95,
"max_ms": 14.6
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 50,
"runs": 50,
"latency": {
"count": 50,
"median_ms": 396.17,
"p95_ms": 397.13,
"max_ms": 397.52
},
"completed": 50,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"recovery_read_ms": 86.99,
"retained_records": 90,
"elapsed_ms": 554.8,
"event_loop_lag": {
"count": 35,
"median_ms": 5.12,
"p95_ms": 11.71,
"max_ms": 46.86
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 200,
"runs": 200,
"latency": {
"count": 200,
"median_ms": 1004.47,
"p95_ms": 1155.52,
"max_ms": 1155.62
},
"completed": 200,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"recovery_read_ms": 313.29,
"retained_records": 200,
"elapsed_ms": 1550.94,
"event_loop_lag": {
"count": 113,
"median_ms": 3.39,
"p95_ms": 10.13,
"max_ms": 12.19
}
},
{
"scenario": "capacity_and_cancel",
"active_limit": 200,
"overflow_rejected": true,
"cancelled": 200,
"cancel_latency": {
"count": 200,
"median_ms": 4.26,
"p95_ms": 5.85,
"max_ms": 12.49
},
"elapsed_ms": 1977.57,
"event_loop_lag": {
"count": 167,
"median_ms": 1.68,
"p95_ms": 4.24,
"max_ms": 6.54
}
},
{
"scenario": "permission_wait",
"runs": 20,
"approved_completed": 10,
"cancelled_waiting_permission": 10,
"subscribers_released": true,
"elapsed_ms": 294.49,
"event_loop_lag": {
"count": 20,
"median_ms": 4.08,
"p95_ms": 6.57,
"max_ms": 9.92
}
},
{
"scenario": "failure_and_timeout_isolation",
"runs": 20,
"success": 5,
"provider_errors": 5,
"model_timeouts": 5,
"tool_timeouts": 5,
"remaining_tool_executors": 0,
"elapsed_ms": 1220.51,
"event_loop_lag": {
"count": 80,
"median_ms": 5.38,
"p95_ms": 6.33,
"max_ms": 11.29
}
},
{
"scenario": "task_api_crud",
"tasks": 100,
"client_concurrency": 20,
"latencies": {
"create": {
"count": 100,
"median_ms": 103.05,
"p95_ms": 143.89,
"max_ms": 167.59
},
"update": {
"count": 100,
"median_ms": 124.21,
"p95_ms": 139.58,
"max_ms": 146.49
},
"list": {
"count": 1,
"median_ms": 3.33,
"p95_ms": 3.33,
"max_ms": 3.33
},
"delete": {
"count": 100,
"median_ms": 118.6,
"p95_ms": 134.84,
"max_ms": 147.0
}
},
"default_page_count": 50,
"default_total": 100,
"pagination_complete": true,
"final_total": 0,
"elapsed_ms": 1841.45,
"event_loop_lag": {
"count": 146,
"median_ms": 1.87,
"p95_ms": 5.05,
"max_ms": 65.53
}
},
{
"scenario": "task_api_crud",
"tasks": 1000,
"client_concurrency": 20,
"latencies": {
"create": {
"count": 1000,
"median_ms": 109.16,
"p95_ms": 121.67,
"max_ms": 133.03
},
"update": {
"count": 1000,
"median_ms": 125.42,
"p95_ms": 146.2,
"max_ms": 165.48
},
"list": {
"count": 10,
"median_ms": 5.12,
"p95_ms": 7.31,
"max_ms": 7.31
},
"delete": {
"count": 1000,
"median_ms": 122.71,
"p95_ms": 145.87,
"max_ms": 171.81
}
},
"default_page_count": 50,
"default_total": 1000,
"pagination_complete": true,
"final_total": 0,
"elapsed_ms": 18067.11,
"event_loop_lag": {
"count": 1540,
"median_ms": 1.33,
"p95_ms": 4.46,
"max_ms": 31.93
}
}
],
"database_bytes": 2932736,
"complete": true
}
@@ -0,0 +1,230 @@
{
"python": "3.13.9",
"platform": "Windows-11-10.0.26100-SP0",
"provider": "mock with 50 ms injected delay per model turn; no network",
"results": [
{
"scenario": "agent_tool_runs",
"concurrency": 1,
"runs": 20,
"latency": {
"count": 20,
"median_ms": 167.71,
"p95_ms": 181.07,
"max_ms": 203.5
},
"completed": 20,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 20,
"elapsed_ms": 3464.02,
"event_loop_lag": {
"count": 224,
"median_ms": 5.41,
"p95_ms": 13.45,
"max_ms": 33.56
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 10,
"runs": 20,
"latency": {
"count": 20,
"median_ms": 219.61,
"p95_ms": 224.28,
"max_ms": 224.39
},
"completed": 20,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 40,
"elapsed_ms": 500.46,
"event_loop_lag": {
"count": 33,
"median_ms": 4.35,
"p95_ms": 13.1,
"max_ms": 24.21
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 50,
"runs": 50,
"latency": {
"count": 50,
"median_ms": 356.51,
"p95_ms": 358.17,
"max_ms": 358.28
},
"completed": 50,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 90,
"elapsed_ms": 495.22,
"event_loop_lag": {
"count": 27,
"median_ms": 2.66,
"p95_ms": 35.07,
"max_ms": 73.78
}
},
{
"scenario": "agent_tool_runs",
"concurrency": 200,
"runs": 200,
"latency": {
"count": 200,
"median_ms": 944.9,
"p95_ms": 1007.22,
"max_ms": 1007.96
},
"completed": 200,
"ordered_events_and_replay": true,
"terminal_recovery": true,
"retained_records": 200,
"elapsed_ms": 1412.6,
"event_loop_lag": {
"count": 77,
"median_ms": 3.46,
"p95_ms": 10.98,
"max_ms": 326.14
}
},
{
"scenario": "capacity_and_cancel",
"active_limit": 200,
"overflow_rejected": true,
"cancelled": 200,
"cancel_latency": {
"count": 200,
"median_ms": 4.47,
"p95_ms": 6.37,
"max_ms": 14.65
},
"elapsed_ms": 2086.28,
"event_loop_lag": {
"count": 174,
"median_ms": 1.48,
"p95_ms": 4.69,
"max_ms": 14.19
}
},
{
"scenario": "permission_wait",
"runs": 20,
"approved_completed": 10,
"cancelled_waiting_permission": 10,
"subscribers_released": true,
"elapsed_ms": 307.52,
"event_loop_lag": {
"count": 20,
"median_ms": 5.28,
"p95_ms": 12.2,
"max_ms": 15.35
}
},
{
"scenario": "failure_and_timeout_isolation",
"runs": 20,
"success": 5,
"provider_errors": 5,
"model_timeouts": 5,
"tool_timeouts": 5,
"remaining_tool_executors": 0,
"elapsed_ms": 1236.24,
"event_loop_lag": {
"count": 80,
"median_ms": 5.44,
"p95_ms": 6.58,
"max_ms": 15.75
}
},
{
"scenario": "task_api_crud",
"tasks": 100,
"client_concurrency": 20,
"latencies": {
"create": {
"count": 100,
"median_ms": 107.62,
"p95_ms": 146.4,
"max_ms": 177.83
},
"update": {
"count": 100,
"median_ms": 119.9,
"p95_ms": 148.86,
"max_ms": 176.92
},
"list": {
"count": 1,
"median_ms": 3.96,
"p95_ms": 3.96,
"max_ms": 3.96
},
"delete": {
"count": 100,
"median_ms": 126.81,
"p95_ms": 180.34,
"max_ms": 181.32
}
},
"default_page_count": 50,
"default_total": 100,
"pagination_complete": true,
"final_total": 0,
"elapsed_ms": 1908.85,
"event_loop_lag": {
"count": 156,
"median_ms": 1.39,
"p95_ms": 5.2,
"max_ms": 65.37
}
},
{
"scenario": "task_api_crud",
"tasks": 1000,
"client_concurrency": 20,
"latencies": {
"create": {
"count": 1000,
"median_ms": 109.25,
"p95_ms": 122.91,
"max_ms": 130.79
},
"update": {
"count": 1000,
"median_ms": 127.29,
"p95_ms": 150.37,
"max_ms": 180.05
},
"list": {
"count": 10,
"median_ms": 4.6,
"p95_ms": 8.08,
"max_ms": 8.08
},
"delete": {
"count": 1000,
"median_ms": 124.33,
"p95_ms": 146.88,
"max_ms": 167.34
}
},
"default_page_count": 50,
"default_total": 1000,
"pagination_complete": true,
"final_total": 0,
"elapsed_ms": 18264.74,
"event_loop_lag": {
"count": 1555,
"median_ms": 1.31,
"p95_ms": 4.68,
"max_ms": 39.35
}
}
],
"database_bytes": 2928640,
"complete": true
}
@@ -0,0 +1,142 @@
{
"run_id": "benchmark_e3047791a92d",
"kind": "agent",
"dataset_id": "agent-core-v1",
"dataset_hash": "sha256:82fd1b84cc2dc2feec0fc67357d64b145dcdf6c44ec2660f182d550e85b9d27f",
"status": "completed",
"config_snapshot": {
"dataset_id": "agent-core-v1",
"provider_id": "provider_65fd326edba646e3bc735c25fbf5ffe8",
"model": "deepseek-v4-flash",
"max_steps": 6,
"timeout_seconds": 90,
"token_budget": 6000,
"repeat": 1,
"allow_network": false,
"offline": false,
"dataset_hash": "sha256:82fd1b84cc2dc2feec0fc67357d64b145dcdf6c44ec2660f182d550e85b9d27f",
"dataset_version": "1.0.0",
"execution": "real_agent_runtime",
"provider_type": "openai_compatible",
"scoring_version": "1.0",
"permission_policy": "runtime_user_decision",
"active_agent_run_id": "run_1230c37a1c2c4559b69cbc4e01b4721b"
},
"metrics": {
"total_cases": 4,
"evaluated_cases": 4,
"task_success_rate": 1,
"tool_selection_accuracy": 1,
"tool_argument_accuracy": 1,
"invalid_tool_call_rate": 0,
"average_steps": 1.75,
"average_latency_ms": 4646.515175001696,
"token_usage": 5939,
"tool_calls": 3,
"expected_calls": 3
},
"cases": [
{
"case_id": "arithmetic",
"repeat": 0,
"agent_run_id": "run_f4baf45c66c34e3f94b60b90f0846a95",
"success": true,
"tool_calls": 1,
"expected_calls": 1,
"selected_calls": 1,
"accurate_calls": 1,
"invalid_calls": 0,
"steps": 2,
"latency_ms": 4902.329800010193,
"token_usage": 1585,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
},
{
"case_id": "echo",
"repeat": 0,
"agent_run_id": "run_5cf8341f73fa431ab7978f9ed4fe84c7",
"success": true,
"tool_calls": 1,
"expected_calls": 1,
"selected_calls": 1,
"accurate_calls": 1,
"invalid_calls": 0,
"steps": 2,
"latency_ms": 5341.693700000178,
"token_usage": 1821,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
},
{
"case_id": "no-tool",
"repeat": 0,
"agent_run_id": "run_226a20145fbc4737ae2d3e812ff8f25c",
"success": true,
"tool_calls": 0,
"expected_calls": 0,
"selected_calls": 0,
"accurate_calls": 0,
"invalid_calls": 0,
"steps": 1,
"latency_ms": 1806.5612000063993,
"token_usage": 541,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
},
{
"case_id": "markdown-catalog",
"repeat": 0,
"agent_run_id": "run_1230c37a1c2c4559b69cbc4e01b4721b",
"success": true,
"tool_calls": 1,
"expected_calls": 1,
"selected_calls": 1,
"accurate_calls": 1,
"invalid_calls": 0,
"steps": 2,
"latency_ms": 6535.4759999900125,
"token_usage": 1992,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
}
],
"error": null,
"error_code": null
}
@@ -0,0 +1,46 @@
{
"html": {
"bytes": 42542,
"sha256": "4c5e3f5eeb0167110cbbc750c12ef0a52420975288853f9653575cb49f3b4098",
"snapshot_present": true,
"static_images": 2
},
"pdf": {
"bytes": 53952,
"sha256": "a2aa2095495cfa612f4b3f2e2c35104f305289468bb5a8a5de8748a899d8f598"
},
"docx": {
"bytes": 59638,
"sha256": "ade1b2d993c8e157e245af5476a7808c891d70ae91b82ac300e12d3fa727288e",
"snapshot_present": true,
"images": [
{
"name": "word/media/image1.png",
"width": 1280,
"height": 1008
},
{
"name": "word/media/image2.png",
"width": 1000,
"height": 280
},
{
"name": "word/media/image3.png",
"width": 135,
"height": 32
}
]
},
"browser_interactions": [
"six_themes",
"wide_narrow_viewer",
"source_toggle",
"zoom_reset",
"edit_refresh",
"theme_switch",
"invalid_expression",
"html_pdf_docx_download",
"print_warning",
"export_cancelled"
]
}
@@ -0,0 +1,19 @@
........................................................................ [ 8%]
........................................................................ [ 16%]
........................................................................ [ 25%]
........................................................................ [ 33%]
........................................................................ [ 41%]
........................................................................ [ 50%]
........................................................................ [ 58%]
........................................................................ [ 66%]
........................................................................ [ 75%]
........................................................................ [ 83%]
........................................................................ [ 91%]
..................................................................... [100%]
============================== warnings summary ===============================
.venv\Lib\site-packages\fastapi\testclient.py:1
C:\Users\KiriAky\.codex\worktrees\7950\NotesAgent\backend\.venv\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
861 passed, 1 warning in 48.32s
@@ -0,0 +1,222 @@
{
"passed": 36,
"total": 36,
"results": [
{
"theme": "light",
"type": "flowchart",
"passed": true,
"warnings": []
},
{
"theme": "light",
"type": "sequence",
"passed": true,
"warnings": []
},
{
"theme": "light",
"type": "class",
"passed": true,
"warnings": []
},
{
"theme": "light",
"type": "state",
"passed": true,
"warnings": []
},
{
"theme": "light",
"type": "er",
"passed": true,
"warnings": []
},
{
"theme": "light",
"type": "gantt",
"passed": true,
"warnings": []
},
{
"theme": "dark",
"type": "flowchart",
"passed": true,
"warnings": []
},
{
"theme": "dark",
"type": "sequence",
"passed": true,
"warnings": []
},
{
"theme": "dark",
"type": "class",
"passed": true,
"warnings": []
},
{
"theme": "dark",
"type": "state",
"passed": true,
"warnings": []
},
{
"theme": "dark",
"type": "er",
"passed": true,
"warnings": []
},
{
"theme": "dark",
"type": "gantt",
"passed": true,
"warnings": []
},
{
"theme": "sepia",
"type": "flowchart",
"passed": true,
"warnings": []
},
{
"theme": "sepia",
"type": "sequence",
"passed": true,
"warnings": []
},
{
"theme": "sepia",
"type": "class",
"passed": true,
"warnings": []
},
{
"theme": "sepia",
"type": "state",
"passed": true,
"warnings": []
},
{
"theme": "sepia",
"type": "er",
"passed": true,
"warnings": []
},
{
"theme": "sepia",
"type": "gantt",
"passed": true,
"warnings": []
},
{
"theme": "paper-moments",
"type": "flowchart",
"passed": true,
"warnings": []
},
{
"theme": "paper-moments",
"type": "sequence",
"passed": true,
"warnings": []
},
{
"theme": "paper-moments",
"type": "class",
"passed": true,
"warnings": []
},
{
"theme": "paper-moments",
"type": "state",
"passed": true,
"warnings": []
},
{
"theme": "paper-moments",
"type": "er",
"passed": true,
"warnings": []
},
{
"theme": "paper-moments",
"type": "gantt",
"passed": true,
"warnings": []
},
{
"theme": "ocean-blue",
"type": "flowchart",
"passed": true,
"warnings": []
},
{
"theme": "ocean-blue",
"type": "sequence",
"passed": true,
"warnings": []
},
{
"theme": "ocean-blue",
"type": "class",
"passed": true,
"warnings": []
},
{
"theme": "ocean-blue",
"type": "state",
"passed": true,
"warnings": []
},
{
"theme": "ocean-blue",
"type": "er",
"passed": true,
"warnings": []
},
{
"theme": "ocean-blue",
"type": "gantt",
"passed": true,
"warnings": []
},
{
"theme": "midnight-purple",
"type": "flowchart",
"passed": true,
"warnings": []
},
{
"theme": "midnight-purple",
"type": "sequence",
"passed": true,
"warnings": []
},
{
"theme": "midnight-purple",
"type": "class",
"passed": true,
"warnings": []
},
{
"theme": "midnight-purple",
"type": "state",
"passed": true,
"warnings": []
},
{
"theme": "midnight-purple",
"type": "er",
"passed": true,
"warnings": []
},
{
"theme": "midnight-purple",
"type": "gantt",
"passed": true,
"warnings": []
}
]
}
@@ -0,0 +1,32 @@
[
{
"theme": "light",
"errors": [],
"plotCount": 4
},
{
"theme": "dark",
"errors": [],
"plotCount": 4
},
{
"theme": "sepia",
"errors": [],
"plotCount": 4
},
{
"theme": "paper-moments",
"errors": [],
"plotCount": 4
},
{
"theme": "ocean-blue",
"errors": [],
"plotCount": 4
},
{
"theme": "midnight-purple",
"errors": [],
"plotCount": 4
}
]
@@ -0,0 +1,560 @@
> notes-agent-frontend@0.2.0 build C:\Users\KiriAky\.codex\worktrees\7950\NotesAgent\frontend
> vue-tsc -b && vite build
vite v6.4.3 building for production...
transforming...
✓ 3262 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.55 kB │ gzip: 0.33 kB
dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff 4.42 kB
dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 4.93 kB
dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 5.21 kB
dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 5.47 kB
dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff 5.98 kB
dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff 6.19 kB
dist/assets/KaTeX_Size1-Regular-C195tn64.woff 6.50 kB
dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 6.91 kB
dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 6.91 kB
dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf 7.59 kB
dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff 7.66 kB
dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff 7.72 kB
dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 9.64 kB
dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 10.34 kB
dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf 10.36 kB
dist/assets/KaTeX_Script-Regular-D5yQViql.woff 10.59 kB
dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 11.32 kB
dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 11.35 kB
dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf 11.51 kB
dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 12.03 kB
dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 12.22 kB
dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf 12.23 kB
dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff 12.32 kB
dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf 12.34 kB
dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf 12.37 kB
dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff 13.21 kB
dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff 13.30 kB
dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 13.57 kB
dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff 14.11 kB
dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff 14.41 kB
dist/assets/paper-moments-DOmjCJQa.theme 15.58 kB
dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff 16.03 kB
dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 16.40 kB
dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 16.44 kB
dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf 16.65 kB
dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 16.78 kB
dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 16.99 kB
dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff 18.67 kB
dist/assets/KaTeX_Math-Italic-DA0__PXp.woff 18.75 kB
dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff 19.41 kB
dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf 19.44 kB
dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf 19.57 kB
dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf 19.58 kB
dist/assets/KaTeX_Main-Italic-BMLOBm91.woff 19.68 kB
dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf 22.36 kB
dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf 24.50 kB
dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 25.32 kB
dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 26.27 kB
dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf 27.56 kB
dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 28.08 kB
dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff 29.91 kB
dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff 30.77 kB
dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf 31.20 kB
dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf 31.31 kB
dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf 32.97 kB
dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff 33.52 kB
dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf 33.58 kB
dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf 51.34 kB
dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf 53.58 kB
dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf 63.63 kB
dist/.vite/manifest.json 258.05 kB │ gzip: 20.84 kB
dist/assets/HeadingStyleSettings-DHfF7b37.css 0.57 kB │ gzip: 0.29 kB
dist/assets/SkillsView-BwLulPo9.css 0.98 kB │ gzip: 0.39 kB
dist/assets/WorkspaceChat-DhGri1NS.css 1.09 kB │ gzip: 0.48 kB
dist/assets/ExtensionInstallDialog-DRNL9bJf.css 1.14 kB │ gzip: 0.45 kB
dist/assets/LogsView-BrIX2apd.css 1.35 kB │ gzip: 0.53 kB
dist/assets/BenchmarkView-DLutUBdD.css 1.39 kB │ gzip: 0.55 kB
dist/assets/TasksView-DUgcfVCX.css 1.47 kB │ gzip: 0.58 kB
dist/assets/SearchView-DW-1gSdx.css 1.47 kB │ gzip: 0.56 kB
dist/assets/FilePicker-BO9C6lOI.css 1.63 kB │ gzip: 0.60 kB
dist/assets/MediaView-D48AE7U_.css 1.98 kB │ gzip: 0.69 kB
dist/assets/ChatPersonaDialog-DHMXahgY.css 2.03 kB │ gzip: 0.72 kB
dist/assets/MarkdownContent-CLXIe9X7.css 3.56 kB │ gzip: 0.94 kB
dist/assets/WorkspaceView-yzL09utK.css 4.35 kB │ gzip: 1.23 kB
dist/assets/McpServersView-BFLRUwRT.css 5.10 kB │ gzip: 1.25 kB
dist/assets/ChatView-BQEJrX-5.css 5.20 kB │ gzip: 1.41 kB
dist/assets/markdown-D2L0VTei.css 5.43 kB │ gzip: 1.51 kB
dist/assets/VaultEntry-DjSz9QsE.css 5.64 kB │ gzip: 1.37 kB
dist/assets/ThemesView-i5ccHc0M.css 7.00 kB │ gzip: 1.72 kB
dist/assets/PluginsView-CwBTnoBl.css 11.21 kB │ gzip: 2.11 kB
dist/assets/AgentView-DSWsxP_V.css 12.59 kB │ gzip: 2.31 kB
dist/assets/SettingsView-CNkfTZZK.css 13.85 kB │ gzip: 3.02 kB
dist/assets/math-katex-0-18-4-CEK31ho9.css 30.19 kB │ gzip: 8.06 kB
dist/assets/index-DUxb9c05.css 50.43 kB │ gzip: 9.59 kB
dist/assets/editor-milkdown-DebjlF2b.css 83.87 kB │ gzip: 16.14 kB
dist/assets/VisualMarkdownEditor-IxqBomSx.css 390.03 kB │ gzip: 105.48 kB
dist/assets/channel-CrNH6nrN.js 0.11 kB │ gzip: 0.13 kB
dist/assets/init-Gi6I4Gst.js 0.15 kB │ gzip: 0.13 kB
dist/assets/chunk-2Q5K7J3B-BZASLIXN.js 0.19 kB │ gzip: 0.16 kB
dist/assets/chunk-XXDRQBXY-ChINTzlJ.js 0.23 kB │ gzip: 0.21 kB
dist/assets/chunk-JWPE2WC7-BNAOuj8n.js 0.30 kB │ gzip: 0.21 kB
dist/assets/diff-DbItnlRl.js 0.31 kB │ gzip: 0.24 kB
dist/assets/chunk-5VM5RSS4-TQefSFWe.js 0.37 kB │ gzip: 0.27 kB
dist/assets/index-DIIxsTGV.js 0.48 kB │ gzip: 0.31 kB
dist/assets/stateDiagram-v2-MP3YSRHH-Nc43I8Ns.js 0.51 kB │ gzip: 0.35 kB
dist/assets/chunk-POPQ4Y6H-BG03mjXr.js 0.53 kB │ gzip: 0.38 kB
dist/assets/codeowners-Bp6g37R7.js 0.55 kB │ gzip: 0.32 kB
dist/assets/classDiagram-ZZMXUADV-CI5Lv9Pw.js 0.55 kB │ gzip: 0.36 kB
dist/assets/classDiagram-v2-VYDZK3BY-CI5Lv9Pw.js 0.55 kB │ gzip: 0.36 kB
dist/assets/brainfuck-C4LP7Hcl.js 0.61 kB │ gzip: 0.33 kB
dist/assets/swimlanesDiagram-VR7AAH4N-C4Ag8puk.js 0.63 kB │ gzip: 0.41 kB
dist/assets/infoDiagram-27XIBGKW-Bb4UVFAL.js 0.66 kB │ gzip: 0.45 kB
dist/assets/properties-C78fOPTZ.js 0.67 kB │ gzip: 0.35 kB
dist/assets/shellsession-BADoaaVG.js 0.71 kB │ gzip: 0.43 kB
dist/assets/tsv-B_m7g4N7.js 0.74 kB │ gzip: 0.34 kB
dist/assets/cmake-BQqOBYOt.js 0.78 kB │ gzip: 0.46 kB
dist/assets/asciiarmor-Df11BRmG.js 0.79 kB │ gzip: 0.41 kB
dist/assets/http-DBlCnlav.js 0.85 kB │ gzip: 0.41 kB
dist/assets/protobuf-ChK-085T.js 0.86 kB │ gzip: 0.52 kB
dist/assets/solr-DehyRSwq.js 0.87 kB │ gzip: 0.46 kB
dist/assets/html-derivative-DlHx6ybY.js 0.90 kB │ gzip: 0.50 kB
dist/assets/troff-wAsdV37c.js 0.96 kB │ gzip: 0.44 kB
dist/assets/git-rebase-r7XF79zn.js 0.98 kB │ gzip: 0.44 kB
dist/assets/qmldir-C8lEn-DE.js 1.00 kB │ gzip: 0.45 kB
dist/assets/FilePicker-CjNjYr-N.js 1.04 kB │ gzip: 0.61 kB
dist/assets/sizeCapture-INFHLROL-DWCqhl6c.js 1.08 kB │ gzip: 0.61 kB
dist/assets/spreadsheet-BCZA_wO0.js 1.14 kB │ gzip: 0.54 kB
dist/assets/toml-Bm5Em-hy.js 1.14 kB │ gzip: 0.54 kB
dist/assets/csv-fuZLfV_i.js 1.14 kB │ gzip: 0.37 kB
dist/assets/ordinal-Cboi1Yqb.js 1.19 kB │ gzip: 0.57 kB
dist/assets/git-commit-F4YmCXRG.js 1.23 kB │ gzip: 0.53 kB
dist/assets/xsl-CtQFsRM5.js 1.39 kB │ gzip: 0.52 kB
dist/assets/mbox-CNhZ1qSd.js 1.40 kB │ gzip: 0.66 kB
dist/assets/dotenv-Da5cRb03.js 1.42 kB │ gzip: 0.53 kB
dist/assets/sparql-rVzFXLq3.js 1.48 kB │ gzip: 0.82 kB
dist/assets/index-Vcq4gwWv.js 1.49 kB │ gzip: 0.82 kB
dist/assets/ini-BEwlwnbL.js 1.53 kB │ gzip: 0.50 kB
dist/assets/MarkdownContent.vue_vue_type_style_index_0_lang-B376YVIn.js 1.56 kB │ gzip: 0.83 kB
dist/assets/sieve-C3Gn_uJK.js 1.62 kB │ gzip: 0.77 kB
dist/assets/rpm-CTu-6PCP.js 1.62 kB │ gzip: 0.83 kB
dist/assets/markdownPreferences-DKoh9bVv.js 1.66 kB │ gzip: 0.86 kB
dist/assets/fortran-fixed-form-CkoXwp7k.js 1.67 kB │ gzip: 0.69 kB
dist/assets/factor-kuTfRLto.js 1.67 kB │ gzip: 0.61 kB
dist/assets/railroadDiagram-O6MQD6OU-DwB0kF9i.js 1.69 kB │ gzip: 0.80 kB
dist/assets/docker-BcOcwvcX.js 1.74 kB │ gzip: 0.60 kB
dist/assets/z80-Hz9HOZM7.js 1.75 kB │ gzip: 0.81 kB
dist/assets/eiffel-CnydiIhH.js 1.81 kB │ gzip: 0.92 kB
dist/assets/desktop-BmXAJ9_W.js 1.83 kB │ gzip: 0.76 kB
dist/assets/pluginCommandForm-DV9KKjoP.js 1.85 kB │ gzip: 0.95 kB
dist/assets/chunk-F27PBJKO-Ctsjd131.js 1.88 kB │ gzip: 0.83 kB
dist/assets/abnfDiagram-VCTEODGH-s3lnCumS.js 1.89 kB │ gzip: 0.94 kB
dist/assets/elm-vLlmbW-K.js 1.89 kB │ gzip: 0.82 kB
dist/assets/hxml-2-FPmUDs.js 1.89 kB │ gzip: 0.90 kB
dist/assets/mathematica-DTrFuWx2.js 1.92 kB │ gzip: 0.82 kB
dist/assets/dockerfile-BKs6k2Af.js 1.95 kB │ gzip: 0.67 kB
dist/assets/turtle-B1tBg_DP.js 1.98 kB │ gzip: 0.90 kB
dist/assets/ebnf-CDyGwa7X.js 1.99 kB │ gzip: 0.81 kB
dist/assets/smalltalk-CnHTOXQT.js 2.01 kB │ gzip: 0.85 kB
dist/assets/pegDiagram-XKGWAZYB-CUdM1qfX.js 2.04 kB │ gzip: 0.97 kB
dist/assets/dtd-DF_7sFjM.js 2.06 kB │ gzip: 0.87 kB
dist/assets/mumps-BT43cFF4.js 2.07 kB │ gzip: 0.98 kB
dist/assets/fcl-Kvtd6kyn.js 2.08 kB │ gzip: 0.95 kB
dist/assets/ebnfDiagram-PWID7BFC-B0NYa1C1.js 2.09 kB │ gzip: 0.92 kB
dist/assets/ntriples-BfvgReVJ.js 2.10 kB │ gzip: 0.75 kB
dist/assets/yacas-BJ4BC0dw.js 2.15 kB │ gzip: 1.09 kB
dist/assets/wenyan-BV7otONQ.js 2.16 kB │ gzip: 1.09 kB
dist/assets/jssm-C2t-YnRu.js 2.24 kB │ gzip: 0.62 kB
dist/assets/simple-mode-GW_nhZxv.js 2.28 kB │ gzip: 1.09 kB
dist/assets/apl-B4CMkyY2.js 2.30 kB │ gzip: 1.23 kB
dist/assets/pascal--L3eBynH.js 2.30 kB │ gzip: 1.19 kB
dist/assets/octave-Ck1zUtKM.js 2.31 kB │ gzip: 1.09 kB
dist/assets/commonlisp-DBKNyK5s.js 2.32 kB │ gzip: 1.10 kB
dist/assets/reg-C-SQnVFl.js 2.35 kB │ gzip: 0.70 kB
dist/assets/tcl-DVfN8rqt.js 2.36 kB │ gzip: 1.23 kB
dist/assets/edge-FbVlp4U3.js 2.36 kB │ gzip: 0.70 kB
dist/assets/index-CjhMCctz.js 2.46 kB │ gzip: 1.52 kB
dist/assets/webidl-ZXfAyPTL.js 2.52 kB │ gzip: 1.26 kB
dist/assets/pig-CevX1Tat.js 2.53 kB │ gzip: 1.38 kB
dist/assets/puppet-DMA9R1ak.js 2.54 kB │ gzip: 1.20 kB
dist/assets/forth-Ffai-XNe.js 2.54 kB │ gzip: 1.33 kB
dist/assets/diff-D97Zzqfu.js 2.57 kB │ gzip: 0.70 kB
dist/assets/shell-CjFT_Tl9.js 2.57 kB │ gzip: 1.21 kB
dist/assets/gleam-BspZqrRM.js 2.58 kB │ gzip: 0.82 kB
dist/assets/erb-DXfck5VN.js 2.61 kB │ gzip: 0.84 kB
dist/assets/hy-DFXneXwc.js 2.65 kB │ gzip: 1.18 kB
dist/assets/index-Bu1SCIgx.js 2.66 kB │ gzip: 1.53 kB
dist/assets/velocity-D8B20fx6.js 2.67 kB │ gzip: 1.11 kB
dist/assets/tiddlywiki-DO-Gjzrf.js 2.78 kB │ gzip: 1.07 kB
dist/assets/modelica-Dc1JOy9r.js 2.79 kB │ gzip: 1.29 kB
dist/assets/json-Cp-IABpG.js 2.82 kB │ gzip: 0.78 kB
dist/assets/openscad-C4EeE6gA.js 2.82 kB │ gzip: 1.01 kB
dist/assets/log-2UxHyX5q.js 2.85 kB │ gzip: 0.90 kB
dist/assets/oz-BzwKVEFT.js 2.90 kB │ gzip: 1.28 kB
dist/assets/cairo-KRGpt6FW.js 2.94 kB │ gzip: 0.81 kB
dist/assets/r-B6wPVr8A.js 2.94 kB │ gzip: 1.36 kB
dist/assets/berry-uYugtg8r.js 3.01 kB │ gzip: 0.81 kB
dist/assets/jsonl-DcaNXYhu.js 3.01 kB │ gzip: 0.79 kB
dist/assets/jsonc-Des-eS-w.js 3.11 kB │ gzip: 0.80 kB
dist/assets/HeadingStyleSettings-Bhiwz6_B.js 3.12 kB │ gzip: 1.48 kB
dist/assets/stex-C3f8Ysf7.js 3.12 kB │ gzip: 1.20 kB
dist/assets/logo-BtOb2qkB.js 3.13 kB │ gzip: 1.47 kB
dist/assets/po-BTJTHyun.js 3.24 kB │ gzip: 0.91 kB
dist/assets/tiki-DGYXhP31.js 3.25 kB │ gzip: 1.26 kB
dist/assets/json5-C9tS-k6U.js 3.25 kB │ gzip: 0.83 kB
dist/assets/mipsasm-CKIfxQSi.js 3.26 kB │ gzip: 1.18 kB
dist/assets/tasl-QIJgUcNo.js 3.29 kB │ gzip: 0.85 kB
dist/assets/rbs-CpoqiR4B.js 3.31 kB │ gzip: 0.73 kB
dist/assets/vhdl-lSbBsy5d.js 3.35 kB │ gzip: 1.51 kB
dist/assets/genie-D0YGMca9.js 3.36 kB │ gzip: 1.21 kB
dist/assets/rel-C3B-1QV4.js 3.37 kB │ gzip: 1.11 kB
dist/assets/vala-CsfeWuGM.js 3.37 kB │ gzip: 1.19 kB
dist/assets/lua-VAEuO923.js 3.41 kB │ gzip: 1.43 kB
dist/assets/mermaidService-EVMjUBQI.js 3.42 kB │ gzip: 1.79 kB
dist/assets/arc-DnWybcCe.js 3.43 kB │ gzip: 1.47 kB
dist/assets/splunk-BtCnVYZw.js 3.44 kB │ gzip: 1.52 kB
dist/assets/index-833-DuH6.js 3.44 kB │ gzip: 1.74 kB
dist/assets/mscgen-BA5vi2Kp.js 3.51 kB │ gzip: 0.98 kB
dist/assets/sparql-DkYu6x3z.js 3.55 kB │ gzip: 1.63 kB
dist/assets/cypher-C_CwsFkJ.js 3.56 kB │ gzip: 1.52 kB
dist/assets/fluent-C4IJs8-o.js 3.61 kB │ gzip: 0.90 kB
dist/assets/ssh-config-_ykCGR6B.js 3.62 kB │ gzip: 1.60 kB
dist/assets/jsonnet-DFQXde-d.js 3.62 kB │ gzip: 1.05 kB
dist/assets/kdl-DV7GczEv.js 3.63 kB │ gzip: 1.04 kB
dist/assets/glsl-DplSGwfg.js 3.63 kB │ gzip: 1.41 kB
dist/assets/hurl-irOxFIW8.js 3.65 kB │ gzip: 1.16 kB
dist/assets/narrat-DRg8JJMk.js 3.67 kB │ gzip: 1.11 kB
dist/assets/_baseMerge-BNBydYQA.js 3.68 kB │ gzip: 1.73 kB
dist/assets/turtle-BsS91CYL.js 3.70 kB │ gzip: 0.98 kB
dist/assets/d-pRatUO7H.js 3.72 kB │ gzip: 1.68 kB
dist/assets/coffeescript-S37ZYGWr.js 3.87 kB │ gzip: 1.69 kB
dist/assets/vb-CmGdzxic.js 3.90 kB │ gzip: 1.79 kB
dist/assets/zenscript-DVFEvuxE.js 3.91 kB │ gzip: 1.28 kB
dist/assets/ron-D8l8udqQ.js 3.91 kB │ gzip: 0.98 kB
dist/assets/swift-BzpIVaGY.js 3.96 kB │ gzip: 1.79 kB
dist/assets/VaultEntry-DmoPppw3.js 3.96 kB │ gzip: 1.95 kB
dist/assets/WorkspaceChat-CrwU7la3.js 3.97 kB │ gzip: 1.89 kB
dist/assets/asn1-EdZsLKOL.js 3.98 kB │ gzip: 1.96 kB
dist/assets/gn-n2N0HUVH.js 4.00 kB │ gzip: 1.49 kB
dist/assets/q-pXgVlZs6.js 4.03 kB │ gzip: 1.67 kB
dist/assets/ttcn-cfg-B9xdYoR4.js 4.05 kB │ gzip: 1.71 kB
dist/assets/dylan-DwRh75JA.js 4.05 kB │ gzip: 1.65 kB
dist/assets/livescript-BwQOo05w.js 4.09 kB │ gzip: 1.64 kB
dist/assets/groovy-D9Dt4D0W.js 4.14 kB │ gzip: 1.77 kB
dist/assets/pascal-D93ZcfNL.js 4.15 kB │ gzip: 1.67 kB
dist/assets/haskell-Cw1EW3IL.js 4.17 kB │ gzip: 1.89 kB
dist/assets/SearchView-B47FoyI1.js 4.26 kB │ gzip: 2.09 kB
dist/assets/diagram-Z3DM3KII-Bb5zNdmf.js 4.33 kB │ gzip: 1.91 kB
dist/assets/tcl-dwOrl1Do.js 4.43 kB │ gzip: 1.52 kB
dist/assets/asterisk-B-8jnY81.js 4.48 kB │ gzip: 1.78 kB
dist/assets/nextflow-C-mBbutL.js 4.51 kB │ gzip: 1.17 kB
dist/assets/rosmsg-BJDFO7_C.js 4.52 kB │ gzip: 1.06 kB
dist/assets/http-jrhK8wxY.js 4.55 kB │ gzip: 1.12 kB
dist/assets/gas-Bneqetm1.js 4.57 kB │ gzip: 1.28 kB
dist/assets/fortran-DYz_wnZ1.js 4.65 kB │ gzip: 2.01 kB
dist/assets/polar-C0HS_06l.js 4.67 kB │ gzip: 1.12 kB
dist/assets/defaultLocale-DX6XiGOO.js 4.69 kB │ gzip: 2.18 kB
dist/assets/sdbl-DVxCFoDh.js 4.70 kB │ gzip: 2.01 kB
dist/assets/fennel-BYunw83y.js 4.77 kB │ gzip: 1.53 kB
dist/assets/mllike-CXdrOF99.js 4.79 kB │ gzip: 1.53 kB
dist/assets/bibtex-CHM0blh-.js 4.80 kB │ gzip: 0.83 kB
dist/assets/ttcn-CfJYG6tj.js 4.80 kB │ gzip: 2.13 kB
dist/assets/SkillsView-BIOhy4r9.js 4.90 kB │ gzip: 2.06 kB
dist/assets/index-BlxF_W4d.js 4.95 kB │ gzip: 1.51 kB
dist/assets/llvm-BZoOZj88.js 5.04 kB │ gzip: 2.01 kB
dist/assets/index-E-vsI35b.js 5.10 kB │ gzip: 2.43 kB
dist/assets/index-D2e4uDXw.js 5.11 kB │ gzip: 2.55 kB
dist/assets/ecl-Cabwm37j.js 5.13 kB │ gzip: 2.31 kB
dist/assets/crystal-SjHAIU92.js 5.13 kB │ gzip: 2.07 kB
dist/assets/wgsl-Dx-B1_4e.js 5.14 kB │ gzip: 1.39 kB
dist/assets/ruby-B2Rjki9n.js 5.16 kB │ gzip: 2.15 kB
dist/assets/gdresource-TyuKm33G.js 5.29 kB │ gzip: 1.34 kB
dist/assets/qml-3beO22l8.js 5.34 kB │ gzip: 1.38 kB
dist/assets/zig-VOosw3JB.js 5.34 kB │ gzip: 1.55 kB
dist/assets/dax-CEL-wOlO.js 5.37 kB │ gzip: 2.23 kB
dist/assets/bicep-Bmn6On1c.js 5.38 kB │ gzip: 1.15 kB
dist/assets/xml-sdJ4AIDG.js 5.38 kB │ gzip: 1.21 kB
dist/assets/julia-DuME0IfC.js 5.39 kB │ gzip: 2.14 kB
dist/assets/awk-DMzUqQB5.js 5.46 kB │ gzip: 1.38 kB
dist/assets/browser-c3U3mMxj.js 5.53 kB │ gzip: 2.79 kB
dist/assets/TasksView-BnSHnCQV.js 5.65 kB │ gzip: 2.49 kB
dist/assets/linear-Dsc1LO7N.js 5.66 kB │ gzip: 2.31 kB
dist/assets/ExtensionInstallDialog-BSAwvlPk.js 5.68 kB │ gzip: 2.80 kB
dist/assets/jinja-f2NsQr07.js 5.69 kB │ gzip: 1.40 kB
dist/assets/lean-BZvkOJ9d.js 5.78 kB │ gzip: 1.92 kB
dist/assets/vbscript-BuJXcnF6.js 5.82 kB │ gzip: 2.57 kB
dist/assets/powerquery-CEu0bR-o.js 5.90 kB │ gzip: 1.52 kB
dist/assets/mirc-CjQqDB4T.js 5.92 kB │ gzip: 2.68 kB
dist/assets/shaderlab-Dg9Lc6iA.js 5.92 kB │ gzip: 2.08 kB
dist/assets/cypher-COkxafJQ.js 5.96 kB │ gzip: 1.73 kB
dist/assets/coq-C7JzOVbR.js 6.01 kB │ gzip: 1.95 kB
dist/assets/vb-Cu-pLBUe.js 6.08 kB │ gzip: 2.33 kB
dist/assets/cobol-CWcv1MsR.js 6.20 kB │ gzip: 2.96 kB
dist/assets/diagram-UQ7AKVKN-Cms5HwfU.js 6.31 kB │ gzip: 2.73 kB
dist/assets/gdshader-DkwncUOv.js 6.33 kB │ gzip: 1.73 kB
dist/assets/ara-BRHolxvo.js 6.36 kB │ gzip: 1.81 kB
dist/assets/scheme-C41bIUwD.js 6.37 kB │ gzip: 2.39 kB
dist/assets/verilog-nZwndyjY.js 6.41 kB │ gzip: 1.92 kB
dist/assets/clojure-P80f7IUj.js 6.41 kB │ gzip: 1.42 kB
dist/assets/postcss-CXtECtnM.js 6.42 kB │ gzip: 1.91 kB
dist/assets/toml-vGWfd6FD.js 6.43 kB │ gzip: 1.28 kB
dist/assets/pieDiagram-E7YTZNPT-YK6ICI5_.js 6.43 kB │ gzip: 2.70 kB
dist/assets/python-BuPzkPfP.js 6.48 kB │ gzip: 2.73 kB
dist/assets/LogsView-ZSnm_Csu.js 6.52 kB │ gzip: 3.14 kB
dist/assets/proto-C7zT0LnQ.js 6.55 kB │ gzip: 1.42 kB
dist/assets/chapel-DTp_pixX.js 6.62 kB │ gzip: 1.85 kB
dist/assets/xquery-DzFWVndE.js 6.62 kB │ gzip: 2.56 kB
dist/assets/pug-DeIclll2.js 6.67 kB │ gzip: 1.94 kB
dist/assets/r-Cf5RLm7j.js 6.74 kB │ gzip: 1.84 kB
dist/assets/talonscript-CkByrt1z.js 6.76 kB │ gzip: 1.49 kB
dist/assets/textile-CnDTJFAw.js 6.80 kB │ gzip: 2.44 kB
dist/assets/nsis-LdVXkNf5.js 6.81 kB │ gzip: 2.99 kB
dist/assets/BenchmarkView-CoNnmLzX.js 6.89 kB │ gzip: 3.19 kB
dist/assets/riscv-BM1_JUlF.js 6.91 kB │ gzip: 1.98 kB
dist/assets/soy-8wufbnw4.js 6.98 kB │ gzip: 1.66 kB
dist/assets/scheme-C98Dy4si.js 7.17 kB │ gzip: 2.05 kB
dist/assets/hlsl-D3lLCCz7.js 7.26 kB │ gzip: 2.19 kB
dist/assets/nginx-DdIZxoE0.js 7.34 kB │ gzip: 2.72 kB
dist/assets/qss-IeuSbFQv.js 7.47 kB │ gzip: 2.58 kB
dist/assets/prisma-Vru482bI.js 7.74 kB │ gzip: 1.48 kB
dist/assets/powershell-CFHJl5sT.js 7.77 kB │ gzip: 3.33 kB
dist/assets/ChatPersonaDialog-BHpRSHc3.js 7.84 kB │ gzip: 3.61 kB
dist/assets/systemd-4A_iFExJ.js 7.87 kB │ gzip: 2.55 kB
dist/assets/haxe-H-WmDvRZ.js 7.89 kB │ gzip: 2.95 kB
dist/assets/regexp-CDVJQ6XC.js 7.99 kB │ gzip: 1.42 kB
dist/assets/erlang-BNw1qcRV.js 8.10 kB │ gzip: 2.89 kB
dist/assets/verilog-C6RDOZhf.js 8.24 kB │ gzip: 3.52 kB
dist/assets/haml-D5jkg6IW.js 8.26 kB │ gzip: 1.81 kB
dist/assets/diagram-S7CK7UJ4-C0vmdMvd.js 8.44 kB │ gzip: 3.77 kB
dist/assets/vue-html-AaS7Mt5G.js 8.47 kB │ gzip: 1.68 kB
dist/assets/plsql-ChMvpjG-.js 8.51 kB │ gzip: 3.00 kB
dist/assets/isArrayLikeObject-Csl51Uc7.js 8.52 kB │ gzip: 3.19 kB
dist/assets/dart-bE4Kk8sk.js 8.62 kB │ gzip: 2.00 kB
dist/assets/kotlin-BdnUsdx6.js 8.79 kB │ gzip: 2.13 kB
dist/assets/make-CHLpvVh8.js 8.96 kB │ gzip: 1.77 kB
dist/assets/sas-DEy46yEz.js 9.06 kB │ gzip: 3.81 kB
dist/assets/smithy-cds9vsN8.js 9.13 kB │ gzip: 1.53 kB
dist/assets/sass-Cj5Yp3dK.js 9.29 kB │ gzip: 2.49 kB
dist/assets/sas-B4kiWyti.js 9.33 kB │ gzip: 4.11 kB
dist/assets/tex-D96PA37w.js 9.67 kB │ gzip: 3.06 kB
dist/assets/jison-wvAkD_A8.js 9.69 kB │ gzip: 1.85 kB
dist/assets/perl-CdXCOZ3F.js 9.75 kB │ gzip: 3.46 kB
dist/assets/cmake-D1j8_8rp.js 9.86 kB │ gzip: 3.37 kB
dist/assets/hcl-BWvSN4gD.js 10.05 kB │ gzip: 1.93 kB
dist/assets/gherkin-heZmZLOM.js 10.16 kB │ gzip: 5.09 kB
dist/assets/cynefinDiagram-5FMLGOSQ-BD2mGua6.js 10.18 kB │ gzip: 3.68 kB
dist/assets/pkl-u5AG7uiY.js 10.37 kB │ gzip: 1.38 kB
dist/assets/beancount-k_qm7-4y.js 10.37 kB │ gzip: 1.44 kB
dist/assets/nextflow-groovy-vE_lwT2v.js 10.44 kB │ gzip: 2.14 kB
dist/assets/dream-maker-BtqSS_iP.js 10.47 kB │ gzip: 2.25 kB
dist/assets/raku-DXvB9xmW.js 10.47 kB │ gzip: 2.94 kB
dist/assets/stateDiagram-D77RDMKH-B-0A9xPX.js 10.48 kB │ gzip: 3.69 kB
dist/assets/yaml-Buea-lGh.js 10.51 kB │ gzip: 2.27 kB
dist/assets/rst-bs7f0vWN.js 10.67 kB │ gzip: 2.42 kB
dist/assets/clojure-BMjYHr_A.js 10.82 kB │ gzip: 3.99 kB
dist/assets/diagram-VSXAHHWV-Df9UVgEq.js 10.90 kB │ gzip: 4.21 kB
dist/assets/elm-DbKCFpqz.js 10.97 kB │ gzip: 2.12 kB
dist/assets/just-Cwhn7H3k.js 11.23 kB │ gzip: 2.80 kB
dist/assets/dagre-GXQ25YYZ-xEIgJp7x.js 11.25 kB │ gzip: 4.25 kB
dist/assets/prolog-CbFg5uaA.js 11.36 kB │ gzip: 3.83 kB
dist/assets/terraform-BETggiCN.js 11.39 kB │ gzip: 2.51 kB
dist/assets/puppet-BMWR74SV.js 11.44 kB │ gzip: 2.11 kB
dist/assets/idl-BEugSyMb.js 11.63 kB │ gzip: 4.52 kB
dist/assets/gherkin-DyxjwDmM.js 11.95 kB │ gzip: 5.05 kB
dist/assets/wasm-MzD3tlZU.js 12.01 kB │ gzip: 2.19 kB
dist/assets/hjson-D5-asLiD.js 12.05 kB │ gzip: 1.64 kB
dist/assets/surrealql-Cjom0U5J.js 12.06 kB │ gzip: 3.30 kB
dist/assets/handlebars-BpdQsYii.js 12.15 kB │ gzip: 2.38 kB
dist/assets/apache-Pmp26Uib.js 12.46 kB │ gzip: 3.72 kB
dist/assets/index-CZ9cYO_F.js 12.75 kB │ gzip: 5.63 kB
dist/assets/bat-CickPsom.js 12.89 kB │ gzip: 3.23 kB
dist/assets/fish-BvzEVeQv.js 13.04 kB │ gzip: 1.74 kB
dist/assets/nsis-BlV79W_Q.js 13.13 kB │ gzip: 4.07 kB
dist/assets/v-BGw2Nkan.js 13.33 kB │ gzip: 2.77 kB
dist/assets/pug-DKIMFp6K.js 13.84 kB │ gzip: 2.58 kB
dist/assets/smalltalk-BOQMe2GC.js 14.13 kB │ gzip: 2.16 kB
dist/assets/clarity-Dn5IMItf.js 14.36 kB │ gzip: 2.51 kB
dist/assets/gnuplot-DdkO51Og.js 14.78 kB │ gzip: 3.27 kB
dist/assets/index-CvBQ3_sw.js 14.83 kB │ gzip: 6.00 kB
dist/assets/rust-B1yitclQ.js 15.07 kB │ gzip: 2.72 kB
dist/assets/kusto-wEQ09or8.js 15.17 kB │ gzip: 3.92 kB
dist/assets/ChatView-yeXQKTN6.js 15.41 kB │ gzip: 6.38 kB
dist/assets/nix-CwoSXNpI.js 15.51 kB │ gzip: 2.48 kB
dist/assets/lua-BaeVxFsk.js 15.54 kB │ gzip: 3.16 kB
dist/assets/actionscript-3-B3316cI-.js 15.54 kB │ gzip: 2.71 kB
dist/assets/MediaView-DDn-_XZH.js 15.57 kB │ gzip: 6.74 kB
dist/assets/chunk-SVP7TREG-C9m5TIe-.js 15.75 kB │ gzip: 4.42 kB
dist/assets/abap-BdImnpbu.js 15.85 kB │ gzip: 5.91 kB
dist/assets/matlab-D7o27uSR.js 16.09 kB │ gzip: 3.06 kB
dist/assets/luau-BnpPk5vE.js 16.14 kB │ gzip: 3.55 kB
dist/assets/diagram-VX7I27RA-ziQFycS8.js 16.15 kB │ gzip: 5.78 kB
dist/assets/cue-D82EKSYY.js 16.20 kB │ gzip: 2.06 kB
dist/assets/elixir-CkH2-t6x.js 16.32 kB │ gzip: 2.80 kB
dist/assets/index-Cs5JlGLq.js 16.35 kB │ gzip: 7.47 kB
dist/assets/solidity-DijEV5ha.js 16.41 kB │ gzip: 3.16 kB
dist/assets/odin-BBf5iR-q.js 16.51 kB │ gzip: 2.94 kB
dist/assets/ts-tags-D351s5mN.js 16.57 kB │ gzip: 2.07 kB
dist/assets/javascript-iXu5QeM3.js 17.08 kB │ gzip: 5.75 kB
dist/assets/ishikawaDiagram-5VMMS53U-DiQySQzt.js 17.65 kB │ gzip: 6.72 kB
dist/assets/move-el3G9tDJ.js 17.66 kB │ gzip: 3.06 kB
dist/assets/graphql-ChdNCCLP.js 18.00 kB │ gzip: 2.52 kB
dist/assets/liquid-C0sCDyMI.js 18.09 kB │ gzip: 3.16 kB
dist/assets/svelte-Cy7k_4gC.js 18.24 kB │ gzip: 3.14 kB
dist/assets/gdscript-DqcFQ5yU.js 19.09 kB │ gzip: 3.77 kB
dist/assets/groovy-gcz8RCvz.js 19.18 kB │ gzip: 3.60 kB
dist/assets/WorkspaceView-D--Q9mXh.js 19.88 kB │ gzip: 8.44 kB
dist/assets/glimmer-js-ByusRIyA.js 20.07 kB │ gzip: 2.95 kB
dist/assets/glimmer-ts-BfAWNZQY.js 20.07 kB │ gzip: 2.94 kB
dist/assets/powershell-BmBUJMz7.js 20.16 kB │ gzip: 4.07 kB
dist/assets/mdc-D1_yUvq7.js 20.18 kB │ gzip: 6.81 kB
dist/assets/viml-CJc9bBzg.js 20.37 kB │ gzip: 6.73 kB
dist/assets/kanban-definition-UXKFOSKX-C3erkLcV.js 20.78 kB │ gzip: 7.34 kB
dist/assets/nushell-D3jzshHO.js 20.88 kB │ gzip: 5.36 kB
dist/assets/index-D0DaXqvL.js 20.96 kB │ gzip: 9.37 kB
dist/assets/index-Cmf0-okb.js 21.23 kB │ gzip: 9.76 kB
dist/assets/wit-5i3qLPDT.js 21.47 kB │ gzip: 2.89 kB
dist/assets/twig-27uCiNez.js 21.98 kB │ gzip: 3.98 kB
dist/assets/clike-B9uivgTg.js 22.30 kB │ gzip: 7.86 kB
dist/assets/nim-BIad80T-.js 22.46 kB │ gzip: 3.16 kB
dist/assets/common-lisp-Cg-RD9OK.js 22.58 kB │ gzip: 6.06 kB
dist/assets/index-DduR1oeT.js 23.10 kB │ gzip: 10.41 kB
dist/assets/sql-CRqJ_cUM.js 23.43 kB │ gzip: 7.42 kB
dist/assets/mindmap-definition-YA3MSWOX-DSZ7Nbk9.js 23.44 kB │ gzip: 7.94 kB
dist/assets/sankeyDiagram-P5KCCOFB-Cnmoq8od.js 23.47 kB │ gzip: 8.63 kB
dist/assets/journeyDiagram-3NMN7TZE-BwGxWKov.js 23.65 kB │ gzip: 8.39 kB
dist/assets/cadence-Bv_4Rxtq.js 23.67 kB │ gzip: 3.67 kB
dist/assets/astro-HNnZUWAn.js 24.01 kB │ gzip: 7.55 kB
dist/assets/apl-CORt7UWP.js 24.04 kB │ gzip: 4.20 kB
dist/assets/templ-DhtptRzy.js 24.06 kB │ gzip: 5.40 kB
dist/assets/vhdl-CeAyd5Ju.js 24.26 kB │ gzip: 3.87 kB
dist/assets/angular-html-DA-rfuFy.js 24.29 kB │ gzip: 4.01 kB
dist/assets/purescript-CklMAg4u.js 24.69 kB │ gzip: 3.25 kB
dist/assets/McpServersView-BXDi-IBv.js 24.81 kB │ gzip: 9.19 kB
dist/assets/moonbit-CHtswR0a.js 24.94 kB │ gzip: 3.79 kB
dist/assets/vue-BqiEGhQt.js 24.96 kB │ gzip: 3.04 kB
dist/assets/typespec-CAFt9gP4.js 25.05 kB │ gzip: 2.63 kB
dist/assets/fsharp-CXgrBDvD.js 25.31 kB │ gzip: 4.13 kB
dist/assets/marko-DjSrsDqO.js 25.48 kB │ gzip: 3.59 kB
dist/assets/stylus-B533Al4x.js 25.80 kB │ gzip: 8.60 kB
dist/assets/wardleyDiagram-VM6X3IG4-bQBfHON_.js 26.18 kB │ gzip: 6.99 kB
dist/assets/c3-Dp5svz6Z.js 26.39 kB │ gzip: 3.99 kB
dist/assets/system-verilog-0hqHdDBg.js 26.56 kB │ gzip: 4.88 kB
dist/assets/PluginsView-L41qWUGa.js 26.83 kB │ gzip: 8.88 kB
dist/assets/codeql-DsOJ9woJ.js 26.88 kB │ gzip: 3.79 kB
dist/assets/css-BnMrqG3P.js 27.13 kB │ gzip: 8.47 kB
dist/assets/AgentView-vuEh8_sU.js 27.16 kB │ gzip: 9.10 kB
dist/assets/scss-D5BDwBP9.js 27.20 kB │ gzip: 4.20 kB
dist/assets/java-CylS5w8V.js 27.22 kB │ gzip: 4.26 kB
dist/assets/coffee-Ch7k5sss.js 27.42 kB │ gzip: 6.35 kB
dist/assets/razor-BjBPvh-w.js 27.51 kB │ gzip: 3.57 kB
dist/assets/index-yVH2efBJ.js 28.27 kB │ gzip: 12.76 kB
dist/assets/index-rtxKP8li.js 28.80 kB │ gzip: 11.71 kB
dist/assets/scala-CqE71os6.js 28.89 kB │ gzip: 3.94 kB
dist/assets/purify.es-5AjVNlXF.js 28.92 kB │ gzip: 11.14 kB
dist/assets/crystal-DGywbUpC.js 29.39 kB │ gzip: 4.45 kB
dist/assets/applescript-Co6uUVPk.js 29.57 kB │ gzip: 5.93 kB
dist/assets/gitGraphDiagram-WWUBYQGX-C3QLpWqM.js 30.02 kB │ gzip: 8.92 kB
dist/assets/index-l9h65gFm.js 30.82 kB │ gzip: 12.67 kB
dist/assets/erDiagram-RLTQ6QDP-gaZhO_Bd.js 31.01 kB │ gzip: 10.68 kB
dist/assets/stylus-BEDo0Tqx.js 31.07 kB │ gzip: 7.99 kB
dist/assets/julia-5Bft2YPA.js 31.08 kB │ gzip: 4.34 kB
dist/assets/requirementDiagram-BXWQKSXE-Cia-WX85.js 31.29 kB │ gzip: 9.87 kB
dist/assets/timeline-definition-24CTP7MA-DRPkaIkb.js 31.43 kB │ gzip: 10.47 kB
dist/assets/ahk-CsyLZFj1.js 31.44 kB │ gzip: 7.81 kB
dist/assets/layout-BPporBfO.js 32.04 kB │ gzip: 11.47 kB
dist/assets/VisualMarkdownEditor-DpC_b1CQ.js 32.34 kB │ gzip: 12.47 kB
dist/assets/index-BtCU2wNO.js 32.36 kB │ gzip: 13.09 kB
dist/assets/bsl-DlhNcFeZ.js 33.95 kB │ gzip: 8.36 kB
dist/assets/quadrantDiagram-AXDQQJYC-BjYsuyTc.js 34.57 kB │ gzip: 10.18 kB
dist/assets/nginx-BpAMiNFr.js 35.37 kB │ gzip: 4.43 kB
dist/assets/haxe-CfZj7gIn.js 35.95 kB │ gzip: 6.01 kB
dist/assets/org-DM6o9KBp.js 36.32 kB │ gzip: 3.70 kB
dist/assets/sql-D0XecflT.js 37.05 kB │ gzip: 10.94 kB
dist/assets/erlang-DsQrWhSR.js 37.48 kB │ gzip: 4.40 kB
dist/assets/chunk-IMKFNOWR-CMjnXVE8.js 38.62 kB │ gzip: 12.68 kB
dist/assets/bird2-Bx8U0n9b.js 38.68 kB │ gzip: 8.50 kB
dist/assets/cobol-nBiQ_Alo.js 39.13 kB │ gzip: 10.86 kB
dist/assets/asm-D_Q5rh1f.js 40.72 kB │ gzip: 8.21 kB
dist/assets/index-CRAC0HLJ.js 40.77 kB │ gzip: 16.77 kB
dist/assets/shellscript-Yzrsuije.js 41.48 kB │ gzip: 6.09 kB
dist/assets/haskell-Df6bDoY_.js 41.49 kB │ gzip: 6.44 kB
dist/assets/vennDiagram-4TSXK5OY-CS54t_H7.js 42.45 kB │ gzip: 15.85 kB
dist/assets/perl-B9cMNwum.js 43.16 kB │ gzip: 4.67 kB
dist/assets/blockDiagram-I7D4REHJ-CwXV7v-G.js 43.24 kB │ gzip: 13.96 kB
dist/assets/d-85-TOEBH.js 43.80 kB │ gzip: 8.47 kB
dist/assets/index-CiwVSVo-.js 44.38 kB │ gzip: 15.39 kB
dist/assets/xychartDiagram-S5SC5T6Z-SD9juUjS.js 44.59 kB │ gzip: 12.66 kB
dist/assets/ahk2-8Zs4aa1G.js 45.07 kB │ gzip: 9.88 kB
dist/assets/index-Dl3jeLBI.js 45.14 kB │ gzip: 19.18 kB
dist/assets/ruby-C0TQ7zu5.js 46.72 kB │ gzip: 5.83 kB
dist/assets/go-C27-OAKa.js 46.82 kB │ gzip: 5.18 kB
dist/assets/apex-DhZFqWV2.js 47.89 kB │ gzip: 6.90 kB
dist/assets/ada-bCR0ucgS.js 48.08 kB │ gzip: 6.03 kB
dist/assets/css-CLj8gQPS.js 49.04 kB │ gzip: 11.86 kB
dist/assets/chunk-TICWLB2K-Dykoa1DO.js 49.34 kB │ gzip: 15.79 kB
dist/assets/imba-DGztddWO.js 49.93 kB │ gzip: 9.46 kB
dist/assets/mermaid-CQcHuHx7.js 54.81 kB │ gzip: 4.81 kB
dist/assets/wikitext-BhOHFoWU.js 55.89 kB │ gzip: 4.76 kB
dist/assets/stata-DI20mbqo.js 56.99 kB │ gzip: 12.36 kB
dist/assets/html-pp8916En.js 57.25 kB │ gzip: 11.69 kB
dist/assets/ballerina-BFfxhgS-.js 58.69 kB │ gzip: 8.15 kB
dist/assets/markdown-Cvjx9yec.js 59.34 kB │ gzip: 5.64 kB
dist/assets/ThemesView-ROhLW1IX.js 60.69 kB │ gzip: 16.28 kB
dist/assets/ocaml-C0hk2d4L.js 62.45 kB │ gzip: 5.02 kB
dist/assets/flowDiagram-HODETNUW-B-wmlXgh.js 62.65 kB │ gzip: 20.06 kB
dist/assets/c4Diagram-7LVT6UL2-CZK_oPAh.js 65.64 kB │ gzip: 18.87 kB
dist/assets/mojo-DJz3ZmWd.js 69.52 kB │ gzip: 9.27 kB
dist/assets/ganttDiagram-EL5Y4UJY-C00FfxS-.js 69.78 kB │ gzip: 23.60 kB
dist/assets/python-B6aJPvgy.js 69.95 kB │ gzip: 9.13 kB
dist/assets/index-C7y4_zOF.js 70.84 kB │ gzip: 25.86 kB
dist/assets/c-BIGW1oBm.js 72.11 kB │ gzip: 10.51 kB
dist/assets/latex-D5pSuvFb.js 72.90 kB │ gzip: 6.78 kB
dist/assets/vyper-CDx5xZoG.js 74.65 kB │ gzip: 10.74 kB
dist/assets/preload-helper-BYUGvJBp.js 77.31 kB │ gzip: 30.71 kB
dist/assets/hack-BWmVpMyf.js 81.13 kB │ gzip: 26.38 kB
dist/assets/cose-bilkent-JH36ORCC-ucdrH9O7.js 81.80 kB │ gzip: 22.53 kB
dist/assets/index-DJmQi6DI.js 87.01 kB │ gzip: 34.53 kB
dist/assets/swift-C2oV4EkX.js 87.23 kB │ gzip: 14.84 kB
dist/assets/fortran-free-form-BxgE0vQu.js 88.97 kB │ gzip: 11.27 kB
dist/assets/csharp-DSvCPggb.js 90.19 kB │ gzip: 10.75 kB
dist/assets/racket-BqYA7rlc.js 92.39 kB │ gzip: 15.02 kB
dist/assets/less-B1dDrJ26.js 97.63 kB │ gzip: 14.70 kB
dist/assets/index-B_zoUCtT.js 97.90 kB │ gzip: 28.48 kB
dist/assets/index-CVdm6qZy.js 103.95 kB │ gzip: 34.40 kB
dist/assets/blade-2xfisSek.js 104.98 kB │ gzip: 28.20 kB
dist/assets/objective-c-DXmwc3jG.js 105.41 kB │ gzip: 23.33 kB
dist/assets/SettingsView-CA8mvhNi.js 110.00 kB │ gzip: 38.72 kB
dist/assets/php-Csjmro_R.js 113.09 kB │ gzip: 28.74 kB
dist/assets/swimlanes-42K2YHIH-DBnK_luN.js 117.20 kB │ gzip: 42.88 kB
dist/assets/sequenceDiagram-WJ2MYXX4-CT529fmH.js 117.53 kB │ gzip: 31.32 kB
dist/assets/asciidoc-CSVQ5wI8.js 136.05 kB │ gzip: 9.50 kB
dist/assets/mdx-Cmh6b_Ma.js 136.11 kB │ gzip: 23.35 kB
dist/assets/architectureDiagram-5GKGNRK7-MxGRspvX.js 152.10 kB │ gzip: 43.27 kB
dist/assets/typst-BUadGCkm.js 163.55 kB │ gzip: 10.98 kB
dist/assets/index-CX-Z_M19.js 165.35 kB │ gzip: 49.44 kB
dist/assets/objective-cpp-CLxacb5B.js 171.97 kB │ gzip: 30.62 kB
dist/assets/javascript-wDzz0qaB.js 174.83 kB │ gzip: 16.51 kB
dist/assets/tsx-COt5Ahok.js 175.54 kB │ gzip: 16.51 kB
dist/assets/jsx-g9-lgVsj.js 177.79 kB │ gzip: 16.61 kB
dist/assets/markdown-D1OO11DI.js 180.56 kB │ gzip: 56.48 kB
dist/assets/typescript-BPQ3VLAy.js 181.08 kB │ gzip: 16.04 kB
dist/assets/angular-ts-BrjP3tb8.js 183.82 kB │ gzip: 16.63 kB
dist/assets/vue-vine-BoDAl6tE.js 190.05 kB │ gzip: 17.99 kB
dist/assets/math-katex-0-18-4-CHandE1Z.js 261.18 kB │ gzip: 77.76 kB
dist/assets/editor-prosemirror-DdSLUblA.js 261.62 kB │ gzip: 81.66 kB
dist/assets/wolfram-lXgVvXCa.js 262.39 kB │ gzip: 77.14 kB
dist/assets/editor-milkdown-CfUWLk7w.js 288.62 kB │ gzip: 86.76 kB
dist/assets/index-BPFAEZBr.js 347.56 kB │ gzip: 114.62 kB
dist/assets/editor-codemirror-BFoSK6g_.js 425.42 kB │ gzip: 137.40 kB
dist/assets/cytoscape.esm-Bch-eiPH.js 443.83 kB │ gzip: 142.41 kB
dist/assets/wasm-CG6Dc4jp.js 622.34 kB │ gzip: 230.29 kB
dist/assets/mermaid.core-C2yjh7Um.js 658.47 kB │ gzip: 158.25 kB
dist/assets/cynefin-OW5HDTMX-DYRSVYbi.js 690.86 kB │ gzip: 155.12 kB
dist/assets/cpp-BMRokrvK.js 785.49 kB │ gzip: 53.20 kB
dist/assets/emacs-lisp-C_m_b--Z.js 790.01 kB │ gzip: 198.83 kB
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 18.06s
@@ -0,0 +1,25 @@
{
"model": "deepseek-v4-flash",
"configured_test_window": 8192,
"vendor_max_context_tested": false,
"detect": {
"error_code": "CONTEXT_COMPRESSION_REQUIRED",
"network_calls": 0
},
"compression": {
"passed": false,
"before_estimate": 2262,
"after_estimate": 523,
"archive_unchanged": true,
"network_calls": 2,
"answer_input_tokens": 132,
"answer_output_tokens": 256
},
"observed_provider_cache": {
"requests": 18,
"reporting_requests": 16,
"positive_hit_requests": 12,
"positive_miss_requests": 16,
"scope": "provider reported usage across this isolated acceptance session; not deterministic cache control"
}
}
@@ -0,0 +1,25 @@
{
"model": "deepseek-v4-flash",
"configured_test_window": 8192,
"vendor_max_context_tested": false,
"detect": {
"error_code": "CONTEXT_COMPRESSION_REQUIRED",
"network_calls": 0
},
"compression": {
"passed": true,
"before_estimate": 2262,
"after_estimate": 516,
"archive_unchanged": true,
"network_calls": 2,
"answer_input_tokens": 129,
"answer_output_tokens": 289
},
"observed_provider_cache": {
"requests": 20,
"reporting_requests": 18,
"positive_hit_requests": 13,
"positive_miss_requests": 18,
"scope": "provider reported usage across this isolated acceptance session; not deterministic cache control"
}
}
@@ -0,0 +1,27 @@
> notes-agent-frontend@0.2.0 test C:\Users\KiriAky\.codex\worktrees\7950\NotesAgent\frontend
> vitest run "--reporter=dot"
RUN v4.1.11 C:/Users/KiriAky/.codex/worktrees/7950/NotesAgent/frontend
stderr | src/features/editor/shikiCodeMirror.spec.ts
Warning: KaTeX doesn't work in quirks mode. Make sure your website has a suitable doctype.
stderr | src/features/themes/ThemesView.spec.ts
Warning: KaTeX doesn't work in quirks mode. Make sure your website has a suitable doctype.
stderr | src/features/editor/diagramIntegration.spec.ts
Warning: KaTeX doesn't work in quirks mode. Make sure your website has a suitable doctype.
Warning: KaTeX doesn't work in quirks mode. Make sure your website has a suitable doctype.
stderr | src/features/editor/EditorPane.spec.ts > EditorPane file switching > recreates the visual editor with the newly loaded file content
Warning: KaTeX doesn't work in quirks mode. Make sure your website has a suitable doctype.
Warning: KaTeX doesn't work in quirks mode. Make sure your website has a suitable doctype.
·····················································································································································································································································································································································································································································
Test Files 80 passed (80)
Tests 437 passed (437)
Start at 02:50:15
Duration 21.99s (transform 24.03s, setup 0ms, import 75.03s, tests 27.89s, environment 143.74s)
@@ -0,0 +1,107 @@
{
"run_id": "benchmark_f98741e7649a",
"kind": "agent",
"dataset_id": "agent-integration-v1",
"dataset_hash": "sha256:af10b953037a35cad6fae678cbc118b519b209e88906f80721080560d49a508c",
"status": "completed",
"config_snapshot": {
"dataset_id": "agent-integration-v1",
"provider_id": "provider_65fd326edba646e3bc735c25fbf5ffe8",
"model": "deepseek-v4-flash",
"max_steps": 10,
"timeout_seconds": 150,
"token_budget": 10000,
"repeat": 1,
"allow_network": true,
"offline": false,
"dataset_hash": "sha256:af10b953037a35cad6fae678cbc118b519b209e88906f80721080560d49a508c",
"dataset_version": "1.0.0",
"execution": "real_agent_runtime",
"provider_type": "openai_compatible",
"scoring_version": "1.0",
"permission_policy": "runtime_user_decision",
"active_agent_run_id": "run_d55e28a22cb0433498ae59d0eabce566"
},
"metrics": {
"total_cases": 2,
"task_success_rate": 1.0,
"tool_selection_accuracy": 1.0,
"tool_argument_accuracy": 1.0,
"invalid_tool_call_rate": 0.0,
"average_steps": 2.5,
"average_latency_ms": 10539.258899982087,
"token_usage": 16070,
"tool_calls": 6,
"expected_calls": 6
},
"cases": [
{
"case_id": "retrieval-tasks",
"repeat": 0,
"agent_run_id": "run_dc5ad9f709c64b59b792bebafad77653",
"success": true,
"tool_calls": 5,
"expected_calls": 5,
"selected_calls": 5,
"accurate_calls": 5,
"invalid_calls": 0,
"steps": 3,
"latency_ms": 13835.580999962986,
"token_usage": 8651,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
},
{
"case_id": "mcp-search",
"repeat": 0,
"agent_run_id": "run_d55e28a22cb0433498ae59d0eabce566",
"success": true,
"tool_calls": 1,
"expected_calls": 1,
"selected_calls": 1,
"accurate_calls": 1,
"invalid_calls": 0,
"steps": 2,
"latency_ms": 7242.936800001189,
"token_usage": 7419,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
}
],
"error": null,
"error_code": null,
"permission_approvals": 3,
"mcp_present": true,
"tasks": [
{
"task_id": "task_7380b0dd0b2c4161a20f3b6c1a19dffa",
"title": "验收-锁顺序"
},
{
"task_id": "task_5e0584d287c442b597ca4c22f8998214",
"title": "验收-循环等待"
},
{
"task_id": "task_22ac49c4e4214c98951b387a6f128a94",
"title": "验收-互斥条件"
}
]
}
@@ -0,0 +1,196 @@
{
"provider_id": "provider_65fd326edba646e3bc735c25fbf5ffe8",
"protocol": "openai_compatible",
"model": "deepseek-v4-flash",
"checks": {
"discovery": {
"passed": true,
"count": 3,
"elapsed_ms": 1904.77
},
"normal_chat": {
"passed": true,
"input_tokens": 87,
"output_tokens": 16,
"elapsed_ms": 2107.22
},
"stream_usage_reasoning": {
"passed": true,
"events": [
"Done",
"TextDelta",
"ThinkingDelta",
"Usage"
],
"usage_reported": true,
"reasoning_observed": true,
"elapsed_ms": 2118.0
},
"stream_cancel": {
"passed": true,
"first_event": "ThinkingDelta",
"close_ms": 12.160799989942461,
"scope": "client stream resource close; provider billing cessation not observable",
"elapsed_ms": 1842.69
},
"error_mapping": {
"passed": true,
"error_code": "PROVIDER_INVALID_REQUEST",
"elapsed_ms": 1590.6
}
},
"unconfigured_protocols": [
"openai_responses",
"anthropic_messages",
"ollama"
],
"not_tested": [
"vendor_maximum_context_boundary"
],
"max_direct_requests": 5,
"agent": {
"run_id": "benchmark_8ebbf8c6f27f",
"kind": "agent",
"dataset_id": "agent-core-v1",
"dataset_hash": "sha256:82fd1b84cc2dc2feec0fc67357d64b145dcdf6c44ec2660f182d550e85b9d27f",
"status": "completed",
"config_snapshot": {
"dataset_id": "agent-core-v1",
"provider_id": "provider_65fd326edba646e3bc735c25fbf5ffe8",
"model": "deepseek-v4-flash",
"max_steps": 6,
"timeout_seconds": 90,
"token_budget": 6000,
"repeat": 1,
"allow_network": false,
"offline": false,
"dataset_hash": "sha256:82fd1b84cc2dc2feec0fc67357d64b145dcdf6c44ec2660f182d550e85b9d27f",
"dataset_version": "1.0.0",
"execution": "real_agent_runtime",
"provider_type": "openai_compatible",
"scoring_version": "1.0",
"permission_policy": "runtime_user_decision",
"active_agent_run_id": "run_c9296aaea4c7429db97528edc72b6180"
},
"metrics": {
"total_cases": 4,
"task_success_rate": 1.0,
"tool_selection_accuracy": 1.0,
"tool_argument_accuracy": 1.0,
"invalid_tool_call_rate": 0.0,
"average_steps": 1.75,
"average_latency_ms": 5682.329325005412,
"token_usage": 5888,
"tool_calls": 3,
"expected_calls": 3
},
"cases": [
{
"case_id": "arithmetic",
"repeat": 0,
"agent_run_id": "run_480c159ffc0341f9883a09914ff003b5",
"success": true,
"tool_calls": 1,
"expected_calls": 1,
"selected_calls": 1,
"accurate_calls": 1,
"invalid_calls": 0,
"steps": 2,
"latency_ms": 4888.349500019103,
"token_usage": 1545,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
},
{
"case_id": "echo",
"repeat": 0,
"agent_run_id": "run_4fdbd0508bb84bcda424d97de1b91241",
"success": true,
"tool_calls": 1,
"expected_calls": 1,
"selected_calls": 1,
"accurate_calls": 1,
"invalid_calls": 0,
"steps": 2,
"latency_ms": 4978.496600000653,
"token_usage": 1601,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
},
{
"case_id": "no-tool",
"repeat": 0,
"agent_run_id": "run_5dfb4ae0a6c547b79706ad102e577bab",
"success": true,
"tool_calls": 0,
"expected_calls": 0,
"selected_calls": 0,
"accurate_calls": 0,
"invalid_calls": 0,
"steps": 1,
"latency_ms": 2549.696500005666,
"token_usage": 541,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
},
{
"case_id": "markdown-catalog",
"repeat": 0,
"agent_run_id": "run_c9296aaea4c7429db97528edc72b6180",
"success": true,
"tool_calls": 1,
"expected_calls": 1,
"selected_calls": 1,
"accurate_calls": 1,
"invalid_calls": 0,
"steps": 2,
"latency_ms": 10312.774699996226,
"token_usage": 2201,
"checks": {
"completed": true,
"tools_selected": true,
"tool_arguments": true,
"no_extra_calls": true,
"tool_results": true,
"output": true,
"citation": true,
"tasks_created": true
},
"error_code": null
}
],
"error": null,
"error_code": null
},
"followup_evidence": [
"context.json",
"integration.json"
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,164 @@
[
{
"theme": "light",
"errors": [],
"overflow": false,
"colors": {
"panel": {
"background": "rgb(255, 255, 255)",
"color": "rgb(31, 35, 40)",
"border": "rgb(228, 231, 235)"
},
"input": {
"background": "rgb(255, 255, 255)",
"color": "rgb(31, 35, 40)",
"border": "rgb(102, 113, 241)"
},
"button": {
"background": "rgb(91, 103, 241)",
"color": "rgb(255, 255, 255)",
"border": "rgba(0, 0, 0, 0)"
},
"metric": {
"background": "rgb(247, 248, 250)",
"color": "rgb(31, 35, 40)",
"border": "rgb(228, 231, 235)"
}
}
},
{
"theme": "dark",
"errors": [],
"overflow": false,
"colors": {
"panel": {
"background": "rgb(22, 27, 34)",
"color": "rgb(230, 237, 243)",
"border": "rgb(48, 54, 61)"
},
"input": {
"background": "rgb(21, 26, 33)",
"color": "rgb(230, 237, 243)",
"border": "rgb(116, 129, 231)"
},
"button": {
"background": "rgb(125, 139, 255)",
"color": "rgb(13, 17, 23)",
"border": "rgba(0, 0, 0, 0)"
},
"metric": {
"background": "rgb(22, 27, 34)",
"color": "rgb(230, 237, 243)",
"border": "rgb(48, 54, 61)"
}
}
},
{
"theme": "sepia",
"errors": [],
"overflow": false,
"colors": {
"panel": {
"background": "rgb(255, 248, 232)",
"color": "rgb(64, 55, 43)",
"border": "rgb(221, 207, 173)"
},
"input": {
"background": "rgb(255, 247, 231)",
"color": "rgb(64, 55, 43)",
"border": "rgb(148, 105, 65)"
},
"button": {
"background": "rgb(138, 91, 50)",
"color": "rgb(255, 255, 255)",
"border": "rgba(0, 0, 0, 0)"
},
"metric": {
"background": "rgb(244, 232, 202)",
"color": "rgb(64, 55, 43)",
"border": "rgb(221, 207, 173)"
}
}
},
{
"theme": "paper-moments",
"errors": [],
"overflow": false,
"colors": {
"panel": {
"background": "rgb(255, 253, 245)",
"color": "rgb(73, 63, 53)",
"border": "rgb(181, 166, 147)"
},
"input": {
"background": "rgb(254, 251, 243)",
"color": "rgb(73, 63, 53)",
"border": "rgb(147, 105, 88)"
},
"button": {
"background": "rgb(134, 82, 66)",
"color": "rgb(255, 253, 245)",
"border": "rgba(0, 0, 0, 0)"
},
"metric": {
"background": "rgb(243, 238, 227)",
"color": "rgb(73, 63, 53)",
"border": "rgb(181, 166, 147)"
}
}
},
{
"theme": "ocean-blue",
"errors": [],
"overflow": false,
"colors": {
"panel": {
"background": "rgb(255, 255, 255)",
"color": "rgb(30, 41, 59)",
"border": "rgb(226, 232, 240)"
},
"input": {
"background": "rgb(255, 255, 255)",
"color": "rgb(30, 41, 59)",
"border": "rgb(27, 133, 189)"
},
"button": {
"background": "rgb(0, 118, 181)",
"color": "rgb(255, 255, 255)",
"border": "rgba(0, 0, 0, 0)"
},
"metric": {
"background": "rgb(248, 250, 252)",
"color": "rgb(30, 41, 59)",
"border": "rgb(226, 232, 240)"
}
}
},
{
"theme": "midnight-purple",
"errors": [],
"overflow": false,
"colors": {
"panel": {
"background": "rgb(36, 40, 59)",
"color": "rgb(192, 202, 245)",
"border": "rgb(59, 63, 92)"
},
"input": {
"background": "rgb(34, 38, 55)",
"color": "rgb(192, 202, 245)",
"border": "rgb(139, 75, 197)"
},
"button": {
"background": "rgb(156, 77, 220)",
"color": "rgb(26, 27, 38)",
"border": "rgba(0, 0, 0, 0)"
},
"metric": {
"background": "rgb(36, 40, 59)",
"color": "rgb(192, 202, 245)",
"border": "rgb(59, 63, 92)"
}
}
}
]
@@ -0,0 +1,106 @@
# 第二阶段全量验收报告
> 以下保留修复前基线审计。其后已完成的收尾实现、真实运行结果和剩余外部边界以[第二阶段收尾实现与验收](第二阶段收尾实现与验收-2026-09-07.md)为准,不能把本报告中的“未实现”继续当作当前分支状态。
> 日期:2026-09-07。基线:`main` / `95095197df8b607e55144748c9be0c7f6daf00df`,与本轮拉取的 `gitea/main` 一致。范围包含三位成员负责的全部第二阶段模块,不沿用历史“不含杨侧验收”的排除范围。
## 结论与口径
**第二阶段主要基础能力已落地,但尚未达到完整 Definition of Done。** 自动化测试通过不代表 Agent Benchmark、函数图像前端、Mermaid 静态导出、真实厂商协议和音频质量已经验收。
依据为[第二阶段团队分工表](../architecture/第二阶段团队分工表.md)、[第二阶段接口契约](../contracts/第二阶段接口契约-开发版.md)、当前实现、自动化测试、隔离压测及浏览器检查。历史文档中未经同步的勾选框不直接作为实时完成率。
本轮不修改业务逻辑、不调用收费模型或外部 MCP、不重跑长录音。测试使用临时数据库/Vault;内置语料检查读取 Git HEAD,避开用户未提交笔记。主工作区原有两份笔记修改保持不变。
## 本轮验证结果
| 检查 | 结果 | 证据与边界 |
| --- | --- | --- |
| 后端全量 | 838 项通过 | Python 3.12,临时 APP_DATA_DIR / APP_DB_PATH / APP_VAULT_PATH1 条既有 Starlette/httpx 弃用提示 |
| 前端全量 | 78 个测试文件、433 项通过 | Vitest;部分测试存在 KaTeX quirks-mode 提示 |
| 类型与生产构建 | 通过 | `npm run build`vue-tsc + Vite;仍有超过 500 kB 的惰性分块提示 |
| Mermaid 真实渲染 | 36 / 36 结构检查通过 | 6 图型 × 6 主题,无 Mermaid mock;不是所有图形字形、缩放和主题外观的完整人工验收 |
| Agent 并发 | 1 / 10 / 50 / 200 并发场景通过 | 共 290 次完成;mock 每轮注入 50 ms,验证事件顺序、回放及终态恢复,不代表真实模型性能 |
| 容量与取消 | 200 个活跃运行上限及取消通过 | 溢出拒绝;取消 P95 9.84 ms;权限等待 10 个批准、10 个取消,订阅释放 |
| 故障隔离 | 通过 | 成功、Provider 错误、模型超时、工具超时各 5 次;剩余工具执行器为 0 |
| Task API | 100 / 1000 条 CRUD、20 并发通过 | 默认分页 50,分页完整,删除后 total=0;1000 条场景创建/更新/删除 P95 为 262.36 / 275.78 / 286.18 ms |
| sqlite-vec 合成对照 | Top-20 一致 | 4000 块 × 384 维,5 次样本;Python JSON 扫描中位 1376.98 mssqlite-vec 12.92 ms;只证明该合成场景效率,不代表检索质量 |
| 内置 RAG 数据集完整性 | 5 / 5 案例关联有效 | 对照 HEAD 中 16 篇笔记、3787 块;预期 note_id / block_id 均存在,不等同于质量指标达标 |
首次后端测试因本地 `.venv` 缺少 `python-docx` 等新合并依赖而在收集阶段失败。执行 `uv sync --frozen` 后补齐 6 个锁定包并重新全量通过;未改动依赖声明或锁文件。这是本机环境未同步,不作为代码回归计数。
性能测试与构建曾并行执行,上述延迟只用于发现功能及容量问题,不作为吞吐 SLA 或性能倍率承诺。200 并发 Agent 的事件循环最大延迟为 334.08 ms,值得单独串行复测。
## 第二阶段任务矩阵
| 任务 | 当前状态 | 实现/验证依据 | 尚缺内容 |
| --- | --- | --- | --- |
| 音频转写 | 替代技术路径已实现,质量待验收 | `app/local_models/worker.py`、阶段 F 记录;当前为 Qwen3-ASR | 原清单 faster-whisper 未交付;需明确接受技术替代并同步清单 |
| 说话人分段 | 部分实现 | ERes2NetV2 片段聚类,历史 CPU/CUDA 与长录音闭环 | 非 pyannote 完整分离;逐字强制对齐、同段多人/重叠分离未实现;CER/WER/DER、阈值缺标注验收 |
| MCP Bridge / Registry | 工程实现与离线回归通过 | `extensions`、MCP Host/Registry 测试、Agent 工具链 | 目标外部服务器专项并非本轮重验;生产 OS 沙箱属于第三阶段 |
| Plugin Command | 已实现 | Palette、工作区右键与工具栏、命令校验及 effect 回归 | 后续复杂 UI 贡献属于扩展阶段 |
| Plugin Settings / Secret | 已实现 | schema 动态表单、独立凭据引用及权限校验测试 | 真实第三方插件组合仍按包验收 |
| Skill / Plugin 本地持久注册 | 已实现 | `extensions/installed.py`、恢复、摘要复核、ZIP 清理回归 | 不应继续列为“尚无持久化”;在线升级事务与社区审核属于第三阶段 |
| Provider 适配 | 工程实现,外部专项待验收 | OpenAI Compatible/Responses、Anthropic、Ollama 适配及协议回归 | 逐厂商的发现、工具往返、流、取消、Usage、错误映射、上下文限制等真实矩阵未全部验收 |
| RAG Benchmark | 后端基础已实现 | `benchmarks`、指标与生命周期测试、5 案例数据集 | 代表性评测集与调优前后对照不足;无前端报告页 |
| Agent Benchmark | 未实现 | 无 `/api/benchmarks/agent/runs``Benchmark开发说明` 明确暂缓 | 标准 Case/Dataset、任务成功率、工具选择/参数准确率、无效调用率、步骤/耗时/Token 报告 |
| Retrieval 调优 | 性能改进已实现,质量验收不完整 | 模型/维度隔离 sqlite-vec、增量索引、过滤及检索回归 | 相同数据集/模型/参数下的质量前后对照;当前 reranker 仍为 LexicalReranker,不是已上线 BGE Cross-Encoder |
| HTML / PDF / DOCX 导出 | 后端基础已实现,界面联调未完成 | `export`、三种产物内容/结构回归 | 前端无 exportService、调用入口及任务下载流程;仍不能直接完成工作区总 Demo |
| 复杂内容导出 | 部分实现 | 警告框、嵌套表格、主题配色已覆盖 | Mermaid 为源码占位;PDF/DOCX 公式为文本,图片嵌入/资源解析仍有限;DOCX 函数图像占位 |
| 函数图像 | 后端解析/静态渲染已实现 | `plot`、SVG/PDF 共享几何与预算测试 | 前端没有 function-plot 渲染、编辑刷新、缩放和预览入口;仅 Mermaid 被前端识别为图表 |
| Theme Manifest / 导入 / 管理 | 已实现 | 文件/URL/ZIP、兼容性、安全校验、隔离预览、启停卸载及前端测试 | 网络 Marketplace 非第二阶段要求;导出固定配色不等于完整主题 CSS 复刻 |
| Agent Trace | 已实现 | Tree/Timeline、参数/结果/耗时/错误、筛选、引用跳转、恢复与取消测试 | 真实 Provider 的特殊事件仍需专项验证 |
| Mermaid 编辑器与预览 | 已实现 | 真实 36 组矩阵、编辑器回归、源码/预览及大图组件 | 静态 SVG/Image 向 Export Service 的传递尚未打通 |
| 跨模块共同 Demo | 部分完成 | 录音→笔记→检索、Agent/工具/Trace 已有历史闭环 | 函数图像前端、三格式导出 UI、Mermaid 静态导出等阻止完整演示闭环 |
## 当前可推进的收尾项
### 优先完成第二阶段明确交付项
1. **Agent Benchmark**:复用已有 Agent Runtime/Trace 实现标准 Case 和报告,不以 mock 压测脚本替代任务成功率验收。
2. **函数图像前端接入**:接入编辑器、只读 Markdown/聊天、源码/预览、编辑刷新、缩放及主题。复用后端语法和资源预算,不新增不一致语法。
3. **导出前端闭环**:工作区格式/选项入口、后台任务状态、取消、下载、warning 呈现,支持未保存内容快照。
4. **Mermaid 静态导出**:安全传递 SVG/Image,补齐三格式产物检查;DOCX 函数图像和公式/图片保真度随后逐项补齐。
5. **检索质量验收**:扩充当前仅 5 案例的 Dataset,冻结语料、模型、维度、参数与回退策略,保存 FTS/vector/hybrid/RRF/rerank 的对照报告。
### 有外部依赖的验收
- 真实 Provider:准备各协议对应账号/模型矩阵与用量预算,逐项记录结果;连接测试不能代替工具、取消、错误与上下文专项。
- 音频质量:准备人工参考文本、说话人及时间标注,明确 CER/WER/DER 和业务阈值。替代技术路径是否满足原始 DoD 应单独确认。
### 本阶段可继续优化
- 单独复测 200 并发 Agent 的事件循环峰值,定位批量持久化/事件分发热点。
- 构建仍含两份 KaTeX 运行时与较大 Mermaid/语法资源块;按兼容性和首屏加载数据优化,不通过调高阈值隐藏问题。
- Mermaid 矩阵循环复用全局主题,结束后页面保留最后一个主题;36/36 只能证明结构检查。视觉验收应使用 `?theme=` 分主题页面或隔离容器并保存截图。
- 清理历史文档状态冲突:团队清单未勾选已有功能;第三阶段规划仍有“安装记录仅内存、待持久化”的旧描述;契约含旧 HashEmbedding 基线,应统一标明历史与当前实现。
## 与第三阶段的边界
Tauri/Rust Host、独立 Sync Server、联网主题/Skill/Plugin/MCP 等社区、签名分发、生产沙箱及跨设备冲突处理,仍按[第三阶段规划](../architecture/第三阶段实施规划.md)推进,不计入本轮第二阶段代码缺陷。第二阶段要求的是本地扩展、包格式与稳定接口;不能将“社区格式已实现”写成“联网社区已交付”。
## 复测与本地证据
```powershell
cd backend
uv sync --frozen
# 先设置 APP_DATA_DIR / APP_DB_PATH / APP_VAULT_PATH 为独立临时目录
uv run pytest tests -q -p no:cacheprovider
# 此脚本自行隔离运行数据,使用 mock,不调用远端 Provider
.venv/Scripts/python.exe scripts/agent-task-stress.py --output ../.local-plans/phase2-audit-20260907/agent-task.json
.venv/Scripts/python.exe scripts/vector-index-benchmark.py
cd ../frontend
npm test -- --reporter=dot
npm run build
npm run dev -- --host 127.0.0.1 --port 5187 --strictPort
```
浏览器入口:`/tests/visual/mermaid-matrix.html`。本轮开发服务器验证完成后关闭,不替换用户正在使用的服务。
本地详细证据:`.local-plans/phase2-audit-20260907/agent-task.json``dataset-integrity.json`。这些是本机生成的审计产物,不作为真实模型质量或生产性能承诺;本报告记录可复查的汇总结果。
## 用户追加验收要求:所有主题支持函数图像
第二阶段收尾任务必须更新所有现有主题对 function-plot 的支持,包括 light、dark、sepia、paper-moments、ocean-blue、midnight-purple,以及实际主题注册表中的其他主题。统一函数图像设计变量及导出配色映射,覆盖坐标轴、刻度、网格、多曲线、图例/标签、背景、错误提示、焦点/悬停和缩放控件。检查工作区编辑器、只读预览、AI 对话、大图窗口及适用的静态导出。不得仅在默认浅色下通过;以逐主题实际渲染和截图验证对比度、文字完整性、窄屏布局及主题切换。PDF/DOCX 如仍采用明确的打印配色策略,应保留可读性和提示,不声称复刻整个主题。
@@ -0,0 +1,24 @@
# 第二阶段审阅修复与主题适配
日期:2026-09-07。工作目录:`G:/OSProject/NotesAgent`,分支:`feat/phase2-completion`
## 修复内容
- DOCX 静态图同时受页面可用宽度和高度约束,保留比例,避免长图超出页面;覆盖 A4、Letter 的横图与长图。
- Agent 参数评分改为最大一对一匹配,避免宽泛参数预期抢占唯一满足具体参数预期的调用;同一调用不可重复计分。
- 导出提交期间取消会在收到任务 ID 后取消后台任务,并读取实际状态。已经完成、失败或取消请求失败时如实显示结果。
- Mermaid 围栏识别统一支持大小写和附加信息,包括 `Mermaid``mermaid title="Flow"`
- Benchmark 页接入共享页面、面板、表单、按钮、状态和主题变量,补充空状态、中文状态、指标格式及窄屏布局;同步更新执行轨迹浏览器检查的定位文字。
## 验证
- 后端:867 passed1 条既有 Starlette/httpx 弃用警告。
- 前端:81 个测试文件、445 项测试通过。
- 前端生产构建通过;仍有较大分包提示。
- 六主题:light、dark、sepia、paper-moments、ocean-blue、midnight-purple。分别生成空状态、报告、窄屏记录、窄屏报告截图,共 24 张;页面脚本错误与窄屏页面横向溢出均为零。
- 主题检查脚本:`frontend/scripts/benchmark-theme-check.cjs`。需先在 5189 端口启动 Vite,并提供 Playwright 与 Edge。接口全部使用拦截测试数据,未调用模型,也不代表真实评测质量。
- 主题检查结果:`evidence/phase2-review-20260907/theme-results.json`;完整本机截图与测试日志在 `.local-plans/phase2-review/`
DOCX 本次验证包括文档内部图片尺寸和比例;未进行 Word/LibreOffice 排版截图验证。此前真实模型验收记录仍以《第二阶段收尾实现与验收-2026-09-07》为准。
用户已有的三份笔记修改未纳入此次修复提交。
@@ -0,0 +1,87 @@
# 第二阶段收尾实现与验收
日期:2026-09-07。实现提交 `89df10b`,开发分支 `feat/phase2-completion`,基于最新拉取的 `gitea/main` / `95095197df8b607e55144748c9be0c7f6daf00df`。本分支尚未合并、推送或发布。本记录覆盖所有成员的第二阶段工程范围,替代早期“不含杨侧验收”的当前状态口径;[此前全量审计](第二阶段全量验收报告-2026-09-07.md)保留为修复前基线。
## 结论
原审计中可在现有环境推进的主要缺项已实现:标准 Agent Benchmark 与报告页、函数图像编辑器和共用 Markdown 预览、三格式未保存快照导出、Mermaid 静态图片、DOCX 图像与受支持公式、真实 RAG 质量对照,以及现有 Provider/MCP 的受限集成验证。
不将本次结果写成“所有厂商、所有文档及音频质量全部通过”。未配置的 Responses/Anthropic/Ollama 真实服务、厂商最大上下文边界、DOCX Office/LibreOffice 整页视觉复核仍列为验收边界;音频质量按本轮范围排除。Tauri/Rust、联网社区、Sync Server、操作系统沙箱仍属第三阶段。
## DoD 矩阵
| 项目 | 本轮状态 | 实现与证据 |
| --- | --- | --- |
| Agent Benchmark | 已实现、真实运行通过 | 标准版本化 Case、正式 Runtime、真实 Trace、逐例检查及成功/选择/参数/无效调用/步骤/耗时/Token 指标;4/4 标准案例与2/2集成案例通过 |
| RAG Benchmark 与报告页 | 已实现、真实本地模型对照完成 | 12篇自建中文工程笔记,关键词/语义改写各12题;6配置×24题×2轮,288个样本,零运行失败;冻结语料与逐例 provenance |
| 函数图像 | 已实现、页面复核通过 | 写作/源码切换、编辑刷新、诊断、只读/AI共用组件、缩放/重置/大图、六主题宽窄屏;共享后端白名单语法与几何 |
| 导出 UI | 已实现、浏览器下载通过 | HTML/PDF/DOCX,点击时快照、标题/纸张选项、后台状态、取消、下载和 warning;包含未保存 PHASE2-SNAPSHOT |
| 静态内容导出 | 已实现、产物检查通过 | Mermaid 浏览器净化后栅格化,按源码摘要绑定PNG;HTML/PDF/DOCX真实嵌图;DOCX函数图、MathText公式及Vault内PNG/JPEG/WebP;无法表示的资源显式warning |
| 主题与 Mermaid | 已复核 | light/dark/sepia/paper-moments/ocean-blue/midnight-purple36组Mermaid用隔离iframe检查,无全局主题串扰 |
| Provider | 已配置协议通过 | DeepSeek OpenAI Compatible:发现、普通、流式、推理事件、Tool往返、Usage、取消与错误映射;额外上下文压缩及缓存字段观测 |
| MCP | 现有服务通过 | MiniMax Coding Plan stdio注册与Agent真实web_search调用,工具权限仍由Runtime约束 |
| Plugin/Skill、Trace、Task、工作区 | 回归通过 | 全量回归;隔离压力测试、权限等待/取消、恢复与分页;本地持久安装已实现 |
| 音频 | 本轮排除质量专项 | 当前Qwen3-ASR + ERes2NetV2;没有将faster-whisper/pyannote写成已交付,也未声称CER/WER/DER达标 |
## RAG 结果与调优决策
冻结模型 `hotchpotch/bekko-embedding-v1-a8m@c721113d59a1d91b447450324f51c4b3332c924a`,384维,真实已安装本地模型;LexicalReranker `lexical-v1/1`。Top K=5、RRF K=60、候选20,参数变体K=20。向量来源逐例为local,无HashEmbedding测试注入或远端失败回退。完整记录见[rag-quality.json](evidence/phase2-20260907/rag-quality.json)。
| 配置 | Hit@1 | Hit@5 / Recall@5 | MRR | Citation Hit Rate | P50 / P95 ms |
| --- | ---: | ---: | ---: | ---: | ---: |
| FTS | .9583 | 1 / 1 | .9667 | .7500 | 9.64 / 12.20 |
| Vector(冷暖混合) | 1 | 1 / 1 | 1 | .5833 | 4571.74 / 10284.19 |
| Hybrid 加权50/50 | .9583 | 1 / 1 | .9688 | .6667 | 15.44 / 19.69 |
| Hybrid RRF | .9167 | 1 / 1 | .9583 | .6667 | 15.44 / 20.28 |
| RRF + Lexical rerank | .9167 | 1 / 1 | .9583 | .8750 | 15.70 / 22.26 |
| RRF K=20 | .9167 | 1 / 1 | .9583 | .6667 | 15.68 / 27.67 |
Citation Hit Rate判断首条返回块是否命中预期引用块,不是“任意返回块命中”。因此高笔记命中率不等于准确引用。数据属于开发集,不是生产分布或留出测试集;没有事后补造质量阈值来宣布生产达标。
RRF K=20没有收益,保留默认60;加权融合提供可选对照,不改变默认策略。Lexical rerank改善本集引用命中,却未提高笔记Top1,不能宣布通用质量提升。
瓶颈是重复查询反复启动本地Embedding推理。新增128条/10分钟进程内缓存,仅存真实本地单文本向量,键包含模型目录/revision、冻结运行配置与查询摘要;返回副本,未安装模型不走缓存,不缓存远端响应。Vector的24个miss P50=9474.32ms24个hit P50=15.11ms。后续Hybrid配置大多复用暖缓存,其时延不能与冷Vector直接比较。改前无缓存Vector P50约9368.46ms;冷推理没有提速。保留本地进程长期驻留优化作为后续独立设计,不扩大本轮范围。
## 真实 Provider、Agent、MCP
[标准报告](evidence/phase2-20260907/live.json):现有DeepSeek `deepseek-v4-flash`,4/4任务成功,工具选择/参数准确率均1,无效调用率0,平均1.75步、5682.33ms5888 Token。发现3个模型;流包含TextDelta/ThinkingDelta/Usage/Done;无效模型映射PROVIDER_INVALID_REQUEST。取消关闭客户端流约12.16ms,不等于能观测到服务商停止计费。
另通过实际Benchmark页面启动、下载与Trace跳转复测,4/4成功、5939 Token、平均4646.52ms,见[UI运行报告](evidence/phase2-20260907/agent-ui-report.json)。修复了未打开工作区时无法从报告进入已有Trace的路由拦截;创建普通新Agent运行仍遵守原工作区入口约束。
[集成报告](evidence/phase2-20260907/integration.json):检索→读笔记→3个任务创建,以及MCP web_search,共2/2通过,工具6/6匹配,16070 Token。写入仅针对隔离数据库的3个任务,按这次运行的权限票据逐项批准。报告保留真实Agent Run ID,可在保留隔离数据库时恢复Trace;Benchmark报告本身是有界内存任务,应下载保存,重启不会恢复评测注册表。
[上下文复测](evidence/phase2-20260907/context.json):不修改已保存Provider配置,在请求级测试配置中设8192窗口;detect在网络前返回CONTEXT_COMPRESSION_REQUIREDcompress调用真实模型生成摘要再回答,估算从2262降至516,原会话不变,最终答案保留Vault事实。首次256输出Token预算耗尽,见[失败记录](evidence/phase2-20260907/context-budget256.json);改用1024上限后最终回答使用289输出Token并通过。四次受限调用包含两轮摘要及回答。该测试验证应用的预算与压缩路径,不证明厂商最大窗口。
验收库中18个该Provider请求上报缓存计数,13个有正命中、18个有正未命中;缺失字段保持未知,不记作0。缓存是否命中由服务商控制,不能强制保证。没有新增账号、密钥或服务,没有打印凭据或请求认证信息。
外部剩余项:当前未配置OpenAI Responses、Anthropic Messages、Ollama真实服务,协议单测通过仍不足以替代真实服务矩阵;厂商极限上下文需要单独预算与目标窗口。本轮没有发出超大输入探测。
## 性能与回归
- 后端全量与前端测试/构建结果以本报告末尾最终验证记录为准。
- [串行压力基线](evidence/phase2-20260907/agent-task-before.json)与[修复后](evidence/phase2-20260907/agent-task-after.json):200并发均完成,事件顺序、回放和终态恢复正确。恢复读取约313ms移出事件循环;该场景loop lag max从326.14降到12.19ms。运行P95从1007.22到1155.52ms,没有声称模型或总吞吐提速。
- 200活跃运行容量拒绝与取消通过,取消P95=5.85ms;权限等待10批准/10取消、订阅释放,故障隔离后无残余工具执行器。
- Task API 100/1000条、20并发CRUD及50条分页完整;1000条创建/更新/删除P95约121.67/146.20/145.87ms。此脚本显式mock,不能代替真实Agent Benchmark。
- KaTeX通过Vite dedupe合为一个运行时chunkMermaid/语法等大惰性资源仍有构建提示,未调高阈值隐藏问题。
## 产物与视觉复核边界
浏览器真实加载项目组件与API,六主题各保存宽屏、390px窄屏和大图截图,检查缩放、源码切换、实时编辑、主题切换、无效表达式及打印warning。使用受限复杂图文档实际从UI取消后台PDF任务并看到已取消终态。[页面结果](evidence/phase2-20260907/browser-results.json)、[Mermaid矩阵](evidence/phase2-20260907/browser-mermaid-matrix.json)。窄屏函数图保留640px坐标区域并可横向滚动,避免刻度被整体缩得过小。
PDF使用嵌入的可用CJK字体,已通过Poppler检查实际页面;未安装可用字体时保留CID回退并明确warning。可用APP_EXPORT_FONT指定兼容TTF/TTC。三格式包含未保存快照、函数图、中文Mermaid及公式;DOCX的OOXML、图片关系和媒体内容通过结构检查,参见[产物清单](evidence/phase2-20260907/artifacts.json)。捆绑文档渲染器因缺少LibreOffice `soffice.exe` 无法完成DOCX整页渲染,不能声称Word分页已视觉通过。没有改用用户安装的Office/LibreOffice绕过工具约束。
MathText是受限TeX子集,不等同于KaTeX全语法;不支持的公式、远端/越界图片明确保留源码或替代文字及warning。服务器不执行客户端SVG/XML、任意TeX命令或外部资源下载。PNG每图4百万像素、总16百万像素/8MiB、最多64附件;重复引用及本地自动补全资源也受文档级累计预算约束;前端最多准备16幅Mermaid。函数图每块16表达式、预览总8000节点,导出每文档16图/8000节点,预览并发2。取消在线程渲染边界生效,不能强杀正在运行的Python绘图线程。
## 复现
1. `uv sync --frozen``pnpm install --frozen-lockfile`;把APP_DATA_DIR/APP_DB_PATH/APP_VAULT_PATH指向全新验收目录,不能指向用户知识库。
2. 仅在用户授权下复用已有Provider凭据及模型配置;APP_MODEL_PYTHON指向已安装模型解释器。语料源为`backend/data/benchmarks/corpus/phase2-v1.json`;运行`backend/scripts/phase2-quality.py --output <report.json>`,生成独立Vault和Dataset、重建真实索引。`--reuse-index`会验证块ID一致,不能跳过缺失索引。
3. 标准真实测试:`backend/scripts/phase2-live.py --provider <已有ID> --output <report.json> --execute`;上下文`phase2-context.py`同参数。写任务/MCP测试用`phase2-integration.py --execute`,先查脚本中隔离端口与现有工具ID;不能复制到生产Vault直接执行。
4. API监听8017,前端以NOTES_API_TARGET=http://127.0.0.1:8017在5187启动。设置NODE_PATH到含Playwright的运行库后执行`node frontend/scripts/phase2-browser-check.cjs <输出目录>`,访问`/tests/visual/phase2.html`及隔离Mermaid矩阵。
5. `backend/.venv/Scripts/python -m pytest backend/tests -q -p no:cacheprovider`;前端目录`pnpm test -- --reporter=dot``pnpm build`。压力脚本`backend/scripts/agent-task-stress.py --output <report.json>`自建隔离数据。
浏览器产物及完整测试日志保留于本机`.local-plans/phase2-completion/`;可复查的配置、指标与逐例证据已提交文档目录,不依赖该忽略目录才能读到结果。主工作区两份原有未提交笔记未修改。
## 最终验证记录
后端861测试通过(Python 3.13.948.32s),含HTTP/OpenAPI、预览累计复杂度、重复资源预算、导出、评分、取消与缓存回归;仅1条既有Starlette/httpx弃用提示。前端80文件437测试通过,vue-tsc及Vite生产构建通过;KaTeX仅一个261.18kB运行时chunkgzip77.76kB),大惰性分块警告仍保留。
+3
View File
@@ -1,5 +1,7 @@
# NotesAgent Frontend
> 第二阶段收尾:标准 Agent/RAG Benchmark 与报告页、函数图预览、三格式快照导出及真实 Provider/MCP 结果见[实现与验收记录](../docs/development/第二阶段收尾实现与验收-2026-09-07.md)。当前分支尚未合并,不更改下文历史 main 基线。
NotesAgent Frontend 是基于 Vue 3、TypeScript、Vite、Pinia、Vue Router、Milkdown 和 CodeMirror 6 的 Web 联调前端。当前页面调用 FastAPI 真实接口,不使用业务 Mock 作为运行时回退;测试文件中的 mock 只用于隔离单元和组件测试。
## 初始化与运行
@@ -19,6 +21,7 @@ pnpm dev
| `/search` | 全文、向量和混合检索;从后端读取并清空搜索历史 |
| `/chat` | 流式 AI 对话、知识库上下文与 Citation |
| `/agent/runs/:runId?` | 创建 Agent 运行,查看可恢复 Trace 与 Tool/Permission 事件 |
| `/benchmarks` | RAG/Agent 数据集、参数、真实运行、报告下载与 Trace 入口 |
| `/media` | 上传音频、创建/取消/重试转写、修订结果并生成知识库笔记 |
| `/tasks` | 管理用户、笔记和 Agent 产生的任务 |
| `/extensions/skills` | Skill 安装、启停与配置 |
@@ -0,0 +1,34 @@
// Visual fixtures only: all API traffic is intercepted; no provider calls or user data.
const {chromium}=require('playwright');const fs=require('node:fs/promises');const path=require('node:path');
(async()=>{
const output=path.resolve(process.argv[2]||'.local-plans/phase2-review/themes');await fs.mkdir(output,{recursive:true});
const browser=await chromium.launch({channel:'msedge',headless:true});const results=[];
for(const theme of ['light','dark','sepia','paper-moments','ocean-blue','midnight-purple']){
const page=await browser.newPage({viewport:{width:1280,height:1400}});let populated=false;const errors=[];page.on('pageerror',e=>errors.push(e.message));
const run=(id,status,progress)=>({run_id:id,kind:'rag',dataset_id:'rag-demo-v1',status,progress,error_code:status==='failed'?'BENCHMARK_RUN_FAILED':null,config_snapshot:{}});
await page.route('**/api/**',async route=>{
const request=route.request();const url=new URL(request.url());let body={items:[]};
if(url.pathname==='/api/benchmarks/datasets')body={items:[{dataset_id:'rag-demo-v1',description:'视觉测试数据集',case_count:24}]};
else if(url.pathname==='/api/providers')body={items:[]};
else if(url.pathname==='/api/benchmarks/rag/runs'&&request.method()==='POST'){populated=true;body=run('visual-completed','completed',1)}
else if(url.pathname==='/api/benchmarks/runs')body={items:populated?[run('visual-completed','completed',1),run('visual-running','running',.5),run('visual-failed','failed',.25)]:[]};
else if(url.pathname.endsWith('/report'))body={metrics:{fts:{total_cases:24,hit_at_1:.875,hit_at_5:1,mrr:.9235,citation_hit_rate:.75,p50_latency_ms:12.43,p95_latency_ms:22.16,failed_cases:0}},cases:[],config_snapshot:{fixture:true}};
else if(url.pathname==='/status')body={status:'ready'};
await route.fulfill({json:body});
});
await page.goto('http://127.0.0.1:5189/#/benchmarks');await page.getByText('还没有评测记录',{exact:true}).waitFor();
await page.evaluate(async id=>{const{useThemeStore}=await import('/src/stores/theme.ts');const store=useThemeStore();if(!['light','dark','sepia'].includes(id))await store.installCommunityTheme(id);if(!store.applyTheme(id,{persist:false}))throw Error('theme failed')},theme);
await page.screenshot({path:path.join(output,`${theme}-empty.png`)});
await page.getByRole('button',{name:'运行评测',exact:true}).click();
await page.getByRole('button',{name:'查看报告',exact:true}).first().waitFor();await page.getByRole('button',{name:'查看报告',exact:true}).first().click();
await page.getByText('87.5%',{exact:true}).waitFor();
await page.locator('#benchmark-topk').focus();
await page.screenshot({path:path.join(output,`${theme}-report.png`)});
const colors=await page.evaluate(()=>{const read=selector=>{const s=getComputedStyle(document.querySelector(selector));return {background:s.backgroundColor,color:s.color,border:s.borderColor}};return {panel:read('.benchmark-config'),input:read('#benchmark-topk'),button:read('.benchmark-config .button-primary'),metric:read('.metric-card')}});
await page.setViewportSize({width:390,height:1100});await page.screenshot({path:path.join(output,`${theme}-narrow.png`)});
const overflow=await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth);
await page.locator('.benchmark-report').scrollIntoViewIfNeeded();await page.screenshot({path:path.join(output,`${theme}-narrow-report.png`)});
if(errors.length||overflow)throw Error(JSON.stringify({theme,errors,overflow}));results.push({theme,errors,overflow,colors});await page.close();
}
await fs.writeFile(path.join(output,'results.json'),JSON.stringify(results,null,2));await browser.close();console.log(JSON.stringify(results));
})().catch(e=>{console.error(e);process.exit(1)});
@@ -0,0 +1,22 @@
const {chromium}=require('playwright');const fs=require('node:fs/promises');const path=require('node:path');
(async()=>{
if(!process.argv.includes('--execute')&&!process.argv.includes('--reuse'))throw Error('--execute required; four real Agent cases use existing quota');
const output=path.resolve('.local-plans/phase2-completion/browser');
const browser=await chromium.launch({channel:'msedge',headless:true});
const page=await browser.newPage({viewport:{width:1300,height:1000}});
await page.goto('http://127.0.0.1:5187/#/benchmarks');
await page.getByLabel(/^类型/).selectOption('agent');
await page.getByLabel(/^数据集/).selectOption('agent-core-v1');
if(!process.argv.includes('--reuse')) await page.getByRole('button',{name:'运行评测',exact:true}).click();
const row=page.locator('tbody tr').filter({hasText:'agent-core-v1'}).first();
await row.getByRole('button',{name:'查看报告'}).waitFor({timeout:180000});
await row.getByRole('button',{name:'查看报告'}).click();
await page.getByRole('heading',{name:'评测报告'}).waitFor();
await page.screenshot({path:path.join(output,'benchmark-report.png'),fullPage:true});
const pending=page.waitForEvent('download');await page.getByRole('button',{name:'下载完整 JSON'}).click();
await(await pending).saveAs(path.join(output,'agent-ui-report.json'));
await row.getByRole('link',{name:'执行轨迹'}).click();
await page.waitForSelector('.agent-page .trace-visualization',{timeout:20000});
await page.screenshot({path:path.join(output,'benchmark-trace.png'),fullPage:true});
await browser.close();console.log('Benchmark UI start/report/download/Trace completed');
})().catch(e=>{console.error(e);process.exit(1)})
+94
View File
@@ -0,0 +1,94 @@
// Run with NODE_PATH pointing to the bundled Playwright package, or a local install.
const { chromium } = require('playwright')
const fs = require('node:fs/promises')
const path = require('node:path')
;(async () => {
const output=path.resolve(process.argv[2] || '../.local-plans/phase2-completion/browser')
await fs.mkdir(output,{recursive:true})
const browser=await chromium.launch({channel:'msedge',headless:true})
const results=[]
for(const theme of ['light','dark','sepia','paper-moments','ocean-blue','midnight-purple']) {
const page=await browser.newPage({viewport:{width:1200,height:1000}})
const errors=[];page.on('pageerror',e=>errors.push(e.message))
await page.goto(`http://127.0.0.1:5187/tests/visual/phase2.html?theme=${theme}`)
await page.waitForSelector('.editor-mermaid-preview polyline',{timeout:60000})
await page.waitForSelector('.markdown-mermaid polyline',{timeout:60000})
const zoom=page.locator('.markdown-mermaid').first()
await zoom.hover()
await zoom.locator('[data-diagram-action="in"]').click()
if(Number(await zoom.getAttribute('data-diagram-scale'))<=1) throw Error('zoom failed')
await zoom.locator('[data-diagram-action="reset"]').click()
await zoom.locator('[data-code-action="source"]').click()
if(!(await zoom.locator('.markdown-code-source').isVisible())) throw Error('source toggle failed')
await zoom.locator('[data-code-action="source"]').click()
await zoom.hover()
await zoom.locator('[data-diagram-action="view"]').click()
await page.screenshot({path:path.join(output,`${theme}-viewer.png`)})
await page.keyboard.press('Escape')
await page.screenshot({path:path.join(output,`${theme}-wide.png`),fullPage:true})
await page.setViewportSize({width:390,height:844})
await page.screenshot({path:path.join(output,`${theme}-narrow.png`),fullPage:true})
if(theme==='light') {
await page.setViewportSize({width:1200,height:1000})
await page.getByRole('button',{name:'导出',exact:true}).click()
for(const format of ['html','pdf','docx']) {
await page.getByLabel('格式',{exact:true}).selectOption(format)
await page.getByRole('button',{name:'开始导出',exact:true}).click()
const row=page.locator('.export-modal > ul > li').filter({hasText:`phase2-demo.${format}`}).first()
await row.getByRole('button',{name:'下载',exact:true}).waitFor({timeout:60000})
const download=page.waitForEvent('download')
await row.getByRole('button',{name:'下载',exact:true}).click()
await (await download).saveAs(path.join(output,`demo.${format}`))
}
await page.screenshot({path:path.join(output,'export-jobs.png')})
}
if(theme==='light') {
await page.getByRole('button',{name:'关闭',exact:true}).click()
await page.getByRole('button',{name:'源码',exact:true}).click()
const input=page.getByRole('textbox',{name:'Markdown 源码编辑器'})
const original=await input.inputValue()
await input.fill(original.replace('y = x^2','y = cos(x)'))
await page.getByRole('button',{name:'写作',exact:true}).click()
await page.waitForFunction(()=>document.querySelector('.editor-mermaid-preview svg')?.textContent.includes('cos(x)'))
await page.getByRole('button',{name:'测试切换主题'}).click()
await page.waitForFunction(()=>document.documentElement.dataset.theme==='dark')
await page.waitForFunction(()=>document.querySelector('.editor-mermaid-preview svg rect')?.getAttribute('fill')!=='#ffffff')
await page.getByRole('button',{name:'源码',exact:true}).click()
await input.fill(original.replace('y = x^2','y = __import__("os")'))
await page.getByRole('button',{name:'写作',exact:true}).click()
await page.waitForFunction(()=>!document.querySelector('.editor-mermaid-preview polyline')&&!document.querySelector('.markdown-function-plot polyline'))
await page.screenshot({path:path.join(output,'invalid-expression.png'),fullPage:true})
await page.getByRole('button',{name:'源码',exact:true}).click()
await input.fill(original)
await page.getByRole('button',{name:'写作',exact:true}).click()
await page.waitForSelector('.editor-mermaid-preview polyline')
await page.getByRole('button',{name:'导出',exact:true}).click()
await page.getByLabel('格式',{exact:true}).selectOption('pdf')
await page.getByRole('button',{name:'开始导出',exact:true}).click()
await page.locator('.export-warnings').first().waitFor({timeout:60000})
await page.screenshot({path:path.join(output,'print-warning.png')})
}
results.push({theme,errors,plotCount:await page.locator('polyline').count()});await page.close()
}
const cancellation=await browser.newPage()
await cancellation.goto('http://127.0.0.1:5187/tests/visual/phase2.html')
await cancellation.getByRole('button',{name:'源码',exact:true}).click()
const heavy=Array(13).fill('```function-plot\ny = '+Array(150).fill('(x+x)').join('+')+'\n```').join('\n\n')
await cancellation.getByRole('textbox',{name:'Markdown 源码编辑器'}).fill(heavy)
await cancellation.getByRole('button',{name:'导出',exact:true}).click()
await cancellation.getByLabel('格式',{exact:true}).selectOption('pdf')
await cancellation.getByRole('button',{name:'开始导出',exact:true}).click()
const cancelRow=cancellation.locator('.export-modal > ul > li').first()
await cancelRow.getByRole('button',{name:'取消',exact:true}).click()
await cancellation.waitForFunction(()=>document.querySelector('.export-modal > ul > li')?.textContent.includes('已取消'),null,{timeout:60000})
await cancellation.screenshot({path:path.join(output,'export-cancelled.png')})
await cancellation.close()
const matrix=await browser.newPage()
await matrix.goto('http://127.0.0.1:5187/tests/visual/mermaid-matrix.html')
await matrix.waitForFunction(()=>document.documentElement.dataset.complete==='true',null,{timeout:120000})
const mermaid=JSON.parse(await matrix.locator('#status').textContent())
if(mermaid.passed!==36 || mermaid.total!==36) throw Error('Mermaid matrix failed')
await fs.writeFile(path.join(output,'mermaid-matrix.json'),JSON.stringify(mermaid,null,2))
await matrix.close()
await fs.writeFile(path.join(output,'results.json'),JSON.stringify(results,null,2));console.log(JSON.stringify(results));await browser.close()
})().catch(e=>{console.error(e);process.exit(1)})
@@ -220,3 +220,8 @@ function close() { disarm(); viewer.value?.close(); svgHtml.value = ''; opener?.
:is(.editor-mermaid-preview, .markdown-mermaid):is(:hover, :focus-within) > .diagram-controls { opacity: 1; pointer-events: auto; }
@media (hover: none) { :is(.editor-mermaid-preview, .markdown-mermaid) > .diagram-controls { opacity: 1; pointer-events: auto; } }
</style>
<style>
/* 窄屏仍保持 10px 坐标轴文字可读,溢出由现有图表容器滚动承接。 */
.function-plot-preview > svg, .markdown-function-plot > svg { min-width: 640px; }
</style>
@@ -24,7 +24,7 @@ const diagramTheme = computed<'light' | 'dark'>(() => (themeStore.isDark ? 'dark
// Mermaid SVG CSS
watch([() => props.source, diagramTheme, () => themeStore.currentThemeId, () => JSON.stringify(markdownPreferences.normalized), () => JSON.stringify([props.citationNumbers, props.citationAliases])], async ([source, theme]) => {
const version = ++renderVersion
const result = await renderMarkdown(source, { theme, preferences: markdownPreferences.normalized, citationNumbers: props.citationNumbers, citationAliases: props.citationAliases })
const result = await renderMarkdown(source, { theme, themeId: themeStore.currentThemeId, preferences: markdownPreferences.normalized, citationNumbers: props.citationNumbers, citationAliases: props.citationAliases })
if (version === renderVersion) html.value = result
}, { immediate: true, flush: 'post' })
</script>
@@ -20,6 +20,7 @@ const navItems = computed(() => [
{ name: 'plugins', icon: Connection, label: 'Plugin' },
{ name: 'mcp-servers', icon: Monitor, label: 'MCP' },
{ name: 'themes', icon: Brush, label: t('主题', 'Themes') },
{ name: 'benchmarks', icon: Monitor, label: 'Benchmark' },
{ name: 'logs', icon: Document, label: t('日志', 'Logs') },
{ name: 'settings', icon: Setting, label: t('设置', 'Settings') },
])
@@ -0,0 +1,29 @@
// @vitest-environment happy-dom
import {mount,flushPromises} from '@vue/test-utils'
import {afterEach,beforeEach,expect,it,vi} from 'vitest'
import BenchmarkView from './BenchmarkView.vue'
const service=vi.hoisted(()=>({datasets:vi.fn(),list:vi.fn(),start:vi.fn(),cancel:vi.fn(),report:vi.fn()}))
vi.mock('@/services/benchmarkService',()=>({benchmarkService:service}))
vi.mock('@/services/providerService',()=>({listProviders:vi.fn().mockResolvedValue([])}))
beforeEach(()=>{service.datasets.mockResolvedValue([{id:'rag-demo',cases:2}]);service.list.mockResolvedValue([])})
afterEach(()=>vi.clearAllMocks())
it('shows a useful empty state and submits the selected retrieval configuration',async()=>{
const wrapper=mount(BenchmarkView,{global:{stubs:{RouterLink:true}}});await flushPromises()
expect(wrapper.text()).toContain('还没有评测记录')
await wrapper.get('#benchmark-fusion').setValue('weighted')
expect(wrapper.get('#benchmark-rrfk').attributes('disabled')).toBeDefined()
await wrapper.get('form').trigger('submit');await flushPromises()
expect(service.start).toHaveBeenCalledWith('rag',expect.objectContaining({dataset_id:'rag-demo',retrieval:expect.objectContaining({fusion:'weighted'})}))
wrapper.unmount()
})
it('renders report percentages, unavailable metrics and localized terminal states',async()=>{
service.list.mockResolvedValue([{id:'r',datasetId:'agent-demo',status:'completed',progress:1,errorCode:null}])
service.report.mockResolvedValue({metrics:{task_success_rate:.75,tool_argument_accuracy:null,token_usage:120},cases:[],config_snapshot:{}})
const wrapper=mount(BenchmarkView,{global:{stubs:{RouterLink:true}}});await flushPromises()
expect(wrapper.text()).toContain('已完成')
await wrapper.findAll('button').find(button=>button.text()==='查看报告')!.trigger('click');await flushPromises()
expect(wrapper.get('.benchmark-report').text()).toContain('75%')
expect(wrapper.get('.benchmark-report').text()).toContain('不适用')
expect(wrapper.get('.benchmark-report').text()).toContain('agent-demo')
wrapper.unmount()
})
@@ -0,0 +1,123 @@
<script setup lang="ts">
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { benchmarkService as service, type BenchmarkRun } from '@/services/benchmarkService'
import { listProviders } from '@/services/providerService'
const kind = ref<'rag' | 'agent'>('rag'), dataset = ref(''), error = ref(''), busy = ref(false)
const datasets = ref<Awaited<ReturnType<typeof service.datasets>>>([]), runs = ref<BenchmarkRun[]>([])
const providers = ref<Awaited<ReturnType<typeof listProviders>>>([]), provider = ref(''), model = ref('')
const report = ref<Awaited<ReturnType<typeof service.report>> | null>(null)
const reportName = ref(''), loading = ref(true)
const statusLabels: Record<string,string> = { queued:'排队中', running:'运行中', completed:'已完成', failed:'失败', cancelled:'已取消' }
const activeCount = computed(() => runs.value.filter(run => ['queued','running'].includes(run.status)).length)
const ratioKeys = new Set(['task_success_rate','tool_selection_accuracy','tool_argument_accuracy','invalid_tool_call_rate','hit_at_1','hit_at_5','recall_at_k','citation_hit_rate','failure_rate'])
const topK = ref(5), rrfK = ref(60), rerank = ref(false)
const fusion = ref<'rrf' | 'weighted'>('rrf')
const metricLabels: Record<string, string> = {
total_cases: '计划样本', evaluated_cases: '已评样本', successful_cases: '运行成功', failed_cases: '运行失败',
task_success_rate: '任务成功率', tool_selection_accuracy: '工具选择准确率', tool_argument_accuracy: '参数准确率',
invalid_tool_call_rate: '无效调用率', average_steps: '平均步骤', average_latency_ms: '平均耗时 (ms)',
token_usage: 'Token 用量', tool_calls: '实际工具调用', expected_calls: '预期工具调用',
hit_at_1: 'Hit@1', hit_at_5: 'Hit@5', recall_at_k: 'Recall@K', mrr: 'MRR', citation_hit_rate: '引用命中率',
p50_latency_ms: 'P50 (ms)', p95_latency_ms: 'P95 (ms)', failure_rate: '运行失败率',
}
const metricGroups = computed(() => {
const metrics = report.value?.metrics ?? {}
const groups = 'task_success_rate' in metrics ? { Agent: metrics } : metrics
return Object.entries(groups).filter(([, value]) => value && typeof value === 'object').map(([name, value]) => ({
name, rows: Object.entries(value as Record<string, unknown>).map(([key, number]) => ({
label: metricLabels[key] ?? key,
value: number === null ? '不适用' : typeof number === 'number' ? ratioKeys.has(key) ? `${Number((number*100).toFixed(2))}%` : Number(number.toFixed(key.includes('latency') ? 2 : 4)).toLocaleString() : String(number),
})),
}))
})
let timer: ReturnType<typeof setTimeout> | undefined, disposed = false
async function loadDatasets() { try { datasets.value = await service.datasets(kind.value); dataset.value = datasets.value[0]?.id ?? '' } catch(e) { error.value = String(e) } }
async function refresh() { try { runs.value = await service.list() } catch(e) { error.value = String(e) } finally { loading.value = false } if (!disposed) timer = setTimeout(refresh, 1500) }
watch(kind, loadDatasets)
watch(provider, id => { model.value = providers.value.find(p => p.provider_id === id)?.default_model ?? '' })
async function start() {
error.value = ''; busy.value = true
try { await service.start(kind.value, kind.value === 'agent' ? { dataset_id: dataset.value, provider_id: provider.value, model: model.value, max_steps: 6, timeout_seconds: 90, token_budget: 6000 } : { dataset_id: dataset.value, modes: ['fts','vector','hybrid'], retrieval: { top_k: topK.value, fusion: fusion.value, rrf_k: rrfK.value, rerank: rerank.value } }) }
catch(e) { error.value = String(e) } finally { busy.value = false }
}
async function action(run: BenchmarkRun, cancel = false) { try { if (cancel) await service.cancel(run.id); else { report.value = await service.report(run.id); reportName.value = run.datasetId } } catch(e) { error.value = String(e) } }
function download() { const url = URL.createObjectURL(new Blob([JSON.stringify(report.value,null,2)], { type:'application/json' })); const a=document.createElement('a'); a.href=url; a.download='benchmark-report.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000) }
onMounted(async () => { void refresh(); void loadDatasets(); try { providers.value=(await listProviders()).filter(p=>p.enabled); provider.value=providers.value[0]?.provider_id ?? '' } catch(e) { error.value=String(e) } })
onBeforeUnmount(() => { disposed=true; clearTimeout(timer) })
</script>
<template>
<main class="feature-page benchmark-page">
<header class="feature-header">
<div><h1>Benchmark 评测</h1><p>比较检索质量与智能体表现查看每次评测的结果和执行轨迹</p></div>
<span class="badge" :class="{ info: activeCount > 0 }">{{ activeCount ? `${activeCount} 项正在运行` : 'RAG / Agent' }}</span>
</header>
<div class="benchmark-content">
<form class="panel benchmark-config" @submit.prevent="start">
<div class="section-heading"><div><h2>创建评测</h2><p class="subtle">选择数据集和运行配置结果将保留在下方列表</p></div></div>
<div class="form-grid">
<div class="field"><label for="benchmark-kind">类型</label><select id="benchmark-kind" v-model="kind" class="select"><option value="rag">RAG 检索</option><option value="agent">Agent 任务</option></select></div>
<div class="field dataset-field"><label for="benchmark-dataset">数据集</label><select id="benchmark-dataset" v-model="dataset" class="select"><option v-if="!datasets.length" value="">暂无可用数据集</option><option v-for="d in datasets" :key="d.id" :value="d.id">{{ d.id }} · {{ d.cases }} 案例</option></select></div>
<template v-if="kind === 'agent'">
<div class="field"><label for="benchmark-provider">提供商</label><select id="benchmark-provider" v-model="provider" class="select"><option v-if="!providers.length" value="">暂无可用提供商</option><option v-for="p in providers" :key="p.provider_id" :value="p.provider_id">{{ p.name }}</option></select></div>
<div class="field"><label for="benchmark-model">模型</label><input id="benchmark-model" v-model="model" class="input" placeholder="模型 ID"></div>
</template>
<template v-else>
<div class="field"><label for="benchmark-fusion">融合方式</label><select id="benchmark-fusion" v-model="fusion" class="select"><option value="rrf">RRF 排名融合</option><option value="weighted">加权 50/50</option></select></div>
<div class="field"><label for="benchmark-topk">Top K</label><input id="benchmark-topk" v-model.number="topK" class="input" type="number" min="1" max="100"></div>
<div class="field"><label for="benchmark-rrfk">RRF K</label><input id="benchmark-rrfk" v-model.number="rrfK" class="input" type="number" min="1" :disabled="fusion !== 'rrf'"></div>
</template>
</div>
<div class="config-footer">
<label v-if="kind === 'rag'" class="checkbox-label"><input v-model="rerank" type="checkbox">启用词面重排Lexical Reranker</label>
<p v-else class="subtle">将使用所选提供商额度需要工具权限时请在执行轨迹中处理</p>
<button class="button-primary" :disabled="busy || !dataset || (kind === 'agent' && (!provider || !model))">{{ busy ? '正在创建…' : '运行评测' }}</button>
</div>
</form>
<p v-if="error" class="error-banner" role="alert">{{ error }}</p>
<section class="panel benchmark-history" aria-labelledby="benchmark-history-title" :aria-busy="loading">
<div class="section-heading"><h2 id="benchmark-history-title">运行记录</h2><span class="badge">{{ runs.length }} </span></div>
<div v-if="!runs.length" class="empty-state"><div><strong>{{ loading ? '正在加载记录…' : '还没有评测记录' }}</strong><p>选择上方的数据集并运行评测完成后可查看指标下载报告</p></div></div>
<div v-else class="table-scroll"><table><thead><tr><th>数据集</th><th>状态</th><th>操作</th></tr></thead><tbody><tr v-for="run in runs" :key="run.id">
<td><strong>{{ run.datasetId }}</strong><small class="subtle run-id">{{ run.id }}</small></td>
<td><span class="badge" :class="{ success:run.status === 'completed', error:run.status === 'failed', info:['queued','running'].includes(run.status), warning:run.status === 'cancelled' }">{{ statusLabels[run.status] ?? run.status }}</span><span v-if="run.progress !== null" class="progress-label subtle">{{ Math.round(run.progress*100) }}%</span><small v-if="run.errorCode" class="run-error">{{ run.errorCode }}</small></td>
<td><div class="inline-actions"><button v-if="['queued','running'].includes(run.status)" class="button-secondary" @click="action(run,true)">取消</button><button v-else class="button-secondary" @click="action(run)">查看报告</button><RouterLink v-if="run.agentId" class="trace-link" :to="`/agent/runs/${run.agentId}`">执行轨迹</RouterLink></div></td>
</tr></tbody></table></div>
</section>
<section v-if="report" class="panel benchmark-report" aria-labelledby="benchmark-report-title">
<div class="section-heading"><div><h2 id="benchmark-report-title">评测报告</h2><p class="subtle">{{ reportName }}</p></div><button class="button-secondary" @click="download">下载完整 JSON</button></div>
<div v-for="group in metricGroups" :key="group.name" class="metric-group"><h3>{{ group.name }}</h3><dl class="metric-grid"><div v-for="row in group.rows" :key="row.label" class="metric-card"><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div></dl></div>
<details class="ui-disclosure"><summary>冻结配置与逐例证据</summary><pre>{{ JSON.stringify(report,null,2) }}</pre></details>
</section>
</div>
</main>
</template>
<style scoped>
.benchmark-page { width:100%; min-width:0; color:var(--color-text-primary); }
.benchmark-content { max-width:1180px; margin:0 auto; display:grid; gap:var(--space-xl); }
.benchmark-content > .panel { width:100%; min-width:0; margin:0; padding:var(--space-xl); }
.section-heading { display:flex; align-items:center; justify-content:space-between; gap:var(--space-md); margin-bottom:var(--space-lg); }
h2 { margin:0; font-size:var(--font-size-lg); font-weight:650; } h3 { margin:0 0 var(--space-md); font-size:var(--font-size-md); }
.section-heading p { margin:var(--space-xs) 0 0; } .form-grid { grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); }
.dataset-field { grid-column:span 2; } .field { min-width:0; }
.config-footer { display:flex; align-items:center; justify-content:space-between; gap:var(--space-lg); margin-top:var(--space-xl); padding-top:var(--space-lg); border-top:1px solid var(--color-border-default); }
.config-footer p { margin:0; } .config-footer .button-primary { margin-left:auto; flex-shrink:0; }
.checkbox-label { display:flex; align-items:center; gap:var(--space-sm); color:var(--color-text-secondary); font-size:var(--font-size-sm); }
.empty-state { min-height:170px; } .empty-state p { margin:0; line-height:1.7; }
.table-scroll { overflow-x:auto; } table { width:100%; min-width:580px; border-collapse:collapse; font-size:var(--font-size-sm); }
th { text-align:left; color:var(--color-text-secondary); background:var(--color-background-secondary); font-weight:600; }
td,th { padding:var(--space-md); border-bottom:1px solid var(--color-border-default); } tbody tr:last-child td { border-bottom:0; }
.run-id,.run-error { display:block; margin-top:var(--space-xs); overflow-wrap:anywhere; } .run-error { color:var(--color-error); }
.progress-label { margin-left:var(--space-sm); } .trace-link { color:var(--color-accent-primary); text-decoration:none; font-weight:600; } .trace-link:hover { text-decoration:underline; }
.metric-group + .metric-group { margin-top:var(--space-xl); }
.metric-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:var(--space-md); margin:0 0 var(--space-xl); }
.metric-card { padding:var(--space-lg); border:1px solid var(--color-border-default); border-radius:var(--radius-md); background:var(--color-background-secondary); }
dt { font-size:var(--font-size-sm); color:var(--color-text-secondary); } dd { margin:var(--space-sm) 0 0; font-size:var(--font-size-2xl); font-weight:650; font-variant-numeric:tabular-nums; }
pre { padding:var(--space-md); border-radius:var(--radius-md); background:var(--color-background-secondary); color:var(--color-text-primary); white-space:pre-wrap; overflow-wrap:anywhere; font-family:var(--font-editor-mono); font-size:var(--font-size-sm); }
.error-banner { margin:0; }
@media(max-width:640px) {
.benchmark-content > .panel { padding:var(--space-lg); }
.form-grid { grid-template-columns:minmax(0,1fr); } .dataset-field { grid-column:auto; }
.section-heading,.config-footer { align-items:flex-start; flex-wrap:wrap; } .config-footer .button-primary { width:100%; }
.metric-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } dd { font-size:var(--font-size-xl); }
}
</style>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useEditorStore } from '@/stores/editor'
import { useWorkspaceStore } from '@/stores/workspace'
import ExportDialog from './ExportDialog.vue'
import { computed, ref } from 'vue'
import ActionDialog from '@/components/common/ActionDialog.vue'
import { useActionDialog } from '@/composables/useActionDialog'
@@ -10,6 +11,7 @@ const editorStore = useEditorStore()
const workspaceStore = useWorkspaceStore()
const { actionDialog, resolveAction, askConfirm } = useActionDialog()
const reloadError = ref('')
const exportOpen = ref(false)
const needsRecovery = computed(() => ['conflict', 'external_changed'].includes(editorStore.saveStatus))
const missingFile = computed(() => needsRecovery.value && editorStore.currentFilePath === workspaceStore.activeFilePath && !workspaceStore.activeFile && !workspaceStore.treeRefreshError)
function downloadCopy() {
@@ -43,9 +45,11 @@ const statusText = computed<Record<string, string>>(() => ({
<template>
<header class="editor-header">
<ExportDialog v-if="exportOpen" @close="exportOpen = false" />
<ActionDialog v-if="actionDialog" v-bind="actionDialog" @resolve="resolveAction" />
<div class="file-identity"><strong>{{ workspaceStore.activeFile?.name ?? t('未命名笔记', 'Untitled note') }}</strong><small>{{ workspaceStore.activeFilePath }}</small></div>
<div class="editor-actions">
<button class="button-secondary" @click="exportOpen = true">{{ t('导出', 'Export') }}</button>
<span class="save-status" :class="editorStore.saveStatus">{{ statusText[editorStore.saveStatus] }}</span>
<button v-if="needsRecovery && !missingFile" class="button-secondary" @click="reload">{{ t('重新加载外部版本', 'Reload external version') }}</button>
<span v-if="missingFile" class="save-status conflict">{{ t('原文件已删除或移动', 'Original file deleted or moved') }}</span>
@@ -0,0 +1,22 @@
// @vitest-environment jsdom
import {mount,flushPromises} from '@vue/test-utils'
import {it,expect,vi} from 'vitest'
import ExportDialog from './ExportDialog.vue'
import {apiClient} from '@/services/apiClient'
vi.mock('@/stores/editor',()=>({useEditorStore:()=>({content:'# snapshot',currentFilePath:'note.md'})}))
vi.mock('@/stores/theme',()=>({useThemeStore:()=>({currentThemeId:'light'})}))
vi.mock('@/services/mermaidService',()=>({renderMermaid:vi.fn()}))
vi.mock('@/services/apiClient',()=>({apiClient:{post:vi.fn(),get:vi.fn()}}))
it('closing the dialog after submitting preserves the background export',async()=>{
let finish!:(value:unknown)=>void
vi.mocked(apiClient.get).mockImplementation(async(path:string)=>path==='/api/exports'?{items:[]}:{job_id:'closing-job',status:'cancelled',warnings:[],error:null,file:null})
vi.mocked(apiClient.post).mockImplementationOnce(()=>new Promise(resolve=>{finish=resolve}) as never).mockResolvedValue({status:'completed'})
const wrapper=mount(ExportDialog,{global:{stubs:{AppDialog:{template:'<div><slot /></div>'}}}})
await flushPromises()
await wrapper.findAll('button').find(b=>b.text()==='开始导出')!.trigger('click')
expect(apiClient.post).toHaveBeenCalledWith('/api/exports',expect.anything())
wrapper.unmount()
finish({job_id:'closing-job',status:'queued',warnings:[],error:null,file:null})
await flushPromises()
expect(apiClient.post).not.toHaveBeenCalledWith('/api/exports/closing-job/cancel')
})
@@ -0,0 +1,57 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import AppDialog from '@/components/common/AppDialog.vue'
import { useEditorStore } from '@/stores/editor'
import { useThemeStore } from '@/stores/theme'
import { exportService, captureExportPalette, type ExportFormat, type ExportJob } from '@/services/exportService'
const emit = defineEmits<{ close: [] }>()
const editor = useEditorStore(), theme = useThemeStore()
const format = ref<ExportFormat>('html'), page = ref('A4'), title = ref(true)
const jobs = ref<ExportJob[]>([]), error = ref(''), preparing = ref(false)
let timer: ReturnType<typeof setTimeout> | undefined, disposed = false
let controller: AbortController | undefined
const labels = { queued: '排队中', running: '渲染中', completed: '已完成', failed: '失败', cancelled: '已取消' }
async function refresh() {
try { const value = await exportService.list(); if (!disposed) jobs.value = value } catch (e) { error.value = String(e) }
if (!disposed) timer = setTimeout(refresh, 1500)
}
async function start() {
preparing.value = true; error.value = ''; controller = new AbortController()
const snapshot = editor.content, name = editor.currentFilePath?.split('/').pop()?.replace(/\.md$/i, '') ?? '笔记'
try {
const job = await exportService.create(snapshot, name, format.value, { theme_id: theme.currentThemeId, include_title: title.value, page_size: page.value, palette: captureExportPalette() }, controller.signal, editor.currentFilePath ?? undefined)
if (!disposed) jobs.value.unshift(job)
} catch (e) { error.value = e instanceof DOMException && e.name === 'AbortError' ? '已取消导出' : String(e) }
finally { preparing.value = false }
}
async function action(job: ExportJob, download = false) {
try { if (download) await exportService.download(job); else await exportService.cancel(job.id) } catch (e) { error.value = String(e) }
}
onMounted(refresh)
onBeforeUnmount(() => { disposed = true; clearTimeout(timer) })
</script>
<template>
<AppDialog label="导出笔记" @close="emit('close')"><section class="modal export-modal">
<h2>导出笔记</h2><p>导出点击时的编辑器快照包含未保存修改关闭窗口后后台任务继续运行</p>
<label for="export-format">格式</label><select id="export-format" v-model="format"><option value="html">HTML</option><option value="pdf">PDF</option><option value="docx">DOCX</option></select>
<label>纸张 <select v-model="page"><option>A4</option><option>Letter</option></select></label>
<label><input v-model="title" type="checkbox">包含标题</label>
<p v-if="format === 'docx'">DOCX 使用浅色打印样式</p>
<p v-if="format === 'pdf'">PDF 使用当前笔记主题与排版样式</p>
<button class="button-primary" :disabled="preparing || !editor.content.trim()" @click="start">{{ preparing ? '准备图表' : '开始导出' }}</button>
<button v-if="preparing" @click="controller?.abort()">取消准备</button>
<p v-if="error" role="alert">{{ error }}</p>
<ul><li v-for="job in jobs" :key="job.id">
<strong>{{ job.fileName ?? job.id }}</strong> · {{ labels[job.status] }}
<button v-if="job.status === 'completed'" @click="action(job, true)">下载</button>
<button v-if="['queued','running'].includes(job.status)" @click="action(job)">取消</button>
<p v-if="job.error" role="alert">{{ job.error }}</p>
<ul v-if="job.warnings.length" class="export-warnings" aria-label="导出警告"><li v-for="warning in job.warnings" :key="warning">{{ warning }}</li></ul>
</li></ul>
<button class="button-secondary" @click="emit('close')">关闭</button>
</section></AppDialog>
</template>
<style scoped>
.export-modal { width: min(640px, 100%); padding: 24px; background: var(--color-surface-primary); border: 1px solid var(--color-border-default); border-radius: 12px; }
label { display: inline-flex; align-items:center; gap: 8px; margin: 8px; } li { margin-block: 12px; overflow-wrap: anywhere; } button { margin: 6px; } .export-warnings { color: var(--color-warning); } [role=alert] { color:var(--color-error); }
</style>
@@ -156,20 +156,20 @@ function foldHeadings(action: 'toggle' | 'all' | 'none') {
}
})
}
const diagramPreviews = new Map<string, { source: string; apply: (value: HTMLElement) => void }>()
function renderDiagram(source: string, apply: (value: HTMLElement) => void) {
const diagramPreviews = new Map<string, { source: string; kind: string; apply: (value: HTMLElement) => void }>()
function renderDiagram(source: string, apply: (value: HTMLElement) => void, kind = 'mermaid') {
for (const [id, entry] of diagramPreviews) {
if (entry.apply === apply) diagramPreviews.delete(id)
}
const element = createMermaidPreview(source, themeStore.isDark, apply)
diagramPreviews.set(element.dataset.previewId!, { source, apply })
const element = createMermaidPreview(source, themeStore.isDark, apply, kind, themeStore.currentThemeId)
diagramPreviews.set(element.dataset.previewId!, { source, apply, kind })
return element
}
watch(() => themeStore.currentThemeId, () => {
const current = [...diagramPreviews.entries()]
diagramPreviews.clear()
for (const [id, entry] of current) {
if (editorRoot.value?.querySelector(`[id="${id}"]`)) entry.apply(renderDiagram(entry.source, entry.apply))
if (editorRoot.value?.querySelector(`[id="${id}"]`)) entry.apply(renderDiagram(entry.source, entry.apply, entry.kind))
}
}, { flush: 'post' })
@@ -333,8 +333,8 @@ onMounted(async () => {
...config,
languages: shikiLanguages(themeStore.resolvedCodeBlockTheme),
renderLanguage: renderCodeLanguage,
renderPreview: (language, content, applyPreview) => language.trim().toLowerCase() === 'mermaid'
? markdownPreferences.diagrams ? renderDiagram(content, applyPreview) : null
renderPreview: (language, content, applyPreview) => ['mermaid', 'function-plot'].includes(language.trim().toLowerCase())
? markdownPreferences.diagrams ? renderDiagram(content, applyPreview, language.trim().toLowerCase()) : null
: config.renderPreview(language, content, applyPreview),
extensions: [basicSetup, keymap.of([indentWithTab]), shikiEditorTheme(themeStore.resolvedCodeBlockTheme),
indentUnit.of(' '.repeat(markdownPreferences.indent)), CodeEditorState.tabSize.of(markdownPreferences.indent),
@@ -1,17 +1,18 @@
import { renderFunctionPlot } from '@/services/functionPlotService'
import { nextTick } from 'vue'
import { renderMermaid } from '@/services/mermaidService'
import { t } from '@/i18n'
import { appendDiagramControls } from '@/utils/diagramControls'
let previewId = 0
export function createMermaidPreview(source: string, dark: boolean, applyPreview: (value: HTMLElement) => void): HTMLElement {
export function createMermaidPreview(source: string, dark: boolean, applyPreview: (value: HTMLElement) => void, kind = 'mermaid', themeId = 'light'): HTMLElement {
// Each revision owns its element, so a slow render cannot replace newer content.
// Milkdown sanitizes Element input to its inner HTML; retain the revision
// marker and controls inside an otherwise disposable envelope.
const envelope = document.createElement('div')
const container = document.createElement('div')
envelope.append(container)
container.className = 'editor-mermaid-preview'
container.className = 'editor-mermaid-preview' + (kind === 'function-plot' ? ' function-plot-preview' : '')
container.id = `editor-mermaid-preview-${++previewId}`
envelope.dataset.previewId = container.id
container.setAttribute('aria-live', 'polite')
@@ -27,8 +28,11 @@ export function createMermaidPreview(source: string, dark: boolean, applyPreview
applyPreview(envelope.cloneNode(true) as HTMLElement)
}
}
void renderMermaid(source, { theme: dark ? 'dark' : 'light' }).then(result => {
if (result.warnings.length) {
void (kind === 'function-plot' ? new Promise<void>(resolve => setTimeout(resolve, 180)) : Promise.resolve()).then<{ svg: string; warnings: string[] }>(() => {
if (kind === 'function-plot' && !document.getElementById(container.id)) throw new Error('stale preview')
return kind === 'function-plot' ? renderFunctionPlot(source, themeId) : renderMermaid(source, { theme: dark ? 'dark' : 'light' })
}).then(result => {
if (result.warnings.length && (kind === 'mermaid' || !result.svg)) {
container.classList.add('has-error')
container.textContent = `${t('图表语法有误,可点击编辑修改:', 'Diagram syntax error. Choose Edit to fix:')} ${result.warnings.join('\n')}`
void publish()
@@ -37,6 +41,7 @@ export function createMermaidPreview(source: string, dark: boolean, applyPreview
// Mermaid runs in strict mode; Milkdown sanitizes the preview before insertion.
container.innerHTML = result.svg
appendDiagramControls(container)
if (result.warnings.length) { const warning = document.createElement('p'); warning.textContent = result.warnings.join('\n'); warning.setAttribute('role', 'status'); container.append(warning) }
void publish()
}).catch(() => {
container.textContent = t('图表渲染失败,请点击编辑检查源码。', 'Unable to render diagram. Choose Edit to inspect the source.')
@@ -69,7 +69,7 @@ it('offers fenced-code aliases and retains the LaTeX selector', () => {
it('offers every bundled Shiki language and alias', () => {
const languages = shikiLanguages('github-light')
expect(languages).toHaveLength(bundledLanguagesInfo.length + 1)
expect(languages).toHaveLength(bundledLanguagesInfo.length + 2)
for (const info of bundledLanguagesInfo) {
const language = languages.find(item => item.alias.includes(info.id))!
expect(language, info.id).toBeDefined()
@@ -60,6 +60,7 @@ export async function shikiLanguage(language: string, theme: CodeTheme): Promise
export function shikiLanguages(theme: CodeTheme): LanguageDescription[] {
return [
LanguageDescription.of({ name: 'function-plot', alias: ['Function Plot'], load: () => shikiLanguage('text', theme) }),
...bundledLanguagesInfo.map(info => LanguageDescription.of({
name: info.id,
alias: [info.name, ...(info.aliases ?? [])],
+4 -1
View File
@@ -3,6 +3,7 @@ import { useWorkspaceStore } from '@/stores/workspace'
import { t } from '@/i18n'
const routes = [
{ path: '/benchmarks', name: 'benchmarks', component: () => import('@/features/benchmarks/BenchmarkView.vue'), meta: { title: 'Benchmark' } },
{ path: '/logs', name: 'logs', component: () => import('@/features/logs/LogsView.vue'), meta: { title: '运行日志' } },
{ path: '/media', name: 'media', component: () => import('@/features/media/MediaView.vue'), meta: { title: '音视频转写', requiresVault: true } },
{
@@ -80,7 +81,9 @@ const router = createRouter({
router.beforeEach((to) => {
const workspaceStore = useWorkspaceStore()
if (to.meta.requiresVault && !workspaceStore.hasVault) {
// Benchmark 不依赖工作区界面;报告中的持久化 Trace 和待决权限入口必须仍可访问。
const existingAgentRun = to.name === 'agent' && Boolean(to.params.runId)
if (to.meta.requiresVault && !workspaceStore.hasVault && !existingAgentRun) {
return { path: '/' }
}
if (to.path === '/' && workspaceStore.hasVault) {
+15
View File
@@ -0,0 +1,15 @@
import { apiClient } from './apiClient'
interface RunWire { run_id: string; kind: 'rag' | 'agent'; dataset_id: string; status: string; progress: number | null; config_snapshot: Record<string, unknown>; error_code: string | null }
export interface BenchmarkRun { id: string; kind: 'rag' | 'agent'; datasetId: string; status: string; progress: number | null; agentId?: string; errorCode: string | null }
const map = (r: RunWire): BenchmarkRun => ({ id: r.run_id, kind: r.kind, datasetId: r.dataset_id, status: r.status, progress: r.progress, agentId: r.config_snapshot.active_agent_run_id as string | undefined, errorCode: r.error_code })
// 保留运行配置中的 Agent Run ID,使报告页可直接进入对应 Trace 和权限处理入口。
export const benchmarkService = {
async datasets(kind: 'rag' | 'agent') {
const r = await apiClient.get<{ items: { dataset_id: string; description: string; case_count: number }[] }>('/api/benchmarks/datasets', { params: { kind } })
return r.items.map(d => ({ id: d.dataset_id, description: d.description, cases: d.case_count }))
},
async list() { return (await apiClient.get<{ items: RunWire[] }>('/api/benchmarks/runs')).items.map(map) },
async start(kind: 'rag' | 'agent', body: object) { return map(await apiClient.post<RunWire>(`/api/benchmarks/${kind}/runs`, body)) },
cancel(id: string) { return apiClient.post(`/api/benchmarks/runs/${encodeURIComponent(id)}/cancel`) },
report(id: string) { return apiClient.get<{ metrics: Record<string, unknown>; cases: unknown[]; config_snapshot: Record<string, unknown> }>(`/api/benchmarks/runs/${encodeURIComponent(id)}/report`) },
}
@@ -0,0 +1,81 @@
// @vitest-environment jsdom
import {webcrypto} from 'node:crypto'
import {describe,it,expect,vi,afterEach} from 'vitest'
vi.mock('./apiClient',()=>({apiClient:{post:vi.fn(),get:vi.fn()}}))
vi.mock('./mermaidService',()=>({renderMermaid:vi.fn()}))
vi.mock('./pdfSnapshotService',()=>({preparePdfSnapshot:vi.fn().mockResolvedValue('<html>theme snapshot</html>')}))
import {preparePdfSnapshot} from './pdfSnapshotService'
import {renderMermaid} from './mermaidService'
import {apiClient} from './apiClient'
import {exportService,captureExportPalette} from './exportService'
describe('export snapshot contract',()=>{
it('submits the unsaved Markdown snapshot and maps warnings and filename',async()=>{
vi.mocked(apiClient.post).mockResolvedValue({job_id:'job',status:'queued',warnings:['print palette'],file:{file_name:'note.pdf'},error:null})
const job=await exportService.create('# unsaved', 'note','pdf',{theme_id:'dark',include_title:true,page_size:'A4'},undefined,'folder/note.md')
expect(apiClient.post).toHaveBeenCalledWith('/api/exports',expect.objectContaining({source:{type:'markdown',markdown:'# unsaved',file_path:'folder/note.md'},format:'pdf'}))
expect(job.warnings).toEqual(['print palette']);expect(job.fileName).toBe('note.pdf')
})
it('aborted preparation does not create a backend job',async()=>{
vi.mocked(apiClient.post).mockClear()
const abort=new AbortController();abort.abort()
await expect(exportService.create('snapshot','note','html',{theme_id:'light',include_title:true,page_size:'A4'},abort.signal)).rejects.toThrow()
expect(apiClient.post).not.toHaveBeenCalled()
})
})
const reviewOptions={theme_id:'light',include_title:true,page_size:'A4'}
const queued={job_id:'review-job',status:'queued',warnings:[],file:null,error:null}
afterEach(()=>{vi.restoreAllMocks();vi.unstubAllGlobals();vi.clearAllMocks()})
it('cancels a created server job after an in-flight submission is aborted',async()=>{
let finish!:(value:unknown)=>void
vi.mocked(apiClient.post).mockImplementationOnce(()=>new Promise(resolve=>{finish=resolve}) as never).mockResolvedValue({status:'completed'})
vi.mocked(apiClient.get).mockResolvedValue({...queued,status:'cancelled'})
const controller=new AbortController()
const pending=exportService.create('# snapshot','review','html',reviewOptions,controller.signal)
controller.abort();finish(queued)
await expect(pending).rejects.toMatchObject({name:'AbortError'})
expect(apiClient.post).toHaveBeenLastCalledWith('/api/exports/review-job/cancel')
})
it('does not report cancellation when the server job already completed',async()=>{
let finish!:(value:unknown)=>void
vi.mocked(apiClient.post).mockImplementationOnce(()=>new Promise(resolve=>{finish=resolve}) as never).mockResolvedValue({status:'completed'})
vi.mocked(apiClient.get).mockResolvedValue({...queued,status:'completed'})
const controller=new AbortController()
const pending=exportService.create('# snapshot','review','html',reviewOptions,controller.signal)
controller.abort();finish(queued)
await expect(pending).rejects.toThrow('导出已完成,无法取消')
})
it('surfaces a server cancellation failure instead of claiming it was cancelled',async()=>{
let finish!:(value:unknown)=>void
vi.mocked(apiClient.post).mockImplementationOnce(()=>new Promise(resolve=>{finish=resolve}) as never).mockRejectedValue(new Error('network failure'))
const controller=new AbortController()
const pending=exportService.create('# snapshot','review','html',reviewOptions,controller.signal)
controller.abort();finish(queued)
await expect(pending).rejects.toThrow('network failure')
})
it.each(['mermaid','Mermaid','mermaid title="Flow"'])('prepares a static asset for %s',async language=>{
vi.stubGlobal('crypto',webcrypto)
vi.stubGlobal('Image',class {src='';decode(){return Promise.resolve()}})
vi.spyOn(HTMLCanvasElement.prototype,'getContext').mockReturnValue({fillStyle:'',fillRect:vi.fn(),drawImage:vi.fn()} as never)
vi.spyOn(HTMLCanvasElement.prototype,'toDataURL').mockReturnValue('data:image/png;base64,YWJj')
vi.mocked(renderMermaid).mockResolvedValue({svg:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 10"></svg>',warnings:[]} as never)
vi.mocked(apiClient.post).mockResolvedValue(queued)
await exportService.create('```'+language+'\nflowchart LR\n A-->B\n```','review','html',reviewOptions)
expect(renderMermaid).toHaveBeenCalledWith('flowchart LR\n A-->B',{mode:'raster',theme:'light'})
expect(apiClient.post).toHaveBeenCalledWith('/api/exports',expect.objectContaining({assets:[expect.objectContaining({kind:'mermaid',png_base64:'YWJj',source_hash:expect.stringMatching(/^[a-f0-9]{64}$/)})]}))
})
it('PDF submits the shared browser snapshot instead of raster assets',async()=>{
vi.mocked(apiClient.post).mockResolvedValue(queued)
const markdown=Array.from({length:17},(_,i)=>'```mermaid\nflowchart LR\n A'+i+'-->B\n```').join('\n\n')
await exportService.create(markdown,'many','pdf',{...reviewOptions,theme_id:'dark'})
expect(preparePdfSnapshot).toHaveBeenCalledWith(markdown,'many',expect.objectContaining({theme_id:'dark'}),undefined,undefined)
expect(renderMermaid).not.toHaveBeenCalled()
expect(apiClient.post).toHaveBeenCalledWith('/api/exports',expect.objectContaining({assets:[],print_html:'<html>theme snapshot</html>'}))
})
it('captures custom theme CSS as a portable palette',()=>{
const values={'background-primary':'#010409','surface-primary':'rgb(22, 27, 34)','text-primary':'#e6edf3','text-secondary':'#b1bac4','background-secondary':'#21262d','border-default':'#57606a','accent-primary':'#79c0ff'}
for(const [key,value] of Object.entries(values))document.documentElement.style.setProperty('--color-'+key,value)
try { expect(captureExportPalette()).toMatchObject({surface:'#161b22',text:'#e6edf3',accent:'#79c0ff'}) }
finally { for(const key of Object.keys(values))document.documentElement.style.removeProperty('--color-'+key) }
})
+110
View File
@@ -0,0 +1,110 @@
import { apiClient } from './apiClient'
import { Marked } from 'marked'
import { renderMermaid } from './mermaidService'
export type ExportFormat = 'html' | 'pdf' | 'docx'
interface JobWire {
job_id: string; status: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
warnings: string[]; error: string | null
file: { file_name: string; size: number } | null
}
export interface ExportJob { id: string; status: JobWire['status']; warnings: string[]; error: string | null; fileName?: string }
const mapJob = (w: JobWire): ExportJob => ({ id: w.job_id, status: w.status, warnings: w.warnings, error: w.error, fileName: w.file?.file_name })
export type ExportPalette = Record<'page' | 'surface' | 'text' | 'muted' | 'code' | 'border' | 'accent', string>
// 冻结导出开始时的主题颜色,避免后台兼容渲染受到后续主题切换影响。
export function captureExportPalette(): ExportPalette | undefined {
const style = getComputedStyle(document.documentElement)
const tokens = { page:'background-primary', surface:'surface-primary', text:'text-primary', muted:'text-secondary', code:'background-secondary', border:'border-default', accent:'accent-primary' }
const entries = Object.entries(tokens).map(([key, token]) => {
const value = style.getPropertyValue(`--color-${token}`).trim()
if (/^#[0-9a-f]{6}$/i.test(value)) return [key,value]
if (/^#[0-9a-f]{3}$/i.test(value)) return [key, '#' + [...value.slice(1)].map(c => c+c).join('')]
const rgb = value.match(/^rgb\(\s*(\d+)[, ]+\s*(\d+)[, ]+\s*(\d+)\s*\)$/)
if (rgb) return [key, '#' + rgb.slice(1,4).map(v => Number(v).toString(16).padStart(2,'0')).join('')]
// 借助浏览器解析命名色、color-mix、OKLCH 和透明色,再冻结为可移植 RGB 色板。
if (typeof CSS !== 'undefined' && CSS.supports('color', value)) {
const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1
const context = canvas.getContext('2d')
if (context) {
context.fillStyle = '#ffffff'; context.fillRect(0,0,1,1)
context.fillStyle = value; context.fillRect(0,0,1,1)
const pixel = context.getImageData(0,0,1,1).data
return [key, '#' + [...pixel.slice(0,3)].map(v => v.toString(16).padStart(2,'0')).join('')]
}
}
return [key, '']
})
return entries.every(([,value]) => value) ? Object.fromEntries(entries) as ExportPalette : undefined
}
export async function rasterize(svg: string, signal?: AbortSignal, unlimited = false, background = '#ffffff'): Promise<string> {
// 非 PDF 格式保留像素预算和解码超时;PDF 的自包含快照解除资源配额。
const doc = new DOMParser().parseFromString(svg, 'image/svg+xml')
const root = doc.documentElement
const box = root.getAttribute('viewBox')?.split(/[ ,]+/).map(Number)
const width = box?.[2] || 800, height = box?.[3] || 600
if (!Number.isFinite(width + height) || width <= 0 || height <= 0) throw new Error('图表尺寸无效')
const scale = Math.min(4, Math.max(2, 1200 / width), unlimited ? Infinity : Math.sqrt(4_000_000 / (width * height)))
root.setAttribute('width', String(Math.floor(width * scale))); root.setAttribute('height', String(Math.floor(height * scale)))
root.style.maxWidth = 'none'
const data = new XMLSerializer().serializeToString(root)
const image = new Image()
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(data)}`
await new Promise<void>((resolve, reject) => {
const abort = () => reject(new DOMException('Aborted', 'AbortError'))
const timer = unlimited ? undefined : setTimeout(() => reject(new Error('图表图片解码超时')), 15000)
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener('abort', abort) }
if (signal?.aborted) { cleanup(); abort(); return }
signal?.addEventListener('abort', abort, { once: true })
image.decode().then(resolve, reject).finally(cleanup)
})
const canvas = document.createElement('canvas')
canvas.width = Math.floor(width * scale); canvas.height = Math.floor(height * scale)
const context = canvas.getContext('2d')!
context.fillStyle = background; context.fillRect(0, 0, canvas.width, canvas.height)
context.drawImage(image, 0, 0, canvas.width, canvas.height)
return canvas.toDataURL('image/png').split(',')[1]!
}
export async function hashSource(source: string) {
return [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(source.trim())))].map(v => v.toString(16).padStart(2, '0')).join('')
}
export const exportService = {
async create(markdown: string, title: string, format: ExportFormat, options: { theme_id: string; include_title: boolean; page_size: string; palette?: ExportPalette }, signal?: AbortSignal, filePath?: string) {
let printHtml: string | undefined
if (format === 'pdf') {
const { preparePdfSnapshot } = await import('./pdfSnapshotService')
printHtml = await preparePdfSnapshot(markdown,title,options,signal,filePath)
}
const blocks: string[] = []
const parser = new Marked()
if (format !== 'pdf') parser.walkTokens(parser.lexer(markdown), token => { if (token.type === 'code' && token.lang?.trim().split(/\s+/)[0]?.toLowerCase() === 'mermaid') blocks.push(token.text) })
const assets = []
for (const source of [...new Set(blocks)]) {
signal?.throwIfAborted()
if (format !== 'pdf' && assets.length >= 16) throw new Error('每次导出最多 16 个 Mermaid 图表')
const pdf = format === 'pdf'
const result = await renderMermaid(source, pdf ? { mode: 'raster', theme: ['dark','midnight-purple'].includes(options.theme_id) ? 'dark' : 'light', palette: options.palette, unlimited: true } : { mode: 'raster', theme: 'light' })
if (result.warnings.length) throw new Error(`Mermaid 无法导出:${result.warnings.join('; ')}`)
assets.push({ kind: 'mermaid', source_hash: await hashSource(source), png_base64: await rasterize(result.svg, signal, pdf, pdf ? options.palette?.surface ?? (['dark','midnight-purple'].includes(options.theme_id) ? '#161b22' : '#ffffff') : '#ffffff') })
}
signal?.throwIfAborted()
// 提交期间收到取消时仍等待服务器返回任务句柄;只中断 HTTP 会遗留无法追踪的后台任务。
const job = mapJob(await apiClient.post<JobWire>('/api/exports', { source: { type: 'markdown', markdown, file_path: filePath }, title, format, options, assets, ...(printHtml ? { print_html:printHtml } : {}) }))
if (signal?.aborted) {
await apiClient.post(`/api/exports/${encodeURIComponent(job.id)}/cancel`)
const current = await apiClient.get<JobWire>(`/api/exports/${encodeURIComponent(job.id)}`)
if (current.status === 'completed') throw new Error('导出已完成,无法取消;请在任务列表中下载。')
if (current.status === 'failed') throw new Error(current.error || '导出任务已失败,请查看任务列表。')
signal.throwIfAborted()
}
return job
},
async get(id: string) { return mapJob(await apiClient.get<JobWire>(`/api/exports/${encodeURIComponent(id)}`)) },
async list() { const response = await apiClient.get<{ items: JobWire[] }>('/api/exports'); return response.items.map(mapJob) },
cancel(id: string) { return apiClient.post(`/api/exports/${encodeURIComponent(id)}/cancel`) },
async download(job: ExportJob) {
const response = await apiClient.get<Response>(`/api/exports/${encodeURIComponent(job.id)}/file`)
const url = URL.createObjectURL(await response.blob())
const link = document.createElement('a'); link.href = url; link.download = job.fileName ?? 'export'
link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000)
},
}
@@ -0,0 +1,22 @@
// @vitest-environment jsdom
import { describe,it,expect,vi,beforeEach } from 'vitest'
vi.mock('./apiClient',()=>({apiClient:{post:vi.fn()}}))
import { apiClient } from './apiClient'
import { renderFunctionPlot } from './functionPlotService'
describe('function plot preview boundary',()=>{
beforeEach(()=>vi.clearAllMocks())
it('shares requests only for the same source and theme, sanitizes SVG',async()=>{
vi.mocked(apiClient.post).mockResolvedValue({result:{content:'<svg onload="alert(1)"><script>alert(2)</script><path d="M0 0L1 1"/></svg>',warnings:[]},diagnostics:[],node_count:3})
const a=await renderFunctionPlot('y = x + 100','dark')
await renderFunctionPlot('y = x + 100','dark')
await renderFunctionPlot('y = x + 100','light')
expect(apiClient.post).toHaveBeenCalledTimes(2)
expect(a.svg).not.toMatch(/onload|script|alert/)
})
it('does not cache network failures',async()=>{
vi.mocked(apiClient.post).mockRejectedValueOnce(new Error('offline')).mockResolvedValueOnce({result:null,diagnostics:[{message:'bad expression',line:2}],node_count:0})
await expect(renderFunctionPlot('bad expression')).rejects.toThrow('offline')
const result=await renderFunctionPlot('bad expression')
expect(result.svg).toBe('');expect(result.warnings[0]).toContain('2')
})
})
@@ -0,0 +1,23 @@
import { apiClient } from './apiClient'
import DOMPurify from 'dompurify'
interface PlotWire {
result: { content: string; width: number; height: number; warnings: string[] } | null
diagnostics: { message: string; severity: string; line?: number }[]
node_count: number
}
const cache = new Map<string, Promise<{ svg: string; warnings: string[]; nodeCount: number }>>()
export function renderFunctionPlot(source: string, themeId = 'light') {
// 缓存 Promise 既合并并发的相同请求,也避免重复渲染;失败结果立即移除以允许重试。
const key = JSON.stringify([source, themeId])
if (cache.has(key)) return cache.get(key)!
const result = apiClient.post<PlotWire>('/api/plots/function', { source, theme_id: themeId }, { timeoutMs: 30000 }).then(wire => ({
// 后端只生成静态 SVG,前端仍在渲染边界执行净化,防止未来响应扩展引入可执行标记。
svg: DOMPurify.sanitize(wire.result?.content ?? '', { USE_PROFILES: { svg: true } }),
warnings: [...wire.diagnostics.map(d => `${d.message}${d.line ? ` (行 ${d.line})` : ''}`), ...(wire.result?.warnings ?? [])],
nodeCount: wire.node_count,
})).catch(error => { cache.delete(key); throw error })
if (cache.size >= 32) cache.delete(cache.keys().next().value!)
cache.set(key, result)
return result
}
+22 -9
View File
@@ -8,8 +8,8 @@ function loadMermaid() {
import { computed } from 'vue'
import { useThemeStore } from '@/stores/theme'
export function mermaidThemeVariables(dark: boolean) {
const style = typeof document === 'undefined' ? null : getComputedStyle(document.documentElement)
export function mermaidThemeVariables(dark: boolean, useDocument = true) {
const style = !useDocument || typeof document === 'undefined' ? null : getComputedStyle(document.documentElement)
const color = (name: string, fallback: string) => style?.getPropertyValue(`--color-${name}`).trim() || fallback
const text = color('text-primary', dark ? '#e6edf3' : '#1f2328')
const border = color('border-default', dark ? '#484f58' : '#d0d7de')
@@ -29,15 +29,28 @@ export function mermaidThemeVariables(dark: boolean) {
}
}
async function ensureInitialized(theme: 'light' | 'dark') {
async function ensureInitialized(theme: 'light' | 'dark', raster = false, palette?: Record<string,string>, unlimited = false, frozenVariables?: ReturnType<typeof mermaidThemeVariables>) {
const mermaid = await loadMermaid()
const dark = palette ? [1,3,5].reduce((sum,index,i) => sum + parseInt(palette.surface!.slice(index,index+2),16) * [0.2126,0.7152,0.0722][i]!,0) < 128 : theme === 'dark'
mermaid.initialize({
startOnLoad: false,
theme: 'base',
themeVariables: mermaidThemeVariables(theme === 'dark'),
themeVariables: frozenVariables ?? (palette ? {
...mermaidThemeVariables(dark, false), background: palette.surface,
primaryColor: palette.code, primaryTextColor: palette.text, primaryBorderColor: palette.border,
secondaryColor: palette.code, secondaryTextColor: palette.text, secondaryBorderColor: palette.border,
tertiaryColor: palette.code, tertiaryTextColor: palette.text, tertiaryBorderColor: palette.border,
textColor: palette.text, lineColor: palette.muted, mainBkg: palette.code, nodeBorder: palette.border,
clusterBkg: palette.surface, clusterBorder: palette.border, edgeLabelBackground: palette.surface,
actorBkg: palette.code, actorBorder: palette.border, actorTextColor: palette.text, actorLineColor: palette.muted,
signalColor: palette.muted, signalTextColor: palette.text, labelBoxBkgColor: palette.surface,
labelBoxBorderColor: palette.border, labelTextColor: palette.text, noteBkgColor: palette.code,
noteTextColor: palette.text, noteBorderColor: palette.border, activationBkgColor: palette.code, activationBorderColor: palette.border,
} : mermaidThemeVariables(theme === 'dark', !raster)),
...(unlimited ? { maxTextSize: Number.MAX_SAFE_INTEGER, maxEdges: Number.MAX_SAFE_INTEGER } : {}),
securityLevel: 'strict',
fontFamily: 'var(--font-ui-sans)',
flowchart: { useMaxWidth: true, htmlLabels: true },
fontFamily: raster ? 'Arial, Microsoft YaHei, sans-serif' : 'var(--font-ui-sans)',
flowchart: { useMaxWidth: true, htmlLabels: !raster },
sequence: { useMaxWidth: true },
gantt: { useMaxWidth: true },
})
@@ -65,18 +78,18 @@ export interface MermaidParseError {
let renderCounter = 0
export function renderMermaid(source: string, options: { theme?: 'light' | 'dark'; mode?: 'interactive' | 'static' } = {}): Promise<MermaidRenderResult> {
export function renderMermaid(source: string, options: { theme?: 'light' | 'dark'; mode?: 'interactive' | 'static' | 'raster'; palette?: Record<string,string>; unlimited?: boolean; themeVariables?: ReturnType<typeof mermaidThemeVariables> } = {}): Promise<MermaidRenderResult> {
return serialized(() => renderMermaidNow(source, options))
}
async function renderMermaidNow(
source: string,
options: { theme?: 'light' | 'dark'; mode?: 'interactive' | 'static' } = {}
options: { theme?: 'light' | 'dark'; mode?: 'interactive' | 'static' | 'raster'; palette?: Record<string,string>; unlimited?: boolean; themeVariables?: ReturnType<typeof mermaidThemeVariables> } = {}
): Promise<MermaidRenderResult> {
const theme = options.theme ?? 'light'
const id = `mermaid-${Date.now()}-${++renderCounter}`
try {
const mermaid = await ensureInitialized(theme)
const mermaid = await ensureInitialized(theme, options.mode === 'raster', options.palette, options.unlimited, options.themeVariables)
const result = await mermaid.render(id, source)
const parser = new DOMParser()
const doc = parser.parseFromString(result.svg, 'image/svg+xml')
@@ -0,0 +1,76 @@
// @vitest-environment jsdom
import {it,expect,vi,afterEach} from 'vitest'
vi.mock('@/utils/markdown',()=>({renderMarkdown:vi.fn().mockResolvedValue('<h1>Heading</h1><details><summary>Tip</summary><p>Body</p></details><div class="markdown-code-toolbar"><button>Copy</button><span>python</span></div>')}))
vi.mock('./apiClient',()=>({apiClient:{post:vi.fn().mockResolvedValue({images:[],plots:[]})}}))
vi.mock('./mermaidService',()=>({mermaidThemeVariables:()=>({primaryColor:'#fff'})}))
vi.mock('@/stores/theme',()=>({useThemeStore:()=>({isDark:false})}))
vi.mock('@/stores/markdownPreferences',()=>({useMarkdownPreferencesStore:()=>({normalized:{wrapCode:true,lineNumbers:true,indent:4}})}))
vi.mock('@/stores/headingAppearance',()=>({useHeadingAppearanceStore:()=>({cssVariables:{'--heading-1-size':'37px'},preferences:{custom:true}})}))
vi.mock('@/components/common/MarkdownContent.vue',()=>({default:{}}))
vi.mock('@/features/editor/VisualMarkdownEditor.vue',()=>({default:{__scopeId:'data-v-editor'}}))
import {preparePdfSnapshot} from './pdfSnapshotService'
import {apiClient} from './apiClient'
import {renderMarkdown} from '@/utils/markdown'
afterEach(()=>vi.clearAllMocks())
it('preserves actual theme CSS, pseudo elements, root attributes and heading preferences',async()=>{
const style=document.createElement('style');style.textContent='[data-theme="paper"] .ProseMirror::before { content:"tape"; transform:rotate(-3deg) }';document.head.append(style)
document.documentElement.dataset.theme='paper'
try {
const html=await preparePdfSnapshot('# Heading','<Title>',{theme_id:'paper',include_title:true,page_size:'A4'})
expect(html).toContain('transform: rotate(-3deg)')
expect(html).toContain('data-theme="paper"')
expect(html).toContain('data-v-editor')
expect(html).toContain('data-heading-style="custom"')
expect(html).toContain('--heading-1-size:37px')
expect(html).toContain('&lt;Title&gt;')
expect(html).toContain('<details open="">')
expect(html).not.toContain('<button>Copy')
expect(html).toContain('<span>python</span>')
expect(renderMarkdown).toHaveBeenCalledWith('# Heading',expect.objectContaining({pdf:expect.anything()}))
} finally {style.remove();delete document.documentElement.dataset.theme}
})
it('rejects a missing image instead of silently producing an incomplete PDF',async()=>{
vi.mocked(renderMarkdown).mockResolvedValueOnce('<img src="missing.png">')
await expect(preparePdfSnapshot('![image](missing.png)','note',{theme_id:'light',include_title:false,page_size:'A4'})).rejects.toThrow('PDF 图片无法读取')
})
it('an aborted snapshot never requests backend resources',async()=>{
const controller=new AbortController();controller.abort()
await expect(preparePdfSnapshot('text','note',{theme_id:'light',include_title:false,page_size:'A4'},controller.signal)).rejects.toMatchObject({name:'AbortError'})
expect(apiClient.post).not.toHaveBeenCalled()
})
it('renders snapshot metadata with editor theme scopes and excludes YAML from the body',async()=>{
const markdown='---\ntitle: "<Current & title>"\ntags: ["<tag>", "未保存标签"]\n---\n# Body'
const html=await preparePdfSnapshot(markdown,'filename',{theme_id:'light',include_title:false,page_size:'A4'})
const doc=new DOMParser().parseFromString(html,'text/html')
const metadata=doc.querySelector('.milkdown-host > .note-metadata')!
expect(metadata.querySelector('h1')?.textContent).toBe('<Current & title>')
expect([...metadata.querySelectorAll('.metadata-tag')].map(el=>el.textContent)).toEqual(['<tag>','未保存标签'])
expect([...metadata.querySelectorAll('*')].every(el=>el.hasAttribute('data-v-editor'))).toBe(true)
expect(metadata.querySelector('button,input,form')).toBeNull()
expect(renderMarkdown).toHaveBeenCalledWith('# Body',expect.anything())
expect(apiClient.post).toHaveBeenCalledWith('/api/exports/preview-resources',expect.objectContaining({source:expect.objectContaining({markdown:'# Body'})}))
})
it('does not invent a metadata bar for plain notes or unsupported frontmatter',async()=>{
for(const source of ['plain text','---\ntitle: [invalid]\n---\nbody']) {
const html=await preparePdfSnapshot(source,'filename',{theme_id:'light',include_title:true,page_size:'A4'})
expect(html).not.toContain('<section class="note-metadata"')
expect(renderMarkdown).toHaveBeenCalledWith(source,expect.anything())
}
})
it('exports metadata-only notes without submitting an empty resource request',async()=>{
for(const tail of ['', '\n \n']) {
const html=await preparePdfSnapshot('---\ntitle: Metadata only\ntags: [draft]\n---\n'+tail,'note',{theme_id:'light',include_title:false,page_size:'A4'})
const doc=new DOMParser().parseFromString(html,'text/html')
expect(doc.querySelector('.note-metadata h1')?.textContent).toBe('Metadata only')
expect(doc.querySelector('.metadata-tag')?.textContent).toBe('draft')
}
expect(apiClient.post).not.toHaveBeenCalled()
})
it('embeds prepared HTML images without retaining local URLs',async()=>{
vi.mocked(renderMarkdown).mockResolvedValueOnce('<p><img src="assets/a&amp;b.png"></p>')
vi.mocked(apiClient.post).mockResolvedValueOnce({images:[{source:'assets/a&b.png',data:'data:image/png;base64,aGVsbG8=',warnings:[]}],plots:[]})
const html=await preparePdfSnapshot('<img src="assets/a&amp;b.png">','note',{theme_id:'light',include_title:false,page_size:'A4'})
expect(new DOMParser().parseFromString(html,'text/html').querySelector('img')?.getAttribute('src')).toBe('data:image/png;base64,aGVsbG8=')
})
+114
View File
@@ -0,0 +1,114 @@
import { apiClient } from './apiClient'
import { renderMarkdown } from '@/utils/markdown'
import { splitNoteMetadata } from '@/utils/noteMetadata'
import { t } from '@/i18n'
import { mermaidThemeVariables } from './mermaidService'
import { useThemeStore } from '@/stores/theme'
import { useMarkdownPreferencesStore } from '@/stores/markdownPreferences'
import { useHeadingAppearanceStore } from '@/stores/headingAppearance'
// 仅加载编辑器及 Markdown 组件的样式,包括 Vue scoped 规则,不额外挂载编辑器实例。
import MarkdownContent from '@/components/common/MarkdownContent.vue'
import VisualMarkdownEditor from '@/features/editor/VisualMarkdownEditor.vue'
void MarkdownContent; void VisualMarkdownEditor
interface Resources { images: {source:string; data:string|null; warnings:string[]}[]; plots: {source:string; svg:string; warnings:string[]}[] }
interface Options { theme_id:string; include_title:boolean; page_size:string }
const printRules = `
@page { margin: 0; }
html, body { margin:0 !important; padding:0 !important; width:auto !important; height:auto !important; min-height:0 !important; overflow:visible !important; display:block !important; }
* { -webkit-print-color-adjust:exact !important; print-color-adjust:exact !important; animation:none !important; transition:none !important; }
.pdf-document, .pdf-document .milkdown-host, .pdf-document .milkdown { display:block !important; height:auto !important; min-height:0 !important; overflow:visible !important; }
.pdf-document .ProseMirror { min-height:0 !important; overflow:visible !important; box-decoration-break:clone; -webkit-box-decoration-break:clone; }
.pdf-document :is(h1,h2,h3,h4,h5,h6) { break-after:avoid; }
.pdf-document img { max-width:100%; }
.pdf-document .markdown-mermaid > svg { width:100% !important; min-width:0 !important; max-width:100% !important; height:auto !important; max-height:250mm; }
.pdf-document :is(.markdown-mermaid,.markdown-math,table) { break-inside:avoid; }
.pdf-document :is(pre,.shiki) { overflow:visible !important; white-space:pre-wrap; overflow-wrap:anywhere; }
.pdf-document .markdown-code-toolbar button, .pdf-document .diagram-controls { display:none !important; }
`
function attrs(element: Element): string {
return [...element.attributes].filter(a => a.name==='class' || a.name==='style' || a.name.startsWith('data-')).map(a=>` ${a.name}="${escape(a.value)}"`).join('')
}
function escape(text: string) { return text.replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;') }
function scopeAttributes(component: unknown) { const id=(component as {__scopeId?:string}).__scopeId; return id ? ` ${id}` : '' }
async function dataUrl(url: string, signal?:AbortSignal):Promise<string> {
const response=await fetch(url,{signal}); if(!response.ok) throw Error(`PDF 资源读取失败:${url}`)
const blob=await response.blob()
return await new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=()=>resolve(String(reader.result));reader.onerror=reject;reader.readAsDataURL(blob)})
}
async function embedCss(css: string, base: string, signal?:AbortSignal) {
// 打印进程完全离线,主题资源必须来自应用同源地址并在此转换为 data URL。
const matches=[...css.matchAll(/url\(\s*(['"]?)(.*?)\1\s*\)/g)]
for(const match of matches) {
const url=match[2]!
if(url.startsWith('data:')||url.startsWith('#'))continue
const absolute=new URL(url,base)
if(absolute.origin!==location.origin)throw Error(`PDF 主题资源必须来自应用:${absolute.href}`)
css=css.replace(match[0],`url("${await dataUrl(absolute.href,signal)}")`)
}
return css
}
function stylesheetSnapshot(): {css:string;base:string}[] {
const sheets: {css:string;base:string}[]=[]
function visit(sheet:CSSStyleSheet) {
for(const rule of [...sheet.cssRules]) {
if(rule instanceof CSSImportRule && rule.styleSheet)visit(rule.styleSheet)
else sheets.push({css:rule.cssText,base:sheet.href || document.baseURI})
}
}
for(const sheet of [...document.styleSheets])visit(sheet)
return sheets
}
export async function preparePdfSnapshot(markdown:string,title:string,options:Options,signal?:AbortSignal,filePath?:string):Promise<string> {
// 在任何异步资源请求前冻结主题、排版和编辑器内容,保证产物对应点击导出时的状态。
signal?.throwIfAborted()
const theme=useThemeStore(), preferences={...useMarkdownPreferencesStore().normalized}, heading=useHeadingAppearanceStore()
if(theme.currentThemeId && theme.currentThemeId!==options.theme_id)throw Error('主题在导出准备期间发生变化,请重新导出。')
const htmlAttrs=attrs(document.documentElement), bodyAttrs=attrs(document.body)
const variables=getComputedStyle(document.documentElement)
const rootVariables=[...variables].filter(name=>name.startsWith('--')).map(name=>`${name}:${variables.getPropertyValue(name)};`).join('')
const styles=stylesheetSnapshot()
const diagramVariables=mermaidThemeVariables(theme.isDark)
const headingStyle=Object.entries(heading.cssVariables).map(([key,value])=>`${key}:${value}`).join(';')
const customHeading=heading.preferences.custom
const dark=theme.isDark
const metadata=splitNoteMetadata(markdown)
const body=metadata?.body ?? markdown
const scope=scopeAttributes(VisualMarkdownEditor)
// 复用编辑器 DOM 与 scoped 样式;元数据只输出展示内容,不携带编辑控件。
const metadataHtml=metadata ? `<section class="note-metadata"${scope} aria-label="${escape(t('笔记属性','Note properties'))}"><span class="metadata-caption"${scope}>${escape(t('笔记属性','Note properties'))}</span>${metadata.title ? `<h1${scope}>${escape(metadata.title)}</h1>` : ''}<div class="metadata-tags"${scope}><span class="metadata-label"${scope}>${escape(t('标签','Tags'))}</span>${metadata.tags.map(tag=>`<span class="metadata-tag"${scope}><span${scope}>${escape(tag)}</span></span>`).join('')}</div></section>` : ''
// 仅含元数据的笔记没有正文资源,跳过请求可避免空 Markdown 触发接口的 422 校验。
const resources:Resources=body.trim() ? await apiClient.post<Resources>('/api/exports/preview-resources',{format:'pdf',source:{type:'markdown',markdown:body,file_path:filePath},options}) : {images:[],plots:[]}
signal?.throwIfAborted()
const rendered=await renderMarkdown(body,{themeId:options.theme_id,theme:dark?'dark':'light',preferences,pdf:{mermaidVariables:diagramVariables,plot:async source=>{
const plot=resources.plots.find(p=>p.source.trim()===source.trim()); if(!plot?.svg)throw Error(plot?.warnings.join('; ')||'函数图像无法导出');return plot
}}})
const fragment=new DOMParser().parseFromString(rendered,'text/html')
for(const image of fragment.querySelectorAll('img')) {
const source=image.getAttribute('src')||''
if(source.startsWith('data:'))continue
const resource=resources.images.find(item=>item.source===source)
if(!resource?.data)throw Error(resource?.warnings.join('; ')||`PDF 图片无法读取:${source}`)
image.src=resource.data
}
// 打印全部警告框内容,只移除交互控件,保留主题装饰。
fragment.querySelectorAll('details').forEach(d=>d.open=true)
// 工作区使用 blockquote 表示警告框;保持相同 DOM 契约,让间距和装饰选择器继续生效。
fragment.querySelectorAll('.markdown-callout:not(blockquote)').forEach(details=>{
const block=fragment.createElement('blockquote')
for(const attribute of [...details.attributes])if(attribute.name!=='open')block.setAttribute(attribute.name,attribute.value)
block.innerHTML=details.innerHTML
const summary=block.querySelector('summary')
if(summary){const title=fragment.createElement('div');title.className=summary.className;title.innerHTML=summary.innerHTML;summary.replaceWith(title)}
details.replaceWith(block)
})
const error=fragment.querySelector('.mermaid-error')
if(error)throw Error(error.textContent||'PDF 图表渲染失败')
fragment.querySelectorAll('.markdown-code-toolbar button,.diagram-controls').forEach(e=>e.remove())
const css=(await Promise.all(styles.map(s=>embedCss(s.css,s.base,signal)))).join('\n')
signal?.throwIfAborted()
return `<!doctype html><html${htmlAttrs}><head><meta charset="utf-8"><title>${escape(title)}</title><style>${css.replace(/<\/style/gi,'<\\/style')}\n:root{${rootVariables}}\n${printRules}</style></head><body${bodyAttrs}><div class="visual-editor pdf-document"${scope} ${customHeading?'data-heading-style="custom"':''} style="${escape(headingStyle)}"><div class="milkdown-host"${scope}>${metadataHtml}<div class="milkdown"><article class="ProseMirror markdown-content" data-code-wrap="${preferences.wrapCode}" data-line-numbers="${preferences.lineNumbers}" style="--markdown-code-indent:${preferences.indent}">${options.include_title?`<h1>${escape(title)}</h1>`:''}${fragment.body.innerHTML}</article></div></div></div></body></html>`
}
+38
View File
@@ -375,3 +375,41 @@ ol {
--color-border-focus: #8a5b32;
--color-border-disabled: #eadfc4;
}
/* 函数图颜色继承所有已安装主题,也允许自定义主题包覆盖这些 Token。 */
:root {
--color-plot-background: var(--color-surface-primary);
--color-plot-text: var(--color-text-primary);
--color-plot-axis: var(--color-text-secondary);
--color-plot-grid: var(--color-border-default);
--color-plot-curve-0: #0969da;
--color-plot-curve-1: #d1242f;
--color-plot-curve-2: #1a7f37;
--color-plot-curve-3: #8250df;
--color-plot-curve-4: #9a6700;
--color-plot-curve-5: #bc4c00;
}
[data-theme='dark'], [data-theme='midnight-purple'] {
--color-plot-curve-0: #79c0ff;
--color-plot-curve-1: #ff9b9b;
--color-plot-curve-2: #7ee787;
--color-plot-curve-3: #d2a8ff;
--color-plot-curve-4: #f2cc60;
--color-plot-curve-5: #ffa657;
}
.function-plot-svg > rect:first-child { fill: var(--color-plot-background); }
.function-plot-svg .plot-grid { stroke: var(--color-plot-grid); }
.function-plot-svg .plot-axis { stroke: var(--color-plot-axis); }
.function-plot-svg text { fill: var(--color-plot-text); }
.function-plot-svg .plot-curve-0 { stroke: var(--color-plot-curve-0); }
.function-plot-svg .plot-legend-0 { fill: var(--color-plot-curve-0); }
.function-plot-svg .plot-curve-1 { stroke: var(--color-plot-curve-1); }
.function-plot-svg .plot-legend-1 { fill: var(--color-plot-curve-1); }
.function-plot-svg .plot-curve-2 { stroke: var(--color-plot-curve-2); }
.function-plot-svg .plot-legend-2 { fill: var(--color-plot-curve-2); }
.function-plot-svg .plot-curve-3 { stroke: var(--color-plot-curve-3); }
.function-plot-svg .plot-legend-3 { fill: var(--color-plot-curve-3); }
.function-plot-svg .plot-curve-4 { stroke: var(--color-plot-curve-4); }
.function-plot-svg .plot-legend-4 { fill: var(--color-plot-curve-4); }
.function-plot-svg .plot-curve-5 { stroke: var(--color-plot-curve-5); }
.function-plot-svg .plot-legend-5 { fill: var(--color-plot-curve-5); }
+20 -12
View File
@@ -1,3 +1,4 @@
import { renderFunctionPlot } from '@/services/functionPlotService'
import DOMPurify from 'dompurify'
import { Marked } from 'marked'
import { defaultMarkdownPreferences, type MarkdownPreferences } from '@/stores/markdownPreferences'
@@ -6,7 +7,7 @@ import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
import { bundledLanguagesInfo } from 'shiki/langs'
import githubDark from '@shikijs/themes/github-dark'
import githubLight from '@shikijs/themes/github-light'
import { renderMermaid } from '@/services/mermaidService'
import { renderMermaid, type mermaidThemeVariables } from '@/services/mermaidService'
import { appendDiagramControls } from './diagramControls'
import katex from 'katex'
import 'katex/dist/katex.min.css'
@@ -125,7 +126,7 @@ export async function getCodeTokenizer(theme: 'github-light' | 'github-dark', re
}
}
export async function renderMarkdown(source: string, options?: { theme?: 'light' | 'dark'; preferences?: MarkdownPreferences; citationNumbers?: number[]; citationAliases?: Record<string, number> }): Promise<string> {
export async function renderMarkdown(source: string, options?: { themeId?: string; theme?: 'light' | 'dark'; preferences?: MarkdownPreferences; pdf?: { plot: (source: string) => Promise<{svg: string; warnings: string[]}>; mermaidVariables: ReturnType<typeof mermaidThemeVariables> }; citationNumbers?: number[]; citationAliases?: Record<string, number> }): Promise<string> {
const preferences = options?.preferences ?? defaultMarkdownPreferences
const marked = createMarkdownParser(preferences)
const citations = new Set(options?.citationNumbers ?? [])
@@ -141,12 +142,14 @@ export async function renderMarkdown(source: string, options?: { theme?: 'light'
const html = marked.parse(source, { async: false }) as string
const documentNode = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html')
const mermaidBlocks: { pre: Element; source: string }[] = []
const mermaidBlocks: { pre: Element; source: string; kind: string }[] = []
for (const code of documentNode.querySelectorAll('pre > code')) {
const requestedLanguage = [...code.classList].find((name) => name.startsWith('language-'))?.slice(9) || 'text'
if (requestedLanguage === 'mermaid' && preferences.diagrams) {
mermaidBlocks.push({ pre: code.parentElement!, source: code.textContent ?? '' })
// Mermaid 与函数图共用静态图表管线;兼容旧的 function_plot 围栏写法。
const diagramKind = requestedLanguage.toLowerCase().split(/\s+/)[0]!.replace('function_plot','function-plot')
if (['mermaid', 'function-plot'].includes(diagramKind) && preferences.diagrams) {
mermaidBlocks.push({ pre: code.parentElement!, source: code.textContent ?? '', kind: diagramKind })
continue
}
if (requestedLanguage.toLowerCase() === 'latex' && preferences.math) {
@@ -168,19 +171,24 @@ export async function renderMarkdown(source: string, options?: { theme?: 'light'
code.parentElement?.replaceWith(wrapper)
}
for (const { pre, source } of mermaidBlocks) {
let plotCount = 0, plotNodes = 0
for (const { pre, source, kind } of mermaidBlocks) {
try {
const result = await renderMermaid(source, { theme: options?.theme, mode: 'static' })
// 交互预览保持数量和 AST 复杂度预算;PDF 已在隔离渲染链路中按需求解除限制。
if (!options?.pdf && kind === 'function-plot' && ++plotCount > 16) throw new Error('函数图像数量超过 16')
const result = kind === 'function-plot' ? (options?.pdf ? await options.pdf.plot(source) : await renderFunctionPlot(source, options?.themeId)) : await renderMermaid(source, { theme: options?.theme, mode: 'static', ...(options?.pdf ? { unlimited:true, themeVariables:options.pdf.mermaidVariables } : {}) })
if (!options?.pdf && 'nodeCount' in result && (plotNodes += Number(result.nodeCount)) > 8000) throw new Error('函数图像累计复杂度超过 8000')
const container = document.createElement('div')
container.className = 'markdown-mermaid'
container.className = 'markdown-mermaid' + (kind === 'function-plot' ? ' markdown-function-plot' : '')
container.innerHTML = result.svg
appendCodeToolbar(container, 'mermaid', source, true)
if (!result.warnings.length) appendDiagramControls(container)
appendCodeToolbar(container, kind, source, true)
if (result.warnings.length) { const message = document.createElement('p'); message.textContent = result.warnings.join('\n'); message.setAttribute('role', 'status'); container.append(message) }
if (!result.warnings.length || (kind === 'function-plot' && result.svg)) appendDiagramControls(container)
pre.replaceWith(container)
} catch {
} catch (error) {
const fallback = document.createElement('pre')
fallback.className = 'mermaid-error'
fallback.textContent = source
fallback.textContent = `${error instanceof Error ? error.message : '图表渲染失败'}\n${source}`
pre.replaceWith(fallback)
}
}
+14
View File
@@ -15,6 +15,19 @@ const fixtures = {
};
const allThemes = [{theme_id:'light',is_dark:false},{theme_id:'dark',is_dark:true},{theme_id:'sepia',is_dark:false},...mockCommunityThemes];
const requested = new URLSearchParams(location.search).get('theme');
if (!requested) {
const results=[];
for (const theme of allThemes) {
const frame=document.createElement('iframe');
frame.title=theme.theme_id; frame.style.cssText='width:100%;height:900px;border:0';
frame.src='?theme='+encodeURIComponent(theme.theme_id); document.querySelector('#results').append(frame);
await new Promise(resolve=>frame.onload=resolve);
while(frame.contentDocument.documentElement.dataset.complete!=='true') await new Promise(resolve=>setTimeout(resolve,100));
results.push(...JSON.parse(frame.contentDocument.querySelector('#status').textContent).results);
}
document.querySelector('#status').textContent=JSON.stringify({passed:results.filter(r=>r.passed).length,total:results.length,results});
document.documentElement.dataset.complete='true';
} else {
const themes = requested ? allThemes.filter(t=>t.theme_id===requested) : allThemes;
const style = document.createElement('style'); document.head.append(style);
const results = [];
@@ -35,4 +48,5 @@ for (const theme of themes) {
}
document.querySelector('#status').textContent=JSON.stringify({passed:results.filter(r=>r.passed).length,total:results.length,results});
document.documentElement.dataset.complete='true';
}
</script></body></html>

Some files were not shown because too many files have changed in this diff Show More