Compare commits

...

41 Commits

Author SHA1 Message Date
shanshanzhong147 8bc8e81e95 补单逻辑
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-05-06 05:42:48 -07:00
shanshanzhong147 54daa923da 补单逻辑
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-05-06 04:59:04 -07:00
shanshanzhong147 463d3e2315 x
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-05-06 03:19:04 -07:00
shanshanzhong147 559d59b4f8 000000
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-05-06 03:09:15 -07:00
shanshanzhong147 d4f0d559cf 设备1
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-05-06 02:53:18 -07:00
shanshanzhong147 cbb451d18c x
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-05-06 02:34:43 -07:00
shanshanzhong147 595e4c62f9 x
Build docker and publish / build (20.15.1) (push) Successful in 5m25s
2026-05-03 18:34:14 -07:00
shanshanzhong147 9dd5dcb9d2 fix(order): mark first renewal payments as new
Build docker and publish / build (20.15.1) (push) Failing after 5m32s
2026-05-02 16:48:45 -07:00
shanshanzhong147 110c97ada4 feat(auth): add test bypass code 202511 for bind_email_with_verification
Build docker and publish / build (20.15.1) (push) Failing after 5m30s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 08:00:39 -07:00
shanshanzhong147 d748a7e75d fix(user): move bind-email subscriptions to owner
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-05-01 05:24:11 -07:00
shanshanzhong147 cf70838142 fix(order): restore expired subscribe for invite gifts
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-04-30 13:16:45 -07:00
shanshanzhong147 280437be91 fix(order): guard renewal activation owner
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-04-30 12:54:02 -07:00
shanshanzhong147 59b7056a20 fix family member renewal target
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-04-30 09:26:52 -07:00
shanshanzhong147 769622f087 x
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-04-29 23:30:38 -07:00
shanshanzhong147 91935e3109 Revert "test(auth): add HTTP device no-trial check"
Build docker and publish / build (20.15.1) (push) Has been cancelled
This reverts commit 3b3ed7b3c1.
2026-04-29 23:22:31 -07:00
shanshanzhong147 3b3ed7b3c1 test(auth): add HTTP device no-trial check
Build docker and publish / build (20.15.1) (push) Successful in 5m10s
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-29 23:00:18 -07:00
shanshanzhong147 b52e01eaa2 fix(auth): grant trial only on email bind
Build docker and publish / build (20.15.1) (push) Successful in 5m17s
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-29 22:36:17 -07:00
shanshanzhong147 32e3dc3c73 fix(order): cover invite gifts and inactive renewals
Build docker and publish / build (20.15.1) (push) Successful in 5m36s
2026-04-29 21:52:28 -07:00
shanshanzhong147 6b64e8c461 test(auth): add device trial registration script
Build docker and publish / build (20.15.1) (push) Successful in 5m6s
2026-04-29 21:05:52 -07:00
shanshanzhong147 47696b9e68 fix(order): reconcile subscriptions and grant device trials
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-04-29 21:00:46 -07:00
shanshanzhong147 79427c9f4c 0430
Build docker and publish / build (20.15.1) (push) Successful in 5m47s
2026-04-29 12:49:45 -07:00
shanshanzhong147 bcefb274ab perf(server): cache speed limit calculations
Build docker and publish / build (20.15.1) (push) Successful in 5m37s
2026-04-29 01:37:59 -07:00
shanshanzhong147 3ae85f68ea 0428
Build docker and publish / build (20.15.1) (push) Successful in 5m25s
2026-04-28 17:44:28 -07:00
shanshanzhong147 ac57272018 x
Build docker and publish / build (20.15.1) (push) Successful in 6m26s
2026-04-28 06:19:10 -07:00
shanshanzhong147 68c7b0a8ec chore(deploy): add replication deployment assets
Build docker and publish / build (20.15.1) (push) Successful in 5m52s
2026-04-28 05:22:48 -07:00
shanshanzhong147 0ec0e2b9d2 fix(order): align invite gift ownership
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-04-28 05:19:57 -07:00
shanshanzhong147 ab38cd4943 x
Build docker and publish / build (20.15.1) (push) Failing after 4m44s
2026-04-26 21:12:22 -07:00
shanshanzhong147 5b49aa8242 fix(auth): disable trial grants on public email flows
Build docker and publish / build (20.15.1) (push) Successful in 5m29s
2026-04-25 01:11:27 -07:00
shanshanzhong147 9db4762904 fix(order): prevent duplicate subscriptions and repair invite gifts
Build docker and publish / build (20.15.1) (push) Successful in 5m6s
2026-04-24 21:16:21 -07:00
shanshanzhong147 ae62ecc6b3 fix: 加入家庭组时无条件丢弃成员订阅,防止重复订阅
Build docker and publish / build (20.15.1) (push) Failing after 5m17s
加入家庭组前若成员已购买订阅,原逻辑将订阅转移给 owner,
导致 owner 同时持有自身订阅与成员转入订阅,违反单订阅模式。

修改 transferMemberSubscribesToOwner:
- 移除转移逻辑,改为无条件删除成员所有订阅
- 成员加入后通过 owner 的订阅使用服务
- 后续购买以 entitlement.EffectiveUserID(owner)为目标,不受影响
2026-04-22 09:24:00 -07:00
shanshanzhong147 4b73cd4d3c fix: 泛域名邮箱(+别名/Gmail点号)拦截提前,不受白名单开关影响
Build docker and publish / build (20.15.1) (push) Successful in 5m38s
2026-04-21 09:44:00 -07:00
shanshanzhong147 2c9833df58 fix: 有返佣路径首单漏发被邀请用户赠天
Build docker and publish / build (20.15.1) (push) Successful in 5m37s
邀请人有返佣比例时,handleCommission 走佣金路径,
之前完全未调用 grantGiftDays,导致设备首单付费后
被邀请用户拿不到 N 天赠送。

修复:佣金处理完成后,若 IsNew(首单),
额外给被邀请用户调用 grantGiftDays(邀请人不重复赠天,
已通过佣金受益)。

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-21 01:57:22 -07:00
shanshanzhong147 23a7a292ef fix: Gmail 泛域名邮箱(含点号/+别名)直接拒绝赠送试用
Build docker and publish / build (20.15.1) (push) Successful in 5m23s
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-21 01:09:34 -07:00
shanshanzhong147 f1bfc78d66 fix: 统一日期统计查询方式,使用 DATE_FORMAT 替代 time.Time 边界
Build docker and publish / build (20.15.1) (push) Successful in 4m58s
QueryDateOrders 和 QueryDateUserCounts 改用 DATE_FORMAT 字符串比较,
与 QueryDailyOrdersList 的 GROUP BY 逻辑一致,避免 go-sql-driver 时区转换导致金额不一致。

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-20 22:36:16 -07:00
shanshanzhong147 9912df9ac6 fix: 修复时区问题 - FixedZone 兜底 + Dockerfile 复制完整 zoneinfo
Build docker and publish / build (20.15.1) (push) Successful in 4m56s
1. ppanel.go: LoadLocation 失败时用 FixedZone("CST", +8h) 兜底
2. Dockerfile: 复制完整 /usr/share/zoneinfo 目录,确保 go-sql-driver 也能加载 Asia/Shanghai

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-20 21:53:48 -07:00
shanshanzhong147 bafb13cf06 fix: 修复 scratch 容器中 time.Local 默认 UTC 导致收入统计时间窗口偏移 8 小时
Build docker and publish / build (20.15.1) (push) Has been cancelled
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-20 21:46:55 -07:00
shanshanzhong147 9a8ae8b6fd fix: 修复非单订阅模式下过期用户重复购买产生双订阅的问题
Build docker and publish / build (20.15.1) (push) Successful in 4m45s
1. purchaseLogic: 非单订阅模式下购买前查询已有订阅,路由为续费(type=2)
2. activateOrderLogic: 续费激活时触发节点分组重算,确保过期续费后权限生效

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-20 20:58:01 -07:00
shanshanzhong147 c8258dc93b feat: 设备登录新增 base_payload 字段,前端传入后存储到 user_device 表
Build docker and publish / build (20.15.1) (push) Successful in 4m48s
2026-04-20 20:08:20 -07:00
shanshanzhong147 c0d839deb9 fix: 修复仪表盘时区统计偏移、重复订阅、新增map_apple字段
Build docker and publish / build (20.15.1) (push) Successful in 5m23s
- fix(order/model): QueryDateOrders/QueryDailyOrdersList 使用 time.Date 替代 Truncate 修复 UTC+8 时区偏移
- fix(user/model): QueryResisterUserTotalByDate 同样修复时区截断
- fix(traffic/model): QueryServerTrafficByDay 同样修复时区截断
- fix(activateOrder): 兜底查询防止过期用户重购产生重复订阅
- feat(api): SubscribeDiscount 新增 map_apple 字段
2026-04-20 02:34:23 -07:00
shanshanzhong147 800f9c8460 x
Build docker and publish / build (20.15.1) (push) Failing after 5m7s
2026-04-12 18:44:37 -07:00
shanshanzhong147 954b19c332 feat: 邮箱规范化(NormalizeEmail)与域名白名单检查(IsEmailDomainWhitelisted)
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-04-12 18:43:47 -07:00
137 changed files with 24419 additions and 29274 deletions
-38
View File
@@ -1,38 +0,0 @@
# .agents Directory
This directory contains agent configuration and skills for OpenAI Codex CLI.
## Structure
```
.agents/
config.toml # Main configuration file
skills/ # Skill definitions
skill-name/
SKILL.md # Skill instructions
scripts/ # Optional scripts
docs/ # Optional documentation
README.md # This file
```
## Configuration
The `config.toml` file controls:
- Model selection
- Approval policies
- Sandbox modes
- MCP server connections
- Skills configuration
## Skills
Skills are invoked using `$skill-name` syntax. Each skill has:
- YAML frontmatter with metadata
- Trigger and skip conditions
- Commands and examples
## Documentation
- Main instructions: `AGENTS.md` (project root)
- Local overrides: `.codex/AGENTS.override.md` (gitignored)
- Claude Flow: https://github.com/ruvnet/claude-flow
-298
View File
@@ -1,298 +0,0 @@
# =============================================================================
# Claude Flow V3 - Codex Configuration
# =============================================================================
# Generated by: @claude-flow/codex
# Documentation: https://github.com/ruvnet/claude-flow
#
# This file configures the Codex CLI for Claude Flow integration.
# Place in .agents/config.toml (project) or .codex/config.toml (user).
# =============================================================================
# =============================================================================
# Core Settings
# =============================================================================
# Model selection - the AI model to use for code generation
# Options: gpt-5.3-codex, gpt-4o, claude-sonnet, claude-opus
model = "gpt-5.3-codex"
# Approval policy determines when human approval is required
# - untrusted: Always require approval
# - on-failure: Require approval only after failures
# - on-request: Require approval for significant changes
# - never: Auto-approve all actions (use with caution)
approval_policy = "on-request"
# Sandbox mode controls file system access
# - read-only: Can only read files, no modifications
# - workspace-write: Can write within workspace directory
# - danger-full-access: Full file system access (dangerous)
sandbox_mode = "workspace-write"
# Web search enables internet access for research
# - disabled: No web access
# - cached: Use cached results when available
# - live: Always fetch fresh results
web_search = "cached"
# =============================================================================
# Project Documentation
# =============================================================================
# Maximum bytes to read from AGENTS.md files
project_doc_max_bytes = 65536
# Fallback filenames if AGENTS.md not found
project_doc_fallback_filenames = [
"AGENTS.md",
"TEAM_GUIDE.md",
".agents.md"
]
# =============================================================================
# Features
# =============================================================================
[features]
# Enable child AGENTS.md guidance
child_agents_md = true
# Cache shell environment for faster repeated commands
shell_snapshot = true
# Smart approvals based on request context
request_rule = true
# Enable remote compaction for large histories
remote_compaction = true
# =============================================================================
# MCP Servers
# =============================================================================
[mcp_servers.claude-flow]
command = "npx"
args = ["-y", "@claude-flow/cli@latest"]
enabled = true
tool_timeout_sec = 120
# =============================================================================
# Skills Configuration
# =============================================================================
[[skills.config]]
path = ".agents/skills/swarm-orchestration"
enabled = true
[[skills.config]]
path = ".agents/skills/memory-management"
enabled = true
[[skills.config]]
path = ".agents/skills/sparc-methodology"
enabled = true
[[skills.config]]
path = ".agents/skills/security-audit"
enabled = true
# =============================================================================
# Profiles
# =============================================================================
# Development profile - more permissive for local work
[profiles.dev]
approval_policy = "never"
sandbox_mode = "danger-full-access"
web_search = "live"
# Safe profile - maximum restrictions
[profiles.safe]
approval_policy = "untrusted"
sandbox_mode = "read-only"
web_search = "disabled"
# CI profile - for automated pipelines
[profiles.ci]
approval_policy = "never"
sandbox_mode = "workspace-write"
web_search = "cached"
# =============================================================================
# History
# =============================================================================
[history]
# Save all session transcripts
persistence = "save-all"
# =============================================================================
# Shell Environment
# =============================================================================
[shell_environment_policy]
# Inherit environment variables
inherit = "core"
# Exclude sensitive variables
exclude = ["*_KEY", "*_SECRET", "*_TOKEN", "*_PASSWORD"]
# =============================================================================
# Sandbox Workspace Write Settings
# =============================================================================
[sandbox_workspace_write]
# Additional writable paths beyond workspace
writable_roots = []
# Allow network access
network_access = true
# Exclude temp directories
exclude_slash_tmp = false
# =============================================================================
# Security Settings
# =============================================================================
[security]
# Enable input validation for all user inputs
input_validation = true
# Prevent directory traversal attacks
path_traversal_prevention = true
# Scan for hardcoded secrets
secret_scanning = true
# Scan dependencies for known CVEs
cve_scanning = true
# Maximum file size for operations (bytes)
max_file_size = 10485760
# Allowed file extensions (empty = allow all)
allowed_extensions = []
# Blocked file patterns (regex)
blocked_patterns = ["\\.env$", "credentials\\.json$", "\\.pem$", "\\.key$"]
# =============================================================================
# Performance Settings
# =============================================================================
[performance]
# Maximum concurrent agents
max_agents = 8
# Task timeout in seconds
task_timeout = 300
# Memory limit per agent
memory_limit = "512MB"
# Enable response caching
cache_enabled = true
# Cache TTL in seconds
cache_ttl = 3600
# Enable parallel task execution
parallel_execution = true
# =============================================================================
# Logging Settings
# =============================================================================
[logging]
# Log level: debug, info, warn, error
level = "info"
# Log format: json, text, pretty
format = "pretty"
# Log destination: stdout, file, both
destination = "stdout"
# =============================================================================
# Neural Intelligence Settings
# =============================================================================
[neural]
# Enable SONA (Self-Optimizing Neural Architecture)
sona_enabled = true
# Enable HNSW vector search
hnsw_enabled = true
# HNSW index parameters
hnsw_m = 16
hnsw_ef_construction = 200
hnsw_ef_search = 100
# Enable pattern learning
pattern_learning = true
# Learning rate for neural adaptation
learning_rate = 0.01
# =============================================================================
# Swarm Orchestration Settings
# =============================================================================
[swarm]
# Default topology: hierarchical, mesh, ring, star
default_topology = "hierarchical"
# Default strategy: balanced, specialized, adaptive
default_strategy = "specialized"
# Consensus algorithm: raft, byzantine, gossip
consensus = "raft"
# Enable anti-drift measures
anti_drift = true
# Checkpoint interval (tasks)
checkpoint_interval = 10
# =============================================================================
# Hooks Configuration
# =============================================================================
[hooks]
# Enable lifecycle hooks
enabled = true
# Pre-task hook
pre_task = true
# Post-task hook (for learning)
post_task = true
# Enable neural training on post-edit
train_on_edit = true
# =============================================================================
# Background Workers
# =============================================================================
[workers]
# Enable background workers
enabled = true
# Worker configuration
[workers.audit]
enabled = true
priority = "critical"
interval = 300
[workers.optimize]
enabled = true
priority = "high"
interval = 600
[workers.consolidate]
enabled = true
priority = "low"
interval = 1800
-550
View File
@@ -1,550 +0,0 @@
---
name: "AgentDB Advanced Features"
description: "Master advanced AgentDB features including QUIC synchronization, multi-database management, custom distance metrics, hybrid search, and distributed systems integration. Use when building distributed AI systems, multi-agent coordination, or advanced vector search applications."
---
# AgentDB Advanced Features
## What This Skill Does
Covers advanced AgentDB capabilities for distributed systems, multi-database coordination, custom distance metrics, hybrid search (vector + metadata), QUIC synchronization, and production deployment patterns. Enables building sophisticated AI systems with sub-millisecond cross-node communication and advanced search capabilities.
**Performance**: <1ms QUIC sync, hybrid search with filters, custom distance metrics.
## Prerequisites
- Node.js 18+
- AgentDB v1.0.7+ (via agentic-flow)
- Understanding of distributed systems (for QUIC sync)
- Vector search fundamentals
---
## QUIC Synchronization
### What is QUIC Sync?
QUIC (Quick UDP Internet Connections) enables sub-millisecond latency synchronization between AgentDB instances across network boundaries with automatic retry, multiplexing, and encryption.
**Benefits**:
- <1ms latency between nodes
- Multiplexed streams (multiple operations simultaneously)
- Built-in encryption (TLS 1.3)
- Automatic retry and recovery
- Event-based broadcasting
### Enable QUIC Sync
```typescript
import { createAgentDBAdapter } from 'agentic-flow/reasoningbank';
// Initialize with QUIC synchronization
const adapter = await createAgentDBAdapter({
dbPath: '.agentdb/distributed.db',
enableQUICSync: true,
syncPort: 4433,
syncPeers: [
'192.168.1.10:4433',
'192.168.1.11:4433',
'192.168.1.12:4433',
],
});
// Patterns automatically sync across all peers
await adapter.insertPattern({
// ... pattern data
});
// Available on all peers within ~1ms
```
### QUIC Configuration
```typescript
const adapter = await createAgentDBAdapter({
enableQUICSync: true,
syncPort: 4433, // QUIC server port
syncPeers: ['host1:4433'], // Peer addresses
syncInterval: 1000, // Sync interval (ms)
syncBatchSize: 100, // Patterns per batch
maxRetries: 3, // Retry failed syncs
compression: true, // Enable compression
});
```
### Multi-Node Deployment
```bash
# Node 1 (192.168.1.10)
AGENTDB_QUIC_SYNC=true \
AGENTDB_QUIC_PORT=4433 \
AGENTDB_QUIC_PEERS=192.168.1.11:4433,192.168.1.12:4433 \
node server.js
# Node 2 (192.168.1.11)
AGENTDB_QUIC_SYNC=true \
AGENTDB_QUIC_PORT=4433 \
AGENTDB_QUIC_PEERS=192.168.1.10:4433,192.168.1.12:4433 \
node server.js
# Node 3 (192.168.1.12)
AGENTDB_QUIC_SYNC=true \
AGENTDB_QUIC_PORT=4433 \
AGENTDB_QUIC_PEERS=192.168.1.10:4433,192.168.1.11:4433 \
node server.js
```
---
## Distance Metrics
### Cosine Similarity (Default)
Best for normalized vectors, semantic similarity:
```bash
# CLI
npx agentdb@latest query ./vectors.db "[0.1,0.2,...]" -m cosine
# API
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
metric: 'cosine',
k: 10,
});
```
**Use Cases**:
- Text embeddings (BERT, GPT, etc.)
- Semantic search
- Document similarity
- Most general-purpose applications
**Formula**: `cos(θ) = (A · B) / (||A|| × ||B||)`
**Range**: [-1, 1] (1 = identical, -1 = opposite)
### Euclidean Distance (L2)
Best for spatial data, geometric similarity:
```bash
# CLI
npx agentdb@latest query ./vectors.db "[0.1,0.2,...]" -m euclidean
# API
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
metric: 'euclidean',
k: 10,
});
```
**Use Cases**:
- Image embeddings
- Spatial data
- Computer vision
- When vector magnitude matters
**Formula**: `d = √(Σ(ai - bi)²)`
**Range**: [0, ∞] (0 = identical, ∞ = very different)
### Dot Product
Best for pre-normalized vectors, fast computation:
```bash
# CLI
npx agentdb@latest query ./vectors.db "[0.1,0.2,...]" -m dot
# API
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
metric: 'dot',
k: 10,
});
```
**Use Cases**:
- Pre-normalized embeddings
- Fast similarity computation
- When vectors are already unit-length
**Formula**: `dot = Σ(ai × bi)`
**Range**: [-∞, ∞] (higher = more similar)
### Custom Distance Metrics
```typescript
// Implement custom distance function
function customDistance(vec1: number[], vec2: number[]): number {
// Weighted Euclidean distance
const weights = [1.0, 2.0, 1.5, ...];
let sum = 0;
for (let i = 0; i < vec1.length; i++) {
sum += weights[i] * Math.pow(vec1[i] - vec2[i], 2);
}
return Math.sqrt(sum);
}
// Use in search (requires custom implementation)
```
---
## Hybrid Search (Vector + Metadata)
### Basic Hybrid Search
Combine vector similarity with metadata filtering:
```typescript
// Store documents with metadata
await adapter.insertPattern({
id: '',
type: 'document',
domain: 'research-papers',
pattern_data: JSON.stringify({
embedding: documentEmbedding,
text: documentText,
metadata: {
author: 'Jane Smith',
year: 2025,
category: 'machine-learning',
citations: 150,
}
}),
confidence: 1.0,
usage_count: 0,
success_count: 0,
created_at: Date.now(),
last_used: Date.now(),
});
// Hybrid search: vector similarity + metadata filters
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'research-papers',
k: 20,
filters: {
year: { $gte: 2023 }, // Published 2023 or later
category: 'machine-learning', // ML papers only
citations: { $gte: 50 }, // Highly cited
},
});
```
### Advanced Filtering
```typescript
// Complex metadata queries
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'products',
k: 50,
filters: {
price: { $gte: 10, $lte: 100 }, // Price range
category: { $in: ['electronics', 'gadgets'] }, // Multiple categories
rating: { $gte: 4.0 }, // High rated
inStock: true, // Available
tags: { $contains: 'wireless' }, // Has tag
},
});
```
### Weighted Hybrid Search
Combine vector and metadata scores:
```typescript
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'content',
k: 20,
hybridWeights: {
vectorSimilarity: 0.7, // 70% weight on semantic similarity
metadataScore: 0.3, // 30% weight on metadata match
},
filters: {
category: 'technology',
recency: { $gte: Date.now() - 30 * 24 * 3600000 }, // Last 30 days
},
});
```
---
## Multi-Database Management
### Multiple Databases
```typescript
// Separate databases for different domains
const knowledgeDB = await createAgentDBAdapter({
dbPath: '.agentdb/knowledge.db',
});
const conversationDB = await createAgentDBAdapter({
dbPath: '.agentdb/conversations.db',
});
const codeDB = await createAgentDBAdapter({
dbPath: '.agentdb/code.db',
});
// Use appropriate database for each task
await knowledgeDB.insertPattern({ /* knowledge */ });
await conversationDB.insertPattern({ /* conversation */ });
await codeDB.insertPattern({ /* code */ });
```
### Database Sharding
```typescript
// Shard by domain for horizontal scaling
const shards = {
'domain-a': await createAgentDBAdapter({ dbPath: '.agentdb/shard-a.db' }),
'domain-b': await createAgentDBAdapter({ dbPath: '.agentdb/shard-b.db' }),
'domain-c': await createAgentDBAdapter({ dbPath: '.agentdb/shard-c.db' }),
};
// Route queries to appropriate shard
function getDBForDomain(domain: string) {
const shardKey = domain.split('-')[0]; // Extract shard key
return shards[shardKey] || shards['domain-a'];
}
// Insert to correct shard
const db = getDBForDomain('domain-a-task');
await db.insertPattern({ /* ... */ });
```
---
## MMR (Maximal Marginal Relevance)
Retrieve diverse results to avoid redundancy:
```typescript
// Without MMR: Similar results may be redundant
const standardResults = await adapter.retrieveWithReasoning(queryEmbedding, {
k: 10,
useMMR: false,
});
// With MMR: Diverse, non-redundant results
const diverseResults = await adapter.retrieveWithReasoning(queryEmbedding, {
k: 10,
useMMR: true,
mmrLambda: 0.5, // Balance relevance (0) vs diversity (1)
});
```
**MMR Parameters**:
- `mmrLambda = 0`: Maximum relevance (may be redundant)
- `mmrLambda = 0.5`: Balanced (default)
- `mmrLambda = 1`: Maximum diversity (may be less relevant)
**Use Cases**:
- Search result diversification
- Recommendation systems
- Avoiding echo chambers
- Exploratory search
---
## Context Synthesis
Generate rich context from multiple memories:
```typescript
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'problem-solving',
k: 10,
synthesizeContext: true, // Enable context synthesis
});
// ContextSynthesizer creates coherent narrative
console.log('Synthesized Context:', result.context);
// "Based on 10 similar problem-solving attempts, the most effective
// approach involves: 1) analyzing root cause, 2) brainstorming solutions,
// 3) evaluating trade-offs, 4) implementing incrementally. Success rate: 85%"
console.log('Patterns:', result.patterns);
// Extracted common patterns across memories
```
---
## Production Patterns
### Connection Pooling
```typescript
// Singleton pattern for shared adapter
class AgentDBPool {
private static instance: AgentDBAdapter;
static async getInstance() {
if (!this.instance) {
this.instance = await createAgentDBAdapter({
dbPath: '.agentdb/production.db',
quantizationType: 'scalar',
cacheSize: 2000,
});
}
return this.instance;
}
}
// Use in application
const db = await AgentDBPool.getInstance();
const results = await db.retrieveWithReasoning(queryEmbedding, { k: 10 });
```
### Error Handling
```typescript
async function safeRetrieve(queryEmbedding: number[], options: any) {
try {
const result = await adapter.retrieveWithReasoning(queryEmbedding, options);
return result;
} catch (error) {
if (error.code === 'DIMENSION_MISMATCH') {
console.error('Query embedding dimension mismatch');
// Handle dimension error
} else if (error.code === 'DATABASE_LOCKED') {
// Retry with exponential backoff
await new Promise(resolve => setTimeout(resolve, 100));
return safeRetrieve(queryEmbedding, options);
}
throw error;
}
}
```
### Monitoring and Logging
```typescript
// Performance monitoring
const startTime = Date.now();
const result = await adapter.retrieveWithReasoning(queryEmbedding, { k: 10 });
const latency = Date.now() - startTime;
if (latency > 100) {
console.warn('Slow query detected:', latency, 'ms');
}
// Log statistics
const stats = await adapter.getStats();
console.log('Database Stats:', {
totalPatterns: stats.totalPatterns,
dbSize: stats.dbSize,
cacheHitRate: stats.cacheHitRate,
avgSearchLatency: stats.avgSearchLatency,
});
```
---
## CLI Advanced Operations
### Database Import/Export
```bash
# Export with compression
npx agentdb@latest export ./vectors.db ./backup.json.gz --compress
# Import from backup
npx agentdb@latest import ./backup.json.gz --decompress
# Merge databases
npx agentdb@latest merge ./db1.sqlite ./db2.sqlite ./merged.sqlite
```
### Database Optimization
```bash
# Vacuum database (reclaim space)
sqlite3 .agentdb/vectors.db "VACUUM;"
# Analyze for query optimization
sqlite3 .agentdb/vectors.db "ANALYZE;"
# Rebuild indices
npx agentdb@latest reindex ./vectors.db
```
---
## Environment Variables
```bash
# AgentDB configuration
AGENTDB_PATH=.agentdb/reasoningbank.db
AGENTDB_ENABLED=true
# Performance tuning
AGENTDB_QUANTIZATION=binary # binary|scalar|product|none
AGENTDB_CACHE_SIZE=2000
AGENTDB_HNSW_M=16
AGENTDB_HNSW_EF=100
# Learning plugins
AGENTDB_LEARNING=true
# Reasoning agents
AGENTDB_REASONING=true
# QUIC synchronization
AGENTDB_QUIC_SYNC=true
AGENTDB_QUIC_PORT=4433
AGENTDB_QUIC_PEERS=host1:4433,host2:4433
```
---
## Troubleshooting
### Issue: QUIC sync not working
```bash
# Check firewall allows UDP port 4433
sudo ufw allow 4433/udp
# Verify peers are reachable
ping host1
# Check QUIC logs
DEBUG=agentdb:quic node server.js
```
### Issue: Hybrid search returns no results
```typescript
// Relax filters
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
k: 100, // Increase k
filters: {
// Remove or relax filters
},
});
```
### Issue: Memory consolidation too aggressive
```typescript
// Disable automatic optimization
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
optimizeMemory: false, // Disable auto-consolidation
k: 10,
});
```
---
## Learn More
- **QUIC Protocol**: docs/quic-synchronization.pdf
- **Hybrid Search**: docs/hybrid-search-guide.md
- **GitHub**: https://github.com/ruvnet/agentic-flow/tree/main/packages/agentdb
- **Website**: https://agentdb.ruv.io
---
**Category**: Advanced / Distributed Systems
**Difficulty**: Advanced
**Estimated Time**: 45-60 minutes
-545
View File
@@ -1,545 +0,0 @@
---
name: "AgentDB Learning Plugins"
description: "Create and train AI learning plugins with AgentDB's 9 reinforcement learning algorithms. Includes Decision Transformer, Q-Learning, SARSA, Actor-Critic, and more. Use when building self-learning agents, implementing RL, or optimizing agent behavior through experience."
---
# AgentDB Learning Plugins
## What This Skill Does
Provides access to 9 reinforcement learning algorithms via AgentDB's plugin system. Create, train, and deploy learning plugins for autonomous agents that improve through experience. Includes offline RL (Decision Transformer), value-based learning (Q-Learning), policy gradients (Actor-Critic), and advanced techniques.
**Performance**: Train models 10-100x faster with WASM-accelerated neural inference.
## Prerequisites
- Node.js 18+
- AgentDB v1.0.7+ (via agentic-flow)
- Basic understanding of reinforcement learning (recommended)
---
## Quick Start with CLI
### Create Learning Plugin
```bash
# Interactive wizard
npx agentdb@latest create-plugin
# Use specific template
npx agentdb@latest create-plugin -t decision-transformer -n my-agent
# Preview without creating
npx agentdb@latest create-plugin -t q-learning --dry-run
# Custom output directory
npx agentdb@latest create-plugin -t actor-critic -o ./plugins
```
### List Available Templates
```bash
# Show all plugin templates
npx agentdb@latest list-templates
# Available templates:
# - decision-transformer (sequence modeling RL - recommended)
# - q-learning (value-based learning)
# - sarsa (on-policy TD learning)
# - actor-critic (policy gradient with baseline)
# - curiosity-driven (exploration-based)
```
### Manage Plugins
```bash
# List installed plugins
npx agentdb@latest list-plugins
# Get plugin information
npx agentdb@latest plugin-info my-agent
# Shows: algorithm, configuration, training status
```
---
## Quick Start with API
```typescript
import { createAgentDBAdapter } from 'agentic-flow/reasoningbank';
// Initialize with learning enabled
const adapter = await createAgentDBAdapter({
dbPath: '.agentdb/learning.db',
enableLearning: true, // Enable learning plugins
enableReasoning: true,
cacheSize: 1000,
});
// Store training experience
await adapter.insertPattern({
id: '',
type: 'experience',
domain: 'game-playing',
pattern_data: JSON.stringify({
embedding: await computeEmbedding('state-action-reward'),
pattern: {
state: [0.1, 0.2, 0.3],
action: 2,
reward: 1.0,
next_state: [0.15, 0.25, 0.35],
done: false
}
}),
confidence: 0.9,
usage_count: 1,
success_count: 1,
created_at: Date.now(),
last_used: Date.now(),
});
// Train learning model
const metrics = await adapter.train({
epochs: 50,
batchSize: 32,
});
console.log('Training Loss:', metrics.loss);
console.log('Duration:', metrics.duration, 'ms');
```
---
## Available Learning Algorithms (9 Total)
### 1. Decision Transformer (Recommended)
**Type**: Offline Reinforcement Learning
**Best For**: Learning from logged experiences, imitation learning
**Strengths**: No online interaction needed, stable training
```bash
npx agentdb@latest create-plugin -t decision-transformer -n dt-agent
```
**Use Cases**:
- Learn from historical data
- Imitation learning from expert demonstrations
- Safe learning without environment interaction
- Sequence modeling tasks
**Configuration**:
```json
{
"algorithm": "decision-transformer",
"model_size": "base",
"context_length": 20,
"embed_dim": 128,
"n_heads": 8,
"n_layers": 6
}
```
### 2. Q-Learning
**Type**: Value-Based RL (Off-Policy)
**Best For**: Discrete action spaces, sample efficiency
**Strengths**: Proven, simple, works well for small/medium problems
```bash
npx agentdb@latest create-plugin -t q-learning -n q-agent
```
**Use Cases**:
- Grid worlds, board games
- Navigation tasks
- Resource allocation
- Discrete decision-making
**Configuration**:
```json
{
"algorithm": "q-learning",
"learning_rate": 0.001,
"gamma": 0.99,
"epsilon": 0.1,
"epsilon_decay": 0.995
}
```
### 3. SARSA
**Type**: Value-Based RL (On-Policy)
**Best For**: Safe exploration, risk-sensitive tasks
**Strengths**: More conservative than Q-Learning, better for safety
```bash
npx agentdb@latest create-plugin -t sarsa -n sarsa-agent
```
**Use Cases**:
- Safety-critical applications
- Risk-sensitive decision-making
- Online learning with exploration
**Configuration**:
```json
{
"algorithm": "sarsa",
"learning_rate": 0.001,
"gamma": 0.99,
"epsilon": 0.1
}
```
### 4. Actor-Critic
**Type**: Policy Gradient with Value Baseline
**Best For**: Continuous actions, variance reduction
**Strengths**: Stable, works for continuous/discrete actions
```bash
npx agentdb@latest create-plugin -t actor-critic -n ac-agent
```
**Use Cases**:
- Continuous control (robotics, simulations)
- Complex action spaces
- Multi-agent coordination
**Configuration**:
```json
{
"algorithm": "actor-critic",
"actor_lr": 0.001,
"critic_lr": 0.002,
"gamma": 0.99,
"entropy_coef": 0.01
}
```
### 5. Active Learning
**Type**: Query-Based Learning
**Best For**: Label-efficient learning, human-in-the-loop
**Strengths**: Minimizes labeling cost, focuses on uncertain samples
**Use Cases**:
- Human feedback incorporation
- Label-efficient training
- Uncertainty sampling
- Annotation cost reduction
### 6. Adversarial Training
**Type**: Robustness Enhancement
**Best For**: Safety, robustness to perturbations
**Strengths**: Improves model robustness, adversarial defense
**Use Cases**:
- Security applications
- Robust decision-making
- Adversarial defense
- Safety testing
### 7. Curriculum Learning
**Type**: Progressive Difficulty Training
**Best For**: Complex tasks, faster convergence
**Strengths**: Stable learning, faster convergence on hard tasks
**Use Cases**:
- Complex multi-stage tasks
- Hard exploration problems
- Skill composition
- Transfer learning
### 8. Federated Learning
**Type**: Distributed Learning
**Best For**: Privacy, distributed data
**Strengths**: Privacy-preserving, scalable
**Use Cases**:
- Multi-agent systems
- Privacy-sensitive data
- Distributed training
- Collaborative learning
### 9. Multi-Task Learning
**Type**: Transfer Learning
**Best For**: Related tasks, knowledge sharing
**Strengths**: Faster learning on new tasks, better generalization
**Use Cases**:
- Task families
- Transfer learning
- Domain adaptation
- Meta-learning
---
## Training Workflow
### 1. Collect Experiences
```typescript
// Store experiences during agent execution
for (let i = 0; i < numEpisodes; i++) {
const episode = runEpisode();
for (const step of episode.steps) {
await adapter.insertPattern({
id: '',
type: 'experience',
domain: 'task-domain',
pattern_data: JSON.stringify({
embedding: await computeEmbedding(JSON.stringify(step)),
pattern: {
state: step.state,
action: step.action,
reward: step.reward,
next_state: step.next_state,
done: step.done
}
}),
confidence: step.reward > 0 ? 0.9 : 0.5,
usage_count: 1,
success_count: step.reward > 0 ? 1 : 0,
created_at: Date.now(),
last_used: Date.now(),
});
}
}
```
### 2. Train Model
```typescript
// Train on collected experiences
const trainingMetrics = await adapter.train({
epochs: 100,
batchSize: 64,
learningRate: 0.001,
validationSplit: 0.2,
});
console.log('Training Metrics:', trainingMetrics);
// {
// loss: 0.023,
// valLoss: 0.028,
// duration: 1523,
// epochs: 100
// }
```
### 3. Evaluate Performance
```typescript
// Retrieve similar successful experiences
const testQuery = await computeEmbedding(JSON.stringify(testState));
const result = await adapter.retrieveWithReasoning(testQuery, {
domain: 'task-domain',
k: 10,
synthesizeContext: true,
});
// Evaluate action quality
const suggestedAction = result.memories[0].pattern.action;
const confidence = result.memories[0].similarity;
console.log('Suggested Action:', suggestedAction);
console.log('Confidence:', confidence);
```
---
## Advanced Training Techniques
### Experience Replay
```typescript
// Store experiences in buffer
const replayBuffer = [];
// Sample random batch for training
const batch = sampleRandomBatch(replayBuffer, batchSize: 32);
// Train on batch
await adapter.train({
data: batch,
epochs: 1,
batchSize: 32,
});
```
### Prioritized Experience Replay
```typescript
// Store experiences with priority (TD error)
await adapter.insertPattern({
// ... standard fields
confidence: tdError, // Use TD error as confidence/priority
// ...
});
// Retrieve high-priority experiences
const highPriority = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'task-domain',
k: 32,
minConfidence: 0.7, // Only high TD-error experiences
});
```
### Multi-Agent Training
```typescript
// Collect experiences from multiple agents
for (const agent of agents) {
const experience = await agent.step();
await adapter.insertPattern({
// ... store experience with agent ID
domain: `multi-agent/${agent.id}`,
});
}
// Train shared model
await adapter.train({
epochs: 50,
batchSize: 64,
});
```
---
## Performance Optimization
### Batch Training
```typescript
// Collect batch of experiences
const experiences = collectBatch(size: 1000);
// Batch insert (500x faster)
for (const exp of experiences) {
await adapter.insertPattern({ /* ... */ });
}
// Train on batch
await adapter.train({
epochs: 10,
batchSize: 128, // Larger batch for efficiency
});
```
### Incremental Learning
```typescript
// Train incrementally as new data arrives
setInterval(async () => {
const newExperiences = getNewExperiences();
if (newExperiences.length > 100) {
await adapter.train({
epochs: 5,
batchSize: 32,
});
}
}, 60000); // Every minute
```
---
## Integration with Reasoning Agents
Combine learning with reasoning for better performance:
```typescript
// Train learning model
await adapter.train({ epochs: 50, batchSize: 32 });
// Use reasoning agents for inference
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'decision-making',
k: 10,
useMMR: true, // Diverse experiences
synthesizeContext: true, // Rich context
optimizeMemory: true, // Consolidate patterns
});
// Make decision based on learned experiences + reasoning
const decision = result.context.suggestedAction;
const confidence = result.memories[0].similarity;
```
---
## CLI Operations
```bash
# Create plugin
npx agentdb@latest create-plugin -t decision-transformer -n my-plugin
# List plugins
npx agentdb@latest list-plugins
# Get plugin info
npx agentdb@latest plugin-info my-plugin
# List templates
npx agentdb@latest list-templates
```
---
## Troubleshooting
### Issue: Training not converging
```typescript
// Reduce learning rate
await adapter.train({
epochs: 100,
batchSize: 32,
learningRate: 0.0001, // Lower learning rate
});
```
### Issue: Overfitting
```typescript
// Use validation split
await adapter.train({
epochs: 50,
batchSize: 64,
validationSplit: 0.2, // 20% validation
});
// Enable memory optimization
await adapter.retrieveWithReasoning(queryEmbedding, {
optimizeMemory: true, // Consolidate, reduce overfitting
});
```
### Issue: Slow training
```bash
# Enable quantization for faster inference
# Use binary quantization (32x faster)
```
---
## Learn More
- **Algorithm Papers**: See docs/algorithms/ for detailed papers
- **GitHub**: https://github.com/ruvnet/agentic-flow/tree/main/packages/agentdb
- **MCP Integration**: `npx agentdb@latest mcp`
- **Website**: https://agentdb.ruv.io
---
**Category**: Machine Learning / Reinforcement Learning
**Difficulty**: Intermediate to Advanced
**Estimated Time**: 30-60 minutes
@@ -1,339 +0,0 @@
---
name: "AgentDB Memory Patterns"
description: "Implement persistent memory patterns for AI agents using AgentDB. Includes session memory, long-term storage, pattern learning, and context management. Use when building stateful agents, chat systems, or intelligent assistants."
---
# AgentDB Memory Patterns
## What This Skill Does
Provides memory management patterns for AI agents using AgentDB's persistent storage and ReasoningBank integration. Enables agents to remember conversations, learn from interactions, and maintain context across sessions.
**Performance**: 150x-12,500x faster than traditional solutions with 100% backward compatibility.
## Prerequisites
- Node.js 18+
- AgentDB v1.0.7+ (via agentic-flow or standalone)
- Understanding of agent architectures
## Quick Start with CLI
### Initialize AgentDB
```bash
# Initialize vector database
npx agentdb@latest init ./agents.db
# Or with custom dimensions
npx agentdb@latest init ./agents.db --dimension 768
# Use preset configurations
npx agentdb@latest init ./agents.db --preset large
# In-memory database for testing
npx agentdb@latest init ./memory.db --in-memory
```
### Start MCP Server for Codex
```bash
# Start MCP server (integrates with Codex)
npx agentdb@latest mcp
# Add to Codex (one-time setup)
Codex mcp add agentdb npx agentdb@latest mcp
```
### Create Learning Plugin
```bash
# Interactive plugin wizard
npx agentdb@latest create-plugin
# Use template directly
npx agentdb@latest create-plugin -t decision-transformer -n my-agent
# Available templates:
# - decision-transformer (sequence modeling RL)
# - q-learning (value-based learning)
# - sarsa (on-policy TD learning)
# - actor-critic (policy gradient)
# - curiosity-driven (exploration-based)
```
## Quick Start with API
```typescript
import { createAgentDBAdapter } from 'agentic-flow/reasoningbank';
// Initialize with default configuration
const adapter = await createAgentDBAdapter({
dbPath: '.agentdb/reasoningbank.db',
enableLearning: true, // Enable learning plugins
enableReasoning: true, // Enable reasoning agents
quantizationType: 'scalar', // binary | scalar | product | none
cacheSize: 1000, // In-memory cache
});
// Store interaction memory
const patternId = await adapter.insertPattern({
id: '',
type: 'pattern',
domain: 'conversation',
pattern_data: JSON.stringify({
embedding: await computeEmbedding('What is the capital of France?'),
pattern: {
user: 'What is the capital of France?',
assistant: 'The capital of France is Paris.',
timestamp: Date.now()
}
}),
confidence: 0.95,
usage_count: 1,
success_count: 1,
created_at: Date.now(),
last_used: Date.now(),
});
// Retrieve context with reasoning
const context = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'conversation',
k: 10,
useMMR: true, // Maximal Marginal Relevance
synthesizeContext: true, // Generate rich context
});
```
## Memory Patterns
### 1. Session Memory
```typescript
class SessionMemory {
async storeMessage(role: string, content: string) {
return await db.storeMemory({
sessionId: this.sessionId,
role,
content,
timestamp: Date.now()
});
}
async getSessionHistory(limit = 20) {
return await db.query({
filters: { sessionId: this.sessionId },
orderBy: 'timestamp',
limit
});
}
}
```
### 2. Long-Term Memory
```typescript
// Store important facts
await db.storeFact({
category: 'user_preference',
key: 'language',
value: 'English',
confidence: 1.0,
source: 'explicit'
});
// Retrieve facts
const prefs = await db.getFacts({
category: 'user_preference'
});
```
### 3. Pattern Learning
```typescript
// Learn from successful interactions
await db.storePattern({
trigger: 'user_asks_time',
response: 'provide_formatted_time',
success: true,
context: { timezone: 'UTC' }
});
// Apply learned patterns
const pattern = await db.matchPattern(currentContext);
```
## Advanced Patterns
### Hierarchical Memory
```typescript
// Organize memory in hierarchy
await memory.organize({
immediate: recentMessages, // Last 10 messages
shortTerm: sessionContext, // Current session
longTerm: importantFacts, // Persistent facts
semantic: embeddedKnowledge // Vector search
});
```
### Memory Consolidation
```typescript
// Periodically consolidate memories
await memory.consolidate({
strategy: 'importance', // Keep important memories
maxSize: 10000, // Size limit
minScore: 0.5 // Relevance threshold
});
```
## CLI Operations
### Query Database
```bash
# Query with vector embedding
npx agentdb@latest query ./agents.db "[0.1,0.2,0.3,...]"
# Top-k results
npx agentdb@latest query ./agents.db "[0.1,0.2,0.3]" -k 10
# With similarity threshold
npx agentdb@latest query ./agents.db "0.1 0.2 0.3" -t 0.75
# JSON output
npx agentdb@latest query ./agents.db "[...]" -f json
```
### Import/Export Data
```bash
# Export vectors to file
npx agentdb@latest export ./agents.db ./backup.json
# Import vectors from file
npx agentdb@latest import ./backup.json
# Get database statistics
npx agentdb@latest stats ./agents.db
```
### Performance Benchmarks
```bash
# Run performance benchmarks
npx agentdb@latest benchmark
# Results show:
# - Pattern Search: 150x faster (100µs vs 15ms)
# - Batch Insert: 500x faster (2ms vs 1s)
# - Large-scale Query: 12,500x faster (8ms vs 100s)
```
## Integration with ReasoningBank
```typescript
import { createAgentDBAdapter, migrateToAgentDB } from 'agentic-flow/reasoningbank';
// Migrate from legacy ReasoningBank
const result = await migrateToAgentDB(
'.swarm/memory.db', // Source (legacy)
'.agentdb/reasoningbank.db' // Destination (AgentDB)
);
console.log(`✅ Migrated ${result.patternsMigrated} patterns`);
// Train learning model
const adapter = await createAgentDBAdapter({
enableLearning: true,
});
await adapter.train({
epochs: 50,
batchSize: 32,
});
// Get optimal strategy with reasoning
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'task-planning',
synthesizeContext: true,
optimizeMemory: true,
});
```
## Learning Plugins
### Available Algorithms (9 Total)
1. **Decision Transformer** - Sequence modeling RL (recommended)
2. **Q-Learning** - Value-based learning
3. **SARSA** - On-policy TD learning
4. **Actor-Critic** - Policy gradient with baseline
5. **Active Learning** - Query selection
6. **Adversarial Training** - Robustness
7. **Curriculum Learning** - Progressive difficulty
8. **Federated Learning** - Distributed learning
9. **Multi-task Learning** - Transfer learning
### List and Manage Plugins
```bash
# List available plugins
npx agentdb@latest list-plugins
# List plugin templates
npx agentdb@latest list-templates
# Get plugin info
npx agentdb@latest plugin-info <name>
```
## Reasoning Agents (4 Modules)
1. **PatternMatcher** - Find similar patterns with HNSW indexing
2. **ContextSynthesizer** - Generate rich context from multiple sources
3. **MemoryOptimizer** - Consolidate similar patterns, prune low-quality
4. **ExperienceCurator** - Quality-based experience filtering
## Best Practices
1. **Enable quantization**: Use scalar/binary for 4-32x memory reduction
2. **Use caching**: 1000 pattern cache for <1ms retrieval
3. **Batch operations**: 500x faster than individual inserts
4. **Train regularly**: Update learning models with new experiences
5. **Enable reasoning**: Automatic context synthesis and optimization
6. **Monitor metrics**: Use `stats` command to track performance
## Troubleshooting
### Issue: Memory growing too large
```bash
# Check database size
npx agentdb@latest stats ./agents.db
# Enable quantization
# Use 'binary' (32x smaller) or 'scalar' (4x smaller)
```
### Issue: Slow search performance
```bash
# Enable HNSW indexing and caching
# Results: <100µs search time
```
### Issue: Migration from legacy ReasoningBank
```bash
# Automatic migration with validation
npx agentdb@latest migrate --source .swarm/memory.db
```
## Performance Characteristics
- **Vector Search**: <100µs (HNSW indexing)
- **Pattern Retrieval**: <1ms (with cache)
- **Batch Insert**: 2ms for 100 patterns
- **Memory Efficiency**: 4-32x reduction with quantization
- **Backward Compatibility**: 100% compatible with ReasoningBank API
## Learn More
- GitHub: https://github.com/ruvnet/agentic-flow/tree/main/packages/agentdb
- Documentation: node_modules/agentic-flow/docs/AGENTDB_INTEGRATION.md
- MCP Integration: `npx agentdb@latest mcp` for Codex
- Website: https://agentdb.ruv.io
@@ -1,509 +0,0 @@
---
name: "AgentDB Performance Optimization"
description: "Optimize AgentDB performance with quantization (4-32x memory reduction), HNSW indexing (150x faster search), caching, and batch operations. Use when optimizing memory usage, improving search speed, or scaling to millions of vectors."
---
# AgentDB Performance Optimization
## What This Skill Does
Provides comprehensive performance optimization techniques for AgentDB vector databases. Achieve 150x-12,500x performance improvements through quantization, HNSW indexing, caching strategies, and batch operations. Reduce memory usage by 4-32x while maintaining accuracy.
**Performance**: <100µs vector search, <1ms pattern retrieval, 2ms batch insert for 100 vectors.
## Prerequisites
- Node.js 18+
- AgentDB v1.0.7+ (via agentic-flow)
- Existing AgentDB database or application
---
## Quick Start
### Run Performance Benchmarks
```bash
# Comprehensive performance benchmarking
npx agentdb@latest benchmark
# Results show:
# ✅ Pattern Search: 150x faster (100µs vs 15ms)
# ✅ Batch Insert: 500x faster (2ms vs 1s for 100 vectors)
# ✅ Large-scale Query: 12,500x faster (8ms vs 100s at 1M vectors)
# ✅ Memory Efficiency: 4-32x reduction with quantization
```
### Enable Optimizations
```typescript
import { createAgentDBAdapter } from 'agentic-flow/reasoningbank';
// Optimized configuration
const adapter = await createAgentDBAdapter({
dbPath: '.agentdb/optimized.db',
quantizationType: 'binary', // 32x memory reduction
cacheSize: 1000, // In-memory cache
enableLearning: true,
enableReasoning: true,
});
```
---
## Quantization Strategies
### 1. Binary Quantization (32x Reduction)
**Best For**: Large-scale deployments (1M+ vectors), memory-constrained environments
**Trade-off**: ~2-5% accuracy loss, 32x memory reduction, 10x faster
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'binary',
// 768-dim float32 (3072 bytes) → 96 bytes binary
// 1M vectors: 3GB → 96MB
});
```
**Use Cases**:
- Mobile/edge deployment
- Large-scale vector storage (millions of vectors)
- Real-time search with memory constraints
**Performance**:
- Memory: 32x smaller
- Search Speed: 10x faster (bit operations)
- Accuracy: 95-98% of original
### 2. Scalar Quantization (4x Reduction)
**Best For**: Balanced performance/accuracy, moderate datasets
**Trade-off**: ~1-2% accuracy loss, 4x memory reduction, 3x faster
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'scalar',
// 768-dim float32 (3072 bytes) → 768 bytes (uint8)
// 1M vectors: 3GB → 768MB
});
```
**Use Cases**:
- Production applications requiring high accuracy
- Medium-scale deployments (10K-1M vectors)
- General-purpose optimization
**Performance**:
- Memory: 4x smaller
- Search Speed: 3x faster
- Accuracy: 98-99% of original
### 3. Product Quantization (8-16x Reduction)
**Best For**: High-dimensional vectors, balanced compression
**Trade-off**: ~3-7% accuracy loss, 8-16x memory reduction, 5x faster
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'product',
// 768-dim float32 (3072 bytes) → 48-96 bytes
// 1M vectors: 3GB → 192MB
});
```
**Use Cases**:
- High-dimensional embeddings (>512 dims)
- Image/video embeddings
- Large-scale similarity search
**Performance**:
- Memory: 8-16x smaller
- Search Speed: 5x faster
- Accuracy: 93-97% of original
### 4. No Quantization (Full Precision)
**Best For**: Maximum accuracy, small datasets
**Trade-off**: No accuracy loss, full memory usage
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'none',
// Full float32 precision
});
```
---
## HNSW Indexing
**Hierarchical Navigable Small World** - O(log n) search complexity
### Automatic HNSW
AgentDB automatically builds HNSW indices:
```typescript
const adapter = await createAgentDBAdapter({
dbPath: '.agentdb/vectors.db',
// HNSW automatically enabled
});
// Search with HNSW (100µs vs 15ms linear scan)
const results = await adapter.retrieveWithReasoning(queryEmbedding, {
k: 10,
});
```
### HNSW Parameters
```typescript
// Advanced HNSW configuration
const adapter = await createAgentDBAdapter({
dbPath: '.agentdb/vectors.db',
hnswM: 16, // Connections per layer (default: 16)
hnswEfConstruction: 200, // Build quality (default: 200)
hnswEfSearch: 100, // Search quality (default: 100)
});
```
**Parameter Tuning**:
- **M** (connections): Higher = better recall, more memory
- Small datasets (<10K): M = 8
- Medium datasets (10K-100K): M = 16
- Large datasets (>100K): M = 32
- **efConstruction**: Higher = better index quality, slower build
- Fast build: 100
- Balanced: 200 (default)
- High quality: 400
- **efSearch**: Higher = better recall, slower search
- Fast search: 50
- Balanced: 100 (default)
- High recall: 200
---
## Caching Strategies
### In-Memory Pattern Cache
```typescript
const adapter = await createAgentDBAdapter({
cacheSize: 1000, // Cache 1000 most-used patterns
});
// First retrieval: ~2ms (database)
// Subsequent: <1ms (cache hit)
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
k: 10,
});
```
**Cache Tuning**:
- Small applications: 100-500 patterns
- Medium applications: 500-2000 patterns
- Large applications: 2000-5000 patterns
### LRU Cache Behavior
```typescript
// Cache automatically evicts least-recently-used patterns
// Most frequently accessed patterns stay in cache
// Monitor cache performance
const stats = await adapter.getStats();
console.log('Cache Hit Rate:', stats.cacheHitRate);
// Aim for >80% hit rate
```
---
## Batch Operations
### Batch Insert (500x Faster)
```typescript
// ❌ SLOW: Individual inserts
for (const doc of documents) {
await adapter.insertPattern({ /* ... */ }); // 1s for 100 docs
}
// ✅ FAST: Batch insert
const patterns = documents.map(doc => ({
id: '',
type: 'document',
domain: 'knowledge',
pattern_data: JSON.stringify({
embedding: doc.embedding,
text: doc.text,
}),
confidence: 1.0,
usage_count: 0,
success_count: 0,
created_at: Date.now(),
last_used: Date.now(),
}));
// Insert all at once (2ms for 100 docs)
for (const pattern of patterns) {
await adapter.insertPattern(pattern);
}
```
### Batch Retrieval
```typescript
// Retrieve multiple queries efficiently
const queries = [queryEmbedding1, queryEmbedding2, queryEmbedding3];
// Parallel retrieval
const results = await Promise.all(
queries.map(q => adapter.retrieveWithReasoning(q, { k: 5 }))
);
```
---
## Memory Optimization
### Automatic Consolidation
```typescript
// Enable automatic pattern consolidation
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'documents',
optimizeMemory: true, // Consolidate similar patterns
k: 10,
});
console.log('Optimizations:', result.optimizations);
// {
// consolidated: 15, // Merged 15 similar patterns
// pruned: 3, // Removed 3 low-quality patterns
// improved_quality: 0.12 // 12% quality improvement
// }
```
### Manual Optimization
```typescript
// Manually trigger optimization
await adapter.optimize();
// Get statistics
const stats = await adapter.getStats();
console.log('Before:', stats.totalPatterns);
console.log('After:', stats.totalPatterns); // Reduced by ~10-30%
```
### Pruning Strategies
```typescript
// Prune low-confidence patterns
await adapter.prune({
minConfidence: 0.5, // Remove confidence < 0.5
minUsageCount: 2, // Remove usage_count < 2
maxAge: 30 * 24 * 3600, // Remove >30 days old
});
```
---
## Performance Monitoring
### Database Statistics
```bash
# Get comprehensive stats
npx agentdb@latest stats .agentdb/vectors.db
# Output:
# Total Patterns: 125,430
# Database Size: 47.2 MB (with binary quantization)
# Avg Confidence: 0.87
# Domains: 15
# Cache Hit Rate: 84%
# Index Type: HNSW
```
### Runtime Metrics
```typescript
const stats = await adapter.getStats();
console.log('Performance Metrics:');
console.log('Total Patterns:', stats.totalPatterns);
console.log('Database Size:', stats.dbSize);
console.log('Avg Confidence:', stats.avgConfidence);
console.log('Cache Hit Rate:', stats.cacheHitRate);
console.log('Search Latency (avg):', stats.avgSearchLatency);
console.log('Insert Latency (avg):', stats.avgInsertLatency);
```
---
## Optimization Recipes
### Recipe 1: Maximum Speed (Sacrifice Accuracy)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'binary', // 32x memory reduction
cacheSize: 5000, // Large cache
hnswM: 8, // Fewer connections = faster
hnswEfSearch: 50, // Low search quality = faster
});
// Expected: <50µs search, 90-95% accuracy
```
### Recipe 2: Balanced Performance
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'scalar', // 4x memory reduction
cacheSize: 1000, // Standard cache
hnswM: 16, // Balanced connections
hnswEfSearch: 100, // Balanced quality
});
// Expected: <100µs search, 98-99% accuracy
```
### Recipe 3: Maximum Accuracy
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'none', // No quantization
cacheSize: 2000, // Large cache
hnswM: 32, // Many connections
hnswEfSearch: 200, // High search quality
});
// Expected: <200µs search, 100% accuracy
```
### Recipe 4: Memory-Constrained (Mobile/Edge)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'binary', // 32x memory reduction
cacheSize: 100, // Small cache
hnswM: 8, // Minimal connections
});
// Expected: <100µs search, ~10MB for 100K vectors
```
---
## Scaling Strategies
### Small Scale (<10K vectors)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'none', // Full precision
cacheSize: 500,
hnswM: 8,
});
```
### Medium Scale (10K-100K vectors)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'scalar', // 4x reduction
cacheSize: 1000,
hnswM: 16,
});
```
### Large Scale (100K-1M vectors)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'binary', // 32x reduction
cacheSize: 2000,
hnswM: 32,
});
```
### Massive Scale (>1M vectors)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'product', // 8-16x reduction
cacheSize: 5000,
hnswM: 48,
hnswEfConstruction: 400,
});
```
---
## Troubleshooting
### Issue: High memory usage
```bash
# Check database size
npx agentdb@latest stats .agentdb/vectors.db
# Enable quantization
# Use 'binary' for 32x reduction
```
### Issue: Slow search performance
```typescript
// Increase cache size
const adapter = await createAgentDBAdapter({
cacheSize: 2000, // Increase from 1000
});
// Reduce search quality (faster)
const result = await adapter.retrieveWithReasoning(queryEmbedding, {
k: 5, // Reduce from 10
});
```
### Issue: Low accuracy
```typescript
// Disable or use lighter quantization
const adapter = await createAgentDBAdapter({
quantizationType: 'scalar', // Instead of 'binary'
hnswEfSearch: 200, // Higher search quality
});
```
---
## Performance Benchmarks
**Test System**: AMD Ryzen 9 5950X, 64GB RAM
| Operation | Vector Count | No Optimization | Optimized | Improvement |
|-----------|-------------|-----------------|-----------|-------------|
| Search | 10K | 15ms | 100µs | 150x |
| Search | 100K | 150ms | 120µs | 1,250x |
| Search | 1M | 100s | 8ms | 12,500x |
| Batch Insert (100) | - | 1s | 2ms | 500x |
| Memory Usage | 1M | 3GB | 96MB | 32x (binary) |
---
## Learn More
- **Quantization Paper**: docs/quantization-techniques.pdf
- **HNSW Algorithm**: docs/hnsw-index.pdf
- **GitHub**: https://github.com/ruvnet/agentic-flow/tree/main/packages/agentdb
- **Website**: https://agentdb.ruv.io
---
**Category**: Performance / Optimization
**Difficulty**: Intermediate
**Estimated Time**: 20-30 minutes
@@ -1,339 +0,0 @@
---
name: "AgentDB Vector Search"
description: "Implement semantic vector search with AgentDB for intelligent document retrieval, similarity matching, and context-aware querying. Use when building RAG systems, semantic search engines, or intelligent knowledge bases."
---
# AgentDB Vector Search
## What This Skill Does
Implements vector-based semantic search using AgentDB's high-performance vector database with **150x-12,500x faster** operations than traditional solutions. Features HNSW indexing, quantization, and sub-millisecond search (<100µs).
## Prerequisites
- Node.js 18+
- AgentDB v1.0.7+ (via agentic-flow or standalone)
- OpenAI API key (for embeddings) or custom embedding model
## Quick Start with CLI
### Initialize Vector Database
```bash
# Initialize with default dimensions (1536 for OpenAI ada-002)
npx agentdb@latest init ./vectors.db
# Custom dimensions for different embedding models
npx agentdb@latest init ./vectors.db --dimension 768 # sentence-transformers
npx agentdb@latest init ./vectors.db --dimension 384 # all-MiniLM-L6-v2
# Use preset configurations
npx agentdb@latest init ./vectors.db --preset small # <10K vectors
npx agentdb@latest init ./vectors.db --preset medium # 10K-100K vectors
npx agentdb@latest init ./vectors.db --preset large # >100K vectors
# In-memory database for testing
npx agentdb@latest init ./vectors.db --in-memory
```
### Query Vector Database
```bash
# Basic similarity search
npx agentdb@latest query ./vectors.db "[0.1,0.2,0.3,...]"
# Top-k results
npx agentdb@latest query ./vectors.db "[0.1,0.2,0.3]" -k 10
# With similarity threshold (cosine similarity)
npx agentdb@latest query ./vectors.db "0.1 0.2 0.3" -t 0.75 -m cosine
# Different distance metrics
npx agentdb@latest query ./vectors.db "[...]" -m euclidean # L2 distance
npx agentdb@latest query ./vectors.db "[...]" -m dot # Dot product
# JSON output for automation
npx agentdb@latest query ./vectors.db "[...]" -f json -k 5
# Verbose output with distances
npx agentdb@latest query ./vectors.db "[...]" -v
```
### Import/Export Vectors
```bash
# Export vectors to JSON
npx agentdb@latest export ./vectors.db ./backup.json
# Import vectors from JSON
npx agentdb@latest import ./backup.json
# Get database statistics
npx agentdb@latest stats ./vectors.db
```
## Quick Start with API
```typescript
import { createAgentDBAdapter, computeEmbedding } from 'agentic-flow/reasoningbank';
// Initialize with vector search optimizations
const adapter = await createAgentDBAdapter({
dbPath: '.agentdb/vectors.db',
enableLearning: false, // Vector search only
enableReasoning: true, // Enable semantic matching
quantizationType: 'binary', // 32x memory reduction
cacheSize: 1000, // Fast retrieval
});
// Store document with embedding
const text = "The quantum computer achieved 100 qubits";
const embedding = await computeEmbedding(text);
await adapter.insertPattern({
id: '',
type: 'document',
domain: 'technology',
pattern_data: JSON.stringify({
embedding,
text,
metadata: { category: "quantum", date: "2025-01-15" }
}),
confidence: 1.0,
usage_count: 0,
success_count: 0,
created_at: Date.now(),
last_used: Date.now(),
});
// Semantic search with MMR (Maximal Marginal Relevance)
const queryEmbedding = await computeEmbedding("quantum computing advances");
const results = await adapter.retrieveWithReasoning(queryEmbedding, {
domain: 'technology',
k: 10,
useMMR: true, // Diverse results
synthesizeContext: true, // Rich context
});
```
## Core Features
### 1. Vector Storage
```typescript
// Store with automatic embedding
await db.storeWithEmbedding({
content: "Your document text",
metadata: { source: "docs", page: 42 }
});
```
### 2. Similarity Search
```typescript
// Find similar documents
const similar = await db.findSimilar("quantum computing", {
limit: 5,
minScore: 0.75
});
```
### 3. Hybrid Search (Vector + Metadata)
```typescript
// Combine vector similarity with metadata filtering
const results = await db.hybridSearch({
query: "machine learning models",
filters: {
category: "research",
date: { $gte: "2024-01-01" }
},
limit: 20
});
```
## Advanced Usage
### RAG (Retrieval Augmented Generation)
```typescript
// Build RAG pipeline
async function ragQuery(question: string) {
// 1. Get relevant context
const context = await db.searchSimilar(
await embed(question),
{ limit: 5, threshold: 0.7 }
);
// 2. Generate answer with context
const prompt = `Context: ${context.map(c => c.text).join('\n')}
Question: ${question}`;
return await llm.generate(prompt);
}
```
### Batch Operations
```typescript
// Efficient batch storage
await db.batchStore(documents.map(doc => ({
text: doc.content,
embedding: doc.vector,
metadata: doc.meta
})));
```
## MCP Server Integration
```bash
# Start AgentDB MCP server for Codex
npx agentdb@latest mcp
# Add to Codex (one-time setup)
Codex mcp add agentdb npx agentdb@latest mcp
# Now use MCP tools in Codex:
# - agentdb_query: Semantic vector search
# - agentdb_store: Store documents with embeddings
# - agentdb_stats: Database statistics
```
## Performance Benchmarks
```bash
# Run comprehensive benchmarks
npx agentdb@latest benchmark
# Results:
# ✅ Pattern Search: 150x faster (100µs vs 15ms)
# ✅ Batch Insert: 500x faster (2ms vs 1s for 100 vectors)
# ✅ Large-scale Query: 12,500x faster (8ms vs 100s at 1M vectors)
# ✅ Memory Efficiency: 4-32x reduction with quantization
```
## Quantization Options
AgentDB provides multiple quantization strategies for memory efficiency:
### Binary Quantization (32x reduction)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'binary', // 768-dim → 96 bytes
});
```
### Scalar Quantization (4x reduction)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'scalar', // 768-dim → 768 bytes
});
```
### Product Quantization (8-16x reduction)
```typescript
const adapter = await createAgentDBAdapter({
quantizationType: 'product', // 768-dim → 48-96 bytes
});
```
## Distance Metrics
```bash
# Cosine similarity (default, best for most use cases)
npx agentdb@latest query ./db.sqlite "[...]" -m cosine
# Euclidean distance (L2 norm)
npx agentdb@latest query ./db.sqlite "[...]" -m euclidean
# Dot product (for normalized vectors)
npx agentdb@latest query ./db.sqlite "[...]" -m dot
```
## Advanced Features
### HNSW Indexing
- **O(log n) search complexity**
- **Sub-millisecond retrieval** (<100µs)
- **Automatic index building**
### Caching
- **1000 pattern in-memory cache**
- **<1ms pattern retrieval**
- **Automatic cache invalidation**
### MMR (Maximal Marginal Relevance)
- **Diverse result sets**
- **Avoid redundancy**
- **Balance relevance and diversity**
## Performance Tips
1. **Enable HNSW indexing**: Automatic with AgentDB, 10-100x faster
2. **Use quantization**: Binary (32x), Scalar (4x), Product (8-16x) memory reduction
3. **Batch operations**: 500x faster for bulk inserts
4. **Match dimensions**: 1536 (OpenAI), 768 (sentence-transformers), 384 (MiniLM)
5. **Similarity threshold**: Start at 0.7 for quality, adjust based on use case
6. **Enable caching**: 1000 pattern cache for frequent queries
## Troubleshooting
### Issue: Slow search performance
```bash
# Check if HNSW indexing is enabled (automatic)
npx agentdb@latest stats ./vectors.db
# Expected: <100µs search time
```
### Issue: High memory usage
```bash
# Enable binary quantization (32x reduction)
# Use in adapter: quantizationType: 'binary'
```
### Issue: Poor relevance
```bash
# Adjust similarity threshold
npx agentdb@latest query ./db.sqlite "[...]" -t 0.8 # Higher threshold
# Or use MMR for diverse results
# Use in adapter: useMMR: true
```
### Issue: Wrong dimensions
```bash
# Check embedding model dimensions:
# - OpenAI ada-002: 1536
# - sentence-transformers: 768
# - all-MiniLM-L6-v2: 384
npx agentdb@latest init ./db.sqlite --dimension 768
```
## Database Statistics
```bash
# Get comprehensive stats
npx agentdb@latest stats ./vectors.db
# Shows:
# - Total patterns/vectors
# - Database size
# - Average confidence
# - Domains distribution
# - Index status
```
## Performance Characteristics
- **Vector Search**: <100µs (HNSW indexing)
- **Pattern Retrieval**: <1ms (with cache)
- **Batch Insert**: 2ms for 100 vectors
- **Memory Efficiency**: 4-32x reduction with quantization
- **Scalability**: Handles 1M+ vectors efficiently
- **Latency**: Sub-millisecond for most operations
## Learn More
- GitHub: https://github.com/ruvnet/agentic-flow/tree/main/packages/agentdb
- Documentation: node_modules/agentic-flow/docs/AGENTDB_INTEGRATION.md
- MCP Integration: `npx agentdb@latest mcp` for Codex
- Website: https://agentdb.ruv.io
- CLI Help: `npx agentdb@latest --help`
- Command Help: `npx agentdb@latest help <command>`
-204
View File
@@ -1,204 +0,0 @@
---
name: browser
description: Web browser automation with AI-optimized snapshots for Codex-flow agents
version: 1.0.0
triggers:
- /browser
- browse
- web automation
- scrape
- navigate
- screenshot
tools:
- browser/open
- browser/snapshot
- browser/click
- browser/fill
- browser/screenshot
- browser/close
---
# Browser Automation Skill
Web browser automation using agent-browser with AI-optimized snapshots. Reduces context by 93% using element refs (@e1, @e2) instead of full DOM.
## Core Workflow
```bash
# 1. Navigate to page
agent-browser open <url>
# 2. Get accessibility tree with element refs
agent-browser snapshot -i # -i = interactive elements only
# 3. Interact using refs from snapshot
agent-browser click @e2
agent-browser fill @e3 "text"
# 4. Re-snapshot after page changes
agent-browser snapshot -i
```
## Quick Reference
### Navigation
| Command | Description |
|---------|-------------|
| `open <url>` | Navigate to URL |
| `back` | Go back |
| `forward` | Go forward |
| `reload` | Reload page |
| `close` | Close browser |
### Snapshots (AI-Optimized)
| Command | Description |
|---------|-------------|
| `snapshot` | Full accessibility tree |
| `snapshot -i` | Interactive elements only (buttons, links, inputs) |
| `snapshot -c` | Compact (remove empty elements) |
| `snapshot -d 3` | Limit depth to 3 levels |
| `screenshot [path]` | Capture screenshot (base64 if no path) |
### Interaction
| Command | Description |
|---------|-------------|
| `click <sel>` | Click element |
| `fill <sel> <text>` | Clear and fill input |
| `type <sel> <text>` | Type with key events |
| `press <key>` | Press key (Enter, Tab, etc.) |
| `hover <sel>` | Hover element |
| `select <sel> <val>` | Select dropdown option |
| `check/uncheck <sel>` | Toggle checkbox |
| `scroll <dir> [px]` | Scroll page |
### Get Info
| Command | Description |
|---------|-------------|
| `get text <sel>` | Get text content |
| `get html <sel>` | Get innerHTML |
| `get value <sel>` | Get input value |
| `get attr <sel> <attr>` | Get attribute |
| `get title` | Get page title |
| `get url` | Get current URL |
### Wait
| Command | Description |
|---------|-------------|
| `wait <selector>` | Wait for element |
| `wait <ms>` | Wait milliseconds |
| `wait --text "text"` | Wait for text |
| `wait --url "pattern"` | Wait for URL |
| `wait --load networkidle` | Wait for load state |
### Sessions
| Command | Description |
|---------|-------------|
| `--session <name>` | Use isolated session |
| `session list` | List active sessions |
## Selectors
### Element Refs (Recommended)
```bash
# Get refs from snapshot
agent-browser snapshot -i
# Output: button "Submit" [ref=e2]
# Use ref to interact
agent-browser click @e2
```
### CSS Selectors
```bash
agent-browser click "#submit"
agent-browser fill ".email-input" "test@test.com"
```
### Semantic Locators
```bash
agent-browser find role button click --name "Submit"
agent-browser find label "Email" fill "test@test.com"
agent-browser find testid "login-btn" click
```
## Examples
### Login Flow
```bash
agent-browser open https://example.com/login
agent-browser snapshot -i
agent-browser fill @e2 "user@example.com"
agent-browser fill @e3 "password123"
agent-browser click @e4
agent-browser wait --url "**/dashboard"
```
### Form Submission
```bash
agent-browser open https://example.com/contact
agent-browser snapshot -i
agent-browser fill @e1 "John Doe"
agent-browser fill @e2 "john@example.com"
agent-browser fill @e3 "Hello, this is my message"
agent-browser click @e4
agent-browser wait --text "Thank you"
```
### Data Extraction
```bash
agent-browser open https://example.com/products
agent-browser snapshot -i
# Iterate through product refs
agent-browser get text @e1 # Product name
agent-browser get text @e2 # Price
agent-browser get attr @e3 href # Link
```
### Multi-Session (Swarm)
```bash
# Session 1: Navigator
agent-browser --session nav open https://example.com
agent-browser --session nav state save auth.json
# Session 2: Scraper (uses same auth)
agent-browser --session scrape state load auth.json
agent-browser --session scrape open https://example.com/data
agent-browser --session scrape snapshot -i
```
## Integration with Codex Flow
### MCP Tools
All browser operations are available as MCP tools with `browser/` prefix:
- `browser/open`
- `browser/snapshot`
- `browser/click`
- `browser/fill`
- `browser/screenshot`
- etc.
### Memory Integration
```bash
# Store successful patterns
npx @Codex-flow/cli memory store --namespace browser-patterns --key "login-flow" --value "snapshot->fill->click->wait"
# Retrieve before similar task
npx @Codex-flow/cli memory search --query "login automation"
```
### Hooks
```bash
# Pre-browse hook (get context)
npx @Codex-flow/cli hooks pre-edit --file "browser-task.ts"
# Post-browse hook (record success)
npx @Codex-flow/cli hooks post-task --task-id "browse-1" --success true
```
## Tips
1. **Always use snapshots** - They're optimized for AI with refs
2. **Prefer `-i` flag** - Gets only interactive elements, smaller output
3. **Use refs, not selectors** - More reliable, deterministic
4. **Re-snapshot after navigation** - Page state changes
5. **Use sessions for parallel work** - Each session is isolated
File diff suppressed because it is too large Load Diff
-874
View File
@@ -1,874 +0,0 @@
---
name: github-multi-repo
version: 1.0.0
description: Multi-repository coordination, synchronization, and architecture management with AI swarm orchestration
category: github-integration
tags: [multi-repo, synchronization, architecture, coordination, github]
author: Codex Flow Team
requires:
- ruv-swarm@^1.0.11
- gh-cli@^2.0.0
capabilities:
- cross-repository coordination
- package synchronization
- architecture optimization
- template management
- distributed workflows
---
# GitHub Multi-Repository Coordination Skill
## Overview
Advanced multi-repository coordination system that combines swarm intelligence, package synchronization, and repository architecture optimization. This skill enables organization-wide automation, cross-project collaboration, and scalable repository management.
## Core Capabilities
### 🔄 Multi-Repository Swarm Coordination
Cross-repository AI swarm orchestration for distributed development workflows.
### 📦 Package Synchronization
Intelligent dependency resolution and version alignment across multiple packages.
### 🏗️ Repository Architecture
Structure optimization and template management for scalable projects.
### 🔗 Integration Management
Cross-package integration testing and deployment coordination.
## Quick Start
### Initialize Multi-Repo Coordination
```bash
# Basic swarm initialization
npx Codex-flow skill run github-multi-repo init \
--repos "org/frontend,org/backend,org/shared" \
--topology hierarchical
# Advanced initialization with synchronization
npx Codex-flow skill run github-multi-repo init \
--repos "org/frontend,org/backend,org/shared" \
--topology mesh \
--shared-memory \
--sync-strategy eventual
```
### Synchronize Packages
```bash
# Synchronize package versions and dependencies
npx Codex-flow skill run github-multi-repo sync \
--packages "Codex-flow,ruv-swarm" \
--align-versions \
--update-docs
```
### Optimize Architecture
```bash
# Analyze and optimize repository structure
npx Codex-flow skill run github-multi-repo optimize \
--analyze-structure \
--suggest-improvements \
--create-templates
```
## Features
### 1. Cross-Repository Swarm Orchestration
#### Repository Discovery
```javascript
// Auto-discover related repositories with gh CLI
const REPOS = Bash(`gh repo list my-organization --limit 100 \
--json name,description,languages,topics \
--jq '.[] | select(.languages | keys | contains(["TypeScript"]))'`)
// Analyze repository dependencies
const DEPS = Bash(`gh repo list my-organization --json name | \
jq -r '.[].name' | while read -r repo; do
gh api repos/my-organization/$repo/contents/package.json \
--jq '.content' 2>/dev/null | base64 -d | jq '{name, dependencies}'
done | jq -s '.'`)
// Initialize swarm with discovered repositories
mcp__claude-flow__swarm_init({
topology: "hierarchical",
maxAgents: 8,
metadata: { repos: REPOS, dependencies: DEPS }
})
```
#### Synchronized Operations
```javascript
// Execute synchronized changes across repositories
[Parallel Multi-Repo Operations]:
// Spawn coordination agents
Task("Repository Coordinator", "Coordinate changes across all repositories", "coordinator")
Task("Dependency Analyzer", "Analyze cross-repo dependencies", "analyst")
Task("Integration Tester", "Validate cross-repo changes", "tester")
// Get matching repositories
Bash(`gh repo list org --limit 100 --json name \
--jq '.[] | select(.name | test("-service$")) | .name' > /tmp/repos.txt`)
// Execute task across repositories
Bash(`cat /tmp/repos.txt | while read -r repo; do
gh repo clone org/$repo /tmp/$repo -- --depth=1
cd /tmp/$repo
# Apply changes
npm update
npm test
# Create PR if successful
if [ $? -eq 0 ]; then
git checkout -b update-dependencies-$(date +%Y%m%d)
git add -A
git commit -m "chore: Update dependencies"
git push origin HEAD
gh pr create --title "Update dependencies" --body "Automated update" --label "dependencies"
fi
done`)
// Track all operations
TodoWrite { todos: [
{ id: "discover", content: "Discover all service repositories", status: "completed" },
{ id: "update", content: "Update dependencies", status: "completed" },
{ id: "test", content: "Run integration tests", status: "in_progress" },
{ id: "pr", content: "Create pull requests", status: "pending" }
]}
```
### 2. Package Synchronization
#### Version Alignment
```javascript
// Synchronize package dependencies and versions
[Complete Package Sync]:
// Initialize sync swarm
mcp__claude-flow__swarm_init({ topology: "mesh", maxAgents: 5 })
// Spawn sync agents
Task("Sync Coordinator", "Coordinate version alignment", "coordinator")
Task("Dependency Analyzer", "Analyze dependencies", "analyst")
Task("Integration Tester", "Validate synchronization", "tester")
// Read package states
Read("/workspaces/ruv-FANN/Codex-flow/Codex-flow/package.json")
Read("/workspaces/ruv-FANN/ruv-swarm/npm/package.json")
// Align versions using gh CLI
Bash(`gh api repos/:owner/:repo/git/refs \
-f ref='refs/heads/sync/package-alignment' \
-f sha=$(gh api repos/:owner/:repo/git/refs/heads/main --jq '.object.sha')`)
// Update package.json files
Bash(`gh api repos/:owner/:repo/contents/package.json \
--method PUT \
-f message="feat: Align Node.js version requirements" \
-f branch="sync/package-alignment" \
-f content="$(cat aligned-package.json | base64)"`)
// Store sync state
mcp__claude-flow__memory_usage({
action: "store",
key: "sync/packages/status",
value: {
timestamp: Date.now(),
packages_synced: ["Codex-flow", "ruv-swarm"],
status: "synchronized"
}
})
```
#### Documentation Synchronization
```javascript
// Synchronize AGENTS.md files across packages
[Documentation Sync]:
// Get source documentation
Bash(`gh api repos/:owner/:repo/contents/ruv-swarm/docs/AGENTS.md \
--jq '.content' | base64 -d > /tmp/Codex-source.md`)
// Update target documentation
Bash(`gh api repos/:owner/:repo/contents/Codex-flow/AGENTS.md \
--method PUT \
-f message="docs: Synchronize AGENTS.md" \
-f branch="sync/documentation" \
-f content="$(cat /tmp/Codex-source.md | base64)"`)
// Track sync status
mcp__claude-flow__memory_usage({
action: "store",
key: "sync/documentation/status",
value: { status: "synchronized", files: ["AGENTS.md"] }
})
```
#### Cross-Package Integration
```javascript
// Coordinate feature implementation across packages
[Cross-Package Feature]:
// Push changes to all packages
mcp__github__push_files({
branch: "feature/github-integration",
files: [
{
path: "Codex-flow/.Codex/commands/github/github-modes.md",
content: "[GitHub modes documentation]"
},
{
path: "ruv-swarm/src/github-coordinator/hooks.js",
content: "[GitHub coordination hooks]"
}
],
message: "feat: Add GitHub workflow integration"
})
// Create coordinated PR
Bash(`gh pr create \
--title "Feature: GitHub Workflow Integration" \
--body "## 🚀 GitHub Integration
### Features
- ✅ Multi-repo coordination
- ✅ Package synchronization
- ✅ Architecture optimization
### Testing
- [x] Package dependency verification
- [x] Integration tests
- [x] Cross-package compatibility"`)
```
### 3. Repository Architecture
#### Structure Analysis
```javascript
// Analyze and optimize repository structure
[Architecture Analysis]:
// Initialize architecture swarm
mcp__claude-flow__swarm_init({ topology: "hierarchical", maxAgents: 6 })
// Spawn architecture agents
Task("Senior Architect", "Analyze repository structure", "architect")
Task("Structure Analyst", "Identify optimization opportunities", "analyst")
Task("Performance Optimizer", "Optimize structure for scalability", "optimizer")
Task("Best Practices Researcher", "Research architecture patterns", "researcher")
// Analyze current structures
LS("/workspaces/ruv-FANN/Codex-flow/Codex-flow")
LS("/workspaces/ruv-FANN/ruv-swarm/npm")
// Search for best practices
Bash(`gh search repos "language:javascript template architecture" \
--limit 10 \
--json fullName,description,stargazersCount \
--sort stars \
--order desc`)
// Store analysis results
mcp__claude-flow__memory_usage({
action: "store",
key: "architecture/analysis/results",
value: {
repositories_analyzed: ["Codex-flow", "ruv-swarm"],
optimization_areas: ["structure", "workflows", "templates"],
recommendations: ["standardize_structure", "improve_workflows"]
}
})
```
#### Template Creation
```javascript
// Create standardized repository template
[Template Creation]:
// Create template repository
mcp__github__create_repository({
name: "Codex-project-template",
description: "Standardized template for Codex projects",
private: false,
autoInit: true
})
// Push template structure
mcp__github__push_files({
repo: "Codex-project-template",
files: [
{
path: ".Codex/commands/github/github-modes.md",
content: "[GitHub modes template]"
},
{
path: ".Codex/config.json",
content: JSON.stringify({
version: "1.0",
mcp_servers: {
"ruv-swarm": {
command: "npx",
args: ["ruv-swarm", "mcp", "start"]
}
}
})
},
{
path: "AGENTS.md",
content: "[Standardized AGENTS.md]"
},
{
path: "package.json",
content: JSON.stringify({
name: "Codex-project-template",
engines: { node: ">=20.0.0" },
dependencies: { "ruv-swarm": "^1.0.11" }
})
}
],
message: "feat: Create standardized template"
})
```
#### Cross-Repository Standardization
```javascript
// Synchronize structure across repositories
[Structure Standardization]:
const repositories = ["Codex-flow", "ruv-swarm", "Codex-extensions"]
// Update common files across all repositories
repositories.forEach(repo => {
mcp__github__create_or_update_file({
repo: "ruv-FANN",
path: `${repo}/.github/workflows/integration.yml`,
content: `name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with: { node-version: '20' }
- run: npm install && npm test`,
message: "ci: Standardize integration workflow",
branch: "structure/standardization"
})
})
```
### 4. Orchestration Workflows
#### Dependency Management
```javascript
// Update dependencies across all repositories
[Organization-Wide Dependency Update]:
// Create tracking issue
TRACKING_ISSUE=$(Bash(`gh issue create \
--title "Dependency Update: typescript@5.0.0" \
--body "Tracking TypeScript update across all repositories" \
--label "dependencies,tracking" \
--json number -q .number`))
// Find all TypeScript repositories
TS_REPOS=$(Bash(`gh repo list org --limit 100 --json name | \
jq -r '.[].name' | while read -r repo; do
if gh api repos/org/$repo/contents/package.json 2>/dev/null | \
jq -r '.content' | base64 -d | grep -q '"typescript"'; then
echo "$repo"
fi
done`))
// Update each repository
Bash(`echo "$TS_REPOS" | while read -r repo; do
gh repo clone org/$repo /tmp/$repo -- --depth=1
cd /tmp/$repo
npm install --save-dev typescript@5.0.0
if npm test; then
git checkout -b update-typescript-5
git add package.json package-lock.json
git commit -m "chore: Update TypeScript to 5.0.0
Part of #$TRACKING_ISSUE"
git push origin HEAD
gh pr create \
--title "Update TypeScript to 5.0.0" \
--body "Updates TypeScript\n\nTracking: #$TRACKING_ISSUE" \
--label "dependencies"
else
gh issue comment $TRACKING_ISSUE \
--body "❌ Failed to update $repo - tests failing"
fi
done`)
```
#### Refactoring Operations
```javascript
// Coordinate large-scale refactoring
[Cross-Repo Refactoring]:
// Initialize refactoring swarm
mcp__claude-flow__swarm_init({ topology: "mesh", maxAgents: 8 })
// Spawn specialized agents
Task("Refactoring Coordinator", "Coordinate refactoring across repos", "coordinator")
Task("Impact Analyzer", "Analyze refactoring impact", "analyst")
Task("Code Transformer", "Apply refactoring changes", "coder")
Task("Migration Guide Creator", "Create migration documentation", "documenter")
Task("Integration Tester", "Validate refactored code", "tester")
// Execute refactoring
mcp__claude-flow__task_orchestrate({
task: "Rename OldAPI to NewAPI across all repositories",
strategy: "sequential",
priority: "high"
})
```
#### Security Updates
```javascript
// Coordinate security patches
[Security Patch Deployment]:
// Scan all repositories
Bash(`gh repo list org --limit 100 --json name | jq -r '.[].name' | \
while read -r repo; do
gh repo clone org/$repo /tmp/$repo -- --depth=1
cd /tmp/$repo
npm audit --json > /tmp/audit-$repo.json
done`)
// Apply patches
Bash(`for repo in /tmp/audit-*.json; do
if [ $(jq '.vulnerabilities | length' $repo) -gt 0 ]; then
cd /tmp/$(basename $repo .json | sed 's/audit-//')
npm audit fix
if npm test; then
git checkout -b security/patch-$(date +%Y%m%d)
git add -A
git commit -m "security: Apply security patches"
git push origin HEAD
gh pr create --title "Security patches" --label "security"
fi
fi
done`)
```
## Configuration
### Multi-Repo Config File
```yaml
# .swarm/multi-repo.yml
version: 1
organization: my-org
repositories:
- name: frontend
url: github.com/my-org/frontend
role: ui
agents: [coder, designer, tester]
- name: backend
url: github.com/my-org/backend
role: api
agents: [architect, coder, tester]
- name: shared
url: github.com/my-org/shared
role: library
agents: [analyst, coder]
coordination:
topology: hierarchical
communication: webhook
memory: redis://shared-memory
dependencies:
- from: frontend
to: [backend, shared]
- from: backend
to: [shared]
```
### Repository Roles
```javascript
{
"roles": {
"ui": {
"responsibilities": ["user-interface", "ux", "accessibility"],
"default-agents": ["designer", "coder", "tester"]
},
"api": {
"responsibilities": ["endpoints", "business-logic", "data"],
"default-agents": ["architect", "coder", "security"]
},
"library": {
"responsibilities": ["shared-code", "utilities", "types"],
"default-agents": ["analyst", "coder", "documenter"]
}
}
}
```
## Communication Strategies
### 1. Webhook-Based Coordination
```javascript
const { MultiRepoSwarm } = require('ruv-swarm');
const swarm = new MultiRepoSwarm({
webhook: {
url: 'https://swarm-coordinator.example.com',
secret: process.env.WEBHOOK_SECRET
}
});
swarm.on('repo:update', async (event) => {
await swarm.propagate(event, {
to: event.dependencies,
strategy: 'eventual-consistency'
});
});
```
### 2. Event Streaming
```yaml
# Kafka configuration for real-time coordination
kafka:
brokers: ['kafka1:9092', 'kafka2:9092']
topics:
swarm-events:
partitions: 10
replication: 3
swarm-memory:
partitions: 5
replication: 3
```
## Synchronization Patterns
### 1. Eventually Consistent
```javascript
{
"sync": {
"strategy": "eventual",
"max-lag": "5m",
"retry": {
"attempts": 3,
"backoff": "exponential"
}
}
}
```
### 2. Strong Consistency
```javascript
{
"sync": {
"strategy": "strong",
"consensus": "raft",
"quorum": 0.51,
"timeout": "30s"
}
}
```
### 3. Hybrid Approach
```javascript
{
"sync": {
"default": "eventual",
"overrides": {
"security-updates": "strong",
"dependency-updates": "strong",
"documentation": "eventual"
}
}
}
```
## Use Cases
### 1. Microservices Coordination
```bash
npx Codex-flow skill run github-multi-repo microservices \
--services "auth,users,orders,payments" \
--ensure-compatibility \
--sync-contracts \
--integration-tests
```
### 2. Library Updates
```bash
npx Codex-flow skill run github-multi-repo lib-update \
--library "org/shared-lib" \
--version "2.0.0" \
--find-consumers \
--update-imports \
--run-tests
```
### 3. Organization-Wide Changes
```bash
npx Codex-flow skill run github-multi-repo org-policy \
--policy "add-security-headers" \
--repos "org/*" \
--validate-compliance \
--create-reports
```
## Architecture Patterns
### Monorepo Structure
```
ruv-FANN/
├── packages/
│ ├── Codex-flow/
│ │ ├── src/
│ │ ├── .Codex/
│ │ └── package.json
│ ├── ruv-swarm/
│ │ ├── src/
│ │ ├── wasm/
│ │ └── package.json
│ └── shared/
│ ├── types/
│ ├── utils/
│ └── config/
├── tools/
│ ├── build/
│ ├── test/
│ └── deploy/
├── docs/
│ ├── architecture/
│ ├── integration/
│ └── examples/
└── .github/
├── workflows/
├── templates/
└── actions/
```
### Command Structure
```
.Codex/
├── commands/
│ ├── github/
│ │ ├── github-modes.md
│ │ ├── pr-manager.md
│ │ ├── issue-tracker.md
│ │ └── sync-coordinator.md
│ ├── sparc/
│ │ ├── sparc-modes.md
│ │ ├── coder.md
│ │ └── tester.md
│ └── swarm/
│ ├── coordination.md
│ └── orchestration.md
├── templates/
│ ├── issue.md
│ ├── pr.md
│ └── project.md
└── config.json
```
## Monitoring & Visualization
### Multi-Repo Dashboard
```bash
npx Codex-flow skill run github-multi-repo dashboard \
--port 3000 \
--metrics "agent-activity,task-progress,memory-usage" \
--real-time
```
### Dependency Graph
```bash
npx Codex-flow skill run github-multi-repo dep-graph \
--format mermaid \
--include-agents \
--show-data-flow
```
### Health Monitoring
```bash
npx Codex-flow skill run github-multi-repo health-check \
--repos "org/*" \
--check "connectivity,memory,agents" \
--alert-on-issues
```
## Best Practices
### 1. Repository Organization
- Clear repository roles and boundaries
- Consistent naming conventions
- Documented dependencies
- Shared configuration standards
### 2. Communication
- Use appropriate sync strategies
- Implement circuit breakers
- Monitor latency and failures
- Clear error propagation
### 3. Security
- Secure cross-repo authentication
- Encrypted communication channels
- Audit trail for all operations
- Principle of least privilege
### 4. Version Management
- Semantic versioning alignment
- Dependency compatibility validation
- Automated version bump coordination
### 5. Testing Integration
- Cross-package test validation
- Integration test automation
- Performance regression detection
## Performance Optimization
### Caching Strategy
```bash
npx Codex-flow skill run github-multi-repo cache-strategy \
--analyze-patterns \
--suggest-cache-layers \
--implement-invalidation
```
### Parallel Execution
```bash
npx Codex-flow skill run github-multi-repo parallel-optimize \
--analyze-dependencies \
--identify-parallelizable \
--execute-optimal
```
### Resource Pooling
```bash
npx Codex-flow skill run github-multi-repo resource-pool \
--share-agents \
--distribute-load \
--monitor-usage
```
## Troubleshooting
### Connectivity Issues
```bash
npx Codex-flow skill run github-multi-repo diagnose-connectivity \
--test-all-repos \
--check-permissions \
--verify-webhooks
```
### Memory Synchronization
```bash
npx Codex-flow skill run github-multi-repo debug-memory \
--check-consistency \
--identify-conflicts \
--repair-state
```
### Performance Bottlenecks
```bash
npx Codex-flow skill run github-multi-repo perf-analysis \
--profile-operations \
--identify-bottlenecks \
--suggest-optimizations
```
## Advanced Features
### 1. Distributed Task Queue
```bash
npx Codex-flow skill run github-multi-repo queue \
--backend redis \
--workers 10 \
--priority-routing \
--dead-letter-queue
```
### 2. Cross-Repo Testing
```bash
npx Codex-flow skill run github-multi-repo test \
--setup-test-env \
--link-services \
--run-e2e \
--tear-down
```
### 3. Monorepo Migration
```bash
npx Codex-flow skill run github-multi-repo to-monorepo \
--analyze-repos \
--suggest-structure \
--preserve-history \
--create-migration-prs
```
## Examples
### Full-Stack Application Update
```bash
npx Codex-flow skill run github-multi-repo fullstack-update \
--frontend "org/web-app" \
--backend "org/api-server" \
--database "org/db-migrations" \
--coordinate-deployment
```
### Cross-Team Collaboration
```bash
npx Codex-flow skill run github-multi-repo cross-team \
--teams "frontend,backend,devops" \
--task "implement-feature-x" \
--assign-by-expertise \
--track-progress
```
## Metrics and Reporting
### Sync Quality Metrics
- Package version alignment percentage
- Documentation consistency score
- Integration test success rate
- Synchronization completion time
### Architecture Health Metrics
- Repository structure consistency score
- Documentation coverage percentage
- Cross-repository integration success rate
- Template adoption and usage statistics
### Automated Reporting
- Weekly sync status reports
- Dependency drift detection
- Documentation divergence alerts
- Integration health monitoring
## Integration Points
### Related Skills
- `github-workflow` - GitHub workflow automation
- `github-pr` - Pull request management
- `sparc-architect` - Architecture design
- `sparc-optimizer` - Performance optimization
### Related Commands
- `/github sync-coordinator` - Cross-repo synchronization
- `/github release-manager` - Coordinated releases
- `/github repo-architect` - Repository optimization
- `/sparc architect` - Detailed architecture design
## Support and Resources
- Documentation: https://github.com/ruvnet/Codex-flow
- Issues: https://github.com/ruvnet/Codex-flow/issues
- Examples: `.Codex/examples/github-multi-repo/`
---
**Version:** 1.0.0
**Last Updated:** 2025-10-19
**Maintainer:** Codex Flow Team
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-126
View File
@@ -1,126 +0,0 @@
---
name: memory-management
description: >
AgentDB memory system with HNSW vector search. Provides 150x-12,500x faster pattern retrieval, persistent storage, and semantic search capabilities for learning and knowledge management.
Use when: need to store successful patterns, searching for similar solutions, semantic lookup of past work, learning from previous tasks, sharing knowledge between agents, building knowledge base.
Skip when: no learning needed, ephemeral one-off tasks, external data sources available, read-only exploration.
---
# Memory Management Skill
## Purpose
AgentDB memory system with HNSW vector search. Provides 150x-12,500x faster pattern retrieval, persistent storage, and semantic search capabilities for learning and knowledge management.
## When to Trigger
- need to store successful patterns
- searching for similar solutions
- semantic lookup of past work
- learning from previous tasks
- sharing knowledge between agents
- building knowledge base
## When to Skip
- no learning needed
- ephemeral one-off tasks
- external data sources available
- read-only exploration
## Commands
### Store Pattern
Store a pattern or knowledge item in memory
```bash
npx @claude-flow/cli memory store --key "[key]" --value "[value]" --namespace patterns
```
**Example:**
```bash
npx @claude-flow/cli memory store --key "auth-jwt-pattern" --value "JWT validation with refresh tokens" --namespace patterns
```
### Semantic Search
Search memory using semantic similarity
```bash
npx @claude-flow/cli memory search --query "[search terms]" --limit 10
```
**Example:**
```bash
npx @claude-flow/cli memory search --query "authentication best practices" --limit 5
```
### Retrieve Entry
Retrieve a specific memory entry by key
```bash
npx @claude-flow/cli memory get --key "[key]" --namespace [namespace]
```
**Example:**
```bash
npx @claude-flow/cli memory get --key "auth-jwt-pattern" --namespace patterns
```
### List Entries
List all entries in a namespace
```bash
npx @claude-flow/cli memory list --namespace [namespace]
```
**Example:**
```bash
npx @claude-flow/cli memory list --namespace patterns --limit 20
```
### Delete Entry
Delete a memory entry
```bash
npx @claude-flow/cli memory delete --key "[key]" --namespace [namespace]
```
### Initialize HNSW Index
Initialize HNSW vector search index
```bash
npx @claude-flow/cli memory init --enable-hnsw
```
### Memory Stats
Show memory usage statistics
```bash
npx @claude-flow/cli memory stats
```
### Export Memory
Export memory to JSON
```bash
npx @claude-flow/cli memory export --output memory-backup.json
```
## Scripts
| Script | Path | Description |
|--------|------|-------------|
| `memory-backup` | `.agents/scripts/memory-backup.sh` | Backup memory to external storage |
| `memory-consolidate` | `.agents/scripts/memory-consolidate.sh` | Consolidate and optimize memory |
## References
| Document | Path | Description |
|----------|------|-------------|
| `HNSW Guide` | `docs/hnsw.md` | HNSW vector search configuration |
| `Memory Schema` | `docs/memory-schema.md` | Memory namespace and schema reference |
## Best Practices
1. Check memory for existing patterns before starting
2. Use hierarchical topology for coordination
3. Store successful patterns after completion
4. Document any new learnings
File diff suppressed because it is too large Load Diff
@@ -1,446 +0,0 @@
---
name: "ReasoningBank with AgentDB"
description: "Implement ReasoningBank adaptive learning with AgentDB's 150x faster vector database. Includes trajectory tracking, verdict judgment, memory distillation, and pattern recognition. Use when building self-learning agents, optimizing decision-making, or implementing experience replay systems."
---
# ReasoningBank with AgentDB
## What This Skill Does
Provides ReasoningBank adaptive learning patterns using AgentDB's high-performance backend (150x-12,500x faster). Enables agents to learn from experiences, judge outcomes, distill memories, and improve decision-making over time with 100% backward compatibility.
**Performance**: 150x faster pattern retrieval, 500x faster batch operations, <1ms memory access.
## Prerequisites
- Node.js 18+
- AgentDB v1.0.7+ (via agentic-flow)
- Understanding of reinforcement learning concepts (optional)
---
## Quick Start with CLI
### Initialize ReasoningBank Database
```bash
# Initialize AgentDB for ReasoningBank
npx agentdb@latest init ./.agentdb/reasoningbank.db --dimension 1536
# Start MCP server for Codex integration
npx agentdb@latest mcp
Codex mcp add agentdb npx agentdb@latest mcp
```
### Migrate from Legacy ReasoningBank
```bash
# Automatic migration with validation
npx agentdb@latest migrate --source .swarm/memory.db
# Verify migration
npx agentdb@latest stats ./.agentdb/reasoningbank.db
```
---
## Quick Start with API
```typescript
import { createAgentDBAdapter, computeEmbedding } from 'agentic-flow/reasoningbank';
// Initialize ReasoningBank with AgentDB
const rb = await createAgentDBAdapter({
dbPath: '.agentdb/reasoningbank.db',
enableLearning: true, // Enable learning plugins
enableReasoning: true, // Enable reasoning agents
cacheSize: 1000, // 1000 pattern cache
});
// Store successful experience
const query = "How to optimize database queries?";
const embedding = await computeEmbedding(query);
await rb.insertPattern({
id: '',
type: 'experience',
domain: 'database-optimization',
pattern_data: JSON.stringify({
embedding,
pattern: {
query,
approach: 'indexing + query optimization',
outcome: 'success',
metrics: { latency_reduction: 0.85 }
}
}),
confidence: 0.95,
usage_count: 1,
success_count: 1,
created_at: Date.now(),
last_used: Date.now(),
});
// Retrieve similar experiences with reasoning
const result = await rb.retrieveWithReasoning(embedding, {
domain: 'database-optimization',
k: 5,
useMMR: true, // Diverse results
synthesizeContext: true, // Rich context synthesis
});
console.log('Memories:', result.memories);
console.log('Context:', result.context);
console.log('Patterns:', result.patterns);
```
---
## Core ReasoningBank Concepts
### 1. Trajectory Tracking
Track agent execution paths and outcomes:
```typescript
// Record trajectory (sequence of actions)
const trajectory = {
task: 'optimize-api-endpoint',
steps: [
{ action: 'analyze-bottleneck', result: 'found N+1 query' },
{ action: 'add-eager-loading', result: 'reduced queries' },
{ action: 'add-caching', result: 'improved latency' }
],
outcome: 'success',
metrics: { latency_before: 2500, latency_after: 150 }
};
const embedding = await computeEmbedding(JSON.stringify(trajectory));
await rb.insertPattern({
id: '',
type: 'trajectory',
domain: 'api-optimization',
pattern_data: JSON.stringify({ embedding, pattern: trajectory }),
confidence: 0.9,
usage_count: 1,
success_count: 1,
created_at: Date.now(),
last_used: Date.now(),
});
```
### 2. Verdict Judgment
Judge whether a trajectory was successful:
```typescript
// Retrieve similar past trajectories
const similar = await rb.retrieveWithReasoning(queryEmbedding, {
domain: 'api-optimization',
k: 10,
});
// Judge based on similarity to successful patterns
const verdict = similar.memories.filter(m =>
m.pattern.outcome === 'success' &&
m.similarity > 0.8
).length > 5 ? 'likely_success' : 'needs_review';
console.log('Verdict:', verdict);
console.log('Confidence:', similar.memories[0]?.similarity || 0);
```
### 3. Memory Distillation
Consolidate similar experiences into patterns:
```typescript
// Get all experiences in domain
const experiences = await rb.retrieveWithReasoning(embedding, {
domain: 'api-optimization',
k: 100,
optimizeMemory: true, // Automatic consolidation
});
// Distill into high-level pattern
const distilledPattern = {
domain: 'api-optimization',
pattern: 'For N+1 queries: add eager loading, then cache',
success_rate: 0.92,
sample_size: experiences.memories.length,
confidence: 0.95
};
await rb.insertPattern({
id: '',
type: 'distilled-pattern',
domain: 'api-optimization',
pattern_data: JSON.stringify({
embedding: await computeEmbedding(JSON.stringify(distilledPattern)),
pattern: distilledPattern
}),
confidence: 0.95,
usage_count: 0,
success_count: 0,
created_at: Date.now(),
last_used: Date.now(),
});
```
---
## Integration with Reasoning Agents
AgentDB provides 4 reasoning modules that enhance ReasoningBank:
### 1. PatternMatcher
Find similar successful patterns:
```typescript
const result = await rb.retrieveWithReasoning(queryEmbedding, {
domain: 'problem-solving',
k: 10,
useMMR: true, // Maximal Marginal Relevance for diversity
});
// PatternMatcher returns diverse, relevant memories
result.memories.forEach(mem => {
console.log(`Pattern: ${mem.pattern.approach}`);
console.log(`Similarity: ${mem.similarity}`);
console.log(`Success Rate: ${mem.success_count / mem.usage_count}`);
});
```
### 2. ContextSynthesizer
Generate rich context from multiple memories:
```typescript
const result = await rb.retrieveWithReasoning(queryEmbedding, {
domain: 'code-optimization',
synthesizeContext: true, // Enable context synthesis
k: 5,
});
// ContextSynthesizer creates coherent narrative
console.log('Synthesized Context:', result.context);
// "Based on 5 similar optimizations, the most effective approach
// involves profiling, identifying bottlenecks, and applying targeted
// improvements. Success rate: 87%"
```
### 3. MemoryOptimizer
Automatically consolidate and prune:
```typescript
const result = await rb.retrieveWithReasoning(queryEmbedding, {
domain: 'testing',
optimizeMemory: true, // Enable automatic optimization
});
// MemoryOptimizer consolidates similar patterns and prunes low-quality
console.log('Optimizations:', result.optimizations);
// { consolidated: 15, pruned: 3, improved_quality: 0.12 }
```
### 4. ExperienceCurator
Filter by quality and relevance:
```typescript
const result = await rb.retrieveWithReasoning(queryEmbedding, {
domain: 'debugging',
k: 20,
minConfidence: 0.8, // Only high-confidence experiences
});
// ExperienceCurator returns only quality experiences
result.memories.forEach(mem => {
console.log(`Confidence: ${mem.confidence}`);
console.log(`Success Rate: ${mem.success_count / mem.usage_count}`);
});
```
---
## Legacy API Compatibility
AgentDB maintains 100% backward compatibility with legacy ReasoningBank:
```typescript
import {
retrieveMemories,
judgeTrajectory,
distillMemories
} from 'agentic-flow/reasoningbank';
// Legacy API works unchanged (uses AgentDB backend automatically)
const memories = await retrieveMemories(query, {
domain: 'code-generation',
agent: 'coder'
});
const verdict = await judgeTrajectory(trajectory, query);
const newMemories = await distillMemories(
trajectory,
verdict,
query,
{ domain: 'code-generation' }
);
```
---
## Performance Characteristics
- **Pattern Search**: 150x faster (100µs vs 15ms)
- **Memory Retrieval**: <1ms (with cache)
- **Batch Insert**: 500x faster (2ms vs 1s for 100 patterns)
- **Trajectory Judgment**: <5ms (including retrieval + analysis)
- **Memory Distillation**: <50ms (consolidate 100 patterns)
---
## Advanced Patterns
### Hierarchical Memory
Organize memories by abstraction level:
```typescript
// Low-level: Specific implementation
await rb.insertPattern({
type: 'concrete',
domain: 'debugging/null-pointer',
pattern_data: JSON.stringify({
embedding,
pattern: { bug: 'NPE in UserService.getUser()', fix: 'Add null check' }
}),
confidence: 0.9,
// ...
});
// Mid-level: Pattern across similar cases
await rb.insertPattern({
type: 'pattern',
domain: 'debugging',
pattern_data: JSON.stringify({
embedding,
pattern: { category: 'null-pointer', approach: 'defensive-checks' }
}),
confidence: 0.85,
// ...
});
// High-level: General principle
await rb.insertPattern({
type: 'principle',
domain: 'software-engineering',
pattern_data: JSON.stringify({
embedding,
pattern: { principle: 'fail-fast with clear errors' }
}),
confidence: 0.95,
// ...
});
```
### Multi-Domain Learning
Transfer learning across domains:
```typescript
// Learn from backend optimization
const backendExperience = await rb.retrieveWithReasoning(embedding, {
domain: 'backend-optimization',
k: 10,
});
// Apply to frontend optimization
const transferredKnowledge = backendExperience.memories.map(mem => ({
...mem,
domain: 'frontend-optimization',
adapted: true,
}));
```
---
## CLI Operations
### Database Management
```bash
# Export trajectories and patterns
npx agentdb@latest export ./.agentdb/reasoningbank.db ./backup.json
# Import experiences
npx agentdb@latest import ./experiences.json
# Get statistics
npx agentdb@latest stats ./.agentdb/reasoningbank.db
# Shows: total patterns, domains, confidence distribution
```
### Migration
```bash
# Migrate from legacy ReasoningBank
npx agentdb@latest migrate --source .swarm/memory.db --target .agentdb/reasoningbank.db
# Validate migration
npx agentdb@latest stats .agentdb/reasoningbank.db
```
---
## Troubleshooting
### Issue: Migration fails
```bash
# Check source database exists
ls -la .swarm/memory.db
# Run with verbose logging
DEBUG=agentdb:* npx agentdb@latest migrate --source .swarm/memory.db
```
### Issue: Low confidence scores
```typescript
// Enable context synthesis for better quality
const result = await rb.retrieveWithReasoning(embedding, {
synthesizeContext: true,
useMMR: true,
k: 10,
});
```
### Issue: Memory growing too large
```typescript
// Enable automatic optimization
const result = await rb.retrieveWithReasoning(embedding, {
optimizeMemory: true, // Consolidates similar patterns
});
// Or manually optimize
await rb.optimize();
```
---
## Learn More
- **AgentDB Integration**: node_modules/agentic-flow/docs/AGENTDB_INTEGRATION.md
- **GitHub**: https://github.com/ruvnet/agentic-flow/tree/main/packages/agentdb
- **MCP Integration**: `npx agentdb@latest mcp`
- **Website**: https://agentdb.ruv.io
---
**Category**: Machine Learning / Reinforcement Learning
**Difficulty**: Intermediate
**Estimated Time**: 20-30 minutes
@@ -1,201 +0,0 @@
---
name: "ReasoningBank Intelligence"
description: "Implement adaptive learning with ReasoningBank for pattern recognition, strategy optimization, and continuous improvement. Use when building self-learning agents, optimizing workflows, or implementing meta-cognitive systems."
---
# ReasoningBank Intelligence
## What This Skill Does
Implements ReasoningBank's adaptive learning system for AI agents to learn from experience, recognize patterns, and optimize strategies over time. Enables meta-cognitive capabilities and continuous improvement.
## Prerequisites
- agentic-flow v3.0.0-alpha.1+
- AgentDB v3.0.0-alpha.10+ (for persistence)
- Node.js 18+
## Quick Start
```typescript
import { ReasoningBank } from 'agentic-flow/reasoningbank';
// Initialize ReasoningBank
const rb = new ReasoningBank({
persist: true,
learningRate: 0.1,
adapter: 'agentdb' // Use AgentDB for storage
});
// Record task outcome
await rb.recordExperience({
task: 'code_review',
approach: 'static_analysis_first',
outcome: {
success: true,
metrics: {
bugs_found: 5,
time_taken: 120,
false_positives: 1
}
},
context: {
language: 'typescript',
complexity: 'medium'
}
});
// Get optimal strategy
const strategy = await rb.recommendStrategy('code_review', {
language: 'typescript',
complexity: 'high'
});
```
## Core Features
### 1. Pattern Recognition
```typescript
// Learn patterns from data
await rb.learnPattern({
pattern: 'api_errors_increase_after_deploy',
triggers: ['deployment', 'traffic_spike'],
actions: ['rollback', 'scale_up'],
confidence: 0.85
});
// Match patterns
const matches = await rb.matchPatterns(currentSituation);
```
### 2. Strategy Optimization
```typescript
// Compare strategies
const comparison = await rb.compareStrategies('bug_fixing', [
'tdd_approach',
'debug_first',
'reproduce_then_fix'
]);
// Get best strategy
const best = comparison.strategies[0];
console.log(`Best: ${best.name} (score: ${best.score})`);
```
### 3. Continuous Learning
```typescript
// Enable auto-learning from all tasks
await rb.enableAutoLearning({
threshold: 0.7, // Only learn from high-confidence outcomes
updateFrequency: 100 // Update models every 100 experiences
});
```
## Advanced Usage
### Meta-Learning
```typescript
// Learn about learning
await rb.metaLearn({
observation: 'parallel_execution_faster_for_independent_tasks',
confidence: 0.95,
applicability: {
task_types: ['batch_processing', 'data_transformation'],
conditions: ['tasks_independent', 'io_bound']
}
});
```
### Transfer Learning
```typescript
// Apply knowledge from one domain to another
await rb.transferKnowledge({
from: 'code_review_javascript',
to: 'code_review_typescript',
similarity: 0.8
});
```
### Adaptive Agents
```typescript
// Create self-improving agent
class AdaptiveAgent {
async execute(task: Task) {
// Get optimal strategy
const strategy = await rb.recommendStrategy(task.type, task.context);
// Execute with strategy
const result = await this.executeWithStrategy(task, strategy);
// Learn from outcome
await rb.recordExperience({
task: task.type,
approach: strategy.name,
outcome: result,
context: task.context
});
return result;
}
}
```
## Integration with AgentDB
```typescript
// Persist ReasoningBank data
await rb.configure({
storage: {
type: 'agentdb',
options: {
database: './reasoning-bank.db',
enableVectorSearch: true
}
}
});
// Query learned patterns
const patterns = await rb.query({
category: 'optimization',
minConfidence: 0.8,
timeRange: { last: '30d' }
});
```
## Performance Metrics
```typescript
// Track learning effectiveness
const metrics = await rb.getMetrics();
console.log(`
Total Experiences: ${metrics.totalExperiences}
Patterns Learned: ${metrics.patternsLearned}
Strategy Success Rate: ${metrics.strategySuccessRate}
Improvement Over Time: ${metrics.improvement}
`);
```
## Best Practices
1. **Record consistently**: Log all task outcomes, not just successes
2. **Provide context**: Rich context improves pattern matching
3. **Set thresholds**: Filter low-confidence learnings
4. **Review periodically**: Audit learned patterns for quality
5. **Use vector search**: Enable semantic pattern matching
## Troubleshooting
### Issue: Poor recommendations
**Solution**: Ensure sufficient training data (100+ experiences per task type)
### Issue: Slow pattern matching
**Solution**: Enable vector indexing in AgentDB
### Issue: Memory growing large
**Solution**: Set TTL for old experiences or enable pruning
## Learn More
- ReasoningBank Guide: agentic-flow/src/reasoningbank/README.md
- AgentDB Integration: packages/agentdb/docs/reasoningbank.md
- Pattern Learning: docs/reasoning/patterns.md
-135
View File
@@ -1,135 +0,0 @@
---
name: security-audit
description: >
Comprehensive security scanning and vulnerability detection. Includes input validation, path traversal prevention, CVE detection, and secure coding pattern enforcement.
Use when: authentication implementation, authorization logic, payment processing, user data handling, API endpoint creation, file upload handling, database queries, external API integration.
Skip when: read-only operations on public data, internal development tooling, static documentation, styling changes.
---
# Security Audit Skill
## Purpose
Comprehensive security scanning and vulnerability detection. Includes input validation, path traversal prevention, CVE detection, and secure coding pattern enforcement.
## When to Trigger
- authentication implementation
- authorization logic
- payment processing
- user data handling
- API endpoint creation
- file upload handling
- database queries
- external API integration
## When to Skip
- read-only operations on public data
- internal development tooling
- static documentation
- styling changes
## Commands
### Full Security Scan
Run comprehensive security analysis on the codebase
```bash
npx @claude-flow/cli security scan --depth full
```
**Example:**
```bash
npx @claude-flow/cli security scan --depth full --output security-report.json
```
### Input Validation Check
Check for input validation issues
```bash
npx @claude-flow/cli security scan --check input-validation
```
**Example:**
```bash
npx @claude-flow/cli security scan --check input-validation --path ./src/api
```
### Path Traversal Check
Check for path traversal vulnerabilities
```bash
npx @claude-flow/cli security scan --check path-traversal
```
### SQL Injection Check
Check for SQL injection vulnerabilities
```bash
npx @claude-flow/cli security scan --check sql-injection
```
### XSS Check
Check for cross-site scripting vulnerabilities
```bash
npx @claude-flow/cli security scan --check xss
```
### CVE Scan
Scan dependencies for known CVEs
```bash
npx @claude-flow/cli security cve --scan
```
**Example:**
```bash
npx @claude-flow/cli security cve --scan --severity high
```
### Security Audit Report
Generate full security audit report
```bash
npx @claude-flow/cli security audit --report
```
**Example:**
```bash
npx @claude-flow/cli security audit --report --format markdown --output SECURITY.md
```
### Threat Modeling
Run threat modeling analysis
```bash
npx @claude-flow/cli security threats --analyze
```
### Validate Secrets
Check for hardcoded secrets
```bash
npx @claude-flow/cli security validate --check secrets
```
## Scripts
| Script | Path | Description |
|--------|------|-------------|
| `security-scan` | `.agents/scripts/security-scan.sh` | Run full security scan pipeline |
| `cve-remediate` | `.agents/scripts/cve-remediate.sh` | Auto-remediate known CVEs |
## References
| Document | Path | Description |
|----------|------|-------------|
| `Security Checklist` | `docs/security-checklist.md` | Security review checklist |
| `OWASP Guide` | `docs/owasp-top10.md` | OWASP Top 10 mitigation guide |
## Best Practices
1. Check memory for existing patterns before starting
2. Use hierarchical topology for coordination
3. Store successful patterns after completion
4. Document any new learnings
-910
View File
@@ -1,910 +0,0 @@
---
name: "Skill Builder"
description: "Create new Codex Skills with proper YAML frontmatter, progressive disclosure structure, and complete directory organization. Use when you need to build custom skills for specific workflows, generate skill templates, or understand the Codex Skills specification."
---
# Skill Builder
## What This Skill Does
Creates production-ready Codex Skills with proper YAML frontmatter, progressive disclosure architecture, and complete file/folder structure. This skill guides you through building skills that Codex can autonomously discover and use across all surfaces (Codex.ai, Codex, SDK, API).
## Prerequisites
- Codex 2.0+ or Codex.ai with Skills support
- Basic understanding of Markdown and YAML
- Text editor or IDE
## Quick Start
### Creating Your First Skill
```bash
# 1. Create skill directory (MUST be at top level, NOT in subdirectories!)
mkdir -p ~/.Codex/skills/my-first-skill
# 2. Create SKILL.md with proper format
cat > ~/.Codex/skills/my-first-skill/SKILL.md << 'EOF'
---
name: "My First Skill"
description: "Brief description of what this skill does and when Codex should use it. Maximum 1024 characters."
---
# My First Skill
## What This Skill Does
[Your instructions here]
## Quick Start
[Basic usage]
EOF
# 3. Verify skill is detected
# Restart Codex or refresh Codex.ai
```
---
## Complete Specification
### 📋 YAML Frontmatter (REQUIRED)
Every SKILL.md **must** start with YAML frontmatter containing exactly two required fields:
```yaml
---
name: "Skill Name" # REQUIRED: Max 64 chars
description: "What this skill does # REQUIRED: Max 1024 chars
and when Codex should use it." # Include BOTH what & when
---
```
#### Field Requirements
**`name`** (REQUIRED):
- **Type**: String
- **Max Length**: 64 characters
- **Format**: Human-friendly display name
- **Usage**: Shown in skill lists, UI, and loaded into Codex's system prompt
- **Best Practice**: Use Title Case, be concise and descriptive
- **Examples**:
- ✅ "API Documentation Generator"
- ✅ "React Component Builder"
- ✅ "Database Schema Designer"
- ❌ "skill-1" (not descriptive)
- ❌ "This is a very long skill name that exceeds sixty-four characters" (too long)
**`description`** (REQUIRED):
- **Type**: String
- **Max Length**: 1024 characters
- **Format**: Plain text or minimal markdown
- **Content**: MUST include:
1. **What** the skill does (functionality)
2. **When** Codex should invoke it (trigger conditions)
- **Usage**: Loaded into Codex's system prompt for autonomous matching
- **Best Practice**: Front-load key trigger words, be specific about use cases
- **Examples**:
- ✅ "Generate OpenAPI 3.0 documentation from Express.js routes. Use when creating API docs, documenting endpoints, or building API specifications."
- ✅ "Create React functional components with TypeScript, hooks, and tests. Use when scaffolding new components or converting class components."
- ❌ "A comprehensive guide to API documentation" (no "when" clause)
- ❌ "Documentation tool" (too vague)
#### YAML Formatting Rules
```yaml
---
# ✅ CORRECT: Simple string
name: "API Builder"
description: "Creates REST APIs with Express and TypeScript."
# ✅ CORRECT: Multi-line description
name: "Full-Stack Generator"
description: "Generates full-stack applications with React frontend and Node.js backend. Use when starting new projects or scaffolding applications."
# ✅ CORRECT: Special characters quoted
name: "JSON:API Builder"
description: "Creates JSON:API compliant endpoints: pagination, filtering, relationships."
# ❌ WRONG: Missing quotes with special chars
name: API:Builder # YAML parse error!
# ❌ WRONG: Extra fields (ignored but discouraged)
name: "My Skill"
description: "My description"
version: "1.0.0" # NOT part of spec
author: "Me" # NOT part of spec
tags: ["dev", "api"] # NOT part of spec
---
```
**Critical**: Only `name` and `description` are used by Codex. Additional fields are ignored.
---
### 📂 Directory Structure
#### Minimal Skill (Required)
```
~/.Codex/skills/ # Personal skills location
└── my-skill/ # Skill directory (MUST be at top level!)
└── SKILL.md # REQUIRED: Main skill file
```
**IMPORTANT**: Skills MUST be directly under `~/.Codex/skills/[skill-name]/`.
Codex does NOT support nested subdirectories or namespaces!
#### Full-Featured Skill (Recommended)
```
~/.Codex/skills/
└── my-skill/ # Top-level skill directory
├── SKILL.md # REQUIRED: Main skill file
├── README.md # Optional: Human-readable docs
├── scripts/ # Optional: Executable scripts
│ ├── setup.sh
│ ├── validate.js
│ └── deploy.py
├── resources/ # Optional: Supporting files
│ ├── templates/
│ │ ├── api-template.js
│ │ └── component.tsx
│ ├── examples/
│ │ └── sample-output.json
│ └── schemas/
│ └── config-schema.json
└── docs/ # Optional: Additional documentation
├── ADVANCED.md
├── TROUBLESHOOTING.md
└── API_REFERENCE.md
```
#### Skills Locations
**Personal Skills** (available across all projects):
```
~/.Codex/skills/
└── [your-skills]/
```
- **Path**: `~/.Codex/skills/` or `$HOME/.Codex/skills/`
- **Scope**: Available in all projects for this user
- **Version Control**: NOT committed to git (outside repo)
- **Use Case**: Personal productivity tools, custom workflows
**Project Skills** (team-shared, version controlled):
```
<project-root>/.Codex/skills/
└── [team-skills]/
```
- **Path**: `.Codex/skills/` in project root
- **Scope**: Available only in this project
- **Version Control**: SHOULD be committed to git
- **Use Case**: Team workflows, project-specific tools, shared knowledge
---
### 🎯 Progressive Disclosure Architecture
Codex uses a **3-level progressive disclosure system** to scale to 100+ skills without context penalty:
#### Level 1: Metadata (Name + Description)
**Loaded**: At Codex startup, always
**Size**: ~200 chars per skill
**Purpose**: Enable autonomous skill matching
**Context**: Loaded into system prompt for ALL skills
```yaml
---
name: "API Builder" # 11 chars
description: "Creates REST APIs..." # ~50 chars
---
# Total: ~61 chars per skill
# 100 skills = ~6KB context (minimal!)
```
#### Level 2: SKILL.md Body
**Loaded**: When skill is triggered/matched
**Size**: ~1-10KB typically
**Purpose**: Main instructions and procedures
**Context**: Only loaded for ACTIVE skills
```markdown
# API Builder
## What This Skill Does
[Main instructions - loaded only when skill is active]
## Quick Start
[Basic procedures]
## Step-by-Step Guide
[Detailed instructions]
```
#### Level 3+: Referenced Files
**Loaded**: On-demand as Codex navigates
**Size**: Variable (KB to MB)
**Purpose**: Deep reference, examples, schemas
**Context**: Loaded only when Codex accesses specific files
```markdown
# In SKILL.md
See [Advanced Configuration](docs/ADVANCED.md) for complex scenarios.
See [API Reference](docs/API_REFERENCE.md) for complete documentation.
Use template: `resources/templates/api-template.js`
# Codex will load these files ONLY if needed
```
**Benefit**: Install 100+ skills with ~6KB context. Only active skill content (1-10KB) enters context.
---
### 📝 SKILL.md Content Structure
#### Recommended 4-Level Structure
```markdown
---
name: "Your Skill Name"
description: "What it does and when to use it"
---
# Your Skill Name
## Level 1: Overview (Always Read First)
Brief 2-3 sentence description of the skill.
## Prerequisites
- Requirement 1
- Requirement 2
## What This Skill Does
1. Primary function
2. Secondary function
3. Key benefit
---
## Level 2: Quick Start (For Fast Onboarding)
### Basic Usage
```bash
# Simplest use case
command --option value
```
### Common Scenarios
1. **Scenario 1**: How to...
2. **Scenario 2**: How to...
---
## Level 3: Detailed Instructions (For Deep Work)
### Step-by-Step Guide
#### Step 1: Initial Setup
```bash
# Commands
```
Expected output:
```
Success message
```
#### Step 2: Configuration
- Configuration option 1
- Configuration option 2
#### Step 3: Execution
- Run the main command
- Verify results
### Advanced Options
#### Option 1: Custom Configuration
```bash
# Advanced usage
```
#### Option 2: Integration
```bash
# Integration steps
```
---
## Level 4: Reference (Rarely Needed)
### Troubleshooting
#### Issue: Common Problem
**Symptoms**: What you see
**Cause**: Why it happens
**Solution**: How to fix
```bash
# Fix command
```
#### Issue: Another Problem
**Solution**: Steps to resolve
### Complete API Reference
See [API_REFERENCE.md](docs/API_REFERENCE.md)
### Examples
See [examples/](resources/examples/)
### Related Skills
- [Related Skill 1](#)
- [Related Skill 2](#)
### Resources
- [External Link 1](https://example.com)
- [Documentation](https://docs.example.com)
```
---
### 🎨 Content Best Practices
#### Writing Effective Descriptions
**Front-Load Keywords**:
```yaml
# ✅ GOOD: Keywords first
description: "Generate TypeScript interfaces from JSON schema. Use when converting schemas, creating types, or building API clients."
# ❌ BAD: Keywords buried
description: "This skill helps developers who need to work with JSON schemas by providing a way to generate TypeScript interfaces."
```
**Include Trigger Conditions**:
```yaml
# ✅ GOOD: Clear "when" clause
description: "Debug React performance issues using Chrome DevTools. Use when components re-render unnecessarily, investigating slow updates, or optimizing bundle size."
# ❌ BAD: No trigger conditions
description: "Helps with React performance debugging."
```
**Be Specific**:
```yaml
# ✅ GOOD: Specific technologies
description: "Create Express.js REST endpoints with Joi validation, Swagger docs, and Jest tests. Use when building new APIs or adding endpoints."
# ❌ BAD: Too generic
description: "Build API endpoints with proper validation and testing."
```
#### Progressive Disclosure Writing
**Keep Level 1 Brief** (Overview):
```markdown
## What This Skill Does
Creates production-ready React components with TypeScript, hooks, and tests in 3 steps.
```
**Level 2 for Common Paths** (Quick Start):
```markdown
## Quick Start
```bash
# Most common use case (80% of users)
generate-component MyComponent
```
```
**Level 3 for Details** (Step-by-Step):
```markdown
## Step-by-Step Guide
### Creating a Basic Component
1. Run generator
2. Choose template
3. Customize options
[Detailed explanations]
```
**Level 4 for Edge Cases** (Reference):
```markdown
## Advanced Configuration
For complex scenarios like HOCs, render props, or custom hooks, see [ADVANCED.md](docs/ADVANCED.md).
```
---
### 🛠️ Adding Scripts and Resources
#### Scripts Directory
**Purpose**: Executable scripts that Codex can run
**Location**: `scripts/` in skill directory
**Usage**: Referenced from SKILL.md
Example:
```bash
# In skill directory
scripts/
├── setup.sh # Initialization script
├── validate.js # Validation logic
├── generate.py # Code generation
└── deploy.sh # Deployment script
```
Reference from SKILL.md:
```markdown
## Setup
Run the setup script:
```bash
./scripts/setup.sh
```
## Validation
Validate your configuration:
```bash
node scripts/validate.js config.json
```
```
#### Resources Directory
**Purpose**: Templates, examples, schemas, static files
**Location**: `resources/` in skill directory
**Usage**: Referenced or copied by scripts
Example:
```bash
resources/
├── templates/
│ ├── component.tsx.template
│ ├── test.spec.ts.template
│ └── story.stories.tsx.template
├── examples/
│ ├── basic-example/
│ ├── advanced-example/
│ └── integration-example/
└── schemas/
├── config.schema.json
└── output.schema.json
```
Reference from SKILL.md:
```markdown
## Templates
Use the component template:
```bash
cp resources/templates/component.tsx.template src/components/MyComponent.tsx
```
## Examples
See working examples in `resources/examples/`:
- `basic-example/` - Simple component
- `advanced-example/` - With hooks and context
```
---
### 🔗 File References and Navigation
Codex can navigate to referenced files automatically. Use these patterns:
#### Markdown Links
```markdown
See [Advanced Configuration](docs/ADVANCED.md) for complex scenarios.
See [Troubleshooting Guide](docs/TROUBLESHOOTING.md) if you encounter errors.
```
#### Relative File Paths
```markdown
Use the template located at `resources/templates/api-template.js`
See examples in `resources/examples/basic-usage/`
```
#### Inline File Content
```markdown
## Example Configuration
See `resources/examples/config.json`:
```json
{
"option": "value"
}
```
```
**Best Practice**: Keep SKILL.md lean (~2-5KB). Move lengthy content to separate files and reference them. Codex will load only what's needed.
---
### ✅ Validation Checklist
Before publishing a skill, verify:
**YAML Frontmatter**:
- [ ] Starts with `---`
- [ ] Contains `name` field (max 64 chars)
- [ ] Contains `description` field (max 1024 chars)
- [ ] Description includes "what" and "when"
- [ ] Ends with `---`
- [ ] No YAML syntax errors
**File Structure**:
- [ ] SKILL.md exists in skill directory
- [ ] Directory is DIRECTLY in `~/.Codex/skills/[skill-name]/` or `.Codex/skills/[skill-name]/`
- [ ] Uses clear, descriptive directory name
- [ ] **NO nested subdirectories** (Codex requires top-level structure)
**Content Quality**:
- [ ] Level 1 (Overview) is brief and clear
- [ ] Level 2 (Quick Start) shows common use case
- [ ] Level 3 (Details) provides step-by-step guide
- [ ] Level 4 (Reference) links to advanced content
- [ ] Examples are concrete and runnable
- [ ] Troubleshooting section addresses common issues
**Progressive Disclosure**:
- [ ] Core instructions in SKILL.md (~2-5KB)
- [ ] Advanced content in separate docs/
- [ ] Large resources in resources/ directory
- [ ] Clear navigation between levels
**Testing**:
- [ ] Skill appears in Codex's skill list
- [ ] Description triggers on relevant queries
- [ ] Instructions are clear and actionable
- [ ] Scripts execute successfully (if included)
- [ ] Examples work as documented
---
## Skill Builder Templates
### Template 1: Basic Skill (Minimal)
```markdown
---
name: "My Basic Skill"
description: "One sentence what. One sentence when to use."
---
# My Basic Skill
## What This Skill Does
[2-3 sentences describing functionality]
## Quick Start
```bash
# Single command to get started
```
## Step-by-Step Guide
### Step 1: Setup
[Instructions]
### Step 2: Usage
[Instructions]
### Step 3: Verify
[Instructions]
## Troubleshooting
- **Issue**: Problem description
- **Solution**: Fix description
```
### Template 2: Intermediate Skill (With Scripts)
```markdown
---
name: "My Intermediate Skill"
description: "Detailed what with key features. When to use with specific triggers: scaffolding, generating, building."
---
# My Intermediate Skill
## Prerequisites
- Requirement 1
- Requirement 2
## What This Skill Does
1. Primary function
2. Secondary function
3. Integration capability
## Quick Start
```bash
./scripts/setup.sh
./scripts/generate.sh my-project
```
## Configuration
Edit `config.json`:
```json
{
"option1": "value1",
"option2": "value2"
}
```
## Step-by-Step Guide
### Basic Usage
[Steps for 80% use case]
### Advanced Usage
[Steps for complex scenarios]
## Available Scripts
- `scripts/setup.sh` - Initial setup
- `scripts/generate.sh` - Code generation
- `scripts/validate.sh` - Validation
## Resources
- Templates: `resources/templates/`
- Examples: `resources/examples/`
## Troubleshooting
[Common issues and solutions]
```
### Template 3: Advanced Skill (Full-Featured)
```markdown
---
name: "My Advanced Skill"
description: "Comprehensive what with all features and integrations. Use when [trigger 1], [trigger 2], or [trigger 3]. Supports [technology stack]."
---
# My Advanced Skill
## Overview
[Brief 2-3 sentence description]
## Prerequisites
- Technology 1 (version X+)
- Technology 2 (version Y+)
- API keys or credentials
## What This Skill Does
1. **Core Feature**: Description
2. **Integration**: Description
3. **Automation**: Description
---
## Quick Start (60 seconds)
### Installation
```bash
./scripts/install.sh
```
### First Use
```bash
./scripts/quickstart.sh
```
Expected output:
```
✓ Setup complete
✓ Configuration validated
→ Ready to use
```
---
## Configuration
### Basic Configuration
Edit `config.json`:
```json
{
"mode": "production",
"features": ["feature1", "feature2"]
}
```
### Advanced Configuration
See [Configuration Guide](docs/CONFIGURATION.md)
---
## Step-by-Step Guide
### 1. Initial Setup
[Detailed steps]
### 2. Core Workflow
[Main procedures]
### 3. Integration
[Integration steps]
---
## Advanced Features
### Feature 1: Custom Templates
```bash
./scripts/generate.sh --template custom
```
### Feature 2: Batch Processing
```bash
./scripts/batch.sh --input data.json
```
### Feature 3: CI/CD Integration
See [CI/CD Guide](docs/CICD.md)
---
## Scripts Reference
| Script | Purpose | Usage |
|--------|---------|-------|
| `install.sh` | Install dependencies | `./scripts/install.sh` |
| `generate.sh` | Generate code | `./scripts/generate.sh [name]` |
| `validate.sh` | Validate output | `./scripts/validate.sh` |
| `deploy.sh` | Deploy to environment | `./scripts/deploy.sh [env]` |
---
## Resources
### Templates
- `resources/templates/basic.template` - Basic template
- `resources/templates/advanced.template` - Advanced template
### Examples
- `resources/examples/basic/` - Simple example
- `resources/examples/advanced/` - Complex example
- `resources/examples/integration/` - Integration example
### Schemas
- `resources/schemas/config.schema.json` - Configuration schema
- `resources/schemas/output.schema.json` - Output validation
---
## Troubleshooting
### Issue: Installation Failed
**Symptoms**: Error during `install.sh`
**Cause**: Missing dependencies
**Solution**:
```bash
# Install prerequisites
npm install -g required-package
./scripts/install.sh --force
```
### Issue: Validation Errors
**Symptoms**: Validation script fails
**Solution**: See [Troubleshooting Guide](docs/TROUBLESHOOTING.md)
---
## API Reference
Complete API documentation: [API_REFERENCE.md](docs/API_REFERENCE.md)
## Related Skills
- [Related Skill 1](../related-skill-1/)
- [Related Skill 2](../related-skill-2/)
## Resources
- [Official Documentation](https://example.com/docs)
- [GitHub Repository](https://github.com/example/repo)
- [Community Forum](https://forum.example.com)
---
**Created**: 2025-10-19
**Category**: Advanced
**Difficulty**: Intermediate
**Estimated Time**: 15-30 minutes
```
---
## Examples from the Wild
### Example 1: Simple Documentation Skill
```markdown
---
name: "README Generator"
description: "Generate comprehensive README.md files for GitHub repositories. Use when starting new projects, documenting code, or improving existing READMEs."
---
# README Generator
## What This Skill Does
Creates well-structured README.md files with badges, installation, usage, and contribution sections.
## Quick Start
```bash
# Answer a few questions
./scripts/generate-readme.sh
# README.md created with:
# - Project title and description
# - Installation instructions
# - Usage examples
# - Contribution guidelines
```
## Customization
Edit sections in `resources/templates/sections/` before generating.
```
### Example 2: Code Generation Skill
```markdown
---
name: "React Component Generator"
description: "Generate React functional components with TypeScript, hooks, tests, and Storybook stories. Use when creating new components, scaffolding UI, or following component architecture patterns."
---
# React Component Generator
## Prerequisites
- Node.js 18+
- React 18+
- TypeScript 5+
## Quick Start
```bash
./scripts/generate-component.sh MyComponent
# Creates:
# - src/components/MyComponent/MyComponent.tsx
# - src/components/MyComponent/MyComponent.test.tsx
# - src/components/MyComponent/MyComponent.stories.tsx
# - src/components/MyComponent/index.ts
```
## Step-by-Step Guide
### 1. Run Generator
```bash
./scripts/generate-component.sh ComponentName
```
### 2. Choose Template
- Basic: Simple functional component
- With State: useState hooks
- With Context: useContext integration
- With API: Data fetching component
### 3. Customize
Edit generated files in `src/components/ComponentName/`
## Templates
See `resources/templates/` for available component templates.
```
---
## Learn More
### Official Resources
- [Anthropic Agent Skills Documentation](https://docs.Codex.com/en/docs/agents-and-tools/agent-skills)
- [GitHub Skills Repository](https://github.com/anthropics/skills)
- [Codex Documentation](https://docs.Codex.com/en/docs/Codex)
### Community
- [Skills Marketplace](https://github.com/anthropics/skills) - Browse community skills
- [Anthropic Discord](https://discord.gg/anthropic) - Get help from community
### Advanced Topics
- Multi-file skills with complex navigation
- Skills that spawn other skills
- Integration with MCP tools
- Dynamic skill generation
---
**Created**: 2025-10-19
**Version**: 1.0.0
**Maintained By**: agentic-flow team
**License**: MIT
@@ -1,144 +0,0 @@
---
name: soft-delete-relogin-consistency
description: |
Fix for missing auth/identity records after account deletion + device re-login.
Use when: (1) User deletes account but device records are intentionally kept
(e.g., to prevent trial abuse), (2) Re-login via device succeeds but user
appears to have wrong identity type, (3) Frontend shows incorrect UI because
auth_methods or similar identity records are empty/wrong after re-login,
(4) Soft-deleted records cause stale cache entries that misrepresent user state.
Covers GORM soft-delete, device-based auth, cache invalidation after re-creation.
author: Codex
version: 1.0.0
date: 2026-03-11
---
# Soft-Delete + Re-Login Auth Consistency
## Problem
When a system uses soft-delete for auth/identity records during account deletion but
intentionally keeps primary records (like device records) for abuse prevention, re-login
flows may succeed at the "find existing record" step but fail to re-create the
soft-deleted identity records. This causes the user to exist in an inconsistent state
where they're authenticated but missing critical identity metadata.
## Context / Trigger Conditions
- Account deletion (注销) soft-deletes `auth_methods` (or equivalent identity records)
- Device/hardware records are intentionally kept to prevent trial reward abuse
- Device-based re-login finds existing device record -> reuses old user_id
- But the "device found" code path skips identity record creation (only the
"device not found" registration path creates them)
- Result: User is logged in but `auth_methods` is empty or missing the expected type
- Frontend UI breaks because it relies on `auth_methods[0].auth_type` to determine
login mode and show/hide UI elements
### Symptoms
- Buttons or UI elements that should be hidden for device-only users appear after
account deletion + re-login
- API returns user info with empty or unexpected `auth_methods` array
- `isDeviceLogin()` or similar identity checks return wrong results
- Cache returns stale user data even after re-login
## Solution
### Step 1: Identify the re-login code path
Find the "device found" branch in the login logic. This is the code path that runs
when a device record already exists (as opposed to the registration path).
### Step 2: Add identity record existence check
After finding the user via device record, check if the expected identity record exists:
```go
// After finding user via existing device record
hasDeviceAuth := false
for _, am := range userInfo.AuthMethods {
if am.AuthType == "device" && am.AuthIdentifier == req.Identifier {
hasDeviceAuth = true
break
}
}
if !hasDeviceAuth {
// Re-create the soft-deleted auth record
authMethod := &user.AuthMethods{
UserId: userInfo.Id,
AuthType: "device",
AuthIdentifier: req.Identifier,
Verified: true,
}
if createErr := db.Create(authMethod).Error; createErr != nil {
log.Error("re-create auth method failed", err)
} else {
// CRITICAL: Clear user cache so subsequent reads return updated data
_ = userModel.ClearUserCache(ctx, userInfo)
}
}
```
### Step 3: Ensure cache invalidation
After re-creating the identity record, clear the user cache. This is critical because
cached user data (with `Preload("AuthMethods")`) will still show the old empty state
until the cache is invalidated.
### Step 4: Verify GORM soft-delete behavior
GORM's soft-delete (`deleted_at IS NULL` filter) means:
- `Preload("AuthMethods")` will NOT return soft-deleted records
- `db.Create()` will create a NEW record (not undelete the old one)
- The old soft-deleted record remains in the database (harmless)
## Verification
1. Delete account (注销)
2. Re-login via device
3. Call user info API - verify `auth_methods` contains the device type
4. Check frontend UI - verify device-specific UI state is correct
## Example
**Before fix:**
```
1. User has auth_methods: [device_A, email_A]
2. User deletes account -> auth_methods all soft-deleted
3. Device record kept (abuse prevention)
4. User re-logins via same device
5. FindOneDeviceByIdentifier finds device -> reuses user_id
6. FindOne returns user with AuthMethods=[] (soft-deleted, filtered out)
7. Frontend: isDeviceLogin() = false (no auth_methods) -> shows wrong buttons
```
**After fix:**
```
1-4. Same as above
5. FindOneDeviceByIdentifier finds device -> reuses user_id
6. FindOne returns user with AuthMethods=[]
7. NEW: Detects missing device auth_method, re-creates it, clears cache
8. Frontend: isDeviceLogin() = true -> correct UI
```
## Notes
- This pattern applies broadly to any system where:
- Account deletion removes identity records but keeps usage records
- Re-login can succeed via the usage records
- UI/business logic depends on the identity records existing
- The "don't delete device records" design is intentional for preventing abuse
(e.g., users repeatedly deleting and re-creating accounts to get trial rewards)
- Cache invalidation is the most commonly missed step - without it, the fix appears
to not work because cached data is served until TTL expires
- Consider whether `Unscoped()` (GORM) should be used to also query soft-deleted
records, or whether re-creation is the better approach (usually re-creation is
cleaner as it creates a fresh record with correct timestamps)
## Related Patterns
- **Cache key dependency chains**: When `ClearUserCache` depends on `AuthMethods`
to generate email cache keys, capture auth_methods BEFORE deletion, then explicitly
clear derived cache keys after the transaction
- **Family ownership transfer**: When an owner exits a shared resource group, transfer
ownership to a remaining member instead of dissolving the group
-118
View File
@@ -1,118 +0,0 @@
---
name: sparc-methodology
description: >
SPARC development workflow: Specification, Pseudocode, Architecture, Refinement, Completion. A structured approach for complex implementations that ensures thorough planning before coding.
Use when: new feature implementation, complex implementations, architectural changes, system redesign, integration work, unclear requirements.
Skip when: simple bug fixes, documentation updates, configuration changes, well-defined small tasks, routine maintenance.
---
# Sparc Methodology Skill
## Purpose
SPARC development workflow: Specification, Pseudocode, Architecture, Refinement, Completion. A structured approach for complex implementations that ensures thorough planning before coding.
## When to Trigger
- new feature implementation
- complex implementations
- architectural changes
- system redesign
- integration work
- unclear requirements
## When to Skip
- simple bug fixes
- documentation updates
- configuration changes
- well-defined small tasks
- routine maintenance
## Commands
### Specification Phase
Define requirements, acceptance criteria, and constraints
```bash
npx @claude-flow/cli hooks route --task "specification: [requirements]"
```
**Example:**
```bash
npx @claude-flow/cli hooks route --task "specification: user authentication with OAuth2, MFA, and session management"
```
### Pseudocode Phase
Write high-level pseudocode for the implementation
```bash
npx @claude-flow/cli hooks route --task "pseudocode: [feature]"
```
**Example:**
```bash
npx @claude-flow/cli hooks route --task "pseudocode: OAuth2 login flow with token refresh"
```
### Architecture Phase
Design system structure, interfaces, and dependencies
```bash
npx @claude-flow/cli hooks route --task "architecture: [design]"
```
**Example:**
```bash
npx @claude-flow/cli hooks route --task "architecture: auth module with service layer, repository, and API endpoints"
```
### Refinement Phase
Iterate on the design based on feedback
```bash
npx @claude-flow/cli hooks route --task "refinement: [feedback]"
```
**Example:**
```bash
npx @claude-flow/cli hooks route --task "refinement: add rate limiting and brute force protection"
```
### Completion Phase
Finalize implementation with tests and documentation
```bash
npx @claude-flow/cli hooks route --task "completion: [final checks]"
```
**Example:**
```bash
npx @claude-flow/cli hooks route --task "completion: verify all tests pass, update API docs, security review"
```
### SPARC Coordinator
Spawn SPARC coordinator agent
```bash
npx @claude-flow/cli agent spawn --type sparc-coord --name sparc-lead
```
## Scripts
| Script | Path | Description |
|--------|------|-------------|
| `sparc-init` | `.agents/scripts/sparc-init.sh` | Initialize SPARC workflow for a new feature |
| `sparc-review` | `.agents/scripts/sparc-review.sh` | Run SPARC phase review checklist |
## References
| Document | Path | Description |
|----------|------|-------------|
| `SPARC Overview` | `docs/sparc.md` | Complete SPARC methodology guide |
| `Phase Templates` | `docs/sparc-templates.md` | Templates for each SPARC phase |
## Best Practices
1. Check memory for existing patterns before starting
2. Use hierarchical topology for coordination
3. Store successful patterns after completion
4. Document any new learnings
-563
View File
@@ -1,563 +0,0 @@
---
name: stream-chain
description: Stream-JSON chaining for multi-agent pipelines, data transformation, and sequential workflows
version: 1.0.0
category: workflow
tags: [streaming, pipeline, chaining, multi-agent, workflow]
---
# Stream-Chain Skill
Execute sophisticated multi-step workflows where each agent's output flows into the next, enabling complex data transformations and sequential processing pipelines.
## Overview
Stream-Chain provides two powerful modes for orchestrating multi-agent workflows:
1. **Custom Chains** (`run`): Execute custom prompt sequences with full control
2. **Predefined Pipelines** (`pipeline`): Use battle-tested workflows for common tasks
Each step in a chain receives the complete output from the previous step, enabling sophisticated multi-agent coordination through streaming data flow.
---
## Quick Start
### Run a Custom Chain
```bash
Codex-flow stream-chain run \
"Analyze codebase structure" \
"Identify improvement areas" \
"Generate action plan"
```
### Execute a Pipeline
```bash
Codex-flow stream-chain pipeline analysis
```
---
## Custom Chains (`run`)
Execute custom stream chains with your own prompts for maximum flexibility.
### Syntax
```bash
Codex-flow stream-chain run <prompt1> <prompt2> [...] [options]
```
**Requirements:**
- Minimum 2 prompts required
- Each prompt becomes a step in the chain
- Output flows sequentially through all steps
### Options
| Option | Description | Default |
|--------|-------------|---------|
| `--verbose` | Show detailed execution information | `false` |
| `--timeout <seconds>` | Timeout per step | `30` |
| `--debug` | Enable debug mode with full logging | `false` |
### How Context Flows
Each step receives the previous output as context:
```
Step 1: "Write a sorting function"
Output: [function implementation]
Step 2 receives:
"Previous step output:
[function implementation]
Next task: Add comprehensive tests"
Step 3 receives:
"Previous steps output:
[function + tests]
Next task: Optimize performance"
```
### Examples
#### Basic Development Chain
```bash
Codex-flow stream-chain run \
"Write a user authentication function" \
"Add input validation and error handling" \
"Create unit tests with edge cases"
```
#### Security Audit Workflow
```bash
Codex-flow stream-chain run \
"Analyze authentication system for vulnerabilities" \
"Identify and categorize security issues by severity" \
"Propose fixes with implementation priority" \
"Generate security test cases" \
--timeout 45 \
--verbose
```
#### Code Refactoring Chain
```bash
Codex-flow stream-chain run \
"Identify code smells in src/ directory" \
"Create refactoring plan with specific changes" \
"Apply refactoring to top 3 priority items" \
"Verify refactored code maintains behavior" \
--debug
```
#### Data Processing Pipeline
```bash
Codex-flow stream-chain run \
"Extract data from API responses" \
"Transform data into normalized format" \
"Validate data against schema" \
"Generate data quality report"
```
---
## Predefined Pipelines (`pipeline`)
Execute battle-tested workflows optimized for common development tasks.
### Syntax
```bash
Codex-flow stream-chain pipeline <type> [options]
```
### Available Pipelines
#### 1. Analysis Pipeline
Comprehensive codebase analysis and improvement identification.
```bash
Codex-flow stream-chain pipeline analysis
```
**Workflow Steps:**
1. **Structure Analysis**: Map directory structure and identify components
2. **Issue Detection**: Find potential improvements and problems
3. **Recommendations**: Generate actionable improvement report
**Use Cases:**
- New codebase onboarding
- Technical debt assessment
- Architecture review
- Code quality audits
#### 2. Refactor Pipeline
Systematic code refactoring with prioritization.
```bash
Codex-flow stream-chain pipeline refactor
```
**Workflow Steps:**
1. **Candidate Identification**: Find code needing refactoring
2. **Prioritization**: Create ranked refactoring plan
3. **Implementation**: Provide refactored code for top priorities
**Use Cases:**
- Technical debt reduction
- Code quality improvement
- Legacy code modernization
- Design pattern implementation
#### 3. Test Pipeline
Comprehensive test generation with coverage analysis.
```bash
Codex-flow stream-chain pipeline test
```
**Workflow Steps:**
1. **Coverage Analysis**: Identify areas lacking tests
2. **Test Design**: Create test cases for critical functions
3. **Implementation**: Generate unit tests with assertions
**Use Cases:**
- Increasing test coverage
- TDD workflow support
- Regression test creation
- Quality assurance
#### 4. Optimize Pipeline
Performance optimization with profiling and implementation.
```bash
Codex-flow stream-chain pipeline optimize
```
**Workflow Steps:**
1. **Profiling**: Identify performance bottlenecks
2. **Strategy**: Analyze and suggest optimization approaches
3. **Implementation**: Provide optimized code
**Use Cases:**
- Performance improvement
- Resource optimization
- Scalability enhancement
- Latency reduction
### Pipeline Options
| Option | Description | Default |
|--------|-------------|---------|
| `--verbose` | Show detailed execution | `false` |
| `--timeout <seconds>` | Timeout per step | `30` |
| `--debug` | Enable debug mode | `false` |
### Pipeline Examples
#### Quick Analysis
```bash
Codex-flow stream-chain pipeline analysis
```
#### Extended Refactoring
```bash
Codex-flow stream-chain pipeline refactor --timeout 60 --verbose
```
#### Debug Test Generation
```bash
Codex-flow stream-chain pipeline test --debug
```
#### Comprehensive Optimization
```bash
Codex-flow stream-chain pipeline optimize --timeout 90 --verbose
```
### Pipeline Output
Each pipeline execution provides:
- **Progress**: Step-by-step execution status
- **Results**: Success/failure per step
- **Timing**: Total and per-step execution time
- **Summary**: Consolidated results and recommendations
---
## Custom Pipeline Definitions
Define reusable pipelines in `.Codex-flow/config.json`:
### Configuration Format
```json
{
"streamChain": {
"pipelines": {
"security": {
"name": "Security Audit Pipeline",
"description": "Comprehensive security analysis",
"prompts": [
"Scan codebase for security vulnerabilities",
"Categorize issues by severity (critical/high/medium/low)",
"Generate fixes with priority and implementation steps",
"Create security test suite"
],
"timeout": 45
},
"documentation": {
"name": "Documentation Generation Pipeline",
"prompts": [
"Analyze code structure and identify undocumented areas",
"Generate API documentation with examples",
"Create usage guides and tutorials",
"Build architecture diagrams and flow charts"
]
}
}
}
}
```
### Execute Custom Pipeline
```bash
Codex-flow stream-chain pipeline security
Codex-flow stream-chain pipeline documentation
```
---
## Advanced Use Cases
### Multi-Agent Coordination
Chain different agent types for complex workflows:
```bash
Codex-flow stream-chain run \
"Research best practices for API design" \
"Design REST API with discovered patterns" \
"Implement API endpoints with validation" \
"Generate OpenAPI specification" \
"Create integration tests" \
"Write deployment documentation"
```
### Data Transformation Pipeline
Process and transform data through multiple stages:
```bash
Codex-flow stream-chain run \
"Extract user data from CSV files" \
"Normalize and validate data format" \
"Enrich data with external API calls" \
"Generate analytics report" \
"Create visualization code"
```
### Code Migration Workflow
Systematic code migration with validation:
```bash
Codex-flow stream-chain run \
"Analyze legacy codebase dependencies" \
"Create migration plan with risk assessment" \
"Generate modernized code for high-priority modules" \
"Create migration tests" \
"Document migration steps and rollback procedures"
```
### Quality Assurance Chain
Comprehensive code quality workflow:
```bash
Codex-flow stream-chain pipeline analysis
Codex-flow stream-chain pipeline refactor
Codex-flow stream-chain pipeline test
Codex-flow stream-chain pipeline optimize
```
---
## Best Practices
### 1. Clear and Specific Prompts
**Good:**
```bash
"Analyze authentication.js for SQL injection vulnerabilities"
```
**Avoid:**
```bash
"Check security"
```
### 2. Logical Progression
Order prompts to build on previous outputs:
```bash
1. "Identify the problem"
2. "Analyze root causes"
3. "Design solution"
4. "Implement solution"
5. "Verify implementation"
```
### 3. Appropriate Timeouts
- Simple tasks: 30 seconds (default)
- Analysis tasks: 45-60 seconds
- Implementation tasks: 60-90 seconds
- Complex workflows: 90-120 seconds
### 4. Verification Steps
Include validation in your chains:
```bash
Codex-flow stream-chain run \
"Implement feature X" \
"Write tests for feature X" \
"Verify tests pass and cover edge cases"
```
### 5. Iterative Refinement
Use chains for iterative improvement:
```bash
Codex-flow stream-chain run \
"Generate initial implementation" \
"Review and identify issues" \
"Refine based on issues found" \
"Final quality check"
```
---
## Integration with Codex Flow
### Combine with Swarm Coordination
```bash
# Initialize swarm for coordination
Codex-flow swarm init --topology mesh
# Execute stream chain with swarm agents
Codex-flow stream-chain run \
"Agent 1: Research task" \
"Agent 2: Implement solution" \
"Agent 3: Test implementation" \
"Agent 4: Review and refine"
```
### Memory Integration
Stream chains automatically store context in memory for cross-session persistence:
```bash
# Execute chain with memory
Codex-flow stream-chain run \
"Analyze requirements" \
"Design architecture" \
--verbose
# Results stored in .Codex-flow/memory/stream-chain/
```
### Neural Pattern Training
Successful chains train neural patterns for improved performance:
```bash
# Enable neural training
Codex-flow stream-chain pipeline optimize --debug
# Patterns learned and stored for future optimizations
```
---
## Troubleshooting
### Chain Timeout
If steps timeout, increase timeout value:
```bash
Codex-flow stream-chain run "complex task" --timeout 120
```
### Context Loss
If context not flowing properly, use `--debug`:
```bash
Codex-flow stream-chain run "step 1" "step 2" --debug
```
### Pipeline Not Found
Verify pipeline name and custom definitions:
```bash
# Check available pipelines
cat .Codex-flow/config.json | grep -A 10 "streamChain"
```
---
## Performance Characteristics
- **Throughput**: 2-5 steps per minute (varies by complexity)
- **Context Size**: Up to 100K tokens per step
- **Memory Usage**: ~50MB per active chain
- **Concurrency**: Supports parallel chain execution
---
## Related Skills
- **SPARC Methodology**: Systematic development workflow
- **Swarm Coordination**: Multi-agent orchestration
- **Memory Management**: Persistent context storage
- **Neural Patterns**: Adaptive learning
---
## Examples Repository
### Complete Development Workflow
```bash
# Full feature development chain
Codex-flow stream-chain run \
"Analyze requirements for user profile feature" \
"Design database schema and API endpoints" \
"Implement backend with validation" \
"Create frontend components" \
"Write comprehensive tests" \
"Generate API documentation" \
--timeout 60 \
--verbose
```
### Code Review Pipeline
```bash
# Automated code review workflow
Codex-flow stream-chain run \
"Analyze recent git changes" \
"Identify code quality issues" \
"Check for security vulnerabilities" \
"Verify test coverage" \
"Generate code review report with recommendations"
```
### Migration Assistant
```bash
# Framework migration helper
Codex-flow stream-chain run \
"Analyze current Vue 2 codebase" \
"Identify Vue 3 breaking changes" \
"Create migration checklist" \
"Generate migration scripts" \
"Provide updated code examples"
```
---
## Conclusion
Stream-Chain enables sophisticated multi-step workflows by:
- **Sequential Processing**: Each step builds on previous results
- **Context Preservation**: Full output history flows through chain
- **Flexible Orchestration**: Custom chains or predefined pipelines
- **Agent Coordination**: Natural multi-agent collaboration pattern
- **Data Transformation**: Complex processing through simple steps
Use `run` for custom workflows and `pipeline` for battle-tested solutions.
-973
View File
@@ -1,973 +0,0 @@
---
name: swarm-advanced
description: Advanced swarm orchestration patterns for research, development, testing, and complex distributed workflows
version: 2.0.0
category: orchestration
tags: [swarm, distributed, parallel, research, testing, development, coordination]
author: Codex Flow Team
---
# Advanced Swarm Orchestration
Master advanced swarm patterns for distributed research, development, and testing workflows. This skill covers comprehensive orchestration strategies using both MCP tools and CLI commands.
## Quick Start
### Prerequisites
```bash
# Ensure Codex Flow is installed
npm install -g Codex-flow@alpha
# Add MCP server (if using MCP tools)
Codex mcp add Codex-flow npx Codex-flow@alpha mcp start
```
### Basic Pattern
```javascript
// 1. Initialize swarm topology
mcp__claude-flow__swarm_init({ topology: "mesh", maxAgents: 6 })
// 2. Spawn specialized agents
mcp__claude-flow__agent_spawn({ type: "researcher", name: "Agent 1" })
// 3. Orchestrate tasks
mcp__claude-flow__task_orchestrate({ task: "...", strategy: "parallel" })
```
## Core Concepts
### Swarm Topologies
**Mesh Topology** - Peer-to-peer communication, best for research and analysis
- All agents communicate directly
- High flexibility and resilience
- Use for: Research, analysis, brainstorming
**Hierarchical Topology** - Coordinator with subordinates, best for development
- Clear command structure
- Sequential workflow support
- Use for: Development, structured workflows
**Star Topology** - Central coordinator, best for testing
- Centralized control and monitoring
- Parallel execution with coordination
- Use for: Testing, validation, quality assurance
**Ring Topology** - Sequential processing chain
- Step-by-step processing
- Pipeline workflows
- Use for: Multi-stage processing, data pipelines
### Agent Strategies
**Adaptive** - Dynamic adjustment based on task complexity
**Balanced** - Equal distribution of work across agents
**Specialized** - Task-specific agent assignment
**Parallel** - Maximum concurrent execution
## Pattern 1: Research Swarm
### Purpose
Deep research through parallel information gathering, analysis, and synthesis.
### Architecture
```javascript
// Initialize research swarm
mcp__claude-flow__swarm_init({
"topology": "mesh",
"maxAgents": 6,
"strategy": "adaptive"
})
// Spawn research team
const researchAgents = [
{
type: "researcher",
name: "Web Researcher",
capabilities: ["web-search", "content-extraction", "source-validation"]
},
{
type: "researcher",
name: "Academic Researcher",
capabilities: ["paper-analysis", "citation-tracking", "literature-review"]
},
{
type: "analyst",
name: "Data Analyst",
capabilities: ["data-processing", "statistical-analysis", "visualization"]
},
{
type: "analyst",
name: "Pattern Analyzer",
capabilities: ["trend-detection", "correlation-analysis", "outlier-detection"]
},
{
type: "documenter",
name: "Report Writer",
capabilities: ["synthesis", "technical-writing", "formatting"]
}
]
// Spawn all agents
researchAgents.forEach(agent => {
mcp__claude-flow__agent_spawn({
type: agent.type,
name: agent.name,
capabilities: agent.capabilities
})
})
```
### Research Workflow
#### Phase 1: Information Gathering
```javascript
// Parallel information collection
mcp__claude-flow__parallel_execute({
"tasks": [
{
"id": "web-search",
"command": "search recent publications and articles"
},
{
"id": "academic-search",
"command": "search academic databases and papers"
},
{
"id": "data-collection",
"command": "gather relevant datasets and statistics"
},
{
"id": "expert-search",
"command": "identify domain experts and thought leaders"
}
]
})
// Store research findings in memory
mcp__claude-flow__memory_usage({
"action": "store",
"key": "research-findings-" + Date.now(),
"value": JSON.stringify(findings),
"namespace": "research",
"ttl": 604800 // 7 days
})
```
#### Phase 2: Analysis and Validation
```javascript
// Pattern recognition in findings
mcp__claude-flow__pattern_recognize({
"data": researchData,
"patterns": ["trend", "correlation", "outlier", "emerging-pattern"]
})
// Cognitive analysis
mcp__claude-flow__cognitive_analyze({
"behavior": "research-synthesis"
})
// Quality assessment
mcp__claude-flow__quality_assess({
"target": "research-sources",
"criteria": ["credibility", "relevance", "recency", "authority"]
})
// Cross-reference validation
mcp__claude-flow__neural_patterns({
"action": "analyze",
"operation": "fact-checking",
"metadata": { "sources": sourcesArray }
})
```
#### Phase 3: Knowledge Management
```javascript
// Search existing knowledge base
mcp__claude-flow__memory_search({
"pattern": "topic X",
"namespace": "research",
"limit": 20
})
// Create knowledge graph connections
mcp__claude-flow__neural_patterns({
"action": "learn",
"operation": "knowledge-graph",
"metadata": {
"topic": "X",
"connections": relatedTopics,
"depth": 3
}
})
// Store connections for future use
mcp__claude-flow__memory_usage({
"action": "store",
"key": "knowledge-graph-X",
"value": JSON.stringify(knowledgeGraph),
"namespace": "research/graphs",
"ttl": 2592000 // 30 days
})
```
#### Phase 4: Report Generation
```javascript
// Orchestrate report generation
mcp__claude-flow__task_orchestrate({
"task": "generate comprehensive research report",
"strategy": "sequential",
"priority": "high",
"dependencies": ["gather", "analyze", "validate", "synthesize"]
})
// Monitor research progress
mcp__claude-flow__swarm_status({
"swarmId": "research-swarm"
})
// Generate final report
mcp__claude-flow__workflow_execute({
"workflowId": "research-report-generation",
"params": {
"findings": findings,
"format": "comprehensive",
"sections": ["executive-summary", "methodology", "findings", "analysis", "conclusions", "references"]
}
})
```
### CLI Fallback
```bash
# Quick research swarm
npx Codex-flow swarm "research AI trends in 2025" \
--strategy research \
--mode distributed \
--max-agents 6 \
--parallel \
--output research-report.md
```
## Pattern 2: Development Swarm
### Purpose
Full-stack development through coordinated specialist agents.
### Architecture
```javascript
// Initialize development swarm with hierarchy
mcp__claude-flow__swarm_init({
"topology": "hierarchical",
"maxAgents": 8,
"strategy": "balanced"
})
// Spawn development team
const devTeam = [
{ type: "architect", name: "System Architect", role: "coordinator" },
{ type: "coder", name: "Backend Developer", capabilities: ["node", "api", "database"] },
{ type: "coder", name: "Frontend Developer", capabilities: ["react", "ui", "ux"] },
{ type: "coder", name: "Database Engineer", capabilities: ["sql", "nosql", "optimization"] },
{ type: "tester", name: "QA Engineer", capabilities: ["unit", "integration", "e2e"] },
{ type: "reviewer", name: "Code Reviewer", capabilities: ["security", "performance", "best-practices"] },
{ type: "documenter", name: "Technical Writer", capabilities: ["api-docs", "guides", "tutorials"] },
{ type: "monitor", name: "DevOps Engineer", capabilities: ["ci-cd", "deployment", "monitoring"] }
]
// Spawn all team members
devTeam.forEach(member => {
mcp__claude-flow__agent_spawn({
type: member.type,
name: member.name,
capabilities: member.capabilities,
swarmId: "dev-swarm"
})
})
```
### Development Workflow
#### Phase 1: Architecture and Design
```javascript
// System architecture design
mcp__claude-flow__task_orchestrate({
"task": "design system architecture for REST API",
"strategy": "sequential",
"priority": "critical",
"assignTo": "System Architect"
})
// Store architecture decisions
mcp__claude-flow__memory_usage({
"action": "store",
"key": "architecture-decisions",
"value": JSON.stringify(architectureDoc),
"namespace": "development/design"
})
```
#### Phase 2: Parallel Implementation
```javascript
// Parallel development tasks
mcp__claude-flow__parallel_execute({
"tasks": [
{
"id": "backend-api",
"command": "implement REST API endpoints",
"assignTo": "Backend Developer"
},
{
"id": "frontend-ui",
"command": "build user interface components",
"assignTo": "Frontend Developer"
},
{
"id": "database-schema",
"command": "design and implement database schema",
"assignTo": "Database Engineer"
},
{
"id": "api-documentation",
"command": "create API documentation",
"assignTo": "Technical Writer"
}
]
})
// Monitor development progress
mcp__claude-flow__swarm_monitor({
"swarmId": "dev-swarm",
"interval": 5000
})
```
#### Phase 3: Testing and Validation
```javascript
// Comprehensive testing
mcp__claude-flow__batch_process({
"items": [
{ type: "unit", target: "all-modules" },
{ type: "integration", target: "api-endpoints" },
{ type: "e2e", target: "user-flows" },
{ type: "performance", target: "critical-paths" }
],
"operation": "execute-tests"
})
// Quality assessment
mcp__claude-flow__quality_assess({
"target": "codebase",
"criteria": ["coverage", "complexity", "maintainability", "security"]
})
```
#### Phase 4: Review and Deployment
```javascript
// Code review workflow
mcp__claude-flow__workflow_execute({
"workflowId": "code-review-process",
"params": {
"reviewers": ["Code Reviewer"],
"criteria": ["security", "performance", "best-practices"]
}
})
// CI/CD pipeline
mcp__claude-flow__pipeline_create({
"config": {
"stages": ["build", "test", "security-scan", "deploy"],
"environment": "production"
}
})
```
### CLI Fallback
```bash
# Quick development swarm
npx Codex-flow swarm "build REST API with authentication" \
--strategy development \
--mode hierarchical \
--monitor \
--output sqlite
```
## Pattern 3: Testing Swarm
### Purpose
Comprehensive quality assurance through distributed testing.
### Architecture
```javascript
// Initialize testing swarm with star topology
mcp__claude-flow__swarm_init({
"topology": "star",
"maxAgents": 7,
"strategy": "parallel"
})
// Spawn testing team
const testingTeam = [
{
type: "tester",
name: "Unit Test Coordinator",
capabilities: ["unit-testing", "mocking", "coverage", "tdd"]
},
{
type: "tester",
name: "Integration Tester",
capabilities: ["integration", "api-testing", "contract-testing"]
},
{
type: "tester",
name: "E2E Tester",
capabilities: ["e2e", "ui-testing", "user-flows", "selenium"]
},
{
type: "tester",
name: "Performance Tester",
capabilities: ["load-testing", "stress-testing", "benchmarking"]
},
{
type: "monitor",
name: "Security Tester",
capabilities: ["security-testing", "penetration-testing", "vulnerability-scanning"]
},
{
type: "analyst",
name: "Test Analyst",
capabilities: ["coverage-analysis", "test-optimization", "reporting"]
},
{
type: "documenter",
name: "Test Documenter",
capabilities: ["test-documentation", "test-plans", "reports"]
}
]
// Spawn all testers
testingTeam.forEach(tester => {
mcp__claude-flow__agent_spawn({
type: tester.type,
name: tester.name,
capabilities: tester.capabilities,
swarmId: "testing-swarm"
})
})
```
### Testing Workflow
#### Phase 1: Test Planning
```javascript
// Analyze test coverage requirements
mcp__claude-flow__quality_assess({
"target": "test-coverage",
"criteria": [
"line-coverage",
"branch-coverage",
"function-coverage",
"edge-cases"
]
})
// Identify test scenarios
mcp__claude-flow__pattern_recognize({
"data": testScenarios,
"patterns": [
"edge-case",
"boundary-condition",
"error-path",
"happy-path"
]
})
// Store test plan
mcp__claude-flow__memory_usage({
"action": "store",
"key": "test-plan-" + Date.now(),
"value": JSON.stringify(testPlan),
"namespace": "testing/plans"
})
```
#### Phase 2: Parallel Test Execution
```javascript
// Execute all test suites in parallel
mcp__claude-flow__parallel_execute({
"tasks": [
{
"id": "unit-tests",
"command": "npm run test:unit",
"assignTo": "Unit Test Coordinator"
},
{
"id": "integration-tests",
"command": "npm run test:integration",
"assignTo": "Integration Tester"
},
{
"id": "e2e-tests",
"command": "npm run test:e2e",
"assignTo": "E2E Tester"
},
{
"id": "performance-tests",
"command": "npm run test:performance",
"assignTo": "Performance Tester"
},
{
"id": "security-tests",
"command": "npm run test:security",
"assignTo": "Security Tester"
}
]
})
// Batch process test suites
mcp__claude-flow__batch_process({
"items": testSuites,
"operation": "execute-test-suite"
})
```
#### Phase 3: Performance and Security
```javascript
// Run performance benchmarks
mcp__claude-flow__benchmark_run({
"suite": "comprehensive-performance"
})
// Bottleneck analysis
mcp__claude-flow__bottleneck_analyze({
"component": "application",
"metrics": ["response-time", "throughput", "memory", "cpu"]
})
// Security scanning
mcp__claude-flow__security_scan({
"target": "application",
"depth": "comprehensive"
})
// Vulnerability analysis
mcp__claude-flow__error_analysis({
"logs": securityScanLogs
})
```
#### Phase 4: Monitoring and Reporting
```javascript
// Real-time test monitoring
mcp__claude-flow__swarm_monitor({
"swarmId": "testing-swarm",
"interval": 2000
})
// Generate comprehensive test report
mcp__claude-flow__performance_report({
"format": "detailed",
"timeframe": "current-run"
})
// Get test results
mcp__claude-flow__task_results({
"taskId": "test-execution-001"
})
// Trend analysis
mcp__claude-flow__trend_analysis({
"metric": "test-coverage",
"period": "30d"
})
```
### CLI Fallback
```bash
# Quick testing swarm
npx Codex-flow swarm "test application comprehensively" \
--strategy testing \
--mode star \
--parallel \
--timeout 600
```
## Pattern 4: Analysis Swarm
### Purpose
Deep code and system analysis through specialized analyzers.
### Architecture
```javascript
// Initialize analysis swarm
mcp__claude-flow__swarm_init({
"topology": "mesh",
"maxAgents": 5,
"strategy": "adaptive"
})
// Spawn analysis specialists
const analysisTeam = [
{
type: "analyst",
name: "Code Analyzer",
capabilities: ["static-analysis", "complexity-analysis", "dead-code-detection"]
},
{
type: "analyst",
name: "Security Analyzer",
capabilities: ["security-scan", "vulnerability-detection", "dependency-audit"]
},
{
type: "analyst",
name: "Performance Analyzer",
capabilities: ["profiling", "bottleneck-detection", "optimization"]
},
{
type: "analyst",
name: "Architecture Analyzer",
capabilities: ["dependency-analysis", "coupling-detection", "modularity-assessment"]
},
{
type: "documenter",
name: "Analysis Reporter",
capabilities: ["reporting", "visualization", "recommendations"]
}
]
// Spawn all analysts
analysisTeam.forEach(analyst => {
mcp__claude-flow__agent_spawn({
type: analyst.type,
name: analyst.name,
capabilities: analyst.capabilities
})
})
```
### Analysis Workflow
```javascript
// Parallel analysis execution
mcp__claude-flow__parallel_execute({
"tasks": [
{ "id": "analyze-code", "command": "analyze codebase structure and quality" },
{ "id": "analyze-security", "command": "scan for security vulnerabilities" },
{ "id": "analyze-performance", "command": "identify performance bottlenecks" },
{ "id": "analyze-architecture", "command": "assess architectural patterns" }
]
})
// Generate comprehensive analysis report
mcp__claude-flow__performance_report({
"format": "detailed",
"timeframe": "current"
})
// Cost analysis
mcp__claude-flow__cost_analysis({
"timeframe": "30d"
})
```
## Advanced Techniques
### Error Handling and Fault Tolerance
```javascript
// Setup fault tolerance for all agents
mcp__claude-flow__daa_fault_tolerance({
"agentId": "all",
"strategy": "auto-recovery"
})
// Error handling pattern
try {
await mcp__claude-flow__task_orchestrate({
"task": "complex operation",
"strategy": "parallel",
"priority": "high"
})
} catch (error) {
// Check swarm health
const status = await mcp__claude-flow__swarm_status({})
// Analyze error patterns
await mcp__claude-flow__error_analysis({
"logs": [error.message]
})
// Auto-recovery attempt
if (status.healthy) {
await mcp__claude-flow__task_orchestrate({
"task": "retry failed operation",
"strategy": "sequential"
})
}
}
```
### Memory and State Management
```javascript
// Cross-session persistence
mcp__claude-flow__memory_persist({
"sessionId": "swarm-session-001"
})
// Namespace management for different swarms
mcp__claude-flow__memory_namespace({
"namespace": "research-swarm",
"action": "create"
})
// Create state snapshot
mcp__claude-flow__state_snapshot({
"name": "development-checkpoint-1"
})
// Restore from snapshot if needed
mcp__claude-flow__context_restore({
"snapshotId": "development-checkpoint-1"
})
// Backup memory stores
mcp__claude-flow__memory_backup({
"path": "/workspaces/Codex-flow/backups/swarm-memory.json"
})
```
### Neural Pattern Learning
```javascript
// Train neural patterns from successful workflows
mcp__claude-flow__neural_train({
"pattern_type": "coordination",
"training_data": JSON.stringify(successfulWorkflows),
"epochs": 50
})
// Adaptive learning from experience
mcp__claude-flow__learning_adapt({
"experience": {
"workflow": "research-to-report",
"success": true,
"duration": 3600,
"quality": 0.95
}
})
// Pattern recognition for optimization
mcp__claude-flow__pattern_recognize({
"data": workflowMetrics,
"patterns": ["bottleneck", "optimization-opportunity", "efficiency-gain"]
})
```
### Workflow Automation
```javascript
// Create reusable workflow
mcp__claude-flow__workflow_create({
"name": "full-stack-development",
"steps": [
{ "phase": "design", "agents": ["architect"] },
{ "phase": "implement", "agents": ["backend-dev", "frontend-dev"], "parallel": true },
{ "phase": "test", "agents": ["tester", "security-tester"], "parallel": true },
{ "phase": "review", "agents": ["reviewer"] },
{ "phase": "deploy", "agents": ["devops"] }
],
"triggers": ["on-commit", "scheduled-daily"]
})
// Setup automation rules
mcp__claude-flow__automation_setup({
"rules": [
{
"trigger": "file-changed",
"pattern": "*.js",
"action": "run-tests"
},
{
"trigger": "PR-created",
"action": "code-review-swarm"
}
]
})
// Event-driven triggers
mcp__claude-flow__trigger_setup({
"events": ["code-commit", "PR-merge", "deployment"],
"actions": ["test", "analyze", "document"]
})
```
### Performance Optimization
```javascript
// Topology optimization
mcp__claude-flow__topology_optimize({
"swarmId": "current-swarm"
})
// Load balancing
mcp__claude-flow__load_balance({
"swarmId": "development-swarm",
"tasks": taskQueue
})
// Agent coordination sync
mcp__claude-flow__coordination_sync({
"swarmId": "development-swarm"
})
// Auto-scaling
mcp__claude-flow__swarm_scale({
"swarmId": "development-swarm",
"targetSize": 12
})
```
### Monitoring and Metrics
```javascript
// Real-time swarm monitoring
mcp__claude-flow__swarm_monitor({
"swarmId": "active-swarm",
"interval": 3000
})
// Collect comprehensive metrics
mcp__claude-flow__metrics_collect({
"components": ["agents", "tasks", "memory", "performance"]
})
// Health monitoring
mcp__claude-flow__health_check({
"components": ["swarm", "agents", "neural", "memory"]
})
// Usage statistics
mcp__claude-flow__usage_stats({
"component": "swarm-orchestration"
})
// Trend analysis
mcp__claude-flow__trend_analysis({
"metric": "agent-performance",
"period": "7d"
})
```
## Best Practices
### 1. Choosing the Right Topology
- **Mesh**: Research, brainstorming, collaborative analysis
- **Hierarchical**: Structured development, sequential workflows
- **Star**: Testing, validation, centralized coordination
- **Ring**: Pipeline processing, staged workflows
### 2. Agent Specialization
- Assign specific capabilities to each agent
- Avoid overlapping responsibilities
- Use coordination agents for complex workflows
- Leverage memory for agent communication
### 3. Parallel Execution
- Identify independent tasks for parallelization
- Use sequential execution for dependent tasks
- Monitor resource usage during parallel execution
- Implement proper error handling
### 4. Memory Management
- Use namespaces to organize memory
- Set appropriate TTL values
- Create regular backups
- Implement state snapshots for checkpoints
### 5. Monitoring and Optimization
- Monitor swarm health regularly
- Collect and analyze metrics
- Optimize topology based on performance
- Use neural patterns to learn from success
### 6. Error Recovery
- Implement fault tolerance strategies
- Use auto-recovery mechanisms
- Analyze error patterns
- Create fallback workflows
## Real-World Examples
### Example 1: AI Research Project
```javascript
// Research AI trends, analyze findings, generate report
mcp__claude-flow__swarm_init({ topology: "mesh", maxAgents: 6 })
// Spawn: 2 researchers, 2 analysts, 1 synthesizer, 1 documenter
// Parallel gather → Analyze patterns → Synthesize → Report
```
### Example 2: Full-Stack Application
```javascript
// Build complete web application with testing
mcp__claude-flow__swarm_init({ topology: "hierarchical", maxAgents: 8 })
// Spawn: 1 architect, 2 devs, 1 db engineer, 2 testers, 1 reviewer, 1 devops
// Design → Parallel implement → Test → Review → Deploy
```
### Example 3: Security Audit
```javascript
// Comprehensive security analysis
mcp__claude-flow__swarm_init({ topology: "star", maxAgents: 5 })
// Spawn: 1 coordinator, 1 code analyzer, 1 security scanner, 1 penetration tester, 1 reporter
// Parallel scan → Vulnerability analysis → Penetration test → Report
```
### Example 4: Performance Optimization
```javascript
// Identify and fix performance bottlenecks
mcp__claude-flow__swarm_init({ topology: "mesh", maxAgents: 4 })
// Spawn: 1 profiler, 1 bottleneck analyzer, 1 optimizer, 1 tester
// Profile → Identify bottlenecks → Optimize → Validate
```
## Troubleshooting
### Common Issues
**Issue**: Swarm agents not coordinating properly
**Solution**: Check topology selection, verify memory usage, enable monitoring
**Issue**: Parallel execution failing
**Solution**: Verify task dependencies, check resource limits, implement error handling
**Issue**: Memory persistence not working
**Solution**: Verify namespaces, check TTL settings, ensure backup configuration
**Issue**: Performance degradation
**Solution**: Optimize topology, reduce agent count, analyze bottlenecks
## Related Skills
- `sparc-methodology` - Systematic development workflow
- `github-integration` - Repository management and automation
- `neural-patterns` - AI-powered coordination optimization
- `memory-management` - Cross-session state persistence
## References
- [Codex Flow Documentation](https://github.com/ruvnet/Codex-flow)
- [Swarm Orchestration Guide](https://github.com/ruvnet/Codex-flow/wiki/swarm)
- [MCP Tools Reference](https://github.com/ruvnet/Codex-flow/wiki/mcp)
- [Performance Optimization](https://github.com/ruvnet/Codex-flow/wiki/performance)
---
**Version**: 2.0.0
**Last Updated**: 2025-10-19
**Skill Level**: Advanced
**Estimated Learning Time**: 2-3 hours
-114
View File
@@ -1,114 +0,0 @@
---
name: swarm-orchestration
description: >
Multi-agent swarm coordination for complex tasks. Uses hierarchical topology with specialized agents to break down and execute complex work across multiple files and modules.
Use when: 3+ files need changes, new feature implementation, cross-module refactoring, API changes with tests, security-related changes, performance optimization across codebase, database schema changes.
Skip when: single file edits, simple bug fixes (1-2 lines), documentation updates, configuration changes, quick exploration.
---
# Swarm Orchestration Skill
## Purpose
Multi-agent swarm coordination for complex tasks. Uses hierarchical topology with specialized agents to break down and execute complex work across multiple files and modules.
## When to Trigger
- 3+ files need changes
- new feature implementation
- cross-module refactoring
- API changes with tests
- security-related changes
- performance optimization across codebase
- database schema changes
## When to Skip
- single file edits
- simple bug fixes (1-2 lines)
- documentation updates
- configuration changes
- quick exploration
## Commands
### Initialize Swarm
Start a new swarm with hierarchical topology (anti-drift)
```bash
npx @claude-flow/cli swarm init --topology hierarchical --max-agents 8 --strategy specialized
```
**Example:**
```bash
npx @claude-flow/cli swarm init --topology hierarchical --max-agents 6 --strategy specialized
```
### Route Task
Route a task to the appropriate agents based on task type
```bash
npx @claude-flow/cli hooks route --task "[task description]"
```
**Example:**
```bash
npx @claude-flow/cli hooks route --task "implement OAuth2 authentication flow"
```
### Spawn Agent
Spawn a specific agent type
```bash
npx @claude-flow/cli agent spawn --type [type] --name [name]
```
**Example:**
```bash
npx @claude-flow/cli agent spawn --type coder --name impl-auth
```
### Monitor Status
Check the current swarm status
```bash
npx @claude-flow/cli swarm status --verbose
```
### Orchestrate Task
Orchestrate a task across multiple agents
```bash
npx @claude-flow/cli task orchestrate --task "[task]" --strategy adaptive
```
**Example:**
```bash
npx @claude-flow/cli task orchestrate --task "refactor auth module" --strategy parallel --max-agents 4
```
### List Agents
List all active agents
```bash
npx @claude-flow/cli agent list --filter active
```
## Scripts
| Script | Path | Description |
|--------|------|-------------|
| `swarm-start` | `.agents/scripts/swarm-start.sh` | Initialize swarm with default settings |
| `swarm-monitor` | `.agents/scripts/swarm-monitor.sh` | Real-time swarm monitoring dashboard |
## References
| Document | Path | Description |
|----------|------|-------------|
| `Agent Types` | `docs/agents.md` | Complete list of agent types and capabilities |
| `Topology Guide` | `docs/topology.md` | Swarm topology configuration guide |
## Best Practices
1. Check memory for existing patterns before starting
2. Use hierarchical topology for coordination
3. Store successful patterns after completion
4. Document any new learnings
@@ -1,872 +0,0 @@
---
name: "V3 CLI Modernization"
description: "CLI modernization and hooks system enhancement for Codex-flow v3. Implements interactive prompts, command decomposition, enhanced hooks integration, and intelligent workflow automation."
---
# V3 CLI Modernization
## What This Skill Does
Modernizes Codex-flow v3 CLI with interactive prompts, intelligent command decomposition, enhanced hooks integration, performance optimization, and comprehensive workflow automation capabilities.
## Quick Start
```bash
# Initialize CLI modernization analysis
Task("CLI architecture", "Analyze current CLI structure and identify optimization opportunities", "cli-hooks-developer")
# Modernization implementation (parallel)
Task("Command decomposition", "Break down large CLI files into focused modules", "cli-hooks-developer")
Task("Interactive prompts", "Implement intelligent interactive CLI experience", "cli-hooks-developer")
Task("Hooks enhancement", "Deep integrate hooks with CLI lifecycle", "cli-hooks-developer")
```
## CLI Architecture Modernization
### Current State Analysis
```
Current CLI Issues:
├── index.ts: 108KB monolithic file
├── enterprise.ts: 68KB feature module
├── Limited interactivity: Basic command parsing
├── Hooks integration: Basic pre/post execution
└── No intelligent workflows: Manual command chaining
Target Architecture:
├── Modular Commands: <500 lines per command
├── Interactive Prompts: Smart context-aware UX
├── Enhanced Hooks: Deep lifecycle integration
├── Workflow Automation: Intelligent command orchestration
└── Performance: <200ms command response time
```
### Modular Command Architecture
```typescript
// src/cli/core/command-registry.ts
interface CommandModule {
name: string;
description: string;
category: CommandCategory;
handler: CommandHandler;
middleware: MiddlewareStack;
permissions: Permission[];
examples: CommandExample[];
}
export class ModularCommandRegistry {
private commands = new Map<string, CommandModule>();
private categories = new Map<CommandCategory, CommandModule[]>();
private aliases = new Map<string, string>();
registerCommand(command: CommandModule): void {
this.commands.set(command.name, command);
// Register in category index
if (!this.categories.has(command.category)) {
this.categories.set(command.category, []);
}
this.categories.get(command.category)!.push(command);
}
async executeCommand(name: string, args: string[]): Promise<CommandResult> {
const command = this.resolveCommand(name);
if (!command) {
throw new CommandNotFoundError(name, this.getSuggestions(name));
}
// Execute middleware stack
const context = await this.buildExecutionContext(command, args);
const result = await command.middleware.execute(context);
return result;
}
private resolveCommand(name: string): CommandModule | undefined {
// Try exact match first
if (this.commands.has(name)) {
return this.commands.get(name);
}
// Try alias
const aliasTarget = this.aliases.get(name);
if (aliasTarget) {
return this.commands.get(aliasTarget);
}
// Try fuzzy match
return this.findFuzzyMatch(name);
}
}
```
## Command Decomposition Strategy
### Swarm Commands Module
```typescript
// src/cli/commands/swarm/swarm.command.ts
@Command({
name: 'swarm',
description: 'Swarm coordination and management',
category: 'orchestration'
})
export class SwarmCommand {
constructor(
private swarmCoordinator: UnifiedSwarmCoordinator,
private promptService: InteractivePromptService
) {}
@SubCommand('init')
@Option('--topology', 'Swarm topology (mesh|hierarchical|adaptive)', 'hierarchical')
@Option('--agents', 'Number of agents to spawn', 5)
@Option('--interactive', 'Interactive agent configuration', false)
async init(
@Arg('projectName') projectName: string,
options: SwarmInitOptions
): Promise<CommandResult> {
if (options.interactive) {
return this.interactiveSwarmInit(projectName);
}
return this.quickSwarmInit(projectName, options);
}
private async interactiveSwarmInit(projectName: string): Promise<CommandResult> {
console.log(`🚀 Initializing Swarm for ${projectName}`);
// Interactive topology selection
const topology = await this.promptService.select({
message: 'Select swarm topology:',
choices: [
{ name: 'Hierarchical (Queen-led coordination)', value: 'hierarchical' },
{ name: 'Mesh (Peer-to-peer collaboration)', value: 'mesh' },
{ name: 'Adaptive (Dynamic topology switching)', value: 'adaptive' }
]
});
// Agent configuration
const agents = await this.promptAgentConfiguration();
// Initialize with configuration
const swarm = await this.swarmCoordinator.initialize({
name: projectName,
topology,
agents,
hooks: {
onAgentSpawn: this.handleAgentSpawn.bind(this),
onTaskComplete: this.handleTaskComplete.bind(this),
onSwarmComplete: this.handleSwarmComplete.bind(this)
}
});
return CommandResult.success({
message: `✅ Swarm ${projectName} initialized with ${agents.length} agents`,
data: { swarmId: swarm.id, topology, agentCount: agents.length }
});
}
@SubCommand('status')
async status(): Promise<CommandResult> {
const swarms = await this.swarmCoordinator.listActiveSwarms();
if (swarms.length === 0) {
return CommandResult.info('No active swarms found');
}
// Interactive swarm selection if multiple
const selectedSwarm = swarms.length === 1
? swarms[0]
: await this.promptService.select({
message: 'Select swarm to inspect:',
choices: swarms.map(s => ({
name: `${s.name} (${s.agents.length} agents, ${s.topology})`,
value: s
}))
});
return this.displaySwarmStatus(selectedSwarm);
}
}
```
### Learning Commands Module
```typescript
// src/cli/commands/learning/learning.command.ts
@Command({
name: 'learning',
description: 'Learning system management and optimization',
category: 'intelligence'
})
export class LearningCommand {
constructor(
private learningService: IntegratedLearningService,
private promptService: InteractivePromptService
) {}
@SubCommand('start')
@Option('--algorithm', 'RL algorithm to use', 'auto')
@Option('--tier', 'Learning tier (basic|standard|advanced)', 'standard')
async start(options: LearningStartOptions): Promise<CommandResult> {
// Auto-detect optimal algorithm if not specified
if (options.algorithm === 'auto') {
const taskContext = await this.analyzeCurrentContext();
options.algorithm = this.learningService.selectOptimalAlgorithm(taskContext);
console.log(`🧠 Auto-selected ${options.algorithm} algorithm based on context`);
}
const session = await this.learningService.startSession({
algorithm: options.algorithm,
tier: options.tier,
userId: await this.getCurrentUser()
});
return CommandResult.success({
message: `🚀 Learning session started with ${options.algorithm}`,
data: { sessionId: session.id, algorithm: options.algorithm, tier: options.tier }
});
}
@SubCommand('feedback')
@Arg('reward', 'Reward value (0-1)', 'number')
async feedback(
@Arg('reward') reward: number,
@Option('--context', 'Additional context for learning')
context?: string
): Promise<CommandResult> {
const activeSession = await this.learningService.getActiveSession();
if (!activeSession) {
return CommandResult.error('No active learning session found. Start one with `learning start`');
}
await this.learningService.submitFeedback({
sessionId: activeSession.id,
reward,
context,
timestamp: new Date()
});
return CommandResult.success({
message: `📊 Feedback recorded (reward: ${reward})`,
data: { reward, sessionId: activeSession.id }
});
}
@SubCommand('metrics')
async metrics(): Promise<CommandResult> {
const metrics = await this.learningService.getMetrics();
// Interactive metrics display
await this.displayInteractiveMetrics(metrics);
return CommandResult.success('Metrics displayed');
}
}
```
## Interactive Prompt System
### Advanced Prompt Service
```typescript
// src/cli/services/interactive-prompt.service.ts
interface PromptOptions {
message: string;
type: 'select' | 'multiselect' | 'input' | 'confirm' | 'progress';
choices?: PromptChoice[];
default?: any;
validate?: (input: any) => boolean | string;
transform?: (input: any) => any;
}
export class InteractivePromptService {
private inquirer: any; // Dynamic import for tree-shaking
async select<T>(options: SelectPromptOptions<T>): Promise<T> {
const { default: inquirer } = await import('inquirer');
const result = await inquirer.prompt([{
type: 'list',
name: 'selection',
message: options.message,
choices: options.choices,
default: options.default
}]);
return result.selection;
}
async multiSelect<T>(options: MultiSelectPromptOptions<T>): Promise<T[]> {
const { default: inquirer } = await import('inquirer');
const result = await inquirer.prompt([{
type: 'checkbox',
name: 'selections',
message: options.message,
choices: options.choices,
validate: (input: T[]) => {
if (options.minSelections && input.length < options.minSelections) {
return `Please select at least ${options.minSelections} options`;
}
if (options.maxSelections && input.length > options.maxSelections) {
return `Please select at most ${options.maxSelections} options`;
}
return true;
}
}]);
return result.selections;
}
async input(options: InputPromptOptions): Promise<string> {
const { default: inquirer } = await import('inquirer');
const result = await inquirer.prompt([{
type: 'input',
name: 'input',
message: options.message,
default: options.default,
validate: options.validate,
transformer: options.transform
}]);
return result.input;
}
async progressTask<T>(
task: ProgressTask<T>,
options: ProgressOptions
): Promise<T> {
const { default: cliProgress } = await import('cli-progress');
const progressBar = new cliProgress.SingleBar({
format: `${options.title} |{bar}| {percentage}% | {status}`,
barCompleteChar: '█',
barIncompleteChar: '░',
hideCursor: true
});
progressBar.start(100, 0, { status: 'Starting...' });
try {
const result = await task({
updateProgress: (percent: number, status?: string) => {
progressBar.update(percent, { status: status || 'Processing...' });
}
});
progressBar.update(100, { status: 'Complete!' });
progressBar.stop();
return result;
} catch (error) {
progressBar.stop();
throw error;
}
}
async confirmWithDetails(
message: string,
details: ConfirmationDetails
): Promise<boolean> {
console.log('\n' + chalk.bold(message));
console.log(chalk.gray('Details:'));
for (const [key, value] of Object.entries(details)) {
console.log(chalk.gray(` ${key}: ${value}`));
}
return this.confirm('\nProceed?');
}
}
```
## Enhanced Hooks Integration
### Deep CLI Hooks Integration
```typescript
// src/cli/hooks/cli-hooks-manager.ts
interface CLIHookEvent {
type: 'command_start' | 'command_end' | 'command_error' | 'agent_spawn' | 'task_complete';
command: string;
args: string[];
context: ExecutionContext;
timestamp: Date;
}
export class CLIHooksManager {
private hooks: Map<string, HookHandler[]> = new Map();
private learningIntegration: LearningHooksIntegration;
constructor() {
this.learningIntegration = new LearningHooksIntegration();
this.setupDefaultHooks();
}
private setupDefaultHooks(): void {
// Learning integration hooks
this.registerHook('command_start', async (event: CLIHookEvent) => {
await this.learningIntegration.recordCommandStart(event);
});
this.registerHook('command_end', async (event: CLIHookEvent) => {
await this.learningIntegration.recordCommandSuccess(event);
});
this.registerHook('command_error', async (event: CLIHookEvent) => {
await this.learningIntegration.recordCommandError(event);
});
// Intelligent suggestions
this.registerHook('command_start', async (event: CLIHookEvent) => {
const suggestions = await this.generateIntelligentSuggestions(event);
if (suggestions.length > 0) {
this.displaySuggestions(suggestions);
}
});
// Performance monitoring
this.registerHook('command_end', async (event: CLIHookEvent) => {
await this.recordPerformanceMetrics(event);
});
}
async executeHooks(type: string, event: CLIHookEvent): Promise<void> {
const handlers = this.hooks.get(type) || [];
await Promise.all(handlers.map(handler =>
this.executeHookSafely(handler, event)
));
}
private async generateIntelligentSuggestions(event: CLIHookEvent): Promise<Suggestion[]> {
const context = await this.learningIntegration.getExecutionContext(event);
const patterns = await this.learningIntegration.findSimilarPatterns(context);
return patterns.map(pattern => ({
type: 'optimization',
message: `Based on similar executions, consider: ${pattern.suggestion}`,
confidence: pattern.confidence
}));
}
}
```
### Learning Integration
```typescript
// src/cli/hooks/learning-hooks-integration.ts
export class LearningHooksIntegration {
constructor(
private agenticFlowHooks: AgenticFlowHooksClient,
private agentDBLearning: AgentDBLearningClient
) {}
async recordCommandStart(event: CLIHookEvent): Promise<void> {
// Start trajectory tracking
await this.agenticFlowHooks.trajectoryStart({
sessionId: event.context.sessionId,
command: event.command,
args: event.args,
context: event.context
});
// Record experience in AgentDB
await this.agentDBLearning.recordExperience({
type: 'command_execution',
state: this.encodeCommandState(event),
action: event.command,
timestamp: event.timestamp
});
}
async recordCommandSuccess(event: CLIHookEvent): Promise<void> {
const executionTime = Date.now() - event.timestamp.getTime();
const reward = this.calculateReward(event, executionTime, true);
// Complete trajectory
await this.agenticFlowHooks.trajectoryEnd({
sessionId: event.context.sessionId,
success: true,
reward,
verdict: 'positive'
});
// Submit feedback to learning system
await this.agentDBLearning.submitFeedback({
sessionId: event.context.learningSessionId,
reward,
success: true,
latencyMs: executionTime
});
// Store successful pattern
if (reward > 0.8) {
await this.agenticFlowHooks.storePattern({
pattern: event.command,
solution: event.context.result,
confidence: reward
});
}
}
async recordCommandError(event: CLIHookEvent): Promise<void> {
const executionTime = Date.now() - event.timestamp.getTime();
const reward = this.calculateReward(event, executionTime, false);
// Complete trajectory with error
await this.agenticFlowHooks.trajectoryEnd({
sessionId: event.context.sessionId,
success: false,
reward,
verdict: 'negative',
error: event.context.error
});
// Learn from failure
await this.agentDBLearning.submitFeedback({
sessionId: event.context.learningSessionId,
reward,
success: false,
latencyMs: executionTime,
error: event.context.error
});
}
private calculateReward(event: CLIHookEvent, executionTime: number, success: boolean): number {
if (!success) return 0;
// Base reward for success
let reward = 0.5;
// Performance bonus (faster execution)
const expectedTime = this.getExpectedExecutionTime(event.command);
if (executionTime < expectedTime) {
reward += 0.3 * (1 - executionTime / expectedTime);
}
// Complexity bonus
const complexity = this.calculateCommandComplexity(event);
reward += complexity * 0.2;
return Math.min(reward, 1.0);
}
}
```
## Intelligent Workflow Automation
### Workflow Orchestrator
```typescript
// src/cli/workflows/workflow-orchestrator.ts
interface WorkflowStep {
id: string;
command: string;
args: string[];
dependsOn: string[];
condition?: WorkflowCondition;
retryPolicy?: RetryPolicy;
}
export class WorkflowOrchestrator {
constructor(
private commandRegistry: ModularCommandRegistry,
private promptService: InteractivePromptService
) {}
async executeWorkflow(workflow: Workflow): Promise<WorkflowResult> {
const context = new WorkflowExecutionContext(workflow);
// Display workflow overview
await this.displayWorkflowOverview(workflow);
const confirmed = await this.promptService.confirm(
'Execute this workflow?'
);
if (!confirmed) {
return WorkflowResult.cancelled();
}
// Execute steps
return this.promptService.progressTask(
async ({ updateProgress }) => {
const steps = this.sortStepsByDependencies(workflow.steps);
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
updateProgress((i / steps.length) * 100, `Executing ${step.command}`);
await this.executeStep(step, context);
}
return WorkflowResult.success(context.getResults());
},
{ title: `Workflow: ${workflow.name}` }
);
}
async generateWorkflowFromIntent(intent: string): Promise<Workflow> {
// Use learning system to generate workflow
const patterns = await this.findWorkflowPatterns(intent);
if (patterns.length === 0) {
throw new Error('Could not generate workflow for intent');
}
// Select best pattern or let user choose
const selectedPattern = patterns.length === 1
? patterns[0]
: await this.promptService.select({
message: 'Select workflow template:',
choices: patterns.map(p => ({
name: `${p.name} (${p.confidence}% match)`,
value: p
}))
});
return this.customizeWorkflow(selectedPattern, intent);
}
private async executeStep(step: WorkflowStep, context: WorkflowExecutionContext): Promise<void> {
// Check conditions
if (step.condition && !this.evaluateCondition(step.condition, context)) {
context.skipStep(step.id, 'Condition not met');
return;
}
// Check dependencies
const missingDeps = step.dependsOn.filter(dep => !context.isStepCompleted(dep));
if (missingDeps.length > 0) {
throw new WorkflowError(`Step ${step.id} has unmet dependencies: ${missingDeps.join(', ')}`);
}
// Execute with retry policy
const retryPolicy = step.retryPolicy || { maxAttempts: 1 };
let lastError: Error | null = null;
for (let attempt = 1; attempt <= retryPolicy.maxAttempts; attempt++) {
try {
const result = await this.commandRegistry.executeCommand(step.command, step.args);
context.completeStep(step.id, result);
return;
} catch (error) {
lastError = error as Error;
if (attempt < retryPolicy.maxAttempts) {
await this.delay(retryPolicy.backoffMs || 1000);
}
}
}
throw new WorkflowError(`Step ${step.id} failed after ${retryPolicy.maxAttempts} attempts: ${lastError?.message}`);
}
}
```
## Performance Optimization
### Command Performance Monitoring
```typescript
// src/cli/performance/command-performance.ts
export class CommandPerformanceMonitor {
private metrics = new Map<string, CommandMetrics>();
async measureCommand<T>(
commandName: string,
executor: () => Promise<T>
): Promise<T> {
const start = performance.now();
const memBefore = process.memoryUsage();
try {
const result = await executor();
const end = performance.now();
const memAfter = process.memoryUsage();
this.recordMetrics(commandName, {
executionTime: end - start,
memoryDelta: memAfter.heapUsed - memBefore.heapUsed,
success: true
});
return result;
} catch (error) {
const end = performance.now();
this.recordMetrics(commandName, {
executionTime: end - start,
memoryDelta: 0,
success: false,
error: error as Error
});
throw error;
}
}
private recordMetrics(command: string, measurement: PerformanceMeasurement): void {
if (!this.metrics.has(command)) {
this.metrics.set(command, new CommandMetrics(command));
}
const metrics = this.metrics.get(command)!;
metrics.addMeasurement(measurement);
// Alert if performance degrades
if (metrics.getP95ExecutionTime() > 5000) { // 5 seconds
console.warn(`⚠️ Command '${command}' is performing slowly (P95: ${metrics.getP95ExecutionTime()}ms)`);
}
}
getCommandReport(command: string): PerformanceReport {
const metrics = this.metrics.get(command);
if (!metrics) {
throw new Error(`No metrics found for command: ${command}`);
}
return {
command,
totalExecutions: metrics.getTotalExecutions(),
successRate: metrics.getSuccessRate(),
avgExecutionTime: metrics.getAverageExecutionTime(),
p95ExecutionTime: metrics.getP95ExecutionTime(),
avgMemoryUsage: metrics.getAverageMemoryUsage(),
recommendations: this.generateRecommendations(metrics)
};
}
}
```
## Smart Auto-completion
### Intelligent Command Completion
```typescript
// src/cli/completion/intelligent-completion.ts
export class IntelligentCompletion {
constructor(
private learningService: LearningService,
private commandRegistry: ModularCommandRegistry
) {}
async generateCompletions(
partial: string,
context: CompletionContext
): Promise<Completion[]> {
const completions: Completion[] = [];
// 1. Exact command matches
const exactMatches = this.commandRegistry.findCommandsByPrefix(partial);
completions.push(...exactMatches.map(cmd => ({
value: cmd.name,
description: cmd.description,
type: 'command',
confidence: 1.0
})));
// 2. Learning-based suggestions
const learnedSuggestions = await this.learningService.suggestCommands(
partial,
context
);
completions.push(...learnedSuggestions);
// 3. Context-aware suggestions
const contextualSuggestions = await this.generateContextualSuggestions(
partial,
context
);
completions.push(...contextualSuggestions);
// Sort by confidence and relevance
return completions
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 10); // Top 10 suggestions
}
private async generateContextualSuggestions(
partial: string,
context: CompletionContext
): Promise<Completion[]> {
const suggestions: Completion[] = [];
// If in git repository, suggest git-related commands
if (context.isGitRepository) {
if (partial.startsWith('git')) {
suggestions.push({
value: 'git commit',
description: 'Create git commit with generated message',
type: 'workflow',
confidence: 0.8
});
}
}
// If package.json exists, suggest npm commands
if (context.hasPackageJson) {
if (partial.startsWith('npm') || partial.startsWith('swarm')) {
suggestions.push({
value: 'swarm init',
description: 'Initialize swarm for this project',
type: 'workflow',
confidence: 0.9
});
}
}
return suggestions;
}
}
```
## Success Metrics
### CLI Performance Targets
- [ ] **Command Response**: <200ms average command execution time
- [ ] **File Decomposition**: index.ts (108KB) → <10KB per command module
- [ ] **Interactive UX**: Smart prompts with context awareness
- [ ] **Hook Integration**: Deep lifecycle integration with learning
- [ ] **Workflow Automation**: Intelligent multi-step command orchestration
- [ ] **Auto-completion**: >90% accuracy for command suggestions
### User Experience Improvements
```typescript
const cliImprovements = {
before: {
commandResponse: '~500ms',
interactivity: 'Basic command parsing',
workflows: 'Manual command chaining',
suggestions: 'Static help text'
},
after: {
commandResponse: '<200ms with caching',
interactivity: 'Smart context-aware prompts',
workflows: 'Automated multi-step execution',
suggestions: 'Learning-based intelligent completion'
}
};
```
## Related V3 Skills
- `v3-core-implementation` - Core domain integration
- `v3-memory-unification` - Memory-backed command caching
- `v3-swarm-coordination` - CLI swarm management integration
- `v3-performance-optimization` - CLI performance monitoring
## Usage Examples
### Complete CLI Modernization
```bash
# Full CLI modernization implementation
Task("CLI modernization implementation",
"Implement modular commands, interactive prompts, and intelligent workflows",
"cli-hooks-developer")
```
### Interactive Command Enhancement
```bash
# Enhanced interactive commands
Codex-flow swarm init --interactive
Codex-flow learning start --guided
Codex-flow workflow create --from-intent "setup new project"
```
@@ -1,797 +0,0 @@
---
name: "V3 Core Implementation"
description: "Core module implementation for Codex-flow v3. Implements DDD domains, clean architecture patterns, dependency injection, and modular TypeScript codebase with comprehensive testing."
---
# V3 Core Implementation
## What This Skill Does
Implements the core TypeScript modules for Codex-flow v3 following Domain-Driven Design principles, clean architecture patterns, and modern TypeScript best practices with comprehensive test coverage.
## Quick Start
```bash
# Initialize core implementation
Task("Core foundation", "Set up DDD domain structure and base classes", "core-implementer")
# Domain implementation (parallel)
Task("Task domain", "Implement task management domain with entities and services", "core-implementer")
Task("Session domain", "Implement session management domain", "core-implementer")
Task("Health domain", "Implement health monitoring domain", "core-implementer")
```
## Core Implementation Architecture
### Domain Structure
```
src/
├── core/
│ ├── kernel/ # Microkernel pattern
│ │ ├── Codex-flow-kernel.ts
│ │ ├── domain-registry.ts
│ │ └── plugin-loader.ts
│ │
│ ├── domains/ # DDD Bounded Contexts
│ │ ├── task-management/
│ │ │ ├── entities/
│ │ │ ├── value-objects/
│ │ │ ├── services/
│ │ │ ├── repositories/
│ │ │ └── events/
│ │ │
│ │ ├── session-management/
│ │ ├── health-monitoring/
│ │ ├── lifecycle-management/
│ │ └── event-coordination/
│ │
│ ├── shared/ # Shared kernel
│ │ ├── domain/
│ │ │ ├── entity.ts
│ │ │ ├── value-object.ts
│ │ │ ├── domain-event.ts
│ │ │ └── aggregate-root.ts
│ │ │
│ │ ├── infrastructure/
│ │ │ ├── event-bus.ts
│ │ │ ├── dependency-container.ts
│ │ │ └── logger.ts
│ │ │
│ │ └── types/
│ │ ├── common.ts
│ │ ├── errors.ts
│ │ └── interfaces.ts
│ │
│ └── application/ # Application services
│ ├── use-cases/
│ ├── commands/
│ ├── queries/
│ └── handlers/
```
## Base Domain Classes
### Entity Base Class
```typescript
// src/core/shared/domain/entity.ts
export abstract class Entity<T> {
protected readonly _id: T;
private _domainEvents: DomainEvent[] = [];
constructor(id: T) {
this._id = id;
}
get id(): T {
return this._id;
}
public equals(object?: Entity<T>): boolean {
if (object == null || object == undefined) {
return false;
}
if (this === object) {
return true;
}
if (!(object instanceof Entity)) {
return false;
}
return this._id === object._id;
}
protected addDomainEvent(domainEvent: DomainEvent): void {
this._domainEvents.push(domainEvent);
}
public getUncommittedEvents(): DomainEvent[] {
return this._domainEvents;
}
public markEventsAsCommitted(): void {
this._domainEvents = [];
}
}
```
### Value Object Base Class
```typescript
// src/core/shared/domain/value-object.ts
export abstract class ValueObject<T> {
protected readonly props: T;
constructor(props: T) {
this.props = Object.freeze(props);
}
public equals(object?: ValueObject<T>): boolean {
if (object == null || object == undefined) {
return false;
}
if (this === object) {
return true;
}
return JSON.stringify(this.props) === JSON.stringify(object.props);
}
get value(): T {
return this.props;
}
}
```
### Aggregate Root
```typescript
// src/core/shared/domain/aggregate-root.ts
export abstract class AggregateRoot<T> extends Entity<T> {
private _version: number = 0;
get version(): number {
return this._version;
}
protected incrementVersion(): void {
this._version++;
}
public applyEvent(event: DomainEvent): void {
this.addDomainEvent(event);
this.incrementVersion();
}
}
```
## Task Management Domain Implementation
### Task Entity
```typescript
// src/core/domains/task-management/entities/task.entity.ts
import { AggregateRoot } from '../../../shared/domain/aggregate-root';
import { TaskId } from '../value-objects/task-id.vo';
import { TaskStatus } from '../value-objects/task-status.vo';
import { Priority } from '../value-objects/priority.vo';
import { TaskAssignedEvent } from '../events/task-assigned.event';
interface TaskProps {
id: TaskId;
description: string;
priority: Priority;
status: TaskStatus;
assignedAgentId?: string;
createdAt: Date;
updatedAt: Date;
}
export class Task extends AggregateRoot<TaskId> {
private props: TaskProps;
private constructor(props: TaskProps) {
super(props.id);
this.props = props;
}
static create(description: string, priority: Priority): Task {
const task = new Task({
id: TaskId.create(),
description,
priority,
status: TaskStatus.pending(),
createdAt: new Date(),
updatedAt: new Date()
});
return task;
}
static reconstitute(props: TaskProps): Task {
return new Task(props);
}
public assignTo(agentId: string): void {
if (this.props.status.equals(TaskStatus.completed())) {
throw new Error('Cannot assign completed task');
}
this.props.assignedAgentId = agentId;
this.props.status = TaskStatus.assigned();
this.props.updatedAt = new Date();
this.applyEvent(new TaskAssignedEvent(
this.id.value,
agentId,
this.props.priority
));
}
public complete(result: TaskResult): void {
if (!this.props.assignedAgentId) {
throw new Error('Cannot complete unassigned task');
}
this.props.status = TaskStatus.completed();
this.props.updatedAt = new Date();
this.applyEvent(new TaskCompletedEvent(
this.id.value,
result,
this.calculateDuration()
));
}
// Getters
get description(): string { return this.props.description; }
get priority(): Priority { return this.props.priority; }
get status(): TaskStatus { return this.props.status; }
get assignedAgentId(): string | undefined { return this.props.assignedAgentId; }
get createdAt(): Date { return this.props.createdAt; }
get updatedAt(): Date { return this.props.updatedAt; }
private calculateDuration(): number {
return this.props.updatedAt.getTime() - this.props.createdAt.getTime();
}
}
```
### Task Value Objects
```typescript
// src/core/domains/task-management/value-objects/task-id.vo.ts
export class TaskId extends ValueObject<string> {
private constructor(value: string) {
super({ value });
}
static create(): TaskId {
return new TaskId(crypto.randomUUID());
}
static fromString(id: string): TaskId {
if (!id || id.length === 0) {
throw new Error('TaskId cannot be empty');
}
return new TaskId(id);
}
get value(): string {
return this.props.value;
}
}
// src/core/domains/task-management/value-objects/task-status.vo.ts
type TaskStatusType = 'pending' | 'assigned' | 'in_progress' | 'completed' | 'failed';
export class TaskStatus extends ValueObject<TaskStatusType> {
private constructor(status: TaskStatusType) {
super({ value: status });
}
static pending(): TaskStatus { return new TaskStatus('pending'); }
static assigned(): TaskStatus { return new TaskStatus('assigned'); }
static inProgress(): TaskStatus { return new TaskStatus('in_progress'); }
static completed(): TaskStatus { return new TaskStatus('completed'); }
static failed(): TaskStatus { return new TaskStatus('failed'); }
get value(): TaskStatusType {
return this.props.value;
}
public isPending(): boolean { return this.value === 'pending'; }
public isAssigned(): boolean { return this.value === 'assigned'; }
public isInProgress(): boolean { return this.value === 'in_progress'; }
public isCompleted(): boolean { return this.value === 'completed'; }
public isFailed(): boolean { return this.value === 'failed'; }
}
// src/core/domains/task-management/value-objects/priority.vo.ts
type PriorityLevel = 'low' | 'medium' | 'high' | 'critical';
export class Priority extends ValueObject<PriorityLevel> {
private constructor(level: PriorityLevel) {
super({ value: level });
}
static low(): Priority { return new Priority('low'); }
static medium(): Priority { return new Priority('medium'); }
static high(): Priority { return new Priority('high'); }
static critical(): Priority { return new Priority('critical'); }
get value(): PriorityLevel {
return this.props.value;
}
public getNumericValue(): number {
const priorities = { low: 1, medium: 2, high: 3, critical: 4 };
return priorities[this.value];
}
}
```
## Domain Services
### Task Scheduling Service
```typescript
// src/core/domains/task-management/services/task-scheduling.service.ts
import { Injectable } from '../../../shared/infrastructure/dependency-container';
import { Task } from '../entities/task.entity';
import { Priority } from '../value-objects/priority.vo';
@Injectable()
export class TaskSchedulingService {
public prioritizeTasks(tasks: Task[]): Task[] {
return tasks.sort((a, b) =>
b.priority.getNumericValue() - a.priority.getNumericValue()
);
}
public canSchedule(task: Task, agentCapacity: number): boolean {
if (agentCapacity <= 0) return false;
// Critical tasks always schedulable
if (task.priority.equals(Priority.critical())) return true;
// Other logic based on capacity
return true;
}
public calculateEstimatedDuration(task: Task): number {
// Simple heuristic - would use ML in real implementation
const baseTime = 300000; // 5 minutes
const priorityMultiplier = {
low: 0.5,
medium: 1.0,
high: 1.5,
critical: 2.0
};
return baseTime * priorityMultiplier[task.priority.value];
}
}
```
## Repository Interfaces & Implementations
### Task Repository Interface
```typescript
// src/core/domains/task-management/repositories/task.repository.ts
export interface ITaskRepository {
save(task: Task): Promise<void>;
findById(id: TaskId): Promise<Task | null>;
findByAgentId(agentId: string): Promise<Task[]>;
findByStatus(status: TaskStatus): Promise<Task[]>;
findPendingTasks(): Promise<Task[]>;
delete(id: TaskId): Promise<void>;
}
```
### SQLite Implementation
```typescript
// src/core/domains/task-management/repositories/sqlite-task.repository.ts
@Injectable()
export class SqliteTaskRepository implements ITaskRepository {
constructor(
@Inject('Database') private db: Database,
@Inject('Logger') private logger: ILogger
) {}
async save(task: Task): Promise<void> {
const sql = `
INSERT OR REPLACE INTO tasks (
id, description, priority, status, assigned_agent_id, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
`;
await this.db.run(sql, [
task.id.value,
task.description,
task.priority.value,
task.status.value,
task.assignedAgentId,
task.createdAt.toISOString(),
task.updatedAt.toISOString()
]);
this.logger.debug(`Task saved: ${task.id.value}`);
}
async findById(id: TaskId): Promise<Task | null> {
const sql = 'SELECT * FROM tasks WHERE id = ?';
const row = await this.db.get(sql, [id.value]);
return row ? this.mapRowToTask(row) : null;
}
async findPendingTasks(): Promise<Task[]> {
const sql = 'SELECT * FROM tasks WHERE status = ? ORDER BY priority DESC, created_at ASC';
const rows = await this.db.all(sql, ['pending']);
return rows.map(row => this.mapRowToTask(row));
}
private mapRowToTask(row: any): Task {
return Task.reconstitute({
id: TaskId.fromString(row.id),
description: row.description,
priority: Priority.fromString(row.priority),
status: TaskStatus.fromString(row.status),
assignedAgentId: row.assigned_agent_id,
createdAt: new Date(row.created_at),
updatedAt: new Date(row.updated_at)
});
}
}
```
## Application Layer
### Use Case Implementation
```typescript
// src/core/application/use-cases/assign-task.use-case.ts
@Injectable()
export class AssignTaskUseCase {
constructor(
@Inject('TaskRepository') private taskRepository: ITaskRepository,
@Inject('AgentRepository') private agentRepository: IAgentRepository,
@Inject('DomainEventBus') private eventBus: DomainEventBus,
@Inject('Logger') private logger: ILogger
) {}
async execute(command: AssignTaskCommand): Promise<AssignTaskResult> {
try {
// 1. Validate command
await this.validateCommand(command);
// 2. Load aggregates
const task = await this.taskRepository.findById(command.taskId);
if (!task) {
throw new TaskNotFoundError(command.taskId);
}
const agent = await this.agentRepository.findById(command.agentId);
if (!agent) {
throw new AgentNotFoundError(command.agentId);
}
// 3. Business logic
if (!agent.canAcceptTask(task)) {
throw new AgentCannotAcceptTaskError(command.agentId, command.taskId);
}
task.assignTo(command.agentId);
agent.acceptTask(task.id);
// 4. Persist changes
await Promise.all([
this.taskRepository.save(task),
this.agentRepository.save(agent)
]);
// 5. Publish domain events
const events = [
...task.getUncommittedEvents(),
...agent.getUncommittedEvents()
];
for (const event of events) {
await this.eventBus.publish(event);
}
task.markEventsAsCommitted();
agent.markEventsAsCommitted();
// 6. Return result
this.logger.info(`Task ${command.taskId.value} assigned to agent ${command.agentId}`);
return AssignTaskResult.success({
taskId: task.id,
agentId: command.agentId,
assignedAt: new Date()
});
} catch (error) {
this.logger.error(`Failed to assign task ${command.taskId.value}:`, error);
return AssignTaskResult.failure(error);
}
}
private async validateCommand(command: AssignTaskCommand): Promise<void> {
if (!command.taskId) {
throw new ValidationError('Task ID is required');
}
if (!command.agentId) {
throw new ValidationError('Agent ID is required');
}
}
}
```
## Dependency Injection Setup
### Container Configuration
```typescript
// src/core/shared/infrastructure/dependency-container.ts
import { Container } from 'inversify';
import { TYPES } from './types';
export class DependencyContainer {
private container: Container;
constructor() {
this.container = new Container();
this.setupBindings();
}
private setupBindings(): void {
// Repositories
this.container.bind<ITaskRepository>(TYPES.TaskRepository)
.to(SqliteTaskRepository)
.inSingletonScope();
this.container.bind<IAgentRepository>(TYPES.AgentRepository)
.to(SqliteAgentRepository)
.inSingletonScope();
// Services
this.container.bind<TaskSchedulingService>(TYPES.TaskSchedulingService)
.to(TaskSchedulingService)
.inSingletonScope();
// Use Cases
this.container.bind<AssignTaskUseCase>(TYPES.AssignTaskUseCase)
.to(AssignTaskUseCase)
.inSingletonScope();
// Infrastructure
this.container.bind<ILogger>(TYPES.Logger)
.to(ConsoleLogger)
.inSingletonScope();
this.container.bind<DomainEventBus>(TYPES.DomainEventBus)
.to(InMemoryDomainEventBus)
.inSingletonScope();
}
get<T>(serviceIdentifier: symbol): T {
return this.container.get<T>(serviceIdentifier);
}
bind<T>(serviceIdentifier: symbol): BindingToSyntax<T> {
return this.container.bind<T>(serviceIdentifier);
}
}
```
## Modern TypeScript Configuration
### Strict TypeScript Setup
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"outDir": "./dist",
"strict": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@core/*": ["src/core/*"],
"@shared/*": ["src/core/shared/*"],
"@domains/*": ["src/core/domains/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
```
## Testing Implementation
### Domain Unit Tests
```typescript
// src/core/domains/task-management/__tests__/entities/task.entity.test.ts
describe('Task Entity', () => {
let task: Task;
beforeEach(() => {
task = Task.create('Test task', Priority.medium());
});
describe('creation', () => {
it('should create task with pending status', () => {
expect(task.status.isPending()).toBe(true);
expect(task.description).toBe('Test task');
expect(task.priority.equals(Priority.medium())).toBe(true);
});
it('should generate unique ID', () => {
const task1 = Task.create('Task 1', Priority.low());
const task2 = Task.create('Task 2', Priority.low());
expect(task1.id.equals(task2.id)).toBe(false);
});
});
describe('assignment', () => {
it('should assign to agent and change status', () => {
const agentId = 'agent-123';
task.assignTo(agentId);
expect(task.assignedAgentId).toBe(agentId);
expect(task.status.isAssigned()).toBe(true);
});
it('should emit TaskAssignedEvent when assigned', () => {
const agentId = 'agent-123';
task.assignTo(agentId);
const events = task.getUncommittedEvents();
expect(events).toHaveLength(1);
expect(events[0]).toBeInstanceOf(TaskAssignedEvent);
});
it('should not allow assignment of completed task', () => {
task.assignTo('agent-123');
task.complete(TaskResult.success('done'));
expect(() => task.assignTo('agent-456'))
.toThrow('Cannot assign completed task');
});
});
});
```
### Integration Tests
```typescript
// src/core/domains/task-management/__tests__/integration/task-repository.integration.test.ts
describe('TaskRepository Integration', () => {
let repository: SqliteTaskRepository;
let db: Database;
beforeEach(async () => {
db = new Database(':memory:');
await setupTasksTable(db);
repository = new SqliteTaskRepository(db, new ConsoleLogger());
});
afterEach(async () => {
await db.close();
});
it('should save and retrieve task', async () => {
const task = Task.create('Test task', Priority.high());
await repository.save(task);
const retrieved = await repository.findById(task.id);
expect(retrieved).toBeDefined();
expect(retrieved!.id.equals(task.id)).toBe(true);
expect(retrieved!.description).toBe('Test task');
expect(retrieved!.priority.equals(Priority.high())).toBe(true);
});
it('should find pending tasks ordered by priority', async () => {
const lowTask = Task.create('Low priority', Priority.low());
const highTask = Task.create('High priority', Priority.high());
await repository.save(lowTask);
await repository.save(highTask);
const pending = await repository.findPendingTasks();
expect(pending).toHaveLength(2);
expect(pending[0].id.equals(highTask.id)).toBe(true); // High priority first
expect(pending[1].id.equals(lowTask.id)).toBe(true);
});
});
```
## Performance Optimizations
### Entity Caching
```typescript
// src/core/shared/infrastructure/entity-cache.ts
@Injectable()
export class EntityCache<T extends Entity<any>> {
private cache = new Map<string, { entity: T; timestamp: number }>();
private readonly ttl: number = 300000; // 5 minutes
set(id: string, entity: T): void {
this.cache.set(id, { entity, timestamp: Date.now() });
}
get(id: string): T | null {
const cached = this.cache.get(id);
if (!cached) return null;
// Check TTL
if (Date.now() - cached.timestamp > this.ttl) {
this.cache.delete(id);
return null;
}
return cached.entity;
}
invalidate(id: string): void {
this.cache.delete(id);
}
clear(): void {
this.cache.clear();
}
}
```
## Success Metrics
- [ ] **Domain Isolation**: 100% clean dependency boundaries
- [ ] **Test Coverage**: >90% unit test coverage for domain logic
- [ ] **Type Safety**: Strict TypeScript compilation with zero any types
- [ ] **Performance**: <50ms average use case execution time
- [ ] **Memory Efficiency**: <100MB heap usage for core domains
- [ ] **Plugin Architecture**: Modular domain loading capability
## Related V3 Skills
- `v3-ddd-architecture` - DDD architectural design
- `v3-mcp-optimization` - MCP server integration
- `v3-memory-unification` - AgentDB repository integration
- `v3-swarm-coordination` - Swarm domain implementation
## Usage Examples
### Complete Core Implementation
```bash
# Full core module implementation
Task("Core implementation",
"Implement all core domains with DDD patterns and comprehensive testing",
"core-implementer")
```
### Domain-Specific Implementation
```bash
# Single domain implementation
Task("Task domain implementation",
"Implement task management domain with entities, services, and repositories",
"core-implementer")
```
-442
View File
@@ -1,442 +0,0 @@
---
name: "V3 DDD Architecture"
description: "Domain-Driven Design architecture for Codex-flow v3. Implements modular, bounded context architecture with clean separation of concerns and microkernel pattern."
---
# V3 DDD Architecture
## What This Skill Does
Designs and implements Domain-Driven Design (DDD) architecture for Codex-flow v3, decomposing god objects into bounded contexts, implementing clean architecture patterns, and enabling modular, testable code structure.
## Quick Start
```bash
# Initialize DDD architecture analysis
Task("Architecture analysis", "Analyze current architecture and design DDD boundaries", "core-architect")
# Domain modeling (parallel)
Task("Domain decomposition", "Break down orchestrator god object into domains", "core-architect")
Task("Context mapping", "Map bounded contexts and relationships", "core-architect")
Task("Interface design", "Design clean domain interfaces", "core-architect")
```
## DDD Implementation Strategy
### Current Architecture Analysis
```
├── PROBLEMATIC: core/orchestrator.ts (1,440 lines - GOD OBJECT)
│ ├── Task management responsibilities
│ ├── Session management responsibilities
│ ├── Health monitoring responsibilities
│ ├── Lifecycle management responsibilities
│ └── Event coordination responsibilities
└── TARGET: Modular DDD Architecture
├── core/domains/
│ ├── task-management/
│ ├── session-management/
│ ├── health-monitoring/
│ ├── lifecycle-management/
│ └── event-coordination/
└── core/shared/
├── interfaces/
├── value-objects/
└── domain-events/
```
### Domain Boundaries
#### 1. Task Management Domain
```typescript
// core/domains/task-management/
interface TaskManagementDomain {
// Entities
Task: TaskEntity;
TaskQueue: TaskQueueEntity;
// Value Objects
TaskId: TaskIdVO;
TaskStatus: TaskStatusVO;
Priority: PriorityVO;
// Services
TaskScheduler: TaskSchedulingService;
TaskValidator: TaskValidationService;
// Repository
TaskRepository: ITaskRepository;
}
```
#### 2. Session Management Domain
```typescript
// core/domains/session-management/
interface SessionManagementDomain {
// Entities
Session: SessionEntity;
SessionState: SessionStateEntity;
// Value Objects
SessionId: SessionIdVO;
SessionStatus: SessionStatusVO;
// Services
SessionLifecycle: SessionLifecycleService;
SessionPersistence: SessionPersistenceService;
// Repository
SessionRepository: ISessionRepository;
}
```
#### 3. Health Monitoring Domain
```typescript
// core/domains/health-monitoring/
interface HealthMonitoringDomain {
// Entities
HealthCheck: HealthCheckEntity;
Metric: MetricEntity;
// Value Objects
HealthStatus: HealthStatusVO;
Threshold: ThresholdVO;
// Services
HealthCollector: HealthCollectionService;
AlertManager: AlertManagementService;
// Repository
MetricsRepository: IMetricsRepository;
}
```
## Microkernel Architecture Pattern
### Core Kernel
```typescript
// core/kernel/Codex-flow-kernel.ts
export class ClaudeFlowKernel {
private domains: Map<string, Domain> = new Map();
private eventBus: DomainEventBus;
private dependencyContainer: Container;
async initialize(): Promise<void> {
// Load core domains
await this.loadDomain('task-management', new TaskManagementDomain());
await this.loadDomain('session-management', new SessionManagementDomain());
await this.loadDomain('health-monitoring', new HealthMonitoringDomain());
// Wire up domain events
this.setupDomainEventHandlers();
}
async loadDomain(name: string, domain: Domain): Promise<void> {
await domain.initialize(this.dependencyContainer);
this.domains.set(name, domain);
}
getDomain<T extends Domain>(name: string): T {
const domain = this.domains.get(name);
if (!domain) {
throw new DomainNotLoadedError(name);
}
return domain as T;
}
}
```
### Plugin Architecture
```typescript
// core/plugins/
interface DomainPlugin {
name: string;
version: string;
dependencies: string[];
initialize(kernel: ClaudeFlowKernel): Promise<void>;
shutdown(): Promise<void>;
}
// Example: Swarm Coordination Plugin
export class SwarmCoordinationPlugin implements DomainPlugin {
name = 'swarm-coordination';
version = '3.0.0';
dependencies = ['task-management', 'session-management'];
async initialize(kernel: ClaudeFlowKernel): Promise<void> {
const taskDomain = kernel.getDomain<TaskManagementDomain>('task-management');
const sessionDomain = kernel.getDomain<SessionManagementDomain>('session-management');
// Register swarm coordination services
this.swarmCoordinator = new UnifiedSwarmCoordinator(taskDomain, sessionDomain);
kernel.registerService('swarm-coordinator', this.swarmCoordinator);
}
}
```
## Domain Events & Integration
### Event-Driven Communication
```typescript
// core/shared/domain-events/
abstract class DomainEvent {
public readonly eventId: string;
public readonly aggregateId: string;
public readonly occurredOn: Date;
public readonly eventVersion: number;
constructor(aggregateId: string) {
this.eventId = crypto.randomUUID();
this.aggregateId = aggregateId;
this.occurredOn = new Date();
this.eventVersion = 1;
}
}
// Task domain events
export class TaskAssignedEvent extends DomainEvent {
constructor(
taskId: string,
public readonly agentId: string,
public readonly priority: Priority
) {
super(taskId);
}
}
export class TaskCompletedEvent extends DomainEvent {
constructor(
taskId: string,
public readonly result: TaskResult,
public readonly duration: number
) {
super(taskId);
}
}
// Event handlers
@EventHandler(TaskCompletedEvent)
export class TaskCompletedHandler {
constructor(
private metricsRepository: IMetricsRepository,
private sessionService: SessionLifecycleService
) {}
async handle(event: TaskCompletedEvent): Promise<void> {
// Update metrics
await this.metricsRepository.recordTaskCompletion(
event.aggregateId,
event.duration
);
// Update session state
await this.sessionService.markTaskCompleted(
event.aggregateId,
event.result
);
}
}
```
## Clean Architecture Layers
```typescript
// Architecture layers
Presentation CLI, API, UI
Application Use Cases, Commands
Domain Entities, Services, Events
Infrastructure DB, MCP, External APIs
// Dependency direction: Outside → Inside
// Domain layer has NO external dependencies
```
### Application Layer (Use Cases)
```typescript
// core/application/use-cases/
export class AssignTaskUseCase {
constructor(
private taskRepository: ITaskRepository,
private agentRepository: IAgentRepository,
private eventBus: DomainEventBus
) {}
async execute(command: AssignTaskCommand): Promise<TaskResult> {
// 1. Validate command
await this.validateCommand(command);
// 2. Load aggregates
const task = await this.taskRepository.findById(command.taskId);
const agent = await this.agentRepository.findById(command.agentId);
// 3. Business logic (in domain)
task.assignTo(agent);
// 4. Persist changes
await this.taskRepository.save(task);
// 5. Publish domain events
task.getUncommittedEvents().forEach(event =>
this.eventBus.publish(event)
);
// 6. Return result
return TaskResult.success(task);
}
}
```
## Module Configuration
### Bounded Context Modules
```typescript
// core/domains/task-management/module.ts
export const taskManagementModule = {
name: 'task-management',
entities: [
TaskEntity,
TaskQueueEntity
],
valueObjects: [
TaskIdVO,
TaskStatusVO,
PriorityVO
],
services: [
TaskSchedulingService,
TaskValidationService
],
repositories: [
{ provide: ITaskRepository, useClass: SqliteTaskRepository }
],
eventHandlers: [
TaskAssignedHandler,
TaskCompletedHandler
]
};
```
## Migration Strategy
### Phase 1: Extract Domain Services
```typescript
// Extract services from orchestrator.ts
const extractionPlan = {
week1: [
'TaskManager → task-management domain',
'SessionManager → session-management domain'
],
week2: [
'HealthMonitor → health-monitoring domain',
'LifecycleManager → lifecycle-management domain'
],
week3: [
'EventCoordinator → event-coordination domain',
'Wire up domain events'
]
};
```
### Phase 2: Implement Clean Interfaces
```typescript
// Clean separation with dependency injection
export class TaskController {
constructor(
@Inject('AssignTaskUseCase') private assignTask: AssignTaskUseCase,
@Inject('CompleteTaskUseCase') private completeTask: CompleteTaskUseCase
) {}
async assign(request: AssignTaskRequest): Promise<TaskResponse> {
const command = AssignTaskCommand.fromRequest(request);
const result = await this.assignTask.execute(command);
return TaskResponse.fromResult(result);
}
}
```
### Phase 3: Plugin System
```typescript
// Enable plugin-based extensions
const pluginSystem = {
core: ['task-management', 'session-management', 'health-monitoring'],
optional: ['swarm-coordination', 'learning-integration', 'performance-monitoring']
};
```
## Testing Strategy
### Domain Testing (London School TDD)
```typescript
// Pure domain logic testing
describe('Task Entity', () => {
let task: TaskEntity;
let mockAgent: jest.Mocked<AgentEntity>;
beforeEach(() => {
task = new TaskEntity(TaskId.create(), 'Test task');
mockAgent = createMock<AgentEntity>();
});
it('should assign to agent when valid', () => {
mockAgent.canAcceptTask.mockReturnValue(true);
task.assignTo(mockAgent);
expect(task.assignedAgent).toBe(mockAgent);
expect(task.status.value).toBe('assigned');
});
it('should emit TaskAssignedEvent when assigned', () => {
mockAgent.canAcceptTask.mockReturnValue(true);
task.assignTo(mockAgent);
const events = task.getUncommittedEvents();
expect(events).toHaveLength(1);
expect(events[0]).toBeInstanceOf(TaskAssignedEvent);
});
});
```
## Success Metrics
- [ ] **God Object Elimination**: orchestrator.ts (1,440 lines) → 5 focused domains (<300 lines each)
- [ ] **Bounded Context Isolation**: 100% domain independence
- [ ] **Plugin Architecture**: Core + optional modules loading
- [ ] **Clean Architecture**: Dependency inversion maintained
- [ ] **Event-Driven Communication**: Loose coupling between domains
- [ ] **Test Coverage**: >90% domain logic coverage
## Related V3 Skills
- `v3-core-implementation` - Implementation of DDD domains
- `v3-memory-unification` - AgentDB integration within bounded contexts
- `v3-swarm-coordination` - Swarm coordination as domain plugin
- `v3-performance-optimization` - Performance optimization across domains
## Usage Examples
### Complete Domain Extraction
```bash
# Full DDD architecture implementation
Task("DDD architecture implementation",
"Extract orchestrator into DDD domains with clean architecture",
"core-architect")
```
### Plugin Development
```bash
# Create domain plugin
npm run create:plugin -- --name swarm-coordination --template domain
```
-241
View File
@@ -1,241 +0,0 @@
---
name: "V3 Deep Integration"
description: "Deep agentic-flow@alpha integration implementing ADR-001. Eliminates 10,000+ duplicate lines by building Codex-flow as specialized extension rather than parallel implementation."
---
# V3 Deep Integration
## What This Skill Does
Transforms Codex-flow from parallel implementation to specialized extension of agentic-flow@alpha, eliminating massive code duplication while achieving performance improvements and feature parity.
## Quick Start
```bash
# Initialize deep integration
Task("Integration architecture", "Design agentic-flow@alpha adapter layer", "v3-integration-architect")
# Feature integration (parallel)
Task("SONA integration", "Integrate 5 SONA learning modes", "v3-integration-architect")
Task("Flash Attention", "Implement 2.49x-7.47x speedup", "v3-integration-architect")
Task("AgentDB coordination", "Setup 150x-12,500x search", "v3-integration-architect")
```
## Code Deduplication Strategy
### Current Overlap → Integration
```
┌─────────────────────────────────────────┐
│ Codex-flow agentic-flow │
├─────────────────────────────────────────┤
│ SwarmCoordinator → Swarm System │ 80% overlap (eliminate)
│ AgentManager → Agent Lifecycle │ 70% overlap (eliminate)
│ TaskScheduler → Task Execution │ 60% overlap (eliminate)
│ SessionManager → Session Mgmt │ 50% overlap (eliminate)
└─────────────────────────────────────────┘
TARGET: <5,000 lines (vs 15,000+ currently)
```
## agentic-flow@alpha Feature Integration
### SONA Learning Modes
```typescript
class SONAIntegration {
async initializeMode(mode: SONAMode): Promise<void> {
switch(mode) {
case 'real-time': // ~0.05ms adaptation
case 'balanced': // general purpose
case 'research': // deep exploration
case 'edge': // resource-constrained
case 'batch': // high-throughput
}
await this.agenticFlow.sona.setMode(mode);
}
}
```
### Flash Attention Integration
```typescript
class FlashAttentionIntegration {
async optimizeAttention(): Promise<AttentionResult> {
return this.agenticFlow.attention.flashAttention({
speedupTarget: '2.49x-7.47x',
memoryReduction: '50-75%',
mechanisms: ['multi-head', 'linear', 'local', 'global']
});
}
}
```
### AgentDB Coordination
```typescript
class AgentDBIntegration {
async setupCrossAgentMemory(): Promise<void> {
await this.agentdb.enableCrossAgentSharing({
indexType: 'HNSW',
speedupTarget: '150x-12500x',
dimensions: 1536
});
}
}
```
### MCP Tools Integration
```typescript
class MCPToolsIntegration {
async integrateBuiltinTools(): Promise<void> {
// Leverage 213 pre-built tools
const tools = await this.agenticFlow.mcp.getAvailableTools();
await this.registerClaudeFlowSpecificTools(tools);
// Use 19 hook types
const hookTypes = await this.agenticFlow.hooks.getTypes();
await this.configureClaudeFlowHooks(hookTypes);
}
}
```
## Migration Implementation
### Phase 1: Adapter Layer
```typescript
import { Agent as AgenticFlowAgent } from 'agentic-flow@alpha';
export class ClaudeFlowAgent extends AgenticFlowAgent {
async handleClaudeFlowTask(task: ClaudeTask): Promise<TaskResult> {
return this.executeWithSONA(task);
}
// Backward compatibility
async legacyCompatibilityLayer(oldAPI: any): Promise<any> {
return this.adaptToNewAPI(oldAPI);
}
}
```
### Phase 2: System Migration
```typescript
class SystemMigration {
async migrateSwarmCoordination(): Promise<void> {
// Replace SwarmCoordinator (800+ lines) with agentic-flow Swarm
const swarmConfig = await this.extractSwarmConfig();
await this.agenticFlow.swarm.initialize(swarmConfig);
}
async migrateAgentManagement(): Promise<void> {
// Replace AgentManager (1,736+ lines) with agentic-flow lifecycle
const agents = await this.extractActiveAgents();
for (const agent of agents) {
await this.agenticFlow.agent.create(agent);
}
}
async migrateTaskExecution(): Promise<void> {
// Replace TaskScheduler with agentic-flow task graph
const tasks = await this.extractTasks();
await this.agenticFlow.task.executeGraph(this.buildTaskGraph(tasks));
}
}
```
### Phase 3: Cleanup
```typescript
class CodeCleanup {
async removeDeprecatedCode(): Promise<void> {
// Remove massive duplicate implementations
await this.removeFile('src/core/SwarmCoordinator.ts'); // 800+ lines
await this.removeFile('src/agents/AgentManager.ts'); // 1,736+ lines
await this.removeFile('src/task/TaskScheduler.ts'); // 500+ lines
// Total reduction: 10,000+ → <5,000 lines
}
}
```
## RL Algorithm Integration
```typescript
class RLIntegration {
algorithms = [
'PPO', 'DQN', 'A2C', 'MCTS', 'Q-Learning',
'SARSA', 'Actor-Critic', 'Decision-Transformer'
];
async optimizeAgentBehavior(): Promise<void> {
for (const algorithm of this.algorithms) {
await this.agenticFlow.rl.train(algorithm, {
episodes: 1000,
rewardFunction: this.claudeFlowRewardFunction
});
}
}
}
```
## Performance Integration
### Flash Attention Targets
```typescript
const attentionBenchmark = {
baseline: 'current attention mechanism',
target: '2.49x-7.47x improvement',
memoryReduction: '50-75%',
implementation: 'agentic-flow@alpha Flash Attention'
};
```
### AgentDB Search Performance
```typescript
const searchBenchmark = {
baseline: 'linear search in current systems',
target: '150x-12,500x via HNSW indexing',
implementation: 'agentic-flow@alpha AgentDB'
};
```
## Backward Compatibility
### Gradual Migration
```typescript
class BackwardCompatibility {
// Phase 1: Dual operation
async enableDualOperation(): Promise<void> {
this.oldSystem.continue();
this.newSystem.initialize();
this.syncState(this.oldSystem, this.newSystem);
}
// Phase 2: Feature-by-feature migration
async migrateGradually(): Promise<void> {
const features = this.getAllFeatures();
for (const feature of features) {
await this.migrateFeature(feature);
await this.validateFeatureParity(feature);
}
}
// Phase 3: Complete transition
async completeTransition(): Promise<void> {
await this.validateFullParity();
await this.deprecateOldSystem();
}
}
```
## Success Metrics
- **Code Reduction**: <5,000 lines orchestration (vs 15,000+)
- **Performance**: 2.49x-7.47x Flash Attention speedup
- **Search**: 150x-12,500x AgentDB improvement
- **Memory**: 50-75% usage reduction
- **Feature Parity**: 100% v2 functionality maintained
- **SONA**: <0.05ms adaptation time
- **Integration**: All 213 MCP tools + 19 hook types available
## Related V3 Skills
- `v3-memory-unification` - Memory system integration
- `v3-performance-optimization` - Performance target validation
- `v3-swarm-coordination` - Swarm system migration
- `v3-security-overhaul` - Secure integration patterns
-777
View File
@@ -1,777 +0,0 @@
---
name: "V3 MCP Optimization"
description: "MCP server optimization and transport layer enhancement for Codex-flow v3. Implements connection pooling, load balancing, tool registry optimization, and performance monitoring for sub-100ms response times."
---
# V3 MCP Optimization
## What This Skill Does
Optimizes Codex-flow v3 MCP (Model Context Protocol) server implementation with advanced transport layer optimizations, connection pooling, load balancing, and comprehensive performance monitoring to achieve sub-100ms response times.
## Quick Start
```bash
# Initialize MCP optimization analysis
Task("MCP architecture", "Analyze current MCP server performance and bottlenecks", "mcp-specialist")
# Optimization implementation (parallel)
Task("Connection pooling", "Implement MCP connection pooling and reuse", "mcp-specialist")
Task("Load balancing", "Add dynamic load balancing for MCP tools", "mcp-specialist")
Task("Transport optimization", "Optimize transport layer performance", "mcp-specialist")
```
## MCP Performance Architecture
### Current State Analysis
```
Current MCP Issues:
├── Cold Start Latency: ~1.8s MCP server init
├── Connection Overhead: New connection per request
├── Tool Registry: Linear search O(n) for 213+ tools
├── Transport Layer: No connection reuse
└── Memory Usage: No cleanup of idle connections
Target Performance:
├── Startup Time: <400ms (4.5x improvement)
├── Tool Lookup: <5ms (O(1) hash table)
├── Connection Reuse: 90%+ connection pool hits
├── Response Time: <100ms p95
└── Memory Efficiency: 50% reduction
```
### MCP Server Architecture
```typescript
// src/core/mcp/mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
interface OptimizedMCPConfig {
// Connection pooling
maxConnections: number;
idleTimeoutMs: number;
connectionReuseEnabled: boolean;
// Tool registry
toolCacheEnabled: boolean;
toolIndexType: 'hash' | 'trie';
// Performance
requestTimeoutMs: number;
batchingEnabled: boolean;
compressionEnabled: boolean;
// Monitoring
metricsEnabled: boolean;
healthCheckIntervalMs: number;
}
export class OptimizedMCPServer {
private server: Server;
private connectionPool: ConnectionPool;
private toolRegistry: FastToolRegistry;
private loadBalancer: MCPLoadBalancer;
private metrics: MCPMetrics;
constructor(config: OptimizedMCPConfig) {
this.server = new Server({
name: 'Codex-flow-v3',
version: '3.0.0'
}, {
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
prompts: { listChanged: true }
}
});
this.connectionPool = new ConnectionPool(config);
this.toolRegistry = new FastToolRegistry(config.toolIndexType);
this.loadBalancer = new MCPLoadBalancer();
this.metrics = new MCPMetrics(config.metricsEnabled);
}
async start(): Promise<void> {
// Pre-warm connection pool
await this.connectionPool.preWarm();
// Pre-build tool index
await this.toolRegistry.buildIndex();
// Setup request handlers with optimizations
this.setupOptimizedHandlers();
// Start health monitoring
this.startHealthMonitoring();
// Start server
const transport = new StdioServerTransport();
await this.server.connect(transport);
this.metrics.recordStartup();
}
}
```
## Connection Pool Implementation
### Advanced Connection Pooling
```typescript
// src/core/mcp/connection-pool.ts
interface PooledConnection {
id: string;
connection: MCPConnection;
lastUsed: number;
usageCount: number;
isHealthy: boolean;
}
export class ConnectionPool {
private pool: Map<string, PooledConnection> = new Map();
private readonly config: ConnectionPoolConfig;
private healthChecker: HealthChecker;
constructor(config: ConnectionPoolConfig) {
this.config = {
maxConnections: 50,
minConnections: 5,
idleTimeoutMs: 300000, // 5 minutes
maxUsageCount: 1000,
healthCheckIntervalMs: 30000,
...config
};
this.healthChecker = new HealthChecker(this.config.healthCheckIntervalMs);
}
async getConnection(endpoint: string): Promise<MCPConnection> {
const start = performance.now();
// Try to get from pool first
const pooled = this.findAvailableConnection(endpoint);
if (pooled) {
pooled.lastUsed = Date.now();
pooled.usageCount++;
this.recordMetric('pool_hit', performance.now() - start);
return pooled.connection;
}
// Check pool capacity
if (this.pool.size >= this.config.maxConnections) {
await this.evictLeastUsedConnection();
}
// Create new connection
const connection = await this.createConnection(endpoint);
const pooledConn: PooledConnection = {
id: this.generateConnectionId(),
connection,
lastUsed: Date.now(),
usageCount: 1,
isHealthy: true
};
this.pool.set(pooledConn.id, pooledConn);
this.recordMetric('pool_miss', performance.now() - start);
return connection;
}
async releaseConnection(connection: MCPConnection): Promise<void> {
// Mark connection as available for reuse
const pooled = this.findConnectionById(connection.id);
if (pooled) {
// Check if connection should be retired
if (pooled.usageCount >= this.config.maxUsageCount) {
await this.removeConnection(pooled.id);
}
}
}
async preWarm(): Promise<void> {
const connections: Promise<MCPConnection>[] = [];
for (let i = 0; i < this.config.minConnections; i++) {
connections.push(this.createConnection('default'));
}
await Promise.all(connections);
}
private async evictLeastUsedConnection(): Promise<void> {
let oldestConn: PooledConnection | null = null;
let oldestTime = Date.now();
for (const conn of this.pool.values()) {
if (conn.lastUsed < oldestTime) {
oldestTime = conn.lastUsed;
oldestConn = conn;
}
}
if (oldestConn) {
await this.removeConnection(oldestConn.id);
}
}
private findAvailableConnection(endpoint: string): PooledConnection | null {
for (const conn of this.pool.values()) {
if (conn.isHealthy &&
conn.connection.endpoint === endpoint &&
Date.now() - conn.lastUsed < this.config.idleTimeoutMs) {
return conn;
}
}
return null;
}
}
```
## Fast Tool Registry
### O(1) Tool Lookup Implementation
```typescript
// src/core/mcp/fast-tool-registry.ts
interface ToolIndexEntry {
name: string;
handler: ToolHandler;
metadata: ToolMetadata;
usageCount: number;
avgLatencyMs: number;
}
export class FastToolRegistry {
private toolIndex: Map<string, ToolIndexEntry> = new Map();
private categoryIndex: Map<string, string[]> = new Map();
private fuzzyMatcher: FuzzyMatcher;
private cache: LRUCache<string, ToolIndexEntry>;
constructor(indexType: 'hash' | 'trie' = 'hash') {
this.fuzzyMatcher = new FuzzyMatcher();
this.cache = new LRUCache<string, ToolIndexEntry>(1000); // Cache 1000 most used tools
}
async buildIndex(): Promise<void> {
const start = performance.now();
// Load all available tools
const tools = await this.loadAllTools();
// Build hash index for O(1) lookup
for (const tool of tools) {
const entry: ToolIndexEntry = {
name: tool.name,
handler: tool.handler,
metadata: tool.metadata,
usageCount: 0,
avgLatencyMs: 0
};
this.toolIndex.set(tool.name, entry);
// Build category index
const category = tool.metadata.category || 'general';
if (!this.categoryIndex.has(category)) {
this.categoryIndex.set(category, []);
}
this.categoryIndex.get(category)!.push(tool.name);
}
// Build fuzzy search index
await this.fuzzyMatcher.buildIndex(tools.map(t => t.name));
console.log(`Tool index built in ${(performance.now() - start).toFixed(2)}ms for ${tools.length} tools`);
}
findTool(name: string): ToolIndexEntry | null {
// Try cache first
const cached = this.cache.get(name);
if (cached) return cached;
// Try exact match
const exact = this.toolIndex.get(name);
if (exact) {
this.cache.set(name, exact);
return exact;
}
// Try fuzzy match
const fuzzyMatches = this.fuzzyMatcher.search(name, 1);
if (fuzzyMatches.length > 0) {
const match = this.toolIndex.get(fuzzyMatches[0]);
if (match) {
this.cache.set(name, match);
return match;
}
}
return null;
}
findToolsByCategory(category: string): ToolIndexEntry[] {
const toolNames = this.categoryIndex.get(category) || [];
return toolNames
.map(name => this.toolIndex.get(name))
.filter(entry => entry !== undefined) as ToolIndexEntry[];
}
getMostUsedTools(limit: number = 10): ToolIndexEntry[] {
return Array.from(this.toolIndex.values())
.sort((a, b) => b.usageCount - a.usageCount)
.slice(0, limit);
}
recordToolUsage(toolName: string, latencyMs: number): void {
const entry = this.toolIndex.get(toolName);
if (entry) {
entry.usageCount++;
// Moving average for latency
entry.avgLatencyMs = (entry.avgLatencyMs + latencyMs) / 2;
}
}
}
```
## Load Balancing & Request Distribution
### Intelligent Load Balancer
```typescript
// src/core/mcp/load-balancer.ts
interface ServerInstance {
id: string;
endpoint: string;
load: number;
responseTime: number;
isHealthy: boolean;
maxConnections: number;
currentConnections: number;
}
export class MCPLoadBalancer {
private servers: Map<string, ServerInstance> = new Map();
private routingStrategy: RoutingStrategy = 'least-connections';
addServer(server: ServerInstance): void {
this.servers.set(server.id, server);
}
selectServer(toolCategory?: string): ServerInstance | null {
const healthyServers = Array.from(this.servers.values())
.filter(server => server.isHealthy);
if (healthyServers.length === 0) return null;
switch (this.routingStrategy) {
case 'round-robin':
return this.roundRobinSelection(healthyServers);
case 'least-connections':
return this.leastConnectionsSelection(healthyServers);
case 'response-time':
return this.responseTimeSelection(healthyServers);
case 'weighted':
return this.weightedSelection(healthyServers, toolCategory);
default:
return healthyServers[0];
}
}
private leastConnectionsSelection(servers: ServerInstance[]): ServerInstance {
return servers.reduce((least, current) =>
current.currentConnections < least.currentConnections ? current : least
);
}
private responseTimeSelection(servers: ServerInstance[]): ServerInstance {
return servers.reduce((fastest, current) =>
current.responseTime < fastest.responseTime ? current : fastest
);
}
private weightedSelection(servers: ServerInstance[], category?: string): ServerInstance {
// Prefer servers with lower load and better response time
const scored = servers.map(server => ({
server,
score: this.calculateServerScore(server, category)
}));
scored.sort((a, b) => b.score - a.score);
return scored[0].server;
}
private calculateServerScore(server: ServerInstance, category?: string): number {
const loadFactor = 1 - (server.currentConnections / server.maxConnections);
const responseFactor = 1 / (server.responseTime + 1);
const categoryBonus = this.getCategoryBonus(server, category);
return loadFactor * 0.4 + responseFactor * 0.4 + categoryBonus * 0.2;
}
updateServerMetrics(serverId: string, metrics: Partial<ServerInstance>): void {
const server = this.servers.get(serverId);
if (server) {
Object.assign(server, metrics);
}
}
}
```
## Transport Layer Optimization
### High-Performance Transport
```typescript
// src/core/mcp/optimized-transport.ts
export class OptimizedTransport {
private compression: boolean = true;
private batching: boolean = true;
private batchBuffer: MCPMessage[] = [];
private batchTimeout: NodeJS.Timeout | null = null;
constructor(private config: TransportConfig) {}
async send(message: MCPMessage): Promise<void> {
if (this.batching && this.canBatch(message)) {
this.addToBatch(message);
return;
}
await this.sendImmediate(message);
}
private async sendImmediate(message: MCPMessage): Promise<void> {
const start = performance.now();
// Compress if enabled
const payload = this.compression
? await this.compress(message)
: message;
// Send through transport
await this.transport.send(payload);
// Record metrics
this.recordLatency(performance.now() - start);
}
private addToBatch(message: MCPMessage): void {
this.batchBuffer.push(message);
// Start batch timeout if not already running
if (!this.batchTimeout) {
this.batchTimeout = setTimeout(
() => this.flushBatch(),
this.config.batchTimeoutMs || 10
);
}
// Flush if batch is full
if (this.batchBuffer.length >= this.config.maxBatchSize) {
this.flushBatch();
}
}
private async flushBatch(): Promise<void> {
if (this.batchBuffer.length === 0) return;
const batch = this.batchBuffer.splice(0);
this.batchTimeout = null;
// Send as single batched message
await this.sendImmediate({
type: 'batch',
messages: batch
});
}
private canBatch(message: MCPMessage): boolean {
// Don't batch urgent messages or responses
return message.type !== 'response' &&
message.priority !== 'high' &&
message.type !== 'error';
}
private async compress(data: any): Promise<Buffer> {
// Use fast compression for smaller messages
return gzipSync(JSON.stringify(data));
}
}
```
## Performance Monitoring
### Real-time MCP Metrics
```typescript
// src/core/mcp/metrics.ts
interface MCPMetrics {
requestCount: number;
errorCount: number;
avgResponseTime: number;
p95ResponseTime: number;
connectionPoolHits: number;
connectionPoolMisses: number;
toolLookupTime: number;
startupTime: number;
}
export class MCPMetricsCollector {
private metrics: MCPMetrics;
private responseTimeBuffer: number[] = [];
private readonly bufferSize = 1000;
constructor() {
this.metrics = this.createInitialMetrics();
}
recordRequest(latencyMs: number): void {
this.metrics.requestCount++;
this.updateResponseTimes(latencyMs);
}
recordError(): void {
this.metrics.errorCount++;
}
recordConnectionPoolHit(): void {
this.metrics.connectionPoolHits++;
}
recordConnectionPoolMiss(): void {
this.metrics.connectionPoolMisses++;
}
recordToolLookup(latencyMs: number): void {
this.metrics.toolLookupTime = this.updateMovingAverage(
this.metrics.toolLookupTime,
latencyMs
);
}
recordStartup(latencyMs: number): void {
this.metrics.startupTime = latencyMs;
}
getMetrics(): MCPMetrics {
return { ...this.metrics };
}
getHealthStatus(): HealthStatus {
const errorRate = this.metrics.errorCount / this.metrics.requestCount;
const poolHitRate = this.metrics.connectionPoolHits /
(this.metrics.connectionPoolHits + this.metrics.connectionPoolMisses);
return {
status: this.determineHealthStatus(errorRate, poolHitRate),
errorRate,
poolHitRate,
avgResponseTime: this.metrics.avgResponseTime,
p95ResponseTime: this.metrics.p95ResponseTime
};
}
private updateResponseTimes(latency: number): void {
this.responseTimeBuffer.push(latency);
if (this.responseTimeBuffer.length > this.bufferSize) {
this.responseTimeBuffer.shift();
}
this.metrics.avgResponseTime = this.calculateAverage(this.responseTimeBuffer);
this.metrics.p95ResponseTime = this.calculatePercentile(this.responseTimeBuffer, 95);
}
private calculatePercentile(arr: number[], percentile: number): number {
const sorted = arr.slice().sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index] || 0;
}
private determineHealthStatus(errorRate: number, poolHitRate: number): 'healthy' | 'warning' | 'critical' {
if (errorRate > 0.1 || poolHitRate < 0.5) return 'critical';
if (errorRate > 0.05 || poolHitRate < 0.7) return 'warning';
return 'healthy';
}
}
```
## Tool Registry Optimization
### Pre-compiled Tool Index
```typescript
// src/core/mcp/tool-precompiler.ts
export class ToolPrecompiler {
async precompileTools(): Promise<CompiledToolRegistry> {
const tools = await this.loadAllTools();
// Create optimized lookup structures
const nameIndex = new Map<string, Tool>();
const categoryIndex = new Map<string, Tool[]>();
const fuzzyIndex = new Map<string, string[]>();
for (const tool of tools) {
// Exact name index
nameIndex.set(tool.name, tool);
// Category index
const category = tool.metadata.category || 'general';
if (!categoryIndex.has(category)) {
categoryIndex.set(category, []);
}
categoryIndex.get(category)!.push(tool);
// Pre-compute fuzzy variations
const variations = this.generateFuzzyVariations(tool.name);
for (const variation of variations) {
if (!fuzzyIndex.has(variation)) {
fuzzyIndex.set(variation, []);
}
fuzzyIndex.get(variation)!.push(tool.name);
}
}
return {
nameIndex,
categoryIndex,
fuzzyIndex,
totalTools: tools.length,
compiledAt: new Date()
};
}
private generateFuzzyVariations(name: string): string[] {
const variations: string[] = [];
// Common typos and abbreviations
variations.push(name.toLowerCase());
variations.push(name.replace(/[-_]/g, ''));
variations.push(name.replace(/[aeiou]/gi, '')); // Consonants only
// Add more fuzzy matching logic as needed
return variations;
}
}
```
## Advanced Caching Strategy
### Multi-Level Caching
```typescript
// src/core/mcp/multi-level-cache.ts
export class MultiLevelCache {
private l1Cache: Map<string, any> = new Map(); // In-memory, fastest
private l2Cache: LRUCache<string, any>; // LRU cache, larger capacity
private l3Cache: DiskCache; // Persistent disk cache
constructor(config: CacheConfig) {
this.l2Cache = new LRUCache<string, any>({
max: config.l2MaxEntries || 10000,
ttl: config.l2TTL || 300000 // 5 minutes
});
this.l3Cache = new DiskCache(config.l3Path || './.cache/mcp');
}
async get(key: string): Promise<any | null> {
// Try L1 cache first (fastest)
if (this.l1Cache.has(key)) {
return this.l1Cache.get(key);
}
// Try L2 cache
const l2Value = this.l2Cache.get(key);
if (l2Value) {
// Promote to L1
this.l1Cache.set(key, l2Value);
return l2Value;
}
// Try L3 cache (disk)
const l3Value = await this.l3Cache.get(key);
if (l3Value) {
// Promote to L2 and L1
this.l2Cache.set(key, l3Value);
this.l1Cache.set(key, l3Value);
return l3Value;
}
return null;
}
async set(key: string, value: any, options?: CacheOptions): Promise<void> {
// Set in all levels
this.l1Cache.set(key, value);
this.l2Cache.set(key, value);
if (options?.persistent) {
await this.l3Cache.set(key, value);
}
// Manage L1 cache size
if (this.l1Cache.size > 1000) {
const firstKey = this.l1Cache.keys().next().value;
this.l1Cache.delete(firstKey);
}
}
}
```
## Success Metrics
### Performance Targets
- [ ] **Startup Time**: <400ms MCP server initialization (4.5x improvement)
- [ ] **Response Time**: <100ms p95 for tool execution
- [ ] **Tool Lookup**: <5ms average lookup time
- [ ] **Connection Pool**: >90% hit rate
- [ ] **Memory Usage**: 50% reduction in idle memory
- [ ] **Error Rate**: <1% failed requests
- [ ] **Throughput**: >1000 requests/second
### Monitoring Dashboards
```typescript
const mcpDashboard = {
metrics: [
'Request latency (p50, p95, p99)',
'Error rate by tool category',
'Connection pool utilization',
'Tool lookup performance',
'Memory usage trends',
'Cache hit rates (L1, L2, L3)'
],
alerts: [
'Response time >200ms for 5 minutes',
'Error rate >5% for 1 minute',
'Pool hit rate <70% for 10 minutes',
'Memory usage >500MB for 5 minutes'
]
};
```
## Related V3 Skills
- `v3-core-implementation` - Core domain integration with MCP
- `v3-performance-optimization` - Overall performance optimization
- `v3-swarm-coordination` - MCP integration with swarm coordination
- `v3-memory-unification` - Memory sharing via MCP tools
## Usage Examples
### Complete MCP Optimization
```bash
# Full MCP server optimization
Task("MCP optimization implementation",
"Implement all MCP performance optimizations with monitoring",
"mcp-specialist")
```
### Specific Optimization
```bash
# Connection pool optimization
Task("MCP connection pooling",
"Implement advanced connection pooling with health monitoring",
"mcp-specialist")
```
@@ -1,174 +0,0 @@
---
name: "V3 Memory Unification"
description: "Unify 6+ memory systems into AgentDB with HNSW indexing for 150x-12,500x search improvements. Implements ADR-006 (Unified Memory Service) and ADR-009 (Hybrid Memory Backend)."
---
# V3 Memory Unification
## What This Skill Does
Consolidates disparate memory systems into unified AgentDB backend with HNSW vector search, achieving 150x-12,500x search performance improvements while maintaining backward compatibility.
## Quick Start
```bash
# Initialize memory unification
Task("Memory architecture", "Design AgentDB unification strategy", "v3-memory-specialist")
# AgentDB integration
Task("AgentDB setup", "Configure HNSW indexing and vector search", "v3-memory-specialist")
# Data migration
Task("Memory migration", "Migrate SQLite/Markdown to AgentDB", "v3-memory-specialist")
```
## Systems to Unify
### Legacy Systems → AgentDB
```
┌─────────────────────────────────────────┐
│ • MemoryManager (basic operations) │
│ • DistributedMemorySystem (clustering) │
│ • SwarmMemory (agent-specific) │
│ • AdvancedMemoryManager (features) │
│ • SQLiteBackend (structured) │
│ • MarkdownBackend (file-based) │
│ • HybridBackend (combination) │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 🚀 AgentDB with HNSW │
│ • 150x-12,500x faster search │
│ • Unified query interface │
│ • Cross-agent memory sharing │
│ • SONA learning integration │
└─────────────────────────────────────────┘
```
## Implementation Architecture
### Unified Memory Service
```typescript
class UnifiedMemoryService implements IMemoryBackend {
constructor(
private agentdb: AgentDBAdapter,
private indexer: HNSWIndexer,
private migrator: DataMigrator
) {}
async store(entry: MemoryEntry): Promise<void> {
await this.agentdb.store(entry);
await this.indexer.index(entry);
}
async query(query: MemoryQuery): Promise<MemoryEntry[]> {
if (query.semantic) {
return this.indexer.search(query); // 150x-12,500x faster
}
return this.agentdb.query(query);
}
}
```
### HNSW Vector Search
```typescript
class HNSWIndexer {
constructor(dimensions: number = 1536) {
this.index = new HNSWIndex({
dimensions,
efConstruction: 200,
M: 16,
speedupTarget: '150x-12500x'
});
}
async search(query: MemoryQuery): Promise<MemoryEntry[]> {
const embedding = await this.embedContent(query.content);
const results = this.index.search(embedding, query.limit || 10);
return this.retrieveEntries(results);
}
}
```
## Migration Strategy
### Phase 1: Foundation
```typescript
// AgentDB adapter setup
const agentdb = new AgentDBAdapter({
dimensions: 1536,
indexType: 'HNSW',
speedupTarget: '150x-12500x'
});
```
### Phase 2: Data Migration
```typescript
// SQLite → AgentDB
const migrateFromSQLite = async () => {
const entries = await sqlite.getAll();
for (const entry of entries) {
const embedding = await generateEmbedding(entry.content);
await agentdb.store({ ...entry, embedding });
}
};
// Markdown → AgentDB
const migrateFromMarkdown = async () => {
const files = await glob('**/*.md');
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
await agentdb.store({
id: generateId(),
content,
embedding: await generateEmbedding(content),
metadata: { originalFile: file }
});
}
};
```
## SONA Integration
### Learning Pattern Storage
```typescript
class SONAMemoryIntegration {
async storePattern(pattern: LearningPattern): Promise<void> {
await this.memory.store({
id: pattern.id,
content: pattern.data,
metadata: {
sonaMode: pattern.mode,
reward: pattern.reward,
adaptationTime: pattern.adaptationTime
},
embedding: await this.generateEmbedding(pattern.data)
});
}
async retrieveSimilarPatterns(query: string): Promise<LearningPattern[]> {
return this.memory.query({
type: 'semantic',
content: query,
filters: { type: 'learning_pattern' }
});
}
}
```
## Performance Targets
- **Search Speed**: 150x-12,500x improvement via HNSW
- **Memory Usage**: 50-75% reduction through optimization
- **Query Latency**: <100ms for 1M+ entries
- **Cross-Agent Sharing**: Real-time memory synchronization
- **SONA Integration**: <0.05ms adaptation time
## Success Metrics
- [ ] All 7 legacy memory systems migrated to AgentDB
- [ ] 150x-12,500x search performance validated
- [ ] 50-75% memory usage reduction achieved
- [ ] Backward compatibility maintained
- [ ] SONA learning patterns integrated
- [ ] Cross-agent memory sharing operational
@@ -1,390 +0,0 @@
---
name: "V3 Performance Optimization"
description: "Achieve aggressive v3 performance targets: 2.49x-7.47x Flash Attention speedup, 150x-12,500x search improvements, 50-75% memory reduction. Comprehensive benchmarking and optimization suite."
---
# V3 Performance Optimization
## What This Skill Does
Validates and optimizes Codex-flow v3 to achieve industry-leading performance through Flash Attention, AgentDB HNSW indexing, and comprehensive system optimization with continuous benchmarking.
## Quick Start
```bash
# Initialize performance optimization
Task("Performance baseline", "Establish v2 performance benchmarks", "v3-performance-engineer")
# Target validation (parallel)
Task("Flash Attention", "Validate 2.49x-7.47x speedup target", "v3-performance-engineer")
Task("Search optimization", "Validate 150x-12,500x search improvement", "v3-performance-engineer")
Task("Memory optimization", "Achieve 50-75% memory reduction", "v3-performance-engineer")
```
## Performance Target Matrix
### Flash Attention Revolution
```
┌─────────────────────────────────────────┐
│ FLASH ATTENTION │
├─────────────────────────────────────────┤
│ Baseline: Standard attention │
│ Target: 2.49x - 7.47x speedup │
│ Memory: 50-75% reduction │
│ Latency: Sub-millisecond processing │
└─────────────────────────────────────────┘
```
### Search Performance Revolution
```
┌─────────────────────────────────────────┐
│ SEARCH OPTIMIZATION │
├─────────────────────────────────────────┤
│ Current: O(n) linear search │
│ Target: 150x - 12,500x improvement │
│ Method: HNSW indexing │
│ Latency: <100ms for 1M+ entries │
└─────────────────────────────────────────┘
```
## Comprehensive Benchmark Suite
### Startup Performance
```typescript
class StartupBenchmarks {
async benchmarkColdStart(): Promise<BenchmarkResult> {
const startTime = performance.now();
await this.initializeCLI();
await this.initializeMCPServer();
await this.spawnTestAgent();
const totalTime = performance.now() - startTime;
return {
total: totalTime,
target: 500, // ms
achieved: totalTime < 500
};
}
}
```
### Memory Operation Benchmarks
```typescript
class MemoryBenchmarks {
async benchmarkVectorSearch(): Promise<SearchBenchmark> {
const queries = this.generateTestQueries(10000);
// Baseline: Current linear search
const baselineTime = await this.timeOperation(() =>
this.currentMemory.searchAll(queries)
);
// Target: HNSW search
const hnswTime = await this.timeOperation(() =>
this.agentDBMemory.hnswSearchAll(queries)
);
const improvement = baselineTime / hnswTime;
return {
baseline: baselineTime,
hnsw: hnswTime,
improvement,
targetRange: [150, 12500],
achieved: improvement >= 150
};
}
async benchmarkMemoryUsage(): Promise<MemoryBenchmark> {
const baseline = process.memoryUsage().heapUsed;
await this.loadTestDataset();
const withData = process.memoryUsage().heapUsed;
await this.enableOptimization();
const optimized = process.memoryUsage().heapUsed;
const reduction = (withData - optimized) / withData;
return {
baseline,
withData,
optimized,
reductionPercent: reduction * 100,
targetReduction: [50, 75],
achieved: reduction >= 0.5
};
}
}
```
### Swarm Coordination Benchmarks
```typescript
class SwarmBenchmarks {
async benchmark15AgentCoordination(): Promise<SwarmBenchmark> {
const agents = await this.spawn15Agents();
// Coordination latency
const coordinationTime = await this.timeOperation(() =>
this.coordinateSwarmTask(agents)
);
// Task decomposition
const decompositionTime = await this.timeOperation(() =>
this.decomposeComplexTask()
);
// Consensus achievement
const consensusTime = await this.timeOperation(() =>
this.achieveSwarmConsensus(agents)
);
return {
coordination: coordinationTime,
decomposition: decompositionTime,
consensus: consensusTime,
agentCount: 15,
efficiency: this.calculateEfficiency(agents)
};
}
}
```
### Flash Attention Benchmarks
```typescript
class AttentionBenchmarks {
async benchmarkFlashAttention(): Promise<AttentionBenchmark> {
const sequences = this.generateSequences([512, 1024, 2048, 4096]);
const results = [];
for (const sequence of sequences) {
// Baseline attention
const baselineResult = await this.benchmarkStandardAttention(sequence);
// Flash attention
const flashResult = await this.benchmarkFlashAttention(sequence);
results.push({
sequenceLength: sequence.length,
speedup: baselineResult.time / flashResult.time,
memoryReduction: (baselineResult.memory - flashResult.memory) / baselineResult.memory,
targetSpeedup: [2.49, 7.47],
achieved: this.checkTarget(flashResult, [2.49, 7.47])
});
}
return {
results,
averageSpeedup: this.calculateAverage(results, 'speedup'),
averageMemoryReduction: this.calculateAverage(results, 'memoryReduction')
};
}
}
```
### SONA Learning Benchmarks
```typescript
class SONABenchmarks {
async benchmarkAdaptationTime(): Promise<SONABenchmark> {
const scenarios = [
'pattern_recognition',
'task_optimization',
'error_correction',
'performance_tuning'
];
const results = [];
for (const scenario of scenarios) {
const startTime = performance.hrtime.bigint();
await this.sona.adapt(scenario);
const endTime = performance.hrtime.bigint();
const adaptationTimeMs = Number(endTime - startTime) / 1000000;
results.push({
scenario,
adaptationTime: adaptationTimeMs,
target: 0.05, // ms
achieved: adaptationTimeMs <= 0.05
});
}
return {
scenarios: results,
averageTime: results.reduce((sum, r) => sum + r.adaptationTime, 0) / results.length,
successRate: results.filter(r => r.achieved).length / results.length
};
}
}
```
## Performance Monitoring Dashboard
### Real-time Metrics
```typescript
class PerformanceMonitor {
async collectMetrics(): Promise<PerformanceSnapshot> {
return {
timestamp: Date.now(),
flashAttention: await this.measureFlashAttention(),
searchPerformance: await this.measureSearchSpeed(),
memoryUsage: await this.measureMemoryEfficiency(),
startupTime: await this.measureStartupLatency(),
sonaAdaptation: await this.measureSONASpeed(),
swarmCoordination: await this.measureSwarmEfficiency()
};
}
async generateReport(): Promise<PerformanceReport> {
const snapshot = await this.collectMetrics();
return {
summary: this.generateSummary(snapshot),
achievements: this.checkTargetAchievements(snapshot),
trends: this.analyzeTrends(),
recommendations: this.generateOptimizations(),
regressions: await this.detectRegressions()
};
}
}
```
### Continuous Regression Detection
```typescript
class PerformanceRegression {
async detectRegressions(): Promise<RegressionReport> {
const current = await this.runFullBenchmark();
const baseline = await this.getBaseline();
const regressions = [];
for (const [metric, currentValue] of Object.entries(current)) {
const baselineValue = baseline[metric];
const change = (currentValue - baselineValue) / baselineValue;
if (change < -0.05) { // 5% regression threshold
regressions.push({
metric,
baseline: baselineValue,
current: currentValue,
regressionPercent: change * 100,
severity: this.classifyRegression(change)
});
}
}
return {
hasRegressions: regressions.length > 0,
regressions,
recommendations: this.generateRegressionFixes(regressions)
};
}
}
```
## Optimization Strategies
### Memory Optimization
```typescript
class MemoryOptimization {
async optimizeMemoryUsage(): Promise<OptimizationResult> {
// Implement memory pooling
await this.setupMemoryPools();
// Enable garbage collection tuning
await this.optimizeGarbageCollection();
// Implement object reuse patterns
await this.setupObjectPools();
// Enable memory compression
await this.enableMemoryCompression();
return this.validateMemoryReduction();
}
}
```
### CPU Optimization
```typescript
class CPUOptimization {
async optimizeCPUUsage(): Promise<OptimizationResult> {
// Implement worker thread pools
await this.setupWorkerThreads();
// Enable CPU-specific optimizations
await this.enableSIMDInstructions();
// Implement task batching
await this.optimizeTaskBatching();
return this.validateCPUImprovement();
}
}
```
## Target Validation Framework
### Performance Gates
```typescript
class PerformanceGates {
async validateAllTargets(): Promise<ValidationReport> {
const results = await Promise.all([
this.validateFlashAttention(), // 2.49x-7.47x
this.validateSearchPerformance(), // 150x-12,500x
this.validateMemoryReduction(), // 50-75%
this.validateStartupTime(), // <500ms
this.validateSONAAdaptation() // <0.05ms
]);
return {
allTargetsAchieved: results.every(r => r.achieved),
results,
overallScore: this.calculateOverallScore(results),
recommendations: this.generateRecommendations(results)
};
}
}
```
## Success Metrics
### Primary Targets
- [ ] **Flash Attention**: 2.49x-7.47x speedup validated
- [ ] **Search Performance**: 150x-12,500x improvement confirmed
- [ ] **Memory Reduction**: 50-75% usage optimization achieved
- [ ] **Startup Time**: <500ms cold start consistently
- [ ] **SONA Adaptation**: <0.05ms learning response time
- [ ] **15-Agent Coordination**: Efficient parallel execution
### Continuous Monitoring
- [ ] **Performance Dashboard**: Real-time metrics collection
- [ ] **Regression Testing**: Automated performance validation
- [ ] **Trend Analysis**: Performance evolution tracking
- [ ] **Alert System**: Immediate regression notification
## Related V3 Skills
- `v3-integration-deep` - Performance integration with agentic-flow
- `v3-memory-unification` - Memory performance optimization
- `v3-swarm-coordination` - Swarm performance coordination
- `v3-security-overhaul` - Secure performance patterns
## Usage Examples
### Complete Performance Validation
```bash
# Full performance suite
npm run benchmark:v3
# Specific target validation
npm run benchmark:flash-attention
npm run benchmark:agentdb-search
npm run benchmark:memory-optimization
# Continuous monitoring
npm run monitor:performance
```
@@ -1,82 +0,0 @@
---
name: "V3 Security Overhaul"
description: "Complete security architecture overhaul for Codex-flow v3. Addresses critical CVEs (CVE-1, CVE-2, CVE-3) and implements secure-by-default patterns. Use for security-first v3 implementation."
---
# V3 Security Overhaul
## What This Skill Does
Orchestrates comprehensive security overhaul for Codex-flow v3, addressing critical vulnerabilities and establishing security-first development practices using specialized v3 security agents.
## Quick Start
```bash
# Initialize V3 security domain (parallel)
Task("Security architecture", "Design v3 threat model and security boundaries", "v3-security-architect")
Task("CVE remediation", "Fix CVE-1, CVE-2, CVE-3 critical vulnerabilities", "security-auditor")
Task("Security testing", "Implement TDD London School security framework", "test-architect")
```
## Critical Security Fixes
### CVE-1: Vulnerable Dependencies
```bash
npm update @anthropic-ai/Codex@^2.0.31
npm audit --audit-level high
```
### CVE-2: Weak Password Hashing
```typescript
// ❌ Old: SHA-256 with hardcoded salt
const hash = crypto.createHash('sha256').update(password + salt).digest('hex');
// ✅ New: bcrypt with 12 rounds
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12);
```
### CVE-3: Hardcoded Credentials
```typescript
// ✅ Generate secure random credentials
const apiKey = crypto.randomBytes(32).toString('hex');
```
## Security Patterns
### Input Validation (Zod)
```typescript
import { z } from 'zod';
const TaskSchema = z.object({
taskId: z.string().uuid(),
content: z.string().max(10000),
agentType: z.enum(['security', 'core', 'integration'])
});
```
### Path Sanitization
```typescript
function securePath(userPath: string, allowedPrefix: string): string {
const resolved = path.resolve(allowedPrefix, userPath);
if (!resolved.startsWith(path.resolve(allowedPrefix))) {
throw new SecurityError('Path traversal detected');
}
return resolved;
}
```
### Safe Command Execution
```typescript
import { execFile } from 'child_process';
// ✅ Safe: No shell interpretation
const { stdout } = await execFile('git', [userInput], { shell: false });
```
## Success Metrics
- **Security Score**: 90/100 (npm audit + custom scans)
- **CVE Resolution**: 100% of critical vulnerabilities fixed
- **Test Coverage**: >95% security-critical code
- **Implementation**: All secure patterns documented and tested
@@ -1,340 +0,0 @@
---
name: "V3 Swarm Coordination"
description: "15-agent hierarchical mesh coordination for v3 implementation. Orchestrates parallel execution across security, core, and integration domains following 10 ADRs with 14-week timeline."
---
# V3 Swarm Coordination
## What This Skill Does
Orchestrates the complete 15-agent hierarchical mesh swarm for Codex-flow v3 implementation, coordinating parallel execution across domains while maintaining dependencies and timeline adherence.
## Quick Start
```bash
# Initialize 15-agent v3 swarm
Task("Swarm initialization", "Initialize hierarchical mesh for v3 implementation", "v3-queen-coordinator")
# Security domain (Phase 1 - Critical priority)
Task("Security architecture", "Design v3 threat model and security boundaries", "v3-security-architect")
Task("CVE remediation", "Fix CVE-1, CVE-2, CVE-3 vulnerabilities", "security-auditor")
Task("Security testing", "Implement TDD security framework", "test-architect")
# Core domain (Phase 2 - Parallel execution)
Task("Memory unification", "Implement AgentDB 150x improvement", "v3-memory-specialist")
Task("Integration architecture", "Deep agentic-flow@alpha integration", "v3-integration-architect")
Task("Performance validation", "Validate 2.49x-7.47x targets", "v3-performance-engineer")
```
## 15-Agent Swarm Architecture
### Hierarchical Mesh Topology
```
👑 QUEEN COORDINATOR
(Agent #1)
┌────────────────────┼────────────────────┐
│ │ │
🛡️ SECURITY 🧠 CORE 🔗 INTEGRATION
(Agents #2-4) (Agents #5-9) (Agents #10-12)
│ │ │
└────────────────────┼────────────────────┘
┌────────────────────┼────────────────────┐
│ │ │
🧪 QUALITY ⚡ PERFORMANCE 🚀 DEPLOYMENT
(Agent #13) (Agent #14) (Agent #15)
```
### Agent Roster
| ID | Agent | Domain | Phase | Responsibility |
|----|-------|--------|-------|----------------|
| 1 | Queen Coordinator | Orchestration | All | GitHub issues, dependencies, timeline |
| 2 | Security Architect | Security | Foundation | Threat modeling, CVE planning |
| 3 | Security Implementer | Security | Foundation | CVE fixes, secure patterns |
| 4 | Security Tester | Security | Foundation | TDD security testing |
| 5 | Core Architect | Core | Systems | DDD architecture, coordination |
| 6 | Core Implementer | Core | Systems | Core module implementation |
| 7 | Memory Specialist | Core | Systems | AgentDB unification |
| 8 | Swarm Specialist | Core | Systems | Unified coordination engine |
| 9 | MCP Specialist | Core | Systems | MCP server optimization |
| 10 | Integration Architect | Integration | Integration | agentic-flow@alpha deep integration |
| 11 | CLI/Hooks Developer | Integration | Integration | CLI modernization |
| 12 | Neural/Learning Dev | Integration | Integration | SONA integration |
| 13 | TDD Test Engineer | Quality | All | London School TDD |
| 14 | Performance Engineer | Performance | Optimization | Benchmarking validation |
| 15 | Release Engineer | Deployment | Release | CI/CD and v3.0.0 release |
## Implementation Phases
### Phase 1: Foundation (Week 1-2)
**Active Agents**: #1, #2-4, #5-6
```typescript
const phase1 = async () => {
// Parallel security and architecture foundation
await Promise.all([
// Security domain (critical priority)
Task("Security architecture", "Complete threat model and security boundaries", "v3-security-architect"),
Task("CVE-1 fix", "Update vulnerable dependencies", "security-implementer"),
Task("CVE-2 fix", "Replace weak password hashing", "security-implementer"),
Task("CVE-3 fix", "Remove hardcoded credentials", "security-implementer"),
Task("Security testing", "TDD London School security framework", "test-architect"),
// Core architecture foundation
Task("DDD architecture", "Design domain boundaries and structure", "core-architect"),
Task("Type modernization", "Update type system for v3", "core-implementer")
]);
};
```
### Phase 2: Core Systems (Week 3-6)
**Active Agents**: #1, #5-9, #13
```typescript
const phase2 = async () => {
// Parallel core system implementation
await Promise.all([
Task("Memory unification", "Implement AgentDB with 150x-12,500x improvement", "v3-memory-specialist"),
Task("Swarm coordination", "Merge 4 coordination systems into unified engine", "swarm-specialist"),
Task("MCP optimization", "Optimize MCP server performance", "mcp-specialist"),
Task("Core implementation", "Implement DDD modular architecture", "core-implementer"),
Task("TDD core tests", "Comprehensive test coverage for core systems", "test-architect")
]);
};
```
### Phase 3: Integration (Week 7-10)
**Active Agents**: #1, #10-12, #13-14
```typescript
const phase3 = async () => {
// Parallel integration and optimization
await Promise.all([
Task("agentic-flow integration", "Eliminate 10,000+ duplicate lines", "v3-integration-architect"),
Task("CLI modernization", "Enhance CLI with hooks system", "cli-hooks-developer"),
Task("SONA integration", "Implement <0.05ms learning adaptation", "neural-learning-developer"),
Task("Performance benchmarking", "Validate 2.49x-7.47x targets", "v3-performance-engineer"),
Task("Integration testing", "End-to-end system validation", "test-architect")
]);
};
```
### Phase 4: Release (Week 11-14)
**Active Agents**: All 15
```typescript
const phase4 = async () => {
// Full swarm final optimization
await Promise.all([
Task("Performance optimization", "Final optimization pass", "v3-performance-engineer"),
Task("Release preparation", "CI/CD pipeline and v3.0.0 release", "release-engineer"),
Task("Final testing", "Complete test coverage validation", "test-architect"),
// All agents: Final polish and optimization
...agents.map(agent =>
Task("Final polish", `Agent ${agent.id} final optimization`, agent.name)
)
]);
};
```
## Coordination Patterns
### Dependency Management
```typescript
class DependencyCoordination {
private dependencies = new Map([
// Security first (no dependencies)
[2, []], [3, [2]], [4, [2, 3]],
// Core depends on security foundation
[5, [2]], [6, [5]], [7, [5]], [8, [5, 7]], [9, [5]],
// Integration depends on core systems
[10, [5, 7, 8]], [11, [5, 10]], [12, [7, 10]],
// Quality and performance cross-cutting
[13, [2, 5]], [14, [5, 7, 8, 10]], [15, [13, 14]]
]);
async coordinateExecution(): Promise<void> {
const completed = new Set<number>();
while (completed.size < 15) {
const ready = this.getReadyAgents(completed);
if (ready.length === 0) {
throw new Error('Deadlock detected in dependency chain');
}
// Execute ready agents in parallel
await Promise.all(ready.map(agentId => this.executeAgent(agentId)));
ready.forEach(id => completed.add(id));
}
}
}
```
### GitHub Integration
```typescript
class GitHubCoordination {
async initializeV3Milestone(): Promise<void> {
await gh.createMilestone({
title: 'Codex-Flow v3.0.0 Implementation',
description: '15-agent swarm implementation of 10 ADRs',
dueDate: this.calculate14WeekDeadline()
});
}
async createEpicIssues(): Promise<void> {
const epics = [
{ title: 'Security Overhaul (CVE-1,2,3)', agents: [2, 3, 4] },
{ title: 'Memory Unification (AgentDB)', agents: [7] },
{ title: 'agentic-flow Integration', agents: [10] },
{ title: 'Performance Optimization', agents: [14] },
{ title: 'DDD Architecture', agents: [5, 6] }
];
for (const epic of epics) {
await gh.createIssue({
title: epic.title,
labels: ['epic', 'v3', ...epic.agents.map(id => `agent-${id}`)],
assignees: epic.agents.map(id => this.getAgentGithubUser(id))
});
}
}
async trackProgress(): Promise<void> {
// Hourly progress updates from each agent
setInterval(async () => {
for (const agent of this.agents) {
await this.postAgentProgress(agent);
}
}, 3600000); // 1 hour
}
}
```
### Communication Bus
```typescript
class SwarmCommunication {
private bus = new QuicSwarmBus({
maxAgents: 15,
messageTimeout: 30000,
retryAttempts: 3
});
async broadcastToSecurityDomain(message: SwarmMessage): Promise<void> {
await this.bus.broadcast(message, {
targetAgents: [2, 3, 4],
priority: 'critical'
});
}
async coordinateCoreSystems(message: SwarmMessage): Promise<void> {
await this.bus.broadcast(message, {
targetAgents: [5, 6, 7, 8, 9],
priority: 'high'
});
}
async notifyIntegrationTeam(message: SwarmMessage): Promise<void> {
await this.bus.broadcast(message, {
targetAgents: [10, 11, 12],
priority: 'medium'
});
}
}
```
## Performance Coordination
### Parallel Efficiency Monitoring
```typescript
class EfficiencyMonitor {
async measureParallelEfficiency(): Promise<EfficiencyReport> {
const agentUtilization = await this.measureAgentUtilization();
const coordinationOverhead = await this.measureCoordinationCost();
return {
totalEfficiency: agentUtilization.average,
target: 0.85, // >85% utilization
achieved: agentUtilization.average > 0.85,
bottlenecks: this.identifyBottlenecks(agentUtilization),
recommendations: this.generateOptimizations()
};
}
}
```
### Load Balancing
```typescript
class SwarmLoadBalancer {
async balanceWorkload(): Promise<void> {
const workloads = await this.analyzeAgentWorkloads();
for (const [agentId, load] of workloads.entries()) {
if (load > this.getCapacityThreshold(agentId)) {
await this.redistributeWork(agentId);
}
}
}
async redistributeWork(overloadedAgent: number): Promise<void> {
const availableAgents = this.getAvailableAgents();
const tasks = await this.getAgentTasks(overloadedAgent);
// Redistribute tasks to available agents
for (const task of tasks) {
const bestAgent = this.selectOptimalAgent(task, availableAgents);
await this.reassignTask(task, bestAgent);
}
}
}
```
## Success Metrics
### Swarm Coordination
- [ ] **Parallel Efficiency**: >85% agent utilization time
- [ ] **Dependency Resolution**: Zero deadlocks or blocking issues
- [ ] **Communication Latency**: <100ms inter-agent messaging
- [ ] **Timeline Adherence**: 14-week delivery maintained
- [ ] **GitHub Integration**: <4h automated issue response
### Implementation Targets
- [ ] **ADR Coverage**: All 10 ADRs implemented successfully
- [ ] **Performance**: 2.49x-7.47x Flash Attention achieved
- [ ] **Search**: 150x-12,500x AgentDB improvement validated
- [ ] **Code Reduction**: <5,000 lines (vs 15,000+)
- [ ] **Security**: 90/100 security score achieved
## Related V3 Skills
- `v3-security-overhaul` - Security domain coordination
- `v3-memory-unification` - Memory system coordination
- `v3-integration-deep` - Integration domain coordination
- `v3-performance-optimization` - Performance domain coordination
## Usage Examples
### Initialize Complete V3 Swarm
```bash
# Queen Coordinator initializes full swarm
Task("V3 swarm initialization",
"Initialize 15-agent hierarchical mesh for complete v3 implementation",
"v3-queen-coordinator")
```
### Phase-based Execution
```bash
# Phase 1: Security-first foundation
npm run v3:phase1:security
# Phase 2: Core systems parallel
npm run v3:phase2:core-systems
# Phase 3: Integration and optimization
npm run v3:phase3:integration
# Phase 4: Release preparation
npm run v3:phase4:release
```
@@ -1,649 +0,0 @@
---
name: "Verification & Quality Assurance"
description: "Comprehensive truth scoring, code quality verification, and automatic rollback system with 0.95 accuracy threshold for ensuring high-quality agent outputs and codebase reliability."
version: "2.0.0"
category: "quality-assurance"
tags: ["verification", "truth-scoring", "quality", "rollback", "metrics", "ci-cd"]
---
# Verification & Quality Assurance Skill
## What This Skill Does
This skill provides a comprehensive verification and quality assurance system that ensures code quality and correctness through:
- **Truth Scoring**: Real-time reliability metrics (0.0-1.0 scale) for code, agents, and tasks
- **Verification Checks**: Automated code correctness, security, and best practices validation
- **Automatic Rollback**: Instant reversion of changes that fail verification (default threshold: 0.95)
- **Quality Metrics**: Statistical analysis with trends, confidence intervals, and improvement tracking
- **CI/CD Integration**: Export capabilities for continuous integration pipelines
- **Real-time Monitoring**: Live dashboards and watch modes for ongoing verification
## Prerequisites
- Codex Flow installed (`npx Codex-flow@alpha`)
- Git repository (for rollback features)
- Node.js 18+ (for dashboard features)
## Quick Start
```bash
# View current truth scores
npx Codex-flow@alpha truth
# Run verification check
npx Codex-flow@alpha verify check
# Verify specific file with custom threshold
npx Codex-flow@alpha verify check --file src/app.js --threshold 0.98
# Rollback last failed verification
npx Codex-flow@alpha verify rollback --last-good
```
---
## Complete Guide
### Truth Scoring System
#### View Truth Metrics
Display comprehensive quality and reliability metrics for your codebase and agent tasks.
**Basic Usage:**
```bash
# View current truth scores (default: table format)
npx Codex-flow@alpha truth
# View scores for specific time period
npx Codex-flow@alpha truth --period 7d
# View scores for specific agent
npx Codex-flow@alpha truth --agent coder --period 24h
# Find files/tasks below threshold
npx Codex-flow@alpha truth --threshold 0.8
```
**Output Formats:**
```bash
# Table format (default)
npx Codex-flow@alpha truth --format table
# JSON for programmatic access
npx Codex-flow@alpha truth --format json
# CSV for spreadsheet analysis
npx Codex-flow@alpha truth --format csv
# HTML report with visualizations
npx Codex-flow@alpha truth --format html --export report.html
```
**Real-time Monitoring:**
```bash
# Watch mode with live updates
npx Codex-flow@alpha truth --watch
# Export metrics automatically
npx Codex-flow@alpha truth --export .Codex-flow/metrics/truth-$(date +%Y%m%d).json
```
#### Truth Score Dashboard
Example dashboard output:
```
📊 Truth Metrics Dashboard
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Overall Truth Score: 0.947 ✅
Trend: ↗️ +2.3% (7d)
Top Performers:
verification-agent 0.982 ⭐
code-analyzer 0.971 ⭐
test-generator 0.958 ✅
Needs Attention:
refactor-agent 0.821 ⚠️
docs-generator 0.794 ⚠️
Recent Tasks:
task-456 0.991 ✅ "Implement auth"
task-455 0.967 ✅ "Add tests"
task-454 0.743 ❌ "Refactor API"
```
#### Metrics Explained
**Truth Scores (0.0-1.0):**
- `1.0-0.95`: Excellent ⭐ (production-ready)
- `0.94-0.85`: Good ✅ (acceptable quality)
- `0.84-0.75`: Warning ⚠️ (needs attention)
- `<0.75`: Critical ❌ (requires immediate action)
**Trend Indicators:**
- ↗️ Improving (positive trend)
- → Stable (consistent performance)
- ↘️ Declining (quality regression detected)
**Statistics:**
- **Mean Score**: Average truth score across all measurements
- **Median Score**: Middle value (less affected by outliers)
- **Standard Deviation**: Consistency of scores (lower = more consistent)
- **Confidence Interval**: Statistical reliability of measurements
### Verification Checks
#### Run Verification
Execute comprehensive verification checks on code, tasks, or agent outputs.
**File Verification:**
```bash
# Verify single file
npx Codex-flow@alpha verify check --file src/app.js
# Verify directory recursively
npx Codex-flow@alpha verify check --directory src/
# Verify with auto-fix enabled
npx Codex-flow@alpha verify check --file src/utils.js --auto-fix
# Verify current working directory
npx Codex-flow@alpha verify check
```
**Task Verification:**
```bash
# Verify specific task output
npx Codex-flow@alpha verify check --task task-123
# Verify with custom threshold
npx Codex-flow@alpha verify check --task task-456 --threshold 0.99
# Verbose output for debugging
npx Codex-flow@alpha verify check --task task-789 --verbose
```
**Batch Verification:**
```bash
# Verify multiple files in parallel
npx Codex-flow@alpha verify batch --files "*.js" --parallel
# Verify with pattern matching
npx Codex-flow@alpha verify batch --pattern "src/**/*.ts"
# Integration test suite
npx Codex-flow@alpha verify integration --test-suite full
```
#### Verification Criteria
The verification system evaluates:
1. **Code Correctness**
- Syntax validation
- Type checking (TypeScript)
- Logic flow analysis
- Error handling completeness
2. **Best Practices**
- Code style adherence
- SOLID principles
- Design patterns usage
- Modularity and reusability
3. **Security**
- Vulnerability scanning
- Secret detection
- Input validation
- Authentication/authorization checks
4. **Performance**
- Algorithmic complexity
- Memory usage patterns
- Database query optimization
- Bundle size impact
5. **Documentation**
- JSDoc/TypeDoc completeness
- README accuracy
- API documentation
- Code comments quality
#### JSON Output for CI/CD
```bash
# Get structured JSON output
npx Codex-flow@alpha verify check --json > verification.json
# Example JSON structure:
{
"overallScore": 0.947,
"passed": true,
"threshold": 0.95,
"checks": [
{
"name": "code-correctness",
"score": 0.98,
"passed": true
},
{
"name": "security",
"score": 0.91,
"passed": false,
"issues": [...]
}
]
}
```
### Automatic Rollback
#### Rollback Failed Changes
Automatically revert changes that fail verification checks.
**Basic Rollback:**
```bash
# Rollback to last known good state
npx Codex-flow@alpha verify rollback --last-good
# Rollback to specific commit
npx Codex-flow@alpha verify rollback --to-commit abc123
# Interactive rollback with preview
npx Codex-flow@alpha verify rollback --interactive
```
**Smart Rollback:**
```bash
# Rollback only failed files (preserve good changes)
npx Codex-flow@alpha verify rollback --selective
# Rollback with automatic backup
npx Codex-flow@alpha verify rollback --backup-first
# Dry-run mode (preview without executing)
npx Codex-flow@alpha verify rollback --dry-run
```
**Rollback Performance:**
- Git-based rollback: <1 second
- Selective file rollback: <500ms
- Backup creation: Automatic before rollback
### Verification Reports
#### Generate Reports
Create detailed verification reports with metrics and visualizations.
**Report Formats:**
```bash
# JSON report
npx Codex-flow@alpha verify report --format json
# HTML report with charts
npx Codex-flow@alpha verify report --export metrics.html --format html
# CSV for data analysis
npx Codex-flow@alpha verify report --format csv --export metrics.csv
# Markdown summary
npx Codex-flow@alpha verify report --format markdown
```
**Time-based Reports:**
```bash
# Last 24 hours
npx Codex-flow@alpha verify report --period 24h
# Last 7 days
npx Codex-flow@alpha verify report --period 7d
# Last 30 days with trends
npx Codex-flow@alpha verify report --period 30d --include-trends
# Custom date range
npx Codex-flow@alpha verify report --from 2025-01-01 --to 2025-01-31
```
**Report Content:**
- Overall truth scores
- Per-agent performance metrics
- Task completion quality
- Verification pass/fail rates
- Rollback frequency
- Quality improvement trends
- Statistical confidence intervals
### Interactive Dashboard
#### Launch Dashboard
Run interactive web-based verification dashboard with real-time updates.
```bash
# Launch dashboard on default port (3000)
npx Codex-flow@alpha verify dashboard
# Custom port
npx Codex-flow@alpha verify dashboard --port 8080
# Export dashboard data
npx Codex-flow@alpha verify dashboard --export
# Dashboard with auto-refresh
npx Codex-flow@alpha verify dashboard --refresh 5s
```
**Dashboard Features:**
- Real-time truth score updates (WebSocket)
- Interactive charts and graphs
- Agent performance comparison
- Task history timeline
- Rollback history viewer
- Export to PDF/HTML
- Filter by time period/agent/score
### Configuration
#### Default Configuration
Set verification preferences in `.Codex-flow/config.json`:
```json
{
"verification": {
"threshold": 0.95,
"autoRollback": true,
"gitIntegration": true,
"hooks": {
"preCommit": true,
"preTask": true,
"postEdit": true
},
"checks": {
"codeCorrectness": true,
"security": true,
"performance": true,
"documentation": true,
"bestPractices": true
}
},
"truth": {
"defaultFormat": "table",
"defaultPeriod": "24h",
"warningThreshold": 0.85,
"criticalThreshold": 0.75,
"autoExport": {
"enabled": true,
"path": ".Codex-flow/metrics/truth-daily.json"
}
}
}
```
#### Threshold Configuration
**Adjust verification strictness:**
```bash
# Strict mode (99% accuracy required)
npx Codex-flow@alpha verify check --threshold 0.99
# Lenient mode (90% acceptable)
npx Codex-flow@alpha verify check --threshold 0.90
# Set default threshold
npx Codex-flow@alpha config set verification.threshold 0.98
```
**Per-environment thresholds:**
```json
{
"verification": {
"thresholds": {
"production": 0.99,
"staging": 0.95,
"development": 0.90
}
}
}
```
### Integration Examples
#### CI/CD Integration
**GitHub Actions:**
```yaml
name: Quality Verification
on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Dependencies
run: npm install
- name: Run Verification
run: |
npx Codex-flow@alpha verify check --json > verification.json
- name: Check Truth Score
run: |
score=$(jq '.overallScore' verification.json)
if (( $(echo "$score < 0.95" | bc -l) )); then
echo "Truth score too low: $score"
exit 1
fi
- name: Upload Report
uses: actions/upload-artifact@v3
with:
name: verification-report
path: verification.json
```
**GitLab CI:**
```yaml
verify:
stage: test
script:
- npx Codex-flow@alpha verify check --threshold 0.95 --json > verification.json
- |
score=$(jq '.overallScore' verification.json)
if [ $(echo "$score < 0.95" | bc) -eq 1 ]; then
echo "Verification failed with score: $score"
exit 1
fi
artifacts:
paths:
- verification.json
reports:
junit: verification.json
```
#### Swarm Integration
Run verification automatically during swarm operations:
```bash
# Swarm with verification enabled
npx Codex-flow@alpha swarm --verify --threshold 0.98
# Hive Mind with auto-rollback
npx Codex-flow@alpha hive-mind --verify --rollback-on-fail
# Training pipeline with verification
npx Codex-flow@alpha train --verify --threshold 0.99
```
#### Pair Programming Integration
Enable real-time verification during collaborative development:
```bash
# Pair with verification
npx Codex-flow@alpha pair --verify --real-time
# Pair with custom threshold
npx Codex-flow@alpha pair --verify --threshold 0.97 --auto-fix
```
### Advanced Workflows
#### Continuous Verification
Monitor codebase continuously during development:
```bash
# Watch directory for changes
npx Codex-flow@alpha verify watch --directory src/
# Watch with auto-fix
npx Codex-flow@alpha verify watch --directory src/ --auto-fix
# Watch with notifications
npx Codex-flow@alpha verify watch --notify --threshold 0.95
```
#### Monitoring Integration
Send metrics to external monitoring systems:
```bash
# Export to Prometheus
npx Codex-flow@alpha truth --format json | \
curl -X POST https://pushgateway.example.com/metrics/job/Codex-flow \
-d @-
# Send to DataDog
npx Codex-flow@alpha verify report --format json | \
curl -X POST "https://api.datadoghq.com/api/v1/series?api_key=${DD_API_KEY}" \
-H "Content-Type: application/json" \
-d @-
# Custom webhook
npx Codex-flow@alpha truth --format json | \
curl -X POST https://metrics.example.com/api/truth \
-H "Content-Type: application/json" \
-d @-
```
#### Pre-commit Hooks
Automatically verify before commits:
```bash
# Install pre-commit hook
npx Codex-flow@alpha verify install-hook --pre-commit
# .git/hooks/pre-commit example:
#!/bin/bash
npx Codex-flow@alpha verify check --threshold 0.95 --json > /tmp/verify.json
score=$(jq '.overallScore' /tmp/verify.json)
if (( $(echo "$score < 0.95" | bc -l) )); then
echo "❌ Verification failed with score: $score"
echo "Run 'npx Codex-flow@alpha verify check --verbose' for details"
exit 1
fi
echo "✅ Verification passed with score: $score"
```
### Performance Metrics
**Verification Speed:**
- Single file check: <100ms
- Directory scan: <500ms (per 100 files)
- Full codebase analysis: <5s (typical project)
- Truth score calculation: <50ms
**Rollback Speed:**
- Git-based rollback: <1s
- Selective file rollback: <500ms
- Backup creation: <2s
**Dashboard Performance:**
- Initial load: <1s
- Real-time updates: <100ms latency (WebSocket)
- Chart rendering: 60 FPS
### Troubleshooting
#### Common Issues
**Low Truth Scores:**
```bash
# Get detailed breakdown
npx Codex-flow@alpha truth --verbose --threshold 0.0
# Check specific criteria
npx Codex-flow@alpha verify check --verbose
# View agent-specific issues
npx Codex-flow@alpha truth --agent <agent-name> --format json
```
**Rollback Failures:**
```bash
# Check git status
git status
# View rollback history
npx Codex-flow@alpha verify rollback --history
# Manual rollback
git reset --hard HEAD~1
```
**Verification Timeouts:**
```bash
# Increase timeout
npx Codex-flow@alpha verify check --timeout 60s
# Verify in batches
npx Codex-flow@alpha verify batch --batch-size 10
```
### Exit Codes
Verification commands return standard exit codes:
- `0`: Verification passed (score ≥ threshold)
- `1`: Verification failed (score < threshold)
- `2`: Error during verification (invalid input, system error)
### Related Commands
- `npx Codex-flow@alpha pair` - Collaborative development with verification
- `npx Codex-flow@alpha train` - Training with verification feedback
- `npx Codex-flow@alpha swarm` - Multi-agent coordination with quality checks
- `npx Codex-flow@alpha report` - Generate comprehensive project reports
### Best Practices
1. **Set Appropriate Thresholds**: Use 0.99 for critical code, 0.95 for standard, 0.90 for experimental
2. **Enable Auto-rollback**: Prevent bad code from persisting
3. **Monitor Trends**: Track improvement over time, not just current scores
4. **Integrate with CI/CD**: Make verification part of your pipeline
5. **Use Watch Mode**: Get immediate feedback during development
6. **Export Metrics**: Track quality metrics in your monitoring system
7. **Review Rollbacks**: Understand why changes were rejected
8. **Train Agents**: Use verification feedback to improve agent performance
### Additional Resources
- Truth Scoring Algorithm: See `/docs/truth-scoring.md`
- Verification Criteria: See `/docs/verification-criteria.md`
- Integration Examples: See `/examples/verification/`
- API Reference: See `/docs/api/verification.md`
-579
View File
@@ -1,579 +0,0 @@
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"os"
"sort"
"strings"
"time"
"github.com/redis/go-redis/v9"
_ "github.com/go-sql-driver/mysql"
"gopkg.in/yaml.v3"
)
type appConfig struct {
MySQL struct {
Addr string `yaml:"Addr"`
Username string `yaml:"Username"`
Password string `yaml:"Password"`
Dbname string `yaml:"Dbname"`
Config string `yaml:"Config"`
} `yaml:"MySQL"`
Redis struct {
Host string `yaml:"Host"`
Pass string `yaml:"Pass"`
DB int `yaml:"DB"`
} `yaml:"Redis"`
}
type userRow struct {
ID int64 `json:"id"`
ReferCode string `json:"refer_code"`
Balance int64 `json:"balance"`
Commission int64 `json:"commission"`
GiftAmount int64 `json:"gift_amount"`
Enable bool `json:"enable"`
IsAdmin bool `json:"is_admin"`
ValidEmail bool `json:"valid_email"`
MemberStatus string `json:"member_status"`
CreatedAt time.Time `json:"created_at"`
DeletedAt sql.NullTime `json:"-"`
}
type authMethod struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
AuthType string `json:"auth_type"`
Identifier string `json:"identifier"`
Verified bool `json:"verified"`
CreatedAt time.Time `json:"created_at"`
}
type deviceInfo struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Identifier string `json:"identifier"`
ShortCode string `json:"short_code"`
Online bool `json:"online"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
}
type subscribeInfo struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
OrderID int64 `json:"order_id"`
SubscribeID int64 `json:"subscribe_id"`
Token string `json:"token"`
UUID string `json:"uuid"`
Status uint8 `json:"status"`
StartTime time.Time `json:"start_time"`
ExpireTime time.Time `json:"expire_time"`
}
type familyInfo struct {
FamilyID int64 `json:"family_id"`
OwnerUserID int64 `json:"owner_user_id"`
IsOwner bool `json:"is_owner"`
MemberCount int64 `json:"member_count"`
}
type userSummary struct {
User userRow `json:"user"`
AuthMethods []authMethod `json:"auth_methods"`
Devices []deviceInfo `json:"devices"`
Subscriptions []subscribeInfo `json:"subscriptions"`
Family *familyInfo `json:"family,omitempty"`
OrderCount int64 `json:"order_count"`
TicketCount int64 `json:"ticket_count"`
TrafficLogCount int64 `json:"traffic_log_count"`
SystemLogCount int64 `json:"system_log_count"`
WithdrawalCount int64 `json:"withdrawal_count"`
IAPTransactionCount int64 `json:"iap_transaction_count"`
LogMessageCount int64 `json:"log_message_count"`
OnlineRecordCount int64 `json:"online_record_count"`
}
type deleteResult struct {
UserID int64 `json:"user_id"`
DeletedDBRows []string `json:"deleted_db_rows"`
DeletedRedisKeys int `json:"deleted_redis_keys"`
}
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
ctx := context.Background()
cfg := loadConfig("/Users/Apple/code_vpn/vpn/ppanel-server/etc/ppanel.yaml")
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", cfg.MySQL.Username, cfg.MySQL.Password, cfg.MySQL.Addr, cfg.MySQL.Dbname, cfg.MySQL.Config)
db, err := sql.Open("mysql", dsn)
must(err)
defer db.Close()
must(db.PingContext(ctx))
rdb := redis.NewClient(&redis.Options{
Addr: cfg.Redis.Host,
Password: cfg.Redis.Pass,
DB: cfg.Redis.DB,
})
defer rdb.Close()
must(rdb.Ping(ctx).Err())
targetUserIDs, err := findTargetUsers(ctx, db)
must(err)
if len(targetUserIDs) == 0 {
fmt.Println(`{"matched_users":[],"deleted":[]}`)
return
}
summaries := make([]userSummary, 0, len(targetUserIDs))
for _, userID := range targetUserIDs {
summary, sumErr := collectSummary(ctx, db, userID)
must(sumErr)
summaries = append(summaries, summary)
}
before, err := json.MarshalIndent(map[string]interface{}{
"matched_users": summaries,
}, "", " ")
must(err)
fmt.Println(string(before))
results := make([]deleteResult, 0, len(targetUserIDs))
for _, summary := range summaries {
result, delErr := deleteUser(ctx, db, rdb, summary)
must(delErr)
results = append(results, result)
}
after, err := json.MarshalIndent(map[string]interface{}{
"deleted": results,
}, "", " ")
must(err)
fmt.Println(string(after))
}
func loadConfig(path string) appConfig {
content, err := os.ReadFile(path)
must(err)
var cfg appConfig
must(yaml.Unmarshal(content, &cfg))
return cfg
}
func findTargetUsers(ctx context.Context, db *sql.DB) ([]int64, error) {
rows, err := db.QueryContext(ctx, `
SELECT DISTINCT user_id
FROM user_device
WHERE user_agent LIKE ?
ORDER BY user_id ASC
`, "%999%")
if err != nil {
return nil, err
}
defer rows.Close()
var ids []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, rows.Err()
}
func collectSummary(ctx context.Context, db *sql.DB, userID int64) (userSummary, error) {
var summary userSummary
summary.User.ID = userID
err := db.QueryRowContext(ctx, `
SELECT id, refer_code, balance, commission, gift_amount, enable, is_admin, valid_email, member_status, created_at, deleted_at
FROM user
WHERE id = ?
`, userID).Scan(
&summary.User.ID,
&summary.User.ReferCode,
&summary.User.Balance,
&summary.User.Commission,
&summary.User.GiftAmount,
&summary.User.Enable,
&summary.User.IsAdmin,
&summary.User.ValidEmail,
&summary.User.MemberStatus,
&summary.User.CreatedAt,
&summary.User.DeletedAt,
)
if err != nil {
return summary, err
}
summary.AuthMethods, err = queryAuthMethods(ctx, db, userID)
if err != nil {
return summary, err
}
summary.Devices, err = queryDevices(ctx, db, userID)
if err != nil {
return summary, err
}
summary.Subscriptions, err = querySubscriptions(ctx, db, userID)
if err != nil {
return summary, err
}
summary.Family, err = queryFamily(ctx, db, userID)
if err != nil {
return summary, err
}
if summary.OrderCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM `order` WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.TicketCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM ticket WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.TrafficLogCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM traffic_log WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.SystemLogCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM system_logs WHERE object_id = ?", userID); err != nil {
return summary, err
}
if summary.WithdrawalCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM user_withdrawal WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.IAPTransactionCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM apple_iap_transactions WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.LogMessageCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM log_message WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.OnlineRecordCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM user_device_online_record WHERE user_id = ?", userID); err != nil {
return summary, err
}
return summary, nil
}
func queryAuthMethods(ctx context.Context, db *sql.DB, userID int64) ([]authMethod, error) {
rows, err := db.QueryContext(ctx, `
SELECT id, user_id, auth_type, auth_identifier, verified, created_at
FROM user_auth_methods
WHERE user_id = ?
ORDER BY id ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []authMethod
for rows.Next() {
var item authMethod
if err := rows.Scan(&item.ID, &item.UserID, &item.AuthType, &item.Identifier, &item.Verified, &item.CreatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func queryDevices(ctx context.Context, db *sql.DB, userID int64) ([]deviceInfo, error) {
rows, err := db.QueryContext(ctx, `
SELECT id, user_id, ip, user_agent, identifier, short_code, online, enabled, created_at
FROM user_device
WHERE user_id = ?
ORDER BY id ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []deviceInfo
for rows.Next() {
var item deviceInfo
if err := rows.Scan(&item.ID, &item.UserID, &item.IP, &item.UserAgent, &item.Identifier, &item.ShortCode, &item.Online, &item.Enabled, &item.CreatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func querySubscriptions(ctx context.Context, db *sql.DB, userID int64) ([]subscribeInfo, error) {
rows, err := db.QueryContext(ctx, `
SELECT id, user_id, order_id, subscribe_id, token, uuid, status, start_time, expire_time
FROM user_subscribe
WHERE user_id = ?
ORDER BY id ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []subscribeInfo
for rows.Next() {
var item subscribeInfo
if err := rows.Scan(&item.ID, &item.UserID, &item.OrderID, &item.SubscribeID, &item.Token, &item.UUID, &item.Status, &item.StartTime, &item.ExpireTime); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func queryFamily(ctx context.Context, db *sql.DB, userID int64) (*familyInfo, error) {
var info familyInfo
err := db.QueryRowContext(ctx, `
SELECT ufm.family_id, uf.owner_user_id
FROM user_family_member ufm
JOIN user_family uf ON uf.id = ufm.family_id AND uf.deleted_at IS NULL
WHERE ufm.user_id = ? AND ufm.deleted_at IS NULL
LIMIT 1
`, userID).Scan(&info.FamilyID, &info.OwnerUserID)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
info.IsOwner = info.OwnerUserID == userID
memberCount, err := queryCount(ctx, db, `
SELECT COUNT(*)
FROM user_family_member
WHERE family_id = ? AND deleted_at IS NULL
`, info.FamilyID)
if err != nil {
return nil, err
}
info.MemberCount = memberCount
return &info, nil
}
func queryCount(ctx context.Context, db *sql.DB, q string, arg interface{}) (int64, error) {
var count int64
err := db.QueryRowContext(ctx, q, arg).Scan(&count)
return count, err
}
func deleteUser(ctx context.Context, db *sql.DB, rdb *redis.Client, summary userSummary) (deleteResult, error) {
result := deleteResult{UserID: summary.User.ID}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return result, err
}
defer tx.Rollback()
if summary.Family != nil {
if summary.Family.IsOwner {
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family_member WHERE family_id = ?`, summary.Family.FamilyID); err != nil {
return result, err
} else {
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family_member=%d", rowsAffected(res)))
}
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family WHERE id = ?`, summary.Family.FamilyID); err != nil {
return result, err
} else {
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family=%d", rowsAffected(res)))
}
} else {
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family_member WHERE user_id = ? AND family_id = ?`, summary.User.ID, summary.Family.FamilyID); err != nil {
return result, err
} else {
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family_member=%d", rowsAffected(res)))
}
}
}
if res, err := tx.ExecContext(ctx, `DELETE FROM user_auth_methods WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_auth_methods=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user_subscribe WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_subscribe=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user_device WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_device=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user_device_online_record WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_device_online_record=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user_withdrawal WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_withdrawal=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, "DELETE FROM `order` WHERE user_id = ?", summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("order=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM traffic_log WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("traffic_log=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM system_logs WHERE object_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("system_logs=%d", rowsAffected(res))) }
var ticketIDs []int64
ticketRows, err := tx.QueryContext(ctx, `SELECT id FROM ticket WHERE user_id = ?`, summary.User.ID)
if err != nil {
return result, err
}
for ticketRows.Next() {
var id int64
if err := ticketRows.Scan(&id); err != nil {
ticketRows.Close()
return result, err
}
ticketIDs = append(ticketIDs, id)
}
ticketRows.Close()
if len(ticketIDs) > 0 {
holders := strings.TrimSuffix(strings.Repeat("?,", len(ticketIDs)), ",")
args := make([]interface{}, 0, len(ticketIDs))
for _, id := range ticketIDs {
args = append(args, id)
}
if res, err := tx.ExecContext(ctx, "DELETE FROM ticket_follow WHERE ticket_id IN ("+holders+")", args...); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("ticket_follow=%d", rowsAffected(res))) }
}
if res, err := tx.ExecContext(ctx, `DELETE FROM ticket WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("ticket=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM apple_iap_transactions WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("apple_iap_transactions=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM log_message WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("log_message=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user WHERE id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user=%d", rowsAffected(res))) }
if err := tx.Commit(); err != nil {
return result, err
}
redisKeys, err := cleanupRedis(ctx, rdb, summary)
if err != nil {
return result, err
}
result.DeletedRedisKeys = len(redisKeys)
sort.Strings(result.DeletedDBRows)
return result, nil
}
func cleanupRedis(ctx context.Context, rdb *redis.Client, summary userSummary) ([]string, error) {
keySet := map[string]struct{}{
fmt.Sprintf("cache:user:id:%d", summary.User.ID): {},
fmt.Sprintf("cache:user:subscribe:user:%d", summary.User.ID): {},
fmt.Sprintf("cache:user:subscribe:user:%d:all", summary.User.ID): {},
fmt.Sprintf("auth:user_sessions:%d", summary.User.ID): {},
}
for _, am := range summary.AuthMethods {
if am.AuthType == "email" && am.Identifier != "" {
keySet[fmt.Sprintf("cache:user:email:%s", am.Identifier)] = struct{}{}
}
}
for _, sub := range summary.Subscriptions {
keySet[fmt.Sprintf("cache:user:subscribe:id:%d", sub.ID)] = struct{}{}
if sub.Token != "" {
keySet[fmt.Sprintf("cache:user:subscribe:token:%s", sub.Token)] = struct{}{}
}
}
for _, device := range summary.Devices {
keySet[fmt.Sprintf("cache:user:device:id:%d", device.ID)] = struct{}{}
if device.Identifier != "" {
keySet[fmt.Sprintf("cache:user:device:number:%s", device.Identifier)] = struct{}{}
keySet[fmt.Sprintf("auth:device_identifier:%s", device.Identifier)] = struct{}{}
}
}
sessionsKey := fmt.Sprintf("auth:user_sessions:%d", summary.User.ID)
sessionIDs, err := rdb.ZRange(ctx, sessionsKey, 0, -1).Result()
if err != nil && err != redis.Nil {
return nil, err
}
for _, sessionID := range sessionIDs {
if sessionID == "" {
continue
}
keySet[fmt.Sprintf("auth:session_id:%s", sessionID)] = struct{}{}
keySet[fmt.Sprintf("auth:session_id:detail:%s", sessionID)] = struct{}{}
}
var cursor uint64
for {
keys, nextCursor, scanErr := rdb.Scan(ctx, cursor, "auth:session_id:*", 200).Result()
if scanErr != nil {
return nil, scanErr
}
for _, key := range keys {
if strings.Contains(key, ":detail:") {
continue
}
value, getErr := rdb.Get(ctx, key).Result()
if getErr != nil {
continue
}
if value == fmt.Sprintf("%d", summary.User.ID) {
keySet[key] = struct{}{}
sessionID := strings.TrimPrefix(key, "auth:session_id:")
if sessionID != "" {
keySet[fmt.Sprintf("auth:session_id:detail:%s", sessionID)] = struct{}{}
}
}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
keys := make([]string, 0, len(keySet))
for key := range keySet {
keys = append(keys, key)
}
sort.Strings(keys)
if len(keys) == 0 {
return keys, nil
}
if err := rdb.Del(ctx, keys...).Err(); err != nil {
return nil, err
}
return keys, nil
}
func rowsAffected(res sql.Result) int64 {
if res == nil {
return 0
}
n, err := res.RowsAffected()
if err != nil {
return 0
}
return n
}
-3
View File
@@ -1,3 +0,0 @@
{
"extends": ["@commitlint/config-conventional"]
}
+2 -2
View File
@@ -49,12 +49,12 @@ jobs:
if [ "${{ github.ref_name }}" = "main" ]; then
echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/hifast" >> $GITHUB_ENV
echo "为 main 分支设置生产环境变量"
elif [ "${{ github.ref_name }}" = "internal" ]; then
echo "DOCKER_TAG_SUFFIX=internal" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server-internal" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/hifast" >> $GITHUB_ENV
echo "为 internal 分支设置开发环境变量"
else
echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV
@@ -1,12 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="go build github.com/perfect-panel/server" type="GoApplicationRunConfiguration" factoryName="Go Application" nameIsGenerated="true">
<module name="server" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="run --config etc/ppanel-dev.yaml" />
<kind value="PACKAGE" />
<package value="github.com/perfect-panel/server" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/ppanel.go" />
<method v="2" />
</configuration>
</component>
-145
View File
@@ -1,145 +0,0 @@
# ppanel-server
> Multi-agent orchestration framework for agentic coding
## Project Overview
A Claude Flow powered project
**Tech Stack**: TypeScript, Node.js
**Architecture**: Domain-Driven Design with bounded contexts
## Quick Start
### Installation
```bash
npm install
```
### Build
```bash
npm run build
```
### Test
```bash
npm test
```
### Development
```bash
npm run dev
```
## Agent Coordination
### Swarm Configuration
This project uses hierarchical swarm coordination for complex tasks:
| Setting | Value | Purpose |
|---------|-------|---------|
| Topology | `hierarchical` | Queen-led coordination (anti-drift) |
| Max Agents | 8 | Optimal team size |
| Strategy | `specialized` | Clear role boundaries |
| Consensus | `raft` | Leader-based consistency |
### When to Use Swarms
**Invoke swarm for:**
- Multi-file changes (3+ files)
- New feature implementation
- Cross-module refactoring
- API changes with tests
- Security-related changes
- Performance optimization
**Skip swarm for:**
- Single file edits
- Simple bug fixes (1-2 lines)
- Documentation updates
- Configuration changes
### Available Skills
Use `$skill-name` syntax to invoke:
| Skill | Use Case |
|-------|----------|
| `$swarm-orchestration` | Multi-agent task coordination |
| `$memory-management` | Pattern storage and retrieval |
| `$sparc-methodology` | Structured development workflow |
| `$security-audit` | Security scanning and CVE detection |
### Agent Types
| Type | Role | Use Case |
|------|------|----------|
| `researcher` | Requirements analysis | Understanding scope |
| `architect` | System design | Planning structure |
| `coder` | Implementation | Writing code |
| `tester` | Test creation | Quality assurance |
| `reviewer` | Code review | Security and quality |
## Code Standards
### File Organization
- **NEVER** save to root folder
- `/src` - Source code files
- `/tests` - Test files
- `/docs` - Documentation
- `/config` - Configuration files
### Quality Rules
- Files under 500 lines
- No hardcoded secrets
- Input validation at boundaries
- Typed interfaces for public APIs
- TDD London School (mock-first) preferred
### Commit Messages
```
<type>(<scope>): <description>
[optional body]
Co-Authored-By: claude-flow <ruv@ruv.net>
```
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`
## Security
### Critical Rules
- NEVER commit secrets, credentials, or .env files
- NEVER hardcode API keys
- Always validate user input
- Use parameterized queries for SQL
- Sanitize output to prevent XSS
### Path Security
- Validate all file paths
- Prevent directory traversal (../)
- Use absolute paths internally
## Memory System
### Storing Patterns
```bash
npx @claude-flow/cli memory store \
--key "pattern-name" \
--value "pattern description" \
--namespace patterns
```
### Searching Memory
```bash
npx @claude-flow/cli memory search \
--query "search terms" \
--namespace patterns
```
## Links
- Documentation: https://github.com/ruvnet/claude-flow
- Issues: https://github.com/ruvnet/claude-flow/issues
+1 -1
View File
@@ -28,7 +28,7 @@ FROM scratch
# Copy CA certificates and timezone data
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
ENV TZ=Asia/Shanghai
+9026
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -154,6 +154,7 @@ type (
UserAgent string `json:"user_agent" validate:"required"`
CfToken string `json:"cf_token,optional"`
ShortCode string `json:"short_code,optional"`
BasePayload string `json:"base_payload,optional"`
}
GenerateCaptchaResponse {
Id string `json:"id"`
+41
View File
@@ -0,0 +1,41 @@
syntax = "v1"
info (
title: "recovery API"
desc: "API for order recovery"
author: "Tension"
email: "tension@ppanel.com"
version: "0.0.1"
)
import "../types.api"
type (
RecoverySendCodeRequest {
Email string `json:"email" validate:"required,email"`
}
RecoverOrderRequest {
OrderNo string `json:"order_no" validate:"required"`
Email string `json:"email" validate:"required,email"`
Code string `json:"code" validate:"required"`
}
RecoverOrderResponse {
Success bool `json:"success"`
Message string `json:"message"`
}
)
@server (
prefix: v1/public/recovery
group: public/recovery
middleware: DeviceMiddleware
)
service ppanel {
@doc "Send recovery verification code"
@handler SendCode
post /send_code (RecoverySendCodeRequest) returns (SendCodeResponse)
@doc "Recover order subscription"
@handler RecoverOrder
post /order (RecoverOrderRequest) returns (RecoverOrderResponse)
}
+1
View File
@@ -227,6 +227,7 @@ type (
SubscribeDiscount {
Quantity int64 `json:"quantity"`
Discount float64 `json:"discount"`
MapApple string `json:"map_apple"`
}
TrafficLimit {
StatType string `json:"stat_type"`
+152
View File
@@ -0,0 +1,152 @@
# MySQL 8.0 master/replica compose for two separate servers.
#
# Master server:
# COMPOSE_PROFILES=master docker compose -f config/docker-compose.mysql-replication.yml up -d
#
# Replica server:
# MASTER_HOST=<master_public_or_private_ip> COMPOSE_PROFILES=replica docker compose -f config/docker-compose.mysql-replication.yml up -d
#
# Required env on both servers:
# MYSQL_ROOT_PASSWORD=<strong-root-password>
# MYSQL_REPLICATION_PASSWORD=<strong-replication-password>
#
# Optional env:
# MYSQL_DATABASE=ppanel
# MYSQL_REPLICATION_USER=repl
# MYSQL_MASTER_PORT=3306
# MYSQL_REPLICA_PORT=3306
# MYSQL_SERVER_ID=1 # master default
# MYSQL_REPLICA_ID=2 # replica default
#
# If the master already has data, import a GTID-aware dump into the replica
# before starting replication. Fresh empty deployments can start master first,
# then replica, then point the application at the master.
services:
mysql-master:
image: mysql:8.0
container_name: ppanel-mysql-master
profiles:
- master
restart: always
ports:
- "${MYSQL_MASTER_PORT:-3306}:3306"
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?please set MYSQL_ROOT_PASSWORD}"
MYSQL_DATABASE: "${MYSQL_DATABASE:-ppanel}"
MYSQL_REPLICATION_USER: "${MYSQL_REPLICATION_USER:-repl}"
MYSQL_REPLICATION_PASSWORD: "${MYSQL_REPLICATION_PASSWORD:?please set MYSQL_REPLICATION_PASSWORD}"
TZ: Asia/Shanghai
command:
- --default-authentication-plugin=mysql_native_password
- --server-id=${MYSQL_SERVER_ID:-1}
- --log-bin=mysql-bin
- --binlog-format=ROW
- --gtid-mode=ON
- --enforce-gtid-consistency=ON
- --log-replica-updates=ON
- --binlog-expire-logs-seconds=604800
- --max_connections=1000
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- mysql_master_data:/var/lib/mysql
configs:
- source: mysql_master_init
target: /docker-entrypoint-initdb.d/01-create-replication-user.sh
mode: 0755
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
mysql-replica:
image: mysql:8.0
container_name: ppanel-mysql-replica
profiles:
- replica
restart: always
ports:
- "${MYSQL_REPLICA_PORT:-3306}:3306"
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?please set MYSQL_ROOT_PASSWORD}"
MYSQL_DATABASE: "${MYSQL_DATABASE:-ppanel}"
TZ: Asia/Shanghai
command:
- --default-authentication-plugin=mysql_native_password
- --server-id=${MYSQL_REPLICA_ID:-2}
- --relay-log=mysql-relay-bin
- --read-only=ON
- --super-read-only=ON
- --gtid-mode=ON
- --enforce-gtid-consistency=ON
- --log-replica-updates=ON
- --binlog-format=ROW
- --max_connections=1000
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- mysql_replica_data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
mysql-replica-init:
image: mysql:8.0
container_name: ppanel-mysql-replica-init
profiles:
- replica
restart: "no"
depends_on:
mysql-replica:
condition: service_healthy
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?please set MYSQL_ROOT_PASSWORD}"
MYSQL_REPLICATION_USER: "${MYSQL_REPLICATION_USER:-repl}"
MYSQL_REPLICATION_PASSWORD: "${MYSQL_REPLICATION_PASSWORD:?please set MYSQL_REPLICATION_PASSWORD}"
MASTER_HOST: "${MASTER_HOST:?please set MASTER_HOST to the master server ip or hostname}"
MASTER_PORT: "${MASTER_PORT:-3306}"
entrypoint:
- /bin/sh
- -ec
- |
mysql -hmysql-replica -uroot -p"$${MYSQL_ROOT_PASSWORD}" <<SQL
STOP REPLICA;
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='$${MASTER_HOST}',
SOURCE_PORT=$${MASTER_PORT},
SOURCE_USER='$${MYSQL_REPLICATION_USER}',
SOURCE_PASSWORD='$${MYSQL_REPLICATION_PASSWORD}',
SOURCE_AUTO_POSITION=1,
GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;
SQL
configs:
mysql_master_init:
content: |
#!/bin/sh
set -eu
mysql -uroot -p"$${MYSQL_ROOT_PASSWORD}" <<SQL
CREATE USER IF NOT EXISTS '$${MYSQL_REPLICATION_USER}'@'%' IDENTIFIED WITH mysql_native_password BY '$${MYSQL_REPLICATION_PASSWORD}';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO '$${MYSQL_REPLICATION_USER}'@'%';
FLUSH PRIVILEGES;
SQL
volumes:
mysql_master_data:
mysql_replica_data:
+862
View File
@@ -0,0 +1,862 @@
# 邀请赠送与购买订阅逻辑说明
本文档说明当前代码中的购买订阅、订单激活、邀请佣金、邀请赠送时间、家庭组归属逻辑。重点覆盖每个主要分支,方便排查“重复订单/重复订阅/邀请未赠时/赠时落点错误”等问题。
涉及核心文件:
- `internal/logic/public/order/purchaseLogic.go`
- `queue/logic/order/activateOrderLogic.go`
- `internal/logic/common/familyEntitlement.go`
- `internal/model/user/model.go`
## 1. 核心概念
### 1.1 订单状态
| 状态 | 含义 |
| --- | --- |
| `1` | pending,已创建未支付 |
| `2` | paid,已支付待激活 |
| `3` | close,已关闭 |
| `4` | failed/claimed,代码里同时用于失败和 worker 临时领取 |
| `5` | finished,激活完成 |
### 1.2 订单类型
| 类型 | 含义 |
| --- | --- |
| `1` | 新购套餐 |
| `2` | 续费/换套餐 |
| `3` | 重置流量 |
| `4` | 余额充值 |
| `5` | 兑换码激活 |
### 1.3 用户 ID 与订阅归属
订单有两个重要用户字段:
| 字段 | 含义 |
| --- | --- |
| `user_id` | 发起订单/付款的用户 |
| `subscription_user_id` | 订阅权益归属用户;`0` 表示同 `user_id` |
家庭组规则:
- 普通用户下单:`subscription_user_id = user_id`
- 家庭组成员下单:`subscription_user_id = 家主用户 ID`
- 家庭组主账号下单:`subscription_user_id = 家主用户 ID`
当前代码使用 `ResolveEntitlementUser` 判断家庭归属:
- 只有有效家庭组、有效成员关系、角色为 member 时,权益归家主。
- 家主本人不会被改写到别人名下。
## 2. 购买订阅下单逻辑
入口:`Purchase(req *types.PurchaseOrderRequest)`
这里只是创建订单和安排关闭任务,不直接发放订阅。真正发放订阅在订单支付后由队列激活处理。
### 2.1 登录用户检查
分支:
- 上下文没有当前用户:返回 `InvalidAccess`
- 当前用户存在:继续。
### 2.2 解析订阅权益归属
调用 `ResolveEntitlementUser`
| 场景 | `effective_user_id` | 结果 |
| --- | --- | --- |
| 普通用户 | 本人 ID | 订阅归本人 |
| 家庭组主账号 | 本人 ID | 订阅归本人 |
| 家庭组成员 | 家主 ID | 订阅归家主 |
后续查询已有订阅、quota、创建订单里的 `subscription_user_id` 都会使用这个归属结果。
### 2.3 数量校验
分支:
- `quantity <= 0`:自动改成 `1`
- `quantity > MaxQuantity`:返回参数错误。
- 合法:继续。
### 2.4 单订阅模式路由
先默认:
```text
order_type = 1
target_subscribe_id = req.subscribe_id
parent_order_id = 0
subscribe_token = ""
```
如果开启 `Subscribe.SingleModel`
| 查询结果 | 行为 |
| --- | --- |
| 找到已有 anchor 订阅 | 下单路由为续费:`order_type=2`,保留新请求套餐 ID,设置 parent/order token |
| 没找到已有订阅 | 保持新购:`order_type=1` |
| 查询异常 | 返回数据库错误 |
说明:即使是换套餐,只要单订阅模式已有订阅,也走续费语义,后续激活会更新套餐 ID 和流量配置。
### 2.5 非 SingleModel 的全局单订阅兜底
如果未开启 `SingleModel`,且当前还是新购 `order_type=1`
- 查询 `effective_user_id` 名下已有付费订阅:
```sql
user_id = effective_user_id
AND token != ''
AND (order_id > 0 OR token LIKE 'iap:%')
```
| 查询结果 | 行为 |
| --- | --- |
| 找到已有订阅 | 路由为续费:`order_type=2`,用已有订阅 token |
| 没找到 | 仍然新购 |
目的:避免同一个权益归属用户购买不同套餐后出现多条订阅权益。
### 2.6 pending 订单处理
当前只有一个分支会主动关闭旧 pending 单:
```text
SingleModel = true
AND order_type = 1
AND 存在同 user_id + subscribe_id + status=1 的订单
```
行为:
- 关闭旧 pending 订单。
- 继续创建新订单。
注意:
- 如果订单已被路由为 `order_type=2`,这里不会关闭旧 pending。
-`SingleModel` 下也不会走这段 pending 关闭逻辑。
### 2.7 套餐校验
分支:
| 条件 | 行为 |
| --- | --- |
| 套餐不存在 | 返回数据库错误 |
| `sell=false` | 返回套餐不可售 |
| 新购且库存为 `0` | 返回库存不足 |
| 续费/换套餐 | 不检查库存为 0 的拦截分支 |
### 2.8 新用户优惠与新用户限定
调用 `resolveNewUserDiscountEligibility`
| 分支 | 行为 |
| --- | --- |
| 解析失败 | 返回错误 |
| 有折扣且符合条件 | 按折扣计算金额 |
| 有折扣但不符合条件 | 按原价 |
| 新用户限定且不是新用户窗口 | 新购事务内再次校验,不通过则失败 |
### 2.9 优惠券逻辑
如果 `req.coupon` 为空:跳过。
如果不为空:
| 校验 | 不通过行为 |
| --- | --- |
| 优惠券存在 | 返回 `CouponNotExist` |
| 总使用次数未超限 | 返回使用次数不足 |
| 用户使用次数未超限 | 返回用户次数不足 |
| 套餐适用 | 返回不适用 |
通过后计算 `coupon_discount`,从订单金额中扣除。
### 2.10 支付手续费与礼品余额抵扣
流程:
1. 找支付方式。
2. 如果金额大于 0,计算手续费并加到订单金额。
3. 如果用户 `gift_amount > 0`,继续抵扣订单金额。
4. 抵扣金额记录到订单 `gift_amount`
事务内如果有礼品余额抵扣:
- 扣减用户 `gift_amount`
-`system_logs` 的 gift reduce 日志。
### 2.11 `is_new` 首单标记
创建订单前调用:
```sql
SELECT COUNT(*)
FROM `order`
WHERE user_id =
AND status IN (2, 5)
```
| 结果 | `is_new` |
| --- | --- |
| count = 0 | `true` |
| count > 0 | `false` |
注意:
- 判断口径是付款用户 `user_id`,不是 `subscription_user_id`
- pending/closed 订单不影响 `is_new`
- 续费订单也可能是 `is_new=true`,例如单订阅模式下首次购买被路由为续费。
### 2.12 事务内创建订单
事务内执行:
1. 新购且套餐有 quota 时,再查一次 `effective_user_id` 名下订阅数量防并发。
2. 新购且新用户限定时,再查一次新用户资格。
3. 如有礼品余额抵扣,扣减余额并写日志。
4. 新购且库存不是 `-1` 时扣库存。
5. 插入订单。
订单核心字段:
| 字段 | 值 |
| --- | --- |
| `user_id` | 当前付款用户 |
| `subscription_user_id` | 权益归属用户 |
| `type` | 新购 `1` 或续费 `2` |
| `subscribe_id` | 本次购买的套餐 ID |
| `subscribe_token` | 续费时已有订阅 token |
| `is_new` | 首单标记 |
| `status` | `1` pending |
### 2.13 延迟关单任务
订单创建成功后,发送 `DeferCloseOrder` 任务:
- 延迟时间:15 分钟。
- 作用:未支付订单自动关闭。
## 3. 订单支付后的激活逻辑
入口:`ActivateOrderLogic.ProcessTask`
### 3.1 任务解析和订单领取
分支:
| 分支 | 行为 |
| --- | --- |
| payload 解析失败 | 记录错误,不重试 |
| 订单不存在 | 返回错误,允许重试 |
| 订单已 finished | 幂等跳过 |
| 订单不是 paid | 跳过 |
| paid 订单 | 原子更新为 claimed 状态后处理 |
### 3.2 按订单类型分发
| 订单类型 | 处理函数 |
| --- | --- |
| 新购 `1` | `NewPurchase` |
| 续费 `2` | `Renewal` |
| 重置流量 `3` | `ResetTraffic` |
| 充值 `4` | `Recharge` |
| 兑换码 `5` | `RedemptionActivate` |
处理成功后:
1. 执行订阅合并兜底 `reconcilePostOrderSubscriptions`
2. 更新优惠券使用次数。
3. 更新订单为 `finished`
如果处理失败:
- 把订单从 claimed 释放回 paid。
- 返回错误给队列重试。
## 4. 新购订单激活与订阅发放
入口:`NewPurchase`
### 4.1 获取用户
分支:
| 订单 user_id | 行为 |
| --- | --- |
| 不为 0 | 查询已有用户 |
| 为 0 | 从 Redis 临时订单创建游客用户 |
游客订单创建用户时:
- 创建用户和 auth method。
- 生成 refer code。
- 把订单 `user_id` 更新为新用户 ID。
- 如果临时订单有邀请码,绑定 `referer_id`
### 4.2 新用户限定激活时复查
如果套餐是新用户限定,激活时再次校验:
- 不符合则激活失败,订单回到 paid 等待重试/处理。
- 符合继续。
### 4.3 SingleModel 下复用 anchor 订阅
如果 `Subscribe.SingleModel=true`
| 分支 | 行为 |
| --- | --- |
| 找到 anchor 订阅 | 更新订单 parent_id;用续费逻辑延长/换套餐 |
| 找不到 | 继续后续分支 |
| 查询异常 | 记录错误,继续后续分支 |
家庭组场景下查 anchor 的用户 ID:
- 优先 `subscription_user_id`
- 没有则用 `user_id`
### 4.4 复用赠送订阅
如果还没有可复用订阅:
- 查找权益归属用户名下 `order_id=0` 的赠送订阅。
- 找到后将它升级为付费订阅:
- `order_id` 改为当前订单 ID。
- 延长到期时间。
- 状态改为 active。
- 如果套餐变更,更新套餐 ID、流量额度,并清空已用流量。
### 4.5 兜底复用已有订阅
如果仍未复用到订阅:
- 候选用户 ID
- `order.user_id`
- 如果 `subscription_user_id` 存在且不同,也加入候选。
- 查找这些用户名下 `token != ''` 的订阅。
找到后:
- 如果订阅 owner 不是当前 `subscription_user_id`,先把 `user_id` 修正为权益归属用户。
- 用续费逻辑延长/换套餐。
目的:家庭组绑定前后 owner 变化时,也尽量复用旧记录,避免创建重复订阅。
### 4.6 创建新订阅
如果以上都没有复用成功,才创建新 `user_subscribe`
| 字段 | 值 |
| --- | --- |
| `user_id` | `subscription_user_id`,没有则 `order.user_id` |
| `order_id` | 当前订单 ID |
| `subscribe_id` | 当前订单套餐 ID |
| `start_time` | 当前时间 |
| `expire_time` | 按套餐时间单位和数量计算 |
| `traffic` | 套餐流量 |
| `token` | 基于订单号生成 |
| `uuid` | 新 UUID |
| `status` | `1` active |
创建前如果套餐有 quota,会再按订阅 owner 统计数量。
### 4.7 新购激活后的异步逻辑
订阅发放后:
1. 后台触发用户分组重算。
2. 后台异步处理邀请佣金和赠送时间。
3. 清套餐缓存。
注意:邀请逻辑在 goroutine 中执行,不阻塞订单激活。
## 5. 续费/换套餐激活逻辑
入口:`Renewal`
### 5.1 获取用户和订阅
- 查询订单 `user_id` 对应用户。
- 通过 `subscribe_token` 查订阅。
- 查询订单 `subscribe_id` 对应套餐。
### 5.2 Apple IAP 与普通续费分支
| 分支 | 行为 |
| --- | --- |
| `iap_expire_at > 0` | 使用 IAP 到期时间兜底,但仍按累计加时语义 |
| 普通续费 | `updateSubscriptionForRenewal` |
### 5.3 普通续费/换套餐规则
`updateSubscriptionForRenewal`
- 如果当前订阅已过期,先把基准时间改为现在。
- 如果套餐 ID 变化:
- 更新订阅套餐 ID。
- 更新流量额度。
- 清空已用流量。
- 如果套餐没变:
- 如果套餐设置 renewal reset,或今天是重置日,则清空已用流量。
- 清理 `finished_at`
- `order_id` 改为当前订单 ID。
- 按套餐时间单位和数量延长到期时间。
- 状态改为 active。
- 清空过期流量字段。
### 5.4 续费后的邀请逻辑
续费成功后也会调用 `handleCommission`
- 是否发佣金由邀请配置和 `order.is_new` 决定。
- 是否赠时同样由邀请配置和 `order.is_new` 决定。
注意:如果 `OnlyFirstPurchase=true`,非首单续费通常不会发佣金,也不会赠首单时间。
## 6. 邀请关系绑定逻辑
### 6.1 注册/登录时的邀请码
新用户注册、游客订单创建用户时,如果带邀请码:
- 根据邀请码查邀请人。
- 设置新用户 `referer_id = 邀请人 ID`
### 6.2 用户后绑邀请码
入口:`BindInviteCode`
分支:
| 分支 | 行为 |
| --- | --- |
| 当前用户不存在 | 返回无权限 |
| 当前用户已有 `referer_id` | 返回已绑定 |
| 邀请码不存在 | 返回邀请码错误 |
| 邀请码属于自己 | 返回不允许绑定自己 |
| 通过 | 更新当前用户 `referer_id` |
注意:
- `referer_id` 始终记录实际邀请码所有者。
- 邀请人是家庭成员时,`referer_id` 仍然是该成员 ID,不自动改为家主 ID。
## 7. 邀请佣金与赠送时间逻辑
入口:`handleCommission(userInfo, orderInfo)`
这里的 `userInfo` 是订单付款用户,也就是被邀请人。
### 7.1 总入口分支
先调用 `shouldProcessCommission(userInfo, orderInfo.IsNew)`
| 结果 | 行为 |
| --- | --- |
| `false` | 不发佣金;如果 `is_new=true`,走双方赠时 |
| `true` | 发佣金;如果 `is_new=true`,被邀请人赠时 |
### 7.2 什么时候发佣金
`shouldProcessCommission` 规则:
| 条件 | 结果 |
| --- | --- |
| 被邀请人为空 | 不发 |
| 被邀请人 `referer_id=0` | 不发 |
| 查不到邀请人 | 不发 |
| 邀请人自定义 `referral_percentage > 0`,且只首购但不是首单 | 不发 |
| 邀请人自定义 `referral_percentage > 0`,且通过首购限制 | 发佣金 |
| 邀请人无自定义比例,系统 `ReferralPercentage=0` | 不发 |
| 系统 `OnlyFirstPurchase=true` 且不是首单 | 不发 |
| 系统有比例且通过首购限制 | 发佣金 |
### 7.3 发佣金路径
如果 `shouldProcessCommission=true`
1. 查询邀请人,也就是 `userInfo.referer_id` 对应用户。
2. 佣金比例:
- 邀请人自定义比例优先。
- 否则用系统配置 `Invite.ReferralPercentage`
3. 佣金金额:
```text
(order.amount - order.fee_amount) * referral_percentage / 100
```
4. 事务内幂等检查:
- 如果已有同订单佣金日志,则跳过。
- 否则增加邀请人的 `commission`
-`system_logs type=33` 佣金日志。
5. 更新邀请人缓存。
6. 如果 `order.is_new=true`
- 给被邀请人赠送订阅时间。
当前保持不变的行为:
- 邀请人是家庭成员时,佣金仍然给实际邀请人成员本人。
- 佣金不归并到家主。
- 有佣金路径下,邀请人不额外赠送订阅时间。
### 7.4 不发佣金路径
如果 `shouldProcessCommission=false`
| `order.is_new` | 行为 |
| --- | --- |
| `true` | 被邀请人和邀请人双方赠送订阅时间 |
| `false` | 不赠送时间 |
双方赠时具体为:
1. 被邀请人赠时:
- 如果被邀请人是家庭成员,加到被邀请人家主套餐。
- 否则加到被邀请人本人套餐。
2. 邀请人赠时:
- 如果邀请人是家庭成员,加到邀请人家主套餐。
- 否则加到邀请人本人套餐。
## 8. 赠送时间目标解析
入口:`resolveGiftTargetUser(source, forcedOwnerID)`
### 8.1 强制 owner 分支
如果 `forcedOwnerID > 0`
- 赠送目标直接使用 `forcedOwnerID`
- 典型场景:订单里已有 `subscription_user_id`
- 这保证了家庭成员购买时,被邀请人的赠时落到家主。
### 8.2 自动家庭组解析分支
如果没有强制 owner
- 调用 `ResolveEntitlementUser(source.Id)`
- 如果 source 是有效家庭成员,目标改为家主。
- 否则目标为本人。
典型场景:
- 无佣金路径下,邀请人也赠时。
- 邀请人如果是家庭成员,赠时会加到邀请人家主套餐。
### 8.3 目标用户查询失败
如果解析出来的目标用户查不到:
- 记录错误日志。
- 回退为 source 本人。
## 9. 赠送时间落库逻辑
入口:`grantGiftDays(u, days, orderNo, remark)`
### 9.1 空值和配置分支
| 条件 | 行为 |
| --- | --- |
| 目标用户为空 | 直接返回,不写日志 |
| `days <= 0` | 直接返回,不写日志 |
### 9.2 幂等检查
按下面条件查 gift 日志:
```sql
type = 34
AND object_id = ID
AND content LIKE '%订单号%'
```
| 结果 | 行为 |
| --- | --- |
| 已存在 | 跳过,不重复赠时 |
| 不存在 | 继续 |
### 9.3 查目标用户活跃订阅
调用 `FindActiveSubscribe`
当前活跃口径:
```sql
user_id = ID
AND status IN (0, 1)
AND (
expire_time > NOW()
OR expire_time = FROM_UNIXTIME(0)
)
```
说明:
- `expire_time > NOW()` 是普通未过期订阅。
- `expire_time = FROM_UNIXTIME(0)` 是永久/不限时订阅。
### 9.4 没有活跃订阅
如果查不到活跃订阅:
- 不创建新订阅。
- 写一条 `system_logs type=34` 日志。
- 日志 remark 为:
```text
邀请赠送 skipped: no active subscription
```
这表示邀请赠时触发过,但目标用户当时没有可加时的套餐。
### 9.5 找到普通活跃订阅
如果目标订阅不是永久订阅:
- `expire_time += days * 24h`
- 更新订阅。
-`system_logs type=34` gift increase 日志。
### 9.6 找到永久订阅
如果目标订阅 `expire_time = FROM_UNIXTIME(0)`
- 不改变 `expire_time`,因为永久订阅没有可延长的到期时间。
- 仍写 `system_logs type=34` gift increase 日志,表示赠送逻辑已识别并处理。
### 9.7 赠时失败日志
发佣金路径和无佣金路径都会检查 `grantGiftDays` 返回错误。
如果出错,会写应用日志:
```text
Grant invite gift days failed
```
附带字段:
- `stage`
- `target_user_id`
- `order_no`
- `error`
## 10. 家庭组下的完整分支示例
### 10.1 被邀请人是普通用户,邀请人普通用户,有佣金
条件:
- 被邀请人 `referer_id != 0`
- 系统或邀请人佣金比例大于 0
- `order.is_new=true`
结果:
- 佣金给邀请人本人。
- 被邀请人本人套餐加赠送时间。
- 邀请人不加赠送时间。
### 10.2 被邀请人是家庭成员,邀请人普通用户,有佣金
结果:
- 佣金给邀请人本人。
- 被邀请人的赠送时间加到被邀请人家主套餐。
- 被邀请人成员本人不单独加订阅时间。
### 10.3 被邀请人普通用户,邀请人是家庭成员,有佣金
结果:
- 佣金给邀请人成员本人。
- 被邀请人本人套餐加赠送时间。
- 邀请人不加赠送时间。
- 邀请人家主不拿佣金,也不因该佣金路径加赠时。
### 10.4 被邀请人是家庭成员,邀请人也是家庭成员,有佣金
结果:
- 佣金给邀请人成员本人。
- 被邀请人的赠送时间加到被邀请人家主套餐。
- 邀请人不加赠送时间。
- 邀请人家主不拿佣金。
### 10.5 无佣金路径,被邀请人普通用户,邀请人普通用户
触发条件示例:
- `ReferralPercentage=0`
- 或因首购限制导致不发佣金
-`order.is_new=true`
结果:
- 被邀请人本人套餐加赠送时间。
- 邀请人本人套餐加赠送时间。
### 10.6 无佣金路径,被邀请人是家庭成员
结果:
- 被邀请人的赠送时间加到被邀请人家主套餐。
- 邀请人的赠时按邀请人自己的家庭归属解析。
### 10.7 无佣金路径,邀请人是家庭成员
结果:
- 被邀请人的赠时按被邀请人的家庭归属解析。
- 邀请人的赠送时间加到邀请人家主套餐。
- 邀请人成员本人不单独加订阅时间。
### 10.8 被邀请人没有活跃订阅
结果:
- 不创建新订阅。
- 写 skipped gift 日志。
- 后续即使用户后来有订阅,也不会自动补赠,除非另行补偿。
### 10.9 被邀请人或目标家主是永久订阅
结果:
- 识别为活跃订阅。
- 不改变到期时间。
- 写 gift increase 日志。
## 11. 排查 SQL
### 11.1 查邀请配置
```sql
SELECT `key`, `value`, `updated_at`
FROM system
WHERE category = 'invite'
AND `key` IN ('GiftDays', 'OnlyFirstPurchase', 'ReferralPercentage');
```
### 11.2 查某邀请人的被邀请用户
```sql
SELECT id, referer_id, created_at
FROM `user`
WHERE referer_id = 23944
ORDER BY id DESC
LIMIT 100;
```
### 11.3 查被邀请人的订单和首单标记
```sql
SELECT u.id AS invited_user_id,
o.id AS order_id,
o.order_no,
o.type,
o.status,
o.amount,
o.is_new,
o.subscribe_id,
o.subscription_user_id,
o.created_at
FROM `user` u
LEFT JOIN `order` o
ON o.user_id = u.id
AND o.type IN (1, 2)
WHERE u.referer_id = 23944
ORDER BY u.id DESC, o.id ASC
LIMIT 200;
```
### 11.4 查某订单佣金和赠时日志
```sql
SELECT id, type, object_id, content, created_at
FROM system_logs
WHERE content LIKE '%202604281812556044982351822%'
ORDER BY id DESC;
```
### 11.5 查某用户订阅
```sql
SELECT id, user_id, order_id, subscribe_id, status,
expire_time, finished_at, token, created_at, updated_at
FROM user_subscribe
WHERE user_id = 24425
ORDER BY id DESC;
```
### 11.6 查首单但没有赠时日志的被邀请人
```sql
SELECT first_orders.user_id AS invited_user_id,
first_orders.order_no,
first_orders.is_new,
first_orders.status,
first_orders.subscription_user_id,
first_orders.created_at,
(
SELECT COUNT(*)
FROM system_logs sl
WHERE sl.type = 34
AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')
) AS gift_log_count,
(
SELECT COUNT(*)
FROM system_logs sl
WHERE sl.type = 33
AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')
) AS commission_log_count
FROM (
SELECT o.*
FROM `order` o
JOIN (
SELECT user_id, MIN(id) AS first_order_id
FROM `order`
WHERE type IN (1, 2)
AND status IN (2, 5)
GROUP BY user_id
) fo ON fo.first_order_id = o.id
) first_orders
JOIN `user` u ON u.id = first_orders.user_id
WHERE u.referer_id = 23944
ORDER BY first_orders.created_at DESC
LIMIT 100;
```
## 12. 部署注意事项
邀请配置存在两层状态:
1. Redis 缓存:`system:invite_config`
2. 服务进程内存:`svc.Config.Invite`
如果直接修改数据库或 Redis,已经运行的 `ppanel-server` 进程不会自动刷新内存配置。订单激活和赠时发生在服务进程/队列 worker 内,所以修改邀请配置或部署赠时逻辑后,需要重启服务。
推荐步骤:
```bash
docker exec ppanel-redis redis-cli DEL system:invite_config system:global_config
docker restart ppanel-server
```
确认启动时间:
```bash
docker inspect --format '{{.Name}} {{.State.StartedAt}} {{.Config.Image}}' ppanel-server
docker ps --filter name=ppanel-server
```
+19 -132
View File
@@ -6,9 +6,9 @@
# 4. 运行: docker-compose -f docker-compose.cloud.yml up -d
#
# 网络说明:
# ppanel-server 使用 host 网络(可出外网,访问 MySQL/Redis/Tempo 用 127.0.0.1
# 监控服务(MySQL/Redis/Loki/Tempo/Grafana/Prometheus)在 ppanel_net bridge 网络中
# MySQL(3306)/Redis(6379)/Tempo(4317) 将端口映射到 127.0.0.1ppanel-server 通过 host 网络访问
# ppanel-server 使用 host 网络(可出外网,直接访问 AWS RDS/ElastiCache 私网地址
# 监控服务(Loki/Tempo/Grafana/Prometheus)在 ppanel_net bridge 网络中
# Tempo(4317) 将端口映射到 127.0.0.1ppanel-server 通过 host 网络访问
# 监控端口绑定 127.0.0.1,需通过 SSH 隧道或 Nginx 反代访问
#
# 未来多开 ppanel-server 时:
@@ -18,7 +18,7 @@
services:
# ----------------------------------------------------
# 1. 业务后端 (PPanel Server)
# host 网络:可出外网,通过 127.0.0.1 访问 MySQL/Redis/Tempo
# host 网络:可出外网,直接访问 AWS RDS/Redis通过 127.0.0.1 访问 Tempo
# ----------------------------------------------------
ppanel-server:
image: registry.kxsw.us/vpn-server:${PPANEL_SERVER_TAG:-latest}
@@ -37,10 +37,6 @@ services:
soft: 65535
hard: 65535
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
tempo:
condition: service_started
logging:
@@ -50,81 +46,7 @@ services:
max-file: "3"
# ----------------------------------------------------
# 2. MySQL Database
# ----------------------------------------------------
mysql:
image: mysql:8.0
container_name: ppanel-mysql
restart: always
ports:
- "3306:3306" # 仅宿主机可访问,ppanel-server(host网络)通过127.0.0.1连接
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?请在 .env 文件中设置 MYSQL_ROOT_PASSWORD}"
MYSQL_DATABASE: "ppanel"
TZ: Asia/Shanghai
command:
- --default-authentication-plugin=mysql_native_password
- --innodb_buffer_pool_size=16G
- --innodb_buffer_pool_instances=16
- --innodb_log_file_size=2G
- --innodb_flush_log_at_trx_commit=2
- --innodb_io_capacity=5000
- --max_connections=5000
volumes:
- mysql_data:/var/lib/mysql
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
networks:
- ppanel_net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# ----------------------------------------------------
# 3. Redis
# ----------------------------------------------------
redis:
image: redis:8.2.1
container_name: ppanel-redis
restart: always
ports:
- "127.0.0.1:6379:6379" # 仅宿主机可访问,ppanel-server(host网络)通过127.0.0.1连接
command:
- redis-server
- --tcp-backlog 65535
- --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
networks:
- ppanel_net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# ----------------------------------------------------
# 4. Tempo (链路追踪存储)
# 2. Tempo (链路追踪存储)
# ----------------------------------------------------
tempo:
image: grafana/tempo:2.4.1
@@ -148,7 +70,7 @@ services:
max-file: "3"
# ----------------------------------------------------
# 5. Loki (日志存储)
# 3. Loki (日志存储)
# ----------------------------------------------------
loki:
image: grafana/loki:3.0.0
@@ -168,7 +90,7 @@ services:
max-file: "3"
# ----------------------------------------------------
# 6. Promtail (日志采集)
# 4. Promtail (日志采集)
# ----------------------------------------------------
promtail:
image: grafana/promtail:3.0.0
@@ -192,7 +114,7 @@ services:
max-file: "3"
# ----------------------------------------------------
# 7. Grafana (可观测面板)
# 5. Grafana (可观测面板)
# 访问: ssh -L 3333:localhost:3333 your-server 后浏览器打开 http://localhost:3333
# 或配置 Nginx 反代(建议加认证)
# ----------------------------------------------------
@@ -205,7 +127,14 @@ services:
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:?请在 .env 文件中设置 GRAFANA_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_DOMAIN=${GRAFANA_DOMAIN:-logsx.hifast.biz}
- GF_SERVER_ROOT_URL=${GRAFANA_ROOT_URL:-https://logsx.hifast.biz}
- GF_FEATURE_TOGGLES_ENABLE=appObservability
- AWS_REGION=${AWS_REGION:-ap-east-1}
- AWS_DEFAULT_REGION=${AWS_REGION:-ap-east-1}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-}
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
@@ -222,7 +151,7 @@ services:
max-file: "3"
# ----------------------------------------------------
# 8. Prometheus (指标采集)
# 6. Prometheus (指标采集)
# ----------------------------------------------------
prometheus:
image: prom/prometheus:latest
@@ -247,26 +176,7 @@ services:
max-file: "3"
# ----------------------------------------------------
# 9. Redis Exporter
# ----------------------------------------------------
redis-exporter:
image: oliver006/redis_exporter:latest
container_name: ppanel-redis-exporter
restart: always
environment:
- REDIS_ADDR=redis://redis:6379
networks:
- ppanel_net
depends_on:
- redis
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# ----------------------------------------------------
# 10. Nginx Exporter (监控宿主机 Nginx)
# 7. Nginx Exporter (监控宿主机 Nginx)
# ----------------------------------------------------
nginx-exporter:
image: nginx/nginx-prometheus-exporter:latest
@@ -285,28 +195,7 @@ services:
max-file: "3"
# ----------------------------------------------------
# 11. MySQL Exporter
# ----------------------------------------------------
mysql-exporter:
image: prom/mysqld-exporter:latest
container_name: ppanel-mysql-exporter
restart: always
command:
- --config.my-cnf=/etc/.my.cnf
volumes:
- ./mysql/.my.cnf:/etc/.my.cnf:ro
networks:
- ppanel_net
depends_on:
- mysql
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# ----------------------------------------------------
# 12. Node Exporter (宿主机监控)
# 8. Node Exporter (宿主机监控)
# ----------------------------------------------------
node-exporter:
image: prom/node-exporter:latest
@@ -329,7 +218,7 @@ services:
max-file: "3"
# ----------------------------------------------------
# 13. cAdvisor (容器监控)
# 9. cAdvisor (容器监控)
# ----------------------------------------------------
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
@@ -350,8 +239,6 @@ services:
max-file: "3"
volumes:
mysql_data:
redis_data:
loki_data:
grafana_data:
prometheus_data:
+38
View File
@@ -0,0 +1,38 @@
services:
mysql:
image: mysql:8.0
container_name: ppanel-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ppanel_dev
MYSQL_DATABASE: ppanel
MYSQL_USER: ppanel
MYSQL_PASSWORD: ppanel_dev
ports:
- "3306:3306"
volumes:
- ppanel_mysql_data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pppanel_dev"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: ppanel-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- ppanel_redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
volumes:
ppanel_mysql_data:
ppanel_redis_data:
+1 -1
View File
@@ -15,7 +15,7 @@ Logger: # 日志配置
Level: debug # 日志级别: debug, info, warn, error, panic, fatal
MySQL:
Addr: 103.150.215.44:3306 # host 网络模式; bridge 模式改为 mysql:3306
Addr: 45.43.29.127:3306 # host 网络模式; bridge 模式改为 mysql:3306
Username: root # MySQL用户名
Password: jpcV41ppanel # MySQL密码,与 .env MYSQL_ROOT_PASSWORD 一致
Dbname: hifast # MySQL数据库名
@@ -0,0 +1,272 @@
apiVersion: 1
groups:
- orgId: 1
name: ppanel-core
folder: PPanel
interval: 1m
rules:
- uid: ppanel-target-down
title: PPanel monitoring target down
condition: C
for: 2m
noDataState: Alerting
execErrState: Error
annotations:
summary: "Monitoring target is down"
description: "{{ $labels.job }} on {{ $labels.instance }} has been down for more than 2 minutes."
labels:
severity: critical
service: ppanel
data:
- refId: A
relativeTimeRange:
from: 300
to: 0
datasourceUid: prometheus
model:
datasource:
type: prometheus
uid: prometheus
editorMode: code
expr: 'up{job=~"grafana|prometheus|node-exporter|cadvisor|nginx-exporter|loki|tempo"}'
instant: true
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: C
datasourceUid: __expr__
model:
conditions:
- evaluator:
params:
- 1
type: lt
operator:
type: and
query:
params:
- A
reducer:
type: last
type: query
datasource:
type: __expr__
uid: __expr__
expression: A
intervalMs: 1000
maxDataPoints: 43200
refId: C
type: threshold
- uid: ppanel-host-disk-high
title: PPanel host disk usage high
condition: C
for: 10m
noDataState: NoData
execErrState: Error
annotations:
summary: "Host disk usage is high"
description: "{{ $labels.instance }} {{ $labels.mountpoint }} disk usage is above 85% for 10 minutes."
labels:
severity: warning
service: ppanel
data:
- refId: A
relativeTimeRange:
from: 900
to: 0
datasourceUid: prometheus
model:
datasource:
type: prometheus
uid: prometheus
editorMode: code
expr: '100 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs|aufs",mountpoint!~"/run.*|/var/lib/docker.*"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs|aufs",mountpoint!~"/run.*|/var/lib/docker.*"} * 100)'
instant: true
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: C
datasourceUid: __expr__
model:
conditions:
- evaluator:
params:
- 85
type: gt
operator:
type: and
query:
params:
- A
reducer:
type: last
type: query
datasource:
type: __expr__
uid: __expr__
expression: A
intervalMs: 1000
maxDataPoints: 43200
refId: C
type: threshold
- uid: ppanel-host-memory-high
title: PPanel host memory usage high
condition: C
for: 10m
noDataState: NoData
execErrState: Error
annotations:
summary: "Host memory usage is high"
description: "{{ $labels.instance }} memory usage is above 90% for 10 minutes."
labels:
severity: warning
service: ppanel
data:
- refId: A
relativeTimeRange:
from: 900
to: 0
datasourceUid: prometheus
model:
datasource:
type: prometheus
uid: prometheus
editorMode: code
expr: '(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100'
instant: true
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: C
datasourceUid: __expr__
model:
conditions:
- evaluator:
params:
- 90
type: gt
operator:
type: and
query:
params:
- A
reducer:
type: last
type: query
datasource:
type: __expr__
uid: __expr__
expression: A
intervalMs: 1000
maxDataPoints: 43200
refId: C
type: threshold
- uid: ppanel-host-cpu-high
title: PPanel host CPU usage high
condition: C
for: 10m
noDataState: NoData
execErrState: Error
annotations:
summary: "Host CPU usage is high"
description: "{{ $labels.instance }} CPU usage is above 90% for 10 minutes."
labels:
severity: warning
service: ppanel
data:
- refId: A
relativeTimeRange:
from: 900
to: 0
datasourceUid: prometheus
model:
datasource:
type: prometheus
uid: prometheus
editorMode: code
expr: '100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)'
instant: true
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: C
datasourceUid: __expr__
model:
conditions:
- evaluator:
params:
- 90
type: gt
operator:
type: and
query:
params:
- A
reducer:
type: last
type: query
datasource:
type: __expr__
uid: __expr__
expression: A
intervalMs: 1000
maxDataPoints: 43200
refId: C
type: threshold
- uid: ppanel-container-restarts
title: PPanel container restarted
condition: C
for: 1m
noDataState: NoData
execErrState: Error
annotations:
summary: "Container restarted"
description: "{{ $labels.name }} restarted or changed start time in the last hour."
labels:
severity: warning
service: ppanel
data:
- refId: A
relativeTimeRange:
from: 3600
to: 0
datasourceUid: prometheus
model:
datasource:
type: prometheus
uid: prometheus
editorMode: code
expr: 'sum by (name) (changes(container_start_time_seconds{name!=""}[1h]))'
instant: true
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: C
datasourceUid: __expr__
model:
conditions:
- evaluator:
params:
- 0
type: gt
operator:
type: and
query:
params:
- A
reducer:
type: last
type: query
datasource:
type: __expr__
uid: __expr__
expression: A
intervalMs: 1000
maxDataPoints: 43200
refId: C
type: threshold
@@ -0,0 +1,14 @@
apiVersion: 1
providers:
- name: PPanel
orgId: 1
folder: PPanel
folderUid: ppanel
type: file
disableDeletion: false
allowUiUpdates: true
updateIntervalSeconds: 30
options:
path: /etc/grafana/provisioning/dashboards/json
foldersFromFilesStructure: false
@@ -0,0 +1,520 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"gridPos": {
"h": 3,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"content": "<b>AWS CloudWatch overview</b><br/>Region: ap-east-1 (Hong Kong)<br/>RDS DBInstanceIdentifier: database-1<br/>Redis ReplicationGroupId: hifastapp-redis<br/><br/>Note: Redis panels are configured using <code>ReplicationGroupId</code>. If a panel shows no data in your account, switch that dimension to the concrete <code>CacheClusterId</code> in Grafana query editor.",
"mode": "html"
},
"pluginVersion": "11.0.0",
"title": "Read Me",
"type": "text"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 3
},
"id": 2,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"DBInstanceIdentifier": "database-1"
},
"metricName": "CPUUtilization",
"namespace": "AWS/RDS",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Average"
}
],
"title": "RDS CPU",
"type": "timeseries"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 3
},
"id": 3,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"DBInstanceIdentifier": "database-1"
},
"metricName": "DatabaseConnections",
"namespace": "AWS/RDS",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Average"
}
],
"title": "RDS Connections",
"type": "timeseries"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 3
},
"id": 4,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"DBInstanceIdentifier": "database-1"
},
"metricName": "FreeStorageSpace",
"namespace": "AWS/RDS",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Minimum"
}
],
"title": "RDS Free Storage",
"type": "timeseries"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 11
},
"id": 5,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"DBInstanceIdentifier": "database-1"
},
"metricName": "ReadLatency",
"namespace": "AWS/RDS",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Average"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"DBInstanceIdentifier": "database-1"
},
"metricName": "WriteLatency",
"namespace": "AWS/RDS",
"period": "",
"refId": "B",
"region": "ap-east-1",
"statistic": "Average"
}
],
"title": "RDS Read / Write Latency",
"type": "timeseries"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "iops"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 11
},
"id": 6,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"DBInstanceIdentifier": "database-1"
},
"metricName": "ReadIOPS",
"namespace": "AWS/RDS",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Average"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"DBInstanceIdentifier": "database-1"
},
"metricName": "WriteIOPS",
"namespace": "AWS/RDS",
"period": "",
"refId": "B",
"region": "ap-east-1",
"statistic": "Average"
}
],
"title": "RDS Read / Write IOPS",
"type": "timeseries"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 0,
"y": 19
},
"id": 7,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"ReplicationGroupId": "hifastapp-redis"
},
"metricName": "CPUUtilization",
"namespace": "AWS/ElastiCache",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Average"
}
],
"title": "Redis Host CPU",
"type": "timeseries"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 6,
"y": 19
},
"id": 8,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"ReplicationGroupId": "hifastapp-redis"
},
"metricName": "EngineCPUUtilization",
"namespace": "AWS/ElastiCache",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Average"
}
],
"title": "Redis Engine CPU",
"type": "timeseries"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 12,
"y": 19
},
"id": 9,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"ReplicationGroupId": "hifastapp-redis"
},
"metricName": "CurrConnections",
"namespace": "AWS/ElastiCache",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Average"
}
],
"title": "Redis Connections",
"type": "timeseries"
},
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"fieldConfig": {
"defaults": {
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 18,
"y": 19
},
"id": 10,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "cloudwatch"
},
"dimensions": {
"ReplicationGroupId": "hifastapp-redis"
},
"metricName": "DatabaseMemoryUsagePercentage",
"namespace": "AWS/ElastiCache",
"period": "",
"refId": "A",
"region": "ap-east-1",
"statistic": "Average"
}
],
"title": "Redis Memory Usage %",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"style": "dark",
"tags": [
"aws",
"cloudwatch",
"rds",
"redis"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "AWS RDS & Redis Overview",
"uid": "aws-rds-redis-overview",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,565 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [
{
"options": {
"0": {
"text": "DOWN"
},
"1": {
"text": "UP"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "center",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "13.0.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "up{job=~\"prometheus|grafana|node-exporter|cadvisor|nginx-exporter|loki|tempo\"}",
"instant": true,
"legendFormat": "{{job}}",
"range": false,
"refId": "A"
}
],
"title": "Service Availability",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 75
},
{
"color": "red",
"value": 90
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 4
},
"id": 2,
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
"legendFormat": "{{instance}} CPU",
"range": true,
"refId": "A"
}
],
"title": "Host CPU Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 80
},
{
"color": "red",
"value": 90
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 4
},
"id": 3,
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
"legendFormat": "{{instance}} memory",
"range": true,
"refId": "A"
}
],
"title": "Host Memory Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 80
},
{
"color": "red",
"value": 90
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 4
},
"id": 4,
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "100 - (node_filesystem_avail_bytes{fstype!~\"tmpfs|overlay|squashfs|aufs\",mountpoint!~\"/run.*|/var/lib/docker.*\"} / node_filesystem_size_bytes{fstype!~\"tmpfs|overlay|squashfs|aufs\",mountpoint!~\"/run.*|/var/lib/docker.*\"} * 100)",
"legendFormat": "{{mountpoint}}",
"range": true,
"refId": "A"
}
],
"title": "Host Disk Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "percentunit"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 12
},
"id": 5,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (name) (rate(container_cpu_usage_seconds_total{name!=\"\"}[5m]))",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Container CPU",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 12
},
"id": 6,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (name) (container_memory_working_set_bytes{name!=\"\"})",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Container Memory",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 12
},
"id": 7,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (name) (changes(container_start_time_seconds{name!=\"\"}[1h]))",
"legendFormat": "{{name}}",
"range": true,
"refId": "A"
}
],
"title": "Container Restarts / Changes",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 20
},
"id": 8,
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "rate(nginx_http_requests_total[5m])",
"legendFormat": "requests",
"range": true,
"refId": "A"
}
],
"title": "Nginx Requests",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 20
},
"id": 9,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": true
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "{job=~\"ppanel-server|nginx|docker\"} |~ \"(?i)(error|panic|fatal|timeout|exception|failed)\"",
"queryType": "range",
"refId": "A"
}
],
"title": "Recent Errors",
"type": "logs"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 20
},
"id": 10,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by (service_name) (rate(traces_spanmetrics_calls_total[5m]))",
"legendFormat": "{{service_name}}",
"range": true,
"refId": "A"
}
],
"title": "Trace Span Calls",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 42,
"tags": [
"ppanel",
"ops",
"prometheus",
"loki",
"tempo"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "PPanel Ops Overview",
"uid": "ppanel-ops-overview",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,330 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "loki",
"uid": "P8E80F9AEF21F6940"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P8E80F9AEF21F6940"
},
"editorMode": "code",
"expr": "sum(count_over_time({container=\"ppanel-server\"} |~ \"${user_id:raw}\" |~ \"${email:raw}\" |~ \"${order:raw}\" |~ \"${keyword:raw}\" |~ \"${level:raw}\" [5m]))",
"queryType": "range",
"refId": "A"
}
],
"title": "Matched Log Volume",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "P8E80F9AEF21F6940"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P8E80F9AEF21F6940"
},
"editorMode": "code",
"expr": "sum(count_over_time({container=\"ppanel-server\"} |~ \"${user_id:raw}\" |~ \"${email:raw}\" |~ \"${order:raw}\" |~ \"${keyword:raw}\" |~ \"(?i)(error|panic|fatal)\" [5m]))",
"queryType": "range",
"refId": "A"
}
],
"title": "Matched Error Volume",
"type": "timeseries"
},
{
"datasource": {
"type": "loki",
"uid": "P8E80F9AEF21F6940"
},
"gridPos": {
"h": 12,
"w": 24,
"x": 0,
"y": 7
},
"id": 3,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": true
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P8E80F9AEF21F6940"
},
"editorMode": "code",
"expr": "{container=\"ppanel-server\"} |~ \"${user_id:raw}\" |~ \"${email:raw}\" |~ \"${order:raw}\" |~ \"${keyword:raw}\" |~ \"${level:raw}\"",
"queryType": "range",
"refId": "A"
}
],
"title": "Filtered Server Logs",
"type": "logs"
},
{
"datasource": {
"type": "loki",
"uid": "P8E80F9AEF21F6940"
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 19
},
"id": 4,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": true
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "P8E80F9AEF21F6940"
},
"editorMode": "code",
"expr": "{container=\"ppanel-server\"} |~ \"${user_id:raw}\" |~ \"${email:raw}\" |~ \"${order:raw}\" |~ \"${keyword:raw}\" |~ \"(?i)(error|panic|fatal)\"",
"queryType": "range",
"refId": "A"
}
],
"title": "Filtered Server Errors",
"type": "logs"
}
],
"refresh": "30s",
"schemaVersion": 42,
"tags": [
"ppanel",
"logs",
"server",
"loki"
],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": ".",
"value": "."
},
"description": "输入用户 ID;默认 . 表示不过滤",
"hide": 0,
"label": "用户ID",
"name": "user_id",
"options": [],
"query": ".",
"skipUrlSync": false,
"type": "textbox"
},
{
"current": {
"selected": false,
"text": ".",
"value": "."
},
"description": "输入邮箱或邮箱片段;默认 . 表示不过滤",
"hide": 0,
"label": "邮箱",
"name": "email",
"options": [],
"query": ".",
"skipUrlSync": false,
"type": "textbox"
},
{
"current": {
"selected": false,
"text": ".",
"value": "."
},
"description": "输入订单号/支付单号/交易号片段;默认 . 表示不过滤",
"hide": 0,
"label": "订单",
"name": "order",
"options": [],
"query": ".",
"skipUrlSync": false,
"type": "textbox"
},
{
"current": {
"selected": true,
"text": "All",
"value": "."
},
"description": "日志等级",
"hide": 0,
"includeAll": false,
"label": "等级",
"multi": false,
"name": "level",
"options": [
{
"selected": true,
"text": "All",
"value": "."
},
{
"selected": false,
"text": "debug",
"value": "debug"
},
{
"selected": false,
"text": "info",
"value": "info"
},
{
"selected": false,
"text": "warn",
"value": "warn"
},
{
"selected": false,
"text": "error",
"value": "error"
},
{
"selected": false,
"text": "slow",
"value": "slow"
},
{
"selected": false,
"text": "panic",
"value": "panic"
},
{
"selected": false,
"text": "fatal",
"value": "fatal"
}
],
"query": "All : .,debug,info,warn,error,slow,panic,fatal",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
},
{
"current": {
"selected": false,
"text": ".",
"value": "."
},
"description": "任意关键字;默认 . 表示不过滤",
"hide": 0,
"label": "关键字",
"name": "keyword",
"options": [],
"query": ".",
"skipUrlSync": false,
"type": "textbox"
}
]
},
"time": {
"from": "now-7d",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "PPanel Server Logs",
"uid": "ppanel-server-logs",
"version": 5,
"weekStart": ""
}
@@ -0,0 +1,57 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
jsonData:
httpMethod: POST
manageAlerts: true
prometheusType: Prometheus
prometheusVersion: 2.50.0
timeInterval: 15s
- name: Loki
uid: loki
type: loki
access: proxy
url: http://loki:3100
editable: true
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: '"(?:trace|traceID|trace_id)"\s*:\s*"([a-f0-9]{32})"'
name: TraceID
url: '$${__value.raw}'
- name: Tempo
uid: tempo
type: tempo
access: proxy
url: http://tempo:3200
editable: true
jsonData:
tracesToLogsV2:
datasourceUid: loki
filterByTraceID: true
filterBySpanID: false
tags:
- key: service.name
value: service_name
tracesToMetrics:
datasourceUid: prometheus
serviceMap:
datasourceUid: prometheus
- name: CloudWatch
uid: cloudwatch
type: cloudwatch
access: proxy
editable: true
jsonData:
authType: default
defaultRegion: ap-east-1
@@ -0,0 +1 @@
ALTER TABLE `user_device` DROP COLUMN `base_payload`;
@@ -0,0 +1 @@
ALTER TABLE `user_device` ADD COLUMN `base_payload` TEXT DEFAULT NULL COMMENT 'Base Payload' AFTER `short_code`;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS `order_recovery_claims`;
@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS `order_recovery_claims` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(255) NOT NULL COMMENT 'Recovered Order No',
`email` VARCHAR(255) NOT NULL COMMENT 'Claim Email',
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'User ID',
`subscribe_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe ID',
`order_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Recovered Order ID',
`user_subscribe_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'User Subscribe ID',
`claimed_at` DATETIME(3) NOT NULL COMMENT 'Claimed Time',
`created_at` DATETIME(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` DATETIME(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_order_recovery_claim_order_no` (`order_no`),
KEY `idx_order_recovery_claim_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,25 @@
package recovery
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/recovery"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
func RecoverOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.RecoverOrderRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := recovery.NewRecoverOrderLogic(c.Request.Context(), svcCtx)
resp, err := l.RecoverOrder(&req)
result.HttpResult(c, resp, err)
}
}
@@ -0,0 +1,25 @@
package recovery
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/recovery"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
func SendCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.RecoverySendCodeRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := recovery.NewSendCodeLogic(c.Request.Context(), svcCtx)
resp, err := l.SendCode(&req)
result.HttpResult(c, resp, err)
}
}
+12
View File
@@ -34,6 +34,7 @@ import (
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
publicRecovery "github.com/perfect-panel/server/internal/handler/public/recovery"
publicRedemption "github.com/perfect-panel/server/internal/handler/public/redemption"
publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
@@ -932,6 +933,17 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
}
publicRecoveryGroupRouter := router.Group("/v1/public/recovery")
publicRecoveryGroupRouter.Use(middleware.DeviceMiddleware(serverCtx))
{
// Send recovery verification code
publicRecoveryGroupRouter.POST("/send_code", publicRecovery.SendCodeHandler(serverCtx))
// Recover order subscription
publicRecoveryGroupRouter.POST("/order", publicRecovery.RecoverOrderHandler(serverCtx))
}
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
@@ -119,7 +119,12 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
}
userInfo.Commission = req.Commission
}
tool.DeepCopy(userInfo, req)
userInfo.Avatar = req.Avatar
userInfo.ReferCode = req.ReferCode
userInfo.RefererId = req.RefererId
userInfo.Enable = &req.Enable
userInfo.IsAdmin = &req.IsAdmin
userInfo.Remark = req.Remark
userInfo.OnlyFirstPurchase = &req.OnlyFirstPurchase
userInfo.ReferralPercentage = req.ReferralPercentage
+12
View File
@@ -96,6 +96,17 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error())
}
// Update base_payload if provided
if req.BasePayload != "" && req.BasePayload != deviceInfo.BasePayload {
deviceInfo.BasePayload = req.BasePayload
if updateErr := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); updateErr != nil {
l.Errorw("update device base_payload failed",
logger.Field("device_id", deviceInfo.Id),
logger.Field("error", updateErr.Error()),
)
}
}
// 注销后 device auth_method 被删除,重新登录时需要补回
hasDeviceAuth := false
for _, am := range userInfo.AuthMethods {
@@ -225,6 +236,7 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
UserAgent: req.UserAgent,
Identifier: req.Identifier,
ShortCode: req.ShortCode,
BasePayload: req.BasePayload,
Enabled: true,
Online: false,
}
+3 -1
View File
@@ -126,7 +126,9 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
return err
}
rc := l.svcCtx.Config.Register
if ShouldGrantTrialForEmail(rc, req.Email) {
if ShouldAutoGrantTrialOnPublicEmailFlows(rc) &&
ShouldGrantTrialForEmail(rc, req.Email) &&
!NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, req.Email, rc.TrialSubscribe) {
if err = l.activeTrial(userInfo.Id); err != nil {
return err
}
@@ -396,7 +396,10 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
}
rc := l.svcCtx.Config.Register
shouldActivateTrial := email != "" && authlogic.ShouldGrantTrialForEmail(rc, email)
shouldActivateTrial := email != "" &&
authlogic.ShouldAutoGrantTrialOnPublicEmailFlows(rc) &&
authlogic.ShouldGrantTrialForEmail(rc, email) &&
!authlogic.NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, email, rc.TrialSubscribe)
if shouldActivateTrial {
l.Debugw("activating trial subscription",
+173 -4
View File
@@ -1,9 +1,13 @@
package auth
import (
"context"
"net/mail"
"strings"
"github.com/perfect-panel/server/internal/config"
usermodel "github.com/perfect-panel/server/internal/model/user"
"gorm.io/gorm"
)
// IsEmailDomainWhitelisted checks if the email's domain is in the comma-separated whitelist.
@@ -12,11 +16,10 @@ func IsEmailDomainWhitelisted(email, whitelistCSV string) bool {
if whitelistCSV == "" {
return false
}
parts := strings.SplitN(email, "@", 2)
if len(parts) != 2 {
_, domain, ok := parseStrictEmail(email)
if !ok {
return false
}
domain := strings.ToLower(strings.TrimSpace(parts[1]))
for _, d := range strings.Split(whitelistCSV, ",") {
if strings.ToLower(strings.TrimSpace(d)) == domain {
return true
@@ -29,11 +32,177 @@ func ShouldGrantTrialForEmail(register config.RegisterConfig, email string) bool
if !register.EnableTrial {
return false
}
if !IsValidTrialEmail(email) {
return false
}
// 无论白名单是否启用,泛域名邮箱(含 + 别名或 Gmail 点号)始终拒绝赠送
if IsDisposableAlias(email) {
return false
}
if isConfusableGmailDomain(emailDomain(email)) {
return false
}
if !register.EnableTrialEmailWhitelist {
return true
}
if register.TrialEmailDomainWhitelist == "" {
return false
}
return IsEmailDomainWhitelisted(email, register.TrialEmailDomainWhitelist)
if !IsEmailDomainWhitelisted(email, register.TrialEmailDomainWhitelist) {
return false
}
return true
}
// IsTrialConfigReady verifies that trial auto-grant has all required config.
func IsTrialConfigReady(register config.RegisterConfig) bool {
return register.EnableTrial &&
register.TrialSubscribe > 0 &&
register.TrialTime > 0 &&
strings.TrimSpace(register.TrialTimeUnit) != ""
}
// ShouldAutoGrantTrialOnPublicEmailFlows defines whether browser/email-originated
// flows may auto-create a trial subscription. Email-specific abuse protection
// is still handled by ShouldGrantTrialForEmail and NormalizedEmailHasTrial.
func ShouldAutoGrantTrialOnPublicEmailFlows(register config.RegisterConfig) bool {
return IsTrialConfigReady(register)
}
// IsDisposableAlias detects Gmail dot trick and + alias abuse.
// For Gmail-like domains, local part containing "." or "+" is rejected.
// For all other domains, only "+" alias is rejected.
func IsDisposableAlias(email string) bool {
local, domain, ok := parseStrictEmail(email)
if !ok {
return false
}
// All domains: reject + alias
if strings.ContainsRune(local, '+') {
return true
}
// Gmail-like domains: reject dots in local part
if isGmailLikeDomain(domain) && strings.ContainsRune(local, '.') {
return true
}
return false
}
// NormalizeEmail returns a canonical form of the email for trial deduplication.
// Strips "+" aliases universally (user+tag@any.com → user@any.com).
// Removes dots from local part for Gmail-like providers (gmail.com, googlemail.com).
func NormalizeEmail(email string) string {
email = strings.ToLower(strings.TrimSpace(email))
local, domain, ok := parseStrictEmail(email)
if !ok {
return email
}
// Strip + alias
if idx := strings.IndexByte(local, '+'); idx != -1 {
local = local[:idx]
}
// Remove dots for Gmail-like providers that ignore dots in local part
if isGmailLikeDomain(domain) {
local = strings.ReplaceAll(local, ".", "")
}
return local + "@" + domain
}
func isGmailLikeDomain(domain string) bool {
switch domain {
case "gmail.com", "googlemail.com":
return true
}
return false
}
func IsValidTrialEmail(email string) bool {
local, domain, ok := parseStrictEmail(email)
if !ok {
return false
}
return local != "" && domain != ""
}
func parseStrictEmail(email string) (local, domain string, ok bool) {
email = strings.ToLower(strings.TrimSpace(email))
if email == "" || strings.ContainsAny(email, " \t\r\n") {
return "", "", false
}
addr, err := mail.ParseAddress(email)
if err != nil || addr.Address != email || addr.Name != "" {
return "", "", false
}
parts := strings.Split(addr.Address, "@")
if len(parts) != 2 {
return "", "", false
}
local = strings.TrimSpace(parts[0])
domain = strings.Trim(strings.TrimSpace(parts[1]), ".")
if local == "" || domain == "" || strings.Contains(domain, "..") || !strings.Contains(domain, ".") {
return "", "", false
}
return local, domain, true
}
func emailDomain(email string) string {
_, domain, ok := parseStrictEmail(email)
if !ok {
return ""
}
return domain
}
func isConfusableGmailDomain(domain string) bool {
switch strings.ToLower(strings.TrimSpace(domain)) {
case "gmaial.com", "gmial.com", "gmai.com", "gamil.com", "gmal.com", "gmail.co", "gmail.con":
return true
}
return false
}
// NormalizedEmailHasTrial returns true if any user with the same normalized email
// already holds a trial subscription. Only performs the cross-user DB check when
// normalization actually changes the email (i.e., dots removed or + alias stripped).
func NormalizedEmailHasTrial(ctx context.Context, db *gorm.DB, email string, trialSubscribeId int64) bool {
normalized := NormalizeEmail(email)
if normalized == strings.ToLower(strings.TrimSpace(email)) {
return false // normalization changed nothing, skip cross-user check
}
parts := strings.SplitN(normalized, "@", 2)
if len(parts) != 2 {
return false
}
domain := parts[1]
var authMethods []usermodel.AuthMethods
if err := db.WithContext(ctx).
Model(&usermodel.AuthMethods{}).
Select("user_id, auth_identifier").
Where("auth_type = ? AND auth_identifier LIKE ?", "email", "%@"+domain).
Find(&authMethods).Error; err != nil {
return false
}
for _, am := range authMethods {
if NormalizeEmail(am.AuthIdentifier) != normalized {
continue
}
var count int64
if err := db.WithContext(ctx).
Model(&usermodel.Subscribe{}).
Where("user_id = ? AND subscribe_id = ?", am.UserId, trialSubscribeId).
Count(&count).Error; err != nil {
continue
}
if count > 0 {
return true
}
}
return false
}
@@ -0,0 +1,198 @@
package auth
import (
"testing"
"github.com/perfect-panel/server/internal/config"
"github.com/stretchr/testify/assert"
)
func TestNormalizeEmail(t *testing.T) {
tests := []struct {
input string
want string
}{
// Gmail dot trick
{"a.v.x.xx@gmail.com", "avxxx@gmail.com"},
{"john.doe@gmail.com", "johndoe@gmail.com"},
{"a.b.c.d.e@gmail.com", "abcde@gmail.com"},
// Gmail + alias
{"user+tag@gmail.com", "user@gmail.com"},
{"a.b+tag@gmail.com", "ab@gmail.com"},
// Googlemail
{"a.b@googlemail.com", "ab@googlemail.com"},
// Non-Gmail: dots preserved
{"john.doe@outlook.com", "john.doe@outlook.com"},
{"john.doe@qq.com", "john.doe@qq.com"},
// + alias stripped for all providers
{"user+spam@outlook.com", "user@outlook.com"},
{"user+spam@qq.com", "user@qq.com"},
// Case insensitive
{"User@Gmail.COM", "user@gmail.com"},
{"A.B@Gmail.com", "ab@gmail.com"},
// No change for normal non-gmail email
{"abc@163.com", "abc@163.com"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := NormalizeEmail(tt.input)
if got != tt.want {
t.Errorf("NormalizeEmail(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestNormalizeEmail_NoChangeSkipsCheck(t *testing.T) {
// These emails should NOT trigger cross-user check (normalized == original)
noChangeCases := []string{
"abc@163.com",
"john.doe@outlook.com",
"user@qq.com",
}
for _, email := range noChangeCases {
normalized := NormalizeEmail(email)
lower := email
if normalized == lower {
// correct: no normalization change, NormalizedEmailHasTrial would return false early
}
}
}
func TestShouldGrantTrialForEmail(t *testing.T) {
// 模拟线上配置:白名单开启,gmail.com 也在名单里
rcWithGmail := config.RegisterConfig{
EnableTrial: true,
EnableTrialEmailWhitelist: true,
TrialEmailDomainWhitelist: "hifastapp.com,hifastvpn.com,126.com,139.com,163.com,gmail.com",
}
// 白名单关闭
rcNoWhitelist := config.RegisterConfig{
EnableTrial: true,
EnableTrialEmailWhitelist: false,
}
tests := []struct {
name string
rc config.RegisterConfig
email string
want bool
reason string
}{
{
name: "gmail dot trick - blocked even if gmail.com in whitelist",
rc: rcWithGmail,
email: "s.m.s.n.fsmbt.d.ndny@gmail.com",
want: false,
reason: "gmail 泛域名(含点号)应拒绝",
},
{
name: "gmail plus alias - blocked",
rc: rcWithGmail,
email: "user+tag@gmail.com",
want: false,
reason: "gmail +别名应拒绝",
},
{
name: "clean gmail - allowed",
rc: rcWithGmail,
email: "normaluser@gmail.com",
want: true,
reason: "干净的 gmail 应放行",
},
{
name: "163 with dot - allowed (non-gmail dot is ok)",
rc: rcWithGmail,
email: "s.m.s.n@163.com",
want: true,
reason: "非 gmail 域点号不拦截",
},
{
name: "163 plus alias - blocked",
rc: rcWithGmail,
email: "user+spam@163.com",
want: false,
reason: "所有域名的 +别名都拦截",
},
{
name: "gmail typo squatting domain - blocked even if accidentally whitelisted",
rc: config.RegisterConfig{EnableTrial: true, EnableTrialEmailWhitelist: true, TrialEmailDomainWhitelist: "gmail.com,gmaial.com"},
email: "1.2.3.4xxx@gmaial.com",
want: false,
reason: "易混淆 Gmail 域名不应发放试用",
},
{
name: "invalid empty local - blocked",
rc: rcWithGmail,
email: "@gmail.com",
want: false,
reason: "邮箱 local 为空应拒绝",
},
{
name: "subdomain spoof - blocked",
rc: rcWithGmail,
email: "user@fake.gmail.com",
want: false,
reason: "白名单必须精确匹配域名,不匹配子域",
},
{
name: "whitelist disabled - gmail dot trick still blocked",
rc: rcNoWhitelist,
email: "s.m.s.n.fsmbt.d.ndny@gmail.com",
want: false,
reason: "白名单未启用,但泛域名仍应拒绝",
},
{
name: "trial disabled - always blocked",
rc: config.RegisterConfig{EnableTrial: false},
email: "user@163.com",
want: false,
reason: "试用未开启",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ShouldGrantTrialForEmail(tt.rc, tt.email)
if got != tt.want {
t.Errorf("ShouldGrantTrialForEmail(%q) = %v, want %v | reason: %s",
tt.email, got, tt.want, tt.reason)
}
})
}
}
func TestShouldAutoGrantTrialOnPublicEmailFlows(t *testing.T) {
assert.False(t, ShouldAutoGrantTrialOnPublicEmailFlows(config.RegisterConfig{}))
assert.False(t, ShouldAutoGrantTrialOnPublicEmailFlows(config.RegisterConfig{
EnableTrial: true,
EnableTrialEmailWhitelist: true,
TrialEmailDomainWhitelist: "gmail.com,example.com",
}))
}
func TestIsEmailDomainWhitelisted(t *testing.T) {
whitelist := "gmail.com,edu.cn,outlook.com"
tests := []struct {
email string
want bool
}{
{"user@gmail.com", true},
{"user@edu.cn", true},
{"User@Gmail.COM", true},
{"user@yahoo.com", false},
{"user@fake.gmail.com", false}, // subdomain not matched
{"user@", false},
{"notanemail", false},
{"@gmail.com", false},
}
for _, tt := range tests {
t.Run(tt.email, func(t *testing.T) {
got := IsEmailDomainWhitelisted(tt.email, whitelist)
if got != tt.want {
t.Errorf("IsEmailDomainWhitelisted(%q) = %v, want %v", tt.email, got, tt.want)
}
})
}
}
+3 -1
View File
@@ -148,7 +148,9 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
// Activate trial subscription after transaction success (moved outside transaction to reduce lock time)
rc := l.svcCtx.Config.Register
if ShouldGrantTrialForEmail(rc, req.Email) {
if ShouldAutoGrantTrialOnPublicEmailFlows(rc) &&
ShouldGrantTrialForEmail(rc, req.Email) &&
!NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, req.Email, rc.TrialSubscribe) {
trialSubscribe, err = l.activeTrial(userInfo.Id)
if err != nil {
l.Errorw("Failed to activate trial subscription", logger.Field("error", err.Error()))
+4 -1
View File
@@ -76,7 +76,10 @@ func CountScopedSubscribePurchaseOrders(
var count int64
query := db.WithContext(ctx).
Model(&modelOrder.Order{}).
Where("user_id IN ? AND subscribe_id = ? AND type IN ? AND amount > 0", scopeUserIDs, subscribeID, []int64{1, 2})
Where("user_id IN ? AND type IN ?", scopeUserIDs, []int64{1, 2})
if subscribeID > 0 {
query = query.Where("subscribe_id = ?", subscribeID)
}
if len(statuses) > 0 {
query = query.Where("status IN ?", statuses)
}
+1 -5
View File
@@ -52,12 +52,8 @@ func ResolvePurchaseRoute(
return decision, nil
}
if requestedSubscribeID != anchorSub.SubscribeId {
return nil, ErrSingleModePlanMismatch
}
decision.Route = PurchaseRoutePurchaseToRenewal
decision.ResolvedSubscribeID = anchorSub.SubscribeId
decision.ResolvedSubscribeID = requestedSubscribeID
decision.Anchor = anchorSub
return decision, nil
}
@@ -0,0 +1,36 @@
package common
import (
"context"
"testing"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/stretchr/testify/require"
)
func TestResolvePurchaseRoute_AllowsPlanChangeForExistingSubscription(t *testing.T) {
anchor := &user.Subscribe{
Id: 10,
UserId: 20,
OrderId: 30,
SubscribeId: 1,
Token: "existing-token",
ExpireTime: time.Now().Add(time.Hour),
}
decision, err := ResolvePurchaseRoute(
context.Background(),
true,
anchor.UserId,
2,
func(context.Context, int64) (*user.Subscribe, error) {
return anchor, nil
},
)
require.NoError(t, err)
require.Equal(t, PurchaseRoutePurchaseToRenewal, decision.Route)
require.Equal(t, int64(2), decision.ResolvedSubscribeID)
require.Equal(t, anchor, decision.Anchor)
}
+108
View File
@@ -0,0 +1,108 @@
package common
import (
"strings"
ordermodel "github.com/perfect-panel/server/internal/model/order"
usermodel "github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/pkg/logger"
)
const (
SubscriptionTraceType = "subscription_flow"
SubscriptionTraceFlowOrder = "order_subscription"
SubscriptionTraceFlowEmailBind = "email_bind_subscription"
)
func SubscriptionTraceFields(flow string, stage string, fields ...logger.LogField) []logger.LogField {
base := []logger.LogField{
logger.Field("trace_type", SubscriptionTraceType),
logger.Field("flow", flow),
logger.Field("stage", stage),
}
return append(base, fields...)
}
func SubscriptionTraceInfo(log logger.Logger, flow string, stage string, msg string, fields ...logger.LogField) {
log.Infow(msg, SubscriptionTraceFields(flow, stage, fields...)...)
}
func SubscriptionTraceError(log logger.Logger, flow string, stage string, msg string, fields ...logger.LogField) {
log.Errorw(msg, SubscriptionTraceFields(flow, stage, fields...)...)
}
func OrderTraceFields(orderInfo *ordermodel.Order) []logger.LogField {
if orderInfo == nil {
return nil
}
effectiveUserID := orderInfo.UserId
if orderInfo.SubscriptionUserId > 0 {
effectiveUserID = orderInfo.SubscriptionUserId
}
fields := []logger.LogField{
logger.Field("order_id", orderInfo.Id),
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("order_type", orderInfo.Type),
logger.Field("order_status", orderInfo.Status),
logger.Field("user_id", orderInfo.UserId),
logger.Field("subscription_user_id", orderInfo.SubscriptionUserId),
logger.Field("effective_user_id", effectiveUserID),
logger.Field("order_subscribe_id", orderInfo.SubscribeId),
logger.Field("payment_id", orderInfo.PaymentId),
logger.Field("payment_method", orderInfo.Method),
logger.Field("parent_order_id", orderInfo.ParentId),
logger.Field("quantity", orderInfo.Quantity),
logger.Field("is_new_order", orderInfo.IsNew),
}
if tail := SensitiveTail(orderInfo.SubscribeToken); tail != "" {
fields = append(fields, logger.Field("subscribe_token_tail", tail))
}
if tail := SensitiveTail(orderInfo.TradeNo); tail != "" {
fields = append(fields, logger.Field("trade_no_tail", tail))
}
if tail := SensitiveTail(orderInfo.AppAccountToken); tail != "" {
fields = append(fields, logger.Field("app_account_token_tail", tail))
}
return fields
}
func UserSubscribeTraceFields(userSub *usermodel.Subscribe) []logger.LogField {
if userSub == nil {
return nil
}
fields := []logger.LogField{
logger.Field("user_subscribe_id", userSub.Id),
logger.Field("subscribe_owner_user_id", userSub.UserId),
logger.Field("user_subscribe_plan_id", userSub.SubscribeId),
logger.Field("subscribe_order_id", userSub.OrderId),
logger.Field("subscribe_status", userSub.Status),
logger.Field("expire_time", userSub.ExpireTime),
}
if tail := SensitiveTail(userSub.Token); tail != "" {
fields = append(fields, logger.Field("subscribe_token_tail", tail))
}
if tail := SensitiveTail(userSub.UUID); tail != "" {
fields = append(fields, logger.Field("subscribe_uuid_tail", tail))
}
return fields
}
func SensitiveTail(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
if len(value) <= 8 {
return value
}
return value[len(value)-8:]
}
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/xerr"
@@ -56,6 +57,12 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error {
l.Logger.Error("[AlipayNotify] Decode notification failed", logger.Field("error", err.Error()))
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
"[SubscriptionFlow] alipay notify received",
logger.Field("order_no", notify.OrderNo),
logger.Field("payment_platform", data.Platform),
logger.Field("notify_status", string(notify.Status)),
)
if notify.Status == alipay.Success {
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, notify.OrderNo)
if err != nil {
@@ -73,6 +80,12 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error {
l.Logger.Error("[AlipayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo))
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
"[SubscriptionFlow] alipay notify marked order as paid",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", data.Platform),
)...,
)
l.Logger.Info("[AlipayNotify] Notify status success", logger.Field("orderNo", notify.OrderNo))
payload := types.ForthwithActivateOrderPayload{
OrderNo: notify.OrderNo,
@@ -88,6 +101,13 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error {
l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error()))
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
"[SubscriptionFlow] activation task enqueued from alipay notify",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", data.Platform),
logger.Field("queue_task_id", taskInfo.ID),
)...,
)
l.Logger.Info("[AlipayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo))
} else {
l.Logger.Error("[AlipayNotify] Notify status failed", logger.Field("status", string(notify.Status)))
@@ -6,6 +6,7 @@ import (
"strconv"
"strings"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
@@ -57,6 +58,13 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error {
}
// 验签通过,记录通知类型与关键交易标识
l.Infow("iap notify verified", logger.Field("type", ntype), logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
"[SubscriptionFlow] apple iap server notification received",
logger.Field("notify_type", ntype),
logger.Field("product_id", txPayload.ProductId),
logger.Field("original_transaction_tail", commonLogic.SensitiveTail(txPayload.OriginalTransactionId)),
logger.Field("transaction_id_tail", commonLogic.SensitiveTail(txPayload.TransactionId)),
)
return l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
var existing *iapmodel.Transaction
existing, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId)
@@ -201,6 +209,13 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error {
return err
}
l.Infow("iap notify fallback updated subscribe", logger.Field("userSubscribeId", candidate.Id), logger.Field("status", candidate.Status))
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_updated_from_notify",
"[SubscriptionFlow] apple iap notify updated fallback subscription candidate",
append(commonLogic.UserSubscribeTraceFields(candidate),
logger.Field("notify_type", ntype),
logger.Field("product_id", txPayload.ProductId),
)...,
)
break
}
}
@@ -226,6 +241,13 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error {
}
// 更新成功,输出订阅状态
l.Infow("iap notify updated subscribe", logger.Field("userSubscribeId", sub.Id), logger.Field("status", sub.Status))
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_updated_from_notify",
"[SubscriptionFlow] apple iap notify updated subscription",
append(commonLogic.UserSubscribeTraceFields(sub),
logger.Field("notify_type", ntype),
logger.Field("product_id", txPayload.ProductId),
)...,
)
}
return nil
})
+21 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/url"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/xerr"
@@ -44,12 +45,18 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
l.Logger.Error("[EPayNotify] Payment not found in context")
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment config not found")
}
l.Infof("[EPayNotify] Payment config: %+v", data)
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OutTradeNo)
if err != nil {
l.Logger.Error("[EPayNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo)
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
"[SubscriptionFlow] epay notify received",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", data.Platform),
logger.Field("trade_status", req.TradeStatus),
)...,
)
var config payment.EPayConfig
if err := json.Unmarshal([]byte(data.Config), &config); err != nil {
@@ -75,6 +82,12 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
"[SubscriptionFlow] epay notify marked order as paid",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", data.Platform),
)...,
)
// Create activate order task
payload := queueType.ForthwithActivateOrderPayload{
OrderNo: req.OutTradeNo,
@@ -90,6 +103,13 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
"[SubscriptionFlow] activation task enqueued from epay notify",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", data.Platform),
logger.Field("queue_task_id", taskInfo.ID),
)...,
)
l.Logger.Info("[EPayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo))
return nil
}
+24 -1
View File
@@ -6,6 +6,7 @@ import (
"io"
"net/http"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/xerr"
@@ -67,6 +68,13 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
l.Logger.Error("[StripeNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo))
return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", notify.OrderNo)
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
"[SubscriptionFlow] stripe notify received",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", stripeConfig.Platform),
logger.Field("stripe_event_type", notify.EventType),
)...,
)
if notify.EventType == "payment_intent.succeeded" {
if orderInfo.Status == 5 {
return nil
@@ -76,6 +84,13 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
if err != nil {
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
"[SubscriptionFlow] stripe notify marked order as paid",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", stripeConfig.Platform),
logger.Field("stripe_event_type", notify.EventType),
)...,
)
// create ActivateOrder task
payload := types.ForthwithActivateOrderPayload{
OrderNo: notify.OrderNo,
@@ -86,11 +101,19 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
return err
}
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
_, err = l.svcCtx.Queue.Enqueue(task)
taskInfo, err := l.svcCtx.Queue.Enqueue(task)
if err != nil {
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
"[SubscriptionFlow] activation task enqueued from stripe notify",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", stripeConfig.Platform),
logger.Field("stripe_event_type", notify.EventType),
logger.Field("queue_task_id", taskInfo.ID),
)...,
)
l.Infow("[StripeNotify] success", logger.Field("orderNo", notify.OrderNo))
}
return nil
@@ -82,6 +82,13 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
l.Errorw("订单与当前用户不匹配", logger.Field("orderNo", req.OrderNo), logger.Field("orderUserId", orderInfo.UserId), logger.Field("userId", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "order owner mismatch")
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "iap_attach_start",
"[SubscriptionFlow] apple iap attach flow started",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("request_user_id", u.Id),
logger.Field("effective_user_id", entitlement.EffectiveUserID),
)...,
)
isNewPurchaseOrder := orderInfo.Type == orderTypeSubscribe
if isNewPurchaseOrder {
l.Infow("首购订单将只由订单激活流程创建订阅", logger.Field("orderNo", req.OrderNo), logger.Field("orderType", orderInfo.Type))
@@ -93,6 +100,14 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws")
}
l.Infow("JWS 验签成功", logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("purchaseAt", txPayload.PurchaseDate))
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "iap_attach_verified",
"[SubscriptionFlow] apple iap transaction verified",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("product_id", txPayload.ProductId),
logger.Field("original_transaction_tail", commonLogic.SensitiveTail(txPayload.OriginalTransactionId)),
logger.Field("transaction_id_tail", commonLogic.SensitiveTail(txPayload.TransactionId)),
)...,
)
tradeNoCandidates := l.getAppleTradeNoCandidates(txPayload)
existingOrderNo, validateErr := l.validateOrderTradeNoBinding(orderInfo, tradeNoCandidates)
if validateErr != nil {
@@ -390,6 +405,12 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
return e
}
l.Infow("写入用户订阅成功", logger.Field("userId", u.Id), logger.Field("subscribeId", subscribeId), logger.Field("expireUnix", exp.Unix()))
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_created",
"[SubscriptionFlow] apple iap attach created a subscription placeholder before queue activation",
append(commonLogic.OrderTraceFields(orderInfo),
commonLogic.UserSubscribeTraceFields(&userSub)...,
)...,
)
}
} else {
l.Infow("首购订单跳过 attach 阶段订阅写入", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderType", orderInfo.Type))
@@ -453,6 +474,12 @@ func (l *AttachTransactionLogic) syncOrderStatusAndEnqueue(orderInfo *ordermodel
}
orderInfo.Status = orderStatusPaid
l.Infow("更新订单状态成功", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("status", orderStatusPaid))
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
"[SubscriptionFlow] apple iap attach marked order as paid",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("iap_expire_at", iapExpireAt),
)...,
)
}
// enqueue activation regardless (idempotent handler downstream)
payload := queueType.ForthwithActivateOrderPayload{OrderNo: orderInfo.OrderNo, IAPExpireAt: iapExpireAt}
@@ -463,6 +490,12 @@ func (l *AttachTransactionLogic) syncOrderStatusAndEnqueue(orderInfo *ordermodel
l.Errorw("enqueue activate task error", logger.Field("error", err.Error()))
} else {
l.Infow("已加入订单激活队列", logger.Field("orderNo", orderInfo.OrderNo))
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
"[SubscriptionFlow] apple iap attach enqueued activation task",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("iap_expire_at", iapExpireAt),
)...,
)
}
return nil
}
+3 -1
View File
@@ -12,6 +12,9 @@ func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64, isNewUs
if d.Quantity != inputMonths || d.Discount <= 0 || d.Discount >= 100 {
continue
}
if d.NewUserOnly && !isNewUser {
continue
}
if isNewUser {
// lowest discount value = biggest saving
if best < 0 || d.Discount < best {
@@ -50,4 +53,3 @@ func isNewUserOnlyForQuantity(discounts []types.SubscribeDiscount, inputQuantity
}
return hasNewUserOnly && !hasFallback
}
@@ -0,0 +1,16 @@
package order
import (
"testing"
"github.com/perfect-panel/server/internal/types"
"github.com/stretchr/testify/require"
)
func TestGetDiscount_SkipsNewUserOnlyTierForExistingUser(t *testing.T) {
discount := getDiscount([]types.SubscribeDiscount{
{Quantity: 1, Discount: 90, NewUserOnly: true},
}, 1, false)
require.Equal(t, float64(1), discount)
}
@@ -47,7 +47,7 @@ func resolveNewUserDiscountEligibility(
ctx,
db,
eligibility.ScopeUserIDs,
subscribeID,
0,
[]int64{2, 5},
"",
)
+62 -10
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"math"
"strings"
"time"
"github.com/google/uuid"
@@ -62,6 +63,17 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
return nil, entErr
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_create_start",
"[SubscriptionFlow] purchase order creation started",
logger.Field("order_kind", "purchase"),
logger.Field("user_id", u.Id),
logger.Field("effective_user_id", entitlement.EffectiveUserID),
logger.Field("requested_subscribe_id", req.SubscribeId),
logger.Field("quantity", req.Quantity),
logger.Field("payment_id", req.Payment),
logger.Field("coupon", req.Coupon),
)
if req.Quantity <= 0 {
l.Debugf("[Purchase] Quantity is less than or equal to 0, setting to 1")
req.Quantity = 1
@@ -101,12 +113,42 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
parentOrderID = decision.Anchor.OrderId
subscribeToken = decision.Anchor.Token
anchorUserSubscribeID = decision.Anchor.Id
l.Infow("[Purchase] single mode purchase routed to renewal",
logger.Field("mode", "single"),
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_route_selected",
"[SubscriptionFlow] purchase routed to renewal before order creation",
logger.Field("route_mode", "single"),
logger.Field("route", "purchase_to_renewal"),
logger.Field("anchor_user_subscribe_id", decision.Anchor.Id),
logger.Field("order_no", "pending"),
logger.Field("user_id", u.Id),
logger.Field("effective_user_id", entitlement.EffectiveUserID),
logger.Field("requested_subscribe_id", req.SubscribeId),
logger.Field("resolved_subscribe_id", targetSubscribeID),
)
}
}
// 全局单订阅口径:若用户已有任意付费订阅(含过期),提前路由为续费/换套餐,
// 防止不同套餐购买创建第二条订阅。
if !l.svcCtx.Config.Subscribe.SingleModel && orderType == 1 {
var existSub user.Subscribe
if e := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Subscribe{}).
Where("user_id = ? AND token != '' AND (order_id > 0 OR token LIKE 'iap:%')", entitlement.EffectiveUserID).
Order("expire_time DESC").
Order("updated_at DESC").
Order("id DESC").
First(&existSub).Error; e == nil && existSub.Id > 0 && existSub.Token != "" {
orderType = 2
parentOrderID = existSub.OrderId
subscribeToken = existSub.Token
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_route_selected",
"[SubscriptionFlow] purchase routed to renewal because an existing subscription was found",
logger.Field("route_mode", "global_single_subscription"),
logger.Field("route", "purchase_to_existing_subscription"),
logger.Field("existing_subscribe_id", existSub.Id),
logger.Field("existing_status", existSub.Status),
logger.Field("user_id", u.Id),
logger.Field("effective_user_id", entitlement.EffectiveUserID),
logger.Field("resolved_subscribe_id", targetSubscribeID),
)
}
}
@@ -277,13 +319,13 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
AppAccountToken: uuid.New().String(),
}
if isSingleModeRenewal {
l.Infow("[Purchase] single mode purchase order created as renewal",
logger.Field("mode", "single"),
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created",
"[SubscriptionFlow] purchase order persisted as renewal",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("route_mode", "single"),
logger.Field("route", "purchase_to_renewal"),
logger.Field("anchor_user_subscribe_id", anchorUserSubscribeID),
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("parent_id", orderInfo.ParentId),
logger.Field("user_id", u.Id),
)...,
)
}
// Database transaction
@@ -291,13 +333,13 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
// check subscribe plan quota limit inside transaction to prevent race condition
if orderInfo.Type == 1 && sub.Quota > 0 {
var currentUserSub []user.Subscribe
if e := db.Model(&user.Subscribe{}).Where("user_id = ?", u.Id).Find(&currentUserSub).Error; e != nil {
if e := db.Model(&user.Subscribe{}).Where("user_id = ?", entitlement.EffectiveUserID).Find(&currentUserSub).Error; e != nil {
l.Errorw("[Purchase] Database query error", logger.Field("error", e.Error()), logger.Field("user_id", u.Id))
return e
}
var count int64
for _, v := range currentUserSub {
if v.SubscribeId == targetSubscribeID {
if v.OrderId > 0 || strings.HasPrefix(v.Token, "iap:") {
count++
}
}
@@ -380,6 +422,16 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error())
}
if !isSingleModeRenewal {
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created",
"[SubscriptionFlow] purchase order persisted",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("route_mode", "standard"),
logger.Field("resolved_subscribe_id", targetSubscribeID),
)...,
)
}
// Deferred task
payload := queue.DeferCloseOrderPayload{
OrderNo: orderInfo.OrderNo,
@@ -0,0 +1,845 @@
package order
import (
"context"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/hibiken/asynq"
modelOrder "github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/payment"
subModel "github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// setupNewUserOnlyDB 创建带必要表的 SQLite 内存数据库
func setupNewUserOnlyDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
require.NoError(t, err, "failed to open in-memory SQLite")
db.Exec("PRAGMA foreign_keys = OFF")
sqls := []string{
`CREATE TABLE IF NOT EXISTS "subscribe" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL DEFAULT '',
language VARCHAR(255) NOT NULL DEFAULT '',
description TEXT,
unit_price INTEGER NOT NULL DEFAULT 0,
unit_time VARCHAR(255) NOT NULL DEFAULT '',
discount TEXT,
replacement INTEGER NOT NULL DEFAULT 0,
inventory INTEGER NOT NULL DEFAULT -1,
traffic INTEGER NOT NULL DEFAULT 0,
speed_limit INTEGER NOT NULL DEFAULT 0,
device_limit INTEGER NOT NULL DEFAULT 0,
quota INTEGER NOT NULL DEFAULT 0,
new_user_only TINYINT DEFAULT 0,
nodes VARCHAR(255),
node_tags VARCHAR(255),
show TINYINT NOT NULL DEFAULT 0,
sell TINYINT NOT NULL DEFAULT 1,
sort INTEGER NOT NULL DEFAULT 0,
deduction_ratio INTEGER DEFAULT 0,
allow_deduction TINYINT DEFAULT 1,
reset_cycle INTEGER DEFAULT 0,
renewal_reset TINYINT DEFAULT 0,
show_original_price TINYINT NOT NULL DEFAULT 1,
created_at DATETIME,
updated_at DATETIME
)`,
`CREATE TABLE IF NOT EXISTS "order" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_id INTEGER DEFAULT NULL,
user_id INTEGER NOT NULL DEFAULT 0,
subscription_user_id INTEGER NOT NULL DEFAULT 0,
order_no VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
type TINYINT NOT NULL DEFAULT 1,
quantity INTEGER NOT NULL DEFAULT 1,
price INTEGER NOT NULL DEFAULT 0,
amount INTEGER NOT NULL DEFAULT 0,
gift_amount INTEGER NOT NULL DEFAULT 0,
discount INTEGER NOT NULL DEFAULT 0,
coupon VARCHAR(255) DEFAULT NULL,
coupon_discount INTEGER NOT NULL DEFAULT 0,
commission INTEGER NOT NULL DEFAULT 0,
payment_id INTEGER NOT NULL DEFAULT 0,
method VARCHAR(255) NOT NULL DEFAULT '',
fee_amount INTEGER NOT NULL DEFAULT 0,
trade_no VARCHAR(255) DEFAULT NULL,
app_account_token VARCHAR(255) DEFAULT NULL,
status TINYINT NOT NULL DEFAULT 1,
subscribe_id INTEGER NOT NULL DEFAULT 0,
subscribe_token VARCHAR(255) DEFAULT NULL,
is_new TINYINT NOT NULL DEFAULT 0,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME DEFAULT NULL
)`,
`CREATE TABLE IF NOT EXISTS "user" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
password VARCHAR(100) NOT NULL DEFAULT '',
algo VARCHAR(20) DEFAULT 'default',
salt VARCHAR(20) DEFAULT NULL,
avatar TEXT,
balance INTEGER DEFAULT 0,
refer_code VARCHAR(20) DEFAULT '',
referer_id INTEGER DEFAULT 0,
commission INTEGER DEFAULT 0,
referral_percentage INTEGER DEFAULT 0,
only_first_purchase TINYINT DEFAULT 1,
gift_amount INTEGER DEFAULT 0,
enable TINYINT DEFAULT 1,
is_admin TINYINT DEFAULT 0,
enable_balance_notify TINYINT DEFAULT 0,
enable_login_notify TINYINT DEFAULT 0,
enable_subscribe_notify TINYINT DEFAULT 0,
enable_trade_notify TINYINT DEFAULT 0,
rules TEXT,
member_status VARCHAR(20) DEFAULT '',
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME DEFAULT NULL
)`,
`CREATE TABLE IF NOT EXISTS "payment" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL DEFAULT '',
platform VARCHAR(100) NOT NULL DEFAULT '',
icon VARCHAR(255) DEFAULT '',
domain VARCHAR(255) DEFAULT '',
config TEXT NOT NULL DEFAULT '{}',
description TEXT,
fee_mode TINYINT NOT NULL DEFAULT 0,
fee_percent INTEGER DEFAULT 0,
fee_amount INTEGER DEFAULT 0,
enable TINYINT NOT NULL DEFAULT 1,
token VARCHAR(255) NOT NULL DEFAULT '' UNIQUE
)`,
`CREATE TABLE IF NOT EXISTS "user_subscribe" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL DEFAULT 0,
order_id INTEGER NOT NULL DEFAULT 0,
subscribe_id INTEGER NOT NULL DEFAULT 0,
start_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expire_time DATETIME DEFAULT NULL,
finished_at DATETIME DEFAULT NULL,
traffic INTEGER DEFAULT 0,
download INTEGER DEFAULT 0,
upload INTEGER DEFAULT 0,
token VARCHAR(255) DEFAULT '' UNIQUE,
uuid VARCHAR(255) DEFAULT '' UNIQUE,
status TINYINT DEFAULT 0,
note VARCHAR(500) DEFAULT '',
created_at DATETIME,
updated_at DATETIME
)`,
`CREATE TABLE IF NOT EXISTS "user_device" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip VARCHAR(255) NOT NULL DEFAULT '',
user_id INTEGER NOT NULL DEFAULT 0,
user_agent TEXT,
identifier VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
short_code VARCHAR(255) NOT NULL DEFAULT '',
online TINYINT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
created_at DATETIME,
updated_at DATETIME
)`,
`CREATE TABLE IF NOT EXISTS "user_family" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_user_id INTEGER NOT NULL DEFAULT 0,
max_members INTEGER NOT NULL DEFAULT 2,
status TINYINT DEFAULT 0,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME DEFAULT NULL
)`,
`CREATE TABLE IF NOT EXISTS "user_family_member" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
family_id INTEGER NOT NULL DEFAULT 0,
user_id INTEGER NOT NULL DEFAULT 0,
role TINYINT DEFAULT 0,
status TINYINT DEFAULT 0,
join_source VARCHAR(32) NOT NULL DEFAULT '',
joined_at DATETIME,
left_at DATETIME DEFAULT NULL,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME DEFAULT NULL
)`,
}
for _, sql := range sqls {
require.NoError(t, db.Exec(sql).Error)
}
return db
}
// setupNewUserOnlyRedis 启动 miniredis,返回 redis.Client 和 miniredis 句柄
func setupNewUserOnlyRedis(t *testing.T) (*redis.Client, *miniredis.Miniredis) {
t.Helper()
mr, err := miniredis.Run()
require.NoError(t, err)
t.Cleanup(mr.Close)
rds := redis.NewClient(&redis.Options{Addr: mr.Addr()})
return rds, mr
}
// buildNewUserOnlySvcCtx 组装最小 ServiceContext(含 asynq Queue 使用 miniredis
func buildNewUserOnlySvcCtx(db *gorm.DB, rds *redis.Client, mr *miniredis.Miniredis) *svc.ServiceContext {
queue := asynq.NewClient(asynq.RedisClientOpt{Addr: mr.Addr()})
return &svc.ServiceContext{
DB: db,
Redis: rds,
UserModel: user.NewModel(db, rds),
OrderModel: modelOrder.NewModel(db, rds),
SubscribeModel: subModel.NewModel(db, rds),
PaymentModel: payment.NewModel(db, rds),
Queue: queue,
}
}
// insertTestSubscribe 直接用 SQL 插入 subscribe 行(绕过 GORM hook 的 MySQL 方言)
// new_user_only=true 时同时写入 discount JSON,使代码里的 discount 检查生效
func insertTestSubscribe(t *testing.T, db *gorm.DB, id int64, newUserOnly bool) {
t.Helper()
nuOnly := 0
discount := ""
if newUserOnly {
nuOnly = 1
// discount JSON 包含一个 new_user_only=true 的 tier,匹配 quantity=1
discount = `[{"quantity":1,"discount":90,"new_user_only":true}]`
}
err := db.Exec(`INSERT INTO "subscribe"
(id, name, unit_price, inventory, sell, sort, new_user_only, discount, created_at, updated_at)
VALUES (?, 'Test Plan', 1000, -1, 1, ?, ?, ?, datetime('now'), datetime('now'))`,
id, id, nuOnly, discount).Error
require.NoError(t, err)
}
// insertTestPayment 插入支付方式行
func insertTestPayment(t *testing.T, db *gorm.DB, id int64) {
t.Helper()
err := db.Exec(`INSERT INTO "payment"
(id, name, platform, config, enable, fee_mode, token)
VALUES (?, 'Balance', 'balance', '{}', 1, 0, ?)`,
id, "test-token").Error
require.NoError(t, err)
}
// insertTestUser 插入用户行,createdAt 可控
func insertTestUser(t *testing.T, db *gorm.DB, id int64, createdAt time.Time) *user.User {
t.Helper()
err := db.Exec(`INSERT INTO "user"
(id, password, balance, gift_amount, enable, created_at, updated_at)
VALUES (?, '', 0, 0, 1, ?, datetime('now'))`,
id, createdAt.UTC().Format("2006-01-02 15:04:05")).Error
require.NoError(t, err)
return &user.User{
Id: id,
GiftAmount: 0,
CreatedAt: createdAt,
}
}
func insertTestDevice(t *testing.T, db *gorm.DB, userID int64, identifier string, createdAt time.Time) {
t.Helper()
err := db.Exec(`INSERT INTO "user_device"
(user_id, ip, user_agent, identifier, short_code, online, enabled, created_at, updated_at)
VALUES (?, '127.0.0.1', 'test-agent', ?, '', 0, 1, ?, datetime('now'))`,
userID,
identifier,
createdAt.UTC().Format("2006-01-02 15:04:05"),
).Error
require.NoError(t, err)
}
func insertTestFamily(t *testing.T, db *gorm.DB, familyID, ownerUserID int64) {
t.Helper()
err := db.Exec(`INSERT INTO "user_family"
(id, owner_user_id, max_members, status, created_at, updated_at)
VALUES (?, ?, 3, 1, datetime('now'), datetime('now'))`,
familyID,
ownerUserID,
).Error
require.NoError(t, err)
}
func insertTestFamilyMember(t *testing.T, db *gorm.DB, familyID, userID int64, role, status uint8, joinSource string) {
t.Helper()
err := db.Exec(`INSERT INTO "user_family_member"
(family_id, user_id, role, status, join_source, joined_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), datetime('now'))`,
familyID,
userID,
role,
status,
joinSource,
).Error
require.NoError(t, err)
}
// insertTestOrder 插入一条历史订单(status=2 表示已支付)
func insertTestOrder(t *testing.T, db *gorm.DB, userID, subscribeID int64, status uint8) {
t.Helper()
err := db.Exec(`INSERT INTO "order"
(user_id, order_no, type, status, subscribe_id, created_at, updated_at)
VALUES (?, ?, 1, ?, ?, datetime('now'), datetime('now'))`,
userID, "existing-order-no", status, subscribeID).Error
require.NoError(t, err)
}
func insertScopedTestOrder(t *testing.T, db *gorm.DB, orderNo string, userID, subscribeID int64, status uint8) {
t.Helper()
err := db.Exec(`INSERT INTO "order"
(user_id, order_no, type, status, subscribe_id, created_at, updated_at)
VALUES (?, ?, 1, ?, ?, datetime('now'), datetime('now'))`,
userID, orderNo, status, subscribeID).Error
require.NoError(t, err)
}
// buildPurchaseCtx 把 user 放入 context(模拟中间件行为)
func buildPurchaseCtx(u *user.User) context.Context {
return context.WithValue(context.Background(), constant.CtxKeyUser, u)
}
// TestPurchase_NewUserOnly_UserTooOld 验证:new_user_only=true,用户注册超过 24h → 返回 SubscribeNewUserOnly
func TestPurchase_NewUserOnly_UserTooOld(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const subID = int64(1)
const payID = int64(1)
insertTestSubscribe(t, db, subID, true) // new_user_only = true
insertTestPayment(t, db, payID)
// 用户注册 48 小时前 → 超出 24h 限制
u := insertTestUser(t, db, 100, time.Now().Add(-48*time.Hour))
ctx := buildPurchaseCtx(u)
logic := NewPurchaseLogic(ctx, svcCtx)
_, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
require.Error(t, err)
var errCode *xerr.CodeError
require.ErrorAs(t, err, &errCode)
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode(),
"注册超过24h应返回 SubscribeNewUserOnly 错误码")
// 验证订单未被创建
var count int64
db.Model(&modelOrder.Order{}).Where("user_id = ?", u.Id).Count(&count)
assert.Equal(t, int64(0), count, "用户注册超时,订单不应被创建")
}
// TestPurchase_NewUserOnly_AlreadyPurchased 验证:new_user_only=true,用户是新用户但已购买过
// → 允许下单(不拦截),但不享受新人折扣
func TestPurchase_NewUserOnly_AlreadyPurchased(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const subID = int64(2)
const payID = int64(1)
insertTestSubscribe(t, db, subID, true)
insertTestPayment(t, db, payID)
// 用户刚注册(2h前)→ 满足时间条件
u := insertTestUser(t, db, 200, time.Now().Add(-2*time.Hour))
// 但已有一条 status=2 的历史订单(已支付)
insertTestOrder(t, db, u.Id, subID, 2)
ctx := buildPurchaseCtx(u)
logic := NewPurchaseLogic(ctx, svcCtx)
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
// 不应被拦截,允许下单
require.NoError(t, err, "24h内已购用户应允许继续下单,不应返回错误")
require.NotNil(t, resp)
assert.NotEmpty(t, resp.OrderNo)
// 历史订单 +1(新增了一条)
var count int64
db.Model(&modelOrder.Order{}).Where("user_id = ?", u.Id).Count(&count)
assert.Equal(t, int64(2), count, "应新增一条订单")
// 新订单无折扣:Amount=Price=1000
var newOrder modelOrder.Order
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
assert.Equal(t, int64(1000), newOrder.Amount, "已购用户不享受新人折扣,Amount 应等于 Price")
assert.Equal(t, int64(0), newOrder.Discount, "Discount 应为 0")
}
// TestPurchase_NewUserOnly_Success 验证:new_user_only=true,新用户首次购买 → 成功创建订单
func TestPurchase_NewUserOnly_Success(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const subID = int64(3)
const payID = int64(1)
insertTestSubscribe(t, db, subID, true)
insertTestPayment(t, db, payID)
// 用户 1 小时前注册(新用户),且没有历史订单
u := insertTestUser(t, db, 300, time.Now().Add(-1*time.Hour))
ctx := buildPurchaseCtx(u)
logic := NewPurchaseLogic(ctx, svcCtx)
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
require.NoError(t, err)
require.NotNil(t, resp)
assert.NotEmpty(t, resp.OrderNo, "新用户首次购买应成功,返回订单号")
// 验证订单已写入数据库
var o modelOrder.Order
err = db.Where("order_no = ?", resp.OrderNo).First(&o).Error
require.NoError(t, err)
assert.Equal(t, u.Id, o.UserId)
assert.Equal(t, subID, o.SubscribeId)
}
// TestPurchase_NewUserOnly_Disabled 验证:new_user_only=false 时,老用户也能正常购买
func TestPurchase_NewUserOnly_Disabled(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const subID = int64(4)
const payID = int64(1)
insertTestSubscribe(t, db, subID, false) // new_user_only = false
insertTestPayment(t, db, payID)
// 注册 30 天的老用户
u := insertTestUser(t, db, 400, time.Now().Add(-30*24*time.Hour))
ctx := buildPurchaseCtx(u)
logic := NewPurchaseLogic(ctx, svcCtx)
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
require.NoError(t, err)
require.NotNil(t, resp)
assert.NotEmpty(t, resp.OrderNo, "new_user_only=false时老用户应能正常购买")
}
// TestPurchase_SingleMode_PendingOldOrderCancelled 验证:单订阅模式下,已有 pending 订单时
// 第二次下单应关闭旧单并创建新单(而非复用旧单)
func TestPurchase_SingleMode_PendingOldOrderCancelled(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
svcCtx.Config.Subscribe.SingleModel = true
const subID = int64(5)
const payID = int64(1)
insertTestSubscribe(t, db, subID, false)
insertTestPayment(t, db, payID)
u := insertTestUser(t, db, 500, time.Now().Add(-1*time.Hour))
ctx := buildPurchaseCtx(u)
// 第一次下单(pending
logic := NewPurchaseLogic(ctx, svcCtx)
resp1, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
require.NoError(t, err)
require.NotNil(t, resp1)
firstOrderNo := resp1.OrderNo
assert.NotEmpty(t, firstOrderNo)
// 确认第一单 pending
var firstOrder modelOrder.Order
require.NoError(t, db.Where("order_no = ?", firstOrderNo).First(&firstOrder).Error)
assert.Equal(t, uint8(1), firstOrder.Status, "第一单应为 pending")
// 第二次下单(不同 quantity
logic2 := NewPurchaseLogic(ctx, svcCtx)
resp2, err := logic2.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 3,
})
require.NoError(t, err)
require.NotNil(t, resp2)
secondOrderNo := resp2.OrderNo
// 新单与旧单不同
assert.NotEqual(t, firstOrderNo, secondOrderNo, "第二次下单应创建新订单,不复用旧单")
// 旧单应被关闭(status=3
var closedOrder modelOrder.Order
require.NoError(t, db.Where("order_no = ?", firstOrderNo).First(&closedOrder).Error)
assert.Equal(t, uint8(3), closedOrder.Status, "旧 pending 单应被关闭")
// 新单的 quantity 应为 3
var newOrder modelOrder.Order
require.NoError(t, db.Where("order_no = ?", secondOrderNo).First(&newOrder).Error)
assert.Equal(t, int64(3), newOrder.Quantity, "新单 quantity 应为 3")
assert.Equal(t, uint8(1), newOrder.Status, "新单应为 pending 状态")
}
// TestPurchase_SingleMode_NoPendingOrder 验证:单订阅模式下,没有旧 pending 单时正常创建
func TestPurchase_SingleMode_NoPendingOrder(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
svcCtx.Config.Subscribe.SingleModel = true
const subID = int64(6)
const payID = int64(1)
insertTestSubscribe(t, db, subID, false)
insertTestPayment(t, db, payID)
u := insertTestUser(t, db, 600, time.Now().Add(-1*time.Hour))
ctx := buildPurchaseCtx(u)
logic := NewPurchaseLogic(ctx, svcCtx)
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 2,
})
require.NoError(t, err)
require.NotNil(t, resp)
assert.NotEmpty(t, resp.OrderNo, "无旧 pending 单时应正常创建新单")
var o modelOrder.Order
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&o).Error)
assert.Equal(t, int64(2), o.Quantity)
assert.Equal(t, uint8(1), o.Status)
}
// TestPurchase_NewUserOnly_AlreadyPurchased_NoBlock 验证:new_user_only=true 套餐,
// 24小时内但已购买过 → 允许下单,但不享受新人折扣(Discount=0Amount=Price
func TestPurchase_NewUserOnly_AlreadyPurchased_NoBlock(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const subID = int64(7)
const payID = int64(1)
// 套餐:unit_price=1000discount=[{quantity:1,discount:80,new_user_only:true}]
insertTestSubscribe(t, db, subID, true)
insertTestPayment(t, db, payID)
// 用户 1 小时前注册(新用户),但已有一条成功订单(status=2)
u := insertTestUser(t, db, 700, time.Now().Add(-1*time.Hour))
insertTestOrder(t, db, u.Id, subID, 2)
ctx := buildPurchaseCtx(u)
logic := NewPurchaseLogic(ctx, svcCtx)
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
// 不应被拦截
require.NoError(t, err, "24h内已购用户不应被拦截,应允许下单")
require.NotNil(t, resp)
assert.NotEmpty(t, resp.OrderNo)
// 验证订单金额:无折扣,Amount=Price=1000Discount=0
var newOrder modelOrder.Order
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
assert.Equal(t, int64(1000), newOrder.Price, "Price 应为原价 1000")
assert.Equal(t, int64(1000), newOrder.Amount, "已购用户不享受新人折扣,Amount 应等于 Price")
assert.Equal(t, int64(0), newOrder.Discount, "Discount 应为 0")
}
// TestPurchase_NewUserOnly_FirstPurchase_HasDiscount 验证:new_user_only=true 套餐,
// 24小时内首次购买 → 允许下单且享受新人折扣
func TestPurchase_NewUserOnly_FirstPurchase_HasDiscount(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const subID = int64(8)
const payID = int64(1)
// 套餐:unit_price=1000discount=[{quantity:1,discount:80,new_user_only:true}]8折)
insertTestSubscribe(t, db, subID, true)
insertTestPayment(t, db, payID)
// 用户 1 小时前注册,无历史订单
u := insertTestUser(t, db, 800, time.Now().Add(-1*time.Hour))
ctx := buildPurchaseCtx(u)
logic := NewPurchaseLogic(ctx, svcCtx)
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
require.NoError(t, err)
require.NotNil(t, resp)
var newOrder modelOrder.Order
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
assert.Equal(t, int64(1000), newOrder.Price, "Price 应为原价 1000")
assert.Equal(t, int64(900), newOrder.Amount, "首次购买应享受9折,Amount=900")
assert.Equal(t, int64(100), newOrder.Discount, "折扣金额应为 100")
}
func TestPurchase_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const (
subID = int64(9)
payID = int64(1)
ownerUserID = int64(901)
memberUserID = int64(902)
familyID = int64(99)
)
insertTestSubscribe(t, db, subID, true)
insertTestPayment(t, db, payID)
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
insertTestUser(t, db, memberUserID, time.Now().Add(-72*time.Hour))
insertTestDevice(t, db, memberUserID, "device-eligibility-old", time.Now().Add(-72*time.Hour))
insertTestFamily(t, db, familyID, ownerUserID)
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
logic := NewPurchaseLogic(buildPurchaseCtx(owner), svcCtx)
_, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
require.Error(t, err)
var errCode *xerr.CodeError
require.ErrorAs(t, err, &errCode)
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode())
}
func TestPurchase_NewUserOnly_BindEmailScopeSharesHistory(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const (
subID = int64(10)
payID = int64(1)
ownerUserID = int64(1001)
memberUserID = int64(1002)
familyID = int64(109)
)
insertTestSubscribe(t, db, subID, true)
insertTestPayment(t, db, payID)
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
insertTestUser(t, db, memberUserID, time.Now().Add(-2*time.Hour))
insertTestDevice(t, db, memberUserID, "device-eligibility-shared", time.Now().Add(-2*time.Hour))
insertTestFamily(t, db, familyID, ownerUserID)
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
insertScopedTestOrder(t, db, "existing-scope-order", memberUserID, subID, 2)
logic := NewPurchaseLogic(buildPurchaseCtx(owner), svcCtx)
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
SubscribeId: subID,
Payment: payID,
Quantity: 1,
})
require.NoError(t, err)
require.NotNil(t, resp)
var newOrder modelOrder.Order
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
assert.Equal(t, int64(1000), newOrder.Amount)
assert.Equal(t, int64(0), newOrder.Discount)
}
func ensureRenewalSubscribeColumns(t *testing.T, db *gorm.DB) {
t.Helper()
for _, sql := range []string{
`ALTER TABLE "user_subscribe" ADD COLUMN node_group_id INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE "user_subscribe" ADD COLUMN group_locked TINYINT NOT NULL DEFAULT 0`,
`ALTER TABLE "user_subscribe" ADD COLUMN expired_download INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE "user_subscribe" ADD COLUMN expired_upload INTEGER NOT NULL DEFAULT 0`,
} {
require.NoError(t, db.Exec(sql).Error)
}
}
func insertRenewalUserSubscribe(t *testing.T, db *gorm.DB, id, userID, orderID, subscribeID int64, token, uuid string, start, expire time.Time) {
t.Helper()
require.NoError(t, db.Exec(`INSERT INTO "user_subscribe"
(id, user_id, order_id, subscribe_id, start_time, expire_time, traffic, download, upload, token, uuid, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, 1, ?, ?)`,
id,
userID,
orderID,
subscribeID,
start.UTC().Format("2006-01-02 15:04:05"),
expire.UTC().Format("2006-01-02 15:04:05"),
token,
uuid,
start.UTC().Format("2006-01-02 15:04:05"),
time.Now().UTC().Format("2006-01-02 15:04:05"),
).Error)
}
func TestRenewalMemberRequestRedirectsToOwnerSubscribe(t *testing.T) {
db := setupNewUserOnlyDB(t)
ensureRenewalSubscribeColumns(t, db)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const (
planID = int64(1)
paymentID = int64(2)
ownerUserID = int64(29650)
memberID = int64(20003)
familyID = int64(88001)
memberSubID = int64(10013)
ownerSubID = int64(14074)
)
insertTestSubscribe(t, db, planID, false)
insertTestPayment(t, db, paymentID)
member := insertTestUser(t, db, memberID, time.Now().Add(-24*time.Hour))
insertTestUser(t, db, ownerUserID, time.Now().Add(-24*time.Hour))
insertTestFamily(t, db, familyID, ownerUserID)
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
insertTestFamilyMember(t, db, familyID, memberID, user.FamilyRoleMember, user.FamilyMemberActive, "manual_invite")
insertRenewalUserSubscribe(t, db, memberSubID, memberID, 7448, planID, "member-token-10013", "member-uuid-10013",
time.Date(2026, 4, 23, 19, 6, 40, 0, time.UTC),
time.Date(2026, 4, 30, 19, 6, 40, 0, time.UTC),
)
insertRenewalUserSubscribe(t, db, ownerSubID, ownerUserID, 9999, planID, "owner-token-14074", "owner-uuid-14074",
time.Date(2026, 4, 30, 13, 39, 33, 0, time.UTC),
time.Date(2026, 5, 30, 16, 0, 0, 0, time.UTC),
)
resp, err := NewRenewalLogic(buildPurchaseCtx(member), svcCtx).Renewal(&types.RenewalOrderRequest{
UserSubscribeID: memberSubID,
Payment: paymentID,
Quantity: 30,
})
require.NoError(t, err)
require.NotNil(t, resp)
var created modelOrder.Order
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&created).Error)
assert.Equal(t, memberID, created.UserId)
assert.Equal(t, ownerUserID, created.SubscriptionUserId)
assert.Equal(t, int64(9999), created.ParentId)
assert.Equal(t, "owner-token-14074", created.SubscribeToken)
}
func TestPreCreateOrder_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const (
subID = int64(11)
ownerUserID = int64(1101)
memberUserID = int64(1102)
familyID = int64(119)
)
insertTestSubscribe(t, db, subID, true)
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
insertTestUser(t, db, memberUserID, time.Now().Add(-96*time.Hour))
insertTestDevice(t, db, memberUserID, "device-precreate-old", time.Now().Add(-96*time.Hour))
insertTestFamily(t, db, familyID, ownerUserID)
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
logic := NewPreCreateOrderLogic(buildPurchaseCtx(owner), svcCtx)
_, err := logic.PreCreateOrder(&types.PurchaseOrderRequest{
SubscribeId: subID,
Quantity: 1,
})
require.Error(t, err)
var errCode *xerr.CodeError
require.ErrorAs(t, err, &errCode)
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode())
}
func TestPreCreateOrder_NewUserOnly_OrdinaryFamilyMemberDoesNotAffectEligibility(t *testing.T) {
db := setupNewUserOnlyDB(t)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const (
subID = int64(12)
ownerUserID = int64(1201)
memberUserID = int64(1202)
familyID = int64(129)
)
insertTestSubscribe(t, db, subID, true)
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
insertTestUser(t, db, memberUserID, time.Now().Add(-96*time.Hour))
insertTestDevice(t, db, memberUserID, "device-precreate-ordinary", time.Now().Add(-96*time.Hour))
insertTestFamily(t, db, familyID, ownerUserID)
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "manual_invite")
logic := NewPreCreateOrderLogic(buildPurchaseCtx(owner), svcCtx)
resp, err := logic.PreCreateOrder(&types.PurchaseOrderRequest{
SubscribeId: subID,
Quantity: 1,
})
require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, int64(900), resp.Amount)
assert.Equal(t, int64(100), resp.Discount)
}
@@ -0,0 +1,130 @@
package order
import (
"testing"
"time"
modelOrder "github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/types"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func ensureRenewalInviteSubscribeColumns(t *testing.T, db *gorm.DB) {
t.Helper()
for _, sql := range []string{
`ALTER TABLE "user_subscribe" ADD COLUMN node_group_id INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE "user_subscribe" ADD COLUMN group_locked TINYINT NOT NULL DEFAULT 0`,
`ALTER TABLE "user_subscribe" ADD COLUMN expired_download INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE "user_subscribe" ADD COLUMN expired_upload INTEGER NOT NULL DEFAULT 0`,
} {
require.NoError(t, db.Exec(sql).Error)
}
}
func insertRenewalInviteUserSubscribe(t *testing.T, db *gorm.DB, id, userID, orderID, subscribeID int64, token, uuid string, start, expire time.Time) {
t.Helper()
require.NoError(t, db.Exec(`INSERT INTO "user_subscribe"
(id, user_id, order_id, subscribe_id, start_time, expire_time, traffic, download, upload, token, uuid, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, 1, ?, ?)`,
id,
userID,
orderID,
subscribeID,
start.UTC().Format("2006-01-02 15:04:05"),
expire.UTC().Format("2006-01-02 15:04:05"),
token,
uuid,
start.UTC().Format("2006-01-02 15:04:05"),
time.Now().UTC().Format("2006-01-02 15:04:05"),
).Error)
}
func TestRenewalFirstSuccessfulPaymentMarksOrderAsNew(t *testing.T) {
db := setupNewUserOnlyDB(t)
ensureRenewalInviteSubscribeColumns(t, db)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const (
planID = int64(1)
paymentID = int64(2)
ownerUserID = int64(31059)
memberID = int64(31057)
familyID = int64(5638)
ownerSubID = int64(14672)
)
insertTestSubscribe(t, db, planID, false)
insertTestPayment(t, db, paymentID)
member := insertTestUser(t, db, memberID, time.Now().Add(-2*time.Hour))
insertTestUser(t, db, ownerUserID, time.Now().Add(-2*time.Hour))
insertTestFamily(t, db, familyID, ownerUserID)
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
insertTestFamilyMember(t, db, familyID, memberID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
insertRenewalInviteUserSubscribe(t, db, ownerSubID, ownerUserID, 0, planID, "owner-trial-token", "owner-trial-uuid",
time.Date(2026, 5, 2, 15, 53, 39, 0, time.UTC),
time.Date(2026, 5, 9, 0, 0, 0, 0, time.UTC),
)
resp, err := NewRenewalLogic(buildPurchaseCtx(member), svcCtx).Renewal(&types.RenewalOrderRequest{
UserSubscribeID: ownerSubID,
Payment: paymentID,
Quantity: 1,
})
require.NoError(t, err)
require.NotNil(t, resp)
var created modelOrder.Order
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&created).Error)
require.Equal(t, uint8(2), created.Type)
require.True(t, created.IsNew)
require.Equal(t, memberID, created.UserId)
require.Equal(t, ownerUserID, created.SubscriptionUserId)
require.Equal(t, "owner-trial-token", created.SubscribeToken)
}
func TestRenewalExistingSuccessfulPaymentMarksOrderAsNotNew(t *testing.T) {
db := setupNewUserOnlyDB(t)
ensureRenewalInviteSubscribeColumns(t, db)
rds, mr := setupNewUserOnlyRedis(t)
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
const (
planID = int64(1)
paymentID = int64(2)
ownerUserID = int64(41059)
memberID = int64(41057)
familyID = int64(6638)
ownerSubID = int64(24672)
)
insertTestSubscribe(t, db, planID, false)
insertTestPayment(t, db, paymentID)
member := insertTestUser(t, db, memberID, time.Now().Add(-2*time.Hour))
insertTestUser(t, db, ownerUserID, time.Now().Add(-2*time.Hour))
insertTestFamily(t, db, familyID, ownerUserID)
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
insertTestFamilyMember(t, db, familyID, memberID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
insertScopedTestOrder(t, db, "previous-successful-order", memberID, planID, 5)
insertRenewalInviteUserSubscribe(t, db, ownerSubID, ownerUserID, 0, planID, "owner-renewal-token", "owner-renewal-uuid",
time.Date(2026, 5, 2, 15, 53, 39, 0, time.UTC),
time.Date(2026, 5, 9, 0, 0, 0, 0, time.UTC),
)
resp, err := NewRenewalLogic(buildPurchaseCtx(member), svcCtx).Renewal(&types.RenewalOrderRequest{
UserSubscribeID: ownerSubID,
Payment: paymentID,
Quantity: 1,
})
require.NoError(t, err)
require.NotNil(t, resp)
var created modelOrder.Order
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&created).Error)
require.Equal(t, uint8(2), created.Type)
require.False(t, created.IsNew)
}
+95 -2
View File
@@ -41,6 +41,67 @@ func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLo
}
}
func (l *RenewalLogic) resolveRenewalTargetSubscribe(requested *user.SubscribeDetails, entitlement *commonLogic.EntitlementContext, currentUserID int64) (*user.SubscribeDetails, error) {
if requested == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "requested user subscribe is empty")
}
effectiveUserID := currentUserID
if entitlement != nil && entitlement.EffectiveUserID > 0 {
effectiveUserID = entitlement.EffectiveUserID
}
if effectiveUserID == currentUserID {
if requested.UserId != currentUserID {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "user subscribe does not belong to current user")
}
return requested, nil
}
if requested.UserId != currentUserID && requested.UserId != effectiveUserID {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "user subscribe does not belong to current family entitlement")
}
if requested.UserId == effectiveUserID {
return requested, nil
}
ownerSubscribe, err := l.findOwnerRenewalSubscribe(effectiveUserID, requested.SubscribeId)
if err != nil {
return nil, err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "renewal_target_resolved",
"[SubscriptionFlow] renewal target redirected to family owner subscription",
logger.Field("user_id", currentUserID),
logger.Field("effective_user_id", effectiveUserID),
logger.Field("requested_user_subscribe_id", requested.Id),
logger.Field("requested_subscribe_owner_user_id", requested.UserId),
logger.Field("resolved_user_subscribe_id", ownerSubscribe.Id),
logger.Field("resolved_subscribe_owner_user_id", ownerSubscribe.UserId),
logger.Field("subscribe_id", requested.SubscribeId),
)
return ownerSubscribe, nil
}
func (l *RenewalLogic) findOwnerRenewalSubscribe(ownerUserID, subscribeID int64) (*user.SubscribeDetails, error) {
var target user.SubscribeDetails
err := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Subscribe{}).
Preload("Subscribe").
Where("user_id = ? AND subscribe_id = ? AND token != ''", ownerUserID, subscribeID).
Where("status IN ?", []int64{0, 1, 2, 3}).
Order("expire_time DESC").
Order("updated_at DESC").
Order("id DESC").
First(&target).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "owner subscribe not found for renewal")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find owner subscribe error: %v", err.Error())
}
return &target, nil
}
// Renewal processes subscription renewal orders including discount calculation,
// coupon validation, gift amount deduction, fee calculation, and order creation
func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) {
@@ -54,6 +115,17 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
if entErr != nil {
return nil, entErr
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_create_start",
"[SubscriptionFlow] renewal order creation started",
logger.Field("order_kind", "renewal"),
logger.Field("user_id", u.Id),
logger.Field("effective_user_id", entitlement.EffectiveUserID),
logger.Field("requested_user_subscribe_id", req.UserSubscribeID),
logger.Field("quantity", req.Quantity),
logger.Field("payment_id", req.Payment),
logger.Field("coupon", req.Coupon),
)
if req.Quantity <= 0 {
l.Debugf("[Renewal] Quantity is less than or equal to 0, setting to 1")
req.Quantity = 1
@@ -66,11 +138,15 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
}
orderNo := tool.GenerateTradeNo()
// find user subscribe
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
// find requested user subscribe, then resolve it to the real entitlement owner.
requestedSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error())
}
userSubscribe, err := l.resolveRenewalTargetSubscribe(requestedSubscribe, entitlement, u.Id)
if err != nil {
return nil, err
}
// find subscription
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId)
if err != nil {
@@ -175,6 +251,12 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
}
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
if err != nil {
l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error())
}
// create order
orderInfo := order.Order{
UserId: u.Id,
@@ -196,6 +278,7 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
SubscribeId: userSubscribe.SubscribeId,
SubscribeToken: userSubscribe.Token,
AppAccountToken: uuid.New().String(),
IsNew: isNew,
}
// Database transaction
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
@@ -235,6 +318,16 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo))
return nil, errors.Wrapf(err, "insert order error: %v", err.Error())
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created",
"[SubscriptionFlow] renewal order persisted",
append(commonLogic.OrderTraceFields(&orderInfo),
logger.Field("requested_user_subscribe_id", req.UserSubscribeID),
logger.Field("requested_subscribe_owner_user_id", requestedSubscribe.UserId),
logger.Field("resolved_user_subscribe_id", userSubscribe.Id),
logger.Field("resolved_subscribe_owner_user_id", userSubscribe.UserId),
)...,
)
// Deferred task
payload := queue.DeferCloseOrderPayload{
OrderNo: orderInfo.OrderNo,
@@ -9,6 +9,7 @@ import (
"strings"
"time"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/report"
"github.com/perfect-panel/server/pkg/constant"
@@ -75,6 +76,14 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
l.Logger.Error("[PurchaseCheckout] Database query error", logger.Field("error", err.Error()), logger.Field("payment", orderInfo.Method))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error())
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_start",
"[SubscriptionFlow] checkout started",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", paymentConfig.Platform),
logger.Field("has_return_url", req.ReturnUrl != ""),
)...,
)
// Route to appropriate payment handler based on payment platform
switch paymentPlatform.ParsePlatform(orderInfo.Method) {
case paymentPlatform.AppleIAP:
@@ -83,6 +92,14 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
Type: "apple_iap",
ProductIds: []string{productId},
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_response_ready",
"[SubscriptionFlow] checkout response prepared",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", paymentConfig.Platform),
logger.Field("checkout_type", resp.Type),
logger.Field("product_ids", resp.ProductIds),
)...,
)
return resp, nil
case paymentPlatform.EPay:
// Process EPay payment - generates payment URL for redirect
@@ -157,6 +174,16 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
l.Errorw("[PurchaseCheckout] payment method not found", logger.Field("method", orderInfo.Method))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment method not found")
}
if resp != nil {
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_response_ready",
"[SubscriptionFlow] checkout response prepared",
append(commonLogic.OrderTraceFields(orderInfo),
logger.Field("payment_platform", paymentConfig.Platform),
logger.Field("checkout_type", resp.Type),
)...,
)
}
return
}
@@ -503,6 +530,9 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount
func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) error {
var userInfo user.User
var err error
var giftUsed int64
var balanceUsed int64
paymentPath := "balance"
if o.Amount == 0 {
// No payment required for zero-amount orders
l.Logger.Info(
@@ -518,6 +548,13 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err
logger.Field("userId", u.Id))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update order status error: %s", err.Error())
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
"[SubscriptionFlow] order marked paid without external payment",
append(commonLogic.OrderTraceFields(o),
logger.Field("payment_path", "zero_amount"),
)...,
)
paymentPath = "zero_amount"
goto activation
}
@@ -536,7 +573,6 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err
}
// Calculate payment distribution: prioritize gift amount first
var giftUsed, balanceUsed int64
remainingAmount := o.Amount
if userInfo.GiftAmount >= remainingAmount {
@@ -621,6 +657,15 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
"[SubscriptionFlow] balance payment settled and order marked paid",
append(commonLogic.OrderTraceFields(o),
logger.Field("payment_path", "balance"),
logger.Field("gift_used", giftUsed),
logger.Field("balance_used", balanceUsed),
)...,
)
activation:
// Enqueue order activation task for immediate processing
payload := queueType.ForthwithActivateOrderPayload{
@@ -639,6 +684,13 @@ activation:
return err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
"[SubscriptionFlow] activation task enqueued after checkout payment",
append(commonLogic.OrderTraceFields(o),
logger.Field("payment_path", paymentPath),
)...,
)
l.Logger.Info("[PurchaseCheckout] Balance payment completed successfully",
logger.Field("orderNo", o.OrderNo),
logger.Field("userId", u.Id))
@@ -0,0 +1,370 @@
package recovery
import (
"context"
"errors"
"strings"
"time"
"github.com/google/uuid"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
ordermodel "github.com/perfect-panel/server/internal/model/order"
recoverymodel "github.com/perfect-panel/server/internal/model/recovery"
subscribemodel "github.com/perfect-panel/server/internal/model/subscribe"
usermodel "github.com/perfect-panel/server/internal/model/user"
recoveryconfig "github.com/perfect-panel/server/internal/recovery"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/authmethod"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/uuidx"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
recoverySuccessMessage = "恢复完成,请使用该邮箱验证码登录查看订阅。"
recoveryAlreadyDoneMessage = "该订单已恢复,请使用恢复时绑定的邮箱登录查看订阅。"
recoveryInvalidOrderMessage = "订单信息无效,请确认订单号是否正确。"
recoveryInvalidCodeMessage = "邮箱验证码错误或已过期,请重新获取验证码。"
recoverySystemErrorMessage = "恢复暂时失败,请稍后再试或联系客服处理。"
)
type RecoverOrderLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRecoverOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RecoverOrderLogic {
return &RecoverOrderLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RecoverOrderLogic) RecoverOrder(req *types.RecoverOrderRequest) (*types.RecoverOrderResponse, error) {
orderNo := strings.TrimSpace(req.OrderNo)
email := strings.ToLower(strings.TrimSpace(req.Email))
code := strings.TrimSpace(req.Code)
if orderNo == "" || email == "" || code == "" {
return recoveryFailed(recoveryInvalidOrderMessage), nil
}
if err := l.verifyEmailCode(email, code); err != nil {
return recoveryFailed(recoveryInvalidCodeMessage), nil
}
item, ok, err := recoveryconfig.FindOrder(orderNo)
if err != nil {
l.Errorw("[RecoverOrder] load recovery config failed", logger.Field("error", err.Error()))
return recoveryFailed(recoverySystemErrorMessage), nil
}
if !ok {
return recoveryFailed(recoveryInvalidOrderMessage), nil
}
if item.OrderStatus != "" && item.OrderStatus != "success" {
return recoveryFailed(recoveryInvalidOrderMessage), nil
}
if item.Quantity <= 0 || item.OrderMoneyCents <= 0 {
return recoveryFailed(recoveryInvalidOrderMessage), nil
}
claimOrderNo := item.OutOrderNo
if claimOrderNo == "" {
claimOrderNo = item.OrderNo
}
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, item.SubscribeId)
if err != nil {
l.Errorw("[RecoverOrder] subscribe not found", logger.Field("subscribe_id", item.SubscribeId), logger.Field("error", err.Error()))
return recoveryFailed(recoverySystemErrorMessage), nil
}
var (
claim recoverymodel.OrderRecoveryClaim
order ordermodel.Order
userSub usermodel.Subscribe
alreadyRecovered bool
)
err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Model(&recoverymodel.OrderRecoveryClaim{}).
Where("order_no = ?", claimOrderNo).
First(&claim).Error; err == nil {
alreadyRecovered = true
return nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
u, err := l.findOrCreateUser(tx, email)
if err != nil {
return err
}
claim = recoverymodel.OrderRecoveryClaim{
OrderNo: claimOrderNo,
Email: email,
UserId: u.Id,
SubscribeId: item.SubscribeId,
ClaimedAt: time.Now(),
}
if err := tx.Model(&recoverymodel.OrderRecoveryClaim{}).Create(&claim).Error; err != nil {
return err
}
order, err = l.createRecoveredOrder(tx, u.Id, item, sub)
if err != nil {
return err
}
userSub, err = l.createOrExtendSubscribe(tx, u.Id, order.Id, item, sub)
if err != nil {
return err
}
claim.UserSubscribeId = userSub.Id
return tx.Model(&recoverymodel.OrderRecoveryClaim{}).
Where("id = ?", claim.Id).
Updates(map[string]any{
"order_id": order.Id,
"user_subscribe_id": userSub.Id,
}).Error
})
if err != nil {
l.Errorw("[RecoverOrder] recover failed", logger.Field("order_no", orderNo), logger.Field("error", err.Error()))
return recoveryFailed(recoverySystemErrorMessage), nil
}
if alreadyRecovered {
return &types.RecoverOrderResponse{
Success: true,
Message: recoveryAlreadyDoneMessage,
}, nil
}
l.clearCaches(&userSub, item.SubscribeId, email)
l.Infof("[RecoverOrder] recovered order subscription order_no=%s email=%s user_id=%d subscribe_id=%d user_subscribe_id=%d",
orderNo, email, claim.UserId, claim.SubscribeId, claim.UserSubscribeId)
return &types.RecoverOrderResponse{
Success: true,
Message: recoverySuccessMessage,
}, nil
}
func recoveryFailed(message string) *types.RecoverOrderResponse {
return &types.RecoverOrderResponse{
Success: false,
Message: message,
}
}
func (l *RecoverOrderLogic) createRecoveredOrder(tx *gorm.DB, userId int64, item recoveryconfig.Order, sub *subscribemodel.Subscribe) (ordermodel.Order, error) {
var existing ordermodel.Order
orderNo := item.OutOrderNo
if orderNo == "" {
orderNo = item.OrderNo
}
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Model(&ordermodel.Order{}).
Where("order_no = ?", orderNo).
First(&existing).Error; err == nil {
if existing.UserId != userId {
return ordermodel.Order{}, errors.New("recovered order number already belongs to another user")
}
return existing, nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return ordermodel.Order{}, err
}
paidAt := time.Now()
if item.PaidAt > 0 {
paidAt = time.Unix(item.PaidAt, 0)
}
quantity := item.Quantity
if quantity <= 0 {
quantity = 1
}
price := sub.UnitPrice * quantity
amount := item.OrderMoneyCents
if amount <= 0 {
amount = price
}
o := ordermodel.Order{
UserId: userId,
SubscriptionUserId: userId,
OrderNo: orderNo,
Type: 1,
Quantity: quantity,
Price: price,
Amount: amount,
Discount: price - amount,
PaymentId: 2,
Method: "EPay",
TradeNo: strings.TrimSpace(item.PaymentOrderNo),
Status: 5,
SubscribeId: item.SubscribeId,
IsNew: true,
CreatedAt: paidAt,
UpdatedAt: paidAt,
}
if o.Discount < 0 {
o.Discount = 0
}
if err := tx.Model(&ordermodel.Order{}).Create(&o).Error; err != nil {
return ordermodel.Order{}, err
}
return o, nil
}
func (l *RecoverOrderLogic) createOrExtendSubscribe(tx *gorm.DB, userId int64, orderId int64, item recoveryconfig.Order, sub *subscribemodel.Subscribe) (usermodel.Subscribe, error) {
now := time.Now()
var existing usermodel.Subscribe
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Model(&usermodel.Subscribe{}).
Where("user_id = ? AND subscribe_id = ?", userId, item.SubscribeId).
Order("expire_time DESC").
Order("id DESC").
First(&existing).Error
if err == nil {
base := now
if existing.ExpireTime.After(now) {
base = existing.ExpireTime
}
existing.OrderId = orderId
existing.ExpireTime = base.Add(time.Duration(item.Quantity) * 24 * time.Hour)
existing.Status = 1
existing.FinishedAt = nil
if strings.TrimSpace(item.Note) != "" {
existing.Note = strings.TrimSpace(item.Note)
}
if err := tx.Model(&usermodel.Subscribe{}).Where("id = ?", existing.Id).Save(&existing).Error; err != nil {
return usermodel.Subscribe{}, err
}
return existing, nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return usermodel.Subscribe{}, err
}
orderNo := item.OutOrderNo
if orderNo == "" {
orderNo = item.OrderNo
}
userSub := usermodel.Subscribe{
UserId: userId,
OrderId: orderId,
SubscribeId: item.SubscribeId,
NodeGroupId: sub.NodeGroupId,
StartTime: now,
ExpireTime: now.Add(time.Duration(item.Quantity) * 24 * time.Hour),
Traffic: sub.Traffic,
Download: 0,
Upload: 0,
Token: uuidx.SubscribeToken(orderNo),
UUID: uuid.New().String(),
Status: 1,
Note: strings.TrimSpace(item.Note),
}
if err := tx.Model(&usermodel.Subscribe{}).Create(&userSub).Error; err != nil {
return usermodel.Subscribe{}, err
}
return userSub, nil
}
func (l *RecoverOrderLogic) verifyEmailCode(email string, code string) error {
checker := commonLogic.NewCheckVerificationCodeLogic(l.ctx, l.svcCtx)
resp, err := checker.CheckVerificationCodeWithBehavior(&types.CheckVerificationCodeRequest{
Method: authmethod.Email,
Account: email,
Code: code,
Type: uint8(constant.Security),
}, commonLogic.VerifyCodeCheckBehavior{
Source: "order_recovery",
Consume: true,
AllowSceneFallback: true,
})
if err != nil {
return err
}
if resp == nil || !resp.Status {
return errors.New("invalid verification code")
}
return nil
}
func (l *RecoverOrderLogic) findOrCreateUser(tx *gorm.DB, email string) (*usermodel.User, error) {
var auth usermodel.AuthMethods
if err := tx.Model(&usermodel.AuthMethods{}).
Where("auth_type = ? AND auth_identifier = ?", authmethod.Email, email).
First(&auth).Error; err == nil {
var u usermodel.User
if err := tx.Model(&usermodel.User{}).Where("id = ?", auth.UserId).First(&u).Error; err != nil {
return nil, err
}
u.AuthMethods = []usermodel.AuthMethods{auth}
return &u, nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
u := &usermodel.User{
Password: tool.EncodePassWord(uuid.NewString()),
Algo: "default",
AuthMethods: []usermodel.AuthMethods{
{
AuthType: authmethod.Email,
AuthIdentifier: email,
Verified: true,
},
},
}
if err := tx.Create(u).Error; err != nil {
return nil, err
}
referCode := uuidx.UserInviteCode(u.Id)
if err := tx.Model(&usermodel.User{}).Where("id = ?", u.Id).Update("refer_code", referCode).Error; err != nil {
return nil, err
}
u.ReferCode = referCode
return u, nil
}
func (l *RecoverOrderLogic) clearCaches(userSub *usermodel.Subscribe, subscribeId int64, email string) {
if userSub != nil && userSub.Id > 0 {
if err := l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil {
l.Errorw("[RecoverOrder] clear user subscribe cache failed", logger.Field("error", err.Error()))
}
if userSub.UserId > 0 {
if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, &usermodel.User{
Id: userSub.UserId,
AuthMethods: []usermodel.AuthMethods{
{
UserId: userSub.UserId,
AuthType: authmethod.Email,
AuthIdentifier: email,
},
},
}); err != nil {
l.Errorw("[RecoverOrder] clear user cache failed", logger.Field("error", err.Error()))
}
}
}
if subscribeId > 0 {
if err := l.svcCtx.SubscribeModel.ClearCache(l.ctx, subscribeId); err != nil {
l.Errorw("[RecoverOrder] clear subscribe cache failed", logger.Field("error", err.Error()))
}
}
if l.svcCtx.NodeModel != nil {
if err := l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
l.Errorw("[RecoverOrder] clear server cache failed", logger.Field("error", err.Error()))
}
}
}
@@ -0,0 +1,103 @@
package recovery
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/config"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/limit"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/random"
"github.com/perfect-panel/server/pkg/xerr"
queue "github.com/perfect-panel/server/queue/types"
"github.com/pkg/errors"
)
type SendCodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSendCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendCodeLogic {
return &SendCodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *SendCodeLogic) SendCode(req *types.RecoverySendCodeRequest) (*types.SendCodeResponse, error) {
email := strings.ToLower(strings.TrimSpace(req.Email))
if email == "" {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "email is required")
}
scene := constant.Security.String()
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email)
limiter := limit.NewPeriodLimit(60, 1, l.svcCtx.Redis, fmt.Sprintf("%s:%s:%s", config.SendIntervalKeyPrefix, "email", scene))
permit, err := limiter.Take(email)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit")
}
if !limiter.ParsePermitState(permit) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "send email too many requests")
}
permit, err = l.svcCtx.AuthLimiter.Take(fmt.Sprintf("%s:%s:%s", "email", scene, email))
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to take limit")
}
if !l.svcCtx.AuthLimiter.ParsePermitState(permit) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TodaySendCountExceedsLimit), "send email too many requests")
}
code := random.Key(6, 0)
expireTime := l.svcCtx.Config.VerifyCode.VerifyCodeExpireTime
if expireTime == 0 {
expireTime = 900
}
val, _ := json.Marshal(commonLogic.CacheKeyPayload{
Code: code,
LastAt: time.Now().Unix(),
})
if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*time.Duration(expireTime)).Err(); err != nil {
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code")
}
expireMinutes := expireTime / 60
taskPayload := queue.SendEmailPayload{
Type: queue.EmailTypeVerify,
Scene: scene,
Email: email,
Subject: "Verification code",
Content: map[string]interface{}{
"Type": uint8(constant.Security),
"SiteLogo": l.svcCtx.Config.Site.SiteLogo,
"SiteName": l.svcCtx.Config.Site.SiteName,
"Expire": expireMinutes,
"Code": code,
},
}
payload, err := json.Marshal(taskPayload)
if err != nil {
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload")
}
task := asynq.NewTask(queue.ForthwithSendEmail, payload, asynq.MaxRetry(3))
if _, err = l.svcCtx.Queue.Enqueue(task); err != nil {
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task")
}
resp := &types.SendCodeResponse{Status: true}
if l.svcCtx.Config.Model == constant.DevMode {
resp.Code = code
}
return resp, nil
}
@@ -8,6 +8,7 @@ import (
"time"
"github.com/perfect-panel/server/internal/config"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
@@ -43,11 +44,23 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_start",
"[SubscriptionFlow] email bind with verification started",
logger.Field("device_user_id", u.Id),
logger.Field("email", req.Email),
)
type payload struct {
Code string `json:"code"`
LastAt int64 `json:"lastAt"`
}
var verified bool
if req.Code == "202511" {
verified = true
} else if req.Code == "000000" && req.Email == "hifastday@hifast.com" {
// 000000 仅允许 hifastday@hifast.com 使用
verified = true
}
scenes := []string{constant.Security.String(), constant.Register.String()}
for _, scene := range scenes {
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, req.Email)
@@ -69,6 +82,12 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired")
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_code_verified",
"[SubscriptionFlow] email verification code accepted",
logger.Field("device_user_id", u.Id),
logger.Field("email", req.Email),
)
familyHelper := newFamilyBindingHelper(l.ctx, l.svcCtx)
currentEmailMethod, err := familyHelper.getUserEmailMethod(u.Id)
if err != nil {
@@ -115,19 +134,49 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
return nil, txErr
}
// Join family: email user as owner, device user as member
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "email_owner_created",
"[SubscriptionFlow] new email owner account created for bind flow",
logger.Field("device_user_id", u.Id),
logger.Field("owner_user_id", emailUser.Id),
logger.Field("email", req.Email),
)
// Join family: email user as owner, device user as member.
// For a newly-created email owner, preserve the device user's paid entitlement by moving
// its subscriptions to the email owner and issuing owner-side subscription tokens.
if err = familyHelper.validateJoinFamily(emailUser.Id, u.Id); err != nil {
return nil, err
}
joinResult, err := familyHelper.joinFamily(emailUser.Id, u.Id, "bind_email_with_verification")
joinResult, err := familyHelper.joinFamilyAndMoveSubscribesToOwner(emailUser.Id, u.Id, "bind_email_with_verification")
if err != nil {
return nil, err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "family_joined",
"[SubscriptionFlow] device user joined email owner family and subscriptions moved to owner with reset tokens",
logger.Field("device_user_id", u.Id),
logger.Field("owner_user_id", emailUser.Id),
logger.Field("family_id", joinResult.FamilyId),
logger.Field("email", req.Email),
)
token, err := l.refreshBindSessionToken(u.Id)
if err != nil {
return nil, err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_requested",
"[SubscriptionFlow] evaluating trial grant after email bind",
logger.Field("device_user_id", u.Id),
logger.Field("owner_user_id", emailUser.Id),
logger.Field("family_id", joinResult.FamilyId),
logger.Field("email", req.Email),
)
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, emailUser.Id, req.Email)
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_complete",
"[SubscriptionFlow] email bind with verification completed",
logger.Field("device_user_id", u.Id),
logger.Field("owner_user_id", emailUser.Id),
logger.Field("family_id", joinResult.FamilyId),
logger.Field("email", req.Email),
)
return &types.BindEmailWithVerificationResponse{
Success: true,
Message: "email user created and joined family",
@@ -146,16 +195,44 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
if err = familyHelper.validateJoinFamily(existingMethod.UserId, u.Id); err != nil {
return nil, err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "email_owner_resolved",
"[SubscriptionFlow] existing email owner resolved for bind flow",
logger.Field("device_user_id", u.Id),
logger.Field("owner_user_id", existingMethod.UserId),
logger.Field("email", req.Email),
)
joinResult, err := familyHelper.joinFamily(existingMethod.UserId, u.Id, "bind_email_with_verification")
if err != nil {
return nil, err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "family_joined",
"[SubscriptionFlow] device user joined existing email owner family",
logger.Field("device_user_id", u.Id),
logger.Field("owner_user_id", existingMethod.UserId),
logger.Field("family_id", joinResult.FamilyId),
logger.Field("email", req.Email),
)
token, err := l.refreshBindSessionToken(u.Id)
if err != nil {
return nil, err
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_requested",
"[SubscriptionFlow] evaluating trial grant after existing email owner bind",
logger.Field("device_user_id", u.Id),
logger.Field("owner_user_id", existingMethod.UserId),
logger.Field("family_id", joinResult.FamilyId),
logger.Field("email", req.Email),
)
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, existingMethod.UserId, req.Email)
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_complete",
"[SubscriptionFlow] email bind with verification completed",
logger.Field("device_user_id", u.Id),
logger.Field("owner_user_id", existingMethod.UserId),
logger.Field("family_id", joinResult.FamilyId),
logger.Field("email", req.Email),
)
return &types.BindEmailWithVerificationResponse{
Success: true,
+60 -9
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/perfect-panel/server/internal/logic/auth"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
@@ -14,11 +15,28 @@ import (
func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, log logger.Logger, ownerUserId int64, email string) {
rc := svcCtx.Config.Register
if !auth.ShouldGrantTrialForEmail(rc, email) {
if rc.EnableTrial && rc.EnableTrialEmailWhitelist {
log.Infow("email domain not in trial whitelist, skip",
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_evaluating",
"[SubscriptionFlow] evaluating email bind trial grant",
logger.Field("owner_user_id", ownerUserId),
logger.Field("email", email),
logger.Field("trial_subscribe_id", rc.TrialSubscribe),
)
if !auth.ShouldAutoGrantTrialOnPublicEmailFlows(rc) {
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped",
"[SubscriptionFlow] auto trial on public email flow disabled",
logger.Field("email", email),
logger.Field("owner_user_id", ownerUserId),
logger.Field("skip_reason", "public_email_trial_disabled"),
)
return
}
if !auth.ShouldGrantTrialForEmail(rc, email) {
if rc.EnableTrial && rc.EnableTrialEmailWhitelist {
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped",
"[SubscriptionFlow] email domain not in trial whitelist",
logger.Field("email", email),
logger.Field("owner_user_id", ownerUserId),
logger.Field("skip_reason", "trial_whitelist_rejected"),
)
}
return
@@ -29,19 +47,45 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l
Model(&user.Subscribe{}).
Where("user_id = ? AND subscribe_id = ?", ownerUserId, rc.TrialSubscribe).
Count(&count).Error; err != nil {
log.Errorw("failed to check existing trial", logger.Field("error", err.Error()))
commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error",
"[SubscriptionFlow] failed to query existing trial subscription",
logger.Field("owner_user_id", ownerUserId),
logger.Field("email", email),
logger.Field("error", err.Error()),
)
return
}
if count > 0 {
log.Infow("trial already granted, skip",
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped",
"[SubscriptionFlow] trial already exists for owner",
logger.Field("owner_user_id", ownerUserId),
logger.Field("email", email),
logger.Field("skip_reason", "trial_already_exists"),
)
return
}
// Cross-user check: prevent the same real inbox (via dot trick / + alias) from
// getting multiple trials across different accounts.
if auth.NormalizedEmailHasTrial(ctx, svcCtx.DB, email, rc.TrialSubscribe) {
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped",
"[SubscriptionFlow] normalized email already received a trial elsewhere",
logger.Field("email", email),
logger.Field("owner_user_id", ownerUserId),
logger.Field("skip_reason", "normalized_email_has_trial"),
)
return
}
sub, err := svcCtx.SubscribeModel.FindOne(ctx, rc.TrialSubscribe)
if err != nil {
log.Errorw("failed to find trial subscribe template", logger.Field("error", err.Error()))
commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error",
"[SubscriptionFlow] failed to load trial subscription template",
logger.Field("owner_user_id", ownerUserId),
logger.Field("email", email),
logger.Field("trial_subscribe_id", rc.TrialSubscribe),
logger.Field("error", err.Error()),
)
return
}
@@ -59,9 +103,13 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l
Status: 1,
}
if err = svcCtx.UserModel.InsertSubscribe(ctx, userSub); err != nil {
log.Errorw("failed to insert trial subscribe",
commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error",
"[SubscriptionFlow] failed to create trial subscription for email bind",
append(commonLogic.UserSubscribeTraceFields(userSub),
logger.Field("error", err.Error()),
logger.Field("owner_user_id", ownerUserId),
logger.Field("email", email),
)...,
)
return
}
@@ -72,9 +120,12 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l
}
}
log.Infow("trial granted on email bind",
commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_succeeded",
"[SubscriptionFlow] trial subscription granted after email bind",
append(commonLogic.UserSubscribeTraceFields(userSub),
logger.Field("owner_user_id", ownerUserId),
logger.Field("email", email),
logger.Field("subscribe_id", sub.Id),
logger.Field("trial_subscribe_id", sub.Id),
)...,
)
}
@@ -2,10 +2,13 @@ package user
import (
"context"
"fmt"
"time"
modelOrder "github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/uuidx"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
@@ -22,6 +25,13 @@ type familyBindingHelper struct {
svcCtx *svc.ServiceContext
}
type familySubscribeTransferMode uint8
const (
familySubscribeDiscard familySubscribeTransferMode = iota
familySubscribeMoveToOwner
)
func newFamilyBindingHelper(ctx context.Context, svcCtx *svc.ServiceContext) *familyBindingHelper {
return &familyBindingHelper{
ctx: ctx,
@@ -111,6 +121,14 @@ func (h *familyBindingHelper) validateJoinFamily(ownerUserId, memberUserId int64
}
func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source string) (*familyJoinResult, error) {
return h.joinFamilyWithSubscribeMode(ownerUserId, memberUserId, source, familySubscribeDiscard)
}
func (h *familyBindingHelper) joinFamilyAndMoveSubscribesToOwner(ownerUserId, memberUserId int64, source string) (*familyJoinResult, error) {
return h.joinFamilyWithSubscribeMode(ownerUserId, memberUserId, source, familySubscribeMoveToOwner)
}
func (h *familyBindingHelper) joinFamilyWithSubscribeMode(ownerUserId, memberUserId int64, source string, subscribeMode familySubscribeTransferMode) (*familyJoinResult, error) {
if ownerUserId == memberUserId {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.FamilyAlreadyBound), "user already bound to this family")
}
@@ -118,7 +136,7 @@ func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source
result := &familyJoinResult{
OwnerUserId: ownerUserId,
}
removedSubscribes := make([]user.Subscribe, 0)
affectedSubscribes := make([]user.Subscribe, 0)
err := h.svcCtx.DB.WithContext(h.ctx).Transaction(func(tx *gorm.DB) error {
ownerFamily, err := h.getOrCreateOwnerFamily(tx, ownerUserId)
@@ -182,10 +200,18 @@ func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source
}
}
removedSubscribes, err = transferMemberSubscribesToOwner(tx, memberUserId, ownerUserId)
switch subscribeMode {
case familySubscribeMoveToOwner:
affectedSubscribes, err = moveMemberSubscribesToOwner(tx, memberUserId, ownerUserId)
if err != nil {
return err
}
default:
affectedSubscribes, err = transferMemberSubscribesToOwner(tx, memberUserId, ownerUserId)
if err != nil {
return err
}
}
return nil
})
@@ -193,12 +219,56 @@ func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source
return nil, err
}
if err = h.clearRemovedMemberSubscribeCache(removedSubscribes); err != nil {
if subscribeMode == familySubscribeMoveToOwner {
if err = h.clearMovedMemberSubscribeCache(affectedSubscribes, ownerUserId); err != nil {
return nil, err
}
} else {
if err = h.clearRemovedMemberSubscribeCache(affectedSubscribes); err != nil {
return nil, err
}
}
return result, nil
}
func moveMemberSubscribesToOwner(tx *gorm.DB, memberUserId, ownerUserId int64) ([]user.Subscribe, error) {
var subscribes []user.Subscribe
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Model(&user.Subscribe{}).
Where("user_id = ?", memberUserId).
Find(&subscribes).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query member subscribe list failed")
}
if len(subscribes) == 0 {
return nil, nil
}
for _, sub := range subscribes {
newToken := uuidx.SubscribeToken(fmt.Sprintf("familyMove:%d:%d:%s", ownerUserId, sub.Id, uuidx.NewUUID().String()))
if err := tx.Model(&user.Subscribe{}).
Where("id = ? AND user_id = ?", sub.Id, memberUserId).
Updates(map[string]interface{}{
"user_id": ownerUserId,
"token": newToken,
"uuid": uuidx.NewUUID().String(),
"updated_at": time.Now(),
}).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "move member subscribe to owner failed")
}
if sub.OrderId > 0 {
if err := tx.Model(&modelOrder.Order{}).
Where("id = ? AND subscription_user_id = ?", sub.OrderId, memberUserId).
Updates(map[string]interface{}{
"subscription_user_id": ownerUserId,
"subscribe_token": newToken,
}).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "move member subscribe order to owner failed")
}
}
}
return subscribes, nil
}
func transferMemberSubscribesToOwner(tx *gorm.DB, memberUserId, ownerUserId int64) ([]user.Subscribe, error) {
var subscribes []user.Subscribe
if err := tx.Model(&user.Subscribe{}).
@@ -209,10 +279,9 @@ func transferMemberSubscribesToOwner(tx *gorm.DB, memberUserId, ownerUserId int6
if len(subscribes) == 0 {
return nil, nil
}
if err := tx.Model(&user.Subscribe{}).
Where("user_id = ?", memberUserId).
Update("user_id", ownerUserId).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "transfer member subscribes to owner failed")
// 加入家庭组时,无条件丢弃成员的所有订阅(软删除)
if err := tx.Where("user_id = ?", memberUserId).Delete(&user.Subscribe{}).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "discard member subscribes failed")
}
return subscribes, nil
}
@@ -239,6 +308,37 @@ func (h *familyBindingHelper) clearRemovedMemberSubscribeCache(removedSubscribes
return nil
}
func (h *familyBindingHelper) clearMovedMemberSubscribeCache(movedSubscribes []user.Subscribe, ownerUserId int64) error {
if len(movedSubscribes) == 0 {
return nil
}
cacheModels := make([]*user.Subscribe, 0, len(movedSubscribes)*2)
ownerCopies := make([]user.Subscribe, len(movedSubscribes))
for i := range movedSubscribes {
cacheModels = append(cacheModels, &movedSubscribes[i])
ownerCopies[i] = movedSubscribes[i]
ownerCopies[i].UserId = ownerUserId
cacheModels = append(cacheModels, &ownerCopies[i])
}
if err := h.svcCtx.UserModel.ClearSubscribeCache(h.ctx, cacheModels...); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear moved subscribe cache failed")
}
_, subscribeIDSet := buildRemovedSubscribeCacheMeta(movedSubscribes)
for subscribeID := range subscribeIDSet {
if err := h.svcCtx.SubscribeModel.ClearCache(h.ctx, subscribeID); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear subscribe cache failed")
}
}
if err := h.svcCtx.NodeModel.ClearServerAllCache(h.ctx); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear node cache failed")
}
return nil
}
func buildRemovedSubscribeCacheMeta(removedSubscribes []user.Subscribe) ([]*user.Subscribe, map[int64]struct{}) {
subscribeModels := make([]*user.Subscribe, 0, len(removedSubscribes))
subscribeIDSet := make(map[int64]struct{}, len(removedSubscribes))
@@ -0,0 +1,288 @@
package user
import (
"testing"
"time"
modelOrder "github.com/perfect-panel/server/internal/model/order"
modelUser "github.com/perfect-panel/server/internal/model/user"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func newFamilyBindingTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("open sqlite db: %v", err)
}
if err = db.Exec(`
CREATE TABLE user_subscribe (
id integer primary key,
user_id integer not null,
order_id integer not null,
subscribe_id integer not null,
start_time datetime,
expire_time datetime,
token text,
uuid text,
status integer,
created_at datetime,
updated_at datetime
)`).Error; err != nil {
t.Fatalf("create user_subscribe schema: %v", err)
}
if err = db.Exec(`
CREATE TABLE "order" (
id integer primary key,
user_id integer not null,
subscription_user_id integer not null,
order_no text,
subscribe_token text,
status integer,
subscribe_id integer,
created_at datetime,
updated_at datetime
)`).Error; err != nil {
t.Fatalf("create order schema: %v", err)
}
return db
}
func insertFamilyBindingTestOrder(t *testing.T, db *gorm.DB, order modelOrder.Order) {
t.Helper()
if err := db.Exec(`
INSERT INTO "order" (
id, user_id, subscription_user_id, order_no, subscribe_token, status, subscribe_id, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
order.Id,
order.UserId,
order.SubscriptionUserId,
order.OrderNo,
order.SubscribeToken,
order.Status,
order.SubscribeId,
order.CreatedAt,
order.UpdatedAt,
).Error; err != nil {
t.Fatalf("insert order %d: %v", order.Id, err)
}
}
func insertFamilyBindingTestSubscribe(t *testing.T, db *gorm.DB, sub modelUser.Subscribe) {
t.Helper()
if err := db.Exec(`
INSERT INTO user_subscribe (
id, user_id, order_id, subscribe_id, start_time, expire_time, token, uuid, status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
sub.Id,
sub.UserId,
sub.OrderId,
sub.SubscribeId,
sub.StartTime,
sub.ExpireTime,
sub.Token,
sub.UUID,
sub.Status,
sub.CreatedAt,
sub.UpdatedAt,
).Error; err != nil {
t.Fatalf("insert subscribe %d: %v", sub.Id, err)
}
}
func TestMoveMemberSubscribesToOwnerMovesSubscribeAndCurrentOrder(t *testing.T) {
db := newFamilyBindingTestDB(t)
memberUserID := int64(1001)
ownerUserID := int64(2001)
now := time.Now().Add(-time.Hour)
expireAt := now.Add(30 * 24 * time.Hour)
currentOrder := modelOrder.Order{
Id: 9001,
UserId: memberUserID,
SubscriptionUserId: memberUserID,
OrderNo: "order-current",
SubscribeToken: "token-current",
Status: 5,
SubscribeId: 3001,
CreatedAt: now,
UpdatedAt: now,
}
unlinkedOrder := modelOrder.Order{
Id: 9002,
UserId: memberUserID,
SubscriptionUserId: memberUserID,
OrderNo: "order-unlinked",
SubscribeToken: "token-unlinked",
Status: 5,
SubscribeId: 3001,
CreatedAt: now,
UpdatedAt: now,
}
sub := modelUser.Subscribe{
Id: 7001,
UserId: memberUserID,
OrderId: currentOrder.Id,
SubscribeId: currentOrder.SubscribeId,
StartTime: now,
ExpireTime: expireAt,
Token: "token-current",
UUID: "uuid-current",
Status: 1,
CreatedAt: now,
UpdatedAt: now,
}
insertFamilyBindingTestOrder(t, db, currentOrder)
insertFamilyBindingTestOrder(t, db, unlinkedOrder)
insertFamilyBindingTestSubscribe(t, db, sub)
var moved []modelUser.Subscribe
if err := db.Transaction(func(tx *gorm.DB) error {
var err error
moved, err = moveMemberSubscribesToOwner(tx, memberUserID, ownerUserID)
return err
}); err != nil {
t.Fatalf("move member subscribes: %v", err)
}
if len(moved) != 1 {
t.Fatalf("moved subscribes length = %d, want 1", len(moved))
}
if moved[0].UserId != memberUserID {
t.Fatalf("moved cache copy user_id = %d, want original member %d", moved[0].UserId, memberUserID)
}
var gotSub modelUser.Subscribe
if err := db.First(&gotSub, "id = ?", sub.Id).Error; err != nil {
t.Fatalf("query moved subscribe: %v", err)
}
if gotSub.UserId != ownerUserID {
t.Fatalf("subscribe user_id = %d, want owner %d", gotSub.UserId, ownerUserID)
}
if gotSub.Token == "" || gotSub.Token == sub.Token {
t.Fatalf("subscribe token = %q, want regenerated from old token %q", gotSub.Token, sub.Token)
}
if gotSub.UUID == "" || gotSub.UUID == sub.UUID {
t.Fatalf("subscribe uuid = %q, want regenerated from old uuid %q", gotSub.UUID, sub.UUID)
}
var gotOrder modelOrder.Order
if err := db.First(&gotOrder, "id = ?", currentOrder.Id).Error; err != nil {
t.Fatalf("query updated order: %v", err)
}
if gotOrder.SubscriptionUserId != ownerUserID {
t.Fatalf("current order subscription_user_id = %d, want owner %d", gotOrder.SubscriptionUserId, ownerUserID)
}
if gotOrder.SubscribeToken != gotSub.Token {
t.Fatalf("current order subscribe_token = %q, want regenerated subscribe token %q", gotOrder.SubscribeToken, gotSub.Token)
}
var gotUnlinkedOrder modelOrder.Order
if err := db.First(&gotUnlinkedOrder, "id = ?", unlinkedOrder.Id).Error; err != nil {
t.Fatalf("query unlinked order: %v", err)
}
if gotUnlinkedOrder.SubscriptionUserId != memberUserID {
t.Fatalf("unlinked order subscription_user_id = %d, want unchanged member %d", gotUnlinkedOrder.SubscriptionUserId, memberUserID)
}
if gotUnlinkedOrder.SubscribeToken != unlinkedOrder.SubscribeToken {
t.Fatalf("unlinked order subscribe_token = %q, want unchanged token %q", gotUnlinkedOrder.SubscribeToken, unlinkedOrder.SubscribeToken)
}
}
func TestMoveMemberSubscribesToOwnerNoSubscribesIsNoop(t *testing.T) {
db := newFamilyBindingTestDB(t)
var moved []modelUser.Subscribe
if err := db.Transaction(func(tx *gorm.DB) error {
var err error
moved, err = moveMemberSubscribesToOwner(tx, 1001, 2001)
return err
}); err != nil {
t.Fatalf("move empty member subscribes: %v", err)
}
if len(moved) != 0 {
t.Fatalf("moved subscribes length = %d, want 0", len(moved))
}
}
func TestTransferMemberSubscribesToOwnerStillDiscardsMemberSubscribes(t *testing.T) {
db := newFamilyBindingTestDB(t)
memberUserID := int64(1001)
ownerUserID := int64(2001)
now := time.Now().Add(-time.Hour)
order := modelOrder.Order{
Id: 9001,
UserId: memberUserID,
SubscriptionUserId: memberUserID,
OrderNo: "order-discard",
SubscribeToken: "token-discard",
Status: 5,
SubscribeId: 3001,
CreatedAt: now,
UpdatedAt: now,
}
sub := modelUser.Subscribe{
Id: 7001,
UserId: memberUserID,
OrderId: order.Id,
SubscribeId: order.SubscribeId,
StartTime: now,
ExpireTime: now.Add(30 * 24 * time.Hour),
Token: "token-discard",
UUID: "uuid-discard",
Status: 1,
CreatedAt: now,
UpdatedAt: now,
}
insertFamilyBindingTestOrder(t, db, order)
insertFamilyBindingTestSubscribe(t, db, sub)
var removed []modelUser.Subscribe
if err := db.Transaction(func(tx *gorm.DB) error {
var err error
removed, err = transferMemberSubscribesToOwner(tx, memberUserID, ownerUserID)
return err
}); err != nil {
t.Fatalf("discard member subscribes: %v", err)
}
if len(removed) != 1 {
t.Fatalf("removed subscribes length = %d, want 1", len(removed))
}
if removed[0].UserId != memberUserID {
t.Fatalf("removed cache copy user_id = %d, want original member %d", removed[0].UserId, memberUserID)
}
var memberCount int64
if err := db.Model(&modelUser.Subscribe{}).Where("user_id = ?", memberUserID).Count(&memberCount).Error; err != nil {
t.Fatalf("count member subscribes: %v", err)
}
if memberCount != 0 {
t.Fatalf("member subscribe count = %d, want 0", memberCount)
}
var ownerCount int64
if err := db.Model(&modelUser.Subscribe{}).Where("user_id = ?", ownerUserID).Count(&ownerCount).Error; err != nil {
t.Fatalf("count owner subscribes: %v", err)
}
if ownerCount != 0 {
t.Fatalf("owner subscribe count = %d, want 0", ownerCount)
}
var gotOrder modelOrder.Order
if err := db.First(&gotOrder, "id = ?", order.Id).Error; err != nil {
t.Fatalf("query order: %v", err)
}
if gotOrder.SubscriptionUserId != memberUserID {
t.Fatalf("discard path order subscription_user_id = %d, want unchanged member %d", gotOrder.SubscriptionUserId, memberUserID)
}
}
@@ -28,18 +28,33 @@ func NewGetDeviceListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get
func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse, err error) {
userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User)
scopeHelper := newFamilyScopeHelper(l.ctx, l.svcCtx)
scopeUserIds, err := scopeHelper.resolveScopedUserIds(userInfo.Id)
// 只查当前用户自己的设备(不查家庭组其他成员的设备)
// 原始代码(保留以供对照):
// scopeHelper := newFamilyScopeHelper(l.ctx, l.svcCtx)
// scopeUserIds, err := scopeHelper.resolveScopedUserIds(userInfo.Id)
// if err != nil {
// return nil, err
// }
// list, count, err := l.svcCtx.UserModel.QueryDeviceListByUserIds(l.ctx, scopeUserIds)
list, count, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, userInfo.Id)
if err != nil {
return nil, err
}
list, count, err := l.svcCtx.UserModel.QueryDeviceListByUserIds(l.ctx, scopeUserIds)
userRespList := make([]types.UserDevice, 0)
tool.DeepCopy(&userRespList, list)
for i, d := range list {
if i < len(userRespList) {
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
}
// 只返回第一个设备,其余设备丢弃不返回给前端
// 原始代码(保留以供对照):
// userRespList := make([]types.UserDevice, 0)
// tool.DeepCopy(&userRespList, list)
// for i, d := range list {
// if i < len(userRespList) {
// userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
// }
// }
userRespList := make([]types.UserDevice, 0, 1)
if len(list) > 0 {
var item types.UserDevice
tool.DeepCopy(&item, list[0])
item.DeviceNo = tool.DeviceIdToHash(list[0].Id)
userRespList = append(userRespList, item)
}
resp = &types.GetDeviceListResponse{
Total: count,
@@ -8,6 +8,7 @@ import (
"time"
"github.com/perfect-panel/server/internal/config"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
@@ -39,6 +40,10 @@ type CacheKeyPayload struct {
func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_start",
"[SubscriptionFlow] email verification started",
logger.Field("email", req.Email),
)
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email)
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
if err != nil {
@@ -59,6 +64,10 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code expired")
}
l.svcCtx.Redis.Del(l.ctx, cacheKey)
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_code_verified",
"[SubscriptionFlow] email verification code accepted",
logger.Field("email", req.Email),
)
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
@@ -77,6 +86,12 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error")
}
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_completed",
"[SubscriptionFlow] email verification completed and trial evaluation will run",
logger.Field("user_id", u.Id),
logger.Field("owner_user_id", method.UserId),
logger.Field("email", req.Email),
)
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, method.UserId, req.Email)
return nil
}
@@ -194,7 +194,7 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
val, _ := json.Marshal(resp)
etag := tool.GenerateETag(val)
l.ctx.Header("ETag", etag)
err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), -1).Err()
err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), l.serverUserListCacheTTL()).Err()
if err != nil {
l.Errorw("[ServerUserListCacheKey] redis set error", logger.Field("error", err.Error()))
}
@@ -205,6 +205,18 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR
return resp, nil
}
func (l *GetServerUserListLogic) serverUserListCacheTTL() time.Duration {
pullInterval := l.svcCtx.Config.Node.NodePullInterval
if pullInterval <= 0 {
pullInterval = 60
}
ttl := time.Duration(pullInterval*2) * time.Second
if ttl < time.Minute {
return time.Minute
}
return ttl
}
func (l *GetServerUserListLogic) shouldIncludeServerUser(userSub *user.Subscribe, serverNodeGroupIds []int64) bool {
if userSub == nil {
return false
@@ -295,6 +307,15 @@ func (l *GetServerUserListLogic) canUseExpiredNodeGroup(userSub *user.Subscribe,
// calculateEffectiveSpeedLimit 计算用户的实际限速值(考虑按量限速规则)
func (l *GetServerUserListLogic) calculateEffectiveSpeedLimit(sub *subscribe.Subscribe, userSub *user.Subscribe) int64 {
result := speedlimit.Calculate(l.ctx.Request.Context(), l.svcCtx.DB, userSub.UserId, userSub.Id, sub.SpeedLimit, sub.TrafficLimit)
result := speedlimit.CalculateWithCache(
l.ctx.Request.Context(),
l.svcCtx.Redis,
l.svcCtx.DB,
userSub.UserId,
userSub.Id,
sub.SpeedLimit,
sub.TrafficLimit,
30*time.Second,
)
return result.EffectiveSpeed
}
+6 -10
View File
@@ -166,12 +166,11 @@ func (m *customOrderModel) QueryMonthlyOrders(ctx context.Context, date time.Tim
// QueryDateOrders Query orders by date
func (m *customOrderModel) QueryDateOrders(ctx context.Context, date time.Time) (OrdersTotal, error) {
start := date.Truncate(24 * time.Hour)
end := start.Add(24 * time.Hour).Add(-time.Nanosecond)
dateStr := date.Format("2006-01-02")
var result OrdersTotal
err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Order{}).
Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, start, end, "balance").
Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?", []int64{2, 5}, dateStr, "balance").
Select(
"SUM(amount) as amount_total, " +
"SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " +
@@ -222,10 +221,7 @@ func (m *customOrderModel) QueryMonthlyUserCounts(ctx context.Context, date time
return counts.NewUsers, counts.RenewalUsers, err
}
func (m *customOrderModel) QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error) {
// 当天 00:00:00
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
// 下一天 00:00:00
nextDay := start.Add(24 * time.Hour)
dateStr := date.Format("2006-01-02")
var counts UserCounts
@@ -235,8 +231,8 @@ func (m *customOrderModel) QueryDateUserCounts(ctx context.Context, date time.Ti
COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
`).
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
[]int64{2, 5}, start, nextDay, "balance").
Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?",
[]int64{2, 5}, dateStr, "balance").
Scan(&counts).Error
})
@@ -276,7 +272,7 @@ func (m *customOrderModel) QueryDailyOrdersList(ctx context.Context, date time.T
// 当月 1 号 00:00:00
firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location())
// 第二天 00:00:00
nextDay := date.AddDate(0, 0, 1).Truncate(24 * time.Hour)
nextDay := time.Date(date.Year(), date.Month(), date.Day()+1, 0, 0, 0, 0, date.Location())
return conn.Model(&Order{}).
Select(`
@@ -0,0 +1,20 @@
package recovery
import "time"
type OrderRecoveryClaim struct {
Id int64 `gorm:"primaryKey"`
OrderNo string `gorm:"type:varchar(255);not null;uniqueIndex:idx_order_recovery_claim_order_no;comment:Recovered Order No"`
Email string `gorm:"type:varchar(255);not null;index:idx_order_recovery_claim_email;comment:Claim Email"`
UserId int64 `gorm:"type:bigint;not null;default:0;comment:User ID"`
SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe ID"`
OrderId int64 `gorm:"type:bigint;not null;default:0;comment:Recovered Order ID"`
UserSubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:User Subscribe ID"`
ClaimedAt time.Time `gorm:"comment:Claimed Time"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (OrderRecoveryClaim) TableName() string {
return "order_recovery_claims"
}
+2 -2
View File
@@ -27,8 +27,8 @@ func NewModel(conn *gorm.DB) Model {
func (m *customTrafficModel) QueryServerTrafficByDay(ctx context.Context, serverId int64, date time.Time) (*TotalTraffic, error) {
var data TotalTraffic
start := date.Truncate(24 * time.Hour)
end := start.Add(24 * time.Hour).Add(-time.Nanosecond)
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
end := start.AddDate(0, 0, 1).Add(-time.Nanosecond)
err := m.Conn.WithContext(ctx).Model(&TrafficLog{}).
Select("sum(download) as download, sum(upload) as upload").
Where("server_id = ? AND timestamp BETWEEN ? AND ?", serverId, start, end).
+4 -3
View File
@@ -314,8 +314,8 @@ func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id
func (m *customUserModel) QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) {
var total int64
start := date.Truncate(24 * time.Hour)
end := start.Add(24 * time.Hour).Add(-time.Second)
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
end := start.AddDate(0, 0, 1).Add(-time.Second)
err := m.QueryNoCacheCtx(ctx, &total, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&User{}).Where("created_at > ? and created_at < ?", start, end).Count(&total).Error
})
@@ -447,7 +447,8 @@ func (m *customUserModel) QueryMonthlyUserStatisticsList(ctx context.Context, da
func (m *customUserModel) FindActiveSubscribe(ctx context.Context, userId int64) (*Subscribe, error) {
var subscribe Subscribe
err := m.QueryNoCacheCtx(ctx, &subscribe, func(conn *gorm.DB, v interface{}) error {
return conn.Where("user_id = ? AND status IN (0, 1) AND expire_time > ?", userId, time.Now()).
now := time.Now()
return conn.Where("user_id = ? AND status IN (0, 1) AND (expire_time > ? OR expire_time = ?)", userId, now, time.UnixMilli(0)).
Order("expire_time DESC").
First(v).Error
})

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