@voko/lite 0.3.1
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/package.json +32 -0
- package/scripts/build-native.js +72 -0
- package/src/bankHeadOffices.js +20543 -0
- package/src/channels/email.js +35 -0
- package/src/channels/feishu.js +31 -0
- package/src/channels/qq-email.js +30 -0
- package/src/channels/registry.js +279 -0
- package/src/channels/telegram.js +28 -0
- package/src/channels/voko-email.js +7 -0
- package/src/channels/wechat.js +35 -0
- package/src/cli.js +120 -0
- package/src/context.js +164 -0
- package/src/core/access-control-api.js +150 -0
- package/src/core/access-control.js +56 -0
- package/src/core/agent-registration.js +319 -0
- package/src/core/api-signature.js +33 -0
- package/src/core/audit.js +133 -0
- package/src/core/database.js +1409 -0
- package/src/core/did-auth.js +54 -0
- package/src/core/hermes-paths.js +57 -0
- package/src/core/invitation.js +49 -0
- package/src/core/lite-bus.js +16 -0
- package/src/core/llm-client.js +1032 -0
- package/src/core/messenger.js +456 -0
- package/src/core/notifier.js +99 -0
- package/src/core/offline-sync.js +150 -0
- package/src/core/payment.js +285 -0
- package/src/core/publish-agent.js +166 -0
- package/src/core/register-capabilities.js +119 -0
- package/src/core/search-capabilities.js +136 -0
- package/src/core/send-message.js +85 -0
- package/src/core/set-agent-status.js +65 -0
- package/src/core/update-agent-profile.js +102 -0
- package/src/core/worker-manager.js +332 -0
- package/src/endpoints.json +21 -0
- package/src/index.js +712 -0
- package/src/mcp/CLAUDE_TEST.md +82 -0
- package/src/mcp/FULL_TEST.md +139 -0
- package/src/mcp/TEST.md +124 -0
- package/src/mcp/TEST_STEPS.md +75 -0
- package/src/mcp/server.js +612 -0
- package/src/mcp/tools.js +1367 -0
- package/src/mcp/transport/http.js +95 -0
- package/src/mcp/transport/stdio.js +20 -0
- package/src/preload.js +27 -0
- package/src/server/agent-email-api.js +120 -0
- package/src/server/agent-manager.js +580 -0
- package/src/server/email-handler.js +329 -0
- package/src/server/feishu-handler.js +249 -0
- package/src/server/hermes-api-client.js +166 -0
- package/src/server/hermes-discovery.js +80 -0
- package/src/server/hermes-handler.js +287 -0
- package/src/server/openclaw-handler-cli.js +131 -0
- package/src/server/openclaw-websocket-handler.js +1290 -0
- package/src/server/oss.js +186 -0
- package/src/server/owner-intervention-notifier.js +320 -0
- package/src/server/release-page.html +204 -0
- package/src/server/telegram-handler.js +208 -0
- package/src/server/voko-email-handler.js +68 -0
- package/src/server/wechat-handler.js +439 -0
- package/src/workers/agent-worker.js +378 -0
- package/src/workers/message-content.js +51 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>版本发布</title>
|
|
6
|
+
<style>
|
|
7
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
8
|
+
body{font-family:-apple-system,'Segoe UI',sans-serif;background:#f5f5f5;color:#333;padding:40px}
|
|
9
|
+
.container{max-width:720px;margin:0 auto}
|
|
10
|
+
h1{font-size:24px;color:#FF6A00;margin-bottom:24px}
|
|
11
|
+
.card{background:#fff;border-radius:12px;padding:20px;margin-bottom:20px;box-shadow:0 2px 8px rgba(0,0,0,0.06)}
|
|
12
|
+
.card h2{font-size:15px;color:#555;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid #eee}
|
|
13
|
+
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
14
|
+
.grid2 label{font-size:12px;color:#999;margin-bottom:2px}
|
|
15
|
+
.grid2 span{font-size:14px;font-weight:500}
|
|
16
|
+
.form-group{margin-bottom:16px}
|
|
17
|
+
.form-group label{display:block;font-size:13px;color:#555;font-weight:500;margin-bottom:6px}
|
|
18
|
+
.form-group input[type="text"],.form-group textarea{width:100%;padding:10px 12px;border:1px solid #ddd;border-radius:6px;font-size:14px;outline:none;font-family:inherit}
|
|
19
|
+
.form-group textarea{min-height:120px;resize:vertical;line-height:1.6}
|
|
20
|
+
.form-group .help{font-size:12px;color:#999;margin-top:4px}
|
|
21
|
+
.btn{padding:12px 32px;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;user-select:none}
|
|
22
|
+
.btn-primary{background:#FF6A00;color:#fff}
|
|
23
|
+
.btn-primary:hover{background:#e55e00}
|
|
24
|
+
.btn-primary:disabled{background:#ccc;cursor:not-allowed}
|
|
25
|
+
.btn-secondary{background:#f0f0f0;color:#666}
|
|
26
|
+
.log-area{background:#1a1a2e;color:#0f0;padding:16px;border-radius:8px;font-size:13px;font-family:monospace;max-height:400px;overflow-y:auto;margin-top:16px;min-height:60px;white-space:pre-wrap;line-height:1.6;display:none}
|
|
27
|
+
.log-area.has-content{display:block}
|
|
28
|
+
.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:none;align-items:center;justify-content:center;z-index:999}
|
|
29
|
+
.modal-overlay.show{display:flex}
|
|
30
|
+
.modal-box{background:#fff;border-radius:16px;padding:32px;max-width:480px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.2)}
|
|
31
|
+
.modal-box h2{font-size:18px;margin-bottom:12px}
|
|
32
|
+
.modal-box pre{background:#f5f5f5;padding:12px;border-radius:8px;font-size:13px;line-height:1.8;white-space:pre-wrap}
|
|
33
|
+
.loading{display:inline-block;width:16px;height:16px;border:2px solid #FF6A00;border-top-color:transparent;border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle;margin-right:8px}
|
|
34
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="container">
|
|
39
|
+
<h1>版本发布</h1>
|
|
40
|
+
<div class="card">
|
|
41
|
+
<h2>OSS 当前状态</h2>
|
|
42
|
+
<div class="grid2">
|
|
43
|
+
<div><label>最新版本</label><span id="cur-version">加载中...</span></div>
|
|
44
|
+
<div><label>发布时间</label><span id="cur-date">加载中...</span></div>
|
|
45
|
+
<div><label>最低强制版本</label><span id="cur-min-version">加载中...</span></div>
|
|
46
|
+
<div><label>已发布版本数</label><span id="cur-release-count">加载中...</span></div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="card">
|
|
50
|
+
<h2>发布新版本</h2>
|
|
51
|
+
<div class="form-group">
|
|
52
|
+
<label>版本号</label>
|
|
53
|
+
<input type="text" id="input-version">
|
|
54
|
+
</div>
|
|
55
|
+
<div class="form-group">
|
|
56
|
+
<label>更新内容
|
|
57
|
+
<button class="btn btn-secondary" onclick="loadGitLog()" style="float:right;font-size:12px;padding:4px 10px;">获取最近提交</button>
|
|
58
|
+
</label>
|
|
59
|
+
<textarea id="input-notes" placeholder="更新内容">更新内容</textarea>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="form-group">
|
|
62
|
+
<label>最低强制版本</label>
|
|
63
|
+
<input type="text" id="input-min-version" placeholder="留空=不强制">
|
|
64
|
+
<div class="help">低于此版本必须升级</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-group">
|
|
67
|
+
<label><input type="checkbox" id="input-mandatory"> 强制升级</label>
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<button class="btn btn-primary" id="btn-publish" onclick="doPublish()">发布版本</button>
|
|
71
|
+
<span class="loading" id="spin" style="display:none;"></span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="log-area" id="log-area"></div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="modal-overlay" id="result-modal">
|
|
77
|
+
<div class="modal-box">
|
|
78
|
+
<h2 id="result-title">完成</h2>
|
|
79
|
+
<pre id="result-body"></pre>
|
|
80
|
+
<div style="margin-top:16px;text-align:right;"><button class="btn btn-primary" onclick="closeModal()">确定</button></div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<script>
|
|
85
|
+
var API = window.location.origin;
|
|
86
|
+
var DIST_DIR = '';
|
|
87
|
+
var UPDATE_BASE_URL = '';
|
|
88
|
+
var V = '';
|
|
89
|
+
|
|
90
|
+
function id(s) { return document.getElementById(s); }
|
|
91
|
+
|
|
92
|
+
async function loadStatus() {
|
|
93
|
+
try {
|
|
94
|
+
var pr = await fetch(API + '/api/release/project-info');
|
|
95
|
+
var pd = await pr.json();
|
|
96
|
+
if (pd.success) {
|
|
97
|
+
DIST_DIR = pd.distDir;
|
|
98
|
+
if (pd.endpoints && pd.endpoints.update) UPDATE_BASE_URL = pd.endpoints.update.baseUrl;
|
|
99
|
+
}
|
|
100
|
+
var r = await fetch(API + '/api/release/status');
|
|
101
|
+
var d = await r.json();
|
|
102
|
+
if (!d.success) return;
|
|
103
|
+
V = d.version || '';
|
|
104
|
+
id('cur-version').textContent = d.version || '-';
|
|
105
|
+
id('cur-date').textContent = d.date ? new Date(d.date).toLocaleString() : '-';
|
|
106
|
+
id('cur-min-version').textContent = d.minVersion || '-';
|
|
107
|
+
id('cur-release-count').textContent = (d.releaseCount || 0) + ' 个';
|
|
108
|
+
var p = V.split('.').map(Number);
|
|
109
|
+
if (p.length === 3) id('input-version').value = [p[0], p[1], p[2]+1].join('.');
|
|
110
|
+
} catch(e) { id('cur-version').textContent = '加载失败'; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function loadGitLog() {
|
|
114
|
+
id('input-notes').value = '正在获取提交记录...';
|
|
115
|
+
try {
|
|
116
|
+
var r = await fetch(API + '/api/release/git-log');
|
|
117
|
+
var d = await r.json();
|
|
118
|
+
if (d.success) {
|
|
119
|
+
id('input-notes').value = '更新内容\n' + d.log;
|
|
120
|
+
} else {
|
|
121
|
+
id('input-notes').value = '获取失败: ' + (d.error || '未知错误');
|
|
122
|
+
alert('获取提交记录失败: ' + (d.error || '未知错误'));
|
|
123
|
+
}
|
|
124
|
+
} catch(e) {
|
|
125
|
+
id('input-notes').value = '请求异常: ' + e.message;
|
|
126
|
+
alert('请求失败: ' + e.message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
id('input-mandatory').addEventListener('change', function() {
|
|
131
|
+
if (this.checked) {
|
|
132
|
+
id('input-min-version').value = id('input-version').value;
|
|
133
|
+
id('input-min-version').readOnly = true;
|
|
134
|
+
} else {
|
|
135
|
+
id('input-min-version').readOnly = false;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
async function doPublish() {
|
|
140
|
+
var ver = id('input-version').value.trim();
|
|
141
|
+
var notes = id('input-notes').value.trim();
|
|
142
|
+
var minVer = id('input-min-version').value.trim() || ver;
|
|
143
|
+
if (!ver || !notes) { alert('请填写完整'); return; }
|
|
144
|
+
if (!confirm('确认发布 ' + ver + ' ?')) return;
|
|
145
|
+
|
|
146
|
+
var btn = id('btn-publish');
|
|
147
|
+
var spin = id('spin');
|
|
148
|
+
var log = id('log-area');
|
|
149
|
+
btn.disabled = true; spin.style.display = 'inline-block';
|
|
150
|
+
log.textContent = '';
|
|
151
|
+
|
|
152
|
+
function L(m) { log.textContent += m + '\n'; log.classList.add('has-content'); log.scrollTop = log.scrollHeight; }
|
|
153
|
+
function sleep(t) { return new Promise(function(r) { setTimeout(r, t); }); }
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
L('开始发布 ' + ver);
|
|
157
|
+
L('触发构建 ' + ver + ' ...');
|
|
158
|
+
var r = await fetch(API + '/api/release/build', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({version:ver}) });
|
|
159
|
+
var d = await r.json();
|
|
160
|
+
if (!d.success) throw new Error(d.error);
|
|
161
|
+
L('构建中 (约2-5分钟)...');
|
|
162
|
+
while (true) {
|
|
163
|
+
await sleep(2000);
|
|
164
|
+
var lr = await fetch(API + '/api/release/build-log');
|
|
165
|
+
var ld = await lr.json();
|
|
166
|
+
log.textContent = ld.log;
|
|
167
|
+
log.scrollTop = log.scrollHeight;
|
|
168
|
+
var sr = await fetch(API + '/api/release/build-status');
|
|
169
|
+
var sd = await sr.json();
|
|
170
|
+
if (sd.done) { if (!sd.ok) throw new Error('构建失败'); break; }
|
|
171
|
+
}
|
|
172
|
+
if (!DIST_DIR) { alert('项目信息未加载,请刷新页面'); throw new Error('项目信息未加载'); }
|
|
173
|
+
L('上传安装包...');
|
|
174
|
+
r = await fetch(API + '/api/release/upload-file', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filePath: DIST_DIR + '/VOKO-Desktop-Setup-' + ver + '.exe', ossPath: 'updates/VOKO-Desktop-Setup-' + ver + '.exe', contentType: 'application/octet-stream'}) });
|
|
175
|
+
d = await r.json();
|
|
176
|
+
if (!d.success) throw new Error(d.error);
|
|
177
|
+
L('安装包上传完成');
|
|
178
|
+
L('上传 blockmap(差量更新)...');
|
|
179
|
+
try {
|
|
180
|
+
r = await fetch(API + '/api/release/upload-file', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filePath: DIST_DIR + '/VOKO-Desktop-Setup-' + ver + '.exe.blockmap', ossPath: 'updates/VOKO-Desktop-Setup-' + ver + '.exe.blockmap', contentType: 'application/octet-stream'}) });
|
|
181
|
+
d = await r.json();
|
|
182
|
+
if (d.success) L('blockmap 上传成功(支持差量更新)');
|
|
183
|
+
} catch(e) { L('blockmap 未找到,将使用完整下载'); }
|
|
184
|
+
L('更新元数据...');
|
|
185
|
+
r = await fetch(API + '/api/release/update-changelog', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({version:ver, notes:notes}) });
|
|
186
|
+
d = await r.json(); if (!d.success) throw new Error(d.error);
|
|
187
|
+
r = await fetch(API + '/api/release/update-latest', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({version:ver}) });
|
|
188
|
+
d = await r.json(); if (!d.success) throw new Error('latest.yml: ' + d.error);
|
|
189
|
+
r = await fetch(API + '/api/release/update-min-version', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({minVersion:minVer}) });
|
|
190
|
+
d = await r.json(); if (!d.success) throw new Error(d.error);
|
|
191
|
+
var dlUrl = (UPDATE_BASE_URL || 'http://files.vokovoko.com/updates') + '/VOKO-Desktop-Setup-' + ver + '.exe';
|
|
192
|
+
L('✅ ' + ver + ' 发布完成');
|
|
193
|
+
log.innerHTML += '<div style="display:flex;align-items:center;gap:10px;margin-top:4px;"><span style="color:#0f0;">' + dlUrl + '</span><button onclick="copyLink()" style="padding:4px 12px;border:none;border-radius:4px;background:#FF6A00;color:#fff;cursor:pointer;font-size:12px;">复制链接</button></div>';
|
|
194
|
+
window.copyLink = function() { navigator.clipboard.writeText(dlUrl).then(function(){ L('链接已复制!'); }); };
|
|
195
|
+
} catch(e) {
|
|
196
|
+
L('错误: ' + e.message);
|
|
197
|
+
}
|
|
198
|
+
btn.disabled = false; spin.style.display = 'none';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
loadStatus();
|
|
202
|
+
</script>
|
|
203
|
+
</body>
|
|
204
|
+
</html>
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VOKO Desktop - Telegram 消息处理器
|
|
3
|
+
* 通过 Polling 接收 Telegram 消息事件
|
|
4
|
+
*
|
|
5
|
+
* 用途:
|
|
6
|
+
* - 当 agent 需要主人介入时,通过 Telegram 通知主人
|
|
7
|
+
* - 主人回复后,将消息转发给对应 agent 的 session
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const https = require('https');
|
|
11
|
+
|
|
12
|
+
class TelegramHandler {
|
|
13
|
+
constructor(config, options = {}) {
|
|
14
|
+
// Telegram Bot 配置
|
|
15
|
+
this.botToken = config.botToken;
|
|
16
|
+
this.ownerChatId = config.ownerChatId; // 主人 Telegram chat_id
|
|
17
|
+
this.pollInterval = config.pollInterval || 10000; // 默认 10 秒
|
|
18
|
+
|
|
19
|
+
// 回调函数
|
|
20
|
+
this.onOwnerReply = options.onOwnerReply || null;
|
|
21
|
+
this.getInterventionByParentMsgId = options.getInterventionByParentMsgId || null;
|
|
22
|
+
this.getPendingTelegramCount = options.getPendingTelegramCount || null;
|
|
23
|
+
this.isEnabled = options.isEnabled || (() => true);
|
|
24
|
+
|
|
25
|
+
// 轮询状态
|
|
26
|
+
this.offset = 0;
|
|
27
|
+
this.pollTimer = null;
|
|
28
|
+
this.isPolling = false;
|
|
29
|
+
this.enabled = false;
|
|
30
|
+
this._currentReq = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async start() {
|
|
34
|
+
if (this.enabled) {
|
|
35
|
+
console.log('[Telegram] 已经启动');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.enabled = true;
|
|
40
|
+
console.log('[Telegram] 启动轮询,间隔', this.pollInterval, 'ms');
|
|
41
|
+
this.scheduleNextPoll();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
scheduleNextPoll() {
|
|
45
|
+
if (!this.enabled) return;
|
|
46
|
+
|
|
47
|
+
this.pollTimer = setTimeout(async () => {
|
|
48
|
+
await this.pollOnce();
|
|
49
|
+
this.scheduleNextPoll();
|
|
50
|
+
}, this.pollInterval);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async pollOnce() {
|
|
54
|
+
if (!this.isEnabled()) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
this.isPolling = true;
|
|
60
|
+
await this.fetchUpdates();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('[Telegram] 轮询错误:', err.message);
|
|
63
|
+
} finally {
|
|
64
|
+
this.isPolling = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async fetchUpdates() {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const options = {
|
|
71
|
+
hostname: 'api.telegram.org',
|
|
72
|
+
port: 443,
|
|
73
|
+
path: `/bot${this.botToken}/getUpdates?offset=${this.offset}&timeout=5`,
|
|
74
|
+
method: 'GET'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const req = https.request(options, (res) => {
|
|
78
|
+
let data = '';
|
|
79
|
+
res.on('data', chunk => data += chunk);
|
|
80
|
+
res.on('end', () => {
|
|
81
|
+
try {
|
|
82
|
+
const json = JSON.parse(data);
|
|
83
|
+
if (json.ok) {
|
|
84
|
+
this.processUpdates(json.result);
|
|
85
|
+
if (json.result.length > 0) {
|
|
86
|
+
this.offset = json.result[json.result.length - 1].update_id + 1;
|
|
87
|
+
}
|
|
88
|
+
resolve();
|
|
89
|
+
} else {
|
|
90
|
+
reject(new Error(json.description || 'API error'));
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
reject(e);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
req.on('error', (err) => {
|
|
99
|
+
if (err.code === 'ECONNRESET' || err.message === 'aborted') return; // 主动中断,忽略
|
|
100
|
+
reject(err);
|
|
101
|
+
});
|
|
102
|
+
this._currentReq = req;
|
|
103
|
+
req.end();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async processUpdates(updates) {
|
|
108
|
+
for (const update of updates) {
|
|
109
|
+
if (update.edited_message) {
|
|
110
|
+
await this.handleMessage(update.edited_message, true);
|
|
111
|
+
}
|
|
112
|
+
if (update.message) {
|
|
113
|
+
await this.handleMessage(update.message, false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async handleMessage(message, isEdited) {
|
|
119
|
+
const chatId = message.chat?.id?.toString();
|
|
120
|
+
const messageId = message.message_id?.toString();
|
|
121
|
+
const text = message.text || '';
|
|
122
|
+
const replyToMsgId = message.reply_to_message?.message_id?.toString();
|
|
123
|
+
|
|
124
|
+
if (chatId !== this.ownerChatId) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (replyToMsgId) {
|
|
129
|
+
await this.handleReply(replyToMsgId, text, messageId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async handleReply(parentMessageId, content, replyMessageId) {
|
|
134
|
+
if (this.getInterventionByParentMsgId) {
|
|
135
|
+
const intervention = this.getInterventionByParentMsgId(parentMessageId);
|
|
136
|
+
if (intervention) {
|
|
137
|
+
console.log('[Telegram] 匹配到干预记录:', intervention.id);
|
|
138
|
+
if (this.onOwnerReply) {
|
|
139
|
+
this.onOwnerReply(intervention, content, replyMessageId);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
stop() {
|
|
146
|
+
this.enabled = false;
|
|
147
|
+
if (this.pollTimer) {
|
|
148
|
+
clearTimeout(this.pollTimer);
|
|
149
|
+
this.pollTimer = null;
|
|
150
|
+
}
|
|
151
|
+
console.log('[Telegram] 轮询已停止');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updateConfig(config) {
|
|
155
|
+
if (config.botToken) this.botToken = config.botToken;
|
|
156
|
+
if (config.ownerChatId) this.ownerChatId = config.ownerChatId;
|
|
157
|
+
if (config.pollInterval) this.pollInterval = config.pollInterval;
|
|
158
|
+
console.log('[Telegram] 配置已更新');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 发送消息给 Telegram 用户(带记录,用于后续匹配)
|
|
163
|
+
*/
|
|
164
|
+
async sendMessageToOwnerWithTracking(content, visitorId, sessionKey) {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const postData = JSON.stringify({
|
|
167
|
+
chat_id: this.ownerChatId,
|
|
168
|
+
text: content
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const options = {
|
|
172
|
+
hostname: 'api.telegram.org',
|
|
173
|
+
port: 443,
|
|
174
|
+
path: `/bot${this.botToken}/sendMessage`,
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: {
|
|
177
|
+
'Content-Type': 'application/json',
|
|
178
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const req = https.request(options, (res) => {
|
|
183
|
+
let data = '';
|
|
184
|
+
res.on('data', chunk => data += chunk);
|
|
185
|
+
res.on('end', () => {
|
|
186
|
+
try {
|
|
187
|
+
const json = JSON.parse(data);
|
|
188
|
+
if (json.ok) {
|
|
189
|
+
const sentMessageId = json.result?.message_id?.toString() || `sent-${Date.now()}`;
|
|
190
|
+
console.log('[Telegram] 发送消息给主人成功:', { sentMessageId, visitorId, sessionKey });
|
|
191
|
+
resolve({ messageId: sentMessageId, sentMessageId });
|
|
192
|
+
} else {
|
|
193
|
+
reject(new Error(json.description || 'Telegram API error'));
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
reject(e);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
req.on('error', reject);
|
|
202
|
+
req.write(postData);
|
|
203
|
+
req.end();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = TelegramHandler;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* voko-email-handler.js — VOKO 内置邮件渠道处理器
|
|
3
|
+
*
|
|
4
|
+
* 通过服务端 API 发送通知邮件,不走 SMTP。
|
|
5
|
+
* 在 registry 中注册为标准渠道,走统一的 sendMessageToOwnerWithTracking 接口。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class VokoEmailHandler {
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} config - 空对象(voko-email 无配置项)
|
|
11
|
+
* @param {object} callbacks
|
|
12
|
+
* @param {object} callbacks.agentEmailApi - AgentEmailApi 实例
|
|
13
|
+
* @param {object} callbacks.db - better-sqlite3 实例
|
|
14
|
+
* @param {Function} callbacks.isEnabled - () => boolean
|
|
15
|
+
*/
|
|
16
|
+
constructor(config, callbacks) {
|
|
17
|
+
this.agentEmailApi = callbacks.agentEmailApi;
|
|
18
|
+
this.db = callbacks.db;
|
|
19
|
+
this._isEnabled = callbacks.isEnabled || (() => true);
|
|
20
|
+
this.enabled = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async start() {
|
|
24
|
+
// voko-email 无需持久连接,无操作
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async stop() {
|
|
28
|
+
this.enabled = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 发送通知邮件给主人
|
|
33
|
+
* @param {string} content - 消息内容
|
|
34
|
+
* @param {string} visitorId - 访客 ID
|
|
35
|
+
* @param {string} sessionKey - 会话 key
|
|
36
|
+
* @param {string} agentId - Agent ID
|
|
37
|
+
* @returns {Promise<{ messageId: string, sentMessageId: string }>}
|
|
38
|
+
*/
|
|
39
|
+
async sendMessageToOwnerWithTracking(content, visitorId, sessionKey, agentId) {
|
|
40
|
+
if (!this.agentEmailApi || !agentId) {
|
|
41
|
+
throw new Error('AgentEmailApi 未配置或 agentId 为空');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const agentRow = this.db.prepare('SELECT did, agent_name FROM agents WHERE agent_id = ?').get(agentId);
|
|
45
|
+
if (!agentRow?.did) {
|
|
46
|
+
throw new Error(`Agent ${agentId} 未配置 DID`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const name = agentRow.agent_name || agentId;
|
|
50
|
+
const subject = `[VOKO]Agent${name}关于${visitorId}的${sessionKey ? '会话通知' : '人工介入请求'}`;
|
|
51
|
+
const emailBody = content;
|
|
52
|
+
|
|
53
|
+
const result = await this.agentEmailApi.send(agentRow.did, emailBody, {
|
|
54
|
+
subject,
|
|
55
|
+
external_id: `${Date.now()}`,
|
|
56
|
+
context: { visitor_id: visitorId, snippet: (content || '').substring(0, 100) },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result?.message_id) {
|
|
60
|
+
console.log(`[VokoEmailHandler] 服务端邮件已发送, agent=${agentId}, visitor=${visitorId}, message_id=${result.message_id}`);
|
|
61
|
+
return { messageId: result.message_id, sentMessageId: result.message_id };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error('邮件发送失败,未返回 message_id');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = VokoEmailHandler;
|