@zhin.js/adapter-sandbox 1.0.22
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.
- package/CHANGELOG.md +187 -0
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/client/RichTextEditor.tsx +359 -0
- package/client/Sandbox.tsx +864 -0
- package/client/index.tsx +11 -0
- package/client/tsconfig.json +19 -0
- package/dist/index.js +5 -0
- package/lib/index.d.ts +54 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +173 -0
- package/lib/index.js.map +1 -0
- package/package.json +65 -0
- package/src/index.ts +235 -0
package/client/index.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { addPage } from '@zhin.js/client'
|
|
2
|
+
import { Terminal } from 'lucide-react'
|
|
3
|
+
import Sandbox from './Sandbox'
|
|
4
|
+
addPage({
|
|
5
|
+
key: 'process-sandbox',
|
|
6
|
+
path: '/sandbox',
|
|
7
|
+
title: '沙盒',
|
|
8
|
+
icon: <Terminal className="w-5 h-5" />,
|
|
9
|
+
element: <Sandbox/>
|
|
10
|
+
})
|
|
11
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "../dist",
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"target": "ES2022",
|
|
9
|
+
"jsx":"react-jsx",
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noEmit": false
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"./**/*"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{jsx as t,jsxs as i}from"react/jsx-runtime";import{cn as he,addPage as Ae}from"@zhin.js/client";import{MessageSquare as Y,Wifi as Ie,WifiOff as $e,Trash2 as Le,Bot as De,User as se,Smile as Fe,Image as Me,X as Ue,Search as me,Check as fe,Send as Be,Info as Pe,Hash as je,Users as _e,Terminal as He}from"lucide-react";import ye,{forwardRef as Oe,useRef as B,useImperativeHandle as We,useState as f,useEffect as Q}from"react";import{Card as X,Box as v,Flex as p,Heading as xe,Badge as C,Button as k,Text as b,TextField as J,Grid as qe,Tabs as V}from"@radix-ui/themes";const ve=Oe(({placeholder:K="输入消息...",onSend:L,onChange:S,onAtTrigger:w,minHeight:G="44px",maxHeight:Z="200px"},m)=>{const u=B(null),x=B(null),E=()=>{if(!u.current)return{text:"",segments:[]};let s="";const a=[],o=Array.from(u.current.childNodes);for(const l of o)if(l.nodeType===Node.TEXT_NODE){const c=l.textContent||"";c&&(s+=c,a.push({type:"text",data:{text:c}}))}else if(l.nodeType===Node.ELEMENT_NODE){const c=l;if(c.classList.contains("editor-face")){const g=c.dataset.id;s+=`[face:${g}]`,a.push({type:"face",data:{id:Number(g)}})}else if(c.classList.contains("editor-image")){const g=c.dataset.url;s+=`[image:${g}]`,a.push({type:"image",data:{url:g}})}else if(c.classList.contains("editor-at")){const g=c.dataset.name,z=c.dataset.id;s+=`[@${g}]`,a.push({type:"at",data:{name:g,qq:z}})}else c.tagName==="BR"&&(s+=`
|
|
2
|
+
`)}return{text:s,segments:a}},D=s=>{if(!u.current)return;const a=document.createElement("img");a.src=`https://face.viki.moe/apng/${s}.png`,a.alt=`[face:${s}]`,a.dataset.type="face",a.dataset.id=String(s),a.className="editor-face",M(a),T()},ee=s=>{if(!u.current||!s.trim())return;const a=document.createElement("img");a.src=s.trim(),a.alt=`[image:${s.trim()}]`,a.dataset.type="image",a.dataset.url=s.trim(),a.className="editor-image",M(a),T()},F=(s,a)=>{if(!u.current||!s.trim())return;const o=document.createElement("span");o.dataset.type="at",o.dataset.name=s,a&&(o.dataset.id=a),o.className="editor-at",o.contentEditable="false";const l=document.createElement("span");l.textContent="@",l.className="editor-at-symbol";const c=document.createElement("span");c.textContent=s,c.className="editor-at-name",o.appendChild(l),o.appendChild(c),M(o),T()},M=s=>{if(!u.current)return;u.current.focus();const a=window.getSelection();if(a&&a.rangeCount>0){const o=a.getRangeAt(0);if(u.current.contains(o.commonAncestorContainer))o.deleteContents(),o.insertNode(s),o.collapse(false),a.removeAllRanges(),a.addRange(o);else{u.current.appendChild(s);const c=document.createRange();c.setStartAfter(s),c.collapse(true),a.removeAllRanges(),a.addRange(c)}}else{u.current.appendChild(s);const o=window.getSelection();if(o){const l=document.createRange();l.setStartAfter(s),l.collapse(true),o.removeAllRanges(),o.addRange(l)}}},P=()=>{u.current&&(u.current.innerHTML="",T())},j=()=>{u.current?.focus()},_=()=>E(),H=()=>{if(!u.current||!w)return;const s=window.getSelection();if(!s||s.rangeCount===0){w(false,""),x.current=null;return}const a=s.getRangeAt(0);if(!u.current.contains(a.commonAncestorContainer)){w(false,""),x.current=null;return}const o=a.startContainer;if(o.nodeType!==Node.TEXT_NODE){w(false,""),x.current=null;return}const l=o,c=l.textContent?.substring(0,a.startOffset)||"",g=c.lastIndexOf("@");if(g!==-1){const z=c.substring(g+1);if(z.includes(" ")||z.includes(`
|
|
3
|
+
`)){w(false,""),x.current=null;return}x.current=l;const R=document.createRange();R.setStart(l,g),R.setEnd(l,g+1);const A=R.getBoundingClientRect(),N=u.current.getBoundingClientRect();w(true,z,{top:A.bottom-N.top,left:A.left-N.left})}else w(false,""),x.current=null},T=()=>{if(H(),S){const{text:s,segments:a}=E();S(s,a)}},te=(s,a)=>{if(!x.current)return;const o=x.current,l=o.textContent||"",c=l.lastIndexOf("@");if(c!==-1){const g=l.substring(c+1),z=c+1+g.split(/[\s\n]/)[0].length,R=l.substring(0,c),A=l.substring(z);o.textContent=R+A;const N=window.getSelection();if(N){const W=document.createRange();W.setStart(o,c),W.collapse(true),N.removeAllRanges(),N.addRange(W)}}x.current=null,F(s,a)},O=s=>{if(s.key==="Enter"&&!s.shiftKey&&(s.preventDefault(),L)){const{text:a,segments:o}=E();L(a,o)}};return We(m,()=>({focus:j,clear:P,insertFace:D,insertImage:ee,insertAt:F,replaceAtTrigger:te,getContent:_})),t("div",{ref:u,contentEditable:true,suppressContentEditableWarning:true,onInput:T,onKeyDown:O,"data-placeholder":K,className:"rich-text-editor",style:{width:"100%",minHeight:G,maxHeight:Z,padding:"0.5rem 0.75rem",border:"1px solid var(--gray-6)",borderRadius:"6px",backgroundColor:"var(--gray-1)",fontSize:"var(--font-size-2)",outline:"none",overflowY:"auto",lineHeight:"1.5",wordWrap:"break-word",color:"var(--gray-12)"}})});ve.displayName="RichTextEditor";function Ke(){const[K,L]=f([]),[S,w]=f([{id:"user_1001",name:"测试用户",type:"private",unread:0},{id:"group_2001",name:"测试群组",type:"group",unread:0},{id:"channel_3001",name:"测试频道",type:"channel",unread:0}]),[G,Z]=f([]),[m,u]=f(S[0]),[x,E]=f(""),[D,ee]=f("ProcessBot"),[F,M]=f(false),[P,j]=f(false),[_,H]=f(false),[T,te]=f(false),[O,s]=f(null),[a,o]=f(""),[l,c]=f(""),[g,z]=f(""),[R,A]=f(""),[N]=f([{id:"10001",name:"张三"},{id:"10002",name:"李四"},{id:"10003",name:"王五"},{id:"10004",name:"赵六"},{id:"10005",name:"测试用户"},{id:"10086",name:"Admin"},{id:"10010",name:"Test User"}]),[W,q]=f([]),[ne,re]=f(false),oe=B(null),U=B(null);B(null);const I=B(null),be=async()=>{try{const r=await(await fetch("https://face.viki.moe/metadata.json")).json();Z(r)}catch(e){console.error("[ProcessSandbox] Failed to fetch face list:",e)}};Q(()=>{be()},[]),Q(()=>{const e=window.location.protocol==="https:"?"wss:":"ws:";return U.current=new WebSocket(`${e}//${window.location.host}/sandbox`),U.current.onopen=()=>{M(true)},U.current.onmessage=r=>{try{const n=JSON.parse(r.data);let h=[];typeof n.content=="string"?h=ae(n.content):Array.isArray(n.content)?h=n.content:h=ae(String(n.content));let d=S.find($=>$.id===n.id);if(!d){const $=n.type==="private"?`私聊-${n.bot||D}`:n.type==="group"?`群组-${n.id}`:`频道-${n.id}`;d={id:n.id,name:$,type:n.type,unread:0},w(Te=>[...Te,d]),u(d)}const y={id:`bot_${n.timestamp}`,type:"received",channelType:n.type,channelId:n.id,channelName:d.name,senderId:"bot",senderName:n.bot||D,content:h,timestamp:n.timestamp};L($=>[...$,y])}catch(n){console.error("[Sandbox] Failed to parse message:",n)}},U.current.onclose=()=>{M(false)},()=>{U.current?.close()}},[D,S]),Q(()=>{oe.current?.scrollIntoView({behavior:"smooth"})},[K]),Q(()=>{if(x.trim()){const e=ae(x);q(e)}else q([])},[x]);const ae=e=>{const r=[],n=/\[@([^\]]+)\]|\[face:(\d+)\]|\[image:([^\]]+)\]/g;let h=0,d;for(;(d=n.exec(e))!==null;){if(d.index>h){const y=e.substring(h,d.index);y&&r.push({type:"text",data:{text:y}})}d[1]?r.push({type:"at",data:{qq:d[1],name:d[1]}}):d[2]?r.push({type:"face",data:{id:parseInt(d[2])}}):d[3]&&r.push({type:"image",data:{url:d[3]}}),h=n.lastIndex}if(h<e.length){const y=e.substring(h);y&&r.push({type:"text",data:{text:y}})}return r.length>0?r:[{type:"text",data:{text:e}}]},we=e=>e.map((r,n)=>{if(typeof r=="string"){const h=r.split(`
|
|
4
|
+
`);return t("span",{children:h.map((d,y)=>i(ye.Fragment,{children:[d,y<h.length-1&&t("br",{})]},y))},n)}switch(r.type){case"text":const h=r.data.text.split(`
|
|
5
|
+
`);return t("span",{children:h.map((d,y)=>i(ye.Fragment,{children:[d,y<h.length-1&&t("br",{})]},y))},n);case"at":return i(C,{color:"blue",variant:"soft",style:{margin:"0 2px"},children:["@",r.data.name||r.data.qq]},n);case"face":return t("img",{src:`https://face.viki.moe/apng/${r.data.id}.png`,alt:`表情${r.data.id}`,style:{width:"24px",height:"24px",display:"inline-block",verticalAlign:"middle",margin:"0 2px"}},n);case"image":return t("img",{src:r.data.url,alt:"图片",style:{maxWidth:"300px",borderRadius:"8px",margin:"4px 0",display:"block"},onError:d=>{d.currentTarget.style.display="none"}},n);case"video":return t(C,{variant:"outline",style:{margin:"0 2px"},children:"📹 视频"},n);case"audio":return t(C,{variant:"outline",style:{margin:"0 2px"},children:"🎵 语音"},n);case"file":return i(C,{variant:"outline",style:{margin:"0 2px"},children:["📎 ",r.data.name||"文件"]},n);default:return t("span",{children:"[未知消息类型]"},n)}}),ce=(e,r)=>{if(!e.trim()||r.length===0)return;const n={id:`msg_${Date.now()}`,type:"sent",channelType:m.type,channelId:m.id,channelName:m.name,senderId:"test_user",senderName:"测试用户",content:r,timestamp:Date.now()};L(h=>[...h,n]),E(""),q([]),I.current?.clear(),U.current?.send(JSON.stringify({type:m.type,id:m.id,content:r,timestamp:Date.now()}))},Ce=()=>{confirm("确定清空所有消息记录?")&&L([])},ze=e=>{u(e),w(r=>r.map(n=>n.id===e.id?{...n,unread:0}:n)),window.innerWidth<768&&re(false)},Re=()=>{const e=["private","group","channel"],r={private:"私聊",group:"群聊",guild:"频道"},n=e[Math.floor(Math.random()*e.length)],h=`${n}_${Date.now()}`,d=prompt(`请输入${r[n]}名称:`);if(d){const y={id:h,name:d,type:n,unread:0};w($=>[...$,y]),u(y)}},le=e=>{switch(e){case"private":return t(se,{size:16});case"group":return t(_e,{size:16});case"channel":return t(je,{size:16});default:return t(Y,{size:16})}},Ne=e=>{I.current?.insertFace(e),j(false)},de=()=>{g.trim()&&(I.current?.insertImage(g.trim()),z(""),H(false))},pe=()=>{R.trim()&&(I.current?.insertAt(R.trim()),A(""),te(false))},ke=e=>{I.current?.replaceAtTrigger(e.name,e.id),s(null),o("")},Se=(e,r,n)=>{if(m.type==="private"){s(null),o("");return}e&&n?(s(n),o(r)):(s(null),o(""))},ue=N.filter(e=>{if(!a.trim())return true;const r=a.toLowerCase();return e.name.toLowerCase().includes(r)||e.id.toLowerCase().includes(r)}),Ee=(e,r)=>{E(e),q(r)},ge=G.filter(e=>e.name.toLowerCase().includes(l.toLowerCase())||e.describe.toLowerCase().includes(l.toLowerCase())),ie=K.filter(e=>e.channelId===m.id);return i("div",{className:"sandbox-container",children:[i("button",{className:"mobile-channel-toggle md:hidden",onClick:()=>re(!ne),children:[t(Y,{size:20}),"频道列表"]}),i(X,{className:he("channel-sidebar",ne&&"show"),children:[t(v,{p:"3",style:{borderBottom:"1px solid var(--gray-6)"},children:i(p,{justify:"between",align:"center",mb:"2",children:[i(p,{align:"center",gap:"2",children:[t(v,{p:"1",style:{borderRadius:"8px",backgroundColor:"var(--purple-3)"},children:t(Y,{size:16,color:"var(--purple-9)"})}),t(xe,{size:"4",children:"频道列表"})]}),t(C,{color:F?"green":"gray",children:i(p,{align:"center",gap:"1",children:[F?t(Ie,{size:12}):t($e,{size:12}),F?"已连接":"未连接"]})})]})}),t(v,{style:{flex:1,overflowY:"auto"},p:"3",children:t(p,{direction:"column",gap:"2",children:S.map(e=>{const r=m.id===e.id;return i("div",{className:he("channel-item",r&&"active"),onClick:()=>ze(e),children:[t("div",{className:"icon",children:le(e.type)}),i("div",{className:"text",children:[t("div",{className:"title",children:e.name}),i("div",{className:"subtitle",children:[e.type==="private"&&"私聊",e.type==="group"&&"群聊",e.type==="channel"&&"频道"]})]}),e.unread>0&&t(C,{color:"red",size:"1",className:"badge",children:e.unread}),r&&t("div",{className:"indicator"})]},e.id)})})}),t(v,{p:"2",style:{borderTop:"1px solid var(--gray-6)"},children:t(k,{variant:"outline",onClick:Re,style:{width:"100%"},children:"+ 添加频道"})})]}),ne&&t("div",{className:"channel-overlay md:hidden",onClick:()=>re(false)}),i("div",{className:"chat-area",children:[t(X,{children:i(p,{justify:"between",align:"center",p:"3",children:[i(p,{align:"center",gap:"3",children:[t(v,{p:"2",style:{borderRadius:"12px",backgroundColor:"var(--blue-3)"},children:le(m.type)}),i(v,{children:[t(xe,{size:"5",children:m.name}),i(p,{align:"center",gap:"2",children:[t(b,{size:"1",color:"gray",children:m.id}),t(C,{variant:"outline",size:"1",children:ie.length}),t(b,{size:"1",color:"gray",children:"条消息"})]})]}),i(C,{color:m.type==="private"?"blue":m.type==="group"?"green":"purple",children:[m.type==="private"&&"私聊",m.type==="group"&&"群聊",m.type==="channel"&&"频道"]})]}),i(p,{align:"center",gap:"2",children:[t(J.Root,{value:D,onChange:e=>ee(e.target.value),placeholder:"机器人名称",style:{width:"120px"}}),i(k,{variant:"soft",onClick:Ce,children:[t(Le,{size:16}),"清空"]})]})]})}),t(X,{style:{flex:1,display:"flex",flexDirection:"column"},children:t(v,{style:{flex:1,overflowY:"auto"},p:"4",children:ie.length===0?i(p,{direction:"column",align:"center",justify:"center",style:{height:"100%"},children:[t(Y,{size:64,color:"var(--gray-6)"}),t(b,{color:"gray",mt:"3",children:"暂无消息,开始对话吧!"})]}):i(p,{direction:"column",gap:"2",children:[ie.map(e=>t(p,{justify:e.type==="sent"?"end":"start",children:i(v,{style:{maxWidth:"70%",padding:"12px",borderRadius:"16px",backgroundColor:e.type==="sent"?"var(--blue-9)":"var(--gray-3)",color:e.type==="sent"?"white":"var(--gray-12)"},children:[i(p,{align:"center",gap:"2",mb:"1",children:[e.type==="received"&&t(De,{size:14}),e.type==="sent"&&t(se,{size:14}),t(b,{size:"1",weight:"medium",style:{opacity:.9},children:e.senderName}),t(b,{size:"1",style:{opacity:.7},children:new Date(e.timestamp).toLocaleTimeString()})]}),t(b,{size:"2",children:we(e.content)})]})},e.id)),t("div",{ref:oe})]})})}),t(X,{children:i(p,{direction:"column",gap:"3",p:"3",children:[i(p,{gap:"2",align:"center",children:[t(k,{variant:P?"solid":"outline",size:"2",onClick:()=>{j(!P),H(false)},title:"插入表情",children:t(Fe,{size:16})}),t(k,{variant:_?"solid":"outline",size:"2",onClick:()=>{H(!_),j(false)},title:"插入图片",children:t(Me,{size:16})}),t(v,{style:{flex:1}}),x&&t(k,{variant:"ghost",size:"2",onClick:()=>{E(""),q([])},title:"清空",children:t(Ue,{size:16})})]}),P&&i(v,{p:"3",style:{border:"1px solid var(--gray-6)",borderRadius:"8px",backgroundColor:"var(--gray-1)",maxHeight:"256px",overflowY:"auto"},children:[t(J.Root,{value:l,onChange:e=>c(e.target.value),placeholder:"搜索表情...",style:{marginBottom:"8px"}}),t(qe,{columns:"8",gap:"2",children:ge.slice(0,80).map(e=>t("button",{onClick:()=>Ne(e.id),style:{width:"40px",height:"40px",borderRadius:"8px",border:"1px solid var(--gray-6)",backgroundColor:"transparent",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center"},title:e.name,children:t("img",{src:`https://face.viki.moe/apng/${e.id}.png`,alt:e.name,style:{width:"32px",height:"32px"}})},e.id))}),ge.length===0&&i(p,{direction:"column",align:"center",gap:"2",py:"4",children:[t(me,{size:32,color:"var(--gray-6)"}),t(b,{size:"2",color:"gray",children:"未找到匹配的表情"})]})]}),_&&t(v,{p:"3",style:{border:"1px solid var(--gray-6)",borderRadius:"8px",backgroundColor:"var(--gray-1)"},children:i(V.Root,{defaultValue:"url",children:[i(V.List,{children:[t(V.Trigger,{value:"url",children:"图片链接"}),t(V.Trigger,{value:"upload",children:"本地上传"})]}),t(v,{pt:"3",children:i(p,{direction:"column",gap:"2",children:[t(J.Root,{value:g,onChange:e=>z(e.target.value),placeholder:"输入图片 URL...",onKeyDown:e=>{e.key==="Enter"&&(e.preventDefault(),de())}}),i(k,{onClick:de,disabled:!g.trim(),children:[t(fe,{size:16}),"插入"]})]})})]})}),T&&t(v,{p:"3",style:{border:"1px solid var(--gray-6)",borderRadius:"8px",backgroundColor:"var(--gray-1)"},children:i(p,{direction:"column",gap:"2",children:[t(J.Root,{value:R,onChange:e=>A(e.target.value),placeholder:"输入用户名...",onKeyDown:e=>{e.key==="Enter"&&(e.preventDefault(),pe())}}),i(k,{onClick:pe,disabled:!R.trim(),children:[t(fe,{size:16}),"插入"]})]})}),i(p,{gap:"2",align:"start",children:[i(v,{style:{flex:1,position:"relative"},children:[t(ve,{ref:I,placeholder:`向 ${m.name} 发送消息...`,onSend:ce,onChange:Ee,onAtTrigger:Se,minHeight:"44px",maxHeight:"200px"}),O&&t(v,{style:{position:"absolute",top:`${O.top}px`,left:`${O.left}px`,zIndex:1e3,backgroundColor:"var(--gray-1)",border:"1px solid var(--gray-6)",borderRadius:"8px",boxShadow:"0 4px 12px rgba(0, 0, 0, 0.15)",minWidth:"240px",maxHeight:"280px",overflowY:"auto",padding:"4px"},children:ue.length>0?ue.map(e=>i(p,{align:"center",gap:"2",p:"2",onClick:()=>ke(e),style:{cursor:"pointer",borderRadius:"6px",transition:"background-color 0.2s"},onMouseEnter:r=>{r.currentTarget.style.backgroundColor="var(--blue-a3)"},onMouseLeave:r=>{r.currentTarget.style.backgroundColor="transparent"},children:[t(se,{size:16,color:"var(--blue-9)"}),i(p,{direction:"column",gap:"0",style:{flex:1},children:[t(b,{size:"2",weight:"medium",children:e.name}),i(b,{size:"1",color:"gray",children:["ID: ",e.id]})]})]},e.id)):i(p,{align:"center",justify:"center",p:"4",direction:"column",gap:"2",children:[t(me,{size:20,color:"var(--gray-8)"}),t(b,{size:"1",color:"gray",children:"未找到匹配的用户"})]})})]}),i(k,{onClick:()=>{const e=I.current?.getContent();e&&ce(e.text,e.segments)},disabled:!x.trim()||W.length===0,size:"3",title:"发送消息 (Enter)",children:[t(Be,{size:16}),"发送"]})]}),i(p,{align:"center",gap:"2",wrap:"wrap",children:[t(Pe,{size:12,color:"var(--gray-9)"}),t(b,{size:"1",color:"gray",children:"快捷操作:"}),t(C,{variant:"outline",size:"1",children:"Enter"}),t(b,{size:"1",color:"gray",children:"发送"}),t(C,{variant:"outline",size:"1",children:"Shift+Enter"}),t(b,{size:"1",color:"gray",children:"换行"}),t(C,{variant:"outline",size:"1",children:"[@名称]"}),t(b,{size:"1",color:"gray",children:"@某人"})]})]})})]})]})}Ae({key:"process-sandbox",path:"/sandbox",title:"沙盒",icon:t(He,{className:"w-5 h-5"}),element:t(Ke,{})});
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { Bot, Adapter, Message, SendOptions, MessageType, MessageElement, Plugin } from "zhin.js";
|
|
3
|
+
import type { WebSocket } from "ws";
|
|
4
|
+
import { Router } from "@zhin.js/http";
|
|
5
|
+
export interface SandboxConfig {
|
|
6
|
+
context: "sandbox";
|
|
7
|
+
ws: WebSocket;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
declare module "zhin.js" {
|
|
11
|
+
namespace Plugin {
|
|
12
|
+
interface Contexts {
|
|
13
|
+
sandbox: SandboxAdapter;
|
|
14
|
+
router: Router;
|
|
15
|
+
web: any;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
interface RegisteredAdapters {
|
|
19
|
+
sandbox: SandboxAdapter;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export declare class SandboxBot extends EventEmitter implements Bot<SandboxConfig, {
|
|
23
|
+
content: MessageElement[];
|
|
24
|
+
ts: number;
|
|
25
|
+
}> {
|
|
26
|
+
adapter: SandboxAdapter;
|
|
27
|
+
$config: SandboxConfig;
|
|
28
|
+
$connected: boolean;
|
|
29
|
+
get $id(): string;
|
|
30
|
+
private logger;
|
|
31
|
+
constructor(adapter: SandboxAdapter, $config: SandboxConfig);
|
|
32
|
+
$connect(): Promise<void>;
|
|
33
|
+
$disconnect(): Promise<void>;
|
|
34
|
+
$formatMessage({ content, type, id, ts }: {
|
|
35
|
+
content: MessageElement[];
|
|
36
|
+
id: string;
|
|
37
|
+
type: MessageType;
|
|
38
|
+
ts: number;
|
|
39
|
+
}): Message<{
|
|
40
|
+
content: MessageElement[];
|
|
41
|
+
ts: number;
|
|
42
|
+
}>;
|
|
43
|
+
$sendMessage(options: SendOptions): Promise<string>;
|
|
44
|
+
$recallMessage(id: string): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
declare class SandboxAdapter extends Adapter<SandboxBot> {
|
|
47
|
+
wss?: ReturnType<Router["ws"]>;
|
|
48
|
+
constructor(plugin: Plugin);
|
|
49
|
+
createBot(config: SandboxConfig): SandboxBot;
|
|
50
|
+
start(): Promise<void>;
|
|
51
|
+
setupWebSocket(router: Router): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EACL,GAAG,EACH,OAAO,EAEP,OAAO,EACP,WAAW,EAGX,WAAW,EACX,cAAc,EACd,MAAM,EACP,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAGvC,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,SAAS,CAAC;IACnB,EAAE,EAAE,SAAS,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,MAAM,CAAC;QACf,UAAU,QAAQ;YAChB,OAAO,EAAE,cAAc,CAAC;YACxB,MAAM,EAAE,MAAM,CAAC;YACf,GAAG,EAAE,GAAG,CAAC;SACV;KACF;IAED,UAAU,kBAAkB;QAC1B,OAAO,EAAE,cAAc,CAAC;KACzB;CACF;AAYD,qBAAa,UAAW,SAAQ,YAAa,YAAW,GAAG,CAAC,aAAa,EAAE;IAAE,OAAO,EAAE,cAAc,EAAE,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;IAShG,OAAO,EAAE,cAAc;IAAS,OAAO,EAAE,aAAa;IARzE,UAAU,EAAE,OAAO,CAAS;IAE5B,IAAI,GAAG,WAEN;IAED,OAAO,CAAC,MAAM,CAAU;gBAEL,OAAO,EAAE,cAAc,EAAS,OAAO,EAAE,aAAa;IAqBnE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAIzB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAKlC,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;QAAE,OAAO,EAAE,cAAc,EAAE,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,WAAW,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE;;;;IAoC5G,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAenD,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD;AAED,cAAM,cAAe,SAAQ,OAAO,CAAC,UAAU,CAAC;IAC9C,GAAG,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;gBAEnB,MAAM,EAAE,MAAM;IAI1B,SAAS,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU;IAOtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAkCpD"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { Adapter, usePlugin, Message, segment, } from "zhin.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
const plugin = usePlugin();
|
|
5
|
+
const logger = plugin.logger;
|
|
6
|
+
export class SandboxBot extends EventEmitter {
|
|
7
|
+
adapter;
|
|
8
|
+
$config;
|
|
9
|
+
$connected = false;
|
|
10
|
+
get $id() {
|
|
11
|
+
return this.$config.name;
|
|
12
|
+
}
|
|
13
|
+
logger = logger;
|
|
14
|
+
constructor(adapter, $config) {
|
|
15
|
+
super();
|
|
16
|
+
this.adapter = adapter;
|
|
17
|
+
this.$config = $config;
|
|
18
|
+
this.$config.ws.on("message", (data) => {
|
|
19
|
+
const message = JSON.parse(data.toString());
|
|
20
|
+
// 确保 content 是 MessageElement[] 格式
|
|
21
|
+
const content = typeof message.content === 'string'
|
|
22
|
+
? [{ type: 'text', data: { text: message.content } }]
|
|
23
|
+
: message.content;
|
|
24
|
+
this.logger.debug(`${this.$config.name} recv ${message.type}(${message.id}):${segment.raw(content)}`);
|
|
25
|
+
const formattedMessage = this.$formatMessage({ content: content, type: message.type, id: message.id, ts: message.timestamp });
|
|
26
|
+
this.adapter.emit("message.receive", formattedMessage);
|
|
27
|
+
});
|
|
28
|
+
this.$config.ws.on("close", () => {
|
|
29
|
+
this.logger.debug(`Sandbox bot ${this.$config.name} disconnected`);
|
|
30
|
+
this.$connected = false;
|
|
31
|
+
// 从 adapter 中移除 bot
|
|
32
|
+
this.adapter.bots.delete(this.$id);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async $connect() {
|
|
36
|
+
this.$connected = true;
|
|
37
|
+
}
|
|
38
|
+
async $disconnect() {
|
|
39
|
+
this.$config.ws.close();
|
|
40
|
+
this.$connected = false;
|
|
41
|
+
}
|
|
42
|
+
$formatMessage({ content, type, id, ts }) {
|
|
43
|
+
const message = Message.from({ content, ts }, {
|
|
44
|
+
$id: `${ts}`,
|
|
45
|
+
$adapter: "sandbox",
|
|
46
|
+
$bot: `${this.$config.name}`,
|
|
47
|
+
$sender: {
|
|
48
|
+
id: `${id}`,
|
|
49
|
+
name: `mock`,
|
|
50
|
+
},
|
|
51
|
+
$channel: {
|
|
52
|
+
id: `${id}`,
|
|
53
|
+
type: type,
|
|
54
|
+
},
|
|
55
|
+
$content: content,
|
|
56
|
+
$raw: segment.raw(content),
|
|
57
|
+
$timestamp: ts,
|
|
58
|
+
$recall: async () => {
|
|
59
|
+
await this.$recallMessage(message.$id);
|
|
60
|
+
},
|
|
61
|
+
$reply: async (content, quote) => {
|
|
62
|
+
if (!Array.isArray(content))
|
|
63
|
+
content = [content];
|
|
64
|
+
if (quote)
|
|
65
|
+
content.unshift({ type: "reply", data: { id: typeof quote === "boolean" ? message.$id : quote } });
|
|
66
|
+
return await this.$sendMessage({
|
|
67
|
+
...message.$channel,
|
|
68
|
+
context: "sandbox",
|
|
69
|
+
bot: `${this.$config.name}`,
|
|
70
|
+
content,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return message;
|
|
75
|
+
}
|
|
76
|
+
async $sendMessage(options) {
|
|
77
|
+
if (!this.$connected)
|
|
78
|
+
return "";
|
|
79
|
+
this.logger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
|
|
80
|
+
options.bot = this.$config.name;
|
|
81
|
+
options.context = "sandbox";
|
|
82
|
+
this.$config.ws.send(JSON.stringify({
|
|
83
|
+
...options,
|
|
84
|
+
content: options.content, // 发送消息段数组
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
}));
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
async $recallMessage(id) {
|
|
90
|
+
// 沙盒不支持撤回消息
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
class SandboxAdapter extends Adapter {
|
|
94
|
+
wss;
|
|
95
|
+
constructor(plugin) {
|
|
96
|
+
super(plugin, "sandbox", []);
|
|
97
|
+
}
|
|
98
|
+
createBot(config) {
|
|
99
|
+
const bot = new SandboxBot(this, config);
|
|
100
|
+
// 将 bot 添加到 bots Map 中
|
|
101
|
+
this.bots.set(bot.$id, bot);
|
|
102
|
+
return bot;
|
|
103
|
+
}
|
|
104
|
+
async start() {
|
|
105
|
+
// start 方法会在 mounted 时被调用
|
|
106
|
+
// WebSocket server 的创建在 useContext("router") 中处理
|
|
107
|
+
}
|
|
108
|
+
async setupWebSocket(router) {
|
|
109
|
+
if (this.wss)
|
|
110
|
+
return; // 已经设置过了
|
|
111
|
+
// 创建 WebSocket server
|
|
112
|
+
this.wss = router.ws("/sandbox");
|
|
113
|
+
this.wss.on("connection", (ws, req) => {
|
|
114
|
+
// 为每个连接创建一个唯一的 bot 名称
|
|
115
|
+
const botName = `sandbox-${Math.random().toString(36).slice(2, 9)}`;
|
|
116
|
+
logger.debug(`New sandbox connection: ${botName} from ${req.socket.remoteAddress}`);
|
|
117
|
+
// 创建 bot 配置
|
|
118
|
+
const config = {
|
|
119
|
+
context: "sandbox",
|
|
120
|
+
ws,
|
|
121
|
+
name: botName,
|
|
122
|
+
};
|
|
123
|
+
// 创建并连接 bot
|
|
124
|
+
const bot = this.createBot(config);
|
|
125
|
+
bot.$connect();
|
|
126
|
+
// WebSocket 关闭时清理
|
|
127
|
+
ws.on("close", () => {
|
|
128
|
+
logger.debug(`Sandbox connection closed: ${botName}`);
|
|
129
|
+
this.bots.delete(bot.$id);
|
|
130
|
+
});
|
|
131
|
+
ws.on("error", (error) => {
|
|
132
|
+
logger.error(`Sandbox WebSocket error for ${botName}:`, error);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
logger.info("Sandbox WebSocket server started at /sandbox");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const { provide } = usePlugin();
|
|
139
|
+
provide({
|
|
140
|
+
name: "sandbox",
|
|
141
|
+
description: "Sandbox Adapter",
|
|
142
|
+
mounted: async (p) => {
|
|
143
|
+
const adapter = new SandboxAdapter(p);
|
|
144
|
+
await adapter.start();
|
|
145
|
+
return adapter;
|
|
146
|
+
},
|
|
147
|
+
dispose: async (adapter) => {
|
|
148
|
+
// 关闭所有 bot 连接
|
|
149
|
+
for (const bot of adapter.bots.values()) {
|
|
150
|
+
await bot.$disconnect();
|
|
151
|
+
}
|
|
152
|
+
// 关闭 WebSocket server
|
|
153
|
+
adapter.wss?.close();
|
|
154
|
+
await adapter.stop();
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
// 使用 router 上下文创建 WebSocket server
|
|
158
|
+
plugin.useContext("router", async (router) => {
|
|
159
|
+
// 等待 sandbox adapter 就绪
|
|
160
|
+
plugin.useContext("sandbox", async (adapter) => {
|
|
161
|
+
await adapter.setupWebSocket(router);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
// 使用 web 上下文注册客户端入口
|
|
165
|
+
plugin.useContext("web", (web) => {
|
|
166
|
+
// 注册 Sandbox 适配器的客户端入口文件
|
|
167
|
+
const dispose = web.addEntry({
|
|
168
|
+
production: path.resolve(import.meta.dirname, "../dist/index.js"),
|
|
169
|
+
development: path.resolve(import.meta.dirname, "../client/index.tsx"),
|
|
170
|
+
});
|
|
171
|
+
return dispose;
|
|
172
|
+
});
|
|
173
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhin.js/adapter-sandbox",
|
|
3
|
+
"version": "1.0.22",
|
|
4
|
+
"description": "zhin adapter for sandbox",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"development": "./src/index.ts",
|
|
12
|
+
"import": "./lib/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"client",
|
|
20
|
+
"src",
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"zhin",
|
|
25
|
+
"adapter",
|
|
26
|
+
"process"
|
|
27
|
+
],
|
|
28
|
+
"author": "lc-cn",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"url": "git+https://github.com/zhinjs/zhin.git",
|
|
32
|
+
"type": "git",
|
|
33
|
+
"directory": "plugins/adapters/process"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19.2.2",
|
|
37
|
+
"@types/react-dom": "^19.2.1",
|
|
38
|
+
"radix-ui": "^1.4.3",
|
|
39
|
+
"@radix-ui/themes": "^3.2.1",
|
|
40
|
+
"lucide-react": "^0.469.0",
|
|
41
|
+
"typescript": "^5.3.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@zhin.js/console": "1.0.13",
|
|
45
|
+
"@zhin.js/core": "1.0.17",
|
|
46
|
+
"@zhin.js/http": "1.0.8",
|
|
47
|
+
"@zhin.js/client": "1.0.5",
|
|
48
|
+
"zhin.js": "1.0.17"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@zhin.js/http": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@zhin.js/console": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@zhin.js/client": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsc && (test -f ../../services/console/lib/bin.js && node ../../services/console/lib/bin.js build || echo 'Skipping client build: console not built yet')",
|
|
63
|
+
"clean": "rm -rf lib"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import {
|
|
3
|
+
Bot,
|
|
4
|
+
Adapter,
|
|
5
|
+
usePlugin,
|
|
6
|
+
Message,
|
|
7
|
+
SendOptions,
|
|
8
|
+
segment,
|
|
9
|
+
SendContent,
|
|
10
|
+
MessageType,
|
|
11
|
+
MessageElement,
|
|
12
|
+
Plugin,
|
|
13
|
+
} from "zhin.js";
|
|
14
|
+
import type { WebSocket } from "ws";
|
|
15
|
+
import { Router } from "@zhin.js/http";
|
|
16
|
+
import path from "path";
|
|
17
|
+
|
|
18
|
+
export interface SandboxConfig {
|
|
19
|
+
context: "sandbox";
|
|
20
|
+
ws: WebSocket;
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module "zhin.js" {
|
|
25
|
+
namespace Plugin {
|
|
26
|
+
interface Contexts {
|
|
27
|
+
sandbox: SandboxAdapter;
|
|
28
|
+
router: Router;
|
|
29
|
+
web: any;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RegisteredAdapters {
|
|
34
|
+
sandbox: SandboxAdapter;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const plugin = usePlugin();
|
|
39
|
+
const logger = plugin.logger;
|
|
40
|
+
|
|
41
|
+
interface WebSocketMessage {
|
|
42
|
+
type: MessageType;
|
|
43
|
+
id: string;
|
|
44
|
+
content: MessageElement[] | string;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class SandboxBot extends EventEmitter implements Bot<SandboxConfig, { content: MessageElement[]; ts: number }> {
|
|
49
|
+
$connected: boolean = false;
|
|
50
|
+
|
|
51
|
+
get $id() {
|
|
52
|
+
return this.$config.name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private logger = logger;
|
|
56
|
+
|
|
57
|
+
constructor(public adapter: SandboxAdapter, public $config: SandboxConfig) {
|
|
58
|
+
super();
|
|
59
|
+
this.$config.ws.on("message", (data) => {
|
|
60
|
+
const message = JSON.parse(data.toString()) as WebSocketMessage;
|
|
61
|
+
// 确保 content 是 MessageElement[] 格式
|
|
62
|
+
const content: MessageElement[] = typeof message.content === 'string'
|
|
63
|
+
? [{ type: 'text', data: { text: message.content } }]
|
|
64
|
+
: message.content;
|
|
65
|
+
this.logger.debug(`${this.$config.name} recv ${message.type}(${message.id}):${segment.raw(content)}`);
|
|
66
|
+
const formattedMessage = this.$formatMessage({ content: content, type: message.type, id: message.id, ts: message.timestamp });
|
|
67
|
+
this.adapter.emit("message.receive", formattedMessage);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.$config.ws.on("close", () => {
|
|
71
|
+
this.logger.debug(`Sandbox bot ${this.$config.name} disconnected`);
|
|
72
|
+
this.$connected = false;
|
|
73
|
+
// 从 adapter 中移除 bot
|
|
74
|
+
this.adapter.bots.delete(this.$id);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async $connect(): Promise<void> {
|
|
79
|
+
this.$connected = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async $disconnect(): Promise<void> {
|
|
83
|
+
this.$config.ws.close();
|
|
84
|
+
this.$connected = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$formatMessage({ content, type, id, ts }: { content: MessageElement[]; id: string; type: MessageType; ts: number }) {
|
|
88
|
+
const message = Message.from(
|
|
89
|
+
{ content, ts },
|
|
90
|
+
{
|
|
91
|
+
$id: `${ts}`,
|
|
92
|
+
$adapter: "sandbox" as const,
|
|
93
|
+
$bot: `${this.$config.name}`,
|
|
94
|
+
$sender: {
|
|
95
|
+
id: `${id}`,
|
|
96
|
+
name: `mock`,
|
|
97
|
+
},
|
|
98
|
+
$channel: {
|
|
99
|
+
id: `${id}`,
|
|
100
|
+
type: type,
|
|
101
|
+
},
|
|
102
|
+
$content: content,
|
|
103
|
+
$raw: segment.raw(content),
|
|
104
|
+
$timestamp: ts,
|
|
105
|
+
$recall: async () => {
|
|
106
|
+
await this.$recallMessage(message.$id);
|
|
107
|
+
},
|
|
108
|
+
$reply: async (content: SendContent, quote?: boolean | string): Promise<string> => {
|
|
109
|
+
if (!Array.isArray(content)) content = [content];
|
|
110
|
+
if (quote) content.unshift({ type: "reply", data: { id: typeof quote === "boolean" ? message.$id : quote } });
|
|
111
|
+
return await this.$sendMessage({
|
|
112
|
+
...message.$channel,
|
|
113
|
+
context: "sandbox",
|
|
114
|
+
bot: `${this.$config.name}`,
|
|
115
|
+
content,
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
return message;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async $sendMessage(options: SendOptions): Promise<string> {
|
|
124
|
+
if (!this.$connected) return "";
|
|
125
|
+
this.logger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
|
|
126
|
+
options.bot = this.$config.name;
|
|
127
|
+
options.context = "sandbox";
|
|
128
|
+
this.$config.ws.send(
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
...options,
|
|
131
|
+
content: options.content, // 发送消息段数组
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async $recallMessage(id: string): Promise<void> {
|
|
139
|
+
// 沙盒不支持撤回消息
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class SandboxAdapter extends Adapter<SandboxBot> {
|
|
144
|
+
wss?: ReturnType<Router["ws"]>;
|
|
145
|
+
|
|
146
|
+
constructor(plugin: Plugin) {
|
|
147
|
+
super(plugin, "sandbox", []);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
createBot(config: SandboxConfig): SandboxBot {
|
|
151
|
+
const bot = new SandboxBot(this, config);
|
|
152
|
+
// 将 bot 添加到 bots Map 中
|
|
153
|
+
this.bots.set(bot.$id, bot);
|
|
154
|
+
return bot;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async start(): Promise<void> {
|
|
158
|
+
// start 方法会在 mounted 时被调用
|
|
159
|
+
// WebSocket server 的创建在 useContext("router") 中处理
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async setupWebSocket(router: Router): Promise<void> {
|
|
163
|
+
if (this.wss) return; // 已经设置过了
|
|
164
|
+
// 创建 WebSocket server
|
|
165
|
+
this.wss = router.ws("/sandbox");
|
|
166
|
+
|
|
167
|
+
this.wss.on("connection", (ws: WebSocket, req) => {
|
|
168
|
+
// 为每个连接创建一个唯一的 bot 名称
|
|
169
|
+
const botName = `sandbox-${Math.random().toString(36).slice(2, 9)}`;
|
|
170
|
+
logger.debug(`New sandbox connection: ${botName} from ${req.socket.remoteAddress}`);
|
|
171
|
+
|
|
172
|
+
// 创建 bot 配置
|
|
173
|
+
const config: SandboxConfig = {
|
|
174
|
+
context: "sandbox",
|
|
175
|
+
ws,
|
|
176
|
+
name: botName,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// 创建并连接 bot
|
|
180
|
+
const bot = this.createBot(config);
|
|
181
|
+
bot.$connect();
|
|
182
|
+
|
|
183
|
+
// WebSocket 关闭时清理
|
|
184
|
+
ws.on("close", () => {
|
|
185
|
+
logger.debug(`Sandbox connection closed: ${botName}`);
|
|
186
|
+
this.bots.delete(bot.$id);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
ws.on("error", (error) => {
|
|
190
|
+
logger.error(`Sandbox WebSocket error for ${botName}:`, error);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
logger.info("Sandbox WebSocket server started at /sandbox");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { provide } = usePlugin();
|
|
199
|
+
|
|
200
|
+
provide({
|
|
201
|
+
name: "sandbox",
|
|
202
|
+
description: "Sandbox Adapter",
|
|
203
|
+
mounted: async (p: Plugin) => {
|
|
204
|
+
const adapter = new SandboxAdapter(p);
|
|
205
|
+
await adapter.start();
|
|
206
|
+
return adapter;
|
|
207
|
+
},
|
|
208
|
+
dispose: async (adapter: SandboxAdapter) => {
|
|
209
|
+
// 关闭所有 bot 连接
|
|
210
|
+
for (const bot of adapter.bots.values()) {
|
|
211
|
+
await bot.$disconnect();
|
|
212
|
+
}
|
|
213
|
+
// 关闭 WebSocket server
|
|
214
|
+
adapter.wss?.close();
|
|
215
|
+
await adapter.stop();
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// 使用 router 上下文创建 WebSocket server
|
|
220
|
+
plugin.useContext("router", async (router: Router) => {
|
|
221
|
+
// 等待 sandbox adapter 就绪
|
|
222
|
+
plugin.useContext("sandbox", async (adapter: SandboxAdapter) => {
|
|
223
|
+
await adapter.setupWebSocket(router);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// 使用 web 上下文注册客户端入口
|
|
228
|
+
plugin.useContext("web", (web: any) => {
|
|
229
|
+
// 注册 Sandbox 适配器的客户端入口文件
|
|
230
|
+
const dispose = web.addEntry({
|
|
231
|
+
production: path.resolve(import.meta.dirname, "../dist/index.js"),
|
|
232
|
+
development: path.resolve(import.meta.dirname, "../client/index.tsx"),
|
|
233
|
+
});
|
|
234
|
+
return dispose;
|
|
235
|
+
});
|