@2en/clawly-plugins 1.13.0 → 1.14.0

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.
Files changed (3) hide show
  1. package/index.ts +9 -1
  2. package/outbound.ts +91 -2
  3. package/package.json +1 -1
package/index.ts CHANGED
@@ -33,7 +33,7 @@ import {registerCronHook} from './cron-hook'
33
33
  import {registerEmail} from './email'
34
34
  import {registerGateway} from './gateway'
35
35
  import {getGatewayConfig} from './gateway-fetch'
36
- import {registerOutboundHook, registerOutboundMethods} from './outbound'
36
+ import {registerOutboundHook, registerOutboundHttpRoute, registerOutboundMethods} from './outbound'
37
37
  import {registerTools} from './tools'
38
38
 
39
39
  type PluginRuntime = {
@@ -92,6 +92,13 @@ export type PluginApi = {
92
92
  opts?: {optional?: boolean},
93
93
  ) => void
94
94
  registerChannel: (registration: {plugin: any}) => void
95
+ registerHttpRoute: (params: {
96
+ path: string
97
+ handler: (
98
+ req: import('node:http').IncomingMessage,
99
+ res: import('node:http').ServerResponse,
100
+ ) => Promise<void> | void
101
+ }) => void
95
102
  }
96
103
 
97
104
  export default {
@@ -101,6 +108,7 @@ export default {
101
108
  register(api: PluginApi) {
102
109
  registerOutboundHook(api)
103
110
  registerOutboundMethods(api)
111
+ registerOutboundHttpRoute(api)
104
112
  registerCommands(api)
105
113
  registerTools(api)
106
114
  registerClawlyCronChannel(api)
package/outbound.ts CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  import crypto from 'node:crypto'
10
10
  import fs from 'node:fs'
11
+ import fsp from 'node:fs/promises'
12
+ import type {IncomingMessage, ServerResponse} from 'node:http'
11
13
  import os from 'node:os'
12
14
  import path from 'node:path'
13
15
 
@@ -29,6 +31,15 @@ function ensureOutboundDir(): void {
29
31
  fs.mkdirSync(OUTBOUND_DIR, {recursive: true})
30
32
  }
31
33
 
34
+ async function fileExists(p: string): Promise<boolean> {
35
+ try {
36
+ await fsp.access(p)
37
+ return true
38
+ } catch {
39
+ return false
40
+ }
41
+ }
42
+
32
43
  // ── Hook: tool_result_persist ───────────────────────────────────────
33
44
 
34
45
  export function registerOutboundHook(api: PluginApi) {
@@ -73,16 +84,94 @@ export function registerOutboundMethods(api: PluginApi) {
73
84
 
74
85
  const dest = outboundFilePath(rawPath)
75
86
 
76
- if (!fs.existsSync(dest)) {
87
+ if (!(await fileExists(dest))) {
77
88
  api.logger.warn(`outbound: file not found: ${rawPath} ${dest}`)
78
89
  respond(false, undefined, {code: 'not_found', message: 'outbound file not found'})
79
90
  return
80
91
  }
81
92
 
82
- const buffer = fs.readFileSync(dest)
93
+ const buffer = await fsp.readFile(dest)
83
94
  const base64 = buffer.toString('base64')
84
95
 
85
96
  api.logger.info(`clawly.file.getOutbound: served ${rawPath} (${buffer.length} bytes)`)
86
97
  respond(true, {base64})
87
98
  })
88
99
  }
100
+
101
+ // ── HTTP route: GET /clawly/file/outbound?path=<original-path> ─────────────
102
+
103
+ const MIME: Record<string, string> = {
104
+ '.mp3': 'audio/mpeg',
105
+ '.wav': 'audio/wav',
106
+ '.ogg': 'audio/ogg',
107
+ '.m4a': 'audio/mp4',
108
+ '.aac': 'audio/aac',
109
+ '.flac': 'audio/flac',
110
+ '.webm': 'audio/webm',
111
+ }
112
+
113
+ function sendJson(res: ServerResponse, status: number, body: Record<string, unknown>) {
114
+ res.writeHead(status, {'Content-Type': 'application/json'})
115
+ res.end(JSON.stringify(body))
116
+ }
117
+
118
+ export function registerOutboundHttpRoute(api: PluginApi) {
119
+ api.registerHttpRoute({
120
+ path: '/clawly/file/outbound',
121
+ handler: async (_req: IncomingMessage, res: ServerResponse) => {
122
+ const url = new URL(_req.url ?? '/', 'http://localhost')
123
+ const rawPath = url.searchParams.get('path')?.trim() ?? ''
124
+
125
+ if (!rawPath) {
126
+ sendJson(res, 400, {error: 'path query parameter is required'})
127
+ return
128
+ }
129
+
130
+ const dest = outboundFilePath(rawPath)
131
+
132
+ if (!(await fileExists(dest))) {
133
+ api.logger.warn(`outbound http: file not found: ${rawPath}`)
134
+ sendJson(res, 404, {error: 'outbound file not found'})
135
+ return
136
+ }
137
+
138
+ const ext = path.extname(dest).toLowerCase()
139
+ const contentType = MIME[ext] ?? 'application/octet-stream'
140
+ const stat = await fsp.stat(dest)
141
+ const total = stat.size
142
+ const rangeHeader = _req.headers.range
143
+
144
+ if (rangeHeader) {
145
+ const match = /bytes=(\d+)-(\d*)/.exec(rangeHeader)
146
+ if (match) {
147
+ const start = Number(match[1])
148
+ const end = match[2] ? Number(match[2]) : total - 1
149
+ const chunkSize = end - start + 1
150
+ const stream = fs.createReadStream(dest, {start, end})
151
+
152
+ res.writeHead(206, {
153
+ 'Content-Type': contentType,
154
+ 'Content-Range': `bytes ${start}-${end}/${total}`,
155
+ 'Accept-Ranges': 'bytes',
156
+ 'Content-Length': chunkSize,
157
+ })
158
+ stream.pipe(res)
159
+ api.logger.info(
160
+ `outbound http: served ${rawPath} -> ${dest} range ${start}-${end}/${total}`,
161
+ )
162
+ return
163
+ }
164
+ }
165
+
166
+ const buffer = await fsp.readFile(dest)
167
+ res.writeHead(200, {
168
+ 'Content-Type': contentType,
169
+ 'Content-Length': total,
170
+ 'Accept-Ranges': 'bytes',
171
+ })
172
+ res.end(buffer)
173
+
174
+ api.logger.info(`outbound http: served ${rawPath} -> ${dest} (${total} bytes)`)
175
+ },
176
+ })
177
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {