@bastdewfn/cc-remote 1.0.9
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/1.jpg +0 -0
- package/2.jpg +0 -0
- package/README.md +150 -0
- package/bin/cc-remote.js +183 -0
- package/commands/cc-remote-close.md +5 -0
- package/commands/cc-remote-doctor.md +20 -0
- package/commands/cc-remote-open.md +5 -0
- package/config.example.json +10 -0
- package/dist/cli.js +313 -0
- package/dist/commands.js +122 -0
- package/dist/config-setup.js +366 -0
- package/dist/core.js +453 -0
- package/dist/engine/events.js +78 -0
- package/dist/feishu/cards/base.js +114 -0
- package/dist/feishu/cards/help.js +33 -0
- package/dist/feishu/cards/live.js +65 -0
- package/dist/feishu/cards/session.js +59 -0
- package/dist/feishu/cards/status.js +60 -0
- package/dist/feishu/client.js +174 -0
- package/dist/feishu/replier.js +143 -0
- package/dist/feishu/router.js +61 -0
- package/dist/feishu-bot.js +139 -0
- package/dist/feishu-mode.js +62 -0
- package/dist/index.js +62 -0
- package/dist/main.js +397 -0
- package/dist/port.js +79 -0
- package/dist/preload.js +41 -0
- package/dist/pty.js +23 -0
- package/dist/relay.js +851 -0
- package/dist/renderer.js +318 -0
- package/dist/weixin/api.js +328 -0
- package/dist/weixin/client.js +136 -0
- package/dist/weixin/replier.js +73 -0
- package/dist/weixin/types.js +10 -0
- package/index.html +32 -0
- package/package.json +85 -0
- package/scripts/patch-7za.js +94 -0
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
if (typeof Terminal === 'undefined') {
|
|
3
|
+
document.body.innerHTML = '<pre style="color:red;padding:20px;">Error: xterm.js did not load. Check script paths in index.html.</pre>';
|
|
4
|
+
throw new Error('Terminal is not defined');
|
|
5
|
+
}
|
|
6
|
+
const term = new Terminal({
|
|
7
|
+
cursorBlink: true,
|
|
8
|
+
cursorStyle: 'underline',
|
|
9
|
+
fontSize: 14,
|
|
10
|
+
fontFamily: '"Cascadia Code", "Fira Code", Consolas, monospace',
|
|
11
|
+
theme: {
|
|
12
|
+
background: '#1e1e1e',
|
|
13
|
+
foreground: '#d4d4d4',
|
|
14
|
+
cursor: '#ffffff',
|
|
15
|
+
},
|
|
16
|
+
allowProposedApi: true,
|
|
17
|
+
});
|
|
18
|
+
const FitAddonClass = FitAddon.FitAddon || FitAddon;
|
|
19
|
+
const fitAddon = new FitAddonClass();
|
|
20
|
+
term.loadAddon(fitAddon);
|
|
21
|
+
// Clipboard addon: auto copy-on-select, Ctrl+C/V
|
|
22
|
+
const ClipboardAddonModule = window.ClipboardAddon;
|
|
23
|
+
if (ClipboardAddonModule) {
|
|
24
|
+
const ClipboardAddonClass = ClipboardAddonModule.ClipboardAddon || ClipboardAddonModule;
|
|
25
|
+
const clipboardAddon = new ClipboardAddonClass();
|
|
26
|
+
term.loadAddon(clipboardAddon);
|
|
27
|
+
}
|
|
28
|
+
const el = document.getElementById('terminal');
|
|
29
|
+
if (el) {
|
|
30
|
+
term.open(el);
|
|
31
|
+
fitAddon.fit();
|
|
32
|
+
term.writeln('Claude Code Terminal\r\n');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
document.body.innerHTML = '<pre style="color:red;padding:20px;">Error: #terminal element not found.</pre>';
|
|
36
|
+
throw new Error('terminal element missing');
|
|
37
|
+
}
|
|
38
|
+
if (typeof window.electronAPI === 'undefined') {
|
|
39
|
+
term.writeln('[Error: electronAPI not available - preload may have failed]\r\n');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Intercept all F-keys in capture phase, before xterm.js sees them
|
|
43
|
+
document.addEventListener('keydown', (e) => {
|
|
44
|
+
// Shift+Enter → newline (xterm.js doesn't pass this to onData)
|
|
45
|
+
if (e.key === 'Enter' && e.shiftKey) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
window.electronAPI.sendInput('\n');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Shift+Tab → unindent (xterm.js doesn't pass this to onData)
|
|
52
|
+
if (e.key === 'Tab' && e.shiftKey) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
window.electronAPI.sendInput('\x1b[Z');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Esc → send immediately without waiting for escape sequence timeout
|
|
59
|
+
if (e.key === 'Escape') {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
window.electronAPI.sendInput('\x1b');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (e.key.startsWith('F') && e.key.length <= 4) {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
e.stopPropagation();
|
|
68
|
+
if (e.key === 'F1') {
|
|
69
|
+
dumpTerminalBuffer();
|
|
70
|
+
}
|
|
71
|
+
else if (e.key === 'F3') {
|
|
72
|
+
const bar = document.getElementById('search-bar');
|
|
73
|
+
if (bar) {
|
|
74
|
+
bar.classList.add('visible');
|
|
75
|
+
bar.value = '';
|
|
76
|
+
bar.focus();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (e.key === 'F11') {
|
|
80
|
+
window.electronAPI.toggleFullscreen();
|
|
81
|
+
}
|
|
82
|
+
else if (e.key === 'F12') {
|
|
83
|
+
window.electronAPI.openDevTools();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, true);
|
|
87
|
+
term.onData((data) => {
|
|
88
|
+
window.electronAPI.sendInput(data);
|
|
89
|
+
});
|
|
90
|
+
window.electronAPI.onData((data) => {
|
|
91
|
+
term.write(data);
|
|
92
|
+
});
|
|
93
|
+
term.onResize(({ cols, rows }) => {
|
|
94
|
+
window.electronAPI.resize(cols, rows);
|
|
95
|
+
});
|
|
96
|
+
window.electronAPI.onExit((code) => {
|
|
97
|
+
term.write(`\r\n\n[Claude exited with code ${code}]\r\n`);
|
|
98
|
+
});
|
|
99
|
+
window.electronAPI.onStatus((text) => {
|
|
100
|
+
console.log(text);
|
|
101
|
+
});
|
|
102
|
+
// Search addon
|
|
103
|
+
const SearchAddonModule = window.SearchAddon;
|
|
104
|
+
let searchAddon = null;
|
|
105
|
+
if (SearchAddonModule) {
|
|
106
|
+
const SearchAddonClass = SearchAddonModule.SearchAddon || SearchAddonModule;
|
|
107
|
+
searchAddon = new SearchAddonClass();
|
|
108
|
+
term.loadAddon(searchAddon);
|
|
109
|
+
}
|
|
110
|
+
// Search bar: Enter to search, Escape/blur to close
|
|
111
|
+
const searchBar = document.getElementById('search-bar');
|
|
112
|
+
if (searchBar) {
|
|
113
|
+
searchBar.addEventListener('keydown', (e) => {
|
|
114
|
+
if (e.key === 'Enter') {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
const query = searchBar.value.trim();
|
|
117
|
+
searchBar.classList.remove('visible');
|
|
118
|
+
if (query && searchAddon)
|
|
119
|
+
searchAddon.findNext(query);
|
|
120
|
+
term.focus();
|
|
121
|
+
}
|
|
122
|
+
else if (e.key === 'Escape') {
|
|
123
|
+
searchBar.classList.remove('visible');
|
|
124
|
+
term.focus();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
searchBar.addEventListener('blur', () => {
|
|
128
|
+
searchBar.classList.remove('visible');
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Right-click: copy if selection, paste if no selection
|
|
132
|
+
el.addEventListener('contextmenu', (e) => {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
const t = term;
|
|
135
|
+
if (t.hasSelection()) {
|
|
136
|
+
const selected = t.getSelection();
|
|
137
|
+
if (selected)
|
|
138
|
+
window.electronAPI.copyText(selected);
|
|
139
|
+
t.clearSelection();
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
doPaste();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Intercept Ctrl+V / Shift+Insert paste to convert HTML to ANSI
|
|
146
|
+
el.addEventListener('paste', (e) => {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
const html = e.clipboardData?.getData('text/html');
|
|
149
|
+
if (html) {
|
|
150
|
+
const ansi = htmlToAnsi(html);
|
|
151
|
+
if (ansi.trim()) {
|
|
152
|
+
window.electronAPI.sendInput(ansi);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const text = e.clipboardData?.getData('text/plain');
|
|
157
|
+
if (text)
|
|
158
|
+
window.electronAPI.sendInput(text);
|
|
159
|
+
});
|
|
160
|
+
// Clipboard addon handles copy-on-select automatically
|
|
161
|
+
}
|
|
162
|
+
function htmlToAnsi(html) {
|
|
163
|
+
const div = document.createElement('div');
|
|
164
|
+
div.innerHTML = html;
|
|
165
|
+
let result = '';
|
|
166
|
+
walk(div);
|
|
167
|
+
return result;
|
|
168
|
+
function parseRgb(hexOrRgb) {
|
|
169
|
+
// #rrggbb or #rgb
|
|
170
|
+
const hexMatch = hexOrRgb.match(/^#([0-9a-f]{3,6})$/i);
|
|
171
|
+
if (hexMatch) {
|
|
172
|
+
const h = hexMatch[1];
|
|
173
|
+
if (h.length === 3)
|
|
174
|
+
return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)];
|
|
175
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
176
|
+
}
|
|
177
|
+
// rgb(r, g, b)
|
|
178
|
+
const rgbMatch = hexOrRgb.match(/rgb\((\d+)\s*,?\s*(\d+)\s*,?\s*(\d+)\)/i);
|
|
179
|
+
if (rgbMatch)
|
|
180
|
+
return [parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3])];
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
function getStyleVal(style, prop) {
|
|
184
|
+
const m = style.match(new RegExp(prop + '\\s*:\\s*([^;]+)', 'i'));
|
|
185
|
+
return m ? m[1].trim() : '';
|
|
186
|
+
}
|
|
187
|
+
function walk(node) {
|
|
188
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
189
|
+
result += node.textContent || '';
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
193
|
+
return;
|
|
194
|
+
const el = node;
|
|
195
|
+
const tag = el.tagName.toLowerCase();
|
|
196
|
+
const style = el.getAttribute('style') || '';
|
|
197
|
+
if (tag === 'br') {
|
|
198
|
+
result += '\n';
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let prefix = '';
|
|
202
|
+
let suffix = '';
|
|
203
|
+
if (tag === 'b' || tag === 'strong') {
|
|
204
|
+
prefix += '\x1b[1m';
|
|
205
|
+
suffix = '\x1b[22m' + suffix;
|
|
206
|
+
}
|
|
207
|
+
if (tag === 'i' || tag === 'em') {
|
|
208
|
+
prefix += '\x1b[3m';
|
|
209
|
+
suffix = '\x1b[23m' + suffix;
|
|
210
|
+
}
|
|
211
|
+
if (tag === 'u') {
|
|
212
|
+
prefix += '\x1b[4m';
|
|
213
|
+
suffix = '\x1b[24m' + suffix;
|
|
214
|
+
}
|
|
215
|
+
const fw = getStyleVal(style, 'font-weight');
|
|
216
|
+
if (fw && (fw === 'bold' || parseInt(fw) >= 600)) {
|
|
217
|
+
prefix += '\x1b[1m';
|
|
218
|
+
suffix = '\x1b[22m' + suffix;
|
|
219
|
+
}
|
|
220
|
+
if (/italic/i.test(getStyleVal(style, 'font-style'))) {
|
|
221
|
+
prefix += '\x1b[3m';
|
|
222
|
+
suffix = '\x1b[23m' + suffix;
|
|
223
|
+
}
|
|
224
|
+
if (/underline/i.test(getStyleVal(style, 'text-decoration'))) {
|
|
225
|
+
prefix += '\x1b[4m';
|
|
226
|
+
suffix = '\x1b[24m' + suffix;
|
|
227
|
+
}
|
|
228
|
+
const colorVal = getStyleVal(style, 'color');
|
|
229
|
+
if (colorVal) {
|
|
230
|
+
const rgb = parseRgb(colorVal);
|
|
231
|
+
if (rgb) {
|
|
232
|
+
prefix += `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
233
|
+
suffix = '\x1b[39m' + suffix;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const bgVal = getStyleVal(style, 'background-color');
|
|
237
|
+
if (bgVal) {
|
|
238
|
+
const rgb = parseRgb(bgVal);
|
|
239
|
+
if (rgb) {
|
|
240
|
+
prefix += `\x1b[48;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
241
|
+
suffix = '\x1b[49m' + suffix;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Block-level elements: add trailing newline
|
|
245
|
+
const block = /^(div|p|pre|h[1-6]|li|tr)$/.test(tag);
|
|
246
|
+
result += prefix;
|
|
247
|
+
for (const child of Array.from(el.childNodes))
|
|
248
|
+
walk(child);
|
|
249
|
+
result += suffix;
|
|
250
|
+
if (block)
|
|
251
|
+
result += '\n';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function doPaste() {
|
|
255
|
+
const html = window.electronAPI.readClipboardHTML();
|
|
256
|
+
if (html) {
|
|
257
|
+
const ansi = htmlToAnsi(html);
|
|
258
|
+
if (ansi.trim()) {
|
|
259
|
+
window.electronAPI.sendInput(ansi);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const text = window.electronAPI.readClipboard();
|
|
264
|
+
if (text)
|
|
265
|
+
window.electronAPI.sendInput(text);
|
|
266
|
+
}
|
|
267
|
+
function dumpTerminalBuffer() {
|
|
268
|
+
const buffer = term.buffer;
|
|
269
|
+
const active = buffer.active;
|
|
270
|
+
const lines = [];
|
|
271
|
+
for (let i = 0; i < active.length; i++) {
|
|
272
|
+
const line = active.getLine(i);
|
|
273
|
+
if (line) {
|
|
274
|
+
lines.push(line.translateToString(false));
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
lines.push('');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
console.log('=== Terminal Buffer (F1 dump) ===');
|
|
281
|
+
console.log(`baseY=${active.baseY}, totalLines=${active.length}, captured=${lines.length}`);
|
|
282
|
+
console.log(lines.join('\n'));
|
|
283
|
+
console.log('=== End of Buffer ===');
|
|
284
|
+
return lines;
|
|
285
|
+
}
|
|
286
|
+
function getLastLines(n) {
|
|
287
|
+
const buffer = term.buffer;
|
|
288
|
+
const active = buffer.active;
|
|
289
|
+
const start = Math.max(0, active.length - n);
|
|
290
|
+
const result = [];
|
|
291
|
+
for (let i = start; i < active.length; i++) {
|
|
292
|
+
const line = active.getLine(i);
|
|
293
|
+
result.push(line ? line.translateToString(false) : '');
|
|
294
|
+
}
|
|
295
|
+
return result.join('\n').trim();
|
|
296
|
+
}
|
|
297
|
+
function getSnapshotContent() {
|
|
298
|
+
const buffer = term.buffer;
|
|
299
|
+
const active = buffer.active;
|
|
300
|
+
let separatorIndex = -1;
|
|
301
|
+
for (let i = active.length - 1; i >= 0; i--) {
|
|
302
|
+
const line = active.getLine(i);
|
|
303
|
+
if (line && /─{30,}/.test(line.translateToString(false))) {
|
|
304
|
+
separatorIndex = i;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const result = [];
|
|
309
|
+
for (let i = separatorIndex + 1; i < active.length; i++) {
|
|
310
|
+
const line = active.getLine(i);
|
|
311
|
+
result.push(line ? line.translateToString(false) : '');
|
|
312
|
+
}
|
|
313
|
+
return result.join('\n').trim();
|
|
314
|
+
}
|
|
315
|
+
window.addEventListener('resize', () => {
|
|
316
|
+
fitAddon.fit();
|
|
317
|
+
});
|
|
318
|
+
term.focus();
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.BOT_TYPE = void 0;
|
|
37
|
+
exports.getUpdates = getUpdates;
|
|
38
|
+
exports.sendMessage = sendMessage;
|
|
39
|
+
exports.notifyStart = notifyStart;
|
|
40
|
+
exports.notifyStop = notifyStop;
|
|
41
|
+
exports.fetchQRCode = fetchQRCode;
|
|
42
|
+
exports.pollQRStatus = pollQRStatus;
|
|
43
|
+
exports.listAccountIds = listAccountIds;
|
|
44
|
+
exports.loadAccount = loadAccount;
|
|
45
|
+
exports.saveAccount = saveAccount;
|
|
46
|
+
exports.resolveAccount = resolveAccount;
|
|
47
|
+
const crypto = __importStar(require("crypto"));
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
52
|
+
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35000;
|
|
53
|
+
const DEFAULT_API_TIMEOUT_MS = 15000;
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Package info (mirrors the plugin's package.json metadata)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
function readPkgVersion() {
|
|
58
|
+
try {
|
|
59
|
+
const pkgPath = path.join(__dirname, '..', '..', 'node_modules', '@tencent-weixin', 'openclaw-weixin', 'package.json');
|
|
60
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
61
|
+
return pkg.version ?? 'unknown';
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return 'unknown';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const CHANNEL_VERSION = readPkgVersion();
|
|
68
|
+
exports.BOT_TYPE = '3';
|
|
69
|
+
function buildBaseInfo() {
|
|
70
|
+
return { channel_version: CHANNEL_VERSION, bot_agent: 'cc-remote' };
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// HTTP helpers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
function randomWechatUin() {
|
|
76
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
77
|
+
return Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
78
|
+
}
|
|
79
|
+
function buildHeaders(token) {
|
|
80
|
+
const headers = {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
AuthorizationType: 'ilink_bot_token',
|
|
83
|
+
'X-WECHAT-UIN': randomWechatUin(),
|
|
84
|
+
};
|
|
85
|
+
if (token?.trim()) {
|
|
86
|
+
headers.Authorization = `Bearer ${token.trim()}`;
|
|
87
|
+
}
|
|
88
|
+
return headers;
|
|
89
|
+
}
|
|
90
|
+
async function apiPost(params) {
|
|
91
|
+
const base = params.baseUrl.endsWith('/') ? params.baseUrl : `${params.baseUrl}/`;
|
|
92
|
+
const url = new URL(params.endpoint, base);
|
|
93
|
+
const headers = buildHeaders(params.token);
|
|
94
|
+
const timeoutMs = params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch(url.toString(), {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers,
|
|
101
|
+
body: params.body,
|
|
102
|
+
signal: controller.signal,
|
|
103
|
+
});
|
|
104
|
+
clearTimeout(t);
|
|
105
|
+
const rawText = await res.text();
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new Error(`${params.label} ${res.status}: ${rawText.slice(0, 200)}`);
|
|
108
|
+
}
|
|
109
|
+
return rawText;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
clearTimeout(t);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function apiGet(params) {
|
|
116
|
+
const base = params.baseUrl.endsWith('/') ? params.baseUrl : `${params.baseUrl}/`;
|
|
117
|
+
const url = new URL(params.endpoint, base);
|
|
118
|
+
const timeoutMs = params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(url.toString(), {
|
|
123
|
+
method: 'GET',
|
|
124
|
+
headers: buildHeaders(),
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
});
|
|
127
|
+
clearTimeout(t);
|
|
128
|
+
const rawText = await res.text();
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
throw new Error(`${params.label} ${res.status}: ${rawText.slice(0, 200)}`);
|
|
131
|
+
}
|
|
132
|
+
return rawText;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
clearTimeout(t);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Core API functions
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
/** Long-poll for new messages */
|
|
142
|
+
async function getUpdates(params) {
|
|
143
|
+
const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
144
|
+
try {
|
|
145
|
+
const rawText = await apiPost({
|
|
146
|
+
baseUrl: params.baseUrl,
|
|
147
|
+
endpoint: 'ilink/bot/getupdates',
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
get_updates_buf: params.get_updates_buf ?? '',
|
|
150
|
+
base_info: buildBaseInfo(),
|
|
151
|
+
}),
|
|
152
|
+
token: params.token,
|
|
153
|
+
timeoutMs: timeout,
|
|
154
|
+
label: 'getUpdates',
|
|
155
|
+
});
|
|
156
|
+
return JSON.parse(rawText);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
160
|
+
return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Send a message to a WeChat user */
|
|
166
|
+
async function sendMessage(params) {
|
|
167
|
+
await apiPost({
|
|
168
|
+
baseUrl: params.baseUrl,
|
|
169
|
+
endpoint: 'ilink/bot/sendmessage',
|
|
170
|
+
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
|
|
171
|
+
token: params.token,
|
|
172
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
173
|
+
label: 'sendMessage',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Notify server that channel client is starting */
|
|
177
|
+
async function notifyStart(params) {
|
|
178
|
+
await apiPost({
|
|
179
|
+
baseUrl: params.baseUrl,
|
|
180
|
+
endpoint: 'ilink/bot/msg/notifystart',
|
|
181
|
+
body: JSON.stringify({ base_info: buildBaseInfo() }),
|
|
182
|
+
token: params.token,
|
|
183
|
+
label: 'notifyStart',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** Notify server that channel client is stopping */
|
|
187
|
+
async function notifyStop(params) {
|
|
188
|
+
await apiPost({
|
|
189
|
+
baseUrl: params.baseUrl,
|
|
190
|
+
endpoint: 'ilink/bot/msg/notifystop',
|
|
191
|
+
body: JSON.stringify({ base_info: buildBaseInfo() }),
|
|
192
|
+
token: params.token,
|
|
193
|
+
label: 'notifyStop',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// QR Login
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
const FIXED_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
200
|
+
/** Fetch a QR code for WeChat login */
|
|
201
|
+
async function fetchQRCode(botType, localTokens) {
|
|
202
|
+
const rawText = await apiPost({
|
|
203
|
+
baseUrl: FIXED_BASE_URL,
|
|
204
|
+
endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
|
|
205
|
+
body: JSON.stringify({ local_token_list: localTokens }),
|
|
206
|
+
label: 'fetchQRCode',
|
|
207
|
+
});
|
|
208
|
+
return JSON.parse(rawText);
|
|
209
|
+
}
|
|
210
|
+
/** Poll QR code scan status */
|
|
211
|
+
async function pollQRStatus(qrcode) {
|
|
212
|
+
try {
|
|
213
|
+
const rawText = await apiGet({
|
|
214
|
+
baseUrl: FIXED_BASE_URL,
|
|
215
|
+
endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
|
|
216
|
+
timeoutMs: 35000,
|
|
217
|
+
label: 'pollQRStatus',
|
|
218
|
+
});
|
|
219
|
+
return JSON.parse(rawText);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
223
|
+
return { status: 'wait' };
|
|
224
|
+
}
|
|
225
|
+
return { status: 'wait' };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Account storage
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
function resolveAccountsDir() {
|
|
232
|
+
const dir = path.join(os.homedir(), '.cc-remote', 'weixin', 'accounts');
|
|
233
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
234
|
+
return dir;
|
|
235
|
+
}
|
|
236
|
+
function listAccountIds() {
|
|
237
|
+
const indexPath = path.join(os.homedir(), '.cc-remote', 'weixin', 'accounts.json');
|
|
238
|
+
const fromIndex = [];
|
|
239
|
+
try {
|
|
240
|
+
if (fs.existsSync(indexPath)) {
|
|
241
|
+
const raw = fs.readFileSync(indexPath, 'utf-8');
|
|
242
|
+
const parsed = JSON.parse(raw);
|
|
243
|
+
if (Array.isArray(parsed)) {
|
|
244
|
+
fromIndex.push(...parsed.filter((id) => typeof id === 'string' && id.trim() !== ''));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { /* ignore */ }
|
|
249
|
+
// Also scan accounts directory for .json files not in the index
|
|
250
|
+
const accountsDir = path.join(os.homedir(), '.cc-remote', 'weixin', 'accounts');
|
|
251
|
+
try {
|
|
252
|
+
if (fs.existsSync(accountsDir)) {
|
|
253
|
+
for (const entry of fs.readdirSync(accountsDir)) {
|
|
254
|
+
if (entry.endsWith('.json') && entry !== 'accounts.json') {
|
|
255
|
+
const id = entry.slice(0, -5); // strip .json
|
|
256
|
+
if (!fromIndex.includes(id))
|
|
257
|
+
fromIndex.push(id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch { /* ignore */ }
|
|
263
|
+
return fromIndex;
|
|
264
|
+
}
|
|
265
|
+
function registerAccountId(accountId) {
|
|
266
|
+
const dir = path.join(os.homedir(), '.cc-remote', 'weixin');
|
|
267
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
268
|
+
const indexPath = path.join(dir, 'accounts.json');
|
|
269
|
+
const existing = listAccountIds();
|
|
270
|
+
if (existing.includes(accountId))
|
|
271
|
+
return;
|
|
272
|
+
existing.push(accountId);
|
|
273
|
+
fs.writeFileSync(indexPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
274
|
+
}
|
|
275
|
+
function loadAccount(accountId) {
|
|
276
|
+
const filePath = path.join(resolveAccountsDir(), `${accountId}.json`);
|
|
277
|
+
try {
|
|
278
|
+
if (fs.existsSync(filePath)) {
|
|
279
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch { /* ignore */ }
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
function saveAccount(accountId, data) {
|
|
286
|
+
const dir = resolveAccountsDir();
|
|
287
|
+
// Clean up all existing accounts, keep only the latest one
|
|
288
|
+
const existingIds = listAccountIds();
|
|
289
|
+
for (const id of existingIds) {
|
|
290
|
+
const p = path.join(dir, `${id}.json`);
|
|
291
|
+
try {
|
|
292
|
+
fs.unlinkSync(p);
|
|
293
|
+
}
|
|
294
|
+
catch { /* ignore */ }
|
|
295
|
+
}
|
|
296
|
+
// Clear accounts index
|
|
297
|
+
const indexPath = path.join(dir, '..', 'accounts.json');
|
|
298
|
+
try {
|
|
299
|
+
fs.unlinkSync(indexPath);
|
|
300
|
+
}
|
|
301
|
+
catch { /* ignore */ }
|
|
302
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
303
|
+
const token = (data.token || '').trim();
|
|
304
|
+
const baseUrl = (data.baseUrl || '').trim();
|
|
305
|
+
const userId = (data.userId || '').trim();
|
|
306
|
+
const accountData = {
|
|
307
|
+
...(token ? { token, savedAt: new Date().toISOString() } : {}),
|
|
308
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
309
|
+
...(userId ? { userId } : {}),
|
|
310
|
+
};
|
|
311
|
+
const filePath = path.join(dir, `${accountId}.json`);
|
|
312
|
+
fs.writeFileSync(filePath, JSON.stringify(accountData, null, 2), 'utf-8');
|
|
313
|
+
registerAccountId(accountId);
|
|
314
|
+
}
|
|
315
|
+
function resolveAccount(accountId) {
|
|
316
|
+
const accounts = listAccountIds();
|
|
317
|
+
const id = accountId?.trim() || accounts[0];
|
|
318
|
+
if (!id) {
|
|
319
|
+
throw new Error('weixin: no account configured — run login first');
|
|
320
|
+
}
|
|
321
|
+
const data = loadAccount(id);
|
|
322
|
+
return {
|
|
323
|
+
accountId: id,
|
|
324
|
+
baseUrl: data?.baseUrl || DEFAULT_BASE_URL,
|
|
325
|
+
token: data?.token,
|
|
326
|
+
configured: Boolean(data?.token),
|
|
327
|
+
};
|
|
328
|
+
}
|