@co-engram/viewer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +38 -0
  2. package/dist/brand-logos.d.ts +9 -0
  3. package/dist/brand-logos.d.ts.map +1 -0
  4. package/dist/brand-logos.js +10 -0
  5. package/dist/brand-logos.js.map +1 -0
  6. package/dist/html.d.ts +21 -0
  7. package/dist/html.d.ts.map +1 -0
  8. package/dist/html.js +299 -0
  9. package/dist/html.js.map +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +8 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/runtime/app.d.ts +11 -0
  15. package/dist/runtime/app.d.ts.map +1 -0
  16. package/dist/runtime/app.js +437 -0
  17. package/dist/runtime/app.js.map +1 -0
  18. package/dist/runtime/decay.d.ts +16 -0
  19. package/dist/runtime/decay.d.ts.map +1 -0
  20. package/dist/runtime/decay.js +108 -0
  21. package/dist/runtime/decay.js.map +1 -0
  22. package/dist/runtime/graph.d.ts +13 -0
  23. package/dist/runtime/graph.d.ts.map +1 -0
  24. package/dist/runtime/graph.js +313 -0
  25. package/dist/runtime/graph.js.map +1 -0
  26. package/dist/runtime/i18n.d.ts +16 -0
  27. package/dist/runtime/i18n.d.ts.map +1 -0
  28. package/dist/runtime/i18n.js +76 -0
  29. package/dist/runtime/i18n.js.map +1 -0
  30. package/dist/runtime/tabs.d.ts +8 -0
  31. package/dist/runtime/tabs.d.ts.map +1 -0
  32. package/dist/runtime/tabs.js +1783 -0
  33. package/dist/runtime/tabs.js.map +1 -0
  34. package/dist/server.d.ts +73 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +985 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/styles.d.ts +13 -0
  39. package/dist/styles.d.ts.map +1 -0
  40. package/dist/styles.js +1632 -0
  41. package/dist/styles.js.map +1 -0
  42. package/dist/vendor/dompurify-source.d.ts +11 -0
  43. package/dist/vendor/dompurify-source.d.ts.map +1 -0
  44. package/dist/vendor/dompurify-source.js +15 -0
  45. package/dist/vendor/dompurify-source.js.map +1 -0
  46. package/dist/vendor/marked-source.d.ts +11 -0
  47. package/dist/vendor/marked-source.d.ts.map +1 -0
  48. package/dist/vendor/marked-source.js +18 -0
  49. package/dist/vendor/marked-source.js.map +1 -0
  50. package/dist/vendor/vis-network-source.d.ts +11 -0
  51. package/dist/vendor/vis-network-source.d.ts.map +1 -0
  52. package/dist/vendor/vis-network-source.js +46 -0
  53. package/dist/vendor/vis-network-source.js.map +1 -0
  54. package/package.json +61 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Viewer v2 runtime — stats / engrams / proposals / audit / trash / config 六个 tab。
3
+ * Graph tab 单独在 graph.ts(需要 vis-network)。
4
+ *
5
+ * @module @co-engram/claude-code/viewer/runtime/tabs
6
+ */
7
+ export declare const TABS_RUNTIME = "\n// ============================================================\n// \u5168\u5C40\u6620\u5C04\u8868(\u4E2D\u6587\u6807\u7B7E)\n// ============================================================\nwindow.CO_ENGRAM_LABELS = {\n kind: { fact: '\u4E8B\u5B9E', observation: '\u89C2\u5BDF', pattern: '\u6A21\u5F0F', procedure: '\u6D41\u7A0B', hypothesis: '\u5047\u8BBE' },\n status: { active: '\u6D3B\u8DC3', dormant: '\u4F11\u7720', forgotten: '\u5DF2\u9057\u5FD8', archived: '\u5DF2\u5F52\u6863' },\n freshness: { stable: '\u7A33\u5B9A', recent: '\u8FD1\u671F', fading: '\u8870\u51CF', stale: '\u8FC7\u65F6' },\n visibility: { public: '\u516C\u5F00', team: '\u56E2\u961F', private: '\u79C1\u6709', restricted: '\u53D7\u9650' },\n emotionalValence: { positive: '\u79EF\u6781', negative: '\u6D88\u6781', neutral: '\u4E2D\u6027' },\n sourceType: { firsthand: '\u4E00\u624B', secondhand: '\u4E8C\u624B', inferred: '\u63A8\u65AD' },\n verification: { unverified: '\u672A\u9A8C\u8BC1', plausible: '\u8C8C\u4F3C\u6210\u7ACB', probable: '\u8F83\u53EF\u80FD', verified: '\u5DF2\u9A8C\u8BC1', refuted: '\u5DF2\u53CD\u9A73' },\n synapse: {\n extends: '\u6269\u5C55', part_of: '\u90E8\u5206', similar_to: '\u76F8\u4F3C',\n depends_on: '\u4F9D\u8D56', causes: '\u5BFC\u81F4', follows: '\u8DDF\u968F',\n derives_from: '\u6D3E\u751F', contradicts: '\u77DB\u76FE', exemplifies: '\u4F8B\u8BC1',\n supersedes: '\u53D6\u4EE3', consolidates: '\u6574\u5408',\n contextualizes: '\u4E0A\u4E0B\u6587'\n },\n synapseDirection: { directional: '\u5355\u5411', bidirectional: '\u53CC\u5411' },\n resolution: { pending: '\u5F85\u5904\u7406', auto_resolved: '\u5DF2\u81EA\u52A8\u88C1\u51B3', escalated: '\u5DF2\u5347\u7EA7', contested: '\u6709\u4E89\u8BAE', resolved: '\u5DF2\u89E3\u51B3' }\n};\n\n// ============================================================\n// Stats\n// ============================================================\nCO_ENGRAM.on('stats', async function() {\n const el = document.getElementById('stats-content');\n if (!el) return;\n if (CO_ENGRAM._statsLoaded) return;\n el.innerHTML = '<div class=\"loading\">\u52A0\u8F7D\u7EDF\u8BA1\u4E2D</div>';\n let data;\n try { data = await CO_ENGRAM.apiGet('/api/stats'); }\n catch (e) { el.innerHTML = '<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>'; return; }\n\n const L = CO_ENGRAM_LABELS;\n const SYNAPSE_LABEL = {\n extends: '\u6269\u5C55', part_of: '\u90E8\u5206', similar_to: '\u76F8\u4F3C',\n depends_on: '\u4F9D\u8D56', causes: '\u5BFC\u81F4', follows: '\u8DDF\u968F',\n derives_from: '\u6D3E\u751F', contradicts: '\u77DB\u76FE', exemplifies: '\u4F8B\u8BC1',\n supersedes: '\u53D6\u4EE3', consolidates: '\u6574\u5408',\n contextualizes: '\u4E0A\u4E0B\u6587'\n };\n\n const kpiClickable = (label, value, sub, tab) => '<div class=\"kpi\"' + (tab ? ' onclick=\"CO_ENGRAM.showTab(\\'' + tab + '\\')\"' : '') + '>'\n + '<div class=\"kpi-label\">' + CO_ENGRAM.escapeHtml(label) + '</div>'\n + '<div class=\"kpi-value\">' + CO_ENGRAM.escapeHtml(value) + '</div>'\n + (sub ? '<div class=\"kpi-sub\">' + CO_ENGRAM.escapeHtml(sub) + '</div>' : '') + '</div>';\n\n const barRow = (label, count, max, color, onclick, tipAttr) => '<div class=\"bar-row\">'\n + '<div class=\"bar-label\"' + (tipAttr || '') + (onclick ? ' onclick=\"' + onclick + '\"' : '') + '>' + CO_ENGRAM.escapeHtml(label) + '</div>'\n + '<div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:' + (max ? (count / max * 100) : 0).toFixed(1) + '%;background:' + (color || '#5eead4') + '\"></div></div>'\n + '<div class=\"bar-value\">' + count + '</div></div>';\n\n const kindMap = data.byKind || {};\n const kindKeys = Object.keys(kindMap);\n const kindMax = Math.max(1, ...kindKeys.map(k => kindMap[k] || 0));\n const statusMap = data.byStatus || {};\n const statusKeys = Object.keys(statusMap);\n const statusMax = Math.max(1, ...statusKeys.map(k => statusMap[k] || 0));\n const synKindMap = data.bySynapseKind || {};\n const synKindKeys = Object.keys(synKindMap);\n const synKindMax = Math.max(1, ...synKindKeys.map(k => synKindMap[k] || 0));\n const tagArr = data.topTags || [];\n const tagMax = tagArr.length ? Math.max(1, ...tagArr.map(t => t.count || 0)) : 1;\n const contribArr = data.topContributors || [];\n const contribMax = contribArr.length ? Math.max(1, ...contribArr.map(c => c.total || 0)) : 1;\n\n let html = '<div class=\"kpi-grid\">'\n + kpiClickable('\u8BB0\u5FC6\u5370\u8FF9\u603B\u6570', data.totalEngrams || 0, '\u70B9\u51FB\u67E5\u770B\u5168\u90E8', 'engrams')\n + kpiClickable('\u8BB0\u5FC6\u7A81\u89E6\u603B\u6570', data.totalSynapses || 0, '\u70B9\u51FB\u67E5\u770B\u56FE\u8C31', 'graph')\n + kpiClickable('\u5F85\u5BA1\u63D0\u6848', data.pendingProposals || 0, '\u70B9\u51FB\u5904\u7406', 'proposals')\n + '</div>';\n\n // \u8BB0\u5FC6\u5370\u8FF9\u533A(\u72EC\u7ACB\u4E00\u5757)\n html += '<div class=\"card\" style=\"margin-top:1.25rem\"><h3 class=\"section-title\"' + CO_ENGRAM.tip('kind.fact') + '>\u8BB0\u5FC6\u5370\u8FF9 \u00B7 \u6309\u7C7B\u578B\u5206\u5E03</h3>';\n if (!kindKeys.length) html += '<div class=\"empty\">\u6682\u65E0\u6570\u636E</div>';\n else kindKeys.forEach(k => html += barRow(L.kind[k] || k, kindMap[k], kindMax, CO_ENGRAM.kindColor(k), 'CO_ENGRAM.showTab(\\'engrams\\')', CO_ENGRAM.tip('kind.' + k)));\n html += '</div>';\n\n html += '<div class=\"card\" style=\"margin-top:1rem\"><h3 class=\"section-title\"' + CO_ENGRAM.tip('status.active') + '>\u8BB0\u5FC6\u5370\u8FF9 \u00B7 \u6309\u72B6\u6001\u5206\u5E03</h3>';\n if (!statusKeys.length) html += '<div class=\"empty\">\u6682\u65E0\u6570\u636E</div>';\n else statusKeys.forEach(k => html += barRow(L.status[k] || k, statusMap[k], statusMax, '#94a3b8', '', CO_ENGRAM.tip('status.' + k)));\n html += '</div>';\n\n // \u8BB0\u5FC6\u7A81\u89E6\u533A(\u72EC\u7ACB\u4E00\u5757,\u4E0E\u5370\u8FF9\u5206\u5F00)\n html += '<div class=\"card\" style=\"margin-top:1rem\"><h3 class=\"section-title\"' + CO_ENGRAM.tip('family.structural') + '>\u8BB0\u5FC6\u7A81\u89E6 \u00B7 \u6309\u7C7B\u578B\u5206\u5E03</h3>';\n if (!synKindKeys.length) html += '<div class=\"empty\">\u6682\u65E0\u7A81\u89E6</div>';\n else synKindKeys.forEach(k => html += barRow(SYNAPSE_LABEL[k] || k, synKindMap[k], synKindMax, CO_ENGRAM.edgeColor(k), 'CO_ENGRAM.showTab(\\'graph\\')', CO_ENGRAM.tip('synapse.' + k)));\n html += '</div>';\n\n // \u8D21\u732E\u8005\u6392\u540D\n if (contribArr.length) {\n html += '<div class=\"card\" style=\"margin-top:1rem\"><h3 class=\"section-title\">\u8D21\u732E\u8005\u6392\u540D \u00B7 \u5370\u8FF9 + \u7A81\u89E6\u5408\u8BA1</h3>';\n html += '<table class=\"data-table\"><thead><tr><th>#</th><th>\u8D21\u732E\u8005</th><th>\u5370\u8FF9</th><th>\u7A81\u89E6</th><th style=\"width:35%\">\u5408\u8BA1</th></tr></thead><tbody>';\n contribArr.forEach((c, i) => {\n const pct = (c.total / contribMax * 100).toFixed(1);\n html += '<tr>'\n + '<td>' + (i + 1) + '</td>'\n + '<td><code>' + CO_ENGRAM.escapeHtml(c.actor) + '</code></td>'\n + '<td>' + c.engramCount + '</td>'\n + '<td>' + c.synapseCount + '</td>'\n + '<td><div class=\"bar-track\" style=\"min-width:120px\"><div class=\"bar-fill\" style=\"width:' + pct + '%;background:#5eead4\"></div></div> <span style=\"margin-left:.4rem\">' + c.total + '</span></td>'\n + '</tr>';\n });\n html += '</tbody></table>';\n html += '</div>';\n }\n\n if (tagArr.length) {\n html += '<div class=\"card\" style=\"margin-top:1rem\"><h3 class=\"section-title\">\u9AD8\u9891\u9886\u57DF\u6807\u7B7E</h3>';\n tagArr.slice(0, 10).forEach(t => { html += barRow(t.tag, t.count, tagMax, '#c084fc'); });\n html += '</div>';\n }\n\n el.innerHTML = html;\n CO_ENGRAM._statsLoaded = true;\n});\n\n// ============================================================\n// Engrams\n// ============================================================\nCO_ENGRAM.on('engrams', async function() {\n const root = document.getElementById('engrams-content');\n if (!root) return;\n if (CO_ENGRAM._engramsLoaded) return;\n CO_ENGRAM._engramsLoaded = true;\n await CO_ENGRAM_ENGRAMS.render(root);\n});\n\nwindow.CO_ENGRAM_ENGRAMS = {\n async render(root) {\n root.innerHTML = '<div class=\"loading\">\u52A0\u8F7D\u8BB0\u5FC6\u5370\u8FF9\u4E2D</div>';\n let data;\n try { data = await CO_ENGRAM.apiGet('/api/engrams'); }\n catch (e) { root.innerHTML = '<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>'; return; }\n\n const all = data.results || [];\n CO_ENGRAM._engramsCache = all;\n CO_ENGRAM._engramsViewMode = CO_ENGRAM._engramsViewMode || 'card';\n\n const T = CO_ENGRAM_T;\n const kindKeys = ['observation', 'fact', 'pattern', 'procedure', 'hypothesis'];\n const kindOptions = kindKeys.map(k => '<option value=\"' + k + '\"' + CO_ENGRAM.tip('kind.' + k) + '>' + CO_ENGRAM.escapeHtml(T.enumLabel('kind', k)) + '</option>').join('');\n\n const filterBar = '<div class=\"filter-bar\">'\n + '<input type=\"search\" placeholder=\"' + CO_ENGRAM.escapeHtml(T.t('engrams.searchPlaceholder')) + '\" id=\"engrams-q\" oninput=\"CO_ENGRAM_ENGRAMS.applyFilter()\">'\n + '<label>' + CO_ENGRAM.escapeHtml(T.t('engrams.filter.kind')) + ' <select id=\"engrams-kind\" onchange=\"CO_ENGRAM_ENGRAMS.applyFilter()\">'\n + '<option value=\"\">' + CO_ENGRAM.escapeHtml(T.t('engrams.filter.kindAll')) + '</option>' + kindOptions + '</select></label>'\n + '<label>' + CO_ENGRAM.escapeHtml(T.t('engrams.filter.sort')) + ' <select id=\"engrams-sort\" onchange=\"CO_ENGRAM_ENGRAMS.applyFilter()\">'\n + '<option value=\"createdAt-desc\">' + CO_ENGRAM.escapeHtml(T.t('engrams.filter.sortNewest')) + '</option>'\n + '<option value=\"createdAt-asc\">' + CO_ENGRAM.escapeHtml(T.t('engrams.filter.sortOldest')) + '</option>'\n + '<option value=\"importance-desc\">' + CO_ENGRAM.escapeHtml(T.t('engrams.filter.sortImportance')) + '</option>'\n + '<option value=\"retrievalCount-desc\">' + CO_ENGRAM.escapeHtml(T.t('engrams.filter.sortRetrievals')) + '</option>'\n + '</select></label>'\n + '<span class=\"spacer\"></span>'\n + '<div class=\"view-toggle\" role=\"group\" aria-label=\"' + CO_ENGRAM.escapeHtml(T.t('engrams.view.card') + ' / ' + T.t('engrams.view.tree')) + '\">'\n + '<button class=\"tab' + (CO_ENGRAM._engramsViewMode === 'card' ? ' active' : '') + '\" onclick=\"CO_ENGRAM_ENGRAMS.setView(\\'card\\')\">' + CO_ENGRAM.escapeHtml(T.t('engrams.view.card')) + '</button>'\n + '<button class=\"tab' + (CO_ENGRAM._engramsViewMode === 'tree' ? ' active' : '') + '\" onclick=\"CO_ENGRAM_ENGRAMS.setView(\\'tree\\')\">' + CO_ENGRAM.escapeHtml(T.t('engrams.view.tree')) + '</button>'\n + '</div>'\n + '<span class=\"chip\" id=\"engrams-count\">' + CO_ENGRAM.escapeHtml(T.t('engrams.countTotal', { n: all.length })) + '</span>'\n + '</div>'\n + '<div id=\"engrams-body\"></div>';\n\n root.innerHTML = filterBar;\n this.applyFilter();\n },\n\n setMode(mode) {\n CO_ENGRAM._engramsViewMode = mode;\n const toggle = document.querySelector('.view-toggle');\n if (toggle) toggle.querySelectorAll('button').forEach(b => b.classList.remove('active'));\n this.applyFilter();\n },\n\n // \u517C\u5BB9\u65E7\u8C03\u7528\u540D\n setView(mode) { this.setMode(mode); },\n\n applyFilter() {\n const cache = CO_ENGRAM._engramsCache || [];\n const q = ((document.getElementById('engrams-q') || {}).value || '').toLowerCase();\n const kind = (document.getElementById('engrams-kind') || {}).value || '';\n const sort = ((document.getElementById('engrams-sort') || {}).value || 'createdAt-desc').split('-');\n const [sortKey, sortDir] = sort;\n const T = CO_ENGRAM_T;\n const mode = CO_ENGRAM._engramsViewMode || 'card';\n\n let filtered = cache.filter(e => {\n if (kind && e.kind !== kind) return false;\n if (q) {\n const title = (e.title || '').toLowerCase();\n const tags = (e.domainTags || []).join(' ').toLowerCase();\n if (!title.includes(q) && !tags.includes(q)) return false;\n }\n return true;\n });\n filtered.sort((a, b) => {\n const av = a[sortKey] || 0;\n const bv = b[sortKey] || 0;\n if (typeof av === 'string' && typeof bv === 'string') {\n return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);\n }\n return sortDir === 'asc' ? av - bv : bv - av;\n });\n\n const body = document.getElementById('engrams-body');\n if (!body) return;\n const countEl = document.getElementById('engrams-count');\n if (countEl) countEl.textContent = T.t('engrams.countFiltered', { shown: filtered.length, total: cache.length });\n\n if (!filtered.length) {\n body.innerHTML = '<div class=\"empty\"><div class=\"icon\">\uD83D\uDD73\uFE0F</div>' + CO_ENGRAM.escapeHtml(T.t('engrams.empty')) + '</div>';\n return;\n }\n\n if (mode === 'tree') {\n // \u76EE\u5F55\u89C6\u56FE:\u6309 domainTags[0] \u6216 kind \u5206\u7EC4\n CO_ENGRAM_ENGRAMS._renderTree(filtered, body);\n return;\n }\n\n // \u5361\u7247\u89C6\u56FE\n body.innerHTML = '<div class=\"grid cols-3\">' + filtered.map(e => {\n const tags = (e.domainTags || []).slice(0, 4)\n .map(t => '<span class=\"chip\">' + CO_ENGRAM.escapeHtml(t) + '</span>').join(' ');\n const more = (e.domainTags || []).length > 4 ? '<span class=\"chip\">+' + ((e.domainTags || []).length - 4) + '</span>' : '';\n const kindTip = CO_ENGRAM.tip('kind.' + e.kind);\n const createdCell = e.createdAt\n ? '<span title=\"' + CO_ENGRAM.escapeHtml(e.createdAt) + '\">' + CO_ENGRAM.escapeHtml(CO_ENGRAM.relativeTime(e.createdAt)) + '</span>'\n : '';\n return '<div class=\"card\">'\n + '<div class=\"card-title\" onclick=\"CO_ENGRAM_ENGRAMS.open(\\'' + CO_ENGRAM.escapeHtml(e.id) + '\\')\">' + CO_ENGRAM.escapeHtml(e.title) + '</div>'\n + '<div><span class=\"chip kind-' + e.kind + '\"' + kindTip + '>' + CO_ENGRAM.escapeHtml(T.enumLabel('kind', e.kind)) + '</span> '\n + CO_ENGRAM.importanceBar(e.importance) + '</div>'\n + '<div class=\"card-meta\">'\n + (e.retrievalCount != null ? '<span' + CO_ENGRAM.tip('retrievalCount') + '>' + CO_ENGRAM.escapeHtml(T.t('engrams.retrievalsCount', { n: e.retrievalCount })) + '</span>' : '')\n + createdCell\n + '</div>'\n + (tags ? '<div class=\"card-meta\">' + tags + more + '</div>' : '')\n + '</div>';\n }).join('') + '</div>';\n },\n\n // \u76EE\u5F55\u89C6\u56FE:\u6309 domainTags[0](\u65E0\u5219\u5F52\u5165\"\u672A\u5206\u7C7B\")\u2192 kind \u4E24\u5C42\u7ED3\u6784\n _renderTree(items, body) {\n const T = CO_ENGRAM_T;\n const groups = new Map(); // groupKey \u2192 { display, items: [] }\n for (const e of items) {\n const topTag = (e.domainTags || [])[0] || '__untagged__';\n const display = topTag === '__untagged__' ? T.t('engrams.untagged') : topTag;\n if (!groups.has(topTag)) groups.set(topTag, { display, items: [] });\n groups.get(topTag).items.push(e);\n }\n // \u6392\u5E8F:\u672A\u5206\u7C7B\u6700\u540E,\u5176\u4ED6\u6309\u5B57\u6BCD\n const sortedGroups = Array.from(groups.entries()).sort((a, b) => {\n if (a[0] === '__untagged__') return 1;\n if (b[0] === '__untagged__') return -1;\n return a[1].display.localeCompare(b[1].display);\n });\n\n let html = '<div class=\"tree-view\">';\n sortedGroups.forEach(([key, group], gi) => {\n const gid = 'tree-group-' + gi;\n const kindSubGroups = new Map();\n for (const e of group.items) {\n if (!kindSubGroups.has(e.kind)) kindSubGroups.set(e.kind, []);\n kindSubGroups.get(e.kind).push(e);\n }\n const kindKeys = Array.from(kindSubGroups.keys()).sort();\n const subRows = kindKeys.map(k => {\n const subItems = kindSubGroups.get(k);\n const subId = gid + '-k-' + k;\n const itemRows = subItems.map(e =>\n '<div class=\"tree-leaf\" onclick=\"CO_ENGRAM_ENGRAMS.open(\\'' + CO_ENGRAM.escapeHtml(e.id) + '\\')\"' + CO_ENGRAM.tip('kind.' + e.kind) + '>'\n + '<span class=\"chip kind-' + e.kind + '\">' + CO_ENGRAM.escapeHtml(T.enumLabel('kind', e.kind)) + '</span> '\n + CO_ENGRAM.escapeHtml(e.title)\n + '</div>'\n ).join('');\n return '<details class=\"tree-subgroup\" open>'\n + '<summary><span class=\"chip kind-' + k + '\"' + CO_ENGRAM.tip('kind.' + k) + '>' + CO_ENGRAM.escapeHtml(T.enumLabel('kind', k)) + '</span> <span class=\"tree-count\">' + subItems.length + '</span></summary>'\n + '<div class=\"tree-leaf-group\">' + itemRows + '</div>'\n + '</details>';\n }).join('');\n\n html += '<details class=\"tree-group\" open>'\n + '<summary><span class=\"tree-folder-icon\">\uD83D\uDCC1</span> ' + CO_ENGRAM.escapeHtml(group.display) + ' <span class=\"tree-count\">' + group.items.length + '</span></summary>'\n + '<div class=\"tree-group-body\">' + subRows + '</div>'\n + '</details>';\n });\n html += '</div>';\n body.innerHTML = html;\n },\n\n async open(id) {\n let d;\n try { d = await CO_ENGRAM.apiGet('/api/engrams/' + encodeURIComponent(id)); }\n catch (e) { CO_ENGRAM.openDrawer('<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>'); return; }\n CO_ENGRAM._currentEngram = d;\n this._renderView(d);\n },\n\n _renderView(d) {\n const T = CO_ENGRAM_T;\n const D = CO_ENGRAM_DECAY;\n const id = CO_ENGRAM.escapeHtml(d.id);\n const tags = (d.domainTags || []).map(t => '<span class=\"chip\">' + CO_ENGRAM.escapeHtml(t) + '</span>').join(' ');\n const ctxTags = (d.contextTags || []).map(t => '<span class=\"chip\">' + CO_ENGRAM.escapeHtml(t) + '</span>').join(' ');\n\n // \u4EF7\u503C\u8BC4\u4F30\u6BB5(emotionalValence + sourceType + verificationStatus + \u8870\u9000\u8FDB\u5EA6 + \u5F3A\u5316\u4FE1\u53F7)\n const valence = d.emotionalValence;\n const valenceLine = valence\n ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('emotionalValence.' + valence) + '>' + T.fieldLabel('emotionalValence') + '</span>' + CO_ENGRAM.escapeHtml(T.enumLabel('emotionalValence', valence)) + '</div>'\n : '';\n const source = d.sourceType;\n const sourceLine = source\n ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('sourceType.' + source) + '>' + T.fieldLabel('sourceType') + '</span>' + CO_ENGRAM.escapeHtml(T.enumLabel('sourceType', source)) + '</div>'\n : '';\n const verif = d.verificationStatus;\n const verifLine = verif\n ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('verification.' + verif) + '>' + T.fieldLabel('verificationStatus') + '</span>' + CO_ENGRAM.escapeHtml(T.enumLabel('verificationStatus', verif)) + '</div>'\n : '';\n\n // \u8870\u9000\u8FDB\u5EA6\u6BB5(\u66FF\u4EE3\u56FA\u5B9A\u534A\u8870\u671F\u663E\u793A)\n const hasHalfLife = d.decayHalfLifeDays !== undefined && d.decayHalfLifeDays !== null;\n const decay = hasHalfLife ? D.computeDecayState(d.lastEffectiveAt, d.decayHalfLifeDays) : null;\n const decayLine = hasHalfLife\n ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('decayHalfLifeDays') + '>' + T.fieldLabel('decayProgress') + '</span><div class=\"decay-block\">' + D.renderDecayBar(decay, d.decayHalfLifeDays) + '</div></div>'\n : '';\n\n const evidenceLine = d.evidenceCount !== undefined\n ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('evidenceCount') + '>' + T.fieldLabel('evidenceCount') + '</span>' + (d.evidenceCount || 0) + '</div>'\n : '';\n const lastEffLine = d.lastEffectiveAt\n ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('lastEffectiveAt') + '>' + T.fieldLabel('lastEffective') + '</span><span title=\"' + CO_ENGRAM.escapeHtml(d.lastEffectiveAt) + '\">' + CO_ENGRAM.escapeHtml(CO_ENGRAM.relativeTime(d.lastEffectiveAt)) + '</span></div>'\n : '';\n const scoreLine = (d.reinforcementScore !== undefined && d.reinforcementScore !== 0)\n ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('reinforcementScore') + '>' + T.fieldLabel('reinforcementScore') + '</span>' + (d.reinforcementScore || 0).toFixed(2) + '</div>'\n : '';\n const valueSection = (valenceLine || sourceLine || verifLine || decayLine || evidenceLine || lastEffLine || scoreLine)\n ? '<h3>' + T.sectionLabel('valueAssessment') + '</h3>' + valenceLine + sourceLine + verifLine + decayLine + evidenceLine + lastEffLine + scoreLine\n : '';\n\n // \u591A\u7EF4\u91CD\u8981\u6027\u6BB5(\u53EF\u9009)\n const iv = d.importanceVector;\n const ivSection = iv\n ? '<h3' + CO_ENGRAM.tip('importanceVector') + '>' + T.sectionLabel('multiDimImportance') + '</h3>'\n + '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('importanceDim.personal') + '>\u4E2A\u4EBA:</span>' + (iv.personal || 0).toFixed(2)\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('importanceDim.team') + '>\u56E2\u961F:</span>' + (iv.team || 0).toFixed(2)\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('importanceDim.project') + '>\u9879\u76EE:</span>' + (iv.project || 0).toFixed(2)\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('importanceDim.network') + '>\u7F51\u7EDC:</span>' + (iv.network || 0).toFixed(2)\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('importanceDim.temporal') + '>\u65F6\u95F4:</span>' + (iv.temporal || 0).toFixed(2)\n + ' <span class=\"field-label\">\u590D\u5408:</span>' + (iv.composite || 0).toFixed(2) + '</div>'\n : '';\n\n // \u8BB0\u5FC6\u4EA7\u751F\u60C5\u5883\u6BB5(\u53EF\u9009)\u2014 section \u6807\u9898\u5DF2\u8BF4\u660E,\u5185\u5D4C field-label \u5197\u4F59,\u76F4\u63A5\u6E32\u67D3\u5185\u5BB9\n const encCtx = d.encodingContext;\n const persp = d.perspective;\n const encSection = (encCtx || persp)\n ? '<h3' + CO_ENGRAM.tip('encodingContext') + '>' + T.sectionLabel('encodingContext') + '</h3>'\n + (persp ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('perspective') + '>' + T.fieldLabel('perspective') + '</span>' + CO_ENGRAM.escapeHtml(persp) + '</div>' : '')\n + (encCtx ? '<div class=\"field markdown-body\"><div>' + CO_ENGRAM.renderMarkdown(encCtx) + '</div></div>' : '')\n : '';\n\n // \u65F6\u95F4\u5B57\u6BB5\u8865 title \u663E\u793A\u5B8C\u6574 ISO\n const createdAtDisplay = d.createdAt\n ? '<span title=\"' + CO_ENGRAM.escapeHtml(d.createdAt) + '\">' + CO_ENGRAM.escapeHtml(CO_ENGRAM.relativeTime(d.createdAt)) + '</span>'\n : CO_ENGRAM.escapeHtml(d.createdAt || '');\n\n const body = '<div class=\"edit-banner\" style=\"display:flex;gap:0.5rem;align-items:center\">'\n + '<strong style=\"margin-right:auto\">' + T.actionLabel('detailView') + '</strong>'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM_ENGRAMS.edit()\">' + T.actionLabel('edit') + '</button>'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_ENGRAMS.confirmDelete()\">' + T.actionLabel('delete') + '</button>'\n + '</div>'\n + '<h2>' + CO_ENGRAM.escapeHtml(d.title) + '</h2>'\n + '<div class=\"field\"><span class=\"chip kind-' + d.kind + '\"' + CO_ENGRAM.tip('kind.' + d.kind) + '>' + CO_ENGRAM.escapeHtml(T.enumLabel('kind', d.kind)) + '</span> '\n + CO_ENGRAM.importanceBar(d.importance) + ' <span class=\"kpi-sub\"' + CO_ENGRAM.tip('importance') + '>' + T.fieldLabel('importance') + ' ' + (d.importance || 0).toFixed(2) + '</span></div>'\n + '<div class=\"field\"><span class=\"field-label\">' + T.fieldLabel('id') + '</span><code>' + id + '</code></div>'\n + (tags ? '<div class=\"field\"><span class=\"field-label\">' + T.fieldLabel('domainTags') + '</span>' + tags + '</div>' : '')\n + (ctxTags ? '<div class=\"field\"><span class=\"field-label\">' + T.fieldLabel('contextTags') + '</span>' + ctxTags + '</div>' : '')\n + '<h3>' + T.sectionLabel('content') + '</h3><div class=\"markdown-body\">' + CO_ENGRAM.renderMarkdown(d.content || '') + '</div>'\n + '<h3>' + T.sectionLabel('stats') + '</h3>'\n + '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('retrievalCount') + '>' + T.fieldLabel('retrievals') + '</span>' + (d.retrievalCount || 0)\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('effectiveRetrievals') + '>' + T.fieldLabel('effective') + '</span>' + (d.effectiveRetrievals || 0)\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('failedUses') + '>' + T.fieldLabel('failures') + '</span>' + (d.failedUses || 0) + '</div>'\n + '<div class=\"field\"><span class=\"field-label\">' + T.fieldLabel('creator') + '</span>' + CO_ENGRAM.escapeHtml(d.createdBy || '')\n + ' <span class=\"field-label\">' + T.fieldLabel('time') + '</span>' + createdAtDisplay + '</div>'\n + '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('confidence') + '>' + T.fieldLabel('confidence') + '</span>' + (d.confidence || 0).toFixed(2)\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('status.' + (d.status || 'active')) + '>' + T.fieldLabel('status') + '</span>' + CO_ENGRAM.escapeHtml(T.enumLabel('status', d.status))\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('freshness.' + (d.freshness || 'fresh')) + '>' + T.fieldLabel('freshness') + '</span>' + CO_ENGRAM.escapeHtml(T.enumLabel('freshness', d.freshness)) + '</div>'\n + valueSection\n + ivSection\n + encSection;\n CO_ENGRAM.openDrawer(body);\n },\n\n edit() {\n const d = CO_ENGRAM._currentEngram;\n if (!d) return;\n const L = CO_ENGRAM_LABELS;\n const id = CO_ENGRAM.escapeHtml(d.id);\n const kindOptions = Object.keys(L.kind).map(k => '<option value=\"' + k + '\"' + (d.kind === k ? ' selected' : '') + CO_ENGRAM.tip('kind.' + k) + '>' + L.kind[k] + '</option>').join('');\n const visOptions = Object.keys(L.visibility).map(v => '<option value=\"' + v + '\"' + (d.visibility === v ? ' selected' : '') + CO_ENGRAM.tip('visibility.' + v) + '>' + L.visibility[v] + '</option>').join('');\n\n const body = '<div class=\"edit-banner\"><strong>\u7F16\u8F91\u6A21\u5F0F</strong> \u00B7 \u4FEE\u6539\u540E\u70B9\u51FB\"\u4FDD\u5B58\"\u63D0\u4EA4</div>'\n + '<h2>\u7F16\u8F91\u8BB0\u5FC6\u5370\u8FF9</h2>'\n + '<div class=\"field\"><span class=\"field-label\">ID:</span><code>' + id + '</code></div>'\n + '<div class=\"field\"><label class=\"field-label\">\u6807\u9898</label><input id=\"ef-title\" type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(d.title || '') + '\"></div>'\n + '<div class=\"field\"><label class=\"field-label\"' + CO_ENGRAM.tip('kind.fact') + '>\u7C7B\u578B</label><select id=\"ef-kind\"' + CO_ENGRAM.tip('kind.fact') + '>' + kindOptions + '</select></div>'\n + '<div class=\"field\"><label class=\"field-label\"' + CO_ENGRAM.tip('importance') + '>\u91CD\u8981\u6027 (0-1,\u53EF\u62D6\u52A8\u6ED1\u5757)</label><input id=\"ef-importance-range\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" value=\"' + (d.importance || 0) + '\" oninput=\"document.getElementById(\\'ef-importance\\').value=this.value\"><input id=\"ef-importance\" type=\"number\" min=\"0\" max=\"1\" step=\"0.01\" value=\"' + (d.importance || 0) + '\" oninput=\"document.getElementById(\\'ef-importance-range\\').value=this.value\" style=\"width:80px;margin-left:0.5rem\"></div>'\n + '<div class=\"field\"><label class=\"field-label\"' + CO_ENGRAM.tip('confidence') + '>\u7F6E\u4FE1\u5EA6 (0-1,\u53EF\u62D6\u52A8\u6ED1\u5757)</label><input id=\"ef-confidence-range\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" value=\"' + (d.confidence || 0) + '\" oninput=\"document.getElementById(\\'ef-confidence\\').value=this.value\"><input id=\"ef-confidence\" type=\"number\" min=\"0\" max=\"1\" step=\"0.01\" value=\"' + (d.confidence || 0) + '\" oninput=\"document.getElementById(\\'ef-confidence-range\\').value=this.value\" style=\"width:80px;margin-left:0.5rem\"></div>'\n + '<div class=\"field\"><label class=\"field-label\">\u9886\u57DF\u6807\u7B7E(\u9017\u53F7\u5206\u9694)</label><input id=\"ef-tags\" type=\"text\" value=\"' + CO_ENGRAM.escapeHtml((d.domainTags || []).join(', ')) + '\"></div>'\n + '<div class=\"field\"><label class=\"field-label\">\u4E0A\u4E0B\u6587\u6807\u7B7E(\u9017\u53F7\u5206\u9694)</label><input id=\"ef-ctx-tags\" type=\"text\" value=\"' + CO_ENGRAM.escapeHtml((d.contextTags || []).join(', ')) + '\"></div>'\n + '<div class=\"field\"><label class=\"field-label\"' + CO_ENGRAM.tip('visibility.public') + '>\u53EF\u89C1\u6027</label><select id=\"ef-visibility\"' + CO_ENGRAM.tip('visibility.public') + '>' + visOptions + '</select></div>'\n + '<div class=\"field\"><label class=\"field-label\">\u5185\u5BB9(Markdown) '\n + '<button type=\"button\" class=\"btn secondary mini\" id=\"ef-preview-toggle\" onclick=\"CO_ENGRAM_ENGRAMS.togglePreview()\">\u9884\u89C8</button>'\n + '<span id=\"ef-content-mode\" class=\"kpi-sub\">\u7F16\u8F91\u6A21\u5F0F</span></label>'\n + '<textarea id=\"ef-content\" rows=\"12\">' + CO_ENGRAM.escapeHtml(d.content || '') + '</textarea>'\n + '<div id=\"ef-content-preview\" class=\"markdown-body\" style=\"display:none;margin-top:0.5rem\"></div></div>'\n + '<div class=\"config-save-bar\">'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_ENGRAMS.cancel()\">\u53D6\u6D88</button>'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM_ENGRAMS.save()\">\u4FDD\u5B58</button>'\n + '</div>';\n CO_ENGRAM.openDrawer(body);\n },\n\n togglePreview() {\n const ta = document.getElementById('ef-content');\n const preview = document.getElementById('ef-content-preview');\n const toggleBtn = document.getElementById('ef-preview-toggle');\n const modeLabel = document.getElementById('ef-content-mode');\n if (!ta || !preview || !toggleBtn || !modeLabel) return;\n if (ta.style.display === 'none') {\n // \u5F53\u524D\u662F\u9884\u89C8,\u5207\u56DE\u7F16\u8F91\n ta.style.display = '';\n preview.style.display = 'none';\n toggleBtn.textContent = '\u9884\u89C8';\n modeLabel.textContent = '\u7F16\u8F91\u6A21\u5F0F';\n } else {\n // \u5F53\u524D\u662F\u7F16\u8F91,\u5207\u5230\u9884\u89C8\n preview.innerHTML = CO_ENGRAM.renderMarkdown(ta.value);\n ta.style.display = 'none';\n preview.style.display = '';\n toggleBtn.textContent = '\u7F16\u8F91';\n modeLabel.textContent = '\u9884\u89C8\u6A21\u5F0F';\n }\n },\n\n cancel() {\n const d = CO_ENGRAM._currentEngram;\n if (d) this._renderView(d);\n },\n\n async save() {\n const d = CO_ENGRAM._currentEngram;\n if (!d) return;\n const patch = {\n title: (document.getElementById('ef-title').value || '').trim(),\n kind: document.getElementById('ef-kind').value,\n importance: Number(document.getElementById('ef-importance').value),\n confidence: Number(document.getElementById('ef-confidence').value),\n domainTags: (document.getElementById('ef-tags').value || '').split(',').map(s => s.trim()).filter(Boolean),\n contextTags: (document.getElementById('ef-ctx-tags').value || '').split(',').map(s => s.trim()).filter(Boolean),\n visibility: document.getElementById('ef-visibility').value,\n content: document.getElementById('ef-content').value\n };\n try {\n const updated = await CO_ENGRAM.apiJson('/api/engrams/' + encodeURIComponent(d.id), 'PATCH', patch);\n CO_ENGRAM._currentEngram = updated;\n this._renderView(updated);\n // \u5237\u65B0\u5217\u8868\u7F13\u5B58\n try {\n const data = await CO_ENGRAM.apiGet('/api/engrams');\n CO_ENGRAM._engramsCache = data.results || [];\n this.applyFilter();\n } catch {}\n } catch (e) { alert('\u4FDD\u5B58\u5931\u8D25:' + (e.message || e)); }\n },\n\n async confirmDelete() {\n const d = CO_ENGRAM._currentEngram;\n if (!d) return;\n if (!confirm('\u786E\u5B9A\u8981\u5220\u9664\"' + (d.title || d.id) + '\"?\\n\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002')) return;\n try {\n await CO_ENGRAM.apiJson('/api/engrams/' + encodeURIComponent(d.id), 'DELETE', null);\n CO_ENGRAM.closeDrawer();\n CO_ENGRAM._currentEngram = null;\n // \u91CD\u65B0\u52A0\u8F7D\u5217\u8868\n const root = document.getElementById('engrams-content');\n if (root) {\n CO_ENGRAM._engramsLoaded = false;\n CO_ENGRAM._engramsCache = null;\n await CO_ENGRAM_ENGRAMS.render(root);\n }\n } catch (e) { alert('\u5220\u9664\u5931\u8D25:' + (e.message || e)); }\n }\n};\n\n// ============================================================\n// Proposals\n// ============================================================\nCO_ENGRAM.on('proposals', async function() {\n const root = document.getElementById('proposals-content');\n if (!root) return;\n if (CO_ENGRAM._proposalsLoaded) return;\n CO_ENGRAM._proposalsLoaded = true;\n await CO_ENGRAM_PROPOSALS.render(root);\n});\n\nwindow.CO_ENGRAM_PROPOSALS = {\n async render(root) {\n root.innerHTML = '<div class=\"loading\">\u52A0\u8F7D\u63D0\u6848\u4E2D</div>';\n let data;\n try { data = await CO_ENGRAM.apiGet('/api/proposals?status=all'); }\n catch (e) { root.innerHTML = '<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>'; return; }\n\n if (data.enabled === false) {\n root.innerHTML = '<div class=\"empty\"><div class=\"icon\">\uD83D\uDCA4</div>\u63D0\u6848\u5F15\u64CE\u672A\u542F\u7528\u3002\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF CO_ENGRAM_PROPOSALS_ENABLED=1 \u53EF\u5F00\u542F\u3002</div>';\n return;\n }\n\n const all = data.results || [];\n CO_ENGRAM._proposalsCache = all;\n this._setStatus('pending');\n },\n\n /**\n * \u542F\u53D1\u5F0F\u63A8\u65AD proposal \u7684\u6807\u9898\u548C\u7C7B\u578B(\u540E\u7AEF Proposal \u6CA1\u6709\u8FD9\u4E24\u4E2A\u5B57\u6BB5)\u3002\n *\n * \u6807\u9898:\u53D6 centroidExcerpt \u9996\u53E5,\u8D85\u8FC7 50 \u5B57\u622A\u65AD;\u7A7A\u65F6\u56DE\u9000 entityId\u3002\n * \u7C7B\u578B:\u57FA\u4E8E\u5173\u952E\u8BCD\u5339\u914D\u3002\u4E2D\u82F1\u53CC\u8BED\u5173\u952E\u8BCD\u8986\u76D6 5 \u79CD EngramKind\u3002\n * - procedure:\u6B65\u9AA4/\u6D41\u7A0B/how to/step\n * - fact:\u5E94\u8BE5/\u5FC5\u987B/always/never/\u4E8B\u5B9E\n * - hypothesis:\u4E5F\u8BB8/\u53EF\u80FD/maybe/probably/\u5047\u8BBE\n * - pattern:\u89C4\u5F8B/\u603B\u662F/usually/pattern\n * - observation:\u89C2\u5BDF\u5230/noticed/\u770B\u5230\n * \u9ED8\u8BA4 observation(\u4E2D\u6027\u3001\u4E0D\u5F3A\u884C\u731C)\u3002\n */\n _inferMeta(p) {\n const text = (p.centroidExcerpt || (p.sampleQuotes || [])[0] || '').toString();\n const lower = text.toLowerCase();\n\n let kind = 'observation';\n if (/(\u6B65\u9AA4|\u6D41\u7A0B|\u600E\u4E48|\u5982\u4F55|how to|step|procedure|process|\u7B97\u6CD5|\u6D41\u7A0B\u56FE)/i.test(text)) kind = 'procedure';\n else if (/(\u5E94\u8BE5|\u5FC5\u987B|\u603B\u662F|\u4E8B\u5B9E|always|never|must|fact|\u89C4\u5219|\u5B9A\u5F8B)/i.test(text)) kind = 'fact';\n else if (/(\u4E5F\u8BB8|\u53EF\u80FD|\u731C\u6D4B|\u5047\u8BBE|maybe|probably|hypoth|hypo|\u731C\u6D4B)/i.test(text)) kind = 'hypothesis';\n else if (/(\u89C4\u5F8B|\u6A21\u5F0F|\u901A\u5E38|\u60EF|pattern|usually|tend to|often)/i.test(text)) kind = 'pattern';\n else if (/(\u89C2\u5BDF|\u770B\u5230|\u53D1\u73B0|noticed|observed|saw|found)/i.test(text)) kind = 'observation';\n\n let title = text.trim();\n if (title) {\n // \u53D6\u9996\u53E5\u4F5C\u4E3A\u6807\u9898\u3002\u6B63\u5219\u5B57\u9762\u91CF\u91CC\u7684\u6362\u884C\u8F6C\u4E49\u5728\u6A21\u677F\u5B57\u7B26\u4E32\u91CC\u5FC5\u987B\u53CC\u91CD\u8F6C\u4E49,\n // \u5426\u5219\u88AB\u5F53\u6210\u5B57\u7B26\u4E32\u7EA7 escape,\u53D8\u6210\u771F\u5B9E\u6362\u884C\u7B26,\u7834\u574F\u6B63\u5219\u8BED\u6CD5\u3002\n const firstClause = title.split(/[\u3002.!?\\n??;\uFF1B]/)[0].trim();\n title = firstClause || title;\n if (title.length > 50) title = title.slice(0, 50) + '\u2026';\n }\n if (!title) title = p.entityId;\n\n return { title, kind };\n },\n\n _setStatus(status) {\n const cache = CO_ENGRAM._proposalsCache || [];\n const filtered = status === 'all' ? cache : cache.filter(p => p.status === status);\n const root = document.getElementById('proposals-content');\n if (!root) return;\n\n const L = CO_ENGRAM_LABELS;\n const STATUS_LABEL = { pending: '\u5F85\u5BA1', accepted: '\u5DF2\u91C7\u7EB3', dismissed: '\u5DF2\u9A73\u56DE', all: '\u5168\u90E8' };\n\n const buttons = (current) => ['pending', 'accepted', 'dismissed', 'all'].map(s =>\n '<button class=\"tab ' + (s === current ? 'active' : '') + '\" onclick=\"CO_ENGRAM_PROPOSALS._setStatus(\\'' + s + '\\')\">'\n + STATUS_LABEL[s] + ' (' + (s === 'all' ? cache.length : cache.filter(p => p.status === s).length) + ')</button>'\n ).join('');\n\n let html = '<div style=\"margin-bottom:1rem\">' + buttons(status) + '</div>';\n if (!filtered.length) {\n html += '<div class=\"empty\"><div class=\"icon\">\u2713</div>\u6CA1\u6709 ' + STATUS_LABEL[status] + ' \u63D0\u6848</div>';\n } else {\n html += '<div class=\"grid cols-3\">';\n for (const p of filtered) {\n const meta = this._inferMeta(p);\n const kindLabel = (L.kind && L.kind[meta.kind]) || meta.kind;\n const preview = (p.centroidExcerpt || (p.sampleQuotes || [])[0] || '').toString();\n const previewClip = preview.length > 120 ? preview.slice(0, 120) + '\u2026' : preview;\n const cardClick = p.status === 'pending'\n ? ' style=\"cursor:pointer\" onclick=\"CO_ENGRAM_PROPOSALS.open(\\'' + CO_ENGRAM.escapeHtml(p.entityId) + '\\')\"'\n : ' style=\"cursor:pointer\" onclick=\"CO_ENGRAM_PROPOSALS.open(\\'' + CO_ENGRAM.escapeHtml(p.entityId) + '\\')\"';\n\n html += '<div class=\"card\"' + cardClick + '>'\n + '<div class=\"card-title\" title=\"' + CO_ENGRAM.escapeHtml(p.entityId) + '\">' + CO_ENGRAM.escapeHtml(meta.title) + '</div>';\n html += '<div class=\"card-meta\" style=\"margin-bottom:0.4rem\">'\n + '<span class=\"chip kind-' + meta.kind + '\">' + CO_ENGRAM.escapeHtml(kindLabel) + '</span>'\n + '<span>\u00D7' + (p.occurrences || 0) + '</span>'\n + (p.createdAt ? '<span>' + CO_ENGRAM.relativeTime(p.createdAt) + '</span>' : '')\n + '<span class=\"chip\">' + (STATUS_LABEL[p.status] || p.status) + '</span>'\n + '</div>';\n if (previewClip) html += '<div style=\"font-size:0.8rem;color:var(--fg-muted);margin-bottom:0.4rem\">' + CO_ENGRAM.escapeHtml(previewClip) + '</div>';\n if (p.status === 'accepted' && p.acceptedEngramId) {\n html += '<div class=\"card-meta\"><span class=\"chip\" style=\"background:rgba(16,185,129,.12);color:var(--accent)\">\u5DF2\u8F6C \u25B8 ' + CO_ENGRAM.escapeHtml(p.acceptedEngramId.slice(0, 12)) + '</span></div>';\n }\n if (p.status === 'dismissed' && p.dismissReason) {\n html += '<div class=\"card-meta\"><span class=\"chip\" style=\"background:rgba(239,68,68,.12);color:#ef4444\">\u9A73\u56DE:' + CO_ENGRAM.escapeHtml((p.dismissReason || '').slice(0, 40)) + '</span></div>';\n }\n html += '</div>';\n }\n html += '</div>';\n }\n root.innerHTML = html;\n },\n\n /**\n * \u6253\u5F00 proposal \u8BE6\u60C5 drawer,\u63D0\u4F9B\u5B8C\u6574\u7F16\u8F91\u8868\u5355\u3002\n * \u7528\u6237\u53EF\u4EE5\u5728\u8FD9\u91CC\u8C03\u6574 title/kind/content/domainTags,\u7136\u540E Accept \u6216 Dismiss\u3002\n * \u66FF\u4EE3\u539F\u6765 prompt() \u7684\u7B80\u964B\u4EA4\u4E92\u3002\n */\n open(entityId) {\n const cache = CO_ENGRAM._proposalsCache || [];\n const p = cache.find(x => x.entityId === entityId);\n if (!p) { CO_ENGRAM.openDrawer('<div class=\"empty\">\u63D0\u6848\u672A\u627E\u5230:' + CO_ENGRAM.escapeHtml(entityId) + '</div>'); return; }\n CO_ENGRAM._currentProposal = p;\n\n const L = CO_ENGRAM_LABELS;\n const meta = this._inferMeta(p);\n const samples = (p.sampleQuotes || []).map(s => '<pre class=\"pre-compact\" style=\"margin:0.3rem 0\">' + CO_ENGRAM.escapeHtml(s) + '</pre>').join('');\n const kindOptions = Object.keys(L.kind || {}).map(k =>\n '<option value=\"' + k + '\"' + (meta.kind === k ? ' selected' : '') + '>' + (L.kind[k] || k) + '</option>'\n ).join('');\n\n const accepted = p.status === 'accepted';\n const dismissed = p.status === 'dismissed';\n const editable = p.status === 'pending';\n\n let actionBtns = '';\n if (editable) {\n actionBtns = '<div class=\"config-save-bar\">'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_PROPOSALS.dismissFromForm()\">\u9A73\u56DE</button>'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM_PROPOSALS.acceptFromForm()\">\u91C7\u7EB3\u5E76\u4FDD\u5B58</button>'\n + '</div>';\n } else {\n actionBtns = '<div class=\"edit-banner\">\u8BE5\u63D0\u6848\u5F53\u524D\u72B6\u6001:<strong>' + (L.status && L.status[p.status] || p.status) + '</strong>'\n + (accepted && p.acceptedEngramId ? '<br>\u5DF2\u521B\u5EFA\u8BB0\u5FC6\u5370\u8FF9:<code>' + CO_ENGRAM.escapeHtml(p.acceptedEngramId) + '</code>' : '')\n + (dismissed && p.dismissedUntil ? '<br>\u9A73\u56DE\u81F3:' + CO_ENGRAM.escapeHtml(p.dismissedUntil) : '')\n + '</div>';\n }\n\n const body = '<div class=\"edit-banner\" style=\"display:flex;gap:0.5rem;align-items:center\">'\n + '<strong style=\"margin-right:auto\">\u5019\u9009\u63D0\u6848\u8BE6\u60C5</strong>'\n + '<code style=\"font-size:0.75rem\">' + CO_ENGRAM.escapeHtml(p.entityId) + '</code>'\n + '</div>'\n + '<div class=\"field\"' + (editable ? '' : ' style=\"opacity:0.6\"') + '>'\n + '<label class=\"field-label\">\u6807\u9898' + (editable ? '' : ' (\u53EA\u8BFB)') + '</label>'\n + '<input id=\"pf-title\" type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(meta.title) + '\"' + (editable ? '' : ' readonly') + '></div>'\n + '<div class=\"field\"'\n + (editable ? '' : ' style=\"opacity:0.6\"') + '>'\n + '<label class=\"field-label\"' + CO_ENGRAM.tip('kind.fact') + '>\u7C7B\u578B' + (editable ? '' : ' (\u53EA\u8BFB)') + '</label>'\n + '<select id=\"pf-kind\"' + (editable ? '' : ' disabled') + '>' + kindOptions + '</select></div>'\n + '<div class=\"field\"' + (editable ? '' : ' style=\"opacity:0.6\"') + '>'\n + '<label class=\"field-label\">\u9886\u57DF\u6807\u7B7E(\u9017\u53F7\u5206\u9694)' + (editable ? '' : ' (\u53EA\u8BFB)') + '</label>'\n + '<input id=\"pf-tags\" type=\"text\" placeholder=\"\u5982:frontend, dark-mode, css\"' + (editable ? '' : ' readonly') + '></div>'\n + '<div class=\"field\"' + (editable ? '' : ' style=\"opacity:0.6\"') + '>'\n + '<label class=\"field-label\">\u5185\u5BB9(\u8F6C\u6210\u8BB0\u5FC6\u5370\u8FF9\u7684\u6B63\u6587)' + (editable ? '' : ' (\u53EA\u8BFB)') + '</label>'\n + '<textarea id=\"pf-content\" rows=\"6\"' + (editable ? '' : ' readonly') + '>' + CO_ENGRAM.escapeHtml(p.centroidExcerpt || '') + '</textarea></div>'\n + '<h3>\u6837\u672C\u5F15\u7528(' + (p.occurrences || 0) + ' \u6B21\u7D2F\u79EF)</h3>'\n + (samples || '<div class=\"empty\" style=\"padding:1rem\">(\u65E0\u6837\u672C)</div>')\n + '<div class=\"field\"><span class=\"field-label\">\u9996\u6B21\u89C1\u5230:</span>' + CO_ENGRAM.escapeHtml(p.firstSeenAt || '\u2014')\n + ' <span class=\"field-label\">\u6700\u540E\u89C1\u5230:</span>' + CO_ENGRAM.escapeHtml(p.lastSeenAt || '\u2014') + '</div>'\n + actionBtns;\n\n CO_ENGRAM.openDrawer(body);\n },\n\n async acceptFromForm() {\n const p = CO_ENGRAM._currentProposal;\n if (!p) return;\n const title = (document.getElementById('pf-title').value || '').trim();\n const content = (document.getElementById('pf-content').value || '').trim();\n const kind = document.getElementById('pf-kind').value;\n const tags = (document.getElementById('pf-tags').value || '').split(',').map(s => s.trim()).filter(Boolean);\n if (!title) { alert('\u8BF7\u586B\u5199\u6807\u9898'); return; }\n if (!content) { alert('\u8BF7\u586B\u5199\u5185\u5BB9'); return; }\n try {\n const r = await CO_ENGRAM.apiJson('/api/proposals/' + encodeURIComponent(p.entityId) + '/accept', 'POST', { title, content, kind, domainTags: tags });\n CO_ENGRAM.closeDrawer();\n CO_ENGRAM._proposalsLoaded = false;\n await this.render(document.getElementById('proposals-content'));\n const engramId = r && r.engramId ? r.engramId : '';\n alert('\u2713 \u5DF2\u91C7\u7EB3' + (engramId ? '\\n\u521B\u5EFA\u8BB0\u5FC6\u5370\u8FF9:' + engramId : ''));\n } catch (e) { alert('\u91C7\u7EB3\u5931\u8D25:' + (e.message || e)); }\n },\n\n async dismissFromForm() {\n const p = CO_ENGRAM._currentProposal;\n if (!p) return;\n const reason = prompt('\u9A73\u56DE\u7406\u7531(\u53EF\u9009):', '') || '';\n const daysStr = prompt('\u9A73\u56DE N \u5929(\u9ED8\u8BA4 30):', '30') || '30';\n const dismissDays = Number(daysStr) || 30;\n try {\n await CO_ENGRAM.apiJson('/api/proposals/' + encodeURIComponent(p.entityId) + '/dismiss', 'POST', { reason, dismissDays });\n CO_ENGRAM.closeDrawer();\n CO_ENGRAM._proposalsLoaded = false;\n await this.render(document.getElementById('proposals-content'));\n } catch (e) { alert('\u9A73\u56DE\u5931\u8D25:' + (e.message || e)); }\n },\n\n /** @deprecated \u4FDD\u7559\u65E7 API \u517C\u5BB9(\u4ECE\u5176\u4ED6\u5730\u65B9\u8C03\u7528),\u5185\u90E8\u8D70 open() */\n async accept(entityId) {\n this.open(entityId);\n },\n\n async dismiss(entityId) {\n const p = (CO_ENGRAM._proposalsCache || []).find(x => x.entityId === entityId);\n if (!p) return;\n CO_ENGRAM._currentProposal = p;\n await this.dismissFromForm();\n }\n};\n\n// ============================================================\n// Audit\n// ============================================================\nCO_ENGRAM.on('audit', async function() {\n const root = document.getElementById('audit-content');\n if (!root) return;\n if (CO_ENGRAM._auditLoaded) return;\n CO_ENGRAM._auditLoaded = true;\n\n const filterBar = '<div class=\"filter-bar\">'\n + '<label>\u53D1\u8D77\u8005 <select id=\"audit-actor\" onchange=\"CO_ENGRAM_AUDIT.applyFilter()\">'\n + '<option value=\"\">\u5168\u90E8</option><option value=\"user\">\u7528\u6237</option><option value=\"llm\">LLM</option><option value=\"system\">\u7CFB\u7EDF</option></select></label>'\n + '<label>\u7C7B\u522B <select id=\"audit-cat\" onchange=\"CO_ENGRAM_AUDIT.applyFilter()\">'\n + '<option value=\"\">\u5168\u90E8</option>'\n + '<option value=\"state\">\u72B6\u6001\u53D8\u66F4</option>'\n + '<option value=\"effective\">\u6709\u6548\u6027</option>'\n + '<option value=\"contradicted\">\u77DB\u76FE</option>'\n + '<option value=\"proposal\">\u63D0\u6848</option></select></label>'\n + '<input type=\"search\" id=\"audit-engram\" placeholder=\"\u6309\u8BB0\u5FC6\u5370\u8FF9\u7F16\u53F7\u8FC7\u6EE4...\" oninput=\"CO_ENGRAM_AUDIT.applyFilter()\">'\n + '<span class=\"chip removable audit-action-chip\" id=\"audit-action-chip\" style=\"display:none\" title=\"\u70B9\u51FB\u6E05\u9664 action \u8FC7\u6EE4\" onclick=\"CO_ENGRAM_AUDIT.clearActionFilter()\"></span>'\n + '<span class=\"spacer\"></span>'\n + '<span class=\"chip\" id=\"audit-count\">\u2014</span>'\n + '</div>'\n + '<div id=\"audit-stats\" class=\"kpi-grid\" style=\"margin-bottom:1rem\"></div>'\n + '<div id=\"audit-timeline\" class=\"timeline\"></div>';\n root.innerHTML = filterBar;\n await CO_ENGRAM_AUDIT.load();\n});\n\nwindow.CO_ENGRAM_AUDIT = {\n async load() {\n const tl = document.getElementById('audit-timeline');\n if (!tl) return;\n tl.innerHTML = '<div class=\"loading\">\u52A0\u8F7D\u5BA1\u8BA1\u65E5\u5FD7\u4E2D</div>';\n let data, engramsData;\n try {\n // \u5E76\u884C\u62C9 audit + engrams(\u540E\u8005\u7528\u6765\u5224\u65AD engramId \u662F\u5426\u4ECD\u5B58\u5728,\u51B3\u5B9A\u663E\u793A\u53EF\u70B9 chip \u8FD8\u662F\u7070\u8272)\n [data, engramsData] = await Promise.all([\n CO_ENGRAM.apiGet('/api/audit?limit=500'),\n CO_ENGRAM.apiGet('/api/engrams?limit=10000').catch(() => ({ results: [] })),\n ]);\n } catch (e) { tl.innerHTML = '<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>'; return; }\n\n if (data.enabled === false) {\n tl.innerHTML = '<div class=\"empty\"><div class=\"icon\">\uD83D\uDCA4</div>\u5BA1\u8BA1\u65E5\u5FD7\u672A\u542F\u7528\u3002</div>';\n return;\n }\n this._existingIds = new Set((engramsData.results || []).map(e => e.id));\n this._cache = (data.results || []).slice().sort((a, b) => (a.ts < b.ts ? 1 : -1));\n this._renderStats();\n this.applyFilter();\n },\n\n _renderStats() {\n const el = document.getElementById('audit-stats');\n if (!el) return;\n const cache = this._cache || [];\n const cat = { state: 0, effective: 0, contradicted: 0, proposal: 0 };\n for (const e of cache) {\n const cls = CO_ENGRAM.auditActionClass(e.action);\n if (cls === 'audit-state') cat.state++;\n else if (cls === 'audit-effective') cat.effective++;\n else if (cls === 'audit-contradicted') cat.contradicted++;\n else cat.proposal++;\n }\n const kpi = (label, n, color) => '<div class=\"kpi\"><div class=\"kpi-label\">' + label + '</div>'\n + '<div class=\"kpi-value\" style=\"color:' + color + '\">' + n + '</div></div>';\n el.innerHTML = kpi('\u603B\u8BA1', cache.length, 'var(--fg)')\n + kpi('\u72B6\u6001\u53D8\u66F4', cat.state, '#3b82f6')\n + kpi('\u6709\u6548\u6027\u4FE1\u53F7', cat.effective, '#10b981')\n + kpi('\u77DB\u76FE', cat.contradicted, '#ef4444')\n + kpi('\u63D0\u6848', cat.proposal, '#8b5cf6');\n },\n\n applyFilter() {\n const cache = this._cache || [];\n const actor = (document.getElementById('audit-actor') || {}).value || '';\n const cat = (document.getElementById('audit-cat') || {}).value || '';\n const engramQ = ((document.getElementById('audit-engram') || {}).value || '').toLowerCase();\n const actionFilter = this._actionFilter || '';\n\n const filtered = cache.filter(e => {\n if (actor && e.actor !== actor) return false;\n if (cat && CO_ENGRAM.auditActionClass(e.action) !== 'audit-' + cat) return false;\n if (engramQ && !(e.engramId || '').toLowerCase().includes(engramQ)) return false;\n if (actionFilter && e.action !== actionFilter) return false;\n return true;\n });\n\n const countEl = document.getElementById('audit-count');\n if (countEl) countEl.textContent = filtered.length + ' / ' + cache.length;\n\n // \u540C\u6B65 action chip \u663E\u793A\n const chipEl = document.getElementById('audit-action-chip');\n if (chipEl) {\n if (actionFilter) {\n chipEl.style.display = 'inline-flex';\n chipEl.innerHTML = 'action=<code>' + CO_ENGRAM.escapeHtml(actionFilter) + '</code> \u2715';\n } else {\n chipEl.style.display = 'none';\n chipEl.innerHTML = '';\n }\n }\n\n const tl = document.getElementById('audit-timeline');\n if (!tl) return;\n if (!filtered.length) {\n tl.innerHTML = '<div class=\"empty\"><div class=\"icon\">\u2014</div>\u6CA1\u6709\u5339\u914D\u7684\u4E8B\u4EF6</div>';\n return;\n }\n\n const ACTOR_LETTER = { user: 'U', llm: 'L', system: 'S' };\n const ACTOR_TIP = { user: '\u7528\u6237 (user):\u7531\u4EBA\u5DE5\u89E6\u53D1\u7684\u4E8B\u4EF6', llm: 'LLM (llm):\u7531\u8BED\u8A00\u6A21\u578B agent \u89E6\u53D1\u7684\u4E8B\u4EF6', system: '\u7CFB\u7EDF (system):\u7531\u540E\u53F0\u7EF4\u62A4/\u81EA\u6108\u6D41\u7A0B\u89E6\u53D1\u7684\u4E8B\u4EF6' };\n const ACTION_TIP = {\n // \u72B6\u6001\u53D8\u66F4\n create: 'create:\u521B\u5EFA\u65B0\u8BB0\u5FC6\u5370\u8FF9',\n update: 'update:\u4FEE\u6539\u5DF2\u6709\u5370\u8FF9\u7684\u5B57\u6BB5',\n update_lifecycle: 'update_lifecycle:\u72B6\u6001\u8FC1\u79FB(archived/forgotten)',\n reinforce: 'reinforce:\u5F3A\u5316(LTP)\u2014 \u68C0\u7D22\u6709\u6548\u3001\u95ED\u73AF\u6210\u529F',\n report_failure: 'report_failure:\u8D1F\u5411\u53CD\u9988(LTD)\u2014 \u68C0\u7D22\u4E0D\u51C6\u3001\u95ED\u73AF\u5931\u8D25',\n forget: 'forget:\u6807\u8BB0\u4E3A forgotten',\n restore: 'restore:\u4ECE forgotten/archived \u6062\u590D\u4E3A active',\n sweep_to_trash: 'sweep_to_trash:forgotten \u6EE1 30 \u5929,\u6587\u4EF6\u79FB\u5230 .trash/',\n restore_from_trash: 'restore_from_trash:\u4ECE .trash/ \u7269\u7406\u6062\u590D',\n purge: 'purge:\u786C\u5220\u9664(\u5185\u5BB9 + \u5143 + \u5173\u8054\u7A81\u89E6)',\n // \u6709\u6548\u6027\n retrieve_hit: 'retrieve_hit:\u641C\u7D22\u547D\u4E2D',\n retrieve_effective: 'retrieve_effective:\u547D\u4E2D\u540E\u88AB\u5B9E\u9645\u91C7\u7528',\n retrieve_inconclusive: 'retrieve_inconclusive:\u547D\u4E2D\u4F46\u4E0D\u786E\u5B9A\u662F\u5426\u6709\u6548',\n // \u77DB\u76FE\n contradicted: 'contradicted:\u68C0\u6D4B\u5230\u4E0E\u5176\u4ED6\u5370\u8FF9\u51B2\u7A81,\u8FDB\u5165\u88C1\u51B3\u6D41\u7A0B',\n // \u63D0\u6848\n propose: 'propose:\u6355\u83B7\u5230\u5019\u9009\u8BB0\u5FC6',\n accept: 'accept:\u91C7\u7EB3\u5019\u9009,\u8F6C\u5316\u4E3A\u6B63\u5F0F\u5370\u8FF9',\n dismiss: 'dismiss:\u9A73\u56DE\u5019\u9009'\n };\n tl.innerHTML = filtered.slice(0, 300).map(e => {\n return CO_ENGRAM_AUDIT.renderRow(e, ACTOR_LETTER, ACTOR_TIP, ACTION_TIP);\n }).join('');\n },\n\n /** \u70B9\u51FB action \u6807\u7B7E \u2192 \u6309\u8BE5 action \u7CBE\u786E\u8FC7\u6EE4;\u518D\u6B21\u70B9\u540C\u4E00\u4E2A \u2192 \u6E05\u9664 */\n filterByAction(action) {\n if (!action) return;\n this._actionFilter = this._actionFilter === action ? '' : action;\n this.applyFilter();\n },\n\n /** \u6E05\u9664 action \u8FC7\u6EE4(\u70B9\u51FB chip \u89E6\u53D1) */\n clearActionFilter() {\n this._actionFilter = '';\n this.applyFilter();\n },\n\n /** \u628A\u4EFB\u610F metadata value \u6E32\u67D3\u4E3A\u53EF\u8BFB\u7684\u77ED\u5B57\u7B26\u4E32(\u5355\u503C,\u7528\u4E8E chip / kv) */\n _formatVal(v) {\n if (v == null) return 'null';\n if (typeof v === 'string') {\n const s = v.length > 60 ? v.slice(0, 57) + '\u2026' : v;\n return '\"' + s + '\"';\n }\n if (typeof v === 'number' || typeof v === 'boolean') return String(v);\n if (Array.isArray(v)) {\n if (v.length === 0) return '[]';\n const sample = v.slice(0, 3).map(x => typeof x === 'string' ? x : JSON.stringify(x)).join(', ');\n return '[' + sample + (v.length > 3 ? ', \u2026\u00D7' + (v.length - 3) : '') + ']';\n }\n try { return JSON.stringify(v); } catch { return String(v); }\n },\n\n /** \u7ED3\u6784\u5316\u6E32\u67D3 audit metadata */\n renderMeta(e) {\n const m = e.metadata || {};\n const keys = Object.keys(m);\n if (keys.length === 0) return '<span class=\"audit-meta-empty\">\u2014</span>';\n\n // 1. update \u7C7B:\u6709 changes \u5B57\u6BB5 \u2192 \u6E32\u67D3 changed fields\n if (m.changes && typeof m.changes === 'object' && !Array.isArray(m.changes)) {\n const fields = Object.keys(m.changes);\n if (fields.length === 0) {\n return '<span class=\"audit-meta-empty\">(\u65E0\u5B57\u6BB5\u5B9E\u9645\u53D8\u5316)</span>';\n }\n const rows = fields.map(f => {\n const ch = m.changes[f] || {};\n const from = CO_ENGRAM.escapeHtml(CO_ENGRAM_AUDIT._formatVal(ch.from));\n const to = CO_ENGRAM.escapeHtml(CO_ENGRAM_AUDIT._formatVal(ch.to));\n return '<div class=\"audit-change-row\">'\n + '<span class=\"audit-field\">' + CO_ENGRAM.escapeHtml(f) + '</span>'\n + '<span class=\"audit-from\">' + from + '</span>'\n + '<span class=\"audit-arrow\">\u2192</span>'\n + '<span class=\"audit-to\">' + to + '</span>'\n + '</div>';\n }).join('');\n // \u5982\u679C\u8FD8\u6709\u5176\u4ED6 metadata(updatedBy \u7B49),\u8FFD\u52A0\u663E\u793A\n const extra = keys.filter(k => k !== 'changes').map(k =>\n '<span class=\"audit-kv\"><b>' + CO_ENGRAM.escapeHtml(k) + '</b>='\n + CO_ENGRAM.escapeHtml(CO_ENGRAM_AUDIT._formatVal(m[k])) + '</span>'\n ).join(' ');\n return '<div class=\"audit-changes\">' + rows + '</div>' + (extra ? '<div class=\"audit-meta-extra\">' + extra + '</div>' : '');\n }\n\n // 2. synapse \u7C7B(target=synapse):\u6E32\u67D3 synapse \u5173\u952E\u5B57\u6BB5\n if (m.target === 'synapse') {\n const chips = ['<span class=\"chip synapse-chip\">\u7A81\u89E6</span>'];\n if (m.kind) chips.push('<span class=\"chip kind-' + CO_ENGRAM.escapeHtml(m.kind) + '\">' + CO_ENGRAM.escapeHtml(m.kind) + '</span>');\n if (m.direction) chips.push('<span class=\"chip\">' + CO_ENGRAM.escapeHtml(m.direction) + '</span>');\n if (typeof m.weight === 'number') chips.push('<span class=\"chip\">w=' + m.weight.toFixed(2) + '</span>');\n if (m.to) chips.push('<span class=\"chip\">\u2192 ' + CO_ENGRAM.escapeHtml(m.to.length > 18 ? m.to.slice(0, 16) + '\u2026' : m.to) + '</span>');\n // \u5176\u5B83\u5B57\u6BB5\n const extras = keys.filter(k => !['target', 'kind', 'direction', 'weight', 'to', 'from', 'synapseId', 'createdBy'].includes(k))\n .map(k => '<span class=\"audit-kv\"><b>' + CO_ENGRAM.escapeHtml(k) + '</b>=' + CO_ENGRAM.escapeHtml(CO_ENGRAM_AUDIT._formatVal(m[k])) + '</span>').join(' ');\n return '<div class=\"audit-chips\">' + chips.join(' ') + (extras ? ' <span class=\"audit-meta-extra\">' + extras + '</span>' : '') + '</div>';\n }\n\n // 3. \u901A\u7528 fallback:\u952E\u503C\u5BF9\n const kvs = keys.map(k =>\n '<span class=\"audit-kv\"><b>' + CO_ENGRAM.escapeHtml(k) + '</b>='\n + CO_ENGRAM.escapeHtml(CO_ENGRAM_AUDIT._formatVal(m[k])) + '</span>'\n ).join(' ');\n return '<div class=\"audit-chips\">' + kvs + '</div>';\n },\n\n /** \u6E32\u67D3\u5355\u6761 audit row,\u5224\u65AD engram/synapse \u662F\u5426\u4ECD\u5B58\u5728,\u751F\u6210\u53EF\u70B9\u6309\u94AE\u6216\u7070\u8272\u6587\u672C */\n renderRow(e, ACTOR_LETTER, ACTOR_TIP, ACTION_TIP) {\n const ts = CO_ENGRAM.relativeTime(e.ts);\n const tsFull = CO_ENGRAM.escapeHtml(e.ts);\n const cls = CO_ENGRAM.auditActionClass(e.action);\n const actorLetter = ACTOR_LETTER[e.actor] || '?';\n const actorTip = ACTOR_TIP[e.actor] || e.actor || '';\n const actionTip = ACTION_TIP[e.action] || e.action || '';\n\n const existing = this._existingIds || new Set();\n const m = e.metadata || {};\n const isSynapse = m.target === 'synapse' || !!m.synapseId;\n // synapse \u7C7B:\u76EE\u6807 from engram(\u7528 metadata.from \u4F18\u5148,fallback \u5230 engramId)\n // engram \u7C7B:\u76EE\u6807 = engramId\n const targetId = isSynapse ? (m.from || e.engramId) : e.engramId;\n const targetExists = !!targetId && existing.has(targetId);\n\n let targetCell;\n if (!targetId) {\n targetCell = '<span class=\"audit-target-none\">\u2014</span>';\n } else if (targetExists) {\n const short = targetId.length > 22 ? targetId.slice(0, 20) + '\u2026' : targetId;\n const label = isSynapse ? '\uD83C\uDF10 \u6253\u5F00\u6E90\u5370\u8FF9' : '\uD83D\uDCC4 \u6253\u5F00\u5370\u8FF9';\n targetCell = '<button class=\"btn-link audit-target-exists\" title=\"' + CO_ENGRAM.escapeHtml(targetId) + '\" '\n + 'onclick=\"CO_ENGRAM.showTab(\\'engrams\\');setTimeout(()=>CO_ENGRAM_ENGRAMS.open(\\'' + CO_ENGRAM.escapeHtml(targetId) + '\\'),50)\">'\n + label + ' <code>' + CO_ENGRAM.escapeHtml(short) + '</code></button>';\n } else {\n // \u76EE\u6807\u5DF2\u4E0D\u5B58\u5728(\u88AB purge / delete)\u2192 \u7070\u8272\u5220\u9664\u7EBF\n const short = targetId.length > 22 ? targetId.slice(0, 20) + '\u2026' : targetId;\n targetCell = '<span class=\"audit-target-gone\" title=\"\u76EE\u6807\u5DF2\u4E0D\u5B58\u5728:' + CO_ENGRAM.escapeHtml(targetId) + '\">'\n + (isSynapse ? '\uD83C\uDF10 ' : '\uD83D\uDCC4 ') + '<code><s>' + CO_ENGRAM.escapeHtml(short) + '</s></code> <em>(\u5DF2\u5220\u9664)</em></span>';\n }\n\n const metaHtml = this.renderMeta(e);\n\n const isActive = this._actionFilter === e.action;\n const actionBtnClass = 'action ' + cls + ' action-button' + (isActive ? ' active' : '');\n\n return '<div class=\"timeline-row audit-row\">'\n + '<span class=\"ts\" title=\"' + tsFull + '\">' + ts + '</span>'\n + '<span class=\"actor-icon ' + e.actor + '\" title=\"' + CO_ENGRAM.escapeHtml(actorTip) + '\">' + actorLetter + '</span>'\n + '<button type=\"button\" class=\"' + actionBtnClass + '\" title=\"' + CO_ENGRAM.escapeHtml(actionTip) + ' \u2014 \u70B9\u51FB\u4EC5\u663E\u793A\u6B64\u7C7B\u4E8B\u4EF6\" onclick=\"CO_ENGRAM_AUDIT.filterByAction(\\'' + CO_ENGRAM.escapeHtml(e.action) + '\\')\">' + CO_ENGRAM.escapeHtml(e.action) + '</button>'\n + targetCell\n + '<div class=\"metadata audit-meta-cell\">' + metaHtml + '</div>'\n + '</div>';\n }\n};\n\n// ============================================================\n// Merges (P4.3) \u2014 git merge driver health dashboard\n// ============================================================\nCO_ENGRAM.on('merges', async function() {\n const root = document.getElementById('merges-content');\n if (!root) return;\n if (CO_ENGRAM._mergesLoaded) return;\n CO_ENGRAM._mergesLoaded = true;\n await CO_ENGRAM_MERGES.render(root);\n});\n\nwindow.CO_ENGRAM_MERGES = {\n async render(root) {\n root.innerHTML = '<div class=\"loading\">\u52A0\u8F7D\u5408\u5E76\u7EDF\u8BA1\u4E2D</div>';\n let payload;\n try {\n payload = await CO_ENGRAM.apiGet('/api/merge-stats?windowDays=7');\n } catch (e) {\n root.innerHTML = '<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>';\n return;\n }\n if (!payload.enabled || !payload.stats) {\n root.innerHTML = '<div class=\"empty\">audit log \u672A\u542F\u7528,\u65E0\u5408\u5E76\u6570\u636E\u3002</div>';\n return;\n }\n\n // \u5F02\u5E38\u544A\u8B66\u6A2A\u5E45(spec \u00A713.2)\u2014 \u5931\u8D25\u4E0D\u963B\u585E\u4E3B\u7EDF\u8BA1\u6E32\u67D3\n let banner = '';\n try {\n const anom = await CO_ENGRAM.apiGet('/api/merge-anomalies?windowDays=7');\n if (anom.enabled && anom.anomalies && anom.anomalies.length > 0) {\n const items = anom.anomalies.map(function(a) {\n const cls = a.severity === 'critical' ? 'anom-critical' : (a.severity === 'warning' ? 'anom-warning' : 'anom-info');\n const icon = a.severity === 'critical' ? '\u2717' : (a.severity === 'warning' ? '\u26A0' : '\u2139');\n return '<li class=\"' + cls + '\"><span class=\"anom-icon\">' + icon + '</span><strong>' + CO_ENGRAM.escapeHtml(a.kind) + '</strong>: ' + CO_ENGRAM.escapeHtml(a.message) + '</li>';\n }).join('');\n banner = '<div class=\"anomaly-banner\"><h3>\u5F02\u5E38\u544A\u8B66 \u00B7 ' + anom.anomalies.length + ' \u6761</h3><ul>' + items + '</ul></div>';\n }\n } catch (_) { /* anomaly API \u53EF\u9009,\u5931\u8D25\u9759\u9ED8 */ }\n\n root.innerHTML = banner + CO_ENGRAM_MERGES.renderHtml(payload.stats, payload.windowDays);\n },\n\n renderHtml(s, windowDays) {\n const pct = (r) => (r * 100).toFixed(1) + '%';\n\n const kpi = (label, value, sub) =>\n '<div class=\"kpi\">' +\n '<div class=\"kpi-label\">' + CO_ENGRAM.escapeHtml(label) + '</div>' +\n '<div class=\"kpi-value\">' + CO_ENGRAM.escapeHtml(String(value)) + '</div>' +\n (sub ? '<div class=\"kpi-sub\">' + CO_ENGRAM.escapeHtml(sub) + '</div>' : '') +\n '</div>';\n\n const bar = (label, count, max, color) =>\n '<div class=\"bar-row\">' +\n '<div class=\"bar-label\">' + CO_ENGRAM.escapeHtml(label) + '</div>' +\n '<div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:' + (max ? (count / max * 100).toFixed(1) : 0) + '%;background:' + (color || '#5eead4') + '\"></div></div>' +\n '<div class=\"bar-value\">' + count + '</div>' +\n '</div>';\n\n let html = '<div class=\"panel\">';\n html += '<div class=\"panel-header\"><h2>\u5408\u5E76\u7EDF\u8BA1 \u00B7 \u6700\u8FD1 ' + windowDays + ' \u5929</h2></div>';\n html += '<div class=\"kpi-grid\" style=\"grid-template-columns:repeat(auto-fit,minmax(140px,1fr));margin-bottom:1.5rem\">';\n html += kpi('\u603B\u5408\u5E76', s.totalMerges);\n html += kpi('\u81EA\u52A8\u89E3\u51B3', s.autoResolved, pct(s.autoResolveRate));\n html += kpi('\u5347\u7EA7\u5230\u51B2\u7A81\u6807\u8BB0', s.escalatedToMarkers);\n html += kpi('Backup \u5931\u8D25', s.backupFailures);\n html += '</div>';\n\n // LLM \u6BB5\n html += '<h3 style=\"margin-top:1.5rem\">LLM \u4EF2\u88C1</h3>';\n html += '<div class=\"kpi-grid\" style=\"grid-template-columns:repeat(auto-fit,minmax(140px,1fr));margin-bottom:1rem\">';\n html += kpi('\u603B\u8C03\u7528', s.llm.totalInvocations);\n html += kpi('\u6210\u529F', s.llm.arbitrated);\n html += kpi('\u5347\u7EA7', s.llm.escalated);\n html += kpi('\u5931\u8D25', s.llm.failed);\n html += kpi('\u6210\u529F\u7387', pct(s.llm.successRate));\n html += '</div>';\n\n // \u6309\u7B56\u7565\n const strategies = Object.entries(s.byStrategy || {}).sort((a, b) => b[1] - a[1]);\n if (strategies.length > 0) {\n const max = strategies[0][1];\n html += '<h3 style=\"margin-top:1.5rem\">\u89E3\u51B3\u7B56\u7565\u5206\u5E03(Top 8)</h3>';\n html += '<div style=\"margin-bottom:1rem\">';\n for (const [name, count] of strategies.slice(0, 8)) {\n html += bar(name, count, max, '#5eead4');\n }\n html += '</div>';\n }\n\n // Hot paths\n const paths = Object.entries(s.byPath || {}).sort((a, b) => b[1] - a[1]);\n if (paths.length > 0) {\n const max = paths[0][1];\n html += '<h3 style=\"margin-top:1.5rem\">\u51B2\u7A81\u70ED\u70B9\u8DEF\u5F84(Top 8)</h3>';\n html += '<div style=\"margin-bottom:1rem\">';\n for (const [p, count] of paths.slice(0, 8)) {\n html += bar(p, count, max, '#fbbf24');\n }\n html += '</div>';\n }\n\n // \u6309\u5929\u8D8B\u52BF\n const days = Object.entries(s.byDay || {}).sort((a, b) => a[0].localeCompare(b[0]));\n if (days.length > 0) {\n const max = Math.max(...days.map((d) => d[1]));\n html += '<h3 style=\"margin-top:1.5rem\">\u6BCF\u65E5\u5408\u5E76\u91CF(\u8D8B\u52BF)</h3>';\n html += '<div style=\"margin-bottom:1rem\">';\n for (const [day, count] of days) {\n html += bar(day, count, max, '#60a5fa');\n }\n html += '</div>';\n }\n\n html += '</div>';\n return html;\n }\n};\n\n// ============================================================\n// Trash\n// ============================================================\nCO_ENGRAM.on('trash', async function() {\n const root = document.getElementById('trash-content');\n if (!root) return;\n if (CO_ENGRAM._trashLoaded) return;\n CO_ENGRAM._trashLoaded = true;\n await CO_ENGRAM_TRASH.render(root);\n});\n\nwindow.CO_ENGRAM_TRASH = {\n async render(root) {\n root.innerHTML = '<div class=\"loading\">\u52A0\u8F7D\u56DE\u6536\u7AD9\u4E2D</div>';\n let data;\n try { data = await CO_ENGRAM.apiGet('/api/trash'); }\n catch (e) { root.innerHTML = '<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>'; return; }\n\n const items = data.results || [];\n if (!items.length) {\n root.innerHTML = '<div class=\"empty\"><div class=\"icon\">\uD83D\uDDD1\uFE0F</div>\u56DE\u6536\u7AD9\u4E3A\u7A7A</div>';\n return;\n }\n // \u9876\u90E8\u5DE5\u5177\u680F:\u7EDF\u8BA1 + \u5206\u533A\u7B5B\u9009 + \u4E00\u952E\u6E05\u7A7A\n const partitions = [...new Set(items.map((t) => t.partition).filter(Boolean))].sort();\n const total = items.length;\n let html = '<div class=\"card\" style=\"display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:1rem\">'\n + '<strong style=\"margin-right:auto\">\u56DE\u6536\u7AD9 \u00B7 \u5171 ' + total + ' \u6761</strong>';\n if (partitions.length > 1) {\n html += '<label style=\"font-weight:normal;font-size:0.9rem\">\u5206\u533A:'\n + '<select id=\"trash-partition-filter\" onchange=\"CO_ENGRAM_TRASH.applyFilter()\" style=\"margin-left:0.4rem\">'\n + '<option value=\"\">\u5168\u90E8</option>'\n + partitions.map((p) => '<option value=\"' + CO_ENGRAM.escapeHtml(p) + '\">' + CO_ENGRAM.escapeHtml(p) + '</option>').join('')\n + '</select></label>';\n }\n html += '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_TRASH.purgeAll(false)\">\u6C38\u4E45\u6E05\u7A7A\u5168\u90E8</button>'\n + '</div>';\n\n html += '<table class=\"data-table\" id=\"trash-table\"><thead><tr>'\n + '<th>ID</th><th>\u5206\u533A</th><th>\u56DE\u6536\u65F6\u95F4</th><th></th>'\n + '</tr></thead><tbody>';\n for (const t of items) {\n const part = t.partition || '';\n html += '<tr data-partition=\"' + CO_ENGRAM.escapeHtml(part) + '\">'\n + '<td><code>' + CO_ENGRAM.escapeHtml(t.id) + '</code></td>'\n + '<td>' + CO_ENGRAM.escapeHtml(t.partition || '\u2014') + '</td>'\n + '<td>' + CO_ENGRAM.escapeHtml(t.trashedAt || '\u2014') + '</td>'\n + '<td>'\n + '<button class=\"btn-link\" onclick=\"CO_ENGRAM_TRASH.preview(\\'' + CO_ENGRAM.escapeHtml(t.id) + '\\')\">\u67E5\u770B</button> '\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_TRASH.restore(\\'' + CO_ENGRAM.escapeHtml(t.id) + '\\')\">\u6062\u590D</button>'\n + '</td>'\n + '</tr>';\n }\n html += '</tbody></table>';\n root.innerHTML = html;\n },\n\n // \u5206\u533A\u7B5B\u9009:\u9690\u85CF\u4E0D\u5339\u914D\u7684\u884C\n applyFilter() {\n const sel = document.getElementById('trash-partition-filter');\n const v = sel ? sel.value : '';\n document.querySelectorAll('#trash-table tbody tr').forEach((tr) => {\n const p = tr.getAttribute('data-partition') || '';\n tr.style.display = (!v || p === v) ? '' : 'none';\n });\n },\n\n // \u9884\u89C8\u5355\u6761\u56DE\u6536\u7AD9\u5185\u5BB9(\u53EA\u8BFB drawer)\n async preview(id) {\n let d;\n try { d = await CO_ENGRAM.apiGet('/api/trash/' + encodeURIComponent(id)); }\n catch (e) { alert('\u52A0\u8F7D\u5931\u8D25:' + (e.message || e)); return; }\n\n const fm = d.frontmatter || {};\n const L = CO_ENGRAM_LABELS;\n const kind = fm.kind ? (L.kind[fm.kind] || fm.kind) : '';\n const status = fm.status ? (L.status[fm.status] || fm.status) : '';\n const valence = fm.emotionalValence ? (L.emotionalValence[fm.emotionalValence] || fm.emotionalValence) : '';\n const source = fm.sourceType ? (L.sourceType[fm.sourceType] || fm.sourceType) : '';\n\n const body = '<div class=\"edit-banner\" style=\"background:rgba(239,68,68,.08);border-left:3px solid #ef4444;padding:0.6rem 0.8rem;margin-bottom:0.8rem\">'\n + '<strong>\u56DE\u6536\u7AD9\u9884\u89C8</strong> \u00B7 \u6B64\u8BB0\u5FC6\u5DF2\u88AB\u79FB\u51FA\u4E3B\u7D22\u5F15,\u9700\u5148\"\u6062\u590D\"\u624D\u80FD\u518D\u6B21\u7F16\u8F91\u6216\u53EC\u56DE\u3002'\n + '</div>'\n + '<h2>' + CO_ENGRAM.escapeHtml(fm.title || id) + '</h2>'\n + '<div class=\"field\"><span class=\"field-label\">ID:</span><code>' + CO_ENGRAM.escapeHtml(id) + '</code></div>'\n + '<div class=\"field\">'\n + (kind ? '<span class=\"chip kind-' + fm.kind + '\"' + CO_ENGRAM.tip('kind.' + fm.kind) + '>' + kind + '</span> ' : '')\n + (status ? '<span class=\"field-label\"' + CO_ENGRAM.tip('status.' + fm.status) + '>\u72B6\u6001:</span>' + CO_ENGRAM.escapeHtml(status) : '')\n + '</div>'\n + (fm.domainTags && fm.domainTags.length\n ? '<div class=\"field\"><span class=\"field-label\">\u9886\u57DF\u6807\u7B7E:</span>' + fm.domainTags.map((t) => '<span class=\"chip\">' + CO_ENGRAM.escapeHtml(t) + '</span>').join(' ') + '</div>'\n : '')\n + '<div class=\"field\"><span class=\"field-label\">\u5206\u533A:</span>' + CO_ENGRAM.escapeHtml(d.partition || '\u2014')\n + ' <span class=\"field-label\"' + CO_ENGRAM.tip('lastEffectiveAt') + '>\u56DE\u6536\u65F6\u95F4:</span>' + CO_ENGRAM.escapeHtml(d.trashedAt || '\u2014') + '</div>'\n + (valence ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('emotionalValence.' + fm.emotionalValence) + '>\u60C5\u611F\u6781\u6027:</span>' + CO_ENGRAM.escapeHtml(valence) + '</div>' : '')\n + (source ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('sourceType.' + fm.sourceType) + '>\u6765\u6E90:</span>' + CO_ENGRAM.escapeHtml(source) + '</div>' : '')\n + (fm.createdBy ? '<div class=\"field\"><span class=\"field-label\">\u521B\u5EFA\u8005:</span>' + CO_ENGRAM.escapeHtml(fm.createdBy) + '</div>' : '')\n + '<h3>\u5185\u5BB9</h3><div class=\"markdown-body\">' + CO_ENGRAM.renderMarkdown(d.content || '') + '</div>'\n + '<div style=\"margin-top:1rem;display:flex;gap:0.5rem\">'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM.closeDrawer();CO_ENGRAM_TRASH.restore(\\'' + CO_ENGRAM.escapeHtml(id) + '\\')\">\u6062\u590D\u5230\u4E3B\u7D22\u5F15</button>'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM.closeDrawer()\">\u5173\u95ED</button>'\n + '</div>';\n CO_ENGRAM.openDrawer(body);\n },\n\n async restore(id) {\n if (!confirm('\u6062\u590D ' + id + ' \u5230\u4E3B\u7D22\u5F15?')) return;\n try {\n await CO_ENGRAM.apiJson('/api/trash/' + encodeURIComponent(id) + '/restore', 'POST', {});\n CO_ENGRAM._trashLoaded = false;\n await this.render(document.getElementById('trash-content'));\n } catch (e) { alert('\u6062\u590D\u5931\u8D25:' + (e.message || e)); }\n },\n\n // \u6C38\u4E45\u6E05\u7A7A:byPartition=true \u53EA\u6E05\u5F53\u524D\u7B5B\u9009\u5206\u533A,false=\u6E05\u5168\u90E8\n async purgeAll(byPartition) {\n const filterSel = document.getElementById('trash-partition-filter');\n const part = byPartition && filterSel ? filterSel.value : '';\n const scope = part ? '\u5206\u533A ' + part + ' \u5185' : '\u5168\u90E8(\u8DE8\u6240\u6709\u5206\u533A)';\n // \u5148 dryRun \u770B\u770B\u4F1A\u5220\u591A\u5C11\u6761\n let preview;\n try {\n const url = '/api/trash?dryRun=1' + (part ? '&partition=' + encodeURIComponent(part) : '');\n preview = await CO_ENGRAM.apiGet(url);\n } catch (e) { alert('\u9884\u626B\u63CF\u5931\u8D25:' + (e.message || e)); return; }\n\n const n = preview.count || 0;\n if (n === 0) { alert('\u5F53\u524D\u8303\u56F4\u65E0\u5185\u5BB9\u53EF\u6E05\u7A7A'); return; }\n if (!confirm('\u5373\u5C06\u6C38\u4E45\u5220\u9664 ' + scope + ' \u7684 ' + n + ' \u6761\u8BB0\u5FC6\u3002\\n\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500(\u7269\u7406 unlink),\u5373\u4F7F\u6709 git \u4ED3\u5E93\u4E5F\u53EA\u80FD\u4ECE\u5386\u53F2 commit \u6062\u590D\u3002\\n\\n\u786E\u8BA4\u7EE7\u7EED?')) return;\n if (!confirm('\u4E8C\u6B21\u786E\u8BA4:\u771F\u7684\u6E05\u7A7A ' + scope + ' \u7684\u5168\u90E8 ' + n + ' \u6761?')) return;\n\n try {\n const url = '/api/trash' + (part ? '?partition=' + encodeURIComponent(part) : '');\n const r = await CO_ENGRAM.apiJson(url, 'DELETE', {});\n alert('\u5DF2\u6C38\u4E45\u5220\u9664 ' + (r.count || 0) + ' \u6761\u8BB0\u5FC6\u3002');\n CO_ENGRAM._trashLoaded = false;\n await this.render(document.getElementById('trash-content'));\n } catch (e) { alert('\u6E05\u7A7A\u5931\u8D25:' + (e.message || e)); }\n }\n};\n\n// ============================================================\n// Config\n// ============================================================\nCO_ENGRAM.on('config', async function() {\n const root = document.getElementById('config-content');\n if (!root) return;\n if (CO_ENGRAM._configLoaded) return;\n CO_ENGRAM._configLoaded = true;\n await CO_ENGRAM_CONFIG.render(root);\n});\n\nwindow.CO_ENGRAM_CONFIG = {\n async render(root) {\n root.innerHTML = '<div class=\"loading\">\u52A0\u8F7D\u914D\u7F6E\u4E2D</div>';\n let data;\n try { data = await CO_ENGRAM.apiGet('/api/config'); }\n catch (e) { root.innerHTML = '<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>'; return; }\n\n CO_ENGRAM._configData = data;\n const persisted = data.persisted || {};\n const runtime = data.runtime || {};\n // hostType \u51B3\u5B9A\u91CD\u542F\u76F8\u5173\u7684\u63D0\u793A\u6587\u5B57\u4E0E\u6309\u94AE\u53EF\u89C1\u6027\n // 'mcp-server' \u2192 \u7531 Claude Code \u7236\u8FDB\u7A0B\u7BA1\u7406,\u652F\u6301\u4F18\u96C5\u91CD\u542F\n // 'openclaw-plugin' \u2192 \u662F openclaw gateway \u7684\u4E00\u90E8\u5206,\u4E0D\u652F\u6301\u81EA\u52A8\u91CD\u542F\n const hostType = data.hostType || 'mcp-server';\n const HOST_LABEL = hostType === 'openclaw-plugin' ? 'openclaw gateway' : 'MCP server';\n const HOST_PARENT = hostType === 'openclaw-plugin' ? 'openclaw' : 'Claude Code';\n const HOST_SUPPORTS_RESTART = hostType !== 'openclaw-plugin';\n\n const LANG_LABEL = { zh: '\u4E2D\u6587', en: 'English' };\n const langOptions = Object.keys(LANG_LABEL).map(k => '<option value=\"' + k + '\"' + (persisted.language === k ? ' selected' : '') + '>' + LANG_LABEL[k] + '</option>').join('');\n const profileOptions = ['minimal', 'standard', 'full'].map(p => '<option value=\"' + p + '\"' + (persisted.toolsProfile === p ? ' selected' : '') + '>' + p + '</option>').join('');\n\n let html = '';\n\n // \u6301\u4E45\u5316(\u53EF\u7F16\u8F91)\n html += '<div class=\"config-section\">';\n html += '<h3>\u6301\u4E45\u5316\u914D\u7F6E(\u53EF\u7F16\u8F91,\u4FDD\u5B58\u540E\u91CD\u542F\u751F\u6548)</h3>';\n html += '<div class=\"config-row\"><div class=\"config-label\">\u8BED\u8A00<span class=\"desc\">UI / \u5DE5\u5177\u63CF\u8FF0 / \u63D0\u793A\u8BCD\u6240\u7528\u8BED\u8A00</span></div>'\n + '<div class=\"config-control\"><select id=\"cf-language\">' + langOptions + '</select></div></div>';\n html += '<div class=\"config-row\"><div class=\"config-label\">\u9ED8\u8BA4\u521B\u5EFA\u8005<span class=\"desc\">\u65B0\u8BB0\u5FC6\u5370\u8FF9\u7684\u9ED8\u8BA4 createdBy \u5B57\u6BB5;\u7559\u7A7A\u56DE\u9000\u5230 git \u8EAB\u4EFD</span></div>'\n + '<div class=\"config-control\"><input id=\"cf-default-created-by\" type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(persisted.defaultCreatedBy || '') + '\" placeholder=\"(\u7559\u7A7A\u4F7F\u7528 git \u4F5C\u8005)\"></div></div>';\n html += '<div class=\"config-row\"><div class=\"config-label\">\u5DE5\u5177 Profile<span class=\"desc\">LLM \u53EF\u89C1\u5DE5\u5177\u6570\u91CF:minimal=\u6700\u5C0F / standard=\u6807\u51C6 / full=\u5168\u90E8</span></div>'\n + '<div class=\"config-control\"><select id=\"cf-tools-profile\">' + profileOptions + '</select></div></div>';\n html += '</div>';\n\n // \u8FD0\u884C\u65F6\u72B6\u6001:\u53EF\u7F16\u8F91(\u4E0B\u6B21\u542F\u52A8\u751F\u6548),viewer \u81EA\u8EAB\u53EA\u8BFB\u907F\u514D UI \u81EA\u6740\n const toggle = (id, label, desc, currentOn, desiredOn, editable) => {\n const effective = (desiredOn !== undefined ? desiredOn : currentOn);\n const onClass = effective ? 'on' : 'off';\n const control = editable\n ? '<label class=\"toggle-switch\"><input type=\"checkbox\" id=\"' + id + '\"' + (effective ? ' checked' : '') + '><span class=\"toggle-slider\"></span></label>'\n + '<span class=\"toggle-state ' + onClass + '\">' + (effective ? '\u542F\u7528' : '\u7981\u7528') + '</span>'\n : '<input type=\"text\" value=\"' + (currentOn ? '\u5DF2\u542F\u7528' : '\u672A\u542F\u7528') + '\" readonly>';\n const badge = (desiredOn !== undefined && desiredOn !== currentOn)\n ? '<span class=\"chip\" style=\"background:rgba(251,191,36,.12);color:var(--accent-warm);margin-left:.5rem\">\u91CD\u542F\u540E\u751F\u6548</span>'\n : '';\n return '<div class=\"config-row\"><div class=\"config-label\">' + label + (desc ? '<span class=\"desc\">' + desc + '</span>' : '') + badge + '</div>'\n + '<div class=\"config-control\" style=\"display:flex;align-items:center;gap:.6rem\">' + control + '</div></div>';\n };\n\n html += '<div class=\"config-section\">';\n html += '<h3>\u8FD0\u884C\u65F6\u72B6\u6001(\u7F16\u8F91\u540E\u9700\u91CD\u542F ' + HOST_LABEL + ' \u751F\u6548)</h3>';\n html += '<div class=\"edit-banner\" style=\"margin-bottom:.8rem\">\u8BF4\u660E:\u8FD9\u4E9B\u5F00\u5173\u628A\"\u4E0B\u6B21\u542F\u52A8\u65F6\u671F\u671B\u7684\u72B6\u6001\"\u6301\u4E45\u5316\u5230 config.json\u3002\u5F53\u524D\u6B63\u5728\u8FD0\u884C\u7684\u5B9E\u4F8B\u4E0D\u4F1A\u53D7\u5F71\u54CD\u2014\u2014\u91CD\u542F ' + HOST_LABEL + ' \u540E,\u65B0\u503C\u624D\u4F1A\u751F\u6548\u3002' + (HOST_SUPPORTS_RESTART ? '' : ' openclaw \u6A21\u5F0F\u4E0B\u8BF7\u4F7F\u7528 <code>openclaw gateway restart</code> \u547D\u4EE4\u3002') + '</div>';\n html += toggle('cf-audit', '\u5BA1\u8BA1\u65E5\u5FD7', '\u8BB0\u5F55\u6240\u6709 API / \u5DE5\u5177\u8C03\u7528\u4E8B\u4EF6', runtime.auditEnabled, persisted.audit?.enabled, true);\n html += toggle('cf-proposals', '\u63D0\u6848\u5F15\u64CE', '\u9690\u5F0F\u6355\u83B7\u5019\u9009\u8BB0\u5FC6\u5F85\u5BA1\u6279', runtime.proposalEnabled, persisted.proposals?.enabled, true);\n html += toggle('cf-maintenance', '\u7EF4\u62A4\u670D\u52A1', '\u540E\u53F0 light/deep/rem \u4E09\u9636\u6BB5\u7EF4\u62A4', runtime.maintenanceEnabled, persisted.maintenance?.enabled, true);\n html += toggle(null, '\u641C\u7D22\u5668', '\u8BED\u4E49 + \u5173\u952E\u8BCD\u68C0\u7D22', runtime.searchEnabled, undefined, false);\n html += toggle(null, 'Web \u67E5\u770B\u5668', '\u672C\u9875\u9762\u6240\u5728 HTTP \u670D\u52A1(\u4E0D\u53EF\u5173\u95ED,\u5426\u5219 UI \u5931\u8054)', runtime.viewerEnabled, undefined, false);\n html += '</div>';\n\n // \u8FD0\u884C\u65F6\u5143\u6570\u636E(\u53EA\u8BFB)\n html += '<div class=\"config-section\">';\n html += '<h3>\u8FD0\u884C\u65F6\u5143\u6570\u636E(\u53EA\u8BFB)</h3>';\n html += '<div class=\"config-row readonly\"><div class=\"config-label\">\u8FD0\u884C Profile</div>'\n + '<div class=\"config-control\"><input type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(runtime.profile || 'standard') + '\" readonly></div></div>';\n html += '<div class=\"config-row readonly\"><div class=\"config-label\">\u5F53\u524D\u8BED\u8A00(\u8FD0\u884C\u65F6)</div>'\n + '<div class=\"config-control\"><input type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(LANG_LABEL[runtime.language] || runtime.language || '') + '\" readonly></div></div>';\n html += '<div class=\"config-row readonly\"><div class=\"config-label\">\u8FD0\u884C\u65F6 createdBy</div>'\n + '<div class=\"config-control\"><input type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(runtime.defaultCreatedBy || '(\u672A\u8BBE\u7F6E)') + '\" readonly></div></div>';\n html += '</div>';\n\n // \u5143\u6570\u636E\n html += '<div class=\"config-section\">';\n html += '<h3>\u4ED3\u5E93\u5143\u6570\u636E</h3>';\n // \u6570\u636E\u6839\u76EE\u5F55:\u5F53\u524D\u503C\u53EA\u8BFB\u5C55\u793A + \"\u5207\u6362\"\u6309\u94AE\u6253\u5F00 drawer \u7F16\u8F91\u4E0B\u6B21\u542F\u52A8\u7684\u671F\u671B\u503C\n const currentDataRoot = data.dataRoot || '(\u672A\u77E5)';\n const desiredDataRoot = persisted.desiredDataRoot || '';\n const dataRootBadge = desiredDataRoot\n ? '<span class=\"chip\" style=\"background:rgba(251,191,36,.12);color:var(--accent-warm);margin-left:.5rem\">\u91CD\u542F\u540E\u5207\u6362\u5230 ' + CO_ENGRAM.escapeHtml(desiredDataRoot) + '</span>'\n : '';\n html += '<div class=\"config-row\"><div class=\"config-label\">\u6570\u636E\u6839\u76EE\u5F55<span class=\"desc\">\u8BB0\u5FC6\u5370\u8FF9/\u7A81\u89E6/\u5BA1\u8BA1\u7684\u5B9E\u9645\u843D\u76D8\u4F4D\u7F6E\u3002env CO_ENGRAM_DATA_ROOT \u4F18\u5148\u4E8E\u6B64\u5904\u6301\u4E45\u5316\u503C\u3002</span>' + dataRootBadge + '</div>'\n + '<div class=\"config-control\" style=\"display:flex;gap:.4rem;align-items:center\">'\n + '<input type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(currentDataRoot) + '\" readonly style=\"flex:1\">'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_CONFIG.editDataRoot()\">\u5207\u6362\u2026</button>'\n + '</div></div>';\n html += '<div class=\"config-row readonly\"><div class=\"config-label\">\u914D\u7F6E\u7248\u672C</div>'\n + '<div class=\"config-control\"><input type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(String(persisted.version || 1)) + '\" readonly></div></div>';\n html += '<div class=\"config-row readonly\"><div class=\"config-label\">\u521B\u5EFA\u65F6\u95F4</div>'\n + '<div class=\"config-control\"><input type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(persisted.createdAt || '\u2014') + '\" readonly></div></div>';\n html += '<div class=\"config-row readonly\"><div class=\"config-label\">\u6700\u540E\u66F4\u65B0</div>'\n + '<div class=\"config-control\"><input type=\"text\" value=\"' + CO_ENGRAM.escapeHtml(persisted.updatedAt || persisted.createdAt || '\u2014') + '\" readonly></div></div>';\n html += '</div>';\n\n html += '<div class=\"config-save-bar\">'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_CONFIG.reload()\">\u91CD\u7F6E</button>'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM_CONFIG.save()\">\u4FDD\u5B58\u914D\u7F6E</button>'\n + '</div>';\n\n root.innerHTML = html;\n },\n\n async reload() {\n CO_ENGRAM._configLoaded = false;\n const root = document.getElementById('config-content');\n if (root) await this.render(root);\n },\n\n async save() {\n const auditEl = document.getElementById('cf-audit');\n const proposalsEl = document.getElementById('cf-proposals');\n const maintenanceEl = document.getElementById('cf-maintenance');\n const auditOn = auditEl ? !!auditEl.checked : undefined;\n const proposalsOn = proposalsEl ? !!proposalsEl.checked : undefined;\n const maintenanceOn = maintenanceEl ? !!maintenanceEl.checked : undefined;\n const body = {\n language: document.getElementById('cf-language').value,\n defaultCreatedBy: document.getElementById('cf-default-created-by').value,\n toolsProfile: document.getElementById('cf-tools-profile').value,\n // \u5D4C\u5957\u7ED3\u6784:\u4E0E config.json schema \u4E00\u81F4\n ...(auditOn !== undefined ? { audit: { enabled: auditOn } } : {}),\n ...(proposalsOn !== undefined ? { proposals: { enabled: proposalsOn } } : {}),\n ...(maintenanceOn !== undefined ? { maintenance: { enabled: maintenanceOn } } : {}),\n };\n // \u68C0\u6D4B\u54EA\u4E9B\u5B57\u6BB5\u9700\u8981\u91CD\u542F\u751F\u6548,\u7528\u4E8E\u5728 banner \u91CC\u7ED9\u7528\u6237\u66F4\u51C6\u786E\u7684\u63D0\u793A\u3002\n const before = CO_ENGRAM._configData?.persisted || {};\n const restartFields = [\n { section: 'audit', label: '\u5BA1\u8BA1\u65E5\u5FD7' },\n { section: 'proposals', label: '\u63D0\u6848\u5F15\u64CE' },\n { section: 'maintenance', label: '\u7EF4\u62A4\u670D\u52A1' }\n ];\n const changed = restartFields.filter(f => {\n const oldVal = before[f.section]?.enabled;\n const newVal = body[f.section]?.enabled;\n return oldVal !== newVal && !(oldVal == null && newVal == null);\n });\n const dataRootChanged = !!document.querySelector('.edit-banner[data-dataroot-changed=\"1\"]');\n try {\n await CO_ENGRAM.apiJson('/api/config', 'PUT', body);\n const needsRestart = changed.length > 0 || dataRootChanged;\n let banner;\n if (needsRestart) {\n const changedLabels = changed.map(c => c.label).concat(dataRootChanged ? ['\u6570\u636E\u6839\u76EE\u5F55'] : []);\n const hostType = (CO_ENGRAM._configData && CO_ENGRAM._configData.hostType) || 'mcp-server';\n const HOST_LABEL = hostType === 'openclaw-plugin' ? 'openclaw gateway' : 'MCP server';\n const HOST_PARENT = hostType === 'openclaw-plugin' ? 'openclaw' : 'Claude Code';\n const restartSupported = hostType !== 'openclaw-plugin';\n const restartBtn = restartSupported\n ? '<button class=\"btn\" style=\"margin-left:auto;padding:.3rem .8rem;font-size:.8rem\" '\n + 'title=\"\u70B9\u51FB\u540E ' + HOST_LABEL + ' \u4F1A\u4F18\u96C5\u9000\u51FA(\u9000\u51FA\u7801 0),\u7531\u7236\u8FDB\u7A0B(\u901A\u5E38 ' + HOST_PARENT + ')\u81EA\u52A8\u91CD\u542F\u3002\\n\\n'\n + '\u5F71\u54CD\u8303\u56F4:\\n'\n + ' \u2022 \u5DE5\u5177\u4F1A\u77ED\u6682\u65AD\u5F00(\u51E0\u79D2\u5185\u81EA\u52A8\u91CD\u8FDE,\u4E0D\u5F71\u54CD\u6B63\u5728\u8FDB\u884C\u7684\u5BF9\u8BDD)\\n'\n + ' \u2022 \u6D4F\u89C8\u5668\u4F1A\u5931\u8054,\u672C\u9875\u9762\u4F1A\u5728\u670D\u52A1\u6062\u590D\u540E\u81EA\u52A8\u5237\u65B0\\n'\n + ' \u2022 \u7EF4\u62A4\u7EBF\u7A0B\u3001proposal \u5F15\u64CE\u7B49\u540E\u53F0\u4EFB\u52A1\u4F1A\u4EE5\u65B0\u914D\u7F6E\u91CD\u65B0\u542F\u52A8\\n\\n'\n + '\u4E0D\u4F1A\u4E22\u5931:\\n'\n + ' \u2022 \u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E(\u521A\u521A\u5199\u5165 config.json)\\n'\n + ' \u2022 \u5DF2\u5B58\u5728\u7684 engram / synapse \u6570\u636E(\u843D\u76D8\u6301\u4E45\u5316)\\n'\n + ' \u2022 \u5F53\u524D\u5BF9\u8BDD\u5386\u53F2(\u7531 ' + HOST_PARENT + ' \u6301\u6709,\u4E0E\u670D\u52A1\u91CD\u542F\u65E0\u5173)\" '\n + 'onclick=\"CO_ENGRAM_CONFIG.restartNow()\">\u7ACB\u5373\u91CD\u542F\u751F\u6548</button>'\n : '<span style=\"margin-left:auto;font-size:.8rem;color:var(--fg-muted)\">openclaw \u6A21\u5F0F\u4E0D\u652F\u6301\u81EA\u52A8\u91CD\u542F,\u8BF7\u624B\u52A8\u6267\u884C <code>openclaw gateway restart</code></span>';\n banner = '<div class=\"edit-banner\" style=\"background:rgba(251,191,36,0.08);border-color:rgba(251,191,36,0.35);color:var(--accent-warm);display:flex;flex-wrap:wrap;gap:.6rem;align-items:center\">'\n + '<span>\u2713 \u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002\u4EE5\u4E0B\u6539\u52A8\u9700\u91CD\u542F ' + HOST_LABEL + ' \u624D\u80FD\u751F\u6548:<strong>' + changedLabels.join('\u3001') + '</strong></span>'\n + restartBtn\n + '</div>';\n } else {\n banner = '<div class=\"edit-banner\" style=\"background:rgba(94,234,212,0.08);border-color:rgba(94,234,212,0.25);color:var(--accent)\">\u2713 \u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002</div>';\n }\n const root = document.getElementById('config-content');\n if (root) root.insertAdjacentHTML('afterbegin', banner);\n // \u91CD\u65B0\u52A0\u8F7D\u6301\u4E45\u5316\u90E8\u5206\n setTimeout(() => { CO_ENGRAM._configLoaded = false; this.render(document.getElementById('config-content')); }, 2000);\n } catch (e) { alert('\u4FDD\u5B58\u5931\u8D25:' + (e.message || e)); }\n },\n\n /**\n * Trigger graceful exit; parent process will auto-restart.\n *\n * Only available in mcp-server mode. In openclaw-plugin mode, /api/restart\n * returns 409 and we prompt the user to manually run 'openclaw gateway restart'.\n *\n * Flow:\n * 1. POST /api/restart - server process.exit(0) after 300ms\n * 2. Show \"restarting\" overlay\n * 3. Poll /api/stats every 500ms; 2 consecutive successes = recovered\n * 4. Reload page to re-render UI\n *\n * Fallback: if not recovered within 30s, prompt user to refresh manually.\n */\n async restartNow() {\n const hostType = (CO_ENGRAM._configData && CO_ENGRAM._configData.hostType) || 'mcp-server';\n const HOST_LABEL = hostType === 'openclaw-plugin' ? 'openclaw gateway' : 'MCP server';\n const HOST_PARENT = hostType === 'openclaw-plugin' ? 'openclaw' : 'Claude Code';\n if (!confirm('\u786E\u8BA4\u91CD\u542F ' + HOST_LABEL + '?\\n\\n \u2022 \u5DE5\u5177\u4F1A\u77ED\u6682\u65AD\u5F00(\u51E0\u79D2\u5185\u81EA\u52A8\u91CD\u8FDE)\\n \u2022 \u6D4F\u89C8\u5668\u4F1A\u5931\u8054,\u672C\u9875\u9762\u4F1A\u5728\u670D\u52A1\u6062\u590D\u540E\u81EA\u52A8\u5237\u65B0\\n \u2022 \u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\u548C engram \u6570\u636E\u4E0D\u4F1A\u4E22\u5931')) return;\n // openclaw-plugin \u6A21\u5F0F:\u670D\u52A1\u7AEF\u4F1A\u62D2\u7EDD,\u76F4\u63A5\u63D0\u793A\u7528\u6237\u624B\u52A8\u91CD\u542F gateway\n if (hostType === 'openclaw-plugin') {\n alert('openclaw plugin \u6A21\u5F0F\u4E0D\u652F\u6301\u4ECE viewer \u81EA\u52A8\u91CD\u542F\u2014\u2014\u8FD9\u4F1A\u6740\u6389\u6574\u4E2A gateway \u8FDB\u7A0B,\u5F71\u54CD\u5176\u4ED6 plugin/\u4F1A\u8BDD\u3002\\n\\n\u8BF7\u624B\u52A8\u6267\u884C:\\n openclaw gateway restart');\n return;\n }\n // \u663E\u793A\u91CD\u542F\u906E\u7F69\n let mask = document.getElementById('restart-mask');\n if (!mask) {\n mask = document.createElement('div');\n mask.id = 'restart-mask';\n mask.style.cssText = 'position:fixed;inset:0;background:rgba(10,14,31,0.85);backdrop-filter:blur(8px);z-index:9999;display:flex;align-items:center;justify-content:center;color:var(--fg);font-size:1rem;flex-direction:column;gap:1rem';\n mask.innerHTML = '<div style=\"font-size:1.5rem\">\u27F3 \u6B63\u5728\u91CD\u542F ' + HOST_LABEL + '\u2026</div>'\n + '<div style=\"color:var(--fg-muted);font-size:.85rem;max-width:480px;text-align:center\">'\n + '\u670D\u52A1\u6B63\u5728\u9000\u51FA\u5E76\u7531\u7236\u8FDB\u7A0B(' + HOST_PARENT + ')\u91CD\u65B0\u62C9\u8D77\u3002\u9875\u9762\u4F1A\u5728\u6062\u590D\u540E\u81EA\u52A8\u5237\u65B0\u3002</div>'\n + '<div class=\"spinner\" style=\"width:24px;height:24px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin 0.8s linear infinite\"></div>';\n document.body.appendChild(mask);\n }\n try {\n await CO_ENGRAM.apiJson('/api/restart', 'POST');\n } catch {\n // \u9884\u671F\u5185:\u670D\u52A1\u9000\u51FA\u4F1A\u5BFC\u81F4 fetch \u5931\u8D25,\u7EE7\u7EED poll\n }\n // Poll until service comes back\n const start = Date.now();\n const deadline = start + 30000;\n let successCount = 0;\n const poll = async () => {\n if (Date.now() > deadline) {\n mask.innerHTML = '<div style=\"font-size:1.2rem;color:var(--accent-warm)\">\u91CD\u542F\u8D85\u65F6(30s)</div>'\n + '<div style=\"color:var(--fg-muted);font-size:.85rem\">\u8BF7\u624B\u52A8\u5237\u65B0\u9875\u9762;\u82E5 ' + HOST_LABEL + ' \u4ECD\u672A\u6062\u590D,\u8BF7\u68C0\u67E5 ' + HOST_PARENT + ' \u72B6\u6001\u3002</div>'\n + '<button class=\"btn\" onclick=\"location.reload()\" style=\"margin-top:.5rem\">\u624B\u52A8\u5237\u65B0</button>';\n return;\n }\n try {\n await CO_ENGRAM.apiGet('/api/stats');\n successCount++;\n if (successCount >= 2) {\n location.reload();\n return;\n }\n } catch {\n successCount = 0;\n }\n setTimeout(poll, 500);\n };\n setTimeout(poll, 800);\n },\n\n /**\n * \u6253\u5F00 drawer \u7F16\u8F91\"\u4E0B\u6B21\u542F\u52A8\u7684\u6570\u636E\u6839\u76EE\u5F55\"\u3002\n * \u5F53\u524D\u8FD0\u884C\u5B9E\u4F8B\u4E0D\u4F1A\u53D7\u5F71\u54CD\u2014\u2014\u5DF2\u52A0\u8F7D\u7684 repository/maintainer/viewer \u4ECD\u6307\u5411\u65E7\u8DEF\u5F84\u3002\n */\n editDataRoot() {\n const data = CO_ENGRAM._configData || {};\n const persisted = data.persisted || {};\n const current = data.dataRoot || '(\u672A\u77E5)';\n const desired = persisted.desiredDataRoot || '';\n const envSet = data.envSet || false;\n const envPath = data.envDataRoot || '';\n const envMatchedRuntime = !!data.envDataRootOverride;\n\n const body = '<div class=\"edit-banner\"><strong>\u5207\u6362\u6570\u636E\u6839\u76EE\u5F55</strong> \u00B7 \u4EC5\u5F71\u54CD\u4E0B\u6B21\u670D\u52A1\u542F\u52A8</div>'\n + '<div class=\"field\"><span class=\"field-label\">\u5F53\u524D\u8FD0\u884C\u65F6:</span><code>' + CO_ENGRAM.escapeHtml(current) + '</code></div>'\n + '<div class=\"field\"><label class=\"field-label\">\u4E0B\u6B21\u542F\u52A8\u4F7F\u7528:</label>'\n + '<input id=\"cf-dataroot\" type=\"text\" style=\"width:100%\" value=\"' + CO_ENGRAM.escapeHtml(desired) + '\" placeholder=\"(\u7559\u7A7A\u56DE\u9000\u5230 env / \u9ED8\u8BA4\u8DEF\u5F84)\">'\n + '<div style=\"font-size:0.75rem;color:var(--fg-muted);margin-top:0.3rem\">'\n + '\u7EDD\u5BF9\u8DEF\u5F84(\u5982 <code>/home/USER/team-memory</code> \u6216 <code>/var/lib/co-engram</code>)\u3002'\n + '\u7559\u7A7A\u5219\u6E05\u9664\u6301\u4E45\u5316\u503C,\u56DE\u9000\u5230 env / \u9ED8\u8BA4\u8DEF\u5F84\u3002'\n + '<br><strong style=\"color:var(--accent)\">\u4F18\u5148\u7EA7:\u6B64\u5904\u914D\u7F6E &gt; env CO_ENGRAM_DATA_ROOT &gt; \u9ED8\u8BA4\u8DEF\u5F84\u3002</strong>'\n + (envSet\n ? ' <span style=\"color:var(--fg-muted)\">\u68C0\u6D4B\u5230 env \u5DF2\u8BBE\u7F6E <code>' + CO_ENGRAM.escapeHtml(envPath || '(\u7A7A)') + '</code>' + (envMatchedRuntime ? '(\u5F53\u524D\u6B63\u662F env \u8DEF\u5F84)' : '') + ';\u6B64\u5904\u586B\u503C\u5C06<strong>\u8986\u76D6</strong> env\u3002</span>'\n : '')\n + '</div></div>'\n + '<div class=\"edit-banner\" style=\"background:rgba(251,191,36,0.06);border-color:rgba(251,191,36,0.25);color:var(--accent-warm)\">'\n + '<strong>\u6CE8\u610F:</strong>\u5207\u6362\u76EE\u5F55\u540E,\u65B0\u76EE\u5F55\u82E5\u4E3A\u7A7A\u5C06\u81EA\u52A8\u521D\u59CB\u5316;\u539F\u76EE\u5F55\u7684\u6570\u636E\u4E0D\u4F1A\u8FC1\u79FB\u3002'\n + '\u4FDD\u5B58\u540E\u9700\u8981\u91CD\u542F\u670D\u52A1\u624D\u4F1A\u5207\u6362\u3002</div>'\n + '<div class=\"config-save-bar\">'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_CONFIG.reload(); CO_ENGRAM.closeDrawer();\">\u53D6\u6D88</button>'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM_CONFIG.saveDataRoot()\">\u4FDD\u5B58\u671F\u671B\u503C</button>'\n + '</div>';\n CO_ENGRAM.openDrawer(body);\n },\n\n async saveDataRoot() {\n const input = document.getElementById('cf-dataroot');\n if (!input) return;\n const value = (input.value || '').trim();\n try {\n await CO_ENGRAM.apiJson('/api/config', 'PUT', { desiredDataRoot: value });\n CO_ENGRAM.closeDrawer();\n const hostType = (CO_ENGRAM._configData && CO_ENGRAM._configData.hostType) || 'mcp-server';\n const HOST_LABEL = hostType === 'openclaw-plugin' ? 'openclaw gateway' : 'MCP server';\n const banner = '<div class=\"edit-banner\" style=\"background:rgba(251,191,36,0.08);border-color:rgba(251,191,36,0.25);color:var(--accent-warm)\">\u2713 \u6570\u636E\u6839\u76EE\u5F55\u671F\u671B\u503C\u5DF2\u4FDD\u5B58:' + (value ? CO_ENGRAM.escapeHtml(value) : '(\u5DF2\u6E05\u9664,\u56DE\u9000\u9ED8\u8BA4)') + '\u3002<strong>\u91CD\u542F ' + HOST_LABEL + ' \u751F\u6548</strong>\u3002</div>';\n const root = document.getElementById('config-content');\n if (root) root.insertAdjacentHTML('afterbegin', banner);\n CO_ENGRAM._configLoaded = false;\n await this.render(document.getElementById('config-content'));\n } catch (e) { alert('\u4FDD\u5B58\u5931\u8D25:' + (e.message || e)); }\n }\n};\n\n// ============================================================\n// Synapses(\u88AB graph tab \u8C03\u7528,\u4E5F\u53EF\u72EC\u7ACB\u6253\u5F00)\n// ============================================================\nwindow.CO_ENGRAM_SYNAPSES = {\n async open(id) {\n let d;\n try { d = await CO_ENGRAM.apiGet('/api/synapses/' + encodeURIComponent(id)); }\n catch (e) { CO_ENGRAM.openDrawer('<div class=\"empty\">\u52A0\u8F7D\u5931\u8D25:' + CO_ENGRAM.escapeHtml(e.message) + '</div>'); return; }\n CO_ENGRAM._currentSynapse = d;\n this._renderView(d);\n },\n\n _renderView(d) {\n const L = CO_ENGRAM_LABELS;\n const id = CO_ENGRAM.escapeHtml(d.id);\n const kindLabel = L.synapse[d.kind] || d.kind;\n const family = CO_ENGRAM.synapseFamily(d.kind);\n const familyLabel = { structural: '\u7ED3\u6784', causal: '\u56E0\u679C', evidential: '\u8BC1\u636E', temporal: '\u65F6\u95F4', modulatory: '\u8C03\u8282' }[family] || family;\n const dirLabel = L.synapseDirection[d.direction] || d.direction || '\u5355\u5411';\n const evidence = (d.evidence || []);\n const evidenceHtml = evidence.length\n ? '<h3>\u8BC1\u636E (' + evidence.length + ')</h3>' + evidence.map(ev => '<div class=\"field markdown-body\" style=\"padding-left:.5rem;border-left:2px solid var(--border);margin-bottom:.4rem\">'\n + CO_ENGRAM.renderMarkdown(ev.description || '')\n + (ev.source ? ' <span class=\"chip\">' + CO_ENGRAM.escapeHtml(ev.source) + '</span>' : '')\n + (ev.confidence != null ? ' <span class=\"kpi-sub\">\u7F6E\u4FE1\u5EA6 ' + Number(ev.confidence).toFixed(2) + '</span>' : '')\n + (ev.addedBy ? ' <span class=\"kpi-sub\">\u00B7 ' + CO_ENGRAM.escapeHtml(ev.addedBy) + '</span>' : '')\n + '</div>').join('')\n : '<div class=\"empty\">\u65E0\u8BC1\u636E</div>';\n\n const body = '<div class=\"edit-banner\" style=\"display:flex;gap:.5rem;align-items:center\">'\n + '<strong style=\"margin-right:auto\">\u7A81\u89E6\u8BE6\u60C5</strong>'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM_SYNAPSES.edit()\">\u7F16\u8F91</button>'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_SYNAPSES.confirmDelete()\">\u5220\u9664</button>'\n + '</div>'\n + '<h2>' + CO_ENGRAM.escapeHtml(kindLabel) + '</h2>'\n + '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('synapse.' + d.kind) + '>\u7C7B\u578B:</span>' + CO_ENGRAM.escapeHtml(kindLabel) + ' (' + CO_ENGRAM.escapeHtml(d.kind) + ')</div>'\n + '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('family.' + family) + '>\u6240\u5C5E\u65CF:</span><span class=\"chip dot\" style=\"color:' + CO_ENGRAM.familyColor(family) + '\">' + CO_ENGRAM.escapeHtml(familyLabel) + '</span></div>'\n + '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('synapseDirection.' + (d.direction || 'directional')) + '>\u65B9\u5411:</span>' + CO_ENGRAM.escapeHtml(dirLabel) + '</div>'\n + '<div class=\"field\"><span class=\"field-label\">\u6743\u91CD:</span>' + (d.weight != null ? Number(d.weight).toFixed(2) : '\u2014') + '</div>'\n + (d.resolutionStatus ? '<div class=\"field\"><span class=\"field-label\"' + CO_ENGRAM.tip('resolution.' + d.resolutionStatus) + '>\u88C1\u51B3\u72B6\u6001:</span><span class=\"chip\" style=\"background:rgba(239,68,68,.12);color:#ef4444\">' + CO_ENGRAM.escapeHtml(L.resolution[d.resolutionStatus] || d.resolutionStatus) + '</span></div>' : '')\n + '<div class=\"field\"><span class=\"field-label\">ID:</span><code>' + id + '</code></div>'\n + '<div class=\"field\"><span class=\"field-label\">\u6E90 \u2192 \u76EE\u6807:</span><span class=\"engram-link\" data-engram-id=\"' + CO_ENGRAM.escapeHtml(d.from) + '\">' + CO_ENGRAM.escapeHtml(d.from) + '</span> \u2192 <span class=\"engram-link\" data-engram-id=\"' + CO_ENGRAM.escapeHtml(d.to) + '\">' + CO_ENGRAM.escapeHtml(d.to) + '</span></div>'\n + '<div class=\"field\"><span class=\"field-label\">\u521B\u5EFA\u8005:</span>' + CO_ENGRAM.escapeHtml(d.createdBy || '')\n + ' <span class=\"field-label\">\u65F6\u95F4:</span>' + CO_ENGRAM.escapeHtml(d.createdAt || '') + '</div>'\n + evidenceHtml;\n CO_ENGRAM.openDrawer(body);\n },\n\n edit() {\n const d = CO_ENGRAM._currentSynapse;\n if (!d) return;\n const L = CO_ENGRAM_LABELS;\n // 12 \u79CD kind,\u6309\u65CF\u5206\u7EC4,\u4E0E stats/graph tab \u4E00\u81F4\n const KINDS_BY_FAMILY = [\n { family: 'structural', label: '\u7ED3\u6784\u65CF', kinds: ['extends', 'part_of', 'similar_to'] },\n { family: 'causal', label: '\u56E0\u679C\u65CF', kinds: ['depends_on', 'causes', 'follows'] },\n { family: 'evidential', label: '\u8BC1\u636E\u65CF', kinds: ['derives_from', 'contradicts', 'exemplifies'] },\n { family: 'temporal', label: '\u65F6\u95F4\u65CF', kinds: ['supersedes', 'consolidates'] },\n { family: 'modulatory', label: '\u8C03\u8282\u65CF', kinds: ['contextualizes'] }\n ];\n const kindOptions = KINDS_BY_FAMILY.map(group =>\n '<optgroup label=\"' + group.label + '\">' + group.kinds.map(k =>\n '<option value=\"' + k + '\"' + (d.kind === k ? ' selected' : '') + CO_ENGRAM.tip('synapse.' + k) + '>' + (L.synapse[k] || k) + ' \u00B7 ' + k + '</option>'\n ).join('') + '</optgroup>'\n ).join('');\n const dirOptions = Object.keys(L.synapseDirection).map(k => '<option value=\"' + k + '\"' + (d.direction === k ? ' selected' : '') + CO_ENGRAM.tip('synapseDirection.' + k) + '>' + L.synapseDirection[k] + '</option>').join('');\n\n const body = '<div class=\"edit-banner\"><strong>\u7F16\u8F91\u6A21\u5F0F</strong> \u00B7 \u4FEE\u6539\u540E\u70B9\u51FB\"\u4FDD\u5B58\"\u63D0\u4EA4</div>'\n + '<h2>\u7F16\u8F91\u8BB0\u5FC6\u7A81\u89E6</h2>'\n + '<div class=\"field\"><span class=\"field-label\">ID:</span><code>' + CO_ENGRAM.escapeHtml(d.id) + '</code></div>'\n + '<div class=\"edit-banner\" style=\"background:rgba(251,191,36,.06);border-color:rgba(251,191,36,.25);color:var(--accent-warm)\">\u63D0\u793A:\u4FEE\u6539\"\u7C7B\u578B\"\u6216\"\u65B9\u5411\"\u4F1A\u8BA9\u7A81\u89E6 ID \u91CD\u65B0\u8BA1\u7B97(\u56E0 ID \u6D3E\u751F\u81EA from+to+kind+direction),\u65E7 ID \u5C06\u5931\u6548,\u4F46\u6240\u6709\u5143\u6570\u636E(\u6743\u91CD/\u8BC1\u636E/\u521B\u5EFA\u8005)\u4F1A\u8FC1\u79FB\u5230\u65B0 ID\u3002</div>'\n + '<div class=\"field\"><label class=\"field-label\"' + CO_ENGRAM.tip('synapse.' + d.kind) + '>\u7C7B\u578B</label><select id=\"sf-kind\"' + CO_ENGRAM.tip('synapse.' + d.kind) + '>' + kindOptions + '</select></div>'\n + '<div class=\"field\"><label class=\"field-label\"' + CO_ENGRAM.tip('synapseDirection.' + (d.direction || 'directional')) + '>\u65B9\u5411</label><select id=\"sf-direction\"' + CO_ENGRAM.tip('synapseDirection.' + (d.direction || 'directional')) + '>' + dirOptions + '</select></div>'\n + '<div class=\"field\"><label class=\"field-label\">\u6743\u91CD (0-1,\u53EF\u62D6\u52A8\u6ED1\u5757)</label><input id=\"sf-weight-range\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" value=\"' + (d.weight || 0) + '\" oninput=\"document.getElementById(\\'sf-weight\\').value=this.value\"><input id=\"sf-weight\" type=\"number\" min=\"0\" max=\"1\" step=\"0.01\" value=\"' + (d.weight || 0) + '\" oninput=\"document.getElementById(\\'sf-weight-range\\').value=this.value\" style=\"width:80px;margin-left:.5rem\"></div>'\n + '<div class=\"field\"><label class=\"field-label\">\u65B0\u589E\u8BC1\u636E\u63CF\u8FF0(\u53EF\u9009,\u7559\u7A7A\u5219\u4E0D\u8FFD\u52A0)</label><input id=\"sf-evidence-desc\" type=\"text\" placeholder=\"\u5982:\u901A\u8FC7 codegraph \u9A8C\u8BC1...\"></div>'\n + '<div class=\"field\"><label class=\"field-label\">\u8BC1\u636E\u6765\u6E90(\u53EF\u9009)</label><input id=\"sf-evidence-source\" type=\"text\" placeholder=\"\u5982:manual / ci / docs\"></div>'\n + '<div class=\"config-save-bar\">'\n + '<button class=\"btn secondary\" onclick=\"CO_ENGRAM_SYNAPSES.cancel()\">\u53D6\u6D88</button>'\n + '<button class=\"btn\" onclick=\"CO_ENGRAM_SYNAPSES.save()\">\u4FDD\u5B58</button>'\n + '</div>';\n CO_ENGRAM.openDrawer(body);\n },\n\n cancel() {\n const d = CO_ENGRAM._currentSynapse;\n if (d) this._renderView(d);\n },\n\n async save() {\n const d = CO_ENGRAM._currentSynapse;\n if (!d) return;\n const nextKind = document.getElementById('sf-kind').value;\n const nextDirection = document.getElementById('sf-direction').value;\n const patch = {\n weight: Number(document.getElementById('sf-weight').value),\n direction: nextDirection\n };\n // \u4EC5\u5F53 kind/direction \u53D8\u5316\u65F6\u4F20 kind(\u907F\u514D\u65E0\u8C13\u7684\u5220\u9664+\u91CD\u5EFA)\n if (nextKind !== d.kind) patch.kind = nextKind;\n const desc = (document.getElementById('sf-evidence-desc').value || '').trim();\n if (desc) {\n const source = (document.getElementById('sf-evidence-source').value || '').trim();\n patch.evidence = [{ description: desc, ...(source ? { source } : {}), addedBy: 'viewer' }];\n }\n try {\n const updated = await CO_ENGRAM.apiJson('/api/synapses/' + encodeURIComponent(d.id), 'PATCH', patch);\n CO_ENGRAM._currentSynapse = updated;\n this._renderView(updated);\n // kind \u53D8\u5316 \u2192 \u56FE\u8C31\u4E2D\u7684\u8FB9 id \u5DF2\u53D8,\u9700\u8981\u91CD\u8F7D\n if (patch.kind && CO_ENGRAM._graphState) {\n CO_ENGRAM._graphState.initialized = false;\n CO_ENGRAM._graphState = null;\n }\n } catch (e) { alert('\u4FDD\u5B58\u5931\u8D25:' + (e.message || e)); }\n },\n\n async confirmDelete() {\n const d = CO_ENGRAM._currentSynapse;\n if (!d) return;\n if (!confirm('\u786E\u5B9A\u5220\u9664\u6B64\u8BB0\u5FC6\u7A81\u89E6?\\n\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002')) return;\n try {\n await CO_ENGRAM.apiJson('/api/synapses/' + encodeURIComponent(d.id), 'DELETE', null);\n CO_ENGRAM.closeDrawer();\n CO_ENGRAM._currentSynapse = null;\n // \u91CD\u65B0\u52A0\u8F7D\u56FE\u8C31(\u5982\u679C\u5F53\u524D\u5728\u56FE\u8C31 tab)\n if (CO_ENGRAM._graphState) {\n CO_ENGRAM._graphState.initialized = false;\n CO_ENGRAM._graphState = null;\n const gc = document.getElementById('graph-canvas');\n if (gc) gc.innerHTML = '<div class=\"loading\">\u91CD\u65B0\u52A0\u8F7D\u56FE\u8C31\u4E2D</div>';\n CO_ENGRAM.onTabEnter('graph');\n }\n } catch (e) { alert('\u5220\u9664\u5931\u8D25:' + (e.message || e)); }\n }\n};\n\n// ============================================================\n// Help \u2014 \u5E2E\u52A9\u6587\u6863(\u6982\u5FF5\u5165\u95E8 + \u5404 tab \u901F\u67E5)\n// ============================================================\nCO_ENGRAM.on('help', async function() {\n const el = document.getElementById('help-content');\n if (!el) return;\n if (CO_ENGRAM._helpLoaded) return;\n CO_ENGRAM._helpLoaded = true;\n el.innerHTML = CO_ENGRAM_HELP.render();\n});\n\nwindow.CO_ENGRAM_HELP = {\n render() {\n return ''\n + '<div class=\"panel\" style=\"max-width:900px;margin:0 auto;padding:1.5rem;line-height:1.7\">'\n + '<h2 style=\"margin-top:0\">Co-Engram \u00B7 \u81EA\u8FDB\u5316\u7684\u56E2\u961F\u8BB0\u5FC6</h2>'\n + '<p>Co-Engram \u628A\u56E2\u961F\u5DE5\u4F5C\u4E2D\u7684\u5BF9\u8BDD\u3001\u51B3\u7B56\u3001\u8E29\u8FC7\u7684\u5751\u6C89\u6DC0\u4E3A<em>\u8BB0\u5FC6\u5370\u8FF9(engram)</em>,'\n + '\u7528<em>\u8BB0\u5FC6\u7A81\u89E6(synapse)</em>\u628A\u5B83\u4EEC\u8FDE\u6210\u53EF\u6F14\u5316\u7684\u77E5\u8BC6\u7F51\u7EDC\u3002\u6A21\u578B\u5728\u540E\u7EED\u4EFB\u52A1\u91CC\u901A\u8FC7 '\n + '<code>memory_search</code> \u53EC\u56DE\u76F8\u5173\u8BB0\u5FC6,\u5F15\u7528\u6709\u6548\u65F6\u8C03 <code>engram_reinforce</code> '\n + '\u5F3A\u5316,\u51FA\u9519\u65F6\u8C03 <code>engram_report_failure</code> \u5F31\u5316\u2014\u2014\u8FD9\u5957\u95ED\u73AF\u8BA9\u9AD8\u4EF7\u503C\u8BB0\u5FC6\u81EA\u52A8\u6D6E\u73B0\u3001\u8FC7\u65F6\u8BB0\u5FC6\u81EA\u52A8\u8870\u51CF\u3002</p>'\n\n + '<h3>\u6838\u5FC3\u6982\u5FF5</h3>'\n + '<dl style=\"padding-left:0.5rem;border-left:3px solid var(--border)\">'\n + '<dt><strong>\u8BB0\u5FC6\u5370\u8FF9(engram)</strong></dt>'\n + '<dd style=\"margin-bottom:0.6rem\">\u4E00\u6761\u7ED3\u6784\u5316\u7684\u8BB0\u5FC6\u6761\u76EE,\u542B\u6807\u9898/\u5185\u5BB9/\u7C7B\u578B/\u6807\u7B7E/\u91CD\u8981\u6027/\u7F6E\u4FE1\u5EA6\u7B49\u5B57\u6BB5\u3002'\n + '\u7C7B\u578B\u5206 5 \u79CD:<code>fact(\u4E8B\u5B9E)</code> <code>observation(\u89C2\u5BDF)</code> <code>pattern(\u6A21\u5F0F)</code> '\n + '<code>procedure(\u6D41\u7A0B)</code> <code>hypothesis(\u5047\u8BBE)</code>\u3002\u9F20\u6807\u60AC\u505C\u5B57\u6BB5\u53EF\u4EE5\u770B\u5230\u8BE5\u5B57\u6BB5\u7684\u89E3\u91CA\u3002</dd>'\n + '<dt><strong>\u8BB0\u5FC6\u7A81\u89E6(synapse)</strong></dt>'\n + '<dd style=\"margin-bottom:0.6rem\">\u8FDE\u63A5\u4E24\u4E2A engram \u7684\u6709\u5411\u8FB9,\u5206 5 \u4E2A\u65CF:'\n + '<code>\u7ED3\u6784\u65CF</code>(extends/part_of/similar_to)\u3001'\n + '<code>\u56E0\u679C\u65CF</code>(depends_on/causes/follows)\u3001'\n + '<code>\u8BC1\u636E\u65CF</code>(derives_from/contradicts/exemplifies)\u3001'\n + '<code>\u65F6\u95F4\u65CF</code>(supersedes/consolidates)\u3001'\n + '<code>\u8C03\u8282\u65CF</code>(contextualizes)\u3002<code>contradicts</code> \u4F1A\u8FDB\u5165\u88C1\u51B3\u6D41\u7A0B\u3002</dd>'\n + '<dt><strong>\u91CD\u8981\u6027(importance)\u4E0E\u7F6E\u4FE1\u5EA6(confidence)</strong></dt>'\n + '<dd style=\"margin-bottom:0.6rem\">\u4E24\u4E2A\u72EC\u7ACB\u7684 0-1 \u6570\u503C\u3002\u91CD\u8981\u6027\u7531\u5F3A\u5316\u4FE1\u53F7 + \u65F6\u95F4\u8870\u51CF\u6D3E\u751F,\u5F71\u54CD\u53EC\u56DE\u6743\u91CD;'\n + '\u7F6E\u4FE1\u5EA6\u53CD\u6620\u8BE5\u8BB0\u5FC6\u6210\u7ACB\u7684\u53EF\u4FE1\u7A0B\u5EA6(\u5143\u8BA4\u77E5\u8BC4\u5206),\u4E0E\u91CD\u8981\u6027\u89E3\u8026\u3002</dd>'\n + '<dt><strong>\u591A\u7EF4\u91CD\u8981\u6027\u5411\u91CF(importanceVector)</strong></dt>'\n + '<dd style=\"margin-bottom:0.6rem\">\u628A\u91CD\u8981\u6027\u62C6\u89E3\u4E3A personal/team/project/network/temporal 5 \u4E2A\u7EF4\u5EA6,\u4FBF\u4E8E\u7CBE\u7EC6\u5316\u8C03\u63A7\u3002'\n + '\u67E5\u770B engram \u8BE6\u60C5\u65F6\u5982\u679C\u5B58\u5728,\u4F1A\u663E\u793A\u5728\u4E13\u95E8\u7684\u6BB5\u843D\u91CC\u3002</dd>'\n + '<dt><strong>\u751F\u547D\u5468\u671F</strong></dt>'\n + '<dd style=\"margin-bottom:0.6rem\"><code>draft \u2192 active \u2192 archived \u2192 forgotten</code>\u3002'\n + '\u9057\u5FD8\u7684\u6587\u4EF6\u4ECD\u5728\u4ED3\u5E93,\u4F46\u9ED8\u8BA4\u4E0D\u53EC\u56DE\u3002\u7EF4\u62A4\u5468\u671F\u4F1A\u81EA\u52A8\u8BC4\u4F30\u5E76\u8FC1\u79FB\u72B6\u6001\u3002</dd>'\n + '</dl>'\n\n + '<h3>\u5404 tab \u7528\u9014</h3>'\n + '<ul style=\"padding-left:1.2rem\">'\n + '<li><strong>\u7EDF\u8BA1</strong>\u2014\u603B\u89C8\u4EEA\u8868\u76D8:\u6309\u7C7B\u578B/\u72B6\u6001/\u65CF\u5206\u5E03,\u663E\u793A\u56E2\u961F\u8D21\u732E\u8005\u548C top \u6807\u7B7E\u3002'\n + '\u9876\u90E8\u641C\u7D22\u6846\u505A\u5168\u6587\u68C0\u7D22\u3002</li>'\n + '<li><strong>\u8BB0\u5FC6\u5370\u8FF9</strong>\u2014\u5168\u90E8 engram \u7684\u5361\u7247/\u76EE\u5F55\u89C6\u56FE,\u652F\u6301\u6309 tag/kind/status \u8FC7\u6EE4,'\n + '\u70B9\u51FB\u8FDB\u5165\u8BE6\u60C5(\u53EF\u7F16\u8F91/\u5220\u9664/\u67E5\u770B\u7A81\u89E6)\u3002</li>'\n + '<li><strong>\u8BB0\u5FC6\u7A81\u89E6</strong>\u2014\u77E5\u8BC6\u56FE\u8C31\u53EF\u89C6\u5316\u3002'\n + '\u53EF\u6309\u65CF/\u7C7B\u578B\u8FC7\u6EE4\u8FB9,\u6309 engram \u7C7B\u578B\u8FC7\u6EE4\u8282\u70B9\u3002\u6253\u5F00 engram \u8BE6\u60C5\u65F6\u56FE\u8C31\u4F1A\u9AD8\u4EAE\u5176\u90BB\u5C45\u3002</li>'\n + '<li><strong>\u8BB0\u5FC6\u63D0\u6848</strong>\u2014\u5019\u9009\u8BB0\u5FC6\u5BA1\u6279\u961F\u5217\u3002\u7CFB\u7EDF\u4ECE\u5BF9\u8BDD\u4E2D\u63D0\u53D6\u5019\u9009,'\n + '\u7531\u4EBA\u5DE5/LLM \u91C7\u7EB3(engram_accept_proposal)\u6216\u5FFD\u7565(engram_dismiss_proposal)\u3002</li>'\n + '<li><strong>\u5BA1\u8BA1</strong>\u2014\u64CD\u4F5C\u65F6\u95F4\u7EBF,\u8BB0\u5F55 create/update/reinforce/report_failure \u7B49\u6240\u6709\u72B6\u6001\u53D8\u66F4,'\n + '\u4FBF\u4E8E\u8FFD\u6EAF\"\u8C01\u5728\u4F55\u65F6\u6539\u4E86\u4EC0\u4E48\"\u3002</li>'\n + '<li><strong>\u8BB0\u5FC6\u56DE\u6536\u7AD9</strong>\u2014\u88AB\u5220\u9664\u7684 engram \u6682\u5B58\u5904\u3002\u53EF\u6062\u590D\u5355\u4E2A,\u6216\u4E00\u952E\u6E05\u7A7A(\u652F\u6301\u6309\u5206\u533A\u7B5B\u9009,'\n + '\u6C38\u4E45\u5220\u9664\u524D\u4F1A dryRun \u9884\u626B\u63CF\u6761\u6570 + \u4E8C\u6B21\u786E\u8BA4)\u3002</li>'\n + '<li><strong>\u914D\u7F6E</strong>\u2014\u6570\u636E\u6839\u76EE\u5F55\u3001\u7EF4\u62A4\u5468\u671F\u3001\u81EA\u8FDB\u5316\u53C2\u6570\u7B49\u3002\u6539\u6301\u4E45\u5316\u914D\u7F6E\u540E\u9700\u91CD\u542F\u5BBF\u4E3B\u751F\u6548\u3002</li>'\n + '</ul>'\n\n + '<h3>\u8BB0\u5FC6\u600E\u4E48\u81EA\u52A8\u8FDB\u5316</h3>'\n + '<ol style=\"padding-left:1.2rem\">'\n + '<li><strong>\u68C0\u7D22</strong>:agent \u8C03 <code>memory_search</code>,FTS + \u4E09\u56E0\u5B50\u6253\u5206\u53EC\u56DE top-N\u3002</li>'\n + '<li><strong>\u5F15\u7528</strong>:agent \u628A\u76F8\u5173\u8BB0\u5FC6\u5185\u5BB9\u5199\u8FDB\u7B54\u6848,\u7528\u6237\u636E\u6B64\u51B3\u7B56\u3002</li>'\n + '<li><strong>\u5F3A\u5316</strong>:agent \u81EA\u4E3B\u5224\u65AD\u5F15\u7528\u662F\u5426\u6709\u6548\u2014\u2014\u6709\u6548\u8C03 <code>engram_reinforce</code>,'\n + '\u51FA\u9519\u8C03 <code>engram_report_failure</code>\u3002</li>'\n + '<li><strong>\u6269\u6563</strong>:\u5F3A\u5316\u901A\u8FC7\u7A81\u89E6\u6309 Hebbian \u6BD4\u4F8B\u6269\u6563\u5230\u90BB\u5C45(contradicts \u9664\u5916)\u3002</li>'\n + '<li><strong>\u8870\u51CF</strong>:\u6BCF\u4E2A engram \u6709 <code>decayHalfLifeDays</code>,'\n + 'importance \u6309 lastEffectiveAt + \u534A\u8870\u671F\u6307\u6570\u8870\u51CF\u3002</li>'\n + '<li><strong>\u7EF4\u62A4</strong>:\u540E\u53F0\u5468\u671F\u8DD1 light/deep/rem \u4E09\u9636\u6BB5,'\n + '\u5B8C\u6210\"\u5DE9\u56FA\u5F3A\u5316 \u2192 \u8870\u51CF\u9057\u5FD8 \u2192 REM \u62BD\u8C61\u6A21\u5F0F \u2192 \u89E6\u53D1\u5143\u8BA4\u77E5\u8BC4\u5206\"\u3002</li>'\n + '</ol>'\n\n + '<h3>\u63D0\u793A</h3>'\n + '<ul style=\"padding-left:1.2rem\">'\n + '<li>\u5B57\u6BB5\u540D\u65C1\u7684 <code>?</code> \u56FE\u6807(\u9F20\u6807\u60AC\u505C)\u6709\u8BE5\u5B57\u6BB5\u7684\u7B80\u77ED\u89E3\u91CA\u3002</li>'\n + '<li>\u8BE6\u60C5\u89C6\u56FE\u7684\"\u4EF7\u503C\u8BC4\u4F30/\u591A\u7EF4\u91CD\u8981\u6027/\u8BB0\u5FC6\u4EA7\u751F\u60C5\u5883\"\u6BB5\u843D\u4EC5\u5728 engram \u643A\u5E26\u76F8\u5E94\u5B57\u6BB5\u65F6\u663E\u793A\u3002</li>'\n + '<li>\u914D\u7F6E tab \u7684\u4FEE\u6539\u9ED8\u8BA4\u5199\u5165\u6301\u4E45\u5316\u6587\u4EF6,\u91CD\u542F\u5BBF\u4E3B(\u5982 <code>openclaw gateway restart</code>)\u540E\u751F\u6548\u3002</li>'\n + '<li>\u9047\u5230\u4ED3\u5E93\u4E0D\u4E00\u81F4,\u53EF\u5728 agent \u4E2D\u8C03 <code>engram_doctor</code> \u81EA\u6108\u626B\u63CF\u3002</li>'\n + '</ul>'\n\n + '</div>';\n }\n};\n";
8
+ //# sourceMappingURL=tabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../src/runtime/tabs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,YAAY,g6oHA+uDxB,CAAC"}