From 1cbd38a8e07f0a742ff2de7c08403cd28fdc478e Mon Sep 17 00:00:00 2001 From: DXC Date: Mon, 8 Jun 2026 12:12:11 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E4=BB=8E=E7=B4=A2=E5=BC=95=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E8=BF=90=E8=A1=8C=E6=97=B6=E4=BA=A7=E7=89=A9=E3=80=81?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E9=85=8D=E7=BD=AE=E3=80=81=E6=97=A7=E8=84=9A?= =?UTF-8?q?=E6=89=8B=E6=9E=B6=EF=BC=9B=E5=AE=8C=E5=96=84=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 38 + .qwen/settings.json | 25 - .qwen/settings.json.orig | 24 - .../code_replacement_state_audit/SKILL.md | 141 - .qwen/skills/facade_kwargs_defense/SKILL.md | 309 --- .qwen/skills/wq_gui_data_flow/SKILL.md | 206 -- .../wq_gui_external_model_panel/SKILL.md | 294 -- .../skills/wq_gui_frontend_scaffold/SKILL.md | 229 -- 1.py | 4 - data/sub/png/watermask.png | Bin 18802 -> 0 bytes data/sub/waterindex.csv | 46 - data/sub/waterindex.xlsx | Bin 13596 -> 0 bytes data/sub/waterindex1125.csv | 46 - new/app/api/_smoke_test_train.py | 201 -- new/app/api/endpoints.py | 222 -- new/app/api/modeling.py | 847 ------ new/app/core/algorithms/__init__.py | 40 - new/app/core/algorithms/base.py | 85 - new/app/core/algorithms/goodman.py | 123 - new/app/core/algorithms/kutser.py | 211 -- new/app/core/algorithms/registry.py | 135 - new/app/core/task_store.py | 91 - new/app/main.py | 64 - new/frontend/.gitignore | 24 - new/frontend/README.md | 5 - new/frontend/index.html | 13 - new/frontend/package-lock.json | 2412 ----------------- new/frontend/package.json | 27 - new/frontend/public/favicon.svg | 1 - new/frontend/public/icons.svg | 24 - new/frontend/src/App.vue | 225 -- new/frontend/src/api/request.ts | 15 - new/frontend/src/api/tasks.ts | 13 - new/frontend/src/assets/hero.png | Bin 13057 -> 0 bytes new/frontend/src/assets/vite.svg | 1 - new/frontend/src/assets/vue.svg | 1 - new/frontend/src/components/HelloWorld.vue | 95 - new/frontend/src/composables/useTaskPoller.ts | 51 - new/frontend/src/main.ts | 9 - new/frontend/src/style.css | 296 -- new/frontend/tsconfig.app.json | 14 - new/frontend/tsconfig.json | 7 - new/frontend/tsconfig.node.json | 24 - new/frontend/vite.config.ts | 7 - src/gui/crash_dump.txt | 103 - src/gui/scaler_params.pkl | Bin 1783 -> 0 bytes tset.py | 11 - 封装问题分析报告.md | 215 -- 软件说明.md | 2048 -------------- 软件说明2.md | 228 -- 降采样光谱.py | 121 - 51 files changed, 38 insertions(+), 9333 deletions(-) delete mode 100644 .qwen/settings.json delete mode 100644 .qwen/settings.json.orig delete mode 100644 .qwen/skills/code_replacement_state_audit/SKILL.md delete mode 100644 .qwen/skills/facade_kwargs_defense/SKILL.md delete mode 100644 .qwen/skills/wq_gui_data_flow/SKILL.md delete mode 100644 .qwen/skills/wq_gui_external_model_panel/SKILL.md delete mode 100644 .qwen/skills/wq_gui_frontend_scaffold/SKILL.md delete mode 100644 1.py delete mode 100644 data/sub/png/watermask.png delete mode 100644 data/sub/waterindex.csv delete mode 100644 data/sub/waterindex.xlsx delete mode 100644 data/sub/waterindex1125.csv delete mode 100644 new/app/api/_smoke_test_train.py delete mode 100644 new/app/api/endpoints.py delete mode 100644 new/app/api/modeling.py delete mode 100644 new/app/core/algorithms/__init__.py delete mode 100644 new/app/core/algorithms/base.py delete mode 100644 new/app/core/algorithms/goodman.py delete mode 100644 new/app/core/algorithms/kutser.py delete mode 100644 new/app/core/algorithms/registry.py delete mode 100644 new/app/core/task_store.py delete mode 100644 new/app/main.py delete mode 100644 new/frontend/.gitignore delete mode 100644 new/frontend/README.md delete mode 100644 new/frontend/index.html delete mode 100644 new/frontend/package-lock.json delete mode 100644 new/frontend/package.json delete mode 100644 new/frontend/public/favicon.svg delete mode 100644 new/frontend/public/icons.svg delete mode 100644 new/frontend/src/App.vue delete mode 100644 new/frontend/src/api/request.ts delete mode 100644 new/frontend/src/api/tasks.ts delete mode 100644 new/frontend/src/assets/hero.png delete mode 100644 new/frontend/src/assets/vite.svg delete mode 100644 new/frontend/src/assets/vue.svg delete mode 100644 new/frontend/src/components/HelloWorld.vue delete mode 100644 new/frontend/src/composables/useTaskPoller.ts delete mode 100644 new/frontend/src/main.ts delete mode 100644 new/frontend/src/style.css delete mode 100644 new/frontend/tsconfig.app.json delete mode 100644 new/frontend/tsconfig.json delete mode 100644 new/frontend/tsconfig.node.json delete mode 100644 new/frontend/vite.config.ts delete mode 100644 src/gui/crash_dump.txt delete mode 100644 src/gui/scaler_params.pkl delete mode 100644 tset.py delete mode 100644 封装问题分析报告.md delete mode 100644 软件说明.md delete mode 100644 软件说明2.md delete mode 100644 降采样光谱.py diff --git a/.gitignore b/.gitignore index 2e6be2a..d7fbf8d 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,41 @@ tmp/ *.bak *.backup *~ + +# ============================================================ +# 不应进入版本控制的文件类型 +# ============================================================ + +# Qwen Code 用户配置(个人环境,每次 clone 都不同) +.qwen/settings.json +.qwen/settings.json.orig + +# Qwen Code 自动生成的 skill 文件(每次会话重新生成) +.qwen/skills/ + +# GUI 运行时生成的文件 +src/gui/scaler_params.pkl +src/gui/crash_dump.txt + +# 临时/调试脚本(根目录) +降采样光谱.py +1.py +tset.py + +# 报告与文档(本地工作产物) +封装问题分析报告.md +软件说明.md +软件说明2.md + +# 数据子目录中非 .gitkeep 的生成文件 +data/sub/waterindex*.csv +data/sub/waterindex*.xlsx +data/sub/png/watermask.png + +# 图标文件(仅需保留 vector/svg,删除像素图标压缩包副本) +data/icons-1/*.ico +data/icons/*.png +data/icons/word/*.png + +# 旧版脚手架(遗留实验代码) +new/ diff --git a/.qwen/settings.json b/.qwen/settings.json deleted file mode 100644 index 15395fd..0000000 --- a/.qwen/settings.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(\"c:\\users\\duxin\\appdata\\local\\programs\\python\\python311\\python.exe\" *)", - "Bash(get-childitem *)", - "Bash(select-object *)", - "Bash(python *)", - "Bash(where *)", - "Bash(conda *)", - "Bash(dir *)", - "Bash(cmd *)", - "Bash(del *)", - "Bash(powershell *)", - "Bash(git *)", - "Bash(type *)", - "Bash(.\\venv\\scripts\\python.exe *)", - "Bash(\"d:\\111\\office\\zhlduijie\\1.wq\\wq_gui\\venv\\scripts\\python.exe\" *)", - "Bash(c:\\users\\duxin\\appdata\\local\\programs\\python\\python311\\python.exe *)", - "Bash(venv\\scripts\\python.exe *)", - "Bash(findstr *)", - "Bash(select-string *)" - ] - }, - "$version": 4 -} \ No newline at end of file diff --git a/.qwen/settings.json.orig b/.qwen/settings.json.orig deleted file mode 100644 index 0fff81c..0000000 --- a/.qwen/settings.json.orig +++ /dev/null @@ -1,24 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(\"c:\\users\\duxin\\appdata\\local\\programs\\python\\python311\\python.exe\" *)", - "Bash(get-childitem *)", - "Bash(select-object *)", - "Bash(python *)", - "Bash(where *)", - "Bash(conda *)", - "Bash(dir *)", - "Bash(cmd *)", - "Bash(del *)", - "Bash(powershell *)", - "Bash(git *)", - "Bash(type *)", - "Bash(.\\venv\\scripts\\python.exe *)", - "Bash(\"d:\\111\\office\\zhlduijie\\1.wq\\wq_gui\\venv\\scripts\\python.exe\" *)", - "Bash(c:\\users\\duxin\\appdata\\local\\programs\\python\\python311\\python.exe *)", - "Bash(venv\\scripts\\python.exe *)", - "Bash(findstr *)" - ] - }, - "$version": 4 -} \ No newline at end of file diff --git a/.qwen/skills/code_replacement_state_audit/SKILL.md b/.qwen/skills/code_replacement_state_audit/SKILL.md deleted file mode 100644 index 79659a7..0000000 --- a/.qwen/skills/code_replacement_state_audit/SKILL.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -name: 代码替换请求的现状审计 -description: 处理用户"代码替换/新增"指令时,先审计磁盘真实状态再用 ask_user_question 确认——避免覆盖已落盘的高版本代码 -source: auto-skill -extracted_at: '2026-06-03T05:36:58.746Z' ---- - -# 代码替换请求的现状审计 - -## 适用场景 - -用户给出"代码替换"或"按某版本代码新增"指令,但**没有提供与磁盘当前状态对比信息**时。典型触发: - -- 用户贴了一段代码说"请帮我写/替换这个" -- 用户引用某个文档/旧版本/旧 chat 说"按这个来" -- 之前的 `state_snapshot` / `memory` / `git log` 描述可能与磁盘现状不一致 - -## 核心原则 - -**永远不要盲信"用户给的代码是最新版本"**——磁盘上的代码可能已经是更完善的版本(用户或其他 agent 已迭代过)。覆盖 = 丢功能。 - -直接覆盖的代价不一定是显式 bug,也可能是"丢失用户已批准的设计决策"(如 duck-type 探测 / ctx 抽象 / 信号协议 / 二次确认窗 / 错误定位)。 - -## 5 步标准操作 - -### 1. 确认文件存在 - -`glob` 或 `list_directory` 看目标文件是否已存在: - -- 不存在 → 新建 -- 存在 → 进入第 2 步审计 - -### 2. grep 关键符号 + 读关键段 - -- 找"用户贴的代码"里的 3-5 个关键符号(函数名 / 类名 / 关键常量 / import) -- 在磁盘文件里 grep 同样的符号 -- `read_file` 关键段(行号从 grep 结果直接拿) - -### 3. 构造差异对照表 - -列出: - -``` -| 目标文件 | 用户贴的版本 | 磁盘现有版本 | 直接覆盖会丢失 | -``` - -**关键列**:"直接覆盖会丢失什么"——让用户判断成本。具体粒度到"功能模块 / 设计决策 / 防御层 / 入口协议",不要写"代码差异"这种空话。 - -### 4. ask_user_question 让用户拍板 - -3 个标准选项(措辞可调,但**必须给出现状 + 三选一**): - -- **A. 保留现状**(推荐,磁盘已是更新版)—— 直接进 Smoke Test -- **B. 强制覆盖到旧版** —— 写明丢什么 + 备份建议(git stash / 复制到 `_old.py`) -- **C. 混合:只取某段增量** —— 见第 5 步 - -**不要在第 1 次 ask 时就列具体的"哪段增量"**——先让用户在 A/B/C 之间选。如果选 C,再做第 5 步。 - -### 5. 若用户选 C,识别"真正增量" - -对比 1.0 vs 2.0,识别 1.0 真正独有的部分(2.0 没有的): - -- ❌ 排除 1.0 比 2.0 简单的(2.0 是超集 / 工厂分层 / 多了 CLI) -- ❌ 排除 1.0 整体被 2.0 工厂分层超越的(_make_objective vs _build_model + _get_search_space) -- ✅ 关注 1.0 独有的功能层(即使 2.0 不"明显"需要) - -对每个候选增量,再问一次"采纳哪段",让用户具体选(multiSelect=false,一次只选 1 段最稳)。 - -## 落地原则 - -执行"采纳 1.0 某段增量到 2.0"时: - -- **最小化外科手术式编辑**:只动需要动的文件,只改需要改的段 -- **保留 2.0 的设计决策**(duck-type 探测 / ctx 抽象 / 信号协议 / 二次确认窗 / 错误定位) -- **顶部 import 增量用 `replace_all=False` 单点插入**,避免破坏其他 import 顺序 -- **同名变量全链路替换**(如 `self.config` → `clean_config`)要贯穿 ctx 构造 / v2 调用 / v1 fallback,避免双源差异 -- **单步模式不一定要清洗**(不走 panel 完整 config,与清洗器无关) -- **清洗器这种"防患于未然"的代码要给日志**(`self.log_message.emit(f"[清洗器] 已删除 N 个未知 key")`)让运行时可见 - -## 验证三件套 - -落地后必跑: - -1. **AST 语法检查**:`ast.parse(open(p, encoding='utf-8-sig').read())` 对 5 个核心文件 - - 必加 `utf-8-sig`:WQ_GUI 的 water_quality_gui.py line 1 是 BOM,plain `utf-8` 必挂 -2. **关键符号 grep**:确认新代码的关键符号(import / 关键函数调用)都命中,hit 数符合预期 -3. **顶层导入测试**:用 mock PyQt5 + `sys.path.insert(0, 'src/gui/core')`,验证模块整体可加载 - - PyQt5 mock 模板见下方"参考代码" - - Windows 环境调 Python:用 conda env 的 `python.exe` 全路径,不要靠 PATH - -## 反例(不要做) - -- ❌ "按用户贴的代码原封不动写入"——1.0 简化版的覆盖陷阱 -- ❌ "保留 state_snapshot 描述"——state snapshot 可能不准确(写的是意图,磁盘才是事实) -- ❌ "用 git log 反推当前状态"——git log 不能反映工作区未提交改动 -- ❌ "靠 memory 推断当前状态"——memory 可能是 22 天前的(已确认过期) -- ❌ "磁盘和用户给的代码看起来一样就不审计"——一行之差可能就是"防弹层"丢失 - -## 参考代码 - -### PyQt5 mock 模板(worker_thread.py 顶层导入测试) - -```python -import os, sys -os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES' -os.environ['SHAPE_ENCODING'] = 'UTF-8' -sys.path.insert(0, 'src/gui/core') - -import types -pyqt5 = types.ModuleType("PyQt5") -qtc = types.ModuleType("PyQt5.QtCore") -class _QThread: - def __init__(self, *a, **kw): pass -class _Signal: - def __init__(self, *a, **kw): pass -qtc.QThread = _QThread -qtc.pyqtSignal = _Signal -qtc.Qt = type("Qt", (), {"QueuedConnection": 1, "UserRole": 0})() -sys.modules["PyQt5"] = pyqt5 -sys.modules["PyQt5.QtCore"] = qtc - -import worker_thread -# 副作用: check_pipeline_dependencies() 会打印依赖检查日志(可忽略) -``` - -### Windows 上跑 conda env python - -```bat -cmd /c "D:\xxx\anconda\envs\XXX\python.exe D:\path\to\script.py" -``` - -PowerShell 单行 `python -c "..."` 在中文路径 / 双引号 / 单引号嵌套时易翻车,**写临时 .py 文件再用 `cmd /c` 调**最稳。 - -## 案例来源(2026-06-03 WQ_GUI 路线 B MVP) - -- 用户贴 1.0 简化版:300 行 automl_trainer / 简化 worker_thread.run() / 简化 on_run_all_clicked -- 磁盘上 2.0 落盘版:545 行 automl_trainer(_build_model + _get_search_space 工厂 / argparse CLI)/ duck-type 探测 v2 + PipelineContext 抽象 / 完整二次确认窗 / 失败步骤 _focus_step 定位 / [DEPRECATED] stop 保留 -- 1.0 唯一真增量 = **"防弹级参数清洗器"**(method_map 14 项 + inspect.signature 过滤未知 key + has_kwargs 豁免 + 未知 key 数量日志) -- 落地:worker_thread.py:run() 内 set_callback 之后插入 53 行清洗器,self.config 6 处替换为 clean_config -- 验证:5 文件 AST 全通过 + 关键符号 7 项命中 + PyQt5 mock 下 import 成功 -- 净增行数:407 → 457(+50 行) diff --git a/.qwen/skills/facade_kwargs_defense/SKILL.md b/.qwen/skills/facade_kwargs_defense/SKILL.md deleted file mode 100644 index 34f4ba5..0000000 --- a/.qwen/skills/facade_kwargs_defense/SKILL.md +++ /dev/null @@ -1,309 +0,0 @@ ---- -name: PipelineRunner Facade 防御性 kwargs 兜底 -description: WQ_GUI 14 个 stepX_... Facade 方法必须以 **kwargs 收尾——配合 PipelineRunner 调度模式杜绝 "unexpected keyword argument" TypeError -source: auto-skill -extracted_at: '2026-06-04T00:54:50.036Z' ---- - -# PipelineRunner Facade 防御性 kwargs 兜底 - -## 适用场景 - -在 WQ_GUI 中,**任何被 `PipelineRunner` 调用的 14 个 `stepX_...` Facade 方法**(位于 `src/core/water_quality_inversion_pipeline_GUI.py`),其形参表末尾**必须**带 `**kwargs`。触发信号: - -- 用户报错 `TypeError: stepX_xxx() got an unexpected keyword argument 'yyy'` -- 改 PIPELINE_STEPS 的 `requires` 列表 -- 新增 / 重命名一个 step 方法 -- 重构 PipelineRunner 的 `_invoke` 注入逻辑 - -## 核心原则 - -**Facade 的形参表 = 显式声明的形参 + `, **kwargs`**。`kwargs` 必须**严格位于形参表最后**(Python 语法硬要求)。 - -```python -# ✅ 正确 -def step3_remove_glint(self, img_path: str, - method: str = "subtract_nir", - # ... 30+ 业务形参 ... - skip_dependency_check: bool = False, - **kwargs) -> str: - ... - -# ❌ 错误:**kwargs 不能放中间或前面 -def step3_remove_glint(self, img_path, **kwargs, skip_dependency_check): # SyntaxError -``` - -## 为什么需要这层防御 - -`PipelineRunner._invoke`(`src/core/pipeline/runner.py`)会向方法注入两类参数: - -| 层 | 来源 | 形参 key 怎么定 | -|---|---|---| -| **L2** | ctx 字段(按 `spec.requires` 列表) | `_default_param_name(ctx_key)` 默认去 `_path` 后缀 | -| **L3** | `ctx.user_config[step_id]`(14 panel dict 整体) | dict 的 key 原样注入 | - -**L2 触发 TypeError 的真实场景**(2026-06-04 真实发生): - -- `PIPELINE_STEPS.step3.requires = ["img_path", "water_mask_path", "glint_mask_path"]` -- Runner 注入 `kwargs["glint_mask_path"] = ctx.glint_mask_path` -- `step3_remove_glint` 形参表**没有** `glint_mask_path`(虽然业务上耀斑掩膜是在子调用 `GlintRemovalStep.run` 内部用的,Facade 本身不接) -- → **TypeError: step3_remove_glint() got an unexpected keyword argument 'glint_mask_path'** - -**L3 触发 TypeError 的场景**: - -- user_config 14 panel dict 里残留了旧字段名 / 跨 step 串味的字段 -- 任何 `user_config[step_id][k]` 中的 `k` 都会被注入 - -`**kwargs` 一次性解决两类问题。 - -## 14 个 Facade 方法清单(截至 2026-06-04 已全部带 **kwargs) - -| step | method | 形参表闭合示例 | -|---|---|---| -| 1 | `step1_generate_water_mask` | `output_path: Optional[str] = None, **kwargs) -> str:` | -| 2 | `step2_find_glint_area` | `skip_dependency_check: bool = False, **kwargs) -> str:` | -| 3 | `step3_remove_glint` | `skip_dependency_check: bool = False, **kwargs) -> str:` | -| 4 | `step4_process_csv` | `skip_dependency_check: bool = False, **kwargs) -> str:` | -| 5 | `step5_extract_training_spectra` | `skip_dependency_check: bool = False, **kwargs) -> str:` | -| 5.5 | `step5_5_calculate_water_quality_indices` | `skip_dependency_check: bool = False, **kwargs) -> str:` | -| 6 | `step6_train_models` | `skip_dependency_check: bool = False, **kwargs) -> str:` | -| 6.5 | `step6_5_non_empirical_modeling` | `skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]:` | -| 6.75 | `step6_75_custom_regression` | `skip_dependency_check: bool = False, **kwargs) -> str:` | -| 7 | `step7_generate_sampling_points` | `skip_dependency_check: bool = False, **kwargs) -> str:` | -| 8 | `step8_predict_water_quality` | `skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]:` | -| 8.5 | `step8_5_predict_with_non_empirical_models` | `skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]:` | -| 8.75 | `step8_75_predict_with_custom_regression` | `skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]:` | -| 9 | `step9_generate_distribution_map` | `skip_dependency_check: bool = False, **kwargs) -> str:` | - -## 标准操作 - -### 1. 编辑(最小外科手术式) - -每个方法的最后形参是 `skip_dependency_check: bool = False`,把这一行改成: - -```python - skip_dependency_check: bool = False, **kwargs) -> str: -``` - -**注意缩进必须与原行一致**(13 空格 / 35 空格 / 47 空格 / 48 空格不等,按方法原始缩进)。用 `edit` 工具的 old_string **必须含 docstring 第一行**(`"""步骤X: ..."""`)作唯一标识。 - -### 2. 验证 - -写一个临时校验脚本(项目根目录运行后删掉): - -```python -import ast, re -target = r'D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\core\water_quality_inversion_pipeline_GUI.py' -src = open(target, encoding='utf-8-sig').read() -ast.parse(src) # AST 语法 - -expected = [ - 'step1_generate_water_mask', 'step2_find_glint_area', 'step3_remove_glint', - 'step4_process_csv', 'step5_extract_training_spectra', - 'step5_5_calculate_water_quality_indices', 'step6_train_models', - 'step7_generate_sampling_points', 'step8_predict_water_quality', - 'step9_generate_distribution_map', 'step6_5_non_empirical_modeling', - 'step6_75_custom_regression', 'step8_5_predict_with_non_empirical_models', - 'step8_75_predict_with_custom_regression', -] -pat = re.compile(r'def\s+(\w+)\s*\((?:[^()]|\([^()]*\))*\*\*kwargs\)\s*->\s*[^:]+:', re.DOTALL) -found = set(pat.findall(src)) -print('Missing:', [m for m in expected if m not in found]) -print('Extra :', [m for m in found if m not in expected]) -``` - -期望输出:两个列表都为空。 - -### 3. Windows 执行 - -```bat -cd /d D:\111\office\ZHLduijie\1.WQ\WQ_GUI -py _check.py -del _check.py -``` - -> 必加 `utf-8-sig`:`water_quality_inversion_pipeline_GUI.py` 头部可能含 BOM(`code_replacement_state_audit` skill 里有同样提示)。 - -## ⚠️ 这层防御不能解决什么 - -**` **kwargs` 兜底 ≠ 形参名错位修复**。如果 Runner 注入 `kwargs["training_csv_path"]` 而方法形参是 `csv_path`: - -- ✅ **不会报 TypeError**(`training_csv_path` 被 `**kwargs` 收走) -- ❌ **但 `csv_path` 仍是 None**(方法体内的 `if csv_path is not None: ... else: ...` 走 fallback 分支,可能读 `self.training_csv_path` 哨兵) - -**已知形参名错位的方法**(2026-06-04 已修,commit `64aa5b8`): - -| step | Runner 注入的 ctx key | 方法实际形参 | 实际落地的修复 | -|---|---|---|---| -| step6_5 | `training_csv_path` | `csv_path` | `parameter_map={"training_csv_path": "csv_path"}` | -| step6_75 | (已切到 `indices_path`) | `csv_path` | ⚠️ 见下方"step6_75 路由修复"专题 | -| step8_5 | `models_dir` | `non_empirical_models_dir` | `parameter_map={"models_dir": "non_empirical_models_dir"}` | -| step8_75 | `models_dir` | `custom_regression_dir` | `parameter_map={"models_dir": "custom_regression_dir"}` | - -> **parameter_map** 是 `StepSpec` 已有字段(runner.py:33),作用是把 ctx 字段重命名到方法形参名。**优先用 parameter_map 而非改 requires**——保持 ctx 字段语义清晰(声明式描述上游依赖),形参名是方法私有约定。 - -### step6_75 路由修复(特殊案例,2026-06-04 commit `64aa5b8`) - -`step6_75_custom_regression` **不是简单的 ctx 字段名错位**——方法体内的 fallback 链透露了**真正的数据源是 `indices_path`**: - -```python -# step6_75 形参:csv_path -# 方法体 fallback: -# if csv_path is not None: input_csv = csv_path -# elif self.indices_path is not None: input_csv = self.indices_path # ★ 真相 -``` - -加 `parameter_map` 把 `training_csv_path → csv_path` 看似能跑通,但**实际用错了数据**(training_csv 是 step5 输出,不是 step6_75 想要的 indices CSV)。 - -**正确做法 = 同时改 requires + parameter_map**: - -```python -StepSpec( - step_id="step6_75", method_name="step6_75_custom_regression", - requires=["indices_path"], # ★ 从 training_csv_path 切到 indices_path - produces=["models_dir"], - parameter_map={"indices_path": "csv_path"}, # ★ 同步改 key - description="自定义回归分析", -), -``` - -**配合 `skip_when_missing` 兜底**:若用户没跑 step5_5(`ctx.indices_path` 为 None),runner 自动 skip 整个 step6_75,不会用错位数据静默执行。 - -**判别何时需要"路由切"vs"纯 rename"**: -- 看方法体 fallback 链:fallback 到 `self.indices_path`/`self.deglint_img_path`/其他 ctx 字段名 → **需要改 requires** -- 仅是 key 名字不同,方法体直接用形参 → 只改 parameter_map - -## L2 注入顺序冲突:多个 requires 字段解析到同一形参名 - -### 场景 - -`StepSpec.requires` 里有**多个 ctx 字段**,经过 `_default_param_name` / `parameter_map` 解析后,**会落到同一个方法形参名**。L2 注入是**顺序敏感**的(后者覆盖前者),后注入的会**默默覆盖**前一个的赋值。 - -### 真实案例(2026-06-04 step5 修复) - -业务需求:step5 真正需要 step4 产物 `processed_csv_path`,但**保留 raw `csv_path` 字段**作为 `user_config` 覆盖入口。 - -**❌ 错误的 parameter_map 写法**(用户原方案的隐藏 bug): - -```python -StepSpec( - step_id="step5", method_name="step5_extract_training_spectra", - requires=["deglint_img_path", "processed_csv_path", "csv_path", ...], # raw csv_path 也在 - parameter_map={"processed_csv_path": "csv_path"}, # ★ 只映射了一个 - ... -) -``` - -L2 注入顺序(`runner.py:184-186`): -1. `deglint_img_path` → `kwargs[deglint_img_path] = ctx.deglint_img_path` -2. `processed_csv_path` → `kwargs[csv_path] = ctx.processed_csv_path` ← 主路径生效 -3. `csv_path`(无映射 → 默认)→ `kwargs[csv_path] = ctx.csv_path` ← **后注入 None 覆盖了主路径!** - -**症状**:step5 形参 `csv_path` 拿到的是 raw `ctx.csv_path`(通常是 None),方法体 fallback 到 `self.processed_csv_path`——但这个 fallback 也可能是 None(step4 没跑),step5 内部空跑 → "**静默错误**"。 - -**✅ 修法:parameter_map 双向映射 + 占位名落 **kwargs**: - -```python -parameter_map={ - "processed_csv_path": "csv_path", # 主路径(注入到方法形参) - "csv_path": "_raw_csv_ignored", # 占位(落到 step5 形参列表末尾的 **kwargs) -}, -``` - -注入顺序重排后: -- `processed_csv_path` → `kwargs[csv_path] = ctx.processed_csv_path` ← 主路径 -- `csv_path` → `kwargs[_raw_csv_ignored] = ctx.csv_path` ← 落 **kwargs(被吞) - -step5 形参 `csv_path` 最终拿到 `ctx.processed_csv_path` 的值 ✓。 - -### 验证模板(行为模拟) - -写临时 `_verify_l2_inject.py` 复刻 `runner.py:184-186` 的 L2 注入循环,**不要只靠 AST 静态检查**——parameter_map 的 key 顺序、requires 的字段顺序都是动态的: - -```python -import sys -sys.path.insert(0, r'D:\111\office\ZHLduijie\1.WQ\WQ_GUI') -from src.core.pipeline.context import PipelineContext -from src.core.pipeline.runner import PIPELINE_STEPS - -spec5 = next(s for s in PIPELINE_STEPS if s.step_id == 'step5') - -# 复刻 L2 注入(与 runner.py:184-186 完全一致) -def l2_inject(spec, ctx): - kwargs = {} - for ctx_key in spec.requires: - param_name = spec.parameter_map.get(ctx_key, ctx_key) # ★ 必须原样复刻 - kwargs[param_name] = ctx.get(ctx_key) - return kwargs - -# 关键断言 -ctx = PipelineContext(processed_csv_path='/csv/processed.csv', csv_path='/csv/raw.csv') -kw = l2_inject(spec5, ctx) -assert kw.get('csv_path') == '/csv/processed.csv', \ - f"形参 csv_path 应等于 processed_csv_path, 实际 {kw.get('csv_path')!r}" -assert '_raw_csv_ignored' in kw, "占位名应被注入到 kwargs" -print(f'OK: csv_path 形参 = {kw["csv_path"]!r} (processed, 主路径正确)') -print(f'OK: _raw_csv_ignored 占位 = {kw["_raw_csv_ignored"]!r} (raw, 落 **kwargs 被吞)') -``` - -跑完删掉:`py _verify_l2_inject.py & del _verify_l2_inject.py`(Windows 一行模式)。 - -### 何时需要警惕这个冲突 - -修改 StepSpec 时检查清单(**先看这一段再写 parameter_map**): - -- [ ] **requires 里是否有多于 1 个 ctx 字段,解析后会落到同名方法形参?** 典型撞车: - - 同名字段(如 `processed_csv_path` 和 `csv_path` 都能映射到 `csv_path`) - - 同名 `_default_param_name` 退化(如 `boundary_path` 和 `boundary_shp_path` 默认都映射到 `boundary_path`——但要注意 `_default_param_name` 已废弃去后缀,原样返回 ctx key,所以 `boundary_path` 和 `boundary_shp_path` 默认就会撞 `boundary_path` / `boundary_shp_path` 不会撞,要撞就必须显式 parameter_map) - - 字段名 + parameter_map 重命名撞车 -- [ ] **"主路径"字段在 requires 列表靠前位置**(让后续"备路径"覆盖,但**这不解决冲突**——只要有第二次注入就一定会覆盖) -- [ ] **"备路径"字段**用占位名 `_xxx_ignored` / `_xxx_kwargs_only` 映射,让它落到 **kwargs -- [ ] **确认方法形参表末尾有 `**kwargs`** 兜底(`facade_kwargs_defense` skill 核心要求,已 14/14 落地) - -### 反例(不要做) - -- ❌ "我让 `csv_path` 不在 requires 里就行了"——会**丢失 user_config 覆盖入口**(如果用户想用 raw CSV 而不是 processed) -- ❌ "改 L2 注入循环,让 parameter_map 字段最后注入"——会**改变 runner 通用语义**,影响所有 step 的注入顺序 -- ❌ "加 `if param_name in kwargs: continue` 在 L2 注入里"——隐式"第一次优先"语义,新人读代码摸不着头脑 -- ❌ "用 position in requires 做加权"——把数据语义(哪个字段优先级高)塞到列表顺序里,runner 应该保持"声明式" - -### 与"纯 rename"的区别 - -| 维度 | 纯 rename(已有 skill 案例) | 多→1 冲突(本节案例) | -|---|---|---| -| 典型场景 | step6_5/6_75/8_5/8_75:1 个 requires 重命名到形参 | step5:2 个 requires 撞到同一形参 | -| parameter_map | 1 个 key→value | 2 个 key→同名 value + 占位名 | -| requires | 1 个字段 | 2 个字段(主 + 备) | -| 冲突来源 | 不会出现(单 key) | 出现(顺序敏感 + 撞名) | -| 修法 | 只加 parameter_map | 双向 parameter_map + 占位名 | - -## 与其他防御层的关系 - -``` -PipelineRunner.run() 主循环 - ├─ L1 runner.py:152 skip_when_missing ─── ctx. 全 None → skip step - ├─ L2 runner.py:182 ctx 字段注入 ─── 形参表里没声明 → TypeError ⚠️ → **kwargs 兜底 - ├─ L3 runner.py:188 user_config 合并 ─── user_config 有"空字符串"/None → 跳过(上一轮加的守卫)✅ - └─ L4 runner.py:211 except 捕获 ─── 业务抛异常 → ctx.status="error" + raise -``` - -`**kwargs` 是 **L2 的"消极兜底"**——宁愿吞掉多余 key 也不报 TypeError。**真正的"积极修复"是 parameter_map**(让 ctx 字段名映射到正确形参名)。两层配合: -- **保守期间(重构初期)**:先 `**kwargs` 兜住,TypeError 消失 -- **稳定阶段**:补 parameter_map,让方法收到正确数据 - -## 反例(不要做) - -- ❌ "不写 `**kwargs`,靠 type hint + IDE 检查兜底"——Runner 是运行时注入,IDE 看不到 -- ❌ "把 `**kwargs` 放形参表中间"——Python 语法错误 -- ❌ "改 requires 列表去掉冗余 ctx 字段"——会导致 `skip_when_missing` 误判(以为 step 不需要该 ctx 字段),应该用 `parameter_map` 重命名而非删除 requires -- ❌ "在 14 个 Facade 方法体里加 `if 'glint_mask_path' in kwargs: kwargs.pop('glint_mask_path')`"——脏活,且每个方法都要加,远不如 `**kwargs` 一行优雅 - -## 案例来源 - -- 2026-06-04 WQ_GUI PipelineRunner 迁移第二步 -- 触发:`step3_remove_glint() got an unexpected keyword argument 'glint_mask_path'` -- 根因:`PIPELINE_STEPS.step3.requires` 写了 `glint_mask_path`,但 `GlintRemovalStep` 内部使用,Facade 自身不接这个形参 -- 落地:14 个 Facade 全部加 `, **kwargs`,0 个 TypeError -- 验证:临时 `_check.py` 14/14 命中 + AST 解析通过 -- 续:4 个 parameter_map 全部落地(commit `64aa5b8`),含 step6_75 路由切到 indices_path;L3 非空过滤同步加入 `runner._invoke:188` -- 2026-06-04 step5 严格依赖修复:发现 L2 注入顺序冲突(requires 多个字段解析到同一形参名),引入"双向 parameter_map + 占位名落 **kwargs"模式;step5 形参 `csv_path` 真正接到 `processed_csv_path`(step4 产物),raw `csv_path` 保留为 user_config 覆盖入口,落占位名 `_raw_csv_ignored` 后被 `**kwargs` 吞。skip_when_missing 块同步加 `_notify` 通知,**拒绝静默跳过**(15 条 _notify 全带具体 missing 字段列表证据)。 diff --git a/.qwen/skills/wq_gui_data_flow/SKILL.md b/.qwen/skills/wq_gui_data_flow/SKILL.md deleted file mode 100644 index a3a4964..0000000 --- a/.qwen/skills/wq_gui_data_flow/SKILL.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -name: WQ_GUI 数据流转架构 -description: WQ_GUI ProjectSession 事件总线驱动的步骤间数据传递机制(完整重构版) -source: auto-skill -extracted_at: '2026-05-28T09:07:34.967Z' ---- - -# WQ_GUI 数据流转架构 - -## 核心结论 - -整个系统是**基于文件路径驱动**的管道,所有数据存储在本地磁盘。重构后通过 `ProjectSession` 事件总线实现 Panel 间完全解耦。 - ---- - -## 1. 旧架构(旧代码中已删除) - -主窗口通过 `self.step_outputs` 字典 + `step_dependencies` 配置 + `auto_populate_*` 系列方法管理步骤间路径填充。存在高度耦合问题: - -```python -# 已废弃并删除 -self.step_outputs = {} -self._init_step_dependencies() -self.update_step_outputs(step_name, work_path) -self.auto_populate_dependent_steps(completed_step) -self.auto_populate_step_inputs(step_id) -self.find_step_output(work_path, step_id, output_type) -self.add_auto_fill_buttons_to_panels() -self.scan_work_directory_for_files(work_path) -``` - ---- - -## 2. 新架构:ProjectSession 事件总线 - -### Session 核心 API(`src/core/project_session.py`) - -```python -class ProjectSession(QObject): - path_updated = pyqtSignal(str, str, str) # step, out_type, path - step_outputs_ready = pyqtSignal(str, str) # step, out_type - - def update_output(step, out_type, path): - """Panel 完成后广播输出路径""" - - def update_outputs(step, {out_type: path, ...}): - """Panel 完成后批量广播多个输出路径""" - - def get_output(step, out_type): - """Panel 可主动查询上游路径(用于自动填充)""" - - def get_step_outputs(step): - """返回该 step 的全部输出字典""" - - def scan_work_directory(): - """主窗口 on_step_completed 末尾调用,扫描并广播所有已知路径""" -``` - -### Panel 重构模板 - -```python -class StepXPanel(QWidget): - def __init__(self, session=None, parent=None): - super().__init__(parent) - self.session = session - self.work_dir = None - self.init_ui() - self._bind_session_signals() - - def _bind_session_signals(self): - if not self.session: - return - self.session.path_updated.connect( - self._on_session_path_updated, Qt.QueuedConnection - ) - - @pyqtSlot(str, str, str) - def _on_session_path_updated(self, step_name, output_type, path): - print(f"[StepX Debug] 收到广播: step={step_name}, type={output_type}, path={path}") - if step_name == 'step1': - if output_type == 'reference_img': - if not self.img_file.get_path().strip(): - self.img_file.set_path(path) - print(f"[StepX] 自动填充参考影像: {path}") - elif output_type == 'water_mask': - if not self.water_mask_file.get_path().strip(): - self.water_mask_file.set_path(path) - print(f"[StepX] 自动填充水域掩膜: {path}") - # ... - - def on_step_finished(self, success, message): - """由主窗口 on_step_completed 通过 getattr 动态调用""" - if not success: - return - if self.session: - outputs = {} - path = self.output_widget.get_path().strip() - if path: - outputs['output_type'] = path - if outputs: - self.session.update_outputs('stepX', outputs) -``` - -### 主窗口两处改动 - -```python -# 1. __init__ 中注入 session(所有 Panel 统一注入) -self.step1_panel = Step1Panel(session=self.session) -self.step2_panel = Step2Panel(session=self.session) -self.step3_panel = Step3Panel(session=self.session) -self.step4_panel = Step4Panel(session=self.session) -self.step5_panel = Step5Panel(session=self.session) -self.step5_5_panel = Step5_5Panel(session=self.session) -self.step6_panel = Step6Panel(session=self.session) -self.step6_5_panel = Step6_5Panel(session=self.session) -self.step6_75_panel = Step6_75Panel(session=self.session) -self.step7_panel = Step7Panel(session=self.session) -self.step8_panel = Step8Panel(session=self.session) -self.step8_5_panel = Step8_5Panel(session=self.session) -self.step8_75_panel = Step8_75Panel(session=self.session) -self.step9_panel = Step9Panel(session=self.session) - -# 2. on_step_completed(通用动态获取,无需维护字典) -def on_step_completed(self, step_name, success, message): - if not success: - return - if hasattr(self, 'session') and self.session: - self.session.scan_work_directory() - - panel = getattr(self, f"{step_name}_panel", None) - if panel and hasattr(panel, 'on_step_finished'): - panel.on_step_finished(success, message) -``` - ---- - -## 3. 全链路事件流 - -### step1 → step2 / step3 路径(通过 Shapefile 栅格化产物) - -| 场景 | 广播的 water_mask 路径 | -|------|----------------------| -| NDWI 模式 | `output_file` 用户指定路径 | -| Shapefile 模式 | `{work_dir}/1_water_mask/water_mask_from_shp.dat`(优先)
若文件不存在则 fallback 回 `mask_file.get_path()` | - -``` -step1 完成 - → step1_panel.on_step_finished() - → session.update_outputs('step1', { - 'reference_img': img_path, - 'water_mask': mask_path # 可能是 .dat 或 .shp(见上表) - }) - → step2_panel._on_session_path_updated() - → step3_panel._on_session_path_updated() -``` - -### step3 → step5 / step7;step5 → 下游训练 - -``` -step3.deglint_image ──┬─→ step5.deglint_image(填充 img_file) - └─→ step7.deglint_image(填充 img_file) - -step5.training_spectra ──┬─→ step5_5.index_features - ├─→ step6.models_dir ──→ step8.predictions - ├─→ step6_5.models_dir ──→ step8_5.predictions - └─→ step6_75.models_dir ──→ step8_75.predictions - -step7.sampling_points ──┬─→ step8 - ├─→ step8_5 - └─→ step8_75 - -step8/8_5/8_75.predictions ──→ step9.distribution_map -``` - -### 各 Panel 监听/发布对照表(完整版) - -| Panel | 监听 | 发布 | -|-------|------|------| -| step1 | — | `reference_img`, `water_mask` | -| step2 | `step1.reference_img`, `step1.water_mask` | `glint_mask` | -| step3 | `step1.reference_img`, `step1.water_mask`, `step2.glint_mask` | `deglint_image` | -| step4 | — | `processed_data` | -| step5 | `step3.deglint_image`, `step4.processed_data`, `step2.glint_mask` | `training_spectra` | -| step5_5 | `step5.training_spectra` | `index_features` | -| step6 | `step5.training_spectra` | `models_dir` | -| step6_5 | `step5.training_spectra` | `models_dir` | -| step6_75 | `step5.training_spectra` | `models_dir` | -| step7 | `step3.deglint_image`, `step1.water_mask`, `step2.glint_mask` | `sampling_points` | -| step8 | `step7.sampling_points`, `step6.models_dir` | `predictions` | -| step8_5 | `step7.sampling_points`, `step6_5.models_dir` | `predictions` | -| step8_75 | `step7.sampling_points`, `step6_75.models_dir` | `predictions` | -| step9 | `step8.predictions`, `step8_5.predictions`, `step8_75.predictions` | `distribution_map` | - ---- - -## 4. 关键约束 - -- `__init__` 参数 `session=None`(向后兼容,主窗口可继续不传) -- 所有 Panel 的 `init_ui / get_config / set_config / update_from_config` 完整保留 -- 删除所有 `self.window().stepX_panel` 跨界访问 -- 使用 `self.session.get_output()` 替代直接读取其他 panel 的 widget -- 监听使用 `Qt.QueuedConnection` 确保跨线程安全 -- 仅在 field 为空时自动填充(`not widget.get_path().strip()`) -- `update_from_config` 中优先从 Session 获取路径,再用 Session 广播 -- 主窗口 `on_step_completed` 中使用 `getattr(self, f"{step_name}_panel", None)` 实现通用动态获取,无需维护硬编码字典 -- `step1` Shapefile 模式下,**不能**直接广播 `.shp` 输入文件,必须拼接 `{work_dir}/1_water_mask/water_mask_from_shp.dat` 作为产物路径 \ No newline at end of file diff --git a/.qwen/skills/wq_gui_external_model_panel/SKILL.md b/.qwen/skills/wq_gui_external_model_panel/SKILL.md deleted file mode 100644 index 4a53d35..0000000 --- a/.qwen/skills/wq_gui_external_model_panel/SKILL.md +++ /dev/null @@ -1,294 +0,0 @@ ---- -name: WQ_GUI PyQt5 面板外部模型导入模式 -description: 在 Step8 等预测面板中通过 QRadioButton + FileSelectWidget + joblib.load 防御性加载实现"内置/导入"双模式切换的标准模式 -source: auto-skill -extracted_at: '2026-06-08T01:38:14.481Z' ---- - -# WQ_GUI PyQt5 面板外部模型导入模式 - -## 适用场景 - -Step8(机器学习预测)、Step8_5、Step8_75 等面板需要同时支持: -1. **内置模式**:使用 `step6` 训练流程生成的模型目录 -2. **导入模式**:用户手动选择本地预训练 `.joblib` 文件直接加载 - ---- - -## 1. 模板(可直接复制到 `__init__` + `init_ui`) - -```python -from PyQt5.QtWidgets import QRadioButton - -class StepXPanel(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.current_model = None # ★ 外部模型实例缓存 - self.init_ui() - - def init_ui(self): - layout = QVBoxLayout() - - # -------- 模型来源选择(单选按钮组) -------- - source_group = QGroupBox("模型来源") - source_layout = QVBoxLayout() - - self.use_trained_model = QRadioButton("使用当前训练流程的模型") - self.use_external_model = QRadioButton("导入本地预训练模型 (.joblib)") - self.use_trained_model.setChecked(True) - source_layout.addWidget(self.use_trained_model) - source_layout.addWidget(self.use_external_model) - - self.use_trained_model.toggled.connect(self._on_model_source_changed) - self.use_external_model.toggled.connect(self._on_model_source_changed) - - source_group.setLayout(source_layout) - layout.addWidget(source_group) - - # -------- 外部模型文件选择(条件显示) -------- - self.external_model_widget = FileSelectWidget( - "预训练模型:", - "Joblib Files (*.joblib);;All Files (*.*)" - ) - # FileSelectWidget 的 browse_btn 默认连着 open file 行为, - # 需要先断开默认连接,再接自定义槽 - self.external_model_widget.browse_btn.clicked.disconnect() - self.external_model_widget.browse_btn.clicked.connect(self._browse_external_model) - self.external_model_widget.setVisible(False) - layout.addWidget(self.external_model_widget) - - # ... 其余原有 UI ... -``` - ---- - -## 2. 槽函数模板 - -### `_on_model_source_changed` - -单选按钮 `toggled` 信号在**两个**按钮上都会触发(点击 A 时 A 触发,B 也触发),所以用 `if not checked: return` 让非选中分支短路。 - -```python -def _on_model_source_changed(self, checked: bool): - """单选按钮切换:控制外部模型文件选择控件的显示/隐藏""" - if not checked: - return - is_external = self.use_external_model.isChecked() - self.external_model_widget.setVisible(is_external) - # 切回"使用当前模型"时清空缓存,释放内存并避免误用旧模型 - if not is_external: - self.current_model = None -``` - -### `_browse_external_model` - -- 用 `QFileDialog.getOpenFileName` 而非 `getExistingDirectory` -- 防御性解析两种格式:`{"model": pipeline, ...}`(Step6 输出格式)和裸 `Pipeline` 对象 -- 失败用 `QMessageBox.warning` 友善提示;成功用 `QMessageBox.information` 告知 - -```python -from PyQt5.QtWidgets import QFileDialog, QMessageBox -from pathlib import Path - -def _browse_external_model(self): - """浏览并加载外部 .joblib 预训练模型文件""" - default = self._get_default_work_dir() - path, _ = QFileDialog.getOpenFileName( - self, - "选择预训练模型 (.joblib)", - default, - "Joblib Files (*.joblib);;All Files (*.*)", - ) - if not path: - return - - try: - import joblib - loaded = joblib.load(path) - # 兼容两种格式:dict{"model": obj} 或裸 Pipeline - if isinstance(loaded, dict) and "model" in loaded: - self.current_model = loaded["model"] - elif hasattr(loaded, "predict"): - self.current_model = loaded - else: - QMessageBox.warning( - self, - "模型格式错误", - f"无法识别的模型格式,文件内容类型为:{type(loaded).__name__}", - ) - return - self.external_model_widget.set_path(path) - QMessageBox.information( - self, - "模型加载成功", - f"已加载模型:{Path(path).name}\n类型:{type(self.current_model).__name__}", - ) - except Exception as e: - self.current_model = None - QMessageBox.warning( - self, - "模型加载失败", - f"加载模型时发生错误:\n{type(e).__name__}: {e}", - ) -``` - ---- - -## 3. `run_step` 改造模板 - -在原有目录加载逻辑之前,插入外部模型优先分支: - -```python -def run_step(self): - """独立运行步骤X""" - # ... 公共输入校验 ... - - # ★ 外部模型优先分支 - if self.use_external_model.isChecked(): - if self.current_model is None: - QMessageBox.warning( - self, - "模型未加载", - "请先点击「浏览...」按钮加载预训练模型文件!", - ) - return - external_model_path = self.external_model_widget.get_path() or "" - main_window = self.window() - if hasattr(main_window, 'run_single_step'): - config = { - 'stepX': self.get_config(), - '_external_model': self.current_model, # ★ 直接传对象 - '_external_model_path': external_model_path, # 供日志/回溯用 - } - main_window.run_single_step('stepX', config) - return - - # 默认流程:使用模型目录(原有逻辑不变) - models_dir = self.models_dir_file.get_path() - if not models_dir: - QMessageBox.warning(self, "输入错误", "请选择模型目录!") - return - # ... 原有 run_step 剩余代码 ... -``` - ---- - -## 4. 后端三层完整接入(2026-06-08 已落地) - -完整数据流分为三层,每层各一处分流点: - -``` -GUI step8_panel - ↓ config = {'_external_model': obj, '_external_model_path': path, 'step8': {...}} - ↓ -worker_thread.run_single_step() [第1处分流:透传顶层 key] - ↓ step_config = config['step8'] + {'_external_model': obj, '_external_model_path': path} - ↓ -prediction_step.predict_water_quality() [第2处分流:接收 + 透传] - ↓ _external_model=obj, _external_model_path=path - ↓ -WaterQualityInference(artifacts_dir, external_model=obj, external_model_path=path) - ↓ -inference_batch.batch_inference_multi_models() [第3处分流:effective_model 短路] - ↓ external_model=obj - ↓ -inference_batch.inference_pipeline() - → self.external_model is not None → self.loaded_model_data = self.external_model(跳过磁盘加载) -``` - -### 4a. worker_thread.py — run_single_step 透传 - -在 `step_config = dict(config.get(step_name, {}))` 之后、"skip_dependency_check" 之前插入: - -```python -# 透传面板顶层传入的外部预训练模型(GUI step8_panel 通过 config['_external_model'] 传入) -# 非空才覆盖(遵循 feedback_never_overwrite_with_empty 原则) -for key in ('_external_model', '_external_model_path'): - val = config.get(key) - if val is not None and val != "": - step_config[key] = val -``` - -### 4b. prediction_step.py — predict_water_quality 签名 + 透传 - -形参表末尾增加两个参数: - -```python -_external_model=None, -_external_model_path=None, -``` - -构造处透传: - -```python -inferencer = WaterQualityInference( - models_dir, - external_model=_external_model, - external_model_path=_external_model_path, -) -all_results = inferencer.batch_inference_multi_models( - models_root_dir=models_dir, - ... - external_model=_external_model, - external_model_path=_external_model_path, -) -``` - -### 4c. inference_batch.py — 三处修改 - -**① `__init__` 存储**: - -```python -def __init__(self, artifacts_dir: str = "models/artifacts", - external_model=None, external_model_path=None): - ... - self.external_model = external_model - self.external_model_path = external_model_path -``` - -**② `batch_inference_multi_models` 短路 + 注入**: - -```python -# 优先级:外部预训练模型 > 从磁盘加载 -if external_model is not None: - effective_model = external_model - print(f"\n使用外部预训练模型: type={type(external_model).__name__}") -else: - effective_model = None - -# 子目录循环中注入: -if effective_model is not None: - model_inferencer = WaterQualityInference( - str(subdir), - external_model=effective_model, - external_model_path=external_model_path, - ) -else: - model_inferencer = WaterQualityInference(str(subdir)) -``` - -**③ `inference_pipeline` 模型加载短路**(`load_best_model` 调用前): - -```python -if self.external_model is not None: - self.loaded_model_data = self.external_model - print(f" 使用外部预训练模型: type={type(self.external_model).__name__}") -elif model_file_path: - self.load_specific_model(model_file_path) -else: - self.load_best_model(metric=metric) -``` - -**关键约束**: -- `joblib.load` 在 panel 槽函数里完成(GUI 进程内),对象通过 config 引用直接透传;**不跨进程**,所以不需要担心 pickle 序列化问题 -- `batch_inference_multi_models` 形参 `external_model` 和 `external_model_path` **与实例属性同名**(`self.external_model`),两者都传是为了让每个子目录创建的 `WaterQualityInference` 实例都能独立持有引用 -- 原有从 `models_dir` 目录加载的逻辑完全保留,只在 `external_model is not None` 时短路 - ---- - -## 5. 已知约束 - -- `FileSelectWidget.browse_btn.clicked` 在 `init_ui` 里会重复 connect,每次 `init_ui` 被调用时会累积;解决方案是在 connect 前先 `.disconnect()`(如模板所示)。 -- `QRadioButton.toggled` 信号在两个按钮上都会触发,**必须**用 `if not checked: return` 短路,否则会导致切换时状态错乱。 -- `self.current_model` 会在面板切换到"使用当前模型"时清空,防止用户忘记换回内置模式后仍使用旧导入模型。 -- 当前项目 venv 路径:`D:\111\office\ZHLduijie\1.WQ\WQ_GUI\venv`,导入 `joblib` 时注意 venv 环境一致性。 \ No newline at end of file diff --git a/.qwen/skills/wq_gui_frontend_scaffold/SKILL.md b/.qwen/skills/wq_gui_frontend_scaffold/SKILL.md deleted file mode 100644 index 5d26c65..0000000 --- a/.qwen/skills/wq_gui_frontend_scaffold/SKILL.md +++ /dev/null @@ -1,229 +0,0 @@ ---- -name: WQ_GUI 前端 Vue3 + Element Plus 脚手架 -description: WQ_GUI 项目 frontend/ 目录的 Vite + Vue 3 + TS + Element Plus 最小可运行脚手架,以及 useTaskPoller 与 Element Plus UI 的接线模式 -source: auto-skill -extracted_at: '2026-06-02T08:17:33.116Z' ---- - -# WQ_GUI 前端脚手架 (Vue 3 + Element Plus) - -## 适用场景 - -为 WQ_GUI FastAPI 后端 (`127.0.0.1:8000`) 搭建一个**最小可联调**的浏览器控制台。 -后端已暴露: - -- `POST /api/modeling/train` → `{ task_id, status, kind }` -- `POST /api/modeling/predict` → `{ task_id, status, kind }` -- `GET /api/tasks/{task_id}` → `TaskRecord`(含 PENDING/PROCESSING/SUCCESS/FAILED + 模型指标 / 输出路径) -- `GET /api/algorithms` → 算法清单 - -前端已有 (`frontend/src/`): - -- `api/request.ts`:axios 单例 + 响应拦截器自动 unwrap,baseURL 走 `VITE_API_BASE_URL` 缺省 `http://127.0.0.1:8000` -- `api/tasks.ts`:所有提交 / 查询函数 + 完整 `TaskRecord` / `TaskStatus` / `TaskKind` 类型 -- `composables/useTaskPoller.ts`:完整轮询 composable,支持 3 种用法(静态 / 响应式 taskId / 手动) - -## 1. 一次性补齐的脚手架文件 - -`frontend/` 初始状态**只有 `src/api` 和 `src/composables`**,缺整个 Vite 骨架。直接照下面这 7 个文件铺一遍: - -``` -frontend/ -├── .env.development # VITE_API_BASE_URL=http://127.0.0.1:8000 -├── .gitignore # node_modules / dist / .vite -├── env.d.ts # vite/client + ImportMeta + *.vue shim -├── index.html # 挂载 #app -├── package.json -├── tsconfig.json # 严格模式 + @ → src + bundler resolution -├── tsconfig.node.json # 给 vite.config.ts 用 -├── vite.config.ts # @ alias + 0.0.0.0:5173 -└── src/ - ├── main.ts - └── App.vue -``` - -### 锁定版本(2026-06 联调通过) - -```json -{ - "dependencies": { - "vue": "^3.4.27", - "element-plus": "^2.7.5", - "@element-plus/icons-vue": "^2.3.1", - "axios": "^1.7.2" - }, - "devDependencies": { - "@types/node": "^20.12.12", - "@vitejs/plugin-vue": "^5.0.4", - "typescript": "^5.4.5", - "vite": "^5.2.11", - "vue-tsc": "^2.0.19" - } -} -``` - -**`@types/node` 必加**——`vite.config.ts` 用了 `import { fileURLToPath, URL } from 'node:url'`,否则 `npm run build` 类型检查必挂。 - -### `tsconfig.json` 关键字段 - -- `"moduleResolution": "bundler"` -- `"allowImportingTsExtensions": true`(配合 `vue-tsc --noEmit`) -- `"paths": { "@/*": ["src/*"] }` + `"baseUrl": "."` -- `"include": ["src/**/*.vue"]`(`vue-tsc` 才会处理 SFC) -- `"references": [{ "path": "./tsconfig.node.json" }]` - -### `vite.config.ts` 关键字段 - -```ts -resolve: { - alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, -}, -server: { host: '0.0.0.0', port: 5173 }, -``` - -`0.0.0.0` 方便局域网真机调试;端口冲突时 `strictPort: false` 允许 Vite 自动 +1。 - ---- - -## 2. main.ts 模板(全量注册 Element Plus) - -```ts -import { createApp } from 'vue' -import ElementPlus from 'element-plus' -import 'element-plus/dist/index.css' -import * as ElementPlusIconsVue from '@element-plus/icons-vue' - -import App from './App.vue' - -const app = createApp(App) -app.use(ElementPlus) - -// 全量注册图标 () -for (const [name, component] of Object.entries(ElementPlusIconsVue)) { - app.component(name, component) -} -app.mount('#app') -``` - -联调期**全量注册最省事**;后期打包体积大再换 `unplugin-vue-components` 按需。 - ---- - -## 3. useTaskPoller 接线模式(双实例) - -训练 / 推断是**两条独立流水线**,各起一个 `useTaskPoller` 实例。核心套路:把 `task_id` 包成 `ref(null)`,composable 内部 `watch` 会**自动 start()**,无需手动调: - -```ts -import { ref, watch, computed } from 'vue' -import { submitTrain, submitPredict, type TaskRecord } from './api/tasks' -import { useTaskPoller } from './composables/useTaskPoller' - -// —— 训练 —— -const trainTaskId = ref(null) -const trainPoller = useTaskPoller(trainTaskId) // 传 ref 进去, 自动 watch - -async function onStartTrain() { - const { task_id } = await submitTrain({ ... }) - trainTaskId.value = task_id // 赋值后 watch 触发 start() -} - -// —— 推断 —— -const predictTaskId = ref(null) -const predictPoller = useTaskPoller(predictTaskId) -const modelId = ref('') - -// 训练一成功, model_id 自动填入推断输入框 -watch( - () => trainPoller.result.value?.model_id, - (newId) => { if (newId) modelId.value = newId }, -) - -async function onStartPredict() { - const { task_id } = await submitPredict({ model_id: modelId.value, ... }) - predictTaskId.value = task_id -} -``` - -**关键点**: - -- `trainPoller.result.value` 才是 SUCCESS 后的完整 `TaskRecord`;`record.value` 是任意时刻(含中间态)的最新记录。模板里同时展示用 `trainPoller.record.value ?? trainPoller.result.value`。 -- `poller.isPolling.value` / `poller.status.value` / `poller.error.value` / `poller.taskId.value` 都是 `Ref`,模板里必须用 `.value`(它们是嵌套 ref,**Vue 模板不会自动 unwrap**)。 - ---- - -## 4. el-progress 状态映射 - -`PollerStatus = 'idle' | 'PENDING' | 'PROCESSING' | 'SUCCESS' | 'FAILED'` -`el-progress` 的 `status` 接受 `'' | 'success' | 'warning' | 'exception'`。 - -```ts -function progressOf(status: string): number { - switch (status) { - case 'idle': - case 'PENDING': return 10 - case 'PROCESSING':return 60 - case 'SUCCESS': - case 'FAILED': return 100 - default: return 0 - } -} -function progressStatusOf(s: string): '' | 'success' | 'exception' { - if (s === 'SUCCESS') return 'success' - if (s === 'FAILED') return 'exception' - return '' -} -``` - -模板里 `v-if="poller.isPolling.value || poller.status.value === 'SUCCESS' || poller.status.value === 'FAILED'"` 控制展示。 - ---- - -## 5. CSS:深色控制台风(slate 渐变 + 卡片玻璃态) - -```css -.app-root { - min-height: 100vh; - background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%); - color: #e2e8f0; -} -.panel { - background: rgba(30, 41, 59, 0.7) !important; - border: 1px solid rgba(148, 163, 184, 0.18) !important; -} -.app-main { - display: grid; - grid-template-columns: 1fr 1fr; /* 左训练 / 右推断 */ - gap: 20px; -} -@media (max-width: 960px) { .app-main { grid-template-columns: 1fr; } } -``` - -深色背景下 Element Plus 的 `el-form-item__label` / `el-descriptions__label` 默认是黑色文字,必须 `:deep()` 覆盖成浅色。 - ---- - -## 6. 启动与验证 - -```bat -cd /d D:\111\office\ZHLduijie\1.WQ\WQ_GUI\frontend -npm install -npm run dev -``` - -打开 `http://127.0.0.1:5173/`,联调期望路径: - -1. 左侧「开始训练」→ 立即拿到 `task_id` + 黄色 `轮询中` + 进度条 60% -2. 后端 SUCCESS → 进度条变绿,下面出现 `model_id` 标签 + R²/RMSE/MAE -3. 右侧 `model_id` 被自动填入 → 「开始推断」→ 走 `output_zarr_path` 展示 -4. 任何一步 FAILED → 进度条变红 + 后端 `error` 字段 - ---- - -## 7. 已知 caveat - -- **第一次 `npm install` 约 150MB**,要耐心等。 -- `useTaskPoller` 已有 `onUnmounted` 自动清理,**不要再手写 `clearInterval`**。 -- `request.ts` 注释里写明 FastAPI dev 期 `allow_origins=["*"]`,**不需要配 Vite proxy**;如果未来后端收紧 CORS,再在 `vite.config.ts` 加 `server.proxy['/api']`。 -- `feature_start` 后端接受 `number | string`;el-input v-model 出来是 string,**直接传给 API 即可**,后端会自己判别。 -- `v-model` 绑 `ref(4)` 类型注解是必须的,否则 TS 会推断成 `Ref`,输入框失焦报错。 -- `@element-plus/icons-vue` 全量注册后用 `` 调,本期 App.vue 没用到但留着扩展位。 diff --git a/1.py b/1.py deleted file mode 100644 index e6c9144..0000000 --- a/1.py +++ /dev/null @@ -1,4 +0,0 @@ - - -new_wavelengths = [np.mean(wavelengths[i:i+3]) for i in range(0, len(wavelengths), 3)] -print(new_wavelengths) \ No newline at end of file diff --git a/data/sub/png/watermask.png b/data/sub/png/watermask.png deleted file mode 100644 index 860cd4cd20b0d2521ca487db0cd0246e59f152b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18802 zcmeIa2~d++w=Rs`t*D4-JAiyTgb;FH{O-BuJNMqN&i$*-U-j2t*DA^i?|ygoUV9DC zde+)JwzW1B-!HviL_|c~{OTWeA|gL$h=}a^x_1xo8=SdV0r20hP&>0rBDj9pdEmn@ zUcX!YE+SH$Cbs3i8~D60=<2Od5s`x(J3qU+1556Th?t9-|M9y+q|4%XTse&5uCNLB zkTms+f81Vw68`2V942m9>YB^G>ki&CdUviwU9g8sA69&S^~h0SeBz^5AJbhjR@ys*(?3+8Y_U3QWOf_x?S$;fRQclM#wz?Ri3KCTR2fd{jV|=1KY@?)bb^*f?VlXB1wv z<%B7$j zutWIKdJwR5JpFl1i|raxIqB}Ok%)-fypx^lV7h3M##k@sn%+hrL?#wqdbUUX&C8ny z&2LeUV%I|aP(3`2*$-673_5fo;&$Nk9{MNw3I)(}cQzogA`kO~9wWWFq`w{8C&=4A z8;&+{st;ZH^&69~E_7U%ey)|@<+MP31Efz-;b*yL^RQk^%B2^*q zgBe!8zpfb5s{6>OuWzo~rnbl1Y~9*ua}}VcU5B)j3oZ1n(T22$P;kbq2K|{YmQ-Xy z3P&_SBfdCD7IFPaml2aFi>5T-e7}6rcgQR}1A2}@cuQd_VC;~Fhp;$~qdOUwPZ^|Z zD%)xYGVUW$B-5J{W|P~mj!t~By#{CBGV>lZ*ahrnxrQjvPJFJV^^vvvDM=v?nj@Es z-{d)^m;g^nWsW5aAzt2j%{Ha}t(N;`+56-?(MUS&g$13*Mf*BCwx-6oxJ-H)j*hf z<>FJ`Sc^4;XYLQ-W_T~yX8&aE>?WlHk+{m+!V}z)@WUtzQ&$-NRG)=E?G3W0K?(Fc zgpfzH{la~7%DapR?GX^|Xn;bc=-e!^l;nE8GpFNu=H)h~V2bwcq?$!NJ1K8wL-KWg zx`qE5Cxd0p&wj)6Y_2h~Y#P)1!u?8naUi$Bc9a`BCIFU<%d5WRY5(Cvd|S+=t0&Id zK^zs`zVZiej0)>4Tm5pZyOow9fz%tf!LB-$B&zzg0el!N9O}=9;UcOVfa7(NI+6yd zW^8pd%atdOzmVKS=zV*oa{hg6V#1SPYVD`F=1Fs!$s8Y~eKNiT8aU!Q=y{SJhV$pO z=HOosYN@&g4rl=9n!z5NC~i*b(QxQp|B0vqJs@_{qU}DwA^I(HWrxi&!UCWw>GL{`8`t4axU4GC%Y>h^o~WX zP=;JzB(=0yDWW)lC2Rv00aztEX%0A@ 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 T}$K54LJG^cq_I*Q( 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 BW=sG8 diff --git a/data/sub/waterindex.csv b/data/sub/waterindex.csv deleted file mode 100644 index 4a04128..0000000 --- a/data/sub/waterindex.csv +++ /dev/null @@ -1,46 +0,0 @@ -Formula_Name,Category,Formula,Reference -BGA_Am09KBBI,Phycocyanin (BGA_PC),(w686 - w658) / (w686 + w658),"Amin, R.; Zhou, J.; Gilerson, A.; Gross, B.; Moshary, F.; Ahmed, S.; Novel optical techniques for detecting and classifying toxic dinoflagellate Karenia brevis blooms using satellite imagery, Optics Express, 2009, 17, 11, 1-13." -BGA_Be162B643sub629,Phycocyanin (BGA_PC),w644 - w629,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538." -BGA_Be162B700sub601,Phycocyanin (BGA_PC),w700 - w601,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539." -BGA_Be162BsubPhy,Phycocyanin (BGA_PC),w715 - w615,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 540." -BGA_Be16FLHBlueRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w458 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538." -BGA_Be16FLHGreenRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w558 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539." -BGA_Be16FLHVioletRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w444 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538." -BGA_Be16MPI,Phycocyanin (BGA_PC),(w615 - w601) - (w644 - w601),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539." -BGA_Be16NDPhyI,Phycocyanin (BGA_PC),(w700 - w622) / (w700 + w622),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 540." -BGA_Be16NDPhyI644over615,Phycocyanin (BGA_PC),(w644 - w615) / (w644 + w615),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 541." -BGA_Be16NDPhyI644over629,Phycocyanin (BGA_PC),(w644 - w629) / (w644 + w629),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 542." -BGA_Be16Phy2BDA644over629,Phycocyanin (BGA_PC),w644 / w629,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 545." -BGA_Da052BDA,Phycocyanin (BGA_PC),w714 / w672,"Wynne, T. T., Stumpf, R. P., Tomlinson, M. C., Warner, R. A., Tester, P. A., Dyble, J.; Relating spectral shape to cyanobacterial blooms in the Laurentian Great Lakes. Int. J. Remote Sens., 2008, 29, 3665-3672." -BGA_Go04MCI,Phycocyanin (BGA_PC),w709 - w681 - (w753 - w681),"Gower, J.F.R.; Brown,L.; Borstad, G.A.; Observation of chlorophyll fluorescence in west coast waters of Canada using the MODIS satellite sensor. Can. J. Remote Sens., 2004, 30 (1), 17?5." -BGA_HU103BDA,Phycocyanin (BGA_PC),(((1 / w615) - (1 / w600)) - w725),"Hunter, P.D.; Tyler, A.N.; Willby, N.J.; Gilvear, D.J.; The spatial dynamics of vertical migration by Microcystis aeruginosa in a eutrophic shallow lake: A case study using high spatial resolution time-series airborne remote sensing. Limn. Oceanogr. 2008, 53, 2391-2406" -BGA_Ku15PhyCI,Phycocyanin (BGA_PC),(-1 * (W681 - W665 - (W709 - W665))),"Kudela, R.M., Palacios, S.L., Austerberry, D.C., Accorsi, E.K., Guild, L.S.; Application of hyperspectral remote sensing to cyanobacterial blooms in inland waters, Torres-Perez, J., 2015, Remote Sens. Environ., 2015, 167, 1-10." -BGA_Ku15SLH,Phycocyanin (BGA_PC),(w715 - w658) + (w715 - w658),"Kudela, R.M., Palacios, S.L., Austerberry, D.C., Accorsi, E.K., Guild, L.S.; Application of hyperspectral remote sensing to cyanobacterial blooms in inland waters, Torres-Perez, J., 2015, Remote Sens. Environ., 2015, 167, 1-11." -BGA_MI092BDA,Phycocyanin (BGA_PC),w700 / w600,"Mishra, S.; Mishra, D.R.; Schluchter, W. M., A novel algorithm for predicting PC concentrations in cyanobacteria: A proximal hyperspectral remote sensing approach. Remote Sens., 2009, 1, 758?75." -BGA_MM092BDA,Phycocyanin (BGA_PC),w724 / w600,"Mishra, S.; Mishra, D.R.; Schluchter, W. M., A novel algorithm for predicting PC concentrations in cyanobacteria: A proximal hyperspectral remote sensing approach. Remote Sens., 2009, 1, 758?76." -BGA_MM12NDCIalt,Phycocyanin (BGA_PC),(w700 - w658) / (w700 + w658),"Mishra, S.; Mishra, D.R.; A novel remote sensing algorithm to quantify phycocyanin in cyanobacterial algal blooms, Env. Res. Lett., 2014, 9 (11), DOI:10.1088/1748-9326/9/11/114003" -BGA_MM143BDAopt,Phycocyanin (BGA_PC),((1 / w629) - (1 / w659)) * w724,"Mishra, S.; Mishra, D.R.; A novel remote sensing algorithm to quantify phycocyanin in cyanobacterial algal blooms, Env. Res. Lett., 2014, 9 (11), DOI:10.1088/1748-9326/9/11/114004" -BGA_SI052BDA,Phycocyanin (BGA_PC),w709 / w620,"Simis, S. G. H.; Peters, S.W. M.; Gons, H. J.; Remote sensing of the cyanobacteria pigment phycocyanin in turbid inland water. Limn. Oceanogr., 2005, 50, 237?45" -BGA_SM122BDA,Phycocyanin (BGA_PC),w709 / w600,"Mishra, S. Remote sensing of cyanobacteria in turbid productive waters, PhD Dissertation. Mississippi State University, USA. 2012." -BGA_SY002BDA,Phycocyanin (BGA_PC),w650 / w625,"Schalles, J.; Yacobi, Y. Remote detection and seasonal patterns of phycocyanin, carotenoid and chlorophyll-a pigments in eutrophic waters. Archiv fur Hydrobiologie, Special Issues Advances in Limnology, 2000, 55,153?68" -BGA_Wy08CI,Phycocyanin (BGA_PC),(-1 * (W686 - W672 - (W715 - W672))),"Wynne, T. T., Stumpf, R. P., Tomlinson, M. C., Warner, R. A., Tester, P. A., Dyble, J.; Relating spectral shape to cyanobacterial blooms in the Laurentian Great Lakes. Int. J. Remote Sens., 2008, 29, 3665-3672." -Chl_Al10SABI,chlorophyll_a,(w857 - w644) / (w458 + w529),"Alawadi, F. Detection of surface algal blooms using the newly developed algorithm surface algal bloom index (SABI). Proc. SPIE 2010, 7825." -Chl_Am092Bsub,chlorophyll_a,w681 - w665,"Amin, R.; Zhou, J.; Gilerson, A.; Gross, B.; Moshary, F.; Ahmed, S. Novel optical techniques for detecting and classifying toxic dinoflagellate Karenia brevis blooms using satellite imagery. Opt. Express 2009, 17, 9126?144." -Chl_Be16FLHblue,chlorophyll_a,w529 - (w644 + (w458 - w644)),"Beck, R.A. and 22 others; Comparison of satellite reflectance algorithms for estimating chlorophyll-a in a temperate reservoir using coincident hyperspectral aircraft imagery and dense coincident surface observations, Remote Sens. Environ., 2016, 178, 15-30." -Chl_Be16FLHviolet,chlorophyll_a,w529 - (w644 + (w429 - w644)),"Beck, R.A. and 22 others; Comparison of satellite reflectance algorithms for estimating chlorophyll-a in a temperate reservoir using coincident hyperspectral aircraft imagery and dense coincident surface observations, Remote Sens. Environ., 2016, 178, 15-30." -Chl_Be16NDTIblue,chlorophyll_a,(w658 - w458) / (w658 + w458),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 543." -Chl_Be16NDTIviolet,chlorophyll_a,(w658 - w444) / (w658 + w444),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 544." -Chl_De933BDA,chlorophyll_a,w600 - w648 - w625,"Dekker, A.; Detection of the optical water quality parameters for eutrophic waters by high resolution remote sensing, Ph.D. thesis, 1993, Free University, Amsterdam." -Chl_Gi033BDA,chlorophyll_a,((1 / w672) - (1 / w715)) * w757,"Gitelson, A.A.; U. Gritz, and M. N. Merzlyak.; Relationships between leaf chlorophyll content and spectral reflectance and algorithms for non-destructive chlorophyll assessment in higher plant leaves. J. Plant Phys. 2003, 160, 271-282." -Chl_Kn07KIVU,chlorophyll_a,(w458 - w644) / w529,"Kneubuhler, M.; Frank T.; Kellenberger, T.W; Pasche N.; Schmid M.; Mapping chlorophyll-a in Lake Kivu with remote sensing methods. 2007, Proceedings of the Envisat Symposium 2007, Montreux, Switzerland 23?7 April 2007 (ESA SP-636, July 2007)." -Chl_MM12NDCI,chlorophyll_a,(w715 - w686) / (w715 + w686),"Mishra, S.; and Mishra, D.R. Normalized difference chlorophyll index: A novel model for remote estimation of chlorophyll-a concentration in turbid productive waters, Remote Sens. Environ., 2012, 117, 394-406" -Chl_Zh10FLH,chlorophyll_a,w686 - (w715 + (w672 - w751)),"Zhao, D.Z.; Xing, X.G.; Liu, Y.G.; Yang, J.H.; Wang, L. The relation of chlorophyll-a concentration with the reflectance peak near 700 nm in algae-dominated waters and sensitivity of fluorescence algorithms for detecting algal bloom. Int. J. Remote Sens. 2010, 31, 39-48" -Turb_Be16GreenPlusRedBothOverViolet,Turbidity,(w558 + w658) / w444,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538" -Turb_Be16RedOverViolet,Turbidity,w658 / w444,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539" -Turb_Bow06RedOverGreen,Turbidity,w658 / w558,"Bowers, D. G., and C. E. Binding. 2006. 闁炽儲缈籬e Optical Properties of Mineral Suspended Particles: A Review and Synthesis.?Estuarine Coastal and Shelf Science 67 (1?): 219?30. doi:10.1016/j.ecss.2005.11.010" -Turb_Chip09NIROverGreen,Turbidity,w857 / w558,"Chipman, J. W.; Olmanson, L.G.; Gitelson, A.A.; Remote sensing methods for lake management: A guide for resource managers and decision-makers. 2009." -Turb_Dox02NIRoverRed,Turbidity,w857 / w658,"Doxaran, D., Froidefond, J.-M.; Castaing, P. ; A reflectance band ratio used to estimate suspended matter concentrations in sediment-dominated coastal waters, Remote Sens., 2002, 23, 5079-5085" -Turb_Frohn09GreenPlusRedBothOverBlue,Turbidity,(w558 + w658) / w458,"Frohn, R. C., & Autrey, B. C. (2009). Water quality assessment in the Ohio River using new indices for turbidity and chlorophyll-a with Landsat-7 Imagery. Draft Internal Report, US Environmental Protection Agency." -Turb_Harr92NIR,Turbidity,w857,"Schiebe F.R., Harrington J.A., Ritchie J.C. Remote-Sensing of Suspended Sediments闁炽儲鏁刪e Lake Chicot, Arkansas Project. Int. J. Remote Sens. 1992;13:1487?509" -Turb_Lath91RedOverBlue,Turbidity,w658 / w458,"Lathrop, R. G., Jr., T. M. Lillesand, and B. S. Yandell, 1991. Testing the utility of simple multi-date Thematic Mapper calibration algorithms for monitoring turbid inland waters. International Journal of Remote Sensing" -Turb_Moore80Red,Turbidity,w658,"Moore, G.K., Satellite remote sensing of water turbidity, Hydrological Sciences, 1980, 25, 4, 407-422" diff --git a/data/sub/waterindex.xlsx b/data/sub/waterindex.xlsx deleted file mode 100644 index bf1d6e219aafcaf5355c1ab635a22f01e8c1b044..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13596 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 diff --git a/data/sub/waterindex1125.csv b/data/sub/waterindex1125.csv deleted file mode 100644 index 7fa5dc4..0000000 --- a/data/sub/waterindex1125.csv +++ /dev/null @@ -1,46 +0,0 @@ -Formula_Name,Category,Formula,Reference -BGA_Am09KBBI,Phycocyanin (BGA_PC),(w686 - w658) / (w686 + w658),"Amin, R.; Zhou, J.; Gilerson, A.; Gross, B.; Moshary, F.; Ahmed, S.; Novel optical techniques for detecting and classifying toxic dinoflagellate Karenia brevis blooms using satellite imagery, Optics Express, 2009, 17, 11, 1-13." -BGA_Be162B643sub629,Phycocyanin (BGA_PC),w644 - w629,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538." -BGA_Be162B700sub601,Phycocyanin (BGA_PC),w700 - w601,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539." -BGA_Be162BsubPhy,Phycocyanin (BGA_PC),w715 - w615,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 540." -BGA_Be16FLHBlueRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w458 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538." -BGA_Be16FLHGreenRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w558 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539." -BGA_Be16FLHVioletRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w444 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538." -BGA_Be16MPI,Phycocyanin (BGA_PC),(w615 - w601) - (w644 - w601),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539." -BGA_Be16NDPhyI,Phycocyanin (BGA_PC),(w700 - w622) / (w700 + w622),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 540." -BGA_Be16NDPhyI644over615,Phycocyanin (BGA_PC),(w644 - w615) / (w644 + w615),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 541." -BGA_Be16NDPhyI644over629,Phycocyanin (BGA_PC),(w644 - w629) / (w644 + w629),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 542." -BGA_Be16Phy2BDA644over629,Phycocyanin (BGA_PC),w644 / w629,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 545." -BGA_Da052BDA,Phycocyanin (BGA_PC),w714 / w672,"Wynne, T. T., Stumpf, R. P., Tomlinson, M. C., Warner, R. A., Tester, P. A., Dyble, J.; Relating spectral shape to cyanobacterial blooms in the Laurentian Great Lakes. Int. J. Remote Sens., 2008, 29, 3665-3672." -BGA_Go04MCI,Phycocyanin (BGA_PC),w709 - w681 - (w753 - w681),"Gower, J.F.R.; Brown,L.; Borstad, G.A.; Observation of chlorophyll fluorescence in west coast waters of Canada using the MODIS satellite sensor. Can. J. Remote Sens., 2004, 30 (1), 17?5." -BGA_HU103BDA,Phycocyanin (BGA_PC),(((1 / w615) - (1 / w600)) - w725),"Hunter, P.D.; Tyler, A.N.; Willby, N.J.; Gilvear, D.J.; The spatial dynamics of vertical migration by Microcystis aeruginosa in a eutrophic shallow lake: A case study using high spatial resolution time-series airborne remote sensing. Limn. Oceanogr. 2008, 53, 2391-2406" -BGA_Ku15PhyCI,Phycocyanin (BGA_PC),-1 * (W681 - W665 - (W709 - W665)),"Kudela, R.M., Palacios, S.L., Austerberry, D.C., Accorsi, E.K., Guild, L.S.; Application of hyperspectral remote sensing to cyanobacterial blooms in inland waters, Torres-Perez, J., 2015, Remote Sens. Environ., 2015, 167, 1-10." -BGA_Ku15SLH,Phycocyanin (BGA_PC),(w715 - w658) + (w715 - w658),"Kudela, R.M., Palacios, S.L., Austerberry, D.C., Accorsi, E.K., Guild, L.S.; Application of hyperspectral remote sensing to cyanobacterial blooms in inland waters, Torres-Perez, J., 2015, Remote Sens. Environ., 2015, 167, 1-11." -BGA_MI092BDA,Phycocyanin (BGA_PC),w700 / w600,"Mishra, S.; Mishra, D.R.; Schluchter, W. M., A novel algorithm for predicting PC concentrations in cyanobacteria: A proximal hyperspectral remote sensing approach. Remote Sens., 2009, 1, 758?75." -BGA_MM092BDA,Phycocyanin (BGA_PC),w724 / w600,"Mishra, S.; Mishra, D.R.; Schluchter, W. M., A novel algorithm for predicting PC concentrations in cyanobacteria: A proximal hyperspectral remote sensing approach. Remote Sens., 2009, 1, 758?76." -BGA_MM12NDCIalt,Phycocyanin (BGA_PC),(w700 - w658) / (w700 + w658),"Mishra, S.; Mishra, D.R.; A novel remote sensing algorithm to quantify phycocyanin in cyanobacterial algal blooms, Env. Res. Lett., 2014, 9 (11), DOI:10.1088/1748-9326/9/11/114003" -BGA_MM143BDAopt,Phycocyanin (BGA_PC),((1 / w629) - (1 / w659)) * w724,"Mishra, S.; Mishra, D.R.; A novel remote sensing algorithm to quantify phycocyanin in cyanobacterial algal blooms, Env. Res. Lett., 2014, 9 (11), DOI:10.1088/1748-9326/9/11/114004" -BGA_SI052BDA,Phycocyanin (BGA_PC),w709 / w620,"Simis, S. G. H.; Peters, S.W. M.; Gons, H. J.; Remote sensing of the cyanobacteria pigment phycocyanin in turbid inland water. Limn. Oceanogr., 2005, 50, 237?45" -BGA_SM122BDA,Phycocyanin (BGA_PC),w709 / w600,"Mishra, S. Remote sensing of cyanobacteria in turbid productive waters, PhD Dissertation. Mississippi State University, USA. 2012." -BGA_SY002BDA,Phycocyanin (BGA_PC),w650 / w625,"Schalles, J.; Yacobi, Y. Remote detection and seasonal patterns of phycocyanin, carotenoid and chlorophyll-a pigments in eutrophic waters. Archiv fur Hydrobiologie, Special Issues Advances in Limnology, 2000, 55,153?68" -BGA_Wy08CI,Phycocyanin (BGA_PC),-1 * (W686 - W672 - (W715 - W672)),"Wynne, T. T., Stumpf, R. P., Tomlinson, M. C., Warner, R. A., Tester, P. A., Dyble, J.; Relating spectral shape to cyanobacterial blooms in the Laurentian Great Lakes. Int. J. Remote Sens., 2008, 29, 3665-3672." -Chl_Al10SABI,chlorophyll_a,(w857 - w644) / (w458 + w529),"Alawadi, F. Detection of surface algal blooms using the newly developed algorithm surface algal bloom index (SABI). Proc. SPIE 2010, 7825." -Chl_Am092Bsub,chlorophyll_a,w681 - w665,"Amin, R.; Zhou, J.; Gilerson, A.; Gross, B.; Moshary, F.; Ahmed, S. Novel optical techniques for detecting and classifying toxic dinoflagellate Karenia brevis blooms using satellite imagery. Opt. Express 2009, 17, 9126?144." -Chl_Be16FLHblue,chlorophyll_a,w529 - (w644 + (w458 - w644)),"Beck, R.A. and 22 others; Comparison of satellite reflectance algorithms for estimating chlorophyll-a in a temperate reservoir using coincident hyperspectral aircraft imagery and dense coincident surface observations, Remote Sens. Environ., 2016, 178, 15-30." -Chl_Be16FLHviolet,chlorophyll_a,w529 - (w644 + (w429 - w644)),"Beck, R.A. and 22 others; Comparison of satellite reflectance algorithms for estimating chlorophyll-a in a temperate reservoir using coincident hyperspectral aircraft imagery and dense coincident surface observations, Remote Sens. Environ., 2016, 178, 15-30." -Chl_Be16NDTIblue,chlorophyll_a,(w658 - w458) / (w658 + w458),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 543." -Chl_Be16NDTIviolet,chlorophyll_a,(w658 - w444) / (w658 + w444),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 544." -Chl_De933BDA,chlorophyll_a,w600 - w648 - w625,"Dekker, A.; Detection of the optical water quality parameters for eutrophic waters by high resolution remote sensing, Ph.D. thesis, 1993, Free University, Amsterdam." -Chl_Gi033BDA,chlorophyll_a,((1 / w672) - (1 / w715)) * w757,"Gitelson, A.A.; U. Gritz, and M. N. Merzlyak.; Relationships between leaf chlorophyll content and spectral reflectance and algorithms for non-destructive chlorophyll assessment in higher plant leaves. J. Plant Phys. 2003, 160, 271-282." -Chl_Kn07KIVU,chlorophyll_a,(w458 - w644) / w529,"Kneubuhler, M.; Frank T.; Kellenberger, T.W; Pasche N.; Schmid M.; Mapping chlorophyll-a in Lake Kivu with remote sensing methods. 2007, Proceedings of the Envisat Symposium 2007, Montreux, Switzerland 23?7 April 2007 (ESA SP-636, July 2007)." -Chl_MM12NDCI,chlorophyll_a,(w715 - w686) / (w715 + w686),"Mishra, S.; and Mishra, D.R. Normalized difference chlorophyll index: A novel model for remote estimation of chlorophyll-a concentration in turbid productive waters, Remote Sens. Environ., 2012, 117, 394-406" -Chl_Zh10FLH,chlorophyll_a,w686 - (w715 + (w672 - w751)),"Zhao, D.Z.; Xing, X.G.; Liu, Y.G.; Yang, J.H.; Wang, L. The relation of chlorophyll-a concentration with the reflectance peak near 700 nm in algae-dominated waters and sensitivity of fluorescence algorithms for detecting algal bloom. Int. J. Remote Sens. 2010, 31, 39-48" -Turb_Be16GreenPlusRedBothOverViolet,Turbidity,(w558 + w658) / w444,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538" -Turb_Be16RedOverViolet,Turbidity,w658 / w444,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539" -Turb_Bow06RedOverGreen,Turbidity,w658 / w558,"Bowers, D. G., and C. E. Binding. 2006. 鈥淭he Optical Properties of Mineral Suspended Particles: A Review and Synthesis.?Estuarine Coastal and Shelf Science 67 (1?): 219?30. doi:10.1016/j.ecss.2005.11.010" -Turb_Chip09NIROverGreen,Turbidity,w857 / w558,"Chipman, J. W.; Olmanson, L.G.; Gitelson, A.A.; Remote sensing methods for lake management: A guide for resource managers and decision-makers. 2009." -Turb_Dox02NIRoverRed,Turbidity,w857 / w658,"Doxaran, D., Froidefond, J.-M.; Castaing, P. ; A reflectance band ratio used to estimate suspended matter concentrations in sediment-dominated coastal waters, Remote Sens., 2002, 23, 5079-5085" -Turb_Frohn09GreenPlusRedBothOverBlue,Turbidity,(w558 + w658) / w458,"Frohn, R. C., & Autrey, B. C. (2009). Water quality assessment in the Ohio River using new indices for turbidity and chlorophyll-a with Landsat-7 Imagery. Draft Internal Report, US Environmental Protection Agency." -Turb_Harr92NIR,Turbidity,w857,"Schiebe F.R., Harrington J.A., Ritchie J.C. Remote-Sensing of Suspended Sediments鈥攖he Lake Chicot, Arkansas Project. Int. J. Remote Sens. 1992;13:1487?509" -Turb_Lath91RedOverBlue,Turbidity,w658 / w458,"Lathrop, R. G., Jr., T. M. Lillesand, and B. S. Yandell, 1991. Testing the utility of simple multi-date Thematic Mapper calibration algorithms for monitoring turbid inland waters. International Journal of Remote Sensing" -Turb_Moore80Red,Turbidity,w658,"Moore, G.K., Satellite remote sensing of water turbidity, Hydrological Sciences, 1980, 25, 4, 407-422" diff --git a/new/app/api/_smoke_test_train.py b/new/app/api/_smoke_test_train.py deleted file mode 100644 index 63404b0..0000000 --- a/new/app/api/_smoke_test_train.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -冒烟测试 _run_train_sync: 用合成数据走通真实训练管线。 -不依赖 FastAPI / xarray / dask, 只验训练 + 持久化 + 回测。 -""" -import sys -import tempfile -from pathlib import Path - -import numpy as np -import pandas as pd - -# 绕过 main.py 触发 app 包导入(只导入 modeling 模块) -# 当前文件位于 new/app/api/_smoke_test_train.py -# app 包在 new/app/__init__.py, 故 new/ 必须在 sys.path 上 -sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from app.api.modeling import ( - _get_model_pipeline, - _load_train_df, - _resolve_feature_start, - _run_train_sync, - _MODEL_CLASS_REGISTRY, -) - - -def make_synthetic_csv(n_samples: int = 200, n_features: int = 8, noise: float = 0.1, seed: int = 42) -> Path: - """生成 [lat, lon, target, lat2, lon2, feat_0, feat_1, ...] 布局的 CSV""" - rng = np.random.default_rng(seed) - lat = rng.uniform(20, 25, n_samples) - lon = rng.uniform(110, 115, n_samples) - target = rng.uniform(0, 50, n_samples) - lat2 = rng.uniform(0, 1, n_samples) # 元数据 - lon2 = rng.uniform(0, 1, n_samples) # 元数据 - feats = rng.normal(0, 1, (n_samples, n_features)) - # 让 y 真正依赖前 3 个特征, RF 至少应该能学到 R² > 0.5 - feats[:, 0] += target / 10 - feats[:, 1] += target / 20 - feats[:, 2] -= target / 15 - - df = pd.DataFrame({ - "lat": lat, - "lon": lon, - "Chl-a": target, - "lat2": lat2, - "lon2": lon2, - **{f"feat_{i}": feats[:, i] for i in range(n_features)}, - }) - tmp = Path(tempfile.mkdtemp()) / "train.csv" - df.to_csv(tmp, index=False) - return tmp - - -def test_load_train_df(): - print("== test_load_train_df ==") - p = make_synthetic_csv(n_samples=50) - df = _load_train_df(str(p)) - assert df.shape == (50, 5 + 8), f"shape={df.shape}" - print(f" shape={df.shape}, columns[:6]={list(df.columns[:6])}") - print(" PASS") - - -def test_resolve_feature_start_int_and_str(): - print("== test_resolve_feature_start (int + str) ==") - p = make_synthetic_csv() - df = _load_train_df(str(p)) - idx_int = _resolve_feature_start(df, 5) - idx_str = _resolve_feature_start(df, "feat_0") - assert idx_int == 5 == idx_str, f"int={idx_int}, str={idx_str}" - print(f" int(5) -> {idx_int}, str('feat_0') -> {idx_str}") - print(" PASS") - - -def test_resolve_feature_start_str_miss(): - print("== test_resolve_feature_start (str 不存在 -> 抛错) ==") - p = make_synthetic_csv() - df = _load_train_df(str(p)) - try: - _resolve_feature_start(df, "not_exist") - print(" FAIL: 应抛 ValueError") - except ValueError as e: - print(f" 正确抛 ValueError: {e}") - print(" PASS") - - -def test_get_model_pipeline_all_types(): - print("== test_get_model_pipeline (5 种 model_type) ==") - for mt in ["RF", "SVR", "LinearRegression", "KNN", "PLS"]: - p = _get_model_pipeline(mt, {}) - assert len(p.steps) == 2 - assert p.steps[0][0] == "scaler" - assert p.steps[1][0] == "model" - print(f" 全部通过: {list(_MODEL_CLASS_REGISTRY)}") - print(" PASS") - - -def test_get_model_pipeline_bad_type(): - print("== test_get_model_pipeline (坏 model_type) ==") - try: - _get_model_pipeline("XGBoost", {}) - print(" FAIL: 应抛 ValueError") - except ValueError as e: - print(f" 正确抛 ValueError: {e}") - print(" PASS") - - -def test_run_train_sync_rf_end_to_end(): - print("== test_run_train_sync (RF 端到端) ==") - p = make_synthetic_csv(n_samples=200) - out_dir = Path(tempfile.mkdtemp()) - out_path = out_dir / "model.joblib" - - import time - t0 = time.time() - metadata = _run_train_sync( - model_type="RF", - target="Chl-a", - train_data_path=str(p), - feature_start=5, - params={"n_estimators": 30, "max_depth": 6, "random_state": 42, "n_jobs": 1}, - output_model_path=out_path, - ) - dt = time.time() - t0 - - assert out_path.exists(), f"joblib 未落盘: {out_path}" - print(f" joblib 落盘: {out_path} ({out_path.stat().st_size} bytes)") - print(f" metadata.test_r2={metadata['test_r2']:.4f} test_rmse={metadata['test_rmse']:.4f} test_mae={metadata['test_mae']:.4f}") - print(f" metadata.n_features={metadata['n_features']} n_samples={metadata['n_samples']} train_size={metadata['train_size']} test_size={metadata['test_size']}") - print(f" 耗时 {dt:.2f}s") - - # 回测: 加载 joblib 再 predict - import joblib - saved = joblib.load(out_path) - assert "model" in saved and "metadata" in saved, f"joblib 双 key 缺失: {saved.keys()}" - assert hasattr(saved["model"], "predict") - assert saved["metadata"]["test_r2"] == metadata["test_r2"] - print(f" joblib 加载 OK, 含 'model' 和 'metadata' 双 key") - print(" PASS") - - -def test_run_train_sync_linearregression_fast(): - print("== test_run_train_sync (LinearRegression 快速路径) ==") - p = make_synthetic_csv(n_samples=150) - out_path = Path(tempfile.mkdtemp()) / "lr.joblib" - metadata = _run_train_sync( - model_type="LinearRegression", - target="Chl-a", - train_data_path=str(p), - feature_start=5, - params={}, - output_model_path=out_path, - ) - print(f" test_r2={metadata['test_r2']:.4f} (LR 学到线性, R² 应 >= 0.4)") - assert metadata["test_r2"] > 0.3, f"LR test_r2={metadata['test_r2']} 太低, 数据生成可能有问题" - print(" PASS") - - -def test_run_train_sync_bad_csv(): - print("== test_run_train_sync (CSV 不存在) ==") - try: - _run_train_sync("RF", "Chl-a", "/no/such/path.csv", 5, {}, Path("/tmp/x.joblib")) - print(" FAIL: 应抛异常") - except (FileNotFoundError, ValueError) as e: - print(f" 正确抛 {type(e).__name__}: {e}") - print(" PASS") - - -def test_run_train_sync_bad_target(): - print("== test_run_train_sync (target 列不存在) ==") - p = make_synthetic_csv() - try: - _run_train_sync("RF", "NopeTarget", str(p), 5, {}, Path("/tmp/x.joblib")) - print(" FAIL: 应抛 ValueError") - except ValueError as e: - print(f" 正确抛 ValueError: {e}") - print(" PASS") - - -def test_run_train_sync_str_feature_start(): - print("== test_run_train_sync (feature_start 用列名) ==") - p = make_synthetic_csv() - out_path = Path(tempfile.mkdtemp()) / "str_fs.joblib" - metadata = _run_train_sync("RF", "Chl-a", str(p), "feat_0", {"n_estimators": 10}, out_path) - assert metadata["feature_start"] == "feat_0" - assert metadata["n_features"] == 8 - assert metadata["feature_columns"][0] == "feat_0" - print(f" 列名 'feat_0' 解析正确, n_features={metadata['n_features']}") - print(" PASS") - - -if __name__ == "__main__": - test_load_train_df() - test_resolve_feature_start_int_and_str() - test_resolve_feature_start_str_miss() - test_get_model_pipeline_all_types() - test_get_model_pipeline_bad_type() - test_run_train_sync_rf_end_to_end() - test_run_train_sync_linearregression_fast() - test_run_train_sync_bad_csv() - test_run_train_sync_bad_target() - test_run_train_sync_str_feature_start() - print("\n>>> ALL SMOKE TESTS PASSED") diff --git a/new/app/api/endpoints.py b/new/app/api/endpoints.py deleted file mode 100644 index 4a012f1..0000000 --- a/new/app/api/endpoints.py +++ /dev/null @@ -1,222 +0,0 @@ -""" -API 路由集合 -============ - -把业务接口统一收口到 APIRouter,再由 main.py 通过 include_router 挂载。 - -当前包含的接口: - GET /api/algorithms 列出已注册的所有去耀斑算法(供前端下拉框) - POST /api/process/deglint 提交去耀斑处理任务,立即返回 task_id - GET /api/tasks/{task_id} 查询指定任务的状态与结果 - -派发链: - POST /api/process/deglint - └─ BackgroundTasks.add_task(execute_glint_removal_task, ...) - └─ get_remover(method) 从注册表拿到算法类 - └─ remover.process(input_zarr, output_zarr, **params) -""" - -import traceback -import uuid -from datetime import datetime -from typing import Any, Dict - -from fastapi import APIRouter, BackgroundTasks, HTTPException -from pydantic import BaseModel, Field - -# 并发安全的任务状态存储(替代旧版的 MOCK_TASK_DB) -from app.core.task_store import get_task, set_task, update_task - -# 算法注册表 API -from app.core.algorithms import get_remover, list_removers - - -# --------------------------------------------------------------------------- -# 路由实例 -# --------------------------------------------------------------------------- -# prefix 不在此处设置,统一在 main.py 挂载时给定,便于将来按版本拆分 -# (例如 /api/v1、/api/v2 共存时复用同一个 router 对象)。 -# --------------------------------------------------------------------------- -router = APIRouter(tags=["deglint"]) - - -# --------------------------------------------------------------------------- -# 请求 / 响应数据模型 -# --------------------------------------------------------------------------- -class DeglintRequest(BaseModel): - """POST /api/process/deglint 的请求体""" - - method: str = Field( - ..., - description="去耀斑方法名称,必须是已注册算法,例如 'kutser' / 'goodman'", - examples=["kutser"], - ) - params: Dict[str, Any] = Field( - default_factory=dict, - description=( - "传递给算法 process() 的超参数字典,例如 " - "Kutser: {'band_lower': 773, 'band_oxy': 845, 'band_upper': 893}; " - "Goodman: {'band_ref': 750, 'band_diff': 640, 'A': 0.0, 'B': 0.0}" - ), - examples=[{"band_lower": 773, "band_oxy": 845, "band_upper": 893}], - ) - - -class TaskAcceptedResponse(BaseModel): - """提交任务成功后立即返回的响应""" - - task_id: str - status: str # 一定是 PENDING - - -class AlgorithmListResponse(BaseModel): - """GET /api/algorithms 的响应""" - - algorithms: list # 已注册算法名列表 - count: int # 算法总数 - - -# --------------------------------------------------------------------------- -# 后台任务执行器(真实派发链) -# --------------------------------------------------------------------------- -# 注意:这里使用 async def。 -# FastAPI / Starlette 的 BackgroundTasks 支持 async function, -# 会在响应返回后自动 await 它,不影响主请求链路。 -# --------------------------------------------------------------------------- -async def execute_glint_removal_task( - task_id: str, - method: str, - params: Dict[str, Any], -) -> None: - """ - 后台异步执行器:按 method 名字从注册表取出算法类,实例化并运行 process()。 - - 状态机: - PENDING -> PROCESSING -> SUCCESS - └──> FAILED(含 error / traceback) - """ - # 0. 安全检查:任务记录必须已存在(POST 阶段已写入) - record = await get_task(task_id) - if record is None: - print(f"[{task_id}] 任务不存在, 跳过") - return - - # 1. 状态推进到 PROCESSING - await update_task( - task_id, - status="PROCESSING", - updated_at=datetime.now().isoformat(), - ) - print(f"[{task_id}] 开始处理 method={method} params={params}") - - # 2. 临时硬编码 IO 路径(未来由数据管理层提供) - # TODO: 替换为真实的数据管理服务返回的 zarr 路径 - input_zarr_path = "./data/temp_in.zarr" - output_zarr_path = f"./data/{task_id}_out.zarr" - - try: - # 3. 按 method 名字从注册表取算法类并实例化 - # get_remover 找不到时会抛 KeyError,下面的 except 会兜住 - algorithm_cls = get_remover(method) - remover = algorithm_cls() - - # 4. 调用算法(注意 await,因为 BaseGlintRemover.process 是 async) - await remover.process(input_zarr_path, output_zarr_path, **params) - - # 5. 成功:写回结果路径与状态 - await update_task( - task_id, - status="SUCCESS", - output_zarr_path=output_zarr_path, - error=None, - updated_at=datetime.now().isoformat(), - ) - print(f"[{task_id}] 处理完成 -> SUCCESS, output={output_zarr_path}") - - except Exception as exc: # noqa: BLE001 顶层兜底,绝不让后台任务静默失败 - # 6. 失败:记录错误信息与堆栈,便于前端排查 - await update_task( - task_id, - status="FAILED", - output_zarr_path=None, - error=f"{type(exc).__name__}: {exc}", - traceback=traceback.format_exc(), - updated_at=datetime.now().isoformat(), - ) - print(f"[{task_id}] 处理失败 -> {type(exc).__name__}: {exc}") - - -# --------------------------------------------------------------------------- -# GET /algorithms -# --------------------------------------------------------------------------- -# 返回当前已注册的所有算法名,供前端动态渲染下拉框 / 选择器。 -# --------------------------------------------------------------------------- -@router.get("/algorithms", response_model=AlgorithmListResponse) -async def list_registered_algorithms() -> Dict[str, Any]: - """列出已注册的去耀斑算法。""" - names = list(list_removers().keys()) - return {"algorithms": names, "count": len(names)} - - -# --------------------------------------------------------------------------- -# POST /process/deglint -# --------------------------------------------------------------------------- -# 提交去耀斑处理任务。FastAPI 在函数返回后才会把响应发给前端, -# 因此通过 BackgroundTasks 把耗时操作丢到后台,接口本身立刻返回 task_id。 -# --------------------------------------------------------------------------- -@router.post("/process/deglint", response_model=TaskAcceptedResponse) -async def submit_deglint( - payload: DeglintRequest, - background_tasks: BackgroundTasks, -) -> Dict[str, Any]: - """提交一个去耀斑处理任务,并立即返回 task_id。""" - - # 1. 生成唯一任务 ID(UUID4 足以保证全局唯一性) - task_id = str(uuid.uuid4()) - - # 2. 在任务库中登记一条 PENDING 记录(并发安全) - # 注意:output_zarr_path / error / traceback 字段在执行过程中被填充 - await set_task( - task_id, - { - "task_id": task_id, - "method": payload.method, - "params": payload.params, - "status": "PENDING", - "output_zarr_path": None, - "error": None, - "traceback": None, - "created_at": datetime.now().isoformat(), - "updated_at": datetime.now().isoformat(), - }, - ) - - # 3. 把真实执行器丢到后台 - background_tasks.add_task( - execute_glint_removal_task, - task_id, - payload.method, - payload.params, - ) - - # 4. 立即返回 task_id 与 PENDING 状态 - return {"task_id": task_id, "status": "PENDING"} - - -# --------------------------------------------------------------------------- -# GET /tasks/{task_id} -# --------------------------------------------------------------------------- -# 前端轮询此接口获取任务状态。PENDING / PROCESSING 表示仍在跑, -# SUCCESS 表示成功(含 output_zarr_path),FAILED 表示失败(含 error / traceback)。 -# --------------------------------------------------------------------------- -@router.get("/tasks/{task_id}") -async def get_task_status(task_id: str) -> Dict[str, Any]: - """查询指定任务的当前状态与结果。""" - - record = await get_task(task_id) - if record is None: - # 找不到 task_id 通常意味着客户端拼错了 ID,或者记录已被清理 - raise HTTPException(status_code=404, detail=f"task_id 不存在: {task_id}") - - # 直接返回字典,FastAPI 会自动 JSON 序列化 - return record diff --git a/new/app/api/modeling.py b/new/app/api/modeling.py deleted file mode 100644 index 01ac34f..0000000 --- a/new/app/api/modeling.py +++ /dev/null @@ -1,847 +0,0 @@ -""" -app/api/modeling.py -=================== - -机器学习与水质反演相关的 API 路由。 - -接口(最终路径, 挂载后): - POST /api/modeling/train 提交模型训练任务, 立即返回 task_id - GET /api/modeling/models 列出已训练好的模型(未来从磁盘 joblib 读) - POST /api/modeling/predict 提交模型推断任务, 立即返回 task_id - -设计要点 --------- -- 训练 / 推断均为异步后台任务, 复用 app.core.task_store 的并发安全任务状态。 -- 模型元数据用模块级 _MODEL_REGISTRY 暂存(开发期内存存储), - 未来从磁盘 joblib 读时只需替换 list_trained_models() 内部实现即可。 -- /predict 已接入真实 sklearn + xarray + dask 流式推断: - * joblib.load 读模型(缺文件时降级为 Dummy RandomForestRegressor) - * xr.open_zarr 延迟打开影像, NaN 填 0 - * xr.apply_ufunc(dask="parallelized") 沿 (y, x) 逐 chunk 调 model.predict - * to_zarr(mode="w", compute=True) 流式写出, 内存峰值 ≈ 1 个 chunk -- /train 已接入真实 sklearn + pandas 训练管线: - * pd.read_csv 读结构化训练表(支持 [lat, lon, target_*, feature_*] 布局) - * 按 target 列 dropna 清洗;按 feature_start 索引/列名切分特征 - * sklearn Pipeline: StandardScaler -> {RF/SVR/LinearRegression/KNN/PLS} - * train_test_split(80/20) 划分, 计算 test_r2/rmse/mae - * joblib.dump({model, metadata}) 落盘 ./data/models/{model_id}.joblib - * 测试指标写回 TASK_STORE, 同时登记到 _MODEL_REGISTRY - 注: 旧版 SPXY / KS 划分留作未来扩展, 当前固定 random 划分 (test_size=0.2, random_state=42)。 -""" - -import asyncio -import shutil -import traceback -import uuid -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, List, Optional, Union - -import joblib -import numpy as np -import pandas as pd -import xarray as xr -from fastapi import APIRouter, BackgroundTasks, HTTPException, UploadFile, File -from pydantic import BaseModel, Field -from sklearn.cross_decomposition import PLSRegression -from sklearn.ensemble import RandomForestRegressor -from sklearn.linear_model import LinearRegression -from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score -from sklearn.model_selection import train_test_split -from sklearn.neighbors import KNeighborsRegressor -from sklearn.pipeline import Pipeline -from sklearn.preprocessing import StandardScaler -from sklearn.svm import SVR - -# 复用并发安全任务状态存储(与 deglint 共享同一份 TASK_STORE, -# 通过 task 记录里的 "kind" 字段区分 train / predict / deglint) -from app.core.task_store import get_task, set_task, update_task - - -# --------------------------------------------------------------------------- -# 路由实例 -# --------------------------------------------------------------------------- -# prefix="/modeling" 让本文件内只写 /train /models /predict 等短路径, -# 最终完整路径由 main.py 挂载时再补 /api。 -# --------------------------------------------------------------------------- -router = APIRouter(prefix="/modeling", tags=["modeling"]) - - -# --------------------------------------------------------------------------- -# 数据模型 -# --------------------------------------------------------------------------- -class TrainRequest(BaseModel): - """POST /api/modeling/train 的请求体""" - - model_type: str = Field( - ..., - description="模型类型, 例如 'RF' (随机森林) / 'SVR' (支持向量回归) / 'XGBoost' / 'MLP'", - examples=["RF", "SVR"], - ) - target: str = Field( - ..., - description="反演目标水质参数, 例如 'Chl-a' (叶绿素a) / 'TSS' (总悬浮物) / 'CDOM' (有色可溶有机物)", - examples=["Chl-a", "TSS", "CDOM"], - ) - train_data_path: str = Field( - ..., - description="训练数据集的 zarr 路径(包含 reflectance 变量与 target 标签)", - examples=["./data/train.zarr"], - ) - feature_start: Union[int, str] = Field( - default=4, - description=( - "特征列起始位置. 表格布局假定为 " - "[lat, lon, target_1, target_2, ..., feature_1, feature_2, ...] " - "可传 int 列索引(如 4)或 str 列名(如 '374.285' 波长起点)。" - "默认 4, 即前 4 列视为元数据/目标, 之后全部是特征。" - ), - examples=[4, "374.285"], - ) - params: Dict[str, Any] = Field( - default_factory=dict, - description="模型超参, 例如 RF 的 {'n_estimators': 100, 'max_depth': 20}", - examples=[{"n_estimators": 100, "max_depth": 20}], - ) - - -class PredictRequest(BaseModel): - """POST /api/modeling/predict 的请求体""" - - model_id: str = Field( - ..., - description="已训练模型的 ID(由 /api/modeling/train 返回或 /api/modeling/models 列出)", - ) - input_zarr_path: str = Field( - ..., - description="待推断影像的 zarr 路径", - examples=["./data/scene.zarr"], - ) - output_zarr_path: Optional[str] = Field( - default=None, - description=( - "输出 zarr 路径, 缺省时由后端按规则生成 " - "(如 ./data/{model_id}_{input_stem}_pred.zarr)" - ), - ) - - -class TaskAcceptedResponse(BaseModel): - """提交训练/推断任务后立即返回的响应""" - - task_id: str - status: str # 一定是 PENDING - kind: str # "train" / "predict", 便于前端识别任务类型 - - -class ModelInfo(BaseModel): - """单个模型的元信息(GET /api/modeling/models 的元素)""" - - model_id: str - model_type: str - target: str - params: Dict[str, Any] - path: str # joblib 文件路径 - created_at: str - train_task_id: str # 产生此模型的那个训练任务的 ID - - -class ModelListResponse(BaseModel): - """GET /api/modeling/models 的响应""" - - models: List[ModelInfo] - count: int - - -# --------------------------------------------------------------------------- -# 模块级模型注册表(开发期内存, 未来替换为磁盘扫描) -# --------------------------------------------------------------------------- -# model_id -> ModelInfo 字典 -# 读多写少, 用一个普通 dict 足够(CPython GIL 兜底)。 -# 写时(训练完成时)只发生一次, 无并发风险。 -# --------------------------------------------------------------------------- -_MODEL_REGISTRY: Dict[str, Dict[str, Any]] = {} - - -def _register_model(record: Dict[str, Any]) -> None: - """将训练完成的模型登记到内存注册表。""" - _MODEL_REGISTRY[record["model_id"]] = record - - -# --------------------------------------------------------------------------- -# 训练管线的模块级辅助函数 -# --------------------------------------------------------------------------- -# 设计要点 (与推断管线一致): -# 1) 模块级函数: dask / joblib 后端若走子进程 pickle, 嵌套闭包会丢字段。 -# 2) 同步执行: execute_train_task 用 asyncio.to_thread 派发, 内部全程同步阻塞。 -# 3) 失败抛异常: 异常由 execute_train_task 捕获, 转 FAILED + traceback。 -# --------------------------------------------------------------------------- - -# model_type (大写字符串) -> sklearn 估计器类 -# 与 OpenClaw model_configs 思路一致, 但此处只保留类 (参数由 params 透传) -_MODEL_CLASS_REGISTRY: Dict[str, type] = { - "RF": RandomForestRegressor, - "SVR": SVR, - "LinearRegression": LinearRegression, - "KNN": KNeighborsRegressor, - "PLS": PLSRegression, -} - - -def _get_model_pipeline(model_type: str, params: Optional[Dict[str, Any]]) -> Pipeline: - """ - 模型工厂: 按 model_type 选 sklearn 类, 用 StandardScaler + 估计器构造 Pipeline。 - - 与 OpenClaw 不同之处: 把 scaler 放进 Pipeline 第一步, - 推断时直接 pipeline.predict(X) 即可, scaler 参数与训练时严格一致。 - """ - model_cls = _MODEL_CLASS_REGISTRY.get(model_type) - if model_cls is None: - raise ValueError( - f"不支持的 model_type='{model_type}', " - f"可选: {sorted(_MODEL_CLASS_REGISTRY.keys())}" - ) - estimator = model_cls(**(params or {})) - return Pipeline([("scaler", StandardScaler()), ("model", estimator)]) - - -def _load_train_df(csv_path: str) -> pd.DataFrame: - """ - 读 CSV 训练表, 规整空串 / 空白 / NULL 等为 NaN。 - - 沿用 OpenClaw modeling_batch.load_data_batch 的读取策略: - na_values 显式列举 + 正则二次清理 (防 cell 内出现 " " 等纯空白)。 - """ - try: - df = pd.read_csv( - csv_path, - na_values=["", " ", "NaN", "nan", "NULL", "null"], - ) - except FileNotFoundError as exc: - raise FileNotFoundError(f"训练数据文件不存在: {csv_path}") from exc - except pd.errors.EmptyDataError as exc: - raise ValueError(f"训练数据文件为空: {csv_path}") from exc - # 二次清理: 残留的纯空白 cell - df = df.replace(r"^\s*$", np.nan, regex=True) - return df - - -def _resolve_feature_start( - df: pd.DataFrame, - feature_start: Union[int, str], -) -> int: - """ - 将 feature_start (int 索引 / str 列名) 统一解析为 int 列索引。 - - 与 OpenClaw modeling_batch.load_data_batch / load_data_single 一致: - str 走 columns.get_loc, int 直接返回。 - """ - if isinstance(feature_start, str): - if feature_start not in df.columns: - raise ValueError( - f"feature_start='{feature_start}' 不在 CSV 列中: {list(df.columns)}" - ) - return int(df.columns.get_loc(feature_start)) - return int(feature_start) - - -def _run_train_sync( - model_type: str, - target: str, - train_data_path: str, - feature_start: Union[int, str], - params: Optional[Dict[str, Any]], - output_model_path: Path, -) -> Dict[str, Any]: - """ - 完整同步训练流程 (由 execute_train_task 在线程池内调用): - - pd.read_csv -> 目标列 dropna -> 切特征 -> train_test_split(80/20) - -> Pipeline(StandardScaler + model).fit -> 评估 test_r2/rmse/mae - -> joblib.dump({model, metadata}, output_model_path) - - Returns: - metadata 字典, 含 test_r2 / test_rmse / test_mae / n_features 等, - 调用方负责写回 TASK_STORE 和 _MODEL_REGISTRY。 - - 注: 旧版 SPXY / KS 划分留作未来扩展 (params.split_method 控制), - 当前固定 random + test_size=0.2 + random_state=42。 - """ - df = _load_train_df(train_data_path) - - if target not in df.columns: - raise ValueError( - f"target='{target}' 不在 CSV 列中, 可选: {list(df.columns)}" - ) - - # 1) 清洗: 仅剔除 target NaN 的行 (与 OpenClaw load_data_single 一致) - df = df[df[target].notna()].copy() - if df.empty: - raise ValueError("target 剔除 NaN 后无样本, 终止训练") - - # 2) 特征切分 - feature_start_idx = _resolve_feature_start(df, feature_start) - feature_columns = list(df.columns[feature_start_idx:]) - - X = df.iloc[:, feature_start_idx:].astype(np.float64) - y = df[target].astype(np.float64).values - - # 3) 划分 (固定 random, 未来扩展 spxy/ks) - X_train, X_test, y_train, y_test = train_test_split( - X.values, - y, - test_size=0.2, - random_state=42, - ) - - # 4) 构造 Pipeline + 训练 - pipeline = _get_model_pipeline(model_type, params) - pipeline.fit(X_train, y_train) - - # 5) 测试集与训练集评估 - y_pred = pipeline.predict(X_test) - test_r2 = float(r2_score(y_test, y_pred)) - test_rmse = float(np.sqrt(mean_squared_error(y_test, y_pred))) - test_mae = float(mean_absolute_error(y_test, y_pred)) - - y_train_pred = pipeline.predict(X_train) - train_r2 = float(r2_score(y_train, y_train_pred)) - train_rmse = float(np.sqrt(mean_squared_error(y_train, y_train_pred))) - train_mae = float(mean_absolute_error(y_train, y_train_pred)) - - metadata: Dict[str, Any] = { - "model_type": model_type, - "target": target, - "feature_start": feature_start, - "feature_columns": feature_columns, - "n_features": int(X.shape[1]), - "n_samples": int(X.shape[0]), - "train_size": int(X_train.shape[0]), - "test_size": int(X_test.shape[0]), - "params": dict(params or {}), - "test_r2": test_r2, - "test_rmse": test_rmse, - "test_mae": test_mae, - "train_r2": train_r2, - "train_rmse": train_rmse, - "train_mae": train_mae, - "split_method": "random", - "trained_at": datetime.now().isoformat(), - } - - # 7) 持久化 (目录可能不存在, 顺手建) - output_model_path = Path(output_model_path) - output_model_path.parent.mkdir(parents=True, exist_ok=True) - joblib.dump( - {"model": pipeline, "metadata": metadata}, - output_model_path, - ) - - return metadata - - -# --------------------------------------------------------------------------- -# 推断管线的模块级辅助函数 -# --------------------------------------------------------------------------- -# 设计要点: -# 1) Dask 调度时, 函数必须可被工作进程 pickle 序列化。 -# 因此 _predict_block / _load_model / _make_dummy_model / _run_predict_sync -# 全部是模块级函数 (而非嵌套), 避免闭包陷阱。 -# 2) _predict_block 通过 model.predict(spectra_2d) 整批预测, -# 整张影像的 O(n_pixels * n_bands) 一次性预测在大矩阵上必 OOM, -# 因此外层用 xr.apply_ufunc(dask="parallelized") 把矩阵切块 -# 逐块进入此函数, 单次内存峰值 ≈ 1 个 (y_chunk, x_chunk, band) 大小。 -# --------------------------------------------------------------------------- - - -def _make_dummy_model(n_features: int) -> RandomForestRegressor: - """ - 构造一个 Dummy 随机森林回归器。 - - 用途: - 1) 真实 joblib 文件不存在时的连通性测试 - 2) 训练骨架尚未接入真实数据时的占位推断 - """ - rng = np.random.default_rng(42) - X = rng.random((200, n_features)) - y = rng.random(200) - model = RandomForestRegressor( - n_estimators=10, max_depth=5, random_state=0, n_jobs=1 - ) - model.fit(X, y) - return model - - -def _load_model(path: str, n_features: int) -> Any: - """ - 加载训练好的 sklearn 模型, 失败时降级 Dummy。 - - 优先级: - 1) path 存在且 joblib.load 成功 -> 返回真实模型 - 2) 否则 -> 降级为 Dummy 随机森林 (n_features 必须指定) - """ - p = Path(path) - if p.is_file() and p.stat().st_size > 0: - try: - print(f"[model] 从磁盘加载: {path}") - return joblib.load(path) - except Exception as exc: # noqa: BLE001 - print(f"[model] joblib.load 失败 ({type(exc).__name__}: {exc}), 降级 Dummy") - print(f"[model] 真实 joblib 不存在 ({path}), 使用 Dummy RandomForest") - return _make_dummy_model(n_features) - - -def _predict_block(spectra_3d: np.ndarray, model: Any) -> np.ndarray: - """ - 单个 dask chunk 的推断函数 (xr.apply_ufunc 会自动调度调用)。 - - Parameters - ---------- - spectra_3d : np.ndarray - 形状 (y_chunk, x_chunk, n_bands)。 - 此形状由 input_core_dims=[["band"]] 决定: - xarray 会把 band 维移到最后一轴, 然后按 (y, x) 的 chunk 切分调用本函数。 - model : 已 fit 好的 sklearn 估计器 - 接受 (n_samples, n_features) 输入, 返回 (n_samples,) 预测。 - - Returns - ------- - np.ndarray - 形状 (y_chunk, x_chunk), dtype float32 的标量预测图。 - """ - yc, xc, nb = spectra_3d.shape - # 2D 化: 每个像素一行光谱 - flat = spectra_3d.reshape(yc * xc, nb) - # sklearn 风格的批量预测 - pred = model.predict(flat) - # 还原为 2D 空间图, 强制 float32 节约一半内存 - return pred.reshape(yc, xc).astype(np.float32, copy=False) - - -def _run_predict_sync( - model: Any, - model_id: str, - input_zarr_path: str, - output_zarr_path: str, -) -> None: - """ - 同步推断主流程 (被 asyncio.to_thread 调用)。 - - 流程: - 1) xr.open_zarr 延迟打开 (dask 数组, 不一次性读入内存) - 2) NaN -> 0 清洗 (model.predict 不接受 NaN) - 3) xr.apply_ufunc 沿 (y, x) 逐 chunk 调 _predict_block - 4) 非水域置 NaN (zarr 支持 float NaN) - 5) to_zarr 触发整图计算 + 流式写出 - """ - # 1. 延迟打开输入 (关键: Dask 不一次性读入内存) - ds = xr.open_zarr(input_zarr_path, chunks="auto") - if "reflectance" not in ds.data_vars: - raise KeyError( - f"输入 zarr 缺少 'reflectance' 变量; 实际: {list(ds.data_vars)}" - ) - - reflectance = ds["reflectance"] # dims: (y, x, band) - n_bands = reflectance.sizes["band"] - - # 2. 水域掩膜 (与去耀斑算法同约定) - if "water_mask" in ds.data_vars or "water_mask" in ds.coords: - water_mask = ds["water_mask"].astype(bool) - else: - water_mask = xr.ones_like(reflectance.isel(band=0), dtype=bool) - - # 3. NaN 清洗: 填充 0 (model.predict 不接受 NaN) - refl_clean = reflectance.fillna(0.0) - - # 4. 核心: 用 apply_ufunc 把 model.predict 沿 (y, x) 应用 - # dask="parallelized" 让每个 (y_chunk, x_chunk, band) chunk - # 独立调 _predict_block, 任意时刻内存中只有若干个 chunk。 - prediction: xr.DataArray = xr.apply_ufunc( - _predict_block, - refl_clean, - kwargs={"model": model}, - input_core_dims=[["band"]], - output_core_dims=[[]], - dask="parallelized", - output_dtypes=[np.float32], - dask_gufunc_kwargs={"allow_rechunk": True}, - vectorize=False, - ) - - # 5. 非水域置 NaN (zarr 支持 float NaN, 便于后续可视化/掩膜分析) - prediction = prediction.where(water_mask, np.nan) - - # 6. 包装为 Dataset 并流式写出 - out = xr.Dataset( - {"prediction": prediction}, - attrs={ - "model_id": model_id, - "input_zarr_path": input_zarr_path, - "n_bands": n_bands, - "created_at": datetime.now().isoformat(), - }, - ) - # 保留 y/x 坐标 - out = out.assign_coords(y=ds["y"], x=ds["x"]) - - # to_zarr + compute=True 触发整图 dask 图求值 - # 中间会按 chunk 逐块调度到线程池, 内存峰值 ≈ 1 个 chunk 的体量 - out.to_zarr(output_zarr_path, mode="w", compute=True) - - -# --------------------------------------------------------------------------- -# 后台任务执行器 -# --------------------------------------------------------------------------- -async def execute_train_task( - task_id: str, - model_type: str, - target: str, - train_data_path: str, - feature_start: Union[int, str], - params: Dict[str, Any], -) -> None: - """ - 训练任务后台执行器(已接入真实 sklearn 训练流程)。 - - 流程: - 1) get_task 校验任务存在 - 2) update_task(PROCESSING) - 3) 生成 model_id / model_path - 4) asyncio.to_thread 派发 _run_train_sync 到默认线程池 - 5) 成功 -> _register_model + update_task(SUCCESS, 附 test_r2/rmse/mae) - 6) 失败 -> update_task(FAILED, 附 error + traceback) - """ - record = await get_task(task_id) - if record is None: - print(f"[{task_id}] 训练任务不存在, 跳过") - return - - await update_task( - task_id, - status="PROCESSING", - updated_at=datetime.now().isoformat(), - ) - print( - f"[{task_id}] 开始训练 model_type={model_type} target={target} " - f"train_data_path={train_data_path} feature_start={feature_start}" - ) - - # model_id 用 uuid4 前 12 位 (8 位易撞, 12 位兼顾可读性) - model_id = f"model_{uuid.uuid4().hex[:12]}" - model_path = Path(f"./data/models/{model_id}.joblib") - - try: - # 同步 sklearn / pandas 训练丢到默认线程池, 不阻塞 event loop - metadata = await asyncio.to_thread( - _run_train_sync, - model_type, - target, - train_data_path, - feature_start, - params, - model_path, - ) - - # 登记到内存注册表 (供 /predict 查 model_id) - _register_model( - { - "model_id": model_id, - "model_type": model_type, - "target": target, - "params": dict(params or {}), - "path": str(model_path), - "feature_start": feature_start, - "n_features": metadata["n_features"], - "test_r2": metadata["test_r2"], - "test_rmse": metadata["test_rmse"], - "test_mae": metadata["test_mae"], - "created_at": datetime.now().isoformat(), - "train_task_id": task_id, - } - ) - - # 把训练指标写回任务记录, 前端轮询时可直接看 - await update_task( - task_id, - status="SUCCESS", - model_id=model_id, - model_path=str(model_path), - test_r2=metadata["test_r2"], - test_rmse=metadata["test_rmse"], - test_mae=metadata["test_mae"], - n_features=metadata["n_features"], - n_samples=metadata["n_samples"], - error=None, - traceback=None, - updated_at=datetime.now().isoformat(), - ) - print( - f"[{task_id}] 训练完成 -> model_id={model_id} " - f"test_r2={metadata['test_r2']:.4f} test_rmse={metadata['test_rmse']:.4f}" - ) - - except Exception as exc: # noqa: BLE001 - # 失败时 model_path 不一定有产物, 显式置 None 方便前端判断 - await update_task( - task_id, - status="FAILED", - model_id=None, - model_path=None, - error=f"{type(exc).__name__}: {exc}", - traceback=traceback.format_exc(), - updated_at=datetime.now().isoformat(), - ) - print(f"[{task_id}] 训练失败 -> {type(exc).__name__}: {exc}") - - -async def execute_predict_task( - task_id: str, - model_id: str, - input_zarr_path: str, - output_zarr_path: Optional[str], -) -> None: - """ - 推断任务后台执行器(真实实现版)。 - - OOM 防护策略: - - xr.open_zarr(..., chunks="auto") 延迟打开, 整图不一次性读入内存 - - xr.apply_ufunc(..., dask="parallelized") 把影像按 chunk 切分 - - 每个 chunk 内部 reshape 成 2D, 调 model.predict, 再 reshape 回 2D - - 任意时刻内存峰值 ≈ 1 个 (y_chunk, x_chunk, band) chunk 的体量 - - 整图完成计算后再 to_zarr(compute=True) 流式写出 - """ - record = await get_task(task_id) - if record is None: - print(f"[{task_id}] 推断任务不存在, 跳过") - return - - # 1. 校验 model_id 是否已注册 (避免在后台任务里报模糊错误) - model_meta = _MODEL_REGISTRY.get(model_id) - if model_meta is None: - await update_task( - task_id, - status="FAILED", - error=f"model_id 不存在: {model_id}", - updated_at=datetime.now().isoformat(), - ) - print(f"[{task_id}] 推断失败 -> model_id 不存在: {model_id}") - return - - # 2. 自动生成 output_zarr_path (若未提供) - if output_zarr_path is None: - stem = input_zarr_path.rstrip("/\\").split("/")[-1].split("\\")[-1] - stem = stem.replace(".zarr", "") - output_zarr_path = f"./data/{model_id}_{stem}_pred.zarr" - - await update_task( - task_id, - status="PROCESSING", - updated_at=datetime.now().isoformat(), - ) - print(f"[{task_id}] 开始推断 model_id={model_id} input={input_zarr_path}") - - try: - # 3. 探测波段数 (用于 Dummy 模型适配) - # 这里只读 zarr 元数据 (.zarray 的 shape), 不读真实数据 - ds_probe = xr.open_zarr(input_zarr_path, chunks="auto") - if "reflectance" not in ds_probe.data_vars: - raise KeyError( - f"输入 zarr 缺少 'reflectance' 变量; 实际: {list(ds_probe.data_vars)}" - ) - n_bands = ds_probe["reflectance"].sizes["band"] - ds_probe.close() - - # 4. 加载模型 (真实文件优先, Dummy 兜底) - model = _load_model(model_meta["path"], n_features=n_bands) - - # 5. 包装同步执行, 丢到线程池, 事件循环不阻塞 - await asyncio.to_thread( - _run_predict_sync, - model, - model_id, - input_zarr_path, - output_zarr_path, - ) - - await update_task( - task_id, - status="SUCCESS", - output_zarr_path=output_zarr_path, - model_id=model_id, - error=None, - updated_at=datetime.now().isoformat(), - ) - print(f"[{task_id}] 推断完成 -> output={output_zarr_path}") - - except Exception as exc: # noqa: BLE001 - tb_text = traceback.format_exc() - await update_task( - task_id, - status="FAILED", - output_zarr_path=None, - error=f"{type(exc).__name__}: {exc}", - traceback=tb_text, - updated_at=datetime.now().isoformat(), - ) - print(f"[{task_id}] 推断失败 -> {type(exc).__name__}: {exc}") - print(tb_text) - - -# --------------------------------------------------------------------------- -# POST /api/modeling/train -# --------------------------------------------------------------------------- -@router.post("/train", response_model=TaskAcceptedResponse) -async def submit_train( - payload: TrainRequest, - background_tasks: BackgroundTasks, -) -> Dict[str, Any]: - """提交一个模型训练任务, 立即返回 task_id。""" - - task_id = str(uuid.uuid4()) - await set_task( - task_id, - { - "task_id": task_id, - "kind": "train", - "model_type": payload.model_type, - "target": payload.target, - "train_data_path": payload.train_data_path, - "feature_start": payload.feature_start, - "params": payload.params, - "status": "PENDING", - "model_id": None, - "model_path": None, - "test_r2": None, - "test_rmse": None, - "test_mae": None, - "n_features": None, - "n_samples": None, - "error": None, - "traceback": None, - "created_at": datetime.now().isoformat(), - "updated_at": datetime.now().isoformat(), - }, - ) - background_tasks.add_task( - execute_train_task, - task_id, - payload.model_type, - payload.target, - payload.train_data_path, - payload.feature_start, - payload.params, - ) - return {"task_id": task_id, "status": "PENDING", "kind": "train"} - - -# --------------------------------------------------------------------------- -# GET /api/modeling/models -# --------------------------------------------------------------------------- -@router.get("/models", response_model=ModelListResponse) -async def list_trained_models() -> Dict[str, Any]: - """ - 列出已训练好的模型。 - - 未来实现: 从 ./data/models/*.joblib 扫描元信息, - 当前直接从内存 _MODEL_REGISTRY 读。 - """ - models = list(_MODEL_REGISTRY.values()) - # 按 created_at 倒序, 最新训练的在前 - models.sort(key=lambda m: m.get("created_at", ""), reverse=True) - return {"models": models, "count": len(models)} - - -# --------------------------------------------------------------------------- -# POST /api/modeling/predict -# --------------------------------------------------------------------------- -@router.post("/predict", response_model=TaskAcceptedResponse) -async def submit_predict( - payload: PredictRequest, - background_tasks: BackgroundTasks, -) -> Dict[str, Any]: - """提交一个模型推断任务, 立即返回 task_id。""" - - task_id = str(uuid.uuid4()) - await set_task( - task_id, - { - "task_id": task_id, - "kind": "predict", - "model_id": payload.model_id, - "input_zarr_path": payload.input_zarr_path, - "output_zarr_path": payload.output_zarr_path, - "status": "PENDING", - "error": None, - "traceback": None, - "created_at": datetime.now().isoformat(), - "updated_at": datetime.now().isoformat(), - }, - ) - background_tasks.add_task( - execute_predict_task, - task_id, - payload.model_id, - payload.input_zarr_path, - payload.output_zarr_path, - ) - return {"task_id": task_id, "status": "PENDING", "kind": "predict"} - - -# --------------------------------------------------------------------------- -# models_router — 独立于 modeling_router,路径前缀为 /models -# 最终完整路径: GET /api/models, POST /api/models/upload -# --------------------------------------------------------------------------- -models_router = APIRouter(prefix="/models", tags=["models"]) - - -# --------------------------------------------------------------------------- -# GET /api/models -# --------------------------------------------------------------------------- -@models_router.get("") -async def list_models() -> Dict[str, Any]: - """ - 扫描 ./data/models/ 目录,返回所有 .joblib 文件名(不含后缀)。 - - 异常处理:目录不存在时自动创建,返回空列表。 - """ - models_dir = Path("./data/models") - models_dir.mkdir(parents=True, exist_ok=True) - - model_names = [ - p.stem for p in models_dir.iterdir() if p.suffix == ".joblib" - ] - return {"models": model_names} - - -# --------------------------------------------------------------------------- -# POST /api/models/upload -# --------------------------------------------------------------------------- -@models_router.post("/upload") -async def upload_model( - file: UploadFile = File(...), -) -> Dict[str, Any]: - """ - 接收上传的 .joblib 模型文件,保存到 ./data/models/ 目录。 - - - 校验后缀必须为 .joblib - - 目录不存在时自动创建 - - 返回状态和文件名(不含后缀) - """ - if not file.filename or not file.filename.lower().endswith(".joblib"): - raise HTTPException( - status_code=400, - detail="仅支持 .joblib 格式的文件", - ) - - models_dir = Path("./data/models") - models_dir.mkdir(parents=True, exist_ok=True) - - dest_path = models_dir / file.filename - - with dest_path.open("wb") as buffer: - shutil.copyfileobj(file.file, buffer) - - return { - "status": "success", - "model_id": dest_path.stem, - } diff --git a/new/app/core/algorithms/__init__.py b/new/app/core/algorithms/__init__.py deleted file mode 100644 index 6cd03b7..0000000 --- a/new/app/core/algorithms/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -去耀斑算法包 -============ - -通过「注册表 + 策略模式」组织不同的去耀斑算法。 -所有具体算法都应继承 BaseGlintRemover,并使用 @register_glint_remover -装饰器把算法名和实现类绑定。 - -外部调用约定 ------------- -1. 所有算法子模块必须在本 __init__ 中显式 import, - 这样装饰器才会被执行、注册表才会被填满。 -2. 上层(endpoints、worker)只允许: - from app.core.algorithms import get_remover - 来获取算法类,不要直接 import 具体实现类, - 保持调度层与具体算法的解耦。 -""" - -from app.core.algorithms.base import BaseGlintRemover -from app.core.algorithms.registry import ( - get_remover, - list_removers, - register_glint_remover, - unregister_glint_remover, -) - -# ---- 算法子模块 import 区 ---- -# 新增算法时,在这里加一行 import,确保装饰器被执行。 -from app.core.algorithms import goodman # Goodman -from app.core.algorithms import kutser # Kutser -# from app.core.algorithms import hedley # Hedley -# from app.core.algorithms import sugar # SUGAR - -__all__ = [ - "BaseGlintRemover", - "register_glint_remover", - "get_remover", - "list_removers", - "unregister_glint_remover", -] diff --git a/new/app/core/algorithms/base.py b/new/app/core/algorithms/base.py deleted file mode 100644 index 47bce25..0000000 --- a/new/app/core/algorithms/base.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -去耀斑算法抽象基类 -================== - -设计目标(策略模式 Strategy Pattern) ------------------------------------- -本模块定义了所有去耀斑算法必须遵守的标准接口。 -未来的 Kutser、Goodman、Hedley、SUGAR 等算法都将继承本基类, -并实现统一的 process() 方法。 - -输入输出规范 ------------- -所有算法的输入与输出均统一为 **Zarr 文件路径**(字符串), -而不是内存中的 numpy ndarray。这样做的核心收益是: - - 1. **解耦数据存储与内存计算**: - 算法只关心「从哪个 zarr 读、写到哪个 zarr」, - 至于数据最初来自 GeoTIFF / HDF5 / NetCDF / 内存数组, - 都由 IO 层负责归一化转为 zarr。 - 2. **支持 Out-of-Core 计算**: - 影像往往超过内存上限,zarr 分块(chunk)天然支持按块读取, - 算法实现可以借助 dask / xarray 进行流式计算。 - 3. **可缓存、可复用**: - 中间产物落盘后,下游算法(大气校正、辐射定标)能直接消费, - 避免重复 IO。 - 4. **易于并行与分布式**: - 任务调度层只需把两个路径扔给 worker,无需关心数据细节。 - -约定 ----- -- 子类应实现 process(),完成「读 -> 计算 -> 写」的完整流程。 -- process() 返回 True 表示成功,False 表示失败。 -- 失败时建议抛出异常而非仅返回 False,便于上层 BackgroundTasks 捕获并写入 error 字段。 -""" - -from abc import ABC, abstractmethod -from typing import Any - - -class BaseGlintRemover(ABC): - """ - 去耀斑算法抽象基类。 - - 所有具体算法(Kutser / Goodman / Hedley / SUGAR …)必须继承本类并实现 process()。 - 子类可在 __init__ 中接收自己的超参数(如参考波段、阈值等), - 真正的输入输出数据则由 process() 的两个 zarr 路径参数指定。 - """ - - # 子类可覆盖的算法名称标识,用于调度层按 method 名字查找 - name: str = "base" - - @abstractmethod - async def process( - self, - input_zarr_path: str, - output_zarr_path: str, - **kwargs: Any, - ) -> bool: - """ - 执行去耀斑处理。 - - Parameters - ---------- - input_zarr_path : str - 输入高光谱影像的 zarr 存储路径。 - 数据已由 IO 层完成格式归一化(波段、坐标系、空间维度均已对齐)。 - output_zarr_path : str - 处理结果(去耀斑后影像)的 zarr 存储路径。 - 子类需自行创建该 zarr 存储并写入结果。 - **kwargs : Any - 算法的可选超参数,例如: - - reference_band: 参考近红外波段索引 - - chunk_size: 计算分块大小 - - 其它算法特定参数 - - Returns - ------- - bool - True 表示处理成功,False 表示失败。 - 建议在出错时直接 raise,由调用方统一记录到任务状态。 - """ - raise NotImplementedError - - def __repr__(self) -> str: # pragma: no cover - 调试辅助 - return f"<{self.__class__.__name__} name={self.name!r}>" diff --git a/new/app/core/algorithms/goodman.py b/new/app/core/algorithms/goodman.py deleted file mode 100644 index c6f3ecf..0000000 --- a/new/app/core/algorithms/goodman.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -app/core/algorithms/goodman.py -=============================== - -Goodman et al. 2008 去耀斑算法的 xarray + dask 流式实现。 - -算法公式 --------- - R_corrected = R_raw - R_750 + A + B * (R_640 - R_750) - -其中: - R_raw -- 原始反射率 (y, x, band) - R_750 -- λ=750 nm 处的反射率(红外参考波段, 远离水汽吸收) - R_640 -- λ=640 nm 处的反射率(可见光差异波段) - A, B -- 经验回归参数(用户可通过 params 传入, 默认全 0) - -后处理 ------- -- 负值截断为 0(Clamp to 0) -- 仅在水域掩膜 (water_mask) 内生效, 水外置 0 - -维度约定 --------- - reflectance: (y, x, band), band 坐标通常为 wavelength (nm) - water_mask : (y, x), 布尔类型, True = 水域 -""" - -import asyncio -from typing import Any - -import xarray as xr - -from app.core.algorithms.base import BaseGlintRemover -from app.core.algorithms.registry import register_glint_remover - - -# --------------------------------------------------------------------------- -# 默认参数 -# --------------------------------------------------------------------------- -# 与原始 Goodman 2008 论文符号保持一致, 方便用户交叉对照。 -# A、B 通常通过对纯净深水区做 (R_corr - R_raw) ~ (R_640 - R_750) 回归得到; -# 在缺乏先验知识时, 退化为 A=0, B=0 即等价于 R_corrected = clip(R_raw - R_750, 0)。 -# --------------------------------------------------------------------------- -DEFAULT_BAND_REF: float = 750.0 # λ_750 nm, 红外参考波段 -DEFAULT_BAND_DIFF: float = 640.0 # λ_640 nm, 可见光差异波段 -DEFAULT_A: float = 0.0 # 公式中的常数偏移项 -DEFAULT_B: float = 0.0 # 公式中的斜率项 - - -@register_glint_remover("goodman") -class GoodmanGlintRemover(BaseGlintRemover): - """Goodman et al. 2008 去耀斑算法""" - - name = "goodman" - - async def process( - self, - input_zarr_path: str, - output_zarr_path: str, - **kwargs: Any, - ) -> bool: - # 1. 解析超参数(带默认值, 方便用户按需覆盖) - band_ref: float = kwargs.get("band_ref", DEFAULT_BAND_REF) - band_diff: float = kwargs.get("band_diff", DEFAULT_BAND_DIFF) - A: float = kwargs.get("A", DEFAULT_A) - B: float = kwargs.get("B", DEFAULT_B) - - # 2. 把同步的 xarray/dask 计算丢到工作线程, - # 避免阻塞 FastAPI 的事件循环 - return await asyncio.to_thread( - self._process_sync, - input_zarr_path, - output_zarr_path, - band_ref, - band_diff, - A, - B, - ) - - @staticmethod - def _process_sync( - input_zarr_path: str, - output_zarr_path: str, - band_ref: float, - band_diff: float, - A: float, - B: float, - ) -> bool: - # 1. 以 zarr 路径打开(dask-backed, 不物化到内存) - # chunks="auto" 让 dask 根据每条坐标轴的大小自动决定分块 - ds = xr.open_zarr(input_zarr_path, chunks="auto") - reflectance = ds["reflectance"] # (y, x, band) - - # 2. 用 sel + method='nearest' 提取两个关键波段 - # 返回形状 (y, x), 后续与 (y, x, band) 算术时会自动广播 - R_750 = reflectance.sel(band=band_ref, method="nearest") - R_640 = reflectance.sel(band=band_diff, method="nearest") - - # 3. Goodman 公式: xarray 沿 band 维度自动广播 - # R_corr = R_raw - R_750 + A + B * (R_640 - R_750) - result = reflectance - R_750 + A + B * (R_640 - R_750) - - # 4. 负值截断为 0(clip(min=0) 优于 where(>0, 0, _): - # 不构造布尔中间数组, 底层走 dask 矢量化 clip 路径) - result = result.clip(min=0) - - # 5. 仅在水域内生效(水外强制为 0) - # 优先从 zarr 内部读 water_mask 变量, 缺失则视为全图水域 - if "water_mask" in ds: - water_mask = ds["water_mask"].astype(bool) - result = result.where(water_mask, 0) - - # 6. 构造输出 Dataset, 保留元信息(波段坐标/属性等) - out = xr.Dataset({"reflectance": result}) - if ds.attrs: - out.attrs = dict(ds.attrs) - if reflectance.attrs: - out["reflectance"].attrs = dict(reflectance.attrs) - - # 7. 流式写出(Out-of-Core):不一次性物化大数组, - # dask 会按 chunk 边算边写, 内存峰值 ≈ 单个 chunk 大小 - out.to_zarr(output_zarr_path, mode="w", compute=True) - return True diff --git a/new/app/core/algorithms/kutser.py b/new/app/core/algorithms/kutser.py deleted file mode 100644 index 73aff2f..0000000 --- a/new/app/core/algorithms/kutser.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Kutser 去耀斑算法(xarray + dask 重构版) -======================================== - -旧版痛点 --------- -原始 Kutser 实现(参考 Kutser et al., 2013)通常写成像这样: - - R_corr = np.zeros_like(R_raw) - for b in range(n_bands): - for y in range(H): - for x in range(W): - if water_mask[y, x]: - R_corr[y, x, b] = ( - R_raw[y, x, b] - G_list[b] * D_norm[y, x] - ) - with rasterio.open(..., 'w') as dst: - dst.write(R_corr) - -问题: - 1. 三重 Python 循环,每次只做一个浮点运算,解释器开销巨大; - 2. 一次性把整张图 R_raw 读进内存,大影像直接 OOM; - 3. rasterio 写出要求 numpy 连续数组,进一步放大内存。 - -本文件用 xarray + dask 重写: - - 用 DataArray 维度广播,三重循环 → 一行表达式; - - 用 dask chunk 保持数据常驻磁盘、流式计算; - - 用 to_zarr 边算边写,输出格式与算法层彻底解耦。 -""" - -import asyncio -from typing import Any - -import xarray as xr - -from app.core.algorithms.base import BaseGlintRemover -from app.core.algorithms.registry import register_glint_remover - - -# --------------------------------------------------------------------------- -# 算法实现 -# --------------------------------------------------------------------------- -@register_glint_remover("kutser") -class KutserGlintRemover(BaseGlintRemover): - """ - Kutser 近红外扣除法去耀斑。 - - 数学公式(与旧版完全等价) - ------------------------- - 1) 水汽吸收深度 D(每像素): - D = (R(λ_lower) + R(λ_upper)) / 2 - R(λ_oxy) - 2) 全局归一化因子 D_max: - D_max = max(D) over 水域 - 归一化: - D_norm = D / D_max - 3) 每波段水域范围: - G_list[b] = max(R[:, :, b] over 水域) - min(R[:, :, b] over 水域) - 4) 校正公式(每像素、每波段): - R_corr(λ_b) = R_raw(λ_b) - G_list[b] * D_norm - """ - - # Kutser 2013 论文里使用的参考波段(nm): - # λ_lower = 773, λ_oxy = 845, λ_upper = 893 - # 允许通过 kwargs 覆盖,便于适配 MERIS / OLCI / Landsat 等不同传感器。 - DEFAULT_BAND_LOWER: float = 773.0 - DEFAULT_BAND_OXY: float = 845.0 - DEFAULT_BAND_UPPER: float = 893.0 - - # -------------------------------------------------------------- - # 公开异步入口 - # -------------------------------------------------------------- - # xarray / dask 的算子本身是同步阻塞的。在 async 函数中, - # 用 asyncio.to_thread 把同步体丢到默认线程池执行, - # 避免阻塞 FastAPI 的事件循环。 - # -------------------------------------------------------------- - async def process( - self, - input_zarr_path: str, - output_zarr_path: str, - **kwargs: Any, - ) -> bool: - return await asyncio.to_thread( - self._process_sync, - input_zarr_path, - output_zarr_path, - kwargs, - ) - - # -------------------------------------------------------------- - # 同步核心实现 - # -------------------------------------------------------------- - def _process_sync( - self, - input_zarr_path: str, - output_zarr_path: str, - kwargs: dict, - ) -> bool: - # ============================================================ - # 步骤 0:打开 zarr,建立 dask 计算图 - # ============================================================ - # chunks="auto":让 dask 根据 zarr 的存储分块自动选择内存上限, - # 数据不会一次性全部 materialize 进 RAM。 - # ============================================================ - ds = xr.open_zarr(input_zarr_path, chunks="auto") - reflectance: xr.DataArray = ds["reflectance"] # 维度约定:(y, x, band) - - # 维度顺序约定(也可根据 ds.dims 自动适配): - assert "y" in reflectance.dims and "x" in reflectance.dims and "band" in reflectance.dims, ( - f"reflectance 必须包含 y/x/band 三个维度,实际为: {reflectance.dims}" - ) - - # ============================================================ - # 步骤 1:取出 3 个参考波段对应的二维 (y, x) 切片 - # ============================================================ - # 假设 band 维度的坐标是 wavelength(nm)。 - # 用 sel(..., method="nearest") 自动匹配最接近的波段。 - # ============================================================ - wl_lower = float(kwargs.get("band_lower", self.DEFAULT_BAND_LOWER)) - wl_oxy = float(kwargs.get("band_oxy", self.DEFAULT_BAND_OXY)) - wl_upper = float(kwargs.get("band_upper", self.DEFAULT_BAND_UPPER)) - - R_lower = reflectance.sel(band=wl_lower, method="nearest") # (y, x) - R_upper = reflectance.sel(band=wl_upper, method="nearest") # (y, x) - R_oxy = reflectance.sel(band=wl_oxy, method="nearest") # (y, x) - - # ============================================================ - # 步骤 2:水域掩膜 - # ============================================================ - # 优先从 zarr 内部读取 water_mask 变量; - # 如果不存在,则假定整幅图都是水域(开发期兜底)。 - # ============================================================ - if "water_mask" in ds: - water_mask = ds["water_mask"].astype(bool) - else: - water_mask = xr.ones_like( - reflectance.isel(band=0), dtype=bool - ) - - # ============================================================ - # 步骤 3:水汽吸收深度 D(每像素,形状 (y, x)) - # ============================================================ - # 旧版:D[y, x] = (R_lower[y, x] + R_upper[y, x]) / 2 - R_oxy[y, x] - # 新版:一行表达式,dask 自动构建 lazy 计算图。 - # ============================================================ - D = (R_lower + R_upper) / 2.0 - R_oxy # (y, x),dtype 与 reflectance 一致 - - # ============================================================ - # 步骤 4:全局归一化因子 D_max(标量,0-dim DataArray) - # ============================================================ - # 关键:先 .where(water_mask) 把非水域置 NaN, - # 再 .max() 跨 (x, y) 聚合,自动规约到 0 维。 - # dask 此时仍然没有真正计算,等到 to_zarr 时再触发。 - # ============================================================ - D_max = D.where(water_mask).max() # scalar - # 容错:如果水域为空导致 D_max 为 NaN,用极小值兜底,避免除零 - D_max = D_max.fillna(1e-6) - - # ============================================================ - # 步骤 5:归一化 D_norm(形状 (y, x)) - # ============================================================ - D_norm = D / D_max # 标量除以 (y, x) 数组 → 自动广播 - - # ============================================================ - # 步骤 6:每波段水域范围 G_list(形状 (band,)) - # ============================================================ - # 旧版三重循环内部还要做一次 min/max 聚合。 - # xarray 版本:把 (y, x) 一起 reduce,只保留 band 维度。 - # ============================================================ - R_water = reflectance.where(water_mask) # (y, x, band),非水域 NaN - G_min = R_water.min(dim=["x", "y"]) # (band,) - G_max = R_water.max(dim=["x", "y"]) # (band,) - G_list = (G_max - G_min).fillna(0.0) # (band,),容错 - - # ============================================================ - # 步骤 7:校正公式(最关键的一行,演示 xarray 广播) - # ============================================================ - # 旧版需要: - # for b in bands: - # for y in range(H): - # for x in range(W): - # R_corr[y,x,b] = R_raw[y,x,b] - G_list[b] * D_norm[y,x] - # - # xarray 维度对齐规则: - # R_raw : (y, x, band) - # G_list: (band,) → 缺失 y, x 自动扩展 - # D_norm: (y, x) → 缺失 band 自动扩展 - # 乘法结果: (y, x, band) → 减法对齐 - # 一行表达式完成「三重 for 循环 + 标量索引」的语义。 - # ============================================================ - corrected = reflectance - G_list * D_norm # (y, x, band) - - # ============================================================ - # 步骤 8:水域掩膜过滤(非水域置 NaN) - # ============================================================ - result = corrected.where(water_mask) - - # ============================================================ - # 步骤 9:持久化为 zarr - # ============================================================ - # mode="w":覆盖写入(如果目标已存在则删除重建)。 - # compute=True:阻塞直到整张图算完并落盘。 - # 由于数据始终是 dask chunk + 流式写出, - # 内存峰值 ≈ 单个 chunk 大小,与整张影像大小无关。 - # ============================================================ - out = xr.Dataset({"reflectance": result}) - # 保留原数据集的全局属性 / 坐标信息(CRS、wavelength、...) - out.attrs = dict(ds.attrs) - out["reflectance"].attrs = dict(reflectance.attrs) - out.to_zarr(output_zarr_path, mode="w", compute=True) - - return True diff --git a/new/app/core/algorithms/registry.py b/new/app/core/algorithms/registry.py deleted file mode 100644 index d70e841..0000000 --- a/new/app/core/algorithms/registry.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -算法注册表(Registry / Factory) -================================ - -通过装饰器把「算法名字符串」与「算法实现类」绑定在一起。 -上层调度层(FastAPI endpoints、BackgroundTasks worker)只需要拿到 -前端传过来的 method 字符串,就可以自动派发到对应的算法实现, -而无需写一长串 if/elif。 - -使用示例 --------- - - from app.core.algorithms import BaseGlintRemover - from app.core.algorithms.registry import ( - register_glint_remover, - get_remover, - list_removers, - ) - - @register_glint_remover("kutser") - class KutserGlintRemover(BaseGlintRemover): - async def process(self, input_zarr_path, output_zarr_path, **kwargs): - ... - - # 派发 - Cls = get_remover(method_from_request) - remover = Cls() - await remover.process(input_zarr_path, output_zarr_path, **kwargs) - -设计要点 --------- -- 注册动作发生在「类定义时」,所以必须在所有算法 import 完之后 - 注册表才完整。可以在 `app/core/algorithms/__init__.py` 中 - 把算法子模块 import 一遍来强制触发注册。 -- 重复注册同名算法会直接抛错,避免静默覆盖。 -- name 会同步写回到类的 `name` 属性,便于算法自身查询身份。 -""" - -from typing import Dict, Type - -from app.core.algorithms.base import BaseGlintRemover - - -# 全局注册表:name(str) -> 实现类(type),类未被实例化 -_REGISTRY: Dict[str, Type[BaseGlintRemover]] = {} - - -def register_glint_remover(name: str): - """ - 类装饰器工厂:把传入 name 的算法类注册到全局注册表。 - - Parameters - ---------- - name : str - 算法标识,建议小写下划线风格,例如 "kutser"、"goodman"。 - - Raises - ------ - ValueError - - name 不是非空字符串 - - name 已经被其它类占用 - TypeError - - 被装饰的对象不是 BaseGlintRemover 的子类 - """ - - # ---- 防御性校验:name 必须是合法字符串 ---- - if not isinstance(name, str) or not name.strip(): - raise ValueError( - f"register_glint_remover 的 name 必须是非空字符串,收到: {name!r}" - ) - - def decorator(cls: Type[BaseGlintRemover]) -> Type[BaseGlintRemover]: - # ---- 防御性校验:被装饰对象必须是 BaseGlintRemover 子类 ---- - if not isinstance(cls, type) or not issubclass(cls, BaseGlintRemover): - raise TypeError( - f"@register_glint_remover 只能装饰 BaseGlintRemover 的子类," - f"收到: {cls!r}" - ) - - # ---- 防御性校验:禁止静默覆盖 ---- - if name in _REGISTRY: - raise ValueError( - f"算法名 {name!r} 已被 {_REGISTRY[name].__name__} 占用," - f"请使用其它名字或先调用 unregister_glint_remover() 注销旧实现。" - ) - - # 同步把 name 写回类属性,便于算法自身和日志输出使用 - cls.name = name - _REGISTRY[name] = cls - return cls - - return decorator - - -def get_remover(name: str) -> Type[BaseGlintRemover]: - """ - 按算法名字符串取出对应的实现类(未实例化)。 - - 调用方拿到类后自行 `Cls(...)` 构造实例,再调用 process()。 - - Raises - ------ - KeyError - 当 name 不在注册表中时抛出,错误信息中附带已注册列表便于排查。 - """ - try: - return _REGISTRY[name] - except KeyError as exc: - known = ", ".join(sorted(_REGISTRY)) or "<空>" - raise KeyError( - f"未注册的算法名: {name!r}。已注册的算法: {known}" - ) from exc - - -def list_removers() -> Dict[str, Type[BaseGlintRemover]]: - """ - 返回当前注册表的浅拷贝。 - 可用于: - - 调试日志 - - 给前端暴露一个 GET /api/algorithms 接口 - - 单元测试断言 - """ - return dict(_REGISTRY) - - -def unregister_glint_remover(name: str) -> None: - """ - 注销指定算法。主要给: - - 单元测试 - - 热重载 / 插件卸载场景 - 生产代码一般不需要调用。 - """ - if name not in _REGISTRY: - raise KeyError(f"未注册的算法名: {name!r}") - del _REGISTRY[name] diff --git a/new/app/core/task_store.py b/new/app/core/task_store.py deleted file mode 100644 index aa1545d..0000000 --- a/new/app/core/task_store.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -app/core/task_store.py -====================== - -并发安全的内存任务状态存储,替代早期 mock 流水线中的 MOCK_TASK_DB。 - -设计目标 --------- -1. 在单进程内提供事件循环级别的互斥(asyncio.Lock), - 避免在 update 与 set/get 之间穿插 await 时发生状态不一致。 -2. 暴露异步 API(set_task / update_task / get_task), - 让调用方在 async 上下文中显式表达临界区。 -3. 保留一个同步的 has_task() 用于轻量存在性判断。 -4. 生产环境应替换为 Redis / SQLite / PostgreSQL, - 但接口形状保持一致, 便于上层调用方无缝迁移。 - -使用约定 --------- -- 写入初始 PENDING 记录: await set_task(task_id, record) -- 增量更新字段(PROCESSING/SUCCESS/FAILED):await update_task(task_id, **fields) -- 读取任务记录: await get_task(task_id) # 可能返回 None -- 同步判断是否存在: has_task(task_id) -""" - -import asyncio -from typing import Any, Dict, Optional - - -# --------------------------------------------------------------------------- -# 全局存储与锁 -# --------------------------------------------------------------------------- -# TASK_STORE: task_id -> 任务记录 -# 任务记录字段约定(与 endpoints.py 保持一致): -# task_id, method, params, status, -# output_zarr_path, error, traceback, -# created_at, updated_at -# --------------------------------------------------------------------------- -TASK_STORE: Dict[str, Dict[str, Any]] = {} - -# 单进程内的事件循环级互斥锁 -# 注意:asyncio.Lock 必须在事件循环内创建, 故在模块顶层实例化时 -# 仅获取引用, 第一次使用 (await lock.acquire()) 会在运行循环内进行。 -_lock: asyncio.Lock = asyncio.Lock() - - -# --------------------------------------------------------------------------- -# 异步 API -# --------------------------------------------------------------------------- -async def set_task(task_id: str, record: Dict[str, Any]) -> None: - """ - 初始化或整体覆盖一个任务记录。 - - 用法:POST 端点收到提交请求后立即调用, 写入 PENDING 状态的初始记录。 - """ - async with _lock: - TASK_STORE[task_id] = record - - -async def update_task(task_id: str, **fields: Any) -> None: - """ - 按字段增量更新任务记录。 - - 用法:后台执行器在 PROCESSING / SUCCESS / FAILED 等状态切换时调用。 - 若 task_id 不存在, setdefault 会自动创建一个空 dict 再 update(防御性兜底)。 - """ - async with _lock: - record = TASK_STORE.setdefault(task_id, {}) - record.update(fields) - - -async def get_task(task_id: str) -> Optional[Dict[str, Any]]: - """ - 读取任务记录; 不存在时返回 None。 - - 用法:GET /api/tasks/{task_id} 用此接口查询。 - """ - async with _lock: - return TASK_STORE.get(task_id) - - -# --------------------------------------------------------------------------- -# 同步 API(轻量) -# --------------------------------------------------------------------------- -def has_task(task_id: str) -> bool: - """ - 同步判断 task_id 是否存在。 - - 适用于不需要锁的轻量场景(例如日志前置判断); - 在 async 上下文中仍可调用, 因为 dict 的 in 判断是原子操作。 - """ - return task_id in TASK_STORE diff --git a/new/app/main.py b/new/app/main.py deleted file mode 100644 index 789ade3..0000000 --- a/new/app/main.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -WQ_GUI FastAPI 后端入口 -======================= - -应用启动与全局中间件配置: - - CORS:开发阶段允许所有来源,方便本地前端(Vite / Webpack dev server)联调 - - 路由:通过 include_router 挂载 app/api/endpoints.py 中的业务接口 - -业务接口说明: - POST /api/process/deglint 提交去耀斑处理任务,立即返回 task_id - GET /api/tasks/{task_id} 查询指定任务的状态与结果 -""" - -from typing import Dict - -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -from app.api.endpoints import router as deglint_router -from app.api.modeling import router as modeling_router -from app.api.modeling import models_router - - -# --------------------------------------------------------------------------- -# FastAPI 应用实例 -# --------------------------------------------------------------------------- -app = FastAPI( - title="WQ_GUI Backend", - description="高光谱影像去耀斑处理 API", - version="0.2.0", -) - - -# --------------------------------------------------------------------------- -# CORS 中间件 -# --------------------------------------------------------------------------- -# 开发阶段:放开所有来源、方法和头部,方便本地前端(任意端口)联调。 -# 生产环境务必收敛 allow_origins 为前端真实域名,避免安全风险。 -# --------------------------------------------------------------------------- -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -# --------------------------------------------------------------------------- -# 路由注册 -# --------------------------------------------------------------------------- -# 统一以 /api 为前缀,便于将来做版本管理(如 /api/v1、/api/v2)。 -# --------------------------------------------------------------------------- -app.include_router(deglint_router, prefix="/api") -app.include_router(modeling_router, prefix="/api") -app.include_router(models_router, prefix="/api") - - -# --------------------------------------------------------------------------- -# 根路径健康检查(方便本地调试,非业务必需) -# --------------------------------------------------------------------------- -@app.get("/") -async def root() -> Dict[str, str]: - return {"service": "WQ_GUI Backend", "status": "ok"} diff --git a/new/frontend/.gitignore b/new/frontend/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/new/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/new/frontend/README.md b/new/frontend/README.md deleted file mode 100644 index 33895ab..0000000 --- a/new/frontend/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Vue 3 + TypeScript + Vite - -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` - - diff --git a/new/frontend/package-lock.json b/new/frontend/package-lock.json deleted file mode 100644 index 50c06bf..0000000 --- a/new/frontend/package-lock.json +++ /dev/null @@ -1,2412 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "axios": "^1.16.1", - "echarts": "^6.1.0", - "element-plus": "^2.14.1", - "pinia": "^3.0.4", - "vue": "^3.5.34", - "vue-router": "^5.1.0" - }, - "devDependencies": { - "@types/node": "^24.12.3", - "@vitejs/plugin-vue": "^6.0.6", - "@vue/tsconfig": "^0.9.1", - "typescript": "~6.0.2", - "vite": "^8.0.12", - "vue-tsc": "^3.2.8" - } - }, - "node_modules/@babel/generator": { - "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.6.tgz", - "integrity": "sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^8.0.0-rc.6", - "@babel/types": "^8.0.0-rc.6", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "@types/jsesc": "^2.5.0", - "jsesc": "^3.0.2" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { - "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.6.tgz", - "integrity": "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==", - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.6.tgz", - "integrity": "sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==", - "license": "MIT", - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.6.tgz", - "integrity": "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==", - "license": "MIT", - "dependencies": { - "@babel/types": "^8.0.0-rc.6" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.6.tgz", - "integrity": "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^8.0.0-rc.6", - "@babel/helper-validator-identifier": "^8.0.0-rc.6" - }, - "engines": { - "node": "^22.18.0 || >=24.11.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", - "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@element-plus/icons-vue": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", - "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", - "license": "MIT", - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "devOptional": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@popperjs/core": { - "name": "@sxzz/popperjs-es", - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", - "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/node": { - "version": "24.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", - "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "license": "MIT" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", - "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@volar/language-core": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", - "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/source-map": "2.4.28" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", - "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@volar/typescript": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", - "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.28", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } - }, - "node_modules/@vue-macros/common": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", - "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==", - "license": "MIT", - "dependencies": { - "@vue/compiler-sfc": "^3.5.22", - "ast-kit": "^2.1.2", - "local-pkg": "^1.1.2", - "magic-string-ast": "^1.0.2", - "unplugin-utils": "^0.3.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/vue-macros" - }, - "peerDependencies": { - "vue": "^2.7.0 || ^3.2.25" - }, - "peerDependenciesMeta": { - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz", - "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/shared": "3.5.35", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", - "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.35", - "@vue/shared": "3.5.35" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", - "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/compiler-core": "3.5.35", - "@vue/compiler-dom": "3.5.35", - "@vue/compiler-ssr": "3.5.35", - "@vue/shared": "3.5.35", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.15", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", - "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.35", - "@vue/shared": "3.5.35" - } - }, - "node_modules/@vue/devtools-api": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", - "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^7.7.9" - } - }, - "node_modules/@vue/devtools-kit": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", - "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", - "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^7.7.9", - "birpc": "^2.3.0", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" - } - }, - "node_modules/@vue/devtools-shared": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", - "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", - "license": "MIT", - "dependencies": { - "rfdc": "^1.4.1" - } - }, - "node_modules/@vue/language-core": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.3.tgz", - "integrity": "sha512-X6p+7nfY7vVT6dQwUJ+v0Jfq/lwIfhL2jMi91dQ3ln4hnlGXlxsDu/FNkeyHYgvYtyQy18ZX76IZy7X4diDbiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.28", - "@vue/compiler-dom": "^3.5.0", - "@vue/shared": "^3.5.0", - "alien-signals": "^3.2.0", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1", - "picomatch": "^4.0.4" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", - "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.35" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz", - "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.35", - "@vue/shared": "3.5.35" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", - "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.35", - "@vue/runtime-core": "3.5.35", - "@vue/shared": "3.5.35", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz", - "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.35", - "@vue/shared": "3.5.35" - }, - "peerDependencies": { - "vue": "3.5.35" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz", - "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", - "license": "MIT" - }, - "node_modules/@vue/tsconfig": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", - "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": ">= 5.8", - "vue": "^3.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/@vueuse/core": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", - "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.3.0", - "@vueuse/shared": "14.3.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@vueuse/metadata": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", - "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", - "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/alien-signals": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", - "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", - "dev": true, - "license": "MIT" - }, - "node_modules/ast-kit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", - "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "pathe": "^2.0.3" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/ast-walker-scope": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz", - "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.2", - "@babel/types": "^7.29.0", - "ast-kit": "^2.2.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/async-validator": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/birpc": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", - "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "license": "MIT" - }, - "node_modules/copy-anything": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", - "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", - "license": "MIT", - "dependencies": { - "is-what": "^5.2.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/dayjs": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", - "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/echarts": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", - "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "2.3.0", - "zrender": "6.1.0" - } - }, - "node_modules/echarts/node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "license": "0BSD" - }, - "node_modules/element-plus": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.1.tgz", - "integrity": "sha512-UFnm1+BckNi+azkKJ7L32q1uXs9ekr99Z9pWTQPeDR05jqEWUwQq51ro4kZMVrANbjknX3Z7ukCZwTi2T6Tr9A==", - "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^4.2.0", - "@element-plus/icons-vue": "^2.3.2", - "@floating-ui/dom": "^1.7.6", - "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", - "@types/lodash": "^4.17.24", - "@types/lodash-es": "^4.17.12", - "@vueuse/core": "14.3.0", - "async-validator": "^4.2.5", - "dayjs": "^1.11.20", - "lodash": "^4.18.1", - "lodash-es": "^4.18.1", - "lodash-unified": "^1.0.3", - "memoize-one": "^6.0.0", - "normalize-wheel-es": "^1.2.0", - "vue-component-type-helpers": "^3.3.1" - }, - "peerDependencies": { - "vue": "^3.3.7" - } - }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hookable": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", - "license": "MIT" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/is-what": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", - "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "devOptional": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/local-pkg": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", - "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, - "node_modules/lodash-unified": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", - "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", - "license": "MIT", - "peerDependencies": { - "@types/lodash-es": "*", - "lodash": "*", - "lodash-es": "*" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magic-string-ast": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz", - "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.19" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/normalize-wheel-es": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", - "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", - "license": "BSD-3-Clause" - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pinia": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", - "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^7.7.7" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "typescript": ">=4.5.0", - "vue": "^3.5.11" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/pkg-types": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", - "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", - "license": "MIT", - "dependencies": { - "confbox": "^0.2.4", - "exsolve": "^1.0.8", - "pathe": "^2.0.3" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, - "node_modules/scule": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", - "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", - "license": "MIT" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/speakingurl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", - "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/superjson": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", - "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", - "license": "MIT", - "dependencies": { - "copy-anything": "^4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/unplugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", - "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/unplugin-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", - "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", - "license": "MIT", - "dependencies": { - "pathe": "^2.0.3", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vue": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", - "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.35", - "@vue/compiler-sfc": "3.5.35", - "@vue/runtime-dom": "3.5.35", - "@vue/server-renderer": "3.5.35", - "@vue/shared": "3.5.35" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-component-type-helpers": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.3.tgz", - "integrity": "sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==", - "license": "MIT" - }, - "node_modules/vue-router": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz", - "integrity": "sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^8.0.0-rc.4", - "@vue-macros/common": "^3.1.1", - "@vue/devtools-api": "^8.1.2", - "ast-walker-scope": "^0.9.0", - "chokidar": "^5.0.0", - "json5": "^2.2.3", - "local-pkg": "^1.1.2", - "magic-string": "^0.30.21", - "mlly": "^1.8.2", - "muggle-string": "^0.4.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "scule": "^1.3.0", - "tinyglobby": "^0.2.16", - "unplugin": "^3.0.0", - "unplugin-utils": "^0.3.1", - "yaml": "^2.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "@pinia/colada": ">=0.21.2", - "@vue/compiler-sfc": "^3.5.34", - "pinia": "^3.0.4", - "vite": "^7.0.0 || ^8.0.0", - "vue": "^3.5.34" - }, - "peerDependenciesMeta": { - "@pinia/colada": { - "optional": true - }, - "@vue/compiler-sfc": { - "optional": true - }, - "pinia": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.2.tgz", - "integrity": "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^8.1.2" - } - }, - "node_modules/vue-router/node_modules/@vue/devtools-kit": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz", - "integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==", - "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^8.1.2", - "birpc": "^2.6.1", - "hookable": "^5.5.3", - "perfect-debounce": "^2.0.0" - } - }, - "node_modules/vue-router/node_modules/@vue/devtools-shared": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz", - "integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==", - "license": "MIT" - }, - "node_modules/vue-router/node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "license": "MIT" - }, - "node_modules/vue-tsc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.3.tgz", - "integrity": "sha512-SWUEG7YRUeDJHT7Xsuhf02elYX2gxPzzAII7OxDAh4KNOr4QHQ0Lls0YfnaO5GNd560CwVa2HTfdqmA5MqvRqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/typescript": "2.4.28", - "@vue/language-core": "3.3.3" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "license": "MIT" - }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/zrender": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz", - "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==", - "license": "BSD-3-Clause", - "dependencies": { - "tslib": "2.3.0" - } - }, - "node_modules/zrender/node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "license": "0BSD" - } - } -} diff --git a/new/frontend/package.json b/new/frontend/package.json deleted file mode 100644 index 68455fb..0000000 --- a/new/frontend/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vue-tsc -b && vite build", - "preview": "vite preview" - }, - "dependencies": { - "axios": "^1.16.1", - "echarts": "^6.1.0", - "element-plus": "^2.14.1", - "pinia": "^3.0.4", - "vue": "^3.5.34", - "vue-router": "^5.1.0" - }, - "devDependencies": { - "@types/node": "^24.12.3", - "@vitejs/plugin-vue": "^6.0.6", - "@vue/tsconfig": "^0.9.1", - "typescript": "~6.0.2", - "vite": "^8.0.12", - "vue-tsc": "^3.2.8" - } -} diff --git a/new/frontend/public/favicon.svg b/new/frontend/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/new/frontend/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/new/frontend/public/icons.svg b/new/frontend/public/icons.svg deleted file mode 100644 index e952219..0000000 --- a/new/frontend/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/new/frontend/src/App.vue b/new/frontend/src/App.vue deleted file mode 100644 index e5db726..0000000 --- a/new/frontend/src/App.vue +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - diff --git a/new/frontend/src/api/request.ts b/new/frontend/src/api/request.ts deleted file mode 100644 index 368e9f2..0000000 --- a/new/frontend/src/api/request.ts +++ /dev/null @@ -1,15 +0,0 @@ -import axios from 'axios' - -const request = axios.create({ - // 注意:直接指向我们刚刚改好的 9090 端口 - baseURL: 'http://127.0.0.1:9090', - timeout: 60000 -}) - -// 拦截器:直接剥离 data -request.interceptors.response.use( - response => response.data, - error => Promise.reject(error) -) - -export default request diff --git a/new/frontend/src/api/tasks.ts b/new/frontend/src/api/tasks.ts deleted file mode 100644 index e9e93f7..0000000 --- a/new/frontend/src/api/tasks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import request from './request' - -export const submitTrain = (data: any) => { - return request.post('/api/modeling/train', data) -} - -export const submitPredict = (data: any) => { - return request.post('/api/modeling/predict', data) -} - -export const getTaskStatus = (task_id: string) => { - return request.get(`/api/tasks/${task_id}`) -} diff --git a/new/frontend/src/assets/hero.png b/new/frontend/src/assets/hero.png deleted file mode 100644 index 02251f4b956c55af2d76fd0788124d7eee2b45eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13057 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 P00000NkvXXu0mjft=yBf diff --git a/new/frontend/src/assets/vite.svg b/new/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/new/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/new/frontend/src/assets/vue.svg b/new/frontend/src/assets/vue.svg deleted file mode 100644 index 770e9d3..0000000 --- a/new/frontend/src/assets/vue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/new/frontend/src/components/HelloWorld.vue b/new/frontend/src/components/HelloWorld.vue deleted file mode 100644 index c232865..0000000 --- a/new/frontend/src/components/HelloWorld.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/new/frontend/src/composables/useTaskPoller.ts b/new/frontend/src/composables/useTaskPoller.ts deleted file mode 100644 index ec47ea8..0000000 --- a/new/frontend/src/composables/useTaskPoller.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ref, watch, onUnmounted, type Ref } from 'vue' -import { getTaskStatus } from '../api/tasks' - -export function useTaskPoller(taskIdRef: Ref) { - const status = ref('') - const isPolling = ref(false) - const error = ref(null) - const result = ref(null) - let timer: any = null - - const start = () => { - if (!taskIdRef.value) return - isPolling.value = true - error.value = null - status.value = 'PENDING' - - timer = setInterval(async () => { - try { - const res = await getTaskStatus(taskIdRef.value!) - status.value = res.status - - if (res.status === 'SUCCESS') { - result.value = res - stop() - } else if (res.status === 'FAILED') { - error.value = res.error || '任务执行失败' - stop() - } - } catch (e: any) { - error.value = '网络请求失败,请检查后端状态' - stop() - } - }, 2000) - } - - const stop = () => { - isPolling.value = false - if (timer) clearInterval(timer) - } - - // 监听 Task ID 变化自动开启轮询 - watch(taskIdRef, (newVal) => { - stop() - if (newVal) start() - }) - - // 组件销毁时清理定时器 - onUnmounted(() => stop()) - - return { status, isPolling, error, result, stop } -} diff --git a/new/frontend/src/main.ts b/new/frontend/src/main.ts deleted file mode 100644 index 9489de8..0000000 --- a/new/frontend/src/main.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createApp } from 'vue' -import ElementPlus from 'element-plus' -import 'element-plus/dist/index.css' -import App from './App.vue' - -const app = createApp(App) - -app.use(ElementPlus) -app.mount('#app') diff --git a/new/frontend/src/style.css b/new/frontend/src/style.css deleted file mode 100644 index 527d4fb..0000000 --- a/new/frontend/src/style.css +++ /dev/null @@ -1,296 +0,0 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -body { - margin: 0; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} - -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#app { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/new/frontend/tsconfig.app.json b/new/frontend/tsconfig.app.json deleted file mode 100644 index 5c750c5..0000000 --- a/new/frontend/tsconfig.app.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "@vue/tsconfig/tsconfig.dom.json", - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "types": ["vite/client"], - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] -} diff --git a/new/frontend/tsconfig.json b/new/frontend/tsconfig.json deleted file mode 100644 index 1ffef60..0000000 --- a/new/frontend/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/new/frontend/tsconfig.node.json b/new/frontend/tsconfig.node.json deleted file mode 100644 index d3c52ea..0000000 --- a/new/frontend/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "es2023", - "lib": ["ES2023"], - "module": "esnext", - "types": ["node"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["vite.config.ts"] -} diff --git a/new/frontend/vite.config.ts b/new/frontend/vite.config.ts deleted file mode 100644 index bbcf80c..0000000 --- a/new/frontend/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [vue()], -}) diff --git a/src/gui/crash_dump.txt b/src/gui/crash_dump.txt deleted file mode 100644 index cad70b0..0000000 --- a/src/gui/crash_dump.txt +++ /dev/null @@ -1,103 +0,0 @@ - -============================================================ -[2026-05-12 11:14:51] -Traceback (most recent call last): - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 130, in - from src.gui.panels.step9_panel import Step9Panel - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\panels\step9_panel.py", line 24, in - from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\core\water_quality_inversion_pipeline_GUI.py", line 45, in - from src.preprocessing.process_water_quality_data import process_water_quality_data - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\preprocessing\process_water_quality_data.py", line 9, in - from scipy import stats - File "", line 1412, in _handle_fromlist - File "D:\111\changyongruanjian\anconda\envs\WQ_GUI\Lib\site-packages\scipy\__init__.py", line 143, in __getattr__ - return _importlib.import_module(f'scipy.{name}') - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "D:\111\changyongruanjian\anconda\envs\WQ_GUI\Lib\importlib\__init__.py", line 90, in import_module - return _bootstrap._gcd_import(name[level:], package, level) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "D:\111\changyongruanjian\anconda\envs\WQ_GUI\Lib\site-packages\scipy\stats\__init__.py", line 632, in - from ._multicomp import * - File "D:\111\changyongruanjian\anconda\envs\WQ_GUI\Lib\site-packages\scipy\stats\_multicomp.py", line 11, in - from scipy.stats._qmc import check_random_state - File "D:\111\changyongruanjian\anconda\envs\WQ_GUI\Lib\site-packages\scipy\stats\_qmc.py", line 26, in - from scipy.sparse.csgraph import minimum_spanning_tree - File "D:\111\changyongruanjian\anconda\envs\WQ_GUI\Lib\site-packages\scipy\sparse\csgraph\__init__.py", line 188, in - from ._shortest_path import ( - File "scipy/sparse/csgraph/_shortest_path.pyx", line 21, in init scipy.sparse.csgraph._shortest_path - File "", line 1349, in _find_and_load -KeyboardInterrupt - -============================================================ -[2026-05-12 11:57:28] -Traceback (most recent call last): - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3123, in - main() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3093, in main - _dialog.exec_() -KeyboardInterrupt - -============================================================ -[2026-05-28 15:45:11] -Traceback (most recent call last): - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3123, in - main() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3097, in main - window = WaterQualityGUI() - ^^^^^^^^^^^^^^^^^ - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 1352, in __init__ - self.init_ui() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 1586, in init_ui - self.create_content_area() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 1943, in create_content_area - self.step2_panel = Step2Panel() - ^^^^^^^^^^^^ -TypeError: Step2Panel.__init__() missing 1 required positional argument: 'session' - -============================================================ -[2026-05-28 15:45:19] -Traceback (most recent call last): - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3123, in - main() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3097, in main - window = WaterQualityGUI() - ^^^^^^^^^^^^^^^^^ - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 1352, in __init__ - self.init_ui() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 1586, in init_ui - self.create_content_area() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 1943, in create_content_area - self.step2_panel = Step2Panel() - ^^^^^^^^^^^^ -TypeError: Step2Panel.__init__() missing 1 required positional argument: 'session' - -============================================================ -[2026-05-28 16:00:53] -Traceback (most recent call last): - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 2149, in on_step_changed - self.auto_populate_step_inputs(item_data) - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 2362, in auto_populate_step_inputs - if step_id not in self.step_dependencies: - ^^^^^^^^^^^^^^^^^^^^^^ -AttributeError: 'WaterQualityGUI' object has no attribute 'step_dependencies'. Did you mean: '_init_step_dependencies'? - -============================================================ -[2026-06-03 13:56:59] -Traceback (most recent call last): - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3354, in - main() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3331, in main - sys.exit(app.exec_()) - ^^^^^^^^^^^ -KeyboardInterrupt - -============================================================ -[2026-06-04 09:54:07] -Traceback (most recent call last): - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3237, in - main() - File "D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\gui\water_quality_gui.py", line 3214, in main - sys.exit(app.exec_()) - ^^^^^^^^^^^ -KeyboardInterrupt diff --git a/src/gui/scaler_params.pkl b/src/gui/scaler_params.pkl deleted file mode 100644 index ae05ef2ee1fac25cea47c820b91cc544ea5a3e7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1783 zcma)7YgkiP77Y|dQb9x%DiskSQW{b~kOX-wc?biF^0e0C%$h;l)c zi?!lNRqCsb)7G{lA5+?|P{vNBsL%@d7#}zz77*&lR~&&!P-o`n>>p>Jv%hoBT6?ea zD6*-mwBk)0>&E0NDN?5vX?2uVr;$+%qfl=V5pq&ba;!tDo>a?8ojg@WDk&XT%e_&| z<#Rr)-FAhZCR7xu<|^1((*s5?H}`F18m-an9n?fNMd}SYiXjwgf=di!=coyWRB4sw z2t!fk7`s4iP-%@KLZ;DCB9%d@SCBd#Y2?_Mj2Vx#;IT8N=a{`*Z`4wjh;_D@+g-7X zOW}MHUtz7nxTK_{gekL4H;FtwCykypot;ZNxN`G6c7}zi8E{^XMygauMbmo{T7}G< zJjc#Wvd~y%f!?4awc7tpbQWWf%9JF-aI`&ZYpI2WpoJGhlV(1NfhKcbqm!E#u~yOl z;YmBOd{U{@Y$sGIQfn@V+jGq6^#rL@Y*DKywVsd~^%TP;IvS*QmK3I~{Sz(Q;l2fh z&8@EK87eG1{N{1zxhhysx%}LCF&B$(T?)~ghpXkF*7xyfH^^TLj)bvgo?DZtrN4wQjcp@0)}RzeMrClSBk__a`D<`8^J+ z2Ome&`98laJl19#sUtYr?fEUB-DwYciRA>)OKC6Kn|7iF74$NTn6qDMY4IPY?EJZe ztOP@uh7ZWLVdIzU1dN-xezxDxQTr9xD$sDf%{d4hTQyO7UWNR|mI3dmt*A|iP4S6j z5OpQya!s%SxnpM*mh8<#kh5n`OTi8t?Z5E%xho4$;baPJ*i!`0#h0ZW} zcj0$mwbW;H6yapT@Rf3p5`YogIArhaCFc)#`;(~64wqj1q)Mf=tE(PaDFmg zsK~1mzSML2ouU3OMfv|#l-KZCZPN$g__j&)ee_Tu)X(sb__u|ax>59$UoXVQ1H)sD zk$$*N)+Dd)7NKXuK<&&U{y1l}zD%(Jh)w(M#O>Tb94uVX-f?v~{>0`*D93|9x@G+< zXfhbfBH3G4twK;hpW&|&i1GFv!qLWGf>{A!ZBP3o7?4#R;1`DC-uTq_K9!+RpKctR z%oju3={7bX4Z-oj`M)N`g+Op&WSv852!w{bt#dL%aMvah_Pc5L@=Z8Z=v}v=wS0ebWGH1Sj2*%|N%t!OZh+W?6nvxj`s|>@s z%-=%M(-5%j)ZuWfzH0SuodS42ow=uJ2b$)s>2FU6!