remnote-bridge 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,7 +57,7 @@ Add a <Suspense fallback=...> component higher in the tree to provide a loading
57
57
  * LICENSE file in the root directory of this source tree.
58
58
  */var qe,Ot,De,it;if(typeof performance=="object"&&typeof performance.now=="function"){var B=performance;K.unstable_now=function(){return B.now()}}else{var wn=Date,Et=wn.now();K.unstable_now=function(){return wn.now()-Et}}if(typeof window>"u"||typeof MessageChannel!="function"){var Tt=null,b=null,Y=function(){if(Tt!==null)try{var L=K.unstable_now();Tt(!0,L),Tt=null}catch(A){throw setTimeout(Y,0),A}};qe=function(L){Tt!==null?setTimeout(qe,0,L):(Tt=L,setTimeout(Y,0))},Ot=function(L,A){b=setTimeout(L,A)},De=function(){clearTimeout(b)},K.unstable_shouldYield=function(){return!1},it=K.unstable_forceFrameRate=function(){}}else{var c=window.setTimeout,Mt=window.clearTimeout;if(typeof console<"u"){var ht=window.cancelAnimationFrame;typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof ht!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")}var He=!1,at=null,Pt=-1,tt=5,ot=0;K.unstable_shouldYield=function(){return K.unstable_now()>=ot},it=function(){},K.unstable_forceFrameRate=function(L){0>L||125<L?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):tt=0<L?Math.floor(1e3/L):5};var te=new MessageChannel,ae=te.port2;te.port1.onmessage=function(){if(at!==null){var L=K.unstable_now();ot=L+tt;try{at(!0,L)?ae.postMessage(null):(He=!1,at=null)}catch(A){throw ae.postMessage(null),A}}else He=!1},qe=function(L){at=L,He||(He=!0,ae.postMessage(null))},Ot=function(L,A){Pt=c(function(){L(K.unstable_now())},A)},De=function(){Mt(Pt),Pt=-1}}function _e(L,A){var $=L.length;L.push(A);e:for(;;){var H=$-1>>>1,ne=L[H];if(ne!==void 0&&0<st(ne,A))L[H]=A,L[$]=ne,$=H;else break e}}function Oe(L){return L=L[0],L===void 0?null:L}function $e(L){var A=L[0];if(A!==void 0){var $=L.pop();if($!==A){L[0]=$;e:for(var H=0,ne=L.length;H<ne;){var U=2*(H+1)-1,ye=L[U],M=U+1,ln=L[M];if(ye!==void 0&&0>st(ye,$))ln!==void 0&&0>st(ln,ye)?(L[H]=ln,L[M]=$,H=M):(L[H]=ye,L[U]=$,H=U);else if(ln!==void 0&&0>st(ln,$))L[H]=ln,L[M]=$,H=M;else break e}}return A}return null}function st(L,A){var $=L.sortIndex-A.sortIndex;return $!==0?$:L.id-A.id}var Z=[],Ze=[],Ye=1,ue=null,fe=3,ee=!1,ie=!1,me=!1;function q(L){for(var A=Oe(Ze);A!==null;){if(A.callback===null)$e(Ze);else if(A.startTime<=L)$e(Ze),A.sortIndex=A.expirationTime,_e(Z,A);else break;A=Oe(Ze)}}function ge(L){if(me=!1,q(L),!ie)if(Oe(Z)!==null)ie=!0,qe(C);else{var A=Oe(Ze);A!==null&&Ot(ge,A.startTime-L)}}function C(L,A){ie=!1,me&&(me=!1,De()),ee=!0;var $=fe;try{for(q(A),ue=Oe(Z);ue!==null&&(!(ue.expirationTime>A)||L&&!K.unstable_shouldYield());){var H=ue.callback;if(typeof H=="function"){ue.callback=null,fe=ue.priorityLevel;var ne=H(ue.expirationTime<=A);A=K.unstable_now(),typeof ne=="function"?ue.callback=ne:ue===Oe(Z)&&$e(Z),q(A)}else $e(Z);ue=Oe(Z)}if(ue!==null)var U=!0;else{var ye=Oe(Ze);ye!==null&&Ot(ge,ye.startTime-A),U=!1}return U}finally{ue=null,fe=$,ee=!1}}var V=it;K.unstable_IdlePriority=5,K.unstable_ImmediatePriority=1,K.unstable_LowPriority=4,K.unstable_NormalPriority=3,K.unstable_Profiling=null,K.unstable_UserBlockingPriority=2,K.unstable_cancelCallback=function(L){L.callback=null},K.unstable_continueExecution=function(){ie||ee||(ie=!0,qe(C))},K.unstable_getCurrentPriorityLevel=function(){return fe},K.unstable_getFirstCallbackNode=function(){return Oe(Z)},K.unstable_next=function(L){switch(fe){case 1:case 2:case 3:var A=3;break;default:A=fe}var $=fe;fe=A;try{return L()}finally{fe=$}},K.unstable_pauseExecution=function(){},K.unstable_requestPaint=V,K.unstable_runWithPriority=function(L,A){switch(L){case 1:case 2:case 3:case 4:case 5:break;default:L=3}var $=fe;fe=L;try{return A()}finally{fe=$}},K.unstable_scheduleCallback=function(L,A,$){var H=K.unstable_now();switch(typeof $=="object"&&$!==null?($=$.delay,$=typeof $=="number"&&0<$?H+$:H):$=H,L){case 1:var ne=-1;break;case 2:ne=250;break;case 5:ne=1073741823;break;case 4:ne=1e4;break;default:ne=5e3}return ne=$+ne,L={id:Ye++,callback:A,priorityLevel:L,startTime:$,expirationTime:ne,sortIndex:-1},$>H?(L.sortIndex=$,_e(Ze,L),Oe(Z)===null&&L===Oe(Ze)&&(me?De():me=!0,Ot(ge,$-H))):(L.sortIndex=ne,_e(Z,L),ie||ee||(ie=!0,qe(C))),L},K.unstable_wrapCallback=function(L){var A=fe;return function(){var $=fe;fe=A;try{return L.apply(this,arguments)}finally{fe=$}}}},825(tn,K,qe){"use strict";tn.exports=qe(742)}},vd={};function Ac(tn){var K=vd[tn];if(K!==void 0)return K.exports;var qe=vd[tn]={exports:{}};return Pf[tn](qe,qe.exports,Ac),qe.exports}Ac.g=(function(){if(typeof globalThis=="object")return globalThis;try{return this||new Function("return this")()}catch{if(typeof window=="object")return window}})();var wp={};(()=>{"use strict";var tn=Ac(216);const K="0.2.1",qe=[29100,29110,29120,29130],Ot=18e3;var De=Object.defineProperty,it=(w,y,E)=>y in w?De(w,y,{enumerable:!0,configurable:!0,writable:!0,value:E}):w[y]=E,B=(w,y,E)=>it(w,typeof y!="symbol"?y+"":y,E);const wn=4e3,Et=4003;class Tt{constructor(y){B(this,"ws",null),B(this,"reconnectAttempts",0),B(this,"reconnectTimeout",null),B(this,"messageHandler",null),B(this,"status","disconnected"),B(this,"isShuttingDown",!1),B(this,"isPreempted",!1),B(this,"_sdkReady"),B(this,"config"),this._sdkReady=y.sdkReady,this.config={url:y.url,pluginVersion:y.pluginVersion,sdkReady:y.sdkReady,twinSlotIndex:y.twinSlotIndex,isTwinConnection:y.isTwinConnection??!1,maxReconnectAttempts:y.maxReconnectAttempts??10,initialReconnectDelay:y.initialReconnectDelay??1e3,maxReconnectDelay:y.maxReconnectDelay??3e4,onStatusChange:y.onStatusChange,onLog:y.onLog,onPreempted:y.onPreempted,onTwinOccupied:y.onTwinOccupied,onOtherOccupied:y.onOtherOccupied}}log(y,E="info"){this.config.onLog?.(y,E)}setStatus(y){this.status!==y&&(this.status=y,this.config.onStatusChange?.(y))}sendHello(){const y={type:"hello",version:this.config.pluginVersion,sdkReady:this._sdkReady,twinSlotIndex:this.config.twinSlotIndex};try{this.ws?.send(JSON.stringify(y)),this.log(`\u53D1\u9001 hello\uFF08v${this.config.pluginVersion}, sdkReady=${this._sdkReady}, twinSlot=${this.config.twinSlotIndex}\uFF09`)}catch(E){this.log(`\u53D1\u9001 hello \u5931\u8D25: ${E}`,"warn")}}connect(){if(!(this.ws?.readyState===WebSocket.OPEN||this.ws?.readyState===WebSocket.CONNECTING)){this.isShuttingDown=!1,this.isPreempted=!1,this.setStatus("connecting");try{this.ws=new WebSocket(this.config.url),this.ws.onopen=()=>{this.log("\u5DF2\u8FDE\u63A5\u5230\u5B88\u62A4\u8FDB\u7A0B"),this.reconnectAttempts=0,this.setStatus("connected"),this.sendHello()},this.ws.onmessage=async y=>{await this.handleMessage(typeof y.data=="string"?y.data:String(y.data))},this.ws.onclose=y=>{y.code!==1006&&this.log(`\u8FDE\u63A5\u65AD\u5F00: ${y.code} ${y.reason}`,"warn"),this.setStatus("disconnected"),y.code===Et?this.config.onTwinOccupied?.():y.code===wn&&this.config.onOtherOccupied?.(),this.isShuttingDown||this.scheduleReconnect()},this.ws.onerror=()=>{}}catch(y){this.log(`\u8FDE\u63A5\u5931\u8D25: ${y}`,"error"),this.setStatus("disconnected"),this.scheduleReconnect()}}}async handleMessage(y){try{const E=JSON.parse(y);if(E.type==="preempted"){this.isPreempted=!0,this.log(`\u88AB\u5B6A\u751F Plugin \u62A2\u5360: ${E.reason}`,"warn"),this.config.onPreempted?.();return}if(E.type==="ping"){this.ws?.send(JSON.stringify({type:"pong"}));return}if(E.id&&E.action&&this.messageHandler){const R=E;this.log(`\u6536\u5230\u8BF7\u6C42: ${R.action}`);try{const N=await this.messageHandler(R),F={id:R.id,result:N};this.ws?.send(JSON.stringify(F)),this.log(`\u5B8C\u6210: ${R.action}`)}catch(N){const F=N instanceof Error?N.message:String(N),Q={id:R.id,error:F};this.ws?.send(JSON.stringify(Q)),this.log(`\u5931\u8D25: ${R.action} - ${F}`,"error")}}}catch(E){this.log(`\u5904\u7406\u6D88\u606F\u5931\u8D25: ${E}`,"error")}}scheduleReconnect(){if(this.isShuttingDown||this.isPreempted||!this.config.isTwinConnection)return;if(this.reconnectAttempts>=this.config.maxReconnectAttempts){this.log("\u5DF2\u8FBE\u6700\u5927\u91CD\u8FDE\u6B21\u6570","error");return}const y=Math.min(this.config.initialReconnectDelay*Math.pow(2,this.reconnectAttempts),this.config.maxReconnectDelay),E=Math.random()*.3*y,R=y+E;this.reconnectAttempts++,this.log(`${Math.round(R)}ms \u540E\u91CD\u8FDE\uFF08\u7B2C ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} \u6B21\uFF09`),this.reconnectTimeout=setTimeout(()=>{this.connect()},R)}setMessageHandler(y){this.messageHandler=y}disconnect(){this.isShuttingDown=!0,this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null),this.ws&&(this.ws.close(1e3,"Plugin disconnect"),this.ws=null),this.setStatus("disconnected")}getStatus(){return this.status}}var b=Object.defineProperty,Y=(w,y,E)=>y in w?b(w,y,{enumerable:!0,configurable:!0,writable:!0,value:E}):w[y]=E,c=(w,y,E)=>Y(w,typeof y!="symbol"?y+"":y,E);class Mt{constructor(y){c(this,"clients",[]),c(this,"slotStates",[]),c(this,"scanTimer",null),c(this,"config"),this.config=y;for(let E=0;E<qe.length;E++){const R=E===y.twinSlotIndex;this.slotStates.push({slotIndex:E,wsPort:qe[E],status:"disconnected",isTwin:R,disconnectReason:"not_started"}),this.clients.push(new Tt({url:`ws://127.0.0.1:${qe[E]}`,pluginVersion:y.pluginVersion,sdkReady:y.sdkReady,twinSlotIndex:y.twinSlotIndex,isTwinConnection:R,maxReconnectAttempts:R?10:0,initialReconnectDelay:1e3,maxReconnectDelay:3e4,onStatusChange:N=>this.handleStatusChange(E,N),onLog:(N,F)=>y.onLog(E,N,F),onPreempted:()=>this.handleDisconnectReason(E,"preempted"),onTwinOccupied:()=>this.handleDisconnectReason(E,"twin_occupied"),onOtherOccupied:()=>this.handleDisconnectReason(E,"other_occupied")}))}}setMessageHandler(y){for(const E of this.clients)E.setMessageHandler(y)}start(){const y=this.config.twinSlotIndex;this.clients[y].connect(),setTimeout(()=>{for(let E=0;E<this.clients.length;E++)E!==y&&this.clients[E].connect()},2e3),this.scanTimer=setInterval(()=>this.scanAndReconnect(),Ot)}stop(){this.scanTimer&&(clearInterval(this.scanTimer),this.scanTimer=null);for(const y of this.clients)y.disconnect()}getSlots(){return this.slotStates.slice()}handleStatusChange(y,E){const R=this.slotStates[y];R.status=E,E==="connected"&&(R.disconnectReason=null),this.notifySlotsChange()}handleDisconnectReason(y,E){this.slotStates[y].disconnectReason=E,this.notifySlotsChange()}notifySlotsChange(){this.config.onSlotsChange(this.slotStates.slice())}scanAndReconnect(){for(let y=0;y<this.clients.length;y++){const E=this.slotStates[y];E.isTwin||E.status==="connected"||E.status==="connecting"||this.clients[y].connect()}}}async function ht(w){const[y,E,R,N]=await Promise.all([w.isPowerupProperty(),w.isPowerupSlot(),w.isPowerupPropertyListItem(),w.isPowerupEnum()]);return y||E||R||N}async function He(w){const y=await Promise.all(w.map(ht));return w.filter((E,R)=>!y[R])}async function at(w){const y=await Promise.all(w.map(E=>E.isPowerup()));return w.filter((E,R)=>!y[R])}async function Pt(w,y){try{return await w.richText.toMarkdown(y)}catch{return tt(y)}}function tt(w){return w.map(y=>{if(typeof y=="string")return y;if(typeof y!="object"||y===null)return"";const E=y;switch(E.i){case"m":return String(E.text??"");case"q":return`[[${String(E._id??"")}]]`;case"u":return E.title?`[${String(E.title)}](${String(E.url)})`:String(E.url??"");case"x":return`$${String(E.text??"")}$`;case"i":return`![image](${String(E.url??"")})`;case"a":return`[audio](${String(E.url??"")})`;case"p":return String(E.text??"");case"g":return String(E.text??"");case"n":return String(E.text??"");case"o":return String(E.text??"");case"s":case"fi":case"ai":return"";default:return String(E.text??E.url??"")}}).join("")}async function ot(w,y,E,R){const N=R?.includePowerup??!1,[F,Q,G,ve,Ae,Ne,he,mt,Me,Qe,wt,Pe,Be,Xe,nt]=await Promise.all([Pt(w,y.text??[]),y.backText?Pt(w,y.backText):Promise.resolve(null),y.getType(),y.isCardItem(),y.isDocument(),y.getTagRems().then(Je=>N?Je:at(Je).then(Ke=>{const Zt=Je.length-Ke.length;return Zt>0&&R?.onFilteredTags?.(Zt),Ke})),y.getPracticeDirection(),y.getFontSize(),y.isTodo(),y.getTodoStatus(),y.isCode(),y.isQuote(),y.isListItem(),y.hasPowerup("dv"),y.type===6?y.getPortalDirectlyIncludedRem():Promise.resolve([])]);let We=!1;E.length>0&&(We=(await Promise.all(E.map(Ke=>Ke.isCardItem()))).some(Boolean));const xt=Xe&&(y.text??[]).length===0,Fe=await Promise.all(Ne.map(async Je=>({id:Je._id,name:te(await Pt(w,Je.text??[]))})));return{id:y._id,markdownText:te(F),markdownBackText:Q!==null?te(Q):null,type:ae(G),hasMultilineChildren:We,practiceDirection:he??"none",isCardItem:ve,isDocument:Ae,isPortal:y.type===6,portalRefs:nt.map(Je=>Je._id),childrenCount:E.length,tags:Fe,fontSize:mt??null,isTodo:Me,todoStatus:Qe??null,isCode:wt,isQuote:Pe,isListItem:Be,isDivider:xt,isTopLevel:y.parent===null}}function te(w){return w.replace(/\n/g," ")}function ae(w){switch(w){case 1:return"concept";case 2:return"descriptor";case 6:return"portal";default:return"default"}}async function _e(w,y){const{includePowerup:E=!1}=y,R=await w.rem.findOne(y.remId);if(!R)throw new Error(`Rem not found: ${y.remId}`);return Oe(w,R,{includePowerup:E})}async function Oe(w,y,E){const{includePowerup:R=!1}=E,[N,F,Q,G,ve,Ae,Ne,he,mt,Me,Qe,wt,Pe,Be,Xe,nt,We,xt,Fe,Je,Ke,Zt,Ge,Kt,un,pn,Wt,ut,ct,Nt,Bt,hn,Dt,Jn,ur,So,er,Tr,_n,Sn,Dn,cr,xr]=await Promise.all([y.isDocument(),y.getFontSize(),y.getHighlightColor(),y.isTodo(),y.getTodoStatus(),y.isCode(),y.isQuote(),y.isListItem(),y.isCardItem(),y.isTable(),y.isSlot(),y.isProperty(),y.getEnablePractice(),y.getPracticeDirection(),y.getTagRems(),y.getSources(),y.getAliases(),y.positionAmongstSiblings(),y.isPowerup(),y.isPowerupEnum(),y.isPowerupProperty(),y.isPowerupPropertyListItem(),y.isPowerupSlot(),y.getPortalType(),y.getPortalDirectlyIncludedRem(),y.getPropertyType(),y.remsBeingReferenced(),y.deepRemsBeingReferenced(),y.remsReferencingThis(),y.taggedRem(),y.ancestorTagRem(),y.descendantTagRem(),y.getDescendants(),y.siblingRem(),y.portalsAndDocumentsIn(),y.allRemInDocumentOrPortal(),y.allRemInFolderQueue(),y.getChildrenRem(),y.timesSelectedInSearch(),y.getLastTimeMovedTo(),y.getSchemaVersion(),y.embeddedQueueViewMode(),y.getLastPracticed()]);let dr=Xe,Cr=y.children??[],mi=0,Mr=0;if(!R){dr=await at(Xe),mi=Xe.length-dr.length;const ze=await He(Tr);Mr=Tr.length-ze.length,Cr=ze.map(An=>An._id)}const fr={id:y._id,text:$e(y.text??[]),backText:y.backText?$e(y.backText):null,type:ae(y.type),isDocument:N,parent:y.parent,children:Cr,fontSize:F??null,highlightColor:Q??null,isTodo:G,todoStatus:ve??null,isCode:Ae,isQuote:Ne,isListItem:he,isCardItem:mt,isTable:!!Me,isSlot:Qe,isProperty:wt,isPowerup:Fe,isPowerupEnum:Je,isPowerupProperty:Ke,isPowerupPropertyListItem:Zt,isPowerupSlot:Ge,portalType:ae(y.type)==="portal"?st(Kt):null,portalDirectlyIncludedRem:un.map(ze=>ze._id).sort(),propertyType:pn??null,enablePractice:Pe,practiceDirection:Be,tags:dr.map(ze=>ze._id).sort(),sources:nt.map(ze=>ze._id).sort(),aliases:We.map(ze=>ze._id).sort(),remsBeingReferenced:Wt.map(ze=>ze._id).sort(),deepRemsBeingReferenced:ut.map(ze=>ze._id).sort(),remsReferencingThis:ct.map(ze=>ze._id).sort(),taggedRem:Nt.map(ze=>ze._id).sort(),ancestorTagRem:Bt.map(ze=>ze._id).sort(),descendantTagRem:hn.map(ze=>ze._id).sort(),descendants:Dt.map(ze=>ze._id).sort(),siblingRem:Jn.map(ze=>ze._id).sort(),portalsAndDocumentsIn:ur.map(ze=>ze._id).sort(),allRemInDocumentOrPortal:So.map(ze=>ze._id).sort(),allRemInFolderQueue:er.map(ze=>ze._id).sort(),positionAmongstSiblings:xt??null,timesSelectedInSearch:_n,lastTimeMovedTo:Sn,schemaVersion:Dn,embeddedQueueViewMode:cr,createdAt:y.createdAt,updatedAt:y.updatedAt,localUpdatedAt:y.localUpdatedAt,lastPracticed:xr};return!R&&(mi>0||Mr>0)?{...fr,powerupFiltered:{tags:mi,children:Mr}}:fr}function $e(w){return w.map(y=>{if(typeof y=="string")return y;const E={};for(const R of Object.keys(y).sort())E[R]=y[R];return E})}function st(w){switch(w){case 2:return"embedded_queue";case 3:return"scaffold";case 4:return"search_portal";default:return"portal"}}function Z(w){return"type"in w&&w.type==="elided"}function Ze(w){if(w.isDivider)return"---";const{markdownText:y,markdownBackText:E,hasMultilineChildren:R,practiceDirection:N}=w;let F;return E!==null?F=y+(R?N==="backward"?" \u2191 ":N==="both"?" \u2195 ":" \u2193 ":N==="backward"?" \u2190 ":N==="both"?" \u2194 ":" \u2192 ")+E:R?F=y+(N==="backward"?" \u2191":N==="both"?" \u2195":" \u2193"):F=y,w.isCode&&(F="`"+F+"`"),w.isListItem&&(F="1. "+F),w.isQuote&&(F="> "+F),w.isTodo&&(F=(w.todoStatus==="Finished"?"- [x] ":"- [ ] ")+F),w.fontSize&&(F=(w.fontSize==="H1"?"# ":w.fontSize==="H2"?"## ":"### ")+F),F}function Ye(w,y){const E=[];w.type==="concept"?E.push("type:concept"):w.type==="descriptor"?E.push("type:descriptor"):w.type==="portal"&&(E.push("type:portal"),w.portalRefs.length>0&&E.push("refs:"+w.portalRefs.join(","))),w.isDocument&&E.push("doc"),w.isCardItem&&E.push("role:card-item"),y&&w.childrenCount>0&&E.push("children:"+w.childrenCount);for(const R of w.tags)E.push("tag:"+R.name+"("+R.id+")");return w.isTopLevel&&E.push("top"),E}function ue(w){return{markdownBackText:null,type:"default",hasMultilineChildren:!1,practiceDirection:"none",isCardItem:!1,isDocument:!1,isPortal:!1,portalRefs:[],tags:[],fontSize:null,isTodo:!1,todoStatus:null,isCode:!1,isQuote:!1,isListItem:!1,isDivider:!1,...w}}function fe(w,y=!1){const E=Ze(w),R=Ye(w,y),N=R.length>0?" "+R.join(" "):"";return`${E} <!--${w.id}${N}-->`}function ee(w){const y=w.isExact?"siblings":"nodes";return`<!--...elided ${w.isExact?"":">="}${w.count} ${y} (parent:${w.parentId} range:${w.rangeFrom}-${w.rangeTo} total:${w.totalSiblings})-->`}function ie(w){const y=[];function E(R,N){const F=" ".repeat(N);if(Z(R)){y.push(F+ee(R));return}const Q=fe(R.rem,R.folded);if(y.push(F+Q),!R.folded)for(const G of R.children)E(G,N+1)}return E(w,0),y.join(`
59
59
  `)}function me(w,y,E){if(w<=y)return{visibleIndices:null,elided:null};const R=Math.ceil(y*.7),N=Math.floor(y*.3);return{visibleIndices:{head:R,tail:N},elided:{count:w-R-N,parentId:E,rangeFrom:R,rangeTo:w-N-1,totalSiblings:w}}}async function q(w,y){const{remId:E,depth:R=3,maxNodes:N=200,maxSiblings:F=20,ancestorLevels:Q=0,includePowerup:G=!1}=y,ve=await w.rem.findOne(E);if(!ve)throw new Error(`Rem not found: ${E}`);let Ae=0,Ne=0,he=0;const mt={remaining:N},Me=[];async function Qe(We,xt,Fe){Ae++,mt.remaining--,Me.push(We._id);const Je=await We.getChildrenRem(),Ke=G?Je:await He(Je);G||(he+=Je.length-Ke.length);const Ge=Fe!==-1&&xt>=Fe&&Ke.length>0,Kt=await ot(w,We,Ke,{includePowerup:G,onFilteredTags:pn=>{Ne+=pn}});Ge&&(Kt.hasMultilineChildren=!1);const un=[];if(!Ge){const{visibleIndices:pn,elided:Wt}=me(Ke.length,F,We._id);if(pn){const{head:ut,tail:ct}=pn;for(let Bt=0;Bt<ut&&mt.remaining>0;Bt++)un.push(await Qe(Ke[Bt],xt+1,Fe));if(Wt){const Bt=Fe!==-1&&xt+1>=Fe;un.push({type:"elided",count:Wt.count,isExact:Bt,parentId:Wt.parentId,rangeFrom:Wt.rangeFrom,rangeTo:Wt.rangeTo,totalSiblings:Wt.totalSiblings})}const Nt=Ke.length-ct;for(let Bt=Nt;Bt<Ke.length&&mt.remaining>0;Bt++)un.push(await Qe(Ke[Bt],xt+1,Fe))}else for(let ut=0;ut<Ke.length;ut++){if(mt.remaining<=0){const ct=Ke.length-ut;un.push({type:"elided",count:ct,isExact:!1,parentId:We._id,rangeFrom:ut,rangeTo:Ke.length-1,totalSiblings:Ke.length});break}un.push(await Qe(Ke[ut],xt+1,Fe))}}return{rem:Kt,children:un,folded:Ge}}const wt=await Qe(ve,0,R);await ve.getParentRem()||(wt.rem.isTopLevel=!0);const Be=ie(wt),Xe={rootId:E,depth:R,nodeCount:Ae,outline:Be,nodeRemIds:Me},nt=Math.min(Math.max(Q,0),10);if(nt>0){const We=[];let xt=ve;for(let Fe=0;Fe<nt;Fe++){const Je=await xt.getParentRem();if(!Je)break;const[Ke,Zt,Ge]=await Promise.all([Pt(w,Je.text??[]),Je.getChildrenRem(),Je.isDocument()]);We.push({id:Je._id,name:te(Ke),childrenCount:Zt.length,isDocument:Ge}),xt=Je}if(We.length>0){const Fe=We[We.length-1];await xt.getParentRem()||(Fe.isTopLevel=!0),Xe.ancestors=We}}return!G&&(Ne>0||he>0)&&(Xe.powerupFiltered={tags:Ne,children:he}),Xe}async function ge(w,y){const E=await q(w,y),R=y.includePowerup??!1,N={};return await Promise.all(E.nodeRemIds.map(async F=>{const Q=await w.rem.findOne(F);Q&&(N[F]=await Oe(w,Q,{includePowerup:R}))})),{...E,remObjects:N}}async function C(w,y){const{depth:E=-1,maxNodes:R=200,maxSiblings:N=20}=y,Q=(await w.rem.getAll()).filter(Pe=>Pe.parent===null),G=await Promise.all(Q.map(Pe=>Pe.isDocument())),ve=Q.filter((Pe,Be)=>G[Be]);let Ae=0;const Ne={remaining:R};async function he(Pe,Be,Xe){Ae++,Ne.remaining--;const nt=await Pe.getChildrenRem(),We=await He(nt),xt=await Promise.all(We.map(ut=>ut.isDocument())),Fe=We.filter((ut,ct)=>xt[ct]),Je=We.length-Fe.length,Zt=Xe!==-1&&Be>=Xe&&Fe.length>0,Ge=Pe.type===6,[Kt,un]=await Promise.all([Pt(w,Pe.text??[]),Ge?Pe.getPortalDirectlyIncludedRem():Promise.resolve([])]),pn=ue({id:Pe._id,markdownText:Kt.replace(/\n/g," "),childrenCount:We.length,isDocument:!0,isTopLevel:Pe.parent===null,isPortal:Ge,...Ge?{type:"portal",portalRefs:un.map(ut=>ut._id)}:{}}),Wt=[];if(!Zt&&Fe.length>0){const{visibleIndices:ut,elided:ct}=me(Fe.length,N,Pe._id);if(ut){const{head:Nt,tail:Bt}=ut;for(let Dt=0;Dt<Nt&&Ne.remaining>0;Dt++)Wt.push(await he(Fe[Dt],Be+1,Xe));if(ct){const Dt=Xe!==-1&&Be+1>=Xe;Wt.push({type:"elided",count:ct.count,isExact:Dt,parentId:ct.parentId,rangeFrom:ct.rangeFrom,rangeTo:ct.rangeTo,totalSiblings:ct.totalSiblings})}const hn=Fe.length-Bt;for(let Dt=hn;Dt<Fe.length&&Ne.remaining>0;Dt++)Wt.push(await he(Fe[Dt],Be+1,Xe))}else for(let Nt=0;Nt<Fe.length;Nt++){if(Ne.remaining<=0){const Bt=Fe.length-Nt;Wt.push({type:"elided",count:Bt,isExact:!1,parentId:Pe._id,rangeFrom:Nt,rangeTo:Fe.length-1,totalSiblings:Fe.length});break}Wt.push(await he(Fe[Nt],Be+1,Xe))}}return{rem:pn,children:Wt,folded:Zt}}const mt=[],{visibleIndices:Me,elided:Qe}=me(ve.length,N,"root");if(Me){const{head:Pe,tail:Be}=Me;for(let nt=0;nt<Pe&&Ne.remaining>0;nt++)mt.push(await he(ve[nt],0,E));Qe&&mt.push({type:"elided",count:Qe.count,isExact:!1,parentId:"root",rangeFrom:Qe.rangeFrom,rangeTo:Qe.rangeTo,totalSiblings:Qe.totalSiblings});const Xe=ve.length-Be;for(let nt=Xe;nt<ve.length&&Ne.remaining>0;nt++)mt.push(await he(ve[nt],0,E))}else for(let Pe=0;Pe<ve.length;Pe++){if(Ne.remaining<=0){const Be=ve.length-Pe;mt.push({type:"elided",count:Be,isExact:!1,parentId:"root",rangeFrom:Pe,rangeTo:ve.length-1,totalSiblings:ve.length});break}mt.push(await he(ve[Pe],0,E))}const wt=["<!-- globe: \u77E5\u8BC6\u5E93\u6982\u89C8 -->"];for(const Pe of mt)Z(Pe)?wt.push(ee(Pe)):wt.push(ie(Pe));return{nodeCount:Ae,outline:wt.join(`
60
- `)}}async function V(w,y){const E=[];let R=y;for(;R;){const N=await Pt(w,R.text??[]);E.unshift(N.replace(/\n/g," ").trim()||R._id),R=await R.getParentRem()}return E}async function L(w,y){const{mode:E="focus",ancestorLevels:R=2,maxNodes:N=200,maxSiblings:F=20,depth:Q=3,focusRemId:G}=y;if(E==="page"){if(G)throw new Error("focusRemId \u4EC5\u5728 focus \u6A21\u5F0F\u4E0B\u6709\u6548\uFF0Cpage \u6A21\u5F0F\u4E0B\u8BF7\u52FF\u6307\u5B9A");return A(w,{maxNodes:N,maxSiblings:F,depth:Q})}return $(w,{ancestorLevels:R,maxNodes:N,maxSiblings:F,focusRemId:G})}async function A(w,y){const E=await w.window.getFocusedPaneId();if(!E)throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u9762\u677F ID\uFF0C\u8BF7\u786E\u4FDD\u6709\u6253\u5F00\u7684\u9875\u9762");const R=await w.window.getOpenPaneRemId(E);if(!R)throw new Error("\u5F53\u524D\u9762\u677F\u6CA1\u6709\u6253\u5F00\u4EFB\u4F55 Rem");const N=await w.rem.findOne(R);if(!N)throw new Error(`Page Rem not found: ${R}`);const F=await V(w,N);let Q=0;const G={remaining:y.maxNodes},ve=await U(w,N,0,y.depth,y.maxSiblings,G);Q=y.maxNodes-G.remaining;const Ne=`<!-- page: ${F[F.length-1]||R} -->
60
+ `)}}async function V(w,y){const E=[];let R=y;for(;R;){const N=await Pt(w,R.text??[]);E.unshift(N.replace(/\n/g," ").trim()||R._id),R=await R.getParentRem()}return E}async function L(w,y){const{mode:E="page",ancestorLevels:R=2,maxNodes:N=200,maxSiblings:F=20,depth:Q=3,focusRemId:G}=y;if(E==="page"){if(G)throw new Error("focusRemId \u4EC5\u5728 focus \u6A21\u5F0F\u4E0B\u6709\u6548\uFF0Cpage \u6A21\u5F0F\u4E0B\u8BF7\u52FF\u6307\u5B9A");return A(w,{maxNodes:N,maxSiblings:F,depth:Q})}return $(w,{ancestorLevels:R,maxNodes:N,maxSiblings:F,focusRemId:G})}async function A(w,y){const E=await w.window.getFocusedPaneId();if(!E)throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u9762\u677F ID\uFF0C\u8BF7\u786E\u4FDD\u6709\u6253\u5F00\u7684\u9875\u9762");const R=await w.window.getOpenPaneRemId(E);if(!R)throw new Error("\u5F53\u524D\u9762\u677F\u6CA1\u6709\u6253\u5F00\u4EFB\u4F55 Rem");const N=await w.rem.findOne(R);if(!N)throw new Error(`Page Rem not found: ${R}`);const F=await V(w,N);let Q=0;const G={remaining:y.maxNodes},ve=await U(w,N,0,y.depth,y.maxSiblings,G);Q=y.maxNodes-G.remaining;const Ne=`<!-- page: ${F[F.length-1]||R} -->
61
61
  <!-- path: ${F.join(" > ")} -->`+`
62
62
  `+ie(ve);return{nodeCount:Q,outline:Ne,breadcrumb:F,mode:"page"}}async function $(w,y){let E;if(y.focusRemId){if(E=await w.rem.findOne(y.focusRemId),!E)throw new Error(`\u6307\u5B9A\u7684 Rem \u4E0D\u5B58\u5728: ${y.focusRemId}`)}else if(E=await w.focus.getFocusedRem(),!E)throw new Error("\u5F53\u524D\u6CA1\u6709\u805A\u7126\u7684 Rem\uFF0C\u8BF7\u5148\u5728 RemNote \u4E2D\u70B9\u51FB\u4E00\u4E2A Rem");const R=await V(w,E),N=[E];let F=E;for(let Me=0;Me<y.ancestorLevels;Me++){const Qe=await F.getParentRem();if(!Qe)break;N.unshift(Qe),F=Qe}const Q={remaining:y.maxNodes};let G=0;const ve=N[0],Ae=await H(w,ve,N,0,E._id,y.maxSiblings,Q);G=y.maxNodes-Q.remaining;const Ne=R[R.length-1]||E._id,mt=`<!-- path: ${R.join(" > ")} -->
63
63
  <!-- focus: ${Ne} (${E._id}) -->`+`
@@ -42,7 +42,7 @@ export async function readContext(
42
42
  payload: ReadContextPayload,
43
43
  ): Promise<ReadContextResult> {
44
44
  const {
45
- mode = 'focus',
45
+ mode = 'page',
46
46
  ancestorLevels = 2,
47
47
  maxNodes = 200,
48
48
  maxSiblings = 20,
@@ -97,13 +97,20 @@ export type PropertyTypeValue =
97
97
 
98
98
  // ─── RemObject 主体 ─────────────────────────────────────────
99
99
 
100
+ /**
101
+ * RemObject 字段注释标记说明:
102
+ * - [R] 只读 / [RW] 可读写 / [R-F] 只读+默认过滤
103
+ * - 🛡️D2 = 参与防线2语义比较(变化 → 硬拒绝)
104
+ * - ⚠️D2 = 敏感元数据(变化 → 放行 + 专门警告)
105
+ * - ⊘D2 = 普通元数据(变化 → 放行 + 统一警告)
106
+ */
100
107
  export interface RemObject {
101
108
 
102
109
  // ══════════════════════════════════════════════════════════
103
110
  // 核心标识
104
111
  // ══════════════════════════════════════════════════════════
105
112
 
106
- /** [R] Rem 唯一 ID。SDK 直接属性 _id */
113
+ /** [R] ⊘D2 Rem 唯一 ID。SDK 直接属性 _id */
107
114
  id: string;
108
115
 
109
116
  // ══════════════════════════════════════════════════════════
@@ -111,12 +118,12 @@ export interface RemObject {
111
118
  // ══════════════════════════════════════════════════════════
112
119
 
113
120
  /**
114
- * [RW] ✅ 正面文本(RichText 数组)。SDK: text / setText()
121
+ * [RW] 🛡️D2 ✅ 正面文本(RichText 数组)。SDK: text / setText()
115
122
  * UI 行为:文本内容立即更新显示,无格式副作用
116
123
  */
117
124
  text: RichText;
118
125
  /**
119
- * [RW] ✅ 背面文本。SDK: backText / setBackText()
126
+ * [RW] 🛡️D2 ✅ 背面文本。SDK: backText / setBackText()
120
127
  * UI 行为:设值后 Rem 显示为 "正面文本 → 背面文本" 格式(箭头分隔符)
121
128
  * 默认 null(无背面);设值即产生闪卡正反面结构
122
129
  * 写入语义:null → setBackText([])(SDK 接受 undefined | RichTextInterface,空数组清除背面)
@@ -129,14 +136,14 @@ export interface RemObject {
129
136
  // ══════════════════════════════════════════════════════════
130
137
 
131
138
  /**
132
- * [RW] ✅ Rem 类型。SDK: type, getType() / setType(SetRemType)
139
+ * [RW] 🛡️D2 ✅ Rem 类型。SDK: type, getType() / setType(SetRemType)
133
140
  * UI 行为:CONCEPT → 文字变粗体;DESCRIPTOR → 保持正常字重(与默认无视觉差异)
134
141
  * SetRemType 不含 PORTAL(6),Portal 只能通过 createPortal() 创建
135
142
  * 底层机制:纯字段修改,不涉及 Powerup Tag 注入或隐藏子 Rem
136
143
  */
137
144
  type: RemTypeValue;
138
145
  /**
139
- * [RW] ✅ 是否作为独立文档页面打开。SDK: isDocument() / setIsDocument()
146
+ * [RW] 🛡️D2 ✅ 是否作为独立文档页面打开。SDK: isDocument() / setIsDocument()
140
147
  * UI 行为:bullet (•) 变为文档页面图标(小方块),Rem 可作为独立页面打开
141
148
  * 独立于 type,CONCEPT Rem 可以同时是 Document
142
149
  * 底层机制:Powerup — 注入"文档" Tag + 自动创建 [Status];;[Draft] descriptor 子 Rem
@@ -148,11 +155,12 @@ export interface RemObject {
148
155
  // ══════════════════════════════════════════════════════════
149
156
 
150
157
  /**
151
- * [RW] ✅ 父 Rem ID。null 表示顶级。SDK: parent / setParent(parent, position?)
158
+ * [RW] ⚠️D2 ✅ 父 Rem ID。null 表示顶级。SDK: parent / setParent(parent, position?)
152
159
  * UI 行为:Rem 从原位置消失,出现在新父级的子列表中
160
+ * 防线2:结构操作后常变化,放行但输出专门警告
153
161
  */
154
162
  parent: string | null;
155
- /** [R] 子 Rem ID 有序数组。SDK 直接属性 children(修改子的 parent 间接改变) */
163
+ /** [R] ⊘D2 子 Rem ID 有序数组。SDK 直接属性 children(修改子的 parent 间接改变) */
156
164
  children: string[];
157
165
 
158
166
  // ══════════════════════════════════════════════════════════
@@ -160,14 +168,14 @@ export interface RemObject {
160
168
  // ══════════════════════════════════════════════════════════
161
169
 
162
170
  /**
163
- * [RW] ✅ 标题大小。SDK: getFontSize() / setFontSize()
171
+ * [RW] 🛡️D2 ✅ 标题大小。SDK: getFontSize() / setFontSize()
164
172
  * UI 行为:H1 → 超大粗体;H2 → 大粗体(略小于 H1);H3 → 中粗体
165
173
  * 默认 null(普通大小);setFontSize(undefined) 恢复
166
174
  * 底层机制:Powerup — 注入"标题" Tag + 创建 [Size];;[H1/H2/H3] descriptor 子 Rem
167
175
  */
168
176
  fontSize: FontSize | null;
169
177
  /**
170
- * [RW] ✅ 高亮颜色。SDK: getHighlightColor() / setHighlightColor()
178
+ * [RW] 🛡️D2 ✅ 高亮颜色。SDK: getHighlightColor() / setHighlightColor()
171
179
  * UI 行为:整行背景变为对应颜色(Red→粉红、Blue→浅蓝),bullet 也着色
172
180
  * 默认 null(无高亮)
173
181
  * SDK 注意:setHighlightColor() 只能设置颜色,不能清除(null/undefined 均被拒绝)
@@ -181,47 +189,47 @@ export interface RemObject {
181
189
  // ══════════════════════════════════════════════════════════
182
190
 
183
191
  /**
184
- * [RW] ✅ 是否待办。SDK: isTodo() / setIsTodo()
192
+ * [RW] 🛡️D2 ✅ 是否待办。SDK: isTodo() / setIsTodo()
185
193
  * UI 行为:文本前出现空心 checkbox(☐);副作用:todoStatus 自动初始化为 "Unfinished"
186
194
  * 底层机制:Powerup — 注入"待办" Tag + 自动创建 [Status];;[Unfinished] descriptor 子 Rem
187
195
  */
188
196
  isTodo: boolean;
189
197
  /**
190
- * [RW] ✅ 待办完成状态。SDK: getTodoStatus() / setTodoStatus()
198
+ * [RW] 🛡️D2 ✅ 待办完成状态。SDK: getTodoStatus() / setTodoStatus()
191
199
  * UI 行为:Finished → checkbox 变蓝色已勾选(☑)+ 文本加删除线
192
200
  * 前提:需先 setIsTodo(true),否则无意义
193
201
  * 写入语义:null → 跳过(清除 todo 状态应通过 isTodo=false 实现,SDK 不接受 null)
194
202
  */
195
203
  todoStatus: TodoStatus | null;
196
204
  /**
197
- * [RW] ✅ 是否代码块。SDK: isCode() / setIsCode()
205
+ * [RW] 🛡️D2 ✅ 是否代码块。SDK: isCode() / setIsCode()
198
206
  * UI 行为:Rem 变为代码块容器——等宽字体、灰色背景、块级缩进
199
207
  * 底层机制:Powerup — 注入"代码" Tag,无参数子 Rem
200
208
  */
201
209
  isCode: boolean;
202
210
  /**
203
- * [RW] ✅ 是否引用块。SDK: isQuote() / setIsQuote()
211
+ * [RW] 🛡️D2 ✅ 是否引用块。SDK: isQuote() / setIsQuote()
204
212
  * UI 行为:左侧出现灰色竖线 + 行背景变浅灰(经典 blockquote 样式)
205
213
  * 底层机制:Powerup — 注入"引用" Tag,无参数子 Rem
206
214
  */
207
215
  isQuote: boolean;
208
216
  /**
209
- * [RW] ✅ 是否列表项。SDK: isListItem() / setIsListItem()
217
+ * [RW] 🛡️D2 ✅ 是否列表项。SDK: isListItem() / setIsListItem()
210
218
  * UI 行为:bullet (•) 变为数字编号 "1."(有序列表样式)
211
219
  * 底层机制:Powerup — 注入"列表项" Tag,无参数子 Rem
212
220
  */
213
221
  isListItem: boolean;
214
222
  /**
215
- * [RW] ✅ 是否卡片项。SDK: isCardItem() / setIsCardItem()
223
+ * [RW] 🛡️D2 ✅ 是否卡片项。SDK: isCardItem() / setIsCardItem()
216
224
  * UI:无明显变化。功能:标记 Rem 以卡片样式显示(类似看板布局),
217
225
  * 而非默认项目符号列表,在 RemNote 的 Card View 中生效
218
226
  * 底层机制:Powerup — 注入"卡片条目" Tag (MultiLineCard),无参数子 Rem
219
227
  */
220
228
  isCardItem: boolean;
221
- /** [R] 是否表格。SDK: isTable()(无 setIsTable,只有 setTableFilter) */
229
+ /** [R] 🛡️D2 是否表格。SDK: isTable()(无 setIsTable,只有 setTableFilter) */
222
230
  isTable: boolean;
223
231
  /**
224
- * [RW] ✅ 是否 Powerup 插槽。SDK: isSlot() / setIsSlot()
232
+ * [RW] 🛡️D2 ✅ 是否 Powerup 插槽。SDK: isSlot() / setIsSlot()
225
233
  * UI:bullet 变为方形图标(☐)。功能:标记 Rem 为 Powerup 的数据插槽(slot),
226
234
  * Powerup 注册时通过 slots 配置定义,用于存储键值对数据(值为 RichText)。
227
235
  * 通过 getPowerupProperty(code, slot) / setPowerupProperty() 读写
@@ -232,7 +240,7 @@ export interface RemObject {
232
240
  */
233
241
  isSlot: boolean;
234
242
  /**
235
- * [RW] ✅ 是否 Tag 属性(表格列)。SDK: isProperty() / setIsProperty()
243
+ * [RW] 🛡️D2 ✅ 是否 Tag 属性(表格列)。SDK: isProperty() / setIsProperty()
236
244
  * UI:bullet 变为方形图标(☐,与 isSlot 相同)。功能:标记 Rem 为父级 Tag 的
237
245
  * 结构化属性列,可通过 getPropertyType() 指定数据类型(text/number/date/checkbox/
238
246
  * single_select/multi_select/url/image 等),通过 getTagPropertyValue(propertyId) /
@@ -240,31 +248,31 @@ export interface RemObject {
240
248
  * 底层机制:与 isSlot 完全相同 — 注入同一个"模板插槽" Tag (vD8KGEg5dkj9bzkRn)
241
249
  */
242
250
  isProperty: boolean;
243
- /** [R-F] 是否 Powerup。SDK: isPowerup()(写入用 addPowerup/removePowerup,参数化)。Powerup 系统标识 */
251
+ /** [R-F] ⊘D2 是否 Powerup。SDK: isPowerup()(写入用 addPowerup/removePowerup,参数化)。Powerup 系统标识 */
244
252
  isPowerup: boolean;
245
- /** [R-F] 是否 Powerup 枚举。SDK: isPowerupEnum()。Powerup 细分类型 */
253
+ /** [R-F] ⊘D2 是否 Powerup 枚举。SDK: isPowerupEnum()。Powerup 细分类型 */
246
254
  isPowerupEnum: boolean;
247
- /** [R-F] 是否 Powerup 属性。SDK: isPowerupProperty()。Powerup 细分类型 */
255
+ /** [R-F] ⊘D2 是否 Powerup 属性。SDK: isPowerupProperty()。Powerup 细分类型 */
248
256
  isPowerupProperty: boolean;
249
- /** [R-F] 是否 Powerup 属性列表项。SDK: isPowerupPropertyListItem()。Powerup 细分类型 */
257
+ /** [R-F] ⊘D2 是否 Powerup 属性列表项。SDK: isPowerupPropertyListItem()。Powerup 细分类型 */
250
258
  isPowerupPropertyListItem: boolean;
251
- /** [R-F] 是否 Powerup 插槽。SDK: isPowerupSlot()。Powerup 细分类型 */
259
+ /** [R-F] ⊘D2 是否 Powerup 插槽。SDK: isPowerupSlot()。Powerup 细分类型 */
252
260
  isPowerupSlot: boolean;
253
261
 
254
262
  // ══════════════════════════════════════════════════════════
255
263
  // Portal 专用
256
264
  // ══════════════════════════════════════════════════════════
257
265
 
258
- /** [R] Portal 子类型。仅 type === 'portal' 时有值。SDK: getPortalType() */
266
+ /** [R] 🛡️D2 Portal 子类型。仅 type === 'portal' 时有值。SDK: getPortalType() */
259
267
  portalType: PortalType | null;
260
- /** [Portal-W] Portal 直接包含的 Rem ID 数组。SDK: getPortalDirectlyIncludedRem() / addToPortal() / removeFromPortal()。写入时使用 diff 机制。仅 type === 'portal' 时可修改 */
268
+ /** [Portal-W] 🛡️D2 Portal 直接包含的 Rem ID 数组。SDK: getPortalDirectlyIncludedRem() / addToPortal() / removeFromPortal()。写入时使用 diff 机制。仅 type === 'portal' 时可修改 */
261
269
  portalDirectlyIncludedRem: string[];
262
270
 
263
271
  // ══════════════════════════════════════════════════════════
264
272
  // 属性类型(当此 Rem 是 tag/powerup 的属性时)
265
273
  // ══════════════════════════════════════════════════════════
266
274
 
267
- /** [R] 属性数据类型。SDK: getPropertyType() */
275
+ /** [R] 🛡️D2 属性数据类型。SDK: getPropertyType() */
268
276
  propertyType: PropertyTypeValue | null;
269
277
 
270
278
  // ══════════════════════════════════════════════════════════
@@ -272,14 +280,14 @@ export interface RemObject {
272
280
  // ══════════════════════════════════════════════════════════
273
281
 
274
282
  /**
275
- * [RW] ✅ 是否启用间隔重复练习。SDK: getEnablePractice() / setEnablePractice()
283
+ * [RW] 🛡️D2 ✅ 是否启用间隔重复练习。SDK: getEnablePractice() / setEnablePractice()
276
284
  * UI:无明显变化。功能:为 true 时,RemNote 根据 Rem 的 text/backText 结构
277
285
  * 自动生成闪卡并纳入间隔重复调度。setType(CONCEPT) 可能自动置为 true
278
286
  * 底层机制:纯字段修改,不涉及 Powerup Tag 注入或隐藏子 Rem
279
287
  */
280
288
  enablePractice: boolean;
281
289
  /**
282
- * [RW] ✅ 闪卡练习方向。SDK: getPracticeDirection() / setPracticeDirection()
290
+ * [RW] 🛡️D2 ✅ 闪卡练习方向。SDK: getPracticeDirection() / setPracticeDirection()
283
291
  * UI:无明显变化。功能:控制闪卡生成方向——forward=正面→背面,
284
292
  * backward=背面→正面,both=双向生成,none=不生成闪卡
285
293
  * 底层机制:纯字段修改,不涉及 Powerup Tag 注入或隐藏子 Rem
@@ -291,7 +299,7 @@ export interface RemObject {
291
299
  // ══════════════════════════════════════════════════════════
292
300
 
293
301
  /**
294
- * [RW] ✅ 标签 Rem ID 数组。SDK: getTagRems() / addTag() / removeTag()
302
+ * [RW] 🛡️D2 ✅ 标签 Rem ID 数组。SDK: getTagRems() / addTag() / removeTag()
295
303
  * UI 行为:行右侧出现标签徽章(圆角矩形,显示标签名 + × 删除按钮)
296
304
  * setType(CONCEPT) 等操作可能自动添加系统标签
297
305
  * 注意:系统 Powerup Tag 会混入此数组(如 setIsCode 注入的"代码" Tag),
@@ -299,12 +307,12 @@ export interface RemObject {
299
307
  */
300
308
  tags: string[];
301
309
  /**
302
- * [RW] ✅ 来源 Rem ID 数组。SDK: getSources() / addSource() / removeSource()
310
+ * [RW] 🛡️D2 ✅ 来源 Rem ID 数组。SDK: getSources() / addSource() / removeSource()
303
311
  * UI 行为:Rem 下方出现来源引用子元素(灰色圆角框,显示来源 Rem 名 + ↗ 图标)
304
312
  */
305
313
  sources: string[];
306
314
  /**
307
- * [R] 别名 Rem ID 数组。SDK: getAliases()
315
+ * [R] 🛡️D2 别名 Rem ID 数组。SDK: getAliases()
308
316
  * 写入接口 getOrCreateAliasWithText(text) 需要文本参数(非 ID),与 RemObject 的 ID 数组形式不匹配。
309
317
  * v1 标记为只读,后续可提供独立命令 `add-alias <remId> <text>`
310
318
  */
@@ -314,37 +322,37 @@ export interface RemObject {
314
322
  // 关联 — 引用关系(ID 数组)
315
323
  // ══════════════════════════════════════════════════════════
316
324
 
317
- /** [R] 本 Rem 引用的其他 Rem ID 数组。SDK: remsBeingReferenced() */
325
+ /** [R] 🛡️D2 本 Rem 引用的其他 Rem ID 数组。SDK: remsBeingReferenced() */
318
326
  remsBeingReferenced: string[];
319
- /** [R-F] 本 Rem 深层引用的 Rem ID 数组。SDK: deepRemsBeingReferenced()。可由 remsBeingReferenced 递归获取 */
327
+ /** [R-F] ⊘D2 本 Rem 深层引用的 Rem ID 数组。SDK: deepRemsBeingReferenced()。可由 remsBeingReferenced 递归获取 */
320
328
  deepRemsBeingReferenced: string[];
321
- /** [R] 引用本 Rem 的 Rem ID 数组(反向链接)。SDK: remsReferencingThis() */
329
+ /** [R] ⊘D2 引用本 Rem 的 Rem ID 数组(反向链接)。SDK: remsReferencingThis() */
322
330
  remsReferencingThis: string[];
323
331
 
324
332
  // ══════════════════════════════════════════════════════════
325
333
  // 关联 — 标签体系(ID 数组)
326
334
  // ══════════════════════════════════════════════════════════
327
335
 
328
- /** [R] 被本 Rem 标记的 Rem ID 数组(本 Rem 作为 tag 时)。SDK: taggedRem() */
336
+ /** [R] ⊘D2 被本 Rem 标记的 Rem ID 数组(本 Rem 作为 tag 时)。SDK: taggedRem() */
329
337
  taggedRem: string[];
330
- /** [R-F] 祖先标签 Rem ID 数组。SDK: ancestorTagRem()。标签继承链,低频 */
338
+ /** [R-F] ⊘D2 祖先标签 Rem ID 数组。SDK: ancestorTagRem()。标签继承链,低频 */
331
339
  ancestorTagRem: string[];
332
- /** [R-F] 后代标签 Rem ID 数组。SDK: descendantTagRem()。标签继承链,低频 */
340
+ /** [R-F] ⊘D2 后代标签 Rem ID 数组。SDK: descendantTagRem()。标签继承链,低频 */
333
341
  descendantTagRem: string[];
334
342
 
335
343
  // ══════════════════════════════════════════════════════════
336
344
  // 关联 — 层级遍历(ID 数组)
337
345
  // ══════════════════════════════════════════════════════════
338
346
 
339
- /** [R] 所有后代 Rem ID 数组。SDK: getDescendants() */
347
+ /** [R] ⊘D2 所有后代 Rem ID 数组。SDK: getDescendants() */
340
348
  descendants: string[];
341
- /** [R] 兄弟 Rem ID 数组。SDK: siblingRem() */
349
+ /** [R] ⊘D2 兄弟 Rem ID 数组。SDK: siblingRem() */
342
350
  siblingRem: string[];
343
- /** [R-F] 包含的 Portal 和文档 Rem ID 数组。SDK: portalsAndDocumentsIn()。使用场景有限 */
351
+ /** [R-F] ⊘D2 包含的 Portal 和文档 Rem ID 数组。SDK: portalsAndDocumentsIn()。使用场景有限 */
344
352
  portalsAndDocumentsIn: string[];
345
- /** [R-F] 文档/Portal 中所有 Rem ID 数组。SDK: allRemInDocumentOrPortal()。可能数据量大 */
353
+ /** [R-F] ⊘D2 文档/Portal 中所有 Rem ID 数组。SDK: allRemInDocumentOrPortal()。可能数据量大 */
346
354
  allRemInDocumentOrPortal: string[];
347
- /** [R-F] 文件夹队列中的 Rem ID 数组。SDK: allRemInFolderQueue()。场景有限 */
355
+ /** [R-F] ⊘D2 文件夹队列中的 Rem ID 数组。SDK: allRemInFolderQueue()。场景有限 */
348
356
  allRemInFolderQueue: string[];
349
357
 
350
358
  // ══════════════════════════════════════════════════════════
@@ -352,36 +360,36 @@ export interface RemObject {
352
360
  // ══════════════════════════════════════════════════════════
353
361
 
354
362
  /**
355
- * [RW] ✅ 在兄弟间的位置(0 起始)。SDK: positionAmongstSiblings() / setParent(parent, position)
363
+ * [RW] ⊘D2 ✅ 在兄弟间的位置(0 起始)。SDK: positionAmongstSiblings() / setParent(parent, position)
356
364
  * UI 行为:Rem 在父级子列表中的显示位置改变(测试:A→B→C 变为 B→C→A)
357
365
  * position 超过实际数量会被钳位到末尾;位置相对于父 Rem 的全部 children
358
366
  */
359
367
  positionAmongstSiblings: number | null;
360
- /** [R-F] 搜索中被选次数。SDK: timesSelectedInSearch()。统计数据,低频 */
368
+ /** [R-F] ⊘D2 搜索中被选次数。SDK: timesSelectedInSearch()。统计数据,低频 */
361
369
  timesSelectedInSearch: number;
362
- /** [R-F] 上次移动时间(毫秒时间戳)。SDK: getLastTimeMovedTo()。过于细粒度 */
370
+ /** [R-F] ⊘D2 上次移动时间(毫秒时间戳)。SDK: getLastTimeMovedTo()。过于细粒度 */
363
371
  lastTimeMovedTo: number;
364
- /** [R-F] Schema 版本号。SDK: getSchemaVersion()。内部版本号 */
372
+ /** [R-F] ⊘D2 Schema 版本号。SDK: getSchemaVersion()。内部版本号 */
365
373
  schemaVersion: number;
366
374
 
367
375
  // ══════════════════════════════════════════════════════════
368
376
  // 队列视图
369
377
  // ══════════════════════════════════════════════════════════
370
378
 
371
- /** [R-F] 嵌入式队列视图模式。SDK: embeddedQueueViewMode()。场景有限 */
379
+ /** [R-F] ⊘D2 嵌入式队列视图模式。SDK: embeddedQueueViewMode()。场景有限 */
372
380
  embeddedQueueViewMode: boolean;
373
381
 
374
382
  // ══════════════════════════════════════════════════════════
375
383
  // 元数据 / 时间戳
376
384
  // ══════════════════════════════════════════════════════════
377
385
 
378
- /** [R] 创建时间(毫秒时间戳)。SDK 直接属性 createdAt */
386
+ /** [R] ⊘D2 创建时间(毫秒时间戳)。SDK 直接属性 createdAt */
379
387
  createdAt: number;
380
- /** [R] 最后更新时间(毫秒时间戳)。SDK 直接属性 updatedAt */
388
+ /** [R] ⊘D2 最后更新时间(毫秒时间戳)。SDK 直接属性 updatedAt */
381
389
  updatedAt: number;
382
- /** [R-F] 本地最后更新时间(毫秒时间戳)。SDK 直接属性 localUpdatedAt。与 updatedAt 重叠 */
390
+ /** [R-F] ⊘D2 本地最后更新时间(毫秒时间戳)。SDK 直接属性 localUpdatedAt。与 updatedAt 重叠 */
383
391
  localUpdatedAt: number;
384
- /** [R-F] 上次练习时间(毫秒时间戳)。SDK: getLastPracticed()。间隔重复内部数据 */
392
+ /** [R-F] ⊘D2 上次练习时间(毫秒时间戳)。SDK: getLastPracticed()。间隔重复内部数据 */
385
393
  lastPracticed: number;
386
394
  }
387
395
 
@@ -169,8 +169,8 @@ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的
169
169
  { "i": "m", "iUrl": "https://example.com", "text": "点击访问" }
170
170
  // 红色高亮 + 粗体(h 是数字,不是字符串)
171
171
  { "b": true, "h": 1, "i": "m", "text": "重点" }
172
- // 完形填空
173
- { "cId": "cloze1", "i": "m", "text": "答案内容" }
172
+ // 完形填空(⚠️ 创建填空优先用 edit-tree 的 {{文本}} 语法,SDK 自动生成安全 cId。edit-rem 仅修改已有 cloze,禁止手动编造 cId)
173
+ { "cId": "8291740362058173", "i": "m", "text": "答案内容" }
174
174
  // Rem 引用(_id 排在所有小写 key 之前)
175
175
  { "_id": "remId", "i": "q" }
176
176
  // Rem 引用加粗
@@ -196,7 +196,7 @@ Portal 的编辑同步意味着修改一处会影响另一处。Portal 引用的
196
196
 
197
197
  #### 序列化确定性
198
198
 
199
- RichText 对象内部按 **key 字母序排列**(`sortRichTextKeys()`),确保 JSON 始终一致。`_id` 中的 `_`(U+005F)排在所有小写字母(`a`=U+0061)之前。防线 2(乐观并发检测)依赖此确定性序列化来比较缓存与最新数据。
199
+ RichText 对象内部按 **key 字母序排列**(`sortRichTextKeys()`),确保 JSON 始终一致。`_id` 中的 `_`(U+005F)排在所有小写字母(`a`=U+0061)之前。防线 2(语义并发检测)依赖此确定性序列化来逐字段比较缓存与最新数据。
200
200
 
201
201
  ### Powerup 机制与噪音过滤
202
202
 
@@ -232,12 +232,12 @@ RemNote 格式设置(fontSize、highlightColor 等)底层通过 Powerup 机
232
232
  | 场景 | 命令 | 特点 |
233
233
  |:-----|:-----|:-----|
234
234
  | 知识库有什么 | `read-globe` | 仅 Document 层级,**无缓存**,最小序列化(无 backText/箭头) |
235
- | 我在编辑什么 | `read-context --mode focus` | 鱼眼视图(焦点 depth=3,siblings depth=1,叔伯 depth=0),**无缓存** |
236
- | 当前页面内容 | `read-context --mode page` | 均匀展开,**无缓存** |
235
+ | 当前页面内容 | `read-context` | 均匀展开(默认,推荐——只需有打开页面即可),**无缓存** |
236
+ | 我光标在哪 / 正在编辑的 Rem | `read-context --mode focus` | 鱼眼视图(需光标在某 Rem 上,否则报错),**无缓存** |
237
237
  | 展开某主题细节 | `read-tree <id>` | 完整子树,**有缓存**供 edit-tree |
238
238
  | 展开子树 + 每个节点属性 | `read-rem-in-tree <id>` | 大纲 + RemObject,**双重缓存**供 edit-tree 和 edit-rem |
239
239
 
240
- **重要**:read-globe、read-context、search 都**不写入缓存**,不能替代 read-tree/read-rem/read-rem-in-tree 作为 edit 的前置条件。read-context 需要用户在 RemNote 中有焦点(focus 模式)或打开页面(page 模式)。
240
+ **重要**:read-globe、read-context、search 都**不写入缓存**,不能替代 read-tree/read-rem/read-rem-in-tree 作为 edit 的前置条件。read-context 需要用户在 RemNote 中有打开页面(page 模式,默认)或有焦点(focus 模式)。
241
241
 
242
242
  ### 修改:用户想改什么?
243
243
 
@@ -409,9 +409,14 @@ search 调用 RemNote SDK 官方搜索方法,其分词基于空格分割。**
409
409
 
410
410
  必须先 read 再 edit。未缓存的 Rem 不允许编辑。
411
411
 
412
- ### 防线 2:乐观并发检测
412
+ ### 防线 2:语义并发检测(三层字段分类)
413
413
 
414
- edit 时从 SDK 重新读取最新数据,与缓存严格比较。被外部修改则拒绝编辑且**不更新缓存**——迫使 Agent 重新 read。
414
+ edit 时从 SDK 重新读取最新数据,逐字段与缓存比较并按三层分类处理:
415
+ - **语义字段**(text/type/tags/highlightColor 等)变化 → **硬拒绝**,不更新缓存,迫使 Agent 重新 read
416
+ - **敏感元数据**(parent)变化 → **放行** + warnings 返回 `"⚠️ parent has changed (was: X, now: Y)..."`,静默刷新缓存
417
+ - **普通元数据**(positionAmongstSiblings/updatedAt/siblingRem 等)变化 → **放行** + warnings 返回 `"ℹ️ Metadata fields changed since last read: ..."`,静默刷新缓存
418
+
419
+ 这意味着 edit-tree 移动/重排 Rem 后,可直接 edit-rem 修改受影响节点,无需重新 read。
415
420
 
416
421
  ### 防线 3:str_replace 精确匹配(仅 edit-tree)
417
422
 
@@ -424,7 +429,9 @@ edit-rem 使用字段直接修改(changes 对象),不经过 str_replace,
424
429
  | 场景 | 缓存行为 | Agent 操作 |
425
430
  |:-----|:---------|:-----------|
426
431
  | 写入成功 | 从 SDK 重新读取 → 更新缓存 | 可继续编辑 |
427
- | 防线 2 拒绝 / 部分写入失败 | **不更新缓存** | 必须重新 read |
432
+ | 仅元数据变化(位置/时间戳等) | 静默刷新缓存并放行 | 可继续编辑(返回 warnings) |
433
+ | 语义字段冲突(text/type 等) | **不更新缓存** | 必须重新 read |
434
+ | 部分写入失败 | **不更新缓存** | 必须重新 read |
428
435
  | 枚举值非法(edit-rem) | 缓存保持不变 | 检查允许的值范围后重试 |
429
436
  | 防线 3 拒绝 / JSON 语法错误(edit-tree) | 缓存保持不变 | 调整 oldStr/newStr 后**直接重试** |
430
437
  | 操作执行中异常(edit-tree) | 已执行的操作保留(**无回滚**),不更新缓存 | 必须重新 read-tree |
@@ -654,7 +661,7 @@ aliases, descendants, siblingRem, isTable, portalType, propertyType
654
661
  | 守护进程未运行 | 未 connect 或已超时 | `connect` |
655
662
  | Plugin 未连接 | RemNote 未打开 | 打开 RemNote(health 三层:daemon→Plugin→SDK 链式依赖) |
656
663
  | has not been read yet | 未先 read | 执行对应 read 命令(search 结果不算 read!) |
657
- | has been modified since last read | 被外部修改 | 重新 read(必须,不可直接重试) |
664
+ | has been modified since last read | 语义字段被外部修改(元数据变化不触发此错误,只返回 warnings) | 重新 read(必须,不可直接重试) |
658
665
  | Invalid value for 'field' | 枚举字段值不合法(edit-rem) | 检查该字段允许的值范围 |
659
666
  | old_str not found | oldStr 不精确(edit-tree) | 检查引号、空格、换行 |
660
667
  | old_str matches N locations | oldStr 不够具体(edit-tree) | 扩大 oldStr 范围,包含更多上下文 |
@@ -14,6 +14,12 @@
14
14
  - **字段白名单校验**:21 个可写字段通过,只读和未知字段产生警告
15
15
  - **前置条件**:必须先 `read-rem` 建立缓存,否则防线 1 拒绝
16
16
 
17
+ > **⚠️ 完形填空(cloze)注意事项**
18
+ >
19
+ > **创建填空优先用 `edit-tree` 的 `{{文本}}` 语法**(SDK 自动生成安全 cId,零碰撞风险)。
20
+ > `edit-rem` 仅用于**修改已有 cloze**(先 `read-rem` 获取现有 cId,修改时保留原 cId)。
21
+ > 如必须通过 `edit-rem` 新建 cloze,cId **禁止**使用有语义的命名(如 `"cloze1"`、`"c1"`),必须使用随机数字串(如 `"7390281645937102"`)。
22
+
17
23
  ---
18
24
 
19
25
  ## 前置条件
@@ -225,28 +231,55 @@ if cache.get('rem:' + remId) === null:
225
231
 
226
232
  **恢复方式**:执行 `read-rem <remId>` 后重试。
227
233
 
228
- ### 防线 2:乐观并发检测
234
+ ### 防线 2:语义并发检测(三层字段分类)
229
235
 
230
236
  **目的**:检测自上次 read 以来,Rem 是否被外部修改(如用户在 RemNote UI 中编辑、其他 Agent 修改等)。
231
237
 
238
+ **核心机制**:将 RemObject 所有字段逐个比较,按差异字段所属层级决定处理方式:
239
+
232
240
  ```
233
241
  currentRemObject = forwardToPlugin('read_rem', { remId })
234
- currentJson = JSON.stringify(currentRemObject, null, 2)
235
- cachedJson = JSON.stringify(cachedObj, null, 2)
242
+ { semantic, warned, ignored } = classifyDiff(cachedObj, currentRemObject)
243
+
244
+ if semantic.length > 0:
245
+ // 第1层:语义字段冲突 → 硬拒绝,不更新缓存
246
+ throw "Rem {remId} has been modified since last read."
247
+
248
+ if warned.length > 0:
249
+ // 第2层:parent 变化 → 放行 + 专门警告
250
+ warnings.push("⚠️ parent has changed (was: {old}, now: {new}). ...")
251
+
252
+ if ignored.length > 0:
253
+ // 第3层:普通元数据变化 → 放行 + 统一警告
254
+ warnings.push("ℹ️ Metadata fields changed since last read: {fields}. ...")
236
255
 
237
- if currentJson !== cachedJson:
238
- // 不更新缓存 — 迫使 AI 重新 read 获取最新状态
239
- throw "Rem {remId} has been modified since last read. Please read it again before editing."
256
+ if warned.length > 0 || ignored.length > 0:
257
+ // 静默刷新缓存(保持新鲜度)
258
+ cache.set('rem:' + remId, currentRemObject)
240
259
  ```
241
260
 
242
- **关键设计**:
243
- - 比较方式:**将当前 RemObject 和缓存 RemObject 分别 JSON.stringify 后做文本比较**
244
- - 失败时**不更新缓存**:防止 AI 跳过 re-read 直接重试
245
- - RichText key 排序保证序列化确定性(`sortRichTextKeys()`)
261
+ **三层字段分类**:
246
262
 
247
- **恢复方式**:执行 `read-rem <remId>` 获取最新状态后重试。
263
+ | 层级 | 包含字段 | 变化时行为 |
264
+ |:-----|:---------|:----------|
265
+ | 第1层:语义字段 | text, backText, type, tags, highlightColor, fontSize 等(不在下面两层的所有字段) | **硬拒绝**,不更新缓存 |
266
+ | 第2层:敏感元数据 | parent | **放行** + `⚠️ parent has changed (was: X, now: Y)...` 警告 + 刷新缓存 |
267
+ | 第3层:普通元数据 | positionAmongstSiblings, updatedAt, siblingRem, children, descendants 等 23 个字段 | **放行** + `ℹ️ Metadata fields changed since last read: ...` 警告 + 刷新缓存 |
248
268
 
249
- ### 两道防线判断树
269
+ **设计意义**:`edit_tree` 移动/重排 Rem 后,可以直接 `edit_rem` 修改受影响节点的文本格式,无需逐个重新 `read_rem`。只有真正的内容/属性并发冲突才会被拦截。
270
+
271
+ **warnings 输出示例**:
272
+
273
+ | 场景 | warnings |
274
+ |:-----|:---------|
275
+ | 无外部变化 | `[]` |
276
+ | edit_tree 重排后 | `["ℹ️ Metadata fields changed since last read: positionAmongstSiblings. This is expected after structural operations. Proceeding with edit."]` |
277
+ | edit_tree 移动后 | `["⚠️ parent has changed (was: oldId, now: newId). The Rem has been moved to a different parent since last read. Proceeding with edit.", "ℹ️ Metadata fields changed since last read: siblingRem, positionAmongstSiblings, updatedAt. ..."]` |
278
+ | 语义字段被外部修改 | 不返回(抛异常 `"has been modified since last read"`) |
279
+
280
+ **恢复方式**(仅语义冲突需要):执行 `read-rem <remId>` 获取最新状态后重试。
281
+
282
+ ### 防线判断树
250
283
 
251
284
  ```
252
285
  edit-rem(remId, changes)
@@ -255,9 +288,11 @@ edit-rem(remId, changes)
255
288
  │ ├─ 否 → ERROR: "has not been read yet"
256
289
  │ └─ 是 → 继续
257
290
 
258
- ├─ 防线 2: 当前值 === 缓存值?
259
- │ ├─ → ERROR: "has been modified since last read"
260
- └─ 继续
291
+ ├─ 防线 2: 三层字段分类比较
292
+ │ ├─ 语义字段变化 → ERROR: "has been modified since last read"
293
+ ├─ parent 变化 放行(warnings += ⚠️ parent 警告,刷新缓存)
294
+ │ ├─ 仅元数据变化 → 放行(warnings += ℹ️ 元数据警告,刷新缓存)
295
+ │ └─ 无变化 → 继续
261
296
 
262
297
  ├─ 字段分类
263
298
  │ ├─ 只读字段 → 警告
@@ -287,7 +322,7 @@ RemObject 51 个字段中,21 个可编辑(RW),30 个只读(R + R-F)
287
322
 
288
323
  | 字段 | SDK setter | 值类型 | 约束 / 特殊处理 |
289
324
  |------|-----------|--------|-----------------|
290
- | `text` | `rem.setText()` | RichText | RichText 数组 |
325
+ | `text` | `rem.setText()` | RichText | RichText 数组。⚠️ 若含 cId 元素(完形填空),须原样保留 cId——禁止编造新 cId,创建填空用 `edit-tree {{文本}}` |
291
326
  | `backText` | `rem.setBackText()` | RichText \| null | null → `setBackText([])`(清除背面);字符串 → 包装为 `[string]` |
292
327
  | `type` | `rem.setType()` | RemTypeValue | `portal` 不可设置(只能通过 `createPortal()` 创建) |
293
328
  | `isDocument` | `rem.setIsDocument()` | boolean | — |
@@ -405,6 +440,7 @@ isPowerupPropertyListItem, isPowerupSlot
405
440
  - RichText 是 JSON 数组,元素为纯字符串或格式化对象
406
441
  - 格式化对象的 key 按**字母序**排列(`_id` < `b` < `i` < `text`)
407
442
  - 修改 text/backText 时,传入的是**完整的新数组**,不是部分替换
443
+ - ⚠️ **完形填空(cloze)**:若原文含 `cId` 元素,写回时**必须原样保留** cId 值,禁止修改或删除。创建新填空请用 `edit-tree` 的 `{{文本}}` 语法(SDK 自动生成安全 cId),不要在此处手动构造含 cId 的 RichText 元素
408
444
 
409
445
  ### Tags Diff 操作
410
446