openclaw-seatalk 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +46 -15
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +20 -0
- package/package.json +29 -7
- package/index.ts +0 -22
- package/setup-entry.ts +0 -4
- package/src/access.ts +0 -46
- package/src/accounts.ts +0 -96
- package/src/bot.ts +0 -856
- package/src/channel.ts +0 -321
- package/src/client.ts +0 -344
- package/src/config-schema.ts +0 -75
- package/src/inbound-resolve.ts +0 -147
- package/src/media-local.ts +0 -14
- package/src/media.ts +0 -194
- package/src/monitor.ts +0 -210
- package/src/outbound-coalescer.ts +0 -84
- package/src/outbound.ts +0 -81
- package/src/probe.ts +0 -33
- package/src/relay-client.ts +0 -210
- package/src/runtime.ts +0 -14
- package/src/send.ts +0 -90
- package/src/setup-surface.ts +0 -461
- package/src/targets.ts +0 -27
- package/src/tool-schema.ts +0 -60
- package/src/tool.ts +0 -192
- package/src/types.ts +0 -93
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
- Publish bundled JS only (`dist/*.js`), fixing install failure on OpenClaw versions that require compiled runtime output for TypeScript entries.
|
|
6
|
+
- Re-declare `openclaw.setupEntry` so the channel appears in onboarding before configuration.
|
|
7
|
+
- Drop `SEATALK_*` env-var credential fallback.
|
|
8
|
+
|
|
9
|
+
## 0.3.2
|
|
10
|
+
|
|
11
|
+
- Detect half-open relay connections: reconnect after 75s of inbound silence; enable TCP keep-alive (60s).
|
|
12
|
+
|
|
13
|
+
## 0.3.1
|
|
14
|
+
|
|
15
|
+
- Update OpenClaw compatibility and remove SDK deprecation warnings.
|
|
16
|
+
|
|
17
|
+
## 0.3.0
|
|
18
|
+
|
|
19
|
+
- Support forwarded messages.
|
|
20
|
+
- Outbound coalescing: consecutive reply payloads are merged into a single message with automatic markdown-aware chunking at 4000 chars, configurable via `outboundCoalescing`.
|
|
21
|
+
- Pairing mode for DM access control with interactive approval flow (`dmPolicy: "pairing"`). Thanks @edvardchen.
|
|
22
|
+
- Inbound media URL allowlist gate and MIME detection fallback for extensionless URLs. Thanks @edvardchen.
|
|
23
|
+
- Retry on rate-limit with exponential backoff and include `x-rid` in all error messages.
|
|
24
|
+
|
|
25
|
+
## 0.2.1
|
|
26
|
+
|
|
27
|
+
- Remove `setupEntry` to avoid plugin id mismatch when channel is unconfigured.
|
|
28
|
+
- Add ClawHub compatibility metadata (`compat.pluginApi`).
|
|
29
|
+
|
|
30
|
+
## 0.2.0
|
|
31
|
+
|
|
32
|
+
- Migrate to new plugin SDK subpath imports. Requires OpenClaw >= 2026.3.22.
|
|
33
|
+
|
|
34
|
+
## 0.1.6
|
|
35
|
+
|
|
36
|
+
- Reuse cached API client in probe to avoid token rate limiting.
|
|
37
|
+
|
|
38
|
+
## 0.1.5
|
|
39
|
+
|
|
40
|
+
- Align plugin id with manifest.
|
|
41
|
+
- Bump minimum Node.js to >= 22.16.0.
|
|
42
|
+
|
|
43
|
+
## 0.1.4
|
|
44
|
+
|
|
45
|
+
- Exclude signingSecret from resolved account to prevent leaking in status output.
|
|
46
|
+
|
|
47
|
+
## 0.1.3
|
|
48
|
+
|
|
49
|
+
- Split local file read into media-local.ts to avoid security scan false positive.
|
|
50
|
+
- Add install metadata to package.json.
|
|
51
|
+
|
|
52
|
+
## 0.1.2
|
|
53
|
+
|
|
54
|
+
- Align plugin manifest id with npm package name.
|
|
55
|
+
|
|
56
|
+
## 0.1.1
|
|
57
|
+
|
|
58
|
+
- Use os.homedir() for media path resolution to avoid security scan false positive.
|
|
59
|
+
|
|
60
|
+
## 0.1.0
|
|
61
|
+
|
|
62
|
+
- Initial release.
|
package/README.md
CHANGED
|
@@ -77,12 +77,11 @@ openclaw plugins install openclaw-seatalk@0.1.6
|
|
|
77
77
|
|
|
78
78
|
### From source (development)
|
|
79
79
|
|
|
80
|
-
Clone the repo and link it directly — no build step required. OpenClaw loads TypeScript via Jiti at runtime.
|
|
81
|
-
|
|
82
80
|
```bash
|
|
83
81
|
git clone https://github.com/lf4096/openclaw-seatalk.git
|
|
84
82
|
cd openclaw-seatalk
|
|
85
|
-
|
|
83
|
+
pnpm install
|
|
84
|
+
pnpm build
|
|
86
85
|
openclaw plugins install -l .
|
|
87
86
|
```
|
|
88
87
|
|
|
@@ -212,14 +211,6 @@ Or edit the OpenClaw config file directly (`~/.openclaw/openclaw.json`).
|
|
|
212
211
|
| `tools.threadHistory` | boolean | `true` | Enable `seatalk` tool `thread_history` action |
|
|
213
212
|
| `tools.getMessage` | boolean | `true` | Enable `seatalk` tool `get_message` action |
|
|
214
213
|
|
|
215
|
-
Credentials can also be provided via environment variables:
|
|
216
|
-
|
|
217
|
-
| Env Var | Config Field |
|
|
218
|
-
|---------|-------------|
|
|
219
|
-
| `SEATALK_APP_ID` | `appId` |
|
|
220
|
-
| `SEATALK_APP_SECRET` | `appSecret` |
|
|
221
|
-
| `SEATALK_SIGNING_SECRET` | `signingSecret` |
|
|
222
|
-
|
|
223
214
|
### Multi-account
|
|
224
215
|
|
|
225
216
|
Each account has its own credentials and gateway mode. Top-level fields (e.g. `tools`, `dmPolicy`) serve as defaults that accounts inherit and can override.
|
|
@@ -272,16 +263,56 @@ Each action can be individually disabled via the `tools` config.
|
|
|
272
263
|
|
|
273
264
|
## Development
|
|
274
265
|
|
|
266
|
+
Install dependencies:
|
|
267
|
+
|
|
275
268
|
```bash
|
|
276
|
-
# Install dependencies
|
|
277
269
|
pnpm install
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Build `dist/`:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
pnpm build
|
|
276
|
+
```
|
|
278
277
|
|
|
279
|
-
|
|
278
|
+
Format code:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
280
281
|
pnpm format
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Lint:
|
|
281
285
|
|
|
282
|
-
|
|
286
|
+
```bash
|
|
283
287
|
pnpm lint
|
|
288
|
+
```
|
|
284
289
|
|
|
285
|
-
|
|
290
|
+
Check formatting and lint:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
286
293
|
pnpm check
|
|
287
294
|
```
|
|
295
|
+
|
|
296
|
+
## Publishing
|
|
297
|
+
|
|
298
|
+
For npm releases, `prepublishOnly` rebuilds `dist/` automatically:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
npm version patch
|
|
302
|
+
npm publish --access public
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
For beta builds, publish with the `beta` dist-tag:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
npm publish --tag beta
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
For ClawHub releases, build first because `prepublishOnly` does not run:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
pnpm build
|
|
315
|
+
clawhub package publish .
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Inspect tarball contents with `npm pack --dry-run`.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {buildChannelConfigSchema,defineChannelPluginEntry,DEFAULT_ACCOUNT_ID,normalizeAccountId,PAIRING_APPROVED_MESSAGE}from'openclaw/plugin-sdk/core';import*as j from'fs';import*as tt from'os';import*as J from'path';import {resolveSendableOutboundReplyParts,sendMediaWithLeadingCaption}from'openclaw/plugin-sdk/reply-payload';import {createPairingPrefixStripper,createChannelPairingController}from'openclaw/plugin-sdk/channel-pairing';import {resolveDmGroupAccessWithLists,DM_GROUP_ACCESS_REASON}from'openclaw/plugin-sdk/channel-policy';import En from'ws';import*as $e from'crypto';import*as Rt from'http';import {z}from'zod';import {createTopLevelChannelDmPolicy,createStandardChannelSetupStatus,mergeAllowFromEntries}from'openclaw/plugin-sdk/setup';import {Type}from'@sinclair/typebox';var qt=Object.defineProperty;var U=(e,t)=>()=>(e&&(t=e(e=0)),t);var ze=(e,t)=>{for(var a in t)qt(e,a,{get:t[a],enumerable:true});};function zt(e){let t=e.channels?.seatalk?.accounts;return !t||typeof t!="object"?[]:Object.keys(t).filter(Boolean)}function ye(e){let t=zt(e);return t.length===0?[DEFAULT_ACCOUNT_ID]:[...t].toSorted((a,n)=>a.localeCompare(n))}function Pe(e){let t=ye(e);return t.includes(DEFAULT_ACCOUNT_ID)?DEFAULT_ACCOUNT_ID:t[0]??DEFAULT_ACCOUNT_ID}function Wt(e,t){let a=e.channels?.seatalk?.accounts;if(!(!a||typeof a!="object"))return a[t]}function Kt(e,t){let a=e.channels?.seatalk,{accounts:n,...o}=a??{},r=Wt(e,t)??{};return {...o,...r}}function q(e){let t=e?.appId?.trim(),a=e?.appSecret?.trim(),n=e?.signingSecret?.trim();return !t||!a||!n?null:{appId:t,appSecret:a,signingSecret:n}}function A(e){let t=normalizeAccountId(e.accountId),n=e.cfg.channels?.seatalk?.enabled!==false,o=Kt(e.cfg,t),r=o.enabled!==false,s=n&&r,l=q(o);return {accountId:t,enabled:s,configured:!!l,appId:l?.appId,appSecret:l?.appSecret,mode:o.mode??"webhook",relayUrl:o.relayUrl,webhookPort:o.webhookPort??8080,webhookPath:o.webhookPath??"/callback",tools:o.tools,config:o}}function ne(e){return ye(e).map(t=>A({cfg:e,accountId:t})).filter(t=>t.enabled&&t.configured)}var N=U(()=>{});function xe(e,t){let a=`${e}:${t}`,n=Ve.get(a);return n||(n=new Me(e,t),Ve.set(a,n)),n}function F(e){return !e.appId||!e.appSecret?null:xe(e.appId,e.appSecret)}var We,Ke,Me,Ve,K=U(()=>{We="https://openapi.seatalk.io",Ke=[1e4,6e4],Me=class{appId;appSecret;tokenInfo=null;tokenPromise=null;constructor(t,a){this.appId=t,this.appSecret=a;}async getAccessToken(){if(this.tokenInfo){let t=Math.floor(Date.now()/1e3);if(this.tokenInfo.expireAt-t>600)return this.tokenInfo.token}return (await this.refreshToken()).token}async refreshToken(){if(this.tokenPromise)return this.tokenPromise;this.tokenPromise=this._fetchToken();try{let t=await this.tokenPromise;return this.tokenInfo=t,t}finally{this.tokenPromise=null;}}async _fetchToken(){let t=new AbortController,a=setTimeout(()=>t.abort(),1e4);try{let n=await fetch(`${We}/auth/app_access_token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:this.appId,app_secret:this.appSecret}),signal:t.signal});if(!n.ok){let r=n.headers.get("x-rid")??void 0;throw new Error(`SeaTalk token request failed: HTTP ${n.status} (x-rid: ${r})`)}let o=await n.json();if(o.code!==0)throw new Error(`SeaTalk token error: code=${o.code} message=${o.message??"unknown"}`);if(!o.app_access_token||!o.expire)throw new Error("SeaTalk token response missing token or expire");return {token:o.app_access_token,expireAt:o.expire}}finally{clearTimeout(a);}}async apiCall(t,a,n,o=true,r=0){let s=await this.getAccessToken(),l=new AbortController,c=setTimeout(()=>l.abort(),1e4),i;try{let g=await fetch(`${We}${a}`,{method:t,headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:n?JSON.stringify(n):void 0,signal:l.signal});if(i=g.headers.get("x-rid")??void 0,!g.ok)throw Object.assign(new Error(`SeaTalk API error: HTTP ${g.status} (x-rid: ${i})`),{httpStatus:g.status,xRid:i});let d=await g.json();if(d.code===100&&o)return await this.refreshToken(),this.apiCall(t,a,n,!1);if(d.code===101){if(r<Ke.length){let p=Ke[r];return await new Promise(f=>setTimeout(f,p)),this.apiCall(t,a,n,o,r+1)}throw Object.assign(new Error(`SeaTalk rate limit exceeded after ${r+1} attempts (x-rid: ${i})`),{code:101,xRid:i})}if(d.code!==0)throw Object.assign(new Error(`SeaTalk API error: code=${d.code} message=${d.message??"unknown"} (x-rid: ${i})`),{code:d.code,xRid:i});return d}catch(g){throw i&&g instanceof Error&&!g.message.includes("x-rid:")&&(g.message+=` (x-rid: ${i})`),g}finally{clearTimeout(c);}}async sendSingleChat(t,a,n){let o=n?{...a,thread_id:n}:a;await this.apiCall("POST","/messaging/v2/single_chat",{employee_code:t,message:o});}async sendGroupChat(t,a,n){let o=n?{...a,thread_id:n}:a;await this.apiCall("POST","/messaging/v2/group_chat",{group_id:t,message:o});}async setSingleChatTyping(t,a){let n={employee_code:t};a&&(n.thread_id=a),await this.apiCall("POST","/messaging/v2/single_chat_typing",n);}async setGroupChatTyping(t,a){let n={group_id:t};a&&(n.thread_id=a),await this.apiCall("POST","/messaging/v2/group_chat_typing",n);}async downloadMedia(t){let a=await this.getAccessToken(),n=new AbortController,o=setTimeout(()=>n.abort(),6e4);try{let r=await fetch(t,{headers:{Authorization:`Bearer ${a}`},signal:n.signal});if(!r.ok)throw new Error(`SeaTalk media download failed: HTTP ${r.status}`);let s=r.headers.get("content-type")??"application/octet-stream",l=await r.arrayBuffer();return {buffer:Buffer.from(l),contentType:s}}finally{clearTimeout(o);}}async getEmployeeCodeByEmail(t){let n=[];for(let o=0;o<t.length;o+=500){let r=t.slice(o,o+500),s=await this.apiCall("POST","/contacts/v2/get_employee_code_with_email",{emails:r});for(let l of s.employees??[])n.push({email:l.email,employeeCode:l.employee_code,status:l.employee_status});}return n}async getGroupChatHistory(t,a){let n=new URLSearchParams({group_id:t,page_size:String(a?.pageSize??50)});return a?.cursor&&n.set("cursor",a.cursor),this.apiCall("GET",`/messaging/v2/group_chat/history?${n}`)}async getJoinedGroupChats(t){let a=new URLSearchParams;t?.pageSize&&a.set("page_size",String(t.pageSize)),t?.cursor&&a.set("cursor",t.cursor);let n=a.toString();return this.apiCall("GET",`/messaging/v2/group_chat/joined${n?`?${n}`:""}`)}async getGroupChatInfo(t){return this.apiCall("GET",`/messaging/v2/group_chat/info?group_id=${encodeURIComponent(t)}`)}async getDmThread(t,a,n){let o=new URLSearchParams({employee_code:t,thread_id:a});return n?.pageSize&&o.set("page_size",String(n.pageSize)),n?.cursor&&o.set("cursor",n.cursor),this.apiCall("GET",`/messaging/v2/single_chat/get_thread_by_thread_id?${o}`)}async getGroupThread(t,a,n){let o=new URLSearchParams({group_id:t,thread_id:a});return n?.pageSize&&o.set("page_size",String(n.pageSize)),n?.cursor&&o.set("cursor",n.cursor),this.apiCall("GET",`/messaging/v2/group_chat/get_thread_by_thread_id?${o}`)}async getMessageByMessageId(t){return this.apiCall("GET",`/messaging/v2/get_message_by_message_id?message_id=${encodeURIComponent(t)}`)}getAppId(){return this.appId}},Ve=new Map;});function et(e){Re=e;}function V(){if(!Re)throw new Error("SeaTalk runtime not initialized");return Re}var Re,ce=U(()=>{Re=null;});function tn(e){let t=e&&e.length>0?e:[...en];return new Set(t.map(a=>a.trim().toLowerCase()).filter(Boolean))}function nn(e,t){let a;try{a=new URL(e);}catch{return {ok:false,detail:"invalid URL"}}if(a.protocol!=="https:")return {ok:false,detail:`only https allowed (got ${a.protocol})`};let n=a.hostname.toLowerCase();return t.has(n)?{ok:true,hostname:n}:{ok:false,detail:`host not in allowlist (${n})`}}async function de(e){let{message:t,client:a,log:n,mediaAllowHosts:o}=e,r=V(),s=tn(o),l,c,i=Oe.file;switch(t.tag){case "image":l=t.image?.content,i=Oe.image;break;case "file":l=t.file?.content,c=t.file?.filename;break;case "video":l=t.video?.content,i=Oe.video;break;default:return null}if(!l)return null;let g=nn(l,s);if(!g.ok)return n?.(`seatalk: rejected inbound ${t.tag} media before download: ${g.detail}`),null;n?.(`seatalk: inbound ${t.tag} media url host=${g.hostname}`);let d=1;for(let p=0;p<=d;p++)try{let f=await a.downloadMedia(l),u=f.contentType;if((!u||u==="application/octet-stream")&&f.buffer.length<Qt){let h=await r.media.detectMime({buffer:f.buffer});h&&(u=h);}let S=await r.channel.media.saveMediaBuffer(f.buffer,u??"application/octet-stream","inbound",Zt,c);return n?.(`seatalk: downloaded ${t.tag} media, saved to ${S.path}`),{path:S.path,contentType:S.contentType,filename:c,placeholder:i}}catch(f){if(p<d){n?.(`seatalk: retry ${p+1}/${d} downloading ${t.tag} media: ${String(f)}`);continue}return n?.(`seatalk: failed to download ${t.tag} media after ${d+1} attempts: ${String(f)}`),null}return null}async function an(e){let t=new AbortController,a=setTimeout(()=>t.abort(),3e4);try{let n=await fetch(e,{signal:t.signal});if(!n.ok)throw new Error(`Failed to fetch media from ${e}: HTTP ${n.status}`);let o=await n.arrayBuffer(),r=Buffer.from(o),s=new URL(e).pathname;return {buffer:r,name:J.basename(s)||"file"}}finally{clearTimeout(a);}}function on(e){let t=e.startsWith("~")?J.join(tt.homedir(),e.slice(1)):e.replace(/^file:\/\//,"");if(!j.existsSync(t))throw new Error(`Media file not found: ${t}`);let a=j.openSync(t,"r");try{let{size:n}=j.fstatSync(a),o=Buffer.alloc(n),r=0;for(;r<n;){let l=j.readSync(a,o,r,n-r,r);if(l===0)break;r+=l;}return {buffer:r<n?o.subarray(0,r):o,name:J.basename(t)}}finally{j.closeSync(a);}}async function nt(e){let t=e.startsWith("http://")||e.startsWith("https://"),{buffer:a,name:n}=t?await an(e):on(e);if(a.length>Yt)throw new Error(`Media file too large: ${(a.length/1024/1024).toFixed(1)}MB exceeds ~3.75MB limit`);let o=J.extname(n).toLowerCase(),r=Xt.has(o)?"image":"file";return {base64:a.toString("base64"),sendAs:r,filename:r==="file"?n.slice(0,100):void 0}}function De(e){let t=e[0],a=e.map(o=>o.path),n=e.map(o=>o.contentType).filter(Boolean);return {MediaPath:t?.path,MediaType:t?.contentType,MediaUrl:t?.path,MediaPaths:a.length>0?a:void 0,MediaUrls:a.length>0?a:void 0,MediaTypes:n.length>0?n:void 0}}var Oe,Xt,Yt,Qt,Zt,en,Se=U(()=>{ce();Oe={image:"<media:image>",file:"<media:document>",video:"<media:video>"},Xt=new Set([".png",".jpg",".jpeg",".gif"]),Yt=3.75*1024*1024,Qt=10*1024*1024,Zt=250*1024*1024,en=["openapi.seatalk.io"];});async function H(e,t,a,n=1,o){await e.sendSingleChat(t,{tag:"text",text:{format:n,content:a}},o);}async function at(e,t,a,n){await e.sendSingleChat(t,{tag:"image",image:{content:a}},n);}async function ot(e,t,a,n,o){await e.sendSingleChat(t,{tag:"file",file:{content:a,filename:n}},o);}async function ae(e,t,a,n=1,o){await e.sendGroupChat(t,{tag:"text",text:{format:n,content:a}},o);}async function we(e){let{client:t,to:a,mediaUrl:n,threadId:o,isGroup:r}=e,s=await nt(n);s&&(r?s.sendAs==="image"?await t.sendGroupChat(a,{tag:"image",image:{content:s.base64}},o):await t.sendGroupChat(a,{tag:"file",file:{content:s.base64,filename:s.filename||"file"}},o):s.sendAs==="image"?await at(t,a,s.base64,o):await ot(t,a,s.base64,s.filename||"file",o));}var oe=U(()=>{Se();});function ht(e){let{groupPolicy:t,groupAllowFrom:a,groupSenderAllowFrom:n,groupId:o,senderEmployeeCode:r,senderEmail:s}=e;return t==="disabled"?{allowed:false,reason:"groupPolicy is disabled"}:t==="allowlist"&&!(a??[]).includes(o)?{allowed:false,reason:`group ${o} not in groupAllowFrom`}:n&&n.length>0&&!n.some(c=>{let i=c.trim();return !!(i==="*"||i===r||s&&i.toLowerCase()===s.toLowerCase())})?{allowed:false,reason:`sender ${r} not in groupSenderAllowFrom`}:{allowed:true}}var kt=U(()=>{});function gn(e){let t=e.sender,a=e.message_sent_time,n=[];return t?.email?n.push(t.email):t?.employee_code&&n.push(t.employee_code),a&&n.push(new Date(a*1e3).toISOString()),n.length>0?`[${n.join(" ")}] `:""}function pn(e){let t=e.tag;return {message_id:e.message_id??"",tag:t,text:e.text,image:e.image,file:e.file,video:e.video,combined_forwarded_chat_history:e.combined_forwarded_chat_history}}async function yt(e,t){let a=e.tag,n=[];if(a==="text"){let o=e.text;return {text:o?.plain_text??o?.content??"",media:n}}if(a==="image"||a==="file"||a==="video"){let o=await de({message:pn(e),client:t.client,mediaAllowHosts:t.mediaAllowHosts,log:t.log});return o?(n.push(o),{text:o.placeholder,media:n}):{text:`<media:${a}>`,media:n}}if(a==="combined_forwarded_chat_history"){let o=e.combined_forwarded_chat_history?.content;if(o){let r=await ge(o,t);return n.push(...r.media),{text:r.lines.length>0?`[Forwarded messages]
|
|
2
|
+
${r.lines.join(`
|
|
3
|
+
`)}`:"[Forwarded messages]",media:n}}return {text:"[Forwarded messages]",media:n}}return {text:`<unsupported:${a??"unknown"}>`,media:n}}async function ge(e,t){let a=[],n=[];for(let o of e){if(Array.isArray(o)){let c=await ge(o,t);a.push(...c.lines),n.push(...c.media);continue}if(!o||typeof o!="object")continue;let r=o,s=gn(r),l=await yt(r,t);n.push(...l.media),l.text&&a.push(`${s}${l.text}`);}return {lines:a,media:n}}async function Be(e){let{client:t,quotedMessageId:a,mediaAllowHosts:n,log:o}=e;try{let r=await t.getMessageByMessageId(a),s=r.sender,l=s?.employee_code??"unknown",c=s?.email?`${l} (${s.email})`:l,i=await yt(r,{client:t,mediaAllowHosts:n,log:o});return {text:`[Quoted from ${c}: ${i.text}]`,media:i.media}}catch(r){return o(`seatalk: failed to resolve quoted message ${a}: ${String(r)}`),null}}async function Le(e){let{mediaUrls:t,client:a,to:n,threadId:o,isGroup:r,log:s}=e;await sendMediaWithLeadingCaption({mediaUrls:t,caption:"",send:async({mediaUrl:l})=>{await we({client:a,to:n,mediaUrl:l,threadId:o,isGroup:r});},onError:async({error:l,mediaUrl:c})=>{s(`seatalk: failed to send media ${c}: ${String(l)}`);}});}var St=U(()=>{Se();oe();});function Ue(e){let{send:t,chunkText:a,maxLength:n,joiner:o,idleFlushMs:r}=e,s="",l,c=Promise.resolve(),i=()=>{l&&(clearTimeout(l),l=void 0);},g=()=>{if(!s)return;let u=s;s="";let S=u.length>n?a(u,n):[u],h=async()=>{for(let w of S)await t(w);};c=c.then(h,h),c.catch(()=>{});},d=()=>{!r||r<=0||(i(),l=setTimeout(()=>{l=void 0,g();},r));};return {append:u=>{if(!u)return;if(i(),!s){s=u,d();return}let S=`${s}${o}${u}`;if(S.length>n){g(),s=u,d();return}s=S,d();},flush:async()=>{i(),g(),await c;},hasBuffered:()=>s.length>0}}var wt=U(()=>{});function kn(e,t,a){return a.some(n=>{let o=n.trim();return !!(o==="*"||o===e||t&&o.toLowerCase()===t.toLowerCase())})}function ve(e){let{cfg:t,event:a,client:n,runtime:o,accountId:r}=e,s=o?.log??console.log,l=o?.error??console.error,c=i=>i().catch(g=>l(`seatalk[${r}]: event error: ${String(g)}`));switch(a.event_type){case "message_from_bot_subscriber":c(()=>In({cfg:t,event:a,client:n,runtime:o,accountId:r}));break;case "new_mentioned_message_received_from_group_chat":case "new_message_received_from_thread":c(()=>$n({cfg:t,event:a,client:n,runtime:o,accountId:r}));break;case "new_bot_subscriber":{let i=a.event?.employee_code;s(`seatalk[${r}]: new subscriber: ${i}`);break}case "bot_added_to_group_chat":{let i=a.event?.group_id;s(`seatalk[${r}]: bot added to group: ${i}`);break}case "bot_removed_from_group_chat":{let i=a.event?.group_id;s(`seatalk[${r}]: bot removed from group: ${i}`);break}default:s(`seatalk[${r}]: unhandled event type: ${a.event_type}`);}}function Ct(e){let t=Date.now();if(t-Tt>wn){for(let[a,n]of Y)t-n>yn&&Y.delete(a);Tt=t;}if(Y.has(e))return false;if(Y.size>=Sn){let a=Y.keys().next().value;Y.delete(a);}return Y.set(e,t),true}function bn(e,t,a){return a?`${e}:dm:${t}:t:${a}`:`${e}:dm:${t}`}function Cn(e,t,a,n){return n?`${e}:grp:${t}:${a}:t:${n}`:`${e}:grp:${t}:${a}`}function _n(e,t){clearTimeout(t.timer);let a=Date.now()-t.firstEventAt,n=Tn-a;if(n<=0){Ge(e);return}let o=Math.min(_t,n);t.timer=setTimeout(()=>Ge(e),o);}function Ge(e){let t=_e.get(e);if(!t)return;_e.delete(e);let a=t.entries;if(a.length===0)return;a[0].kind==="dm"?vn(a,t.context).catch(r=>{(t.context.runtime?.error??console.error)(`seatalk[${t.context.accountId}]: flush error: ${String(r)}`);}):An(a,t.context).catch(r=>{(t.context.runtime?.error??console.error)(`seatalk[${t.context.accountId}]: group flush error: ${String(r)}`);});}function It(e,t,a){let n=_e.get(e);n||(n={entries:[],timer:setTimeout(()=>Ge(e),_t),firstEventAt:Date.now(),context:a},_e.set(e,n)),n.entries.push(t),_n(e,n);}async function vn(e,t){let{cfg:a,client:n,runtime:o,accountId:r}=t,s=o?.log??console.log,l=o?.error??console.error,c=e[0].parsedEvent,i=c.employee_code,g=c.email,d=A({cfg:a,accountId:r}),p=d.config,f=V(),u=p?.dmPolicy??"allowlist",S=(p?.allowFrom??[]).map(T=>String(T)),h=createChannelPairingController({core:f,channel:"seatalk",accountId:r}),w=u==="pairing"?await h.readAllowFromStore().catch(()=>[]):[],b=resolveDmGroupAccessWithLists({isGroup:false,dmPolicy:u,groupPolicy:"disabled",allowFrom:S,groupAllowFrom:[],storeAllowFrom:w,isSenderAllowed:T=>kn(i,g,T)});if(b.decision==="pairing"){(await h.issueChallenge({senderId:i,senderIdLine:`Your SeaTalk employee code: ${i}`,meta:g?{email:g}:void 0,onCreated:({code:y})=>{s(`seatalk[${r}]: pairing request sender=${i} code=${y}`);},sendPairingReply:async y=>{await H(n,i,y,1,c.message.thread_id);},onReplyError:y=>{s(`seatalk[${r}]: pairing reply failed for ${i}: ${String(y)}`);}})).created||s(`seatalk[${r}]: pairing already pending for ${i}`);return}if(b.decision!=="allow"){b.reasonCode===DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED?s(`seatalk[${r}]: blocked DM from ${i} (dmPolicy=disabled)`):s(`seatalk[${r}]: sender ${i} not in allowlist, dropping`);return}let $=p?.mediaAllowHosts,v={client:n,mediaAllowHosts:$,log:s},C=[],M=[];for(let{parsedEvent:T}of e){let y=T.message;switch(y.tag){case "text":(y.text?.plain_text||y.text?.content)&&C.push(y.text.plain_text??y.text.content??"");break;case "image":case "file":case "video":{let E=await de({message:y,client:n,mediaAllowHosts:$,log:s});E&&M.push(E);break}case "combined_forwarded_chat_history":{let E=y.combined_forwarded_chat_history?.content;if(E){let z=await ge(E,v);M.push(...z.media),C.push(z.lines.length>0?`[Forwarded messages]
|
|
4
|
+
${z.lines.join(`
|
|
5
|
+
`)}`:"[Forwarded messages]");}break}}}let G=new Set,x=[];for(let{parsedEvent:T}of e){let y=T.message.quoted_message_id;if(!y||G.has(y))continue;G.add(y);let E=await Be({client:n,quotedMessageId:y,mediaAllowHosts:$,log:s});E&&(x.push(E.text),M.push(...E.media));}let re=De(M),R=C.join(`
|
|
6
|
+
`);if(x.length>0){let T=x.join(`
|
|
7
|
+
`);R=R?`${T}
|
|
8
|
+
${R}`:T;}if(!R&&M.length>0&&(R=M.map(T=>T.placeholder).join(" ")),!R&&M.length===0){s(`seatalk[${r}]: skipping empty message from ${i}`);return}let se=i+(g?` (${g})`:""),_=c.message.message_id,I=c.message.thread_id;try{let T=`seatalk:${i}`,y=i,E=f.channel.routing.resolveAgentRoute({cfg:a,channel:"seatalk",accountId:r,peer:{kind:"direct",id:i}}),z=R.replace(/\s+/g," ").slice(0,160);f.system.enqueueSystemEvent(`SeaTalk[${r}] DM from ${se}: ${z}`,{sessionKey:E.sessionKey,contextKey:`seatalk:message:${i}:${_}`});let Q=e[0].event.timestamp,ie=Q?new Date(Q*1e3):new Date,Ae=f.channel.reply.resolveEnvelopeFormatOptions(a),Ne=`${se}: ${R}`,fe=f.channel.reply.formatAgentEnvelope({channel:"SeaTalk",from:i,timestamp:ie,envelope:Ae,body:Ne}),Z={};I&&(Z.threadId=I);let le=c.message.quoted_message_id;le&&(Z.quotedMessageId=le);let me=f.channel.reply.finalizeInboundContext({Body:fe,BodyForAgent:R,RawBody:R,CommandBody:R,From:T,To:y,SessionKey:E.sessionKey,AccountId:E.accountId,ChatType:"direct",SenderName:se,SenderId:i,Provider:"seatalk",Surface:"seatalk",MessageSid:_,MessageThreadId:I||void 0,Timestamp:Q?Q*1e3:Date.now(),WasMentioned:!1,CommandAuthorized:!0,OriginatingChannel:"seatalk",OriginatingTo:y,...Object.keys(Z).length>0?{Metadata:Z}:{},...re});(d.config?.processingIndicator??"typing")==="typing"&&n.setSingleChatTyping(i,I).catch(B=>s(`seatalk[${r}]: typing failed: ${String(B)}`));let ee=p?.outboundCoalescing!==!1,he=B=>H(n,i,B,1,I),O=(B,L)=>f.channel.text.chunkMarkdownText(B,L),P=ee?Ue({send:he,chunkText:O,maxLength:Ce,joiner:`
|
|
9
|
+
|
|
10
|
+
`,idleFlushMs:vt}):null,te=f.channel.reply.createReplyDispatcherWithTyping({humanDelay:f.channel.reply.resolveHumanDelayConfig(a,E.agentId),deliver:async B=>{let L=resolveSendableOutboundReplyParts(B);if(!(!L.hasText&&!L.hasMedia)){if(L.hasText)if(s(`seatalk[${r}]: inline deliver DM to ${i} threadId=${I||"none"}`),P)P.append(L.trimmedText);else {let jt=O(L.trimmedText,Ce);for(let Ht of jt)await he(Ht);}L.hasMedia&&(P&&await P.flush(),await Le({mediaUrls:L.mediaUrls,client:n,to:i,threadId:I,isGroup:!1,log:s}));}},onError:B=>{l(`seatalk[${r}]: reply delivery failed: ${String(B)}`);}}),Ee={agentId:E.agentId,...te.replyOptions};s(`seatalk[${r}]: dispatching to agent (session=${E.sessionKey})`);try{let{queuedFinal:B,counts:L}=await f.channel.reply.dispatchReplyFromConfig({ctx:me,cfg:a,dispatcher:te.dispatcher,replyOptions:Ee});s(`seatalk[${r}]: dispatch complete (queuedFinal=${B}, counts=${JSON.stringify(L)})`);}finally{te.markDispatchIdle(),P&&(await te.dispatcher.waitForIdle(),await P.flush());}}catch(T){l(`seatalk[${r}]: failed to dispatch message: ${String(T)}`);}}async function In(e){let{cfg:t,event:a,client:n,runtime:o,accountId:r}=e,s=o?.log??console.log;if(!Ct(`${r}:${a.event_id}`)){s(`seatalk[${r}]: skipping duplicate event ${a.event_id}`);return}let l=a.event;if(!l?.employee_code||!l?.message){s(`seatalk[${r}]: malformed message event, skipping`);return}s(`seatalk[${r}]: received ${l.message.tag} from ${l.employee_code} (threadId=${l.message.thread_id||"none"})`);let c=bn(r,l.employee_code,l.message.thread_id);It(c,{kind:"dm",event:a,parsedEvent:l},{cfg:t,client:n,runtime:o,accountId:r});}async function $n(e){let{cfg:t,event:a,client:n,runtime:o,accountId:r}=e,s=o?.log??console.log;if(!Ct(`${r}:${a.event_id}`)){s(`seatalk[${r}]: skipping duplicate group event ${a.event_id}`);return}let l=a.event,c=l?.group_id,i=l?.message,g=i?.sender;if(!c||!i||!g?.employee_code){s(`seatalk[${r}]: malformed group message event, skipping`);return}if(g.sender_type===2){s(`seatalk[${r}]: ignoring bot message in group ${c}`);return}let d=g.employee_code,p=g.email,f=i.thread_id;s(`seatalk[${r}]: group ${c} ${i.tag} from ${d} (event=${a.event_type})`);let S=A({cfg:t,accountId:r}).config,h=ht({groupPolicy:S?.groupPolicy??"disabled",groupAllowFrom:S?.groupAllowFrom,groupSenderAllowFrom:S?.groupSenderAllowFrom,groupId:c,senderEmployeeCode:d,senderEmail:p});if(!h.allowed){s(`seatalk[${r}]: group access denied: ${h.reason}`);return}let w=Cn(r,c,d,f);It(w,{kind:"group",event:a,groupEvent:l,groupId:c,eventType:a.event_type},{cfg:t,client:n,runtime:o,accountId:r});}async function An(e,t){let{cfg:a,client:n,runtime:o,accountId:r}=t,s=o?.log??console.log,l=o?.error??console.error,c=e[0],i=c.groupId,g=c.groupEvent.message,d=g.sender,p=d.employee_code,f=d.email,u=g.thread_id,h=A({cfg:a,accountId:r}).config,w=h?.mediaAllowHosts,b={client:n,mediaAllowHosts:w,log:s},$=[],v=[];for(let{groupEvent:_}of e){let I=_.message;switch(I.tag){case "text":(I.text?.plain_text||I.text?.content)&&$.push(I.text.plain_text??I.text.content??"");break;case "image":case "file":case "video":{let T=await de({message:I,client:n,mediaAllowHosts:w,log:s});T&&v.push(T);break}case "combined_forwarded_chat_history":{let T=I.combined_forwarded_chat_history?.content;if(T){let y=await ge(T,b);v.push(...y.media),$.push(y.lines.length>0?`[Forwarded messages]
|
|
11
|
+
${y.lines.join(`
|
|
12
|
+
`)}`:"[Forwarded messages]");}break}}}let C=c.groupEvent.message.quoted_message_id,M=null;if(C){let _=await Be({client:n,quotedMessageId:C,mediaAllowHosts:w,log:s});_&&(M=_.text,v.push(..._.media));}let G=De(v),x=$.join(`
|
|
13
|
+
`);if(M&&(x=x?`${M}
|
|
14
|
+
${x}`:M),!x&&v.length>0&&(x=v.map(_=>_.placeholder).join(" ")),!x&&v.length===0){s(`seatalk[${r}]: skipping empty group message from ${p} in ${i}`);return}let re=p+(f?` (${f})`:""),R=g.message_id,se=e.some(_=>_.eventType==="new_mentioned_message_received_from_group_chat");try{let _=V(),I=_.channel.routing.resolveAgentRoute({cfg:a,channel:"seatalk",accountId:r,peer:{kind:"group",id:i}}),T=x.replace(/\s+/g," ").slice(0,160);_.system.enqueueSystemEvent(`SeaTalk[${r}] Group(${i}) from ${re}: ${T}`,{sessionKey:I.sessionKey,contextKey:`seatalk:group:${i}:${R}`});let y=c.groupEvent.message.message_sent_time,E=y?new Date(y*1e3):new Date,z=_.channel.reply.resolveEnvelopeFormatOptions(a),Q=_.channel.reply.formatAgentEnvelope({channel:"SeaTalk",from:p,timestamp:E,envelope:z,body:`${re}: ${x}`}),ie={groupId:i};u&&(ie.threadId=u),C&&(ie.quotedMessageId=C);let Ae=_.channel.reply.finalizeInboundContext({Body:Q,BodyForAgent:x,RawBody:x,CommandBody:x,From:`seatalk:${p}`,To:`group:${i}`,SessionKey:I.sessionKey,AccountId:I.accountId,ChatType:"group",SenderName:re,SenderId:p,Provider:"seatalk",Surface:"seatalk",MessageSid:R,MessageThreadId:u||void 0,Timestamp:y?y*1e3:Date.now(),WasMentioned:se,CommandAuthorized:!0,OriginatingChannel:"seatalk",OriginatingTo:`group:${i}`,Metadata:ie,...G});(h?.processingIndicator??"typing")==="typing"&&n.setGroupChatTyping(i,u).catch(O=>s(`seatalk[${r}]: group typing failed: ${String(O)}`));let fe=u||void 0,Z=h?.outboundCoalescing!==!1,le=O=>ae(n,i,O,1,fe),me=(O,P)=>_.channel.text.chunkMarkdownText(O,P),W=Z?Ue({send:le,chunkText:me,maxLength:Ce,joiner:`
|
|
15
|
+
|
|
16
|
+
`,idleFlushMs:vt}):null,ee=_.channel.reply.createReplyDispatcherWithTyping({humanDelay:_.channel.reply.resolveHumanDelayConfig(a,I.agentId),deliver:async O=>{let P=resolveSendableOutboundReplyParts(O);if(!(!P.hasText&&!P.hasMedia)){if(P.hasText)if(W)W.append(P.trimmedText);else {let te=me(P.trimmedText,Ce);for(let Ee of te)await le(Ee);}P.hasMedia&&(W&&await W.flush(),await Le({mediaUrls:P.mediaUrls,client:n,to:i,threadId:fe,isGroup:!0,log:s}));}},onError:O=>{l(`seatalk[${r}]: group reply delivery failed: ${String(O)}`);}}),he={agentId:I.agentId,...ee.replyOptions};s(`seatalk[${r}]: dispatching group message (session=${I.sessionKey})`);try{let{queuedFinal:O,counts:P}=await _.channel.reply.dispatchReplyFromConfig({ctx:Ae,cfg:a,dispatcher:ee.dispatcher,replyOptions:he});s(`seatalk[${r}]: group dispatch complete (queuedFinal=${O}, counts=${JSON.stringify(P)})`);}finally{ee.markDispatchIdle(),W&&(await ee.dispatcher.waitForIdle(),await W.flush());}}catch(_){l(`seatalk[${r}]: failed to dispatch group message: ${String(_)}`);}}var yn,Sn,wn,Y,Tt,_t,Tn,Ce,vt,_e,je=U(()=>{kt();N();St();Se();wt();ce();oe();yn=1800*1e3,Sn=1e3,wn=300*1e3,Y=new Map,Tt=Date.now();_t=1500,Tn=5e3,Ce=4e3,vt=1e3,_e=new Map;});var Pt={};ze(Pt,{connectSeaTalkRelay:()=>Rn});function xn(e,t){return new Promise(a=>{let n=()=>{clearTimeout(o),a();},o=setTimeout(()=>{t?.removeEventListener("abort",n),a();},e);t?.addEventListener("abort",n,{once:true});})}async function Et(e){let{cfg:t,account:a,relayUrl:n,runtime:o,abortSignal:r}=e,{accountId:s}=a,l=o?.log??console.log,c=o?.error??console.error,i=q(a.config)?.signingSecret;if(!a.appId||!a.appSecret||!i)throw new Error(`SeaTalk account "${s}" missing credentials for relay mode`);let g=F(a);if(!g)throw new Error(`SeaTalk client not available for account "${s}"`);let d=$t;for(;!r?.aborted;){try{await new Promise((p,f)=>{if(r?.aborted){p();return}l(`seatalk[${s}]: connecting to relay ${n}...`);let u=new En(n),S,h=()=>{S&&(clearTimeout(S),S=void 0);},w=()=>{h(),S=setTimeout(()=>{c(`seatalk[${s}]: relay silent for ${At}ms, terminating`),u.terminate();},At);},b=()=>{h(),u.close(),p();};r?.addEventListener("abort",b,{once:!0}),u.on("upgrade",v=>{v.socket.setKeepAlive(!0,6e4);}),u.on("open",()=>{w(),l(`seatalk[${s}]: relay connected, authenticating...`),u.send(JSON.stringify({type:"auth",appId:a.appId,appSecret:a.appSecret,signingSecret:i}));});let $=!1;u.on("message",v=>{w();let C;try{C=JSON.parse(String(v));}catch{c(`seatalk[${s}]: relay sent invalid JSON`);return}if(!$){C.type==="auth_ok"?($=!0,d=$t,l(`seatalk[${s}]: relay authenticated`)):C.type==="auth_fail"&&(c(`seatalk[${s}]: relay auth failed: ${C.error}`),u.close(),f(new Error(`Relay auth failed: ${C.error}`)));return}switch(C.type){case "event":C.event&&g&&ve({cfg:t,event:C.event,client:g,runtime:o,accountId:s});break;case "ping":u.send(JSON.stringify({type:"pong"}));break;case "replaced":l(`seatalk[${s}]: connection replaced by another instance`),u.close(),p();return;default:l(`seatalk[${s}]: unknown relay message type: ${C.type}`);}}),u.on("close",(v,C)=>{h(),r?.removeEventListener("abort",b),$&&l(`seatalk[${s}]: relay disconnected (code=${v}, reason=${String(C)})`),p();}),u.on("error",v=>{h(),r?.removeEventListener("abort",b),c(`seatalk[${s}]: relay connection error: ${String(v)}`),p();});});}catch(p){let f=String(p);if(f.includes("Relay auth failed"))throw p;c(`seatalk[${s}]: relay error: ${f}`);}if(r?.aborted)break;l(`seatalk[${s}]: reconnecting in ${d}ms...`),await xn(d,r),d=Math.min(d*Mn,Pn);}}async function Rn(e){let t=e.config;if(!t)throw new Error("Config is required for SeaTalk relay client");let a=e.runtime?.log??console.log;if(e.accountId){let o=A({cfg:t,accountId:e.accountId});if(!o.enabled||!o.configured)throw new Error(`SeaTalk account "${e.accountId}" not configured or disabled`);return Et({cfg:t,account:o,relayUrl:e.relayUrl,runtime:e.runtime,abortSignal:e.abortSignal})}let n=ne(t);if(n.length===0)throw new Error("No enabled SeaTalk accounts configured");a(`seatalk: connecting ${n.length} account(s) to relay: ${n.map(o=>o.accountId).join(", ")}`),await Promise.all(n.map(o=>Et({cfg:t,account:o,relayUrl:e.relayUrl,runtime:e.runtime,abortSignal:e.abortSignal})));}var $t,Pn,Mn,At,Mt=U(()=>{N();je();K();$t=1e3,Pn=3e4,Mn=2,At=75e3;});var Dt={};ze(Dt,{monitorSeaTalkProvider:()=>Ot});function On(e,t,a){let n=Buffer.from(t,"latin1"),o=$e.createHash("sha256").update(Buffer.concat([e,n])).digest("hex");try{return $e.timingSafeEqual(Buffer.from(o,"hex"),Buffer.from(a,"hex"))}catch{return false}}function Fn(e){return new Promise((t,a)=>{let n=0,o=[];e.on("data",r=>{if(n+=r.length,n>Dn){e.destroy(new Ie);return}o.push(r);}),e.on("end",()=>t(Buffer.concat(o))),e.on("error",a);})}async function xt(e){let{cfg:t,account:a,runtime:n,abortSignal:o}=e,{accountId:r}=a,s=n?.log??console.log,l=n?.error??console.error,c=a.webhookPort,i=a.webhookPath,g=q(a.config)?.signingSecret;if(!g)throw new Error(`SeaTalk account "${r}" missing signingSecret`);let d=F(a);if(!d)throw new Error(`SeaTalk client not available for account "${r}"`);s(`seatalk[${r}]: starting webhook server on port ${c}, path ${i}...`);let p=Rt.createServer();return p.on("request",async(f,u)=>{let S=new URL(f.url??"/",`http://localhost:${c}`).pathname;if(f.method!=="POST"||S!==i){u.writeHead(404),u.end("Not Found");return}try{let h=await Fn(f),w=f.headers.signature;if(!w||!On(h,g,w)){s(`seatalk[${r}]: signature verification failed`),u.writeHead(403),u.end("Forbidden");return}let b=JSON.parse(h.toString("utf-8"));if(b.event_type==="event_verification"){let $=b.event?.seatalk_challenge;u.writeHead(200,{"Content-Type":"application/json"}),u.end(JSON.stringify({seatalk_challenge:$})),s(`seatalk[${r}]: URL verification challenge responded`);return}u.writeHead(200),u.end("OK"),ve({cfg:t,event:b,client:d,runtime:n,accountId:r});}catch(h){l(`seatalk[${r}]: request processing error: ${String(h)}`),u.headersSent||(h instanceof Ie?(u.writeHead(413),u.end("Payload Too Large")):(u.writeHead(500),u.end("Internal Server Error")));}}),new Promise((f,u)=>{let S=()=>{p.close();},h=()=>{s(`seatalk[${r}]: abort signal received, stopping webhook server`),S(),f();};if(o?.aborted){S(),f();return}o?.addEventListener("abort",h,{once:true}),p.listen(c,()=>{s(`seatalk[${r}]: webhook server listening on port ${c}`);}),p.on("error",w=>{l(`seatalk[${r}]: webhook server error: ${w}`),o?.removeEventListener("abort",h),u(w);});})}async function Ot(e={}){let t=e.config;if(!t)throw new Error("Config is required for SeaTalk monitor");let a=e.runtime?.log??console.log;if(e.accountId){let o=A({cfg:t,accountId:e.accountId});if(!o.enabled||!o.configured)throw new Error(`SeaTalk account "${e.accountId}" not configured or disabled`);return xt({cfg:t,account:o,runtime:e.runtime,abortSignal:e.abortSignal})}let n=ne(t);if(n.length===0)throw new Error("No enabled SeaTalk accounts configured");a(`seatalk: starting ${n.length} account(s): ${n.map(o=>o.accountId).join(", ")}`),await Promise.all(n.map(o=>xt({cfg:t,account:o,runtime:e.runtime,abortSignal:e.abortSignal})));}var Dn,Ie,He=U(()=>{N();je();K();Dn=1024*1024,Ie=class extends Error{constructor(){super("Request body too large"),this.name="PayloadTooLargeError";}};});N();K();var Je=z.enum(["open","allowlist","pairing"]),Xe=z.enum(["webhook","relay"]),Ye=z.enum(["disabled","allowlist","open"]),Qe=z.enum(["typing","off"]),Vt=z.object({groupInfo:z.boolean().optional().default(true),groupHistory:z.boolean().optional().default(true),groupList:z.boolean().optional().default(true),threadHistory:z.boolean().optional().default(true),getMessage:z.boolean().optional().default(true)}).strict(),Jt=z.object({enabled:z.boolean().optional(),appId:z.string().optional(),appSecret:z.string().optional(),signingSecret:z.string().optional(),mode:Xe.optional(),relayUrl:z.string().optional(),webhookPort:z.number().int().positive().optional(),webhookPath:z.string().optional(),dmPolicy:Je.optional(),allowFrom:z.array(z.string()).optional(),groupPolicy:Ye.optional(),groupAllowFrom:z.array(z.string()).optional(),groupSenderAllowFrom:z.array(z.string()).optional(),processingIndicator:Qe.optional(),mediaAllowHosts:z.array(z.string()).optional(),outboundCoalescing:z.boolean().optional()}).strict(),Ze=z.object({enabled:z.boolean().optional(),appId:z.string().optional(),appSecret:z.string().optional(),signingSecret:z.string().optional(),mode:Xe.optional().default("webhook"),relayUrl:z.string().optional(),webhookPort:z.number().int().positive().optional().default(8080),webhookPath:z.string().optional().default("/callback"),dmPolicy:Je.optional().default("allowlist"),allowFrom:z.array(z.string()).optional(),groupPolicy:Ye.optional().default("disabled"),groupAllowFrom:z.array(z.string()).optional(),groupSenderAllowFrom:z.array(z.string()).optional(),processingIndicator:Qe.optional().default("typing"),mediaAllowHosts:z.array(z.string()).optional(),outboundCoalescing:z.boolean().optional().default(true),tools:Vt.optional(),accounts:z.record(z.string(),Jt.optional()).optional()}).strict().superRefine((e,t)=>{e.dmPolicy==="open"&&((e.allowFrom??[]).some(o=>o.trim()==="*")||t.addIssue({code:z.ZodIssueCode.custom,path:["allowFrom"],message:'channels.seatalk.dmPolicy="open" requires channels.seatalk.allowFrom to include "*"'}));});N();K();ce();oe();var rt="group:";function st(e){let t=e.trim();return t||null}function Te(e){return e.startsWith(rt)}function be(e){return e.slice(rt.length)}function X(e){return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.trim())}function it(e){let t=e.trim();return t?X(t)?true:Te(t)?be(t).length>0:/^[a-zA-Z0-9_-]+$/.test(t):false}function lt(e,t){let a=A({cfg:e,accountId:t}),n=F(a);if(!n)throw new Error(`SeaTalk client not available for account ${a.accountId}`);return n}async function ct(e,t){if(!X(t))return t;let n=(await e.getEmployeeCodeByEmail([t])).find(o=>o.employeeCode&&o.status===2);if(n?.employeeCode)return n.employeeCode;throw new Error(`No active SeaTalk employee found for email: ${t}`)}function dt(e){if(e!=null)return String(e)}var ut={deliveryMode:"direct",chunker:(e,t)=>V().channel.text.chunkMarkdownText(e,t),chunkerMode:"markdown",textChunkLimit:4e3,sendText:async({cfg:e,to:t,text:a,accountId:n,threadId:o})=>{let r=lt(e,n??void 0),s=dt(o);if(Te(t)){let c=be(t);return await ae(r,c,a,1,s),{channel:"seatalk",messageId:"",chatId:t}}let l=await ct(r,t);return await H(r,l,a,1,s),{channel:"seatalk",messageId:"",chatId:l}},sendMedia:async({cfg:e,to:t,text:a,mediaUrl:n,accountId:o,threadId:r})=>{let s=lt(e,o??void 0),l=dt(r),c=Te(t),i=c?be(t):await ct(s,t);if(a?.trim()&&(c?await ae(s,i,a,1,l):await H(s,i,a,1,l)),n)try{await we({client:s,to:i,mediaUrl:n,threadId:l,isGroup:c});}catch(g){let d=`[Media send failed: ${g instanceof Error?g.message:String(g)}]`;c?await ae(s,i,d,2,l):await H(s,i,d,2,l);}return {channel:"seatalk",messageId:"",chatId:c?t:i}}};K();async function ue(e){if(!e?.appId||!e?.appSecret)return {ok:false,error:"missing credentials (appId, appSecret)"};try{let t=xe(e.appId,e.appSecret),a=Date.now();await t.getAccessToken();let n=Date.now()-a;return {ok:!0,appId:e.appId,latencyMs:n}}catch(t){return {ok:false,appId:e.appId,error:t instanceof Error?t.message:String(t)}}}oe();N();var pt="seatalk";function Fe(e){return e.split(/[\n,;]+/g).map(t=>t.trim()).filter(Boolean)}async function ft(e){let{cfg:t,prompter:a}=e,n=t.channels?.seatalk?.allowFrom??[];for(await a.note(["Allowlist SeaTalk DMs by email or employee_code.","Examples:","- alice@company.com","- 12345678"].join(`
|
|
17
|
+
`),"SeaTalk allowlist");;){let o=await a.text({message:"SeaTalk allowFrom (emails or employee_codes)",placeholder:"alice@company.com, 12345678",initialValue:n[0]?String(n[0]):void 0,validate:l=>String(l??"").trim()?void 0:"Required"}),r=Fe(String(o));if(r.length===0){await a.note("Enter at least one user.","SeaTalk allowlist");continue}let s=mergeAllowFromEntries(n.map(l=>String(l)),r);return {...t,channels:{...t.channels,seatalk:{...t.channels?.seatalk,allowFrom:s}}}}}var cn=createTopLevelChannelDmPolicy({label:"SeaTalk",channel:pt,policyKey:"channels.seatalk.dmPolicy",allowFromKey:"channels.seatalk.allowFrom",getCurrent:e=>e.channels?.seatalk?.dmPolicy??"allowlist",promptAllowFrom:async({cfg:e,prompter:t})=>ft({cfg:e,prompter:t})});async function gt(e){let t=String(await e.text({message:"Enter SeaTalk App ID",validate:o=>o?.trim()?void 0:"Required"})).trim(),a=String(await e.text({message:"Enter SeaTalk App Secret",validate:o=>o?.trim()?void 0:"Required"})).trim(),n=String(await e.text({message:"Enter SeaTalk Signing Secret",validate:o=>o?.trim()?void 0:"Required"})).trim();return {appId:t,appSecret:a,signingSecret:n}}async function dn(e){await e.note(["1) Go to SeaTalk Open Platform (open.seatalk.io)","2) Create a Bot App","3) Get App ID and App Secret from Basic Info & Credentials","4) Get Signing Secret from Event Callback settings","5) Enable Bot capability and set status to Online",'6) Enable "Send Message to Bot User" permission'].join(`
|
|
18
|
+
`),"SeaTalk credentials");}var mt={channel:pt,status:createStandardChannelSetupStatus({channelLabel:"SeaTalk",configuredLabel:"configured",unconfiguredLabel:"needs app credentials",configuredHint:"configured",unconfiguredHint:"needs app creds",configuredScore:2,unconfiguredScore:0,resolveConfigured:({cfg:e})=>{let t=e.channels?.seatalk;return !!q(t)}}),credentials:[],finalize:async({cfg:e,prompter:t,forceAllowFrom:a})=>{let n=e,o=n.channels?.seatalk,r=q(o),s=!!(o?.appId?.trim()&&o?.appSecret?.trim()&&o?.signingSecret?.trim()),l=null,c=null,i=null;if(r||await dn(t),s?await t.confirm({message:"SeaTalk credentials already configured. Keep them?",initialValue:true})||({appId:l,appSecret:c,signingSecret:i}=await gt(t)):{appId:l,appSecret:c,signingSecret:i}=await gt(t),l&&c&&i){n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,enabled:true,appId:l,appSecret:c,signingSecret:i,dmPolicy:o?.dmPolicy??"allowlist"}}};try{let h=await ue({appId:l,appSecret:c});h.ok?await t.note(`Connected successfully (latency: ${h.latencyMs}ms)`,"SeaTalk connection test"):await t.note(`Connection failed: ${h.error??"unknown error"}`,"SeaTalk connection test");}catch(h){await t.note(`Connection test failed: ${String(h)}`,"SeaTalk connection test");}await t.note(["Important reminders:",'- Bot App must be set to "Online" status in SeaTalk Open Platform','- "Send Message to Bot User" permission must be enabled',"- Configure the callback URL in Event Callback settings"].join(`
|
|
19
|
+
`),"SeaTalk setup");}let g=n.channels?.seatalk?.mode??"webhook",d=await t.select({message:"Gateway mode",options:[{value:"webhook",label:"Webhook \u2014 receive event callbacks directly (default)"},{value:"relay",label:"Relay \u2014 connect to a relay service as client"}],initialValue:g}),p=String(d);if(n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,mode:p}}},p==="relay"){let h=n.channels?.seatalk?.relayUrl??"",w=await t.text({message:"Relay WebSocket URL",placeholder:"wss://relay.example.com/ws",initialValue:h||void 0,validate:$=>{let v=String($??"").trim();if(!v)return "Required";if(!v.startsWith("ws://")&&!v.startsWith("wss://"))return "Must be a ws:// or wss:// URL"}}),b=String(w).trim();b.startsWith("ws://")&&await t.note("ws:// transmits credentials (appSecret, signingSecret) unencrypted. Consider using wss:// for production.","Security warning"),n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,relayUrl:b}}};}else {let h=n.channels?.seatalk?.webhookPort??8080,w=await t.text({message:"Webhook port",initialValue:String(h),validate:M=>{let G=Number(M);return G>0&&G<65536?void 0:"Enter a valid port number (1-65535)"}}),b=Number(w);b&&b!==h&&(n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,webhookPort:b}}});let $=n.channels?.seatalk?.webhookPath??"/callback",v=await t.text({message:"Webhook path",initialValue:$,validate:M=>{let G=String(M??"").trim();if(!G)return "Required";if(!G.startsWith("/"))return "Path must start with /"}}),C=String(v??$).trim();C&&C!==$&&(n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,webhookPath:C}}});}let f=await t.select({message:"Group chat policy",options:[{value:"disabled",label:"Disabled \u2014 ignore all group messages (default)"},{value:"allowlist",label:"Allowlist \u2014 respond only in specific groups"},{value:"open",label:"Open \u2014 respond in all groups the bot joins"}],initialValue:n.channels?.seatalk?.groupPolicy??"disabled"}),u=String(f);if(n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,groupPolicy:u}}},u==="allowlist"){let h=n.channels?.seatalk?.groupAllowFrom??[],w=await t.text({message:"Allowed group IDs (comma-separated)",placeholder:"group_abc123, group_def456",initialValue:h.length>0?h.join(", "):void 0,validate:b=>String(b??"").trim()?void 0:"Enter at least one group ID"});n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,groupAllowFrom:Fe(String(w))}}};}if(u!=="disabled"&&await t.confirm({message:"Restrict which users can trigger the bot in groups? (sender allowlist)",initialValue:true})){let w=n.channels?.seatalk?.groupSenderAllowFrom??[],b=await t.text({message:"Sender allowlist (emails or employee_codes, comma-separated)",placeholder:"alice@company.com, 12345678",initialValue:w.length>0?w.join(", "):void 0,validate:$=>String($??"").trim()?void 0:"Enter at least one user"});n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,groupSenderAllowFrom:Fe(String(b))}}};}let S=await t.select({message:"Processing indicator",options:[{value:"typing",label:"Typing \u2014 show typing status while processing (default)"},{value:"off",label:"Off \u2014 no processing indicator"}],initialValue:n.channels?.seatalk?.processingIndicator??"typing"});return n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,processingIndicator:String(S)}}},a&&(n=await ft({cfg:n,prompter:t})),{cfg:n}},dmPolicy:cn,disable:e=>({...e,channels:{...e.channels,seatalk:{...e.channels?.seatalk,enabled:false}}})};var Gn={id:"seatalk",label:"SeaTalk",selectionLabel:"SeaTalk (plugin)",blurb:"SeaTalk internal messaging integration.",docsPath:"/channels/seatalk",aliases:[],order:70,quickstartAllowFrom:true},qe={id:"seatalk",meta:Gn,pairing:{idLabel:"employeeCode",normalizeAllowEntry:createPairingPrefixStripper(/^(seatalk|st):/i),notifyApproval:async({cfg:e,id:t})=>{let a=Pe(e),n=A({cfg:e,accountId:a}),o=F(n);o&&await H(o,t,PAIRING_APPROVED_MESSAGE,1);}},capabilities:{chatTypes:["direct","group"],polls:false,threads:true,media:true,reactions:false,edit:false,reply:false},reload:{configPrefixes:["channels.seatalk"]},configSchema:buildChannelConfigSchema(Ze),config:{listAccountIds:e=>ye(e),resolveAccount:(e,t)=>A({cfg:e,accountId:t}),defaultAccountId:e=>Pe(e),setAccountEnabled:({cfg:e,accountId:t,enabled:a})=>{if(t===DEFAULT_ACCOUNT_ID)return {...e,channels:{...e.channels,seatalk:{...e.channels?.seatalk,enabled:a}}};let o=e.channels?.seatalk;return {...e,channels:{...e.channels,seatalk:{...o,accounts:{...o?.accounts,[t]:{...o?.accounts?.[t],enabled:a}}}}}},deleteAccount:({cfg:e,accountId:t})=>{if(t===DEFAULT_ACCOUNT_ID){let r={...e},s={...e.channels};s.seatalk=void 0;let l=Object.values(s).some(c=>c!==void 0);return r.channels=l?s:void 0,r}let n=e.channels?.seatalk,o={...n?.accounts};return delete o[t],{...e,channels:{...e.channels,seatalk:{...n,accounts:Object.keys(o).length>0?o:void 0}}}},isConfigured:e=>e.configured,describeAccount:e=>({accountId:e.accountId,enabled:e.enabled,configured:e.configured,appId:e.appId,mode:e.mode,...e.mode==="relay"?{relayUrl:e.relayUrl}:{webhookPort:e.webhookPort}}),resolveAllowFrom:({cfg:e,accountId:t})=>(A({cfg:e,accountId:t}).config?.allowFrom??[]).map(n=>String(n)),formatAllowFrom:({allowFrom:e})=>e.map(t=>String(t).trim()).filter(Boolean)},security:{collectWarnings:({cfg:e,accountId:t})=>{let a=A({cfg:e,accountId:t});return (a.config?.dmPolicy??"allowlist")!=="open"?[]:[`- SeaTalk[${a.accountId}]: dmPolicy="open" allows any subscriber to message the bot. Set channels.seatalk.dmPolicy to "allowlist" or "pairing" to restrict senders.`]}},setup:{resolveAccountId:()=>DEFAULT_ACCOUNT_ID,applyAccountConfig:({cfg:e,accountId:t})=>{if(!t||t===DEFAULT_ACCOUNT_ID)return {...e,channels:{...e.channels,seatalk:{...e.channels?.seatalk,enabled:true}}};let n=e.channels?.seatalk;return {...e,channels:{...e.channels,seatalk:{...n,accounts:{...n?.accounts,[t]:{...n?.accounts?.[t],enabled:true}}}}}}},setupWizard:mt,messaging:{normalizeTarget:e=>st(e)??void 0,targetResolver:{looksLikeId:it,hint:"<employee_code> or <email>"}},resolver:{resolveTargets:async({cfg:e,accountId:t,inputs:a})=>{let n=a.filter(c=>X(c));if(n.length===0)return a.map(c=>({input:c,resolved:true,id:c}));let o=c=>a.map(i=>{let g=X(i);return {input:i,resolved:!g,id:g?void 0:i,note:g?c:void 0}}),r=A({cfg:e,accountId:t}),s=F(r);if(!s)return o("SeaTalk client not available");let l=new Map;try{let c=await s.getEmployeeCodeByEmail(n);for(let i of c)i.employeeCode&&i.status===2&&l.set(i.email.toLowerCase(),i.employeeCode);}catch{return o("Failed to resolve email")}return a.map(c=>{if(!X(c))return {input:c,resolved:true,id:c};let i=l.get(c.toLowerCase());return i?{input:c,resolved:true,id:i,name:c}:{input:c,resolved:false,note:"No active employee found for this email"}})}},outbound:ut,status:{defaultRuntime:{accountId:DEFAULT_ACCOUNT_ID,running:false,lastStartAt:null,lastStopAt:null,lastError:null,port:null},buildChannelSummary:({snapshot:e})=>({configured:e.configured??false,running:e.running??false,lastStartAt:e.lastStartAt??null,lastStopAt:e.lastStopAt??null,lastError:e.lastError??null,port:e.port??null,probe:e.probe,lastProbeAt:e.lastProbeAt??null}),probeAccount:({account:e})=>ue(e),buildAccountSnapshot:({account:e,runtime:t,probe:a})=>({accountId:e.accountId,enabled:e.enabled,configured:e.configured,appId:e.appId,mode:e.mode,...e.mode==="relay"?{relayUrl:e.relayUrl}:{webhookPort:e.webhookPort},running:t?.running??false,lastStartAt:t?.lastStartAt??null,lastStopAt:t?.lastStopAt??null,lastError:t?.lastError??null,port:t?.port??null,probe:a})},gateway:{startAccount:async e=>{let t=A({cfg:e.cfg,accountId:e.accountId});if(t.mode==="relay"){if(!t.relayUrl)throw new Error(`SeaTalk account "${e.accountId}" mode=relay but relayUrl is not configured`);e.setStatus({accountId:e.accountId,mode:"relay"}),e.log?.info(`starting seatalk[${e.accountId}] (relay client \u2192 ${t.relayUrl})`);let{connectSeaTalkRelay:o}=await Promise.resolve().then(()=>(Mt(),Pt));return o({config:e.cfg,runtime:e.runtime,abortSignal:e.abortSignal,accountId:e.accountId,relayUrl:t.relayUrl})}e.setStatus({accountId:e.accountId,port:t.webhookPort}),e.log?.info(`starting seatalk[${e.accountId}] (webhook on port ${t.webhookPort})`);let{monitorSeaTalkProvider:n}=await Promise.resolve().then(()=>(He(),Dt));return n({config:e.cfg,runtime:e.runtime,abortSignal:e.abortSignal,accountId:e.accountId})}}};ce();N();K();var Ft="Pagination cursor. Omit for the first request to get the latest messages. To fetch older messages, pass the next_cursor value from the previous response.",Bt=Type.Union([Type.Object({action:Type.Literal("group_history",{description:"Get group chat message history (requires 'Get Chat History' app permission). Messages are returned in chronological order (oldest to newest). The first page (no cursor) contains the most recent messages. Use next_cursor from the response to fetch older pages."}),group_id:Type.String({description:"Group chat ID"}),page_size:Type.Optional(Type.Number({description:"Page size (1-100, default 50)"})),cursor:Type.Optional(Type.String({description:Ft}))}),Type.Object({action:Type.Literal("group_info",{description:"Get group chat details"}),group_id:Type.String({description:"Group chat ID"})}),Type.Object({action:Type.Literal("group_list",{description:"List groups the bot has joined"}),page_size:Type.Optional(Type.Number({description:"Page size (1-100, default 50)"})),cursor:Type.Optional(Type.String({description:"Pagination cursor. Omit for the first request. To fetch more groups, pass the next_cursor value from the previous response."}))}),Type.Object({action:Type.Literal("thread_history",{description:"Get thread messages in chronological order (oldest to newest). The first page (no cursor) contains the most recent replies. Use next_cursor to fetch older replies."}),thread_id:Type.String({description:"Thread ID"}),group_id:Type.Optional(Type.String({description:"Group chat ID (provide for group thread, omit for DM thread)"})),employee_code:Type.Optional(Type.String({description:"Employee code (required for DM thread when group_id is omitted)"})),page_size:Type.Optional(Type.Number({description:"Page size (1-100, default 50)"})),cursor:Type.Optional(Type.String({description:Ft}))}),Type.Object({action:Type.Literal("get_message",{description:"Get a message by its ID. Can resolve any message_id or quoted_message_id."}),message_id:Type.String({description:"The message ID to retrieve"})})]);function D(e){return {content:[{type:"text",text:JSON.stringify(e,null,2)}],details:e}}function jn(e){return {groupInfo:e?.groupInfo??true,groupHistory:e?.groupHistory??true,groupList:e?.groupList??true,threadHistory:e?.threadHistory??true,getMessage:e?.getMessage??true}}async function Lt(e,t,a){for(let n of t){if(!n||typeof n!="object")continue;let o=n.quoted_message_id;if(!(!o||typeof o!="string"))try{let r=await e.getMessageByMessageId(o);n.quoted_message=r;}catch(r){a?.(`seatalk tool: failed to resolve quoted message ${o}: ${String(r)}`),n.quoted_message=null;}}}function Ut(e,t){let a=e[t];if(Array.isArray(a)){let n=a.toReversed();return e[t]=n,n}return []}function Gt(e){if(!e.config){e.logger.debug?.("seatalk tool: No config available, skipping");return}let t=ne(e.config);if(t.length===0){e.logger.debug?.("seatalk tool: No enabled SeaTalk accounts, skipping");return}let a=t[0],n=jn(a.tools);if(!(n.groupInfo||n.groupHistory||n.groupList||n.threadHistory||n.getMessage)){e.logger.debug?.("seatalk tool: All actions disabled, skipping");return}let r=s=>e.logger.warn?.(s);e.registerTool(s=>{let l=s.agentAccountId,c=()=>{if(l){let i=A({cfg:e.config,accountId:l});if(i.configured)return F(i)}return F(a)};return {name:"seatalk",label:"SeaTalk",description:"SeaTalk operations. Actions: group_history (group chat messages, chronological order), group_info (group details), group_list (joined groups), thread_history (thread messages, chronological order), get_message (retrieve a single message by ID). History and thread results include resolved quoted_message for messages that quote another message.",parameters:Bt,async execute(i,g){let d=g;try{let p=c();if(!p)return D({error:`SeaTalk client not available${l?` for account ${l}`:""}`});switch(d.action){case "group_history":{if(!n.groupHistory)return D({error:"groupHistory is disabled in config"});let f=await p.getGroupChatHistory(d.group_id,{pageSize:d.page_size,cursor:d.cursor}),u=Ut(f,"group_chat_messages");return await Lt(p,u,r),D(f)}case "group_info":return n.groupInfo?D(await p.getGroupChatInfo(d.group_id)):D({error:"groupInfo is disabled in config"});case "group_list":return n.groupList?D(await p.getJoinedGroupChats({pageSize:d.page_size,cursor:d.cursor})):D({error:"groupList is disabled in config"});case "thread_history":{if(!n.threadHistory)return D({error:"threadHistory is disabled in config"});let f;if(d.group_id)f=await p.getGroupThread(d.group_id,d.thread_id,{pageSize:d.page_size,cursor:d.cursor});else {if(!d.employee_code)return D({error:"employee_code is required for DM thread (when group_id is absent)"});f=await p.getDmThread(d.employee_code,d.thread_id,{pageSize:d.page_size,cursor:d.cursor});}let u=Ut(f,"thread_messages");return await Lt(p,u,r),D(f)}case "get_message":return n.getMessage?D(await p.getMessageByMessageId(d.message_id)):D({error:"getMessage is disabled in config"});default:return D({error:`Unknown action: ${String(d.action)}`})}}catch(p){return D({error:p instanceof Error?p.message:String(p)})}}}},{name:"seatalk"}),e.logger.info?.("seatalk tool: Registered");}oe();He();var so=defineChannelPluginEntry({id:"openclaw-seatalk",name:"SeaTalk",description:"SeaTalk channel plugin",plugin:qe,setRuntime:et,registerFull:e=>Gt(e)});
|
|
20
|
+
export{so as default,Ot as monitorSeaTalkProvider,ue as probeSeaTalk,qe as seatalkPlugin,ot as sendFileMessage,at as sendImageMessage,H as sendTextMessage};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {buildChannelConfigSchema,defineSetupPluginEntry,DEFAULT_ACCOUNT_ID,normalizeAccountId,PAIRING_APPROVED_MESSAGE}from'openclaw/plugin-sdk/core';import*as L from'fs';import*as Xe from'os';import*as W from'path';import {resolveSendableOutboundReplyParts,sendMediaWithLeadingCaption}from'openclaw/plugin-sdk/reply-payload';import {createPairingPrefixStripper,createChannelPairingController}from'openclaw/plugin-sdk/channel-pairing';import {resolveDmGroupAccessWithLists,DM_GROUP_ACCESS_REASON}from'openclaw/plugin-sdk/channel-policy';import wn from'ws';import*as _e from'crypto';import*as $t from'http';import {z as z$1}from'zod';import {createTopLevelChannelDmPolicy,createStandardChannelSetupStatus,mergeAllowFromEntries}from'openclaw/plugin-sdk/setup';var xt=Object.defineProperty;var D=(e,t)=>()=>(e&&(t=e(e=0)),t);var je=(e,t)=>{for(var a in t)xt(e,a,{get:t[a],enumerable:true});};function Ot(e){let t=e.channels?.seatalk?.accounts;return !t||typeof t!="object"?[]:Object.keys(t).filter(Boolean)}function pe(e){let t=Ot(e);return t.length===0?[DEFAULT_ACCOUNT_ID]:[...t].toSorted((a,n)=>a.localeCompare(n))}function Ee(e){let t=pe(e);return t.includes(DEFAULT_ACCOUNT_ID)?DEFAULT_ACCOUNT_ID:t[0]??DEFAULT_ACCOUNT_ID}function Ft(e,t){let a=e.channels?.seatalk?.accounts;if(!(!a||typeof a!="object"))return a[t]}function Dt(e,t){let a=e.channels?.seatalk,{accounts:n,...o}=a??{},r=Ft(e,t)??{};return {...o,...r}}function G(e){let t=e?.appId?.trim(),a=e?.appSecret?.trim(),n=e?.signingSecret?.trim();return !t||!a||!n?null:{appId:t,appSecret:a,signingSecret:n}}function A(e){let t=normalizeAccountId(e.accountId),n=e.cfg.channels?.seatalk?.enabled!==false,o=Dt(e.cfg,t),r=o.enabled!==false,s=n&&r,l=G(o);return {accountId:t,enabled:s,configured:!!l,appId:l?.appId,appSecret:l?.appSecret,mode:o.mode??"webhook",relayUrl:o.relayUrl,webhookPort:o.webhookPort??8080,webhookPath:o.webhookPath??"/callback",tools:o.tools,config:o}}function fe(e){return pe(e).map(t=>A({cfg:e,accountId:t})).filter(t=>t.enabled&&t.configured)}var q=D(()=>{});function Pe(e,t){let a=`${e}:${t}`,n=He.get(a);return n||(n=new Ae(e,t),He.set(a,n)),n}function U(e){return !e.appId||!e.appSecret?null:Pe(e.appId,e.appSecret)}var Ge,Ne,Ae,He,Z=D(()=>{Ge="https://openapi.seatalk.io",Ne=[1e4,6e4],Ae=class{appId;appSecret;tokenInfo=null;tokenPromise=null;constructor(t,a){this.appId=t,this.appSecret=a;}async getAccessToken(){if(this.tokenInfo){let t=Math.floor(Date.now()/1e3);if(this.tokenInfo.expireAt-t>600)return this.tokenInfo.token}return (await this.refreshToken()).token}async refreshToken(){if(this.tokenPromise)return this.tokenPromise;this.tokenPromise=this._fetchToken();try{let t=await this.tokenPromise;return this.tokenInfo=t,t}finally{this.tokenPromise=null;}}async _fetchToken(){let t=new AbortController,a=setTimeout(()=>t.abort(),1e4);try{let n=await fetch(`${Ge}/auth/app_access_token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:this.appId,app_secret:this.appSecret}),signal:t.signal});if(!n.ok){let r=n.headers.get("x-rid")??void 0;throw new Error(`SeaTalk token request failed: HTTP ${n.status} (x-rid: ${r})`)}let o=await n.json();if(o.code!==0)throw new Error(`SeaTalk token error: code=${o.code} message=${o.message??"unknown"}`);if(!o.app_access_token||!o.expire)throw new Error("SeaTalk token response missing token or expire");return {token:o.app_access_token,expireAt:o.expire}}finally{clearTimeout(a);}}async apiCall(t,a,n,o=true,r=0){let s=await this.getAccessToken(),l=new AbortController,c=setTimeout(()=>l.abort(),1e4),i;try{let u=await fetch(`${Ge}${a}`,{method:t,headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:n?JSON.stringify(n):void 0,signal:l.signal});if(i=u.headers.get("x-rid")??void 0,!u.ok)throw Object.assign(new Error(`SeaTalk API error: HTTP ${u.status} (x-rid: ${i})`),{httpStatus:u.status,xRid:i});let m=await u.json();if(m.code===100&&o)return await this.refreshToken(),this.apiCall(t,a,n,!1);if(m.code===101){if(r<Ne.length){let h=Ne[r];return await new Promise(f=>setTimeout(f,h)),this.apiCall(t,a,n,o,r+1)}throw Object.assign(new Error(`SeaTalk rate limit exceeded after ${r+1} attempts (x-rid: ${i})`),{code:101,xRid:i})}if(m.code!==0)throw Object.assign(new Error(`SeaTalk API error: code=${m.code} message=${m.message??"unknown"} (x-rid: ${i})`),{code:m.code,xRid:i});return m}catch(u){throw i&&u instanceof Error&&!u.message.includes("x-rid:")&&(u.message+=` (x-rid: ${i})`),u}finally{clearTimeout(c);}}async sendSingleChat(t,a,n){let o=n?{...a,thread_id:n}:a;await this.apiCall("POST","/messaging/v2/single_chat",{employee_code:t,message:o});}async sendGroupChat(t,a,n){let o=n?{...a,thread_id:n}:a;await this.apiCall("POST","/messaging/v2/group_chat",{group_id:t,message:o});}async setSingleChatTyping(t,a){let n={employee_code:t};a&&(n.thread_id=a),await this.apiCall("POST","/messaging/v2/single_chat_typing",n);}async setGroupChatTyping(t,a){let n={group_id:t};a&&(n.thread_id=a),await this.apiCall("POST","/messaging/v2/group_chat_typing",n);}async downloadMedia(t){let a=await this.getAccessToken(),n=new AbortController,o=setTimeout(()=>n.abort(),6e4);try{let r=await fetch(t,{headers:{Authorization:`Bearer ${a}`},signal:n.signal});if(!r.ok)throw new Error(`SeaTalk media download failed: HTTP ${r.status}`);let s=r.headers.get("content-type")??"application/octet-stream",l=await r.arrayBuffer();return {buffer:Buffer.from(l),contentType:s}}finally{clearTimeout(o);}}async getEmployeeCodeByEmail(t){let n=[];for(let o=0;o<t.length;o+=500){let r=t.slice(o,o+500),s=await this.apiCall("POST","/contacts/v2/get_employee_code_with_email",{emails:r});for(let l of s.employees??[])n.push({email:l.email,employeeCode:l.employee_code,status:l.employee_status});}return n}async getGroupChatHistory(t,a){let n=new URLSearchParams({group_id:t,page_size:String(a?.pageSize??50)});return a?.cursor&&n.set("cursor",a.cursor),this.apiCall("GET",`/messaging/v2/group_chat/history?${n}`)}async getJoinedGroupChats(t){let a=new URLSearchParams;t?.pageSize&&a.set("page_size",String(t.pageSize)),t?.cursor&&a.set("cursor",t.cursor);let n=a.toString();return this.apiCall("GET",`/messaging/v2/group_chat/joined${n?`?${n}`:""}`)}async getGroupChatInfo(t){return this.apiCall("GET",`/messaging/v2/group_chat/info?group_id=${encodeURIComponent(t)}`)}async getDmThread(t,a,n){let o=new URLSearchParams({employee_code:t,thread_id:a});return n?.pageSize&&o.set("page_size",String(n.pageSize)),n?.cursor&&o.set("cursor",n.cursor),this.apiCall("GET",`/messaging/v2/single_chat/get_thread_by_thread_id?${o}`)}async getGroupThread(t,a,n){let o=new URLSearchParams({group_id:t,thread_id:a});return n?.pageSize&&o.set("page_size",String(n.pageSize)),n?.cursor&&o.set("cursor",n.cursor),this.apiCall("GET",`/messaging/v2/group_chat/get_thread_by_thread_id?${o}`)}async getMessageByMessageId(t){return this.apiCall("GET",`/messaging/v2/get_message_by_message_id?message_id=${encodeURIComponent(t)}`)}getAppId(){return this.appId}},He=new Map;});function z(){if(!Je)throw new Error("SeaTalk runtime not initialized");return Je}var Je,me=D(()=>{Je=null;});function qt(e){let t=e&&e.length>0?e:[...Ht];return new Set(t.map(a=>a.trim().toLowerCase()).filter(Boolean))}function zt(e,t){let a;try{a=new URL(e);}catch{return {ok:false,detail:"invalid URL"}}if(a.protocol!=="https:")return {ok:false,detail:`only https allowed (got ${a.protocol})`};let n=a.hostname.toLowerCase();return t.has(n)?{ok:true,hostname:n}:{ok:false,detail:`host not in allowlist (${n})`}}async function re(e){let{message:t,client:a,log:n,mediaAllowHosts:o}=e,r=z(),s=qt(o),l,c,i=Me.file;switch(t.tag){case "image":l=t.image?.content,i=Me.image;break;case "file":l=t.file?.content,c=t.file?.filename;break;case "video":l=t.video?.content,i=Me.video;break;default:return null}if(!l)return null;let u=zt(l,s);if(!u.ok)return n?.(`seatalk: rejected inbound ${t.tag} media before download: ${u.detail}`),null;n?.(`seatalk: inbound ${t.tag} media url host=${u.hostname}`);let m=1;for(let h=0;h<=m;h++)try{let f=await a.downloadMedia(l),d=f.contentType;if((!d||d==="application/octet-stream")&&f.buffer.length<Gt){let p=await r.media.detectMime({buffer:f.buffer});p&&(d=p);}let y=await r.channel.media.saveMediaBuffer(f.buffer,d??"application/octet-stream","inbound",Nt,c);return n?.(`seatalk: downloaded ${t.tag} media, saved to ${y.path}`),{path:y.path,contentType:y.contentType,filename:c,placeholder:i}}catch(f){if(h<m){n?.(`seatalk: retry ${h+1}/${m} downloading ${t.tag} media: ${String(f)}`);continue}return n?.(`seatalk: failed to download ${t.tag} media after ${m+1} attempts: ${String(f)}`),null}return null}async function Wt(e){let t=new AbortController,a=setTimeout(()=>t.abort(),3e4);try{let n=await fetch(e,{signal:t.signal});if(!n.ok)throw new Error(`Failed to fetch media from ${e}: HTTP ${n.status}`);let o=await n.arrayBuffer(),r=Buffer.from(o),s=new URL(e).pathname;return {buffer:r,name:W.basename(s)||"file"}}finally{clearTimeout(a);}}function Kt(e){let t=e.startsWith("~")?W.join(Xe.homedir(),e.slice(1)):e.replace(/^file:\/\//,"");if(!L.existsSync(t))throw new Error(`Media file not found: ${t}`);let a=L.openSync(t,"r");try{let{size:n}=L.fstatSync(a),o=Buffer.alloc(n),r=0;for(;r<n;){let l=L.readSync(a,o,r,n-r,r);if(l===0)break;r+=l;}return {buffer:r<n?o.subarray(0,r):o,name:W.basename(t)}}finally{L.closeSync(a);}}async function Ye(e){let t=e.startsWith("http://")||e.startsWith("https://"),{buffer:a,name:n}=t?await Wt(e):Kt(e);if(a.length>jt)throw new Error(`Media file too large: ${(a.length/1024/1024).toFixed(1)}MB exceeds ~3.75MB limit`);let o=W.extname(n).toLowerCase(),r=Ut.has(o)?"image":"file";return {base64:a.toString("base64"),sendAs:r,filename:r==="file"?n.slice(0,100):void 0}}function xe(e){let t=e[0],a=e.map(o=>o.path),n=e.map(o=>o.contentType).filter(Boolean);return {MediaPath:t?.path,MediaType:t?.contentType,MediaUrl:t?.path,MediaPaths:a.length>0?a:void 0,MediaUrls:a.length>0?a:void 0,MediaTypes:n.length>0?n:void 0}}var Me,Ut,jt,Gt,Nt,Ht,he=D(()=>{me();Me={image:"<media:image>",file:"<media:document>",video:"<media:video>"},Ut=new Set([".png",".jpg",".jpeg",".gif"]),jt=3.75*1024*1024,Gt=10*1024*1024,Nt=250*1024*1024,Ht=["openapi.seatalk.io"];});async function j(e,t,a,n=1,o){await e.sendSingleChat(t,{tag:"text",text:{format:n,content:a}},o);}async function Vt(e,t,a,n){await e.sendSingleChat(t,{tag:"image",image:{content:a}},n);}async function Jt(e,t,a,n,o){await e.sendSingleChat(t,{tag:"file",file:{content:a,filename:n}},o);}async function ee(e,t,a,n=1,o){await e.sendGroupChat(t,{tag:"text",text:{format:n,content:a}},o);}async function ke(e){let{client:t,to:a,mediaUrl:n,threadId:o,isGroup:r}=e,s=await Ye(n);s&&(r?s.sendAs==="image"?await t.sendGroupChat(a,{tag:"image",image:{content:s.base64}},o):await t.sendGroupChat(a,{tag:"file",file:{content:s.base64,filename:s.filename||"file"}},o):s.sendAs==="image"?await Vt(t,a,s.base64,o):await Jt(t,a,s.base64,s.filename||"file",o));}var se=D(()=>{he();});function ct(e){let{groupPolicy:t,groupAllowFrom:a,groupSenderAllowFrom:n,groupId:o,senderEmployeeCode:r,senderEmail:s}=e;return t==="disabled"?{allowed:false,reason:"groupPolicy is disabled"}:t==="allowlist"&&!(a??[]).includes(o)?{allowed:false,reason:`group ${o} not in groupAllowFrom`}:n&&n.length>0&&!n.some(c=>{let i=c.trim();return !!(i==="*"||i===r||s&&i.toLowerCase()===s.toLowerCase())})?{allowed:false,reason:`sender ${r} not in groupSenderAllowFrom`}:{allowed:true}}var dt=D(()=>{});function nn(e){let t=e.sender,a=e.message_sent_time,n=[];return t?.email?n.push(t.email):t?.employee_code&&n.push(t.employee_code),a&&n.push(new Date(a*1e3).toISOString()),n.length>0?`[${n.join(" ")}] `:""}function an(e){let t=e.tag;return {message_id:e.message_id??"",tag:t,text:e.text,image:e.image,file:e.file,video:e.video,combined_forwarded_chat_history:e.combined_forwarded_chat_history}}async function ut(e,t){let a=e.tag,n=[];if(a==="text"){let o=e.text;return {text:o?.plain_text??o?.content??"",media:n}}if(a==="image"||a==="file"||a==="video"){let o=await re({message:an(e),client:t.client,mediaAllowHosts:t.mediaAllowHosts,log:t.log});return o?(n.push(o),{text:o.placeholder,media:n}):{text:`<media:${a}>`,media:n}}if(a==="combined_forwarded_chat_history"){let o=e.combined_forwarded_chat_history?.content;if(o){let r=await ie(o,t);return n.push(...r.media),{text:r.lines.length>0?`[Forwarded messages]
|
|
2
|
+
${r.lines.join(`
|
|
3
|
+
`)}`:"[Forwarded messages]",media:n}}return {text:"[Forwarded messages]",media:n}}return {text:`<unsupported:${a??"unknown"}>`,media:n}}async function ie(e,t){let a=[],n=[];for(let o of e){if(Array.isArray(o)){let c=await ie(o,t);a.push(...c.lines),n.push(...c.media);continue}if(!o||typeof o!="object")continue;let r=o,s=nn(r),l=await ut(r,t);n.push(...l.media),l.text&&a.push(`${s}${l.text}`);}return {lines:a,media:n}}async function Oe(e){let{client:t,quotedMessageId:a,mediaAllowHosts:n,log:o}=e;try{let r=await t.getMessageByMessageId(a),s=r.sender,l=s?.employee_code??"unknown",c=s?.email?`${l} (${s.email})`:l,i=await ut(r,{client:t,mediaAllowHosts:n,log:o});return {text:`[Quoted from ${c}: ${i.text}]`,media:i.media}}catch(r){return o(`seatalk: failed to resolve quoted message ${a}: ${String(r)}`),null}}async function Fe(e){let{mediaUrls:t,client:a,to:n,threadId:o,isGroup:r,log:s}=e;await sendMediaWithLeadingCaption({mediaUrls:t,caption:"",send:async({mediaUrl:l})=>{await ke({client:a,to:n,mediaUrl:l,threadId:o,isGroup:r});},onError:async({error:l,mediaUrl:c})=>{s(`seatalk: failed to send media ${c}: ${String(l)}`);}});}var gt=D(()=>{he();se();});function De(e){let{send:t,chunkText:a,maxLength:n,joiner:o,idleFlushMs:r}=e,s="",l,c=Promise.resolve(),i=()=>{l&&(clearTimeout(l),l=void 0);},u=()=>{if(!s)return;let d=s;s="";let y=d.length>n?a(d,n):[d],p=async()=>{for(let S of y)await t(S);};c=c.then(p,p),c.catch(()=>{});},m=()=>{!r||r<=0||(i(),l=setTimeout(()=>{l=void 0,u();},r));};return {append:d=>{if(!d)return;if(i(),!s){s=d,m();return}let y=`${s}${o}${d}`;if(y.length>n){u(),s=d,m();return}s=y,m();},flush:async()=>{i(),u(),await c;},hasBuffered:()=>s.length>0}}var pt=D(()=>{});function ln(e,t,a){return a.some(n=>{let o=n.trim();return !!(o==="*"||o===e||t&&o.toLowerCase()===t.toLowerCase())})}function Ce(e){let{cfg:t,event:a,client:n,runtime:o,accountId:r}=e,s=o?.log??console.log,l=o?.error??console.error,c=i=>i().catch(u=>l(`seatalk[${r}]: event error: ${String(u)}`));switch(a.event_type){case "message_from_bot_subscriber":c(()=>kn({cfg:t,event:a,client:n,runtime:o,accountId:r}));break;case "new_mentioned_message_received_from_group_chat":case "new_message_received_from_thread":c(()=>yn({cfg:t,event:a,client:n,runtime:o,accountId:r}));break;case "new_bot_subscriber":{let i=a.event?.employee_code;s(`seatalk[${r}]: new subscriber: ${i}`);break}case "bot_added_to_group_chat":{let i=a.event?.group_id;s(`seatalk[${r}]: bot added to group: ${i}`);break}case "bot_removed_from_group_chat":{let i=a.event?.group_id;s(`seatalk[${r}]: bot removed from group: ${i}`);break}default:s(`seatalk[${r}]: unhandled event type: ${a.event_type}`);}}function ht(e){let t=Date.now();if(t-ft>un){for(let[a,n]of V)t-n>cn&&V.delete(a);ft=t;}if(V.has(e))return false;if(V.size>=dn){let a=V.keys().next().value;V.delete(a);}return V.set(e,t),true}function pn(e,t,a){return a?`${e}:dm:${t}:t:${a}`:`${e}:dm:${t}`}function fn(e,t,a,n){return n?`${e}:grp:${t}:${a}:t:${n}`:`${e}:grp:${t}:${a}`}function mn(e,t){clearTimeout(t.timer);let a=Date.now()-t.firstEventAt,n=gn-a;if(n<=0){Be(e);return}let o=Math.min(kt,n);t.timer=setTimeout(()=>Be(e),o);}function Be(e){let t=be.get(e);if(!t)return;be.delete(e);let a=t.entries;if(a.length===0)return;a[0].kind==="dm"?hn(a,t.context).catch(r=>{(t.context.runtime?.error??console.error)(`seatalk[${t.context.accountId}]: flush error: ${String(r)}`);}):Sn(a,t.context).catch(r=>{(t.context.runtime?.error??console.error)(`seatalk[${t.context.accountId}]: group flush error: ${String(r)}`);});}function St(e,t,a){let n=be.get(e);n||(n={entries:[],timer:setTimeout(()=>Be(e),kt),firstEventAt:Date.now(),context:a},be.set(e,n)),n.entries.push(t),mn(e,n);}async function hn(e,t){let{cfg:a,client:n,runtime:o,accountId:r}=t,s=o?.log??console.log,l=o?.error??console.error,c=e[0].parsedEvent,i=c.employee_code,u=c.email,m=A({cfg:a,accountId:r}),h=m.config,f=z(),d=h?.dmPolicy??"allowlist",y=(h?.allowFrom??[]).map(w=>String(w)),p=createChannelPairingController({core:f,channel:"seatalk",accountId:r}),S=d==="pairing"?await p.readAllowFromStore().catch(()=>[]):[],T=resolveDmGroupAccessWithLists({isGroup:false,dmPolicy:d,groupPolicy:"disabled",allowFrom:y,groupAllowFrom:[],storeAllowFrom:S,isSenderAllowed:w=>ln(i,u,w)});if(T.decision==="pairing"){(await p.issueChallenge({senderId:i,senderIdLine:`Your SeaTalk employee code: ${i}`,meta:u?{email:u}:void 0,onCreated:({code:k})=>{s(`seatalk[${r}]: pairing request sender=${i} code=${k}`);},sendPairingReply:async k=>{await j(n,i,k,1,c.message.thread_id);},onReplyError:k=>{s(`seatalk[${r}]: pairing reply failed for ${i}: ${String(k)}`);}})).created||s(`seatalk[${r}]: pairing already pending for ${i}`);return}if(T.decision!=="allow"){T.reasonCode===DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED?s(`seatalk[${r}]: blocked DM from ${i} (dmPolicy=disabled)`):s(`seatalk[${r}]: sender ${i} not in allowlist, dropping`);return}let $=h?.mediaAllowHosts,v={client:n,mediaAllowHosts:$,log:s},b=[],P=[];for(let{parsedEvent:w}of e){let k=w.message;switch(k.tag){case "text":(k.text?.plain_text||k.text?.content)&&b.push(k.text.plain_text??k.text.content??"");break;case "image":case "file":case "video":{let I=await re({message:k,client:n,mediaAllowHosts:$,log:s});I&&P.push(I);break}case "combined_forwarded_chat_history":{let I=k.combined_forwarded_chat_history?.content;if(I){let N=await ie(I,v);P.push(...N.media),b.push(N.lines.length>0?`[Forwarded messages]
|
|
4
|
+
${N.lines.join(`
|
|
5
|
+
`)}`:"[Forwarded messages]");}break}}}let B=new Set,M=[];for(let{parsedEvent:w}of e){let k=w.message.quoted_message_id;if(!k||B.has(k))continue;B.add(k);let I=await Oe({client:n,quotedMessageId:k,mediaAllowHosts:$,log:s});I&&(M.push(I.text),P.push(...I.media));}let te=xe(P),x=b.join(`
|
|
6
|
+
`);if(M.length>0){let w=M.join(`
|
|
7
|
+
`);x=x?`${w}
|
|
8
|
+
${x}`:w;}if(!x&&P.length>0&&(x=P.map(w=>w.placeholder).join(" ")),!x&&P.length===0){s(`seatalk[${r}]: skipping empty message from ${i}`);return}let ne=i+(u?` (${u})`:""),C=c.message.message_id,_=c.message.thread_id;try{let w=`seatalk:${i}`,k=i,I=f.channel.routing.resolveAgentRoute({cfg:a,channel:"seatalk",accountId:r,peer:{kind:"direct",id:i}}),N=x.replace(/\s+/g," ").slice(0,160);f.system.enqueueSystemEvent(`SeaTalk[${r}] DM from ${ne}: ${N}`,{sessionKey:I.sessionKey,contextKey:`seatalk:message:${i}:${C}`});let J=e[0].event.timestamp,ae=J?new Date(J*1e3):new Date,$e=f.channel.reply.resolveEnvelopeFormatOptions(a),Ue=`${ne}: ${x}`,ce=f.channel.reply.formatAgentEnvelope({channel:"SeaTalk",from:i,timestamp:ae,envelope:$e,body:Ue}),X={};_&&(X.threadId=_);let oe=c.message.quoted_message_id;oe&&(X.quotedMessageId=oe);let de=f.channel.reply.finalizeInboundContext({Body:ce,BodyForAgent:x,RawBody:x,CommandBody:x,From:w,To:k,SessionKey:I.sessionKey,AccountId:I.accountId,ChatType:"direct",SenderName:ne,SenderId:i,Provider:"seatalk",Surface:"seatalk",MessageSid:C,MessageThreadId:_||void 0,Timestamp:J?J*1e3:Date.now(),WasMentioned:!1,CommandAuthorized:!0,OriginatingChannel:"seatalk",OriginatingTo:k,...Object.keys(X).length>0?{Metadata:X}:{},...te});(m.config?.processingIndicator??"typing")==="typing"&&n.setSingleChatTyping(i,_).catch(O=>s(`seatalk[${r}]: typing failed: ${String(O)}`));let Y=h?.outboundCoalescing!==!1,ue=O=>j(n,i,O,1,_),R=(O,F)=>f.channel.text.chunkMarkdownText(O,F),E=Y?De({send:ue,chunkText:R,maxLength:Te,joiner:`
|
|
9
|
+
|
|
10
|
+
`,idleFlushMs:yt}):null,Q=f.channel.reply.createReplyDispatcherWithTyping({humanDelay:f.channel.reply.resolveHumanDelayConfig(a,I.agentId),deliver:async O=>{let F=resolveSendableOutboundReplyParts(O);if(!(!F.hasText&&!F.hasMedia)){if(F.hasText)if(s(`seatalk[${r}]: inline deliver DM to ${i} threadId=${_||"none"}`),E)E.append(F.trimmedText);else {let Pt=R(F.trimmedText,Te);for(let Mt of Pt)await ue(Mt);}F.hasMedia&&(E&&await E.flush(),await Fe({mediaUrls:F.mediaUrls,client:n,to:i,threadId:_,isGroup:!1,log:s}));}},onError:O=>{l(`seatalk[${r}]: reply delivery failed: ${String(O)}`);}}),Ie={agentId:I.agentId,...Q.replyOptions};s(`seatalk[${r}]: dispatching to agent (session=${I.sessionKey})`);try{let{queuedFinal:O,counts:F}=await f.channel.reply.dispatchReplyFromConfig({ctx:de,cfg:a,dispatcher:Q.dispatcher,replyOptions:Ie});s(`seatalk[${r}]: dispatch complete (queuedFinal=${O}, counts=${JSON.stringify(F)})`);}finally{Q.markDispatchIdle(),E&&(await Q.dispatcher.waitForIdle(),await E.flush());}}catch(w){l(`seatalk[${r}]: failed to dispatch message: ${String(w)}`);}}async function kn(e){let{cfg:t,event:a,client:n,runtime:o,accountId:r}=e,s=o?.log??console.log;if(!ht(`${r}:${a.event_id}`)){s(`seatalk[${r}]: skipping duplicate event ${a.event_id}`);return}let l=a.event;if(!l?.employee_code||!l?.message){s(`seatalk[${r}]: malformed message event, skipping`);return}s(`seatalk[${r}]: received ${l.message.tag} from ${l.employee_code} (threadId=${l.message.thread_id||"none"})`);let c=pn(r,l.employee_code,l.message.thread_id);St(c,{kind:"dm",event:a,parsedEvent:l},{cfg:t,client:n,runtime:o,accountId:r});}async function yn(e){let{cfg:t,event:a,client:n,runtime:o,accountId:r}=e,s=o?.log??console.log;if(!ht(`${r}:${a.event_id}`)){s(`seatalk[${r}]: skipping duplicate group event ${a.event_id}`);return}let l=a.event,c=l?.group_id,i=l?.message,u=i?.sender;if(!c||!i||!u?.employee_code){s(`seatalk[${r}]: malformed group message event, skipping`);return}if(u.sender_type===2){s(`seatalk[${r}]: ignoring bot message in group ${c}`);return}let m=u.employee_code,h=u.email,f=i.thread_id;s(`seatalk[${r}]: group ${c} ${i.tag} from ${m} (event=${a.event_type})`);let y=A({cfg:t,accountId:r}).config,p=ct({groupPolicy:y?.groupPolicy??"disabled",groupAllowFrom:y?.groupAllowFrom,groupSenderAllowFrom:y?.groupSenderAllowFrom,groupId:c,senderEmployeeCode:m,senderEmail:h});if(!p.allowed){s(`seatalk[${r}]: group access denied: ${p.reason}`);return}let S=fn(r,c,m,f);St(S,{kind:"group",event:a,groupEvent:l,groupId:c,eventType:a.event_type},{cfg:t,client:n,runtime:o,accountId:r});}async function Sn(e,t){let{cfg:a,client:n,runtime:o,accountId:r}=t,s=o?.log??console.log,l=o?.error??console.error,c=e[0],i=c.groupId,u=c.groupEvent.message,m=u.sender,h=m.employee_code,f=m.email,d=u.thread_id,p=A({cfg:a,accountId:r}).config,S=p?.mediaAllowHosts,T={client:n,mediaAllowHosts:S,log:s},$=[],v=[];for(let{groupEvent:C}of e){let _=C.message;switch(_.tag){case "text":(_.text?.plain_text||_.text?.content)&&$.push(_.text.plain_text??_.text.content??"");break;case "image":case "file":case "video":{let w=await re({message:_,client:n,mediaAllowHosts:S,log:s});w&&v.push(w);break}case "combined_forwarded_chat_history":{let w=_.combined_forwarded_chat_history?.content;if(w){let k=await ie(w,T);v.push(...k.media),$.push(k.lines.length>0?`[Forwarded messages]
|
|
11
|
+
${k.lines.join(`
|
|
12
|
+
`)}`:"[Forwarded messages]");}break}}}let b=c.groupEvent.message.quoted_message_id,P=null;if(b){let C=await Oe({client:n,quotedMessageId:b,mediaAllowHosts:S,log:s});C&&(P=C.text,v.push(...C.media));}let B=xe(v),M=$.join(`
|
|
13
|
+
`);if(P&&(M=M?`${P}
|
|
14
|
+
${M}`:P),!M&&v.length>0&&(M=v.map(C=>C.placeholder).join(" ")),!M&&v.length===0){s(`seatalk[${r}]: skipping empty group message from ${h} in ${i}`);return}let te=h+(f?` (${f})`:""),x=u.message_id,ne=e.some(C=>C.eventType==="new_mentioned_message_received_from_group_chat");try{let C=z(),_=C.channel.routing.resolveAgentRoute({cfg:a,channel:"seatalk",accountId:r,peer:{kind:"group",id:i}}),w=M.replace(/\s+/g," ").slice(0,160);C.system.enqueueSystemEvent(`SeaTalk[${r}] Group(${i}) from ${te}: ${w}`,{sessionKey:_.sessionKey,contextKey:`seatalk:group:${i}:${x}`});let k=c.groupEvent.message.message_sent_time,I=k?new Date(k*1e3):new Date,N=C.channel.reply.resolveEnvelopeFormatOptions(a),J=C.channel.reply.formatAgentEnvelope({channel:"SeaTalk",from:h,timestamp:I,envelope:N,body:`${te}: ${M}`}),ae={groupId:i};d&&(ae.threadId=d),b&&(ae.quotedMessageId=b);let $e=C.channel.reply.finalizeInboundContext({Body:J,BodyForAgent:M,RawBody:M,CommandBody:M,From:`seatalk:${h}`,To:`group:${i}`,SessionKey:_.sessionKey,AccountId:_.accountId,ChatType:"group",SenderName:te,SenderId:h,Provider:"seatalk",Surface:"seatalk",MessageSid:x,MessageThreadId:d||void 0,Timestamp:k?k*1e3:Date.now(),WasMentioned:ne,CommandAuthorized:!0,OriginatingChannel:"seatalk",OriginatingTo:`group:${i}`,Metadata:ae,...B});(p?.processingIndicator??"typing")==="typing"&&n.setGroupChatTyping(i,d).catch(R=>s(`seatalk[${r}]: group typing failed: ${String(R)}`));let ce=d||void 0,X=p?.outboundCoalescing!==!1,oe=R=>ee(n,i,R,1,ce),de=(R,E)=>C.channel.text.chunkMarkdownText(R,E),H=X?De({send:oe,chunkText:de,maxLength:Te,joiner:`
|
|
15
|
+
|
|
16
|
+
`,idleFlushMs:yt}):null,Y=C.channel.reply.createReplyDispatcherWithTyping({humanDelay:C.channel.reply.resolveHumanDelayConfig(a,_.agentId),deliver:async R=>{let E=resolveSendableOutboundReplyParts(R);if(!(!E.hasText&&!E.hasMedia)){if(E.hasText)if(H)H.append(E.trimmedText);else {let Q=de(E.trimmedText,Te);for(let Ie of Q)await oe(Ie);}E.hasMedia&&(H&&await H.flush(),await Fe({mediaUrls:E.mediaUrls,client:n,to:i,threadId:ce,isGroup:!0,log:s}));}},onError:R=>{l(`seatalk[${r}]: group reply delivery failed: ${String(R)}`);}}),ue={agentId:_.agentId,...Y.replyOptions};s(`seatalk[${r}]: dispatching group message (session=${_.sessionKey})`);try{let{queuedFinal:R,counts:E}=await C.channel.reply.dispatchReplyFromConfig({ctx:$e,cfg:a,dispatcher:Y.dispatcher,replyOptions:ue});s(`seatalk[${r}]: group dispatch complete (queuedFinal=${R}, counts=${JSON.stringify(E)})`);}finally{Y.markDispatchIdle(),H&&(await Y.dispatcher.waitForIdle(),await H.flush());}}catch(C){l(`seatalk[${r}]: failed to dispatch group message: ${String(C)}`);}}var cn,dn,un,V,ft,kt,gn,Te,yt,be,Le=D(()=>{dt();q();gt();he();pt();me();se();cn=1800*1e3,dn=1e3,un=300*1e3,V=new Map,ft=Date.now();kt=1500,gn=5e3,Te=4e3,yt=1e3,be=new Map;});var Ct={};je(Ct,{connectSeaTalkRelay:()=>vn});function Cn(e,t){return new Promise(a=>{let n=()=>{clearTimeout(o),a();},o=setTimeout(()=>{t?.removeEventListener("abort",n),a();},e);t?.addEventListener("abort",n,{once:true});})}async function bt(e){let{cfg:t,account:a,relayUrl:n,runtime:o,abortSignal:r}=e,{accountId:s}=a,l=o?.log??console.log,c=o?.error??console.error,i=G(a.config)?.signingSecret;if(!a.appId||!a.appSecret||!i)throw new Error(`SeaTalk account "${s}" missing credentials for relay mode`);let u=U(a);if(!u)throw new Error(`SeaTalk client not available for account "${s}"`);let m=wt;for(;!r?.aborted;){try{await new Promise((h,f)=>{if(r?.aborted){h();return}l(`seatalk[${s}]: connecting to relay ${n}...`);let d=new wn(n),y,p=()=>{y&&(clearTimeout(y),y=void 0);},S=()=>{p(),y=setTimeout(()=>{c(`seatalk[${s}]: relay silent for ${Tt}ms, terminating`),d.terminate();},Tt);},T=()=>{p(),d.close(),h();};r?.addEventListener("abort",T,{once:!0}),d.on("upgrade",v=>{v.socket.setKeepAlive(!0,6e4);}),d.on("open",()=>{S(),l(`seatalk[${s}]: relay connected, authenticating...`),d.send(JSON.stringify({type:"auth",appId:a.appId,appSecret:a.appSecret,signingSecret:i}));});let $=!1;d.on("message",v=>{S();let b;try{b=JSON.parse(String(v));}catch{c(`seatalk[${s}]: relay sent invalid JSON`);return}if(!$){b.type==="auth_ok"?($=!0,m=wt,l(`seatalk[${s}]: relay authenticated`)):b.type==="auth_fail"&&(c(`seatalk[${s}]: relay auth failed: ${b.error}`),d.close(),f(new Error(`Relay auth failed: ${b.error}`)));return}switch(b.type){case "event":b.event&&u&&Ce({cfg:t,event:b.event,client:u,runtime:o,accountId:s});break;case "ping":d.send(JSON.stringify({type:"pong"}));break;case "replaced":l(`seatalk[${s}]: connection replaced by another instance`),d.close(),h();return;default:l(`seatalk[${s}]: unknown relay message type: ${b.type}`);}}),d.on("close",(v,b)=>{p(),r?.removeEventListener("abort",T),$&&l(`seatalk[${s}]: relay disconnected (code=${v}, reason=${String(b)})`),h();}),d.on("error",v=>{p(),r?.removeEventListener("abort",T),c(`seatalk[${s}]: relay connection error: ${String(v)}`),h();});});}catch(h){let f=String(h);if(f.includes("Relay auth failed"))throw h;c(`seatalk[${s}]: relay error: ${f}`);}if(r?.aborted)break;l(`seatalk[${s}]: reconnecting in ${m}ms...`),await Cn(m,r),m=Math.min(m*bn,Tn);}}async function vn(e){let t=e.config;if(!t)throw new Error("Config is required for SeaTalk relay client");let a=e.runtime?.log??console.log;if(e.accountId){let o=A({cfg:t,accountId:e.accountId});if(!o.enabled||!o.configured)throw new Error(`SeaTalk account "${e.accountId}" not configured or disabled`);return bt({cfg:t,account:o,relayUrl:e.relayUrl,runtime:e.runtime,abortSignal:e.abortSignal})}let n=fe(t);if(n.length===0)throw new Error("No enabled SeaTalk accounts configured");a(`seatalk: connecting ${n.length} account(s) to relay: ${n.map(o=>o.accountId).join(", ")}`),await Promise.all(n.map(o=>bt({cfg:t,account:o,relayUrl:e.relayUrl,runtime:e.runtime,abortSignal:e.abortSignal})));}var wt,Tn,bn,Tt,vt=D(()=>{q();Le();Z();wt=1e3,Tn=3e4,bn=2,Tt=75e3;});var It={};je(It,{monitorSeaTalkProvider:()=>En});function _n(e,t,a){let n=Buffer.from(t,"latin1"),o=_e.createHash("sha256").update(Buffer.concat([e,n])).digest("hex");try{return _e.timingSafeEqual(Buffer.from(o,"hex"),Buffer.from(a,"hex"))}catch{return false}}function In(e){return new Promise((t,a)=>{let n=0,o=[];e.on("data",r=>{if(n+=r.length,n>$n){e.destroy(new ve);return}o.push(r);}),e.on("end",()=>t(Buffer.concat(o))),e.on("error",a);})}async function _t(e){let{cfg:t,account:a,runtime:n,abortSignal:o}=e,{accountId:r}=a,s=n?.log??console.log,l=n?.error??console.error,c=a.webhookPort,i=a.webhookPath,u=G(a.config)?.signingSecret;if(!u)throw new Error(`SeaTalk account "${r}" missing signingSecret`);let m=U(a);if(!m)throw new Error(`SeaTalk client not available for account "${r}"`);s(`seatalk[${r}]: starting webhook server on port ${c}, path ${i}...`);let h=$t.createServer();return h.on("request",async(f,d)=>{let y=new URL(f.url??"/",`http://localhost:${c}`).pathname;if(f.method!=="POST"||y!==i){d.writeHead(404),d.end("Not Found");return}try{let p=await In(f),S=f.headers.signature;if(!S||!_n(p,u,S)){s(`seatalk[${r}]: signature verification failed`),d.writeHead(403),d.end("Forbidden");return}let T=JSON.parse(p.toString("utf-8"));if(T.event_type==="event_verification"){let $=T.event?.seatalk_challenge;d.writeHead(200,{"Content-Type":"application/json"}),d.end(JSON.stringify({seatalk_challenge:$})),s(`seatalk[${r}]: URL verification challenge responded`);return}d.writeHead(200),d.end("OK"),Ce({cfg:t,event:T,client:m,runtime:n,accountId:r});}catch(p){l(`seatalk[${r}]: request processing error: ${String(p)}`),d.headersSent||(p instanceof ve?(d.writeHead(413),d.end("Payload Too Large")):(d.writeHead(500),d.end("Internal Server Error")));}}),new Promise((f,d)=>{let y=()=>{h.close();},p=()=>{s(`seatalk[${r}]: abort signal received, stopping webhook server`),y(),f();};if(o?.aborted){y(),f();return}o?.addEventListener("abort",p,{once:true}),h.listen(c,()=>{s(`seatalk[${r}]: webhook server listening on port ${c}`);}),h.on("error",S=>{l(`seatalk[${r}]: webhook server error: ${S}`),o?.removeEventListener("abort",p),d(S);});})}async function En(e={}){let t=e.config;if(!t)throw new Error("Config is required for SeaTalk monitor");let a=e.runtime?.log??console.log;if(e.accountId){let o=A({cfg:t,accountId:e.accountId});if(!o.enabled||!o.configured)throw new Error(`SeaTalk account "${e.accountId}" not configured or disabled`);return _t({cfg:t,account:o,runtime:e.runtime,abortSignal:e.abortSignal})}let n=fe(t);if(n.length===0)throw new Error("No enabled SeaTalk accounts configured");a(`seatalk: starting ${n.length} account(s): ${n.map(o=>o.accountId).join(", ")}`),await Promise.all(n.map(o=>_t({cfg:t,account:o,runtime:e.runtime,abortSignal:e.abortSignal})));}var $n,ve,Et=D(()=>{q();Le();Z();$n=1024*1024,ve=class extends Error{constructor(){super("Request body too large"),this.name="PayloadTooLargeError";}};});q();Z();var qe=z$1.enum(["open","allowlist","pairing"]),ze=z$1.enum(["webhook","relay"]),We=z$1.enum(["disabled","allowlist","open"]),Ke=z$1.enum(["typing","off"]),Bt=z$1.object({groupInfo:z$1.boolean().optional().default(true),groupHistory:z$1.boolean().optional().default(true),groupList:z$1.boolean().optional().default(true),threadHistory:z$1.boolean().optional().default(true),getMessage:z$1.boolean().optional().default(true)}).strict(),Lt=z$1.object({enabled:z$1.boolean().optional(),appId:z$1.string().optional(),appSecret:z$1.string().optional(),signingSecret:z$1.string().optional(),mode:ze.optional(),relayUrl:z$1.string().optional(),webhookPort:z$1.number().int().positive().optional(),webhookPath:z$1.string().optional(),dmPolicy:qe.optional(),allowFrom:z$1.array(z$1.string()).optional(),groupPolicy:We.optional(),groupAllowFrom:z$1.array(z$1.string()).optional(),groupSenderAllowFrom:z$1.array(z$1.string()).optional(),processingIndicator:Ke.optional(),mediaAllowHosts:z$1.array(z$1.string()).optional(),outboundCoalescing:z$1.boolean().optional()}).strict(),Ve=z$1.object({enabled:z$1.boolean().optional(),appId:z$1.string().optional(),appSecret:z$1.string().optional(),signingSecret:z$1.string().optional(),mode:ze.optional().default("webhook"),relayUrl:z$1.string().optional(),webhookPort:z$1.number().int().positive().optional().default(8080),webhookPath:z$1.string().optional().default("/callback"),dmPolicy:qe.optional().default("allowlist"),allowFrom:z$1.array(z$1.string()).optional(),groupPolicy:We.optional().default("disabled"),groupAllowFrom:z$1.array(z$1.string()).optional(),groupSenderAllowFrom:z$1.array(z$1.string()).optional(),processingIndicator:Ke.optional().default("typing"),mediaAllowHosts:z$1.array(z$1.string()).optional(),outboundCoalescing:z$1.boolean().optional().default(true),tools:Bt.optional(),accounts:z$1.record(z$1.string(),Lt.optional()).optional()}).strict().superRefine((e,t)=>{e.dmPolicy==="open"&&((e.allowFrom??[]).some(o=>o.trim()==="*")||t.addIssue({code:z$1.ZodIssueCode.custom,path:["allowFrom"],message:'channels.seatalk.dmPolicy="open" requires channels.seatalk.allowFrom to include "*"'}));});q();Z();me();se();var Qe="group:";function Ze(e){let t=e.trim();return t||null}function ye(e){return e.startsWith(Qe)}function Se(e){return e.slice(Qe.length)}function K(e){return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.trim())}function et(e){let t=e.trim();return t?K(t)?true:ye(t)?Se(t).length>0:/^[a-zA-Z0-9_-]+$/.test(t):false}function tt(e,t){let a=A({cfg:e,accountId:t}),n=U(a);if(!n)throw new Error(`SeaTalk client not available for account ${a.accountId}`);return n}async function nt(e,t){if(!K(t))return t;let n=(await e.getEmployeeCodeByEmail([t])).find(o=>o.employeeCode&&o.status===2);if(n?.employeeCode)return n.employeeCode;throw new Error(`No active SeaTalk employee found for email: ${t}`)}function at(e){if(e!=null)return String(e)}var ot={deliveryMode:"direct",chunker:(e,t)=>z().channel.text.chunkMarkdownText(e,t),chunkerMode:"markdown",textChunkLimit:4e3,sendText:async({cfg:e,to:t,text:a,accountId:n,threadId:o})=>{let r=tt(e,n??void 0),s=at(o);if(ye(t)){let c=Se(t);return await ee(r,c,a,1,s),{channel:"seatalk",messageId:"",chatId:t}}let l=await nt(r,t);return await j(r,l,a,1,s),{channel:"seatalk",messageId:"",chatId:l}},sendMedia:async({cfg:e,to:t,text:a,mediaUrl:n,accountId:o,threadId:r})=>{let s=tt(e,o??void 0),l=at(r),c=ye(t),i=c?Se(t):await nt(s,t);if(a?.trim()&&(c?await ee(s,i,a,1,l):await j(s,i,a,1,l)),n)try{await ke({client:s,to:i,mediaUrl:n,threadId:l,isGroup:c});}catch(u){let m=`[Media send failed: ${u instanceof Error?u.message:String(u)}]`;c?await ee(s,i,m,2,l):await j(s,i,m,2,l);}return {channel:"seatalk",messageId:"",chatId:c?t:i}}};Z();async function we(e){if(!e?.appId||!e?.appSecret)return {ok:false,error:"missing credentials (appId, appSecret)"};try{let t=Pe(e.appId,e.appSecret),a=Date.now();await t.getAccessToken();let n=Date.now()-a;return {ok:!0,appId:e.appId,latencyMs:n}}catch(t){return {ok:false,appId:e.appId,error:t instanceof Error?t.message:String(t)}}}se();q();var st="seatalk";function Re(e){return e.split(/[\n,;]+/g).map(t=>t.trim()).filter(Boolean)}async function it(e){let{cfg:t,prompter:a}=e,n=t.channels?.seatalk?.allowFrom??[];for(await a.note(["Allowlist SeaTalk DMs by email or employee_code.","Examples:","- alice@company.com","- 12345678"].join(`
|
|
17
|
+
`),"SeaTalk allowlist");;){let o=await a.text({message:"SeaTalk allowFrom (emails or employee_codes)",placeholder:"alice@company.com, 12345678",initialValue:n[0]?String(n[0]):void 0,validate:l=>String(l??"").trim()?void 0:"Required"}),r=Re(String(o));if(r.length===0){await a.note("Enter at least one user.","SeaTalk allowlist");continue}let s=mergeAllowFromEntries(n.map(l=>String(l)),r);return {...t,channels:{...t.channels,seatalk:{...t.channels?.seatalk,allowFrom:s}}}}}var Zt=createTopLevelChannelDmPolicy({label:"SeaTalk",channel:st,policyKey:"channels.seatalk.dmPolicy",allowFromKey:"channels.seatalk.allowFrom",getCurrent:e=>e.channels?.seatalk?.dmPolicy??"allowlist",promptAllowFrom:async({cfg:e,prompter:t})=>it({cfg:e,prompter:t})});async function rt(e){let t=String(await e.text({message:"Enter SeaTalk App ID",validate:o=>o?.trim()?void 0:"Required"})).trim(),a=String(await e.text({message:"Enter SeaTalk App Secret",validate:o=>o?.trim()?void 0:"Required"})).trim(),n=String(await e.text({message:"Enter SeaTalk Signing Secret",validate:o=>o?.trim()?void 0:"Required"})).trim();return {appId:t,appSecret:a,signingSecret:n}}async function en(e){await e.note(["1) Go to SeaTalk Open Platform (open.seatalk.io)","2) Create a Bot App","3) Get App ID and App Secret from Basic Info & Credentials","4) Get Signing Secret from Event Callback settings","5) Enable Bot capability and set status to Online",'6) Enable "Send Message to Bot User" permission'].join(`
|
|
18
|
+
`),"SeaTalk credentials");}var lt={channel:st,status:createStandardChannelSetupStatus({channelLabel:"SeaTalk",configuredLabel:"configured",unconfiguredLabel:"needs app credentials",configuredHint:"configured",unconfiguredHint:"needs app creds",configuredScore:2,unconfiguredScore:0,resolveConfigured:({cfg:e})=>{let t=e.channels?.seatalk;return !!G(t)}}),credentials:[],finalize:async({cfg:e,prompter:t,forceAllowFrom:a})=>{let n=e,o=n.channels?.seatalk,r=G(o),s=!!(o?.appId?.trim()&&o?.appSecret?.trim()&&o?.signingSecret?.trim()),l=null,c=null,i=null;if(r||await en(t),s?await t.confirm({message:"SeaTalk credentials already configured. Keep them?",initialValue:true})||({appId:l,appSecret:c,signingSecret:i}=await rt(t)):{appId:l,appSecret:c,signingSecret:i}=await rt(t),l&&c&&i){n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,enabled:true,appId:l,appSecret:c,signingSecret:i,dmPolicy:o?.dmPolicy??"allowlist"}}};try{let p=await we({appId:l,appSecret:c});p.ok?await t.note(`Connected successfully (latency: ${p.latencyMs}ms)`,"SeaTalk connection test"):await t.note(`Connection failed: ${p.error??"unknown error"}`,"SeaTalk connection test");}catch(p){await t.note(`Connection test failed: ${String(p)}`,"SeaTalk connection test");}await t.note(["Important reminders:",'- Bot App must be set to "Online" status in SeaTalk Open Platform','- "Send Message to Bot User" permission must be enabled',"- Configure the callback URL in Event Callback settings"].join(`
|
|
19
|
+
`),"SeaTalk setup");}let u=n.channels?.seatalk?.mode??"webhook",m=await t.select({message:"Gateway mode",options:[{value:"webhook",label:"Webhook \u2014 receive event callbacks directly (default)"},{value:"relay",label:"Relay \u2014 connect to a relay service as client"}],initialValue:u}),h=String(m);if(n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,mode:h}}},h==="relay"){let p=n.channels?.seatalk?.relayUrl??"",S=await t.text({message:"Relay WebSocket URL",placeholder:"wss://relay.example.com/ws",initialValue:p||void 0,validate:$=>{let v=String($??"").trim();if(!v)return "Required";if(!v.startsWith("ws://")&&!v.startsWith("wss://"))return "Must be a ws:// or wss:// URL"}}),T=String(S).trim();T.startsWith("ws://")&&await t.note("ws:// transmits credentials (appSecret, signingSecret) unencrypted. Consider using wss:// for production.","Security warning"),n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,relayUrl:T}}};}else {let p=n.channels?.seatalk?.webhookPort??8080,S=await t.text({message:"Webhook port",initialValue:String(p),validate:P=>{let B=Number(P);return B>0&&B<65536?void 0:"Enter a valid port number (1-65535)"}}),T=Number(S);T&&T!==p&&(n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,webhookPort:T}}});let $=n.channels?.seatalk?.webhookPath??"/callback",v=await t.text({message:"Webhook path",initialValue:$,validate:P=>{let B=String(P??"").trim();if(!B)return "Required";if(!B.startsWith("/"))return "Path must start with /"}}),b=String(v??$).trim();b&&b!==$&&(n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,webhookPath:b}}});}let f=await t.select({message:"Group chat policy",options:[{value:"disabled",label:"Disabled \u2014 ignore all group messages (default)"},{value:"allowlist",label:"Allowlist \u2014 respond only in specific groups"},{value:"open",label:"Open \u2014 respond in all groups the bot joins"}],initialValue:n.channels?.seatalk?.groupPolicy??"disabled"}),d=String(f);if(n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,groupPolicy:d}}},d==="allowlist"){let p=n.channels?.seatalk?.groupAllowFrom??[],S=await t.text({message:"Allowed group IDs (comma-separated)",placeholder:"group_abc123, group_def456",initialValue:p.length>0?p.join(", "):void 0,validate:T=>String(T??"").trim()?void 0:"Enter at least one group ID"});n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,groupAllowFrom:Re(String(S))}}};}if(d!=="disabled"&&await t.confirm({message:"Restrict which users can trigger the bot in groups? (sender allowlist)",initialValue:true})){let S=n.channels?.seatalk?.groupSenderAllowFrom??[],T=await t.text({message:"Sender allowlist (emails or employee_codes, comma-separated)",placeholder:"alice@company.com, 12345678",initialValue:S.length>0?S.join(", "):void 0,validate:$=>String($??"").trim()?void 0:"Enter at least one user"});n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,groupSenderAllowFrom:Re(String(T))}}};}let y=await t.select({message:"Processing indicator",options:[{value:"typing",label:"Typing \u2014 show typing status while processing (default)"},{value:"off",label:"Off \u2014 no processing indicator"}],initialValue:n.channels?.seatalk?.processingIndicator??"typing"});return n={...n,channels:{...n.channels,seatalk:{...n.channels?.seatalk,processingIndicator:String(y)}}},a&&(n=await it({cfg:n,prompter:t})),{cfg:n}},dmPolicy:Zt,disable:e=>({...e,channels:{...e.channels,seatalk:{...e.channels?.seatalk,enabled:false}}})};var xn={id:"seatalk",label:"SeaTalk",selectionLabel:"SeaTalk (plugin)",blurb:"SeaTalk internal messaging integration.",docsPath:"/channels/seatalk",aliases:[],order:70,quickstartAllowFrom:true},At={id:"seatalk",meta:xn,pairing:{idLabel:"employeeCode",normalizeAllowEntry:createPairingPrefixStripper(/^(seatalk|st):/i),notifyApproval:async({cfg:e,id:t})=>{let a=Ee(e),n=A({cfg:e,accountId:a}),o=U(n);o&&await j(o,t,PAIRING_APPROVED_MESSAGE,1);}},capabilities:{chatTypes:["direct","group"],polls:false,threads:true,media:true,reactions:false,edit:false,reply:false},reload:{configPrefixes:["channels.seatalk"]},configSchema:buildChannelConfigSchema(Ve),config:{listAccountIds:e=>pe(e),resolveAccount:(e,t)=>A({cfg:e,accountId:t}),defaultAccountId:e=>Ee(e),setAccountEnabled:({cfg:e,accountId:t,enabled:a})=>{if(t===DEFAULT_ACCOUNT_ID)return {...e,channels:{...e.channels,seatalk:{...e.channels?.seatalk,enabled:a}}};let o=e.channels?.seatalk;return {...e,channels:{...e.channels,seatalk:{...o,accounts:{...o?.accounts,[t]:{...o?.accounts?.[t],enabled:a}}}}}},deleteAccount:({cfg:e,accountId:t})=>{if(t===DEFAULT_ACCOUNT_ID){let r={...e},s={...e.channels};s.seatalk=void 0;let l=Object.values(s).some(c=>c!==void 0);return r.channels=l?s:void 0,r}let n=e.channels?.seatalk,o={...n?.accounts};return delete o[t],{...e,channels:{...e.channels,seatalk:{...n,accounts:Object.keys(o).length>0?o:void 0}}}},isConfigured:e=>e.configured,describeAccount:e=>({accountId:e.accountId,enabled:e.enabled,configured:e.configured,appId:e.appId,mode:e.mode,...e.mode==="relay"?{relayUrl:e.relayUrl}:{webhookPort:e.webhookPort}}),resolveAllowFrom:({cfg:e,accountId:t})=>(A({cfg:e,accountId:t}).config?.allowFrom??[]).map(n=>String(n)),formatAllowFrom:({allowFrom:e})=>e.map(t=>String(t).trim()).filter(Boolean)},security:{collectWarnings:({cfg:e,accountId:t})=>{let a=A({cfg:e,accountId:t});return (a.config?.dmPolicy??"allowlist")!=="open"?[]:[`- SeaTalk[${a.accountId}]: dmPolicy="open" allows any subscriber to message the bot. Set channels.seatalk.dmPolicy to "allowlist" or "pairing" to restrict senders.`]}},setup:{resolveAccountId:()=>DEFAULT_ACCOUNT_ID,applyAccountConfig:({cfg:e,accountId:t})=>{if(!t||t===DEFAULT_ACCOUNT_ID)return {...e,channels:{...e.channels,seatalk:{...e.channels?.seatalk,enabled:true}}};let n=e.channels?.seatalk;return {...e,channels:{...e.channels,seatalk:{...n,accounts:{...n?.accounts,[t]:{...n?.accounts?.[t],enabled:true}}}}}}},setupWizard:lt,messaging:{normalizeTarget:e=>Ze(e)??void 0,targetResolver:{looksLikeId:et,hint:"<employee_code> or <email>"}},resolver:{resolveTargets:async({cfg:e,accountId:t,inputs:a})=>{let n=a.filter(c=>K(c));if(n.length===0)return a.map(c=>({input:c,resolved:true,id:c}));let o=c=>a.map(i=>{let u=K(i);return {input:i,resolved:!u,id:u?void 0:i,note:u?c:void 0}}),r=A({cfg:e,accountId:t}),s=U(r);if(!s)return o("SeaTalk client not available");let l=new Map;try{let c=await s.getEmployeeCodeByEmail(n);for(let i of c)i.employeeCode&&i.status===2&&l.set(i.email.toLowerCase(),i.employeeCode);}catch{return o("Failed to resolve email")}return a.map(c=>{if(!K(c))return {input:c,resolved:true,id:c};let i=l.get(c.toLowerCase());return i?{input:c,resolved:true,id:i,name:c}:{input:c,resolved:false,note:"No active employee found for this email"}})}},outbound:ot,status:{defaultRuntime:{accountId:DEFAULT_ACCOUNT_ID,running:false,lastStartAt:null,lastStopAt:null,lastError:null,port:null},buildChannelSummary:({snapshot:e})=>({configured:e.configured??false,running:e.running??false,lastStartAt:e.lastStartAt??null,lastStopAt:e.lastStopAt??null,lastError:e.lastError??null,port:e.port??null,probe:e.probe,lastProbeAt:e.lastProbeAt??null}),probeAccount:({account:e})=>we(e),buildAccountSnapshot:({account:e,runtime:t,probe:a})=>({accountId:e.accountId,enabled:e.enabled,configured:e.configured,appId:e.appId,mode:e.mode,...e.mode==="relay"?{relayUrl:e.relayUrl}:{webhookPort:e.webhookPort},running:t?.running??false,lastStartAt:t?.lastStartAt??null,lastStopAt:t?.lastStopAt??null,lastError:t?.lastError??null,port:t?.port??null,probe:a})},gateway:{startAccount:async e=>{let t=A({cfg:e.cfg,accountId:e.accountId});if(t.mode==="relay"){if(!t.relayUrl)throw new Error(`SeaTalk account "${e.accountId}" mode=relay but relayUrl is not configured`);e.setStatus({accountId:e.accountId,mode:"relay"}),e.log?.info(`starting seatalk[${e.accountId}] (relay client \u2192 ${t.relayUrl})`);let{connectSeaTalkRelay:o}=await Promise.resolve().then(()=>(vt(),Ct));return o({config:e.cfg,runtime:e.runtime,abortSignal:e.abortSignal,accountId:e.accountId,relayUrl:t.relayUrl})}e.setStatus({accountId:e.accountId,port:t.webhookPort}),e.log?.info(`starting seatalk[${e.accountId}] (webhook on port ${t.webhookPort})`);let{monitorSeaTalkProvider:n}=await Promise.resolve().then(()=>(Et(),It));return n({config:e.cfg,runtime:e.runtime,abortSignal:e.abortSignal,accountId:e.accountId})}}};var Ha=defineSetupPluginEntry(At);
|
|
20
|
+
export{Ha as default};
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-seatalk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "OpenClaw SeaTalk channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
6
7
|
"license": "Apache-2.0",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
@@ -10,19 +11,29 @@
|
|
|
10
11
|
},
|
|
11
12
|
"homepage": "https://github.com/lf4096/openclaw-seatalk#readme",
|
|
12
13
|
"bugs": "https://github.com/lf4096/openclaw-seatalk/issues",
|
|
13
|
-
"keywords": [
|
|
14
|
+
"keywords": [
|
|
15
|
+
"openclaw",
|
|
16
|
+
"seatalk",
|
|
17
|
+
"chatbot",
|
|
18
|
+
"channel-plugin"
|
|
19
|
+
],
|
|
14
20
|
"engines": {
|
|
15
21
|
"node": ">=22.16.0"
|
|
16
22
|
},
|
|
17
|
-
"files": [
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/**/*.js",
|
|
25
|
+
"openclaw.plugin.json",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
18
28
|
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
19
30
|
"format": "biome format --write .",
|
|
20
31
|
"format:check": "biome format .",
|
|
21
32
|
"lint": "biome lint .",
|
|
22
33
|
"lint:fix": "biome lint --fix .",
|
|
23
34
|
"check": "biome check .",
|
|
24
35
|
"check:fix": "biome check --fix .",
|
|
25
|
-
"
|
|
36
|
+
"prepublishOnly": "pnpm build"
|
|
26
37
|
},
|
|
27
38
|
"dependencies": {
|
|
28
39
|
"@sinclair/typebox": "0.34.48",
|
|
@@ -32,7 +43,9 @@
|
|
|
32
43
|
"devDependencies": {
|
|
33
44
|
"@biomejs/biome": "^1.9.0",
|
|
34
45
|
"@types/ws": "^8.5.0",
|
|
35
|
-
"openclaw": "latest"
|
|
46
|
+
"openclaw": "latest",
|
|
47
|
+
"tsup": "^8.3.0",
|
|
48
|
+
"typescript": "^5.6.0"
|
|
36
49
|
},
|
|
37
50
|
"peerDependencies": {
|
|
38
51
|
"openclaw": ">=2026.3.22"
|
|
@@ -43,10 +56,15 @@
|
|
|
43
56
|
}
|
|
44
57
|
},
|
|
45
58
|
"pnpm": {
|
|
46
|
-
"onlyBuiltDependencies": [
|
|
59
|
+
"onlyBuiltDependencies": [
|
|
60
|
+
"@biomejs/biome"
|
|
61
|
+
]
|
|
47
62
|
},
|
|
48
63
|
"openclaw": {
|
|
49
|
-
"extensions": [
|
|
64
|
+
"extensions": [
|
|
65
|
+
"./dist/index.js"
|
|
66
|
+
],
|
|
67
|
+
"setupEntry": "./dist/setup-entry.js",
|
|
50
68
|
"channel": {
|
|
51
69
|
"id": "seatalk",
|
|
52
70
|
"label": "SeaTalk",
|
|
@@ -65,6 +83,10 @@
|
|
|
65
83
|
"build": {
|
|
66
84
|
"openclawVersion": "2026.3.22",
|
|
67
85
|
"pluginSdkVersion": "2026.3.22"
|
|
86
|
+
},
|
|
87
|
+
"release": {
|
|
88
|
+
"publishToNpm": true,
|
|
89
|
+
"publishToClawHub": true
|
|
68
90
|
}
|
|
69
91
|
}
|
|
70
92
|
}
|
package/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
-
import { seatalkPlugin } from "./src/channel.js";
|
|
3
|
-
import { setSeatalkRuntime } from "./src/runtime.js";
|
|
4
|
-
import { registerSeaTalkTool } from "./src/tool.js";
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
sendTextMessage,
|
|
8
|
-
sendImageMessage,
|
|
9
|
-
sendFileMessage,
|
|
10
|
-
} from "./src/send.js";
|
|
11
|
-
export { probeSeaTalk } from "./src/probe.js";
|
|
12
|
-
export { monitorSeaTalkProvider } from "./src/monitor.js";
|
|
13
|
-
export { seatalkPlugin } from "./src/channel.js";
|
|
14
|
-
|
|
15
|
-
export default defineChannelPluginEntry({
|
|
16
|
-
id: "openclaw-seatalk",
|
|
17
|
-
name: "SeaTalk",
|
|
18
|
-
description: "SeaTalk channel plugin",
|
|
19
|
-
plugin: seatalkPlugin,
|
|
20
|
-
setRuntime: setSeatalkRuntime,
|
|
21
|
-
registerFull: (api) => registerSeaTalkTool(api),
|
|
22
|
-
});
|
package/setup-entry.ts
DELETED
package/src/access.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export function checkGroupAccess(params: {
|
|
2
|
-
groupPolicy: string;
|
|
3
|
-
groupAllowFrom?: string[];
|
|
4
|
-
groupSenderAllowFrom?: string[];
|
|
5
|
-
groupId: string;
|
|
6
|
-
senderEmployeeCode: string;
|
|
7
|
-
senderEmail?: string;
|
|
8
|
-
}): { allowed: boolean; reason?: string } {
|
|
9
|
-
const {
|
|
10
|
-
groupPolicy,
|
|
11
|
-
groupAllowFrom,
|
|
12
|
-
groupSenderAllowFrom,
|
|
13
|
-
groupId,
|
|
14
|
-
senderEmployeeCode,
|
|
15
|
-
senderEmail,
|
|
16
|
-
} = params;
|
|
17
|
-
|
|
18
|
-
if (groupPolicy === "disabled") {
|
|
19
|
-
return { allowed: false, reason: "groupPolicy is disabled" };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (groupPolicy === "allowlist") {
|
|
23
|
-
const list = groupAllowFrom ?? [];
|
|
24
|
-
if (!list.includes(groupId)) {
|
|
25
|
-
return { allowed: false, reason: `group ${groupId} not in groupAllowFrom` };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (groupSenderAllowFrom && groupSenderAllowFrom.length > 0) {
|
|
30
|
-
const match = groupSenderAllowFrom.some((entry) => {
|
|
31
|
-
const e = entry.trim();
|
|
32
|
-
if (e === "*") return true;
|
|
33
|
-
if (e === senderEmployeeCode) return true;
|
|
34
|
-
if (senderEmail && e.toLowerCase() === senderEmail.toLowerCase()) return true;
|
|
35
|
-
return false;
|
|
36
|
-
});
|
|
37
|
-
if (!match) {
|
|
38
|
-
return {
|
|
39
|
-
allowed: false,
|
|
40
|
-
reason: `sender ${senderEmployeeCode} not in groupSenderAllowFrom`,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return { allowed: true };
|
|
46
|
-
}
|