Initial commit
This commit is contained in:
15
.claude-plugin/plugin.json
Normal file
15
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "walrus-ts",
|
||||||
|
"description": "Walrus TypeScript SDK 专业化 AI 技能集,提供代码生成、错误诊断、监控分析和性能优化功能,专为 Walrus 去中心化存储应用开发者打造",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "icehugh",
|
||||||
|
"url": "https://github.com/icehugh"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/walrus-code-generator.md",
|
||||||
|
"./skills/walrus-debugger.md",
|
||||||
|
"./skills/walrus-monitor.md",
|
||||||
|
"./skills/walrus-performance-optimizer.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# walrus-ts
|
||||||
|
|
||||||
|
Walrus TypeScript SDK 专业化 AI 技能集,提供代码生成、错误诊断、监控分析和性能优化功能,专为 Walrus 去中心化存储应用开发者打造
|
||||||
57
plugin.lock.json
Normal file
57
plugin.lock.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:IceHugh/web3-skills:plugins/walrus-ts",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "06d68ff1bf73ce56b1d9e51b692e8c69277090b2",
|
||||||
|
"treeHash": "64320018af6f422c00f00245e9acab8eee6a96350c8d4b0254b190f8419e4376",
|
||||||
|
"generatedAt": "2025-11-28T10:11:42.066243Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "walrus-ts",
|
||||||
|
"description": "Walrus TypeScript SDK 专业化 AI 技能集,提供代码生成、错误诊断、监控分析和性能优化功能,专为 Walrus 去中心化存储应用开发者打造",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "c83f9e4d8d2f29b7530bb5ae2c1d5de617ff56cfd8e4d8072600c6a033e87d93"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "dbbf5f4988e401c731ec8cfe4f222e50917f81b88847b9ce2d7a09768eb5db07"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/walrus-debugger.md",
|
||||||
|
"sha256": "08c4cc9a125b1d339e17cfdedd1bba446cbfb6608bdb4c9e9348985a1b6e2486"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/walrus-monitor.md",
|
||||||
|
"sha256": "177168c232f00d9a8c32f91901a7c37165921e41f219f1dc10eb74c38f4abf53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/walrus-code-generator.md",
|
||||||
|
"sha256": "369009349d62f07b85df07655e447dccaba50579a533ee16f149481f158855d9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/walrus-performance-optimizer.md",
|
||||||
|
"sha256": "e8a5b724f84d5966fca546a5c3d6f7bd6a9759a1e328cc102c99433a302380c9"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "64320018af6f422c00f00245e9acab8eee6a96350c8d4b0254b190f8419e4376"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
833
skills/walrus-code-generator.md
Normal file
833
skills/walrus-code-generator.md
Normal file
@@ -0,0 +1,833 @@
|
|||||||
|
---
|
||||||
|
name: walrus-code-generator
|
||||||
|
description: 自动生成 Walrus TypeScript SDK 集成代码和最佳实践模板
|
||||||
|
parameters:
|
||||||
|
- name: use-case
|
||||||
|
type: string
|
||||||
|
description: 使用场景 (basic-upload/advanced-storage/browser-app/batch-processing)
|
||||||
|
required: true
|
||||||
|
- name: framework
|
||||||
|
type: string
|
||||||
|
description: 开发框架 (react/vue/nodejs/nextjs/express)
|
||||||
|
default: nodejs
|
||||||
|
- name: features
|
||||||
|
type: array
|
||||||
|
description: 需要的功能特性
|
||||||
|
default: []
|
||||||
|
- name: environment
|
||||||
|
type: string
|
||||||
|
description: 部署环境 (development/production)
|
||||||
|
default: development
|
||||||
|
---
|
||||||
|
|
||||||
|
# Walrus 代码生成技能
|
||||||
|
|
||||||
|
## 技能概述
|
||||||
|
|
||||||
|
`walrus-code-generator` 是一个专业的代码生成助手,根据您的具体需求自动生成 Walrus TypeScript SDK 集成代码。支持多种使用场景、开发框架和部署环境,确保生成的代码符合最佳实践。
|
||||||
|
|
||||||
|
## 支持的使用场景
|
||||||
|
|
||||||
|
### 1. 基础文件上传 (basic-upload)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 自动生成的基础上传服务
|
||||||
|
skill: "walrus-code-generator"
|
||||||
|
// use-case: basic-upload
|
||||||
|
// framework: nodejs
|
||||||
|
// features: ["error-handling", "progress-tracking"]
|
||||||
|
|
||||||
|
export class BasicWalrusUploader {
|
||||||
|
private client: any;
|
||||||
|
private signer: any;
|
||||||
|
|
||||||
|
constructor(network: 'testnet' | 'mainnet' = 'testnet') {
|
||||||
|
const suiClient = new SuiJsonRpcClient({
|
||||||
|
url: getFullnodeUrl(network),
|
||||||
|
network,
|
||||||
|
}).$extend(walrus());
|
||||||
|
|
||||||
|
this.client = suiClient;
|
||||||
|
this.signer = this.createSigner();
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile(
|
||||||
|
filePath: string,
|
||||||
|
options: UploadOptions = {}
|
||||||
|
): Promise<UploadResult> {
|
||||||
|
const {
|
||||||
|
epochs = 10,
|
||||||
|
deletable = true,
|
||||||
|
tags = {},
|
||||||
|
onProgress = () => {}
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
onProgress(25, '读取文件...');
|
||||||
|
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
const fileContent = await fs.readFile(filePath);
|
||||||
|
const fileName = filePath.split('/').pop() || 'unknown';
|
||||||
|
|
||||||
|
onProgress(50, '创建 Walrus 文件...');
|
||||||
|
|
||||||
|
const file = WalrusFile.from({
|
||||||
|
contents: fileContent,
|
||||||
|
identifier: `uploads/${Date.now()}_${fileName}`,
|
||||||
|
mimeType: this.getMimeType(filePath),
|
||||||
|
attributes: {
|
||||||
|
'original-path': filePath,
|
||||||
|
'file-size': fileContent.length.toString(),
|
||||||
|
'upload-time': new Date().toISOString(),
|
||||||
|
...tags
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onProgress(75, '上传到网络...');
|
||||||
|
|
||||||
|
const result = await this.client.walrus.writeFiles({
|
||||||
|
files: [file],
|
||||||
|
epochs,
|
||||||
|
deletable,
|
||||||
|
signer: this.signer
|
||||||
|
});
|
||||||
|
|
||||||
|
onProgress(100, '上传完成');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
blobId: result[0].blobId,
|
||||||
|
fileName,
|
||||||
|
size: fileContent.length,
|
||||||
|
storedUntil: result[0].storedUntil
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error.message);
|
||||||
|
throw new Error(`上传失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSigner(): Ed25519Keypair {
|
||||||
|
const privateKey = process.env.WALRUS_PRIVATE_KEY;
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error('未找到私钥环境变量 WALRUS_PRIVATE_KEY');
|
||||||
|
}
|
||||||
|
return Ed25519Keypair.fromSecretKey(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMimeType(filePath: string): string {
|
||||||
|
const ext = filePath.split('.').pop()?.toLowerCase();
|
||||||
|
const mimeTypes: Record<string, string> = {
|
||||||
|
'txt': 'text/plain',
|
||||||
|
'pdf': 'application/pdf',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'png': 'image/png',
|
||||||
|
'json': 'application/json'
|
||||||
|
};
|
||||||
|
return mimeTypes[ext || ''] || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadOptions {
|
||||||
|
epochs?: number;
|
||||||
|
deletable?: boolean;
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
onProgress?: (progress: number, message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadResult {
|
||||||
|
success: boolean;
|
||||||
|
blobId: string;
|
||||||
|
fileName: string;
|
||||||
|
size: number;
|
||||||
|
storedUntil: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 高级存储管理 (advanced-storage)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 自动生成的高级存储管理服务
|
||||||
|
skill: "walrus-code-generator"
|
||||||
|
// use-case: advanced-storage
|
||||||
|
// framework: nodejs
|
||||||
|
// features: ["batch-operations", "retry-logic", "cache-management", "metadata-indexing"]
|
||||||
|
|
||||||
|
export class AdvancedWalrusStorage {
|
||||||
|
private client: any;
|
||||||
|
private signer: any;
|
||||||
|
private cache: Map<string, CachedFile> = new Map();
|
||||||
|
private retryConfig: RetryConfig;
|
||||||
|
|
||||||
|
constructor(options: AdvancedStorageOptions = {}) {
|
||||||
|
const {
|
||||||
|
network = 'testnet',
|
||||||
|
retryAttempts = 3,
|
||||||
|
retryDelay = 1000,
|
||||||
|
cacheEnabled = true,
|
||||||
|
cacheTTL = 300000 // 5分钟
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const suiClient = new SuiJsonRpcClient({
|
||||||
|
url: getFullnodeUrl(network),
|
||||||
|
network,
|
||||||
|
}).$extend(walrus({
|
||||||
|
storageNodeClientOptions: {
|
||||||
|
timeout: 60000,
|
||||||
|
onError: (error) => console.warn('存储节点错误:', error)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.client = suiClient;
|
||||||
|
this.signer = this.createSigner();
|
||||||
|
this.retryConfig = { attempts: retryAttempts, delay: retryDelay };
|
||||||
|
this.cacheEnabled = cacheEnabled;
|
||||||
|
this.cacheTTL = cacheTTL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchUpload(
|
||||||
|
files: FileUploadRequest[],
|
||||||
|
options: BatchUploadOptions = {}
|
||||||
|
): Promise<BatchUploadResult> {
|
||||||
|
const {
|
||||||
|
epochs = 10,
|
||||||
|
deletable = true,
|
||||||
|
maxConcurrency = 3,
|
||||||
|
commonTags = {}
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const results: UploadResult[] = [];
|
||||||
|
const errors: UploadError[] = [];
|
||||||
|
|
||||||
|
// 分批处理
|
||||||
|
for (let i = 0; i < files.length; i += maxConcurrency) {
|
||||||
|
const batch = files.slice(i, i + maxConcurrency);
|
||||||
|
|
||||||
|
const batchPromises = batch.map(async (fileRequest, index) => {
|
||||||
|
try {
|
||||||
|
const result = await this.uploadWithRetry(fileRequest, {
|
||||||
|
epochs,
|
||||||
|
deletable,
|
||||||
|
tags: { ...commonTags, ...fileRequest.tags }
|
||||||
|
});
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
...result,
|
||||||
|
originalPath: fileRequest.path,
|
||||||
|
batchIndex: i + index
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
errors.push({
|
||||||
|
originalPath: fileRequest.path,
|
||||||
|
error: error.message,
|
||||||
|
batchIndex: i + index
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(batchPromises);
|
||||||
|
|
||||||
|
// 批次间延迟
|
||||||
|
if (i + maxConcurrency < files.length) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalFiles: files.length,
|
||||||
|
successful: results.length,
|
||||||
|
failed: errors.length,
|
||||||
|
results,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async smartDownload(
|
||||||
|
blobId: string,
|
||||||
|
options: SmartDownloadOptions = {}
|
||||||
|
): Promise<SmartDownloadResult> {
|
||||||
|
const {
|
||||||
|
forceRefresh = false,
|
||||||
|
verifyIntegrity = true,
|
||||||
|
cacheKey = blobId
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if (!forceRefresh && this.cacheEnabled) {
|
||||||
|
const cached = this.getFromCache(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
console.log('从缓存返回文件:', blobId);
|
||||||
|
return {
|
||||||
|
...cached,
|
||||||
|
fromCache: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.downloadWithVerification(blobId, verifyIntegrity);
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
if (this.cacheEnabled) {
|
||||||
|
this.setToCache(cacheKey, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
fromCache: false
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`智能下载失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchByTags(tags: Record<string, string>): Promise<SearchResult[]> {
|
||||||
|
// 这是一个概念性实现
|
||||||
|
// 实际搜索需要基于您自己的元数据索引系统
|
||||||
|
const indexedFiles = await this.getIndexedFiles();
|
||||||
|
|
||||||
|
return indexedFiles.filter(file => {
|
||||||
|
return Object.entries(tags).every(([key, value]) =>
|
||||||
|
file.tags[key] === value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadWithRetry(
|
||||||
|
fileRequest: FileUploadRequest,
|
||||||
|
options: UploadOptions
|
||||||
|
): Promise<UploadResult> {
|
||||||
|
const { attempts, delay } = this.retryConfig;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await this.uploadSingleFile(fileRequest, options);
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt < attempts && this.isRetryableError(error)) {
|
||||||
|
console.log(`上传失败,第 ${attempt} 次重试...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('上传失败,已达到最大重试次数');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadSingleFile(
|
||||||
|
fileRequest: FileUploadRequest,
|
||||||
|
options: UploadOptions
|
||||||
|
): Promise<UploadResult> {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
const fileContent = await fs.readFile(fileRequest.path);
|
||||||
|
|
||||||
|
const file = WalrusFile.from({
|
||||||
|
contents: fileContent,
|
||||||
|
identifier: fileRequest.identifier || `files/${Date.now()}_${fileRequest.path}`,
|
||||||
|
mimeType: fileRequest.mimeType || this.getMimeType(fileRequest.path),
|
||||||
|
attributes: {
|
||||||
|
'original-path': fileRequest.path,
|
||||||
|
'file-size': fileContent.length.toString(),
|
||||||
|
'upload-time': new Date().toISOString(),
|
||||||
|
...options.tags
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await this.client.walrus.writeFiles({
|
||||||
|
files: [file],
|
||||||
|
epochs: options.epochs,
|
||||||
|
deletable: options.deletable,
|
||||||
|
signer: this.signer
|
||||||
|
});
|
||||||
|
|
||||||
|
// 索引文件元数据
|
||||||
|
await this.indexFile({
|
||||||
|
blobId: result[0].blobId,
|
||||||
|
identifier: file.identifier,
|
||||||
|
path: fileRequest.path,
|
||||||
|
size: fileContent.length,
|
||||||
|
tags: options.tags,
|
||||||
|
uploadTime: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
blobId: result[0].blobId,
|
||||||
|
identifier: file.identifier,
|
||||||
|
size: fileContent.length,
|
||||||
|
storedUntil: result[0].storedUntil
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async downloadWithVerification(
|
||||||
|
blobId: string,
|
||||||
|
verify: boolean
|
||||||
|
): Promise<SmartDownloadResult> {
|
||||||
|
const [file] = await this.client.walrus.getFiles({ ids: [blobId] });
|
||||||
|
|
||||||
|
const exists = await file.exists();
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error(`文件不存在: ${blobId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await file.bytes();
|
||||||
|
let verified = true;
|
||||||
|
|
||||||
|
if (verify) {
|
||||||
|
const verification = await this.verifyFileIntegrity(blobId, content);
|
||||||
|
verified = verification.valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blobId,
|
||||||
|
content: Buffer.from(content),
|
||||||
|
size: content.length,
|
||||||
|
storedUntil: await file.storedUntil(),
|
||||||
|
verified,
|
||||||
|
metadata: await this.getFileMetadata(file)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyFileIntegrity(
|
||||||
|
blobId: string,
|
||||||
|
content: Uint8Array
|
||||||
|
): Promise<{ valid: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
// 基本完整性检查
|
||||||
|
if (content.length === 0) {
|
||||||
|
return { valid: false, error: '文件内容为空' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以添加更复杂的验证逻辑
|
||||||
|
return { valid: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { valid: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存管理
|
||||||
|
private getFromCache(key: string): CachedFile | null {
|
||||||
|
const cached = this.cache.get(key);
|
||||||
|
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
this.cache.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setToCache(key: string, file: SmartDownloadResult): void {
|
||||||
|
this.cache.set(key, {
|
||||||
|
...file,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private isRetryableError(error: any): boolean {
|
||||||
|
return error instanceof RetryableWalrusClientError ||
|
||||||
|
error.message.includes('timeout') ||
|
||||||
|
error.message.includes('network');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
interface FileUploadRequest {
|
||||||
|
path: string;
|
||||||
|
identifier?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
tags?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchUploadOptions {
|
||||||
|
epochs?: number;
|
||||||
|
deletable?: boolean;
|
||||||
|
maxConcurrency?: number;
|
||||||
|
commonTags?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchUploadResult {
|
||||||
|
totalFiles: number;
|
||||||
|
successful: number;
|
||||||
|
failed: number;
|
||||||
|
results: (UploadResult & { originalPath: string; batchIndex: number })[];
|
||||||
|
errors: UploadError[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmartDownloadOptions {
|
||||||
|
forceRefresh?: boolean;
|
||||||
|
verifyIntegrity?: boolean;
|
||||||
|
cacheKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmartDownloadResult {
|
||||||
|
blobId: string;
|
||||||
|
content: Buffer;
|
||||||
|
size: number;
|
||||||
|
storedUntil: Date;
|
||||||
|
verified: boolean;
|
||||||
|
metadata: any;
|
||||||
|
fromCache: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedFile extends SmartDownloadResult {
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RetryConfig {
|
||||||
|
attempts: number;
|
||||||
|
delay: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. React 浏览器应用 (browser-app)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 自动生成的 React Hook 和组件
|
||||||
|
skill: "walrus-code-generator"
|
||||||
|
// use-case: browser-app
|
||||||
|
// framework: react
|
||||||
|
// features: ["wallet-integration", "progress-tracking", "drag-drop"]
|
||||||
|
|
||||||
|
import React, { useState, useCallback, createContext, useContext } from 'react';
|
||||||
|
import { useWallet } from '@suiet/wallet-kit';
|
||||||
|
|
||||||
|
// Walrus Context
|
||||||
|
const WalrusContext = createContext<{
|
||||||
|
upload: (files: FileList) => Promise<UploadResult[]>;
|
||||||
|
download: (blobId: string, filename?: string) => Promise<void>;
|
||||||
|
isUploading: boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
export const WalrusProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const { connected, signAndExecuteTransactionBlock } = useWallet();
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
|
const upload = useCallback(async (files: FileList): Promise<UploadResult[]> => {
|
||||||
|
if (!connected) {
|
||||||
|
throw new Error('请先连接钱包');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
const results: UploadResult[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await uploadFile(file, signAndExecuteTransactionBlock);
|
||||||
|
results.push({ ...result, originalName: file.name, index: i });
|
||||||
|
} catch (error) {
|
||||||
|
results.push({
|
||||||
|
success: false,
|
||||||
|
originalName: file.name,
|
||||||
|
index: i,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}, [connected, signAndExecuteTransactionBlock]);
|
||||||
|
|
||||||
|
const download = useCallback(async (blobId: string, filename?: string) => {
|
||||||
|
await downloadFile(blobId, filename);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WalrusContext.Provider value={{ upload, download, isUploading }}>
|
||||||
|
{children}
|
||||||
|
</WalrusContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWalrus = () => {
|
||||||
|
const context = useContext(WalrusContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useWalrus must be used within WalrusProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件上传组件
|
||||||
|
export const WalrusUpload: React.FC<{
|
||||||
|
onUploadComplete?: (results: UploadResult[]) => void;
|
||||||
|
maxFiles?: number;
|
||||||
|
acceptedTypes?: string;
|
||||||
|
}> = ({ onUploadComplete, maxFiles = 10, acceptedTypes = '*' }) => {
|
||||||
|
const { upload, isUploading } = useWalrus();
|
||||||
|
const [dragActive, setDragActive] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFiles = useCallback(async (files: FileList) => {
|
||||||
|
if (files.length > maxFiles) {
|
||||||
|
alert(`最多只能上传 ${maxFiles} 个文件`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setProgress(10);
|
||||||
|
const results = await upload(files);
|
||||||
|
setProgress(100);
|
||||||
|
onUploadComplete?.(results);
|
||||||
|
} catch (error) {
|
||||||
|
alert(`上传失败: ${error.message}`);
|
||||||
|
setProgress(0);
|
||||||
|
}
|
||||||
|
}, [upload, maxFiles, onUploadComplete]);
|
||||||
|
|
||||||
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragActive(false);
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
handleFiles(files);
|
||||||
|
}
|
||||||
|
}, [handleFiles]);
|
||||||
|
|
||||||
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
handleFiles(files);
|
||||||
|
}
|
||||||
|
}, [handleFiles]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="walrus-upload">
|
||||||
|
<div
|
||||||
|
className={`upload-area ${dragActive ? 'active' : ''} ${isUploading ? 'uploading' : ''}`}
|
||||||
|
onDragEnter={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragActive(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragActive(false);
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept={acceptedTypes}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isUploading ? (
|
||||||
|
<div className="upload-progress">
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div
|
||||||
|
className="progress-fill"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>上传中... {progress}%</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="upload-prompt">
|
||||||
|
<div className="upload-icon">📁</div>
|
||||||
|
<h3>拖拽文件到此处或点击上传</h3>
|
||||||
|
<p>支持最多 {maxFiles} 个文件</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件列表组件
|
||||||
|
export const WalrusFileList: React.FC<{
|
||||||
|
files: UploadResult[];
|
||||||
|
onDownload?: (blobId: string, filename: string) => void;
|
||||||
|
}> = ({ files, onDownload }) => {
|
||||||
|
const { download } = useWalrus();
|
||||||
|
|
||||||
|
const handleDownload = useCallback(async (blobId: string, filename: string) => {
|
||||||
|
try {
|
||||||
|
await onDownload ? onDownload(blobId, filename) : download(blobId, filename);
|
||||||
|
} catch (error) {
|
||||||
|
alert(`下载失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}, [download, onDownload]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-list">
|
||||||
|
<h3>文件列表</h3>
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<div key={index} className={`file-item ${file.success ? 'success' : 'error'}`}>
|
||||||
|
<div className="file-info">
|
||||||
|
<span className="file-name">{file.originalName}</span>
|
||||||
|
{file.success ? (
|
||||||
|
<span className="file-success">✓ 上传成功</span>
|
||||||
|
) : (
|
||||||
|
<span className="file-error">✗ {file.error}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{file.success && file.blobId && (
|
||||||
|
<div className="file-actions">
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownload(file.blobId!, file.originalName)}
|
||||||
|
className="download-btn"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</button>
|
||||||
|
<span className="blob-id">{file.blobId.slice(0, 16)}...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数
|
||||||
|
async function uploadFile(
|
||||||
|
file: File,
|
||||||
|
signAndExecuteTransactionBlock: any
|
||||||
|
): Promise<UploadResult> {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const walrusFile = WalrusFile.from({
|
||||||
|
contents: new Uint8Array(arrayBuffer),
|
||||||
|
identifier: `browser/${Date.now()}_${file.name}`,
|
||||||
|
mimeType: file.type,
|
||||||
|
attributes: {
|
||||||
|
'original-name': file.name,
|
||||||
|
'file-size': file.size.toString(),
|
||||||
|
'upload-source': 'browser'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const suiClient = new SuiClient({
|
||||||
|
url: getFullnodeUrl('testnet'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const walrusClient = new WalrusClient({
|
||||||
|
network: 'testnet',
|
||||||
|
suiClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
const writeFlow = walrusClient.walrus.writeFilesFlow({
|
||||||
|
files: [walrusFile],
|
||||||
|
epochs: 10,
|
||||||
|
deletable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await writeFlow.executeWithSigner({
|
||||||
|
signAndExecuteTransactionBlock
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
blobId: result[0].blobId,
|
||||||
|
size: file.size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFile(blobId: string, filename?: string) {
|
||||||
|
// 实现文件下载逻辑
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = `#download-${blobId}`;
|
||||||
|
link.download = filename || 'downloaded-file';
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadResult {
|
||||||
|
success: boolean;
|
||||||
|
blobId?: string;
|
||||||
|
size?: number;
|
||||||
|
originalName: string;
|
||||||
|
index: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基础代码生成
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-code-generator"
|
||||||
|
// use-case: basic-upload
|
||||||
|
// framework: nodejs
|
||||||
|
// features: ["error-handling", "logging"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### React 应用生成
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-code-generator"
|
||||||
|
// use-case: browser-app
|
||||||
|
// framework: react
|
||||||
|
// features: ["wallet-integration", "drag-drop", "progress-bar"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级存储系统
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-code-generator"
|
||||||
|
// use-case: advanced-storage
|
||||||
|
// framework: nodejs
|
||||||
|
// features: ["batch-operations", "caching", "retry-logic", "metadata-indexing"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量处理系统
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-code-generator"
|
||||||
|
// use-case: batch-processing
|
||||||
|
// framework: express
|
||||||
|
// features: ["api-endpoints", "queue-system", "monitoring"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生成的代码特性
|
||||||
|
|
||||||
|
### 🛡️ 错误处理
|
||||||
|
- 智能重试机制
|
||||||
|
- 详细错误信息
|
||||||
|
- 优雅降级策略
|
||||||
|
|
||||||
|
### 📊 进度跟踪
|
||||||
|
- 实时上传进度
|
||||||
|
- 批量操作状态
|
||||||
|
- 性能监控
|
||||||
|
|
||||||
|
### 🔧 配置管理
|
||||||
|
- 灵活的配置选项
|
||||||
|
- 环境适配
|
||||||
|
- 类型安全
|
||||||
|
|
||||||
|
### 🚀 性能优化
|
||||||
|
- 缓存机制
|
||||||
|
- 并发控制
|
||||||
|
- 内存管理
|
||||||
|
|
||||||
|
### 🔒 安全考虑
|
||||||
|
- 输入验证
|
||||||
|
- 文件完整性检查
|
||||||
|
- 访问控制
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **错误处理** - 生成的代码包含完整的错误处理逻辑
|
||||||
|
2. **类型安全** - 使用 TypeScript 提供完整的类型定义
|
||||||
|
3. **性能优化** - 包含缓存、批量处理和并发控制
|
||||||
|
4. **可扩展性** - 模块化设计,易于扩展和维护
|
||||||
|
5. **监控日志** - 集成日志记录和性能监控
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*更新时间:2025-11-11*
|
||||||
828
skills/walrus-debugger.md
Normal file
828
skills/walrus-debugger.md
Normal file
@@ -0,0 +1,828 @@
|
|||||||
|
---
|
||||||
|
name: walrus-debugger
|
||||||
|
description: Walrus 错误诊断专家 - 智能分析和解决 Walrus 应用中的问题
|
||||||
|
parameters:
|
||||||
|
- name: error-type
|
||||||
|
type: string
|
||||||
|
description: 错误类型 (upload/download/network/configuration/timeout/authentication)
|
||||||
|
required: true
|
||||||
|
- name: error-context
|
||||||
|
type: string
|
||||||
|
description: 错误信息、代码片段或问题描述
|
||||||
|
required: true
|
||||||
|
- name: environment
|
||||||
|
type: string
|
||||||
|
description: 运行环境 (browser/nodejs/nextjs/production)
|
||||||
|
default: nodejs
|
||||||
|
---
|
||||||
|
|
||||||
|
# Walrus 错误诊断技能
|
||||||
|
|
||||||
|
## 技能概述
|
||||||
|
|
||||||
|
`walrus-debugger` 是专业的错误诊断助手,专门分析 Walrus 应用中的各种问题,提供智能的解决方案和预防措施。
|
||||||
|
|
||||||
|
## 常见错误类型及解决方案
|
||||||
|
|
||||||
|
### 1. 上传失败错误
|
||||||
|
|
||||||
|
#### 错误类型: Upload Failed
|
||||||
|
```typescript
|
||||||
|
// 错误示例
|
||||||
|
Error: Upload failed: insufficient storage balance
|
||||||
|
Error: Upload failed: blob size exceeds maximum limit
|
||||||
|
Error: Upload failed: transaction rejected
|
||||||
|
|
||||||
|
// 诊断和解决方案
|
||||||
|
class UploadErrorDiagnostics {
|
||||||
|
async diagnoseUploadError(error: Error, context: UploadContext): Promise<Diagnosis> {
|
||||||
|
const analysis = this.analyzeErrorMessage(error.message);
|
||||||
|
|
||||||
|
switch (analysis.type) {
|
||||||
|
case 'INSUFFICIENT_BALANCE':
|
||||||
|
return this.diagnoseBalanceError(context);
|
||||||
|
case 'SIZE_LIMIT':
|
||||||
|
return this.diagnoseSizeError(context);
|
||||||
|
case 'TRANSACTION_REJECTED':
|
||||||
|
return this.diagnoseTransactionError(error, context);
|
||||||
|
case 'TIMEOUT':
|
||||||
|
return this.diagnoseTimeoutError(context);
|
||||||
|
default:
|
||||||
|
return this.diagnoseGenericUploadError(error, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async diagnoseBalanceError(context: UploadContext): Promise<Diagnosis> {
|
||||||
|
const walletBalance = await this.checkWalletBalance(context.signerAddress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorType: 'INSUFFICIENT_BALANCE',
|
||||||
|
description: '钱包余额不足,无法支付存储费用',
|
||||||
|
causes: [
|
||||||
|
'WAL 代币余额不足',
|
||||||
|
'SUI 代币余额不足以支付 gas 费',
|
||||||
|
'存储成本计算错误'
|
||||||
|
],
|
||||||
|
solutions: [
|
||||||
|
{
|
||||||
|
title: '检查并充值钱包',
|
||||||
|
code: `
|
||||||
|
// 检查钱包余额
|
||||||
|
async function checkBalances(address: string) {
|
||||||
|
const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') });
|
||||||
|
const balance = await suiClient.getBalance({ owner: address });
|
||||||
|
|
||||||
|
console.log('SUI Balance:', balance.totalBalance);
|
||||||
|
|
||||||
|
// 检查 WAL 余额(如果支持)
|
||||||
|
const walBalance = await suiClient.getBalance({
|
||||||
|
owner: address,
|
||||||
|
coinType: '0x2::sui::SUI' // 替换为 WAL coin type
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sui: balance.totalBalance, wal: walBalance.totalBalance };
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '优化存储策略',
|
||||||
|
code: `
|
||||||
|
// 使用更短的存储周期
|
||||||
|
const result = await client.walrus.writeFiles({
|
||||||
|
files: [file],
|
||||||
|
epochs: 5, // 减少存储周期
|
||||||
|
deletable: true,
|
||||||
|
signer: keypair
|
||||||
|
});
|
||||||
|
|
||||||
|
// 或者使用更小的文件
|
||||||
|
function compressFile(file: File): Promise<File> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (file.size > 10 * 1024 * 1024) { // 10MB
|
||||||
|
// 压缩或分割文件
|
||||||
|
compressImage(file).then(resolve);
|
||||||
|
} else {
|
||||||
|
resolve(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
prevention: [
|
||||||
|
'定期检查钱包余额',
|
||||||
|
'设置余额预警',
|
||||||
|
'实施费用估算功能'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async diagnoseSizeError(context: UploadContext): Promise<Diagnosis> {
|
||||||
|
const fileStats = await this.analyzeFileSize(context.file);
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorType: 'SIZE_LIMIT',
|
||||||
|
description: '文件大小超出限制',
|
||||||
|
causes: [
|
||||||
|
`文件大小 ${fileStats.size}MB 超出限制`,
|
||||||
|
'网络环境不支持大文件传输',
|
||||||
|
'客户端内存不足'
|
||||||
|
],
|
||||||
|
solutions: [
|
||||||
|
{
|
||||||
|
title: '文件压缩',
|
||||||
|
code: `
|
||||||
|
// 图片压缩
|
||||||
|
async function compressImage(file: File, quality = 0.8): Promise<File> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
// 计算压缩后的尺寸
|
||||||
|
const scale = Math.min(1, 1920 / Math.max(img.width, img.height));
|
||||||
|
canvas.width = img.width * scale;
|
||||||
|
canvas.height = img.height * scale;
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
resolve(new File([blob!], file.name, { type: file.type }));
|
||||||
|
}, file.type, quality);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文本文件压缩
|
||||||
|
async function compressTextFile(file: File): Promise<File> {
|
||||||
|
const text = await file.text();
|
||||||
|
const compressed = await this.compressString(text);
|
||||||
|
return new File([compressed], file.name + '.gz', { type: 'application/gzip' });
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件分割上传',
|
||||||
|
code: `
|
||||||
|
// 分割大文件
|
||||||
|
async function splitAndUpload(file: File, chunkSize = 5 * 1024 * 1024): Promise<string[]> {
|
||||||
|
const chunks: string[] = [];
|
||||||
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||||
|
|
||||||
|
for (let i = 0; i < totalChunks; i++) {
|
||||||
|
const start = i * chunkSize;
|
||||||
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
|
||||||
|
const chunkFile = new File([chunk], \`\${file.name}.part\${i}\`);
|
||||||
|
const result = await uploadSingleFile(chunkFile);
|
||||||
|
chunks.push(result.blobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建清单文件记录分割信息
|
||||||
|
async function createManifest(chunks: string[], originalFile: File): Promise<string> {
|
||||||
|
const manifest = {
|
||||||
|
originalName: originalFile.name,
|
||||||
|
originalSize: originalFile.size,
|
||||||
|
chunks: chunks.map((blobId, index) => ({
|
||||||
|
blobId,
|
||||||
|
index,
|
||||||
|
size: originalFile.size / chunks.length
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const manifestBlob = new Blob([JSON.stringify(manifest)], { type: 'application/json' });
|
||||||
|
const manifestFile = new File([manifestBlob], \`\${originalFile.name}.manifest\`);
|
||||||
|
|
||||||
|
const result = await uploadSingleFile(manifestFile);
|
||||||
|
return result.blobId;
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
prevention: [
|
||||||
|
'预先检查文件大小',
|
||||||
|
'实施文件大小限制',
|
||||||
|
'提供文件压缩工具'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 网络连接错误
|
||||||
|
|
||||||
|
#### 错误类型: Network Error
|
||||||
|
```typescript
|
||||||
|
// 错误示例
|
||||||
|
Error: Network timeout
|
||||||
|
Error: Connection refused
|
||||||
|
Error: Failed to fetch
|
||||||
|
|
||||||
|
class NetworkErrorDiagnostics {
|
||||||
|
async diagnoseNetworkError(error: Error, context: NetworkContext): Promise<Diagnosis> {
|
||||||
|
const networkStatus = await this.checkNetworkStatus();
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorType: 'NETWORK_ERROR',
|
||||||
|
description: '网络连接问题',
|
||||||
|
causes: [
|
||||||
|
'网络连接不稳定',
|
||||||
|
'RPC 节点不可用',
|
||||||
|
'防火墙或代理阻止连接',
|
||||||
|
'DNS 解析失败'
|
||||||
|
],
|
||||||
|
solutions: [
|
||||||
|
{
|
||||||
|
title: '实施自动重试机制',
|
||||||
|
code: `
|
||||||
|
class RobustWalrusClient {
|
||||||
|
private readonly MAX_RETRIES = 3;
|
||||||
|
private readonly RETRY_DELAY_BASE = 1000;
|
||||||
|
|
||||||
|
async withRetry<T>(
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
operationName: string
|
||||||
|
): Promise<T> {
|
||||||
|
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
if (!this.isRetryableError(error) || attempt === this.MAX_RETRIES) {
|
||||||
|
throw new Error(\`\${operationName} failed after \${attempt} attempts: \${error.message}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = this.RETRY_DELAY_BASE * Math.pow(2, attempt - 1);
|
||||||
|
console.warn(\`\${operationName} failed (attempt \${attempt}/\${this.MAX_RETRIES}), retrying in \${delay}ms\`);
|
||||||
|
|
||||||
|
await this.sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Should not reach here');
|
||||||
|
}
|
||||||
|
|
||||||
|
private isRetryableError(error: any): boolean {
|
||||||
|
return error instanceof RetryableWalrusClientError ||
|
||||||
|
error.message.includes('timeout') ||
|
||||||
|
error.message.includes('network') ||
|
||||||
|
error.message.includes('connection') ||
|
||||||
|
error.status >= 500; // Server errors
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFiles(files: WalrusFile[]): Promise<any> {
|
||||||
|
return this.withRetry(
|
||||||
|
() => this.client.walrus.writeFiles({ files, epochs: 10, signer: this.signer }),
|
||||||
|
'File upload'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '实施节点健康检查',
|
||||||
|
code: `
|
||||||
|
// 节点健康检查
|
||||||
|
class NodeHealthChecker {
|
||||||
|
private nodes: string[] = [
|
||||||
|
'https://fullnode.testnet.sui.io',
|
||||||
|
'https://fullnode.testnet.sui.io:443',
|
||||||
|
// 添加备用节点
|
||||||
|
];
|
||||||
|
|
||||||
|
async getHealthyNode(): Promise<string> {
|
||||||
|
for (const node of this.nodes) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(\`\${node}/\`, {
|
||||||
|
method: 'HEAD',
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log(\`Node \${node} is healthy\`);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(\`Node \${node} is unhealthy: \${error.message}\`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No healthy nodes available');
|
||||||
|
}
|
||||||
|
|
||||||
|
async testAllNodes(): Promise<NodeHealth[]> {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
this.nodes.map(async (node) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
try {
|
||||||
|
const response = await fetch(\`\${node}/\`, { method: 'HEAD', timeout: 5000 });
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
healthy: response.ok,
|
||||||
|
responseTime: Date.now() - startTime,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
healthy: false,
|
||||||
|
responseTime: Date.now() - startTime,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.map(result =>
|
||||||
|
result.status === 'fulfilled' ? result.value : null
|
||||||
|
).filter(Boolean) as NodeHealth[];
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '配置超时和重试',
|
||||||
|
code: `
|
||||||
|
// 配置自定义超时和重试
|
||||||
|
const client = new SuiJsonRpcClient({
|
||||||
|
url: getFullnodeUrl('testnet'),
|
||||||
|
network: 'testnet',
|
||||||
|
}).$extend(walrus({
|
||||||
|
storageNodeClientOptions: {
|
||||||
|
timeout: 60000, // 60秒超时
|
||||||
|
retryAttempts: 5,
|
||||||
|
retryDelay: 2000,
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Storage node error:', error);
|
||||||
|
// 发送错误到监控系统
|
||||||
|
this.trackError('storage_node_error', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
prevention: [
|
||||||
|
'实施网络监控',
|
||||||
|
'使用多个备用节点',
|
||||||
|
'设置合理的超时时间',
|
||||||
|
'实施断路器模式'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkNetworkStatus(): Promise<NetworkStatus> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://httpbin.org/ip');
|
||||||
|
const ipInfo = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
online: true,
|
||||||
|
ip: ipInfo.ip,
|
||||||
|
latency: Date.now() - this.startTime
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
online: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置错误
|
||||||
|
|
||||||
|
#### 错误类型: Configuration Error
|
||||||
|
```typescript
|
||||||
|
// 错误示例
|
||||||
|
Error: Invalid network configuration
|
||||||
|
Error: WASM module failed to load
|
||||||
|
Error: Invalid signer configuration
|
||||||
|
|
||||||
|
class ConfigurationErrorDiagnostics {
|
||||||
|
async diagnoseConfigError(error: Error, context: ConfigContext): Promise<Diagnosis> {
|
||||||
|
return {
|
||||||
|
errorType: 'CONFIGURATION_ERROR',
|
||||||
|
description: '配置问题',
|
||||||
|
causes: [
|
||||||
|
'网络配置错误',
|
||||||
|
'WASM 模块路径错误',
|
||||||
|
'签名者配置无效',
|
||||||
|
'环境变量缺失'
|
||||||
|
],
|
||||||
|
solutions: [
|
||||||
|
{
|
||||||
|
title: '验证配置完整性',
|
||||||
|
code: `
|
||||||
|
// 配置验证器
|
||||||
|
class ConfigValidator {
|
||||||
|
static validateWalrusConfig(config: any): ValidationResult {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// 验证必需字段
|
||||||
|
if (!config.network) {
|
||||||
|
errors.push('network is required (testnet/mainnet)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.rpcUrl) {
|
||||||
|
errors.push('rpcUrl is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证网络配置
|
||||||
|
if (config.network && !['testnet', 'mainnet'].includes(config.network)) {
|
||||||
|
errors.push('network must be either "testnet" or "mainnet"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 URL 格式
|
||||||
|
if (config.rpcUrl && !this.isValidUrl(config.rpcUrl)) {
|
||||||
|
errors.push('rpcUrl must be a valid URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证超时配置
|
||||||
|
if (config.timeout && (config.timeout < 1000 || config.timeout > 300000)) {
|
||||||
|
errors.push('timeout must be between 1000ms and 300000ms');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async validateEnvironment(): Promise<ValidationResult> {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// 检查环境变量
|
||||||
|
const requiredEnvVars = ['WALRUS_NETWORK', 'SUI_RPC_URL'];
|
||||||
|
for (const envVar of requiredEnvVars) {
|
||||||
|
if (!process.env[envVar]) {
|
||||||
|
errors.push(\`Environment variable \${envVar} is required\`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 WASM 支持
|
||||||
|
if (typeof window !== 'undefined' && !window.WebAssembly) {
|
||||||
|
errors.push('WebAssembly is not supported in this browser');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证网络连接
|
||||||
|
try {
|
||||||
|
const response = await fetch(process.env.SUI_RPC_URL!, {
|
||||||
|
method: 'HEAD',
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
errors.push('Cannot connect to SUI RPC endpoint');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(\`Network connection failed: \${error.message}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isValidUrl(string: string): boolean {
|
||||||
|
try {
|
||||||
|
new URL(string);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用配置验证
|
||||||
|
const config = {
|
||||||
|
network: 'testnet',
|
||||||
|
rpcUrl: getFullnodeUrl('testnet'),
|
||||||
|
timeout: 30000
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = ConfigValidator.validateWalrusConfig(config);
|
||||||
|
if (!validation.valid) {
|
||||||
|
console.error('Configuration validation failed:', validation.errors);
|
||||||
|
throw new Error('Invalid configuration');
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WASM 模块配置',
|
||||||
|
code: `
|
||||||
|
// WASM 配置验证和加载
|
||||||
|
class WasmLoader {
|
||||||
|
static async loadWasmWithFallback(): Promise<any> {
|
||||||
|
const strategies = [
|
||||||
|
() => this.loadFromBundle(),
|
||||||
|
() => this.loadFromCDN(),
|
||||||
|
() => this.loadFromLocal()
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const strategy of strategies) {
|
||||||
|
try {
|
||||||
|
const wasm = await strategy();
|
||||||
|
console.log('WASM loaded successfully');
|
||||||
|
return wasm;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('WASM loading strategy failed:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('All WASM loading strategies failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadFromBundle(): Promise<any> {
|
||||||
|
// Vite 示例
|
||||||
|
if (typeof import.meta !== 'undefined' && import.meta.url) {
|
||||||
|
const wasmUrl = new URL('@mysten/walrus-wasm/web/walrus_wasm_bg.wasm', import.meta.url);
|
||||||
|
const wasmModule = await import(wasmUrl);
|
||||||
|
return wasmModule.default;
|
||||||
|
}
|
||||||
|
throw new Error('Bundle loading not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadFromCDN(): Promise<any> {
|
||||||
|
const cdnUrls = [
|
||||||
|
'https://unpkg.com/@mysten/walrus-wasm@latest/web/walrus_wasm_bg.wasm',
|
||||||
|
'https://cdn.jsdelivr.net/npm/@mysten/walrus-wasm@latest/web/walrus_wasm_bg.wasm'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const url of cdnUrls) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
const wasmBuffer = await response.arrayBuffer();
|
||||||
|
return WebAssembly.compile(wasmBuffer);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(\`CDN loading failed for \${url}:\`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('All CDN URLs failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async loadFromLocal(): Promise<any> {
|
||||||
|
// 尝试从本地路径加载
|
||||||
|
const localPaths = [
|
||||||
|
'/walrus/walrus_wasm_bg.wasm',
|
||||||
|
'./node_modules/@mysten/walrus-wasm/web/walrus_wasm_bg.wasm'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of localPaths) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(path);
|
||||||
|
if (response.ok) {
|
||||||
|
const wasmBuffer = await response.arrayBuffer();
|
||||||
|
return WebAssembly.compile(wasmBuffer);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(\`Local loading failed for \${path}:\`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Local loading failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在客户端配置中使用 WASM
|
||||||
|
const client = new SuiJsonRpcClient({
|
||||||
|
url: getFullnodeUrl('testnet'),
|
||||||
|
network: 'testnet',
|
||||||
|
}).$extend(
|
||||||
|
walrus({
|
||||||
|
wasmUrl: await WasmLoader.loadWasmWithFallback()
|
||||||
|
})
|
||||||
|
);`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
prevention: [
|
||||||
|
'实施配置验证',
|
||||||
|
'提供配置模板',
|
||||||
|
'设置默认值',
|
||||||
|
'实施配置监控'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 浏览器环境特定错误
|
||||||
|
|
||||||
|
#### 错误类型: Browser Environment Error
|
||||||
|
```typescript
|
||||||
|
class BrowserErrorDiagnostics {
|
||||||
|
async diagnoseBrowserError(error: Error, context: BrowserContext): Promise<Diagnosis> {
|
||||||
|
return {
|
||||||
|
errorType: 'BROWSER_ERROR',
|
||||||
|
description: '浏览器环境特定问题',
|
||||||
|
causes: [
|
||||||
|
'WASM 不支持',
|
||||||
|
'CORS 策略阻止',
|
||||||
|
'内存不足',
|
||||||
|
'钱包连接问题'
|
||||||
|
],
|
||||||
|
solutions: [
|
||||||
|
{
|
||||||
|
title: '浏览器兼容性检查',
|
||||||
|
code: `
|
||||||
|
// 浏览器兼容性检查
|
||||||
|
class BrowserCompatibility {
|
||||||
|
static checkCompatibility(): BrowserCompatibilityResult {
|
||||||
|
const checks = {
|
||||||
|
webAssembly: this.checkWebAssembly(),
|
||||||
|
fetch: this.checkFetch(),
|
||||||
|
blob: this.checkBlob(),
|
||||||
|
fileAPI: this.checkFileAPI(),
|
||||||
|
webWorkers: this.checkWebWorkers(),
|
||||||
|
localStorage: this.checkLocalStorage()
|
||||||
|
};
|
||||||
|
|
||||||
|
const supported = Object.values(checks).every(check => check.supported);
|
||||||
|
|
||||||
|
return {
|
||||||
|
supported,
|
||||||
|
checks,
|
||||||
|
recommendations: this.getRecommendations(checks)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static checkWebAssembly(): CompatibilityCheck {
|
||||||
|
const supported = typeof WebAssembly === 'object' && WebAssembly !== null;
|
||||||
|
return {
|
||||||
|
supported,
|
||||||
|
message: supported ? 'WebAssembly is supported' : 'WebAssembly is not supported',
|
||||||
|
fix: !supported ? 'Please use a modern browser that supports WebAssembly' : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getRecommendations(checks: Record<string, CompatibilityCheck>): string[] {
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
if (!checks.webAssembly.supported) {
|
||||||
|
recommendations.push('Upgrade to a modern browser (Chrome 57+, Firefox 52+, Safari 11+)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checks.webWorkers.supported) {
|
||||||
|
recommendations.push('Consider enabling Web Workers for better performance');
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用兼容性检查
|
||||||
|
const compatibility = BrowserCompatibility.checkCompatibility();
|
||||||
|
if (!compatibility.supported) {
|
||||||
|
console.error('Browser compatibility issues:', compatibility.recommendations);
|
||||||
|
// 显示用户友好的错误信息
|
||||||
|
this.showCompatibilityError(compatibility);
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '内存管理',
|
||||||
|
code: `
|
||||||
|
// 浏览器内存管理
|
||||||
|
class BrowserMemoryManager {
|
||||||
|
private memoryThreshold = 100 * 1024 * 1024; // 100MB
|
||||||
|
|
||||||
|
async checkMemoryUsage(): Promise<MemoryUsage> {
|
||||||
|
if ('memory' in performance) {
|
||||||
|
const memory = (performance as any).memory;
|
||||||
|
return {
|
||||||
|
used: memory.usedJSHeapSize,
|
||||||
|
total: memory.totalJSHeapSize,
|
||||||
|
limit: memory.jsHeapSizeLimit,
|
||||||
|
usageRatio: memory.usedJSHeapSize / memory.jsHeapSizeLimit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { used: 0, total: 0, limit: 0, usageRatio: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async manageMemoryForLargeFile(file: File): Promise<boolean> {
|
||||||
|
const memoryUsage = await this.checkMemoryUsage();
|
||||||
|
|
||||||
|
// 检查是否有足够内存处理文件
|
||||||
|
const estimatedUsage = file.size * 3; // 估算处理过程中需要的内存
|
||||||
|
const availableMemory = memoryUsage.limit - memoryUsage.used;
|
||||||
|
|
||||||
|
if (estimatedUsage > availableMemory) {
|
||||||
|
console.warn('Insufficient memory for file processing');
|
||||||
|
|
||||||
|
// 触发垃圾回收(如果可能)
|
||||||
|
if (window.gc) {
|
||||||
|
window.gc();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新检查内存
|
||||||
|
const newUsage = await this.checkMemoryUsage();
|
||||||
|
return (file.size * 3) < (newUsage.limit - newUsage.used);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流式处理大文件
|
||||||
|
async processLargeFileInChunks(
|
||||||
|
file: File,
|
||||||
|
processor: (chunk: Uint8Array) => Promise<void>,
|
||||||
|
chunkSize = 1024 * 1024 // 1MB
|
||||||
|
): Promise<void> {
|
||||||
|
const reader = new FileReader();
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
const processChunk = () => {
|
||||||
|
if (offset >= file.size) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const chunk = file.slice(offset, offset + chunkSize);
|
||||||
|
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
try {
|
||||||
|
if (event.target?.result) {
|
||||||
|
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
await processor(uint8Array);
|
||||||
|
offset += chunkSize;
|
||||||
|
|
||||||
|
// 检查内存使用
|
||||||
|
const memoryUsage = await this.checkMemoryUsage();
|
||||||
|
if (memoryUsage.usageRatio > 0.8) {
|
||||||
|
console.warn('High memory usage, forcing garbage collection');
|
||||||
|
if (window.gc) window.gc();
|
||||||
|
}
|
||||||
|
|
||||||
|
await processChunk();
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => reject(new Error('File reading failed'));
|
||||||
|
reader.readAsArrayBuffer(chunk);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return processChunk();
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
prevention: [
|
||||||
|
'实施浏览器兼容性检查',
|
||||||
|
'监控内存使用',
|
||||||
|
'提供降级方案',
|
||||||
|
'实施优雅错误处理'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 上传错误诊断
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-debugger"
|
||||||
|
// error-type: upload
|
||||||
|
// error-context: Upload failed: insufficient storage balance, trying to upload 10MB file to testnet
|
||||||
|
// environment: browser
|
||||||
|
```
|
||||||
|
|
||||||
|
### 网络连接问题
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-debugger"
|
||||||
|
// error-type: network
|
||||||
|
// error-context: Network timeout when connecting to storage nodes, intermittent connection issues
|
||||||
|
// environment: production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置验证
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-debugger"
|
||||||
|
// error-type: configuration
|
||||||
|
// error-context: WASM module failed to load in Vite production build, error: "Failed to resolve module"
|
||||||
|
// environment: nodejs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能问题
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-debugger"
|
||||||
|
// error-type: performance
|
||||||
|
// error-context: Memory usage spikes when uploading multiple large files, browser crashes
|
||||||
|
// environment: browser
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*更新时间:2025-11-11*
|
||||||
757
skills/walrus-monitor.md
Normal file
757
skills/walrus-monitor.md
Normal file
@@ -0,0 +1,757 @@
|
|||||||
|
---
|
||||||
|
name: walrus-monitor
|
||||||
|
description: Walrus 监控分析专家 - 存储使用监控、成本分析和性能指标跟踪
|
||||||
|
parameters:
|
||||||
|
- name: monitor-type
|
||||||
|
type: string
|
||||||
|
description: 监控类型 (usage/cost/performance/errors/analytics)
|
||||||
|
required: true
|
||||||
|
- name: time-range
|
||||||
|
type: string
|
||||||
|
description: 时间范围 (1h/24h/7d/30d/custom)
|
||||||
|
default: 24h
|
||||||
|
- name: metrics
|
||||||
|
type: array
|
||||||
|
description: 需要监控的具体指标
|
||||||
|
default: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Walrus 监控分析技能
|
||||||
|
|
||||||
|
## 技能概述
|
||||||
|
|
||||||
|
`walrus-monitor` 是专业的监控分析助手,提供全面的 Walrus 存储系统监控、成本分析和性能指标跟踪功能,帮助开发者优化资源使用和控制成本。
|
||||||
|
|
||||||
|
## 监控维度
|
||||||
|
|
||||||
|
### 1. 存储使用监控
|
||||||
|
|
||||||
|
#### 实时存储指标
|
||||||
|
```typescript
|
||||||
|
class StorageMonitor {
|
||||||
|
private metrics: StorageMetrics[] = [];
|
||||||
|
private alerts: Alert[] = [];
|
||||||
|
|
||||||
|
async collectStorageMetrics(): Promise<StorageMetrics> {
|
||||||
|
const client = await this.getWalrusClient();
|
||||||
|
const accountAddress = this.getAccountAddress();
|
||||||
|
|
||||||
|
// 获取账户存储信息
|
||||||
|
const storageInfo = await client.getStorageInfo(accountAddress);
|
||||||
|
const blobIds = storageInfo.storedBlobs || [];
|
||||||
|
|
||||||
|
// 计算存储统计
|
||||||
|
const metrics: StorageMetrics = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
totalFiles: blobIds.length,
|
||||||
|
totalSize: 0,
|
||||||
|
totalCost: 0,
|
||||||
|
storageDistribution: {},
|
||||||
|
fileAges: [],
|
||||||
|
oldestFile: null,
|
||||||
|
newestFile: null,
|
||||||
|
expiringSoon: [],
|
||||||
|
expired: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分析每个文件
|
||||||
|
for (const blobId of blobIds) {
|
||||||
|
try {
|
||||||
|
const [file] = await client.walrus.getFiles({ ids: [blobId] });
|
||||||
|
const metadata = await this.getFileMetadata(file);
|
||||||
|
const storedUntil = await file.storedUntil();
|
||||||
|
|
||||||
|
metrics.totalSize += metadata.size;
|
||||||
|
metrics.totalCost += metadata.estimatedCost;
|
||||||
|
|
||||||
|
// 文件类型分布
|
||||||
|
const fileType = this.getFileType(metadata.mimeType);
|
||||||
|
metrics.storageDistribution[fileType] =
|
||||||
|
(metrics.storageDistribution[fileType] || 0) + 1;
|
||||||
|
|
||||||
|
// 文件年龄分析
|
||||||
|
const age = Date.now() - metadata.uploadTime;
|
||||||
|
metrics.fileAges.push(age);
|
||||||
|
|
||||||
|
// 检查过期状态
|
||||||
|
const daysToExpiry = Math.floor((storedUntil.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
||||||
|
if (daysToExpiry < 7) {
|
||||||
|
metrics.expiringSoon.push({
|
||||||
|
blobId,
|
||||||
|
fileName: metadata.identifier,
|
||||||
|
daysToExpiry,
|
||||||
|
size: metadata.size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysToExpiry < 0) {
|
||||||
|
metrics.expired.push({
|
||||||
|
blobId,
|
||||||
|
fileName: metadata.identifier,
|
||||||
|
daysExpired: Math.abs(daysToExpiry),
|
||||||
|
size: metadata.size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最新和最旧文件
|
||||||
|
if (!metrics.oldestFile || age > metrics.oldestFile.age) {
|
||||||
|
metrics.oldestFile = {
|
||||||
|
blobId,
|
||||||
|
fileName: metadata.identifier,
|
||||||
|
age,
|
||||||
|
uploadDate: new Date(metadata.uploadTime)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!metrics.newestFile || age < metrics.newestFile.age) {
|
||||||
|
metrics.newestFile = {
|
||||||
|
blobId,
|
||||||
|
fileName: metadata.identifier,
|
||||||
|
age,
|
||||||
|
uploadDate: new Date(metadata.uploadTime)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to analyze blob ${blobId}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储指标历史
|
||||||
|
this.metrics.push(metrics);
|
||||||
|
if (this.metrics.length > 1000) {
|
||||||
|
this.metrics.shift(); // 保留最近1000条记录
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查告警条件
|
||||||
|
this.checkStorageAlerts(metrics);
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkStorageAlerts(metrics: StorageMetrics): void {
|
||||||
|
// 存储空间告警
|
||||||
|
if (metrics.totalSize > 100 * 1024 * 1024 * 1024) { // 100GB
|
||||||
|
this.createAlert({
|
||||||
|
type: 'STORAGE_LIMIT',
|
||||||
|
severity: 'WARNING',
|
||||||
|
message: `存储使用量达到 ${this.formatBytes(metrics.totalSize)}`,
|
||||||
|
recommendation: '考虑清理过期文件或升级存储计划'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件数量告警
|
||||||
|
if (metrics.totalFiles > 10000) {
|
||||||
|
this.createAlert({
|
||||||
|
type: 'FILE_COUNT_LIMIT',
|
||||||
|
severity: 'INFO',
|
||||||
|
message: `文件数量达到 ${metrics.totalFiles}`,
|
||||||
|
recommendation: '考虑实施文件归档策略'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 即将过期告警
|
||||||
|
if (metrics.expiringSoon.length > 0) {
|
||||||
|
this.createAlert({
|
||||||
|
type: 'EXPIRING_FILES',
|
||||||
|
severity: 'WARNING',
|
||||||
|
message: `${metrics.expiringSoon.length} 个文件将在7天内过期`,
|
||||||
|
recommendation: '续期或备份重要文件'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStorageTrends(timeRange: number = 7 * 24 * 60 * 60 * 1000): StorageTrends {
|
||||||
|
const cutoff = Date.now() - timeRange;
|
||||||
|
const relevantMetrics = this.metrics.filter(m => m.timestamp >= cutoff);
|
||||||
|
|
||||||
|
if (relevantMetrics.length < 2) {
|
||||||
|
return {
|
||||||
|
growthRate: 0,
|
||||||
|
costGrowthRate: 0,
|
||||||
|
fileGrowthRate: 0,
|
||||||
|
projectedUsage: null,
|
||||||
|
projectedCost: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldest = relevantMetrics[0];
|
||||||
|
const newest = relevantMetrics[relevantMetrics.length - 1];
|
||||||
|
const timeDiff = newest.timestamp - oldest.timestamp;
|
||||||
|
|
||||||
|
return {
|
||||||
|
growthRate: this.calculateGrowthRate(oldest.totalSize, newest.totalSize, timeDiff),
|
||||||
|
costGrowthRate: this.calculateGrowthRate(oldest.totalCost, newest.totalCost, timeDiff),
|
||||||
|
fileGrowthRate: this.calculateGrowthRate(oldest.totalFiles, newest.totalFiles, timeDiff),
|
||||||
|
projectedUsage: this.projectUsage(newest.totalSize, this.calculateGrowthRate(oldest.totalSize, newest.totalSize, timeDiff)),
|
||||||
|
projectedCost: this.projectCost(newest.totalCost, this.calculateGrowthRate(oldest.totalCost, newest.totalCost, timeDiff))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 成本分析
|
||||||
|
|
||||||
|
#### 成本监控和优化
|
||||||
|
```typescript
|
||||||
|
class CostAnalyzer {
|
||||||
|
private costHistory: CostRecord[] = [];
|
||||||
|
private pricingModel: PricingModel;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.pricingModel = {
|
||||||
|
storagePerGBPerEpoch: 0.001,
|
||||||
|
writeFeePerMB: 0.0001,
|
||||||
|
readFeePerMB: 0.00001,
|
||||||
|
gasMultiplier: 1.0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyzeCosts(timeRange: number = 30 * 24 * 60 * 60 * 1000): Promise<CostAnalysis> {
|
||||||
|
const records = this.getRelevantCostRecords(timeRange);
|
||||||
|
const storageMetrics = await this.getStorageMetrics(timeRange);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCost: this.calculateTotalCost(records),
|
||||||
|
storageCost: this.calculateStorageCost(storageMetrics),
|
||||||
|
transactionCost: this.calculateTransactionCost(records),
|
||||||
|
breakdown: this.getCostBreakdown(records),
|
||||||
|
optimization: this.getOptimizationSuggestions(storageMetrics, records),
|
||||||
|
forecast: this.forecastCosts(records, storageMetrics)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateStorageCost(metrics: StorageMetrics[]): number {
|
||||||
|
return metrics.reduce((total, metric) => {
|
||||||
|
const storageEpochs = 30; // 假设30天的轮数
|
||||||
|
const sizeGB = metric.totalSize / (1024 * 1024 * 1024);
|
||||||
|
return total + (sizeGB * this.pricingModel.storagePerGBPerEpoch * storageEpochs);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateTransactionCost(records: CostRecord[]): number {
|
||||||
|
return records
|
||||||
|
.filter(r => r.type === 'transaction')
|
||||||
|
.reduce((total, record) => total + record.amount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOptimizationSuggestions(metrics: StorageMetrics[], records: CostRecord[]): OptimizationSuggestion[] {
|
||||||
|
const suggestions: OptimizationSuggestion[] = [];
|
||||||
|
|
||||||
|
// 分析文件大小分布
|
||||||
|
const sizeDistribution = this.analyzeSizeDistribution(metrics);
|
||||||
|
if (sizeDistribution.smallFiles > sizeDistribution.largeFiles * 2) {
|
||||||
|
suggestions.push({
|
||||||
|
type: 'BUNDLE_SMALL_FILES',
|
||||||
|
description: '检测到大量小文件,建议使用 Quilt 打包存储',
|
||||||
|
potentialSavings: '20-30%',
|
||||||
|
difficulty: 'medium',
|
||||||
|
implementation: this.getBundleImplementation()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析存储时长
|
||||||
|
const avgStorageTime = this.calculateAverageStorageTime(metrics);
|
||||||
|
if (avgStorageTime > 90) { // 超过90天
|
||||||
|
suggestions.push({
|
||||||
|
type: 'OPTIMIZE_STORAGE_DURATION',
|
||||||
|
description: `平均存储时长 ${avgStorageTime} 天,建议实施生命周期管理`,
|
||||||
|
potentialSavings: '40-60%',
|
||||||
|
difficulty: 'low',
|
||||||
|
implementation: this.getLifecycleImplementation()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析访问模式
|
||||||
|
const accessPattern = this.analyzeAccessPattern(records);
|
||||||
|
if (accessPattern.readHeavy) {
|
||||||
|
suggestions.push({
|
||||||
|
type: 'IMPLEMENT_CACHING',
|
||||||
|
description: '读取频繁,建议实施缓存层',
|
||||||
|
potentialSavings: '50-70%',
|
||||||
|
difficulty: 'medium',
|
||||||
|
implementation: this.getCacheImplementation()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBundleImplementation(): string {
|
||||||
|
return `
|
||||||
|
// 使用 Quilt 打包小文件
|
||||||
|
async function bundleSmallFiles(files: WalrusFile[]): Promise<string> {
|
||||||
|
const quilt = QuiltBuilder.create()
|
||||||
|
.addFiles(files)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const result = await client.walrus.writeFiles({
|
||||||
|
files: [quilt],
|
||||||
|
epochs: 30,
|
||||||
|
deletable: true,
|
||||||
|
signer: keypair
|
||||||
|
});
|
||||||
|
|
||||||
|
return result[0].blobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原本多个小文件:
|
||||||
|
// File 1: 10KB, File 2: 15KB, File 3: 8KB = 33KB 总计
|
||||||
|
// 打包后:单个 Quilt 包含所有文件,减少存储开销
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLifecycleImplementation(): string {
|
||||||
|
return `
|
||||||
|
// 自动生命周期管理
|
||||||
|
class StorageLifecycleManager {
|
||||||
|
async manageExpiredFiles() {
|
||||||
|
const expiringFiles = await this.getFilesExpiringWithin(7);
|
||||||
|
|
||||||
|
for (const file of expiringFiles) {
|
||||||
|
const accessFrequency = await this.getAccessFrequency(file.blobId);
|
||||||
|
|
||||||
|
if (accessFrequency === 'never') {
|
||||||
|
// 删除未访问的文件
|
||||||
|
await this.deleteFile(file.blobId);
|
||||||
|
} else if (accessFrequency === 'rare') {
|
||||||
|
// 降低存储时长
|
||||||
|
await this.reduceStorageEpochs(file.blobId, 10);
|
||||||
|
} else {
|
||||||
|
// 续期重要文件
|
||||||
|
await this.extendStorage(file.blobId, 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAccessFrequency(blobId: string): Promise<'never' | 'rare' | 'frequent'> {
|
||||||
|
// 分析访问日志
|
||||||
|
const accessLogs = await this.getAccessLogs(blobId, 30); // 30天内
|
||||||
|
const accessCount = accessLogs.length;
|
||||||
|
|
||||||
|
if (accessCount === 0) return 'never';
|
||||||
|
if (accessCount < 5) return 'rare';
|
||||||
|
return 'frequent';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 性能监控
|
||||||
|
|
||||||
|
#### 实时性能指标
|
||||||
|
```typescript
|
||||||
|
class PerformanceMonitor {
|
||||||
|
private performanceMetrics: PerformanceMetric[] = [];
|
||||||
|
private slowQueries: SlowQuery[] = [];
|
||||||
|
private alerts: PerformanceAlert[] = [];
|
||||||
|
|
||||||
|
async recordPerformanceMetric(
|
||||||
|
operation: string,
|
||||||
|
duration: number,
|
||||||
|
success: boolean,
|
||||||
|
metadata: any = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const metric: PerformanceMetric = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
operation,
|
||||||
|
duration,
|
||||||
|
success,
|
||||||
|
metadata
|
||||||
|
};
|
||||||
|
|
||||||
|
this.performanceMetrics.push(metric);
|
||||||
|
|
||||||
|
// 限制历史数据
|
||||||
|
if (this.performanceMetrics.length > 10000) {
|
||||||
|
this.performanceMetrics.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查性能告警
|
||||||
|
this.checkPerformanceAlerts(metric);
|
||||||
|
|
||||||
|
// 记录慢操作
|
||||||
|
if (duration > this.getSlowThreshold(operation)) {
|
||||||
|
this.slowQueries.push({
|
||||||
|
...metric,
|
||||||
|
threshold: this.getSlowThreshold(operation),
|
||||||
|
severity: this.getSeverityLevel(duration, this.getSlowThreshold(operation))
|
||||||
|
});
|
||||||
|
|
||||||
|
// 限制慢查询记录
|
||||||
|
if (this.slowQueries.length > 1000) {
|
||||||
|
this.slowQueries.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPerformanceAnalysis(timeRange: number = 24 * 60 * 60 * 1000): PerformanceAnalysis {
|
||||||
|
const metrics = this.getRelevantMetrics(timeRange);
|
||||||
|
|
||||||
|
return {
|
||||||
|
overview: this.getPerformanceOverview(metrics),
|
||||||
|
operations: this.analyzeOperations(metrics),
|
||||||
|
trends: this.analyzeTrends(metrics),
|
||||||
|
bottlenecks: this.identifyBottlenecks(metrics),
|
||||||
|
recommendations: this.getPerformanceRecommendations(metrics)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private analyzeOperations(metrics: PerformanceMetric[]): OperationAnalysis[] {
|
||||||
|
const operationGroups = this.groupBy(metrics, 'operation');
|
||||||
|
const analyses: OperationAnalysis[] = [];
|
||||||
|
|
||||||
|
for (const [operation, operationMetrics] of Object.entries(operationGroups)) {
|
||||||
|
const durations = operationMetrics.map(m => m.duration);
|
||||||
|
const successRate = operationMetrics.filter(m => m.success).length / operationMetrics.length;
|
||||||
|
|
||||||
|
analyses.push({
|
||||||
|
operation,
|
||||||
|
totalCalls: operationMetrics.length,
|
||||||
|
successRate,
|
||||||
|
avgDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
|
||||||
|
minDuration: Math.min(...durations),
|
||||||
|
maxDuration: Math.max(...durations),
|
||||||
|
p95: this.percentile(durations, 0.95),
|
||||||
|
p99: this.percentile(durations, 0.99),
|
||||||
|
errorRate: 1 - successRate,
|
||||||
|
slowQueries: operationMetrics.filter(m => m.duration > this.getSlowThreshold(operation)).length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return analyses.sort((a, b) => b.avgDuration - a.avgDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private identifyBottlenecks(metrics: PerformanceMetric[]): PerformanceBottleneck[] {
|
||||||
|
const bottlenecks: PerformanceBottleneck[] = [];
|
||||||
|
|
||||||
|
// 识别慢操作
|
||||||
|
const slowOperations = this.analyzeOperations(metrics)
|
||||||
|
.filter(op => op.avgDuration > this.getSlowThreshold(op.operation));
|
||||||
|
|
||||||
|
for (const operation of slowOperations) {
|
||||||
|
bottlenecks.push({
|
||||||
|
type: 'SLOW_OPERATION',
|
||||||
|
operation: operation.operation,
|
||||||
|
severity: this.getBottleneckSeverity(operation.avgDuration, this.getSlowThreshold(operation.operation)),
|
||||||
|
impact: 'medium',
|
||||||
|
description: `\${operation.operation} 平均耗时 \${operation.avgDuration}ms`,
|
||||||
|
suggestion: this.getOptimizationSuggestion(operation.operation)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 识别错误率高的操作
|
||||||
|
const highErrorOps = this.analyzeOperations(metrics)
|
||||||
|
.filter(op => op.errorRate > 0.05); // 5% 错误率
|
||||||
|
|
||||||
|
for (const operation of highErrorOps) {
|
||||||
|
bottlenecks.push({
|
||||||
|
type: 'HIGH_ERROR_RATE',
|
||||||
|
operation: operation.operation,
|
||||||
|
severity: 'high',
|
||||||
|
impact: 'high',
|
||||||
|
description: `\${operation.operation} 错误率 \${(operation.errorRate * 100).toFixed(1)}%`,
|
||||||
|
suggestion: this.getErrorOptimizationSuggestion(operation.operation)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return bottlenecks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPerformanceRecommendations(metrics: PerformanceMetric[]): PerformanceRecommendation[] {
|
||||||
|
const recommendations: PerformanceRecommendation[] = [];
|
||||||
|
|
||||||
|
// 分析上传性能
|
||||||
|
const uploadMetrics = metrics.filter(m => m.operation.startsWith('upload'));
|
||||||
|
if (uploadMetrics.length > 0) {
|
||||||
|
const avgUploadSize = this.getAverageUploadSize(uploadMetrics);
|
||||||
|
const avgUploadTime = uploadMetrics.reduce((sum, m) => sum + m.duration, 0) / uploadMetrics.length;
|
||||||
|
|
||||||
|
if (avgUploadSize > 10 * 1024 * 1024 && avgUploadTime > 10000) { // 10MB, 10s
|
||||||
|
recommendations.push({
|
||||||
|
type: 'UPLOAD_OPTIMIZATION',
|
||||||
|
priority: 'high',
|
||||||
|
title: '大文件上传优化',
|
||||||
|
description: '检测到大文件上传性能问题',
|
||||||
|
implementation: `
|
||||||
|
// 实施分块上传和断点续传
|
||||||
|
class ChunkedUploader {
|
||||||
|
async uploadLargeFile(file: File, chunkSize = 5 * 1024 * 1024): Promise<string> {
|
||||||
|
const chunks = Math.ceil(file.size / chunkSize);
|
||||||
|
const chunkIds: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks; i++) {
|
||||||
|
const start = i * chunkSize;
|
||||||
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
|
||||||
|
const result = await this.uploadChunk(chunk, i, chunks);
|
||||||
|
chunkIds.push(result.blobId);
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
this.updateProgress((i + 1) / chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文件清单
|
||||||
|
return await this.createManifest(chunkIds, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用压缩减少传输量
|
||||||
|
const compressedFile = await this.compressFile(file);
|
||||||
|
const uploadResult = await client.walrus.writeFiles({
|
||||||
|
files: [compressedFile],
|
||||||
|
epochs: 10,
|
||||||
|
signer: keypair
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
expectedImprovement: '50-70%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析批量操作性能
|
||||||
|
const batchMetrics = metrics.filter(m => m.operation.includes('batch'));
|
||||||
|
if (batchMetrics.length > 0) {
|
||||||
|
const concurrencyIssues = batchMetrics.filter(m => m.duration > 60000).length; // 超过1分钟
|
||||||
|
|
||||||
|
if (concurrencyIssues > batchMetrics.length * 0.3) { // 30%超时
|
||||||
|
recommendations.push({
|
||||||
|
type: 'BATCH_OPTIMIZATION',
|
||||||
|
priority: 'medium',
|
||||||
|
title: '批量操作并发优化',
|
||||||
|
description: '批量操作存在并发瓶颈',
|
||||||
|
implementation: `
|
||||||
|
// 优化并发控制
|
||||||
|
class OptimizedBatchProcessor {
|
||||||
|
private readonly MAX_CONCURRENT = 3;
|
||||||
|
private readonly RETRY_DELAY = 1000;
|
||||||
|
|
||||||
|
async processBatch<T>(items: T[], processor: (item: T) => Promise<any>): Promise<any[]> {
|
||||||
|
const results: any[] = [];
|
||||||
|
const chunks = this.chunkArray(items, this.MAX_CONCURRENT);
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const chunkPromises = chunk.map(async (item, index) => {
|
||||||
|
try {
|
||||||
|
return await this.withRetry(() => processor(item), 3);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(\`Batch item failed: \${error.message}\`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunkResults = await Promise.all(chunkPromises);
|
||||||
|
results.push(...chunkResults);
|
||||||
|
|
||||||
|
// 批次间延迟
|
||||||
|
if (chunks.indexOf(chunk) < chunks.length - 1) {
|
||||||
|
await this.delay(this.RETRY_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedImprovement: '30-50%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 错误监控
|
||||||
|
|
||||||
|
#### 错误统计和分析
|
||||||
|
```typescript
|
||||||
|
class ErrorMonitor {
|
||||||
|
private errorLog: ErrorRecord[] = [];
|
||||||
|
private errorPatterns: ErrorPattern[] = [];
|
||||||
|
|
||||||
|
recordError(error: Error, context: ErrorContext): void {
|
||||||
|
const errorRecord: ErrorRecord = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
type: this.classifyError(error),
|
||||||
|
severity: this.determineSeverity(error, context),
|
||||||
|
context,
|
||||||
|
resolved: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.errorLog.push(errorRecord);
|
||||||
|
|
||||||
|
// 分析错误模式
|
||||||
|
this.analyzeErrorPattern(errorRecord);
|
||||||
|
|
||||||
|
// 错误告警
|
||||||
|
this.checkErrorAlerts(errorRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorAnalysis(timeRange: number = 24 * 60 * 60 * 1000): ErrorAnalysis {
|
||||||
|
const recentErrors = this.getRecentErrors(timeRange);
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: this.getErrorSummary(recentErrors),
|
||||||
|
trends: this.getErrorTrends(recentErrors),
|
||||||
|
patterns: this.getErrorPatterns(recentErrors),
|
||||||
|
recommendations: this.getErrorRecommendations(recentErrors),
|
||||||
|
resolutionRate: this.calculateResolutionRate(recentErrors)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private analyzeErrorPattern(errorRecord: ErrorRecord): void {
|
||||||
|
const patternKey = this.getPatternKey(errorRecord);
|
||||||
|
let pattern = this.errorPatterns.find(p => p.key === patternKey);
|
||||||
|
|
||||||
|
if (!pattern) {
|
||||||
|
pattern = {
|
||||||
|
key: patternKey,
|
||||||
|
type: errorRecord.type,
|
||||||
|
count: 0,
|
||||||
|
firstOccurrence: errorRecord.timestamp,
|
||||||
|
lastOccurrence: errorRecord.timestamp,
|
||||||
|
contexts: [],
|
||||||
|
resolutions: []
|
||||||
|
};
|
||||||
|
this.errorPatterns.push(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern.count++;
|
||||||
|
pattern.lastOccurrence = errorRecord.timestamp;
|
||||||
|
|
||||||
|
// 记录上下文
|
||||||
|
const contextHash = this.hashContext(errorRecord.context);
|
||||||
|
if (!pattern.contexts.some(c => c.hash === contextHash)) {
|
||||||
|
pattern.contexts.push({
|
||||||
|
hash: contextHash,
|
||||||
|
context: errorRecord.context,
|
||||||
|
occurrences: 1
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const existingContext = pattern.contexts.find(c => c.hash === contextHash);
|
||||||
|
if (existingContext) {
|
||||||
|
existingContext.occurrences++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getErrorRecommendations(errors: ErrorRecord[]): ErrorRecommendation[] {
|
||||||
|
const recommendations: ErrorRecommendation[] = [];
|
||||||
|
|
||||||
|
// 分析最常见的错误类型
|
||||||
|
const errorTypes = this.groupBy(errors, 'type');
|
||||||
|
const mostCommonType = Object.entries(errorTypes)
|
||||||
|
.sort(([, a], [, b]) => b.length - a.length)[0];
|
||||||
|
|
||||||
|
if (mostCommonType && mostCommonType[1].length > errors.length * 0.3) { // 30%以上
|
||||||
|
recommendations.push({
|
||||||
|
type: 'PREVENTION',
|
||||||
|
priority: 'high',
|
||||||
|
title: `高频率 \${mostCommonType[0]} 错误`,
|
||||||
|
description: `\${mostCommonType[1].length} 次错误中 \${mostCommonType[1].length} 次是 \${mostCommonType[0]} 错误`,
|
||||||
|
implementation: this.getPreventionImplementation(mostCommonType[0]),
|
||||||
|
expectedReduction: '60-80%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析网络相关错误
|
||||||
|
const networkErrors = errors.filter(e => e.type === 'network' || e.type === 'timeout');
|
||||||
|
if (networkErrors.length > errors.length * 0.2) { // 20%以上
|
||||||
|
recommendations.push({
|
||||||
|
type: 'NETWORK_OPTIMIZATION',
|
||||||
|
priority: 'medium',
|
||||||
|
title: '网络连接优化',
|
||||||
|
description: '检测到大量网络相关错误',
|
||||||
|
implementation: `
|
||||||
|
// 实施网络优化策略
|
||||||
|
class NetworkOptimizer {
|
||||||
|
private readonly circuitBreaker = new CircuitBreaker({
|
||||||
|
failureThreshold: 5,
|
||||||
|
recoveryTimeout: 60000,
|
||||||
|
monitoringPeriod: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
async robustRequest(url: string, options: RequestInit = {}): Promise<Response> {
|
||||||
|
return this.circuitBreaker.execute(async () => {
|
||||||
|
// 使用指数退避重试
|
||||||
|
return this.withRetry(() => fetch(url, {
|
||||||
|
...options,
|
||||||
|
timeout: 30000
|
||||||
|
}), 3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async withRetry<T>(
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
maxRetries: number = 3
|
||||||
|
): Promise<T> {
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === maxRetries || !this.isRetryableError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
|
||||||
|
await this.delay(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Max retries exceeded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedReduction: '70-90%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 存储使用监控
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-monitor"
|
||||||
|
// monitor-type: usage
|
||||||
|
// time-range: 7d
|
||||||
|
// metrics: ["total-size", "file-count", "cost", "aging"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 成本分析
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-monitor"
|
||||||
|
// monitor-type: cost
|
||||||
|
// time-range: 30d
|
||||||
|
// metrics: ["storage-cost", "transaction-cost", "optimization-suggestions"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能监控
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-monitor"
|
||||||
|
// monitor-type: performance
|
||||||
|
// time-range: 24h
|
||||||
|
// metrics: ["upload-speed", "download-speed", "error-rate", "bottlenecks"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误分析
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-monitor"
|
||||||
|
// monitor-type: errors
|
||||||
|
// time-range: 7d
|
||||||
|
// metrics: ["error-patterns", "resolution-rate", "prevention-suggestions"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*更新时间:2025-11-11*
|
||||||
761
skills/walrus-performance-optimizer.md
Normal file
761
skills/walrus-performance-optimizer.md
Normal file
@@ -0,0 +1,761 @@
|
|||||||
|
---
|
||||||
|
name: walrus-performance-optimizer
|
||||||
|
description: Walrus 性能优化专家 - 分析和优化 Walrus 应用的性能瓶颈
|
||||||
|
parameters:
|
||||||
|
- name: analysis-type
|
||||||
|
type: string
|
||||||
|
description: 分析类型 (upload/download/batch/memory/network)
|
||||||
|
required: true
|
||||||
|
- name: target-metric
|
||||||
|
type: string
|
||||||
|
description: 目标指标 (speed/throughput/cost/latency)
|
||||||
|
default: speed
|
||||||
|
- name: code-context
|
||||||
|
type: string
|
||||||
|
description: 代码上下文或问题描述
|
||||||
|
required: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Walrus 性能优化技能
|
||||||
|
|
||||||
|
## 技能概述
|
||||||
|
|
||||||
|
`walrus-performance-optimizer` 是专业的性能优化助手,专门分析 Walrus 应用的性能瓶颈,提供数据驱动的优化建议和具体的实施方案。
|
||||||
|
|
||||||
|
## 性能分析维度
|
||||||
|
|
||||||
|
### 1. 上传性能优化
|
||||||
|
|
||||||
|
#### 批量操作优化
|
||||||
|
```typescript
|
||||||
|
// 优化前:逐个上传文件
|
||||||
|
async function uploadFilesSlow(files: File[]) {
|
||||||
|
const results = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const result = await uploadSingleFile(file);
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化后:批量并发上传
|
||||||
|
class OptimizedUploader {
|
||||||
|
private readonly MAX_CONCURRENT = 5;
|
||||||
|
private readonly BATCH_DELAY = 100;
|
||||||
|
|
||||||
|
async uploadFilesOptimized(files: File[]): Promise<UploadResult[]> {
|
||||||
|
const results: UploadResult[] = [];
|
||||||
|
const chunks = this.chunkArray(files, this.MAX_CONCURRENT);
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const chunkPromises = chunk.map(file => this.uploadWithRetry(file));
|
||||||
|
const chunkResults = await Promise.allSettled(chunkPromises);
|
||||||
|
|
||||||
|
results.push(...this.processChunkResults(chunkResults));
|
||||||
|
|
||||||
|
// 批次间延迟,避免过载
|
||||||
|
if (chunks.indexOf(chunk) < chunks.length - 1) {
|
||||||
|
await this.delay(this.BATCH_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadWithRetry(file: File, maxRetries = 3): Promise<UploadResult> {
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await this.uploadSingleFile(file);
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === maxRetries || !this.isRetryableError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指数退避
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
|
||||||
|
await this.delay(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private chunkArray<T>(array: T[], size: number): T[][] {
|
||||||
|
const chunks: T[][] = [];
|
||||||
|
for (let i = 0; i < array.length; i += size) {
|
||||||
|
chunks.push(array.slice(i, i + size));
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isRetryableError(error: any): boolean {
|
||||||
|
return error instanceof RetryableWalrusClientError ||
|
||||||
|
error.message.includes('timeout') ||
|
||||||
|
error.message.includes('network');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 文件压缩优化
|
||||||
|
```typescript
|
||||||
|
class CompressionOptimizer {
|
||||||
|
async optimizeUpload(file: File): Promise<OptimizedFile> {
|
||||||
|
const fileBuffer = await file.arrayBuffer();
|
||||||
|
const originalSize = fileBuffer.byteLength;
|
||||||
|
|
||||||
|
// 文本文件压缩
|
||||||
|
if (this.isTextFile(file)) {
|
||||||
|
const compressed = await this.compressText(fileBuffer);
|
||||||
|
if (compressed.size < originalSize * 0.8) {
|
||||||
|
return {
|
||||||
|
data: compressed.data,
|
||||||
|
originalSize,
|
||||||
|
compressedSize: compressed.size,
|
||||||
|
compressionRatio: compressed.size / originalSize,
|
||||||
|
method: 'gzip'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片文件优化
|
||||||
|
if (this.isImageFile(file)) {
|
||||||
|
const optimized = await this.optimizeImage(fileBuffer);
|
||||||
|
if (optimized.size < originalSize * 0.9) {
|
||||||
|
return {
|
||||||
|
data: optimized.data,
|
||||||
|
originalSize,
|
||||||
|
compressedSize: optimized.size,
|
||||||
|
compressionRatio: optimized.size / originalSize,
|
||||||
|
method: 'image-optimization'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: new Uint8Array(fileBuffer),
|
||||||
|
originalSize,
|
||||||
|
compressedSize: originalSize,
|
||||||
|
compressionRatio: 1,
|
||||||
|
method: 'none'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async compressText(data: ArrayBuffer): Promise<CompressedData> {
|
||||||
|
const compressionStream = new CompressionStream('gzip');
|
||||||
|
const writer = compressionStream.writable.getWriter();
|
||||||
|
const reader = compressionStream.readable.getReader();
|
||||||
|
|
||||||
|
writer.write(new Uint8Array(data));
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
let totalSize = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
chunks.push(value);
|
||||||
|
totalSize += value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined = new Uint8Array(totalSize);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
combined.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: combined, size: totalSize };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async optimizeImage(data: ArrayBuffer): Promise<CompressedData> {
|
||||||
|
// 使用 WebAssembly 进行图片优化
|
||||||
|
// 这里可以集成像 sharp.js 或其他图片处理库
|
||||||
|
const blob = new Blob([data], { type: 'image/jpeg' });
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
// 降低质量但保持合理尺寸
|
||||||
|
const scale = Math.min(1, 1920 / Math.max(img.width, img.height));
|
||||||
|
canvas.width = img.width * scale;
|
||||||
|
canvas.height = img.height * scale;
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
blob.arrayBuffer().then(buffer => {
|
||||||
|
const data = new Uint8Array(buffer);
|
||||||
|
resolve({ data, size: data.length });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 'image/jpeg', 0.85);
|
||||||
|
};
|
||||||
|
img.src = URL.createObjectURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 下载性能优化
|
||||||
|
|
||||||
|
#### 智能缓存系统
|
||||||
|
```typescript
|
||||||
|
class IntelligentCache {
|
||||||
|
private cache = new Map<string, CacheEntry>();
|
||||||
|
private accessOrder = new Map<string, number>();
|
||||||
|
private accessCounter = 0;
|
||||||
|
private readonly MAX_SIZE = 100; // 最大缓存条目
|
||||||
|
private readonly TTL = 300000; // 5分钟过期
|
||||||
|
|
||||||
|
async get(blobId: string): Promise<Uint8Array | null> {
|
||||||
|
const entry = this.cache.get(blobId);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (Date.now() - entry.timestamp > this.TTL) {
|
||||||
|
this.delete(blobId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新访问顺序 (LRU)
|
||||||
|
this.accessOrder.set(blobId, ++this.accessCounter);
|
||||||
|
|
||||||
|
return entry.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(blobId: string, data: Uint8Array): Promise<void> {
|
||||||
|
// 检查缓存大小,必要时清理
|
||||||
|
if (this.cache.size >= this.MAX_SIZE) {
|
||||||
|
this.evictLRU();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cache.set(blobId, {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
size: data.length
|
||||||
|
});
|
||||||
|
|
||||||
|
this.accessOrder.set(blobId, ++this.accessCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private evictLRU(): void {
|
||||||
|
let oldestKey = '';
|
||||||
|
let oldestAccess = Infinity;
|
||||||
|
|
||||||
|
for (const [key, accessTime] of this.accessOrder) {
|
||||||
|
if (accessTime < oldestAccess) {
|
||||||
|
oldestAccess = accessTime;
|
||||||
|
oldestKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldestKey) {
|
||||||
|
this.delete(oldestKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private delete(blobId: string): void {
|
||||||
|
this.cache.delete(blobId);
|
||||||
|
this.accessOrder.delete(blobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats(): CacheStats {
|
||||||
|
return {
|
||||||
|
size: this.cache.size,
|
||||||
|
totalMemoryUsage: Array.from(this.cache.values())
|
||||||
|
.reduce((sum, entry) => sum + entry.size, 0),
|
||||||
|
hitRate: this.hitRate,
|
||||||
|
evictions: this.evictions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 预加载策略
|
||||||
|
```typescript
|
||||||
|
class PreloadingStrategy {
|
||||||
|
private preloadQueue = new Set<string>();
|
||||||
|
private preloadCache = new Map<string, PreloadEntry>();
|
||||||
|
|
||||||
|
async preloadFiles(blobIds: string[], priority: 'high' | 'medium' | 'low' = 'medium') {
|
||||||
|
const delay = priority === 'high' ? 0 : priority === 'medium' ? 100 : 500;
|
||||||
|
|
||||||
|
for (const blobId of blobIds) {
|
||||||
|
if (!this.preloadQueue.has(blobId)) {
|
||||||
|
this.preloadQueue.add(blobId);
|
||||||
|
|
||||||
|
// 延迟执行,避免阻塞主要操作
|
||||||
|
setTimeout(() => this.preloadFile(blobId), delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async preloadFile(blobId: string) {
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 使用低优先级下载
|
||||||
|
const [file] = await client.walrus.getFiles({
|
||||||
|
ids: [blobId],
|
||||||
|
options: { priority: 'low' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = await file.bytes();
|
||||||
|
const loadTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.preloadCache.set(blobId, {
|
||||||
|
data: content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
loadTime
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`预加载完成: ${blobId} (${loadTime}ms)`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`预加载失败: ${blobId}`, error.message);
|
||||||
|
} finally {
|
||||||
|
this.preloadQueue.delete(blobId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPreloaded(blobId: string): Promise<Uint8Array | null> {
|
||||||
|
const entry = this.preloadCache.get(blobId);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (Date.now() - entry.timestamp > 600000) { // 10分钟
|
||||||
|
this.preloadCache.delete(blobId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 网络性能优化
|
||||||
|
|
||||||
|
#### 连接池管理
|
||||||
|
```typescript
|
||||||
|
class ConnectionPool {
|
||||||
|
private connections: Map<string, Connection[]> = new Map();
|
||||||
|
private readonly MAX_CONNECTIONS_PER_HOST = 5;
|
||||||
|
private readonly CONNECTION_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
async getConnection(host: string): Promise<Connection> {
|
||||||
|
const pool = this.connections.get(host) || [];
|
||||||
|
|
||||||
|
// 尝试复用现有连接
|
||||||
|
for (const conn of pool) {
|
||||||
|
if (!conn.inUse && this.isConnectionHealthy(conn)) {
|
||||||
|
conn.inUse = true;
|
||||||
|
conn.lastUsed = Date.now();
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新连接
|
||||||
|
if (pool.length < this.MAX_CONNECTIONS_PER_HOST) {
|
||||||
|
const newConn = await this.createConnection(host);
|
||||||
|
newConn.inUse = true;
|
||||||
|
pool.push(newConn);
|
||||||
|
this.connections.set(host, pool);
|
||||||
|
return newConn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待连接可用
|
||||||
|
return this.waitForAvailableConnection(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseConnection(connection: Connection) {
|
||||||
|
connection.inUse = false;
|
||||||
|
connection.lastUsed = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createConnection(host: string): Promise<Connection> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(host);
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error('Connection timeout'));
|
||||||
|
}, this.CONNECTION_TIMEOUT);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve({
|
||||||
|
socket: ws,
|
||||||
|
created: Date.now(),
|
||||||
|
lastUsed: Date.now(),
|
||||||
|
inUse: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(new Error('Connection failed'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private isConnectionHealthy(conn: Connection): boolean {
|
||||||
|
return conn.socket.readyState === WebSocket.OPEN &&
|
||||||
|
Date.now() - conn.lastUsed < 300000; // 5分钟未使用则关闭
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForAvailableConnection(host: string): Promise<Connection> {
|
||||||
|
const pool = this.connections.get(host) || [];
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
const available = pool.find(conn => !conn.inUse && this.isConnectionHealthy(conn));
|
||||||
|
if (available) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
available.inUse = true;
|
||||||
|
available.lastUsed = Date.now();
|
||||||
|
resolve(available);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 请求优化
|
||||||
|
```typescript
|
||||||
|
class RequestOptimizer {
|
||||||
|
private pendingRequests = new Map<string, Promise<any>>();
|
||||||
|
private requestQueue: QueuedRequest[] = [];
|
||||||
|
private processing = false;
|
||||||
|
|
||||||
|
async batchRequest<T>(requests: BatchRequest[]): Promise<T[]> {
|
||||||
|
// 合并相同的请求
|
||||||
|
const uniqueRequests = this.deduplicateRequests(requests);
|
||||||
|
|
||||||
|
// 按优先级排序
|
||||||
|
uniqueRequests.sort((a, b) => b.priority - a.priority);
|
||||||
|
|
||||||
|
const results: T[] = [];
|
||||||
|
|
||||||
|
for (const request of uniqueRequests) {
|
||||||
|
try {
|
||||||
|
const result = await this.executeRequest(request);
|
||||||
|
results.push(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`批量请求失败: ${request.id}`, error);
|
||||||
|
results.push(null as T);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private deduplicateRequests(requests: BatchRequest[]): BatchRequest[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return requests.filter(req => {
|
||||||
|
const key = this.getRequestKey(req);
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeRequest(request: BatchRequest): Promise<any> {
|
||||||
|
const key = this.getRequestKey(request);
|
||||||
|
|
||||||
|
// 检查是否有相同请求正在进行
|
||||||
|
if (this.pendingRequests.has(key)) {
|
||||||
|
return await this.pendingRequests.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestPromise = this.performRequest(request);
|
||||||
|
this.pendingRequests.set(key, requestPromise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestPromise;
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
this.pendingRequests.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performRequest(request: BatchRequest): Promise<any> {
|
||||||
|
// 实现实际的请求逻辑
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), request.timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(request.url, {
|
||||||
|
...request.options,
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 内存优化
|
||||||
|
|
||||||
|
#### 内存池管理
|
||||||
|
```typescript
|
||||||
|
class MemoryPool {
|
||||||
|
private pools = new Map<number, ArrayBuffer[]>();
|
||||||
|
private readonly MAX_POOL_SIZE = 50;
|
||||||
|
private readonly ALLOC_INCREMENT = 1024; // 1KB increments
|
||||||
|
|
||||||
|
allocate(size: number): ArrayBuffer {
|
||||||
|
const actualSize = Math.ceil(size / this.ALLOC_INCREMENT) * this.ALLOC_INCREMENT;
|
||||||
|
const pool = this.pools.get(actualSize) || [];
|
||||||
|
|
||||||
|
if (pool.length > 0) {
|
||||||
|
return pool.pop()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayBuffer(actualSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
release(buffer: ArrayBuffer): void {
|
||||||
|
const size = buffer.byteLength;
|
||||||
|
const pool = this.pools.get(size) || [];
|
||||||
|
|
||||||
|
if (pool.length < this.MAX_POOL_SIZE) {
|
||||||
|
// 清零缓冲区(安全考虑)
|
||||||
|
new Uint8Array(buffer).fill(0);
|
||||||
|
pool.push(buffer);
|
||||||
|
this.pools.set(size, pool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats(): MemoryPoolStats {
|
||||||
|
const stats: MemoryPoolStats = {
|
||||||
|
totalBuffers: 0,
|
||||||
|
totalMemory: 0,
|
||||||
|
poolSizes: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [size, pool] of this.pools) {
|
||||||
|
stats.totalBuffers += pool.length;
|
||||||
|
stats.totalMemory += size * pool.length;
|
||||||
|
stats.poolSizes[size] = pool.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 流式处理
|
||||||
|
```typescript
|
||||||
|
class StreamProcessor {
|
||||||
|
async processLargeFile(
|
||||||
|
file: File,
|
||||||
|
processor: (chunk: Uint8Array, index: number) => Promise<void>,
|
||||||
|
chunkSize = 1024 * 1024 // 1MB chunks
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
let offset = 0;
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
const processChunk = () => {
|
||||||
|
if (offset >= file.size) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunk = file.slice(offset, offset + chunkSize);
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
if (event.target?.result) {
|
||||||
|
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await processor(uint8Array, index);
|
||||||
|
offset += chunkSize;
|
||||||
|
index++;
|
||||||
|
processChunk();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(chunk);
|
||||||
|
};
|
||||||
|
|
||||||
|
processChunk();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadStream(
|
||||||
|
file: File,
|
||||||
|
onProgress?: (progress: number) => void
|
||||||
|
): Promise<string> {
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
const totalSize = file.size;
|
||||||
|
|
||||||
|
await this.processLargeFile(file, async (chunk, index) => {
|
||||||
|
chunks.push(chunk);
|
||||||
|
|
||||||
|
const progress = ((index + 1) * chunk.length) / totalSize;
|
||||||
|
onProgress?.(Math.min(progress, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 合并所有块
|
||||||
|
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||||
|
const combined = new Uint8Array(totalLength);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
combined.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传合并后的数据
|
||||||
|
const walrusFile = WalrusFile.from({
|
||||||
|
contents: combined,
|
||||||
|
identifier: `stream/${Date.now()}_${file.name}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await client.walrus.writeFiles({
|
||||||
|
files: [walrusFile],
|
||||||
|
epochs: 10,
|
||||||
|
deletable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return result[0].blobId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能监控
|
||||||
|
|
||||||
|
### 实时性能指标
|
||||||
|
```typescript
|
||||||
|
class PerformanceMonitor {
|
||||||
|
private metrics = new Map<string, MetricEntry[]>();
|
||||||
|
private readonly MAX_METRICS = 1000;
|
||||||
|
|
||||||
|
recordMetric(name: string, value: number, tags: Record<string, string> = {}) {
|
||||||
|
const entry: MetricEntry = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
value,
|
||||||
|
tags
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = this.metrics.get(name) || [];
|
||||||
|
existing.push(entry);
|
||||||
|
|
||||||
|
// 限制历史数据数量
|
||||||
|
if (existing.length > this.MAX_METRICS) {
|
||||||
|
existing.splice(0, existing.length - this.MAX_METRICS);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.metrics.set(name, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetrics(name: string, timeRange?: number): MetricEntry[] {
|
||||||
|
const allMetrics = this.metrics.get(name) || [];
|
||||||
|
|
||||||
|
if (!timeRange) {
|
||||||
|
return allMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutoff = Date.now() - timeRange;
|
||||||
|
return allMetrics.filter(m => m.timestamp >= cutoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAggregatedMetrics(name: string, timeRange = 300000): AggregatedMetrics {
|
||||||
|
const metrics = this.getMetrics(name, timeRange);
|
||||||
|
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
return {
|
||||||
|
count: 0,
|
||||||
|
sum: 0,
|
||||||
|
avg: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 0,
|
||||||
|
p50: 0,
|
||||||
|
p95: 0,
|
||||||
|
p99: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = metrics.map(m => m.value).sort((a, b) => a - b);
|
||||||
|
const sum = values.reduce((s, v) => s + v, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: values.length,
|
||||||
|
sum,
|
||||||
|
avg: sum / values.length,
|
||||||
|
min: values[0],
|
||||||
|
max: values[values.length - 1],
|
||||||
|
p50: this.percentile(values, 0.5),
|
||||||
|
p95: this.percentile(values, 0.95),
|
||||||
|
p99: this.percentile(values, 0.99)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private percentile(sorted: number[], p: number): number {
|
||||||
|
const index = Math.ceil(sorted.length * p) - 1;
|
||||||
|
return sorted[Math.max(0, index)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基础性能分析
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-performance-optimizer"
|
||||||
|
// analysis-type: upload
|
||||||
|
// target-metric: speed
|
||||||
|
// code-context: 我的文件上传速度很慢,单个10MB文件需要2分钟,如何优化?
|
||||||
|
```
|
||||||
|
|
||||||
|
### 批量操作优化
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-performance-optimizer"
|
||||||
|
// analysis-type: batch
|
||||||
|
// target-metric: throughput
|
||||||
|
// code-context: 批量上传100个文件时经常超时,需要优化并发处理
|
||||||
|
```
|
||||||
|
|
||||||
|
### 内存使用优化
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-performance-optimizer"
|
||||||
|
// analysis-type: memory
|
||||||
|
// target-metric: cost
|
||||||
|
// code-context: 大文件处理时内存占用过高,浏览器经常崩溃
|
||||||
|
```
|
||||||
|
|
||||||
|
### 网络优化
|
||||||
|
```typescript
|
||||||
|
skill: "walrus-performance-optimizer"
|
||||||
|
// analysis-type: network
|
||||||
|
// target-metric: latency
|
||||||
|
// code-context: 网络不稳定环境下频繁上传失败,需要优化网络策略
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*更新时间:2025-11-11*
|
||||||
Reference in New Issue
Block a user