feat: purge legacy agent hosts and land vault Chrome extension path
Retire ACP/Hermes/OpenCode surfaces and rename hermes_tools to mnote_agent_tools so Page AI stays on Pi Lab only. Add Chrome vault extension + extension token route, pre-release purge design, and soft-retire legacy smokes for the small-group production cut.
This commit is contained in:
@@ -163,12 +163,17 @@
|
||||
|
||||
function ensureFormUi() {
|
||||
if (!state._formUi) {
|
||||
state._formUi = { openAccountIds: {}, expandedSecretPanels: {} };
|
||||
state._formUi = {
|
||||
openAccountIds: {},
|
||||
expandedSecretPanels: {},
|
||||
folderPickerOpen: {},
|
||||
};
|
||||
}
|
||||
if (!state._formUi.openAccountIds) state._formUi.openAccountIds = {};
|
||||
if (!state._formUi.expandedSecretPanels) {
|
||||
state._formUi.expandedSecretPanels = {};
|
||||
}
|
||||
if (!state._formUi.folderPickerOpen) state._formUi.folderPickerOpen = {};
|
||||
return state._formUi;
|
||||
}
|
||||
|
||||
@@ -892,6 +897,8 @@
|
||||
kind: (s && s.kind) || 'apikey',
|
||||
label: (s && s.label) || '',
|
||||
value: (s && s.value) || { state: 'absent' },
|
||||
// Draft-only flag from reRenderFormKeepingSlots; absent on server items.
|
||||
valueClear: !!(s && s.valueClear),
|
||||
accountId: (s && (s.accountId || s.account_id)) || '',
|
||||
};
|
||||
}
|
||||
@@ -924,6 +931,96 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 将 ISO 时间格式化为本地可读短串;无效则原样返回。 */
|
||||
function formatVaultDateTime(raw) {
|
||||
var s = String(raw || '').trim();
|
||||
if (!s) return '';
|
||||
var d = new Date(s);
|
||||
if (isNaN(d.getTime())) return s;
|
||||
try {
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
} catch (_e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号级登录态展示(扩展 Cookie session)。
|
||||
* 优先账号槽位字段;合成单账号时回退条目级 hasLoginSession / sessionUpdatedAt。
|
||||
*/
|
||||
function accountSessionView(acc, item, isSyntheticSingle) {
|
||||
var has =
|
||||
acc && typeof acc.hasLoginSession === 'boolean'
|
||||
? acc.hasLoginSession
|
||||
: isSyntheticSingle && item
|
||||
? !!item.hasLoginSession
|
||||
: false;
|
||||
var savedAt =
|
||||
(acc && (acc.lastLoginAt || acc.sessionUpdatedAt)) ||
|
||||
(isSyntheticSingle && item
|
||||
? item.lastLoginAt || item.sessionUpdatedAt || ''
|
||||
: '') ||
|
||||
'';
|
||||
var expiresAt =
|
||||
(acc && acc.sessionExpiresAt) ||
|
||||
(isSyntheticSingle && item ? item.sessionExpiresAt || '' : '') ||
|
||||
'';
|
||||
return {
|
||||
hasLoginSession: !!has,
|
||||
savedAt: String(savedAt || '').trim(),
|
||||
expiresAt: String(expiresAt || '').trim(),
|
||||
source: (acc && acc.sessionSource) || '',
|
||||
};
|
||||
}
|
||||
|
||||
/** 详情页:账号下「是否保存登录态 + 保存日期」行。 */
|
||||
function renderAccountSessionRows(sess, showEmpty) {
|
||||
if (!sess.hasLoginSession && !showEmpty) {
|
||||
// 未保存时也简短展示一行,避免用户以为功能缺失
|
||||
return (
|
||||
'<div class="mnote-vault-field-row mnote-vault-session-row" data-testid="vault-account-session">' +
|
||||
'<label>登录态</label>' +
|
||||
'<div><span class="mnote-vault-session-badge is-absent">未保存</span></div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
var badge = sess.hasLoginSession
|
||||
? '<span class="mnote-vault-session-badge is-saved" data-testid="vault-session-saved">已保存</span>'
|
||||
: '<span class="mnote-vault-session-badge is-absent" data-testid="vault-session-absent">未保存</span>';
|
||||
var dateText = sess.hasLoginSession
|
||||
? formatVaultDateTime(sess.savedAt) || '—'
|
||||
: '—';
|
||||
var html =
|
||||
'<div class="mnote-vault-field-row mnote-vault-session-row" data-testid="vault-account-session">' +
|
||||
'<label>登录态</label>' +
|
||||
'<div class="mnote-vault-session-meta">' +
|
||||
badge +
|
||||
'</div></div>' +
|
||||
'<div class="mnote-vault-field-row mnote-vault-session-row" data-testid="vault-account-session-date">' +
|
||||
'<label>保存日期</label>' +
|
||||
'<div>' +
|
||||
(sess.hasLoginSession
|
||||
? escapeHtml(dateText)
|
||||
: '<span class="mnote-vault-muted">—</span>') +
|
||||
'</div></div>';
|
||||
if (sess.hasLoginSession && sess.expiresAt) {
|
||||
html +=
|
||||
'<div class="mnote-vault-field-row mnote-vault-session-row" data-testid="vault-account-session-expires">' +
|
||||
'<label>过期时间</label>' +
|
||||
'<div>' +
|
||||
escapeHtml(formatVaultDateTime(sess.expiresAt) || sess.expiresAt) +
|
||||
'</div></div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function normalizeAccountsFromItem(item, forEdit) {
|
||||
var topSecrets = normalizeSecretsFromItem(item);
|
||||
var accounts = (item && item.accounts) || [];
|
||||
@@ -941,6 +1038,25 @@
|
||||
return s.accountId && s.accountId === a.id;
|
||||
});
|
||||
}
|
||||
// 多账号时:若槽位未挂 session 元数据,首账号可回退条目级(兼容旧投影)
|
||||
var hasSess =
|
||||
typeof a.hasLoginSession === 'boolean'
|
||||
? a.hasLoginSession
|
||||
: aidx === 0
|
||||
? !!(item && item.hasLoginSession)
|
||||
: false;
|
||||
var lastLogin =
|
||||
a.lastLoginAt ||
|
||||
(aidx === 0 && item ? item.lastLoginAt : null) ||
|
||||
null;
|
||||
var sessUpdated =
|
||||
a.sessionUpdatedAt ||
|
||||
(aidx === 0 && item ? item.sessionUpdatedAt : null) ||
|
||||
null;
|
||||
var sessExpires =
|
||||
a.sessionExpiresAt ||
|
||||
(aidx === 0 && item ? item.sessionExpiresAt : null) ||
|
||||
null;
|
||||
return {
|
||||
id: a.id || '',
|
||||
label: a.label || '',
|
||||
@@ -952,7 +1068,14 @@
|
||||
: a.email || '',
|
||||
passwordHint: a.passwordHint || '',
|
||||
password: a.password || { state: 'absent' },
|
||||
// Draft-only flag from reRenderFormKeepingSlots; absent on server items.
|
||||
passwordClear: !!a.passwordClear,
|
||||
secrets: nested,
|
||||
hasLoginSession: hasSess,
|
||||
lastLoginAt: lastLogin,
|
||||
sessionUpdatedAt: sessUpdated,
|
||||
sessionExpiresAt: sessExpires,
|
||||
sessionSource: a.sessionSource || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -974,6 +1097,13 @@
|
||||
passwordHint: (item && item.passwordHint) || '',
|
||||
password: (item && item.password) || { state: 'absent' },
|
||||
secrets: topSecrets,
|
||||
// 合成单账号:登录态取条目级
|
||||
hasLoginSession: !!(item && item.hasLoginSession),
|
||||
lastLoginAt: (item && item.lastLoginAt) || null,
|
||||
sessionUpdatedAt: (item && item.sessionUpdatedAt) || null,
|
||||
sessionExpiresAt: (item && item.sessionExpiresAt) || null,
|
||||
sessionSource: '',
|
||||
_syntheticFromItem: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -986,6 +1116,12 @@
|
||||
passwordHint: '',
|
||||
password: { state: 'absent' },
|
||||
secrets: [],
|
||||
hasLoginSession: !!(item && item.hasLoginSession),
|
||||
lastLoginAt: (item && item.lastLoginAt) || null,
|
||||
sessionUpdatedAt: (item && item.sessionUpdatedAt) || null,
|
||||
sessionExpiresAt: (item && item.sessionExpiresAt) || null,
|
||||
sessionSource: '',
|
||||
_syntheticFromItem: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1157,6 +1293,11 @@
|
||||
acc.username ||
|
||||
acc.email ||
|
||||
'账号 ' + (idx + 1);
|
||||
var sess = accountSessionView(
|
||||
acc,
|
||||
item,
|
||||
!!acc._syntheticFromItem || accounts.length === 1
|
||||
);
|
||||
var body =
|
||||
fieldRow('标签名', acc.label, showEmpty) +
|
||||
fieldRow('用户名', acc.username, showEmpty) +
|
||||
@@ -1164,7 +1305,8 @@
|
||||
secretRow('密码', 'password', acc.password, showEmpty, {
|
||||
accountId: acc.id || '',
|
||||
}) +
|
||||
fieldRow('密码提示', acc.passwordHint, showEmpty);
|
||||
fieldRow('密码提示', acc.passwordHint, showEmpty) +
|
||||
renderAccountSessionRows(sess, showEmpty);
|
||||
var secList = acc.secrets || [];
|
||||
var nestedSecretsHtml = secList
|
||||
.map(function (sec, sidx) {
|
||||
@@ -1220,6 +1362,9 @@
|
||||
(secList.length
|
||||
? '<span class="mnote-vault-muted">密钥 ' + secList.length + '</span>'
|
||||
: '') +
|
||||
(sess.hasLoginSession
|
||||
? '<span class="mnote-vault-session-badge is-saved is-compact" title="已保存登录态">登录态</span>'
|
||||
: '') +
|
||||
'</summary>' +
|
||||
'<div class="mnote-vault-slot-body">' +
|
||||
body +
|
||||
@@ -1329,7 +1474,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
/** Unique folderPath values from current list (for select / datalist). */
|
||||
/** Unique folderPath values from current list (for picker / datalist). */
|
||||
function collectFolderPaths() {
|
||||
var set = {};
|
||||
(state.items || []).forEach(function (item) {
|
||||
@@ -1346,47 +1491,152 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested tree of known folderPath segments for the expand-style picker.
|
||||
* @returns {{ name: string, path: string, children: Array }}
|
||||
*/
|
||||
function buildFolderPickerTree(paths) {
|
||||
var root = { name: '', path: '', children: [] };
|
||||
var byPath = { '': root };
|
||||
(paths || []).forEach(function (fp) {
|
||||
var parts = String(fp || '')
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
var acc = [];
|
||||
var parent = root;
|
||||
parts.forEach(function (part) {
|
||||
acc.push(part);
|
||||
var p = acc.join('/');
|
||||
if (!byPath[p]) {
|
||||
var node = { name: part, path: p, children: [] };
|
||||
byPath[p] = node;
|
||||
parent.children.push(node);
|
||||
}
|
||||
parent = byPath[p];
|
||||
});
|
||||
});
|
||||
function sortNodes(nodes) {
|
||||
nodes.sort(function (a, b) {
|
||||
return a.name.localeCompare(b.name, 'zh');
|
||||
});
|
||||
nodes.forEach(function (n) {
|
||||
if (n.children && n.children.length) sortNodes(n.children);
|
||||
});
|
||||
}
|
||||
sortNodes(root.children);
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a picker branch starts expanded.
|
||||
* Default: all collapsed. Only expand ancestors of the current selection
|
||||
* (so the selected path stays visible), or branches the user toggled open.
|
||||
*/
|
||||
function isFolderPickerBranchOpen(path, depth, current) {
|
||||
var ui = ensureFormUi();
|
||||
if (!ui.folderPickerOpen) ui.folderPickerOpen = {};
|
||||
if (Object.prototype.hasOwnProperty.call(ui.folderPickerOpen, path)) {
|
||||
return !!ui.folderPickerOpen[path];
|
||||
}
|
||||
// Only auto-open ancestors of the selected path (not the leaf itself unless it has kids under selection).
|
||||
if (current && path && current.indexOf(path + '/') === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderFolderPickerNode(node, depth, current) {
|
||||
var kids = node.children || [];
|
||||
var hasKids = kids.length > 0;
|
||||
var selected = current === node.path;
|
||||
var open = hasKids && isFolderPickerBranchOpen(node.path, depth, current);
|
||||
var html = '';
|
||||
html +=
|
||||
'<div class="mnote-vault-folder-picker-node" data-vault-picker-path="' +
|
||||
escapeHtml(node.path) +
|
||||
'" style="--vault-picker-depth:' +
|
||||
depth +
|
||||
'">';
|
||||
html += '<div class="mnote-vault-folder-picker-row' + (selected ? ' is-selected' : '') + '">';
|
||||
if (hasKids) {
|
||||
html +=
|
||||
'<button type="button" class="mnote-vault-folder-picker-chevron" data-vault-folder-picker-toggle="' +
|
||||
escapeHtml(node.path) +
|
||||
'" aria-expanded="' +
|
||||
(open ? 'true' : 'false') +
|
||||
'" title="' +
|
||||
(open ? '收起' : '展开') +
|
||||
'">' +
|
||||
(open ? '▼' : '▶') +
|
||||
'</button>';
|
||||
} else {
|
||||
html += '<span class="mnote-vault-folder-picker-chevron is-leaf" aria-hidden="true"></span>';
|
||||
}
|
||||
html +=
|
||||
'<button type="button" class="mnote-vault-folder-picker-label" data-vault-folder-pick="' +
|
||||
escapeHtml(node.path) +
|
||||
'" data-testid="vault-folder-pick-' +
|
||||
escapeHtml(node.path || 'root') +
|
||||
'">' +
|
||||
escapeHtml(node.name || node.path) +
|
||||
'</button>';
|
||||
html += '</div>';
|
||||
if (hasKids) {
|
||||
html +=
|
||||
'<div class="mnote-vault-folder-picker-children"' +
|
||||
(open ? '' : ' hidden') +
|
||||
'>';
|
||||
kids.forEach(function (child) {
|
||||
html += renderFolderPickerNode(child, depth + 1, current);
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function formFolderPathField(value) {
|
||||
var current = (value || '').trim();
|
||||
var current = normalizeFolderPath(value || '');
|
||||
var paths = collectFolderPaths();
|
||||
var seen = {};
|
||||
paths.forEach(function (p) {
|
||||
seen[p] = true;
|
||||
});
|
||||
var options =
|
||||
'<option value=""' +
|
||||
(!current ? ' selected' : '') +
|
||||
'>(无分组)</option>';
|
||||
paths.forEach(function (p) {
|
||||
options +=
|
||||
'<option value="' +
|
||||
escapeHtml(p) +
|
||||
'"' +
|
||||
(p === current ? ' selected' : '') +
|
||||
'>' +
|
||||
escapeHtml(p) +
|
||||
'</option>';
|
||||
});
|
||||
// 当前值若不在列表中,仍挂到树上便于高亮/再选。
|
||||
var treePaths = paths.slice();
|
||||
if (current && !seen[current]) {
|
||||
options +=
|
||||
'<option value="' +
|
||||
escapeHtml(current) +
|
||||
'" selected>' +
|
||||
escapeHtml(current) +
|
||||
'</option>';
|
||||
treePaths.push(current);
|
||||
var parts = current.split('/').filter(Boolean);
|
||||
for (var i = 1; i < parts.length; i++) {
|
||||
var prefix = parts.slice(0, i).join('/');
|
||||
if (treePaths.indexOf(prefix) < 0) treePaths.push(prefix);
|
||||
}
|
||||
}
|
||||
var tree = buildFolderPickerTree(treePaths);
|
||||
var treeHtml = '';
|
||||
treeHtml +=
|
||||
'<button type="button" class="mnote-vault-folder-picker-none' +
|
||||
(!current ? ' is-selected' : '') +
|
||||
'" data-vault-folder-pick="" data-testid="vault-folder-pick-none">(无分组)</button>';
|
||||
tree.children.forEach(function (child) {
|
||||
treeHtml += renderFolderPickerNode(child, 0, current);
|
||||
});
|
||||
if (!tree.children.length && !current) {
|
||||
treeHtml +=
|
||||
'<div class="mnote-vault-folder-picker-empty mnote-vault-muted">暂无已有分组,可在下方输入新建</div>';
|
||||
}
|
||||
return (
|
||||
'<div class="mnote-vault-field-row mnote-vault-folder-field">' +
|
||||
'<label for="vault-f-folderPath">分组</label>' +
|
||||
'<div class="mnote-vault-folder-controls">' +
|
||||
'<select id="vault-f-folderSelect" data-vault-folder-select data-testid="vault-folder-select" aria-label="选择分组">' +
|
||||
options +
|
||||
'</select>' +
|
||||
'<div class="mnote-vault-folder-picker" data-vault-folder-picker data-testid="vault-folder-select" role="listbox" aria-label="选择分组">' +
|
||||
treeHtml +
|
||||
'</div>' +
|
||||
'<input id="vault-f-folderPath" name="folderPath" type="text" ' +
|
||||
'value="' +
|
||||
escapeHtml(current) +
|
||||
'" placeholder="新分组可直接输入,用 / 分层" ' +
|
||||
'data-vault-folder-input data-testid="vault-folder-path" list="vault-folder-path-list" />' +
|
||||
'" placeholder="点上方分组选择,或直接输入;用 / 分层" ' +
|
||||
'data-vault-folder-input data-testid="vault-folder-path" list="vault-folder-path-list" autocomplete="off" />' +
|
||||
'<datalist id="vault-folder-path-list">' +
|
||||
paths
|
||||
.map(function (p) {
|
||||
@@ -1394,39 +1644,75 @@
|
||||
})
|
||||
.join('') +
|
||||
'</datalist>' +
|
||||
'<p class="mnote-vault-form-hint mnote-vault-folder-hint">分组默认折叠;点 ▶ 展开,点名称即可选中。也可在下方直接输入路径。</p>' +
|
||||
'</div></div>'
|
||||
);
|
||||
}
|
||||
|
||||
function syncFolderPickerSelection(form, path) {
|
||||
if (!form) return;
|
||||
var picker = qs('[data-vault-folder-picker]', form);
|
||||
if (!picker) return;
|
||||
var normalized = normalizeFolderPath(path || '');
|
||||
qsa('[data-vault-folder-pick]', picker).forEach(function (btn) {
|
||||
var p = btn.getAttribute('data-vault-folder-pick');
|
||||
if (p == null) p = '';
|
||||
var row = btn.closest('.mnote-vault-folder-picker-row');
|
||||
var selected = p === normalized;
|
||||
btn.classList.toggle('is-selected', selected);
|
||||
if (row) row.classList.toggle('is-selected', selected);
|
||||
if (btn.classList.contains('mnote-vault-folder-picker-none')) {
|
||||
btn.classList.toggle('is-selected', selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindFolderPathControls(form) {
|
||||
if (!form) return;
|
||||
var select = qs('[data-vault-folder-select]', form);
|
||||
var input = qs('[data-vault-folder-input]', form);
|
||||
if (select && input) {
|
||||
select.addEventListener('change', function () {
|
||||
input.value = select.value || '';
|
||||
state.dirty = true;
|
||||
var picker = qs('[data-vault-folder-picker]', form);
|
||||
if (picker) {
|
||||
picker.addEventListener('click', function (ev) {
|
||||
var t = ev.target;
|
||||
if (!(t instanceof Element)) return;
|
||||
var toggle = t.closest('[data-vault-folder-picker-toggle]');
|
||||
if (toggle && picker.contains(toggle)) {
|
||||
ev.preventDefault();
|
||||
var tpath = toggle.getAttribute('data-vault-folder-picker-toggle') || '';
|
||||
var nodeEl = toggle.closest('.mnote-vault-folder-picker-node');
|
||||
// First matching descendants is the direct children panel for this node.
|
||||
var kidsEl =
|
||||
nodeEl && nodeEl.querySelector('.mnote-vault-folder-picker-children');
|
||||
var open = toggle.getAttribute('aria-expanded') === 'true';
|
||||
var next = !open;
|
||||
ensureFormUi().folderPickerOpen = ensureFormUi().folderPickerOpen || {};
|
||||
ensureFormUi().folderPickerOpen[tpath] = next;
|
||||
toggle.setAttribute('aria-expanded', next ? 'true' : 'false');
|
||||
toggle.textContent = next ? '▼' : '▶';
|
||||
toggle.setAttribute('title', next ? '收起' : '展开');
|
||||
if (kidsEl) kidsEl.hidden = !next;
|
||||
return;
|
||||
}
|
||||
var pick = t.closest('[data-vault-folder-pick]');
|
||||
if (pick && picker.contains(pick)) {
|
||||
ev.preventDefault();
|
||||
var chosen = pick.getAttribute('data-vault-folder-pick');
|
||||
if (chosen == null) chosen = '';
|
||||
chosen = normalizeFolderPath(chosen);
|
||||
if (input) input.value = chosen;
|
||||
syncFolderPickerSelection(form, chosen);
|
||||
state.dirty = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (input) {
|
||||
input.addEventListener('input', function () {
|
||||
var n = normalizeFolderPath(input.value);
|
||||
// keep select in sync when user picks a known path via typing
|
||||
if (select) {
|
||||
var match = false;
|
||||
for (var i = 0; i < select.options.length; i++) {
|
||||
if (select.options[i].value === n) {
|
||||
select.selectedIndex = i;
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) select.selectedIndex = 0;
|
||||
}
|
||||
syncFolderPickerSelection(form, input.value);
|
||||
});
|
||||
input.addEventListener('change', function () {
|
||||
var n = normalizeFolderPath(input.value);
|
||||
input.value = n;
|
||||
syncFolderPickerSelection(form, n);
|
||||
state.dirty = true;
|
||||
});
|
||||
}
|
||||
@@ -1439,24 +1725,55 @@
|
||||
var urls = normalizeUrlsFromItem(item);
|
||||
if (!urls.length) urls = [''];
|
||||
state._formUrls = urls.slice();
|
||||
/**
|
||||
* Rebuild draft slots from item projection.
|
||||
* reRenderFormKeepingSlots injects in-progress plaintext as
|
||||
* `{ state: 'revealed', value }` — must become valueText/passwordValue,
|
||||
* not be wiped to '' (that made the 1st new secret disappear after +密钥).
|
||||
* valueClear/passwordClear on the pseudo-item are also preserved.
|
||||
*/
|
||||
state._formAccounts = accounts.map(function (a) {
|
||||
var pwd = a.password || { state: 'absent' };
|
||||
var passwordClear = !!a.passwordClear;
|
||||
var passwordValue = '';
|
||||
var passwordState = pwd;
|
||||
if (passwordClear) {
|
||||
passwordState = { state: 'absent' };
|
||||
passwordValue = '';
|
||||
} else if (pwd && pwd.state === 'revealed') {
|
||||
// Draft plaintext typed in this session (not yet saved / mid re-render).
|
||||
passwordValue = String(pwd.value || '');
|
||||
// Treat as "no stored secret yet" for keep-logic; save uses passwordValue.
|
||||
passwordState = { state: 'absent' };
|
||||
}
|
||||
return {
|
||||
id: a.id || newClientId('acc'),
|
||||
label: a.label || '',
|
||||
username: a.username || '',
|
||||
email: a.email || '',
|
||||
passwordHint: a.passwordHint || '',
|
||||
passwordState: a.password || { state: 'absent' },
|
||||
passwordClear: false,
|
||||
passwordValue: '',
|
||||
passwordState: passwordState,
|
||||
passwordClear: passwordClear,
|
||||
passwordValue: passwordValue,
|
||||
secrets: (a.secrets || []).map(function (s) {
|
||||
var v = s.value || { state: 'absent' };
|
||||
var valueClear = !!s.valueClear;
|
||||
var valueText = '';
|
||||
var valueState = v;
|
||||
if (valueClear) {
|
||||
valueState = { state: 'absent' };
|
||||
valueText = '';
|
||||
} else if (v && v.state === 'revealed') {
|
||||
valueText = String(v.value || '');
|
||||
valueState = { state: 'absent' };
|
||||
}
|
||||
return {
|
||||
id: s.id || newClientId('sec'),
|
||||
kind: s.kind || 'apikey',
|
||||
label: s.label || '',
|
||||
valueState: s.value || { state: 'absent' },
|
||||
valueClear: false,
|
||||
valueText: '',
|
||||
valueState: valueState,
|
||||
valueClear: valueClear,
|
||||
valueText: valueText,
|
||||
accountId: a.id || '',
|
||||
};
|
||||
}),
|
||||
@@ -1592,9 +1909,11 @@
|
||||
'<div class="mnote-vault-secret-edit">' +
|
||||
'<input type="text" autocomplete="off" spellcheck="false" data-acc-field="password" data-acc-id="' +
|
||||
escapeHtml(acc.id) +
|
||||
'" class="mnote-vault-secret-input-plain" placeholder="' +
|
||||
'" class="mnote-vault-secret-input-plain" value="' +
|
||||
escapeHtml(acc.passwordValue || '') +
|
||||
'" placeholder="' +
|
||||
escapeHtml(
|
||||
!isCreate && hasPwd
|
||||
!isCreate && hasPwd && !acc.passwordValue
|
||||
? '留空不改;输入新值覆盖'
|
||||
: '可选;可用 [Key] 密文片段'
|
||||
) +
|
||||
@@ -1602,7 +1921,11 @@
|
||||
(!isCreate && hasPwd
|
||||
? '<button type="button" data-acc-show-password="' +
|
||||
escapeHtml(acc.id) +
|
||||
'" data-testid="vault-acc-show-password">显示</button>' +
|
||||
'"' +
|
||||
(acc.passwordValue ? ' data-shown="1"' : '') +
|
||||
' data-testid="vault-acc-show-password">' +
|
||||
(acc.passwordValue ? '隐藏' : '显示') +
|
||||
'</button>' +
|
||||
'<button type="button" data-acc-clear-password="' +
|
||||
escapeHtml(acc.id) +
|
||||
'">清空</button>'
|
||||
@@ -1624,6 +1947,7 @@
|
||||
sec.kind === 'token' ? 'Token' : sec.kind === 'other' ? '其他' : 'API Key';
|
||||
var title = sec.label || kindLabel + ' ' + (idx + 1);
|
||||
var hasVal = sec.valueState && sec.valueState.state === 'masked';
|
||||
var draftText = sec.valueText || '';
|
||||
return (
|
||||
'<div class="mnote-vault-nested-secret" data-vault-secret-slot="' +
|
||||
escapeHtml(sec.id) +
|
||||
@@ -1672,15 +1996,21 @@
|
||||
escapeHtml(sec.id) +
|
||||
'" data-acc-id="' +
|
||||
escapeHtml(accountId) +
|
||||
'" value="" placeholder="' +
|
||||
escapeHtml(hasVal ? '留空不改;输入新值覆盖' : '密钥明文') +
|
||||
'" value="' +
|
||||
escapeHtml(draftText) +
|
||||
'" placeholder="' +
|
||||
escapeHtml(hasVal && !draftText ? '留空不改;输入新值覆盖' : '密钥明文') +
|
||||
'" />' +
|
||||
(hasVal
|
||||
? '<button type="button" data-sec-show-value="' +
|
||||
escapeHtml(sec.id) +
|
||||
'" data-acc-id="' +
|
||||
escapeHtml(accountId) +
|
||||
'" data-testid="vault-sec-show-value">显示</button>' +
|
||||
'"' +
|
||||
(draftText ? ' data-shown="1"' : '') +
|
||||
' data-testid="vault-sec-show-value">' +
|
||||
(draftText ? '隐藏' : '显示') +
|
||||
'</button>' +
|
||||
'<button type="button" data-sec-clear-value="' +
|
||||
escapeHtml(sec.id) +
|
||||
'" data-acc-id="' +
|
||||
@@ -1830,6 +2160,8 @@
|
||||
username: a.username,
|
||||
email: a.email,
|
||||
passwordHint: a.passwordHint,
|
||||
// Flags survive re-render so clear intent is not lost.
|
||||
passwordClear: !!a.passwordClear,
|
||||
password: a.passwordClear
|
||||
? { state: 'absent' }
|
||||
: a.passwordValue
|
||||
@@ -1841,6 +2173,7 @@
|
||||
kind: s.kind,
|
||||
label: s.label,
|
||||
accountId: a.id,
|
||||
valueClear: !!s.valueClear,
|
||||
value: s.valueClear
|
||||
? { state: 'absent' }
|
||||
: s.valueText
|
||||
@@ -2040,13 +2373,30 @@
|
||||
var folderPath = normalizeFolderPath(String(fd.get('folderPath') || '').trim());
|
||||
var accounts = (state._formAccounts || [])
|
||||
.map(function (acc) {
|
||||
// Prefer live input draft; fall back to revealed draft left by re-render inject.
|
||||
var raw = String(acc.passwordValue || '');
|
||||
if (
|
||||
!raw &&
|
||||
acc.passwordState &&
|
||||
acc.passwordState.state === 'revealed' &&
|
||||
acc.passwordState.value
|
||||
) {
|
||||
raw = String(acc.passwordState.value);
|
||||
}
|
||||
if (/^[•*·]+$/.test(raw) || raw === '••••••••') {
|
||||
toast('不能将掩码写回 password', 'error');
|
||||
throw new Error('mask');
|
||||
}
|
||||
var nestedSecrets = (acc.secrets || []).map(function (sec) {
|
||||
var sraw = String(sec.valueText || '');
|
||||
if (
|
||||
!sraw &&
|
||||
sec.valueState &&
|
||||
sec.valueState.state === 'revealed' &&
|
||||
sec.valueState.value
|
||||
) {
|
||||
sraw = String(sec.valueState.value);
|
||||
}
|
||||
if (/^[•*·]+$/.test(sraw) || sraw === '••••••••') {
|
||||
toast('不能将掩码写回 secret', 'error');
|
||||
throw new Error('mask');
|
||||
|
||||
Reference in New Issue
Block a user