Compare commits
10 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
bace10775b | |
|
|
585cc659b8 | |
|
|
dc92b8857e | |
|
|
dee3a429b6 | |
|
|
8a2123f605 | |
|
|
2b5c0c61d6 | |
|
|
09552407b5 | |
|
|
be889623bd | |
|
|
1ee8188ecd | |
|
|
db3e4ba348 |
17
.env.example
17
.env.example
|
|
@ -8,18 +8,7 @@
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
GEMINI_API_KEY=your-gemini-api-key-here
|
GEMINI_API_KEY=your-gemini-api-key-here
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# Gemini 模型名称(可选,默认:gemini-2.0-flash-lite)
|
||||||
# OpenClaw Auth Runtime 配置(用于 API 访问)
|
# GEMINI_MODEL=gemini-2.0-flash-lite
|
||||||
# 从 ~/clawd/skills/_shared 获取 CLIENT_KEY
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# 认证基础 URL(默认:https://api-gw-test.yuanwei-lnc.com)
|
|
||||||
# AUTH_BASE=https://api-gw-test.yuanwei-lnc.com
|
|
||||||
|
|
||||||
# 客户端密钥(必需)- 从 ~/clawd/skills/_shared 获取
|
# Auth is handled automatically via ~/.openclaw/.env
|
||||||
# CLIENT_KEY=your-client-key-here
|
|
||||||
|
|
||||||
# 缓存目录(默认:/tmp/skill-auth-cache)
|
|
||||||
# AUTH_CACHE_DIR=/tmp/skill-auth-cache
|
|
||||||
|
|
||||||
# 最小令牌 TTL 秒数(默认:60)
|
|
||||||
# AUTH_MIN_TTL_SEC=60
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
name: register-skill-release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
register:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: http://192.168.0.108:3030/agent-skills/shared-actions/register-skill@main
|
||||||
|
with:
|
||||||
|
client_key: ${{ secrets.CLIENT_KEY }}
|
||||||
|
|
@ -3,3 +3,7 @@
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
*.env
|
*.env
|
||||||
|
|
||||||
|
# uv build artifacts
|
||||||
|
python_auth_runtime/build/
|
||||||
|
python_auth_runtime/src/*.egg-info/
|
||||||
|
|
|
||||||
215
AUTH_RUNTIME.md
215
AUTH_RUNTIME.md
|
|
@ -1,215 +0,0 @@
|
||||||
# OpenClaw Auth Runtime Python 实现
|
|
||||||
|
|
||||||
基于 `~/clawd/skills/_shared/auth-runtime` 的 TypeScript 实现,提供 Python 版本的客户端密钥认证。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- ✅ 使用 CLIENT_KEY 获取访问令牌
|
|
||||||
- ✅ 支持令牌缓存(可配置 TTL)
|
|
||||||
- ✅ 自动刷新过期令牌
|
|
||||||
- ✅ 401/403 自动重试
|
|
||||||
- ✅ 从 .env 文件自动加载配置
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 1. 配置环境变量
|
|
||||||
|
|
||||||
在 `.env` 文件中设置 CLIENT_KEY:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 从 ~/clawd/skills/_shared 获取 CLIENT_KEY
|
|
||||||
CLIENT_KEY=your-client-key-here
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 获取访问令牌
|
|
||||||
|
|
||||||
```python
|
|
||||||
from auth_runtime import create_env_config, get_access_token
|
|
||||||
|
|
||||||
# 创建配置(自动从环境变量加载)
|
|
||||||
config = create_env_config()
|
|
||||||
|
|
||||||
# 获取访问令牌(带缓存)
|
|
||||||
token = get_access_token(dry_run=False, config=config)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 发送 API 请求
|
|
||||||
|
|
||||||
```python
|
|
||||||
from auth_runtime import request_api_with_auto_refresh
|
|
||||||
|
|
||||||
# 发送请求(自动处理令牌刷新)
|
|
||||||
response = request_api_with_auto_refresh(
|
|
||||||
method="POST",
|
|
||||||
url="https://api-gw-test.yuanwei-lnc.com/your-api-endpoint",
|
|
||||||
dry_run=False,
|
|
||||||
config=config,
|
|
||||||
body={"key": "value"},
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"状态码:{response.status}")
|
|
||||||
print(f"响应体:{response.body}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 参考
|
|
||||||
|
|
||||||
### 配置
|
|
||||||
|
|
||||||
#### `create_env_config() -> EnvConfig`
|
|
||||||
|
|
||||||
从环境变量创建配置:
|
|
||||||
|
|
||||||
| 环境变量 | 默认值 | 说明 |
|
|
||||||
|---------|--------|------|
|
|
||||||
| `AUTH_BASE` | `https://api-gw-test.yuanwei-lnc.com` | 认证基础 URL |
|
|
||||||
| `CLIENT_KEY` | **必需** | 客户端密钥 |
|
|
||||||
| `AUTH_CACHE_DIR` | `/tmp/skill-auth-cache` | 缓存目录 |
|
|
||||||
| `AUTH_MIN_TTL_SEC` | `60` | 最小令牌 TTL(秒) |
|
|
||||||
|
|
||||||
### 令牌管理
|
|
||||||
|
|
||||||
#### `get_access_token(dry_run, config) -> str`
|
|
||||||
|
|
||||||
获取访问令牌(带缓存):
|
|
||||||
1. 检查缓存
|
|
||||||
2. 如果缓存有效,返回缓存令牌
|
|
||||||
3. 否则请求新令牌并缓存
|
|
||||||
|
|
||||||
#### `refresh_access_token(dry_run, config) -> str`
|
|
||||||
|
|
||||||
刷新访问令牌(绕过缓存)
|
|
||||||
|
|
||||||
### API 请求
|
|
||||||
|
|
||||||
#### `request_api(method, url, auth_token, body) -> ApiResponse`
|
|
||||||
|
|
||||||
发送 HTTP 请求
|
|
||||||
|
|
||||||
#### `request_api_with_auto_refresh(method, url, dry_run, config, body) -> ApiResponse`
|
|
||||||
|
|
||||||
发送 API 请求并自动刷新令牌:
|
|
||||||
1. 使用当前令牌发送请求
|
|
||||||
2. 如果是 401/403 错误,刷新令牌并重试一次
|
|
||||||
|
|
||||||
### 便捷函数
|
|
||||||
|
|
||||||
#### `load_client_key_from_env(env_path) -> str`
|
|
||||||
|
|
||||||
从 .env 文件加载 CLIENT_KEY
|
|
||||||
|
|
||||||
优先级:
|
|
||||||
1. 环境变量 `CLIENT_KEY`
|
|
||||||
2. .env 文件中的 `CLIENT_KEY`
|
|
||||||
3. 抛出异常
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 示例 1: 简单的 API 调用
|
|
||||||
|
|
||||||
```python
|
|
||||||
from auth_runtime import create_env_config, request_api_with_auto_refresh
|
|
||||||
|
|
||||||
config = create_env_config()
|
|
||||||
|
|
||||||
response = request_api_with_auto_refresh(
|
|
||||||
method="POST",
|
|
||||||
url=f"{config.auth_base}/your-endpoint",
|
|
||||||
dry_run=False,
|
|
||||||
config=config,
|
|
||||||
body={"url": "https://example.com"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status == 200:
|
|
||||||
print("成功:", response.body)
|
|
||||||
else:
|
|
||||||
print("失败:", response.status, response.body)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例 2: 结合 Excel 翻译使用
|
|
||||||
|
|
||||||
```python
|
|
||||||
from auth_runtime import create_env_config, request_api_with_auto_refresh
|
|
||||||
import json
|
|
||||||
|
|
||||||
config = create_env_config()
|
|
||||||
|
|
||||||
# 调用 1688 API 获取商品详情
|
|
||||||
response = request_api_with_auto_refresh(
|
|
||||||
method="POST",
|
|
||||||
url=f"{config.auth_base}/ecom/tasks/scrape",
|
|
||||||
dry_run=False,
|
|
||||||
config=config,
|
|
||||||
body={"url": "https://detail.1688.com/offer/123.html"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status == 200:
|
|
||||||
data = json.loads(response.body)
|
|
||||||
print("商品价格:", data["price"])
|
|
||||||
```
|
|
||||||
|
|
||||||
## 缓存机制
|
|
||||||
|
|
||||||
令牌缓存位置:`/tmp/skill-auth-cache/token_<hash>.json`
|
|
||||||
|
|
||||||
缓存文件内容:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
||||||
"expires_at": 1234567890.123,
|
|
||||||
"expires_in": 900
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
缓存策略:
|
|
||||||
- 令牌在过期前 `AUTH_MIN_TTL_SEC` 秒内视为无效
|
|
||||||
- 默认最小 TTL 为 60 秒
|
|
||||||
- 缓存文件自动创建和删除
|
|
||||||
|
|
||||||
## 错误处理
|
|
||||||
|
|
||||||
### 常见错误
|
|
||||||
|
|
||||||
| 错误 | 原因 | 解决方案 |
|
|
||||||
|------|------|---------|
|
|
||||||
| `CLIENT_KEY is required` | 未设置 CLIENT_KEY | 在 .env 文件或环境变量中设置 |
|
|
||||||
| `Auth session request failed` | 认证请求失败 | 检查 CLIENT_KEY 是否有效 |
|
|
||||||
| `Missing accessToken` | 认证响应缺少令牌 | 检查认证服务是否正常 |
|
|
||||||
|
|
||||||
### 自动重试
|
|
||||||
|
|
||||||
以下情况会自动刷新令牌并重试一次:
|
|
||||||
- HTTP 401 Unauthorized
|
|
||||||
- HTTP 403 Forbidden
|
|
||||||
- 响应体包含 "expired"、"unauthorized" 等标记
|
|
||||||
|
|
||||||
## 测试
|
|
||||||
|
|
||||||
运行测试脚本:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /Users/xiaolongxia/Documents/ai-build-app/skills/excel-toolkit
|
|
||||||
uv run python scripts/test_auth.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 与 TypeScript 版本的差异
|
|
||||||
|
|
||||||
| 特性 | TypeScript | Python |
|
|
||||||
|------|-----------|--------|
|
|
||||||
| HTTP 客户端 | 原生 fetch | requests |
|
|
||||||
| 缓存文件 | Bun/Node fs | pathlib |
|
|
||||||
| 环境变量 | process.env | os.getenv |
|
|
||||||
| 类型系统 | TypeScript | dataclass |
|
|
||||||
|
|
||||||
功能完全对等,可以互换使用。
|
|
||||||
|
|
||||||
## 获取 CLIENT_KEY
|
|
||||||
|
|
||||||
CLIENT_KEY 存储在 `~/clawd/skills/_shared` 目录中。
|
|
||||||
|
|
||||||
查看相关文档:
|
|
||||||
- `~/clawd/skills/_shared/AUTH_RUNTIME_IMPLEMENTATION.md`
|
|
||||||
- `~/clawd/skills/_shared/auth-runtime/README.md`
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
与 OpenClaw 项目保持一致。
|
|
||||||
324
SKILL.md
324
SKILL.md
|
|
@ -1,291 +1,81 @@
|
||||||
|
---
|
||||||
|
name: excel-toolkit
|
||||||
|
description: "Excel 文件智能处理工具,支持读取、合并、编辑、筛选、翻译等操作"
|
||||||
|
metadata: {"openclaw":{"requires":{"bins":["uv"]},"install":[{"kind":"uv","label":"Install uv"}]}}
|
||||||
|
---
|
||||||
|
|
||||||
# Excel Toolkit - Excel 文件智能处理
|
# Excel Toolkit - Excel 文件智能处理
|
||||||
|
|
||||||
## 技能描述
|
## ⚠️ 调用方式(必须遵守)
|
||||||
|
|
||||||
提供 Excel 文件的智能处理功能,包括读取、合并、编辑、筛选、翻译等操作。支持 .xlsx 和 .csv 格式,可批量处理多个文件。
|
**所有脚本必须通过 `{baseDir}/run.sh` 执行,确保使用 uv 虚拟环境:**
|
||||||
|
|
||||||
**核心特性:自扩展能力** - 遇到不支持的操作时,自动生成并执行临时脚本。
|
```bash
|
||||||
|
# 执行脚本(兼容模式)
|
||||||
|
{baseDir}/run.sh read_excel --file data.xlsx
|
||||||
|
|
||||||
## 触发条件
|
# 或明确使用 run 子命令
|
||||||
|
{baseDir}/run.sh run read_excel --file data.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
当用户提及以下关键词时激活此技能:
|
**不要直接使用 `python scripts/xxx.py`,会污染系统环境!**
|
||||||
|
|
||||||
- Excel 相关:`excel`、`xlsx`、`电子表格`、`工作簿`、`工作表`
|
## 可用脚本
|
||||||
- 文件操作:`读取 excel`、`打开 excel`、`合并 excel`、`合并工作表`
|
|
||||||
- 数据处理:`筛选数据`、`排序数据`、`去重`、`替换内容`、`翻译单元格`
|
| 脚本 | 功能 | 示例 |
|
||||||
- **翻译相关**:`翻译 excel`、`中文转英文`、`translate excel`、`excel translation`、`翻译表格`
|
|------|------|------|
|
||||||
- 批量操作:`批量处理 excel`、`批量合并`、`批量替换`
|
| `read_excel` | 读取 Excel/CSV | `./run.sh read_excel --file data.xlsx` |
|
||||||
- CSV 相关:`csv`、`csv 转 excel`、`excel 转 csv`
|
| `merge_excel` | 合并多个文件 | `./run.sh merge_excel --files a.xlsx b.xlsx` |
|
||||||
- **自扩展触发**:`计算`、`转换`、`透视`、`清洗`、`货币`、`汇率`、`公式`、`合并列`、`拆分列`
|
| `replace_cells` | 替换内容 | `./run.sh replace_cells --file data.xlsx --old "旧" --new "新"` |
|
||||||
|
| `filter_data` | 筛选排序 | `./run.sh filter_data --file data.xlsx --column "姓名" --value "张三"` |
|
||||||
|
| `batch_process` | 批量处理 | `./run.sh batch_process --dir ./files --replace "旧\|新"` |
|
||||||
|
| `translate_excel` | 翻译中→英 | `./run.sh translate_excel --file data.xlsx` |
|
||||||
|
| `auto_script` | 自扩展 | `./run.sh auto_script --prompt "计算每列总和"` |
|
||||||
|
|
||||||
|
## 依赖管理
|
||||||
|
|
||||||
|
### 首次使用 / 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd {baseDir}
|
||||||
|
./run.sh sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run.sh add pandas openpyxl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动同步依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run.sh sync
|
||||||
|
```
|
||||||
|
|
||||||
## 核心功能
|
## 核心功能
|
||||||
|
|
||||||
### 1. 基础文件操作
|
### 1. 基础文件操作
|
||||||
- 读取 Excel 文件(.xlsx, .csv)
|
- 读取 Excel 文件(.xlsx, .csv)
|
||||||
- 创建新的 Excel 文件
|
- 创建新的 Excel 文件
|
||||||
- 保存和导出
|
|
||||||
|
|
||||||
### 2. 合并功能
|
### 2. 合并功能
|
||||||
- 按行合并多个文件
|
- 按行/列合并多个文件
|
||||||
- 按列合并多个文件
|
- 合并多个 sheet
|
||||||
- 合并同一文件中的多个 sheet
|
|
||||||
- 合并多个文件的指定 sheet
|
|
||||||
|
|
||||||
### 3. 数据处理
|
### 3. 数据处理
|
||||||
- 单元格内容替换
|
- 替换、筛选、排序、去重
|
||||||
- 批量替换(支持正则表达式)
|
|
||||||
- 数据筛选(按条件筛选行)
|
|
||||||
- 数据排序(按列排序)
|
|
||||||
- 数据去重(基于指定列)
|
|
||||||
|
|
||||||
### 4. 🆕 翻译功能
|
### 4. 翻译功能
|
||||||
- **中文→英文翻译**:使用 Google Gemini Flash Lite API 翻译 Excel/CSV 中的中文内容
|
- 中文→英文(Google Gemini API)
|
||||||
- **智能检测**:自动检测包含中文字符的单元格
|
- 保留原始格式
|
||||||
- **批量处理**:支持批量翻译,提高效率
|
|
||||||
- **保留格式**:保留原文件格式、样式、公式和结构
|
|
||||||
- **灵活控制**:可指定特定列或工作表进行翻译
|
|
||||||
- **预览模式**:支持 dry-run 预览,查看翻译范围
|
|
||||||
|
|
||||||
### 5. 🆕 自扩展功能
|
### 5. 自扩展功能
|
||||||
- **自动脚本生成**:根据自然语言需求自动生成处理脚本
|
- 根据自然语言自动生成脚本
|
||||||
- **模板复用**:常用操作使用预置模板,确保稳定可靠
|
|
||||||
- **智能缓存**:相同需求自动复用已生成的脚本
|
|
||||||
- **灵活扩展**:无模板时自动生成通用脚本框架
|
|
||||||
|
|
||||||
## 脚本说明
|
## 环境变量
|
||||||
|
|
||||||
所有脚本位于 `scripts/` 目录,使用 Python 编写:
|
|
||||||
|
|
||||||
### 基础脚本
|
|
||||||
- `read_excel.py` - 读取 Excel 文件并显示内容
|
|
||||||
- `merge_excel.py` - 合并多个 Excel 文件
|
|
||||||
- `replace_cells.py` - 替换单元格内容
|
|
||||||
- `filter_data.py` - 筛选和排序数据
|
|
||||||
- `batch_process.py` - 批量处理多个文件
|
|
||||||
|
|
||||||
### 🆕 翻译脚本
|
|
||||||
- `translate_excel.py` - 翻译 Excel/CSV 中的中文内容为英文(使用 Google Gemini Flash Lite)
|
|
||||||
|
|
||||||
### 🆕 自扩展脚本
|
|
||||||
- `auto_script.py` - 核心脚本引擎,分析需求并生成/执行脚本
|
|
||||||
|
|
||||||
## 翻译功能详解
|
|
||||||
|
|
||||||
### 功能特性
|
|
||||||
|
|
||||||
- **自动检测中文**:使用正则表达式检测包含中文字符的单元格
|
|
||||||
- **批量翻译**:将多个单元格合并为一个 API 请求,提高效率
|
|
||||||
- **保留原始格式**:工作表结构、样式、公式完整保留
|
|
||||||
- **生成新文件**:不修改原文件,生成 `{原文件名}_en.xlsx` 或 `{原文件名}_en.csv`
|
|
||||||
- **灵活控制**:支持按列、按工作表指定翻译范围
|
|
||||||
- **预览模式**:`--dry-run` 参数可预览翻译范围而不实际生成文件
|
|
||||||
|
|
||||||
### 使用方法
|
|
||||||
|
|
||||||
#### 翻译整个文件
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 翻译整个 Excel 文件
|
# Google Gemini API Key(翻译功能需要)
|
||||||
python scripts/translate_excel.py --file data.xlsx
|
|
||||||
|
|
||||||
# 翻译整个 CSV 文件
|
|
||||||
python scripts/translate_excel.py --file data.csv
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 指定输出文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/translate_excel.py --file data.xlsx --output translated.xlsx
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 指定列翻译
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 只翻译指定列
|
|
||||||
python scripts/translate_excel.py --file data.xlsx --columns "姓名,地址,备注"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 指定工作表(Excel)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/translate_excel.py --file data.xlsx --sheet "Sheet1"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 预览模式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看将要翻译的内容,不生成文件
|
|
||||||
python scripts/translate_excel.py --file data.xlsx --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 使用自定义 API 密钥
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 方法 1:通过参数提供
|
|
||||||
python scripts/translate_excel.py --file data.xlsx --api-key "your-api-key"
|
|
||||||
|
|
||||||
# 方法 2:通过环境变量(推荐)
|
|
||||||
export GEMINI_API_KEY="your-api-key"
|
export GEMINI_API_KEY="your-api-key"
|
||||||
python scripts/translate_excel.py --file data.xlsx
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 使用不同模型
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 使用其他 Gemini 模型
|
|
||||||
python scripts/translate_excel.py --file data.xlsx --model "gemini-2.0-flash-exp"
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 密钥配置
|
|
||||||
|
|
||||||
翻译功能需要 Google Gemini API 密钥,配置方法:
|
|
||||||
|
|
||||||
1. **环境变量(推荐)**:
|
|
||||||
```bash
|
|
||||||
export GEMINI_API_KEY="your-api-key-here"
|
|
||||||
# 或
|
|
||||||
export GOOGLE_API_KEY="your-api-key-here"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **命令行参数**:
|
|
||||||
```bash
|
|
||||||
python scripts/translate_excel.py --file data.xlsx --api-key "your-api-key"
|
|
||||||
```
|
|
||||||
|
|
||||||
获取 API 密钥:https://aistudio.google.com/app/apikey
|
|
||||||
|
|
||||||
### 翻译策略
|
|
||||||
|
|
||||||
- **保留专有名词**:人名、地名、品牌名等保持原样
|
|
||||||
- **数字格式**:保留数字、日期、时间的原始格式
|
|
||||||
- **技术术语**:使用标准英文翻译技术术语
|
|
||||||
- **空值跳过**:自动跳过空单元格、数字、公式单元格
|
|
||||||
|
|
||||||
### 输出示例
|
|
||||||
|
|
||||||
```
|
|
||||||
输入文件: data.xlsx
|
|
||||||
输出文件: data_en.xlsx
|
|
||||||
翻译列: 姓名, 地址, 职位
|
|
||||||
|
|
||||||
翻译工作表 'Sheet1' 中的 25 个单元格...
|
|
||||||
已保存翻译结果到: data_en.xlsx
|
|
||||||
|
|
||||||
翻译统计 - data.xlsx
|
|
||||||
============================================================
|
|
||||||
总单元格数: 100
|
|
||||||
包含中文: 25
|
|
||||||
已翻译: 25
|
|
||||||
跳过: 75
|
|
||||||
|
|
||||||
工作表: Sheet1
|
|
||||||
翻译列: 姓名, 地址, 职位
|
|
||||||
总数: 25, 中文: 25, 已翻译: 25
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🆕 自扩展能力详解
|
|
||||||
|
|
||||||
### 工作原理
|
|
||||||
|
|
||||||
1. **需求分析**:解析用户的自然语言描述
|
|
||||||
2. **模板匹配**:检查是否有可用的脚本模板
|
|
||||||
3. **脚本生成**:
|
|
||||||
- 有模板:使用模板 + 参数生成
|
|
||||||
- 无模板:自动生成通用脚本框架
|
|
||||||
4. **执行并缓存**:执行脚本并保存到 `temp_scripts/` 供复用
|
|
||||||
5. **结果返回**:输出执行结果和统计信息
|
|
||||||
|
|
||||||
### 可用模板
|
|
||||||
|
|
||||||
模板位于 `script_templates/` 目录:
|
|
||||||
|
|
||||||
| 模板 | 功能 | 触发关键词 |
|
|
||||||
|------|------|-----------|
|
|
||||||
| `currency_convert.py` | 货币/汇率转换 | 货币、汇率、转换、currency、convert |
|
|
||||||
| `pivot_summary.py` | 数据透视汇总 | 透视、汇总、聚合、pivot、summary |
|
|
||||||
| `data_clean.py` | 数据清洗(去空、格式化) | 清洗、去空、格式化、clean |
|
|
||||||
| `column_calc.py` | 列计算(加减乘除、公式) | 计算、加减乘除、公式、calc、calculate |
|
|
||||||
| `merge_columns.py` | 列合并/拆分 | 合并列、拆分、split、join |
|
|
||||||
|
|
||||||
### 自然语言触发示例
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 货币转换
|
|
||||||
python scripts/auto_script.py "把金额列从美元转换为人民币,汇率7.2" --file sales.xlsx --output converted.xlsx
|
|
||||||
|
|
||||||
# 数据透视
|
|
||||||
python scripts/auto_script.py "按地区和产品透视汇总销售额" --file sales.xlsx --output summary.xlsx --group_by "地区,产品" --agg_column "销售额" --agg_func "sum"
|
|
||||||
|
|
||||||
# 数据清洗
|
|
||||||
python scripts/auto_script.py "清洗数据,删除空行并去除空格" --file data.xlsx --output cleaned.xlsx --drop_na --strip_whitespace
|
|
||||||
|
|
||||||
# 列计算
|
|
||||||
python scripts/auto_script.py "计算总价 = 单价 * 数量" --file products.xlsx --output result.xlsx --operation multiply --column1 "单价" --column2 "数量" --result_column "总价"
|
|
||||||
|
|
||||||
# 仅生成不执行(预览)
|
|
||||||
python scripts/auto_script.py "计算利润" --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
### 脚本缓存
|
|
||||||
|
|
||||||
相同需求会自动复用已生成的脚本,存储位置:
|
|
||||||
- `temp_scripts/script_[hash].py`
|
|
||||||
- 哈希值基于需求描述生成
|
|
||||||
- 手动清理:删除 `temp_scripts/` 目录
|
|
||||||
|
|
||||||
## 基础功能使用示例
|
|
||||||
|
|
||||||
### 读取 Excel
|
|
||||||
```bash
|
|
||||||
python scripts/read_excel.py /path/to/file.xlsx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 合并文件
|
|
||||||
```bash
|
|
||||||
# 按行合并
|
|
||||||
python scripts/merge_excel.py --mode row file1.xlsx file2.xlsx output.xlsx
|
|
||||||
|
|
||||||
# 按列合并
|
|
||||||
python scripts/merge_excel.py --mode col file1.xlsx file2.xlsx output.xlsx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 替换内容
|
|
||||||
```bash
|
|
||||||
python scripts/replace_cells.py input.xlsx "旧值" "新值" output.xlsx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 筛选数据
|
|
||||||
```bash
|
|
||||||
python scripts/filter_data.py input.xlsx --filter "列A > 100" --sort "列B" output.xlsx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 批量处理
|
|
||||||
```bash
|
|
||||||
python scripts/batch_process.py --replace "旧值|新值" *.xlsx
|
|
||||||
```
|
|
||||||
|
|
||||||
## 技术依赖
|
|
||||||
|
|
||||||
- Python 3.8+
|
|
||||||
- openpyxl (读写 .xlsx)
|
|
||||||
- pandas (数据处理)
|
|
||||||
- **google-generativeai** (翻译功能,>=0.3.0)
|
|
||||||
|
|
||||||
安装依赖:
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 大文件处理可能需要较多内存
|
|
||||||
- 合并前请确保文件结构兼容
|
|
||||||
- 备份原始文件后再进行批量操作
|
|
||||||
- 公式可能在某些操作中丢失,建议保留原始文件
|
|
||||||
- 自扩展生成的脚本默认超时时间为 5 分钟
|
|
||||||
- 自动生成的脚本可能需要手动调整参数以适应特定需求
|
|
||||||
- **翻译功能**:
|
|
||||||
- 需要配置 Google Gemini API 密钥
|
|
||||||
- 大量翻译可能产生 API 费用
|
|
||||||
- 建议先用 `--dry-run` 预览翻译范围
|
|
||||||
- Gemini Flash Lite 有速率限制,大批量翻译建议分批处理
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ dependencies = [
|
||||||
"pandas>=1.5.0",
|
"pandas>=1.5.0",
|
||||||
"openpyxl>=3.0.0",
|
"openpyxl>=3.0.0",
|
||||||
"google-generativeai>=0.8.0",
|
"google-generativeai>=0.8.0",
|
||||||
"requests>=2.28.0",
|
"clawd-auth-runtime-py @ file:///${PROJECT_ROOT}/../auth-runtime-py",
|
||||||
# 引用本地 auth-runtime 包
|
|
||||||
"python-auth-runtime-py @ file:///${PROJECT_ROOT}/python_auth_runtime",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
# @clawd/auth-runtime-py
|
|
||||||
|
|
||||||
Python 版本的 OpenClaw Auth Runtime。
|
|
||||||
|
|
||||||
**注意**: 本模块**不加载 .env 文件**,只从环境变量读取配置。
|
|
||||||
|
|
||||||
加载 `.env.local` 应该在具体的 skill 中实现。
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
your-skill/
|
|
||||||
├── .env.local # 敏感配置(不提交到 git)
|
|
||||||
├── scripts/
|
|
||||||
│ ├── main.py # 加载 .env.local 并调用 auth_runtime
|
|
||||||
│ └── load_env.py # 从 python_auth_runtime 复制
|
|
||||||
└── pyproject.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv pip install /path/to/python_auth_runtime
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
|
|
||||||
### 步骤 1: 在具体 skill 中加载 .env.local
|
|
||||||
|
|
||||||
参考 `~/clawd/skills/1688-product-master/scripts/run.ts` 的 `loadEnvLocal()` 实现:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# your-skill/scripts/main.py
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
|
|
||||||
def load_env_local():
|
|
||||||
"""加载 .env.local 文件到环境变量"""
|
|
||||||
script_dir = Path(__file__).parent
|
|
||||||
env_local_path = script_dir.parent / ".env.local"
|
|
||||||
|
|
||||||
if env_local_path.exists():
|
|
||||||
with open(env_local_path, "r", encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
eq_index = line.find("=")
|
|
||||||
if eq_index > 0:
|
|
||||||
key = line[:eq_index].strip()
|
|
||||||
value = line[eq_index + 1:].strip()
|
|
||||||
# 去除引号
|
|
||||||
if (value.startswith('"') and value.endswith('"')) or \
|
|
||||||
(value.startswith("'") and value.endswith("'")):
|
|
||||||
value = value[1:-1]
|
|
||||||
# 只设置尚未存在的环境变量
|
|
||||||
if key not in os.environ:
|
|
||||||
os.environ[key] = value
|
|
||||||
|
|
||||||
# 在 main() 函数开始时调用
|
|
||||||
load_env_local()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 步骤 2: 使用 auth_runtime
|
|
||||||
|
|
||||||
```python
|
|
||||||
from python_auth_runtime import create_env_config, request_api_with_auto_refresh
|
|
||||||
|
|
||||||
# 创建配置(自动从环境变量读取)
|
|
||||||
config = create_env_config()
|
|
||||||
|
|
||||||
# 调用 API
|
|
||||||
response = request_api_with_auto_refresh(
|
|
||||||
method="POST",
|
|
||||||
url=f"{config.auth_base}/ecom/tasks/scrape",
|
|
||||||
dry_run=False,
|
|
||||||
config=config,
|
|
||||||
body={"url": "https://detail.1688.com/offer/123.html"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status == 200:
|
|
||||||
import json
|
|
||||||
data = json.loads(response.body)
|
|
||||||
print("商品价格:", data["price"])
|
|
||||||
```
|
|
||||||
|
|
||||||
### 完整示例
|
|
||||||
|
|
||||||
```python
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
# your-skill/scripts/main.py
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from python_auth_runtime import create_env_config, request_api_with_auto_refresh
|
|
||||||
|
|
||||||
def load_env_local():
|
|
||||||
"""加载 .env.local 文件到环境变量"""
|
|
||||||
script_dir = Path(__file__).parent
|
|
||||||
env_local_path = script_dir.parent / ".env.local"
|
|
||||||
|
|
||||||
if env_local_path.exists():
|
|
||||||
with open(env_local_path, "r", encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
eq_index = line.find("=")
|
|
||||||
if eq_index > 0:
|
|
||||||
key = line[:eq_index].strip()
|
|
||||||
value = line[eq_index + 1:].strip()
|
|
||||||
if (value.startswith('"') and value.endswith('"')) or \
|
|
||||||
(value.startswith("'") and value.endswith("'")):
|
|
||||||
value = value[1:-1]
|
|
||||||
if key not in os.environ:
|
|
||||||
os.environ[key] = value
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# 加载 .env.local
|
|
||||||
load_env_local()
|
|
||||||
|
|
||||||
# 创建配置
|
|
||||||
config = create_env_config()
|
|
||||||
|
|
||||||
# 调用 API
|
|
||||||
response = request_api_with_auto_refresh(
|
|
||||||
method="POST",
|
|
||||||
url=f"{config.auth_base}/ecom/tasks/scrape",
|
|
||||||
dry_run=False,
|
|
||||||
config=config,
|
|
||||||
body={"url": "https://detail.1688.com/offer/123.html"},
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"状态:{response.status}")
|
|
||||||
print(f"响应:{response.body}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 环境变量
|
|
||||||
|
|
||||||
| 变量 | 默认值 | 说明 |
|
|
||||||
|------|--------|------|
|
|
||||||
| `CLIENT_KEY` | **必需** | 客户端密钥 |
|
|
||||||
| `AUTH_BASE` | `https://api-gw-test.yuanwei-lnc.com` | 认证基础 URL |
|
|
||||||
| `AUTH_CACHE_DIR` | `/tmp/skill-auth-cache` | 缓存目录 |
|
|
||||||
| `AUTH_MIN_TTL_SEC` | `60` | 最小令牌 TTL(秒) |
|
|
||||||
|
|
||||||
## 命令行参数覆盖
|
|
||||||
|
|
||||||
可以在命令行设置环境变量,优先级高于 `.env.local`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
CLIENT_KEY="override-key" python scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /Users/xiaolongxia/Documents/ai-build-app/skills/excel-toolkit/python_auth_runtime
|
|
||||||
uv run python scripts/example_usage.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 与 TypeScript 版本对比
|
|
||||||
|
|
||||||
| 特性 | TypeScript | Python |
|
|
||||||
|------|-----------|--------|
|
|
||||||
| 模块 | `@clawd/auth-runtime` | `python_auth_runtime` |
|
|
||||||
| .env 加载 | skill 自己实现 (`loadEnvLocal()`) | skill 自己实现 (`load_env_local()`) |
|
|
||||||
| 环境变量 | `process.env` | `os.getenv()` |
|
|
||||||
| 缓存 | `/tmp/skill-auth-cache` | `/tmp/skill-auth-cache` |
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
[project]
|
|
||||||
name = "@clawd/auth-runtime-py"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "Python 版本的 OpenClaw Auth Runtime"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.9"
|
|
||||||
dependencies = [
|
|
||||||
"requests>=2.28.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["setuptools>=61.0"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
|
||||||
where = ["src"]
|
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
index-url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
示例:如何在具体 skill 中使用 auth_runtime
|
|
||||||
|
|
||||||
参考:~/clawd/skills/1688-product-master/scripts/run.ts
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# 添加当前目录到路径
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
||||||
|
|
||||||
# 步骤 1: 加载 .env.local
|
|
||||||
from load_env import load_env
|
|
||||||
load_env() # 加载 .env.local 或 .env
|
|
||||||
|
|
||||||
# 步骤 2: 导入 auth_runtime
|
|
||||||
from python_auth_runtime import create_env_config, request_api_with_auto_refresh
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print("Auth Runtime 使用示例")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# 步骤 3: 创建配置(从环境变量读取)
|
|
||||||
config = create_env_config()
|
|
||||||
print(f"\n✓ 配置创建成功")
|
|
||||||
print(f" AUTH_BASE: {config.auth_base}")
|
|
||||||
print(f" CLIENT_KEY: {config.client_key[:10]}..." if config.client_key else " CLIENT_KEY: <empty>")
|
|
||||||
|
|
||||||
# 步骤 4: 调用 API
|
|
||||||
print(f"\n【测试】获取访问令牌...")
|
|
||||||
try:
|
|
||||||
response = request_api_with_auto_refresh(
|
|
||||||
method="POST",
|
|
||||||
url=f"{config.auth_base}/auth/skill-credit/session",
|
|
||||||
dry_run=False,
|
|
||||||
config=config,
|
|
||||||
body={"clientKey": config.client_key} if config.client_key else {},
|
|
||||||
)
|
|
||||||
print(f"✓ 响应状态:{response.status}")
|
|
||||||
if response.status == 200:
|
|
||||||
print(f" 响应体:{response.body[:100]}...")
|
|
||||||
else:
|
|
||||||
print(f" 响应体:{response.body}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 失败:{e}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
加载 .env.local 或 .env 文件到环境变量
|
|
||||||
|
|
||||||
在具体 skill 中使用:
|
|
||||||
from load_env import load_env
|
|
||||||
load_env() # 加载 .env.local 或 .env
|
|
||||||
|
|
||||||
参考:~/clawd/skills/1688-product-master/scripts/run.ts 中的 loadEnvLocal()
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def load_env(env_path: Path | None = None) -> bool:
|
|
||||||
"""
|
|
||||||
加载 .env.local 或 .env 文件到环境变量
|
|
||||||
|
|
||||||
优先级:
|
|
||||||
1. 指定的 env_path
|
|
||||||
2. 当前目录的 .env.local
|
|
||||||
3. 当前目录的 .env
|
|
||||||
4. 父目录的 .env.local
|
|
||||||
5. 父目录的 .env
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否成功加载
|
|
||||||
"""
|
|
||||||
if env_path is None:
|
|
||||||
# 查找 .env 文件
|
|
||||||
possible_paths = [
|
|
||||||
Path.cwd() / ".env.local",
|
|
||||||
Path.cwd() / ".env",
|
|
||||||
Path.cwd().parent / ".env.local",
|
|
||||||
Path.cwd().parent / ".env",
|
|
||||||
]
|
|
||||||
for p in possible_paths:
|
|
||||||
if p.exists():
|
|
||||||
env_path = p
|
|
||||||
break
|
|
||||||
|
|
||||||
if env_path is None or not env_path.exists():
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"📄 加载环境变量:{env_path}")
|
|
||||||
|
|
||||||
with open(env_path, "r", encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
# 跳过空行和注释
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 解析 KEY=VALUE
|
|
||||||
eq_index = line.find("=")
|
|
||||||
if eq_index <= 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key = line[:eq_index].strip()
|
|
||||||
value = line[eq_index + 1:].strip()
|
|
||||||
|
|
||||||
# 去除引号
|
|
||||||
if (value.startswith('"') and value.endswith('"')) or \
|
|
||||||
(value.startswith("'") and value.endswith("'")):
|
|
||||||
value = value[1:-1]
|
|
||||||
|
|
||||||
# 只设置尚未存在的环境变量(命令行参数优先级更高)
|
|
||||||
if key not in os.environ:
|
|
||||||
os.environ[key] = value
|
|
||||||
print(f" ✓ {key} = {value[:10]}..." if len(str(value)) > 10 else f" ✓ {key} = {value}")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
|
|
||||||
env_file = Path(sys.argv[1]) if len(sys.argv) > 1 else None
|
|
||||||
|
|
||||||
if load_env(env_file):
|
|
||||||
print("\n✅ 环境变量加载成功")
|
|
||||||
print(f"\n当前环境变量:")
|
|
||||||
for key in ["CLIENT_KEY", "AUTH_BASE", "AUTH_CACHE_DIR"]:
|
|
||||||
value = os.getenv(key, "<not set>")
|
|
||||||
print(f" {key} = {value[:10]}..." if len(str(value)) > 10 else f" {key} = {value}")
|
|
||||||
else:
|
|
||||||
print("❌ 未找到 .env 文件")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
@ -1,312 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
@clawd/auth-runtime-py - Python 版本的 OpenClaw Auth Runtime
|
|
||||||
|
|
||||||
基于 ~/clawd/skills/_shared/auth-runtime 的 TypeScript 实现,
|
|
||||||
提供 Python 版本的客户端密钥认证。
|
|
||||||
|
|
||||||
使用方式:
|
|
||||||
from python_auth_runtime import create_env_config, get_access_token, request_api_with_auto_refresh
|
|
||||||
|
|
||||||
config = create_env_config()
|
|
||||||
token = get_access_token(dry_run=False, config=config)
|
|
||||||
|
|
||||||
response = request_api_with_auto_refresh(
|
|
||||||
method="POST",
|
|
||||||
url=f"{config.auth_base}/your-endpoint",
|
|
||||||
dry_run=False,
|
|
||||||
config=config,
|
|
||||||
body={"key": "value"},
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
import requests # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class EnvConfig:
|
|
||||||
"""环境配置"""
|
|
||||||
auth_base: str = "https://api-gw-test.yuanwei-lnc.com"
|
|
||||||
client_key: str = ""
|
|
||||||
auth_cache_dir: str = "/tmp/skill-auth-cache"
|
|
||||||
auth_min_ttl_sec: int = 60
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SessionResponse:
|
|
||||||
"""会话响应"""
|
|
||||||
access_token: str
|
|
||||||
hook_url: Optional[str] = None
|
|
||||||
hook_token: Optional[str] = None
|
|
||||||
expires_in: int = 900
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CachedTokenData:
|
|
||||||
"""缓存的令牌数据"""
|
|
||||||
access_token: str
|
|
||||||
expires_at: float
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ApiResponse:
|
|
||||||
"""HTTP 响应"""
|
|
||||||
status: int
|
|
||||||
body: str
|
|
||||||
headers: dict[str, str]
|
|
||||||
|
|
||||||
|
|
||||||
# 可重试的状态码
|
|
||||||
RETRYABLE_STATUS = {401, 403}
|
|
||||||
|
|
||||||
# 可重试的响应体标记
|
|
||||||
RETRYABLE_BODY_MARKERS = [
|
|
||||||
'session not found or expired',
|
|
||||||
'invalid or expired token',
|
|
||||||
'unauthorized',
|
|
||||||
'client key expired',
|
|
||||||
'client key revoked',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def create_env_config() -> EnvConfig:
|
|
||||||
"""
|
|
||||||
从环境变量创建配置
|
|
||||||
|
|
||||||
环境变量:
|
|
||||||
- AUTH_BASE: 认证基础 URL(默认:https://api-gw-test.yuanwei-lnc.com)
|
|
||||||
- CLIENT_KEY: 客户端密钥(必需)
|
|
||||||
- AUTH_CACHE_DIR: 缓存目录(默认:/tmp/skill-auth-cache)
|
|
||||||
- AUTH_MIN_TTL_SEC: 最小令牌 TTL 秒数(默认:60)
|
|
||||||
"""
|
|
||||||
auth_base = os.getenv("AUTH_BASE", "https://api-gw-test.yuanwei-lnc.com").rstrip("/")
|
|
||||||
client_key = os.getenv("CLIENT_KEY", "")
|
|
||||||
auth_cache_dir = os.getenv("AUTH_CACHE_DIR", "/tmp/skill-auth-cache")
|
|
||||||
auth_min_ttl_sec = int(os.getenv("AUTH_MIN_TTL_SEC", "60"))
|
|
||||||
|
|
||||||
return EnvConfig(
|
|
||||||
auth_base=auth_base,
|
|
||||||
client_key=client_key,
|
|
||||||
auth_cache_dir=auth_cache_dir,
|
|
||||||
auth_min_ttl_sec=auth_min_ttl_sec,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_cache_file(auth_base: str, client_key: str, cache_dir: str) -> Path:
|
|
||||||
"""获取缓存文件路径"""
|
|
||||||
cache_path = Path(cache_dir)
|
|
||||||
cache_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
key_str = f"{auth_base}:{client_key}"
|
|
||||||
hash_value = hashlib.sha256(key_str.encode()).hexdigest()[:16]
|
|
||||||
|
|
||||||
return cache_path / f"token_{hash_value}.json"
|
|
||||||
|
|
||||||
|
|
||||||
def read_cached_token(cache_file: Path, min_ttl_sec: int) -> Optional[str]:
|
|
||||||
"""读取缓存的令牌"""
|
|
||||||
if not cache_file.exists():
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(cache_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
cached = CachedTokenData(
|
|
||||||
access_token=data["access_token"],
|
|
||||||
expires_at=data["expires_at"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if time.time() + min_ttl_sec < cached.expires_at:
|
|
||||||
return cached.access_token
|
|
||||||
else:
|
|
||||||
cache_file.unlink(missing_ok=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
except (json.JSONDecodeError, KeyError, Exception):
|
|
||||||
cache_file.unlink(missing_ok=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def write_cache(cache_file: Path, session: SessionResponse) -> None:
|
|
||||||
"""写入缓存"""
|
|
||||||
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"access_token": session.access_token,
|
|
||||||
"expires_at": time.time() + session.expires_in,
|
|
||||||
"expires_in": session.expires_in,
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(cache_file, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
def delete_cache(cache_file: Path) -> None:
|
|
||||||
"""删除缓存"""
|
|
||||||
cache_file.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_session_json(dry_run: bool, config: EnvConfig) -> SessionResponse:
|
|
||||||
"""从认证端点获取会话 JSON"""
|
|
||||||
if dry_run:
|
|
||||||
return SessionResponse(
|
|
||||||
access_token="<dry-run-token>",
|
|
||||||
hook_url="<dry-run-hook-url>",
|
|
||||||
hook_token="<dry-run-hook-token>",
|
|
||||||
expires_in=900,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not config.client_key:
|
|
||||||
raise ValueError("CLIENT_KEY is required")
|
|
||||||
|
|
||||||
payload = {"clientKey": config.client_key}
|
|
||||||
url = f"{config.auth_base}/auth/skill-credit/session"
|
|
||||||
|
|
||||||
response = requests.post(url, json=payload, timeout=30)
|
|
||||||
|
|
||||||
if response.status_code < 200 or response.status_code >= 300:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Auth session request failed: HTTP {response.status_code} - {response.text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
session_data = response.json()
|
|
||||||
|
|
||||||
if not session_data.get("accessToken"):
|
|
||||||
raise RuntimeError(f"Missing accessToken in session response: {response.text}")
|
|
||||||
|
|
||||||
return SessionResponse(
|
|
||||||
access_token=session_data["accessToken"],
|
|
||||||
hook_url=session_data.get("hookUrl"),
|
|
||||||
hook_token=session_data.get("hookToken"),
|
|
||||||
expires_in=session_data.get("expiresIn", 900),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_access_token(dry_run: bool, config: EnvConfig) -> str:
|
|
||||||
"""获取访问令牌(带缓存)"""
|
|
||||||
if dry_run:
|
|
||||||
return "<dry-run-token>"
|
|
||||||
|
|
||||||
if not config.client_key:
|
|
||||||
raise ValueError("CLIENT_KEY is required")
|
|
||||||
|
|
||||||
cache_file = get_cache_file(config.auth_base, config.client_key, config.auth_cache_dir)
|
|
||||||
cached_token = read_cached_token(cache_file, config.auth_min_ttl_sec)
|
|
||||||
|
|
||||||
if cached_token:
|
|
||||||
return cached_token
|
|
||||||
|
|
||||||
session = fetch_session_json(dry_run, config)
|
|
||||||
write_cache(cache_file, session)
|
|
||||||
|
|
||||||
return session.access_token
|
|
||||||
|
|
||||||
|
|
||||||
def refresh_access_token(dry_run: bool, config: EnvConfig) -> str:
|
|
||||||
"""刷新访问令牌(绕过缓存)"""
|
|
||||||
if dry_run:
|
|
||||||
return "<dry-run-token>"
|
|
||||||
|
|
||||||
if not config.client_key:
|
|
||||||
raise ValueError("CLIENT_KEY is required")
|
|
||||||
|
|
||||||
cache_file = get_cache_file(config.auth_base, config.client_key, config.auth_cache_dir)
|
|
||||||
delete_cache(cache_file)
|
|
||||||
|
|
||||||
return get_access_token(dry_run, config)
|
|
||||||
|
|
||||||
|
|
||||||
def is_retryable_session_error(response: ApiResponse) -> bool:
|
|
||||||
"""检查响应是否表示会话过期/无效"""
|
|
||||||
if response.status not in RETRYABLE_STATUS:
|
|
||||||
return False
|
|
||||||
|
|
||||||
body = (response.body or "").lower()
|
|
||||||
if not body:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return any(marker in body for marker in RETRYABLE_BODY_MARKERS)
|
|
||||||
|
|
||||||
|
|
||||||
def request_api(
|
|
||||||
method: str,
|
|
||||||
url: str,
|
|
||||||
auth_token: Optional[str] = None,
|
|
||||||
body: Optional[dict[str, Any]] = None,
|
|
||||||
timeout: int = 30,
|
|
||||||
) -> ApiResponse:
|
|
||||||
"""发送 HTTP 请求"""
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
|
|
||||||
if auth_token:
|
|
||||||
headers["Authorization"] = f"Bearer {auth_token}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
if method.upper() == "GET":
|
|
||||||
response = requests.get(url, headers=headers, timeout=timeout)
|
|
||||||
elif method.upper() == "POST":
|
|
||||||
response = requests.post(url, headers=headers, json=body, timeout=timeout)
|
|
||||||
elif method.upper() == "PUT":
|
|
||||||
response = requests.put(url, headers=headers, json=body, timeout=timeout)
|
|
||||||
elif method.upper() == "DELETE":
|
|
||||||
response = requests.delete(url, headers=headers, timeout=timeout)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
||||||
|
|
||||||
return ApiResponse(
|
|
||||||
status=response.status_code,
|
|
||||||
body=response.text,
|
|
||||||
headers=dict(response.headers),
|
|
||||||
)
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return ApiResponse(
|
|
||||||
status=0,
|
|
||||||
body=str(e),
|
|
||||||
headers={},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def request_api_with_auto_refresh(
|
|
||||||
method: str,
|
|
||||||
url: str,
|
|
||||||
dry_run: bool,
|
|
||||||
config: EnvConfig,
|
|
||||||
body: Optional[dict[str, Any]] = None,
|
|
||||||
access_token: Optional[str] = None,
|
|
||||||
) -> ApiResponse:
|
|
||||||
"""发送 API 请求并自动刷新令牌"""
|
|
||||||
token = access_token or get_access_token(dry_run, config)
|
|
||||||
|
|
||||||
first = request_api(method, url, token, body)
|
|
||||||
|
|
||||||
if not is_retryable_session_error(first):
|
|
||||||
return first
|
|
||||||
|
|
||||||
fresh_token = refresh_access_token(dry_run, config)
|
|
||||||
return request_api(method, url, fresh_token, body)
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
|
||||||
__all__ = [
|
|
||||||
"EnvConfig",
|
|
||||||
"SessionResponse",
|
|
||||||
"ApiResponse",
|
|
||||||
"create_env_config",
|
|
||||||
"get_access_token",
|
|
||||||
"refresh_access_token",
|
|
||||||
"fetch_session_json",
|
|
||||||
"request_api",
|
|
||||||
"request_api_with_auto_refresh",
|
|
||||||
"is_retryable_session_error",
|
|
||||||
]
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# 检查 uv
|
||||||
|
if ! command -v uv &> /dev/null; then
|
||||||
|
echo "❌ uv not found. Install: curl -LsSf https://astral.sh/uv/install.sh | sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 子命令
|
||||||
|
case "${1:-}" in
|
||||||
|
sync|install)
|
||||||
|
echo "📦 Syncing dependencies..."
|
||||||
|
uv sync
|
||||||
|
exit $?
|
||||||
|
;;
|
||||||
|
add)
|
||||||
|
shift
|
||||||
|
echo "📦 Adding package: $@"
|
||||||
|
uv add "$@"
|
||||||
|
exit $?
|
||||||
|
;;
|
||||||
|
run)
|
||||||
|
shift
|
||||||
|
SCRIPT_NAME="$1"
|
||||||
|
shift
|
||||||
|
if [ -z "$SCRIPT_NAME" ]; then
|
||||||
|
echo "Usage: ./run.sh run <script> [args...]"
|
||||||
|
ls scripts/*.py | xargs -n1 basename | sed 's/\.py$//' | sed 's/^/ /'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
uv run python "scripts/${SCRIPT_NAME}.py" "$@"
|
||||||
|
exit $?
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
echo "Excel Toolkit - uv managed"
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
echo " ./run.sh sync # Install/sync dependencies"
|
||||||
|
echo " ./run.sh add <package> # Add a new dependency"
|
||||||
|
echo " ./run.sh run <script> # Run a script with uv"
|
||||||
|
echo ""
|
||||||
|
echo "Scripts:"
|
||||||
|
ls scripts/*.py 2>/dev/null | xargs -n1 basename | sed 's/\.py$//' | sed 's/^/ /'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# 兼容模式:直接当脚本名
|
||||||
|
SCRIPT_NAME="$1"
|
||||||
|
shift
|
||||||
|
SCRIPT_PATH="scripts/${SCRIPT_NAME}.py"
|
||||||
|
if [ ! -f "$SCRIPT_PATH" ]; then
|
||||||
|
echo "❌ Script not found: $SCRIPT_PATH" >&2
|
||||||
|
echo "Try: ./run.sh run <script>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
uv run python "$SCRIPT_PATH" "$@"
|
||||||
|
exit $?
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -20,6 +20,10 @@ from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from load_env import load_env
|
||||||
|
load_env() # 加载 .env 文件
|
||||||
try:
|
try:
|
||||||
import google.generativeai as genai # type: ignore
|
import google.generativeai as genai # type: ignore
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|
@ -96,6 +100,11 @@ def get_api_key() -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_name() -> str:
|
||||||
|
import os
|
||||||
|
return os.getenv("GEMINI_MODEL", "gemini-2.0-flash-lite")
|
||||||
|
|
||||||
|
|
||||||
def extract_chinese_content(
|
def extract_chinese_content(
|
||||||
input_path: Path,
|
input_path: Path,
|
||||||
columns: list[str] | None = None,
|
columns: list[str] | None = None,
|
||||||
|
|
@ -160,9 +169,11 @@ def extract_chinese_content(
|
||||||
|
|
||||||
def translate_entries(
|
def translate_entries(
|
||||||
entries: list[TranslationEntry],
|
entries: list[TranslationEntry],
|
||||||
model_name: str = "gemini-2.0-flash-lite",
|
model_name: str | None = None,
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
) -> list[TranslationEntry]:
|
) -> list[TranslationEntry]:
|
||||||
|
if model_name is None:
|
||||||
|
model_name = get_model_name()
|
||||||
if not entries:
|
if not entries:
|
||||||
print("✓ 没有需要翻译的内容")
|
print("✓ 没有需要翻译的内容")
|
||||||
return entries
|
return entries
|
||||||
|
|
@ -263,10 +274,12 @@ def translate_excel_file(
|
||||||
output_path: Path | None = None,
|
output_path: Path | None = None,
|
||||||
columns: list[str] | None = None,
|
columns: list[str] | None = None,
|
||||||
sheet_name: str | None = None,
|
sheet_name: str | None = None,
|
||||||
model_name: str = "gemini-2.0-flash-lite",
|
model_name: str | None = None,
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
|
if model_name is None:
|
||||||
|
model_name = get_model_name()
|
||||||
if not output_path:
|
if not output_path:
|
||||||
output_path = input_path.parent / f"{input_path.stem}_en{input_path.suffix}"
|
output_path = input_path.parent / f"{input_path.stem}_en{input_path.suffix}"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue