diff --git a/lib/index.js b/lib/index.js index ae0c136..a5556da 100644 --- a/lib/index.js +++ b/lib/index.js @@ -350,7 +350,7 @@ Generate a new one`,"\u4E8C\u7EF4\u7801\u56FE\u7247\u6B63\u5728\u751F\u6210\u202 `),a=[],A="",r=!1,s=[],g=d=>{if(d.length<=i){if(!A){A=d;return}let f=`${A} ${d}`;f.length>i?(a.push(A),A=d):A=f;return}A&&(a.push(A),A="");let B=d;for(;B.length>i;){let f=qV(B,i);a.push(B.slice(0,f)),B=B.slice(f)}A=B},c=()=>{if(s.length===0)return;let d=s.join(` `);s=[],g(d)},I=d=>{let B=d;for(;B.length>i;){A&&(a.push(A),A="");let f=qV(B,i);a.push(B.slice(0,f)),B=B.slice(f)}g(B)};for(let d of e){if(VNe.test(d)){c(),!r&&A&&(a.push(A),A=""),r=!r,I(d);continue}if(r){I(d);continue}if(WNe.test(d)){s.push(d);continue}c(),I(d)}return c(),A&&a.push(A),a}function e0e(){let n=Date.now()%1e8,t=Math.floor(Math.random()*65536);return(n^t)%65536}async function zV(n,t,o,{logger:i}={}){let e=$Ne(o),a=[],A=t?.msgId?ZNe[t.scope]:null,r=A!==null&&e.length>A,s=r?A-1:e.length,g=t?.msgId?{scope:t.scope,targetId:t.targetId}:t,c=!1,I=async()=>{if(!(c||!t?.msgId)){c=!0;try{a.push(await n.sendText(t,XNe()))}catch(d){i?.warn?.("[dsh-im:qq] unable to send partial reply notice:",d)}}};for(let[d,B]of e.entries()){let f=r&&d>=s?g:t;if(typeof n?.send=="function")try{a.push(await n.send({target:f,msgType:2,markdown:{content:B},extra:{msg_seq:e0e()}}));continue}catch(Q){i?.warn?.("[dsh-im:qq] markdown delivery failed; retrying as plain text:",Q)}try{a.push(await n.sendText(f,B))}catch(Q){if(a.length===0)throw Q;await I();break}}return a}function Mm(){return D("\u8FD9\u4E2A\u95EE\u9898\u5DF2\u5728\u5176\u4ED6\u5BA2\u6237\u7AEF\u5904\u7406\uFF0C\u65E0\u9700\u518D\u6B21\u56DE\u7B54\u3002")}var _m=12e4,A0e=Object.freeze([".myqcloud.com",".qpic.cn",".qq.com",".qq.com.cn",".tencentcos.com",".ugcimg.cn"]),t0e=/\.(?:gif|jpe?g|png|webp)$/i;function r0e(){return[D("QQ \u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5 DeepSeek Harness\u3002"),"",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(` -`)}function a0e(n){return`${n.kind}:${n.kind==="group"?n.groupOpenid:n.senderId}`}function KC(n){return typeof n?.content=="string"?n.content.trim():""}function WV(n){let t=qi(n?.content_type??n?.contentType);return t?t.split(";",1)[0].trim().toLowerCase():null}function nv(n){return WV(n)?.startsWith("image/")===!0||t0e.test(qi(n?.filename)??"")}function HC(n){return Array.isArray(n?.attachments)&&n.attachments.some(nv)}function ed(n){return Array.isArray(n?.attachments)&&n.attachments.some(t=>!nv(t))}async function i0e(n,{fetchImpl:t,signal:o}){let i=n.startsWith("//")?`https:${n}`:n,e=await t(new URL(i),{method:"GET",signal:o,redirect:"follow"});if(!e?.ok)throw await e?.body?.cancel?.().catch?.(()=>{}),new Error(`QQ file download failed with HTTP ${e?.status??"unknown"}`);return Buffer.from(await e.arrayBuffer())}function VV(n,{fetchImpl:t=fetch}={}){if(typeof t!="function")throw new TypeError("fetchImpl must be a function");let o=[],i=[];for(let e of n?.attachments??[]){let a=qi(e?.url),A=qi(e?.filename)??void 0,r=WV(e),s=Number(e?.size);if(nv(e)){o.push({...A?{name:A}:{},...r?.startsWith("image/")?{mediaType:r}:{},...Number.isFinite(s)&&s>=0?{size:s}:{},load:({signal:g,maxBytes:c})=>{if(!a)throw new Error("QQ image attachment has no download URL");return no(a,{fetchImpl:t,signal:g,maxBytes:c,allowedHosts:A0e})}});continue}i.push({name:A??(i.length===0?"file":`file-${i.length+1}`),...r?.includes("/")?{mediaType:r}:{},...Number.isFinite(s)&&s>=0?{size:s}:{},load:({signal:g}={})=>{if(!a)throw new Error("QQ file attachment has no download URL");return i0e(a,{fetchImpl:t,signal:g})}})}return{content:KC(n),images:o,files:i}}function qi(n){return typeof n=="string"&&n.trim()?n.trim():null}function o0e(n,t){let o=String(n??D("\u7ED3\u679C\u6587\u4EF6")).replace(/[\r\n]+/g," ").trim()||D("\u7ED3\u679C\u6587\u4EF6");if(t?.name==="UploadDailyLimitExceededError")return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46 QQ \u4ECA\u65E5\u6587\u4EF6\u4E0A\u4F20\u989D\u5EA6\u5DF2\u7528\u5B8C\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{name:o});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:o});case"artifact-permission-required":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46\u5F53\u524D QQ \u673A\u5668\u4EBA\u6CA1\u6709\u6587\u4EF6\u6D88\u606F\u6743\u9650\u3002",{name:o});case"artifact-too-large":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u8D85\u8FC7\u5F53\u524D QQ \u673A\u5668\u4EBA\u53EF\u53D1\u9001\u7684\u6587\u4EF6\u5927\u5C0F\uFF0C\u672A\u53D1\u9001\u3002",{name:o});case"artifact-empty":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u4E3A\u7A7A\uFF0CQQ \u4E0D\u5141\u8BB8\u53D1\u9001\u7A7A\u6587\u4EF6\u3002",{name:o});case"artifact-changed":case"artifact-invalid":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:o});case"artifact-rate-limited":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u6682\u65F6\u88AB QQ \u9650\u6D41\uFF0C\u672A\u80FD\u53D1\u9001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{name:o});case"artifact-provider-rejected":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46 QQ \u62D2\u7EDD\u4E86\u8BE5\u6587\u4EF6\u6216\u6587\u4EF6\u6D88\u606F\u3002",{name:o});default:return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46\u6682\u65F6\u672A\u80FD\u901A\u8FC7 QQ \u53D1\u9001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{name:o})}}function n0e(n,t){return typeof n=="string"&&n.trim()?n:t.length>0?D("\u7ED3\u679C\u6587\u4EF6\u5DF2\u751F\u6210\u3002"):n}function ZV(n,{dispatched:t=!1}={}){if(n?.code?.startsWith?.("artifact-"))return n;if(n?.name==="UploadDailyLimitExceededError"){let e=new Error("QQ daily file upload limit exceeded",{cause:n});return e.name=n.name,e.code="artifact-rate-limited",e}let o=Number(n?.httpStatus),i=new Error("QQ file delivery failed",{cause:n});return o===401||o===403?i.code="artifact-permission-required":o===413?i.code="artifact-too-large":o===429?i.code="artifact-rate-limited":[400,404,405,406,410,415,422].includes(o)?i.code="artifact-provider-rejected":i.code=t?"artifact-delivery-uncertain":"artifact-provider-failed",i}function sv(n){return n?.reason instanceof Error?n.reason:new DOMException("The operation was aborted","AbortError")}function XV(n,t){return t?(t.throwIfAborted(),new Promise((o,i)=>{let e=!1,a=(r,s)=>{e||(e=!0,t.removeEventListener("abort",A),r(s))},A=()=>a(i,sv(t));t.addEventListener("abort",A,{once:!0}),Promise.resolve(n).then(r=>a(o,r),r=>a(i,r)),t.aborted&&A()})):n}async function s0e(n,t,o,{signal:i,timeoutMs:e=_m}={}){if(i?.throwIfAborted(),typeof n?.sendImage!="function"){let a=new Error("QQ image delivery is unavailable");throw a.code="artifact-provider-unavailable",a}try{let a=AbortSignal.timeout(e),A=i?AbortSignal.any([i,a]):a,r=n.sendImage(t,{buffer:o.bytes},{onProgress:()=>i?.throwIfAborted()});return Zo(o,r),await XV(r,A)}catch(a){throw i?.aborted?sv(i):ZV(a,{dispatched:!0})}}async function g0e(n,t,o,{signal:i,timeoutMs:e=_m}={}){if(i?.throwIfAborted(),typeof n?.sendFile!="function"){let a=new Error("QQ file delivery is unavailable");throw a.code="artifact-provider-unavailable",a}try{let a=AbortSignal.timeout(e),A=i?AbortSignal.any([i,a]):a,r=n.sendFile(t,{buffer:o.bytes},{fileName:o.fileName,onProgress:()=>i?.throwIfAborted()});return Zo(o,r),await XV(r,A)}catch(a){throw i?.aborted?sv(i):ZV(a,{dispatched:!0})}}function c0e(n,t){return t.questions[t.index]&&qi(n?.senderId)===t.actor&&(n.kind!=="group"||n.rawEventType==="GROUP_AT_MESSAGE_CREATE")&&!HC(n)&&!ed(n)&&qi(KC(n))}function gv(){return{messagesReceived:0,messagesReplied:0,messagesRejected:0,artifactsSent:0,artifactSendErrors:0,lastMessageAt:null,lastReplyAt:null,lastRejectedAt:null,lastError:null,lastMessageError:null}}var Tm=class{#e;#A;#t;#r;#a;#i;#c;#s;#I;#o;#n=new Map;#g=new Map;#l=new Map;#p=new Set;#d=new Set;#h=new Set;#E;#u=new Ii;constructor({bot:t,ownerUserOpenid:o,harness:i,state:e,status:a=gv(),logger:A=console,replyTimeoutMs:r=6e5,signal:s,fetchImpl:g=fetch,fileUploadTimeoutMs:c=_m}){if(!t||typeof t.sendText!="function")throw new TypeError("QQ bot client is required");if(!o)throw new TypeError("QQ scanner identity is required");if(!i||!e)throw new TypeError("Harness client and state store are required");if(typeof g!="function")throw new TypeError("fetchImpl must be a function");if(!Number.isInteger(c)||c<1)throw new TypeError("fileUploadTimeoutMs must be a positive integer");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,this.#o=Math.min(c,_m),this.#E=new ii({label:"qq",logger:A})}get status(){return structuredClone(this.#a)}accept(t){if(this.#s?.aborted)return Promise.resolve();let o=qi(t?.messageId),i=qi(t?.senderId);if(!o||!i||t?.senderIsBot===!0||!["c2c","group"].includes(t?.kind)||this.#r.hasSeen(o)||this.#p.has(o))return Promise.resolve();let e=a0e(t);this.#p.add(o),t.kind==="c2c"&&(this.#A==="*"||i===this.#A)&&t.replyTarget?.scope==="c2c"&&qi(t.replyTarget.targetId)===i&&io(this.#r,t.replyTarget);let a=this.#g.get(e),A=KC(t),r=this.#A==="*"||i===this.#A,s=t.kind!=="group"||t.rawEventType==="GROUP_AT_MESSAGE_CREATE",g=lo(A),c=this.#u.status(e);if(g&&r&&s&&t.kind==="group")return this.#m(t,o,{message:Tn()});if(r&&t.kind==="c2c"&&(g||c.phase==="collecting")){let f=/^\/batch$/iu.test(A)&&c.phase==="idle"&&(this.#n.has(e)||a||this.#E.hasPending(e))?{handled:!0,kind:"busy",message:po()}:this.#u.handle(e,A,{plainText:!!A&&!HC(t)&&!ed(t)});if(f.handled)return f.kind==="submit"?this.#B({...t,content:f.prompt,attachments:[]},o,e,{batchSubmission:f}):this.#m(t,o,f)}let I=ed(t)?null:Pi(A)?gi:Gi(A)?Fi:Li(A)?ci:null;if(I&&r&&s){let B;return B=this.#f(t,o,e,A,I).catch(f=>{if(f?.code==="turn-stopped"||this.#s?.aborted)return;this.#a.lastError=f?.message??String(f);let Q=At(this.#a,f);return this.#i.error?.(`[dsh-im:qq] failed to process a command [${Q.referenceId}]:`,f),this.#e.sendText(t.replyTarget,dt(Q)).catch(()=>{})}).finally(()=>{this.#p.delete(o),this.#h.delete(B)}),this.#h.add(B),B}let d=this.#E.claimReply({key:e,actor:i,messageId:o,text:HC(t)||ed(t)?"":KC(t),addressed:t.kind!=="group"||t.rawEventType==="GROUP_AT_MESSAGE_CREATE",hasPendingQuestion:!!a,questionCompletion:a?.submitting||a?.claimedReplyMessageId?a.queue:null,isQuestionPending:()=>this.#g.has(e),send:B=>this.#e.sendText(t.replyTarget,B)});if(d){let B;return B=d.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.#p.delete(o),this.#d.delete(B)}),this.#d.add(B),B}if(a&&i!==a.actor)return this.#B(t,o,e);if(a?.submitting||a?.claimedReplyMessageId)return this.#B(t,o,e);if(a){c0e(t,a)&&(a.claimedReplyMessageId=o);let f=(a.queue??Promise.resolve()).catch(()=>{}).then(()=>this.#C(t,o,e,a)).catch(Q=>this.#N(t,o,Q)).finally(()=>{this.#p.delete(o),a.claimedReplyMessageId===o&&(a.claimedReplyMessageId=null),a.queue===f&&(a.queue=null)});return a.queue=f,f}return this.#B(t,o,e)}#B(t,o,i,{releaseMessageId:e=!0,alreadyRecorded:a=!1,batchSubmission:A=null}={}){let r=this.#A==="*"||t.senderId===this.#A,s=t.kind!=="group"||t.rawEventType==="GROUP_AT_MESSAGE_CREATE",g=r&&s?xl(VV(t,{fetchImpl:this.#I}),{signal:this.#s}):void 0,I=(this.#n.get(i)??Promise.resolve()).catch(()=>{}).then(()=>this.#Q(t,i,{alreadyRecorded:a,preparedMessage:g,batchSubmission:A})).finally(()=>{e&&this.#p.delete(o),this.#n.get(i)===I&&this.#n.delete(i)});return this.#n.set(i,I),I}async waitForIdle(){await Promise.allSettled([...this.#n.values(),...[...this.#g.values()].flatMap(t=>t.queue?[t.queue]:[]),...this.#d,...this.#h])}async#f(t,o,i,e,a){if(this.#s?.throwIfAborted(),this.#r.hasSeen(o))return;await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString();let A=await a(e,this.#t,this.#r,i,{signal:this.#s,hasImages:HC(t),hasFiles:ed(t),pendingInteraction:this.#g.has(i)||this.#E.hasPending(i),control:{owner:this,key:i}});A?.stopped&&await Promise.allSettled([this.#k(i),this.#E.closeRoute(i)]);for(let r of A?.messages??[A?.message])r&&await this.#e.sendText(t.replyTarget,r);this.#a.lastError=null}#m(t,o,i){let e;return e=Promise.resolve().then(async()=>{this.#r.hasSeen(o)||(await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString(),i.message&&await this.#e.sendText(t.replyTarget,i.message),this.#a.lastError=null)}).catch(async a=>{if(this.#s?.aborted)return;this.#a.lastError=a?.message??String(a);let A=At(this.#a,a);this.#i.error?.(`[dsh-im:qq] failed to process a batch input message [${A.referenceId}]:`,a),await this.#e.sendText(t.replyTarget,dt(A)).catch(()=>{})}).finally(()=>{this.#p.delete(o),this.#h.delete(e)}),this.#h.add(e),e}async#y(t,o,i=[],e=null){if(i.length===0)return{receipt:e,failureNoticeVisible:!1,artifactSendErrors:0};let a=await Co({artifacts:i,baseReceipt:e,deliveryId:o,aggregatePresentation:e?"qq-text-and-files":"qq-files",alwaysMerge:!0,channelKey:"qq",signal:this.#s,sendImage:A=>s0e(this.#e,t,A,{signal:this.#s,timeoutMs:this.#o}),sendFile:A=>g0e(this.#e,t,A,{signal:this.#s,timeoutMs:this.#o}),onFailure:(A,r)=>At(this.#a,r,{userMessage:o0e(A?.fileName,r),reason:r?.code}),sendFailureNotice:(A,r,s)=>this.#e.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,failureNoticeVisible:a.failureNoticeVisible,artifactSendErrors:a.artifactSendErrors}}async#Q(t,o,{alreadyRecorded:i=!1,preparedMessage:e,batchSubmission:a=null}={}){if(this.#s?.aborted)return;let A=qi(t?.messageId),r=qi(t?.senderId);if(!A||!r||t.senderIsBot===!0||!["c2c","group"].includes(t.kind))return;if(!i){if(this.#r.hasSeen(A))return;this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString()}let s=i,g=async()=>{s||(await this.#r.markSeen(A),s=!0)};if(this.#A!=="*"&&r!==this.#A){this.#a.messagesRejected+=1,this.#a.lastRejectedAt=new Date().toISOString();return}if(t.kind==="group"&&t.rawEventType!=="GROUP_AT_MESSAGE_CREATE")return;let c=t.replyTarget,I=e??VV(t,{fetchImpl:this.#I}),d=I.content,B=qt(I),f=Gt(I),Q=null,y=a===null;try{if(!d&&!B&&!f){await this.#e.sendText(c,D("\u76EE\u524D\u652F\u6301\u6587\u5B57\u3001\u56FE\u7247\u548C\u6587\u4EF6\u6D88\u606F\u3002")),await g();return}let m=d.toLowerCase();if(!B&&!f&&m==="/help"){await this.#e.sendText(c,r0e()),await g();return}if(!B&&!f&&m==="/status"){await this.#t.ensureRunning({signal:this.#s}),await this.#e.sendText(c,D("QQ \u673A\u5668\u4EBA\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002")),await g();return}if(!B&&!f&&m==="/new"){await this.#r.clearSession(o),await this.#e.sendText(c,D("\u5DF2\u5F00\u542F\u65B0\u4F1A\u8BDD\u3002\u8BF7\u53D1\u9001\u4F60\u7684\u95EE\u9898\u3002")),await g();return}let _=B||f?null:await Io(d,this.#t,o);if(_){for(let T of _.messages??[_.message])await this.#e.sendText(c,T);await g();return}let N=B||f?null:await si(d,this.#t,this.#r,o,{signal:this.#s});if(N){await this.#e.sendText(c,N.message),await g();return}let G=B?await so(I,{signal:this.#s}):void 0;if(t.kind==="c2c"&&c?.msgId&&typeof this.#e.openStream=="function")try{Q=this.#e.openStream({target:c})}catch(T){this.#i.warn?.("[dsh-im:qq] unable to start a QQ stream; using markdown fallback:",T)}let Y=[],j,H=[];try{await g(),{answer:j,artifacts:H=[]}=await ni({harness:this.#t,state:this.#r,key:o,...B?{content:G}:{text:d},createOptions:{signal:this.#s},existsOptions:{signal:this.#s},askOptions:{timeoutMs:this.#c,signal:this.#s,control:{owner:this,key:o},progressMode:"all",onUpdate:T=>{if(T.error){let k=(qi(T.toolName)??D("\u5DE5\u5177")).replace(/[\r\n]+/gu," ").slice(0,80);Y.push(D("\u5DE5\u5177\u8C03\u7528\u300C{name}\u300D\u672A\u6210\u529F\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u5177\u914D\u7F6E\u6216\u7A0D\u540E\u91CD\u8BD5\u3002",{name:k}))}},onInteraction:T=>this.#b(T,{key:o,actor:r,target:c,requiresMention:t.kind==="group"}),onInteractionResolved:T=>this.#M(T),files:I.files}}),a&&(this.#u.complete(o,a.token),y=!0)}finally{await Promise.allSettled([this.#k(o),this.#E.closeRoute(o)])}this.#s?.throwIfAborted();let J=n0e(j,H),q=Y.length>0?`${J} +`)}function a0e(n){return`${n.kind}:${n.kind==="group"?n.groupOpenid:n.senderId}`}function KC(n){return typeof n?.content=="string"?n.content.trim():""}function WV(n){let t=qi(n?.content_type??n?.contentType);return t?t.split(";",1)[0].trim().toLowerCase():null}function nv(n){return WV(n)?.startsWith("image/")===!0||t0e.test(qi(n?.filename)??"")}function HC(n){return Array.isArray(n?.attachments)&&n.attachments.some(nv)}function ed(n){return Array.isArray(n?.attachments)&&n.attachments.some(t=>!nv(t))}async function i0e(n,{fetchImpl:t,signal:o}){let i=n.startsWith("//")?`https:${n}`:n,e=await t(new URL(i),{method:"GET",signal:o,redirect:"follow"});if(!e?.ok)throw await e?.body?.cancel?.().catch?.(()=>{}),new Error(`QQ file download failed with HTTP ${e?.status??"unknown"}`);return Buffer.from(await e.arrayBuffer())}function VV(n,{fetchImpl:t=fetch}={}){if(typeof t!="function")throw new TypeError("fetchImpl must be a function");let o=[],i=[];for(let e of n?.attachments??[]){let a=qi(e?.url),A=qi(e?.filename)??void 0,r=WV(e),s=Number(e?.size);if(nv(e)){o.push({...A?{name:A}:{},...r?.startsWith("image/")?{mediaType:r}:{},...Number.isFinite(s)&&s>=0?{size:s}:{},load:({signal:g,maxBytes:c})=>{if(!a)throw new Error("QQ image attachment has no download URL");return no(a,{fetchImpl:t,signal:g,maxBytes:c,allowedHosts:A0e})}});continue}i.push({name:A??(i.length===0?"file":`file-${i.length+1}`),...r?.includes("/")?{mediaType:r}:{},...Number.isFinite(s)&&s>=0?{size:s}:{},load:({signal:g}={})=>{if(!a)throw new Error("QQ file attachment has no download URL");return i0e(a,{fetchImpl:t,signal:g})}})}return{content:KC(n),images:o,files:i}}function qi(n){return typeof n=="string"&&n.trim()?n.trim():null}function o0e(n,t){let o=String(n??D("\u7ED3\u679C\u6587\u4EF6")).replace(/[\r\n]+/g," ").trim()||D("\u7ED3\u679C\u6587\u4EF6");if(t?.name==="UploadDailyLimitExceededError")return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46 QQ \u4ECA\u65E5\u6587\u4EF6\u4E0A\u4F20\u989D\u5EA6\u5DF2\u7528\u5B8C\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{name:o});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:o});case"artifact-permission-required":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46\u5F53\u524D QQ \u673A\u5668\u4EBA\u6CA1\u6709\u6587\u4EF6\u6D88\u606F\u6743\u9650\u3002",{name:o});case"artifact-too-large":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u8D85\u8FC7\u5F53\u524D QQ \u673A\u5668\u4EBA\u53EF\u53D1\u9001\u7684\u6587\u4EF6\u5927\u5C0F\uFF0C\u672A\u53D1\u9001\u3002",{name:o});case"artifact-empty":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u4E3A\u7A7A\uFF0CQQ \u4E0D\u5141\u8BB8\u53D1\u9001\u7A7A\u6587\u4EF6\u3002",{name:o});case"artifact-changed":case"artifact-invalid":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:o});case"artifact-rate-limited":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u6682\u65F6\u88AB QQ \u9650\u6D41\uFF0C\u672A\u80FD\u53D1\u9001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{name:o});case"artifact-provider-rejected":return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46 QQ \u62D2\u7EDD\u4E86\u8BE5\u6587\u4EF6\u6216\u6587\u4EF6\u6D88\u606F\u3002",{name:o});default:return D("\u7ED3\u679C\u6587\u4EF6\u300C{name}\u300D\u5DF2\u751F\u6210\uFF0C\u4F46\u6682\u65F6\u672A\u80FD\u901A\u8FC7 QQ \u53D1\u9001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",{name:o})}}function n0e(n,t){return typeof n=="string"&&n.trim()?n:t.length>0?D("\u7ED3\u679C\u6587\u4EF6\u5DF2\u751F\u6210\u3002"):n}function ZV(n,{dispatched:t=!1}={}){if(n?.code?.startsWith?.("artifact-"))return n;if(n?.name==="UploadDailyLimitExceededError"){let e=new Error("QQ daily file upload limit exceeded",{cause:n});return e.name=n.name,e.code="artifact-rate-limited",e}let o=Number(n?.httpStatus),i=new Error("QQ file delivery failed",{cause:n});return o===401||o===403?i.code="artifact-permission-required":o===413?i.code="artifact-too-large":o===429?i.code="artifact-rate-limited":[400,404,405,406,410,415,422].includes(o)?i.code="artifact-provider-rejected":i.code=t?"artifact-delivery-uncertain":"artifact-provider-failed",i}function sv(n){return n?.reason instanceof Error?n.reason:new DOMException("The operation was aborted","AbortError")}function XV(n,t){return t?(t.throwIfAborted(),new Promise((o,i)=>{let e=!1,a=(r,s)=>{e||(e=!0,t.removeEventListener("abort",A),r(s))},A=()=>a(i,sv(t));t.addEventListener("abort",A,{once:!0}),Promise.resolve(n).then(r=>a(o,r),r=>a(i,r)),t.aborted&&A()})):n}async function s0e(n,t,o,{signal:i,timeoutMs:e=_m}={}){if(i?.throwIfAborted(),typeof n?.sendImage!="function"){let a=new Error("QQ image delivery is unavailable");throw a.code="artifact-provider-unavailable",a}try{let a=AbortSignal.timeout(e),A=i?AbortSignal.any([i,a]):a,r=n.sendImage(t,{buffer:o.bytes},{onProgress:()=>i?.throwIfAborted()});return Zo(o,r),await XV(r,A)}catch(a){throw i?.aborted?sv(i):ZV(a,{dispatched:!0})}}async function g0e(n,t,o,{signal:i,timeoutMs:e=_m}={}){if(i?.throwIfAborted(),typeof n?.sendFile!="function"){let a=new Error("QQ file delivery is unavailable");throw a.code="artifact-provider-unavailable",a}try{let a=AbortSignal.timeout(e),A=i?AbortSignal.any([i,a]):a,r=n.sendFile(t,{buffer:o.bytes},{fileName:o.fileName,onProgress:()=>i?.throwIfAborted()});return Zo(o,r),await XV(r,A)}catch(a){throw i?.aborted?sv(i):ZV(a,{dispatched:!0})}}function c0e(n,t){return t.questions[t.index]&&qi(n?.senderId)===t.actor&&(n.kind!=="group"||n.rawEventType==="GROUP_AT_MESSAGE_CREATE")&&!HC(n)&&!ed(n)&&qi(KC(n))}function gv(){return{messagesReceived:0,messagesReplied:0,messagesRejected:0,artifactsSent:0,artifactSendErrors:0,lastMessageAt:null,lastReplyAt:null,lastRejectedAt:null,lastError:null,lastMessageError:null}}var Tm=class{#e;#A;#t;#r;#a;#i;#c;#s;#I;#o;#n=new Map;#g=new Map;#l=new Map;#p=new Set;#d=new Set;#h=new Set;#E;#u=new Ii;constructor({bot:t,ownerUserOpenid:o,harness:i,state:e,status:a=gv(),logger:A=console,replyTimeoutMs:r=6e5,signal:s,fetchImpl:g=fetch,fileUploadTimeoutMs:c=_m}){if(!t||typeof t.sendText!="function")throw new TypeError("QQ bot client is required");if(!o)throw new TypeError("QQ scanner identity is required");if(!i||!e)throw new TypeError("Harness client and state store are required");if(typeof g!="function")throw new TypeError("fetchImpl must be a function");if(!Number.isInteger(c)||c<1)throw new TypeError("fileUploadTimeoutMs must be a positive integer");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,this.#o=Math.min(c,_m),this.#E=new ii({label:"qq",logger:A})}get status(){return structuredClone(this.#a)}accept(t){if(this.#s?.aborted)return Promise.resolve();let o=qi(t?.messageId),i=qi(t?.senderId);if(!o||!i||t?.senderIsBot===!0||!["c2c","group"].includes(t?.kind)||this.#r.hasSeen(o)||this.#p.has(o))return Promise.resolve();let e=a0e(t);this.#p.add(o),t.kind==="c2c"&&(this.#A==="*"||i===this.#A)&&t.replyTarget?.scope==="c2c"&&qi(t.replyTarget.targetId)===i&&io(this.#r,t.replyTarget);let a=this.#g.get(e),A=KC(t),r=this.#A==="*"||i===this.#A,s=t.kind!=="group"||t.rawEventType==="GROUP_AT_MESSAGE_CREATE",g=lo(A),c=this.#u.status(e);if(g&&r&&s&&t.kind==="group")return this.#m(t,o,{message:Tn()});if(r&&t.kind==="c2c"&&(g||c.phase==="collecting")){let f=/^\/batch$/iu.test(A)&&c.phase==="idle"&&(this.#n.has(e)||a||this.#E.hasPending(e))?{handled:!0,kind:"busy",message:po()}:this.#u.handle(e,A,{plainText:!!A&&!HC(t)&&!ed(t)});if(f.handled)return f.kind==="submit"?this.#B({...t,content:f.prompt,attachments:[]},o,e,{batchSubmission:f}):this.#m(t,o,f)}let I=ed(t)?null:Pi(A)?gi:Gi(A)?Fi:Li(A)?ci:null;if(I&&r&&s){let B;return B=this.#f(t,o,e,A,I).catch(f=>{if(f?.code==="turn-stopped"||this.#s?.aborted)return;this.#a.lastError=f?.message??String(f);let Q=At(this.#a,f);return this.#i.error?.(`[dsh-im:qq] failed to process a command [${Q.referenceId}]:`,f),this.#e.sendText(t.replyTarget,dt(Q)).catch(()=>{})}).finally(()=>{this.#p.delete(o),this.#h.delete(B)}),this.#h.add(B),B}let d=this.#E.claimReply({key:e,actor:i,messageId:o,text:HC(t)||ed(t)?"":KC(t),addressed:t.kind!=="group"||t.rawEventType==="GROUP_AT_MESSAGE_CREATE",hasPendingQuestion:!!a,questionCompletion:a?.submitting||a?.claimedReplyMessageId?a.queue:null,isQuestionPending:()=>this.#g.has(e),send:B=>this.#e.sendText(t.replyTarget,B)});if(d){let B;return B=d.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.#p.delete(o),this.#d.delete(B)}),this.#d.add(B),B}if(a&&i!==a.actor)return this.#B(t,o,e);if(a?.submitting||a?.claimedReplyMessageId)return this.#B(t,o,e);if(a){c0e(t,a)&&(a.claimedReplyMessageId=o);let f=(a.queue??Promise.resolve()).catch(()=>{}).then(()=>this.#C(t,o,e,a)).catch(Q=>this.#N(t,o,Q)).finally(()=>{this.#p.delete(o),a.claimedReplyMessageId===o&&(a.claimedReplyMessageId=null),a.queue===f&&(a.queue=null)});return a.queue=f,f}return this.#B(t,o,e)}#B(t,o,i,{releaseMessageId:e=!0,alreadyRecorded:a=!1,batchSubmission:A=null}={}){let r=this.#A==="*"||t.senderId===this.#A,s=t.kind!=="group"||t.rawEventType==="GROUP_AT_MESSAGE_CREATE",g=r&&s?xl(VV(t,{fetchImpl:this.#I}),{signal:this.#s}):void 0,I=(this.#n.get(i)??Promise.resolve()).catch(()=>{}).then(()=>this.#Q(t,i,{alreadyRecorded:a,preparedMessage:g,batchSubmission:A})).finally(()=>{e&&this.#p.delete(o),this.#n.get(i)===I&&this.#n.delete(i)});return this.#n.set(i,I),I}async waitForIdle(){await Promise.allSettled([...this.#n.values(),...[...this.#g.values()].flatMap(t=>t.queue?[t.queue]:[]),...this.#d,...this.#h])}async#f(t,o,i,e,a){if(this.#s?.throwIfAborted(),this.#r.hasSeen(o))return;await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString();let A=await a(e,this.#t,this.#r,i,{signal:this.#s,hasImages:HC(t),hasFiles:ed(t),pendingInteraction:this.#g.has(i)||this.#E.hasPending(i),control:{owner:this,key:i}});A?.stopped&&await Promise.allSettled([this.#k(i),this.#E.closeRoute(i)]);for(let r of A?.messages??[A?.message])r&&await this.#e.sendText(t.replyTarget,r);this.#a.lastError=null}#m(t,o,i){let e;return e=Promise.resolve().then(async()=>{this.#r.hasSeen(o)||(await this.#r.markSeen(o),this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString(),i.message&&await this.#e.sendText(t.replyTarget,i.message),this.#a.lastError=null)}).catch(async a=>{if(this.#s?.aborted)return;this.#a.lastError=a?.message??String(a);let A=At(this.#a,a);this.#i.error?.(`[dsh-im:qq] failed to process a batch input message [${A.referenceId}]:`,a),await this.#e.sendText(t.replyTarget,dt(A)).catch(()=>{})}).finally(()=>{this.#p.delete(o),this.#h.delete(e)}),this.#h.add(e),e}async#y(t,o,i=[],e=null){if(i.length===0)return{receipt:e,failureNoticeVisible:!1,artifactSendErrors:0};let a=await Co({artifacts:i,baseReceipt:e,deliveryId:o,aggregatePresentation:e?"qq-text-and-files":"qq-files",alwaysMerge:!0,channelKey:"qq",signal:this.#s,sendImage:A=>s0e(this.#e,t,A,{signal:this.#s,timeoutMs:this.#o}),sendFile:A=>g0e(this.#e,t,A,{signal:this.#s,timeoutMs:this.#o}),onFailure:(A,r)=>At(this.#a,r,{userMessage:o0e(A?.fileName,r),reason:r?.code}),sendFailureNotice:(A,r,s)=>this.#e.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,failureNoticeVisible:a.failureNoticeVisible,artifactSendErrors:a.artifactSendErrors}}async#Q(t,o,{alreadyRecorded:i=!1,preparedMessage:e,batchSubmission:a=null}={}){if(this.#s?.aborted)return;let A=qi(t?.messageId),r=qi(t?.senderId);if(!A||!r||t.senderIsBot===!0||!["c2c","group"].includes(t.kind))return;if(!i){if(this.#r.hasSeen(A))return;this.#a.messagesReceived+=1,this.#a.lastMessageAt=new Date().toISOString()}let s=i,g=async()=>{s||(await this.#r.markSeen(A),s=!0)};if(this.#A!=="*"&&r!==this.#A){this.#a.messagesRejected+=1,this.#a.lastRejectedAt=new Date().toISOString();return}if(t.kind==="group"&&t.rawEventType!=="GROUP_AT_MESSAGE_CREATE")return;let c=t.replyTarget,I=e??VV(t,{fetchImpl:this.#I}),d=I.content,B=qt(I),f=Gt(I),Q=null,y=a===null;try{if(!d&&!B&&!f){await this.#e.sendText(c,D("\u76EE\u524D\u652F\u6301\u6587\u5B57\u3001\u56FE\u7247\u548C\u6587\u4EF6\u6D88\u606F\u3002")),await g();return}let m=d.toLowerCase();if(!B&&!f&&m==="/help"){await this.#e.sendText(c,r0e()),await g();return}if(!B&&!f&&m==="/status"){await this.#t.ensureRunning({signal:this.#s}),await this.#e.sendText(c,D("QQ \u673A\u5668\u4EBA\u4E0E DeepSeek Harness \u8FDE\u63A5\u6B63\u5E38\u3002")),await g();return}if(!B&&!f&&m==="/new"){await this.#r.clearSession(o),await this.#e.sendText(c,D("\u5DF2\u5F00\u542F\u65B0\u4F1A\u8BDD\u3002\u8BF7\u53D1\u9001\u4F60\u7684\u95EE\u9898\u3002")),await g();return}let _=B||f?null:await Io(d,this.#t,o);if(_){for(let T of _.messages??[_.message])await this.#e.sendText(c,T);await g();return}let N=B||f?null:await si(d,this.#t,this.#r,o,{signal:this.#s});if(N){await this.#e.sendText(c,N.message),await g();return}let G=B?await so(I,{signal:this.#s}):void 0,Y=[],j,H=[];try{await g(),{answer:j,artifacts:H=[]}=await ni({harness:this.#t,state:this.#r,key:o,...B?{content:G}:{text:d},createOptions:{signal:this.#s},existsOptions:{signal:this.#s},askOptions:{timeoutMs:this.#c,signal:this.#s,control:{owner:this,key:o},progressMode:"all",onUpdate:T=>{if(T.error){let k=(qi(T.toolName)??D("\u5DE5\u5177")).replace(/[\r\n]+/gu," ").slice(0,80);Y.push(D("\u5DE5\u5177\u8C03\u7528\u300C{name}\u300D\u672A\u6210\u529F\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u5177\u914D\u7F6E\u6216\u7A0D\u540E\u91CD\u8BD5\u3002",{name:k}))}},onInteraction:T=>this.#b(T,{key:o,actor:r,target:c,requiresMention:t.kind==="group"}),onInteractionResolved:T=>this.#M(T),files:I.files}}),a&&(this.#u.complete(o,a.token),y=!0)}finally{await Promise.allSettled([this.#k(o),this.#E.closeRoute(o)])}this.#s?.throwIfAborted();let J=n0e(j,H),q=Y.length>0?`${J} --- diff --git a/src/channels/qq/qq-bridge.mjs b/src/channels/qq/qq-bridge.mjs index 36cf134..5712053 100644 --- a/src/channels/qq/qq-bridge.mjs +++ b/src/channels/qq/qq-bridge.mjs @@ -776,16 +776,8 @@ export class QqHarnessBridge { const content = hasImages ? await promptContentForMessage(promptMessage, { signal: this.#signal }) : undefined; - // QQ C2C keeps one stream bubble. Progress is collected but never submitted: - // some clients reject replacing an already visible stream frame, which would - // otherwise leave a stale progress bubble plus a separate fallback answer. - if (message.kind === 'c2c' && target?.msgId && typeof this.#bot.openStream === 'function') { - try { - stream = this.#bot.openStream({ target }); - } catch (error) { - this.#logger.warn?.('[dsh-im:qq] unable to start a QQ stream; using markdown fallback:', error); - } - } + // QQ stream_messages can acknowledge a final frame without rendering it in + // some C2C clients. Standard Markdown delivery is the reliable reply path. const toolErrors = []; let answer; let artifacts = []; diff --git a/test/channels/qq/bridge.test.mjs b/test/channels/qq/bridge.test.mjs index fe4f7cb..4cd4132 100644 --- a/test/channels/qq/bridge.test.mjs +++ b/test/channels/qq/bridge.test.mjs @@ -808,18 +808,14 @@ test('QQ remembers any authorized private inbound as a connection-test target', assert.equal(sent.length, 2); }); -test('QQ private messages produce one final stream bubble', async () => { +test('QQ private messages deliver the final answer as Markdown without opening a stream', async () => { const sent = []; - const frames = []; + let streamCalls = 0; const seen = new Set(); const bridge = new QqHarnessBridge({ bot: { sendText: async (_target, text) => sent.push(text), - openStream: () => ({ - update: async (text) => frames.push(text), - complete: async () => frames.push('DONE'), - cancel() {}, - }), + openStream: () => { streamCalls += 1; }, }, ownerUserOpenid: 'owner-openid', harness: { @@ -843,45 +839,12 @@ test('QQ private messages produce one final stream bubble', async () => { }); await bridge.accept(message()); - assert.deepEqual(frames, ['最终回答', 'DONE']); - assert.deepEqual(sent, []); + assert.deepEqual(sent, ['最终回答']); + assert.equal(streamCalls, 0); assert.equal(seen.has('msg-1'), true); assert.equal(bridge.status.messagesReplied, 1); }); -test('QQ does not duplicate a visible private answer when stream completion fails', async () => { - const frames = []; - const sent = []; - const bridge = new QqHarnessBridge({ - bot: { - sendText: async (_target, text) => sent.push(text), - openStream: () => ({ - update: async (text) => frames.push(text), - complete: async () => { throw new Error('already submitted'); }, - cancel() {}, - }), - }, - ownerUserOpenid: 'owner-openid', - harness: { - sessionExists: async () => true, - ask: async () => '最终回答', - }, - state: { - hasSeen: () => false, - markSeen: async () => {}, - sessionFor: () => 'session-stream-complete-failure', - setSession: async () => {}, - clearSession: async () => {}, - }, - logger: { warn() {}, error() {} }, - }); - - await bridge.accept(message({ messageId: 'msg-stream-complete-failure' })); - assert.deepEqual(frames, ['最终回答']); - assert.deepEqual(sent, []); - assert.equal(bridge.status.messagesReplied, 1); -}); - test('QQ group messages suppress every successful progress update', async () => { const sent = []; let streamCalls = 0;