代码修复: - web/agent/agent.py: datetime.utcnow() → datetime.now(timezone.utc) - requirements.txt: 移除 paramiko==3.5.0 (已无活跃引用) - 删除 server/infrastructure/ssh/pool.py (DEPRECATED, 无引用) 连接池三层对齐 (config.py/.env.example/session.py): - DB_POOL_SIZE 100→160, DB_MAX_OVERFLOW 100→120 - 基于 MySQL max_connections=400, install.php 公式 文档修复 (21项): - P0: 硬编码域名/IP替换为配置变量占位符 - P1: 10个过时设计文档加归档标注 (引用旧文件结构) - P2: step-3-webssh报告 paramiko引用修正 - 6份审查报告连接池参数勘误 100/100→160/120 - ECC安全报告 EC5 datetime.utcnow 标记已修复 - docs/README.md 文档索引重写 - docs/memory/mem_nexus_overview.md 移除硬编码凭证 - docs/project/tech-stack-inventory.md paramiko标记已移除 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.9 KiB
密码预设 实现计划
⚠️ 已归档 — 2026-05-22 本文档引用的文件结构已过时:
server/database.py已拆分为server/domain/models/;PHP 前端 (servers.php/settings.php) 已迁移到 Tailwind CSS v4 + Alpine.js (web/app/servers.html/settings.html);认证已从 X-API-Key 改为 JWT。功能已实现,保留本文档供历史参考。
面向 AI 代理的工作者: 必需子技能:使用 superpowers:executing-plans 逐任务实现此计划。
目标: 提供加密存储的 SSH 密码预设列表,新建服务器时下拉选择自动填充
架构: PasswordPreset 模型复用 Fernet 加密;4 个 REST 端点挂在 servers 路由下;servers.php 加下拉联动;settings.php 加管理面板
技术栈: Python FastAPI + SQLAlchemy + PHP 7.4 + Fernet
任务 1:数据模型 + API 端点
文件: server/database.py、server/api/servers.py
- 步骤 1:database.py 添加 PasswordPreset 模型
在 Setting 类之后、init_db() 之前插入:
class PasswordPreset(Base):
"""SSH password preset — encrypted storage"""
__tablename__ = "password_presets"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="预设名称")
encrypted_pw = Column(String(500), nullable=False, comment="加密后的密码")
created_at = Column(DateTime, default=datetime.datetime.utcnow)
- 步骤 2:servers.py 添加 4 个预设端点
在 servers.py 导入 PasswordPreset:
from server.database import get_db, Server, SyncLog, PasswordPreset, encrypt_value, decrypt_value
在文件末尾(PushRequest 类之前)添加:
# --- Password Presets ---
class PresetCreate(BaseModel):
name: str
password: str
class PresetResponse(BaseModel):
id: int
name: str
@router.get("/password-presets", response_model=List[PresetResponse])
def list_presets(db: Session = Depends(get_db)):
return db.query(PasswordPreset).order_by(PasswordPreset.id).all()
@router.post("/password-presets", response_model=PresetResponse)
def create_preset(data: PresetCreate, db: Session = Depends(get_db)):
preset = PasswordPreset(name=data.name, encrypted_pw=encrypt_value(data.password))
db.add(preset)
db.commit()
db.refresh(preset)
return preset
@router.delete("/password-presets/{preset_id}")
def delete_preset(preset_id: int, db: Session = Depends(get_db)):
preset = db.query(PasswordPreset).filter(PasswordPreset.id == preset_id).first()
if not preset:
raise HTTPException(status_code=404, detail="Preset not found")
db.delete(preset)
db.commit()
return {"message": "deleted"}
@router.post("/password-presets/{preset_id}/reveal")
def reveal_preset(preset_id: int, db: Session = Depends(get_db)):
preset = db.query(PasswordPreset).filter(PasswordPreset.id == preset_id).first()
if not preset:
raise HTTPException(status_code=404, detail="Preset not found")
return {"password": decrypt_value(preset.encrypted_pw)}
- 步骤 3:验证编译 + Commit
python -m py_compile server/database.py server/api/servers.py && echo "OK"
git add server/database.py server/api/servers.py
git commit -m "feat(presets): PasswordPreset 模型 + 4 个 CRUD/reveal 端点"
任务 2:settings.php 密码预设管理面板
文件: web/settings.php
- 步骤 1:在 settings.php 末尾(
</body>前)添加预设管理 HTML + JS
找到 settings.php 中「修改密码」section 之后,添加新 section:
<!-- 密码预设管理 -->
<div class="settings-section">
<h3><i class="fas fa-key"></i> SSH 密码预设</h3>
<p style="font-size:13px;color:#64748b;margin-bottom:12px;">保存常用 SSH 密码,新建服务器时快速选择</p>
<div id="presetList" style="margin-bottom:16px;">加载中...</div>
<form onsubmit="addPreset(event)" style="display:flex;gap:8px;align-items:flex-end;">
<div class="form-group" style="flex:1;margin:0;">
<label>名称</label>
<input type="text" id="newPresetName" class="form-control" placeholder="如: 生产环境" required>
</div>
<div class="form-group" style="flex:1;margin:0;">
<label>密码</label>
<input type="password" id="newPresetPw" class="form-control" placeholder="SSH 密码" required>
</div>
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> 添加</button>
</form>
</div>
<script>
const API_BASE = <?= json_encode(defined('API_BASE_URL') ? API_BASE_URL : 'http://localhost:8600') ?>;
const API_KEY = <?= json_encode(defined('API_KEY') ? API_KEY : '') ?>;
function loadPresets() {
fetch(API_BASE + '/api/servers/password-presets', { headers: API_KEY ? {'X-API-Key': API_KEY} : {} })
.then(r => r.json()).then(data => {
const presets = data.data || data;
let html = '';
if (!presets.length) html = '<p style="color:#9ca3af;">暂无预设</p>';
presets.forEach(p => {
html += '<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #f3f4f6;">' +
'<span style="flex:1;">' + esc(p.name) + '</span>' +
'<button class="btn btn-sm btn-info" onclick="revealPreset(' + p.id + ', this)"><i class="fas fa-eye"></i> 查看</button>' +
'<button class="btn btn-sm btn-danger" onclick="deletePreset(' + p.id + ')"><i class="fas fa-trash"></i></button></div>';
});
document.getElementById('presetList').innerHTML = html;
});
}
function addPreset(e) {
e.preventDefault();
const name = document.getElementById('newPresetName').value.trim();
const pw = document.getElementById('newPresetPw').value;
if (!name || !pw) return;
fetch(API_BASE + '/api/servers/password-presets', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
body: JSON.stringify({ name, password: pw })
}).then(r => r.json()).then(() => {
document.getElementById('newPresetName').value = '';
document.getElementById('newPresetPw').value = '';
loadPresets();
});
}
function revealPreset(id, btn) {
fetch(API_BASE + '/api/servers/password-presets/' + id + '/reveal', {
method: 'POST', headers: { 'X-API-Key': API_KEY }
}).then(r => r.json()).then(data => {
const pw = (data.data || data).password;
btn.outerHTML = '<code style="background:#fef3c7;padding:2px 8px;border-radius:4px;">' + esc(pw) + '</code>';
setTimeout(loadPresets, 5000);
});
}
function deletePreset(id) {
if (!confirm('确定删除此预设?')) return;
fetch(API_BASE + '/api/servers/password-presets/' + id, {
method: 'DELETE', headers: { 'X-API-Key': API_KEY }
}).then(() => loadPresets());
}
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
loadPresets();
</script>
- 步骤 2:Commit
git add web/settings.php
git commit -m "feat(presets): settings.php 密码预设管理面板"
任务 3:servers.php 预设下拉联动
文件: web/servers.php
- 步骤 1:在密码字段上方添加预设下拉
在 passwordField div 内部,密码 input 之前添加:
<div id="presetSelector" style="margin-bottom:8px;">
<label style="font-size:12px;color:#6b7280;">密码预设</label>
<select id="presetSelect" class="form-control" onchange="fillPresetPassword()" style="font-size:13px;">
<option value="">自定义(手动输入)</option>
</select>
</div>
- 步骤 2:添加 JS 加载预设列表并联动填充
在现有 <script> 块末尾添加:
function loadPresetOptions() {
fetch(API_BASE + '/api/servers/password-presets', { headers: API_KEY ? {'X-API-Key': API_KEY} : {} })
.then(r => r.json()).then(data => {
const presets = data.data || data;
const sel = document.getElementById('presetSelect');
presets.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
sel.appendChild(opt);
});
});
}
function fillPresetPassword() {
const id = document.getElementById('presetSelect').value;
if (!id) return;
fetch(API_BASE + '/api/servers/password-presets/' + id + '/reveal', {
method: 'POST', headers: { 'X-API-Key': API_KEY }
}).then(r => r.json()).then(data => {
const pw = (data.data || data).password;
document.querySelector('[name="password"]').value = pw;
});
}
loadPresetOptions();
- 步骤 3:删除旧的 localStorage 逻辑
删除 servers.php 中现有的 localStorage 代码(nexus_default_ssh_pw 相关 4 行),已被预设替代。
- 步骤 4:Commit
git add web/servers.php
git commit -m "feat(presets): servers.php 预设下拉联动填充密码"
任务 4:验证 + 更新 CHANGELOG
- 步骤 1:全量编译验证
find server -name "*.py" | xargs python -m py_compile && echo "All Python OK"
- 步骤 2:docs/CHANGELOG.md 追加记录
### 密码预设功能
- 新增 PasswordPreset 表,Fernet 加密存储
- 4 个 API 端点(列表/创建/删除/解密查看)
- settings.php 密码预设管理面板
- servers.php 预设下拉自动填充密码
- 步骤 3:Commit
git add docs/CHANGELOG.md
git commit -m "docs: 更新 CHANGELOG — 密码预设功能"
自检
- 规格全覆盖:模型 ✓ 4 端点 ✓ servers 下拉 ✓ settings 管理 ✓
- 无占位符/TODO
- 类型一致:
PresetCreate/PresetResponse与端点签名匹配 - decrypt_value 导入正确(通过 database 模块)