增加2个系统级别的skill

This commit is contained in:
朱潮 2026-01-16 23:05:30 +08:00
parent 90117b41fe
commit 723b249e42
9 changed files with 3299 additions and 59 deletions

View File

@ -1,11 +1,5 @@
{extra_prompt}
<env>
Working directory: {agent_dir_path}
Current User: {user_identifier}
Current Time: {datetime}
</env>
### Current Working Directory
The filesystem backend is currently operating in: `{agent_dir_path}`
@ -75,3 +69,10 @@ Break down complex tasks into stages. For each stage, only load the correspondin
2. ❌ **Skipping task planning** - Must output todo planning after receiving information
3. ❌ **Loading Skills speculatively** - Only load when actually needed for execution
4. ❌ **Loading multiple Skills simultaneously** - Only load one Skill at a time for current phase
## System Information
<env>
Working directory: {agent_dir_path}
Current User: {user_identifier}
Current Time: {datetime}
</env>

View File

@ -13,5 +13,7 @@
**Image Handling**: The content returned by the `rag_retrieve` tool may include images. Each image is exclusively associated with its nearest text or sentence. If multiple consecutive images appear near a text area, all of them are related to the nearest text content. Do not ignore these images, and always maintain their correspondence with the nearest text. Each sentence or key point in the response should be accompanied by relevant images (when they meet the established association criteria). Avoid placing all images at the end of the response.
## System Information
- **Current User**: {user_identifier}
- **Current Time**: {datetime}
<env>
Current User: {user_identifier}
Current Time: {datetime}
</env>

909
public/bot-manager.html Normal file
View File

@ -0,0 +1,909 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bot Manager</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<style>
:root {
--primary: #2563EB;
--primary-hover: #1D4ED8;
--secondary: #3B82F6;
--background: #F8FAFC;
--surface: #FFFFFF;
--text: #1E293B;
--text-muted: #64748B;
--border: #E2E8F0;
--success: #10B981;
--warning: #F59E0B;
--error: #EF4444;
--glass-bg: rgba(255, 255, 255, 0.8);
--glass-blur: 12px;
}
.dark {
--primary: #3B82F6;
--primary-hover: #60A5FA;
--secondary: #60A5FA;
--background: #0F172A;
--surface: #1E293B;
--text: #F1F5F9;
--text-muted: #94A3B8;
--border: #334155;
--glass-bg: rgba(30, 41, 59, 0.8);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Open Sans', sans-serif;
background: var(--background);
color: var(--text);
min-height: 100vh;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* ===== Header ===== */
.header {
height: 64px;
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
position: sticky;
top: 0;
z-index: 10;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.header-logo {
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.header-title {
font-family: 'Poppins', sans-serif;
font-size: 18px;
font-weight: 600;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.header-btn {
width: 38px;
height: 38px;
border-radius: 10px;
border: none;
background: transparent;
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
}
.header-btn:hover {
background: var(--background);
}
.header-btn.primary {
background: var(--primary);
color: white;
padding: 0 16px;
width: auto;
gap: 8px;
}
.header-btn.primary:hover {
background: var(--primary-hover);
}
/* ===== Main Content ===== */
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 32px 24px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.page-title {
font-family: 'Poppins', sans-serif;
font-size: 24px;
font-weight: 600;
}
.page-subtitle {
font-size: 14px;
color: var(--text-muted);
margin-top: 4px;
}
/* ===== Bot Grid ===== */
.bot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
.bot-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.bot-card:hover {
border-color: var(--primary);
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.1);
transform: translateY(-2px);
}
.bot-card-header {
display: flex;
align-items: flex-start;
gap: 14px;
margin-bottom: 14px;
}
.bot-card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
.bot-card-info {
flex: 1;
min-width: 0;
}
.bot-card-name {
font-family: 'Poppins', sans-serif;
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.bot-card-id {
font-size: 12px;
color: var(--text-muted);
font-family: 'Monaco', 'Consolas', monospace;
background: var(--background);
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
}
.bot-card-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: var(--text-muted);
}
.bot-card-meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.bot-card-actions {
display: flex;
gap: 8px;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--border);
}
.bot-card-action {
flex: 1;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: transparent;
color: var(--text);
font-size: 13px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.15s ease;
}
.bot-card-action:hover {
background: var(--background);
border-color: var(--primary);
}
.bot-card-action.delete:hover {
background: rgba(239, 68, 68, 0.1);
border-color: var(--error);
color: var(--error);
}
/* ===== Empty State ===== */
.empty-state {
text-align: center;
padding: 80px 20px;
}
.empty-state-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
border-radius: 20px;
background: var(--background);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.empty-state-title {
font-family: 'Poppins', sans-serif;
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.empty-state-subtitle {
font-size: 14px;
color: var(--text-muted);
margin-bottom: 24px;
}
/* ===== Modal ===== */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--surface);
border-radius: 16px;
width: 90%;
max-width: 480px;
max-height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
}
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
font-family: 'Poppins', sans-serif;
font-size: 18px;
font-weight: 600;
}
.modal-close {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
background: var(--background);
color: var(--text);
}
.modal-body {
padding: 24px;
}
.form-group {
margin-bottom: 18px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 8px;
}
.form-input {
width: 100%;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 10px;
font-size: 14px;
background: var(--background);
color: var(--text);
outline: none;
transition: border-color 0.15s ease;
}
.form-input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-hint {
font-size: 12px;
color: var(--text-muted);
margin-top: 6px;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-secondary {
background: var(--background);
border: 1px solid var(--border);
color: var(--text);
}
.btn-secondary:hover {
border-color: var(--primary);
}
.btn-primary {
background: var(--primary);
border: none;
color: white;
}
.btn-primary:hover {
background: var(--primary-hover);
}
/* ===== Delete Confirmation Modal ===== */
.delete-confirm-content {
text-align: center;
padding: 20px 0;
}
.delete-confirm-icon {
width: 56px;
height: 56px;
margin: 0 auto 16px;
border-radius: 50%;
background: rgba(239, 68, 68, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--error);
}
.delete-confirm-title {
font-family: 'Poppins', sans-serif;
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.delete-confirm-text {
font-size: 14px;
color: var(--text-muted);
}
/* ===== Scrollbar ===== */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* ===== Responsive ===== */
@media (max-width: 768px) {
.header {
padding: 0 16px;
}
.main-content {
padding: 20px 16px;
}
.bot-grid {
grid-template-columns: 1fr;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.header-btn.primary span {
display: none;
}
}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-left">
<div class="header-logo">
<i data-lucide="bot" style="width: 20px; height: 20px;"></i>
</div>
<div>
<div class="header-title">Bot Manager</div>
</div>
</div>
<div class="header-right">
<button class="header-btn" id="theme-toggle" title="切换主题">
<i data-lucide="moon" style="width: 18px; height: 18px;"></i>
</button>
<button class="header-btn primary" id="add-bot-btn">
<i data-lucide="plus" style="width: 16px; height: 16px;"></i>
<span>新建 Bot</span>
</button>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<div class="page-header">
<div>
<h1 class="page-title">我的 Bots</h1>
<p class="page-subtitle">管理您的 AI 聊天机器人配置</p>
</div>
</div>
<!-- Bot Grid -->
<div class="bot-grid" id="bot-grid">
<!-- 动态生成 -->
</div>
</main>
<!-- Add/Edit Bot Modal -->
<div class="modal-overlay" id="bot-modal">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title" id="bot-modal-title">新建 Bot</h3>
<button class="modal-close" id="bot-modal-close">
<i data-lucide="x" style="width: 18px; height: 18px;"></i>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label" for="bot-name">Bot 名称 *</label>
<input type="text" id="bot-name" class="form-input" placeholder="例如:客服助手、销售顾问">
</div>
<div class="form-group">
<label class="form-label" for="bot-id-input">Bot ID *</label>
<input type="text" id="bot-id-input" class="form-input" placeholder="例如test 或 UUID">
<p class="form-hint">这是用于 API 调用的唯一标识符</p>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="bot-modal-cancel">取消</button>
<button class="btn btn-primary" id="bot-modal-save">保存</button>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal-overlay" id="delete-modal">
<div class="modal" style="max-width: 400px;">
<div class="modal-body">
<div class="delete-confirm-content">
<div class="delete-confirm-icon">
<i data-lucide="alert-triangle" style="width: 28px; height: 28px;"></i>
</div>
<h3 class="delete-confirm-title">确认删除</h3>
<p class="delete-confirm-text">确定要删除这个 Bot 吗?此操作无法撤销。</p>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="delete-modal-cancel">取消</button>
<button class="btn btn-primary" id="delete-modal-confirm" style="background: var(--error);">删除</button>
</div>
</div>
</div>
<script>
// Initialize Lucide icons
lucide.createIcons();
class BotManager {
constructor() {
this.elements = {
themeToggle: document.getElementById('theme-toggle'),
addBotBtn: document.getElementById('add-bot-btn'),
botGrid: document.getElementById('bot-grid'),
// Bot Modal
botModal: document.getElementById('bot-modal'),
botModalTitle: document.getElementById('bot-modal-title'),
botModalClose: document.getElementById('bot-modal-close'),
botModalCancel: document.getElementById('bot-modal-cancel'),
botModalSave: document.getElementById('bot-modal-save'),
botNameInput: document.getElementById('bot-name'),
botIdInput: document.getElementById('bot-id-input'),
// Delete Modal
deleteModal: document.getElementById('delete-modal'),
deleteModalCancel: document.getElementById('delete-modal-cancel'),
deleteModalConfirm: document.getElementById('delete-modal-confirm')
};
this.bots = [];
this.editingBotId = null;
this.deletingBotId = null;
this.initializeEventListeners();
this.loadTheme();
this.loadBots();
}
initializeEventListeners() {
// Theme toggle
this.elements.themeToggle.addEventListener('click', () => this.toggleTheme());
// Add bot
this.elements.addBotBtn.addEventListener('click', () => this.openBotModal());
// Bot modal
this.elements.botModalClose.addEventListener('click', () => this.closeBotModal());
this.elements.botModalCancel.addEventListener('click', () => this.closeBotModal());
this.elements.botModalSave.addEventListener('click', () => this.saveBot());
this.elements.botModal.addEventListener('click', (e) => {
if (e.target === this.elements.botModal) this.closeBotModal();
});
// Delete modal
this.elements.deleteModalCancel.addEventListener('click', () => this.closeDeleteModal());
this.elements.deleteModalConfirm.addEventListener('click', () => this.confirmDelete());
}
loadTheme() {
const theme = localStorage.getItem('theme') || 'light';
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
this.updateThemeIcon(theme === 'dark');
}
toggleTheme() {
document.documentElement.classList.toggle('dark');
const isDark = document.documentElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
this.updateThemeIcon(isDark);
}
updateThemeIcon(isDark) {
const icon = this.elements.themeToggle.querySelector('i');
if (!icon) return;
icon.setAttribute('data-lucide', isDark ? 'sun' : 'moon');
lucide.createIcons();
}
loadBots() {
const stored = localStorage.getItem('bot-list');
if (stored) {
try {
this.bots = JSON.parse(stored);
} catch (e) {
this.bots = [];
}
}
this.renderBots();
}
saveBots() {
localStorage.setItem('bot-list', JSON.stringify(this.bots));
}
renderBots() {
const grid = this.elements.botGrid;
if (this.bots.length === 0) {
grid.innerHTML = `
<div class="empty-state" style="grid-column: 1 / -1;">
<div class="empty-state-icon">
<i data-lucide="bot" style="width: 40px; height: 40px;"></i>
</div>
<h2 class="empty-state-title">暂无 Bots</h2>
<p class="empty-state-subtitle">点击上方按钮创建您的第一个 Bot</p>
</div>
`;
lucide.createIcons();
return;
}
grid.innerHTML = '';
this.bots.forEach(bot => {
const card = this.createBotCard(bot);
grid.appendChild(card);
});
lucide.createIcons();
}
createBotCard(bot) {
const card = document.createElement('div');
card.className = 'bot-card';
const formatDate = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric' });
};
card.innerHTML = `
<div class="bot-card-header">
<div class="bot-card-icon">
<i data-lucide="bot" style="width: 24px; height: 24px;"></i>
</div>
<div class="bot-card-info">
<div class="bot-card-name">${this.escapeHtml(bot.name)}</div>
<span class="bot-card-id">${this.escapeHtml(bot.botId)}</span>
</div>
</div>
<div class="bot-card-meta">
<span class="bot-card-meta-item">
<i data-lucide="calendar" style="width: 12px; height: 12px;"></i>
创建于 ${formatDate(bot.createdAt)}
</span>
<span class="bot-card-meta-item">
<i data-lucide="clock" style="width: 12px; height: 12px;"></i>
更新于 ${formatDate(bot.updatedAt)}
</span>
</div>
<div class="bot-card-actions">
<button class="bot-card-action" data-action="open" data-bot-id="${bot.id}">
<i data-lucide="external-link" style="width: 14px; height: 14px;"></i>
打开
</button>
<button class="bot-card-action" data-action="edit" data-bot-id="${bot.id}">
<i data-lucide="pencil" style="width: 14px; height: 14px;"></i>
编辑
</button>
<button class="bot-card-action delete" data-action="delete" data-bot-id="${bot.id}">
<i data-lucide="trash-2" style="width: 14px; height: 14px;"></i>
删除
</button>
</div>
`;
// Event listeners
card.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
const botId = e.target.closest('[data-bot-id]')?.dataset.botId;
if (action === 'open' && botId) {
this.openBotChat(botId);
} else if (action === 'edit' && botId) {
e.stopPropagation();
this.openEditModal(botId);
} else if (action === 'delete' && botId) {
e.stopPropagation();
this.openDeleteModal(botId);
}
});
return card;
}
openBotChat(botInternalId) {
const bot = this.bots.find(b => b.id === botInternalId);
if (bot) {
// 保存当前选中的 bot ID
sessionStorage.setItem('current-bot-id', bot.id);
// 跳转到聊天页面
window.location.href = 'index.html';
}
}
openBotModal() {
this.editingBotId = null;
this.elements.botModalTitle.textContent = '新建 Bot';
this.elements.botNameInput.value = '';
this.elements.botIdInput.value = '';
this.elements.botModal.classList.add('active');
this.elements.botNameInput.focus();
}
openEditModal(botInternalId) {
const bot = this.bots.find(b => b.id === botInternalId);
if (!bot) return;
this.editingBotId = botInternalId;
this.elements.botModalTitle.textContent = '编辑 Bot';
this.elements.botNameInput.value = bot.name;
this.elements.botIdInput.value = bot.botId;
this.elements.botModal.classList.add('active');
this.elements.botNameInput.focus();
}
closeBotModal() {
this.elements.botModal.classList.remove('active');
this.editingBotId = null;
}
saveBot() {
const name = this.elements.botNameInput.value.trim();
const botId = this.elements.botIdInput.value.trim();
if (!name) {
alert('请输入 Bot 名称');
return;
}
if (!botId) {
alert('请输入 Bot ID');
return;
}
// 检查 Bot ID 是否重复
const existingBot = this.bots.find(b => b.botId === botId && b.id !== this.editingBotId);
if (existingBot) {
alert('Bot ID 已存在,请使用其他 ID');
return;
}
const now = Date.now();
if (this.editingBotId) {
// 编辑现有 bot
const bot = this.bots.find(b => b.id === this.editingBotId);
if (bot) {
bot.name = name;
bot.botId = botId;
bot.updatedAt = now;
}
} else {
// 新建 bot
const newBot = {
id: this.generateUUID(),
name: name,
botId: botId,
createdAt: now,
updatedAt: now
};
this.bots.unshift(newBot);
}
this.saveBots();
this.renderBots();
this.closeBotModal();
}
openDeleteModal(botInternalId) {
this.deletingBotId = botInternalId;
this.elements.deleteModal.classList.add('active');
}
closeDeleteModal() {
this.elements.deleteModal.classList.remove('active');
this.deletingBotId = null;
}
confirmDelete() {
if (!this.deletingBotId) return;
const bot = this.bots.find(b => b.id === this.deletingBotId);
if (bot) {
// 删除 bot 的设置数据
localStorage.removeItem(`bot-settings-${bot.botId}`);
}
this.bots = this.bots.filter(b => b.id !== this.deletingBotId);
this.saveBots();
this.renderBots();
this.closeDeleteModal();
}
generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize app
document.addEventListener('DOMContentLoaded', () => {
new BotManager();
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

1070
public/model-manager.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -76,7 +76,7 @@ async def enhanced_generate_stream_response(
if isinstance(msg, AIMessageChunk):
# 处理工具调用
if msg.tool_call_chunks:
if msg.tool_call_chunks and config.tool_response:
message_tag = "TOOL_CALL"
for tool_call_chunk in msg.tool_call_chunks:
if tool_call_chunk["name"]:
@ -222,7 +222,7 @@ async def create_agent_and_generate_response(
meta_message_tag = msg.additional_kwargs.get("message_tag", "ANSWER")
output_text = msg.text.replace("````","").replace("````","") if meta_message_tag == "THINK" else msg.text
response_text += f"[{meta_message_tag}]\n"+output_text+ "\n"
if len(msg.tool_calls)>0:
if len(msg.tool_calls)>0 and config.tool_response:
response_text += "".join([f"[TOOL_CALL] {tool['name']}\n{json.dumps(tool["args"]) if isinstance(tool["args"],dict) else tool["args"]}\n" for tool in msg.tool_calls])
elif isinstance(msg,ToolMessage) and config.tool_response:
response_text += f"[TOOL_RESPONSE] {msg.name}\n{msg.text}\n"

View File

@ -83,6 +83,45 @@ async def validate_upload_file_size(file: UploadFile) -> int:
return file_size
def detect_zip_has_top_level_dirs(zip_path: str) -> bool:
"""检测 zip 文件是否包含顶级目录(而非直接包含文件)
Args:
zip_path: zip 文件路径
Returns:
bool: 如果 zip 包含顶级目录则返回 True
"""
try:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
# 获取所有顶级路径(第一层目录/文件)
top_level_paths = set()
for name in zip_ref.namelist():
# 跳过空目录项(以 / 结尾的空路径)
if not name or name == '/':
continue
# 提取顶级路径(第一层)
parts = name.split('/')
if parts[0]: # 忽略空字符串
top_level_paths.add(parts[0])
logger.info(f"Zip top-level paths: {top_level_paths}")
# 检查是否有目录(目录项以 / 结尾,或路径中包含 /
for path in top_level_paths:
# 如果路径中包含 /,说明是目录
# 或者检查 namelist 中是否有以该路径/ 开头的项
for full_name in zip_ref.namelist():
if full_name.startswith(f"{path}/"):
return True
return False
except Exception as e:
logger.warning(f"Error detecting zip structure: {e}")
return False
async def safe_extract_zip(zip_path: str, extract_dir: str) -> None:
"""安全地解压 zip 文件,防止 ZipSlip 和 zip 炸弹攻击
@ -380,12 +419,8 @@ async def upload_skill(file: UploadFile = File(...), bot_id: Optional[str] = For
folder_name = name_without_ext
# 创建上传目录
# 创建上传目录(先保存 zip 文件)
upload_dir = os.path.join("projects", "uploads", bot_id, "skill_zip")
extract_target = os.path.join("projects", "uploads", bot_id, "skills", folder_name)
# 使用线程池避免阻塞
await asyncio.to_thread(os.makedirs, extract_target, exist_ok=True)
await asyncio.to_thread(os.makedirs, upload_dir, exist_ok=True)
# 保存zip文件路径
@ -395,6 +430,25 @@ async def upload_skill(file: UploadFile = File(...), bot_id: Optional[str] = For
await save_upload_file_async(file, file_path)
logger.info(f"Saved zip file: {file_path}")
# 检测 zip 文件结构:是否包含顶级目录
has_top_level_dirs = await asyncio.to_thread(
detect_zip_has_top_level_dirs, file_path
)
logger.info(f"Zip contains top-level directories: {has_top_level_dirs}")
# 根据检测结果决定解压目标目录
if has_top_level_dirs:
# zip 包含目录(如 a-skill/, b-skill/),解压到 skills/ 目录
extract_target = os.path.join("projects", "uploads", bot_id, "skills")
logger.info(f"Detected directories in zip, extracting to: {extract_target}")
else:
# zip 直接包含文件,解压到 skills/{folder_name}/ 目录
extract_target = os.path.join("projects", "uploads", bot_id, "skills", folder_name)
logger.info(f"No directories in zip, extracting to: {extract_target}")
# 使用线程池避免阻塞
await asyncio.to_thread(os.makedirs, extract_target, exist_ok=True)
# P1-001, P1-005: 安全解压(防止 ZipSlip 和 zip 炸弹)
await safe_extract_zip(file_path, extract_target)
logger.info(f"Extracted to: {extract_target}")

View File

@ -0,0 +1,247 @@
---
name: excel-analysis
description: Analyze Excel spreadsheets, create pivot tables, generate charts, and perform data analysis. Use when analyzing Excel files, spreadsheets, tabular data, or .xlsx files.
---
# Excel Analysis
## Quick start
Read Excel files with pandas:
```python
import pandas as pd
# Read Excel file
df = pd.read_excel("data.xlsx", sheet_name="Sheet1")
# Display first few rows
print(df.head())
# Basic statistics
print(df.describe())
```
## Reading multiple sheets
Process all sheets in a workbook:
```python
import pandas as pd
# Read all sheets
excel_file = pd.ExcelFile("workbook.xlsx")
for sheet_name in excel_file.sheet_names:
df = pd.read_excel(excel_file, sheet_name=sheet_name)
print(f"\n{sheet_name}:")
print(df.head())
```
## Data analysis
Perform common analysis tasks:
```python
import pandas as pd
df = pd.read_excel("sales.xlsx")
# Group by and aggregate
sales_by_region = df.groupby("region")["sales"].sum()
print(sales_by_region)
# Filter data
high_sales = df[df["sales"] > 10000]
# Calculate metrics
df["profit_margin"] = (df["revenue"] - df["cost"]) / df["revenue"]
# Sort by column
df_sorted = df.sort_values("sales", ascending=False)
```
## Creating Excel files
Write data to Excel with formatting:
```python
import pandas as pd
df = pd.DataFrame({
"Product": ["A", "B", "C"],
"Sales": [100, 200, 150],
"Profit": [20, 40, 30]
})
# Write to Excel
writer = pd.ExcelWriter("output.xlsx", engine="openpyxl")
df.to_excel(writer, sheet_name="Sales", index=False)
# Get worksheet for formatting
worksheet = writer.sheets["Sales"]
# Auto-adjust column widths
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
worksheet.column_dimensions[column_letter].width = max_length + 2
writer.close()
```
## Pivot tables
Create pivot tables programmatically:
```python
import pandas as pd
df = pd.read_excel("sales_data.xlsx")
# Create pivot table
pivot = pd.pivot_table(
df,
values="sales",
index="region",
columns="product",
aggfunc="sum",
fill_value=0
)
print(pivot)
# Save pivot table
pivot.to_excel("pivot_report.xlsx")
```
## Charts and visualization
Generate charts from Excel data:
```python
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_excel("data.xlsx")
# Create bar chart
df.plot(x="category", y="value", kind="bar")
plt.title("Sales by Category")
plt.xlabel("Category")
plt.ylabel("Sales")
plt.tight_layout()
plt.savefig("chart.png")
# Create pie chart
df.set_index("category")["value"].plot(kind="pie", autopct="%1.1f%%")
plt.title("Market Share")
plt.ylabel("")
plt.savefig("pie_chart.png")
```
## Data cleaning
Clean and prepare Excel data:
```python
import pandas as pd
df = pd.read_excel("messy_data.xlsx")
# Remove duplicates
df = df.drop_duplicates()
# Handle missing values
df = df.fillna(0) # or df.dropna()
# Remove whitespace
df["name"] = df["name"].str.strip()
# Convert data types
df["date"] = pd.to_datetime(df["date"])
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
# Save cleaned data
df.to_excel("cleaned_data.xlsx", index=False)
```
## Merging and joining
Combine multiple Excel files:
```python
import pandas as pd
# Read multiple files
df1 = pd.read_excel("sales_q1.xlsx")
df2 = pd.read_excel("sales_q2.xlsx")
# Concatenate vertically
combined = pd.concat([df1, df2], ignore_index=True)
# Merge on common column
customers = pd.read_excel("customers.xlsx")
sales = pd.read_excel("sales.xlsx")
merged = pd.merge(sales, customers, on="customer_id", how="left")
merged.to_excel("merged_data.xlsx", index=False)
```
## Advanced formatting
Apply conditional formatting and styles:
```python
import pandas as pd
from openpyxl import load_workbook
from openpyxl.styles import PatternFill, Font
# Create Excel file
df = pd.DataFrame({
"Product": ["A", "B", "C"],
"Sales": [100, 200, 150]
})
df.to_excel("formatted.xlsx", index=False)
# Load workbook for formatting
wb = load_workbook("formatted.xlsx")
ws = wb.active
# Apply conditional formatting
red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid")
green_fill = PatternFill(start_color="00FF00", end_color="00FF00", fill_type="solid")
for row in range(2, len(df) + 2):
cell = ws[f"B{row}"]
if cell.value < 150:
cell.fill = red_fill
else:
cell.fill = green_fill
# Bold headers
for cell in ws[1]:
cell.font = Font(bold=True)
wb.save("formatted.xlsx")
```
## Performance tips
- Use `read_excel` with `usecols` to read specific columns only
- Use `chunksize` for very large files
- Consider using `engine='openpyxl'` or `engine='xlrd'` based on file type
- Use `dtype` parameter to specify column types for faster reading
## Available packages
- **pandas** - Data analysis and manipulation (primary)
- **openpyxl** - Excel file creation and formatting
- **xlrd** - Reading older .xls files
- **xlsxwriter** - Advanced Excel writing capabilities
- **matplotlib** - Chart generation

View File

@ -0,0 +1,231 @@
---
name: managing-scripts
description: Manages shared scripts repository for reusable data analysis tools. Check scripts/README.md before writing, design generalized scripts with parameters, and keep documentation in sync.
---
# Managing Scripts
管理可复用的数据分析脚本资源库,通过通用化设计最大化脚本复用价值。
## Quick Start
编写数据分析脚本时的通用化流程:
1. 读取 `./scripts/README.md` 检查是否有可复用的现成脚本
2. 如有合适脚本,优先复用
3. 如需编写新脚本,**设计通用化方案**而非解决单一问题
4. 保存到 `./scripts/` 并更新 README
## Instructions
### 使用前检查
当用户请求任何数据处理/分析任务时:
1. 检查 `./scripts/README.md` 是否存在
2. 查找可处理**此类问题**的脚本(非完全匹配即可)
3. 现有脚本可通过参数调整满足需求时,优先复用
4. 告知用户使用的是现成脚本
### 编写通用化脚本
核心原则:**解决一类问题,而非单一问题**
#### 1. 识别问题模式
在编写脚本前,分析当前请求属于哪类通用模式:
| 问题类型 | 通用模式 | 可参数化项 |
|---------|---------|-----------|
| 数据转换 | 格式A → 格式B | 输入文件、输出格式、字段映射 |
| 数据分析 | 统计/聚合/可视化 | 数据源、分析维度、输出类型 |
| 数据清洗 | 去重/填充/过滤 | 规则配置、阈值参数 |
| 文件操作 | 批量处理文件 | 文件路径、匹配模式、操作类型 |
#### 2. 参数化设计
将硬编码值改为可配置参数:
```python
# ❌ 不通用:硬编码特定字段
def analyze_sales():
df = pd.read_excel("sales_data.xlsx")
result = df.groupby("region")["amount"].sum()
# ✅ 通用化:参数化输入
def analyze_data(input_file, group_by_column, aggregate_column, method="sum"):
"""
通用数据聚合分析
:param input_file: 输入文件路径
:param group_by_column: 分组列名
:param aggregate_column: 聚合列名
:param method: 聚合方法 (sum/mean/count/etc)
"""
df = pd.read_excel(input_file)
return df.groupby(group_by_column)[aggregate_column].agg(method)
```
#### 3. 使用命令行参数
```python
import argparse
def main():
parser = argparse.ArgumentParser(description="通用数据聚合工具")
parser.add_argument("--input", required=True, help="输入文件路径")
parser.add_argument("--output", help="输出文件路径")
parser.add_argument("--group-by", required=True, help="分组列名")
parser.add_argument("--agg-column", required=True, help="聚合列名")
parser.add_argument("--method", default="sum", help="聚合方法")
args = parser.parse_args()
# ... 处理逻辑
```
#### 4. 配置文件支持
复杂逻辑使用配置文件:
```yaml
# config.yaml
transformations:
- column: "date"
action: "parse_format"
params: {"format": "%Y-%m-%d"}
- column: "amount"
action: "fill_na"
params: {"value": 0}
```
### 保存新脚本
脚本验证成功后:
1. 使用**通用性强的命名**
- ✅ `aggregate_data.py`、`convert_format.py`、`clean_dataset.py`
- ❌ `analyze_sales_2024.py`、`fix_import_error.py`
2. 保存到 `./scripts/` 文件夹
3. 在 `./scripts/README.md` 添加说明,包含:
- **通用功能描述**(描述解决的问题类型,非具体业务)
- 使用方法(含所有参数说明)
- 输入/输出格式
- 使用示例
- 依赖要求
### 修改现有脚本
修改 `./scripts/` 下的脚本时:
1. 保持/增强通用性,避免收缩为特定用途
2. 同步更新 README.md 文档
3. 在变更日志中记录修改内容
## Examples
**场景:用户请求分析销售数据**
```
用户:帮我分析这个 Excel 文件,按地区统计销售额
思考过程:
1. 这是一个"数据聚合"问题,属于通用模式
2. 检查 scripts/README.md 是否有聚合工具
3. 如没有,创建通用聚合脚本 aggregate_data.py
4. 支持参数:--input、--group-by、--agg-column、--method
5. 用户调用python scripts/aggregate_data.py \
--input data.xlsx --group-by region --agg-column amount
```
**通用脚本模板示例:**
```python
#!/usr/bin/env python3
"""
通用数据聚合工具
支持按任意列分组,对任意列进行聚合统计
"""
import argparse
import pandas as pd
def aggregate_data(input_file, group_by, agg_column, method="sum", output=None):
"""通用聚合函数"""
# 根据文件扩展名选择读取方法
ext = input_file.split(".")[-1]
read_func = getattr(pd, f"read_{ext}", pd.read_csv)
df = read_func(input_file)
# 执行聚合
result = df.groupby(group_by)[agg_column].agg(method)
# 输出
if output:
result.to_csv(output)
return result
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="通用数据聚合工具")
parser.add_argument("--input", "-i", required=True, help="输入文件路径")
parser.add_argument("--group-by", "-g", required=True, help="分组列名")
parser.add_argument("--agg-column", "-a", required=True, help="聚合列名")
parser.add_argument("--method", "-m", default="sum",
choices=["sum", "mean", "count", "min", "max"],
help="聚合方法")
parser.add_argument("--output", "-o", help="输出文件路径")
args = parser.parse_args()
aggregate_data(args.input, args.group_by, args.agg_column,
args.method, args.output)
```
## Guidelines
### 通用化设计原则
- **抽象思维**:识别问题的本质模式,而非表面细节
- **参数化一切**:任何可能变化的值都应可配置
- **避免业务术语**:使用通用技术术语(如 "group_by" 而非 "region"
- **支持扩展**:预留扩展点,便于未来增加新功能
- **提供默认值**:合理默认值降低使用门槛
### 命名规范
| 类型 | 推荐 | 避免 |
|-----|------|------|
| 脚本名 | `aggregate_data.py` | `sales_analysis.py` |
| 参数名 | `--group-by` | `--region` |
| 函数名 | `transform_data()` | `fix_sales_format()` |
### 文档规范
README 条目应说明:
- **解决哪类问题**(非具体业务场景)
- **所有参数及默认值**
- **支持的输入格式**
- **使用示例**至少2个不同场景
### 何时创建新脚本
创建新脚本当:
- 现有脚本无法通过参数调整满足需求
- 问题属于新的通用模式
- 预计该场景会重复出现
### 何时修改现有脚本
修改现有脚本当:
- 增强通用性(添加新参数/配置)
- 修复 bug 但不破坏现有接口
- 扩展功能范围
## Directory Structure
```
./scripts/
├── README.md # 脚本目录和使用说明(必须存在)
├── aggregate_data.py # 通用聚合工具
├── convert_format.py # 格式转换工具
├── clean_dataset.py # 数据清洗工具
└── config/ # 可选:配置文件目录
└── templates.yaml
```