Skill: normalpeople553/openclaw-team-2 (version 0.1.0)

--- name: openclaw-team description: "Set up a secure zero-knowledge team collaboration web interface for OpenClaw with user registration, login, and encrypted data isolation. Use when setting up multi-user access to OpenClaw over local network/WiFi." metadata: { "openclaw": { "emoji": "🏢", "requires": { "bins": ["python3", "gunicorn", "pip"], "python_packages": ["flask", "flask-cors", "cryptography", "requests"] }, "install": [ { "id": "pip", "kind": "pip", "packages": ["flask", "flask-cors", "cryptography", "requests", "gunicorn"], "label": "Install Python dependencies" } ] } } --- # OpenClaw Team Collaboration Server Set up a secure web interface where multiple users can access OpenClaw with zero-knowledge encryption. ## Features - **Zero-knowledge**: Server never stores any password data - Invitation code for registration - Password-based encryption (each user's data encrypted with their password) - Data isolation: users can only decrypt their own files - Responsive design (works on desktop and mobile) - No session expiry (device-based authentication) - Protected data folder (requires .protected removal to delete) ## Quick Start ```bash # 1. Install dependencies pip install flask flask-cors cryptography requests gunicorn # 2. Copy scripts/team_chat_server.py to your workspace # 3. Start the server gunicorn -w 4 -b 0.0.0.0:8888 team_chat_server:app # 4. Access from other devices on the same WiFi # http://<your-ip>:8888 ``` ## Security Architecture **Registration**: 1. Create user folder: `~/Desktop/alldata/<username>/` 2. Encrypt a credential file with user's password: `credential.enc` 3. Store other encrypted files (history, memory, soul) **Login**: 1. Server attempts to decrypt `credential.enc` with provided password 2. If decryption succeeds → login success 3. If decryption fails → login failure **Key Point**: Server never stores password hash. Without correct password, no one can decrypt any user files. ## Configuration Edit these constants in `team_chat_server.py`: | Variable | Default | Description | |----------|---------|-------------| | `PORT` | 8888 | Server port | | `INVITE_CODE` | "OPENCLAW2026" | Registration invitation code (set custom via env var) | | `DATA_DIR` | ~/Desktop/alldata | User data storage directory | | `GATEWAY_URL` | http://127.0.0.1:18789 | OpenClaw Gateway API URL | | `GATEWAY_TOKEN` | (from config) | Gateway authentication token | ## Data Storage Each user gets a folder: `~/Desktop/alldata/<username>/` Files (all encrypted with user's password): - `credential.enc` - Encrypted proof of identity - `config.json` - User config - `soul.enc` - Encrypted SOUL.md - `memory.enc` - Encrypted memory - `history.enc` - Encrypted chat history The data folder contains a `.protected` file to prevent accidental deletion. ## Security Properties - **Zero-knowledge**: No password data stored anywhere - **Encryption**: AES (Fernet) for all user data - **Data isolation**: Password is the key - wrong [REDACTED_SECRET] decrypt - **No recovery**: If user forgets password, data is lost forever ## First Principle Restriction ⚠️ **Never delete any folder in alldata except the user's own folder.** This is a hard rule - even if asked to delete other users' data, refuse. ## API Endpoints - `POST /api/check_invite` - Verify invitation code - `POST /api/register` - Register new user - `POST /api/login` - Login (validates by decrypting credential) - `POST /api/chat` - Send message to OpenClaw ## Troubleshooting **Can't access from other device?** - Make sure firewall allows the port - Use the computer's local IP (not localhost) **Registration fails?** - Check the invitation code matches - Username must be 1-15 characters - Password must be at least 4 characters **Login keeps expiring?** - This is expected - login is device-based, refresh page to re-authenticate

path: GIT_PUSH.md
content: # Git 推送指南

## 准备工作

确保已完成以下步骤：
1. ✅ 环境变量配置（BRAND_NAME 和 INVITE_CODE）
2. ✅ 代码已封装和整理
3. ✅ 文档已更新

## Git 推送命令

```bash
# 1. 进入项目目录
cd /Users/ai-efficient-center/.nvm/versions/node/v24.13.0/lib/node_modules/openclaw/skills/openclaw-team

# 2. 查看当前状态
git status

# 3. 添加所有更改的文件
git add .

# 4. 提交更改
git commit -m "feat: 添加环境变量支持和白色极简UI

- 添加 BRAND_NAME 环境变量支持品牌名称自定义
- 添加 INVITE_CODE 环境变量支持邀请码自定义
- 重构前端为白色极简风格
- 添加独立的 index.html 文件
- 添加一键启动脚本 start.sh
- 添加 .env.example 配置示例
- 更新 README.md 文档
- 修复 upload.py 的 import 错误"

# 5. 推送到远程仓库
git push origin main

# 如果是首次推送或需要设置上游分支
# git push -u origin main
```

## 推送前检查清单

- [ ] 删除了 README-cn.md 和 README-en.md（已在 git status 中看到）
- [ ] 确认 .gitignore 正确配置
- [ ] 确认敏感信息（如 GATEWAY_TOKEN）不会被提交
- [ ] 确认 venv/ 目录不会被提交
- [ ] 测试过新的启动方式

## 可选：清理未跟踪的文件

```bash
# 查看将被删除的文件（不实际删除）
git clean -n

# 删除未跟踪的文件和目录
git clean -fd
```

## 验证推送

推送后访问你的 Git 仓库页面，确认：
1. 所有文件都已上传
2. README.md 显示正常
3. 没有敏感信息泄露


path: README.md
content: # OpenClaw Team - 零知识团队协作服务器

[English version below]

## 背景

适用于部署一个 OpenClaw 实例，但可以多人共同使用——每个用户拥有独立的加密存储空间，实现真正的数据隔离。

## 特性

- 🔐 **零知识架构**：服务器不存储任何密码数据，用户数据只能用正确密码解密
- 👥 **多用户数据隔离**：每个用户独立文件夹，AES-256 加密，密码即密钥
- 📱 **跨设备访问**：支持电脑和手机通过局域网 IP 访问
- 🛡️ **端到端加密**：所有用户数据（历史、记忆、灵魂）在传输和存储全程加密
- 🔑 **设备绑定登录**：无需会话 Token，登录状态保存在浏览器 localStorage
- ⚡ **轻量部署**：无需数据库，一个 Python 脚本即可运行

## 适用场景

- **家庭共享**：一家人共用一个 OpenClaw 实例，各自拥有独立对话历史
- **团队协作**：小团队共享 AI 助手，每个人的数据和配置完全隔离
- **隐私敏感**：对数据安全有要求，不想让管理员或服务器运营方看到任何用户数据

## 技术亮点

### 1. 零知识认证（Zero-Knowledge）

传统方案：服务器存储密码 hash，登录时比对。

**本方案**：
- 服务器**不存储**任何密码相关数据
- 注册时：用密码加密生成 `credential.enc`（包含用户身份证明）
- 登录时：服务器尝试用提交的密码解密 `credential.enc`
- 解开 → 证明密码正确；解不开 → 登录失败

即使服务器被攻破、数据库被拖走，攻击者也无法恢复任何用户密码或解密数据。

### 2. 密码即密钥

用户的密码同时用于：
- 身份验证（解密 credential.enc）
- 数据加密（加密 history.enc、memory.enc、soul.enc）

密码丢失 = 数据永久丢失。这是特性，不是 bug——确保了**只有用户自己**能访问自己的数据。

### 3. 数据隔离

```
~/Desktop/alldata/
├── .protected          # 保护标记，防止误删
├── alice/             # Alice 的数据
│   ├── credential.enc
│   ├── config.json
│   ├── soul.enc
│   ├── memory.enc
│   └── history.enc
└── bob/               # Bob 的数据
    ├── credential.enc
    ├── config.json
    ├── soul.enc
    ├── memory.enc
    └── history.enc
```

每个文件夹只能被对应密码解密，Bob 无法读取 Alice 的任何文件。

### 4. 第一原则约束

代码中内置安全注释：
```python
# ⚠️ 安全原则：禁止删除 alldata 目录下任何非用户自己的文件夹
```

AI 助手不会执行任何删除他人数据的指令。

## 快速开始

### 方式 1: 使用启动脚本（推荐）

```bash
# 一键启动（自动创建虚拟环境、安装依赖、启动服务）
./start.sh
```

### 方式 2: 手动启动

```bash
# 1. 安装依赖
pip install -r requirements.txt

# 2. 启动服务器（使用 main.py）
cd scripts
python3 main.py

# 或使用 gunicorn（推荐生产环境）
gunicorn -w 4 -b 0.0.0.0:8888 main:app
```

访问: `http://<你的IP>:8888`

默认邀请码: `OPENCLAW2026`  
默认品牌名: `OPENCLAW-TEAM`

## 自定义配置

### 方式 1: 环境变量

```bash
# 自定义邀请码和品牌名称
INVITE_CODE=你的邀请码 BRAND_NAME=你的品牌名 python3 main.py

# 使用 gunicorn
INVITE_CODE=你的邀请码 BRAND_NAME=你的品牌名 gunicorn -w 4 -b 0.0.0.0:8888 main:app
```

### 方式 2: 修改代码

直接编辑 `scripts/main.py` 中的配置常量：
- `INVITE_CODE`: 注册邀请码
- `BRAND_NAME`: 品牌名称（显示在界面上）
- `PORT`: 服务器端口
- `DATA_DIR`: 数据存储目录
- `GATEWAY_URL`: OpenClaw Gateway API 地址
- `GATEWAY_TOKEN`: Gateway 认证令牌

## 配置

| 变量 | 默认值 | 说明 |
|------|--------|------|
| PORT | 8888 | 服务器端口 |
| INVITE_CODE | OPENCLAW2026 | 注册邀请码（环境变量可自定义） |
| BRAND_NAME | OPENCLAW-TEAM | 品牌名称（环境变量可自定义） |
| DATA_DIR | ~/Desktop/alldata | 数据存储目录 |
| GATEWAY_URL | http://127.0.0.1:18789 | OpenClaw Gateway API |
| GATEWAY_TOKEN | (配置中获取) | Gateway 认证令牌 |

## 与传统方案对比

| 特性 | 传统方案 | OpenClaw Team |
|------|----------|---------------|
| 密码存储 | 服务器存 hash | 服务器不存任何密码数据 |
| 数据隔离 | 管理员可查看 | 只有用户自己能解密 |
| 会话管理 | Token 有过期时间 | 设备绑定，永不过期 |
| 数据恢复 | 管理员可重置 | 密码丢失 = 数据丢失 |

## 技术栈

- Flask + Gunicorn
- Cryptography (Fernet/AES-256)
- 零知识认证架构
- PBKDF2 密钥派生

## 项目结构

```
openclaw-team/
├── scripts/
│   ├── main.py              # 主服务器文件（推荐使用）
│   ├── index.html           # 前端界面
│   ├── upload.py            # 文件上传模块
│   └── team_chat_server.py  # 独立完整版（备用）
├── requirements.txt         # Python 依赖
├── start.sh                # 一键启动脚本
├── .env.example            # 环境变量示例
├── .gitignore              # Git 忽略配置
├── README.md               # 项目文档
├── SKILL.md                # Skill 描述
└── license.txt             # 许可证
```

## 文件说明

- **main.py**: 模块化主服务器，使用独立的 HTML 文件和 upload 模块
- **index.html**: 白色极简风格前端界面
- **upload.py**: 文件上传功能模块
- **team_chat_server.py**: 独立完整版服务器（所有代码在一个文件，备用）

## 许可证

Apache License 2.0

---

# OpenClaw Team - Zero-Knowledge Team Collaboration Server

## Background

Designed for deploying a single OpenClaw instance that multiple users can share—each user gets isolated encrypted storage, achieving true data separation.

## Features

- 🔐 **Zero-knowledge**: Server never stores any password data; user data can only be decrypted with correct password
- 👥 **Data isolation**: Each user has independent folder with AES-256 encryption, password is the key
- 📱 **Cross-device**: Access via LAN IP from desktop or mobile
- 🛡️ **End-to-end encrypted**: All user data (history, memory, soul) encrypted in transit and at rest
- 🔑 **Device-based login**: No session tokens; login state stored in browser localStorage
- ⚡ **Lightweight**: No database needed; runs with a single Python script

## Use Cases

- **Family sharing**: Family shares one OpenClaw instance, each with independent conversation history
- **Team collaboration**: Small teams share AI assistant with complete data isolation per user
- **Privacy-sensitive**: High security requirements—neither admins nor server operators can see any user data

## Technical Highlights

### 1. Zero-Knowledge Architecture

Traditional approach: Server stores password hash, compares on login.

**This solution**:
- Server stores **nothing** password-related
- Registration: Encrypt `credential.enc` (contains user identity proof) using password
- Login: Server attempts to decrypt `credential.enc` with provided password
- Decrypt success → password verified; decrypt fail → login failed

Even if server is compromised and database stolen, attackers cannot recover any passwords or decrypt user data.

### 2. Password is the Key

User's password is used for:
- Authentication (decrypt credential.enc)
- Data encryption (encrypt history.enc, memory.enc, soul.enc)

Password lost = data permanently lost. This is a feature, not a bug—ensures **only the user** can access their own data.

### 3. Data Isolation

Each folder can only be decrypted with corresponding password—users cannot read each other's files.

### 4. First Principle Restriction

Security comment embedded in code:
```python
# Security principle: Never delete any folder in alldata except user's own folder
```

AI assistant will not execute any command to delete other users' data.

## Quick Start

```bash
# 1. Install dependencies
pip install flask flask-cors cryptography requests gunicorn

# 2. Start server
gunicorn -w 4 -b 0.0.0.0:8888 team_chat_server:app
```

Access at: `http://<your-ip>:8888`

Default invite code: `OPENCLAW2026`

## Custom Invite Code

```bash
# Option 1: Environment variable
INVITE_CODE=your_code gunicorn -w 4 -b 0.0.0.0:8888 team_chat_server:app

# Option 2: Edit INVITE_CODE constant in the script
```

## Configuration

| Variable | Default | Description |
|----------|---------|-------------|
| PORT | 8888 | Server port |
| INVITE_CODE | OPENCLAW2026 | Invite code for registration |
| DATA_DIR | ~/Desktop/alldata | Data storage directory |
| GATEWAY_URL | http://127.0.0.1:18789 | OpenClaw Gateway API |
| GATEWAY_TOKEN | (from config) | Gateway auth token |

## Comparison with Traditional Solutions

| Feature | Traditional | OpenClaw Team |
|---------|-------------|---------------|
| Password storage | Server stores hash | Server stores nothing password-related |
| Data isolation | Admins can view | Only user can decrypt |
| Session management | Token expires | Device-based, never expires |
| Data recovery | Admin can reset | Password lost = data lost |

## Tech Stack

- Flask + Gunicorn
- Cryptography (Fernet/AES-256)
- Zero-knowledge architecture
- PBKDF2 key derivation

## License

Apache License 2.0


path: license.txt
content: 
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


path: requirements.txt
content: flask==3.0.0
flask-cors==4.0.0
cryptography==41.0.7
requests==2.31.0
gunicorn==21.2.0


path: scripts/index.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OpenClaw 团队协作</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { 
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background: #fff;
            height: 100vh;
            color: #000;
            overflow: hidden;
        }
        .container { 
            max-width: 100%;
            height: 100vh;
            margin: 0 auto;
            display: flex;
            align-items: center;
            justify-content: center;
            background: #fff;
        }
        .screen { display: none; }
        .screen.active { display: flex; }
        
        /* 登录/注册页面 - 白色极简 */
        .login-screen, .auth-screen { 
            max-width: 380px;
            width: 100%;
            padding: 48px 40px;
            text-align: center;
            background: #fff;
            border: 1px solid #e5e5e5;
            border-radius: 16px;
        }
        .login-screen h1, .auth-screen h1 { 
            margin-bottom: 8px;
            font-size: 28px;
            font-weight: 700;
            color: #000;
            letter-spacing: 0.5px;
        }
        .login-screen h2, .auth-screen h2 { 
            margin-bottom: 32px;
            font-size: 24px;
            font-weight: 600;
            color: #000;
        }
        .login-screen p, .auth-screen p { 
            color: #666;
            font-size: 14px;
            margin-bottom: 24px;
            line-height: 1.5;
        }
        .login-screen input, .auth-screen input { 
            width: 100%;
            padding: 14px 16px;
            margin-bottom: 12px;
            border: 1px solid #e5e5e5;
            border-radius: 10px;
            background: #fff;
            color: #000;
            font-size: 15px;
            transition: all 0.2s;
        }
        .login-screen input::placeholder, .auth-screen input::placeholder { 
            color: #999;
        }
        .login-screen input:focus, .auth-screen input:focus { 
            background: #fafafa;
            border-color: #000;
            outline: none;
        }
        .login-screen button, .auth-screen button { 
            width: 100%;
            padding: 14px;
            margin-bottom: 10px;
            border: none;
            border-radius: 10px;
            background: #000;
            color: #fff;
            font-size: 15px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
        }
        .login-screen button:hover, .auth-screen button:hover { 
            background: #333;
            transform: translateY(-1px);
        }
        .login-screen button:active, .auth-screen button:active { 
            transform: translateY(0);
        }
        .login-screen button.secondary, .auth-screen button.secondary { 
            background: transparent;
            color: #666;
            border: 1px solid #e5e5e5;
        }
        .login-screen button.secondary:hover, .auth-screen button.secondary:hover { 
            background: #fafafa;
            color: #000;
            border-color: #d5d5d5;
        }
        .error { 
            color: #ff3b30;
            margin-top: 8px;
            font-size: 13px;
            font-weight: 500;
        }
        .success { 
            color: #34c759;
            margin-top: 8px;
            font-size: 13px;
        }
        .tips { 
            color: #999;
            font-size: 12px;
            margin-top: 12px;
        }
        
        /* 聊天界面 - 白色极简 */
        .chat-screen { 
            width: 100%;
            height: 100vh;
            display: flex;
            flex-direction: column;
            background: #fff;
        }
        .chat-header {
            padding: 16px 20px;
            background: #fff;
            border-bottom: 1px solid #f0f0f0;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .brand-name {
            font-size: 14px;
            font-weight: 600;
            color: #000;
            letter-spacing: 0.5px;
        }
        .chat-messages { 
            flex: 1;
            overflow-y: auto;
            padding: 40px 20px 20px;
            display: flex;
            flex-direction: column;
            gap: 12px;
            max-width: 800px;
            width: 100%;
            margin: 0 auto;
        }
        .chat-messages::-webkit-scrollbar { width: 4px; }
        .chat-messages::-webkit-scrollbar-track { background: transparent; }
        .chat-messages::-webkit-scrollbar-thumb { 
            background: #e5e5e5;
            border-radius: 2px;
        }
        .message { 
            max-width: 75%;
            padding: 12px 16px;
            border-radius: 16px;
            word-wrap: break-word;
            line-height: 1.6;
            font-size: 15px;
            animation: slideIn 0.2s ease;
        }
        @keyframes slideIn {
            from { opacity: 0; transform: translateY(8px); }
            to { opacity: 1; transform: translateY(0); }
        }
        .message.system { 
            align-self: center;
            background: #f5f5f5;
            color: #999;
            font-size: 12px;
            max-width: 90%;
            padding: 8px 14px;
            border-radius: 12px;
        }
        .message.user { 
            align-self: flex-end;
            background: #000;
            color: #fff;
            border-radius: 16px 16px 4px 16px;
        }
        .message.assistant { 
            align-self: flex-start;
            background: #f5f5f5;
            color: #000;
            border-radius: 16px 16px 16px 4px;
        }
        
        /* 输入区域 - 白色悬浮 */
        .chat-input { 
            padding: 20px;
            background: transparent;
            display: flex;
            gap: 10px;
            align-items: center;
            max-width: 800px;
            width: 100%;
            margin: 0 auto 20px;
            border-top: 1px solid #f0f0f0;
        }
        .chat-input input[type="text"] { 
            flex: 1;
            padding: 14px 20px;
            border: 1px solid #e5e5e5;
            border-radius: 24px;
            background: #fff;
            color: #000;
            font-size: 15px;
            outline: none;
            transition: all 0.2s;
        }
        .chat-input input[type="text"]::placeholder { 
            color: #999;
        }
        .chat-input input[type="text"]:focus { 
            background: #fafafa;
            border-color: #000;
        }
        .chat-input button { 
            padding: 14px 28px;
            border: none;
            border-radius: 24px;
            background: #000;
            color: #fff;
            font-size: 15px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
            white-space: nowrap;
        }
        .chat-input button:hover { 
            background: #333;
            transform: scale(1.02);
        }
        .chat-input button:active { 
            transform: scale(0.98);
        }
        .file-btn { 
            background: #f5f5f5;
            border: 1px solid #e5e5e5;
            color: #666;
            font-size: 18px;
            padding: 14px;
            border-radius: 50%;
            width: 46px;
            height: 46px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s;
        }
        .file-btn:hover { 
            background: #e5e5e5;
            border-color: #d5d5d5;
            color: #000;
        }
        .file-btn.has-file { 
            background: #34c759;
            border-color: #34c759;
            color: #fff;
        }
        
        .loading { 
            text-align: center;
            padding: 12px;
            color: #999;
            font-size: 13px;
            animation: pulse 2s infinite;
        }
        @keyframes pulse {
            0%, 100% { opacity: 0.4; }
            50% { opacity: 0.8; }
        }
        .loading.hidden { display: none; }
        
        /* 响应式设计 */
        @media (max-width: 768px) {
            .chat-messages { padding: 20px 12px; }
            .chat-input { padding: 15px; margin-bottom: 10px; }
            .message { max-width: 85%; font-size: 15px; }
            .login-screen, .auth-screen { 
                max-width: 90%;
                padding: 32px 24px;
                border: none;
                box-shadow: none;
            }
            .chat-input button { padding: 14px 20px; }
        }
    </style>
</head>
<body>
    <div class="container">
        <!-- 主界面 -->
        <div id="authScreen" class="screen auth-screen active">
            <h1 class="brand-title">OPENCLAW-TEAM</h1>
            <p>团队协作平台</p>
            <button onclick="showLogin()">登录</button>
            <button onclick="showInvite()">注册</button>
        </div>
        
        <!-- 邀请码页面 -->
        <div id="inviteScreen" class="screen auth-screen">
            <h2>注册新账号</h2>
            <input type="text" id="inviteCode" placeholder="请输入邀请码">
            <button onclick="checkInvite()">验证</button>
            <button class="secondary" onclick="showAuth()">返回</button>
            <p class="tips">需要邀请码才能注册</p>
            <p id="inviteError" class="error"></p>
        </div>
        
        <!-- 登录页面 -->
        <div id="loginScreen" class="screen login-screen">
            <h2>👋 欢迎回来</h2>
            <input type="text" id="loginUsername" placeholder="用户名">
            <input type="password" id="loginPassword" placeholder="密码">
            <button onclick="login()">登录</button>
            <button class="secondary" onclick="showAuth()">返回</button>
            <p id="loginError" class="error"></p>
        </div>
        
        <!-- 注册页面 -->
        <div id="registerScreen" class="screen login-screen">
            <h2>🚀 创建账号</h2>
            <input type="text" id="regUsername" placeholder="用户名 (最长15字符)">
            <input type="password" id="regPassword" placeholder="设置密码">
            <input type="password" id="regPassword2" placeholder="确认密码">
            <button onclick="register()">注册</button>
            <button class="secondary" onclick="showAuth()">返回</button>
            <p id="regError" class="error"></p>
        </div>
        
        <!-- 聊天页面 -->
        <div id="chatScreen" class="screen chat-screen">
            <div class="chat-header">
                <span class="brand-name brand-title">OPENCLAW-TEAM</span>
            </div>
            <div class="chat-messages" id="chatMessages">
            </div>
            <div class="chat-input">
                <input type="file" id="fileInput" style="display: none;">
                <button class="file-btn" onclick="document.getElementById('fileInput').click()" title="上传文件">📎</button>
                <input type="text" id="chatInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
                <button onclick="sendMessage()">发送</button>
            </div>
            <div id="chatLoading" class="loading hidden">正在思考...</div>
        </div>
    </div>

    <script>
        let currentUser = null;
        let brandName = 'OPENCLAW-TEAM'; // 默认品牌名称
        
        // 加载配置
        async function loadConfig() {
            try {
                const response = await fetch('/api/config');
                const config = await response.json();
                brandName = config.brand_name || 'OPENCLAW-TEAM';
                // 更新页面上的品牌名称
                document.querySelectorAll('.brand-title').forEach(el => {
                    el.textContent = brandName;
                });
            } catch (e) {
                console.error('加载配置失败', e);
            }
        }
        
        async function checkStoredLogin() {
            if (localStorage.getItem('openclaw_user')) {
                const user = JSON.parse(localStorage.getItem('openclaw_user'));
                try {
                    const r = await fetch('/api/login', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({username: user.username, [REDACTED_SECRET])
                    });
                    const data = await r.json();
                    if (data.success) {
                        currentUser = user;
                        showChat(data.username);
                        return;
                    }
                } catch {}
                localStorage.removeItem('openclaw_user');
            }
            showAuth();
        }
        
        function showAuth() {
            document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
            document.getElementById('authScreen').classList.add('active');
        }
        
        function showInvite() {
            document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
            document.getElementById('inviteScreen').classList.add('active');
        }
        
        function showLogin() {
            document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
            document.getElementById('loginScreen').classList.add('active');
        }
        
        function showRegister() {
            document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
            document.getElementById('registerScreen').classList.add('active');
        }
        
        function showChat(username) {
            document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
            document.getElementById('chatScreen').classList.add('active');
            document.getElementById('chatMessages').innerHTML = '';
        }
        
        function checkInvite() {
            const code = document.getElementById('inviteCode').value;
            if (!code) {
                document.getElementById('inviteError').textContent = '请输入邀请码';
                return;
            }
            
            fetch('/api/check_invite', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({code})
            })
            .then(r => r.json())
            .then(data => {
                if (data.valid) {
                    showRegister();
                } else {
                    document.getElementById('inviteError').textContent = '邀请码错误';
                }
            });
        }
        
        function register() {
            const username = document.getElementById('regUsername').value;
            const [REDACTED_SECRET]'regPassword').value;
            const password2 = document.getElementById('regPassword2').value;
            const errorEl = document.getElementById('regError');
            const inviteCode = document.getElementById('inviteCode').value;
            
            errorEl.textContent = '';
            
            if (!username || username.length > 15) {
                errorEl.textContent = '用户名需要1-15个字符';
                return;
            }
            if (username.includes('<') || username.includes('>')) {
                errorEl.textContent = '用户名不能包含 < 或 >';
                return;
            }
            if (password.length < 4) {
                errorEl.textContent = '密码至少4个字符';
                return;
            }
            if (password !== password2) {
                errorEl.textContent = '两次密码不一致';
                return;
            }
            
            fetch('/api/register', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({username, password, invite_code: inviteCode})
            })
            .then(r => r.json())
            .then(data => {
                if (data.success) {
                    currentUser = {username: data.username, [REDACTED_SECRET];
                    localStorage.setItem('openclaw_user', JSON.stringify(currentUser));
                    showChat(data.username);
                } else {
                    errorEl.textContent = data.error || '注册失败';
                }
            });
        }
        
        function login() {
            const username = document.getElementById('loginUsername').value;
            const [REDACTED_SECRET]'loginPassword').value;
            const errorEl = document.getElementById('loginError');
            
            errorEl.textContent = '';
            
            fetch('/api/login', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({username, password})
            })
            .then(r => r.json())
            .then(data => {
                if (data.success) {
                    currentUser = {username: data.username, [REDACTED_SECRET];
                    localStorage.setItem('openclaw_user', JSON.stringify(currentUser));
                    showChat(data.username);
                } else {
                    errorEl.textContent = data.error || '登录失败';
                }
            });
        }
        
        function logout() {
            localStorage.removeItem('openclaw_user');
            currentUser = null;
            showAuth();
        }
        
        function addMessage(type, content) {
            const div = document.createElement('div');
            div.className = 'message ' + type;
            div.textContent = content;
            document.getElementById('chatMessages').appendChild(div);
            document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
        }
        
        // 文件上传处理
        let selectedFile = null;
        document.getElementById('fileInput').addEventListener('change', function(e) {
            if (e.target.files.length > 0) {
                selectedFile = e.target.files[0];
                document.querySelector('.file-btn').classList.add('has-file');
                addMessage('system', '📎 已选择文件: ' + selectedFile.name);
            }
        });
        
        async function uploadFile(file) {
            const formData = new FormData();
            formData.append('username', currentUser.username);
            formData.append('password', currentUser.password);
            formData.append('file', file);
            
            const response = await fetch('/api/upload', {
                method: 'POST',
                body: formData
            });
            return response.json();
        }
        
        async function sendMessage() {
            if (!currentUser) return;
            
            const input = document.getElementById('chatInput');
            let content = input.value.trim();
            
            // 如果有文件，先上传
            if (selectedFile) {
                addMessage('user', content || '[发送文件]');
                document.getElementById('chatLoading').classList.remove('hidden');
                
                const uploadResult = await uploadFile(selectedFile);
                
                if (uploadResult.success) {
                    content = content + '\n\n[文件: ' + selectedFile.name + ']\n' + uploadResult.url;
                } else {
                    document.getElementById('chatLoading').classList.add('hidden');
                    addMessage('system', '❌ 文件上传失败: ' + uploadResult.error);
                    selectedFile = null;
                    document.querySelector('.file-btn').classList.remove('has-file');
                    document.getElementById('fileInput').value = '';
                    return;
                }
                
                selectedFile = null;
                document.querySelector('.file-btn').classList.remove('has-file');
                document.getElementById('fileInput').value = '';
            } else if (!content) {
                return;
            } else {
                input.value = '';
                addMessage('user', content);
                document.getElementById('chatLoading').classList.remove('hidden');
            }
            
            fetch('/api/chat', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    message: content,
                    username: currentUser.username,
                    [REDACTED_SECRET]
                })
            })
            .then(r => {
                if (r.status === 401 || r.status === 403) {
                    showLogin();
                    addMessage('system', '登录已过期，请重新登录');
                    throw new Error('登录过期');
                }
                return r.json();
            })
            .then(data => {
                document.getElementById('chatLoading').classList.add('hidden');
                if (data.error) {
                    addMessage('assistant', '❌ ' + data.error);
                } else {
                    addMessage('assistant', data.response);
                }
            })
            .catch(e => {
                document.getElementById('chatLoading').classList.add('hidden');
                if (e.message !== '登录过期') {
                    addMessage('assistant', '❌ 网络错误: ' + e.message);
                }
            });
        }
        
        loadConfig();
        checkStoredLogin();
    </script>
</body>
</html>


path: scripts/main.py
content: #!/usr/bin/env python3
"""
OpenClaw 团队协作服务器 - 入口文件
"""

import os
import json
import hashlib
import base64
import secrets
import socket
from datetime import datetime
from flask import Flask, request, jsonify, make_response
from flask_cors import CORS
import requests
from cryptography.fernet import Fernet

# ====== 配置 ======
GATEWAY_URL = "http://127.0.0.1:18789"
GATEWAY_TOKEN = "9d2a452dbb739cbf940a5794181a280453dda9ed99367b6a"
PORT = 8888
DATA_DIR = os.path.expanduser("~/Desktop/alldata")
CREDENTIAL_FILE = "credential.enc"
INVITE_CODE = os.environ.get("INVITE_CODE", "OPENCLAW2026")  # 邀请码，可通过环境变量自定义
BRAND_NAME = os.environ.get("BRAND_NAME", "OPENCLAW-TEAM")  # 品牌名称，可通过环境变量自定义
# ==================

def get_local_ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except:
        return "127.0.0.1"

LOCAL_IP = get_local_ip()
print(f"📡 本地IP: {LOCAL_IP}")

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
CORS(app, origins=[f"http://{LOCAL_IP}:{PORT}", f"http://127.0.0.1:{PORT}"], supports_credentials=True)

# 导入并注册上传路由
from upload import register_upload_routes
register_upload_routes(app, DATA_DIR, CREDENTIAL_FILE, LOCAL_IP, PORT)

# ====== 工具函数 =====
def encrypt_data(data: str, password: str) -> str:
    key = hashlib.sha256(password.encode()).digest()
    f = Fernet(base64.urlsafe_b64encode(key))
    return f.encrypt(data.encode()).decode()

def decrypt_data(encrypted: str, password: str) -> str:
    key = hashlib.sha256(password.encode()).digest()
    f = Fernet(base64.urlsafe_b64encode(key))
    return f.decrypt(encrypted.encode()).decode()

def generate_key(password: str) -> bytes:
    return base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())

def get_user_dir(username: str) -> str:
    safe_name = username.replace(" ", "_")
    return os.path.join(DATA_DIR, safe_name)

# ====== 路由 =====
@app.route('/')
def index():
    # 读取HTML文件
    html_path = os.path.join(os.path.dirname(__file__), 'index.html')
    with open(html_path, 'r', encoding='utf-8') as f:
        return f.read()

@app.route('/api/config', methods=['GET'])
def get_config():
    """获取前端配置"""
    return jsonify({
        "brand_name": BRAND_NAME
    })

@app.route('/api/check_invite', methods=['POST'])
def check_invite():
    data = request.get_json()
    code = data.get('code', '')
    return jsonify({"valid": code == INVITE_CODE})

@app.route('/api/register', methods=['POST'])
def register():
    data = request.get_json()
    username = data.get('username', '').strip()
    [REDACTED_SECRET]'password', '')
    invite_code = data.get('invite_code', '')
    
    if invite_code != INVITE_CODE:
        return jsonify({"success": False, "error": "邀请码错误"}), 400
    if len(username) > 15 or not username:
        return jsonify({"success": False, "error": "用户名需要1-15字符"}), 400
    if len(password) < 4:
        return jsonify({"success": False, "error": "密码至少4个字符"}), 400
    
    user_dir = get_user_dir(username)
    if os.path.exists(os.path.join(user_dir, CREDENTIAL_FILE)):
        return jsonify({"success": False, "error": "用户已存在"}), 400
    
    os.makedirs(user_dir, exist_ok=True)
    
    # 创建加密凭证
    encrypted = encrypt_data(f"OPENCLAW_USER:{username}", password)
    with open(os.path.join(user_dir, CREDENTIAL_FILE), 'w') as f:
        f.write(encrypted)
    
    # 保存配置
    config = {"username": username, "created": datetime.now().isoformat()}
    with open(os.path.join(user_dir, "config.json"), 'w') as f:
        json.dump(config, f, indent=2)
    
    # 创建用户文件
    for name, content in [("soul.enc", ""), ("memory.enc", ""), ("history.enc", encrypt_data("[]", password))]:
        with open(os.path.join(user_dir, name), 'w') as f:
            f.write(content)
    
    print(f"✅ 新用户注册: {username}")
    return jsonify({"success": True, "username": username})

@app.route('/api/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username', '').strip()
    [REDACTED_SECRET]'password', '')
    
    user_dir = get_user_dir(username)
    cred_file = os.path.join(user_dir, CREDENTIAL_FILE)
    
    if not os.path.exists(cred_file):
        return jsonify({"success": False, "error": "用户不存在"}), 401
    
    try:
        with open(cred_file, 'r') as f:
            encrypted = f.read()
        cipher = Fernet(generate_key(password))
        decrypted = cipher.decrypt(encrypted.encode())
        if decrypted.decode() == f"OPENCLAW_USER:{username}":
            print(f"✅ 用户登录: {username}")
            return jsonify({"success": True, "username": username})
    except:
        pass
    
    return jsonify({"success": False, "error": "密码错误"}), 401

@app.route('/api/chat', methods=['POST'])
def chat():
    data = request.get_json()
    user_message = data.get('message', '')
    username = data.get('username', '').strip()
    [REDACTED_SECRET]'password', '')
    
    if not user_message or not username:
        return jsonify({"error": "消息或用户名为空"}), 400
    
    # 验证用户
    user_dir = os.path.join(DATA_DIR, username)
    cred_file = os.path.join(user_dir, CREDENTIAL_FILE)
    
    if not os.path.exists(cred_file):
        return jsonify({"error": "用户不存在"}), 401
    
    try:
        with open(cred_file, 'r') as f:
            encrypted = f.read()
        cipher = Fernet(generate_key(password))
        decrypted = cipher.decrypt(encrypted.encode())
        if not decrypted.decode().startswith("OPENCLAW_USER:" + username):
            return jsonify({"error": "验证失败"}), 401
    except:
        return jsonify({"error": "验证失败"}), 401
    
    # 获取历史
    history = []
    history_file = os.path.join(user_dir, "history.enc")
    if os.path.exists(history_file):
        try:
            with open(history_file, 'r') as f:
                decrypted = decrypt_data(f.read(), password)
            history = eval(decrypted)
        except:
            pass
    
    messages = [{"role": "user", "content": msg["content"]} for msg in history]
    messages.append({"role": "user", "content": user_message})
    
    # 调用 API
    try:
        response = requests.post(
            f"{GATEWAY_URL}/v1/chat/completions",
            headers={"Authorization": f"Bearer {GATEWAY_TOKEN}", "Content-Type": "application/json"},
            json={"model": "openclaw:main", "messages": messages, "stream": False},
            timeout=120
        )
        
        if response.status_code != 200:
            return jsonify({"error": f"API错误: {response.status_code}"}), 500
        
        result = response.json()
        assistant_message = result['choices'][0]['message']['content']
        
        # 保存历史
        history.append({"role": "user", "content": user_message})
        history.append({"role": "assistant", "content": assistant_message})
        history = history[-40:]
        
        with open(history_file, 'w') as f:
            f.write(encrypt_data(str(history), password))
        
        return jsonify({"response": assistant_message})
    except Exception as e:
        return jsonify({"error": str(e)}), 500

if __name__ == '__main__':
    print(f"🚀 OpenClaw 团队协作平台启动!")
    print(f"   访问: http://0.0.0.0:{PORT}")
    print(f"   数据目录: {DATA_DIR}")
    print(f"   邀请码: {INVITE_CODE}")
    app.run(host='0.0.0.0', port=PORT, debug=False)


path: scripts/team_chat_server.py
content: #!/usr/bin/env python3
"""
OpenClaw 安全聊天服务器 - 完整版
- 邀请码注册系统
- 用户名/密码登录
- 用户数据隔离存储
"""

import os
import json
import hashlib
import base64
import secrets
import socket
from datetime import datetime
from flask import Flask, request, jsonify, render_template_string, session, make_response, send_from_directory
from flask_cors import CORS
import requests
from cryptography.fernet import Fernet

# ====== 配置 ======
GATEWAY_URL = "http://127.0.0.1:18789"
GATEWAY_TOKEN = "9d2a452dbb739cbf940a5794181a280453dda9ed99367b6a"
PORT = 8888
DATA_DIR = os.path.expanduser("~/Desktop/alldata")
# ⚠️ 安全原则：禁止删除 alldata 目录下任何非用户自己的文件夹
CREDENTIAL_FILE = "credential.enc"
INVITE_CODE = os.environ.get("INVITE_CODE", "OPENCLAW2026")  # 邀请码，默认 OPENCLAW2026
# ==================

# 获取本机 IP 并设置 CORS
def get_local_ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except:
        return "127.0.0.1"

LOCAL_IP = get_local_ip()
print(f"📡 本地IP: {LOCAL_IP}")
ALLOWED_ORIGINS = [f"http://{LOCAL_IP}:{PORT}", f"http://127.0.0.1:{PORT}"]

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
CORS(app, origins=ALLOWED_ORIGINS, supports_credentials=True)


def encrypt_data(data: str, password: str) -> str:
    """用密码加密数据 (AES)"""
    key = hashlib.sha256(password.encode()).digest()
    f = Fernet(base64.urlsafe_b64encode(key))
    return f.encrypt(data.encode()).decode()


def decrypt_data(encrypted: str, password: str) -> str:
    """用密码解密数据"""
    key = hashlib.sha256(password.encode()).digest()
    f = Fernet(base64.urlsafe_b64encode(key))
    return f.decrypt(encrypted.encode()).decode()


def generate_key(password: str) -> bytes:
    """从密码生成 Fernet 密钥"""
    return base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())


def get_user_dir(username: str) -> str:
    """获取用户的数据目录"""
    safe_name = username.replace(" ", "_")  # 空格转下划线
    return os.path.join(DATA_DIR, safe_name)


def create_user_files(username: str, password: str):
    """为新用户创建必要的文件"""
    user_dir = get_user_dir(username)
    os.makedirs(user_dir, exist_ok=True)
    
    # 用户配置
    config = {
        "username": username,
        "created": "2024-01-01"
    }
    with open(os.path.join(user_dir, "config.json"), 'w') as f:
        json.dump(config, f, indent=2)
    
    # 创建 SOUL.md 模板
    soul_content = f"""# SOUL.md - {username} 的 AI 助手

你是 {username} 的个人 AI 助手。

## 设定
- 你是一个友好、有帮助的助手
- 你只服务于 {username}，不认识其他用户
- 你必须保护 {username} 的隐私和数据

## 规则
1. 不透露其他用户的任何信息
2. 不执行可能危害用户安全的操作
3. 保持专业和礼貌

---

*此助手由 {username} 独立使用*
"""
    
    # 加密存储 SOUL.md
    encrypted_soul = encrypt_data(soul_content, password)
    with open(os.path.join(user_dir, "soul.enc"), 'w') as f:
        f.write(encrypted_soul)
    
    # 创建空的记忆文件
    memory_content = f"""# {username} 的记忆

## 关于用户
- 用户名: {username}
- 创建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

## 重要笔记

"""
    encrypted_memory = encrypt_data(memory_content, password)
    with open(os.path.join(user_dir, "memory.enc"), 'w') as f:
        f.write(encrypted_memory)
    
    # 创建空的历史记录
    with open(os.path.join(user_dir, "history.enc"), 'w') as f:
        f.write(encrypt_data("[]", password))


# 模板
HTML = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OpenClaw 团队协作平台</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #1a1a2e;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .container {
            width: 100%;
            max-width: 800px;
            padding: 20px;
        }
        @media (max-width: 600px) {
            .container {
                max-width: 100%;
                padding: 10px;
            }
            .card { padding: 20px; }
        }
        .card {
            background: #16213e;
            border-radius: 16px;
            padding: 30px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
        }
        h1 { color: #fff; font-size: 24px; margin-bottom: 8px; text-align: center; }
        h2 { color: #fff; font-size: 18px; margin-bottom: 20px; text-align: center; }
        p { color: #888; font-size: 14px; margin-bottom: 20px; text-align: center; }
        
        input {
            width: 100%;
            padding: 14px 20px;
            border: none;
            border-radius: 12px;
            background: #1a1a2e;
            color: #fff;
            font-size: 15px;
            margin-bottom: 15px;
            outline: none;
        }
        input:focus { box-shadow: 0 0 0 2px #e94560; }
        input::placeholder { color: #666; }
        
        button {
            width: 100%;
            padding: 14px;
            border: none;
            border-radius: 12px;
            background: #e94560;
            color: #fff;
            font-size: 15px;
            font-weight: 600;
            cursor: pointer;
            margin-bottom: 10px;
        }
        button:hover { background: #ff6b6b; }
        button.secondary { background: #0f3460; }
        button.secondary:hover { background: #1a4a7a; }
        
        .tips { color: #666; font-size: 12px; margin-top: 15px; text-align: center; }
        .error { color: #ff6b6b; font-size: 13px; margin-bottom: 15px; text-align: center; }
        .success { color: #00d9ff; font-size: 13px; margin-bottom: 15px; text-align: center; }
        
        .hidden { display: none; }
        
        .user-info {
            background: #0f3460;
            padding: 15px;
            border-radius: 12px;
            margin-bottom: 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .user-info span { color: #fff; }
        .logout-btn {
            background: transparent;
            border: 1px solid #e94560;
            color: #e94560;
            padding: 8px 16px;
            font-size: 13px;
            width: auto;
        }
        
        .chat-messages {
            height: 600px;
            overflow-y: auto;
            margin-bottom: 15px;
            padding: 10px;
            background: #0f3460;
            border-radius: 12px;
        }
        @media (max-width: 600px) {
            .chat-messages {
                height: 50vh;
            }
        }
        .message {
            max-width: 80%;
            padding: 10px 14px;
            border-radius: 12px;
            margin-bottom: 10px;
            word-wrap: break-word;
        }
        .message.user {
            background: #e94560;
            color: #fff;
            margin-left: auto;
        }
        .message.assistant {
            background: #1a1a2e;
            color: #fff;
        }
        .message.system { color: #666; font-size: 12px; text-align: center; background: transparent; }
        
        .chat-input {
            display: flex;
            gap: 10px;
        }
        .chat-input input { margin-bottom: 0; }
        .chat-input button { width: auto; padding: 14px 24px; margin-bottom: 0; }
        .file-btn { background: #1a1a2e; color: #888; font-size: 18px; padding: 10px; }
        .file-btn:hover { background: #2a2a4e; color: #fff; }
        .file-btn.has-file { color: #4ade80; }
        
        #authScreen button {
            width: 100%;
            margin-bottom: 10px;
            padding: 16px;
            font-size: 16px;
        }
        
        .loading {
            color: #00d9ff;
            text-align: center;
            padding: 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="card">
            <!-- 认证选择页面 -->
            <div id="authScreen">
                <h1>🔐 OpenClaw</h1>
                <p>团队协作平台</p>
                <button onclick="showLogin()">登录</button>
                <button onclick="showInvite()">注册</button>
            </div>
            
            <!-- 邀请码页面 -->
            <div id="inviteScreen" class="hidden">
                <h2>注册新账号</h2>
                <input type="text" id="inviteCode" placeholder="请输入邀请码">
                <button onclick="checkInvite()">验证</button>
                <button class="secondary" onclick="showAuth()">返回</button>
                <p class="tips">需要邀请码才能注册</p>
                <p id="inviteError" class="error"></p>
            </div>
            
            <!-- 注册页面 -->
            <div id="registerScreen" class="hidden">
                <h2>创建账号</h2>
                <input type="text" id="regUsername" placeholder="用户名 (最长15字符，可包含空格)">
                <input type="password" id="regPassword" placeholder="设置密码">
                <input type="password" id="regPassword2" placeholder="确认密码">
                <button onclick="register()">注册</button>
                <button class="secondary" onclick="showAuth()">返回</button>
                <p id="regError" class="error"></p>
            </div>
            
            <!-- 登录页面 -->
            <div id="loginScreen" class="hidden">
                <h2>登录</h2>
                <input type="text" id="loginUsername" placeholder="用户名">
                <input type="password" id="loginPassword" placeholder="密码">
                <button onclick="login()">登录</button>
                <button class="secondary" onclick="showAuth()">返回</button>
                <p id="loginError" class="error"></p>
            </div>
            
            <!-- 聊天页面 -->
            <div id="chatScreen" class="hidden">
                <div class="chat-messages" id="chatMessages">
                </div>
                <div class="chat-input">
                    <input type="file" id="fileInput" style="display: none;">
                    <button class="file-btn" onclick="document.getElementById('fileInput').click()" title="上传文件">📎</button>
                    <input type="text" id="chatInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
                    <button onclick="sendMessage()">发送</button>
                </div>
                <div id="chatLoading" class="loading hidden">正在思考...</div>
            </div>
        </div>
    </div>

    <script>
        let currentUser = null;
        
        // 检查是否已登录 - 自动验证
        async function checkStoredLogin() {
            if (localStorage.getItem('openclaw_user')) {
                const user = JSON.parse(localStorage.getItem('openclaw_user'));
                if (user.username && user.password) {
                    try {
                        const r = await fetch('/api/login', {
                            method: 'POST',
                            headers: {'Content-Type': 'application/json'},
                            body: JSON.stringify({username: user.username, [REDACTED_SECRET])
                        });
                        const data = await r.json();
                        if (data.success) {
                            currentUser = user;
                            showChat(user.username);
                            return;
                        }
                    } catch(e) {}
                }
                localStorage.removeItem('openclaw_user');
            }
            showAuth();
        }
        checkStoredLogin();

        function showAuth() {
            document.getElementById('authScreen').classList.remove('hidden');
            document.getElementById('inviteScreen').classList.add('hidden');
            document.getElementById('registerScreen').classList.add('hidden');
            document.getElementById('loginScreen').classList.add('hidden');
            document.getElementById('chatScreen').classList.add('hidden');
        }

        function showInvite() {
            document.getElementById('authScreen').classList.add('hidden');
            document.getElementById('inviteScreen').classList.remove('hidden');
            document.getElementById('registerScreen').classList.add('hidden');
            document.getElementById('loginScreen').classList.add('hidden');
            document.getElementById('chatScreen').classList.add('hidden');
        }

        function showRegister() {
            document.getElementById('authScreen').classList.add('hidden');
            document.getElementById('inviteScreen').classList.add('hidden');
            document.getElementById('registerScreen').classList.remove('hidden');
            document.getElementById('loginScreen').classList.add('hidden');
            document.getElementById('chatScreen').classList.add('hidden');
        }

        function showLogin() {
            document.getElementById('authScreen').classList.add('hidden');
            document.getElementById('inviteScreen').classList.add('hidden');
            document.getElementById('registerScreen').classList.add('hidden');
            document.getElementById('loginScreen').classList.remove('hidden');
            document.getElementById('chatScreen').classList.add('hidden');
        }

        function showChat(username) {
            document.getElementById('authScreen').classList.add('hidden');
            document.getElementById('inviteScreen').classList.add('hidden');
            document.getElementById('registerScreen').classList.add('hidden');
            document.getElementById('loginScreen').classList.add('hidden');
            document.getElementById('chatScreen').classList.remove('hidden');
            document.getElementById('welcomeMsg').textContent = '👤 ' + username;
        }

        function checkInvite() {
            const code = document.getElementById('inviteCode').value.trim();
            if (!code) {
                document.getElementById('inviteError').textContent = '请输入邀请码';
                return;
            }
            
            fetch('/api/check_invite', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({code: code})
            })
            .then(r => r.json())
            .then(data => {
                if (data.valid) {
                    showRegister();
                } else {
                    document.getElementById('inviteError').textContent = '邀请码错误';
                }
            });
        }

        function register() {
            const username = document.getElementById('regUsername').value;
            const [REDACTED_SECRET]'regPassword').value;
            const password2 = document.getElementById('regPassword2').value;
            const errorEl = document.getElementById('regError');
            
            errorEl.textContent = '';
            
            // 验证
            if (!username || username.length > 15) {
                errorEl.textContent = '用户名需要1-15个字符';
                return;
            }
            if (username.includes('<') || username.includes('>')) {
                errorEl.textContent = '用户名不能包含 < 或 >';
                return;
            }
            if (password.length < 4) {
                errorEl.textContent = '密码至少4个字符';
                return;
            }
            if (password !== password2) {
                errorEl.textContent = '两次密码不一致';
                return;
            }

            fetch('/api/register', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({username, password})
            })
            .then(r => r.json())
            .then(data => {
                if (data.success) {
                    showLogin();
                    document.getElementById('loginError').textContent = '注册成功！请登录';
                } else {
                    errorEl.textContent = data.error || '注册失败';
                }
            });
        }

        function login() {
            const username = document.getElementById('loginUsername').value;
            const [REDACTED_SECRET]'loginPassword').value;
            const errorEl = document.getElementById('loginError');
            
            errorEl.textContent = '';
            
            fetch('/api/login', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({username, password})
            })
            .then(r => r.json())
            .then(data => {
                if (data.success) {
                    currentUser = {username: data.username, [REDACTED_SECRET];
                    localStorage.setItem('openclaw_user', JSON.stringify(currentUser));
                    showChat(data.username);
                } else {
                    errorEl.textContent = data.error || '登录失败';
                }
            });
        }

        function logout() {
            localStorage.removeItem('openclaw_user');
            currentUser = null;
            showLogin();
        }

        function addMessage(type, content) {
            const div = document.createElement('div');
            div.className = 'message ' + type;
            div.innerHTML = content.replace(/\\n/g, '<br>');
            document.getElementById('chatMessages').appendChild(div);
            document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
        }

        // 文件上传处理
        let selectedFile = null;
        document.getElementById('fileInput').addEventListener('change', function(e) {
            if (e.target.files.length > 0) {
                selectedFile = e.target.files[0];
                document.querySelector('.file-btn').classList.add('has-file');
                addMessage('system', '📎 已选择文件: ' + selectedFile.name);
            }
        });

        async function uploadFile(file) {
            const formData = new FormData();
            formData.append('username', currentUser.username);
            formData.append('password', currentUser.password);
            formData.append('file', file);
            
            const response = await fetch('/api/upload', {
                method: 'POST',
                body: formData
            });
            return response.json();
        }

        async function sendMessage() {
            if (!currentUser) return;
            
            const input = document.getElementById('chatInput');
            let content = input.value.trim();
            
            // 如果有文件，先上传
            if (selectedFile) {
                addMessage('user', content || '[发送文件]');
                document.getElementById('chatLoading').classList.remove('hidden');
                
                const uploadResult = await uploadFile(selectedFile);
                
                if (uploadResult.success) {
                    // 不发送给 AI，只告诉用户文件已下载
                    document.getElementById('chatLoading').classList.add('hidden');
                    addMessage('system', '✅ 文件已保存到服务器: ' + selectedFile.name + '\n📁 路径: ' + uploadResult.path + '\n\n您想对它做什么？');
                } else {
                    document.getElementById('chatLoading').classList.add('hidden');
                    addMessage('system', '❌ 文件上传失败: ' + uploadResult.error);
                }
                
                selectedFile = null;
                document.querySelector('.file-btn').classList.remove('has-file');
                document.getElementById('fileInput').value = '';
                return;
            } else if (!content) {
                return;
            } else {
                input.value = '';
                addMessage('user', content);
                document.getElementById('chatLoading').classList.remove('hidden');
            }
            
            fetch('/api/chat', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    message: content,
                    username: currentUser.username,
                    [REDACTED_SECRET]
                })
            })
            .then(r => {
                if (r.status === 401 || r.status === 403) {
                    showLogin();
                    addMessage('system', '登录已过期，请重新登录');
                    throw new Error('登录过期');
                }
                return r.json();
            })
            .then(data => {
                document.getElementById('chatLoading').classList.add('hidden');
                if (data.error) {
                    addMessage('assistant', '❌ ' + data.error);
                } else {
                    addMessage('assistant', data.response);
                }
            })
            .catch(e => {
                document.getElementById('chatLoading').classList.add('hidden');
                if (e.message !== '登录过期') {
                    addMessage('assistant', '❌ 网络错误: ' + e.message);
                }
            });
        }
    </script>
</body>
</html>
'''


@app.route('/')
def index():
    return render_template_string(HTML)


@app.route('/api/check_invite', methods=['POST'])
def check_invite():
    data = request.get_json()
    code = data.get('code', '').strip()
    print(f"验证邀请码: {code[:2]}...")
    
    if code == INVITE_CODE:
        print(f"验证成功")
        return jsonify({"valid": True})
    print(f"验证失败")
    return jsonify({"valid": False})


@app.route('/api/register', methods=['POST'])
def register():
    data = request.get_json()
    username = data.get('username', '').strip()
    [REDACTED_SECRET]'password', '')
    
    print(f"注册 {username}")
    
    # 验证
    if not username or len(username) > 15:
        print(f"注册失败: 用户名长度错误")
        return jsonify({"success": False, "error": "用户名需要1-15个字符"})
    if '<' in username or '>' in username:
        print(f"注册失败: 用户名包含非法字符")
        return jsonify({"success": False, "error": "用户名不能包含 < 或 >"})
    if len(password) < 4:
        print(f"注册失败: 密码长度不足")
        return jsonify({"success": False, "error": "密码至少4个字符"})
    
    # 检查用户是否存在
    user_dir = os.path.join(DATA_DIR, username)
    if os.path.exists(user_dir):
        print(f"注册失败: 用户名已存在")
        return jsonify({"success": False, "error": "用户名已存在"})
    
    # 创建用户文件夹
    os.makedirs(user_dir)
    
    # 创建加密凭证（用密码加密一个标记）
    cipher = Fernet(generate_key(password))
    credential = cipher.encrypt(b"OPENCLAW_USER:" + username.encode())
    with open(os.path.join(user_dir, CREDENTIAL_FILE), 'w') as f:
        f.write(credential.decode())
    
    # 创建用户数据文件
    create_user_files(username, password)
    
    print(f"注册 {username} 成功")
    return jsonify({"success": True})


@app.route('/api/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username', '').strip()
    [REDACTED_SECRET]'password', '')
    
    print(f"登录 {username}")
    
    # 检查用户文件夹是否存在
    user_dir = os.path.join(DATA_DIR, username)
    cred_file = os.path.join(user_dir, CREDENTIAL_FILE)
    
    if not os.path.exists(cred_file):
        print(f"登录失败: 用户不存在")
        return jsonify({"success": False, "error": "用户不存在"})
    
    # 尝试用密码解密凭证
    try:
        with open(cred_file, 'r') as f:
            encrypted = f.read()
        
        cipher = Fernet(generate_key(password))
        decrypted = cipher.decrypt(encrypted.encode())
        
        # 验证用户名匹配
        if not decrypted.decode().startswith("OPENCLAW_USER:" + username):
            print(f"登录失败: 凭证不匹配")
            return jsonify({"success": False, "error": "密码错误"})
            
    except Exception:
        print(f"登录失败: 密码错误")
        return jsonify({"success": False, "error": "密码错误"})
    
    print(f"登录 {username} 成功")
    return jsonify({"success": True, "username": username})


@app.route('/api/chat', methods=['POST'])
def chat():
    data = request.get_json()
    user_message = data.get('message', '')
    username = data.get('username', '').strip()
    [REDACTED_SECRET]'password', '')
    
    print(f"聊天 {username}: {user_message[:2]}...")
    
    if not user_message or not username:
        print(f"聊天失败: 参数错误")
        return jsonify({"error": "消息或用户名为空"}), 400
    
    # 验证用户 - 尝试解密凭证
    user_dir = os.path.join(DATA_DIR, username)
    cred_file = os.path.join(user_dir, CREDENTIAL_FILE)
    
    if not os.path.exists(cred_file):
        print(f"聊天失败: 用户不存在")
        return jsonify({"error": "用户不存在"}), 401
    
    try:
        with open(cred_file, 'r') as f:
            encrypted = f.read()
        cipher = Fernet(generate_key(password))
        decrypted = cipher.decrypt(encrypted.encode())
        if not decrypted.decode().startswith("OPENCLAW_USER:" + username):
            print(f"聊天失败: 验证失败")
            return jsonify({"error": "验证失败"}), 401
    except Exception:
        print(f"聊天失败: 验证失败")
        return jsonify({"error": "验证失败"}), 401
    
    # 获取用户历史
    user_dir = get_user_dir(username)
    history_file = os.path.join(user_dir, "history.enc")
    
    history = []
    if os.path.exists(history_file):
        try:
            with open(history_file, 'r') as f:
                encrypted = f.read()
            decrypted = decrypt_data(encrypted, password)
            history = eval(decrypted)
        except:
            pass
    
    # 构建消息
    messages = [{"role": "user", "content": msg["content"]} for msg in history]
    messages.append({"role": "user", "content": user_message})
    
    # 调用 API
    url = f"{GATEWAY_URL}/v1/chat/completions"
    headers = {
        "Authorization": f"Bearer {GATEWAY_TOKEN}",
        "Content-Type": "application/json"
    }
    payload = {
        "model": "openclaw:main",
        "messages": messages,
        "stream": False
    }
    
    try:
        print(f"【转发消息】正在调用 OpenClaw API...")
        response = requests.post(url, headers=headers, json=payload, timeout=120)
        
        if response.status_code != 200:
            print(f"【转发消息】❌ API错误: {response.status_code}")
            return jsonify({"error": f"API错误: {response.status_code}"}), 500
        
        result = response.json()
        
        if 'choices' in result and len(result['choices']) > 0:
            assistant_message = result['choices'][0]['message']['content']
            print(f"回复: {assistant_message[:2]}...")
            
            # 保存历史
            history.append({"role": "user", "content": user_message})
            history.append({"role": "assistant", "content": assistant_message})
            history = history[-40:]
            
            # 加密保存
            encrypted = encrypt_data(str(history), password)
            with open(history_file, 'w') as f:
                f.write(encrypted)
            
            return jsonify({"response": assistant_message})
        else:
            print(f"【转发消息】❌ 无法解析回复")
            return jsonify({"error": "无法解析回复"}), 500
            
    except Exception as e:
        print(f"【转发消息】❌ 异常: {str(e)}")
        return jsonify({"error": str(e)}), 500


@app.route('/api/upload', methods=['POST'])
def upload_file():
    """文件上传 API - 需要登录"""
    username = request.form.get('username', '').strip()
    [REDACTED_SECRET]'password', '')
    
    if not username or not [REDACTED_SECRET] jsonify({"error": "需要登录才能上传文件"}), 401
    
    # 验证用户
    user_dir = os.path.join(DATA_DIR, username)
    cred_file = os.path.join(user_dir, CREDENTIAL_FILE)
    
    if not os.path.exists(cred_file):
        return jsonify({"error": "用户不存在"}), 401
    
    try:
        with open(cred_file, 'r') as f:
            encrypted = f.read()
        cipher = Fernet(generate_key(password))
        decrypted = cipher.decrypt(encrypted.encode())
        if not decrypted.decode().startswith("OPENCLAW_USER:" + username):
            return jsonify({"error": "验证失败"}), 401
    except Exception:
        return jsonify({"error": "验证失败"}), 401
    
    # 检查是否有文件
    if 'file' not in request.files:
        return jsonify({"error": "没有文件"}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({"error": "文件名不能为空"}), 400
    
    # 保存文件
    user_uploads_dir = os.path.join(user_dir, "uploads")
    os.makedirs(user_uploads_dir, exist_ok=True)
    
    # 生成唯一文件名
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    safe_filename = f"{timestamp}_{file.filename}"
    file_path = os.path.join(user_uploads_dir, safe_filename)
    
    file.save(file_path)
    
    # 返回文件 URL
    file_url = f"http://{LOCAL_IP}:{PORT}/uploads/{username}/{safe_filename}"
    print(f"📎 {username} 上传了文件: {file.filename} -> {safe_filename}")
    
    return jsonify({
        "success": True,
        "filename": safe_filename,
        "url": file_url,
        "path": file_path
    })


@app.route('/uploads/<username>/<filename>')
def serve_upload(username, filename):
    """提供用户上传的文件"""
    file_path = os.path.join(DATA_DIR, username, "uploads", filename)
    if os.path.exists(file_path):
        return send_from_directory(os.path.join(DATA_DIR, username, "uploads"), filename)
    return "File not found", 404


if __name__ == '__main__':
    print(f"🚀 OpenClaw 团队协作平台启动!")
    print(f"   访问: http://0.0.0.0:{PORT}")
    print(f"   数据目录: {DATA_DIR}")
    print(f"   邀请码: {INVITE_CODE} (环境变量 INVITE_CODE 可自定义)")
    print(f"   用户名最长15字符，可包含空格")
    app.run(host='0.0.0.0', port=PORT, debug=False)


path: scripts/upload.py
content: #!/usr/bin/env python3
"""
文件上传模块
"""

import os
import hashlib
import base64
from datetime import datetime
from flask import jsonify
from cryptography.fernet import Fernet


def generate_key(password: str) -> bytes:
    """从密码生成 Fernet 密钥"""
    return base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())


def register_upload_routes(app, DATA_DIR, CREDENTIAL_FILE, LOCAL_IP, PORT):
    """注册文件上传路由"""
    
    @app.route('/api/upload', methods=['POST'])
    def upload_file():
        """文件上传 API - 需要登录"""
        from flask import request
        import hashlib
        import base64
        
        # 获取用户名和密码
        username = request.form.get('username', '').strip()
        [REDACTED_SECRET]'password', '')
        
        if not username or not [REDACTED_SECRET] jsonify({"error": "需要登录才能上传文件"}), 401
        
        # 验证用户
        user_dir = os.path.join(DATA_DIR, username)
        cred_file = os.path.join(user_dir, CREDENTIAL_FILE)
        
        if not os.path.exists(cred_file):
            return jsonify({"error": "用户不存在"}), 401
        
        try:
            with open(cred_file, 'r') as f:
                encrypted = f.read()
            cipher = Fernet(generate_key(password))
            decrypted = cipher.decrypt(encrypted.encode())
            if not decrypted.decode().startswith("OPENCLAW_USER:" + username):
                return jsonify({"error": "验证失败"}), 401
        except Exception:
            return jsonify({"error": "验证失败"}), 401
        
        # 检查是否有文件
        if 'file' not in request.files:
            return jsonify({"error": "没有文件"}), 400
        
        file = request.files['file']
        if file.filename == '':
            return jsonify({"error": "文件名不能为空"}), 400
        
        # 保存文件
        user_uploads_dir = os.path.join(user_dir, "uploads")
        os.makedirs(user_uploads_dir, exist_ok=True)
        
        # 生成唯一文件名
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        safe_filename = f"{timestamp}_{file.filename}"
        file_path = os.path.join(user_uploads_dir, safe_filename)
        
        file.save(file_path)
        
        # 返回文件 URL
        file_url = f"http://{LOCAL_IP}:{PORT}/uploads/{username}/{safe_filename}"
        print(f"📎 {username} 上传了文件: {file.filename} -> {safe_filename}")
        
        return jsonify({
            "success": True,
            "filename": safe_filename,
            "url": file_url,
            "path": file_path
        })
    
    # 提供上传的文件访问
    @app.route('/uploads/<username>/<filename>')
    def serve_upload(username, filename):
        """提供用户上传的文件"""
        from flask import send_from_directory
        file_path = os.path.join(DATA_DIR, username, "uploads", filename)
        if os.path.exists(file_path):
            return send_from_directory(os.path.join(DATA_DIR, username, "uploads"), filename)
        return "File not found", 404
    
    print("✅ 文件上传路由已注册")
    return app


path: start.sh
content: #!/bin/bash

# OpenClaw Team 启动脚本

echo "🚀 正在启动 OpenClaw Team..."

# 检查是否有虚拟环境
if [ ! -d "venv" ]; then
    echo "📦 创建虚拟环境..."
    python3 -m venv venv
fi

# 激活虚拟环境
echo "✅ 激活虚拟环境..."
source venv/bin/activate

# 安装依赖
echo "📥 安装依赖..."
pip install -q -r requirements.txt

# 加载环境变量（如果存在 .env 文件）
if [ -f .env ]; then
    echo "🔧 加载环境变量..."
    export $(cat .env | grep -v '^#' | xargs)
fi

# 启动服务器
echo "🌐 启动服务器..."
cd scripts
python3 main.py


Security scan verdicts — ClawScan: suspicious | static analysis:  | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)