浏览代码

Merge branch 'cfclub' of http://git.iwintrue.com/liaoxg/cfc into cfclub

Sisyphus 15 小时之前
父节点
当前提交
17bb3ed59d
共有 56 个文件被更改,包括 8326 次插入 和 32 次删除
  1. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java
  2. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java
  3. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/DanReportController.java
  4. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/IndicatorController.java
  5. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java
  6. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateFamilyMemberDTO.java
  7. 130 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java
  8. 14 1
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
  9. 77 1
      cfc-backend/src/main/java/com/etotem/cfc/service/IndicatorTrendService.java
  10. 6 0
      cfc-backend/src/main/resources/application.yml
  11. 0 2
      cfc-backend/src/main/resources/schema.sql
  12. 46 0
      cfc-desktop-pet/.github/workflows/release.yml
  13. 3 0
      cfc-desktop-pet/.gitignore
  14. 19 0
      cfc-desktop-pet/src-tauri/.cargo/config.toml
  15. 2 0
      cfc-desktop-pet/src-tauri/.gitignore
  16. 5303 0
      cfc-desktop-pet/src-tauri/Cargo.lock
  17. 22 0
      cfc-desktop-pet/src-tauri/Cargo.toml
  18. 3 0
      cfc-desktop-pet/src-tauri/build.rs
  19. 7 0
      cfc-desktop-pet/src-tauri/capabilities/default.json
  20. 5 0
      cfc-desktop-pet/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml
  21. 4 0
      cfc-desktop-pet/src-tauri/icons/android/values/ic_launcher_background.xml
  22. 二进制
      cfc-desktop-pet/src-tauri/icons/icon.icns
  23. 二进制
      cfc-desktop-pet/src-tauri/icons/icon.ico
  24. 188 0
      cfc-desktop-pet/src-tauri/src/api/mod.rs
  25. 237 0
      cfc-desktop-pet/src-tauri/src/api/models.rs
  26. 10 0
      cfc-desktop-pet/src-tauri/src/app_state.rs
  27. 290 0
      cfc-desktop-pet/src-tauri/src/commands.rs
  28. 123 0
      cfc-desktop-pet/src-tauri/src/config.rs
  29. 58 0
      cfc-desktop-pet/src-tauri/src/main.rs
  30. 236 0
      cfc-desktop-pet/src-tauri/src/poller.rs
  31. 108 0
      cfc-desktop-pet/src-tauri/src/settings.rs
  32. 202 0
      cfc-desktop-pet/src-tauri/src/state.rs
  33. 71 0
      cfc-desktop-pet/src-tauri/src/token.rs
  34. 65 0
      cfc-desktop-pet/src-tauri/src/tray.rs
  35. 50 0
      cfc-desktop-pet/src-tauri/tauri.conf.json
  36. 27 0
      cfc-desktop-pet/src/DefaultPets/fubao/pet-config.json
  37. 27 0
      cfc-desktop-pet/src/DefaultPets/xibao/pet-config.json
  38. 46 0
      cfc-desktop-pet/src/animations.css
  39. 46 0
      cfc-desktop-pet/src/bridge.js
  40. 69 0
      cfc-desktop-pet/src/index.html
  41. 80 0
      cfc-desktop-pet/src/interactions.js
  42. 120 0
      cfc-desktop-pet/src/pet-engine.js
  43. 162 0
      cfc-desktop-pet/src/settings.html
  44. 0 0
      cfc-frontend/components/GenerationPicker.vue
  45. 8 1
      cfc-frontend/pages.json
  46. 198 0
      cfc-frontend/pages/dan-assessment/comparison.vue
  47. 2 2
      cfc-frontend/pages/family/add-member.vue
  48. 1 1
      cfc-frontend/pages/health/components/TrendChart.vue
  49. 47 10
      cfc-frontend/pages/health/indicator-curve.vue
  50. 11 8
      cfc-frontend/pages/profile-extra/family-members.vue
  51. 1 1
      cfc-web/.last_build_commit
  52. 2 2
      cfc-web/package-lock.json
  53. 1 1
      cfc-web/package.json
  54. 81 0
      cfc-web/public/CHANGELOG-v1.0.md
  55. 82 1
      cfc-web/public/CHANGELOG.md
  56. 0 1
      cfc-web/src/views/admin/ProductEdit.vue

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -42,6 +42,9 @@ private static final String[] PUBLIC_PATHS = {
         "/api/auth/check-phone",
         "/api/auth/direct-register",
         "/api/auth/register-with-invite",
+        "/api/auth/wechat-web/qr",
+        "/api/auth/wechat-web/callback",
+        "/api/auth/wechat-web/poll",
         "/api/internal/",
         "/api/product/list",
         "/api/product/detail",

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java

@@ -102,6 +102,9 @@ public class WebConfig implements WebMvcConfigurer {
                 "/api/auth/check-phone",
                 "/api/auth/direct-register",
                 "/api/auth/register-with-idcard",
+                "/api/auth/wechat-web/qr",
+                "/api/auth/wechat-web/callback",
+                "/api/auth/wechat-web/poll",
                 "/api/admin-auth/send-code",
                 "/api/admin-auth/login",
                 "/api/admin-auth/login-by-password",
@@ -145,6 +148,9 @@ public class WebConfig implements WebMvcConfigurer {
                 "/api/auth/check-phone",
                 "/api/auth/direct-register",
                 "/api/auth/register-with-idcard",
+                "/api/auth/wechat-web/qr",
+                "/api/auth/wechat-web/callback",
+                "/api/auth/wechat-web/poll",
                 "/api/admin-auth/send-code",
                 "/api/admin-auth/login",
                 "/api/admin-auth/login-by-password",

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/DanReportController.java

@@ -72,4 +72,12 @@ public class DanReportController {
         String dimension = (String) body.get("dimension");
         return uploadService.listByMember(memberId, dimension);
     }
+
+    /** DAN 报告两版对比接口(设计:两次上传按 uploaded_at 对齐 parsed/edited_items 算 delta) */
+    @PostMapping("/compare")
+    public Result<?> compare(@RequestBody Map<String, Object> params) {
+        Long uploadIdA = ParamUtils.getLong(params.get("uploadIdA"));
+        Long uploadIdB = ParamUtils.getLong(params.get("uploadIdB"));
+        return uploadService.compareUploads(uploadIdA, uploadIdB);
+    }
 }

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/IndicatorController.java

@@ -103,6 +103,18 @@ public class IndicatorController {
         return Result.success(indicatorTrendService.quickLookup(subjectId, category, page, pageSize, familyId));
     }
 
+    /** 成员已有指标时序的报告类型列表(曲线页类型筛选动态化) */
+    @PostMapping("/source-types")
+    public Result<?> sourceTypes(@RequestBody Map<String, Object> params,
+                                 @RequestAttribute("familyId") Long familyId) {
+        Long subjectId = com.etotem.cfc.util.ParamUtils.getLong(params.get("subjectId"));
+        Result<Void> check = checkSubjectBelongs(subjectId, familyId);
+        if (check != null) {
+            return check;
+        }
+        return Result.success(indicatorTrendService.listSourceTypes(subjectId));
+    }
+
     /** 指标关联查询(设计 6.4) */
     @PostMapping("/correlation/list")
     public Result<?> correlationList(@RequestBody Map<String, Object> params,

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java

@@ -64,6 +64,9 @@ public class FamilyMemberVO {
     /** 是否配偶 */
     private Boolean isSpouse;
 
+    /** 出生日期(yyyy-MM-dd 字符串,供前端编辑回显) */
+    private String birthday;
+
     /** 信任度分数 */
     private java.math.BigDecimal trustScore;
 

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateFamilyMemberDTO.java

@@ -14,4 +14,8 @@ public class UpdateFamilyMemberDTO {
     private String relationshipType;
     private String roleOverride;
     private Integer showToFamily;
+    /** 辈分等级字符串(仅管理员可更新):parent/child/grandparent/peer/great_grandparent 等 */
+    private String generationLevel;
+    /** 同辈类型(generationLevel=peer 时填写):sibling=兄弟姐妹,spouse=配偶 */
+    private String peerType;
 }

+ 130 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java

@@ -26,10 +26,13 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import com.etotem.cfc.util.SortUtil;
 
 @Service
@@ -617,4 +620,131 @@ if (!DanReportUpload.DIM_MIND.equals(dimension) && !DanReportUpload.DIM_WISDOM.e
         }
         return Result.success(vo);
     }
+
+    /** DAN 报告两版对比:按 uploaded_at 先后排序,较早的为基准。
+     * 返回 parsed/edited items 差异列表(数值 delta + 新增/删除标记)。
+     */
+    public Result<Map<String, Object>> compareUploads(Long uploadIdA, Long uploadIdB) {
+        if (uploadIdA == null || uploadIdB == null) {
+            return Result.error("缺少 uploadIdA 或 uploadIdB");
+        }
+        DanReportUpload uploadA = uploadMapper.selectById(uploadIdA);
+        DanReportUpload uploadB = uploadMapper.selectById(uploadIdB);
+        if (uploadA == null || uploadB == null) {
+            return Result.error("上传记录不存在");
+        }
+        if (!uploadA.getFamilyMemberId().equals(uploadB.getFamilyMemberId())) {
+            return Result.error("两次上传必须属于同一成员");
+        }
+        // 以 uploaded_at 先后顺序排序:较早的为基准
+        DanReportUpload first = uploadA.getUploadedAt() != null && uploadB.getUploadedAt() != null
+                && uploadA.getUploadedAt().before(uploadB.getUploadedAt()) ? uploadA : uploadB;
+        DanReportUpload second = first == uploadA ? uploadB : uploadA;
+        // 使用 edited_* 优先,否则 parsed_*
+        String firstItems = first.getEditedItems() != null ? first.getEditedItems() : first.getParsedItems();
+        String firstSummary = first.getEditedSummary() != null ? first.getEditedSummary() : first.getParsedSummary();
+        String secondItems = second.getEditedItems() != null ? second.getEditedItems() : second.getParsedItems();
+        String secondSummary = second.getEditedSummary() != null ? second.getEditedSummary() : second.getParsedSummary();
+        List<Map<String, Object>> diff = diffDeltas(firstItems, secondItems, first, second);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("uploadIdA", uploadA.getId());
+        result.put("uploadIdB", uploadB.getId());
+        result.put("firstUploadId", first.getId());
+        result.put("secondUploadId", second.getId());
+        result.put("orderBy", "uploaded_at_asc");
+        result.put("deltas", diff);
+        result.put("firstSummary", firstSummary);
+        result.put("secondSummary", secondSummary);
+        return Result.success(result);
+    }
+
+    /** 指定两次上传的 items 差异:以 code 对齐,数值算 delta,单边项标 added/removed */
+    private List<Map<String, Object>> diffDeltas(String itemsJson1, String itemsJson2,
+                                                 DanReportUpload first, DanReportUpload second) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (itemsJson1 == null || itemsJson2 == null) {
+            return result;
+        }
+        List<Map<String, Object>> list1, list2;
+        try {
+            list1 = objectMapper.readValue(itemsJson1,
+                    new TypeReference<List<Map<String, Object>>>() {});
+            list2 = objectMapper.readValue(itemsJson2,
+                    new TypeReference<List<Map<String, Object>>>() {});
+        } catch (Exception e) {
+            return result;
+        }
+        if (list1.isEmpty() || list2.isEmpty()) {
+            return result;
+        }
+        // 以 code 为键建立映射(code 唯一标识一项指标)
+        Map<String, Map<String, Object>> map1 = new LinkedHashMap<>();
+        for (Map<String, Object> item : list1) {
+            String code = item.get("code") != null ? String.valueOf(item.get("code")) : null;
+            if (code != null) map1.put(code, item);
+        }
+        Map<String, Map<String, Object>> map2 = new LinkedHashMap<>();
+        for (Map<String, Object> item : list2) {
+            String code = item.get("code") != null ? String.valueOf(item.get("code")) : null;
+            if (code != null) map2.put(code, item);
+        }
+        // 合并所有 codes(保持先出现顺序 + 新增 codes)
+        LinkedHashSet<String> codes = new LinkedHashSet<>();
+        for (Map<String, Object> item : list1) {
+            String code = item.get("code") != null ? String.valueOf(item.get("code")) : null;
+            if (code != null) codes.add(code);
+        }
+        for (Map<String, Object> item : list2) {
+            String code = item.get("code") != null ? String.valueOf(item.get("code")) : null;
+            if (code != null) codes.add(code);
+        }
+        for (String code : codes) {
+            Map<String, Object> m1 = map1.get(code);
+            Map<String, Object> m2 = map2.get(code);
+            if (m1 != null && m2 != null) {
+                Map<String, Object> delta = new LinkedHashMap<>();
+                delta.put("status", "modified");
+                delta.put("code", code);
+                delta.put("name1", m1.get("name"));
+                delta.put("name2", m2.get("name"));
+                delta.put("category1", m1.get("category"));
+                delta.put("category2", m2.get("category"));
+                Object v1 = m1.get("value");
+                Object v2 = m2.get("value");
+                if (v1 instanceof Number && v2 instanceof Number) {
+                    double d1 = ((Number) v1).doubleValue();
+                    double d2 = ((Number) v2).doubleValue();
+                    delta.put("value1", v1);
+                    delta.put("value2", v2);
+                    delta.put("valueDelta", Math.round((d2 - d1) * 10.0) / 10.0);
+                    delta.put("change", Math.abs(d2 - d1) > 0.001 ? "increase" : "no_change");
+                } else {
+                    delta.put("value1", v1);
+                    delta.put("value2", v2);
+                    delta.put("valueDelta", null);
+                    delta.put("change", "text_changed");
+                }
+                result.add(delta);
+            } else if (m1 == null) {
+                Map<String, Object> added = new LinkedHashMap<>();
+                added.put("status", "added");
+                added.put("code", code);
+                added.put("name2", m2.get("name"));
+                added.put("value2", m2.get("value"));
+                added.put("category2", m2.get("category"));
+                added.put("from", second.getUploadedAt() != null ? second.getUploadedAt().toString() : "unknown");
+                result.add(added);
+            } else {
+                Map<String, Object> removed = new LinkedHashMap<>();
+                removed.put("status", "removed");
+                removed.put("code", code);
+                removed.put("name1", m1.get("name"));
+                removed.put("value1", m1.get("value"));
+                removed.put("category1", m1.get("category"));
+                removed.put("from", first.getUploadedAt() != null ? first.getUploadedAt().toString() : "unknown");
+                result.add(removed);
+            }
+        }
+        return result;
+    }
 }

+ 14 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -261,8 +261,9 @@ public class FamilyMemberService {
             vo.setShowToFamily(fm.getShowToFamily());
             if (fm.getBirthday() != null) {
                 vo.setAge(calculateAge(fm.getBirthday()));
+                vo.setBirthday(formatDate(fm.getBirthday()));
             }
-            // 计算视角相关的关系标签(优先使用关系表)
+            // 计算视角相关的关系标签
             if (viewerMember != null) {
                 Integer genValue = null;
                 if (familyRelationshipService != null && viewerMember.getId() != null) {
@@ -1019,6 +1020,7 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
         vo.setShowToFamily(member.getShowToFamily());
         if (member.getBirthday() != null) {
             vo.setAge(calculateAge(member.getBirthday()));
+            vo.setBirthday(formatDate(member.getBirthday()));
         }
         // 设置 isSwitchable:仅无 openid 的不可登录成员可被切换(可登录成员不允许切换视角)
         if (member.getUserId() != null) {
@@ -1045,6 +1047,17 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
         return Math.max(0, age);
     }
 
+    /** 格式化为 yyyy-MM-dd(前端编辑回显用),返回 null 时不修改 */
+    private String formatDate(java.util.Date d) {
+        if (d == null) return null;
+        try {
+            java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+            return sdf.format(d);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     /**
      * 计算成员的实际角色(effective role)
      * 优先使用年龄判定(birthday → age < childThreshold → child),

+ 77 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/IndicatorTrendService.java

@@ -107,6 +107,14 @@ public class IndicatorTrendService {
         }
         List<IndicatorValue> rows = valueMapper.selectList(qw.orderByAsc(IndicatorValue::getReportDate));
 
+        // 该成员(+报告类型)全部观测日期全集:用于计算"相邻两次对比中间缺失的报告期数"
+        java.util.List<java.sql.Date> allDates = rows.stream()
+                .map(IndicatorValue::getReportDate)
+                .filter(d -> d != null)
+                .distinct()
+                .sorted()
+                .collect(Collectors.toList());
+
         // 按 definitionId 分组
         Map<Long, List<IndicatorValue>> byDef = rows.stream()
                 .collect(Collectors.groupingBy(IndicatorValue::getDefinitionId));
@@ -124,6 +132,23 @@ public class IndicatorTrendService {
             if ("adjacent".equals(scope)) {
                 before = sorted.get(sorted.size() - 2);
             }
+            // skipGaps:before/after 之间存在报告日期、但该指标缺失的期数(跨期跳比标记)
+            // before 在 adjacent 分支被重新赋值,lambda 需 final 副本
+            long skipGaps = 0;
+            if ("adjacent".equals(scope)
+                    && before.getReportDate() != null && after.getReportDate() != null) {
+                final IndicatorValue beforeF = before;
+                final IndicatorValue afterF = after;
+                long reportsBetween = allDates.stream()
+                        .filter(d -> d.after(beforeF.getReportDate()) && d.before(afterF.getReportDate()))
+                        .count();
+                long obsBetween = sorted.stream()
+                        .filter(v -> v.getReportDate() != null
+                                && v.getReportDate().after(beforeF.getReportDate())
+                                && v.getReportDate().before(afterF.getReportDate()))
+                        .count();
+                skipGaps = Math.max(0, reportsBetween - obsBetween);
+            }
             IndicatorDefinition def = defCache.computeIfAbsent(e.getKey(),
                     id -> definitionMapper.selectById(id));
             String defCategory = def != null ? def.getCategory() : null;
@@ -136,9 +161,10 @@ public class IndicatorTrendService {
                 continue;
             }
             BigDecimal delta = av.subtract(bv);
+            // 基数为 0 时百分比无意义(如 0 → 0.5 显示 +0.5 而非误导性的 0%),置 null 由前端降级为只显示 Δ
             BigDecimal deltaPct = bv.compareTo(BigDecimal.ZERO) != 0
                     ? delta.multiply(BigDecimal.valueOf(100)).divide(bv, 1, RoundingMode.HALF_UP)
-                    : BigDecimal.ZERO;
+                    : null;
             String movement = "flat";
             if (delta.compareTo(BigDecimal.ZERO) > 0) movement = "up";
             else if (delta.compareTo(BigDecimal.ZERO) < 0) movement = "down";
@@ -165,6 +191,7 @@ public class IndicatorTrendService {
             item.put("after", afterMap);
             item.put("delta", delta);
             item.put("deltaPct", deltaPct);
+            item.put("skipGaps", skipGaps);
             item.put("movement", movement);
             item.put("trendLabel", trendLabel);
             items.add(item);
@@ -194,6 +221,55 @@ public class IndicatorTrendService {
         return data;
     }
 
+    /** 该成员已有指标时序的报告类型(indicator_values.source_type 去重,曲线页类型筛选动态来源) */
+    public List<Map<String, Object>> listSourceTypes(Long subjectId) {
+        List<Map<String, Object>> items = new ArrayList<>();
+        if (subjectId == null) {
+            return items;
+        }
+        List<IndicatorValue> rows = valueMapper.selectList(new LambdaQueryWrapper<IndicatorValue>()
+                .eq(IndicatorValue::getSubjectId, subjectId)
+                .isNotNull(IndicatorValue::getSourceType)
+                .isNotNull(IndicatorValue::getReportDate)
+                .select(IndicatorValue::getSourceType));
+        Set<String> types = new LinkedHashSet<>();
+        for (IndicatorValue v : rows) {
+            String t = v.getSourceType();
+            if (t != null && !t.trim().isEmpty()) {
+                types.add(t.trim());
+            }
+        }
+        for (String t : types) {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("sourceType", t);
+            m.put("label", sourceTypeLabel(t));
+            items.add(m);
+        }
+        return items;
+    }
+
+    private String sourceTypeLabel(String t) {
+        if (t == null) {
+            return t;
+        }
+        if (t.startsWith("gut_")) {
+            return "肠道菌群";
+        }
+        if ("food_recommend".equals(t)) {
+            return "食材推荐";
+        }
+        if (t.startsWith("dan_")) {
+            return "DAN 测评";
+        }
+        if ("physical_exam".equals(t)) {
+            return "体检";
+        }
+        if ("tongue".equals(t)) {
+            return "舌诊";
+        }
+        return t;
+    }
+
     /** 指标速查(设计 6.3):分类分页 + 五维/七维评分小节 */
     public Map<String, Object> quickLookup(Long subjectId, String category, Integer page,
                                            Integer pageSize, Long familyId) {

+ 6 - 0
cfc-backend/src/main/resources/application.yml

@@ -103,6 +103,12 @@ wechat:
   promotion-tier-template-id: ${WECHAT_PROMOTION_TIER_TEMPLATE_ID:}
   # 小程序码环境:dev=development / test=trial / prod=release
   env-version: ${WECHAT_ENV_VERSION:release}
+  # 网站应用(微信开放平台“网站应用”)——桌面端扫码登录 SSO。
+  # 前置:该网站应用必须与小程序(appid=wx5ba8038ef16fb245)绑定同一开放平台账号,否则 unionid 不一致导致匹配失败。
+  web:
+    appid: ${WECHAT_WEB_APPID:}
+    secret: ${WECHAT_WEB_SECRET:}
+    redirect-uri: ${WECHAT_WEB_REDIRECT_URI:https://cfc.cf-club.com/api/auth/wechat-web/callback}
 
 dify:
   base-url: ${DIFY_BASE_URL:http://dify.bianwoyou.cn/v1}

+ 0 - 2
cfc-backend/src/main/resources/schema.sql

@@ -1190,7 +1190,6 @@ CREATE TABLE IF NOT EXISTS products (
     visible_roles JSON DEFAULT NULL COMMENT '可见角色: ["parent","child","teacher","admin"],NULL=全部可见',
     min_visible_level VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME,NULL=全部可见)',
     min_discount_level VARCHAR(50) DEFAULT NULL COMMENT '享受会员价所需的最低会员级别编码(FREE/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)',
-    vendor_visible TINYINT(1) DEFAULT NULL COMMENT '是否仅供应商体系内用户可见(列表接口生效,直接分享可 bypass)',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
@@ -1827,7 +1826,6 @@ CREATE TABLE IF NOT EXISTS health_reports (
     gut_type VARCHAR(50) COMMENT '肠型',
     file_url VARCHAR(500) COMMENT '报告文件URL',
     file_hash VARCHAR(64) DEFAULT NULL COMMENT '文件MD5哈希,用于重复上传检测',
-    report_format VARCHAR(32) DEFAULT NULL COMMENT '报告格式: pdf/image',
     status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/archived',
     remark TEXT COMMENT '备注',
     proxy_uploader_id BIGINT COMMENT '代上传人ID(规划师/营养师)',

+ 46 - 0
cfc-desktop-pet/.github/workflows/release.yml

@@ -0,0 +1,46 @@
+name: desktop-pet-release
+
+on:
+  workflow_dispatch:
+  push:
+    tags: ["desktop-pet-v*"]
+
+jobs:
+  build:
+    permissions:
+      contents: write
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - platform: windows-latest
+            args: ""
+          - platform: macos-latest
+            args: "--target universal-apple-darwin"
+    runs-on: ${{ matrix.platform }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Rust toolchain
+        uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
+
+      - name: Run tests
+        working-directory: cfc-desktop-pet/src-tauri
+        run: cargo test
+
+      - uses: tauri-apps/tauri-action@v0
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
+          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
+          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
+          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
+          APPLE_ID: ${{ secrets.APPLE_ID }}
+          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+        with:
+          projectPath: cfc-desktop-pet
+          args: ${{ matrix.args }}

+ 3 - 0
cfc-desktop-pet/.gitignore

@@ -0,0 +1,3 @@
+# .NET / WPF 构建产物(旧版 XafPet.Desktop / tests 目录)
+bin/
+obj/

+ 19 - 0
cfc-desktop-pet/src-tauri/.cargo/config.toml

@@ -0,0 +1,19 @@
+# 项目级 cargo 配置:crates.io 直连在 CN 网络下 HTTP/2 流重置频繁,改用 rsproxy.cn 稀疏索引镜像
+[source.crates-io]
+replace-with = 'rsproxy-sparse'
+
+[source.rsproxy]
+registry = "https://rsproxy.cn/crates.io-index"
+
+[source.rsproxy-sparse]
+registry = "sparse+https://rsproxy.cn/index/"
+
+[registries.rsproxy]
+index = "https://rsproxy.cn/crates.io-index"
+
+[net]
+git-fetch-with-cli = true
+
+# 规避 HTTP/2 流重置(PROTOCOL_ERROR 0x1)导致 index 更新卡死
+[http]
+multiplexing = false

+ 2 - 0
cfc-desktop-pet/src-tauri/.gitignore

@@ -0,0 +1,2 @@
+/target
+/gen

+ 5303 - 0
cfc-desktop-pet/src-tauri/Cargo.lock

@@ -0,0 +1,5303 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "atk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "blocking"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "brotli"
+version = "9.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "6.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.13.2",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9"
+dependencies = [
+ "semver",
+ "serde",
+ "toml 1.1.8+spec-1.1.0",
+]
+
+[[package]]
+name = "cc"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3"
+dependencies = [
+ "fnv",
+ "uuid",
+ "web-time",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.1",
+ "rand_core",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "cookie"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
+dependencies = [
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation 0.10.1",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation 0.10.1",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a"
+dependencies = [
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "ctor"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d"
+
+[[package]]
+name = "darling"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "dom_query"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc"
+dependencies = [
+ "bit-set",
+ "cssparser",
+ "foldhash",
+ "html5ever",
+ "precomputed-hash",
+ "selectors",
+ "tendril",
+]
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "embed-resource"
+version = "3.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml 1.1.8+spec-1.1.0",
+ "vswhom",
+ "winreg",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
+
+[[package]]
+name = "flate2"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.9.1",
+ "zlib-rs",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.13.2",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html5ever"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
+dependencies = [
+ "log",
+ "markup5ever",
+]
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ico"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
+dependencies = [
+ "byteorder",
+ "png 0.17.16",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "infer"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jiff"
+version = "0.2.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-link",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
+dependencies = [
+ "defmt",
+ "log",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
+dependencies = [
+ "bitflags 2.13.2",
+ "serde",
+]
+
+[[package]]
+name = "keyring"
+version = "3.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
+dependencies = [
+ "byteorder",
+ "log",
+ "security-framework 2.11.1",
+ "security-framework 3.7.0",
+ "windows-sys 0.60.2",
+ "zeroize",
+]
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.190"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
+
+[[package]]
+name = "markup5ever"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
+dependencies = [
+ "log",
+ "tendril",
+ "web_atoms",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "muda"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.21",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.13.2",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags 2.13.2",
+ "dispatch2",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "bitflags 2.13.2",
+ "dispatch2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-text"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.13.2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-core-text",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
+[[package]]
+name = "plist"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb"
+dependencies = [
+ "base64 0.23.1",
+ "indexmap 2.14.2",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide 0.8.9",
+]
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.13.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide 0.8.9",
+]
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit 0.25.17+spec-1.1.0",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.21",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe"
+dependencies = [
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.21",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0"
+dependencies = [
+ "libredox",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
+dependencies = [
+ "bitflags 2.13.2",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.2",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "selectors"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d"
+dependencies = [
+ "bitflags 2.13.2",
+ "cssparser",
+ "derive_more",
+ "log",
+ "new_debug_unreachable",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "servo_arc",
+ "smallvec",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1741ab7a6cc54a03a89b5d563ed60075c277d9e3cfa73ad0c1f23f23974703c6"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f"
+dependencies = [
+ "base64 0.23.1",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.2",
+ "jiff",
+ "schemars 0.9.0",
+ "schemars 1.2.2",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "siphasher"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
+dependencies = [
+ "bytemuck",
+ "js-sys",
+ "ndk",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "redox_syscall",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "string_cache"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.2",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.37.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400"
+dependencies = [
+ "bitflags 2.13.2",
+ "block2",
+ "core-foundation 0.10.1",
+ "core-graphics",
+ "crossbeam-channel",
+ "dbus",
+ "dispatch2",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "parking_lot",
+ "percent-encoding",
+ "raw-window-handle",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows",
+ "windows-core",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri"
+version = "2.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c404ecf956ff241bf6b39dd688614724e1bf1f63255abb4fc46d1c40371672ea"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cookie",
+ "dirs",
+ "dunce",
+ "embed_plist",
+ "getrandom 0.3.4",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "percent-encoding",
+ "plist",
+ "raw-window-handle",
+ "reqwest 0.13.5",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror 2.0.21",
+ "tokio",
+ "tray-icon",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eea0b6302f82c574b21af6fc6ae6e86183b9280853c22621d0abf0936911ffc"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02f468f9a9bcd404f57e5c191d91024e3de953d4b610f78c2907e7110615cb1c"
+dependencies = [
+ "base64 0.23.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png 0.18.1",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.119",
+ "tauri-utils",
+ "thiserror 2.0.21",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "131bffec939b20642380317f8403a33c8888489925e14bb965ceaff0d949b1cb"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin-single-instance"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c142ed88deee222bd2d979269d35c73b6c1c0f6ebd5b79b4ff80066fcad6af1"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tauri",
+ "thiserror 2.0.21",
+ "tokio",
+ "tracing",
+ "windows-sys 0.61.2",
+ "zbus",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.21",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dom_query",
+ "dunce",
+ "glob",
+ "http",
+ "infer",
+ "json-patch",
+ "log",
+ "memchr",
+ "phf",
+ "plist",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.21",
+ "toml 1.1.8+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6"
+dependencies = [
+ "dunce",
+ "embed-resource",
+ "toml 1.1.8+spec-1.1.0",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tendril"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
+dependencies = [
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl 2.0.21",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
+
+[[package]]
+name = "tokio"
+version = "1.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.8+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
+dependencies = [
+ "indexmap 2.14.2",
+ "serde_core",
+ "serde_spanned 1.1.2",
+ "toml_datetime 1.1.2+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.14.2",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap 2.14.2",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.17+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
+dependencies = [
+ "indexmap 2.14.2",
+ "toml_datetime 1.1.2+spec-1.1.0",
+ "toml_parser",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
+dependencies = [
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.13.2",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72"
+dependencies = [
+ "crossbeam-channel",
+ "dirs",
+ "libappindicator",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.21",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "unicase"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b"
+dependencies = [
+ "icu_properties",
+ "regex",
+ "serde",
+ "url",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
+dependencies = [
+ "js-sys",
+ "tokio",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web_atoms"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"
+dependencies = [
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.39.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.39.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb"
+dependencies = [
+ "thiserror 2.0.21",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580"
+dependencies = [
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "windows-sys 0.61.2",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "wry"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0"
+dependencies = [
+ "base64 0.22.1",
+ "block2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs",
+ "dom_query",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "libc",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.21",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows",
+ "windows-core",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "xaf-pet-desktop"
+version = "0.1.0"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "keyring",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-single-instance",
+ "tokio",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+ "synstructure",
+]
+
+[[package]]
+name = "zbus"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 1.0.4",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zvariant",
+]
+
+[[package]]
+name = "zcheapstr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+
+[[package]]
+name = "zvariant"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 1.0.4",
+ "zcheapstr",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.7",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 3.0.7",
+ "winnow 1.0.4",
+]

+ 22 - 0
cfc-desktop-pet/src-tauri/Cargo.toml

@@ -0,0 +1,22 @@
+[package]
+name = "xaf-pet-desktop"
+version = "0.1.0"
+description = "浠艾福桌面宠物(Windows + macOS)"
+edition = "2021"
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+
+[dependencies]
+tauri = { version = "2", features = ["tray-icon", "macos-private-api"] }
+tauri-plugin-single-instance = "2"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "multipart"] }
+tokio = { version = "1", features = ["time", "macros", "rt-multi-thread"] }
+chrono = "0.4"
+base64 = "0.22"
+keyring = { version = "3", features = ["apple-native", "windows-native"] }
+
+[profile.release]
+strip = true

+ 3 - 0
cfc-desktop-pet/src-tauri/build.rs

@@ -0,0 +1,3 @@
+fn main() {
+    tauri_build::build()
+}

+ 7 - 0
cfc-desktop-pet/src-tauri/capabilities/default.json

@@ -0,0 +1,7 @@
+{
+  "$schema": "../gen/schemas/desktop-schema.json",
+  "identifier": "default",
+  "description": "XafPet 前端权限:core 默认集(含 event listen)",
+  "windows": ["main", "settings"],
+  "permissions": ["core:default"]
+}

+ 5 - 0
cfc-desktop-pet/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+  <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+  <background android:drawable="@color/ic_launcher_background"/>
+</adaptive-icon>

+ 4 - 0
cfc-desktop-pet/src-tauri/icons/android/values/ic_launcher_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <color name="ic_launcher_background">#FFFFFF</color>
+</resources>

二进制
cfc-desktop-pet/src-tauri/icons/icon.icns


二进制
cfc-desktop-pet/src-tauri/icons/icon.ico


+ 188 - 0
cfc-desktop-pet/src-tauri/src/api/mod.rs

@@ -0,0 +1,188 @@
+pub mod models;
+
+pub use models::*;
+use serde::de::DeserializeOwned;
+use std::sync::RwLock;
+
+#[derive(Debug, Clone)]
+pub enum ApiError {
+    Http(String),
+    Parse,
+    Api { code: i32, message: String },
+    EmptyData,
+}
+
+impl std::fmt::Display for ApiError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ApiError::Http(m) => write!(f, "http: {m}"),
+            ApiError::Parse => write!(f, "响应解析失败"),
+            ApiError::Api { code, message } => write!(f, "服务错误 {code}: {message}"),
+            ApiError::EmptyData => write!(f, "响应 data 为空"),
+        }
+    }
+}
+
+pub struct ApiClient {
+    http: reqwest::Client,
+    base_url: RwLock<String>,
+    token: RwLock<String>,
+}
+
+impl ApiClient {
+    pub fn new(base_url: String, token: String) -> Self {
+        Self {
+            http: reqwest::Client::new(),
+            base_url: RwLock::new(base_url.trim_end_matches('/').to_string()),
+            token: RwLock::new(token),
+        }
+    }
+
+    pub fn update_token(&self, token: String) {
+        *self.token.write().unwrap() = token;
+    }
+
+    pub fn set_base_url(&self, base_url: String) {
+        *self.base_url.write().unwrap() = base_url.trim_end_matches('/').to_string();
+    }
+
+    pub fn parse_response<T: DeserializeOwned>(&self, body: &str) -> Result<T, ApiError> {
+        let result: ApiResult<T> = serde_json::from_str(body).map_err(|_| ApiError::Parse)?;
+        if !result.is_success() {
+            return Err(ApiError::Api {
+                code: result.code,
+                message: result.message.unwrap_or_default(),
+            });
+        }
+        result.data.ok_or(ApiError::EmptyData)
+    }
+
+    async fn post<T: DeserializeOwned>(&self, path: &str, body: Option<&serde_json::Value>) -> Result<T, ApiError> {
+        let url = format!("{}{}", *self.base_url.read().unwrap(), path);
+        let token = self.token.read().unwrap().clone();
+        let mut req = self.http.post(&url).bearer_auth(token);
+        if let Some(b) = body {
+            req = req.json(b);
+        }
+        let resp = req.send().await.map_err(|e| ApiError::Http(e.to_string()))?;
+        let text = resp.text().await.map_err(|e| ApiError::Http(e.to_string()))?;
+        self.parse_response(&text)
+    }
+
+    pub async fn tasks_today(&self) -> Result<Vec<CfcTask>, ApiError> {
+        self.post("/api/tasks/today", Some(&serde_json::json!({}))).await
+    }
+
+    pub async fn energy_overview(&self) -> Result<EnergyOverview, ApiError> {
+        // 后端 @RequestBody(required=false),但实测必须带 body 才会进入业务逻辑
+        self.post("/api/energy/overview", Some(&serde_json::json!({}))).await
+    }
+
+    /// 最近喝水记录(memberId 由后端从 JWT 解析)
+    pub async fn water_list(&self) -> Result<Vec<WaterRecord>, ApiError> {
+        self.post("/api/health/water/list", Some(&serde_json::json!({}))).await
+    }
+
+    /// 完成一次喝水打卡(固定快捷打卡:250ml / plain / 当前时间)
+    pub async fn complete_water(&self) -> Result<WaterRecord, ApiError> {
+        let drunk_at = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
+        self.post(
+            "/api/health/water/create",
+            Some(&serde_json::json!({
+                "amountMl": 250,
+                "waterType": "plain",
+                "drunkAt": drunk_at
+            })),
+        )
+        .await
+    }
+
+    pub async fn mascot_get(&self) -> Result<MascotInfo, ApiError> {
+        self.post("/api/user/mascot/get", None).await
+    }
+
+    /// 一键完成任务(memberId 由后端从 JWT 解析)
+    pub async fn complete_task(&self, task_id: i64) -> Result<serde_json::Value, ApiError> {
+        self.post(&format!("/api/tasks/{task_id}/complete"), Some(&serde_json::json!({}))).await
+    }
+
+    /// 上传图片等媒体文件(multipart,复用 /api/media/upload)
+    pub async fn upload_media(
+        &self,
+        filename: &str,
+        bytes: Vec<u8>,
+        mime: &str,
+    ) -> Result<MediaRecord, ApiError> {
+        let url = format!("{}/api/media/upload", *self.base_url.read().unwrap());
+        let token = self.token.read().unwrap().clone();
+        let part = reqwest::multipart::Part::bytes(bytes.clone()).file_name(filename.to_string());
+        let part = if mime.is_empty() {
+            part
+        } else {
+            part.mime_str(mime).unwrap_or_else(|_| {
+                reqwest::multipart::Part::bytes(bytes).file_name(filename.to_string())
+            })
+        };
+        let form = reqwest::multipart::Form::new().part("file", part);
+        let resp = self
+            .http
+            .post(&url)
+            .bearer_auth(token)
+            .multipart(form)
+            .send()
+            .await
+            .map_err(|e| ApiError::Http(e.to_string()))?;
+        let text = resp.text().await.map_err(|e| ApiError::Http(e.to_string()))?;
+        self.parse_response(&text)
+    }
+
+    /// 微信网站应用扫码登录:获取二维码链接
+    pub async fn wechat_web_qr(&self) -> Result<WechatQrResp, ApiError> {
+        self.post("/api/auth/wechat-web/qr", Some(&serde_json::json!({}))).await
+    }
+
+    /// 轮询扫码登录结果(state 由 wechat_web_qr 返回)
+    pub async fn wechat_web_poll(&self, state: &str) -> Result<WechatPollResp, ApiError> {
+        self.post("/api/auth/wechat-web/poll", Some(&serde_json::json!({ "state": state }))).await
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn parse_success_response() {
+        let client = ApiClient::new("http://x".into(), "t".into());
+        let data: Vec<CfcTask> = client
+            .parse_response(r#"{"code":200,"data":[{"id":1,"title":"t","status":"PENDING","deadline":"2026-08-15T00:00:00"}]}"#)
+            .unwrap();
+        assert_eq!(data.len(), 1);
+        assert_eq!(
+            data[0].deadline.as_ref().unwrap().to_local_date(),
+            chrono::NaiveDate::from_ymd_opt(2026, 8, 15)
+        );
+    }
+
+    #[test]
+    fn parse_error_code_returns_api_error() {
+        let client = ApiClient::new("http://x".into(), "t".into());
+        let err = client.parse_response::<serde_json::Value>(r#"{"code":401,"message":"unauthorized"}"#).unwrap_err();
+        match err {
+            ApiError::Api { code, .. } => assert_eq!(code, 401),
+            other => panic!("unexpected error: {other:?}"),
+        }
+    }
+
+    #[test]
+    fn parse_malformed_returns_parse_error() {
+        let client = ApiClient::new("http://x".into(), "t".into());
+        assert!(matches!(client.parse_response::<serde_json::Value>("not json"), Err(ApiError::Parse)));
+    }
+
+    #[test]
+    fn parse_empty_data_returns_empty_error() {
+        let client = ApiClient::new("http://x".into(), "t".into());
+        assert!(matches!(client.parse_response::<CfcTask>(r#"{"code":200}"#), Err(ApiError::EmptyData)));
+    }
+}

+ 237 - 0
cfc-desktop-pet/src-tauri/src/api/models.rs

@@ -0,0 +1,237 @@
+use chrono::TimeZone;
+use serde::Deserialize;
+
+/// CFC 统一响应包装 Result<T>(code=200 视为成功)
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ApiResult<T> {
+    pub code: i32,
+    pub message: Option<String>,
+    pub data: Option<T>,
+}
+
+impl<T> ApiResult<T> {
+    pub fn is_success(&self) -> bool {
+        self.code == 200
+    }
+}
+
+/// 兼容后端可能返回的日期格式:ISO 8601 字符串 或 epoch 毫秒数字。
+///
+/// Spring Boot 默认把 `java.util.Date` 序列化为 epoch 毫秒(数字);项目内个别
+/// 字段用 `@JsonFormat` 输出字符串。两种都接受,统一归一化为本地日期。
+#[derive(Debug, Clone, Deserialize)]
+#[serde(untagged)]
+pub enum FlexDate {
+    Iso(String),
+    Millis(i64),
+}
+
+impl FlexDate {
+    /// 归一化为本地时区日期(yyyy-MM-dd)。
+    pub fn to_local_date(&self) -> Option<chrono::NaiveDate> {
+        match self {
+            FlexDate::Iso(s) => {
+                if s.len() < 10 {
+                    return None;
+                }
+                let head = &s[..10];
+                chrono::NaiveDate::parse_from_str(head, "%Y-%m-%d").ok()
+            }
+            FlexDate::Millis(ms) => chrono::Local
+                .timestamp_millis_opt(*ms)
+                .single()
+                .map(|dt| dt.date_naive()),
+        }
+    }
+}
+
+/// POST /api/tasks/today 返回的 Task(`tasks` 表)
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CfcTask {
+    pub id: i64,
+    pub title: Option<String>,
+    pub status: Option<String>, // PENDING / IN_PROGRESS / COMPLETED / REJECTED
+    pub deadline: Option<FlexDate>,
+}
+
+/// /api/energy/overview 五维维度项:{code,name,icon,element,energy,healthIndex}
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DimensionValue {
+    pub code: Option<String>, // body/mind/wisdom/action/wealth
+    pub name: Option<String>, // 身/心/智/行/富
+    pub icon: Option<String>,
+    pub element: Option<String>,
+    #[serde(default)]
+    pub energy: i32,
+    #[serde(default)]
+    pub health_index: i32,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EnergyOverview {
+    #[serde(default)]
+    pub dimensions: Vec<DimensionValue>,
+    #[serde(default)]
+    pub total_energy: i32,
+    #[serde(default)]
+    pub total_health_index: i32,
+}
+
+/// /api/user/mascot/get → {"mascot": "xibao"|"fubao"|null}
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MascotInfo {
+    pub mascot: Option<String>,
+}
+
+/// /api/health/water/list|create(`water_record` 表)
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct WaterRecord {
+    pub id: Option<i64>,
+    pub member_id: Option<i64>,
+    pub checkin_id: Option<i64>,
+    pub amount_ml: Option<i32>,
+    pub water_type: Option<String>,
+    pub drunk_at: Option<FlexDate>,
+    pub created_at: Option<FlexDate>,
+}
+
+/// /api/media/upload 返回的媒体记录(仅取所需字段,其余忽略)
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MediaRecord {
+    #[allow(dead_code)]
+    pub id: Option<i64>,
+    pub url: Option<String>,
+}
+
+/// POST /api/auth/wechat-web/qr → {state, url}
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct WechatQrResp {
+    pub state: String,
+    pub url: String,
+}
+
+/// POST /api/auth/wechat-web/poll → {status, token?, userId?, role?, nickname?, message?}
+/// status: pending(等待扫码) / success / notfound(微信未绑定小程序账号) / expired / error
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct WechatPollResp {
+    pub status: String,
+    pub token: Option<String>,
+    pub user_id: Option<i64>,
+    pub role: Option<String>,
+    pub nickname: Option<String>,
+    #[allow(dead_code)]
+    pub message: Option<String>,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn parses_media_record_upload_response() {
+        let json = r#"{"code":200,"message":"success","data":{"id":9,"type":"image","url":"https://cdn/x.png","creatorId":null}}"#;
+        let r: ApiResult<MediaRecord> = serde_json::from_str(json).unwrap();
+        assert!(r.is_success());
+        let rec = r.data.unwrap();
+        assert_eq!(rec.id, Some(9));
+        assert_eq!(rec.url.as_deref(), Some("https://cdn/x.png"));
+    }
+
+    #[test]
+    fn parses_task_with_iso_deadline() {
+        let json = r#"{"id":3,"title":"喝水","status":"PENDING","deadline":"2026-08-15T10:00:00"}"#;
+        let task: CfcTask = serde_json::from_str(json).unwrap();
+        assert_eq!(task.id, 3);
+        assert_eq!(task.status.as_deref(), Some("PENDING"));
+        assert_eq!(
+            task.deadline.as_ref().unwrap().to_local_date(),
+            chrono::NaiveDate::from_ymd_opt(2026, 8, 15)
+        );
+    }
+
+    #[test]
+    fn parses_task_with_millis_deadline() {
+        let json = r#"{"id":4,"title":"t","status":"PENDING","deadline":1786752000000}"#;
+        let task: CfcTask = serde_json::from_str(json).unwrap();
+        assert!(task.deadline.as_ref().unwrap().to_local_date().is_some());
+    }
+
+    #[test]
+    fn parses_task_with_null_deadline() {
+        let json = r#"{"id":5,"title":"t","status":"PENDING","deadline":null}"#;
+        let task: CfcTask = serde_json::from_str(json).unwrap();
+        assert!(task.deadline.is_none());
+    }
+
+    #[test]
+    fn flex_date_millis_zero_is_a_valid_date() {
+        let d: FlexDate = serde_json::from_str("0").unwrap();
+        assert!(d.to_local_date().is_some());
+    }
+
+    #[test]
+    fn parses_energy_overview_and_defaults_to_empty() {
+        let json = r##"{"dimensions":[{"code":"body","name":"身","energy":65,"healthIndex":0}],"totalEnergy":65,"totalHealthIndex":0}"##;
+        let e: EnergyOverview = serde_json::from_str(json).unwrap();
+        assert_eq!(e.dimensions.len(), 1);
+        assert_eq!(e.dimensions[0].energy, 65);
+        assert_eq!(e.dimensions[0].code.as_deref(), Some("body"));
+
+        let empty: EnergyOverview = serde_json::from_str(r#"{}"#).unwrap();
+        assert!(empty.dimensions.is_empty());
+        assert_eq!(empty.total_energy, 0);
+    }
+
+    #[test]
+    fn parses_mascot_response() {
+        let json = r#"{"code":200,"message":"success","data":{"mascot":"xibao"}}"#;
+        let r: ApiResult<MascotInfo> = serde_json::from_str(json).unwrap();
+        assert!(r.is_success());
+        assert_eq!(r.data.unwrap().mascot.as_deref(), Some("xibao"));
+    }
+
+    #[test]
+    fn parses_mascot_null() {
+        let json = r#"{"code":200,"message":"success","data":{"mascot":null}}"#;
+        let r: ApiResult<MascotInfo> = serde_json::from_str(json).unwrap();
+        assert!(r.is_success());
+        assert!(r.data.unwrap().mascot.is_none());
+    }
+
+    #[test]
+    fn parses_wechat_qr_response() {
+        let json = r#"{"code":200,"message":"success","data":{"state":"abc123","url":"https://open.weixin.qq.com/connect/qrconnect?appid=x"}}"#;
+        let r: ApiResult<WechatQrResp> = serde_json::from_str(json).unwrap();
+        assert!(r.is_success());
+        let q = r.data.unwrap();
+        assert_eq!(q.state, "abc123");
+        assert!(q.url.contains("qrconnect"));
+    }
+
+    #[test]
+    fn parses_wechat_poll_success_and_pending() {
+        let ok: ApiResult<WechatPollResp> = serde_json::from_str(
+            r#"{"code":200,"data":{"status":"success","token":"jwt","userId":7,"role":"parent","nickname":"牛"}}"#,
+        )
+        .unwrap();
+        let p = ok.data.unwrap();
+        assert_eq!(p.status, "success");
+        assert_eq!(p.token.as_deref(), Some("jwt"));
+        assert_eq!(p.user_id, Some(7));
+
+        let pending: ApiResult<WechatPollResp> =
+            serde_json::from_str(r#"{"code":200,"data":{"status":"pending"}}"#).unwrap();
+        let p2 = pending.data.unwrap();
+        assert_eq!(p2.status, "pending");
+        assert!(p2.token.is_none());
+    }
+}

+ 10 - 0
cfc-desktop-pet/src-tauri/src/app_state.rs

@@ -0,0 +1,10 @@
+use crate::api::ApiClient;
+use std::sync::{Arc, Mutex};
+
+pub struct AppState {
+    pub passthrough: Mutex<bool>,
+    pub poll_handle: Mutex<Option<tauri::async_runtime::JoinHandle<()>>>,
+    /// 当前提醒中、可一键完成的待办任务 id(点击宠物即完成)
+    pub pending_task: Mutex<Option<i64>>,
+    pub api: Arc<ApiClient>,
+}

+ 290 - 0
cfc-desktop-pet/src-tauri/src/commands.rs

@@ -0,0 +1,290 @@
+use crate::app_state::AppState;
+use crate::poller;
+use crate::settings::SettingsStore;
+use crate::state::{PetState, PetStatePayload};
+use crate::token::TokenStore;
+use tauri::{AppHandle, Emitter, Manager, State};
+
+#[tauri::command]
+pub async fn on_interact(app: AppHandle, action: String, _data: Option<String>) -> Result<(), String> {
+    match action.as_str() {
+        "click" => {
+            // 有可一键完成的待办任务时优先完成它,否则回退到喝水打卡
+            let pending = app
+                .try_state::<AppState>()
+                .and_then(|s| *s.pending_task.lock().unwrap());
+            match pending {
+                Some(id) => complete_task_by_id(&app, id).await,
+                None => complete_water(&app).await,
+            }
+        }
+        "pet" => {
+            let _ = app.emit("pet://bubble", serde_json::json!({"text": "嘻嘻~", "durationMs": 2000}));
+            Ok(())
+        }
+        _ => Ok(()),
+    }
+}
+
+#[tauri::command]
+pub async fn drag_start(app: AppHandle) -> Result<(), String> {
+    if let Some(win) = app.get_webview_window("main") {
+        win.start_dragging().map_err(|e| e.to_string())?;
+    }
+    Ok(())
+}
+
+#[tauri::command]
+pub async fn drag_end() -> Result<(), String> {
+    Ok(())
+}
+
+#[tauri::command]
+pub fn open_settings(app: AppHandle) -> Result<(), String> {
+    if let Some(win) = app.get_webview_window("settings") {
+        let _ = win.show();
+        let _ = win.set_focus();
+    }
+    Ok(())
+}
+
+#[tauri::command]
+pub fn quit_app(app: AppHandle) -> Result<(), String> {
+    app.exit(0);
+    Ok(())
+}
+
+#[tauri::command]
+pub fn get_settings(
+    _state: State<AppState>,
+    settings: State<SettingsStore>,
+) -> Result<serde_json::Value, String> {
+    let cfg = settings.load();
+    let has_token = TokenStore::new().load().map(|t| t.is_some()).unwrap_or(false);
+    Ok(serde_json::json!({
+        "baseUrl": cfg.base_url,
+        "hasToken": has_token,
+        "customMascotUrl": cfg.custom_mascot_url
+    }))
+}
+
+#[tauri::command]
+pub async fn upload_mascot(
+    app: AppHandle,
+    settings: State<'_, SettingsStore>,
+    filename: String,
+    mime: String,
+    data_base64: String,
+) -> Result<String, String> {
+    use base64::Engine;
+    let bytes = base64::engine::general_purpose::STANDARD
+        .decode(data_base64.as_bytes())
+        .map_err(|e| format!("图片解码失败: {e}"))?;
+    if bytes.is_empty() {
+        return Err("图片内容为空".into());
+    }
+    if bytes.len() > 10 * 1024 * 1024 {
+        return Err("图片过大(上限 10MB)".into());
+    }
+
+    let state = app.state::<AppState>();
+    let record = state
+        .api
+        .upload_media(&filename, bytes, &mime)
+        .await
+        .map_err(|e| format!("上传失败: {e}"))?;
+    let url = record.url.ok_or_else(|| "上传响应缺少 url".to_string())?;
+
+    let mut cfg = settings.load();
+    cfg.custom_mascot_url = Some(url.clone());
+    settings.save(&cfg)?;
+
+    let _ = app.emit("pet://custom-image", serde_json::json!({ "url": url }));
+    Ok(url)
+}
+
+#[tauri::command]
+pub fn clear_custom_mascot(app: AppHandle, settings: State<SettingsStore>) -> Result<(), String> {
+    let mut cfg = settings.load();
+    cfg.custom_mascot_url = None;
+    settings.save(&cfg)?;
+    let _ = app.emit("pet://custom-image", serde_json::json!({ "url": null }));
+    Ok(())
+}
+
+/// 微信扫码登录:取二维码链接 → 用系统浏览器打开 → 后台轮询换取 JWT → 写入 keyring。
+/// 结果通过 `pet://wechat-login` 事件回传
+/// (status: pending / success / notfound / expired / error / timeout)。
+#[tauri::command]
+pub async fn start_wechat_login(app: AppHandle) -> Result<serde_json::Value, String> {
+    let qr = {
+        let state = app.state::<AppState>();
+        state
+            .api
+            .wechat_web_qr()
+            .await
+            .map_err(|e| format!("获取二维码失败: {e}"))?
+    };
+    open_url(&qr.url)?;
+
+    let api = app.state::<AppState>().api.clone();
+    let state_token = qr.state.clone();
+    let handle = app.clone();
+    tauri::async_runtime::spawn(async move {
+        // 最多轮询约 3 分钟(2s 间隔 × 90 次)
+        for _ in 0..90 {
+            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
+            match api.wechat_web_poll(&state_token).await {
+                Ok(resp) => match resp.status.as_str() {
+                    "pending" => continue,
+                    "success" => {
+                        let Some(token) = resp.token else {
+                            let _ = handle.emit(
+                                "pet://wechat-login",
+                                serde_json::json!({"status": "error", "message": "响应缺少 token"}),
+                            );
+                            return;
+                        };
+                        if let Err(e) = TokenStore::new().save(&token) {
+                            let _ = handle.emit(
+                                "pet://wechat-login",
+                                serde_json::json!({"status": "error", "message": format!("保存令牌失败: {e}")}),
+                            );
+                            return;
+                        }
+                        api.update_token(token);
+                        if let Some(s) = handle.try_state::<AppState>() {
+                            restart_polling(&handle, &s);
+                        }
+                        let _ = handle.emit(
+                            "pet://wechat-login",
+                            serde_json::json!({"status": "success", "nickname": resp.nickname}),
+                        );
+                        return;
+                    }
+                    other => {
+                        let _ = handle.emit(
+                            "pet://wechat-login",
+                            serde_json::json!({"status": other, "message": resp.message}),
+                        );
+                        return;
+                    }
+                },
+                Err(_) => continue, // 瞬时网络错误:继续轮询
+            }
+        }
+        let _ = handle.emit("pet://wechat-login", serde_json::json!({"status": "timeout"}));
+    });
+
+    Ok(serde_json::json!({ "status": "pending", "url": qr.url }))
+}
+
+/// 用系统默认浏览器打开 URL(Windows: cmd start / macOS: open / 其它: xdg-open)。
+fn open_url(url: &str) -> Result<(), String> {
+    #[cfg(target_os = "windows")]
+    {
+        std::process::Command::new("cmd")
+            .args(["/C", "start", "", url])
+            .spawn()
+            .map_err(|e| e.to_string())?;
+    }
+    #[cfg(target_os = "macos")]
+    {
+        std::process::Command::new("open")
+            .arg(url)
+            .spawn()
+            .map_err(|e| e.to_string())?;
+    }
+    #[cfg(not(any(target_os = "windows", target_os = "macos")))]
+    {
+        std::process::Command::new("xdg-open")
+            .arg(url)
+            .spawn()
+            .map_err(|e| e.to_string())?;
+    }
+    Ok(())
+}
+
+#[tauri::command]
+pub fn save_settings(
+    app: AppHandle,
+    state: State<AppState>,
+    settings: State<SettingsStore>,
+    base_url: String,
+    token: Option<String>,
+) -> Result<(), String> {
+    let mut cfg = settings.load();
+    cfg.base_url = base_url.trim().to_string();
+    settings.save(&cfg)?;
+
+    if let Some(t) = token {
+        if !t.trim().is_empty() {
+            TokenStore::new().save(t.trim())?;
+        }
+    }
+
+    // 更新运行时客户端并重启轮询(对齐现 ShowSettings 的 Stop/Start)
+    state.api.set_base_url(cfg.base_url.clone());
+    if let Some(t) = TokenStore::new().load()? {
+        state.api.update_token(t);
+    }
+    restart_polling(&app, &state);
+
+    if let Some(win) = app.get_webview_window("settings") {
+        let _ = win.hide();
+    }
+    Ok(())
+}
+
+fn restart_polling(app: &AppHandle, state: &State<AppState>) {
+    if let Some(h) = state.poll_handle.lock().unwrap().take() {
+        h.abort();
+    }
+    let handle = poller::spawn(app.clone(), state.api.clone());
+    *state.poll_handle.lock().unwrap() = Some(handle);
+}
+
+async fn complete_water(app: &AppHandle) -> Result<(), String> {
+    let state = app.state::<AppState>();
+    let api = state.api.clone();
+    match api.complete_water().await {
+        Ok(_) => {
+            let payload = PetStatePayload {
+                state: PetState::Happy,
+                reason: Some("drink_complete".into()),
+                data: None,
+            };
+            let _ = app.emit("pet://state", payload);
+            let _ = app.emit("pet://bubble", serde_json::json!({"text": "喝水打卡成功!太棒了~", "durationMs": 4000}));
+            Ok(())
+        }
+        Err(_) => {
+            let _ = app.emit("pet://bubble", serde_json::json!({"text": "打卡失败,请稍后再试", "durationMs": 4000}));
+            Err("checkin failed".into())
+        }
+    }
+}
+
+async fn complete_task_by_id(app: &AppHandle, task_id: i64) -> Result<(), String> {
+    let state = app.state::<AppState>();
+    let api = state.api.clone();
+    match api.complete_task(task_id).await {
+        Ok(_) => {
+            if let Some(s) = app.try_state::<AppState>() {
+                *s.pending_task.lock().unwrap() = None;
+            }
+            let payload = PetStatePayload {
+                state: PetState::Happy,
+                reason: Some("task_complete".into()),
+                data: None,
+            };
+            let _ = app.emit("pet://state", payload);
+            let _ = app.emit("pet://bubble", serde_json::json!({"text": "任务完成!太棒了~", "durationMs": 4000}));
+            Ok(())
+        }
+        Err(_) => {
+            let _ = app.emit("pet://bubble", serde_json::json!({"text": "完成任务失败,请稍后再试", "durationMs": 4000}));
+            Err("task complete failed".into())
+        }
+    }
+}

+ 123 - 0
cfc-desktop-pet/src-tauri/src/config.rs

@@ -0,0 +1,123 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum PetType {
+    #[default]
+    Animal,
+    Human,
+    Custom,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub struct PetConfig {
+    #[serde(default = "default_version")]
+    pub version: String,
+    #[serde(rename = "type", default)]
+    pub pet_type: PetType,
+    #[serde(default = "default_species")]
+    pub species: String,
+    #[serde(default = "default_gender")]
+    pub gender: String,
+    #[serde(default)]
+    pub image_base_url: String,
+    #[serde(default = "default_image")]
+    pub default_image: String,
+    #[serde(default)]
+    pub actions: HashMap<String, String>,
+    #[serde(default)]
+    pub sounds: HashMap<String, String>,
+    #[serde(default)]
+    pub parameters: PetParameters,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub struct PetParameters {
+    #[serde(default = "default_move_distance")]
+    pub move_distance: i32,
+    #[serde(default = "default_animation_speed")]
+    pub animation_speed: f64,
+    #[serde(default = "default_interaction_timeout")]
+    pub interaction_timeout: i32,
+}
+
+impl Default for PetParameters {
+    fn default() -> Self {
+        Self {
+            move_distance: default_move_distance(),
+            animation_speed: default_animation_speed(),
+            interaction_timeout: default_interaction_timeout(),
+        }
+    }
+}
+
+fn default_version() -> String { "1.0".into() }
+fn default_species() -> String { "other".into() }
+fn default_gender() -> String { "neutral".into() }
+fn default_image() -> String { "standing.png".into() }
+fn default_move_distance() -> i32 { 15 }
+fn default_animation_speed() -> f64 { 1.0 }
+fn default_interaction_timeout() -> i32 { 300 }
+
+impl PetConfig {
+    /// 解析 JSON;actions 为空时补默认映射(idle/happy/busy/sleeping)
+    pub fn parse(json: &str) -> Result<Self, serde_json::Error> {
+        let mut cfg: PetConfig = serde_json::from_str(json)?;
+        if cfg.actions.is_empty() {
+            cfg.actions = [
+                ("idle".to_string(), "idle".to_string()),
+                ("happy".to_string(), "happy".to_string()),
+                ("busy".to_string(), "busy".to_string()),
+                ("sleeping".to_string(), "sleeping".to_string()),
+            ]
+            .into_iter()
+            .collect();
+        }
+        Ok(cfg)
+    }
+
+    pub fn get_action_for_state(&self, state_name: &str) -> String {
+        self.actions.get(state_name).cloned().unwrap_or_else(|| state_name.to_string())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const XIBAO_JSON: &str = include_str!("../../src/DefaultPets/xibao/pet-config.json");
+
+    #[test]
+    fn parses_real_xibao_config() {
+        let cfg = PetConfig::parse(XIBAO_JSON).unwrap();
+        assert_eq!(cfg.version, "1.0");
+        assert!(matches!(cfg.pet_type, PetType::Animal));
+        assert_eq!(cfg.image_base_url, "DefaultPets/xibao/");
+        assert_eq!(cfg.default_image, "standing.png");
+        assert!(cfg.actions.contains_key("idle"));
+        assert_eq!(cfg.get_action_for_state("happy"), "wings_spread");
+    }
+
+    #[test]
+    fn fills_default_actions_when_empty() {
+        let json = r#"{"version":"1.0","type":"custom","actions":{}}"#;
+        let cfg = PetConfig::parse(json).unwrap();
+        assert_eq!(cfg.actions.len(), 4);
+        assert_eq!(cfg.actions["idle"], "idle");
+    }
+
+    #[test]
+    fn action_falls_back_to_state_name() {
+        let json = r#"{"type":"custom","actions":{"idle":"idle"}}"#;
+        let cfg = PetConfig::parse(json).unwrap();
+        assert_eq!(cfg.get_action_for_state("no_such_state"), "no_such_state");
+    }
+
+    #[test]
+    fn parse_error_on_bad_json() {
+        assert!(PetConfig::parse("not json").is_err());
+    }
+}

+ 58 - 0
cfc-desktop-pet/src-tauri/src/main.rs

@@ -0,0 +1,58 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+mod api;
+mod app_state;
+mod commands;
+mod config;
+mod poller;
+mod settings;
+mod state;
+mod token;
+mod tray;
+
+use app_state::AppState;
+use std::sync::Arc;
+use tauri::Manager;
+
+fn main() {
+    tauri::Builder::default()
+        // 单实例插件必须最先注册
+        .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
+            if let Some(win) = app.get_webview_window("main") {
+                let _ = win.show();
+                let _ = win.set_focus();
+            }
+        }))
+        .setup(|app| {
+            let config_dir = app.path().app_config_dir().map_err(|e| e.to_string())?;
+            let settings = settings::SettingsStore::new(config_dir);
+            let cfg = settings.load();
+            let token = token::TokenStore::new().load().ok().flatten().unwrap_or_else(|| "dev-token".into());
+            let api = Arc::new(api::ApiClient::new(cfg.base_url.clone(), token));
+
+            let handle = poller::spawn(app.handle().clone(), api.clone());
+            app.manage(settings);
+            app.manage(AppState {
+                passthrough: std::sync::Mutex::new(false),
+                poll_handle: std::sync::Mutex::new(Some(handle)),
+                pending_task: std::sync::Mutex::new(None),
+                api,
+            });
+            tray::setup_tray(app)?;
+            Ok(())
+        })
+        .invoke_handler(tauri::generate_handler![
+            commands::on_interact,
+            commands::drag_start,
+            commands::drag_end,
+            commands::get_settings,
+            commands::save_settings,
+            commands::open_settings,
+            commands::quit_app,
+            commands::upload_mascot,
+            commands::clear_custom_mascot,
+            commands::start_wechat_login,
+        ])
+        .run(tauri::generate_context!())
+        .expect("error while running tauri application");
+}

+ 236 - 0
cfc-desktop-pet/src-tauri/src/poller.rs

@@ -0,0 +1,236 @@
+use crate::api::{models::{CfcTask, WaterRecord}, ApiClient};
+use crate::app_state::AppState;
+use crate::config::PetConfig;
+use crate::state::{map_state, PetState, PetStatePayload};
+use chrono::Timelike;
+use std::sync::Arc;
+use std::time::Duration;
+use tauri::{AppHandle, Emitter, Manager};
+
+/// 启动轮询任务:启动即同步吉祥物+喝水检查,然后 30s 任务 / 60s 健康 / 1h 喝水。
+/// 返回 JoinHandle 以便 save_settings 时 abort 重启。
+pub fn spawn(app: AppHandle, api: Arc<ApiClient>) -> tauri::async_runtime::JoinHandle<()> {
+    tauri::async_runtime::spawn(async move {
+        sync_mascot(&app, &api).await;
+        check_water(&app, &api).await;
+
+        let mut tasks_iv = tokio::time::interval(Duration::from_secs(30));
+        let mut health_iv = tokio::time::interval(Duration::from_secs(60));
+        let mut water_iv = tokio::time::interval(Duration::from_secs(3600));
+
+        loop {
+            tokio::select! {
+                _ = tasks_iv.tick() => poll_tasks(&app, &api).await,
+                _ = health_iv.tick() => poll_health(&app, &api).await,
+                _ = water_iv.tick() => check_water(&app, &api).await,
+            }
+        }
+    })
+}
+
+fn in_active_hour(hour: u32) -> bool {
+    (10..=20).contains(&hour)
+}
+
+async fn sync_mascot(app: &AppHandle, api: &ApiClient) {
+    let Ok(mascot) = api.mascot_get().await else { return };
+    let pet_name = mascot.mascot.as_deref().unwrap_or("xibao");
+    let path = format!("DefaultPets/{pet_name}/pet-config.json");
+
+    // 优先读嵌入资产;失败回退 resource_dir
+    let bytes = app
+        .asset_resolver()
+        .get(path.clone())
+        .map(|a| a.bytes)
+        .or_else(|| std::fs::read(app.path().resource_dir().unwrap_or_default().join(&path)).ok());
+    let Some(bytes) = bytes else { return };
+
+    if let Ok(json) = String::from_utf8(bytes) {
+        if let Ok(cfg) = PetConfig::parse(&json) {
+            if let Ok(payload) = serde_json::to_string(&cfg) {
+                let _ = app.emit("pet://config", payload);
+            }
+        }
+    }
+}
+
+async fn poll_tasks(app: &AppHandle, api: &ApiClient) {
+    match api.tasks_today().await {
+        Ok(tasks) => {
+            let state = map_state(Some(&tasks), None, false, false);
+            let payload = PetStatePayload { state, reason: Some("tasks".into()), data: None };
+            let _ = app.emit("pet://state", payload);
+
+            // 选出最紧急的待办任务作为「点提醒即完成」的目标
+            let urgent = pick_urgent_task(&tasks);
+            let new_id = urgent.as_ref().map(|t| t.id);
+            let changed = match app.try_state::<AppState>() {
+                Some(s) => {
+                    let mut cur = s.pending_task.lock().unwrap();
+                    let changed = *cur != new_id;
+                    *cur = new_id;
+                    changed
+                }
+                None => false,
+            };
+            if changed {
+                if let Some(t) = &urgent {
+                    let title = t.title.clone().unwrap_or_else(|| "待办任务".into());
+                    let _ = app.emit(
+                        "pet://bubble",
+                        serde_json::json!({
+                            "text": format!("「{title}」快到点了,点我完成!"),
+                            "durationMs": 15000
+                        }),
+                    );
+                }
+            }
+        }
+        Err(_) => {
+            let _ = app.emit(
+                "pet://bubble",
+                serde_json::json!({"text": "连接后端失败,请检查服务", "durationMs": 5000}),
+            );
+        }
+    }
+}
+
+/// 从今日任务中选出最紧急、可一键完成的待办任务(未完成且已到期/今日到期)
+fn pick_urgent_task(tasks: &[CfcTask]) -> Option<CfcTask> {
+    let today = chrono::Local::now().date_naive();
+    tasks
+        .iter()
+        .filter(|t| matches!(t.status.as_deref(), Some("PENDING") | Some("IN_PROGRESS")))
+        .filter(|t| {
+            t.deadline
+                .as_ref()
+                .and_then(|d| d.to_local_date())
+                .map(|due| due <= today)
+                .unwrap_or(false)
+        })
+        .min_by_key(|t| t.deadline.as_ref().and_then(|d| d.to_local_date()))
+        .cloned()
+}
+
+async fn poll_health(app: &AppHandle, api: &ApiClient) {
+    match (api.energy_overview().await, api.water_list().await) {
+        (Ok(energy), Ok(water)) => {
+            let done = water_done_today(&water);
+            let state = map_state(None, Some(&energy), done, false);
+            let payload = PetStatePayload { state, reason: Some("health".into()), data: None };
+            let _ = app.emit("pet://state", payload);
+        }
+        _ => { /* 静默,与现版一致 */ }
+    }
+}
+
+async fn check_water(app: &AppHandle, api: &ApiClient) {
+    let hour = chrono::Local::now().hour();
+    if !in_active_hour(hour) {
+        return;
+    }
+    let water = match api.water_list().await {
+        Ok(w) => w,
+        Err(_) => return,
+    };
+    if !water_done_today(&water) {
+        let payload = PetStatePayload {
+            state: PetState::WaitingForInteraction,
+            reason: Some("drink_water".into()),
+            data: None,
+        };
+        let _ = app.emit("pet://state", payload);
+        let _ = app.emit(
+            "pet://bubble",
+            serde_json::json!({"text": "该喝水啦~点我确认完成!", "durationMs": 15000}),
+        );
+    }
+}
+
+/// 今天是否已有喝水记录(drunkAt 优先,回退 createdAt)
+fn water_done_today(records: &[WaterRecord]) -> bool {
+    let today = chrono::Local::now().date_naive();
+    records.iter().any(|r| {
+        r.drunk_at
+            .as_ref()
+            .or(r.created_at.as_ref())
+            .and_then(|d| d.to_local_date())
+            .map(|d| d == today)
+            .unwrap_or(false)
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::api::models::FlexDate;
+
+    #[test]
+    fn active_hour_window_is_10_to_20_inclusive() {
+        assert!(!in_active_hour(9));
+        assert!(in_active_hour(10));
+        assert!(in_active_hour(15));
+        assert!(in_active_hour(20));
+        assert!(!in_active_hour(21));
+    }
+
+    fn water(drunk_at: Option<&str>, created_at: Option<&str>) -> WaterRecord {
+        WaterRecord {
+            id: None,
+            member_id: None,
+            checkin_id: None,
+            amount_ml: None,
+            water_type: None,
+            drunk_at: drunk_at.map(|s| FlexDate::Iso(s.to_string())),
+            created_at: created_at.map(|s| FlexDate::Iso(s.to_string())),
+        }
+    }
+
+    #[test]
+    fn water_done_today_matches_today_only() {
+        let today = chrono::Local::now().format("%Y-%m-%d").to_string();
+        let today_dt = format!("{today}T08:00:00");
+        assert!(water_done_today(&[water(Some(&today_dt), None)]));
+        assert!(!water_done_today(&[water(Some("2000-01-01T08:00:00"), None)]));
+        assert!(!water_done_today(&[]));
+    }
+
+    #[test]
+    fn water_done_today_falls_back_to_created_at() {
+        let today = chrono::Local::now().format("%Y-%m-%d").to_string();
+        let today_dt = format!("{today}T09:30:00");
+        assert!(water_done_today(&[water(None, Some(&today_dt))]));
+    }
+
+    fn task(id: i64, status: &str, due: Option<&str>) -> CfcTask {
+        CfcTask {
+            id,
+            title: None,
+            status: Some(status.to_string()),
+            deadline: due.map(|s| FlexDate::Iso(s.to_string())),
+        }
+    }
+
+    #[test]
+    fn pick_urgent_prefers_earliest_due_pending() {
+        let today = chrono::Local::now().format("%Y-%m-%d").to_string();
+        let tasks = vec![
+            task(1, "COMPLETED", Some("2000-01-01")),
+            task(2, "PENDING", Some(&today)),
+            task(3, "PENDING", Some("2000-01-01")),
+        ];
+        assert_eq!(pick_urgent_task(&tasks).unwrap().id, 3);
+    }
+
+    #[test]
+    fn pick_urgent_none_when_not_yet_due() {
+        let tasks = vec![task(1, "PENDING", Some("2099-12-31"))];
+        assert!(pick_urgent_task(&tasks).is_none());
+    }
+
+    #[test]
+    fn pick_urgent_none_when_no_deadline() {
+        let tasks = vec![task(1, "PENDING", None)];
+        assert!(pick_urgent_task(&tasks).is_none());
+    }
+}

+ 108 - 0
cfc-desktop-pet/src-tauri/src/settings.rs

@@ -0,0 +1,108 @@
+use serde::{Deserialize, Serialize};
+use std::fs;
+use std::path::PathBuf;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AppSettings {
+    #[serde(default = "default_base_url")]
+    pub base_url: String,
+    /// 自定义形象图片 URL(通过 /api/media/upload 上传后本地记录)
+    #[serde(default)]
+    pub custom_mascot_url: Option<String>,
+}
+
+impl Default for AppSettings {
+    fn default() -> Self {
+        Self { base_url: default_base_url(), custom_mascot_url: None }
+    }
+}
+
+fn default_base_url() -> String {
+    "https://cfc.cf-club.com".to_string()
+}
+
+pub struct SettingsStore {
+    path: PathBuf,
+}
+
+impl SettingsStore {
+    pub fn new(app_config_dir: PathBuf) -> Self {
+        Self { path: app_config_dir.join("settings.json") }
+    }
+
+    pub fn load(&self) -> AppSettings {
+        match fs::read_to_string(&self.path) {
+            Ok(txt) => serde_json::from_str(&txt).unwrap_or_default(),
+            Err(_) => AppSettings::default(),
+        }
+    }
+
+    pub fn save(&self, s: &AppSettings) -> Result<(), String> {
+        if let Some(dir) = self.path.parent() {
+            fs::create_dir_all(dir).map_err(|e| e.to_string())?;
+        }
+        let txt = serde_json::to_string_pretty(s).map_err(|e| e.to_string())?;
+        fs::write(&self.path, txt).map_err(|e| e.to_string())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs;
+    use std::sync::atomic::{AtomicUsize, Ordering};
+
+    static COUNTER: AtomicUsize = AtomicUsize::new(0);
+
+    fn temp_dir() -> std::path::PathBuf {
+        let n = COUNTER.fetch_add(1, Ordering::SeqCst);
+        let dir = std::env::temp_dir().join(format!("xafpet-settings-test-{}", n));
+        let _ = fs::remove_dir_all(&dir);
+        dir
+    }
+
+    #[test]
+    fn default_when_file_missing() {
+        let store = SettingsStore::new(temp_dir());
+        let cfg = store.load();
+        assert_eq!(cfg.base_url, "https://cfc.cf-club.com");
+    }
+
+    #[test]
+    fn default_when_file_malformed() {
+        let dir = temp_dir();
+        fs::create_dir_all(&dir).unwrap();
+        fs::write(dir.join("settings.json"), "not json").unwrap();
+        let store = SettingsStore::new(dir);
+        let cfg = store.load();
+        assert_eq!(cfg.base_url, "https://cfc.cf-club.com");
+    }
+
+    #[test]
+    fn roundtrip_save_load() {
+        let dir = temp_dir();
+        let store = SettingsStore::new(dir.clone());
+        let mut cfg = store.load();
+        cfg.base_url = "http://192.168.1.5:9082".into();
+        store.save(&cfg).unwrap();
+
+        let store2 = SettingsStore::new(dir.clone());
+        assert_eq!(store2.load().base_url, "http://192.168.1.5:9082");
+        let _ = fs::remove_dir_all(&dir);
+    }
+
+    #[test]
+    fn custom_mascot_url_roundtrip() {
+        let dir = temp_dir();
+        let store = SettingsStore::new(dir.clone());
+        let mut cfg = store.load();
+        assert!(cfg.custom_mascot_url.is_none());
+        cfg.custom_mascot_url = Some("https://cdn.example/x.png".into());
+        store.save(&cfg).unwrap();
+
+        let loaded = SettingsStore::new(dir.clone()).load();
+        assert_eq!(loaded.custom_mascot_url.as_deref(), Some("https://cdn.example/x.png"));
+        let _ = fs::remove_dir_all(&dir);
+    }
+}

+ 202 - 0
cfc-desktop-pet/src-tauri/src/state.rs

@@ -0,0 +1,202 @@
+use serde::Serialize;
+
+use crate::api::models::{CfcTask, EnergyOverview};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum PetState {
+    Idle,
+    Happy,
+    Busy,
+    Worried,
+    Excited,
+    Remind,
+    Energetic,
+    Tired,
+    Curious,
+    Sad,
+    Sleeping,
+    Watching,
+    WaitingForInteraction,
+    MiniGame,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PetStatePayload {
+    pub state: PetState,
+    pub reason: Option<String>,
+    pub data: Option<serde_json::Value>,
+}
+
+/// 核心状态映射。优先级:Sleeping > Excited > Happy > Worried > Busy > Sad > Tired/Energetic/Curious > Idle
+pub fn map_state(
+    tasks: Option<&[CfcTask]>,
+    energy: Option<&EnergyOverview>,
+    water_done_today: bool,
+    user_inactive: bool,
+) -> PetState {
+    let has_pending = tasks
+        .map(|ts| {
+            ts.iter().any(|t| matches!(t.status.as_deref(), Some("PENDING") | Some("IN_PROGRESS")))
+        })
+        .unwrap_or(false);
+
+    if user_inactive && !has_pending {
+        return PetState::Sleeping;
+    }
+
+    if water_done_today {
+        return PetState::Excited;
+    }
+
+    if let Some(ts) = tasks {
+        if !ts.is_empty() {
+            if ts.iter().all(|t| t.status.as_deref() == Some("COMPLETED")) {
+                return PetState::Happy;
+            }
+            let today = chrono::Local::now().date_naive();
+            let overdue = ts.iter().any(|t| {
+                matches!(t.status.as_deref(), Some("PENDING") | Some("IN_PROGRESS"))
+                    && t.deadline
+                        .as_ref()
+                        .and_then(|d| d.to_local_date())
+                        .map(|due| due < today)
+                        .unwrap_or(false)
+            });
+            if overdue {
+                return PetState::Worried;
+            }
+            return PetState::Busy;
+        }
+    }
+
+    if let Some(e) = energy {
+        if !e.dimensions.is_empty() {
+            let all_low = e.dimensions.iter().all(|d| d.energy < 30);
+            if all_low {
+                return PetState::Sad;
+            }
+            let body = e.dimensions.iter().find(|d| d.code.as_deref() == Some("body"));
+            if let Some(b) = body {
+                if b.energy < 30 {
+                    return PetState::Tired;
+                }
+                if b.energy >= 85 {
+                    return PetState::Energetic;
+                }
+            }
+            let mind = e.dimensions.iter().find(|d| d.code.as_deref() == Some("wisdom"));
+            if let Some(m) = mind {
+                if m.energy >= 85 {
+                    return PetState::Curious;
+                }
+            }
+        }
+    }
+
+    PetState::Idle
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::api::models::{DimensionValue, FlexDate};
+
+    fn task(status: &str, due: Option<&str>) -> CfcTask {
+        CfcTask {
+            id: 1,
+            title: None,
+            status: Some(status.to_string()),
+            deadline: due.map(|s| FlexDate::Iso(s.to_string())),
+        }
+    }
+
+    fn energy(dims: &[(&str, i32)]) -> EnergyOverview {
+        EnergyOverview {
+            dimensions: dims
+                .iter()
+                .map(|(code, v)| DimensionValue {
+                    code: Some(code.to_string()),
+                    name: None,
+                    icon: None,
+                    element: None,
+                    energy: *v,
+                    health_index: 0,
+                })
+                .collect(),
+            total_energy: 0,
+            total_health_index: 0,
+        }
+    }
+
+    #[test]
+    fn sleeping_when_inactive_and_no_pending() {
+        assert_eq!(map_state(Some(&[]), None, false, true), PetState::Sleeping);
+    }
+
+    #[test]
+    fn not_sleeping_when_inactive_but_pending() {
+        let tasks = vec![task("PENDING", None)];
+        assert_eq!(map_state(Some(&tasks), None, false, true), PetState::Busy);
+    }
+
+    #[test]
+    fn excited_when_water_done_today() {
+        let tasks = vec![task("PENDING", None)];
+        assert_eq!(map_state(Some(&tasks), None, true, false), PetState::Excited);
+    }
+
+    #[test]
+    fn happy_when_all_completed() {
+        let tasks = vec![task("COMPLETED", None), task("COMPLETED", Some("2026-01-01"))];
+        assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Happy);
+    }
+
+    #[test]
+    fn worried_when_overdue() {
+        let tasks = vec![task("PENDING", Some("2000-01-01"))];
+        assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Worried);
+    }
+
+    #[test]
+    fn busy_when_in_progress_not_overdue() {
+        let tasks = vec![task("IN_PROGRESS", Some("2099-12-31"))];
+        assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Busy);
+    }
+
+    #[test]
+    fn sad_when_all_dimensions_low() {
+        let e = energy(&[("body", 10), ("wisdom", 20), ("mind", 15)]);
+        assert_eq!(map_state(None, Some(&e), false, false), PetState::Sad);
+    }
+
+    #[test]
+    fn tired_when_body_low() {
+        let e = energy(&[("body", 20), ("wisdom", 90)]);
+        assert_eq!(map_state(None, Some(&e), false, false), PetState::Tired);
+    }
+
+    #[test]
+    fn energetic_when_body_high() {
+        let e = energy(&[("body", 90)]);
+        assert_eq!(map_state(None, Some(&e), false, false), PetState::Energetic);
+    }
+
+    #[test]
+    fn curious_when_mind_high() {
+        let e = energy(&[("body", 60), ("wisdom", 88)]);
+        assert_eq!(map_state(None, Some(&e), false, false), PetState::Curious);
+    }
+
+    #[test]
+    fn idle_when_nothing_qualifies() {
+        assert_eq!(map_state(None, None, false, false), PetState::Idle);
+    }
+
+    #[test]
+    fn state_serializes_to_camel_case() {
+        let json = serde_json::to_string(&PetState::WaitingForInteraction).unwrap();
+        assert_eq!(json, "\"waitingForInteraction\"");
+    }
+}

+ 71 - 0
cfc-desktop-pet/src-tauri/src/token.rs

@@ -0,0 +1,71 @@
+use keyring::Entry;
+
+/// 令牌存取:Windows → Credential Manager,macOS → Keychain
+pub struct TokenStore {
+    service: String,
+    account: String,
+}
+
+impl TokenStore {
+    pub fn new() -> Self {
+        Self::with_names("xafpet", "jwt")
+    }
+
+    pub fn with_names(service: &str, account: &str) -> Self {
+        Self { service: service.into(), account: account.into() }
+    }
+
+    pub fn save(&self, token: &str) -> Result<(), String> {
+        Entry::new(&self.service, &self.account)
+            .map_err(|e| e.to_string())?
+            .set_password(token)
+            .map_err(|e| e.to_string())
+    }
+
+    pub fn load(&self) -> Result<Option<String>, String> {
+        let entry = Entry::new(&self.service, &self.account).map_err(|e| e.to_string())?;
+        match entry.get_password() {
+            Ok(p) => Ok(Some(p)),
+            Err(keyring::Error::NoEntry) => Ok(None),
+            Err(e) => Err(e.to_string()),
+        }
+    }
+
+    pub fn clear(&self) -> Result<(), String> {
+        let entry = Entry::new(&self.service, &self.account).map_err(|e| e.to_string())?;
+        match entry.delete_credential() {
+            Ok(()) => Ok(()),
+            Err(keyring::Error::NoEntry) => Ok(()),
+            Err(e) => Err(e.to_string()),
+        }
+    }
+}
+
+impl Default for TokenStore {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[cfg(not(target_os = "macos"))]
+    #[test]
+    fn roundtrip_save_load_clear() {
+        let store = TokenStore::with_names("xafpet-test", "jwt");
+        let _ = store.clear();
+
+        assert_eq!(store.load().unwrap(), None);
+
+        store.save("tok-123").unwrap();
+        assert_eq!(store.load().unwrap(), Some("tok-123".into()));
+
+        store.save("tok-456").unwrap();
+        assert_eq!(store.load().unwrap(), Some("tok-456".into()));
+
+        store.clear().unwrap();
+        assert_eq!(store.load().unwrap(), None);
+    }
+}

+ 65 - 0
cfc-desktop-pet/src-tauri/src/tray.rs

@@ -0,0 +1,65 @@
+use crate::app_state::AppState;
+use tauri::menu::{MenuBuilder, MenuItemBuilder};
+use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
+use tauri::{App, Manager};
+
+pub fn setup_tray(app: &App) -> tauri::Result<()> {
+    let settings_i = MenuItemBuilder::with_id("settings", "设置").build(app)?;
+    let toggle_i = MenuItemBuilder::with_id("toggle", "显示/隐藏").build(app)?;
+    let passthrough_i = MenuItemBuilder::with_id("passthrough", "鼠标穿透").build(app)?;
+    let quit_i = MenuItemBuilder::with_id("quit", "退出").build(app)?;
+    let menu = MenuBuilder::new(app)
+        .items(&[&settings_i, &toggle_i, &passthrough_i, &quit_i])
+        .build()?;
+
+    let _tray = TrayIconBuilder::new()
+        .icon(app.default_window_icon().expect("bundle icon").clone())
+        .menu(&menu)
+        .show_menu_on_left_click(false)
+        .on_menu_event(|app, event| match event.id().as_ref() {
+            "settings" => show_settings(app),
+            "toggle" => toggle_main(app),
+            "passthrough" => toggle_passthrough(app),
+            "quit" => app.exit(0),
+            _ => {}
+        })
+        .on_tray_icon_event(|tray, event| {
+            if let TrayIconEvent::Click {
+                button: MouseButton::Left,
+                button_state: MouseButtonState::Up,
+                ..
+            } = event
+            {
+                toggle_main(tray.app_handle());
+            }
+        })
+        .build(app)?;
+
+    Ok(())
+}
+
+pub fn show_settings(app: &tauri::AppHandle) {
+    if let Some(win) = app.get_webview_window("settings") {
+        let _ = win.show();
+        let _ = win.set_focus();
+    }
+}
+
+pub fn toggle_main(app: &tauri::AppHandle) {
+    if let Some(win) = app.get_webview_window("main") {
+        match win.is_visible() {
+            Ok(true) => { let _ = win.hide(); }
+            _ => { let _ = win.show(); }
+        }
+    }
+}
+
+pub fn toggle_passthrough(app: &tauri::AppHandle) {
+    if let Some(state) = app.try_state::<AppState>() {
+        let mut passthrough = state.passthrough.lock().unwrap();
+        *passthrough = !*passthrough;
+        if let Some(win) = app.get_webview_window("main") {
+            let _ = win.set_ignore_cursor_events(*passthrough);
+        }
+    }
+}

+ 50 - 0
cfc-desktop-pet/src-tauri/tauri.conf.json

@@ -0,0 +1,50 @@
+{
+  "$schema": "https://schema.tauri.app/config/2",
+  "productName": "XafPet",
+  "version": "0.1.0",
+  "identifier": "com.etotem.xafpet",
+  "build": {
+    "frontendDist": "../src"
+  },
+  "app": {
+    "withGlobalTauri": true,
+    "macOSPrivateApi": true,
+    "windows": [
+      {
+        "label": "main",
+        "title": "浠艾福桌面宠物",
+        "width": 200,
+        "height": 200,
+        "transparent": true,
+        "decorations": false,
+        "shadow": false,
+        "alwaysOnTop": true,
+        "skipTaskbar": false,
+        "resizable": false
+      },
+      {
+        "label": "settings",
+        "title": "设置",
+        "url": "settings.html",
+        "width": 360,
+        "height": 470,
+        "resizable": false,
+        "visible": false
+      }
+    ],
+    "security": {
+      "csp": null
+    }
+  },
+  "bundle": {
+    "active": true,
+    "targets": "all",
+    "icon": [
+      "icons/32x32.png",
+      "icons/128x128.png",
+      "icons/128x128@2x.png",
+      "icons/icon.icns",
+      "icons/icon.ico"
+    ]
+  }
+}

+ 27 - 0
cfc-desktop-pet/src/DefaultPets/fubao/pet-config.json

@@ -0,0 +1,27 @@
+{
+  "version": "1.0",
+  "type": "animal",
+  "species": "british_shorthair",
+  "gender": "male",
+  "image_base_url": "DefaultPets/fubao/",
+  "default_image": "standing.png",
+  "actions": {
+    "idle": "tail_wrap",
+    "happy": "belly_show",
+    "busy": "paw_press",
+    "waitingForInteraction": "tail_flick",
+    "sleeping": "breath_loop",
+    "celebrate": "celebrate",
+    "sad": "sad_animation"
+  },
+  "sounds": {
+    "click": "tap.mp3",
+    "complete": "bell.mp3",
+    "remind": "water_please.mp3"
+  },
+  "parameters": {
+    "move_distance": 12,
+    "animation_speed": 1.0,
+    "interaction_timeout": 300
+  }
+}

+ 27 - 0
cfc-desktop-pet/src/DefaultPets/xibao/pet-config.json

@@ -0,0 +1,27 @@
+{
+  "version": "1.0",
+  "type": "animal",
+  "species": "cockatiel",
+  "gender": "female",
+  "image_base_url": "DefaultPets/xibao/",
+  "default_image": "standing.png",
+  "actions": {
+    "idle": "wings_idle",
+    "happy": "wings_spread",
+    "busy": "head_tilt",
+    "waitingForInteraction": "hand_gesture",
+    "sleeping": "breath_loop",
+    "celebrate": "celebrate",
+    "sad": "sad_animation"
+  },
+  "sounds": {
+    "click": "pip.mp3",
+    "complete": "cheer.mp3",
+    "remind": "water_please.mp3"
+  },
+  "parameters": {
+    "move_distance": 15,
+    "animation_speed": 1.0,
+    "interaction_timeout": 300
+  }
+}

+ 46 - 0
cfc-desktop-pet/src/animations.css

@@ -0,0 +1,46 @@
+/* ===== 浠艾福桌面宠物动画定义 ===== */
+@keyframes wings_idle { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(-3px) } }
+@keyframes wings_spread { 0% { transform: scale(1) } 30% { transform: scale(1.15, 0.95) } 70% { transform: scale(0.95, 1.1) } 100% { transform: scale(1) } }
+@keyframes head_tilt { 0%, 100% { transform: rotate(0deg) } 25% { transform: rotate(-8deg) } 75% { transform: rotate(6deg) } }
+@keyframes hand_gesture { 0%, 100% { transform: translateY(0) } 20% { transform: translateY(-8px) } 40% { transform: translateY(0) } 60% { transform: translateY(-5px) } 80% { transform: translateY(0) } }
+@keyframes breath_loop { 0%, 100% { transform: scale(1, 1) } 50% { transform: scale(1.04, 0.96) } }
+@keyframes celebrate { 0%, 100% { transform: translateY(0) rotate(0) } 25% { transform: translateY(-16px) rotate(-4deg) } 50% { transform: translateY(0) rotate(0) } 75% { transform: translateY(-16px) rotate(4deg) } }
+@keyframes sad_animation { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(2px) } }
+@keyframes tail_wrap { 0%, 100% { transform: rotate(0deg) } 50% { transform: rotate(3deg) } }
+@keyframes belly_show { 0%, 100% { transform: rotate(0deg) scale(1) } 50% { transform: rotate(-4deg) scale(1.05) } }
+@keyframes paw_press { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(3px) } }
+@keyframes tail_flick { 0%, 100% { transform: rotate(0deg) } 30% { transform: rotate(10deg) } 60% { transform: rotate(-10deg) } }
+@keyframes blink_eye { 0%, 90%, 100% { transform: scaleY(1) } 95% { transform: scaleY(0.1) } }
+@keyframes look_around { 0%, 100% { transform: rotate(0deg) } 25% { transform: rotate(-12deg) } 75% { transform: rotate(12deg) } }
+@keyframes wave { 0%, 100% { transform: rotate(0deg) } 25% { transform: rotate(-15deg) } 50% { transform: rotate(10deg) } 75% { transform: rotate(-15deg) } }
+@keyframes stretch { 0%, 100% { transform: scaleY(1) } 50% { transform: scaleY(1.15) } }
+@keyframes clap { 0%, 100% { transform: scale(1) } 40% { transform: scale(1.1, 0.9) } 70% { transform: scale(0.95, 1.05) } }
+@keyframes bow { 0%, 100% { transform: rotate(0deg) } 50% { transform: rotate(10deg) } }
+@keyframes whistle { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(-4px) } }
+@keyframes ear_scrape { 0%, 100% { transform: rotate(0deg) } 50% { transform: rotate(6deg) } }
+@keyframes pet_touch { 0% { transform: scale(1) } 50% { transform: scale(1.08) } 100% { transform: scale(1) } }
+@keyframes puzzle_active { 0%, 100% { transform: rotate(0deg) } 50% { transform: rotate(180deg) } }
+@keyframes worried_walk { 0%, 100% { transform: translateX(0) } 25% { transform: translateX(6px) } 75% { transform: translateX(-6px) } }
+
+.anim-wings_idle { animation: wings_idle 2s ease-in-out infinite; }
+.anim-wings_spread { animation: wings_spread 1.2s ease-in-out 1; }
+.anim-head_tilt { animation: head_tilt 1.6s ease-in-out infinite; }
+.anim-hand_gesture { animation: hand_gesture 1.8s ease-in-out infinite; }
+.anim-breath_loop { animation: breath_loop 2.4s ease-in-out infinite; }
+.anim-celebrate { animation: celebrate 0.8s ease-in-out 3; }
+.anim-sad_animation { animation: sad_animation 3s ease-in-out infinite; }
+.anim-tail_wrap { animation: tail_wrap 2s ease-in-out infinite; }
+.anim-belly_show { animation: belly_show 1.4s ease-in-out 2; }
+.anim-paw_press { animation: paw_press 1s ease-in-out 2; }
+.anim-tail_flick { animation: tail_flick 1.5s ease-in-out 2; }
+.anim-blink_eye { animation: blink_eye 3.5s ease-in-out infinite; }
+.anim-look_around { animation: look_around 2s ease-in-out 1; }
+.anim-wave { animation: wave 1.2s ease-in-out 2; }
+.anim-stretch { animation: stretch 1.8s ease-in-out 1; }
+.anim-clap { animation: clap 0.6s ease-in-out 3; }
+.anim-bow { animation: bow 1.2s ease-in-out 2; }
+.anim-whistle { animation: whistle 0.9s ease-in-out 3; }
+.anim-ear_scrape { animation: ear_scrape 1s ease-in-out 2; }
+.anim-pet_touch { animation: pet_touch 0.6s ease-in-out 1; }
+.anim-puzzle_active { animation: puzzle_active 1s ease-in-out infinite; }
+.anim-worried_walk { animation: worried_walk 1.2s ease-in-out infinite; }

+ 46 - 0
cfc-desktop-pet/src/bridge.js

@@ -0,0 +1,46 @@
+(function () {
+  'use strict';
+
+  var invoke = window.__TAURI__.core.invoke;
+  var listen = window.__TAURI__.event.listen;
+
+  // ── Rust → JS ──────────────────────────────
+  listen('pet://state', function (e) {
+    if (window.cfcPetAPI) window.cfcPetAPI.notifyState(e.payload);
+  });
+  listen('pet://bubble', function (e) {
+    if (window.cfcPetAPI) window.cfcPetAPI.showBubble(e.payload.text, e.payload.durationMs);
+  });
+  listen('pet://config', function (e) {
+    if (window.cfcPetAPI) window.cfcPetAPI.loadConfig(e.payload);
+  });
+  listen('pet://custom-image', function (e) {
+    if (window.cfcPetAPI) window.cfcPetAPI.setCustomImage(e.payload && e.payload.url);
+  });
+
+  // 启动时应用已保存的自定义形象
+  try {
+    invoke('get_settings').then(function (s) {
+      if (s && s.customMascotUrl && window.cfcPetAPI) {
+        window.cfcPetAPI.setCustomImage(s.customMascotUrl);
+      }
+    }).catch(function () {});
+  } catch (_) { /* noop */ }
+
+  // ── JS → Rust(替换 chrome.webview.hostObjects.sync.cfcHost)──
+  window.cfcHost = {
+    onInteract: function (action, dataJson) {
+      try {
+        invoke('on_interact', { action: action, data: dataJson });
+      } catch (err) {
+        console.log('[interaction]', action, dataJson);
+      }
+    },
+    onDragStart: function () {
+      try { invoke('drag_start'); } catch (e) { /* noop */ }
+    },
+    onDragEnd: function () {
+      try { invoke('drag_end'); } catch (e) { /* noop */ }
+    }
+  };
+})();

+ 69 - 0
cfc-desktop-pet/src/index.html

@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8" />
+<title>Xaf Pet</title>
+<link rel="stylesheet" href="animations.css" />
+<style>
+  html, body {
+    width: 100%; height: 100%; margin: 0; overflow: hidden;
+    background: transparent !important;
+    -webkit-app-region: no-drag;
+  }
+  #pet-stage {
+    position: fixed; inset: 0; display: flex; align-items: flex-end;
+    justify-content: center; pointer-events: none;
+    background: transparent !important;
+  }
+  #pet {
+    width: 120px; height: 140px; pointer-events: auto;
+    cursor: grab; user-select: none;
+    -webkit-app-region: no-drag;
+  }
+  #pet img { width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
+  #bubble {
+    position: absolute; bottom: 150px; left: 50%; transform: translateX(-50%);
+    background: rgba(255,255,255,0.95); border: 2px solid #4A9BD7;
+    border-radius: 12px; padding: 6px 12px;
+    font: 13px/1.4 'PingFang SC', sans-serif; color: #333;
+    white-space: nowrap; display: none; box-shadow: 0 2px 8px rgba(0,0,0,.12);
+    pointer-events: none;
+  }
+  #bubble::after {
+    content: ''; position: absolute; top: 100%; left: 50%;
+    transform: translateX(-50%);
+    border: 6px solid transparent; border-top-color: #4A9BD7;
+  }
+  /* 右键菜单 */
+  #context-menu {
+    position: absolute; display: none;
+    background: #fff; border: 1px solid #ccc; border-radius: 6px;
+    box-shadow: 0 2px 12px rgba(0,0,0,.15); padding: 4px 0;
+    min-width: 120px; z-index: 9999; pointer-events: auto;
+  }
+  #context-menu .item {
+    padding: 6px 16px; font-size: 13px; cursor: pointer; color: #333;
+  }
+  #context-menu .item:hover { background: #f0f6ff; }
+  #context-menu .divider { height: 1px; background: #e0e0e0; margin: 4px 0; }
+</style>
+</head>
+<body>
+<div id="pet-stage">
+  <div id="pet">
+    <img id="pet-img" src="" alt="pet" draggable="false" />
+    <div id="bubble"></div>
+  </div>
+</div>
+<!-- 右键菜单 -->
+<div id="context-menu">
+  <div class="item" data-action="settings">设置</div>
+  <div class="item" data-action="passthrough">鼠标穿透</div>
+  <div class="divider"></div>
+  <div class="item" data-action="quit">退出</div>
+</div>
+<script src="pet-engine.js"></script>
+<script src="bridge.js"></script>
+<script src="interactions.js"></script>
+</body>
+</html>

+ 80 - 0
cfc-desktop-pet/src/interactions.js

@@ -0,0 +1,80 @@
+(function () {
+  'use strict';
+
+  const petEl = document.getElementById('pet');
+  const ctxMenu = document.getElementById('context-menu');
+  const invoke = window.__TAURI__?.core?.invoke;
+
+  let dragStartX = 0, dragStartY = 0;
+  let pressTimer = null;
+  let isDragging = false;
+
+  // ── 右键菜单 ──────────────────────────────
+  petEl.addEventListener('contextmenu', function (e) {
+    e.preventDefault();
+    const x = Math.min(e.clientX, window.innerWidth - 130);
+    const y = Math.min(e.clientY, window.innerHeight - 100);
+    ctxMenu.style.left = x + 'px';
+    ctxMenu.style.top = y + 'px';
+    ctxMenu.style.display = 'block';
+  });
+
+  document.addEventListener('click', function (e) {
+    if (!ctxMenu.contains(e.target)) ctxMenu.style.display = 'none';
+  });
+  document.addEventListener('keydown', function (e) {
+    if (e.key === 'Escape') ctxMenu.style.display = 'none';
+  });
+
+ctxMenu.addEventListener('click', function (e) {
+    const item = e.target.closest('.item');
+    if (!item) return;
+    const action = item.dataset.action;
+    ctxMenu.style.display = 'none';
+    if (action === 'settings') invoke?.('open_settings');
+    else if (action === 'quit') { try { invoke?.('quit_app'); } catch (_) { window.close(); } }
+});
+
+  // ── 点击 / 长按 / 拖拽 ───────────────────
+  petEl.addEventListener('mousedown', function (e) {
+    dragStartX = e.clientX;
+    dragStartY = e.clientY;
+    isDragging = false;
+    pressTimer = setTimeout(() => {
+      notifyInteract('pet', {});
+    }, 1500);
+  });
+
+  petEl.addEventListener('mousemove', function (e) {
+    const dx = e.clientX - dragStartX;
+    const dy = e.clientY - dragStartY;
+    if (Math.abs(dx) > 4 || Math.abs(dy) > 4) {
+      isDragging = true;
+      clearTimeout(pressTimer);
+      notifyDragStart();
+    }
+  });
+
+  petEl.addEventListener('mouseup', function (e) {
+    clearTimeout(pressTimer);
+    if (!isDragging) {
+      notifyInteract('click', {});
+    } else {
+      notifyDragEnd();
+    }
+  });
+
+  function notifyInteract(action, data) {
+    try {
+      window.cfcHost.onInteract(action, JSON.stringify(data));
+    } catch (err) { console.log('[interaction]', action, data); }
+  }
+
+  function notifyDragStart() {
+    try { window.cfcHost.onDragStart(); } catch (e) { /* noop */ }
+  }
+
+  function notifyDragEnd() {
+    try { window.cfcHost.onDragEnd(); } catch (e) { /* noop */ }
+  }
+})();

+ 120 - 0
cfc-desktop-pet/src/pet-engine.js

@@ -0,0 +1,120 @@
+(function () {
+  'use strict';
+
+  const petImg = document.getElementById('pet-img');
+  const petEl = document.getElementById('pet');
+  const bubble = document.getElementById('bubble');
+
+  let currentState = 'idle';
+  let currentAnimation = null;
+  let config = null;
+  let lastConfig = null;
+  let customImage = null;
+  let bubbleTimer = null;
+
+  const DEFAULT_IMAGE = 'data:image/svg+xml;utf8,' +
+    encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="120" height="140">' +
+      '<text x="60" y="100" font-size="90" text-anchor="middle">🦜</text></svg>');
+
+  const BUBBLE_TEXTS = {
+    happy: '完成了!继续保持~',
+    busy: '有任务在等你呢~',
+    worried: '任务要过期啦,快去看看!',
+    excited: '打卡成功,太棒了!',
+    remind: '该喝水啦~',
+    tired: '有点累了呢…',
+    sad: '要照顾好自己哦…',
+    waitingForInteraction: '来和我互动一下吧!',
+    miniGame: '我们一起玩吧!',
+    sleeping: 'zZ…',
+    custom: '自定义状态'
+  };
+
+  window.cfcPetAPI = {
+    notifyState: function (stateJson) {
+      const payload = typeof stateJson === 'string' ? JSON.parse(stateJson) : stateJson;
+      setState(payload.state, payload.reason || null, payload.data || null);
+    },
+    setCustomState: function (stateName, dataJson) {
+      const data = typeof dataJson === 'string' ? JSON.parse(dataJson) : dataJson;
+      setState(stateName, 'custom', data);
+    },
+    loadConfig: function (configJson) {
+      config = typeof configJson === 'string' ? JSON.parse(configJson) : configJson;
+      applyImage(config);
+    },
+    setCustomImage: function (url) {
+      customImage = url || null;
+      if (customImage) {
+        petImg.src = customImage;
+        petImg.onerror = function () { petImg.src = DEFAULT_IMAGE; };
+      } else if (lastConfig) {
+        applyImage(lastConfig);
+      } else {
+        petImg.src = DEFAULT_IMAGE;
+      }
+    },
+    showBubble: function (text, durationMs) {
+      bubble.textContent = text;
+      bubble.style.display = 'block';
+      clearTimeout(bubbleTimer);
+      if (durationMs) bubbleTimer = setTimeout(hideBubble, durationMs);
+    },
+    hideBubble: hideBubble,
+    getState: function () { return currentState; }
+  };
+
+  function hideBubble() { bubble.style.display = 'none'; }
+
+  function applyImage(cfg) {
+    if (!cfg) return;
+    lastConfig = cfg;
+    const src = customImage || (cfg.default_image ? cfg.image_base_url + cfg.default_image : null);
+    if (!src) return;
+    petImg.src = src;
+    petImg.onerror = function () { petImg.src = DEFAULT_IMAGE; };
+  }
+
+  function setState(stateName, reason, data) {
+    currentState = stateName;
+    if (currentAnimation) { petEl.classList.remove(currentAnimation); currentAnimation = null; }
+
+    let animClass = null;
+    if (config && config.actions && config.actions[stateName]) {
+      animClass = 'anim-' + config.actions[stateName];
+    } else {
+      animClass = 'anim-' + stateName;
+    }
+
+    const styleEl = document.getElementById('pet-animations');
+    const sheet = styleEl ? styleEl.sheet : null;
+    const hasClass = sheet && Array.from(sheet.cssRules).some(r => r.selectorText === '.' + animClass);
+    if (hasClass) { petEl.classList.add(animClass); currentAnimation = animClass; }
+
+    if (reason === 'drink_water') {
+      bubble.textContent = '该喝水啦~';
+      bubble.style.display = 'block';
+      clearTimeout(bubbleTimer);
+      bubbleTimer = setTimeout(hideBubble, 8000);
+    } else if (reason === 'welcome') {
+      bubble.textContent = '我来了!';
+      bubble.style.display = 'block';
+      clearTimeout(bubbleTimer);
+      bubbleTimer = setTimeout(hideBubble, 3000);
+    } else if (BUBBLE_TEXTS[stateName]) {
+      bubble.textContent = BUBBLE_TEXTS[stateName];
+      bubble.style.display = 'block';
+      clearTimeout(bubbleTimer);
+      bubbleTimer = setTimeout(hideBubble, 5000);
+    }
+  }
+
+  petImg.src = DEFAULT_IMAGE;
+
+  setInterval(function () {
+    if (document.getElementById('pet-img')) {
+      petImg.classList.add('anim-blink_eye');
+      setTimeout(() => petImg.classList.remove('anim-blink_eye'), 200);
+    }
+  }, 3500);
+})();

+ 162 - 0
cfc-desktop-pet/src/settings.html

@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8" />
+<title>设置</title>
+<style>
+  body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; margin: 16px; }
+  label { display: block; margin-bottom: 4px; font-size: 13px; color: #555; }
+  input { width: 100%; box-sizing: border-box; padding: 6px 8px; margin-bottom: 12px;
+          border: 1px solid #ccc; border-radius: 6px; font-size: 13px; }
+  button { width: 100%; padding: 8px; border: none; border-radius: 6px;
+           background: #4A9BD7; color: #fff; font-size: 14px; cursor: pointer; }
+  button.secondary { background: #9aa4ad; margin-top: 8px; }
+  button:disabled { opacity: .6; cursor: default; }
+  .hint { font-size: 12px; color: #999; margin: -8px 0 8px; }
+  #mascot-preview { width: 72px; height: 72px; object-fit: contain;
+                    border: 1px dashed #ccc; border-radius: 8px; display: none; margin-bottom: 8px; }
+  hr { border: none; border-top: 1px solid #eee; margin: 14px 0; }
+  #status { margin-top: 10px; font-size: 12px; color: #999; }
+</style>
+</head>
+<body>
+  <label for="base-url">后端地址</label>
+  <input id="base-url" placeholder="https://cfc.cf-club.com" />
+  <label for="token">令牌(留空则不修改)</label>
+  <input id="token" type="password" placeholder="JWT Token" />
+  <button id="save">保存</button>
+
+  <hr />
+
+  <label>宠物形象</label>
+  <img id="mascot-preview" alt="preview" />
+  <input id="mascot-file" type="file" accept="image/png,image/jpeg,image/webp" />
+  <div class="hint">支持 PNG / JPG / WebP,建议正方形、≤2MB</div>
+  <button id="upload-mascot" disabled>上传并使用此形象</button>
+  <button id="clear-mascot" class="secondary">恢复默认形象</button>
+
+  <hr />
+
+  <label>微信登录</label>
+  <button id="wechat-login">微信扫码登录</button>
+  <div class="hint">用微信扫码,自动与小程序账号同步登录(无需手动填令牌)</div>
+
+  <div id="status"></div>
+  <script>
+    (function () {
+      'use strict';
+      var invoke = window.__TAURI__.core.invoke;
+      var baseUrlEl = document.getElementById('base-url');
+      var tokenEl = document.getElementById('token');
+      var statusEl = document.getElementById('status');
+      var fileEl = document.getElementById('mascot-file');
+      var previewEl = document.getElementById('mascot-preview');
+      var uploadBtn = document.getElementById('upload-mascot');
+      var clearBtn = document.getElementById('clear-mascot');
+
+      function setStatus(msg) { statusEl.textContent = msg || ''; }
+
+      invoke('get_settings').then(function (s) {
+        baseUrlEl.value = s.baseUrl || 'https://cfc.cf-club.com';
+        if (s.customMascotUrl) {
+          previewEl.src = s.customMascotUrl;
+          previewEl.style.display = 'block';
+        }
+        setStatus(s.hasToken ? '已保存令牌' : '未设置令牌');
+      }).catch(function () {});
+
+      document.getElementById('save').addEventListener('click', function () {
+        var baseUrl = baseUrlEl.value.trim();
+        var token = tokenEl.value.trim();
+        invoke('save_settings', { baseUrl: baseUrl, token: token ? token : null })
+          .then(function () {
+            setStatus('已保存,轮询已重启');
+            setTimeout(function () { setStatus(''); }, 2500);
+          })
+          .catch(function (e) { setStatus('保存失败:' + e); });
+      });
+
+      fileEl.addEventListener('change', function () {
+        var file = fileEl.files && fileEl.files[0];
+        uploadBtn.disabled = !file;
+        if (!file) { previewEl.style.display = 'none'; return; }
+        var reader = new FileReader();
+        reader.onload = function () {
+          previewEl.src = reader.result;
+          previewEl.style.display = 'block';
+        };
+        reader.readAsDataURL(file);
+      });
+
+      uploadBtn.addEventListener('click', function () {
+        var file = fileEl.files && fileEl.files[0];
+        if (!file) return;
+        if (file.size > 10 * 1024 * 1024) { setStatus('图片过大(上限 10MB)'); return; }
+        uploadBtn.disabled = true;
+        setStatus('上传中…');
+        var reader = new FileReader();
+        reader.onload = function () {
+          var result = String(reader.result);
+          var comma = result.indexOf(',');
+          var meta = result.substring(0, comma);
+          var base64 = result.substring(comma + 1);
+          var mime = meta.indexOf(':') >= 0 && meta.indexOf(';') > 0
+            ? meta.substring(meta.indexOf(':') + 1, meta.indexOf(';')) : '';
+          invoke('upload_mascot', {
+            filename: file.name,
+            mime: mime,
+            dataBase64: base64
+          }).then(function () {
+            setStatus('已上传并使用该形象');
+            uploadBtn.disabled = false;
+            setTimeout(function () { setStatus(''); }, 3000);
+          }).catch(function (e) {
+            setStatus('上传失败:' + e);
+            uploadBtn.disabled = false;
+          });
+        };
+        reader.onerror = function () { setStatus('读取图片失败'); uploadBtn.disabled = false; };
+        reader.readAsDataURL(file);
+      });
+
+      clearBtn.addEventListener('click', function () {
+        invoke('clear_custom_mascot').then(function () {
+          fileEl.value = '';
+          previewEl.style.display = 'none';
+          uploadBtn.disabled = true;
+          setStatus('已恢复默认形象');
+          setTimeout(function () { setStatus(''); }, 2500);
+        }).catch(function (e) { setStatus('操作失败:' + e); });
+      });
+
+      document.getElementById('wechat-login').addEventListener('click', function () {
+        setStatus('正在打开微信登录…');
+        invoke('start_wechat_login').then(function () {
+          setStatus('请用微信扫码,完成后自动登录');
+        }).catch(function (e) { setStatus('发起登录失败:' + e); });
+      });
+
+      // 后台轮询任务完成后由 Rust 侧 emit
+      try {
+        window.__TAURI__.event.listen('pet://wechat-login', function (ev) {
+          var p = (ev && ev.payload) || {};
+          if (p.status === 'success') {
+            setStatus('登录成功' + (p.nickname ? ':' + p.nickname : ''));
+            tokenEl.value = '';
+          } else if (p.status === 'pending') {
+            setStatus('等待扫码…');
+          } else if (p.status === 'timeout') {
+            setStatus('登录超时,请重试');
+          } else if (p.status === 'notfound') {
+            setStatus('该微信未绑定小程序账号');
+          } else if (p.status === 'expired') {
+            setStatus('二维码已过期,请重试');
+          } else {
+            setStatus('登录失败:' + (p.message || p.status));
+          }
+        });
+      } catch (e) { /* event API 不可用时忽略 */ }
+    })();
+  </script>
+</body>
+</html>

+ 0 - 0
cfc-frontend/pages/family/components/GenerationPicker.vue → cfc-frontend/components/GenerationPicker.vue


+ 8 - 1
cfc-frontend/pages.json

@@ -486,6 +486,13 @@
             "navigationBarTitleText": "DAN 测评报告",
             "navigationStyle": "custom"
           }
+        },
+        {
+          "path": "comparison",
+          "style": {
+            "navigationBarTitleText": "DAN 报告对比",
+            "navigationStyle": "custom"
+          }
         }
       ]
     },
@@ -1824,7 +1831,7 @@
         {
           "path": "message-history",
           "style": {
-            "navigationBarTitleText": "消息历史"
+            "navigationBarTitleText": ""
           }
         }
       ]

+ 198 - 0
cfc-frontend/pages/dan-assessment/comparison.vue

@@ -0,0 +1,198 @@
+<template>
+  <view class="page">
+    <view class="filter-bar">
+      <view class="filter-item">{{ currentMemberName }} ▾</view>
+      <picker :range="reportIds" @change="onReportIdChange">
+        <view class="filter-item">{{ currentReportIdLabel }}</view>
+      </picker>
+      <picker :range="[ 'uploadIdA', 'uploadIdB' ]" @change="onUploadIdChange">
+        <view class="filter-item">{{ currentUploadLabel }}</view>
+      </picker>
+    </view>
+
+    <view v-if="loading" class="loading-wrap">
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <view v-if="error" class="error-wrap">
+      <text class="error-text">{{ error }}</text>
+    </view>
+
+    <view v-if="comparison" class="comparison-wrap">
+      <view class="section-title">DAN 报告对比</view>
+
+      <view class="comparison-info">
+        <view class="info-item">
+          <text class="label">首次上传</text>
+          <text class="value">{{ firstUploadLabel }}</text>
+        </view>
+        <view class="info-item">
+          <text class="label">最近上传</text>
+          <text class="value">{{ secondUploadLabel }}</text>
+        </view>
+      </view>
+
+      <view v-if="deltas && deltas.length > 0" class="deltas-list">
+        <view v-for="(delta, di) in deltas" :key="di" class="delta-item">
+          <view :class="delta.status === 'added' ? 'delta-added' : delta.status === 'removed' ? 'delta-removed' : 'delta-modified'">
+            <text class="delta-code">{{ delta.code || '—' }}</text>
+            <view class="delta-detail">
+              <text v-if="delta.status === 'added'">新增</text>
+              <text v-if="delta.status === 'removed'>删除</text>
+              <text v-if="delta.status === 'modified'">修改</text>
+              <text class="delta-name">{{ delta.name1 || '' }} → {{ delta.name2 || '' }}</text>
+              <text v-if="delta.change">({{ delta.change }})</text>
+              <text v-if="delta.valueDelta !== null">Δ:{{ delta.valueDelta }}</text>
+              <text v-if="delta.categoryChanged">类别变化</text>
+            </view>
+          </view>
+        </view>
+        <view v-else class="empty-tip">两次上传无变化差异</view>
+      </view>
+
+      <view v-else class="empty-tip">暂无对比数据</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getFamilyMemberList } from '@/utils/api'
+import request from '@/utils/request'
+
+export default {
+  components: {},
+  data() {
+    return {
+      memberId: null,
+      reportIds: [],
+      currentReportId: null,
+      currentMemberName: '',
+      currentReportIdLabel: '选择报告',
+      currentUploadLabel: '选择上传',
+      uploadIdA: null,
+      uploadIdB: null,
+      loading: true,
+      error: null,
+      comparison: null
+    }
+  },
+  computed: {
+    memberNames() {
+      return this.memberId ? this.members.map(m => m.name || m.nickname || '').filter(Boolean) : []
+    }
+  },
+  onLoad(options) {
+    var self = this
+    // 从 query 获取 memberId,或默认取当前登录成员
+    if (options.memberId) {
+      self.memberId = parseInt(options.memberId)
+    }
+    self.loadMembers()
+    self.loadReportIds()
+  },
+  methods: {
+    loadMembers() {
+      var self = this
+      getFamilyMemberList({ visibleOnly: true }).then(function(res) {
+        var arr = (res.data && res.data.list) || (res.data instanceof Array ? res.data : [])
+        self.members = arr
+        if (!self.memberId && arr.length > 0) {
+          self.memberId = arr[0].id
+        }
+        self.loadReportIds()
+      })
+    },
+    loadReportIds() {
+      var self = this
+      if (!self.memberId) {
+        self.reportIds = []
+        self.currentReportIdLabel = '选择成员后加载'
+        return
+      }
+      request('/api/dan-report/list', 'POST', { memberId: self.memberId }).then(function(res) {
+        self.reportIds = (res.data || []).map(function(u) { return String(u.id) })
+        if (self.reportIds.length > 0) {
+          self.currentReportId = self.reportIds[0]
+          self.onReportIdChange(self.reportIds[0])
+        }
+      }).catch(function(e) {
+        self.error = '获取报告列表失败'
+        console.error(e)
+      })
+    },
+    onReportIdChange(value) {
+      this.currentReportId = value
+      this.loadComparison()
+    },
+    onUploadIdChange(value, uploadType) {
+      if (uploadType === 'A') {
+        this.uploadIdA = value
+      } else {
+        this.uploadIdB = value
+      }
+      this.loadComparison()
+    },
+    loadComparison() {
+      var self = this
+      if (!self.uploadIdA || !self.uploadIdB) {
+        self.comparison = null
+        return
+      }
+      self.loading = true
+      request('/api/dan-report/compare', 'POST', { uploadIdA: self.uploadIdA, uploadIdB: self.uploadIdB }).then(function(res) {
+        self.loading = false
+        if (res.code !== 200) {
+          self.error = res.message || '对比失败'
+          return
+        }
+        self.comparison = res.data
+        self.error = null
+      }).catch(function(e) {
+        self.loading = false
+        self.error = '对比异常'
+        console.error(e)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  background: #F5F9FC;
+  padding: 24rpx;
+  box-sizing: border-box;
+}
+.filter-bar {
+  display: flex;
+  flex-direction: row;
+  margin-bottom: 24rpx;
+}
+.filter-item {
+  flex: 1;
+  background: #FFFFFF;
+  border: 1rpx solid #E2E8F0;
+  border-radius: 16rpx;
+  padding: 16rpx 24rpx;
+  margin-right: 16rpx;
+  font-size: 26rpx;
+  color: #1E293B;
+  text-align: center;
+}
+.filter-item:last-child { margin-right: 0; }
+
+/* 对比区 */
+.comparison-wrap { background: #FFFFFF; border-radius: 20rpx; padding: 24rpx; margin-bottom: 24rpx; box-shadow: 0 4rpx 16rpx rgba(30,41,59,0.06); }
+.section-title { font-size: 30rpx; font-weight: 600; color: #1E293B; margin-bottom: 20rpx; }
+.info-item { display: flex; align-items: center; margin-bottom: 16rpx; }
+.label { font-size: 24rpx; color: #64748B; margin-right: 12rpx; }
+.value { font-size: 24rpx; color: #1E293B; }
+.deltas-list { margin-top: 24rpx; }
+.delta-item { padding: 12rpx 0; border-bottom: 1rpx solid #E2E8F0; display: flex; align-items: center; }
+.delta-code { flex: 1; font-size: 24rpx; color: #64748B; }
+.delta-added { color: #32CD32; }
+.delta-removed { color: #FF6B6B; }
+.delta-modified { color: #FFA500; }
+.empty-tip { padding: 60rpx 0; text-align: center; font-size: 26rpx; color: #94A3B8; }
+</style>

+ 2 - 2
cfc-frontend/pages/family/add-member.vue

@@ -108,7 +108,7 @@
 
 <script>
 import { addFamilyMember, updateFamilyMember, recordConsent, checkPhone } from '../../utils/api.js'
-import GenerationPicker from '@/pages/family/components/GenerationPicker.vue'
+import GenerationPicker from '@/components/GenerationPicker.vue'
 import { parseDate } from '../../utils/format.js'
 
 export default {
@@ -392,7 +392,7 @@ this.submitting = false
 }
 .modal-link {
   font-size: 24rpx;
-  color: #667eea;
+  color: #F97316;
   display: block;
   text-align: center;
   margin-bottom: 24rpx;

+ 1 - 1
cfc-frontend/components/TrendChart.vue → cfc-frontend/pages/health/components/TrendChart.vue

@@ -189,7 +189,7 @@ export default {
         ctx.fillText(self._fmtNum(tv), padL - 4, ty + 3)
       }
       // 折线 + 数据点 + 数值
-      var palette = ['#F97316', '#0EA5E9', '#10B981', '#8B5CF6', '#EF4444', '#F59E0B']
+      var palette = ['#F97316', '#0EA5E9', '#10B981', '#F97316', '#EF4444', '#F59E0B']
       seriesList.forEach(function(s, si) {
         var color = s.color || palette[si % palette.length]
         ctx.strokeStyle = color

+ 47 - 10
cfc-frontend/pages/health/indicator-curve.vue

@@ -62,15 +62,17 @@
             <text :class="'rank-delta ' + item._cssClass">{{ item._deltaText }}</text>
           </view>
           <text :class="'rank-trend ' + item._cssClass">{{ item._trendLabel }} {{ item._arrow }}</text>
+          <text class="rank-gap" v-if="item.skipGaps > 0">中间 {{ item.skipGaps }} 期缺失</text>
         </view>
       </view>
+      <view class="rank-empty" v-else>暂无变化数据</view>
     </view>
   </view>
 </template>
 
 <script>
-import TrendChart from '@/components/TrendChart.vue'
-import { getIndicatorTrend, getIndicatorChangeRanking } from '@/utils/api'
+import TrendChart from '@/pages/health/components/TrendChart.vue'
+import { getIndicatorTrend, getIndicatorChangeRanking, getIndicatorSourceTypes } from '@/utils/api'
 import { getFamilyMemberList } from '@/utils/api'
 import { resolveChangeStyle, formatDelta, colorClassFor } from '@/utils/indicator-change'
 import { parseDate } from '@/utils/format'
@@ -80,11 +82,8 @@ export default {
   data: function() {
     return {
       members: [], currentMemberId: null,
-      reportTypeOptions: [
-        { value: 'gut_flora', label: '肠道菌群' },
-        { value: 'food_recommend', label: '食材推荐' }
-      ],
-      activeReportType: 'gut_flora',
+      reportTypeOptions: [],
+      activeReportType: '',
       indicatorOptions: [], theIndicatorId: null,
       series: [], loadedDefinitionIds: [],
       rankTabs: [ { value: 'flora', label: '菌群变化' }, { value: 'food', label: '食材变化' }, { value: 'indicator', label: '指标变化' } ],
@@ -108,7 +107,7 @@ export default {
     },
     currentReportTypeName: function() {
       var self = this
-      var o = this.reportTypeOptions.find(function(x) { return x.value === self.activeReportType })
+      var o = this.reportTypeOptions.find(function(x) { return x.sourceType === self.activeReportType })
       return (o && o.label) || ''
     }
   },
@@ -142,10 +141,34 @@ export default {
         if (!self.currentMemberId && arr.length > 0) {
           self.currentMemberId = arr[0].id
           self.onMemberSelected()
+          self.loadSourceTypes()
         }
       })
     },
     onMemberSelected: function() { this.reloadAll() },
+    loadSourceTypes: function() {
+      var self = this
+      if (!this.currentMemberId) return
+      // 报告类型动态化:以该成员 indicator_values 实际存在的 source_type 为准,避免硬编码类型错配
+      getIndicatorSourceTypes({ subjectId: this.currentMemberId }).then(function(res) {
+        var arr = (res.data instanceof Array) ? res.data : []
+        if (arr.length > 0) {
+          self.reportTypeOptions = arr
+          var first = arr[0].sourceType
+          if (self.activeReportType !== first) {
+            self.activeReportType = first
+            self.reloadAll()
+          }
+        } else {
+          // 接口空/失败回退历史默认,避免筛选器无选项
+          self.reportTypeOptions = [
+            { sourceType: 'gut_flora', label: '肠道菌群' },
+            { sourceType: 'food_recommend', label: '食材推荐' }
+          ]
+          if (!self.activeReportType) { self.activeReportType = 'gut_flora'; self.reloadAll() }
+        }
+      })
+    },
     onMemberChange: function(e) {
       var idx = Number(e.detail.value)
       var m = this.members[idx]
@@ -154,7 +177,7 @@ export default {
     onReportTypeChange: function(e) {
       var idx = Number(e.detail.value)
       var o = this.reportTypeOptions[idx]
-      if (o) { this.activeReportType = o.value; this.reloadAll() }
+      if (o) { this.activeReportType = o.sourceType; this.reloadAll() }
     },
     reloadAll: function() {
       if (!this.currentMemberId) return
@@ -168,7 +191,9 @@ export default {
     loadRankingThenCurve: function() {
       var self = this
       if (!this.currentMemberId) return
-      var reportType = this.activeReportType === 'food_recommend' ? null : this.activeReportType
+      // food_recommend 无时序数据:FoodItem 为评分型数据,NormalizeIndicatorPipeline 不写 indicator_values,
+      // 此处回退为全量指标参与曲线/排名(有意为之,非 bug)
+      var reportType = (!this.activeReportType || this.activeReportType === 'food_recommend') ? null : this.activeReportType
       getIndicatorChangeRanking({
         subjectId: this.currentMemberId,
         reportType: reportType,
@@ -319,6 +344,18 @@ export default {
   font-size: 26rpx;
   color: #94A3B8;
 }
+.rank-gap {
+  display: inline-block;
+  margin: 4rpx 0;
+  color: #e53e3e;
+  font-size: 20rpx;
+}
+.rank-empty {
+  padding: 60rpx 0;
+  text-align: center;
+  font-size: 24rpx;
+  color: #94A3B8;
+}
 
 /* 排名 Tab */
 .rank-tabs {

+ 11 - 8
cfc-frontend/pages/profile-extra/family-members.vue

@@ -106,8 +106,8 @@
           <button class="detail-action-btn action-questionnaire" @click="doQuestionnaire(detailMember)">关系问卷</button>
           <button class="detail-action-btn action-interaction" @click="recordInteraction(detailMember)">记录互动</button>
           <button class="detail-action-btn action-kick" @click="confirmKick(detailMember)" v-if="isAdmin && detailMember && !detailMember.isSelf">删除成员</button>
-        </view>
-      </view>
+       </view>
+     </view>
     </view>
 
     <!-- ===== 底部操作栏 ===== -->
@@ -138,9 +138,10 @@ import {
   rejectJoinRequest, reportAppEnter
 } from '../../utils/api.js'
 import store from '../../store/index.js'
+import GenerationPicker from '@/components/GenerationPicker.vue'
 
 export default {
-  components: {},
+  components: { GenerationPicker },
   data() {
     return {
       userId: 0,
@@ -160,8 +161,9 @@ export default {
 
       inviteToken: '',
       joinRequests: [],
-      shareCardImage: ''
-    }
+      shareCardImage: '',
+
+      }
   },
   computed: {
     isSelfView() {
@@ -189,7 +191,8 @@ export default {
     reportAppEnter('members');
     
     var userInfo = uni.getStorageSync('userInfo')
-    this.userId = (userInfo && userInfo.id) || 0
+    // storage 里的 key 是 userId(utils/api.js 设的是 userId),切勿写 .id(永远为 0)
+    this.userId = (userInfo && userInfo.userId) || (uni.getStorageSync('userId')) || 0
     this.userNickname = uni.getStorageSync('nickname') || (userInfo && userInfo.nickname) || ''
     this.userAvatar = uni.getStorageSync('avatar') || (userInfo && userInfo.avatar) || ''
     // 家庭在用户注册时已由后端创建(登录接口另有兜底),此处不再触发建家庭逻辑;
@@ -1006,8 +1009,8 @@ export default {
   color: #67c23a;
 }
 .action-questionnaire {
-  background: #f3e8ff;
-  color: #8b5cf6;
+  background: #EEF0FF;
+  color: #6366F1;
 }
 .action-interaction {
   background: #fff7ed;

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-9ad8ca2e234f133b3eace922781ff848ab8a9af7
+379184e3af2b52cec9b1f01302ff4dcd11f9d897

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1577",
+  "version": "1.0.1580",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1577",
+      "version": "1.0.1580",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1578",
+  "version": "1.0.1581",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 81 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,87 @@
 
 ---
 
+## v1.0.1581 (2026-10-12)
+
+### Bug 修复
+- 修复辈分关系和出生日期保存回显问题
+- 透明无边框/右键菜单/任务栏图标/默认接口/点击完成待办
+- 修复 isAdmin 恒为 false 的 bug,并清理新增的修改关系入口
+- 首页合并冲突采用 feed 集成方案,移除浮动通知条
+- 对齐真实后台接口契约(任务/能量/吉祥物/喝水)
+
+### 其他
+- - 修复 UserService.updateFamilyMember:在管理员权限下保存 generationLevel/peerType
+- - 新增 FamilyMemberVO.birthday 字段,并回显至前端
+- - 修复 FamilyMemberService.toFamilyMemberVO:回写 birthday 为 yyyy-MM-dd 字符串
+- - 前端 family-members.vue onEditRelation:传递 birthday/generationLevel 在 URL
+- - 前端 add-member.vue onLoad:从 options 回显 birthday/generationLevel
+- - 前端 family-members.vue:修复 userId 取值(.id→.userId),移除修改关系入口
+- 
+- - api/mod.rs: upload_media(multipart); api/models.rs: MediaRecord
+- - settings.rs: AppSettings.custom_mascot_url 持久化
+- - commands.rs: upload_mascot/clear_custom_mascot; get_settings 返回 customMascotUrl
+- - settings.html: 形象选择+预览+上传/恢复默认 UI; 窗口高度 340->470
+- - pet-engine.js: setCustomImage 优先渲染自定义形象; bridge.js 监听 pet://custom-image 并启动加载
+- 
+- - index.html/interactions.js: 背景透明 + 右键菜单(设置/鼠标穿透/退出)
+- - commands.rs/main.rs: open_settings/quit_app 命令; 点击宠物优先完成待办任务
+- - poller.rs: 到期任务提醒并记录可一键完成的 pending_task
+- - settings.rs/settings.html: 默认接口地址改为 https://cfc.cf-club.com
+- - api/mod.rs: complete_task 封装
+- 
+- - 移除修改关系弹窗及入口:用户确认改用编辑资料,清理相关代码
+- - 恢复 GenerationPicker 注册(编辑资料流程必需)
+- - add-member.vue: modal-link 颜色改为品牌橙 #F97316
+- 
+- - Backend: IndicatorController source-types endpoint
+- - Backend: schema.sql delete duplicate report_format L1829
+- - Backend: DanReportUploadService compareUploads(new) + DanReportController /compare
+- - Frontend: indicator-curve.vue reportTypeOptions dynamic (source-types) + fallback
+- - Frontend: indicator-curve.vue skipGaps >0 display + rank-empty tip
+- - Frontend: dan-assessment/comparison.vue + pages.json registration
+- - Frontend: utils/api.js getIndicatorSourceTypes wrapper
+- 
+
+### 新功能
+- 设置内自定义宠物形象上传(复用 /api/media/upload)
+- DAN compare + indicator curve dynamic types + skipGaps + schema cleanup
+- JS<->Rust 桥接、设置窗口与命令实现
+- 托盘菜单、单实例与主进程组装
+- 轮询器(任务/能量/喝水)与 AppState
+- ApiClient 端点封装与响应解析
+- 设置文件读写与默认值
+- keyring 令牌存储(双平台)
+- 宠物形象配置解析(真实 JSON 测试)
+- 宠物状态映射与优先级测试
+- API serde 模型层与解析测试
+- Tauri v2 脚手架与双平台窗口配置
+
+### 重构
+- TrendChart 移入 pages/health 子包并更新引用
+- 移除已退役的 WPF 版
+
+### 文档
+- 双平台 CI、分发说明与 README
+
+
+## v1.0.1580 (2026-10-12)
+
+### Bug 修复
+- 移除 ProductEdit.vue 重复 if 行,修复编译失败
+
+
+## v1.0.1579 (2026-10-12)
+
+### 新功能
+- preserve pagination page when editing products
+
+### 其他
+- - ProductEdit.vue: read page from route query and redirect back with preserved page
+- - Fixes issue where editing a product on page 2 would redirect back to page 1 after save
+- 
+
+
 ## v1.0.1578 (2026-10-11)
 
 ### Bug 修复

+ 82 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1578
+> 当前版本: v1.0.1581
 
 ## 历史版本
 
@@ -8,6 +8,87 @@
 
 ---
 
+## v1.0.1581 (2026-10-12)
+
+### Bug 修复
+- 修复辈分关系和出生日期保存回显问题
+- 透明无边框/右键菜单/任务栏图标/默认接口/点击完成待办
+- 修复 isAdmin 恒为 false 的 bug,并清理新增的修改关系入口
+- 首页合并冲突采用 feed 集成方案,移除浮动通知条
+- 对齐真实后台接口契约(任务/能量/吉祥物/喝水)
+
+### 其他
+- - 修复 UserService.updateFamilyMember:在管理员权限下保存 generationLevel/peerType
+- - 新增 FamilyMemberVO.birthday 字段,并回显至前端
+- - 修复 FamilyMemberService.toFamilyMemberVO:回写 birthday 为 yyyy-MM-dd 字符串
+- - 前端 family-members.vue onEditRelation:传递 birthday/generationLevel 在 URL
+- - 前端 add-member.vue onLoad:从 options 回显 birthday/generationLevel
+- - 前端 family-members.vue:修复 userId 取值(.id→.userId),移除修改关系入口
+- 
+- - api/mod.rs: upload_media(multipart); api/models.rs: MediaRecord
+- - settings.rs: AppSettings.custom_mascot_url 持久化
+- - commands.rs: upload_mascot/clear_custom_mascot; get_settings 返回 customMascotUrl
+- - settings.html: 形象选择+预览+上传/恢复默认 UI; 窗口高度 340->470
+- - pet-engine.js: setCustomImage 优先渲染自定义形象; bridge.js 监听 pet://custom-image 并启动加载
+- 
+- - index.html/interactions.js: 背景透明 + 右键菜单(设置/鼠标穿透/退出)
+- - commands.rs/main.rs: open_settings/quit_app 命令; 点击宠物优先完成待办任务
+- - poller.rs: 到期任务提醒并记录可一键完成的 pending_task
+- - settings.rs/settings.html: 默认接口地址改为 https://cfc.cf-club.com
+- - api/mod.rs: complete_task 封装
+- 
+- - 移除修改关系弹窗及入口:用户确认改用编辑资料,清理相关代码
+- - 恢复 GenerationPicker 注册(编辑资料流程必需)
+- - add-member.vue: modal-link 颜色改为品牌橙 #F97316
+- 
+- - Backend: IndicatorController source-types endpoint
+- - Backend: schema.sql delete duplicate report_format L1829
+- - Backend: DanReportUploadService compareUploads(new) + DanReportController /compare
+- - Frontend: indicator-curve.vue reportTypeOptions dynamic (source-types) + fallback
+- - Frontend: indicator-curve.vue skipGaps >0 display + rank-empty tip
+- - Frontend: dan-assessment/comparison.vue + pages.json registration
+- - Frontend: utils/api.js getIndicatorSourceTypes wrapper
+- 
+
+### 新功能
+- 设置内自定义宠物形象上传(复用 /api/media/upload)
+- DAN compare + indicator curve dynamic types + skipGaps + schema cleanup
+- JS<->Rust 桥接、设置窗口与命令实现
+- 托盘菜单、单实例与主进程组装
+- 轮询器(任务/能量/喝水)与 AppState
+- ApiClient 端点封装与响应解析
+- 设置文件读写与默认值
+- keyring 令牌存储(双平台)
+- 宠物形象配置解析(真实 JSON 测试)
+- 宠物状态映射与优先级测试
+- API serde 模型层与解析测试
+- Tauri v2 脚手架与双平台窗口配置
+
+### 重构
+- TrendChart 移入 pages/health 子包并更新引用
+- 移除已退役的 WPF 版
+
+### 文档
+- 双平台 CI、分发说明与 README
+
+
+## v1.0.1580 (2026-10-12)
+
+### Bug 修复
+- 移除 ProductEdit.vue 重复 if 行,修复编译失败
+
+
+## v1.0.1579 (2026-10-12)
+
+### 新功能
+- preserve pagination page when editing products
+
+### 其他
+- - ProductEdit.vue: read page from route query and redirect back with preserved page
+- - Fixes issue where editing a product on page 2 would redirect back to page 1 after save
+- 
+
+
 ## v1.0.1578 (2026-10-11)
 
 ### Bug 修复

+ 0 - 1
cfc-web/src/views/admin/ProductEdit.vue

@@ -1082,7 +1082,6 @@ async loadGiftRule(productId) {
           }
           if (res.code === 200) {
             // Save dimension weights
-            if (productId && Array.isArray(dimensionWeights)) {
             if (productId && Array.isArray(dimensionWeights)) {
               try {
                 await saveDimensionWeights('product', productId, dimensionWeights)