skyloom 1.22.0 → 1.23.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.
- package/README.md +4 -0
- package/dist/gateway/channels/feishu.d.ts.map +1 -1
- package/dist/gateway/channels/feishu.js +53 -0
- package/dist/gateway/channels/feishu.js.map +1 -1
- package/dist/gateway/channels/qq.d.ts.map +1 -1
- package/dist/gateway/channels/qq.js +45 -0
- package/dist/gateway/channels/qq.js.map +1 -1
- package/dist/gateway/channels/wecom.d.ts.map +1 -1
- package/dist/gateway/channels/wecom.js +41 -0
- package/dist/gateway/channels/wecom.js.map +1 -1
- package/dist/gateway/gateway.d.ts.map +1 -1
- package/dist/gateway/gateway.js +79 -9
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/helpers.d.ts +23 -0
- package/dist/gateway/helpers.d.ts.map +1 -1
- package/dist/gateway/helpers.js +90 -0
- package/dist/gateway/helpers.js.map +1 -1
- package/dist/gateway/types.d.ts +39 -0
- package/dist/gateway/types.d.ts.map +1 -1
- package/dist/gateway/types.js +25 -0
- package/dist/gateway/types.js.map +1 -1
- package/dist/gateway/vision.d.ts +23 -0
- package/dist/gateway/vision.d.ts.map +1 -0
- package/dist/gateway/vision.js +77 -0
- package/dist/gateway/vision.js.map +1 -0
- package/package.json +1 -1
- package/src/gateway/channels/feishu.ts +49 -2
- package/src/gateway/channels/qq.ts +43 -2
- package/src/gateway/channels/wecom.ts +47 -2
- package/src/gateway/gateway.ts +77 -8
- package/src/gateway/helpers.ts +60 -0
- package/src/gateway/types.ts +58 -0
- package/src/gateway/vision.ts +78 -0
- package/tests/gateway.test.ts +84 -1
package/dist/gateway/helpers.js
CHANGED
|
@@ -4,6 +4,39 @@
|
|
|
4
4
|
* fallback), and a tiny JSON HTTP client. Kept dependency-light (axios is
|
|
5
5
|
* already a project dep) and injectable-free — adapters call these directly.
|
|
6
6
|
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
7
40
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
41
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
42
|
};
|
|
@@ -12,7 +45,12 @@ exports.TokenCache = void 0;
|
|
|
12
45
|
exports.resolveSecret = resolveSecret;
|
|
13
46
|
exports.postJson = postJson;
|
|
14
47
|
exports.getJson = getJson;
|
|
48
|
+
exports.loadMedia = loadMedia;
|
|
49
|
+
exports.isSendableSrc = isSendableSrc;
|
|
50
|
+
exports.postMultipart = postMultipart;
|
|
15
51
|
const axios_1 = __importDefault(require("axios"));
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
16
54
|
/**
|
|
17
55
|
* Resolve a secret/config value. Accepts a literal string, or an env-ref object
|
|
18
56
|
* `{ source: 'env', id: 'NAME' }` (OpenClaw-compatible), falling back to the
|
|
@@ -54,6 +92,58 @@ async function getJson(url, opts) {
|
|
|
54
92
|
});
|
|
55
93
|
return res.data;
|
|
56
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Load media bytes from a local filesystem path or an http(s) URL. Local paths
|
|
97
|
+
* are read directly; remote URLs are fetched (capped at 30 MiB to avoid
|
|
98
|
+
* pulling something huge into memory). Throws if the source can't be loaded.
|
|
99
|
+
*/
|
|
100
|
+
async function loadMedia(src) {
|
|
101
|
+
if (/^https?:\/\//i.test(src)) {
|
|
102
|
+
const res = await axios_1.default.get(src, {
|
|
103
|
+
responseType: 'arraybuffer',
|
|
104
|
+
timeout: 30000,
|
|
105
|
+
maxContentLength: 30 * 1024 * 1024,
|
|
106
|
+
validateStatus: (s) => s >= 200 && s < 300,
|
|
107
|
+
});
|
|
108
|
+
const urlName = path.basename(new URL(src).pathname) || 'file';
|
|
109
|
+
const ct = res.headers['content-type'];
|
|
110
|
+
return {
|
|
111
|
+
data: Buffer.from(res.data),
|
|
112
|
+
filename: urlName,
|
|
113
|
+
contentType: typeof ct === 'string' ? ct : undefined,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const data = fs.readFileSync(src); // throws ENOENT if missing — caller handles
|
|
117
|
+
return { data, filename: path.basename(src) };
|
|
118
|
+
}
|
|
119
|
+
/** Is this a sendable media source (http(s) URL or an existing local file)? */
|
|
120
|
+
function isSendableSrc(src) {
|
|
121
|
+
if (/^https?:\/\//i.test(src))
|
|
122
|
+
return true;
|
|
123
|
+
try {
|
|
124
|
+
return fs.existsSync(src) && fs.statSync(src).isFile();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** POST multipart/form-data (Node 18+ FormData/Blob), return parsed JSON. */
|
|
131
|
+
async function postMultipart(url, fields, opts) {
|
|
132
|
+
const form = new FormData();
|
|
133
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
134
|
+
if (typeof v === 'string')
|
|
135
|
+
form.append(k, v);
|
|
136
|
+
else
|
|
137
|
+
form.append(k, new Blob([v.data], v.contentType ? { type: v.contentType } : undefined), v.filename);
|
|
138
|
+
}
|
|
139
|
+
const res = await axios_1.default.post(url, form, {
|
|
140
|
+
headers: { ...(opts?.headers || {}) },
|
|
141
|
+
timeout: opts?.timeoutMs ?? 30000,
|
|
142
|
+
maxBodyLength: Infinity,
|
|
143
|
+
validateStatus: (s) => s >= 200 && s < 300,
|
|
144
|
+
});
|
|
145
|
+
return res.data;
|
|
146
|
+
}
|
|
57
147
|
/**
|
|
58
148
|
* A small token cache: fetch an access token via `fetcher`, cache it until it
|
|
59
149
|
* is near expiry, and refresh transparently. Channels (Feishu/WeCom) all need
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/gateway/helpers.ts"],"names":[],"mappings":";AAAA;;;;GAIG
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/gateway/helpers.ts"],"names":[],"mappings":"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"}
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -39,6 +39,29 @@ export interface MediaAttachment {
|
|
|
39
39
|
/** Direct URL, when the platform provides one. */
|
|
40
40
|
url?: string;
|
|
41
41
|
}
|
|
42
|
+
/** An outbound media item the agent wants to send (parsed from its reply). */
|
|
43
|
+
export interface OutboundMedia {
|
|
44
|
+
kind: 'image' | 'file';
|
|
45
|
+
/** Local filesystem path or http(s) URL to the binary. */
|
|
46
|
+
src: string;
|
|
47
|
+
/** Optional caption / alt text. */
|
|
48
|
+
alt?: string;
|
|
49
|
+
}
|
|
50
|
+
/** The result of splitting an agent reply into plain text + outbound media. */
|
|
51
|
+
export interface ParsedReply {
|
|
52
|
+
text: string;
|
|
53
|
+
media: OutboundMedia[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse media directives out of an agent's reply so channels can upload+send
|
|
57
|
+
* them. Recognized forms (stripped from the returned text):
|
|
58
|
+
* - Markdown image: 
|
|
59
|
+
* - Explicit image: [[image:src]] or [[image:src|alt]]
|
|
60
|
+
* - Explicit file: [[file:src]] or [[file:src|alt]]
|
|
61
|
+
* `src` is a local path or http(s) URL. Only http(s) and existing local files
|
|
62
|
+
* are treated as media; anything else is left in the text untouched.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseReply(reply: string): ParsedReply;
|
|
42
65
|
/** Render a media list into a compact, model-readable description line. */
|
|
43
66
|
export declare function describeMedia(media: MediaAttachment[] | undefined): string;
|
|
44
67
|
/** Opaque, channel-specific destination for an outbound reply. */
|
|
@@ -98,6 +121,22 @@ export interface ChannelAdapter {
|
|
|
98
121
|
* should throttle their own updates and tolerate an empty/aborted stream.
|
|
99
122
|
*/
|
|
100
123
|
sendStreaming?(target: ReplyTarget, chunks: AsyncIterable<string>): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Optional: upload and send an image or file. When an adapter implements this,
|
|
126
|
+
* the gateway extracts media directives from the agent's reply (parseReply)
|
|
127
|
+
* and delivers them after the text. Adapters without it simply keep the
|
|
128
|
+
* media reference in the text.
|
|
129
|
+
*/
|
|
130
|
+
sendMedia?(target: ReplyTarget, item: OutboundMedia): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Optional: download an inbound media attachment's bytes so the gateway can
|
|
133
|
+
* run vision over an image. `att` is one entry from InboundMessage.media.
|
|
134
|
+
* Returns the binary or null if it can't be fetched.
|
|
135
|
+
*/
|
|
136
|
+
fetchMedia?(att: MediaAttachment, msg: InboundMessage): Promise<{
|
|
137
|
+
data: Buffer;
|
|
138
|
+
contentType?: string;
|
|
139
|
+
} | null>;
|
|
101
140
|
}
|
|
102
141
|
/** Factory signature: build an adapter from its config block (or null if disabled/misconfigured). */
|
|
103
142
|
export type ChannelFactory = (cfg: any, env: NodeJS.ProcessEnv) => ChannelAdapter | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/gateway/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAEhD,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,WAAW,CAAC;IACrB,oEAAoE;IACpE,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,wEAAwE;IACxE,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACjE,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,SAAS,GAAG,MAAM,CAQ1E;AAED,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,mBAAmB,CAAC;IAC7B,KAAK,EAAE,eAAe,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B,sEAAsE;IACtE,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,6CAA6C;IAC7C,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB;;;;OAIG;IACH,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAExD,6CAA6C;IAC7C,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvD;;;;;OAKG;IACH,aAAa,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/gateway/types.ts"],"names":[],"mappings":"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"}
|
package/dist/gateway/types.js
CHANGED
|
@@ -11,7 +11,32 @@
|
|
|
11
11
|
* stays platform-neutral.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.parseReply = parseReply;
|
|
14
15
|
exports.describeMedia = describeMedia;
|
|
16
|
+
/**
|
|
17
|
+
* Parse media directives out of an agent's reply so channels can upload+send
|
|
18
|
+
* them. Recognized forms (stripped from the returned text):
|
|
19
|
+
* - Markdown image: 
|
|
20
|
+
* - Explicit image: [[image:src]] or [[image:src|alt]]
|
|
21
|
+
* - Explicit file: [[file:src]] or [[file:src|alt]]
|
|
22
|
+
* `src` is a local path or http(s) URL. Only http(s) and existing local files
|
|
23
|
+
* are treated as media; anything else is left in the text untouched.
|
|
24
|
+
*/
|
|
25
|
+
function parseReply(reply) {
|
|
26
|
+
const media = [];
|
|
27
|
+
let text = reply;
|
|
28
|
+
// [[image:src|alt]] / [[file:src|alt]]
|
|
29
|
+
text = text.replace(/\[\[(image|file):([^\]|]+)(?:\|([^\]]*))?\]\]/gi, (_m, kind, src, alt) => {
|
|
30
|
+
media.push({ kind: kind.toLowerCase(), src: String(src).trim(), alt: alt ? String(alt).trim() : undefined });
|
|
31
|
+
return '';
|
|
32
|
+
});
|
|
33
|
+
// Markdown images: 
|
|
34
|
+
text = text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, alt, src) => {
|
|
35
|
+
media.push({ kind: 'image', src: String(src).trim(), alt: alt ? String(alt).trim() : undefined });
|
|
36
|
+
return '';
|
|
37
|
+
});
|
|
38
|
+
return { text: text.replace(/\n{3,}/g, '\n\n').trim(), media };
|
|
39
|
+
}
|
|
15
40
|
/** Render a media list into a compact, model-readable description line. */
|
|
16
41
|
function describeMedia(media) {
|
|
17
42
|
if (!media || media.length === 0)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/gateway/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/gateway/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AA2DH,gCAiBC;AAGD,sCAQC;AArCD;;;;;;;;GAQG;AACH,SAAgB,UAAU,CAAC,KAAa;IACtC,MAAM,KAAK,GAAoB,EAAE,CAAC;IAClC,IAAI,IAAI,GAAG,KAAK,CAAC;IAEjB,uCAAuC;IACvC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,iDAAiD,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAC5F,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,EAAsB,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;QACjI,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;IAEH,+BAA+B;IAC/B,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,4CAA4C,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACjF,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;QAClG,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC;AACjE,CAAC;AAED,2EAA2E;AAC3E,SAAgB,aAAa,CAAC,KAAoC;IAChE,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC;QACjD,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACnD,OAAO,IAAI,GAAG,GAAG,CAAC;IACpB,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vision describe — turn an inbound image into a text description so the agent
|
|
3
|
+
* can "see" what the user sent, without rewiring the core text-only LLM loop.
|
|
4
|
+
*
|
|
5
|
+
* Self-contained on purpose: a single OpenAI-compatible chat/completions call
|
|
6
|
+
* with an image_url (base64 data URL) content block. The model + key are
|
|
7
|
+
* resolved from config.channels.<id>.visionModel / config.llm.vision_model
|
|
8
|
+
* (default gpt-4o-mini), falling back to env keys the same way the rest of
|
|
9
|
+
* Skyloom does. If no key/model is available, vision is skipped silently and the
|
|
10
|
+
* gateway just uses the media description line.
|
|
11
|
+
*/
|
|
12
|
+
import type { LoadedMedia } from './helpers';
|
|
13
|
+
export interface VisionOptions {
|
|
14
|
+
model?: string;
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
prompt?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Describe one or more images. Returns a description string, or null if vision
|
|
20
|
+
* is unavailable (no key/model) or fails — callers fall back to the media line.
|
|
21
|
+
*/
|
|
22
|
+
export declare function describeImages(images: LoadedMedia[], opts?: VisionOptions): Promise<string | null>;
|
|
23
|
+
//# sourceMappingURL=vision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vision.d.ts","sourceRoot":"","sources":["../../src/gateway/vision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAyB7C,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4B5G"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vision describe — turn an inbound image into a text description so the agent
|
|
4
|
+
* can "see" what the user sent, without rewiring the core text-only LLM loop.
|
|
5
|
+
*
|
|
6
|
+
* Self-contained on purpose: a single OpenAI-compatible chat/completions call
|
|
7
|
+
* with an image_url (base64 data URL) content block. The model + key are
|
|
8
|
+
* resolved from config.channels.<id>.visionModel / config.llm.vision_model
|
|
9
|
+
* (default gpt-4o-mini), falling back to env keys the same way the rest of
|
|
10
|
+
* Skyloom does. If no key/model is available, vision is skipped silently and the
|
|
11
|
+
* gateway just uses the media description line.
|
|
12
|
+
*/
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.describeImages = describeImages;
|
|
18
|
+
const axios_1 = __importDefault(require("axios"));
|
|
19
|
+
const logger_1 = require("../core/logger");
|
|
20
|
+
const log = (0, logger_1.getLogger)('gateway-vision');
|
|
21
|
+
/** OpenAI-compatible base URL for a provider inferred from the model id. */
|
|
22
|
+
function baseUrlFor(model) {
|
|
23
|
+
const l = model.toLowerCase();
|
|
24
|
+
if (l.includes('claude'))
|
|
25
|
+
return 'https://api.anthropic.com/v1'; // not OpenAI-shaped; skipped below
|
|
26
|
+
if (l.includes('gemini'))
|
|
27
|
+
return 'https://generativelanguage.googleapis.com/v1beta/openai';
|
|
28
|
+
if (l.includes('grok') || l.includes('xai'))
|
|
29
|
+
return 'https://api.x.ai/v1';
|
|
30
|
+
if (l.includes('qwen') || l.includes('dashscope'))
|
|
31
|
+
return 'https://dashscope.aliyuncs.com/compatible-mode/v1';
|
|
32
|
+
return 'https://api.openai.com/v1';
|
|
33
|
+
}
|
|
34
|
+
/** Resolve an API key for the vision model from env (best-effort). */
|
|
35
|
+
function keyFor(model, env) {
|
|
36
|
+
const l = model.toLowerCase();
|
|
37
|
+
const candidates = l.includes('gemini') ? ['GEMINI_API_KEY', 'GOOGLE_API_KEY']
|
|
38
|
+
: l.includes('grok') || l.includes('xai') ? ['XAI_API_KEY']
|
|
39
|
+
: l.includes('qwen') || l.includes('dashscope') ? ['DASHSCOPE_API_KEY', 'QWEN_API_KEY']
|
|
40
|
+
: ['OPENAI_API_KEY'];
|
|
41
|
+
for (const c of candidates)
|
|
42
|
+
if (env[c])
|
|
43
|
+
return env[c];
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Describe one or more images. Returns a description string, or null if vision
|
|
48
|
+
* is unavailable (no key/model) or fails — callers fall back to the media line.
|
|
49
|
+
*/
|
|
50
|
+
async function describeImages(images, opts = {}) {
|
|
51
|
+
if (!images.length)
|
|
52
|
+
return null;
|
|
53
|
+
const env = opts.env || process.env;
|
|
54
|
+
const model = opts.model || 'gpt-4o-mini';
|
|
55
|
+
// Anthropic isn't OpenAI-chat-shaped here; skip to keep this helper simple.
|
|
56
|
+
if (model.toLowerCase().includes('claude'))
|
|
57
|
+
return null;
|
|
58
|
+
const key = keyFor(model, env);
|
|
59
|
+
if (!key)
|
|
60
|
+
return null;
|
|
61
|
+
const prompt = opts.prompt || '请用中文简洁描述这些图片的内容(关键物体、文字、场景);如果含可读文字请转写出来。';
|
|
62
|
+
const content = [{ type: 'text', text: prompt }];
|
|
63
|
+
for (const img of images.slice(0, 4)) {
|
|
64
|
+
const mime = img.contentType || 'image/png';
|
|
65
|
+
content.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${img.data.toString('base64')}` } });
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const res = await axios_1.default.post(`${baseUrlFor(model)}/chat/completions`, { model, messages: [{ role: 'user', content }], max_tokens: 500, temperature: 0.2 }, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` }, timeout: 30000, validateStatus: (s) => s >= 200 && s < 300 });
|
|
69
|
+
const text = res.data?.choices?.[0]?.message?.content;
|
|
70
|
+
return typeof text === 'string' && text.trim() ? text.trim() : null;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
log.warn('vision_describe_failed', { model, error: String(e).slice(0, 160) });
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=vision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vision.js","sourceRoot":"","sources":["../../src/gateway/vision.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import * as crypto from 'crypto';
|
|
17
|
+
import axios from 'axios';
|
|
17
18
|
import { getLogger } from '../../core/logger';
|
|
18
|
-
import { resolveSecret, postJson, TokenCache } from '../helpers';
|
|
19
|
-
import type { ChannelAdapter, MediaAttachment, RawRequest, ReplyTarget, WebhookOutcome } from '../types';
|
|
19
|
+
import { resolveSecret, postJson, postMultipart, loadMedia, TokenCache } from '../helpers';
|
|
20
|
+
import type { ChannelAdapter, InboundMessage, MediaAttachment, OutboundMedia, RawRequest, ReplyTarget, WebhookOutcome } from '../types';
|
|
20
21
|
|
|
21
22
|
const log = getLogger('channel-feishu');
|
|
22
23
|
|
|
@@ -238,5 +239,51 @@ export function createFeishuAdapter(cfg: any, env: NodeJS.ProcessEnv): ChannelAd
|
|
|
238
239
|
// Final flush so the last tokens always land.
|
|
239
240
|
if (dirty || acc) await patchCard(messageId, acc.trim() || '(无回复)');
|
|
240
241
|
},
|
|
242
|
+
|
|
243
|
+
async sendMedia(target: ReplyTarget, item: OutboundMedia): Promise<void> {
|
|
244
|
+
const chatId = target.chatId as string;
|
|
245
|
+
if (!chatId) return;
|
|
246
|
+
const loaded = await loadMedia(item.src);
|
|
247
|
+
const headers = await authHeader();
|
|
248
|
+
|
|
249
|
+
if (item.kind === 'image') {
|
|
250
|
+
const up = await postMultipart(`${base}/open-apis/im/v1/images`, {
|
|
251
|
+
image_type: 'message',
|
|
252
|
+
image: { data: loaded.data, filename: loaded.filename || 'image', contentType: loaded.contentType || 'image/png' },
|
|
253
|
+
}, { headers });
|
|
254
|
+
if (up.code !== 0) { onTokenError(up.code); throw new Error(`feishu image upload ${up.code}: ${up.msg}`); }
|
|
255
|
+
const imageKey = up.data?.image_key;
|
|
256
|
+
const send = await postJson(`${base}/open-apis/im/v1/messages?receive_id_type=chat_id`,
|
|
257
|
+
{ receive_id: chatId, msg_type: 'image', content: JSON.stringify({ image_key: imageKey }) },
|
|
258
|
+
{ headers });
|
|
259
|
+
if (send.code !== 0) { onTokenError(send.code); throw new Error(`feishu image send ${send.code}: ${send.msg}`); }
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// file: upload to im/v1/files then send a file message
|
|
264
|
+
const up = await postMultipart(`${base}/open-apis/im/v1/files`, {
|
|
265
|
+
file_type: 'stream',
|
|
266
|
+
file_name: loaded.filename || 'file',
|
|
267
|
+
file: { data: loaded.data, filename: loaded.filename || 'file', contentType: loaded.contentType || 'application/octet-stream' },
|
|
268
|
+
}, { headers });
|
|
269
|
+
if (up.code !== 0) { onTokenError(up.code); throw new Error(`feishu file upload ${up.code}: ${up.msg}`); }
|
|
270
|
+
const fileKey = up.data?.file_key;
|
|
271
|
+
const send = await postJson(`${base}/open-apis/im/v1/messages?receive_id_type=chat_id`,
|
|
272
|
+
{ receive_id: chatId, msg_type: 'file', content: JSON.stringify({ file_key: fileKey }) },
|
|
273
|
+
{ headers });
|
|
274
|
+
if (send.code !== 0) { onTokenError(send.code); throw new Error(`feishu file send ${send.code}: ${send.msg}`); }
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async fetchMedia(att: MediaAttachment, msg: InboundMessage): Promise<{ data: Buffer; contentType?: string } | null> {
|
|
278
|
+
const messageId = (msg.raw as any)?.event?.message?.message_id;
|
|
279
|
+
if (!messageId || !att.ref) return null;
|
|
280
|
+
const token = await tokenCache.get();
|
|
281
|
+
const res = await axios.get(
|
|
282
|
+
`${base}/open-apis/im/v1/messages/${messageId}/resources/${att.ref}?type=${att.kind === 'image' ? 'image' : 'file'}`,
|
|
283
|
+
{ headers: { Authorization: `Bearer ${token}` }, responseType: 'arraybuffer', timeout: 30000, validateStatus: (s) => s >= 200 && s < 300 },
|
|
284
|
+
);
|
|
285
|
+
const ct = res.headers['content-type'];
|
|
286
|
+
return { data: Buffer.from(res.data), contentType: typeof ct === 'string' ? ct : undefined };
|
|
287
|
+
},
|
|
241
288
|
};
|
|
242
289
|
}
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import * as crypto from 'crypto';
|
|
19
19
|
import { getLogger } from '../../core/logger';
|
|
20
|
-
import { resolveSecret, postJson, TokenCache } from '../helpers';
|
|
21
|
-
import type { ChannelAdapter, MediaAttachment, RawRequest, ReplyTarget, WebhookOutcome } from '../types';
|
|
20
|
+
import { resolveSecret, postJson, loadMedia, TokenCache } from '../helpers';
|
|
21
|
+
import type { ChannelAdapter, MediaAttachment, OutboundMedia, RawRequest, ReplyTarget, WebhookOutcome } from '../types';
|
|
22
22
|
|
|
23
23
|
const log = getLogger('channel-qq');
|
|
24
24
|
|
|
@@ -147,5 +147,46 @@ export function createQQAdapter(cfg: any, env: NodeJS.ProcessEnv): ChannelAdapte
|
|
|
147
147
|
throw new Error(`qq send error: ${e?.response?.status || ''} ${String(e?.message || e).slice(0, 120)}`);
|
|
148
148
|
}
|
|
149
149
|
},
|
|
150
|
+
|
|
151
|
+
// QQ's v2 rich-media flow takes a URL (the platform fetches it): POST
|
|
152
|
+
// /files → file_info, then send msg_type:7 referencing that file_info.
|
|
153
|
+
// Group/C2C only; raw local bytes aren't supported, so the src must be a URL.
|
|
154
|
+
async sendMedia(target: ReplyTarget, item: OutboundMedia): Promise<void> {
|
|
155
|
+
if (!/^https?:\/\//i.test(item.src)) {
|
|
156
|
+
throw new Error('qq sendMedia requires an http(s) URL (platform fetches it)');
|
|
157
|
+
}
|
|
158
|
+
const base = target.kind === 'group'
|
|
159
|
+
? `https://api.sgroup.qq.com/v2/groups/${target.groupOpenid}`
|
|
160
|
+
: target.kind === 'c2c'
|
|
161
|
+
? `https://api.sgroup.qq.com/v2/users/${target.userOpenid}`
|
|
162
|
+
: null;
|
|
163
|
+
if (!base) throw new Error('qq sendMedia unsupported for channel target');
|
|
164
|
+
const headers = { ...(await authHeaders()), 'Content-Type': 'application/json' };
|
|
165
|
+
// file_type: 1=image 2=video 3=audio 4=file
|
|
166
|
+
const fileType = item.kind === 'image' ? 1 : 4;
|
|
167
|
+
let fileInfo: string;
|
|
168
|
+
try {
|
|
169
|
+
const up = await postJson(`${base}/files`, { file_type: fileType, url: item.src, srv_send_msg: false }, { headers });
|
|
170
|
+
fileInfo = up.file_info;
|
|
171
|
+
} catch (e: any) {
|
|
172
|
+
if (e?.response?.status === 401) tokenCache.invalidate();
|
|
173
|
+
throw new Error(`qq file upload error: ${e?.response?.status || ''} ${String(e?.message || e).slice(0, 120)}`);
|
|
174
|
+
}
|
|
175
|
+
const payload: any = { msg_type: 7, media: { file_info: fileInfo } };
|
|
176
|
+
if (target.msgId) payload.msg_id = target.msgId;
|
|
177
|
+
await postJson(`${base}/messages`, payload, { headers });
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async fetchMedia(att: MediaAttachment): Promise<{ data: Buffer; contentType?: string } | null> {
|
|
181
|
+
// QQ delivers attachments with a direct URL — just download it.
|
|
182
|
+
if (!att.url) return null;
|
|
183
|
+
try {
|
|
184
|
+
const loaded = await loadMedia(att.url);
|
|
185
|
+
return { data: loaded.data, contentType: loaded.contentType || att.mimeType };
|
|
186
|
+
} catch (e) {
|
|
187
|
+
log.warn('qq_media_fetch_failed', { error: String(e) });
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
150
191
|
};
|
|
151
192
|
}
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import * as crypto from 'crypto';
|
|
19
|
+
import axios from 'axios';
|
|
19
20
|
import { getLogger } from '../../core/logger';
|
|
20
|
-
import { resolveSecret, postJson, getJson, TokenCache } from '../helpers';
|
|
21
|
-
import type { ChannelAdapter, MediaAttachment, RawRequest, ReplyTarget, WebhookOutcome } from '../types';
|
|
21
|
+
import { resolveSecret, postJson, getJson, postMultipart, loadMedia, TokenCache } from '../helpers';
|
|
22
|
+
import type { ChannelAdapter, MediaAttachment, OutboundMedia, RawRequest, ReplyTarget, WebhookOutcome } from '../types';
|
|
22
23
|
|
|
23
24
|
const log = getLogger('channel-wecom');
|
|
24
25
|
|
|
@@ -147,5 +148,49 @@ export function createWecomAdapter(cfg: any, env: NodeJS.ProcessEnv): ChannelAda
|
|
|
147
148
|
throw new Error(`wecom send error ${data.errcode}: ${data.errmsg}`);
|
|
148
149
|
}
|
|
149
150
|
},
|
|
151
|
+
|
|
152
|
+
async sendMedia(target: ReplyTarget, item: OutboundMedia): Promise<void> {
|
|
153
|
+
const toUser = target.toUser as string;
|
|
154
|
+
if (!toUser || !agentId) return;
|
|
155
|
+
const loaded = await loadMedia(item.src);
|
|
156
|
+
const accessToken = await tokenCache.get();
|
|
157
|
+
const type = item.kind === 'image' ? 'image' : 'file';
|
|
158
|
+
// Upload to the temporary-media store (valid 3 days), then push by media_id.
|
|
159
|
+
const up = await postMultipart(
|
|
160
|
+
`https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=${encodeURIComponent(accessToken)}&type=${type}`,
|
|
161
|
+
{ media: { data: loaded.data, filename: loaded.filename || (type === 'image' ? 'image.png' : 'file'), contentType: loaded.contentType } },
|
|
162
|
+
);
|
|
163
|
+
if (up.errcode && up.errcode !== 0) {
|
|
164
|
+
if (up.errcode === 42001 || up.errcode === 40014) tokenCache.invalidate();
|
|
165
|
+
throw new Error(`wecom media upload ${up.errcode}: ${up.errmsg}`);
|
|
166
|
+
}
|
|
167
|
+
const mediaId = up.media_id;
|
|
168
|
+
const body: any = { touser: toUser, msgtype: type, agentid: Number(agentId) };
|
|
169
|
+
body[type] = { media_id: mediaId };
|
|
170
|
+
const send = await postJson(
|
|
171
|
+
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${encodeURIComponent(accessToken)}`,
|
|
172
|
+
body,
|
|
173
|
+
);
|
|
174
|
+
if (send.errcode !== 0) {
|
|
175
|
+
if (send.errcode === 42001 || send.errcode === 40014) tokenCache.invalidate();
|
|
176
|
+
throw new Error(`wecom media send ${send.errcode}: ${send.errmsg}`);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async fetchMedia(att: MediaAttachment): Promise<{ data: Buffer; contentType?: string } | null> {
|
|
181
|
+
if (!att.ref) return null;
|
|
182
|
+
const accessToken = await tokenCache.get();
|
|
183
|
+
const res = await axios.get(
|
|
184
|
+
`https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=${encodeURIComponent(accessToken)}&media_id=${encodeURIComponent(att.ref)}`,
|
|
185
|
+
{ responseType: 'arraybuffer', timeout: 30000, validateStatus: (s) => s >= 200 && s < 300 },
|
|
186
|
+
);
|
|
187
|
+
// An error comes back as JSON, not the binary — detect and bail.
|
|
188
|
+
const ct = res.headers['content-type'];
|
|
189
|
+
if (typeof ct === 'string' && ct.includes('application/json')) {
|
|
190
|
+
log.warn('wecom_media_get_failed', { body: Buffer.from(res.data).toString('utf8').slice(0, 120) });
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return { data: Buffer.from(res.data), contentType: typeof ct === 'string' ? ct : undefined };
|
|
194
|
+
},
|
|
150
195
|
};
|
|
151
196
|
}
|