diff --git a/plugins/obsidian/main.js b/plugins/obsidian/main.js
index 322bfa5..1ac45c5 100644
--- a/plugins/obsidian/main.js
+++ b/plugins/obsidian/main.js
@@ -1,77 +1,88 @@
-var E=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var Y=Object.prototype.hasOwnProperty;var j=(a,r,e)=>r in a?E(a,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[r]=e;var Q=(a,r)=>{for(var e in r)E(a,e,{get:r[e],enumerable:!0})},K=(a,r,e,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of W(r))!Y.call(a,n)&&n!==e&&E(a,n,{get:()=>r[n],enumerable:!(i=O(r,n))||i.enumerable});return a};var U=a=>K(E({},"__esModule",{value:!0}),a);var s=(a,r,e)=>j(a,typeof r!="symbol"?r+"":r,e);var Z={};Q(Z,{default:()=>M});module.exports=U(Z);var m=require("obsidian");var I=require("obsidian");var X="http://localhost:7821",F="zhiyi-dev-key-2026";async function x(a,r={}){let n={method:r.method??"GET",headers:{"X-API-Key":F,"Content-Type":"application/json"}};r.body!==void 0&&(n.body=JSON.stringify(r.body));let o=await fetch(`${X}${a}`,n);if(!o.ok)throw new Error(`API ${a} failed: ${o.status} ${o.statusText}`);let l=await o.text();return l?JSON.parse(l):{}}async function P(a){let r=new URLSearchParams;a.namespace&&r.set("namespace",a.namespace),a.limit&&r.set("limit",String(a.limit)),a.offset!==void 0&&r.set("offset",String(a.offset));let e=r.toString();return x(`/api/v1/memories?${e}`)}async function R(a,r="hermes-main"){return x("/api/v1/search/recall",{method:"POST",body:{query:a,namespace:r,top_k:20}})}async function _(a){return x(`/api/v1/graph/navigate?entity=${encodeURIComponent(a)}&depth=1`)}async function N(a="hermes-main",r=100){return x(`/api/v1/graph/export?namespace=${a}&limit=${r}`)}async function H(){return x("/api/v1/distill/status")}async function V(){return x("/api/v1/distill/quota")}async function C(a=30){return x(`/api/v1/graph/pagerank?limit=${a}`)}async function $(a){return x(`/api/v1/memories/by-entity/${encodeURIComponent(a)}`)}var b="zhiyi-memory-view",k=class extends I.ItemView{constructor(e){super(e);s(this,"container");s(this,"memories",[]);s(this,"currentPage",0);s(this,"pageSize",20);s(this,"totalMemories",0);s(this,"status_interval",null);this.container=this.contentEl}getViewType(){return b}getDisplayText(){return"\u7EC7\u5FC6\u8BB0\u5FC6"}async onOpen(){this.render(),this.startStatusPoll()}async onClose(){this.status_interval!==null&&window.clearInterval(this.status_interval)}startStatusPoll(){let e=window.setInterval(()=>this.renderDistillBanner(),3e4);this.status_interval=e}async renderDistillBanner(){let e=this.container.querySelector(".zhiyi-distill-banner");if(e)try{let[i,n]=await Promise.all([H(),V()]);i.queue_len>0?(e.innerHTML=`\u26A0\uFE0F \u84B8\u998F\u961F\u5217: ${i.queue_len} \u6761`,e.style.display="block"):e.style.display="none"}catch{}}async render(){this.container.empty(),this.container.createEl("style",{text:`
- .zhiyi-memory-view { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
- .zhiyi-header { padding: 10px 12px 6px; border-bottom: 1px solid var(--background-modifier-border); }
- .zhiyi-header h1 { font-size: 14px; font-weight: 600; margin: 0 0 4px; }
- .zhiyi-stats { color: var(--text-muted); font-size: 11px; margin: 0; }
- .zhiyi-distill-banner {
- display: none; background: #c44; color: white; padding: 4px 12px;
- font-size: 12px; font-weight: 600; cursor: pointer;
- }
- .zhiyi-body { flex: 1; overflow-y: auto; }
- .zhiyi-item {
- padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border);
- cursor: pointer;
- }
- .zhiyi-item:hover { background: var(--background-modifier-hover); }
- .zhiyi-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; }
- .zhiyi-item-cat { font-size: 10px; color: var(--text-muted); background: var(--background-secondary); padding: 1px 4px; border-radius: 2px; }
- .zhiyi-item-score { font-size: 10px; color: var(--text-muted); }
- .zhiyi-item-content { font-size: 12px; color: var(--text-normal); line-height: 1.4; }
- .zhiyi-item-time { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
- .zhiyi-item-id { font-size: 10px; color: var(--text-faint); font-family: monospace; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; }
- .zhiyi-pagination { display: flex; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--background-modifier-border); }
- .zhiyi-pagination button { flex: 1; padding: 4px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; font-size: 11px; }
- .zhiyi-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
- .zhiyi-loading { padding: 20px; text-align: center; color: var(--text-muted); }
- .zhiyi-empty { padding: 20px; text-align: center; color: var(--text-muted); }
- `}),this.container.createEl("div",{cls:"zhiyi-memory-view"}).append(this.container.createEl("div",{cls:"zhiyi-distill-banner"}),this.container.createEl("div",{cls:"zhiyi-header"}).append(this.container.createEl("h1",{text:"\u{1F9E0} \u7EC7\u5FC6\u8BB0\u5FC6"}),this.container.createEl("p",{cls:"zhiyi-stats",text:"\u52A0\u8F7D\u4E2D..."})),this.container.createEl("div",{cls:"zhiyi-body"}),this.container.createEl("div",{cls:"zhiyi-pagination"})),await this.loadMemories(),await this.renderDistillBanner()}async loadMemories(){let e=this.container.querySelector(".zhiyi-body"),i=this.container.querySelector(".zhiyi-stats");e.innerHTML='
\u52A0\u8F7D\u4E2D\u2026
';try{let n=await P({namespace:"hermes-main",limit:this.pageSize,offset:this.currentPage*this.pageSize});this.memories=n.memories||[],this.totalMemories=n.total||0,i.setText(`${this.totalMemories} \u6761\u8BB0\u5FC6 \xB7 \u7B2C ${this.currentPage+1} \u9875`),this.renderList()}catch(n){e.innerHTML=`\u52A0\u8F7D\u5931\u8D25: ${n.message}
`}}renderList(){let e=this.container.querySelector(".zhiyi-body"),i=this.container.querySelector(".zhiyi-pagination");e.empty(),this.memories.length===0&&e.createEl("div",{cls:"zhiyi-empty",text:"\u6682\u65E0\u8BB0\u5FC6"});for(let o of this.memories){let l=e.createEl("div",{cls:"zhiyi-item"}),d=l.createEl("div",{cls:"zhiyi-item-header"});d.createEl("span",{cls:"zhiyi-item-cat",text:o.category||"normal"}),d.createEl("span",{cls:"zhiyi-item-score",text:`score: ${(o.quality_score??0).toFixed(2)}`}),l.createEl("div",{cls:"zhiyi-item-content",text:this.truncate(o.content,120)}),l.createEl("div",{cls:"zhiyi-item-time",text:this.formatTime(o.created_at)}),l.createEl("div",{cls:"zhiyi-item-id",text:o.id})}let n=Math.ceil(this.totalMemories/this.pageSize);if(i.empty(),n>1){let o=i.createEl("button",{text:"\u25C0 \u4E0A\u4E00\u9875"});o.setAttr("disabled",this.currentPage===0?"true":""),o.onclick=()=>{this.currentPage--,this.loadMemories()},i.createEl("span",{text:`${this.currentPage+1}/${n}`,cls:"zhiyi-item-cat"});let l=i.createEl("button",{text:"\u4E0B\u4E00\u9875 \u25B6"});l.setAttr("disabled",this.currentPage>=n-1?"true":""),l.onclick=()=>{this.currentPage++,this.loadMemories()}}}truncate(e,i){return e?e.length>i?e.slice(0,i)+"\u2026":e:""}formatTime(e){if(!e)return"";try{return new Date(e).toLocaleString("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return e}}};var D=require("obsidian");async function J(){return window.d3?window.d3:new Promise((a,r)=>{let e=document.createElement("script");e.src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js",e.onload=()=>a(window.d3),e.onerror=r,document.head.appendChild(e)})}var w="zhiyi-graph-view",S=class extends D.ItemView{constructor(e){super(e);s(this,"container");s(this,"currentEntity","");s(this,"d3",null);s(this,"svg",null);s(this,"simulation",null);this.container=this.contentEl}getViewType(){return w}getDisplayText(){return"\u7EC7\u5FC6\u56FE\u8C31"}async onOpen(){this.render()}async onClose(){this.simulation?.stop()}async render(){this.container.empty(),this.container.createEl("style",{text:`
- .zhiyi-graph-view { height: 100%; display: flex; flex-direction: column; }
- .zhiyi-graph-toolbar {
- display: flex; gap: 6px; padding: 6px 10px;
- border-bottom: 1px solid var(--background-modifier-border);
- align-items: center;
- }
- .zhiyi-graph-toolbar input {
- flex: 1; padding: 4px 8px; font-size: 12px;
- background: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: 4px; color: var(--text-normal);
- }
- .zhiyi-graph-toolbar button {
- padding: 4px 10px; font-size: 12px; cursor: pointer;
- background: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: 4px;
- }
- .zhiyi-graph-toolbar .info {
- font-size: 11px; color: var(--text-muted); padding: 0 6px;
- }
- .zhiyi-graph-canvas { flex: 1; position: relative; overflow: hidden; }
- .zhiyi-graph-canvas svg { width: 100%; height: 100%; }
- .zhiyi-graph-detail {
- position: absolute; bottom: 0; left: 0; right: 0;
- background: var(--background-secondary);
- border-top: 1px solid var(--background-modifier-border);
- padding: 8px 12px; font-size: 11px; max-height: 120px; overflow-y: auto;
- }
- .zhiyi-graph-detail-title { font-weight: 600; margin-bottom: 4px; }
- .zhiyi-graph-node-label { font-size: 10px; fill: var(--text-muted); pointer-events: none; }
- .zhiyi-legend { position: absolute; top: 8px; right: 8px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; font-size: 10px; }
- .zhiyi-legend-item { display: flex; align-items: center; gap: 4px; margin: 2px 0; }
- .zhiyi-legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
- .zhiyi-tooltip {
- position: absolute; background: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: 4px; padding: 6px 8px; font-size: 11px;
- pointer-events: none; opacity: 0; transition: opacity 0.15s;
- max-width: 200px; z-index: 10;
- }
- `});let e=this.container.createEl("div",{cls:"zhiyi-graph-view"}),i=e.createEl("div",{cls:"zhiyi-graph-toolbar"}),n=i.createEl("input",{attr:{placeholder:"\u8F93\u5165\u5B9E\u4F53\u540D\u79F0\uFF0C\u56DE\u8F66\u67E5\u770B ego-network\uFF08\u5982: \u7267\u5C18\uFF09"}}),o=i.createEl("button",{text:"\u52A0\u8F7D\u56FE\u8C31"});i.createEl("span",{cls:"info",text:"\u70B9\u51FB\u8282\u70B9\u67E5\u770B\u8BE6\u60C5"});let l=i.createEl("button",{text:"Top \u8282\u70B9"}),d=e.createEl("div",{cls:"zhiyi-graph-canvas"}),p=d.createEl("div",{cls:"zhiyi-tooltip"}),u=d.createEl("div",{cls:"zhiyi-legend"});u.innerHTML=`
+var G=Object.defineProperty;var oe=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var ce=Object.prototype.hasOwnProperty;var le=(a,n,e)=>n in a?G(a,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[n]=e;var de=(a,n)=>()=>(a&&(n=a(a=0)),n);var X=(a,n)=>{for(var e in n)G(a,e,{get:n[e],enumerable:!0})},pe=(a,n,e,t)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of se(n))!ce.call(a,r)&&r!==e&&G(a,r,{get:()=>n[r],enumerable:!(t=oe(n,r))||t.enumerable});return a};var Z=a=>pe(G({},"__esModule",{value:!0}),a);var p=(a,n,e)=>le(a,typeof n!="symbol"?n+"":n,e);var te={};X(te,{fetchByEntity:()=>W,fetchCategories:()=>K,fetchDistillQuota:()=>B,fetchDistillStatus:()=>V,fetchGraphExport:()=>D,fetchMemories:()=>H,fetchNeighbors:()=>$,fetchPageRank:()=>R,fetchStats:()=>ue,getApiBase:()=>J,getApiKey:()=>ee,getRuntimeNamespace:()=>v,searchMemories:()=>A,setRuntimeConfig:()=>C,writeMemory:()=>j});function J(){return(S.apiUrl||he).replace(/\/$/,"")}function ee(){return S.apiKey||ge}function C(a,n,e){S.apiUrl=a,S.apiKey=n,S.namespace=e}function v(){return S.namespace||"hermes-main"}async function z(a,n={}){let e=n.method??"GET",t={"X-API-Key":ee(),"Content-Type":"application/json"},r={method:e,headers:t};n.body!==void 0&&(r.body=JSON.stringify(n.body));let o=await fetch(`${J()}${a}`,r);if(!o.ok)throw new Error(`API ${a} failed: ${o.status} ${o.statusText}`);let s=await o.text();return s?JSON.parse(s):{}}async function ue(){return z("/api/v1/stats")}async function H(a){let n=new URLSearchParams,e=a.namespace||v();e&&n.set("namespace",e),a.limit&&n.set("limit",String(a.limit)),a.offset!==void 0&&n.set("offset",String(a.offset)),a.category&&n.set("category",a.category);let t=n.toString();return z(`/api/v1/memories?${t}`)}async function A(a,n){return z("/api/v1/search/recall",{method:"POST",body:{query:a,namespace:n||v(),top_k:20}})}async function $(a){return z(`/api/v1/graph/navigate?entity=${encodeURIComponent(a)}&depth=1`)}async function D(a,n=100){return z(`/api/v1/graph/export?namespace=${a||v()}&limit=${n}`)}async function V(){return z("/api/v1/distill/status")}async function B(){return z("/api/v1/distill/quota")}async function R(a=30){return z(`/api/v1/graph/pagerank?limit=${a}`)}async function W(a){return z(`/api/v1/memories/by-entity/${encodeURIComponent(a)}`)}async function j(a,n="normal",e){return z("/api/v1/memories",{method:"POST",body:{content:a,category:n,namespace:e||v(),source:"obsidian-plugin"}})}async function K(){try{let a=await H({limit:500}),n=new Set;for(let e of a.memories)e.category&&n.add(e.category);return Array.from(n).sort()}catch{return[]}}var he,ge,S,T=de(()=>{he="http://localhost:7821",ge="zhiyi-dev-key-2026",S={}});var ye={};X(ye,{default:()=>I});module.exports=Z(ye);var x=require("obsidian");var ie=require("obsidian");T();var M="zhiyi-memory-view",O=30,N=class extends ie.ItemView{constructor(e){super(e);p(this,"memories",[]);p(this,"currentPage",0);p(this,"totalMemories",0);p(this,"status_interval",null);p(this,"categories",[]);p(this,"activeCategory","");p(this,"expandedId",null)}getViewType(){return M}getDisplayText(){return"\u7EC7\u5FC6\u8BB0\u5FC6"}async onOpen(){this.render(),this.startStatusPoll()}async onClose(){this.status_interval!==null&&window.clearInterval(this.status_interval)}startStatusPoll(){let e=window.setInterval(()=>this.renderDistillBanner(),3e4);this.status_interval=e}async renderDistillBanner(){let e=this.container.querySelector(".zhiyi-distill-banner");if(e)try{let[t,r]=await Promise.all([V(),B()]);t.queue_len>0?(e.innerHTML=`\u26A0\uFE0F \u84B8\u998F\u961F\u5217: ${t.queue_len} \u6761`,e.style.display="block"):e.style.display="none"}catch{}}async loadCategories(){try{this.categories=await K()}catch{this.categories=[]}}async render(){this.container.empty(),this.container.createEl("style",{text:`
+ .zhiyi-memory-view { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
+ .zhiyi-header { padding: 8px 12px 4px; border-bottom: 1px solid var(--background-modifier-border); }
+ .zhiyi-header-row1 { display: flex; align-items: center; gap: 6px; }
+ .zhiyi-header h1 { font-size: 14px; font-weight: 600; margin: 0; flex: 1; }
+ .zhiyi-stats { color: var(--text-muted); font-size: 11px; margin: 3px 0 0; }
+ .zhiyi-toolbar { display: flex; gap: 4px; padding: 4px 12px; border-bottom: 1px solid var(--background-modifier-border); flex-wrap: wrap; align-items: center; }
+ .zhiyi-toolbar select { background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; color: var(--text-normal); font-size: 11px; padding: 2px 4px; max-width: 120px; }
+ .zhiyi-filter-chip { padding: 2px 8px; border-radius: 10px; font-size: 10px; border: 1px solid var(--background-modifier-border); background: var(--background-secondary); color: var(--text-muted); cursor: pointer; white-space: nowrap; }
+ .zhiyi-filter-chip:hover { background: var(--background-modifier-hover); }
+ .zhiyi-filter-chip.active { background: var(--text-accent); color: var(--bg-on-accent, #fff); border-color: var(--text-accent); }
+ .zhiyi-btn-icon { padding: 2px 6px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; color: var(--text-muted); font-size: 11px; }
+ .zhiyi-btn-icon:hover { background: var(--background-modifier-hover); color: var(--text-normal); }
+ .zhiyi-distill-banner { display: none; background: #c44; color: white; padding: 4px 12px; font-size: 12px; font-weight: 600; cursor: pointer; }
+ .zhiyi-body { flex: 1; overflow-y: auto; }
+ .zhiyi-item { padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border); cursor: pointer; }
+ .zhiyi-item:hover { background: var(--background-modifier-hover); }
+ .zhiyi-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px; }
+ .zhiyi-item-cat { font-size: 10px; color: var(--text-muted); background: var(--background-secondary); padding: 1px 5px; border-radius: 2px; }
+ .zhiyi-item-right { display: flex; align-items: center; gap: 6px; }
+ .zhiyi-quality-bar { width: 40px; height: 4px; background: var(--background-modifier-border); border-radius: 2px; overflow: hidden; }
+ .zhiyi-quality-fill { height: 100%; border-radius: 2px; }
+ .zhiyi-item-content { font-size: 12px; color: var(--text-normal); line-height: 1.4; }
+ .zhiyi-item-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 3px; }
+ .zhiyi-item-time { font-size: 10px; color: var(--text-muted); }
+ .zhiyi-item-meta { font-size: 10px; color: var(--text-faint); font-family: monospace; }
+ .zhiyi-item-actions { display: flex; gap: 6px; opacity: 0; transition: opacity 0.15s; }
+ .zhiyi-item:hover .zhiyi-item-actions { opacity: 1; }
+ .zhiyi-action-btn { font-size: 10px; color: var(--text-accent); cursor: pointer; background: none; border: none; padding: 0; }
+ .zhiyi-action-btn:hover { text-decoration: underline; }
+ .zhiyi-pagination { display: flex; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--background-modifier-border); align-items: center; }
+ .zhiyi-pagination button { padding: 4px 10px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; font-size: 11px; }
+ .zhiyi-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
+ .zhiyi-pagination .page-info { flex: 1; text-align: center; font-size: 11px; color: var(--text-muted); }
+ .zhiyi-loading { padding: 30px; text-align: center; color: var(--text-muted); }
+ .zhiyi-empty { padding: 30px; text-align: center; color: var(--text-muted); }
+ /* Modal */
+ .zhiyi-detail-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
+ .zhiyi-detail-box { background: var(--modal-background); border: 1px solid var(--background-modifier-border); border-radius: 8px; max-width: 600px; width: 90%; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
+ .zhiyi-detail-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--background-modifier-border); }
+ .zhiyi-detail-header h3 { margin: 0; font-size: 14px; font-weight: 600; }
+ .zhiyi-detail-close { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 18px; padding: 0; }
+ .zhiyi-detail-body { flex: 1; overflow-y: auto; padding: 16px; }
+ .zhiyi-detail-meta { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
+ .zhiyi-detail-meta span { font-size: 11px; color: var(--text-muted); background: var(--background-secondary); padding: 2px 6px; border-radius: 3px; }
+ .zhiyi-detail-content { font-size: 13px; line-height: 1.6; color: var(--text-normal); white-space: pre-wrap; word-break: break-word; }
+ .zhiyi-detail-footer { padding: 10px 16px; border-top: 1px solid var(--background-modifier-border); display: flex; gap: 8px; justify-content: flex-end; }
+ .zhiyi-detail-footer button { padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; }
+ /* Write dialog */
+ .zhiyi-write-section { padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border); }
+ .zhiyi-write-input { width: 100%; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; color: var(--text-normal); padding: 6px 8px; font-size: 12px; resize: vertical; min-height: 50px; box-sizing: border-box; }
+ .zhiyi-write-input:focus { outline: none; border-color: var(--text-accent); }
+ .zhiyi-write-meta { display: flex; gap: 6px; margin-top: 4px; align-items: center; }
+ .zhiyi-write-meta select { background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; color: var(--text-normal); font-size: 11px; padding: 2px 4px; }
+ .zhiyi-write-meta button { padding: 3px 10px; background: var(--text-accent); color: var(--bg-on-accent, #fff); border: none; border-radius: 4px; cursor: pointer; font-size: 11px; }
+ .zhiyi-write-meta button:disabled { opacity: 0.5; cursor: not-allowed; }
+ .zhiyi-write-hint { font-size: 10px; color: var(--text-faint); margin-left: auto; }
+ `}),this.container.createEl("div",{cls:"zhiyi-memory-view"}).append(this.container.createEl("div",{cls:"zhiyi-distill-banner"}),this.container.createEl("div",{cls:"zhiyi-header"}).append(Object.assign(this.container.createEl("div",{cls:"zhiyi-header-row1"}),{innerHTML:"\u{1F9E0} \u7EC7\u5FC6\u8BB0\u5FC6
"}),this.container.createEl("p",{cls:"zhiyi-stats",text:"\u52A0\u8F7D\u4E2D..."})),this.container.createEl("div",{cls:"zhiyi-toolbar"}),this.container.createEl("div",{cls:"zhiyi-body"}),this.container.createEl("div",{cls:"zhiyi-pagination"})),await this.loadCategories(),await this.loadMemories(),await this.renderDistillBanner()}async loadMemories(){let e=this.container.querySelector(".zhiyi-body"),t=this.container.querySelector(".zhiyi-stats"),r=this.container.querySelector(".zhiyi-toolbar");e.innerHTML='\u52A0\u8F7D\u4E2D\u2026
';try{let o=v(),s=await H({namespace:o,limit:O,offset:this.currentPage*O,category:this.activeCategory||void 0});this.memories=s.memories||[],this.totalMemories=s.total||0,t.setText(`${this.totalMemories} \u6761\u8BB0\u5FC6 \xB7 \u7B2C ${this.currentPage+1} \u9875 \xB7 ${o}`),this.renderToolbar(r),this.renderList()}catch(o){e.innerHTML=`\u52A0\u8F7D\u5931\u8D25: ${o.message}
`}}renderToolbar(e){e.empty();let t=e.createEl("select");t.createEl("option",{value:"hermes-main",text:"hermes-main"}),t.createEl("option",{value:"openclaw",text:"openclaw"}),t.value=v(),t.onchange=()=>{let c=t.value;window.zhiyi_namespace=c,this.currentPage=0,this.loadMemories()};let r=e.createEl("span",{cls:"zhiyi-filter-chip"+(this.activeCategory===""?" active":""),text:"\u5168\u90E8"});r.onclick=()=>{this.activeCategory="",this.currentPage=0,this.loadMemories()};for(let c of this.categories.slice(0,8)){let l=e.createEl("span",{cls:"zhiyi-filter-chip"+(this.activeCategory===c?" active":""),text:c});l.onclick=()=>{this.activeCategory=c,this.currentPage=0,this.loadMemories()}}let o=e.createEl("button",{cls:"zhiyi-btn-icon",text:"\u21BB \u5237\u65B0"});o.onclick=()=>{this.currentPage=0,this.loadMemories()};let s=e.createEl("button",{cls:"zhiyi-btn-icon",text:"+ \u5199\u8BB0\u5FC6"});s.onclick=()=>this.showWriteSection(e)}showWriteSection(e){let t=e.querySelector(".zhiyi-write-section");if(t){t.remove();return}let r=e.createEl("div",{cls:"zhiyi-write-section"}),o=r.createEl("textarea",{cls:"zhiyi-write-input",attr:{placeholder:"\u8F93\u5165\u8BB0\u5FC6\u5185\u5BB9\uFF0C\u6309 Enter \u6216\u70B9\u51FB\u300C\u5199\u5165\u300D\u63D0\u4EA4\u2026"}}),s=r.createEl("div",{cls:"zhiyi-write-meta"}),c=s.createEl("select");c.createEl("option",{value:"normal",text:"\u5206\u7C7B"});for(let d of this.categories)c.createEl("option",{value:d,text:d});let l=s.createEl("span",{cls:"zhiyi-write-hint",text:"Enter \u5199\u5165 \xB7 Esc \u53D6\u6D88"}),h=s.createEl("button",{text:"\u5199\u5165"}),m=async()=>{let d=o.value.trim();if(d){h.setAttr("disabled","true");try{await j(d,c.value||"normal"),o.value="",r.remove(),this.currentPage=0,await this.loadCategories(),await this.loadMemories()}catch(y){l.setText(`\u5931\u8D25: ${y.message}`)}h.removeAttr("disabled")}};h.onclick=m,o.onkeydown=d=>{d.key==="Enter"&&!d.shiftKey&&(d.preventDefault(),m()),d.key==="Escape"&&r.remove()},o.focus()}renderList(){let e=this.container.querySelector(".zhiyi-body"),t=this.container.querySelector(".zhiyi-pagination");e.empty(),this.memories.length===0&&e.createEl("div",{cls:"zhiyi-empty",text:"\u6682\u65E0\u8BB0\u5FC6"});for(let c of this.memories){let l=e.createEl("div",{cls:"zhiyi-item"}),h=l.createEl("div",{cls:"zhiyi-item-header"});h.createEl("span",{cls:"zhiyi-item-cat",text:c.category||"normal"});let m=h.createEl("div",{cls:"zhiyi-item-right"}),d=c.quality_score??0,y=d>.7?"#34c759":d>.4?"#f6c945":"#ff6b6b",w=m.createEl("div",{cls:"zhiyi-quality-bar"}).createEl("div",{cls:"zhiyi-quality-fill"});w.style.width=`${Math.round(d*100)}%`,w.style.background=y,m.createEl("span",{cls:"zhiyi-item-score",text:(d*100).toFixed(0)+"%"}),l.createEl("div",{cls:"zhiyi-item-content",text:this.truncate(c.content,160)});let b=l.createEl("div",{cls:"zhiyi-item-footer"});b.createEl("span",{cls:"zhiyi-item-time",text:this.formatTime(c.created_at)});let P=b.createEl("span",{cls:"zhiyi-item-meta",text:c.id.slice(0,16)}),E=b.createEl("div",{cls:"zhiyi-item-actions"});E.createEl("button",{cls:"zhiyi-action-btn",text:"\u5C55\u5F00"}).onclick=g=>{g.stopPropagation(),this.showDetail(c)},E.createEl("button",{cls:"zhiyi-action-btn",text:"\u590D\u5236"}).onclick=g=>{g.stopPropagation(),navigator.clipboard.writeText(c.content).catch(()=>{})},l.onclick=()=>this.showDetail(c)}let r=Math.ceil(this.totalMemories/O)||1;t.empty();let o=t.createEl("button",{text:"\u25C0"});o.setAttr("disabled",this.currentPage===0?"true":""),o.onclick=()=>{this.currentPage--,this.loadMemories()},t.createEl("span",{cls:"page-info",text:`${this.currentPage+1} / ${r}`});let s=t.createEl("button",{text:"\u25B6"});s.setAttr("disabled",this.currentPage>=r-1?"true":""),s.onclick=()=>{this.currentPage++,this.loadMemories()}}showDetail(e){let t=this.container.createEl("div",{cls:"zhiyi-detail-modal"}),r=t.createEl("div",{cls:"zhiyi-detail-box"}),o=r.createEl("div",{cls:"zhiyi-detail-header"});o.createEl("h3",{text:"\u8BB0\u5FC6\u8BE6\u60C5"});let s=o.createEl("button",{cls:"zhiyi-detail-close",text:"\xD7"});s.onclick=()=>t.remove(),t.onclick=m=>{m.target===t&&t.remove()};let c=r.createEl("div",{cls:"zhiyi-detail-body"}),l=c.createEl("div",{cls:"zhiyi-detail-meta"});l.createEl("span",{text:`\u5206\u7C7B: ${e.category||"normal"}`}),l.createEl("span",{text:`\u8D28\u91CF: ${((e.quality_score??0)*100).toFixed(1)}%`}),l.createEl("span",{text:`Namespace: ${e.namespace}`}),l.createEl("span",{text:`\u65F6\u95F4: ${this.formatTime(e.created_at)}`}),e.recall_count!==void 0&&l.createEl("span",{text:`\u53EC\u56DE: ${e.recall_count}`}),c.createEl("div",{cls:"zhiyi-detail-content",text:e.content});let h=r.createEl("div",{cls:"zhiyi-detail-footer"});h.createEl("button",{text:"\u590D\u5236\u5185\u5BB9",attr:{style:"background:var(--background-secondary);border:1px solid var(--background-modifier-border);color:var(--text-normal)"}}).onclick=()=>{navigator.clipboard.writeText(e.content).catch(()=>{})},h.createEl("button",{text:"\u5173\u95ED",attr:{style:"background:var(--text-accent);border:none;color:var(--bg-on-accent,#fff)"}}).onclick=()=>t.remove()}truncate(e,t){return e?e.length>t?e.slice(0,t)+"\u2026":e:""}formatTime(e){if(!e)return"";try{return new Date(e).toLocaleString("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return e}}};var re=require("obsidian");T();async function me(){return window.d3?window.d3:new Promise((a,n)=>{let e=document.createElement("script");e.src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js",e.onload=()=>a(window.d3),e.onerror=n,document.head.appendChild(e)})}var L="zhiyi-graph-view",_=class extends re.ItemView{constructor(e){super(e);p(this,"currentEntity","");p(this,"d3",null);p(this,"svg",null);p(this,"simulation",null);p(this,"topEntities",[])}getViewType(){return L}getDisplayText(){return"\u7EC7\u5FC6\u56FE\u8C31"}async onOpen(){this.render()}async onClose(){this.simulation?.stop()}async render(){this.container.empty(),this.container.createEl("style",{text:`
+ .zhiyi-graph-view { height: 100%; display: flex; flex-direction: column; }
+ .zhiyi-graph-toolbar { display: flex; gap: 6px; padding: 6px 10px; border-bottom: 1px solid var(--background-modifier-border); align-items: center; }
+ .zhiyi-graph-toolbar input { flex: 1; padding: 4px 8px; font-size: 12px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; color: var(--text-normal); }
+ .zhiyi-graph-toolbar button { padding: 4px 10px; font-size: 12px; cursor: pointer; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; }
+ .zhiyi-graph-toolbar .info { font-size: 11px; color: var(--text-muted); padding: 0 4px; }
+ .zhiyi-graph-body { flex: 1; display: flex; overflow: hidden; }
+ .zhiyi-graph-entity-list { width: 160px; border-right: 1px solid var(--background-modifier-border); overflow-y: auto; padding: 4px 0; flex-shrink: 0; }
+ .zhiyi-graph-entity-list h4 { margin: 0; padding: 4px 8px; font-size: 11px; color: var(--text-muted); font-weight: 600; border-bottom: 1px solid var(--background-modifier-border); }
+ .zhiyi-entity-item { padding: 4px 8px; font-size: 11px; cursor: pointer; color: var(--text-normal); display: flex; justify-content: space-between; align-items: center; }
+ .zhiyi-entity-item:hover { background: var(--background-modifier-hover); }
+ .zhiyi-entity-item.active { color: var(--text-accent); background: var(--background-modifier-hover); }
+ .zhiyi-entity-score { font-size: 9px; color: var(--text-faint); }
+ .zhiyi-graph-canvas { flex: 1; position: relative; overflow: hidden; }
+ .zhiyi-graph-canvas svg { width: 100%; height: 100%; }
+ .zhiyi-graph-detail { position: absolute; bottom: 0; left: 0; right: 0; background: var(--background-secondary); border-top: 1px solid var(--background-modifier-border); padding: 8px 12px; font-size: 11px; max-height: 120px; overflow-y: auto; }
+ .zhiyi-graph-detail-title { font-weight: 600; margin-bottom: 4px; }
+ .zhiyi-graph-node-label { font-size: 10px; fill: var(--text-muted); pointer-events: none; }
+ .zhiyi-legend { position: absolute; top: 8px; right: 8px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; font-size: 10px; }
+ .zhiyi-legend-item { display: flex; align-items: center; gap: 4px; margin: 2px 0; }
+ .zhiyi-legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
+ .zhiyi-tooltip { position: absolute; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; font-size: 11px; pointer-events: none; opacity: 0; transition: opacity 0.15s; max-width: 200px; z-index: 10; }
+ `});let e=this.container.createEl("div",{cls:"zhiyi-graph-view"}),t=e.createEl("div",{cls:"zhiyi-graph-toolbar"}),r=t.createEl("input",{attr:{placeholder:"\u8F93\u5165\u5B9E\u4F53\u540D\u79F0\uFF0C\u56DE\u8F66\u67E5\u770B ego-network\uFF08\u5982: \u7267\u5C18\uFF09"}}),o=t.createEl("button",{text:"\u52A0\u8F7D\u56FE\u8C31"});t.createEl("span",{cls:"info",text:"\u70B9\u51FB\u8282\u70B9\u67E5\u770B\u8BE6\u60C5"});let s=t.createEl("button",{text:"Top \u8282\u70B9"}),c=t.createEl("button",{text:"\u5237\u65B0\u5217\u8868"}),l=e.createEl("div",{cls:"zhiyi-graph-body"}),h=l.createEl("div",{cls:"zhiyi-graph-entity-list"});h.createEl("h4",{text:"\u{1F4CA} Top \u5B9E\u4F53"});let m=h.createEl("div",{cls:"zhiyi-graph-entity-list-body"}),d=l.createEl("div",{cls:"zhiyi-graph-canvas"}),y=d.createEl("div",{cls:"zhiyi-tooltip"}),f=d.createEl("div",{cls:"zhiyi-legend"});f.innerHTML=`
\u9ED8\u8BA4
\u4EBA\u7269
\u9879\u76EE
\u4E8B\u4EF6
- `;let y=e.createEl("div",{cls:"zhiyi-graph-detail",attr:{style:"display:none"}}),g=y.createEl("div",{cls:"zhiyi-graph-detail-title"}),f=y.createEl("div",{cls:"zhiyi-graph-detail-content"});this.d3=await J(),this.svg=this.d3.select(d.createEl("svg")),await this.renderGlobalGraph(),o.onclick=async()=>{n.value.trim()&&await this.renderEgoGraph(n.value.trim(),d,p,y,g,f)},n.onkeydown=async h=>{h.key==="Enter"&&n.value.trim()&&await this.renderEgoGraph(n.value.trim(),d,p,y,g,f)},l.onclick=async()=>{try{let z=(await C(20)).pagerank?.[0]?.entity||"\u7267\u5C18";n.value=z,await this.renderEgoGraph(z,d,p,y,g,f)}catch{}}}async renderGlobalGraph(){try{let e=await N("hermes-main",80);await this.renderD3Graph(e.nodes,e.edges,null)}catch{}}async renderEgoGraph(e,i,n,o,l,d){this.currentEntity=e,this.simulation?.stop();try{let[p,u]=await Promise.all([_(e),$(e)]),g=[{id:e,label:e},...p.nodes.filter(h=>h.id!==e)];await this.renderD3Graph(g,p.edges,e),o.style.display="block",l.setText(`\u{1F4CC} ${e}`);let f=u.memories||[];d.innerHTML=f.slice(0,3).map(h=>`\u2022 ${this.truncate(h.content,100)}
`).join("")||"\u65E0\u5173\u8054\u8BB0\u5FC6
"}catch(p){let u=i.createEl("div",{text:`\u52A0\u8F7D\u5931\u8D25: ${p.message}`,cls:"zhiyi-empty"});setTimeout(()=>u.remove(),3e3)}}async renderD3Graph(e,i,n){if(!this.d3||!this.svg)return;let o=this.d3,l=this.svg;l.selectAll("*").remove();let d=l.node().clientWidth||600,p=l.node().clientHeight||400,u={\u4EBA\u7269:"#f6c945",\u4EBA\u7269_name:"#f6c945",project:"#34c759",\u9879\u76EE:"#34c759",event:"#ff6b6b",\u4E8B\u4EF6:"#ff6b6b",concept:"#a855f7",\u6982\u5FF5:"#a855f7",location:"#06b6d4",distilled:"#4a9eff",default:"#4a9eff"},y=t=>t.id===n?"#ff4d4d":t.category&&u[t.category]?u[t.category]:u[t.weight!==void 0&&t.weight>5?"distilled":"default"],g=e.map(t=>({id:t.id,label:t.label||t.id,category:t.category,weight:t.weight,isCentral:t.id===n})),f=new Map(g.map(t=>[t.id,t])),h=i.filter(t=>f.has(t.source)&&f.has(t.target)).map(t=>({source:t.source,target:t.target,relation:t.relation,weight:t.weight})),z=o.zoom().scaleExtent([.2,5]).on("zoom",t=>{L.attr("transform",t.transform)});l.call(z);let L=l.append("g");l.append("defs").append("marker").attr("id","arrowhead").attr("viewBox","-0 -5 10 10").attr("refX",15).attr("refY",0).attr("orient","auto").attr("markerWidth",6).attr("markerHeight",6).append("path").attr("d","M 0,-5 L 10,0 L 0,5").attr("fill","#666");let q=L.append("g").selectAll("line").data(h).enter().append("line").attr("stroke","#999").attr("stroke-opacity",.6).attr("stroke-width",t=>Math.min(2,t.weight||1)).attr("marker-end","url(#arrowhead)"),v=L.append("g").selectAll("g").data(g).enter().append("g").style("cursor","pointer");v.append("circle").attr("r",t=>t.isCentral?12:6+(t.weight||1)*.5).attr("fill",t=>y(t)).attr("stroke",t=>t.isCentral?"#ff4d4d":"white").attr("stroke-width",t=>t.isCentral?2:1.5),v.append("text").text(t=>t.label.length>12?t.label.slice(0,12)+"\u2026":t.label).attr("dy",-8).attr("text-anchor","middle").attr("class","zhiyi-graph-node-label").style("font-size",t=>t.isCentral?"11px":"9px").style("fill","var(--text-muted)");let B=o.drag().on("start",(t,c)=>{t.active||this.simulation.alphaTarget(.3).restart(),c.fx=c.x,c.fy=c.y}).on("drag",(t,c)=>{c.fx=t.x,c.fy=t.y}).on("end",(t,c)=>{t.active||this.simulation.alphaTarget(0),c.fx=null,c.fy=null});v.call(B),v.on("click",(t,c)=>{this.renderEgoGraph(c.id,canvas.parentElement,tooltip,canvas.parentElement.querySelector(".zhiyi-graph-detail"),canvas.parentElement.querySelector(".zhiyi-graph-detail-title"),canvas.parentElement.querySelector(".zhiyi-graph-detail-content"))}),v.on("mouseover",(t,c)=>{tooltip.style.opacity="1",tooltip.style.left=t.offsetX+10+"px",tooltip.style.top=t.offsetY-10+"px",tooltip.setText(`${c.label}${c.weight!==void 0?` (\xD7${c.weight})`:""}`)}),v.on("mouseout",()=>{tooltip.style.opacity="0"}),this.simulation=o.forceSimulation(g).force("link",o.forceLink(h).id(t=>t.id).distance(80)).force("charge",o.forceManyBody().strength(-200)).force("center",o.forceCenter(d/2,p/2)).force("collision",o.forceCollide(20)).on("tick",()=>{q.attr("x1",t=>t.source.x).attr("y1",t=>t.source.y).attr("x2",t=>t.target.x).attr("y2",t=>t.target.y),v.attr("transform",t=>`translate(${t.x},${t.y})`)})}truncate(e,i){return e?e.length>i?e.slice(0,i)+"\u2026":e:""}};var A=require("obsidian");var T=class extends A.Modal{constructor(e){super(e);s(this,"results",[]);s(this,"inputEl");s(this,"listEl");s(this,"loading",!1);s(this,"debounceTimer",null);s(this,"onSelect")}onOpen(){this.contentEl.createEl("style",{text:`
+ `;let w=e.createEl("div",{cls:"zhiyi-graph-detail",attr:{style:"display:none"}}),b=w.createEl("div",{cls:"zhiyi-graph-detail-title"}),P=w.createEl("div",{cls:"zhiyi-graph-detail-content"});this.d3=await me(),this.svg=this.d3.select(d.createEl("svg")),await this.renderGlobalGraph(),await this.loadEntityList(m);let E=async g=>{r.value=g,await this.renderEgoGraph(g,d,y,w,b,P),m.querySelectorAll(".zhiyi-entity-item").forEach(k=>{k.classList.toggle("active",k.dataset.entity===g)})};o.onclick=async()=>{r.value.trim()&&await E(r.value.trim())},r.onkeydown=async g=>{g.key==="Enter"&&r.value.trim()&&await E(r.value.trim())},s.onclick=async()=>{try{let k=(await R(20)).pagerank?.[0]?.entity||"\u7267\u5C18";await E(k)}catch{}},c.onclick=async()=>{await this.loadEntityList(m)}}async loadEntityList(e){try{let t=await R(30);if(this.topEntities=t.pagerank||[],e.empty(),this.topEntities.length===0){e.createEl("div",{text:"\u6682\u65E0\u6570\u636E",attrs:{style:"padding:8px;font-size:11px;color:var(--text-muted)"}});return}for(let r of this.topEntities){let o=e.createEl("div",{cls:"zhiyi-entity-item",attr:{"data-entity":r.entity}});o.createEl("span",{text:r.entity.length>14?r.entity.slice(0,14)+"\u2026":r.entity}),o.createEl("span",{cls:"zhiyi-entity-score",text:r.score.toFixed(2)}),o.onclick=async()=>{let c=this.container.querySelector(".zhiyi-graph-toolbar")?.querySelector("input");c&&(c.value=r.entity,await this.renderEgoGraph(r.entity,null,null,this.container.querySelector(".zhiyi-graph-detail"),this.container.querySelector(".zhiyi-graph-detail-title"),this.container.querySelector(".zhiyi-graph-detail-content"))),e.querySelectorAll(".zhiyi-entity-item").forEach(l=>l.classList.remove("active")),o.classList.add("active")}}}catch{e.empty(),e.createEl("div",{text:"\u52A0\u8F7D\u5931\u8D25",attrs:{style:"padding:8px;font-size:11px;color:var(--text-muted)"}})}}async renderGlobalGraph(){try{let e=v(),t=await D(e,80);await this.renderD3Graph(t.nodes,t.edges,null)}catch{}}async renderEgoGraph(e,t,r,o,s,c){this.currentEntity=e,this.simulation?.stop();try{let[l,h]=await Promise.all([$(e),W(e)]),d=[{id:e,label:e},...l.nodes.filter(f=>f.id!==e)];await this.renderD3Graph(d,l.edges,e),o.style.display="block",s.setText(`\u{1F4CC} ${e}`);let y=(h.memories||[]).slice(0,5);c.innerHTML=y.length>0?y.map(f=>`\u2022 ${this.truncate(f.content,80)}
`).join(""):"\u65E0\u5173\u8054\u8BB0\u5FC6
"}catch(l){let h=t.createEl("div",{text:`\u52A0\u8F7D\u5931\u8D25: ${l.message}`,cls:"zhiyi-empty"});setTimeout(()=>h.remove(),3e3)}}async renderD3Graph(e,t,r){if(!this.d3||!this.svg)return;let o=this.d3,s=this.svg;s.selectAll("*").remove();let c=s.node().clientWidth||600,l=s.node().clientHeight||400,h={\u4EBA\u7269:"#f6c945",\u4EBA\u7269_name:"#f6c945",project:"#34c759",\u9879\u76EE:"#34c759",event:"#ff6b6b",\u4E8B\u4EF6:"#ff6b6b",concept:"#a855f7",\u6982\u5FF5:"#a855f7",location:"#06b6d4",distilled:"#4a9eff",default:"#4a9eff"},m=i=>i.id===r?"#ff4d4d":i.category&&h[i.category]?h[i.category]:h[i.weight!==void 0&&i.weight>5?"distilled":"default"],d=e.map(i=>({id:i.id,label:i.label||i.id,category:i.category,weight:i.weight,isCentral:i.id===r})),y=new Map(d.map(i=>[i.id,i])),f=t.filter(i=>y.has(i.source)&&y.has(i.target)).map(i=>({source:i.source,target:i.target,relation:i.relation,weight:i.weight})),w=o.zoom().scaleExtent([.2,5]).on("zoom",i=>{b.attr("transform",i.transform)});s.call(w);let b=s.append("g");s.append("defs").append("marker").attr("id","arrowhead").attr("viewBox","-0 -5 10 10").attr("refX",15).attr("refY",0).attr("orient","auto").attr("markerWidth",6).attr("markerHeight",6).append("path").attr("d","M 0,-5 L 10,0 L 0,5").attr("fill","#666");let P=b.append("g").selectAll("line").data(f).enter().append("line").attr("stroke","#999").attr("stroke-opacity",.6).attr("stroke-width",i=>Math.min(2,i.weight||1)).attr("marker-end","url(#arrowhead)"),E=b.append("g").selectAll("text").data(f).enter().append("text").text(i=>i.relation||"").attr("text-anchor","middle").attr("dy",-3).attr("font-size","8px").attr("fill","#999").attr("pointer-events","none").style("display",i=>i.relation?"block":"none"),g=b.append("g").selectAll("g").data(d).enter().append("g").style("cursor","pointer");g.append("circle").attr("r",i=>i.isCentral?12:6+(i.weight||1)*.5).attr("fill",i=>m(i)).attr("stroke",i=>i.isCentral?"#ff4d4d":"white").attr("stroke-width",i=>i.isCentral?2:1.5),g.append("text").text(i=>i.label.length>12?i.label.slice(0,12)+"\u2026":i.label).attr("dy",-8).attr("text-anchor","middle").attr("class","zhiyi-graph-node-label").style("font-size",i=>i.isCentral?"11px":"9px").style("fill","var(--text-muted)");let k=o.drag().on("start",(i,u)=>{i.active||this.simulation.alphaTarget(.3).restart(),u.fx=u.x,u.fy=u.y}).on("drag",(i,u)=>{u.fx=i.x,u.fy=i.y}).on("end",(i,u)=>{i.active||this.simulation.alphaTarget(0),u.fx=null,u.fy=null});g.call(k),g.on("click",(i,u)=>{if(u.id===this.currentEntity)return;let F=this.container.querySelector(".zhiyi-graph-toolbar")?.querySelector("input");F&&(F.value=u.id,this.renderEgoGraph(u.id,this.container.querySelector(".zhiyi-graph-canvas"),this.container.querySelector(".zhiyi-tooltip"),this.container.querySelector(".zhiyi-graph-detail"),this.container.querySelector(".zhiyi-graph-detail-title"),this.container.querySelector(".zhiyi-graph-detail-content"))),this.container.querySelector(".zhiyi-graph-entity-list-body")?.querySelectorAll(".zhiyi-entity-item").forEach(Q=>{Q.classList.toggle("active",Q.dataset.entity===u.id)})}),g.on("mouseover",(i,u)=>{tooltip.style.opacity="1",tooltip.style.left=i.offsetX+10+"px",tooltip.style.top=i.offsetY-10+"px",tooltip.setText(`${u.label}${u.weight!==void 0?` (\xD7${u.weight})`:""}`)}),g.on("mouseout",()=>{tooltip.style.opacity="0"}),this.simulation=o.forceSimulation(d).force("link",o.forceLink(f).id(i=>i.id).distance(80)).force("charge",o.forceManyBody().strength(-200)).force("center",o.forceCenter(c/2,l/2)).force("collision",o.forceCollide(20)).on("tick",()=>{P.attr("x1",i=>i.source.x).attr("y1",i=>i.source.y).attr("x2",i=>i.target.x).attr("y2",i=>i.target.y),E.attr("x",i=>(i.source.x+i.target.x)/2).attr("y",i=>(i.source.y+i.target.y)/2),g.attr("transform",i=>`translate(${i.x},${i.y})`)})}truncate(e,t){return e?e.length>t?e.slice(0,t)+"\u2026":e:""}};var ae=require("obsidian");T();var q=class extends ae.Modal{constructor(e){super(e);p(this,"results",[]);p(this,"inputEl");p(this,"listEl");p(this,"loading",!1);p(this,"debounceTimer",null);p(this,"onSelect")}onOpen(){this.contentEl.createEl("style",{text:`
.zhiyi-search-modal { width: 600px; max-height: 80vh; display: flex; flex-direction: column; }
.zhiyi-search-header {
display: flex; align-items: center; gap: 8px;
@@ -99,4 +110,4 @@ var E=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var W=Object.g
.zhiyi-search-loading { padding: 20px; text-align: center; color: var(--text-muted); }
.zhiyi-search-empty { padding: 20px; text-align: center; color: var(--text-muted); }
.zhiyi-search-hint { padding: 8px 16px; font-size: 11px; color: var(--text-muted); text-align: center; }
- `});let e=this.contentEl.createEl("div",{cls:"zhiyi-search-modal"}),i=e.createEl("div",{cls:"zhiyi-search-header"});i.createEl("h2",{text:"\u{1F50D} \u7EC7\u5FC6\u8BED\u4E49\u641C\u7D22"}),this.inputEl=i.createEl("input",{cls:"zhiyi-search-input",attr:{placeholder:"\u8F93\u5165\u5173\u952E\u8BCD\uFF0C\u8BED\u4E49\u641C\u7D22\u8BB0\u5FC6\u2026",autofocus:"true"}}),this.listEl=e.createEl("div",{cls:"zhiyi-search-results"}),e.createEl("div",{cls:"zhiyi-search-hint",text:"\u5B9E\u65F6\u641C\u7D22 \xB7 \u6309 Enter \u9009\u62E9\u7B2C\u4E00\u6761\u7ED3\u679C"}),this.inputEl.oninput=()=>{this.debounceTimer!==null&&window.clearTimeout(this.debounceTimer),this.debounceTimer=window.setTimeout(()=>this.doSearch(),300)},this.inputEl.onkeydown=n=>{n.key==="Enter"&&this.results.length>0&&this.selectResult(this.results[0]),n.key==="Escape"&&this.close()}}async doSearch(){let e=this.inputEl.value.trim();if(!e){this.results=[],this.listEl.empty();return}this.listEl.innerHTML='\u641C\u7D22\u4E2D\u2026
',this.loading=!0;try{let i=await R(e,"hermes-main");this.results=i.memories||[],this.renderResults()}catch(i){this.listEl.innerHTML=`\u641C\u7D22\u5931\u8D25: ${i.message}
`}}renderResults(){if(this.listEl.empty(),this.results.length===0){this.listEl.createEl("div",{cls:"zhiyi-search-empty",text:"\u672A\u627E\u5230\u7ED3\u679C"});return}for(let e of this.results){let i=this.listEl.createEl("div",{cls:"zhiyi-search-result"}),n=i.createEl("div",{cls:"zhiyi-search-result-header"});n.createEl("span",{cls:"zhiyi-search-cat",text:e.category||"normal"}),n.createEl("span",{cls:"zhiyi-search-score",text:` relevance: ${(e.score??0).toFixed(3)}`}),i.createEl("div",{cls:"zhiyi-search-content",text:this.truncate(e.content,200)}),i.createEl("div",{cls:"zhiyi-search-id",text:e.id}),i.onclick=()=>this.selectResult(e)}}selectResult(e){this.onSelect?.(e.id,e.content),this.close()}truncate(e,i){return e?e.length>i?e.slice(0,i)+"\u2026":e:""}onClose(){this.listEl.empty()}};var M=class extends m.Plugin{constructor(){super(...arguments);s(this,"ribbonIcon",null)}async onload(){this.registerView(b,e=>new k(e)),this.registerView(w,e=>new S(e)),this.addCommand({id:"zhiyi-open-memory-view",name:"\u6253\u5F00\u7EC7\u5FC6\u8BB0\u5FC6\u9762\u677F",callback:()=>this.openMemoryView()}),this.addCommand({id:"zhiyi-open-graph-view",name:"\u6253\u5F00\u7EC7\u5FC6\u56FE\u8C31\u9762\u677F",callback:()=>this.openGraphView()}),this.addCommand({id:"zhiyi-search-memories",name:"\u641C\u7D22\u7EC7\u5FC6\u8BB0\u5FC6",callback:()=>this.openSearchModal()}),this.addStatusBarItem().setText("\u{1F9E0} \u7EC7\u5FC6"),this.addStatusBarItem().setText("Ctrl+P \u2192 \u7EC7\u5FC6"),this.addSettingTab(new G(this.app,this))}onunload(){this.app.workspace.getLeavesOfType(b).forEach(e=>e.detach()),this.app.workspace.getLeavesOfType(w).forEach(e=>e.detach())}async openMemoryView(){let e=this.app.workspace.getLeaf("right");await e.setViewState({type:b,active:!0}),this.app.workspace.revealLeaf(e)}async openGraphView(){let e=this.app.workspace.getLeaf("right");await e.setViewState({type:w,active:!0}),this.app.workspace.revealLeaf(e)}openSearchModal(){let e=new T(this.app);e.onSelect=(i,n)=>{navigator.clipboard.writeText(n).catch(()=>{}),new this.app.notify(`\u5DF2\u590D\u5236: ${n.slice(0,80)}\u2026`)},e.open()}},G=class extends m.PluginSettingTab{constructor(r,e){super(r,e)}display(){let{containerEl:r}=this;r.empty(),r.createEl("h2",{text:"\u{1F9E0} \u7EC7\u5FC6\u8BBE\u7F6E"}),new m.Setting(r).setName("API \u5730\u5740").setDesc("\u7EC7\u5FC6 Go API \u670D\u52A1\u5730\u5740\uFF08\u9ED8\u8BA4 http://localhost:7821\uFF09").addText(e=>e.setPlaceholder("http://localhost:7821").setValue("http://localhost:7821").onChange(i=>{window.zhiyi_api_url=i})),new m.Setting(r).setName("API Key").setDesc("\u7EC7\u5FC6\u8BA4\u8BC1\u5BC6\u94A5").addText(e=>e.setPlaceholder("zhiyi-dev-key-2026").onChange(i=>{window.zhiyi_api_key=i})),new m.Setting(r).setName("\u9ED8\u8BA4 Namespace").setDesc("\u641C\u7D22\u548C\u67E5\u8BE2\u4F7F\u7528\u7684\u9ED8\u8BA4\u547D\u540D\u7A7A\u95F4").addText(e=>e.setValue("hermes-main").onChange(i=>{window.zhiyi_namespace=i})),new m.Setting(r).setName("\u6253\u5F00\u8BB0\u5FC6\u9762\u677F").addButton(e=>e.setButtonText("\u6253\u5F00 \u{1F9E0}").onClick(()=>{this.plugin.openMemoryView()})),new m.Setting(r).setName("\u6253\u5F00\u56FE\u8C31\u9762\u677F").addButton(e=>e.setButtonText("\u6253\u5F00 \u{1F4D0}").onClick(()=>{this.plugin.openGraphView()}))}};
+ `});let e=this.contentEl.createEl("div",{cls:"zhiyi-search-modal"}),t=e.createEl("div",{cls:"zhiyi-search-header"});t.createEl("h2",{text:"\u{1F50D} \u7EC7\u5FC6\u8BED\u4E49\u641C\u7D22"}),this.inputEl=t.createEl("input",{cls:"zhiyi-search-input",attr:{placeholder:"\u8F93\u5165\u5173\u952E\u8BCD\uFF0C\u8BED\u4E49\u641C\u7D22\u8BB0\u5FC6\u2026",autofocus:"true"}}),this.listEl=e.createEl("div",{cls:"zhiyi-search-results"}),e.createEl("div",{cls:"zhiyi-search-hint",text:"\u5B9E\u65F6\u641C\u7D22 \xB7 \u6309 Enter \u9009\u62E9\u7B2C\u4E00\u6761\u7ED3\u679C"}),this.inputEl.oninput=()=>{this.debounceTimer!==null&&window.clearTimeout(this.debounceTimer),this.debounceTimer=window.setTimeout(()=>this.doSearch(),300)},this.inputEl.onkeydown=r=>{r.key==="Enter"&&this.results.length>0&&this.selectResult(this.results[0]),r.key==="Escape"&&this.close()}}async doSearch(){let e=this.inputEl.value.trim();if(!e){this.results=[],this.listEl.empty();return}this.listEl.innerHTML='\u641C\u7D22\u4E2D\u2026
',this.loading=!0;try{let t=await A(e,"hermes-main");this.results=t.memories||[],this.renderResults()}catch(t){this.listEl.innerHTML=`\u641C\u7D22\u5931\u8D25: ${t.message}
`}}renderResults(){if(this.listEl.empty(),this.results.length===0){this.listEl.createEl("div",{cls:"zhiyi-search-empty",text:"\u672A\u627E\u5230\u7ED3\u679C"});return}for(let e of this.results){let t=this.listEl.createEl("div",{cls:"zhiyi-search-result"}),r=t.createEl("div",{cls:"zhiyi-search-result-header"});r.createEl("span",{cls:"zhiyi-search-cat",text:e.category||"normal"}),r.createEl("span",{cls:"zhiyi-search-score",text:` relevance: ${(e.score??0).toFixed(3)}`}),t.createEl("div",{cls:"zhiyi-search-content",text:this.truncate(e.content,200)}),t.createEl("div",{cls:"zhiyi-search-id",text:e.id}),t.onclick=()=>this.selectResult(e)}}selectResult(e){this.onSelect?.(e.id,e.content),this.close()}truncate(e,t){return e?e.length>t?e.slice(0,t)+"\u2026":e:""}onClose(){this.listEl.empty()}};T();var ne="zhiyi-settings.json",U={apiUrl:"http://localhost:7821",apiKey:"zhiyi-dev-key-2026",namespace:"hermes-main"},I=class extends x.Plugin{constructor(){super(...arguments);p(this,"settings",{...U});p(this,"ribbonIcon",null)}async onload(){await this.loadSettings(),this.registerView(M,e=>new N(e)),this.registerView(L,e=>new _(e)),this.addCommand({id:"zhiyi-open-memory-view",name:"\u6253\u5F00\u7EC7\u5FC6\u8BB0\u5FC6\u9762\u677F",callback:()=>this.openMemoryView()}),this.addCommand({id:"zhiyi-open-graph-view",name:"\u6253\u5F00\u7EC7\u5FC6\u56FE\u8C31\u9762\u677F",callback:()=>this.openGraphView()}),this.addCommand({id:"zhiyi-search-memories",name:"\u641C\u7D22\u7EC7\u5FC6\u8BB0\u5FC6",callback:()=>this.openSearchModal()}),this.addCommand({id:"zhiyi-write-memory",name:"\u5C06\u9009\u4E2D\u5185\u5BB9\u5199\u5165\u7EC7\u5FC6",editorCallback:e=>{let t=e.getSelection();t&&this.openWriteModal(t)}}),this.ribbonIcon=this.addStatusBarItem(),this.ribbonIcon.setIcon("brain"),this.ribbonIcon.setTooltip("\u7EC7\u5FC6\u8BB0\u5FC6"),this.ribbonIcon.onClick(()=>this.openMemoryView()),this.addStatusBarItem().setText("\u{1F9E0} \u7EC7\u5FC6 v1.1"),this.addSettingTab(new Y(this.app,this))}onunload(){this.app.workspace.getLeavesOfType(M).forEach(e=>e.detach()),this.app.workspace.getLeavesOfType(L).forEach(e=>e.detach())}async loadSettings(){try{let e=await this.loadData(ne);e&&(this.settings={...U,...e})}catch{this.settings={...U}}C(this.settings.apiUrl,this.settings.apiKey,this.settings.namespace)}async saveSettings(){await this.saveData(this.settings,ne),C(this.settings.apiUrl,this.settings.apiKey,this.settings.namespace)}getSettings(){return this.settings}async openMemoryView(){let e=this.app.workspace.getLeaf("right");await e.setViewState({type:M,active:!0}),this.app.workspace.revealLeaf(e)}async openGraphView(){let e=this.app.workspace.getLeaf("right");await e.setViewState({type:L,active:!0}),this.app.workspace.revealLeaf(e)}openSearchModal(){let e=new q(this.app);e.onSelect=(t,r)=>{navigator.clipboard.writeText(r).catch(()=>{}),this.app.notify(`\u5DF2\u590D\u5236: ${r.slice(0,80)}\u2026`)},e.open()}openWriteModal(e=""){let{writeMemory:t}=(T(),Z(te)),r=e||window.prompt("\u8F93\u5165\u8BB0\u5FC6\u5185\u5BB9\uFF1A");if(!r?.trim())return;let o=window.prompt("\u5206\u7C7B\uFF08\u53EF\u9009\uFF0C\u5982 normal/user_pref/project\uFF09\uFF1A","normal")||"normal";t(r.trim(),o,this.settings.namespace).then(()=>{this.app.notify("\u2705 \u8BB0\u5FC6\u5DF2\u5199\u5165\u7EC7\u5FC6")}).catch(s=>{this.app.notify(`\u274C \u5199\u5165\u5931\u8D25: ${s.message}`)})}},Y=class extends x.PluginSettingTab{constructor(e,t){super(e,t);p(this,"plugin",t)}display(){let{containerEl:e}=this;e.empty(),e.createEl("h2",{text:"\u{1F9E0} \u7EC7\u5FC6\u8BBE\u7F6E"}),new x.Setting(e).setName("API \u5730\u5740").setDesc("\u7EC7\u5FC6 Go API \u670D\u52A1\u5730\u5740\uFF08\u9ED8\u8BA4 http://localhost:7821\uFF09").addText(t=>{t.setPlaceholder("http://localhost:7821").setValue(this.plugin.getSettings().apiUrl).onChange(async r=>{this.plugin.getSettings().apiUrl=r,await this.plugin.saveSettings()})}),new x.Setting(e).setName("API Key").setDesc("\u7EC7\u5FC6\u8BA4\u8BC1\u5BC6\u94A5").addText(t=>{t.setPlaceholder("zhiyi-dev-key-2026").setValue(this.plugin.getSettings().apiKey).onChange(async r=>{this.plugin.getSettings().apiKey=r,await this.plugin.saveSettings()})}),new x.Setting(e).setName("\u9ED8\u8BA4 Namespace").setDesc("\u641C\u7D22\u548C\u67E5\u8BE2\u4F7F\u7528\u7684\u9ED8\u8BA4\u547D\u540D\u7A7A\u95F4").addText(t=>{t.setValue(this.plugin.getSettings().namespace).onChange(async r=>{this.plugin.getSettings().namespace=r,await this.plugin.saveSettings()})}),new x.Setting(e).setName("\u8BB0\u5FC6\u9762\u677F").addButton(t=>t.setButtonText("\u6253\u5F00 \u{1F9E0}").onClick(()=>{this.plugin.openMemoryView()})),new x.Setting(e).setName("\u56FE\u8C31\u9762\u677F").addButton(t=>t.setButtonText("\u6253\u5F00 \u{1F4D0}").onClick(()=>{this.plugin.openGraphView()})),new x.Setting(e).setName("\u8BF4\u660E").setDesc("Ctrl+P \u2192 \u7EC7\u5FC6 \u53EF\u5FEB\u901F\u641C\u7D22\u8BB0\u5FC6\u3002\u9009\u4E2D\u6587\u5B57\u540E\u53F3\u952E\u6216\u547D\u4EE4\u9762\u677F\u9009\u62E9\u300C\u5C06\u9009\u4E2D\u5185\u5BB9\u5199\u5165\u7EC7\u5FC6\u300D\u53EF\u4FDD\u5B58\u7B14\u8BB0\u3002")}};
diff --git a/plugins/obsidian/main.ts b/plugins/obsidian/main.ts
index 7014ad6..95e2184 100644
--- a/plugins/obsidian/main.ts
+++ b/plugins/obsidian/main.ts
@@ -1,52 +1,108 @@
-import { App, Plugin, PluginSettingTab, Setting, addIcon } from "obsidian";
+import { App, Plugin, PluginSettingTab, Setting, moment } from "obsidian";
import { MemoryView, MEMORY_VIEW_TYPE } from "./src/MemoryView";
import { GraphView, GRAPH_VIEW_TYPE } from "./src/GraphView";
import { SearchModal } from "./src/SearchModal";
+import { setRuntimeConfig, getApiBase, getApiKey, getRuntimeNamespace, writeMemory } from "./src/api";
+
+const SETTINGS_FILE = "zhiyi-settings.json";
+
+interface ZhiYiSettings {
+ apiUrl: string;
+ apiKey: string;
+ namespace: string;
+}
+
+const DEFAULT_SETTINGS: ZhiYiSettings = {
+ apiUrl: "http://localhost:7821",
+ apiKey: "zhiyi-dev-key-2026",
+ namespace: "hermes-main",
+};
export default class ZhiYiPlugin extends Plugin {
+ private settings: ZhiYiSettings = { ...DEFAULT_SETTINGS };
private ribbonIcon: HTMLElement | null = null;
async onload() {
- // ─── 注册视图 ─────────────────────────────────────────────────────
+ // 加载持久化设置
+ await this.loadSettings();
+
+ // 注册视图
this.registerView(MEMORY_VIEW_TYPE, (leaf) => new MemoryView(leaf));
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf));
- // ─── 侧边栏命令 ───────────────────────────────────────────────────
- // 记忆面板
+ // ─── 命令 ─────────────────────────────────────────────────────
this.addCommand({
id: "zhiyi-open-memory-view",
name: "打开织忆记忆面板",
callback: () => this.openMemoryView(),
});
- // 图谱面板
this.addCommand({
id: "zhiyi-open-graph-view",
name: "打开织忆图谱面板",
callback: () => this.openGraphView(),
});
- // 搜索模态框
this.addCommand({
id: "zhiyi-search-memories",
name: "搜索织忆记忆",
callback: () => this.openSearchModal(),
});
- // ─── 状态栏 ────────────────────────────────────────────────────────
- this.addStatusBarItem().setText("🧠 织忆");
- this.addStatusBarItem().setText("Ctrl+P → 织忆");
+ // 写记忆(选中文本)
+ this.addCommand({
+ id: "zhiyi-write-memory",
+ name: "将选中内容写入织忆",
+ editorCallback: (editor) => {
+ const selection = editor.getSelection();
+ if (selection) {
+ this.openWriteModal(selection);
+ }
+ },
+ });
- // ─── 设置页 ────────────────────────────────────────────────────────
+ // Ribbon 图标
+ this.ribbonIcon = this.addStatusBarItem();
+ this.ribbonIcon.setIcon("brain");
+ this.ribbonIcon.setTooltip("织忆记忆");
+ this.ribbonIcon.onClick(() => this.openMemoryView());
+
+ // 状态栏
+ this.addStatusBarItem().setText("🧠 织忆 v1.1");
+
+ // 设置页
this.addSettingTab(new ZhiYiSettingTab(this.app, this));
}
onunload() {
- // 关闭所有视图
this.app.workspace.getLeavesOfType(MEMORY_VIEW_TYPE).forEach((leaf) => leaf.detach());
this.app.workspace.getLeavesOfType(GRAPH_VIEW_TYPE).forEach((leaf) => leaf.detach());
}
+ // ─── 设置读写 ───────────────────────────────────────────────────
+
+ async loadSettings() {
+ try {
+ const data = await this.loadData(SETTINGS_FILE);
+ if (data) {
+ this.settings = { ...DEFAULT_SETTINGS, ...data };
+ }
+ } catch {
+ this.settings = { ...DEFAULT_SETTINGS };
+ }
+ // 同步到 api 运行时配置
+ setRuntimeConfig(this.settings.apiUrl, this.settings.apiKey, this.settings.namespace);
+ }
+
+ async saveSettings() {
+ await this.saveData(this.settings, SETTINGS_FILE);
+ setRuntimeConfig(this.settings.apiUrl, this.settings.apiKey, this.settings.namespace);
+ }
+
+ getSettings() { return this.settings; }
+
+ // ─── 视图操作 ────────────────────────────────────────────────────
+
async openMemoryView() {
const leaf = this.app.workspace.getLeaf("right");
await leaf.setViewState({ type: MEMORY_VIEW_TYPE, active: true });
@@ -62,20 +118,37 @@ export default class ZhiYiPlugin extends Plugin {
openSearchModal() {
const modal = new SearchModal(this.app);
modal.onSelect = (id, content) => {
- // 选好后复制到剪贴板并在通知中显示
navigator.clipboard.writeText(content).catch(() => {});
- new (this.app as unknown as { notify: (m: string) => void }).notify(
+ (this.app as unknown as { notify: (m: string) => void }).notify(
`已复制: ${content.slice(0, 80)}…`
);
};
modal.open();
}
+
+ // 写入记忆模态框
+ private openWriteModal(initialContent = "") {
+ const { writeMemory } = require("./src/api");
+ // 使用 Obsidian 的 PromptModal 或简易 confirm
+ const content = initialContent || window.prompt("输入记忆内容:");
+ if (!content?.trim()) return;
+
+ const category = window.prompt("分类(可选,如 normal/user_pref/project):", "normal") || "normal";
+
+ writeMemory(content.trim(), category, this.settings.namespace)
+ .then(() => {
+ (this.app as unknown as { notify: (m: string) => void }).notify("✅ 记忆已写入织忆");
+ })
+ .catch((e: Error) => {
+ (this.app as unknown as { notify: (m: string) => void }).notify(`❌ 写入失败: ${e.message}`);
+ });
+ }
}
// ─── 设置页 ────────────────────────────────────────────────────────────────
class ZhiYiSettingTab extends PluginSettingTab {
- constructor(app: App, plugin: ZhiYiPlugin) {
+ constructor(app: App, private plugin: ZhiYiPlugin) {
super(app, plugin);
}
@@ -87,36 +160,40 @@ class ZhiYiSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("API 地址")
.setDesc("织忆 Go API 服务地址(默认 http://localhost:7821)")
- .addText((text) =>
- text
- .setPlaceholder("http://localhost:7821")
- .setValue("http://localhost:7821")
- .onChange((val) => {
- // API 地址暂存(后续在 api.ts 中使用)
- (window as unknown as Record)["zhiyi_api_url"] = val;
- })
- );
+ .addText((text) => {
+ text.setPlaceholder("http://localhost:7821")
+ .setValue(this.plugin.getSettings().apiUrl)
+ .onChange(async (val) => {
+ this.plugin.getSettings().apiUrl = val;
+ await this.plugin.saveSettings();
+ });
+ });
new Setting(containerEl)
.setName("API Key")
.setDesc("织忆认证密钥")
- .addText((text) =>
- text.setPlaceholder("zhiyi-dev-key-2026").onChange((val) => {
- (window as unknown as Record)["zhiyi_api_key"] = val;
- })
- );
+ .addText((text) => {
+ text.setPlaceholder("zhiyi-dev-key-2026")
+ .setValue(this.plugin.getSettings().apiKey)
+ .onChange(async (val) => {
+ this.plugin.getSettings().apiKey = val;
+ await this.plugin.saveSettings();
+ });
+ });
new Setting(containerEl)
.setName("默认 Namespace")
.setDesc("搜索和查询使用的默认命名空间")
- .addText((text) =>
- text.setValue("hermes-main").onChange((val) => {
- (window as unknown as Record)["zhiyi_namespace"] = val;
- })
- );
+ .addText((text) => {
+ text.setValue(this.plugin.getSettings().namespace)
+ .onChange(async (val) => {
+ this.plugin.getSettings().namespace = val;
+ await this.plugin.saveSettings();
+ });
+ });
new Setting(containerEl)
- .setName("打开记忆面板")
+ .setName("记忆面板")
.addButton((btn) =>
btn.setButtonText("打开 🧠").onClick(() => {
(this.plugin as unknown as { openMemoryView: () => void }).openMemoryView();
@@ -124,11 +201,15 @@ class ZhiYiSettingTab extends PluginSettingTab {
);
new Setting(containerEl)
- .setName("打开图谱面板")
+ .setName("图谱面板")
.addButton((btn) =>
btn.setButtonText("打开 📐").onClick(() => {
(this.plugin as unknown as { openGraphView: () => void }).openGraphView();
})
);
+
+ new Setting(containerEl)
+ .setName("说明")
+ .setDesc("Ctrl+P → 织忆 可快速搜索记忆。选中文字后右键或命令面板选择「将选中内容写入织忆」可保存笔记。");
}
}
\ No newline at end of file
diff --git a/plugins/obsidian/manifest.json b/plugins/obsidian/manifest.json
index 5f60a69..b89069e 100644
--- a/plugins/obsidian/manifest.json
+++ b/plugins/obsidian/manifest.json
@@ -1,9 +1,9 @@
{
"id": "zhiyi-memory",
"name": "织忆",
- "version": "0.1.0",
+ "version": "1.1.0",
"minAppVersion": "0.15.0",
- "description": "织忆记忆系统可视化 — 图谱探索、记忆搜索、蒸馏状态监控",
+ "description": "织忆记忆系统可视化 — 图谱探索、记忆搜索、蒸馏状态监控、语义搜索、分类过滤、记忆写入",
"author": "小唯 A06",
"fundingurl": "",
"isDesktopOnly": false,
diff --git a/plugins/obsidian/src/GraphView.ts b/plugins/obsidian/src/GraphView.ts
index 8476d6c..d13e74f 100644
--- a/plugins/obsidian/src/GraphView.ts
+++ b/plugins/obsidian/src/GraphView.ts
@@ -1,5 +1,14 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
-import { fetchGraphExport, fetchNeighbors, fetchByEntity, fetchPageRank, type GraphNode, type GraphEdge, type MemoryRecord } from "./api";
+import {
+ fetchGraphExport,
+ fetchNeighbors,
+ fetchByEntity,
+ fetchPageRank,
+ getRuntimeNamespace,
+ type GraphNode,
+ type GraphEdge,
+ type MemoryRecord,
+} from "./api";
// 动态加载 D3.js(从 CDN,不打包)
async function loadD3(): Promise {
@@ -31,15 +40,14 @@ interface SimLink extends d3.SimulationLinkDatum {
}
export class GraphView extends ItemView {
- private container: HTMLElement;
private currentEntity = "";
private d3: typeof import("d3") | null = null;
private svg: d3.Selection | null = null;
private simulation: d3.Simulation | null = null;
+ private topEntities: Array<{ entity: string; score: number }> = [];
constructor(leaf: WorkspaceLeaf) {
super(leaf);
- this.container = this.contentEl;
}
getViewType() { return GRAPH_VIEW_TYPE; }
@@ -55,51 +63,29 @@ export class GraphView extends ItemView {
async render() {
this.container.empty();
- this.container.createEl("style", {
- text: `
- .zhiyi-graph-view { height: 100%; display: flex; flex-direction: column; }
- .zhiyi-graph-toolbar {
- display: flex; gap: 6px; padding: 6px 10px;
- border-bottom: 1px solid var(--background-modifier-border);
- align-items: center;
- }
- .zhiyi-graph-toolbar input {
- flex: 1; padding: 4px 8px; font-size: 12px;
- background: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: 4px; color: var(--text-normal);
- }
- .zhiyi-graph-toolbar button {
- padding: 4px 10px; font-size: 12px; cursor: pointer;
- background: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: 4px;
- }
- .zhiyi-graph-toolbar .info {
- font-size: 11px; color: var(--text-muted); padding: 0 6px;
- }
- .zhiyi-graph-canvas { flex: 1; position: relative; overflow: hidden; }
- .zhiyi-graph-canvas svg { width: 100%; height: 100%; }
- .zhiyi-graph-detail {
- position: absolute; bottom: 0; left: 0; right: 0;
- background: var(--background-secondary);
- border-top: 1px solid var(--background-modifier-border);
- padding: 8px 12px; font-size: 11px; max-height: 120px; overflow-y: auto;
- }
- .zhiyi-graph-detail-title { font-weight: 600; margin-bottom: 4px; }
- .zhiyi-graph-node-label { font-size: 10px; fill: var(--text-muted); pointer-events: none; }
- .zhiyi-legend { position: absolute; top: 8px; right: 8px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; font-size: 10px; }
- .zhiyi-legend-item { display: flex; align-items: center; gap: 4px; margin: 2px 0; }
- .zhiyi-legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
- .zhiyi-tooltip {
- position: absolute; background: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: 4px; padding: 6px 8px; font-size: 11px;
- pointer-events: none; opacity: 0; transition: opacity 0.15s;
- max-width: 200px; z-index: 10;
- }
- `
- });
+ this.container.createEl("style", { text: `
+ .zhiyi-graph-view { height: 100%; display: flex; flex-direction: column; }
+ .zhiyi-graph-toolbar { display: flex; gap: 6px; padding: 6px 10px; border-bottom: 1px solid var(--background-modifier-border); align-items: center; }
+ .zhiyi-graph-toolbar input { flex: 1; padding: 4px 8px; font-size: 12px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; color: var(--text-normal); }
+ .zhiyi-graph-toolbar button { padding: 4px 10px; font-size: 12px; cursor: pointer; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; }
+ .zhiyi-graph-toolbar .info { font-size: 11px; color: var(--text-muted); padding: 0 4px; }
+ .zhiyi-graph-body { flex: 1; display: flex; overflow: hidden; }
+ .zhiyi-graph-entity-list { width: 160px; border-right: 1px solid var(--background-modifier-border); overflow-y: auto; padding: 4px 0; flex-shrink: 0; }
+ .zhiyi-graph-entity-list h4 { margin: 0; padding: 4px 8px; font-size: 11px; color: var(--text-muted); font-weight: 600; border-bottom: 1px solid var(--background-modifier-border); }
+ .zhiyi-entity-item { padding: 4px 8px; font-size: 11px; cursor: pointer; color: var(--text-normal); display: flex; justify-content: space-between; align-items: center; }
+ .zhiyi-entity-item:hover { background: var(--background-modifier-hover); }
+ .zhiyi-entity-item.active { color: var(--text-accent); background: var(--background-modifier-hover); }
+ .zhiyi-entity-score { font-size: 9px; color: var(--text-faint); }
+ .zhiyi-graph-canvas { flex: 1; position: relative; overflow: hidden; }
+ .zhiyi-graph-canvas svg { width: 100%; height: 100%; }
+ .zhiyi-graph-detail { position: absolute; bottom: 0; left: 0; right: 0; background: var(--background-secondary); border-top: 1px solid var(--background-modifier-border); padding: 8px 12px; font-size: 11px; max-height: 120px; overflow-y: auto; }
+ .zhiyi-graph-detail-title { font-weight: 600; margin-bottom: 4px; }
+ .zhiyi-graph-node-label { font-size: 10px; fill: var(--text-muted); pointer-events: none; }
+ .zhiyi-legend { position: absolute; top: 8px; right: 8px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; font-size: 10px; }
+ .zhiyi-legend-item { display: flex; align-items: center; gap: 4px; margin: 2px 0; }
+ .zhiyi-legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
+ .zhiyi-tooltip { position: absolute; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 6px 8px; font-size: 11px; pointer-events: none; opacity: 0; transition: opacity 0.15s; max-width: 200px; z-index: 10; }
+ ` });
const root = this.container.createEl("div", { cls: "zhiyi-graph-view" });
const toolbar = root.createEl("div", { cls: "zhiyi-graph-toolbar" });
@@ -110,13 +96,18 @@ export class GraphView extends ItemView {
const loadBtn = toolbar.createEl("button", { text: "加载图谱" });
toolbar.createEl("span", { cls: "info", text: "点击节点查看详情" });
-
- // 快捷入口:Top 实体
const topBtn = toolbar.createEl("button", { text: "Top 节点" });
+ const reloadEntitiesBtn = toolbar.createEl("button", { text: "刷新列表" });
- const canvas = root.createEl("div", { cls: "zhiyi-graph-canvas" });
+ const body = root.createEl("div", { cls: "zhiyi-graph-body" });
+
+ // 实体列表侧边栏
+ const entityList = body.createEl("div", { cls: "zhiyi-graph-entity-list" });
+ entityList.createEl("h4", { text: "📊 Top 实体" });
+ const entityListBody = entityList.createEl("div", { cls: "zhiyi-graph-entity-list-body" });
+
+ const canvas = body.createEl("div", { cls: "zhiyi-graph-canvas" });
const tooltip = canvas.createEl("div", { cls: "zhiyi-tooltip" });
-
const legendEl = canvas.createEl("div", { cls: "zhiyi-legend" });
legendEl.innerHTML = `
默认
@@ -129,36 +120,72 @@ export class GraphView extends ItemView {
const detailTitle = detail.createEl("div", { cls: "zhiyi-graph-detail-title" });
const detailContent = detail.createEl("div", { cls: "zhiyi-graph-detail-content" });
- // 加载 D3 并渲染全局图谱
this.d3 = await loadD3();
this.svg = this.d3.select(canvas.createEl("svg"));
await this.renderGlobalGraph();
+ await this.loadEntityList(entityListBody);
+
+ const renderEgo = async (entity: string) => {
+ searchInput.value = entity;
+ await this.renderEgoGraph(entity, canvas, tooltip, detail, detailTitle, detailContent);
+ // 高亮列表中的活动实体
+ entityListBody.querySelectorAll(".zhiyi-entity-item").forEach(el => {
+ el.classList.toggle("active", (el as HTMLElement).dataset.entity === entity);
+ });
+ };
loadBtn.onclick = async () => {
- if (searchInput.value.trim()) {
- await this.renderEgoGraph(searchInput.value.trim(), canvas, tooltip, detail, detailTitle, detailContent);
- }
+ if (searchInput.value.trim()) await renderEgo(searchInput.value.trim());
};
-
searchInput.onkeydown = async (e) => {
- if (e.key === "Enter" && searchInput.value.trim()) {
- await this.renderEgoGraph(searchInput.value.trim(), canvas, tooltip, detail, detailTitle, detailContent);
- }
+ if (e.key === "Enter" && searchInput.value.trim()) await renderEgo(searchInput.value.trim());
};
-
topBtn.onclick = async () => {
try {
const data = await fetchPageRank(20);
const topEntity = data.pagerank?.[0]?.entity || "牧尘";
- searchInput.value = topEntity;
- await this.renderEgoGraph(topEntity, canvas, tooltip, detail, detailTitle, detailContent);
+ await renderEgo(topEntity);
} catch (_) {}
};
+ reloadEntitiesBtn.onclick = async () => {
+ await this.loadEntityList(entityListBody);
+ };
+ }
+
+ private async loadEntityList(container: HTMLElement) {
+ try {
+ const data = await fetchPageRank(30);
+ this.topEntities = data.pagerank || [];
+ container.empty();
+ if (this.topEntities.length === 0) {
+ container.createEl("div", { text: "暂无数据", attrs: { style: "padding:8px;font-size:11px;color:var(--text-muted)" } });
+ return;
+ }
+ for (const item of this.topEntities) {
+ const row = container.createEl("div", { cls: "zhiyi-entity-item", attr: { "data-entity": item.entity } });
+ row.createEl("span", { text: item.entity.length > 14 ? item.entity.slice(0, 14) + "…" : item.entity });
+ row.createEl("span", { cls: "zhiyi-entity-score", text: item.score.toFixed(2) });
+ row.onclick = async () => {
+ const toolbar = this.container.querySelector(".zhiyi-graph-toolbar") as HTMLElement;
+ const searchInput = toolbar?.querySelector("input") as HTMLInputElement;
+ if (searchInput) {
+ searchInput.value = item.entity;
+ await this.renderEgoGraph(item.entity, null as unknown as HTMLElement, null as unknown as HTMLElement, this.container.querySelector(".zhiyi-graph-detail") as HTMLElement, this.container.querySelector(".zhiyi-graph-detail-title") as HTMLElement, this.container.querySelector(".zhiyi-graph-detail-content") as HTMLElement);
+ }
+ container.querySelectorAll(".zhiyi-entity-item").forEach(el => el.classList.remove("active"));
+ row.classList.add("active");
+ };
+ }
+ } catch {
+ container.empty();
+ container.createEl("div", { text: "加载失败", attrs: { style: "padding:8px;font-size:11px;color:var(--text-muted)" } });
+ }
}
private async renderGlobalGraph() {
try {
- const data = await fetchGraphExport("hermes-main", 80);
+ const ns = getRuntimeNamespace();
+ const data = await fetchGraphExport(ns, 80);
await this.renderD3Graph(data.nodes, data.edges, null);
} catch (_) {}
}
@@ -178,18 +205,16 @@ export class GraphView extends ItemView {
fetchNeighbors(entity),
fetchByEntity(entity),
]);
- // 中心节点
const central: GraphNode = { id: entity, label: entity };
const nodes = [central, ...graphData.nodes.filter(n => n.id !== entity)];
await this.renderD3Graph(nodes, graphData.edges, entity);
- // 详情面板
detail.style.display = "block";
detailTitle.setText(`📌 ${entity}`);
- const mems = memData.memories || [];
- detailContent.innerHTML = mems.slice(0, 3).map((m: MemoryRecord) =>
- `• ${this.truncate(m.content, 100)}
`
- ).join("") || "无关联记忆
";
+ const mems = (memData.memories || []).slice(0, 5);
+ detailContent.innerHTML = mems.length > 0
+ ? mems.map((m: MemoryRecord) => `• ${this.truncate(m.content, 80)}
`).join("")
+ : "无关联记忆
";
} catch (e) {
const errEl = canvas.createEl("div", { text: `加载失败: ${(e as Error).message}`, cls: "zhiyi-empty" });
setTimeout(() => errEl.remove(), 3000);
@@ -251,6 +276,7 @@ export class GraphView extends ItemView {
.append("path").attr("d", "M 0,-5 L 10,0 L 0,5")
.attr("fill", "#666");
+ // 边(带关系标签)
const link = g.append("g").selectAll("line")
.data(simLinks)
.enter().append("line")
@@ -258,6 +284,18 @@ export class GraphView extends ItemView {
.attr("stroke-width", d => Math.min(2, d.weight || 1))
.attr("marker-end", "url(#arrowhead)");
+ // 关系标签(居中显示)
+ const linkLabel = g.append("g").selectAll("text")
+ .data(simLinks)
+ .enter().append("text")
+ .text(d => d.relation || "")
+ .attr("text-anchor", "middle")
+ .attr("dy", -3)
+ .attr("font-size", "8px")
+ .attr("fill", "#999")
+ .attr("pointer-events", "none")
+ .style("display", d => d.relation ? "block" : "none");
+
const node = g.append("g").selectAll("g")
.data(simNodes)
.enter().append("g")
@@ -290,11 +328,24 @@ export class GraphView extends ItemView {
});
node.call(drag as unknown as (s: d3.Selection) => void);
+ // 点击节点 → ego 图
node.on("click", (_event, d) => {
- this.renderEgoGraph(d.id, canvas.parentElement!, tooltip,
- canvas.parentElement!.querySelector(".zhiyi-graph-detail") as HTMLElement,
- canvas.parentElement!.querySelector(".zhiyi-graph-detail-title") as HTMLElement,
- canvas.parentElement!.querySelector(".zhiyi-graph-detail-content") as HTMLElement);
+ if (d.id === this.currentEntity) return;
+ const toolbar = this.container.querySelector(".zhiyi-graph-toolbar") as HTMLElement;
+ const searchInput = toolbar?.querySelector("input") as HTMLInputElement;
+ if (searchInput) {
+ searchInput.value = d.id;
+ this.renderEgoGraph(d.id,
+ this.container.querySelector(".zhiyi-graph-canvas") as HTMLElement,
+ this.container.querySelector(".zhiyi-tooltip") as HTMLElement,
+ this.container.querySelector(".zhiyi-graph-detail") as HTMLElement,
+ this.container.querySelector(".zhiyi-graph-detail-title") as HTMLElement,
+ this.container.querySelector(".zhiyi-graph-detail-content") as HTMLElement);
+ }
+ const entityListBody = this.container.querySelector(".zhiyi-graph-entity-list-body") as HTMLElement;
+ entityListBody?.querySelectorAll(".zhiyi-entity-item").forEach(el => {
+ el.classList.toggle("active", (el as HTMLElement).dataset.entity === d.id);
+ });
});
node.on("mouseover", (event, d) => {
@@ -317,6 +368,9 @@ export class GraphView extends ItemView {
.attr("y1", d => (d.source as SimNode).y!)
.attr("x2", d => (d.target as SimNode).x!)
.attr("y2", d => (d.target as SimNode).y!);
+ linkLabel
+ .attr("x", d => ((d.source as SimNode).x! + (d.target as SimNode).x!) / 2)
+ .attr("y", d => ((d.source as SimNode).y! + (d.target as SimNode).y!) / 2);
node.attr("transform", d => `translate(${d.x},${d.y})`);
});
}
diff --git a/plugins/obsidian/src/MemoryView.ts b/plugins/obsidian/src/MemoryView.ts
index 0d8f734..0bff7aa 100644
--- a/plugins/obsidian/src/MemoryView.ts
+++ b/plugins/obsidian/src/MemoryView.ts
@@ -1,19 +1,29 @@
-import { ItemView, WorkspaceLeaf } from "obsidian";
-import { fetchMemories, fetchDistillStatus, fetchDistillQuota, type MemoryRecord } from "./api";
+import { ItemView, WorkspaceLeaf, Modal, TFile } from "obsidian";
+import {
+ fetchMemories,
+ fetchDistillStatus,
+ fetchDistillQuota,
+ fetchCategories,
+ writeMemory,
+ getRuntimeNamespace,
+ type MemoryRecord,
+} from "./api";
export const MEMORY_VIEW_TYPE = "zhiyi-memory-view";
+const PAGE_SIZE = 30;
+
export class MemoryView extends ItemView {
- private container: HTMLElement;
private memories: MemoryRecord[] = [];
private currentPage = 0;
- private pageSize = 20;
private totalMemories = 0;
private status_interval: number | null = null;
+ private categories: string[] = [];
+ private activeCategory = "";
+ private expandedId: string | null = null;
constructor(leaf: WorkspaceLeaf) {
super(leaf);
- this.container = this.contentEl;
}
getViewType() { return MEMORY_VIEW_TYPE; }
@@ -31,7 +41,6 @@ export class MemoryView extends ItemView {
}
private startStatusPoll() {
- // 轮询蒸馏状态(每 30 秒)
const id = (window as unknown as { setInterval: (fn: () => void, ms: number) => number }).setInterval(
() => this.renderDistillBanner(),
30000
@@ -50,97 +59,279 @@ export class MemoryView extends ItemView {
} else {
banner.style.display = "none";
}
- } catch (_) {
- // ignore
+ } catch (_) {}
+ }
+
+ private async loadCategories() {
+ try {
+ this.categories = await fetchCategories();
+ } catch {
+ this.categories = [];
}
}
async render() {
this.container.empty();
- this.container.createEl("style", {
- text: `
- .zhiyi-memory-view { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
- .zhiyi-header { padding: 10px 12px 6px; border-bottom: 1px solid var(--background-modifier-border); }
- .zhiyi-header h1 { font-size: 14px; font-weight: 600; margin: 0 0 4px; }
- .zhiyi-stats { color: var(--text-muted); font-size: 11px; margin: 0; }
- .zhiyi-distill-banner {
- display: none; background: #c44; color: white; padding: 4px 12px;
- font-size: 12px; font-weight: 600; cursor: pointer;
- }
- .zhiyi-body { flex: 1; overflow-y: auto; }
- .zhiyi-item {
- padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border);
- cursor: pointer;
- }
- .zhiyi-item:hover { background: var(--background-modifier-hover); }
- .zhiyi-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px; }
- .zhiyi-item-cat { font-size: 10px; color: var(--text-muted); background: var(--background-secondary); padding: 1px 4px; border-radius: 2px; }
- .zhiyi-item-score { font-size: 10px; color: var(--text-muted); }
- .zhiyi-item-content { font-size: 12px; color: var(--text-normal); line-height: 1.4; }
- .zhiyi-item-time { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
- .zhiyi-item-id { font-size: 10px; color: var(--text-faint); font-family: monospace; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; }
- .zhiyi-pagination { display: flex; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--background-modifier-border); }
- .zhiyi-pagination button { flex: 1; padding: 4px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; font-size: 11px; }
- .zhiyi-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
- .zhiyi-loading { padding: 20px; text-align: center; color: var(--text-muted); }
- .zhiyi-empty { padding: 20px; text-align: center; color: var(--text-muted); }
- `
- });
- this.container.createEl("div", { cls: "zhiyi-memory-view" }).append(
+ this.container.createEl("style", { text: `
+ .zhiyi-memory-view { height: 100%; display: flex; flex-direction: column; font-size: 13px; }
+ .zhiyi-header { padding: 8px 12px 4px; border-bottom: 1px solid var(--background-modifier-border); }
+ .zhiyi-header-row1 { display: flex; align-items: center; gap: 6px; }
+ .zhiyi-header h1 { font-size: 14px; font-weight: 600; margin: 0; flex: 1; }
+ .zhiyi-stats { color: var(--text-muted); font-size: 11px; margin: 3px 0 0; }
+ .zhiyi-toolbar { display: flex; gap: 4px; padding: 4px 12px; border-bottom: 1px solid var(--background-modifier-border); flex-wrap: wrap; align-items: center; }
+ .zhiyi-toolbar select { background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; color: var(--text-normal); font-size: 11px; padding: 2px 4px; max-width: 120px; }
+ .zhiyi-filter-chip { padding: 2px 8px; border-radius: 10px; font-size: 10px; border: 1px solid var(--background-modifier-border); background: var(--background-secondary); color: var(--text-muted); cursor: pointer; white-space: nowrap; }
+ .zhiyi-filter-chip:hover { background: var(--background-modifier-hover); }
+ .zhiyi-filter-chip.active { background: var(--text-accent); color: var(--bg-on-accent, #fff); border-color: var(--text-accent); }
+ .zhiyi-btn-icon { padding: 2px 6px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; color: var(--text-muted); font-size: 11px; }
+ .zhiyi-btn-icon:hover { background: var(--background-modifier-hover); color: var(--text-normal); }
+ .zhiyi-distill-banner { display: none; background: #c44; color: white; padding: 4px 12px; font-size: 12px; font-weight: 600; cursor: pointer; }
+ .zhiyi-body { flex: 1; overflow-y: auto; }
+ .zhiyi-item { padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border); cursor: pointer; }
+ .zhiyi-item:hover { background: var(--background-modifier-hover); }
+ .zhiyi-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px; }
+ .zhiyi-item-cat { font-size: 10px; color: var(--text-muted); background: var(--background-secondary); padding: 1px 5px; border-radius: 2px; }
+ .zhiyi-item-right { display: flex; align-items: center; gap: 6px; }
+ .zhiyi-quality-bar { width: 40px; height: 4px; background: var(--background-modifier-border); border-radius: 2px; overflow: hidden; }
+ .zhiyi-quality-fill { height: 100%; border-radius: 2px; }
+ .zhiyi-item-content { font-size: 12px; color: var(--text-normal); line-height: 1.4; }
+ .zhiyi-item-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 3px; }
+ .zhiyi-item-time { font-size: 10px; color: var(--text-muted); }
+ .zhiyi-item-meta { font-size: 10px; color: var(--text-faint); font-family: monospace; }
+ .zhiyi-item-actions { display: flex; gap: 6px; opacity: 0; transition: opacity 0.15s; }
+ .zhiyi-item:hover .zhiyi-item-actions { opacity: 1; }
+ .zhiyi-action-btn { font-size: 10px; color: var(--text-accent); cursor: pointer; background: none; border: none; padding: 0; }
+ .zhiyi-action-btn:hover { text-decoration: underline; }
+ .zhiyi-pagination { display: flex; gap: 6px; padding: 8px 12px; border-top: 1px solid var(--background-modifier-border); align-items: center; }
+ .zhiyi-pagination button { padding: 4px 10px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; cursor: pointer; font-size: 11px; }
+ .zhiyi-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
+ .zhiyi-pagination .page-info { flex: 1; text-align: center; font-size: 11px; color: var(--text-muted); }
+ .zhiyi-loading { padding: 30px; text-align: center; color: var(--text-muted); }
+ .zhiyi-empty { padding: 30px; text-align: center; color: var(--text-muted); }
+ /* Modal */
+ .zhiyi-detail-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
+ .zhiyi-detail-box { background: var(--modal-background); border: 1px solid var(--background-modifier-border); border-radius: 8px; max-width: 600px; width: 90%; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
+ .zhiyi-detail-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--background-modifier-border); }
+ .zhiyi-detail-header h3 { margin: 0; font-size: 14px; font-weight: 600; }
+ .zhiyi-detail-close { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 18px; padding: 0; }
+ .zhiyi-detail-body { flex: 1; overflow-y: auto; padding: 16px; }
+ .zhiyi-detail-meta { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
+ .zhiyi-detail-meta span { font-size: 11px; color: var(--text-muted); background: var(--background-secondary); padding: 2px 6px; border-radius: 3px; }
+ .zhiyi-detail-content { font-size: 13px; line-height: 1.6; color: var(--text-normal); white-space: pre-wrap; word-break: break-word; }
+ .zhiyi-detail-footer { padding: 10px 16px; border-top: 1px solid var(--background-modifier-border); display: flex; gap: 8px; justify-content: flex-end; }
+ .zhiyi-detail-footer button { padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; }
+ /* Write dialog */
+ .zhiyi-write-section { padding: 8px 12px; border-bottom: 1px solid var(--background-modifier-border); }
+ .zhiyi-write-input { width: 100%; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; color: var(--text-normal); padding: 6px 8px; font-size: 12px; resize: vertical; min-height: 50px; box-sizing: border-box; }
+ .zhiyi-write-input:focus { outline: none; border-color: var(--text-accent); }
+ .zhiyi-write-meta { display: flex; gap: 6px; margin-top: 4px; align-items: center; }
+ .zhiyi-write-meta select { background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 4px; color: var(--text-normal); font-size: 11px; padding: 2px 4px; }
+ .zhiyi-write-meta button { padding: 3px 10px; background: var(--text-accent); color: var(--bg-on-accent, #fff); border: none; border-radius: 4px; cursor: pointer; font-size: 11px; }
+ .zhiyi-write-meta button:disabled { opacity: 0.5; cursor: not-allowed; }
+ .zhiyi-write-hint { font-size: 10px; color: var(--text-faint); margin-left: auto; }
+ ` });
+
+ const root = this.container.createEl("div", { cls: "zhiyi-memory-view" });
+ root.append(
this.container.createEl("div", { cls: "zhiyi-distill-banner" }),
this.container.createEl("div", { cls: "zhiyi-header" }).append(
- this.container.createEl("h1", { text: "🧠 织忆记忆" }),
+ Object.assign(this.container.createEl("div", { cls: "zhiyi-header-row1" }), {
+ innerHTML: `🧠 织忆记忆
`
+ }) as unknown as Node,
this.container.createEl("p", { cls: "zhiyi-stats", text: "加载中..." })
),
+ this.container.createEl("div", { cls: "zhiyi-toolbar" }),
this.container.createEl("div", { cls: "zhiyi-body" }),
this.container.createEl("div", { cls: "zhiyi-pagination" })
);
+
+ await this.loadCategories();
await this.loadMemories();
await this.renderDistillBanner();
}
- async loadMemories() {
+ private async loadMemories() {
const body = this.container.querySelector(".zhiyi-body") as HTMLElement;
const stats = this.container.querySelector(".zhiyi-stats") as HTMLElement;
+ const toolbar = this.container.querySelector(".zhiyi-toolbar") as HTMLElement;
body.innerHTML = '加载中…
';
+
try {
- const data = await fetchMemories({ namespace: "hermes-main", limit: this.pageSize, offset: this.currentPage * this.pageSize });
+ const ns = getRuntimeNamespace();
+ const data = await fetchMemories({
+ namespace: ns,
+ limit: PAGE_SIZE,
+ offset: this.currentPage * PAGE_SIZE,
+ category: this.activeCategory || undefined,
+ });
this.memories = data.memories || [];
this.totalMemories = data.total || 0;
- stats.setText(`${this.totalMemories} 条记忆 · 第 ${this.currentPage + 1} 页`);
+ stats.setText(`${this.totalMemories} 条记忆 · 第 ${this.currentPage + 1} 页 · ${ns}`);
+ this.renderToolbar(toolbar);
this.renderList();
} catch (e) {
body.innerHTML = `加载失败: ${(e as Error).message}
`;
}
}
+ private renderToolbar(toolbar: HTMLElement) {
+ toolbar.empty();
+
+ // Namespace 切换
+ const nsSelect = toolbar.createEl("select");
+ nsSelect.createEl("option", { value: "hermes-main", text: "hermes-main" });
+ nsSelect.createEl("option", { value: "openclaw", text: "openclaw" });
+ nsSelect.value = getRuntimeNamespace();
+ nsSelect.onchange = () => {
+ const val = nsSelect.value;
+ (window as unknown as Record)["zhiyi_namespace"] = val;
+ this.currentPage = 0;
+ this.loadMemories();
+ };
+
+ // 分类筛选 chip
+ const allChip = toolbar.createEl("span", { cls: "zhiyi-filter-chip" + (this.activeCategory === "" ? " active" : ""), text: "全部" });
+ allChip.onclick = () => { this.activeCategory = ""; this.currentPage = 0; this.loadMemories(); };
+ for (const cat of this.categories.slice(0, 8)) {
+ const chip = toolbar.createEl("span", {
+ cls: "zhiyi-filter-chip" + (this.activeCategory === cat ? " active" : ""),
+ text: cat,
+ });
+ chip.onclick = () => { this.activeCategory = cat; this.currentPage = 0; this.loadMemories(); };
+ }
+
+ // 刷新
+ const refreshBtn = toolbar.createEl("button", { cls: "zhiyi-btn-icon", text: "↻ 刷新" });
+ refreshBtn.onclick = () => { this.currentPage = 0; this.loadMemories(); };
+
+ // 写记忆
+ const writeBtn = toolbar.createEl("button", { cls: "zhiyi-btn-icon", text: "+ 写记忆" });
+ writeBtn.onclick = () => this.showWriteSection(toolbar);
+ }
+
+ private showWriteSection(toolbar: HTMLElement) {
+ // 避免重复
+ const existing = toolbar.querySelector(".zhiyi-write-section");
+ if (existing) { existing.remove(); return; }
+
+ const section = toolbar.createEl("div", { cls: "zhiyi-write-section" });
+ const textarea = section.createEl("textarea", {
+ cls: "zhiyi-write-input",
+ attr: { placeholder: "输入记忆内容,按 Enter 或点击「写入」提交…" }
+ }) as HTMLTextAreaElement;
+
+ const metaRow = section.createEl("div", { cls: "zhiyi-write-meta" });
+ const catSel = metaRow.createEl("select");
+ catSel.createEl("option", { value: "normal", text: "分类" });
+ for (const c of this.categories) {
+ catSel.createEl("option", { value: c, text: c });
+ }
+ const hint = metaRow.createEl("span", { cls: "zhiyi-write-hint", text: "Enter 写入 · Esc 取消" });
+ const submitBtn = metaRow.createEl("button", { text: "写入" });
+
+ const doWrite = async () => {
+ const content = textarea.value.trim();
+ if (!content) return;
+ submitBtn.setAttr("disabled", "true");
+ try {
+ await writeMemory(content, catSel.value || "normal");
+ textarea.value = "";
+ section.remove();
+ this.currentPage = 0;
+ await this.loadCategories();
+ await this.loadMemories();
+ } catch (e) {
+ hint.setText(`失败: ${(e as Error).message}`);
+ }
+ submitBtn.removeAttr("disabled");
+ };
+
+ submitBtn.onclick = doWrite;
+ textarea.onkeydown = (e) => {
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); doWrite(); }
+ if (e.key === "Escape") section.remove();
+ };
+ textarea.focus();
+ }
+
private renderList() {
const body = this.container.querySelector(".zhiyi-body") as HTMLElement;
const pag = this.container.querySelector(".zhiyi-pagination") as HTMLElement;
body.empty();
+
if (this.memories.length === 0) {
body.createEl("div", { cls: "zhiyi-empty", text: "暂无记忆" });
}
+
for (const mem of this.memories) {
const item = body.createEl("div", { cls: "zhiyi-item" });
+
const header = item.createEl("div", { cls: "zhiyi-item-header" });
header.createEl("span", { cls: "zhiyi-item-cat", text: mem.category || "normal" });
- header.createEl("span", { cls: "zhiyi-item-score", text: `score: ${(mem.quality_score ?? 0).toFixed(2)}` });
- item.createEl("div", { cls: "zhiyi-item-content", text: this.truncate(mem.content, 120) });
- item.createEl("div", { cls: "zhiyi-item-time", text: this.formatTime(mem.created_at) });
- item.createEl("div", { cls: "zhiyi-item-id", text: mem.id });
+
+ const right = header.createEl("div", { cls: "zhiyi-item-right" });
+ // 质量分条
+ const q = mem.quality_score ?? 0;
+ const qColor = q > 0.7 ? "#34c759" : q > 0.4 ? "#f6c945" : "#ff6b6b";
+ const barWrap = right.createEl("div", { cls: "zhiyi-quality-bar" });
+ const barFill = barWrap.createEl("div", { cls: "zhiyi-quality-fill" });
+ barFill.style.width = `${Math.round(q * 100)}%`;
+ barFill.style.background = qColor;
+ right.createEl("span", { cls: "zhiyi-item-score", text: (q * 100).toFixed(0) + "%" });
+
+ item.createEl("div", { cls: "zhiyi-item-content", text: this.truncate(mem.content, 160) });
+
+ const footer = item.createEl("div", { cls: "zhiyi-item-footer" });
+ footer.createEl("span", { cls: "zhiyi-item-time", text: this.formatTime(mem.created_at) });
+ const meta = footer.createEl("span", { cls: "zhiyi-item-meta", text: mem.id.slice(0, 16) });
+ const actions = footer.createEl("div", { cls: "zhiyi-item-actions" });
+ actions.createEl("button", { cls: "zhiyi-action-btn", text: "展开" }).onclick = (e) => {
+ e.stopPropagation();
+ this.showDetail(mem);
+ };
+ actions.createEl("button", { cls: "zhiyi-action-btn", text: "复制" }).onclick = (e) => {
+ e.stopPropagation();
+ navigator.clipboard.writeText(mem.content).catch(() => {});
+ };
+
+ item.onclick = () => this.showDetail(mem);
}
- const totalPages = Math.ceil(this.totalMemories / this.pageSize);
+
+ const totalPages = Math.ceil(this.totalMemories / PAGE_SIZE) || 1;
pag.empty();
- if (totalPages > 1) {
- const prev = pag.createEl("button", { text: "◀ 上一页" });
- prev.setAttr("disabled", this.currentPage === 0 ? "true" : "");
- prev.onclick = () => { this.currentPage--; this.loadMemories(); };
- pag.createEl("span", { text: `${this.currentPage + 1}/${totalPages}`, cls: "zhiyi-item-cat" });
- const next = pag.createEl("button", { text: "下一页 ▶" });
- next.setAttr("disabled", this.currentPage >= totalPages - 1 ? "true" : "");
- next.onclick = () => { this.currentPage++; this.loadMemories(); };
- }
+ const prev = pag.createEl("button", { text: "◀" });
+ prev.setAttr("disabled", this.currentPage === 0 ? "true" : "");
+ prev.onclick = () => { this.currentPage--; this.loadMemories(); };
+ pag.createEl("span", { cls: "page-info", text: `${this.currentPage + 1} / ${totalPages}` });
+ const next = pag.createEl("button", { text: "▶" });
+ next.setAttr("disabled", this.currentPage >= totalPages - 1 ? "true" : "");
+ next.onclick = () => { this.currentPage++; this.loadMemories(); };
+ }
+
+ private showDetail(mem: MemoryRecord) {
+ const modal = this.container.createEl("div", { cls: "zhiyi-detail-modal" });
+ const box = modal.createEl("div", { cls: "zhiyi-detail-box" });
+
+ const header = box.createEl("div", { cls: "zhiyi-detail-header" });
+ header.createEl("h3", { text: "记忆详情" });
+ const closeBtn = header.createEl("button", { cls: "zhiyi-detail-close", text: "×" });
+ closeBtn.onclick = () => modal.remove();
+ modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
+
+ const body = box.createEl("div", { cls: "zhiyi-detail-body" });
+ const meta = body.createEl("div", { cls: "zhiyi-detail-meta" });
+ meta.createEl("span", { text: `分类: ${mem.category || "normal"}` });
+ meta.createEl("span", { text: `质量: ${((mem.quality_score ?? 0) * 100).toFixed(1)}%` });
+ meta.createEl("span", { text: `Namespace: ${mem.namespace}` });
+ meta.createEl("span", { text: `时间: ${this.formatTime(mem.created_at)}` });
+ if (mem.recall_count !== undefined) meta.createEl("span", { text: `召回: ${mem.recall_count}` });
+ body.createEl("div", { cls: "zhiyi-detail-content", text: mem.content });
+
+ const footer = box.createEl("div", { cls: "zhiyi-detail-footer" });
+ footer.createEl("button", { text: "复制内容", attr: { style: "background:var(--background-secondary);border:1px solid var(--background-modifier-border);color:var(--text-normal)" } }).onclick = () => {
+ navigator.clipboard.writeText(mem.content).catch(() => {});
+ };
+ footer.createEl("button", { text: "关闭", attr: { style: "background:var(--text-accent);border:none;color:var(--bg-on-accent,#fff)" } }).onclick = () => modal.remove();
}
private truncate(s: string, max: number) {
diff --git a/plugins/obsidian/src/api.ts b/plugins/obsidian/src/api.ts
index 5c9492a..1dc5528 100644
--- a/plugins/obsidian/src/api.ts
+++ b/plugins/obsidian/src/api.ts
@@ -1,10 +1,30 @@
-
-
// ─── API 客户端(调用织忆 Go API)─────────────────────────────────────────────
const API_BASE = "http://localhost:7821";
const API_KEY = "zhiyi-dev-key-2026";
+// 运行时配置覆盖(由设置页写入)
+interface RuntimeConfig {
+ apiUrl?: string;
+ apiKey?: string;
+ namespace?: string;
+}
+const cfg: RuntimeConfig = {};
+export function getApiBase() {
+ return (cfg.apiUrl || API_BASE).replace(/\/$/, "");
+}
+export function getApiKey() {
+ return cfg.apiKey || API_KEY;
+}
+export function setRuntimeConfig(url: string, key: string, ns: string) {
+ cfg.apiUrl = url;
+ cfg.apiKey = key;
+ cfg.namespace = ns;
+}
+export function getRuntimeNamespace() {
+ return cfg.namespace || "hermes-main";
+}
+
interface ApiOptions {
method?: string;
body?: unknown;
@@ -16,7 +36,7 @@ async function apiCall(
): Promise {
const method = opts.method ?? "GET";
const headers: Record = {
- "X-API-Key": API_KEY,
+ "X-API-Key": getApiKey(),
"Content-Type": "application/json",
};
@@ -25,12 +45,11 @@ async function apiCall(
init.body = JSON.stringify(opts.body);
}
- const res = await fetch(`${API_BASE}${path}`, init);
+ const res = await fetch(`${getApiBase()}${path}`, init);
if (!res.ok) {
throw new Error(`API ${path} failed: ${res.status} ${res.statusText}`);
}
- // 有些端点返回空 body
const text = await res.text();
if (!text) return {} as T;
return JSON.parse(text) as T;
@@ -46,6 +65,9 @@ export interface MemoryRecord {
quality_score: number;
created_at: string;
agent_id?: string;
+ importance?: number;
+ recall_count?: number;
+ tier?: string;
}
export interface GraphNode {
@@ -69,7 +91,7 @@ export interface GraphNavigateResult {
}
export interface RecallResult {
- memories: Array<{ id: string; content: string; score: number; category: string }>;
+ memories: Array<{ id: string; content: string; score: number; category: string; created_at: string }>;
}
export interface DistillStatus {
@@ -99,19 +121,22 @@ export async function fetchMemories(opts: {
namespace?: string;
limit?: number;
offset?: number;
+ category?: string;
}): Promise<{ memories: MemoryRecord[]; total: number }> {
const params = new URLSearchParams();
- if (opts.namespace) params.set("namespace", opts.namespace);
+ const ns = opts.namespace || getRuntimeNamespace();
+ if (ns) params.set("namespace", ns);
if (opts.limit) params.set("limit", String(opts.limit));
if (opts.offset !== undefined) params.set("offset", String(opts.offset));
+ if (opts.category) params.set("category", opts.category);
const qs = params.toString();
return apiCall(`/api/v1/memories?${qs}`);
}
-export async function searchMemories(query: string, namespace = "hermes-main") {
+export async function searchMemories(query: string, namespace?: string) {
return apiCall<{ memories: RecallResult["memories"] }>("/api/v1/search/recall", {
method: "POST",
- body: { query, namespace, top_k: 20 },
+ body: { query, namespace: namespace || getRuntimeNamespace(), top_k: 20 },
});
}
@@ -119,9 +144,9 @@ export async function fetchNeighbors(entity: string) {
return apiCall(`/api/v1/graph/navigate?entity=${encodeURIComponent(entity)}&depth=1`);
}
-export async function fetchGraphExport(namespace = "hermes-main", limit = 100) {
+export async function fetchGraphExport(namespace?: string, limit = 100) {
return apiCall<{ nodes: GraphNode[]; edges: GraphEdge[] }>(
- `/api/v1/graph/export?namespace=${namespace}&limit=${limit}`
+ `/api/v1/graph/export?namespace=${namespace || getRuntimeNamespace()}&limit=${limit}`
);
}
@@ -143,4 +168,37 @@ export async function fetchByEntity(entity: string) {
return apiCall<{ memories: MemoryRecord[] }>(
`/api/v1/memories/by-entity/${encodeURIComponent(entity)}`
);
+}
+
+// ─── 写入记忆 ──────────────────────────────────────────────────────────────
+
+export async function writeMemory(
+ content: string,
+ category = "normal",
+ namespace?: string
+): Promise<{ id: string }> {
+ return apiCall<{ id: string }>("/api/v1/memories", {
+ method: "POST",
+ body: {
+ content,
+ category,
+ namespace: namespace || getRuntimeNamespace(),
+ source: "obsidian-plugin",
+ },
+ });
+}
+
+// ─── 获取所有 category(从记忆列表中提取) ──────────────────────────────────
+
+export async function fetchCategories(): Promise {
+ try {
+ const data = await fetchMemories({ limit: 500 });
+ const cats = new Set();
+ for (const m of data.memories) {
+ if (m.category) cats.add(m.category);
+ }
+ return Array.from(cats).sort();
+ } catch {
+ return [];
+ }
}
\ No newline at end of file