@yvhitxcel/opencode-remote 0.15.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 +82 -0
- package/bin/opencode-remote.js +70 -0
- package/bin/opencode-weixin.js +10 -0
- package/dist/AGENTS.md +20 -0
- package/dist/MEMORY.md +21 -0
- package/dist/bot-runner.js +180 -0
- package/dist/cli.js +256 -0
- package/dist/core/approval.js +95 -0
- package/dist/core/auth.js +119 -0
- package/dist/core/config.js +61 -0
- package/dist/core/notifications.js +134 -0
- package/dist/core/qiniu.js +267 -0
- package/dist/core/registry.js +86 -0
- package/dist/core/router.js +344 -0
- package/dist/core/session.js +403 -0
- package/dist/core/setup.js +418 -0
- package/dist/core/types.js +16 -0
- package/dist/feishu/adapter.js +72 -0
- package/dist/feishu/bot.js +168 -0
- package/dist/feishu/commands.js +601 -0
- package/dist/feishu/handler.js +380 -0
- package/dist/index.js +60 -0
- package/dist/opencode/client.js +823 -0
- package/dist/package-lock.json +762 -0
- package/dist/patch_spawn.js +28 -0
- package/dist/plugins/agents/acp/acp-adapter.js +42 -0
- package/dist/plugins/agents/claude-code/index.js +69 -0
- package/dist/plugins/agents/codex/index.js +44 -0
- package/dist/plugins/agents/copilot/index.js +44 -0
- package/dist/plugins/agents/opencode/index.js +66 -0
- package/dist/telegram/bot.js +288 -0
- package/dist/utils/message-split.js +38 -0
- package/dist/web/code-viewer.js +266 -0
- package/dist/weixin/adapter.js +135 -0
- package/dist/weixin/api.js +179 -0
- package/dist/weixin/bot.js +183 -0
- package/dist/weixin/commands.js +758 -0
- package/dist/weixin/handler.js +577 -0
- package/dist/weixin/node_modules/encodeurl/LICENSE +22 -0
- package/dist/weixin/node_modules/encodeurl/README.md +109 -0
- package/dist/weixin/node_modules/encodeurl/index.js +60 -0
- package/dist/weixin/node_modules/encodeurl/package.json +40 -0
- package/dist/weixin/node_modules/qiniu/.claude/settings.local.json +7 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/ci-test.yml +36 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/npm-publish.yml +20 -0
- package/dist/weixin/node_modules/qiniu/.github/workflows/version-check.yml +19 -0
- package/dist/weixin/node_modules/qiniu/.idea/MarsCodeWorkspaceAppSettings.xml +7 -0
- package/dist/weixin/node_modules/qiniu/.idea/codeStyles/Project.xml +44 -0
- package/dist/weixin/node_modules/qiniu/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/dist/weixin/node_modules/qiniu/.idea/git_toolbox_blame.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/jsLibraryMappings.xml +6 -0
- package/dist/weixin/node_modules/qiniu/.idea/modules.xml +8 -0
- package/dist/weixin/node_modules/qiniu/.idea/nodejs-sdk.iml +12 -0
- package/dist/weixin/node_modules/qiniu/.idea/vcs.xml +6 -0
- package/dist/weixin/node_modules/qiniu/CHANGELOG.md +292 -0
- package/dist/weixin/node_modules/qiniu/README.md +56 -0
- package/dist/weixin/node_modules/qiniu/StorageResponseInterface.d.ts +239 -0
- package/dist/weixin/node_modules/qiniu/codecov.yml +28 -0
- package/dist/weixin/node_modules/qiniu/index.d.ts +1995 -0
- package/dist/weixin/node_modules/qiniu/index.js +32 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/HISTORY.md +14 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/LICENSE +22 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/README.md +128 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/index.js +60 -0
- package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/package.json +40 -0
- package/dist/weixin/node_modules/qiniu/package.json +80 -0
- package/dist/weixin/node_modules/qiniu/qiniu/auth/digest.js +13 -0
- package/dist/weixin/node_modules/qiniu/qiniu/cdn.js +149 -0
- package/dist/weixin/node_modules/qiniu/qiniu/conf.js +254 -0
- package/dist/weixin/node_modules/qiniu/qiniu/fop.js +112 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/client.js +253 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpoint.js +66 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsProvider.js +27 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsRetryPolicy.js +76 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/base.js +31 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/index.js +9 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/qiniuAuth.js +53 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/retryDomains.js +101 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/ua.js +36 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/region.js +349 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsProvider.js +788 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsRetryPolicy.js +242 -0
- package/dist/weixin/node_modules/qiniu/qiniu/httpc/responseWrapper.js +40 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/index.js +4 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/retrier.js +99 -0
- package/dist/weixin/node_modules/qiniu/qiniu/retry/retryPolicy.js +55 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rpc.js +237 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/app.js +123 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/credentials.js +57 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/room.js +118 -0
- package/dist/weixin/node_modules/qiniu/qiniu/rtc/util.js +16 -0
- package/dist/weixin/node_modules/qiniu/qiniu/sms/message.js +58 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/form.js +442 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/internal.js +214 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/resume.js +1272 -0
- package/dist/weixin/node_modules/qiniu/qiniu/storage/rs.js +1764 -0
- package/dist/weixin/node_modules/qiniu/qiniu/util.js +382 -0
- package/dist/weixin/node_modules/qiniu/qiniu/zone.js +230 -0
- package/dist/weixin/node_modules/qiniu/tsconfig.json +112 -0
- package/dist/weixin/types.js +25 -0
- package/package.json +56 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Code viewer utilities for Weixin bot
|
|
2
|
+
// Uses OpenCode SDK file.status() API - no git dependency
|
|
3
|
+
import { readFileSync, existsSync, statSync, readdirSync } from 'fs';
|
|
4
|
+
import { join, extname } from 'path';
|
|
5
|
+
|
|
6
|
+
function getProjectRoot() {
|
|
7
|
+
const candidates = [
|
|
8
|
+
process.env.OPENCODE_PROJECT_ROOT,
|
|
9
|
+
process.cwd(),
|
|
10
|
+
process.env.HOME,
|
|
11
|
+
];
|
|
12
|
+
for (const p of candidates) {
|
|
13
|
+
if (p && existsSync(p)) {
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return process.cwd();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Get modified files from OpenCode session
|
|
21
|
+
export async function getModifiedFilesFromSession(session) {
|
|
22
|
+
try {
|
|
23
|
+
const result = await session.client.file.status({});
|
|
24
|
+
if (result.error) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const files = result.data || [];
|
|
28
|
+
return files.map(f => ({
|
|
29
|
+
path: f.path,
|
|
30
|
+
additions: f.additions || 0,
|
|
31
|
+
deletions: f.deletions || 0,
|
|
32
|
+
size: f.size || 0,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('Failed to get modified files from session:', error.message);
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Fallback: scan directory for recently modified files
|
|
42
|
+
export function getModifiedFiles() {
|
|
43
|
+
try {
|
|
44
|
+
const root = getProjectRoot();
|
|
45
|
+
const files = [];
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const oneHourAgo = now - 3600000;
|
|
48
|
+
scanDir(root, root, oneHourAgo, files);
|
|
49
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
50
|
+
return files.slice(0, 50).map(f => ({
|
|
51
|
+
path: f.relativePath,
|
|
52
|
+
additions: 0,
|
|
53
|
+
deletions: 0,
|
|
54
|
+
size: f.size,
|
|
55
|
+
mtime: f.mtime,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error('Failed to get modified files:', error.message);
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scanDir(root, dir, cutoff, results, depth = 0) {
|
|
65
|
+
if (depth > 10) return;
|
|
66
|
+
try {
|
|
67
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
70
|
+
const fullPath = join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
scanDir(root, fullPath, cutoff, results, depth + 1);
|
|
73
|
+
}
|
|
74
|
+
else if (entry.isFile()) {
|
|
75
|
+
try {
|
|
76
|
+
const stat = statSync(fullPath);
|
|
77
|
+
if (stat.mtimeMs > cutoff) {
|
|
78
|
+
const relativePath = fullPath.substring(root.length + 1);
|
|
79
|
+
results.push({
|
|
80
|
+
relativePath,
|
|
81
|
+
size: stat.size,
|
|
82
|
+
mtime: stat.mtimeMs,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { /* ignore */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getFileContent(filePath, maxLines = 500) {
|
|
94
|
+
try {
|
|
95
|
+
const root = getProjectRoot();
|
|
96
|
+
const fullPath = join(root, filePath);
|
|
97
|
+
if (!existsSync(fullPath)) {
|
|
98
|
+
return { error: `File not found: ${filePath}` };
|
|
99
|
+
}
|
|
100
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
const truncated = lines.length > maxLines;
|
|
103
|
+
const displayLines = truncated ? lines.slice(0, maxLines) : lines;
|
|
104
|
+
const numbered = displayLines.map((line, i) => `${String(i + 1).padStart(4)}: ${line}`).join('\n');
|
|
105
|
+
return {
|
|
106
|
+
path: filePath,
|
|
107
|
+
content: numbered,
|
|
108
|
+
totalLines: lines.length,
|
|
109
|
+
truncated,
|
|
110
|
+
extension: extname(filePath).slice(1),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return { error: `Failed to read file: ${error.message}` };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getFileDiff(filePath) {
|
|
119
|
+
try {
|
|
120
|
+
const root = getProjectRoot();
|
|
121
|
+
const fullPath = join(root, filePath);
|
|
122
|
+
if (!existsSync(fullPath)) {
|
|
123
|
+
return { path: filePath, diff: `File not found: ${filePath}` };
|
|
124
|
+
}
|
|
125
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
126
|
+
return { path: filePath, diff: content };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return { path: filePath, diff: '(unable to read file)' };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getAllDiffs() {
|
|
134
|
+
const files = getModifiedFiles();
|
|
135
|
+
if (files.length === 0) {
|
|
136
|
+
return '(no recently modified files found)';
|
|
137
|
+
}
|
|
138
|
+
const root = getProjectRoot();
|
|
139
|
+
let result = '';
|
|
140
|
+
for (const f of files.slice(0, 10)) {
|
|
141
|
+
const fullPath = join(root, f.path);
|
|
142
|
+
if (existsSync(fullPath)) {
|
|
143
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
144
|
+
result += `===== ${f.path} =====\n${content}\n\n`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result || '(unable to read files)';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseMdSections(content) {
|
|
151
|
+
const sections = [];
|
|
152
|
+
const lines = content.split('\n');
|
|
153
|
+
let currentSection = { title: 'Introduction', content: [], level: 0 };
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
156
|
+
if (headingMatch) {
|
|
157
|
+
if (currentSection.content.length > 0) {
|
|
158
|
+
sections.push({ ...currentSection, content: currentSection.content.join('\n') });
|
|
159
|
+
}
|
|
160
|
+
currentSection = {
|
|
161
|
+
title: headingMatch[2].trim(),
|
|
162
|
+
content: [],
|
|
163
|
+
level: headingMatch[1].length,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
currentSection.content.push(line);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (currentSection.content.length > 0) {
|
|
171
|
+
sections.push({ ...currentSection, content: currentSection.content.join('\n') });
|
|
172
|
+
}
|
|
173
|
+
return sections;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function scoreRelevance(section, keywords) {
|
|
177
|
+
const text = `${section.title} ${section.content}`.toLowerCase();
|
|
178
|
+
let score = 0;
|
|
179
|
+
for (const keyword of keywords) {
|
|
180
|
+
const lower = keyword.toLowerCase();
|
|
181
|
+
if (text.includes(lower)) {
|
|
182
|
+
score += 10;
|
|
183
|
+
if (section.title.toLowerCase().includes(lower)) {
|
|
184
|
+
score += 20;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return score;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function getRelevantMdSections(filePath, keywords, maxSections = 5) {
|
|
192
|
+
try {
|
|
193
|
+
const root = getProjectRoot();
|
|
194
|
+
const fullPath = join(root, filePath);
|
|
195
|
+
if (!existsSync(fullPath)) {
|
|
196
|
+
return { error: `File not found: ${filePath}` };
|
|
197
|
+
}
|
|
198
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
199
|
+
const sections = parseMdSections(content);
|
|
200
|
+
if (keywords.length === 0) {
|
|
201
|
+
const preview = sections.slice(0, maxSections).map((s, i) => ({
|
|
202
|
+
index: i + 1,
|
|
203
|
+
title: s.title,
|
|
204
|
+
content: s.content.length > 300 ? s.content.slice(0, 300) + '...' : s.content,
|
|
205
|
+
}));
|
|
206
|
+
return { path: filePath, sections: preview, totalSections: sections.length };
|
|
207
|
+
}
|
|
208
|
+
const scored = sections
|
|
209
|
+
.map((s, i) => ({ ...s, index: i + 1, score: scoreRelevance(s, keywords) }))
|
|
210
|
+
.filter(s => s.score > 0)
|
|
211
|
+
.sort((a, b) => b.score - a.score)
|
|
212
|
+
.slice(0, maxSections);
|
|
213
|
+
if (scored.length === 0) {
|
|
214
|
+
const preview = sections.slice(0, maxSections).map((s, i) => ({
|
|
215
|
+
index: i + 1,
|
|
216
|
+
title: s.title,
|
|
217
|
+
content: s.content.length > 300 ? s.content.slice(0, 300) + '...' : s.content,
|
|
218
|
+
}));
|
|
219
|
+
return { path: filePath, sections: preview, totalSections: sections.length };
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
path: filePath,
|
|
223
|
+
sections: scored.map(s => ({
|
|
224
|
+
index: s.index,
|
|
225
|
+
title: s.title,
|
|
226
|
+
content: s.content.length > 500 ? s.content.slice(0, 500) + '...' : s.content,
|
|
227
|
+
score: s.score,
|
|
228
|
+
})),
|
|
229
|
+
totalSections: sections.length,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
return { error: `Failed to read MD file: ${error.message}` };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function findMdFiles() {
|
|
238
|
+
try {
|
|
239
|
+
const root = getProjectRoot();
|
|
240
|
+
const files = [];
|
|
241
|
+
scanMdFiles(root, root, files, 0);
|
|
242
|
+
return files;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function scanMdFiles(root, dir, results, depth) {
|
|
250
|
+
if (depth > 8) return;
|
|
251
|
+
try {
|
|
252
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
255
|
+
const fullPath = join(dir, entry.name);
|
|
256
|
+
if (entry.isDirectory()) {
|
|
257
|
+
scanMdFiles(root, fullPath, results, depth + 1);
|
|
258
|
+
}
|
|
259
|
+
else if (entry.isFile() && /\.(md|MD|markdown)$/i.test(entry.name)) {
|
|
260
|
+
const relativePath = fullPath.replace(root + '\\', '').replace(root + '/', '');
|
|
261
|
+
results.push(relativePath);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch { /* ignore */ }
|
|
266
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import { sendMessage as sendWeixinMessage, getConfig, sendTyping } from './api.js';
|
|
3
|
+
|
|
4
|
+
function createWeixinAdapter(baseUrl, token, botId) {
|
|
5
|
+
const contextTokens = new Map();
|
|
6
|
+
const typingTickets = new Map();
|
|
7
|
+
const processedMessages = new Map();
|
|
8
|
+
const DEDUP_WINDOW_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
function isDuplicate(messageId) {
|
|
11
|
+
if (!messageId) return false;
|
|
12
|
+
const seenAt = processedMessages.get(messageId);
|
|
13
|
+
if (seenAt && Date.now() - seenAt < DEDUP_WINDOW_MS) return true;
|
|
14
|
+
processedMessages.set(messageId, Date.now());
|
|
15
|
+
if (processedMessages.size > 1000) {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
for (const [id, ts] of processedMessages.entries()) {
|
|
18
|
+
if (now - ts > DEDUP_WINDOW_MS) processedMessages.delete(id);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
contextTokens,
|
|
26
|
+
typingTickets,
|
|
27
|
+
isDuplicate,
|
|
28
|
+
_baseUrl: baseUrl,
|
|
29
|
+
_token: token,
|
|
30
|
+
_botId: botId,
|
|
31
|
+
async reply(threadId, text) {
|
|
32
|
+
let contextToken = contextTokens.get(threadId);
|
|
33
|
+
let retryCount = 0;
|
|
34
|
+
const maxRetries = 2;
|
|
35
|
+
|
|
36
|
+
while (retryCount <= maxRetries) {
|
|
37
|
+
if (!contextToken) {
|
|
38
|
+
try {
|
|
39
|
+
console.log(`[Weixin] Fetching contextToken for ${threadId} (attempt ${retryCount + 1})...`);
|
|
40
|
+
const r = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: undefined });
|
|
41
|
+
contextToken = r.context_token || r.typing_ticket;
|
|
42
|
+
if (contextToken) {
|
|
43
|
+
contextTokens.set(threadId, contextToken);
|
|
44
|
+
console.log(`[Weixin] Got contextToken: ${contextToken.slice(0, 8)}...`);
|
|
45
|
+
} else if (r.errcode === -14) {
|
|
46
|
+
console.log(`[Weixin] Session timeout, retrying with fresh token...`);
|
|
47
|
+
contextTokens.delete(threadId);
|
|
48
|
+
retryCount++;
|
|
49
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
50
|
+
continue;
|
|
51
|
+
} else {
|
|
52
|
+
console.error(`[Weixin] getConfig returned no contextToken, errcode: ${r.errcode}`);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(`[Weixin] getConfig failed:`, e.message);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!contextToken) {
|
|
62
|
+
console.error(`[Weixin] reply failed: no contextToken for thread ${threadId} after ${retryCount} retries`);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await sendWeixinMessage({
|
|
68
|
+
baseUrl, token,
|
|
69
|
+
body: { msg: { from_user_id: botId, to_user_id: threadId, client_id: `${Date.now()}-${randomBytes(8).toString('hex')}`, message_type: 2, message_state: 2, context_token: contextToken, item_list: [{ type: 1, text_item: { text } }] } }
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
const errMsg = e.message || '';
|
|
74
|
+
if (errMsg.includes('-14') || errMsg.includes('timeout') || errMsg.includes('session')) {
|
|
75
|
+
console.log(`[Weixin] Send failed (timeout), clearing token and retrying...`);
|
|
76
|
+
contextTokens.delete(threadId);
|
|
77
|
+
contextToken = null;
|
|
78
|
+
retryCount++;
|
|
79
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
console.error(`[Weixin] reply failed:`, e.message);
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const err = new Error(`reply failed after ${maxRetries + 1} attempts`);
|
|
87
|
+
console.error(`[Weixin] ${err.message}`);
|
|
88
|
+
throw err;
|
|
89
|
+
},
|
|
90
|
+
async sendTypingIndicator(threadId) {
|
|
91
|
+
const cachedTicket = typingTickets.get(threadId);
|
|
92
|
+
let ticket = cachedTicket;
|
|
93
|
+
|
|
94
|
+
if (!ticket) {
|
|
95
|
+
try {
|
|
96
|
+
const r = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: contextTokens.get(threadId) });
|
|
97
|
+
if (r.errcode === -14) {
|
|
98
|
+
contextTokens.delete(threadId);
|
|
99
|
+
typingTickets.delete(threadId);
|
|
100
|
+
const freshConfig = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: undefined });
|
|
101
|
+
ticket = freshConfig.typing_ticket;
|
|
102
|
+
if (freshConfig.context_token) {
|
|
103
|
+
contextTokens.set(threadId, freshConfig.context_token);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
ticket = r.typing_ticket;
|
|
107
|
+
}
|
|
108
|
+
if (ticket) typingTickets.set(threadId, ticket);
|
|
109
|
+
} catch { console.debug('[typing] getConfig failed'); }
|
|
110
|
+
}
|
|
111
|
+
if (ticket) {
|
|
112
|
+
try {
|
|
113
|
+
await sendTyping({ baseUrl, token, body: { ilink_user_id: threadId, typing_ticket: ticket, status: 1 } });
|
|
114
|
+
} catch (e) {
|
|
115
|
+
if (e.message?.includes('-14') || e.message?.includes('timeout')) {
|
|
116
|
+
typingTickets.delete(threadId);
|
|
117
|
+
contextTokens.delete(threadId);
|
|
118
|
+
try {
|
|
119
|
+
const freshConfig = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: undefined });
|
|
120
|
+
if (freshConfig.typing_ticket) {
|
|
121
|
+
typingTickets.set(threadId, freshConfig.typing_ticket);
|
|
122
|
+
await sendTyping({ baseUrl, token, body: { ilink_user_id: threadId, typing_ticket: freshConfig.typing_ticket, status: 1 } });
|
|
123
|
+
}
|
|
124
|
+
} catch { console.debug('[typing] retry getConfig failed'); }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
async updateMessage(threadId, messageId, text) { await this.reply(threadId, text); },
|
|
130
|
+
async deleteMessage() {},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { createWeixinAdapter };
|
|
135
|
+
export default createWeixinAdapter;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weixin API client (based on @tencent-weixin/openclaw-weixin)
|
|
3
|
+
* Simplified version without OpenClaw framework dependency.
|
|
4
|
+
*/
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { DEFAULT_BASE_URL } from './types.js';
|
|
7
|
+
// Build headers
|
|
8
|
+
function buildHeaders(body, token) {
|
|
9
|
+
const headers = {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
'AuthorizationType': 'ilink_bot_token',
|
|
12
|
+
'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
|
|
13
|
+
'X-WECHAT-UIN': randomWechatUin(),
|
|
14
|
+
};
|
|
15
|
+
if (token?.trim()) {
|
|
16
|
+
headers['Authorization'] = `Bearer ${token.trim()}`;
|
|
17
|
+
}
|
|
18
|
+
return headers;
|
|
19
|
+
}
|
|
20
|
+
// Random X-WECHAT-UIN header
|
|
21
|
+
function randomWechatUin() {
|
|
22
|
+
const uint32 = randomBytes(4).readUInt32BE(0);
|
|
23
|
+
return Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
24
|
+
}
|
|
25
|
+
// Ensure URL has trailing slash
|
|
26
|
+
function ensureTrailingSlash(url) {
|
|
27
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
28
|
+
}
|
|
29
|
+
// Generic fetch wrapper
|
|
30
|
+
async function apiFetch(params) {
|
|
31
|
+
const base = ensureTrailingSlash(params.baseUrl);
|
|
32
|
+
const url = new URL(params.endpoint, base);
|
|
33
|
+
const headers = buildHeaders(params.body, params.token);
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const t = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(url.toString(), {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers,
|
|
40
|
+
body: params.body,
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
clearTimeout(t);
|
|
44
|
+
const rawText = await res.text();
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
console.error(`[apiFetch] ${params.label} failed: ${res.status}`, rawText);
|
|
47
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
48
|
+
}
|
|
49
|
+
if (rawText.includes('"errcode"') && rawText.includes('-14')) {
|
|
50
|
+
console.error(`[apiFetch] ${params.label} session timeout:`, rawText);
|
|
51
|
+
}
|
|
52
|
+
return rawText;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
clearTimeout(t);
|
|
56
|
+
console.error(`[apiFetch] ${params.label} error:`, err.message);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Login APIs
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
/**
|
|
64
|
+
* Fetch QR code for login
|
|
65
|
+
*/
|
|
66
|
+
export async function fetchQRCode(baseUrl = DEFAULT_BASE_URL, botType = '3') {
|
|
67
|
+
const base = ensureTrailingSlash(baseUrl);
|
|
68
|
+
const url = new URL(`ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`, base);
|
|
69
|
+
const res = await fetch(url.toString());
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const body = await res.text().catch(() => '(unreadable)');
|
|
72
|
+
throw new Error(`Failed to fetch QR code: ${res.status} ${res.statusText}`);
|
|
73
|
+
}
|
|
74
|
+
return await res.json();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Poll QR code status
|
|
78
|
+
*/
|
|
79
|
+
export async function pollQRStatus(baseUrl = DEFAULT_BASE_URL, qrcode) {
|
|
80
|
+
const base = ensureTrailingSlash(baseUrl);
|
|
81
|
+
const url = new URL(`ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, base);
|
|
82
|
+
const headers = {
|
|
83
|
+
'iLink-App-ClientVersion': '1',
|
|
84
|
+
};
|
|
85
|
+
const res = await fetch(url.toString(), { headers });
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const body = await res.text().catch(() => '(unreadable)');
|
|
88
|
+
throw new Error(`Failed to poll QR status: ${res.status} ${res.statusText}`);
|
|
89
|
+
}
|
|
90
|
+
return await res.json();
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Message APIs
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
96
|
+
const DEFAULT_API_TIMEOUT_MS = 15_000;
|
|
97
|
+
/**
|
|
98
|
+
* Long-poll for new messages
|
|
99
|
+
*/
|
|
100
|
+
export async function getUpdates(params) {
|
|
101
|
+
const timeout = params.longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
102
|
+
try {
|
|
103
|
+
const rawText = await apiFetch({
|
|
104
|
+
baseUrl: params.baseUrl,
|
|
105
|
+
endpoint: 'ilink/bot/getupdates',
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
get_updates_buf: params.get_updates_buf ?? '',
|
|
108
|
+
base_info: { channel_version: '1.0.0' },
|
|
109
|
+
}),
|
|
110
|
+
token: params.token,
|
|
111
|
+
timeoutMs: timeout,
|
|
112
|
+
label: 'getUpdates',
|
|
113
|
+
});
|
|
114
|
+
return JSON.parse(rawText);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
// Long-poll timeout is normal; return empty response
|
|
118
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
119
|
+
return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Send a message
|
|
126
|
+
*/
|
|
127
|
+
export async function sendMessage(params) {
|
|
128
|
+
const rawText = await apiFetch({
|
|
129
|
+
baseUrl: params.baseUrl,
|
|
130
|
+
endpoint: 'ilink/bot/sendmessage',
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
...params.body,
|
|
133
|
+
base_info: { channel_version: '1.0.0' },
|
|
134
|
+
}),
|
|
135
|
+
token: params.token,
|
|
136
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
137
|
+
label: 'sendMessage',
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
JSON.parse(rawText);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
console.debug('[api] Non-JSON response:', e.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get bot config (includes typing_ticket)
|
|
148
|
+
*/
|
|
149
|
+
export async function getConfig(params) {
|
|
150
|
+
const rawText = await apiFetch({
|
|
151
|
+
baseUrl: params.baseUrl,
|
|
152
|
+
endpoint: 'ilink/bot/getconfig',
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
ilink_user_id: params.ilinkUserId,
|
|
155
|
+
context_token: params.contextToken,
|
|
156
|
+
base_info: { channel_version: '1.0.0' },
|
|
157
|
+
}),
|
|
158
|
+
token: params.token,
|
|
159
|
+
timeoutMs: params.timeoutMs ?? 10_000,
|
|
160
|
+
label: 'getConfig',
|
|
161
|
+
});
|
|
162
|
+
return JSON.parse(rawText);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Send typing indicator
|
|
166
|
+
*/
|
|
167
|
+
export async function sendTyping(params) {
|
|
168
|
+
await apiFetch({
|
|
169
|
+
baseUrl: params.baseUrl,
|
|
170
|
+
endpoint: 'ilink/bot/sendtyping',
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
...params.body,
|
|
173
|
+
base_info: { channel_version: '1.0.0' },
|
|
174
|
+
}),
|
|
175
|
+
token: params.token,
|
|
176
|
+
timeoutMs: params.timeoutMs ?? 10_000,
|
|
177
|
+
label: 'sendTyping',
|
|
178
|
+
});
|
|
179
|
+
}
|