spec: add BEHAVIOR_SPEC and fix B-01~B-10 (resolved/decay/scoring)

- Add BEHAVIOR_SPEC.md as full system behaviour reference
- B-01: stop auto-archiving resolved buckets in update()
- B-03: keep activation_count as float in calculate_score
- B-04: initialise activation_count=0 on create
- B-05: time score coefficient 0.1 -> 0.02
- B-06: w_time default 2.5 -> 1.5
- B-07: content_weight default 3.0 -> 1.0
- B-08: refresh local meta after auto_resolve
- B-09: user-supplied valence/arousal takes priority over analyze()
- B-10: allow empty domain for feel buckets
- Refresh INTERNALS/README/dashboard accordingly
This commit is contained in:
P0luz
2026-04-21 18:45:52 +08:00
parent c7ddfd46ad
commit ccdffdb626
10 changed files with 1186 additions and 36 deletions

View File

@@ -607,6 +607,7 @@
<div class="search-bar">
<input type="text" id="search-input" placeholder="搜索记忆…" />
</div>
<button onclick="doLogout()" title="退出登录" style="margin-left:12px;background:none;border:1px solid var(--border);color:var(--text-dim);border-radius:20px;padding:6px 14px;font-size:12px;cursor:pointer;">退出</button>
</div>
<div class="tabs">
@@ -615,6 +616,7 @@
<div class="tab" data-tab="network">记忆网络</div>
<div class="tab" data-tab="config">配置</div>
<div class="tab" data-tab="import">导入</div>
<div class="tab" data-tab="settings">设置</div>
</div>
<div class="content" id="list-view">
@@ -778,7 +780,186 @@
<div id="detail-content"></div>
</div>
<!-- Settings Tab View -->
<div class="content" id="settings-view" style="display:none">
<div style="max-width:580px;margin:0 auto;">
<div class="config-section">
<h3>服务状态</h3>
<div id="settings-status" style="font-size:13px;color:var(--text-dim);line-height:2;">加载中…</div>
<button onclick="loadSettingsStatus()" style="margin-top:8px;font-size:12px;padding:4px 12px;">刷新状态</button>
</div>
<div class="config-section">
<h3>修改密码</h3>
<div id="settings-env-notice" style="display:none;font-size:12px;color:var(--warning);margin-bottom:10px;">
⚠ 当前使用环境变量 OMBRE_DASHBOARD_PASSWORD请直接修改环境变量。
</div>
<div id="settings-pwd-form">
<div class="config-row">
<label>当前密码</label>
<input type="password" id="settings-current-pwd" placeholder="当前密码" />
</div>
<div class="config-row">
<label>新密码</label>
<input type="password" id="settings-new-pwd" placeholder="新密码至少6位" />
</div>
<div class="config-row">
<label>确认新密码</label>
<input type="password" id="settings-new-pwd2" placeholder="再次输入新密码" />
</div>
<button class="btn-primary" onclick="changePassword()" style="margin-top:4px;">修改密码</button>
<div id="settings-pwd-msg" style="margin-top:10px;font-size:13px;"></div>
</div>
</div>
<div class="config-section">
<h3>账号操作</h3>
<button onclick="doLogout()" style="color:var(--negative);border-color:var(--negative);">退出登录</button>
</div>
</div>
</div>
<!-- Auth Overlay -->
<div id="auth-overlay" style="position:fixed;inset:0;z-index:9999;background:var(--bg-gradient);background-attachment:fixed;display:flex;align-items:center;justify-content:center;">
<div style="background:var(--surface);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:24px;padding:48px 40px;max-width:400px;width:90%;text-align:center;box-shadow:0 8px 40px rgba(0,0,0,0.12);">
<h2 style="font-family:'Cormorant Garamond',serif;font-size:28px;color:var(--accent);margin-bottom:8px;">◐ Ombre Brain</h2>
<p style="color:var(--text-dim);font-size:13px;margin-bottom:28px;" id="auth-subtitle">验证身份</p>
<!-- Setup form -->
<div id="auth-setup-form" style="display:none;">
<p style="font-size:13px;color:var(--text-dim);margin-bottom:16px;">首次使用,请设置访问密码</p>
<input type="password" id="auth-setup-pwd" placeholder="设置密码至少6位" style="display:block;width:100%;padding:10px 16px;border:1px solid var(--border);border-radius:10px;background:var(--surface-solid);color:var(--text);font-size:14px;margin-bottom:10px;" />
<input type="password" id="auth-setup-pwd2" placeholder="确认密码" style="display:block;width:100%;padding:10px 16px;border:1px solid var(--border);border-radius:10px;background:var(--surface-solid);color:var(--text);font-size:14px;margin-bottom:16px;" onkeydown="if(event.key==='Enter')doSetup()" />
<button onclick="doSetup()" style="width:100%;padding:12px;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:14px;cursor:pointer;">设置密码并进入</button>
</div>
<!-- Login form -->
<div id="auth-login-form" style="display:none;">
<input type="password" id="auth-login-pwd" placeholder="输入访问密码" style="display:block;width:100%;padding:10px 16px;border:1px solid var(--border);border-radius:10px;background:var(--surface-solid);color:var(--text);font-size:14px;margin-bottom:16px;" onkeydown="if(event.key==='Enter')doLogin()" />
<button onclick="doLogin()" style="width:100%;padding:12px;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:14px;cursor:pointer;">登录</button>
</div>
<div id="auth-error" style="color:var(--negative);font-size:13px;margin-top:12px;display:none;"></div>
</div>
</div>
<script>
// ========================================
// Auth system / 认证系统
// ========================================
async function checkAuth() {
try {
const resp = await fetch('/auth/status');
const data = await resp.json();
if (data.setup_needed) {
document.getElementById('auth-subtitle').textContent = '首次设置';
document.getElementById('auth-setup-form').style.display = 'block';
} else if (data.authenticated) {
document.getElementById('auth-overlay').style.display = 'none';
} else {
document.getElementById('auth-subtitle').textContent = '请输入访问密码';
document.getElementById('auth-login-form').style.display = 'block';
}
} catch {
document.getElementById('auth-overlay').style.display = 'none';
}
}
function showAuthError(msg) {
const el = document.getElementById('auth-error');
el.textContent = msg;
el.style.display = 'block';
}
async function doSetup() {
const p1 = document.getElementById('auth-setup-pwd').value;
const p2 = document.getElementById('auth-setup-pwd2').value;
if (p1.length < 6) return showAuthError('密码至少6位');
if (p1 !== p2) return showAuthError('两次密码不一致');
const resp = await fetch('/auth/setup', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({password: p1}) });
if (resp.ok) {
document.getElementById('auth-overlay').style.display = 'none';
} else {
const d = await resp.json();
showAuthError(d.detail || '设置失败');
}
}
async function doLogin() {
const pwd = document.getElementById('auth-login-pwd').value;
const resp = await fetch('/auth/login', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({password: pwd}) });
if (resp.ok) {
document.getElementById('auth-overlay').style.display = 'none';
} else {
const d = await resp.json();
showAuthError(d.detail || '密码错误');
}
}
async function doLogout() {
await fetch('/auth/logout', { method: 'POST' });
document.getElementById('auth-setup-form').style.display = 'none';
document.getElementById('auth-login-form').style.display = 'none';
document.getElementById('auth-login-form').style.display = 'block';
document.getElementById('auth-subtitle').textContent = '请输入访问密码';
document.getElementById('auth-error').style.display = 'none';
document.getElementById('auth-overlay').style.display = 'flex';
}
async function changePassword() {
const currentPwd = document.getElementById('settings-current-pwd').value;
const newPwd = document.getElementById('settings-new-pwd').value;
const newPwd2 = document.getElementById('settings-new-pwd2').value;
const msgEl = document.getElementById('settings-pwd-msg');
if (newPwd.length < 6) { msgEl.style.color = 'var(--negative)'; msgEl.textContent = '新密码至少6位'; return; }
if (newPwd !== newPwd2) { msgEl.style.color = 'var(--negative)'; msgEl.textContent = '两次密码不一致'; return; }
const resp = await authFetch('/auth/change-password', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({current: currentPwd, new: newPwd}) });
if (!resp) return;
if (resp.ok) {
msgEl.style.color = 'var(--accent)'; msgEl.textContent = '密码修改成功';
document.getElementById('settings-current-pwd').value = '';
document.getElementById('settings-new-pwd').value = '';
document.getElementById('settings-new-pwd2').value = '';
} else {
const d = await resp.json();
msgEl.style.color = 'var(--negative)'; msgEl.textContent = d.detail || '修改失败';
}
}
async function loadSettingsStatus() {
const el = document.getElementById('settings-status');
try {
const resp = await authFetch('/api/status');
if (!resp) return;
const d = await resp.json();
const noticeEl = document.getElementById('settings-env-notice');
if (d.using_env_password) noticeEl.style.display = 'block';
else noticeEl.style.display = 'none';
el.innerHTML = `
<b>版本</b>${d.version}<br>
<b>Bucket 总数</b>${(d.buckets?.total ?? 0)} (永久:${d.buckets?.permanent ?? 0} / 动态:${d.buckets?.dynamic ?? 0} / 归档:${d.buckets?.archive ?? 0}<br>
<b>衰减引擎</b>${d.decay_engine}<br>
<b>向量搜索</b>${d.embedding_enabled ? '已启用' : '未启用'}<br>
`;
} catch(e) {
el.textContent = '加载失败: ' + e;
}
}
// authFetch: wraps fetch, shows auth overlay on 401
async function authFetch(url, options) {
const resp = await fetch(url, options);
if (resp.status === 401) {
doLogout();
return null;
}
return resp;
}
// ========================================
const BASE = location.origin;
let allBuckets = [];
let currentFilter = 'all';
@@ -793,9 +974,11 @@ document.querySelectorAll('.tab').forEach(tab => {
document.getElementById('network-view').style.display = target === 'network' ? '' : 'none';
document.getElementById('config-view').style.display = target === 'config' ? '' : 'none';
document.getElementById('import-view').style.display = target === 'import' ? '' : 'none';
document.getElementById('settings-view').style.display = target === 'settings' ? '' : 'none';
if (target === 'network') loadNetwork();
if (target === 'config') loadConfig();
if (target === 'import') { pollImportStatus(); loadImportResults(); }
if (target === 'settings') loadSettingsStatus();
});
});
@@ -1237,7 +1420,7 @@ async function saveConfig(persist) {
}
}
loadBuckets();
checkAuth().then(() => loadBuckets());
// --- Import functions ---
const uploadZone = document.getElementById('import-upload-zone');
@@ -1300,6 +1483,7 @@ function updateImportUI(s) {
document.getElementById('import-status-text').textContent = statusMap[s.status] || s.status;
document.getElementById('import-pause-btn').style.display = s.status === 'running' ? '' : 'none';
if (s.status !== 'running') clearInterval(importPollTimer);
if (s.status === 'completed') loadImportResults();
const errDiv = document.getElementById('import-errors');
if (s.errors && s.errors.length) {
errDiv.style.display = '';