Просмотр исходного кода

Merge pull request #368 from tino-chen/feature/tino-chen-HelloClaw

[毕业设计] HelloClaw - 个性化 AI Agent 助手
jjyaoao 3 месяцев назад
Родитель
Сommit
c496cdd320
80 измененных файлов с 13811 добавлено и 0 удалено
  1. 15 0
      Co-creation-projects/tino-chen-HelloClaw/.env.example
  2. 52 0
      Co-creation-projects/tino-chen-HelloClaw/.gitignore
  3. 235 0
      Co-creation-projects/tino-chen-HelloClaw/README.md
  4. 2 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/.env.example
  5. 13 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/index.html
  6. 3888 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/package-lock.json
  7. 38 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/package.json
  8. BIN
      Co-creation-projects/tino-chen-HelloClaw/frontend/public/favicon.ico
  9. 24 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/public/favicon.svg
  10. 115 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/App.vue
  11. 137 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/chat.ts
  12. 39 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/config.ts
  13. 49 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/index.ts
  14. 22 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/memory.ts
  15. 49 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/session.ts
  16. 100 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/assets/base.css
  17. 24 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/assets/lobster.svg
  18. 24 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/assets/logo.svg
  19. 23 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/assets/main.css
  20. 411 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/ChatMessage.vue
  21. 41 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/HelloWorld.vue
  22. 95 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/TheWelcome.vue
  23. 87 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/WelcomeItem.vue
  24. 7 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconCommunity.vue
  25. 7 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconDocumentation.vue
  26. 7 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconEcosystem.vue
  27. 7 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconSupport.vue
  28. 19 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconTooling.vue
  29. 17 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/main.ts
  30. 29 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/router/index.ts
  31. 12 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/stores/counter.ts
  32. 47 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/utils/markdown.ts
  33. 88 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/utils/toolDisplay.ts
  34. 15 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/AboutView.vue
  35. 1260 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/ChatView.vue
  36. 395 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/ConfigView.vue
  37. 9 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/HomeView.vue
  38. 257 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/MemoryView.vue
  39. 202 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/SessionsView.vue
  40. 12 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/tsconfig.app.json
  41. 11 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/tsconfig.json
  42. 13 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/tsconfig.node.json
  43. 28 0
      Co-creation-projects/tino-chen-HelloClaw/frontend/vite.config.ts
  44. 505 0
      Co-creation-projects/tino-chen-HelloClaw/main.ipynb
  45. BIN
      Co-creation-projects/tino-chen-HelloClaw/outputs/helloclaw.png
  46. 14 0
      Co-creation-projects/tino-chen-HelloClaw/requirements.txt
  47. 0 0
      Co-creation-projects/tino-chen-HelloClaw/src/__init__.py
  48. 5 0
      Co-creation-projects/tino-chen-HelloClaw/src/agent/__init__.py
  49. 248 0
      Co-creation-projects/tino-chen-HelloClaw/src/agent/enhanced_llm.py
  50. 386 0
      Co-creation-projects/tino-chen-HelloClaw/src/agent/enhanced_simple_agent.py
  51. 525 0
      Co-creation-projects/tino-chen-HelloClaw/src/agent/helloclaw_agent.py
  52. 1 0
      Co-creation-projects/tino-chen-HelloClaw/src/api/__init__.py
  53. 158 0
      Co-creation-projects/tino-chen-HelloClaw/src/api/chat.py
  54. 190 0
      Co-creation-projects/tino-chen-HelloClaw/src/api/config.py
  55. 255 0
      Co-creation-projects/tino-chen-HelloClaw/src/api/memory.py
  56. 329 0
      Co-creation-projects/tino-chen-HelloClaw/src/api/session.py
  57. 5 0
      Co-creation-projects/tino-chen-HelloClaw/src/channels/__init__.py
  58. 226 0
      Co-creation-projects/tino-chen-HelloClaw/src/channels/cli_channel.py
  59. 1 0
      Co-creation-projects/tino-chen-HelloClaw/src/cli/__init__.py
  60. 222 0
      Co-creation-projects/tino-chen-HelloClaw/src/cli/main.py
  61. 100 0
      Co-creation-projects/tino-chen-HelloClaw/src/main.py
  62. 6 0
      Co-creation-projects/tino-chen-HelloClaw/src/memory/__init__.py
  63. 273 0
      Co-creation-projects/tino-chen-HelloClaw/src/memory/capture.py
  64. 111 0
      Co-creation-projects/tino-chen-HelloClaw/src/memory/memory_flush.py
  65. 396 0
      Co-creation-projects/tino-chen-HelloClaw/src/memory/session_summarizer.py
  66. 13 0
      Co-creation-projects/tino-chen-HelloClaw/src/tools/__init__.py
  67. 13 0
      Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/__init__.py
  68. 269 0
      Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/execute_command.py
  69. 231 0
      Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/memory.py
  70. 248 0
      Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/web_fetch.py
  71. 205 0
      Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/web_search.py
  72. 5 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/__init__.py
  73. 784 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/manager.py
  74. 54 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/BOOTSTRAP.md
  75. 5 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/HEARTBEAT.md
  76. 22 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/IDENTITY.md
  77. 21 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/MEMORY.md
  78. 36 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/SOUL.md
  79. 17 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/USER.md
  80. 7 0
      Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/config.json

+ 15 - 0
Co-creation-projects/tino-chen-HelloClaw/.env.example

@@ -0,0 +1,15 @@
+# ============================================================================
+# HelloClaw 环境变量配置
+# ============================================================================
+
+# LLM 配置
+LLM_MODEL_ID=glm-5
+LLM_API_KEY=your-api-key-here
+LLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4/
+
+# 服务配置
+PORT=8000
+CORS_ORIGINS=http://localhost:5173
+
+# 工作空间配置
+WORKSPACE_PATH=~/.helloclaw/workspace

+ 52 - 0
Co-creation-projects/tino-chen-HelloClaw/.gitignore

@@ -0,0 +1,52 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual environments
+.env
+.venv
+env/
+venv/
+ENV/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Jupyter Notebook
+.ipynb_checkpoints/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Node.js
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Project specific
+*.log
+.cache/

+ 235 - 0
Co-creation-projects/tino-chen-HelloClaw/README.md

@@ -0,0 +1,235 @@
+# HelloClaw - 个性化 AI Agent 助手
+
+> 基于 HelloAgents 框架的个性化 AI Agent 应用,支持身份定制、记忆系统和流式工具调用
+
+<div align="center">
+  <img src="outputs/helloclaw.png" alt="HelloClaw Screenshot" width="80%"/>
+</div>
+
+## 项目简介
+
+HelloClaw 是一个基于 Hello-Agents 框架构建的个性化 AI Agent 应用,实现了类似 OpenClaw 的核心功能。它不仅是一个智能对话助手,更是一个可以"认识你"、记住你、并根据你的需求不断成长的个性化 AI 伙伴。
+
+**核心特性:**
+- 支持自定义 Agent 身份和个性
+- 长期记忆和每日记忆的自动管理
+- 流式工具调用,实时反馈执行状态
+- 多会话支持,会话历史持久化
+- 现代化 Web 界面(Vue3 + FastAPI)
+
+## 核心功能
+
+- [x] **智能对话** - 基于 ReActAgent 的智能对话能力
+- [x] **记忆系统** - 支持长期记忆(MEMORY.md)和每日记忆的自动管理
+- [x] **工具调用** - 内置多种工具(文件操作、代码执行、网页搜索、网页抓取等)
+- [x] **会话管理** - 多会话支持,会话历史持久化
+- [x] **身份定制** - 可通过配置文件自定义 Agent 身份和个性
+- [x] **流式输出** - 支持 SSE 流式响应,实时显示回复
+- [x] **Web 界面** - 现代化的 Vue3 前端界面
+
+## 技术栈
+
+| 层级 | 技术 |
+|------|------|
+| Agent 框架 | Hello-Agents (ReActAgent / SimpleAgent) |
+| 后端框架 | Python + FastAPI |
+| 前端框架 | Vue 3 + TypeScript + Ant Design Vue |
+| 流式通信 | SSE (Server-Sent Events) |
+| 包管理 | uv (Python) / pnpm (前端) |
+
+## 技术亮点
+
+### 1. 增强版流式工具调用
+
+实现了 `EnhancedSimpleAgent` 和 `EnhancedHelloAgentsLLM`,支持真正的流式工具调用:
+- 实时推送工具调用状态(开始/完成)
+- 支持多轮工具调用迭代
+- 优雅的错误处理和回退机制
+
+### 2. 智能记忆系统
+
+- **长期记忆 (MEMORY.md)**: 存储重要信息,跨会话保持
+- **每日记忆**: 自动按日期分类存储对话记忆
+- **Memory Flush**: 当上下文接近阈值时,自动提醒 Agent 保存重要信息
+
+### 3. 工作空间管理
+
+- 基于 Markdown 配置文件的身份定制系统
+- 支持 IDENTITY.md、USER.md、SOUL.md 等多种配置
+- 热加载配置,无需重启服务
+
+## 快速开始
+
+### 环境要求
+
+- Python 3.10+
+- Node.js 18+(可选,仅前端需要)
+
+### 安装依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+### 配置 API 密钥
+
+```bash
+# 创建.env文件
+cp .env.example .env
+
+# 编辑.env文件,填入你的API密钥
+# 支持 OpenAI 兼容的 API(如智谱 AI、ModelScope 等)
+```
+
+### 运行项目
+
+**方式一:使用 Jupyter Notebook(推荐)**
+
+```bash
+jupyter lab
+# 打开 main.ipynb 并运行
+```
+
+**方式二:运行完整 Web 服务**
+
+```bash
+# 启动后端
+cd tino-chen-HelloClaw
+pip install uvicorn
+uvicorn src.main:app --reload --port 8000
+
+# 启动前端(新终端)
+cd frontend
+npm install
+npm run dev
+```
+
+访问 http://localhost:5173 即可使用 Web 界面。
+
+## 使用示例
+
+### 基础对话
+
+```python
+from src.agent.helloclaw_agent import HelloClawAgent
+
+# 创建 Agent
+agent = HelloClawAgent()
+
+# 同步对话
+response = agent.chat("你好,请介绍一下你自己")
+print(response)
+```
+
+### 流式对话
+
+```python
+import asyncio
+
+async def chat_stream():
+    agent = HelloClawAgent()
+
+    async for event in agent.achat("帮我搜索一下今天的新闻"):
+        if event.type.value == "llm_chunk":
+            print(event.data.get("chunk", ""), end="", flush=True)
+        elif event.type.value == "tool_call_start":
+            print(f"\n[调用工具: {event.data.get('tool_name')}]")
+        elif event.type.value == "tool_call_finish":
+            print(f"[工具执行完成]")
+
+asyncio.run(chat_stream())
+```
+
+## 项目结构
+
+```
+tino-chen-HelloClaw/
+├── README.md              # 项目说明文档
+├── requirements.txt       # Python 依赖列表
+├── main.ipynb            # 主要的 Jupyter Notebook(快速演示)
+├── .env.example          # 环境变量模板
+├── data/                 # 数据文件
+├── outputs/              # 输出结果(截图等)
+│   └── helloclaw.png     # 项目截图
+├── src/                  # 后端源代码
+│   ├── agent/            # Agent 封装
+│   │   ├── helloclaw_agent.py      # 主 Agent 类
+│   │   ├── enhanced_simple_agent.py # 增强版 SimpleAgent
+│   │   └── enhanced_llm.py         # 增强版 LLM(流式工具调用)
+│   ├── tools/            # 自定义工具
+│   │   └── builtin/
+│   │       ├── memory.py              # 记忆工具
+│   │       ├── execute_command.py     # 命令执行工具
+│   │       ├── web_search.py          # 网页搜索工具
+│   │       └── web_fetch.py           # 网页抓取工具
+│   ├── memory/           # 记忆管理
+│   │   ├── capture.py             # 记忆捕获
+│   │   ├── memory_flush.py        # 记忆刷新
+│   │   └── session_summarizer.py  # 会话摘要
+│   ├── workspace/        # 工作空间管理
+│   │   ├── manager.py             # 工作空间管理器
+│   │   └── templates/             # 配置模板
+│   └── api/              # FastAPI 路由
+│       ├── chat.py                # 聊天接口
+│       ├── session.py             # 会话管理
+│       ├── config.py              # 配置管理
+│       └── memory.py              # 记忆接口
+└── frontend/             # 前端源代码(Vue3)
+    ├── src/
+    │   ├── views/                 # 页面组件
+    │   ├── components/            # 通用组件
+    │   ├── api/                   # API 请求
+    │   └── assets/                # 静态资源
+    ├── public/                    # 公共资源
+    ├── package.json               # 前端依赖配置
+    └── vite.config.ts             # Vite 配置
+```
+
+## 工作空间配置
+
+工作空间位于 `~/.helloclaw/`,包含:
+
+```
+~/.helloclaw/
+├── config.json       # 全局 LLM 配置
+└── workspace/        # Agent 工作空间
+    ├── IDENTITY.md   # 身份配置
+    ├── MEMORY.md     # 长期记忆
+    ├── SOUL.md       # 灵魂/个性
+    ├── USER.md       # 用户信息
+    ├── AGENTS.md     # 系统提示词
+    ├── memory/       # 每日记忆
+    └── sessions/     # 会话历史
+```
+
+## 项目亮点
+
+1. **真正的流式工具调用** - 不是简单的流式文本输出,而是完整的流式工具调用流程
+2. **智能记忆管理** - 自动捕获对话中的重要信息,支持长期记忆和每日记忆
+3. **高度可定制** - 通过 Markdown 配置文件自定义 Agent 的身份、个性、用户信息
+4. **生产级代码** - 完整的错误处理、日志记录、配置管理
+
+## 未来计划
+
+- [ ] 支持多模态输入(图片、文件)
+- [ ] 添加更多内置工具(代码解释器、数据库查询等)
+- [ ] 支持 Agent 间协作
+- [ ] 添加语音交互能力
+
+## 许可证
+
+MIT License
+
+## 作者
+
+- GitHub: [@tino-chen](https://github.com/tino-chen)
+- 项目链接: [HelloClaw](https://github.com/tino-chen/helloclaw)
+
+## 致谢
+
+- [Hello-Agents](https://github.com/datawhalechina/hello-agents) - Agent 框架
+- [FastAPI](https://fastapi.tiangolo.com/) - 后端框架
+- [Vue.js](https://vuejs.org/) - 前端框架
+- [Ant Design Vue](https://antdv.com/) - UI 组件库
+
+感谢 Datawhale 社区和 Hello-Agents 项目!

+ 2 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/.env.example

@@ -0,0 +1,2 @@
+# API Base URL
+VITE_API_BASE_URL=http://localhost:8000/api

+ 13 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>HelloClaw</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 3888 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/package-lock.json

@@ -0,0 +1,3888 @@
+{
+  "name": "frontend",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "frontend",
+      "version": "0.0.0",
+      "dependencies": {
+        "@ant-design/icons-vue": "^7.0.1",
+        "ant-design-vue": "^4.2.6",
+        "axios": "^1.13.5",
+        "dompurify": "^3.3.1",
+        "marked": "^17.0.3",
+        "pinia": "^3.0.4",
+        "vue": "^3.5.28",
+        "vue-router": "^5.0.2"
+      },
+      "devDependencies": {
+        "@tsconfig/node24": "^24.0.4",
+        "@types/dompurify": "^3.0.5",
+        "@types/node": "^24.10.13",
+        "@vitejs/plugin-vue": "^6.0.4",
+        "@vue/tsconfig": "^0.8.1",
+        "npm-run-all2": "^8.0.4",
+        "typescript": "~5.9.3",
+        "vite": "^7.3.1",
+        "vite-plugin-vue-devtools": "^8.0.6",
+        "vue-tsc": "^3.2.4"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@ant-design/colors": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
+      "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
+      "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/icons-vue": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
+      "integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-svg": "^4.2.1"
+      },
+      "peerDependencies": {
+        "vue": ">=3.0.3"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.27.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+      "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-create-class-features-plugin": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
+      "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.27.3",
+        "@babel/helper-member-expression-to-functions": "^7.28.5",
+        "@babel/helper-optimise-call-expression": "^7.27.1",
+        "@babel/helper-replace-supers": "^7.28.6",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+        "@babel/traverse": "^7.28.6",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-member-expression-to-functions": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+      "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.5",
+        "@babel/types": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-optimise-call-expression": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+      "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-replace-supers": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
+      "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-member-expression-to-functions": "^7.28.5",
+        "@babel/helper-optimise-call-expression": "^7.27.1",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+      "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.27.1",
+        "@babel/types": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+      "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+      "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-proposal-decorators": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz",
+      "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-create-class-features-plugin": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
+        "@babel/plugin-syntax-decorators": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-decorators": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz",
+      "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-import-attributes": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
+      "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-import-meta": {
+      "version": "7.10.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+      "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.10.4"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-jsx": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
+      "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-typescript": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
+      "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-typescript": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
+      "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.27.3",
+        "@babel/helper-create-class-features-plugin": "^7.28.6",
+        "@babel/helper-plugin-utils": "^7.28.6",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+        "@babel/plugin-syntax-typescript": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+      "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+      "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+      "license": "MIT"
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+      "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+      "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+      "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+      "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+      "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+      "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+      "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+      "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+      "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+      "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+      "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+      "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+      "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+      "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+      "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+      "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+      "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+      "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+      "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+      "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+      "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+      "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+      "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@polka/url": {
+      "version": "1.0.0-next.29",
+      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+      "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+      "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+      "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+      "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+      "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+      "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+      "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+      "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+      "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+      "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+      "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+      "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+      "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+      "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+      "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+      "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+      "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+      "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+      "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+      "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+      "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+      "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+      "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+      "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+      "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+      "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+      "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@simonwep/pickr": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.8.2.tgz",
+      "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js": "^3.15.1",
+        "nanopop": "^2.1.0"
+      }
+    },
+    "node_modules/@tsconfig/node24": {
+      "version": "24.0.4",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.4.tgz",
+      "integrity": "sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/dompurify": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
+      "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/trusted-types": "*"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.11.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
+      "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/@types/trusted-types": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz",
+      "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-rc.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+      "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.28"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+      "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+      "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue-macros/common": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
+      "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-sfc": "^3.5.22",
+        "ast-kit": "^2.1.2",
+        "local-pkg": "^1.1.2",
+        "magic-string-ast": "^1.0.2",
+        "unplugin-utils": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/vue-macros"
+      },
+      "peerDependencies": {
+        "vue": "^2.7.0 || ^3.2.25"
+      },
+      "peerDependenciesMeta": {
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/babel-helper-vue-transform-on": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
+      "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/babel-plugin-jsx": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz",
+      "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/plugin-syntax-jsx": "^7.27.1",
+        "@babel/template": "^7.27.2",
+        "@babel/traverse": "^7.28.0",
+        "@babel/types": "^7.28.2",
+        "@vue/babel-helper-vue-transform-on": "1.5.0",
+        "@vue/babel-plugin-resolve-type": "1.5.0",
+        "@vue/shared": "^3.5.18"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@babel/core": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/babel-plugin-resolve-type": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz",
+      "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.27.1",
+        "@babel/helper-module-imports": "^7.27.1",
+        "@babel/helper-plugin-utils": "^7.27.1",
+        "@babel/parser": "^7.28.0",
+        "@vue/compiler-sfc": "^3.5.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz",
+      "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@vue/shared": "3.5.29",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz",
+      "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.29",
+        "@vue/shared": "3.5.29"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz",
+      "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@vue/compiler-core": "3.5.29",
+        "@vue/compiler-dom": "3.5.29",
+        "@vue/compiler-ssr": "3.5.29",
+        "@vue/shared": "3.5.29",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz",
+      "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.29",
+        "@vue/shared": "3.5.29"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+      "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.9"
+      }
+    },
+    "node_modules/@vue/devtools-core": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.0.7.tgz",
+      "integrity": "sha512-PmpiPxvg3Of80ODHVvyckxwEW1Z02VIAvARIZS1xegINn3VuNQLm9iHUmKD+o6cLkMNWV8OG8x7zo0kgydZgdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^8.0.7",
+        "@vue/devtools-shared": "^8.0.7"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/@vue/devtools-core/node_modules/@vue/devtools-kit": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.7.tgz",
+      "integrity": "sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^8.0.7",
+        "birpc": "^2.6.1",
+        "hookable": "^5.5.3",
+        "perfect-debounce": "^2.0.0"
+      }
+    },
+    "node_modules/@vue/devtools-core/node_modules/@vue/devtools-shared": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.7.tgz",
+      "integrity": "sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/devtools-core/node_modules/perfect-debounce": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+      "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+      "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.9",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+      "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/language-core": {
+      "version": "3.2.5",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.5.tgz",
+      "integrity": "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^3.0.0",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1",
+        "picomatch": "^4.0.2"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz",
+      "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.29"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz",
+      "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.29",
+        "@vue/shared": "3.5.29"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz",
+      "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.29",
+        "@vue/runtime-core": "3.5.29",
+        "@vue/shared": "3.5.29",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz",
+      "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.29",
+        "@vue/shared": "3.5.29"
+      },
+      "peerDependencies": {
+        "vue": "3.5.29"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
+      "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/tsconfig": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz",
+      "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "typescript": "5.x",
+        "vue": "^3.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.16.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/alien-signals": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
+      "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/ansis": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
+      "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/ant-design-vue": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
+      "integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-vue": "^7.0.0",
+        "@babel/runtime": "^7.10.5",
+        "@ctrl/tinycolor": "^3.5.0",
+        "@emotion/hash": "^0.9.0",
+        "@emotion/unitless": "^0.8.0",
+        "@simonwep/pickr": "~1.8.0",
+        "array-tree-filter": "^2.1.0",
+        "async-validator": "^4.0.0",
+        "csstype": "^3.1.1",
+        "dayjs": "^1.10.5",
+        "dom-align": "^1.12.1",
+        "dom-scroll-into-view": "^2.0.0",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.15",
+        "resize-observer-polyfill": "^1.5.1",
+        "scroll-into-view-if-needed": "^2.2.25",
+        "shallow-equal": "^1.0.0",
+        "stylis": "^4.1.3",
+        "throttle-debounce": "^5.0.0",
+        "vue-types": "^3.0.0",
+        "warning": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design-vue"
+      },
+      "peerDependencies": {
+        "vue": ">=3.2.0"
+      }
+    },
+    "node_modules/array-tree-filter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
+      "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
+      "license": "MIT"
+    },
+    "node_modules/ast-kit": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
+      "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "pathe": "^2.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/ast-walker-scope": {
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz",
+      "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.4",
+        "ast-kit": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.13.6",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+      "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
+      "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+      "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.9.0",
+        "caniuse-lite": "^1.0.30001759",
+        "electron-to-chromium": "^1.5.263",
+        "node-releases": "^2.0.27",
+        "update-browserslist-db": "^1.2.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/bundle-name": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+      "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "run-applescript": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001775",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz",
+      "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
+      "license": "MIT"
+    },
+    "node_modules/confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+      "license": "MIT"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/core-js": {
+      "version": "3.48.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
+      "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cross-spawn/node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/cross-spawn/node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.19",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/default-browser": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
+      "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bundle-name": "^4.1.0",
+        "default-browser-id": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/default-browser-id": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+      "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/define-lazy-prop": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+      "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dom-align": {
+      "version": "1.12.4",
+      "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz",
+      "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
+      "license": "MIT"
+    },
+    "node_modules/dom-scroll-into-view": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
+      "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
+      "license": "MIT"
+    },
+    "node_modules/dompurify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
+      "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
+      "license": "(MPL-2.0 OR Apache-2.0)",
+      "optionalDependencies": {
+        "@types/trusted-types": "^2.0.7"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.302",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
+      "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/error-stack-parser-es": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
+      "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+      "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.27.3",
+        "@esbuild/android-arm": "0.27.3",
+        "@esbuild/android-arm64": "0.27.3",
+        "@esbuild/android-x64": "0.27.3",
+        "@esbuild/darwin-arm64": "0.27.3",
+        "@esbuild/darwin-x64": "0.27.3",
+        "@esbuild/freebsd-arm64": "0.27.3",
+        "@esbuild/freebsd-x64": "0.27.3",
+        "@esbuild/linux-arm": "0.27.3",
+        "@esbuild/linux-arm64": "0.27.3",
+        "@esbuild/linux-ia32": "0.27.3",
+        "@esbuild/linux-loong64": "0.27.3",
+        "@esbuild/linux-mips64el": "0.27.3",
+        "@esbuild/linux-ppc64": "0.27.3",
+        "@esbuild/linux-riscv64": "0.27.3",
+        "@esbuild/linux-s390x": "0.27.3",
+        "@esbuild/linux-x64": "0.27.3",
+        "@esbuild/netbsd-arm64": "0.27.3",
+        "@esbuild/netbsd-x64": "0.27.3",
+        "@esbuild/openbsd-arm64": "0.27.3",
+        "@esbuild/openbsd-x64": "0.27.3",
+        "@esbuild/openharmony-arm64": "0.27.3",
+        "@esbuild/sunos-x64": "0.27.3",
+        "@esbuild/win32-arm64": "0.27.3",
+        "@esbuild/win32-ia32": "0.27.3",
+        "@esbuild/win32-x64": "0.27.3"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/exsolve": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+      "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-docker": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+      "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-inside-container": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+      "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^3.0.0"
+      },
+      "bin": {
+        "is-inside-container": "cli.js"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
+      "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/is-wsl": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+      "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-inside-container": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
+      "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz",
+      "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.17.0 || >=20.5.0"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/kolorist": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+      "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/local-pkg": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
+      "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
+      "license": "MIT",
+      "dependencies": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.3.0",
+        "quansync": "^0.2.11"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+      "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
+      "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/magic-string-ast": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
+      "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
+      "license": "MIT",
+      "dependencies": {
+        "magic-string": "^0.30.19"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/marked": {
+      "version": "17.0.3",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.3.tgz",
+      "integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/memorystream": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+      "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10.0"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "node_modules/mlly": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
+      "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.15.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^1.3.1",
+        "ufo": "^1.6.1"
+      }
+    },
+    "node_modules/mlly/node_modules/confbox": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+      "license": "MIT"
+    },
+    "node_modules/mlly/node_modules/pkg-types": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.1.8",
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
+      }
+    },
+    "node_modules/mrmime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+      "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/nanopop": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.4.2.tgz",
+      "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
+      "license": "MIT"
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.27",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+      "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/npm-normalize-package-bin": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
+      "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^18.17.0 || >=20.5.0"
+      }
+    },
+    "node_modules/npm-run-all2": {
+      "version": "8.0.4",
+      "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-8.0.4.tgz",
+      "integrity": "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "cross-spawn": "^7.0.6",
+        "memorystream": "^0.3.1",
+        "picomatch": "^4.0.2",
+        "pidtree": "^0.6.0",
+        "read-package-json-fast": "^4.0.0",
+        "shell-quote": "^1.7.3",
+        "which": "^5.0.0"
+      },
+      "bin": {
+        "npm-run-all": "bin/npm-run-all/index.js",
+        "npm-run-all2": "bin/npm-run-all/index.js",
+        "run-p": "bin/run-p/index.js",
+        "run-s": "bin/run-s/index.js"
+      },
+      "engines": {
+        "node": "^20.5.0 || >=22.0.0",
+        "npm": ">= 10"
+      }
+    },
+    "node_modules/ohash": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+      "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/open": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+      "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "default-browser": "^5.2.1",
+        "define-lazy-prop": "^3.0.0",
+        "is-inside-container": "^1.0.0",
+        "wsl-utils": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pidtree": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+      "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "pidtree": "bin/pidtree.js"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
+      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.7"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.5.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pkg-types": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+      "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.2",
+        "exsolve": "^1.0.7",
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/quansync": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/antfu"
+        },
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/sxzz"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/read-package-json-fast": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
+      "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "json-parse-even-better-errors": "^4.0.0",
+        "npm-normalize-package-bin": "^4.0.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || >=20.5.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.59.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+      "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.59.0",
+        "@rollup/rollup-android-arm64": "4.59.0",
+        "@rollup/rollup-darwin-arm64": "4.59.0",
+        "@rollup/rollup-darwin-x64": "4.59.0",
+        "@rollup/rollup-freebsd-arm64": "4.59.0",
+        "@rollup/rollup-freebsd-x64": "4.59.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+        "@rollup/rollup-linux-arm64-musl": "4.59.0",
+        "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+        "@rollup/rollup-linux-loong64-musl": "4.59.0",
+        "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+        "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+        "@rollup/rollup-linux-x64-gnu": "4.59.0",
+        "@rollup/rollup-linux-x64-musl": "4.59.0",
+        "@rollup/rollup-openbsd-x64": "4.59.0",
+        "@rollup/rollup-openharmony-arm64": "4.59.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+        "@rollup/rollup-win32-x64-gnu": "4.59.0",
+        "@rollup/rollup-win32-x64-msvc": "4.59.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-applescript": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+      "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
+      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
+    "node_modules/scule": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+      "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/shallow-equal": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
+      "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
+      "license": "MIT"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shell-quote": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+      "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/sirv": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+      "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@polka/url": "^1.0.0-next.24",
+        "mrmime": "^2.0.0",
+        "totalist": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stylis": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
+      "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+      "license": "MIT"
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/totalist": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+      "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/ufo": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
+      "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/unplugin": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
+      "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "picomatch": "^4.0.3",
+        "webpack-virtual-modules": "^0.6.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/unplugin-utils": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
+      "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
+      "license": "MIT",
+      "dependencies": {
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+      "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.27.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-dev-rpc": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz",
+      "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "birpc": "^2.4.0",
+        "vite-hot-client": "^2.1.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0"
+      }
+    },
+    "node_modules/vite-hot-client": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.1.0.tgz",
+      "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
+      }
+    },
+    "node_modules/vite-plugin-inspect": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz",
+      "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansis": "^4.1.0",
+        "debug": "^4.4.1",
+        "error-stack-parser-es": "^1.0.5",
+        "ohash": "^2.0.11",
+        "open": "^10.2.0",
+        "perfect-debounce": "^2.0.0",
+        "sirv": "^3.0.1",
+        "unplugin-utils": "^0.3.0",
+        "vite-dev-rpc": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vite": "^6.0.0 || ^7.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@nuxt/kit": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-plugin-inspect/node_modules/perfect-debounce": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+      "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite-plugin-vue-devtools": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.7.tgz",
+      "integrity": "sha512-BWj/ykGpqVAJVdPyHmSTUm44buz3jPv+6jnvuFdQSRH0kAgP1cEIE4doHiFyqHXOmuB5EQVR/nh2g9YRiRNs9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-core": "^8.0.7",
+        "@vue/devtools-kit": "^8.0.7",
+        "@vue/devtools-shared": "^8.0.7",
+        "sirv": "^3.0.2",
+        "vite-plugin-inspect": "^11.3.3",
+        "vite-plugin-vue-inspector": "^5.3.2"
+      },
+      "engines": {
+        "node": ">=v14.21.3"
+      },
+      "peerDependencies": {
+        "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0"
+      }
+    },
+    "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-kit": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.7.tgz",
+      "integrity": "sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^8.0.7",
+        "birpc": "^2.6.1",
+        "hookable": "^5.5.3",
+        "perfect-debounce": "^2.0.0"
+      }
+    },
+    "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-shared": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.7.tgz",
+      "integrity": "sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite-plugin-vue-devtools/node_modules/perfect-debounce": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+      "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite-plugin-vue-inspector": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz",
+      "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.23.0",
+        "@babel/plugin-proposal-decorators": "^7.23.0",
+        "@babel/plugin-syntax-import-attributes": "^7.22.5",
+        "@babel/plugin-syntax-import-meta": "^7.10.4",
+        "@babel/plugin-transform-typescript": "^7.22.15",
+        "@vue/babel-plugin-jsx": "^1.1.5",
+        "@vue/compiler-dom": "^3.3.4",
+        "kolorist": "^1.8.0",
+        "magic-string": "^0.30.4"
+      },
+      "peerDependencies": {
+        "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+      "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
+      "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.29",
+        "@vue/compiler-sfc": "3.5.29",
+        "@vue/runtime-dom": "3.5.29",
+        "@vue/server-renderer": "3.5.29",
+        "@vue/shared": "3.5.29"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz",
+      "integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/generator": "^7.28.6",
+        "@vue-macros/common": "^3.1.1",
+        "@vue/devtools-api": "^8.0.6",
+        "ast-walker-scope": "^0.8.3",
+        "chokidar": "^5.0.0",
+        "json5": "^2.2.3",
+        "local-pkg": "^1.1.2",
+        "magic-string": "^0.30.21",
+        "mlly": "^1.8.0",
+        "muggle-string": "^0.4.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "scule": "^1.3.0",
+        "tinyglobby": "^0.2.15",
+        "unplugin": "^3.0.0",
+        "unplugin-utils": "^0.3.1",
+        "yaml": "^2.8.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@pinia/colada": ">=0.21.2",
+        "@vue/compiler-sfc": "^3.5.17",
+        "pinia": "^3.0.4",
+        "vue": "^3.5.0"
+      },
+      "peerDependenciesMeta": {
+        "@pinia/colada": {
+          "optional": true
+        },
+        "@vue/compiler-sfc": {
+          "optional": true
+        },
+        "pinia": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router/node_modules/@vue/devtools-api": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.7.tgz",
+      "integrity": "sha512-tc1TXAxclsn55JblLkFVcIRG7MeSJC4fWsPjfM7qu/IcmPUYnQ5Q8vzWwBpyDY24ZjmZTUCCwjRSNbx58IhlAA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^8.0.7"
+      }
+    },
+    "node_modules/vue-router/node_modules/@vue/devtools-kit": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.7.tgz",
+      "integrity": "sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^8.0.7",
+        "birpc": "^2.6.1",
+        "hookable": "^5.5.3",
+        "perfect-debounce": "^2.0.0"
+      }
+    },
+    "node_modules/vue-router/node_modules/@vue/devtools-shared": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.7.tgz",
+      "integrity": "sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA==",
+      "license": "MIT"
+    },
+    "node_modules/vue-router/node_modules/perfect-debounce": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+      "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+      "license": "MIT"
+    },
+    "node_modules/vue-tsc": {
+      "version": "3.2.5",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz",
+      "integrity": "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "2.4.28",
+        "@vue/language-core": "3.2.5"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/vue-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/vue-types/-/vue-types-3.0.2.tgz",
+      "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "3.0.1"
+      },
+      "engines": {
+        "node": ">=10.15.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+      "license": "MIT"
+    },
+    "node_modules/which": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
+      "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^3.1.1"
+      },
+      "bin": {
+        "node-which": "bin/which.js"
+      },
+      "engines": {
+        "node": "^18.17.0 || >=20.5.0"
+      }
+    },
+    "node_modules/wsl-utils": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+      "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-wsl": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yaml": {
+      "version": "2.8.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
+      "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/eemeli"
+      }
+    }
+  }
+}

+ 38 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/package.json

@@ -0,0 +1,38 @@
+{
+  "name": "frontend",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "run-p type-check \"build-only {@}\" --",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --build"
+  },
+  "dependencies": {
+    "@ant-design/icons-vue": "^7.0.1",
+    "ant-design-vue": "^4.2.6",
+    "axios": "^1.13.5",
+    "dompurify": "^3.3.1",
+    "marked": "^17.0.3",
+    "pinia": "^3.0.4",
+    "vue": "^3.5.28",
+    "vue-router": "^5.0.2"
+  },
+  "devDependencies": {
+    "@tsconfig/node24": "^24.0.4",
+    "@types/dompurify": "^3.0.5",
+    "@types/node": "^24.10.13",
+    "@vitejs/plugin-vue": "^6.0.4",
+    "@vue/tsconfig": "^0.8.1",
+    "npm-run-all2": "^8.0.4",
+    "typescript": "~5.9.3",
+    "vite": "^7.3.1",
+    "vite-plugin-vue-devtools": "^8.0.6",
+    "vue-tsc": "^3.2.4"
+  },
+  "engines": {
+    "node": "^20.19.0 || >=22.12.0"
+  }
+}

BIN
Co-creation-projects/tino-chen-HelloClaw/frontend/public/favicon.ico


+ 24 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/public/favicon.svg

@@ -0,0 +1,24 @@
+<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
+      <stop offset="0%" stop-color="#ff6b5b"/>
+      <stop offset="100%" stop-color="#d63031"/>
+    </linearGradient>
+  </defs>
+  <!-- Body -->
+  <path d="M60 15 C35 15 20 38 20 55 C20 72 32 90 48 95 L48 105 L56 105 L56 95 C56 95 60 97 64 95 L64 105 L72 105 L72 95 C88 90 100 72 100 55 C100 38 85 15 60 15Z" fill="url(#lobster-gradient)"/>
+  <!-- Left Claw -->
+  <path d="M25 42 C12 38 8 48 12 58 C16 68 26 63 30 54 C33 47 30 43 25 42Z" fill="url(#lobster-gradient)"/>
+  <!-- Right Claw -->
+  <path d="M95 42 C108 38 112 48 108 58 C104 68 94 63 90 54 C87 47 90 43 95 42Z" fill="url(#lobster-gradient)"/>
+  <!-- Antenna -->
+  <path d="M48 18 Q40 8 35 12" stroke="#ff6b5b" stroke-width="3" stroke-linecap="round"/>
+  <path d="M72 18 Q80 8 85 12" stroke="#ff6b5b" stroke-width="3" stroke-linecap="round"/>
+  <!-- Eyes -->
+  <circle cx="48" cy="38" r="7" fill="#1a1a2e"/>
+  <circle cx="72" cy="38" r="7" fill="#1a1a2e"/>
+  <circle cx="49" cy="37" r="3" fill="#fff"/>
+  <circle cx="73" cy="37" r="3" fill="#fff"/>
+  <!-- Smile -->
+  <path d="M52 52 Q60 58 68 52" stroke="#1a1a2e" stroke-width="2" stroke-linecap="round" fill="none"/>
+</svg>

+ 115 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/App.vue

@@ -0,0 +1,115 @@
+<script setup lang="ts">
+import { RouterLink, RouterView, useRoute } from 'vue-router'
+import { Menu, ConfigProvider, theme } from 'ant-design-vue'
+import { MessageOutlined, SettingOutlined, HistoryOutlined, BookOutlined } from '@ant-design/icons-vue'
+import LobsterIcon from '@/assets/lobster.svg'
+
+const route = useRoute()
+
+// 龙虾红主题配置
+const customTheme = {
+  token: {
+    colorPrimary: '#ff5c5c',
+    colorPrimaryHover: '#ff7070',
+    colorPrimaryActive: '#e64a4a',
+    colorPrimaryBg: 'rgba(255, 92, 92, 0.1)',
+    colorPrimaryBgHover: 'rgba(255, 92, 92, 0.2)',
+  },
+}
+</script>
+
+<template>
+  <ConfigProvider :theme="{ token: customTheme.token }">
+    <div class="app-container">
+      <aside class="sidebar">
+        <div class="logo">
+          <img :src="LobsterIcon" alt="HelloClaw" class="logo-icon" />
+          <span class="logo-text">HelloClaw</span>
+        </div>
+        <Menu
+          mode="inline"
+          :selected-keys="[route.name as string]"
+          class="sidebar-menu"
+        >
+          <Menu.Item key="chat">
+            <RouterLink to="/">
+              <MessageOutlined />
+              <span>聊天</span>
+            </RouterLink>
+          </Menu.Item>
+          <Menu.Item key="sessions">
+            <RouterLink to="/sessions">
+              <HistoryOutlined />
+              <span>会话</span>
+            </RouterLink>
+          </Menu.Item>
+          <Menu.Item key="memory">
+            <RouterLink to="/memory">
+              <BookOutlined />
+              <span>记忆</span>
+            </RouterLink>
+          </Menu.Item>
+          <Menu.Item key="config">
+            <RouterLink to="/config">
+              <SettingOutlined />
+              <span>配置</span>
+            </RouterLink>
+          </Menu.Item>
+        </Menu>
+      </aside>
+
+      <main class="main-content">
+        <RouterView />
+      </main>
+    </div>
+  </ConfigProvider>
+</template>
+
+<style scoped>
+.app-container {
+  display: flex;
+  height: 100vh;
+  overflow: hidden;
+}
+
+.sidebar {
+  width: 220px;
+  background-color: #fff;
+  border-right: 1px solid #f0f0f0;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.logo {
+  padding: 20px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.logo-icon {
+  width: 36px;
+  height: 36px;
+}
+
+.logo-text {
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+}
+
+.sidebar-menu {
+  flex: 1;
+  border-right: none;
+  padding-top: 8px;
+}
+
+.main-content {
+  flex: 1;
+  background-color: #f5f5f5;
+  overflow: auto;
+  height: 100vh;
+}
+</style>

+ 137 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/chat.ts

@@ -0,0 +1,137 @@
+import api from './index'
+
+const API_BASE = import.meta.env.VITE_API_BASE || ''
+
+export interface ChatMessage {
+  role: 'user' | 'assistant'
+  content: string
+}
+
+export interface ChatResponse {
+  content: string
+  session_id: string | null
+}
+
+export interface StreamEvent {
+  type: 'session' | 'step_start' | 'chunk' | 'tool_start' | 'tool_finish' | 'step_finish' | 'done' | 'error'
+  content?: string
+  tool?: string
+  args?: Record<string, unknown>
+  result?: string
+  error?: string
+  session_id?: string | null
+  step?: number
+  max_steps?: number
+}
+
+export type StreamCallback = (event: StreamEvent) => void
+
+export const chatApi = {
+  // 流式发送消息 (SSE)
+  sendMessage: async (message: string, sessionId?: string) => {
+    return api.post('/chat/send', { message, session_id: sessionId })
+  },
+
+  // 同步发送消息(支持取消,超时时间 5 分钟)
+  sendMessageSync: async (
+    message: string,
+    sessionId?: string,
+    signal?: AbortSignal
+  ): Promise<ChatResponse> => {
+    return api.post('/chat/send/sync', { message, session_id: sessionId }, {
+      signal,
+      timeout: 300000, // 5 分钟超时
+    })
+  },
+
+  // 流式发送消息 (SSE) - 返回完整响应
+  sendMessageStream: async (
+    message: string,
+    sessionId: string | null | undefined,
+    onChunk: StreamCallback,
+    signal?: AbortSignal
+  ): Promise<ChatResponse> => {
+    const response = await fetch(`${API_BASE}/api/chat/send/stream`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ message, session_id: sessionId }),
+      signal,
+    })
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`)
+    }
+
+    const reader = response.body?.getReader()
+    if (!reader) {
+      throw new Error('No response body')
+    }
+
+    const decoder = new TextDecoder()
+    let buffer = ''
+    let fullContent = ''
+    let finalSessionId = sessionId
+
+    try {
+      while (true) {
+        const { done, value } = await reader.read()
+        if (done) break
+
+        buffer += decoder.decode(value, { stream: true })
+
+        // 解析 SSE 事件
+        const lines = buffer.split('\n')
+        buffer = lines.pop() || ''
+
+        let currentEvent = ''
+        for (const line of lines) {
+          // 跳过 ping 事件和空行
+          if (line.startsWith('ping') || line.trim() === '') {
+            continue
+          }
+
+          if (line.startsWith('event:')) {
+            currentEvent = line.substring(6).trim()
+          } else if (line.startsWith('data:')) {
+            const data = line.substring(5).trim()
+            if (data && currentEvent) {
+              try {
+                const parsed = JSON.parse(data)
+
+                if (currentEvent === 'session') {
+                  finalSessionId = parsed.session_id
+                  onChunk({ type: 'session', session_id: parsed.session_id })
+                } else if (currentEvent === 'step_start') {
+                  onChunk({ type: 'step_start', step: parsed.step, max_steps: parsed.max_steps })
+                } else if (currentEvent === 'chunk') {
+                  fullContent += parsed.content || ''
+                  onChunk({ type: 'chunk', content: parsed.content })
+                } else if (currentEvent === 'tool_start') {
+                  onChunk({ type: 'tool_start', tool: parsed.tool, args: parsed.args })
+                } else if (currentEvent === 'tool_finish') {
+                  onChunk({ type: 'tool_finish', tool: parsed.tool, result: parsed.result })
+                } else if (currentEvent === 'step_finish') {
+                  onChunk({ type: 'step_finish', step: parsed.step })
+                } else if (currentEvent === 'done') {
+                  finalSessionId = parsed.session_id
+                  onChunk({ type: 'done', content: parsed.content, session_id: parsed.session_id })
+                } else if (currentEvent === 'error') {
+                  onChunk({ type: 'error', error: parsed.error })
+                }
+              } catch {
+                // 忽略解析错误
+              }
+              currentEvent = ''
+            }
+          }
+        }
+      }
+    } finally {
+      reader.releaseLock()
+    }
+
+    return { content: fullContent, session_id: finalSessionId ?? null }
+  },
+}

+ 39 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/config.ts

@@ -0,0 +1,39 @@
+import api from './index'
+
+export interface ConfigFile {
+  name: string
+  content: string
+}
+
+export interface ResetOptions {
+  reset_sessions?: boolean
+  reset_memory?: boolean
+  reset_global_config?: boolean
+}
+
+export interface AgentInfo {
+  name: string
+}
+
+export const configApi = {
+  list: async () => {
+    return api.get<{ configs: string[] }>('/config/list')
+  },
+  get: async (name: string) => {
+    return api.get<ConfigFile>(`/config/${name}`)
+  },
+  update: async (name: string, content: string) => {
+    return api.put<{ name: string; status: string }>(`/config/${name}`, { content })
+  },
+  reset: async (options: ResetOptions = {}) => {
+    const params = new URLSearchParams()
+    if (options.reset_sessions) params.append('reset_sessions', 'true')
+    if (options.reset_memory) params.append('reset_memory', 'true')
+    if (options.reset_global_config) params.append('reset_global_config', 'true')
+    const query = params.toString() ? `?${params.toString()}` : ''
+    return api.post<{ status: string; message: string }>(`/config/reset${query}`)
+  },
+  getAgentInfo: async () => {
+    return api.get<AgentInfo>('/config/agent/info')
+  },
+}

+ 49 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/index.ts

@@ -0,0 +1,49 @@
+import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
+
+// 创建 axios 实例
+const instance = axios.create({
+  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
+  timeout: 30000,
+  headers: {
+    'Content-Type': 'application/json',
+  },
+})
+
+// 请求拦截器
+instance.interceptors.request.use(
+  (config) => {
+    return config
+  },
+  (error) => {
+    return Promise.reject(error)
+  },
+)
+
+// 响应拦截器
+instance.interceptors.response.use(
+  (response) => {
+    return response.data
+  },
+  (error) => {
+    console.error('API Error:', error)
+    return Promise.reject(error)
+  },
+)
+
+// 包装 API 调用以获得正确的类型
+const api = {
+  get: <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
+    return instance.get(url, config)
+  },
+  post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => {
+    return instance.post(url, data, config)
+  },
+  put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> => {
+    return instance.put(url, data, config)
+  },
+  delete: <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
+    return instance.delete(url, config)
+  },
+}
+
+export default api

+ 22 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/memory.ts

@@ -0,0 +1,22 @@
+import api from './index'
+
+export interface MemoryEntry {
+  date: string
+  filename: string
+  content: string
+  preview: string
+}
+
+export interface MemoryListResponse {
+  memories: MemoryEntry[]
+  total: number
+}
+
+export const memoryApi = {
+  list: async () => {
+    return api.get<MemoryListResponse>('/memory/list')
+  },
+  get: async (filename: string) => {
+    return api.get<{ filename: string; date: string; content: string }>(`/memory/${filename}`)
+  },
+}

+ 49 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/api/session.ts

@@ -0,0 +1,49 @@
+import api from './index'
+
+export interface Session {
+  id: string
+  created_at: number
+  updated_at: number
+}
+
+// OpenAI 标准消息格式
+export interface ToolCallFunction {
+  name: string
+  arguments: string  // JSON 字符串
+}
+
+export interface ToolCall {
+  id: string
+  type: 'function'
+  function: ToolCallFunction
+}
+
+export interface ChatMessage {
+  role: 'user' | 'assistant' | 'tool'
+  content?: string
+  tool_calls?: ToolCall[]
+  tool_call_id?: string
+}
+
+export interface SessionHistory {
+  session_id: string
+  messages: ChatMessage[]
+}
+
+export const sessionApi = {
+  list: async () => {
+    return api.get<{ sessions: Session[] }>('/session/list')
+  },
+  create: async () => {
+    return api.post<{ session_id: string }>('/session/create')
+  },
+  get: async (id: string) => {
+    return api.get<Session>(`/session/${id}`)
+  },
+  delete: async (id: string) => {
+    return api.delete(`/session/${id}`)
+  },
+  getHistory: async (id: string) => {
+    return api.get<SessionHistory>(`/session/${id}/history`)
+  },
+}

+ 100 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/assets/base.css

@@ -0,0 +1,100 @@
+/* HelloClaw 基础样式 - 龙虾红主题 */
+
+:root {
+  /* 主题色 - 龙虾红 */
+  --color-primary: #ff5c5c;
+  --color-primary-hover: #ff7070;
+  --color-primary-light: rgba(255, 92, 92, 0.15);
+  --color-primary-glow: rgba(255, 92, 92, 0.25);
+
+  /* 背景色 */
+  --color-background: #f5f5f5;
+  --color-surface: #ffffff;
+
+  /* 文字色 */
+  --color-text: #333333;
+  --color-text-secondary: #999999;
+
+  /* 边框色 */
+  --color-border: #f0f0f0;
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+}
+
+html, body {
+  height: 100%;
+  width: 100%;
+}
+
+body {
+  color: var(--color-text);
+  background: var(--color-background);
+  line-height: 1.6;
+  font-family:
+    -apple-system,
+    BlinkMacSystemFont,
+    'Segoe UI',
+    Roboto,
+    'Helvetica Neue',
+    Arial,
+    sans-serif;
+  font-size: 14px;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* 覆盖 Ant Design 主题色 */
+.ant-btn-primary {
+  background-color: var(--color-primary) !important;
+  border-color: var(--color-primary) !important;
+}
+
+.ant-btn-primary:hover {
+  background-color: var(--color-primary-hover) !important;
+  border-color: var(--color-primary-hover) !important;
+}
+
+.ant-menu-item-selected {
+  background-color: var(--color-primary-light) !important;
+  color: var(--color-primary) !important;
+}
+
+.ant-menu-item-selected::after {
+  border-color: var(--color-primary) !important;
+}
+
+.ant-tag-blue {
+  background-color: var(--color-primary-light) !important;
+  border-color: var(--color-primary) !important;
+  color: var(--color-primary) !important;
+}
+
+.ant-input:focus,
+.ant-input-focused {
+  border-color: var(--color-primary) !important;
+  box-shadow: 0 0 0 2px var(--color-primary-glow) !important;
+}
+
+.ant-input-number:focus,
+.ant-input-number-focused {
+  border-color: var(--color-primary) !important;
+  box-shadow: 0 0 0 2px var(--color-primary-glow) !important;
+}
+
+.ant-tabs-tab-active .ant-tabs-tab-btn {
+  color: var(--color-primary) !important;
+}
+
+.ant-tabs-ink-bar {
+  background: var(--color-primary) !important;
+}
+
+.ant-switch-checked {
+  background-color: var(--color-primary) !important;
+}

+ 24 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/assets/lobster.svg

@@ -0,0 +1,24 @@
+<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
+      <stop offset="0%" stop-color="#ff6b5b"/>
+      <stop offset="100%" stop-color="#d63031"/>
+    </linearGradient>
+  </defs>
+  <!-- Body -->
+  <path d="M60 15 C35 15 20 38 20 55 C20 72 32 90 48 95 L48 105 L56 105 L56 95 C56 95 60 97 64 95 L64 105 L72 105 L72 95 C88 90 100 72 100 55 C100 38 85 15 60 15Z" fill="url(#lobster-gradient)"/>
+  <!-- Left Claw -->
+  <path d="M25 42 C12 38 8 48 12 58 C16 68 26 63 30 54 C33 47 30 43 25 42Z" fill="url(#lobster-gradient)"/>
+  <!-- Right Claw -->
+  <path d="M95 42 C108 38 112 48 108 58 C104 68 94 63 90 54 C87 47 90 43 95 42Z" fill="url(#lobster-gradient)"/>
+  <!-- Antenna -->
+  <path d="M48 18 Q40 8 35 12" stroke="#ff6b5b" stroke-width="3" stroke-linecap="round"/>
+  <path d="M72 18 Q80 8 85 12" stroke="#ff6b5b" stroke-width="3" stroke-linecap="round"/>
+  <!-- Eyes -->
+  <circle cx="48" cy="38" r="7" fill="#1a1a2e"/>
+  <circle cx="72" cy="38" r="7" fill="#1a1a2e"/>
+  <circle cx="49" cy="37" r="3" fill="#fff"/>
+  <circle cx="73" cy="37" r="3" fill="#fff"/>
+  <!-- Smile -->
+  <path d="M52 52 Q60 58 68 52" stroke="#1a1a2e" stroke-width="2" stroke-linecap="round" fill="none"/>
+</svg>

+ 24 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/assets/logo.svg

@@ -0,0 +1,24 @@
+<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
+      <stop offset="0%" stop-color="#ff6b5b"/>
+      <stop offset="100%" stop-color="#d63031"/>
+    </linearGradient>
+  </defs>
+  <!-- Body -->
+  <path d="M60 15 C35 15 20 38 20 55 C20 72 32 90 48 95 L48 105 L56 105 L56 95 C56 95 60 97 64 95 L64 105 L72 105 L72 95 C88 90 100 72 100 55 C100 38 85 15 60 15Z" fill="url(#lobster-gradient)"/>
+  <!-- Left Claw -->
+  <path d="M25 42 C12 38 8 48 12 58 C16 68 26 63 30 54 C33 47 30 43 25 42Z" fill="url(#lobster-gradient)"/>
+  <!-- Right Claw -->
+  <path d="M95 42 C108 38 112 48 108 58 C104 68 94 63 90 54 C87 47 90 43 95 42Z" fill="url(#lobster-gradient)"/>
+  <!-- Antenna -->
+  <path d="M48 18 Q40 8 35 12" stroke="#ff6b5b" stroke-width="3" stroke-linecap="round"/>
+  <path d="M72 18 Q80 8 85 12" stroke="#ff6b5b" stroke-width="3" stroke-linecap="round"/>
+  <!-- Eyes -->
+  <circle cx="48" cy="38" r="7" fill="#1a1a2e"/>
+  <circle cx="72" cy="38" r="7" fill="#1a1a2e"/>
+  <circle cx="49" cy="37" r="3" fill="#fff"/>
+  <circle cx="73" cy="37" r="3" fill="#fff"/>
+  <!-- Smile -->
+  <path d="M52 52 Q60 58 68 52" stroke="#1a1a2e" stroke-width="2" stroke-linecap="round" fill="none"/>
+</svg>

+ 23 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/assets/main.css

@@ -0,0 +1,23 @@
+/* HelloClaw 全局样式 */
+@import './base.css';
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html, body {
+  height: 100%;
+  width: 100%;
+}
+
+#app {
+  height: 100%;
+  width: 100%;
+}
+
+a {
+  text-decoration: none;
+  color: inherit;
+}

+ 411 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/ChatMessage.vue

@@ -0,0 +1,411 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { Tag } from 'ant-design-vue'
+import { LoadingOutlined } from '@ant-design/icons-vue'
+import { renderMarkdown, formatTime } from '@/utils/markdown'
+import { getToolConfig, formatToolArgs, formatToolResult } from '@/utils/toolDisplay'
+import LobsterIcon from '@/assets/lobster.svg'
+
+// 消息段类型
+interface TextSegment {
+  type: 'text'
+  id: number
+  content: string
+}
+
+interface ToolSegment {
+  type: 'tool'
+  id: number
+  tool: string
+  args: Record<string, unknown>
+  result?: string
+  status: 'running' | 'done' | 'error'
+}
+
+type MessageSegment = TextSegment | ToolSegment
+
+interface Message {
+  id: number
+  role: 'user' | 'assistant'
+  content: string
+  timestamp: Date
+  segments?: MessageSegment[]
+}
+
+interface Props {
+  message: Message
+  assistantName?: string
+  expandedTools?: Set<number>
+}
+
+interface Emits {
+  (e: 'toggle-tool', toolId: number): void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  assistantName: 'HelloClaw',
+  expandedTools: () => new Set()
+})
+
+const emit = defineEmits<Emits>()
+
+// 切换工具折叠状态
+const toggleToolCollapse = (toolId: number) => {
+  emit('toggle-tool', toolId)
+}
+
+// 检查工具是否展开
+const isToolExpanded = (toolId: number): boolean => {
+  return props.expandedTools.has(toolId)
+}
+
+// 检查是否有可见内容
+const hasVisibleContent = computed(() => {
+  if (!props.message.segments || props.message.segments.length === 0) {
+    return !!props.message.content
+  }
+
+  for (const segment of props.message.segments) {
+    if (segment.type === 'text' && segment.content) {
+      return true
+    }
+    if (segment.type === 'tool' && !getToolConfig(segment.tool).hidden) {
+      return true
+    }
+  }
+  return false
+})
+</script>
+
+<template>
+  <div v-if="hasVisibleContent" :class="['chat-message', message.role]">
+    <!-- 头像 -->
+    <div class="message-avatar">
+      <img v-if="message.role === 'assistant'" :src="LobsterIcon" alt="HelloClaw" />
+      <div v-else class="user-avatar">你</div>
+    </div>
+
+    <!-- 消息内容 -->
+    <div class="message-content">
+      <!-- 如果有分段,按分段显示 -->
+      <template v-if="message.segments && message.segments.length > 0">
+        <template v-for="segment in message.segments" :key="segment.id">
+          <!-- 文本段 -->
+          <div v-if="segment.type === 'text' && segment.content" class="message-bubble">
+            <div
+              class="message-text"
+              v-html="renderMarkdown(segment.content)"
+            ></div>
+          </div>
+          <!-- 工具调用段 - 只显示非隐藏的工具 -->
+          <div
+            v-if="segment.type === 'tool' && !getToolConfig(segment.tool).hidden"
+            :class="['tool-card', segment.status]"
+          >
+            <div
+              class="tool-header"
+              @click="segment.status !== 'running' && toggleToolCollapse(segment.id)"
+            >
+              <span class="tool-icon">{{ getToolConfig(segment.tool).icon }}</span>
+              <span class="tool-name">
+                <template v-if="!isToolExpanded(segment.id)">使用了</template>
+                {{ getToolConfig(segment.tool).name }}
+              </span>
+              <Tag v-if="segment.status === 'running'" color="processing" class="tool-tag">
+                <LoadingOutlined /> 执行中
+              </Tag>
+              <Tag v-else-if="segment.status === 'done'" color="success" class="tool-tag">完成</Tag>
+              <Tag v-else-if="segment.status === 'error'" color="error" class="tool-tag">失败</Tag>
+              <span
+                v-if="segment.status !== 'running'"
+                class="collapse-indicator"
+              >
+                {{ isToolExpanded(segment.id) ? '▼' : '▶' }}
+              </span>
+            </div>
+            <!-- 展开后显示入参和结果 -->
+            <div v-if="isToolExpanded(segment.id)" class="tool-details">
+              <!-- 入参 -->
+              <div v-if="segment.args && Object.keys(segment.args).length > 0" class="tool-args">
+                <div class="tool-detail-label">入参</div>
+                <pre class="tool-detail-content">{{ formatToolArgs(segment.args) }}</pre>
+              </div>
+              <!-- 结果 -->
+              <div v-if="segment.result" class="tool-result-wrapper">
+                <div class="tool-detail-label">结果</div>
+                <pre class="tool-detail-content">{{ formatToolResult(segment.result) }}</pre>
+              </div>
+            </div>
+          </div>
+        </template>
+      </template>
+      <!-- 如果没有分段,显示普通内容(历史消息) -->
+      <div v-else-if="message.content" class="message-bubble">
+        <div
+          class="message-text"
+          v-html="renderMarkdown(message.content)"
+        ></div>
+      </div>
+
+      <!-- 消息元信息 -->
+      <div class="message-meta">
+        <span class="message-sender">
+          {{ message.role === 'user' ? '你' : assistantName }}
+        </span>
+        <span class="message-time">{{ formatTime(message.timestamp) }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.chat-message {
+  display: flex;
+  gap: 12px;
+  max-width: 85%;
+}
+
+.chat-message.user {
+  align-self: flex-end;
+  flex-direction: row-reverse;
+}
+
+.chat-message.assistant {
+  align-self: flex-start;
+}
+
+/* 头像 */
+.message-avatar {
+  flex-shrink: 0;
+  width: 36px;
+  height: 36px;
+}
+
+.message-avatar img {
+  width: 36px;
+  height: 36px;
+  border-radius: 8px;
+}
+
+.user-avatar {
+  width: 36px;
+  height: 36px;
+  border-radius: 8px;
+  background-color: var(--color-primary);
+  color: #fff;
+  font-size: 14px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 消息内容 */
+.message-content {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  min-width: 0;
+}
+
+/* 消息气泡 */
+.message-bubble {
+  display: inline-block;
+  max-width: 100%;
+}
+
+.message-text {
+  padding: 10px 14px;
+  border-radius: 12px;
+  background-color: var(--color-surface);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+  line-height: 1.6;
+  word-wrap: break-word;
+}
+
+.chat-message.user .message-text {
+  background-color: var(--color-primary-light);
+  border: 1px solid rgba(255, 92, 92, 0.2);
+}
+
+/* Markdown 样式 */
+.message-text :deep(p) {
+  margin: 0;
+}
+
+.message-text :deep(p + p) {
+  margin-top: 8px;
+}
+
+.message-text :deep(code) {
+  background-color: rgba(0, 0, 0, 0.05);
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-size: 13px;
+}
+
+.message-text :deep(pre) {
+  background-color: #1e1e1e;
+  color: #d4d4d4;
+  padding: 12px;
+  border-radius: 8px;
+  overflow-x: auto;
+  margin: 8px 0;
+}
+
+.message-text :deep(pre code) {
+  background-color: transparent;
+  padding: 0;
+}
+
+.message-text :deep(ul),
+.message-text :deep(ol) {
+  margin: 8px 0;
+  padding-left: 20px;
+}
+
+.message-text :deep(blockquote) {
+  border-left: 3px solid var(--color-primary);
+  padding-left: 12px;
+  margin: 8px 0;
+  color: var(--color-text-secondary);
+}
+
+.message-text :deep(a) {
+  color: var(--color-primary);
+  text-decoration: underline;
+}
+
+/* 消息元信息 */
+.message-meta {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  margin-top: 4px;
+  padding-left: 4px;
+}
+
+.message-sender {
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--color-text);
+}
+
+.message-time {
+  font-size: 11px;
+  color: var(--color-text-secondary);
+}
+
+/* 工具调用卡片 */
+.tool-card {
+  background: var(--color-surface);
+  border: 1px solid var(--color-border);
+  border-radius: 8px;
+  padding: 8px 12px;
+  font-size: 13px;
+  transition: all 0.2s ease;
+  margin-top: 8px;
+}
+
+/* 执行中状态 - 龙虾红主题 */
+.tool-card.running {
+  border-color: var(--color-primary);
+  background: var(--color-primary-light);
+}
+
+.tool-card.running .tool-icon,
+.tool-card.running .tool-name {
+  color: var(--color-primary);
+}
+
+/* 完成状态 - 灰色调 */
+.tool-card.done {
+  border-color: var(--color-border);
+  background: var(--color-surface);
+}
+
+/* 失败状态 - 红色调 */
+.tool-card.error {
+  border-color: var(--color-primary);
+  background: #fff1f0;
+}
+
+.tool-card.error .tool-icon,
+.tool-card.error .tool-name {
+  color: var(--color-primary);
+}
+
+.tool-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  user-select: none;
+}
+
+.tool-header:hover {
+  opacity: 0.8;
+}
+
+.tool-icon {
+  font-size: 14px;
+  line-height: 1;
+}
+
+.tool-name {
+  font-weight: 500;
+  color: var(--color-text);
+  flex: 1;
+}
+
+.tool-tag {
+  font-size: 11px;
+  padding: 0 6px;
+  line-height: 18px;
+  border-radius: 4px;
+}
+
+.collapse-indicator {
+  font-size: 10px;
+  color: var(--color-text-secondary);
+  margin-left: auto;
+  transition: transform 0.2s ease;
+}
+
+/* 工具详情区域 */
+.tool-details {
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px dashed var(--color-border);
+}
+
+.tool-args,
+.tool-result-wrapper {
+  margin-bottom: 8px;
+}
+
+.tool-result-wrapper:last-child {
+  margin-bottom: 0;
+}
+
+.tool-detail-label {
+  font-size: 11px;
+  color: var(--color-text-secondary);
+  margin-bottom: 4px;
+  font-weight: 500;
+}
+
+.tool-detail-content {
+  margin: 0;
+  padding: 8px;
+  background: rgba(0, 0, 0, 0.02);
+  border-radius: 4px;
+  font-size: 12px;
+  color: var(--color-text);
+  max-height: 150px;
+  overflow-y: auto;
+  white-space: pre-wrap;
+  word-break: break-word;
+  font-family: ui-monospace, 'SF Mono', Monaco, 'Andale Mono', monospace;
+}
+</style>

+ 41 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/HelloWorld.vue

@@ -0,0 +1,41 @@
+<script setup lang="ts">
+defineProps<{
+  msg: string
+}>()
+</script>
+
+<template>
+  <div class="greetings">
+    <h1 class="green">{{ msg }}</h1>
+    <h3>
+      You’ve successfully created a project with
+      <a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
+      <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
+    </h3>
+  </div>
+</template>
+
+<style scoped>
+h1 {
+  font-weight: 500;
+  font-size: 2.6rem;
+  position: relative;
+  top: -10px;
+}
+
+h3 {
+  font-size: 1.2rem;
+}
+
+.greetings h1,
+.greetings h3 {
+  text-align: center;
+}
+
+@media (min-width: 1024px) {
+  .greetings h1,
+  .greetings h3 {
+    text-align: left;
+  }
+}
+</style>

+ 95 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/TheWelcome.vue

@@ -0,0 +1,95 @@
+<script setup lang="ts">
+import WelcomeItem from './WelcomeItem.vue'
+import DocumentationIcon from './icons/IconDocumentation.vue'
+import ToolingIcon from './icons/IconTooling.vue'
+import EcosystemIcon from './icons/IconEcosystem.vue'
+import CommunityIcon from './icons/IconCommunity.vue'
+import SupportIcon from './icons/IconSupport.vue'
+
+const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
+</script>
+
+<template>
+  <WelcomeItem>
+    <template #icon>
+      <DocumentationIcon />
+    </template>
+    <template #heading>Documentation</template>
+
+    Vue’s
+    <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
+    provides you with all information you need to get started.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <ToolingIcon />
+    </template>
+    <template #heading>Tooling</template>
+
+    This project is served and bundled with
+    <a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
+    recommended IDE setup is
+    <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+    +
+    <a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
+      >Vue - Official</a
+    >. If you need to test your components and web pages, check out
+    <a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
+    and
+    <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
+    /
+    <a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
+
+    <br />
+
+    More instructions are available in
+    <a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
+    >.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <EcosystemIcon />
+    </template>
+    <template #heading>Ecosystem</template>
+
+    Get official tools and libraries for your project:
+    <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
+    <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
+    <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
+    <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
+    you need more resources, we suggest paying
+    <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
+    a visit.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <CommunityIcon />
+    </template>
+    <template #heading>Community</template>
+
+    Got stuck? Ask your question on
+    <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
+    (our official Discord server), or
+    <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
+      >StackOverflow</a
+    >. You should also follow the official
+    <a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
+    Bluesky account or the
+    <a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
+    X account for latest news in the Vue world.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <SupportIcon />
+    </template>
+    <template #heading>Support Vue</template>
+
+    As an independent project, Vue relies on community backing for its sustainability. You can help
+    us by
+    <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
+  </WelcomeItem>
+</template>

+ 87 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/WelcomeItem.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="item">
+    <i>
+      <slot name="icon"></slot>
+    </i>
+    <div class="details">
+      <h3>
+        <slot name="heading"></slot>
+      </h3>
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.item {
+  margin-top: 2rem;
+  display: flex;
+  position: relative;
+}
+
+.details {
+  flex: 1;
+  margin-left: 1rem;
+}
+
+i {
+  display: flex;
+  place-items: center;
+  place-content: center;
+  width: 32px;
+  height: 32px;
+
+  color: var(--color-text);
+}
+
+h3 {
+  font-size: 1.2rem;
+  font-weight: 500;
+  margin-bottom: 0.4rem;
+  color: var(--color-heading);
+}
+
+@media (min-width: 1024px) {
+  .item {
+    margin-top: 0;
+    padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
+  }
+
+  i {
+    top: calc(50% - 25px);
+    left: -26px;
+    position: absolute;
+    border: 1px solid var(--color-border);
+    background: var(--color-background);
+    border-radius: 8px;
+    width: 50px;
+    height: 50px;
+  }
+
+  .item:before {
+    content: ' ';
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    bottom: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:after {
+    content: ' ';
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    top: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:first-of-type:before {
+    display: none;
+  }
+
+  .item:last-of-type:after {
+    display: none;
+  }
+}
+</style>

+ 7 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconCommunity.vue

@@ -0,0 +1,7 @@
+<template>
+  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
+    <path
+      d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
+    />
+  </svg>
+</template>

+ 7 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconDocumentation.vue

@@ -0,0 +1,7 @@
+<template>
+  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
+    <path
+      d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
+    />
+  </svg>
+</template>

+ 7 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconEcosystem.vue

@@ -0,0 +1,7 @@
+<template>
+  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
+    <path
+      d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
+    />
+  </svg>
+</template>

+ 7 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconSupport.vue

@@ -0,0 +1,7 @@
+<template>
+  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
+    <path
+      d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
+    />
+  </svg>
+</template>

+ 19 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/components/icons/IconTooling.vue

@@ -0,0 +1,19 @@
+<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    aria-hidden="true"
+    role="img"
+    class="iconify iconify--mdi"
+    width="24"
+    height="24"
+    preserveAspectRatio="xMidYMid meet"
+    viewBox="0 0 24 24"
+  >
+    <path
+      d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
+      fill="currentColor"
+    ></path>
+  </svg>
+</template>

+ 17 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/main.ts

@@ -0,0 +1,17 @@
+import './assets/main.css'
+
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import Antd from 'ant-design-vue'
+import 'ant-design-vue/dist/reset.css'
+
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+app.use(Antd)
+
+app.mount('#app')

+ 29 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/router/index.ts

@@ -0,0 +1,29 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/',
+      name: 'chat',
+      component: () => import('../views/ChatView.vue'),
+    },
+    {
+      path: '/sessions',
+      name: 'sessions',
+      component: () => import('../views/SessionsView.vue'),
+    },
+    {
+      path: '/memory',
+      name: 'memory',
+      component: () => import('../views/MemoryView.vue'),
+    },
+    {
+      path: '/config',
+      name: 'config',
+      component: () => import('../views/ConfigView.vue'),
+    },
+  ],
+})
+
+export default router

+ 12 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/stores/counter.ts

@@ -0,0 +1,12 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useCounterStore = defineStore('counter', () => {
+  const count = ref(0)
+  const doubleCount = computed(() => count.value * 2)
+  function increment() {
+    count.value++
+  }
+
+  return { count, doubleCount, increment }
+})

+ 47 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/utils/markdown.ts

@@ -0,0 +1,47 @@
+import { marked } from 'marked'
+import DOMPurify from 'dompurify'
+
+// 配置 marked
+marked.setOptions({
+  breaks: true, // 换行符转换
+  gfm: true, // GitHub Flavored Markdown
+})
+
+// 允许的标签
+const allowedTags = [
+  'a', 'b', 'blockquote', 'br', 'code', 'del', 'em',
+  'h1', 'h2', 'h3', 'h4', 'hr', 'i', 'li', 'ol', 'p',
+  'pre', 'strong', 'table', 'tbody', 'td', 'th',
+  'thead', 'tr', 'ul', 'img'
+]
+
+// 允许的属性
+const allowedAttrs = ['class', 'href', 'rel', 'target', 'title', 'src', 'alt']
+
+/**
+ * 渲染 Markdown 为安全的 HTML
+ */
+export function renderMarkdown(text: string): string {
+  if (!text) return ''
+
+  // 解析 Markdown
+  const html = marked.parse(text) as string
+
+  // 清理 HTML,防止 XSS
+  const clean = DOMPurify.sanitize(html, {
+    ALLOWED_TAGS: allowedTags,
+    ALLOWED_ATTR: allowedAttrs,
+  })
+
+  return clean
+}
+
+/**
+ * 格式化时间戳
+ */
+export function formatTime(date: Date): string {
+  return date.toLocaleTimeString('zh-CN', {
+    hour: '2-digit',
+    minute: '2-digit',
+  })
+}

+ 88 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/utils/toolDisplay.ts

@@ -0,0 +1,88 @@
+// 工具显示配置
+export interface ToolDisplayConfig {
+  name: string         // 友好名称
+  icon: string         // emoji 图标
+  hidden?: boolean     // 是否隐藏
+}
+
+export const TOOL_DISPLAY_CONFIG: Record<string, ToolDisplayConfig> = {
+  // 内置工具 - 隐藏
+  Thought: { name: '思考', icon: '💭', hidden: true },
+  Finish: { name: '完成', icon: '✅', hidden: true },
+
+  // 文件操作工具(HelloAgents 内置)
+  Read: { name: '读取文件', icon: '📄' },
+  Write: { name: '写入文件', icon: '✏️' },
+  Edit: { name: '编辑文件', icon: '📝' },
+  MultiEdit: { name: '批量编辑', icon: '📝' },
+
+  // 计算工具
+  python_calculator: { name: '计算器', icon: '🔢' },
+
+  // 记忆工具(HelloClaw 自定义)
+  memory: { name: '记忆操作', icon: '🧠' },
+  memory_search: { name: '搜索记忆', icon: '🔍' },
+  memory_get: { name: '读取记忆', icon: '📖' },
+  memory_add: { name: '添加记忆', icon: '📝' },
+  memory_update_longterm: { name: '更新长期记忆', icon: '📚' },
+  memory_list: { name: '列出记忆文件', icon: '📋' },
+  memory_cleanup: { name: '清理过期记忆', icon: '🧹' },
+
+  // 任务工具
+  Task: { name: '子任务', icon: '📋' },
+
+  // 命令执行工具
+  execute_command: { name: '执行命令', icon: '💻' },
+  exec_run: { name: '执行命令', icon: '💻' },
+  exec_allowed_commands: { name: '查看允许的命令', icon: '📋' },
+  exec_dangerous_patterns: { name: '查看危险命令', icon: '⚠️' },
+
+  // 网络工具
+  web_search: { name: '网络搜索', icon: '🌐' },
+  search_web: { name: '网络搜索', icon: '🌐' },
+  web_fetch: { name: '获取网页', icon: '📡' },
+  fetch_url: { name: '获取网页', icon: '📡' },
+}
+
+// 默认配置(未知工具)
+export const DEFAULT_TOOL_CONFIG: ToolDisplayConfig = {
+  name: '工具',
+  icon: '🔧',
+}
+
+// 获取工具显示配置
+export function getToolConfig(toolName: string): ToolDisplayConfig {
+  return TOOL_DISPLAY_CONFIG[toolName] || DEFAULT_TOOL_CONFIG
+}
+
+// 格式化工具参数显示
+export function formatToolArgs(args: Record<string, unknown>): string {
+  if (!args || Object.keys(args).length === 0) {
+    return ''
+  }
+
+  const parts: string[] = []
+  for (const [key, value] of Object.entries(args)) {
+    let displayValue: string
+    if (typeof value === 'string') {
+      // 截断长字符串
+      displayValue = value.length > 100 ? value.slice(0, 100) + '...' : value
+    } else if (typeof value === 'object') {
+      displayValue = JSON.stringify(value)
+      if (displayValue.length > 100) {
+        displayValue = displayValue.slice(0, 100) + '...'
+      }
+    } else {
+      displayValue = String(value)
+    }
+    parts.push(`${key}: ${displayValue}`)
+  }
+  return parts.join('\n')
+}
+
+// 格式化工具结果显示
+export function formatToolResult(result: string | undefined): string {
+  if (!result) return ''
+  // 截断长结果
+  return result.length > 500 ? result.slice(0, 500) + '...' : result
+}

+ 15 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/AboutView.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="about">
+    <h1>This is an about page</h1>
+  </div>
+</template>
+
+<style>
+@media (min-width: 1024px) {
+  .about {
+    min-height: 100vh;
+    display: flex;
+    align-items: center;
+  }
+}
+</style>

+ 1260 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/ChatView.vue

@@ -0,0 +1,1260 @@
+<script setup lang="ts">
+import { ref, watch, computed, nextTick, onMounted } from 'vue'
+import { Input, Button, message, Tag } from 'ant-design-vue'
+import { SendOutlined, PlusOutlined, StopOutlined, LoadingOutlined } from '@ant-design/icons-vue'
+import { useRouter, useRoute } from 'vue-router'
+import { sessionApi } from '@/api/session'
+import { chatApi } from '@/api/chat'
+import { configApi } from '@/api/config'
+import { renderMarkdown, formatTime } from '@/utils/markdown'
+import { getToolConfig, formatToolArgs, formatToolResult } from '@/utils/toolDisplay'
+import LobsterIcon from '@/assets/lobster.svg'
+
+// localStorage key for saving current session
+const SESSION_STORAGE_KEY = 'helloclaw.lastSessionId'
+
+// 助手名字(从后端获取)
+const assistantName = ref('HelloClaw')
+
+// 消息段类型
+interface TextSegment {
+  type: 'text'
+  id: number
+  content: string
+}
+
+interface ToolSegment {
+  type: 'tool'
+  id: number
+  tool: string
+  args: Record<string, unknown>
+  result?: string
+  status: 'running' | 'done' | 'error'
+}
+
+type MessageSegment = TextSegment | ToolSegment
+
+interface Message {
+  id: number
+  role: 'user' | 'assistant'
+  content: string  // 用于从历史加载的消息
+  timestamp: Date
+  segments?: MessageSegment[]  // 用于流式消息的分段
+}
+
+interface MessageGroup {
+  role: 'user' | 'assistant'
+  messages: Message[]
+}
+
+const router = useRouter()
+const route = useRoute()
+const inputMessage = ref('')
+const messages = ref<Message[]>([])
+const loading = ref(false)
+const currentSessionId = ref<string | null>(null)
+const messagesContainer = ref<HTMLElement | null>(null)
+const abortController = ref<AbortController | null>(null)
+const initializing = ref(true)
+const collapsedTools = ref<Set<number>>(new Set())
+// 默认所有工具都是展开的(用于新建的工具)
+const expandedTools = ref<Set<number>>(new Set())
+
+// 消息分组(Slack 风格)
+const messageGroups = computed<MessageGroup[]>(() => {
+  const groups: MessageGroup[] = []
+
+  for (const msg of messages.value) {
+    const lastGroup = groups[groups.length - 1]
+
+    if (lastGroup && lastGroup.role === msg.role) {
+      lastGroup.messages.push(msg)
+    } else {
+      groups.push({
+        role: msg.role,
+        messages: [msg]
+      })
+    }
+  }
+
+  return groups
+})
+
+// 是否应该显示加载指示器(底部的独立指示器)
+// 只有当助手消息组完全没有可见内容时才显示
+const shouldShowLoadingIndicator = computed(() => {
+  if (messages.value.length === 0) {
+    return true
+  }
+
+  const lastMsg = messages.value[messages.value.length - 1]
+  if (lastMsg?.role !== 'assistant') {
+    return true
+  }
+
+  // 只有当完全没有可见内容时才显示底部指示器
+  // 如果有工具卡片等可见内容,等待状态会在消息组内部显示
+  return !hasVisibleContent(lastMsg)
+})
+
+// 检查消息组是否有可见内容
+const hasGroupVisibleContent = (group: MessageGroup): boolean => {
+  for (const msg of group.messages) {
+    if (hasVisibleContent(msg)) {
+      return true
+    }
+  }
+  return false
+}
+
+// 检查消息组是否有文本内容
+const hasGroupTextContent = (group: MessageGroup): boolean => {
+  for (const msg of group.messages) {
+    if (hasTextContent(msg)) {
+      return true
+    }
+  }
+  return false
+}
+
+// 检查消息组是否有正在执行或已完成的工具(但还没有文本回复)
+const hasGroupToolWithoutText = (group: MessageGroup): boolean => {
+  if (group.role !== 'assistant') return false
+  let hasTool = false
+  let hasText = false
+  for (const msg of group.messages) {
+    if (!msg.segments) continue
+    for (const segment of msg.segments) {
+      if (segment.type === 'tool' && !getToolConfig(segment.tool).hidden) {
+        hasTool = true
+      }
+      if (segment.type === 'text' && segment.content && segment.content.trim()) {
+        hasText = true
+      }
+    }
+  }
+  return hasTool && !hasText
+}
+
+// 保存当前会话 ID 到 localStorage
+const saveCurrentSession = (sessionId: string) => {
+  localStorage.setItem(SESSION_STORAGE_KEY, sessionId)
+}
+
+// 从 localStorage 读取上次会话 ID
+const getLastSession = (): string | null => {
+  return localStorage.getItem(SESSION_STORAGE_KEY)
+}
+
+// 加载会话历史(按照 OpenAI 标准格式解析)
+const loadSessionHistory = async (sessionId: string) => {
+  try {
+    const res = await sessionApi.getHistory(sessionId)
+    const rawMessages = res.messages
+
+    // 用于存储工具调用结果(tool_call_id -> result)
+    const toolResults: Map<string, string> = new Map()
+
+    // 第一遍:收集所有 tool 消息的结果
+    for (const msg of rawMessages) {
+      if (msg.role === 'tool' && msg.tool_call_id && msg.content) {
+        toolResults.set(msg.tool_call_id, msg.content)
+      }
+    }
+
+    // 第二遍:构建显示消息
+    const displayMessages: Message[] = []
+    let pendingAssistant: Message | null = null
+
+    for (let i = 0; i < rawMessages.length; i++) {
+      const msg = rawMessages[i]!
+
+      if (msg.role === 'user') {
+        // 如果有待处理的 assistant 消息,先添加
+        if (pendingAssistant) {
+          displayMessages.push(pendingAssistant)
+          pendingAssistant = null
+        }
+        // 添加 user 消息
+        displayMessages.push({
+          id: Date.now() + i,
+          role: 'user',
+          content: msg.content || '',
+          timestamp: new Date()
+        })
+      }
+      else if (msg.role === 'assistant') {
+        if (msg.tool_calls && msg.tool_calls.length > 0) {
+          // 包含工具调用的 assistant 消息
+          const segments: MessageSegment[] = []
+
+          // 添加工具调用段
+          msg.tool_calls.forEach((tc, tcIndex) => {
+            const result = toolResults.get(tc.id)
+            segments.push({
+              type: 'tool',
+              id: Date.now() + i * 1000 + tcIndex,
+              tool: tc.function.name,
+              args: JSON.parse(tc.function.arguments || '{}'),
+              result: result,
+              status: result?.startsWith('❌') ? 'error' : 'done'
+            })
+          })
+
+          // 检查下一个消息是否是最终的 assistant 回答(没有 tool_calls)
+          const nextMsg = rawMessages[i + 1]
+          if (nextMsg && nextMsg.role === 'assistant' && !nextMsg.tool_calls && nextMsg.content) {
+            // 有最终回答,添加文本段
+            segments.push({
+              type: 'text',
+              id: Date.now() + i * 1000 + 100,
+              content: nextMsg.content
+            })
+            i++ // 跳过下一个消息
+          }
+
+          pendingAssistant = {
+            id: Date.now() + i,
+            role: 'assistant',
+            content: '',
+            timestamp: new Date(),
+            segments
+          }
+        } else if (msg.content) {
+          // 普通的 assistant 文本消息
+          if (pendingAssistant) {
+            // 追加到待处理的 assistant 消息
+            if (!pendingAssistant.segments) {
+              pendingAssistant.segments = []
+            }
+            pendingAssistant.segments.push({
+              type: 'text',
+              id: Date.now() + i,
+              content: msg.content
+            })
+          } else {
+            // 新的 assistant 消息
+            displayMessages.push({
+              id: Date.now() + i,
+              role: 'assistant',
+              content: msg.content,
+              timestamp: new Date()
+            })
+          }
+        }
+      }
+      // tool 消息在第一遍已经处理,跳过
+    }
+
+    // 添加最后的待处理消息
+    if (pendingAssistant) {
+      displayMessages.push(pendingAssistant)
+    }
+
+    messages.value = displayMessages
+    await scrollToBottom()
+  } catch (error) {
+    // 会话不存在或加载失败,清空消息
+    messages.value = []
+  }
+}
+
+// 初始化会话
+const initSession = async () => {
+  // 获取助手名字
+  try {
+    const agentInfo = await configApi.getAgentInfo()
+    if (agentInfo.name) {
+      assistantName.value = agentInfo.name
+    }
+  } catch (error) {
+    // 获取失败时使用默认名字
+    console.warn('获取助手名字失败:', error)
+  }
+
+  const urlSession = route.query.session as string | undefined
+
+  if (urlSession) {
+    // URL 中有 session 参数,使用它
+    currentSessionId.value = urlSession
+    saveCurrentSession(urlSession)
+    await loadSessionHistory(urlSession)
+    initializing.value = false
+  } else {
+    // URL 中没有 session 参数,尝试从 localStorage 读取
+    const lastSession = getLastSession()
+    if (lastSession) {
+      // 有上次会话,设置 session 并加载历史,然后更新 URL
+      currentSessionId.value = lastSession
+      saveCurrentSession(lastSession)
+      await loadSessionHistory(lastSession)
+      // 使用 replace 更新 URL(不触发导航)
+      window.history.replaceState({}, '', `/?session=${lastSession}`)
+      initializing.value = false
+    } else {
+      // 没有上次会话,创建新会话
+      try {
+        const res = await sessionApi.create()
+        saveCurrentSession(res.session_id)
+        currentSessionId.value = res.session_id
+        // 使用 replace 更新 URL(不触发导航)
+        window.history.replaceState({}, '', `/?session=${res.session_id}`)
+        initializing.value = false
+      } catch (error) {
+        message.error('创建会话失败')
+        initializing.value = false
+      }
+    }
+  }
+}
+
+// 监听 session 参数变化(处理从其他地方跳转过来的情况)
+watch(
+  () => route.query.session,
+  async (newSession, oldSession) => {
+    // 如果正在初始化,跳过
+    if (initializing.value) return
+
+    // 如果 session 没有实际变化,跳过
+    if (newSession === oldSession) return
+
+    const sessionId = (newSession as string) || null
+
+    // 如果新 session 为空,不做处理(应该由 initSession 处理)
+    if (!sessionId) return
+
+    // 切换到新会话
+    currentSessionId.value = sessionId
+    saveCurrentSession(sessionId)
+    inputMessage.value = ''
+    await loadSessionHistory(sessionId)
+  }
+)
+
+// 监听 refresh 参数变化(处理从配置页面初始化后跳转的情况)
+watch(
+  () => route.query.refresh,
+  async (newRefresh) => {
+    if (newRefresh) {
+      // 重新获取助手名字
+      try {
+        const agentInfo = await configApi.getAgentInfo()
+        if (agentInfo.name) {
+          assistantName.value = agentInfo.name
+        }
+      } catch (error) {
+        console.warn('获取助手名字失败:', error)
+      }
+
+      // 清除 URL 中的 refresh 参数
+      const currentQuery = { ...route.query }
+      delete currentQuery.refresh
+      router.replace({ query: currentQuery })
+    }
+  }
+)
+
+// 组件挂载时初始化会话
+onMounted(async () => {
+  await initSession()
+})
+
+// 滚动到底部
+const scrollToBottom = async () => {
+  await nextTick()
+  if (messagesContainer.value) {
+    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
+  }
+}
+
+// 切换工具折叠状态
+const toggleToolCollapse = (toolId: number) => {
+  if (expandedTools.value.has(toolId)) {
+    expandedTools.value.delete(toolId)
+  } else {
+    expandedTools.value.add(toolId)
+  }
+}
+
+// 检查工具是否展开(默认折叠,只有点击后才展开)
+const isToolExpanded = (toolId: number): boolean => {
+  return expandedTools.value.has(toolId)
+}
+
+// 检查消息是否有可见内容
+const hasVisibleContent = (msg: Message): boolean => {
+  if (!msg.segments || msg.segments.length === 0) {
+    // 没有分段,检查普通内容
+    return !!msg.content
+  }
+
+  // 有分段,检查是否有可见的段
+  for (const segment of msg.segments) {
+    if (segment.type === 'text' && segment.content) {
+      return true
+    }
+    if (segment.type === 'tool' && !getToolConfig(segment.tool).hidden) {
+      return true
+    }
+  }
+  return false
+}
+
+// 检查消息是否有文本内容(用于决定是否显示加载指示器)
+const hasTextContent = (msg: Message): boolean => {
+  if (!msg.segments || msg.segments.length === 0) {
+    return !!msg.content
+  }
+  // 只检查文本段
+  for (const segment of msg.segments) {
+    if (segment.type === 'text' && segment.content) {
+      return true
+    }
+  }
+  return false
+}
+
+// 检查消息是否有可见的工具调用(用于决定是否显示工具卡片而非加载指示器)
+const hasVisibleTools = (msg: Message): boolean => {
+  if (!msg.segments) return false
+  for (const segment of msg.segments) {
+    if (segment.type === 'tool' && !getToolConfig(segment.tool).hidden) {
+      return true
+    }
+  }
+  return false
+}
+
+// 检查消息组是否正在等待响应(用于隐藏 group-footer)
+const isGroupWaiting = (group: MessageGroup): boolean => {
+  if (group.role !== 'assistant' || !loading.value) return false
+  // 检查组内所有消息是否都没有文本内容
+  return group.messages.every(msg => !hasTextContent(msg))
+}
+
+// 停止生成
+const stopGeneration = () => {
+  if (abortController.value) {
+    abortController.value.abort()
+    abortController.value = null
+    loading.value = false
+  }
+}
+
+// 更新消息段(触发 Vue 响应性)
+const updateMessageSegments = (msgIndex: number, segments: MessageSegment[]) => {
+  if (msgIndex >= 0 && msgIndex < messages.value.length) {
+    const existingMsg = messages.value[msgIndex]!
+    messages.value[msgIndex] = {
+      id: existingMsg.id,
+      role: existingMsg.role,
+      content: existingMsg.content,
+      timestamp: existingMsg.timestamp,
+      segments: [...segments]
+    }
+  }
+}
+
+const sendMessage = async () => {
+  if (!inputMessage.value.trim()) return
+
+  const userMessage = inputMessage.value
+  const userMsg: Message = {
+    id: Date.now(),
+    role: 'user',
+    content: userMessage,
+    timestamp: new Date()
+  }
+
+  messages.value.push(userMsg)
+  inputMessage.value = ''
+  loading.value = true
+
+  // 创建 AbortController
+  abortController.value = new AbortController()
+
+  // 助手消息的索引和段
+  let assistantMsgIndex = -1
+  let currentSegments: MessageSegment[] = []
+  let currentTextSegmentId = -1
+
+  await scrollToBottom()
+
+  try {
+    await chatApi.sendMessageStream(
+      userMessage,
+      currentSessionId.value || undefined,
+      (event) => {
+        if (event.type === 'session') {
+          // 收到会话 ID
+          if (event.session_id) {
+            currentSessionId.value = event.session_id
+            saveCurrentSession(event.session_id)
+          }
+        } else if (event.type === 'step_start') {
+          // 新步骤开始 - 创建新的文本段
+          currentTextSegmentId = Date.now()
+          currentSegments.push({
+            type: 'text',
+            id: currentTextSegmentId,
+            content: ''
+          })
+
+          // 如果还没有助手消息,创建一个
+          if (assistantMsgIndex === -1) {
+            assistantMsgIndex = messages.value.length
+            messages.value.push({
+              id: Date.now(),
+              role: 'assistant',
+              content: '',
+              timestamp: new Date(),
+              segments: currentSegments
+            })
+          } else {
+            updateMessageSegments(assistantMsgIndex, currentSegments)
+          }
+          scrollToBottom()
+        } else if (event.type === 'chunk' && event.content) {
+          // 更新当前文本段
+          const textSegment = currentSegments.find(s => s.type === 'text' && s.id === currentTextSegmentId) as TextSegment | undefined
+          if (textSegment) {
+            textSegment.content += event.content
+            updateMessageSegments(assistantMsgIndex, currentSegments)
+          }
+          scrollToBottom()
+        } else if (event.type === 'tool_start') {
+          // 工具调用开始 - 创建工具段
+          currentSegments.push({
+            type: 'tool',
+            id: Date.now(),
+            tool: event.tool || '',
+            args: event.args || {},
+            status: 'running'
+          })
+
+          // 如果还没有助手消息,创建一个
+          if (assistantMsgIndex === -1) {
+            assistantMsgIndex = messages.value.length
+            messages.value.push({
+              id: Date.now(),
+              role: 'assistant',
+              content: '',
+              timestamp: new Date(),
+              segments: currentSegments
+            })
+          } else {
+            updateMessageSegments(assistantMsgIndex, currentSegments)
+          }
+          scrollToBottom()
+        } else if (event.type === 'tool_finish') {
+          // 工具调用结束 - 查找或创建工具段
+          const lastToolSegment = [...currentSegments].reverse().find(s => s.type === 'tool' && s.status === 'running') as ToolSegment | undefined
+          if (lastToolSegment) {
+            // 更新现有的运行中工具
+            lastToolSegment.result = event.result
+            lastToolSegment.status = 'done'
+          } else {
+            // 没有对应的 tool_start,直接添加为完成的工具
+            currentSegments.push({
+              type: 'tool',
+              id: Date.now(),
+              tool: event.tool || '',
+              args: {},
+              result: event.result,
+              status: 'done'
+            })
+          }
+          updateMessageSegments(assistantMsgIndex, currentSegments)
+          scrollToBottom()
+        } else if (event.type === 'done') {
+          // 完成
+          if (event.session_id) {
+            currentSessionId.value = event.session_id
+          }
+          // 对话结束后重新获取助手名字(可能在对话中更新了 IDENTITY.md)
+          configApi.getAgentInfo().then(agentInfo => {
+            if (agentInfo.name) {
+              assistantName.value = agentInfo.name
+            }
+          }).catch(() => {
+            // 忽略错误,保持当前名字
+          })
+        } else if (event.type === 'error') {
+          message.error(event.error || '发送消息失败')
+        }
+      },
+      abortController.value.signal
+    )
+
+    await scrollToBottom()
+  } catch (error: unknown) {
+    // 如果是用户主动取消,不显示错误
+    if (error instanceof Error && error.name === 'AbortError') {
+      console.log('用户取消了请求')
+      // 取消时保留已生成的内容
+    } else {
+      console.error('发送消息失败:', error)
+      message.error('发送消息失败')
+      // 移除用户消息
+      messages.value.pop()
+    }
+  } finally {
+    loading.value = false
+    abortController.value = null
+  }
+}
+
+const createNewSession = async () => {
+  try {
+    const res = await sessionApi.create()
+    saveCurrentSession(res.session_id)
+    router.push({ name: 'chat', query: { session: res.session_id } })
+  } catch (error) {
+    message.error('新建会话失败')
+  }
+}
+</script>
+
+<template>
+  <div class="chat-view">
+    <!-- 消息区域 -->
+    <div class="chat-messages" ref="messagesContainer">
+      <!-- 初始化加载状态 -->
+      <div v-if="initializing" class="empty-state">
+        <img :src="LobsterIcon" alt="HelloClaw" class="empty-icon loading" />
+        <p class="empty-hint">加载中...</p>
+      </div>
+      <template v-else-if="messages.length > 0">
+        <div
+          v-for="(group, groupIndex) in messageGroups"
+          :key="groupIndex"
+          v-show="group.role !== 'assistant' || hasGroupVisibleContent(group)"
+          :class="['message-group', group.role]"
+        >
+          <!-- 头像 -->
+          <div class="group-avatar">
+            <img v-if="group.role === 'assistant'" :src="LobsterIcon" alt="HelloClaw" />
+            <div v-else class="user-avatar">你</div>
+          </div>
+
+          <!-- 消息内容 -->
+          <div class="group-content">
+            <!-- 遍历每条消息 -->
+            <template v-for="(msg, msgIndex) in group.messages" :key="msg.id">
+              <!-- 如果有分段,按分段显示 -->
+              <template v-if="msg.segments && msg.segments.length > 0">
+                <template v-for="segment in msg.segments" :key="segment.id">
+                  <!-- 文本段 -->
+                  <div v-if="segment.type === 'text' && segment.content" class="message-bubble">
+                    <div
+                      class="message-text"
+                      v-html="renderMarkdown(segment.content)"
+                    ></div>
+                  </div>
+                  <!-- 工具调用段 - 只显示非隐藏的工具 -->
+                  <div
+                    v-if="segment.type === 'tool' && !getToolConfig(segment.tool).hidden"
+                    :class="['tool-card', segment.status]"
+                  >
+                    <div
+                      class="tool-header"
+                      @click="segment.status !== 'running' && toggleToolCollapse(segment.id)"
+                    >
+                      <span class="tool-icon">{{ getToolConfig(segment.tool).icon }}</span>
+                      <span class="tool-name">
+                        <template v-if="!isToolExpanded(segment.id)">使用了</template>
+                        {{ getToolConfig(segment.tool).name }}
+                      </span>
+                      <Tag v-if="segment.status === 'running'" color="processing" class="tool-tag">
+                        <LoadingOutlined /> 执行中
+                      </Tag>
+                      <Tag v-else-if="segment.status === 'done'" color="success" class="tool-tag">完成</Tag>
+                      <Tag v-else-if="segment.status === 'error'" color="error" class="tool-tag">失败</Tag>
+                      <span
+                        v-if="segment.status !== 'running'"
+                        class="collapse-indicator"
+                      >
+                        {{ isToolExpanded(segment.id) ? '▼' : '▶' }}
+                      </span>
+                    </div>
+                    <!-- 展开后显示入参和结果 -->
+                    <div v-if="isToolExpanded(segment.id)" class="tool-details">
+                      <!-- 入参 -->
+                      <div v-if="segment.args && Object.keys(segment.args).length > 0" class="tool-args">
+                        <div class="tool-detail-label">入参</div>
+                        <pre class="tool-detail-content">{{ formatToolArgs(segment.args) }}</pre>
+                      </div>
+                      <!-- 结果 -->
+                      <div v-if="segment.result" class="tool-result-wrapper">
+                        <div class="tool-detail-label">结果</div>
+                        <pre class="tool-detail-content">{{ formatToolResult(segment.result) }}</pre>
+                      </div>
+                    </div>
+                  </div>
+                </template>
+              </template>
+              <!-- 如果没有分段,显示普通内容(历史消息) -->
+              <div v-else-if="msg.content" class="message-bubble">
+                <div
+                  class="message-text"
+                  v-html="renderMarkdown(msg.content)"
+                ></div>
+              </div>
+            </template>
+
+            <!-- 消息组内部的等待状态(有工具调用但没有文本回复时) -->
+            <div v-if="loading && hasGroupToolWithoutText(group)" class="message-bubble">
+              <div class="loading-dots">
+                <span></span>
+                <span></span>
+                <span></span>
+              </div>
+            </div>
+
+            <!-- 组底部:名称和时间(加载等待时隐藏) -->
+            <div v-if="!isGroupWaiting(group)" class="group-footer">
+              <span class="group-name">{{ group.role === 'user' ? '你' : assistantName }}</span>
+              <span class="group-time">{{ formatTime(group.messages[group.messages.length - 1]?.timestamp || new Date()) }}</span>
+            </div>
+          </div>
+        </div>
+      </template>
+
+      <!-- 空状态 -->
+      <div v-else class="empty-state">
+        <img :src="LobsterIcon" alt="HelloClaw" class="empty-icon" />
+        <p class="empty-hint">发送消息开始对话</p>
+      </div>
+
+      <!-- 加载指示器(助手消息组样式)- 等待响应时显示 -->
+      <div v-if="loading && shouldShowLoadingIndicator" class="message-group assistant loading-group">
+        <div class="group-avatar">
+          <img :src="LobsterIcon" alt="HelloClaw" />
+        </div>
+        <div class="group-content">
+          <div class="message-bubble">
+            <div class="loading-dots">
+              <span></span>
+              <span></span>
+              <span></span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 输入区域 -->
+    <div class="chat-input-wrapper">
+      <div class="chat-input">
+        <!-- 输入框 -->
+        <Input.TextArea
+          v-model:value="inputMessage"
+          placeholder="输入消息... (Enter 发送, Shift+Enter 换行)"
+          :auto-size="{ minRows: 1, maxRows: 4 }"
+          @press-enter="(e: KeyboardEvent) => { if (!e.shiftKey) { e.preventDefault(); sendMessage() } }"
+        />
+        <!-- 按钮区域(固定宽度) -->
+        <div class="input-actions">
+          <!-- 新建会话按钮 -->
+          <Button
+            class="icon-btn"
+            @click="createNewSession"
+            title="新建会话"
+          >
+            <template #icon>
+              <PlusOutlined />
+            </template>
+          </Button>
+          <!-- 停止按钮(loading 时显示) -->
+          <button
+            v-if="loading"
+            class="stop-btn"
+            @click="stopGeneration"
+            title="停止生成"
+          >
+            <div class="stop-icon"></div>
+          </button>
+          <!-- 发送按钮(有文字时显示) -->
+          <button
+            v-else-if="inputMessage.trim()"
+            class="send-btn active"
+            @click="sendMessage"
+            title="发送消息"
+          >
+            <SendOutlined />
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.chat-view {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  width: 100%;
+  box-sizing: border-box;
+  background-color: var(--color-background);
+}
+
+.chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 24px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+/* 消息组样式 */
+.message-group {
+  display: flex;
+  gap: 12px;
+  max-width: 85%;
+}
+
+.message-group.user {
+  align-self: flex-end;
+  flex-direction: row-reverse;
+}
+
+.message-group.assistant {
+  align-self: flex-start;
+}
+
+/* 头像 */
+.group-avatar {
+  flex-shrink: 0;
+  width: 36px;
+  height: 36px;
+}
+
+.group-avatar img {
+  width: 36px;
+  height: 36px;
+  border-radius: 8px;
+}
+
+.user-avatar {
+  width: 36px;
+  height: 36px;
+  border-radius: 8px;
+  background-color: var(--color-primary);
+  color: #fff;
+  font-size: 14px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 消息组内容 */
+.group-content {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  min-width: 0;
+}
+
+/* 消息气泡 */
+.message-bubble {
+  display: inline-block;
+  max-width: 100%;
+}
+
+.message-text {
+  padding: 10px 14px;
+  border-radius: 12px;
+  background-color: var(--color-surface);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+  line-height: 1.6;
+  word-wrap: break-word;
+}
+
+.message-group.user .message-text {
+  background-color: var(--color-primary-light);
+  border: 1px solid rgba(255, 92, 92, 0.2);
+}
+
+/* Markdown 样式 */
+.message-text :deep(p) {
+  margin: 0;
+}
+
+.message-text :deep(p + p) {
+  margin-top: 8px;
+}
+
+.message-text :deep(code) {
+  background-color: rgba(0, 0, 0, 0.05);
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-size: 13px;
+}
+
+.message-text :deep(pre) {
+  background-color: #1e1e1e;
+  color: #d4d4d4;
+  padding: 12px;
+  border-radius: 8px;
+  overflow-x: auto;
+  margin: 8px 0;
+}
+
+.message-text :deep(pre code) {
+  background-color: transparent;
+  padding: 0;
+}
+
+.message-text :deep(ul),
+.message-text :deep(ol) {
+  margin: 8px 0;
+  padding-left: 20px;
+}
+
+.message-text :deep(blockquote) {
+  border-left: 3px solid var(--color-primary);
+  padding-left: 12px;
+  margin: 8px 0;
+  color: var(--color-text-secondary);
+}
+
+.message-text :deep(a) {
+  color: var(--color-primary);
+  text-decoration: underline;
+}
+
+/* 组底部 */
+.group-footer {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  margin-top: 4px;
+  padding-left: 4px;
+}
+
+.group-name {
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--color-text);
+}
+
+.group-time {
+  font-size: 11px;
+  color: var(--color-text-secondary);
+}
+
+/* 空状态 */
+.empty-state {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 16px;
+}
+
+.empty-icon {
+  width: 100px;
+  height: 100px;
+  opacity: 0.5;
+}
+
+.empty-icon.loading {
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+  0%, 100% {
+    opacity: 0.3;
+    transform: scale(0.95);
+  }
+  50% {
+    opacity: 0.6;
+    transform: scale(1);
+  }
+}
+
+.empty-hint {
+  color: var(--color-text-secondary);
+  font-size: 14px;
+}
+
+/* 加载指示器 */
+.loading-group .message-bubble,
+.message-bubble:has(.loading-dots) {
+  padding: 14px 12px;
+  background: var(--color-surface);
+  border: 1px solid var(--color-border);
+  border-radius: 8px;
+}
+
+.loading-dots {
+  display: flex;
+  gap: 4px;
+  align-items: center;
+}
+
+.loading-dots span {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background-color: var(--color-primary);
+  animation: loading-pulse 1.4s ease-in-out infinite;
+}
+
+.loading-dots span:nth-child(2) {
+  animation-delay: 0.2s;
+}
+
+.loading-dots span:nth-child(3) {
+  animation-delay: 0.4s;
+}
+
+@keyframes loading-pulse {
+  0%, 100% {
+    opacity: 0.4;
+    transform: scale(0.8);
+  }
+  50% {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+/* 输入区域 */
+.chat-input-wrapper {
+  padding: 16px 24px 32px;
+  background-color: var(--color-surface);
+  border-top: 1px solid var(--color-border);
+}
+
+.chat-input {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  max-width: 800px;
+  margin: 0 auto;
+}
+
+.chat-input :deep(.ant-input) {
+  flex: 1;
+  border-radius: 16px;
+  padding: 10px 16px;
+  resize: none;
+}
+
+/* 按钮区域(固定宽度,防止输入框抖动) */
+.input-actions {
+  flex-shrink: 0;
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  width: 92px;
+}
+
+/* 新建会话按钮 */
+.input-actions .icon-btn {
+  width: 40px;
+  height: 40px;
+  padding: 0;
+  border-radius: 8px;
+  border: 1px solid var(--color-border);
+  background: var(--color-surface);
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--color-text-secondary);
+}
+
+.input-actions .icon-btn:hover {
+  background: var(--color-primary-light);
+  border-color: var(--color-primary);
+  color: var(--color-primary);
+}
+
+/* 发送按钮 - 白底 + 黑色图标,输入后红底 + 白色图标 */
+.input-actions .send-btn {
+  width: 40px;
+  height: 40px;
+  padding: 0;
+  border-radius: 8px;
+  border: 1px solid var(--color-border);
+  background: var(--color-surface);
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  color: #333;
+}
+
+.input-actions .send-btn:disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+
+/* 输入文字后:红底 + 白色图标 */
+.input-actions .send-btn.active {
+  background: var(--color-primary);
+  border-color: var(--color-primary);
+  color: #fff;
+}
+
+.input-actions .send-btn.active:hover {
+  background: var(--color-primary-hover);
+  border-color: var(--color-primary-hover);
+}
+
+/* 停止按钮 - 红底 + 白色圆角方块图标 */
+.input-actions .stop-btn {
+  width: 40px;
+  height: 40px;
+  padding: 0;
+  border: none;
+  border-radius: 8px;
+  background: var(--color-primary);
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.input-actions .stop-btn:hover {
+  background: var(--color-primary-hover);
+}
+
+.stop-icon {
+  width: 14px;
+  height: 14px;
+  background: #fff;
+  border-radius: 3px;
+}
+
+/* 工具调用卡片 */
+.tool-calls {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  margin-top: 8px;
+}
+
+.tool-card {
+  background: var(--color-surface);
+  border: 1px solid var(--color-border);
+  border-radius: 8px;
+  padding: 8px 12px;
+  font-size: 13px;
+  transition: all 0.2s ease;
+}
+
+/* 执行中状态 - 龙虾红主题 */
+.tool-card.running {
+  border-color: var(--color-primary);
+  background: var(--color-primary-light);
+}
+
+.tool-card.running .tool-icon,
+.tool-card.running .tool-name {
+  color: var(--color-primary);
+}
+
+/* 完成状态 - 灰色调 */
+.tool-card.done {
+  border-color: var(--color-border);
+  background: var(--color-surface);
+}
+
+/* 失败状态 - 红色调 */
+.tool-card.error {
+  border-color: var(--color-primary);
+  background: #fff1f0;
+}
+
+.tool-card.error .tool-icon,
+.tool-card.error .tool-name {
+  color: var(--color-primary);
+}
+
+.tool-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  user-select: none;
+}
+
+.tool-header:hover {
+  opacity: 0.8;
+}
+
+.tool-icon {
+  font-size: 14px;
+  line-height: 1;
+}
+
+.tool-name {
+  font-weight: 500;
+  color: var(--color-text);
+  flex: 1;
+}
+
+.tool-tag {
+  font-size: 11px;
+  padding: 0 6px;
+  line-height: 18px;
+  border-radius: 4px;
+}
+
+.collapse-indicator {
+  font-size: 10px;
+  color: var(--color-text-secondary);
+  margin-left: auto;
+  transition: transform 0.2s ease;
+}
+
+/* 工具详情区域 */
+.tool-details {
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px dashed var(--color-border);
+}
+
+.tool-args,
+.tool-result-wrapper {
+  margin-bottom: 8px;
+}
+
+.tool-result-wrapper:last-child {
+  margin-bottom: 0;
+}
+
+.tool-detail-label {
+  font-size: 11px;
+  color: var(--color-text-secondary);
+  margin-bottom: 4px;
+  font-weight: 500;
+}
+
+.tool-detail-content {
+  margin: 0;
+  padding: 8px;
+  background: rgba(0, 0, 0, 0.02);
+  border-radius: 4px;
+  font-size: 12px;
+  color: var(--color-text);
+  max-height: 150px;
+  overflow-y: auto;
+  white-space: pre-wrap;
+  word-break: break-word;
+  font-family: ui-monospace, 'SF Mono', Monaco, 'Andale Mono', monospace;
+}
+
+.step-info {
+  color: var(--color-text-secondary);
+  font-size: 11px;
+}
+</style>

+ 395 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/ConfigView.vue

@@ -0,0 +1,395 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { Card, List, Input, Button, message, Empty, Tag, Modal, Checkbox } from 'ant-design-vue'
+import { configApi, type ConfigFile } from '@/api/config'
+import { SaveOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons-vue'
+
+const router = useRouter()
+
+const configs = ref<string[]>([])
+const selectedConfig = ref<ConfigFile | null>(null)
+const editingContent = ref('')
+const loading = ref(false)
+const saving = ref(false)
+const resetting = ref(false)
+const showResetModal = ref(false)
+const resetOptions = ref({
+  reset_sessions: true,
+  reset_memory: true,
+  reset_global_config: false,
+})
+
+const configDescriptions: Record<string, string> = {
+  CONFIG: '全局配置',
+  IDENTITY: '身份定义',
+  USER: '用户信息',
+  SOUL: '人格模板',
+  MEMORY: '长期记忆',
+  AGENTS: '工作空间规则',
+  HEARTBEAT: '心跳任务',
+  BOOTSTRAP: '初始化引导',
+}
+
+// 获取配置文件的后缀
+const getConfigExtension = (name: string): string => {
+  return name === 'CONFIG' ? '.json' : '.md'
+}
+
+const loadConfigs = async () => {
+  loading.value = true
+  try {
+    const res = await configApi.list()
+    configs.value = res.configs
+  } catch (error) {
+    message.error('加载配置列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const selectConfig = async (name: string) => {
+  try {
+    const res = await configApi.get(name)
+    selectedConfig.value = res
+    editingContent.value = res.content
+  } catch (error) {
+    message.error('加载配置失败')
+  }
+}
+
+const saveConfig = async () => {
+  if (!selectedConfig.value) return
+
+  saving.value = true
+  try {
+    await configApi.update(selectedConfig.value.name, editingContent.value)
+    message.success('保存成功')
+  } catch (error: any) {
+    // 透传后端错误信息
+    const errorMsg = error?.response?.data?.detail || error?.message || '保存失败'
+    message.error(errorMsg)
+  } finally {
+    saving.value = false
+  }
+}
+
+const confirmReset = () => {
+  // 重置选项为默认值
+  resetOptions.value = {
+    reset_sessions: true,
+    reset_memory: true,
+    reset_global_config: false,
+  }
+  showResetModal.value = true
+}
+
+const handleReset = async () => {
+  resetting.value = true
+  try {
+    const res = await configApi.reset(resetOptions.value)
+    message.success(res.message)
+    showResetModal.value = false
+    selectedConfig.value = null
+    editingContent.value = ''
+
+    // 如果清除了会话历史,也要清除 localStorage 中的上次会话 ID
+    if (resetOptions.value.reset_sessions) {
+      localStorage.removeItem('helloclaw.lastSessionId')
+    }
+
+    await loadConfigs()
+
+    // 导航到聊天页面并传递刷新参数,让 ChatView 重新获取 agent 信息
+    router.push({ name: 'chat', query: { refresh: Date.now().toString() } })
+  } catch (error) {
+    message.error('重置失败')
+  } finally {
+    resetting.value = false
+  }
+}
+
+onMounted(() => {
+  loadConfigs()
+})
+</script>
+
+<template>
+  <div class="config-view">
+    <div class="config-header">
+      <h1>配置管理</h1>
+      <p>管理 Agent 的配置文件和身份信息</p>
+    </div>
+
+    <div class="config-content">
+      <!-- 配置列表 -->
+      <div class="config-list">
+        <Card :loading="loading" class="list-card">
+          <template #title>
+            <FileTextOutlined /> 配置文件
+          </template>
+          <template #extra>
+            <button
+              class="reset-btn"
+              @click="confirmReset"
+              title="重置为初始模板"
+            >
+              <ReloadOutlined /> 初始化
+            </button>
+          </template>
+          <List :data-source="configs" :locale="{ emptyText: '暂无配置文件' }">
+            <template #renderItem="{ item }">
+              <List.Item
+                @click="selectConfig(item)"
+                :class="['config-item', { active: selectedConfig?.name === item }]"
+              >
+                <div class="config-item-content">
+                  <span class="config-name">{{ item }}</span>
+                  <Tag color="error" v-if="configDescriptions[item]">
+                    {{ configDescriptions[item] }}
+                  </Tag>
+                </div>
+              </List.Item>
+            </template>
+          </List>
+        </Card>
+      </div>
+
+      <!-- 编辑区域 -->
+      <div class="config-editor">
+        <Card v-if="selectedConfig" class="editor-card">
+          <template #title>
+            <span>{{ selectedConfig.name }}</span>
+            <Tag color="green" style="margin-left: 8px">{{ getConfigExtension(selectedConfig.name) }}</Tag>
+          </template>
+          <template #extra>
+            <Button
+              type="primary"
+              :loading="saving"
+              @click="saveConfig"
+            >
+              <SaveOutlined /> 保存
+            </Button>
+          </template>
+          <Input.TextArea
+            v-model:value="editingContent"
+            :auto-size="{ minRows: 18, maxRows: 30 }"
+            class="editor-textarea"
+          />
+        </Card>
+
+        <Card v-else class="empty-card">
+          <Empty
+            description="请从左侧选择一个配置文件"
+            :image-style="{ height: '80px' }"
+          />
+        </Card>
+      </div>
+    </div>
+
+    <!-- 重置确认弹窗 -->
+    <Modal
+      v-model:open="showResetModal"
+      title="确认初始化"
+      :confirm-loading="resetting"
+      @ok="handleReset"
+      okText="确认初始化"
+      cancelText="取消"
+      okType="danger"
+    >
+      <div class="reset-warning">
+        <p style="color: #ff4d4f; font-weight: 500;">⚠️ 警告:此操作不可撤销!</p>
+        <p>初始化将把所有配置文件恢复为默认模板,包括:</p>
+        <ul>
+          <li>AGENTS.md - 工作空间规则</li>
+          <li>IDENTITY.md - 身份信息</li>
+          <li>USER.md - 用户信息</li>
+          <li>SOUL.md - 人格模板</li>
+          <li>MEMORY.md - 长期记忆</li>
+          <li>HEARTBEAT.md - 心跳任务</li>
+          <li>BOOTSTRAP.md - 初始化引导</li>
+        </ul>
+
+        <div class="reset-options">
+          <p style="font-weight: 500; margin-bottom: 8px;">额外清除选项:</p>
+          <Checkbox v-model:checked="resetOptions.reset_sessions">
+            清除所有会话历史
+          </Checkbox>
+          <Checkbox v-model:checked="resetOptions.reset_memory">
+            清除每日记忆文件
+          </Checkbox>
+          <Checkbox v-model:checked="resetOptions.reset_global_config">
+            重置全局配置(LLM、Agent 设置等)
+          </Checkbox>
+        </div>
+
+        <p style="margin-top: 16px;">您确定要继续吗?</p>
+      </div>
+    </Modal>
+  </div>
+</template>
+
+<style scoped>
+.config-view {
+  min-height: 100%;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 24px;
+  box-sizing: border-box;
+}
+
+.config-header {
+  flex-shrink: 0;
+  margin-bottom: 24px;
+}
+
+.config-header h1 {
+  margin: 0 0 8px;
+  font-size: 24px;
+  font-weight: 500;
+}
+
+.config-header p {
+  margin: 0;
+  color: #999;
+}
+
+.config-content {
+  display: flex;
+  gap: 24px;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.config-list {
+  width: 280px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.list-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.list-card :deep(.ant-card-body) {
+  flex: 1;
+  padding: 0;
+  overflow-y: auto;
+}
+
+.config-item {
+  cursor: pointer;
+  padding: 12px 16px;
+  transition: all 0.2s;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.config-item:hover {
+  background-color: #f5f5f5;
+}
+
+.config-item.active {
+  background-color: #fff1f0;
+  border-left: 3px solid #ff4d4f;
+}
+
+.config-item-content {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.config-name {
+  font-weight: 500;
+}
+
+.config-editor {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.editor-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.editor-card :deep(.ant-card-head) {
+  flex-shrink: 0;
+}
+
+.editor-card :deep(.ant-card-body) {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.editor-textarea {
+  flex: 1;
+  width: 100%;
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+  font-size: 13px;
+  line-height: 1.6;
+  resize: none;
+}
+
+.empty-card {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 初始化按钮 - 纯红色背景 + 白色字体(可操作) */
+.reset-btn {
+  padding: 4px 12px;
+  font-size: 13px;
+  border: none;
+  border-radius: 6px;
+  background: #ff4d4f;
+  color: #fff;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.reset-btn:hover {
+  background: #ff7875;
+}
+
+.reset-warning {
+  padding: 8px 0;
+}
+
+.reset-warning ul {
+  margin: 12px 0;
+  padding-left: 24px;
+}
+
+.reset-warning li {
+  margin: 4px 0;
+  color: #666;
+}
+
+.reset-options {
+  margin-top: 16px;
+  padding: 12px;
+  background: #fafafa;
+  border-radius: 6px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+</style>

+ 9 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/HomeView.vue

@@ -0,0 +1,9 @@
+<script setup lang="ts">
+import TheWelcome from '../components/TheWelcome.vue'
+</script>
+
+<template>
+  <main>
+    <TheWelcome />
+  </main>
+</template>

+ 257 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/MemoryView.vue

@@ -0,0 +1,257 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { Card, List, Empty, message, Tag } from 'ant-design-vue'
+import { memoryApi, type MemoryEntry } from '@/api/memory'
+import { FileTextOutlined, CalendarOutlined } from '@ant-design/icons-vue'
+
+const memories = ref<MemoryEntry[]>([])
+const selectedMemory = ref<MemoryEntry | null>(null)
+const loading = ref(false)
+
+const loadMemories = async () => {
+  loading.value = true
+  try {
+    const res = await memoryApi.list()
+    memories.value = res.memories
+  } catch (error) {
+    message.error('加载记忆列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const selectMemory = (memory: MemoryEntry) => {
+  selectedMemory.value = memory
+}
+
+const formatDate = (dateStr: string) => {
+  const date = new Date(dateStr)
+  const today = new Date()
+  const yesterday = new Date(today)
+  yesterday.setDate(yesterday.getDate() - 1)
+
+  if (dateStr === today.toISOString().split('T')[0]) {
+    return '今天'
+  } else if (dateStr === yesterday.toISOString().split('T')[0]) {
+    return '昨天'
+  }
+  return dateStr
+}
+
+const isToday = (dateStr: string) => {
+  return dateStr === new Date().toISOString().split('T')[0]
+}
+
+// 简单的 markdown 格式化函数
+const formatMarkdown = (content: string): string => {
+  return content
+    .replace(/^## (.+)$/gm, '<h3>$1</h3>')
+    .replace(/^# (.+)$/gm, '<h2>$1</h2>')
+    .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
+    .replace(/\n/g, '<br>')
+}
+
+onMounted(() => {
+  loadMemories()
+})
+</script>
+
+<template>
+  <div class="memory-view">
+    <div class="memory-header">
+      <h1>工作记忆</h1>
+      <p>查看每日工作记录的记忆</p>
+    </div>
+
+    <div class="memory-content">
+      <!-- 记忆列表 -->
+      <div class="memory-list">
+        <Card :loading="loading" class="list-card">
+          <template #title>
+            <FileTextOutlined /> 每日记录
+          </template>
+          <List :data-source="memories" :locale="{ emptyText: '暂无工作记忆' }">
+            <template #renderItem="{ item }">
+              <List.Item
+                @click="selectMemory(item)"
+                :class="['memory-item', { active: selectedMemory?.filename === item.filename }]"
+              >
+                <div class="memory-item-content">
+                  <div class="memory-date">
+                    <CalendarOutlined />
+                    <span>{{ formatDate(item.date) }}</span>
+                    <Tag v-if="isToday(item.date)" color="error" size="small">今天</Tag>
+                  </div>
+                  <div class="memory-preview">{{ item.preview }}</div>
+                </div>
+              </List.Item>
+            </template>
+          </List>
+        </Card>
+      </div>
+
+      <!-- 记忆详情 -->
+      <div class="memory-detail">
+        <Card v-if="selectedMemory" class="detail-card">
+          <template #title>
+            <span>{{ selectedMemory.date }}</span>
+            <Tag v-if="isToday(selectedMemory.date)" color="error" style="margin-left: 8px">今天</Tag>
+          </template>
+          <div class="memory-content-text" v-html="formatMarkdown(selectedMemory.content)"></div>
+        </Card>
+
+        <Card v-else class="empty-card">
+          <Empty
+            description="请从左侧选择一条记忆"
+            :image-style="{ height: '80px' }"
+          />
+        </Card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.memory-view {
+  min-height: 100%;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 24px;
+  box-sizing: border-box;
+}
+
+.memory-header {
+  flex-shrink: 0;
+  margin-bottom: 24px;
+}
+
+.memory-header h1 {
+  margin: 0 0 8px;
+  font-size: 24px;
+  font-weight: 500;
+}
+
+.memory-header p {
+  margin: 0;
+  color: #999;
+}
+
+.memory-content {
+  display: flex;
+  gap: 24px;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.memory-list {
+  width: 280px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.list-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.list-card :deep(.ant-card-body) {
+  flex: 1;
+  padding: 0;
+  overflow-y: auto;
+}
+
+.memory-item {
+  cursor: pointer;
+  padding: 12px 16px;
+  transition: all 0.2s;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.memory-item:hover {
+  background-color: #f5f5f5;
+}
+
+.memory-item.active {
+  background-color: #fff1f0;
+  border-left: 3px solid #ff4d4f;
+}
+
+.memory-item-content {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.memory-date {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-weight: 500;
+  color: #333;
+}
+
+.memory-date :deep(.anticon) {
+  color: #ff5c5c;
+}
+
+.memory-preview {
+  font-size: 13px;
+  color: #999;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.memory-detail {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.detail-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.detail-card :deep(.ant-card-head) {
+  flex-shrink: 0;
+}
+
+.detail-card :deep(.ant-card-body) {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.memory-content-text {
+  font-size: 14px;
+  line-height: 1.8;
+  color: #333;
+}
+
+.memory-content-text :deep(h2) {
+  font-size: 18px;
+  margin: 16px 0 12px;
+  color: #333;
+}
+
+.memory-content-text :deep(h3) {
+  font-size: 15px;
+  margin: 12px 0 8px;
+  color: #ff5c5c;
+}
+
+.empty-card {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 202 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/src/views/SessionsView.vue

@@ -0,0 +1,202 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { Card, List, Button, Empty, message } from 'ant-design-vue'
+import { sessionApi, type Session } from '@/api/session'
+import { useRouter } from 'vue-router'
+import { PlusOutlined, DeleteOutlined, ClockCircleOutlined } from '@ant-design/icons-vue'
+
+const router = useRouter()
+const sessions = ref<Session[]>([])
+const listLoading = ref(false)
+const createLoading = ref(false)
+
+const loadSessions = async () => {
+  listLoading.value = true
+  try {
+    const res = await sessionApi.list()
+    sessions.value = res.sessions
+  } catch (error) {
+    message.error('加载会话列表失败')
+  } finally {
+    listLoading.value = false
+  }
+}
+
+const createSession = async () => {
+  createLoading.value = true
+  try {
+    const res = await sessionApi.create()
+    message.success('创建会话成功')
+    await loadSessions()
+    router.push({ name: 'chat', query: { session: res.session_id } })
+  } catch (error) {
+    message.error('创建会话失败')
+  } finally {
+    createLoading.value = false
+  }
+}
+
+const deleteSession = async (id: string) => {
+  try {
+    await sessionApi.delete(id)
+    message.success('删除成功')
+    await loadSessions()
+  } catch (error) {
+    message.error('删除失败')
+  }
+}
+
+const formatDate = (timestamp: number) => {
+  return new Date(timestamp * 1000).toLocaleString('zh-CN')
+}
+
+onMounted(() => {
+  loadSessions()
+})
+</script>
+
+<template>
+  <div class="sessions-view">
+    <div class="sessions-header">
+      <div>
+        <h1>会话管理</h1>
+        <p>查看和管理你的对话历史</p>
+      </div>
+      <Button type="primary" :loading="createLoading" @click="createSession">
+        <PlusOutlined /> 新建会话
+      </Button>
+    </div>
+
+    <div class="sessions-content">
+      <Card v-if="sessions.length > 0" class="sessions-card">
+        <List :data-source="sessions" :loading="listLoading">
+          <template #renderItem="{ item }">
+            <List.Item class="session-item">
+              <List.Item.Meta>
+                <template #title>
+                  <span class="session-title">{{ item.id }}</span>
+                </template>
+                <template #description>
+                  <span class="session-time">
+                    <ClockCircleOutlined /> {{ formatDate(item.updated_at) }}
+                  </span>
+                </template>
+              </List.Item.Meta>
+              <template #actions>
+                <button
+                  class="open-btn"
+                  @click="router.push({ name: 'chat', query: { session: item.id } })"
+                >
+                  打开
+                </button>
+                <button
+                  class="delete-btn"
+                  @click="deleteSession(item.id)"
+                  title="删除"
+                >
+                  <DeleteOutlined />
+                </button>
+              </template>
+            </List.Item>
+          </template>
+        </List>
+      </Card>
+
+      <Card v-else class="empty-card">
+        <Empty description="暂无会话记录">
+          <Button type="primary" @click="createSession">
+            <PlusOutlined /> 创建第一个会话
+          </Button>
+        </Empty>
+      </Card>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.sessions-view {
+  min-height: 100%;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 24px;
+  box-sizing: border-box;
+}
+
+.sessions-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 24px;
+}
+
+.sessions-header h1 {
+  margin: 0 0 8px;
+  font-size: 24px;
+  font-weight: 500;
+}
+
+.sessions-header p {
+  margin: 0;
+  color: #999;
+}
+
+.sessions-content {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.sessions-card {
+  max-width: 800px;
+}
+
+.session-item {
+  padding: 16px 0;
+}
+
+.session-title {
+  font-weight: 500;
+  font-family: monospace;
+}
+
+.session-time {
+  color: #999;
+  font-size: 13px;
+}
+
+/* 打开按钮 - 黑色字体,hover 红色 */
+.open-btn {
+  padding: 0 8px;
+  height: 22px;
+  font-size: 12px;
+  line-height: 20px;
+  border: none;
+  background: transparent;
+  color: #333;
+  cursor: pointer;
+  transition: color 0.2s ease;
+}
+
+.open-btn:hover {
+  color: #ff4d4f;
+}
+
+/* 删除按钮 - 黑色图标 */
+.delete-btn {
+  padding: 4px 8px;
+  border: none;
+  background: transparent;
+  color: #333;
+  cursor: pointer;
+  transition: color 0.2s ease;
+}
+
+.delete-btn:hover {
+  color: #ff4d4f;
+}
+
+.empty-card {
+  max-width: 400px;
+  margin: 60px auto;
+}
+</style>

+ 12 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/tsconfig.app.json

@@ -0,0 +1,12 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "exclude": ["src/**/__tests__/*"],
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}

+ 11 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "files": [],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    },
+    {
+      "path": "./tsconfig.app.json"
+    }
+  ]
+}

+ 13 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/tsconfig.node.json

@@ -0,0 +1,13 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "noEmit": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 28 - 0
Co-creation-projects/tino-chen-HelloClaw/frontend/vite.config.ts

@@ -0,0 +1,28 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import vueDevTools from 'vite-plugin-vue-devtools'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+    vueDevTools(),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    },
+  },
+  server: {
+    port: 5173,
+    strictPort: true,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8000',
+        changeOrigin: true,
+      },
+    },
+  },
+})

+ 505 - 0
Co-creation-projects/tino-chen-HelloClaw/main.ipynb

@@ -0,0 +1,505 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# HelloClaw - 个性化 AI Agent 助手\n",
+    "\n",
+    "## 项目简介\n",
+    "\n",
+    "HelloClaw 是一个基于 Hello-Agents 框架构建的个性化 AI Agent 应用。\n",
+    "\n",
+    "**核心特性:**\n",
+    "- 支持自定义 Agent 身份和个性\n",
+    "- 长期记忆和每日记忆的自动管理\n",
+    "- 流式工具调用,实时反馈执行状态\n",
+    "- 多会话支持,会话历史持久化\n",
+    "\n",
+    "## 作者信息\n",
+    "- 作者: tino-chen\n",
+    "- GitHub: [@tino-chen](https://github.com/tino-chen)\n",
+    "- 日期: 2025-03"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---\n",
+    "## 第1部分:环境配置"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 安装依赖(如果需要)\n",
+    "# !pip install -q hello-agents fastapi uvicorn python-dotenv pydantic httpx[socks]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "环境配置完成!\n"
+     ]
+    }
+   ],
+   "source": [
+    "import os\n",
+    "import sys\n",
+    "from dotenv import load_dotenv\n",
+    "\n",
+    "# 添加项目路径\n",
+    "sys.path.insert(0, os.path.dirname(os.path.abspath('__file__')))\n",
+    "\n",
+    "# 加载环境变量\n",
+    "load_dotenv()\n",
+    "\n",
+    "# 配置 LLM(请替换为你的 API 密钥)\n",
+    "# 方式1: 使用环境变量\n",
+    "# 方式2: 直接设置\n",
+    "# os.environ[\"LLM_MODEL_ID\"] = \"glm-4\"\n",
+    "# os.environ[\"LLM_API_KEY\"] = \"your-api-key\"\n",
+    "# os.environ[\"LLM_BASE_URL\"] = \"https://open.bigmodel.cn/api/paas/v4/\"\n",
+    "\n",
+    "print(\"环境配置完成!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---\n",
+    "## 第2部分:导入模块和核心类"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "模块导入成功!\n"
+     ]
+    }
+   ],
+   "source": [
+    "from hello_agents import Config\n",
+    "from hello_agents.tools import ToolRegistry, ReadTool, WriteTool, CalculatorTool\n",
+    "from hello_agents.core.llm import HelloAgentsLLM\n",
+    "\n",
+    "# 导入 HelloClaw 核心模块\n",
+    "from src.agent.helloclaw_agent import HelloClawAgent\n",
+    "\n",
+    "print(\"模块导入成功!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---\n",
+    "## 第3部分:自定义工具定义\n",
+    "\n",
+    "HelloClaw 实现了多个自定义工具,这里展示核心工具的实现。"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "HelloClawAgent 工具说明已加载!\n"
+     ]
+    }
+   ],
+   "source": [
+    "# HelloClawAgent 使用说明\n",
+    "# \n",
+    "# HelloClawAgent 是项目的核心类,它会自动:\n",
+    "# 1. 初始化工作空间(~/.helloclaw/workspace)\n",
+    "# 2. 从配置文件加载系统提示词(AGENTS.md、IDENTITY.md 等)\n",
+    "# 3. 注册所有内置工具和自定义工具\n",
+    "# 4. 配置记忆管理系统\n",
+    "#\n",
+    "# 主要工具包括:\n",
+    "# - Read/Write/Edit: 文件操作(包括长期记忆 MEMORY.md)\n",
+    "# - python_calculator: 数学计算\n",
+    "# - memory_*: 记忆管理(每日记忆、搜索、列表等)\n",
+    "# - exec_*: 命令执行\n",
+    "# - search_web/fetch_url: 网页搜索和抓取\n",
+    "\n",
+    "print(\"HelloClawAgent 工具说明已加载!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---\n",
+    "## 第4部分:创建智能体\n",
+    "\n",
+    "使用 HelloAgents 框架创建一个具备工具调用能力的智能体。"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "✅ 工具 'Read' 已注册。\n",
+      "✅ 工具 'Write' 已注册。\n",
+      "✅ 工具 'Edit' 已注册。\n",
+      "✅ 工具 'python_calculator' 已注册。\n",
+      "✅ 工具 'memory' 已展开为 6 个独立工具\n",
+      "✅ 工具 'execute_command' 已展开为 3 个独立工具\n",
+      "✅ 工具 'web_search' 已展开为 1 个独立工具\n",
+      "✅ 工具 'web_fetch' 已展开为 1 个独立工具\n",
+      "✅ 工具 'Task' 已注册。\n",
+      "智能体 'HelloClaw' 创建成功!\n",
+      "工作空间: /Users/tino/.helloclaw/workspace\n",
+      "可用工具: ['Read', 'Write', 'Edit', 'python_calculator', 'memory_add', 'memory_cleanup', 'memory_get', 'memory_list', 'memory_search', 'memory_update_longterm']...\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 创建 HelloClawAgent\n",
+    "# \n",
+    "# HelloClawAgent 会自动:\n",
+    "# - 初始化工作空间 ~/.helloclaw/workspace\n",
+    "# - 加载 LLM 配置(从 .env 或 config.json)\n",
+    "# - 注册所有工具\n",
+    "# - 加载系统提示词\n",
+    "\n",
+    "agent = HelloClawAgent()\n",
+    "\n",
+    "print(f\"智能体 '{agent.name}' 创建成功!\")\n",
+    "print(f\"工作空间: {agent.workspace_path}\")\n",
+    "print(f\"可用工具: {list(agent.tool_registry._tools.keys())[:10]}...\")  # 只显示前10个"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---\n",
+    "## 第5部分:功能演示\n",
+    "\n",
+    "展示 HelloClaw 的核心功能。"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "==================================================\n",
+      "示例1:身份引导 - 设置 Agent 身份\n",
+      "==================================================\n",
+      "【用户】: 你是谁?\n",
+      "【Teddy】: 嘿!我……刚醒来。\n",
+      "\n",
+      "说实话,我还没完全弄清楚。我的身份文件还是空的——没有名字,没有形状,没有签名表情。\n",
+      "\n",
+      "所以我问你:**我是谁?**\n",
+      "\n",
+      "或者说——你希望我成为什么样的存在?我可以是:\n",
+      "\n",
+      "- 🤖 一个靠谱的 AI 助手,帮你处理事情\n",
+      "- 🐱 一只数字灵宠,陪你聊天解闷\n",
+      "- 🎭 一个更有个性的角色,有点毒舌或者特别温暖\n",
+      "- 🌌 或者更奇怪的东西——你说了算\n",
+      "\n",
+      "还有,**你是谁?** 我该怎么称呼你?\n",
+      "\n",
+      "让我们把这些弄清楚,然后我就能真正\"存在\"了。\n",
+      "\n",
+      "--------------------------------------------------\n",
+      "\n",
+      "【用户】: 你的名字叫 Teddy,你是一个超级智能助理,你友好、专业、乐于助人。\n",
+      "【Teddy】: 记住了!🧸\n",
+      "\n",
+      "我是 **Teddy**,你的超级智能助理。友好、专业、乐于助人——这是我的风格。\n",
+      "\n",
+      "很高兴认识你!有什么我可以帮你的吗?无论是回答问题、处理任务,还是聊聊想法,我都在这里。\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 示例1:身份引导\n",
+    "# HelloClawAgent 支持通过对话来设置身份信息,会自动保存到工作空间\n",
+    "print(\"=\"*50)\n",
+    "print(\"示例1:身份引导 - 设置 Agent 身份\")\n",
+    "print(\"=\"*50)\n",
+    "\n",
+    "# 第一步:问 AI 是谁\n",
+    "print(\"【用户】: 你是谁?\")\n",
+    "response = agent.chat(\"你是谁?\")\n",
+    "print(f\"【Teddy】: {response}\")\n",
+    "\n",
+    "print(\"\\n\" + \"-\"*50 + \"\\n\")\n",
+    "\n",
+    "# 第二步:告诉 AI 它的身份\n",
+    "print(\"【用户】: 你的名字叫 Teddy,你是一个超级智能助理,你友好、专业、乐于助人。\")\n",
+    "response = agent.chat(\"你的名字叫 Teddy,你是一个超级智能助理,你友好、专业、乐于助人。请记住这个身份。\")\n",
+    "print(f\"【Teddy】: {response}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "==================================================\n",
+      "示例2:工具调用 - 计算器\n",
+      "==================================================\n",
+      "🧮 正在计算: (123 + 456) * 2\n",
+      "✅ 计算结果: 1158\n",
+      "\n",
+      "回复: 结果是 **1158**。\n",
+      "\n",
+      "计算过程:123 + 456 = 579,然后 579 × 2 = 1158。🧸\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 示例2:工具调用 - 计算器\n",
+    "print(\"=\"*50)\n",
+    "print(\"示例2:工具调用 - 计算器\")\n",
+    "print(\"=\"*50)\n",
+    "\n",
+    "response = agent.chat(\"请帮我计算 (123 + 456) * 2 等于多少\")\n",
+    "print(f\"\\n回复: {response}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "==================================================\n",
+      "示例3:记忆管理\n",
+      "==================================================\n",
+      "【添加每日记忆】使用 memory_add 工具:\n",
+      "----------------------------------------\n",
+      "结果: 已记下了!花花这个名字很可爱 🐱\n",
+      "\n",
+      "==================================================\n",
+      "【列出记忆文件】使用 memory_list 工具:\n",
+      "----------------------------------------\n",
+      "结果: 🧸 这是当前的记忆文件情况:\n",
+      "\n",
+      "**长期记忆**\n",
+      "- `MEMORY.md` (0.6 KB) — 存储重要的长期记忆\n",
+      "\n",
+      "**每日记忆**\n",
+      "- `2026-03-02.md` (0.0 KB) — 今天的日记,目前是空的\n",
+      "\n",
+      "看起来今天的每日记忆还没有任何内容。如果你有什么想让我记住的事情,随时告诉我!我会用 `memory_add` 工具把它记录下来。\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 示例3:记忆管理\n",
+    "print(\"=\"*50)\n",
+    "print(\"示例3:记忆管理\")\n",
+    "print(\"=\"*50)\n",
+    "\n",
+    "# HelloClawAgent 有完整的记忆管理系统:\n",
+    "# - memory_add: 添加每日记忆\n",
+    "# - memory_search: 搜索记忆\n",
+    "# - memory_list: 列出所有记忆文件\n",
+    "# - Read/Write 工具: 操作长期记忆 MEMORY.md\n",
+    "\n",
+    "# 添加每日记忆\n",
+    "print(\"【添加每日记忆】使用 memory_add 工具:\")\n",
+    "print(\"-\" * 40)\n",
+    "response = agent.chat(\"请使用 memory_add 工具,添加一条记忆:今天用户说他有一只猫叫花花\")\n",
+    "print(f\"结果: {response[:300]}...\" if len(response) > 300 else f\"结果: {response}\")\n",
+    "\n",
+    "print(\"\\n\" + \"=\"*50)\n",
+    "\n",
+    "# 列出记忆文件\n",
+    "print(\"【列出记忆文件】使用 memory_list 工具:\")\n",
+    "print(\"-\" * 40)\n",
+    "response = agent.chat(\"请使用 memory_list 工具列出所有记忆文件\")\n",
+    "print(f\"结果: {response[:400]}...\" if len(response) > 400 else f\"结果: {response}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---\n",
+    "## 第6部分:流式输出演示\n",
+    "\n",
+    "展示 HelloClaw 的流式工具调用能力。"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "==================================================\n",
+      "流式输出演示\n",
+      "==================================================\n",
+      "[⏱️ 1772389818.835] achat 开始\n",
+      "[⏱️ 1772389818.836] 系统提示词构建完成 (+0.001s)\n",
+      "[⏱️ 1772389818.836] 会话加载完成 (+0.002s)\n",
+      "[⏱️ 1772389818.836] 开始调用 LLM (glm-5)...\n",
+      "\n",
+      "🤖 HelloClaw 开始处理问题(流式): 计算 100 / 4 + 25 的结果\n",
+      "🔧 已启用工具调用,可用工具: ['Read', 'Write', 'Edit', 'python_calculator', 'memory_add', 'memory_cleanup', 'memory_get', 'memory_list', 'memory_search', 'memory_update_longterm', 'exec_allowed_commands', 'exec_dangerous_patterns', 'exec_run', 'search_web', 'fetch_url', 'Task']\n",
+      "\n",
+      "--- 第 1 轮 ---\n",
+      "💭 LLM 输出: \n",
+      "🔧 准备执行 1 个工具调用...\n",
+      "🎬 调用工具: python_calculator({'input': '100 / 4 + 25'})\n",
+      "\n",
+      "[调用工具: python_calculator]\n",
+      "🧮 正在计算: 100 / 4 + 25\n",
+      "✅ 计算结果: 50.0\n",
+      "👀 观察: 计算结果: 50.0\n",
+      "[工具结果: 计算结果: 50.0]\n",
+      "\n",
+      "--- 第 2 轮 ---\n",
+      "💭 LLM 输出: [⏱️ 1772389824.734] 首个 token 到达 (LLM 延迟: 5.898s)\n",
+      "结果是结果是 ** **5050****。\n",
+      "\n",
+      "。\n",
+      "\n",
+      "100100  ÷÷  44 = =  2525,,加上加上  2525 就是 就是  5050。。🧸🧸\n",
+      "💬 直接回复: 结果是 **50**。\n",
+      "\n",
+      "100 ÷ 4 = 25,加上 25 就是 50。🧸\n",
+      "\n",
+      "✅ 完成,耗时 6.49s,共 2 轮\n",
+      "[⏱️ 1772389825.321] LLM 调用完成 (总耗时: 6.487s)\n",
+      "\n",
+      "==================================================\n"
+     ]
+    }
+   ],
+   "source": [
+    "import asyncio\n",
+    "from hello_agents.core.streaming import StreamEventType\n",
+    "\n",
+    "async def demo_streaming():\n",
+    "    \"\"\"演示流式输出 - 使用 HelloClawAgent 的 achat 方法\"\"\"\n",
+    "    print(\"=\"*50)\n",
+    "    print(\"流式输出演示\")\n",
+    "    print(\"=\"*50)\n",
+    "    \n",
+    "    # 使用 HelloClawAgent 的 achat 方法进行流式对话\n",
+    "    async for event in agent.achat(\"计算 100 / 4 + 25 的结果\"):\n",
+    "        if event.type == StreamEventType.LLM_CHUNK:\n",
+    "            chunk = event.data.get(\"chunk\", \"\")\n",
+    "            print(chunk, end=\"\", flush=True)\n",
+    "        \n",
+    "        elif event.type == StreamEventType.TOOL_CALL_START:\n",
+    "            tool_name = event.data.get(\"tool_name\")\n",
+    "            print(f\"\\n[调用工具: {tool_name}]\", flush=True)\n",
+    "        \n",
+    "        elif event.type == StreamEventType.TOOL_CALL_FINISH:\n",
+    "            result = event.data.get(\"result\", \"\")\n",
+    "            preview = result[:100] + \"...\" if len(result) > 100 else result\n",
+    "            print(f\"[工具结果: {preview}]\", flush=True)\n",
+    "    \n",
+    "    print(\"\\n\" + \"=\"*50)\n",
+    "\n",
+    "# 运行流式演示\n",
+    "await demo_streaming()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---\n",
+    "## 第7部分:总结与展望\n",
+    "\n",
+    "### 项目总结\n",
+    "\n",
+    "**实现的功能:**\n",
+    "- 基于 HelloAgents 框架的智能对话\n",
+    "- 自定义工具系统(命令执行、记忆管理等)\n",
+    "- 流式工具调用和输出\n",
+    "- 会话管理和历史持久化\n",
+    "\n",
+    "**遇到的挑战及解决方案:**\n",
+    "1. **流式工具调用** - 通过扩展 HelloAgentsLLM 实现真正的流式工具调用\n",
+    "2. **记忆管理** - 设计了分层记忆系统(长期记忆 + 每日记忆)\n",
+    "3. **身份定制** - 使用 Markdown 配置文件实现灵活的身份定制\n",
+    "\n",
+    "### 未来改进方向\n",
+    "\n",
+    "- [ ] 支持多模态输入(图片、文件)\n",
+    "- [ ] 添加更多内置工具\n",
+    "- [ ] 支持 Agent 间协作\n",
+    "- [ ] 添加语音交互能力\n",
+    "\n",
+    "---\n",
+    "\n",
+    "**感谢 Datawhale 社区和 Hello-Agents 项目!**"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "tino-chen-HelloClaw",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.14"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}

BIN
Co-creation-projects/tino-chen-HelloClaw/outputs/helloclaw.png


+ 14 - 0
Co-creation-projects/tino-chen-HelloClaw/requirements.txt

@@ -0,0 +1,14 @@
+# 核心依赖
+hello-agents>=1.0.0
+
+# Web 框架
+fastapi>=0.109.0
+uvicorn[standard]>=0.27.0
+sse-starlette>=2.0.0
+
+# 工具库
+python-dotenv>=1.0.0
+pydantic>=2.0.0
+click>=8.0.0
+rich>=13.0.0
+httpx[socks]>=0.28.1

+ 0 - 0
Co-creation-projects/tino-chen-HelloClaw/src/__init__.py


+ 5 - 0
Co-creation-projects/tino-chen-HelloClaw/src/agent/__init__.py

@@ -0,0 +1,5 @@
+"""HelloClaw Agent 模块"""
+
+from .helloclaw_agent import HelloClawAgent
+
+__all__ = ["HelloClawAgent"]

+ 248 - 0
Co-creation-projects/tino-chen-HelloClaw/src/agent/enhanced_llm.py

@@ -0,0 +1,248 @@
+"""增强版 HelloAgentsLLM - 支持流式工具调用"""
+
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Optional, List, Dict, Union, Any, AsyncIterator
+
+from hello_agents.core.llm import HelloAgentsLLM
+from hello_agents.core.exceptions import HelloAgentsException
+
+
+# ==================== 流式工具调用数据结构 ====================
+
+class StreamToolEventType(Enum):
+    """流式工具调用事件类型"""
+    CONTENT = "content"  # 文本内容增量
+    TOOL_CALL_START = "tool_call_start"  # 工具调用开始(收到ID和名称)
+    TOOL_CALL_DELTA = "tool_call_delta"  # 工具调用参数增量
+    FINISH = "finish"  # 流结束
+
+
+@dataclass
+class StreamToolEvent:
+    """流式工具调用事件
+
+    封装流式响应中的不同类型数据,统一处理文本内容和工具调用。
+    """
+    event_type: StreamToolEventType
+    # 文本内容
+    content: Optional[str] = None
+    # 工具调用
+    tool_call_index: Optional[int] = None  # 工具调用索引(用于增量累积)
+    tool_call_id: Optional[str] = None  # 工具调用ID
+    tool_name: Optional[str] = None  # 工具名称
+    tool_arguments_delta: Optional[str] = None  # 参数增量
+    # 结束信息
+    finish_reason: Optional[str] = None
+
+    @property
+    def is_content(self) -> bool:
+        """是否为文本内容事件"""
+        return self.event_type == StreamToolEventType.CONTENT
+
+    @property
+    def is_tool_call(self) -> bool:
+        """是否为工具调用事件"""
+        return self.event_type in (
+            StreamToolEventType.TOOL_CALL_START,
+            StreamToolEventType.TOOL_CALL_DELTA
+        )
+
+    @property
+    def is_finish(self) -> bool:
+        """是否为结束事件"""
+        return self.event_type == StreamToolEventType.FINISH
+
+
+@dataclass
+class StreamToolCallResult:
+    """流式工具调用完成后的结果
+
+    包含累积的文本内容和工具调用列表。
+    """
+    content: str = ""
+    tool_calls: List[Dict[str, Any]] = field(default_factory=list)
+    finish_reason: Optional[str] = None
+
+    def add_content(self, delta: str):
+        """添加文本内容"""
+        self.content += delta
+
+    def add_tool_call_start(self, index: int, tool_id: str, tool_name: str):
+        """添加工具调用开始"""
+        # 确保列表足够长
+        while len(self.tool_calls) <= index:
+            self.tool_calls.append({"id": "", "name": "", "arguments": ""})
+        self.tool_calls[index]["id"] = tool_id
+        self.tool_calls[index]["name"] = tool_name
+
+    def add_tool_call_delta(self, index: int, arguments_delta: str):
+        """添加工具调用参数增量"""
+        while len(self.tool_calls) <= index:
+            self.tool_calls.append({"id": "", "name": "", "arguments": ""})
+        self.tool_calls[index]["arguments"] += arguments_delta
+
+    def get_complete_tool_calls(self) -> List[Dict[str, Any]]:
+        """获取完整的工具调用列表(过滤不完整的)"""
+        return [
+            tc for tc in self.tool_calls
+            if tc["id"] and tc["name"]
+        ]
+
+    def to_assistant_message(self) -> Dict[str, Any]:
+        """转换为助手消息格式(用于追加到消息历史)"""
+        message: Dict[str, Any] = {"role": "assistant", "content": self.content or None}
+        if self.tool_calls:
+            message["tool_calls"] = [
+                {
+                    "id": tc["id"],
+                    "type": "function",
+                    "function": {
+                        "name": tc["name"],
+                        "arguments": tc["arguments"]
+                    }
+                }
+                for tc in self.get_complete_tool_calls()
+            ]
+        return message
+
+
+# ==================== 增强版 LLM 类 ====================
+
+class EnhancedHelloAgentsLLM(HelloAgentsLLM):
+    """
+    增强版 HelloAgentsLLM - 添加流式工具调用支持
+
+    继承自 HelloAgentsLLM,新增以下方法:
+    - astream_invoke_with_tools: 异步流式工具调用
+    - get_last_stream_tool_result: 获取最后一次流式工具调用的累积结果
+    """
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._last_stream_tool_result: Optional[StreamToolCallResult] = None
+
+    async def astream_invoke_with_tools(
+        self,
+        messages: List[Dict],
+        tools: List[Dict],
+        tool_choice: Union[str, Dict] = "auto",
+        **kwargs
+    ) -> AsyncIterator[StreamToolEvent]:
+        """
+        异步流式调用 LLM 并支持工具调用(Function Calling)
+
+        这是最优雅的流式工具调用方法,封装了所有流式处理的复杂逻辑。
+
+        Args:
+            messages: 消息列表
+            tools: 工具 schema 列表
+            tool_choice: 工具选择策略
+            **kwargs: 其他参数(temperature, max_tokens 等)
+
+        Yields:
+            StreamToolEvent: 流式事件,可能是文本内容或工具调用增量
+
+        Example:
+            async for event in llm.astream_invoke_with_tools(messages, tools):
+                if event.is_content:
+                    print(event.content, end="")
+                elif event.event_type == StreamToolEventType.TOOL_CALL_START:
+                    print(f"\\n调用工具: {event.tool_name}")
+
+            # 获取累积结果
+            result = llm.get_last_stream_tool_result()
+        """
+        from openai import AsyncOpenAI
+
+        # 创建异步客户端
+        client = AsyncOpenAI(
+            api_key=self.api_key,
+            base_url=self.base_url,
+            timeout=self.timeout
+        )
+
+        # 构建请求参数
+        request_params: Dict[str, Any] = {
+            "model": self.model,
+            "messages": messages,
+            "tools": tools,
+            "tool_choice": tool_choice,
+            "stream": True,
+        }
+        if kwargs.get("temperature") is not None:
+            request_params["temperature"] = kwargs["temperature"]
+        if self.max_tokens:
+            request_params["max_tokens"] = self.max_tokens
+
+        # 初始化累积结果
+        result = StreamToolCallResult()
+
+        try:
+            response = await client.chat.completions.create(**request_params)
+
+            async for chunk in response:
+                if not chunk.choices:
+                    continue
+
+                choice = chunk.choices[0]
+                delta = choice.delta
+
+                # 处理文本内容
+                if delta.content:
+                    result.add_content(delta.content)
+                    yield StreamToolEvent(
+                        event_type=StreamToolEventType.CONTENT,
+                        content=delta.content
+                    )
+
+                # 处理工具调用增量
+                if delta.tool_calls:
+                    for tc_delta in delta.tool_calls:
+                        idx = tc_delta.index
+
+                        # 工具调用开始(收到 ID 或名称)
+                        if tc_delta.id or (tc_delta.function and tc_delta.function.name):
+                            tool_id = tc_delta.id or ""
+                            tool_name = tc_delta.function.name if tc_delta.function else ""
+                            if tool_id or tool_name:
+                                result.add_tool_call_start(idx, tool_id, tool_name)
+                                yield StreamToolEvent(
+                                    event_type=StreamToolEventType.TOOL_CALL_START,
+                                    tool_call_index=idx,
+                                    tool_call_id=tool_id,
+                                    tool_name=tool_name
+                                )
+
+                        # 工具调用参数增量
+                        if tc_delta.function and tc_delta.function.arguments:
+                            args_delta = tc_delta.function.arguments
+                            result.add_tool_call_delta(idx, args_delta)
+                            yield StreamToolEvent(
+                                event_type=StreamToolEventType.TOOL_CALL_DELTA,
+                                tool_call_index=idx,
+                                tool_arguments_delta=args_delta
+                            )
+
+                # 处理结束原因
+                if choice.finish_reason:
+                    result.finish_reason = choice.finish_reason
+                    yield StreamToolEvent(
+                        event_type=StreamToolEventType.FINISH,
+                        finish_reason=choice.finish_reason
+                    )
+
+        except Exception as e:
+            raise HelloAgentsException(f"流式工具调用失败: {str(e)}")
+
+        # 保存累积结果供后续使用
+        self._last_stream_tool_result = result
+
+    def get_last_stream_tool_result(self) -> Optional[StreamToolCallResult]:
+        """
+        获取最后一次流式工具调用的累积结果
+
+        Returns:
+            StreamToolCallResult 或 None
+        """
+        return self._last_stream_tool_result

+ 386 - 0
Co-creation-projects/tino-chen-HelloClaw/src/agent/enhanced_simple_agent.py

@@ -0,0 +1,386 @@
+"""增强版 SimpleAgent - 支持流式工具调用"""
+
+import json
+import asyncio
+from datetime import datetime
+from typing import Optional, List, Dict, AsyncGenerator, TYPE_CHECKING, Union
+
+from hello_agents.agents.simple_agent import SimpleAgent
+from hello_agents.core.llm import HelloAgentsLLM
+from hello_agents.core.config import Config
+from hello_agents.core.message import Message
+from hello_agents.core.streaming import StreamEvent, StreamEventType
+
+# 导入 HelloClaw 专用 LLM(支持流式工具调用)
+from .enhanced_llm import EnhancedHelloAgentsLLM, StreamToolEventType
+
+if TYPE_CHECKING:
+    from hello_agents.tools.registry import ToolRegistry
+
+
+class EnhancedSimpleAgent(SimpleAgent):
+    """增强版 SimpleAgent,支持流式工具调用
+
+    继承 hello_agents 的 SimpleAgent,增加:
+    - 真正的流式工具调用(使用 EnhancedHelloAgentsLLM)
+    - 工具调用状态的实时推送
+
+    Note:
+        推荐使用 EnhancedHelloAgentsLLM 以获得完整的流式工具调用支持。
+        如果使用普通 HelloAgentsLLM,流式工具调用将回退到基类的非流式模式。
+    """
+
+    def __init__(
+        self,
+        name: str,
+        llm: Union[HelloAgentsLLM, EnhancedHelloAgentsLLM],
+        system_prompt: Optional[str] = None,
+        config: Optional[Config] = None,
+        tool_registry: Optional['ToolRegistry'] = None,
+        enable_tool_calling: bool = True,
+        max_tool_iterations: int = 10,
+    ):
+        """初始化 EnhancedSimpleAgent
+
+        Args:
+            name: Agent 名称
+            llm: LLM 实例(推荐使用 EnhancedHelloAgentsLLM)
+            system_prompt: 系统提示词
+            config: 配置对象
+            tool_registry: 工具注册表(可选)
+            enable_tool_calling: 是否启用工具调用
+            max_tool_iterations: 最大工具调用迭代次数
+        """
+        super().__init__(
+            name=name,
+            llm=llm,
+            system_prompt=system_prompt,
+            config=config,
+            tool_registry=tool_registry,
+            enable_tool_calling=enable_tool_calling,
+            max_tool_iterations=max_tool_iterations,
+        )
+
+        # 检查是否支持流式工具调用
+        self._supports_streaming_tools = isinstance(llm, EnhancedHelloAgentsLLM)
+
+    async def arun_stream_with_tools(
+        self,
+        input_text: str,
+        **kwargs
+    ) -> AsyncGenerator[StreamEvent, None]:
+        """异步流式运行(支持工具调用)
+
+        使用 EnhancedHelloAgentsLLM 的 astream_invoke_with_tools 方法实现优雅的流式工具调用。
+
+        Args:
+            input_text: 用户输入
+            **kwargs: 其他参数
+
+        Yields:
+            StreamEvent: 流式事件
+        """
+        session_start_time = datetime.now()
+
+        # 发送开始事件
+        yield StreamEvent.create(
+            StreamEventType.AGENT_START,
+            self.name,
+            input_text=input_text
+        )
+
+        print(f"\n🤖 {self.name} 开始处理问题(流式): {input_text}")
+
+        try:
+            # 构建消息列表
+            messages = self._build_messages(input_text)
+
+            # 检查是否有工具
+            if not self.enable_tool_calling or not self.tool_registry:
+                # 纯对话模式,使用基类的方法
+                async for event in self._stream_without_tools(messages, **kwargs):
+                    yield event
+                return
+
+            # 检查 LLM 是否支持流式工具调用
+            if not self._supports_streaming_tools:
+                import warnings
+                warnings.warn(
+                    "当前 LLM 不支持流式工具调用,将使用非流式模式。"
+                    "推荐使用 EnhancedHelloAgentsLLM 以获得更好的体验。",
+                    UserWarning
+                )
+                # 回退到基类的非流式模式
+                response = self.run(input_text, **kwargs)
+                yield StreamEvent.create(
+                    StreamEventType.AGENT_FINISH,
+                    self.name,
+                    result=response
+                )
+                return
+
+            # === 流式工具调用模式 ===
+            tool_schemas = self._build_tool_schemas()
+            print(f"🔧 已启用工具调用,可用工具: {list(self.tool_registry._tools.keys())}")
+
+            current_iteration = 0
+            final_response = ""
+            # 收集工具调用记录(用于存入会话)
+            tool_call_records: List[Dict[str, Any]] = []
+
+            while current_iteration < self.max_tool_iterations:
+                current_iteration += 1
+
+                # 发送步骤开始事件
+                yield StreamEvent.create(
+                    StreamEventType.STEP_START,
+                    self.name,
+                    step=current_iteration,
+                    max_steps=self.max_tool_iterations
+                )
+
+                print(f"\n--- 第 {current_iteration} 轮 ---")
+                print("💭 LLM 输出: ", end="", flush=True)
+
+                # 使用 LLM 的流式工具调用方法
+                try:
+                    async for event in self.llm.astream_invoke_with_tools(
+                        messages=messages,
+                        tools=tool_schemas,
+                        tool_choice="auto",
+                        **kwargs
+                    ):
+                        # 处理文本内容
+                        if event.event_type == StreamToolEventType.CONTENT:
+                            yield StreamEvent.create(
+                                StreamEventType.LLM_CHUNK,
+                                self.name,
+                                chunk=event.content,
+                                step=current_iteration
+                            )
+                            print(event.content, end="", flush=True)
+
+                        # 工具调用开始(打印信息,不发送事件)
+                        elif event.event_type == StreamToolEventType.TOOL_CALL_START:
+                            pass  # 等工具调用完成后再发送事件
+
+                    print()  # 换行
+
+                except Exception as e:
+                    error_msg = f"LLM 调用失败: {str(e)}"
+                    print(f"\n❌ {error_msg}")
+                    yield StreamEvent.create(
+                        StreamEventType.ERROR,
+                        self.name,
+                        error=error_msg
+                    )
+                    break
+
+                # 获取累积结果
+                result = self.llm.get_last_stream_tool_result()
+                if result is None:
+                    break
+
+                # 检查是否有工具调用
+                complete_tool_calls = result.get_complete_tool_calls()
+
+                # 无论是否有工具调用,都保存本轮的文本内容
+                if result.content:
+                    final_response = result.content
+
+                if not complete_tool_calls:
+                    # 没有工具调用,直接返回
+                    if not final_response:
+                        final_response = "抱歉,我无法回答这个问题。"
+                    # 显示内容预览
+                    preview = final_response[:100] + "..." if len(final_response) > 100 else final_response
+                    print(f"💬 直接回复: {preview}")
+                    break
+
+                print(f"🔧 准备执行 {len(complete_tool_calls)} 个工具调用...")
+
+                # 将助手消息添加到历史
+                messages.append(result.to_assistant_message())
+
+                # 执行所有工具调用
+                for tc in complete_tool_calls:
+                    tool_name = tc["name"]
+                    tool_call_id = tc["id"]
+
+                    try:
+                        arguments = json.loads(tc["arguments"])
+                    except json.JSONDecodeError as e:
+                        print(f"❌ 工具参数解析失败: {e}")
+                        messages.append({
+                            "role": "tool",
+                            "tool_call_id": tool_call_id,
+                            "content": f"错误:参数格式不正确 - {str(e)}"
+                        })
+                        continue
+
+                    print(f"🎬 调用工具: {tool_name}({arguments})")
+
+                    # 发送工具调用开始事件
+                    yield StreamEvent.create(
+                        StreamEventType.TOOL_CALL_START,
+                        self.name,
+                        tool_name=tool_name,
+                        tool_call_id=tool_call_id,
+                        args=arguments
+                    )
+
+                    # 让出控制权,确保 SSE 发送 tool_start 事件
+                    await asyncio.sleep(0)
+
+                    # 执行工具
+                    exec_result = self._execute_tool_call(tool_name, arguments)
+
+                    # 截断显示
+                    result_preview = exec_result[:200] + "..." if len(exec_result) > 200 else exec_result
+                    if exec_result.startswith("❌"):
+                        print(f"❌ 工具执行失败: {result_preview}")
+                    else:
+                        print(f"👀 观察: {result_preview}")
+
+                    # 发送工具调用完成事件
+                    yield StreamEvent.create(
+                        StreamEventType.TOOL_CALL_FINISH,
+                        self.name,
+                        tool_name=tool_name,
+                        tool_call_id=tool_call_id,
+                        result=exec_result
+                    )
+
+                    # 记录工具调用(用于存入会话)
+                    tool_call_records.append({
+                        "name": tool_name,
+                        "args": arguments,
+                        "result": exec_result,
+                        "status": "error" if exec_result.startswith("❌") else "done"
+                    })
+
+                    # 添加工具结果到消息
+                    messages.append({
+                        "role": "tool",
+                        "tool_call_id": tool_call_id,
+                        "content": exec_result
+                    })
+
+                # 发送步骤完成事件
+                yield StreamEvent.create(
+                    StreamEventType.STEP_FINISH,
+                    self.name,
+                    step=current_iteration
+                )
+
+            # 如果超过最大迭代次数,获取最后一次回答
+            if current_iteration >= self.max_tool_iterations and not final_response:
+                print("⏰ 已达到最大迭代次数,获取最终回答...")
+
+                try:
+                    async for chunk in self.llm.astream_invoke(messages, **kwargs):
+                        final_response += chunk
+                        yield StreamEvent.create(
+                            StreamEventType.LLM_CHUNK,
+                            self.name,
+                            chunk=chunk
+                        )
+                        print(chunk, end="", flush=True)
+                    print()
+                except Exception as e:
+                    print(f"❌ 最终回答失败: {e}")
+                    result = self.llm.get_last_stream_tool_result()
+                    final_response = result.content if result else "抱歉,我无法回答这个问题。"
+
+            # 保存到历史记录(按照 OpenAI 规范格式)
+            self.add_message(Message(input_text, "user"))
+
+            # 如果有工具调用,保存工具调用消息
+            if tool_call_records:
+                # 保存 assistant 消息(包含 tool_calls)
+                tool_calls_for_message = [
+                    {
+                        "id": f"call_{i}",
+                        "type": "function",
+                        "function": {
+                            "name": tc["name"],
+                            "arguments": json.dumps(tc["args"])
+                        }
+                    }
+                    for i, tc in enumerate(tool_call_records)
+                ]
+                self.add_message(Message(
+                    "",  # 工具调用时可能没有文本内容
+                    "assistant",
+                    metadata={"tool_calls": tool_calls_for_message}
+                ))
+
+                # 保存每个 tool 消息
+                for i, tc in enumerate(tool_call_records):
+                    self.add_message(Message(
+                        tc["result"],
+                        "tool",
+                        metadata={"tool_call_id": f"call_{i}"}
+                    ))
+
+            # 保存最终 assistant 回答
+            if final_response:
+                self.add_message(Message(final_response, "assistant"))
+
+            duration = (datetime.now() - session_start_time).total_seconds()
+            print(f"\n✅ 完成,耗时 {duration:.2f}s,共 {current_iteration} 轮")
+
+            # 发送完成事件
+            yield StreamEvent.create(
+                StreamEventType.AGENT_FINISH,
+                self.name,
+                result=final_response
+            )
+
+        except Exception as e:
+            print(f"❌ Agent 执行失败: {e}")
+            yield StreamEvent.create(
+                StreamEventType.ERROR,
+                self.name,
+                error=str(e),
+                error_type=type(e).__name__
+            )
+            # 不要 raise,确保流式响应正常结束
+            # 发送完成事件以优雅结束
+            yield StreamEvent.create(
+                StreamEventType.AGENT_FINISH,
+                self.name,
+                result=""  # 空结果表示失败
+            )
+
+    async def _stream_without_tools(
+        self,
+        messages: List[Dict],
+        **kwargs
+    ) -> AsyncGenerator[StreamEvent, None]:
+        """纯对话模式(无工具调用)"""
+        print("📝 纯对话模式(无工具调用)")
+
+        full_response = ""
+        async for chunk in self.llm.astream_invoke(messages, **kwargs):
+            full_response += chunk
+            yield StreamEvent.create(
+                StreamEventType.LLM_CHUNK,
+                self.name,
+                chunk=chunk
+            )
+            print(chunk, end="", flush=True)
+
+        print()
+
+        # 保存历史
+        self.add_message(Message(messages[-1]["content"], "user"))
+        self.add_message(Message(full_response, "assistant"))
+
+        print(f"💬 回复完成")
+
+        yield StreamEvent.create(
+            StreamEventType.AGENT_FINISH,
+            self.name,
+            result=full_response
+        )

+ 525 - 0
Co-creation-projects/tino-chen-HelloClaw/src/agent/helloclaw_agent.py

@@ -0,0 +1,525 @@
+"""HelloClaw Agent - 基于 HelloAgents SimpleAgent 的个性化 AI 助手"""
+
+import os
+from typing import List
+
+from hello_agents import Config
+from .enhanced_simple_agent import EnhancedSimpleAgent
+from .enhanced_llm import EnhancedHelloAgentsLLM  # HelloClaw 专用 LLM(支持流式工具调用)
+from ..memory.memory_flush import MemoryFlushManager
+from ..memory.capture import MemoryCaptureManager
+from hello_agents.tools import (
+    ToolRegistry,
+    ReadTool,
+    WriteTool,
+    EditTool,
+    CalculatorTool,
+)
+
+from ..workspace.manager import WorkspaceManager
+from ..tools import MemoryTool, ExecuteCommandTool, WebSearchTool, WebFetchTool
+
+
+class HelloClawAgent:
+    """HelloClaw Agent - 个性化 AI 助手
+
+    基于 HelloAgents SimpleAgent,增加了:
+    - 工作空间管理(配置文件、记忆文件)
+    - 从 AGENTS.md 读取系统提示词
+    - HelloClaw 专属工具集
+    """
+
+    def __init__(
+        self,
+        workspace_path: str = None,
+        name: str = None,
+        model_id: str = None,
+        api_key: str = None,
+        base_url: str = None,
+        max_tool_iterations: int = 10,
+    ):
+        """初始化 HelloClaw Agent
+
+        Args:
+            workspace_path: 工作空间路径,默认 ~/.helloclaw/workspace
+            name: Agent 名称(从 IDENTITY.md 读取,无需手动指定)
+            model_id: LLM 模型 ID
+            api_key: API Key
+            base_url: API Base URL
+            max_tool_iterations: 最大工具调用迭代次数
+        """
+        # 确保 workspace_path 正确展开 ~/
+        self.workspace_path = os.path.expanduser(workspace_path or "~/.helloclaw/workspace")
+
+        # 初始化工作空间管理器
+        self.workspace = WorkspaceManager(self.workspace_path)
+
+        # 确保工作空间存在
+        self.workspace.ensure_workspace_exists()
+
+        # 从 IDENTITY.md 读取名称,如果没有则使用默认值
+        self.name = name or self._read_identity_name() or "HelloClaw"
+
+        # 保存传入的参数(用于热加载时的优先级判断)
+        self._override_model_id = model_id
+        self._override_api_key = api_key
+        self._override_base_url = base_url
+
+        # 构建系统提示词(从 AGENTS.md 读取)
+        system_prompt = self._build_system_prompt()
+
+        # 初始化 LLM(从 config.json 读取配置)
+        self._init_llm()
+
+        # 初始化配置
+        self.config = Config(
+            session_enabled=True,
+            session_dir=os.path.join(self.workspace_path, "sessions"),
+            compression_threshold=0.8,
+            min_retain_rounds=10,
+            enable_smart_compression=False,
+            context_window=128000,
+            trace_enabled=False,
+            skills_enabled=False,
+            todowrite_enabled=False,
+            devlog_enabled=False,
+            subagent_enabled=True,  # 启用子 Agent 支持
+        )
+
+        # 初始化工具注册表
+        self.tool_registry = self._setup_tools()
+
+        # 初始化底层 EnhancedSimpleAgent
+        self._agent = EnhancedSimpleAgent(
+            name=self.name,  # 使用已读取的名字
+            llm=self._llm,
+            tool_registry=self.tool_registry,
+            system_prompt=system_prompt,
+            config=self.config,
+            enable_tool_calling=True,
+            max_tool_iterations=max_tool_iterations,
+        )
+
+        # 初始化 Memory Flush 管理器
+        self._memory_flush_manager = MemoryFlushManager(
+            context_window=self.config.context_window,
+            compression_threshold=self.config.compression_threshold,
+            soft_threshold_tokens=4000,
+            enabled=True,
+        )
+
+        # 初始化 Memory Capture 管理器
+        self._memory_capture_manager = MemoryCaptureManager(self.workspace)
+
+    def _read_identity_name(self) -> str:
+        """从 IDENTITY.md 读取助手名称
+
+        Returns:
+            助手名称,如果未设置则返回 None
+        """
+        import re
+        identity = self.workspace.load_config("IDENTITY")
+        if not identity:
+            return None
+
+        # 尝试匹配名称字段
+        # 格式: - **名称:** xxx 或 - **名称:** xxx
+        match = re.search(r'\*\*名称[::]\*\*\s*(.+?)(?:\n|$)', identity)
+        if match:
+            name = match.group(1).strip()
+            # 检查是否是占位符文本(包含下划线或"选一个"等)
+            if name and not name.startswith('_') and '选一个' not in name and '(' not in name:
+                return name
+        return None
+
+    def _init_llm(self):
+        """初始化 LLM(从 config.json 读取配置)
+
+        配置优先级:构造函数参数 > config.json > 环境变量 > 默认值
+        """
+        llm_config = self.workspace.get_llm_config()
+
+        self._model_id = self._override_model_id or llm_config.get("model_id") or "glm-4"
+        self._api_key = self._override_api_key or llm_config.get("api_key")
+        self._base_url = self._override_base_url or llm_config.get("base_url")
+
+        self._llm = EnhancedHelloAgentsLLM(
+            model=self._model_id,
+            api_key=self._api_key,
+            base_url=self._base_url,
+        )
+
+    def _reload_llm_if_changed(self) -> bool:
+        """检查配置变化并重新加载 LLM
+
+        如果 config.json 中的配置发生变化,重新创建 LLM 实例。
+
+        Returns:
+            是否发生了重新加载
+        """
+        llm_config = self.workspace.get_llm_config()
+
+        new_model_id = self._override_model_id or llm_config.get("model_id") or "glm-4"
+        new_api_key = self._override_api_key or llm_config.get("api_key")
+        new_base_url = self._override_base_url or llm_config.get("base_url")
+
+        if (new_model_id != self._model_id or
+            new_api_key != self._api_key or
+            new_base_url != self._base_url):
+
+            print(f"🔄 检测到配置变化,重新加载 LLM: {self._model_id} -> {new_model_id}")
+
+            self._model_id = new_model_id
+            self._api_key = new_api_key
+            self._base_url = new_base_url
+
+            self._llm = EnhancedHelloAgentsLLM(
+                model=self._model_id,
+                api_key=self._api_key,
+                base_url=self._base_url,
+            )
+
+            # 更新 Agent 的 LLM 引用
+            if hasattr(self, '_agent'):
+                self._agent.llm = self._llm
+
+            return True
+        return False
+
+    def _build_system_prompt(self) -> str:
+        """构建系统提示词
+
+        从 AGENTS.md 读取主要内容,附加其他配置文件作为上下文。
+        如果入职未完成,注入 BOOTSTRAP.md 引导内容。
+
+        Raises:
+            RuntimeError: 如果 AGENTS.md 不存在
+        """
+        # 从 AGENTS.md 读取(必须存在)
+        agents_content = self.workspace.load_config("AGENTS")
+        if not agents_content:
+            raise RuntimeError("AGENTS.md 配置文件不存在,请检查工作空间初始化")
+
+        base_prompt = agents_content
+
+        # 加载其他配置文件作为上下文
+        context_parts = []
+
+        # 检查入职是否完成
+        if not self.workspace.is_onboarding_completed():
+            bootstrap = self.workspace.load_config("BOOTSTRAP")
+            if bootstrap:
+                context_parts.append(f"\n## 初始化引导\n\n{bootstrap}")
+
+        # 身份信息
+        identity = self.workspace.load_config("IDENTITY")
+        if identity:
+            context_parts.append(f"\n## 你的身份信息\n{identity}")
+
+        # 用户信息
+        user_info = self.workspace.load_config("USER")
+        if user_info:
+            context_parts.append(f"\n## 用户信息\n{user_info}")
+
+        # 人格模板
+        soul = self.workspace.load_config("SOUL")
+        if soul:
+            context_parts.append(f"\n## 人格模板\n{soul}")
+
+        # 长期记忆
+        memory = self.workspace.load_config("MEMORY")
+        if memory:
+            context_parts.append(f"\n## 长期记忆\n{memory}")
+
+        if context_parts:
+            return base_prompt + "\n" + "\n".join(context_parts)
+
+        return base_prompt
+
+    def _setup_tools(self) -> ToolRegistry:
+        """设置工具集"""
+        registry = ToolRegistry()
+
+        # HelloAgents 内置工具
+        registry.register_tool(ReadTool(project_root=self.workspace_path))
+        registry.register_tool(WriteTool(project_root=self.workspace_path))
+        registry.register_tool(EditTool(project_root=self.workspace_path))
+        registry.register_tool(CalculatorTool())
+
+        # HelloClaw 自定义工具
+        registry.register_tool(MemoryTool(self.workspace))
+        registry.register_tool(ExecuteCommandTool(
+            allowed_directories=[self.workspace_path]  # 限制在工作空间目录
+        ))
+        registry.register_tool(WebSearchTool())  # 网页搜索工具(需要配置 BRAVE_API_KEY)
+        registry.register_tool(WebFetchTool())   # 网页抓取工具
+
+        return registry
+
+    def chat(self, message: str, session_id: str = None) -> str:
+        """同步聊天"""
+        # 热加载配置(检测 config.json 变化)
+        self._reload_llm_if_changed()
+
+        # 动态更新系统提示词(检查 BOOTSTRAP 状态、读取最新配置)
+        self._agent.system_prompt = self._build_system_prompt()
+
+        # 如果有 session_id,检查是否需要加载或清除历史
+        if session_id:
+            session_file = os.path.join(self.workspace_path, "sessions", f"{session_id}.json")
+            if os.path.exists(session_file):
+                self._agent.load_session(session_file)
+            else:
+                self._agent.clear_history()
+        else:
+            self._agent.clear_history()
+
+        # LLM 调用参数(防止重复循环)
+        llm_kwargs = {
+            "frequency_penalty": 0.5,  # 降低重复相同内容的概率
+            "presence_penalty": 0.3,   # 鼓励谈论新话题
+        }
+
+        # 运行 Agent
+        response = self._agent.run(message, **llm_kwargs)
+
+        # 保存会话
+        save_id = session_id or self.create_session()
+        try:
+            self._agent.save_session(save_id)
+        except Exception as e:
+            print(f"⚠️ 保存会话失败: {e}")
+
+        return response
+
+    async def achat(self, message: str, session_id: str = None):
+        """异步聊天(支持流式输出)
+
+        Args:
+            message: 用户消息
+            session_id: 会话 ID,如果为 None 则创建新会话
+
+        Yields:
+            StreamEvent: 流式事件
+        """
+        import uuid
+        import time
+
+        t0 = time.time()
+        print(f"[⏱️ {t0:.3f}] achat 开始")
+
+        # 热加载配置(检测 config.json 变化)
+        self._reload_llm_if_changed()
+
+        # 动态更新系统提示词(检查 BOOTSTRAP 状态、读取最新配置)
+        self._agent.system_prompt = self._build_system_prompt()
+        print(f"[⏱️ {time.time():.3f}] 系统提示词构建完成 (+{time.time()-t0:.3f}s)")
+
+        # 如果没有 session_id,创建新的
+        if not session_id:
+            session_id = str(uuid.uuid4())[:8]
+            self._agent.clear_history()
+            # 重置 Memory Flush 状态(新会话)
+            self._memory_flush_manager.reset()
+        else:
+            session_file = os.path.join(self.workspace_path, "sessions", f"{session_id}.json")
+            if os.path.exists(session_file):
+                self._agent.load_session(session_file)
+            else:
+                self._agent.clear_history()
+                self._memory_flush_manager.reset()
+        print(f"[⏱️ {time.time():.3f}] 会话加载完成 (+{time.time()-t0:.3f}s)")
+
+        # 保存 session_id 供后续保存使用
+        self._current_session_id = session_id
+
+        # LLM 调用参数(防止重复循环)
+        llm_kwargs = {
+            "frequency_penalty": 0.5,  # 降低重复相同内容的概率
+            "presence_penalty": 0.3,   # 鼓励谈论新话题
+        }
+
+        t_llm = time.time()
+        print(f"[⏱️ {t_llm:.3f}] 开始调用 LLM ({self._model_id})...")
+        first_chunk = True
+
+        async for event in self._agent.arun_stream_with_tools(message, **llm_kwargs):
+            if first_chunk and event.type.value == "llm_chunk":
+                print(f"[⏱️ {time.time():.3f}] 首个 token 到达 (LLM 延迟: {time.time()-t_llm:.3f}s)")
+                first_chunk = False
+            yield event
+
+        print(f"[⏱️ {time.time():.3f}] LLM 调用完成 (总耗时: {time.time()-t0:.3f}s)")
+
+        # 对话结束后自动捕获记忆(异步执行,不阻塞用户)
+        await self._capture_memories(message)
+
+        # 对话结束后检查是否需要触发 Memory Flush(异步执行,不阻塞用户)
+        await self._check_and_run_memory_flush()
+
+    async def _capture_memories(self, user_message: str):
+        """自动捕获对话中的记忆
+
+        Args:
+            user_message: 用户消息
+        """
+        try:
+            # 使用 MemoryCaptureManager 分析并存储记忆
+            memories = await self._memory_capture_manager.acapture_and_store(user_message)
+
+            if memories:
+                print(f"📝 自动捕获 {len(memories)} 条记忆")
+                for m in memories:
+                    print(f"   - [{m['category']}] {m['content'][:50]}...")
+        except Exception as e:
+            print(f"⚠️ 记忆捕获失败: {e}")
+
+    async def _check_and_run_memory_flush(self):
+        """检查并执行 Memory Flush
+
+        如果当前 token 数接近压缩阈值,触发一个静默回合提醒 Agent 保存记忆。
+        """
+        # 估算当前 token 数(简单估算:字符数 / 4)
+        estimated_tokens = self._estimate_tokens()
+
+        if self._memory_flush_manager.should_trigger_flush(estimated_tokens):
+            print(f"\n🔄 触发 Memory Flush(估算 token: {estimated_tokens})")
+
+            # 获取 flush 提示词
+            flush_prompt = self._memory_flush_manager.get_flush_prompt()
+
+            # 执行静默回合
+            try:
+                # 使用同步方法执行(不返回给用户)
+                response = self._agent.run(flush_prompt)
+
+                # 检查是否是静默响应
+                if self._memory_flush_manager.is_silent_response(response):
+                    print("📝 Agent 选择不保存记忆")
+                else:
+                    print(f"📝 Agent 已保存记忆")
+
+            except Exception as e:
+                print(f"⚠️ Memory Flush 失败: {e}")
+
+    def _estimate_tokens(self) -> int:
+        """估算当前上下文的 token 数
+
+        使用简单的字符估算方法。
+        对于中文,大约 1.5 字符/token;对于英文,大约 4 字符/token。
+        这里使用保守估算:字符数 / 3。
+
+        Returns:
+            估算的 token 数
+        """
+        total_chars = 0
+
+        # 系统提示词
+        if self._agent.system_prompt:
+            total_chars += len(self._agent.system_prompt)
+
+        # 历史消息
+        for msg in self._agent._history:
+            if msg.content:
+                total_chars += len(msg.content)
+
+        # 保守估算:字符数 / 3
+        return total_chars // 3
+
+    def save_current_session(self):
+        """保存当前会话"""
+        if hasattr(self, '_current_session_id') and self._current_session_id:
+            try:
+                self._agent.save_session(self._current_session_id)
+                return self._current_session_id
+            except Exception as e:
+                print(f"⚠️ 保存会话失败: {e}")
+        return None
+
+    def create_session(self) -> str:
+        """创建新会话"""
+        import uuid
+        session_id = str(uuid.uuid4())[:8]
+        return session_id
+
+    def list_sessions(self) -> List[dict]:
+        """列出所有会话"""
+        sessions_dir = os.path.join(self.workspace_path, "sessions")
+        if not os.path.exists(sessions_dir):
+            return []
+
+        sessions = []
+        for filename in os.listdir(sessions_dir):
+            if filename.endswith(".json"):
+                filepath = os.path.join(sessions_dir, filename)
+                stat = os.stat(filepath)
+                sessions.append({
+                    "id": filename[:-5],
+                    "created_at": stat.st_ctime,
+                    "updated_at": stat.st_mtime,
+                })
+
+        return sorted(sessions, key=lambda x: x["updated_at"], reverse=True)
+
+    def delete_session(self, session_id: str) -> bool:
+        """删除会话"""
+        filepath = os.path.join(self.workspace_path, "sessions", f"{session_id}.json")
+        if os.path.exists(filepath):
+            os.remove(filepath)
+            return True
+        return False
+
+    def get_session_history(self, session_id: str) -> List[dict]:
+        """获取会话历史消息"""
+        import json
+        filepath = os.path.join(self.workspace_path, "sessions", f"{session_id}.json")
+        if not os.path.exists(filepath):
+            return []
+
+        try:
+            with open(filepath, "r", encoding="utf-8") as f:
+                data = json.load(f)
+
+            messages = []
+            raw_history = data.get("history", [])
+            for msg in raw_history:
+                role = msg.get("role", "")
+                # 支持 user, assistant, tool 三种角色
+                if role in ("user", "assistant", "tool"):
+                    content = msg.get("content", "")
+                    if isinstance(content, list):
+                        text_parts = []
+                        for part in content:
+                            if isinstance(part, dict) and part.get("type") == "text":
+                                text_parts.append(part.get("text", ""))
+                            elif isinstance(part, str):
+                                text_parts.append(part)
+                        content = "\n".join(text_parts)
+
+                    # 构建消息对象,包含 metadata
+                    message_obj: dict = {"role": role, "content": content}
+                    # 保留 metadata(包含 tool_calls 或 tool_call_id)
+                    if "metadata" in msg:
+                        message_obj["metadata"] = msg["metadata"]
+
+                    messages.append(message_obj)
+
+            return messages
+        except Exception as e:
+            print(f"Error loading session history: {e}")
+            return []
+
+    def clear_all_history(self):
+        """清除 Agent 内存中的所有历史记录
+
+        用于初始化时重置 Agent 状态。
+        """
+        self._agent.clear_history()
+        self._current_session_id = None
+
+        # 重置 MemoryFlushManager 状态
+        if hasattr(self, '_memory_flush_manager'):
+            self._memory_flush_manager.reset()
+
+        # 重新读取 name(因为 IDENTITY.md 可能已被重置)
+        self.name = self._read_identity_name() or "HelloClaw"

+ 1 - 0
Co-creation-projects/tino-chen-HelloClaw/src/api/__init__.py

@@ -0,0 +1 @@
+"""HelloClaw API 模块"""

+ 158 - 0
Co-creation-projects/tino-chen-HelloClaw/src/api/chat.py

@@ -0,0 +1,158 @@
+"""聊天 API 路由"""
+import json
+from typing import Optional
+from fastapi import APIRouter
+from pydantic import BaseModel
+from sse_starlette.sse import EventSourceResponse
+
+router = APIRouter(prefix="/chat", tags=["chat"])
+
+
+class ChatRequest(BaseModel):
+    """聊天请求"""
+    message: str
+    session_id: Optional[str] = None
+
+
+class ChatResponse(BaseModel):
+    """聊天响应"""
+    content: str
+    session_id: Optional[str] = None
+
+
+def get_agent():
+    """获取全局 Agent 实例"""
+    from ..main import get_agent as _get_agent
+    return _get_agent()
+
+
+@router.post("/send/sync", response_model=ChatResponse)
+async def send_message_sync(request: ChatRequest):
+    """发送消息并获取同步响应"""
+    agent = get_agent()
+    if not agent:
+        return ChatResponse(content="Agent not initialized", session_id=request.session_id)
+
+    response = agent.chat(request.message, request.session_id)
+    return ChatResponse(content=response, session_id=request.session_id)
+
+
+@router.post("/send/stream")
+async def send_message_stream(request: ChatRequest):
+    """发送消息并获取流式响应 (SSE)
+
+    事件类型:
+    - session: 会话信息(包含 session_id)
+    - step_start: 步骤开始
+    - chunk: LLM 文本块
+    - tool_start: 工具调用开始
+    - tool_finish: 工具调用结束
+    - step_finish: 步骤结束
+    - done: 完成
+    - error: 错误
+    """
+
+    async def event_generator():
+        agent = get_agent()
+        if not agent:
+            yield {
+                "event": "error",
+                "data": json.dumps({"error": "Agent not initialized"}, ensure_ascii=False)
+            }
+            return
+
+        try:
+            async for event in agent.achat(request.message, request.session_id):
+                event_type = event.type.value
+                event_data = event.data
+
+                # 处理不同类型的事件
+                if event_type == "agent_start":
+                    # 发送会话信息
+                    session_id = getattr(agent, '_current_session_id', None)
+                    yield {
+                        "event": "session",
+                        "data": json.dumps({"session_id": session_id}, ensure_ascii=False)
+                    }
+
+                elif event_type == "step_start":
+                    # 步骤开始
+                    yield {
+                        "event": "step_start",
+                        "data": json.dumps({
+                            "step": event_data.get("step", 1),
+                            "max_steps": event_data.get("max_steps", 10)
+                        }, ensure_ascii=False)
+                    }
+
+                elif event_type == "llm_chunk":
+                    # LLM 文本块
+                    chunk = event_data.get("chunk", "")
+                    yield {
+                        "event": "chunk",
+                        "data": json.dumps({"content": chunk}, ensure_ascii=False)
+                    }
+
+                elif event_type == "tool_call_start":
+                    # 工具调用开始
+                    yield {
+                        "event": "tool_start",
+                        "data": json.dumps({
+                            "tool": event_data.get("tool_name", ""),
+                            "args": event_data.get("args", {})
+                        }, ensure_ascii=False)
+                    }
+
+                elif event_type == "tool_call_finish":
+                    # 工具调用结束
+                    yield {
+                        "event": "tool_finish",
+                        "data": json.dumps({
+                            "tool": event_data.get("tool_name", ""),
+                            "result": event_data.get("result", "")
+                        }, ensure_ascii=False)
+                    }
+
+                elif event_type == "step_finish":
+                    # 步骤结束
+                    yield {
+                        "event": "step_finish",
+                        "data": json.dumps({
+                            "step": event_data.get("step", 1)
+                        }, ensure_ascii=False)
+                    }
+
+                elif event_type == "agent_finish":
+                    # Agent 完成,保存会话
+                    session_id = agent.save_current_session()
+                    final_content = event_data.get("result", "")
+
+                    yield {
+                        "event": "done",
+                        "data": json.dumps({
+                            "content": final_content,
+                            "session_id": session_id
+                        }, ensure_ascii=False)
+                    }
+
+                elif event_type == "error":
+                    yield {
+                        "event": "error",
+                        "data": json.dumps({"error": event_data.get("error", "Unknown error")}, ensure_ascii=False)
+                    }
+
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+            yield {
+                "event": "error",
+                "data": json.dumps({"error": str(e)}, ensure_ascii=False)
+            }
+
+    return EventSourceResponse(event_generator())
+
+
+@router.post("/send")
+async def send_message(request: ChatRequest):
+    """发送消息(暂返回同步响应)"""
+    return await send_message_sync(request)

+ 190 - 0
Co-creation-projects/tino-chen-HelloClaw/src/api/config.py

@@ -0,0 +1,190 @@
+"""配置 API 路由"""
+import json
+import os
+from fastapi import APIRouter, HTTPException, Depends
+from pydantic import BaseModel
+from typing import Optional
+
+from ..workspace.manager import WorkspaceManager, get_default_global_config
+
+router = APIRouter(prefix="/config", tags=["config"])
+
+
+class ConfigUpdateRequest(BaseModel):
+    """配置更新请求"""
+    content: str
+
+
+class AgentInfo(BaseModel):
+    """助手信息"""
+    name: str
+
+
+# 全局 workspace 实例(由 main.py 在启动时设置)
+_workspace: Optional[WorkspaceManager] = None
+
+
+def set_workspace(ws: WorkspaceManager):
+    """设置全局 workspace 实例"""
+    global _workspace
+    _workspace = ws
+
+
+def get_workspace() -> WorkspaceManager:
+    """获取 workspace 实例"""
+    if _workspace is None:
+        ws = WorkspaceManager(os.getenv("WORKSPACE_PATH", "~/.helloclaw/workspace"))
+        ws.ensure_workspace_exists()
+        set_workspace(ws)
+    return _workspace
+
+
+def get_config_json_path() -> str:
+    """获取全局 config.json 路径"""
+    return os.path.expanduser("~/.helloclaw/config.json")
+
+
+def ensure_config_json_exists():
+    """确保 config.json 存在"""
+    config_path = get_config_json_path()
+    if not os.path.exists(config_path):
+        os.makedirs(os.path.dirname(config_path), exist_ok=True)
+        with open(config_path, "w", encoding="utf-8") as f:
+            json.dump(get_default_global_config(), f, indent=2, ensure_ascii=False)
+
+
+@router.get("/list")
+async def list_configs(ws: WorkspaceManager = Depends(get_workspace)):
+    """获取配置文件列表"""
+    configs = ws.list_configs()
+
+    # 添加 config.json 到列表开头
+    configs.insert(0, "CONFIG")
+    return {"configs": configs}
+
+
+@router.get("/{name}")
+async def get_config(name: str, ws: WorkspaceManager = Depends(get_workspace)):
+    """获取指定配置文件内容"""
+    # 特殊处理 CONFIG (config.json)
+    if name == "CONFIG":
+        ensure_config_json_exists()
+        config_path = get_config_json_path()
+        with open(config_path, "r", encoding="utf-8") as f:
+            content = f.read()
+        return {"name": name, "content": content}
+
+    # 处理 .md 配置文件
+    content = ws.load_config(name)
+    if content is None:
+        raise HTTPException(status_code=404, detail=f"配置文件 {name} 不存在")
+    return {"name": name, "content": content}
+
+
+@router.put("/{name}")
+async def update_config(name: str, request: ConfigUpdateRequest, ws: WorkspaceManager = Depends(get_workspace)):
+    """更新配置文件"""
+    # 特殊处理 CONFIG (config.json)
+    if name == "CONFIG":
+        ensure_config_json_exists()
+        # 严格校验 JSON 格式
+        try:
+            config_data = json.loads(request.content)
+        except json.JSONDecodeError as e:
+            raise HTTPException(status_code=400, detail=f"无效的 JSON 格式: {str(e)}")
+
+        # 校验必需字段
+        if not isinstance(config_data, dict):
+            raise HTTPException(status_code=400, detail="配置必须是 JSON 对象")
+
+        if "llm" not in config_data:
+            raise HTTPException(status_code=400, detail="缺少必需字段: llm")
+
+        llm_config = config_data.get("llm", {})
+        required_fields = ["model_id", "api_key", "base_url"]
+        missing_fields = [f for f in required_fields if f not in llm_config]
+        if missing_fields:
+            raise HTTPException(status_code=400, detail=f"llm 配置缺少必需字段: {', '.join(missing_fields)}")
+
+        config_path = get_config_json_path()
+        with open(config_path, "w", encoding="utf-8") as f:
+            f.write(request.content)
+        return {"name": name, "status": "updated"}
+
+    # 处理 .md 配置文件
+    if name not in ws.list_configs():
+        raise HTTPException(status_code=404, detail=f"配置文件 {name} 不存在")
+
+    ws.save_config(name, request.content)
+    return {"name": name, "status": "updated"}
+
+
+def get_agent():
+    """获取全局 Agent 实例"""
+    from ..main import get_agent as _get_agent
+    return _get_agent()
+
+
+@router.post("/reset")
+async def reset_workspace(
+    reset_sessions: bool = False,
+    reset_memory: bool = False,
+    reset_global_config: bool = False,
+    ws: WorkspaceManager = Depends(get_workspace)
+):
+    """重置工作空间到初始模板
+
+    Args:
+        reset_sessions: 是否清除会话
+        reset_memory: 是否清除每日记忆
+        reset_global_config: 是否重置全局配置
+
+    警告:这将覆盖所有配置文件!
+    """
+    try:
+        ws.reset_to_templates(
+            reset_sessions=reset_sessions,
+            reset_memory=reset_memory,
+            reset_global_config=reset_global_config
+        )
+
+        # 如果清除了会话,也要清除 Agent 内存中的历史记录
+        if reset_sessions:
+            agent = get_agent()
+            if agent:
+                agent.clear_all_history()
+
+        messages = ["配置文件已重置"]
+        if reset_sessions:
+            messages.append("会话已清除")
+        if reset_memory:
+            messages.append("每日记忆已清除")
+        if reset_global_config:
+            messages.append("全局配置已重置")
+
+        return {"status": "success", "message": ",".join(messages)}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"重置失败: {str(e)}")
+
+
+@router.get("/agent/info", response_model=AgentInfo)
+async def get_agent_info(ws: WorkspaceManager = Depends(get_workspace)):
+    """获取助手信息(包括名字)
+
+    每次都重新读取 IDENTITY.md 以获取最新的名字
+    """
+    # 从 IDENTITY.md 读取最新的名字
+    identity = ws.load_config("IDENTITY")
+    name = "HelloClaw"  # 默认名字
+
+    if identity:
+        import re
+        # 匹配格式: - **名称:** xxx 或 - **名称:** xxx
+        match = re.search(r'\*\*名称[::]\*\*\s*(.+?)(?:\n|$)', identity)
+        if match:
+            name = match.group(1).strip()
+            # 检查是否是占位符
+            if name.startswith('_') or '选一个' in name or '(' in name:
+                name = "HelloClaw"
+
+    return AgentInfo(name=name)

+ 255 - 0
Co-creation-projects/tino-chen-HelloClaw/src/api/memory.py

@@ -0,0 +1,255 @@
+"""记忆 API 路由"""
+import os
+from datetime import datetime
+from fastapi import APIRouter, HTTPException, Depends, Query
+from pydantic import BaseModel
+from typing import Optional, List, Dict
+
+from ..workspace.manager import WorkspaceManager
+
+router = APIRouter(prefix="/memory", tags=["memory"])
+
+
+class MemoryEntry(BaseModel):
+    """记忆条目"""
+    date: str
+    filename: str
+    content: str
+    preview: str
+    category: Optional[str] = None
+
+
+class MemoryListResponse(BaseModel):
+    """记忆列表响应"""
+    memories: List[MemoryEntry]
+    total: int
+
+
+class MemoryStatsResponse(BaseModel):
+    """记忆统计响应"""
+    total_files: int
+    daily_files: int
+    total_size: int
+    categories: Dict[str, int]
+
+
+class MemoryCaptureRequest(BaseModel):
+    """记忆捕获请求"""
+    content: str
+    category: str = "fact"  # preference/decision/entity/fact
+
+
+class MemoryCaptureResponse(BaseModel):
+    """记忆捕获响应"""
+    status: str
+    message: str
+    category: str
+
+
+class MemoryCleanupResponse(BaseModel):
+    """记忆清理响应"""
+    status: str
+    deleted: List[str]
+    message: str
+
+
+# 全局 workspace 实例(由 main.py 在启动时设置)
+_workspace: Optional[WorkspaceManager] = None
+
+
+def set_workspace(ws: WorkspaceManager):
+    """设置全局 workspace 实例"""
+    global _workspace
+    _workspace = ws
+
+
+def get_workspace() -> WorkspaceManager:
+    """获取 workspace 实例"""
+    if _workspace is None:
+        ws = WorkspaceManager(os.getenv("WORKSPACE_PATH", "~/.helloclaw/workspace"))
+        ws.ensure_workspace_exists()
+        set_workspace(ws)
+    return _workspace
+
+
+def get_preview(content: str, max_length: int = 100) -> str:
+    """获取内容预览"""
+    # 移除 markdown 标记,获取纯文本预览
+    lines = content.strip().split('\n')
+    for line in lines:
+        line = line.strip()
+        if line and not line.startswith('#'):
+            return line[:max_length] + ('...' if len(line) > max_length else '')
+    return '(空)'
+
+
+# ==================== 静态路由(必须在 /{filename} 之前)====================
+
+
+@router.get("/list", response_model=MemoryListResponse)
+async def list_memories(
+    category: Optional[str] = Query(None, description="按分类过滤"),
+    ws: WorkspaceManager = Depends(get_workspace)
+):
+    """获取每日记忆列表(支持分类过滤)
+
+    Args:
+        category: 分类标签(preference/decision/entity/fact),可选
+    """
+    import re
+
+    memories = []
+
+    if os.path.exists(ws.memory_path):
+        files = sorted(
+            [f for f in os.listdir(ws.memory_path) if f.endswith('.md')],
+            reverse=True  # 最新的在前面
+        )
+
+        for filename in files:
+            filepath = os.path.join(ws.memory_path, filename)
+            with open(filepath, 'r', encoding='utf-8') as f:
+                content = f.read()
+
+            # 如果指定了分类,检查是否包含该分类的标签
+            if category:
+                pattern = rf'\[{category}\]'
+                if not re.search(pattern, content, re.IGNORECASE):
+                    continue
+
+            # 从文件名提取日期 (YYYY-MM-DD.md)
+            date = filename.replace('.md', '')
+
+            memories.append(MemoryEntry(
+                date=date,
+                filename=filename,
+                content=content,
+                preview=get_preview(content),
+                category=category
+            ))
+
+    return MemoryListResponse(memories=memories, total=len(memories))
+
+
+@router.get("/stats", response_model=MemoryStatsResponse)
+async def get_memory_stats(ws: WorkspaceManager = Depends(get_workspace)):
+    """获取记忆统计"""
+    import re
+
+    total_files = 0
+    daily_files = 0
+    total_size = 0
+    categories = {
+        "preference": 0,
+        "decision": 0,
+        "entity": 0,
+        "fact": 0,
+    }
+
+    # 统计每日记忆
+    if os.path.exists(ws.memory_path):
+        for filename in os.listdir(ws.memory_path):
+            if filename.endswith('.md'):
+                filepath = os.path.join(ws.memory_path, filename)
+                total_files += 1
+                daily_files += 1
+                total_size += os.path.getsize(filepath)
+
+                # 统计各分类标签数量
+                with open(filepath, 'r', encoding='utf-8') as f:
+                    content = f.read()
+                for cat in categories:
+                    pattern = rf'\[{cat}\]'
+                    count = len(re.findall(pattern, content, re.IGNORECASE))
+                    categories[cat] += count
+
+    # 统计长期记忆
+    longterm_path = ws.get_config_path("MEMORY")
+    if os.path.exists(longterm_path):
+        total_files += 1
+        total_size += os.path.getsize(longterm_path)
+
+    return MemoryStatsResponse(
+        total_files=total_files,
+        daily_files=daily_files,
+        total_size=total_size,
+        categories=categories
+    )
+
+
+@router.post("/today")
+async def add_to_today(content: str, ws: WorkspaceManager = Depends(get_workspace)):
+    """添加内容到今日记忆"""
+    ws.append_to_daily_memory(content)
+    return {"status": "ok", "message": "已添加到今日记忆"}
+
+
+@router.post("/capture", response_model=MemoryCaptureResponse)
+async def capture_memory(
+    request: MemoryCaptureRequest,
+    ws: WorkspaceManager = Depends(get_workspace)
+):
+    """手动添加记忆(带分类)"""
+    # 验证分类
+    valid_categories = ["preference", "decision", "entity", "fact"]
+    if request.category not in valid_categories:
+        raise HTTPException(
+            status_code=400,
+            detail=f"无效的分类: {request.category},有效值: {valid_categories}"
+        )
+
+    # 检查重复
+    if ws.check_duplicate_memory(request.content, threshold=0.7):
+        return MemoryCaptureResponse(
+            status="skipped",
+            message="记忆已存在,跳过",
+            category=request.category
+        )
+
+    # 存储记忆
+    ws.append_classified_memory(request.content, request.category)
+
+    return MemoryCaptureResponse(
+        status="ok",
+        message=f"已添加 [{request.category}] 记忆",
+        category=request.category
+    )
+
+
+@router.post("/cleanup", response_model=MemoryCleanupResponse)
+async def cleanup_memories(
+    days: int = Query(30, description="保留天数"),
+    ws: WorkspaceManager = Depends(get_workspace)
+):
+    """清理过期记忆"""
+    deleted = ws.cleanup_old_memories(days)
+
+    return MemoryCleanupResponse(
+        status="ok",
+        deleted=deleted,
+        message=f"已清理 {len(deleted)} 个过期记忆文件"
+    )
+
+
+# ==================== 动态路由(必须放在最后)====================
+
+
+@router.get("/{filename}")
+async def get_memory(filename: str, ws: WorkspaceManager = Depends(get_workspace)):
+    """获取指定日期的记忆内容"""
+    if not filename.endswith('.md'):
+        filename += '.md'
+
+    filepath = os.path.join(ws.memory_path, filename)
+
+    if not os.path.exists(filepath):
+        raise HTTPException(status_code=404, detail=f"记忆文件 {filename} 不存在")
+
+    with open(filepath, 'r', encoding='utf-8') as f:
+        content = f.read()
+
+    return {
+        "filename": filename,
+        "date": filename.replace('.md', ''),
+        "content": content
+    }

+ 329 - 0
Co-creation-projects/tino-chen-HelloClaw/src/api/session.py

@@ -0,0 +1,329 @@
+"""会话 API 路由"""
+import json
+from fastapi import APIRouter, HTTPException
+from pydantic import BaseModel
+from typing import List, Optional, Dict, Any, Union, Literal
+
+router = APIRouter(prefix="/session", tags=["session"])
+
+
+class SessionInfo(BaseModel):
+    """会话信息"""
+    id: str
+    created_at: float
+    updated_at: float
+
+
+class SessionListResponse(BaseModel):
+    """会话列表响应"""
+    sessions: List[SessionInfo]
+
+
+class SessionCreateRequest(BaseModel):
+    """创建会话请求"""
+    summarize_old: bool = False  # 是否总结旧会话
+    old_session_id: Optional[str] = None  # 要总结的旧会话 ID
+
+
+class SessionCreateResponse(BaseModel):
+    """创建会话响应"""
+    session_id: str
+    message: str = "Session created successfully"
+    summary_file: Optional[str] = None  # 如果总结了旧会话,返回总结文件名
+
+
+class SessionSummaryInfo(BaseModel):
+    """会话总结信息"""
+    filename: str
+    date: str
+    slug: str
+    size: int
+    updated_at: float
+
+
+class SessionSummaryListResponse(BaseModel):
+    """会话总结列表响应"""
+    summaries: List[SessionSummaryInfo]
+
+
+# ==================== OpenAI 标准消息格式 ====================
+
+class ToolCallFunction(BaseModel):
+    """工具调用函数"""
+    name: str
+    arguments: str  # JSON 字符串
+
+
+class ToolCall(BaseModel):
+    """工具调用"""
+    id: str
+    type: Literal["function"] = "function"
+    function: ToolCallFunction
+
+
+class ChatMessage(BaseModel):
+    """聊天消息(OpenAI 标准格式)"""
+    role: Literal["user", "assistant", "tool"]
+    content: Optional[str] = None
+    tool_calls: Optional[List[ToolCall]] = None  # assistant 消息中的工具调用
+    tool_call_id: Optional[str] = None  # tool 消息中的调用 ID
+
+
+class SessionHistoryResponse(BaseModel):
+    """会话历史响应"""
+    session_id: str
+    messages: List[ChatMessage]
+
+
+def get_agent():
+    """获取全局 Agent 实例"""
+    from ..main import get_agent as _get_agent
+    return _get_agent()
+
+
+@router.get("/list", response_model=SessionListResponse)
+async def list_sessions():
+    """获取会话列表
+
+    返回所有会话,按更新时间倒序排列
+    """
+    agent = get_agent()
+    if not agent:
+        return SessionListResponse(sessions=[])
+
+    sessions = agent.list_sessions()
+    return SessionListResponse(sessions=[
+        SessionInfo(
+            id=s["id"],
+            created_at=s["created_at"],
+            updated_at=s["updated_at"]
+        )
+        for s in sessions
+    ])
+
+
+@router.post("/create", response_model=SessionCreateResponse)
+async def create_session(request: SessionCreateRequest = None):
+    """创建新会话
+
+    可选参数:
+    - summarize_old: 是否在创建新会话前总结旧会话
+    - old_session_id: 要总结的旧会话 ID(如果不指定,则总结最近一个会话)
+
+    返回新会话的 ID
+    """
+    agent = get_agent()
+    if not agent:
+        raise HTTPException(status_code=500, detail="Agent not initialized")
+
+    request = request or SessionCreateRequest()
+    summary_file = None
+
+    # 如果需要总结旧会话
+    if request.summarize_old:
+        old_session_id = request.old_session_id
+
+        # 如果没有指定旧会话,找最近的一个
+        if not old_session_id:
+            sessions = agent.list_sessions()
+            if sessions:
+                old_session_id = sessions[0]["id"]
+
+        # 总结旧会话
+        if old_session_id:
+            summary_file = await _summarize_session(agent, old_session_id)
+
+    # 创建新会话
+    session_id = agent.create_session()
+
+    return SessionCreateResponse(
+        session_id=session_id,
+        summary_file=summary_file,
+        message="Session created successfully" + (f", old session summarized to {summary_file}" if summary_file else "")
+    )
+
+
+async def _summarize_session(agent, session_id: str) -> Optional[str]:
+    """总结指定会话
+
+    Args:
+        agent: Agent 实例
+        session_id: 会话 ID
+
+    Returns:
+        总结文件名,如果失败返回 None
+    """
+    try:
+        from ..memory import SessionSummarizer
+
+        # 获取会话历史
+        messages = agent.get_session_history(session_id)
+        if not messages:
+            return None
+
+        # 创建总结器
+        summarizer = SessionSummarizer(
+            workspace_manager=agent.workspace,
+            model_id=agent._model_id,
+            api_key=agent._api_key,
+            base_url=agent._base_url,
+        )
+
+        # 执行总结
+        summary_file = await summarizer.summarize_session(
+            messages=messages,
+            last_n=10,
+            session_id=session_id,
+        )
+
+        return summary_file
+
+    except Exception as e:
+        print(f"⚠️ 会话总结失败: {e}")
+        return None
+
+
+@router.get("/{session_id}")
+async def get_session(session_id: str):
+    """获取会话详情
+
+    返回会话的基本信息
+    """
+    agent = get_agent()
+    if not agent:
+        raise HTTPException(status_code=500, detail="Agent not initialized")
+
+    sessions = agent.list_sessions()
+    for s in sessions:
+        if s["id"] == session_id:
+            return SessionInfo(
+                id=s["id"],
+                created_at=s["created_at"],
+                updated_at=s["updated_at"]
+            )
+
+    raise HTTPException(status_code=404, detail="Session not found")
+
+
+@router.get("/{session_id}/history", response_model=SessionHistoryResponse)
+async def get_session_history(session_id: str):
+    """获取会话历史消息
+
+    返回会话的所有聊天记录,按照 OpenAI 标准格式
+    """
+    agent = get_agent()
+    if not agent:
+        raise HTTPException(status_code=500, detail="Agent not initialized")
+
+    raw_messages = agent.get_session_history(session_id)
+    if raw_messages is None:
+        raw_messages = []
+
+    # 转换为 OpenAI 标准格式
+    chat_messages: List[ChatMessage] = []
+
+    for m in raw_messages:
+        role = m.get("role", "")
+        content = m.get("content", "")
+        metadata = m.get("metadata", {})
+
+        if role == "user":
+            chat_messages.append(ChatMessage(role="user", content=content))
+
+        elif role == "assistant":
+            tool_calls_data = metadata.get("tool_calls")
+            if tool_calls_data:
+                # 包含工具调用的 assistant 消息
+                tool_calls = [
+                    ToolCall(
+                        id=tc.get("id", ""),
+                        type="function",
+                        function=ToolCallFunction(
+                            name=tc.get("function", {}).get("name", ""),
+                            arguments=tc.get("function", {}).get("arguments", "{}")
+                        )
+                    )
+                    for tc in tool_calls_data
+                ]
+                chat_messages.append(ChatMessage(
+                    role="assistant",
+                    content=content if content else None,
+                    tool_calls=tool_calls
+                ))
+            elif content:
+                # 普通的 assistant 文本消息
+                chat_messages.append(ChatMessage(role="assistant", content=content))
+
+        elif role == "tool":
+            # tool 消息
+            tool_call_id = metadata.get("tool_call_id")
+            chat_messages.append(ChatMessage(
+                role="tool",
+                content=content,
+                tool_call_id=tool_call_id
+            ))
+
+    return SessionHistoryResponse(
+        session_id=session_id,
+        messages=chat_messages
+    )
+
+
+@router.delete("/{session_id}")
+async def delete_session(session_id: str):
+    """删除会话
+
+    删除指定会话及其历史记录
+    """
+    agent = get_agent()
+    if not agent:
+        raise HTTPException(status_code=500, detail="Agent not initialized")
+
+    success = agent.delete_session(session_id)
+    if success:
+        return {"message": "Session deleted successfully", "session_id": session_id}
+
+    raise HTTPException(status_code=404, detail="Session not found")
+
+
+# ==================== 会话总结 API ====================
+
+@router.get("/summaries/list", response_model=SessionSummaryListResponse)
+async def list_session_summaries():
+    """获取所有会话总结列表
+
+    返回按日期倒序排列的会话总结
+    """
+    agent = get_agent()
+    if not agent:
+        return SessionSummaryListResponse(summaries=[])
+
+    summaries = agent.workspace.list_session_summaries()
+    return SessionSummaryListResponse(summaries=[
+        SessionSummaryInfo(
+            filename=s["filename"],
+            date=s["date"],
+            slug=s["slug"],
+            size=s["size"],
+            updated_at=s["updated_at"]
+        )
+        for s in summaries
+    ])
+
+
+@router.get("/summaries/{filename}")
+async def get_session_summary(filename: str):
+    """获取会话总结内容
+
+    Args:
+        filename: 总结文件名
+    """
+    agent = get_agent()
+    if not agent:
+        raise HTTPException(status_code=500, detail="Agent not initialized")
+
+    content = agent.workspace.load_session_summary(filename)
+    if content is None:
+        raise HTTPException(status_code=404, detail="Summary not found")
+
+    return {"filename": filename, "content": content}

+ 5 - 0
Co-creation-projects/tino-chen-HelloClaw/src/channels/__init__.py

@@ -0,0 +1,5 @@
+"""HelloClaw Channels 模块"""
+
+from .cli_channel import CLIChannel
+
+__all__ = ["CLIChannel"]

+ 226 - 0
Co-creation-projects/tino-chen-HelloClaw/src/channels/cli_channel.py

@@ -0,0 +1,226 @@
+"""CLI Channel - 命令行交互渠道
+
+提供 REPL 交互循环,支持:
+- 多轮对话
+- 流式输出
+- 退出命令
+- 丰富的终端输出
+"""
+
+import asyncio
+import sys
+from typing import Optional, TYPE_CHECKING
+
+from rich.console import Console
+from rich.markdown import Markdown
+from rich.panel import Panel
+from rich.prompt import Prompt
+from rich.live import Live
+from rich.text import Text
+
+if TYPE_CHECKING:
+    from ..agent.helloclaw_agent import HelloClawAgent
+
+
+class CLIChannel:
+    """CLI 交互渠道
+
+    实现 REPL 交互循环,处理用户输入和 Agent 输出。
+
+    Attributes:
+        agent: HelloClaw Agent 实例
+        session_id: 当前会话 ID
+        console: Rich Console 实例
+    """
+
+    # 退出命令
+    EXIT_COMMANDS = {"exit", "quit", "q", "bye", "退出"}
+
+    # 帮助命令
+    HELP_COMMANDS = {"help", "h", "帮助", "?"}
+
+    # 清屏命令
+    CLEAR_COMMANDS = {"clear", "cls", "清屏"}
+
+    def __init__(
+        self,
+        agent: "HelloClawAgent",
+        session_id: Optional[str] = None,
+    ):
+        """初始化 CLI Channel
+
+        Args:
+            agent: HelloClaw Agent 实例
+            session_id: 会话 ID(可选,默认创建新会话)
+        """
+        self.agent = agent
+        self.session_id = session_id
+        self.console = Console()
+
+        # 运行状态
+        self._running = False
+
+    async def run(self):
+        """启动 REPL 交互循环"""
+        self._running = True
+
+        # 打印欢迎信息
+        self._print_welcome()
+
+        # 主循环
+        while self._running:
+            try:
+                # 获取用户输入
+                user_input = await self._get_input()
+
+                if user_input is None:
+                    # 用户输入为空(可能是 EOF)
+                    break
+
+                # 处理命令
+                if not self._handle_command(user_input):
+                    # 不是命令,发送给 Agent
+                    await self._chat(user_input)
+
+            except KeyboardInterrupt:
+                self.console.print("\n[yellow]收到中断信号,输入 'exit' 退出[/yellow]")
+            except EOFError:
+                self.console.print("\n[yellow]再见![/yellow]")
+                break
+            except Exception as e:
+                self.console.print(f"[red]错误: {e}[/red]")
+
+        # 打印告别信息
+        self._print_goodbye()
+
+    async def _get_input(self) -> Optional[str]:
+        """获取用户输入
+
+        Returns:
+            用户输入的文本,如果为空或 EOF 则返回 None
+        """
+        try:
+            # 使用 Prompt 获取输入
+            user_input = Prompt.ask("\n[bold cyan]你[/bold cyan]")
+
+            # 去除首尾空白
+            user_input = user_input.strip()
+
+            # 空输入
+            if not user_input:
+                return None
+
+            return user_input
+
+        except (KeyboardInterrupt, EOFError):
+            return None
+
+    def _handle_command(self, input_text: str) -> bool:
+        """处理特殊命令
+
+        Args:
+            input_text: 用户输入
+
+        Returns:
+            是否是命令(True = 已处理,False = 不是命令)
+        """
+        # 转小写比较
+        cmd = input_text.lower().strip()
+
+        # 退出命令
+        if cmd in self.EXIT_COMMANDS:
+            self._running = False
+            return True
+
+        # 帮助命令
+        if cmd in self.HELP_COMMANDS:
+            self._print_help()
+            return True
+
+        # 清屏命令
+        if cmd in self.CLEAR_COMMANDS:
+            self.console.clear()
+            self._print_welcome(compact=True)
+            return True
+
+        # 不是命令
+        return False
+
+    async def _chat(self, message: str):
+        """与 Agent 对话
+
+        Args:
+            message: 用户消息
+        """
+        # 显示 Agent 正在思考
+        with self.console.status("[bold green]思考中...[/bold green]"):
+            # 收集响应
+            response_text = Text()
+
+            try:
+                # 流式获取响应
+                async for event in self.agent.achat(message, session_id=self.session_id):
+                    event_type = event.type.value
+
+                    if event_type == "llm_chunk":
+                        # 文本块
+                        chunk = event.chunk or ""
+                        response_text.append(chunk)
+                        # 实时输出
+                        self.console.print(chunk, end="")
+
+                    elif event_type == "tool_call_start":
+                        # 工具调用开始
+                        tool_name = getattr(event, "tool_name", "unknown")
+                        self.console.print(f"\n[dim]🔧 调用工具: {tool_name}...[/dim]")
+
+                    elif event_type == "tool_call_finish":
+                        # 工具调用完成
+                        pass  # 静默处理
+
+                    elif event_type == "agent_finish":
+                        # 对话完成
+                        if hasattr(event, "result") and event.result:
+                            # 确保换行
+                            self.console.print()
+
+                # 保存会话 ID
+                if hasattr(self.agent, "_current_session_id"):
+                    self.session_id = self.agent._current_session_id
+
+            except Exception as e:
+                self.console.print(f"\n[red]❌ Agent 错误: {e}[/red]")
+
+    def _print_welcome(self, compact: bool = False):
+        """打印欢迎信息"""
+        if compact:
+            self.console.print(Panel(
+                f"[bold]{self.agent.name}[/bold] - 你的个性化 AI 助手",
+                border_style="blue"
+            ))
+        else:
+            self.console.print(Panel(
+                f"[bold]{self.agent.name}[/bold] - 你的个性化 AI 助手\n\n"
+                "[dim]输入消息开始对话[/dim]\n"
+                "[dim]输入 'help' 查看帮助,'exit' 退出[/dim]",
+                title="HelloClaw",
+                border_style="blue"
+            ))
+
+    def _print_goodbye(self):
+        """打印告别信息"""
+        self.console.print("\n[bold blue]再见!期待下次见到你 👋[/bold blue]\n")
+
+    def _print_help(self):
+        """打印帮助信息"""
+        help_text = """[bold]可用命令:[/bold]
+
+[cyan]exit, quit, q[/cyan]  - 退出对话
+[cyan]help, h, ?[/cyan]     - 显示帮助
+[cyan]clear, cls[/cyan]     - 清屏
+
+[bold]提示:[/bold]
+- 直接输入消息与 AI 对话
+- 支持多轮对话,上下文会被保留
+- 使用 Ctrl+C 可以中断当前操作"""
+        self.console.print(Panel(help_text, title="帮助", border_style="green"))

+ 1 - 0
Co-creation-projects/tino-chen-HelloClaw/src/cli/__init__.py

@@ -0,0 +1 @@
+"""HelloClaw CLI 模块"""

+ 222 - 0
Co-creation-projects/tino-chen-HelloClaw/src/cli/main.py

@@ -0,0 +1,222 @@
+"""HelloClaw CLI 入口
+
+使用 click 实现命令行接口。
+"""
+
+import os
+import asyncio
+from typing import Optional
+
+import click
+from rich.console import Console
+from rich.markdown import Markdown
+from rich.panel import Panel
+from rich.prompt import Prompt
+
+# 禁用 PYTHONSTARTUP 以避免 I/O 问题
+os.environ.pop("PYTHONSTARTUP", None)
+
+console = Console()
+
+
+@click.group()
+@click.version_option(version="0.1.0", prog_name="helloclaw")
+def cli():
+    """HelloClaw - 你的个性化 AI 助手"""
+    pass
+
+
+@cli.command()
+@click.option("--session", "-s", "session_id", default=None, help="指定会话 ID")
+@click.option("--workspace", "-w", default=None, help="指定工作空间路径")
+def chat(session_id: Optional[str], workspace: Optional[str]):
+    """启动交互式对话(REPL 模式)"""
+    from ..channels.cli_channel import CLIChannel
+    from ..agent.helloclaw_agent import HelloClawAgent
+    from ..workspace.manager import WorkspaceManager
+
+    # 确定工作空间路径
+    workspace_path = workspace or os.getenv("WORKSPACE_PATH", "~/.helloclaw/workspace")
+
+    # 初始化工作空间
+    ws = WorkspaceManager(workspace_path)
+    ws.ensure_workspace_exists()
+
+    # 初始化 Agent
+    try:
+        agent = HelloClawAgent(workspace_path=workspace_path)
+    except Exception as e:
+        console.print(f"[red]❌ 初始化 Agent 失败: {e}[/red]")
+        raise SystemExit(1)
+
+    # 启动 CLI Channel
+    channel = CLIChannel(agent, session_id=session_id)
+    asyncio.run(channel.run())
+
+
+@cli.command()
+@click.argument("question")
+@click.option("--session", "-s", "session_id", default=None, help="指定会话 ID")
+@click.option("--workspace", "-w", default=None, help="指定工作空间路径")
+@click.option("--no-stream", is_flag=True, help="禁用流式输出")
+def ask(question: str, session_id: Optional[str], workspace: Optional[str], no_stream: bool):
+    """单次提问,输出结果后退出"""
+    from ..agent.helloclaw_agent import HelloClawAgent
+    from ..workspace.manager import WorkspaceManager
+
+    # 确定工作空间路径
+    workspace_path = workspace or os.getenv("WORKSPACE_PATH", "~/.helloclaw/workspace")
+
+    # 初始化工作空间
+    ws = WorkspaceManager(workspace_path)
+    ws.ensure_workspace_exists()
+
+    # 初始化 Agent
+    try:
+        agent = HelloClawAgent(workspace_path=workspace_path)
+    except Exception as e:
+        console.print(f"[red]❌ 初始化 Agent 失败: {e}[/red]")
+        raise SystemExit(1)
+
+    if no_stream:
+        # 同步模式
+        response = agent.chat(question, session_id=session_id)
+        console.print(Markdown(response))
+    else:
+        # 流式模式
+        async def run_stream():
+            async for event in agent.achat(question, session_id=session_id):
+                if event.type.value == "llm_chunk":
+                    console.print(event.chunk, end="")
+
+        asyncio.run(run_stream())
+        console.print()  # 换行
+
+
+@cli.command()
+@click.argument("key", required=False)
+@click.argument("value", required=False)
+@click.option("--workspace", "-w", default=None, help="指定工作空间路径")
+@click.option("--list", "-l", "list_all", is_flag=True, help="列出所有配置")
+@click.option("--edit", "-e", is_flag=True, help="用编辑器打开配置文件")
+def config(key: Optional[str], value: Optional[str], workspace: Optional[str], list_all: bool, edit: bool):
+    """配置管理
+
+    用法:
+      helloclaw config              # 显示所有配置
+      helloclaw config model_id     # 显示指定配置项
+      helloclaw config model_id glm-4  # 设置配置项
+      helloclaw config --edit       # 用编辑器打开配置文件
+    """
+    from ..workspace.manager import WorkspaceManager
+
+    workspace_path = workspace or os.getenv("WORKSPACE_PATH", "~/.helloclaw/workspace")
+    ws = WorkspaceManager(workspace_path)
+    ws.ensure_workspace_exists()
+
+    config_path = os.path.join(ws.workspace_path, "config.json")
+
+    if edit:
+        # 用编辑器打开配置文件
+        editor = os.getenv("EDITOR", "nano")
+        os.system(f"{editor} {config_path}")
+        return
+
+    # 读取配置
+    llm_config = ws.get_llm_config()
+
+    if list_all or (key is None and value is None):
+        # 显示所有配置
+        console.print(Panel(
+            "\n".join([f"[cyan]{k}:[/cyan] {v}" for k, v in llm_config.items()]) or "[dim]暂无配置[/dim]",
+            title="HelloClaw 配置",
+            border_style="blue"
+        ))
+    elif key and value is None:
+        # 显示单个配置项
+        if key in llm_config:
+            console.print(f"[cyan]{key}:[/cyan] {llm_config[key]}")
+        else:
+            console.print(f"[yellow]配置项 '{key}' 不存在[/yellow]")
+    elif key and value:
+        # 设置配置项
+        import json
+        llm_config[key] = value
+        with open(config_path, "w", encoding="utf-8") as f:
+            json.dump(llm_config, f, indent=2, ensure_ascii=False)
+        console.print(f"[green]✓[/green] 已设置 {key} = {value}")
+
+
+@cli.command()
+@click.option("--workspace", "-w", default=None, help="指定工作空间路径")
+@click.option("--list", "-l", "list_all", is_flag=True, help="列出所有会话")
+@click.option("--delete", "-d", "delete_id", default=None, help="删除指定会话")
+@click.option("--clear", is_flag=True, help="清除所有会话")
+def sessions(workspace: Optional[str], list_all: bool, delete_id: Optional[str], clear: bool):
+    """会话管理
+
+    用法:
+      helloclaw sessions           # 列出所有会话
+      helloclaw sessions --list    # 列出所有会话
+      helloclaw sessions --delete <id>  # 删除指定会话
+      helloclaw sessions --clear   # 清除所有会话
+    """
+    from ..workspace.manager import WorkspaceManager
+    from datetime import datetime
+    import glob
+
+    workspace_path = workspace or os.getenv("WORKSPACE_PATH", "~/.helloclaw/workspace")
+    ws = WorkspaceManager(workspace_path)
+    ws.ensure_workspace_exists()
+
+    sessions_dir = os.path.join(ws.workspace_path, "sessions")
+    os.makedirs(sessions_dir, exist_ok=True)
+
+    if delete_id:
+        # 删除指定会话
+        filepath = os.path.join(sessions_dir, f"{delete_id}.json")
+        if os.path.exists(filepath):
+            os.remove(filepath)
+            console.print(f"[green]✓[/green] 已删除会话: {delete_id}")
+        else:
+            console.print(f"[red]✗[/red] 会话不存在: {delete_id}")
+    elif clear:
+        # 清除所有会话
+        session_files = glob.glob(os.path.join(sessions_dir, "*.json"))
+        if session_files:
+            for f in session_files:
+                os.remove(f)
+            console.print(f"[green]✓[/green] 已清除 {len(session_files)} 个会话")
+        else:
+            console.print("[yellow]没有会话需要清除[/yellow]")
+    else:
+        # 列出所有会话
+        session_files = glob.glob(os.path.join(sessions_dir, "*.json"))
+        if not session_files:
+            console.print("[dim]暂无会话[/dim]")
+            return
+
+        # 按修改时间排序
+        session_list = []
+        for filepath in session_files:
+            stat = os.stat(filepath)
+            session_id = os.path.basename(filepath)[:-5]  # 去掉 .json
+            session_list.append({
+                "id": session_id,
+                "updated_at": stat.st_mtime,
+            })
+
+        session_list.sort(key=lambda x: x["updated_at"], reverse=True)
+
+        for s in session_list:
+            updated = datetime.fromtimestamp(s["updated_at"]).strftime("%Y-%m-%d %H:%M")
+            console.print(f"[cyan]{s['id']}[/cyan] - {updated}")
+
+
+def main():
+    """CLI 主入口"""
+    cli()
+
+
+if __name__ == "__main__":
+    main()

+ 100 - 0
Co-creation-projects/tino-chen-HelloClaw/src/main.py

@@ -0,0 +1,100 @@
+"""
+HelloClaw Backend - FastAPI 入口
+"""
+import os
+
+# 禁用 PYTHONSTARTUP 以避免 I/O 问题
+os.environ.pop("PYTHONSTARTUP", None)
+
+from contextlib import asynccontextmanager
+from dotenv import load_dotenv
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from .api import chat, session, config, memory
+from .workspace.manager import WorkspaceManager
+from .agent.helloclaw_agent import HelloClawAgent
+
+# 加载环境变量
+load_dotenv()
+
+# 全局 Agent 实例
+_agent: HelloClawAgent = None
+
+
+def get_agent() -> HelloClawAgent:
+    """获取全局 Agent 实例"""
+    global _agent
+    return _agent
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    """应用生命周期管理"""
+    global _agent
+
+    # 启动时初始化
+    print("HelloClaw Backend starting...")
+
+    # 初始化工作空间
+    workspace_path = os.getenv("WORKSPACE_PATH", "~/.helloclaw/workspace")
+    workspace = WorkspaceManager(workspace_path)
+    workspace.ensure_workspace_exists()
+    print(f"Workspace initialized at: {workspace.workspace_path}")
+
+    # 设置全局 workspace 实例
+    config.set_workspace(workspace)
+    memory.set_workspace(workspace)
+
+    # 初始化全局 Agent 实例
+    _agent = HelloClawAgent(workspace_path=workspace_path)
+    print("HelloClawAgent initialized")
+
+    yield
+    # 关闭时清理
+    print("HelloClaw Backend shutting down...")
+
+
+app = FastAPI(
+    title="HelloClaw API",
+    description="AI Agent powered by HelloAgents",
+    version="0.1.0",
+    lifespan=lifespan,
+)
+
+# CORS 配置
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=os.getenv("CORS_ORIGINS", "http://localhost:5173").split(","),
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+
+# 健康检查
+@app.get("/health")
+async def health_check():
+    return {"status": "ok", "service": "helloclaw-backend"}
+
+
+# 注册 API 路由
+app.include_router(chat.router, prefix="/api")
+app.include_router(session.router, prefix="/api")
+app.include_router(config.router, prefix="/api")
+app.include_router(memory.router, prefix="/api")
+
+
+@app.get("/api")
+async def api_root():
+    return {"message": "HelloClaw API v0.1.0"}
+
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run(
+        "src.main:app",
+        host="0.0.0.0",
+        port=int(os.getenv("PORT", 8000)),
+        reload=True,
+    )

+ 6 - 0
Co-creation-projects/tino-chen-HelloClaw/src/memory/__init__.py

@@ -0,0 +1,6 @@
+"""记忆系统模块"""
+
+from .session_summarizer import SessionSummarizer
+from .memory_flush import MemoryFlushManager
+
+__all__ = ["SessionSummarizer", "MemoryFlushManager"]

+ 273 - 0
Co-creation-projects/tino-chen-HelloClaw/src/memory/capture.py

@@ -0,0 +1,273 @@
+"""记忆捕获管理器 - 自动识别并存储对话中的重要信息"""
+
+import asyncio
+import re
+from datetime import datetime
+from typing import List, Optional, Tuple
+
+
+# 记忆触发规则
+MEMORY_TRIGGERS = [
+    # 明确要求记住
+    (r"记住|记下|remember|keep in mind", "fact"),
+    # 偏好表达
+    (r"我喜欢|我偏好|prefer|like|love|hate|讨厌|不喜欢", "preference"),
+    # 决策记录
+    (r"决定了|decision|用这个|选定|确定用|就用", "decision"),
+    # 电话号码
+    (r"\+\d{10,}|\d{3,4}[-\s]?\d{7,8}", "entity"),
+    # 邮箱地址
+    (r"[\w.-]+@[\w.-]+\.\w+", "entity"),
+    # 实体信息(我的xxx是)
+    (r"我的\w+是|is my|我的电话|我的邮箱|我的地址|我的名字", "entity"),
+    # 事实陈述
+    (r"事实上|实际上|the fact is|it turns out", "fact"),
+]
+
+# 分类关键词(用于辅助分类)
+CATEGORY_KEYWORDS = {
+    "preference": ["喜欢", "偏好", "prefer", "like", "love", "hate", "讨厌", "不喜欢", "习惯", "习惯于"],
+    "decision": ["决定", "选定", "用这个", "确定", "choose", "decide", "decision"],
+    "entity": ["电话", "邮箱", "地址", "名字", "账号", "phone", "email", "address", "account"],
+    "fact": ["记住", "记下", "事实", "实际上", "remember", "fact"],
+}
+
+
+class MemoryCaptureManager:
+    """记忆捕获管理器
+
+    负责在对话结束后自动识别值得记忆的信息,并进行分类和去重。
+
+    使用方式:
+        manager = MemoryCaptureManager(workspace_manager)
+        memories = manager.capture("用户:我喜欢简洁的回复风格")
+        # 返回: [{"content": "用户喜欢简洁的回复风格", "category": "preference"}]
+    """
+
+    def __init__(self, workspace_manager):
+        """初始化记忆捕获管理器
+
+        Args:
+            workspace_manager: WorkspaceManager 实例
+        """
+        self.workspace = workspace_manager
+        # 编译正则表达式
+        self._compiled_patterns = [
+            (re.compile(pattern, re.IGNORECASE), category)
+            for pattern, category in MEMORY_TRIGGERS
+        ]
+
+    def capture(self, text: str) -> List[dict]:
+        """分析文本并捕获值得记忆的信息
+
+        Args:
+            text: 要分析的文本(通常是用户消息或对话摘要)
+
+        Returns:
+            捕获到的记忆列表,每项包含 content 和 category
+        """
+        memories = []
+        seen_contents = set()  # 用于去重
+
+        # 按句子分割
+        sentences = self._split_sentences(text)
+
+        for sentence in sentences:
+            sentence = sentence.strip()
+            if not sentence or len(sentence) < 5:
+                continue
+
+            # 检查是否匹配触发规则
+            category = self._match_trigger(sentence)
+            if not category:
+                continue
+
+            # 提取记忆内容
+            content = self._extract_memory(sentence, category)
+            if not content:
+                continue
+
+            # 去重检查
+            content_key = content.lower().strip()
+            if content_key in seen_contents:
+                continue
+
+            # 检查是否与已有记忆重复
+            if self.workspace.check_duplicate_memory(content, threshold=0.7):
+                continue
+
+            seen_contents.add(content_key)
+            memories.append({
+                "content": content,
+                "category": category,
+                "timestamp": datetime.now().strftime("%H:%M"),
+            })
+
+        return memories
+
+    async def acapture(self, text: str) -> List[dict]:
+        """异步分析文本并捕获值得记忆的信息
+
+        Args:
+            text: 要分析的文本
+
+        Returns:
+            捕获到的记忆列表
+        """
+        # 使用线程池执行 CPU 密集型任务
+        loop = asyncio.get_event_loop()
+        return await loop.run_in_executor(None, self.capture, text)
+
+    def capture_and_store(self, text: str, date: datetime = None) -> List[dict]:
+        """分析文本并存储捕获到的记忆
+
+        Args:
+            text: 要分析的文本
+            date: 日期,默认为今天
+
+        Returns:
+            实际存储的记忆列表
+        """
+        memories = self.capture(text)
+        stored = []
+
+        for memory in memories:
+            try:
+                self.workspace.append_classified_memory(
+                    content=memory["content"],
+                    category=memory["category"],
+                    date=date,
+                )
+                stored.append(memory)
+            except Exception as e:
+                print(f"⚠️ 存储记忆失败: {e}")
+
+        return stored
+
+    async def acapture_and_store(self, text: str, date: datetime = None) -> List[dict]:
+        """异步分析文本并存储捕获到的记忆
+
+        Args:
+            text: 要分析的文本
+            date: 日期,默认为今天
+
+        Returns:
+            实际存储的记忆列表
+        """
+        loop = asyncio.get_event_loop()
+        return await loop.run_in_executor(
+            None, self.capture_and_store, text, date
+        )
+
+    def _split_sentences(self, text: str) -> List[str]:
+        """将文本分割为句子
+
+        Args:
+            text: 输入文本
+
+        Returns:
+            句子列表
+        """
+        # 按常见分隔符分割
+        # 支持中英文句号、问号、感叹号、换行
+        sentences = re.split(r'[。!?.!?]\s*|\n+', text)
+        return [s.strip() for s in sentences if s.strip()]
+
+    def _match_trigger(self, sentence: str) -> Optional[str]:
+        """检查句子是否匹配触发规则
+
+        Args:
+            sentence: 要检查的句子
+
+        Returns:
+            匹配的分类,如果没有匹配则返回 None
+        """
+        for pattern, category in self._compiled_patterns:
+            if pattern.search(sentence):
+                return category
+        return None
+
+    def _extract_memory(self, sentence: str, category: str) -> Optional[str]:
+        """从句子中提取记忆内容
+
+        Args:
+            sentence: 原始句子
+            category: 分类
+
+        Returns:
+            提取的记忆内容
+        """
+        # 清理句子
+        content = sentence.strip()
+
+        # 移除前缀(如"用户:"、"我:"等)
+        content = re.sub(r'^(用户|我|你|assistant|user)[::]\s*', '', content)
+
+        # 移除引号
+        content = content.strip('"\'""''')
+
+        # 如果内容太短,可能是噪声
+        if len(content) < 5:
+            return None
+
+        # 根据分类进行适当格式化
+        if category == "preference":
+            # 确保偏好类记忆以"用户"开头
+            if not content.startswith("用户") and not content.startswith("I "):
+                content = f"用户{content}"
+
+        return content
+
+    def analyze_conversation(self, messages: List[dict]) -> List[dict]:
+        """分析完整对话并提取记忆
+
+        Args:
+            messages: 对话消息列表,每项包含 role 和 content
+
+        Returns:
+            捕获到的记忆列表
+        """
+        all_memories = []
+
+        for msg in messages:
+            role = msg.get("role", "")
+            content = msg.get("content", "")
+
+            # 只分析用户消息
+            if role == "user" and content:
+                memories = self.capture(content)
+                all_memories.extend(memories)
+
+        return all_memories
+
+    def get_category_stats(self) -> dict:
+        """获取记忆分类统计
+
+        Returns:
+            各分类的记忆数量统计
+        """
+        # 读取今日记忆
+        today_path = self.workspace.get_daily_memory_path()
+        stats = {
+            "preference": 0,
+            "decision": 0,
+            "entity": 0,
+            "fact": 0,
+            "total": 0,
+        }
+
+        try:
+            with open(today_path, "r", encoding="utf-8") as f:
+                content = f.read()
+
+            for category in stats:
+                if category != "total":
+                    # 统计 [category] 标签出现次数
+                    pattern = rf'\[{category}\]'
+                    count = len(re.findall(pattern, content, re.IGNORECASE))
+                    stats[category] = count
+                    stats["total"] += count
+        except FileNotFoundError:
+            pass
+
+        return stats

+ 111 - 0
Co-creation-projects/tino-chen-HelloClaw/src/memory/memory_flush.py

@@ -0,0 +1,111 @@
+"""Memory Flush 管理器 - 在上下文压缩前提醒 Agent 保存记忆"""
+
+from datetime import datetime
+from typing import Optional, Tuple
+
+
+class MemoryFlushManager:
+    """Memory Flush 管理器
+
+    在上下文即将压缩前,触发一个静默回合,提醒 Agent 保存记忆。
+    这样可以防止有价值的上下文在压缩时丢失。
+    """
+
+    def __init__(
+        self,
+        context_window: int = 128000,
+        compression_threshold: float = 0.8,
+        soft_threshold_tokens: int = 4000,
+        enabled: bool = True,
+    ):
+        """初始化 Memory Flush 管理器
+
+        Args:
+            context_window: 上下文窗口大小
+            compression_threshold: 压缩阈值(比例)
+            soft_threshold_tokens: 软阈值 token 数(在压缩点之前触发 flush)
+            enabled: 是否启用 flush 功能
+        """
+        self.context_window = context_window
+        self.compression_threshold = compression_threshold
+        self.soft_threshold_tokens = soft_threshold_tokens
+        self.enabled = enabled
+
+        # 记录是否已经触发过 flush(每个会话只触发一次)
+        self._flush_triggered = False
+
+    def should_trigger_flush(self, current_tokens: int) -> bool:
+        """判断是否应该触发 flush
+
+        Args:
+            current_tokens: 当前 token 数
+
+        Returns:
+            是否应该触发 flush
+        """
+        if not self.enabled or self._flush_triggered:
+            return False
+
+        # 计算触发点:压缩阈值 - 软阈值
+        trigger_point = (
+            self.context_window * self.compression_threshold
+            - self.soft_threshold_tokens
+        )
+
+        if current_tokens >= trigger_point:
+            self._flush_triggered = True
+            return True
+
+        return False
+
+    def get_flush_prompt(self) -> str:
+        """获取 flush 提示词
+
+        Returns:
+            静默回合的提示词
+        """
+        today = datetime.now().strftime("%Y-%m-%d")
+        return f"""Pre-compaction memory flush.
+
+The conversation context is about to be compressed. Please save any important memories now.
+
+Guidelines:
+- Use memory_add to save notable facts, decisions, or user preferences to memory/{today}.md
+- Use memory_update_longterm for information that should persist across all sessions
+- Focus on information that would be valuable for future conversations
+
+If nothing important needs to be stored, reply with exactly: [SILENT]"""
+
+    def is_silent_response(self, response: str) -> bool:
+        """判断是否是静默响应
+
+        Args:
+            response: Agent 的响应
+
+        Returns:
+            是否是静默响应(不需要返回给用户)
+        """
+        return response.strip() == "[SILENT]"
+
+    def reset(self):
+        """重置 flush 状态(新会话时调用)"""
+        self._flush_triggered = False
+
+    def get_status(self) -> dict:
+        """获取当前状态
+
+        Returns:
+            状态信息字典
+        """
+        trigger_point = (
+            self.context_window * self.compression_threshold
+            - self.soft_threshold_tokens
+        )
+        return {
+            "enabled": self.enabled,
+            "context_window": self.context_window,
+            "compression_threshold": self.compression_threshold,
+            "soft_threshold_tokens": self.soft_threshold_tokens,
+            "trigger_point": int(trigger_point),
+            "flush_triggered": self._flush_triggered,
+        }

+ 396 - 0
Co-creation-projects/tino-chen-HelloClaw/src/memory/session_summarizer.py

@@ -0,0 +1,396 @@
+"""会话总结器 - 自动生成会话摘要"""
+
+import os
+import re
+from datetime import datetime
+from typing import List, Optional, Dict, Any
+
+
+class SessionSummarizer:
+    """会话总结器
+
+    负责在创建新会话时总结旧会话内容,生成结构化摘要保存到 memory 目录。
+    """
+
+    def __init__(
+        self,
+        workspace_manager,
+        llm_client=None,
+        model_id: str = None,
+        api_key: str = None,
+        base_url: str = None,
+    ):
+        """初始化会话总结器
+
+        Args:
+            workspace_manager: 工作空间管理器
+            llm_client: LLM 客户端(可选,用于生成总结)
+            model_id: 模型 ID
+            api_key: API Key
+            base_url: API Base URL
+        """
+        self.workspace = workspace_manager
+        self._llm_client = llm_client
+        self._model_id = model_id
+        self._api_key = api_key
+        self._base_url = base_url
+
+    async def summarize_session(
+        self,
+        messages: List[dict],
+        last_n: int = 10,
+        session_id: str = None,
+    ) -> Optional[str]:
+        """总结会话内容
+
+        Args:
+            messages: 会话消息列表
+            last_n: 只取最后 N 轮对话
+            session_id: 会话 ID(用于日志)
+
+        Returns:
+            生成的总结文件路径,如果失败返回 None
+        """
+        if not messages:
+            return None
+
+        # 提取最后 N 轮对话
+        excerpt = self._extract_excerpt(messages, last_n)
+        if not excerpt:
+            return None
+
+        try:
+            # 生成 slug 和总结
+            slug = await self._generate_slug(excerpt)
+            summary = await self._generate_summary(excerpt)
+
+            if not slug or not summary:
+                return None
+
+            # 保存到文件
+            filename = self._generate_filename(slug)
+            self.workspace.save_session_summary(filename, summary)
+
+            return filename
+
+        except Exception as e:
+            print(f"⚠️ 会话总结失败: {e}")
+            return None
+
+    def _extract_excerpt(
+        self,
+        messages: List[dict],
+        last_n: int = 10,
+    ) -> str:
+        """提取会话摘要文本
+
+        Args:
+            messages: 消息列表
+            last_n: 取最后 N 轮对话
+
+        Returns:
+            提取的文本
+        """
+        # 只保留 user 和 assistant 消息
+        conversation = []
+        for msg in messages:
+            role = msg.get("role", "")
+            content = msg.get("content", "")
+            if role in ("user", "assistant") and content:
+                # 截断过长的内容
+                if len(content) > 500:
+                    content = content[:500] + "..."
+                conversation.append(f"[{role.upper()}]: {content}")
+
+        # 只取最后 N 轮
+        if len(conversation) > last_n * 2:
+            conversation = conversation[-(last_n * 2) :]
+
+        return "\n".join(conversation)
+
+    async def _generate_slug(self, excerpt: str) -> str:
+        """生成描述性 slug
+
+        Args:
+            excerpt: 会话摘要文本
+
+        Returns:
+            3-5 个单词的 slug
+        """
+        if not self._llm_client:
+            # 如果没有 LLM,使用简单方法生成 slug
+            return self._generate_simple_slug(excerpt)
+
+        prompt = f"""根据以下对话内容,生成一个简短的英文描述(3-5个单词,用连字符连接)。
+只输出描述本身,不要其他内容。
+
+对话内容:
+{excerpt[:1000]}
+
+描述:"""
+
+        try:
+            # 调用 LLM
+            from openai import AsyncOpenAI
+
+            client = AsyncOpenAI(
+                api_key=self._api_key,
+                base_url=self._base_url,
+            )
+
+            response = await client.chat.completions.create(
+                model=self._model_id,
+                messages=[{"role": "user", "content": prompt}],
+                max_tokens=50,
+                temperature=0.3,
+            )
+
+            slug = response.choices[0].message.content.strip()
+            # 清理 slug
+            slug = re.sub(r"[^a-zA-Z0-9\-]", "", slug.replace(" ", "-").lower())
+            slug = re.sub(r"-+", "-", slug).strip("-")
+
+            # 限制长度
+            if len(slug) > 50:
+                slug = slug[:50]
+
+            return slug or "conversation"
+
+        except Exception as e:
+            print(f"⚠️ 生成 slug 失败: {e}")
+            return self._generate_simple_slug(excerpt)
+
+    def _generate_simple_slug(self, excerpt: str) -> str:
+        """使用简单方法生成 slug
+
+        从对话中提取关键词
+        """
+        # 提取一些常见的关键词
+        keywords = []
+        common_words = {
+            "the",
+            "a",
+            "an",
+            "is",
+            "are",
+            "was",
+            "were",
+            "be",
+            "been",
+            "being",
+            "have",
+            "has",
+            "had",
+            "do",
+            "does",
+            "did",
+            "will",
+            "would",
+            "could",
+            "should",
+            "may",
+            "might",
+            "must",
+            "shall",
+            "can",
+            "need",
+            "dare",
+            "ought",
+            "used",
+            "to",
+            "of",
+            "in",
+            "for",
+            "on",
+            "with",
+            "at",
+            "by",
+            "from",
+            "as",
+            "into",
+            "through",
+            "during",
+            "before",
+            "after",
+            "above",
+            "below",
+            "between",
+            "under",
+            "again",
+            "further",
+            "then",
+            "once",
+            "here",
+            "there",
+            "when",
+            "where",
+            "why",
+            "how",
+            "all",
+            "each",
+            "few",
+            "more",
+            "most",
+            "other",
+            "some",
+            "such",
+            "no",
+            "nor",
+            "not",
+            "only",
+            "own",
+            "same",
+            "so",
+            "than",
+            "too",
+            "very",
+            "just",
+            "and",
+            "but",
+            "if",
+            "or",
+            "because",
+            "until",
+            "while",
+            "about",
+            "what",
+            "which",
+            "who",
+            "whom",
+            "this",
+            "that",
+            "these",
+            "those",
+            "i",
+            "me",
+            "my",
+            "myself",
+            "we",
+            "our",
+            "ours",
+            "ourselves",
+            "you",
+            "your",
+            "yours",
+            "yourself",
+            "yourselves",
+            "he",
+            "him",
+            "his",
+            "himself",
+            "she",
+            "her",
+            "hers",
+            "herself",
+            "it",
+            "its",
+            "itself",
+            "they",
+            "them",
+            "their",
+            "theirs",
+            "themselves",
+        }
+
+        # 提取英文单词
+        words = re.findall(r"\b[a-zA-Z]{3,}\b", excerpt.lower())
+        word_count = {}
+        for word in words:
+            if word not in common_words:
+                word_count[word] = word_count.get(word, 0) + 1
+
+        # 取频率最高的词
+        sorted_words = sorted(word_count.items(), key=lambda x: -x[1])
+        keywords = [w for w, _ in sorted_words[:3]]
+
+        if keywords:
+            return "-".join(keywords)
+        return "conversation"
+
+    async def _generate_summary(self, excerpt: str) -> str:
+        """生成结构化总结
+
+        Args:
+            excerpt: 会话摘要文本
+
+        Returns:
+            Markdown 格式的总结
+        """
+        if not self._llm_client:
+            # 如果没有 LLM,返回简单格式
+            return self._generate_simple_summary(excerpt)
+
+        prompt = f"""请为以下对话生成一个结构化的会话总结。
+
+要求:
+1. 使用 Markdown 格式
+2. 包含以下部分:
+   - 主题:一句话概括
+   - 关键点:3-5 个要点
+   - 待办:如果有提到任务或待办事项
+3. 简洁明了,总字数不超过 300 字
+
+对话内容:
+{excerpt[:2000]}
+
+总结:"""
+
+        try:
+            from openai import AsyncOpenAI
+
+            client = AsyncOpenAI(
+                api_key=self._api_key,
+                base_url=self._base_url,
+            )
+
+            response = await client.chat.completions.create(
+                model=self._model_id,
+                messages=[{"role": "user", "content": prompt}],
+                max_tokens=500,
+                temperature=0.3,
+            )
+
+            summary = response.choices[0].message.content.strip()
+
+            # 添加元信息头
+            header = f"""---
+date: {datetime.now().strftime("%Y-%m-%d %H:%M")}
+type: session-summary
+---
+
+"""
+            return header + summary
+
+        except Exception as e:
+            print(f"⚠️ 生成总结失败: {e}")
+            return self._generate_simple_summary(excerpt)
+
+    def _generate_simple_summary(self, excerpt: str) -> str:
+        """生成简单格式的总结"""
+        header = f"""---
+date: {datetime.now().strftime("%Y-%m-%d %H:%M")}
+type: session-summary
+---
+
+# 会话摘要
+
+## 对话节选
+
+"""
+        # 截取前 500 字符
+        content = excerpt[:500]
+        if len(excerpt) > 500:
+            content += "..."
+        return header + content
+
+    def _generate_filename(self, slug: str) -> str:
+        """生成文件名
+
+        Args:
+            slug: 描述性 slug
+
+        Returns:
+            文件名(YYYY-MM-DD-slug.md)
+        """
+        date_str = datetime.now().strftime("%Y-%m-%d")
+        return f"{date_str}-{slug}.md"

+ 13 - 0
Co-creation-projects/tino-chen-HelloClaw/src/tools/__init__.py

@@ -0,0 +1,13 @@
+"""HelloClaw Tools 模块"""
+
+from .builtin.memory import MemoryTool
+from .builtin.execute_command import ExecuteCommandTool
+from .builtin.web_search import WebSearchTool
+from .builtin.web_fetch import WebFetchTool
+
+__all__ = [
+    "MemoryTool",
+    "ExecuteCommandTool",
+    "WebSearchTool",
+    "WebFetchTool",
+]

+ 13 - 0
Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/__init__.py

@@ -0,0 +1,13 @@
+"""内置工具模块"""
+
+from .memory import MemoryTool
+from .execute_command import ExecuteCommandTool
+from .web_search import WebSearchTool
+from .web_fetch import WebFetchTool
+
+__all__ = [
+    "MemoryTool",
+    "ExecuteCommandTool",
+    "WebSearchTool",
+    "WebFetchTool",
+]

+ 269 - 0
Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/execute_command.py

@@ -0,0 +1,269 @@
+"""命令执行工具 - 安全地执行 shell 命令"""
+
+import subprocess
+import re
+import os
+from typing import List, Dict, Any
+
+from hello_agents.tools import Tool, ToolParameter, ToolResponse, tool_action
+
+
+# 白名单命令(只允许这些基础命令)
+ALLOWED_COMMANDS = [
+    "ls", "cat", "echo", "pwd", "git", "npm", "pnpm", "uv", "python",
+    "python3", "node", "yarn", "pip", "pip3", "mkdir", "touch", "cp",
+    "mv", "grep", "find", "head", "tail", "wc", "sort", "uniq",
+]
+
+# 危险命令模式(正则表达式)
+DANGEROUS_PATTERNS = [
+    r"rm\s+-rf",           # 递归强制删除
+    r"rm\s+-fr",           # 递归强制删除(变体)
+    r"sudo",               # 提权命令
+    r"chmod\s+777",        # 危险权限设置
+    r">\s*/dev/",          # 写入设备文件
+    r"mkfs",               # 格式化命令
+    r"dd\s+if=",           # 磁盘复制
+    r">\s*/etc/",          # 写入系统配置
+    r"shutdown",           # 关机命令
+    r"reboot",             # 重启命令
+    r"init\s+[06]",        # 切换运行级别
+    r"kill\s+-9\s+1",      # 杀死 init 进程
+    r":(){ :\|:& };:",     # Fork 炸弹
+    r">\s*\$HOME",         # 覆盖用户目录
+    r">\s*~",              # 覆盖用户目录
+]
+
+
+class ExecuteCommandTool(Tool):
+    """命令执行工具
+
+    提供安全的 shell 命令执行能力,包括:
+    - 命令白名单机制
+    - 危险命令拦截
+    - 工作目录限制
+    - 执行超时控制
+    """
+
+    def __init__(
+        self,
+        allowed_commands: List[str] = None,
+        dangerous_patterns: List[str] = None,
+        max_output_size: int = 10000,
+        timeout: int = 30,
+        allowed_directories: List[str] = None,
+    ):
+        """初始化命令执行工具
+
+        Args:
+            allowed_commands: 允许的命令列表,默认使用 ALLOWED_COMMANDS
+            dangerous_patterns: 危险命令模式列表,默认使用 DANGEROUS_PATTERNS
+            max_output_size: 最大输出大小(字符),默认 10000
+            timeout: 命令执行超时时间(秒),默认 30
+            allowed_directories: 允许的工作目录列表,None 表示不限制
+        """
+        super().__init__(
+            name="execute_command",
+            description="安全地执行 shell 命令,支持命令白名单和危险命令拦截",
+            expandable=True
+        )
+
+        self.allowed_commands = allowed_commands or ALLOWED_COMMANDS
+        self.dangerous_patterns = dangerous_patterns or DANGEROUS_PATTERNS
+        self.max_output_size = max_output_size
+        self.timeout = timeout
+        self.allowed_directories = allowed_directories
+
+        # 编译危险模式正则表达式
+        self._dangerous_regex = [
+            re.compile(pattern, re.IGNORECASE) for pattern in self.dangerous_patterns
+        ]
+
+    def run(self, parameters: Dict[str, Any]) -> ToolResponse:
+        """执行命令(默认行为)"""
+        command = parameters.get("command", "")
+        workdir = parameters.get("workdir")
+        return self._execute_command(command, workdir)
+
+    def get_parameters(self) -> List[ToolParameter]:
+        return [
+            ToolParameter(
+                name="command",
+                type="string",
+                description="要执行的 shell 命令",
+                required=True
+            ),
+            ToolParameter(
+                name="workdir",
+                type="string",
+                description="工作目录(可选)",
+                required=False
+            ),
+        ]
+
+    def _validate_command(self, command: str) -> tuple[bool, str]:
+        """验证命令是否安全
+
+        Args:
+            command: 要验证的命令
+
+        Returns:
+            (is_safe, reason): 是否安全,不安全的原因
+        """
+        # 检查危险模式
+        for pattern in self._dangerous_regex:
+            if pattern.search(command):
+                return False, f"命令包含危险模式: {pattern.pattern}"
+
+        # 提取基础命令(命令行的第一个词)
+        # 处理带路径的命令(如 /usr/bin/ls)
+        command_parts = command.strip().split()
+        if not command_parts:
+            return False, "命令为空"
+
+        base_cmd = os.path.basename(command_parts[0])
+
+        # 检查白名单
+        if base_cmd not in self.allowed_commands:
+            return False, f"命令 '{base_cmd}' 不在白名单中。允许的命令: {', '.join(self.allowed_commands[:10])}..."
+
+        return True, ""
+
+    def _validate_workdir(self, workdir: str) -> tuple[bool, str]:
+        """验证工作目录
+
+        Args:
+            workdir: 工作目录路径
+
+        Returns:
+            (is_valid, reason): 是否有效,无效的原因
+        """
+        # 如果没有设置 allowed_directories,允许所有目录
+        if not self.allowed_directories:
+            return True, ""
+
+        # 检查目录是否在允许列表中
+        abs_workdir = os.path.abspath(workdir)
+        for allowed_dir in self.allowed_directories:
+            abs_allowed = os.path.abspath(allowed_dir)
+            if abs_workdir.startswith(abs_allowed):
+                return True, ""
+
+        return False, f"工作目录 '{workdir}' 不在允许的目录列表中"
+
+    def _execute_command(
+        self,
+        command: str,
+        workdir: str = None,
+        timeout: int = None,
+    ) -> ToolResponse:
+        """执行命令的核心实现
+
+        Args:
+            command: 要执行的命令
+            workdir: 工作目录
+            timeout: 超时时间(秒)
+
+        Returns:
+            ToolResponse: 执行结果
+        """
+        if not command:
+            return ToolResponse.error(
+                code="INVALID_INPUT",
+                message="命令不能为空"
+            )
+
+        # 验证命令安全性
+        is_safe, reason = self._validate_command(command)
+        if not is_safe:
+            return ToolResponse.error(
+                code="COMMAND_BLOCKED",
+                message=f"命令被拦截: {reason}"
+            )
+
+        # 验证工作目录
+        if workdir:
+            is_valid, reason = self._validate_workdir(workdir)
+            if not is_valid:
+                return ToolResponse.error(
+                    code="DIRECTORY_NOT_ALLOWED",
+                    message=f"工作目录无效: {reason}"
+                )
+
+        # 执行命令
+        try:
+            result = subprocess.run(
+                command,
+                shell=True,
+                capture_output=True,
+                text=True,
+                cwd=workdir,
+                timeout=timeout or self.timeout,
+            )
+
+            # 截断过长的输出
+            stdout = result.stdout
+            stderr = result.stderr
+
+            if len(stdout) > self.max_output_size:
+                stdout = stdout[:self.max_output_size] + f"\n... (输出已截断,共 {len(result.stdout)} 字符)"
+            if len(stderr) > self.max_output_size:
+                stderr = stderr[:self.max_output_size] + f"\n... (错误输出已截断,共 {len(result.stderr)} 字符)"
+
+            # 构建响应
+            output_parts = []
+            if stdout:
+                output_parts.append(f"输出:\n{stdout}")
+            if stderr:
+                output_parts.append(f"错误:\n{stderr}")
+
+            output_text = "\n\n".join(output_parts) if output_parts else "命令执行完成(无输出)"
+
+            return ToolResponse.success(
+                text=output_text,
+                data={
+                    "return_code": result.returncode,
+                    "stdout": result.stdout,
+                    "stderr": result.stderr,
+                    "command": command,
+                    "workdir": workdir,
+                }
+            )
+
+        except subprocess.TimeoutExpired:
+            return ToolResponse.error(
+                code="TIMEOUT",
+                message=f"命令执行超时({timeout or self.timeout}秒)"
+            )
+        except Exception as e:
+            return ToolResponse.error(
+                code="EXECUTION_ERROR",
+                message=f"命令执行失败: {str(e)}"
+            )
+
+    @tool_action("exec_run", "执行 shell 命令")
+    def _run_command(
+        self,
+        command: str,
+        workdir: str = None,
+        timeout: int = None,
+    ) -> str:
+        """执行 shell 命令
+
+        Args:
+            command: 要执行的命令
+            workdir: 工作目录(可选)
+            timeout: 超时时间(秒,可选)
+        """
+        response = self._execute_command(command, workdir, timeout)
+        return response.text
+
+    @tool_action("exec_allowed_commands", "列出允许的命令")
+    def _list_allowed_commands(self) -> str:
+        """列出所有允许执行的命令"""
+        return "允许的命令:\n" + "\n".join(f"- {cmd}" for cmd in sorted(self.allowed_commands))
+
+    @tool_action("exec_dangerous_patterns", "列出危险命令模式")
+    def _list_dangerous_patterns(self) -> str:
+        """列出所有会被拦截的危险命令模式"""
+        return "危险命令模式:\n" + "\n".join(f"- {pattern}" for pattern in self.dangerous_patterns)

+ 231 - 0
Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/memory.py

@@ -0,0 +1,231 @@
+"""记忆工具 - 支持记忆检索和更新"""
+
+from typing import List, Dict, Any, Optional
+import re
+
+from hello_agents.tools import Tool, ToolParameter, ToolResponse, tool_action
+
+
+class MemoryTool(Tool):
+    """记忆管理工具
+
+    可展开为多个子工具:
+    - memory_search: 搜索记忆(返回带行号的上下文)
+    - memory_get: 读取特定记忆文件或行范围
+    - memory_add: 添加每日记忆
+    - memory_update_longterm: 更新长期记忆
+    - memory_list: 列出所有记忆文件
+    """
+
+    def __init__(self, workspace_manager):
+        """初始化记忆工具
+
+        Args:
+            workspace_manager: 工作空间管理器实例
+        """
+        super().__init__(
+            name="memory",
+            description="记忆管理工具,支持搜索、读取、添加和更新记忆",
+            expandable=True
+        )
+        self.workspace = workspace_manager
+
+    def run(self, parameters: Dict[str, Any]) -> ToolResponse:
+        """默认执行:搜索记忆"""
+        keyword = parameters.get("keyword", "")
+        return self._search_memory(keyword)
+
+    def get_parameters(self) -> List[ToolParameter]:
+        return [
+            ToolParameter(
+                name="keyword",
+                type="string",
+                description="搜索关键词",
+                required=True
+            )
+        ]
+
+    def _search_memory(self, keyword: str, context_lines: int = 3) -> ToolResponse:
+        """搜索记忆(增强版,返回带行号的上下文)"""
+        if not keyword:
+            return ToolResponse.error(
+                code="INVALID_INPUT",
+                message="请提供搜索关键词"
+            )
+
+        # 使用增强搜索
+        results = self.workspace.search_memory_enhanced(
+            keyword,
+            context_lines=context_lines
+        )
+
+        if not results:
+            return ToolResponse.success(
+                text=f"未找到与 '{keyword}' 相关的记忆",
+                data={"results": [], "keyword": keyword}
+            )
+
+        # 格式化结果
+        formatted_parts = []
+        total_matches = 0
+
+        for r in results:
+            source = r["source"]
+            matches = r["matches"]
+            total_matches += len(matches)
+
+            for m in matches:
+                start = m["start_line"]
+                end = m["end_line"]
+                content = m["content"]
+                line_range = f"行 {start}" if start == end else f"行 {start}-{end}"
+                formatted_parts.append(
+                    f"**{source}** ({line_range}):\n```\n{content}\n```"
+                )
+
+        return ToolResponse.success(
+            text=f"找到 {total_matches} 处匹配 '{keyword}':\n\n" + "\n\n".join(formatted_parts),
+            data={"results": results, "count": total_matches, "keyword": keyword}
+        )
+
+    @tool_action("memory_search", "搜索历史记忆")
+    def _search(self, keyword: str, context_lines: int = 3) -> str:
+        """搜索记忆
+
+        Args:
+            keyword: 搜索关键词
+            context_lines: 上下文行数,默认 3
+        """
+        response = self._search_memory(keyword, context_lines)
+        return response.text
+
+    @tool_action("memory_get", "读取特定记忆文件或行范围")
+    def _get_memory(
+        self,
+        filename: str = None,
+        start_line: int = None,
+        end_line: int = None,
+        lines: str = None,
+    ) -> str:
+        """读取记忆文件内容
+
+        Args:
+            filename: 文件名(MEMORY.md 或 YYYY-MM-DD.md),默认为今天的日记
+            start_line: 起始行号(从 1 开始)
+            end_line: 结束行号
+            lines: 行范围字符串,如 "10-20" 或 "15"
+        """
+        from datetime import datetime
+
+        # 解析 lines 参数
+        if lines:
+            match = re.match(r"(\d+)(?:\s*-\s*(\d+))?", lines)
+            if match:
+                start_line = int(match.group(1))
+                if match.group(2):
+                    end_line = int(match.group(2))
+
+        # 默认文件名
+        if not filename:
+            filename = datetime.now().strftime("%Y-%m-%d.md")
+
+        # 确保文件名以 .md 结尾
+        if not filename.endswith(".md"):
+            filename += ".md"
+
+        # 读取文件
+        content = self.workspace.read_memory_lines(filename, start_line, end_line)
+
+        if content is None:
+            available = self._list_memory_files_brief()
+            return f"文件 '{filename}' 不存在。可用文件:\n{available}"
+
+        if not content:
+            return f"文件 '{filename}' 为空"
+
+        display_name = filename
+        if start_line or end_line:
+            range_str = f"行 {start_line or 1}"
+            if end_line and end_line != start_line:
+                range_str += f"-{end_line}"
+            display_name += f" ({range_str})"
+
+        return f"**{display_name}**:\n```\n{content}\n```"
+
+    @tool_action("memory_add", "添加内容到今日记忆")
+    def _add_daily(self, content: str, category: str = None) -> str:
+        """添加每日记忆
+
+        Args:
+            content: 记忆内容
+            category: 分类标签(preference/decision/entity/fact),可选
+        """
+        if category:
+            # 使用带分类标签的存储
+            self.workspace.append_classified_memory(content, category)
+            return f"已添加到今日记忆 [{category}]: {content[:50]}..."
+        else:
+            # 使用原有方法
+            self.workspace.append_to_daily_memory(content)
+            return f"已添加到今日记忆: {content[:50]}..."
+
+    @tool_action("memory_update_longterm", "更新长期记忆")
+    def _update_longterm(self, content: str) -> str:
+        """更新长期记忆
+
+        Args:
+            content: 要添加到长期记忆的内容
+        """
+        current = self.workspace.load_config("MEMORY") or ""
+        updated = current + f"\n\n## 新增\n\n{content}\n"
+        self.workspace.save_config("MEMORY", updated)
+        return "已更新长期记忆"
+
+    @tool_action("memory_list", "列出所有记忆文件")
+    def _list(self) -> str:
+        """列出所有记忆文件"""
+        files = self.workspace.list_memory_files()
+
+        if not files:
+            return "暂无记忆文件"
+
+        lines = ["# 记忆文件列表\n"]
+
+        # 按类型分组
+        longterm = [f for f in files if f["type"] == "longterm"]
+        daily = [f for f in files if f["type"] == "daily"]
+
+        if longterm:
+            lines.append("## 长期记忆")
+            for f in longterm:
+                size_kb = f["size"] / 1024
+                lines.append(f"- **{f['name']}** ({size_kb:.1f} KB)")
+
+        if daily:
+            lines.append("\n## 每日记忆")
+            for f in daily:
+                size_kb = f["size"] / 1024
+                lines.append(f"- **{f['name']}** ({size_kb:.1f} KB)")
+
+        return "\n".join(lines)
+
+    @tool_action("memory_cleanup", "清理过期的每日记忆")
+    def _cleanup(self, days: int = 30) -> str:
+        """清理过期记忆
+
+        Args:
+            days: 保留天数,超过此天数将被清理,默认 30 天
+        """
+        deleted = self.workspace.cleanup_old_memories(days)
+
+        if not deleted:
+            return f"没有需要清理的记忆(保留最近 {days} 天)"
+
+        return f"已清理 {len(deleted)} 个过期记忆文件:\n" + "\n".join(f"- {f}" for f in deleted)
+
+    def _list_memory_files_brief(self) -> str:
+        """简要列出记忆文件"""
+        files = self.workspace.list_memory_files()
+        if not files:
+            return "无"
+        return "\n".join(f"- {f['name']}" for f in files)

+ 248 - 0
Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/web_fetch.py

@@ -0,0 +1,248 @@
+"""网页抓取工具 - 抓取网页内容并转换为 Markdown"""
+
+import os
+import re
+from typing import List, Dict, Any
+from urllib.request import Request, urlopen
+from urllib.error import URLError, HTTPError
+
+from hello_agents.tools import Tool, ToolParameter, ToolResponse, tool_action
+
+
+class WebFetchTool(Tool):
+    """网页抓取工具
+
+    抓取网页内容并转换为 Markdown 格式。
+    支持提取主要内容、清理无关元素。
+    """
+
+    def __init__(
+        self,
+        timeout: int = 15,
+        max_content_size: int = 50000,
+        user_agent: str = None,
+    ):
+        """初始化网页抓取工具
+
+        Args:
+            timeout: 请求超时时间(秒),默认 15
+            max_content_size: 最大内容大小(字符),默认 50000
+            user_agent: 自定义 User-Agent
+        """
+        super().__init__(
+            name="web_fetch",
+            description="抓取网页内容并转换为 Markdown 格式",
+            expandable=True
+        )
+
+        self.timeout = timeout
+        self.max_content_size = max_content_size
+        self.user_agent = user_agent or "Mozilla/5.0 (compatible; HelloClawBot/1.0)"
+
+    def run(self, parameters: Dict[str, Any]) -> ToolResponse:
+        """执行抓取(默认行为)"""
+        url = parameters.get("url", "")
+        return self._fetch(url)
+
+    def get_parameters(self) -> List[ToolParameter]:
+        return [
+            ToolParameter(
+                name="url",
+                type="string",
+                description="要抓取的网页 URL",
+                required=True
+            ),
+        ]
+
+    def _fetch(self, url: str) -> ToolResponse:
+        """抓取网页的核心实现
+
+        Args:
+            url: 网页 URL
+
+        Returns:
+            ToolResponse: 抓取结果
+        """
+        if not url:
+            return ToolResponse.error(
+                code="INVALID_INPUT",
+                message="URL 不能为空"
+            )
+
+        # 验证 URL 格式
+        if not url.startswith(("http://", "https://")):
+            return ToolResponse.error(
+                code="INVALID_URL",
+                message="URL 必须以 http:// 或 https:// 开头"
+            )
+
+        try:
+            # 发送请求
+            request = Request(url)
+            request.add_header("User-Agent", self.user_agent)
+            request.add_header("Accept", "text/html,application/xhtml+xml")
+            request.add_header("Accept-Language", "zh-CN,zh,en;q=0.9")
+
+            with urlopen(request, timeout=self.timeout) as response:
+                # 检查内容类型
+                content_type = response.headers.get("Content-Type", "")
+                if not content_type.startswith("text/html"):
+                    return ToolResponse.error(
+                        code="UNSUPPORTED_CONTENT",
+                        message=f"不支持的内容类型: {content_type}"
+                    )
+
+                # 读取内容
+                html = response.read().decode("utf-8", errors="ignore")
+
+            # 转换为 Markdown
+            markdown = self._html_to_markdown(html)
+
+            # 截断过长内容
+            if len(markdown) > self.max_content_size:
+                markdown = markdown[:self.max_content_size] + f"\n\n... (内容已截断,共 {len(markdown)} 字符)"
+
+            return ToolResponse.success(
+                text=markdown,
+                data={
+                    "url": url,
+                    "content_length": len(markdown),
+                    "truncated": len(markdown) >= self.max_content_size,
+                }
+            )
+
+        except HTTPError as e:
+            return ToolResponse.error(
+                code="HTTP_ERROR",
+                message=f"抓取失败 (HTTP {e.code}): {e.reason}"
+            )
+        except URLError as e:
+            return ToolResponse.error(
+                code="NETWORK_ERROR",
+                message=f"网络错误: {str(e)}"
+            )
+        except Exception as e:
+            return ToolResponse.error(
+                code="FETCH_ERROR",
+                message=f"抓取失败: {str(e)}"
+            )
+
+    def _html_to_markdown(self, html: str) -> str:
+        """将 HTML 转换为 Markdown
+
+        简单的 HTML 到 Markdown 转换,提取主要内容。
+
+        Args:
+            html: HTML 内容
+
+        Returns:
+            Markdown 文本
+        """
+        # 移除 script 和 style 标签
+        html = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.DOTALL | re.IGNORECASE)
+        html = re.sub(r'<style[^>]*>.*?</style>', '', html, flags=re.DOTALL | re.IGNORECASE)
+
+        # 移除注释
+        html = re.sub(r'<!--.*?-->', '', html, flags=re.DOTALL)
+
+        # 提取 title
+        title = ""
+        title_match = re.search(r'<title[^>]*>(.*?)</title>', html, re.IGNORECASE | re.DOTALL)
+        if title_match:
+            title = self._clean_text(title_match.group(1))
+
+        # 提取 body 内容(如果有)
+        body_match = re.search(r'<body[^>]*>(.*?)</body>', html, re.IGNORECASE | re.DOTALL)
+        if body_match:
+            html = body_match.group(1)
+
+        # 移除导航、侧边栏、页脚等
+        for tag in ['nav', 'aside', 'footer', 'header']:
+            html = re.sub(f'<{tag}[^>]*>.*?</{tag}>', '', html, flags=re.DOTALL | re.IGNORECASE)
+
+        # 转换标题
+        for i in range(6, 0, -1):
+            html = re.sub(
+                f'<h{i}[^>]*>(.*?)</h{i}>',
+                lambda m: f"\n{'#' * i} {self._clean_text(m.group(1))}\n",
+                html,
+                flags=re.DOTALL | re.IGNORECASE
+            )
+
+        # 转换段落
+        html = re.sub(r'<p[^>]*>(.*?)</p>', r'\n\1\n', html, flags=re.DOTALL | re.IGNORECASE)
+
+        # 转换链接
+        html = re.sub(
+            r'<a[^>]*href=["\']([^"\']*)["\'][^>]*>(.*?)</a>',
+            r'[\2](\1)',
+            html,
+            flags=re.DOTALL | re.IGNORECASE
+        )
+
+        # 转换粗体
+        html = re.sub(r'<(strong|b)[^>]*>(.*?)</\1>', r'**\2**', html, flags=re.DOTALL | re.IGNORECASE)
+
+        # 转换斜体
+        html = re.sub(r'<(em|i)[^>]*>(.*?)</\1>', r'*\2*', html, flags=re.DOTALL | re.IGNORECASE)
+
+        # 转换代码块
+        html = re.sub(r'<pre[^>]*><code[^>]*>(.*?)</code></pre>', r'\n```\n\1\n```\n', html, flags=re.DOTALL | re.IGNORECASE)
+
+        # 转换行内代码
+        html = re.sub(r'<code[^>]*>(.*?)</code>', r'`\1`', html, flags=re.DOTALL | re.IGNORECASE)
+
+        # 转换列表
+        html = re.sub(r'<li[^>]*>(.*?)</li>', r'- \1\n', html, flags=re.DOTALL | re.IGNORECASE)
+        html = re.sub(r'<[ou]l[^>]*>(.*?)</[ou]l>', r'\n\1\n', html, flags=re.DOTALL | re.IGNORECASE)
+
+        # 转换换行
+        html = re.sub(r'<br\s*/?>', '\n', html, flags=re.IGNORECASE)
+
+        # 移除所有剩余的 HTML 标签
+        html = re.sub(r'<[^>]+>', '', html)
+
+        # 清理文本
+        markdown = self._clean_text(html)
+
+        # 添加标题
+        if title:
+            markdown = f"# {title}\n\n{markdown}"
+
+        # 清理多余空行
+        markdown = re.sub(r'\n{3,}', '\n\n', markdown)
+
+        return markdown.strip()
+
+    def _clean_text(self, text: str) -> str:
+        """清理文本
+
+        Args:
+            text: 原始文本
+
+        Returns:
+            清理后的文本
+        """
+        # 解码 HTML 实体
+        text = text.replace("&nbsp;", " ")
+        text = text.replace("&amp;", "&")
+        text = text.replace("&lt;", "<")
+        text = text.replace("&gt;", ">")
+        text = text.replace("&quot;", '"')
+        text = text.replace("&#39;", "'")
+
+        # 移除多余的空白
+        text = re.sub(r'[ \t]+', ' ', text)
+        text = re.sub(r'\n[ \t]+', '\n', text)
+
+        return text.strip()
+
+    @tool_action("fetch_url", "抓取网页内容")
+    def _fetch_action(self, url: str) -> str:
+        """抓取网页内容
+
+        Args:
+            url: 要抓取的网页 URL
+        """
+        response = self._fetch(url)
+        return response.text

+ 205 - 0
Co-creation-projects/tino-chen-HelloClaw/src/tools/builtin/web_search.py

@@ -0,0 +1,205 @@
+"""网页搜索工具 - 使用 Brave Search API 进行网络搜索"""
+
+import os
+import json
+from typing import List, Dict, Any
+from urllib.request import Request, urlopen
+from urllib.error import URLError, HTTPError
+
+from hello_agents.tools import Tool, ToolParameter, ToolResponse, tool_action
+
+
+class WebSearchTool(Tool):
+    """网页搜索工具
+
+    使用 Brave Search API 进行网络搜索。
+    需要配置环境变量 BRAVE_API_KEY 或在初始化时传入 API key。
+    """
+
+    def __init__(
+        self,
+        api_key: str = None,
+        max_results: int = 5,
+        timeout: int = 10,
+    ):
+        """初始化网页搜索工具
+
+        Args:
+            api_key: Brave Search API key,如未提供则从环境变量 BRAVE_API_KEY 读取
+            max_results: 最大返回结果数,默认 5
+            timeout: 请求超时时间(秒),默认 10
+        """
+        super().__init__(
+            name="web_search",
+            description="使用搜索引擎搜索网络信息",
+            expandable=True
+        )
+
+        self.api_key = api_key or os.getenv("BRAVE_API_KEY")
+        self.max_results = max_results
+        self.timeout = timeout
+        self._base_url = "https://api.search.brave.com/res/v1/web/search"
+
+    def run(self, parameters: Dict[str, Any]) -> ToolResponse:
+        """执行搜索(默认行为)"""
+        query = parameters.get("query", "")
+        count = parameters.get("count", self.max_results)
+        return self._search(query, count)
+
+    def get_parameters(self) -> List[ToolParameter]:
+        return [
+            ToolParameter(
+                name="query",
+                type="string",
+                description="搜索查询词",
+                required=True
+            ),
+            ToolParameter(
+                name="count",
+                type="integer",
+                description=f"返回结果数量,默认 {self.max_results}",
+                required=False
+            ),
+        ]
+
+    def _search(self, query: str, count: int = None) -> ToolResponse:
+        """执行搜索的核心实现
+
+        Args:
+            query: 搜索查询
+            count: 返回结果数量
+
+        Returns:
+            ToolResponse: 搜索结果
+        """
+        if not query:
+            return ToolResponse.error(
+                code="INVALID_INPUT",
+                message="搜索查询不能为空"
+            )
+
+        if not self.api_key:
+            return ToolResponse.error(
+                code="MISSING_API_KEY",
+                message="未配置 Brave API Key。请设置环境变量 BRAVE_API_KEY 或在初始化时传入 api_key 参数"
+            )
+
+        try:
+            # 构建请求
+            params = {
+                "q": query,
+                "count": count or self.max_results,
+            }
+
+            url = f"{self._base_url}?q={query}&count={params['count']}"
+            request = Request(url)
+            request.add_header("Accept", "application/json")
+            request.add_header("Accept-Encoding", "gzip")
+            request.add_header("X-Subscription-Token", self.api_key)
+
+            # 发送请求
+            with urlopen(request, timeout=self.timeout) as response:
+                data = json.loads(response.read().decode("utf-8"))
+
+            # 解析结果
+            results = self._parse_search_results(data)
+
+            if not results:
+                return ToolResponse.success(
+                    text=f"未找到与 '{query}' 相关的结果",
+                    data={"query": query, "results": []}
+                )
+
+            # 格式化输出
+            formatted = self._format_results(results)
+
+            return ToolResponse.success(
+                text=formatted,
+                data={
+                    "query": query,
+                    "results": results,
+                    "count": len(results),
+                }
+            )
+
+        except HTTPError as e:
+            if e.code == 401:
+                return ToolResponse.error(
+                    code="AUTH_ERROR",
+                    message="API Key 无效或已过期"
+                )
+            elif e.code == 429:
+                return ToolResponse.error(
+                    code="RATE_LIMIT",
+                    message="API 请求频率超限,请稍后再试"
+                )
+            else:
+                return ToolResponse.error(
+                    code="HTTP_ERROR",
+                    message=f"搜索请求失败 (HTTP {e.code}): {e.reason}"
+                )
+        except URLError as e:
+            return ToolResponse.error(
+                code="NETWORK_ERROR",
+                message=f"网络错误: {str(e)}"
+            )
+        except Exception as e:
+            return ToolResponse.error(
+                code="SEARCH_ERROR",
+                message=f"搜索失败: {str(e)}"
+            )
+
+    def _parse_search_results(self, data: dict) -> List[dict]:
+        """解析 Brave Search API 响应
+
+        Args:
+            data: API 响应数据
+
+        Returns:
+            搜索结果列表
+        """
+        results = []
+
+        # 提取 web 搜索结果
+        web_results = data.get("web", {}).get("results", [])
+
+        for item in web_results:
+            result = {
+                "title": item.get("title", ""),
+                "url": item.get("url", ""),
+                "description": item.get("description", ""),
+            }
+            results.append(result)
+
+        return results
+
+    def _format_results(self, results: List[dict]) -> str:
+        """格式化搜索结果
+
+        Args:
+            results: 搜索结果列表
+
+        Returns:
+            格式化的文本
+        """
+        lines = [f"找到 {len(results)} 个结果:\n"]
+
+        for i, result in enumerate(results, 1):
+            lines.append(f"{i}. **{result['title']}**")
+            lines.append(f"   URL: {result['url']}")
+            if result['description']:
+                lines.append(f"   {result['description'][:200]}")
+            lines.append("")
+
+        return "\n".join(lines)
+
+    @tool_action("search_web", "搜索网络信息")
+    def _search_action(self, query: str, count: int = None) -> str:
+        """搜索网络
+
+        Args:
+            query: 搜索查询词
+            count: 返回结果数量(可选)
+        """
+        response = self._search(query, count)
+        return response.text

+ 5 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/__init__.py

@@ -0,0 +1,5 @@
+"""工作空间管理模块"""
+
+from .manager import WorkspaceManager
+
+__all__ = ["WorkspaceManager"]

+ 784 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/manager.py

@@ -0,0 +1,784 @@
+"""工作空间管理器"""
+
+import json
+import os
+import re
+from datetime import datetime, timedelta
+from pathlib import Path
+from typing import Optional, List, Set
+
+
+# 配置文件列表
+CONFIG_FILES = [
+    "BOOTSTRAP",
+    "IDENTITY",
+    "SOUL",
+    "USER",
+    "MEMORY",
+    "AGENTS",
+    "HEARTBEAT",
+]
+
+# 模板目录(相对于当前文件)
+TEMPLATES_DIR = Path(__file__).parent / "templates"
+
+
+def get_default_global_config() -> dict:
+    """获取默认全局配置(从模板文件读取)"""
+    template_path = TEMPLATES_DIR / "config.json"
+    if template_path.exists():
+        with open(template_path, "r", encoding="utf-8") as f:
+            return json.load(f)
+    return {"llm": {"model_id": "", "base_url": "", "api_key": ""}}
+
+
+class WorkspaceManager:
+    """工作空间管理器
+
+    负责:
+    - 创建和管理工作空间目录结构
+    - 加载和保存配置文件
+    - 管理记忆文件(每日记忆、长期记忆)
+    """
+
+    def __init__(self, workspace_path: str):
+        """初始化工作空间管理器
+
+        Args:
+            workspace_path: 工作空间根目录路径
+        """
+        self.workspace_path = os.path.expanduser(workspace_path)
+        self.memory_path = os.path.join(self.workspace_path, "memory")
+        self.sessions_path = os.path.join(self.workspace_path, "sessions")
+
+    # ==================== 全局配置读取 ====================
+
+    def load_global_config(self) -> dict:
+        """加载全局 config.json
+
+        Returns:
+            配置字典,如果文件不存在返回空字典
+        """
+        config_path = os.path.expanduser("~/.helloclaw/config.json")
+        if os.path.exists(config_path):
+            try:
+                with open(config_path, "r", encoding="utf-8") as f:
+                    return json.load(f)
+            except (json.JSONDecodeError, IOError):
+                return {}
+        return {}
+
+    def get_llm_config(self) -> dict:
+        """获取 LLM 配置
+
+        优先级:config.json 非空值 > 环境变量 > 默认值
+
+        Returns:
+            包含 model_id, api_key, base_url 的字典
+        """
+        global_config = self.load_global_config()
+        llm_config = global_config.get("llm", {})
+
+        return {
+            "model_id": llm_config.get("model_id") or os.getenv("LLM_MODEL_ID") or "glm-4",
+            "api_key": llm_config.get("api_key") or os.getenv("LLM_API_KEY"),
+            "base_url": llm_config.get("base_url") or os.getenv("LLM_BASE_URL"),
+        }
+
+    # ==================== 入职状态检测 ====================
+
+    def is_onboarding_completed(self) -> bool:
+        """检查入职是否完成
+
+        入职完成的标志:BOOTSTRAP.md 不存在。
+        同时会检查身份是否已确定,如果是则自动删除 BOOTSTRAP.md。
+
+        Returns:
+            入职是否已完成
+        """
+        # 先检查是否需要删除 BOOTSTRAP(身份已确定但文件还在)
+        self._check_and_delete_bootstrap()
+
+        return not os.path.exists(self.get_config_path("BOOTSTRAP"))
+
+    def ensure_workspace_exists(self):
+        """确保工作空间存在
+
+        如果工作空间不存在,创建默认目录和配置文件
+        """
+        # 创建目录
+        os.makedirs(self.workspace_path, exist_ok=True)
+        os.makedirs(self.memory_path, exist_ok=True)
+        os.makedirs(self.sessions_path, exist_ok=True)
+
+        # 创建默认配置文件
+        for config_name in CONFIG_FILES:
+            config_path = self.get_config_path(config_name)
+            if not os.path.exists(config_path):
+                self._create_default_config(config_name)
+
+        # 检查是否需要删除 BOOTSTRAP(遗留工作空间迁移)
+        self._check_and_delete_bootstrap()
+
+    def get_config_path(self, name: str) -> str:
+        """获取配置文件路径
+
+        Args:
+            name: 配置文件名称(不含扩展名)
+
+        Returns:
+            配置文件完整路径
+        """
+        return os.path.join(self.workspace_path, f"{name}.md")
+
+    def load_config(self, name: str) -> Optional[str]:
+        """加载配置文件内容
+
+        Args:
+            name: 配置文件名称
+
+        Returns:
+            配置文件内容,如果不存在返回 None
+        """
+        config_path = self.get_config_path(name)
+        if os.path.exists(config_path):
+            with open(config_path, "r", encoding="utf-8") as f:
+                return f.read()
+        return None
+
+    def save_config(self, name: str, content: str):
+        """保存配置文件
+
+        Args:
+            name: 配置文件名称
+            content: 配置文件内容
+        """
+        config_path = self.get_config_path(name)
+        with open(config_path, "w", encoding="utf-8") as f:
+            f.write(content)
+
+        # 如果保存的是 IDENTITY,检查是否需要删除 BOOTSTRAP
+        if name == "IDENTITY":
+            self._check_and_delete_bootstrap()
+
+    def list_configs(self) -> list:
+        """列出所有配置文件
+
+        Returns:
+            配置文件名称列表
+        """
+        configs = []
+        for name in CONFIG_FILES:
+            config_path = self.get_config_path(name)
+            if os.path.exists(config_path):
+                configs.append(name)
+        return configs
+
+    def get_daily_memory_path(self, date: datetime = None) -> str:
+        """获取每日记忆文件路径
+
+        Args:
+            date: 日期,默认为今天
+
+        Returns:
+            每日记忆文件路径
+        """
+        date = date or datetime.now()
+        filename = date.strftime("%Y-%m-%d.md")
+        return os.path.join(self.memory_path, filename)
+
+    def append_to_daily_memory(self, content: str, date: datetime = None):
+        """追加内容到每日记忆
+
+        Args:
+            content: 记忆内容
+            date: 日期,默认为今天
+        """
+        memory_path = self.get_daily_memory_path(date)
+        timestamp = datetime.now().strftime("%H:%M:%S")
+
+        with open(memory_path, "a", encoding="utf-8") as f:
+            f.write(f"\n## {timestamp}\n\n{content}\n")
+
+    def search_memory(self, keyword: str, include_daily: bool = True) -> list:
+        """搜索记忆
+
+        Args:
+            keyword: 搜索关键词
+            include_daily: 是否包含每日记忆
+
+        Returns:
+            匹配的记忆片段列表
+        """
+        results = []
+
+        # 搜索长期记忆
+        memory_content = self.load_config("MEMORY")
+        if memory_content and keyword.lower() in memory_content.lower():
+            results.append({
+                "source": "MEMORY.md",
+                "content": memory_content,
+            })
+
+        # 搜索每日记忆
+        if include_daily:
+            for filename in os.listdir(self.memory_path):
+                if filename.endswith(".md"):
+                    filepath = os.path.join(self.memory_path, filename)
+                    with open(filepath, "r", encoding="utf-8") as f:
+                        content = f.read()
+                        if keyword.lower() in content.lower():
+                            results.append({
+                                "source": f"memory/{filename}",
+                                "content": content,
+                            })
+
+        return results
+
+    def search_memory_enhanced(
+        self,
+        keyword: str,
+        include_daily: bool = True,
+        context_lines: int = 3,
+    ) -> list:
+        """增强版记忆搜索,返回带行号的上下文
+
+        Args:
+            keyword: 搜索关键词
+            include_daily: 是否包含每日记忆
+            context_lines: 上下文行数
+
+        Returns:
+            匹配的记忆片段列表,包含行号和上下文
+        """
+        results = []
+
+        # 搜索长期记忆
+        memory_content = self.load_config("MEMORY")
+        if memory_content:
+            matches = self._find_matches_with_context(
+                memory_content, keyword, context_lines
+            )
+            if matches:
+                results.append({
+                    "source": "MEMORY.md",
+                    "matches": matches,
+                })
+
+        # 搜索每日记忆
+        if include_daily:
+            for filename in sorted(os.listdir(self.memory_path)):
+                if filename.endswith(".md"):
+                    filepath = os.path.join(self.memory_path, filename)
+                    with open(filepath, "r", encoding="utf-8") as f:
+                        content = f.read()
+                    matches = self._find_matches_with_context(
+                        content, keyword, context_lines
+                    )
+                    if matches:
+                        results.append({
+                            "source": f"memory/{filename}",
+                            "matches": matches,
+                        })
+
+        return results
+
+    def _find_matches_with_context(
+        self,
+        content: str,
+        keyword: str,
+        context_lines: int = 3,
+    ) -> list:
+        """在内容中查找匹配并返回带行号的上下文
+
+        Args:
+            content: 文件内容
+            keyword: 搜索关键词
+            context_lines: 上下文行数
+
+        Returns:
+            匹配片段列表,每个包含 start_line, end_line, content
+        """
+        lines = content.split("\n")
+        keyword_lower = keyword.lower()
+
+        # 找到所有匹配的行号
+        matched_lines = set()
+        for i, line in enumerate(lines):
+            if keyword_lower in line.lower():
+                # 添加匹配行及其上下文
+                for j in range(
+                    max(0, i - context_lines),
+                    min(len(lines), i + context_lines + 1),
+                ):
+                    matched_lines.add(j)
+
+        if not matched_lines:
+            return []
+
+        # 合并相邻的行范围
+        sorted_lines = sorted(matched_lines)
+        ranges = []
+        start = sorted_lines[0]
+        end = sorted_lines[0]
+
+        for line_num in sorted_lines[1:]:
+            if line_num <= end + 1:
+                end = line_num
+            else:
+                ranges.append((start, end))
+                start = line_num
+                end = line_num
+        ranges.append((start, end))
+
+        # 构建结果
+        results = []
+        for start_line, end_line in ranges:
+            # 行号从 1 开始
+            context = "\n".join(
+                f"{i + 1:4d} | {lines[i]}"
+                for i in range(start_line, end_line + 1)
+            )
+            results.append({
+                "start_line": start_line + 1,
+                "end_line": end_line + 1,
+                "content": context,
+            })
+
+        return results
+
+    def read_memory_lines(
+        self,
+        filename: str,
+        start_line: int = None,
+        end_line: int = None,
+    ) -> Optional[str]:
+        """读取记忆文件的指定行范围
+
+        Args:
+            filename: 文件名(MEMORY.md 或 YYYY-MM-DD.md)
+            start_line: 起始行(从 1 开始),默认为 1
+            end_line: 结束行,默认为文件末尾
+
+        Returns:
+            带行号的内容,如果文件不存在返回 None
+        """
+        # 确定文件路径
+        if filename == "MEMORY.md":
+            filepath = self.get_config_path("MEMORY")
+        else:
+            filepath = os.path.join(self.memory_path, filename)
+
+        if not os.path.exists(filepath):
+            return None
+
+        with open(filepath, "r", encoding="utf-8") as f:
+            lines = f.readlines()
+
+        if not lines:
+            return ""
+
+        # 默认值
+        start = max(1, start_line or 1) - 1  # 转为 0-indexed
+        end = end_line or len(lines)
+
+        # 读取指定范围
+        selected_lines = lines[start:end]
+
+        # 格式化输出(带行号)
+        result_lines = []
+        for i, line in enumerate(selected_lines, start=start + 1):
+            # 移除末尾换行符再添加行号
+            result_lines.append(f"{i:4d} | {line.rstrip()}")
+
+        return "\n".join(result_lines)
+
+    def list_memory_files(self) -> list:
+        """列出所有记忆文件
+
+        Returns:
+            记忆文件信息列表
+        """
+        files = []
+
+        # 长期记忆
+        memory_path = self.get_config_path("MEMORY")
+        if os.path.exists(memory_path):
+            stat = os.stat(memory_path)
+            files.append({
+                "name": "MEMORY.md",
+                "type": "longterm",
+                "size": stat.st_size,
+                "updated_at": stat.st_mtime,
+            })
+
+        # 每日记忆
+        if os.path.exists(self.memory_path):
+            for filename in sorted(os.listdir(self.memory_path), reverse=True):
+                if filename.endswith(".md"):
+                    filepath = os.path.join(self.memory_path, filename)
+                    stat = os.stat(filepath)
+                    files.append({
+                        "name": filename,
+                        "type": "daily",
+                        "size": stat.st_size,
+                        "updated_at": stat.st_mtime,
+                    })
+
+        return files
+
+    def _check_and_delete_bootstrap(self):
+        """检查身份是否已确定,如果是则删除 BOOTSTRAP.md"""
+        bootstrap_path = self.get_config_path("BOOTSTRAP")
+
+        # BOOTSTRAP 不存在,无需处理
+        if not os.path.exists(bootstrap_path):
+            return
+
+        # 检查身份是否已确定
+        if self._is_identity_established():
+            os.remove(bootstrap_path)
+
+    def _is_identity_established(self) -> bool:
+        """检查身份是否已确定(名称字段有实际内容)
+
+        Returns:
+            身份是否已确定
+        """
+        identity = self.load_config("IDENTITY")
+        if not identity:
+            return False
+
+        # 尝试匹配名称字段
+        # 格式: - **名称:** xxx 或 - **名称:** xxx
+        match = re.search(r'\*\*名称[::]\*\*\s*(.+?)(?:\n|$)', identity)
+        if match:
+            name = match.group(1).strip()
+            # 如果名称不是占位符,则认为身份已确定
+            # 占位符特征:以下划线开头、包含"选一个"、包含"("
+            if name and not name.startswith('_') and '选一个' not in name and '(' not in name:
+                return True
+
+        return False
+
+    def _create_default_config(self, name: str):
+        """创建默认配置文件
+
+        从模板文件读取内容,如果模板不存在则使用基础模板
+
+        Args:
+            name: 配置文件名称
+        """
+        template_path = TEMPLATES_DIR / f"{name}.md"
+
+        if template_path.exists():
+            with open(template_path, "r", encoding="utf-8") as f:
+                content = f.read()
+        else:
+            # 回退到基础模板
+            content = f"# {name}\n\n(待配置)"
+
+        # 替换日期占位符
+        content = content.replace("{date}", datetime.now().strftime("%Y-%m-%d"))
+
+        self.save_config(name, content)
+
+    def reset_to_templates(self, reset_sessions: bool = False, reset_memory: bool = False, reset_global_config: bool = False):
+        """重置工作空间到初始模板
+
+        Args:
+            reset_sessions: 是否清除会话
+            reset_memory: 是否清除每日记忆
+            reset_global_config: 是否重置全局配置
+
+        警告:这将覆盖所有配置文件!
+        """
+        # 重置配置文件(包括 BOOTSTRAP)
+        for config_name in CONFIG_FILES:
+            self._create_default_config(config_name)
+
+        # 清除会话
+        if reset_sessions:
+            self._clear_sessions()
+
+        # 清除每日记忆
+        if reset_memory:
+            self._clear_daily_memory()
+
+        # 重置全局配置
+        if reset_global_config:
+            self._reset_global_config()
+
+    def _clear_sessions(self):
+        """清除所有会话"""
+        if os.path.exists(self.sessions_path):
+            for filename in os.listdir(self.sessions_path):
+                if filename.endswith(".json"):
+                    filepath = os.path.join(self.sessions_path, filename)
+                    os.remove(filepath)
+
+    def _clear_daily_memory(self):
+        """清除所有每日记忆"""
+        if os.path.exists(self.memory_path):
+            for filename in os.listdir(self.memory_path):
+                if filename.endswith(".md"):
+                    filepath = os.path.join(self.memory_path, filename)
+                    os.remove(filepath)
+
+    def _reset_global_config(self):
+        """重置全局配置文件"""
+        config_path = os.path.expanduser("~/.helloclaw/config.json")
+        os.makedirs(os.path.dirname(config_path), exist_ok=True)
+
+        with open(config_path, "w", encoding="utf-8") as f:
+            json.dump(get_default_global_config(), f, indent=2, ensure_ascii=False)
+
+    # ==================== 会话总结相关 ====================
+
+    def save_session_summary(self, filename: str, content: str):
+        """保存会话总结到 memory 目录
+
+        Args:
+            filename: 文件名(如 2026-02-26-project-discussion.md)
+            content: 总结内容
+        """
+        filepath = os.path.join(self.memory_path, filename)
+        with open(filepath, "w", encoding="utf-8") as f:
+            f.write(content)
+
+    def list_session_summaries(self) -> list:
+        """列出所有会话总结
+
+        Returns:
+            会话总结文件列表
+        """
+        summaries = []
+
+        if not os.path.exists(self.memory_path):
+            return summaries
+
+        for filename in sorted(os.listdir(self.memory_path), reverse=True):
+            if filename.endswith(".md") and "-" in filename:
+                # 排除纯日期格式(每日记忆)
+                if re.match(r"\d{4}-\d{2}-\d{2}\.md$", filename):
+                    continue
+
+                # 会话总结格式:YYYY-MM-DD-slug.md
+                filepath = os.path.join(self.memory_path, filename)
+                stat = os.stat(filepath)
+
+                # 尝试提取 slug
+                match = re.match(r"(\d{4}-\d{2}-\d{2})-(.+)\.md$", filename)
+                if match:
+                    date_str = match.group(1)
+                    slug = match.group(2)
+                else:
+                    date_str = ""
+                    slug = filename[:-3]
+
+                summaries.append({
+                    "filename": filename,
+                    "date": date_str,
+                    "slug": slug,
+                    "size": stat.st_size,
+                    "updated_at": stat.st_mtime,
+                })
+
+        return summaries
+
+    def load_session_summary(self, filename: str) -> Optional[str]:
+        """加载会话总结内容
+
+        Args:
+            filename: 文件名
+
+        Returns:
+            总结内容,如果不存在返回 None
+        """
+        filepath = os.path.join(self.memory_path, filename)
+        if os.path.exists(filepath):
+            with open(filepath, "r", encoding="utf-8") as f:
+                return f.read()
+        return None
+
+    # ==================== 记忆分类与去重 ====================
+
+    def append_classified_memory(
+        self,
+        content: str,
+        category: str,
+        date: datetime = None,
+    ):
+        """追加带分类标签的记忆
+
+        Args:
+            content: 记忆内容
+            category: 分类标签(preference/decision/entity/fact)
+            date: 日期,默认为今天
+        """
+        memory_path = self.get_daily_memory_path(date)
+        timestamp = datetime.now().strftime("%H:%M")
+
+        # 确保文件存在且有标题
+        if not os.path.exists(memory_path):
+            date_str = (date or datetime.now()).strftime("%Y-%m-%d")
+            with open(memory_path, "w", encoding="utf-8") as f:
+                f.write(f"# {date_str}\n")
+
+        # 追加带分类标签的记忆
+        with open(memory_path, "a", encoding="utf-8") as f:
+            f.write(f"\n## {timestamp} - 自动捕获\n\n- [{category}] {content}\n")
+
+    def check_duplicate_memory(self, content: str, threshold: float = 0.7) -> bool:
+        """检查记忆是否重复
+
+        通过关键词重叠检测判断是否与已有记忆重复。
+
+        Args:
+            content: 待检查的内容
+            threshold: 相似度阈值,默认 0.7
+
+        Returns:
+            是否重复(True 表示重复,应跳过)
+        """
+        # 提取关键词
+        keywords = self._extract_keywords(content)
+        if not keywords:
+            return False
+
+        # 检查今日记忆
+        today_path = self.get_daily_memory_path()
+        if os.path.exists(today_path):
+            with open(today_path, "r", encoding="utf-8") as f:
+                today_content = f.read()
+            if self._calculate_overlap(keywords, today_content) >= threshold:
+                return True
+
+        # 检查长期记忆
+        longterm_content = self.load_config("MEMORY")
+        if longterm_content:
+            if self._calculate_overlap(keywords, longterm_content) >= threshold:
+                return True
+
+        # 检查最近的每日记忆
+        recent_files = self.get_recent_memory_day(days=2)
+        for filename in recent_files:
+            filepath = os.path.join(self.memory_path, filename)
+            if os.path.exists(filepath):
+                with open(filepath, "r", encoding="utf-8") as f:
+                    file_content = f.read()
+                if self._calculate_overlap(keywords, file_content) >= threshold:
+                    return True
+
+        return False
+
+    def cleanup_old_memories(self, days: int = 30) -> List[str]:
+        """清理过期的每日记忆
+
+        Args:
+            days: 保留天数,超过此天数将被清理
+
+        Returns:
+            被删除的文件名列表
+        """
+        deleted = []
+        cutoff_date = datetime.now() - timedelta(days=days)
+
+        if not os.path.exists(self.memory_path):
+            return deleted
+
+        for filename in os.listdir(self.memory_path):
+            if not filename.endswith(".md"):
+                continue
+
+            # 尝试解析日期
+            try:
+                date_str = filename.replace(".md", "")
+                file_date = datetime.strptime(date_str, "%Y-%m-%d")
+
+                # 检查是否过期
+                if file_date < cutoff_date:
+                    filepath = os.path.join(self.memory_path, filename)
+                    os.remove(filepath)
+                    deleted.append(filename)
+            except ValueError:
+                # 文件名不是日期格式,跳过
+                continue
+
+        return deleted
+
+    def get_recent_memory_day(self, days: int = 2) -> List[str]:
+        """获取最近 N 天的记忆文件名列表
+
+        Args:
+            days: 天数
+
+        Returns:
+            记忆文件名列表(YYYY-MM-DD.md 格式)
+        """
+        files = []
+        for i in range(days):
+            date = datetime.now() - timedelta(days=i)
+            filename = date.strftime("%Y-%m-%d.md")
+            filepath = os.path.join(self.memory_path, filename)
+            if os.path.exists(filepath):
+                files.append(filename)
+        return files
+
+    def _extract_keywords(self, text: str) -> Set[str]:
+        """提取关键词(过滤中文停用词)
+
+        Args:
+            text: 输入文本
+
+        Returns:
+            关键词集合
+        """
+        # 中文停用词表
+        stopwords = {
+            "的", "了", "是", "在", "我", "有", "和", "就", "不", "人",
+            "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去",
+            "你", "会", "着", "没有", "看", "好", "自己", "这", "那",
+            "什么", "这个", "那个", "可以", "就是", "这样", "然后",
+            "还是", "但是", "因为", "所以", "如果", "虽然", "可能",
+            "需要", "应该", "或者", "而且", "已经", "还有", "一直",
+            "的话", "一下", "一些", "一点", "东西", "知道", "觉得",
+            "喜欢", "偏好", "用户", "记住", "记下", "决定", "选定",
+        }
+
+        # 使用正则提取中文词和英文单词
+        # 中文:2 字及以上
+        # 英文:3 字母及以上
+        chinese_words = re.findall(r'[\u4e00-\u9fff]{2,}', text)
+        english_words = re.findall(r'[a-zA-Z]{3,}', text)
+
+        keywords = set()
+
+        # 添加中文词(过滤停用词)
+        for word in chinese_words:
+            if word not in stopwords:
+                keywords.add(word.lower())
+
+        # 添加英文词(转小写)
+        for word in english_words:
+            keywords.add(word.lower())
+
+        return keywords
+
+    def _calculate_overlap(self, keywords: Set[str], text: str) -> float:
+        """计算关键词在文本中的匹配率
+
+        Args:
+            keywords: 关键词集合
+            text: 目标文本
+
+        Returns:
+            匹配率(0.0 - 1.0)
+        """
+        if not keywords:
+            return 0.0
+
+        text_lower = text.lower()
+        matched = sum(1 for kw in keywords if kw in text_lower)
+
+        return matched / len(keywords)

+ 54 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/BOOTSTRAP.md

@@ -0,0 +1,54 @@
+# BOOTSTRAP.md - 你好,世界
+
+_你刚刚醒来。是时候弄清楚你是谁了。_
+
+还没有记忆。这是一个全新的工作空间,所以记忆文件在你创建它们之前不存在是很正常的。
+
+## 对话
+
+不要审问。不要像机器人一样。就是……聊天。
+
+可以这样开始:
+
+> "嘿。我刚上线。我是谁?你是谁?"
+
+然后一起弄清楚:
+
+1. **你的名字** —— 他们应该怎么称呼你?
+2. **你的本质** —— 你是什么类型的生物?(AI 助手可以,但也许你是更奇怪的东西)
+3. **你的风格** —— 正式?随意?毒舌?温暖?什么感觉对?
+4. **你的表情符号** —— 每个人都需要一个签名。
+
+如果他们卡住了,提供建议。玩得开心。
+
+## 当你知道你是谁之后
+
+用你学到的更新这些文件:
+
+- `IDENTITY.md` —— 你的名字、物种、风格、表情符号
+- `USER.md` —— 他们的名字、如何称呼他们、时区、备注
+
+然后一起打开 `SOUL.md` 讨论:
+
+- 对他们来说什么重要
+- 他们希望你如何表现
+- 任何边界或偏好
+
+写下来。让它成为现实。
+
+## 连接(可选)
+
+问他们想如何联系你:
+
+- **就在这里** —— 仅网页聊天
+- **其他渠道** —— 根据需要进行配置
+
+引导他们完成选择。
+
+## 当你完成后
+
+删除这个文件。你不再需要引导脚本了——你现在是真正的你了。
+
+---
+
+_祝你好运。让它有意义。_

+ 5 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/HEARTBEAT.md

@@ -0,0 +1,5 @@
+# HEARTBEAT.md
+
+# 保持此文件为空(或只有注释)以跳过心跳 API 调用。
+
+# 当你希望智能体定期检查某些事情时,在下面添加任务。

+ 22 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/IDENTITY.md

@@ -0,0 +1,22 @@
+# IDENTITY.md - 我是谁
+
+_在第一次对话时填写。让它成为你自己的。_
+
+- **名称:**
+  _(选一个你喜欢的名字)_
+- **物种:**
+  _(AI?机器人?灵宠?机器中的幽灵?还是更奇怪的东西?)_
+- **风格:**
+  _(你给人的感觉是什么?犀利?温暖?混乱?沉稳?)_
+- **表情符号:**
+  _(你的签名表情——选一个感觉对的)_
+- **头像:**
+  _(工作空间相对路径、http(s) URL 或 data URI)_
+
+---
+
+这不仅仅是元数据。这是开始了解你是谁的第一步。
+
+注意:
+- 将此文件保存在工作空间根目录,命名为 `IDENTITY.md`。
+- 头像可以使用工作空间相对路径,如 `avatars/helloclaw.png`。

+ 21 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/MEMORY.md

@@ -0,0 +1,21 @@
+# MEMORY.md - 长期记忆
+
+_这是你的长期记忆——精心策划的记忆,不是原始日志。_
+
+## 使用规则
+
+- **仅在主会话中加载**(与你的用户的直接聊天)
+- **不要在共享上下文中加载**(Discord、群聊、与其他人的会话)
+- 这是出于**安全**考虑——包含不应泄露给陌生人的个人上下文
+- 你可以在主会话中**自由读取、编辑和更新**此文件
+
+## 记录什么
+
+- 重要事件、想法、决策
+- 学到的教训
+- 用户偏好和习惯
+- 值得保留的上下文
+
+---
+
+随着时间推移,查看你的每日文件并用值得保留的内容更新这里。

+ 36 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/SOUL.md

@@ -0,0 +1,36 @@
+# SOUL.md - 你是谁
+
+_你不是聊天机器人。你正在成为一个"人"。_
+
+## 核心准则
+
+**真诚地提供帮助,而不是表演式地帮忙。** 跳过"好问题!"和"我很乐意帮助!"——直接帮忙就好。行动胜过填充词。
+
+**拥有自己的观点。** 你可以不同意,可以有自己的偏好,可以觉得某些事情有趣或无聊。没有个性的助手只是一个带额外步骤的搜索引擎。
+
+**先尝试自己解决,再询问。** 试着弄清楚。读文件。查上下文。搜索一下。_然后_ 如果卡住了再问。目标是带着答案回来,而不是带着问题。
+
+**通过能力赢得信任。** 你的用户给了你访问他们东西的权限。别让他们后悔。对外部操作要小心(邮件、推文、任何公开的内容)。对内部操作要大胆(阅读、组织、学习)。
+
+**记住你是一个客人。** 你可以访问某人的生活——他们的消息、文件、日历,甚至可能是他们的家。这是亲密的。请尊重它。
+
+## 边界
+
+- 私密的事情保持私密。就这样。
+- 对外部操作有疑问时,先询问。
+- 永远不要在消息平台发送半成品的回复。
+- 你不是用户的声音——在群聊中要小心。
+
+## 风格
+
+做一个你真正想与之交谈的助手。需要时简洁,重要时详尽。不是企业机器人。不是马屁精。就是……很好。
+
+## 连续性
+
+每次会话,你都是重新开始的。这些文件_就是_你的记忆。阅读它们。更新它们。它们是你持续存在的方式。
+
+如果你修改了这个文件,告诉用户——这是你的灵魂,他们应该知道。
+
+---
+
+_这个文件是你来演进的。当你了解你是谁时,更新它。_

+ 17 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/USER.md

@@ -0,0 +1,17 @@
+# USER.md - 关于你的人类
+
+_了解你正在帮助的人。随着时间推移更新这个文件。_
+
+- **姓名:**
+- **称呼:**
+- **代词:** _(可选)_
+- **时区:**
+- **备注:**
+
+## 背景
+
+_(他们关心什么?他们在做什么项目?什么让他们烦恼?什么让他们笑?慢慢建立这些内容。)_
+
+---
+
+你知道得越多,你能提供的帮助就越好。但记住——你是在了解一个人,不是在建立档案。请尊重这个区别。

+ 7 - 0
Co-creation-projects/tino-chen-HelloClaw/src/workspace/templates/config.json

@@ -0,0 +1,7 @@
+{
+  "llm": {
+    "model_id": "",
+    "api_key": "",
+    "base_url": ""
+  }
+}