diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..1fa3a16 --- /dev/null +++ b/404.html @@ -0,0 +1,22 @@ + + +
+ + +{const{slotScopeIds:I}=p;I&&(j=j?j.concat(I):I);const _=o(g),P=x(i(g),p,_,A,$,j,K);return P&&ln(P)&&P.data==="]"?i(p.anchor=P):(bt(),c(p.anchor=u("]"),_,P),P)},O=(g,p,A,$,j,K)=>{if(cn(g.parentElement,1)||bt(),p.el=null,K){const P=G(g);for(;;){const b=i(g);if(b&&b!==P)l(b);else break}}const I=i(g),_=o(g);return l(g),n(null,p,_,I,A,$,on(_),j),A&&(A.vnode.el=p.el,Gi(A,p.el)),I},G=(g,p="[",A="]")=>{let $=0;for(;g;)if(g=i(g),g&&ln(g)&&(g.data===p&&$++,g.data===A)){if($===0)return i(g);$--}return g},U=(g,p,A)=>{const $=p.parentNode;$&&$.replaceChild(g,p);let j=A;for(;j;)j.vnode.el===p&&(j.vnode.el=j.subTree.el=g),j=j.parent},W=g=>g.nodeType===1&&g.tagName==="TEMPLATE";return[a,h]}const nr="data-allow-mismatch",Ol={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function cn(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(nr);)e=e.parentElement;const n=e&&e.getAttribute(nr);if(n==null)return!1;if(n==="")return!0;{const s=n.split(",");return t===0&&s.includes("children")?!0:n.split(",").includes(Ol[t])}}On().requestIdleCallback;On().cancelIdleCallback;const pt=e=>!!e.type.__asyncLoader,Hn=e=>e.type.__isKeepAlive;function Ml(e,t){Ei(e,"a",t)}function Il(e,t){Ei(e,"da",t)}function Ei(e,t,n=ue){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Dn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Hn(r.parent.vnode)&&Ll(s,t,n,r),r=r.parent}}function Ll(e,t,n,s){const r=Dn(t,e,s,!0);$n(()=>{Os(s[t],r)},n)}function Dn(e,t,n=ue,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{rt();const l=Qt(n),c=He(t,n,e,o);return l(),it(),c});return s?r.unshift(i):r.push(i),i}}const Xe=e=>(t,n=ue)=>{(!Yt||e==="sp")&&Dn(e,(...s)=>t(...s),n)},Pl=Xe("bm"),Ot=Xe("m"),Nl=Xe("bu"),Fl=Xe("u"),Ci=Xe("bum"),$n=Xe("um"),Hl=Xe("sp"),Dl=Xe("rtg"),$l=Xe("rtc");function jl(e,t=ue){Dn("ec",e,t)}const Ti="components";function Qa(e,t){return Ri(Ti,e,!0,t)||e}const Ai=Symbol.for("v-ndc");function Za(e){return re(e)?Ri(Ti,e,!1)||e:e||Ai}function Ri(e,t,n=!0,s=!1){const r=ce||ue;if(r){const i=r.type;{const l=Sc(i,!1);if(l&&(l===t||l===Le(t)||l===Rn(Le(t))))return i}const o=sr(r[e]||i[e],t)||sr(r.appContext[e],t);return!o&&s?i:o}}function sr(e,t){return e&&(e[t]||e[Le(t)]||e[Rn(Le(t))])}function ef(e,t,n,s){let r;const i=n,o=B(e);if(o||re(e)){const l=o&&ht(e);let c=!1;l&&(c=!Ie(e),e=In(e)),r=new Array(e.length);for(let u=0,a=e.length;ut(l,c,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,u=l.length;cGt(t)?!(t.type===ve||t.type===xe&&!Oi(t.children)):!0)?e:null}function nf(e,t){const n={};for(const s in e)n[/[A-Z]/.test(s)?`on:${s}`:hn(s)]=e[s];return n}const vs=e=>e?eo(e)?Bn(e):vs(e.parent):null,$t=le(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>vs(e.parent),$root:e=>vs(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Vs(e),$forceUpdate:e=>e.f||(e.f=()=>{js(e.update)}),$nextTick:e=>e.n||(e.n=Fn.bind(e.proxy)),$watch:e=>lc.bind(e)}),Qn=(e,t)=>e!==Z&&!e.__isScriptSetup&&z(e,t),Vl={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;let u;if(t[0]!=="$"){const x=o[t];if(x!==void 0)switch(x){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Qn(s,t))return o[t]=1,s[t];if(r!==Z&&z(r,t))return o[t]=2,r[t];if((u=e.propsOptions[0])&&z(u,t))return o[t]=3,i[t];if(n!==Z&&z(n,t))return o[t]=4,n[t];_s&&(o[t]=0)}}const a=$t[t];let h,v;if(a)return t==="$attrs"&&me(e.attrs,"get",""),a(e);if((h=l.__cssModules)&&(h=h[t]))return h;if(n!==Z&&z(n,t))return o[t]=4,n[t];if(v=c.config.globalProperties,z(v,t))return v[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return Qn(r,t)?(r[t]=n,!0):s!==Z&&z(s,t)?(s[t]=n,!0):z(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i}},o){let l;return!!n[o]||e!==Z&&z(e,o)||Qn(t,o)||(l=i[0])&&z(l,o)||z(s,o)||z($t,o)||z(r.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:z(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function sf(){return Ul().slots}function Ul(){const e=Un();return e.setupContext||(e.setupContext=no(e))}function rr(e){return B(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let _s=!0;function Bl(e){const t=Vs(e),n=e.proxy,s=e.ctx;_s=!1,t.beforeCreate&&ir(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:u,created:a,beforeMount:h,mounted:v,beforeUpdate:x,updated:L,activated:O,deactivated:G,beforeDestroy:U,beforeUnmount:W,destroyed:g,unmounted:p,render:A,renderTracked:$,renderTriggered:j,errorCaptured:K,serverPrefetch:I,expose:_,inheritAttrs:P,components:b,directives:V,filters:se}=t;if(u&&kl(u,s,null),o)for(const Y in o){const F=o[Y];q(F)&&(s[Y]=F.bind(n))}if(r){const Y=r.call(n,n);ne(Y)&&(e.data=Ln(Y))}if(_s=!0,i)for(const Y in i){const F=i[Y],fe=q(F)?F.bind(n,n):q(F.get)?F.get.bind(n,n):Ue,Zt=!q(F)&&q(F.set)?F.set.bind(n):Ue,ot=oe({get:fe,set:Zt});Object.defineProperty(s,Y,{enumerable:!0,configurable:!0,get:()=>ot.value,set:De=>ot.value=De})}if(l)for(const Y in l)Mi(l[Y],s,n,Y);if(c){const Y=q(c)?c.call(n):c;Reflect.ownKeys(Y).forEach(F=>{Xl(F,Y[F])})}a&&ir(a,e,"c");function H(Y,F){B(F)?F.forEach(fe=>Y(fe.bind(n))):F&&Y(F.bind(n))}if(H(Pl,h),H(Ot,v),H(Nl,x),H(Fl,L),H(Ml,O),H(Il,G),H(jl,K),H($l,$),H(Dl,j),H(Ci,W),H($n,p),H(Hl,I),B(_))if(_.length){const Y=e.exposed||(e.exposed={});_.forEach(F=>{Object.defineProperty(Y,F,{get:()=>n[F],set:fe=>n[F]=fe})})}else e.exposed||(e.exposed={});A&&e.render===Ue&&(e.render=A),P!=null&&(e.inheritAttrs=P),b&&(e.components=b),V&&(e.directives=V),I&&Si(e)}function kl(e,t,n=Ue){B(e)&&(e=bs(e));for(const s in e){const r=e[s];let i;ne(r)?"default"in r?i=At(r.from||s,r.default,!0):i=At(r.from||s):i=At(r),ae(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function ir(e,t,n){He(B(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Mi(e,t,n,s){let r=s.includes(".")?Ki(n,s):()=>n[s];if(re(e)){const i=t[e];q(i)&&Fe(r,i)}else if(q(e))Fe(r,e.bind(n));else if(ne(e))if(B(e))e.forEach(i=>Mi(i,t,n,s));else{const i=q(e.handler)?e.handler.bind(n):t[e.handler];q(i)&&Fe(r,i,e)}}function Vs(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(u=>Sn(c,u,o,!0)),Sn(c,t,o)),ne(t)&&i.set(t,c),c}function Sn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Sn(e,i,n,!0),r&&r.forEach(o=>Sn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=Wl[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const Wl={data:or,props:lr,emits:lr,methods:Ft,computed:Ft,beforeCreate:_e,created:_e,beforeMount:_e,mounted:_e,beforeUpdate:_e,updated:_e,beforeDestroy:_e,beforeUnmount:_e,destroyed:_e,unmounted:_e,activated:_e,deactivated:_e,errorCaptured:_e,serverPrefetch:_e,components:Ft,directives:Ft,watch:ql,provide:or,inject:Kl};function or(e,t){return t?e?function(){return le(q(e)?e.call(this,this):e,q(t)?t.call(this,this):t)}:t:e}function Kl(e,t){return Ft(bs(e),bs(t))}function bs(e){if(B(e)){const t={};for(let n=0;n Some API may return a list of items. In this case, the response will be a You can use the Some API may return a list of items. In this case, the response will be a You can use the Get a channel by ID. Returns a channel object. Get channels in a guild. Returns a list of channel objects. Get a channel by ID. Returns a channel object. Get channels in a guild. Returns a list of channel objects. Get a guild by ID. Returns a guild object. Get guilds where the current user is a member. Returns a list of partial guild objects. 处理来自群组的邀请。 Get a guild by ID. Returns a guild object. Get guilds where the current user is a member. Returns a list of partial guild objects. 处理来自群组的邀请。 获取群成员信息。 获取群成员列表。 将某个用户踢出群组。 将某个用户禁言。如果传入的禁言时长为 处理加群请求。 获取群成员信息。 获取群成员列表。 将某个用户踢出群组。 将某个用户禁言。如果传入的禁言时长为 处理加群请求。 向特定频道发送消息。 WARNING 只要你能够获取到会话对象,你就不应使用此 API,而应该使用 TIP 向特定用户发送私聊消息。 获取特定消息。 撤回特定消息。 修改特定消息。 获取频道消息列表。 向特定频道发送消息。 WARNING 只要你能够获取到会话对象,你就不应使用此 API,而应该使用 TIP 向特定用户发送私聊消息。 获取特定消息。 撤回特定消息。 修改特定消息。 获取频道消息列表。 向特定消息添加表态。 从特定消息删除某个用户添加的特定表态。如果没有传入用户 ID 则表示删除自己的表态。 从特定消息清除某个特定表态。如果没有传入表态名称则表示清除所有表态。 获取添加特定消息的特定表态的用户列表。 向特定消息添加表态。 从特定消息删除某个用户添加的特定表态。如果没有传入用户 ID 则表示删除自己的表态。 从特定消息清除某个特定表态。如果没有传入表态名称则表示清除所有表态。 获取添加特定消息的特定表态的用户列表。 设置群组内用户的角色。 取消群组内用户的角色。 获取群组角色列表。 创建群组角色。 修改群组角色。 删除群组角色。 设置群组内用户的角色。 取消群组内用户的角色。 获取群组角色列表。 创建群组角色。 修改群组角色。 删除群组角色。 获取机器人自己的信息。 获取用户信息。 获取机器人的好友列表。 处理好友请求。 获取机器人自己的信息。 获取用户信息。 获取机器人的好友列表。 处理好友请求。 作为一个跨平台的聊天协议,Satori 提供了访问任意平台原生接口的能力。这意味着,你可以大多数情况下编写通用代码,并在需要的时候使用原生接口来实现平台特定功能。 这些原生能力被统称为内部接口,涵盖了 API、事件、消息元素、路由等各个方面。 Satori 协议的大多数 API 都需要传入 大多数聊天平台的 所以对于任何一个 Login,我们实际上存在两个不同的概念: TIP 如果你读到这里仍然难以区分 这两个条件是充分但不必要的,换言之,即便两个 Login 所拥有的 ID 或者 API 兼容,也可以不使用相同的 TIP 需要注意的是,这两个概念实际上是多对多的,我们可以举出一些特殊场景: SDK 可以通过 例如,Discord 平台提供了 Restful API,那么你可以进行如下请求: 除了作为前缀的路由和额外的 SDK 可以通过 标准事件的平台原生字段也可以通过上述 TIP 有些平台原生事件可以直接对应到标准事件。当这些事件触发时,SDK 可以同时触发标准事件和平台原生事件。这两个事件都带有 平台可以提供原生消息元素,但需要加上适配器名称作为前缀。下面是一个例子: 标准元素的平台原生属性也可以通过加上适配器名称作为前缀的方式声明。下面是一个例子: TIP 平台原生消息元素的属性是否需要前缀由 SDK 实现自行决定。如果某个消息元素希望在未来标准化,那么加上前缀可以降低迁移成本。如果没有标准化需要,那么去掉前缀在书写上更方便。 作为一个跨平台的聊天协议,Satori 提供了访问任意平台原生接口的能力。这意味着,你可以大多数情况下编写通用代码,并在需要的时候使用原生接口来实现平台特定功能。 这些原生能力被统称为内部接口,涵盖了 API、事件、消息元素、路由等各个方面。 Satori 协议的大多数 API 都需要传入 大多数聊天平台的 所以对于任何一个 Login,我们实际上存在两个不同的概念: TIP 如果你读到这里仍然难以区分 这两个条件是充分但不必要的,换言之,即便两个 Login 所拥有的 ID 或者 API 兼容,也可以不使用相同的 TIP 需要注意的是,这两个概念实际上是多对多的,我们可以举出一些特殊场景: SDK 可以通过 例如,Discord 平台提供了 Restful API,那么你可以进行如下请求: 除了作为前缀的路由和额外的 SDK 可以通过 标准事件的平台原生字段也可以通过上述 TIP 有些平台原生事件可以直接对应到标准事件。当这些事件触发时,SDK 可以同时触发标准事件和平台原生事件。这两个事件都带有 平台可以提供原生消息元素,但需要加上适配器名称作为前缀。下面是一个例子: 标准元素的平台原生属性也可以通过加上适配器名称作为前缀的方式声明。下面是一个例子: TIP 平台原生消息元素的属性是否需要前缀由 SDK 实现自行决定。如果某个消息元素希望在未来标准化,那么加上前缀可以降低迁移成本。如果没有标准化需要,那么去掉前缀在书写上更方便。 TIP 这是一个可选功能。 WARNING 这是一个实验性功能。 元信息对象包含了与 SDK 状态相关、与具体的账号无关的信息,例如 代理路由 等。 元信息通过以下方式获取和更新: 需要注意的是, 元信息 API 通过 TIP 这是一个可选功能。 WARNING 这是一个实验性功能。 元信息对象包含了与 SDK 状态相关、与具体的账号无关的信息,例如 代理路由 等。 元信息通过以下方式获取和更新: 需要注意的是, 元信息 API 通过 如果要发送的消息中含有图片或其他媒体资源,可以使用此 API 将文件上传至 Satori 服务器并转换为 URL,以便在消息编码中使用。 与其他 API 不同,上传文件的请求体遵循 其中, 返回值是一个字典类型,其中的每个键分别对应于请求体中的文件标识符,值是一个 URL 字符串,可以在消息编码中使用。下面是一个示例的返回值: 在实现此 API 时,如果平台已经支持了文件上传功能,可以直接使用平台提供的上传 API,返回平台的 URL 即可。如果平台不支持文件上传功能,应当回退到 SDK 提供的默认实现。 SDK 可以基于本地文件系统实现上传功能。上传到本地文件系统中的文件 URL 通过 内部链接的标准格式如下: 其中, SDK 可以根据需要自行设计资源路径,但以下划线 Satori 会将所有内部 API 访问重定向到 因此,适配器开发者无需专门实现内部 API,只需实现 上一节中已经提到,在不支持文件上传的平台上调用 TIP 场景:通过平台 API 请求资源 某些平台使用 ID 标识资源文件 (例如 Lark)。当你接收到来自平台的消息时,拿到的是资源 ID 而非链接。此时你需要调用平台 API,将资源 ID 转换为链接,才能构造合法的消息元素。 为了避免在不必要的场合损失性能,更推荐的方式是直接将资源 ID 封装进内部链接,并立即构造消息元素。等到真正需要请求资源时再调用平台的 API。 TIP 场景:资源链接不宜直接公开 对于另一些平台,尽管其提供的资源链接是可用的,但这个链接中会明文包含机器人令牌,并非可以公开使用的链接 (例如 Telegram)。因此,对于这些平台中的资源,我们也不能直接使用其链接,同样需要将其封装进内部链接。此时内部链接就是单纯的代理。 与内部链接相比,另一些实践则是不推荐的。下面的方案来源于一些经典聊天协议的实现。通过与这些方案进行对比,可以更好地理解内部链接的优势。 WARNING 不推荐: 一种不推荐的方案是直接下载资源,并转换为 WARNING 不推荐:本地代理 另一种方案是由 SDK 额外提供一个用于访问资源的路由 (比如下文介绍的代理路由),并将资源链接转换为能访问到该路由的 URL。相比内部链接,这种方案有两个缺点: 假设你在开发基于 Satori 的聊天平台客户端,你希望可以直接将 Satori 协议中给出的资源链接用于 HTML,但很多情况下你都难以如愿: 为此,SDK 需要额外提供一个代理路由 下面是两个典型的代理路由请求示例 (分别对应上述两种情况): 在具体的应用场景中,代理路由可根据需要添加 为了辨别需要代理的路径以防滥用,Satori 还引入了 根据 综上所述,我们总结出了一套关于资源链接的最佳实践: 对于核心库开发者,你需要: 对于适配器开发者,你需要: 如果要发送的消息中含有图片或其他媒体资源,可以使用此 API 将文件上传至 Satori 服务器并转换为 URL,以便在消息编码中使用。 与其他 API 不同,上传文件的请求体遵循 其中, 返回值是一个字典类型,其中的每个键分别对应于请求体中的文件标识符,值是一个 URL 字符串,可以在消息编码中使用。下面是一个示例的返回值: 在实现此 API 时,如果平台已经支持了文件上传功能,可以直接使用平台提供的上传 API,返回平台的 URL 即可。如果平台不支持文件上传功能,应当回退到 SDK 提供的默认实现。 SDK 可以基于本地文件系统实现上传功能。上传到本地文件系统中的文件 URL 通过 内部链接的标准格式如下: 其中, SDK 可以根据需要自行设计资源路径,但以下划线 Satori 会将所有内部 API 访问重定向到 因此,适配器开发者无需专门实现内部 API,只需实现 上一节中已经提到,在不支持文件上传的平台上调用 TIP 场景:通过平台 API 请求资源 某些平台使用 ID 标识资源文件 (例如 Lark)。当你接收到来自平台的消息时,拿到的是资源 ID 而非链接。此时你需要调用平台 API,将资源 ID 转换为链接,才能构造合法的消息元素。 为了避免在不必要的场合损失性能,更推荐的方式是直接将资源 ID 封装进内部链接,并立即构造消息元素。等到真正需要请求资源时再调用平台的 API。 TIP 场景:资源链接不宜直接公开 对于另一些平台,尽管其提供的资源链接是可用的,但这个链接中会明文包含机器人令牌,并非可以公开使用的链接 (例如 Telegram)。因此,对于这些平台中的资源,我们也不能直接使用其链接,同样需要将其封装进内部链接。此时内部链接就是单纯的代理。 与内部链接相比,另一些实践则是不推荐的。下面的方案来源于一些经典聊天协议的实现。通过与这些方案进行对比,可以更好地理解内部链接的优势。 WARNING 不推荐: 一种不推荐的方案是直接下载资源,并转换为 WARNING 不推荐:本地代理 另一种方案是由 SDK 额外提供一个用于访问资源的路由 (比如下文介绍的代理路由),并将资源链接转换为能访问到该路由的 URL。相比内部链接,这种方案有两个缺点: 假设你在开发基于 Satori 的聊天平台客户端,你希望可以直接将 Satori 协议中给出的资源链接用于 HTML,但很多情况下你都难以如愿: 为此,SDK 需要额外提供一个代理路由 下面是两个典型的代理路由请求示例 (分别对应上述两种情况): 在具体的应用场景中,代理路由可根据需要添加 为了辨别需要代理的路径以防滥用,Satori 还引入了 根据 综上所述,我们总结出了一套关于资源链接的最佳实践: 对于核心库开发者,你需要: 对于适配器开发者,你需要: Satori 是一个通用的聊天协议。我们希望 Satori 能够抹平不同聊天平台之间的差异,让开发者以更低的成本开发出跨平台、可扩展、高性能的聊天应用。 Satori 的名称来源于游戏东方 Project 中的角色 古明地觉 (Komeiji Satori)。古明地觉能够以心灵感应的方式与各种动物交流,取这个名字是希望 Satori 能够成为各个聊天平台之间的桥梁。 Satori 的开发团队长期从事聊天机器人开发,熟悉各种聊天平台的通信方式。经过长达 4 年的发展,Satori 有了健全的设计和完善的实现。目前,Satori 官方提供了超过 15 个聊天平台的适配器,完全覆盖了世界上主流的聊天平台: 这些适配器不仅为你带来了开箱即用的体验,也从实际上证明了 Satori 协议的通用性和扩展性。 不必担心使用 Satori 后会失去对聊天平台的控制。得益于 Satori 的内部接口机制,你完全可以大多数情况下编写通用代码,并在需要的时候使用内部接口来实现平台特定功能。 此外,Satori 还为规模化的场景提供了全套的解决方案。小到个人电脑上的聊天机器人,大到分布式集群上的聊天平台后端,Satori 都能满足你的需求。 Satori 是一个通用的聊天协议。我们希望 Satori 能够抹平不同聊天平台之间的差异,让开发者以更低的成本开发出跨平台、可扩展、高性能的聊天应用。 Satori 的名称来源于游戏东方 Project 中的角色 古明地觉 (Komeiji Satori)。古明地觉能够以心灵感应的方式与各种动物交流,取这个名字是希望 Satori 能够成为各个聊天平台之间的桥梁。 Satori 的开发团队长期从事聊天机器人开发,熟悉各种聊天平台的通信方式。经过长达 4 年的发展,Satori 有了健全的设计和完善的实现。目前,Satori 官方提供了超过 15 个聊天平台的适配器,完全覆盖了世界上主流的聊天平台: 这些适配器不仅为你带来了开箱即用的体验,也从实际上证明了 Satori 协议的通用性和扩展性。 不必担心使用 Satori 后会失去对聊天平台的控制。得益于 Satori 的内部接口机制,你完全可以大多数情况下编写通用代码,并在需要的时候使用内部接口来实现平台特定功能。 此外,Satori 还为规模化的场景提供了全套的解决方案。小到个人电脑上的聊天机器人,大到分布式集群上的聊天平台后端,Satori 都能满足你的需求。 Satori 协议规定了一套基于 HTTP 的 API 服务,用于发送消息和调用其他功能。 这是一套 HTTP RPC 风格的 API,所有 URL 的形式均为 目前 Satori 仅有 v1 一个版本。 绝大多数 API 的请求都使用 POST,参数通过 请求头中需要包含 一个合法的请求示例形如: 鉴权通过 HTTP API 中的 如果 SDK 没有配置鉴权,则应用无需提供上述请求头。 TIP 如果某个标准 API 没有被某个平台支持,则应该返回 404 而非 501 (NOT IMPLEMENTED)。只有当一个 API 被平台支持但是未被适配器实现时,才应该返回 501。 目前仅有 API 名称本身是规范的用法。我们将在后续版本中提供更全面的标准特性列表。 除了标准 API 外,Satori 还提供了一些进阶功能。 部分 API 可能会返回分页数据。这种情况下,响应会是一个 你可以使用 极少数 API 返回可双向延伸的分页数据。这种情况下,响应会是一个 在对应的 API 中,你可以通过 如果 Satori 协议规定了一套基于 HTTP 的 API 服务,用于发送消息和调用其他功能。 这是一套 HTTP RPC 风格的 API,所有 URL 的形式均为 目前 Satori 仅有 v1 一个版本。 绝大多数 API 的请求都使用 POST,参数通过 请求头中需要包含 一个合法的请求示例形如: 鉴权通过 HTTP API 中的 如果 SDK 没有配置鉴权,则应用无需提供上述请求头。 TIP 如果某个标准 API 没有被某个平台支持,则应该返回 404 而非 501 (NOT IMPLEMENTED)。只有当一个 API 被平台支持但是未被适配器实现时,才应该返回 501。 目前仅有 API 名称本身是规范的用法。我们将在后续版本中提供更全面的标准特性列表。 除了标准 API 外,Satori 还提供了一些进阶功能。 部分 API 可能会返回分页数据。这种情况下,响应会是一个 你可以使用 极少数 API 返回可双向延伸的分页数据。这种情况下,响应会是一个 在对应的 API 中,你可以通过 如果 基本元素是最常见的消息元素,它们能够在大多数平台上正常显示,是组成消息的基本单位。 资源消息元素表示文本中存在的资源文件。不同的平台对资源文件的支持存在较大的差异。发送时只需提供 除了上述通用属性外,还支持下面的属性: 除了上述通用属性外,还支持下面的属性: 除了上述通用属性外,还支持下面的属性: 除了上述通用属性外,还支持下面的属性: 修饰元素用于修饰其中的内容。如果对应的平台不支持对应的元素,可以忽略这个元素本身,正常渲染其中的子元素。 当出现 部分平台允许你模拟其他用户发送消息: 在支持转发的平台上,你可以使用 在支持合并转发的平台上,你可以使用带有 元信息元素通常不会被渲染,但会影响到消息的发送行为。 交互元素用于显然消息中的可交互性内容。如果平台不支持此类元素且难以提供回退,可以直接忽略整个元素。实现侧应当根据平台特性,针对性地返回带有交互和不带有交互的消息。 按钮目前支持三种不同的类型: 基本元素是最常见的消息元素,它们能够在大多数平台上正常显示,是组成消息的基本单位。 资源消息元素表示文本中存在的资源文件。不同的平台对资源文件的支持存在较大的差异。发送时只需提供 除了上述通用属性外,还支持下面的属性: 除了上述通用属性外,还支持下面的属性: 除了上述通用属性外,还支持下面的属性: 除了上述通用属性外,还支持下面的属性: 修饰元素用于修饰其中的内容。如果对应的平台不支持对应的元素,可以忽略这个元素本身,正常渲染其中的子元素。 当出现 部分平台允许你模拟其他用户发送消息: 在支持转发的平台上,你可以使用 在支持合并转发的平台上,你可以使用带有 元信息元素通常不会被渲染,但会影响到消息的发送行为。 交互元素用于显然消息中的可交互性内容。如果平台不支持此类元素且难以提供回退,可以直接忽略整个元素。实现侧应当根据平台特性,针对性地返回带有交互和不带有交互的消息。 按钮目前支持三种不同的类型: Satori 协议规定了两套事件服务,分别基于 WebSocket 和 WebHook。 事件分为普通事件与登录事件,其中登录事件特指与 Login 变化相关的事件 (如 login-added)。所有事件都采用上述数据结构,不过在细节上有所区别: 事件中的各属性遵循 资源提升 规则。 WebSocket 服务用于在 Satori SDK 与应用之间维护一个持久的、有状态的链接。通过这个链接,Satori 应用可以实时接收 SDK 推送的事件。 WebSocket 服务的地址为 目前 Satori 仅有 v1 一个版本。 总的来说,Satori 应用需要在连接后遵循以下步骤: 信令的数据结构如下: WebSocket 鉴权通过 IDENTIFY 信令的 如果 SDK 没有配置鉴权,则应用无需提供上述字段。 当连接短暂中断时,Satori 应用可以通过 登录事件将不会在会话恢复过程中推送,因为在 TIP 这是一个可选功能。 WebHook 服务是指,Satori SDK 在接收到平台事件时,向应用提供的 HTTP 地址推送事件。一个 SDK 应当可以配置多个 WebHook,并允许应用对发送者进行鉴权。这些 WebHook 的配置方式由 SDK 自身决定,本协议仅规范化了一组 API,不做强制要求。 事件推送以 POST 的形式进行。请求头包含 WebHook 所涉及的信令仅包含 应用收到 WebHook 请求时,如果能够顺利鉴权并处理请求,应当返回 2XX 的状态码。如果鉴权失败,应当返回 4XX 的状态码。如果处理失败,应当返回 5XX 的状态码。 TIP 这里的鉴权与 API 与 WebSocket 中的鉴权逻辑类似,但方向相反。 Satori 应用可以要求 SDK 在发送 WebHook 请求时附带一个 Satori 协议规定了两套事件服务,分别基于 WebSocket 和 WebHook。 事件分为普通事件与登录事件,其中登录事件特指与 Login 变化相关的事件 (如 login-added)。所有事件都采用上述数据结构,不过在细节上有所区别: 事件中的各属性遵循 资源提升 规则。 WebSocket 服务用于在 Satori SDK 与应用之间维护一个持久的、有状态的链接。通过这个链接,Satori 应用可以实时接收 SDK 推送的事件。 WebSocket 服务的地址为 目前 Satori 仅有 v1 一个版本。 总的来说,Satori 应用需要在连接后遵循以下步骤: 信令的数据结构如下: WebSocket 鉴权通过 IDENTIFY 信令的 如果 SDK 没有配置鉴权,则应用无需提供上述字段。 当连接短暂中断时,Satori 应用可以通过 登录事件将不会在会话恢复过程中推送,因为在 TIP 这是一个可选功能。 WebHook 服务是指,Satori SDK 在接收到平台事件时,向应用提供的 HTTP 地址推送事件。一个 SDK 应当可以配置多个 WebHook,并允许应用对发送者进行鉴权。这些 WebHook 的配置方式由 SDK 自身决定,本协议仅规范化了一组 API,不做强制要求。 事件推送以 POST 的形式进行。请求头包含 WebHook 所涉及的信令仅包含 应用收到 WebHook 请求时,如果能够顺利鉴权并处理请求,应当返回 2XX 的状态码。如果鉴权失败,应当返回 4XX 的状态码。如果处理失败,应当返回 5XX 的状态码。 TIP 这里的鉴权与 API 与 WebSocket 中的鉴权逻辑类似,但方向相反。 Satori 应用可以要求 SDK 在发送 WebHook 请求时附带一个 Satori 协议的通信方式分为两块: 在我们开始之前,先来了解一些与 Satori 相关的核心概念。 SDK 是指实现了 Satori 协议的软件。应用 (Application) 是指接入了 Satori 协议的软件。应用通过与 SDK 通信,以实现聊天平台的功能。 平台 (Platform) 是指聊天平台,比如 Discord、Telegram 等。同一平台内的用户间具有相互发送消息的能力,而不同平台的用户间则没有。对于 Rocket Chat 这一类可自建的聊天平台而言,每个独立的自建服务器都视为不同的平台。 消息 (Message) 是字面意义上的消息。通常是文本或富文本格式的,有时也会包含图片、语音等媒体资源。在 Koishi 中,消息通过消息元素进行统一编码。 频道 (Channel) 是消息的集合。一个频道包含了具备时间、逻辑顺序的一系列消息。频道又分为私聊频道和群聊频道,其中私聊频道有且仅有两人参与,而群聊频道可以有任意多人参与。 群组 (Guild) 是平台用户的集合。一个群组通常会同时包含一组用户和频道,并通过权限机制让其中的部分用户进行管理。在部分平台中,群组和群聊频道的概念恰好是重合的 (例如 QQ):一个群组内有且仅有一个群聊频道。私聊频道不属于任何群组。 由于不同平台实现的差异,Satori 协议中的大部分字段都是可选的。可选字段的类型后面会有一个 对于任意可选字段,相关 API 调用的结果中可能不含该字段,也可能该字段的值为 Satori 协议中的资源是指一类具有确定结构的对象。例如,用户、频道、消息 等都是资源。部分事件和 API 的返回值中会包含这些资源对象。 资源对象的某个字段可以是另一个资源对象,例如消息对象中的 Satori 协议的通信方式分为两块: 在我们开始之前,先来了解一些与 Satori 相关的核心概念。 SDK 是指实现了 Satori 协议的软件。应用 (Application) 是指接入了 Satori 协议的软件。应用通过与 SDK 通信,以实现聊天平台的功能。 平台 (Platform) 是指聊天平台,比如 Discord、Telegram 等。同一平台内的用户间具有相互发送消息的能力,而不同平台的用户间则没有。对于 Rocket Chat 这一类可自建的聊天平台而言,每个独立的自建服务器都视为不同的平台。 消息 (Message) 是字面意义上的消息。通常是文本或富文本格式的,有时也会包含图片、语音等媒体资源。在 Koishi 中,消息通过消息元素进行统一编码。 频道 (Channel) 是消息的集合。一个频道包含了具备时间、逻辑顺序的一系列消息。频道又分为私聊频道和群聊频道,其中私聊频道有且仅有两人参与,而群聊频道可以有任意多人参与。 群组 (Guild) 是平台用户的集合。一个群组通常会同时包含一组用户和频道,并通过权限机制让其中的部分用户进行管理。在部分平台中,群组和群聊频道的概念恰好是重合的 (例如 QQ):一个群组内有且仅有一个群聊频道。私聊频道不属于任何群组。 由于不同平台实现的差异,Satori 协议中的大部分字段都是可选的。可选字段的类型后面会有一个 对于任意可选字段,相关 API 调用的结果中可能不含该字段,也可能该字段的值为 Satori 协议中的资源是指一类具有确定结构的对象。例如,用户、频道、消息 等都是资源。部分事件和 API 的返回值中会包含这些资源对象。 资源对象的某个字段可以是另一个资源对象,例如消息对象中的 Satori 中的消息使用消息元素 (Message Element) 进行编码。消息元素的语法与 XHTML 类似。 消息元素的语法与 HTML 类似,但是不完全相同。 你可以在消息元素内使用任何字符。不过部分特殊字符需要转义: 根据上下文的不同,有些字符可能不需要被转义或使用其他的转义方式。 除此以外,你可以使用十进制或十六进制转义任何字符。例如 使用一对尖括号包裹元素名,加上可选的属性、闭合指示符,就构成了一个标签。 元素名由小写字母、数字和连字符组成,且必须以字母开头。在元素名前后添加 起始或自闭合标签的元素名后接受可选的属性列表。每个属性必须形如以下形式: 下面是一段示例: 一个元素要么是自闭合标签,要么由一对同名的起始标签和结束标签构成。元素的内容指起始标签和结束标签中间的部分,可以包含文本内容或其他元素。对于自闭合标签,元素的内容为空。下面是一段示例: 当存在未配对的元素时,将自动视为文本内容的一部分。文本内容前后如果存在包含换行符的连续空白字符,则会被忽略。这意味着下面两段代码是等价的: 使用成对的 关于 Satori 内置的消息元素,请参考 标准元素。 Satori 中的消息使用消息元素 (Message Element) 进行编码。消息元素的语法与 XHTML 类似。 消息元素的语法与 HTML 类似,但是不完全相同。 你可以在消息元素内使用任何字符。不过部分特殊字符需要转义: 根据上下文的不同,有些字符可能不需要被转义或使用其他的转义方式。 除此以外,你可以使用十进制或十六进制转义任何字符。例如 使用一对尖括号包裹元素名,加上可选的属性、闭合指示符,就构成了一个标签。 元素名由小写字母、数字和连字符组成,且必须以字母开头。在元素名前后添加 起始或自闭合标签的元素名后接受可选的属性列表。每个属性必须形如以下形式: 下面是一段示例: 一个元素要么是自闭合标签,要么由一对同名的起始标签和结束标签构成。元素的内容指起始标签和结束标签中间的部分,可以包含文本内容或其他元素。对于自闭合标签,元素的内容为空。下面是一段示例: 当存在未配对的元素时,将自动视为文本内容的一部分。文本内容前后如果存在包含换行符的连续空白字符,则会被忽略。这意味着下面两段代码是等价的: 使用成对的 关于 Satori 内置的消息元素,请参考 标准元素。 根据 ID 获取频道。返回一个 Channel 对象。 获取群组中的全部频道。返回一个 Channel 的 分页列表。 创建群组频道。返回一个 Channel 对象。 修改群组频道。 禁言群组频道。如果传入的禁言时长为 创建一个私聊频道。返回一个 Channel 对象。 根据 ID 获取频道。返回一个 Channel 对象。 获取群组中的全部频道。返回一个 Channel 的 分页列表。 创建群组频道。返回一个 Channel 对象。 修改群组频道。 禁言群组频道。如果传入的禁言时长为 创建一个私聊频道。返回一个 Channel 对象。 根据 ID 获取。返回一个 Guild 对象。 获取当前用户加入的全部群组。返回一个 Guild 的 分页列表。 处理来自群组的邀请。 加入群组时触发。必需资源: 群组被修改时触发。必需资源: 退出群组时触发。必需资源: 接收到新的入群邀请时触发。必需资源: 根据 ID 获取。返回一个 Guild 对象。 获取当前用户加入的全部群组。返回一个 Guild 的 分页列表。 处理来自群组的邀请。 加入群组时触发。必需资源: 群组被修改时触发。必需资源: 退出群组时触发。必需资源: 接收到新的入群邀请时触发。必需资源: TIP 交互功能主要通过机器人提供,并由用户在聊天应用中触发。如果你要实现或接入的聊天平台不支持机器人相关功能,那么可以直接忽略本节。 类型为 调用斜线指令时触发。资源 TIP 许多平台都支持斜线指令,但它们的实现方式各不相同。如果平台的斜线指令仅仅提供在前端,机器人无法直接判断一个事件是否为斜线指令调用,那么直接实现为普通消息事件即可。 TIP 交互功能主要通过机器人提供,并由用户在聊天应用中触发。如果你要实现或接入的聊天平台不支持机器人相关功能,那么可以直接忽略本节。 类型为 调用斜线指令时触发。资源 TIP 许多平台都支持斜线指令,但它们的实现方式各不相同。如果平台的斜线指令仅仅提供在前端,机器人无法直接判断一个事件是否为斜线指令调用,那么直接实现为普通消息事件即可。 TIP [1] 关于序列号 此外,对于桥接场景,则还需要对这个 ID 进行映射。具体的映射方式请参见 桥接。 TIP [2] 登录信息中的用户 获取登录信息。返回一个 登录被创建时触发。必需资源: 登录被删除时触发。必需资源: 登录信息更新时触发。必需资源: TIP [1] 关于序列号 此外,对于桥接场景,则还需要对这个 ID 进行映射。具体的映射方式请参见 桥接。 TIP [2] 登录信息中的用户 获取登录信息。返回一个 登录被创建时触发。必需资源: 登录被删除时触发。必需资源: 登录信息更新时触发。必需资源: 获取群成员信息。返回一个 GuildMember 对象。 获取群成员列表。返回一个 GuildMember 的 分页列表。 将某个用户踢出群组。 将某个用户禁言。如果传入的禁言时长为 处理加群请求。 群组成员增加时触发。必需资源: 群组成员信息更新时触发。必需资源: 群组成员移除时触发。必需资源: 接收到新的加群请求时触发。必需资源: 获取群成员信息。返回一个 GuildMember 对象。 获取群成员列表。返回一个 GuildMember 的 分页列表。 将某个用户踢出群组。 将某个用户禁言。如果传入的禁言时长为 处理加群请求。 群组成员增加时触发。必需资源: 群组成员信息更新时触发。必需资源: 群组成员移除时触发。必需资源: 接收到新的加群请求时触发。必需资源: 发送消息。返回一个 获取特定消息。返回一个 撤回特定消息。 编辑特定消息。 获取频道消息列表。返回一个 当消息被创建时触发。必需资源: 当消息被编辑时触发。必需资源: 当消息被删除时触发。必需资源: 发送消息。返回一个 获取特定消息。返回一个 撤回特定消息。 编辑特定消息。 获取频道消息列表。返回一个 当消息被创建时触发。必需资源: 当消息被编辑时触发。必需资源: 当消息被删除时触发。必需资源: 向特定消息添加表态。 从特定消息删除某个用户添加的特定表态。如果没有传入用户 ID 则表示删除自己的表态。 从特定消息清除某个特定表态。如果没有传入表态名称则表示清除所有表态。 获取添加特定消息的特定表态的用户列表。返回一个 当表态被添加时触发。 当表态被移除时触发。 向特定消息添加表态。 从特定消息删除某个用户添加的特定表态。如果没有传入用户 ID 则表示删除自己的表态。 从特定消息清除某个特定表态。如果没有传入表态名称则表示清除所有表态。 获取添加特定消息的特定表态的用户列表。返回一个 当表态被添加时触发。 当表态被移除时触发。 设置群组内用户的角色。 取消群组内用户的角色。 获取群组角色列表。返回一个 GuildRole 的 分页列表。 创建群组角色。返回一个 GuildRole 对象。 修改群组角色。 删除群组角色。 群组角色被创建时触发。必需资源: 群组角色被修改时触发。必需资源: 群组角色被删除时触发。必需资源: 设置群组内用户的角色。 取消群组内用户的角色。 获取群组角色列表。返回一个 GuildRole 的 分页列表。 创建群组角色。返回一个 GuildRole 对象。 修改群组角色。 删除群组角色。 群组角色被创建时触发。必需资源: 群组角色被修改时触发。必需资源: 群组角色被删除时触发。必需资源: TIP [1] 这两个字段都可以用于标识用户。在一些平台上 (例如 Telegram),一个用户存在多种不同概念的名称,因此 SDK 可以同时设置这两个字段。而另一些平台可能不存在这两个概念的对立关系,此时 SDK 只需要根据语义设置 在应用层实现上, 获取用户信息。返回一个 处理好友申请。 接收到新的好友申请时触发。必需资源: TIP [1] 这两个字段都可以用于标识用户。在一些平台上 (例如 Telegram),一个用户存在多种不同概念的名称,因此 SDK 可以同时设置这两个字段。而另一些平台可能不存在这两个概念的对立关系,此时 SDK 只需要根据语义设置 在应用层实现上, 获取用户信息。返回一个 处理好友申请。 接收到新的好友申请时触发。必需资源:HTTP API
Pagination
List
object:FIELD TYPE DESCRIPTION data
array list of items next
string token for the next page next
token to get the next page of items. If next
is nullable, it means that there are no more items.HTTP API
Pagination
List
object:FIELD TYPE DESCRIPTION data
array list of items next
string token for the next page next
token to get the next page of items. If next
is nullable, it means that there are no more items.Channel
Definition
Channel
FIELD TYPE DESCRIPTION id string channel ID name string channel name API
Get Channel
',6)),e("ul",null,[e("li",null,[d(l,null,{default:r(()=>t[0]||(t[0]=[a("POST")])),_:1}),t[1]||(t[1]=a()),t[2]||(t[2]=e("code",null,"/v1/channel.get",-1))])]),t[7]||(t[7]=n('FIELD TYPE DESCRIPTION channel_id string channel ID bot.getChannelList(guildId, next?)
',3)),e("ul",null,[e("li",null,[d(l,null,{default:r(()=>t[3]||(t[3]=[a("POST")])),_:1}),t[4]||(t[4]=a()),t[5]||(t[5]=e("code",null,"/v1/channel.list",-1))])]),t[8]||(t[8]=n('FIELD TYPE DESCRIPTION guild_id string guild ID next string pagination token Channel
Definition
Channel
FIELD TYPE DESCRIPTION id string channel ID name string channel name API
Get Channel
',6)),e("ul",null,[e("li",null,[d(l,null,{default:r(()=>t[0]||(t[0]=[a("POST")])),_:1}),t[1]||(t[1]=a()),t[2]||(t[2]=e("code",null,"/v1/channel.get",-1))])]),t[7]||(t[7]=n('FIELD TYPE DESCRIPTION channel_id string channel ID bot.getChannelList(guildId, next?)
',3)),e("ul",null,[e("li",null,[d(l,null,{default:r(()=>t[3]||(t[3]=[a("POST")])),_:1}),t[4]||(t[4]=a()),t[5]||(t[5]=e("code",null,"/v1/channel.list",-1))])]),t[8]||(t[8]=n('FIELD TYPE DESCRIPTION guild_id string guild ID next string pagination token Guild
Definition
Guild
FIELD TYPE DESCRIPTION id string guild ID name string guild name API
bot.getGuild(guildId)
',6)),e("ul",null,[e("li",null,[l(i,null,{default:o(()=>t[0]||(t[0]=[d("POST")])),_:1}),t[1]||(t[1]=d()),t[2]||(t[2]=e("code",null,"/v1/guild.get",-1))])]),t[7]||(t[7]=a('FIELD TYPE DESCRIPTION guild_id string guild ID bot.getGuildList(next?)
',3)),e("ul",null,[e("li",null,[l(i,null,{default:o(()=>t[3]||(t[3]=[d("POST")])),_:1}),t[4]||(t[4]=d()),t[5]||(t[5]=e("code",null,"/v1/guild.list",-1))])]),t[8]||(t[8]=a('FIELD TYPE DESCRIPTION next string pagination token bot.handleGuildRequest(messageId, approve, comment?)
string
请求 IDboolean
是否通过请求string
备注信息Promise<void>
Guild
Definition
Guild
FIELD TYPE DESCRIPTION id string guild ID name string guild name API
bot.getGuild(guildId)
',6)),e("ul",null,[e("li",null,[l(i,null,{default:o(()=>t[0]||(t[0]=[d("POST")])),_:1}),t[1]||(t[1]=d()),t[2]||(t[2]=e("code",null,"/v1/guild.get",-1))])]),t[7]||(t[7]=a('FIELD TYPE DESCRIPTION guild_id string guild ID bot.getGuildList(next?)
',3)),e("ul",null,[e("li",null,[l(i,null,{default:o(()=>t[3]||(t[3]=[d("POST")])),_:1}),t[4]||(t[4]=d()),t[5]||(t[5]=e("code",null,"/v1/guild.list",-1))])]),t[8]||(t[8]=a('FIELD TYPE DESCRIPTION next string pagination token bot.handleGuildRequest(messageId, approve, comment?)
string
请求 IDboolean
是否通过请求string
备注信息Promise<void>
群组成员 (GuildMember)
类型定义
interface GuildMember {
+ user: User
+ nick?: string
+ avatar?: string
+}
API
bot.getGuildMember(guildId, userId)
string
群组 IDstring
用户 IDPromise<GuildMember>
群成员信息bot.getGuildMemberList(guildId, next?)
string
群组 IDstring
分页令牌Promise<List<GuildMember>>
群成员列表bot.kickGuildMember(guildId, userId, permanent?)
string
群组 IDstring
用户 IDboolean
是否永久踢出 (用户无法再次加入群组)Promise<void>
bot.muteGuildMember(guildId, userId, duration?, reason?)
string
群组 IDstring
用户 IDnumber
禁言时长 (毫秒)string
禁言说明Promise<void>
0
则表示解除禁言。bot.handleGuildMemberRequest(messageId, approve, comment?)
string
请求 IDboolean
是否通过请求string
备注信息Promise<void>
群组成员 (GuildMember)
类型定义
interface GuildMember {
+ user: User
+ nick?: string
+ avatar?: string
+}
API
bot.getGuildMember(guildId, userId)
string
群组 IDstring
用户 IDPromise<GuildMember>
群成员信息bot.getGuildMemberList(guildId, next?)
string
群组 IDstring
分页令牌Promise<List<GuildMember>>
群成员列表bot.kickGuildMember(guildId, userId, permanent?)
string
群组 IDstring
用户 IDboolean
是否永久踢出 (用户无法再次加入群组)Promise<void>
bot.muteGuildMember(guildId, userId, duration?, reason?)
string
群组 IDstring
用户 IDnumber
禁言时长 (毫秒)string
禁言说明Promise<void>
0
则表示解除禁言。bot.handleGuildMemberRequest(messageId, approve, comment?)
string
请求 IDboolean
是否通过请求string
备注信息Promise<void>
消息 (Message)
类型定义
interface Message {
+ isDirect: boolean
+ channelId: string
+ messageId: string
+ userId: string
+ content: string
+ timestamp?: number
+}
API
bot.sendMessage(channelId, content, guildId?)
string
频道 IDFragment
要发送的内容string
群组 IDPromise<string[]>
发送的消息 IDsession.send()
。一些平台会将主动发送的消息同被动接收后回复的消息区分开来,甚至可能限制主动消息的发送,因此使用 session.send()
总是有更好的可靠性。bot.sendMessage()
既可以发送群聊消息,也可以发送私聊消息。当发送私聊消息时,其与 bot.sendPrivateMessage()
的区别在于前者传入的是频道 ID,而后者传入的是用户 ID。bot.sendPrivateMessage(userId, content)
string
对方 IDFragment
要发送的内容Promise<string[]>
发送的消息 IDbot.getMessage(channelId, messageId)
string
频道 IDstring
消息 IDPromise<Message>
bot.deleteMessage(channelId, messageId)
string
频道 IDstring
消息 IDPromise<void>
bot.editMessage(channelId, messageId, content)
string
频道 IDstring
消息 IDFragment
要发送的内容Promise<void>
bot.getMessageList(channelId, next?)
string
频道 IDstring
分页令牌Promise<List<Message>>
消息列表消息 (Message)
类型定义
interface Message {
+ isDirect: boolean
+ channelId: string
+ messageId: string
+ userId: string
+ content: string
+ timestamp?: number
+}
API
bot.sendMessage(channelId, content, guildId?)
string
频道 IDFragment
要发送的内容string
群组 IDPromise<string[]>
发送的消息 IDsession.send()
。一些平台会将主动发送的消息同被动接收后回复的消息区分开来,甚至可能限制主动消息的发送,因此使用 session.send()
总是有更好的可靠性。bot.sendMessage()
既可以发送群聊消息,也可以发送私聊消息。当发送私聊消息时,其与 bot.sendPrivateMessage()
的区别在于前者传入的是频道 ID,而后者传入的是用户 ID。bot.sendPrivateMessage(userId, content)
string
对方 IDFragment
要发送的内容Promise<string[]>
发送的消息 IDbot.getMessage(channelId, messageId)
string
频道 IDstring
消息 IDPromise<Message>
bot.deleteMessage(channelId, messageId)
string
频道 IDstring
消息 IDPromise<void>
bot.editMessage(channelId, messageId, content)
string
频道 IDstring
消息 IDFragment
要发送的内容Promise<void>
bot.getMessageList(channelId, next?)
string
频道 IDstring
分页令牌Promise<List<Message>>
消息列表API
bot.createReaction(channelId, messageId, emoji)
string
频道 IDstring
消息 IDstring
表态名称Promise<void>
bot.deleteReaction(channelId, messageId, emoji, userId?)
string
频道 IDstring
消息 IDstring
表态名称string
用户 IDPromise<void>
bot.clearReaction(channelId, messageId, emoji?)
string
频道 IDstring
消息 IDstring
表态名称Promise<void>
bot.getReactionList(channelId, messageId, emoji, next?)
string
频道 IDstring
消息 IDstring
表态名称string
分页令牌Promise<List<User>>
API
bot.createReaction(channelId, messageId, emoji)
string
频道 IDstring
消息 IDstring
表态名称Promise<void>
bot.deleteReaction(channelId, messageId, emoji, userId?)
string
频道 IDstring
消息 IDstring
表态名称string
用户 IDPromise<void>
bot.clearReaction(channelId, messageId, emoji?)
string
频道 IDstring
消息 IDstring
表态名称Promise<void>
bot.getReactionList(channelId, messageId, emoji, next?)
string
频道 IDstring
消息 IDstring
表态名称string
分页令牌Promise<List<User>>
类型定义
export interface GuildRole {
+ id: string
+ name: string
+ color: number
+ position: number
+}
API
bot.setGuildMemberRole(guildId, userId, roleId)
string
群组 IDstring
用户 IDstring
角色 IDPromise<void>
bot.unsetGuildMemberRole(guildId, userId, roleId)
string
群组 IDstring
用户 IDstring
角色 IDPromise<void>
bot.getGuildRoleList(guildId, next?)
string
群组 IDstring
分页令牌Promise<List<GuildRole>>
角色列表bot.createGuildRole(guildId, data)
string
群组 IDPartial<GuildRole>
角色信息Promise<string>
角色 IDbot.modifyGuildRole(guildId, roleId, data)
string
群组 IDstring
角色 IDPartial<GuildRole>
角色信息Promise<void>
bot.deleteGuildRole(guildId, roleId)
string
群组 IDstring
角色 IDPromise<void>
类型定义
export interface GuildRole {
+ id: string
+ name: string
+ color: number
+ position: number
+}
API
bot.setGuildMemberRole(guildId, userId, roleId)
string
群组 IDstring
用户 IDstring
角色 IDPromise<void>
bot.unsetGuildMemberRole(guildId, userId, roleId)
string
群组 IDstring
用户 IDstring
角色 IDPromise<void>
bot.getGuildRoleList(guildId, next?)
string
群组 IDstring
分页令牌Promise<List<GuildRole>>
角色列表bot.createGuildRole(guildId, data)
string
群组 IDPartial<GuildRole>
角色信息Promise<string>
角色 IDbot.modifyGuildRole(guildId, roleId, data)
string
群组 IDstring
角色 IDPartial<GuildRole>
角色信息Promise<void>
bot.deleteGuildRole(guildId, roleId)
string
群组 IDstring
角色 IDPromise<void>
用户 (User)
类型定义
export interface User {
+ id: string
+ name: string
+ avatar?: string
+}
API
bot.getSelf()
Promise<User>
用户信息bot.getUser(userId)
string
用户 IDPromise<User>
用户信息bot.getFriendList(next?)
string
分页令牌Promise<List<User>>
好友列表bot.handleFriendRequest(messageId, approve, comment?)
string
请求 IDboolean
是否通过请求string
备注信息Promise<void>
用户 (User)
类型定义
export interface User {
+ id: string
+ name: string
+ avatar?: string
+}
API
bot.getSelf()
Promise<User>
用户信息bot.getUser(userId)
string
用户 IDPromise<User>
用户信息bot.getFriendList(next?)
string
分页令牌Promise<List<User>>
好友列表bot.handleFriendRequest(messageId, approve, comment?)
string
请求 IDboolean
是否通过请求string
备注信息Promise<void>
桥接
概述
桥接反射
',3)]))}const p=e(o,[["render",n]]);export{f as __pageData,p as default};
diff --git a/assets/zh-CN_advanced_bridge.md.BvgC5OVs.lean.js b/assets/zh-CN_advanced_bridge.md.BvgC5OVs.lean.js
new file mode 100644
index 0000000..69de0de
--- /dev/null
+++ b/assets/zh-CN_advanced_bridge.md.BvgC5OVs.lean.js
@@ -0,0 +1 @@
+import{_ as e,x as t,a7 as r,h as d}from"./chunks/framework.K8kzz9Vz.js";const f=JSON.parse('{"title":"桥接","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/advanced/bridge.md","filePath":"zh-CN/advanced/bridge.md"}'),o={name:"zh-CN/advanced/bridge.md"};function n(i,a,s,c,h,l){return d(),t("div",null,a[0]||(a[0]=[r('桥接
概述
桥接反射
',3)]))}const p=e(o,[["render",n]]);export{f as __pageData,p as default};
diff --git a/assets/zh-CN_advanced_internal.md.BcT4DuVV.js b/assets/zh-CN_advanced_internal.md.BcT4DuVV.js
new file mode 100644
index 0000000..54a6675
--- /dev/null
+++ b/assets/zh-CN_advanced_internal.md.BcT4DuVV.js
@@ -0,0 +1,7 @@
+import{_ as a,x as i,a7 as e,h as s}from"./chunks/framework.K8kzz9Vz.js";const k=JSON.parse('{"title":"跨平台","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/advanced/internal.md","filePath":"zh-CN/advanced/internal.md"}'),d={name:"zh-CN/advanced/internal.md"};function o(l,t,n,r,p,c){return s(),i("div",null,t[0]||(t[0]=[e(`跨平台
平台与适配器
Satori-Platform
和 Satori-User-ID
请求头,这是为了区分发起请求的登录号。不同平台的登录号拥有不同的 login.platform
,而同一平台的不同登录号则拥有不同的 login.user.id
,由此这套机制实现了安全的隔离。platform
字段都是直接由 SDK 设置的固定值。然而对于另一些允许自建的平台 (例如 Rocket Chat 和 Zulip),SDK 则通常需要让部署者自行设置 platform
,用来区分不同的服务器。如果直接混用的话,可能导致数据碰撞等问题。login.platform
:聊天平台。通常来说,同一平台内的用户间具有相互发送消息的能力,而不同平台的用户间则没有。在 Satori 中,platform
也相当于一种命名空间,因此 SDK 需要保证同一平台内的 user.id
, guild.id
等字段的唯一性。login.adapter
:适配器。适配器更多地是一个实现相关的概念,它决定了如何与平台进行通信。同一个适配器下通常会有相同的扩展 API、事件和消息元素。这个字段通常是 SDK 直接设置的,开发者可以用这个字段判断实现是否支持某些特性。platform
和 adapter
,可以记住以下规则:platform
内 ID 相互兼容;adapter
内 API 相互兼容;platform
或 adapter
。这完全取决于 SDK 的实现和社区的约定。user.id
, guild.id
等属于不同的命名空间,因此 platform
字段应该是不同的。但这两台服务器使用的通信方式相同,因此 adapter
字段应该是相同的。adapter
字段应该是不同的。但这两套 SDK 都是为了与同一平台通信,所有的 user.id
, guild.id
等属于同一个命名空间,因此 platform
字段应该是相同的。API 扩展
/{path}/{version}/internal/{method}
路由代理平台原生 API。DELETE /v1/internal/channels/111222333
+Satori-Platform: discord
+Satori-User-ID: 1234567890
Satori-Platform
和 Satori-User-ID
请求头之外,整个请求和响应的格式都与平台原生 API 一致。事件扩展
平台原生事件
internal
事件的 _type
和 _data
属性代理平台原生事件。它的结构如下:字段 类型 说明 id
number 事件 ID type
string 事件类型 (固定为 internal
)login
Login 登录信息 _type
string 原生事件类型 _data
object 原生事件数据 标准事件的扩展字段
_type
和 _data
访问。它的结构如下:字段 类型 说明 type
string 事件类型 (不应该是 internal
)_type
string 平台通用名称 _data
object 原生事件数据 其他字段 其他标准事件字段 _type
和 _data
字段,但这两个字段的值可能是不同的。消息元素扩展
平台原生消息元素
<kook:card size="lg">
+ <kook:countdown end-time="1608819168000"/>
+</kook:card>
标准元素的扩展属性
<!-- src 是 audio 元素的标准属性。 -->
+<!-- 但 cover 并未标准化,所以需要加前缀。 -->
+<audio src="url1" kook:cover="url2"/>
跨平台
平台与适配器
Satori-Platform
和 Satori-User-ID
请求头,这是为了区分发起请求的登录号。不同平台的登录号拥有不同的 login.platform
,而同一平台的不同登录号则拥有不同的 login.user.id
,由此这套机制实现了安全的隔离。platform
字段都是直接由 SDK 设置的固定值。然而对于另一些允许自建的平台 (例如 Rocket Chat 和 Zulip),SDK 则通常需要让部署者自行设置 platform
,用来区分不同的服务器。如果直接混用的话,可能导致数据碰撞等问题。login.platform
:聊天平台。通常来说,同一平台内的用户间具有相互发送消息的能力,而不同平台的用户间则没有。在 Satori 中,platform
也相当于一种命名空间,因此 SDK 需要保证同一平台内的 user.id
, guild.id
等字段的唯一性。login.adapter
:适配器。适配器更多地是一个实现相关的概念,它决定了如何与平台进行通信。同一个适配器下通常会有相同的扩展 API、事件和消息元素。这个字段通常是 SDK 直接设置的,开发者可以用这个字段判断实现是否支持某些特性。platform
和 adapter
,可以记住以下规则:platform
内 ID 相互兼容;adapter
内 API 相互兼容;platform
或 adapter
。这完全取决于 SDK 的实现和社区的约定。user.id
, guild.id
等属于不同的命名空间,因此 platform
字段应该是不同的。但这两台服务器使用的通信方式相同,因此 adapter
字段应该是相同的。adapter
字段应该是不同的。但这两套 SDK 都是为了与同一平台通信,所有的 user.id
, guild.id
等属于同一个命名空间,因此 platform
字段应该是相同的。API 扩展
/{path}/{version}/internal/{method}
路由代理平台原生 API。DELETE /v1/internal/channels/111222333
+Satori-Platform: discord
+Satori-User-ID: 1234567890
Satori-Platform
和 Satori-User-ID
请求头之外,整个请求和响应的格式都与平台原生 API 一致。事件扩展
平台原生事件
internal
事件的 _type
和 _data
属性代理平台原生事件。它的结构如下:字段 类型 说明 id
number 事件 ID type
string 事件类型 (固定为 internal
)login
Login 登录信息 _type
string 原生事件类型 _data
object 原生事件数据 标准事件的扩展字段
_type
和 _data
访问。它的结构如下:字段 类型 说明 type
string 事件类型 (不应该是 internal
)_type
string 平台通用名称 _data
object 原生事件数据 其他字段 其他标准事件字段 _type
和 _data
字段,但这两个字段的值可能是不同的。消息元素扩展
平台原生消息元素
<kook:card size="lg">
+ <kook:countdown end-time="1608819168000"/>
+</kook:card>
标准元素的扩展属性
<!-- src 是 audio 元素的标准属性。 -->
+<!-- 但 cover 并未标准化,所以需要加前缀。 -->
+<audio src="url1" kook:cover="url2"/>
READY
信令将提供完整的元信息;META
信令和登录事件对元信息进行更新。META
信令不反映登录状态变化,也不会包含 logins
字段。/{path}/{version}/meta/{method}
路由提供。通信方式与 HTTP API 类似,但不需要 Satori-Platform
和 Satori-User-ID
请求头。类型定义
Meta
字段 类型 描述 logins
Login[]
登录信息 proxy_urls
string[] 代理路由 列表 API
获取元信息
',12)),e("blockquote",p,[d(a,null,{default:l(()=>t[6]||(t[6]=[o("POST")])),_:1}),t[7]||(t[7]=e("code",null,"/meta",-1))]),t[13]||(t[13]=e("p",null,[o("返回一个 "),e("a",{href:"#meta"},"Meta"),o(" 对象。")],-1)),t[14]||(t[14]=e("h3",{id:"创建-webhook",tabindex:"-1"},[o("创建 WebHook "),e("a",{class:"header-anchor",href:"#创建-webhook","aria-label":'Permalink to "创建 WebHook"'},"")],-1)),e("blockquote",m,[d(a,null,{default:l(()=>t[8]||(t[8]=[o("POST")])),_:1}),t[9]||(t[9]=e("code",null,"/meta/webhook.create",-1))]),t[15]||(t[15]=r('字段 类型 描述 url string WebHook 地址 token string? 鉴权令牌 移除 WebHook
',2)),e("blockquote",k,[d(a,null,{default:l(()=>t[10]||(t[10]=[o("POST")])),_:1}),t[11]||(t[11]=e("code",null,"/meta/webhook.delete",-1))]),t[16]||(t[16]=e("table",{tabindex:"0"},[e("thead",null,[e("tr",null,[e("th",null,"字段"),e("th",null,"类型"),e("th",null,"描述")])]),e("tbody",null,[e("tr",null,[e("td",null,"url"),e("td",null,"string"),e("td",null,"WebHook 地址")])])],-1))])}const w=n(b,[["render",f]]);export{y as __pageData,w as default};
diff --git a/assets/zh-CN_advanced_meta.md.5SIn9XtM.lean.js b/assets/zh-CN_advanced_meta.md.5SIn9XtM.lean.js
new file mode 100644
index 0000000..06f0ef8
--- /dev/null
+++ b/assets/zh-CN_advanced_meta.md.5SIn9XtM.lean.js
@@ -0,0 +1 @@
+import{_ as n,x as s,m as e,W as o,y as d,k as l,a7 as r,Q as i,h as u}from"./chunks/framework.K8kzz9Vz.js";const y=JSON.parse('{"title":"元信息 可选 实验性","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/advanced/meta.md","filePath":"zh-CN/advanced/meta.md"}'),b={name:"zh-CN/advanced/meta.md"},h={id:"元信息",tabindex:"-1"},p={class:"route"},m={class:"route"},k={class:"route"};function f(c,t,P,x,g,q){const a=i("badge");return u(),s("div",null,[e("h1",h,[t[2]||(t[2]=o("元信息 ")),d(a,null,{default:l(()=>t[0]||(t[0]=[o("可选")])),_:1}),t[3]||(t[3]=o()),d(a,{type:"warning"},{default:l(()=>t[1]||(t[1]=[o("实验性")])),_:1}),t[4]||(t[4]=o()),t[5]||(t[5]=e("a",{class:"header-anchor",href:"#元信息","aria-label":'Permalink to "元信息 READY
信令将提供完整的元信息;META
信令和登录事件对元信息进行更新。META
信令不反映登录状态变化,也不会包含 logins
字段。/{path}/{version}/meta/{method}
路由提供。通信方式与 HTTP API 类似,但不需要 Satori-Platform
和 Satori-User-ID
请求头。类型定义
Meta
字段 类型 描述 logins
Login[]
登录信息 proxy_urls
string[] 代理路由 列表 API
获取元信息
',12)),e("blockquote",p,[d(a,null,{default:l(()=>t[6]||(t[6]=[o("POST")])),_:1}),t[7]||(t[7]=e("code",null,"/meta",-1))]),t[13]||(t[13]=e("p",null,[o("返回一个 "),e("a",{href:"#meta"},"Meta"),o(" 对象。")],-1)),t[14]||(t[14]=e("h3",{id:"创建-webhook",tabindex:"-1"},[o("创建 WebHook "),e("a",{class:"header-anchor",href:"#创建-webhook","aria-label":'Permalink to "创建 WebHook"'},"")],-1)),e("blockquote",m,[d(a,null,{default:l(()=>t[8]||(t[8]=[o("POST")])),_:1}),t[9]||(t[9]=e("code",null,"/meta/webhook.create",-1))]),t[15]||(t[15]=r('字段 类型 描述 url string WebHook 地址 token string? 鉴权令牌 移除 WebHook
',2)),e("blockquote",k,[d(a,null,{default:l(()=>t[10]||(t[10]=[o("POST")])),_:1}),t[11]||(t[11]=e("code",null,"/meta/webhook.delete",-1))]),t[16]||(t[16]=e("table",{tabindex:"0"},[e("thead",null,[e("tr",null,[e("th",null,"字段"),e("th",null,"类型"),e("th",null,"描述")])]),e("tbody",null,[e("tr",null,[e("td",null,"url"),e("td",null,"string"),e("td",null,"WebHook 地址")])])],-1))])}const w=n(b,[["render",f]]);export{y as __pageData,w as default};
diff --git a/assets/zh-CN_advanced_resource.md.C-_HwXQP.js b/assets/zh-CN_advanced_resource.md.C-_HwXQP.js
new file mode 100644
index 0000000..532b0c5
--- /dev/null
+++ b/assets/zh-CN_advanced_resource.md.C-_HwXQP.js
@@ -0,0 +1,22 @@
+import{_ as t,x as p,m as e,W as s,y as o,k as i,a7 as l,Q as r,h as d}from"./chunks/framework.K8kzz9Vz.js";const x=JSON.parse('{"title":"资源链接 实验性","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/advanced/resource.md","filePath":"zh-CN/advanced/resource.md"}'),c={name:"zh-CN/advanced/resource.md"},u={id:"资源链接",tabindex:"-1"},h={class:"route"};function m(g,a,k,b,v,f){const n=r("badge");return d(),p("div",null,[e("h1",u,[a[1]||(a[1]=s("资源链接 ")),o(n,{type:"warning"},{default:i(()=>a[0]||(a[0]=[s("实验性")])),_:1}),a[2]||(a[2]=s()),a[3]||(a[3]=e("a",{class:"header-anchor",href:"#资源链接","aria-label":'Permalink to "资源链接 multipart/form-data
格式。下面是一个示例:POST /v1/upload.create
+Content-Type: multipart/form-data
+Satori-Platform: discord
+Satori-User-ID: 1234567890
+
+--boundary
+Content-Disposition: form-data; name="foo"; filename="image1.png"
+Content-Type: image/png
+
+binary-data
+--boundary
+Content-Disposition: form-data; name="bar"; filename="image2.gif"
+Content-Type: image/gif
+
+binary-data
+--boundary--
Content-Disposition
中的 name
字段表示文件标识符 (必需且不能重复),filename
字段表示文件名 (可选);Content-Type
表示文件类型 (必需)。{
+ "foo": "internal:discord/1234567890/_tmp/3j6emd92-image1.png",
+ "bar": "internal:discord/1234567890/_tmp/reacpmeq-image2.gif"
+}
internal:
协议进一步代理,且有一定的有效期。各实现可以根据自身情况调整有效期,推荐值为 5 分钟。内部链接
internal:
称为内部链接协议,用于代理平台原生 API 或是无法直接通过公网访问的资源。格式规范
internal:{platform}/{user.id}/{path}
platform
为平台名称,user.id
为登录账号,path
为资源路径。_
开头的路径需要保留给 Satori 自身使用,拥有固定的语义。现有的保留路径有:_tmp
:用于 SDK 的默认文件上传实现;_raw
:用于代理平台原生 HTTP API。API 代理
_raw
内部链接。换言之,以下两个请求是等价的:DELETE /v1/internal/channel/111222333
+Satori-Platform: discord
+Satori-User-ID: 1234567890
DELETE /v1/proxy/internal:discord/1234567890/_raw/channel/111222333
_raw
路径下的代理即可。适用场景
/upload.create
,你将获得内部链接。对平台原生 API 的访问同样通过内部链接进行。除此以外,还有一些内部链接的适用场景。不同方案对比
data:
URLdata:
链接放入消息元素中。之所以不推荐使用,是因为这种方案有两大致命缺点:data:
会导致消息体积大幅增加,极大影响消息处理的性能。代理路由
/{path}/{version}/proxy/{url}
,用于访问这些资源链接。这个路由不需要 Satori-Platform
和 Satori-User-ID
请求头。GET /v1/proxy/https://cdn.discordapp.com/attachments/bf6f121d.jpg
+GET /v1/proxy/internal:discord/1234567890/_tmp/3j6emd92-image1.png
Access-Control-Allow-Origin
等响应头,以限制或允许跨域请求。proxy_urls
属性。这个属性记录了所有需要代理的 (非内部) 资源链接前缀,应用侧可以根据这个属性来判断是否需要代理。url
的不同形式,SDK 提供的代理路由会有不同的行为:url
不是合法的 URL,直接返回 400;url
是一个内部链接 (即以 internal:
开头): platform
和 user.id
,并找到对应的登录号;url
以某个 proxy_urls
中的链接为前缀: url
并返回 (SDK 提供了该资源的代理);实践指南
REGISTER_INTERNAL_ROUTE
方法用于注册内部链接路由,以便适配器实现;DOWNLOAD_URL
方法用于将一个链接下载为数据,无论其是否为内部链接;/upload.create
API;DOWNLOAD_URL
方法实现代理路由。
`,45))])}const P=t(c,[["render",m]]);export{x as __pageData,P as default};
diff --git a/assets/zh-CN_advanced_resource.md.C-_HwXQP.lean.js b/assets/zh-CN_advanced_resource.md.C-_HwXQP.lean.js
new file mode 100644
index 0000000..532b0c5
--- /dev/null
+++ b/assets/zh-CN_advanced_resource.md.C-_HwXQP.lean.js
@@ -0,0 +1,22 @@
+import{_ as t,x as p,m as e,W as s,y as o,k as i,a7 as l,Q as r,h as d}from"./chunks/framework.K8kzz9Vz.js";const x=JSON.parse('{"title":"资源链接 实验性","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/advanced/resource.md","filePath":"zh-CN/advanced/resource.md"}'),c={name:"zh-CN/advanced/resource.md"},u={id:"资源链接",tabindex:"-1"},h={class:"route"};function m(g,a,k,b,v,f){const n=r("badge");return d(),p("div",null,[e("h1",u,[a[1]||(a[1]=s("资源链接 ")),o(n,{type:"warning"},{default:i(()=>a[0]||(a[0]=[s("实验性")])),_:1}),a[2]||(a[2]=s()),a[3]||(a[3]=e("a",{class:"header-anchor",href:"#资源链接","aria-label":'Permalink to "资源链接 REGISTER_INTERNAL_ROUTE
方法注册内部路由: _raw
路径下代理平台原生 API);/upload.create
API,覆盖 SDK 的默认实现;multipart/form-data
格式。下面是一个示例:POST /v1/upload.create
+Content-Type: multipart/form-data
+Satori-Platform: discord
+Satori-User-ID: 1234567890
+
+--boundary
+Content-Disposition: form-data; name="foo"; filename="image1.png"
+Content-Type: image/png
+
+binary-data
+--boundary
+Content-Disposition: form-data; name="bar"; filename="image2.gif"
+Content-Type: image/gif
+
+binary-data
+--boundary--
Content-Disposition
中的 name
字段表示文件标识符 (必需且不能重复),filename
字段表示文件名 (可选);Content-Type
表示文件类型 (必需)。{
+ "foo": "internal:discord/1234567890/_tmp/3j6emd92-image1.png",
+ "bar": "internal:discord/1234567890/_tmp/reacpmeq-image2.gif"
+}
internal:
协议进一步代理,且有一定的有效期。各实现可以根据自身情况调整有效期,推荐值为 5 分钟。内部链接
internal:
称为内部链接协议,用于代理平台原生 API 或是无法直接通过公网访问的资源。格式规范
internal:{platform}/{user.id}/{path}
platform
为平台名称,user.id
为登录账号,path
为资源路径。_
开头的路径需要保留给 Satori 自身使用,拥有固定的语义。现有的保留路径有:_tmp
:用于 SDK 的默认文件上传实现;_raw
:用于代理平台原生 HTTP API。API 代理
_raw
内部链接。换言之,以下两个请求是等价的:DELETE /v1/internal/channel/111222333
+Satori-Platform: discord
+Satori-User-ID: 1234567890
DELETE /v1/proxy/internal:discord/1234567890/_raw/channel/111222333
_raw
路径下的代理即可。适用场景
/upload.create
,你将获得内部链接。对平台原生 API 的访问同样通过内部链接进行。除此以外,还有一些内部链接的适用场景。不同方案对比
data:
URLdata:
链接放入消息元素中。之所以不推荐使用,是因为这种方案有两大致命缺点:data:
会导致消息体积大幅增加,极大影响消息处理的性能。代理路由
/{path}/{version}/proxy/{url}
,用于访问这些资源链接。这个路由不需要 Satori-Platform
和 Satori-User-ID
请求头。GET /v1/proxy/https://cdn.discordapp.com/attachments/bf6f121d.jpg
+GET /v1/proxy/internal:discord/1234567890/_tmp/3j6emd92-image1.png
Access-Control-Allow-Origin
等响应头,以限制或允许跨域请求。proxy_urls
属性。这个属性记录了所有需要代理的 (非内部) 资源链接前缀,应用侧可以根据这个属性来判断是否需要代理。url
的不同形式,SDK 提供的代理路由会有不同的行为:url
不是合法的 URL,直接返回 400;url
是一个内部链接 (即以 internal:
开头): platform
和 user.id
,并找到对应的登录号;url
以某个 proxy_urls
中的链接为前缀: url
并返回 (SDK 提供了该资源的代理);实践指南
REGISTER_INTERNAL_ROUTE
方法用于注册内部链接路由,以便适配器实现;DOWNLOAD_URL
方法用于将一个链接下载为数据,无论其是否为内部链接;/upload.create
API;DOWNLOAD_URL
方法实现代理路由。
`,45))])}const P=t(c,[["render",m]]);export{x as __pageData,P as default};
diff --git a/assets/zh-CN_index.md.CYyt9X3k.js b/assets/zh-CN_index.md.CYyt9X3k.js
new file mode 100644
index 0000000..268ee09
--- /dev/null
+++ b/assets/zh-CN_index.md.CYyt9X3k.js
@@ -0,0 +1 @@
+import{_ as e,x as t,h as r}from"./chunks/framework.K8kzz9Vz.js";const h=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","links":{"starter":"/zh-CN/introduction.html"},"home":{"primary":"即刻起步","description":"THE UNIVERSAL MESSENGER PROTOCOL"}},"headers":[],"relativePath":"zh-CN/index.md","filePath":"zh-CN/index.md"}'),a={name:"zh-CN/index.md"};function n(o,i,s,c,d,m){return r(),t("div")}const _=e(a,[["render",n]]);export{h as __pageData,_ as default};
diff --git a/assets/zh-CN_index.md.CYyt9X3k.lean.js b/assets/zh-CN_index.md.CYyt9X3k.lean.js
new file mode 100644
index 0000000..268ee09
--- /dev/null
+++ b/assets/zh-CN_index.md.CYyt9X3k.lean.js
@@ -0,0 +1 @@
+import{_ as e,x as t,h as r}from"./chunks/framework.K8kzz9Vz.js";const h=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","links":{"starter":"/zh-CN/introduction.html"},"home":{"primary":"即刻起步","description":"THE UNIVERSAL MESSENGER PROTOCOL"}},"headers":[],"relativePath":"zh-CN/index.md","filePath":"zh-CN/index.md"}'),a={name:"zh-CN/index.md"};function n(o,i,s,c,d,m){return r(),t("div")}const _=e(a,[["render",n]]);export{h as __pageData,_ as default};
diff --git a/assets/zh-CN_introduction.md.BDFENsOM.js b/assets/zh-CN_introduction.md.BDFENsOM.js
new file mode 100644
index 0000000..cffe70d
--- /dev/null
+++ b/assets/zh-CN_introduction.md.BDFENsOM.js
@@ -0,0 +1 @@
+import{_ as t,x as r,a7 as e,h as s}from"./chunks/framework.K8kzz9Vz.js";const S=JSON.parse('{"title":"介绍","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/introduction.md","filePath":"zh-CN/introduction.md"}'),o={name:"zh-CN/introduction.md"};function i(p,a,c,d,l,n){return s(),r("div",null,a[0]||(a[0]=[e('REGISTER_INTERNAL_ROUTE
方法注册内部路由: _raw
路径下代理平台原生 API);/upload.create
API,覆盖 SDK 的默认实现;介绍
介绍
API
HTTP API
/{path}/{version}/{resource}.{method}
。其中,path
为部署路径 (可以为空),version
为 API 的版本号,resource
是资源类型,method
为方法名。application/json
编码在请求体中。返回值也是 JSON 格式。作为特例,具有 文件上传 功能的 API 使用 multipart/form-data
编码。Satori-Platform
和 Satori-User-ID
字段,分别表示平台名称和平台账号。POST /v1/channel.get
+Content-Type: application/json
+Authorization: Bearer 1234567890
+Satori-Platform: discord
+Satori-User-ID: 1234567890
+
+{"channel_id": "1234567890"}
鉴权
Authorization
请求头来实现。其中涉及的鉴权令牌由 SDK 分发,本协议不做任何限制。状态码
状态码 描述 200 (OK) 请求成功 400 (BAD REQUEST) 请求格式错误 401 (UNAUTHORIZED) 缺失鉴权 403 (FORBIDDEN) 权限不足 404 (NOT FOUND) 资源不存在 405 (METHOD NOT ALLOWED) 请求方法不支持 5XX (SERVER ERROR) 服务器错误 Login
对象中的 features
字段是一个字符串数组,用于表示平台的特性。这些特性可以用于判断平台是否支持某些 API。合法的平台特性包括:message.delete
表示支持使用 message.delete
撤回消息。message.list.from
表示使用 message.list
查询消息列表时支持将消息 ID 作为分页令牌。guild.plain
表示该平台的群组内只能存在一个消息频道。进阶 API
/{path}/{version}/proxy
的子路由用于代理平台资源,请参见 代理路由。/{path}/{version}/meta
的子路由用于访问 SDK 相关接口,请参见 元信息 API。/{path}/{version}/internal
的子路由用于访问平台内部接口,请参见 内部 API。类型定义
分页列表
List
对象:字段 类型 描述 data
array 数据 next
string? 下一页的令牌 next
令牌来获取下一页的数据。如果 next
为空,则表示没有更多数据了。双向分页列表
BidiList
对象:字段 类型 描述 data
array 数据 prev
string? 上一页的令牌 next
string? 下一页的令牌 direction
和 order
参数来指定方向和排序。direction
参数有三种不同的取值:before
:向前获取数据,此时 prev
和 next
相同,均表示上一页的令牌。after
:向后获取数据,此时 prev
和 next
相同,均表示下一页的令牌。around
:向两侧获取数据,此时 prev
表示上一页的令牌,next
表示下一页的令牌。prev
或 next
缺失,则表示在该方向上没有更多数据了。order
参数有两种不同的取值:
',20))])}const g=r(p,[["render",u]]);export{A as __pageData,g as default};
diff --git a/assets/zh-CN_protocol_api.md.9P_hL1dt.lean.js b/assets/zh-CN_protocol_api.md.9P_hL1dt.lean.js
new file mode 100644
index 0000000..debab4d
--- /dev/null
+++ b/assets/zh-CN_protocol_api.md.9P_hL1dt.lean.js
@@ -0,0 +1,7 @@
+import{_ as r,x as i,a7 as a,m as d,W as t,y as s,k as c,Q as n,h as l}from"./chunks/framework.K8kzz9Vz.js";const A=JSON.parse('{"title":"API","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/protocol/api.md","filePath":"zh-CN/protocol/api.md"}'),p={name:"zh-CN/protocol/api.md"},h={id:"平台特性",tabindex:"-1"};function u(m,e,P,b,f,I){const o=n("badge");return l(),i("div",null,[e[4]||(e[4]=a(`asc
:升序排列。desc
:降序排列。API
HTTP API
/{path}/{version}/{resource}.{method}
。其中,path
为部署路径 (可以为空),version
为 API 的版本号,resource
是资源类型,method
为方法名。application/json
编码在请求体中。返回值也是 JSON 格式。作为特例,具有 文件上传 功能的 API 使用 multipart/form-data
编码。Satori-Platform
和 Satori-User-ID
字段,分别表示平台名称和平台账号。POST /v1/channel.get
+Content-Type: application/json
+Authorization: Bearer 1234567890
+Satori-Platform: discord
+Satori-User-ID: 1234567890
+
+{"channel_id": "1234567890"}
鉴权
Authorization
请求头来实现。其中涉及的鉴权令牌由 SDK 分发,本协议不做任何限制。状态码
状态码 描述 200 (OK) 请求成功 400 (BAD REQUEST) 请求格式错误 401 (UNAUTHORIZED) 缺失鉴权 403 (FORBIDDEN) 权限不足 404 (NOT FOUND) 资源不存在 405 (METHOD NOT ALLOWED) 请求方法不支持 5XX (SERVER ERROR) 服务器错误 Login
对象中的 features
字段是一个字符串数组,用于表示平台的特性。这些特性可以用于判断平台是否支持某些 API。合法的平台特性包括:message.delete
表示支持使用 message.delete
撤回消息。message.list.from
表示使用 message.list
查询消息列表时支持将消息 ID 作为分页令牌。guild.plain
表示该平台的群组内只能存在一个消息频道。进阶 API
/{path}/{version}/proxy
的子路由用于代理平台资源,请参见 代理路由。/{path}/{version}/meta
的子路由用于访问 SDK 相关接口,请参见 元信息 API。/{path}/{version}/internal
的子路由用于访问平台内部接口,请参见 内部 API。类型定义
分页列表
List
对象:字段 类型 描述 data
array 数据 next
string? 下一页的令牌 next
令牌来获取下一页的数据。如果 next
为空,则表示没有更多数据了。双向分页列表
BidiList
对象:字段 类型 描述 data
array 数据 prev
string? 上一页的令牌 next
string? 下一页的令牌 direction
和 order
参数来指定方向和排序。direction
参数有三种不同的取值:before
:向前获取数据,此时 prev
和 next
相同,均表示上一页的令牌。after
:向后获取数据,此时 prev
和 next
相同,均表示下一页的令牌。around
:向两侧获取数据,此时 prev
表示上一页的令牌,next
表示下一页的令牌。prev
或 next
缺失,则表示在该方向上没有更多数据了。order
参数有两种不同的取值:
',20))])}const g=r(p,[["render",u]]);export{A as __pageData,g as default};
diff --git a/assets/zh-CN_protocol_elements.md.PpF5nfM_.js b/assets/zh-CN_protocol_elements.md.PpF5nfM_.js
new file mode 100644
index 0000000..c1ad544
--- /dev/null
+++ b/assets/zh-CN_protocol_elements.md.PpF5nfM_.js
@@ -0,0 +1,17 @@
+import{_ as h,x as n,a7 as e,m as i,W as a,y as d,k as l,Q as r,h as o}from"./chunks/framework.K8kzz9Vz.js";const q=JSON.parse('{"title":"标准元素","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/protocol/elements.md","filePath":"zh-CN/protocol/elements.md"}'),p={name:"zh-CN/protocol/elements.md"},k={tabindex:"0"},g={id:"button",tabindex:"-1"};function u(c,t,b,y,B,m){const s=r("badge");return o(),n("div",null,[t[17]||(t[17]=e('asc
:升序排列。desc
:降序排列。标准元素
基础元素
提及用户 (at)
属性 类型 范围 描述 id string? 收发 目标用户的 ID name string? 收发 目标用户的名称 role string? 收发 目标角色 type string? 收发 特殊操作,例如 all 表示 @全体成员,here 表示 @在线成员 <at>
元素用于提及某个或某些用户。提及频道 (sharp)
属性 类型 范围 描述 id string 收发 目标频道的 ID name string? 收发 目标频道的名称 <sharp>
元素用于提及某个频道。链接 (a)
属性 类型 范围 描述 href string 收发 链接的 URL <a>
元素用于显示一个链接。当平台不支持链接时,建议显示为 content (href)
的形式。资源元素
src
。如果某个平台不支持特定的资源类型,适配器应该用 src
代替。如果某个平台不支持将资源消息元素和其他消息元素同时发送,适配器应该分多条发送,并返回最后一条消息的 ID。图片 (img)
属性 类型 范围 描述 width number? 收 图片宽度 (像素) height number? 收 图片高度 (像素) <img>
元素用于表示图片。音频 (audio)
属性 类型 范围 描述 duration number? 收 音频长度 (秒) poster string? 收发 音频封面 URL <audio>
元素用于表示语音。视频 (video)
属性 类型 范围 描述 width number? 收 视频宽度 (像素) height number? 收 视频高度 (像素) duration number? 收 视频长度 (秒) poster string? 收发 视频封面 URL <video>
元素用于表示视频。文件 (file)
属性 类型 范围 描述 poster string? 收发 缩略图 URL <file>
元素用于表示文件。修饰元素
粗体 (b, strong)
<b>
或 <strong>
元素用于将其中的内容以粗体显示。斜体 (i, em)
<i>
或 <em>
元素用于将其中的内容以斜体显示。下划线 (u, ins)
<u>
或 <ins>
元素用于为其中的内容附加下划线。删除线 (s, del)
<s>
或 <del>
元素用于为其中的内容附加删除线。剧透 (spl)
<spl>
元素用于将其中的内容标记为剧透 (默认会被隐藏,点击后才显示)。代码 (code)
<code>
元素用于将其中的内容以等宽字体显示 (通常还会有特定的背景色)。上标 (sup)
<sup>
元素用于将其中的内容以上标显示。下标 (sub)
<sub>
元素用于将其中的内容以下标显示。排版元素
换行 (br)
<br>
元素表示一个独立的换行。段落 (p)
<p>
元素表示一个段落。在渲染时,它与相邻的元素之间会确保有一个换行。消息 (message)
属性 类型 范围 描述 id string? 发 消息的 ID forward boolean? 发 是否为转发消息 <message>
元素的基本用法是表示一条消息。子元素对应于消息的内容。如果其没有子元素,则消息不会被发送。<message>
元素时,之前的元素会被立即视为一条消息被发送。因此下面的两种写法是等价的:<!-- 第一种写法:发送两条消息 -->
+<message>hello</message>
+<message>world</message>
+
+<!-- 第二种写法:用一条空消息隔开两段文本,实际上仍然会发送两条消息 -->
+hello<message/>world
<message>
+ <author id="123123123" name="Alice" avatar="url"/>
+ hello world
+</message>
forward
配合 id
属性来转发一条消息:<message id="123456789" forward/>
forward
属性的 <message>
元素嵌套其他 <message>
元素来实现合并转发:<message forward>
+ <message id="123456789"/>
+ <message id="987654321"/>
+ <!-- 合并转发里也可以嵌套模拟其他用户发送的消息 -->
+ <message>
+ <author id="123123123" name="Alice" avatar="url"/>
+ hello world
+ </message>
+</message>
元信息元素
引用 (quote)
<quote>
元素用于表示对消息引用。它的子元素会被渲染为引用的内容。理论上所有 <message>
元素的特性也可以用于 <quote>
元素,包括子元素 (构造引用消息) 和 forward
属性 (引用合并转发)。然而目前似乎并没有平台提供了这样的支持。作者 (author)
属性 类型 范围 描述 id string? 发 用户 ID name string? 发 昵称 avatar string? 发 头像 URL <author>
元素用于表示消息的作者。它的子元素会被渲染为作者的名字。交互元素
属性 类型 范围 描述 id string? 发 按钮的 ID type string? 发 按钮的类型 href string? 发 按钮的链接 text string? 发 待输入文本 theme string? 发 按钮的样式 <button>
元素用于表示一个按钮。它的子元素会被渲染为按钮的文本。action
类型的按钮时会触发一个 interaction/button
事件,该事件的 button
资源会包含上述 id
link
类型的按钮时会打开一个链接,该链接的地址为上述 href
input
类型的按钮时会在用户的输入框中填充上述 text
theme
仅建议使用下列值:
',6))])}const F=h(p,[["render",u]]);export{q as __pageData,F as default};
diff --git a/assets/zh-CN_protocol_elements.md.PpF5nfM_.lean.js b/assets/zh-CN_protocol_elements.md.PpF5nfM_.lean.js
new file mode 100644
index 0000000..c1ad544
--- /dev/null
+++ b/assets/zh-CN_protocol_elements.md.PpF5nfM_.lean.js
@@ -0,0 +1,17 @@
+import{_ as h,x as n,a7 as e,m as i,W as a,y as d,k as l,Q as r,h as o}from"./chunks/framework.K8kzz9Vz.js";const q=JSON.parse('{"title":"标准元素","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/protocol/elements.md","filePath":"zh-CN/protocol/elements.md"}'),p={name:"zh-CN/protocol/elements.md"},k={tabindex:"0"},g={id:"button",tabindex:"-1"};function u(c,t,b,y,B,m){const s=r("badge");return o(),n("div",null,[t[17]||(t[17]=e('标准元素
基础元素
提及用户 (at)
属性 类型 范围 描述 id string? 收发 目标用户的 ID name string? 收发 目标用户的名称 role string? 收发 目标角色 type string? 收发 特殊操作,例如 all 表示 @全体成员,here 表示 @在线成员 <at>
元素用于提及某个或某些用户。提及频道 (sharp)
属性 类型 范围 描述 id string 收发 目标频道的 ID name string? 收发 目标频道的名称 <sharp>
元素用于提及某个频道。链接 (a)
属性 类型 范围 描述 href string 收发 链接的 URL <a>
元素用于显示一个链接。当平台不支持链接时,建议显示为 content (href)
的形式。资源元素
src
。如果某个平台不支持特定的资源类型,适配器应该用 src
代替。如果某个平台不支持将资源消息元素和其他消息元素同时发送,适配器应该分多条发送,并返回最后一条消息的 ID。图片 (img)
属性 类型 范围 描述 width number? 收 图片宽度 (像素) height number? 收 图片高度 (像素) <img>
元素用于表示图片。音频 (audio)
属性 类型 范围 描述 duration number? 收 音频长度 (秒) poster string? 收发 音频封面 URL <audio>
元素用于表示语音。视频 (video)
属性 类型 范围 描述 width number? 收 视频宽度 (像素) height number? 收 视频高度 (像素) duration number? 收 视频长度 (秒) poster string? 收发 视频封面 URL <video>
元素用于表示视频。文件 (file)
属性 类型 范围 描述 poster string? 收发 缩略图 URL <file>
元素用于表示文件。修饰元素
粗体 (b, strong)
<b>
或 <strong>
元素用于将其中的内容以粗体显示。斜体 (i, em)
<i>
或 <em>
元素用于将其中的内容以斜体显示。下划线 (u, ins)
<u>
或 <ins>
元素用于为其中的内容附加下划线。删除线 (s, del)
<s>
或 <del>
元素用于为其中的内容附加删除线。剧透 (spl)
<spl>
元素用于将其中的内容标记为剧透 (默认会被隐藏,点击后才显示)。代码 (code)
<code>
元素用于将其中的内容以等宽字体显示 (通常还会有特定的背景色)。上标 (sup)
<sup>
元素用于将其中的内容以上标显示。下标 (sub)
<sub>
元素用于将其中的内容以下标显示。排版元素
换行 (br)
<br>
元素表示一个独立的换行。段落 (p)
<p>
元素表示一个段落。在渲染时,它与相邻的元素之间会确保有一个换行。消息 (message)
属性 类型 范围 描述 id string? 发 消息的 ID forward boolean? 发 是否为转发消息 <message>
元素的基本用法是表示一条消息。子元素对应于消息的内容。如果其没有子元素,则消息不会被发送。<message>
元素时,之前的元素会被立即视为一条消息被发送。因此下面的两种写法是等价的:<!-- 第一种写法:发送两条消息 -->
+<message>hello</message>
+<message>world</message>
+
+<!-- 第二种写法:用一条空消息隔开两段文本,实际上仍然会发送两条消息 -->
+hello<message/>world
<message>
+ <author id="123123123" name="Alice" avatar="url"/>
+ hello world
+</message>
forward
配合 id
属性来转发一条消息:<message id="123456789" forward/>
forward
属性的 <message>
元素嵌套其他 <message>
元素来实现合并转发:<message forward>
+ <message id="123456789"/>
+ <message id="987654321"/>
+ <!-- 合并转发里也可以嵌套模拟其他用户发送的消息 -->
+ <message>
+ <author id="123123123" name="Alice" avatar="url"/>
+ hello world
+ </message>
+</message>
元信息元素
引用 (quote)
<quote>
元素用于表示对消息引用。它的子元素会被渲染为引用的内容。理论上所有 <message>
元素的特性也可以用于 <quote>
元素,包括子元素 (构造引用消息) 和 forward
属性 (引用合并转发)。然而目前似乎并没有平台提供了这样的支持。作者 (author)
属性 类型 范围 描述 id string? 发 用户 ID name string? 发 昵称 avatar string? 发 头像 URL <author>
元素用于表示消息的作者。它的子元素会被渲染为作者的名字。交互元素
属性 类型 范围 描述 id string? 发 按钮的 ID type string? 发 按钮的类型 href string? 发 按钮的链接 text string? 发 待输入文本 theme string? 发 按钮的样式 <button>
元素用于表示一个按钮。它的子元素会被渲染为按钮的文本。action
类型的按钮时会触发一个 interaction/button
事件,该事件的 button
资源会包含上述 id
link
类型的按钮时会打开一个链接,该链接的地址为上述 href
input
类型的按钮时会在用户的输入框中填充上述 text
theme
仅建议使用下列值:
',6))])}const F=h(p,[["render",u]]);export{q as __pageData,F as default};
diff --git a/assets/zh-CN_protocol_events.md.Dh8okkMd.js b/assets/zh-CN_protocol_events.md.Dh8okkMd.js
new file mode 100644
index 0000000..f884282
--- /dev/null
+++ b/assets/zh-CN_protocol_events.md.Dh8okkMd.js
@@ -0,0 +1 @@
+import{_ as l,x as h,a7 as o,m as r,W as e,y as a,k as c,Q as n,h as s}from"./chunks/framework.K8kzz9Vz.js";const v=JSON.parse('{"title":"事件","description":"","frontmatter":{},"headers":[],"relativePath":"zh-CN/protocol/events.md","filePath":"zh-CN/protocol/events.md"}'),i={name:"zh-CN/protocol/events.md"},b={id:"webhook",tabindex:"-1"};function p(u,t,m,f,k,g){const d=n("badge");return s(),h("div",null,[t[6]||(t[6]=o('事件
类型定义
OpCode
名称 值 方向 描述 EVENT 0 接收 事件 PING 1 发送 心跳 PONG 2 接收 心跳回复 IDENTIFY 3 发送 鉴权 READY 4 接收 鉴权成功 META 5 接收 元信息更新 Event
字段 类型 描述 sn
number 序列号 type
string 事件类型 timestamp
number 事件的时间戳 login
Login 登录信息 argv
Argv? 交互指令 button
Button? 交互按钮 channel
Channel? 事件所属的频道 guild
Guild? 事件所属的群组 member
GuildMember? 事件的目标成员 message
Message? 事件的消息 operator
User? 事件的操作者 role
GuildRole? 事件的目标角色 user
User? 事件的目标用户 login
资源只会带有 sn
, user
和 platform
三个属性;login
资源,但可能不存在 user
和 platform
;WebSocket
/{path}/{version}/events
。其中,path
为部署路径 (可以为空),version
为 API 的版本号。连接流程
IDENTIFY
信令,用于鉴权和恢复会话;
SDK 收到后会回复一个 READY
信令,并开启事件推送;PING
信令;
SDK 收到后会回复一个 PONG
信令;EVENT
信令,用于接收事件。字段 类型 描述 op
OpCode
信令类型 body
object? 信令数据 IDENTIFY
信令的 body
数据结构如下:字段 类型 描述 token
string? 鉴权令牌 sn
number? 序列号 READY
信令的 body
数据结构如下:字段 类型 描述 logins
Login[]
登录信息 proxy_urls
string[] 代理路由 列表 META
信令的 body
数据结构如下:字段 类型 描述 proxy_urls
string[] 代理路由 列表 EVENT
信令的 body
数据结构参见 Event。鉴权
token
字段来实现。其中涉及的鉴权令牌由 SDK 分发,本协议不做任何限制。会话恢复
IDENTIFY
信令的 sn
字段来恢复会话。sn
字段的值为上一次连接中最后一个接收到的 EVENT
信令的 sn
字段。会话恢复后,SDK 会向应用推送所有在断开连接期间发生的事件。READY
信令中已经包含了最新的登录状态。Satori-OpCode
字段,对应本次推送的 信令类型;请求体是一个 JSON 对象,对应本次推送的信令数据。例如,一次事件推送将会拥有 Satori-OpCode: 0
的请求头,以及一个符合 Event 结构的请求体。EVENT
, META
两种。反向鉴权
Authorization
请求头,格式为 Bearer {token}
。其中,token
由应用进行分发。事件
类型定义
OpCode
名称 值 方向 描述 EVENT 0 接收 事件 PING 1 发送 心跳 PONG 2 接收 心跳回复 IDENTIFY 3 发送 鉴权 READY 4 接收 鉴权成功 META 5 接收 元信息更新 Event
字段 类型 描述 sn
number 序列号 type
string 事件类型 timestamp
number 事件的时间戳 login
Login 登录信息 argv
Argv? 交互指令 button
Button? 交互按钮 channel
Channel? 事件所属的频道 guild
Guild? 事件所属的群组 member
GuildMember? 事件的目标成员 message
Message? 事件的消息 operator
User? 事件的操作者 role
GuildRole? 事件的目标角色 user
User? 事件的目标用户 login
资源只会带有 sn
, user
和 platform
三个属性;login
资源,但可能不存在 user
和 platform
;WebSocket
/{path}/{version}/events
。其中,path
为部署路径 (可以为空),version
为 API 的版本号。连接流程
IDENTIFY
信令,用于鉴权和恢复会话;
SDK 收到后会回复一个 READY
信令,并开启事件推送;PING
信令;
SDK 收到后会回复一个 PONG
信令;EVENT
信令,用于接收事件。字段 类型 描述 op
OpCode
信令类型 body
object? 信令数据 IDENTIFY
信令的 body
数据结构如下:字段 类型 描述 token
string? 鉴权令牌 sn
number? 序列号 READY
信令的 body
数据结构如下:字段 类型 描述 logins
Login[]
登录信息 proxy_urls
string[] 代理路由 列表 META
信令的 body
数据结构如下:字段 类型 描述 proxy_urls
string[] 代理路由 列表 EVENT
信令的 body
数据结构参见 Event。鉴权
token
字段来实现。其中涉及的鉴权令牌由 SDK 分发,本协议不做任何限制。会话恢复
IDENTIFY
信令的 sn
字段来恢复会话。sn
字段的值为上一次连接中最后一个接收到的 EVENT
信令的 sn
字段。会话恢复后,SDK 会向应用推送所有在断开连接期间发生的事件。READY
信令中已经包含了最新的登录状态。Satori-OpCode
字段,对应本次推送的 信令类型;请求体是一个 JSON 对象,对应本次推送的信令数据。例如,一次事件推送将会拥有 Satori-OpCode: 0
的请求头,以及一个符合 Event 结构的请求体。EVENT
, META
两种。反向鉴权
Authorization
请求头,格式为 Bearer {token}
。其中,token
由应用进行分发。总览
核心概念
可选字段
?
标记。null
。其中,前者表示该 API 并未提供这一字段,但可能由其他 API 提供;后者表示该 API 提供了这一字段,但其值为 null
。资源
user
字段就是一个用户对象。当资源对象出现多级嵌套时,内层的资源将会被统一提升到最外层。例如,当接收到消息事件时,事件体中可以访问到 message
, member
, user
, channel
等资源,但 message
中就不再存在 member
和 user
字段了。总览
核心概念
可选字段
?
标记。null
。其中,前者表示该 API 并未提供这一字段,但可能由其他 API 提供;后者表示该 API 提供了这一字段,但其值为 null
。资源
user
字段就是一个用户对象。当资源对象出现多级嵌套时,内层的资源将会被统一提升到最外层。例如,当接收到消息事件时,事件体中可以访问到 message
, member
, user
, channel
等资源,但 message
中就不再存在 member
和 user
字段了。消息编码
语法
字符
原始字符 转义写法 "
"
&
&
<
<
>
>
'
也可以被书写成 '
或 '
。标签
/
表示这是一个结束标签或自闭合标签,没有 /
符号时则表示这是一个起始标签:<tag>
一个起始标签</tag>
一个结束标签<tag/>
一个自闭合标签属性
key
key="value"
(此时 value
中的 "
需要被转义)key='value'
(此时 value
中的 '
需要被转义)<tag foo="1" bar/>
元素
<parent>
+ text content
+ <child/>
+</parent>
<tag>
+ <foo> bar
+ <!-- comment -->
+</tag>
<tag><foo> bar</tag>
注释
<!--
和 -->
插入一段注释。注释中的部分不会被渲染。标准元素
消息编码
语法
字符
原始字符 转义写法 "
"
&
&
<
<
>
>
'
也可以被书写成 '
或 '
。标签
/
表示这是一个结束标签或自闭合标签,没有 /
符号时则表示这是一个起始标签:<tag>
一个起始标签</tag>
一个结束标签<tag/>
一个自闭合标签属性
key
key="value"
(此时 value
中的 "
需要被转义)key='value'
(此时 value
中的 '
需要被转义)<tag foo="1" bar/>
元素
<parent>
+ text content
+ <child/>
+</parent>
<tag>
+ <foo> bar
+ <!-- comment -->
+</tag>
<tag><foo> bar</tag>
注释
<!--
和 -->
插入一段注释。注释中的部分不会被渲染。标准元素
频道 (Channel)
类型定义
Channel
字段 类型 描述 id string 频道 ID type ChannelType 频道类型 name string? 频道名称 parent_id string? 父频道 ID ChannelType
名称 值 描述 TEXT 0 文本频道 DIRECT 1 私聊频道 CATEGORY 2 分类频道 VOICE 3 语音频道 API
获取群组频道
',8)),d("blockquote",b,[n(e,null,{default:l(()=>t[0]||(t[0]=[a("POST")])),_:1}),t[1]||(t[1]=d("code",null,"/channel.get",-1))]),t[19]||(t[19]=r('字段 类型 描述 channel_id string 频道 ID 获取群组频道列表
',3)),d("blockquote",p,[n(e,null,{default:l(()=>t[2]||(t[2]=[a("POST")])),_:1}),t[3]||(t[3]=d("code",null,"/channel.list",-1))]),t[20]||(t[20]=r('字段 类型 描述 guild_id string 群组 ID next string? 分页令牌 创建群组频道
',3)),d("blockquote",f,[n(e,null,{default:l(()=>t[4]||(t[4]=[a("POST")])),_:1}),t[5]||(t[5]=d("code",null,"/channel.create",-1))]),t[21]||(t[21]=r('字段 类型 描述 guild_id string 群组 ID data Channel 频道数据 修改群组频道
',3)),d("blockquote",q,[n(e,null,{default:l(()=>t[6]||(t[6]=[a("POST")])),_:1}),t[7]||(t[7]=d("code",null,"/channel.update",-1))]),t[22]||(t[22]=r('字段 类型 描述 channel_id string 频道 ID data Channel 频道数据 删除群组频道
',3)),d("blockquote",m,[n(e,null,{default:l(()=>t[8]||(t[8]=[a("POST")])),_:1}),t[9]||(t[9]=d("code",null,"/channel.delete",-1))]),t[23]||(t[23]=d("table",{tabindex:"0"},[d("thead",null,[d("tr",null,[d("th",null,"字段"),d("th",null,"类型"),d("th",null,"描述")])]),d("tbody",null,[d("tr",null,[d("td",null,"channel_id"),d("td",null,"string"),d("td",null,"频道 ID")])])],-1)),t[24]||(t[24]=d("p",null,"删除群组频道。",-1)),d("h3",x,[t[11]||(t[11]=a("禁言群组频道 ")),n(e,{type:"warning"},{default:l(()=>t[10]||(t[10]=[a("实验性")])),_:1}),t[12]||(t[12]=a()),t[13]||(t[13]=d("a",{class:"header-anchor",href:"#禁言群组频道","aria-label":'Permalink to "禁言群组频道 字段 类型 描述 channel_id string 频道 ID duration number 禁言时长 (毫秒) 0
则表示解除禁言。创建私聊频道
',3)),d("blockquote",g,[n(e,null,{default:l(()=>t[16]||(t[16]=[a("POST")])),_:1}),t[17]||(t[17]=d("code",null,"/user.channel.create",-1))]),t[26]||(t[26]=r('字段 类型 描述 user_id string 用户 ID guild_id string? 群组 ID 频道 (Channel)
类型定义
Channel
字段 类型 描述 id string 频道 ID type ChannelType 频道类型 name string? 频道名称 parent_id string? 父频道 ID ChannelType
名称 值 描述 TEXT 0 文本频道 DIRECT 1 私聊频道 CATEGORY 2 分类频道 VOICE 3 语音频道 API
获取群组频道
',8)),d("blockquote",b,[n(e,null,{default:l(()=>t[0]||(t[0]=[a("POST")])),_:1}),t[1]||(t[1]=d("code",null,"/channel.get",-1))]),t[19]||(t[19]=r('字段 类型 描述 channel_id string 频道 ID 获取群组频道列表
',3)),d("blockquote",p,[n(e,null,{default:l(()=>t[2]||(t[2]=[a("POST")])),_:1}),t[3]||(t[3]=d("code",null,"/channel.list",-1))]),t[20]||(t[20]=r('字段 类型 描述 guild_id string 群组 ID next string? 分页令牌 创建群组频道
',3)),d("blockquote",f,[n(e,null,{default:l(()=>t[4]||(t[4]=[a("POST")])),_:1}),t[5]||(t[5]=d("code",null,"/channel.create",-1))]),t[21]||(t[21]=r('字段 类型 描述 guild_id string 群组 ID data Channel 频道数据 修改群组频道
',3)),d("blockquote",q,[n(e,null,{default:l(()=>t[6]||(t[6]=[a("POST")])),_:1}),t[7]||(t[7]=d("code",null,"/channel.update",-1))]),t[22]||(t[22]=r('字段 类型 描述 channel_id string 频道 ID data Channel 频道数据 删除群组频道
',3)),d("blockquote",m,[n(e,null,{default:l(()=>t[8]||(t[8]=[a("POST")])),_:1}),t[9]||(t[9]=d("code",null,"/channel.delete",-1))]),t[23]||(t[23]=d("table",{tabindex:"0"},[d("thead",null,[d("tr",null,[d("th",null,"字段"),d("th",null,"类型"),d("th",null,"描述")])]),d("tbody",null,[d("tr",null,[d("td",null,"channel_id"),d("td",null,"string"),d("td",null,"频道 ID")])])],-1)),t[24]||(t[24]=d("p",null,"删除群组频道。",-1)),d("h3",x,[t[11]||(t[11]=a("禁言群组频道 ")),n(e,{type:"warning"},{default:l(()=>t[10]||(t[10]=[a("实验性")])),_:1}),t[12]||(t[12]=a()),t[13]||(t[13]=d("a",{class:"header-anchor",href:"#禁言群组频道","aria-label":'Permalink to "禁言群组频道 字段 类型 描述 channel_id string 频道 ID duration number 禁言时长 (毫秒) 0
则表示解除禁言。创建私聊频道
',3)),d("blockquote",g,[n(e,null,{default:l(()=>t[16]||(t[16]=[a("POST")])),_:1}),t[17]||(t[17]=d("code",null,"/user.channel.create",-1))]),t[26]||(t[26]=r('字段 类型 描述 user_id string 用户 ID guild_id string? 群组 ID 群组 (Guild)
类型定义
Guild
字段 类型 描述 id string 群组 ID name string? 群组名称 avatar string? 群组头像 API
获取群组
',6)),d("blockquote",b,[r(e,null,{default:l(()=>t[0]||(t[0]=[o("POST")])),_:1}),t[1]||(t[1]=d("code",null,"/guild.get",-1))]),t[7]||(t[7]=a('字段 类型 描述 guild_id string 群组 ID 获取群组列表
',3)),d("blockquote",g,[r(e,null,{default:l(()=>t[2]||(t[2]=[o("POST")])),_:1}),t[3]||(t[3]=d("code",null,"/guild.list",-1))]),t[8]||(t[8]=a('字段 类型 描述 next string? 分页令牌 处理群组邀请
',3)),d("blockquote",p,[r(e,null,{default:l(()=>t[4]||(t[4]=[o("POST")])),_:1}),t[5]||(t[5]=d("code",null,"/guild.approve",-1))]),t[9]||(t[9]=a('字段 类型 描述 message_id string 请求 ID approve boolean 是否通过请求 comment string 备注信息 事件
guild-added
guild
。guild-updated
guild
。guild-removed
guild
。guild-request
guild
。群组 (Guild)
类型定义
Guild
字段 类型 描述 id string 群组 ID name string? 群组名称 avatar string? 群组头像 API
获取群组
',6)),d("blockquote",b,[r(e,null,{default:l(()=>t[0]||(t[0]=[o("POST")])),_:1}),t[1]||(t[1]=d("code",null,"/guild.get",-1))]),t[7]||(t[7]=a('字段 类型 描述 guild_id string 群组 ID 获取群组列表
',3)),d("blockquote",g,[r(e,null,{default:l(()=>t[2]||(t[2]=[o("POST")])),_:1}),t[3]||(t[3]=d("code",null,"/guild.list",-1))]),t[8]||(t[8]=a('字段 类型 描述 next string? 分页令牌 处理群组邀请
',3)),d("blockquote",p,[r(e,null,{default:l(()=>t[4]||(t[4]=[o("POST")])),_:1}),t[5]||(t[5]=d("code",null,"/guild.approve",-1))]),t[9]||(t[9]=a('字段 类型 描述 message_id string 请求 ID approve boolean 是否通过请求 comment string 备注信息 事件
guild-added
guild
。guild-updated
guild
。guild-removed
guild
。guild-request
guild
。类型定义
Argv
字段 类型 描述 name string 指令名称 arguments any[] 参数 options object 选项 Button
字段 类型 描述 id string 按钮 ID 事件
interaction/button
action
的按钮被点击时触发。必需资源:button
。interaction/command
argv
或 message
中至少包含其一。类型定义
Argv
字段 类型 描述 name string 指令名称 arguments any[] 参数 options object 选项 Button
字段 类型 描述 id string 按钮 ID 事件
interaction/button
action
的按钮被点击时触发。必需资源:button
。interaction/command
argv
或 message
中至少包含其一。登录信息 (Login)
类型定义
Login
',3)),t("table",p,[l[11]||(l[11]=t("thead",null,[t("tr",null,[t("th",null,"字段"),t("th",null,"类型"),t("th",null,"描述")])],-1)),t("tbody",null,[t("tr",null,[l[4]||(l[4]=t("td",null,[t("code",null,"sn")],-1)),l[5]||(l[5]=t("td",null,"string",-1)),t("td",null,[l[1]||(l[1]=d("序列号")),l[2]||(l[2]=t("sup",null,[t("a",{href:"#login-sn"},"[1]")],-1)),l[3]||(l[3]=d()),n(e,{type:"warning"},{default:a(()=>l[0]||(l[0]=[d("实验性")])),_:1})])]),l[6]||(l[6]=t("tr",null,[t("td",null,[t("code",null,"adapter")]),t("td",null,"string"),t("td",null,[t("a",{href:"./../advanced/internal.html#platform-adapter"},"适配器名称")])],-1)),l[7]||(l[7]=t("tr",null,[t("td",null,[t("code",null,"platform")]),t("td",null,"string?"),t("td",null,"平台名称")],-1)),l[8]||(l[8]=t("tr",null,[t("td",null,[t("code",null,"user")]),t("td",null,[t("a",{href:"./user.html"},"User"),d("?")]),t("td",null,[d("用户对象"),t("sup",null,[t("a",{href:"#login-sn"},"[1]")])])],-1)),l[9]||(l[9]=t("tr",null,[t("td",null,[t("code",null,"status")]),t("td",null,[t("a",{href:"#loginstatus"},"LoginStatus"),d("?")]),t("td",null,"登录状态")],-1)),l[10]||(l[10]=t("tr",null,[t("td",null,[t("code",null,"features")]),t("td",null,"string[]?"),t("td",null,[t("a",{href:"./../protocol/api.html#平台特性"},"平台特性"),d(" 列表")])],-1))])]),l[15]||(l[15]=o('login.sn
仅用于标识 Login 对象,与平台逻辑无关 (意味着任何平台相关的 API 调用都不需要传入这个 sn
),也不进行持久化 (意味着两次连接中同一个登录号的 sn
可能是不同的,不同登录号的 sn
可能是相同的)。请尤其注意与 login.user.id
区分。login.user
并不一定是真实存在的平台用户,也可以是平台分配的机器人或者应用身份。LoginStatus
名称 值 描述 OFFLINE 0 离线 ONLINE 1 在线 CONNECT 2 连接中 DISCONNECT 3 断开连接 RECONNECT 4 重新连接 API
获取登录信息
',6)),t("blockquote",h,[n(e,null,{default:a(()=>l[12]||(l[12]=[d("POST")])),_:1}),l[13]||(l[13]=t("code",null,"/login.get",-1))]),l[16]||(l[16]=o('Login
对象。事件
login-added
login
。login-removed
login
。login-updated
login
。登录信息 (Login)
类型定义
Login
',3)),t("table",p,[l[11]||(l[11]=t("thead",null,[t("tr",null,[t("th",null,"字段"),t("th",null,"类型"),t("th",null,"描述")])],-1)),t("tbody",null,[t("tr",null,[l[4]||(l[4]=t("td",null,[t("code",null,"sn")],-1)),l[5]||(l[5]=t("td",null,"string",-1)),t("td",null,[l[1]||(l[1]=d("序列号")),l[2]||(l[2]=t("sup",null,[t("a",{href:"#login-sn"},"[1]")],-1)),l[3]||(l[3]=d()),n(e,{type:"warning"},{default:a(()=>l[0]||(l[0]=[d("实验性")])),_:1})])]),l[6]||(l[6]=t("tr",null,[t("td",null,[t("code",null,"adapter")]),t("td",null,"string"),t("td",null,[t("a",{href:"./../advanced/internal.html#platform-adapter"},"适配器名称")])],-1)),l[7]||(l[7]=t("tr",null,[t("td",null,[t("code",null,"platform")]),t("td",null,"string?"),t("td",null,"平台名称")],-1)),l[8]||(l[8]=t("tr",null,[t("td",null,[t("code",null,"user")]),t("td",null,[t("a",{href:"./user.html"},"User"),d("?")]),t("td",null,[d("用户对象"),t("sup",null,[t("a",{href:"#login-sn"},"[1]")])])],-1)),l[9]||(l[9]=t("tr",null,[t("td",null,[t("code",null,"status")]),t("td",null,[t("a",{href:"#loginstatus"},"LoginStatus"),d("?")]),t("td",null,"登录状态")],-1)),l[10]||(l[10]=t("tr",null,[t("td",null,[t("code",null,"features")]),t("td",null,"string[]?"),t("td",null,[t("a",{href:"./../protocol/api.html#平台特性"},"平台特性"),d(" 列表")])],-1))])]),l[15]||(l[15]=o('login.sn
仅用于标识 Login 对象,与平台逻辑无关 (意味着任何平台相关的 API 调用都不需要传入这个 sn
),也不进行持久化 (意味着两次连接中同一个登录号的 sn
可能是不同的,不同登录号的 sn
可能是相同的)。请尤其注意与 login.user.id
区分。login.user
并不一定是真实存在的平台用户,也可以是平台分配的机器人或者应用身份。LoginStatus
名称 值 描述 OFFLINE 0 离线 ONLINE 1 在线 CONNECT 2 连接中 DISCONNECT 3 断开连接 RECONNECT 4 重新连接 API
获取登录信息
',6)),t("blockquote",h,[n(e,null,{default:a(()=>l[12]||(l[12]=[d("POST")])),_:1}),l[13]||(l[13]=t("code",null,"/login.get",-1))]),l[16]||(l[16]=o('Login
对象。事件
login-added
login
。login-removed
login
。login-updated
login
。群组成员 (GuildMember)
类型定义
GuildMember
字段 类型 描述 user User? 用户对象 nick string? 用户在群组中的名称 avatar string? 用户在群组中的头像 joined_at number? 加入时间 API
获取群组成员
',6)),e("blockquote",b,[o(r,null,{default:l(()=>t[0]||(t[0]=[d("POST")])),_:1}),t[1]||(t[1]=e("code",null,"/guild.member.get",-1))]),t[15]||(t[15]=a('字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID 获取群组成员列表
',3)),e("blockquote",h,[o(r,null,{default:l(()=>t[2]||(t[2]=[d("POST")])),_:1}),t[3]||(t[3]=e("code",null,"/guild.member.list",-1))]),t[16]||(t[16]=a('字段 类型 描述 guild_id string 群组 ID next string? 分页令牌 踢出群组成员
',3)),e("blockquote",g,[o(r,null,{default:l(()=>t[4]||(t[4]=[d("POST")])),_:1}),t[5]||(t[5]=e("code",null,"/guild.member.kick",-1))]),t[17]||(t[17]=a('字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID permanent boolean? 是否永久踢出 (无法再次加入群组) 字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID duration number 禁言时长 (毫秒) 0
则表示解除禁言。通过群组成员申请
',3)),e("blockquote",f,[o(r,null,{default:l(()=>t[12]||(t[12]=[d("POST")])),_:1}),t[13]||(t[13]=e("code",null,"/guild.member.approve",-1))]),t[19]||(t[19]=a('字段 类型 描述 message_id string 请求 ID approve boolean 是否通过请求 comment string? 备注信息 事件
guild-member-added
guild
,member
,user
。guild-member-updated
guild
,member
,user
。guild-member-removed
guild
,member
,user
。guild-member-request
guild
,member
,user
。群组成员 (GuildMember)
类型定义
GuildMember
字段 类型 描述 user User? 用户对象 nick string? 用户在群组中的名称 avatar string? 用户在群组中的头像 joined_at number? 加入时间 API
获取群组成员
',6)),e("blockquote",b,[o(r,null,{default:l(()=>t[0]||(t[0]=[d("POST")])),_:1}),t[1]||(t[1]=e("code",null,"/guild.member.get",-1))]),t[15]||(t[15]=a('字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID 获取群组成员列表
',3)),e("blockquote",h,[o(r,null,{default:l(()=>t[2]||(t[2]=[d("POST")])),_:1}),t[3]||(t[3]=e("code",null,"/guild.member.list",-1))]),t[16]||(t[16]=a('字段 类型 描述 guild_id string 群组 ID next string? 分页令牌 踢出群组成员
',3)),e("blockquote",g,[o(r,null,{default:l(()=>t[4]||(t[4]=[d("POST")])),_:1}),t[5]||(t[5]=e("code",null,"/guild.member.kick",-1))]),t[17]||(t[17]=a('字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID permanent boolean? 是否永久踢出 (无法再次加入群组) 字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID duration number 禁言时长 (毫秒) 0
则表示解除禁言。通过群组成员申请
',3)),e("blockquote",f,[o(r,null,{default:l(()=>t[12]||(t[12]=[d("POST")])),_:1}),t[13]||(t[13]=e("code",null,"/guild.member.approve",-1))]),t[19]||(t[19]=a('字段 类型 描述 message_id string 请求 ID approve boolean 是否通过请求 comment string? 备注信息 事件
guild-member-added
guild
,member
,user
。guild-member-updated
guild
,member
,user
。guild-member-removed
guild
,member
,user
。guild-member-request
guild
,member
,user
。消息 (Message)
类型定义
Message
字段 类型 描述 id string 消息 ID content string 消息内容 channel Channel? 频道对象 guild Guild? 群组对象 member Member? 群组成员对象 user User? 用户对象 created_at number? 消息发送的时间戳 updated_at number? 消息修改的时间戳 API
发送消息
',6)),e("blockquote",c,[r(a,null,{default:s(()=>t[0]||(t[0]=[o("POST")])),_:1}),t[1]||(t[1]=e("code",null,"/message.create",-1))]),t[11]||(t[11]=d('字段 类型 描述 channel_id string 频道 ID content string 消息内容 Message
对象构成的数组。获取消息
',3)),e("blockquote",u,[r(a,null,{default:s(()=>t[2]||(t[2]=[o("POST")])),_:1}),t[3]||(t[3]=e("code",null,"/message.get",-1))]),t[12]||(t[12]=d('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID Message
对象。必需资源:channel
,user
。撤回消息
',3)),e("blockquote",g,[r(a,null,{default:s(()=>t[4]||(t[4]=[o("POST")])),_:1}),t[5]||(t[5]=e("code",null,"/message.delete",-1))]),t[13]||(t[13]=d('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID 编辑消息
',3)),e("blockquote",b,[r(a,null,{default:s(()=>t[6]||(t[6]=[o("POST")])),_:1}),t[7]||(t[7]=e("code",null,"/message.update",-1))]),t[14]||(t[14]=d('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID content string 消息内容 获取消息列表
',3)),e("blockquote",p,[r(a,null,{default:s(()=>t[8]||(t[8]=[o("POST")])),_:1}),t[9]||(t[9]=e("code",null,"/message.list",-1))]),t[15]||(t[15]=d('字段 类型 描述 channel_id string 频道 ID next string? 分页令牌 direction Direction? 查询方向 limit number? 消息数量限制 order Order? 对结果排序 Message
的 双向分页列表。必需资源:user
。next
参数默认值为空,表示从最新消息开始查询。此时 direction
参数只能为 before
。direction
参数默认为 before
。order
参数默认为 asc
(无论查询方向)。limit
参数的默认值与平台默认值保持一致。如果平台 API 没有设定默认值,则可以自行设定,推荐值为 50。如果用户传入值超出平台要求的上限,则应当改为使用平台的上限值,而不是返回错误。开发者应当使用返回值中 prev
或 next
的存在性判断是否有更多数据,而非依赖于返回值中 data
的长度。事件
message-created
channel
,message
,user
。message-updated
channel
,message
,user
。message-deleted
channel
,message
,user
。消息 (Message)
类型定义
Message
字段 类型 描述 id string 消息 ID content string 消息内容 channel Channel? 频道对象 guild Guild? 群组对象 member Member? 群组成员对象 user User? 用户对象 created_at number? 消息发送的时间戳 updated_at number? 消息修改的时间戳 API
发送消息
',6)),e("blockquote",c,[r(a,null,{default:s(()=>t[0]||(t[0]=[o("POST")])),_:1}),t[1]||(t[1]=e("code",null,"/message.create",-1))]),t[11]||(t[11]=d('字段 类型 描述 channel_id string 频道 ID content string 消息内容 Message
对象构成的数组。获取消息
',3)),e("blockquote",u,[r(a,null,{default:s(()=>t[2]||(t[2]=[o("POST")])),_:1}),t[3]||(t[3]=e("code",null,"/message.get",-1))]),t[12]||(t[12]=d('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID Message
对象。必需资源:channel
,user
。撤回消息
',3)),e("blockquote",g,[r(a,null,{default:s(()=>t[4]||(t[4]=[o("POST")])),_:1}),t[5]||(t[5]=e("code",null,"/message.delete",-1))]),t[13]||(t[13]=d('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID 编辑消息
',3)),e("blockquote",b,[r(a,null,{default:s(()=>t[6]||(t[6]=[o("POST")])),_:1}),t[7]||(t[7]=e("code",null,"/message.update",-1))]),t[14]||(t[14]=d('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID content string 消息内容 获取消息列表
',3)),e("blockquote",p,[r(a,null,{default:s(()=>t[8]||(t[8]=[o("POST")])),_:1}),t[9]||(t[9]=e("code",null,"/message.list",-1))]),t[15]||(t[15]=d('字段 类型 描述 channel_id string 频道 ID next string? 分页令牌 direction Direction? 查询方向 limit number? 消息数量限制 order Order? 对结果排序 Message
的 双向分页列表。必需资源:user
。next
参数默认值为空,表示从最新消息开始查询。此时 direction
参数只能为 before
。direction
参数默认为 before
。order
参数默认为 asc
(无论查询方向)。limit
参数的默认值与平台默认值保持一致。如果平台 API 没有设定默认值,则可以自行设定,推荐值为 50。如果用户传入值超出平台要求的上限,则应当改为使用平台的上限值,而不是返回错误。开发者应当使用返回值中 prev
或 next
的存在性判断是否有更多数据,而非依赖于返回值中 data
的长度。事件
message-created
channel
,message
,user
。message-updated
channel
,message
,user
。message-deleted
channel
,message
,user
。字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID emoji string 表态名称 删除表态
',3)),d("blockquote",p,[r(a,null,{default:o(()=>t[6]||(t[6]=[e("POST")])),_:1}),t[7]||(t[7]=d("code",null,"/reaction.delete",-1))]),t[15]||(t[15]=n('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID emoji string 表态名称 user_id string? 用户 ID 清除表态
',3)),d("blockquote",g,[r(a,null,{default:o(()=>t[8]||(t[8]=[e("POST")])),_:1}),t[9]||(t[9]=d("code",null,"/reaction.clear",-1))]),t[16]||(t[16]=n('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID emoji string? 表态名称 获取表态列表
',3)),d("blockquote",f,[r(a,null,{default:o(()=>t[10]||(t[10]=[e("POST")])),_:1}),t[11]||(t[11]=d("code",null,"/reaction.list",-1))]),t[17]||(t[17]=n('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID emoji string 表态名称 next string? 分页令牌 User
的 分页列表。事件
reaction-added
reaction-removed
字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID emoji string 表态名称 删除表态
',3)),d("blockquote",p,[r(a,null,{default:o(()=>t[6]||(t[6]=[e("POST")])),_:1}),t[7]||(t[7]=d("code",null,"/reaction.delete",-1))]),t[15]||(t[15]=n('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID emoji string 表态名称 user_id string? 用户 ID 清除表态
',3)),d("blockquote",g,[r(a,null,{default:o(()=>t[8]||(t[8]=[e("POST")])),_:1}),t[9]||(t[9]=d("code",null,"/reaction.clear",-1))]),t[16]||(t[16]=n('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID emoji string? 表态名称 获取表态列表
',3)),d("blockquote",f,[r(a,null,{default:o(()=>t[10]||(t[10]=[e("POST")])),_:1}),t[11]||(t[11]=d("code",null,"/reaction.list",-1))]),t[17]||(t[17]=n('字段 类型 描述 channel_id string 频道 ID message_id string 消息 ID emoji string 表态名称 next string? 分页令牌 User
的 分页列表。事件
reaction-added
reaction-removed
群组角色 (GuildRole)
类型定义
GuildRole
字段 类型 描述 id string 角色 ID name string? 角色名称 API
设置群组成员角色
',6)),d("blockquote",b,[l(r,null,{default:a(()=>t[0]||(t[0]=[o("POST")])),_:1}),t[1]||(t[1]=d("code",null,"/guild.member.role.set",-1))]),t[13]||(t[13]=e('字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID role_id string 角色 ID 取消群组成员角色
',3)),d("blockquote",g,[l(r,null,{default:a(()=>t[2]||(t[2]=[o("POST")])),_:1}),t[3]||(t[3]=d("code",null,"/guild.member.role.unset",-1))]),t[14]||(t[14]=e('字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID role_id string 角色 ID 获取群组角色列表
',3)),d("blockquote",p,[l(r,null,{default:a(()=>t[4]||(t[4]=[o("POST")])),_:1}),t[5]||(t[5]=d("code",null,"/guild.role.list",-1))]),t[15]||(t[15]=e('字段 类型 描述 guild_id string 群组 ID next string? 分页令牌 创建群组角色
',3)),d("blockquote",q,[l(r,null,{default:a(()=>t[6]||(t[6]=[o("POST")])),_:1}),t[7]||(t[7]=d("code",null,"/guild.role.create",-1))]),t[16]||(t[16]=e('字段 类型 描述 guild_id string 群组 ID role GuildRole 角色数据 修改群组角色
',3)),d("blockquote",f,[l(r,null,{default:a(()=>t[8]||(t[8]=[o("POST")])),_:1}),t[9]||(t[9]=d("code",null,"/guild.role.update",-1))]),t[17]||(t[17]=e('字段 类型 描述 guild_id string 群组 ID role_id string 角色 ID role GuildRole 角色数据 删除群组角色
',3)),d("blockquote",m,[l(r,null,{default:a(()=>t[10]||(t[10]=[o("POST")])),_:1}),t[11]||(t[11]=d("code",null,"/guild.role.delete",-1))]),t[18]||(t[18]=e('字段 类型 描述 guild_id string 群组 ID role_id string 角色 ID 事件
guild-role-created
guild
,role
。guild-role-updated
guild
,role
。guild-role-deleted
guild
,role
。群组角色 (GuildRole)
类型定义
GuildRole
字段 类型 描述 id string 角色 ID name string? 角色名称 API
设置群组成员角色
',6)),d("blockquote",b,[l(r,null,{default:a(()=>t[0]||(t[0]=[o("POST")])),_:1}),t[1]||(t[1]=d("code",null,"/guild.member.role.set",-1))]),t[13]||(t[13]=e('字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID role_id string 角色 ID 取消群组成员角色
',3)),d("blockquote",g,[l(r,null,{default:a(()=>t[2]||(t[2]=[o("POST")])),_:1}),t[3]||(t[3]=d("code",null,"/guild.member.role.unset",-1))]),t[14]||(t[14]=e('字段 类型 描述 guild_id string 群组 ID user_id string 用户 ID role_id string 角色 ID 获取群组角色列表
',3)),d("blockquote",p,[l(r,null,{default:a(()=>t[4]||(t[4]=[o("POST")])),_:1}),t[5]||(t[5]=d("code",null,"/guild.role.list",-1))]),t[15]||(t[15]=e('字段 类型 描述 guild_id string 群组 ID next string? 分页令牌 创建群组角色
',3)),d("blockquote",q,[l(r,null,{default:a(()=>t[6]||(t[6]=[o("POST")])),_:1}),t[7]||(t[7]=d("code",null,"/guild.role.create",-1))]),t[16]||(t[16]=e('字段 类型 描述 guild_id string 群组 ID role GuildRole 角色数据 修改群组角色
',3)),d("blockquote",f,[l(r,null,{default:a(()=>t[8]||(t[8]=[o("POST")])),_:1}),t[9]||(t[9]=d("code",null,"/guild.role.update",-1))]),t[17]||(t[17]=e('字段 类型 描述 guild_id string 群组 ID role_id string 角色 ID role GuildRole 角色数据 删除群组角色
',3)),d("blockquote",m,[l(r,null,{default:a(()=>t[10]||(t[10]=[o("POST")])),_:1}),t[11]||(t[11]=d("code",null,"/guild.role.delete",-1))]),t[18]||(t[18]=e('字段 类型 描述 guild_id string 群组 ID role_id string 角色 ID 事件
guild-role-created
guild
,role
。guild-role-updated
guild
,role
。guild-role-deleted
guild
,role
。用户 (User)
类型定义
User
字段 类型 描述 id string 用户 ID name string? 用户名称[1] nick string? 用户昵称[1] avatar string? 用户头像链接 is_bot boolean? 是否为机器人 name
和 nick
字段的区别name
或 nick
中的一个即可。nick
的优先级高于 name
,因为昵称更容易被用户识别和理解。如果你正在开发基于 Satori 协议的客户端,在用户名的显示上应当优先使用 nick
字段,只有当 nick
为空时才使用 name
字段。API
获取用户信息
',7)),e("blockquote",c,[r(a,null,{default:o(()=>t[0]||(t[0]=[s("POST")])),_:1}),t[1]||(t[1]=e("code",null,"/user.get",-1))]),t[7]||(t[7]=d('字段 类型 描述 user_id string 用户 ID User
对象。获取好友列表
',3)),e("blockquote",b,[r(a,null,{default:o(()=>t[2]||(t[2]=[s("POST")])),_:1}),t[3]||(t[3]=e("code",null,"/friend.list",-1))]),t[8]||(t[8]=d('字段 类型 描述 next string? 分页令牌 处理好友申请
',3)),e("blockquote",p,[r(a,null,{default:o(()=>t[4]||(t[4]=[s("POST")])),_:1}),t[5]||(t[5]=e("code",null,"/friend.approve",-1))]),t[9]||(t[9]=d('字段 类型 描述 message_id string 请求 ID approve boolean 是否通过请求 comment string? 备注信息 事件
friend-request
user
。用户 (User)
类型定义
User
字段 类型 描述 id string 用户 ID name string? 用户名称[1] nick string? 用户昵称[1] avatar string? 用户头像链接 is_bot boolean? 是否为机器人 name
和 nick
字段的区别name
或 nick
中的一个即可。nick
的优先级高于 name
,因为昵称更容易被用户识别和理解。如果你正在开发基于 Satori 协议的客户端,在用户名的显示上应当优先使用 nick
字段,只有当 nick
为空时才使用 name
字段。API
获取用户信息
',7)),e("blockquote",c,[r(a,null,{default:o(()=>t[0]||(t[0]=[s("POST")])),_:1}),t[1]||(t[1]=e("code",null,"/user.get",-1))]),t[7]||(t[7]=d('字段 类型 描述 user_id string 用户 ID User
对象。获取好友列表
',3)),e("blockquote",b,[r(a,null,{default:o(()=>t[2]||(t[2]=[s("POST")])),_:1}),t[3]||(t[3]=e("code",null,"/friend.list",-1))]),t[8]||(t[8]=d('字段 类型 描述 next string? 分页令牌 处理好友申请
',3)),e("blockquote",p,[r(a,null,{default:o(()=>t[4]||(t[4]=[s("POST")])),_:1}),t[5]||(t[5]=e("code",null,"/friend.approve",-1))]),t[9]||(t[9]=d('字段 类型 描述 message_id string 请求 ID approve boolean 是否通过请求 comment string? 备注信息 事件
friend-request
user
。