chore: 同步本地更改
- 新增 aio-mcp 项目框架 - 新增 .trae/skills/ OCR/SKU 设计工具 - 更新 auto-check 配置和 stress 包 - 删除 fnhelp 文档目录 - 更新 Docker 压力测试脚本
This commit is contained in:
366
.trae/skills/ocr-extract/parse_sku_prices.py
Normal file
366
.trae/skills/ocr-extract/parse_sku_prices.py
Normal file
@@ -0,0 +1,366 @@
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
|
||||
# Load OCR results
|
||||
with open(r"D:\workspace\membank\售价\ocr_results.json", "r", encoding="utf-8") as f:
|
||||
all_results = json.load(f)
|
||||
|
||||
# Extract structured SKU prices from each image
|
||||
# Pattern: SKU name patterns like "蜗牛Nas-xxx-4g+64g", "J1900-4g+32g", etc.
|
||||
# Prices appear as decimal numbers typically followed by a discount of 5.00
|
||||
|
||||
def extract_sku_prices(text_lines):
|
||||
"""Extract SKU name and price pairs from OCR text lines"""
|
||||
entries = []
|
||||
i = 0
|
||||
while i < len(text_lines):
|
||||
text, conf = text_lines[i]
|
||||
text = text.strip()
|
||||
|
||||
# Look for SKU names containing known patterns
|
||||
sku_patterns = [
|
||||
r'(?:蜗牛Nas|蜗牛|Nas|cNas)[\s\w\-\.\+\(\)一-鿿]+?\d+g[+一-鿿]*\d*g',
|
||||
r'(?:J1900|j1900|J3160|j3160|I3|i3|I5|i5|1037U|1037u|N5105|N5100|N5095|J3355|J3060)[\-\.\+\(\)\s\w一-鿿]*?\d+g[\+一-鿿]*\d*g',
|
||||
r'(?:J1900|j1900|J3160|j3160|I3|i3|I5|i5|1037U|1037u|N5105|N5100|N5095|J3355|J3060)[\-\.\+\(\)\s\w一-鿿]*?\d+g',
|
||||
r'(?:16G[BW])[\-\s]*(?:windows|蜗牛|群晖)',
|
||||
r'(?:DDR|D3|D4)[\-\.\s]*\d+\s*[Gg]',
|
||||
]
|
||||
|
||||
is_sku = False
|
||||
for pat in sku_patterns:
|
||||
if re.search(pat, text, re.IGNORECASE):
|
||||
is_sku = True
|
||||
break
|
||||
|
||||
if is_sku:
|
||||
sku_name = text
|
||||
# Look for price pattern nearby: two numbers where one is ~5.00 off from the other
|
||||
# Pattern: price1 (like 247.50) ... 5.00 ... price2 (like 242.50)
|
||||
prices_found = []
|
||||
for j in range(i, min(i + 10, len(text_lines))):
|
||||
t2 = text_lines[j][0].strip()
|
||||
# Match decimal prices
|
||||
price_match = re.findall(r'\d{2,4}\.\d{2}', t2)
|
||||
for p in price_match:
|
||||
prices_found.append(float(p))
|
||||
# Also match integer prices
|
||||
price_match2 = re.findall(r'(?<!\d\.)(?:\d{2,4})(?:\.\d{2})?(?!\d|\.\d{3,})', t2)
|
||||
for p in price_match2:
|
||||
try:
|
||||
prices_found.append(float(p))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Deduplicate and find likely actual price (the larger number, minus 5 = smaller number)
|
||||
prices_found = sorted(set(prices_found))
|
||||
actual_price = None
|
||||
for p in prices_found:
|
||||
if p > 10 and (p - 5) in prices_found:
|
||||
actual_price = p # The larger one is the original price
|
||||
break
|
||||
|
||||
if actual_price:
|
||||
discounted = actual_price - 5
|
||||
entries.append({
|
||||
'sku': sku_name,
|
||||
'price': discounted,
|
||||
'original_price': actual_price,
|
||||
'confidence': conf
|
||||
})
|
||||
|
||||
i += 1
|
||||
|
||||
return entries
|
||||
|
||||
def extract_prices_v2(text_lines):
|
||||
"""
|
||||
Better approach: parse each image by looking for patterns:
|
||||
SKU name -> (price, discount 5.00, discounted_price)
|
||||
"""
|
||||
results = []
|
||||
|
||||
# Flatten texts
|
||||
all_text = [t[0].strip() for t in text_lines]
|
||||
all_with_conf = list(text_lines)
|
||||
|
||||
# Known CPU/memory patterns
|
||||
pattern = re.compile(
|
||||
r'.*?(?:蜗牛|Nas|NAS|nas|cNas|黑裙|黑群晖|客服|咨询).*?(?:'
|
||||
r'J1900|j1900|J3160|j3160|'
|
||||
r'i3|I3|i5|I5|'
|
||||
r'1037[Uu]|1037u|'
|
||||
r'N5105|N5100|N5095|'
|
||||
r'J3355|J3060|'
|
||||
r'16G[Bb]'
|
||||
r').*',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
# Scan through all texts looking for price patterns
|
||||
for i, (text, conf) in enumerate(all_with_conf):
|
||||
text = text.strip()
|
||||
|
||||
# Skip non-SKU lines
|
||||
if len(text) < 3:
|
||||
continue
|
||||
|
||||
# Check if this looks like a SKU description
|
||||
has_cpu = bool(re.search(r'(J1900|j1900|J3160|j3160|i3|I3|i5|I5|1037[Uu]|N5105|N5100|N5095|J3355|J3060|J4125)', text))
|
||||
has_nas = bool(re.search(r'(蜗牛|Nas|NAS|cNas|黑裙)', text))
|
||||
has_mem = bool(re.search(r'\d+g[\+一-鿿]*\d+g|\d+g\b|16G[Bb]', text))
|
||||
|
||||
if has_cpu or (has_nas and has_mem) or ('16G' in text.upper() and ('windows' in text.lower() or '蜗牛' in text or '群晖' in text)):
|
||||
# This is a SKU name - look for nearby prices
|
||||
sku_name = text
|
||||
|
||||
# Look at next 8 lines for prices
|
||||
prices = []
|
||||
for j in range(i+1, min(i+12, len(all_with_conf))):
|
||||
t = all_with_conf[j][0].strip()
|
||||
# Find decimal numbers
|
||||
nums = re.findall(r'(\d{2,4}\.\d{2})', t)
|
||||
prices.extend([float(n) for n in nums])
|
||||
|
||||
# Also look at previous 2 lines
|
||||
for j in range(max(0, i-2), i):
|
||||
t = all_with_conf[j][0].strip()
|
||||
nums = re.findall(r'(\d{2,4}\.\d{2})', t)
|
||||
prices.extend([float(n) for n in nums])
|
||||
|
||||
if prices:
|
||||
# Find pairs where diff = 5.00
|
||||
prices = sorted(set(prices))
|
||||
found_price = None
|
||||
for p in prices:
|
||||
if p > 20 and (p - 5.00) in [round(x, 2) for x in prices]:
|
||||
found_price = p
|
||||
break
|
||||
|
||||
# If no pair found, take the most common price range
|
||||
if not found_price:
|
||||
valid_prices = [p for p in prices if 100 < p < 500]
|
||||
if valid_prices:
|
||||
found_price = max(valid_prices)
|
||||
|
||||
if found_price and found_price > 20:
|
||||
discounted = round(found_price - 5.0, 2)
|
||||
results.append({
|
||||
'sku': sku_name,
|
||||
'price': discounted,
|
||||
'original_price': round(found_price, 2),
|
||||
'confidence': float(conf) if conf else 0
|
||||
})
|
||||
|
||||
# Deduplicate - keep highest confidence for each SKU
|
||||
seen = {}
|
||||
for r in results:
|
||||
key = r['sku']
|
||||
if key not in seen or r['confidence'] > seen[key]['confidence']:
|
||||
seen[key] = r
|
||||
|
||||
return sorted(seen.values(), key=lambda x: x['price'])
|
||||
|
||||
|
||||
def extract_prices_v3(text_lines):
|
||||
"""
|
||||
Most robust: look for every <SKU name> ... <price_a> ... 5.00 ... <price_b> pattern
|
||||
where price_a = price_b - 5 (or vice versa)
|
||||
"""
|
||||
results = []
|
||||
all_with_conf = list(text_lines)
|
||||
|
||||
for i, (text, conf) in enumerate(all_with_conf):
|
||||
text = text.strip()
|
||||
|
||||
# Must contain key SKU indicators
|
||||
has_indicator = (
|
||||
bool(re.search(r'(J1900|j1900|J3160|j3160|i3|I3|i5|I5|1037[Uu]|N5105|N5100|N5095|J3355|J3060|J4125|5005)', text)) or
|
||||
bool(re.search(r'(蜗牛|Nas|NAS|黑裙|黑群晖|客服|咨询)', text)) or
|
||||
bool(re.search(r'16G[Bb].*(?:windows|蜗牛|群晖|系统)', text)) or
|
||||
bool(re.search(r'(?:预装系统|系统).*(?:双盘位|单盘位)', text))
|
||||
)
|
||||
|
||||
if not has_indicator or len(text) < 4:
|
||||
continue
|
||||
|
||||
# But also filter out pure UI text
|
||||
skip_patterns = ['商品价格', '关注券', '折扣', '券后价', '商品ID', '活动', '修改', '查看', '营销', '官方', '拼多多', '商家']
|
||||
if any(s in text for s in skip_patterns):
|
||||
continue
|
||||
|
||||
sku_name = text
|
||||
|
||||
# Search window around this line for prices
|
||||
window_texts = []
|
||||
for j in range(max(0, i-3), min(i+15, len(all_with_conf))):
|
||||
window_texts.append(all_with_conf[j][0].strip())
|
||||
|
||||
# Extract all decimal prices from the window
|
||||
prices = []
|
||||
for wt in window_texts:
|
||||
nums = re.findall(r'(\d{2,4}\.\d{2})', wt)
|
||||
prices.extend([float(n) for n in nums])
|
||||
|
||||
if not prices:
|
||||
continue
|
||||
|
||||
prices = sorted(set(prices))
|
||||
|
||||
# Filter noise prices (too small)
|
||||
prices = [p for p in prices if p > 15]
|
||||
|
||||
# Find the discount pattern: original_price and (original_price - 5)
|
||||
found_price = None
|
||||
price_set = set(round(p, 2) for p in prices)
|
||||
|
||||
for p in prices:
|
||||
if round(p - 5.0, 2) in price_set:
|
||||
# p is the higher (original), p-5 is the discounted
|
||||
found_price = p
|
||||
break
|
||||
if round(p + 5.0, 2) in price_set:
|
||||
# p is the discounted, p+5 is the original
|
||||
found_price = p + 5.0
|
||||
break
|
||||
|
||||
if not found_price:
|
||||
# Fallback: take the highest price that looks like a product price
|
||||
valid = [p for p in prices if 100 < p < 600]
|
||||
if valid:
|
||||
found_price = max(valid)
|
||||
|
||||
if found_price and found_price > 20:
|
||||
results.append({
|
||||
'sku': sku_name,
|
||||
'price': round(found_price - 5.0, 2),
|
||||
'original_price': round(found_price, 2),
|
||||
'confidence': float(conf) if conf else 0
|
||||
})
|
||||
|
||||
# Deduplicate by SKU name similarity
|
||||
def sku_key(s):
|
||||
return re.sub(r'[\s\-\.\+]+', '', s.lower())
|
||||
|
||||
deduped = {}
|
||||
for r in results:
|
||||
key = sku_key(r['sku'])
|
||||
if key not in deduped or r['confidence'] > deduped[key]['confidence']:
|
||||
deduped[key] = r
|
||||
|
||||
return sorted(deduped.values(), key=lambda x: x['price'])
|
||||
|
||||
|
||||
print("=" * 80)
|
||||
print("Extracting SKU Price Summary")
|
||||
print("=" * 80)
|
||||
|
||||
all_entries = []
|
||||
for img_name, lines in all_results.items():
|
||||
print(f"\nProcessing: {img_name}")
|
||||
entries = extract_prices_v3(lines)
|
||||
print(f" Found {len(entries)} SKUs")
|
||||
all_entries.extend(entries)
|
||||
|
||||
# Global deduplication
|
||||
def normalize_sku(name):
|
||||
"""Normalize SKU name for deduplication"""
|
||||
name = name.strip()
|
||||
# Remove leading symbols
|
||||
name = re.sub(r'^[>\s\-\.]+', '', name)
|
||||
# Standardize separators
|
||||
name = name.replace('+', '+').replace('+', '+')
|
||||
# Remove trailing garbage
|
||||
name = re.sub(r'[>\s\.\-]+$', '', name)
|
||||
return name
|
||||
|
||||
# Categorize entries
|
||||
# Dedup keeping highest confidence
|
||||
global_dedup = {}
|
||||
for e in all_entries:
|
||||
norm = normalize_sku(e['sku'])
|
||||
# Further normalize for key
|
||||
key = re.sub(r'[\s\-\>\.\(\)]', '', norm.lower())
|
||||
# Extract CPU type for grouping
|
||||
if key not in global_dedup or e['confidence'] > global_dedup[key]['confidence']:
|
||||
global_dedup[key] = e
|
||||
|
||||
final_entries = sorted(global_dedup.values(), key=lambda x: x['price'])
|
||||
|
||||
# Group by CPU/platform
|
||||
groups = {}
|
||||
for e in final_entries:
|
||||
name = e['sku']
|
||||
# Extract group
|
||||
if '16G' in name.upper() or '16g' in name.lower():
|
||||
group = '系统U盘 (16GB)'
|
||||
elif re.search(r'(i3|I3|i5|I5)', name):
|
||||
if '双盘' in name or '雙盤' in name:
|
||||
group = 'NAS 双盘位 - i3/i5系列'
|
||||
else:
|
||||
group = 'NAS - i3/i5系列'
|
||||
elif re.search(r'1037[Uu]', name):
|
||||
if '单盘' in name or '單盤' in name or 'A4' in name:
|
||||
group = 'NAS 单盘位 - 1037U系列'
|
||||
elif '双盘' in name or '雙盤' in name:
|
||||
group = 'NAS 双盘位 - 1037U系列'
|
||||
else:
|
||||
group = 'NAS - 1037U系列'
|
||||
elif re.search(r'J1900|j1900', name):
|
||||
group = 'NAS 双盘位 - J1900系列'
|
||||
elif re.search(r'i3|I3', name):
|
||||
group = 'NAS - i3系列'
|
||||
elif re.search(r'i5|I5', name):
|
||||
group = 'NAS - i5系列'
|
||||
elif '黑裙' in name or '客服' in name or '咨询' in name:
|
||||
group = 'NAS - 黑裙晖系列'
|
||||
else:
|
||||
group = '其他'
|
||||
|
||||
if group not in groups:
|
||||
groups[group] = []
|
||||
groups[group].append(e)
|
||||
|
||||
# Generate markdown output
|
||||
md_lines = []
|
||||
md_lines.append("# SKU 售价汇总")
|
||||
md_lines.append("")
|
||||
md_lines.append(f"> 从拼多多商家后台截图 OCR 识别提取")
|
||||
md_lines.append(f"> 提取日期: 2026-06-16")
|
||||
md_lines.append(f"> 数据来源: `售价/` 目录下 {len(all_results)} 张截图")
|
||||
md_lines.append(f"> 共识别 {len(final_entries)} 个 SKU 售价(已去重)")
|
||||
md_lines.append("")
|
||||
md_lines.append("---")
|
||||
md_lines.append("")
|
||||
|
||||
for group_name in sorted(groups.keys()):
|
||||
entries = groups[group_name]
|
||||
md_lines.append(f"## {group_name}")
|
||||
md_lines.append("")
|
||||
md_lines.append("| SKU 名称 | 券后售价 (¥) | 券前原价 (¥) | OCR 置信度 |")
|
||||
md_lines.append("|---------|------------:|------------:|----------:|")
|
||||
|
||||
for e in entries:
|
||||
name = normalize_sku(e['sku'])
|
||||
# Clean up OCR artifacts in name
|
||||
name = name.replace('關注券5元优惠券', '').strip()
|
||||
name = re.sub(r'\s+', ' ', name)
|
||||
md_lines.append(f"| {name} | {e['price']:.2f} | {e['original_price']:.2f} | {e['confidence']:.2f} |")
|
||||
|
||||
md_lines.append("")
|
||||
|
||||
md_lines.append("---")
|
||||
md_lines.append("")
|
||||
md_lines.append("## 说明")
|
||||
md_lines.append("")
|
||||
md_lines.append("- **券后售价** = 券前原价 - 5.00元 (店铺关注券)")
|
||||
md_lines.append("- 价格数据通过 RapidOCR 从拼多多商家后台截图自动识别提取")
|
||||
md_lines.append("- 同一SKU在不同截图中出现时保留置信度最高的识别结果")
|
||||
md_lines.append("- OCR 识别可能存在误差,请以实际后台数据为准")
|
||||
|
||||
output_path = r"D:\workspace\membank\售价\SKU售价汇总.md"
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(md_lines))
|
||||
|
||||
print(f"\n\nSummary saved to: {output_path}")
|
||||
print(f"Total: {len(final_entries)} SKUs, {len(groups)} categories")
|
||||
258
.trae/skills/ocr-extract/parse_v2.py
Normal file
258
.trae/skills/ocr-extract/parse_v2.py
Normal file
@@ -0,0 +1,258 @@
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
|
||||
# Load OCR results
|
||||
with open(r"D:\workspace\membank\售价\ocr_results.json", "r", encoding="utf-8") as f:
|
||||
all_results = json.load(f)
|
||||
|
||||
def extract_sku_v4(text_lines):
|
||||
"""
|
||||
Robust extraction:
|
||||
Pattern in PDD backend: SKU_name ... [price1] ... [5.00] ... [price2]
|
||||
where price2 = price1 - 5
|
||||
"""
|
||||
results = []
|
||||
|
||||
# Build full text with confidences
|
||||
items = [(t[0].strip(), float(t[1]) if t[1] else 0.0) for t in text_lines]
|
||||
|
||||
i = 0
|
||||
while i < len(items):
|
||||
text, conf = items[i]
|
||||
|
||||
# Check for price patterns - find lines with two prices and 5.00 in between
|
||||
# Look ahead for price patterns
|
||||
prices_in_window = []
|
||||
window_items = []
|
||||
|
||||
for j in range(i, min(i + 8, len(items))):
|
||||
t = items[j][0]
|
||||
nums = re.findall(r'(\d{2,4}\.\d{2})', t)
|
||||
if nums:
|
||||
prices_in_window.extend([float(n) for n in nums])
|
||||
window_items.append((j, t, [float(n) for n in nums]))
|
||||
|
||||
if not prices_in_window:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
prices_set = set(round(p, 2) for p in prices_in_window)
|
||||
|
||||
# Try to find original price and discounted price (diff = 5)
|
||||
found_prices = []
|
||||
for p in sorted(prices_set):
|
||||
if round(p - 5, 2) in prices_set and p > 15:
|
||||
found_prices.append((round(p - 5, 2), p)) # (discounted, original)
|
||||
elif round(p + 5, 2) in prices_set and p > 15:
|
||||
found_prices.append((p, round(p + 5, 2)))
|
||||
|
||||
if not found_prices:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Now find the SKU name - look backwards from the price
|
||||
# The SKU name is typically 1-3 lines before the price
|
||||
sku_name = None
|
||||
for j in range(i-1, max(0, i-5), -1):
|
||||
t = items[j][0]
|
||||
# Must contain CPU or platform identifiers
|
||||
if re.search(r'(J1900|j1900|J3160|j3160|i3|I3|i5|I5|1037[Uu]|5005[Uu]|N5105|16G[Bb]|预装系统|双盘位|单盘位|三盘位|四盘位|黑裙|黑群晖|飞牛|蜗牛|Nas|NAS|盘位)', t):
|
||||
# Not a pure UI element
|
||||
if not re.match(r'^(商品|营销|官方|拼多多|商家|查看|修改|活动|价格|关注|折扣|券后|ID)', t):
|
||||
sku_name = t
|
||||
break
|
||||
|
||||
if sku_name:
|
||||
for discounted, original in found_prices:
|
||||
results.append({
|
||||
'sku': sku_name,
|
||||
'price': discounted,
|
||||
'original_price': original,
|
||||
'confidence': conf
|
||||
})
|
||||
|
||||
i += 1
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def normalize_sku_key(name):
|
||||
"""Create a normalized key for deduplication by extracting core identifiers"""
|
||||
name = name.strip()
|
||||
# Extract CPU type
|
||||
cpu = ''
|
||||
cpu_match = re.search(r'(J1900|j1900|J3160|j3160|i3|I3|i5|I5|1037[Uu]|5005[Uu]|N5105|N5095|J3355)', name)
|
||||
if cpu_match:
|
||||
cpu = cpu_match.group(1).upper().replace('J1900','J1900').replace('J3160','J3160')
|
||||
|
||||
# Extract memory
|
||||
mem = ''
|
||||
mem_match = re.search(r'(\d+[Gg])[+]?(\d+[Gg])?', name)
|
||||
if mem_match:
|
||||
mem = mem_match.group(0).upper()
|
||||
|
||||
# Extract form factor
|
||||
form = ''
|
||||
if '单盘' in name or '单盤' in name:
|
||||
form = '单盘'
|
||||
elif '双盘' in name or '雙盤' in name:
|
||||
form = '双盘'
|
||||
elif '三盘' in name:
|
||||
form = '三盘'
|
||||
elif '四盘' in name:
|
||||
form = '四盘'
|
||||
|
||||
# System USB
|
||||
if '16G' in name.upper() or '16g' in name:
|
||||
if 'windows' in name.lower():
|
||||
return 'USB:16GB:windows'
|
||||
elif '飞牛' in name or '蜗牛' in name:
|
||||
return 'USB:16GB:飞牛'
|
||||
return f'USB:16GB:{name[:20]}'
|
||||
|
||||
return f"{cpu}:{mem}:{form}"
|
||||
|
||||
|
||||
def clean_sku_name(name):
|
||||
"""Clean up OCR artifacts in SKU name"""
|
||||
name = name.strip()
|
||||
# Remove known noise
|
||||
name = re.sub(r'关注券5元优惠券', '', name)
|
||||
name = re.sub(r'店铺关注券5元优惠券', '', name)
|
||||
name = re.sub(r'[>\s]+$', '', name)
|
||||
name = re.sub(r'^[>\s]+', '', name)
|
||||
name = re.sub(r'\s+', ' ', name)
|
||||
return name
|
||||
|
||||
|
||||
# Collect all unique SKUs
|
||||
all_entries = []
|
||||
for img_name, lines in all_results.items():
|
||||
entries = extract_sku_v4(lines)
|
||||
all_entries.extend(entries)
|
||||
|
||||
# Deduplicate - keep highest confidence per normalized key
|
||||
dedup = {}
|
||||
for e in all_entries:
|
||||
key = normalize_sku_key(e['sku'])
|
||||
if key and key not in dedup:
|
||||
dedup[key] = e
|
||||
elif key and e['confidence'] > dedup[key]['confidence']:
|
||||
dedup[key] = e
|
||||
|
||||
# Sort by price
|
||||
sorted_entries = sorted(dedup.values(), key=lambda x: x['price'])
|
||||
|
||||
# Print for debugging
|
||||
print("=" * 60)
|
||||
print("Extracted unique SKUs:")
|
||||
for i, e in enumerate(sorted_entries):
|
||||
key = normalize_sku_key(e['sku'])
|
||||
print(f" [{key}] {clean_sku_name(e['sku'])} -> ${e['price']} (orig: ${e['original_price']})")
|
||||
|
||||
# Build markdown
|
||||
md = []
|
||||
md.append("# SKU 售价汇总")
|
||||
md.append("")
|
||||
md.append("> 从拼多多商家后台「商品价格管理」截图 OCR 识别提取")
|
||||
md.append(f"> 提取日期: 2026-06-16")
|
||||
md.append(f"> 图片数量: {len(all_results)} 张")
|
||||
md.append(f"> 识别 SKU: {len(sorted_entries)} 个(已去重合并)")
|
||||
md.append("")
|
||||
md.append("---")
|
||||
md.append("")
|
||||
|
||||
# Group by platform
|
||||
groups = {
|
||||
'系统U盘 (16GB)': [],
|
||||
'单盘位 - 1037U': [],
|
||||
'双盘位 - 1037U': [],
|
||||
'单盘位 - J1900': [],
|
||||
'双盘位 - J1900': [],
|
||||
'单盘位 - J3160': [],
|
||||
'双盘位 - J3160': [],
|
||||
'单盘位 - i3': [],
|
||||
'双盘位 - i3': [],
|
||||
'双盘位 - i5': [],
|
||||
'三盘位/四盘位 - i3/i5': [],
|
||||
'5005U 系列': [],
|
||||
'其他NAS/黑裙晖': [],
|
||||
}
|
||||
|
||||
for e in sorted_entries:
|
||||
name = e['sku']
|
||||
key = normalize_sku_key(name)
|
||||
|
||||
if 'USB' in key:
|
||||
groups['系统U盘 (16GB)'].append(e)
|
||||
elif '1037U' in key:
|
||||
if '单盘' in key:
|
||||
groups['单盘位 - 1037U'].append(e)
|
||||
else:
|
||||
groups['双盘位 - 1037U'].append(e)
|
||||
elif 'J1900' in key:
|
||||
if '单盘' in key:
|
||||
groups['单盘位 - J1900'].append(e)
|
||||
else:
|
||||
groups['双盘位 - J1900'].append(e)
|
||||
elif 'J3160' in key:
|
||||
if '单盘' in key:
|
||||
groups['单盘位 - J3160'].append(e)
|
||||
else:
|
||||
groups['双盘位 - J3160'].append(e)
|
||||
elif 'I3' in key:
|
||||
if '三盘' in key or '四盘' in key:
|
||||
groups['三盘位/四盘位 - i3/i5'].append(e)
|
||||
elif '单盘' in key:
|
||||
groups['单盘位 - i3'].append(e)
|
||||
else:
|
||||
groups['双盘位 - i3'].append(e)
|
||||
elif 'I5' in key:
|
||||
if '三盘' in key or '四盘' in key:
|
||||
groups['三盘位/四盘位 - i3/i5'].append(e)
|
||||
else:
|
||||
groups['双盘位 - i5'].append(e)
|
||||
elif '5005U' in key:
|
||||
groups['5005U 系列'].append(e)
|
||||
else:
|
||||
groups['其他NAS/黑裙晖'].append(e)
|
||||
|
||||
for group_name in groups:
|
||||
entries = groups[group_name]
|
||||
if not entries:
|
||||
continue
|
||||
|
||||
md.append(f"## {group_name}")
|
||||
md.append("")
|
||||
md.append("| SKU 配置 | 券后售价 (¥) | 券前原价 (¥) | 优惠 |")
|
||||
md.append("|---------|------------:|------------:|-----|")
|
||||
|
||||
for e in entries:
|
||||
name = clean_sku_name(e['sku'])
|
||||
md.append(f"| {name} | {e['price']:.2f} | {e['original_price']:.2f} | -5.00 |")
|
||||
|
||||
md.append("")
|
||||
|
||||
md.append("---")
|
||||
md.append("")
|
||||
md.append("## 说明")
|
||||
md.append("")
|
||||
md.append("- **优惠**: 统一使用店铺关注券 -5.00 元")
|
||||
md.append("- **数据来源**: 拼多多商家后台「商品价格管理」页面截图")
|
||||
md.append("- **识别工具**: RapidOCR (rapidocr-onnxruntime)")
|
||||
md.append("- **注意事项**: OCR 识别的 SKU 名称可能有碎片化/错字,建议对比后台实际数据确认")
|
||||
md.append("- **价格日期**: 2026-06-05 (截图时间)")
|
||||
md.append("")
|
||||
|
||||
# Fix the price summary line
|
||||
overall_min = min(e['price'] for e in sorted_entries) if sorted_entries else 0
|
||||
overall_max = max(e['price'] for e in sorted_entries) if sorted_entries else 0
|
||||
md.append(f"> 价格范围: ¥{overall_min:.2f} ~ ¥{overall_max:.2f}")
|
||||
|
||||
output_path = r"D:\workspace\membank\售价\SKU售价汇总.md"
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(md))
|
||||
|
||||
print(f"\n\nSaved to: {output_path}")
|
||||
print(f"Total unique SKUs: {len(sorted_entries)}")
|
||||
54
.trae/skills/ocr-extract/run_ocr.py
Normal file
54
.trae/skills/ocr-extract/run_ocr.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from rapidocr_onnxruntime import RapidOCR
|
||||
from PIL import Image
|
||||
import os, time, json
|
||||
|
||||
ocr = RapidOCR()
|
||||
img_dir = r"D:\workspace\membank\售价"
|
||||
images = sorted([f for f in os.listdir(img_dir) if f.lower().endswith(('.jpeg', '.jpg', '.png'))])
|
||||
|
||||
all_results = {}
|
||||
|
||||
for img_name in images:
|
||||
img_path = os.path.join(img_dir, img_name)
|
||||
img = Image.open(img_path)
|
||||
w, h = img.size
|
||||
print(f"\n=== {img_name} ({w}x{h}) ===")
|
||||
|
||||
# Use chunking for images taller than 2000px
|
||||
if h > 2000:
|
||||
chunk_size = 1200
|
||||
all_text = []
|
||||
for i in range(0, h, chunk_size):
|
||||
bottom = min(i + chunk_size, h)
|
||||
chunk = img.crop((0, i, w, bottom))
|
||||
tmp_path = os.path.join(os.environ['TEMP'], f'ocr_chunk_{i}.png')
|
||||
chunk.save(tmp_path)
|
||||
result, _ = ocr(tmp_path)
|
||||
if result:
|
||||
for line in result:
|
||||
text = line[1]
|
||||
conf = line[2]
|
||||
bbox = line[0]
|
||||
y_center = (bbox[0][1] + bbox[2][1]) / 2 + i # absolute y position
|
||||
all_text.append((y_center, text, conf))
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
# Sort by vertical position
|
||||
all_text.sort(key=lambda x: x[0])
|
||||
lines = [(t[1], t[2]) for t in all_text]
|
||||
else:
|
||||
result, _ = ocr(img_path)
|
||||
if result:
|
||||
lines = [(line[1], line[2]) for line in result]
|
||||
else:
|
||||
lines = []
|
||||
|
||||
all_results[img_name] = lines
|
||||
for text, conf in lines:
|
||||
print(f" [{conf}] {text}")
|
||||
|
||||
# Save raw results as JSON for later processing
|
||||
with open(os.path.join(img_dir, "ocr_results.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(all_results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("\n\n=== OCR完成,结果已保存到 ocr_results.json ===")
|
||||
132
.trae/skills/sku-designer/principles/fyd-nas.md
Normal file
132
.trae/skills/sku-designer/principles/fyd-nas.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 飞牛 NAS 产品设计原则
|
||||
|
||||
## 基础版-金
|
||||
|
||||
### 机箱与CPU对应
|
||||
- 2.5金-单(单SATA口):amd-a4, n2840, n2930, j1800
|
||||
- 3.5金属大-双(双SATA口):1037u
|
||||
- 3.5金属小-单(单SATA口):amd-a4, n2840, n2930, j1800
|
||||
- 3.5金属小-双(双SATA口):1037u
|
||||
|
||||
### 内存硬盘
|
||||
- 4G-32G
|
||||
|
||||
### 建议零售价
|
||||
- 成本+30
|
||||
|
||||
## 基础版-亚
|
||||
|
||||
### 机箱与CPU对应
|
||||
- 亚克力-双(双SATA口):1037u
|
||||
- 亚克力-单(单SATA口):amd-a4, n2840, n2930, j1800
|
||||
|
||||
### 内存硬盘
|
||||
- 4G-32G
|
||||
|
||||
### 建议零售价
|
||||
- 成本+30
|
||||
|
||||
## 金属机箱 3.5寸大
|
||||
|
||||
### 机箱类型
|
||||
- 3盘版(单SATA口CPU):3855U, J1900, J3160
|
||||
- 4盘版(双SATA口CPU):1037U, 3217U, 5005U
|
||||
|
||||
### 内存硬盘可用
|
||||
- 4G-32G, 4G-64G, 8G-64G, 8G-128G
|
||||
|
||||
## 金属机箱 3.5寸小
|
||||
|
||||
### 机箱类型
|
||||
- 3.5+2.5盘位(双SATA口):1037U
|
||||
|
||||
### 内存硬盘
|
||||
- 4G-32G
|
||||
|
||||
### 数据盘可选
|
||||
- 500G, 1T, 3T, 4T, 6T
|
||||
|
||||
### 建议零售价
|
||||
- 主机(无硬盘):+50
|
||||
- 主机+500G:+80
|
||||
- 主机+1T:+90
|
||||
- 主机+3T:+100
|
||||
- 主机+4T/6T:+110
|
||||
|
||||
## 亚克力机箱-单 配置原则
|
||||
|
||||
### 主板要求
|
||||
- 可安装单SATA口主板
|
||||
|
||||
### 可用CPU
|
||||
- AMD-A4,J1900,J3160
|
||||
|
||||
### 可用内存
|
||||
- D3-2G,D3-4G
|
||||
|
||||
### 可用系统盘
|
||||
- mSATA-16G,mSATA-32G,mSATA-64G,mSATA-128G
|
||||
|
||||
### 推荐价格
|
||||
- 配2G内存:成本+20
|
||||
- 配4G内存:成本+30
|
||||
|
||||
## 亚克力机箱-双 配置原则
|
||||
|
||||
### 主板要求
|
||||
- 可安装双SATA口主板
|
||||
|
||||
### 可用CPU
|
||||
- 1037U,3217U,5005U
|
||||
|
||||
### 可用内存
|
||||
- 2G-16G,2G-32G,4G-64G,4G-128G,8G-128G
|
||||
|
||||
## 高性能版-亚-双-J1900
|
||||
|
||||
### 机箱
|
||||
- 亚克力-双机箱
|
||||
|
||||
### CPU
|
||||
- J1900(单SATA口,需双SATA转换器)
|
||||
|
||||
### 内存硬盘可用
|
||||
- 4G-32G, 4G-64G, 8G-64G, 8G-128G
|
||||
|
||||
### 建议零售价
|
||||
- 成本+50
|
||||
|
||||
## 高性能版-亚-双-J3160
|
||||
|
||||
### 机箱
|
||||
- 亚克力-双机箱
|
||||
|
||||
### CPU
|
||||
- J3160(单SATA口,需双SATA转换器)
|
||||
|
||||
### 内存硬盘可用
|
||||
- 4G-32G, 4G-64G, 8G-64G, 8G-128G
|
||||
|
||||
### 建议零售价
|
||||
- 成本+70
|
||||
|
||||
## 高性能-i5-i7
|
||||
|
||||
### 可用机箱
|
||||
- 亚克力机箱-双
|
||||
- 金属机箱3.5寸大
|
||||
|
||||
### CPU i5
|
||||
- 可用:i5-2代,i5-3代,i5-4代
|
||||
|
||||
### CPU i7
|
||||
- 可用:i7-2代,i7-3代,i7-4代
|
||||
|
||||
### 内存硬盘
|
||||
- i5:4G-128G,8G-128G
|
||||
- i7统一:8G-128G
|
||||
|
||||
### 建议零售价
|
||||
- 成本+80
|
||||
|
||||
## 金属机箱3.5寸大-三或四盘版
|
||||
36
.trae/skills/sku-designer/principles/guajibao.md
Normal file
36
.trae/skills/sku-designer/principles/guajibao.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 挂机宝产品设计原则
|
||||
|
||||
## 概述
|
||||
|
||||
挂机宝分为一拖一和一拖三两种规格
|
||||
|
||||
## 一拖一
|
||||
|
||||
### 可选CPU
|
||||
- 1037u, i3-3217u, i3-5005u, 3855u
|
||||
|
||||
### 可选内存-系统盘
|
||||
- 4G-64G, 4G-128G, 8G-128G
|
||||
|
||||
### 可选硬盘
|
||||
- HD 500G
|
||||
|
||||
### 机箱
|
||||
- 2.5寸金属机箱
|
||||
|
||||
### 可选
|
||||
- 无线网卡
|
||||
|
||||
## 一拖三
|
||||
|
||||
### 可选CPU
|
||||
- 1037u, i3-3217u, i3-5005u, 3855u
|
||||
|
||||
### 可选内存-系统盘
|
||||
- 4G-64G, 4G-128G, 8G-128G
|
||||
|
||||
### 机箱
|
||||
- 一拖三亚克力机箱
|
||||
|
||||
### 可选
|
||||
- 无线网卡
|
||||
31
.trae/skills/sku-designer/principles/win-host.md
Normal file
31
.trae/skills/sku-designer/principles/win-host.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Win 小主机产品设计原则
|
||||
|
||||
## 通用规格
|
||||
|
||||
- win小主机全部使用2.5寸机箱
|
||||
|
||||
## 龙虾盒子
|
||||
|
||||
### 可用CPU
|
||||
- i3-5005u, 3855u
|
||||
|
||||
### 内存硬盘可使用
|
||||
- 4G-64G, 4G-128G, 8G-128G
|
||||
|
||||
### 标配
|
||||
- 无线网卡
|
||||
|
||||
### 可选
|
||||
- HD 500G硬盘
|
||||
|
||||
## Win10 小主机
|
||||
|
||||
### 可用CPU
|
||||
- 1037u, i3-3217u, i3-5005u, 3855u
|
||||
|
||||
### 内存硬盘可使用
|
||||
- 4G-64G, 4G-128G, 8G-128G
|
||||
|
||||
### 可选
|
||||
- 无线网卡
|
||||
- HD 500G硬盘
|
||||
12
aio-mcp/.gitignore
vendored
Normal file
12
aio-mcp/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# 构建产物
|
||||
/aio-mcp
|
||||
/aio-mcp.exe
|
||||
/dist/
|
||||
*.exe
|
||||
|
||||
# 运行时数据
|
||||
/data/
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
100
aio-mcp/README.md
Normal file
100
aio-mcp/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# aio-mcp
|
||||
|
||||
aio-system 的自定义 MCP 服务器(Go)。作为 AI Agent 的「手」,以 MCP 协议对外暴露工具;后续的 SQLite / 文件 / 打印 / 检测等自定义工具都往这里加。
|
||||
|
||||
基于官方 SDK:[github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk)(v1.7.0)。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
aio-mcp/
|
||||
├── main.go # 入口:选择 stdio / http 传输
|
||||
└── internal/
|
||||
├── server/
|
||||
│ └── server.go # 服务器装配(创建实例 + 注册工具)
|
||||
└── tools/
|
||||
├── registry.go # 工具注册总表 RegisterAll
|
||||
├── ping.go # 示例工具:健康检查(无参数)
|
||||
└── echo.go # 示例工具:参数传递(有参数)
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
go build -o aio-mcp.exe . # 构建
|
||||
|
||||
# stdio 模式(默认,供本地 Agent 拉起,如 Reasonix / Claude)
|
||||
aio-mcp.exe
|
||||
|
||||
# http 模式(远程客户端访问)
|
||||
aio-mcp.exe -transport http -addr localhost:8000
|
||||
```
|
||||
|
||||
依赖拉取走内网代理,若需直连官方源:`go env -w GOPROXY=https://proxy.golang.org,direct`。
|
||||
|
||||
## 新增一个工具
|
||||
|
||||
1. 在 `internal/tools/` 新建文件(如 `weather.go`),用参数 struct + 泛型 `mcp.AddTool` 注册:
|
||||
|
||||
```go
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
type weatherArgs struct {
|
||||
City string `json:"city" jsonschema:"城市名,必填"`
|
||||
Days int `json:"days,omitempty" jsonschema:"预报天数,默认 1"`
|
||||
}
|
||||
|
||||
func registerWeather(s *mcp.Server) {
|
||||
mcp.AddTool(s, &mcp.Tool{
|
||||
Name: "weather",
|
||||
Description: "查询某城市未来几天的天气。",
|
||||
}, func(_ context.Context, _ *mcp.CallToolRequest, args weatherArgs) (*mcp.CallToolResult, any, error) {
|
||||
// ...业务逻辑...
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "晴,25℃"}},
|
||||
}, nil, nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
2. 在 `internal/tools/registry.go` 的 `RegisterAll` 中追加一行:`registerWeather(s)`。
|
||||
|
||||
工具需要共享状态(SQLite 连接等)时,参考 `registry.go` 顶部注释,把 `RegisterAll` 演进为带 `deps` 参数。
|
||||
|
||||
## 冒烟测试
|
||||
|
||||
stdio 模式用换行分隔的 JSON-RPC 直接喂协议消息:
|
||||
|
||||
```bash
|
||||
printf '%s\n' \
|
||||
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}' \
|
||||
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
||||
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
|
||||
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"你好","times":2}}}' \
|
||||
| aio-mcp.exe
|
||||
```
|
||||
|
||||
http 模式:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/mcp -H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0.0.1"}}}'
|
||||
```
|
||||
|
||||
## 接入 Agent(以 Reasonix / Claude 为例)
|
||||
|
||||
在客户端 MCP 配置中注册,指向构建产物:
|
||||
|
||||
```json
|
||||
{ "mcpServers": { "aio-mcp": { "command": "D:\\workspace\\membank\\aio-mcp\\aio-mcp.exe" } } }
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- stdio 模式下 **stdout 是协议通道**:日志已固定输出到 stderr,业务代码请勿往 stdout 打印。
|
||||
- 工具名只允许 `a-z A-Z 0-9 _ - .`。
|
||||
16
aio-mcp/go.mod
Normal file
16
aio-mcp/go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module aio-mcp
|
||||
|
||||
go 1.25.1
|
||||
|
||||
require github.com/modelcontextprotocol/go-sdk v1.7.0
|
||||
|
||||
require (
|
||||
github.com/google/jsonschema-go v0.4.3 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
24
aio-mcp/go.sum
Normal file
24
aio-mcp/go.sum
Normal file
@@ -0,0 +1,24 @@
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
||||
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44=
|
||||
github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts=
|
||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
|
||||
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
22
aio-mcp/internal/server/server.go
Normal file
22
aio-mcp/internal/server/server.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package server 负责 MCP 服务器的装配:创建 server 实例并注册全部工具。
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"aio-mcp/internal/tools"
|
||||
)
|
||||
|
||||
// New 创建并装配 aio-mcp 服务器。
|
||||
// version 为服务器实现版本号,随 MCP initialize 握手上报给客户端。
|
||||
func New(version string) *mcp.Server {
|
||||
s := mcp.NewServer(&mcp.Implementation{
|
||||
Name: "aio-mcp",
|
||||
Version: version,
|
||||
}, nil)
|
||||
|
||||
// 注册全部工具。新增工具后在 tools.RegisterAll 里追加即可。
|
||||
tools.RegisterAll(s)
|
||||
|
||||
return s
|
||||
}
|
||||
40
aio-mcp/internal/tools/echo.go
Normal file
40
aio-mcp/internal/tools/echo.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// echoArgs 是 echo 工具的输入参数。
|
||||
// json tag 决定参数名;jsonschema tag 提供描述,供客户端(LLM)理解参数。
|
||||
type echoArgs struct {
|
||||
Message string `json:"message" jsonschema:"要回显的文本,必填"`
|
||||
Times int `json:"times,omitempty" jsonschema:"回显次数,默认 1"`
|
||||
}
|
||||
|
||||
// registerEcho 注册 echo 工具:演示带参数的工具写法。
|
||||
func registerEcho(s *mcp.Server) {
|
||||
mcp.AddTool(s, &mcp.Tool{
|
||||
Name: "echo",
|
||||
Description: "把传入的文本原样回显指定次数,用于演示参数传递与工具调用。",
|
||||
}, func(_ context.Context, _ *mcp.CallToolRequest, args echoArgs) (*mcp.CallToolResult, any, error) {
|
||||
times := args.Times
|
||||
if times <= 0 {
|
||||
times = 1
|
||||
}
|
||||
// 用换行分隔,避免重复文本粘连。
|
||||
line := strings.TrimRight(args.Message, "\n")
|
||||
lines := make([]string, times)
|
||||
for i := range lines {
|
||||
lines[i] = line
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("%s\n(共 %d 次)", strings.Join(lines, "\n"), times)},
|
||||
},
|
||||
}, nil, nil
|
||||
})
|
||||
}
|
||||
22
aio-mcp/internal/tools/ping.go
Normal file
22
aio-mcp/internal/tools/ping.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// registerPing 注册 ping 工具:无参数健康检查,返回 pong 与服务器时间。
|
||||
func registerPing(s *mcp.Server) {
|
||||
mcp.AddTool(s, &mcp.Tool{
|
||||
Name: "ping",
|
||||
Description: "健康检查。返回 pong 与服务器当前时间,用于确认 aio-mcp 连接正常。",
|
||||
}, func(_ context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "pong " + time.Now().Format(time.RFC3339)},
|
||||
},
|
||||
}, nil, nil
|
||||
})
|
||||
}
|
||||
26
aio-mcp/internal/tools/registry.go
Normal file
26
aio-mcp/internal/tools/registry.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Package tools 集中管理 aio-mcp 的全部 MCP 工具。
|
||||
//
|
||||
// # 新增工具的固定步骤
|
||||
//
|
||||
// 1. 在本目录新建一个文件(如 weather.go),实现
|
||||
// func registerXXX(s *mcp.Server)
|
||||
// 2. 在 RegisterAll 中追加一行 registerXXX(s)
|
||||
//
|
||||
// 工具通过泛型 mcp.AddTool[In, Out] 注册:输入参数用 struct 定义,
|
||||
// 加 json 与 jsonschema tag(描述、必填),SDK 会自动生成 inputSchema
|
||||
// 并做输入校验,无需手工写 JSON Schema。
|
||||
//
|
||||
// 后续工具需要共享状态(SQLite 连接、配置、http client 等)时,
|
||||
// 把 RegisterAll 演进为 RegisterAll(s *mcp.Server, deps *Deps),
|
||||
// 在 server.New 里构造 deps 传入即可,各工具文件只改签名。
|
||||
package tools
|
||||
|
||||
import (
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// RegisterAll 注册所有工具。
|
||||
func RegisterAll(s *mcp.Server) {
|
||||
registerPing(s)
|
||||
registerEcho(s)
|
||||
}
|
||||
53
aio-mcp/main.go
Normal file
53
aio-mcp/main.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// aio-mcp:aio-system 的自定义 MCP 服务器。
|
||||
//
|
||||
// 作为 AI Agent 的「手」,以 MCP 协议对外暴露工具。
|
||||
// 默认以 stdio 传输运行(本地 Agent 直接拉起),也可切换为
|
||||
// streamable HTTP 供远程客户端访问。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"aio-mcp/internal/server"
|
||||
)
|
||||
|
||||
// version 可用 -ldflags "-X main.version=..." 在构建时覆盖。
|
||||
var version = "0.1.0"
|
||||
|
||||
func main() {
|
||||
transport := flag.String("transport", "stdio", "传输方式:stdio(默认,供本地 Agent 拉起)或 http")
|
||||
addr := flag.String("addr", "localhost:8000", "http 模式下的监听地址")
|
||||
flag.Parse()
|
||||
|
||||
// 日志统一走 stderr:stdio 模式下 stdout 是 MCP 协议通道,绝不能混入日志。
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
|
||||
svr := server.New(version)
|
||||
|
||||
switch *transport {
|
||||
case "stdio":
|
||||
slog.Info("aio-mcp starting", "transport", "stdio")
|
||||
// server.Run 会阻塞,直到 stdin 关闭或出错。
|
||||
if err := svr.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
|
||||
slog.Error("stdio server failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "http":
|
||||
handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return svr }, nil)
|
||||
slog.Info("aio-mcp starting", "transport", "http", "addr", *addr)
|
||||
if err := http.ListenAndServe(*addr, handler); err != nil {
|
||||
slog.Error("http server failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "未知 transport: %q(支持: stdio, http)\n", *transport)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
6
auto-check/.gitignore
vendored
6
auto-check/.gitignore
vendored
@@ -2,6 +2,11 @@
|
||||
reports/
|
||||
*.exe
|
||||
*.test
|
||||
run.log
|
||||
run.err
|
||||
|
||||
# 编译产物
|
||||
/auto-check
|
||||
|
||||
# 导出的镜像文件(体积大,不入库;维护时重新 docker save 生成)
|
||||
docker/stress-test/*.tar
|
||||
@@ -9,3 +14,4 @@ docker/stress-test/*.tar
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
.codewhale/
|
||||
|
||||
@@ -55,7 +55,7 @@ go build -o auto-check .
|
||||
### `run` / `stress`
|
||||
| 标志 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `--stress` | `cpu,memory` | 压测类型: `cpu`, `memory`, `disk`, `memnative`, `full` |
|
||||
| `--stress` | `cpu,memory` | 压测类型: `cpu`, `memory`, `disk`, `memnative`, `fnstable`, `full` |
|
||||
| `--duration` | `30s` | 持续时间 |
|
||||
| `--threads` | `4` | 线程数 |
|
||||
| `--ips` | _(必填,仅stress)_ | 目标 IP (逗号分隔) |
|
||||
@@ -68,8 +68,24 @@ go build -o auto-check .
|
||||
| `memory` | stress-ng | 内存压测 |
|
||||
| `disk` | stress-ng | 磁盘IO混合压测 |
|
||||
| `memnative` | stressapptest | 内存稳定性精压(数据完整性校验) |
|
||||
| `fnstable` | fnscript | 稳定性门禁:社区脚本 self_inspection.py 先行检测硬件(stress CPU + memtester 内存校验),通过后自有压测再跑;未通过则自有压测全部跳过 |
|
||||
| `full` | stress-ng | CPU+内存+磁盘 三合一综合压测 |
|
||||
|
||||
## 稳定性门禁(fnstable)
|
||||
|
||||
基于飞牛社区 [fnscript](https://gitee.com/xiao-zhu245/fnscript)(MIT)的 `self_inspection.py`,vendor 原样收录在 `docker/stress-test/vendor/fnscript/`,源码不做修改:
|
||||
|
||||
1. 门禁先跑社区脚本:CPU 稳定性(`stress`)+ 内存数据校验(`memtester` 可用内存 97% 单遍,30-120 分钟)
|
||||
2. 门禁通过 → 自有压测工具(stress-ng/fio 等)再跑一遍,按自有格式出报告
|
||||
3. 门禁失败 → 自有压测全部 skip,报告仍完整输出(硬件已有问题无需再跑)
|
||||
|
||||
适配说明(不改社区脚本源码的代价,已在适配层 `scripts/fnstable.sh` 规避):
|
||||
- 脚本 CLI 模式无论成败 exit code 均为 0,结果按 stdout 关键词("未发现问题")判定
|
||||
- `--memory` 有 y/n 交互确认,用 `printf 'y\ny\n'` 管道喂入
|
||||
- 依赖(python3/psutil/memtester/stress/sudo)预装在压测镜像中
|
||||
|
||||
配置(`auto-check.yaml`):`stress.fn_memory` 控制门禁是否执行 memtester 内存校验(默认 true,快速验证可设 false)。
|
||||
|
||||
## 压测执行方式(Docker 容器)
|
||||
|
||||
压测统一在 Docker 容器中执行:工具与测试脚本预装在镜像里,无需在目标机在线安装,环境一致性好(飞牛 NAS 默认已安装 Docker)。
|
||||
@@ -80,13 +96,15 @@ auto-check/
|
||||
├── docker/ # Docker 镜像维护目录(独立)
|
||||
│ └── stress-test/
|
||||
│ ├── Dockerfile # 镜像定义
|
||||
│ ├── build.sh # 构建 + 导出 + 同步配置(时间戳 tag)
|
||||
│ ├── scripts/ # 测试脚本(打包进镜像)
|
||||
│ │ ├── run.sh # 入口脚本(环境变量驱动)
|
||||
│ │ ├── run.sh # 入口脚本(环境变量驱动 + 门禁调度)
|
||||
│ │ ├── fnstable.sh # 稳定性门禁适配器(fnscript 社区脚本 → 结构化协议)
|
||||
│ │ ├── cpu.sh # CPU 压测
|
||||
│ │ ├── memory.sh # 内存压测
|
||||
│ │ ├── disk.sh # 磁盘压测
|
||||
│ │ └── ...
|
||||
│ └── README.md
|
||||
│ └── vendor/fnscript/ # 社区脚本原样收录(self_inspection.py,MIT)
|
||||
└── pkg/stress/
|
||||
└── runner.go # 核心逻辑:送镜像 + 驱动容器测试
|
||||
```
|
||||
@@ -97,24 +115,71 @@ auto-check/
|
||||
3. 启动容器执行测试(脚本在镜像内,环境变量传参)
|
||||
4. 解析容器输出 → 生成报告
|
||||
|
||||
### 容器内执行流程(run.sh)
|
||||
1. 系统信息(`===SYSTEM_INFO===`)
|
||||
2. 温度监控后台采样(`temp_start.sh`)
|
||||
3. 稳定性门禁(`fnstable.sh`,types 含 `fnstable` 时):fnscript 社区脚本先行检测硬件稳定性,未通过则后续自有压测全部 skip
|
||||
4. 各项压测:先单项(cpu/memory/disk/memnative)、综合(full)最后
|
||||
5. 温度监控收尾(`===MONITOR:temp===`)
|
||||
6. 内核日志(`===MONITOR:dmesg===`,`-T` 带时间戳)+ 结束标记
|
||||
|
||||
### 配置方式(在 `auto-check.yaml` 中)
|
||||
```yaml
|
||||
stress:
|
||||
docker_image: "stress-test:latest" # 压测镜像(飞牛 NAS 默认支持 Docker)
|
||||
docker_image: "stress-test:20260824-153045" # 压测镜像 tag(build.sh 自动维护)
|
||||
docker_tar: "docker/stress-test/stress-test.tar" # 本地镜像文件,目标机无镜像时自动上传并 load
|
||||
docker_args: "--network=host --privileged"
|
||||
```
|
||||
|
||||
### 容器运行时环境变量
|
||||
| 变量 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `AUTOCHECK_TYPES` | 测试类型(逗号分隔) | `cpu,memory` |
|
||||
| `AUTOCHECK_DURATION` | 持续时间(秒) | `30` |
|
||||
| `AUTOCHECK_THREADS` | 线程数 | `4` |
|
||||
| `AUTOCHECK_MEM_SIZE_MB` | 内存压测大小 | `0` (自动) |
|
||||
| `AUTOCHECK_DISK_SIZE_MB` | 磁盘压测大小 | `1024` |
|
||||
| `AUTOCHECK_DISK_DIR` | 磁盘压测目录 | `/tmp` |
|
||||
| `AUTOCHECK_TEMP_INTERVAL` | 温度采样间隔 | `10` |
|
||||
| `AUTOCHECK_SAMPLE_INTERVAL` | 运行时采样间隔 | `2` |
|
||||
| `AUTOCHECK_FN_MEMORY` | fnstable 门禁是否执行 memtester 内存校验 | `1` |
|
||||
|
||||
### 容器运行时挂载(由 auto-check 自动添加)
|
||||
| 挂载 | 作用 |
|
||||
|------|------|
|
||||
| `-v /sys:/sys:ro` | 容器内是独立 sysfs,挂载后才能读到宿主机硬件传感器(温度采集依赖此项) |
|
||||
| `-v /etc/localtime:/etc/localtime:ro` | 容器时区与宿主机一致,dmesg 时间戳与采样时间才准确 |
|
||||
|
||||
> 注意:镜像内已设置 `LC_ALL=C.UTF-8`。若 locale 为纯 C,`sensors` 会输出 "+51.0 C"(无 ° 符号),导致温度正则失配、采集不到数据。
|
||||
|
||||
### 构建镜像(维护时使用)
|
||||
固定分发模式:不依赖 registry,镜像以 tar 文件形式放在工程目录。推荐用 `build.sh`(自动时间戳 tag):
|
||||
|
||||
```bash
|
||||
cd docker/stress-test
|
||||
docker build -t stress-test:latest .
|
||||
# 导出到工程目录,供测试时上传分发(不依赖 registry)
|
||||
docker save -o stress-test.tar stress-test:latest
|
||||
./build.sh # 自动取当前时间戳(YYYYMMDD-HHMMSS)
|
||||
./build.sh 20260824-153045 # 手动指定时间戳(重打/复现)
|
||||
```
|
||||
|
||||
### 详细文档
|
||||
镜像维护说明请参考:[docker/stress-test/README.md](docker/stress-test/README.md)
|
||||
`build.sh` 会同时完成:
|
||||
1. 用时间戳 tag 构建镜像(不保留 `latest`,`docker images` 可区分每次构建)
|
||||
2. 导出两份 tar:带时间戳归档 `stress-test-<时间戳>.tar` + 同步覆盖 `stress-test.tar`(auto-check 默认引用)
|
||||
3. 自动更新 `auto-check.yaml` 的 `docker_image` 字段,确保配置与新 tag 同步
|
||||
|
||||
手动构建(不推荐)需自行同步 `auto-check.yaml` 的 `docker_image`;旧时间戳镜像可 `docker rmi stress-test:<旧时间戳>` 清理。
|
||||
|
||||
### 镜像内预装工具
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| `stress-ng` | CPU/内存/磁盘压测 |
|
||||
| `fio` | 磁盘 IO 测试 |
|
||||
| `lm-sensors` | 温度监控 |
|
||||
| `sysstat` | 系统性能统计 |
|
||||
| `procps` | 进程监控 |
|
||||
| `python3` + `psutil` | fnscript 稳定性门禁运行环境 |
|
||||
| `memtester` | fnstable 门禁内存数据校验 |
|
||||
| `stress` | fnstable 门禁 CPU 压测 |
|
||||
| `sudo` | fnscript 脚本内部调用 `sudo memtester`(容器以 root 运行,无密码) |
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
||||
Binary file not shown.
@@ -19,7 +19,9 @@ ssh:
|
||||
|
||||
# --- 压力测试 ---
|
||||
stress:
|
||||
types: "cpu,memory,disk,full" # 单项在前、综合(full)最后
|
||||
# fnstable(fnscript 社区稳定性脚本)放在最前作门禁:先检测硬件真实稳定性,
|
||||
# 通过后自有压测再跑一遍出报告;未通过则自有压测全部跳过
|
||||
types: "fnstable,cpu,memory,disk,full" # 单项在前、综合(full)最后
|
||||
duration: 30s
|
||||
threads: 4
|
||||
mem_size_mb: 0 # 内存压测占用(MB),0=自动(可用内存60%)
|
||||
@@ -27,6 +29,8 @@ stress:
|
||||
disk_dir: "" # 磁盘压测目录,空=自动临时目录
|
||||
temp_interval: 10s # 温度采样间隔
|
||||
temp_limit: 90 # 温度上限(°C),超过判失败,0=不判定
|
||||
fn_memory: true # fnstable 门禁的 memtester 内存校验(可用内存97%单遍,
|
||||
# 耗时 30-120 分钟按内存大小定;false=仅 CPU 门禁,快速出报告)
|
||||
# --- Docker 镜像(压测统一在容器中执行,工具与脚本预装在镜像里)---
|
||||
# 固定分发模式:目标机无镜像时,上传本地导出的 tar 并 docker load(不依赖 registry)
|
||||
docker_image: "stress-test:20260824-144244" # 压测镜像名,镜像维护见 docker/stress-test/
|
||||
|
||||
@@ -32,20 +32,34 @@ FROM debian:12-slim
|
||||
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
|
||||
|
||||
# 清理缓存,减少镜像体积
|
||||
# - python3/python3-psutil/memtester/stress/sudo:fnscript 稳定性门禁的依赖
|
||||
# (self_inspection.py 调 psutil 计算测试内存、sudo memtester 做数据校验、
|
||||
# stress 做 CPU 压测;脚本原样收录在 vendor/fnscript/,源码不做修改)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
stress-ng \
|
||||
fio \
|
||||
lm-sensors \
|
||||
sysstat \
|
||||
procps \
|
||||
python3 \
|
||||
python3-psutil \
|
||||
memtester \
|
||||
stress \
|
||||
sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 验证工具安装
|
||||
RUN stress-ng --version && fio --version && sensors --version
|
||||
RUN stress-ng --version && fio --version && sensors --version \
|
||||
&& python3 -c "import psutil; print('psutil', psutil.__version__)" \
|
||||
&& command -v memtester && command -v stress && command -v sudo
|
||||
|
||||
# 复制测试脚本到镜像中
|
||||
COPY scripts/ /scripts/
|
||||
|
||||
# fnscript 稳定性测试脚本(社区脚本 vendor 收录,源码不改,见 vendor/fnscript/README.md)
|
||||
COPY vendor/fnscript/script/self_inspection.py /vendor/fnscript/self_inspection.py
|
||||
COPY vendor/fnscript/LICENSE /vendor/fnscript/LICENSE
|
||||
|
||||
# 设置脚本执行权限
|
||||
RUN chmod +x /scripts/*.sh
|
||||
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
# stress-test 镜像
|
||||
|
||||
压力测试镜像,为 auto-check 提供预装压测工具与测试脚本的容器环境。压测统一在容器内执行,无需在目标机在线安装任何依赖。
|
||||
|
||||
## 工作流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ auto-check 工作流程 │
|
||||
│ │
|
||||
│ 1. 检测目标机器的 Docker 可用性 │
|
||||
│ 2. 检查镜像是否存在(不存在则上传本地导出的 tar 并 docker load)│
|
||||
│ 3. 启动容器执行测试(脚本已打包在镜像中) │
|
||||
│ 4. 收集测试结果并解析 │
|
||||
│ 5. 生成报告 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 工具获取 | 预装在镜像中,无需在线安装 |
|
||||
| 网络依赖 | 镜像已缓存后目标机无需联网 |
|
||||
| 环境一致性 | 统一的容器环境,不受目标机发行版差异影响 |
|
||||
|
||||
## 构建与导出(维护时使用)
|
||||
|
||||
固定分发模式:不依赖 registry,镜像以 tar 文件形式放在工程目录,
|
||||
目标机无镜像时由 auto-check 自动上传并 `docker load`。
|
||||
|
||||
### 推荐:使用构建脚本(自动时间戳 tag)
|
||||
|
||||
`build.sh` 会以 `YYYYMMDD-HHMMSS` 格式自动生成时间戳 tag(如 `stress-test:20260824-153045`),
|
||||
便于在 `docker images` 中一眼区分新旧版本。脚本会同时:
|
||||
|
||||
1. 用时间戳 tag 构建镜像(**不再保留 `latest`**)
|
||||
2. 导出两份 tar:带时间戳的归档 + 同步覆盖 `stress-test.tar`(auto-check 默认引用)
|
||||
3. 自动更新 `auto-check.yaml` 的 `docker_image` 字段,确保配置与新 tag 同步
|
||||
|
||||
```bash
|
||||
cd docker/stress-test
|
||||
./build.sh # 自动取当前时间戳
|
||||
./build.sh 20260824-153045 # 手动指定时间戳(重打/复现)
|
||||
```
|
||||
|
||||
产物:
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| 镜像 `stress-test:<时间戳>` | 唯一 tag,`docker images` 可看到每次构建 |
|
||||
| Tar `stress-test-<时间戳>.tar` | 带时间戳归档,保留历史版本 |
|
||||
| Tar `stress-test.tar` | auto-check 默认引用,脚本自动覆盖为最新 |
|
||||
| `auto-check.yaml` | `docker_image` 自动同步为新 tag |
|
||||
|
||||
> 旧时间戳镜像可手动 `docker rmi stress-test:<旧时间戳>` 清理。
|
||||
|
||||
### 手动构建(不推荐)
|
||||
|
||||
如需绕过脚本手动构建,必须自行同步 `auto-check.yaml`:
|
||||
|
||||
```bash
|
||||
cd docker/stress-test
|
||||
docker build -t stress-test:20260824-153045 . # 必须带时间戳
|
||||
docker save -o stress-test.tar stress-test:20260824-153045
|
||||
# 然后编辑 ../../auto-check.yaml,把 docker_image 改成新 tag
|
||||
```
|
||||
|
||||
> 注意:手动构建如忘记带时间戳,重复构建后会丢失版本信息,调试时易混淆。
|
||||
|
||||
## 使用
|
||||
|
||||
在 `auto-check.yaml` 中配置(`docker_image` 由 `build.sh` 自动维护):
|
||||
|
||||
```yaml
|
||||
stress:
|
||||
docker_image: "stress-test:20260824-153045" # 每次构建自动更新为新时间戳
|
||||
docker_tar: "docker/stress-test/stress-test.tar"
|
||||
docker_args: "--network=host --privileged"
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
容器运行时通过以下环境变量传入参数:
|
||||
|
||||
| 变量 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `AUTOCHECK_TYPES` | 测试类型(逗号分隔) | `cpu,memory` |
|
||||
| `AUTOCHECK_DURATION` | 持续时间(秒) | `30` |
|
||||
| `AUTOCHECK_THREADS` | 线程数 | `4` |
|
||||
| `AUTOCHECK_MEM_SIZE_MB` | 内存压测大小 | `0` (自动) |
|
||||
| `AUTOCHECK_DISK_SIZE_MB` | 磁盘压测大小 | `1024` |
|
||||
| `AUTOCHECK_DISK_DIR` | 磁盘压测目录 | `/tmp` |
|
||||
| `AUTOCHECK_TEMP_INTERVAL` | 温度采样间隔 | `10` |
|
||||
| `AUTOCHECK_SAMPLE_INTERVAL` | 运行时采样间隔 | `2` |
|
||||
|
||||
## 容器内执行流程(run.sh)
|
||||
|
||||
1. 系统信息(`===SYSTEM_INFO===`)
|
||||
2. 温度监控后台采样(`temp_start.sh`)
|
||||
3. 各项压测:先单项(cpu/memory/disk/memnative)、综合(full)最后
|
||||
4. 温度监控收尾(`===MONITOR:temp===`)
|
||||
5. 内核日志(`===MONITOR:dmesg===`,`-T` 带时间戳)+ 结束标记
|
||||
|
||||
## 容器运行时挂载(由 auto-check 自动添加)
|
||||
|
||||
| 挂载 | 作用 |
|
||||
|------|------|
|
||||
| `-v /sys:/sys:ro` | 容器内是独立 sysfs,挂载后才能读到宿主机硬件传感器(温度采集依赖此项) |
|
||||
| `-v /etc/localtime:/etc/localtime:ro` | 容器时区与宿主机一致,dmesg 时间戳与采样时间才准确 |
|
||||
|
||||
> 注意:镜像内已设置 `LC_ALL=C.UTF-8`。若 locale 为纯 C,`sensors` 会输出 "+51.0 C"(无 ° 符号),导致温度正则失配、采集不到数据。
|
||||
|
||||
## 镜像内容
|
||||
|
||||
```
|
||||
/stress-test/
|
||||
├── Dockerfile # 镜像定义
|
||||
├── scripts/ # 测试脚本
|
||||
│ ├── run.sh # 入口脚本
|
||||
│ ├── cpu.sh # CPU 压测
|
||||
│ ├── memory.sh # 内存压测
|
||||
│ ├── disk.sh # 磁盘压测
|
||||
│ ├── full.sh # 综合压测
|
||||
│ ├── memnative.sh # 内存精压
|
||||
│ ├── sysinfo.sh # 系统信息
|
||||
│ ├── temp_start.sh # 温度监控启动
|
||||
│ ├── temp_end.sh # 温度监控收尾
|
||||
│ └── dmesg.sh # 内核日志
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 依赖工具
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| `stress-ng` | CPU/内存/磁盘压测 |
|
||||
| `fio` | 磁盘 IO 测试 |
|
||||
| `lm-sensors` | 温度监控 |
|
||||
| `sysstat` | 系统性能统计 |
|
||||
| `procps` | 进程监控 |
|
||||
81
auto-check/docker/stress-test/scripts/fnstable.sh
Normal file
81
auto-check/docker/stress-test/scripts/fnstable.sh
Normal file
@@ -0,0 +1,81 @@
|
||||
# fnstable.sh - fnscript 稳定性门禁(社区脚本 self_inspection.py,vendor 原样收录不改源码)
|
||||
#
|
||||
# 思路:先用社区脚本检测硬件真实稳定性(stress CPU 压测 + memtester 内存数据校验),
|
||||
# 通过后自有压测工具再跑一遍出报告;不通过则 run.sh 跳过自有压测。
|
||||
#
|
||||
# 适配注意(不改源码的代价,已逐一规避):
|
||||
# 1. self_inspection.py CLI 模式无论成败 exit code 均为 0,
|
||||
# 只能按 stdout 关键词判定结果("未发现问题" = 通过)。
|
||||
# 2. --memory 内含两处 y/n 交互确认(input()),非交互环境会 EOFError,
|
||||
# 用 printf 'y\ny\n' 管道喂入(两处确认都覆盖)。
|
||||
# 3. 脚本内部调 sudo memtester / stress,依赖已在镜像中预装(root 下 sudo 直接可用)。
|
||||
# 4. memtester 测试可用内存的 97%,单遍耗时 30-120 分钟(由内存大小决定),
|
||||
# 可用 AUTOCHECK_FN_MEMORY=0 关闭内存校验只做 CPU 门禁。
|
||||
#
|
||||
# 输出遵守 auto-check 结构化协议(===TEST:fnstable=== 块),
|
||||
# 并设置 GATE_PASSED 供 run.sh 决定是否继续自有压测。
|
||||
|
||||
echo "===TEST:fnstable==="
|
||||
|
||||
FN_SCRIPT="/vendor/fnscript/self_inspection.py"
|
||||
FN_DURATION="${AUTOCHECK_DURATION:-30}"
|
||||
FN_MEMORY="${AUTOCHECK_FN_MEMORY:-1}"
|
||||
|
||||
FN_ALL_PASSED=1
|
||||
|
||||
START_TIME=$(date +%s%N)
|
||||
|
||||
# ---- 1. CPU 稳定性(fnscript: stress --cpu 4 --timeout N)----
|
||||
set +e
|
||||
FN_CPU_OUT=$(python3 "$FN_SCRIPT" --cpu "$FN_DURATION" 2>&1)
|
||||
set -e
|
||||
if printf '%s' "$FN_CPU_OUT" | grep -q "未发现问题"; then
|
||||
FN_CPU_STATUS="pass"
|
||||
else
|
||||
FN_CPU_STATUS="fail"
|
||||
FN_ALL_PASSED=0
|
||||
fi
|
||||
|
||||
# ---- 2. 内存稳定性(fnscript: sudo memtester <可用内存97%> 1,数据校验)----
|
||||
FN_MEM_STATUS="skip"
|
||||
FN_MEM_NOTE="未启用(AUTOCHECK_FN_MEMORY=0)"
|
||||
if [ "$FN_MEMORY" = "1" ]; then
|
||||
FN_MEM_NOTE=""
|
||||
set +e
|
||||
FN_MEM_OUT=$(printf 'y\ny\n' | python3 "$FN_SCRIPT" --memory 2>&1)
|
||||
set -e
|
||||
if printf '%s' "$FN_MEM_OUT" | grep -q "未发现问题"; then
|
||||
FN_MEM_STATUS="pass"
|
||||
else
|
||||
FN_MEM_STATUS="fail"
|
||||
FN_ALL_PASSED=0
|
||||
fi
|
||||
fi
|
||||
|
||||
END_TIME=$(date +%s%N)
|
||||
DURATION_MS=$(( (END_TIME - START_TIME) / 1000000 ))
|
||||
|
||||
# ---- 3. 汇总结果(门禁语义:任一子项失败即整体失败)----
|
||||
if [ "$FN_ALL_PASSED" -eq 1 ]; then
|
||||
echo 'status:pass'
|
||||
else
|
||||
echo 'status:fail'
|
||||
fi
|
||||
echo "duration:${DURATION_MS}ms"
|
||||
|
||||
echo "output:[CPU] ${FN_CPU_STATUS} (stress ${FN_DURATION}s)"
|
||||
printf '%s\n' "$FN_CPU_OUT" | while IFS= read -r line; do
|
||||
[ -n "$line" ] && echo "output: $line"
|
||||
done
|
||||
if [ "$FN_MEM_STATUS" = "skip" ]; then
|
||||
echo "output:[MEM] skip - ${FN_MEM_NOTE}"
|
||||
else
|
||||
echo "output:[MEM] ${FN_MEM_STATUS} (memtester 可用内存97% 单遍校验)"
|
||||
printf '%s\n' "$FN_MEM_OUT" | while IFS= read -r line; do
|
||||
[ -n "$line" ] && echo "output: $line"
|
||||
done
|
||||
fi
|
||||
echo ''
|
||||
|
||||
# 供 run.sh 门禁判断(fnstable 失败 → 跳过自有压测)
|
||||
GATE_PASSED=$FN_ALL_PASSED
|
||||
@@ -28,7 +28,7 @@ source /scripts/temp_start.sh
|
||||
# 未知类型输出 skip,保证报告完整
|
||||
for T in $(echo "$AUTOCHECK_TYPES" | tr ',' ' '); do
|
||||
case "$T" in
|
||||
cpu|memory|disk|memnative|full) ;;
|
||||
cpu|memory|disk|memnative|full|fnstable) ;;
|
||||
*)
|
||||
echo "===TEST:$T==="
|
||||
echo "status:skip"
|
||||
@@ -38,16 +38,37 @@ for T in $(echo "$AUTOCHECK_TYPES" | tr ',' ' '); do
|
||||
esac
|
||||
done
|
||||
|
||||
# 3. 按固定顺序执行请求的测试(先单项、综合 full 最后)
|
||||
# 3. 稳定性门禁(fnscript 社区脚本先行,不通过则跳过自有压测)
|
||||
# fnstable.sh 内部设置 GATE_PASSED(1=通过 / 0=失败)
|
||||
GATE_PASSED=1
|
||||
REQ_TYPES=",${AUTOCHECK_TYPES// /},"
|
||||
for TYPE in cpu memory disk memnative full; do
|
||||
case "$REQ_TYPES" in
|
||||
*",$TYPE,"*) source "/scripts/$TYPE.sh" ;;
|
||||
esac
|
||||
done
|
||||
case "$REQ_TYPES" in
|
||||
*,fnstable,*) source /scripts/fnstable.sh ;;
|
||||
esac
|
||||
|
||||
# 4. 温度监控收尾
|
||||
# 4. 按固定顺序执行请求的自有测试(先单项、综合 full 最后)
|
||||
# 门禁未通过 → 硬件已有问题,自有压测全部 skip(报告仍完整输出)
|
||||
if [ "$GATE_PASSED" -eq 1 ]; then
|
||||
for TYPE in cpu memory disk memnative full; do
|
||||
case "$REQ_TYPES" in
|
||||
*",$TYPE,"*) source "/scripts/$TYPE.sh" ;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
for TYPE in cpu memory disk memnative full; do
|
||||
case "$REQ_TYPES" in
|
||||
*",$TYPE,"*)
|
||||
echo "===TEST:$TYPE==="
|
||||
echo "status:skip"
|
||||
echo "output:fnscript 稳定性门禁未通过,跳过自有压测"
|
||||
echo ""
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
# 5. 温度监控收尾
|
||||
source /scripts/temp_end.sh
|
||||
|
||||
# 5. 内核日志 + 结束标记
|
||||
# 6. 内核日志 + 结束标记
|
||||
source /scripts/dmesg.sh
|
||||
|
||||
21
auto-check/docker/stress-test/vendor/fnscript/LICENSE
vendored
Normal file
21
auto-check/docker/stress-test/vendor/fnscript/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 又菜又爱玩的小朱猪
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
54
auto-check/docker/stress-test/vendor/fnscript/README.md
vendored
Normal file
54
auto-check/docker/stress-test/vendor/fnscript/README.md
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# 2.0版本重磅来袭
|
||||
### ✅脚本菜单改进
|
||||
 \
|
||||

|
||||
### 🖱TUI交互式界面,感觉就像在使用Windows
|
||||

|
||||
### 终端绑定功能,进入root自动运行脚本
|
||||

|
||||
|
||||
### ⬇git下载和更新
|
||||
✅ 操作简单,维护更新方便,采用Gitee国内源,确保全国地区均可访问
|
||||
### 📄独立脚本文件
|
||||
✅继续沿用1.0脚本分开管理,便于维护
|
||||
### MIT最宽松的开源协议
|
||||
|
||||
|
||||
# 如何使用
|
||||
## TUI交互式运行
|
||||
所有脚本目前均已实现图形化TUI交互式操作,您可以直接使用一键使用命令后根据菜单提示选择对应功能,默认使用的是TUI。由于脚本TUI采用的是 `textual` 库,这个库无法通过apt进行安装和管理,所以必须强行通过pip来进行安装(曾考虑过进行打包后执行但部分脚本可能存在打包后运行问题)
|
||||
[点击这里查阅详细冲突风险以及解决方案](https://github.com/yxsj245/fnscript/blob/2.0/pip%E5%BA%93%E5%86%B2%E7%AA%81%E9%A3%8E%E9%99%A9.md)
|
||||
### 使用运行命令
|
||||
```bash
|
||||
git clone https://gitee.com/xiao-zhu245/fnscript.git
|
||||
cd fnscript/
|
||||
python3 menu.py
|
||||
```
|
||||
### 建议使用 1panel面板终端或者CMD终端进行TUI操作,其它软件的终端可能存在兼容问题。使用脚本建议进入root用户下运行。
|
||||
|
||||
## 传统命令行运行
|
||||
若您担心pip库冲突风险或无法使用交互式终端情况下,我将部分功能单一的脚本增加了传统通过命令行形式运行的功能,在无需安装`textual`库就可以直接运行使用
|
||||
### 如何使用
|
||||
您可以通过此命令拉取任意脚本后直接使用
|
||||
```bash
|
||||
python3 此处替换脚本名称 -h #获取参数信息
|
||||
```
|
||||
# 脚本信息
|
||||
|
||||
| 脚本名称 | 作用 | 是否支持TUI | 是否支持命令行 |
|
||||
| ------- | ------- | ------- | ------- |
|
||||
| cdmount.py | 挂载CD/DVD |✅|❎|
|
||||
| filestorage.py | 分析目录文件大小 |✅|❎|
|
||||
| Powermanagement.py | 电源管理 |✅|✅|
|
||||
| qcowtools.py | qcow2转换工具 |✅|✅|
|
||||
| self_inspection.py | 硬件压测 |✅|✅|
|
||||
| startIOMMU.py | 开启IOMMU直通 |✅|✅|
|
||||
| swapinfo.py | 管理swap |✅|✅|
|
||||
| vmtoolses.py | 安装虚拟机工具 |✅|✅|
|
||||
| WOLstart.py | 开启WOL网络唤醒 |✅|✅|
|
||||
| network_diagnostic_tool.py | 网络诊断与修改 |✅|✅|
|
||||
| raid_repair_tui.py | 阵列状态和修复 |✅|❎|
|
||||
| ffmpeg_converter_tui.py | 影音万能格式转换 |✅|❎|
|
||||
|
||||
# 其它合作脚本
|
||||
[七月七夕纯命令行传统脚本](https://github.com/qiyueqixi/fnos)
|
||||
757
auto-check/docker/stress-test/vendor/fnscript/script/self_inspection.py
vendored
Normal file
757
auto-check/docker/stress-test/vendor/fnscript/script/self_inspection.py
vendored
Normal file
@@ -0,0 +1,757 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import subprocess
|
||||
import psutil
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
# 尝试导入textual库,如果不存在则设置标志
|
||||
HAS_TEXTUAL = True
|
||||
try:
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Header, Footer, Static, Button, Label, Input
|
||||
from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.screen import Screen, ModalScreen
|
||||
import asyncio
|
||||
except ImportError:
|
||||
HAS_TEXTUAL = False
|
||||
# 如果没有textual但尝试无参数运行,我们需要提示用户
|
||||
if len(sys.argv) == 1:
|
||||
print("错误: 未安装textual库,无法启动图形界面。")
|
||||
print("您可以通过命令行参数使用此脚本的核心功能:")
|
||||
print(" 内存测试: python self_inspection.py --memory")
|
||||
print(" CPU测试: python self_inspection.py --cpu [测试时长(秒)]")
|
||||
print(" 查看帮助: python self_inspection.py -h")
|
||||
sys.exit(1)
|
||||
|
||||
# 定义颜色常量(保留以兼容原始代码)
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
BLUE = "\033[34m"
|
||||
MAGENTA = "\033[35m"
|
||||
CYAN = "\033[36m"
|
||||
WHITE = "\033[37m"
|
||||
|
||||
# 全局变量存储当前运行的进程
|
||||
current_process = None
|
||||
|
||||
# 工具函数
|
||||
def clear_screen():
|
||||
"""清空终端屏幕"""
|
||||
os.system('clear' if os.name == 'posix' else 'cls')
|
||||
|
||||
def get_test_memory():
|
||||
"""计算可用于测试的内存 (剩余内存的 97%)"""
|
||||
available_memory = psutil.virtual_memory().available // (1024 * 1024) # 获取可用内存 (MB)
|
||||
return int(available_memory * 0.97) # 取 97% 进行测试
|
||||
|
||||
def check_memory_usage():
|
||||
"""检查当前内存使用情况"""
|
||||
return psutil.virtual_memory().percent
|
||||
|
||||
def run_command(command):
|
||||
"""执行命令并返回结果"""
|
||||
global current_process
|
||||
|
||||
result = {
|
||||
'status_code': 0,
|
||||
'stdout': '',
|
||||
'stderr': ''
|
||||
}
|
||||
|
||||
try:
|
||||
# 保存进程引用以便后续可以终止
|
||||
current_process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# 等待进程完成
|
||||
stdout, stderr = current_process.communicate()
|
||||
|
||||
# 获取返回码
|
||||
result['status_code'] = current_process.returncode
|
||||
result['stdout'] = stdout
|
||||
result['stderr'] = stderr
|
||||
|
||||
# 清除当前进程引用
|
||||
current_process = None
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
if current_process:
|
||||
try:
|
||||
current_process.terminate()
|
||||
except:
|
||||
pass
|
||||
current_process = None
|
||||
|
||||
result['status_code'] = 1
|
||||
result['stderr'] = str(e)
|
||||
return result
|
||||
|
||||
def stop_current_process():
|
||||
"""停止当前正在运行的进程"""
|
||||
global current_process
|
||||
if current_process:
|
||||
try:
|
||||
current_process.terminate()
|
||||
# 给进程一点时间来终止
|
||||
time.sleep(0.5)
|
||||
# 如果进程还在运行,强制结束
|
||||
if current_process.poll() is None:
|
||||
current_process.kill()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
finally:
|
||||
current_process = None
|
||||
return False
|
||||
|
||||
# 命令行直接执行内存测试的函数
|
||||
def cli_memory_test():
|
||||
"""通过命令行直接执行内存测试"""
|
||||
memory_usage = check_memory_usage()
|
||||
|
||||
if memory_usage > 80:
|
||||
print(f"警告: 当前系统内存使用率已达 {memory_usage}%!")
|
||||
print("建议关闭一些软件后再运行,继续运行可能检测效果不够准确")
|
||||
print("是否继续? (y/n)")
|
||||
response = input().strip().lower()
|
||||
if response != 'y':
|
||||
print("操作已取消")
|
||||
return False
|
||||
|
||||
print("建议关闭所有docker容器以及停用所有应用程序以保证检测的准确性。")
|
||||
print("是否继续? (y/n)")
|
||||
response = input().strip().lower()
|
||||
if response != 'y':
|
||||
print("操作已取消")
|
||||
return False
|
||||
|
||||
mem_size = f"{get_test_memory()}M"
|
||||
print("正在运行内存测试,请勿进行其它操作,如果运行过程中出现死机重启则大概率为内存问题。")
|
||||
print("整个测试过程预计 30-120 分钟 具体根据内存大小,如果需要终止测试请按Ctrl+C。")
|
||||
|
||||
try:
|
||||
# 设置信号处理器以便可以通过Ctrl+C终止测试
|
||||
def signal_handler(sig, frame):
|
||||
print("\n测试已被用户终止")
|
||||
stop_current_process()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# 执行测试
|
||||
result = run_command(["sudo", "memtester", mem_size, "1"])
|
||||
|
||||
if result['status_code'] == 0:
|
||||
print("内存测试完成!未发现问题。")
|
||||
return True
|
||||
else:
|
||||
print(f"内存测试失败!错误信息:{result['stderr']}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"测试过程中发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
# 命令行直接执行CPU测试的函数
|
||||
def cli_cpu_test(duration=60):
|
||||
"""通过命令行直接执行CPU测试
|
||||
|
||||
Args:
|
||||
duration (int): 测试持续时间(秒),默认为60秒
|
||||
"""
|
||||
try:
|
||||
duration = int(duration)
|
||||
if duration <= 0:
|
||||
print("错误: 测试时长必须大于0")
|
||||
return False
|
||||
except ValueError:
|
||||
print("错误: 测试时长必须是一个有效的整数")
|
||||
return False
|
||||
|
||||
print(f"正在进行 CPU 压测,持续 {duration} 秒...如需提前结束测试,请按Ctrl+C。")
|
||||
|
||||
try:
|
||||
# 设置信号处理器以便可以通过Ctrl+C终止测试
|
||||
def signal_handler(sig, frame):
|
||||
print("\n测试已被用户终止")
|
||||
stop_current_process()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# 执行测试
|
||||
result = run_command(["stress", "--cpu", "4", "--timeout", str(duration)])
|
||||
|
||||
if result['status_code'] == 0:
|
||||
print("CPU 压测完成!未发现问题。")
|
||||
return True
|
||||
else:
|
||||
print(f"CPU 压测失败!错误信息:{result['stderr']}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"测试过程中发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def parse_arguments():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description='系统健康检测工具')
|
||||
parser.add_argument('--memory', action='store_true',
|
||||
help='执行内存测试')
|
||||
parser.add_argument('--cpu', nargs='?', const=60, type=int,
|
||||
help='执行CPU压测,可选参数为测试时长(秒),默认为60秒')
|
||||
return parser.parse_args()
|
||||
|
||||
# 只有在导入了textual库的情况下才定义这些类
|
||||
if HAS_TEXTUAL:
|
||||
# 确认对话框
|
||||
class ConfirmDialog(ModalScreen):
|
||||
"""确认对话框"""
|
||||
|
||||
def __init__(self, message: str, action_callback=None):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
self.action_callback = action_callback
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="dialog-container"):
|
||||
yield Label(f"[yellow]{self.message}[/yellow]", id="dialog-title")
|
||||
with Horizontal(id="dialog-buttons"):
|
||||
yield Button("确认", id="confirm", variant="primary")
|
||||
yield Button("取消", id="cancel", variant="error")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "confirm" and self.action_callback:
|
||||
self.dismiss(True)
|
||||
self.action_callback()
|
||||
else:
|
||||
self.dismiss(False)
|
||||
|
||||
# 成功提示对话框
|
||||
class SuccessDialog(ModalScreen):
|
||||
"""成功提示对话框"""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="dialog-container"):
|
||||
yield Label(f"[green]{self.message}[/green]", id="dialog-title")
|
||||
with Horizontal(id="dialog-buttons"):
|
||||
yield Button("确定", id="ok", variant="primary")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.dismiss(True)
|
||||
|
||||
# 错误提示对话框
|
||||
class ErrorDialog(ModalScreen):
|
||||
"""错误提示对话框"""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="dialog-container"):
|
||||
yield Label(f"[red]{self.message}[/red]", id="dialog-title")
|
||||
with Horizontal(id="dialog-buttons"):
|
||||
yield Button("确定", id="ok", variant="primary")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.dismiss(True)
|
||||
|
||||
# 输入对话框
|
||||
class InputDialog(ModalScreen):
|
||||
"""输入对话框"""
|
||||
|
||||
def __init__(self, message: str, default_value: str = "", action_callback=None):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
self.default_value = default_value
|
||||
self.action_callback = action_callback
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="dialog-container"):
|
||||
yield Label(f"[blue]{self.message}[/blue]", id="dialog-title")
|
||||
yield Input(value=self.default_value, id="input-field")
|
||||
with Horizontal(id="dialog-buttons"):
|
||||
yield Button("确认", id="confirm", variant="primary")
|
||||
yield Button("取消", id="cancel", variant="error")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "confirm" and self.action_callback:
|
||||
input_value = self.query_one("#input-field", Input).value
|
||||
self.dismiss(input_value)
|
||||
self.action_callback(input_value)
|
||||
else:
|
||||
self.dismiss(None)
|
||||
|
||||
class SystemHealthApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
#title {
|
||||
dock: top;
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
background: $accent;
|
||||
color: $text;
|
||||
padding: 1;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
#menu-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 1;
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
margin: 1;
|
||||
min-width: 30;
|
||||
min-height: 3;
|
||||
background: $primary;
|
||||
color: $text;
|
||||
border: tall $primary-lighten-2;
|
||||
}
|
||||
|
||||
#memory-test {
|
||||
background: $success-darken-1;
|
||||
color: $text;
|
||||
border: tall $success;
|
||||
}
|
||||
|
||||
#cpu-test {
|
||||
background: $warning-darken-1;
|
||||
color: $text;
|
||||
border: tall $warning;
|
||||
}
|
||||
|
||||
#stop-test {
|
||||
background: $error-darken-1;
|
||||
color: $text;
|
||||
border: tall $error;
|
||||
margin: 1;
|
||||
min-width: 30;
|
||||
min-height: 3;
|
||||
}
|
||||
|
||||
#status {
|
||||
height: auto;
|
||||
min-height: 3;
|
||||
margin: 1;
|
||||
padding: 1;
|
||||
border: solid $accent;
|
||||
background: $surface-darken-1;
|
||||
}
|
||||
|
||||
/* 对话框样式 */
|
||||
#dialog-container {
|
||||
width: 60%;
|
||||
height: auto;
|
||||
padding: 2;
|
||||
background: $surface;
|
||||
border: thick $accent;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
/* 成功对话框样式 */
|
||||
SuccessDialog #dialog-container {
|
||||
border: thick $success;
|
||||
}
|
||||
|
||||
/* 错误对话框样式 */
|
||||
ErrorDialog #dialog-container {
|
||||
border: thick $error;
|
||||
}
|
||||
|
||||
/* 确认对话框样式 */
|
||||
ConfirmDialog #dialog-container {
|
||||
border: thick $warning;
|
||||
}
|
||||
|
||||
/* 输入对话框样式 */
|
||||
InputDialog #dialog-container {
|
||||
border: thick $primary;
|
||||
}
|
||||
|
||||
#dialog-title {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
|
||||
#input-field {
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#dialog-buttons {
|
||||
align: center middle;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#dialog-buttons Button {
|
||||
margin: 0 2;
|
||||
min-width: 10;
|
||||
}
|
||||
|
||||
/* 按钮样式增强 */
|
||||
Button {
|
||||
background: $primary;
|
||||
border: tall $primary-lighten-2;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
background: $primary-lighten-1;
|
||||
}
|
||||
|
||||
Button#confirm {
|
||||
background: $success-darken-1;
|
||||
border: tall $success;
|
||||
}
|
||||
|
||||
Button#confirm:hover {
|
||||
background: $success;
|
||||
}
|
||||
|
||||
Button#cancel, Button#stop-test {
|
||||
background: $error-darken-1;
|
||||
border: tall $error;
|
||||
}
|
||||
|
||||
Button#cancel:hover, Button#stop-test:hover {
|
||||
background: $error;
|
||||
}
|
||||
|
||||
Button#ok {
|
||||
background: $primary-darken-1;
|
||||
border: tall $primary;
|
||||
}
|
||||
|
||||
Button#ok:hover {
|
||||
background: $primary;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("q", "quit", "退出"),
|
||||
("escape", "quit", "退出"),
|
||||
("ctrl+c", "stop_test", "停止测试"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_testing = False
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
yield Footer()
|
||||
|
||||
with Container(id="main-container"):
|
||||
yield Label("[b]系统健康检测工具[/b]", id="title")
|
||||
|
||||
with Container(id="menu-container"):
|
||||
yield Button("内存检测 (使用 memtester)", id="memory-test", classes="menu-button")
|
||||
yield Button("CPU 压测", id="cpu-test", classes="menu-button")
|
||||
yield Button("停止当前测试", id="stop-test", disabled=True)
|
||||
|
||||
# 状态输出
|
||||
yield Static("准备就绪,请选择检测项目。", id="status")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""按钮点击事件处理"""
|
||||
button_id = event.button.id
|
||||
|
||||
if button_id == "memory-test":
|
||||
self.start_memory_test()
|
||||
elif button_id == "cpu-test":
|
||||
self.start_cpu_test()
|
||||
elif button_id == "stop-test":
|
||||
self.stop_test()
|
||||
|
||||
def action_stop_test(self) -> None:
|
||||
"""键盘快捷键停止测试"""
|
||||
self.stop_test()
|
||||
|
||||
def stop_test(self) -> None:
|
||||
"""停止当前测试"""
|
||||
if not self.is_testing:
|
||||
return
|
||||
|
||||
status = self.query_one("#status")
|
||||
status.update("[yellow]正在停止测试...[/yellow]")
|
||||
|
||||
# 停止当前进程
|
||||
if stop_current_process():
|
||||
status.update("[green]测试已停止。[/green]")
|
||||
self.show_success_dialog("测试已成功停止。")
|
||||
else:
|
||||
status.update("[red]无法停止测试,可能测试已经完成。[/red]")
|
||||
|
||||
# 更新界面状态
|
||||
self.is_testing = False
|
||||
|
||||
# 使用try/except捕获可能的NoMatches异常
|
||||
try:
|
||||
stop_button = self.query_one("#stop-test", Button)
|
||||
if stop_button:
|
||||
stop_button.disabled = True
|
||||
|
||||
memory_button = self.query_one("#memory-test", Button)
|
||||
if memory_button:
|
||||
memory_button.disabled = False
|
||||
|
||||
cpu_button = self.query_one("#cpu-test", Button)
|
||||
if cpu_button:
|
||||
cpu_button.disabled = False
|
||||
except Exception as e:
|
||||
# 记录错误但不中断程序
|
||||
print(f"更新按钮状态时出错: {str(e)}")
|
||||
|
||||
def start_memory_test(self) -> None:
|
||||
"""开始内存测试"""
|
||||
status = self.query_one("#status")
|
||||
memory_usage = check_memory_usage()
|
||||
|
||||
if memory_usage > 80:
|
||||
warning_message = f"警告: 当前系统内存使用率已达 {memory_usage}%!\n建议关闭一些软件后再运行,继续运行可能检测效果不够准确"
|
||||
status.update(f"[yellow]{warning_message}[/yellow]")
|
||||
|
||||
# 显示确认对话框
|
||||
def confirm_callback():
|
||||
self.show_memory_test_warning()
|
||||
|
||||
self.push_screen(ConfirmDialog(warning_message, confirm_callback))
|
||||
else:
|
||||
self.show_memory_test_warning()
|
||||
|
||||
def show_memory_test_warning(self):
|
||||
"""显示内存测试警告"""
|
||||
warning = "建议关闭所有docker容器以及停用所有应用程序以保证检测的准确性。"
|
||||
status = self.query_one("#status")
|
||||
status.update(f"[red]{warning}[/red]")
|
||||
|
||||
def confirm_callback():
|
||||
self.run_memory_test()
|
||||
|
||||
self.push_screen(ConfirmDialog(warning, confirm_callback))
|
||||
|
||||
def run_memory_test(self) -> None:
|
||||
"""执行内存测试"""
|
||||
status = self.query_one("#status")
|
||||
mem_size = f"{get_test_memory()}M"
|
||||
|
||||
message = "正在运行内存测试,请勿进行其它操作,如果运行过程中出现死机重启则大概率为内存问题。整个测试过程预计 30-120 分钟 具体根据内存大小,如果需要终止测试请按Ctrl+C或点击停止按钮。"
|
||||
status.update(f"[blue]{message}[/blue]")
|
||||
|
||||
# 更新界面状态
|
||||
self.is_testing = True
|
||||
|
||||
# 使用try/except捕获可能的NoMatches异常
|
||||
try:
|
||||
stop_button = self.query_one("#stop-test", Button)
|
||||
if stop_button:
|
||||
stop_button.disabled = False
|
||||
|
||||
memory_button = self.query_one("#memory-test", Button)
|
||||
if memory_button:
|
||||
memory_button.disabled = True
|
||||
|
||||
cpu_button = self.query_one("#cpu-test", Button)
|
||||
if cpu_button:
|
||||
cpu_button.disabled = True
|
||||
except Exception as e:
|
||||
# 记录错误但不中断程序
|
||||
print(f"更新按钮状态时出错: {str(e)}")
|
||||
|
||||
# 创建异步任务
|
||||
asyncio.create_task(self.do_memory_test(mem_size))
|
||||
|
||||
async def do_memory_test(self, mem_size):
|
||||
"""异步执行内存测试"""
|
||||
status = self.query_one("#status")
|
||||
|
||||
try:
|
||||
# 在异步中执行测试
|
||||
result = await asyncio.to_thread(run_command, ["sudo", "memtester", mem_size, "1"])
|
||||
|
||||
if result['status_code'] == 0:
|
||||
success_message = "内存测试完成!未发现问题。"
|
||||
status.update(f"[green]{success_message}[/green]")
|
||||
self.show_success_dialog(success_message)
|
||||
else:
|
||||
# 检查是否是被用户手动终止的
|
||||
if current_process is None and not result['stderr']:
|
||||
status.update("[yellow]测试已被用户终止。[/yellow]")
|
||||
else:
|
||||
error_message = f"内存测试失败!错误信息:{result['stderr']}"
|
||||
status.update(f"[red]{error_message}[/red]")
|
||||
self.show_error_dialog(error_message)
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"测试过程中发生错误: {str(e)}"
|
||||
status.update(f"[red]{error_message}[/red]")
|
||||
self.show_error_dialog(error_message)
|
||||
finally:
|
||||
# 恢复界面状态
|
||||
self.is_testing = False
|
||||
|
||||
# 使用try/except捕获可能的NoMatches异常
|
||||
try:
|
||||
stop_button = self.query_one("#stop-test", Button)
|
||||
if stop_button:
|
||||
stop_button.disabled = True
|
||||
|
||||
memory_button = self.query_one("#memory-test", Button)
|
||||
if memory_button:
|
||||
memory_button.disabled = False
|
||||
|
||||
cpu_button = self.query_one("#cpu-test", Button)
|
||||
if cpu_button:
|
||||
cpu_button.disabled = False
|
||||
except Exception as e:
|
||||
# 记录错误但不中断程序
|
||||
print(f"更新按钮状态时出错: {str(e)}")
|
||||
|
||||
def start_cpu_test(self) -> None:
|
||||
"""开始CPU测试"""
|
||||
def input_callback(duration):
|
||||
if duration and duration.isdigit() and int(duration) > 0:
|
||||
self.run_cpu_test(duration)
|
||||
else:
|
||||
self.show_error_dialog("请输入有效的时间(秒)!")
|
||||
|
||||
self.push_screen(InputDialog("请输入 CPU 压测时长 (秒):", "60", input_callback))
|
||||
|
||||
def run_cpu_test(self, duration):
|
||||
"""执行CPU测试"""
|
||||
status = self.query_one("#status")
|
||||
|
||||
message = f"正在进行 CPU 压测,持续 {duration} 秒...如需提前结束测试,请按Ctrl+C或点击停止按钮。"
|
||||
status.update(f"[blue]{message}[/blue]")
|
||||
|
||||
# 更新界面状态
|
||||
self.is_testing = True
|
||||
|
||||
# 使用try/except捕获可能的NoMatches异常
|
||||
try:
|
||||
stop_button = self.query_one("#stop-test", Button)
|
||||
if stop_button:
|
||||
stop_button.disabled = False
|
||||
|
||||
memory_button = self.query_one("#memory-test", Button)
|
||||
if memory_button:
|
||||
memory_button.disabled = True
|
||||
|
||||
cpu_button = self.query_one("#cpu-test", Button)
|
||||
if cpu_button:
|
||||
cpu_button.disabled = True
|
||||
except Exception as e:
|
||||
# 记录错误但不中断程序
|
||||
print(f"更新按钮状态时出错: {str(e)}")
|
||||
|
||||
# 创建异步任务
|
||||
asyncio.create_task(self.do_cpu_test(duration))
|
||||
|
||||
async def do_cpu_test(self, duration):
|
||||
"""异步执行CPU测试"""
|
||||
status = self.query_one("#status")
|
||||
|
||||
try:
|
||||
# 在异步中执行测试
|
||||
result = await asyncio.to_thread(run_command, ["stress", "--cpu", "4", "--timeout", duration])
|
||||
|
||||
if result['status_code'] == 0:
|
||||
success_message = "CPU 压测完成!未发现问题。"
|
||||
status.update(f"[green]{success_message}[/green]")
|
||||
self.show_success_dialog(success_message)
|
||||
else:
|
||||
# 检查是否是被用户手动终止的
|
||||
if current_process is None and not result['stderr']:
|
||||
status.update("[yellow]测试已被用户终止。[/yellow]")
|
||||
else:
|
||||
error_message = f"CPU 压测失败!错误信息:{result['stderr']}"
|
||||
status.update(f"[red]{error_message}[/red]")
|
||||
self.show_error_dialog(error_message)
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"测试过程中发生错误: {str(e)}"
|
||||
status.update(f"[red]{error_message}[/red]")
|
||||
self.show_error_dialog(error_message)
|
||||
finally:
|
||||
# 恢复界面状态
|
||||
self.is_testing = False
|
||||
|
||||
# 使用try/except捕获可能的NoMatches异常
|
||||
try:
|
||||
stop_button = self.query_one("#stop-test", Button)
|
||||
if stop_button:
|
||||
stop_button.disabled = True
|
||||
|
||||
memory_button = self.query_one("#memory-test", Button)
|
||||
if memory_button:
|
||||
memory_button.disabled = False
|
||||
|
||||
cpu_button = self.query_one("#cpu-test", Button)
|
||||
if cpu_button:
|
||||
cpu_button.disabled = False
|
||||
except Exception as e:
|
||||
# 记录错误但不中断程序
|
||||
print(f"更新按钮状态时出错: {str(e)}")
|
||||
|
||||
def show_success_dialog(self, message: str) -> None:
|
||||
"""显示成功对话框"""
|
||||
self.push_screen(SuccessDialog(message))
|
||||
|
||||
def show_error_dialog(self, message: str) -> None:
|
||||
"""显示错误对话框"""
|
||||
self.push_screen(ErrorDialog(message))
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_arguments()
|
||||
|
||||
# 如果指定了命令行参数,则直接执行相应功能
|
||||
if args.memory:
|
||||
cli_memory_test()
|
||||
sys.exit(0)
|
||||
|
||||
if args.cpu is not None:
|
||||
cli_cpu_test(args.cpu)
|
||||
sys.exit(0)
|
||||
|
||||
# 如果没有指定命令行参数,则启动图形界面(如果textual可用)
|
||||
if HAS_TEXTUAL:
|
||||
app = SystemHealthApp()
|
||||
app.run()
|
||||
else:
|
||||
# 这种情况不应该发生,因为在导入时已经处理了,但为了代码完整性保留
|
||||
print("错误: 未安装textual库,无法启动图形界面。")
|
||||
print("您可以通过命令行参数使用此脚本的核心功能:")
|
||||
print(" 内存测试: python self_inspection.py --memory")
|
||||
print(" CPU测试: python self_inspection.py --cpu [测试时长(秒)]")
|
||||
print(" 查看帮助: python self_inspection.py -h")
|
||||
sys.exit(1)
|
||||
@@ -51,6 +51,7 @@ type StressConfig struct {
|
||||
DiskDir string `yaml:"disk_dir"` // 磁盘压测目录,空=自动临时目录
|
||||
TempInterval time.Duration `yaml:"temp_interval"` // 温度采样间隔,0=默认10s
|
||||
TempLimit float64 `yaml:"temp_limit"` // 温度上限(°C),0=不判定
|
||||
FnMemory bool `yaml:"fn_memory"` // fnstable 门禁的 memtester 内存校验(30-120 分钟)
|
||||
// 压测统一在 Docker 容器中执行(工具与脚本预装在镜像里)
|
||||
DockerImage string `yaml:"docker_image"` // 压测镜像名称,如 stress-test:latest
|
||||
DockerTar string `yaml:"docker_tar"` // 本地导出的镜像文件(docker save),目标机无镜像时上传并 load
|
||||
@@ -110,6 +111,7 @@ func Default() *Config {
|
||||
DiskSizeMB: 1024,
|
||||
TempInterval: 10 * time.Second,
|
||||
TempLimit: 90,
|
||||
FnMemory: true,
|
||||
DockerImage: "stress-test:latest",
|
||||
DockerTar: "docker/stress-test/stress-test.tar",
|
||||
DockerArgs: "--network=host --privileged",
|
||||
|
||||
@@ -46,6 +46,20 @@ func Connect(ip string, cfg *Config) (*ssh.Client, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SSH 连接 %s 失败: %w", addr, err)
|
||||
}
|
||||
|
||||
// KeepAlive:长时压测(如 fnstable 门禁的 memtester 可达 30-120 分钟)期间
|
||||
// 连接上无数据流动,定期发 keepalive 请求防止 NAT/防火墙掐断空闲 TCP 连接。
|
||||
// 连接关闭后 SendRequest 返回错误,goroutine 自行退出。
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if _, _, err := client.SendRequest("keepalive@golang.org", true, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ func BuildMetrics(types []TestType) []Metric {
|
||||
m.Tool = "stressapptest"
|
||||
case TestFull:
|
||||
m.Tool = "stress-ng"
|
||||
case TestFnStable:
|
||||
m.Tool = "fnscript"
|
||||
default:
|
||||
m.Enabled = false
|
||||
m.DisableReason = "未知测试类型"
|
||||
@@ -64,6 +66,7 @@ type Config struct {
|
||||
DiskDir string // 空=自动临时目录
|
||||
TempLogInt time.Duration
|
||||
TempLimit float64 // 温度上限(°C),0=不判定
|
||||
FnMemory bool // fnstable 门禁是否执行 memtester 内存校验(30-120 分钟)
|
||||
// 压测统一在 Docker 容器中执行(工具预装在镜像里)
|
||||
DockerImage string // 压测镜像名称,如 stress-test:20260824-153045(由 build.sh 自动维护)
|
||||
DockerTar string // 本地导出的镜像文件(docker save),目标机无镜像时上传并 load
|
||||
|
||||
@@ -304,6 +304,7 @@ func (r *Runner) runInDocker() (string, error) {
|
||||
"-e AUTOCHECK_DISK_DIR=%s "+
|
||||
"-e AUTOCHECK_TEMP_INTERVAL=%d "+
|
||||
"-e AUTOCHECK_SAMPLE_INTERVAL=2 "+
|
||||
"-e AUTOCHECK_FN_MEMORY=%d "+
|
||||
"%s %s",
|
||||
containerName,
|
||||
r.cfg.Types,
|
||||
@@ -313,6 +314,7 @@ func (r *Runner) runInDocker() (string, error) {
|
||||
r.cfg.DiskSizeMB,
|
||||
r.cfg.DiskDir,
|
||||
int(r.cfg.TempLogInt.Seconds()),
|
||||
boolToInt(r.cfg.FnMemory),
|
||||
r.cfg.DockerArgs,
|
||||
r.cfg.DockerImage)
|
||||
|
||||
@@ -403,6 +405,7 @@ func TestDevice(ip string, cfg config.Config) *Report {
|
||||
stressCfg.DiskDir = cfg.Stress.DiskDir
|
||||
stressCfg.TempLogInt = cfg.Stress.TempInterval
|
||||
stressCfg.TempLimit = cfg.Stress.TempLimit
|
||||
stressCfg.FnMemory = cfg.Stress.FnMemory
|
||||
// Docker 镜像配置(工具与脚本预装在镜像中,本地导出的 tar 上传到目标机后 load)
|
||||
stressCfg.DockerImage = cfg.Stress.DockerImage
|
||||
stressCfg.DockerTar = cfg.Stress.DockerTar
|
||||
@@ -453,6 +456,14 @@ func queryRemoteMAC(client *ssh.Client) string {
|
||||
// 工具函数
|
||||
// ============================
|
||||
|
||||
// boolToInt bool → 0/1(Docker 环境变量传参用)
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// IsPassed 报告是否通过
|
||||
func IsPassed(rpt *Report) bool {
|
||||
return rpt != nil && rpt.Failed == 0 && rpt.Errors == 0
|
||||
|
||||
@@ -15,6 +15,7 @@ const (
|
||||
TestDiskIO TestType = "disk"
|
||||
TestMemNative TestType = "memnative"
|
||||
TestFull TestType = "full"
|
||||
TestFnStable TestType = "fnstable" // fnscript 稳定性门禁(社区脚本先行,通过后自有压测再跑)
|
||||
MonitorTemp TestType = "temp"
|
||||
MonitorDmesg TestType = "dmesg"
|
||||
)
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
[配置] 加载完成: 网段=10.0.3.0/24 间隔=3m0s 压测=cpu,memory,disk,full
|
||||
══ 工作流启动(间隔 3m0s,设备以 IP 为 key)══
|
||||
|
||||
════ 轮次开始 [01:18:28] ════
|
||||
[扫描] 网段 10.0.3.0/24,共 254 个IP,10 并发 ping 探测...
|
||||
[扫描] 完成,发现 13 台存活设备
|
||||
[扫描] 设备 13 台
|
||||
[测试] 10.0.3.171 () 开始压测...
|
||||
[测试] 10.0.3.171 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.171:22 失败: dial tcp 10.0.3.171:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.175 () 开始压测...
|
||||
[测试] 10.0.3.175 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.175:22 失败: dial tcp 10.0.3.175:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.236 () 开始压测...
|
||||
[测试] 10.0.3.236 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.236:22 失败: dial tcp 10.0.3.236:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.4 () 开始压测...
|
||||
[测试] 10.0.3.4 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.4:22 失败: dial tcp 10.0.3.4:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.1 () 开始压测...
|
||||
[测试] 10.0.3.1 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.1:22 失败: dial tcp 10.0.3.1:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.104 () 开始压测...
|
||||
[测试] 10.0.3.104 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.104:22 失败: dial tcp 10.0.3.104:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.123 () 开始压测...
|
||||
[测试] 10.0.3.123 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.123:22 失败: dial tcp 10.0.3.123:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.119 () 开始压测...
|
||||
[测试] 10.0.3.119 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.119:22 失败: dial tcp 10.0.3.119:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.147 () 开始压测...
|
||||
[测试] 10.0.3.147 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.147:22 失败: dial tcp 10.0.3.147:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.148 () 开始压测...
|
||||
[MAC] 10.0.3.148 -> fe:49:6a:ab:e1:79
|
||||
|
||||
════════ [10.0.3.148] 压力测试开始 ════════
|
||||
工具: stress-ng(stress-ng, version 0.15.06 (gcc 12.2.0, x86_64 Linux 6.18.18.c877-trim)) fio(fio-3.33)
|
||||
指标: cpu memory disk full dmesg
|
||||
[脚本] 拼装完成,10334 字节
|
||||
[报告] reports/report-10.0.3.148.html
|
||||
|
||||
════════ [10.0.3.148] 压力测试完成 ════════
|
||||
══ [10.0.3.148] 压力测试报告 ══
|
||||
耗时: 2m23.341s
|
||||
|
||||
✓ cpu pass
|
||||
跑分: 1622.1 ops/s
|
||||
最高温度: 53.0°C
|
||||
平均CPU利用率: 98.4%
|
||||
stress-ng: info: [3783625] Working directory / is not read/writeable, some I/O tests may fail
|
||||
stress-ng: info: [3783625] setting to a 30 second run per stressor
|
||||
stress-ng: info: [3783625] dispatching hogs: 4 cpu
|
||||
stress-ng: metrc: [3783625] stressor bogo ops real time usr time sys time bogo ops/s bogo ops/s
|
||||
stress-ng: metrc: [3783625] (secs) (secs) (secs) (real time) (usr+sys time)
|
||||
stress-ng: metrc: [3783625] cpu 48668 30.00 113.93 0.32 1622.06 426.00
|
||||
stress-ng: info: [3783625] successful run completed in 30.02s
|
||||
✓ memory pass
|
||||
跑分: 44033.6 ops/s
|
||||
最高温度: 53.0°C
|
||||
峰值内存使用率: 52.5%
|
||||
stress-ng: info: [3783877] Working directory / is not read/writeable, some I/O tests may fail
|
||||
stress-ng: info: [3783877] setting to a 30 second run per stressor
|
||||
stress-ng: info: [3783877] dispatching hogs: 4 vm
|
||||
stress-ng: metrc: [3783877] stressor bogo ops real time usr time sys time bogo ops/s bogo ops/s
|
||||
stress-ng: metrc: [3783877] (secs) (secs) (secs) (real time) (usr+sys time)
|
||||
stress-ng: metrc: [3783877] vm 1321192 30.00 96.84 5.67 44033.61 12888.25
|
||||
stress-ng: info: [3783877] successful run completed in 30.01s
|
||||
✓ disk pass
|
||||
跑分: 391.0 MB/s
|
||||
读IOPS: 67.0
|
||||
写IOPS: 29.0
|
||||
总IOPS: 96.0
|
||||
读吞吐: 271.0MB/s
|
||||
写吞吐: 120.0MB/s
|
||||
总吞吐: 391.0MB/s
|
||||
autocheck: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=32
|
||||
fio-3.33
|
||||
Starting 1 process
|
||||
autocheck: Laying out IO file (1 file / 1024MiB)
|
||||
note: both iodepth >= 1 and synchronous I/O engine are selected, queue depth will be capped at 1
|
||||
autocheck: (groupid=0, jobs=1): err= 0: pid=3784176: Fri Aug 14 01:21:03 2026
|
||||
read: IOPS=67, BW=271KiB/s (277kB/s)(8328KiB/30763msec)
|
||||
clat (usec): min=256, max=919497, avg=14116.10, stdev=41899.33
|
||||
lat (usec): min=257, max=919500, avg=14118.33, stdev=41899.33
|
||||
clat percentiles (msec):
|
||||
| 1.00th=[ 4], 5.00th=[ 5], 10.00th=[ 6], 20.00th=[ 7],
|
||||
| 30.00th=[ 8], 40.00th=[ 10], 50.00th=[ 11], 60.00th=[ 12],
|
||||
| 70.00th=[ 13], 80.00th=[ 14], 90.00th=[ 15], 95.00th=[ 18],
|
||||
| 99.00th=[ 65], 99.50th=[ 74], 99.90th=[ 869], 99.95th=[ 894],
|
||||
| 99.99th=[ 919]
|
||||
bw ( KiB/s): min= 84, max= 396, per=100.00%, avg=277.27, stdev=87.49, samples=30
|
||||
iops : min= 21, max= 99, avg=69.30, stdev=21.85, samples=30
|
||||
write: IOPS=29, BW=120KiB/s (123kB/s)(3684KiB/30763msec); 0 zone resets
|
||||
clat (usec): min=135, max=285109, avg=1405.37, stdev=9787.22
|
||||
lat (usec): min=136, max=285113, avg=1408.31, stdev=9787.24
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 212], 5.00th=[ 289], 10.00th=[ 338], 20.00th=[ 400],
|
||||
| 30.00th=[ 469], 40.00th=[ 537], 50.00th=[ 627], 60.00th=[ 758],
|
||||
| 70.00th=[ 881], 80.00th=[ 1037], 90.00th=[ 1270], 95.00th=[ 1549],
|
||||
| 99.00th=[ 17433], 99.50th=[ 22414], 99.90th=[283116], 99.95th=[283116],
|
||||
| 99.99th=[283116]
|
||||
bw ( KiB/s): min= 32, max= 192, per=100.00%, avg=122.73, stdev=43.53, samples=30
|
||||
iops : min= 8, max= 48, avg=30.63, stdev=10.85, samples=30
|
||||
lat (usec) : 250=0.87%, 500=9.59%, 750=7.86%, 1000=5.76%
|
||||
lat (msec) : 2=5.56%, 4=1.00%, 10=32.17%, 20=33.70%, 50=0.87%
|
||||
lat (msec) : 100=2.33%, 250=0.07%, 500=0.07%, 750=0.07%, 1000=0.10%
|
||||
cpu : usr=0.56%, sys=1.94%, ctx=3031, majf=0, minf=12
|
||||
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
|
||||
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
issued rwts: total=2082,921,0,0 short=0,0,0,0 dropped=0,0,0,0
|
||||
latency : target=0, window=0, percentile=100.00%, depth=32
|
||||
Run status group 0 (all jobs):
|
||||
READ: bw=271KiB/s (277kB/s), 271KiB/s-271KiB/s (277kB/s-277kB/s), io=8328KiB (8528kB), run=30763-30763msec
|
||||
WRITE: bw=120KiB/s (123kB/s), 120KiB/s-120KiB/s (123kB/s-123kB/s), io=3684KiB (3772kB), run=30763-30763msec
|
||||
Disk stats (read/write):
|
||||
sda: ios=2085/984, merge=0/88, ticks=28333/21834, in_queue=54712, util=93.86%
|
||||
✓ full pass
|
||||
跑分: 33777.8 ops/s
|
||||
最高温度: 54.0°C
|
||||
平均CPU利用率: 92.1%
|
||||
峰值内存使用率: 51.9%
|
||||
stress-ng: info: [3784311] Working directory / is not read/writeable, some I/O tests may fail
|
||||
stress-ng: info: [3784311] setting to a 30 second run per stressor
|
||||
stress-ng: info: [3784311] dispatching hogs: 4 cpu, 4 vm, 2 iomix
|
||||
stress-ng: metrc: [3784311] stressor bogo ops real time usr time sys time bogo ops/s bogo ops/s
|
||||
stress-ng: metrc: [3784311] (secs) (secs) (secs) (real time) (usr+sys time)
|
||||
stress-ng: metrc: [3784311] cpu 24082 30.01 57.65 0.34 802.40 415.28
|
||||
stress-ng: metrc: [3784311] vm 1005657 30.64 44.02 4.26 32825.38 20831.04
|
||||
stress-ng: metrc: [3784311] iomix 4543 30.28 2.18 3.87 150.03 750.28
|
||||
stress-ng: info: [3784311] successful run completed in 32.51s
|
||||
✓ dmesg pass
|
||||
无硬件相关内核报错
|
||||
|
||||
共 5 项: 5 通过, 0 失败, 0 跳过, 0 错误
|
||||
|
||||
[测试] 10.0.3.160 () 开始压测...
|
||||
[测试] 10.0.3.160 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.160:22 失败: dial tcp 10.0.3.160:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.152 () 开始压测...
|
||||
[测试] 10.0.3.152 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.152:22 失败: dial tcp 10.0.3.152:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[测试] 10.0.3.162 () 开始压测...
|
||||
[测试] 10.0.3.162 SSH 连接失败,跳过报告输出: SSH 连接 10.0.3.162:22 失败: dial tcp 10.0.3.162:22: connectex: No connection could be made because the target machine actively refused it.
|
||||
[状态] 10.0.3.4 ✗ → fail
|
||||
[状态] 10.0.3.104 ✗ → fail
|
||||
[状态] 10.0.3.147 ✗ → fail
|
||||
[状态] 10.0.3.152 ✗ → fail
|
||||
[状态] 10.0.3.236 ✗ → fail
|
||||
[状态] 10.0.3.1 ✗ → fail
|
||||
[状态] 10.0.3.123 ✗ → fail
|
||||
[状态] 10.0.3.119 ✗ → fail
|
||||
[状态] 10.0.3.148 (fe:49:6a:ab:e1:79) ✓ → pass
|
||||
[状态] 10.0.3.160 ✗ → fail
|
||||
[状态] 10.0.3.162 ✗ → fail
|
||||
[状态] 10.0.3.171 ✗ → fail
|
||||
[状态] 10.0.3.175 ✗ → fail
|
||||
@@ -1,79 +0,0 @@
|
||||
# 飞牛 fnOS 帮助中心
|
||||
|
||||
本文档是从 [飞牛 NAS 帮助中心](https://help.fnnas.com/) 爬取并整理的离线版本。
|
||||
|
||||
## 目录结构
|
||||
|
||||
### 1. 从这里开始
|
||||
- [如何安装和初始化飞牛 fnOS?](./start/install-os.md)
|
||||
- [如何安装 App 并连接到飞牛 NAS?](./start/install-app.md)
|
||||
- [如何安装飞牛 TV 并连接到影视服务器?](./start/install-tv.md)
|
||||
|
||||
### 2. 存储管理
|
||||
- [如何创建存储空间?](./volume/create.md)
|
||||
- [硬盘会自动休眠吗?](./volume/hibernate.md)
|
||||
- [如何使用 SSD 缓存加速?](./volume/ssd-cache.md)
|
||||
|
||||
### 3. 飞牛影视
|
||||
- [如何安装飞牛影视和创建媒体库?](./media/media-install.md)
|
||||
- [如何为家人朋友开通影视账号?](./media/create-users.md)
|
||||
- [Apple TV 上如何通过 Infuse 使用飞牛影视?](./media/atv-infuse.md)
|
||||
|
||||
### 4. 相册
|
||||
- [如何备份手机照片?](./photo/photo-backup.md)
|
||||
- [如何共享相册给设备内其他用户?](./photo/photo-share.md)
|
||||
- [AI 相册功能说明](./photo/photo-ai.md)
|
||||
|
||||
### 5. 远程访问
|
||||
- [如何远程访问到飞牛 NAS?](./access/how-access.md)
|
||||
- [如何管理 FN Connect 权益?](./access/how-to-set.md)
|
||||
|
||||
### 6. 系统设置
|
||||
- [如何创建新用户?](./settings/create-users.md)
|
||||
- [如何使用无线网卡?](./settings/wifi.md)
|
||||
- [如何修改飞牛系统的端口?](./settings/port-customization.md)
|
||||
|
||||
### 7. 文件管理
|
||||
- [如何将我的文件共享给其他用户?](./file/share.md)
|
||||
- [如何创建文件的外部分享链接?](./file/create-share-link.md)
|
||||
- [如何共享团队文件?](./file/team.md)
|
||||
|
||||
### 8. 文件共享协议
|
||||
- [Windows 通过 WebDAV 挂载飞牛注意事项](./file-service/webdav.md)
|
||||
- [Windows 通过 NFS 挂载飞牛注意事项](./file-service/nfs.md)
|
||||
- [SMB 3 多通道生效条件](./file-service/smb3-multichannel.md)
|
||||
|
||||
### 9. 虚拟机
|
||||
- [如何安装和使用虚拟机?](./virtual-machine/install.md)
|
||||
- [如何使用虚拟机硬件直通?](./virtual-machine/passthrough.md)
|
||||
|
||||
### 10. 飞牛同步
|
||||
- [飞牛同步功能介绍](./sync/how-to-use-fnsync.md)
|
||||
- [飞牛同步常见使用问题说明](./sync/fnsync-faq.md)
|
||||
|
||||
### 11. 备份
|
||||
- [加密备份功能介绍](./backup/encrypted.md)
|
||||
|
||||
### 12. iSCSI
|
||||
- [iSCSI 名词介绍](./iscsi/iscsi-definition.md)
|
||||
- [iSCSI 基础功能介绍](./iscsi/iscsi-create.md)
|
||||
- [iSCSI 用户组配置说明](./iscsi/iscsi-group.md)
|
||||
|
||||
### 13. AI & OpenClaw
|
||||
- [飞牛 OpenClaw 配置微信 ClawBot 教程](./ai/openclaw-weixin.md)
|
||||
- [飞牛 OpenClaw 配置钉钉机器人教程](./ai/openclaw-dingtalk.md)
|
||||
- [飞牛 OpenClaw 配置飞书机器人教程](./ai/openclaw-feishu.md)
|
||||
|
||||
### 14. 安全性
|
||||
- [安全披露](./safety/vulnerability-report.md)
|
||||
- [安全漏洞报告奖励计划](./safety/safe-report.md)
|
||||
|
||||
### 15. 更多
|
||||
- [联系我们](./contact/contact-us.md)
|
||||
- [本地账号与飞牛账号的区别?](./contact/account-diff.md)
|
||||
- [管理员和普通用户权限的区别?](./contact/user-role.md)
|
||||
|
||||
---
|
||||
|
||||
> 📅 爬取日期:2026-04-08
|
||||
> 🌐 来源:https://help.fnnas.com/
|
||||
@@ -1,43 +0,0 @@
|
||||
# 如何远程访问到飞牛 NAS?
|
||||
|
||||
飞牛 fnOS 有提供公网 IP 直连、FN Connect 远程访问服务、DDNS 域名转发、P2P 等多种远程访问方式。
|
||||
|
||||
## 方法1:公网 IP 直连
|
||||
|
||||
你可以与运营商申请公网 IP,并且在路由器中配置端口转发。打开网页浏览器或者在飞牛 App 上直接输入公网 IPv4 地址或 IPv6 地址以及端口号即可访问。
|
||||
|
||||
> **注意**:
|
||||
> - 飞牛 fnOS 的 HTTP 访问端口为 8000,HTTPS 访问端口为 8001;
|
||||
> - 若有配置端口转发,必须要在访问地址后增加端口号才能正确访问到飞牛 NAS;
|
||||
> - 公网 IP 直连的网络速度无折损,但公网 IP 直连无安全证书,HTTP 访问明文传输,请在安全的网络环境下访问。
|
||||
|
||||
## 方法2:配置 DDNS 域名和证书
|
||||
|
||||
DDNS 服务将自动定时更新你的飞牛 NAS 的出口 IP,并更新 IP 映射域名,通过域名即可轻松访问到飞牛 NAS。在此基础上,配置对应域名的 SSL 证书,可通过 HTTPS 安全访问到你的飞牛 NAS。
|
||||
|
||||
> **注意**:
|
||||
> - 购买并配置域名及证书;
|
||||
> - 飞牛支持以下服务商的 DDNS 服务:腾讯云、阿里云、华为云、花生壳、DuckDNS;
|
||||
> - 推荐配置 DDNS 域名及证书,速度无损且安全。
|
||||
|
||||
## 方法3:FN Connect 远程访问服务
|
||||
|
||||
FN Connect 是由飞牛自研的远程访问服务,可以为你提供安全、高效远程访问到飞牛 NAS 的服务。使用本服务,你需创建唯一的 FN ID 以标识你的飞牛 NAS 设备。
|
||||
|
||||
> **注意**:
|
||||
> - 使用 FN Connect 服务,你将被要求注册或登录飞牛账号;
|
||||
> - FN Connect 服务将为你的飞牛 NAS 创建 FN ID 对应的**唯一子域名的 SSL 证书**,可安全地访问到飞牛 NAS;
|
||||
> - FN Connect 将根据当下你所处的网络环境,判断选择最优的访问方式自动连接;
|
||||
> - FN Connect 中继转发服务会产生流量成本,因此会有**限速**。
|
||||
|
||||
## 方法4:客户端 P2P 连接
|
||||
|
||||
使用飞牛 App,在使用 FN Connect 服务的基础下,如果网络的 IPv6 可以通,则会自动切换到 P2P 方式直连到你的飞牛 NAS。
|
||||
|
||||
> **注意**:
|
||||
> - P2P 连接状态下,可能因为网络不稳定而回退至**中继转发**。
|
||||
> - P2P 连接,没有经过飞牛服务器,而是飞牛 NAS 设备与手机客户端直接加密通信,因此**安全无限速**。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,49 +0,0 @@
|
||||
# 如何管理 FN Connect 权益?
|
||||
|
||||
## 1. 绑定操作
|
||||
|
||||
### 1.1 购买后直接绑定
|
||||
|
||||
- 单台 NAS:自动绑定
|
||||
- 多台 NAS:购买后弹出绑定弹窗选择设备
|
||||
- 首次绑定需要3分钟线路匹配时间
|
||||
|
||||
### 1.2 购买后延迟绑定
|
||||
|
||||
- 通过"我的账号-我的设备"进入设备管理
|
||||
- 支持选择多个未生效权益进行绑定
|
||||
- 一台NAS只能绑定一个已生效权益
|
||||
|
||||
### 1.3 调整权益激活顺序
|
||||
|
||||
- 可拖动调整未生效权益的顺序
|
||||
- 已生效权益固定为第一位
|
||||
|
||||
## 2. 解绑操作
|
||||
|
||||
- 未生效权益解绑后仍为未生效状态
|
||||
- 已生效权益解绑后会持续计算使用时长
|
||||
- 解绑后会自动激活下一个未生效权益
|
||||
|
||||
## 3. 换绑操作
|
||||
|
||||
- 支持两台NAS之间已生效权益的对调
|
||||
- 不影响未生效权益的绑定情况
|
||||
|
||||
## 4. 其他重要说明
|
||||
|
||||
- 权益归属于购买时的飞牛账号
|
||||
- 设备重装或解绑不会丢失权益
|
||||
- 3天内支持无责任退款
|
||||
- 带宽问题可通过订单咨询提交工单
|
||||
|
||||
## 关键注意事项
|
||||
|
||||
- 权益绑定后需要3分钟线路匹配时间
|
||||
- 一台NAS只能有一个已生效权益
|
||||
- 解绑已生效权益需谨慎操作
|
||||
- 权益与账号绑定,不随设备变化而丢失
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,27 +0,0 @@
|
||||
# 飞牛 OpenClaw 配置钉钉机器人教程
|
||||
|
||||
## 1. 前置准备
|
||||
|
||||
1. 在应用中心安装好飞牛 OpenClaw 应用和相关的依赖应用。
|
||||
2. 给 OpenClaw 配置 1 个模型。
|
||||
- 官方合作推荐:阿里云百炼,免费体验 7000 万 Token。
|
||||
|
||||
## 2. 钉钉机器人配置
|
||||
|
||||
### 2.1 打开钉钉开发者平台并创建应用
|
||||
|
||||
1. 登录[开发者后台](https://open-dev.dingtalk.com/)。
|
||||
2. 在**应用开发**下,点击**立即创建**,可一键创建 OpenClaw 机器人。
|
||||
3. 在创建 OpenClaw 界面,填写机器人基本信息。
|
||||
4. OpenClaw 创建成功后,会自动展示应用的 **Client ID** 和 **Client Secret**,请保存好用于后续使用。
|
||||
|
||||
### 2.2 配置钉钉机器人消息渠道
|
||||
|
||||
1. 回到飞牛 OpenClaw。
|
||||
2. 打开"消息渠道" -> "添加渠道"。
|
||||
3. 选择"钉钉",输入 Client ID 和 Client Secret,并保存。
|
||||
4. 重启 OpenClaw,回到钉钉发送给机器人消息,有回复,即完成钉钉机器人配置。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,64 +0,0 @@
|
||||
# 飞牛 OpenClaw 配置飞书机器人教程
|
||||
|
||||
## 1. 前置准备
|
||||
|
||||
1. 在应用中心安装好飞牛 OpenClaw 应用和相关的依赖应用。
|
||||
2. 给 OpenClaw 配置 1 个模型。
|
||||
|
||||
## 2. 飞书机器人配置
|
||||
|
||||
### 2.1 打开飞书开放平台并创建应用
|
||||
|
||||
1. 打开飞书开放平台:https://open.feishu.cn/app
|
||||
2. 登录你的飞书账号
|
||||
3. 点击"创建企业自建应用"
|
||||
|
||||
### 2.2 获取 App ID 与 App Secret
|
||||
|
||||
1. 创建完成后,进入应用详情页
|
||||
2. 记录 App ID 和 App Secret
|
||||
|
||||
> ⚠️ **重要**:App Secret 务必保密,不要截图外传,不要发到群里。
|
||||
|
||||
### 2.3 权限配置
|
||||
|
||||
点击左侧"权限管理",点击"批量导入权限",粘贴相关权限配置。
|
||||
|
||||
### 2.4 启用机器人
|
||||
|
||||
1. 点击左侧"应用能力"
|
||||
2. 找到"机器人"卡片,点击"启用"
|
||||
3. 设置机器人名称
|
||||
4. 点击"保存"
|
||||
|
||||
### 2.5 发布版本
|
||||
|
||||
1. 点击左侧"版本管理与发布"
|
||||
2. 点击"创建版本"
|
||||
3. 填写版本信息
|
||||
4. 点击"保存"
|
||||
5. 点击"申请发布"
|
||||
|
||||
> 💡 **提示**:审批通过后,应用状态会变为"已发布"。这时候才能进行下一步。
|
||||
|
||||
### 2.6 在飞牛 OpenClaw 中配置飞书机器人
|
||||
|
||||
1. 在消息渠道中配置"添加渠道",选择飞书,输入 App ID 与 App Secret,并保存
|
||||
2. 重启 OpenClaw 网关
|
||||
|
||||
### 2.7 在飞书平台开启事件订阅
|
||||
|
||||
1. 回到飞书开放平台
|
||||
2. 点击左侧"事件与回调"
|
||||
3. 在"事件订阅方式"中,选择"长连接"并保存
|
||||
4. 在"订阅事件"区域,点击"添加事件",搜索 `im.message.receive_v1`
|
||||
|
||||
### 2.8 完成配对并验证
|
||||
|
||||
1. 在飞书中找到你的机器人,进入私聊,发送任何消息,获得配对码
|
||||
2. 在 OpenClaw 中完成配对
|
||||
3. 只要机器人正常回复即可
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,30 +0,0 @@
|
||||
# 飞牛 OpenClaw 配置微信 ClawBot 教程
|
||||
|
||||
## 1. 前置准备
|
||||
|
||||
1. 在应用中心安装好飞牛 OpenClaw 应用和相关的依赖应用。
|
||||
2. 给 OpenClaw 配置 1 个模型。
|
||||
- 官方合作推荐:阿里云百炼,免费体验 7000 万 Token。
|
||||
|
||||
## 2. 微信 ClawBot 配置
|
||||
|
||||
### 2.1 确认微信有 ClawBot 功能
|
||||
|
||||
微信正在逐步放开 ClawBot 功能,需要先升级微信到最新版本后,再确认当前的微信是否已有 ClawBot 功能。
|
||||
|
||||
1. 打开微信设置。
|
||||
2. 打开插件。
|
||||
3. 若看到微信 ClawBot 功能,则确定有 ClawBot 功能。
|
||||
|
||||
### 2.2 微信扫码完成绑定
|
||||
|
||||
1. 回到飞牛 OpenClaw。
|
||||
2. 打开"消息渠道" -> "添加渠道"。
|
||||
3. 选择"微信" -> "生成二维码"。
|
||||
4. 使用手机微信扫描二维码绑定。
|
||||
5. 手机上确定连接。
|
||||
6. 重启 OpenClaw,用手机发送消息,有回复,即完成微信 ClawBot 配置。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,52 +0,0 @@
|
||||
# 加密备份功能介绍
|
||||
|
||||
## 1. 备份飞牛时开启加密
|
||||
|
||||
加密备份功能允许在将数据从飞牛NAS备份到百度网盘或阿里云盘时,对文件进行轻量化加密。
|
||||
|
||||
**操作步骤:**
|
||||
|
||||
1. 进入"备份飞牛",选择目标网盘(百度网盘/阿里云盘)。
|
||||
2. 在第四步"备份规则"页面,启用"数据加密"并设置密码。
|
||||
3. 确认创建任务后即可开始加密备份。
|
||||
|
||||
**注意事项:**
|
||||
|
||||
- 密码最长32位,区分英文大小写,支持特殊符号:`! @ # % & * ~`。
|
||||
- 加密后本地文件保持不加密状态。
|
||||
- 加密后的文件名会被混淆,扩展名变更为`.fot`。
|
||||
- 加密任务创建后无法取消数据加密设置,请妥善保管密码。
|
||||
|
||||
## 2. 备份网盘时开启解密
|
||||
|
||||
当从网盘恢复数据到飞牛NAS时,需启用解密功能。
|
||||
|
||||
**操作步骤:**
|
||||
|
||||
1. 进入"备份远程存储",选择来源网盘。
|
||||
2. 在第四步"备份规则"页面,启用"数据解密"并输入密码。
|
||||
3. 确认创建任务后即可开始解密。
|
||||
|
||||
## 3. 常见问题
|
||||
|
||||
**Q1:加密后文件名会变吗?**
|
||||
|
||||
A:会。加密后文件名会被混淆,后缀统一变为`.fot`。
|
||||
|
||||
**Q2:什么情况下备份至百度网盘的加密任务失败?**
|
||||
|
||||
A:因百度网盘限制,需生成临时加密文件存放在本地后再上传。
|
||||
|
||||
**Q3:什么情况会解密失败?**
|
||||
|
||||
A:常见原因:
|
||||
1. 密码错误(注意英文大小写和特殊符号)。
|
||||
2. 配置解密任务后频繁修改网盘中加密文件夹。
|
||||
|
||||
**Q4:忘记密码怎么办?**
|
||||
|
||||
A:为保障数据安全,密码以密文形式存储在本地,不会上报至飞牛服务器,因此无法找回。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,25 +0,0 @@
|
||||
# 本地账号与飞牛账号的区别?
|
||||
|
||||
## 本地账号
|
||||
|
||||
- **用途**:用于访问本地网络中的飞牛 NAS 设备的用户验证。
|
||||
- **访问场景**:
|
||||
- 飞牛 fnOS 登录页面;
|
||||
- 飞牛 App 登录页面。
|
||||
- **如何创建**:
|
||||
- 在系统安装及初始化时,创建管理员账号;
|
||||
- 在 **系统设置** > **用户管理**,可创建普通用户账号。
|
||||
- **忘记密码**:
|
||||
- 管理员:如果有其他管理员,可联系其他管理员进行重置密码。若无,请重置飞牛 fnOS。
|
||||
- 普通用户:请联系管理员进行重置密码。
|
||||
|
||||
## 飞牛账号
|
||||
|
||||
- **用途**:在使用飞牛云服务,比如 **FN Connect 远程服务** 时,飞牛 NAS 需要绑定有效的 **飞牛账号**。
|
||||
- **访问场景**:[飞牛账号](https://account.fnnas.com/login) 登录页面。
|
||||
- **如何创建**:填写手机号码,短信验证成功后,填写基础信息完成创建。
|
||||
- **忘记密码**:点击 [忘记密码](https://account.fnnas.com/forget),填写手机号,验证成功后可重置密码。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,26 +0,0 @@
|
||||
# 联系我们
|
||||
|
||||
## 官网链接
|
||||
|
||||
- 飞牛官网:https://www.fnnas.com
|
||||
|
||||
## 公众号
|
||||
|
||||
- 关注飞牛官方公众号:"飞牛fnOS",了解最新消息。
|
||||
|
||||
## 商务合作
|
||||
|
||||
- 商务合作,请添加飞牛商务微信:fn_business
|
||||
|
||||
## 功能问题/意见反馈
|
||||
|
||||
- 请填写[问题/意见反馈表单](https://trim-nas.feishu.cn/share/base/form/shrcn2iVepeqaASZzMF0iMNAC5e)
|
||||
- 可加入飞牛公测反馈群,与飞牛团队面对面
|
||||
|
||||
## 应用生态
|
||||
|
||||
- 请求上架飞牛应用中心,请填写[申请表](https://trim-nas.feishu.cn/share/base/form/shrcn5m4MsyeqvEh6c4tm97pQKb),提交后,请静候。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,22 +0,0 @@
|
||||
# 管理员和普通用户权限的区别?
|
||||
|
||||
在 fnOS 上,用户分为 **管理员** 和 **普通用户** 两种角色。
|
||||
|
||||
## 超级管理员
|
||||
|
||||
**超级管理员** 特指在系统初始化时创建的第一个管理员账号,其用户 ID 为 1000。相较于其他管理员,超级管理员的特殊性仅体现在以下两点:
|
||||
|
||||
- 创建后不能被删除,只能被停用。
|
||||
- 默认开启 SSH 权限(可在 **设置 > 用户管理 > 用户详情 > 更多** 中关闭)。
|
||||
|
||||
## 关键信息总结
|
||||
|
||||
| 角色类型 | 关键权限/特点 |
|
||||
|----------|---------------|
|
||||
| **管理员** | 拥有系统管理权限,可进行各项设置操作 |
|
||||
| **普通用户** | 权限受限,仅能使用被授权的功能 |
|
||||
| **超级管理员** | 初始化创建,UID 1000,不可删除,默认开启 SSH |
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,28 +0,0 @@
|
||||
# Windows 通过 NFS 挂载飞牛注意事项
|
||||
|
||||
## 1. 开启 NFS 服务
|
||||
|
||||
- **目的:** 在 Windows 系统中启用 NFS 客户端功能,以便能够通过 NFS 协议挂载飞牛 NAS。
|
||||
|
||||
- **步骤:**
|
||||
1. 打开"控制面板" > "程序" > "启用或关闭 Windows 功能"。
|
||||
2. 找到"NFS 服务",勾选所有子项(如"NFS 客户端"等)。
|
||||
3. 点击"确定"保存设置,系统会自动安装所需组件。
|
||||
|
||||
## 2. 文件名乱码修正
|
||||
|
||||
- **问题描述:** 在挂载后,文件名可能显示为乱码,原因是 Windows 和飞牛 NAS 使用的字符编码不匹配。
|
||||
|
||||
- **解决方案:**
|
||||
1. 使用快捷键 `Win + R` 打开"运行"窗口,输入 `intl.cpl` 并回车。
|
||||
2. 在"区域"设置中,切换到"管理"选项卡,点击"更改系统区域设置"。
|
||||
3. 勾选"Beta 版:使用 Unicode UTF-8 提供全球语言支持"。
|
||||
4. 点击"确定"保存设置,并重启计算机使配置生效。
|
||||
|
||||
## 其他注意事项
|
||||
|
||||
- 除了通过 Windows 自带的文件资源管理器添加网络位置外,用户还可以使用第三方工具(如 RaiDrive、WinSCP、FileZilla)来挂载 NFS 共享。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,15 +0,0 @@
|
||||
# SMB 3 多通道生效条件
|
||||
|
||||
多通道是 SMB 3 的一项功能,可以让多个有线网络分担流量来提高传输速度和增强可靠性。
|
||||
|
||||
若你已在飞牛 **系统设置** > **文件共享协议** > **SMB** > **高级设置** 中开启了 **SMB 3 多通道**,但速度上未有显著提升,请参考此文档检查以下条件是否满足:
|
||||
|
||||
1. NAS 和客户端都具有多个网口,且 NAS 上实际连接的网口数量是客户端网口数量的整数倍;
|
||||
2. NAS 与客户端网口可以互相通信(在同一子网或正确路由下);
|
||||
3. NAS 与客户端所有网口都具有相同的速率(否则,当有不同速率的网口时,系统可能会优先选择最快的网口);
|
||||
4. 客户端支持 SMB 3 多通道,包括 Windows 8、Windows Server 2012、macOS 10.13 (High Sierra) 及以上版本;
|
||||
5. 硬盘及 CPU 性能良好,非性能瓶颈。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,29 +0,0 @@
|
||||
# Windows 通过 WebDAV 挂载飞牛注意事项
|
||||
|
||||
## 一、开启 HTTP 支持
|
||||
|
||||
Windows 默认不支持通过 HTTP 协议连接 WebDAV 服务,需要通过修改注册表来开启。具体步骤如下:
|
||||
|
||||
1. 使用快捷键 Win + R 打开运行窗口,输入 `regedit` 打开注册表编辑器。
|
||||
2. 在地址栏中粘贴路径:`计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters`,然后按回车键。
|
||||
3. 找到 `BasicAuthLevel` 项,双击打开修改窗口,将数值数据改为 `2`,然后点击确定。
|
||||
4. 再次使用 Win + R 打开运行窗口,输入 `services.msc` 打开服务窗口。
|
||||
5. 找到 `WebClient` 服务,右键选择"重新启动"。
|
||||
|
||||
## 二、解决文件大小超出限制的问题
|
||||
|
||||
Windows WebDAV 默认传输文件大小限制为 50MB,可以通过修改注册表来调整这一限制。具体操作如下:
|
||||
|
||||
1. 使用 Win + R 打开运行窗口,输入 `regedit` 打开注册表编辑器。
|
||||
2. 在地址栏中粘贴路径:`计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters`
|
||||
3. 找到 `FileSizeLimitInBytes` 项,双击打开修改窗口,将默认值 `2faf080`(50MB)修改为 `ffffffff`(4GB)
|
||||
4. 再次使用 Win + R 打开运行窗口,输入 `services.msc` 打开服务窗口。
|
||||
5. 找到 `WebClient` 服务,右键选择"重新启动"。
|
||||
|
||||
## 其他说明
|
||||
|
||||
- 文章还提到了在 Windows 上挂载 WebDAV 的其他方式,例如使用第三方文件管理客户端(如 RaiDrive、WinSCP、FileZilla)。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,40 +0,0 @@
|
||||
# 如何创建文件的外部分享链接?
|
||||
|
||||
将 fnOS 更新到 V0.8.26 版本之后,你可以在 **文件管理** 中创建文件的 **外部分享链接**,你的家人朋友可以通过此链接访问并且下载你分享的文件。
|
||||
|
||||
## 如何设置外部分享链接?
|
||||
|
||||
打开 **文件管理**,在左下角可打开 **文件管理设置** - **外链分享设置**。
|
||||
|
||||
你可以在 **外链分享设置** 中,设置以下功能:
|
||||
|
||||
- **外链分享权限**:可以选择 **所有用户** 或 **仅管理员** 都能够使用 **外链分享**
|
||||
- **分享链接设置**:
|
||||
- **默认链接**:若你有开启 FN Connect 远程访问服务,则每次分享时会生成 FN Connect 专属的文件外链分享链接。
|
||||
- **自定义链接**:可设置生成的外部分享链接使用 HTTPS 或 HTTP 协议。
|
||||
|
||||
## 如何创建外部分享链接?
|
||||
|
||||
在 **文件管理**,可选中文件,点击右键或更多,创建外部分享链接。
|
||||
|
||||
创建外部分享链接时,你可以设置:
|
||||
|
||||
- **到期时间**:若不勾选,则为 **永久**。
|
||||
- **密码**:若不勾选,则为 **无密码**,若勾选,请输入至少4位的密码。
|
||||
- **可访问次数**:默认不勾选,若不勾选,则为 **无限制次数**
|
||||
|
||||
点击确定,即可创建外部分享链接。同一个文件可分别创建多个不同的分享链接。
|
||||
|
||||
## 如何管理外部分享链接?
|
||||
|
||||
你可以在 **文件管理** - **外链分享** 中看见并管理的所有你创建的文件外部分享链接。
|
||||
|
||||
文件外部分享链接,有以下 3 个状态:
|
||||
|
||||
1. **有效**:未达到期时间、可访问次数不为0、当前用户有使用功能的权限。
|
||||
2. **失效**:时间已经到了到期时间 或者 可访问次数 = 0,则此链接失效。
|
||||
3. **无效**:用户无外链分享功能的使用权限等异常情况会导致链接无效。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,34 +0,0 @@
|
||||
# 如何将我的文件共享给其他用户?
|
||||
|
||||
在 fnOS 上,你可以在多个用户之间共享位于 NAS 存储空间上的文件。文件所在的位置不同,共享的方法也有所差别:
|
||||
|
||||
1. 文件位于 **我的文件** 下,即存放于某个用户的个人目录中:目录所有者可通过 **共享给设备的用户** 功能,允许其他用户从 **他人共享** 中访问其个人目录下的文件。
|
||||
|
||||
2. 文件位于 **团队文件夹** 中,即存放于存储空间下的 @team 目录中,不归属于任何用户:管理员可对团队文件夹及其子文件夹或文件设置用户/用户组的访问权限。
|
||||
|
||||
## 如何将我的文件共享给他人?
|
||||
|
||||
1. 选择 **我的文件** 下的任一文件夹,右键,点击 **共享给设备内的用户**,打开共享设置弹窗。
|
||||
2. 在页面左侧选择要共享给哪些用户或用户组,可搜索名称或备注。
|
||||
3. 在右侧选择被共享者的访问权限,可选择只读、读写或禁止访问。选择后,点击确认。
|
||||
4. 设置共享名称,此名称将展示给被共享者。默认为文件夹名称。确认后,点击开启共享。
|
||||
|
||||
## 如何修改共享权限?
|
||||
|
||||
1. 打开 **文件管理 > 我的共享**,找到要修改的共享文件夹,右键点击共享设置。
|
||||
2. 修改权限,或新增用户,点击确定。
|
||||
3. 选择是否要将此次修改应用到已存在的子文件夹和文件上。
|
||||
|
||||
## 被共享者如何访问他人共享的文件?
|
||||
|
||||
1. 打开 **文件管理 > 他人共享**,双击共享者。
|
||||
2. 即可查看到被共享的文件夹。
|
||||
|
||||
## 如何关闭共享?
|
||||
|
||||
1. 打开 **文件管理 > 我的共享**,找到要修改的共享文件夹,右键点击共享设置。点击关闭共享。
|
||||
2. 关闭共享的本质是移除已为其他用户添加的权限。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,35 +0,0 @@
|
||||
# 如何共享团队文件?
|
||||
|
||||
本文详细介绍了在飞牛 fnOS 系统中如何共享团队文件,包括团队文件夹的创建、权限设置、成员管理以及临时共享子文件的方法。
|
||||
|
||||
## 1. 团队文件夹的创建
|
||||
|
||||
- 登录管理员账号,进入"我的文件 > 团队文件",点击"新建团队文件夹"。
|
||||
- 填写文件夹名称、选择存储空间,并设置容量上限、文件共享协议权限及回收站管理权限。
|
||||
- 设置成员及权限(支持按用户、用户组或应用设置只读、读写或禁止访问)。
|
||||
|
||||
## 2. 修改团队文件夹的成员权限
|
||||
|
||||
- 在"团队文件"中找到目标文件夹,右键选择"成员权限"进行修改。
|
||||
- 支持将权限变更应用到现有子文件夹和文件。
|
||||
|
||||
## 3. 收紧子文件权限
|
||||
|
||||
- 针对特定子文件夹或文件,可通过"详细信息 > 权限"设置独立于父级的权限。
|
||||
- 适用于限制用户访问部分内容的情况。
|
||||
|
||||
## 4. 临时共享子文件给非团队成员
|
||||
|
||||
- 对子文件设置权限后,复制其原始路径发送给被共享者。
|
||||
- 被共享者可通过粘贴路径直接访问,但无权限浏览上层目录。
|
||||
|
||||
## 关键注意事项
|
||||
|
||||
- **权限继承规则:**
|
||||
- 用户组权限由组内成员继承,多组权限取并集。
|
||||
- 用户单独设置的权限优先级高于用户组权限。
|
||||
- **功能依赖:** 团队文件功能需 fnOS V0.9.35 或更高版本支持。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
1874
fnhelp/fnOS完整帮助文档.md
1874
fnhelp/fnOS完整帮助文档.md
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
||||
# iSCSI 基础功能介绍
|
||||
|
||||
## 1. 创建LUN
|
||||
|
||||
- **前置条件**:建议在千兆及以上局域网环境中使用iSCSI。
|
||||
- **步骤**:
|
||||
1. 进入"应用-LUN",选择"新建LUN"。
|
||||
2. 设置LUN所在的存储空间(支持Btrfs、EXT4文件系统)、容量(≥5GB,建议不超过2TB)及类型(推荐Thick Provisioning)。
|
||||
3. 创建或选择已有的Target进行映射。
|
||||
4. 确认后等待3-5秒完成创建。
|
||||
|
||||
## 2. 电脑端挂载
|
||||
|
||||
### Windows系统
|
||||
|
||||
1. 按`Win+R`,输入`iscsicpl`打开iSCSI初始化器。
|
||||
2. 在"发现"选项卡中添加NAS的IP地址和端口(默认3260)。
|
||||
3. 在"目标"选项卡中选择对应的Target IQN并连接。
|
||||
4. 通过`diskmgmt.msc`打开磁盘管理,初始化磁盘(推荐GPT分区),格式化并分配盘符。
|
||||
|
||||
### macOS系统
|
||||
|
||||
(需借助第三方工具,如DAEMON Tools)
|
||||
|
||||
1. 打开DAEMON Tools,添加iSCSI设备(输入NAS IP和端口)。
|
||||
2. 选择Target并连接。
|
||||
3. 在"磁盘工具"中格式化LUN(可选APFS或exFAT文件系统)。
|
||||
4. 挂载后即可在Finder中访问。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 若连接失败,请检查IP/端口、网络连通性、iSCSI服务状态及防火墙设置。
|
||||
- MBR分区仅支持2TB以下容量,超过部分无法识别。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,45 +0,0 @@
|
||||
# iSCSI 名词介绍
|
||||
|
||||
## 1. iSCSI
|
||||
|
||||
基于 IP 网络的存储协议,允许通过以太网远程访问和管理存储设备,数据通过网络传输到 NAS。
|
||||
|
||||
## 2. LUN(Logical Unit Number)
|
||||
|
||||
代表可被定位的 SCSI 逻辑设备,相当于一块虚拟硬盘,可指定大小和存储空间,供客户端挂载和读写。
|
||||
|
||||
## 3. Thick 和 Thin 模式
|
||||
|
||||
- **Thick**:创建时一次性分配指定容量,性能稳定、延迟低。
|
||||
- **Thin**:创建时仅分配少量空间,根据数据写入动态扩展,存储空间利用率更高。
|
||||
- 两种模式的 LUN 大小均需至少 5 GB。
|
||||
|
||||
## 4. Target
|
||||
|
||||
NAS 提供的访问地址,客户端通过 Target 连接 LUN,支持设置 CHAP 密码提升安全性。
|
||||
|
||||
## 5. IQN(iSCSI Qualified Name)
|
||||
|
||||
用于唯一标识客户端和 Target 的名称,遵循特定命名格式,类似"身份证"。
|
||||
|
||||
## 6. Initiator(启动器)
|
||||
|
||||
客户端上运行的程序,通过 IQN 向 Target 发起连接请求。
|
||||
|
||||
- Windows 系统自带 iSCSI Initiator
|
||||
- macOS 需安装第三方工具(如 DAEMON Tools)
|
||||
|
||||
## 7. 用户组
|
||||
|
||||
将不同设备的 Initiator IQN 分组,批量设置对 LUN 的读写权限。
|
||||
|
||||
## 8. CHAP(Challenge-Handshake Authentication Protocol)
|
||||
|
||||
密码认证机制,防止未授权访问,支持两种模式:
|
||||
|
||||
- **单向 CHAP**:仅 Initiator 验证 Target。
|
||||
- **相互 CHAP**:Initiator 和 Target 相互验证。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,30 +0,0 @@
|
||||
# iSCSI 用户组配置说明
|
||||
|
||||
本文介绍如何配置 iSCSI 用户组,包括如何获取启动器IQN以及设置用户组权限的方法。
|
||||
|
||||
## 获取启动器 IQN
|
||||
|
||||
### Windows 系统
|
||||
|
||||
1. 按`Win+R`,输入`iscsicpl`打开iSCSI初始化器。
|
||||
2. 在"目标"选项卡中点击"获取 IQN",即可复制当前的 IQN。
|
||||
|
||||
### Mac 系统
|
||||
|
||||
1. 打开终端,输入命令获取 IQN。
|
||||
2. 复制显示的 IQN 字符串。
|
||||
|
||||
## 设置用户组权限
|
||||
|
||||
用户组可以将不同设备的 Initiator IQN 分组,批量设置对 LUN 的读写权限。
|
||||
|
||||
**操作步骤:**
|
||||
|
||||
1. 进入"应用-用户组",点击"新建用户组"。
|
||||
2. 设置用户组名称。
|
||||
3. 将各设备的 IQN 添加到用户组中。
|
||||
4. 创建或编辑 LUN 时,选择该用户组即可批量设置权限。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,68 +0,0 @@
|
||||
# Apple TV 上如何通过 Infuse 使用飞牛影视?
|
||||
|
||||
[Infuse](https://firecore.com/infuse) 是 Apple 生态下的一款播放器,适用于 iPhone、iPad、Apple TV 和 Mac 。由于飞牛影视暂未发布 Apple TV 版本,有需要的用户可在 Apple TV 上自行安装 Infuse ,并按照本文指引连接飞牛影视,达到以下使用效果:
|
||||
|
||||
- 从 Infuse 上查看飞牛影视上的媒体库,显示由飞牛刮削的影片信息
|
||||
- 播放影片,支持切换音频,支持切换内置、外挂字幕,播放记录与飞牛客户端同步
|
||||
- 通过搜索、按类型筛选、排序等方式查找影片
|
||||
|
||||
> **注意:**
|
||||
> - 此功能需将飞牛影视应用升级至 V0.8.17 及以上版本,Apple tvOS 和 iOS 需升级到18 版本,Infuse 建议升级到最新版本。
|
||||
> - 飞牛影视对 Infuse 的主要功能做了兼容处理。由于二者在功能上存在客观差异,在 Infuse 上使用与在飞牛上使用的体验将略有差别。
|
||||
|
||||
## 1. Infuse 连接飞牛影视服务器
|
||||
|
||||
### TV - 方式 1:局域网内扫描发现(Apple TV)
|
||||
|
||||
1. 打开 Infuse,点击右上角 **设置** ,点击 **共享**
|
||||
2. 点击 **添加媒体服务器**
|
||||
3. 在 **可用服务器** 中找到带有"飞牛影视"前缀的服务器,点击选择
|
||||
4. 输入飞牛影视的用户名和密码
|
||||
5. (可选)点击 **高级设置** > **媒体库模式** 开关,打开后 Infuse 首页将按照 Infuse 方式聚合展示飞牛上的影片
|
||||
6. 点击 **保存** 确认连接,连接成功后首页将出现飞牛影视服务器上的媒体库
|
||||
|
||||
### TV - 方式 2:手动连接(Apple TV)
|
||||
|
||||
1. 打开 Infuse,点击右上角 **设置** ,点击 **共享**
|
||||
2. 点击 **添加媒体服务器**
|
||||
3. 在 **可用服务器** 中选择 **其他 Jellyfin**
|
||||
4. 输入飞牛影视服务器的 IP、用户名和密码,(可选)将名称修改为便于识别的名称
|
||||
5. 点击 **高级设置** , **端口** 输入 8005
|
||||
6. (可选)**媒体库模式** 开关,打开后 Infuse 首页将按照 Infuse 方式聚合展示飞牛上的影片
|
||||
7. 点击 **新增** 确认连接,连接成功后首页将出现飞牛影视服务器上的媒体库
|
||||
|
||||
### App - 方式 1:局域网内扫描发现(iPad / iPhone)
|
||||
|
||||
1. 打开 Infuse,点击右下角 **设置** ,点击 **新增文件来源**
|
||||
2. 在 **可用的共享** 中找到带有"飞牛影视"前缀的服务器,点击选择
|
||||
3. 输入飞牛影视的用户名和密码
|
||||
4. (可选)点击 **高级设置** > **媒体库模式** 开关
|
||||
5. 点击 **新增** 确认连接
|
||||
|
||||
> **注意:**
|
||||
> - 当局域网内有其他 Emby 或 Jellyfin 服务器时,将可能无法扫描到飞牛影视服务器。请改用手动连接方式。
|
||||
> - 如果飞牛设备有多个网卡,请检查扫描到的服务器 IP 是否符合预期。如果不是,请改用手动连接方式连接目标 IP 。
|
||||
|
||||
### App - 方式 2:手动连接(iPad / iPhone)
|
||||
|
||||
1. 打开 Infuse,点击右下角 **设置** ,点击 **新增文件来源**
|
||||
2. 共享列表滚动到底部,点击 **媒体服务器** > **其他 Jellyfin**
|
||||
3. 输入飞牛影视服务器的 IP、用户名和密码
|
||||
4. 点击 **高级设置** , **端口** 输入 8005
|
||||
5. (可选)**媒体库模式** 开关
|
||||
6. 点击 **新增** 确认连接
|
||||
|
||||
## 2. 飞牛影视与 Infuse 体验差异
|
||||
|
||||
在 Infuse 上使用飞牛影视,相较于直接使用飞牛影视客户端:
|
||||
|
||||
- 同个影片或同一集的多个版本不会聚合,将平铺展示(在 **高级** 中打开 **媒体库模式** ,则从首页访问时会聚合)
|
||||
|
||||
相较于在 Infuse 上直接挂载影视目录或使用其他服务器:
|
||||
|
||||
- 部分排序方式不支持,如按文件夹、按播放次数等
|
||||
- 不支持安装 InfuseSync 插件
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,48 +0,0 @@
|
||||
# 如何为家人朋友开通影视账号?
|
||||
|
||||
## 1. 影视独立账号
|
||||
|
||||
### 1.1 如何开通?
|
||||
|
||||
- 使用影视管理员账号登录影视应用。
|
||||
- 点击首页右上角 → 设置 → 影视服务器 - 用户 → 新增用户。
|
||||
- 设置用户名、密码,并选择可访问的媒体库范围。
|
||||
- 点击确认保存。
|
||||
|
||||
### 1.2 如何登录?
|
||||
|
||||
- **网页端**:在浏览器中输入NAS地址后加 `/v`,使用影视独立账号登录。
|
||||
- **App端**:目前不支持独立账号登录,后续计划推出独立影视App。
|
||||
- **TV端**:参考安装指南。
|
||||
|
||||
## 2. NAS账号登录
|
||||
|
||||
### 2.1 哪些NAS用户可以访问影视?
|
||||
|
||||
- 默认所有NAS用户均可访问影视应用。
|
||||
- 可修改为仅管理员可见:
|
||||
1. 使用NAS管理员账号进入设置 → 应用 → 影视。
|
||||
2. 将"哪些用户可以从桌面访问"设置为"仅管理员"。
|
||||
3. 点击确定保存(注:此设置仅限制桌面图标显示,不影响通过浏览器直接访问)。
|
||||
|
||||
### 2.2 如何用NAS账号登录?
|
||||
|
||||
- **网页端/TV端**:在影视登录页选择"使用NAS账号登录",跳转至NAS授权页面确认。
|
||||
- **App端**:同网页端流程。
|
||||
|
||||
### 2.3 用NAS账号登录的用户可访问哪些媒体库?
|
||||
|
||||
- 新用户默认无媒体库访问权限,需由影视管理员设置:
|
||||
1. 使用影视管理员账号进入设置 → 影视服务器 - 用户 → 新用户默认权限设置。
|
||||
2. 选择可访问的媒体库范围并保存。
|
||||
- 此设置影响后续所有NAS新用户的默认权限,支持按需个性化调整。
|
||||
|
||||
## 关键说明
|
||||
|
||||
- 影视账号与NAS账号为两套独立体系,可根据需求选择开通类型。
|
||||
- 影视独立账号适用于仅需影视服务的用户;NAS账号适用于需使用全功能(如文件存储、相册等)的用户。
|
||||
- 权限管理灵活,支持按用户或媒体库细分访问范围。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,76 +0,0 @@
|
||||
# 如何安装飞牛影视和创建媒体库?
|
||||
|
||||
## 1. 安装飞牛影视
|
||||
|
||||
### 步骤1:通过应用中心安装
|
||||
|
||||
使用管理员账号登录网页端,打开"应用中心",找到"影视"应用并点击安装。
|
||||
|
||||
### 步骤2:确认安装位置
|
||||
|
||||
默认安装在系统分区,需指定媒体元数据存储位置(后续可修改)。
|
||||
|
||||
### 步骤3:检查信息并确认安装
|
||||
|
||||
### 步骤4:安装完成
|
||||
|
||||
桌面图标更新为已安装状态。
|
||||
|
||||
## 2. 设置影视可访问的文件范围
|
||||
|
||||
**入口**:通过应用中心或系统设置中的"影视"应用管理。
|
||||
|
||||
**添加文件夹**:可选择"我的文件""存储空间""外接存储""远程挂载"等路径,并设置为读写权限。
|
||||
|
||||
**保存设置**:点击"确定"保存,若路径异常需参考相关问题处理。
|
||||
|
||||
## 3. 影视初始化
|
||||
|
||||
### 3.1 打开方式
|
||||
|
||||
通过桌面图标、App或浏览器直接访问(NAS地址后加`/v`)。
|
||||
|
||||
### 3.2 授权登录
|
||||
|
||||
使用NAS管理员账号授权登录。
|
||||
|
||||
### 3.3 创建影视管理员账号
|
||||
|
||||
设置独立的影视管理员用户名和密码(与NAS账号体系独立)。
|
||||
|
||||
### 3.4 服务器设置
|
||||
|
||||
- 设置服务器名称
|
||||
- 首选语言
|
||||
- 元数据存储位置
|
||||
- 实时监控文件更新等
|
||||
|
||||
## 4. 创建媒体库
|
||||
|
||||
### 4.1 基础信息
|
||||
|
||||
选择内容类型(如电影、电视剧等),创建后不可修改。
|
||||
|
||||
### 4.2 媒体文件夹
|
||||
|
||||
添加已授权访问的文件夹路径。
|
||||
|
||||
### 4.3 高级设置
|
||||
|
||||
- 元数据下载语言(中文/英文)
|
||||
- 是否允许成人内容(基于TMDB标签)
|
||||
- 自动下载字幕及字幕语言设置
|
||||
|
||||
### 4.4 创建成功
|
||||
|
||||
系统自动开始扫描文件、刮削元数据、下载字幕等。
|
||||
|
||||
## 关键注意事项
|
||||
|
||||
- 影视应用的用户权限与NAS权限独立,管理员需单独设置文件访问范围。
|
||||
- 朋友仅需影视账号,家人可同时使用NAS账号登录影视。
|
||||
- 成人内容过滤依赖TMDB标签,部分未标记内容可能无法完全屏蔽。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,58 +0,0 @@
|
||||
# AI 相册功能说明
|
||||
|
||||
## 一、安装说明
|
||||
|
||||
### 1.1 硬件配置要求
|
||||
|
||||
- **内存**:最低 2 GB(智能识别增强模型需 8 GB)
|
||||
- **存储空间**:模型安装位置需 ≥20 GB
|
||||
|
||||
### 1.2 下载安装模型
|
||||
|
||||
- **人脸识别**:仅支持"标准模型"
|
||||
- **智能识别**:可选"基础模型"(内存≥2GB)或"增强模型"(内存≥8GB)
|
||||
|
||||
### 1.3 GPU 设置
|
||||
|
||||
- **支持显卡**:Intel GPU、NVIDIA(GeForce 700以上)
|
||||
- **常见问题**:驱动缺失、显存不足、GPU繁忙或老旧显卡不兼容
|
||||
|
||||
## 二、任务执行
|
||||
|
||||
- **触发方式**:安装模型后自动执行、上传文件后自动触发、手动执行未识别内容、重置识别任务
|
||||
- **任务失败处理**:检查GPU兼容性、存储状态、文件权限,或等待系统资源空闲后重试
|
||||
|
||||
## 三、AI 相册功能详解
|
||||
|
||||
### 3.1 人脸识别高级设置
|
||||
|
||||
- **置信度阈值**:影响人脸检测准确度(不建议修改)
|
||||
- **最少照片数**:控制生成人物相册的最低照片数量
|
||||
- **相似度差异阈值**:调整人物聚合的精确度(可选"宽松聚合")
|
||||
|
||||
### 3.2 人物管理
|
||||
|
||||
支持命名、合并人物、隐藏/显示人物、手动添加面孔。
|
||||
|
||||
### 3.3 AI 搜索
|
||||
|
||||
支持自然语言搜索(如"在海边散步""吃披萨")。开启"视频增强识别"可提升视频搜索效果。
|
||||
|
||||
### 3.4 智能分类
|
||||
|
||||
自动根据照片/视频内容生成分类标签(如地点、活动)。
|
||||
|
||||
### 3.5 视频增强识别
|
||||
|
||||
- 仅支持 **.mp4、.mov** 格式
|
||||
- 占用资源较多,建议在完成基础识别后单独启用
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 模型文件需通过官方渠道下载,避免安装失败
|
||||
- 任务执行时避免同时运行其他高负载应用
|
||||
- 视频增强识别耗时较长,需耐心等待
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,69 +0,0 @@
|
||||
# 如何备份手机照片?
|
||||
|
||||
通过飞牛 App 的 **相册备份** 功能,您可以将手机本地的照片备份上传到飞牛 NAS 上,以节省手机空间。
|
||||
|
||||
> **前置准备:**
|
||||
>
|
||||
> 1. 确保已经安装 **相册** 应用;
|
||||
> 2. 确保 **备份位置** 的存储空间足够,如果不足,请 **修改备份位置**;
|
||||
> 3. 下载安装飞牛 App,连接到飞牛 NAS;
|
||||
> 4. 手机设备需要给飞牛 App 开启本地照片或文件的访问权限。
|
||||
|
||||
## 操作步骤
|
||||
|
||||
1. 打开飞牛 App → **相册** → **设置** → **备份设置**;
|
||||
2. 开启 **相册自动备份** 功能开关。开启备份后,可查看到 **备份状态**:
|
||||
|
||||
> **备份状态说明:**
|
||||
>
|
||||
> 1. **扫描中**:正在扫描手机设备本地的照片和视频数量以及大小,准备备份上传。
|
||||
> 2. **正在备份**:正在备份上传,会显示剩余数量以及已完成备份上传的数量。
|
||||
> 3. **备份成功**:该次备份已完成,全部成功或部分失败。如果部分失败,点击 **备份状态栏** 可跳转至 **任务传输** 查看详细错误信息。
|
||||
> 4. **备份暂停或失败**:可能存在以下原因:
|
||||
> - **电量不足**:如果有开启 **节能备份**,当手机设备电量低于 15% 时,将暂停备份。
|
||||
> - **没有开启 Wi-Fi**:如果没有开启 **使用流量备份**,在非 Wi-Fi 环境下将会暂停备份。
|
||||
> - **连接断开**:如果网络环境变换,导致与飞牛 NAS 的连接断开,备份将暂停。
|
||||
> - **存储空间不足**:如果 **备份位置** 的存储空间不足。
|
||||
> - **存储空间无权限**:请联系管理员处理。
|
||||
|
||||
3. 如果在备份的过程中关闭备份,将取消正在备份以及未备份的队列任务,但已完成备份的照片及视频将保留。
|
||||
|
||||
## 备份规则
|
||||
|
||||
- **备份所有**:将扫描和备份设备中的所有照片和视频,将跳过已备份的照片,不会重复备份。
|
||||
- **备份新照片**:仅备份从现在起新拍摄的照片和视频。
|
||||
|
||||
**继续备份** 将在上一次备份规则的基础上继续备份。
|
||||
|
||||
## 备份目录偏好
|
||||
|
||||
### 苹果设备
|
||||
|
||||
- **按拍摄日期**:按照片的拍摄日期,创建并保存到子文件夹。"备份位置/年/月/"
|
||||
- **保存在一个文件夹下**:所有照片都保存在一个文件夹下。
|
||||
|
||||
### 安卓设备
|
||||
|
||||
- **默认**:区分照片的目录,DCIM 目录下的照片按拍摄日期保存。
|
||||
- **按拍摄日期**:按照片的拍摄日期保存。
|
||||
- **按设备目录**:按当前设备的文件夹目录结构直接保存。
|
||||
- **保存在一个文件夹下**:所有照片都保存在一个文件夹下。
|
||||
|
||||
## 不熄屏备份
|
||||
|
||||
开启备份之后,若需要备份大量照片和视频,可开启 **不熄屏备份**,允许飞牛相册在屏幕全暗的状态下持续备份上传直至完成。
|
||||
|
||||
> 若要保持 **不熄屏备份**,请注意:
|
||||
>
|
||||
> - 保持稳定的网络连接;
|
||||
> - 保持设备在充电状态;
|
||||
> - 请勿关闭手机屏幕或退出 App。
|
||||
|
||||
## 其他备份设置
|
||||
|
||||
- **按拍摄日期重命名**:上传时按照"IMG_年月日_时分秒"的格式重命名文件。
|
||||
- **同步创建相册**:上传、备份时,根据已有的手机相册同步在飞牛相册应用中创建相册。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,39 +0,0 @@
|
||||
# 如何共享相册给设备内其他用户?
|
||||
|
||||
你可以在相册应用中通过 **共享相册** 功能将相册共享给设备内的其他用户,可以设置公开给设备内所有用户或者仅共享给指定用户。
|
||||
|
||||
## 1. 管理员创建用户
|
||||
|
||||
使用管理员账号,在 **设置** > **用户管理** > **新增用户** 可以创建用户或者用户组。
|
||||
|
||||
## 2. 共享相册
|
||||
|
||||
你可以通过 2 种方式,将相册共享给设备内的其他用户。
|
||||
|
||||
### Web 端操作
|
||||
|
||||
**1. 共享相册(Web)**
|
||||
|
||||
在相册详情中,点击 **共享** 可设置将相册 **共享给此设备的所有账号** 或者 **共享给指定账号**。
|
||||
|
||||
> 注意:相册共享可选择设置 2 种权限
|
||||
> - **可查看** 权限:被共享者可查看相册、预览内容详细、可下载内容。
|
||||
> - **可添加** 权限:被共享者在拥有 **可查看** 权限基础上,可添加内容进入相册、可从相册中移除内容。
|
||||
|
||||
**2. 共享照片将自动创建共享相册(Web)**
|
||||
|
||||
在 **图库**、**相册详情**、**预览详情** 或 **搜索结果** 中,选中多个或者单个照片进行 **共享**。此方式共享,会以当前日期命名创建 **共享相册**,并且显示在 **我的共享** 中。
|
||||
|
||||
### App 端操作
|
||||
|
||||
**1. 共享相册(App)**
|
||||
|
||||
在相册详情中,点击 **共享** 可设置将相册 **共享给此设备的所有账号** 或者 **共享给指定账号**。
|
||||
|
||||
**2. 共享照片将自动创建共享相册(App)**
|
||||
|
||||
在 **图库**、**相册详情**、**预览详情** 或 **搜索结果** 中,选中多个或者单个照片进行 **共享**。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,63 +0,0 @@
|
||||
# 安全漏洞报告奖励计划
|
||||
|
||||
飞牛NAS(fnOS)官方发布的"安全漏洞报告奖励计划",旨在邀请安全研究人员、技术爱好者和开发者共同参与fnOS系统的安全共建。
|
||||
|
||||
## 1. 漏洞测试范围
|
||||
|
||||
**适用对象**:
|
||||
- fnOS官方正式发布版本
|
||||
- 飞牛自有域名与服务
|
||||
- 官方App与API接口
|
||||
|
||||
**不包含**:
|
||||
- 已停止维护的历史版本
|
||||
- 第三方插件或组件
|
||||
- 非官方修改固件
|
||||
- 非飞牛控制的第三方服务
|
||||
|
||||
## 2. 漏洞等级与奖励标准
|
||||
|
||||
| 等级 | 奖励范围 |
|
||||
|------|----------|
|
||||
| **严重漏洞(Critical)** | ¥20,000 – ¥50,000 |
|
||||
| **高危漏洞(High)** | ¥5,000 – ¥20,000 |
|
||||
| **中危漏洞(Medium)** | ¥1,000 – ¥5,000 |
|
||||
| **低危漏洞(Low)** | ¥300 – ¥1,000 |
|
||||
|
||||
## 3. 漏洞提交方式
|
||||
|
||||
- **邮箱**:safety@fnnas.com
|
||||
|
||||
**提交内容需包含**:
|
||||
- 漏洞详细描述及风险分析
|
||||
- 可复现的操作步骤
|
||||
- 受影响产品及fnOS版本号
|
||||
- 有效PoC或视频演示
|
||||
- 联系方式
|
||||
|
||||
## 4. 报告流程
|
||||
|
||||
1. **提交与评估**:48小时内初步评估。
|
||||
2. **验证与修复**:根据漏洞等级制定修复计划(严重漏洞10个工作日内修复)。
|
||||
3. **复测验证**:邀请提交者参与复测。
|
||||
4. **奖励发放**:复测通过后30天内发放奖励。
|
||||
|
||||
## 5. 威胁情报协作计划
|
||||
|
||||
- 鼓励提交与fnOS生态相关的威胁情报(如攻击源IP、恶意域名、攻击样本等)。
|
||||
- 高价值情报将根据其价值给予奖励或致谢。
|
||||
|
||||
## 6. 第三方应用安全说明
|
||||
|
||||
- 第三方应用的漏洞责任归开发者所有,飞牛不直接提供现金奖励。
|
||||
- 飞牛将协助建立沟通渠道,并视情况采取下架、公告等措施。
|
||||
|
||||
## 关键条款
|
||||
|
||||
- **负责任披露原则**:禁止在修复前公开漏洞细节,禁止利用漏洞进行破坏性行为。
|
||||
- **安全港条款**:遵循规则的研究人员不会被视为非法入侵。
|
||||
- **最终解释权**归飞牛所有。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,59 +0,0 @@
|
||||
# 安全披露
|
||||
|
||||
## 2026-02-24 应用安全更新公告
|
||||
|
||||
### 青龙面板
|
||||
|
||||
**风险等级**:高危
|
||||
|
||||
**风险描述**:该应用存在安全缺陷,在端口公网暴露情况下,通过特定端口可能导致设备被恶意利用进行非法写入。
|
||||
|
||||
**影响范围**:已安装青龙面板(包括应用中心或 Docker 方式)并开启公网端口映射的实例。
|
||||
|
||||
**处置措施**:
|
||||
- 已从应用中心下架,等待开发者更新;
|
||||
- 立即停用该应用,或避免公网暴露 15700 端口。
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-14 应用安全更新公告
|
||||
|
||||
### Aria2
|
||||
|
||||
**风险等级**:高危
|
||||
|
||||
**风险描述**:该应用默认配置存在安全缺陷,在公网暴露情况下,通过特定方式可能导致设备被非法写入。
|
||||
|
||||
**影响范围**:所有已安装版本,开启公网的特定端口映射的实例。
|
||||
|
||||
**处置措施**:
|
||||
- 已从应用中心下架,等待开发者更新;
|
||||
- 立即停用该应用,选择替代应用或等待后续更新。
|
||||
|
||||
### 道理鱼音乐
|
||||
|
||||
**风险等级**:中危
|
||||
|
||||
**风险描述**:旧版本存在 Docker 容器权限提升风险,导致该应用能够访问其容器限定范围之外的其他当前容器目录。
|
||||
|
||||
**影响范围**:0.1.9 及之前版本。
|
||||
|
||||
**处置措施**:
|
||||
- 已发布修复版本 0.2.4;
|
||||
- 前往应用中心尽快升级。
|
||||
|
||||
### Office 预览
|
||||
|
||||
**风险等级**:低危
|
||||
|
||||
**风险描述**:在服务暴露于公网,且获取应用密钥情况下存在被碰撞文件风险隐患。
|
||||
|
||||
**影响范围**:0.8.7 及之前版本,且配置为公网访问的实例。
|
||||
|
||||
**处置措施**:
|
||||
- 已发布修复版本 0.8.8;
|
||||
- 前往应用中心尽快升级。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,32 +0,0 @@
|
||||
# 如何创建新用户?
|
||||
|
||||
在 fnOS 上,你可以创建多个用户账号,方便多人共同使用 NAS。
|
||||
|
||||
## 1. 设置入口
|
||||
|
||||
使用管理员账号,打开 **设置 > 用户管理 > 用户**,点击 **新增用户**。
|
||||
|
||||
## 2. 账号信息
|
||||
|
||||
- **用户类型**:区分管理员和普通用户 2 种角色权限。
|
||||
- **用户名**:支持中文、英文、数字,不能包含特殊符号和空格。
|
||||
- **备注**:可选填写。
|
||||
- **密码**:不少于 6 位,至少包含大写字母、小写字母、数字、符号中的 2 种。点击 **使用随机密码**,将填入随机密码并自动复制到剪贴板。
|
||||
|
||||
## 3. 加入用户组
|
||||
|
||||
可选择是否要将新用户加入到已有的用户组中。如需创建用户组,请前往 **设置 > 用户管理 > 用户组 > 新增用户组**。
|
||||
|
||||
## 4. 设置可用存储空间及配额
|
||||
|
||||
你可以按单个用户设置允许其使用哪个存储空间,不希望该用户使用的存储空间,可点击 **×** 按钮移除。管理员用户允许使用系统上的所有存储空间。
|
||||
|
||||
每个存储空间下该用户可使用的容量默认无限制,可手动输入数值修改。
|
||||
|
||||
## 5. 创建完成
|
||||
|
||||
后续可在用户详情中查看该用户的存储空间用量,进行修改、停用、删除等操作。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,47 +0,0 @@
|
||||
# 如何修改飞牛系统的端口?
|
||||
|
||||
fnOS 版本更新到 V0.8.22 版本之后,在 **设置** 中可以修改飞牛NAS的 **HTTP 和 HTTPS 端口**、设置 **强制 HTTPS 连接**、设置是否固定占用并 **重定向 80 与 443 端口**。
|
||||
|
||||
## 1. 默认端口
|
||||
|
||||
fnOS 从 V0.8.22 版本之后,默认端口修改为 **HTTP 5666** 和 **HTTPS 5667** 端口。
|
||||
|
||||
> **注意:**
|
||||
> - 在公测阶段飞牛仍继续占用 8000 和 8001 端口,允许用户通过 8000 和 8001 端口访问到飞牛系统。
|
||||
> - 在正式版本之后将不再使用 8000 和 8001 端口。
|
||||
|
||||
## 2. 如何使用不同端口号访问 fnOS?
|
||||
|
||||
### 网页
|
||||
|
||||
在网页浏览器的地址栏中输入 `http://192.168.1.10:5666`(IP 地址例子)
|
||||
|
||||
> **注意:**
|
||||
> - 如果已开启了 **重定向 80 与 443 端口**,则可以不需要填写端口号。
|
||||
|
||||
### App
|
||||
|
||||
在访问方式中输入 `http://192.168.1.10:5666`(IP 地址例子)
|
||||
|
||||
> **注意:**
|
||||
> - **如果不填写端口,App 和 TV 会默认跳转 HTTP 8000 和 HTTPS 8001 端口**。
|
||||
|
||||
## 3. 如何修改端口
|
||||
|
||||
系统升级到 V0.8.22 版本之后,在 **设置 - 安全性** 中可以设置端口。
|
||||
|
||||
> **注意:**
|
||||
> - HTTP 和 HTTPS 端口号不可为空,部分端口号不可用。
|
||||
> - 确认修改端口,将会导致正在上传的任务或者正在进行的服务中断。
|
||||
|
||||
### 强制 HTTPS 连接
|
||||
|
||||
启用了**强制 HTTPS 连接**之后,系统将 HTTP 连接重定向到 HTTPS 连接。HTTPS 连接,浏览器将会验证安全、有效的证书才能正常访问。
|
||||
|
||||
### 重定向 80 与 443 端口
|
||||
|
||||
系统将默认启用 80 与 443 端口。启用之后,若你访问 80 或 443 端口,系统将 80 与 443 端口分别重定向到飞牛系统的 HTTP 端口 和 HTTPS 端口。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,27 +0,0 @@
|
||||
# 如何使用无线网卡?
|
||||
|
||||
fnOS 支持无线网卡,你可通过《 fnOS 支持网卡范围验证表》查看目前已验证兼容的网卡型号。
|
||||
|
||||
## 1. 启用无线网卡
|
||||
|
||||
使用管理员账号,打开 **系统设置** > **网络设置**,可以看到已接入的无线网卡。点击 **更多** > **启用无线网卡**。
|
||||
|
||||
## 2. 连接网络
|
||||
|
||||
启用后将自动扫描局域网内的网络。点击要连接的网络,输入密码,点击确定。
|
||||
|
||||
如果网络未被扫描发现,或为隐藏网络,你可以点击 **手动添加**,输入网络名称和密码来连接。
|
||||
|
||||
## 3. 切换网络
|
||||
|
||||
选择无线网卡,点击 **更多** > **编辑**,打开网络列表,点击或手动添加要切换的网络。
|
||||
|
||||
> **注意:**
|
||||
> 切换网络后IP将发生变化,可能会导致当前界面连接断开。建议:
|
||||
> - 若当前有其他已连接的网络,请记住其IP,在切换网络后通过该 IP 访问;
|
||||
> - 若已开启 FNConnect 服务,在切换网络后可通过 FNID 访问;
|
||||
> - 若你当前和设备位于同个局域网,在切换网络后可通过飞牛 App 扫描局域网内的设备以获取新 IP。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,56 +0,0 @@
|
||||
# 如何安装 App 并连接到飞牛 NAS?
|
||||
|
||||
本文将介绍如何安装飞牛 App 并且连接到飞牛 NAS。
|
||||
|
||||
## 1. 下载并安装 APP
|
||||
|
||||
### iOS 设备
|
||||
|
||||
1. 打开 AppStore 搜索"飞牛",找到飞牛 App;
|
||||
2. 下载并安装飞牛 App。
|
||||
|
||||
### Android 设备
|
||||
|
||||
1. 手机浏览器打开飞牛官网,点击"下载 App"开始下载 APK 应用文件;
|
||||
2. 安装飞牛 App。
|
||||
|
||||
## 2. 连接到飞牛 NAS
|
||||
|
||||
### 2.1 同意隐私协议
|
||||
|
||||
请同意隐私协议以使用飞牛 App。
|
||||
|
||||
### 2.2 选择访问方式
|
||||
|
||||
你可通过以下多种访问方式连接到 NAS:
|
||||
|
||||
**输入 IP 地址、域名或 FN ID**
|
||||
|
||||
- **IP 地址**:安装并且完成初始化系统后,可通过路由器管理中找到飞牛 NAS 的 IP 地址。
|
||||
- **DDNS 域名**:如果你在 fnOS 或路由器中配置了 DDNS 域名,可填写域名链接。
|
||||
- **FN ID**:如果你在 fnOS 中开启了 FN Connect 远程访问服务,可输入 FN ID 访问到飞牛 NAS,成功登录后,FN Connect 会自动安全地访问到飞牛 NAS。
|
||||
|
||||
> **注意:**
|
||||
> - HTTP 访问 fnOS 默认端口为 5666(V0.8.22 版本改为 5666,依旧支持 8000);
|
||||
> - HTTPS 访问 fnOS 默认端口为 5667(V0.8.22 版本改为 5667,依旧支持 8001);
|
||||
> - App 登录会根据 HTTPS 开关使用默认端口,如果你在路由器中修改了端口设置,需要在 IP 地址后填写端口以正确访问到飞牛 NAS,比如 `192.168.41.459:端口号`;
|
||||
> - FN ID 无需填写端口号。
|
||||
|
||||
**发现局域网内设备**
|
||||
|
||||
- 进入 App 后,点击"发现局域网内设备"可进入查找列表,会自动发现并展示设备同局域网下的飞牛 NAS 设备,选择要连接的 NAS 设备;
|
||||
|
||||
> **注意:**
|
||||
> - 使用"发现局域网内设备"功能需要开启设备的本地网络访问权限。
|
||||
|
||||
**历史登录**
|
||||
|
||||
- 如果该设备有历史登录记录,可通过"历史登录"快速填充访问信息及账号信息。
|
||||
|
||||
### 2.3 输入登录信息
|
||||
|
||||
输入飞牛 NAS 的登录账号和密码。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,93 +0,0 @@
|
||||
# 如何安装和初始化飞牛 fnOS?
|
||||
|
||||
你可以将 fnOS 安装部署到你的设备上,我们支持主流的 x86 架构硬件。以下是安装步骤:
|
||||
|
||||
## 1. 下载安装镜像
|
||||
|
||||
前往 [飞牛官网](https://www.fnnas.com/download?key=fnos) 下载最新的系统安装 iso 镜像。
|
||||
|
||||
## 2. 制作引导盘
|
||||
|
||||
此步骤需要在电脑上使用写盘工具将镜像文件烧录到 U盘中。U 盘在完成安装后就可推出。写盘工具可选择 Rufus ([Rufus4.5 下载 👍](https://static2.fnnas.com/installer/rufus-4.5.exe)、[Rufus 4.0 下载](https://static2.fnnas.com/installer/rufus-4.0.exe)、[Rufus 官网](https://rufus.ie/))、[Balenaetcher](https://etcher.balena.io/) 等。下文以 Rufus 为例:
|
||||
|
||||
1. **选择要写入的 U 盘**
|
||||
2. **选择下载好的镜像**
|
||||
3. **点击开始**
|
||||
|
||||
进度条显示 "准备就绪" 即为完成,可拔出 U 盘。
|
||||
|
||||
## 3. 将系统安装到设备上
|
||||
|
||||
### 3.1 设备准备
|
||||
|
||||
- 将主机接入至少一块硬盘用于做系统盘。系统盘推荐选择 SSD 固态硬盘以获得更快的系统运行速度。
|
||||
- 将主机接好网线。暂时不支持只有无线网卡的情况下安装,后续有计划支持。
|
||||
- 接好电源、显示器、键盘、鼠标,鼠标非必需。
|
||||
|
||||
### 3.2 选择引导盘
|
||||
|
||||
将制作好的引导盘插入到主机的接口。按主机电源开机,开机同时快速按键盘热键进入boot 设备选择界面。不同设备主板进入boot 页面的热键不同,F2、F7、F8、F10、F11、F12、esc 、Delete 键都有可能。
|
||||
|
||||
选择引导盘,按回车,进入安装页面。
|
||||
|
||||
### 3.3 选择安装方式
|
||||
|
||||
推荐选择 Graphical Install(图形化安装流程),回车进入下一步。
|
||||
|
||||
> **注意:**
|
||||
> - 如果页面提示 "暂不支持在当前模式下安装。请前往BIOS关闭 Secure Boot 后重试",请参考安装系统异常情况处理中第 1 点
|
||||
> - 如果在后续图形化安装步骤中遇到错误安装失败,请重启设备、在这一步选择 Rescuing Install(应急模式安装)重试。
|
||||
|
||||
### 3.4 选择系统盘
|
||||
|
||||
方向键切换选择要用于系统安装的硬盘,回车或 Alt+N 进入下一步。
|
||||
|
||||
### 3.5 设置系统分区和 Swap 大小
|
||||
|
||||
在系统盘容量充足的情况下,建议系统分区设置为 64 GB。如安装在虚拟机下,可按情况适当减小。Swap 分区大小已按物理内存大小计算推荐值,可按硬件情况自行调整。
|
||||
|
||||
> **注意:** 在 fnOS 上,系统将只安装在一块硬盘中。系统盘除系统分区以外的空间,可用于创建存储空间安装应用和存储文件。建议根据需要选择合适容量的 SSD 作为系统盘。
|
||||
|
||||
确认后,回车或 Alt+N 进入下一步。
|
||||
|
||||
### 3.6 确认格式化
|
||||
|
||||
所选择的系统盘将被格式化,所有内容将被删除,回车或 Alt+O 确认后开始安装。
|
||||
|
||||
### 3.7 设置网络
|
||||
|
||||
安装完成后将自动进入网络设置,系统将自动检测已连接的网卡并获取动态 IP。如网卡未开启 DHCP,你需要手动设置 IP。确认后,回车或 Alt+S 保存设置。
|
||||
|
||||
### 3.8 重启设备
|
||||
|
||||
网络设置保存成功后,你可以将引导盘从主机上拔出。回车或 Alt+O 重启系统。
|
||||
|
||||
### 3.9 重启成功进入终端
|
||||
|
||||
设备重启成功后将自动进入终端登录页,这意味着系统已成功安装并启动!请记住所设置的 IP 地址,接下来你将通过浏览器访问此地址来设置和使用 fnOS。现在你可以将显示器、键盘、鼠标从主机上拔除了。
|
||||
|
||||
## 4. 浏览器访问 fnOS
|
||||
|
||||
在电脑浏览器上访问上一步所设置的 IP 地址。首次进入 fnOS,你需要完成 2 项简单的系统初始化设置。点击 **开始 NAS 之旅** 进入初始化。
|
||||
|
||||
### 4.1 设置设备名称
|
||||
|
||||
设备名称支持数字、英文及短横线(-),不能以短横线开头或结尾。
|
||||
|
||||
### 4.2 设置管理员账号
|
||||
|
||||
fnOS 用户分为管理员用户和普通用户,且支持创建多个管理员。首位管理员为超级管理员,相较于其他管理员的特殊性在于:创建后不能删除、只能停用,且默认开启 SSH 权限。请设置高强度的密码以提高账号安全性。
|
||||
|
||||
确认后,点击 **进入fnOS** 完成设置。
|
||||
|
||||
### 4.3 进入桌面
|
||||
|
||||
建议尽快创建存储空间以便上传文件和安装应用。如果硬盘还没有准备好,也可以点击 **稍后** 跳过,后续可从 **设置** > **存储空间管理** 中创建。
|
||||
|
||||
## 5. 下载安装 App
|
||||
|
||||
推荐下载飞牛 App,具体方式请参考 [如何安装 App 并连接到飞牛 NAS?](./install-app.md)
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,60 +0,0 @@
|
||||
# 如何安装飞牛 TV 并连接到影视服务器?
|
||||
|
||||
## 前置准备
|
||||
|
||||
1. 需要安装并且初始化飞牛 fnOS 系统;
|
||||
2. 需要在飞牛 NAS 上,安装飞牛影视应用;
|
||||
3. 若需要使用 U 盘安装到安卓 TV,需要准备 U 盘用以拷贝飞牛 TV 安装包,并将 U 盘插到 TV 设备安装。
|
||||
|
||||
## 1. 下载和安装飞牛 TV
|
||||
|
||||
### 安卓 TV
|
||||
|
||||
以下有 2 种方式可下载安装飞牛 TV 到安卓电视:
|
||||
|
||||
**方式 1:U 盘安装**
|
||||
|
||||
1. 在[飞牛官网](https://www.fnnas.com)下载飞牛 TV 的安装包 (.apk 文件);
|
||||
2. 将安装包文件转移到 U 盘中;
|
||||
3. 将带有安装包文件的 U 盘插入 TV 设备,并在设备中安装飞牛 TV。
|
||||
|
||||
**方式 2:各大应用商店下载安装**
|
||||
|
||||
飞牛 TV 已上架以下安卓 TV 应用商店或电视品牌:
|
||||
- 当贝市场、创维、酷开、海尔、暴风
|
||||
- 欢视商店、TCL、长虹、东芝、三洋
|
||||
- OPPO、联想
|
||||
|
||||
> 注意:由于电视品牌的机型和商店政策差异,可能仅覆盖部分机型。若您所使用的电视或商店不在其中,请选择方式 1(U 盘安装)。
|
||||
|
||||
### Apple TV
|
||||
|
||||
1. 打开 Apple TV 中的 App Store;
|
||||
2. 搜索"飞牛TV",找到飞牛 TV App;
|
||||
3. 获取并安装飞牛 TV App。
|
||||
|
||||
## 2. 同意条款以继续使用
|
||||
|
||||
1. 完成安装后,在"应用"中找到飞牛 TV;
|
||||
2. 打开飞牛 TV,同意飞牛的**服务条款**和**隐私协议**以继续使用。
|
||||
|
||||
## 3. 连接影视服务器并登录
|
||||
|
||||
### 3.1 连接影视服务器
|
||||
|
||||
- **自动发现**:打开飞牛 TV 后,自动发现当前局域网下的飞牛影视服务器。
|
||||
> 注意:如果没有发现服务器,请检查设备开机情况、网络是否正常、影视服务器是否已创建。
|
||||
- **手动添加服务器**:输入飞牛影视服务器的 IP 地址或配置的 DDNS 域名。
|
||||
|
||||
### 3.2 登录影视账号
|
||||
|
||||
- **扫码登录**:打开飞牛 App,进入"首页 > 左上角头像 > 更多 > 扫码",扫描电视上的二维码,并在 App 上同意授权。
|
||||
- **账号密码登录**:手动填写影视账号和密码。
|
||||
|
||||
### 3.3 成功连接并登录
|
||||
|
||||
成功连接并登录后,即可进入飞牛 TV。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,45 +0,0 @@
|
||||
# 飞牛同步 - 常见使用问题说明
|
||||
|
||||
## 1. 新设备连接不上
|
||||
|
||||
- **原因**:飞牛同步默认使用HTTPS连接,需要正确输入HTTPS端口。
|
||||
- **解决方案**:如果使用IP连接且未修改端口,默认端口为5667。
|
||||
|
||||
## 2. 任务显示状态异常/账号异常
|
||||
|
||||
### NAS 连接问题
|
||||
|
||||
- **可能原因**:NAS关闭、HTTPS端口被修改、账号密码变更等导致连接中断。
|
||||
- **解决方法**:通过"编辑"按钮更新NAS连接方式,重新授权。
|
||||
|
||||
### 文件变更问题
|
||||
|
||||
- **可能原因**:文件被删除、移动或重命名导致同步异常。
|
||||
- **解决方法**:通过"恢复"按钮重启任务,重新同步文件。
|
||||
|
||||
## 3. 传输项目与文件数量对不上
|
||||
|
||||
- **原因1**:同步任务包含子文件夹,创建文件夹也会被统计为传输项目。
|
||||
- **原因2**:电脑端隐藏文件未被同步(需取消隐藏才能同步)。
|
||||
|
||||
## 4. 电脑端出现了冲突副本文件
|
||||
|
||||
- **常见场景**:
|
||||
- 多用户同时编辑同一文件。
|
||||
- 文件被占用(编辑或预览)。
|
||||
- 未基于最新版本修改文件。
|
||||
- **表现**:冲突文件会被重命名(含用户名、"冲突副本"字样和时间戳),且不会上传至NAS。
|
||||
|
||||
## 5. 电脑端文件一直没有被同步
|
||||
|
||||
- **原因**:文件被其他应用程序占用,操作系统限制无法监听。
|
||||
- **解决方法**:退出占用文件的应用程序后,同步任务会自动继续。
|
||||
|
||||
## 6. 已创建的同步任务是否可以编辑
|
||||
|
||||
- **可编辑项**:同步模式、文件过滤规则、NAS连接方式。
|
||||
- **不可编辑项**:电脑端和NAS端的同步文件夹路径(无法修改)。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,64 +0,0 @@
|
||||
# 飞牛同步功能介绍
|
||||
|
||||
## 1. 下载对应的客户端
|
||||
|
||||
### Windows
|
||||
|
||||
1. 电脑端浏览器打开飞牛官网,选中 Windows 下载应用文件;
|
||||
2. 安装飞牛同步应用。
|
||||
|
||||
### Mac
|
||||
|
||||
1. 电脑端浏览器打开飞牛官网,点击 Mac ,选择你的电脑对应的处理器版本下载应用文件;
|
||||
2. 安装飞牛同步应用。
|
||||
|
||||
## 2. 连接 NAS
|
||||
|
||||
### 连接新设备
|
||||
|
||||
点击左下角"创建新任务"打开"连接新 NAS 设备"弹窗,你可通过以下多种访问方式连接到 NAS:
|
||||
|
||||
1. **输入 IP 地址、域名或 FN ID**
|
||||
2. **发现局域网内设备**
|
||||
|
||||
> **注意:**
|
||||
> - 出于数据安全性考量,飞牛同步当前仅支持 HTTPS;
|
||||
> - HTTPS 访问 fnOS 默认端口为 5667。
|
||||
|
||||
## 3. 登录授权
|
||||
|
||||
连接 NAS 设备成功后需进行账号授权。
|
||||
|
||||
> **注意:**
|
||||
> - 授权后并不会将你的账号密码保存在客户端。
|
||||
|
||||
## 4. 创建任务
|
||||
|
||||
### 4.1 设置同步规则
|
||||
|
||||
1. **同步模式**:目前支持3种同步模式:
|
||||
- **双向同步**:NAS 端和电脑互为镜像,两端文件完全保持一致。
|
||||
- **仅下载**:仅同步 NAS 数据至电脑;NAS 文件删除时,电脑端文件将保留。
|
||||
- **仅上传**:仅同步电脑数据至 NAS;电脑端文件删除时,NAS 文件将保留。
|
||||
|
||||
2. **更多设置-设置过滤规则**:
|
||||
可以根据需要自行定义规则,排除指定大小或格式的文件。
|
||||
> **注意:** `tmp` 和 `temp` 为系统强制过滤的文件扩展名。
|
||||
|
||||
### 4.2 设置同步路径
|
||||
|
||||
- **电脑文件路径**:选择电脑端上要同步的文件夹路径。
|
||||
- **NAS 文件路径**:选择 NAS 端上要同步的文件夹路径。
|
||||
|
||||
## 5. 查看任务
|
||||
|
||||
1. **主页面查看**:可通过任务卡片查看此任务的同步进度和同步状态。
|
||||
2. **系统托盘查看**:可通过系统任务栏中飞牛同步的icon快速打开托盘。
|
||||
|
||||
## 6. 系统设置
|
||||
|
||||
可通过「设置」功能对飞牛同步进行更新、限速等设置。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,75 +0,0 @@
|
||||
# 如何安装和使用虚拟机?
|
||||
|
||||
## 1. 安装应用
|
||||
|
||||
### 1.1 应用中心安装虚拟机
|
||||
|
||||
- 使用管理员账号登录飞牛 fnOS。
|
||||
- 打开 **应用中心**,搜索并找到 **虚拟机** 应用。
|
||||
- 点击 **安装**。
|
||||
|
||||
### 1.2 确认安装位置
|
||||
|
||||
- 虚拟机应用默认安装在系统分区。
|
||||
- 确认后点击 **下一步**。
|
||||
|
||||
### 1.3 检查信息
|
||||
|
||||
- 点击 **确定** 开始安装。
|
||||
|
||||
### 1.4 安装完成
|
||||
|
||||
- 安装完成后,桌面会出现 **虚拟机** 应用图标。
|
||||
|
||||
## 2. 使用准备
|
||||
|
||||
### 2.1 网络设置
|
||||
|
||||
- 在 **系统设置 > 网络管理** 中,为有线网口或聚合网口启用 OVS(Open vSwitch),以提供虚拟网络支持(无线网口暂不支持)。
|
||||
|
||||
### 2.2 驱动程序下载
|
||||
|
||||
- [VirtIO Guest Tool 驱动程序(适用于 Windows 7)](https://iso.liveupdate.fnnas.com/help_static/images/virtio-win-0.1.173.iso)
|
||||
- [VirtIO Guest Tool 驱动程序(适用于 Windows 10 及以上)](https://iso.liveupdate.fnnas.com/help_static/images/virtio-win-0.1.266.iso)
|
||||
|
||||
### 2.3 开启 CPU 虚拟化支持
|
||||
|
||||
- 进入 BIOS/UEFI 设置(开机按 `Del`、`F2`、`F10` 或 `Esc`)。
|
||||
- 启用 **Intel Virtualization Technology**(Intel CPU)或 **SVM Mode**(AMD CPU)。
|
||||
- 保存设置并重启。
|
||||
|
||||
## 3. 创建虚拟机
|
||||
|
||||
### Windows 系统创建步骤
|
||||
|
||||
1. 在 **虚拟机** 应用中点击 **创建虚拟机**。
|
||||
2. 输入虚拟机名称,选择操作系统类型和版本。
|
||||
3. 配置镜像和参数(如 ISO 文件、驱动文件、CPU 核心、内存等)。
|
||||
4. 添加虚拟磁盘(最多 8 个),选择总线类型(如 VirtIO)和存储空间。
|
||||
5. 添加虚拟网络(最多 8 个),选择 OVS 网口和网络类型。
|
||||
6. 点击 **创建**,完成后启动虚拟机并通过 VNC 连接。
|
||||
|
||||
### Linux 系统创建步骤
|
||||
|
||||
1. 同上步骤创建虚拟机,选择 Linux 类型和版本。
|
||||
2. 配置镜像和参数(无需额外驱动,Linux 自带 VirtIO 驱动)。
|
||||
3. 添加虚拟磁盘和虚拟网络。
|
||||
4. 创建并启动虚拟机。
|
||||
|
||||
## 4. 操作系统安装流程
|
||||
|
||||
### Windows 11 安装注意事项
|
||||
|
||||
- 选择 **Q35 + UEFI** 配置(支持 TPM 2.0)。
|
||||
- 在启动时按任意键进入安装流程。
|
||||
- 如使用 VirtIO 磁盘/网络,需加载对应驱动程序。
|
||||
|
||||
### VirtIO 存储驱动程序安装
|
||||
|
||||
1. 在 Windows 安装界面点击"加载驱动程序"。
|
||||
2. 选择驱动 ISO 中的对应目录(如 `amd64/win11`)。
|
||||
3. 安装 **Red Hat VirtIO SCSI Controller**。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,51 +0,0 @@
|
||||
# 如何使用虚拟机硬件直通?
|
||||
|
||||
硬件直通是一种虚拟化技术,它允许虚拟机直接访问宿主机上的物理硬件设备,而不是通过虚拟化层进行仿真。
|
||||
|
||||
## 应用场景
|
||||
|
||||
1. **显卡直通**:支持物理显示器输出,提升图形处理性能;
|
||||
2. **声卡直通**:实现音响或耳机的高质量音频输出;
|
||||
3. **USB设备直通**:便于读取USB存储设备或连接其他USB设备;
|
||||
4. **网卡直通**:满足网络加速和复杂的家庭组网需求。
|
||||
|
||||
> **【硬件直通的风险和限制】**
|
||||
>
|
||||
> 硬件直通可能带来一定风险和限制,错误配置可能导致**设备冲突**或**系统崩溃**。在操作前,请充分了解以下风险:
|
||||
>
|
||||
> 1. **存储直通风险**:错误直通SATA、NVMe或USB存储设备可能损坏宿主机上的数据,且不可逆,请务必提前备份;
|
||||
> 2. **网卡直通风险**:请确保宿主机保留可用网卡,否则可能导致宿主机网络中断;
|
||||
> 3. **显卡直通风险**:直通显卡成功率并不高,且有可能造成宿主机崩溃,需谨慎操作。
|
||||
>
|
||||
> 不建议非专业用户尝试硬件直通操作。
|
||||
|
||||
## 1. 开启硬件直通选项
|
||||
|
||||
打开虚拟机应用,在侧边栏点击 **硬件**,找到 **硬件直通选项** 的 **开启** 按钮,在仔细阅读 **《硬件直通技术免责声明》** 后, 点击 **确认开启**。
|
||||
|
||||
## 2. 虚拟机配置硬件直通
|
||||
|
||||
在创建虚拟机或编辑虚拟机界面, **添加设备** 按钮在 **硬件直通选项** 开启后便激活可点击。
|
||||
|
||||
### 2.1 添加 USB 设备
|
||||
|
||||
1. 点击 **添加硬件** 按钮,点击 **添加 USB 设备**
|
||||
2. 选择需要直通给当前虚拟机的 USB 设备
|
||||
3. 点击 **确定** 按钮保存
|
||||
|
||||
### 2.2 添加 PCI 设备
|
||||
|
||||
1. 点击 **添加硬件** 按钮,点击 **添加 PCI 设备**
|
||||
2. 在列表中选择需要直通的 PCI 设备
|
||||
3. 由于 PCI 设备根据 IOMMU 分组进行内存隔离,所以直通时需要将同一 IOMMU 分组的 PCI 设备一并直通
|
||||
4. 点击 **确定** 按钮保存
|
||||
|
||||
## 3. 虚拟机详细日志指引
|
||||
|
||||
由于多方面因素影响,虚拟机可能启动失败,可在日志中查询相关详细原因。
|
||||
|
||||
日志路径:`/var/apps/trim.vm/var/log/error.log`
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,52 +0,0 @@
|
||||
# 如何创建存储空间?
|
||||
|
||||
本文详细介绍了在飞牛 fnOS 系统中创建存储空间的步骤,适用于管理员用户通过网页端或 App 操作。
|
||||
|
||||
## 创建存储空间的核心步骤
|
||||
|
||||
### 1. 选择文件系统
|
||||
|
||||
支持 Btrfs、ZFS 和 ext4 三种文件系统(自 V0.9.35 版本起)。用户需根据使用需求、硬件性能等因素选择。
|
||||
|
||||
### 2. 选择硬盘
|
||||
|
||||
从左侧列表中选择一个或多个硬盘。
|
||||
|
||||
> **注意:**
|
||||
> - 不同类型、接口协议、连接方式或逻辑扇区大小的硬盘不能混合组存储空间。
|
||||
> - 系统盘剩余容量可单独创建存储空间,但不能组 RAID 或扩容。
|
||||
> - 外接硬盘需确保接口牢固,避免因松动导致存储空间异常。
|
||||
|
||||
### 3. 选择存储模式
|
||||
|
||||
根据所选文件系统和硬盘数量,系统会动态显示可用的存储模式(如 RAID 类型)。
|
||||
|
||||
确认预计容量后点击"下一步"。
|
||||
|
||||
### 4. 设置用户权限
|
||||
|
||||
- **管理员**:默认无容量上限,如需限制需在"用户管理"中单独设置。
|
||||
- **普通用户**:可指定允许使用的用户及其容量上限。
|
||||
|
||||
### 5. 确认信息
|
||||
|
||||
可填写存储空间描述(可选),并选择是否创建后自动执行硬盘读写检测。
|
||||
|
||||
### 6. 确认格式化
|
||||
|
||||
系统会提示硬盘将被格式化,所有数据丢失。需输入管理员密码确认。
|
||||
|
||||
### 7. 创建成功
|
||||
|
||||
完成上述步骤后,存储空间即创建成功。
|
||||
|
||||
## 关键注意事项
|
||||
|
||||
- **兼容性**:硬盘需满足类型、接口等一致性要求。
|
||||
- **系统盘限制**:系统盘剩余空间不能用于 RAID 或扩容。
|
||||
- **外接硬盘风险**:需确保接口稳定,避免因松动引发问题。
|
||||
- **权限管理**:管理员可全局访问存储空间,普通用户需单独授权。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,31 +0,0 @@
|
||||
# 硬盘会自动休眠吗?
|
||||
|
||||
fnOS 支持硬盘在闲时自动休眠,包括除了系统盘外的内置盘和支持休眠的外接盘。默认开启,修改设置方式如下:
|
||||
|
||||
使用管理员账号打开网页端 **设置** > **硬盘信息** > **硬盘休眠**,设置内置和外接硬盘进入休眠的时间。
|
||||
|
||||
## 硬盘自动唤醒偏好
|
||||
|
||||
自系统版本 v0.9.10 起新增支持设置**硬盘自动唤醒偏好**。当开启了硬盘休眠后,可选择硬盘唤醒的策略:
|
||||
|
||||
### 1. 按需唤醒:仅在对存储空间读写数据时
|
||||
|
||||
选择此选项后,系统只在读写数据时唤醒硬盘,不被读写的硬盘仍将保持休眠状态。
|
||||
|
||||
### 2. 响应更快:有用户登录即唤醒
|
||||
|
||||
选择此选项后,除了读写数据会唤醒硬盘外,有用户登录网页端、App时,系统也将自动唤醒全部硬盘,以提升读写操作时的响应速度,使用体验更丝滑。
|
||||
|
||||
> **注意:**
|
||||
>
|
||||
> - 常见会触发读写硬盘的操作有:
|
||||
> - 访问文件管理;
|
||||
> - 硬盘所在的存储空间上有正在执行的文件任务,包括上传、下载、移动、复制等;
|
||||
> - 应用或 Docker 安装在硬盘所在的存储空间上,且正在运行产生传输;
|
||||
> - 应用或 Docker 正在访问存储空间上的文件,包括影视刮削播放、相册查看和 AI 识别等;
|
||||
> - 下载类应用或 Docker 容器中有任务正在下载或做种。
|
||||
> - 除了读写文件外,访问 **系统设置 > 存储空间管理** 和 **硬盘信息** 页面时,由于需要查询存储相关信息,也会唤醒已被用于存储空间的所有硬盘。如不希望唤醒,可通过 **资源监控** 查看硬盘状态和信息。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
@@ -1,62 +0,0 @@
|
||||
# 如何使用 SSD 缓存加速?
|
||||
|
||||
SSD 缓存加速功能是指将固态硬盘 (SSD) 作为存储空间的缓存,从而将频繁访问的数据缓存到高速 SSD 上,以此提升日常使用中的读写性能。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 创建 SSD 缓存的前提条件是:有至少1 块内置的固态硬盘 (SSD) 未被用于创建存储空间,且容量大于10G。系统盘除系统分区外的剩余空间如果满足上述条件,也可以被用于创建 SSD 缓存。这将增加系统盘的读写,因此可能缩短系统盘的寿命,请谨慎选择。
|
||||
2. 一个 SSD 缓存可以被分配给多个存储空间。创建时分配容量有剩余的,创建完成后可以继续分配给其他存储空间。
|
||||
3. 多块硬盘组成 SSD 时可选择阵列模式,以获得数据保护或速度上的提升。
|
||||
4. 有 2 种缓存模式可选:只读缓存和读写缓存(ZFS 下为写入缓存)。不稳定的盘做读写缓存容易造成数据丢失、导致整个存储空间损坏,请谨慎选择。
|
||||
5. 如要删除读写模式的 SSD 缓存,请务必从界面上操作,切勿直接将 SSD 从设备中拔出,否则数据可能会丢失。
|
||||
6. 暂不支持对已经创建好的缓存进行扩容、更换硬盘、修改存储模式等操作,如有需要,请删除缓存后重建。
|
||||
|
||||
## 创建 SSD 缓存
|
||||
|
||||
登录管理员账号,从桌面打开 **系统设置** > **存储空间管理** > **SSD 缓存加速** ,点击 **创建SSD 缓存**。
|
||||
|
||||
### 1. 选择存储空间
|
||||
|
||||
选择要使用 SSD 缓存的存储空间。
|
||||
|
||||
请注意,对于 Btrfs 和 ext4 的存储空间,虽然创建时只能选择一个存储空间,但如果分配后缓存的剩余容量大于 10G,你仍可将剩余容量分配给其他存储空间,分配方式详见下文《3. 分配 SSD 缓存容量》。而对于 ZFS 的存储空间,则无法多个存储空间共用一个 SSD 缓存。
|
||||
|
||||
### 2. 选择缓存模式
|
||||
|
||||
对于 Btrfs 和 ext4 的存储空间,可选择只读缓存或读写缓存。只读缓存只缓存常读取的数据,适用于读多写少的场景。读写缓存适用于高频写入的场景。
|
||||
|
||||
对于 ZFS 的存储空间,可选择只读缓存(L2ARC)或读写缓存(SLOG)。ZFS 默认使用物理内存作为一级缓存(ARC)。添加 L2ARC 将加速常用文件读取,适合读多写少的场景,但将增加内存占用。SLOG 将加速同步写入操作,适合频繁写入或虚拟化等需要高数据一致性的场景。
|
||||
|
||||
### 3. 选择硬盘和存储模式
|
||||
|
||||
在页面左侧选择要用于创建 SSD 缓存的 1 个或多个硬盘。根据所选的硬盘个数,右侧将动态显示可供选择的存储模式。选择存储模式后,可以在弹窗底部确认缓存的预计容量情况。确认无误后,点击 **下一步**。
|
||||
|
||||
> 注意:如果缓存的存储模式不具备数据保护能力(如 Basic、RAID 0),且选择了读写缓存(或 ZFS 的写入缓存)模式,则在 SSD 损坏、掉盘或非正常移除时,有较大概率导致数据丢失,请谨慎选择。
|
||||
|
||||
### 4. 分配缓存容量
|
||||
|
||||
对于 Btrfs 和 ext4 的存储空间,可指定要分配给此存储空间的缓存容量,剩余容量可分配给其他存储空间。
|
||||
|
||||
### 5. 确认信息
|
||||
|
||||
确认所选的存储空间、缓存模式、硬盘及存储模式、分配容量等信息。
|
||||
|
||||
### 6. 确认格式化
|
||||
|
||||
用于组缓存的硬盘将被格式化,所有数据将被删除。请输入管理员密码进行最终确认。
|
||||
|
||||
### 7. 创建成功
|
||||
|
||||
缓存创建完成后,可以在 SSD 缓存加速页面查看缓存状态。
|
||||
|
||||
## 分配 SSD 缓存容量
|
||||
|
||||
对于 Btrfs 和 ext4 的存储空间,多个存储空间可共用同个 SSD 缓存。当 SSD 缓存存在未分配的分区时,点击 **分配**,选择存储空间、缓存模式及分配容量,点击确认。输入管理员密码,点击开始分配。分配成功后,缓存容量将被分配给指定存储空间。
|
||||
|
||||
## 删除 SSD 缓存
|
||||
|
||||
当缓存仅被分配给一个存储空间时,点击 **缓存** > **更多** > **删除**,可删除缓存。当缓存被分配给多个存储空间时,点击 **缓存空间容量分配** > **移除**,分别将所有分区移除后,缓存将自动被删除。请注意,移除读写缓存时需要将已缓存的数据回写到存储空间中,这可能会需要一些时间。
|
||||
|
||||
---
|
||||
|
||||
*来源:飞牛 fnOS 帮助中心*
|
||||
Reference in New Issue
Block a user