From 1445b938911afd5110e539db72b9514e07138dd3 Mon Sep 17 00:00:00 2001 From: xmanrui <841206367@qq.com> Date: Tue, 25 Aug 2026 17:15:06 +0800 Subject: [PATCH] fix: preserve error reactions for failed interactions --- lib/index.js | 2 +- src/channels/shared/text-harness-bridge.mjs | 8 +++++- .../shared/text-harness-bridge.test.mjs | 26 ++++++++++++++++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index 5572817..31faf07 100644 --- a/lib/index.js +++ b/lib/index.js @@ -215,7 +215,7 @@ ${N}`:j;y&&await Q.finish(H)||await this.#O(g,H)}catch{this.#c.error?.("[dsh-din `,{encoding:"utf8",mode:384}),await Vpe(o,this.#e)}};var sx=Object.freeze({botPrefix:"discord",tokenRefPrefix:"DSH_DISCORD_BOT_TOKEN"});function gx(n){return aE(n,sx)}function cx(n){return _f(n,D("Discord\u673A\u5668\u4EBA"))}var Tf=class extends Vl{constructor(t){super(t,{channel:"Discord",...sx})}};function iE(n){return typeof n=="string"&&n.trim()?n.trim():null}function oE(n,t){return Object.freeze({code:n,message:t})}var Wl=class{#e;#A;#t;#r;#a;#i;#c;#s;#I;#o=new Map;#n=new Map;#g=new Map;#l=0;#p=!1;constructor({descriptor:t,credentials:o,configStore:i,inspectToken:e,deriveIdentity:a,maskPlatformId:A,createRuntime:r,deleteState:s=async()=>{},logger:g=console}){if(!t?.key||!t?.label||!t?.connectionLabel)throw new TypeError("TokenBotController requires a channel descriptor");if(!o||typeof o.resolve!="function"||typeof o.set!="function"||typeof o.unset!="function")throw new TypeError(`${t.label} requires the DSH credential provider`);if(!i||typeof i.list!="function"||typeof i.save!="function"||typeof i.remove!="function")throw new TypeError(`${t.label} requires a config store`);if(typeof e!="function"||typeof a!="function"||typeof A!="function"||typeof r!="function")throw new TypeError(`${t.label} controller dependencies are incomplete`);this.#e=t,this.#A=o,this.#t=i,this.#r=e,this.#a=a,this.#i=A,this.#c=r,this.#s=s,this.#I=g}async initialize(){if(this.#p)return this.status();for(let t of this.#t.list())await this.#B(t.botId,async()=>{if(this.#p||this.#o.get(t.botId)?.status?.ready)return;let o=await this.#E(t.tokenRef);if(!o){this.#n.set(t.botId,oE("missing-token",D("{label}\u673A\u5668\u4EBA\u51ED\u636E\u7F3A\u5931\uFF0C\u8BF7\u79FB\u9664\u540E\u91CD\u65B0\u63A5\u5165\u3002",{label:this.#e.label})));return}try{await this.#d(t,o),this.#n.delete(t.botId)}catch(i){this.#n.set(t.botId,oE("connection-failed",D("{label}\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002",{label:this.#e.label}))),this.#I.warn?.(`[dsh-im:${this.#e.key}] bot ${t.botId} failed to initialize:`,i)}finally{this.#f()}});return this.status()}async bindCredentials({token:t}={}){if(this.#p)throw new Error(`${this.#e.label} controller is closed`);let o=iE(t);if(!o)throw new TypeError(`${this.#e.label} Bot Token is required`);let i=await this.#r(o),e=iE(i?.platformId),a=iE(i?.name);if(!e||!a)throw new Error(`${this.#e.label} returned an invalid bot identity`);let A=this.#a(e);return await this.#B(A.botId,async()=>{if(this.#p)throw new Error(`${this.#e.label} controller is closed`);let r=this.#t.getByPlatformId(e),s=await this.#A.resolve(A.tokenRef).catch(()=>{}),g={botId:A.botId,platformId:e,tokenRef:A.tokenRef,name:a,username:iE(i.username),createdAt:r?.createdAt??new Date().toISOString(),connectedAt:new Date().toISOString()};await this.#A.set(A.tokenRef,o);try{await this.#t.save(g)}catch(c){throw await this.#u(A.tokenRef,s),c}try{await this.#d(g,o),this.#n.delete(A.botId)}catch(c){this.#n.set(A.botId,oE("connection-failed",D("{label}\u673A\u5668\u4EBA\u5DF2\u63A5\u5165\uFF0C\u6D88\u606F\u8FDE\u63A5\u6682\u672A\u5C31\u7EEA\u3002",{label:this.#e.label}))),this.#I.warn?.(`[dsh-im:${this.#e.key}] bot ${A.botId} credential connection failed:`,c)}this.#f()}),this.status()}async reconnectBot(t){let o=this.#t.get(t);if(!o)throw new Error(`Unknown ${this.#e.label} bot`);return await this.#B(t,async()=>{let i=await this.#E(o.tokenRef);if(!i)throw new Error(`${this.#e.label} bot token is missing`);try{await this.#d(o,i),this.#n.delete(t)}catch(e){throw this.#n.set(t,oE("connection-failed",D("{label}\u8FDE\u63A5\u4ECD\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{label:this.#e.label}))),e}finally{this.#f()}}),this.status()}async updateBotConfig(t,o){if(this.#p)throw new Error(`${this.#e.label} controller is closed`);if(typeof o!="function")throw new TypeError("Bot config update must be a function");return await this.#B(t,async()=>{if(this.#p)throw new Error(`${this.#e.label} controller is closed`);let i=this.#t.get(t);if(!i)throw new Error(`Unknown ${this.#e.label} bot`);let e=await this.#E(i.tokenRef);if(!e)throw new Error(`${this.#e.label} bot token is missing`);if(this.#p)throw new Error(`${this.#e.label} controller is closed`);let a=o(i),A=await this.#t.save(a);try{await this.#d(A,e),this.#n.delete(t)}catch(r){throw this.#n.set(t,oE("connection-failed",D("{label}\u8FDE\u63A5\u4ECD\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{label:this.#e.label}))),r}finally{this.#f()}}),this.status()}async sendConnectionTest(t){let o=this.#t.get(t);if(!o)throw new Error(`Unknown ${this.#e.label} bot`);return this.#B(t,async()=>{let i=this.#o.get(t);if(!i?.status?.ready||typeof i.sendConnectionTest!="function"){let a=new Error(D("{label}\u673A\u5668\u4EBA\u5C1A\u672A\u8FDE\u63A5",{label:this.#e.label}));throw a.code="test-target-unavailable",a}let e=D("{name}\uFF08{id}\uFF09",{name:o.name,id:this.#i(o.platformId)});return await i.sendConnectionTest(ma(e,D("{label}\u673A\u5668\u4EBA",{label:this.#e.label}))),{sent:!0}})}async deleteBot(t){let o=this.#t.get(t);if(!o)throw new Error(`Unknown ${this.#e.label} bot`);return await this.#B(t,async()=>{let i=await this.#A.resolve(o.tokenRef).catch(()=>{});await this.#h(t);try{await this.#A.unset(o.tokenRef),await this.#t.remove(t)}catch(e){throw i?.value&&(await this.#A.set(o.tokenRef,i.value).catch(()=>{}),await this.#d(o,i.value).catch(()=>{})),new Error(`Unable to remove the ${this.#e.label} bot safely.`,{cause:e})}await this.#s({botId:t,config:o}).catch(e=>{this.#I.warn?.(`[dsh-im:${this.#e.key}] bot ${t} state cleanup failed:`,e)}),this.#n.delete(t),this.#f()}),this.status()}status(){let t=this.#t.list().map(i=>{let e=this.#o.get(i.botId)?.status??null,a=e?.ready===!0&&e.connectionState==="connected"&&e.harnessReachable===!0,A=a?"connected":e?.connectionState==="connecting"?"connecting":this.#n.has(i.botId)||e?.connectionState==="failed"?"error":"offline";return{botId:i.botId,state:A,connected:a,configured:!0,bot:{name:i.name,username:i.username,idMasked:this.#i(i.platformId)},health:{status:a?"healthy":A==="error"?"error":"offline",summary:a?D("{label}{connectionLabel}\u8FD0\u884C\u6B63\u5E38",{label:this.#e.label,connectionLabel:D(this.#e.connectionLabel)}):A==="error"?D("{label}\u8FDE\u63A5\u672A\u5C31\u7EEA\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u91CD\u8BD5",{label:this.#e.label}):D("{label}\u8FDE\u63A5\u5F53\u524D\u79BB\u7EBF",{label:this.#e.label}),lastCheckedAt:e?.lastCheckedAt??null,lastConnectedAt:e?.lastConnectedAt??null},stats:{messagesReceived:e?.messagesReceived??0,messagesReplied:e?.messagesReplied??0},lastMessageError:qr(e?.lastMessageError),error:structuredClone(this.#n.get(i.botId)??null)}}),o=t.filter(i=>i.connected).length;return{schemaVersion:1,revision:this.#l,state:t.length===0?"disconnected":o===t.length?"connected":o>0?"degraded":"offline",bots:t,totals:{configured:t.length,connected:o}}}async close(){this.#p||(this.#p=!0,await Promise.allSettled([...this.#g.values()]),await Promise.allSettled([...this.#o.keys()].map(t=>this.#h(t))))}async#d(t,o){if(this.#p)throw new Error(`${this.#e.label} controller is closed`);if(await this.#h(t.botId),this.#p)throw new Error(`${this.#e.label} controller is closed`);let i=await this.#c({botId:t.botId,config:t,token:o});if(!i||typeof i.start!="function"||typeof i.stop!="function")throw new TypeError(`createRuntime returned an invalid ${this.#e.label} runtime`);this.#o.set(t.botId,i);try{await i.start()}catch(e){throw await i.stop().catch(()=>{}),this.#o.delete(t.botId),e}}async#h(t){let o=this.#o.get(t);this.#o.delete(t),await o?.stop().catch(i=>{this.#I.warn?.(`[dsh-im:${this.#e.key}] bot ${t} failed to stop cleanly:`,i)})}async#E(t){let o=await this.#A.resolve(t).catch(()=>{});return iE(o?.value)}async#u(t,o){o?.value?await this.#A.set(t,o.value).catch(()=>{}):await this.#A.unset(t).catch(()=>{})}#B(t,o){let a=(this.#g.get(t)??Promise.resolve()).catch(()=>{}).then(o).finally(()=>{this.#g.get(t)===a&&this.#g.delete(t)});return this.#g.set(t,a),a}#f(){this.#l+=1}};import{createHash as $pe}from"node:crypto";var ede="https://discord.com/api/v10/",Ade=12e4,tde=new Set([50001,50013]),rde=new Set([40005]);function vn(n){return typeof n=="string"&&n.trim()?n.trim():null}function Ix(n,t){let o=AbortSignal.timeout(t);return n?AbortSignal.any([n,o]):o}function CM(n){return n?.reason instanceof Error?n.reason:new DOMException("The operation was aborted","AbortError")}function ade(n,t){if(!Number.isInteger(n)||n<1)throw new TypeError(`${t} must be a positive integer`);return n}function px(n,t){return t?.providerCode!==void 0&&(n.providerCode=t.providerCode),t?.retry_after!==void 0&&(n.retry_after=t.retry_after,n.retryAfter=t.retry_after),Number.isInteger(t?.status)&&(n.status=t.status),n}function ide(n){let t=Number(n?.providerCode),o=Number(n?.status),i=vn(n?.message)??"",e="artifact-provider-rejected",a="Discord rejected the attachment.";o===401||o===403||tde.has(t)?(e="artifact-permission-required",a="Discord denied permission to send the attachment."):o===413||rde.has(t)||/(?:request|attachment|file).{0,24}too large/i.test(i)?(e="artifact-too-large",a="The attachment exceeds Discord's size limit."):o===429?(e="artifact-rate-limited",a="Discord rate-limited attachment delivery."):o>=500&&(e="artifact-delivery-uncertain",a="Discord attachment delivery result is uncertain.");let A=new Error(a,{cause:n});return A.code=e,px(A,n)}function ode(n){let t=new Error("Discord attachment delivery result is uncertain",{cause:n});return t.code="artifact-delivery-uncertain",px(t,n)}function nde(n,t){return new Promise((o,i)=>{if(t?.aborted){i(t.reason??new DOMException("Aborted","AbortError"));return}let e=setTimeout(o,n);e?.unref?.(),t?.addEventListener("abort",()=>{clearTimeout(e),i(t.reason??new DOMException("Aborted","AbortError"))},{once:!0})})}function Ga(n,t){let o=vn(n);if(!o||!/^\d{5,30}$/.test(o))throw new TypeError(`Invalid Discord ${t}`);return o}function lx(n){let t=vn(n);if(!t)throw new TypeError("A Discord reaction emoji is required");return t}function sde(n){return typeof n=="string"&&/^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{20,}$/.test(n.trim())}var nE=class{#e;#A;#t;#r;constructor({token:t,fetchImpl:o=fetch,baseUrl:i=ede,fileUploadTimeoutMs:e=Ade}){if(!sde(t))throw new TypeError("Discord Bot Token is invalid");if(typeof o!="function")throw new TypeError("DiscordApi requires fetch");this.#e=t.trim(),this.#A=o,this.#t=new URL(i),this.#r=ade(e,"fileUploadTimeoutMs")}getCurrentUser(t={}){return this.#a("users/@me",{...t,method:"GET"})}getGatewayBot(t={}){return this.#a("gateway/bot",{...t,method:"GET"})}getChannel({channelId:t,signal:o}={}){return this.#a(`channels/${Ga(t,"channel id")}`,{method:"GET",signal:o})}startThreadFromMessage({channelId:t,messageId:o,name:i,signal:e}={}){let a=vn(i);if(!a||[...a].length>100)throw new TypeError("Discord thread name must contain 1-100 characters");if(e?.aborted)throw CM(e);return this.#a(`channels/${Ga(t,"channel id")}/messages/${Ga(o,"message id")}/threads`,{method:"POST",signal:e,body:{name:a}})}createMessage({channelId:t,content:o,replyToMessageId:i,signal:e}){return this.#a(`channels/${Ga(t,"channel id")}/messages`,{method:"POST",signal:e,body:{content:o,allowed_mentions:{parse:[],replied_user:!1},...i?{message_reference:{message_id:Ga(i,"message id"),channel_id:Ga(t,"channel id"),fail_if_not_exists:!1}}:{}}})}async createFileMessage({channelId:t,file:o,replyToMessageId:i,signal:e}){if(!o||typeof o!="object"||typeof o.fileName!="string"||!o.fileName||!Buffer.isBuffer(o.bytes))throw new TypeError("A Discord attachment is required");let a=vn(o.deliveryKey)??vn(o.artifactId),A=a?$pe("sha256").update(a).digest("hex").slice(0,25):void 0,r=new FormData;r.append("payload_json",JSON.stringify({allowed_mentions:{parse:[],replied_user:!1},attachments:[{id:0,filename:o.fileName}],...A?{nonce:A,enforce_nonce:!0}:{},...i?{message_reference:{message_id:Ga(i,"message id"),channel_id:Ga(t,"channel id"),fail_if_not_exists:!1}}:{}})),r.append("files[0]",new Blob([o.bytes],{type:o.mediaType??"application/octet-stream"}),o.fileName);let s=Ga(t,"channel id");if(e?.aborted)throw CM(e);let g=Ix(e,this.#r);try{return await this.#a(`channels/${s}/messages`,{method:"POST",signal:g,timeoutMs:this.#r,body:r,multipart:!0})}catch(c){throw e?.aborted?CM(e):c?.code?.startsWith?.("discord-")?ide(c):ode(c)}}editMessage({channelId:t,messageId:o,content:i,signal:e}){return this.#a(`channels/${Ga(t,"channel id")}/messages/${Ga(o,"message id")}`,{method:"PATCH",signal:e,body:{content:i,allowed_mentions:{parse:[],replied_user:!1}}})}sendTyping({channelId:t,signal:o}){return this.#a(`channels/${Ga(t,"channel id")}/typing`,{method:"POST",signal:o,expectBody:!1})}async addOwnReaction({channelId:t,messageId:o,emoji:i,signal:e}={}){let a=lx(i);return await this.#a(`channels/${Ga(t,"channel id")}/messages/${Ga(o,"message id")}/reactions/${encodeURIComponent(a)}/@me`,{method:"PUT",signal:e,expectBody:!1,retry:!1}),a}removeOwnReaction({channelId:t,messageId:o,emoji:i,signal:e}={}){let a=lx(i);return this.#a(`channels/${Ga(t,"channel id")}/messages/${Ga(o,"message id")}/reactions/${encodeURIComponent(a)}/@me`,{method:"DELETE",signal:e,expectBody:!1,retry:!1})}async#a(t,{method:o,body:i,signal:e,timeoutMs:a=15e3,expectBody:A=!0,retry:r=!0,multipart:s=!1}){let g;try{g=await this.#A(new URL(t,this.#t),{method:o,headers:{authorization:`Bot ${this.#e}`,...s?{}:{"content-type":"application/json"},"user-agent":"DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 1.1.0)"},...i===void 0?{}:{body:s?i:JSON.stringify(i)},signal:Ix(e,a),redirect:"error"})}catch(I){throw I?.name==="AbortError"||I?.name==="TimeoutError"?I:new Error(`Discord ${o} transport failed`)}let c=null;if(A||g.status===429||!g.ok)try{c=await g.json()}catch{if(A){let I=new Error(`Discord ${o} returned invalid JSON`);throw I.status=g?.status,I}}if(g.status===429&&r){let I=Math.min(1e4,Math.max(50,Number(c?.retry_after)*1e3||1e3));return await nde(I,e),this.#a(t,{method:o,body:i,signal:e,timeoutMs:a,expectBody:A,retry:!1,multipart:s})}if(!g.ok){let I=new Error(vn(c?.message)??`Discord API failed with HTTP ${g.status}`);I.code=`discord-${g.status}`,I.status=g.status,(Number.isInteger(c?.code)||typeof c?.code=="string")&&(I.providerCode=c.code);let d=Number(c?.retry_after);throw Number.isFinite(d)&&d>=0&&(I.retry_after=d,I.retryAfter=d),I}return A?c:null}};async function dx(n,t={}){let i=await new nE({token:n,...t}).getCurrentUser();if(!i?.id||i?.bot!==!0)throw new Error("Discord token does not belong to a bot");return{platformId:String(i.id),name:vn(i.global_name)??vn(i.username)??D("Discord\u673A\u5668\u4EBA"),username:vn(i.username)}}var gde=Object.freeze({success(){},error(){},clear(){},settled:()=>Promise.resolve()});function hx(n,t){!n||typeof n!="object"||(n[t]=(n[t]??0)+1)}async function cde(n,t){let o=AbortSignal.timeout(t),i,e=new Promise((a,A)=>{i=()=>A(o.reason??new DOMException("Timed out","TimeoutError")),o.addEventListener("abort",i,{once:!0})});try{return await Promise.race([n(o),e])}finally{o.removeEventListener("abort",i)}}function Nf({adapter:n,target:t,reactions:o,status:i,logger:e=console,label:a="channel",timeoutMs:A=2e3}={}){if(!t||typeof n?.addReaction!="function"||typeof n?.removeReaction!="function"||typeof o?.processing!="string"||!o.processing||!Number.isSafeInteger(A)||A<=0)return gde;let r=null,s=!1,g=async(B,f)=>{try{let Q=await cde(f,A);return hx(i,B==="add"?"reactionsAdded":"reactionsRemoved"),{ok:!0,value:Q}}catch(Q){return hx(i,"reactionErrors"),e.warn?.(`[dsh-im:${a}] status reaction ${B} failed:`,Q?.message??Q?.name??String(Q)),{ok:!1,value:null}}},c=async B=>{if(r!==null){let Q=r;r=null,await g("remove",y=>n.removeReaction(t,Q,{signal:y}))}if(typeof B!="string"||!B)return;let f=await g("add",Q=>n.addReaction(t,B,{signal:Q}));f.ok&&f.value!==void 0&&f.value!==null&&(r=f.value)},I=c(o.processing),d=B=>{s||(s=!0,I=I.then(()=>c(B),()=>c(B)),I.catch(()=>{}))};return Object.freeze({success:()=>d(o.success),error:()=>d(o.error),clear:()=>d(null),settled:()=>I})}var Ex="\u8FD9\u4E2A\u95EE\u9898\u5DF2\u5728\u5176\u4ED6\u5BA2\u6237\u7AEF\u5904\u7406\uFF0C\u65E0\u9700\u518D\u6B21\u56DE\u7B54\u3002",Ide="\u4EFB\u52A1\u5DF2\u5B8C\u6210\u3002";function Ci(n){return typeof n=="string"?n.trim():""}function lde(n,t,o){return t.actor===o&&(n.kind!=="group"||n.addressed===!0)&&!qt(n)&&!Gt(n)&&!!Ci(n.content)}function pde(n,t,o){let i=String(n??D("\u7ED3\u679C\u6587\u4EF6")).replace(/[\r\n]+/g," ").trim().slice(0,255)||D("\u7ED3\u679C\u6587\u4EF6");switch(t?.code){case"artifact-delivery-uncertain":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u7684\u53D1\u9001\u7ED3\u679C\u672A\u80FD\u786E\u8BA4\uFF0C\u8BF7\u5148\u68C0\u67E5\u804A\u5929\u5185\u662F\u5426\u5DF2\u6536\u5230\uFF0C\u4E0D\u8981\u7ACB\u5373\u91CD\u8BD5\u3002",{name:i});case"artifact-permission-required":return o?.key==="slack"?D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46 Slack \u5E94\u7528\u7F3A\u5C11 files:write \u6743\u9650\u3002\u8BF7\u66F4\u65B0 Manifest\u3001\u91CD\u65B0\u5B89\u88C5\u5E94\u7528\u5E76\u91CD\u65B0\u8FDE\u63A5\u673A\u5668\u4EBA\u540E\u91CD\u8BD5\u3002",{name:i}):o?.key==="discord"?D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46\u673A\u5668\u4EBA\u7F3A\u5C11 Discord \u7684 Send Messages\u3001Attach Files \u6216 Read Message History \u6743\u9650\u3002",{name:i}):o?.key==="telegram"?D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46 Telegram \u4E0D\u5141\u8BB8\u673A\u5668\u4EBA\u5728\u5F53\u524D\u804A\u5929\u53D1\u9001\u6587\u6863\uFF0C\u8BF7\u68C0\u67E5\u804A\u5929\u6743\u9650\u3002",{name:i}):D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46\u5F53\u524D\u673A\u5668\u4EBA\u6CA1\u6709\u6587\u4EF6\u53D1\u9001\u6743\u9650\uFF0C\u8BF7\u68C0\u67E5\u6E20\u9053\u6743\u9650\u3002",{name:i});case"artifact-too-large":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u8D85\u8FC7\u5F53\u524D\u6E20\u9053\u5927\u5C0F\u4E0A\u9650\uFF0C\u672A\u53D1\u9001\u3002",{name:i});case"artifact-empty":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u4E3A\u7A7A\uFF0C\u672A\u53D1\u9001\u3002",{name:i});case"artifact-invalid":case"artifact-changed":case"artifact-unavailable":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u6216\u51C6\u5907\u53D1\u9001\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u4ECD\u53EF\u8BBF\u95EE\u540E\u91CD\u8BD5\u3002",{name:i});case"artifact-rate-limited":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u6682\u65F6\u88AB\u5F53\u524D\u6E20\u9053\u9650\u6D41\uFF0C\u672A\u80FD\u53D1\u9001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{name:i});case"artifact-provider-rejected":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46\u5F53\u524D\u6E20\u9053\u62D2\u7EDD\u4E86\u8BE5\u6587\u4EF6\u6216\u6587\u4EF6\u6D88\u606F\u3002",{name:i});default:return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46\u5F53\u524D\u6E20\u9053\u6682\u65F6\u672A\u80FD\u53D1\u9001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{name:i})}}function Ui(){return{messagesReceived:0,messagesReplied:0,messagesRejected:0,lastMessageAt:null,lastReplyAt:null,lastRejectedAt:null,lastError:null,lastMessageError:null,reactionsAdded:0,reactionsRemoved:0,reactionErrors:0}}var Pn=class{#e;#A;#t;#r;#a;#i;#c;#s;#I=new Map;#o=new Map;#n=new Map;#g=new Set;#l=new Set;#p=new Set;#d;#h=new Ii;constructor({descriptor:t,bot:o,harness:i,state:e,status:a=Ui(),logger:A=console,replyTimeoutMs:r=6e5,signal:s}){if(!t?.key||!t?.label)throw new TypeError("A channel descriptor is required");if(!o||typeof o.sendText!="function")throw new TypeError("A bot client is required");if(!i||!e)throw new TypeError("Harness client and state store are required");this.#e=t,this.#A=o,this.#t=i,this.#r=e,this.#a=a,this.#i=A,this.#c=r,this.#s=s,this.#d=new ii({label:t.key,logger:A})}get status(){return structuredClone(this.#a)}accept(t){if(this.#s?.aborted)return Promise.resolve();let o=Ci(t?.conversationId),i=t?.kind==="group"?"group":"direct",e={...t,kind:i,conversationId:o},a=Ci(e.messageId),A=Ci(e.senderId);if(!a||!A||!o||e.senderIsBot===!0||this.#r.hasSeen(a)||this.#g.has(a))return Promise.resolve();this.#g.add(a);let r=Nf({adapter:this.#A,target:e.kind==="direct"||e.addressed===!0?e.reactionTarget:null,reactions:this.#e.reactions,status:this.#a,logger:this.#i,label:this.#e.key});e.statusReaction=r;let s=this.#E(e,a,A);return s.then(()=>r.success(),()=>r.error()),s}#E(t,o,i){t.kind==="direct"&&io(this.#r,t.connectionTestTarget??t.replyTarget);let e=`${t.kind}:${t.conversationId}`,a=this.#o.get(e),A=Ci(t.content),r=lo(A);if(r&&t.kind==="group"&&t.addressed===!0)return this.#u(t,o,Tn());if(r&&t.kind==="direct"){if(/^\/batch$/iu.test(A)&&(this.#I.has(e)||a||this.#d.hasPending(e)))return this.#u(t,o,po());let d=this.#h.handle(e,A,{plainText:!!A&&t.plainText!==!1&&!qt(t)&&!Gt(t)});if(d.handled)return d.kind==="submit"?this.#B({...t,content:d.prompt,batchSubmission:{token:d.token}},o,i,e):this.#u(t,o,d.message)}else if(t.kind==="direct"&&this.#h.status(e).phase==="collecting"){let I=this.#h.handle(e,A,{plainText:!!A&&t.plainText!==!1&&!qt(t)&&!Gt(t)});if(I.handled)return this.#u(t,o,I.message)}let g=t.kind==="direct"&&this.#h.status(e).phase==="collecting"||Gt(t)?null:Pi(A)?gi:Gi(A)?Fi:Li(A)?ci:null;if(g&&(t.kind!=="group"||t.addressed===!0)){let I;return I=this.#f(t,o,e,g).finally(()=>{this.#g.delete(o),this.#p.delete(I)}),this.#p.add(I),I}let c=this.#d.claimReply({key:e,actor:i,messageId:o,text:qt(t)||Gt(t)?"":t.content,addressed:t.kind!=="group"||t.addressed===!0,hasPendingQuestion:!!a,questionCompletion:a?.submitting||a?.claimedReplyMessageId?a.queue:null,isQuestionPending:()=>this.#o.has(e),send:I=>this.#A.sendText(t.replyTarget,I)});if(c){let I;return I=c.process(async()=>this.#r.hasSeen(o)?!1:(await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString(),!0)).finally(()=>{this.#g.delete(o),this.#l.delete(I)}),this.#l.add(I),I}if(a&&a.actor!==i)return this.#B(t,o,i,e);if(a?.submitting||a?.claimedReplyMessageId)return this.#B(t,o,i,e);if(a){lde(t,a,i)&&(a.claimedReplyMessageId=o);let d=(a.queue??Promise.resolve()).catch(()=>{}).then(()=>this.#Q(t,o,i,e,a)).finally(()=>{this.#g.delete(o),a.claimedReplyMessageId===o&&(a.claimedReplyMessageId=null),a.queue===d&&(a.queue=null)});return a.queue=d,d}return this.#B(t,o,i,e)}#u(t,o,i){let e;return e=(async()=>{this.#r.hasSeen(o)||(await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString(),i&&await this.#A.sendText(t.replyTarget,i),this.#a.lastError=null)})().catch(async a=>{if(this.#s?.aborted){t.statusReaction?.clear();return}t.statusReaction?.error(),this.#a.lastError=a?.message??String(a);let A=At(this.#a,a);this.#i.error?.(`[dsh-im:${this.#e.key}] failed to process a batch input message [${A.referenceId}]:`,a)}).finally(()=>{this.#g.delete(o),this.#p.delete(e)}),this.#p.add(e),e}#B(t,o,i,e,{releaseMessageId:a=!0,alreadyRecorded:A=!1}={}){let s=(this.#I.get(e)??Promise.resolve()).catch(()=>{}).then(()=>this.#y(t,o,i,e,{alreadyRecorded:A})).finally(()=>{a&&this.#g.delete(o),this.#I.get(e)===s&&this.#I.delete(e)});return this.#I.set(e,s),s}async waitForIdle(){await Promise.allSettled([...this.#I.values(),...[...this.#o.values()].flatMap(t=>t.queue?[t.queue]:[]),...this.#l,...this.#p])}async#f(t,o,i,e){if(this.#r.hasSeen(o))return;await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString();let a=t.replyTarget;try{let A=await e(Ci(t.content),this.#t,this.#r,i,{signal:this.#s,hasImages:qt(t),hasFiles:Gt(t),pendingInteraction:this.#o.has(i)||this.#d.hasPending(i),control:{owner:this,key:i}});A?.stopped&&await Promise.allSettled([this.#k(i),this.#d.closeRoute(i)]);for(let r of A?.messages??[A?.message])r&&await this.#A.sendText(a,r);this.#a.lastError=null}catch(A){if(A?.code==="turn-stopped"||this.#s?.aborted){t.statusReaction?.clear();return}t.statusReaction?.error(),this.#a.lastError=A?.message??String(A);let r=At(this.#a,A);this.#i.error?.(`[dsh-im:${this.#e.key}] failed to process a command [${r.referenceId}]:`,A),await this.#A.sendText(a,dt(r)).catch(()=>{})}}sendConnectionTest(t){return Tg({state:this.#r,text:t,channelLabel:D("{label}\u673A\u5668\u4EBA",{label:this.#e.label}),send:(o,i)=>this.#A.sendText(o,i)})}async#m(t,o,i=[],e){let a=await Co({artifacts:i,baseReceipt:e,deliveryId:o,channelKey:this.#e.key,signal:this.#s,sendImage:typeof this.#A.sendImage=="function"?A=>this.#A.sendImage(t,A):void 0,sendFile:typeof this.#A.sendFile=="function"?A=>this.#A.sendFile(t,A):void 0,onFailure:(A,r)=>At(this.#a,r,{userMessage:pde(A?.fileName,r,this.#e),reason:r?.code}),sendFailureNotice:(A,r,s)=>this.#A.sendText(t,dt(s)),logger:this.#i});return this.#a.artifactsSent=(this.#a.artifactsSent??0)+a.artifactsSent,this.#a.artifactSendErrors=(this.#a.artifactSendErrors??0)+a.artifactSendErrors,{receipt:a.receipt,userVisible:a.userVisible,artifactSendErrors:a.artifactSendErrors}}async#y(t,o,i,e,{alreadyRecorded:a=!1}={}){if(!a){if(this.#r.hasSeen(o))return;await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString()}let A=t.replyTarget,r=Ci(t.content),s=t.batchSubmission,g=null,c=!1;try{if(this.#s?.throwIfAborted(),t.kind==="group"&&t.addressed!==!0){this.#a.messagesRejected+=1,this.#a.lastRejectedAt=new Date().toISOString();return}let I=qt(t),d=Gt(t);if(!r&&!I&&!d){await this.#A.sendText(A,D("\u76EE\u524D\u652F\u6301\u6587\u5B57\u3001\u56FE\u7247\u548C\u6587\u4EF6\u6D88\u606F\u3002"));return}let B=r.toLowerCase();if(!I&&!d&&B==="/help"){await this.#A.sendText(A,[D("{label}\u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5 DeepSeek Harness\u3002",{label:this.#e.label}),"",D("\u76F4\u63A5\u53D1\u9001\u6587\u5B57\u3001\u56FE\u7247\u6216\u6587\u4EF6\u5373\u53EF\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u3002"),D("/new \u5F00\u542F\u4E00\u4E2A\u5168\u65B0\u4F1A\u8BDD"),D("/compact \u538B\u7F29\u5F53\u524D\u4F1A\u8BDD\u7684\u8F83\u65E9\u4E0A\u4E0B\u6587"),D("/workspace \u5DE5\u4F5C\u533A\u7EDD\u5BF9\u8DEF\u5F84 \u5207\u6362\u5DE5\u4F5C\u533A"),D("/workspacelist \u5217\u51FA\u5DE5\u4F5C\u533A\u7EDD\u5BF9\u8DEF\u5F84"),D("/sessionlist [\u5DE5\u4F5C\u533A\u5E8F\u53F7\u6216\u7EDD\u5BF9\u8DEF\u5F84] \u5217\u51FA\u4F1A\u8BDD ID \u548C\u6807\u9898"),D("/session Session ID \u6216\u5F53\u524D\u5DE5\u4F5C\u533A\u5E8F\u53F7 \u5C06\u5F53\u524D\u804A\u5929\u7ED1\u5B9A\u5230\u6307\u5B9A\u4F1A\u8BDD"),D("/models \u6309\u5E8F\u53F7\u5217\u51FA\u6240\u6709\u53EF\u7528\u6A21\u578B"),D("/reasoninglist \u6216 /reasonings \u6309\u5E8F\u53F7\u5217\u51FA\u5F53\u524D\u6A21\u578B\u53EF\u7528\u63A8\u7406\u7B49\u7EA7"),D("/reasoning [\u5E8F\u53F7\u3001\u7B49\u7EA7ID\u6216 --default] \u67E5\u770B\u6216\u5207\u6362\u5F53\u524D\u63A8\u7406\u7B49\u7EA7"),D("/model [\u5E8F\u53F7\u6216\u5B8C\u6574\u6A21\u578BID] [\u63A8\u7406\u7B49\u7EA7ID] \u67E5\u770B\u6216\u5207\u6362\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B"),D("\u793A\u4F8B\uFF1A\u5148\u53D1 /models\uFF0C\u518D\u53D1 /model 2 [\u63A8\u7406\u7B49\u7EA7ID]"),D("/presetlist \u6309\u5E8F\u53F7\u5217\u51FA\u53EF\u7528 Agent Preset"),D("/preset [\u5E8F\u53F7\u6216\u5B8C\u6574ID] \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D\u673A\u5668\u4EBA Agent Preset"),D("\u7EAF\u6570\u5B57 ID\uFF1A/preset id:"),D("/preset --default \u8DDF\u968F Host \u9ED8\u8BA4"),D("/stop \u505C\u6B62\u5F53\u524D\u4EFB\u52A1"),D("/steer \u8865\u5145\u6307\u4EE4 \u7EA0\u504F\u5F53\u524D\u4EFB\u52A1"),D("/batch \u5F00\u59CB\u6279\u91CF\u8F93\u5165\uFF08\u4EC5\u79C1\u804A\uFF0C\u6700\u591A 10 \u6761\u6587\u5B57\uFF09"),D("/send \u63D0\u4EA4\u5F53\u524D\u6279\u6B21"),D("/cancel \u53D6\u6D88\u5F53\u524D\u6279\u6B21"),D("/status \u68C0\u67E5\u8FDE\u63A5\u72B6\u6001"),D("/version \u67E5\u770B\u63D2\u4EF6\u7248\u672C"),D("/help \u663E\u793A\u672C\u5E2E\u52A9")].join(` `));return}if(!I&&!d&&B==="/status"){await this.#t.ensureRunning({signal:this.#s}),await this.#A.sendText(A,D("{label}\u673A\u5668\u4EBA\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002",{label:this.#e.label}));return}let f=!I&&!d?await Io(r,this.#t,e):null;if(f){for(let R of f.messages??[f.message])await this.#A.sendText(A,R);return}if(!I&&!d&&B==="/new"){await this.#r.clearSession(e),await this.#A.sendText(A,D("\u5DF2\u5F00\u542F\u65B0\u4F1A\u8BDD\u3002\u8BF7\u53D1\u9001\u4F60\u7684\u95EE\u9898\u3002"));return}let Q=!I&&!d?await si(r,this.#t,this.#r,e,{signal:this.#s}):null;if(Q){await this.#A.sendText(A,Q.message);return}await this.#A.sendTyping?.(A).catch(R=>{this.#i.warn?.(`[dsh-im:${this.#e.key}] typing indicator failed:`,R)});let y=!1;if(typeof this.#A.openDeliveryStream=="function")try{g=await this.#A.openDeliveryStream(A),c=!0}catch(R){this.#i.warn?.(`[dsh-im:${this.#e.key}] unable to start a semantic reply stream; using final delivery:`,R)}else if(typeof this.#A.openStream=="function")try{g=await this.#A.openStream(A)}catch(R){this.#i.warn?.(`[dsh-im:${this.#e.key}] unable to start a streamed reply; using text:`,R)}let m=I?await so(t,{signal:this.#s}):void 0,{answer:_,artifacts:N=[]}=await ni({harness:this.#t,state:this.#r,key:e,text:r,content:m,createOptions:this.#s?{signal:this.#s}:void 0,existsOptions:this.#s?{signal:this.#s}:void 0,askOptions:{timeoutMs:this.#c,signal:this.#s,control:{owner:this,key:e},onUpdate:g?async R=>{let V=R.type==="text"?R.text:R.type==="tool"?D("\u6B63\u5728\u4F7F\u7528{name}\u2026",{name:R.name}):R.text;if(V){let L=R.type==="text"?"markdown":"plain";await g.update(c?Rn(V,L):V)}}:void 0,onInteraction:R=>this.#C(R,{key:e,actor:i,target:A,requiresMention:t.kind==="group"&&t.requiresMention!==!1}),onInteractionResolved:R=>this.#b(R),files:t.files}});s&&this.#h.complete(e,s.token);let G=!Ci(_)&&N.length>0,Y=G?D(Ide):_,j=G?"plain":"markdown",H=null,J=null;if(g)try{let R=await g.finish(c?Rn(Y,j):Y);y=!0,J=zt({deliveryId:o,presentation:R?.presentation??g.presentation??`${this.#e.key}-stream`,providerMessageIds:[...li(g),...li(R)],deliveryOutcome:R?.deliveryOutcome,reason:R?.reason})}catch(R){g.cancel?.(),this.#i.warn?.(`[dsh-im:${this.#e.key}] streamed reply finalization failed; using text:`,R)}if(!y)try{let R=typeof this.#A.sendDelivery=="function"?await this.#A.sendDelivery(A,Rn(Y,j)):await this.#A.sendText(A,Y);J=zt({deliveryId:o,presentation:R?.presentation??`${this.#e.key}-text`,providerMessageIds:li(R),deliveryOutcome:R?.deliveryOutcome,reason:R?.reason})}catch(R){H=Sa(R)}let q=J?.deliveryOutcome==="unknown";J?.deliveryOutcome==="failed"?H=Sa(new Error(`Final text delivery failed (${J.reason??"unknown"})`),{uncertain:!1}):q&&(H=Sa(new Error(`Final text delivery outcome is unknown (${J.reason??"unknown"})`)));let U=await this.#m(A,o,N,J);if(H&&(!U.userVisible||q))throw H.deliveryReceipt=U.receipt,H;return H&&U.artifactSendErrors===0&&At(this.#a,H),U.userVisible&&(this.#a.messagesReplied+=1,this.#a.lastReplyAt=new Date().toISOString(),this.#a.lastError=null,!H&&U.artifactSendErrors===0&&oo(this.#a)),U.receipt}catch(I){let d=I?.code==="turn-stopped";s&&d&&this.#h.complete(e,s.token);let B=s&&!d?this.#h.fail(e,s.token):null;if(d){if(t.statusReaction?.clear(),g)try{await g.finish(D("\u5DF2\u505C\u6B62\u3002"))}catch{g.cancel?.()}return}if(this.#s?.aborted){t.statusReaction?.clear(),g?.cancel?.();return}t.statusReaction?.error(),this.#a.lastError=I?.message??String(I);let f=async N=>{let G=typeof g?.fail=="function"?"fail":typeof g?.finish=="function"?"finish":null;if(!G)return!1;try{let Y=await g[G](N);return G==="fail"?!!Y&&Y.deliveryOutcome!=="failed":Y!==!1&&Y?.deliveryOutcome!=="failed"}catch(Y){return this.#i.warn?.(`[dsh-im:${this.#e.key}] unable to finalize the failed stream:`,Y),!1}},Q=go(I),y=Eo(I),m=At(this.#a,I,{userMessage:y??Q,reason:vi(I)?.reason}),_=B?.retained?`${dt(m)} -${B.message}`:dt(m);if(this.#i.error?.(`[dsh-im:${this.#e.key}] failed to process a message [${m.referenceId}]:`,I),await f(_))return I.deliveryReceipt;g?.cancel?.();try{await this.#A.sendText(A,_)}catch(N){this.#i.error?.(`[dsh-im:${this.#e.key}] failed to send the safe error reply:`,N)}return I.deliveryReceipt}finally{await Promise.allSettled([this.#k(e),this.#d.closeRoute(e)])}}async#Q(t,o,i,e,a){if(this.#s?.aborted){t.statusReaction?.clear();return}let A=this.#o.get(e),r=a.claimedReplyMessageId===o;if(!A||A!==a||A.submitting)return r&&(!A||A!==a)?this.#w(t,o):this.#B(t,o,i,e,{releaseMessageId:!1});if(this.#r.hasSeen(o))return;if(await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString(),t.kind==="group"&&t.addressed!==!0){this.#a.messagesRejected+=1,this.#a.lastRejectedAt=new Date().toISOString();return}let s=t.replyTarget,g=Ci(t.content);if(!g||qt(t)||Gt(t)){try{await this.#A.sendText(s,D("\u8BF7\u7528\u6587\u5B57\u56DE\u7B54\u5F53\u524D\u95EE\u9898\u3002"))}catch(d){this.#i.error?.(`[dsh-im:${this.#e.key}] failed to reject a non-text interaction reply:`,d)}return}let c=this.#o.get(e);if(!c||c!==a||c.submitting)return r&&(!c||c!==a)?this.#w(t,o,{alreadyRecorded:!0}):this.#B(t,o,i,e,{releaseMessageId:!1,alreadyRecorded:!0});if(c.target=s,c.needsPresentation){let d=c.presentationTask!==null;try{await this.#M(c)}catch(f){this.#a.lastError=D("{label}\u4EA4\u4E92\u95EE\u9898\u53D1\u9001\u5931\u8D25\u3002",{label:this.#e.label}),this.#i.error?.(`[dsh-im:${this.#e.key}] failed to retry an interaction question:`,f),c.interaction.reconnect?.();return}let B=this.#o.get(e);if(!B||B!==a||B.submitting)return r&&(!B||B!==a)?this.#w(t,o,{alreadyRecorded:!0}):this.#B(t,o,i,e,{releaseMessageId:!1,alreadyRecorded:!0});if(!d)return}let I=c.questions[c.index];if(I){if(c.answers.push(ai(I,g)),c.index+=1,c.indexthis.#A.sendText(e,c)});if(t?.kind!=="question")return;let A=t?.payload?.questions,r=Ci(t?.interactionId)||Ci(t?.rpcId);if(!Ci(t?.rpcId)||!r||!Ci(t?.sessionId)||!Array.isArray(A)||A.length===0||A.some(c=>!ti(c))){this.#i.warn?.(`[dsh-im:${this.#e.key}] ignored an invalid Harness question interaction`);return}if(t.recovered===!0){await this.#R(t,`${this.#e.label} safely cancelled an interaction left by an earlier client.`);try{await this.#A.sendText(e,D("\u68C0\u6D4B\u5230\u8FD9\u4E2A Session \u4E2D\u9057\u7559\u7684\u5F85\u56DE\u7B54\u95EE\u9898\uFF0C\u5DF2\u5B89\u5168\u53D6\u6D88\u5E76\u7EE7\u7EED\u5904\u7406\u4F60\u521A\u624D\u7684\u6D88\u606F\u3002"))}catch(c){this.#i.error?.(`[dsh-im:${this.#e.key}] failed to send an interaction recovery notice:`,c)}return}let s=this.#o.get(o);if(s?.interactionId===r){s.interaction=t,s.needsPresentation&&await this.#M(s);return}if(this.#n.has(r))return;if(s){this.#i.warn?.(`[dsh-im:${this.#e.key}] cancelled a second pending Harness question`),await this.#R(t,`${this.#e.label} is already handling another user interaction.`);return}let g={kind:"question",interactionId:r,sessionId:t.sessionId,interaction:t,actor:i,requiresMention:a,questions:A,answers:[],index:0,target:e,queue:null,claimedReplyMessageId:null,submitting:!1,needsPresentation:!0,presentationTask:null};this.#o.set(o,g),this.#n.set(r,o),await this.#M(g)}async#b(t){if(t?.kind==="approval"){await this.#d.handleResolved(t);return}let o=Ci(t?.interactionId);if(t?.kind!=="question"||!o)return;let i=this.#n.get(o);i&&this.#_(i,o)}#M(t){if(t.presentationTask)return t.presentationTask;let o=t.questions[t.index];if(!o)return Promise.resolve();let i=(async()=>{await this.#A.sendText(t.target,ri(o,t.index,t.questions.length,{requiresMention:t.requiresMention})),t.needsPresentation=!1})();return t.presentationTask=i,i.then(()=>{t.presentationTask===i&&(t.presentationTask=null)},()=>{t.presentationTask===i&&(t.presentationTask=null)}),i}async#w(t,o,{alreadyRecorded:i=!1}={}){if(!i){if(this.#r.hasSeen(o))return;await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString()}try{await this.#A.sendText(t.replyTarget,D(Ex))}catch(e){this.#i.error?.(`[dsh-im:${this.#e.key}] failed to send an expired interaction notice:`,e)}}#S(t,o){let i=this.#o.get(t);return!i||o!==void 0&&i.interactionId!==o?null:(this.#o.delete(t),this.#n.delete(i.interactionId),i)}#_(t,o){return this.#S(t,o)!==null}async#R(t,o){try{await t.respond({ok:!1,error:{code:"cancelled",message:o,details:{}}},{signal:AbortSignal.timeout(5e3)})}catch(i){if(i?.code!=="interaction-not-pending")throw i}}async#k(t){let o=this.#S(t);if(!(!o||o.kind!=="question"))try{await this.#R(o.interaction,`The ${this.#e.label} interaction ended before the user answered.`)}catch(i){this.#i.warn?.(`[dsh-im:${this.#e.key}] failed to cancel a pending Harness interaction:`,i)}}};var uM=Object.freeze({key:"discord",label:"Discord",connectionLabel:" Gateway \u957F\u8FDE\u63A5",reactions:Object.freeze({processing:"\u{1F440}",success:"\u2705",error:"\u274C"})}),Rf=class extends Pn{constructor(t){super({descriptor:uM,...t})}};var sE=class extends Wl{constructor(t){super({...t,descriptor:uM,inspectToken:t.inspectToken??dx,deriveIdentity:gx,maskPlatformId:cx})}};var kf=class extends lr{constructor(t){super({...t,rpcIdPrefix:"discord",logPrefix:"dsh-discord"})}};function Gn(n,t){let o=typeof n=="string"?n.trim():"";if(!o)return[];let i=[],e=o;for(;e.length>t;){let a=e.lastIndexOf(` +${B.message}`:dt(m);if(this.#i.error?.(`[dsh-im:${this.#e.key}] failed to process a message [${m.referenceId}]:`,I),await f(_))return I.deliveryReceipt;g?.cancel?.();try{await this.#A.sendText(A,_)}catch(N){this.#i.error?.(`[dsh-im:${this.#e.key}] failed to send the safe error reply:`,N)}return I.deliveryReceipt}finally{await Promise.allSettled([this.#k(e),this.#d.closeRoute(e)])}}async#Q(t,o,i,e,a){if(this.#s?.aborted){t.statusReaction?.clear();return}let A=this.#o.get(e),r=a.claimedReplyMessageId===o;if(!A||A!==a||A.submitting)return r&&(!A||A!==a)?this.#w(t,o):this.#B(t,o,i,e,{releaseMessageId:!1});if(this.#r.hasSeen(o))return;if(await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString(),t.kind==="group"&&t.addressed!==!0){this.#a.messagesRejected+=1,this.#a.lastRejectedAt=new Date().toISOString();return}let s=t.replyTarget,g=Ci(t.content);if(!g||qt(t)||Gt(t)){try{await this.#A.sendText(s,D("\u8BF7\u7528\u6587\u5B57\u56DE\u7B54\u5F53\u524D\u95EE\u9898\u3002"))}catch(d){this.#i.error?.(`[dsh-im:${this.#e.key}] failed to reject a non-text interaction reply:`,d)}return}let c=this.#o.get(e);if(!c||c!==a||c.submitting)return r&&(!c||c!==a)?this.#w(t,o,{alreadyRecorded:!0}):this.#B(t,o,i,e,{releaseMessageId:!1,alreadyRecorded:!0});if(c.target=s,c.needsPresentation){let d=c.presentationTask!==null;try{await this.#M(c)}catch(f){t.statusReaction?.error(),this.#a.lastError=D("{label}\u4EA4\u4E92\u95EE\u9898\u53D1\u9001\u5931\u8D25\u3002",{label:this.#e.label}),this.#i.error?.(`[dsh-im:${this.#e.key}] failed to retry an interaction question:`,f),c.interaction.reconnect?.();return}let B=this.#o.get(e);if(!B||B!==a||B.submitting)return r&&(!B||B!==a)?this.#w(t,o,{alreadyRecorded:!0}):this.#B(t,o,i,e,{releaseMessageId:!1,alreadyRecorded:!0});if(!d)return}let I=c.questions[c.index];if(I){if(c.answers.push(ai(I,g)),c.index+=1,c.indexthis.#A.sendText(e,c)});if(t?.kind!=="question")return;let A=t?.payload?.questions,r=Ci(t?.interactionId)||Ci(t?.rpcId);if(!Ci(t?.rpcId)||!r||!Ci(t?.sessionId)||!Array.isArray(A)||A.length===0||A.some(c=>!ti(c))){this.#i.warn?.(`[dsh-im:${this.#e.key}] ignored an invalid Harness question interaction`);return}if(t.recovered===!0){await this.#R(t,`${this.#e.label} safely cancelled an interaction left by an earlier client.`);try{await this.#A.sendText(e,D("\u68C0\u6D4B\u5230\u8FD9\u4E2A Session \u4E2D\u9057\u7559\u7684\u5F85\u56DE\u7B54\u95EE\u9898\uFF0C\u5DF2\u5B89\u5168\u53D6\u6D88\u5E76\u7EE7\u7EED\u5904\u7406\u4F60\u521A\u624D\u7684\u6D88\u606F\u3002"))}catch(c){this.#i.error?.(`[dsh-im:${this.#e.key}] failed to send an interaction recovery notice:`,c)}return}let s=this.#o.get(o);if(s?.interactionId===r){s.interaction=t,s.needsPresentation&&await this.#M(s);return}if(this.#n.has(r))return;if(s){this.#i.warn?.(`[dsh-im:${this.#e.key}] cancelled a second pending Harness question`),await this.#R(t,`${this.#e.label} is already handling another user interaction.`);return}let g={kind:"question",interactionId:r,sessionId:t.sessionId,interaction:t,actor:i,requiresMention:a,questions:A,answers:[],index:0,target:e,queue:null,claimedReplyMessageId:null,submitting:!1,needsPresentation:!0,presentationTask:null};this.#o.set(o,g),this.#n.set(r,o),await this.#M(g)}async#b(t){if(t?.kind==="approval"){await this.#d.handleResolved(t);return}let o=Ci(t?.interactionId);if(t?.kind!=="question"||!o)return;let i=this.#n.get(o);i&&this.#_(i,o)}#M(t){if(t.presentationTask)return t.presentationTask;let o=t.questions[t.index];if(!o)return Promise.resolve();let i=(async()=>{await this.#A.sendText(t.target,ri(o,t.index,t.questions.length,{requiresMention:t.requiresMention})),t.needsPresentation=!1})();return t.presentationTask=i,i.then(()=>{t.presentationTask===i&&(t.presentationTask=null)},()=>{t.presentationTask===i&&(t.presentationTask=null)}),i}async#w(t,o,{alreadyRecorded:i=!1}={}){if(!i){if(this.#r.hasSeen(o))return;await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString()}try{await this.#A.sendText(t.replyTarget,D(Ex))}catch(e){this.#i.error?.(`[dsh-im:${this.#e.key}] failed to send an expired interaction notice:`,e)}}#S(t,o){let i=this.#o.get(t);return!i||o!==void 0&&i.interactionId!==o?null:(this.#o.delete(t),this.#n.delete(i.interactionId),i)}#_(t,o){return this.#S(t,o)!==null}async#R(t,o){try{await t.respond({ok:!1,error:{code:"cancelled",message:o,details:{}}},{signal:AbortSignal.timeout(5e3)})}catch(i){if(i?.code!=="interaction-not-pending")throw i}}async#k(t){let o=this.#S(t);if(!(!o||o.kind!=="question"))try{await this.#R(o.interaction,`The ${this.#e.label} interaction ended before the user answered.`)}catch(i){this.#i.warn?.(`[dsh-im:${this.#e.key}] failed to cancel a pending Harness interaction:`,i)}}};var uM=Object.freeze({key:"discord",label:"Discord",connectionLabel:" Gateway \u957F\u8FDE\u63A5",reactions:Object.freeze({processing:"\u{1F440}",success:"\u2705",error:"\u274C"})}),Rf=class extends Pn{constructor(t){super({descriptor:uM,...t})}};var sE=class extends Wl{constructor(t){super({...t,descriptor:uM,inspectToken:t.inspectToken??dx,deriveIdentity:gx,maskPlatformId:cx})}};var kf=class extends lr{constructor(t){super({...t,rpcIdPrefix:"discord",logPrefix:"dsh-discord"})}};function Gn(n,t){let o=typeof n=="string"?n.trim():"";if(!o)return[];let i=[],e=o;for(;e.length>t;){let a=e.lastIndexOf(` `,t);anull,logger:r=console}){let s,g=[],c=null,I=null,d=null,B=!1,f=n,Q=()=>{B||I!==null||d||!c||(I=setTimeout(()=>{I=null;let y=c;c=null;let m=Gn(y,t)[0]??n;d=Promise.resolve(m===f?void 0:e(s,m)).then(()=>{f=m}).catch(_=>r.warn?.("[dsh-im] streamed message update failed:",_)).finally(()=>{d=null,Q()})},o),I?.unref?.())};return{get messageId(){return s},get providerMessageIds(){return[...g]},async start(){return s=await i(n),(typeof s=="string"&&s.trim()||Number.isSafeInteger(s))&&g.push(String(s)),this},update(y){B||typeof y!="string"||!y.trim()||(c=y,Q())},async finish(y){if(B)throw new Error("Message stream is already closed");B=!0,I!==null&&clearTimeout(I),I=null,c=null,await d?.catch(()=>{});let m=Gn(y,t),_=m[0]??D("\u5904\u7406\u5B8C\u6210\u3002");_!==f&&await e(s,_),f=_;for(let N of m.slice(1)){let G=await a(N),Y=A(G);(typeof Y=="string"&&Y.trim()||Number.isSafeInteger(Y))&&g.push(String(Y))}},cancel(){B=!0,c=null,I!==null&&clearTimeout(I),I=null}}}async function Cx(n){try{await n?.body?.cancel?.()}catch{}}function dde(n,t){return!Array.isArray(t)||t.some(o=>typeof o=="string"&&(n.hostname===o||o.startsWith(".")&&(n.hostname===o.slice(1)||n.hostname.endsWith(o))))}async function Zl(n,{fetchImpl:t=fetch,headers:o,signal:i,allowedHosts:e}={}){let a=new URL(n);if(a.protocol!=="https:")throw new Error("File download URL must use HTTPS");if(!dde(a,e))throw new Error("File download URL is not hosted by the messaging platform");let A=await t(a,{method:"GET",headers:o,signal:i,redirect:"manual"});if(Number.isInteger(A?.status)&&A.status>=300&&A.status<400){await Cx(A);let r=new Error(`File download redirect was blocked (HTTP ${A.status})`);throw r.code="file-redirect-blocked",r}if(!A?.ok){await Cx(A);let r=new Error(`File download failed with HTTP ${A?.status??"unknown"}`);throw r.code="file-http-error",r.status=A?.status,r}if(A.body?.[Symbol.asyncIterator])return{stream:A.body};if(typeof A.arrayBuffer=="function"){let r=Buffer.from(await A.arrayBuffer());return{stream:(async function*(){yield r})()}}throw new Error("File download returned no readable body")}var hde=37377,Ede=5e3,ux=Object.freeze([1e3,3e3,5e3,1e4,3e4]),Cde=0,ude=5,Bde=10,fde=11,Qde=12,yde=new Set([Bde,fde,Qde]),mde=new Set(["image/jpeg","image/png","image/webp","image/gif"]),Sde=new Map([[".jpg","image/jpeg"],[".jpeg","image/jpeg"],[".png","image/png"],[".webp","image/webp"],[".gif","image/gif"]]),Qx=Object.freeze(["cdn.discordapp.com"]);function Dde(n){let t=new URL(n);if(t.protocol!=="wss:")throw new Error("Discord returned an insecure Gateway URL");return t.searchParams.set("v","10"),t.searchParams.set("encoding","json"),t.href}function BM(n,t,o){if(typeof n.addEventListener=="function")n.addEventListener(t,o);else if(typeof n.on=="function")n.on(t,o);else throw new TypeError("Discord WebSocket does not support events")}function wde(n){return typeof n=="string"?n:typeof n?.data=="string"?n.data:Buffer.isBuffer(n)?n.toString("utf8"):Buffer.isBuffer(n?.data)?n.data.toString("utf8"):null}function Ode(n){if(n===4004){let o=new Error(D("Discord Bot Token \u65E0\u6548\uFF0C\u8BF7\u91CD\u65B0\u586B\u5199\u3002"));return o.code="discord-401",o}if(n===4013||n===4014){let o=new Error(D("Discord Gateway Intents \u914D\u7F6E\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5 Developer Portal \u7684 Bot \u8BBE\u7F6E\u3002"));return o.code="discord-intents",o}let t=new Error(`Discord Gateway closed (${n||"unknown"})`);return t.code="discord-gateway-closed",t}function yx(n,t){return typeof n!="string"?"":n.replace(new RegExp(`<@!?${t}>`,"g"),"").trim()}function bde(n,t){return[...yx(n?.content??"",t).replace(/\s+/g," ").trim()||"DeepSeek Harness"].slice(0,100).join("")}function yM(n){return yde.has(Number(n?.type))}function mx(n,t){return yM(n)&&String(n?.id??"")===String(t?.id??"")&&String(n?.parent_id??"")===String(t?.channel_id??"")}function fM(n,t){return n?.id&&t?.(n),n}function rI(n,t,o,{fromSourceMessage:i=!1,fallback:e=null,notice:a=null}={}){let A=String(t?.id??n.conversationId),r=yM(t),s=r&&String(t?.owner_id??"")===String(o),g=r&&t?.parent_id?String(t.parent_id):A;return{...n,conversationId:A,addressed:n.addressed||s,requiresMention:r?!s:n.kind==="group",replyTarget:{channelId:A,...!i&&n.replyTarget?.replyToMessageId?{replyToMessageId:n.replyTarget.replyToMessageId}:{},...a?{notice:a}:{}},connectionTestTarget:{channelId:A},conversationRoute:{peerId:g,...r?{threadId:A,managed:s}:{},...e?{fallback:e}:{}}}}function Mde(n){let t=Number(n?.status);return!Number.isInteger(t)||t>=500}function Bx(n){let t=new Error("Discord Thread creation result is uncertain",{cause:n});return t.code="discord-thread-create-uncertain",t}async function _de(n,t,{signal:o,onChannel:i}={}){try{let e=fM(await n.getChannel({channelId:String(t.id),signal:o}),i);return mx(e,t)?e:null}catch(e){if(e?.status===404)return null;throw e}}async function fx(n,t,o){try{await n.createMessage({channelId:t.replyTarget.channelId,replyToMessageId:t.replyTarget.replyToMessageId,content:"Thread \u521B\u5EFA\u7ED3\u679C\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u3002\u82E5\u5DF2\u521B\u5EFA\uFF0C\u8BF7\u5728\u5BF9\u5E94 Thread \u4E2D\u91CD\u8BD5\uFF1B\u82E5\u672A\u521B\u5EFA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u65B0 @\u673A\u5668\u4EBA\u3002",signal:o})}catch(i){if(o?.aborted)throw o.reason??i}}function Sx(n){let t=typeof n?.content_type=="string"?n.content_type.split(";",1)[0].trim().toLowerCase():"";if(mde.has(t))return t;let o=typeof n?.filename=="string"?n.filename.toLowerCase():"";for(let[i,e]of Sde)if(o.endsWith(i))return e;return null}function Dx(n){return Number.isSafeInteger(n)&&n>=0?n:void 0}function Tde(n,t){let o=Sx(n);return!o||typeof n?.url!="string"?null:{name:typeof n.filename=="string"?n.filename:void 0,mediaType:o,size:Dx(n.size),load:i=>no(n.url,{...i,fetchImpl:t,allowedHosts:Qx})}}function Nde(n,t){if(Sx(n)||typeof n?.url!="string"||!n.url)return null;let o=typeof n.content_type=="string"&&n.content_type?n.content_type.split(";",1)[0].trim().toLowerCase():void 0;return{name:typeof n.filename=="string"&&n.filename?n.filename:String(n.id??"discord-file"),...o?{mediaType:o}:{},size:Dx(n.size),load:({signal:i}={})=>Zl(n.url,{fetchImpl:t,signal:i,allowedHosts:Qx})}}function Rde(n,t,{fetchImpl:o=fetch}={}){if(!n?.id||!n?.channel_id||!n?.author?.id||Number(n.type)===21)return null;let i=!n.guild_id,e=i||n.mentions?.some(a=>String(a?.id)===String(t));return{messageId:String(n.id),senderId:String(n.author.id),senderIsBot:n.author.bot===!0,kind:i?"direct":"group",conversationId:String(n.channel_id),content:yx(n.content??"",t),plainText:(!Array.isArray(n.attachments)||n.attachments.length===0)&&(!Array.isArray(n.sticker_items)||n.sticker_items.length===0)&&!n.poll,images:Array.isArray(n.attachments)?n.attachments.map(a=>Tde(a,o)).filter(Boolean):[],files:Array.isArray(n.attachments)?n.attachments.map(a=>Nde(a,o)).filter(Boolean):[],addressed:e,replyTarget:{channelId:String(n.channel_id),replyToMessageId:String(n.id)},reactionTarget:{channelId:String(n.channel_id),messageId:String(n.id)},connectionTestTarget:{channelId:String(n.channel_id)}}}async function kde(n,t,{api:o,channel:i,fetchImpl:e=fetch,signal:a,onChannel:A}={}){let r=Rde(n,t,{fetchImpl:e});if(!r||r.senderIsBot)return r;if(a?.throwIfAborted(),r.kind==="direct")return rI(r,{id:r.conversationId,type:1},t);if(!o||typeof o.getChannel!="function")throw new TypeError("Discord route resolution requires the Discord API");let s=fM(i??await o.getChannel({channelId:r.conversationId,signal:a}),A);if(yM(s)||!r.addressed)return rI(r,s,t);let g=Number(s?.type);if(g!==Cde&&g!==ude)return rI(r,s,t,{fallback:"unsupported-channel",notice:"\u5F53\u524D\u9891\u9053\u4E0D\u652F\u6301\u81EA\u52A8\u521B\u5EFA Thread\uFF0C\u5DF2\u76F4\u63A5\u5728\u5F53\u524D\u9891\u9053\u56DE\u590D\u3002"});a?.throwIfAborted();let c;try{if(c=fM(await o.startThreadFromMessage({channelId:r.conversationId,messageId:r.messageId,name:bde(n,t),signal:a}),A),!mx(c,n))throw new Error("Discord returned an invalid thread for the source message")}catch(I){let d;try{d=await _de(o,n,{signal:AbortSignal.timeout(Ede),onChannel:A})}catch(B){throw a?.aborted?a.reason??I:(await fx(o,r,a),Bx(B))}if(d)return rI(r,d,t,{fromSourceMessage:!0});if(a?.aborted)throw a.reason??I;if(Mde(I))throw await fx(o,r,a),Bx(I);return rI(r,s,t,{fallback:"thread-create-failed",notice:"\u65E0\u6CD5\u521B\u5EFA Thread\uFF0C\u5DF2\u76F4\u63A5\u5728\u5F53\u524D\u9891\u9053\u56DE\u590D\u3002"})}return rI(r,c,t,{fromSourceMessage:!0})}var QM=class{#e;#A;#t=new WeakSet;constructor({api:t,signal:o}){this.#e=t,this.#A=o}async sendText(t,o){let i=!this.#t.has(t)&&t?.notice?String(t.notice):null,e=Gn(i?`${i} ${o}`:o,1900),a=[];for(let[A,r]of e.entries()){let s=await this.#e.createMessage({channelId:t.channelId,content:r,replyToMessageId:A===0?t.replyToMessageId:void 0,signal:this.#A});i&&A===0&&this.#t.add(t),typeof s?.id=="string"&&s.id&&a.push(s.id)}return{providerMessageIds:a}}sendTyping(t){return this.#e.sendTyping({channelId:t.channelId,signal:this.#A})}sendFile(t,o){return this.#e.createFileMessage({channelId:t.channelId,file:o,replyToMessageId:t.replyToMessageId,signal:this.#A})}addReaction(t,o,{signal:i}={}){return this.#e.addOwnReaction({channelId:t.channelId,messageId:t.messageId,emoji:o,signal:this.#r(i)})}removeReaction(t,o,{signal:i}={}){return this.#e.removeOwnReaction({channelId:t.channelId,messageId:t.messageId,emoji:o,signal:this.#r(i)})}async openStream(t){let o=!this.#t.has(t)&&t?.notice?String(t.notice):null,i=a=>o?`${o} diff --git a/src/channels/shared/text-harness-bridge.mjs b/src/channels/shared/text-harness-bridge.mjs index 8b9dccd..799a422 100644 --- a/src/channels/shared/text-harness-bridge.mjs +++ b/src/channels/shared/text-harness-bridge.mjs @@ -866,6 +866,7 @@ export class TextHarnessBridge { try { await this.#presentInteraction(pending); } catch (error) { + message.statusReaction?.error(); this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label }); this.#logger.error?.( `[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`, @@ -904,6 +905,7 @@ export class TextHarnessBridge { try { await this.#presentInteraction(pending); } catch (error) { + message.statusReaction?.error(); this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label }); this.#logger.error?.( `[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`, @@ -946,7 +948,11 @@ export class TextHarnessBridge { message.statusReaction?.clear(); return; } - if (this.#pendingInteractions.get(key) !== pending) return; + if (this.#pendingInteractions.get(key) !== pending) { + message.statusReaction?.clear(); + return; + } + message.statusReaction?.error(); pending.submitting = false; pending.answers.pop(); pending.index -= 1; diff --git a/test/channels/shared/text-harness-bridge.test.mjs b/test/channels/shared/text-harness-bridge.test.mjs index 1347ddc..da53105 100644 --- a/test/channels/shared/text-harness-bridge.test.mjs +++ b/test/channels/shared/text-harness-bridge.test.mjs @@ -141,9 +141,10 @@ function createBridge({ bot, signal, logger, + reactions, } = {}) { return new TextHarnessBridge({ - descriptor: { key: 'test', label: 'Test' }, + descriptor: { key: 'test', label: 'Test', reactions }, bot, harness, state, @@ -2264,11 +2265,22 @@ test('deduplicates replays and safely closes recovered questions and approvals', test('keeps a failed interaction response pending so the actor can retry', async () => { const fixture = stateFixture(); const sent = []; + const reactions = []; const completed = deferred(); const submittedAnswers = []; const bridge = createBridge({ state: fixture.state, - bot: { sendText: async (target, text) => sent.push({ target, text }) }, + reactions: { processing: 'eyes', success: 'done', error: 'error' }, + bot: { + addReaction: async (target, emoji) => { + reactions.push(['add', target.id, emoji]); + return emoji; + }, + removeReaction: async (target, emoji) => { + reactions.push(['remove', target.id, emoji]); + }, + sendText: async (target, text) => sent.push({ target, text }), + }, harness: { createSession: async () => 'session-one', ask: async (sessionId, _text, options) => { @@ -2290,13 +2302,21 @@ test('keeps a failed interaction response pending so the actor can retry', async const processing = bridge.accept(message('retry-start', '启动可重试交互')); await eventually(() => sent.some(({ text }) => text.includes('请回答'))); - await bridge.accept(message('retry-first', '第一次答案')); + await bridge.accept(message('retry-first', '第一次答案', { + reactionTarget: { id: 'source-retry-first' }, + })); + await eventually(() => reactions.length === 3); assert.equal(sent.some(({ text }) => text.includes('回答提交失败')), true); await bridge.accept(message('retry-second', '重试后的答案')); await processing; assert.deepEqual(submittedAnswers, ['第一次答案', '重试后的答案']); assert.equal(sent.at(-1).text, '重试成功'); + assert.deepEqual(reactions, [ + ['add', 'source-retry-first', 'eyes'], + ['remove', 'source-retry-first', 'eyes'], + ['add', 'source-retry-first', 'error'], + ]); }); test('notifies the actor when an in-flight response resolves elsewhere before rejection', async () => {