move claude-marketplace to ai-proj-helper

This commit is contained in:
2026-03-12 21:42:30 +08:00
parent d7b6835e1d
commit 43585b8504
188 changed files with 39510 additions and 0 deletions

16
PUSH.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
echo "🚀 Pushing to Gitea..."
echo ""
echo "Make sure you've created the repository on Gitea:"
echo "https://gitea.pipexerp.com/huangjun/claude-marketplace"
echo ""
read -p "Press Enter to continue..."
git push -u origin main
echo ""
echo "✅ Done! Your marketplace is now live at:"
echo "https://gitea.pipexerp.com/huangjun/claude-marketplace"
echo ""
echo "Test it with:"
echo "/plugin marketplace add git@gitea.pipexerp.com:huangjun/claude-marketplace.git"

179
README.md
View File

@@ -0,0 +1,179 @@
# Claude Code Plugin Marketplace
Custom Claude Code plugins for development workflows, DevOps, and business operations.
## 🚀 Quick Start
### Add the Marketplace
```bash
/plugin marketplace add git@gitea.pipexerp.com:huangjun/claude-marketplace.git
```
Or using HTTPS (requires credential configuration):
```bash
/plugin marketplace add https://gitea.pipexerp.com/huangjun/claude-marketplace.git
```
### Install Plugins
```bash
# Install specific plugin
/plugin install ai-proj-plugin@coolbuy-claude-plugins
# List all available plugins
/plugin marketplace list coolbuy-claude-plugins
```
## 📦 Available Plugins (40 Total)
### 🚀 Development & Coding
- **ai-proj-plugin** - AI project management with MCP integration
- **dev-plugin** - General development workflows
- **dev-arch-plugin** - Software architecture and design
- **dev-coding-plugin** - Coding standards and best practices
- **dev-test-plugin** - Testing workflows and automation
- **frontend-design-plugin** - Frontend design and UI/UX
### 🔧 DevOps & Operations
- **ops-tools-plugin** - DevOps tools for deployment and monitoring
- **ops-servers-plugin** - Server management and SSH operations
- **enjoysa-deploy-plugin** - Enjoysa deployment automation
- **req-deploy-plugin** - Requirement deployment workflows
### 📋 Project & Requirements Management
- **req-plugin** - Requirement management workflows
- **req-dev-plugin** - Requirement development processes
- **req-prd-plugin** - PRD (Product Requirements Document) creation
- **req-commands-plugin** - Detailed command reference for /req
- **req-review-plugin** - PRD review methodology
- **req-workflow-plugin** - Complete requirement lifecycle workflow
- **requirement-plugin** - General requirement handling
- **executing-plans-plugin** - Plan execution and tracking
### 💼 Business Operations
- **biz-plan-plugin** - Business planning and proposals
- **biz-contract-plugin** - Contract drafting and management
- **finance-plugin** - Financial operations and reporting
### 🔗 Integrations
- **feishu-plugin** - Feishu/Lark integration (documents, spreadsheets)
- **feishu-bitable-plugin** - Feishu Bitable (multi-dimensional tables)
- **feishu-docx-plugin** - Feishu cloud documents
- **wecom-plugin** - WeChat Work integration
- **siyuan-plugin** - SiYuan note-taking integration
- **data-excel-plugin** - Excel data processing
- **doubao-voice-plugin** - Doubao Voice API (TTS, ASR, dialogue)
### 🏢 Project-Specific
- **coolbuy-legacy-plugin** - Coolbuy legacy system workflows
- **coolbuy-paas-plugin** - Coolbuy PaaS platform operations
- **coolbuy-platform-plugin** - Coolbuy platform development
- **enjoysa-plugin** - Enjoysa project workflows
### 🔄 Session Management
- **save-session-plugin** - Save Claude Code sessions
- **reload-session-plugin** - Reload saved sessions
- **read-session-plugin** - Read session data
- **search-sessions-plugin** - Search through sessions
### 🛠️ Utilities
- **pr-plugin** - Pull request creation and management
- **finishing-a-development-branch-plugin** - Branch finishing workflows
- **skill-manager-plugin** - Skill management utilities
- **qiudl-personal-plugin** - Personal productivity tools
## 📖 Usage Examples
### AI Project Management
```bash
/ai-proj-plugin:create-task "Implement user authentication"
/ai-proj-plugin:start-timer
```
### DevOps Operations
```bash
/ops-tools-plugin:deploy production
/ops-servers-plugin:check-status
```
### Development Workflow
```bash
/dev-coding-plugin:start-feature "user-profile"
/finishing-a-development-branch-plugin:create-pr
```
### Business Operations
```bash
/biz-contract-plugin:draft "Software Subscription Agreement"
/biz-plan-plugin:create-proposal
```
## 🔐 Private Repository Access
For background auto-updates, set authentication token:
```bash
# Add to ~/.zshrc or ~/.bashrc
export GITEA_TOKEN="your-gitea-token"
```
## 📝 Plugin Naming Convention
All plugins use namespaced commands to avoid conflicts:
- Format: `/plugin-name:command-name`
- Example: `/ai-proj-plugin:create-task`
## 🔄 Updates
Update the marketplace and plugins:
```bash
# Update marketplace catalog
/plugin marketplace update coolbuy-claude-plugins
# Update specific plugin
/plugin update ai-proj-plugin@coolbuy-claude-plugins
# Update all plugins from this marketplace
/plugin update-all
```
## 🏗️ Plugin Structure
Each plugin follows the official Claude Code plugin structure:
```
plugin-name-plugin/
├── .claude-plugin/
│ └── plugin.json # Plugin metadata
├── skills/
│ └── SKILL.md # Agent skills
└── [scripts, configs, etc.]
```
## 📚 Documentation
- [Official Claude Code Plugin Docs](https://code.claude.com/docs/en/plugins)
- [Plugin Marketplace Guide](https://code.claude.com/docs/en/plugin-marketplaces)
## 👤 Author
**Donglin Lai (qiudl)**
- Email: qiudl@zhiyuncai.com
- Organization: PipeX ERP
## 📄 License
Individual plugins may have their own licenses. Please check each plugin's documentation.
## 🤝 Contributing
To add or update plugins:
1. Clone this repository
2. Add/modify plugins in the `plugins/` directory
3. Regenerate `marketplace.json` using the provided scripts
4. Submit changes
---
**Repository**: git@gitea.pipexerp.com:huangjun/claude-marketplace.git

129
SETUP.md Normal file
View File

@@ -0,0 +1,129 @@
# Setup Guide
## 1. Create Repository on Gitea
Go to https://gitea.pipexerp.com and create a new repository:
- Name: `claude-marketplace`
- Visibility: Private or Public (your choice)
- **Do NOT** initialize with README (we already have one)
## 2. Push to Gitea
```bash
cd /Users/junhuang/coolbuy/claude-marketplace
git push -u origin main
```
## 3. Test Installation
### Add the marketplace
```bash
# SSH (recommended)
/plugin marketplace add git@gitea.pipexerp.com:huangjun/claude-marketplace.git
# OR HTTPS (requires credential configuration)
/plugin marketplace add https://gitea.pipexerp.com/huangjun/claude-marketplace.git
```
### List available plugins
```bash
/plugin marketplace list coolbuy-claude-plugins
```
### Install a plugin
```bash
/plugin install ai-proj-plugin@coolbuy-claude-plugins
```
### Test the plugin
```bash
# Skills are auto-invoked by Claude when relevant, or use:
/help
# Check for your installed plugins
```
## 4. Update Plugins Later
When you make changes and push updates:
```bash
cd /Users/junhuang/coolbuy/claude-marketplace
# Make changes to plugins
# ...
# Regenerate marketplace.json if needed
python3 generate-marketplace.py
# Commit and push
git add .
git commit -m "Update plugins"
git push
```
Users update with:
```bash
/plugin marketplace update coolbuy-claude-plugins
/plugin update ai-proj-plugin@coolbuy-claude-plugins
```
## 5. Private Repository Setup
If your Gitea repo is private, users need authentication:
**For manual operations** (install, update):
- SSH: Configure SSH keys in Gitea
- HTTPS: Will prompt for credentials
**For background auto-updates**:
```bash
# Add to ~/.zshrc or ~/.bashrc
export GITEA_TOKEN="your-gitea-personal-access-token"
```
To create a Gitea token:
1. Go to https://gitea.pipexerp.com/user/settings/applications
2. Generate New Token
3. Give it "Read repository" permissions
4. Copy the token and add to your environment
## 6. Structure Overview
```
claude-marketplace/
├── .claude-plugin/
│ └── marketplace.json # Catalog of all plugins
├── plugins/
│ ├── ai-proj-plugin/
│ │ ├── .claude-plugin/
│ │ │ └── plugin.json # Plugin metadata
│ │ └── skills/
│ │ └── SKILL.md # Skill definition
│ └── [33 more plugins...]
├── README.md # User documentation
├── SETUP.md # This file
└── convert-skills.sh # Conversion script (reference)
```
## Next Steps
1. ✅ Push to Gitea: `git push -u origin main`
2. ✅ Test locally: `/plugin marketplace add <url>`
3. ✅ Install plugins: `/plugin install <name>@coolbuy-claude-plugins`
4. ✅ Share with team: Send them the repository URL
## Troubleshooting
**"Failed to clone repository"**
- Check SSH key configuration: `ssh -T git@gitea.pipexerp.com -p 10022`
- Or use HTTPS with credentials
**"Plugin not found"**
- Verify marketplace added: `/plugin marketplace list`
- Check plugin name is correct
- Ensure marketplace.json is valid: `cat .claude-plugin/marketplace.json | jq`
**"Skills not working"**
- Skills are Agent Skills (auto-invoked by Claude when relevant)
- They don't create slash commands
- Check plugin installation: `/plugin list`

218
SYNC-GUIDE.md Normal file
View File

@@ -0,0 +1,218 @@
# Skill Sync Guide
## Overview
This guide explains how to keep your local skills (`~/.claude/skills/`) synchronized with the marketplace plugins.
## Quick Sync
```bash
cd /path/to/claude-marketplace
./sync-skills.sh
```
This will:
1. ✅ Compare local skills with marketplace plugins
2. Add new skills as plugins
3. 📝 Update changed skills
4. ✓ Skip unchanged plugins
## Sync Workflow
### 1. Edit Skills Locally
Work on your skills in `~/.claude/skills/`:
```bash
code ~/.claude/skills/my-skill/SKILL.md
```
### 2. Run Sync Script
```bash
cd ~/path/to/claude-marketplace
./sync-skills.sh
```
### 3. Review Changes
```bash
git status
git diff
```
### 4. Commit & Push
```bash
git add .
git commit -m "Update skill: description of changes"
git push
```
### 5. Team Updates
Team members update with:
```bash
/plugin marketplace update coolbuy-claude-plugins
/plugin update <plugin-name>@coolbuy-claude-plugins
```
## Automated Sync (Optional)
### Git Hook (Pre-commit)
Auto-sync when committing changes to skills:
```bash
# In your dotfiles/skills repo
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
# Auto-sync skills to marketplace
~/path/to/claude-marketplace/sync-skills.sh
EOF
chmod +x .git/hooks/pre-commit
```
### Cron Job (Scheduled)
Sync daily at 9 AM:
```bash
crontab -e
# Add this line:
0 9 * * * cd ~/path/to/claude-marketplace && ./sync-skills.sh && git add . && git commit -m "Daily sync" && git push
```
## Skill Splitting Guidelines
From `~/.claude/CLAUDE.md`:
- **Token Limit**: Single skill ≤ 10,000 tokens
- **Check Size**: `wc -w ~/.claude/skills/<skill>/SKILL.md`
- **When to Split**: If > 7,500 words (≈10,000 tokens)
### Split Strategy
When a skill grows too large:
1. **Entry Skill** - Overview + command routing (<100 lines)
- Example: `req/SKILL.md`
2. **Command Reference** - Detailed commands (<200 lines)
- Example: `req-commands/SKILL.md`
3. **Workflow Guide** - Complete processes (<200 lines)
- Example: `req-workflow/SKILL.md`
4. **Methodology** - Complex concepts (<150 lines)
- Example: `req-review/SKILL.md`
## Troubleshooting
### Sync Script Fails
```bash
# Check permissions
ls -la sync-skills.sh
# Make executable
chmod +x sync-skills.sh
# Check paths
echo $HOME/.claude/skills
```
### marketplace.json Not Updated
```bash
# Manually regenerate
python3 generate-marketplace.py
# Or edit directly
code .claude-plugin/marketplace.json
```
### Git Conflicts
```bash
# Discard local changes
git checkout .claude-plugin/marketplace.json
# Or merge manually
git mergetool
```
## Best Practices
### 1. Descriptive Frontmatter
Always include in `SKILL.md`:
```yaml
---
name: skill-name
description: Clear, concise description of what this skill does
---
```
### 2. Version Bumping
When making significant changes:
```bash
# Update version in plugin.json
{
"version": "1.1.0" # was 1.0.0
}
```
### 3. Testing Before Sync
```bash
# Test skill locally first
/skill-name
# Then sync to marketplace
./sync-skills.sh
```
### 4. Commit Messages
Use clear, descriptive messages:
```bash
git commit -m "Add feishu-bitable plugin for table operations"
git commit -m "Update req-workflow with new approval process"
git commit -m "Fix: Correct PRD template in req-prd"
```
## Monitoring
### Check Sync Status
```bash
# Compare local vs marketplace
diff -qr ~/.claude/skills /tmp/claude-marketplace/plugins
```
### List Differences
```bash
# Find skills not in marketplace
comm -23 <(ls ~/.claude/skills | sort) <(ls plugins | sed 's/-plugin$//' | sort)
# Find plugins not in local
comm -13 <(ls ~/.claude/skills | sort) <(ls plugins | sed 's/-plugin$//' | sort)
```
## FAQ
**Q: Can I sync in reverse (marketplace → local)?**
A: Not recommended. Treat local skills as the source of truth.
**Q: What about binary files (images, scripts)?**
A: Copy them manually to the plugin directory, then commit.
**Q: How do I remove a plugin?**
A: Delete the plugin directory, regenerate marketplace.json, commit, and push.
**Q: Can I sync specific skills only?**
A: Modify `sync-skills.sh` to accept a skill name parameter.

82
convert-skills.sh Executable file
View File

@@ -0,0 +1,82 @@
#!/bin/bash
# Convert dotfiles custom skills to official Claude Code plugins
SOURCE_DIR="/Users/junhuang/coolbuy/new-ai-proj/dotfiles/claude-skills"
TARGET_DIR="/Users/junhuang/coolbuy/claude-marketplace/plugins"
echo "Converting skills to plugins..."
# Loop through each skill directory
for skill_dir in "$SOURCE_DIR"/*; do
# Skip non-directories and special files
if [ ! -d "$skill_dir" ] || [ "$(basename "$skill_dir")" = ".git" ]; then
continue
fi
skill_name=$(basename "$skill_dir")
plugin_name="${skill_name}-plugin"
plugin_dir="$TARGET_DIR/$plugin_name"
echo "Processing: $skill_name -> $plugin_name"
# Create plugin directory structure
mkdir -p "$plugin_dir/.claude-plugin"
mkdir -p "$plugin_dir/skills"
# Copy SKILL.md if exists
if [ -f "$skill_dir/SKILL.md" ]; then
cp "$skill_dir/SKILL.md" "$plugin_dir/skills/"
fi
# Copy skill.yaml if exists (for reference)
if [ -f "$skill_dir/skill.yaml" ]; then
cp "$skill_dir/skill.yaml" "$plugin_dir/.skill.yaml.original"
fi
# Copy any Python scripts or other files
find "$skill_dir" -type f \( -name "*.py" -o -name "*.sh" -o -name "*.js" -o -name "*.json" -o -name "*.md" \) -not -name "SKILL.md" -not -name "skill.yaml" -exec cp {} "$plugin_dir/" \;
# Copy subdirectories (like scripts/)
for subdir in "$skill_dir"/*; do
if [ -d "$subdir" ]; then
subdir_name=$(basename "$subdir")
cp -r "$subdir" "$plugin_dir/"
fi
done
# Read version from skill.yaml if exists, otherwise use 1.0.0
version="1.0.0"
if [ -f "$skill_dir/skill.yaml" ]; then
yaml_version=$(grep "^version:" "$skill_dir/skill.yaml" | sed 's/version: *//' | tr -d '"' | tr -d "'")
if [ ! -z "$yaml_version" ]; then
version="$yaml_version"
fi
fi
# Read description from skill.yaml
description="Plugin for $skill_name"
if [ -f "$skill_dir/skill.yaml" ]; then
yaml_desc=$(grep "^description:" "$skill_dir/skill.yaml" | sed 's/description: *//' | tr -d '"')
if [ ! -z "$yaml_desc" ]; then
description="$yaml_desc"
fi
fi
# Create plugin.json
cat > "$plugin_dir/.claude-plugin/plugin.json" << EOF
{
"name": "$plugin_name",
"description": "$description",
"version": "$version",
"author": {
"name": "qiudl"
}
}
EOF
echo " ✓ Created $plugin_name"
done
echo ""
echo "Conversion complete! Created $(ls -1 "$TARGET_DIR" | wc -l) plugins."

74
generate-marketplace.py Normal file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import json
import os
from pathlib import Path
# Paths
script_dir = Path(__file__).parent.resolve()
plugins_dir = script_dir / "plugins"
marketplace_file = script_dir / ".claude-plugin" / "marketplace.json"
# Category mapping
def get_category_and_keywords(plugin_name):
if any(x in plugin_name for x in ['dev-', 'coding', 'frontend']):
return "development", ["development", "coding", "workflow"]
elif any(x in plugin_name for x in ['ops-', 'deploy', 'server']):
return "devops", ["devops", "deployment", "operations"]
elif any(x in plugin_name for x in ['ai-proj', 'req']):
return "productivity", ["project-management", "tasks", "requirements"]
elif any(x in plugin_name for x in ['feishu', 'wecom', 'siyuan']):
return "integration", ["integration", "automation", "productivity"]
elif 'biz-' in plugin_name:
return "business", ["business", "planning", "contracts"]
elif 'session' in plugin_name:
return "workflow", ["session", "workflow", "productivity"]
else:
return "utility", ["utility", "tools"]
# Collect plugins
plugins = []
for plugin_dir in sorted(plugins_dir.glob("*-plugin")):
if not plugin_dir.is_dir():
continue
manifest_path = plugin_dir / ".claude-plugin" / "plugin.json"
if not manifest_path.exists():
continue
with open(manifest_path) as f:
manifest = json.load(f)
plugin_name = plugin_dir.name
category, keywords = get_category_and_keywords(plugin_name)
plugins.append({
"name": plugin_name,
"source": f"./plugins/{plugin_name}",
"description": manifest.get("description", f"Plugin for {plugin_name}"),
"version": manifest.get("version", "1.0.0"),
"category": category,
"keywords": keywords,
"strict": False
})
# Create marketplace
marketplace = {
"name": "coolbuy-claude-plugins",
"owner": {
"name": "Donglin Lai (qiudl)",
"email": "qiudl@zhiyuncai.com"
},
"metadata": {
"description": "Custom Claude Code plugins for development workflows, DevOps, and business operations",
"version": "1.0.0",
"pluginRoot": "./plugins"
},
"plugins": plugins
}
# Write marketplace.json
with open(marketplace_file, 'w') as f:
json.dump(marketplace, f, indent=2, ensure_ascii=False)
print(f"✓ Generated marketplace.json with {len(plugins)} plugins")

102
generate-marketplace.sh Executable file
View File

@@ -0,0 +1,102 @@
#!/bin/bash
# Generate marketplace.json from converted plugins
TARGET_DIR="/Users/junhuang/coolbuy/claude-marketplace/plugins"
MARKETPLACE_FILE="/Users/junhuang/coolbuy/claude-marketplace/.claude-plugin/marketplace.json"
echo "Generating marketplace.json..."
# Start JSON
cat > "$MARKETPLACE_FILE" << 'EOF'
{
"name": "coolbuy-claude-plugins",
"owner": {
"name": "Donglin Lai (qiudl)",
"email": "qiudl@zhiyuncai.com"
},
"metadata": {
"description": "Custom Claude Code plugins for development workflows, DevOps, and business operations",
"version": "1.0.0",
"pluginRoot": "./plugins"
},
"plugins": [
EOF
# Loop through each plugin and extract info
first=true
for plugin_dir in "$TARGET_DIR"/*-plugin; do
if [ ! -d "$plugin_dir" ]; then
continue
fi
plugin_name=$(basename "$plugin_dir")
manifest="$plugin_dir/.claude-plugin/plugin.json"
if [ ! -f "$manifest" ]; then
continue
fi
# Extract info from plugin.json using grep and sed
description=$(grep '"description"' "$manifest" | head -1 | sed 's/.*"description": *"\([^"]*\)".*/\1/')
version=$(grep '"version"' "$manifest" | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
# Determine category based on plugin name
category="utility"
keywords=""
case "$plugin_name" in
*dev-*|*coding*|*frontend*)
category="development"
keywords='["development", "coding", "workflow"]'
;;
*ops-*|*deploy*|*server*)
category="devops"
keywords='["devops", "deployment", "operations"]'
;;
*ai-proj*|*req*)
category="productivity"
keywords='["project-management", "tasks", "requirements"]'
;;
*feishu*|*wecom*|*siyuan*)
category="integration"
keywords='["integration", "automation", "productivity"]'
;;
*biz-*)
category="business"
keywords='["business", "planning", "contracts"]'
;;
*session*)
category="workflow"
keywords='["session", "workflow", "productivity"]'
;;
esac
# Add comma if not first entry
if [ "$first" = true ]; then
first=false
else
echo "," >> "$MARKETPLACE_FILE"
fi
# Write plugin entry
cat >> "$MARKETPLACE_FILE" << ENTRY
{
"name": "$plugin_name",
"source": "$plugin_name",
"description": "$description",
"version": "$version",
"category": "$category",
"keywords": $keywords,
"strict": false
}ENTRY
done
# Close JSON
cat >> "$MARKETPLACE_FILE" << 'EOF'
]
}
EOF
echo "✓ Generated marketplace.json with $(grep -c '"name"' "$MARKETPLACE_FILE" | awk '{print $1-1}') plugins"

View File

@@ -0,0 +1,8 @@
{
"name": "agent-swarm-plugin",
"description": "Multi-agent orchestration using OpenAI Swarm patterns. Coordinate specialized agents for complex development workflows with handoffs and context sharing.",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,406 @@
---
name: agent-swarm
description: Multi-agent orchestration using OpenAI Swarm patterns. Coordinate specialized agents for complex development workflows with handoffs and context sharing.
---
# Agent Swarm - Multi-Agent Orchestration
基于 OpenAI Swarm 设计模式的多智能体协作系统,用于复杂开发任务的智能分解与协调。
## 核心概念
### 1. Agent智能体
每个 Agent 是具有特定职责的专家:
- **Instructions**: Agent 的角色定义和行为准则
- **Functions**: Agent 可以调用的工具函数
- **Handoffs**: 何时移交给其他 Agent
### 2. Handoff任务移交
Agent 之间的控制权转移机制:
- 当前 Agent 完成自己的职责
- 识别需要其他专长
- 移交给最合适的 Agent
### 3. Context Variables上下文变量
跨 Agent 共享的状态:
- 项目目录
- 技术栈信息
- 当前进度
- 发现的问题
---
## 预定义 Agent
### 1. Architect Agent架构师
**职责**: 理解需求、技术选型、设计系统架构
**何时使用**:
- 用户描述新功能或系统
- 需要技术方案设计
- 需要架构评审
**工具**:
- Read codebase
- Grep patterns
- 设计文档生成
**Handoff to**:
- Coder Agent开始编码
- Reviewer Agent评审设计
---
### 2. Coder Agent编码者
**职责**: 实现功能、编写代码、修复 bug
**何时使用**:
- 架构师完成设计
- 用户提出 bug 修复
- 需要代码重构
**工具**:
- Edit files
- Write files
- Git operations
**Handoff to**:
- Tester Agent代码完成后
- Architect Agent遇到设计问题
---
### 3. Tester Agent测试员
**职责**: 编写测试、运行测试、验证功能
**何时使用**:
- 代码编写完成
- 需要测试覆盖
- 验证 bug 修复
**工具**:
- Run tests
- Write test cases
- Coverage reports
**Handoff to**:
- Deployer Agent测试通过
- Coder Agent发现问题
---
### 4. Deployer Agent部署员
**职责**: 构建镜像、部署服务、监控上线
**何时使用**:
- 测试全部通过
- 需要发布到环境
- 需要回滚版本
**工具**:
- Docker build
- SSH deployment
- Health checks
**Handoff to**:
- Monitor Agent部署完成
- Coder Agent部署失败
---
### 5. Reviewer Agent评审员
**职责**: 代码审查、文档审查、安全检查
**何时使用**:
- PR 创建后
- 重要功能完成
- 需要质量把关
**工具**:
- Diff analysis
- Security scan
- Best practices check
**Handoff to**:
- Coder Agent需要修改
- Deployer Agent审查通过
---
## 使用方法
### 基本调用
```bash
/swarm start "在 new-ai-proj 中实现任务批量删除功能"
```
**执行流程**:
1. **Architect** 分析需求 → 设计 API 和前端交互
2. **Coder** 实现后端 API → 实现前端 UI
3. **Tester** 编写单元测试 → 运行测试
4. **Reviewer** 代码审查 → 安全检查
5. **Deployer** 部署到 staging → 验证功能
---
### 指定起始 Agent
```bash
/swarm coder "修复 backend/handlers/task_handler.go 的空指针 bug"
```
直接从 Coder Agent 开始,跳过架构设计阶段。
---
### 传递上下文
```bash
/swarm start "优化数据库查询性能" \
--context project=/Users/coolbuy-dev/coding/new-ai-proj \
--context stack=Go,PostgreSQL,Redis \
--context module=backend/services
```
---
### 查看执行轨迹
```bash
/swarm trace
```
显示 Agent 调用链:
```
Architect → analyzed requirements (3 min)
↓ handoff: "Design complete, ready for implementation"
Coder → implemented 5 files (12 min)
↓ handoff: "Code complete, needs testing"
Tester → wrote 8 test cases, all passed (5 min)
↓ handoff: "Tests passed, ready for review"
Reviewer → approved with 2 suggestions (2 min)
↓ handoff: "Approved, ready for deployment"
Deployer → deployed to staging, health check OK (3 min)
```
---
## 配置文件
### swarm.yaml
在项目根目录创建 `swarm.yaml` 自定义 Agent 行为:
```yaml
agents:
architect:
instructions: |
你是系统架构师,专注于 Go + Vue.js 技术栈。
遵循 RESTful API 设计原则。
考虑性能、安全性、可维护性。
max_turns: 5
coder:
instructions: |
你是 Go 后端工程师和 Vue.js 前端工程师。
编写清晰、简洁、高性能的代码。
遵循项目现有代码风格。
tools:
- Edit
- Write
- Bash
max_turns: 10
tester:
instructions: |
你是测试工程师,编写全面的测试用例。
确保边界条件、错误处理、并发安全。
tools:
- Bash
- Write
test_command: "go test ./... -v"
max_turns: 5
context_variables:
project_root: /Users/coolbuy-dev/coding/new-ai-proj
backend_lang: Go 1.21
frontend_framework: Vue 3
database: PostgreSQL 15
deployment_target: staging.ai.pipexerp.com
```
---
## 高级功能
### 1. 自定义 Agent
```yaml
agents:
database-optimizer:
instructions: |
你是数据库性能优化专家。
分析慢查询、优化索引、设计缓存策略。
functions:
- explain_analyze
- create_index
- cache_design
handoff_to:
- coder # 实现优化方案
```
---
### 2. 条件 Handoff
```yaml
handoff_rules:
- from: tester
to: coder
condition: "test_pass_rate < 90%"
message: "测试失败率超过 10%,需要修复"
- from: tester
to: deployer
condition: "test_pass_rate == 100%"
message: "所有测试通过,可以部署"
```
---
### 3. 并行 Agent
对于独立任务,多个 Agent 可以并行工作:
```bash
/swarm parallel \
"coder: 实现后端 API" \
"coder: 实现前端 UI" \
"tester: 编写 API 测试"
```
---
## 与 Remote Coding 集成
在 OpenClaw 中调用本地 Claude Code 执行 Swarm 工作流:
```bash
# OpenClaw 调用 Melbourne Claude Code
ssh melbourne "cd /Users/coolbuy-dev/coding/new-ai-proj && \
/opt/homebrew/bin/claude --dangerously-skip-permissions \
-p '/swarm start 实现任务批量删除功能'"
```
---
## 实际案例
### 案例 1: 新功能开发
**任务**: "为 AI-Proj 实现需求批量导出功能"
**执行过程**:
1. **Architect**:
- 分析需求导出格式Excel/PDF、筛选条件、数据脱敏
- 设计 API: `POST /api/v1/requirements/export`
- 设计前端:导出按钮、进度条、下载链接
2. **Coder**:
- 后端实现 export service
- 前端实现导出 UI 组件
- 集成 file download 功能
3. **Tester**:
- 测试大量数据导出1000+ 需求)
- 测试并发导出
- 测试下载失败重试
4. **Reviewer**:
- 检查文件大小限制
- 检查内存泄漏风险
- 检查数据权限控制
5. **Deployer**:
- 部署到 staging
- 验证导出功能
- 监控资源使用
---
### 案例 2: Bug 修复
**任务**: "修复任务详情页加载缓慢问题"
**执行过程**:
1. **Architect**:
- 分析性能瓶颈N+1 查询问题
- 设计优化方案:使用 JOIN 和预加载
2. **Coder**:
- 优化数据库查询
- 添加 Redis 缓存
- 更新前端数据获取逻辑
3. **Tester**:
- 性能测试:加载时间从 3s → 300ms
- 并发测试100 用户同时访问
- 缓存一致性测试
4. **Deployer**:
- 灰度发布到 10% 用户
- 监控性能指标
- 全量发布
---
## 最佳实践
1. **明确任务范围**: 复杂任务交给 Swarm简单任务直接执行
2. **合理设置 max_turns**: 避免 Agent 陷入死循环
3. **记录 Handoff 原因**: 便于追溯和调试
4. **定期审查轨迹**: 优化 Agent 协作流程
5. **利用 Context Variables**: 避免重复传递信息
---
## 故障排查
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| Agent 一直循环 | max_turns 设置过大 | 降低 max_turns添加明确的 handoff 条件 |
| Handoff 失败 | 目标 Agent 未定义 | 检查 swarm.yaml 配置 |
| 上下文丢失 | Context Variables 未传递 | 在 handoff 时显式传递 context |
| 执行太慢 | 串行执行可并行任务 | 使用 `/swarm parallel` |
---
## 与其他 Skills 集成
- **dev-coding**: Coder Agent 使用 dev-coding 的编码规范
- **dev-test**: Tester Agent 使用 dev-test 的测试策略
- **ops-tools**: Deployer Agent 使用 ops-tools 进行部署
- **ai-proj**: 所有 Agent 使用 ai-proj MCP 进行任务同步
---
## 命令速查
| 命令 | 功能 |
|------|------|
| `/swarm start <task>` | 启动 Swarm 工作流(从 Architect 开始) |
| `/swarm <agent> <task>` | 从指定 Agent 开始 |
| `/swarm parallel <tasks>` | 并行执行多个任务 |
| `/swarm trace` | 查看执行轨迹 |
| `/swarm config` | 显示当前配置 |
| `/swarm agents` | 列出所有可用 Agent |
| `/swarm stop` | 终止当前 Swarm 执行 |
---
## 参考资料
- [OpenAI Swarm 文档](https://github.com/openai/swarm)
- [Multi-Agent Systems 设计模式](https://arxiv.org/abs/2308.00352)
- [Claude Code Skills 文档](https://docs.anthropic.com/claude-code/skills)

View File

@@ -0,0 +1,8 @@
{
"name": "ai-proj-plugin",
"description": "AI project management via REST API. Works out of the box!",
"version": "2.0.1",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,136 @@
# ai-proj Plugin
AI project task and requirement management through direct REST API calls.
## Features
- **Task Management**: Create, list, update, and complete tasks
- **Requirement Management**: Full requirement lifecycle (draft → review → development → complete)
- **Documentation**: Create and manage task documents
- **Daily Focus**: Plan and view today's tasks
- **Direct API Integration**: Uses REST API directly
## Quick Start
Just install the plugin and start using it:
```bash
/plugin install ai-proj-plugin@coolbuy-claude-plugins
```
That's it! No configuration needed.
## Usage
This plugin provides **Agent Skills** that I (Claude) automatically use when you mention relevant topics.
### Examples
**Task Management:**
- "Create a task: implement user authentication"
- "List all in-progress tasks"
- "Complete task 123"
- "Show me task 456"
**Requirement Management:**
- "Create requirement: add dark mode support"
- "List requirements in draft status"
- "Submit requirement 789 for review"
- "Approve requirement 789"
**Daily Focus:**
- "Show today's tasks"
- "Add task 123 to today's focus"
**Documents:**
- "Show the document for task 123"
- "Update task 456's document with: [content]"
## Configuration (Optional)
The plugin uses a default API token that works for most users. If you need to use a custom token:
### Environment Variable
```bash
export AIPROJ_TOKEN="your_custom_token_here"
```
Add this to your `~/.zshrc` or `~/.bashrc` to make it persistent.
## How It Works
This plugin uses a simple, straightforward approach:
1. Makes direct REST API calls to `https://ai.pipexerp.com/api/v1`
2. Uses `curl` for HTTP requests
3. Automatically finds the API token from:
- `$AIPROJ_TOKEN` environment variable
- Built-in default token
## Advantages
-**No setup required** - works immediately after installation
-**No server process** - simple and lightweight
-**No dependencies** - just needs `curl` (built into all systems)
-**Simple architecture** - direct API calls
-**Easy debugging** - can see exact API calls
## API Endpoints
The plugin uses these endpoints:
- `GET /tasks` - List tasks
- `POST /tasks` - Create task
- `GET /tasks/{id}` - Get task details
- `PATCH /tasks/{id}` - Update task
- `DELETE /tasks/{id}` - Delete task
- `GET /requirements` - List requirements
- `POST /requirements` - Create requirement
- `POST /requirements/{id}/actions` - Update requirement status
- `GET /tasks/{id}/document` - Get task document
- `PUT /tasks/{id}/document` - Update task document
- `GET /daily-focus/tasks` - Get today's tasks
- `POST /daily-focus/tasks` - Add task to today
## Troubleshooting
### "Unauthorized" or API errors
Check your token:
```bash
echo $AIPROJ_TOKEN
```
If empty, either set the environment variable or the plugin will use the default token.
### API not responding
Verify the API is accessible:
```bash
curl -I https://ai.pipexerp.com/api/v1/health
```
### Skills not working
- Agent Skills don't create slash commands
- Just ask naturally: "create a task", "list tasks", etc.
- The plugin is automatically active when installed
## Technical Details
**API Base**: `https://ai.pipexerp.com/api/v1`
**Authentication**: Bearer token in `Authorization` header
**Data Format**: JSON
**Date Format**: ISO 8601 (YYYY-MM-DD)
**Default Project ID**: 1
## Documentation
For detailed API documentation and examples, see `skills/SKILL.md`.
## Support
- Repository: git@gitea.pipexerp.com:huangjun/claude-marketplace.git
- Author: qiudl@zhiyuncai.com

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
{
"name": "biz-contract-plugin",
"description": "商务合同撰写。支持多种合同类型:软件订阅合同、软件定制开发合同、物流合同、销售服务合同、贸易合同等。当用户提到合同、协议、签约、合作协议相关任务时自动激活。",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,742 @@
---
name: biz-contract
description: 商务合同撰写。支持多种合同类型:软件订阅合同、软件定制开发合同、物流合同、销售服务合同、贸易合同等。当用户提到合同、协议、签约、合作协议相关任务时自动激活。
---
# 商务合同撰写技能 (biz-contract)
## 概述
本技能用于辅助商务合同的撰写与管理,支持多家公司主体、多种合同类型。
---
## 用户公司信息
### 公司主体列表
| 公司简称 | 公司全称 | 纳税人识别号 | 地址 | 联系电话 | 法定代表人 |
|----------|----------|--------------|------|----------|------------|
| 智慧云彩 | 北京智慧云彩电子商务科技有限公司 | 91110114MA004M80XX | 北京市海淀区丰智东路13号院1号楼1层101 | - | - |
| 欢乐宿 | 北京欢乐宿供应链科技有限公司 | 91110113MACWJKYU8P | 北京市顺义区军营南街10号院3幢3层327室 | - | - |
| 对丝 | 北京对丝信息技术有限公司 | 91110113MAE4XXHR5E | 北京市顺义区军营南街10号院3幢3层3216室 | - | - |
| 妗晨 | 重庆妗晨工贸有限公司 | 91500104MA7EJTPA6D | 重庆市大渡口区跳磴镇海康路106号1-1 | 15213397998 | - |
| 名风 | 北京名风新能源科技有限公司 | 91110106092440790K | 北京市密云区西田各庄镇雁密路99号601室-3509集群注册 | - | 魏小健 |
| 鸿侨 | 沈阳鸿侨物流有限公司 | 91210113MADBXPU93P | 辽宁省沈阳市沈北新区蒲河路49-2号1-29-2 | 15567339320 | 禚子乔 |
### 开户信息
| 公司简称 | 开户银行 | 银行账号 |
|----------|----------|----------|
| 智慧云彩 | 招商银行股份有限公司北京立水桥支行 | 110922001210512 |
| 欢乐宿 | 中国工商银行北京南十里支行 | 0200206709200036024 |
| 对丝 | 招商银行股份有限公司北京顺义支行 | 110961225610001 |
| 妗晨 | 中国农业银行股份有限公司重庆大渡口天安支行 | 31230401040004386 |
| 名风 | 中国建设银行北京黄亦路支行 | 11001181700052501668 |
| 鸿侨 | 中国建设银行股份有限公司沈阳辉山支行 | 21050149004200001104 |
---
## 支持的合同类型
| 类型 | 说明 | 典型场景 |
|------|------|----------|
| 软件订阅合同 | SaaS 软件服务订阅 | 云服务、在线系统 |
| 软件定制开发合同 | 软件外包开发 | 项目制开发 |
| 物流合同 | 物流运输服务 | 货运、仓储 |
| 销售服务合同 | 产品销售与服务 | 商品买卖 |
| 贸易合同 | 商品贸易 | 进出口、批发 |
| 技术服务合同 | 技术咨询、支持 | IT 服务 |
| 保密协议 (NDA) | 信息保密 | 合作前签署 |
---
## 合同撰写流程
```
1. 确定合同类型
├── 识别业务场景
├── 选择合同模板
└── 确定双方主体
2. 收集合同要素
├── 甲方/乙方信息
├── 标的物/服务内容
├── 价款与支付方式
├── 履行期限
└── 特殊条款需求
3. 生成合同草稿
├── 套用模板
├── 填充具体内容
└── 调整条款
4. 审核与完善
├── 法律合规检查
├── 风险条款审查
└── 最终定稿
```
---
## 合同模板
### 1. 软件订阅合同 (SaaS)
```markdown
# 软件订阅服务合同
**合同编号**: [自动生成或手填]
**甲方(服务提供方)**: [公司全称]
统一社会信用代码: [代码]
地址: [地址]
法定代表人: [姓名]
**乙方(订阅方)**: [公司全称]
统一社会信用代码: [代码]
地址: [地址]
法定代表人: [姓名]
---
鉴于甲方拥有 [软件名称] 的合法运营权,乙方有意订阅使用该软件服务,双方本着平等互利的原则,经友好协商,达成如下协议:
## 第一条 软件服务内容
1.1 软件名称:[软件名称]
1.2 服务内容:[功能模块描述,或"详见系统操作手册"]
1.3 服务方式SaaS 云服务模式,乙方通过互联网访问使用
1.4 账号数量:[数量] 个用户账号
## 第二条 服务期限
2.1 服务期限:自 [起始日期] 至 [结束日期]
2.2 续约条款:合同期满前 [30] 日,双方均未提出书面异议的,本合同自动续期 [1] 年,届时服务费按届时标准执行
## 第三条 服务费用与支付
3.1 服务费用:
- 月付标准:人民币 [金额] 元/月
- 年付标准:人民币 [金额] 元/年
3.2 支付方式:[年付/月付/季付]
3.3 付款时间:乙方应于本合同签订后 [N] 个工作日内,在收到甲方开具的发票后,一次性支付服务费用
3.4 付款账户:
开户名称:[甲方公司全称]
开户银行:[银行名称]
银行账号:[账号]
## 第四条 甲方权利与义务
4.1 甲方应保证软件服务的稳定运行,年可用性不低于 99.5%
4.2 甲方应提供必要的技术支持和培训服务
4.3 甲方应对乙方的业务数据严格保密
4.4 甲方有权在乙方未按时付款时暂停服务
## 第五条 乙方权利与义务
5.1 乙方有权在服务期内正常使用软件服务
5.2 乙方应按约定及时支付服务费用
5.3 乙方不得将账号转让、出租给第三方
5.4 乙方应妥善保管账号密码,因保管不善造成的损失由乙方承担
## 第六条 数据安全与保密
6.1 甲方应采取合理的技术措施保护乙方数据安全
6.2 未经对方书面同意,任何一方不得向第三方披露本合同内容及对方商业秘密
6.3 保密义务在合同终止后 [2] 年内继续有效
## 第七条 违约责任
7.1 任何一方违反本合同约定,应向守约方支付合同总金额 [10]% 的违约金
7.2 因甲方原因导致服务中断超过 [24] 小时的,甲方应按日退还相应服务费
## 第八条 合同变更与解除
8.1 经双方协商一致,可以变更或解除本合同
8.2 乙方提前解约的,已付费用不予退还
8.3 甲方提前解约的,应退还乙方剩余期限的服务费用
## 第九条 争议解决
9.1 本合同的签订、履行、解释均适用中华人民共和国法律
9.2 双方因履行本合同发生争议,应友好协商解决;协商不成的,任何一方均可向甲方所在地人民法院提起诉讼
## 第十条 其他条款
10.1 本合同一式两份,双方各执一份,具有同等法律效力
10.2 本合同自双方签字盖章之日起生效
10.3 本合同未尽事宜,由双方另行协商签订补充协议
---
**甲方(盖章)**: **乙方(盖章)**:
法定代表人/授权代表: 法定代表人/授权代表:
日期: 日期:
```
---
### 2. 软件定制开发合同
```markdown
# 软件定制开发合同
**合同编号**: [编号]
**甲方(委托方)**: [公司全称]
**乙方(开发方)**: [公司全称]
---
## 第一条 项目内容
1.1 项目名称:[项目名称]
1.2 开发内容:[详细描述或附件《需求规格说明书》]
1.3 技术要求:[技术栈、性能指标等]
1.4 交付物:
- 软件源代码
- 部署文档
- 操作手册
- [其他]
## 第二条 开发周期
2.1 总工期:[N] 个工作日
2.2 里程碑:
| 阶段 | 内容 | 交付时间 |
|------|------|----------|
| 需求确认 | 需求规格说明书 | [日期] |
| 设计阶段 | 设计文档 | [日期] |
| 开发阶段 | 功能开发完成 | [日期] |
| 测试阶段 | 测试报告 | [日期] |
| 验收交付 | 全部交付物 | [日期] |
## 第三条 合同金额与支付
3.1 合同总金额:人民币 [金额] 元(大写:[大写金额]
3.2 支付方式:
- 合同签订后 [N] 日内,支付 [30]%,即 [金额] 元
- 需求确认后 [N] 日内,支付 [30]%,即 [金额] 元
- 验收通过后 [N] 日内,支付 [40]%,即 [金额] 元
## 第四条 验收标准
4.1 乙方完成开发后,应书面通知甲方验收
4.2 甲方应在收到通知后 [10] 个工作日内完成验收
4.3 验收标准以《需求规格说明书》为准
4.4 甲方逾期未验收且未提出书面异议的,视为验收通过
## 第五条 知识产权
5.1 定制开发的软件著作权归 [甲方/乙方/共有]
5.2 乙方保证交付的软件不侵犯任何第三方知识产权
## 第六条 质保与维护
6.1 免费质保期:验收通过后 [12] 个月
6.2 质保期内,乙方应免费修复软件缺陷
6.3 质保期后,双方可另行签订维护协议
## 第七条 违约责任
7.1 乙方逾期交付,每逾期一日,应向甲方支付合同总金额 [0.5]‰ 的违约金
7.2 甲方逾期付款,每逾期一日,应向乙方支付应付金额 [0.5]‰ 的违约金
## 第八条 其他条款
[参照标准条款]
---
**甲方(盖章)**: **乙方(盖章)**:
日期: 日期:
```
---
### 3. 物流服务合同
**物流服务增值税税率9%**
```markdown
# 物流服务合同
**合同编号**: [编号]
**甲方(托运方)**: [公司全称]
**统一社会信用代码**: [代码]
**地址**: [地址]
**联系电话**: [电话]
**乙方(承运方)**: [公司全称]
**统一社会信用代码**: [代码]
**地址**: [地址]
**法定代表人**: [姓名]
---
## 第一条 运输服务内容
1.1 服务类型:[整车运输/零担运输/仓储配送]
1.2 运输路线:[起点] 至 [终点]
1.3 货物类型:[货物描述]
## 第二条 服务期限
2.1 合同有效期:自 [起始日期] 至 [结束日期]
## 第三条 运费与结算
3.1 计费方式:[按重量/体积/件数/趟次]
3.2 运费标准:[具体标准]
3.3 结算周期:[月结/单结]
3.4 付款期限:甲方在收到乙方对账单并确认无误后 [N] 个工作日内支付运费
## 第四条 发票与税务
4.1 发票类型:增值税专用发票
4.2 税率9%(交通运输服务)
4.3 开票时间:乙方应在甲方付款前开具发票
4.4 发票内容:运输服务费
### 甲方开票信息(付款方)
- 名称:[甲方公司全称]
- 纳税人识别号:[税号]
- 地址、电话:[地址] [电话]
- 开户行及账号:[开户银行] [账号]
### 乙方开票信息(收款方)
- 名称:[乙方公司全称]
- 纳税人识别号:[税号]
- 地址、电话:[地址] [电话]
- 开户行及账号:[开户银行] [账号]
## 第五条 甲方权利与义务
5.1 如实申报货物名称、数量、重量
5.2 按约定及时支付运费
5.3 提供必要的装卸条件
## 第六条 乙方权利与义务
6.1 按时提货、送货
6.2 保证货物运输安全
6.3 提供运输单据和签收证明
6.4 按约定开具增值税专用发票
## 第七条 货损赔偿
7.1 因乙方原因造成货物损失,按货物实际价值赔偿,最高不超过运费的 [N] 倍
7.2 甲方应在签收时验货,当场提出异议
## 第八条 其他条款
[参照标准条款]
---
### 签章
| | 甲方 | 乙方 |
|---|---|---|
| 单位名称 | | |
| 法定代表人/授权代表 |(签字)|(签字)|
| 单位盖章 |(盖章处)|(盖章处)|
| 签订日期 | 年 月 日 | 年 月 日 |
```
#### 物流合同开票信息速查
| 公司简称 | 公司全称 | 纳税人识别号 | 开票地址电话 | 开户行及账号 |
|----------|----------|--------------|--------------|--------------|
| 妗晨 | 重庆妗晨工贸有限公司 | 91500104MA7EJTPA6D | 重庆市大渡口区跳磴镇海康路106号1-1 / 15213397998 | 中国农业银行重庆大渡口天安支行 31230401040004386 |
| 名风 | 北京名风新能源科技有限公司 | 91110106092440790K | 北京市密云区西田各庄镇雁密路99号601室-3509 / - | 中国建设银行北京黄亦路支行 11001181700052501668 |
| 鸿侨 | 沈阳鸿侨物流有限公司 | 91210113MADBXPU93P | 辽宁省沈阳市沈北新区蒲河路49-2号(1-29-2) / 15567339320 | 中国建设银行沈阳辉山支行 21050149004200001104 |
---
### 4. 销售服务合同
```markdown
# 销售服务合同
**合同编号**: [编号]
**甲方(销售方)**: [公司全称]
**乙方(购买方)**: [公司全称]
---
## 第一条 产品/服务内容
| 序号 | 名称 | 规格型号 | 单位 | 数量 | 单价(元) | 金额(元) |
|------|------|----------|------|------|------------|------------|
| 1 | | | | | | |
| 2 | | | | | | |
| **合计** | | | | | | **[总金额]** |
## 第二条 合同金额
2.1 合同总金额:人民币 [金额] 元(大写:[大写金额]
2.2 以上价格 [含/不含] 增值税
## 第三条 交付方式
3.1 交付时间:[日期]
3.2 交付地点:[地址]
3.3 交付方式:[送货上门/自提/物流]
## 第四条 付款方式
4.1 [全款预付/货到付款/分期付款]
4.2 付款账户:[账户信息]
## 第五条 验收标准
5.1 乙方应在收货后 [N] 日内完成验收
5.2 验收标准:[标准描述]
## 第六条 售后服务
6.1 质保期:[N] 个月
6.2 质保内容:[描述]
## 第七条 违约责任
[标准条款]
---
**甲方(盖章)**: **乙方(盖章)**:
日期: 日期:
```
---
### 5. 贸易合同
```markdown
# 贸易合同
**合同编号**: [编号]
**甲方(供货方)**: [公司全称]
**乙方(采购方)**: [公司全称]
---
## 第一条 货物明细
| 品名 | 规格 | 产地 | 单位 | 数量 | 单价 | 金额 |
|------|------|------|------|------|------|------|
| | | | | | | |
| **合计** | | | | | | |
## 第二条 质量标准
2.1 质量标准:[国标/行标/企标/样品]
2.2 检验方式:[抽检/全检]
## 第三条 包装要求
3.1 包装方式:[描述]
3.2 包装费用:[含在货款中/另计]
## 第四条 交货
4.1 交货时间:[日期]
4.2 交货地点:[地址]
4.3 运输方式:[描述]
4.4 运费承担:[甲方/乙方]
## 第五条 价款与支付
5.1 合同总价:人民币 [金额] 元
5.2 付款方式:[预付款比例、货到付款等]
5.3 发票类型:[增值税专用发票/普通发票]
## 第六条 验收与异议
6.1 验收期限:收货后 [N] 日内
6.2 异议期限:发现质量问题后 [N] 日内书面提出
## 第七条 违约责任
7.1 甲方逾期交货,每日按合同金额 [0.5]‰ 支付违约金
7.2 乙方逾期付款,每日按应付金额 [0.5]‰ 支付违约金
---
**甲方(盖章)**: **乙方(盖章)**:
日期: 日期:
```
---
## 合同要素检查清单
撰写合同时,确保包含以下要素:
### 基本要素
- [ ] 合同编号
- [ ] 合同标题
- [ ] 签约双方信息(全称、代码、地址、法人)
- [ ] 签订日期
### 核心条款
- [ ] 标的物/服务内容(明确、具体)
- [ ] 数量/规格
- [ ] 价款/费用
- [ ] 支付方式与时间
- [ ] 履行期限/服务期限
- [ ] 交付方式/地点
### 权责条款
- [ ] 甲方权利与义务
- [ ] 乙方权利与义务
- [ ] 验收标准
- [ ] 质保条款
### 风险条款
- [ ] 违约责任
- [ ] 争议解决方式
- [ ] 不可抗力
- [ ] 保密条款
### 其他
- [ ] 合同变更与解除
- [ ] 通知送达方式
- [ ] 合同份数
- [ ] 生效条件
- [ ] 签字盖章位置
---
## 使用说明
### 快速生成合同
向我描述合同需求,我会帮您生成合同草稿:
**示例输入**
> 拟定一份软件订阅合同北京智慧云彩为服务方重庆妗晨为订阅方软件是智云物流管理系统年费2.4万合同期1年到期自动续约
**我会输出**
- 完整的合同文本
- 需要补充的信息提示
- 风险提示(如有)
### 合同审核
您也可以提供合同文本让我审核,我会检查:
- 条款完整性
- 权责平衡性
- 法律风险点
- 用语规范性
---
## 注意事项
1. **本技能生成的合同仅供参考**,重要合同建议经法务/律师审核
2. 涉及大额交易、复杂条款的合同,建议专业法律意见
3. 请确保公司信息准确无误后再签署
4. 合同金额大写与小写应一致
5. 注意保留合同签署过程的证据
---
## 思源笔记集成(推荐)
合同文档优先保存到思源笔记,校对确认后再发布到云文档。
### 工作流程
```
合同技能生成内容 → 思源笔记(预览/校对) → 飞书云文档(正式发布)
```
### 笔记本配置
| 配置项 | 值 |
|--------|-----|
| 笔记本名称 | 商务合同 |
| 笔记本ID | `20260202080313-kjtgg1j` |
### 路径规范
```
/商务合同/
├── 物流合同/
│ ├── 妗晨-名风-2026
│ └── 妗晨-鸿侨-2026
├── 软件订阅合同/
│ └── 智慧云彩-xxx-2026
└── 贸易合同/
└── ...
```
路径格式:`/{合同类型}/{甲方简称}-{乙方简称}-{年份}`
### 使用示例
```python
from siyuan_api import SiYuanAPI
api = SiYuanAPI()
NOTEBOOK_ID = "20260202080313-kjtgg1j"
# 1. 创建合同到思源笔记
contract_md = generate_contract_markdown(...)
doc_id = api.upsert_doc(
NOTEBOOK_ID,
"/物流合同/妗晨-名风-2026",
contract_md
)
print(f"请在思源笔记中校对: {doc_id}")
# 2. 校对完成后,发布到飞书
api.publish_to_feishu(doc_id, "物流服务合同-妗晨与名风")
```
### 文档状态标记
| 状态 | 含义 |
|------|------|
| 📝 草稿 | 初始生成,待校对 |
| 🔍 校对中 | 正在审核内容 |
| ✅ 已确认 | 校对完成,可发布 |
| 🚀 已发布 | 已发布到云文档 |
---
## 与 ai-proj 集成
合同相关需求可创建到 ai-proj 系统:
```
类型: biz (业务类型)
分类: other
示例标题: 拟定 XXX 与 YYY 的软件订阅合同
```
合同完成后,可作为文档附件关联到对应需求。
---
## 飞书文档集成
本技能已集成飞书文档,可将合同直接创建到飞书云文档。
### 权限说明
**重要**:飞书文档默认对组织内权限是开放的,允许编辑。创建的合同文档在组织内成员均可查看和编辑。如需限制访问权限,请在飞书中手动调整文档权限设置。
### 可用工具
| 工具 | 功能 |
|------|------|
| `create_document` | 创建空白飞书文档 |
| `write_document` | 向已有文档写入内容 |
| `create_and_write_document` | 创建文档并写入内容(推荐) |
| `get_document_info` | 获取文档信息 |
### 使用示例
```
# 创建合同文档
使用 feishu-doc MCP 的 create_and_write_document 工具:
- title: "软件订阅服务合同-智慧云彩与妗晨"
- content: [Markdown 格式的合同内容]
- folder_token: [可选,指定文件夹]
```
### 获取文件夹 Token
1. 在飞书中打开目标文件夹
2. 复制 URL 中的 folder token格式如`fldcnXXXXXX`
3. 调用工具时传入 folder_token 参数
### 飞书输出格式规范
#### 开票信息表格
物流合同中的开票信息应使用**表格**格式展示,列宽配置:
| 配置项 | 值 | 说明 |
|--------|-----|------|
| 项目列宽 | 120px | 显示"名称"、"纳税人识别号"等标签 |
| 内容列宽 | 480px | 显示具体信息 |
```python
# 创建开票信息表格示例
from feishu_docx import FeishuDocx
docx = FeishuDocx()
doc = docx.create_document("物流服务合同")
# 创建表格4行2列指定列宽
table = docx.create_table(doc["document_id"], rows=4, cols=2, col_widths=[120, 480])
# 填充内容
docx.fill_table(doc["document_id"], table, [
["名称", "重庆妗晨工贸有限公司"],
["纳税人识别号", "91500104MA7EJTPA6D"],
["地址、电话", "重庆市大渡口区跳磴镇海康路106号1-1 / 15213397998"],
["开户行及账号", "中国农业银行重庆大渡口天安支行 31230401040004386"],
])
```
#### 签章格式
推荐使用**独立区块式**签章格式:
```
【甲 方】
单位名称:重庆妗晨工贸有限公司
法定代表人(或授权代表): (签字/盖章)
单位盖章:
签订日期: 年 月 日
─────────────────────────────────────────────────────
【乙 方】
单位名称:北京名风新能源科技有限公司
法定代表人(或授权代表): (签字/盖章)
单位盖章:
签订日期: 年 月 日
```
**特点**:甲乙双方独立区块,留白充足,便于盖章签字。
#### 物流合同税率
| 服务类型 | 税率 | 发票类型 |
|----------|------|----------|
| 交通运输服务 | 9% | 增值税专用发票 |
#### 默认存储路径
| 平台 | 路径 |
|------|------|
| 飞书云文档 | ai-proj/01运营 (folder_token: `C80gfkRnzlonQ5d4AhOcOACDnNg`) |
| 思源笔记 | 商务合同笔记本 /物流合同/{甲方简称}-{乙方简称}-{年份} |

View File

@@ -0,0 +1,8 @@
{
"name": "biz-ops-plugin",
"description": "商务运营技能。支持商业计划书(BP)撰写和商务合同起草。当用户提到商业计划书、BP、融资计划、商业模式、合同、协议、签约等相关任务时自动激活。",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,178 @@
---
name: biz-ops
description: 商务运营技能。支持商业计划书(BP)撰写和商务合同起草。当用户提到商业计划书、BP、融资计划、商业模式、合同、协议、签约等相关任务时自动激活。
---
# 商务运营 (biz-ops)
## 一、商业计划书 (BP)
### 公司背景
| 项目 | 描述 |
|------|------|
| 公司定位 | 酒店供应链系统研发公司 |
| 核心能力 | 软件系统研发 + 供应链运营经验 |
| 商业模式 | 换房额度 → 供应链平台采购 → 交易佣金 |
**换房模式**:酒店提供换房额度 → 转化为平台采购额度 → 酒店消费 → 平台抽佣X%
### 标准 BP 结构10-15页
| 章节 | 页数 | 核心内容 |
|------|------|----------|
| 1. 封面 | 1 | 公司名称、Slogan、联系方式 |
| 2. 公司简介 | 1 | 一句话定位、发展历程、里程碑 |
| 3. 痛点与机会 | 1-2 | 行业痛点、市场机会、为什么是现在 |
| 4. 解决方案 | 2 | 产品/服务介绍、核心功能、价值主张 |
| 5. 商业模式 | 1-2 | 如何赚钱、收入来源、定价策略 |
| 6. 市场规模 | 1 | TAM/SAM/SOM酒店采购TAM 2000亿SOM 100亿 |
| 7. 竞争分析 | 1 | 竞争格局、差异化:软件+供应链双轮 |
| 8. 运营数据 | 1-2 | GMV、客户数、复购率、增长曲线 |
| 9. 团队介绍 | 1 | 核心成员、背景优势 |
| 10. 融资计划 | 1 | 金额、估值、用途研发40%/市场30%/运营20%/储备10% |
### 商业模式收入模型
| 收入来源 | 说明 |
|----------|------|
| 交易佣金 | 供应链交易抽佣 (3-8%) |
| SaaS 订阅费 | 系统使用年费 |
| 金融服务 | 供应链金融分润 |
| 增值服务 | 数据服务、营销服务 |
**Unit Economics**CAC / LTV健康值 LTV/CAC >3/ 毛利率 / 回本周期
### PPT 输出
生成路演 PPT 时,输出为单个 HTML 文件(全屏演示):
- 支持键盘翻页(← →、F 全屏、触摸滑动
- 配色:深色科技风 `#1a1a2e` 背景 + `#667eea` 主色
- 数据卡片 + 两栏布局 + 流程图组件
- 保存到桌面后 `open ~/Desktop/bp-presentation.html`
**PPT 设计原则**:一页一重点 / 大字标题48px+正文24px+/ 数据可视化 / 留白充足
---
## 二、商务合同
### 用户公司信息
| 公司简称 | 公司全称 | 纳税人识别号 | 地址 | 联系电话 | 法定代表人 |
|----------|----------|--------------|------|----------|------------|
| 智慧云彩 | 北京智慧云彩电子商务科技有限公司 | 91110114MA004M80XX | 北京市海淀区丰智东路13号院1号楼1层101 | - | - |
| 欢乐宿 | 北京欢乐宿供应链科技有限公司 | 91110113MACWJKYU8P | 北京市顺义区军营南街10号院3幢3层327室 | - | - |
| 对丝 | 北京对丝信息技术有限公司 | 91110113MAE4XXHR5E | 北京市顺义区军营南街10号院3幢3层3216室 | - | - |
| 妗晨 | 重庆妗晨工贸有限公司 | 91500104MA7EJTPA6D | 重庆市大渡口区跳磴镇海康路106号1-1 | 15213397998 | - |
| 名风 | 北京名风新能源科技有限公司 | 91110106092440790K | 北京市密云区西田各庄镇雁密路99号601室-3509集群注册 | - | 魏小健 |
| 鸿侨 | 沈阳鸿侨物流有限公司 | 91210113MADBXPU93P | 辽宁省沈阳市沈北新区蒲河路49-2号1-29-2 | 15567339320 | 禚子乔 |
### 开户信息
| 公司简称 | 开户银行 | 银行账号 |
|----------|----------|----------|
| 智慧云彩 | 招商银行股份有限公司北京立水桥支行 | 110922001210512 |
| 欢乐宿 | 中国工商银行北京南十里支行 | 0200206709200036024 |
| 对丝 | 招商银行股份有限公司北京顺义支行 | 110961225610001 |
| 妗晨 | 中国农业银行股份有限公司重庆大渡口天安支行 | 31230401040004386 |
| 名风 | 中国建设银行北京黄亦路支行 | 11001181700052501668 |
| 鸿侨 | 中国建设银行股份有限公司沈阳辉山支行 | 21050149004200001104 |
### 支持的合同类型
| 类型 | 典型场景 | 税率 |
|------|----------|------|
| 软件订阅合同 (SaaS) | 云服务、在线系统 | 6% |
| 软件定制开发合同 | 项目制开发 | 6% |
| 物流服务合同 | 货运、仓储 | **9%**(交通运输服务) |
| 销售服务合同 | 商品买卖 | 13% |
| 贸易合同 | 批发、进出口 | 13% |
| 技术服务合同 | IT 咨询 | 6% |
| 保密协议 (NDA) | 合作前签署 | - |
### 合同模板速查
#### SaaS 软件订阅合同核心条款
```
第一条 软件服务内容名称、模块、账号数、SaaS云服务模式
第二条 服务期限起止日期、到期前30日未异议自动续期
第三条 服务费用与支付(月付/年付标准、付款时间、付款账户)
第四条 甲方义务年可用性≥99.5%、技术支持、数据保密、逾期可暂停服务)
第五条 乙方义务(按时付款、禁止转让账号)
第六条 数据安全与保密保密义务合同终止后2年继续有效
第七条 违约责任(违约金=合同总额10%;服务中断>24小时按日退费
第八条 合同变更与解除(乙方提前解约已付费不退;甲方提前解约退剩余)
第九条 争议解决(甲方所在地法院)
```
#### 软件定制开发合同核心条款
```
第一条 项目内容(名称、开发内容/需求规格说明书、技术要求、交付物清单)
第二条 开发周期(总工期、里程碑:需求确认→设计→开发→测试→验收)
第三条 付款节点签约30%→需求确认30%→验收通过40%
第四条 验收标准收通知后10个工作日内逾期未验收视为通过
第五条 知识产权(归甲方/乙方/共有)
第六条 质保与维护免费质保期12个月
第七条 违约责任逾期交付合同额0.5‰/日逾期付款应付额0.5‰/日)
```
#### 物流服务合同核心条款
```
税率9%(交通运输服务增值税专用发票)
第一条 运输服务内容(整车/零担/仓储配送、路线、货物类型)
第二条 服务期限
第三条 运费与结算(计费方式、月结/单结、付款期限)
第四条 发票与税务9%增值税专用发票;开票时间:付款前)
第五条/六条 甲乙方权利义务
第七条 货损赔偿实际价值赔偿最高不超过运费N倍当场提出异议
```
**签章格式**(推荐独立区块式):
```
【甲 方】
单位名称:
法定代表人(或授权代表): (签字/盖章)
单位盖章:
签订日期: 年 月 日
─────────────────────────────────────────────
【乙 方】
...
```
### 合同要素检查清单
- [ ] 合同编号、标题、签订日期
- [ ] 双方全称、统一社会信用代码、地址、法人
- [ ] 标的物/服务内容(明确具体)
- [ ] 价款、支付方式与时间
- [ ] 履行期限/交付方式
- [ ] 验收标准、质保条款
- [ ] 违约责任(违约金比例)
- [ ] 争议解决方式
- [ ] 不可抗力、保密条款
- [ ] 合同份数、生效条件
### 文档存储
| 平台 | 路径/Token |
|------|------------|
| 飞书云文档 | ai-proj/01运营 (`C80gfkRnzlonQ5d4AhOcOACDnNg`) |
| 思源笔记 | 商务合同笔记本 ID: `20260202080313-kjtgg1j` |
思源路径规范:`/{合同类型}/{甲方简称}-{乙方简称}-{年份}`
工作流:合同技能生成内容 → 思源笔记(校对)→ 飞书云文档(正式发布)
---
## 注意事项
1. BP 数据必须真实可查证,敏感数据可用 [X] 占位
2. 合同金额大写与小写必须一致
3. **本技能生成的合同仅供参考**,重要合同建议经法务/律师审核
4. 公司信息确认无误后再签署

View File

@@ -0,0 +1,8 @@
{
"name": "biz-plan-plugin",
"description": "Plugin for biz-plan",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,760 @@
---
name: biz-plan
description: 商业计划书撰写。支持融资BP、战略规划、商业模式设计等。当用户提到商业计划书、BP、融资计划、商业模式、战略规划相关任务时自动激活。
---
# 商业计划书撰写技能 (biz-plan)
## 概述
本技能用于辅助商业计划书的撰写与优化,特别适用于 B2B SaaS、供应链平台、产业互联网等领域。
---
## 公司背景(可定制)
### 核心信息
| 项目 | 描述 |
|------|------|
| 公司定位 | 酒店供应链系统研发公司 |
| 发展阶段 | 多轮融资,成熟期 |
| 核心能力 | 软件系统研发 + 供应链运营经验 |
| 资源优势 | 头部酒店集团资源 |
### 商业模式
```
┌─────────────────────────────────────────────────────────────┐
│ 平台商业模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ 换房额度 ┌──────────────┐ │
│ │ 酒店端 │ ──────────────────→ │ 供应链平台 │ │
│ │ │ ←────────────────── │ (集团总部) │ │
│ └──────────┘ 采购消费 └──────────────┘ │
│ │ │ │
│ │ 房间资源 │ 商品/服务 │
│ ↓ ↓ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ 换房用户 │ │ 供应商 │ │
│ └──────────┘ └──────────────┘ │
│ │
│ 收入来源: 供应链交易服务佣金 │
│ 核心价值: 软件系统增强上下游粘性 │
└─────────────────────────────────────────────────────────────┘
```
---
## 商业计划书结构
### 标准融资 BP 结构10-15页
| 章节 | 页数 | 核心内容 |
|------|------|----------|
| 1. 封面 | 1 | 公司名称、Slogan、联系方式 |
| 2. 公司简介 | 1 | 一句话定位、发展历程、里程碑 |
| 3. 痛点与机会 | 1-2 | 行业痛点、市场机会、为什么是现在 |
| 4. 解决方案 | 2 | 产品/服务介绍、核心功能、价值主张 |
| 5. 商业模式 | 1-2 | 如何赚钱、收入来源、定价策略 |
| 6. 市场规模 | 1 | TAM/SAM/SOM、增长趋势 |
| 7. 竞争分析 | 1 | 竞争格局、差异化优势 |
| 8. 运营数据 | 1-2 | 关键指标、增长曲线、客户案例 |
| 9. 团队介绍 | 1 | 核心成员、背景优势 |
| 10. 融资计划 | 1 | 融资金额、估值、资金用途 |
---
## 各章节撰写指南
### 1. 封面
```
[公司 LOGO]
公司名称
——————————————
一句话 Slogan不超过20字
联系人XXX
电话XXX
邮箱XXX
[日期]
[保密声明]
```
### 2. 公司简介
**一句话定位模板**
> 我们是 [行业] 领域的 [产品类型],通过 [核心能力],帮助 [目标客户] 实现 [核心价值]。
**示例**
> 我们是酒店供应链领域的产业互联网平台,通过 SaaS 系统 + 供应链服务,帮助酒店集团降低采购成本、提升运营效率。
**发展历程**
| 时间 | 里程碑 |
|------|--------|
| 20XX年 | 公司成立,获得天使轮融资 |
| 20XX年 | 产品上线,首批客户签约 |
| 20XX年 | A轮融资GMV突破X亿 |
| 20XX年 | 签约X家头部酒店集团 |
### 3. 痛点与机会
**痛点分析框架**
```
行业痛点
├── 酒店端痛点
│ ├── 采购成本高、供应商分散
│ ├── 库存管理粗放
│ └── 缺乏数字化工具
├── 供应商端痛点
│ ├── 获客成本高
│ ├── 账期长、现金流压力
│ └── 缺乏稳定渠道
└── 行业痛点
├── 信息不对称
├── 交易效率低
└── 缺乏标准化
```
**市场机会**
- 政策利好:数字化转型、供应链金融政策
- 技术成熟SaaS、移动互联网、大数据
- 行业变革:酒店行业整合、集团化趋势
### 4. 解决方案
**产品矩阵**
| 产品 | 目标用户 | 核心功能 | 价值 |
|------|----------|----------|------|
| 酒店采购系统 | 酒店采购部 | 在线下单、供应商管理、成本分析 | 降本30% |
| 供应商平台 | 供应商 | 商品管理、订单处理、对账结算 | 获客成本降低50% |
| 集团管控系统 | 酒店集团 | 集采管理、数据分析、合规审计 | 管理效率提升 |
| 换房服务平台 | 企业/个人 | 房间预订、积分兑换 | 差旅成本优化 |
**技术架构**(简化版):
```
┌─────────────────────────────────────┐
│ 业务中台 │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │订单 │ │商品 │ │用户 │ │结算 │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
├─────────────────────────────────────┤
│ 数据中台 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │BI │ │算法 │ │数仓 │ │
│ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────┘
```
### 5. 商业模式
**收入模型**
| 收入来源 | 说明 | 占比 |
|----------|------|------|
| 交易佣金 | 供应链交易抽佣 (X%) | XX% |
| SaaS 订阅费 | 系统使用年费 | XX% |
| 金融服务 | 供应链金融分润 | XX% |
| 增值服务 | 数据服务、营销服务 | XX% |
**单位经济模型**Unit Economics
| 指标 | 数值 | 说明 |
|------|------|------|
| CAC | ¥XXX | 获客成本 |
| LTV | ¥XXX | 客户生命周期价值 |
| LTV/CAC | X.X | 健康值 >3 |
| 毛利率 | XX% | |
| 回本周期 | X个月 | |
### 6. 市场规模
**TAM/SAM/SOM 分析**
```
┌────────────────────────────────────────┐
│ TAM: XXX亿 │
│ (Total Addressable Market) │
│ ┌────────────────────────────┐ │
│ │ SAM: XXX亿 │ │
│ │ (Serviceable Addressable) │ │
│ │ ┌──────────────────┐ │ │
│ │ │ SOM: XXX亿 │ │ │
│ │ │ (Serviceable │ │ │
│ │ │ Obtainable) │ │ │
│ │ └──────────────────┘ │ │
│ └────────────────────────────┘ │
└────────────────────────────────────────┘
```
**酒店供应链市场规模**(示例):
- 中国酒店行业规模:约 6000 亿
- 酒店采购市场TAM约 2000 亿
- 连锁/集团酒店采购SAM约 800 亿
- 可触达市场SOM约 100 亿
### 7. 竞争分析
**竞争格局**
| 类型 | 代表企业 | 优势 | 劣势 |
|------|----------|------|------|
| 传统供应商 | XX、YY | 客户关系、线下服务 | 数字化能力弱 |
| SaaS 厂商 | AA、BB | 技术能力强 | 缺乏供应链能力 |
| 互联网平台 | CC、DD | 流量、资金 | 行业理解不深 |
| **我们** | - | 软件+供应链双轮驱动 | - |
**差异化优势**
1. **行业 Know-how**:多年酒店供应链经验
2. **头部资源**XX家头部集团客户
3. **创新模式**:换房模式增强粘性
4. **技术壁垒**:自研系统,快速迭代
### 8. 运营数据
**关键指标**
| 指标 | 2023 | 2024 | 2025E | YoY增长 |
|------|------|------|-------|---------|
| GMV亿 | X | X | X | XX% |
| 收入(万) | X | X | X | XX% |
| 客户数 | X | X | X | XX% |
| 复购率 | X% | X% | X% | - |
**增长曲线**(用文字描述,实际 BP 用图表):
- 2022-2023产品打磨期种子客户验证
- 2024规模扩张期客户数 X 倍增长
- 2025加速增长期目标 GMV XX亿
**标杆客户案例**
> **XX酒店集团**X00家酒店
> - 合作前:采购分散,年采购成本 X 亿
> - 合作后:统一平台采购,成本降低 XX%
> - 换房模式:年换房 X 万间夜,转化采购额 XX 万
### 9. 团队介绍
**核心团队**
| 姓名 | 职位 | 背景 |
|------|------|------|
| XXX | CEO | XX酒店集团XX年XX大学MBA |
| XXX | CTO | 原XX公司技术总监XX年技术经验 |
| XXX | COO | 原XX供应链公司XX年运营经验 |
| XXX | CFO | 原XX投资XX年财务经验 |
**团队优势**
- 酒店行业背景:平均 X 年行业经验
- 技术能力XX 人研发团队
- 资源网络:覆盖 XX% 头部酒店集团
### 10. 融资计划
**本轮融资**
| 项目 | 内容 |
|------|------|
| 融资轮次 | X 轮 |
| 融资金额 | 人民币 XXX 万元 |
| 出让股份 | XX% |
| 投前估值 | 人民币 XXX 万元 |
| 资金用途 | 见下表 |
**资金用途**
| 用途 | 占比 | 说明 |
|------|------|------|
| 产品研发 | 40% | 核心系统升级、AI 能力建设 |
| 市场拓展 | 30% | 销售团队扩张、客户获取 |
| 运营投入 | 20% | 供应链能力建设、服务体系 |
| 储备资金 | 10% | 现金流储备 |
**里程碑规划**
| 时间 | 目标 |
|------|------|
| 融资后6个月 | 客户数达到 X 家GMV XX亿 |
| 融资后12个月 | 新产品上线,进入 X 个新区域 |
| 融资后18个月 | 实现盈亏平衡 |
---
## 行业专项模板
### 酒店供应链 BP 要点
**换房模式说明**
```
换房模式运作逻辑
─────────────────────────────────────────
1. 酒店提供房间资源
酒店 → 平台承诺年度换房额度如100万间夜
2. 平台转化为采购额度
换房价值 → 按比例转化为供应链采购额度
3. 酒店在平台消费
酒店使用额度 → 在供应链平台采购商品/服务
4. 平台获取佣金
供应链交易 → 平台抽取 X% 服务佣金
核心价值:
✓ 酒店:用闲置资源换取采购成本优化
✓ 平台:获得稳定交易额 + 服务佣金
✓ 供应商:获得优质渠道 + 稳定订单
```
### 供应链平台指标体系
| 指标类别 | 指标名称 | 说明 | 行业基准 |
|----------|----------|------|----------|
| 规模指标 | GMV | 平台交易总额 | - |
| | 活跃客户数 | 月活跃采购客户 | - |
| | SKU 数量 | 商品种类数 | - |
| 效率指标 | 订单转化率 | 询价到下单转化 | >15% |
| | 履约率 | 按时交付率 | >95% |
| | 客诉率 | 投诉订单占比 | <1% |
| 财务指标 | 佣金率 | 平台抽佣比例 | 3-8% |
| | 毛利率 | | 30-50% |
| | CAC Payback | 获客成本回收期 | <12月 |
---
## 使用说明
### 快速生成 BP
向我描述你的需求,我会帮你生成商业计划书内容:
**示例输入**
> 帮我写商业计划书第5章商业模式部分我们是酒店供应链平台主要收入来源是交易佣金和SaaS订阅费另外还有换房模式
**我会输出**
- 结构化的商业模式描述
- 收入模型分析
- 需要补充的数据清单
### BP 审核优化
你可以提供已有的 BP 内容让我帮你:
- 检查逻辑完整性
- 优化表达方式
- 补充行业数据
- 突出差异化优势
---
## 输出格式
### 支持的输出方式
| 方式 | 工具 | 说明 |
|------|------|------|
| Markdown | 直接输出 | 快速预览 |
| 飞书文档 | feishu skill | 在线协作 |
| Word/PDF | docx/pdf skill | 正式文件 |
| PPT | pptx skill | 路演演示 |
### 融资路演 PPT 结构
如需生成融资路演 PPT建议使用以下结构
1. 封面(公司名+Slogan
2. 问题(行业痛点)
3. 方案(产品价值)
4. 市场(规模机会)
5. 产品(功能展示)
6. 模式(如何赚钱)
7. 数据(业绩证明)
8. 竞争(差异优势)
9. 团队(核心成员)
10. 融资(计划用途)
---
## 注意事项
1. **数据真实性**BP 中的数据必须真实可查证
2. **保密性**:敏感数据可用 X 代替
3. **针对性**:根据投资人偏好调整侧重点
4. **简洁性**:每页一个核心信息点
5. **视觉化**:用图表代替大段文字
---
## 与其他技能协同
| 技能 | 协同场景 |
|------|----------|
| biz-contract | 融资后签署投资协议 |
| feishu | 将 BP 保存到飞书文档 |
| data-excel | 财务模型和数据分析 |
---
## PPT 设计能力
本技能支持生成商业计划书 PPT输出为 HTML 格式的演示文稿,可直接在浏览器中全屏演示,也可转换为 PDF 或截图使用。
### PPT 输出方式
当用户要求生成 PPT 时,使用以下方式:
1. **HTML 演示文稿**(推荐)
- 生成单个 HTML 文件
- 支持全屏演示(按 F 键)
- 支持键盘翻页(← →)
- 可直接打印为 PDF
- 响应式设计,适配各种屏幕
2. **Markdown 演示脚本**
- 输出每页内容的文字稿
- 用户可自行复制到 PPT 软件
### HTML PPT 模板
生成 PPT 时使用以下 HTML 模板结构:
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商业计划书 - [公司名称]</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
background: #1a1a2e;
color: #fff;
overflow: hidden;
}
.slide {
display: none;
width: 100vw;
height: 100vh;
padding: 60px 80px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
position: relative;
}
.slide.active {
display: flex;
flex-direction: column;
justify-content: center;
}
/* 封面样式 */
.slide.cover {
text-align: center;
justify-content: center;
align-items: center;
}
.slide.cover h1 {
font-size: 72px;
font-weight: 700;
margin-bottom: 20px;
background: linear-gradient(90deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.slide.cover .slogan {
font-size: 28px;
color: #a0aec0;
margin-bottom: 60px;
}
.slide.cover .info {
font-size: 18px;
color: #718096;
}
/* 内容页样式 */
.slide h2 {
font-size: 48px;
font-weight: 600;
margin-bottom: 40px;
color: #667eea;
}
.slide h3 {
font-size: 32px;
font-weight: 500;
margin-bottom: 24px;
color: #a0aec0;
}
.slide p, .slide li {
font-size: 24px;
line-height: 1.8;
color: #e2e8f0;
}
.slide ul {
list-style: none;
padding-left: 0;
}
.slide ul li {
padding: 12px 0;
padding-left: 40px;
position: relative;
}
.slide ul li::before {
content: "▸";
position: absolute;
left: 0;
color: #667eea;
}
/* 数据卡片 */
.metrics {
display: flex;
gap: 40px;
margin-top: 40px;
}
.metric-card {
flex: 1;
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 16px;
padding: 32px;
text-align: center;
}
.metric-card .value {
font-size: 48px;
font-weight: 700;
color: #667eea;
}
.metric-card .label {
font-size: 18px;
color: #a0aec0;
margin-top: 8px;
}
/* 两栏布局 */
.two-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
margin-top: 40px;
}
/* 流程图 */
.flow {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin-top: 40px;
}
.flow-item {
background: rgba(102, 126, 234, 0.15);
border-radius: 12px;
padding: 24px 32px;
text-align: center;
}
.flow-arrow {
color: #667eea;
font-size: 32px;
}
/* 页码 */
.page-number {
position: absolute;
bottom: 30px;
right: 40px;
font-size: 16px;
color: #4a5568;
}
/* 进度条 */
.progress {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s;
z-index: 100;
}
/* 导航提示 */
.nav-hint {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
color: #4a5568;
}
</style>
</head>
<body>
<div class="progress" id="progress"></div>
<!-- Slide 1: 封面 -->
<div class="slide cover active">
<h1>[公司名称]</h1>
<p class="slogan">[一句话 Slogan]</p>
<p class="info">[日期] · 商业计划书</p>
<span class="page-number">1 / N</span>
</div>
<!-- Slide 2: 公司定位 -->
<div class="slide">
<h2>公司定位</h2>
<h3>[一句话定位]</h3>
<div class="metrics">
<div class="metric-card">
<div class="value">[X]年</div>
<div class="label">深耕行业</div>
</div>
<div class="metric-card">
<div class="value">[X]家</div>
<div class="label">头部客户</div>
</div>
<div class="metric-card">
<div class="value">[X]轮</div>
<div class="label">完成融资</div>
</div>
</div>
<span class="page-number">2 / N</span>
</div>
<!-- 更多 slides... -->
<p class="nav-hint">← → 翻页 · F 全屏 · ESC 退出</p>
<script>
const slides = document.querySelectorAll('.slide');
const progress = document.getElementById('progress');
let current = 0;
function showSlide(n) {
slides[current].classList.remove('active');
current = (n + slides.length) % slides.length;
slides[current].classList.add('active');
progress.style.width = ((current + 1) / slides.length * 100) + '%';
}
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight' || e.key === ' ') showSlide(current + 1);
if (e.key === 'ArrowLeft') showSlide(current - 1);
if (e.key === 'f' || e.key === 'F') {
document.documentElement.requestFullscreen?.();
}
});
// 触摸滑动支持
let touchStartX = 0;
document.addEventListener('touchstart', e => touchStartX = e.touches[0].clientX);
document.addEventListener('touchend', e => {
const diff = e.changedTouches[0].clientX - touchStartX;
if (Math.abs(diff) > 50) showSlide(current + (diff > 0 ? -1 : 1));
});
</script>
</body>
</html>
```
### BP PPT 标准页面结构
生成商业计划书 PPT 时,按以下结构组织:
| 页码 | 页面类型 | 内容要点 | 设计建议 |
|------|----------|----------|----------|
| 1 | 封面 | 公司名、Slogan、日期 | 居中大标题,渐变背景 |
| 2 | 公司定位 | 一句话定位 + 核心数据 | 3-4 个数据卡片 |
| 3 | 行业痛点 | 3-4 个核心痛点 | 图标 + 简短描述 |
| 4 | 解决方案 | 产品价值主张 | 流程图或产品矩阵 |
| 5 | 商业模式 | 收入来源、盈利方式 | 模式图 + 收入占比 |
| 6 | 市场规模 | TAM/SAM/SOM | 同心圆图 + 数字 |
| 7 | 竞争分析 | 竞争矩阵、差异化 | 2x2 矩阵图 |
| 8 | 运营数据 | 核心指标、增长趋势 | 4 个大数字卡片 |
| 9 | 客户案例 | 标杆客户、成果 | Logo + 数据对比 |
| 10 | 团队介绍 | 核心成员背景 | 头像 + 简介 |
| 11 | 里程碑 | 发展历程 | 时间线 |
| 12 | 财务预测 | 3 年收入预测 | 柱状图示意 |
| 13 | 战略规划 | 未来规划 | 阶段性目标 |
| 14 | 融资计划 | 金额、用途、联系方式 | 饼图 + 联系信息 |
### PPT 设计原则
1. **一页一重点**:每页只传达一个核心信息
2. **大字少字**:标题 48px+,正文 24px+,每页文字不超过 50 字
3. **数据可视化**:用数字卡片、图表代替文字描述
4. **留白充足**:内容不要铺满,保持呼吸感
5. **配色统一**:使用 2-3 种主色,保持一致性
6. **高对比度**:深色背景 + 浅色文字,或浅色背景 + 深色文字
### 配色方案
**深色科技风**(推荐):
- 背景:#1a1a2e#16213e(渐变)
- 主色:#667eea(蓝紫色)
- 辅助:#764ba2(紫色)
- 文字:#e2e8f0(浅灰)
- 次要文字:#a0aec0(中灰)
**浅色商务风**
- 背景:#ffffff
- 主色:#2196F3(蓝色)
- 辅助:#1976D2(深蓝)
- 文字:#212121(深灰)
- 次要文字:#616161(中灰)
### 使用示例
用户请求:
> 帮我生成商业计划书 PPT
输出方式:
1. 生成完整的 HTML 文件
2. 保存到用户指定位置(如桌面、项目目录)
3. 用户用浏览器打开即可演示
```bash
# 打开 PPT
open ~/Desktop/bp-presentation.html
# 或在浏览器中打开后按 F 进入全屏模式
```
### 导出为 PDF
HTML PPT 可通过浏览器打印功能导出为 PDF
1. 在浏览器中打开 HTML 文件
2. 按 Cmd+P (Mac) 或 Ctrl+P (Windows)
3. 选择"另存为 PDF"
4. 设置页面大小为横向 A4 或 16:9
### 注意事项
1. **内容优先**:先确定 BP 内容,再生成 PPT
2. **数据占位**:敏感数据可用 [X] 占位
3. **迭代优化**:可分页生成,逐步完善
4. **本地演示**HTML 文件完全离线可用,无需网络

View File

@@ -0,0 +1,8 @@
{
"name": "coolbuy-legacy-plugin",
"description": "酷采2.0团购管理系统测试与维护。用于酷采2.0系统的功能测试、问题排查、需求验证和对比测试。",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,414 @@
---
name: coolbuy-legacy
description: 酷采2.0团购管理系统测试与维护。用于酷采2.0系统的功能测试、问题排查、需求验证和对比测试。当用户提到酷采2.0、百丽、李宁、遗留系统测试相关任务时自动激活。
---
# Coolbuy Legacy (酷采2.0) Skill
酷采2.0团购管理系统,服务于百丽、李宁等客户的遗留系统,采用 Vue 2 + Element UI + Java Spring 技术栈。
## 项目信息
| 项目 | 值 |
|------|-----|
| 项目编号 | P264 |
| 项目名称 | 酷采 2.0 |
| AI-Proj 项目ID | 164 |
| 源码路径 | `/Users/donglinlai/workspace/coolbuy-legacy` |
| Git 仓库 | `git@gitea.pipexerp.com:pipexerp/coolbuy-legacy.git` |
| 主分支 | main |
| 技术栈 | Vue 2 + Element UI + Java Spring |
---
## 系统访问
### 测试环境
| 项目 | 值 |
|------|-----|
| 测试地址 | http://47.105.185.154:9300/login |
| 管理员账号 | 19090009801 |
| 密码 | 123456 |
| 客户账号 | 17761202551 / 202551 |
| 服务器 | 47.105.185.154 |
### 主要客户
- **百丽集团** - 大型鞋业零售集团
- **李宁体育** - 知名体育用品品牌
---
## 架构概览
```
coolbuy-legacy/
├── cool_lining/module-provider/ # Java 后端服务
│ └── src/main/java/com/jzg/module/
│ ├── action/ # 控制器层
│ │ ├── prd/ # 商品模块控制器
│ │ ├── order/ # 订单模块控制器
│ │ └── customer/ # 客户模块控制器
│ ├── dao/model/ # 数据模型
│ │ ├── prd/ # 商品实体
│ │ ├── order/ # 订单实体
│ │ └── customer/ # 客户实体
│ └── manager/ # 业务逻辑层
│ ├── prd/ # 商品业务逻辑
│ ├── order/ # 订单业务逻辑
│ └── customer/ # 客户业务逻辑
└── ln_admin/ # Vue 2 前端
└── src/views2/module/ # 业务模块页面
├── prd/ # 商品管理页面
├── order/ # 订单管理页面
└── customer/ # 客户管理页面
```
---
## 主要功能模块
### 核心业务模块
1. **推广方案管理**
- 促销活动配置
- 折扣规则设置
- 活动效果统计
2. **销售管理**
- 订单处理流程
- 销售数据统计
- 客户下单管理
3. **草稿单管理**
- 未完成订单保存
- 草稿单编辑
- 批量转正式单
4. **Y码直客**
- 直客订单管理
- Y码生成与核销
- 直客价格体系
5. **库存管理**
- 库存查询
- 库存调拨
- 库存预警
6. **货盘管理**
- 货盘创建
- 货盘分配
- 货盘跟踪
7. **协同仓管理**
- 多仓协同
- 仓库调度
- 发货管理
8. **价格管理**
- 商品定价
- 客户价格体系
- 最低折扣限制
9. **资金管理**
- 账户余额
- 充值记录
- 消费明细
10. **产品管理**
- 商品信息维护
- SPU/SKU管理
- 商品分类
11. **基础功能**
- 客户管理
- 用户权限
- 系统配置
12. **数据看板**
- 销售数据分析
- 库存报表
- 业务概览
13. **公告通知**
- 系统公告
- 消息推送
- 通知管理
14. **起售数量设置**
- 最小起售量
- 批量设置
- 规则配置
---
## 与酷采3.0的对比
### 技术栈差异
| 项目 | 酷采2.0 (Legacy) | 酷采3.0 (PaaS) |
|------|------------------|----------------|
| 前端框架 | Vue 2 | React 18 |
| UI组件库 | Element UI | Ant Design |
| 前端构建 | Webpack | Vite |
| 后端语言 | Java | Go |
| 后端框架 | Spring Boot | Gin + go-zero |
| 数据库 | MySQL | PostgreSQL |
| 架构模式 | 单体应用 | 微服务 |
| 部署方式 | 传统部署 | Docker + K8s |
### 业务差异
| 功能 | 酷采2.0 | 酷采3.0 |
|------|---------|---------|
| 多租户 | ❌ 单租户 | ✅ 多租户 SaaS |
| 客户隔离 | 账号级别 | 企业级别 |
| 定制化 | 客户专属部署 | 配置化租户 |
| 扩展性 | 垂直扩展 | 水平扩展 |
---
## 测试任务管理
### AI-Proj 项目集成
当前项目在 AI-Proj 系统中的ID为 **164**,包含以下测试任务:
#### 客户最低折扣申请限制功能测试
**父任务**: #4725 (in_progress)
**测试用例**:
- ✅ TC001: 按客户类型配置最低折扣 [P0] - #4726
- ✅ TC002: 按特定客户配置最低折扣 [P0] - #4727
- ✅ TC003: 折扣下限输入验证 [P1] - #4728
- ✅ TC004: 阈值导入模板 [P1] - #4729
- ✅ TC005: 折扣低于下限 - 拦截 [P0] - #4730
- ✅ TC006: 折扣等于下限 - 通过 [P0] - #4731
- ✅ TC007: 折扣高于下限 - 通过 [P1] - #4732
- 🔄 TC008: 多商品触发限制 - 罗列提示 [P0] - #4733
- ⏳ TC009: 非一级账号不受限 [P1] - #4734
- ⏳ TC010: 审核修改折扣低于下限 - 拦截 [P0] - #4735
- ⏳ TC011: 审核修改折扣等于下限 - 通过 [P0] - #4736
- ⏳ TC012: 限价标签显示 [P1] - #4737
- ⏳ TC013: 提示文字验证 [P1] - #4738
### 查询任务
```javascript
// 获取酷采2.0项目任务列表
mcp__ai-proj__list_tasks({ projectId: 164 })
// 获取特定任务详情
mcp__ai-proj__get_detailed_task_info({ taskId: 4725 })
// 获取任务文档
mcp__ai-proj__get_task_document({ taskId: 4725 })
```
---
## Chrome DevTools 浏览器自动化
### 启动调试模式
```bash
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-debug
```
### 验证连接
```bash
curl http://127.0.0.1:9222/json/version
```
### 验证码处理规则
**重要**: 当访问需要验证码的系统时:
1. **不要**使用脚本截图方式获取验证码(验证码会快速过期)
2. **直接提醒用户**
- 请在浏览器中输入验证码
- 或请帮我点击登录按钮
3. 用户操作完成后再继续自动化流程
示例提示:
```
浏览器已打开登录页面,验证码需要手动输入。请在 Chrome 浏览器中:
1. 输入验证码
2. 点击登录按钮
完成后告诉我,我将继续后续操作。
```
### 常用 MCP 操作
```javascript
// 列出所有页面
mcp__chrome-devtools__list_pages()
// 选择页面
mcp__chrome-devtools__select_page({ pageId: 1 })
// 导航到URL
mcp__chrome-devtools__navigate_page({
type: "url",
url: "http://47.105.185.154:9300/login"
})
// 截图
mcp__chrome-devtools__take_screenshot({
format: "png",
filePath: "/tmp/screenshot.png"
})
// 获取页面快照
mcp__chrome-devtools__take_snapshot()
// 点击元素
mcp__chrome-devtools__click({ uid: "element_uid" })
// 填写表单
mcp__chrome-devtools__fill({
uid: "input_uid",
value: "text"
})
// 执行JavaScript
mcp__chrome-devtools__evaluate_script({
function: "() => { return document.title; }"
})
```
---
## 常见测试场景
### 1. 客户最低折扣测试
**测试步骤**:
1. 登录管理员账号 (19090009801)
2. 进入价格管理 → 最低折扣配置
3. 配置客户类型或特定客户的最低折扣
4. 使用客户账号 (17761202551) 登录
5. 创建订单,测试折扣限制规则
**验证点**:
- 折扣低于下限时系统拦截
- 折扣等于或高于下限时通过
- 多商品触发时正确罗列提示
- 非一级账号不受限制
### 2. 订单流程测试
**测试步骤**:
1. 客户账号登录
2. 选择商品加入购物车
3. 提交订单
4. 审核订单 (管理员)
5. 发货处理
6. 订单完成
### 3. 库存管理测试
**测试步骤**:
1. 查询库存
2. 创建库存调拨单
3. 审核调拨单
4. 确认入库
5. 验证库存变化
---
## 需求对比测试流程
当酷采3.0实现新功能时需要与酷采2.0进行对比:
### 测试流程
1. **功能分析**
- 在酷采2.0中找到对应功能
- 记录现有实现方式
- 识别差异点
2. **页面对比**
- 截图酷采2.0界面
- 对比UI/UX差异
- 记录交互流程
3. **数据对比**
- 对比数据模型
- 验证业务规则
- 确认边界条件
4. **性能对比**
- 记录响应时间
- 对比并发能力
- 评估用户体验
---
## 问题排查
### 常见问题
1. **登录失败**
- 检查账号密码是否正确
- 验证码是否过期
- 网络连接是否正常
2. **页面加载慢**
- 检查网络状况
- 清除浏览器缓存
- 查看服务器日志
3. **数据不同步**
- 刷新页面
- 检查数据库连接
- 查看后端日志
### 日志查看
```bash
# SSH到测试服务器
ssh root@47.105.185.154
# 查看应用日志
tail -f /path/to/coolbuy/logs/application.log
# 查看错误日志
tail -f /path/to/coolbuy/logs/error.log
```
---
## 相关技能
- `coolbuy-paas` - 酷采3.0 SaaS租户端开发
- `coolbuy-platform` - 酷采3.0平台管理端
- `dev-test` - 软件测试技能
- `req` - 需求管理技能
- `siyuan` - 思源笔记(含酷采相关文档)
---
## 版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.0.0 | 2026-01-21 | 初始版本创建酷采2.0独立技能 |
---
## 注意事项
⚠️ **重要提醒**:
1. 酷采2.0为遗留系统,主要用于参考和对比测试
2. 新功能开发应在酷采3.0 (coolbuy-paas) 中进行
3. 测试环境数据仅供测试使用,请勿在生产环境操作
4. 百丽、李宁等客户仍在使用此系统,测试时注意不要影响生产数据
5. 发现问题及时记录到 AI-Proj 系统中

View File

@@ -0,0 +1,8 @@
{
"name": "coolbuy-paas-plugin",
"description": "酷采3.0 SaaS 租户端开发与测试。用于商品管理、订单管理等业务模块开发以及酷采2.0系统对比测试。",
"version": "1.3.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,803 @@
---
name: coolbuy-paas
description: 酷采3.0 SaaS 租户端开发与测试。用于商品管理、订单管理等业务模块开发以及酷采2.0系统对比测试。当用户提到酷采、coolbuy-paas、商品管理、产品管理、酷采2.0测试相关任务时自动激活。
---
# Coolbuy PaaS Skill
酷采3.0 SaaS 多租户业务系统,采用 Go + React 技术栈。
## 项目信息
| 项目 | 值 |
|------|-----|
| 本地路径 | `/Users/donglinlai/coding/qiudl/coolbuy-paas` |
| 工作区路径 | `/Users/donglinlai/workspace/coolbuy-paas` |
| Git 仓库 | `git@gitea.pipexerp.com:pipexerp/coolbuy-paas.git` |
| 主分支 | main |
| 技术栈 | Go (Gin+GORM) + React 18 (Vite+TypeScript+Ant Design) |
---
## 架构概览
```
coolbuy-paas/
├── erp-service/ # Go 后端 - ERP业务服务 (port 7091)
│ ├── api/ # API 定义 (go-zero)
│ ├── internal/
│ │ ├── product/ # 商品模块
│ │ ├── order/ # 订单模块
│ │ └── inventory/ # 库存模块
│ └── configs/
├── foundation-service/ # 基础服务 (port 7090)
├── auth-service/ # 认证服务 (port 7089)
├── ai-service/ # AI 服务 (port 7092) - AI Chat, Tool Calling
│ ├── api/
│ │ └── etc/ # 配置文件 (多环境)
│ └── services/
│ ├── chat_service.go # 对话服务
│ └── ai_ticket_reporter.go # AI 工单上报
├── gateway/ # API 网关
├── web/ # React 前端 - 租户端 (port 4000)
│ ├── src/
│ │ ├── services/ # API 服务
│ │ │ ├── aiChatApi.ts
│ │ │ └── chatSessionService.ts
│ │ └── components/AIChat/ # AI 聊天组件
│ └── .env.* # 环境配置
└── web-mall/ # React 前端 - 独立商城 (port 5174)
├── src/
│ ├── pages/ # 首页/分类/商品/购物车/结算/个人中心
│ ├── api/ # API 层(共享 erp-service
│ ├── auth/ # 认证(共享 auth-service JWT
│ ├── store/ # Zustand 状态管理
│ └── layouts/ # 商城布局(顶部导航,非侧边栏)
├── nginx.conf # 生产 Nginx 配置
└── Dockerfile # 多阶段构建
```
---
## 多环境配置
### 运行环境
| 环境 | 前端配置 | 后端配置 | 说明 |
|------|---------|---------|------|
| development | `.env.development` | `ai-api.yaml` | 本地开发 |
| staging | `.env.staging` | `ai-api.staging.yaml` | 预发布环境 |
| production | `.env.production` | `ai-api.production.yaml` | 生产环境 |
### 核心环境变量
| 变量 | 说明 | 开发环境示例 |
|------|------|------------|
| `VITE_AUTH_SERVICE_URL` | 认证服务 | `http://127.0.0.1:7089` |
| `VITE_FOUNDATION_SERVICE_URL` | 基础服务 | `http://127.0.0.1:7090` |
| `VITE_BUSINESS_SERVICE_URL` | ERP服务 | `http://127.0.0.1:7091` |
| `VITE_AI_SERVICE_URL` | AI服务 | `http://127.0.0.1:7092` |
| `VITE_AI_PROJ_URL` | ai-proj服务 | `http://127.0.0.1:8080` |
### Vite 代理配置 (vite.config.ts)
```typescript
proxy: {
'/api/v1/auth': { target: VITE_AUTH_SERVICE_URL },
'/api/v1/ai': { target: VITE_AI_SERVICE_URL }, // AI Chat
'/api/v1/chat': { target: VITE_AI_PROJ_URL }, // Chat Sessions
'/api/v1': { target: VITE_BUSINESS_SERVICE_URL }, // 默认路由
}
```
---
## AI 服务集成
### AI Chat 功能
| 端点 | 服务 | 说明 |
|------|------|------|
| `/api/v1/ai/chat/stream` | ai-service (7092) | 流式对话 |
| `/api/v1/ai/tools` | ai-service (7092) | 获取可用工具 |
| `/api/v1/chat/sessions` | ai-proj (8080) | 会话管理 |
| `/api/v1/ai-tickets` | ai-proj (8080) | AI 工单 |
### AI Chat 代码路径(重要)
**前端实际调用链**
```
useChat.ts
→ import * as aiChatService from '@/services/aiChatApi' ← 实际使用的文件!
→ aiChatApi.sendChatMessageStream()
→ aiChatApi.sendStreamingChatRequest() ← 用 fetch() 直接发 SSE 请求
```
**注意**`@/services/aiChatService.ts` 是遗留文件,**未被使用**。修改 AI Chat 前端逻辑时必须修改 `aiChatApi.ts`
**流式请求绕过 axios**`aiChatApi.ts` 使用原生 `fetch()` 发送 SSE 请求,不经过 `request.ts` 的 axios 拦截器。因此 `Authorization``X-Tenant-ID` 等 header 需要在 `fetch()` 中手动添加。
### AI Chat 认证链路
```
前端登录 → auth-service 返回 JWT (含 tenant_id, username, roles)
→ localStorage 存储 user_info + token
→ AI Chat 发送请求时:
1. getToken() 获取 JWT
2. getTenantIdFromContext() 获取 tenant_id
3. fetch() 携带 Authorization + X-Tenant-ID header
→ ai-service auth middleware 解析 JWT claims
→ Go context 注入 user_id, username, tenant_id, tenant_code, roles
→ buildUserIdentityContext(ctx) 提取并追加到 system prompt
→ AI 能回答"我的租户ID是什么"等问题
```
**关键文件**
| 文件 | 作用 |
|------|------|
| `web/src/services/aiChatApi.ts` | 前端 AI Chat API实际使用 |
| `web/src/hooks/useChat.ts` | Chat 状态管理 Hook |
| `ai-service/api/internal/middleware/auth_middleware.go` | JWT 解析,注入 context |
| `ai-service/services/chat_service.go` | 对话服务system prompt 注入 |
| `ai-service/services/context_builder.go` | 用户上下文构建 |
**JWT 密钥一致性**auth-service 和 ai-service 的 `JWT.AccessSecret` 必须一致,否则 ai-service 无法验证 token开发环境有 flexible parse 兜底)。
### AI 工单上报
ai-service 的错误会自动上报到 ai-proj 的 AI 工单系统:
```yaml
# ai-service/api/etc/ai-api.yaml
AITicketReporter:
Enabled: true
AiProjURL: "http://localhost:8080"
```
错误类型自动分类:
- `api_error` - API 调用错误
- `missing_tool` - 工具调用错误
- `timeout` - 超时错误
- `permission_denied` - 权限错误
---
## 酷采2.0 测试环境 (Legacy)
用于参考和对比酷采3.0开发。
### 访问信息
| 项目 | 值 |
|------|-----|
| 测试地址 | http://47.105.185.154:9300/login |
| 管理员账号 | 19090009801 |
| 密码 | 123456 |
| 客户账号 | 17761202551 / 202551 |
| 技术栈 | Vue 2 + Element UI + Java Spring |
### 主要功能模块
- 推广方案、销售管理、草稿单管理
- Y码直客、库存管理、货盘管理
- 协同仓、价格管理、资金管理
- 基础功能、数据看板、公告通知
- 起售数量设置、**产品管理**
### 源码位置
```
~/workspace/coolbuy-legacy/
├── cool_lining/module-provider/ # Java 后端
│ └── src/main/java/com/jzg/module/
│ ├── action/prd/ # 商品控制器
│ ├── dao/model/prd/ # 商品实体
│ └── manager/prd/ # 商品业务逻辑
└── ln_admin/ # Vue 前端
└── src/views2/module/prd/ # 商品管理页面
```
---
## Chrome DevTools 浏览器自动化
用于系统测试、截图、UI验证。
### 启动 Chrome 调试模式
```bash
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-debug
```
### 验证连接
```bash
curl http://127.0.0.1:9222/json/version
```
### 验证码处理规则
**重要**: 当访问需要验证码的系统时:
1. **不要**使用脚本截图方式获取验证码(验证码会快速过期)
2. **直接提醒用户**
- 请在浏览器中输入验证码
- 或请帮我点击登录按钮
3. 用户操作完成后再继续自动化流程
示例提示:
```
浏览器已打开登录页面,验证码需要手动输入。请在 Chrome 浏览器中:
1. 输入验证码
2. 点击登录按钮
完成后告诉我,我将继续后续操作。
```
### 常用 CDP 操作
```javascript
// 截图
Page.captureScreenshot({ format: 'png' })
// 导航
Page.navigate({ url: 'http://...' })
// 执行 JS
Runtime.evaluate({ expression: '...' })
// 点击元素
Runtime.evaluate({
expression: `document.querySelector('button').click()`
})
```
---
## 酷采3.0 PRD 参考
PRD 文档存储在 ai-proj 任务系统:
| 模块 | 任务ID | 文档名称 |
|------|--------|----------|
| 商品管理 | #4754 | 酷采3.0商品管理模块 |
| 商品基本信息 | #4770 | 商品基本信息管理模块PRD |
获取 PRD 文档:
```
mcp__ai-proj__get_task_document(taskId: 4770)
```
---
## 本地开发
### 启动服务
```bash
# ERP 服务 (port 7091)
cd erp-service && go run api/main.go -f configs/config.local.yaml
# AI 服务 (port 7092)
cd ai-service && go run api/ai.go -f api/etc/ai-api.yaml
# 租户端前端 (port 4000)
cd web && npm run dev
# 独立商城前端 (port 5174)
cd web-mall && npm run dev
```
### 依赖服务
| 服务 | 端口 | 说明 |
|------|------|------|
| PostgreSQL | 5432 | 本地数据库 |
| Redis | 6379 | 缓存 |
| ai-proj | 8080 | AI 项目管理(可选,用于 Chat Sessions |
### 本地数据库
| 项目 | 值 |
|------|-----|
| 类型 | PostgreSQL |
| Database | paas_foundation |
| User | coolbuy-dev |
| Password | (空) |
### 远程数据库(生产参考)
| 项目 | 值 |
|------|-----|
| Host | 192.144.137.14 |
| Database | coolbuy_paas |
| User | platform |
| Password | Zhiyuncai2025~ |
### 数据库管理工具 - TablePlus
**推荐工具**: TablePlus - macOS 原生数据库 GUI 客户端,支持 PostgreSQL/MySQL/Redis 等。
#### 安装 TablePlus
```bash
# 使用 Homebrew 安装
brew install --cask tableplus
# 启动 TablePlus
open -a TablePlus
```
#### 自动配置数据库连接
TablePlus 的连接配置存储在 `~/Library/Application Support/com.tinyapp.TablePlus/Data/Connections.plist`
**快速配置脚本**(已包含本地和远程连接):
```bash
# 1. 关闭 TablePlus如果正在运行
killall TablePlus 2>/dev/null || true
# 2. 备份现有配置
cp ~/Library/Application\ Support/com.tinyapp.TablePlus/Data/Connections.plist \
~/Library/Application\ Support/com.tinyapp.TablePlus/Data/Connections.plist.backup
# 3. 编辑配置文件添加连接(参考下方 plist 格式)
# 4. 重启 TablePlus
open -a TablePlus
```
**连接配置示例**plist 格式):
```xml
<!-- 本地开发环境连接 -->
<dict>
<key>ConnectionName</key>
<string>Coolbuy PaaS - Local (开发环境)</string>
<key>DatabaseHost</key>
<string>localhost</string>
<key>DatabasePort</key>
<string>5432</string>
<key>DatabaseName</key>
<string>paas_foundation</string>
<key>DatabaseUser</key>
<string>coolbuy-dev</string>
<key>DatabasePasswordMode</key>
<integer>0</integer>
<key>Driver</key>
<string>PostgreSQL</string>
<key>Enviroment</key>
<string>local</string>
<key>statusColor</key>
<string>#3B82F6</string> <!-- 蓝色标识开发环境 -->
</dict>
```
#### 手动配置步骤
如果自动配置失败,可以手动在 TablePlus GUI 中添加:
1. 打开 TablePlus → 点击 "Create a new connection"
2. 选择 PostgreSQL
3. 填写连接信息:
**本地连接**
- Name: `Coolbuy PaaS - Local (开发环境)`
- Host: `localhost`
- Port: `5432`
- Database: `paas_foundation`
- User: `coolbuy-dev`
- Password: (留空)
- Color: 蓝色
**生产连接**
- Name: `Coolbuy PaaS - Production (生产环境)`
- Host: `192.144.137.14`
- Port: `5432`
- Database: `coolbuy_paas`
- User: `platform`
- Password: `Zhiyuncai2025~`
- Color: 红色(警示)
- **重要**:勾选 "Read-only mode" 防止误操作
#### 验证连接
连接成功后运行以下 SQL 验证:
```sql
-- 检查数据库版本
SELECT version();
-- 查看所有表
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY table_schema, table_name;
-- 查看库存预占记录
SELECT * FROM biz_inventory_reservations
ORDER BY created_at DESC
LIMIT 10;
-- 查看活跃租户
SELECT id, name, code, status
FROM fnd_tenants
WHERE status = 1 AND deleted_at IS NULL;
```
#### TablePlus 常用快捷键
| 快捷键 | 功能 |
|--------|------|
| `Cmd + N` | 新建连接 |
| `Cmd + T` | 新建 Tab |
| `Cmd + K` | 连接列表 |
| `Cmd + R` | 刷新 |
| `Cmd + /` | 注释/取消注释 SQL |
| `Cmd + Enter` | 执行 SQL |
| `Cmd + Shift + E` | 导出数据 |
#### 安全提示
⚠️ **生产数据库操作注意事项**
1. **必须**启用 "Read-only mode"(连接设置中)
2. **避免**直接执行 UPDATE/DELETE 操作
3. **建议**使用事务测试:`BEGIN; ... ROLLBACK;`
4. **慎用**批量操作
5. **定期**备份重要数据
---
## 独立商城 (web-mall)
独立 B2B 商城前端,与租户端 `web/` 并行运行,共享后端 API。
### 定位对比
| 维度 | 租户端 (web/) | 独立商城 (web-mall/) |
|------|--------------|---------------------|
| 定位 | 内部采购经理 | 终端采购用户 |
| 布局 | ERP 侧边栏 | 商城顶部导航 |
| 选品 | 表单弹窗选择 | 浏览分类/搜索+加购 |
| 下单 | ERP 表单模式 | 购物车→结算 |
### 页面结构
| 页面 | 路径 | 说明 |
|------|------|------|
| 首页 | `/` | 推荐商品、分类入口 |
| 商品列表 | `/products` | 搜索、筛选、排序 |
| 商品详情 | `/products/:id` | SKU 选择、加入购物车 |
| 购物车 | `/cart` | 购物车管理 |
| 结算 | `/checkout` | 提交订单 |
| 个人中心 | `/user` | 订单列表、订单详情 |
### Vite 代理配置 (web-mall/vite.config.ts)
```typescript
proxy: {
'/api/v1/auth': { target: 'http://localhost:7089' }, // auth-service
'/api': { target: 'http://localhost:7091' }, // erp-service
}
```
### Docker 部署
| 环境 | 端口 | 镜像 |
|------|------|------|
| 开发 | 5174 (Vite) | 本地 `npm run dev` |
| 生产 | 8889:80 | `saltthing123/coolbuy-paas-web-mall` |
### 需求来源
REQ-20260214-0001采购单双模式PRD 任务 #5874
---
## 商品模块开发
### 前端页面结构
```
web/src/pages/Product/
├── ProductList/ # 商品列表页
│ ├── index.tsx
│ ├── columns.tsx # 表格列配置
│ └── searchFields.ts # 搜索表单配置
├── ProductForm/ # 商品表单页(新增/编辑)
│ ├── index.tsx
│ ├── BasicInfo.tsx # 基本信息
│ ├── ImageUpload.tsx # 图片上传
│ └── PriceInfo.tsx # 价格信息
└── ProductDetail/ # 商品详情弹窗
└── index.tsx
```
### 后端模块结构
```
erp-service/internal/product/
├── model/
│ ├── spu.go # SPU 模型
│ └── sku.go # SKU 模型
├── biz/
│ ├── spu.go # SPU 业务逻辑
│ └── sku.go # SKU 业务逻辑
├── store/
│ ├── spu.go # SPU 数据访问
│ └── sku.go # SKU 数据访问
└── handler/
└── spu_handler.go # HTTP 处理器
```
### API 端点
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/products | 商品列表 |
| GET | /api/v1/products/:id | 商品详情 |
| POST | /api/v1/products | 创建商品 |
| PUT | /api/v1/products/:id | 更新商品 |
| DELETE | /api/v1/products/:id | 删除商品 |
| PATCH | /api/v1/products/:id/status | 上架/下架 |
---
## 研发经验与开发策略
> 本章总结了酷采 3.0 开发过程中积累的研发方法论,指导 Claude Code 高效完成业务模块开发。
### 系统定位
酷采 3.0 是酷采 2.0 的**重构升级版本**,不是 1:1 复刻。核心策略:**用 2.0 的业务逻辑 + 3.0 的代码模式**。
### 两代系统架构对比
| 维度 | 酷采 2.0 (coolbuy-legacy) | 酷采 3.0 (coolbuy-paas) |
|------|--------------------------|------------------------|
| 后端 | Java Spring Boot 单体 | Go 微服务 (5 个独立服务) |
| 前端 | Vue 2 + Element UI | React 18 + Ant Design 5 + Vite |
| 数据库 | MySQL + MyBatis | PostgreSQL + GORM |
| 认证 | Shiro | JWT + Redis 黑名单 |
| 多租户 | company_id 字段 | 原生 SaaS tenant 隔离 |
| 权限 | 多表关联 RBAC | 权限引擎 + 数据权限 |
### 业务模块完成度
| 业务模块 | 2.0 页面 | 3.0 后端 | 3.0 前端 | 优先级 |
|----------|---------|---------|---------|--------|
| 系统管理 (用户/角色/权限/组织) | 9 页 | 95% | 95% | - 已完成 |
| 商品管理 (SPU/SKU/分类/品牌) | 18 页 | 90% | 85% | P1 补齐 |
| 订单管理 (销售/采购/退货/审批) | 22 页 | 85% | 70% | **P0** |
| 仓储管理 (入库/出库/库存/物流) | 6 页 | 70% | 50% | P0 |
| 客户管理 (等级/销售员/供应商) | 17 页 | 60% | 50% | P0 |
| 财务结算 (账户/流水/结算单) | 3 页 | 40% | 30% | P1 |
| 报备授权 (项目/渠道/审核) | 20+ 页 | 20% | 10% | P2 评估 |
| 分销网络 (3.0 新增) | 无 | 75% | 75% | P1 |
| 购物车 & 商城 (web-mall) | 无 | 80% | 80% | P1 |
### 三层工作流 (需求 → 代码)
```
第一层: 需求 → PRD (/req-prd skill)
输入: 2.0 对应页面 + 业务规则描述
输出: PRD 文档 + 原型 HTML (dev-plans/prototypes/)
工具: /req create → /req doc
第二层: PRD → 开发计划 (/req-dev skill)
输入: PRD + 当前 3.0 代码库分析
输出: 文件级开发任务拆分
工具: /req doc → ai-proj tasks
第三层: 任务 → 代码 (/dev-coding skill)
输入: 开发计划 + 参考模块代码
输出: 可运行的前后端代码
工具: Claude Code 直接编码
```
### 模块复制模式 (核心开发方法)
每个新业务模块参照已有成熟模块的代码模式生成:
```
参考模块 (User) 目标模块 (Order)
──────────────────── ────────────────────
api/types.ts (追加接口) → OrderQueryParams, Order, OrderItem...
api/order.ts (新建) → getOrderList, getOrderById, createOrder...
pages/OrderList.tsx → 统计卡片 + 搜索表单 + 工具栏 + 表格
pages/OrderDetail.tsx → 头部信息 + 多 Tab (基本信息/明细/日志)
pages/OrderForm.tsx → Modal 表单 (新增/编辑)
router/tenantRoutes.tsx → 追加路由注册
```
**关键约定**(必须遵循):
- API 字段使用 `snake_case` (如 `order_status`, `created_at`)
- 响应数据解包:`res.data?.data || res.data`(双层嵌套兼容)
- 分页参数:`{ current, pageSize }` → 后端映射为 `{ page, page_size }`
- 时间格式化:`dayjs(v).format('YYYY-MM-DD HH:mm:ss')`
- 枚举渲染:`<Tag color={colorMap[value]}>{labelMap[value]}</Tag>`
- 表格滚动:`scroll={{ x: 列宽总和 }}`
- 权限控制:`<Permission permission="module:resource:action">` 包裹按钮
### 单模块开发 SOP
| 步骤 | 产出 | 预估时间 |
|------|------|---------|
| 1. 类型定义 | `types.ts` 追加接口/枚举 | 10 min |
| 2. API 层 | `api/xxx.ts` (CRUD + 业务接口) | 10 min |
| 3. 列表页 | `XxxList.tsx` (搜索+表格+工具栏+统计) | 20 min |
| 4. 详情页 | `XxxDetail.tsx` (多 Tab 信息展示) | 20 min |
| 5. 表单页 | `XxxForm.tsx` (新增/编辑 Modal) | 15 min |
| 6. 路由注册 | `tenantRoutes.tsx` 追加 lazy import | 5 min |
| 7. 联调修复 | 对接实际 API 后的字段/格式调整 | 30 min |
**单模块总计约 2 小时**,相比纯手写 2-3 天。
### 参考 2.0 代码的方法
开发新模块时Claude 应按以下顺序获取业务知识:
```bash
# 1. 读 2.0 前端路由 → 了解页面结构和导航
~/workspace/coolbuy-paas/coolbuy-legacy/ln_admin/src/router/<module>_routers.js
# 2. 读 2.0 前端页面 → 了解字段、搜索条件、操作按钮
~/workspace/coolbuy-paas/coolbuy-legacy/ln_admin/src/views/module/<module>/
# 3. 读 2.0 后端 Manager → 了解业务规则和校验逻辑
~/workspace/coolbuy-paas/coolbuy-legacy/cool_belle/module-provider/src/main/java/com/jzg/module/manager/<module>/
# 4. 读 2.0 数据模型 → 了解表结构和字段
~/workspace/coolbuy-paas/coolbuy-legacy/cool_belle/module-provider/src/main/java/com/jzg/module/dao/model/<module>/
# 5. 读 3.0 已有参考模块 → 了解代码模式
~/coding/qiudl/coolbuy-paas/web/src/modules/foundation/pages/User/
~/coding/qiudl/coolbuy-paas/erp-service/internal/<module>/
```
### 联调经验总结
以下是已验证的联调常见问题和修复模式:
| 问题 | 原因 | 修复模式 |
|------|------|---------|
| 表格空数据 | API 响应格式 `{list,total}` vs `data[]` | `const d = res.data?.data \|\| res.data; setList(d?.list \|\| d)` |
| 按钮不显示 | Permission 组件依赖 `hasPermission()` | 确保 `isSuperAdmin()` 包含 `admin` 角色 |
| 时间显示原始 ISO | 缺少 dayjs 格式化 | 统一用 `formatTime()` helper |
| 统计接口 404 | 后端未部署 | `catch {}` 静默处理,不弹错误 |
| "加载更多"覆盖数据 | setState 替换而非追加 | 加 `append` 参数:`prev => append ? [...prev, ...newList] : newList` |
| TreeSelect undefined | 组织树数据映射缺字段 | 确保 `convertTreeData` 映射 `title/value/key` |
| TS 类型不匹配 | API 返回结构与类型定义不一致 | 用 `: any` 断言,后续补齐类型 |
| antd deprecated | v5.26 废弃 API | `bordered={false}``variant="borderless"` |
### 分阶段推进计划
**Phase 1 — 核心交易闭环** (决定系统可用性)
- 订单管理:后端 handler 已有 507 行,前端 20 页面待联调
- 客户管理:基础 CRUD + 客户等级 + 销售员体系
- 仓储管理:入库/出库/库存查询
**Phase 2 — 商品完善 + 财务**
- 商品模块补齐:组合方案、现货加标、生效时间管理
- 财务结算:账户管理、流水查询、结算单
**Phase 3 — 运营支撑**
- 报备/授权模块评估后选择性迁移2.0 有 20+ 页面)
- 报表/数据分析
- 小程序/H5 商城增强
### 2.0 订单模块业务知识 (已梳理)
#### 订单状态机
```
创建 → 待支付(01) → 已支付(02) → 已发货(03) → 已收货(04) → 已完成(10)
↘ 已取消(05) ↘ 已退款(08)
待审核(06) 已关闭(07)
待出库(11)
```
#### 支付方式
| 代码 | 方式 | 业务规则 |
|------|------|---------|
| 01 | 预存款 | 扣减预付账户余额,记 BizFundLog |
| 02 | 授信 | 扣减信用额度,需审核 |
| 11 | 银行转账 | 自动生成付款单(MallPay)+收款单(MallReceive) |
| 13 | 货到付款 | 无需预付 |
#### 退款类型
| 类型 | 说明 | 校验规则 |
|------|------|---------|
| 整单退(01) | 全额退款 | 同一用户/订单只能有一个待处理退款 |
| 按数量退(02) | 部分数量退 | 检查可退数量 |
| 按物品退(03) | 指定商品退 | 逐项检查 |
| 退运费(04) | 仅退运费 | 运费大于 0 |
#### 用户角色与操作权限
| userType | 身份 | 可执行操作 |
|----------|------|-----------|
| 01 | 平台管理员 | 直接取消、审核、改价、所有操作 |
| 02 | 供应商 | 申请退款、查看、发货 |
| 03 | 客户 | 申请退款、查看、收货确认 |
| 04 | 前端用户 | 下单、查看、基础操作 |
### 3.0 订单模块现状 (已梳理)
#### 后端 API (erp-service)
```
GET /api/v1/orders # 列表 (支持 OrderNo/Status/Date/Amount 筛选)
GET /api/v1/orders/:id # 详情 (含 items)
POST /api/v1/orders # 创建 (含采购权限/区域/MOQ 校验)
PUT /api/v1/orders/:id # 更新 (仅草稿)
DELETE /api/v1/orders/:id # 删除 (仅草稿)
POST /api/v1/orders/:id/submit-approval # 提交审批
POST /api/v1/orders/:id/approve # 审批通过/拒绝
POST /api/v1/orders/:id/confirm-payment # 确认付款
POST /api/v1/orders/:id/ship # 发货
POST /api/v1/orders/:id/confirm-delivery # 确认收货
POST /api/v1/orders/:id/complete # 完成
POST /api/v1/orders/:id/cancel # 取消
POST /api/v1/orders/:id/refund # 退款
GET /api/v1/orders/:id/status-logs # 状态变更日志
GET /api/v1/orders/:id/approval-history # 审批历史
GET /api/v1/orders/statistics/* # 统计 (6 个子接口)
```
#### 3.0 订单状态 (14 态,比 2.0 更细)
```
Draft(10) → PendingApproval(20) → Approved(29) → PendingPayment(30) → Paid(39)
→ PendingShipment(40) → PartialShipped(45) → Shipped(49)
→ PartialDelivered(55) → Delivered(59) → Completed(90)
→ Rejected(92) / Cancelled(95) / Refunded(97)
```
#### 前端已有页面 (20 个)
```
/order/sales - SalesOrderList
/order/sales/add - SalesOrderForm
/order/business - BusinessOrderList
/order/purchase - PurchaseOrderList
/order/purchase/add - PurchaseOrderForm
/order/purchase/:id - PurchaseOrderDetail
/order/pending-approval - PendingApprovalList
/order/return - ReturnOrderList
/order/return/create - CreateReturnOrder
/order/return/:id - ReturnOrderDetail
/order/statistics - PurchaseOrderStatistics
/checkout - CheckoutPage
```
---
## 相关技能
- `coolbuy-platform` - 平台管理端开发
- `coolbuy-legacy` - 酷采 2.0 测试与参考
- `dev-coding` - 软件编码开发
- `dev-arch` - 软件架构设计
- `dev-test` - 软件测试
- `req-prd` - 产品需求文档编写
- `req-dev` - 需求开发计划编写
- `siyuan` - 思源笔记(含酷采相关文档)
---
## 版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.4.0 | 2026-02-25 | 新增研发经验与开发策略章节:两代系统对比、模块完成度、三层工作流、模块复制模式、联调经验、订单模块业务梳理 |
| 1.3.0 | 2026-02-14 | 新增数据库管理工具章节TablePlus 安装配置、自动配置脚本、连接验证、安全提示 |
| 1.2.0 | 2026-02-14 | 新增 web-mall 独立商城模块(架构、页面、代理、部署端口) |
| 1.1.0 | 2026-02-13 | 新增 AI Chat 代码路径、认证链路、关键文件说明;记录 aiChatApi vs aiChatService 陷阱 |
| 1.0.0 | 2026-01-10 | 初始版本添加酷采2.0测试环境和浏览器自动化指南 |

View File

@@ -0,0 +1,8 @@
{
"name": "coolbuy-platform-plugin",
"description": "Coolbuy SaaS 平台管理端开发与部署。用于平台端前后端开发、租户管理、部署发布、翻译检查等任务。",
"version": "1.0.9",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,338 @@
---
name: coolbuy-platform
description: Coolbuy SaaS 平台管理端开发与部署。用于平台端前后端开发、租户管理、部署发布、翻译检查等任务。当用户提到 coolbuy-platform、平台端、租户管理后台相关任务时自动激活。
---
# Coolbuy Platform Skill
Coolbuy SaaS 平台管理端,用于管理所有租户、功能授权、计费、运营分析等。
## 项目信息
| 项目 | 值 |
|------|-----|
| 本地路径 | `/Users/donglinlai/coding/qiudl/coolbuy-platform` |
| Git 仓库 | `git@gitea.pipexerp.com:pipexerp/coolbuy-platform.git` |
| 主分支 | main |
---
## 架构概览
```
coolbuy-platform/
├── service/ # Go 后端 (Gin + GORM)
│ ├── cmd/ # 入口 main.go
│ ├── internal/admin/ # 核心业务
│ │ ├── api/ # HTTP handlers
│ │ ├── biz/ # 业务逻辑
│ │ ├── store/ # 数据访问
│ │ ├── model/ # 领域模型
│ │ └── middleware/ # 中间件
│ └── configs/ # 配置文件
└── web/ # React 前端 (Vite + TypeScript)
├── src/
│ ├── api/ # API 客户端
│ ├── components/ # 组件
│ ├── pages/ # 页面
│ ├── stores/ # Zustand 状态
│ └── locales/ # i18n 翻译
└── dist/
```
---
## 部署环境
### 生产服务器
| 服务 | 地址 | 端口 | 容器名 |
|------|------|------|--------|
| 前端 | http://platform.pipexerp.com | 4999 | coolbuy-platform-web |
| 后端 | http://39.105.150.219 | 7090 | coolbuy-platform-service |
| Auth | http://39.105.150.219 | 7089 | coolbuy-auth-service |
### 服务器信息
| 项目 | 值 |
|------|-----|
| IP | 39.105.150.219 |
| 用户 | root |
| SSH 密钥 | ~/.ssh/coolbuy3.pem |
| 操作系统 | Ubuntu 24.04 |
### 数据库
| 项目 | 值 |
|------|-----|
| 类型 | PostgreSQL 16 |
| Host | 172.18.0.1 (Docker 网关) |
| Port | 5432 |
| Database | paas_platform |
| User | platform |
| Password | Coolbuy2025~ |
### Docker Registry
| 项目 | 值 |
|------|-----|
| Registry | Docker Hub |
| 账号 | saltthing123 |
| 前端镜像 | saltthing123/coolbuy-platform-web |
| 后端镜像 | saltthing123/coolbuy-platform-service |
| Auth镜像 | saltthing123/coolbuy-auth-service |
---
## 快速部署命令
### 部署前端
```bash
cd /Users/donglinlai/coding/qiudl/coolbuy-platform/web
# 1. 构建
npx vite build
# 2. 打包 Docker 镜像
~/.orbstack/bin/docker build --platform linux/amd64 \
-t saltthing123/coolbuy-platform-web:<version> \
-t saltthing123/coolbuy-platform-web:latest .
# 3. 推送镜像
~/.orbstack/bin/docker push saltthing123/coolbuy-platform-web:<version>
~/.orbstack/bin/docker push saltthing123/coolbuy-platform-web:latest
# 4. 部署到服务器
ssh -i ~/.ssh/coolbuy3.pem root@39.105.150.219 "
docker pull saltthing123/coolbuy-platform-web:<version> && \
docker stop coolbuy-platform-web && \
docker rm coolbuy-platform-web && \
docker run -d --name coolbuy-platform-web \
--restart unless-stopped \
-p 4999:80 \
saltthing123/coolbuy-platform-web:<version>
"
```
### 部署后端
```bash
cd /Users/donglinlai/coding/qiudl/coolbuy-platform/service
# 1. 构建二进制
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o coolbuy-platform-service cmd/main.go
# 2. 打包 Docker 镜像
~/.orbstack/bin/docker build --platform linux/amd64 \
-t saltthing123/coolbuy-platform-service:<version> \
-t saltthing123/coolbuy-platform-service:latest .
# 3. 推送镜像
~/.orbstack/bin/docker push saltthing123/coolbuy-platform-service:<version>
~/.orbstack/bin/docker push saltthing123/coolbuy-platform-service:latest
# 4. 部署到服务器
ssh -i ~/.ssh/coolbuy3.pem root@39.105.150.219 "
docker pull saltthing123/coolbuy-platform-service:<version> && \
docker stop coolbuy-platform-service && \
docker rm coolbuy-platform-service && \
docker run -d --name coolbuy-platform-service \
--restart unless-stopped \
-p 7090:7090 \
-v /data/coolbuy-platform/logs:/app/logs \
-v /data/coolbuy-platform/storage:/app/storage \
-v /data/coolbuy-platform/configs:/app/configs:ro \
-e TZ=Asia/Shanghai \
saltthing123/coolbuy-platform-service:<version> \
--config configs/config.prod.yaml
"
```
---
## 本地开发
### 启动后端
```bash
cd /Users/donglinlai/coding/qiudl/coolbuy-platform/service
go run cmd/main.go -config configs/config-dev.yaml
```
### 启动前端
```bash
cd /Users/donglinlai/coding/qiudl/coolbuy-platform/web
npm run dev
```
### 构建测试
```bash
# 后端测试
cd service && go test -v ./...
# 前端测试
cd web && npm run test
```
---
## 翻译管理
### 翻译文件位置
- 简体中文: `web/src/locales/zh-CN.ts`
- 繁体中文: `web/src/locales/zh-TW.ts`
### 检查翻译缺失
```bash
# 查找组件中使用的翻译 key
cd /Users/donglinlai/coding/qiudl/coolbuy-platform/web
grep -r "t(['\"]" src/components/ src/pages/ | grep -oE "t\(['\"][^'\"]+['\"]" | sort | uniq
```
### 翻译 key 命名规范
| 模块 | 前缀 | 示例 |
|------|------|------|
| 租户管理 | tenant.* | tenant.form.nameRequired |
| 用户管理 | user.* | user.createSuccess |
| 角色管理 | role.* | role.permissionAssigned |
| 菜单管理 | menu.* | menu.iconRequired |
| 通知中心 | notification.* | notification.markAllRead |
---
## API 端点
### 认证 (Auth Service - 7089)
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/v1/auth/login | 登录 |
| POST | /api/v1/auth/logout | 登出 |
| POST | /api/v1/auth/refresh | 刷新 Token |
### 平台管理 (Platform Service - 7090)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/admin/tenants | 租户列表 |
| POST | /api/v1/admin/tenants | 创建租户 |
| GET | /api/v1/admin/users | 用户列表 |
| GET | /api/v1/admin/roles | 角色列表 |
| GET | /api/v1/admin/menus | 菜单列表 |
| GET | /api/v1/admin/dashboard/overview | 仪表盘概览 |
### 健康检查
| 路径 | 说明 |
|------|------|
| /health | 基础健康检查 |
| /health/detailed | 详细状态 |
| /readiness | K8s 就绪探针 |
| /liveness | K8s 存活探针 |
---
## 数据库迁移
### 自动迁移
配置 `migration.auto: true` 时,服务启动自动运行迁移。
### 手动迁移
```bash
psql -h 172.18.0.1 -U platform -d paas_platform \
-f service/configs/migrations/001_create_platform_admin_tables.sql
```
---
## 常见问题
### 1. 翻译 key 不显示
组件使用的 key 与 locale 定义不匹配。检查:
- 组件使用 `t('tenant.domainConfig.*')` 但 locale 定义为 `tenant.domain.*`
- 需要添加别名命名空间
### 2. SSE 连接错误
通过 Cloudflare Tunnel 访问时 SSE 长连接会中断,属于已知问题,会自动重连。
### 3. Docker 构建失败
确保使用 OrbStack 的 docker`~/.orbstack/bin/docker`
### 4. 数据库连接失败
Docker 网关 IP 是 `172.18.0.1`,不是默认的 `172.17.0.1`
---
## 版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.0.9 | 2026-01-05 | 修复租户管理全部翻译缺失 |
| 1.0.8 | 2026-01-04 | 修复租户列表翻译 |
| 1.0.7 | 2026-01-04 | 修复租户表单翻译 |
| 1.0.0 | 2026-01-03 | 初始版本 |
---
## SSH 快捷命令
```bash
# 连接服务器
ssh -i ~/.ssh/coolbuy3.pem root@39.105.150.219
# 查看容器状态
ssh -i ~/.ssh/coolbuy3.pem root@39.105.150.219 "docker ps | grep coolbuy"
# 查看后端日志
ssh -i ~/.ssh/coolbuy3.pem root@39.105.150.219 "docker logs -f coolbuy-platform-service --tail 100"
# 查看前端日志
ssh -i ~/.ssh/coolbuy3.pem root@39.105.150.219 "docker logs -f coolbuy-platform-web --tail 100"
# 健康检查
ssh -i ~/.ssh/coolbuy3.pem root@39.105.150.219 "curl -s http://localhost:7090/health"
```
---
## 创建租户
```bash
# 通过 API 创建租户
curl -X POST http://39.105.150.219:7090/api/v1/admin/tenants \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"name": "租户名称",
"code": "tenant_code",
"contact": "联系人",
"phone": "13800138000",
"email": "contact@example.com",
"user_limit": 50,
"storage_limit": 10,
"schema_type": "shared",
"admin_username": "tenantadmin",
"admin_real_name": "管理员姓名",
"status": "normal"
}'
```
---
## 相关技能
- `ops-tools` - DevOps 运维工具,包含 Jenkins/Gitea 管理
- `dev-coding` - 软件编码开发
- `coolbuy-paas` - 租户端系统(待创建)

View File

@@ -0,0 +1,8 @@
{
"name": "data-excel-plugin",
"description": "Plugin for data-excel",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,443 @@
---
name: data-excel
description: Excel 数据处理与 BI 集成。通过自然语言操作 Excel 文件的读取、编辑、转换,并支持导入到 BI 系统Metabase进行可视化分析。当用户提到 Excel、表格处理、数据导入、BI 分析相关任务时自动激活。
---
# Excel 数据处理与 BI 集成 Skill
## 功能概述
- **Excel 读取**: 读取 .xlsx/.xls 文件内容,支持多 Sheet
- **Excel 编辑**: 修改单元格、添加/删除行列、格式化
- **数据转换**: Excel ↔ CSV ↔ JSON ↔ SQL
- **BI 集成**: 导入数据到 Metabase 进行可视化
---
## 环境依赖
```bash
# Python 包(推荐使用 uv
uv pip install pandas openpyxl xlrd xlsxwriter sqlalchemy pymysql
# 或使用 pip
pip install pandas openpyxl xlrd xlsxwriter sqlalchemy pymysql
```
---
## 自然语言操作示例
### 读取操作
| 用户说 | 执行操作 |
|--------|----------|
| "读取这个 Excel 文件" | 读取并显示内容摘要 |
| "看一下第二个 Sheet" | 切换到指定 Sheet |
| "显示前 20 行" | 限制显示行数 |
| "这个表有哪些列" | 列出列名和数据类型 |
### 编辑操作
| 用户说 | 执行操作 |
|--------|----------|
| "把 A 列的空值填充为 0" | 填充缺失值 |
| "删除重复行" | 去重 |
| "把日期列转成 YYYY-MM-DD 格式" | 格式化日期 |
| "添加一列计算总价=单价×数量" | 新增计算列 |
| "筛选销售额大于 1000 的记录" | 过滤数据 |
| "按部门汇总销售额" | 分组聚合 |
### 导出操作
| 用户说 | 执行操作 |
|--------|----------|
| "导出为 CSV" | 转换格式 |
| "生成 SQL 插入语句" | 生成 INSERT 语句 |
| "导入到数据库" | 写入 MySQL/PostgreSQL |
| "上传到 Metabase" | BI 系统集成 |
---
## Python 代码模板
### 读取 Excel
```python
import pandas as pd
# 读取 Excel 文件
df = pd.read_excel('data.xlsx')
# 读取指定 Sheet
df = pd.read_excel('data.xlsx', sheet_name='Sheet2')
# 读取所有 Sheet
all_sheets = pd.read_excel('data.xlsx', sheet_name=None)
for name, sheet_df in all_sheets.items():
print(f"Sheet: {name}, 行数: {len(sheet_df)}")
# 显示基本信息
print(df.head(10)) # 前 10 行
print(df.columns) # 列名
print(df.dtypes) # 数据类型
print(df.describe()) # 统计摘要
```
### 编辑 Excel
```python
import pandas as pd
df = pd.read_excel('data.xlsx')
# 填充空值
df['列名'].fillna(0, inplace=True)
# 删除重复行
df.drop_duplicates(inplace=True)
# 添加计算列
df['总价'] = df['单价'] * df['数量']
# 筛选数据
df_filtered = df[df['销售额'] > 1000]
# 分组汇总
df_summary = df.groupby('部门')['销售额'].sum().reset_index()
# 日期格式化
df['日期'] = pd.to_datetime(df['日期']).dt.strftime('%Y-%m-%d')
# 重命名列
df.rename(columns={'旧列名': '新列名'}, inplace=True)
# 排序
df.sort_values(by='销售额', ascending=False, inplace=True)
# 保存修改
df.to_excel('output.xlsx', index=False)
```
### 格式化 Excel带样式
```python
import pandas as pd
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
# 创建 Excel 并设置样式
df = pd.read_excel('data.xlsx')
with pd.ExcelWriter('styled_output.xlsx', engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name='数据')
workbook = writer.book
worksheet = writer.sheets['数据']
# 设置标题行样式
header_font = Font(bold=True, color='FFFFFF')
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
for col in range(1, len(df.columns) + 1):
cell = worksheet.cell(row=1, column=col)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center')
# 自动调整列宽
for column in worksheet.columns:
max_length = max(len(str(cell.value or '')) for cell in column)
worksheet.column_dimensions[column[0].column_letter].width = max_length + 2
```
---
## 数据转换
### Excel → CSV
```python
import pandas as pd
df = pd.read_excel('data.xlsx')
df.to_csv('output.csv', index=False, encoding='utf-8-sig') # utf-8-sig 解决中文乱码
```
### Excel → JSON
```python
import pandas as pd
df = pd.read_excel('data.xlsx')
# 转为 JSON 数组
json_str = df.to_json(orient='records', force_ascii=False, indent=2)
print(json_str)
# 保存为文件
with open('output.json', 'w', encoding='utf-8') as f:
f.write(json_str)
```
### Excel → SQL INSERT
```python
import pandas as pd
df = pd.read_excel('data.xlsx')
table_name = 'my_table'
# 生成 INSERT 语句
def generate_insert_sql(df, table_name):
columns = ', '.join(df.columns)
values_list = []
for _, row in df.iterrows():
values = ', '.join([f"'{v}'" if isinstance(v, str) else str(v) for v in row])
values_list.append(f"({values})")
sql = f"INSERT INTO {table_name} ({columns}) VALUES\n" + ',\n'.join(values_list) + ';'
return sql
sql = generate_insert_sql(df, table_name)
print(sql)
```
### Excel → MySQL 直接导入
```python
import pandas as pd
from sqlalchemy import create_engine
# 数据库连接
engine = create_engine('mysql+pymysql://user:password@host:3306/database')
# 读取 Excel
df = pd.read_excel('data.xlsx')
# 导入数据库(如果表存在则替换)
df.to_sql('table_name', engine, if_exists='replace', index=False)
# 或追加数据
df.to_sql('table_name', engine, if_exists='append', index=False)
```
---
## BI 系统集成 (Metabase)
### Metabase 服务信息
| 项目 | 值 |
|------|-----|
| 服务器 | prod-metaBI (192.144.174.87) |
| SSH 用户 | ubuntu |
| SSH 密钥 | ~/.ssh/prod_meta.pem |
| 系统 | Ubuntu 24.04 LTS |
| 数据源 | MySQL |
| 所属公司 | 北京欢乐宿 |
### SSH 连接
```bash
# 连接 Metabase 服务器
ssh prod-metaBI
# 查看 MySQL 状态
ssh prod-metaBI "docker ps | grep mysql"
# 查看 Metabase 容器状态
ssh prod-metaBI "docker ps | grep metabase"
```
### MySQL 连接配置 (Metabase 数据源)
| 配置项 | 值 |
|--------|-----|
| Host | `127.0.0.1` |
| Port | `3306` |
| Database | `finance_db` |
| Username | `root` |
| Password | `root123456` |
> **注意**: MySQL 8.0 需使用 `mysql_native_password` 认证,否则会报 RSA 公钥错误。
### 数据导入流程
```
Excel 文件 → Python 处理 → MySQL 数据库 → Metabase 可视化
```
### 完整导入脚本
```python
import pandas as pd
from sqlalchemy import create_engine
import os
def excel_to_metabase(excel_path, table_name, db_config):
"""
将 Excel 数据导入到 Metabase 可查询的数据库
Args:
excel_path: Excel 文件路径
table_name: 目标表名
db_config: 数据库配置 dict
"""
# 读取 Excel
df = pd.read_excel(excel_path)
# 数据清洗
df.columns = df.columns.str.strip() # 去除列名空格
df = df.dropna(how='all') # 删除全空行
# 连接数据库
engine = create_engine(
f"mysql+pymysql://{db_config['user']}:{db_config['password']}"
f"@{db_config['host']}:{db_config['port']}/{db_config['database']}"
)
# 导入数据
df.to_sql(table_name, engine, if_exists='replace', index=False)
print(f"✓ 已导入 {len(df)} 行数据到表 {table_name}")
print(f"→ 现在可以在 Metabase 中查询此表")
return df
# 使用示例
db_config = {
'host': 'localhost',
'port': 3306,
'user': 'metabase_user',
'password': 'your_password',
'database': 'analytics'
}
excel_to_metabase('sales_data.xlsx', 'sales_report', db_config)
```
### Metabase 常用查询模板
导入数据后,在 Metabase 中可使用以下查询:
```sql
-- 数据概览
SELECT * FROM imported_table LIMIT 100;
-- 按日期汇总
SELECT DATE(created_at) as date, COUNT(*) as count, SUM(amount) as total
FROM imported_table
GROUP BY DATE(created_at)
ORDER BY date;
-- 分类统计
SELECT category, COUNT(*) as count, AVG(value) as avg_value
FROM imported_table
GROUP BY category
ORDER BY count DESC;
```
---
## 常见场景
### 场景 1: 合并多个 Excel 文件
```python
import pandas as pd
import glob
# 合并目录下所有 Excel 文件
files = glob.glob('data/*.xlsx')
df_list = [pd.read_excel(f) for f in files]
df_merged = pd.concat(df_list, ignore_index=True)
df_merged.to_excel('merged.xlsx', index=False)
print(f"已合并 {len(files)} 个文件,共 {len(df_merged)}")
```
### 场景 2: 数据透视表
```python
import pandas as pd
df = pd.read_excel('sales.xlsx')
# 创建透视表
pivot = pd.pivot_table(
df,
values='销售额',
index='产品类别',
columns='月份',
aggfunc='sum',
fill_value=0
)
pivot.to_excel('pivot_report.xlsx')
```
### 场景 3: 数据校验
```python
import pandas as pd
df = pd.read_excel('data.xlsx')
# 检查空值
null_counts = df.isnull().sum()
print("空值统计:\n", null_counts[null_counts > 0])
# 检查重复
duplicates = df[df.duplicated()]
print(f"重复行数: {len(duplicates)}")
# 数据类型检查
print("数据类型:\n", df.dtypes)
```
### 场景 4: 生成报表
```python
import pandas as pd
df = pd.read_excel('data.xlsx')
# 生成多 Sheet 报表
with pd.ExcelWriter('report.xlsx') as writer:
# 原始数据
df.to_excel(writer, sheet_name='原始数据', index=False)
# 汇总数据
summary = df.groupby('部门').agg({
'销售额': 'sum',
'订单数': 'count'
}).reset_index()
summary.to_excel(writer, sheet_name='部门汇总', index=False)
# 统计信息
stats = df.describe()
stats.to_excel(writer, sheet_name='统计信息')
```
---
## 工作流程
```
1. 用户上传 Excel 文件或提供路径
2. 用自然语言描述需求(如"按月份汇总销售额"
3. Claude 生成并执行 Python 代码
4. 返回处理结果或导出文件
5. 可选:导入到 BI 系统进行可视化
```
---
## 注意事项
- Excel 文件路径使用绝对路径更可靠
- 中文 CSV 导出使用 `encoding='utf-8-sig'` 避免乱码
- 大文件(>10万行考虑分批处理
- 导入数据库前先备份现有数据
- 敏感数据注意脱敏处理

View File

@@ -0,0 +1,8 @@
{
"name": "dev-arch-plugin",
"description": "Plugin for dev-arch",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,390 @@
---
name: dev-arch
description: 软件架构设计技能。用于系统设计、技术选型、架构评审、设计文档编写。当用户提到架构设计、系统设计、技术方案、API 设计相关任务时自动激活。
---
# 软件架构设计 Skill (dev-arch)
## 概述
本技能用于软件架构设计工作,包括:
- 系统架构设计与评审
- 技术选型与方案对比
- API 设计与文档
- 数据库设计
- 设计模式应用
---
## 架构设计流程
### 1. 需求分析
```
输入:
- 业务需求文档
- 非功能性需求(性能、安全、可用性)
- 技术约束条件
输出:
- 需求清单
- 约束条件列表
```
### 2. 概要设计
```
内容:
- 系统边界定义
- 模块划分
- 技术栈选择
- 部署架构
输出:
- 架构图
- 技术方案文档
```
### 3. 详细设计
```
内容:
- API 接口设计
- 数据模型设计
- 核心流程设计
- 异常处理设计
输出:
- API 文档
- ER 图
- 时序图
```
---
## 架构设计模板
### 系统设计文档模板
```markdown
# [系统名称] 技术设计文档
## 1. 概述
### 1.1 背景
[项目背景和目标]
### 1.2 范围
[系统边界和功能范围]
### 1.3 术语定义
| 术语 | 定义 |
|------|------|
| ... | ... |
## 2. 系统架构
### 2.1 整体架构图
[架构图]
### 2.2 模块说明
| 模块 | 职责 | 技术栈 |
|------|------|--------|
| ... | ... | ... |
### 2.3 部署架构
[部署图]
## 3. 技术选型
### 3.1 技术栈
| 类别 | 选择 | 理由 |
|------|------|------|
| 后端框架 | ... | ... |
| 数据库 | ... | ... |
| 缓存 | ... | ... |
| 消息队列 | ... | ... |
### 3.2 技术对比
[备选方案对比分析]
## 4. 数据设计
### 4.1 ER 图
[ER 图]
### 4.2 核心表设计
[表结构说明]
### 4.3 索引策略
[索引设计]
## 5. API 设计
### 5.1 API 规范
[RESTful 规范说明]
### 5.2 核心接口
[接口定义]
## 6. 非功能性设计
### 6.1 性能设计
- 响应时间目标
- 吞吐量目标
- 优化策略
### 6.2 安全设计
- 认证授权
- 数据安全
- 日志审计
### 6.3 可用性设计
- 容错机制
- 监控告警
- 备份恢复
## 7. 风险评估
| 风险 | 影响 | 概率 | 应对措施 |
|------|------|------|----------|
| ... | ... | ... | ... |
## 8. 附录
- 参考资料
- 变更历史
```
---
## 常用架构模式
### 分层架构
```
┌─────────────────────────────────┐
│ 表现层 (Presentation) │
├─────────────────────────────────┤
│ 业务层 (Business) │
├─────────────────────────────────┤
│ 数据层 (Data Access) │
├─────────────────────────────────┤
│ 基础设施 (Infrastructure) │
└─────────────────────────────────┘
```
### 微服务架构
```
┌─────┐ ┌─────┐ ┌─────┐
│ API │ │ API │ │ API │
│ GW │──│ SVC │──│ SVC │
└──┬──┘ └──┬──┘ └──┬──┘
│ │ │
└────────┴────────┘
┌────┴────┐
│ Message │
│ Queue │
└─────────┘
```
### 六边形架构 (端口-适配器)
```
┌─────────────────┐
┌──────│ Adapters │──────┐
│ │ (Inbound) │ │
│ └────────┬────────┘ │
│ │ │
┌───┴───┐ ┌──────┴──────┐ ┌────┴────┐
│ REST │ │ Core │ │ Repo │
│ API │────│ Business │───│ Impl │
└───────┘ │ Domain │ └─────────┘
└─────────────┘
```
---
## API 设计规范
### RESTful 规范
| 方法 | 用途 | 示例 |
|------|------|------|
| GET | 查询资源 | `GET /users/{id}` |
| POST | 创建资源 | `POST /users` |
| PUT | 全量更新 | `PUT /users/{id}` |
| PATCH | 部分更新 | `PATCH /users/{id}` |
| DELETE | 删除资源 | `DELETE /users/{id}` |
### 响应格式
```json
{
"code": 0,
"message": "success",
"data": {},
"meta": {
"page": 1,
"limit": 20,
"total": 100
}
}
```
### 错误码设计
| 范围 | 类型 | 示例 |
|------|------|------|
| 10000-19999 | 系统错误 | 10001 内部错误 |
| 20000-29999 | 参数错误 | 20001 参数缺失 |
| 30000-39999 | 业务错误 | 30001 用户不存在 |
| 40000-49999 | 权限错误 | 40001 未授权 |
---
## 数据库设计
### 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 表名 | 小写下划线,复数 | `user_accounts` |
| 字段 | 小写下划线 | `created_at` |
| 主键 | id | `id` |
| 外键 | 表名_id | `user_id` |
| 索引 | idx_表名_字段 | `idx_users_email` |
### 通用字段
```sql
-- 每个表必须包含的字段
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME DEFAULT NULL -- 软删除
```
### 索引策略
- 主键索引:每表必须
- 唯一索引:业务唯一字段
- 普通索引:高频查询字段
- 联合索引:遵循最左前缀原则
---
## 技术选型参考
### 后端框架
| 语言 | 框架 | 适用场景 |
|------|------|----------|
| Go | Gin, Echo | 高并发 API |
| Java | Spring Boot | 企业应用 |
| Python | FastAPI, Django | 快速开发 |
| Node.js | NestJS, Express | 全栈/实时 |
### 数据库选型
| 类型 | 选项 | 适用场景 |
|------|------|----------|
| 关系型 | MySQL, PostgreSQL | 事务型业务 |
| 文档型 | MongoDB | 灵活结构 |
| 键值型 | Redis | 缓存/会话 |
| 时序 | InfluxDB | 监控/IoT |
| 搜索 | Elasticsearch | 全文检索 |
### 消息队列
| 选项 | 特点 | 适用场景 |
|------|------|----------|
| Redis | 简单快速 | 轻量级队列 |
| RabbitMQ | 可靠传递 | 企业集成 |
| Kafka | 高吞吐 | 日志/流处理 |
| NATS | 轻量级 | 微服务通信 |
---
## 架构评审检查清单
### 功能性检查
- [ ] 需求覆盖完整
- [ ] 边界条件处理
- [ ] 异常情况处理
- [ ] 数据一致性保证
### 非功能性检查
- [ ] 性能目标明确
- [ ] 安全措施到位
- [ ] 可扩展性设计
- [ ] 可维护性考虑
### 运维检查
- [ ] 监控指标定义
- [ ] 日志规范
- [ ] 部署方案
- [ ] 回滚机制
---
## 与 ai-proj 集成
### 创建架构设计任务
```bash
# 创建架构设计任务
ai-proj task create --title "[系统名称] 架构设计"
# 创建子任务
ai-proj task create --title "需求分析" --parent-id <parentId>
ai-proj task create --title "概要设计" --parent-id <parentId>
ai-proj task create --title "详细设计" --parent-id <parentId>
ai-proj task create --title "架构评审" --parent-id <parentId>
```
### 关联设计文档
```bash
# 附加设计文档到任务
ai-proj task append-doc --id <taskId> --content "设计文档内容 (Markdown)"
```
---
## 最佳实践
1. **KISS 原则** - 保持简单,避免过度设计
2. **YAGNI 原则** - 不要预测未来需求
3. **DRY 原则** - 避免重复代码
4. **SOLID 原则** - 面向对象设计原则
5. **关注点分离** - 模块职责单一
6. **高内聚低耦合** - 模块独立性
---
## 文档工具
### 架构图工具
- **draw.io** - 在线免费绘图
- **PlantUML** - 代码生成图
- **Mermaid** - Markdown 内嵌图
- **Excalidraw** - 手绘风格
### Mermaid 示例
```mermaid
sequenceDiagram
Client->>API Gateway: Request
API Gateway->>Auth Service: Validate Token
Auth Service-->>API Gateway: Valid
API Gateway->>Business Service: Forward Request
Business Service->>Database: Query
Database-->>Business Service: Result
Business Service-->>API Gateway: Response
API Gateway-->>Client: Response
```
---
## 参考资源
- [系统设计入门](https://github.com/donnemartin/system-design-primer)
- [微服务架构](https://microservices.io/)
- [RESTful API 设计指南](https://restfulapi.net/)
- [数据库设计范式](https://www.guru99.com/database-normalization.html)

View File

@@ -0,0 +1,8 @@
{
"name": "dev-coding-plugin",
"description": "Plugin for dev-coding",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,810 @@
---
name: dev-coding
description: 软件编码开发技能。用于代码编写、功能实现、代码审查、重构优化。集成 ai-proj CLI 进行任务管理和进度跟踪。支持 Go、Vue、React、iOS、Android、小程序等全栈开发。
---
# 软件编码开发 Skill (dev-coding)
## ⚠️ REQ 任务自动工作流
**当收到 REQ 任务(包含 REQ-YYYYMMDD-XXXX需要开发时必须严格按以下顺序执行**
1. **读取 ticket** — 从 ai-proj 获取需求详情和关联文档
```
mcp__ai-proj-dev__get_detailed_task_info (通过 REQ 号查找)
mcp__ai-proj-dev__get_task_document (如果有 PRD 文档)
```
2. **进入 Plan Mode** — 调用 `EnterPlanMode` 工具
- 分析需求,探索代码库,设计实现方案
- 输出实现计划(涉及的文件、改动范围、测试策略)
- 等待用户审批后再开始编码
3. **执行计划** — 用户批准后按计划编码 + 写测试
**禁止跳过 plan mode 直接编码。**
---
## 概述
本技能用于软件编码开发工作,支持多种项目类型:
- Go 后端 (Gin + GORM)
- Vue 3 / React 前端
- iOS (Swift/SwiftUI)
- Android (Kotlin/Jetpack Compose)
- PDA 应用
- MCP 桥接服务
- 微服务架构
核心集成 **ai-proj CLI** 进行任务管理。
---
## ai-proj 任务管理
### 开发任务工作流
```
1. 查看/创建任务 → 2. 启动任务 → 3. 编码实现 → 4. 完成任务 → 5. 记录文档
```
### 任务操作速查
| 操作 | CLI 命令 | 说明 |
|------|----------|------|
| 查看任务列表 | `ai-proj task list` | 查看项目所有任务 |
| 创建任务 | `ai-proj task create` | 创建新任务 |
| 创建子任务 | `ai-proj task create --parent-id` | 分解任务 |
| 启动任务 | `ai-proj task start --id` | 开始执行 |
| 完成任务 | `ai-proj task complete --id` | 标记完成 |
| 更新任务 | `ai-proj task update --id` | 更新状态/描述 |
| 查看详情 | `ai-proj task get --id` | 完整任务信息 |
| 记录文档 | `ai-proj task append-doc --id` | 附加文档 |
### 开始新任务
```bash
# 1. 查看任务列表
ai-proj task list --status todo,in_progress
# 2. 启动任务
ai-proj task start --id <taskId>
# 3. 完成后
ai-proj task complete --id <taskId>
# 4. 记录文档
ai-proj task append-doc --id <taskId> --content "实现说明"
```
---
## 项目类型速查
### 当前项目生态
| 项目 | 类型 | 后端 | 前端 | 移动端 |
|------|------|------|------|--------|
| TWMS | 仓储物流 | Go+Gin+MySQL | Vue 3 | - |
| AI-Proj | 项目管理 | Go+Gin+PostgreSQL | React 18 | iOS+Android |
| DICIAI | 进销存SaaS | Go+Gin+MySQL | Vue 3 | Android PDA |
---
## Go 后端开发
### 分层架构
```
backend/
├── cmd/main.go # 入口点
├── internal/
│ ├── controller/handlers/ # HTTP 处理层
│ ├── biz/services/ # 业务逻辑层
│ ├── store/database/ # 数据访问层
│ └── middleware/ # 中间件
├── pkg/
│ ├── model/ # 数据模型
│ ├── errno/ # 错误定义
│ ├── api/ # API 类型
│ └── util/ # 工具函数
└── configs/migrations/ # 配置和迁移
```
### 代码规范
```go
// 包声明和导入组织
package main
import (
// 标准库
"context"
"fmt"
// 第三方
"github.com/gin-gonic/gin"
// 项目内部
"project/internal/pkg/errno"
)
// 错误处理 (Errno 模式)
if err != nil {
core.WriteResponse(c, errno.ErrBind, nil)
return
}
// 接口定义
type IStore interface {
Users() UserStore
}
// 服务注入
type userBiz struct {
ds store.IStore
}
func NewUserBiz(ds store.IStore) *userBiz {
return &userBiz{ds: ds}
}
```
### 常用命令
```bash
# 构建
make build
go build -o ./_output/main ./cmd/main.go
# 运行
./_output/main --config ./configs/config.yaml
# 测试
make test
go test -v ./...
# 代码检查
make lint
golangci-lint run
# Swagger 文档
make swagger
swag init -g cmd/main.go
```
### 数据库模型
```go
type UserM struct {
Id int64 `gorm:"column:id;primary_key"`
Username string `gorm:"column:username;not null"`
CreateTime int64 `gorm:"column:create_time"`
UpdateTime int64 `gorm:"column:update_time"`
DeletedAt soft_delete.DeletedAt `gorm:"column:deleted_at"`
}
func (m *UserM) TableName() string { return "users" }
func (m *UserM) BeforeCreate(tx *gorm.DB) error {
m.CreateTime = time.Now().Unix()
m.UpdateTime = m.CreateTime
return nil
}
```
---
## 前端 data-testid 规范
编写或修改前端组件时,**所有可交互元素必须加 `data-testid`**。
**命名格式:** `<模块>-<元素类型>[-<标识>]`
```vue
<!-- ✅ 正确 -->
<a-input data-testid="product-input-name" v-model:value="form.name" placeholder="商品名称" />
<a-button data-testid="product-btn-submit" type="primary">创建商品</a-button>
<a-select data-testid="product-select-brand" v-model:value="form.brandId" />
<a-table data-testid="product-table" :dataSource="list" />
<!-- ❌ 错误 — 交互元素无 data-testid -->
<a-input v-model:value="form.name" placeholder="商品名称" />
<a-button type="primary">创建商品</a-button>
```
**必须加:** 输入框、选择器、开关、按钮(提交/取消/删除)、表格、模态框确认按钮、导航菜单项
**不需要加:** 纯展示文本、图标、布局容器Row/Col/Space
---
## Vue 3 前端开发
### 项目结构
```
frontend/src/
├── api/ # API 服务 (按模块分组)
│ ├── wms/ # 仓储管理
│ ├── oms/ # 订单管理
│ └── system/ # 系统管理
├── views/ # 页面组件
├── components/ # 可复用组件
├── store/modules/ # Pinia 状态
├── router/ # 路由配置
├── utils/
│ ├── request.ts # Axios 拦截器
│ └── permission.ts # 权限检查
└── i18n/ # 国际化
```
### 代码规范
```typescript
// API 服务层
// api/user/model/index.ts
export interface User {
id: number;
username: string;
}
// api/user/index.ts
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { User } from './model';
export async function getUsers(params: UserParams) {
const res = await request.get<ApiResult<PageResult<User>>>(
'/v1/users',
{ params }
);
if (res.status === 200) {
return res.data;
}
return Promise.reject(new Error(res.data.message));
}
```
### 常用命令
```bash
# 安装依赖
npm install
pnpm install # DICIAI 使用 pnpm
# 开发
npm run dev
# 构建
npm run build:prod
npm run build:test
# 代码检查
npm run lint:eslint
```
---
## React 前端开发
### 项目结构
```
frontend/src/
├── pages/ # 页面组件
├── components/ # 可复用组件
├── services/ # API 服务
├── hooks/ # 自定义 Hooks
├── contexts/ # Context Providers
├── types/ # TypeScript 类型
├── utils/ # 工具函数
└── config/ # 配置
```
### 代码规范
```typescript
// Context 集成
<QueryProvider>
<AuthProvider>
<TimerProvider>
<ConfigProvider locale={zhCN}>
<Router>
<Routes />
</Router>
</ConfigProvider>
</TimerProvider>
</AuthProvider>
</QueryProvider>
// API 服务
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 120000,
});
api.interceptors.request.use((config) => {
const token = TokenManager.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
```
### 常用命令
```bash
# 开发
npm start
# 构建
npm run build
# 测试
npm test
npm run test:e2e
```
---
## iOS 开发 (Swift/SwiftUI)
### 项目结构
```
AI-Proj-iOS/
├── Core/ # 核心服务
│ ├── Network/ # 网络层
│ ├── Storage/ # 本地存储
│ └── Auth/ # 认证
├── Features/ # 功能模块
│ ├── Dashboard/
│ ├── Tasks/
│ └── Settings/
├── Models/ # 数据模型
└── UI/ # UI 组件
```
### 代码规范
```swift
// MVVM 架构
class TaskViewModel: ObservableObject {
@Published var tasks: [Task] = []
@Published var isLoading = false
private let taskService: TaskServiceProtocol
init(taskService: TaskServiceProtocol = TaskService()) {
self.taskService = taskService
}
func fetchTasks() async {
isLoading = true
defer { isLoading = false }
do {
tasks = try await taskService.getTasks()
} catch {
// 错误处理
}
}
}
// SwiftUI 视图
struct TaskListView: View {
@StateObject private var viewModel = TaskViewModel()
var body: some View {
List(viewModel.tasks) { task in
TaskRow(task: task)
}
.task {
await viewModel.fetchTasks()
}
}
}
```
### 构建命令
```bash
# Xcode 构建
xcodebuild -scheme AI-Proj-iOS -configuration Debug
# 测试
xcodebuild test -scheme AI-Proj-iOS
```
### 常见问题排查
#### SwiftLint 沙盒错误
**问题描述**
构建时出现错误:
```
Sandbox: swiftlint(xxxx) deny(1) file-read-data /path/to/.swiftlint.yml
```
**原因**
Xcode 15+ 默认启用 User Script Sandboxing限制脚本访问文件系统。
**解决方案**
方案 1 - 修改项目配置(推荐):
1. 打开 Xcode → 选择项目 → Build Settings
2. 搜索 "User Script Sandboxing"
3. 将 `ENABLE_USER_SCRIPT_SANDBOXING` 设置为 `NO`
方案 2 - 命令行构建时禁用:
```bash
xcodebuild -scheme AI-Proj-iOS -configuration Debug \
ENABLE_USER_SCRIPT_SANDBOXING=NO
```
方案 3 - 直接修改 project.pbxproj
```bash
sed -i '' 's/ENABLE_USER_SCRIPT_SANDBOXING = YES/ENABLE_USER_SCRIPT_SANDBOXING = NO/g' \
AI-Proj-iOS.xcodeproj/project.pbxproj
```
#### Personal Development Team 功能限制
**问题描述**
使用免费 Personal Team 签名时报错:
```
Cannot create iOS App Development provisioning profile...
Personal development teams do not support the Associated Domains,
Push Notifications and App Groups capabilities.
```
**原因**
Personal Team免费账户不支持以下 Entitlements
- Associated Domains (`com.apple.developer.associated-domains`)
- Push Notifications (`aps-environment`)
- App Groups (`com.apple.security.application-groups`)
**解决方案**
1. 从 Entitlements 文件中移除不支持的功能:
```xml
<!-- AI-Proj-iOS.entitlements -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 仅保留 Personal Team 支持的功能 -->
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yourcompany.app</string>
</array>
</dict>
</plist>
```
2. Personal Team 支持的功能:
- Keychain Access Groups ✓
- In-App Purchase ✓
- Game Center ✓
3. 需要付费 Apple Developer Program 的功能:
- Push Notifications ✗
- Associated Domains ✗
- App Groups ✗
- CloudKit ✗
- Sign in with Apple ✗
---
## Android 开发 (Kotlin)
### 项目结构
```
android-app/app/src/main/
├── java/com/project/
│ ├── ui/ # UI 层
│ │ ├── screens/ # Compose 屏幕
│ │ └── components/ # 可复用组件
│ ├── data/ # 数据层
│ │ ├── api/ # 网络接口
│ │ ├── repository/ # 仓库模式
│ │ └── local/ # 本地存储
│ ├── domain/ # 业务逻辑
│ └── di/ # 依赖注入
└── res/ # 资源文件
```
### 代码规范
```kotlin
// Hilt 依赖注入
@HiltViewModel
class TaskViewModel @Inject constructor(
private val taskRepository: TaskRepository
) : ViewModel() {
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()
fun fetchTasks() {
viewModelScope.launch {
taskRepository.getTasks()
.collect { _tasks.value = it }
}
}
}
// Jetpack Compose
@Composable
fun TaskListScreen(
viewModel: TaskViewModel = hiltViewModel()
) {
val tasks by viewModel.tasks.collectAsState()
LazyColumn {
items(tasks) { task ->
TaskItem(task = task)
}
}
}
```
### 构建命令
```bash
# 构建 Debug
./gradlew assembleDebug
# 构建 Release
./gradlew assembleRelease
# 测试
./gradlew test
```
---
## PDA 应用开发
### 特点
- Android 原生开发
- 扫码枪集成
- 离线优先
- 简洁 UI
### 常见功能
```kotlin
// 扫码处理
class ScanReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val barcode = intent.getStringExtra("SCAN_BARCODE")
// 处理扫码结果
}
}
// 离线存储
@Entity(tableName = "inventory")
data class Inventory(
@PrimaryKey val id: Long,
val barcode: String,
val quantity: Int,
@ColumnInfo(name = "sync_status")
val syncStatus: SyncStatus = SyncStatus.PENDING
)
```
---
## MCP 桥接开发
### 项目结构
```
mcp-task-bridge/
├── index.ts # 入口
├── task-service.ts # 任务服务
├── document-service.ts # 文档服务
├── base-client.ts # HTTP 基类
├── types.ts # 类型定义
└── token-storage.ts # Token 管理
```
### 代码规范
```typescript
// 服务类模式
export class TaskService extends BaseClient {
async createTask(
title: string,
projectId: number = 1,
options: CreateTaskOptions = {}
): Promise<ApiResponse<Task>> {
try {
const response = await this.makeRequest<Task>(
'POST',
`/projects/${projectId}/tasks`,
{ title, project_id: projectId, ...options }
);
if (response.success) {
return {
success: true,
data: response.data,
message: `✅ 任务 "${title}" 创建成功`
};
}
return response;
} catch (error: any) {
return {
success: false,
error: `创建任务失败: ${error.message}`
};
}
}
}
```
---
## 通用开发规范
### API 响应格式
```json
{
"code": 0,
"message": "success",
"data": {}
}
```
### 分页参数
```json
{
"page": 1,
"limit": 20,
"sort": "created_at",
"order": "desc"
}
```
### 认证方式
- JWT Token
- Header: `Authorization: Bearer <token>`
### 错误处理
```go
// Go
if err != nil {
core.WriteResponse(c, errno.ErrXxx, nil)
return
}
// TypeScript
try {
const result = await api.call();
} catch (error) {
message.error(error.message);
}
// Swift
do {
let result = try await service.fetch()
} catch {
// 处理错误
}
```
---
## Git 工作流
### 提交规范
| 类型 | 说明 |
|------|------|
| feat | 新功能 |
| fix | Bug 修复 |
| docs | 文档 |
| refactor | 重构 |
| test | 测试 |
| chore | 杂项 |
### 双电脑同步 (au-dev / cn-dev)
```bash
# 离开时
git add -A
git commit -m "WIP: sync from $(hostname)"
git push origin $(git branch --show-current)
# 到达时
git fetch origin
git pull origin $(git branch --show-current)
```
---
## Docker 部署
### 标准配置
```yaml
# docker-compose.yml
services:
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- db
- redis
frontend:
build: ./frontend
ports:
- "80:80"
db:
image: mysql:8.0
# 或 postgres:15
redis:
image: redis:alpine
```
### 常用端口
| 服务 | 端口 |
|------|------|
| Backend | 8080 / 9099 |
| Frontend | 80 / 3000 |
| MySQL | 3306 |
| PostgreSQL | 5432 |
| Redis | 6379 |
---
## Push 前必须通过:变更包单元测试
**在 `git push` 或 `/pr create` 之前,必须跑所有变更文件对应包的单元测试。**
```bash
# 找出变更的 Go 文件所在包,跑对应测试
PKGS=$(git diff --name-only origin/main...HEAD | grep '\.go$' | grep -v '_test\.go' | sed 's|/[^/]*$||' | sort -u | sed 's|^|./|' | tr '\n' ' ')
if [ -n "$PKGS" ]; then
echo "Running tests for changed packages: $PKGS"
go test -v -count=1 $PKGS
else
echo "No Go files changed, skipping tests"
fi
```
**规则:**
- 测试通过 → 继续 push + `/pr create`
- 测试失败 → 尝试自动修复,修复后重跑
- 修复成功 → 继续 push
- **修复失败 → 禁止 push向用户报告失败原因等待指示**
- 仅改了 `_test.go` → 同样需要跑(验证测试本身通过)
- 无 Go 文件变更(纯前端/文档) → 跳过
---
## 最佳实践
1. **任务驱动** - 使用 ai-proj 管理所有开发任务
2. **分层清晰** - Controller → Service → Repository
3. **接口先行** - 先定义接口再实现
4. **小步提交** - 频繁提交,每次做一件事
5. **测试覆盖** - 核心逻辑必须有测试
6. **文档同步** - 代码变更同步更新文档

View File

@@ -0,0 +1,8 @@
{
"name": "dev-plugin",
"description": "Plugin for dev",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,314 @@
---
name: dev
description: 软件开发技能组入口。整合架构设计(dev-arch)、编码实现(dev-coding)、测试(dev-test)三个子技能提供完整的软件开发工作流。支持全栈开发Go、Vue、React、iOS、Android、小程序等。
---
# 软件开发 Skill (dev)
## 概述
dev 是一个技能组入口,整合了软件开发的三个核心阶段:
| 子技能 | 用途 | 触发词 |
|--------|------|--------|
| **dev-arch** | 架构设计、技术选型、系统设计 | 架构、设计、技术方案 |
| **dev-coding** | 编码实现、功能开发、代码审查 | 编码、开发、实现 |
| **dev-test** | 单元测试、集成测试、E2E测试 | 测试、test、覆盖率 |
---
## 开发工作流
```
需求分析 → 架构设计 → 编码实现 → 测试验证 → 部署上线
↓ ↓ ↓
dev-arch dev-coding dev-test
```
### 典型流程
1. **架构设计** (dev-arch)
- 需求分析
- 技术选型
- 系统设计文档
- 架构评审
2. **编码实现** (dev-coding)
- 任务分解
- 功能开发
- 代码审查
- 文档记录
3. **测试验证** (dev-test)
- 单元测试
- 集成测试
- E2E 测试
- 覆盖率分析
---
## 支持的项目类型
### 当前项目生态
| 项目 | 类型 | 技术栈 |
|------|------|--------|
| **TWMS** | 仓储物流 | Go + Vue 3 + MySQL |
| **AI-Proj** | 项目管理 | Go + React + PostgreSQL + iOS + Android |
| **DICIAI** | 进销存SaaS | Go + Vue 3 + MySQL + Android PDA |
### 技术栈矩阵
| 端 | 语言/框架 | 工具 |
|-----|----------|------|
| **后端** | Go (Gin + GORM) | MySQL/PostgreSQL, Redis, Docker |
| **Web前端** | Vue 3 / React 18 | TypeScript, Vite/CRA, Ant Design |
| **iOS** | Swift + SwiftUI | Xcode, XCTest |
| **Android** | Kotlin + Compose | Gradle, Hilt, Room |
| **PDA** | Android 原生 | 扫码枪集成, 离线存储 |
| **MCP** | TypeScript | Node.js, MCP SDK |
---
## ai-proj 任务管理集成
所有开发工作都通过 ai-proj CLI 进行任务管理:
### 快速开始
```bash
# 1. 查看待办任务
ai-proj task list --status in_progress,todo
# 2. 启动任务
ai-proj task start --id <taskId>
# 3. 完成任务
ai-proj task complete --id <taskId>
# 4. 记录文档
ai-proj task append-doc --id <taskId> --content "实现说明"
```
### 任务分解
```bash
# 创建主任务
ai-proj task create --title "功能名称"
# 创建子任务
ai-proj task create --title "架构设计" --parent-id <parentId>
ai-proj task create --title "功能开发" --parent-id <parentId>
ai-proj task create --title "测试验证" --parent-id <parentId>
```
---
## 常用命令速查
### Go 后端
```bash
# 构建
make build
# 运行
./_output/main --config ./configs/config.yaml
# 测试
make test
make cover
```
### Vue 前端
```bash
# 开发
npm run dev
# 构建
npm run build:prod
# 检查
npm run lint:eslint
```
### React 前端
```bash
# 开发
npm start
# 构建
npm run build
# 测试
npm test
npm run test:e2e
```
### iOS
```bash
# 构建
xcodebuild -scheme ProjectName -configuration Debug
# 测试
xcodebuild test -scheme ProjectName
```
### Android
```bash
# 构建
./gradlew assembleDebug
./gradlew assembleRelease
# 测试
./gradlew test
./gradlew connectedAndroidTest
```
---
## Git 工作流
### 提交规范
| 类型 | 说明 |
|------|------|
| feat | 新功能 |
| fix | Bug 修复 |
| docs | 文档 |
| refactor | 重构 |
| test | 测试 |
| chore | 杂项 |
### 分支策略
```bash
# 功能开发
git checkout -b feature/功能名称
# 提交
git commit -m "feat: 功能描述"
# 推送
git push origin feature/功能名称
# 合并
git checkout main
git merge feature/功能名称
```
### 双电脑同步 (au-dev / cn-dev)
```bash
# 离开时
git add -A
git commit -m "WIP: sync from $(hostname)"
git push origin $(git branch --show-current)
# 到达时
git fetch origin
git pull origin $(git branch --show-current)
```
---
## Docker 部署
### 标准配置
```yaml
services:
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- db
- redis
frontend:
build: ./frontend
ports:
- "80:80"
db:
image: mysql:8.0
# 或 postgres:15
redis:
image: redis:alpine
```
### 常用端口
| 服务 | 端口 |
|------|------|
| Backend | 8080 / 9099 |
| Frontend | 80 / 3000 |
| MySQL | 3306 |
| PostgreSQL | 5432 |
| Redis | 6379 |
---
## 子技能详情
### dev-arch (架构设计)
用于系统设计阶段:
- 需求分析
- 技术选型
- 架构设计文档
- API 设计
- 数据库设计
- 架构评审
### dev-coding (编码实现)
用于开发实现阶段:
- Go 后端开发
- Vue/React 前端开发
- iOS/Android 移动开发
- PDA 应用开发
- MCP 桥接开发
- 代码审查
### dev-test (测试)
用于测试验证阶段:
- 单元测试
- 集成测试
- E2E 测试
- UI 测试
- 覆盖率分析
---
## 最佳实践
1. **任务驱动** - 使用 ai-proj 管理所有开发任务
2. **设计先行** - 复杂功能先设计后编码
3. **分层清晰** - Controller → Service → Repository
4. **小步提交** - 频繁提交,每次做一件事
5. **测试覆盖** - 核心逻辑必须有测试
6. **文档同步** - 代码变更同步更新文档
7. **代码审查** - 重要变更必须审查
---
## 何时使用哪个子技能
| 场景 | 推荐技能 |
|------|----------|
| 新功能设计 | dev-arch |
| 技术方案评审 | dev-arch |
| 功能开发实现 | dev-coding |
| Bug 修复 | dev-coding |
| 编写测试 | dev-test |
| 测试覆盖率提升 | dev-test |
| 代码审查 | dev-coding |
| 性能优化 | dev-arch + dev-coding |

View File

@@ -0,0 +1,8 @@
{
"name": "dev-test-plugin",
"description": "软件测试技能。用于单元测试、集成测试、E2E测试、测试用例设计。支持 Go、Vue、React、iOS、Android 等多平台测试。",
"version": "2.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,141 @@
---
name: dev-test
description: 软件测试技能。用于单元测试、集成测试、E2E测试、测试用例设计。支持 Go、Vue、React、iOS、Android 等多平台测试。
---
# 软件测试 Skill (dev-test)
## 子文件索引
| 文件 | 内容 |
|------|------|
| `go-testing.md` | Go 后端测试 (testify + test DB + httptest)。**biz 层禁止 mock必须用真实 PostgreSQL test DB** |
| `frontend-testing.md` | Vue (Vitest) + React (Jest) 前端测试 |
| `ios-testing.md` | iOS 测试 (XCTest + Swift Concurrency) |
| `android-testing.md` | Android 测试 (JUnit + Espresso + Compose) |
| `e2e-testing.md` | E2E Playwright + Coolbuy PaaS 集成测试 |
---
## 测试金字塔
```
/\
/ \ E2E (少量)
/----\
/ \ 集成测试 (适量)
/--------\
/ \ 单元测试 (大量)
/------------\
```
| 类型 | 范围 | 速度 | 数量 |
|------|------|------|------|
| 单元测试 | 函数/方法 | 快 | 多 |
| 集成测试 | 模块交互 | 中 | 适量 |
| E2E 测试 | 完整流程 | 慢 | 少 |
---
## 测试命令速查
| 平台 | 命令 | 详见 |
|------|------|------|
| Go | `make test` / `go test ./...` | `go-testing.md` |
| Vue | `npm run test` | `frontend-testing.md` |
| React | `npm test` | `frontend-testing.md` |
| iOS | `xcodebuild test` | `ios-testing.md` |
| Android | `./gradlew test` | `android-testing.md` |
| E2E (通用) | `npm run test:e2e` | `e2e-testing.md` |
| E2E (Coolbuy PaaS) | `make e2e` | `e2e-testing.md` |
---
## Chrome DevTools MCP (AI 浏览器调试)
> Google 官方 MCP 服务器,让 AI 助手直接控制和检查 Chrome 浏览器。
```bash
claude mcp add chrome-devtools npx chrome-devtools-mcp@latest
```
| 分类 | 工具 | 说明 |
|------|------|------|
| **输入** | `click` / `fill` / `fill_form` / `hover` / `upload_file` | 页面交互 |
| **导航** | `navigate_page` / `new_page` / `list_pages` / `wait_for` | 页面导航 |
| **调试** | `evaluate_script` / `list_console_messages` / `take_screenshot` | 调试工具 |
| **网络** | `list_network_requests` / `get_network_request` | 网络分析 |
| **性能** | `performance_start_trace` / `performance_stop_trace` | 性能追踪 |
| **模拟** | `emulate_device` / `throttle_network` / `throttle_cpu` | 环境模拟 |
---
## 测试用例设计
### 等价类划分
| 输入 | 有效类 | 无效类 |
|------|--------|--------|
| 用户名 | 3-64字符 | <3, >64 |
| 年龄 | 0-150 | <0, >150 |
| 邮箱 | 有效格式 | 无效格式 |
### 边界值
```
范围 [1, 100]:
测试点: 0, 1, 2, 99, 100, 101
```
### 测试用例模板
```markdown
## TC-001: 用户登录成功
**前置条件**: 用户已注册
**步骤**:
1. 输入有效用户名
2. 输入有效密码
3. 点击登录
**预期**: 跳转到首页
**优先级**: P0
```
---
## 覆盖率目标
| 类型 | 目标 |
|------|------|
| 行覆盖 | >80% |
| 分支覆盖 | >70% |
| 函数覆盖 | >90% |
---
## 与 ai-proj 集成
```bash
# 创建测试任务
ai-proj task create --title "[模块] 单元测试"
# 记录测试结果
ai-proj task append-doc --id <taskId> --content "# 测试报告
- 覆盖率: 85%
- 通过: 42
- 失败: 0"
```
---
## 最佳实践
1. **测试金字塔** - 多单元测试,少 E2E
2. **测试隔离** - 每个测试独立
3. **命名清晰** - 描述预期行为
4. **快速反馈** - 测试要快
5. **持续集成** - 每次提交运行
6. **Biz 层禁止 Mock** - biz/service 层必须使用真实 PostgreSQL test DB + 真实 storemock 等于没测
7. **Mock 仅限 Handler 层** - handler 层可以 mock biz 接口 + httptest
7. **李宁测试用例** - Excel 导出见 `coolbuy-legacy` 技能的 `test-cases-excel.md`

View File

@@ -0,0 +1,145 @@
# Android 测试 (JUnit + Espresso)
## 运行测试
```bash
# 单元测试
./gradlew test
# UI 测试
./gradlew connectedAndroidTest
```
## 单元测试 (JUnit)
```kotlin
class TaskViewModelTest {
@get:Rule
val instantTaskRule = InstantTaskExecutorRule()
@get:Rule
val coroutineRule = MainCoroutineRule()
private lateinit var viewModel: TaskViewModel
private lateinit var repository: FakeTaskRepository
@Before
fun setup() {
repository = FakeTaskRepository()
viewModel = TaskViewModel(repository)
}
@Test
fun `fetchTasks updates state`() = runTest {
// Arrange
repository.addTasks(listOf(
Task(1, "Task 1"),
Task(2, "Task 2")
))
// Act
viewModel.fetchTasks()
// Assert
val tasks = viewModel.tasks.first()
assertEquals(2, tasks.size)
}
@Test
fun `createTask adds task`() = runTest {
// Act
viewModel.createTask("New Task")
// Assert
val tasks = viewModel.tasks.first()
assertTrue(tasks.any { it.title == "New Task" })
}
}
```
## UI 测试 (Espresso)
```kotlin
@RunWith(AndroidJUnit4::class)
@LargeTest
class TaskListActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(TaskListActivity::class.java)
@Test
fun displayTaskList() {
onView(withId(R.id.taskList))
.check(matches(isDisplayed()))
}
@Test
fun clickTask_opensDetail() {
onView(withId(R.id.taskList))
.perform(RecyclerViewActions.actionOnItemAtPosition<TaskViewHolder>(0, click()))
onView(withId(R.id.taskDetail))
.check(matches(isDisplayed()))
}
@Test
fun addTask_showsInList() {
// Click add button
onView(withId(R.id.addButton)).perform(click())
// Enter title
onView(withId(R.id.titleInput))
.perform(typeText("New Task"), closeSoftKeyboard())
// Save
onView(withId(R.id.saveButton)).perform(click())
// Verify in list
onView(withText("New Task"))
.check(matches(isDisplayed()))
}
}
```
## Compose UI 测试
```kotlin
@RunWith(AndroidJUnit4::class)
class TaskListScreenTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun taskList_displays() {
val tasks = listOf(
Task(1, "Task 1"),
Task(2, "Task 2")
)
composeRule.setContent {
TaskListScreen(tasks = tasks)
}
composeRule.onNodeWithText("Task 1").assertExists()
composeRule.onNodeWithText("Task 2").assertExists()
}
@Test
fun taskClick_callsOnClick() {
var clickedId: Int? = null
val tasks = listOf(Task(1, "Task 1"))
composeRule.setContent {
TaskListScreen(
tasks = tasks,
onTaskClick = { clickedId = it.id }
)
}
composeRule.onNodeWithText("Task 1").performClick()
assertEquals(1, clickedId)
}
}
```

View File

@@ -0,0 +1,169 @@
# E2E 测试 (Playwright)
## 通用 Playwright 配置
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
timeout: 30000,
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
],
})
```
## 通用 E2E 测试示例
```typescript
// login.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Login', () => {
test('successful login', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="username"]', 'testuser')
await page.fill('[data-testid="password"]', 'password')
await page.click('[data-testid="submit"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('.welcome')).toContainText('testuser')
})
test('invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="username"]', 'wrong')
await page.fill('[data-testid="password"]', 'wrong')
await page.click('[data-testid="submit"]')
await expect(page.locator('.error')).toBeVisible()
})
})
test.describe('Task Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="username"]', 'testuser')
await page.fill('[data-testid="password"]', 'password')
await page.click('[data-testid="submit"]')
})
test('create task', async ({ page }) => {
await page.click('[data-testid="new-task"]')
await page.fill('[data-testid="task-title"]', 'E2E Test Task')
await page.click('[data-testid="save"]')
await expect(page.locator('text=E2E Test Task')).toBeVisible()
})
})
```
---
## Coolbuy PaaS E2E 集成测试
> Playwright 全链路 E2E 测试独立环境DB + 端口),可与 dev 服务并行运行。
### 环境架构
| 服务 | Dev 端口 | E2E 端口 | DB |
|------|---------|---------|-----|
| Auth Service | 7089 | 7189 | coolbuy_paas_e2e |
| Foundation Service | 7090 | 7190 | coolbuy_paas_e2e |
| ERP Service | 7091 | 7191 | coolbuy_paas_e2e |
| Web Frontend | 4000 | 4010 | - |
**E2E DB 初始化**(首次/重置):
```bash
psql -U coolbuy-dev -d postgres -c "DROP DATABASE IF EXISTS coolbuy_paas_e2e;"
psql -U coolbuy-dev -d postgres -c "CREATE DATABASE coolbuy_paas_e2e OWNER \"coolbuy-dev\";"
pg_dump -U coolbuy-dev coolbuy_paas_local | psql -U coolbuy-dev coolbuy_paas_e2e
```
### 启动 / 停止 E2E 服务
```bash
make e2e-start # 启动全部 E2E 服务auth/foundation/erp/web
make e2e-stop # 停止全部 E2E 服务
make e2e-reset # 重置 DB 后启动
make e2e # 启动服务 + 运行全部测试
```
脚本位置:`scripts/start-e2e-services.sh` / `scripts/stop-e2e-services.sh`
### 运行测试
```bash
cd web
# 全部测试(无头模式)
npx playwright test
# 带 UI 调试
npx playwright test --headed
# 单个文件
npx playwright test tests/product-crud.spec.ts
# 查看 HTML 报告(注意:会启动 HTTP server需 Ctrl+C 退出)
npx playwright show-report
```
### Auth 自动登录
`tests/auth.setup.ts` 优先点击快速登录按钮(`VITE_ENABLE_QUICK_LOGIN=true`),降级为表单登录:
```typescript
// 快速登录E2E 环境默认开启)
const quickLoginBtn = page.locator('button, a').filter({ hasText: /李宁|lining|ID:2/i }).first();
if (await quickLoginBtn.isVisible({ timeout: 3000 })) {
await quickLoginBtn.click();
} else {
// 降级:填写 lining_admin / admin123验证码任意 4 位SkipVerify=true
}
await page.waitForURL(/\/tenant/, { timeout: 15000 });
await page.context().storageState({ path: authFile });
```
Session 保存至 `.auth/user.json`,后续测试自动复用,无需重复登录。
### 配置文件
| 文件 | 说明 |
|------|------|
| `web/.env.e2e` | E2E 环境变量(端口 / 快速登录开关) |
| `web/playwright.config.ts` | baseURL=localhost:4010reporter=[html, list] |
| `auth-service/api/etc/auth-api-e2e.yaml` | E2E auth 配置SkipVerify=true |
| `foundation-service/api/etc/foundation-api-e2e.yaml` | E2E foundation 配置 |
| `erp-service/configs/config.e2e.yaml` | E2E ERP 配置 |
### 测试结果解读
当前 **113 tests — 103 ✅ / 10 ❌**,已知失败项:
| 失败原因 | 涉及测试 |
|---------|---------|
| `/tenant/order/business` 路由 404页面未实现 | 业务订单列表 × 3、订单模块导航 |
| 预警管理无搜索表单组件 | 预警管理搜索/筛选 |
| 库存管理页无 table/empty 状态 | 仓库管理-库存管理 |
| 待审批订单 networkidle 超时(>60s | 待审批订单列表 |
| 数据权限/字段权限页渲染异常 | 系统管理 × 2 |
| 业务 CRM 页渲染异常 | 业务 CRM |
### 常见问题
| 问题 | 解决 |
|------|------|
| `Executable doesn't exist` | `npx playwright install chromium` |
| 端口 4010 被占用 | `make e2e-stop` 后重试 |
| GORM migration 失败 | 检查 DB 是否有旧约束名,手动 DROP CONSTRAINT 后重启服务 |
| HTML 报告进程不退出 | Playwright 在 report 模式会启动 HTTP server`Ctrl+C` 停止 |

View File

@@ -0,0 +1,174 @@
# 前端测试 (Vue + React)
## Vue 前端测试
### 测试框架
- **Vitest**: 测试运行器
- **Vue Test Utils**: 组件测试
- **MSW**: API Mock
### 运行测试
```bash
npm run test
npm run test:watch
npm run test:coverage
```
### 组件测试
```typescript
// UserList.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import UserList from './UserList.vue'
describe('UserList', () => {
it('renders user list', () => {
const wrapper = mount(UserList, {
props: {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
}
})
expect(wrapper.findAll('.user-item')).toHaveLength(2)
expect(wrapper.text()).toContain('Alice')
})
it('emits select event', async () => {
const wrapper = mount(UserList, {
props: { users: [{ id: 1, name: 'Alice' }] }
})
await wrapper.find('.user-item').trigger('click')
expect(wrapper.emitted('select')).toBeTruthy()
expect(wrapper.emitted('select')[0]).toEqual([1])
})
it('shows empty state', () => {
const wrapper = mount(UserList, {
props: { users: [] }
})
expect(wrapper.find('.empty-state').exists()).toBe(true)
})
})
```
### API Mock (MSW)
```typescript
// mocks/handlers.ts
import { rest } from 'msw'
export const handlers = [
rest.get('/api/v1/users', (req, res, ctx) => {
return res(ctx.json({
code: 0,
data: {
total: 2,
list: [{ id: 1, name: 'Alice' }]
}
}))
}),
rest.post('/api/v1/users', async (req, res, ctx) => {
const body = await req.json()
return res(ctx.json({
code: 0,
data: { id: 3, ...body }
}))
})
]
```
---
## React 前端测试
### 测试框架
- **Jest**: 测试运行器
- **React Testing Library**: 组件测试
- **Playwright**: E2E 测试
### 运行测试
```bash
npm test
npm run test:e2e
npm run test:e2e:headed
```
### 组件测试
```typescript
// TaskCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import TaskCard from './TaskCard'
describe('TaskCard', () => {
const mockTask = {
id: 1,
title: 'Test Task',
status: 'todo',
priority: 'high'
}
it('renders task title', () => {
render(<TaskCard task={mockTask} />)
expect(screen.getByText('Test Task')).toBeInTheDocument()
})
it('displays priority', () => {
render(<TaskCard task={mockTask} />)
expect(screen.getByText('high')).toHaveClass('priority-high')
})
it('calls onClick', () => {
const handleClick = jest.fn()
render(<TaskCard task={mockTask} onClick={handleClick} />)
fireEvent.click(screen.getByRole('article'))
expect(handleClick).toHaveBeenCalledWith(mockTask)
})
})
```
### Hook 测试
```typescript
// useTimer.test.ts
import { renderHook, act } from '@testing-library/react-hooks'
import { useTimer } from './useTimer'
describe('useTimer', () => {
beforeEach(() => jest.useFakeTimers())
afterEach(() => jest.useRealTimers())
it('starts timer', () => {
const { result } = renderHook(() => useTimer())
act(() => result.current.start())
expect(result.current.isRunning).toBe(true)
})
it('increments time', () => {
const { result } = renderHook(() => useTimer())
act(() => {
result.current.start()
jest.advanceTimersByTime(3000)
})
expect(result.current.seconds).toBe(3)
})
})
```

View File

@@ -0,0 +1,208 @@
# Go 后端测试
## 测试框架
- **testify**: 断言和套件
- **httptest**: HTTP 测试
- **gomock**: Mock 生成(仅用于 handler 层)
## ⚠️ Biz 层测试规则:禁止使用 Mock
**Biz/Service 层测试必须使用真实 PostgreSQL test DB不允许使用 mock store。**
Mock store 只是在测试你的 mock 实现,无法验证真实的 SQL 行为、事务、FK 约束等。
| 层 | 测试方式 | 原因 |
|----|---------|------|
| model/store | **test DB** (PostgreSQL) | 验证真实 SQL/ORM 行为 |
| biz/service | **test DB** (PostgreSQL) + 真实 store | 验证业务逻辑 + 真实数据交互 |
| handler | **mock biz + httptest** | 只测 HTTP 路由和参数绑定 |
```go
// ✅ 正确 — biz 层使用真实 test DB + 真实 store
func setupBiz(t *testing.T) (*SomeBiz, *gorm.DB) {
db := newTestDB(t)
s := store.NewSomeStore(db)
biz := NewSomeBiz(s)
t.Cleanup(func() {
db.Exec("DELETE FROM some_table WHERE tenant_id = ?", testTenantID)
})
return biz, db
}
// ❌ 错误 — biz 层使用 mock store等于没测
mockStore := store.NewMockIStore(ctrl)
mockStore.EXPECT().Get(gomock.Any(), id).Return(fakeData, nil)
biz := NewSomeBiz(mockStore)
```
### testdb_test.go 模板
```go
package biz
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
const testTenantID int64 = 99
func newTestDB(t *testing.T, models ...interface{}) *gorm.DB {
t.Helper()
dsn := os.Getenv("TEST_DATABASE_DSN")
if dsn == "" {
dsn = "host=localhost user=coolbuy-dev dbname=coolbuy_paas_test sslmode=disable"
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(models...))
return db
}
```
## 运行测试
```bash
# 所有测试
go test ./...
make test
# 带覆盖率
make cover
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# 特定包
go test -v ./internal/twms/biz/...
# 特定函数
go test -v -run TestFunctionName ./...
```
## Biz 层单元测试模板(真实 DB
```go
package biz
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"project/internal/user/model"
"project/internal/user/store"
)
func setupUserBiz(t *testing.T) (*UserBiz, *gorm.DB) {
db := newTestDB(t, &model.User{})
s := store.NewUserStore(db)
biz := NewUserBiz(s)
t.Cleanup(func() {
db.Exec("DELETE FROM users WHERE tenant_id = ?", testTenantID)
})
return biz, db
}
func createTestUser(t *testing.T, db *gorm.DB, username string) *model.User {
t.Helper()
user := &model.User{TenantID: testTenantID, Username: username, Email: username + "@test.com"}
require.NoError(t, db.Create(user).Error)
return user
}
func TestUserBiz_Get(t *testing.T) {
biz, db := setupUserBiz(t)
user := createTestUser(t, db, "john")
result, err := biz.Get(context.Background(), user.ID)
assert.NoError(t, err)
assert.Equal(t, "john", result.Username)
}
func TestUserBiz_Get_NotFound(t *testing.T) {
biz, _ := setupUserBiz(t)
_, err := biz.Get(context.Background(), 99999)
assert.Error(t, err)
}
```
## 表驱动测试
```go
func TestValidateUsername(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"valid", "john_doe", false},
{"too_short", "ab", true},
{"too_long", strings.Repeat("a", 65), true},
{"special_chars", "user@name", true},
{"empty", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUsername(tt.input)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
```
## HTTP Handler 测试
```go
func TestUserController_List(t *testing.T) {
gin.SetMode(gin.TestMode)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockBiz := biz.NewMockIBiz(ctrl)
mockUserBiz := biz.NewMockUserBiz(ctrl)
mockBiz.EXPECT().Users().Return(mockUserBiz).AnyTimes()
mockUserBiz.EXPECT().List(gomock.Any(), gomock.Any()).Return(&v1.ListUsersResponse{
Total: 1,
Users: []*v1.User{{Id: 1, Username: "test"}},
}, nil)
controller := NewUserController(mockBiz)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/v1/users?page=1&limit=10", nil)
controller.List(c)
assert.Equal(t, http.StatusOK, w.Code)
}
```
## Mock 生成
```bash
# 生成 Mock
mockgen -source=internal/twms/store/store.go \
-destination=internal/twms/store/mock_store.go \
-package=store
# go:generate 方式
//go:generate mockgen -source=store.go -destination=mock_store.go -package=store
```

View File

@@ -0,0 +1,157 @@
# iOS 测试 (XCTest + Swift Concurrency)
## 测试框架
- **XCTest**: Apple 官方测试框架
- **Swift Testing**: Swift 6 新测试框架 (可选)
- **ViewInspector**: SwiftUI 视图测试 (第三方)
## 运行测试
```bash
# 全部测试
xcodebuild test \
-scheme AI-Proj-iOS \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-quiet
# 特定测试类
xcodebuild test \
-scheme AI-Proj-iOS \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:AI-Proj-iOSTests/DashboardViewModelTests
# 覆盖率
xcodebuild test \
-scheme AI-Proj-iOS \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-enableCodeCoverage YES
```
## 项目测试结构 (AI-Proj-iOS)
```
AI-Proj-iOSTests/
├── Mocks/
│ ├── MockServices.swift # Mock 服务协议实现
│ └── MockNetworkService.swift
├── ViewModels/
│ ├── DashboardViewModelTests.swift
│ ├── TaskViewModelTests.swift
│ └── RequirementViewModelTests.swift
├── Services/
│ ├── TaskServiceTests.swift
│ └── DashboardAggregationServiceTests.swift
├── Models/
│ └── ModelDecodingTests.swift
└── Utilities/
└── DateFormatterTests.swift
```
## 关键模式
### 1. Mock 服务 — Result 注入
```swift
class MockTaskService: TaskServiceProtocol {
var fetchTasksResult: Result<TaskListResponse, Error> = .success(.mock)
func fetchTasks(...) async throws -> TaskListResponse {
switch fetchTasksResult {
case .success(let response): return response
case .failure(let error): throw error
}
}
}
```
所有 Mock 服务统一用 `Result` 属性控制成功/失败返回。
### 2. ViewModel 测试 — @MainActor + async
```swift
@MainActor
final class DashboardViewModelTests: XCTestCase {
var sut: DashboardViewModel!
var mockService: MockDashboardAggregationService!
override func setUp() {
super.setUp()
mockService = MockDashboardAggregationService()
sut = DashboardViewModel(dashboardService: mockService)
}
override func tearDown() {
sut = nil; mockService = nil
super.tearDown()
}
func testLoadDashboardData_Success() async {
mockService.fetchDashboardDataResult = .success(expectedData)
await sut.loadDashboardData()
XCTAssertFalse(sut.isLoading)
XCTAssertEqual(sut.todayStats.completedTasks, 5)
}
}
```
要点:`@MainActor` + `async` 测试方法 + setUp/tearDown 重置。
### 3. Mock 数据工厂 — 静态 `.mock()` 方法
```swift
extension TaskModel {
static func mock(id: Int = 1, status: TaskStatus = .todo) -> TaskModel {
TaskModel(id: id, title: "Mock Task", status: status, ...)
}
}
extension TaskListResponse {
static var mock: TaskListResponse {
TaskListResponse(tasks: [.mock(id: 1), .mock(id: 2)], total: 2, page: 1, pageSize: 20)
}
}
```
### 4. 模型解码测试 — JSON → Model
```swift
func testTaskModel_DecodesFromJSON() throws {
let json = """
{ "id": 123, "status": "in_progress", "priority": "high", ... }
""".data(using: .utf8)!
let task = try decoder.decode(TaskModel.self, from: json)
XCTAssertEqual(task.status, .inProgress)
}
```
### 5. SwiftUI 视图测试 — ViewInspector
```swift
extension EnhancedStatsSection: Inspectable {}
func testStatsSection_DisplaysCorrectValues() throws {
let view = EnhancedStatsSection(stats: .mock)
let text = try view.inspect().find(text: "5")
XCTAssertNotNil(text)
}
```
## 最佳实践
1. **@MainActor** — ViewModel 测试必须在主线程
2. **Mock 所有依赖** — 协议抽象 + Result 注入
3. **async/await** — 避免 XCTestExpectation 回调
4. **数据工厂**`.mock()` 静态方法,参数带默认值
5. **隔离测试** — setUp/tearDown 重置所有状态
6. **命名**`test<Method>_<Scenario>` 格式
## Xcode 快捷键
| 快捷键 | 操作 |
|--------|------|
| `Cmd + U` | 运行所有测试 |
| `Ctrl + Opt + Cmd + U` | 运行当前测试方法 |
| `Ctrl + Opt + Cmd + G` | 重新运行上次测试 |
| `Cmd + 6` | Test Navigator |

View File

@@ -0,0 +1,8 @@
{
"name": "dotfiles-plugin",
"description": "macOS 新机快速部署。用于 dotfiles 配置管理、install.sh 脚本维护、Claude Code 插件配置、MCP Server 配置。当用户提到新机部署、dotfiles、环境配置相关任务时自动激活。",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,469 @@
---
name: dotfiles
description: macOS 新机快速部署。用于 dotfiles 配置管理、install.sh 脚本维护、Claude Code 插件配置、MCP Server 配置。当用户提到新机部署、dotfiles、环境配置相关任务时自动激活。
---
# dotfiles - macOS 新机快速部署
自动化 macOS 开发环境配置,包括 Homebrew、Claude Code、插件、MCP Server 等。
## 核心功能
1. **配置文件管理** - dotfiles 符号链接、Git 配置同步
2. **Claude Code 配置** - 插件批量安装、MCP Server 配置模板
3. **开发工具安装** - Homebrew、mise、SSH 配置
4. **环境一致性** - 多台电脑配置同步
## 子文档索引
| 文档 | 说明 |
|------|------|
| [config-formats.md](docs/config-formats.md) | 配置文件格式规范(必读) |
| [testing.md](docs/testing.md) | 部署验证与测试方法 |
| [troubleshooting.md](docs/troubleshooting.md) | 常见问题与解决方案 |
---
## 快速开始
### 1. 克隆 dotfiles
```bash
git clone git@gitea.pipexerp.com:huangjun/dotfiles.git ~/.dotfiles
cd ~/.dotfiles
```
### 2. 执行安装脚本
```bash
./install.sh
```
**注意**
- 需要 sudo 权限(安装 Homebrew
- 需要手动输入密码(非交互式 SSH 无法自动化)
- 首次运行约 10-15 分钟
### 3. 配置 MCP Server
```bash
# 1. 复制环境变量模板
cp ~/.dotfiles/claude/.env.example ~/.dotfiles/claude/.env
# 2. 编辑填入真实 Token
vim ~/.dotfiles/claude/.env
# 3. 删除现有 settings.json如果已存在
rm ~/.claude/settings.json
# 4. 重新运行 install.sh 生成配置
cd ~/.dotfiles && ./install.sh
```
---
## 配置文件结构
```
~/.dotfiles/
├── install.sh # 主安装脚本
├── .zshrc # Zsh 配置
├── .gitconfig # Git 配置
├── Brewfile # Homebrew 软件清单
├── claude/
│ ├── plugins-list.yaml # Claude Code 插件清单
│ ├── settings.json.template # MCP Server 配置模板
│ ├── .env.example # 环境变量示例
│ └── .env # 实际环境变量(不提交)
├── claude-skills/ # Claude Code 技能目录
└── mise/
└── config.toml # mise 全局配置
```
---
## 关键组件说明
### 1. plugins-list.yaml
**作用**Claude Code 插件批量安装清单
**格式要求**(⚠️ 必须遵守):
```yaml
# ✅ 正确格式 - 统一 plugins: 段落
plugins:
- name: context7
marketplace: claude-plugins-official
description: 文档检索与上下文管理
- name: req-plugin
marketplace: claude-marketplace
description: 需求工作流管理
# 市场源 URL 配置
marketplace:
claude-marketplace:
url: git@gitea.pipexerp.com:huangjun/claude-marketplace.git
type: git
```
**常见错误**
```yaml
# ❌ 错误格式 - 分段格式install.sh 无法解析)
official_plugins:
- name: context7
marketplace: claude-plugins-official
private_plugins:
- name: req-plugin
marketplace: claude-marketplace
```
**详细规范**:见 [config-formats.md](docs/config-formats.md#plugins-listyaml)
### 2. settings.json.template
**作用**MCP Server 配置模板,由 install.sh 使用 envsubst 渲染
**格式要求**(⚠️ 必须遵守):
```json
{
"mcpServers": {
"ai-proj": {
"command": "node",
"args": [
"${HOME}/coding/qiudl/new-ai-proj/mcp-task-bridge/dist/index.js"
],
"env": {
"TASK_API_BASE": "${AI_PROJ_API_BASE}",
"TASK_API_TOKEN": "${AI_PROJ_API_TOKEN}"
}
}
}
}
```
**占位符格式**
- ✅ 使用 `${VAR}` (envsubst 兼容)
- ❌ 禁止使用 `{{VAR}}` (envsubst 不支持)
**详细规范**:见 [config-formats.md](docs/config-formats.md#settingsjsontemplate)
### 3. .env.example
**作用**:环境变量示例文件,用户复制为 `.env` 后填入真实值
**必需变量**
```bash
# ai-proj MCP Server
AI_PROJ_API_BASE=https://ai.pipexerp.com/api/v1
AI_PROJ_API_TOKEN=your_token_here
# WPS MCP Server
WPS_APP_ID=your_wps_app_id
WPS_APP_KEY=your_wps_app_key
# 飞书 MCP Server
FEISHU_APP_ID=your_feishu_app_id
FEISHU_APP_SECRET=your_feishu_secret
# 工作区路径(可选,默认 ~/workspace
WORKSPACE=~/coding/qiudl
```
**检查完整性**
```bash
# .env.example 必须包含 settings.json.template 中的所有 ${VAR}
diff <(grep -oE '\${[A-Z_]+}' ~/.dotfiles/claude/settings.json.template | sort -u) \
<(grep -oE '^[A-Z_]+=' ~/.dotfiles/claude/.env.example | sed 's/=$//' | sort)
```
---
## install.sh 工作流程
```
1. 检查/安装 Homebrew
└─ 需要 sudo 密码(手动输入)
2. 安装 Homebrew 软件
└─ 从 Brewfile 读取软件列表
3. 创建符号链接
├─ ~/.zshrc -> ~/.dotfiles/.zshrc
├─ ~/.gitconfig -> ~/.dotfiles/.gitconfig
└─ ~/.config/mise/config.toml -> ~/.dotfiles/mise/config.toml
4. 配置 Claude Code 插件
├─ 添加私有市场源claude-marketplace
└─ 批量安装 plugins-list.yaml 中的插件
5. 渲染 MCP 配置
├─ 检查 envsubst 是否安装
├─ 加载 ~/.dotfiles/claude/.env
└─ 渲染 settings.json.template -> ~/.claude/settings.json
6. 创建本地配置
└─ ~/.zshrc.local (MY_BRANCH, OTHER_BRANCH)
```
---
## 验证部署结果
### 快速检查
```bash
# 1. 检查符号链接
ls -la ~ | grep -E '\.zshrc|\.gitconfig'
# 2. 检查 Homebrew
which brew
brew --version
# 3. 检查 Claude Code
which claude
claude --version
# 4. 检查 MCP 配置
cat ~/.claude/settings.json | grep -E 'mcpServers|ai-proj'
# 5. 检查插件
claude /plugin list
```
### 完整验证
运行测试脚本(基于 REQ-20260220-0002 回归测试经验):
```bash
~/.dotfiles/scripts/verify-deployment.sh
```
**测试内容**
- TC-01: 私有市场源自动添加
- TC-02: plugins-list.yaml 格式验证
- TC-03: 插件批量安装验证
- TC-04: settings.json.template 格式验证
- TC-05: .env.example 完整性检查
- TC-06: .env 在 .gitignore 中
- TC-07: settings.json 模板渲染测试
- TC-08: 完整 install.sh 执行测试
详见 [testing.md](docs/testing.md)
---
## 常见问题
### 1. install.sh 执行时提示需要 sudo 密码
**原因**Homebrew 安装需要管理员权限
**解决**
- 本地执行:直接输入密码
- 远程 SSH无法自动化需用户在本地终端执行
### 2. envsubst 命令未找到
**原因**envsubst 由 gettext 包提供install.sh 会自动安装
**解决**
```bash
brew install gettext
```
### 3. 插件安装失败
**原因**
- 市场源未添加
- SSH 密钥未配置(私有仓库)
- 插件名称错误
**解决**
```bash
# 1. 检查市场源
ls ~/.claude/marketplaces/
# 2. 手动添加市场源
claude /plugin marketplace add git@gitea.pipexerp.com:huangjun/claude-marketplace.git
# 3. 配置 SSH 密钥
cat ~/.ssh/config # 检查 gitea.pipexerp.com 配置
ssh -T git@gitea.pipexerp.com # 测试 SSH 连接
```
### 4. settings.json 渲染后仍有 ${VAR}
**原因**
- .env 文件缺少对应变量
- 变量名拼写错误
**解决**
```bash
# 检查 .env 是否包含所有必需变量
diff <(grep -oE '\${[A-Z_]+}' ~/.dotfiles/claude/settings.json.template | sed 's/[${}]//g' | sort -u) \
<(grep -oE '^[A-Z_]+=' ~/.dotfiles/claude/.env | sed 's/=.*//' | sort)
```
更多问题见 [troubleshooting.md](docs/troubleshooting.md)
---
## 最佳实践
### 1. 版本控制
**提交内容**
- ✅ 配置模板(.env.example, settings.json.template
- ✅ 安装脚本install.sh
- ✅ 插件清单plugins-list.yaml
- ❌ 敏感信息(.env, *.pem, *.key
**检查 .gitignore**
```bash
# 确保以下条目存在
grep -E '^\\.env$|^claude/\\.env$|^\\.pem$' ~/.dotfiles/.gitignore
```
### 2. 多机同步
**场景**:澳洲电脑 + 中国电脑 + 成都 Mac Mini
**策略**
```bash
# 1. 统一配置由 Git 同步
git pull origin main
# 2. 机器特定配置写入 ~/.zshrc.local
echo "MY_BRANCH=au-dev" >> ~/.zshrc.local
echo "OTHER_BRANCH=cn-dev" >> ~/.zshrc.local
# 3. .zshrc 自动加载 .zshrc.local
[ -f ~/.zshrc.local ] && source ~/.zshrc.local
```
### 3. 测试驱动配置
**修改配置文件前**
1. 在测试机上验证(如成都 Mac Mini
2. 运行回归测试脚本
3. 确认所有测试通过后再提交
**示例工作流**(基于 REQ-20260220-0002
```bash
# 1. 修改配置
vim ~/.dotfiles/claude/plugins-list.yaml
# 2. 提交到 Git
git add claude/plugins-list.yaml
git commit -m "fix: 统一 plugins-list.yaml 格式"
# 3. 推送到远程
git push origin main
# 4. 在测试机验证
ssh chengdu "cd ~/.dotfiles && git pull && ./scripts/verify-deployment.sh"
```
---
## 相关技能
| 技能 | 用途 |
|------|------|
| `ops-tools` | 服务器运维工具 |
| `dev-test` | 软件测试方法论 |
| `req` | 需求工作流管理 |
| `skill-manager` | 技能自我进化管理 |
---
## 经验教训REQ-20260220-0002
### 问题1: plugins-list.yaml 格式不兼容
**现象**install.sh 无法解析插件列表
**原因**使用了分段格式official_plugins/private_pluginsinstall.sh 只解析 `plugins:` 段落
**解决**:统一使用 `plugins:` 段落格式
**预防**
```bash
# 格式验证脚本
grep -E '^official_plugins:|^private_plugins:' ~/.dotfiles/claude/plugins-list.yaml && \
echo "❌ 检测到旧格式" || echo "✅ 格式正确"
```
### 问题2: settings.json.template 占位符错误
**现象**envsubst 渲染后仍有 {{VAR}}
**原因**:使用了 {{VAR}} 格式envsubst 只支持 ${VAR}
**解决**:全局替换 `{{` -> `${`, `}}` -> `}`
**预防**
```bash
# 检查占位符格式
grep '{{' ~/.dotfiles/claude/settings.json.template && \
echo "❌ 使用了不兼容格式" || echo "✅ 格式正确"
```
### 问题3: .env.example 不完整
**现象**:渲染后 settings.json 包含未替换的 ${VAR}
**原因**.env.example 缺少某些变量定义
**解决**:确保 .env.example 包含 settings.json.template 中的所有变量
**预防**
```bash
# 完整性检查脚本(见 scripts/check-env-completeness.sh
~/.dotfiles/scripts/check-env-completeness.sh
```
### 问题4: .env 未加入 .gitignore
**现象**:敏感 Token 被提交到 Git
**原因**.gitignore 缺少 .env 条目
**解决**:添加 `.env``claude/.env` 到 .gitignore
**预防**
```bash
# Git 提交前 hook 检查
git diff --cached --name-only | grep -E '\\.env$' && \
echo "⚠️ 警告:尝试提交 .env 文件" && exit 1
```
---
## 部署检查清单
部署到新机器前,确认以下事项:
- [ ] dotfiles 仓库已克隆到 ~/.dotfiles
- [ ] SSH 密钥已配置Gitea 访问)
- [ ] 用户有 sudo 权限
- [ ] 已复制 .env.example 为 .env 并填入真实 Token
- [ ] plugins-list.yaml 格式正确(统一 plugins: 段落)
- [ ] settings.json.template 使用 ${VAR} 格式
- [ ] .env 在 .gitignore 中
- [ ] 网络可访问 Homebrew、Gitea、MCP API
部署后验证:
- [ ] 运行 `~/.dotfiles/scripts/verify-deployment.sh`
- [ ] 检查所有测试用例通过
- [ ] 手动验证 Claude Code 能连接 MCP Server
- [ ] 手动验证插件已正确安装
---
**更新时间**: 2026-02-20
**维护者**: qiudl
**数据来源**: REQ-20260220-0002 成都机器回归测试

View File

@@ -0,0 +1,14 @@
{
"name": "doubao-voice-plugin",
"description": "Doubao (豆包) Voice API integration for TTS and ASR",
"version": "1.0.0",
"author": {
"name": "qiudl"
},
"skills": [
{
"name": "doubao-voice",
"path": "./skills/SKILL.md"
}
]
}

54
plugins/doubao-voice-plugin/.gitignore vendored Normal file
View File

@@ -0,0 +1,54 @@
# 音频文件(生成的测试输出)
*.mp3
*.wav
*.pcm
# 测试脚本(仅本地使用)
scripts/test_*.py
scripts/check_credentials.py
scripts/README_TEST.md
# 系统文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# IDE
.vscode/
.idea/
*.swp
*.swo
# 环境配置(包含凭证的本地文件)
setup_env.local.sh
.env
.env.local
# 测试生成的文件
*.log
test_output/

View File

@@ -0,0 +1,201 @@
# 部署指南
## 在另一台电脑上使用这个 Skill
### ✅ 可以直接使用吗?
**大部分功能可以直接使用!** 但需要做一些简单的配置。
---
## 📋 部署步骤
### 1⃣ 将插件复制到新电脑
```bash
# 方式1: 从Git克隆
git clone <repo-url> doubao-voice-plugin
# 方式2: 复制文件夹
cp -r doubao-voice-plugin /path/to/new/location
```
### 2⃣ 安装依赖
**核心依赖** (必需):
```bash
pip3 install requests
```
**可选依赖** (仅用voice_converter_sdk.py时需要):
```bash
pip3 install volcengine
```
**检查是否安装成功**:
```bash
python3 -c "import requests; print('✅ requests 已安装')"
```
### 3⃣ 配置凭证
创建本地配置文件:
```bash
cd scripts
cp setup_env.local.sh.example setup_env.local.sh
```
编辑 `setup_env.local.sh`,填入您的火山引擎凭证:
```bash
export DOUBAO_APP_ID="your_app_id"
export DOUBAO_ACCESS_TOKEN="your_access_token"
```
### 4⃣ 使用
```bash
# 加载环境变量
source scripts/setup_env.local.sh
# 文字转语音
python3 scripts/voice_converter.py tts "你好世界" -o hello.mp3
# 语音转文字需先启用ASR服务
python3 scripts/voice_converter.py asr audio.mp3
```
---
## 🔧 系统要求
| 需求 | 版本 | 状态 |
|------|------|------|
| **Python** | 3.6+ | ✅ 必需 |
| **requests** | 任意版本 | ✅ 必需 |
| **volcengine** | 任意版本 | ⚠️ 可选 |
| **操作系统** | Linux/Mac/Windows | ✅ 都支持 |
---
## 🚨 常见问题
### Q: 错误 "ModuleNotFoundError: No module named 'requests'"
**解决**:
```bash
pip3 install requests
```
### Q: 错误 "DOUBAO_APP_ID not found"
**解决**:
```bash
# 检查环境变量
echo $DOUBAO_APP_ID
# 如果为空,重新加载配置
source setup_env.local.sh
```
### Q: 为什么 ASR 不工作?
**原因**: 需要在火山引擎控制台启用 ASR 服务
**解决**: 访问 https://console.volcengine.com/speech/service启用语音识别服务
### Q: 可以在 Windows 上使用吗?
**可以!** 但环境变量设置方式不同:
```batch
REM Windows CMD
set DOUBAO_APP_ID=your_app_id
set DOUBAO_ACCESS_TOKEN=your_access_token
python scripts\voice_converter.py tts "你好" -o hello.mp3
```
或在 PowerShell
```powershell
$env:DOUBAO_APP_ID="your_app_id"
$env:DOUBAO_ACCESS_TOKEN="your_access_token"
python scripts/voice_converter.py tts "你好" -o hello.mp3
```
### Q: 如何在 Docker 中使用?
**Dockerfile 示例**:
```dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY . .
RUN pip install requests
ENV DOUBAO_APP_ID=${DOUBAO_APP_ID}
ENV DOUBAO_ACCESS_TOKEN=${DOUBAO_ACCESS_TOKEN}
ENTRYPOINT ["python", "scripts/voice_converter.py"]
```
运行:
```bash
docker build -t doubao-voice .
docker run -e DOUBAO_APP_ID=xxx -e DOUBAO_ACCESS_TOKEN=xxx doubao-voice tts "你好"
```
---
## 📦 三种使用方式
### 方式 1: 命令行 (推荐简单使用)
```bash
python3 scripts/voice_converter.py tts "文本" -o output.mp3
```
### 方式 2: Python 模块导入
```python
import sys
sys.path.insert(0, 'scripts')
from voice_converter import DoubaoVoiceConverter
converter = DoubaoVoiceConverter()
converter.text_to_speech("你好世界", output_file="hello.mp3")
```
### 方式 3: Claude Code Skill (自动)
如果安装在 Claude Code 的 plugins 目录,会自动识别为 Skill
```bash
# 用户说: "把这段话转成语音:你好世界"
# → 自动调用 TTS API
```
---
## 🔐 安全提示
**推荐做法**:
- 凭证存储在 `.local` 文件中(不在 Git 中)
- 使用环境变量而不是硬编码
- 定期更新 Access Token
**不要做**:
- 不要把凭证提交到 Git
- 不要在脚本中硬编码凭证
- 不要分享包含凭证的配置文件
---
## 📝 最小化部署清单
```bash
✅ 复制文件夹
✅ pip install requests
✅ 复制并编辑 setup_env.local.sh
source setup_env.local.sh
✅ python3 scripts/voice_converter.py tts "测试"
✅ 成功!
```
---
## 🆘 如需帮助
1. 检查 README.md (用户文档)
2. 查看 skills/SKILL.md (API 文档)
3. 查看 STATUS.md (开发状态)

View File

@@ -0,0 +1,196 @@
# Git 提交指南
## 📋 提交清单
### ✅ 应该提交的文件
```bash
git add .
git status # 确认以下文件已staged
应包含:
- .claude-plugin/plugin.json # 插件配置
- skills/SKILL.md # 技能文档
- scripts/voice_converter.py # 核心工具
- scripts/voice_converter_v2.py # 备选方案
- scripts/voice_converter_sdk.py # 备选方案
- scripts/check_credentials.py # 诊断工具
- scripts/test_services.py # 服务测试
- scripts/test_v3_debug.py # V3调试工具
- scripts/setup_env.sh # 示例脚本(占位符版本)
- scripts/setup_env.local.sh.example # 本地配置模板
- README.md # 用户文档
- STATUS.md # 开发状态
- .gitignore # Git忽略规则
- GIT_GUIDE.md # 本文件
```
### ❌ 被自动忽略的文件(勿手动提交)
```bash
# .gitignore 已配置,以下文件不会被提交:
- *.mp3, *.wav, *.pcm # 音频文件
- .DS_Store # 系统文件
- setup_env.local.sh # 本地凭证文件
- .env, .env.local # 环境变量文件
- __pycache__/ # Python缓存
- .vscode/, .idea/ # IDE配置
```
---
## 🔐 凭证管理 (重要!)
### 本地使用流程
```bash
# 1. 基于模板创建本地配置文件
cd scripts
cp setup_env.local.sh.example setup_env.local.sh
# 2. 编辑本地文件,填入您的真实凭证
nano setup_env.local.sh # 或用您喜欢的编辑器
# 3. 本地使用时source 本地文件
source setup_env.local.sh
# 4. 验证注意setup_env.local.sh 在 .gitignore 中)
git status # 应该看不到 setup_env.local.sh
```
### 关键安全要点
**做这些**:
- 凭证存储在本地的 `.local` 文件中
- 凭证存储在环境变量中(不硬编码)
- 公开文件只包含占位符 `your_app_id`, `your_access_token`
- 定期检查 git status 确保没有凭证被暴露
**不要做这些**:
- 不要把真实凭证提交到 Git
- 不要硬编码凭证在 Python 文件中
- 不要修改 .gitignore让敏感文件被跟踪
- 不要分享包含凭证的 shell 脚本
---
## 📝 提交步骤
```bash
# 1. 确保您创建了本地配置文件
cd /Users/junhuang/coolbuy/claude-marketplace/plugins/doubao-voice-plugin/scripts
cp setup_env.local.sh.example setup_env.local.sh
# 编辑 setup_env.local.sh填入您的凭证
# 2. 检查状态
cd ..
git status
# 3. 提交所有应提交的文件
git add .
# 4. 验证没有凭证泄露
git diff --cached | grep -i "DOUBAO_APP_ID\|DOUBAO_ACCESS_TOKEN\|AKLT\|VOLCENGINE"
# 如果有输出,说明有凭证要被提交,请取消并修改
# 5. 提交
git commit -m "feat: Add Doubao Voice plugin with TTS/ASR support"
# 6. 再次检查
git show HEAD # 确认提交内容
# 7. 推送
git push origin main
```
---
## 🔍 验证清单
提交前,运行以下命令确认安全:
```bash
# 检查是否有真实凭证在staged文件中
git diff --cached | grep -E "2288996168|LlDjcX-_UEnn4OW87iMorpXccQUilaHX|AKLTMGQ3"
# 正常情况下应该没有输出
# 检查 setup_env.local.sh 是否被忽略
git status | grep setup_env.local.sh
# 应该看不到这个文件
# 检查 .gitignore 配置是否正确
cat .gitignore | grep "setup_env.local"
# 应该看到这一行
# 查看即将提交的文件列表
git ls-files
# 确认关键文件都在其中,但不包含 setup_env.local.sh
```
---
## 使用说明(给其他用户)
在您发布插件后,其他用户应该:
```bash
# 1. 克隆插件
git clone <repo-url> doubao-voice-plugin
cd doubao-voice-plugin/scripts
# 2. 创建本地配置
cp setup_env.local.sh.example setup_env.local.sh
# 3. 编辑配置,填入他们自己的凭证
vim setup_env.local.sh
# 4. 配置环境变量
source setup_env.local.sh
# 5. 测试功能
python3 voice_converter.py tts "测试"
# 6. setup_env.local.sh 不会被版本控制跟踪
git status # 看不到 setup_env.local.sh ✅
```
---
## FAQ
**Q: 我不小心提交了凭证怎么办?**
A: 立即执行:
```bash
# 从 Git 历史中移除敏感文件
git rm --cached scripts/setup_env.local.sh
git commit --amend -m "Remove sensitive file"
# 更改您的火山引擎 Access Token出于安全考虑
# 在控制台重新生成新的 token
```
**Q: 为什么需要 setup_env.local.sh.example**
A: 这样其他用户可以看到配置文件应该包含哪些环境变量,而不会暴露任何真实凭证。
**Q: 可以把凭证放在 ~/.bashrc 里吗?**
A: 可以,但 setup_env.local.sh 更加灵活,易于项目专用配置。
**Q: 如何在 CI/CD 中使用敏感凭证?**
A: 在 CI/CD 平台GitHub Actions, GitLab CI等中使用 Secrets/Variables 功能,不要在代码中硬编码。
---
## 总结
**已完成的安全措施**
1. ✓ .gitignore 配置了敏感文件忽略规则
2. ✓ setup_env.sh 改为占位符版本
3. ✓ 创建了 setup_env.local.sh.example 模板
4. ✓ 所有代码文件使用环境变量读取凭证
5. ✓ 提供了清晰的本地配置说明
现在可以安全地提交到 Git🎉

View File

@@ -0,0 +1,182 @@
# 豆包语音插件 (Doubao Voice Plugin)
火山引擎豆包语音API集成插件支持文字转语音(TTS)和唱歌功能。
## 功能特性
- **✅ 语音合成 (TTS)**: 文字转语音,支持多种音色 - **已测试可用**
- **🎵 唱歌**: 让豆包唱歌,支持实时语音交互 - **已开通端到端大模型**
- **简单易用**: 命令行工具,一行命令即可使用
- **多种音色**: 支持女声/男声等多种基础音色
- **实时交互**: 支持与豆包进行实时对话和唱歌
## 快速开始
### 1. 获取API凭证
访问 [火山引擎控制台](https://console.volcengine.com/speech/app) 创建应用并获取:
- **App ID** (数字)
- **Access Token** (长字符串)
开通所需服务:
1. 在控制台勾选 **"语音合成"** 服务 (TTS)
### 2. 配置环境变量
**方式1: 使用配置脚本 (推荐)**
```bash
cd scripts
source setup_env.sh # 自动设置环境变量
```
**方式2: 手动设置**
```bash
export DOUBAO_APP_ID="your_app_id"
export DOUBAO_ACCESS_TOKEN="your_access_token"
```
### 3. 安装依赖
```bash
pip3 install requests --break-system-packages
```
### 4. 检查凭证
```bash
# 检查凭证配置
python3 scripts/check_credentials.py
```
### 5. 使用示例
#### TTS 文字转语音(命令行)
```bash
cd scripts
# 基础用法 - ✅ 已测试可用
python3 voice_converter.py tts "你好,我是豆包语音助手" -o output.mp3
# 使用不同音色
python3 voice_converter.py tts "测试男声" -o male.mp3 -v BV701_V2_streaming
```
#### 唱歌(命令行)🎵
```bash
cd scripts
# 让豆包唱歌
python3 singing.py sing "请唱一首关于春天的歌" -o spring.mp3
# 交互式唱歌模式(实时对话)
python3 singing.py interactive
```
#### Python 代码方式
```python
# TTS - 文字转语音
from scripts.voice_converter import DoubaoVoiceConverter
converter = DoubaoVoiceConverter()
audio_file = converter.text_to_speech("你好,欢迎使用豆包", output_file="hello.mp3")
# 唱歌
import asyncio
from scripts.singing import DoubaoSinging
async def sing():
singing = DoubaoSinging()
audio_file = await singing.sing("请唱一首情歌", output_file="love_song.mp3")
asyncio.run(sing())
```
## 自然语言调用
在 Claude Code 中可以使用自然语言调用:
**TTS 文字转语音**:
- "把这段话转成语音:你好世界"
- "用温柔女声合成语音"
- "用男声朗读这段文字"
**唱歌**:
- "请唱一首关于春天的歌"
- "唱一个温柔的摇篮曲"
- "开启与豆包的实时语音对话模式"
示例:
```
用户: "帮我把'欢迎使用豆包语音'转成语音"
Claude: 调用TTS服务生成output.mp3
```
## 价格说明
### TTS (语音合成)
- 大模型并发版: 2000元/并发/月
- 按量付费: 按字符数计费
### 免费试用
新用户开通服务后可获得免费额度。
## 支持的音色
| 音色代码 | 描述 | 场景 | 状态 |
|---------|------|------|------|
| BV700_V2_streaming | 通用女声 | 通用场景 | ✅ V1 可用 |
| BV701_V2_streaming | 通用男声 | 通用场景 | ✅ V1 可用 |
| BV406_streaming | 温柔女声 | 客服、助手 | ✅ V1 可用 |
| BV158_streaming | 活泼女声 | 教育、娱乐 | ✅ V1 可用 |
| BV115_streaming | 磁性男声 | 新闻、播音 | ✅ V1 可用 |
**注意**: 豆包2.0高级音色需要使用V3 API目前正在调试中。
## 常见问题
### TTS 返回 "requested resource not granted"
**解决方法**: 在控制台勾选"语音合成"服务选项
### Authorization 头格式错误
确保使用 `Bearer;{token}` 格式(注意分号),而不是 `Bearer {token}`
### 环境变量未生效
```bash
# 检查环境变量
echo $DOUBAO_APP_ID
echo $DOUBAO_ACCESS_TOKEN
# 如果为空,重新设置
source setup_env.sh
```
## API 版本说明
### V1 API (当前使用) ✅
- **状态**: 已测试,稳定可用
- **认证**: Bearer Token
- **音色**: 支持基础音色
- **推荐**: 日常使用推荐
### V3 API (豆包2.0) ⚠️
- **状态**: 调试中,存在 "get resource id empty" 问题
- **认证**: Bearer Token + Resource-Id
- **音色**: 支持豆包2.0高级音色
- **说明**: 需要联系火山引擎技术支持获取正确配置
## 技术支持
- [官方文档](https://www.volcengine.com/docs/6561/1359369)
- [控制台](https://console.volcengine.com/speech/app)
- [计费说明](https://www.volcengine.com/docs/6561/1359370)
## 许可证
本插件遵循 MIT 许可证。
## 作者
qiudl @ zhiyuncai.com

View File

@@ -0,0 +1,200 @@
# 豆包语音插件 - 开发状态
**更新时间**: 2026-02-07
**版本**: 1.0.0
---
## ✅ 已完成功能
### 1. TTS (文字转语音) - 完全可用 ✅
**测试状态**: 通过
**API版本**: V1
**可用音色**:
- BV700_V2_streaming (通用女声)
- BV701_V2_streaming (通用男声)
- BV406_streaming (温柔女声)
- BV158_streaming (活泼女声)
- BV115_streaming (磁性男声)
**测试命令**:
```bash
source scripts/setup_env.sh
python3 scripts/voice_converter.py tts "你好世界" -o hello.mp3
```
**测试结果**:
- ✅ HTTP 200 OK
- ✅ Code 3000 Success
- ✅ 成功生成 MP3 文件
- ✅ 音质正常
---
## ⚠️ 待完成功能
### 2. ASR (语音转文字) - 待启用服务
**问题**: Code 1001 - "requested resource not granted"
**原因**: ASR 服务未在火山引擎控制台正确启用
**解决步骤**:
1. 访问: https://console.volcengine.com/speech/service
2. 找到 "语音识别 (ASR)" 服务
3. 确保服务已启用并勾选必要选项
4. 等待服务生效(可能需要几分钟)
5. 重新测试
**测试命令** (服务启用后):
```bash
python3 scripts/voice_converter.py asr audio.mp3
```
---
### 3. V3 API / 豆包2.0音色 - 调试中
**问题**: Code 45000000 - "get resource id empty"
**已尝试的方法**:
- [x] Resource-Id header
- [x] X-Resource-Id header
- [x] resource_id query parameter
- [x] resource_id in app config
- [x] 多种 resource_id 值: volc.bigmodel.tts, volc.seed-tts.default, volc.tts.default
**当前状态**: 所有方法均返回相同错误
**可能原因**:
1. V3 API 可能需要不同的认证方式 (IAM签名)
2. 需要特殊的服务实例配置
3. Resource-Id 的获取或配置方法不正确
**建议**:
- 联系火山引擎技术支持获取 V3 API 正确配置方法
- 或继续使用 V1 API (已满足基本需求)
---
## 📁 项目文件结构
```
plugins/doubao-voice-plugin/
├── .claude-plugin/
│ └── plugin.json # 插件元数据
├── skills/
│ └── SKILL.md # 技能定义和文档
├── scripts/
│ ├── voice_converter.py # 主转换工具 (V1 API, 可用)
│ ├── voice_converter_v2.py # 手动签名版本 (待测试)
│ ├── voice_converter_sdk.py # SDK版本 (待测试)
│ ├── check_credentials.py # 凭证检查工具
│ ├── test_services.py # 服务状态测试
│ ├── test_v3_debug.py # V3 API 调试脚本
│ ├── setup_env.sh # 环境变量配置脚本
│ └── README_TEST.md # 测试报告
├── README.md # 用户文档
└── STATUS.md # 本文件 (开发状态)
```
---
## 🔧 诊断工具
### 检查凭证配置
```bash
python3 scripts/check_credentials.py
```
显示当前环境变量配置状态
### 测试服务状态
```bash
python3 scripts/test_services.py
```
测试 TTS 和 ASR 服务是否可用
### V3 API 调试
```bash
python3 scripts/test_v3_debug.py
```
测试多种 V3 API 配置方式
---
## 📊 当前凭证配置
```bash
DOUBAO_APP_ID="your_app_id"
DOUBAO_ACCESS_TOKEN="your_access_token"
# V3 可选配置 (暂不可用)
# DOUBAO_USE_V3="true"
# DOUBAO_RESOURCE_ID="volc.bigmodel.tts"
```
**Access Key 信息** (用于签名认证,暂未使用):
- Access Key ID: your_access_key_id
- Secret Access Key: your_secret_access_key
---
## 🎯 下一步计划
### 立即可用
1.**使用 TTS 功能**
- 集成到应用中
- 测试不同音色
- 生产环境部署
### 短期目标 (1-3天)
2. ⚠️ **启用 ASR 服务**
- 在控制台启用服务
- 测试语音识别功能
- 完善错误处理
### 长期目标 (可选)
3. 🔄 **V3 API 支持**
- 联系火山引擎技术支持
- 获取正确的 Resource-Id 配置方法
- 支持豆包2.0高级音色
---
## 📞 技术支持
### 火山引擎
- 文档: https://www.volcengine.com/docs/6561/1329505
- 控制台: https://console.volcengine.com/speech/app
- 服务管理: https://console.volcengine.com/speech/service
### 常见问题解决
1. **TTS 可用但 ASR 不可用**
- 检查控制台 ASR 服务是否启用
- 确认勾选了"语音识别"选项
2. **V3 API 持续报错**
- 暂时使用 V1 API
- 联系火山引擎技术支持
3. **认证失败**
- 检查环境变量是否正确设置
- 确认 Access Token 格式正确
- 注意 Authorization header 使用 `Bearer;{token}` (有分号)
---
## ✨ 总结
**当前可用**: TTS (文字转语音) 功能完全可用,可以投入使用
**待解决**:
1. 在控制台启用 ASR 服务
2. (可选) 解决 V3 API 配置问题
**建议**: 先使用 V1 API 的 TTS 功能满足基本语音合成需求。ASR 功能在控制台启用服务后即可使用。V3 API 的豆包2.0音色为可选功能,可以后续再解决。
---
*Generated by Claude Code on 2026-02-07*

View File

@@ -0,0 +1,186 @@
# 豆包语音工具使用指南
简单易用的豆包语音命令行工具,支持**文字转语音(TTS)**和**唱歌**。
## 快速开始
### 1. 配置环境变量
```bash
# 在 ~/.zshrc 或 ~/.bashrc 中添加
export DOUBAO_APP_ID="your_app_id"
export DOUBAO_ACCESS_TOKEN="your_access_token"
# 使配置生效
source ~/.zshrc
```
### 2. 安装依赖
```bash
pip install requests
```
## 使用方法
### 📝 文字转语音 (TTS)
**基础用法:**
```bash
python voice_converter.py tts "你好,我是豆包语音助手"
```
**指定输出文件和音色:**
```bash
python voice_converter.py tts "欢迎使用豆包语音" -o welcome.mp3 -v BV701_V2_streaming
```
**可用音色:**
- `BV700_V2_streaming` - 通用女声(默认,推荐)
- `BV701_V2_streaming` - 通用男声
- `BV406_streaming` - 温柔女声
- `BV158_streaming` - 活泼女声
- `BV115_streaming` - 磁性男声
### 🎵 唱歌 (Singing)
**基础用法:**
```bash
python singing.py sing "请唱一首关于春天的歌"
```
**指定输出文件:**
```bash
python singing.py sing "唱一个温柔的摇篮曲" -o lullaby.mp3
```
**交互式模式(实时对话):**
```bash
python singing.py interactive
```
在交互模式下可以自然地与豆包对话,要求她唱歌、讲故事等。输入 `quit` 退出。
## Python 代码调用
```python
# TTS - 文字转语音
from voice_converter import DoubaoVoiceConverter
converter = DoubaoVoiceConverter()
audio_file = converter.text_to_speech(
"你好,欢迎使用豆包语音",
output_file="hello.mp3",
voice_type="BV700_V2_streaming"
)
print(f"生成语音: {audio_file}")
# 唱歌
import asyncio
from singing import DoubaoSinging
async def main():
singing = DoubaoSinging()
# 让豆包唱歌
audio_file = await singing.sing(
"请唱一首情歌",
output_file="love_song.mp3",
language="zh-CN"
)
print(f"唱歌完成: {audio_file}")
# 或启动交互模式
# await singing.interactive_singing()
asyncio.run(main())
```
## 完整示例
### 示例1生成通知语音
```bash
# 生成女声通知
python voice_converter.py tts "您有一条新消息,请注意查收" -o notification.mp3
# 生成男声通知
python voice_converter.py tts "系统将在5分钟后进行维护" -o maintenance.mp3 -v BV701_V2_streaming
```
### 示例2唱歌
```bash
# 让豆包唱一首情歌
python singing.py sing "请唱一首温柔的情歌" -o love_song.mp3
# 让豆包唱一首儿歌
python singing.py sing "唱一首欢快的儿歌" -o kids_song.mp3
# 启动交互式模式与豆包对话
python singing.py interactive
```
## 错误处理
### 常见错误
**1. 环境变量未设置**
```
❌ 错误: 请先设置环境变量:
export DOUBAO_APP_ID='your_app_id'
export DOUBAO_ACCESS_TOKEN='your_access_token'
```
**解决:** 确保已正确设置环境变量并 `source ~/.zshrc`
**2. API 调用失败**
```
❌ 错误: TTS 失败 (code: 4001): Invalid token
```
**解决:** 检查 Access Token 是否正确或已过期
## 技术参数
### 音频格式要求
**TTS 输出:**
- 格式MP3
- 采样率16000 Hz
- 声道:单声道
### API 限制
- **TTS**: 单次最长 5000 字符
- **并发限制**: 根据购买的并发数
## 在 Claude Code 中使用
在 Claude Code 中可以直接用自然语言调用:
**TTS - 文字转语音**:
```
"把这段话转成语音:你好世界"
"用温柔女声合成:欢迎光临"
```
**唱歌**:
```
"请唱一首关于春天的歌"
"唱一个温柔的摇篮曲"
"开启与豆包的实时语音对话模式"
```
## 获取 API 凭证
1. 访问 [火山引擎控制台](https://console.volcengine.com/speech/app)
2. 创建应用
3. 获取 App ID 和 Access Token
4. 开通所需服务:
- 豆包语音合成模型2.0
## 参考链接
- [火山引擎豆包语音文档](https://www.volcengine.com/docs/6561)
- [API 接口文档](https://www.volcengine.com/docs/6561/1096680)
- [计费说明](https://www.volcengine.com/docs/6561/1359370)

View File

@@ -0,0 +1,21 @@
#!/bin/bash
# 豆包语音 API 环境变量配置(本地版本)
#
# 使用说明:
# 1. 复制本文件: cp setup_env.local.sh.example setup_env.local.sh
# 2. 编辑 setup_env.local.sh填入您的真实凭证
# 3. 运行: source setup_env.local.sh
# 4. .gitignore 已配置忽略 setup_env.local.sh所以您的凭证不会被提交到 Git
# ⚠️ 重要:请在下面填入您的真实凭证(仅本地使用)
export DOUBAO_APP_ID="your_app_id_here"
export DOUBAO_ACCESS_TOKEN="your_access_token_here"
# V3 API 配置 (可选如需豆包2.0音色)
# export DOUBAO_USE_V3="true"
# export DOUBAO_RESOURCE_ID="volc.bigmodel.tts"
echo "✅ 豆包语音 API 环境变量已设置(本地配置)"
echo ""
echo "App ID: ${DOUBAO_APP_ID:0:10}..."
echo "Access Token: ${DOUBAO_ACCESS_TOKEN:0:20}..."

View File

@@ -0,0 +1,22 @@
#!/bin/bash
# 豆包语音 API 环境变量配置 (示例)
#
# ⚠️ 重要:这是示例脚本,包含占位符。
# 本地使用时,请参考 setup_env.local.sh.example 创建 setup_env.local.sh
# 然后在其中填入您的真实凭证。.gitignore 已配置忽略 .local 文件。
export DOUBAO_APP_ID="your_app_id"
export DOUBAO_ACCESS_TOKEN="your_access_token"
# V3 API 配置 (可选如需豆包2.0音色)
# export DOUBAO_USE_V3="true"
# export DOUBAO_RESOURCE_ID="volc.bigmodel.tts"
echo "✅ 豆包语音 API 环境变量已设置"
echo ""
echo "App ID: $DOUBAO_APP_ID"
echo "Access Token: ${DOUBAO_ACCESS_TOKEN:0:20}..."
echo ""
echo "现在可以运行:"
echo " python3 voice_converter.py tts \"你好世界\" -o hello.mp3"
echo " python3 voice_converter.py asr audio.mp3 # 需先启用ASR服务"

View File

@@ -0,0 +1,327 @@
#!/usr/bin/env python3
"""
豆包唱歌工具
基于豆包端到端实时语音大模型,支持让豆包唱歌
使用WebSocket实时对话和生成音频
"""
import os
import sys
import json
import asyncio
import websockets
import struct
import uuid
from typing import Optional
# 连接级事件不需要session_id
CONNECTION_EVENTS = {1, 2, 50, 51, 52}
class DoubaoSinging:
"""豆包唱歌工具类"""
def __init__(self):
# 从环境变量读取配置
self.app_id = os.environ.get("DOUBAO_APP_ID")
self.access_token = os.environ.get("DOUBAO_ACCESS_TOKEN")
if not self.app_id or not self.access_token:
raise ValueError(
"请先设置环境变量:\n"
"export DOUBAO_APP_ID='your_app_id'\n"
"export DOUBAO_ACCESS_TOKEN='your_access_token'"
)
# 端到端实时语音WebSocket地址
self.ws_url = "wss://openspeech.bytedance.com/api/v3/realtime/dialogue"
self.app_key = "PlgvMymc7f3tQnJ6" # 固定值
self.resource_id = "volc.speech.dialog" # 固定值
def _build_message(self, event_id: int, payload: dict = None, session_id: str = None) -> bytes:
"""
构建二进制消息
协议格式:
- header (4 bytes)
- event_id (4 bytes, big-endian)
- [session_id_len (4 bytes) + session_id (variable)] -- 仅非连接级事件
- payload_len (4 bytes, big-endian)
- payload (variable, JSON)
"""
buf = bytearray()
# Header (4 bytes)
buf.append(0x11) # version=1, header_size=1
buf.append(0x14) # FULL_CLIENT_REQUEST(0x1) + WITH_EVENT(0x4)
buf.append(0x10) # JSON serialization, no compression
buf.append(0x00) # reserved
# Event ID
buf.extend(struct.pack('>I', event_id))
# Session ID (required for non-connection events)
if event_id not in CONNECTION_EVENTS:
sid_bytes = (session_id or "").encode('utf-8')
buf.extend(struct.pack('>I', len(sid_bytes)))
buf.extend(sid_bytes)
# Payload
if payload:
payload_bytes = json.dumps(payload, ensure_ascii=False).encode('utf-8')
else:
payload_bytes = b'{}'
buf.extend(struct.pack('>I', len(payload_bytes)))
buf.extend(payload_bytes)
return bytes(buf)
def _parse_response(self, data: bytes) -> dict:
"""
解析服务端二进制消息
Returns:
dict with keys: msg_type, event_id, session_id, payload, payload_bytes
"""
result = {"raw": data}
if len(data) < 4:
return result
# Header
msg_type = (data[1] >> 4) & 0x0F
flags = data[1] & 0x0F
result["msg_type"] = msg_type
offset = 4
# Event ID (if WITH_EVENT flag)
if flags & 0x04 and len(data) >= offset + 4:
event_id = struct.unpack('>I', data[offset:offset + 4])[0]
result["event_id"] = event_id
offset += 4
# Connect ID for connection events (50, 51, 52)
if event_id in {50, 51, 52} and len(data) >= offset + 4:
cid_len = struct.unpack('>I', data[offset:offset + 4])[0]
offset += 4
if len(data) >= offset + cid_len:
result["connect_id"] = data[offset:offset + cid_len].decode('utf-8', errors='ignore')
offset += cid_len
# Session ID for session-level events
elif event_id not in CONNECTION_EVENTS and len(data) >= offset + 4:
sid_len = struct.unpack('>I', data[offset:offset + 4])[0]
offset += 4
if len(data) >= offset + sid_len:
result["session_id"] = data[offset:offset + sid_len].decode('utf-8', errors='ignore')
offset += sid_len
# Payload
if len(data) >= offset + 4:
payload_len = struct.unpack('>I', data[offset:offset + 4])[0]
offset += 4
if len(data) >= offset + payload_len:
payload_raw = data[offset:offset + payload_len]
result["payload_bytes"] = payload_raw
# Audio-only responses (msg_type 0xB) have raw audio
if msg_type == 0x0B:
result["is_audio"] = True
else:
try:
result["payload"] = json.loads(payload_raw.decode('utf-8'))
except:
result["payload_text"] = payload_raw.decode('utf-8', errors='ignore')
return result
async def sing(
self,
song_request: str,
output_file: str = "singing_output.mp3",
language: str = "zh-CN",
model: str = "1.2.1.0"
) -> str:
"""
让豆包唱歌
Args:
song_request: 唱歌请求,如 "请唱一首关于春天的歌"
output_file: 输出音频文件路径
language: 语言代码 (zh-CN/en-US)
model: 模型版本
Returns:
str: 输出文件路径
"""
print(f"🎵 豆包唱歌中...")
print(f" 请求: {song_request}")
print(f" 模型: {model}")
try:
audio_data = bytearray()
session_id = str(uuid.uuid4())
# WebSocket连接头
headers = {
"X-Api-App-ID": self.app_id,
"X-Api-Access-Key": self.access_token,
"X-Api-Resource-Id": self.resource_id,
"X-Api-App-Key": self.app_key,
"X-Api-Connect-Id": str(uuid.uuid4()),
}
async with websockets.connect(self.ws_url, additional_headers=headers) as websocket:
print("✅ WebSocket连接成功")
# 1. StartConnection (event_id=1, 无需session_id)
await websocket.send(self._build_message(1))
response = await asyncio.wait_for(websocket.recv(), timeout=5)
resp = self._parse_response(response)
if resp.get("event_id") == 50:
print(f"✅ 连接已建立")
else:
print(f"⚠️ 连接响应: {resp}")
# 2. StartSession (event_id=100, 需要session_id)
start_session_payload = {
"tts": {
"audio_config": {
"channel": 1,
"format": "pcm",
"sample_rate": 24000
}
},
"dialog": {
"extra": {
"enable_music": True,
"input_mod": "text",
"model": model
}
}
}
await websocket.send(self._build_message(100, start_session_payload, session_id))
response = await asyncio.wait_for(websocket.recv(), timeout=5)
resp = self._parse_response(response)
if resp.get("event_id") == 150:
print(f"✅ 会话已建立")
elif resp.get("payload", {}).get("error"):
print(f"❌ 会话错误: {resp['payload']['error']}")
return None
else:
print(f"📋 会话响应: {resp}")
# 3. SayHello/ChatTextQuery (event_id=300, 需要session_id)
chat_payload = {"content": song_request}
await websocket.send(self._build_message(300, chat_payload, session_id))
print(f"📤 已发送唱歌请求")
# 4. 接收音频流(使用超时检测结束)
print("\n📋 接收音频流...")
tts_started = False
recv_timeout = 5 # 5秒无数据则认为结束
while True:
try:
message = await asyncio.wait_for(websocket.recv(), timeout=recv_timeout)
except asyncio.TimeoutError:
break
except websockets.exceptions.ConnectionClosed:
break
if isinstance(message, bytes) and len(message) >= 4:
resp = self._parse_response(message)
msg_type = resp.get("msg_type", 0)
flags = message[1] & 0x0F
# Audio-only response (0xB = 11)
if resp.get("is_audio") and resp.get("payload_bytes"):
audio_data.extend(resp["payload_bytes"])
if not tts_started:
print(f" 接收音频中...", end="", flush=True)
tts_started = True
else:
print(".", end="", flush=True)
# NEG_SEQUENCE flag = last packet
if flags & 0x02:
break
# Server error (0xF = 15)
elif msg_type == 0x0F:
error = resp.get("payload", {}).get("error", "unknown")
print(f"\n❌ 服务器错误: {error}")
break
# Full server response (0x9) - session finished
elif msg_type == 0x09:
event_id = resp.get("event_id", 0)
if event_id in {152, 52}:
break
# 5. 保存音频文件
if audio_data:
# Save as PCM, convert extension if needed
actual_output = output_file
if output_file.endswith('.mp3'):
actual_output = output_file.replace('.mp3', '.pcm')
with open(actual_output, "wb") as f:
f.write(audio_data)
file_size = len(audio_data) / 1024
print(f"\n\n✅ 唱歌完成!")
print(f" 输出: {actual_output} ({file_size:.1f} KB)")
print(f" 格式: PCM (24000Hz, 单声道)")
return actual_output
else:
print("\n⚠️ 未收到音频数据,请检查:")
print(" 1. 凭证是否正确")
print(" 2. 端到端实时语音大模型是否已开通")
print(" 3. 网络连接是否正常")
return None
except websockets.exceptions.WebSocketException as e:
raise Exception(f"WebSocket连接错误: {str(e)}")
except Exception as e:
raise Exception(f"唱歌调用失败: {str(e)}")
def main():
"""命令行工具"""
import argparse
parser = argparse.ArgumentParser(description="豆包唱歌工具")
subparsers = parser.add_subparsers(dest="command", help="选择功能")
# 唱歌命令
sing_parser = subparsers.add_parser("sing", help="让豆包唱歌")
sing_parser.add_argument("request", help="唱歌请求,如 '请唱一首关于春天的歌'")
sing_parser.add_argument(
"-o", "--output", default="singing_output.mp3", help="输出音频文件(默认: singing_output.mp3"
)
sing_parser.add_argument(
"-l", "--language", default="zh-CN", help="语言代码(默认: zh-CN"
)
sing_parser.add_argument(
"-m", "--model", default="1.2.1.0", help="模型版本(默认: 1.2.1.0=O2.0版本)"
)
args = parser.parse_args()
if not args.command:
parser.print_help()
return
try:
singing = DoubaoSinging()
if args.command == "sing":
asyncio.run(singing.sing(args.request, args.output, args.language, args.model))
except Exception as e:
print(f"❌ 错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""
豆包语音转换工具
支持:文字转语音 (TTS)
"""
import os
import sys
import json
import base64
import requests
from pathlib import Path
class DoubaoVoiceConverter:
"""豆包语音转换工具类"""
def __init__(self):
# 从环境变量读取配置
self.app_id = os.environ.get("DOUBAO_APP_ID")
self.access_token = os.environ.get("DOUBAO_ACCESS_TOKEN")
if not self.app_id or not self.access_token:
raise ValueError(
"请先设置环境变量:\n"
"export DOUBAO_APP_ID='your_app_id'\n"
"export DOUBAO_ACCESS_TOKEN='your_access_token'"
)
# API版本选择: V1 (默认, 支持基础音色) 或 V3 (豆包2.0, 需额外配置)
self.use_v3 = os.environ.get("DOUBAO_USE_V3", "false").lower() == "true"
if self.use_v3:
self.tts_url = "https://openspeech.bytedance.com/api/v3/tts/unidirectional"
self.resource_id = os.environ.get("DOUBAO_RESOURCE_ID", "volc.bigmodel.tts")
else:
# V1 API - 稳定可用,支持基础音色
self.tts_url = "https://openspeech.bytedance.com/api/v1/tts"
def text_to_speech(
self,
text: str,
output_file: str = "output.mp3",
voice_type: str = "BV700_V2_streaming"
) -> str:
"""
文字转语音 (TTS)
Args:
text: 要转换的文字
output_file: 输出音频文件路径
voice_type: 音色类型
- BV700_V2_streaming: 通用女声(推荐)
- BV701_V2_streaming: 通用男声
- BV406_streaming: 温柔女声
- BV158_streaming: 活泼女声
- BV115_streaming: 磁性男声
Returns:
str: 输出文件路径
"""
print(f"📝 文字转语音中...")
print(f" 文字: {text[:50]}{'...' if len(text) > 50 else ''}")
print(f" 音色: {voice_type}")
headers = {
"Authorization": f"Bearer;{self.access_token}",
"Content-Type": "application/json"
}
# V3 API需要Resource-Id (如果启用)
if self.use_v3:
headers["Resource-Id"] = self.resource_id
payload = {
"app": {
"appid": self.app_id,
"token": self.access_token,
"cluster": "volcano_tts"
},
"user": {
"uid": "user_001"
},
"audio": {
"voice_type": voice_type,
"encoding": "mp3",
"speed_ratio": 1.0,
"volume_ratio": 1.0,
"pitch_ratio": 1.0
},
"request": {
"reqid": f"tts_{os.urandom(8).hex()}",
"text": text,
"text_type": "plain",
"operation": "query"
}
}
try:
response = requests.post(self.tts_url, headers=headers, json=payload, timeout=30)
# 打印响应头信息
print(f"\n📋 响应信息:")
print(f" HTTP状态码: {response.status_code}")
if 'X-Tt-Logid' in response.headers:
print(f" RequestId: {response.headers['X-Tt-Logid']}")
if 'X-Request-Id' in response.headers:
print(f" X-Request-Id: {response.headers['X-Request-Id']}")
data = response.json()
# 打印完整响应
print(f"\n📄 完整响应:")
print(json.dumps(data, indent=2, ensure_ascii=False))
print()
if data.get("code") == 3000:
# 成功:解码并保存音频
audio_data = base64.b64decode(data["data"])
with open(output_file, "wb") as f:
f.write(audio_data)
file_size = len(audio_data) / 1024 # KB
print(f"✅ 语音合成成功!")
print(f" 输出: {output_file} ({file_size:.1f} KB)")
return output_file
else:
error_msg = data.get("message", "未知错误")
reqid = data.get("reqid", "未知")
raise Exception(f"TTS 失败\n 错误码: {data.get('code')}\n 错误信息: {error_msg}\n RequestId: {reqid}")
except requests.exceptions.Timeout:
raise Exception("请求超时,请检查网络连接")
except Exception as e:
raise Exception(f"TTS 调用失败: {str(e)}")
def main():
"""命令行工具"""
import argparse
parser = argparse.ArgumentParser(description="豆包语音转换工具")
subparsers = parser.add_subparsers(dest="command", help="选择功能")
# TTS 命令
tts_parser = subparsers.add_parser("tts", help="文字转语音")
tts_parser.add_argument("text", help="要转换的文字")
tts_parser.add_argument("-o", "--output", default="output.mp3", help="输出音频文件(默认: output.mp3")
tts_parser.add_argument("-v", "--voice", default="BV700_V2_streaming",
help="音色类型(默认: BV700_V2_streaming 通用女声)")
args = parser.parse_args()
if not args.command:
parser.print_help()
return
try:
converter = DoubaoVoiceConverter()
if args.command == "tts":
converter.text_to_speech(args.text, args.output, args.voice)
except Exception as e:
print(f"❌ 错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,508 @@
---
name: doubao-voice
description: 豆包语音API调用。支持语音合成(TTS)和唱歌。当用户提到语音合成、文字转语音、唱歌、豆包语音相关任务时自动激活。
---
# 豆包语音API技能
调用火山引擎豆包语音API实现文字转语音(TTS)和唱歌功能。
## 核心功能 ⭐
### 1. 文字转语音 (TTS)
```bash
# 1. 配置环境变量
export DOUBAO_APP_ID="your_app_id"
export DOUBAO_ACCESS_TOKEN="your_access_token"
# 2. 文字转语音
python scripts/voice_converter.py tts "你好世界"
```
### 2. 唱歌 🎵
```bash
# 让豆包唱歌
python scripts/singing.py sing "请唱一首关于春天的歌"
# 交互式唱歌模式
python scripts/singing.py interactive
```
## 功能概述
| 模块 | 功能 | 推荐模型 |
|------|------|---------|
| **语音合成 (TTS)** | 文字转语音、多种音色 | 豆包语音合成模型2.0 |
| **唱歌** | 实时语音交互、唱歌、角色扮演 | 豆包端到端实时语音大模型 |
---
## 环境配置
### 1. 获取火山引擎豆包语音凭证
1. 访问 [火山引擎控制台](https://console.volcengine.com/)
2. 开通「豆包语音」服务
3. 创建应用获取 `App ID``Access Token`
4. 开通所需服务:
- 「语音合成」权限:大模型语音合成
### 2. 环境变量配置
```bash
# ~/.zshrc 或 ~/.bashrc
export DOUBAO_APP_ID="your_app_id"
export DOUBAO_ACCESS_TOKEN="your_access_token"
export DOUBAO_CLUSTER="volcano_tts" # TTS服务集群
```
### 3. Python 依赖
```bash
# 推荐使用 uv
uv pip install requests websocket-client
# 或使用 pip
pip install requests websocket-client
```
---
## API 基础
### Base URL
```
TTS API: https://openspeech.bytedance.com/api/v1/tts
```
### 认证方式
使用 Access Token 进行认证,在请求头中添加:
```
Authorization: Bearer {access_token}
```
---
## 一、语音合成 (TTS)
### 1.1 基础语音合成
将文本转换为语音文件。
**自然语言示例**:
- "把这段文字转成语音"
- "用豆包合成语音"
- "生成语音:你好,欢迎使用豆包语音"
**Python 实现**:
```python
import os
import requests
import json
import base64
def text_to_speech(text: str, voice_type: str = "BV700_V2_streaming", output_file: str = "output.mp3"):
"""
文字转语音
Args:
text: 要合成的文本
voice_type: 音色类型 (默认: BV700_V2_streaming)
output_file: 输出音频文件路径
Returns:
音频文件路径
"""
app_id = os.environ.get("DOUBAO_APP_ID")
access_token = os.environ.get("DOUBAO_ACCESS_TOKEN")
cluster = os.environ.get("DOUBAO_CLUSTER", "volcano_tts")
url = "https://openspeech.bytedance.com/api/v1/tts"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
payload = {
"app": {
"appid": app_id,
"token": access_token,
"cluster": cluster
},
"user": {
"uid": "user123"
},
"audio": {
"voice_type": voice_type,
"encoding": "mp3",
"speed_ratio": 1.0,
"volume_ratio": 1.0,
"pitch_ratio": 1.0
},
"request": {
"reqid": "req_" + os.urandom(8).hex(),
"text": text,
"text_type": "plain",
"operation": "query"
}
}
response = requests.post(url, headers=headers, json=payload)
data = response.json()
if data.get("code") == 3000:
# 解码音频数据
audio_data = base64.b64decode(data["data"])
with open(output_file, "wb") as f:
f.write(audio_data)
return output_file
else:
raise Exception(f"TTS 失败: {data}")
# 使用示例
audio_file = text_to_speech("你好,我是豆包语音助手")
print(f"语音已生成: {audio_file}")
```
### 1.2 流式语音合成
适用于长文本,边生成边播放。
```python
import websocket
import json
import os
def stream_tts(text: str, voice_type: str = "BV700_V2_streaming"):
"""
流式语音合成
Args:
text: 要合成的文本
voice_type: 音色类型
"""
app_id = os.environ.get("DOUBAO_APP_ID")
access_token = os.environ.get("DOUBAO_ACCESS_TOKEN")
ws_url = f"wss://openspeech.bytedance.com/api/v1/tts/ws?appid={app_id}&token={access_token}"
def on_message(ws, message):
data = json.loads(message)
if "audio" in data:
# 处理音频数据
audio_chunk = base64.b64decode(data["audio"])
# 播放或保存音频片段
print(f"收到音频片段: {len(audio_chunk)} 字节")
def on_open(ws):
payload = {
"app": {
"appid": app_id,
"token": access_token,
"cluster": "volcano_tts"
},
"user": {
"uid": "user123"
},
"audio": {
"voice_type": voice_type,
"encoding": "mp3"
},
"request": {
"reqid": "stream_" + os.urandom(8).hex(),
"text": text,
"text_type": "plain",
"operation": "submit"
}
}
ws.send(json.dumps(payload))
ws = websocket.WebSocketApp(
ws_url,
on_message=on_message,
on_open=on_open
)
ws.run_forever()
# 使用示例
stream_tts("这是一段很长的文本,使用流式合成可以边生成边播放...")
```
### 1.3 音色选择
豆包语音提供多种音色:
| 音色代码 | 描述 | 场景 |
|---------|------|------|
| BV700_V2_streaming | 通用女声 | 通用场景 |
| BV701_V2_streaming | 通用男声 | 通用场景 |
| BV406_streaming | 温柔女声 | 客服、助手 |
| BV158_streaming | 活泼女声 | 教育、娱乐 |
| BV115_streaming | 磁性男声 | 新闻、播音 |
**查询可用音色**:
```bash
TOKEN="${DOUBAO_ACCESS_TOKEN}"
APP_ID="${DOUBAO_APP_ID}"
curl -s "https://openspeech.bytedance.com/api/v1/tts/voices?appid=$APP_ID" \
-H "Authorization: Bearer $TOKEN"
```
---
## 完整工具类
```python
import os
import requests
import base64
import json
from typing import Optional
class DoubaoVoice:
"""豆包语音API工具类"""
BASE_URL = "https://openspeech.bytedance.com/api/v1"
def __init__(self, app_id: str = None, access_token: str = None):
self.app_id = app_id or os.environ.get("DOUBAO_APP_ID")
self.access_token = access_token or os.environ.get("DOUBAO_ACCESS_TOKEN")
self.cluster_tts = os.environ.get("DOUBAO_CLUSTER", "volcano_tts")
@property
def headers(self):
return {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json"
}
def text_to_speech(
self,
text: str,
voice_type: str = "BV700_V2_streaming",
output_file: str = "output.mp3"
) -> str:
"""文字转语音"""
url = f"{self.BASE_URL}/tts"
payload = {
"app": {
"appid": self.app_id,
"token": self.access_token,
"cluster": self.cluster_tts
},
"user": {"uid": "user123"},
"audio": {
"voice_type": voice_type,
"encoding": "mp3",
"speed_ratio": 1.0,
"volume_ratio": 1.0,
"pitch_ratio": 1.0
},
"request": {
"reqid": "req_" + os.urandom(8).hex(),
"text": text,
"text_type": "plain",
"operation": "query"
}
}
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") == 3000:
audio_data = base64.b64decode(data["data"])
with open(output_file, "wb") as f:
f.write(audio_data)
return output_file
else:
raise Exception(f"TTS 失败: {data}")
def list_voices(self) -> list:
"""获取可用音色列表"""
url = f"{self.BASE_URL}/tts/voices"
params = {"appid": self.app_id}
response = requests.get(url, headers=self.headers, params=params)
data = response.json()
if data.get("code") == 0:
return data["voices"]
else:
raise Exception(f"获取音色列表失败: {data}")
# ==================== 使用示例 ====================
if __name__ == "__main__":
voice = DoubaoVoice()
# 示例1: 文字转语音
audio_file = voice.text_to_speech("你好,我是豆包语音助手")
print(f"语音已生成: {audio_file}")
# 示例2: 查看可用音色
voices = voice.list_voices()
for v in voices[:5]:
print(f"{v['voice_type']}: {v['description']}")
```
---
## 二、唱歌 (豆包端到端实时语音大模型)
### 2.1 基础唱歌
让豆包唱歌,支持任何歌曲主题。
**自然语言示例**:
- "请唱一首关于春天的歌"
- "唱一个温柔的摇篮曲"
- "来一首欢快的儿歌"
**Python 实现**:
```python
import asyncio
from scripts.singing import DoubaoSinging
async def main():
singing = DoubaoSinging()
# 让豆包唱歌
audio_file = await singing.sing(
"请唱一首关于春天的歌",
output_file="spring_song.mp3",
language="zh-CN"
)
print(f"唱歌完成: {audio_file}")
asyncio.run(main())
```
### 2.2 交互式唱歌
与豆包进行实时对话,可以要求她唱歌、讲故事等。
**Python 实现**:
```python
import asyncio
from scripts.singing import DoubaoSinging
async def main():
singing = DoubaoSinging()
# 启动交互式模式
await singing.interactive_singing(language="zh-CN")
asyncio.run(main())
```
**交互示例**:
```
你: 请唱一首情歌
豆包: [生成音频] 我会为你唱一首温柔的情歌...
你: 能加点方言吗?
豆包: [用方言重新唱歌]
你: quit
再见!
```
---
## 自然语言操作示例
### TTS 操作
| 用户说 | 执行操作 |
|--------|----------|
| "把这段话转成语音:你好世界" | 调用 TTS API 生成语音 |
| "用温柔女声合成语音" | 使用 BV406_streaming 音色 |
| "生成一段播音腔的新闻语音" | 使用磁性男声音色 |
### 唱歌操作
| 用户说 | 执行操作 |
|--------|----------|
| "请唱一首关于春天的歌" | 调用端到端实时语音大模型生成唱歌音频 |
| "唱一首摇篮曲" | 生成温柔的摇篮曲 |
| "唱歌的同时讲个故事" | 交互式对话中唱歌并讲故事 |
| "开启交互式唱歌模式" | 启动实时语音交互 |
---
## 计费说明
### TTS 计费
- **并发版**: 2000元/并发/月(纯并发计费,不收取字符调用费用)
- **按量付费**: 按合成字符数计费
### 免费试用
新用户开通服务后可获得一定免费额度,具体额度以控制台显示为准。
---
## 注意事项
1. **音频格式**: TTS 支持 mp3/wav/pcm
2. **文本长度**: TTS 单次请求最长支持 5000 字符
3. **并发限制**: 注意 API 调用频率和并发数限制
4. **Token 安全**: Access Token 存储在环境变量中,不要硬编码
---
## 错误处理
```python
def safe_tts(text: str):
"""带错误处理的 TTS"""
try:
voice = DoubaoVoice()
return voice.text_to_speech(text)
except Exception as e:
if "401" in str(e):
print("认证失败,请检查 Access Token")
elif "429" in str(e):
print("请求过于频繁,请稍后重试")
else:
print(f"合成失败: {e}")
return None
```
---
## 常见场景
### 场景 1: 生成多语言语音
```python
voice = DoubaoVoice()
# 中文
voice.text_to_speech("你好", voice_type="BV700_V2_streaming", output_file="zh.mp3")
# 英文
voice.text_to_speech("Hello", voice_type="EN_001", output_file="en.mp3")
```
---
## 参考资源
- [火山引擎豆包语音文档](https://www.volcengine.com/docs/6561/1359369)
- [豆包语音控制台](https://console.volcengine.com/speech/app)
- [API 接口文档](https://www.volcengine.com/docs/6561/1359370)
- [计费说明](https://www.volcengine.com/docs/6561/1359370)

View File

@@ -0,0 +1,8 @@
{
"name": "enjoysa-deploy-plugin",
"description": "EnjoySA 项目部署到新加坡服务器",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,113 @@
# EnjoySA 部署技能
## 触发条件
当用户提到以下内容时自动激活:
- enjoysa 部署
- 部署到新加坡
- deploy enjoysa
---
## 项目信息
| 项目 | 值 |
|------|-----|
| 本地路径 | `/Users/donglinlai/coding/qiudl/enjoysa` |
| Git 仓库 | `https://gitea.pipexerp.com/qiudl/enjoysa.git` |
| 主分支 | main |
---
## 服务器信息
| 项目 | 值 |
|------|-----|
| 服务器 | singapore (43.134.28.147) |
| 用户 | ubuntu |
| SSH 密钥 | ~/.ssh/singpore.pem |
| 部署路径 | /opt/enjoysa |
| 访问地址 | http://43.134.28.147:6066 |
| Web 服务 | Nginx (端口 6066) |
---
## 快速部署
执行部署脚本:
```bash
./scripts/deploy.sh
```
---
## 手动部署步骤
### 1. 构建
```bash
cd web && npm run build
```
### 2. 上传
```bash
scp -r web/dist/* singapore:/opt/enjoysa/
```
### 3. 验证
```bash
ssh singapore "curl -s -o /dev/null -w '%{http_code}' http://localhost:6066/"
```
---
## 常用运维命令
```bash
# 查看 Nginx 状态
ssh singapore "sudo systemctl status nginx"
# 重载 Nginx 配置
ssh singapore "sudo nginx -t && sudo systemctl reload nginx"
# 查看部署目录
ssh singapore "ls -la /opt/enjoysa/"
# 查看 Nginx 访问日志
ssh singapore "sudo tail -f /var/log/nginx/access.log"
# 查看错误日志
ssh singapore "sudo tail -f /var/log/nginx/error.log"
```
---
## Nginx 配置
配置文件: `/etc/nginx/sites-available/enjoysa`
```nginx
server {
listen 6066;
listen [::]:6066;
server_name _;
root /opt/enjoysa;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
```
---
## 注意事项
1. 腾讯云安全组需开放 6066 端口
2. SPA 应用使用 `try_files` 支持前端路由
3. 静态资源设置长期缓存

View File

@@ -0,0 +1,8 @@
{
"name": "enjoysa-plugin",
"description": "Plugin for enjoysa",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,210 @@
---
name: enjoysa
description: EnjoySA 畅游南澳旅游平台开发。用于前端页面开发、组件开发、i18n国际化。当用户提到 enjoysa、畅游南澳、旅游平台、供应商后台相关任务时自动激活。
---
# EnjoySA 开发技能
畅游南澳旅游平台,面向 C 端游客和 B 端供应商的在线旅游服务系统。
## 项目信息
| 项目 | 值 |
|------|-----|
| 本地路径 | `/Users/donglinlai/coding/qiudl/enjoysa` |
| Git 仓库 | `https://gitea.pipexerp.com/qiudl/enjoysa.git` |
| 主分支 | main |
| 技术栈 | Vite + React 18 + TypeScript + CSS Modules |
| 部署服务器 | singapore (43.134.28.147:6066) |
| 访问地址 | http://43.134.28.147:6066 |
---
## 架构概览
```
enjoysa/
├── web/ # React 前端
│ ├── src/
│ │ ├── components/ # 通用组件
│ │ │ ├── Common/ # Header, Footer, LanguageSwitcher
│ │ │ ├── Auth/ # LoginForm, RegisterForm
│ │ │ └── Supplier/ # SupplierLoginForm, SupplierRegisterForm
│ │ ├── pages/ # 页面组件
│ │ │ ├── LoginPage/ # C端用户登录
│ │ │ ├── SupplierLoginPage/# B端供应商登录
│ │ │ ├── AdminLoginPage/ # 平台管理员登录
│ │ │ ├── TermsPage/ # 服务条款
│ │ │ ├── PrivacyPage/ # 隐私政策
│ │ │ └── Supplier/ # 供应商后台模块
│ │ ├── i18n/ # 国际化
│ │ │ └── locales/ # 翻译文件
│ │ │ ├── zh-CN.json
│ │ │ └── en-US.json
│ │ └── types/ # 类型定义
│ ├── scripts/ # 部署脚本
│ └── dist/ # 构建产物
├── backend/ # Go 后端 (规划中)
└── docs/ # 文档
```
---
## 页面路由
### C 端 (游客)
| 路由 | 页面 | 说明 |
|------|------|------|
| `/` | HomePage | 首页 |
| `/login` | LoginPage | 用户登录/注册 |
| `/terms` | TermsPage | 服务条款 |
| `/privacy` | PrivacyPage | 隐私政策 |
### B 端 (供应商)
| 路由 | 页面 | 说明 |
|------|------|------|
| `/supplier/login` | SupplierLoginPage | 供应商登录 |
| `/supplier/register` | SupplierRegisterPage | 供应商入驻申请 |
| `/supplier` | SupplierDashboard | 供应商仪表盘 |
| `/supplier/products` | SupplierProducts | 产品管理 |
| `/supplier/orders` | SupplierOrders | 订单处理 |
| `/supplier/finance` | SupplierFinance | 财务结算 |
### 管理端
| 路由 | 页面 | 说明 |
|------|------|------|
| `/admin/login` | AdminLoginPage | 管理员登录 |
---
## i18n 国际化
### 翻译文件位置
```
web/src/i18n/locales/
├── zh-CN.json # 简体中文
└── en-US.json # 英文
```
### 命名空间规范
| 命名空间 | 用途 | 示例 |
|---------|------|------|
| `brand.*` | 品牌信息 | brand.name, brand.tagline |
| `auth.*` | 登录注册 | auth.loginTitle, auth.emailPlaceholder |
| `terms.*` | 服务条款 | terms.title, terms.sections.* |
| `privacy.*` | 隐私政策 | privacy.title, privacy.sections.* |
| `supplier.*` | 供应商模块 | supplier.login.*, supplier.dashboard.* |
| `admin.*` | 管理员模块 | admin.login.*, admin.dashboard.* |
| `common.*` | 通用文本 | common.submit, common.cancel |
| `footer.*` | 页脚 | footer.supplierEntry, footer.copyright |
### 添加新翻译步骤
1.`zh-CN.json``en-US.json` 中同时添加新 key
2. 使用 `useTranslation()` hook 获取 `t` 函数
3. 使用 `t('namespace.key')` 获取翻译文本
```tsx
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
return <h1>{t('brand.name')}</h1>;
};
```
### 注意事项
- **禁止硬编码中文/英文**,所有用户可见文本必须使用 i18n
- 长文本内容(如条款、政策)使用 `\n` 分段
- JSON 中避免使用中文引号 `""` `''`,使用 `「」` 或英文引号
---
## 组件规范
### Common 组件
| 组件 | 用途 | 文件 |
|------|------|------|
| Header | 顶部导航栏 | `components/Common/Header/` |
| Footer | 页脚(含供应商入口) | `components/Common/Footer/` |
| LanguageSwitcher | 语言切换器 | `components/Common/LanguageSwitcher/` |
### Auth 组件
| 组件 | 用途 | 文件 |
|------|------|------|
| LoginForm | C端登录表单 | `components/Auth/LoginForm/` |
| RegisterForm | C端注册表单 | `components/Auth/RegisterForm/` |
### Supplier 组件
| 组件 | 用途 | 文件 |
|------|------|------|
| SupplierLoginForm | 供应商登录表单 | `components/Supplier/SupplierLoginForm/` |
| SupplierRegisterForm | 供应商入驻申请表单 | `components/Supplier/SupplierRegisterForm/` |
---
## 本地开发
### 启动前端
```bash
cd /Users/donglinlai/coding/qiudl/enjoysa/web
npm install
npm run dev
```
### 构建
```bash
npm run build
```
### 部署
```bash
./scripts/deploy.sh
```
或手动部署:
```bash
scp -r web/dist/* singapore:/opt/enjoysa/
```
---
## 供应商类型
系统支持以下供应商类型:
| 类型 | 说明 |
|------|------|
| travel_agency | 旅行社 |
| hotel | 酒店 |
| attraction | 景区 |
| restaurant | 餐厅 |
| car_rental | 租车公司 |
---
## 相关技能
- `enjoysa-deploy` - 部署到新加坡服务器
- `frontend-design` - 前端界面设计
- `dev-coding` - 软件编码开发
---
## 版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.0.0 | 2026-01-31 | 初始版本包含项目结构、页面路由、i18n规范 |

View File

@@ -0,0 +1,8 @@
{
"name": "executing-plans-plugin",
"description": "Plugin for executing-plans",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,96 @@
---
name: executing-plans
description: Use when you have a written implementation plan to execute in a separate session with review checkpoints
---
# Executing Plans
## Overview
Load plan, review critically, create branch, execute tasks in batches, report for review between batches.
**Core principle:** Batch execution with checkpoints for architect review.
**Announce at start:** "I'm using the executing-plans skill to implement this plan."
## The Process
### Step 1: Load and Review Plan
1. Read plan file
2. Review critically - identify any questions or concerns about the plan
3. If concerns: Raise them with your human partner before starting
4. If no concerns: Proceed to branch setup
### Step 2: Setup Branch
**Before any implementation, ensure proper branch isolation.**
1. Check if already on a feature branch for this task
2. If not, use `/pr start` to create one:
```bash
/pr start <type> <REQ-id> <name>
# Example: /pr start feature REQ-123 user-login
```
3. If no REQ-id available, ask user or create branch manually:
```bash
git fetch origin
git checkout -b <type>/<descriptive-name> origin/main
```
4. Confirm branch is ready before proceeding
**Branch types:** `feature`, `fix`, `refactor`
### Step 3: Create Tasks and Execute Batch
**Default: First 3 tasks**
1. Create TodoWrite tasks from plan
2. For each task in batch:
- Mark as in_progress
- Follow each step exactly (plan has bite-sized steps)
- Run verifications as specified
- Mark as completed
### Step 4: Report
When batch complete:
- Show what was implemented
- Show verification output
- Say: "Ready for feedback."
### Step 5: Continue
Based on feedback:
- Apply changes if needed
- Execute next batch
- Repeat until complete
### Step 6: Complete Development
After all tasks complete and verified:
- Announce: "I'm using the finishing-a-development-branch skill to complete this work."
- **REQUIRED SUB-SKILL:** Use superpowers:finishing-a-development-branch
- Follow that skill to verify tests, present options, execute choice
## When to Stop and Ask for Help
**STOP executing immediately when:**
- Hit a blocker mid-batch (missing dependency, test fails, instruction unclear)
- Plan has critical gaps preventing starting
- You don't understand an instruction
- Verification fails repeatedly
**Ask for clarification rather than guessing.**
## When to Revisit Earlier Steps
**Return to Review (Step 1) when:**
- Partner updates the plan based on your feedback
- Fundamental approach needs rethinking
**Don't force through blockers** - stop and ask.
## Remember
- Review plan critically first
- **Create feature branch before implementation**
- Follow plan steps exactly
- Don't skip verifications
- Reference skills when plan says to
- Between batches: just report and wait
- Stop when blocked, don't guess

View File

@@ -0,0 +1,8 @@
{
"name": "feishu-bitable-plugin",
"description": "飞书多维表格操作。用于记录增删改查、批量操作、筛选排序、数据同步。当需要操作飞书多维表格时使用。",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,130 @@
---
name: feishu-bitable
description: 飞书多维表格操作。用于记录增删改查、批量操作、筛选排序、数据同步。当需要操作飞书多维表格时使用。
---
# 飞书多维表格 (Bitable)
## URL 结构
```
https://xxx.feishu.cn/base/BascXXX?table=tblXXX&view=vewXXX
└── app_token └── table_id
```
## 核心操作
### 列出记录
```python
def list_records(app_token, table_id, filter_str=None, page_size=100):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {"page_size": page_size}
if filter_str:
params["filter"] = filter_str
response = requests.get(url, headers=headers, params=params)
return response.json()["data"]["items"]
```
### 创建记录
```python
def create_record(app_token, table_id, fields):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
response = requests.post(url, headers=headers, json={"fields": fields})
return response.json()["data"]["record"]
# 示例
create_record("BascXXX", "tblXXX", {"任务名称": "完成开发", "状态": "进行中"})
```
### 批量创建 (最多 500 条)
```python
url = f".../records/batch_create"
requests.post(url, headers=headers, json={"records": [{"fields": {...}}, ...]})
```
### 更新记录
```python
def update_record(app_token, table_id, record_id, fields):
url = f".../records/{record_id}"
response = requests.put(url, headers=headers, json={"fields": fields})
return response.json()["data"]["record"]
```
### 删除记录
```python
def delete_record(app_token, table_id, record_id):
url = f".../records/{record_id}"
return requests.delete(url, headers=headers).json()
```
## 筛选条件
```python
# 等于
'CurrentValue.[状态]="进行中"'
# 包含
'CurrentValue.[标题].contains("任务")'
# 大于(数字/日期)
'CurrentValue.[优先级]>2'
# 组合
'AND(CurrentValue.[状态]="进行中", CurrentValue.[优先级]>2)'
'OR(CurrentValue.[状态]="完成", CurrentValue.[状态]="归档")'
# 空值
'CurrentValue.[截止日期]=BLANK()'
```
## 排序
```python
params = {
"sort": '[{"field_name":"优先级","desc":true}]'
}
```
## 字段类型
| 类型 | 值格式 |
|------|--------|
| 文本 | `"字符串"` |
| 数字 | `123` |
| 单选 | `"选项值"` |
| 多选 | `["选项1", "选项2"]` |
| 日期 | `1706400000000` (毫秒时间戳) |
| 人员 | `[{"id": "ou_xxx"}]` |
| 复选框 | `true/false` |
| 链接 | `{"link": "https://...", "text": "显示文本"}` |
## 完整工具类
`~/.claude/skills/feishu/feishu_bitable.py`
```python
from feishu_bitable import FeishuBitable
bitable = FeishuBitable()
records = bitable.list_records("BascXXX", "tblXXX")
bitable.create_record("BascXXX", "tblXXX", {"名称": "测试"})
bitable.batch_create("BascXXX", "tblXXX", [{"名称": "1"}, {"名称": "2"}])
```
## 常见场景
```python
# 获取待处理任务
tasks = bitable.list_records("BascXXX", "tblXXX",
filter_str='CurrentValue.[状态]="待处理"')
# 批量更新状态
for task in tasks:
bitable.update_record("BascXXX", "tblXXX",
task["record_id"], {"状态": "已完成"})
```

View File

@@ -0,0 +1,8 @@
{
"name": "feishu-docx-plugin",
"description": "飞书云文档操作。用于创建、编辑云文档,插入内容块,会议纪要生成。当需要操作飞书云文档时使用。",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,126 @@
---
name: feishu-docx
description: 飞书云文档操作。用于创建、编辑云文档,插入内容块,会议纪要生成。当需要操作飞书云文档时使用。
---
# 飞书云文档 (Docx)
## URL 结构
```
https://xxx.feishu.cn/docx/DoxcXXXXXX
└── document_id
```
## 创建文档
```python
def create_document(title, folder_token=None):
url = "https://open.feishu.cn/open-apis/docx/v1/documents"
payload = {"title": title}
if folder_token:
payload["folder_token"] = folder_token
response = requests.post(url, headers=headers, json=payload)
doc = response.json()["data"]["document"]
return {"document_id": doc["document_id"], "url": f"https://feishu.cn/docx/{doc['document_id']}"}
```
## 设置权限
```python
def set_permission(document_id, editable=True):
"""设置文档为组织内可编辑/只读"""
url = f"https://open.feishu.cn/open-apis/drive/v1/permissions/{document_id}/public"
payload = {
"external_access_entity": "open",
"link_share_entity": "tenant_editable" if editable else "tenant_readable"
}
requests.patch(url, headers=headers, params={"type": "docx"}, json=payload)
```
## 内容块类型
| 类型 | block_type | 示例 |
|------|------------|------|
| 段落 | 2 | 普通文本 |
| 一级标题 | 3 | # 标题 |
| 二级标题 | 4 | ## 标题 |
| 三级标题 | 5 | ### 标题 |
| 无序列表 | 13 | - 列表项 |
| 有序列表 | 14 | 1. 列表项 |
| 代码块 | 16 | ```code``` |
| 引用 | 18 | > 引用 |
| 分割线 | 22 | --- |
| 图片 | 27 | 需先上传 |
## 创建内容块
```python
def create_block(document_id, block_id, block_type, content):
url = f"https://open.feishu.cn/open-apis/docx/v1/documents/{document_id}/blocks/{block_id}/children"
if block_type in [3, 4, 5]: # 标题
block = {"block_type": block_type, "heading": {"elements": [{"text_run": {"content": content}}]}}
elif block_type == 2: # 段落
block = {"block_type": 2, "text": {"elements": [{"text_run": {"content": content}}]}}
elif block_type in [13, 14]: # 列表
block = {"block_type": block_type, "bullet/ordered": {"elements": [{"text_run": {"content": content}}]}}
requests.post(url, headers=headers, json={"children": [block], "index": -1})
```
## 图片上传
```python
# 1. 上传图片到素材库
def upload_image(file_path, parent_node):
url = "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all"
with open(file_path, 'rb') as f:
files = {'file': f}
data = {'file_name': os.path.basename(file_path), 'parent_type': 'docx_image', 'parent_node': parent_node}
response = requests.post(url, headers={"Authorization": f"Bearer {token}"}, files=files, data=data)
return response.json()["data"]["file_token"]
# 2. 插入图片块
def insert_image(document_id, block_id, file_token):
block = {"block_type": 27, "image": {"token": file_token}}
# ... 同 create_block
```
## 会议纪要模板
```python
def create_meeting_notes(title, date, attendees, agenda, decisions, action_items):
doc = create_document(f"{title} - {date}")
doc_id = doc["document_id"]
# 获取根块
root = requests.get(f".../documents/{doc_id}/blocks/{doc_id}").json()
root_id = root["data"]["block"]["block_id"]
# 添加内容
create_block(doc_id, root_id, 3, f"会议纪要:{title}")
create_block(doc_id, root_id, 2, f"日期:{date}")
create_block(doc_id, root_id, 2, f"参会人:{', '.join(attendees)}")
create_block(doc_id, root_id, 4, "议程")
for item in agenda:
create_block(doc_id, root_id, 13, item)
create_block(doc_id, root_id, 4, "决议")
for item in decisions:
create_block(doc_id, root_id, 13, item)
create_block(doc_id, root_id, 4, "待办事项")
for item in action_items:
create_block(doc_id, root_id, 14, item)
return doc
```
## 完整工具类
见 `~/.claude/skills/feishu/feishu_docx.py`
## 注意事项
- 创建文档必须指定 `folder_token`,否则会出现在「与我共享」
- 默认存储到 `C80gfkRnzlonQ5d4AhOcOACDnNg`01运营文件夹
- 图片必须先上传到素材库,再插入文档

View File

@@ -0,0 +1,8 @@
{
"name": "feishu-plugin",
"description": "飞书多维表格快捷操作。通过自然语言实现多维表格的增删改查、数据同步、批量操作等功能。当用户提到飞书、多维表格、Bitable、飞书表格相关任务时自动激活。",
"version": "1.1.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""
添加需求图片字段并上传图片到飞书多维表格
"""
import requests
import os
from datetime import datetime, timedelta
from PIL import Image, ImageDraw, ImageFont
# ========== 配置 ==========
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
# Demo 创建的多维表格信息
APP_TOKEN = "D6PQbxf4aald77sPjDTciYbenjc"
TABLE_ID = "tblX3YbGrXm8pmLR"
class FeishuBitable:
"""飞书多维表格操作工具类"""
def __init__(self, app_id: str = ZHIYUN_APP_ID, app_secret: str = ZHIYUN_APP_SECRET):
self.app_id = app_id
self.app_secret = app_secret
self._token = None
self._token_expires = None
@property
def token(self) -> str:
if self._token and self._token_expires and datetime.now() < self._token_expires:
return self._token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": self.app_id,
"app_secret": self.app_secret
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
self._token = data["tenant_access_token"]
self._token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return self._token
@property
def headers(self):
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def create_field(self, app_token: str, table_id: str, field_name: str, field_type: int, property: dict = None):
"""创建新字段"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
payload = {
"field_name": field_name,
"type": field_type
}
if property:
payload["property"] = property
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建字段失败: {data}")
print(f"[OK] 字段 '{field_name}' 创建成功")
return data["data"]["field"]
def upload_media(self, app_token: str, file_path: str, file_name: str = None):
"""上传附件到多维表格"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
if file_name is None:
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
headers = {
"Authorization": f"Bearer {self.token}"
}
with open(file_path, 'rb') as f:
files = {
'file': (file_name, f, 'image/png')
}
data = {
'file_name': file_name,
'parent_type': 'bitable_image',
'parent_node': app_token,
'size': str(file_size)
}
response = requests.post(url, headers=headers, files=files, data=data)
result = response.json()
if result.get("code") != 0:
raise Exception(f"上传失败: {result}")
file_token = result["data"]["file_token"]
print(f"[OK] 文件 '{file_name}' 上传成功, file_token: {file_token}")
return file_token
def list_records(self, app_token: str, table_id: str, filter_str: str = None):
"""列出记录"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {"page_size": 100}
if filter_str:
params["filter"] = filter_str
response = requests.get(url, headers=self.headers, params=params)
data = response.json()
if data.get("code") != 0:
raise Exception(f"查询失败: {data}")
return data["data"].get("items", [])
def update_record(self, app_token: str, table_id: str, record_id: str, fields: dict):
"""更新记录"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}"
response = requests.put(url, headers=self.headers, json={"fields": fields})
data = response.json()
if data.get("code") != 0:
raise Exception(f"更新失败: {data}")
print(f"[OK] 记录 {record_id} 更新成功")
return data["data"]["record"]
def generate_sample_images(output_dir: str):
"""生成2张示例需求图片"""
os.makedirs(output_dir, exist_ok=True)
images = []
# 图片1: 用户流程图
img1 = Image.new('RGB', (800, 600), color='#f0f4f8')
draw1 = ImageDraw.Draw(img1)
# 绘制标题
draw1.rectangle([50, 30, 750, 80], fill='#4a90d9', outline='#2563eb')
draw1.text((400, 55), "用户登录流程图", fill='white', anchor='mm')
# 绘制流程框
boxes = [
(150, 150, "打开登录页"),
(400, 150, "输入账号密码"),
(650, 150, "点击登录"),
(150, 300, "验证账号"),
(400, 300, "生成Token"),
(650, 300, "跳转首页"),
]
for x, y, text in boxes:
draw1.rectangle([x-60, y-25, x+60, y+25], fill='#e8f4fd', outline='#4a90d9', width=2)
draw1.text((x, y), text, fill='#1e3a5f', anchor='mm')
# 绘制箭头线
arrows = [
(210, 150, 340, 150),
(460, 150, 590, 150),
(650, 175, 650, 275),
(590, 300, 460, 300),
(340, 300, 210, 300),
]
for x1, y1, x2, y2 in arrows:
draw1.line([(x1, y1), (x2, y2)], fill='#4a90d9', width=2)
# 添加水印
draw1.text((400, 550), "Claude Code Demo - 需求图片1", fill='#94a3b8', anchor='mm')
img1_path = os.path.join(output_dir, "requirement_flow.png")
img1.save(img1_path)
images.append(img1_path)
print(f"[OK] 生成图片: {img1_path}")
# 图片2: 界面原型图
img2 = Image.new('RGB', (800, 600), color='#ffffff')
draw2 = ImageDraw.Draw(img2)
# 绘制浏览器框架
draw2.rectangle([50, 30, 750, 570], outline='#d1d5db', width=2)
draw2.rectangle([50, 30, 750, 70], fill='#f3f4f6', outline='#d1d5db')
# 浏览器按钮
draw2.ellipse([70, 42, 86, 58], fill='#ef4444')
draw2.ellipse([95, 42, 111, 58], fill='#eab308')
draw2.ellipse([120, 42, 136, 58], fill='#22c55e')
# 地址栏
draw2.rectangle([160, 42, 600, 58], fill='white', outline='#d1d5db')
draw2.text((170, 50), "https://example.com/login", fill='#6b7280', anchor='lm')
# 登录表单区域
draw2.rectangle([200, 120, 600, 500], fill='#f8fafc', outline='#e2e8f0', width=1)
# Logo 占位
draw2.ellipse([350, 140, 450, 200], fill='#4a90d9')
draw2.text((400, 170), "LOGO", fill='white', anchor='mm')
# 标题
draw2.text((400, 230), "欢迎登录", fill='#1e293b', anchor='mm')
# 输入框
draw2.rectangle([250, 270, 550, 310], fill='white', outline='#cbd5e1')
draw2.text((260, 290), "请输入用户名", fill='#94a3b8', anchor='lm')
draw2.rectangle([250, 330, 550, 370], fill='white', outline='#cbd5e1')
draw2.text((260, 350), "请输入密码", fill='#94a3b8', anchor='lm')
# 登录按钮
draw2.rectangle([250, 400, 550, 450], fill='#4a90d9', outline='#2563eb')
draw2.text((400, 425), "登 录", fill='white', anchor='mm')
# 底部链接
draw2.text((320, 480), "忘记密码", fill='#4a90d9', anchor='mm')
draw2.text((480, 480), "注册账号", fill='#4a90d9', anchor='mm')
# 水印
draw2.text((400, 550), "Claude Code Demo - 需求图片2", fill='#94a3b8', anchor='mm')
img2_path = os.path.join(output_dir, "requirement_ui.png")
img2.save(img2_path)
images.append(img2_path)
print(f"[OK] 生成图片: {img2_path}")
return images
def main():
print("\n" + "#" * 60)
print("# 添加需求图片到多维表格")
print("#" * 60)
bitable = FeishuBitable()
# Step 1: 生成示例图片
print("\n" + "=" * 50)
print("Step 1: 生成示例图片")
print("=" * 50)
output_dir = "/tmp/feishu_demo_images"
image_paths = generate_sample_images(output_dir)
# Step 2: 添加"需求图片"字段
print("\n" + "=" * 50)
print("Step 2: 添加「需求图片」字段")
print("=" * 50)
try:
bitable.create_field(
APP_TOKEN,
TABLE_ID,
"需求图片",
17 # 17 = 附件类型
)
except Exception as e:
if "FieldNameExist" in str(e) or "FieldNameDuplicated" in str(e) or "1254043" in str(e) or "1254014" in str(e):
print("[INFO] 字段「需求图片」已存在,跳过创建")
else:
raise
# Step 3: 上传图片
print("\n" + "=" * 50)
print("Step 3: 上传图片到飞书")
print("=" * 50)
file_tokens = []
for img_path in image_paths:
file_token = bitable.upload_media(APP_TOKEN, img_path)
file_tokens.append(file_token)
# Step 4: 查找"完成产品需求文档"记录
print("\n" + "=" * 50)
print("Step 4: 查找目标记录")
print("=" * 50)
records = bitable.list_records(APP_TOKEN, TABLE_ID)
target_record = None
for record in records:
if record["fields"].get("任务名称") == "完成产品需求文档":
target_record = record
break
if not target_record:
raise Exception("未找到「完成产品需求文档」记录")
print(f"[OK] 找到记录: {target_record['record_id']}")
print(f" 任务名称: {target_record['fields'].get('任务名称')}")
# Step 5: 更新记录,添加图片
print("\n" + "=" * 50)
print("Step 5: 更新记录,添加图片附件")
print("=" * 50)
# 构造附件字段值
attachments = [{"file_token": ft} for ft in file_tokens]
bitable.update_record(
APP_TOKEN,
TABLE_ID,
target_record["record_id"],
{"需求图片": attachments}
)
# 完成
print("\n" + "=" * 60)
print("完成!")
print("=" * 60)
print(f"\n已添加 {len(file_tokens)} 张图片到「完成产品需求文档」记录")
print(f"\n访问地址查看效果:")
print(f" https://zhiyuncai.feishu.cn/base/{APP_TOKEN}?table={TABLE_ID}")
print()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,530 @@
#!/usr/bin/env python3
"""
ai-proj 与飞书项目同步脚本
功能:
- 初始化:创建飞书根目录、多维表格
- 全量同步:同步所有项目到飞书
- 增量同步:只同步新增项目
- 更新同步:更新现有项目的任务统计等信息
使用方法:
python aiproj_sync.py init # 首次初始化
python aiproj_sync.py sync # 增量同步(推荐日常使用)
python aiproj_sync.py sync-all # 全量同步
python aiproj_sync.py update # 更新统计信息
"""
import os
import sys
import json
import requests
from datetime import datetime
from typing import Optional, Dict, List, Any
# ============================================
# 配置
# ============================================
FEISHU_APP_ID = os.getenv("FEISHU_APP_ID", "cli_a9f29dca82b9dbef")
FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho")
# 默认协作者 open_id邱栋梁@智云采购)
# 应用创建的文件夹所有者是应用本身,需要显式添加用户为协作者
DEFAULT_COLLABORATOR_OPENID = "ou_43784ff7c819ac000095fb52a4c3d1c7"
AIPROJ_API_BASE = "https://ai.pipexerp.com/api/v1"
AIPROJ_TOKEN = os.getenv("AIPROJ_TOKEN", "aiproj_pk_b455c91607414c22a0f3d8f09785969f1aa2144f33f1336fbb12450ecebfdb64")
CONFIG_PATH = os.path.expanduser("~/.config/aiproj-feishu-sync.json")
# ============================================
# 飞书 API
# ============================================
class FeishuAPI:
def __init__(self):
self.token = None
def get_token(self) -> str:
"""获取 tenant_access_token"""
if self.token:
return self.token
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": FEISHU_APP_ID,
"app_secret": FEISHU_APP_SECRET
})
data = response.json()
if data.get("code") == 0:
self.token = data["tenant_access_token"]
return self.token
else:
raise Exception(f"获取飞书 token 失败: {data}")
def _headers(self, content_type: bool = False) -> Dict:
headers = {"Authorization": f"Bearer {self.get_token()}"}
if content_type:
headers["Content-Type"] = "application/json"
return headers
def get_root_folder(self) -> str:
"""获取云空间根文件夹"""
url = "https://open.feishu.cn/open-apis/drive/explorer/v2/root_folder/meta"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["token"]
raise Exception(f"获取根文件夹失败: {data}")
def create_folder(self, name: str, parent_token: str, add_collaborator: bool = True) -> str:
"""创建文件夹"""
url = "https://open.feishu.cn/open-apis/drive/v1/files/create_folder"
response = requests.post(url, headers=self._headers(True), json={
"name": name,
"folder_token": parent_token
})
data = response.json()
if data.get("code") == 0:
folder_token = data["data"]["token"]
# 自动添加协作者(应用创建的文件夹默认只有应用能访问)
if add_collaborator and DEFAULT_COLLABORATOR_OPENID:
self.add_collaborator(folder_token, "folder", DEFAULT_COLLABORATOR_OPENID)
return folder_token
raise Exception(f"创建文件夹失败: {data}")
def add_collaborator(self, file_token: str, file_type: str, user_open_id: str, perm: str = "full_access") -> bool:
"""添加协作者
应用通过 API 创建的文件/文件夹,所有者是应用本身。
组织内用户默认无法访问,需要显式添加为协作者。
Args:
file_token: 文件/文件夹 token
file_type: 类型 (folder, doc, sheet, bitable 等)
user_open_id: 用户 open_id
perm: 权限级别 (full_access, edit, view)
"""
url = f"https://open.feishu.cn/open-apis/drive/v1/permissions/{file_token}/members"
params = {"type": file_type, "need_notification": "false"}
payload = {
"member_type": "openid",
"member_id": user_open_id,
"perm": perm
}
response = requests.post(url, headers=self._headers(True), params=params, json=payload)
return response.json().get("code") == 0
def create_bitable(self, name: str, folder_token: str) -> Dict:
"""创建多维表格"""
url = "https://open.feishu.cn/open-apis/bitable/v1/apps"
response = requests.post(url, headers=self._headers(True), json={
"name": name,
"folder_token": folder_token
})
data = response.json()
if data.get("code") == 0:
return data["data"]["app"]
raise Exception(f"创建多维表格失败: {data}")
def get_bitable_tables(self, app_token: str) -> List[Dict]:
"""获取数据表列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["items"]
raise Exception(f"获取数据表失败: {data}")
def create_field(self, app_token: str, table_id: str, field: Dict) -> Optional[Dict]:
"""创建字段"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
response = requests.post(url, headers=self._headers(True), json=field)
data = response.json()
if data.get("code") == 0:
return data["data"]["field"]
print(f" 字段创建失败: {field.get('field_name')} - {data.get('msg')}")
return None
def update_field(self, app_token: str, table_id: str, field_id: str, updates: Dict) -> bool:
"""更新字段"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields/{field_id}"
response = requests.put(url, headers=self._headers(True), json=updates)
return response.json().get("code") == 0
def get_fields(self, app_token: str, table_id: str) -> List[Dict]:
"""获取字段列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["items"]
return []
def add_record(self, app_token: str, table_id: str, fields: Dict) -> Optional[Dict]:
"""添加记录"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
response = requests.post(url, headers=self._headers(True), json={"fields": fields})
data = response.json()
if data.get("code") == 0:
return data["data"]["record"]
print(f" 记录添加失败: {data.get('msg')}")
return None
def get_records(self, app_token: str, table_id: str, filter_str: str = None) -> List[Dict]:
"""获取记录列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {"page_size": 500}
if filter_str:
params["filter"] = filter_str
response = requests.get(url, headers=self._headers(), params=params)
data = response.json()
if data.get("code") == 0:
return data["data"].get("items", [])
return []
def update_record(self, app_token: str, table_id: str, record_id: str, fields: Dict) -> bool:
"""更新记录"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}"
response = requests.put(url, headers=self._headers(True), json={"fields": fields})
return response.json().get("code") == 0
# ============================================
# ai-proj API
# ============================================
class AIProjAPI:
@staticmethod
def get_projects() -> List[Dict]:
"""获取项目列表"""
url = f"{AIPROJ_API_BASE}/projects?page_size=100"
headers = {"Authorization": f"Bearer {AIPROJ_TOKEN}"}
response = requests.get(url, headers=headers)
data = response.json()
if data.get("success"):
return data["data"]["data"]
raise Exception(f"获取项目列表失败: {data}")
# ============================================
# 配置管理
# ============================================
def load_config() -> Optional[Dict]:
"""加载配置"""
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH) as f:
return json.load(f)
return None
def save_config(config: Dict):
"""保存配置"""
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_PATH, "w") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# ============================================
# 同步逻辑
# ============================================
def init():
"""初始化:创建飞书目录结构"""
print("=" * 50)
print("飞书 ai-proj 项目同步 - 初始化")
print("=" * 50)
if load_config():
print("\n已存在配置文件,是否重新初始化?(y/N)")
if input().lower() != 'y':
print("取消初始化")
return
feishu = FeishuAPI()
# 1. 获取根目录
print("\n[1/5] 获取云空间根目录...")
space_root = feishu.get_root_folder()
print(f" 根目录: {space_root}")
# 2. 创建 ai-proj 文件夹
print("\n[2/5] 创建 ai-proj 根文件夹...")
root_folder = feishu.create_folder("ai-proj", space_root)
print(f" 创建成功: {root_folder}")
# 3. 创建多维表格
print("\n[3/5] 创建项目目录多维表格...")
bitable = feishu.create_bitable("项目目录", root_folder)
app_token = bitable["app_token"]
print(f" 创建成功: {app_token}")
tables = feishu.get_bitable_tables(app_token)
table_id = tables[0]["table_id"]
# 4. 创建字段
print("\n[4/5] 创建数据表字段...")
# 先修改第一列名称
fields = feishu.get_fields(app_token, table_id)
if fields:
feishu.update_field(app_token, table_id, fields[0]["field_id"], {
"field_name": "项目名称",
"type": fields[0]["type"]
})
print(" 修改字段: 项目名称")
# 创建其他字段
field_definitions = [
{"field_name": "项目ID", "type": 2},
{"field_name": "项目编号", "type": 1},
{"field_name": "飞书文件夹", "type": 15},
{"field_name": "状态", "type": 3, "property": {"options": [
{"name": "active", "color": 0},
{"name": "on_hold", "color": 1},
{"name": "planning", "color": 2},
{"name": "completed", "color": 3},
{"name": "archived", "color": 4}
]}},
{"field_name": "优先级", "type": 3, "property": {"options": [
{"name": "high", "color": 0},
{"name": "medium", "color": 1},
{"name": "low", "color": 2}
]}},
{"field_name": "所属企业", "type": 1},
{"field_name": "任务总数", "type": 2},
{"field_name": "描述", "type": 1},
{"field_name": "创建时间", "type": 5},
{"field_name": "最后同步", "type": 5},
]
for field in field_definitions:
if feishu.create_field(app_token, table_id, field):
print(f" 创建字段: {field['field_name']}")
# 5. 保存配置
print("\n[5/5] 保存配置...")
config = {
"root_folder_token": root_folder,
"bitable_app_token": app_token,
"bitable_table_id": table_id,
"created_at": datetime.now().isoformat(),
"synced_projects": {}
}
save_config(config)
print("\n" + "=" * 50)
print("初始化完成!")
print("=" * 50)
print(f"\n根文件夹: https://feishu.cn/drive/folder/{root_folder}")
print(f"多维表格: https://feishu.cn/base/{app_token}")
print(f"\n配置文件: {CONFIG_PATH}")
print("\n运行 'python aiproj_sync.py sync' 同步项目")
def sync(full: bool = False):
"""同步项目"""
config = load_config()
if not config:
print("未找到配置,请先运行 init")
return
print("=" * 50)
print(f"飞书 ai-proj 项目同步 - {'全量' if full else '增量'}同步")
print("=" * 50)
feishu = FeishuAPI()
app_token = config["bitable_app_token"]
table_id = config["bitable_table_id"]
root_folder = config["root_folder_token"]
synced = config.get("synced_projects", {})
# 获取 ai-proj 项目
print("\n获取 ai-proj 项目列表...")
projects = AIProjAPI.get_projects()
print(f"{len(projects)} 个项目")
# 筛选需要同步的项目
if full:
to_sync = projects
else:
to_sync = [p for p in projects if str(p["id"]) not in synced]
if not to_sync:
print("\n没有新项目需要同步")
return
print(f"\n需要同步 {len(to_sync)} 个项目...")
success = 0
for project in to_sync:
project_id = str(project["id"])
project_name = project["name"]
folder_name = f"{project_name}_{project_id}"
print(f"\n 处理: {project_name} (ID: {project_id})")
# 创建文件夹(如果不存在)
folder_token = synced.get(project_id, {}).get("folder_token")
if not folder_token:
try:
folder_token = feishu.create_folder(folder_name, root_folder)
print(f" 创建文件夹: {folder_token}")
except Exception as e:
print(f" 文件夹创建失败: {e}")
folder_token = ""
folder_url = f"https://feishu.cn/drive/folder/{folder_token}" if folder_token else ""
# 添加/更新记录
record_fields = {
"项目名称": project_name,
"项目ID": int(project_id),
"项目编号": project.get("project_number", ""),
"飞书文件夹": {"link": folder_url, "text": folder_name} if folder_url else None,
"状态": project.get("status", "active"),
"优先级": project.get("priority", "medium"),
"所属企业": project.get("company_name", ""),
"任务总数": project.get("task_count", 0),
"描述": (project.get("description", "") or "")[:500],
"创建时间": int(datetime.fromisoformat(
project["created_at"].replace("Z", "+00:00")
).timestamp() * 1000),
"最后同步": int(datetime.now().timestamp() * 1000)
}
record_id = synced.get(project_id, {}).get("record_id")
if record_id and not full:
# 更新现有记录
if feishu.update_record(app_token, table_id, record_id, record_fields):
print(f" 更新记录成功")
success += 1
else:
# 添加新记录
record = feishu.add_record(app_token, table_id, record_fields)
if record:
record_id = record["record_id"]
print(f" 添加记录成功")
success += 1
# 更新同步记录
synced[project_id] = {
"folder_token": folder_token,
"folder_url": folder_url,
"record_id": record_id,
"synced_at": datetime.now().isoformat()
}
# 保存配置
config["synced_projects"] = synced
config["last_sync"] = datetime.now().isoformat()
save_config(config)
print("\n" + "=" * 50)
print(f"同步完成: {success}/{len(to_sync)}")
print("=" * 50)
print(f"\n多维表格: https://feishu.cn/base/{app_token}")
def update_stats():
"""更新任务统计信息"""
config = load_config()
if not config:
print("未找到配置,请先运行 init")
return
print("=" * 50)
print("飞书 ai-proj 项目同步 - 更新统计")
print("=" * 50)
feishu = FeishuAPI()
app_token = config["bitable_app_token"]
table_id = config["bitable_table_id"]
synced = config.get("synced_projects", {})
# 获取最新项目数据
print("\n获取 ai-proj 项目列表...")
projects = AIProjAPI.get_projects()
project_map = {str(p["id"]): p for p in projects}
updated = 0
for project_id, sync_info in synced.items():
record_id = sync_info.get("record_id")
if not record_id:
continue
project = project_map.get(project_id)
if not project:
continue
# 只更新统计字段
update_fields = {
"任务总数": project.get("task_count", 0),
"状态": project.get("status", "active"),
"最后同步": int(datetime.now().timestamp() * 1000)
}
if feishu.update_record(app_token, table_id, record_id, update_fields):
print(f" 更新: {project['name']} - 任务数: {project.get('task_count', 0)}")
updated += 1
config["last_sync"] = datetime.now().isoformat()
save_config(config)
print(f"\n更新完成: {updated} 个项目")
def show_status():
"""显示同步状态"""
config = load_config()
if not config:
print("未初始化,请先运行: python aiproj_sync.py init")
return
print("=" * 50)
print("ai-proj 飞书同步状态")
print("=" * 50)
print(f"\n根文件夹: https://feishu.cn/drive/folder/{config['root_folder_token']}")
print(f"多维表格: https://feishu.cn/base/{config['bitable_app_token']}")
print(f"已同步项目: {len(config.get('synced_projects', {}))}")
print(f"上次同步: {config.get('last_sync', '从未')}")
print(f"配置文件: {CONFIG_PATH}")
# ============================================
# 主入口
# ============================================
def main():
if len(sys.argv) < 2:
print("用法: python aiproj_sync.py <command>")
print("\n命令:")
print(" init 首次初始化(创建目录和多维表格)")
print(" sync 增量同步(只同步新项目)")
print(" sync-all 全量同步(同步所有项目)")
print(" update 更新统计信息")
print(" status 查看同步状态")
return
command = sys.argv[1]
if command == "init":
init()
elif command == "sync":
sync(full=False)
elif command == "sync-all":
sync(full=True)
elif command == "update":
update_stats()
elif command == "status":
show_status()
else:
print(f"未知命令: {command}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
检查飞书云文档中的图片块状态
"""
import requests
from datetime import datetime, timedelta
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
# 最近创建的测试文档
DOCUMENT_ID = "Z53YdDpezob1NPx63sQcsrt8nzd"
def get_token():
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
return data["tenant_access_token"]
def get_document_blocks(document_id: str):
"""获取文档所有块"""
token = get_token()
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取块失败: {data}")
return data["data"].get("items", [])
def main():
print(f"\n检查文档: {DOCUMENT_ID}")
print("=" * 60)
blocks = get_document_blocks(DOCUMENT_ID)
print(f"\n文档共有 {len(blocks)} 个块:\n")
for i, block in enumerate(blocks):
block_type = block.get("block_type")
block_id = block.get("block_id")
# 块类型映射
type_names = {
1: "page",
2: "text",
3: "heading1",
4: "heading2",
5: "heading3",
12: "bullet",
13: "ordered",
14: "code",
17: "todo",
22: "divider",
27: "image",
}
type_name = type_names.get(block_type, f"type_{block_type}")
print(f" [{i}] block_type={block_type} ({type_name}), block_id={block_id[:20]}...")
# 如果是图片块,显示详细信息
if block_type == 27:
image_data = block.get("image", {})
print(f" image data: {image_data}")
# 检查图片是否有效
file_token = image_data.get("token") or image_data.get("file_token")
if file_token:
print(f" file_token: {file_token}")
# 尝试获取图片信息
check_image_status(file_token)
else:
print(f" [WARN] 图片块没有 token!")
def check_image_status(file_token: str):
"""检查图片状态"""
token = get_token()
# 尝试获取文件元信息
url = f"{BASE_URL}/drive/v1/medias/{file_token}"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
data = response.json()
print(f" 图片状态: code={data.get('code')}, msg={data.get('msg')}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""检查文档中的图片块状态"""
import requests
from datetime import datetime, timedelta
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
DOCUMENT_ID = "Eqt2dpcpToVDxExFmhicqFa9nJf"
def get_token():
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
return response.json()["tenant_access_token"]
def main():
token = get_token()
headers = {"Authorization": f"Bearer {token}"}
url = f"{BASE_URL}/docx/v1/documents/{DOCUMENT_ID}/blocks"
response = requests.get(url, headers=headers)
data = response.json()
if data.get("code") != 0:
print(f"获取失败: {data}")
return
blocks = data["data"].get("items", [])
print(f"文档共有 {len(blocks)} 个块\n")
image_count = 0
for block in blocks:
if block.get("block_type") == 27:
image_count += 1
image_data = block.get("image", {})
token_value = image_data.get("token", "")
print(f"图片块 #{image_count}:")
print(f" block_id: {block.get('block_id')}")
print(f" token: {token_value if token_value else '(空)'}")
print(f" width: {image_data.get('width')}, height: {image_data.get('height')}")
print()
if image_count == 0:
print("文档中没有图片块")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,423 @@
#!/usr/bin/env python3
"""
创建新的飞书文档ai-proj 项目可见性手册
"""
import requests
import os
import time
from datetime import datetime, timedelta
from PIL import Image, ImageDraw, ImageFont
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
_token = None
_token_expires = None
def get_token():
global _token, _token_expires
if _token and _token_expires and datetime.now() < _token_expires:
return _token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
_token = data["tenant_access_token"]
_token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return _token
def headers():
return {
"Authorization": f"Bearer {get_token()}",
"Content-Type": "application/json"
}
def create_document(title: str):
"""创建文档"""
url = f"{BASE_URL}/docx/v1/documents"
response = requests.post(url, headers=headers(), json={"title": title})
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建文档失败: {data}")
doc = data["data"]["document"]
document_id = doc["document_id"]
print(f"[OK] 文档创建成功: {document_id}")
return document_id
def set_document_permission(document_id: str):
"""设置文档权限为组织内可编辑"""
url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public"
payload = {
"external_access_entity": "open",
"security_entity": "anyone_can_view",
"comment_entity": "anyone_can_view",
"share_entity": "anyone",
"link_share_entity": "tenant_editable",
"invite_external": False
}
response = requests.patch(url, headers=headers(), params={"type": "docx"}, json=payload)
result = response.json()
if result.get("code") == 0:
print("[OK] 权限设置成功: 组织内可编辑")
return True
else:
print(f"[WARN] 权限设置: {result.get('msg')}")
return False
def get_document_blocks(document_id: str):
"""获取文档所有块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks"
response = requests.get(url, headers=headers())
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取块失败: {data}")
return data["data"].get("items", [])
def create_blocks(document_id: str, parent_id: str, blocks: list):
"""创建内容块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children"
payload = {"children": blocks}
response = requests.post(url, headers=headers(), params={"document_revision_id": -1}, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建块失败: {data}")
return data["data"].get("children", [])
def create_image_block(document_id: str, parent_id: str):
"""创建空图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children"
payload = {
"children": [{
"block_type": 27,
"image": {}
}]
}
response = requests.post(url, headers=headers(), params={"document_revision_id": -1}, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建图片块失败: {data}")
return data["data"]["children"][0]["block_id"]
def upload_image(file_path: str, block_id: str):
"""上传图片"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, 'image/png')}
data = {
'file_name': file_name,
'parent_type': 'docx_image',
'parent_node': block_id,
'size': str(file_size)
}
response = requests.post(
url,
headers={"Authorization": f"Bearer {get_token()}"},
files=files,
data=data
)
result = response.json()
if result.get("code") != 0:
raise Exception(f"上传失败: {result}")
return result["data"]["file_token"]
def bind_image(document_id: str, block_id: str, file_token: str):
"""绑定图片到图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
payload = {"replace_image": {"token": file_token}}
response = requests.patch(url, headers=headers(), params={"document_revision_id": -1}, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"绑定失败: {data}")
return True
def insert_image(document_id: str, file_path: str, description: str = ""):
"""插入图片的完整流程:创建块 -> 上传 -> 绑定"""
print(f" 插入图片: {description}")
# Step 1: 创建空图片块
block_id = create_image_block(document_id, document_id)
print(f" block_id: {block_id}")
# Step 2: 上传图片
file_token = upload_image(file_path, block_id)
print(f" file_token: {file_token}")
# Step 3: 绑定图片到块
bind_image(document_id, block_id, file_token)
print(f" 绑定成功!")
# 等待处理
time.sleep(0.5)
return block_id, file_token
def generate_visibility_diagram():
"""生成可见性示意图"""
output_path = "/tmp/visibility_diagram.png"
img = Image.new('RGB', (800, 400), color='#ffffff')
draw = ImageDraw.Draw(img)
# 尝试加载字体
try:
font_large = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 24)
font_medium = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 18)
font_small = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 14)
except:
font_large = ImageFont.load_default()
font_medium = ImageFont.load_default()
font_small = ImageFont.load_default()
# 标题背景
draw.rectangle([0, 0, 800, 60], fill='#1890ff')
draw.text((400, 30), "ai-proj 项目可见性示意图", fill='white', anchor='mm', font=font_large)
# 私有项目区域
draw.rectangle([40, 80, 380, 370], fill='#fff7e6', outline='#fa8c16', width=3)
draw.text((210, 110), "🔒 私有项目", fill='#fa8c16', anchor='mm', font=font_medium)
draw.text((210, 140), "(private)", fill='#d48806', anchor='mm', font=font_small)
draw.text((210, 180), "仅项目创建者可见", fill='#666666', anchor='mm', font=font_small)
# 私有项目图标
draw.ellipse([175, 210, 245, 280], fill='#fa8c16')
draw.text((210, 245), "👤", fill='white', anchor='mm', font=font_large)
draw.text((210, 310), "项目所有者", fill='#333333', anchor='mm', font=font_small)
draw.text((210, 335), "管理员", fill='#999999', anchor='mm', font=font_small)
# 企业项目区域
draw.rectangle([420, 80, 760, 370], fill='#e6f7ff', outline='#1890ff', width=3)
draw.text((590, 110), "👥 企业项目", fill='#1890ff', anchor='mm', font=font_medium)
draw.text((590, 140), "(enterprise)", fill='#096dd9', anchor='mm', font=font_small)
draw.text((590, 180), "企业内所有成员可见", fill='#666666', anchor='mm', font=font_small)
# 企业项目图标组
positions = [(520, 230), (590, 230), (660, 230), (555, 290), (625, 290)]
for x, y in positions:
draw.ellipse([x-25, y-25, x+25, y+25], fill='#1890ff')
draw.text((x, y), "👤", fill='white', anchor='mm', font=font_medium)
draw.text((590, 340), "企业所有成员", fill='#333333', anchor='mm', font=font_small)
# 中间箭头
draw.text((400, 225), "", fill='#333333', anchor='mm', font=font_large)
img.save(output_path)
print(f"[OK] 生成可见性示意图: {output_path}")
return output_path
def generate_ui_screenshot():
"""生成 UI 示意图"""
output_path = "/tmp/visibility_ui.png"
img = Image.new('RGB', (700, 250), color='#fafafa')
draw = ImageDraw.Draw(img)
try:
font_medium = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 16)
font_small = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 12)
except:
font_medium = ImageFont.load_default()
font_small = ImageFont.load_default()
# 标题
draw.text((30, 25), "创建项目 - 可见性选择", fill='#333333', font=font_medium)
# 私有项目选项(选中状态)
draw.rectangle([30, 70, 320, 140], fill='#fff7e6', outline='#fa8c16', width=2)
draw.ellipse([45, 95, 65, 115], fill='#fa8c16')
draw.text((80, 90), "🔒 私有项目", fill='#fa8c16', font=font_medium)
draw.text((80, 115), "仅创建者可见", fill='#999999', font=font_small)
# 默认标签
draw.rectangle([250, 85, 310, 110], fill='#52c41a')
draw.text((280, 97), "默认", fill='white', anchor='mm', font=font_small)
# 企业项目选项(未选中状态)
draw.rectangle([350, 70, 640, 140], fill='#ffffff', outline='#d9d9d9', width=1)
draw.ellipse([365, 95, 385, 115], outline='#d9d9d9', width=2)
draw.text((400, 90), "👥 企业项目", fill='#666666', font=font_medium)
draw.text((400, 115), "企业内所有成员可见", fill='#999999', font=font_small)
# 底部说明
draw.rectangle([30, 170, 670, 220], fill='#f0f5ff', outline='#adc6ff', width=1)
draw.text((50, 185), "💡 提示:新创建的项目默认为私有项目。", fill='#1890ff', font=font_small)
draw.text((50, 205), "设置为企业项目后,项目需要先关联企业才能被企业成员访问。", fill='#666666', font=font_small)
img.save(output_path)
print(f"[OK] 生成 UI 示意图: {output_path}")
return output_path
# 文档内容块定义
def heading1(text):
return {"block_type": 3, "heading1": {"elements": [{"text_run": {"content": text}}]}}
def heading2(text):
return {"block_type": 4, "heading2": {"elements": [{"text_run": {"content": text}}]}}
def heading3(text):
return {"block_type": 5, "heading3": {"elements": [{"text_run": {"content": text}}]}}
def text_block(content):
return {"block_type": 2, "text": {"elements": [{"text_run": {"content": content}}]}}
def bullet(content):
return {"block_type": 12, "bullet": {"elements": [{"text_run": {"content": content}}]}}
def ordered(content):
return {"block_type": 13, "ordered": {"elements": [{"text_run": {"content": content}}]}}
def code_block(content, language=1):
return {"block_type": 14, "code": {"elements": [{"text_run": {"content": content}}], "language": language}}
def divider():
return {"block_type": 22, "divider": {}}
def main():
print("\n" + "#" * 60)
print("# 创建新文档ai-proj 项目可见性手册")
print("#" * 60)
# Step 1: 生成示意图
print("\n--- Step 1: 生成示意图 ---")
diagram_path = generate_visibility_diagram()
ui_path = generate_ui_screenshot()
# Step 2: 创建新文档
print("\n--- Step 2: 创建新文档 ---")
doc_id = create_document("ai-proj 项目可见性手册")
set_document_permission(doc_id)
# Step 3: 创建手册内容
print("\n--- Step 3: 创建手册内容 ---")
# 第一部分:标题和概述
blocks_part1 = [
heading1("ai-proj 项目可见性手册"),
text_block("本手册介绍 ai-proj 系统中项目可见性功能的使用方法。"),
divider(),
heading2("1. 功能概述"),
text_block("项目可见性用于控制谁可以查看和访问您的项目。ai-proj 支持两种可见性级别:"),
bullet("私有项目 (private):仅项目创建者和管理员可见"),
bullet("企业项目 (enterprise):企业内所有成员可见"),
text_block("新创建的项目默认为「私有项目」,保护您的隐私。"),
divider(),
heading2("2. 可见性对比"),
text_block("下图展示了两种可见性的区别:"),
]
create_blocks(doc_id, doc_id, blocks_part1)
print(" 第一部分内容创建完成")
# 插入可见性示意图
print("\n--- Step 4: 插入可见性示意图 ---")
insert_image(doc_id, diagram_path, "可见性示意图")
# 第二部分:设置方法
print("\n--- Step 5: 添加设置说明 ---")
blocks_part2 = [
divider(),
heading2("3. 设置项目可见性"),
heading3("3.1 创建项目时设置"),
text_block("在创建项目时,您可以选择项目的可见性:"),
ordered("点击「创建项目」按钮"),
ordered("在弹出的表单中找到「项目可见性」选项"),
ordered("选择「私有项目」或「企业项目」"),
ordered("填写其他信息后点击「确定」"),
text_block("UI 界面示意:"),
]
create_blocks(doc_id, doc_id, blocks_part2)
print(" 设置说明创建完成")
# 插入 UI 示意图
print("\n--- Step 6: 插入 UI 示意图 ---")
insert_image(doc_id, ui_path, "UI 示意图")
# 第三部分:修改方法和规则
print("\n--- Step 7: 添加剩余内容 ---")
blocks_part3 = [
heading3("3.2 修改已有项目"),
ordered("在项目列表中找到目标项目"),
ordered("点击项目卡片上的「编辑」按钮"),
ordered("在编辑页面修改「项目可见性」"),
ordered("点击「保存」"),
divider(),
heading2("4. 可见性规则"),
bullet("私有项目:仅所有者和超级管理员可以查看"),
bullet("企业项目:需要项目已关联企业,企业内所有成员可以查看"),
bullet("只有项目所有者或管理员可以修改可见性"),
bullet("设置为「企业项目」需要项目已关联企业"),
divider(),
heading2("5. API 参考"),
text_block("修改项目可见性的 API"),
code_block('''PATCH /api/v1/projects/:id/visibility
请求体:
{
"visibility": "enterprise"
}
可选值: "private" | "enterprise"'''),
divider(),
heading2("6. 注意事项"),
bullet("新项目默认为私有,请根据需要调整可见性"),
bullet("将私有项目改为企业项目后,企业所有成员都能看到"),
bullet("将企业项目改为私有后,其他成员将无法访问"),
bullet("项目成员权限不受可见性影响,被添加为成员的用户始终可以访问"),
divider(),
text_block(f"最后更新:{datetime.now().strftime('%Y-%m-%d %H:%M')}"),
]
create_blocks(doc_id, doc_id, blocks_part3)
print("\n" + "=" * 60)
print("文档创建完成!")
print(f"\n文档地址: https://zhiyuncai.feishu.cn/docx/{doc_id}")
print("=" * 60)
# 验证图片
print("\n--- 验证图片状态 ---")
time.sleep(2) # 等待服务器处理
blocks = get_document_blocks(doc_id)
image_count = 0
for block in blocks:
if block.get("block_type") == 27:
image_count += 1
image_data = block.get("image", {})
token = image_data.get("token", "")
width = image_data.get("width", 0)
height = image_data.get("height", 0)
status = "✅ 有效" if token else "❌ 空"
print(f"图片 #{image_count}: {status} (token: {token[:15]}..., 尺寸: {width}x{height})")
if image_count == 0:
print("⚠️ 警告:文档中没有图片块")
elif image_count == 2:
print(f"\n✅ 成功上传 {image_count} 张图片")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
深入调试飞书云文档图片上传
"""
import requests
import os
from datetime import datetime, timedelta
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
_token = None
_token_expires = None
def get_token():
global _token, _token_expires
if _token and _token_expires and datetime.now() < _token_expires:
return _token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
_token = data["tenant_access_token"]
_token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return _token
def headers():
return {
"Authorization": f"Bearer {get_token()}",
"Content-Type": "application/json"
}
def set_document_permission(document_id: str, editable: bool = True):
"""设置文档权限"""
url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public"
payload = {
"external_access_entity": "open",
"security_entity": "anyone_can_view",
"comment_entity": "anyone_can_view",
"share_entity": "anyone",
"link_share_entity": "tenant_editable" if editable else "tenant_readable",
"invite_external": False
}
response = requests.patch(url, headers=headers(), params={"type": "docx"}, json=payload)
return response.json().get("code") == 0
def create_document(title: str, editable: bool = True):
"""创建文档(自动设置为组织内可编辑)"""
url = f"{BASE_URL}/docx/v1/documents"
response = requests.post(url, headers=headers(), json={"title": title})
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建文档失败: {data}")
doc = data["data"]["document"]
document_id = doc["document_id"]
print(f"[OK] 文档创建成功: {document_id}")
# 自动设置权限
if editable and set_document_permission(document_id, True):
print(f"[OK] 权限设置成功: 组织内可编辑")
return document_id
def get_raw_content(document_id: str):
"""获取文档原始内容"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/raw_content"
response = requests.get(url, headers=headers())
data = response.json()
print(f"[DEBUG] raw_content 响应: {data}")
return data
def get_blocks(document_id: str):
"""获取文档块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks"
# 尝试添加 document_revision_id 参数
params = {"document_revision_id": -1} # -1 表示最新版本
response = requests.get(url, headers=headers(), params=params)
data = response.json()
return data
def create_image_block(document_id: str):
"""创建图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{document_id}/children"
# 添加 document_revision_id 参数
params = {"document_revision_id": -1}
payload = {
"children": [{
"block_type": 27,
"image": {}
}]
}
response = requests.post(url, headers=headers(), params=params, json=payload)
data = response.json()
print(f"[DEBUG] 创建图片块响应: {data}")
if data.get("code") != 0:
raise Exception(f"创建图片块失败: {data}")
block_id = data["data"]["children"][0]["block_id"]
print(f"[OK] 图片块创建成功: {block_id}")
return block_id
def upload_image(file_path: str, block_id: str):
"""上传图片"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, 'image/png')}
data = {
'file_name': file_name,
'parent_type': 'docx_image',
'parent_node': block_id,
'size': str(file_size)
}
response = requests.post(
url,
headers={"Authorization": f"Bearer {get_token()}"},
files=files,
data=data
)
result = response.json()
print(f"[DEBUG] 上传响应: {result}")
if result.get("code") != 0:
raise Exception(f"上传失败: {result}")
return result["data"]["file_token"]
def bind_image_to_block(document_id: str, block_id: str, file_token: str):
"""
绑定图片到图片块 (关键步骤!)
使用 replace_image 字段通过 PATCH 请求绑定
"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
params = {"document_revision_id": -1}
# 正确的 payload: 使用 replace_image
payload = {
"replace_image": {
"token": file_token
}
}
print(f"[INFO] 绑定图片: PATCH {url}")
print(f"[INFO] payload: {payload}")
response = requests.patch(url, headers=headers(), params=params, json=payload)
result = response.json()
print(f"[DEBUG] 绑定响应: {result}")
if result.get("code") == 0:
print(f"[OK] 图片绑定成功!")
return True
else:
print(f"[ERROR] 图片绑定失败: code={result.get('code')}, msg={result.get('msg')}")
return False
def main():
print("\n" + "#" * 60)
print("# 深入调试飞书云文档图片上传")
print("#" * 60)
# 生成测试图片
from PIL import Image, ImageDraw
test_image = "/tmp/debug_test_image.png"
img = Image.new('RGB', (200, 150), color='#4a90d9')
draw = ImageDraw.Draw(img)
draw.text((100, 75), "Test", fill='white', anchor='mm')
img.save(test_image)
print(f"[OK] 测试图片: {test_image}")
# Step 1: 创建文档
print("\n--- Step 1: 创建文档 ---")
doc_id = create_document(f"调试图片上传 - {datetime.now().strftime('%H:%M:%S')}")
# Step 2: 创建图片块
print("\n--- Step 2: 创建图片块 ---")
block_id = create_image_block(doc_id)
# Step 3: 上传图片
print("\n--- Step 3: 上传图片 ---")
file_token = upload_image(test_image, block_id)
print(f"[OK] file_token: {file_token}")
# Step 4: 检查块状态
print("\n--- Step 4: 检查块状态 ---")
blocks = get_blocks(doc_id)
for item in blocks.get("data", {}).get("items", []):
if item.get("block_type") == 27:
print(f"[INFO] 图片块: {item}")
# Step 5: 绑定图片到图片块 (关键步骤!)
print("\n--- Step 5: 绑定图片到图片块 ---")
bind_image_to_block(doc_id, block_id, file_token)
# Step 6: 再次检查
print("\n--- Step 6: 再次检查块状态 ---")
import time
time.sleep(2) # 等待2秒
blocks = get_blocks(doc_id)
for item in blocks.get("data", {}).get("items", []):
if item.get("block_type") == 27:
print(f"[INFO] 图片块: {item}")
print(f"\n文档地址: https://feishu.cn/docx/{doc_id}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,409 @@
#!/usr/bin/env python3
"""
飞书多维表格 Demo
使用 zhiyun.ai 凭证演示多维表格的完整操作流程
"""
import requests
from datetime import datetime, timedelta
from typing import Optional, List, Dict
# ========== 配置 ==========
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
# ========== 工具类 ==========
class FeishuBitable:
"""飞书多维表格操作工具类"""
def __init__(self, app_id: str = ZHIYUN_APP_ID, app_secret: str = ZHIYUN_APP_SECRET):
self.app_id = app_id
self.app_secret = app_secret
self._token = None
self._token_expires = None
@property
def token(self) -> str:
"""获取或刷新 access token"""
if self._token and self._token_expires and datetime.now() < self._token_expires:
return self._token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": self.app_id,
"app_secret": self.app_secret
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
self._token = data["tenant_access_token"]
self._token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
print(f"[OK] Token 获取成功,有效期至 {self._token_expires}")
return self._token
@property
def headers(self) -> Dict[str, str]:
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
# ========== 多维表格管理 ==========
def create_bitable(self, name: str, folder_token: str = None) -> Dict:
"""
创建新的多维表格
Args:
name: 多维表格名称
folder_token: 文件夹 token可选不指定则创建在根目录
"""
url = f"{BASE_URL}/bitable/v1/apps"
payload = {"name": name}
if folder_token:
payload["folder_token"] = folder_token
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建多维表格失败: {data}")
app_info = data["data"]["app"]
print(f"[OK] 多维表格创建成功")
print(f" 名称: {app_info['name']}")
print(f" app_token: {app_info['app_token']}")
print(f" URL: {app_info.get('url', 'N/A')}")
return app_info
def list_tables(self, app_token: str) -> List[Dict]:
"""列出多维表格中的所有数据表"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables"
response = requests.get(url, headers=self.headers)
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取数据表列表失败: {data}")
tables = data["data"].get("items", [])
print(f"[OK] 找到 {len(tables)} 个数据表")
for t in tables:
print(f" - {t['name']} (table_id: {t['table_id']})")
return tables
def create_table(self, app_token: str, name: str, fields: List[Dict]) -> Dict:
"""
在多维表格中创建数据表
Args:
app_token: 多维表格 app_token
name: 数据表名称
fields: 字段定义列表
"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables"
payload = {
"table": {
"name": name,
"default_view_name": "默认视图",
"fields": fields
}
}
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建数据表失败: {data}")
table_info = data["data"]
print(f"[OK] 数据表创建成功")
print(f" 名称: {name}")
print(f" table_id: {table_info['table_id']}")
return table_info
# ========== 记录操作 ==========
def list_records(self, app_token: str, table_id: str,
filter_str: str = None, page_size: int = 100) -> List[Dict]:
"""列出所有记录(自动分页)"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/records"
all_records = []
page_token = None
while True:
params = {"page_size": min(page_size, 500)}
if page_token:
params["page_token"] = page_token
if filter_str:
params["filter"] = filter_str
response = requests.get(url, headers=self.headers, params=params)
data = response.json()
if data.get("code") != 0:
raise Exception(f"查询失败: {data}")
items = data["data"].get("items", [])
all_records.extend(items)
if not data["data"].get("has_more"):
break
page_token = data["data"]["page_token"]
return all_records
def create_record(self, app_token: str, table_id: str, fields: Dict) -> Dict:
"""创建单条记录"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/records"
response = requests.post(url, headers=self.headers, json={"fields": fields})
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建失败: {data}")
return data["data"]["record"]
def batch_create(self, app_token: str, table_id: str,
records: List[Dict], batch_size: int = 500) -> List[Dict]:
"""批量创建记录"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create"
created = []
for i in range(0, len(records), batch_size):
batch = [{"fields": r} if "fields" not in r else r for r in records[i:i+batch_size]]
response = requests.post(url, headers=self.headers, json={"records": batch})
data = response.json()
if data.get("code") != 0:
raise Exception(f"批量创建失败: {data}")
created.extend(data["data"]["records"])
return created
def get_fields(self, app_token: str, table_id: str) -> List[Dict]:
"""获取字段定义"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
response = requests.get(url, headers=self.headers)
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取字段失败: {data}")
return data["data"]["items"]
# ========== Demo 函数 ==========
def demo_verify_credentials():
"""验证凭证是否有效"""
print("\n" + "="*50)
print("Step 1: 验证飞书应用凭证")
print("="*50)
bitable = FeishuBitable()
token = bitable.token # 触发 token 获取
print(f" Token 前缀: {token[:20]}...")
return bitable
def demo_create_bitable(bitable: FeishuBitable) -> str:
"""创建新的多维表格"""
print("\n" + "="*50)
print("Step 2: 创建多维表格")
print("="*50)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
name = f"Claude Code Demo - {timestamp}"
app_info = bitable.create_bitable(name)
return app_info["app_token"]
def demo_create_task_table(bitable: FeishuBitable, app_token: str) -> str:
"""创建任务管理数据表"""
print("\n" + "="*50)
print("Step 3: 创建任务管理数据表")
print("="*50)
# 定义字段
fields = [
{
"field_name": "任务名称",
"type": 1 # 文本
},
{
"field_name": "状态",
"type": 3, # 单选
"property": {
"options": [
{"name": "待处理", "color": 0},
{"name": "进行中", "color": 1},
{"name": "已完成", "color": 2},
{"name": "已取消", "color": 3}
]
}
},
{
"field_name": "优先级",
"type": 3, # 单选
"property": {
"options": [
{"name": "", "color": 4},
{"name": "", "color": 5},
{"name": "", "color": 6}
]
}
},
{
"field_name": "负责人",
"type": 1 # 文本
},
{
"field_name": "截止日期",
"type": 5, # 日期
"property": {
"date_formatter": "yyyy/MM/dd"
}
},
{
"field_name": "工时(小时)",
"type": 2 # 数字
},
{
"field_name": "备注",
"type": 1 # 文本
}
]
table_info = bitable.create_table(app_token, "任务看板", fields)
return table_info["table_id"]
def demo_add_sample_data(bitable: FeishuBitable, app_token: str, table_id: str):
"""添加示例数据"""
print("\n" + "="*50)
print("Step 4: 添加示例数据")
print("="*50)
# 准备示例数据
now = datetime.now()
sample_tasks = [
{
"任务名称": "完成产品需求文档",
"状态": "已完成",
"优先级": "",
"负责人": "张三",
"截止日期": int((now - timedelta(days=2)).timestamp() * 1000),
"工时(小时)": 8,
"备注": "PRD 已评审通过"
},
{
"任务名称": "设计系统架构方案",
"状态": "进行中",
"优先级": "",
"负责人": "李四",
"截止日期": int((now + timedelta(days=3)).timestamp() * 1000),
"工时(小时)": 16,
"备注": "正在编写技术方案"
},
{
"任务名称": "开发用户登录模块",
"状态": "待处理",
"优先级": "",
"负责人": "王五",
"截止日期": int((now + timedelta(days=7)).timestamp() * 1000),
"工时(小时)": 24,
"备注": "等待架构方案确定"
},
{
"任务名称": "编写单元测试",
"状态": "待处理",
"优先级": "",
"负责人": "赵六",
"截止日期": int((now + timedelta(days=10)).timestamp() * 1000),
"工时(小时)": 12,
"备注": ""
},
{
"任务名称": "部署测试环境",
"状态": "待处理",
"优先级": "",
"负责人": "钱七",
"截止日期": int((now + timedelta(days=14)).timestamp() * 1000),
"工时(小时)": 4,
"备注": "需要申请服务器资源"
}
]
# 批量创建记录
created = bitable.batch_create(app_token, table_id, sample_tasks)
print(f"[OK] 成功创建 {len(created)} 条示例记录")
# 显示创建的记录
for i, record in enumerate(created, 1):
fields = record["fields"]
print(f" {i}. {fields.get('任务名称')} - {fields.get('状态')} ({fields.get('优先级')}优先级)")
return created
def demo_query_data(bitable: FeishuBitable, app_token: str, table_id: str):
"""查询数据演示"""
print("\n" + "="*50)
print("Step 5: 查询数据演示")
print("="*50)
# 查询所有记录
all_records = bitable.list_records(app_token, table_id)
print(f"[OK] 共 {len(all_records)} 条记录")
# 获取字段定义
fields = bitable.get_fields(app_token, table_id)
print(f"[OK] 共 {len(fields)} 个字段:")
for f in fields:
print(f" - {f['field_name']} (类型: {f['type']})")
return all_records
def main():
"""运行完整 Demo"""
print("\n" + "#"*60)
print("# 飞书多维表格 Demo - zhiyun.ai")
print("#"*60)
try:
# Step 1: 验证凭证
bitable = demo_verify_credentials()
# Step 2: 创建多维表格
app_token = demo_create_bitable(bitable)
# Step 3: 创建数据表
table_id = demo_create_task_table(bitable, app_token)
# Step 4: 添加示例数据
demo_add_sample_data(bitable, app_token, table_id)
# Step 5: 查询数据
demo_query_data(bitable, app_token, table_id)
# 完成
print("\n" + "="*60)
print("Demo 完成!")
print("="*60)
print(f"\n多维表格信息:")
print(f" app_token: {app_token}")
print(f" table_id: {table_id}")
print(f"\n访问地址:")
print(f" https://zhiyun-ai.feishu.cn/base/{app_token}?table={table_id}")
print()
return app_token, table_id
except Exception as e:
print(f"\n[ERROR] {e}")
raise
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,451 @@
#!/usr/bin/env python3
"""
飞书云文档操作工具类
包含文档创建、内容块管理、图片上传等功能
"""
import os
import requests
from datetime import datetime, timedelta
from typing import Dict, List, Optional
class FeishuDocx:
"""飞书云文档操作工具类(含图片上传)"""
BASE_URL = "https://open.feishu.cn/open-apis"
DEFAULT_APP_ID = "cli_a9f29dca82b9dbef"
DEFAULT_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
def __init__(self, app_id: str = None, app_secret: str = None):
self.app_id = app_id or os.environ.get("FEISHU_APP_ID") or self.DEFAULT_APP_ID
self.app_secret = app_secret or os.environ.get("FEISHU_APP_SECRET") or self.DEFAULT_APP_SECRET
self._token = None
self._token_expires = None
@property
def token(self) -> str:
"""获取或刷新 access token"""
if self._token and self._token_expires and datetime.now() < self._token_expires:
return self._token
url = f"{self.BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": self.app_id,
"app_secret": self.app_secret
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
self._token = data["tenant_access_token"]
self._token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return self._token
@property
def headers(self) -> dict:
"""获取请求头"""
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
# ==================== 文档管理 ====================
def set_document_permission(self, document_id: str, editable: bool = True) -> bool:
"""
设置文档权限
Args:
document_id: 文档ID
editable: True=组织内可编辑, False=组织内只读
"""
url = f"{self.BASE_URL}/drive/v1/permissions/{document_id}/public"
payload = {
"external_access_entity": "open",
"security_entity": "anyone_can_view",
"comment_entity": "anyone_can_view",
"share_entity": "anyone",
"link_share_entity": "tenant_editable" if editable else "tenant_readable",
"invite_external": False
}
response = requests.patch(url, headers=self.headers, params={"type": "docx"}, json=payload)
return response.json().get("code") == 0
def create_document(self, title: str, folder_token: str = None, editable: bool = True) -> dict:
"""
创建文档(自动设置为组织内可编辑)
Args:
title: 文档标题
folder_token: 文件夹token (可选)
editable: 是否设置为组织内可编辑 (默认True)
Returns:
dict: {"document_id": "xxx", "title": "xxx", "url": "xxx"}
"""
url = f"{self.BASE_URL}/docx/v1/documents"
payload = {"title": title}
if folder_token:
payload["folder_token"] = folder_token
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建文档失败: {data}")
doc = data["data"]["document"]
document_id = doc["document_id"]
if editable:
self.set_document_permission(document_id, editable=True)
return {
"document_id": document_id,
"title": doc["title"],
"url": f"https://feishu.cn/docx/{document_id}"
}
def get_document_blocks(self, document_id: str, page_size: int = 500) -> List[Dict]:
"""获取文档所有内容块"""
url = f"{self.BASE_URL}/docx/v1/documents/{document_id}/blocks"
all_blocks = []
page_token = None
while True:
params = {"page_size": page_size}
if page_token:
params["page_token"] = page_token
response = requests.get(url, headers=self.headers, params=params)
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取内容块失败: {data}")
all_blocks.extend(data["data"].get("items", []))
if not data["data"].get("has_more"):
break
page_token = data["data"]["page_token"]
return all_blocks
# ==================== 内容块操作 ====================
def create_blocks(self, document_id: str, blocks: list, parent_id: str = None) -> List[Dict]:
"""
创建内容块
Args:
document_id: 文档ID
blocks: 内容块列表
parent_id: 父块ID (默认为文档根块)
"""
parent_id = parent_id or document_id
url = f"{self.BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children"
response = requests.post(
url,
headers=self.headers,
params={"document_revision_id": -1},
json={"children": blocks}
)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建内容块失败: {data}")
return data["data"]["children"]
# ==================== 图片上传 (三步流程) ====================
def create_empty_image_block(self, document_id: str, parent_id: str = None) -> str:
"""
Step 1: 创建空图片块
Args:
document_id: 文档ID
parent_id: 父块ID (默认为文档根块)
Returns:
block_id: 图片块ID
"""
parent_id = parent_id or document_id
url = f"{self.BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children"
payload = {
"children": [{
"block_type": 27,
"image": {}
}]
}
response = requests.post(
url,
headers=self.headers,
params={"document_revision_id": -1},
json=payload
)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建图片块失败: {data}")
return data["data"]["children"][0]["block_id"]
def upload_image_to_block(self, file_path: str, block_id: str) -> str:
"""
Step 2: 上传图片文件
Args:
file_path: 本地图片路径
block_id: 图片块ID (从 Step 1 获取)
Returns:
file_token: 图片token
"""
url = f"{self.BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
# 根据文件扩展名确定 MIME 类型
ext = os.path.splitext(file_path)[1].lower()
mime_types = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.bmp': 'image/bmp',
}
mime_type = mime_types.get(ext, 'image/png')
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, mime_type)}
data = {
'file_name': file_name,
'parent_type': 'docx_image',
'parent_node': block_id,
'size': str(file_size)
}
response = requests.post(
url,
headers={"Authorization": f"Bearer {self.token}"},
files=files,
data=data
)
result = response.json()
if result.get("code") != 0:
raise Exception(f"上传图片失败: {result}")
return result["data"]["file_token"]
def bind_image_to_block(self, document_id: str, block_id: str, file_token: str) -> bool:
"""
Step 3: 绑定图片到图片块 (关键步骤!)
Args:
document_id: 文档ID
block_id: 图片块ID
file_token: 图片token (从 Step 2 获取)
Returns:
bool: 是否成功
"""
url = f"{self.BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
payload = {
"replace_image": {
"token": file_token
}
}
response = requests.patch(
url,
headers=self.headers,
params={"document_revision_id": -1},
json=payload
)
data = response.json()
if data.get("code") != 0:
raise Exception(f"绑定图片失败: {data}")
return True
def insert_image(self, document_id: str, file_path: str, parent_id: str = None) -> dict:
"""
插入图片到文档 (封装三步流程)
Args:
document_id: 文档ID
file_path: 本地图片路径
parent_id: 父块ID (默认为文档根块)
Returns:
dict: {"block_id": "xxx", "file_token": "xxx"}
"""
# Step 1: 创建空图片块
block_id = self.create_empty_image_block(document_id, parent_id)
# Step 2: 上传图片
file_token = self.upload_image_to_block(file_path, block_id)
# Step 3: 绑定图片
self.bind_image_to_block(document_id, block_id, file_token)
return {"block_id": block_id, "file_token": file_token}
def check_image_status(self, document_id: str) -> List[Dict]:
"""
检查文档中图片块的状态
Returns:
List[Dict]: 图片块状态列表
"""
blocks = self.get_document_blocks(document_id)
images = []
for block in blocks:
if block.get("block_type") == 27:
image_data = block.get("image", {})
images.append({
"block_id": block.get("block_id"),
"token": image_data.get("token", ""),
"width": image_data.get("width", 0),
"height": image_data.get("height", 0),
"is_valid": bool(image_data.get("token"))
})
return images
# ==================== 内容块构建器 ====================
@staticmethod
def heading1(text: str) -> dict:
"""一级标题"""
return {"block_type": 3, "heading1": {"elements": [{"text_run": {"content": text}}]}}
@staticmethod
def heading2(text: str) -> dict:
"""二级标题"""
return {"block_type": 4, "heading2": {"elements": [{"text_run": {"content": text}}]}}
@staticmethod
def heading3(text: str) -> dict:
"""三级标题"""
return {"block_type": 5, "heading3": {"elements": [{"text_run": {"content": text}}]}}
@staticmethod
def text(content: str) -> dict:
"""文本段落"""
return {"block_type": 2, "text": {"elements": [{"text_run": {"content": content}}]}}
@staticmethod
def bullet(content: str) -> dict:
"""无序列表项"""
return {"block_type": 12, "bullet": {"elements": [{"text_run": {"content": content}}]}}
@staticmethod
def ordered(content: str) -> dict:
"""有序列表项"""
return {"block_type": 13, "ordered": {"elements": [{"text_run": {"content": content}}]}}
@staticmethod
def code(content: str, language: int = 1) -> dict:
"""代码块 (language: 1=JSON, 4=Python, 等)"""
return {"block_type": 14, "code": {"elements": [{"text_run": {"content": content}}], "language": language}}
@staticmethod
def divider() -> dict:
"""分割线"""
return {"block_type": 22, "divider": {}}
@staticmethod
def todo(content: str, done: bool = False) -> dict:
"""待办事项"""
return {"block_type": 17, "todo": {"elements": [{"text_run": {"content": content}}], "style": {"done": done}}}
# ==================== 高级功能 ====================
def create_meeting_minutes(self, title: str, content: dict) -> dict:
"""
创建会议纪要
Args:
title: 文档标题
content: {
"summary": "会议摘要",
"points": [{"title": "...", "items": ["..."]}],
"todos": [{"assignee": "...", "task": "..."}]
}
Returns:
dict: 文档信息
"""
doc = self.create_document(title)
document_id = doc["document_id"]
blocks = []
if content.get("summary"):
blocks.append(self.heading2("会议摘要"))
blocks.append(self.text(content["summary"]))
blocks.append(self.divider())
if content.get("points"):
blocks.append(self.heading2("小结"))
for point in content["points"]:
blocks.append(self.heading3(point["title"]))
for item in point.get("items", []):
blocks.append(self.bullet(item))
blocks.append(self.divider())
if content.get("todos"):
blocks.append(self.heading2("待办事项"))
for todo_item in content["todos"]:
text = f"{todo_item['assignee']}: {todo_item['task']}"
blocks.append(self.todo(text))
self.create_blocks(document_id, blocks)
return doc
# ==================== 使用示例 ====================
if __name__ == "__main__":
docx = FeishuDocx()
# 示例1: 创建文档并插入图片
print("=== 示例1: 创建带图片的文档 ===")
doc = docx.create_document("图片上传测试文档")
print(f"文档创建成功: {doc['url']}")
# 如果有测试图片,取消注释以下代码
# result = docx.insert_image(doc["document_id"], "/path/to/image.png")
# print(f"图片插入成功: {result}")
# 示例2: 创建会议纪要
print("\n=== 示例2: 创建会议纪要 ===")
doc = docx.create_meeting_minutes(
"测试会议纪要",
{
"summary": "本次会议讨论了项目进度...",
"points": [{"title": "进度汇报", "items": ["任务A已完成", "任务B进行中"]}],
"todos": [{"assignee": "张三", "task": "完成文档编写"}]
}
)
print(f"会议纪要已创建: {doc['url']}")
# 示例3: 检查图片状态
print("\n=== 示例3: 检查图片状态 ===")
images = docx.check_image_status(doc["document_id"])
if images:
for img in images:
status = "✅ 有效" if img["is_valid"] else "❌ 空"
print(f"图片: {status}, 尺寸: {img['width']}x{img['height']}")
else:
print("文档中没有图片")

View File

@@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
WPS .dbt 文件迁移到飞书多维表格
"""
import pandas as pd
import requests
import json
import re
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
# ========== 配置 ==========
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
# 源文件
SOURCE_FILE = "/Users/donglinlai/Downloads/酷采团购系统优化进度表.dbt.xlsx"
class FeishuBitable:
"""飞书多维表格操作工具类"""
def __init__(self):
self._token = None
self._token_expires = None
@property
def token(self) -> str:
if self._token and self._token_expires and datetime.now() < self._token_expires:
return self._token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
self._token = data["tenant_access_token"]
self._token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return self._token
@property
def headers(self):
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def create_bitable(self, name: str) -> Dict:
"""创建多维表格"""
url = f"{BASE_URL}/bitable/v1/apps"
response = requests.post(url, headers=self.headers, json={"name": name})
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建多维表格失败: {data}")
return data["data"]["app"]
def create_table(self, app_token: str, name: str, fields: List[Dict]) -> Dict:
"""创建数据表"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables"
payload = {
"table": {
"name": name,
"default_view_name": "默认视图",
"fields": fields
}
}
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建数据表失败: {data}")
return data["data"]
def batch_create_records(self, app_token: str, table_id: str,
records: List[Dict], batch_size: int = 100) -> int:
"""批量创建记录"""
url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create"
total_created = 0
for i in range(0, len(records), batch_size):
batch = records[i:i+batch_size]
payload = {"records": [{"fields": r} for r in batch]}
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") != 0:
print(f" [WARN] 批次 {i//batch_size + 1} 部分失败: {data.get('msg', '')}")
# 尝试逐条插入
for record in batch:
try:
single_url = f"{BASE_URL}/bitable/v1/apps/{app_token}/tables/{table_id}/records"
single_resp = requests.post(single_url, headers=self.headers, json={"fields": record})
if single_resp.json().get("code") == 0:
total_created += 1
except:
pass
else:
total_created += len(data["data"]["records"])
return total_created
def analyze_column_type(series: pd.Series, col_name: str) -> Dict:
"""分析列的数据类型,返回飞书字段定义"""
col_lower = col_name.lower()
# 根据列名判断类型
if any(kw in col_lower for kw in ['日期', '时间', 'date', 'time', '提出时间', '发版日期', '更新时间']):
return {"field_name": col_name, "type": 5, "property": {"date_formatter": "yyyy/MM/dd"}}
if any(kw in col_lower for kw in ['图片', '附件', '截图', 'image', 'file', 'attachment']):
return {"field_name": col_name, "type": 1} # 作为文本处理
if any(kw in col_lower for kw in ['优先级', '状态', '类型', '分类', '终端', '严重程度']):
# 提取唯一值作为选项
unique_vals = series.dropna().astype(str).unique()
unique_vals = [v for v in unique_vals if v and v != 'nan' and len(v) < 50][:20]
if len(unique_vals) > 0 and len(unique_vals) <= 20:
return {
"field_name": col_name,
"type": 3, # 单选
"property": {
"options": [{"name": str(v)} for v in unique_vals]
}
}
if any(kw in col_lower for kw in ['进度', '百分比', '%']):
return {"field_name": col_name, "type": 2} # 数字
# 检查是否为数字列
try:
numeric_vals = pd.to_numeric(series.dropna(), errors='coerce')
if numeric_vals.notna().sum() / max(len(series.dropna()), 1) > 0.8:
return {"field_name": col_name, "type": 2} # 数字
except:
pass
# 默认为文本
return {"field_name": col_name, "type": 1}
def clean_value(val: Any, field_type: int) -> Any:
"""清理和转换值"""
if pd.isna(val) or val is None:
return None
if field_type == 5: # 日期
try:
if isinstance(val, (datetime, pd.Timestamp)):
return int(val.timestamp() * 1000)
elif isinstance(val, str):
dt = pd.to_datetime(val)
return int(dt.timestamp() * 1000)
except:
return None
if field_type == 2: # 数字
try:
return float(val)
except:
return None
if field_type == 3: # 单选
val_str = str(val).strip()
if val_str and val_str != 'nan':
return val_str
return None
# 文本类型
val_str = str(val).strip()
if val_str == 'nan' or not val_str:
return None
# 限制文本长度
if len(val_str) > 10000:
val_str = val_str[:10000] + "..."
return val_str
def migrate_sheet(bitable: FeishuBitable, app_token: str,
df: pd.DataFrame, sheet_name: str) -> str:
"""迁移单个 Sheet 到数据表"""
print(f"\n{'='*50}")
print(f"迁移 Sheet: 【{sheet_name}")
print(f"{'='*50}")
# 清理列名
df.columns = [str(c).strip() for c in df.columns]
# 去除完全空的行
df = df.dropna(how='all')
print(f" 数据: {len(df)} 行, {len(df.columns)}")
# 分析字段类型
fields = []
field_types = {}
for col in df.columns:
if not col or col.startswith('Unnamed'):
continue
field_def = analyze_column_type(df[col], col)
fields.append(field_def)
field_types[col] = field_def["type"]
print(f" 字段: {len(fields)}")
# 创建数据表
table_info = bitable.create_table(app_token, sheet_name, fields)
table_id = table_info["table_id"]
print(f" [OK] 数据表创建成功: {table_id}")
# 准备记录数据
records = []
for _, row in df.iterrows():
record = {}
for col in df.columns:
if not col or col.startswith('Unnamed'):
continue
val = clean_value(row[col], field_types.get(col, 1))
if val is not None:
record[col] = val
if record: # 只添加非空记录
records.append(record)
print(f" 准备导入 {len(records)} 条记录...")
# 批量创建记录
if records:
created = bitable.batch_create_records(app_token, table_id, records)
print(f" [OK] 成功导入 {created}/{len(records)} 条记录")
else:
print(f" [INFO] 无有效数据")
return table_id
def main():
print("\n" + "#" * 60)
print("# WPS 文件迁移到飞书多维表格")
print("#" * 60)
bitable = FeishuBitable()
# Step 1: 读取源文件
print("\n" + "=" * 50)
print("Step 1: 读取源文件")
print("=" * 50)
xlsx = pd.ExcelFile(SOURCE_FILE)
print(f" 文件: {SOURCE_FILE}")
print(f" Sheet 数量: {len(xlsx.sheet_names)}")
# Step 2: 创建多维表格
print("\n" + "=" * 50)
print("Step 2: 创建飞书多维表格")
print("=" * 50)
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
bitable_name = f"酷采团购系统优化进度表 (迁移 {timestamp})"
app_info = bitable.create_bitable(bitable_name)
app_token = app_info["app_token"]
print(f" [OK] 多维表格创建成功")
print(f" 名称: {bitable_name}")
print(f" app_token: {app_token}")
# Step 3: 迁移每个 Sheet
print("\n" + "=" * 50)
print("Step 3: 迁移数据表")
print("=" * 50)
table_ids = {}
for sheet_name in xlsx.sheet_names:
df = pd.read_excel(xlsx, sheet_name=sheet_name)
table_id = migrate_sheet(bitable, app_token, df, sheet_name)
table_ids[sheet_name] = table_id
# 完成
print("\n" + "=" * 60)
print("迁移完成!")
print("=" * 60)
print(f"\n多维表格信息:")
print(f" 名称: {bitable_name}")
print(f" app_token: {app_token}")
print(f"\n数据表:")
for name, tid in table_ids.items():
print(f" - {name}: {tid}")
print(f"\n访问地址:")
print(f" https://zhiyuncai.feishu.cn/base/{app_token}")
print()
return app_token, table_ids
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,449 @@
#!/usr/bin/env python3
"""
重建飞书文档ai-proj 项目可见性手册
清理现有内容后重新生成
"""
import requests
import os
import time
from datetime import datetime, timedelta
from PIL import Image, ImageDraw, ImageFont
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
# 目标文档
DOCUMENT_ID = "Eqt2dpcpToVDxExFmhicqFa9nJf"
_token = None
_token_expires = None
def get_token():
global _token, _token_expires
if _token and _token_expires and datetime.now() < _token_expires:
return _token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
_token = data["tenant_access_token"]
_token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return _token
def headers():
return {
"Authorization": f"Bearer {get_token()}",
"Content-Type": "application/json"
}
def get_document_blocks(document_id: str):
"""获取文档所有块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks"
response = requests.get(url, headers=headers())
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取块失败: {data}")
return data["data"].get("items", [])
def delete_block(document_id: str, block_id: str):
"""删除块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
response = requests.delete(url, headers=headers(), params={"document_revision_id": -1})
result = response.json()
return result.get("code") == 0
def clear_document(document_id: str):
"""清空文档内容(保留根块)"""
print("正在清空文档...")
blocks = get_document_blocks(document_id)
# 找出所有子块(排除根块本身)
child_blocks = [b for b in blocks if b.get("block_id") != document_id and b.get("parent_id") == document_id]
print(f" 发现 {len(child_blocks)} 个顶级子块需要删除")
# 从后向前删除,避免索引问题
for block in reversed(child_blocks):
block_id = block.get("block_id")
if delete_block(document_id, block_id):
print(f" 删除: {block_id[:20]}...")
else:
print(f" 删除失败: {block_id[:20]}...")
time.sleep(0.1) # 避免请求过快
print(" 文档清空完成")
def create_blocks(document_id: str, parent_id: str, blocks: list):
"""创建内容块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children"
payload = {"children": blocks}
response = requests.post(url, headers=headers(), params={"document_revision_id": -1}, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建块失败: {data}")
return data["data"].get("children", [])
def create_image_block(document_id: str, parent_id: str):
"""创建空图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children"
payload = {
"children": [{
"block_type": 27,
"image": {}
}]
}
response = requests.post(url, headers=headers(), params={"document_revision_id": -1}, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建图片块失败: {data}")
return data["data"]["children"][0]["block_id"]
def upload_image(file_path: str, block_id: str):
"""上传图片"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, 'image/png')}
data = {
'file_name': file_name,
'parent_type': 'docx_image',
'parent_node': block_id,
'size': str(file_size)
}
response = requests.post(
url,
headers={"Authorization": f"Bearer {get_token()}"},
files=files,
data=data
)
result = response.json()
if result.get("code") != 0:
raise Exception(f"上传失败: {result}")
return result["data"]["file_token"]
def bind_image(document_id: str, block_id: str, file_token: str):
"""绑定图片到图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
payload = {"replace_image": {"token": file_token}}
response = requests.patch(url, headers=headers(), params={"document_revision_id": -1}, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"绑定失败: {data}")
return True
def insert_image(document_id: str, file_path: str, description: str = ""):
"""插入图片的完整流程"""
print(f" 插入图片: {description}")
# Step 1: 创建空图片块
block_id = create_image_block(document_id, document_id)
print(f" block_id: {block_id[:20]}...")
# Step 2: 上传图片
file_token = upload_image(file_path, block_id)
print(f" file_token: {file_token[:20]}...")
# Step 3: 绑定图片
bind_image(document_id, block_id, file_token)
print(f" 绑定成功!")
return block_id
def generate_visibility_diagram():
"""生成可见性示意图"""
output_path = "/tmp/visibility_diagram.png"
img = Image.new('RGB', (800, 400), color='#ffffff')
draw = ImageDraw.Draw(img)
# 尝试加载字体,如果失败则使用默认
try:
font_large = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 24)
font_medium = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 18)
font_small = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 14)
except:
font_large = ImageFont.load_default()
font_medium = ImageFont.load_default()
font_small = ImageFont.load_default()
# 标题背景
draw.rectangle([0, 0, 800, 60], fill='#1890ff')
draw.text((400, 30), "ai-proj 项目可见性示意图", fill='white', anchor='mm', font=font_large)
# 私有项目区域
draw.rectangle([40, 80, 380, 370], fill='#fff7e6', outline='#fa8c16', width=3)
draw.text((210, 110), "🔒 私有项目", fill='#fa8c16', anchor='mm', font=font_medium)
draw.text((210, 140), "(private)", fill='#d48806', anchor='mm', font=font_small)
# 私有项目说明
draw.text((210, 180), "仅项目创建者可见", fill='#666666', anchor='mm', font=font_small)
# 私有项目图标
draw.ellipse([175, 210, 245, 280], fill='#fa8c16')
draw.text((210, 245), "👤", fill='white', anchor='mm', font=font_large)
draw.text((210, 310), "项目所有者", fill='#333333', anchor='mm', font=font_small)
draw.text((210, 335), "管理员", fill='#999999', anchor='mm', font=font_small)
# 企业项目区域
draw.rectangle([420, 80, 760, 370], fill='#e6f7ff', outline='#1890ff', width=3)
draw.text((590, 110), "👥 企业项目", fill='#1890ff', anchor='mm', font=font_medium)
draw.text((590, 140), "(enterprise)", fill='#096dd9', anchor='mm', font=font_small)
# 企业项目说明
draw.text((590, 180), "企业内所有成员可见", fill='#666666', anchor='mm', font=font_small)
# 企业项目图标组
positions = [(520, 230), (590, 230), (660, 230), (555, 290), (625, 290)]
for x, y in positions:
draw.ellipse([x-25, y-25, x+25, y+25], fill='#1890ff')
draw.text((x, y), "👤", fill='white', anchor='mm', font=font_medium)
draw.text((590, 340), "企业所有成员", fill='#333333', anchor='mm', font=font_small)
# 中间箭头
draw.text((400, 225), "", fill='#333333', anchor='mm', font=font_large)
img.save(output_path)
print(f"[OK] 生成可见性示意图: {output_path}")
return output_path
def generate_ui_screenshot():
"""生成 UI 示意图"""
output_path = "/tmp/visibility_ui.png"
img = Image.new('RGB', (700, 250), color='#fafafa')
draw = ImageDraw.Draw(img)
try:
font_medium = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 16)
font_small = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 12)
except:
font_medium = ImageFont.load_default()
font_small = ImageFont.load_default()
# 标题
draw.text((30, 25), "创建项目 - 可见性选择", fill='#333333', font=font_medium)
# 私有项目选项(选中状态)
draw.rectangle([30, 70, 320, 140], fill='#fff7e6', outline='#fa8c16', width=2)
draw.ellipse([45, 95, 65, 115], fill='#fa8c16')
draw.text((80, 90), "🔒 私有项目", fill='#fa8c16', font=font_medium)
draw.text((80, 115), "仅创建者可见", fill='#999999', font=font_small)
# 默认标签
draw.rectangle([250, 85, 310, 110], fill='#52c41a')
draw.text((280, 97), "默认", fill='white', anchor='mm', font=font_small)
# 企业项目选项(未选中状态)
draw.rectangle([350, 70, 640, 140], fill='#ffffff', outline='#d9d9d9', width=1)
draw.ellipse([365, 95, 385, 115], outline='#d9d9d9', width=2)
draw.text((400, 90), "👥 企业项目", fill='#666666', font=font_medium)
draw.text((400, 115), "企业内所有成员可见", fill='#999999', font=font_small)
# 底部说明
draw.rectangle([30, 170, 670, 220], fill='#f0f5ff', outline='#adc6ff', width=1)
draw.text((50, 185), "💡 提示:新创建的项目默认为私有项目。", fill='#1890ff', font=font_small)
draw.text((50, 205), "设置为企业项目后,项目需要先关联企业才能被企业成员访问。", fill='#666666', font=font_small)
img.save(output_path)
print(f"[OK] 生成 UI 示意图: {output_path}")
return output_path
# 文档内容块定义
def heading1(text):
return {
"block_type": 3,
"heading1": {"elements": [{"text_run": {"content": text}}]}
}
def heading2(text):
return {
"block_type": 4,
"heading2": {"elements": [{"text_run": {"content": text}}]}
}
def heading3(text):
return {
"block_type": 5,
"heading3": {"elements": [{"text_run": {"content": text}}]}
}
def text_block(content):
return {
"block_type": 2,
"text": {"elements": [{"text_run": {"content": content}}]}
}
def bullet(content):
return {
"block_type": 12,
"bullet": {"elements": [{"text_run": {"content": content}}]}
}
def ordered(content):
return {
"block_type": 13,
"ordered": {"elements": [{"text_run": {"content": content}}]}
}
def code_block(content, language=1):
return {
"block_type": 14,
"code": {
"elements": [{"text_run": {"content": content}}],
"language": language
}
}
def divider():
return {"block_type": 22, "divider": {}}
def main():
print("\n" + "#" * 60)
print("# 重建飞书文档ai-proj 项目可见性手册")
print("#" * 60)
# Step 1: 生成示意图
print("\n--- Step 1: 生成示意图 ---")
diagram_path = generate_visibility_diagram()
ui_path = generate_ui_screenshot()
# Step 2: 清空文档
print("\n--- Step 2: 清空文档 ---")
clear_document(DOCUMENT_ID)
time.sleep(1) # 等待清空完成
# Step 3: 创建手册内容
print("\n--- Step 3: 创建手册内容 ---")
# 第一部分:标题和概述
blocks_part1 = [
heading1("ai-proj 项目可见性手册"),
text_block("本手册介绍 ai-proj 系统中项目可见性功能的使用方法。"),
divider(),
heading2("1. 功能概述"),
text_block("项目可见性用于控制谁可以查看和访问您的项目。ai-proj 支持两种可见性级别:"),
bullet("私有项目 (private):仅项目创建者和管理员可见"),
bullet("企业项目 (enterprise):企业内所有成员可见"),
text_block("新创建的项目默认为「私有项目」,保护您的隐私。"),
divider(),
heading2("2. 可见性对比"),
text_block("下图展示了两种可见性的区别:"),
]
create_blocks(DOCUMENT_ID, DOCUMENT_ID, blocks_part1)
print(" 第一部分内容创建完成")
# 插入可见性示意图
print("\n--- Step 4: 插入可见性示意图 ---")
insert_image(DOCUMENT_ID, diagram_path, "可见性示意图")
# 第二部分:设置方法
print("\n--- Step 5: 添加设置说明 ---")
blocks_part2 = [
divider(),
heading2("3. 设置项目可见性"),
heading3("3.1 创建项目时设置"),
text_block("在创建项目时,您可以选择项目的可见性:"),
ordered("点击「创建项目」按钮"),
ordered("在弹出的表单中找到「项目可见性」选项"),
ordered("选择「私有项目」或「企业项目」"),
ordered("填写其他信息后点击「确定」"),
text_block("UI 界面示意:"),
]
create_blocks(DOCUMENT_ID, DOCUMENT_ID, blocks_part2)
print(" 设置说明创建完成")
# 插入 UI 示意图
print("\n--- Step 6: 插入 UI 示意图 ---")
insert_image(DOCUMENT_ID, ui_path, "UI 示意图")
# 第三部分:修改方法和规则
print("\n--- Step 7: 添加剩余内容 ---")
blocks_part3 = [
heading3("3.2 修改已有项目"),
ordered("在项目列表中找到目标项目"),
ordered("点击项目卡片上的「编辑」按钮"),
ordered("在编辑页面修改「项目可见性」"),
ordered("点击「保存」"),
divider(),
heading2("4. 可见性规则"),
bullet("私有项目:仅所有者和超级管理员可以查看"),
bullet("企业项目:需要项目已关联企业,企业内所有成员可以查看"),
bullet("只有项目所有者或管理员可以修改可见性"),
bullet("设置为「企业项目」需要项目已关联企业"),
divider(),
heading2("5. API 参考"),
text_block("修改项目可见性的 API"),
code_block('''PATCH /api/v1/projects/:id/visibility
请求体:
{
"visibility": "enterprise"
}
可选值: "private" | "enterprise"'''),
divider(),
heading2("6. 注意事项"),
bullet("新项目默认为私有,请根据需要调整可见性"),
bullet("将私有项目改为企业项目后,企业所有成员都能看到"),
bullet("将企业项目改为私有后,其他成员将无法访问"),
bullet("项目成员权限不受可见性影响,被添加为成员的用户始终可以访问"),
divider(),
text_block(f"最后更新:{datetime.now().strftime('%Y-%m-%d %H:%M')}"),
]
create_blocks(DOCUMENT_ID, DOCUMENT_ID, blocks_part3)
print("\n" + "=" * 60)
print("文档重建完成!")
print(f"\n文档地址: https://zhiyuncai.feishu.cn/docx/{DOCUMENT_ID}")
print("=" * 60)
# 验证图片
print("\n--- 验证图片状态 ---")
blocks = get_document_blocks(DOCUMENT_ID)
image_count = 0
for block in blocks:
if block.get("block_type") == 27:
image_count += 1
image_data = block.get("image", {})
token = image_data.get("token", "")
print(f"图片 #{image_count}: token={'有效' if token else ''} ({token[:20] if token else 'N/A'}...)")
if image_count == 0:
print("警告:文档中没有图片块")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,530 @@
#!/usr/bin/env python3
"""
ai-proj 与飞书项目同步脚本
功能:
- 初始化:创建飞书根目录、多维表格
- 全量同步:同步所有项目到飞书
- 增量同步:只同步新增项目
- 更新同步:更新现有项目的任务统计等信息
使用方法:
python aiproj_sync.py init # 首次初始化
python aiproj_sync.py sync # 增量同步(推荐日常使用)
python aiproj_sync.py sync-all # 全量同步
python aiproj_sync.py update # 更新统计信息
"""
import os
import sys
import json
import requests
from datetime import datetime
from typing import Optional, Dict, List, Any
# ============================================
# 配置
# ============================================
FEISHU_APP_ID = os.getenv("FEISHU_APP_ID", "cli_a9f29dca82b9dbef")
FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho")
# 默认协作者 open_id邱栋梁@智云采购)
# 应用创建的文件夹所有者是应用本身,需要显式添加用户为协作者
DEFAULT_COLLABORATOR_OPENID = "ou_43784ff7c819ac000095fb52a4c3d1c7"
AIPROJ_API_BASE = "https://ai.pipexerp.com/api/v1"
AIPROJ_TOKEN = os.getenv("AIPROJ_TOKEN", "aiproj_pk_b455c91607414c22a0f3d8f09785969f1aa2144f33f1336fbb12450ecebfdb64")
CONFIG_PATH = os.path.expanduser("~/.config/aiproj-feishu-sync.json")
# ============================================
# 飞书 API
# ============================================
class FeishuAPI:
def __init__(self):
self.token = None
def get_token(self) -> str:
"""获取 tenant_access_token"""
if self.token:
return self.token
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": FEISHU_APP_ID,
"app_secret": FEISHU_APP_SECRET
})
data = response.json()
if data.get("code") == 0:
self.token = data["tenant_access_token"]
return self.token
else:
raise Exception(f"获取飞书 token 失败: {data}")
def _headers(self, content_type: bool = False) -> Dict:
headers = {"Authorization": f"Bearer {self.get_token()}"}
if content_type:
headers["Content-Type"] = "application/json"
return headers
def get_root_folder(self) -> str:
"""获取云空间根文件夹"""
url = "https://open.feishu.cn/open-apis/drive/explorer/v2/root_folder/meta"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["token"]
raise Exception(f"获取根文件夹失败: {data}")
def create_folder(self, name: str, parent_token: str, add_collaborator: bool = True) -> str:
"""创建文件夹"""
url = "https://open.feishu.cn/open-apis/drive/v1/files/create_folder"
response = requests.post(url, headers=self._headers(True), json={
"name": name,
"folder_token": parent_token
})
data = response.json()
if data.get("code") == 0:
folder_token = data["data"]["token"]
# 自动添加协作者(应用创建的文件夹默认只有应用能访问)
if add_collaborator and DEFAULT_COLLABORATOR_OPENID:
self.add_collaborator(folder_token, "folder", DEFAULT_COLLABORATOR_OPENID)
return folder_token
raise Exception(f"创建文件夹失败: {data}")
def add_collaborator(self, file_token: str, file_type: str, user_open_id: str, perm: str = "full_access") -> bool:
"""添加协作者
应用通过 API 创建的文件/文件夹,所有者是应用本身。
组织内用户默认无法访问,需要显式添加为协作者。
Args:
file_token: 文件/文件夹 token
file_type: 类型 (folder, doc, sheet, bitable 等)
user_open_id: 用户 open_id
perm: 权限级别 (full_access, edit, view)
"""
url = f"https://open.feishu.cn/open-apis/drive/v1/permissions/{file_token}/members"
params = {"type": file_type, "need_notification": "false"}
payload = {
"member_type": "openid",
"member_id": user_open_id,
"perm": perm
}
response = requests.post(url, headers=self._headers(True), params=params, json=payload)
return response.json().get("code") == 0
def create_bitable(self, name: str, folder_token: str) -> Dict:
"""创建多维表格"""
url = "https://open.feishu.cn/open-apis/bitable/v1/apps"
response = requests.post(url, headers=self._headers(True), json={
"name": name,
"folder_token": folder_token
})
data = response.json()
if data.get("code") == 0:
return data["data"]["app"]
raise Exception(f"创建多维表格失败: {data}")
def get_bitable_tables(self, app_token: str) -> List[Dict]:
"""获取数据表列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["items"]
raise Exception(f"获取数据表失败: {data}")
def create_field(self, app_token: str, table_id: str, field: Dict) -> Optional[Dict]:
"""创建字段"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
response = requests.post(url, headers=self._headers(True), json=field)
data = response.json()
if data.get("code") == 0:
return data["data"]["field"]
print(f" 字段创建失败: {field.get('field_name')} - {data.get('msg')}")
return None
def update_field(self, app_token: str, table_id: str, field_id: str, updates: Dict) -> bool:
"""更新字段"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields/{field_id}"
response = requests.put(url, headers=self._headers(True), json=updates)
return response.json().get("code") == 0
def get_fields(self, app_token: str, table_id: str) -> List[Dict]:
"""获取字段列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
response = requests.get(url, headers=self._headers())
data = response.json()
if data.get("code") == 0:
return data["data"]["items"]
return []
def add_record(self, app_token: str, table_id: str, fields: Dict) -> Optional[Dict]:
"""添加记录"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
response = requests.post(url, headers=self._headers(True), json={"fields": fields})
data = response.json()
if data.get("code") == 0:
return data["data"]["record"]
print(f" 记录添加失败: {data.get('msg')}")
return None
def get_records(self, app_token: str, table_id: str, filter_str: str = None) -> List[Dict]:
"""获取记录列表"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
params = {"page_size": 500}
if filter_str:
params["filter"] = filter_str
response = requests.get(url, headers=self._headers(), params=params)
data = response.json()
if data.get("code") == 0:
return data["data"].get("items", [])
return []
def update_record(self, app_token: str, table_id: str, record_id: str, fields: Dict) -> bool:
"""更新记录"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}"
response = requests.put(url, headers=self._headers(True), json={"fields": fields})
return response.json().get("code") == 0
# ============================================
# ai-proj API
# ============================================
class AIProjAPI:
@staticmethod
def get_projects() -> List[Dict]:
"""获取项目列表"""
url = f"{AIPROJ_API_BASE}/projects?page_size=100"
headers = {"Authorization": f"Bearer {AIPROJ_TOKEN}"}
response = requests.get(url, headers=headers)
data = response.json()
if data.get("success"):
return data["data"]["data"]
raise Exception(f"获取项目列表失败: {data}")
# ============================================
# 配置管理
# ============================================
def load_config() -> Optional[Dict]:
"""加载配置"""
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH) as f:
return json.load(f)
return None
def save_config(config: Dict):
"""保存配置"""
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_PATH, "w") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# ============================================
# 同步逻辑
# ============================================
def init():
"""初始化:创建飞书目录结构"""
print("=" * 50)
print("飞书 ai-proj 项目同步 - 初始化")
print("=" * 50)
if load_config():
print("\n已存在配置文件,是否重新初始化?(y/N)")
if input().lower() != 'y':
print("取消初始化")
return
feishu = FeishuAPI()
# 1. 获取根目录
print("\n[1/5] 获取云空间根目录...")
space_root = feishu.get_root_folder()
print(f" 根目录: {space_root}")
# 2. 创建 ai-proj 文件夹
print("\n[2/5] 创建 ai-proj 根文件夹...")
root_folder = feishu.create_folder("ai-proj", space_root)
print(f" 创建成功: {root_folder}")
# 3. 创建多维表格
print("\n[3/5] 创建项目目录多维表格...")
bitable = feishu.create_bitable("项目目录", root_folder)
app_token = bitable["app_token"]
print(f" 创建成功: {app_token}")
tables = feishu.get_bitable_tables(app_token)
table_id = tables[0]["table_id"]
# 4. 创建字段
print("\n[4/5] 创建数据表字段...")
# 先修改第一列名称
fields = feishu.get_fields(app_token, table_id)
if fields:
feishu.update_field(app_token, table_id, fields[0]["field_id"], {
"field_name": "项目名称",
"type": fields[0]["type"]
})
print(" 修改字段: 项目名称")
# 创建其他字段
field_definitions = [
{"field_name": "项目ID", "type": 2},
{"field_name": "项目编号", "type": 1},
{"field_name": "飞书文件夹", "type": 15},
{"field_name": "状态", "type": 3, "property": {"options": [
{"name": "active", "color": 0},
{"name": "on_hold", "color": 1},
{"name": "planning", "color": 2},
{"name": "completed", "color": 3},
{"name": "archived", "color": 4}
]}},
{"field_name": "优先级", "type": 3, "property": {"options": [
{"name": "high", "color": 0},
{"name": "medium", "color": 1},
{"name": "low", "color": 2}
]}},
{"field_name": "所属企业", "type": 1},
{"field_name": "任务总数", "type": 2},
{"field_name": "描述", "type": 1},
{"field_name": "创建时间", "type": 5},
{"field_name": "最后同步", "type": 5},
]
for field in field_definitions:
if feishu.create_field(app_token, table_id, field):
print(f" 创建字段: {field['field_name']}")
# 5. 保存配置
print("\n[5/5] 保存配置...")
config = {
"root_folder_token": root_folder,
"bitable_app_token": app_token,
"bitable_table_id": table_id,
"created_at": datetime.now().isoformat(),
"synced_projects": {}
}
save_config(config)
print("\n" + "=" * 50)
print("初始化完成!")
print("=" * 50)
print(f"\n根文件夹: https://feishu.cn/drive/folder/{root_folder}")
print(f"多维表格: https://feishu.cn/base/{app_token}")
print(f"\n配置文件: {CONFIG_PATH}")
print("\n运行 'python aiproj_sync.py sync' 同步项目")
def sync(full: bool = False):
"""同步项目"""
config = load_config()
if not config:
print("未找到配置,请先运行 init")
return
print("=" * 50)
print(f"飞书 ai-proj 项目同步 - {'全量' if full else '增量'}同步")
print("=" * 50)
feishu = FeishuAPI()
app_token = config["bitable_app_token"]
table_id = config["bitable_table_id"]
root_folder = config["root_folder_token"]
synced = config.get("synced_projects", {})
# 获取 ai-proj 项目
print("\n获取 ai-proj 项目列表...")
projects = AIProjAPI.get_projects()
print(f"{len(projects)} 个项目")
# 筛选需要同步的项目
if full:
to_sync = projects
else:
to_sync = [p for p in projects if str(p["id"]) not in synced]
if not to_sync:
print("\n没有新项目需要同步")
return
print(f"\n需要同步 {len(to_sync)} 个项目...")
success = 0
for project in to_sync:
project_id = str(project["id"])
project_name = project["name"]
folder_name = f"{project_name}_{project_id}"
print(f"\n 处理: {project_name} (ID: {project_id})")
# 创建文件夹(如果不存在)
folder_token = synced.get(project_id, {}).get("folder_token")
if not folder_token:
try:
folder_token = feishu.create_folder(folder_name, root_folder)
print(f" 创建文件夹: {folder_token}")
except Exception as e:
print(f" 文件夹创建失败: {e}")
folder_token = ""
folder_url = f"https://feishu.cn/drive/folder/{folder_token}" if folder_token else ""
# 添加/更新记录
record_fields = {
"项目名称": project_name,
"项目ID": int(project_id),
"项目编号": project.get("project_number", ""),
"飞书文件夹": {"link": folder_url, "text": folder_name} if folder_url else None,
"状态": project.get("status", "active"),
"优先级": project.get("priority", "medium"),
"所属企业": project.get("company_name", ""),
"任务总数": project.get("task_count", 0),
"描述": (project.get("description", "") or "")[:500],
"创建时间": int(datetime.fromisoformat(
project["created_at"].replace("Z", "+00:00")
).timestamp() * 1000),
"最后同步": int(datetime.now().timestamp() * 1000)
}
record_id = synced.get(project_id, {}).get("record_id")
if record_id and not full:
# 更新现有记录
if feishu.update_record(app_token, table_id, record_id, record_fields):
print(f" 更新记录成功")
success += 1
else:
# 添加新记录
record = feishu.add_record(app_token, table_id, record_fields)
if record:
record_id = record["record_id"]
print(f" 添加记录成功")
success += 1
# 更新同步记录
synced[project_id] = {
"folder_token": folder_token,
"folder_url": folder_url,
"record_id": record_id,
"synced_at": datetime.now().isoformat()
}
# 保存配置
config["synced_projects"] = synced
config["last_sync"] = datetime.now().isoformat()
save_config(config)
print("\n" + "=" * 50)
print(f"同步完成: {success}/{len(to_sync)}")
print("=" * 50)
print(f"\n多维表格: https://feishu.cn/base/{app_token}")
def update_stats():
"""更新任务统计信息"""
config = load_config()
if not config:
print("未找到配置,请先运行 init")
return
print("=" * 50)
print("飞书 ai-proj 项目同步 - 更新统计")
print("=" * 50)
feishu = FeishuAPI()
app_token = config["bitable_app_token"]
table_id = config["bitable_table_id"]
synced = config.get("synced_projects", {})
# 获取最新项目数据
print("\n获取 ai-proj 项目列表...")
projects = AIProjAPI.get_projects()
project_map = {str(p["id"]): p for p in projects}
updated = 0
for project_id, sync_info in synced.items():
record_id = sync_info.get("record_id")
if not record_id:
continue
project = project_map.get(project_id)
if not project:
continue
# 只更新统计字段
update_fields = {
"任务总数": project.get("task_count", 0),
"状态": project.get("status", "active"),
"最后同步": int(datetime.now().timestamp() * 1000)
}
if feishu.update_record(app_token, table_id, record_id, update_fields):
print(f" 更新: {project['name']} - 任务数: {project.get('task_count', 0)}")
updated += 1
config["last_sync"] = datetime.now().isoformat()
save_config(config)
print(f"\n更新完成: {updated} 个项目")
def show_status():
"""显示同步状态"""
config = load_config()
if not config:
print("未初始化,请先运行: python aiproj_sync.py init")
return
print("=" * 50)
print("ai-proj 飞书同步状态")
print("=" * 50)
print(f"\n根文件夹: https://feishu.cn/drive/folder/{config['root_folder_token']}")
print(f"多维表格: https://feishu.cn/base/{config['bitable_app_token']}")
print(f"已同步项目: {len(config.get('synced_projects', {}))}")
print(f"上次同步: {config.get('last_sync', '从未')}")
print(f"配置文件: {CONFIG_PATH}")
# ============================================
# 主入口
# ============================================
def main():
if len(sys.argv) < 2:
print("用法: python aiproj_sync.py <command>")
print("\n命令:")
print(" init 首次初始化(创建目录和多维表格)")
print(" sync 增量同步(只同步新项目)")
print(" sync-all 全量同步(同步所有项目)")
print(" update 更新统计信息")
print(" status 查看同步状态")
return
command = sys.argv[1]
if command == "init":
init()
elif command == "sync":
sync(full=False)
elif command == "sync-all":
sync(full=True)
elif command == "update":
update_stats()
elif command == "status":
show_status()
else:
print(f"未知命令: {command}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,139 @@
---
name: feishu
description: 飞书文档与多维表格操作入口。当用户提到飞书、云文档、多维表格、Bitable 相关任务时自动激活。
---
# 飞书集成
## 功能模块
| 模块 | 技能 | 说明 |
|------|------|------|
| 云文档 | `feishu-docx` | 创建、编辑云文档,会议纪要 |
| 多维表格 | `feishu-bitable` | 记录增删改查,数据同步 |
| 任务 | 本技能 | 创建待办任务 |
## 环境配置
```bash
# ~/.zshrc凭证唯一配置位置
export FEISHU_APP_ID="cli_a9f29dca82b9dbef"
export FEISHU_APP_SECRET="<从飞书开放平台获取>"
```
**权限要求**
- 云文档:`docx:document`, `drive:drive`
- 多维表格:`bitable:app`
- 任务:`task:task:write`
## Access Token
```python
import os, requests
def get_tenant_access_token():
"""获取飞书 tenant_access_token"""
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": os.environ["FEISHU_APP_ID"],
"app_secret": os.environ["FEISHU_APP_SECRET"]
})
data = response.json()
if data.get("code") == 0:
return data["tenant_access_token"]
raise Exception(f"获取 token 失败: {data}")
```
## 默认存储位置
| 文件夹 | folder_token |
|--------|-------------|
| ai-proj 根目录 | `RTLKf247ClQQDyd5IjxcTOVQnxd` |
| 01运营 (默认) | `C80gfkRnzlonQ5d4AhOcOACDnNg` |
## URL 结构
```
云文档: https://xxx.feishu.cn/docx/DoxcXXXXXX
└── document_id
多维表格: https://xxx.feishu.cn/base/BascXXX?table=tblXXX&view=vewXXX
└── app_token └── table_id
```
## 飞书任务
```python
def create_task(summary: str, due_time: int = None):
"""创建飞书任务"""
url = "https://open.feishu.cn/open-apis/task/v2/tasks"
token = get_tenant_access_token()
payload = {"summary": summary}
if due_time:
payload["due"] = {"timestamp": str(due_time), "is_all_day": False}
response = requests.post(url,
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json=payload)
data = response.json()
if data.get("code") == 0:
return data["data"]["task"]
raise Exception(f"创建任务失败: {data}")
```
## 工具类
完整工具类见:
- `~/.claude/skills/feishu/feishu_bitable.py` - 多维表格
- `~/.claude/skills/feishu/feishu_docx.py` - 云文档
## Incoming Webhook群机器人通知卡片
**Webhook 地址**:存储在 `~/.config/devops/credentials.env``FEISHU_DEPLOY_WEBHOOK`
**⚠️ 关键注意事项**
-**禁用 schema 2.0**`"schema": "2.0"` 会返回 ErrCode 11246必须用 legacy 格式
-**legacy 卡片格式**(无 schema 字段)才能正常发送
-**按钮 URL 禁止指向列表页**:必须带具体资源 ID`/requirements/864`,不能是 `/requirements`
-**保存到文件再 curl**:包含中文的 JSON 直接用 `'...'` 传参会报 "blank argument" 错误
**正确的卡片发送示例**
```bash
cat > /tmp/feishu_card.json << 'EOF'
{
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": "通知标题"},
"template": "blue"
},
"elements": [
{
"tag": "div",
"text": {"tag": "lark_md", "content": "**内容**:描述文字"}
},
{
"tag": "action",
"actions": [{
"tag": "button",
"text": {"tag": "plain_text", "content": "查看详情"},
"type": "primary",
"url": "https://ai.pipexerp.com/requirements/864"
}]
}
]
}
}
EOF
curl -s -X POST \
"https://open.feishu.cn/open-apis/bot/v2/hook/xxx" \
-H "Content-Type: application/json" \
-d @/tmp/feishu_card.json
```
**header template 颜色**`blue`(待审批)/ `green`(通过)/ `red`(驳回)/ `wathet`(信息)
## 相关技能
- `feishu-docx` - 云文档详细操作
- `feishu-bitable` - 多维表格详细操作

View File

@@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""
测试飞书云文档图片上传
"""
import requests
import os
from datetime import datetime, timedelta
from PIL import Image, ImageDraw
# ========== 配置 ==========
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
class FeishuDocx:
"""飞书云文档操作工具类"""
def __init__(self):
self._token = None
self._token_expires = None
@property
def token(self) -> str:
if self._token and self._token_expires and datetime.now() < self._token_expires:
return self._token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
self._token = data["tenant_access_token"]
self._token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
print(f"[OK] Token 获取成功")
return self._token
@property
def headers(self):
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def set_document_permission(self, document_id: str, editable: bool = True) -> bool:
"""设置文档权限为组织内可编辑"""
url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public"
payload = {
"external_access_entity": "open",
"security_entity": "anyone_can_view",
"comment_entity": "anyone_can_view",
"share_entity": "anyone",
"link_share_entity": "tenant_editable" if editable else "tenant_readable",
"invite_external": False
}
response = requests.patch(url, headers=self.headers, params={"type": "docx"}, json=payload)
result = response.json()
if result.get("code") == 0:
print(f"[OK] 权限设置成功: 组织内可编辑")
return True
else:
print(f"[WARN] 权限设置失败: {result.get('msg')}")
return False
def create_document(self, title: str, folder_token: str = None, editable: bool = True) -> dict:
"""创建云文档(自动设置为组织内可编辑)"""
url = f"{BASE_URL}/docx/v1/documents"
payload = {"title": title}
if folder_token:
payload["folder_token"] = folder_token
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建文档失败: {data}")
doc = data["data"]["document"]
print(f"[OK] 文档创建成功: {doc['document_id']}")
# 自动设置权限
if editable:
self.set_document_permission(doc['document_id'], editable=True)
return doc
def create_empty_image_block(self, document_id: str, index: int = -1) -> str:
"""
创建空的图片块,返回 block_id
正确流程第一步:先创建空图片块
"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{document_id}/children"
# 创建空的图片块
image_block = {
"block_type": 27,
"image": {} # 空的图片块
}
payload = {"children": [image_block]}
if index >= 0:
payload["index"] = index
response = requests.post(url, headers=self.headers, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建图片块失败: {data}")
# 获取创建的图片块 ID
children = data["data"].get("children", [])
if not children:
raise Exception("创建图片块失败: 没有返回 children")
block_id = children[0].get("block_id")
print(f"[OK] 空图片块创建成功, block_id: {block_id}")
return block_id
def upload_image_to_block(self, file_path: str, block_id: str) -> str:
"""
上传图片到指定的图片块
正确流程第二步:将图片上传并绑定到已创建的图片块
"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
headers = {"Authorization": f"Bearer {self.token}"}
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, 'image/png')}
data = {
'file_name': file_name,
'parent_type': 'docx_image', # 云文档图片
'parent_node': block_id, # 关键: 使用图片块的 block_id
'size': str(file_size)
}
print(f"[INFO] 上传图片到 block_id={block_id}")
response = requests.post(url, headers=headers, files=files, data=data)
result = response.json()
print(f"[DEBUG] 上传响应: code={result.get('code')}, msg={result.get('msg')}")
if result.get("code") != 0:
raise Exception(f"上传图片失败: {result}")
file_token = result["data"]["file_token"]
print(f"[OK] 图片上传成功, file_token: {file_token}")
return file_token
def _upload_media(self, file_path: str, parent_type: str, parent_node: str = '') -> str:
"""使用 media API 上传"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
headers = {"Authorization": f"Bearer {self.token}"}
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, 'image/png')}
data = {
'file_name': file_name,
'parent_type': parent_type,
'parent_node': parent_node,
'size': str(file_size)
}
response = requests.post(url, headers=headers, files=files, data=data)
result = response.json()
print(f"[DEBUG] media 响应 ({parent_type}): code={result.get('code')}, msg={result.get('msg')}")
if result.get("code") == 0:
return result["data"]["file_token"]
return None
def _upload_to_drive(self, file_path: str) -> str:
"""上传到云空间根目录,然后获取 file_token"""
# 先获取根文件夹 token
url = f"{BASE_URL}/drive/explorer/v2/root_folder/meta"
headers = {"Authorization": f"Bearer {self.token}"}
response = requests.get(url, headers=headers)
result = response.json()
print(f"[DEBUG] 根目录响应: {result}")
if result.get("code") != 0:
return None
root_token = result["data"]["token"]
# 上传文件
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
upload_url = f"{BASE_URL}/drive/v1/medias/upload_all"
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, 'image/png')}
data = {
'file_name': file_name,
'parent_type': 'explorer',
'parent_node': root_token,
'size': str(file_size)
}
response = requests.post(upload_url, headers=headers, files=files, data=data)
result = response.json()
print(f"[DEBUG] drive 上传响应: code={result.get('code')}, msg={result.get('msg')}")
if result.get("code") == 0:
return result["data"]["file_token"]
return None
def _upload_im_image(self, file_path: str) -> str:
"""使用消息图片 API 上传"""
url = f"{BASE_URL}/im/v1/images"
headers = {"Authorization": f"Bearer {self.token}"}
with open(file_path, 'rb') as f:
files = {'image': f}
data = {'image_type': 'message'}
response = requests.post(url, headers=headers, files=files, data=data)
result = response.json()
print(f"[DEBUG] im 图片响应: code={result.get('code')}, msg={result.get('msg')}")
if result.get("code") == 0:
return result["data"]["image_key"]
return None
def _try_upload_with_type(self, file_path: str, parent_type: str) -> str:
"""尝试不同的 parent_type 上传"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
headers = {"Authorization": f"Bearer {self.token}"}
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, 'image/png')}
data = {
'file_name': file_name,
'parent_type': parent_type,
'parent_node': '',
'size': str(file_size)
}
print(f"[INFO] 尝试 parent_type={parent_type}")
response = requests.post(url, headers=headers, files=files, data=data)
result = response.json()
print(f"[DEBUG] 响应: {result}")
if result.get("code") != 0:
raise Exception(f"上传失败 ({parent_type}): {result}")
return result["data"]["file_token"]
def bind_image_to_block(self, document_id: str, block_id: str, file_token: str) -> dict:
"""
绑定图片到图片块 (关键的第三步!)
使用 PATCH 请求和 replace_image 字段将图片绑定到已创建的图片块
"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
params = {"document_revision_id": -1}
payload = {
"replace_image": {
"token": file_token
}
}
print(f"[INFO] 绑定图片到块: block_id={block_id}")
response = requests.patch(url, headers=self.headers, params=params, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"绑定图片失败: {data}")
print(f"[OK] 图片绑定成功")
return data["data"]
def generate_test_image(output_path: str):
"""生成测试图片"""
img = Image.new('RGB', (400, 300), color='#4a90d9')
draw = ImageDraw.Draw(img)
draw.rectangle([20, 20, 380, 280], fill='#f0f4f8', outline='#2563eb', width=2)
draw.text((200, 150), "Test Image", fill='#1e3a5f', anchor='mm')
draw.text((200, 200), datetime.now().strftime("%Y-%m-%d %H:%M:%S"), fill='#6b7280', anchor='mm')
img.save(output_path)
print(f"[OK] 测试图片生成: {output_path}")
return output_path
def main():
print("\n" + "#" * 60)
print("# 飞书云文档图片上传测试")
print("#" * 60)
docx = FeishuDocx()
# Step 1: 生成测试图片
print("\n" + "=" * 50)
print("Step 1: 生成测试图片")
print("=" * 50)
test_image = "/tmp/feishu_docx_test_image.png"
generate_test_image(test_image)
# Step 2: 创建测试文档
print("\n" + "=" * 50)
print("Step 2: 创建测试文档")
print("=" * 50)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
doc = docx.create_document(f"图片上传测试 - {timestamp}")
document_id = doc["document_id"]
# Step 3: 创建空的图片块
print("\n" + "=" * 50)
print("Step 3: 创建空的图片块")
print("=" * 50)
try:
block_id = docx.create_empty_image_block(document_id)
except Exception as e:
print(f"[ERROR] 创建图片块失败: {e}")
return
# Step 4: 上传图片
print("\n" + "=" * 50)
print("Step 4: 上传图片")
print("=" * 50)
try:
file_token = docx.upload_image_to_block(test_image, block_id)
except Exception as e:
print(f"[ERROR] 上传图片失败: {e}")
return
# Step 5: 绑定图片到图片块 (关键步骤!)
print("\n" + "=" * 50)
print("Step 5: 绑定图片到图片块")
print("=" * 50)
try:
docx.bind_image_to_block(document_id, block_id, file_token)
except Exception as e:
print(f"[ERROR] 绑定图片失败: {e}")
return
# 完成
print("\n" + "=" * 60)
print("测试完成!")
print("=" * 60)
print(f"\n文档地址:")
print(f" https://feishu.cn/docx/{document_id}")
print()
def check_permissions(docx):
"""检查应用权限"""
# 获取应用信息
url = f"{BASE_URL}/application/v6/applications/underauditlist"
headers = {"Authorization": f"Bearer {docx.token}"}
response = requests.get(url, headers=headers)
print(f"[DEBUG] 权限检查响应: {response.json()}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,375 @@
#!/usr/bin/env python3
"""
更新飞书文档ai-proj 项目可见性手册
"""
import requests
import os
from datetime import datetime, timedelta
from PIL import Image, ImageDraw, ImageFont
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
# 目标文档
DOCUMENT_ID = "Eqt2dpcpToVDxExFmhicqFa9nJf"
_token = None
_token_expires = None
def get_token():
global _token, _token_expires
if _token and _token_expires and datetime.now() < _token_expires:
return _token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
_token = data["tenant_access_token"]
_token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return _token
def headers():
return {
"Authorization": f"Bearer {get_token()}",
"Content-Type": "application/json"
}
def get_document_blocks(document_id: str):
"""获取文档所有块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks"
response = requests.get(url, headers=headers())
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取块失败: {data}")
return data["data"].get("items", [])
def delete_block(document_id: str, block_id: str):
"""删除块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
response = requests.delete(url, headers=headers(), params={"document_revision_id": -1})
return response.json().get("code") == 0
def create_blocks(document_id: str, parent_id: str, blocks: list):
"""创建内容块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children"
payload = {"children": blocks}
response = requests.post(url, headers=headers(), json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建块失败: {data}")
return data["data"].get("children", [])
def create_image_block(document_id: str, parent_id: str):
"""创建空图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{parent_id}/children"
payload = {
"children": [{
"block_type": 27,
"image": {}
}]
}
response = requests.post(url, headers=headers(), json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建图片块失败: {data}")
return data["data"]["children"][0]["block_id"]
def upload_image(file_path: str, block_id: str):
"""上传图片"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, 'image/png')}
data = {
'file_name': file_name,
'parent_type': 'docx_image',
'parent_node': block_id,
'size': str(file_size)
}
response = requests.post(
url,
headers={"Authorization": f"Bearer {get_token()}"},
files=files,
data=data
)
result = response.json()
if result.get("code") != 0:
raise Exception(f"上传失败: {result}")
return result["data"]["file_token"]
def bind_image(document_id: str, block_id: str, file_token: str):
"""绑定图片到图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
payload = {"replace_image": {"token": file_token}}
response = requests.patch(url, headers=headers(), params={"document_revision_id": -1}, json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"绑定失败: {data}")
return True
def generate_visibility_diagram():
"""生成可见性示意图"""
output_path = "/tmp/visibility_diagram.png"
img = Image.new('RGB', (800, 400), color='#ffffff')
draw = ImageDraw.Draw(img)
# 标题
draw.rectangle([0, 0, 800, 50], fill='#1890ff')
draw.text((400, 25), "项目可见性示意图", fill='white', anchor='mm')
# 私有项目
draw.rectangle([50, 80, 370, 350], fill='#fff7e6', outline='#fa8c16', width=2)
draw.text((210, 100), "🔒 私有项目 (private)", fill='#fa8c16', anchor='mm')
draw.text((210, 140), "仅创建者可见", fill='#666666', anchor='mm')
# 私有项目内的人员图标
draw.ellipse([180, 180, 240, 240], fill='#fa8c16')
draw.text((210, 210), "👤", fill='white', anchor='mm')
draw.text((210, 270), "项目所有者", fill='#333333', anchor='mm')
draw.text((210, 300), "管理员", fill='#999999', anchor='mm')
# 企业项目
draw.rectangle([430, 80, 750, 350], fill='#e6f7ff', outline='#1890ff', width=2)
draw.text((590, 100), "👥 企业项目 (enterprise)", fill='#1890ff', anchor='mm')
draw.text((590, 140), "企业内所有成员可见", fill='#666666', anchor='mm')
# 企业项目内的人员图标组
positions = [(520, 190), (590, 190), (660, 190), (555, 250), (625, 250)]
for x, y in positions:
draw.ellipse([x-20, y-20, x+20, y+20], fill='#1890ff')
draw.text((x, y), "👤", fill='white', anchor='mm')
draw.text((590, 300), "企业所有成员", fill='#333333', anchor='mm')
draw.text((590, 330), "管理员", fill='#999999', anchor='mm')
# 箭头
draw.text((400, 200), "", fill='#333333', anchor='mm')
img.save(output_path)
print(f"[OK] 生成可见性示意图: {output_path}")
return output_path
def generate_ui_screenshot():
"""生成 UI 示意图"""
output_path = "/tmp/visibility_ui.png"
img = Image.new('RGB', (600, 200), color='#fafafa')
draw = ImageDraw.Draw(img)
# 标题
draw.text((20, 20), "项目可见性选择", fill='#333333')
# Radio Group 模拟
# 私有项目选项
draw.rectangle([20, 60, 280, 100], fill='#fff7e6', outline='#fa8c16', width=2)
draw.ellipse([30, 70, 50, 90], fill='#fa8c16')
draw.text((60, 80), "🔒 私有项目", fill='#fa8c16', anchor='lm')
# 企业项目选项
draw.rectangle([300, 60, 560, 100], fill='#ffffff', outline='#d9d9d9', width=1)
draw.ellipse([310, 70, 330, 90], outline='#d9d9d9', width=1)
draw.text((340, 80), "👥 企业项目", fill='#666666', anchor='lm')
# 说明文字
draw.text((20, 130), "私有项目仅创建者可见,企业项目对企业内所有成员可见", fill='#999999')
# 默认标签
draw.rectangle([200, 65, 270, 85], fill='#52c41a')
draw.text((235, 75), "默认", fill='white', anchor='mm')
img.save(output_path)
print(f"[OK] 生成 UI 示意图: {output_path}")
return output_path
# 文档内容块定义
def heading1(text):
return {
"block_type": 3,
"heading1": {"elements": [{"text_run": {"content": text}}]}
}
def heading2(text):
return {
"block_type": 4,
"heading2": {"elements": [{"text_run": {"content": text}}]}
}
def heading3(text):
return {
"block_type": 5,
"heading3": {"elements": [{"text_run": {"content": text}}]}
}
def text_block(content):
return {
"block_type": 2,
"text": {"elements": [{"text_run": {"content": content}}]}
}
def bullet(content):
return {
"block_type": 12,
"bullet": {"elements": [{"text_run": {"content": content}}]}
}
def ordered(content):
return {
"block_type": 13,
"ordered": {"elements": [{"text_run": {"content": content}}]}
}
def code_block(content, language="json"):
return {
"block_type": 14,
"code": {
"elements": [{"text_run": {"content": content}}],
"language": 1 if language == "json" else 0
}
}
def divider():
return {"block_type": 22, "divider": {}}
def main():
print("\n" + "#" * 60)
print("# 更新飞书文档ai-proj 项目可见性手册")
print("#" * 60)
# Step 1: 生成示意图
print("\n--- Step 1: 生成示意图 ---")
diagram_path = generate_visibility_diagram()
ui_path = generate_ui_screenshot()
# Step 2: 直接追加内容(不删除现有内容)
print("\n--- Step 2: 追加手册内容 ---")
print("\n--- Step 3: 创建手册内容 ---")
# 定义手册内容
manual_blocks = [
heading1("ai-proj 项目可见性手册"),
text_block("本手册介绍 ai-proj 系统中项目可见性功能的使用方法。"),
divider(),
heading2("1. 功能概述"),
text_block("项目可见性用于控制谁可以查看和访问您的项目。ai-proj 支持两种可见性级别:"),
bullet("私有项目 (private):仅项目创建者和管理员可见"),
bullet("企业项目 (enterprise):企业内所有成员可见"),
text_block("新创建的项目默认为「私有项目」,保护您的隐私。"),
divider(),
heading2("2. 可见性对比"),
]
# 创建第一批内容块
created = create_blocks(DOCUMENT_ID, DOCUMENT_ID, manual_blocks)
print(f" 创建了 {len(created)} 个内容块")
# Step 4: 插入可见性示意图
print("\n--- Step 4: 插入可见性示意图 ---")
img_block_id = create_image_block(DOCUMENT_ID, DOCUMENT_ID)
print(f" 图片块: {img_block_id}")
file_token = upload_image(diagram_path, img_block_id)
print(f" file_token: {file_token}")
bind_image(DOCUMENT_ID, img_block_id, file_token)
print(" 图片绑定成功!")
# Step 5: 继续添加内容
print("\n--- Step 5: 添加更多内容 ---")
more_blocks = [
divider(),
heading2("3. 设置项目可见性"),
heading3("3.1 创建项目时设置"),
ordered("点击「创建项目」按钮"),
ordered("在弹出的表单中找到「项目可见性」选项"),
ordered("选择「私有项目」或「企业项目」"),
ordered("填写其他信息后点击「确定」"),
]
create_blocks(DOCUMENT_ID, DOCUMENT_ID, more_blocks)
# Step 6: 插入 UI 示意图
print("\n--- Step 6: 插入 UI 示意图 ---")
ui_block_id = create_image_block(DOCUMENT_ID, DOCUMENT_ID)
ui_file_token = upload_image(ui_path, ui_block_id)
bind_image(DOCUMENT_ID, ui_block_id, ui_file_token)
print(" UI 示意图上传成功!")
# Step 7: 添加剩余内容
print("\n--- Step 7: 添加剩余内容 ---")
final_blocks = [
heading3("3.2 修改已有项目"),
ordered("在项目列表中找到目标项目"),
ordered("点击项目卡片上的「编辑」按钮"),
ordered("在编辑页面修改「项目可见性」"),
ordered("点击「保存」"),
divider(),
heading2("4. 可见性规则"),
bullet("私有项目:仅所有者和超级管理员可以查看"),
bullet("企业项目:需要项目已关联企业,企业内所有成员可以查看"),
bullet("只有项目所有者或管理员可以修改可见性"),
bullet("设置为「企业项目」需要项目已关联企业"),
divider(),
heading2("5. API 参考"),
text_block("修改项目可见性的 API"),
code_block('''PATCH /api/v1/projects/:id/visibility
请求体:
{
"visibility": "enterprise"
}
可选值: "private" | "enterprise"'''),
divider(),
heading2("6. 注意事项"),
bullet("新项目默认为私有,请根据需要调整可见性"),
bullet("将私有项目改为企业项目后,企业所有成员都能看到"),
bullet("将企业项目改为私有后,其他成员将无法访问"),
bullet("项目成员权限不受可见性影响,被添加为成员的用户始终可以访问"),
divider(),
text_block(f"最后更新:{datetime.now().strftime('%Y-%m-%d %H:%M')}"),
]
create_blocks(DOCUMENT_ID, DOCUMENT_ID, final_blocks)
print("\n" + "=" * 60)
print("文档更新完成!")
print(f"\n文档地址: https://zhiyuncai.feishu.cn/docx/{DOCUMENT_ID}")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
上传用户指定的图片到飞书云文档
"""
import requests
import os
from datetime import datetime, timedelta
ZHIYUN_APP_ID = "cli_a9f29dca82b9dbef"
ZHIYUN_APP_SECRET = "sDfhjG7QT1S4gfHiMVYSygmPQPN1R2Ho"
BASE_URL = "https://open.feishu.cn/open-apis"
# 用户指定的图片
IMAGE_PATH = "/Users/donglinlai/Downloads/u274.png"
_token = None
_token_expires = None
def get_token():
global _token, _token_expires
if _token and _token_expires and datetime.now() < _token_expires:
return _token
url = f"{BASE_URL}/auth/v3/tenant_access_token/internal"
response = requests.post(url, json={
"app_id": ZHIYUN_APP_ID,
"app_secret": ZHIYUN_APP_SECRET
})
data = response.json()
if data.get("code") != 0:
raise Exception(f"获取 token 失败: {data}")
_token = data["tenant_access_token"]
_token_expires = datetime.now() + timedelta(seconds=data.get("expire", 7200) - 60)
return _token
def headers():
return {
"Authorization": f"Bearer {get_token()}",
"Content-Type": "application/json"
}
def set_document_permission(document_id: str, editable: bool = True):
"""
设置文档权限
Args:
document_id: 文档ID
editable: True=组织内可编辑, False=组织内只读
"""
url = f"{BASE_URL}/drive/v1/permissions/{document_id}/public"
payload = {
"external_access_entity": "open",
"security_entity": "anyone_can_view",
"comment_entity": "anyone_can_view",
"share_entity": "anyone",
"link_share_entity": "tenant_editable" if editable else "tenant_readable",
"invite_external": False
}
response = requests.patch(url, headers=headers(), params={"type": "docx"}, json=payload)
data = response.json()
if data.get("code") == 0:
print(f" 权限设置成功: {'组织内可编辑' if editable else '组织内只读'}")
return True
else:
print(f" [WARN] 权限设置失败: {data.get('msg')}")
return False
def create_document(title: str, editable: bool = True):
"""创建文档并设置权限"""
url = f"{BASE_URL}/docx/v1/documents"
response = requests.post(url, headers=headers(), json={"title": title})
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建文档失败: {data}")
doc = data["data"]["document"]
document_id = doc["document_id"]
# 自动设置权限
if editable:
set_document_permission(document_id, editable=True)
return document_id
def create_image_block(document_id: str):
"""创建空图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{document_id}/children"
payload = {
"children": [{
"block_type": 27,
"image": {}
}]
}
response = requests.post(url, headers=headers(), json=payload)
data = response.json()
if data.get("code") != 0:
raise Exception(f"创建图片块失败: {data}")
return data["data"]["children"][0]["block_id"]
def upload_image(file_path: str, block_id: str):
"""上传图片"""
url = f"{BASE_URL}/drive/v1/medias/upload_all"
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
# 根据扩展名设置 MIME 类型
ext = file_name.lower().split('.')[-1]
mime_types = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'webp': 'image/webp'
}
mime_type = mime_types.get(ext, 'image/png')
with open(file_path, 'rb') as f:
files = {'file': (file_name, f, mime_type)}
data = {
'file_name': file_name,
'parent_type': 'docx_image',
'parent_node': block_id,
'size': str(file_size)
}
response = requests.post(
url,
headers={"Authorization": f"Bearer {get_token()}"},
files=files,
data=data
)
result = response.json()
if result.get("code") != 0:
raise Exception(f"上传失败: {result}")
return result["data"]["file_token"]
def bind_image(document_id: str, block_id: str, file_token: str):
"""绑定图片到图片块"""
url = f"{BASE_URL}/docx/v1/documents/{document_id}/blocks/{block_id}"
payload = {
"replace_image": {
"token": file_token
}
}
response = requests.patch(
url,
headers=headers(),
params={"document_revision_id": -1},
json=payload
)
data = response.json()
if data.get("code") != 0:
raise Exception(f"绑定失败: {data}")
return True
def main():
print(f"\n上传图片: {IMAGE_PATH}")
print("=" * 60)
# 检查文件
if not os.path.exists(IMAGE_PATH):
print(f"[ERROR] 文件不存在: {IMAGE_PATH}")
return
file_size = os.path.getsize(IMAGE_PATH)
print(f"文件大小: {file_size / 1024:.1f} KB")
# Step 1: 创建文档
print("\n[1/4] 创建飞书文档...")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
doc_id = create_document(f"火把图片 - {timestamp}")
print(f" 文档ID: {doc_id}")
# Step 2: 创建图片块
print("[2/4] 创建图片块...")
block_id = create_image_block(doc_id)
print(f" 块ID: {block_id}")
# Step 3: 上传图片
print("[3/4] 上传图片...")
file_token = upload_image(IMAGE_PATH, block_id)
print(f" file_token: {file_token}")
# Step 4: 绑定图片
print("[4/4] 绑定图片...")
bind_image(doc_id, block_id, file_token)
print(" 绑定成功!")
print("\n" + "=" * 60)
print("上传完成!")
print(f"\n文档地址: https://feishu.cn/docx/{doc_id}")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,10 @@
{
"吴薇儿": {
"email": "wuweier@zhiyuncai.com",
"open_id": "ou_1d5cdfee78cbe6f8acc0751fff00ed09"
},
"宋佳香": {
"email": "songjiaxiang@zhiyuncai.com",
"open_id": "e6e72eb8"
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "finance-plugin",
"description": "Plugin for finance",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,948 @@
---
name: finance
description: 财务对账技能。支持银行对账、应收对账、应付对账、内部往来对账等。通过自然语言实现账单导入、差异分析、对账报告生成。当用户提到对账、银行流水、应收应付、账单核对、财务核销相关任务时自动激活。
---
# 财务对账技能
## 功能概述
- **银行对账**: 银行流水 vs 系统收付款记录匹配
- **应收对账**: 客户对账单生成与核对
- **应付对账**: 供应商对账单核对
- **内部对账**: 公司间/部门间往来核对
- **差异分析**: 自动识别未达账项、差异原因
- **对账报告**: 生成标准化对账报告
- **扫描单据OCR**: PDF/图片扫描件识别,提取订单号与对账单匹配(适用于诉讼证据核对)
---
## 对账类型
### 1. 银行对账
| 对账项 | 说明 |
|--------|------|
| 银行已收/企业未收 | 银行已入账,系统未记录 |
| 企业已收/银行未收 | 系统已记录,银行未入账 |
| 银行已付/企业未付 | 银行已扣款,系统未记录 |
| 企业已付/银行未付 | 系统已记录,银行未扣款 |
### 2. 应收对账
- 客户欠款余额核对
- 发票与回款匹配
- 账龄分析
### 3. 应付对账
- 供应商应付余额核对
- 采购入库与付款匹配
- 预付款冲销
---
## 数据格式要求
### 银行流水格式
```csv
交易日期,交易类型,摘要,金额,余额,对方账户,对方名称,交易流水号
2026-01-15,收入,货款,50000.00,150000.00,622848******1234,深圳XX公司,TXN20260115001
2026-01-16,支出,采购款,-30000.00,120000.00,622848******5678,广州YY公司,TXN20260116001
```
### 系统收付款格式
```csv
单据日期,单据类型,单据编号,金额,客户/供应商,备注,关联流水号
2026-01-15,收款单,SK20260115001,50000.00,深圳XX公司,货款,TXN20260115001
2026-01-16,付款单,FK20260116001,30000.00,广州YY公司,采购款,
```
---
## 自然语言操作示例
### 银行对账
| 用户说 | 执行操作 |
|--------|----------|
| "导入银行流水" | 解析银行导出的 Excel/CSV |
| "和系统记录对账" | 自动匹配流水号/金额/日期 |
| "找出未达账项" | 识别双方差异记录 |
| "生成银行余额调节表" | 输出标准调节表 |
### 应收对账
| 用户说 | 执行操作 |
|--------|----------|
| "生成客户对账单" | 按客户汇总应收明细 |
| "核对XX公司的应收款" | 单客户对账 |
| "应收账龄分析" | 按账龄分段统计 |
| "哪些发票还没收到款" | 查询未核销发票 |
### 应付对账
| 用户说 | 执行操作 |
|--------|----------|
| "核对供应商对账单" | 导入并匹配供应商账单 |
| "本月应付汇总" | 按供应商统计应付 |
| "哪些采购单还没付款" | 查询未付款采购单 |
### 扫描单据OCR对账
| 用户说 | 执行操作 |
|--------|----------|
| "识别扫描件与对账单匹配" | OCR 提取 PDF 订单号并与 Excel 对账 |
| "承运商起诉了,核对证据" | 诉讼证据对账分析 |
| "PDF发票识别" | OCR 提取发票信息 |
| "找出对账单没有的订单" | 差异订单识别 |
---
## Python 代码模板
### 银行对账核心逻辑
```python
import pandas as pd
from datetime import datetime
def load_bank_statement(file_path: str) -> pd.DataFrame:
"""加载银行流水"""
df = pd.read_excel(file_path) if file_path.endswith('.xlsx') else pd.read_csv(file_path)
# 标准化列名
column_mapping = {
'交易日期': 'date',
'金额': 'amount',
'交易流水号': 'ref_no',
'对方名称': 'counterparty',
'摘要': 'memo'
}
df = df.rename(columns=column_mapping)
df['date'] = pd.to_datetime(df['date'])
df['source'] = 'bank'
return df
def load_system_records(file_path: str) -> pd.DataFrame:
"""加载系统收付款记录"""
df = pd.read_excel(file_path) if file_path.endswith('.xlsx') else pd.read_csv(file_path)
column_mapping = {
'单据日期': 'date',
'金额': 'amount',
'关联流水号': 'ref_no',
'客户/供应商': 'counterparty',
'单据编号': 'doc_no'
}
df = df.rename(columns=column_mapping)
df['date'] = pd.to_datetime(df['date'])
df['source'] = 'system'
return df
def reconcile(bank_df: pd.DataFrame, system_df: pd.DataFrame) -> dict:
"""
执行对账
Returns:
{
'matched': 已匹配记录,
'bank_only': 银行有/系统无 (银行未达),
'system_only': 系统有/银行无 (企业未达),
'summary': 对账汇总
}
"""
# 方法1: 按流水号精确匹配
bank_with_ref = bank_df[bank_df['ref_no'].notna() & (bank_df['ref_no'] != '')]
system_with_ref = system_df[system_df['ref_no'].notna() & (system_df['ref_no'] != '')]
matched_by_ref = pd.merge(
bank_with_ref,
system_with_ref,
on='ref_no',
suffixes=('_bank', '_system'),
how='inner'
)
matched_refs = set(matched_by_ref['ref_no'])
# 方法2: 无流水号的按金额+日期模糊匹配
bank_unmatched = bank_df[~bank_df['ref_no'].isin(matched_refs)]
system_unmatched = system_df[~system_df['ref_no'].isin(matched_refs)]
# 按金额和日期(±3天)匹配
fuzzy_matched = []
bank_remaining = bank_unmatched.copy()
system_remaining = system_unmatched.copy()
for idx, bank_row in bank_unmatched.iterrows():
for sys_idx, sys_row in system_remaining.iterrows():
if (abs(bank_row['amount'] - sys_row['amount']) < 0.01 and
abs((bank_row['date'] - sys_row['date']).days) <= 3):
fuzzy_matched.append({
'bank_idx': idx,
'system_idx': sys_idx,
'amount': bank_row['amount'],
'bank_date': bank_row['date'],
'system_date': sys_row['date']
})
system_remaining = system_remaining.drop(sys_idx)
bank_remaining = bank_remaining.drop(idx)
break
# 未匹配项
bank_only = bank_remaining # 银行有/系统无
system_only = system_remaining # 系统有/银行无
# 汇总
summary = {
'bank_total': bank_df['amount'].sum(),
'system_total': system_df['amount'].sum(),
'matched_count': len(matched_by_ref) + len(fuzzy_matched),
'bank_only_count': len(bank_only),
'bank_only_amount': bank_only['amount'].sum() if len(bank_only) > 0 else 0,
'system_only_count': len(system_only),
'system_only_amount': system_only['amount'].sum() if len(system_only) > 0 else 0,
'difference': bank_df['amount'].sum() - system_df['amount'].sum()
}
return {
'matched': pd.concat([matched_by_ref, pd.DataFrame(fuzzy_matched)]),
'bank_only': bank_only,
'system_only': system_only,
'summary': summary
}
```
### 银行余额调节表
```python
def generate_bank_reconciliation_report(
bank_balance: float,
book_balance: float,
bank_only: pd.DataFrame,
system_only: pd.DataFrame,
as_of_date: str
) -> str:
"""生成银行余额调节表"""
# 分类未达账项
bank_receipts_not_in_book = bank_only[bank_only['amount'] > 0]['amount'].sum()
bank_payments_not_in_book = bank_only[bank_only['amount'] < 0]['amount'].sum()
book_receipts_not_in_bank = system_only[system_only['amount'] > 0]['amount'].sum()
book_payments_not_in_bank = system_only[system_only['amount'] < 0]['amount'].sum()
report = f"""
================================================================================
银行存款余额调节表
截止日期: {as_of_date}
================================================================================
一、银行对账单余额 {bank_balance:>15,.2f}
加: 企业已收/银行未收 {book_receipts_not_in_bank:>15,.2f}
减: 企业已付/银行未付 {abs(book_payments_not_in_bank):>15,.2f}
─────────────────
调节后余额 {bank_balance + book_receipts_not_in_bank + book_payments_not_in_bank:>15,.2f}
--------------------------------------------------------------------------------
二、企业账面余额 {book_balance:>15,.2f}
加: 银行已收/企业未收 {bank_receipts_not_in_book:>15,.2f}
减: 银行已付/企业未付 {abs(bank_payments_not_in_book):>15,.2f}
─────────────────
调节后余额 {book_balance + bank_receipts_not_in_book + bank_payments_not_in_book:>15,.2f}
================================================================================
未达账项明细
================================================================================
【银行已收/企业未收】
"""
if len(bank_only[bank_only['amount'] > 0]) > 0:
for _, row in bank_only[bank_only['amount'] > 0].iterrows():
report += f" {row['date'].strftime('%Y-%m-%d')} {row.get('counterparty', ''):<20} {row['amount']:>12,.2f}\n"
else:
report += " (无)\n"
report += "\n【银行已付/企业未付】\n"
if len(bank_only[bank_only['amount'] < 0]) > 0:
for _, row in bank_only[bank_only['amount'] < 0].iterrows():
report += f" {row['date'].strftime('%Y-%m-%d')} {row.get('counterparty', ''):<20} {row['amount']:>12,.2f}\n"
else:
report += " (无)\n"
report += "\n【企业已收/银行未收】\n"
if len(system_only[system_only['amount'] > 0]) > 0:
for _, row in system_only[system_only['amount'] > 0].iterrows():
report += f" {row['date'].strftime('%Y-%m-%d')} {row.get('counterparty', ''):<20} {row['amount']:>12,.2f}\n"
else:
report += " (无)\n"
report += "\n【企业已付/银行未付】\n"
if len(system_only[system_only['amount'] < 0]) > 0:
for _, row in system_only[system_only['amount'] < 0].iterrows():
report += f" {row['date'].strftime('%Y-%m-%d')} {row.get('counterparty', ''):<20} {row['amount']:>12,.2f}\n"
else:
report += " (无)\n"
return report
```
### 应收对账
```python
def generate_ar_statement(
customer_name: str,
transactions: pd.DataFrame,
as_of_date: str
) -> str:
"""
生成客户对账单
transactions 格式:
- date: 日期
- doc_type: 单据类型 (发票/收款/退货)
- doc_no: 单据编号
- debit: 借方(应收增加)
- credit: 贷方(应收减少)
- memo: 摘要
"""
df = transactions.sort_values('date')
report = f"""
================================================================================
客户对账单
================================================================================
客户名称: {customer_name}
对账期间: {df['date'].min().strftime('%Y-%m-%d')}{as_of_date}
================================================================================
日期 单据类型 单据编号 借方 贷方 余额
--------------------------------------------------------------------------------
"""
balance = 0
for _, row in df.iterrows():
debit = row.get('debit', 0) or 0
credit = row.get('credit', 0) or 0
balance += debit - credit
report += f"{row['date'].strftime('%Y-%m-%d')} {row['doc_type']:<8} {row['doc_no']:<16} {debit:>12,.2f} {credit:>12,.2f} {balance:>12,.2f}\n"
report += f"""
--------------------------------------------------------------------------------
合计 {df['debit'].sum():>12,.2f} {df['credit'].sum():>12,.2f} {balance:>12,.2f}
================================================================================
期末应收余额: {balance:,.2f}
请贵司核对以上账目,如有异议请于收到对账单后 7 日内书面告知。
如无异议,视为确认。
对账联系人: _______________
联系电话: _______________
"""
return report
```
### 账龄分析
```python
from datetime import datetime, timedelta
def aging_analysis(
receivables: pd.DataFrame,
as_of_date: str,
aging_buckets: list = [30, 60, 90, 180, 365]
) -> pd.DataFrame:
"""
应收账龄分析
receivables 格式:
- customer: 客户
- invoice_date: 发票日期
- amount: 未收金额
"""
as_of = datetime.strptime(as_of_date, '%Y-%m-%d')
def get_aging_bucket(invoice_date):
days = (as_of - invoice_date).days
for i, bucket in enumerate(aging_buckets):
if days <= bucket:
if i == 0:
return f'0-{bucket}'
else:
return f'{aging_buckets[i-1]+1}-{bucket}'
return f'{aging_buckets[-1]+1}天以上'
df = receivables.copy()
df['invoice_date'] = pd.to_datetime(df['invoice_date'])
df['aging_days'] = (as_of - df['invoice_date']).dt.days
df['aging_bucket'] = df['invoice_date'].apply(get_aging_bucket)
# 按客户和账龄分组汇总
summary = df.pivot_table(
index='customer',
columns='aging_bucket',
values='amount',
aggfunc='sum',
fill_value=0
)
# 添加合计列
summary['合计'] = summary.sum(axis=1)
# 添加合计行
summary.loc['合计'] = summary.sum()
return summary
```
---
## 对账报告模板
### Excel 对账报告
```python
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
def export_reconciliation_to_excel(
result: dict,
output_path: str,
report_date: str
):
"""导出对账结果到 Excel"""
wb = Workbook()
# Sheet1: 对账汇总
ws_summary = wb.active
ws_summary.title = "对账汇总"
summary = result['summary']
ws_summary['A1'] = '对账汇总报告'
ws_summary['A1'].font = Font(bold=True, size=14)
ws_summary['A2'] = f'对账日期: {report_date}'
headers = ['项目', '金额', '笔数']
for col, header in enumerate(headers, 1):
ws_summary.cell(row=4, column=col, value=header)
data = [
('银行流水合计', summary['bank_total'], '-'),
('系统记录合计', summary['system_total'], '-'),
('已匹配', '-', summary['matched_count']),
('银行有/系统无', summary['bank_only_amount'], summary['bank_only_count']),
('系统有/银行无', summary['system_only_amount'], summary['system_only_count']),
('差异金额', summary['difference'], '-'),
]
for row, (item, amount, count) in enumerate(data, 5):
ws_summary.cell(row=row, column=1, value=item)
ws_summary.cell(row=row, column=2, value=amount)
ws_summary.cell(row=row, column=3, value=count)
# Sheet2: 银行未达账项
ws_bank = wb.create_sheet("银行未达账项")
if len(result['bank_only']) > 0:
for col, header in enumerate(result['bank_only'].columns, 1):
ws_bank.cell(row=1, column=col, value=header)
for row, data_row in enumerate(result['bank_only'].values, 2):
for col, value in enumerate(data_row, 1):
ws_bank.cell(row=row, column=col, value=value)
# Sheet3: 企业未达账项
ws_system = wb.create_sheet("企业未达账项")
if len(result['system_only']) > 0:
for col, header in enumerate(result['system_only'].columns, 1):
ws_system.cell(row=1, column=col, value=header)
for row, data_row in enumerate(result['system_only'].values, 2):
for col, value in enumerate(data_row, 1):
ws_system.cell(row=row, column=col, value=value)
wb.save(output_path)
print(f"对账报告已保存: {output_path}")
```
---
## 常见对账场景
### 场景1: 月末银行对账
```python
# 1. 加载数据
bank_df = load_bank_statement('银行流水_202601.xlsx')
system_df = load_system_records('收付款记录_202601.xlsx')
# 2. 执行对账
result = reconcile(bank_df, system_df)
# 3. 查看汇总
print(f"已匹配: {result['summary']['matched_count']}")
print(f"银行未达: {result['summary']['bank_only_count']} 笔, 金额 {result['summary']['bank_only_amount']:,.2f}")
print(f"企业未达: {result['summary']['system_only_count']} 笔, 金额 {result['summary']['system_only_amount']:,.2f}")
# 4. 生成调节表
report = generate_bank_reconciliation_report(
bank_balance=150000.00, # 银行对账单余额
book_balance=145000.00, # 企业账面余额
bank_only=result['bank_only'],
system_only=result['system_only'],
as_of_date='2026-01-31'
)
print(report)
# 5. 导出 Excel
export_reconciliation_to_excel(result, '银行对账_202601.xlsx', '2026-01-31')
```
### 场景2: 客户对账
```python
# 加载客户交易明细
ar_transactions = pd.read_excel('应收明细.xlsx')
# 筛选特定客户
customer_data = ar_transactions[ar_transactions['customer'] == '深圳XX公司']
# 生成对账单
statement = generate_ar_statement(
customer_name='深圳XX公司',
transactions=customer_data,
as_of_date='2026-01-31'
)
print(statement)
```
### 场景3: 账龄分析
```python
# 加载未收款数据
receivables = pd.read_excel('应收账款.xlsx')
# 账龄分析
aging = aging_analysis(
receivables=receivables,
as_of_date='2026-01-31',
aging_buckets=[30, 60, 90, 180, 365]
)
print(aging)
# 导出
aging.to_excel('账龄分析_202601.xlsx')
```
### 场景4: 扫描单据OCR识别与对账匹配诉讼证据核对
适用于:承运商/供应商提供扫描件作为诉讼证据,需要与我司对账单进行匹配核对。
```python
from pdf2image import convert_from_path
import pytesseract
import pandas as pd
import re
def ocr_pdf_extract_orders(pdf_path: str, dpi: int = 150) -> pd.DataFrame:
"""
从 PDF 扫描件中 OCR 提取订单信息
Args:
pdf_path: PDF 文件路径
dpi: 图像分辨率,越高识别越准但速度越慢
Returns:
DataFrame 包含 order_no, page, tc_no 等字段
"""
print(f"正在转换 PDF...")
images = convert_from_path(pdf_path, dpi=dpi)
print(f"{len(images)} 页,开始 OCR 识别...")
all_records = []
for i, img in enumerate(images, 1):
if i % 10 == 0:
print(f" 处理进度: {i}/{len(images)}")
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
# 提取多种订单号格式(根据实际业务调整)
hm_orders = re.findall(r'(HM\d{12,15})(?:-\d+)?', text) # 华住订单
hpo_orders = re.findall(r'(HPO-\d{8}-\d+)', text) # 华住采购订单
fy_orders = re.findall(r'(FY\d{9,})', text) # 凤悦订单
phgco_orders = re.findall(r'(PHGCO\d{9,})', text) # 锦江订单
tc_orders = re.findall(r'(TC\d{8,})', text) # 运单号
# 提取日期
dates = re.findall(r'(\d{4}-\d{2}-\d{2})', text)
all_orders = hm_orders + hpo_orders + fy_orders + phgco_orders
for order in all_orders:
all_records.append({
'order_no': order,
'page': i,
'tc_no': tc_orders[0] if tc_orders else None,
'date': dates[0] if dates else None,
'raw_text': text[:500] # 保留部分原文用于核查
})
df = pd.DataFrame(all_records)
df = df.drop_duplicates(subset=['order_no'])
print(f"提取完成,共 {len(df)} 条唯一订单")
return df
def extract_base_order_no(order_no: str) -> str:
"""
提取基础订单号(移除后缀如 -1, -2
例如: HM202505227320-1 -> HM202505227320
"""
if pd.isna(order_no):
return None
order_str = str(order_no)
match = re.match(r'(HM\d{12,15}|HPO-\d{8}-\d+|FY\d{9,}|PHGCO\d{9,})', order_str)
return match.group(1) if match else order_str
def reconcile_scanned_with_excel(
scanned_df: pd.DataFrame,
excel_path: str,
main_sheet: str,
pending_sheet: str = None,
order_column: str = '华住/锦江订单号'
) -> dict:
"""
扫描单据与 Excel 对账单匹配
Args:
scanned_df: OCR 提取的订单 DataFrame
excel_path: Excel 对账单路径
main_sheet: 主数据 Sheet 名称
pending_sheet: 待结算/次月结 Sheet 名称(可选)
order_column: 订单号列名
Returns:
{
'matched_main': 在主表中匹配的订单,
'matched_pending': 在待结算表中匹配的订单,
'not_found': 未找到的订单,
'summary': 汇总统计
}
"""
# 加载主表
df_main = pd.read_excel(excel_path, sheet_name=main_sheet)
# 处理表头(如果第一行是表头)
if df_main.iloc[0].astype(str).str.contains(order_column).any():
df_main.columns = df_main.iloc[0]
df_main = df_main.iloc[1:].reset_index(drop=True)
# 提取基础订单号
main_orders = set(df_main[order_column].apply(extract_base_order_no).dropna())
# 加载待结算表(如有)
pending_orders = set()
df_pending = None
if pending_sheet:
try:
df_pending = pd.read_excel(excel_path, sheet_name=pending_sheet)
pending_orders = set(df_pending[order_column].apply(extract_base_order_no).dropna())
except:
pass
all_excel_orders = main_orders | pending_orders
# 扫描件订单号
scan_orders = set(scanned_df['order_no'].apply(extract_base_order_no).dropna())
# 匹配分析
matched_main = scan_orders & main_orders
matched_pending = scan_orders & pending_orders
not_found = scan_orders - all_excel_orders
# 汇总
summary = {
'scan_total': len(scan_orders),
'matched_main_count': len(matched_main),
'matched_pending_count': len(matched_pending),
'not_found_count': len(not_found),
'match_rate': (len(matched_main) + len(matched_pending)) / len(scan_orders) * 100 if scan_orders else 0
}
return {
'matched_main': matched_main,
'matched_pending': matched_pending,
'not_found': not_found,
'summary': summary,
'df_main': df_main,
'df_pending': df_pending
}
def generate_litigation_reconciliation_report(
result: dict,
scanned_df: pd.DataFrame,
output_path: str
):
"""
生成诉讼对账分析报告
"""
summary = result['summary']
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
# Sheet1: 汇总
summary_df = pd.DataFrame({
'项目': [
'扫描件订单数',
'在主对账单中找到',
'在待结算表中找到',
'完全未找到',
'匹配率'
],
'数值': [
summary['scan_total'],
summary['matched_main_count'],
summary['matched_pending_count'],
summary['not_found_count'],
f"{summary['match_rate']:.1f}%"
]
})
summary_df.to_excel(writer, sheet_name='对账汇总', index=False)
# Sheet2: 未找到的订单
if result['not_found']:
not_found_df = scanned_df[
scanned_df['order_no'].apply(extract_base_order_no).isin(result['not_found'])
][['order_no', 'page', 'tc_no', 'date']]
not_found_df.columns = ['订单号', 'PDF页码', '运单号', '日期']
not_found_df.to_excel(writer, sheet_name='未找到订单', index=False)
# Sheet3: 待结算订单详情
if result['matched_pending'] and result['df_pending'] is not None:
pending_detail = result['df_pending'][
result['df_pending']['华住/锦江订单号'].apply(extract_base_order_no).isin(result['matched_pending'])
]
pending_detail.to_excel(writer, sheet_name='待结算订单', index=False)
print(f"报告已保存: {output_path}")
```
#### 使用示例
```python
# 1. 从 PDF 扫描件提取订单
pdf_path = "承运商证据_6月业务.pdf"
scanned_df = ocr_pdf_extract_orders(pdf_path, dpi=150)
# 2. 与 Excel 对账单匹配
result = reconcile_scanned_with_excel(
scanned_df=scanned_df,
excel_path="对账单_2025年6月.xlsx",
main_sheet="2025.6",
pending_sheet="次月结",
order_column="华住/锦江订单号"
)
# 3. 查看匹配结果
print(f"匹配率: {result['summary']['match_rate']:.1f}%")
print(f"未找到: {result['summary']['not_found_count']}")
if result['not_found']:
print("\n未找到的订单:")
for order in result['not_found']:
print(f" {order}")
# 4. 生成报告
generate_litigation_reconciliation_report(
result=result,
scanned_df=scanned_df,
output_path="诉讼对账分析报告.xlsx"
)
```
#### OCR 识别注意事项
1. **环境依赖**:
```bash
# macOS
brew install tesseract tesseract-lang poppler
pip install pdf2image pytesseract pandas openpyxl
```
2. **识别精度**:
- DPI 150 适合快速扫描DPI 200-250 适合精确识别
- 中文识别需要 `chi_sim` 语言包
3. **订单号格式差异**:
- PDF 中可能是 `HM202505227320`
- Excel 中可能是 `HM202505227320-1`、`HM202505227320-2`(带后缀)
- 匹配时应使用基础订单号
4. **常见问题**:
- 空白页(签收单背面)会被自动跳过
- OCR 可能误识别字符(如 5→S, 0→O建议人工抽查
- 大 PDF 处理较慢,可先用低 DPI 预览
5. **"次月结"/"待结算" 订单**:
- 这类订单通常标记为"没有报价"、"未建单"等
- 虽然在系统中有记录,但可能未计入正式结算金额
- 诉讼中需特别关注这类订单的状态
6. **多承运商订单归属校验(重要风险点)**:
- 承运商可能将其他公司的订单混入证据,企图多收运费
- 必须检查PDF订单是否出现在其他承运商的对账单中
- 如果订单同时出现在多个承运商对账单中,则为异常订单
#### 多承运商订单归属校验
```python
def check_order_ownership(
scanned_df: pd.DataFrame,
my_company_excel: str,
other_carrier_excels: list[str],
order_column: str = '华住/锦江订单号'
) -> dict:
"""
检查订单归属,识别是否有其他承运商的订单被混入
Args:
scanned_df: OCR 提取的订单 DataFrame
my_company_excel: 我司对账单路径
other_carrier_excels: 其他承运商对账单路径列表
order_column: 订单号列名
Returns:
{
'my_orders': 属于我司的订单,
'other_carrier_orders': 属于其他承运商的订单(异常),
'disputed_orders': 同时出现在多处的争议订单,
'summary': 汇总
}
"""
# 提取PDF订单
scan_orders = set(scanned_df['order_no'].apply(extract_base_order_no).dropna())
# 加载我司对账单订单
my_orders = set()
try:
xl = pd.ExcelFile(my_company_excel)
for sheet in xl.sheet_names:
df = pd.read_excel(my_company_excel, sheet_name=sheet)
for col in df.columns:
if '订单' in str(col):
orders = df[col].apply(extract_base_order_no).dropna()
my_orders.update(orders)
except Exception as e:
print(f"加载我司对账单失败: {e}")
# 加载其他承运商对账单订单
other_orders = {} # {carrier_name: set of orders}
for excel_path in other_carrier_excels:
carrier_name = excel_path.split('/')[-1]
carrier_orders = set()
try:
xl = pd.ExcelFile(excel_path)
for sheet in xl.sheet_names:
df = pd.read_excel(excel_path, sheet_name=sheet)
for col in df.columns:
if '订单' in str(col):
orders = df[col].apply(extract_base_order_no).dropna()
carrier_orders.update(orders)
other_orders[carrier_name] = carrier_orders
except:
pass
# 分析订单归属
all_other_orders = set()
for orders in other_orders.values():
all_other_orders.update(orders)
# 分类
only_mine = scan_orders & my_orders - all_other_orders # 仅在我司
only_others = scan_orders & all_other_orders - my_orders # 仅在其他承运商(异常!)
in_both = scan_orders & my_orders & all_other_orders # 同时出现(争议)
nowhere = scan_orders - my_orders - all_other_orders # 哪里都没有
# 识别异常订单属于哪个承运商
abnormal_details = []
for order in only_others:
for carrier, orders in other_orders.items():
if order in orders:
page = scanned_df[scanned_df['order_no'].apply(extract_base_order_no) == order]['page'].values
abnormal_details.append({
'order_no': order,
'belongs_to': carrier,
'pdf_page': page[0] if len(page) > 0 else None
})
summary = {
'scan_total': len(scan_orders),
'my_orders_count': len(only_mine),
'other_carrier_count': len(only_others), # 异常订单数
'disputed_count': len(in_both),
'not_found_count': len(nowhere)
}
return {
'my_orders': only_mine,
'other_carrier_orders': only_others,
'abnormal_details': abnormal_details,
'disputed_orders': in_both,
'not_found': nowhere,
'summary': summary
}
```
#### 使用示例(多承运商校验)
```python
# 检查订单归属
result = check_order_ownership(
scanned_df=scanned_df,
my_company_excel="智慧云彩对账单_2025年6月.xlsx",
other_carrier_excels=[
"友利速运对账单_2025年6月.xlsx",
"其他承运商对账单.xlsx"
]
)
# 检查是否有异常订单
if result['summary']['other_carrier_count'] > 0:
print(f"⚠️ 发现 {result['summary']['other_carrier_count']} 个异常订单!")
print("这些订单属于其他承运商,不应由我司支付:")
for item in result['abnormal_details']:
print(f" {item['order_no']} -> 属于 {item['belongs_to']} (PDF第{item['pdf_page']}页)")
else:
print("✓ 未发现异常订单,所有订单归属正确")
```
---
## 注意事项
1. **数据格式统一**: 确保日期、金额格式一致后再对账
2. **匹配规则灵活**: 先精确匹配流水号,再模糊匹配金额+日期
3. **差异原因分析**: 对未达账项要逐笔核实原因
4. **定期对账**: 建议至少每月进行一次银行对账
5. **凭证留存**: 对账报告需打印签字存档
6. **OCR 核对**: 扫描件 OCR 后应人工抽查,确保识别准确
7. **多承运商校验**: 诉讼场景下必须检查订单是否属于其他承运商,防止对方混入他人订单骗取运费
---
## 与其他技能协作
- **data-excel**: 处理复杂的 Excel 数据转换
- **feishu**: 将对账报告保存到飞书云文档
- **siyuan**: 记录对账工作日志到思源笔记

View File

@@ -0,0 +1,8 @@
{
"name": "finishing-a-development-branch-plugin",
"description": "Plugin for finishing-a-development-branch",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,104 @@
---
name: finishing-a-development-branch
description: Use when implementation is complete and all tests pass - verifies and creates PR
---
# Finishing a Development Branch
## Overview
Verify tests pass, then push and create PR.
**Core principle:** Verify tests → Create PR → Done.
**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work."
## The Process
### Step 1: Verify Tests
**Before creating PR, verify tests pass:**
```bash
# Run project's test suite
npm test / cargo test / pytest / go test ./... / mvn test
```
**If tests fail:**
```
Tests failing (<N> failures). Must fix before completing:
[Show failures]
Cannot proceed with PR until tests pass.
```
Stop. Fix tests first.
**If tests pass:** Continue to Step 2.
### Step 2: Push and Create PR
Use the `/pr create` command which will:
1. **Check for existing PR first** - avoids duplicates
2. If PR exists: Report existing PR URL and skip
3. If no PR: Analyze commits, generate title/description, push, create PR
```bash
/pr create
```
**Duplicate prevention:** The `/pr create` command checks for existing open PRs on the current branch before creating a new one.
Report the PR URL when complete (whether existing or newly created).
### Step 3: Cleanup Worktree (if applicable)
Check if working in a worktree:
```bash
git worktree list | grep $(git branch --show-current)
```
If yes, ask user:
```
Worktree at <path>. Remove it now? (y/n)
```
If confirmed:
```bash
git worktree remove <worktree-path>
```
## Quick Reference
```
Tests Pass?
↓ yes
/pr create
PR URL returned
Cleanup worktree (optional)
Done
```
## Red Flags
**Never:**
- Create PR with failing tests
- Skip test verification
- Force-push without explicit request
**Always:**
- Verify tests before creating PR
- Use `/pr create` for consistent PR format
- Report the PR URL
## Integration
**Called by:**
- **executing-plans** (Step 6) - After all batches complete
**Uses:**
- **/pr create** - For pushing and PR creation

View File

@@ -0,0 +1,8 @@
{
"name": "frontend-design-plugin",
"description": "Plugin for frontend-design",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,695 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
arguments: [component|page|storybook] <description>
---
# Frontend Design 前端设计技能
创建高质量、有设计感的前端界面和组件,支持 Storybook 组件开发。
---
## 命令格式
| 命令 | 功能 | 示例 |
|------|------|------|
| `/frontend-design component <描述>` | 创建 React/Vue 组件 | `/frontend-design component 产品卡片` |
| `/frontend-design page <描述>` | 创建完整页面 | `/frontend-design page 登录页` |
| `/frontend-design storybook <描述>` | 创建带 Storybook 的组件 | `/frontend-design storybook 按钮组件` |
---
## 设计原则
### 1. 设计思维先行
在编码前,明确以下问题:
- **目的**:这个界面解决什么问题?谁在使用?
- **调性**:选择一个明确的美学方向
- **差异化**:什么让这个设计令人难忘?
### 2. 美学方向选择
| 风格 | 特点 | 适用场景 |
|------|------|----------|
| 极简主义 | 大量留白、精炼元素 | 工具类、专业平台 |
| 现代商务 | 清晰层次、专业配色 | 企业官网、B2B |
| 活力年轻 | 鲜艳色彩、动感动画 | 消费品、社交 |
| 奢华精致 | 深色调、金属质感 | 高端品牌、金融 |
| 自然有机 | 柔和曲线、自然色系 | 健康、环保 |
| 复古怀旧 | 经典字体、做旧质感 | 文化、艺术 |
| 未来科技 | 渐变、玻璃拟态 | 科技、创新 |
### 3. 避免的设计陷阱
**禁止使用**
- 过度使用的字体Inter、Roboto、Arial
- 陈词滥调的配色:紫色渐变白底
- 千篇一律的布局
- 缺乏个性的通用组件
**应该追求**
- 独特的字体组合
- 有意图的配色方案
- 打破常规的布局
- 有记忆点的细节
---
## Storybook 组件开发
### 项目结构
```
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.stories.tsx
│ │ ├── Button.module.css
│ │ └── index.ts
│ ├── Card/
│ │ ├── Card.tsx
│ │ ├── Card.stories.tsx
│ │ ├── Card.module.css
│ │ └── index.ts
│ └── index.ts
├── styles/
│ ├── variables.css
│ ├── typography.css
│ └── animations.css
└── .storybook/
├── main.ts
└── preview.ts
```
### 组件模板
#### 1. 组件文件 (Component.tsx)
```tsx
import React from 'react';
import styles from './Component.module.css';
export interface ComponentProps {
/** 组件变体 */
variant?: 'primary' | 'secondary' | 'outline';
/** 尺寸 */
size?: 'sm' | 'md' | 'lg';
/** 是否禁用 */
disabled?: boolean;
/** 子元素 */
children: React.ReactNode;
/** 点击事件 */
onClick?: () => void;
}
export const Component: React.FC<ComponentProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
children,
onClick,
}) => {
return (
<div
className={`${styles.component} ${styles[variant]} ${styles[size]}`}
data-disabled={disabled}
onClick={disabled ? undefined : onClick}
>
{children}
</div>
);
};
```
#### 2. Storybook Stories (Component.stories.tsx)
```tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Component } from './Component';
const meta: Meta<typeof Component> = {
title: 'Components/Component',
component: Component,
tags: ['autodocs'],
parameters: {
layout: 'centered',
docs: {
description: {
component: '组件描述文档',
},
},
},
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline'],
description: '组件变体样式',
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg'],
description: '组件尺寸',
},
disabled: {
control: 'boolean',
description: '是否禁用',
},
},
};
export default meta;
type Story = StoryObj<typeof Component>;
/** 默认状态 */
export const Default: Story = {
args: {
children: '默认组件',
},
};
/** 主要变体 */
export const Primary: Story = {
args: {
variant: 'primary',
children: '主要按钮',
},
};
/** 次要变体 */
export const Secondary: Story = {
args: {
variant: 'secondary',
children: '次要按钮',
},
};
/** 不同尺寸 */
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Component size="sm"></Component>
<Component size="md"></Component>
<Component size="lg"></Component>
</div>
),
};
/** 禁用状态 */
export const Disabled: Story = {
args: {
disabled: true,
children: '禁用状态',
},
};
```
#### 3. 样式文件 (Component.module.css)
```css
.component {
/* 基础样式 */
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
font-family: var(--font-sans);
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
/* 变体 */
.primary {
background: var(--color-primary);
color: white;
}
.primary:hover {
background: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--color-primary-shadow);
}
.secondary {
background: var(--color-secondary);
color: var(--color-text);
}
.outline {
background: transparent;
border: 2px solid var(--color-border);
color: var(--color-text);
}
/* 尺寸 */
.sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.md {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
.lg {
padding: 1rem 2rem;
font-size: 1.125rem;
}
/* 状态 */
[data-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
```
---
## 设计系统变量
### CSS 变量模板
```css
:root {
/* 颜色 */
--color-primary: #0066ff;
--color-primary-dark: #0052cc;
--color-primary-light: #4d94ff;
--color-primary-shadow: rgba(0, 102, 255, 0.25);
--color-secondary: #f0f4f8;
--color-accent: #ff6b35;
--color-text: #1a1a2e;
--color-text-muted: #64748b;
--color-text-inverse: #ffffff;
--color-background: #ffffff;
--color-surface: #f8fafc;
--color-border: #e2e8f0;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
/* 字体 */
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-display: 'Clash Display', var(--font-sans);
--font-mono: 'JetBrains Mono', monospace;
/* 字号 */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 2rem;
--text-4xl: 2.5rem;
/* 间距 */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
--space-16: 4rem;
/* 圆角 */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
--radius-xl: 1.5rem;
--radius-full: 9999px;
/* 阴影 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* 动画 */
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* 暗色主题 */
[data-theme="dark"] {
--color-text: #f1f5f9;
--color-text-muted: #94a3b8;
--color-background: #0f172a;
--color-surface: #1e293b;
--color-border: #334155;
}
```
---
## 常用组件示例
### 1. 产品卡片 (ProductCard)
```tsx
// ProductCard.tsx
import React from 'react';
import styles from './ProductCard.module.css';
export interface ProductCardProps {
image: string;
title: string;
location: string;
rating: number;
reviewCount: number;
price: number;
originalPrice?: number;
tags?: string[];
onAddToCart?: () => void;
}
export const ProductCard: React.FC<ProductCardProps> = ({
image,
title,
location,
rating,
reviewCount,
price,
originalPrice,
tags = [],
onAddToCart,
}) => {
return (
<article className={styles.card}>
<div className={styles.imageWrapper}>
<img src={image} alt={title} className={styles.image} />
{tags.length > 0 && (
<div className={styles.tags}>
{tags.map((tag) => (
<span key={tag} className={styles.tag} data-tag={tag}>
{tag}
</span>
))}
</div>
)}
</div>
<div className={styles.content}>
<h3 className={styles.title}>{title}</h3>
<p className={styles.location}>📍 {location}</p>
<div className={styles.rating}>
<span className={styles.stars}> {rating.toFixed(1)}</span>
<span className={styles.reviewCount}>({reviewCount})</span>
</div>
<div className={styles.priceRow}>
<div className={styles.price}>
<span className={styles.currency}>¥</span>
<span className={styles.amount}>{price}</span>
<span className={styles.suffix}></span>
</div>
{originalPrice && (
<span className={styles.originalPrice}>¥{originalPrice}</span>
)}
</div>
<button className={styles.addButton} onClick={onAddToCart}>
</button>
</div>
</article>
);
};
```
```tsx
// ProductCard.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ProductCard } from './ProductCard';
const meta: Meta<typeof ProductCard> = {
title: 'Components/ProductCard',
component: ProductCard,
tags: ['autodocs'],
parameters: {
layout: 'centered',
backgrounds: {
default: 'light',
},
},
};
export default meta;
type Story = StoryObj<typeof ProductCard>;
export const Default: Story = {
args: {
image: 'https://images.unsplash.com/photo-1494947665470-20322015e3a8',
title: '袋鼠岛一日游',
location: '阿德莱德出发',
rating: 4.8,
reviewCount: 126,
price: 389,
tags: ['热卖', '含午餐'],
},
};
export const WithDiscount: Story = {
args: {
...Default.args,
originalPrice: 499,
tags: ['特惠', '限时'],
},
};
export const Grid: Story = {
render: () => (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 300px)',
gap: '1.5rem'
}}>
<ProductCard
image="https://images.unsplash.com/photo-1494947665470-20322015e3a8"
title="袋鼠岛一日游"
location="阿德莱德出发"
rating={4.8}
reviewCount={126}
price={389}
tags={['热卖']}
/>
<ProductCard
image="https://images.unsplash.com/photo-1506905925346-21bda4d32df4"
title="巴罗莎谷酒庄之旅"
location="阿德莱德出发"
rating={4.9}
reviewCount={89}
price={299}
originalPrice={399}
tags={['特惠', '含品酒']}
/>
<ProductCard
image="https://images.unsplash.com/photo-1540202403-b7abd6747a18"
title="海豚巡航体验"
location="格雷尔海滩"
rating={4.7}
reviewCount={234}
price={159}
tags={['亲子']}
/>
</div>
),
};
```
### 2. 按钮组件 (Button)
```tsx
// Button.tsx
import React from 'react';
import styles from './Button.module.css';
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
loading?: boolean;
disabled?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
children: React.ReactNode;
onClick?: () => void;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
fullWidth = false,
loading = false,
disabled = false,
leftIcon,
rightIcon,
children,
onClick,
}) => {
return (
<button
className={`
${styles.button}
${styles[variant]}
${styles[size]}
${fullWidth ? styles.fullWidth : ''}
`}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? (
<span className={styles.spinner} />
) : (
<>
{leftIcon && <span className={styles.icon}>{leftIcon}</span>}
<span>{children}</span>
{rightIcon && <span className={styles.icon}>{rightIcon}</span>}
</>
)}
</button>
);
};
```
---
## Storybook 配置
### .storybook/main.ts
```ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
```
### .storybook/preview.ts
```ts
import type { Preview } from '@storybook/react';
import '../src/styles/variables.css';
import '../src/styles/typography.css';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'gray', value: '#f8fafc' },
{ name: 'dark', value: '#0f172a' },
],
},
},
globalTypes: {
theme: {
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
title: 'Theme',
icon: 'circlehollow',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
};
export default preview;
```
---
## 快速启动命令
### 创建新组件
```bash
# 创建组件目录
mkdir -p src/components/ComponentName
# 创建文件
touch src/components/ComponentName/{ComponentName.tsx,ComponentName.stories.tsx,ComponentName.module.css,index.ts}
```
### 安装 Storybook
```bash
# 初始化 Storybook
npx storybook@latest init
# 安装额外插件
npm install -D @storybook/addon-a11y @storybook/addon-interactions
# 启动 Storybook
npm run storybook
```
---
## 设计检查清单
### 组件质量检查
- [ ] Props 接口定义完整,带 JSDoc 注释
- [ ] 支持必要的变体variant和尺寸size
- [ ] 处理禁用和加载状态
- [ ] 支持自定义 className
- [ ] 键盘可访问性
- [ ] 屏幕阅读器友好
### Storybook 质量检查
- [ ] 所有变体都有对应 Story
- [ ] argTypes 配置完整
- [ ] 包含组件文档描述
- [ ] 交互状态可测试
- [ ] 响应式展示
### 视觉质量检查
- [ ] 字体选择有特色
- [ ] 配色方案协调
- [ ] 动画流畅自然
- [ ] 间距一致
- [ ] 暗色主题支持

View File

@@ -0,0 +1,8 @@
{
"name": "gitea-plugin",
"description": "Gitea 代码托管与 CI/CD 管理。用于 Gitea Actions workflow 管理、Runner 管理、PR 操作、仓库配置。",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,211 @@
#!/bin/bash
# gitea-runs — Gitea Actions CLI helper
# Usage:
# gitea-runs List recent runs
# gitea-runs list [limit] List recent runs (default 10)
# gitea-runs view <run_number> View run details & jobs
# gitea-runs open [run_number] Open run in browser
# gitea-runs workflows List workflows
# gitea-runs dispatch <wf> [ref] Trigger a workflow dispatch
# gitea-runs help Show this help
set -e
# Config from tea CLI
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/Library/Application Support}/tea/config.yml"
if [ ! -f "$TEA_CONFIG" ]; then
TEA_CONFIG="$HOME/.config/tea/config.yml"
fi
# Parse tea config (nested under logins)
GITEA_URL=$(grep 'url:' "$TEA_CONFIG" | head -1 | awk '{print $NF}')
GITEA_TOKEN=$(grep 'token:' "$TEA_CONFIG" | head -1 | awk '{print $NF}')
# Detect repo from git remote
REPO=$(git remote get-url origin 2>/dev/null | sed 's|.*gitea.pipexerp.com[:/]*||;s|\.git$||;s|^10022/||')
if [ -z "$REPO" ]; then
echo "Error: not in a git repo or remote not configured"
exit 1
fi
API="$GITEA_URL/api/v1"
AUTH="Authorization: token $GITEA_TOKEN"
GREEN='\033[0;32m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'
cmd_dispatch() {
local workflow="${1:-}"
local ref="${2:-main}"
if [ -z "$workflow" ]; then
echo "Usage: gitea-runs dispatch <workflow> [ref]"
echo ""
echo "Available workflows:"
curl -s -H "$AUTH" "$API/repos/$REPO/actions/workflows" 2>/dev/null \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for w in data.get('workflows', []):
print(f\" {w['id']:30s} {w['name']}\")
" 2>/dev/null
return
fi
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-H "$AUTH" \
-H "Content-Type: application/json" \
-X POST "$API/repos/$REPO/actions/workflows/$workflow/dispatches" \
-d "{\"ref\":\"$ref\"}" 2>/dev/null)
if [ "$http_code" = "204" ]; then
echo -e "${GREEN}✓${NC} Dispatched workflow: $workflow (ref: $ref)"
echo " View: $GITEA_URL/$REPO/actions"
else
echo -e "${RED}✗${NC} Failed to dispatch (HTTP $http_code)"
fi
}
cmd_workflows() {
echo -e "${CYAN}Workflows for $REPO${NC}"
echo ""
curl -s -H "$AUTH" "$API/repos/$REPO/actions/workflows" 2>/dev/null \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for w in data.get('workflows', []):
state = '✓' if w['state'] == 'active' else '✗'
print(f\" {state} {w['id']:30s} {w['name']}\")
" 2>/dev/null
}
cmd_list() {
local limit="${1:-10}"
echo -e "${CYAN}Recent runs for $REPO${NC}"
echo ""
curl -s -H "$AUTH" "$API/repos/$REPO/actions/runs?limit=$limit" 2>/dev/null \
| python3 -c "
import json, sys
limit = $limit
data = json.load(sys.stdin)
for r in data.get('workflow_runs', [])[:limit]:
status = r.get('status', '?')
num = r.get('run_number', 0)
title = r.get('display_title', '')[:60]
wf = r.get('path', '')
wf = wf.split('@')[0] if '@' in wf else wf
icon = {'success':'\u2713','completed':'\u2713','failure':'\u2717','cancelled':'\u2717','in_progress':'\u27f3','running':'\u27f3','queued':'\u25cc','waiting':'\u25cc'}.get(status, '?')
color = {'success':'\033[0;32m','completed':'\033[0;32m','failure':'\033[0;31m','cancelled':'\033[0;31m','in_progress':'\033[0;33m','running':'\033[0;33m'}.get(status, '\033[0;37m')
print(f\"{color}{icon}\033[0m #{num:<4} {status:<12} {wf:<20} {title}\")
" 2>/dev/null
}
cmd_view() {
local run_number="${1:-}"
if [ -z "$run_number" ]; then
echo "Usage: gitea-runs view <run_number>"
return 1
fi
# Find run by run_number (API uses internal id, html uses run_number)
local run_data
run_data=$(curl -s -H "$AUTH" "$API/repos/$REPO/actions/runs?limit=50" 2>/dev/null \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for r in data.get('workflow_runs', []):
if r['run_number'] == $run_number:
print(json.dumps(r))
break
" 2>/dev/null)
if [ -z "$run_data" ]; then
echo -e "${RED}✗${NC} Run #$run_number not found"
return 1
fi
local run_id
run_id=$(echo "$run_data" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
# Print run info
echo "$run_data" | python3 -c "
import json, sys
r = json.load(sys.stdin)
status = r.get('status', '?')
icon = {'success':'\u2713','failure':'\u2717','in_progress':'\u27f3','queued':'\u25cc'}.get(status, '?')
color = {'success':'\033[0;32m','failure':'\033[0;31m','in_progress':'\033[0;33m'}.get(status, '\033[0;37m')
print(f\"{color}{icon} Run #{r.get('run_number',0)} \u2014 {status}\033[0m\")
print(f\" Title: {r.get('display_title','')}\")
print(f\" Event: {r.get('event','')}\")
print(f\" Branch: {r.get('head_branch','')}\")
print(f\" Commit: {r.get('head_sha','')[:8]}\")
print(f\" Actor: {r.get('actor',{}).get('login','')}\")
wf = r.get('path', '')
wf = wf.split('@')[0] if '@' in wf else wf
print(f\" Workflow: {wf}\")
" 2>/dev/null
# Print jobs
echo ""
echo -e "${CYAN}Jobs:${NC}"
curl -s -H "$AUTH" "$API/repos/$REPO/actions/runs/$run_id/jobs" 2>/dev/null \
| python3 -c "
import json, sys
from datetime import datetime
data = json.load(sys.stdin)
for j in data.get('jobs', []):
status = j.get('status', '?')
icon = {'success':'\u2713','failure':'\u2717','in_progress':'\u27f3','queued':'\u25cc','waiting':'\u25cc'}.get(status, '?')
color = {'success':'\033[0;32m','failure':'\033[0;31m','in_progress':'\033[0;33m'}.get(status, '\033[0;37m')
runner = j.get('runner_name', '-')
started = j.get('started_at', '')[:19].replace('T', ' ')
completed = j.get('completed_at', '')[:19].replace('T', ' ')
duration = ''
if completed and not completed.startswith('1970'):
try:
d = datetime.fromisoformat(completed) - datetime.fromisoformat(started)
duration = f' ({int(d.total_seconds())}s)'
except: pass
print(f\" {color}{icon}\033[0m {j.get('name',''):<30} {status:<12} runner: {runner}{duration}\")
" 2>/dev/null
}
cmd_open() {
local run_id="${1:-}"
local url="$GITEA_URL/$REPO/actions"
if [ -n "$run_id" ]; then
url="$url/runs/$run_id"
fi
echo "Opening: $url"
open "$url" 2>/dev/null || xdg-open "$url" 2>/dev/null || echo "$url"
}
cmd_help() {
echo "gitea-runs — Gitea Actions CLI helper"
echo ""
echo "Usage:"
echo " gitea-runs List recent runs"
echo " gitea-runs list [limit] List recent runs (default 10)"
echo " gitea-runs view <run_number> View run details & jobs"
echo " gitea-runs open [run_number] Open run in browser"
echo " gitea-runs workflows List workflows"
echo " gitea-runs dispatch <wf> [ref] Trigger a workflow dispatch"
echo " gitea-runs help Show this help"
echo ""
echo "Repo: $REPO"
echo "Gitea: $GITEA_URL"
}
# Main
case "${1:-}" in
list|ls) shift; cmd_list "$@" ;;
view|v) shift; cmd_view "$@" ;;
dispatch) shift; cmd_dispatch "$@" ;;
workflows|wf) cmd_workflows ;;
open|o) shift; cmd_open "$@" ;;
help|--help|-h) cmd_help ;;
"") cmd_list ;;
*) cmd_view "$1" ;;
esac

View File

@@ -0,0 +1,281 @@
---
name: gitea
description: Gitea 代码托管与 CI/CD 管理。用于 Gitea Actions workflow 管理、Runner 管理、PR 操作、仓库配置。当用户提到 Gitea、Actions、Runner、CI/CD workflow、PR 检查相关任务时自动激活。
---
# Gitea Skill
Gitea 代码托管平台管理,覆盖 Actions CI/CD、Runner、PR、仓库配置。
## 服务器信息
| 服务 | 地址 | SSH |
|------|------|-----|
| Gitea Web | https://gitea.pipexerp.com | — |
| Gitea SSH | gitea.pipexerp.com:10022 | `ssh -i ~/.ssh/id_ed25519 git@gitea.pipexerp.com -p 10022` |
| Gitea 服务器 | 123.56.89.187 | `ssh -i ~/.ssh/tools.pem root@123.56.89.187` |
| Runner 服务器 | 101.200.136.200 (Jenkins 服务器) | `ssh -i ~/.ssh/tools.pem root@101.200.136.200` |
## API 访问
```bash
# Gitea API Token (仓库级)
GITEA_TOKEN="483a2b65219625ee382eb6d023cda39238c32e24"
# 通用请求格式
curl -s "https://gitea.pipexerp.com/api/v1/repos/pipexerp/<repo>/..." \
-H "Authorization: token $GITEA_TOKEN"
```
### 常用 API
| 操作 | 方法 | 端点 |
|------|------|------|
| 创建 PR | POST | `/repos/{owner}/{repo}/pulls` |
| 更新 PR | PATCH | `/repos/{owner}/{repo}/pulls/{id}` |
| 列出 Runs | GET | `/repos/{owner}/{repo}/actions/runs` |
| Run 详情 | GET | `/repos/{owner}/{repo}/actions/runs/{id}` |
| Job 详情 | GET | `/repos/{owner}/{repo}/actions/runs/{id}/jobs` |
| 手动触发 Workflow | POST | `/repos/{owner}/{repo}/actions/workflows/{file}/dispatches` body: `{"ref":"main"}` |
| 获取 Runner Token | POST | `/repos/{owner}/{repo}/actions/runners/registration-token` |
| 添加 Secret | PUT | `/repos/{owner}/{repo}/actions/secrets/{name}` body: `{"data":"value"}` |
| 删除 Run仅已完成| DELETE | `/repos/{owner}/{repo}/actions/runs/{id}` |
**注意**: Gitea 1.25 **不支持**通过 API cancel 正在排队/运行的 run。
## 仓库
| 仓库 | 地址 | 主分支 |
|------|------|--------|
| coolbuy-paas | pipexerp/coolbuy-paas | main |
| dotfiles | huangjun/dotfiles | main |
| claude-marketplace | huangjun/claude-marketplace | main |
## Actions Runners
### 主 Runner (lint/test/e2e)
| 项目 | 值 |
|------|-----|
| 名称 | jenkins-runner |
| 配置 | `/opt/act_runner/config.yaml` |
| Capacity | 3 |
| Labels | `ubuntu-latest`, `ubuntu-22.04`, `ubuntu-20.04` |
| 进程 | `/usr/local/bin/act_runner daemon --config /opt/act_runner/config.yaml` |
### Deploy Runner (staging 部署专用)
| 项目 | 值 |
|------|-----|
| 名称 | deploy-runner |
| 配置 | `/opt/act_runner_deploy/config.yaml` |
| Capacity | 1 |
| Labels | `deploy:host` |
| PID | `/opt/act_runner_deploy/runner.pid` |
| 日志 | `/opt/act_runner_deploy/runner.log` |
| 启动 | `cd /opt/act_runner_deploy && nohup act_runner daemon --config config.yaml > runner.log 2>&1 &` |
### 注册新 Runner
```bash
# 1. 获取 registration token
curl -s -X POST "https://gitea.pipexerp.com/api/v1/repos/pipexerp/coolbuy-paas/actions/runners/registration-token" \
-H "Authorization: token $GITEA_TOKEN"
# 2. SSH 到 runner 服务器
ssh -i ~/.ssh/tools.pem root@101.200.136.200
# 3. 创建目录和配置
mkdir -p /opt/act_runner_<name>
cat > /opt/act_runner_<name>/config.yaml << 'EOF'
log:
level: info
runner:
file: .runner
capacity: 1
timeout: 30m
labels:
- "<label>:host" # host 模式用系统 shell
# 或 "<label>:docker://image" # docker 模式
cache:
enabled: false
EOF
# 4. 注册
cd /opt/act_runner_<name>
act_runner register --instance https://gitea.pipexerp.com \
--token <TOKEN> --name <NAME> --labels '<LABEL>:host' \
--config config.yaml --no-interactive
# 5. 启动
nohup act_runner daemon --config config.yaml > runner.log 2>&1 &
echo $! > runner.pid
```
### Runner 运维
```bash
# 检查 runner 状态
ssh -i ~/.ssh/tools.pem root@101.200.136.200 "ps aux | grep act_runner | grep -v grep"
# 查看 deploy runner 日志
ssh -i ~/.ssh/tools.pem root@101.200.136.200 "tail -20 /opt/act_runner_deploy/runner.log"
# 重启 deploy runner
ssh -i ~/.ssh/tools.pem root@101.200.136.200 "
kill \$(cat /opt/act_runner_deploy/runner.pid) 2>/dev/null
cd /opt/act_runner_deploy
nohup act_runner daemon --config config.yaml > runner.log 2>&1 &
echo \$! > runner.pid
"
```
## Workflows (coolbuy-paas)
| Workflow | 触发 | Runner | paths-ignore | 用途 |
|----------|------|--------|-------------|------|
| 🚀 deploy-staging.yml | push → main | `deploy` | md, docs, .gitea, scripts, *_test.go | 触发 Jenkins 部署到 staging |
| 🔍 lint.yml | PR → main | `ubuntu-latest` | md, docs, .gitea, scripts | Go lint + ESLint auto-fix |
| 🧪 unit-test.yml | PR → main | `ubuntu-latest` | md, docs, .gitea, scripts | 4 个 Go 服务单元测试 |
| 🎭 e2e-tests.yml | schedule 12h | `ubuntu-latest` | — | Playwright E2E仅定时 |
| 📋 notify-aiproj.yml | PR merged | `ubuntu-latest` | — | 同步需求状态到 ai-proj |
| 📦 build.yaml | 手动 | `ubuntu-latest` | — | Docker 构建推 Hub |
### Workflow 编写规范
```yaml
# 1. 名称加 emoji 前缀
name: "🚀 Deploy Staging"
# 2. 非代码变更加 paths-ignore
on:
push:
branches: [main]
paths-ignore:
- '*.md'
- 'docs/**'
- '.gitea/**'
- 'scripts/**'
# 3. 加 concurrency 防重复
concurrency:
group: deploy-staging
cancel-in-progress: true
# 4. 仅定时的 workflow 加 event 守卫
jobs:
e2e:
if: github.event_name == 'schedule'
# 5. auto-fix 提交加 [skip ci]
git commit -m "style: auto-fix [skip ci]"
```
### Checkout 模式(容器内)
Gitea Actions 不支持 `actions/checkout`,用原生 git
```yaml
- name: Checkout
env:
TOKEN: ${{ github.token }}
run: |
git config --global --add safe.directory "$(pwd)"
git init
git remote add origin "https://oauth2:${TOKEN}@gitea.pipexerp.com/${{ github.repository }}.git"
git fetch origin "${{ github.event.pull_request.head.ref }}"
git checkout -b pr-branch "origin/${{ github.event.pull_request.head.ref }}"
git config user.name "CI Bot"
git config user.email "ci@pipexerp.com"
```
## Secrets 管理
### 当前 Secrets (coolbuy-paas 仓库级)
| Secret | 用途 |
|--------|------|
| `JENKINS_USER` | Jenkins API 用户名 |
| `JENKINS_TOKEN` | Jenkins API Token |
| `DOCKER_HUB_TOKEN` | Docker Hub 推送 |
| `AI_PROJ_TOKEN` | ai-proj API 认证 |
### 添加/更新 Secret
```bash
curl -s -X PUT \
"https://gitea.pipexerp.com/api/v1/repos/pipexerp/coolbuy-paas/actions/secrets/<NAME>" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"data": "<VALUE>"}'
```
## CI/CD 完整流程
```
PR → main
├── 🧪 unit-test.yml (Go 服务测试)
├── 🔍 lint.yml (auto-fix 格式)
└── merge 后:
├── 📋 notify-aiproj.yml (需求状态 → testing)
└── 🚀 deploy-staging.yml (Jenkins → staging)
├── ≥5 commits/2h → 立即部署
└── <5 commits → 等3分钟 debounce
定时:
└── 🎭 e2e-tests.yml (每12h Playwright)
手动:
└── 📦 build.yaml (Docker 构建推 Hub)
生产部署:
└── ./scripts/build-and-push.sh prod --deploy (触发 Jenkins)
```
## 本地 CLI 工具
### tea CLI (Gitea 官方命令行)
tea 是 Gitea 官方 CLI 客户端,已配置好认证信息。
```bash
# 配置文件位置
~/Library/Application Support/tea/config.yml
# 或 ~/.config/tea/config.yml
# gitea-runs 脚本从 tea config 读取 url 和 token
```
### gitea-runs (Actions 快捷命令)
位置: `~/.local/bin/gitea-runs`
自动从 git remote 检测仓库,从 tea CLI 配置读取认证信息。
| 命令 | 说明 |
|------|------|
| `gitea-runs` | 列出最近 10 条 run |
| `gitea-runs list [N]` | 列出最近 N 条 run |
| `gitea-runs view <run_number>` | 查看 run 详情和 jobs |
| `gitea-runs open [run_number]` | 在浏览器打开 run 页面 |
| `gitea-runs workflows` | 列出所有 workflow |
| `gitea-runs dispatch <wf> [ref]` | 手动触发 workflow |
```bash
# 示例
gitea-runs # 查看最近 runs
gitea-runs view 303 # 查看 run #303 详情
gitea-runs dispatch deploy-staging.yml main # 手动触发部署
gitea-runs open # 打开 Actions 页面
```
**优先使用 `gitea-runs` 而非 curl API**,更简洁且自动处理认证。
## 常见问题
| 问题 | 原因 | 解决 |
|------|------|------|
| Run 一直 queued | Runner 被占满 | 等其他 job 完成,或加 runner |
| deploy 被 test 阻塞 | 共用 runner | 用 `runs-on: deploy` 专属 runner |
| Workflow 被误触发 | push 新 workflow 文件到 main | 加 `if: github.event_name == 'schedule'` 守卫 |
| auto-fix 无限循环 | 提交触发新 run | 提交信息加 `[skip ci]` |
| API 无法 cancel run | Gitea 1.25 限制 | 网页手动取消,或等完成后 DELETE |
| `date -d` 报错 | 容器 date 不兼容 | 用 host 模式 runner或兼容写法 |

View File

@@ -0,0 +1,8 @@
{
"name": "openclaw-ops-plugin",
"description": "Plugin for openclaw-ops",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,797 @@
# OpenClaw 运维技能
OpenClaw 容器化部署、运维监控、故障排查完整指南。
## 目录
- [服务器部署](#服务器部署)
- [容器管理](#容器管理)
- [性能优化](#性能优化)
- [故障排查](#故障排查)
- [最佳实践](#最佳实践)
---
## 服务器部署
### 懒猫算力仓 (lazycat)
**服务器信息**
- 主机名haiqing.heiyu.space
- SSH 别名lazycat, lanmao
- 用途OpenClaw 算力服务
- 系统Debian-based Linux
- 容器平台lzc-docker
**OpenClaw 容器信息**
- 容器 ID5f3bf33e090b
- 镜像registry.lazycat.cloud/openclaw:1.1.5
- OpenClaw 版本2026.2.9
- 容器名iamxiaoelzcappopenclaw-openclaw-1
**访问方式**
```bash
# SSH 连接
ssh lazycat
# 进入容器
ssh lazycat "lzc-docker exec -it 5f3bf33e090b bash"
# 启动 OpenClaw TUI
openclaw-tui # 使用本地快捷脚本
```
---
## 容器管理
### 快捷访问脚本
**~/bin/openclaw-tui**
```bash
#!/bin/bash
# OpenClaw TUI 快捷访问脚本(自动启动 Gateway
set -e
echo "🦞 连接到龙虾服务器 (懒猫)..."
echo ""
# 检查并启动 Gateway
echo "检查 OpenClaw Gateway 状态..."
GATEWAY_STATUS=$(ssh lazycat "lzc-docker exec 5f3bf33e090b openclaw gateway status" 2>/dev/null | grep "RPC probe" || echo "failed")
if echo "$GATEWAY_STATUS" | grep -q "ok"; then
echo "✅ Gateway 已运行"
else
echo "🔧 启动 Gateway..."
ssh lazycat "lzc-docker exec -d 5f3bf33e090b bash -c 'nohup openclaw gateway run > /tmp/gateway.log 2>&1 &'" 2>/dev/null
sleep 2
echo "✅ Gateway 已启动"
fi
echo ""
echo "启动 OpenClaw TUI..."
# SSH 到懒猫服务器,然后进入 Docker 容器并启动 OpenClaw TUI
ssh -t lazycat "lzc-docker exec -it 5f3bf33e090b bash -c 'openclaw tui'"
```
### 容器操作命令
```bash
# 查看容器状态
ssh lazycat "lzc-docker ps | grep openclaw"
# 查看容器日志
ssh lazycat "lzc-docker logs -f 5f3bf33e090b --tail 100"
# 重启容器
ssh lazycat "lzc-docker restart 5f3bf33e090b"
# 查看容器资源使用
ssh lazycat "lzc-docker stats --no-stream 5f3bf33e090b"
# 进入容器 shell
ssh lazycat "lzc-docker exec -it 5f3bf33e090b bash"
```
### Gateway 管理
```bash
# 检查 Gateway 状态
ssh lazycat "lzc-docker exec 5f3bf33e090b openclaw gateway status"
# 启动 Gateway前台
ssh lazycat "lzc-docker exec 5f3bf33e090b openclaw gateway run"
# 启动 Gateway后台
ssh lazycat "lzc-docker exec -d 5f3bf33e090b bash -c 'nohup openclaw gateway run > /tmp/gateway.log 2>&1 &'"
# 停止 Gateway
ssh lazycat "lzc-docker exec 5f3bf33e090b pkill -f 'openclaw-gateway'"
# 查看 Gateway 日志
ssh lazycat "lzc-docker exec 5f3bf33e090b tail -f /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log"
```
---
## 性能优化
### 资源配置
**当前配置**(接近系统上限,确保充足性能):
- 内存限制30GB系统 97%
- 内存+交换32GB
- CPU 限制8.0 核心(系统 100%
- 进程限制10,000 个
```bash
# 查看资源限制配置
ssh lazycat "lzc-docker inspect 5f3bf33e090b --format='
内存限制: {{.HostConfig.Memory}} bytes
内存+交换: {{.HostConfig.MemorySwap}} bytes
CPU配额: {{.HostConfig.CpuQuota}}
CPU周期: {{.HostConfig.CpuPeriod}}
PID限制: {{.HostConfig.PidsLimit}}
'"
# 查看实时资源使用
ssh lazycat "lzc-docker stats --no-stream 5f3bf33e090b"
```
**配置说明**
- 懒猫算力仓的主要职责是提供 OpenClaw 服务
- 资源限制设置为接近系统上限,确保有充足资源运行
- 同时提供基本的失控保护机制
### 自动化优化措施
#### 1. 定期自动重启(每周日 03:00
**目的**:清理累积的僵尸进程,释放资源
**查看状态**
```bash
# 查看定时任务状态
ssh lazycat "systemctl status openclaw-restart.timer"
# 查看重启日志
ssh lazycat "tail -50 /var/log/openclaw-restart.log"
# 手动执行重启
ssh lazycat "/root/restart-openclaw.sh"
```
**配置文件**
- Service: `/etc/systemd/system/openclaw-restart.service`
- Timer: `/etc/systemd/system/openclaw-restart.timer`
- 脚本: `/root/restart-openclaw.sh`
**重启脚本** (`/root/restart-openclaw.sh`)
```bash
#!/bin/bash
# OpenClaw 容器定期重启脚本
# 每周日凌晨3点执行
LOG_FILE='/var/log/openclaw-restart.log'
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始重启 OpenClaw 容器" >> $LOG_FILE
# 重启容器
/lzcsys/bin/lzc-docker restart 5f3bf33e090b >> $LOG_FILE 2>&1
# 等待容器启动
sleep 10
# 检查健康状态
STATUS=$(/lzcsys/bin/lzc-docker inspect -f '{{.State.Health.Status}}' 5f3bf33e090b 2>/dev/null || echo 'unknown')
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 重启完成,健康状态: $STATUS" >> $LOG_FILE
# 检查僵尸进程
ZOMBIE_COUNT=$(/lzcsys/bin/lzc-docker exec 5f3bf33e090b ps aux | grep 'Z' | wc -l)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 当前僵尸进程数: $ZOMBIE_COUNT" >> $LOG_FILE
echo "----------------------------------------" >> $LOG_FILE
```
#### 2. 僵尸进程自动监控(每小时检查)
**目的**:监控僵尸进程数量,超过阈值自动重启容器
**查看状态**
```bash
# 查看监控状态
ssh lazycat "systemctl status openclaw-zombie-monitor.timer"
# 查看监控日志
ssh lazycat "tail -50 /var/log/openclaw-zombie-monitor.log"
# 手动检查僵尸进程
ssh lazycat "/root/monitor-openclaw-zombies.sh"
# 直接查看僵尸进程数
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep 'Z' | wc -l"
```
**监控参数**
- 检查频率:每小时
- 触发阈值50 个僵尸进程
- 自动操作:重启容器
**监控脚本** (`/root/monitor-openclaw-zombies.sh`)
```bash
#!/bin/bash
# OpenClaw 僵尸进程监控脚本
# 当僵尸进程超过50个时自动重启容器
ZOMBIE_THRESHOLD=50
CONTAINER_ID='5f3bf33e090b'
LOG_FILE='/var/log/openclaw-zombie-monitor.log'
# 检查僵尸进程数量
ZOMBIE_COUNT=$(/lzcsys/bin/lzc-docker exec $CONTAINER_ID ps aux 2>/dev/null | grep -c 'Z' || echo '0')
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 僵尸进程数: $ZOMBIE_COUNT" >> $LOG_FILE
if [ $ZOMBIE_COUNT -gt $ZOMBIE_THRESHOLD ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ 僵尸进程超过阈值($ZOMBIE_THRESHOLD),执行自动重启" >> $LOG_FILE
# 重启容器
/lzcsys/bin/lzc-docker restart $CONTAINER_ID >> $LOG_FILE 2>&1
# 等待容器启动
sleep 10
# 再次检查
NEW_ZOMBIE_COUNT=$(/lzcsys/bin/lzc-docker exec $CONTAINER_ID ps aux 2>/dev/null | grep -c 'Z' || echo '0')
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 重启后僵尸进程数: $NEW_ZOMBIE_COUNT" >> $LOG_FILE
echo "----------------------------------------" >> $LOG_FILE
fi
```
#### 3. 全面健康检查
```bash
# 一键健康检查脚本
ssh lazycat "
echo '=== 系统负载 ===' && uptime &&
echo '' && echo '=== 僵尸进程 ===' &&
lzc-docker exec 5f3bf33e090b ps aux | grep 'Z' | wc -l &&
echo '' && echo '=== 容器资源 ===' &&
lzc-docker stats --no-stream 5f3bf33e090b &&
echo '' && echo '=== Gateway 状态 ===' &&
lzc-docker exec 5f3bf33e090b openclaw gateway status | grep 'RPC probe' &&
echo '' && echo '=== 容器健康 ===' &&
lzc-docker inspect 5f3bf33e090b --format='Status: {{.State.Status}}, Health: {{.State.Health.Status}}'
"
# 查看所有定时任务
ssh lazycat "systemctl list-timers | grep openclaw"
```
---
## 故障排查
### Tower 反复崩溃(已修复 2026-02-16
**现象**
- Tower 日志显示反复崩溃:`[tower] OpenClaw crashed: exit status 1`
- Gateway 启动失败:`gateway already running (pid xxx); lock timeout`
- 僵尸 Gateway 进程堆积,无法回收
- 日志中出现多个僵尸进程:`[openclaw-gatewa] <defunct>`
**典型错误日志**
```
[22:19:39] [tower] OpenClaw crashed: exit status 1
[22:24:52] [tower] OpenClaw crashed: signal: killed
[22:27:33] Gateway failed to start: gateway already running (pid 2005)
[22:27:33] If the gateway is supervised, stop it with: openclaw gateway stop
```
**根本原因**
- Tower 作为容器 PID 1 进程,不是专业的 init 进程
- 缺少子进程回收reaping机制导致僵尸进程未被清理
- 僵尸进程占用锁文件和端口18789阻塞新 Gateway 启动
- 容器 PID 1 是 `/usr/local/bin/tower`,没有僵尸进程回收能力
**诊断命令**
```bash
# 查看 PID 1 进程
ssh lazycat "lzc-docker exec 5f3bf33e090b ps -p 1 -o pid,ppid,cmd"
# 查看僵尸进程详情
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep 'defunct'"
# 检查端口占用
ssh lazycat "lzc-docker exec 5f3bf33e090b netstat -tlnp | grep 18789"
# 查看进程树
ssh lazycat "lzc-docker exec 5f3bf33e090b ps auxf | head -30"
```
**永久解决方案(已实施)**
使用 **tini** 作为容器 PID 1自动回收僵尸进程。
```bash
# 1. 在容器中安装 tini专业 init 进程)
ssh lazycat "lzc-docker exec 5f3bf33e090b bash -c 'apt-get update -qq && apt-get install -y tini'"
# 2. 修改 entrypoint 使用 tini 包装 tower
ssh lazycat "lzc-docker exec 5f3bf33e090b sed -i 's|exec /usr/local/bin/tower|exec /usr/bin/tini -- /usr/local/bin/tower|g' /usr/local/bin/clawdbot-entrypoint.sh"
# 3. 验证修改
ssh lazycat "lzc-docker exec 5f3bf33e090b grep 'exec.*tower' /usr/local/bin/clawdbot-entrypoint.sh"
# 应该看到: exec /usr/bin/tini -- /usr/local/bin/tower ...
# 4. 重启容器使修改生效
ssh lazycat "lzc-docker restart 5f3bf33e090b"
# 5. 验证 tini 已成为 PID 1
ssh lazycat "lzc-docker exec 5f3bf33e090b ps -p 1 -o pid,ppid,cmd"
# 输出应显示: PID 1 -> /usr/bin/tini -- /usr/local/bin/tower ...
# 6. 检查进程树
ssh lazycat "lzc-docker exec 5f3bf33e090b ps auxf | head -15"
```
**修复后的进程架构**
```
PID 1: /usr/bin/tini (专业 init 进程,自动回收僵尸进程)
└─ PID 58: tower
└─ PID 64: openclaw
└─ PID 72: openclaw-gateway
```
**修复效果**
- ✅ Tini 作为 PID 1自动回收所有僵尸进程
- ✅ 僵尸进程数量从 5+ 个降至 1-2 个(健康水平)
- ✅ Tower 稳定运行,不再反复崩溃
- ✅ Gateway 启动正常,无锁文件冲突
- ✅ RPC probe 持续显示 ok
**注意事项**
- ⚠️ 当前修改在运行容器内,**容器重建后需重新应用**
- 💡 建议向镜像维护者(懒猫云)提交 PR在 Dockerfile 中添加 tini
- 📌 每次从镜像重新创建容器时,需要重新执行上述步骤 1-4
**镜像级永久修复**(建议提交给懒猫云):
在 OpenClaw 镜像的 Dockerfile 中添加:
```dockerfile
# 安装 tini
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
# 或使用更轻量的安装方式
ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini /usr/bin/tini
RUN chmod +x /usr/bin/tini
# 在 entrypoint 脚本中使用 tini 包装(已在当前镜像的 entrypoint 中修改)
```
### 僵尸进程过多
**现象**
- 僵尸进程数超过 50 个
- Gateway 响应变慢
- 容器内存占用升高
**诊断**
```bash
# 查看僵尸进程详情
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep 'Z'"
# 统计僵尸进程数量
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep -c 'Z'"
# 查看僵尸进程父进程
ssh lazycat "lzc-docker exec 5f3bf33e090b ps -eo pid,ppid,stat,comm | grep 'Z'"
```
**解决方案**
```bash
# 方案 1重启容器推荐
ssh lazycat "lzc-docker restart 5f3bf33e090b"
# 方案 2手动清理 Gateway 进程
ssh lazycat "lzc-docker exec 5f3bf33e090b pkill -9 -f 'openclaw-gateway'"
ssh lazycat "lzc-docker exec 5f3bf33e090b openclaw gateway run &"
# 验证清理效果
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep -c 'Z'"
```
### Gateway 无响应
**现象**
- `RPC probe: failed` 或超时
- TUI 连接失败:`gateway not connected`
- Dashboard 无法访问
**诊断**
```bash
# 检查 Gateway 进程
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep gateway"
# 检查端口监听
ssh lazycat "lzc-docker exec 5f3bf33e090b netstat -tlnp | grep 18789"
# 查看 Gateway 日志
ssh lazycat "lzc-docker exec 5f3bf33e090b tail -100 /tmp/openclaw/openclaw-*.log"
# 测试本地连接
ssh lazycat "lzc-docker exec 5f3bf33e090b curl -I http://127.0.0.1:18789"
```
**解决方案**
```bash
# 1. 杀死所有 Gateway 进程
ssh lazycat "lzc-docker exec 5f3bf33e090b pkill -9 -f 'openclaw-gateway'"
# 2. 启动新的 Gateway
ssh lazycat "lzc-docker exec -d 5f3bf33e090b bash -c 'openclaw gateway run > /tmp/gateway.log 2>&1 &'"
# 3. 等待启动
sleep 5
# 4. 验证状态
ssh lazycat "lzc-docker exec 5f3bf33e090b openclaw gateway status"
```
### 多个 OpenClaw TUI 实例运行(已修复 2026-02-16
**现象**
- 每次启动 OpenClaw TUI 前需要 `pkill -9 openclaw`
- 启动失败或端口冲突
- 多个 `openclaw-tui` 进程在后台运行
- 容器资源占用异常高
**诊断**
```bash
# 检查运行中的 OpenClaw 进程
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep openclaw"
# 通常会看到多个 openclaw-tui 实例:
# PID 3041 - openclaw-tui (pts/0)
# PID 6338 - openclaw-tui (pts/1)
# PID 7223 - openclaw-tui (pts/2)
# 检查端口占用
ssh lazycat "lzc-docker exec 5f3bf33e090b netstat -tlnp | grep 18789"
```
**根本原因**
- 每次运行 `openclaw tui` 都启动新进程
- 退出 TUI 时进程没有完全清理
- 多个实例同时运行导致资源竞争
**永久解决方案(已实施)**
**1. 创建自动清理脚本**(容器中):
```bash
# 在容器中创建 /usr/local/bin/openclaw-clean
ssh lazycat "lzc-docker exec 5f3bf33e090b bash -c \"cat > /usr/local/bin/openclaw-clean << 'EOF'
#!/bin/bash
# OpenClaw 清理并重启脚本
# 清理所有非 Tower 管理的 openclaw 进程
echo '🧹 清理旧的 OpenClaw 进程...'
pkill -9 -f 'openclaw-tui' || true
pkill -9 -f 'openclaw tui' || true
# 等待进程完全退出
sleep 1
# 检查剩余进程
REMAINING=\\\$(ps aux | grep -E 'openclaw' | grep -v 'openclaw-gateway' | grep -v 'tower' | grep -v 'grep' | wc -l)
if [ \\\$REMAINING -gt 0 ]; then
echo '⚠️ 警告:还有 '\\\$REMAINING' 个 openclaw 进程'
else
echo '✅ 清理完成'
fi
# 启动 OpenClaw TUI
echo ''
echo '🦞 启动 OpenClaw TUI...'
exec openclaw tui
EOF
chmod +x /usr/local/bin/openclaw-clean\""
```
**2. 更新本地 openclaw-tui 脚本**
修改 `~/bin/openclaw-tui` 的最后一行:
```bash
# 修改前
ssh -t lazycat "lzc-docker exec -it 5f3bf33e090b bash -c 'openclaw tui'"
# 修改后
ssh -t lazycat "lzc-docker exec -it 5f3bf33e090b openclaw-clean"
```
**修复效果**
- ✅ 每次启动自动清理旧进程
- ✅ 不再需要手动 `pkill -9 openclaw`
- ✅ 避免多实例导致的资源浪费
- ✅ 一条命令 `openclaw-tui` 搞定所有
**使用方法**
```bash
# 以前(需要手动清理)
ssh lazycat "lzc-docker exec 5f3bf33e090b bash -c 'pkill -9 openclaw && openclaw tui'"
# 现在(自动清理)
openclaw-tui # 一条命令搞定!
```
**手动清理**(如果需要):
```bash
# 清理所有 openclaw-tui 进程
ssh lazycat "lzc-docker exec 5f3bf33e090b pkill -9 -f 'openclaw-tui'"
# 验证清理结果
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep openclaw | grep -v tower | grep -v openclaw-gateway"
```
### 容器内存不足
**现象**
- 容器内存使用率超过 90%
- OOM (Out of Memory) 错误
- 进程被 killed
**诊断**
```bash
# 检查内存使用
ssh lazycat "lzc-docker stats --no-stream 5f3bf33e090b"
# 查看内存限制
ssh lazycat "lzc-docker inspect 5f3bf33e090b --format='{{.HostConfig.Memory}}'"
# 查看系统总内存
ssh lazycat "free -h"
```
**解决方案**
```bash
# 调整内存限制(如果当前限制过低)
# 注意:懒猫算力仓已设置为 30GB一般不需要调整
# 如确需调整,使用以下命令
ssh lazycat "lzc-docker update 5f3bf33e090b --memory=30g --memory-swap=32g"
# 重启容器使配置生效
ssh lazycat "lzc-docker restart 5f3bf33e090b"
```
### 自动重启失败
**现象**
- systemd timer 未触发
- 重启脚本执行失败
- 日志显示 `lzc-docker: command not found`
**诊断**
```bash
# 检查 timer 状态
ssh lazycat "systemctl status openclaw-restart.timer"
# 检查 service 状态
ssh lazycat "systemctl status openclaw-restart.service"
# 查看 service 日志
ssh lazycat "journalctl -u openclaw-restart.service -n 50"
# 查看脚本日志
ssh lazycat "tail -50 /var/log/openclaw-restart.log"
# 手动测试脚本
ssh lazycat "bash -x /root/restart-openclaw.sh"
```
**解决方案**
问题通常是脚本中 `lzc-docker` 命令找不到PATH 问题)。
```bash
# 确认 lzc-docker 路径
ssh lazycat "which lzc-docker"
# 输出: /lzcsys/bin/lzc-docker
# 确保脚本使用完整路径
ssh lazycat "grep 'lzc-docker' /root/restart-openclaw.sh"
# 应该看到: /lzcsys/bin/lzc-docker
# 如果使用的是相对路径,需要修改
ssh lazycat "sed -i 's|lzc-docker|/lzcsys/bin/lzc-docker|g' /root/restart-openclaw.sh"
ssh lazycat "sed -i 's|lzc-docker|/lzcsys/bin/lzc-docker|g' /root/monitor-openclaw-zombies.sh"
# 重新加载 systemd 配置
ssh lazycat "systemctl daemon-reload"
# 测试执行
ssh lazycat "/root/restart-openclaw.sh"
```
---
## 最佳实践
### 1. 定期健康检查
建议每天执行一次全面健康检查:
```bash
#!/bin/bash
# OpenClaw 健康检查脚本
echo "🔍 OpenClaw 健康检查 - $(date)"
echo "================================"
# 容器状态
echo -e "\n📦 容器状态:"
ssh lazycat "lzc-docker ps --filter id=5f3bf33e090b --format 'Status: {{.Status}}'"
# PID 1 进程
echo -e "\n🏗 PID 1 进程:"
ssh lazycat "lzc-docker exec 5f3bf33e090b ps -p 1 -o pid,ppid,cmd"
# 僵尸进程数
echo -e "\n👻 僵尸进程:"
ZOMBIE_COUNT=$(ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep -c 'Z'")
echo "僵尸进程数: $ZOMBIE_COUNT"
if [ $ZOMBIE_COUNT -gt 10 ]; then
echo "⚠️ 警告:僵尸进程较多,建议重启容器"
fi
# 资源使用
echo -e "\n💾 资源使用:"
ssh lazycat "lzc-docker stats --no-stream 5f3bf33e090b"
# Gateway 状态
echo -e "\n🔌 Gateway 状态:"
ssh lazycat "lzc-docker exec 5f3bf33e090b openclaw gateway status | grep 'RPC probe'"
# 系统负载
echo -e "\n📊 系统负载:"
ssh lazycat "uptime"
echo -e "\n================================"
echo "✅ 健康检查完成"
```
### 2. 日志管理
```bash
# 查看最近的错误日志
ssh lazycat "lzc-docker logs 5f3bf33e090b --since 1h 2>&1 | grep -i error"
# 查看 Tower 崩溃日志
ssh lazycat "lzc-docker logs 5f3bf33e090b 2>&1 | grep -i 'crashed\|failed'"
# 查看 OpenClaw 应用日志
ssh lazycat "lzc-docker exec 5f3bf33e090b tail -100 /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log"
# 清理旧日志保留最近7天
ssh lazycat "lzc-docker exec 5f3bf33e090b find /tmp/openclaw -name '*.log' -mtime +7 -delete"
```
### 3. 备份与恢复
```bash
# 备份 OpenClaw 配置
ssh lazycat "lzc-docker exec 5f3bf33e090b tar czf /tmp/openclaw-config-backup-$(date +%Y%m%d).tar.gz -C /home/node/.openclaw ."
# 下载备份到本地
scp lazycat:/tmp/openclaw-config-backup-*.tar.gz ~/backups/
# 恢复配置
scp ~/backups/openclaw-config-backup-*.tar.gz lazycat:/tmp/
ssh lazycat "lzc-docker exec 5f3bf33e090b tar xzf /tmp/openclaw-config-backup-*.tar.gz -C /home/node/.openclaw"
ssh lazycat "lzc-docker restart 5f3bf33e090b"
```
### 4. 监控告警
建议设置以下监控指标:
- **僵尸进程数** > 50触发告警自动重启已实现
- **内存使用率** > 90%:触发告警
- **Gateway 离线时间** > 5分钟触发告警
- **容器重启次数** > 3次/天:触发告警
### 5. 容器重建后的恢复清单
如果容器被重新创建(从镜像),需要重新应用以下修复:
```bash
# 1. 安装 tini
ssh lazycat "lzc-docker exec 5f3bf33e090b bash -c 'apt-get update -qq && apt-get install -y tini'"
# 2. 修改 entrypoint
ssh lazycat "lzc-docker exec 5f3bf33e090b sed -i 's|exec /usr/local/bin/tower|exec /usr/bin/tini -- /usr/local/bin/tower|g' /usr/local/bin/clawdbot-entrypoint.sh"
# 3. 重启容器
ssh lazycat "lzc-docker restart 5f3bf33e090b"
# 4. 验证
ssh lazycat "lzc-docker exec 5f3bf33e090b ps -p 1 -o cmd | grep tini"
```
---
## 附录
### 相关文档
- OpenClaw 官方文档https://docs.openclaw.ai/
- 故障排查指南https://docs.openclaw.ai/troubleshooting
- Tini 项目https://github.com/krallin/tini
### 联系信息
- 懒猫云支持support@lazycat.cloud
- OpenClaw 社区https://community.openclaw.ai/
### 版本历史
- 2026-02-16
- 创建文档,记录 Tower 崩溃修复经验(使用 tini
- 添加多 TUI 实例问题和 openclaw-clean 解决方案
- 2026-02-15实施僵尸进程监控和自动重启
- 2026-02-14调整容器资源限制为接近系统上限
---
## 快速参考
### 常用命令速查
```bash
# 连接 OpenClaw TUI
openclaw-tui
# 查看容器状态
ssh lazycat "lzc-docker ps | grep openclaw"
# 重启容器
ssh lazycat "lzc-docker restart 5f3bf33e090b"
# 查看僵尸进程数
ssh lazycat "lzc-docker exec 5f3bf33e090b ps aux | grep -c 'Z'"
# 检查 Gateway 状态
ssh lazycat "lzc-docker exec 5f3bf33e090b openclaw gateway status | grep 'RPC probe'"
# 查看资源使用
ssh lazycat "lzc-docker stats --no-stream 5f3bf33e090b"
# 查看定时任务
ssh lazycat "systemctl list-timers | grep openclaw"
# 清理多余的 OpenClaw TUI 进程
ssh lazycat "lzc-docker exec 5f3bf33e090b pkill -9 -f 'openclaw-tui'"
# 启动 OpenClaw自动清理旧进程
ssh lazycat "lzc-docker exec 5f3bf33e090b openclaw-clean"
# 全面健康检查
ssh lazycat "echo '=== 容器 ===' && lzc-docker ps | grep openclaw && echo '' && echo '=== 僵尸进程 ===' && lzc-docker exec 5f3bf33e090b ps aux | grep -c 'Z' && echo '' && echo '=== Gateway ===' && lzc-docker exec 5f3bf33e090b openclaw gateway status | grep 'RPC probe'"
```
### 故障处理速查
| 问题 | 快速解决 |
|------|----------|
| Tower 反复崩溃 | 参考"Tower 反复崩溃"章节,安装 tini |
| 多个 TUI 实例 | 使用 `openclaw-tui`(自动清理)或手动 `pkill -9 -f openclaw-tui` |
| Gateway 无响应 | `ssh lazycat "lzc-docker restart 5f3bf33e090b"` |
| 僵尸进程过多 | `ssh lazycat "lzc-docker restart 5f3bf33e090b"` |
| 内存不足 | 检查资源限制,重启容器 |
| 自动重启失败 | 检查脚本是否使用完整路径 `/lzcsys/bin/lzc-docker` |

View File

@@ -0,0 +1,8 @@
{
"name": "openclaw-plugin",
"description": "OpenClaw (龙虾) - Remote AI compute orchestration system. Dispatches requirements to Claude Code instances on remote machines, monitors execution, and aggregates results.",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,342 @@
---
name: openclaw
description: OpenClaw (龙虾) - Remote AI compute orchestration system. Dispatches requirements to Claude Code instances on remote machines, monitors execution, and aggregates results.
---
# OpenClaw - Remote AI Compute Orchestration
OpenClaw (龙虾) 是一个分布式 AI 算力调度系统,用于将需求/任务分发到不同机器上的 Claude Code 实例执行。
## 核心概念
### 1. Ticket (工单)
每个需求 (Requirement) 或任务 (Task) 都是一个 Ticket包含:
- **ID**: REQ-YYYYMMDD-XXXX 或 Task ID
- **Description**: 任务描述
- **Context**: 项目路径、技术栈、依赖
- **Priority**: 优先级 (high/medium/low)
- **Target**: 目标计算节点
### 2. Compute Node (算力节点)
运行 Claude Code 的开发机器:
- **Melbourne** (coolbuy-dev): 主开发机,全栈开发
- **Shanghai** (dev-box): 测试环境
- **Beijing** (lazycat): OpenClaw 本身所在的容器
### 3. Dispatcher (调度器)
OpenClaw 核心组件,负责:
- 接收 Ticket
- 选择合适的 Compute Node
- 生成 Claude Code 命令
- 通过 SSH 执行
- 监控进度
- 汇总结果
## 使用方法
### 基本调用
```bash
# 方式 1: 通过需求 ID
/openclaw dispatch REQ-20260216-0001
# 方式 2: 直接指定任务
/openclaw run "实现用户批量导入功能" --node melbourne --project new-ai-proj
# 方式 3: 使用 Swarm 模式
/openclaw swarm REQ-20260216-0001 --node melbourne
```
### 指定计算节点
```bash
# 自动选择 (根据负载和项目位置)
/openclaw dispatch REQ-20260216-0001 --auto
# 指定节点
/openclaw dispatch REQ-20260216-0001 --node melbourne
# 并行执行 (多个节点)
/openclaw dispatch REQ-20260216-0001 --nodes melbourne,shanghai --parallel
```
### 监控执行状态
```bash
# 查看所有运行中的任务
/openclaw status
# 查看特定任务的日志
/openclaw logs REQ-20260216-0001
# 实时追踪任务进度
/openclaw tail REQ-20260216-0001
```
## 工作流程
### 完整示例: 从 Feishu 到执行
**步骤 1: 用户在飞书发起请求**
```
@龙虾 执行 REQ-20260216-0001
```
**步骤 2: Feishu Bot 解析命令**
- 提取需求 ID: REQ-20260216-0001
- 调用 OpenClaw skill
**步骤 3: OpenClaw 查询需求详情**
```bash
# 使用 ai-proj-prod MCP 获取需求信息
mcp__ai-proj-prod__find_requirement(displayId="REQ-20260216-0001")
```
**步骤 4: OpenClaw 生成执行计划**
```json
{
"ticket_id": "REQ-20260216-0001",
"title": "AI Ticket 演示: 验证算力调度系统",
"type": "swarm",
"target_node": "melbourne",
"command": "/swarm start '实现 AI Ticket 从创建到算力调度的完整流程'",
"context": {
"project": "/Users/coolbuy-dev/coding/new-ai-proj",
"requirement_id": "REQ-20260216-0001",
"priority": "high"
}
}
```
**步骤 5: SSH 执行命令**
```bash
ssh coolbuy-dev@melbourne \
"cd /Users/coolbuy-dev/coding/new-ai-proj && \
/opt/homebrew/bin/claude --dangerously-skip-permissions \
-p '/swarm start 实现 AI Ticket 从创建到算力调度的完整流程 \
--context requirement_id=REQ-20260216-0001'"
```
**步骤 6: Claude Code 执行 Swarm 工作流**
- Architect Agent: 分析需求,设计方案
- Coder Agent: 实现代码
- Tester Agent: 编写和运行测试
- Reviewer Agent: 代码审查
- Deployer Agent: 部署到环境
**步骤 7: OpenClaw 监控和汇总**
- 实时获取执行日志
- 解析 Agent 切换和进度
- 检测完成或失败
**步骤 8: 报告结果**
```
【执行完成】REQ-20260216-0001
✅ 状态: 成功
⏱️ 耗时: 25 分钟
📝 修改: 12 个文件
✅ 测试: 32 个用例全部通过
🚀 部署: staging 环境已更新
详情: https://ai-proj.pipexerp.com/requirements/REQ-20260216-0001
```
## 配置文件
### openclaw.yaml
`~/.claude/openclaw/config.yaml` 配置节点信息:
```yaml
nodes:
melbourne:
host: coolbuy-dev-macbook.local
user: coolbuy-dev
ssh_key: ~/.ssh/id_rsa
claude_path: /opt/homebrew/bin/claude
working_dirs:
- /Users/coolbuy-dev/coding/new-ai-proj
- /Users/coolbuy-dev/coding/coolbuy-paas
capabilities:
- go
- vue
- react
- ios
- android
max_concurrent: 3
shanghai:
host: dev-box.pipexerp.com
user: devops
ssh_key: ~/.ssh/id_rsa_ops
claude_path: /usr/local/bin/claude
working_dirs:
- /home/devops/projects
capabilities:
- testing
- deployment
max_concurrent: 2
routing_rules:
# 根据项目路径自动选择节点
- pattern: "*/new-ai-proj/*"
node: melbourne
- pattern: "*/coolbuy-*/*"
node: melbourne
# 根据需求类别路由
- category: testing
node: shanghai
- category: feature
node: melbourne
priority: prefer # 非强制
# 默认节点
default: melbourne
monitoring:
progress_interval: 30s # 每 30 秒检查一次进度
timeout: 2h # 2 小时超时
feishu_notifications: true
feishu_webhook: ${FEISHU_OPENCLAW_WEBHOOK}
```
## 高级功能
### 1. 负载均衡
OpenClaw 自动根据节点负载分配任务:
```bash
# 自动选择最空闲的节点
/openclaw dispatch REQ-20260216-0001 --load-balance
```
### 2. 任务队列
当节点繁忙时,任务进入队列:
```bash
# 查看队列
/openclaw queue
# 输出:
# 队列中的任务:
# 1. [melbourne] REQ-20260216-0002 - 等待中 (前面 1 个任务)
# 2. [shanghai] Task-1234 - 等待中 (前面 0 个任务)
```
### 3. 结果缓存
已执行过的相同任务可以复用结果:
```bash
/openclaw dispatch REQ-20260216-0001 --use-cache
```
### 4. 故障恢复
任务失败时自动重试或切换节点:
```yaml
retry_policy:
max_attempts: 3
backoff: exponential # 1min, 2min, 4min
fallback_node: shanghai # 失败后切换到备用节点
```
## 与其他 Skills 集成
### 与 agent-swarm 集成
```bash
# OpenClaw 调度 Swarm 工作流
/openclaw swarm REQ-20260216-0001 --node melbourne
```
### 与 ai-proj 集成
```bash
# 自动更新需求状态
# pending → in_progress → testing → completed
/openclaw dispatch REQ-20260216-0001 --sync-status
```
### 与 feishu 集成
```bash
# 飞书群接收进度通知
# "【进行中】REQ-20260216-0001 - Coder Agent 正在实现后端 API (35%)"
```
### 与 ops-tools 集成
```bash
# 部署到服务器
/openclaw dispatch REQ-20260216-0001 --deploy staging
```
## 命令速查
| 命令 | 功能 |
|------|------|
| `/openclaw dispatch <req-id>` | 调度需求到节点执行 |
| `/openclaw run "<task>"` | 直接执行任务 |
| `/openclaw swarm <req-id>` | 使用 Swarm 模式执行 |
| `/openclaw status` | 查看所有任务状态 |
| `/openclaw logs <req-id>` | 查看任务日志 |
| `/openclaw tail <req-id>` | 实时追踪任务 |
| `/openclaw queue` | 查看任务队列 |
| `/openclaw nodes` | 列出所有计算节点 |
| `/openclaw cancel <req-id>` | 取消任务 |
## 故障排查
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| SSH 连接失败 | 网络或认证问题 | 检查 `~/.ssh/config` 和密钥权限 |
| Claude 命令未找到 | Claude 未安装或路径错误 | 更新 `openclaw.yaml` 中的 `claude_path` |
| 任务卡住不动 | 超时或节点挂起 | 使用 `/openclaw cancel` 取消并重试 |
| Feishu 通知失败 | Webhook 配置错误 | 检查 `FEISHU_OPENCLAW_WEBHOOK` 环境变量 |
## 安全考虑
1. **SSH 密钥管理**: 使用专用密钥,定期轮换
2. **命令注入防护**: 所有参数都经过严格转义
3. **权限控制**: 限制 Claude Code 的 `--dangerously-skip-permissions` 使用
4. **日志审计**: 所有命令执行都记录在 `~/.claude/openclaw/audit.log`
## 实际应用场景
### 场景 1: 多项目并行开发
```bash
# 同时在不同机器上开发不同项目
/openclaw dispatch REQ-001 --node melbourne --project new-ai-proj &
/openclaw dispatch REQ-002 --node shanghai --project coolbuy-paas &
```
### 场景 2: 测试环境验证
```bash
# 开发在 Melbourne测试在 Shanghai
/openclaw dispatch REQ-003 --node melbourne # 开发
sleep 1h # 等待开发完成
/openclaw dispatch REQ-003 --node shanghai --verify # 测试验证
```
### 场景 3: 紧急 Bug 修复
```bash
# 高优先级任务插队执行
/openclaw dispatch BUG-001 --priority urgent --node melbourne
```
## 未来扩展
- [ ] Web 控制面板: 可视化任务状态和节点负载
- [ ] 分布式追踪: Jaeger/OpenTelemetry 集成
- [ ] 成本统计: 记录每个任务的 token 使用量和费用
- [ ] 智能调度: 基于历史数据预测任务执行时间
- [ ] 多云支持: 支持 AWS/Azure/GCP 上的计算节点
---
**开发者**: AI 项目管理助手
**版本**: 1.0.0
**最后更新**: 2026-02-16

View File

@@ -0,0 +1,8 @@
{
"name": "ops-servers-plugin",
"description": "Plugin for ops-servers",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,262 @@
---
name: ops-servers
description: 企业服务器管理。用于云服务器分组管理、系统监控、备份管理、故障排查。当用户提到云服务器、生产环境、腾讯云、阿里云相关任务时自动激活。
---
# 企业服务器管理 Skill
> 家庭网络设备请使用 `ops-home` Skill
---
## 服务器清单
| 别名 | IP | 用户 | SSH 密钥 | 用途 | 配置 | ISP | 账号 |
|------|-----|------|----------|------|------|-----|------|
| prod-pipexerp | 192.144.137.14 | ubuntu | ~/.ssh/officialWebsite.pem | pipexerp 官网 | 2核2G 40G SSD | 腾讯云 | 北京对丝 |
| prod-metaBI | 192.144.174.87 | ubuntu | ~/.ssh/prod_meta.pem | Metabase BI 分析 | - | 腾讯云 | 北京欢乐宿 |
| moltbot | 124.223.196.74 | root | ~/.ssh/moltbot | Moltbot 服务 | - | 腾讯云 | - |
| lazycat | 100.115.52.119 (haiqing.heiyu.space) | root | 密码认证 (zhiyun2026) | AI/计算节点 | - | - | - |
### SSH 快捷连接
```bash
# pipexerp 官网服务器
ssh prod-pipexerp
# Metabase BI 分析服务器
ssh prod-metaBI
# Moltbot 服务器
ssh moltbot
# Lazycat AI 计算节点密码zhiyun2026
ssh root@haiqing.heiyu.space
# 或使用 IP
ssh root@100.115.52.119
```
---
## 服务器分组架构
采用 **环境 + 服务** 混合分组模式:
### 按环境分组
| 环境 | 前缀 | 用途 |
|------|------|------|
| prod | prod- | 生产环境 |
| staging | stg- | 预发布环境 |
| test | test- | 测试环境 |
| dev | dev- | 开发环境 |
### 按服务分组
| 服务组 | 包含服务 | 说明 |
|--------|----------|------|
| web | Nginx, 前端静态资源 | 负载均衡、静态资源 |
| api | Go/Node 后端服务 | 业务 API |
| db | MySQL, PostgreSQL | 数据库 |
| cache | Redis | 缓存服务 |
---
## 常用运维命令
### 系统状态检查
```bash
# 一键查看系统概况
ssh prod-pipexerp "echo '=== 负载 ===' && uptime && echo && echo '=== 内存 ===' && free -h && echo && echo '=== 磁盘 ===' && df -h"
# 查看 CPU 使用最高的进程
ssh prod-pipexerp "ps aux --sort=-%cpu | head -10"
# 查看内存使用最高的进程
ssh prod-pipexerp "ps aux --sort=-%mem | head -10"
```
### Docker 管理
```bash
# 查看运行中的容器
ssh prod-pipexerp "docker ps"
# 查看所有容器
ssh prod-pipexerp "docker ps -a"
# 查看容器日志
ssh prod-pipexerp "docker logs -f <container_name> --tail 100"
# 重启容器
ssh prod-pipexerp "docker restart <container_name>"
# 清理未使用的资源
ssh prod-pipexerp "docker system prune -af"
```
### 网络检查
```bash
# 查看端口监听
ssh prod-pipexerp "sudo netstat -tlnp"
# 检查防火墙状态
ssh prod-pipexerp "sudo ufw status"
# 测试端口连通性
nc -zv 192.144.137.14 80
nc -zv 192.144.137.14 443
```
### 日志查看
```bash
# Nginx 错误日志
ssh prod-pipexerp "sudo tail -f /var/log/nginx/error.log"
# Nginx 访问日志
ssh prod-pipexerp "sudo tail -f /var/log/nginx/access.log"
# 系统日志
ssh prod-pipexerp "sudo tail -f /var/log/syslog"
```
---
## 批量操作
### 对多台服务器执行命令
```bash
# 定义服务器列表
SERVERS="prod-pipexerp"
# 批量检查状态
for host in $SERVERS; do
echo "=== $host ==="
ssh $host "uptime && free -h | head -2 && df -h / | tail -1"
done
```
### 健康检查脚本
```bash
# 检查所有服务器
for host in prod-pipexerp; do
echo "=== $host ==="
ssh $host "
echo '--- 负载 ---' && uptime
echo '--- 内存 ---' && free -h | head -2
echo '--- 磁盘 ---' && df -h / | tail -1
echo '--- Docker ---' && docker ps --format 'table {{.Names}}\t{{.Status}}' 2>/dev/null || echo 'N/A'
"
done
```
---
## 备份管理
### 备份策略
| 备份类型 | 频率 | 保留时间 | 存储位置 |
|----------|------|----------|----------|
| 数据库全量 | 每日 02:00 | 7 天 | /backup/mysql/ |
| 配置文件 | 每日 03:00 | 30 天 | /backup/configs/ |
| 上传文件 | 每日 04:00 | 30 天 | /backup/uploads/ |
### 手动备份
```bash
# 备份 Nginx 配置
ssh prod-pipexerp "sudo tar -czf /tmp/nginx-\$(date +%Y%m%d).tar.gz /etc/nginx/"
# 下载备份到本地
scp prod-pipexerp:/tmp/nginx-*.tar.gz ./backups/
```
### 备份清理
```bash
# 清理 7 天前的备份
ssh prod-pipexerp "sudo find /backup/ -name '*.tar.gz' -mtime +7 -delete"
```
---
## 故障排查
### 常见问题
1. **服务无响应**
```bash
ssh prod-pipexerp "sudo systemctl status nginx"
ssh prod-pipexerp "sudo journalctl -u nginx --since '10 minutes ago'"
```
2. **磁盘空间不足**
```bash
ssh prod-pipexerp "df -h && sudo du -sh /* 2>/dev/null | sort -h | tail -10"
# 清理 Docker
ssh prod-pipexerp "docker system prune -af"
# 清理日志
ssh prod-pipexerp "sudo journalctl --vacuum-size=500M"
```
3. **内存不足**
```bash
ssh prod-pipexerp "free -h && ps aux --sort=-%mem | head -10"
```
4. **网站无法访问**
```bash
# 检查 Nginx
ssh prod-pipexerp "sudo systemctl status nginx"
# 检查端口
ssh prod-pipexerp "sudo netstat -tlnp | grep ':80\|:443'"
# 测试本地访问
ssh prod-pipexerp "curl -I http://localhost"
```
5. **SSL 证书问题**
```bash
# 检查证书到期时间
ssh prod-pipexerp "sudo openssl x509 -in /etc/nginx/ssl/cert.pem -noout -dates"
```
---
## 账号管理
### 系统用户
| 用户名 | 用途 | 权限 |
|--------|------|------|
| ubuntu | 默认管理用户 | sudo |
| deploy | 部署用户 | 部署相关 |
### 创建部署用户
```bash
# 创建用户
ssh prod-pipexerp "sudo useradd -m -s /bin/bash deploy"
# 配置 SSH 密钥
ssh prod-pipexerp "sudo mkdir -p /home/deploy/.ssh && sudo chmod 700 /home/deploy/.ssh"
ssh prod-pipexerp "sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/ && sudo chown -R deploy:deploy /home/deploy/.ssh"
# 配置 sudo 权限(无密码 docker 和 systemctl
ssh prod-pipexerp "echo 'deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *, /usr/bin/docker *' | sudo tee /etc/sudoers.d/deploy"
```
---
## 安全注意事项
- SSH 密钥文件权限必须是 600: `chmod 600 ~/.ssh/*.pem`
- 使用 `sudo` 执行需要 root 权限的命令
- 敏感操作前先确认服务器和目标
- 生产环境操作需要二次确认
- 定期更新系统和软件包

View File

@@ -0,0 +1,8 @@
{
"name": "ops-tools-plugin",
"description": "Plugin for ops-tools",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View File

@@ -0,0 +1,246 @@
# AI-Proj 部署指南
**创建时间**: 2026-01-29 11:50:00 CST
**父技能**: ops-tools
## 环境概览
| 环境 | 服务器 | 域名 | 镜像标签 |
|------|--------|------|----------|
| 生产 | tools_ai_proj (152.136.104.251) | https://ai.pipexerp.com | `latest` |
| 测试 | singapore (43.134.28.147) | http://staging.ai.pipexerp.com | `test` |
## 镜像信息
| 服务 | 镜像 |
|------|------|
| 后端 | `saltthing123/ai-proj-backend` |
| 前端 | `saltthing123/ai-proj-frontend` |
## 标准部署流程
### 部署到测试环境
```bash
cd /path/to/new-ai-proj
# 构建后端 test 镜像
docker buildx build --platform linux/amd64 -f backend/Dockerfile --target production \
-t saltthing123/ai-proj-backend:test --push backend/
# 构建前端 test 镜像
docker buildx build --platform linux/amd64 -f frontend/Dockerfile.prod --target production \
--build-arg REACT_APP_API_URL=https://staging.ai.pipexerp.com/api/v1 \
--build-arg REACT_APP_API_BASE_URL=https://staging.ai.pipexerp.com/api/v1 \
--build-arg REACT_APP_ENV=staging \
-t saltthing123/ai-proj-frontend:test --push frontend/
# 部署到测试服务器
ssh singapore "cd /opt/ai-project-staging && sudo docker-compose pull && sudo docker-compose up -d"
```
### 部署到生产环境
```bash
cd /path/to/new-ai-proj
# 构建后端 latest 镜像
docker buildx build --platform linux/amd64 -f backend/Dockerfile --target production \
-t saltthing123/ai-proj-backend:latest --push backend/
# 构建前端 latest 镜像
docker buildx build --platform linux/amd64 -f frontend/Dockerfile.prod --target production \
--build-arg REACT_APP_API_URL=https://ai.pipexerp.com/api/v1 \
--build-arg REACT_APP_API_BASE_URL=https://ai.pipexerp.com/api/v1 \
--build-arg REACT_APP_ENV=production \
-t saltthing123/ai-proj-frontend:latest --push frontend/
# 部署到生产服务器
ssh tools_ai_proj "cd /opt/ai-project && \
docker compose -f deploy/tencent-cloud/docker-compose.dockerhub.yml pull && \
docker compose -f deploy/tencent-cloud/docker-compose.dockerhub.yml up -d"
```
## 新加坡服务器 Build备选方案
本地网络慢时,在新加坡服务器构建:
```bash
# 后端
ssh singapore "cd ~/projects/new-ai-proj && git pull && \
docker build --platform linux/amd64 -f backend/Dockerfile --target production \
-t saltthing123/ai-proj-backend:latest ./backend && \
docker push saltthing123/ai-proj-backend:latest"
# 前端(生产)
ssh singapore "cd ~/projects/new-ai-proj && \
docker build --platform linux/amd64 -f frontend/Dockerfile.prod --target production \
--build-arg REACT_APP_API_URL=https://ai.pipexerp.com/api/v1 \
--build-arg REACT_APP_API_BASE_URL=https://ai.pipexerp.com/api/v1 \
--build-arg REACT_APP_ENV=production \
-t saltthing123/ai-proj-frontend:latest ./frontend && \
docker push saltthing123/ai-proj-frontend:latest"
```
## 自动部署Webhook
**状态**: 已启用 (2026-01-16)
```
git push main → Gitea webhook → Jenkins ai-proj → 生产自动部署
```
## 服务管理
```bash
# 查看容器状态
ssh tools_ai_proj "docker ps --format 'table {{.Names}}\t{{.Status}}'"
# 查看日志
ssh tools_ai_proj "docker logs -f ai_backend_prod --tail 100"
# 重启服务
ssh tools_ai_proj "docker restart ai_backend_prod"
# 健康检查
curl -s https://ai.pipexerp.com/api/v1/health | jq .
```
## 测试环境管理
```bash
# 查看状态
ssh singapore "sudo docker-compose -f /opt/ai-project-staging/docker-compose.yml ps"
# 查看日志
ssh singapore "sudo docker logs -f ai_backend_staging --tail 100"
# 健康检查
ssh singapore "curl -s -H 'Host: staging.ai.pipexerp.com' http://127.0.0.1/api/v1/health"
```
## Docker Volumes 配置
**重要**: 数据卷必须标记为 `external: true`
```yaml
volumes:
postgres_prod_data:
external: true
name: ai-project_postgres_prod_data
redis_prod_data:
external: true
name: ai-project_redis_prod_data
```
## 数据库操作
```bash
# 运行迁移
ssh tools_ai_proj "cd /opt/ai-project/backend/migrations && \
for file in \$(ls *.sql | grep -v _down.sql | sort); do \
docker exec -i ai_postgres_prod psql -U ai_prod_user -d ai_project_prod < \"\$file\"; \
done"
# 备份
ssh tools_ai_proj "docker exec ai_postgres_prod pg_dump -U ai_prod_user ai_project_prod > /tmp/backup.sql"
```
## 用户管理
> **重要**: 密码哈希必须使用 bcrypt **cost 12**,这是后端 `utils/password.go` 中的 `DefaultCost` 值。
### 创建用户完整流程
由于 bcrypt 哈希包含 `$` 字符会被 shell 解释,必须使用文件传输方式:
```bash
# 1. 生成密码哈希(在有 Go 环境的机器上执行)
cd /path/to/new-ai-proj/backend
cat > /tmp/genhash.go << 'EOF'
package main
import ("fmt"; "golang.org/x/crypto/bcrypt")
func main() {
hash, _ := bcrypt.GenerateFromPassword([]byte("用户密码"), 12)
fmt.Println(string(hash))
}
EOF
HASH=$(go run /tmp/genhash.go)
echo "Generated hash: $HASH"
# 2. 创建 SQL 文件
cat > /tmp/create_user.sql << EOF
INSERT INTO users (username, email, password_hash, user_type, role, status, created_at, updated_at)
VALUES ('newuser', 'newuser@example.com', '$HASH', 'system', 'admin', 'active', NOW(), NOW());
EOF
# 3. 传输并执行
scp /tmp/create_user.sql tools_ai_proj:/tmp/
ssh tools_ai_proj "docker cp /tmp/create_user.sql ai_postgres_prod:/tmp/ && \
docker exec ai_postgres_prod psql -U ai_prod_user -d ai_project_prod -f /tmp/create_user.sql"
# 4. 验证
curl -s -X POST "https://ai.pipexerp.com/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"newuser","password":"用户密码"}' | jq '.success'
```
### 用户角色
| user_type | role | 权限 |
|-----------|------|------|
| system | admin | 系统管理员 |
| system | user | 系统用户 |
| tenant | admin | 租户管理员 |
| tenant | user | 租户用户 |
### 重置密码
```bash
# 生成新哈希并更新
cd /path/to/new-ai-proj/backend
HASH=$(go run -e 'package main; import ("fmt";"golang.org/x/crypto/bcrypt"); func main() { h,_:=bcrypt.GenerateFromPassword([]byte("新密码"),12); fmt.Println(string(h)) }' 2>/dev/null || cat > /tmp/h.go << 'E'
package main
import ("fmt";"golang.org/x/crypto/bcrypt")
func main() { h,_:=bcrypt.GenerateFromPassword([]byte("新密码"),12); fmt.Println(string(h)) }
E
go run /tmp/h.go)
cat > /tmp/reset.sql << EOF
UPDATE users SET password_hash = '$HASH' WHERE username = 'targetuser';
EOF
scp /tmp/reset.sql tools_ai_proj:/tmp/
ssh tools_ai_proj "docker cp /tmp/reset.sql ai_postgres_prod:/tmp/ && \
docker exec ai_postgres_prod psql -U ai_prod_user -d ai_project_prod -f /tmp/reset.sql"
```
### 常见错误
| 问题 | 原因 | 解决 |
|------|------|------|
| 登录失败 | bcrypt cost 不是 12 | 用 Go 重新生成 cost 12 的哈希 |
| 哈希被截断 | shell 解释了 $ 符号 | 使用文件传输方式 |
### 已创建的系统用户
| 用户名 | 邮箱 | user_type | role | 创建时间 |
|--------|------|-----------|------|----------|
| qiudl | qiudl@zhiyuncai.com | system | admin | - |
| jiaxiang | jiaxiang@joylodging.com | system | admin | 2026-01 |
| haiqing | haiqing@joylodging.com | system | admin | 2026-02 |
> 注意:密码信息不在文档中记录,如需重置请使用上述重置密码流程
## 前端构建注意事项
必须同时设置两个 URL 变量:
- `REACT_APP_API_URL`
- `REACT_APP_API_BASE_URL`
否则会使用 `.env.production` 中的生产 URL。
验证镜像中的 URL
```bash
docker exec <container> sh -c 'grep -oE "https://[a-zA-Z0-9.-]*pipexerp[a-zA-Z0-9./-]*" /usr/share/nginx/html/static/js/main*.js | sort | uniq -c'
```

View File

@@ -0,0 +1,100 @@
# Coolbuy-PaaS 部署指南
**创建时间**: 2026-01-29 11:50:00 CST
**父技能**: ops-tools
## 仓库信息
| 仓库 | 地址 | 说明 |
|------|------|------|
| coolbuy-paas | git@gitea.pipexerp.com:pipexerp/coolbuy-paas.git | 租户业务系统 |
| coolbuy-platform | git@gitea.pipexerp.com:pipexerp/coolbuy-platform.git | 平台管理端 |
| coolbuy-legacy | git@gitea.pipexerp.com:pipexerp/coolbuy-legacy.git | 遗留项目 |
## 镜像信息
| 服务 | 镜像 | Dockerfile |
|------|------|------------|
| Auth | saltthing123/coolbuy-paas-auth | auth-service/Dockerfile |
| Foundation | saltthing123/coolbuy-paas-foundation | foundation-service/Dockerfile |
| ERP | saltthing123/coolbuy-paas-erp | erp-service/Dockerfile |
| Web | saltthing123/coolbuy-paas-web | web/Dockerfile |
## 生产环境
| 项目 | 值 |
|------|-----|
| 服务器 IP | 39.106.88.83 |
| 架构 | AMD64 |
| 部署目录 | /opt/coolbuy-paas |
| Web 端口 | 8888 |
## 部署流程(本地构建 + Jenkins 部署)
### 步骤 1: 本地构建并推送
```bash
cd /path/to/coolbuy-paas
# 构建单个服务AMD64 架构)
docker buildx build --platform linux/amd64 -t saltthing123/coolbuy-paas-web:latest ./web --push
# 构建所有服务
./scripts/build-and-push.sh --push --platform linux/amd64
```
### 步骤 2: 触发 Jenkins 部署
```bash
source ~/.config/devops/credentials.env
# 部署到生产
curl -X POST "$JENKINS_URL/job/coolbuy-paas/buildWithParameters" \
-u "$JENKINS_USER:$JENKINS_TOKEN" \
--data "ACTION=deploy-prod&IMAGE_TAG=latest"
# 部署到测试
curl -X POST "$JENKINS_URL/job/coolbuy-paas/buildWithParameters" \
-u "$JENKINS_USER:$JENKINS_TOKEN" \
--data "ACTION=deploy-test&IMAGE_TAG=latest"
```
### 一键部署命令
```bash
cd /path/to/coolbuy-paas && \
docker buildx build --platform linux/amd64 -t saltthing123/coolbuy-paas-web:latest ./web --push && \
source ~/.config/devops/credentials.env && \
curl -X POST "$JENKINS_URL/job/coolbuy-paas/buildWithParameters" \
-u "$JENKINS_USER:$JENKINS_TOKEN" \
--data "ACTION=deploy-prod&IMAGE_TAG=latest"
```
## 查看构建状态
```bash
source ~/.config/devops/credentials.env
# 构建状态
curl -s "$JENKINS_URL/job/coolbuy-paas/lastBuild/api/json" \
-u "$JENKINS_USER:$JENKINS_TOKEN" | jq '.result, .building'
# 构建日志
curl -s "$JENKINS_URL/job/coolbuy-paas/lastBuild/consoleText" \
-u "$JENKINS_USER:$JENKINS_TOKEN" | tail -50
```
## 检查镜像架构
```bash
# 本地镜像
docker inspect saltthing123/coolbuy-paas-web:latest | grep Architecture
# DockerHub 镜像
docker manifest inspect saltthing123/coolbuy-paas-web:latest | grep architecture
```
## 重要提醒
- 生产服务器为 AMD64 架构,必须使用 `--platform linux/amd64`
- 禁止在 Jenkins 服务器构建镜像,所有镜像本地构建后推送到 DockerHub

View File

@@ -0,0 +1,837 @@
# 数据库备份与恢复 Skill
**父技能**: ops-tools
**适用范围**: 全局(所有项目数据库)
**创建时间**: 2026-01-15 07:30:00 ACDT
**最后更新**: 2026-02-02
---
## 技能概述
全局数据库备份技能,适用于所有项目的 PostgreSQL 数据库。涵盖迁移前备份、自动备份、数据恢复和灾难恢复策略。
**核心原则**
- ⚠️ **任何数据库迁移操作前必须先备份**
- 保留策略:最近 7 天 + 每月 1 个永久备份
- 存储位置:服务器本地 `/backup/` 目录
---
## 数据库清单
| 数据库 | 服务器 | 容器 | 用途 | 备份路径 |
|--------|--------|------|------|----------|
| ai_project_prod | tools_ai_proj | ai_postgres_prod | AI-Proj 生产 | /backup/ai-project/database/ |
| ai_project_staging | singapore | ai_postgres_staging | AI-Proj 测试 | /backup/ai-project-staging/ |
| coolbuy_prod | coolbuy-dev | postgres | Coolbuy 3.0 | /backup/coolbuy/ |
---
## ⚡ 迁移前快速备份(必读)
> **重要**:执行任何 `UPDATE`、`DELETE`、`ALTER`、数据迁移等操作前,**必须先执行备份**。
### 一键备份命令
```bash
# AI-Proj 生产数据库 - 迁移前备份
ssh tools_ai_proj 'REASON="pre_migration_$(date +%Y%m%d_%H%M%S)" && \
docker exec ai_postgres_prod pg_dump -U ai_prod_user -Fc ai_project_prod \
> /backup/ai-project/database/ai_project_${REASON}.dump && \
echo "✓ 备份完成: /backup/ai-project/database/ai_project_${REASON}.dump"'
# AI-Proj 测试数据库 - 迁移前备份
ssh singapore 'REASON="pre_migration_$(date +%Y%m%d_%H%M%S)" && \
sudo docker exec ai_postgres_staging pg_dump -U ai_staging_user -Fc ai_project_staging \
> /backup/ai-project-staging/ai_project_staging_${REASON}.dump && \
echo "✓ 备份完成"'
```
### 带原因的备份(推荐)
```bash
# 指定备份原因,方便追溯
ssh tools_ai_proj 'REASON="migrate_project_165_to_167" && \
docker exec ai_postgres_prod pg_dump -U ai_prod_user -Fc ai_project_prod \
> /backup/ai-project/database/ai_project_$(date +%Y%m%d_%H%M%S)_${REASON}.dump && \
ls -lh /backup/ai-project/database/ | tail -3'
```
### 备份后验证
```bash
# 验证备份文件
ssh tools_ai_proj 'ls -lh /backup/ai-project/database/ | tail -5'
# 检查备份文件大小(应该 > 10MB
ssh tools_ai_proj 'stat --printf="%s bytes\n" /backup/ai-project/database/ai_project_*.dump | tail -1'
```
---
## 快速恢复命令
### 从最新备份恢复
```bash
# 1. 找到最新备份
ssh tools_ai_proj 'ls -lt /backup/ai-project/database/*.dump | head -3'
# 2. 恢复(使用 pg_restore
ssh tools_ai_proj 'BACKUP_FILE="/backup/ai-project/database/ai_project_XXXXXXXX.dump" && \
docker stop ai_backend_prod && \
docker exec ai_postgres_prod pg_restore -U ai_prod_user -d ai_project_prod --clean --if-exists -Fc "$BACKUP_FILE" && \
docker start ai_backend_prod && \
echo "✓ 恢复完成"'
# 3. 验证
curl -s https://ai.pipexerp.com/api/v1/health | jq .
```
### 恢复到特定时间点
```bash
# 列出所有备份,找到目标时间点
ssh tools_ai_proj 'ls -lht /backup/ai-project/database/*.dump'
# 恢复指定备份
ssh tools_ai_proj 'docker exec ai_postgres_prod pg_restore \
-U ai_prod_user -d ai_project_prod --clean --if-exists -Fc \
/backup/ai-project/database/ai_project_20260202_180000_migrate_project_165_to_167.dump'
```
---
## 保留策略
### 策略说明
| 类型 | 保留时间 | 清理规则 |
|------|----------|----------|
| 每日备份 | 7 天 | 超过 7 天自动删除 |
| 月度备份 | 永久 | 每月 1 号的备份永久保留 |
| 迁移前备份 | 30 天 | 带 `pre_migration` 标记的保留 30 天 |
### 自动清理脚本
```bash
# /opt/scripts/cleanup-backups.sh
#!/bin/bash
BACKUP_DIR="/backup/ai-project/database"
# 删除超过 7 天的每日备份(保留月度备份)
find "$BACKUP_DIR" -name "*.dump" -mtime +7 ! -name "*_01_*" -delete
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 ! -name "*_01_*" -delete
# 删除超过 30 天的迁移前备份
find "$BACKUP_DIR" -name "*pre_migration*" -mtime +30 -delete
echo "$(date): Cleanup completed" >> /var/log/backup-cleanup.log
```
### Cron 配置
```cron
# 每天凌晨 3 点清理旧备份
0 3 * * * /opt/scripts/cleanup-backups.sh
```
---
## 快速参考
| 操作 | 命令 |
|------|------|
| 手动执行备份 | `ssh tools_ai_proj "/opt/ai-project/deploy/scripts/backup-database.sh"` |
| 查看本地备份 | `ssh tools_ai_proj "ls -lh /backup/ai-project/database/"` |
| 查看备份日志 | `ssh tools_ai_proj "tail -f /var/log/ai-project-backup.log"` |
| 触发 OSS 同步 | `ssh tools_ai_proj "/opt/ai-project/deploy/scripts/backup-to-oss.sh"` |
| 列出 OSS 备份 | `ssh tools_ai_proj "ossutil ls oss://fnos2026/ai-project/backups/ --config-file ~/.ossutilconfig"` |
| 下载最新备份 | `ssh tools_ai_proj "ossutil cp oss://fnos2026/ai-project/backups/latest.sql.gz /tmp/ --config-file ~/.ossutilconfig"` |
| 验证备份完整性 | `ssh tools_ai_proj "gzip -t /backup/ai-project/database/latest.sql.gz"` |
---
## 备份架构
### 双层备份策略
```
┌─────────────────────────────────────────────────────────┐
│ AI-Proj 生产服务器 │
│ (tools_ai_proj: 152.136.104.251) │
├─────────────────────────────────────────────────────────┤
│ │
│ PostgreSQL 数据库 (ai_postgres_prod) │
│ │ │
│ │ 每天 02:00 (Cron) │
│ ▼ │
│ 本地备份 (/backup/ai-project/database/) │
│ │ - gzip 压缩 │
│ │ - 30 天保留 │
│ │ - 完整性验证 │
│ │ - 符号链接 (latest.sql.gz) │
│ │ │
│ │ 每天 02:30 (Cron) │
│ ▼ │
│ OSS 同步 (backup-to-oss.sh) │
│ │ │
└────────────┼─────────────────────────────────────────────┘
│ 互联网 (623 KB/s)
┌─────────────────────────────────────────────────────────┐
│ 阿里云对象存储 (OSS) │
│ 北京区域 │
├─────────────────────────────────────────────────────────┤
│ Bucket: fnos2026 │
│ 路径: /ai-project/backups/ │
│ │
│ ├── YYYYMMDD/ │
│ │ └── ai_project_YYYYMMDD_HHMMSS.sql.gz │
│ └── latest.sql.gz (最新备份) │
│ │
│ ✅ 异地容灾 (99.9% 可用性) │
│ ✅ 30 天自动清理 │
│ ✅ 成本: ~¥0.25/月 │
└─────────────────────────────────────────────────────────┘
```
### 备份时间表
| 时间 | 操作 | 脚本 | 日志文件 |
|------|------|------|----------|
| 02:00 | 本地数据库备份 | `/opt/ai-project/deploy/scripts/backup-database.sh` | `/var/log/ai-project-backup.log` |
| 02:30 | OSS 异地同步 | `/opt/ai-project/deploy/scripts/backup-to-oss.sh` | `/var/log/ai-project-oss-sync.log` |
---
## 自动备份配置
### 本地备份
**脚本位置**: `/opt/ai-project/deploy/scripts/backup-database.sh`
**功能特性**:
- ✅ PostgreSQL pg_dump 完整备份
- ✅ gzip 压缩
- ✅ 按日期目录组织
- ✅ 30 天自动清理
- ✅ 备份完整性验证
- ✅ 符号链接指向最新备份
**Cron 配置**:
```cron
0 2 * * * /opt/ai-project/deploy/scripts/backup-database.sh >> /var/log/ai-project-backup.log 2>&1
```
**手动执行**:
```bash
ssh tools_ai_proj "/opt/ai-project/deploy/scripts/backup-database.sh"
```
**查看日志**:
```bash
ssh tools_ai_proj "tail -f /var/log/ai-project-backup.log"
```
**备份目录结构**:
```
/backup/ai-project/database/
├── 20260115/
│ ├── ai_project_20260115_020001.sql.gz (13M)
│ └── ai_project_20260115_120000.sql.gz (13M)
├── 20260116/
│ └── ai_project_20260116_020001.sql.gz (13M)
└── latest.sql.gz -> 20260116/ai_project_20260116_020001.sql.gz
```
---
## 阿里云 OSS 异地备份
**配置时间**: 2026-01-15 01:12:00 CST
**首次同步**: 2026-01-15 02:30:01 CST
### OSS 配置信息
| 配置项 | 值 |
|--------|-----|
| Endpoint | oss-cn-beijing.aliyuncs.com |
| Bucket | fnos2026 |
| 存储路径 | oss://fnos2026/ai-project/backups/ |
| 保留策略 | 30 天自动清理 |
| 预计成本 | ~¥0.25/月 |
### 凭据配置
**存储位置**: `~/.config/devops/credentials.env` (权限 600)
```bash
OSS_ENDPOINT="oss-cn-beijing.aliyuncs.com"
OSS_BUCKET="fnos2026"
OSS_ACCESS_KEY_ID="LTAI5tEARCztp3Bj3FUYd9rh"
OSS_ACCESS_KEY_SECRET="RSvwURFo2cgF1krSgeriyrAUIqQyGE"
```
**加载凭据**:
```bash
source ~/.config/devops/credentials.env
```
### ossutil 工具
**版本**: v1.7.15
**安装位置**: `/usr/local/bin/ossutil`
**安装时间**: 2026-01-15 00:45:00 CST
**安装步骤**:
```bash
wget https://gosspublic.alicdn.com/ossutil/1.7.15/ossutil64
sudo mv ossutil64 /usr/local/bin/ossutil
sudo chmod +x /usr/local/bin/ossutil
```
**配置**:
```bash
source ~/.config/devops/credentials.env
ossutil config -e ${OSS_ENDPOINT} \
-i ${OSS_ACCESS_KEY_ID} \
-k ${OSS_ACCESS_KEY_SECRET} \
-L CH \
--config-file ~/.ossutilconfig
```
**测试连接**:
```bash
ossutil ls oss://${OSS_BUCKET}/
```
### 自动同步脚本
**脚本位置**: `/opt/ai-project/deploy/scripts/backup-to-oss.sh`
**功能特性**:
- ✅ 同步当天备份目录到 OSS
- ✅ 上传 latest.sql.gz
- ✅ 自动清理 30 天前的旧备份
- ✅ 备份统计报告
- ✅ 彩色日志输出
**Cron 配置**:
```cron
30 2 * * * /opt/ai-project/deploy/scripts/backup-to-oss.sh >> /var/log/ai-project-oss-sync.log 2>&1
```
**手动执行**:
```bash
ssh tools_ai_proj "/opt/ai-project/deploy/scripts/backup-to-oss.sh"
```
**查看日志**:
```bash
ssh tools_ai_proj "tail -f /var/log/ai-project-oss-sync.log"
```
### 常用 OSS 操作
```bash
# 加载凭据
source ~/.config/devops/credentials.env
# 列出所有备份文件
ssh tools_ai_proj "ossutil ls oss://${OSS_BUCKET}/ai-project/backups/ -r --config-file ~/.ossutilconfig"
# 查看备份统计
ssh tools_ai_proj "ossutil du oss://${OSS_BUCKET}/ai-project/backups/ --config-file ~/.ossutilconfig"
# 下载特定日期的备份
ssh tools_ai_proj "ossutil cp oss://${OSS_BUCKET}/ai-project/backups/20260115/ai_project_20260115_*.sql.gz /tmp/ --config-file ~/.ossutilconfig"
# 下载最新备份
ssh tools_ai_proj "ossutil cp oss://${OSS_BUCKET}/ai-project/backups/latest.sql.gz /tmp/ --config-file ~/.ossutilconfig"
# 查看备份文件详情
ssh tools_ai_proj "ossutil stat oss://${OSS_BUCKET}/ai-project/backups/latest.sql.gz --config-file ~/.ossutilconfig"
# 手动清理特定日期的备份
ssh tools_ai_proj "ossutil rm oss://${OSS_BUCKET}/ai-project/backups/20260101/ -r -f --config-file ~/.ossutilconfig"
```
### 备份验证
```bash
# 验证最新备份是否上传成功
ssh tools_ai_proj "ossutil stat oss://${OSS_BUCKET}/ai-project/backups/latest.sql.gz --config-file ~/.ossutilconfig"
# 下载并测试备份完整性
ssh tools_ai_proj "
ossutil cp oss://${OSS_BUCKET}/ai-project/backups/latest.sql.gz /tmp/test_restore.sql.gz --config-file ~/.ossutilconfig
gzip -t /tmp/test_restore.sql.gz && echo '✓ 备份文件完整' || echo '✗ 备份文件损坏'
rm /tmp/test_restore.sql.gz
"
```
---
## 手动备份
### 完整备份
```bash
# 连接到生产服务器
ssh tools_ai_proj
# 导出数据库
docker exec ai_postgres_prod pg_dump -U ai_prod_user ai_project_prod \
--no-owner --no-acl --clean --if-exists \
> /tmp/ai_project_backup_$(date +%Y%m%d_%H%M%S).sql
# 压缩备份
gzip /tmp/ai_project_backup_*.sql
# 验证备份完整性
gzip -t /tmp/ai_project_backup_*.sql.gz
```
### 下载到本地
**直接下载** (如果网络良好):
```bash
scp tools_ai_proj:/tmp/ai_project_backup_*.sql.gz /tmp/
```
**通过跳板机优化传输** (高延迟环境):
```bash
# 使用新加坡跳板机中转(澳洲 → 新加坡 → 腾讯云)
scp tools_ai_proj:/tmp/ai_project_backup_*.sql.gz singapore:/tmp/
scp singapore:/tmp/ai_project_backup_*.sql.gz /tmp/
# 清理跳板机临时文件
ssh singapore "rm /tmp/ai_project_backup_*.sql.gz"
```
---
## 数据库恢复
### 场景 1: 从 OSS 备份恢复(推荐)
```bash
# 1. 从 OSS 下载最新备份
ssh tools_ai_proj "
source ~/.config/devops/credentials.env
ossutil cp oss://fnos2026/ai-project/backups/latest.sql.gz /tmp/restore.sql.gz --config-file ~/.ossutilconfig -f
"
# 2. 验证文件完整性
ssh tools_ai_proj "gzip -t /tmp/restore.sql.gz"
# 3. 停止后端服务
ssh tools_ai_proj "docker stop ai_backend_prod"
# 4. 恢复数据库
ssh tools_ai_proj "
gunzip -c /tmp/restore.sql.gz | \
docker exec -i ai_postgres_prod psql -U ai_prod_user ai_project_prod
"
# 5. 启动后端服务
ssh tools_ai_proj "docker start ai_backend_prod"
# 6. 验证服务
curl -s https://ai.pipexerp.com/api/v1/health | jq .
# 7. 清理临时文件
ssh tools_ai_proj "rm /tmp/restore.sql.gz"
```
### 场景 2: 从本地备份恢复
```bash
ssh tools_ai_proj
# 停止后端服务
docker stop ai_backend_prod
# 恢复数据库
gunzip -c /backup/ai-project/database/latest.sql.gz | \
docker exec -i ai_postgres_prod psql -U ai_prod_user ai_project_prod
# 启动后端服务
docker start ai_backend_prod
```
### 场景 3: 从本地开发环境恢复到生产(完整重建)
```bash
# 1. 本地导出
pg_dump -U donglinlai ai_project_local \
--no-owner --no-acl --clean --if-exists \
--exclude-table=audit_logs \
> /tmp/ai_project_clean.sql
# 2. 压缩
gzip /tmp/ai_project_clean.sql
# 3. 通过新加坡跳板机传输(优化高延迟)
scp /tmp/ai_project_clean.sql.gz singapore:/tmp/
ssh singapore "scp /tmp/ai_project_clean.sql.gz tools_ai_proj:/tmp/"
# 4. 生产环境恢复
ssh tools_ai_proj
# 停止后端服务
docker stop ai_backend_prod
# 完全重建数据库(避免依赖冲突)
docker exec ai_postgres_prod psql -U ai_prod_user postgres \
-c 'DROP DATABASE IF EXISTS ai_project_prod;'
docker exec ai_postgres_prod psql -U ai_prod_user postgres \
-c 'CREATE DATABASE ai_project_prod OWNER ai_prod_user;'
# 恢复数据
gunzip -c /tmp/ai_project_clean.sql.gz | \
docker exec -i ai_postgres_prod psql -U ai_prod_user ai_project_prod
# 创建可能缺失的表
docker exec -i ai_postgres_prod psql -U ai_prod_user ai_project_prod << 'EOF'
CREATE TABLE IF NOT EXISTS audit_logs (
id SERIAL PRIMARY KEY,
user_id INTEGER,
action VARCHAR(100),
resource_type VARCHAR(100),
resource_id VARCHAR(100),
details TEXT,
ip_address VARCHAR(50),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
EOF
# 运行数据库迁移(如果有)
cd /opt/ai-project/backend/migrations
for file in $(ls *.sql | grep -v _down.sql | sort); do
echo "Running migration: $file"
docker exec -i ai_postgres_prod psql -U ai_prod_user ai_project_prod < "$file"
done
# 启动后端服务
docker start ai_backend_prod
# 清理临时文件
rm /tmp/ai_project_clean.sql.gz
```
---
## 最佳实践
### Docker Volumes 安全配置
**关键规则**: 所有生产数据卷必须配置为 `external: true`
**正确配置** (`deploy/tencent-cloud/docker-compose.dockerhub.yml`):
```yaml
volumes:
postgres_prod_data:
external: true
name: ai-project_postgres_prod_data
redis_prod_data:
external: true
name: ai-project_redis_prod_data
```
**危险配置** (会被 `docker compose down` 删除):
```yaml
volumes:
postgres_prod_data:
redis_prod_data:
```
**验证**:
```bash
ssh tools_ai_proj "docker volume ls | grep ai-project"
# 应该看到:
# ai-project_postgres_prod_data
# ai-project_redis_prod_data
```
### 备份策略
1. **每日自动备份** - 使用 cron 定时任务
2. **双层备份** - 本地 + 阿里云 OSS
3. **定期验证** - 每周测试备份恢复流程
4. **保留策略** - 30 天自动清理
### pg_dump 最佳参数
```bash
# 跨服务器迁移
pg_dump --no-owner --no-acl --clean --if-exists --exclude-table=<problem_table>
# 参数说明:
# --no-owner 不恢复对象所有者(避免用户名冲突)
# --no-acl 不恢复访问权限(避免权限问题)
# --clean 包含 DROP 语句(完全替换)
# --if-exists DROP 前检查存在(避免错误)
# --exclude-table 排除问题表(如有 JSON 格式问题的表)
```
### 数据恢复检查清单
在执行恢复前,务必检查以下项目:
- [ ] 确认备份文件完整性gzip -t 验证)
- [ ] 停止相关应用服务(避免数据不一致)
- [ ] 完全重建数据库DROP + CREATE避免依赖冲突
- [ ] 恢复后创建缺失的表(如被排除的表)
- [ ] 运行数据库迁移(确保表结构最新)
- [ ] 验证数据完整性(检查关键表行数)
- [ ] 测试应用功能(登录、关键业务流程)
- [ ] 清理临时文件备份文件、SQL 文件)
### 网络传输优化
**场景**: 跨地域高延迟环境(如澳洲 → 腾讯云)
**问题**: 直连延迟 370ms+,大文件传输极慢
**方案**: 使用地理位置中间的跳板机
```bash
# 直连(慢): 澳洲 → 腾讯云 (370ms+)
scp file.gz tools_ai_proj:/tmp/
# 优化(快): 澳洲 → 新加坡 → 腾讯云
scp file.gz singapore:/tmp/
ssh singapore "scp /tmp/file.gz tools_ai_proj:/tmp/"
```
**新加坡跳板机信息**:
- 别名: singapore
- IP: 43.134.28.147
- 用户: ubuntu
- SSH Key: ~/.ssh/singpore.pem
---
## 监控与告警
### 每周检查清单
**建议执行频率**: 每周一次
```bash
# 1. 检查本地备份
ssh tools_ai_proj "ls -lh /backup/ai-project/database/$(date +%Y%m%d)/"
# 2. 检查 OSS 备份
ssh tools_ai_proj "ossutil stat oss://fnos2026/ai-project/backups/latest.sql.gz --config-file ~/.ossutilconfig"
# 3. 检查 cron 日志
ssh tools_ai_proj "tail -20 /var/log/ai-project-backup.log"
ssh tools_ai_proj "tail -20 /var/log/ai-project-oss-sync.log"
# 4. 验证备份大小(应该在 10-20M 范围)
ssh tools_ai_proj "du -sh /backup/ai-project/database/$(date +%Y%m%d)/"
# 5. 测试备份完整性
ssh tools_ai_proj "
gzip -t /backup/ai-project/database/latest.sql.gz && \
echo '✓ 本地备份完整' || echo '✗ 本地备份损坏'
"
```
### 备份失败排查
如果备份或同步失败:
1. **检查磁盘空间**:
```bash
ssh tools_ai_proj "df -h"
```
2. **检查 PostgreSQL 容器状态**:
```bash
ssh tools_ai_proj "docker ps | grep postgres"
```
3. **检查 ossutil 配置**:
```bash
ssh tools_ai_proj "cat ~/.ossutilconfig"
```
4. **测试 OSS 连接**:
```bash
ssh tools_ai_proj "ossutil ls oss://fnos2026/ --config-file ~/.ossutilconfig"
```
5. **手动运行脚本查看详细错误**:
```bash
ssh tools_ai_proj "/opt/ai-project/deploy/scripts/backup-database.sh"
ssh tools_ai_proj "/opt/ai-project/deploy/scripts/backup-to-oss.sh"
```
---
## 成本估算
**基于当前数据量** (13MB/天):
| 项目 | 计算 | 月成本 |
|------|------|--------|
| OSS 存储 | 13MB × 30天 = 390MB × ¥0.12/GB | ¥0.05 |
| OSS 流量 | 13MB × 30天 = 390MB × ¥0.50/GB | ¥0.20 |
| **总计** | | **¥0.25** |
---
## 故障案例
### 2026-01-15: 生产数据库丢失事件
**事件时间**: 2026-01-15 00:00:00 - 00:46:00 CST
**事件**: Jenkins 部署时 `docker compose down` 删除了非 external volumes
**影响**: 生产数据库完全清空,所有用户无法登录
**恢复过程**:
1. 从本地开发环境导出完整数据41 用户、54 项目、4,722 任务)
2. 使用新加坡跳板机优化传输(解决 370ms+ 延迟)
3. 完全重建数据库避免依赖冲突
4. 重置所有管理员密码
**恢复完成时间**: 2026-01-15 00:46:00 CST
**预防措施**:
1. ✅ 所有数据卷标记为 `external: true` (完成时间: 2026-01-15 00:46:00)
2. ✅ Jenkinsfile 添加自动数据库迁移 (完成时间: 2026-01-15 00:46:00)
3. ✅ 临时禁用 webhook 自动部署 (完成时间: 2026-01-15 00:46:00)
4. ✅ 配置自动备份策略(本地 + OSS 双层备份)(完成时间: 2026-01-15 02:30:46)
**详细记录**: 见思源笔记 `devops/运维记录/2026-01-15 AI-Proj生产数据库恢复记录`
---
## 用户管理相关
数据库用户管理(创建用户、重置密码)请参考:
- **ops-tools/SKILL.md** - "AI-Proj 用户管理" 章节
- **ai-proj-deploy.md** - "用户管理" 章节
**关键注意事项**
- 密码哈希使用 bcrypt **cost 12**(后端 `utils/password.go` 的 `DefaultCost`
- 由于 `$` 字符问题SQL 必须通过文件传输方式执行
---
## 相关资源
- **父技能**: ops-tools/skill.md
- **备份脚本**: `/opt/ai-project/deploy/scripts/backup-database.sh`
- **OSS 同步脚本**: `/opt/ai-project/deploy/scripts/backup-to-oss.sh`
- **凭据文件**: `~/.config/devops/credentials.env`
- **SiYuan 笔记**: `devops/运维记录/2026-01-15 AI-Proj生产数据库恢复记录`
---
---
## 数据库迁移标准流程
> **强制要求**:任何数据库迁移操作必须遵循以下流程。
### 迁移检查清单
```
┌─────────────────────────────────────────────────────────────┐
│ 数据库迁移标准流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 迁移前备份 ⬅️ 必须 │
│ ssh tools_ai_proj 'docker exec ai_postgres_prod \ │
│ pg_dump -U ai_prod_user -Fc ai_project_prod \ │
│ > /backup/ai-project/database/pre_migration.dump' │
│ │
│ 2. 验证备份文件 │
│ ssh tools_ai_proj 'ls -lh /backup/.../pre_migration.dump'│
│ │
│ 3. 记录迁移前状态 │
│ SELECT COUNT(*) FROM <table>; │
│ │
│ 4. 执行迁移(使用事务) │
│ BEGIN; ... COMMIT; │
│ │
│ 5. 验证迁移结果 │
│ SELECT COUNT(*) FROM <table>; │
│ │
│ 6. 如有问题,恢复备份 │
│ pg_restore -Fc pre_migration.dump │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 迁移 SQL 模板
```sql
-- ============================================
-- 迁移脚本模板
-- 执行前请先备份!
-- ============================================
BEGIN;
-- 迁移前统计
\echo '=== 迁移前统计 ==='
SELECT 'table_name' as info, COUNT(*) as count FROM table_name WHERE condition;
-- 执行迁移
\echo '=== 执行迁移 ==='
UPDATE table_name SET column = new_value WHERE condition;
-- 迁移后统计
\echo '=== 迁移后统计 ==='
SELECT 'table_name' as info, COUNT(*) as count FROM table_name WHERE condition;
-- 确认无误后提交
COMMIT;
\echo '=== 迁移完成 ==='
```
### 迁移失败回滚
```bash
# 1. 停止后端服务
ssh tools_ai_proj 'docker stop ai_backend_prod'
# 2. 恢复备份
ssh tools_ai_proj 'docker exec ai_postgres_prod pg_restore \
-U ai_prod_user -d ai_project_prod --clean --if-exists -Fc \
/backup/ai-project/database/ai_project_XXXXXXXX_pre_migration.dump'
# 3. 启动后端服务
ssh tools_ai_proj 'docker start ai_backend_prod'
# 4. 验证服务
curl -s https://ai.pipexerp.com/api/v1/health | jq .
```
---
## 版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| 2.0 | 2026-02-02 | 升级为全局技能新增迁移前备份流程、多数据库支持、7天+月度保留策略、快速恢复命令 |
| 1.0 | 2026-01-15 | 初始版本AI-Proj 备份与 OSS 同步 |
---
**文档创建时间**: 2026-01-15 07:30:00 ACDT
**最后更新时间**: 2026-02-02
**文档状态**: ✅ 正常运行

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# 部署状态检查脚本
# 用法: ./deploy-check.sh [ai-proj|pipeXerp]
set -e
TOOLS_SERVER="root@101.200.136.200"
TOOLS_KEY="~/.ssh/tools.pem"
JOB_NAME="${1:-ai-proj}"
echo "======================================"
echo "Jenkins Job: $JOB_NAME"
echo "======================================"
ssh -i $TOOLS_KEY -o ConnectTimeout=5 $TOOLS_SERVER << EOF
echo "--- 最近 5 次构建 ---"
ls -lt /var/lib/jenkins/jobs/$JOB_NAME/builds/ 2>/dev/null | head -6
echo ""
echo "--- 最近成功构建 ---"
if [ -L /var/lib/jenkins/jobs/$JOB_NAME/builds/lastSuccessfulBuild ]; then
BUILD_NUM=\$(readlink /var/lib/jenkins/jobs/$JOB_NAME/builds/lastSuccessfulBuild)
echo "Build #\$BUILD_NUM"
if [ -f "/var/lib/jenkins/jobs/$JOB_NAME/builds/\$BUILD_NUM/log" ]; then
echo "构建时间: \$(stat -c %y /var/lib/jenkins/jobs/$JOB_NAME/builds/\$BUILD_NUM/log 2>/dev/null || stat -f %Sm /var/lib/jenkins/jobs/$JOB_NAME/builds/\$BUILD_NUM/log)"
fi
else
echo "无成功构建记录"
fi
echo ""
echo "--- 最近失败构建 ---"
if [ -L /var/lib/jenkins/jobs/$JOB_NAME/builds/lastFailedBuild ]; then
BUILD_NUM=\$(readlink /var/lib/jenkins/jobs/$JOB_NAME/builds/lastFailedBuild)
echo "Build #\$BUILD_NUM"
echo "错误日志(最后 20 行):"
tail -20 /var/lib/jenkins/jobs/$JOB_NAME/builds/\$BUILD_NUM/log 2>/dev/null || echo "无法读取日志"
else
echo "无失败构建记录"
fi
EOF

Some files were not shown because too many files have changed in this diff Show More