skyloom 1.22.0 → 1.24.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 +10 -1
- package/dist/cli/main.js +69 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/core/commands.d.ts.map +1 -1
- package/dist/core/commands.js +10 -0
- package/dist/core/commands.js.map +1 -1
- 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/qr.d.ts +8 -0
- package/dist/gateway/qr.d.ts.map +1 -0
- package/dist/gateway/qr.js +23 -0
- package/dist/gateway/qr.js.map +1 -0
- package/dist/gateway/setup.d.ts +57 -0
- package/dist/gateway/setup.d.ts.map +1 -0
- package/dist/gateway/setup.js +127 -0
- package/dist/gateway/setup.js.map +1 -0
- 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 +2 -1
- package/src/cli/main.ts +62 -0
- package/src/core/commands.ts +10 -0
- 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/qr.ts +21 -0
- package/src/gateway/setup.ts +145 -0
- package/src/gateway/types.ts +58 -0
- package/src/gateway/vision.ts +78 -0
- package/tests/channel_setup.test.ts +88 -0
- package/tests/gateway.test.ts +84 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Terminal QR rendering — a thin, dependency-isolated wrapper over
|
|
4
|
+
* qrcode-terminal (single file, zero transitive deps). Returns the QR as a
|
|
5
|
+
* string so callers control where it's written (and so it's testable).
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.renderQR = renderQR;
|
|
9
|
+
// qrcode-terminal ships no types; declare the tiny surface we use.
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
+
const qrcode = require('qrcode-terminal');
|
|
12
|
+
/** Render `text` as a scannable QR code (compact) into a string. */
|
|
13
|
+
function renderQR(text) {
|
|
14
|
+
let out = '';
|
|
15
|
+
try {
|
|
16
|
+
qrcode.generate(text, { small: true }, (s) => { out = s; });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=qr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/gateway/qr.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAQH,4BAQC;AAdD,mEAAmE;AACnE,iEAAiE;AACjE,MAAM,MAAM,GACV,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE7B,oEAAoE;AACpE,SAAgB,QAAQ,CAAC,IAAY;IACnC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAS,EAAE,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel setup metadata + persistence — the data behind the `sky channels`
|
|
3
|
+
* wizard. Each channel declares the credential fields it needs, where to create
|
|
4
|
+
* the bot (a platform console URL we render as a QR for quick mobile access),
|
|
5
|
+
* and a short how-to. Kept pure/testable; the interactive prompts live in the
|
|
6
|
+
* CLI, and QR rendering is a thin wrapper over qrcode-terminal.
|
|
7
|
+
*
|
|
8
|
+
* Note: Feishu / WeCom / QQ are all official-bot APIs — credentials are created
|
|
9
|
+
* in each platform's developer console, there is no "scan to log in" the way
|
|
10
|
+
* personal WeChat works. So the QR here is a convenience link to the console
|
|
11
|
+
* (scan on your phone → open the console → create the bot → copy the keys), plus
|
|
12
|
+
* a QR of the gateway callback URL to paste back into the console.
|
|
13
|
+
*/
|
|
14
|
+
export interface ChannelField {
|
|
15
|
+
/** Config key under channels.<id>. */
|
|
16
|
+
key: string;
|
|
17
|
+
/** Human label shown in the wizard. */
|
|
18
|
+
label: string;
|
|
19
|
+
/** Whether the wizard must collect it (some are optional). */
|
|
20
|
+
required: boolean;
|
|
21
|
+
/** Treat as a secret (mask input / store as env-ref suggestion). */
|
|
22
|
+
secret?: boolean;
|
|
23
|
+
/** Env var that also supplies this value. */
|
|
24
|
+
env?: string;
|
|
25
|
+
/** One-line hint on where to find it. */
|
|
26
|
+
hint?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ChannelSetupSpec {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
/** Platform console where the bot/app is created (rendered as a QR). */
|
|
32
|
+
consoleUrl: string;
|
|
33
|
+
/** Docs link for the full setup walkthrough. */
|
|
34
|
+
docsUrl?: string;
|
|
35
|
+
/** Webhook path the platform must call back. */
|
|
36
|
+
webhookPath: string;
|
|
37
|
+
/** Ordered credential fields to collect. */
|
|
38
|
+
fields: ChannelField[];
|
|
39
|
+
/** Short, numbered how-to shown before collecting fields. */
|
|
40
|
+
steps: string[];
|
|
41
|
+
}
|
|
42
|
+
export declare const CHANNEL_SETUP: Record<string, ChannelSetupSpec>;
|
|
43
|
+
export declare const SETUP_CHANNEL_IDS: string[];
|
|
44
|
+
/** Build the full webhook callback URL for a channel from a public base. */
|
|
45
|
+
export declare function callbackUrl(base: string, channelId: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Persist a channel's collected values into ~/.skyloom/config.yaml under
|
|
48
|
+
* channels.<id>, merging with any existing block. Secret-looking values are
|
|
49
|
+
* stored as-is (the file is chmod 0600); callers may instead keep secrets in
|
|
50
|
+
* env and store an { source: env, id } ref. Returns the config path written.
|
|
51
|
+
*/
|
|
52
|
+
export declare function saveChannelConfig(channelId: string, values: Record<string, string>, opts?: {
|
|
53
|
+
configPath?: string;
|
|
54
|
+
}): string;
|
|
55
|
+
/** Which required fields are still missing from a values map. */
|
|
56
|
+
export declare function missingRequired(channelId: string, values: Record<string, string>): string[];
|
|
57
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/gateway/setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,YAAY;IAC3B,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,QAAQ,EAAE,OAAO,CAAC;IAClB,oEAAoE;IACpE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAyD1D,CAAC;AAEF,eAAO,MAAM,iBAAiB,UAA6B,CAAC;AAE5D,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKnE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,MAAM,CAcR;AAED,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAI3F"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Channel setup metadata + persistence — the data behind the `sky channels`
|
|
4
|
+
* wizard. Each channel declares the credential fields it needs, where to create
|
|
5
|
+
* the bot (a platform console URL we render as a QR for quick mobile access),
|
|
6
|
+
* and a short how-to. Kept pure/testable; the interactive prompts live in the
|
|
7
|
+
* CLI, and QR rendering is a thin wrapper over qrcode-terminal.
|
|
8
|
+
*
|
|
9
|
+
* Note: Feishu / WeCom / QQ are all official-bot APIs — credentials are created
|
|
10
|
+
* in each platform's developer console, there is no "scan to log in" the way
|
|
11
|
+
* personal WeChat works. So the QR here is a convenience link to the console
|
|
12
|
+
* (scan on your phone → open the console → create the bot → copy the keys), plus
|
|
13
|
+
* a QR of the gateway callback URL to paste back into the console.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.SETUP_CHANNEL_IDS = exports.CHANNEL_SETUP = void 0;
|
|
17
|
+
exports.callbackUrl = callbackUrl;
|
|
18
|
+
exports.saveChannelConfig = saveChannelConfig;
|
|
19
|
+
exports.missingRequired = missingRequired;
|
|
20
|
+
exports.CHANNEL_SETUP = {
|
|
21
|
+
feishu: {
|
|
22
|
+
id: 'feishu',
|
|
23
|
+
name: '飞书 / Lark',
|
|
24
|
+
consoleUrl: 'https://open.feishu.cn/app',
|
|
25
|
+
docsUrl: 'https://open.feishu.cn/document/home/index',
|
|
26
|
+
webhookPath: '/webhook/feishu',
|
|
27
|
+
fields: [
|
|
28
|
+
{ key: 'appId', label: 'App ID', required: true, env: 'FEISHU_APP_ID', hint: '开发者后台 → 凭证与基础信息 → App ID' },
|
|
29
|
+
{ key: 'appSecret', label: 'App Secret', required: true, secret: true, env: 'FEISHU_APP_SECRET', hint: '同页 App Secret' },
|
|
30
|
+
{ key: 'verificationToken', label: 'Verification Token', required: false, secret: true, env: 'FEISHU_VERIFICATION_TOKEN', hint: '事件订阅 → Verification Token(可选)' },
|
|
31
|
+
{ key: 'encryptKey', label: 'Encrypt Key', required: false, secret: true, env: 'FEISHU_ENCRYPT_KEY', hint: '事件订阅 → Encrypt Key(开启加密时填)' },
|
|
32
|
+
],
|
|
33
|
+
steps: [
|
|
34
|
+
'扫码或打开 https://open.feishu.cn/app 创建「企业自建应用」',
|
|
35
|
+
'在「凭证与基础信息」复制 App ID / App Secret',
|
|
36
|
+
'开启「机器人」能力,在「权限管理」添加 im:message 等权限',
|
|
37
|
+
'「事件订阅」填入下方回调 URL,订阅 im.message.receive_v1',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
wecom: {
|
|
41
|
+
id: 'wecom',
|
|
42
|
+
name: '企业微信 WeCom',
|
|
43
|
+
consoleUrl: 'https://work.weixin.qq.com/wework_admin/frame',
|
|
44
|
+
docsUrl: 'https://developer.work.weixin.qq.com/document/path/90664',
|
|
45
|
+
webhookPath: '/webhook/wecom',
|
|
46
|
+
fields: [
|
|
47
|
+
{ key: 'corpId', label: 'CorpID(企业ID)', required: true, env: 'WECOM_CORP_ID', hint: '管理后台 → 我的企业 → 企业ID' },
|
|
48
|
+
{ key: 'corpSecret', label: 'Secret(应用Secret)', required: true, secret: true, env: 'WECOM_CORP_SECRET', hint: '应用管理 → 自建应用 → Secret' },
|
|
49
|
+
{ key: 'agentId', label: 'AgentId', required: true, env: 'WECOM_AGENT_ID', hint: '同应用页 AgentId' },
|
|
50
|
+
{ key: 'token', label: 'Token', required: true, secret: true, env: 'WECOM_TOKEN', hint: '应用 → 接收消息 → API 接收 → Token' },
|
|
51
|
+
{ key: 'encodingAesKey', label: 'EncodingAESKey', required: true, secret: true, env: 'WECOM_AES_KEY', hint: '同页 EncodingAESKey(43 位)' },
|
|
52
|
+
],
|
|
53
|
+
steps: [
|
|
54
|
+
'扫码或打开企业微信管理后台,进入「应用管理 → 自建 → 创建应用」',
|
|
55
|
+
'复制企业ID、应用 Secret、AgentId',
|
|
56
|
+
'「接收消息」选 API 接收,设置 Token 与 EncodingAESKey',
|
|
57
|
+
'把下方回调 URL 填入「URL」,保存时企业微信会回调验证',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
qq: {
|
|
61
|
+
id: 'qq',
|
|
62
|
+
name: 'QQ 机器人',
|
|
63
|
+
consoleUrl: 'https://q.qq.com/#/app/bot',
|
|
64
|
+
docsUrl: 'https://bot.q.qq.com/wiki/',
|
|
65
|
+
webhookPath: '/webhook/qq',
|
|
66
|
+
fields: [
|
|
67
|
+
{ key: 'appId', label: 'AppID(机器人ID)', required: true, env: 'QQ_BOT_APPID', hint: 'QQ 开放平台 → 机器人 → 开发设置 → AppID' },
|
|
68
|
+
{ key: 'secret', label: 'AppSecret', required: true, secret: true, env: 'QQ_BOT_SECRET', hint: '同页 AppSecret' },
|
|
69
|
+
],
|
|
70
|
+
steps: [
|
|
71
|
+
'扫码或打开 https://q.qq.com 创建机器人,完成开发者认证',
|
|
72
|
+
'在「开发设置」复制 AppID 与 AppSecret',
|
|
73
|
+
'「回调配置」选择 Webhook,填入下方回调 URL',
|
|
74
|
+
'在沙箱里把机器人加为好友 / 拉进群进行测试',
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
exports.SETUP_CHANNEL_IDS = Object.keys(exports.CHANNEL_SETUP);
|
|
79
|
+
/** Build the full webhook callback URL for a channel from a public base. */
|
|
80
|
+
function callbackUrl(base, channelId) {
|
|
81
|
+
const spec = exports.CHANNEL_SETUP[channelId];
|
|
82
|
+
if (!spec)
|
|
83
|
+
return '';
|
|
84
|
+
const trimmed = base.replace(/\/+$/, '');
|
|
85
|
+
return `${trimmed}${spec.webhookPath}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Persist a channel's collected values into ~/.skyloom/config.yaml under
|
|
89
|
+
* channels.<id>, merging with any existing block. Secret-looking values are
|
|
90
|
+
* stored as-is (the file is chmod 0600); callers may instead keep secrets in
|
|
91
|
+
* env and store an { source: env, id } ref. Returns the config path written.
|
|
92
|
+
*/
|
|
93
|
+
function saveChannelConfig(channelId, values, opts) {
|
|
94
|
+
const path = require('path');
|
|
95
|
+
const fs = require('fs');
|
|
96
|
+
const yaml = require('yaml');
|
|
97
|
+
const cfgPath = opts?.configPath || path.join(require('os').homedir(), '.skyloom', 'config.yaml');
|
|
98
|
+
const dir = path.dirname(cfgPath);
|
|
99
|
+
if (!fs.existsSync(dir))
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
101
|
+
let cfg = {};
|
|
102
|
+
if (fs.existsSync(cfgPath)) {
|
|
103
|
+
try {
|
|
104
|
+
cfg = yaml.parse(fs.readFileSync(cfgPath, 'utf-8')) || {};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
cfg = {};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!cfg.channels)
|
|
111
|
+
cfg.channels = {};
|
|
112
|
+
cfg.channels[channelId] = { ...(cfg.channels[channelId] || {}), ...values, enabled: true };
|
|
113
|
+
fs.writeFileSync(cfgPath, yaml.stringify(cfg), { encoding: 'utf-8', mode: 0o600 });
|
|
114
|
+
try {
|
|
115
|
+
fs.chmodSync(cfgPath, 0o600);
|
|
116
|
+
}
|
|
117
|
+
catch { /* best-effort on Windows */ }
|
|
118
|
+
return cfgPath;
|
|
119
|
+
}
|
|
120
|
+
/** Which required fields are still missing from a values map. */
|
|
121
|
+
function missingRequired(channelId, values) {
|
|
122
|
+
const spec = exports.CHANNEL_SETUP[channelId];
|
|
123
|
+
if (!spec)
|
|
124
|
+
return [];
|
|
125
|
+
return spec.fields.filter((f) => f.required && !values[f.key]?.trim()).map((f) => f.key);
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/gateway/setup.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skyloom",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "天空织机 Skyloom — 6 weather-themed AI agents: Fog, Rain, Frost, Snow, Dew, Fair",
|
|
5
5
|
"preferGlobal": true,
|
|
6
6
|
"type": "commonjs",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"chalk": "^5.3.0",
|
|
47
47
|
"commander": "^12.1.0",
|
|
48
48
|
"glob": "^11.0.0",
|
|
49
|
+
"qrcode-terminal": "^0.12.0",
|
|
49
50
|
"sql.js": "^1.14.1",
|
|
50
51
|
"yaml": "^2.4.1"
|
|
51
52
|
},
|
package/src/cli/main.ts
CHANGED
|
@@ -38,6 +38,8 @@ program.command("mcp").action(() => { import("../core/mcp_server").then(m => m.s
|
|
|
38
38
|
program.command("gateway").description("Run the channel gateway (Feishu / WeCom / QQ)")
|
|
39
39
|
.option("-p,--port <p>", "port", "8848")
|
|
40
40
|
.action((o: { port?: string }) => { import("../gateway/gateway").then(m => m.startGateway({ port: parseInt(o.port || "8848") })); });
|
|
41
|
+
program.command("channels").description("Configure a chat channel (Feishu / WeCom / QQ) with QR shortcuts")
|
|
42
|
+
.action(async () => { await channelsWizard(); });
|
|
41
43
|
program.command("config").action(() => { const c = loadConfig(); process.stdout.write(chalk.cyan("\nConfig: ") + USER_CONFIG_DIR + "\n"); for (const [n, a] of Object.entries(c.agents || {})) process.stdout.write(` ${chalk.bold(n)}: ${(a as any).model || "default"}\n`); });
|
|
42
44
|
program.command("init").action(() => { if (!fs.existsSync(USER_CONFIG_DIR)) fs.mkdirSync(USER_CONFIG_DIR, { recursive: true }); process.stdout.write(chalk.green("✓ ") + USER_CONFIG_DIR + "\n"); });
|
|
43
45
|
program.command("apikey").description("Manage API keys (persisted to ~/.skyloom/config.yaml)")
|
|
@@ -275,6 +277,65 @@ async function setupWizard(): Promise<{ provider: string; key: string; model: st
|
|
|
275
277
|
return { provider: prov.id, key: key.trim(), model };
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
/* ═══════════════════════════════════════
|
|
281
|
+
Channel setup wizard (sky channels)
|
|
282
|
+
═══════════════════════════════════════ */
|
|
283
|
+
async function channelsWizard(): Promise<void> {
|
|
284
|
+
const { CHANNEL_SETUP, SETUP_CHANNEL_IDS, callbackUrl, saveChannelConfig, missingRequired } = require("../gateway/setup");
|
|
285
|
+
const { renderQR } = require("../gateway/qr");
|
|
286
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
287
|
+
const ask = (q: string): Promise<string> => new Promise(r => rl.question(q, r));
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
process.stdout.write("\n" + chalk.cyan(" ✦ 渠道接入向导 · sky channels ✦\n\n"));
|
|
291
|
+
process.stdout.write(chalk.dim(" 选择要配置的聊天软件:\n\n"));
|
|
292
|
+
SETUP_CHANNEL_IDS.forEach((id: string, i: number) => {
|
|
293
|
+
process.stdout.write(chalk.dim(` ${i + 1}. ${CHANNEL_SETUP[id].name}\n`));
|
|
294
|
+
});
|
|
295
|
+
const choice = await ask(chalk.cyan(`\n 编号 (1-${SETUP_CHANNEL_IDS.length}, q 退出): `));
|
|
296
|
+
if (choice.trim().toLowerCase() === "q") return;
|
|
297
|
+
const idx = parseInt(choice) - 1;
|
|
298
|
+
if (isNaN(idx) || idx < 0 || idx >= SETUP_CHANNEL_IDS.length) { process.stdout.write(chalk.dim(" 已取消\n")); return; }
|
|
299
|
+
const spec = CHANNEL_SETUP[SETUP_CHANNEL_IDS[idx]];
|
|
300
|
+
|
|
301
|
+
// Steps + QR to the platform console.
|
|
302
|
+
process.stdout.write("\n" + chalk.bold(` 配置 ${spec.name}\n\n`));
|
|
303
|
+
spec.steps.forEach((s: string, i: number) => process.stdout.write(chalk.dim(` ${i + 1}. ${s}\n`)));
|
|
304
|
+
process.stdout.write(chalk.dim(`\n 📱 扫码打开管理后台: `) + chalk.cyan(spec.consoleUrl) + "\n");
|
|
305
|
+
const consoleQR = renderQR(spec.consoleUrl);
|
|
306
|
+
if (consoleQR) process.stdout.write("\n" + consoleQR.split("\n").map((l: string) => " " + l).join("\n") + "\n");
|
|
307
|
+
if (spec.docsUrl) process.stdout.write(chalk.dim(` 📖 文档: ${spec.docsUrl}\n`));
|
|
308
|
+
|
|
309
|
+
// Collect credential fields.
|
|
310
|
+
process.stdout.write("\n" + chalk.dim(" 逐项填入凭据(回车跳过可选项):\n\n"));
|
|
311
|
+
const values: Record<string, string> = {};
|
|
312
|
+
for (const f of spec.fields) {
|
|
313
|
+
const req = f.required ? chalk.red("*") : chalk.dim("(可选)");
|
|
314
|
+
if (f.hint) process.stdout.write(chalk.dim(` ↳ ${f.hint}\n`));
|
|
315
|
+
const v = await ask(chalk.cyan(` ${f.label} ${req}: `));
|
|
316
|
+
if (v.trim()) values[f.key] = v.trim();
|
|
317
|
+
}
|
|
318
|
+
const missing = missingRequired(spec.id, values);
|
|
319
|
+
if (missing.length) {
|
|
320
|
+
process.stdout.write(chalk.yellow(`\n ⚠ 缺少必填项: ${missing.join(", ")} — 已保存现有项,可再次运行补全。\n`));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const cfgPath = saveChannelConfig(spec.id, values);
|
|
324
|
+
process.stdout.write(chalk.green(`\n ✓ 已保存到 ${cfgPath} 的 channels.${spec.id}\n`));
|
|
325
|
+
|
|
326
|
+
// Callback URL + QR to paste into the platform console.
|
|
327
|
+
const base = (await ask(chalk.cyan("\n 你的网关公网地址(如 https://bot.example.com,回车用 http://localhost:8848): "))).trim()
|
|
328
|
+
|| "http://localhost:8848";
|
|
329
|
+
const cb = callbackUrl(base, spec.id);
|
|
330
|
+
process.stdout.write(chalk.dim("\n 把下面的回调 URL 填入平台后台的事件/接收配置:\n ") + chalk.cyan(cb) + "\n");
|
|
331
|
+
const cbQR = renderQR(cb);
|
|
332
|
+
if (cbQR) process.stdout.write("\n" + cbQR.split("\n").map((l: string) => " " + l).join("\n") + "\n");
|
|
333
|
+
process.stdout.write(chalk.dim(`\n 完成后运行 sky gateway 启动网关。\n\n`));
|
|
334
|
+
} finally {
|
|
335
|
+
rl.close();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
278
339
|
async function chat(agentName: string, modelOverride?: string, classic?: boolean): Promise<void> {
|
|
279
340
|
const haveKey = checkApiKeys();
|
|
280
341
|
if (!haveKey) {
|
|
@@ -508,6 +569,7 @@ async function chat(agentName: string, modelOverride?: string, classic?: boolean
|
|
|
508
569
|
}
|
|
509
570
|
if (cmdL.startsWith("/task ")) { const g = inp.slice(6); process.stdout.write(chalk.cyan("\n ✦ " + g + "\n\n")); await runTask(g); continue; }
|
|
510
571
|
if (cmdL === "/setup") { const r = await setupWizard(); if (r) process.stdout.write(chalk.green(` ${r.provider} · ${r.model} — Ready!\n`)); continue; }
|
|
572
|
+
if (cmdL === "/channels") { await channelsWizard(); continue; }
|
|
511
573
|
if (cmdL === "/model" || cmdL.startsWith("/model ")) {
|
|
512
574
|
const { setAgentModel, setUnifiedModel, clearAgentModel, setAgentApiKey, describeAgentLLM } = require("../core/model_config");
|
|
513
575
|
const cfg = (ctx as any).config;
|
package/src/core/commands.ts
CHANGED
|
@@ -482,6 +482,16 @@ export const BUILTIN_COMMANDS: CommandInfo[] = [
|
|
|
482
482
|
takesArgs: false,
|
|
483
483
|
source: 'builtin',
|
|
484
484
|
},
|
|
485
|
+
{
|
|
486
|
+
name: 'channels',
|
|
487
|
+
aliases: [],
|
|
488
|
+
description: 'Configure a chat channel (Feishu / WeCom / QQ) with QR shortcuts',
|
|
489
|
+
label: '渠道接入向导(飞书/企业微信/QQ · 含二维码)',
|
|
490
|
+
category: 'config',
|
|
491
|
+
hints: [],
|
|
492
|
+
takesArgs: false,
|
|
493
|
+
source: 'builtin',
|
|
494
|
+
},
|
|
485
495
|
{
|
|
486
496
|
name: 'apikey',
|
|
487
497
|
aliases: [],
|
|
@@ -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
|
}
|