@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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderLiveCard = renderLiveCard;
|
|
4
|
+
const base_1 = require("./base");
|
|
5
|
+
function renderLiveCard(state) {
|
|
6
|
+
const elems = [];
|
|
7
|
+
if (state.assistantBuf) {
|
|
8
|
+
elems.push((0, base_1.md)((0, base_1.limitTables)((0, base_1.truncate)(state.assistantBuf, 4500))));
|
|
9
|
+
}
|
|
10
|
+
if (state.currentTool) {
|
|
11
|
+
elems.push((0, base_1.hr)());
|
|
12
|
+
elems.push((0, base_1.md)(`🛠 **${state.currentTool.name}**\n\`\`\`\n${(0, base_1.truncate)(state.currentTool.input, 800)}\n\`\`\``));
|
|
13
|
+
}
|
|
14
|
+
if (state.toolResults > 0 && state.phase === 'streaming') {
|
|
15
|
+
elems.push((0, base_1.md)(`_已完成 ${state.toolResults} 次工具调用_`));
|
|
16
|
+
}
|
|
17
|
+
if (state.phase === 'error' && state.error) {
|
|
18
|
+
elems.push((0, base_1.hr)());
|
|
19
|
+
elems.push((0, base_1.md)(`⚠️ **错误**: ${(0, base_1.truncate)(state.error, 800)}`));
|
|
20
|
+
}
|
|
21
|
+
if (state.phase !== 'streaming' && state.usage) {
|
|
22
|
+
const proj = state.cwd ? (0, base_1.projectName)(state.cwd) : '';
|
|
23
|
+
elems.push((0, base_1.hr)());
|
|
24
|
+
elems.push((0, base_1.md)(`${proj ? `**${proj}** · ` : ''}tokens · in ${state.usage.inputTokens} · out ${state.usage.outputTokens} · cache-r ${state.usage.cacheReadTokens} · cache-c ${state.usage.cacheCreationTokens}${state.durationMs ? ` · ${(state.durationMs / 1000).toFixed(1)}s` : ''}`));
|
|
25
|
+
}
|
|
26
|
+
if (state.phase === 'done') {
|
|
27
|
+
if (state.stateless) {
|
|
28
|
+
elems.push((0, base_1.btnRow)([
|
|
29
|
+
(0, base_1.cmdBtn)('📂 项目', 'project', ''),
|
|
30
|
+
(0, base_1.cmdBtn)('📋 会话', 'session', 'list'),
|
|
31
|
+
(0, base_1.cmdBtn)('🟢 当前', 'session', 'current'),
|
|
32
|
+
]));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
elems.push((0, base_1.btnRow)([
|
|
36
|
+
(0, base_1.cmdBtn)('📋 查看会话', 'session', 'list'),
|
|
37
|
+
(0, base_1.cmdBtn)('🛑 停止会话', 'session', `stop ${state.threadKey}`, 'danger'),
|
|
38
|
+
]));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (state.phase === 'streaming') {
|
|
42
|
+
elems.push((0, base_1.btnRow)([
|
|
43
|
+
(0, base_1.cmdBtn)('⏹ 中断', 'stop', state.threadKey, 'danger'),
|
|
44
|
+
]));
|
|
45
|
+
}
|
|
46
|
+
return (0, base_1.card)((0, base_1.cardHeader)(titleFor(state), colorFor(state)), elems);
|
|
47
|
+
}
|
|
48
|
+
function titleFor(state) {
|
|
49
|
+
const proj = state.cwd ? (0, base_1.projectName)(state.cwd) : '';
|
|
50
|
+
const suffix = proj ? ` · ${proj}` : '';
|
|
51
|
+
switch (state.phase) {
|
|
52
|
+
case 'done': return `✅ 完成${suffix}`;
|
|
53
|
+
case 'error': return `⚠️ 出错${suffix}`;
|
|
54
|
+
case 'interrupted': return `🛑 已中断${suffix}`;
|
|
55
|
+
default: return `💬 Claude 思考中…${suffix}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function colorFor(state) {
|
|
59
|
+
switch (state.phase) {
|
|
60
|
+
case 'done': return 'green';
|
|
61
|
+
case 'error': return 'red';
|
|
62
|
+
case 'interrupted': return 'orange';
|
|
63
|
+
default: return 'blue';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderSessionListCard = renderSessionListCard;
|
|
4
|
+
const base_1 = require("./base");
|
|
5
|
+
function renderSessionListCard(pool, meta, userKey) {
|
|
6
|
+
const scoped = pool.listByScope(meta.chatId, meta.senderId);
|
|
7
|
+
const activeKey = pool.getActive(userKey)?.threadKey;
|
|
8
|
+
if (scoped.length === 0) {
|
|
9
|
+
return (0, base_1.card)((0, base_1.cardHeader)('📋 会话列表', 'blue'), [
|
|
10
|
+
(0, base_1.md)('**当前没有任何会话**\n\n💡 启动新会话后可以直接发送消息与 Claude Code 交互'),
|
|
11
|
+
(0, base_1.hr)(),
|
|
12
|
+
(0, base_1.btnRow)([
|
|
13
|
+
(0, base_1.toastBtn)('➕ 启动会话', '请发送:/session start 或 /session start @项目别名', 'primary'),
|
|
14
|
+
]),
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
const elements = [];
|
|
18
|
+
elements.push((0, base_1.md)(`共 **${scoped.length}** 个会话 · ▸ 标记为当前活跃`));
|
|
19
|
+
elements.push((0, base_1.hr)());
|
|
20
|
+
for (let i = 0; i < scoped.length; i++) {
|
|
21
|
+
const s = scoped[i];
|
|
22
|
+
const slot = s.threadKey.split(':').slice(2).join(':') || 'default';
|
|
23
|
+
const isActive = s.threadKey === activeKey;
|
|
24
|
+
const marker = isActive ? '▸ ' : ' ';
|
|
25
|
+
const statusIcon = s.active ? '🟢' : '⚪';
|
|
26
|
+
const sid = s.sessionId ? s.sessionId.slice(0, 8) : '-';
|
|
27
|
+
const elapsed = timeSince(s.lastUsed);
|
|
28
|
+
elements.push((0, base_1.md)(`${marker}**${i + 1}. ${statusIcon}** \`${slot}\`\n` +
|
|
29
|
+
`📁 \`${s.cwd}\` · sid \`${sid}\` · ⏱ ${elapsed}`));
|
|
30
|
+
const btns = [];
|
|
31
|
+
if (!isActive) {
|
|
32
|
+
btns.push((0, base_1.cmdBtnRefresh)('▶ 激活', 'session', `switch ${slot}`, 'session_list', 'primary'));
|
|
33
|
+
}
|
|
34
|
+
btns.push((0, base_1.cmdBtnRefresh)('⛔ 关闭', 'session', `stop ${slot}`, 'session_list', 'danger'));
|
|
35
|
+
elements.push((0, base_1.btnRow)(btns));
|
|
36
|
+
elements.push((0, base_1.hr)());
|
|
37
|
+
}
|
|
38
|
+
elements.push((0, base_1.btnRow)([
|
|
39
|
+
(0, base_1.toastBtn)('➕ 新建会话', '请发送:/session start @项目别名', 'primary'),
|
|
40
|
+
(0, base_1.cmdBtn)('📊 状态', 'status', ''),
|
|
41
|
+
]));
|
|
42
|
+
return (0, base_1.card)((0, base_1.cardHeader)('📋 会话列表', 'blue'), elements);
|
|
43
|
+
}
|
|
44
|
+
function timeSince(d) {
|
|
45
|
+
if (!d)
|
|
46
|
+
return '-';
|
|
47
|
+
const ms = Date.now() - d.getTime();
|
|
48
|
+
const sec = Math.floor(ms / 1000);
|
|
49
|
+
if (sec < 60)
|
|
50
|
+
return `${sec}秒`;
|
|
51
|
+
const min = Math.floor(sec / 60);
|
|
52
|
+
if (min < 60)
|
|
53
|
+
return `${min}分钟`;
|
|
54
|
+
const hrs = Math.floor(min / 60);
|
|
55
|
+
const remMin = min % 60;
|
|
56
|
+
if (remMin > 0)
|
|
57
|
+
return `${hrs}小时${remMin}分钟`;
|
|
58
|
+
return `${hrs}小时`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderStatusCard = renderStatusCard;
|
|
4
|
+
const base_1 = require("./base");
|
|
5
|
+
function formatUptime(seconds) {
|
|
6
|
+
const d = Math.floor(seconds / 86400);
|
|
7
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
8
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
9
|
+
const s = Math.floor(seconds % 60);
|
|
10
|
+
const parts = [];
|
|
11
|
+
if (d > 0)
|
|
12
|
+
parts.push(`${d}天`);
|
|
13
|
+
if (h > 0)
|
|
14
|
+
parts.push(`${h}时`);
|
|
15
|
+
if (m > 0)
|
|
16
|
+
parts.push(`${m}分`);
|
|
17
|
+
parts.push(`${s}秒`);
|
|
18
|
+
return parts.join('');
|
|
19
|
+
}
|
|
20
|
+
function renderStatusCard(cfg, pool, configPath) {
|
|
21
|
+
const sessions = pool.list();
|
|
22
|
+
const activeCount = sessions.filter((s) => s.active).length;
|
|
23
|
+
const sysLines = [];
|
|
24
|
+
sysLines.push(`**cc-feishu-plugin** v1.0.0`);
|
|
25
|
+
sysLines.push(`Node \`${process.version}\``);
|
|
26
|
+
sysLines.push(`进程运行: \`${formatUptime(Math.floor(process.uptime()))}\``);
|
|
27
|
+
if (configPath)
|
|
28
|
+
sysLines.push(`配置: \`${configPath}\``);
|
|
29
|
+
sysLines.push(`默认目录: \`${cfg.defaultCwd}\``);
|
|
30
|
+
let sessionMd = '*(无活跃会话)*';
|
|
31
|
+
if (sessions.length > 0) {
|
|
32
|
+
const lines = sessions.map((s) => {
|
|
33
|
+
const marker = s.active ? '🟢' : '⚪';
|
|
34
|
+
const sid = s.sessionId ? s.sessionId.slice(0, 8) : '-';
|
|
35
|
+
return `${marker} \`${s.threadKey}\` · sid \`${sid}\` · cwd \`${s.cwd}\``;
|
|
36
|
+
});
|
|
37
|
+
sessionMd = lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
const dangerStatus = cfg.claudeDangerMode
|
|
40
|
+
? '⚠️ Danger 模式:**开启**'
|
|
41
|
+
: '🔒 Danger 模式:**关闭**';
|
|
42
|
+
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
|
43
|
+
return (0, base_1.card)((0, base_1.cardHeader)('📊 系统状态', 'indigo'), [
|
|
44
|
+
(0, base_1.md)(sysLines.join('\n')),
|
|
45
|
+
(0, base_1.hr)(),
|
|
46
|
+
(0, base_1.md)(`**🔄 会话 (${activeCount} 活跃 / ${sessions.length} 总)**\n${sessionMd}`),
|
|
47
|
+
(0, base_1.hr)(),
|
|
48
|
+
(0, base_1.md)(dangerStatus),
|
|
49
|
+
(0, base_1.hr)(),
|
|
50
|
+
(0, base_1.btnRow)([
|
|
51
|
+
(0, base_1.cmdBtnRefresh)('🔄 刷新', 'status', '', 'status', 'primary'),
|
|
52
|
+
(0, base_1.cmdBtn)('📋 会话列表', 'session', 'list'),
|
|
53
|
+
]),
|
|
54
|
+
(0, base_1.btnRow)([
|
|
55
|
+
(0, base_1.cmdBtn)('❓ 帮助', 'help', ''),
|
|
56
|
+
(0, base_1.cmdBtn)('📂 项目', 'project', ''),
|
|
57
|
+
]),
|
|
58
|
+
(0, base_1.md)(`*⏱️ ${now}*`),
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
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.createFeishuClient = createFeishuClient;
|
|
37
|
+
exports.createWsClient = createWsClient;
|
|
38
|
+
exports.startFeishuBot = startFeishuBot;
|
|
39
|
+
// Suppress noisy SDK Chinese log output (causes mojibake on Windows GBK terminals)
|
|
40
|
+
// Must run BEFORE SDK import, as SDK may log during module initialization
|
|
41
|
+
const _origWarn = console.warn.bind(console);
|
|
42
|
+
const _origLog = console.log.bind(console);
|
|
43
|
+
const _origError = console.error.bind(console);
|
|
44
|
+
const hasNonAscii = (msg) => /[^\x00-\x7F]/.test(msg);
|
|
45
|
+
console.warn = (...args) => {
|
|
46
|
+
const msg = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
|
|
47
|
+
if (hasNonAscii(msg) || msg.includes('persistent connection'))
|
|
48
|
+
return;
|
|
49
|
+
_origWarn(...args);
|
|
50
|
+
};
|
|
51
|
+
console.log = (...args) => {
|
|
52
|
+
const msg = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
|
|
53
|
+
if (hasNonAscii(msg))
|
|
54
|
+
return;
|
|
55
|
+
_origLog(...args);
|
|
56
|
+
};
|
|
57
|
+
console.error = (...args) => {
|
|
58
|
+
const msg = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
|
|
59
|
+
if (hasNonAscii(msg))
|
|
60
|
+
return;
|
|
61
|
+
_origError(...args);
|
|
62
|
+
};
|
|
63
|
+
const lark = __importStar(require("@larksuiteoapi/node-sdk"));
|
|
64
|
+
const https = __importStar(require("https"));
|
|
65
|
+
const http = __importStar(require("http"));
|
|
66
|
+
// 全局 Keep-Alive agent
|
|
67
|
+
try {
|
|
68
|
+
https.globalAgent = new https.Agent({ keepAlive: true, maxSockets: 20, keepAliveMsecs: 30000 });
|
|
69
|
+
http.globalAgent = new http.Agent({ keepAlive: true, maxSockets: 20, keepAliveMsecs: 30000 });
|
|
70
|
+
}
|
|
71
|
+
catch { /* read-only in some environments */ }
|
|
72
|
+
let _onStatus = (_text) => { };
|
|
73
|
+
function createFeishuClient(cfg) {
|
|
74
|
+
return new lark.Client({
|
|
75
|
+
appId: cfg.appId,
|
|
76
|
+
appSecret: cfg.appSecret,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function createWsClient(cfg) {
|
|
80
|
+
return new lark.WSClient({
|
|
81
|
+
appId: cfg.appId,
|
|
82
|
+
appSecret: cfg.appSecret,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function extractMeta(data) {
|
|
86
|
+
const msg = data.message;
|
|
87
|
+
if (!msg?.message_id || !msg.chat_id || msg.message_type !== 'text')
|
|
88
|
+
return undefined;
|
|
89
|
+
return {
|
|
90
|
+
messageId: msg.message_id,
|
|
91
|
+
chatId: msg.chat_id,
|
|
92
|
+
senderId: data.sender?.sender_id?.open_id ?? '',
|
|
93
|
+
mentionBot: Array.isArray(msg.mentions) && msg.mentions.length > 0,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function extractText(data, meta) {
|
|
97
|
+
const msg = data.message;
|
|
98
|
+
let raw = msg?.content ?? '';
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
let text = parsed.text?.trim() ?? '';
|
|
107
|
+
if (meta.mentionBot && Array.isArray(msg?.mentions)) {
|
|
108
|
+
for (const m of msg.mentions) {
|
|
109
|
+
if (m.key)
|
|
110
|
+
text = text.split(m.key).join('');
|
|
111
|
+
}
|
|
112
|
+
text = text.trim();
|
|
113
|
+
}
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
function startFeishuBot(client, ws, handlers) {
|
|
117
|
+
if (handlers.onStatus)
|
|
118
|
+
_onStatus = handlers.onStatus;
|
|
119
|
+
_onStatus('\r\n飞书机器人启动中...\r\n');
|
|
120
|
+
const registeredHandlers = {
|
|
121
|
+
'im.message.receive_v1': (raw) => {
|
|
122
|
+
// Use setImmediate to avoid blocking the Feishu WS thread
|
|
123
|
+
setImmediate(() => {
|
|
124
|
+
try {
|
|
125
|
+
const data = raw;
|
|
126
|
+
const msg = data.message;
|
|
127
|
+
if (msg?.message_type && msg.message_type !== 'text') {
|
|
128
|
+
_onStatus(`\r\n[飞书] 收到非文本消息: ${msg.message_type}\r\n`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const meta = extractMeta(data);
|
|
132
|
+
if (!meta)
|
|
133
|
+
return;
|
|
134
|
+
const text = extractText(data, meta);
|
|
135
|
+
if (!text)
|
|
136
|
+
return;
|
|
137
|
+
_onStatus(`\r\n[飞书] 收到: ${text} | chatId: ${meta.chatId}\r\n`);
|
|
138
|
+
handlers.onMessage(text, meta);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error('[飞书] WS 消息处理异常:', err);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
if (handlers.cardAction) {
|
|
147
|
+
registeredHandlers['card.action.trigger'] = handlers.cardAction;
|
|
148
|
+
}
|
|
149
|
+
const eventDispatcher = new lark.EventDispatcher({}).register(registeredHandlers);
|
|
150
|
+
let reconnectTimer = null;
|
|
151
|
+
let isRunning = true;
|
|
152
|
+
function startWithRetry() {
|
|
153
|
+
if (!isRunning)
|
|
154
|
+
return;
|
|
155
|
+
ws.start({ eventDispatcher }).then(() => {
|
|
156
|
+
_onStatus(`\r\n✅ 飞书机器人已连接\r\n`);
|
|
157
|
+
}).catch((err) => {
|
|
158
|
+
_onStatus(`\r\n⚠️ 飞书连接断开: ${err},3秒后重连...\r\n`);
|
|
159
|
+
if (handlers.onDisconnect)
|
|
160
|
+
handlers.onDisconnect();
|
|
161
|
+
reconnectTimer = setTimeout(startWithRetry, 3000);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
startWithRetry();
|
|
165
|
+
return {
|
|
166
|
+
stop() {
|
|
167
|
+
isRunning = false;
|
|
168
|
+
if (reconnectTimer) {
|
|
169
|
+
clearTimeout(reconnectTimer);
|
|
170
|
+
reconnectTimer = null;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Replier = void 0;
|
|
4
|
+
class Replier {
|
|
5
|
+
constructor(client, logger) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
this.logger = logger ?? ((_msg) => { });
|
|
8
|
+
}
|
|
9
|
+
async replyText(rootMessageId, text) {
|
|
10
|
+
try {
|
|
11
|
+
const resp = await this.client.im.v1.message.reply({
|
|
12
|
+
path: { message_id: rootMessageId },
|
|
13
|
+
data: { msg_type: 'text', content: JSON.stringify({ text }) },
|
|
14
|
+
});
|
|
15
|
+
return resp.data?.message_id;
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19
|
+
this.logger(`[飞书] 回复文本失败: ${msg}`);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async createReaction(messageId, emoji) {
|
|
24
|
+
try {
|
|
25
|
+
this.logger(`[飞书→] createReaction(msgId=${messageId}, emoji=${emoji})`);
|
|
26
|
+
await this.client.im.v1.messageReaction.create({
|
|
27
|
+
path: { message_id: messageId },
|
|
28
|
+
data: { reaction_type: { emoji_type: emoji } },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
+
this.logger(`[飞书→] createReaction 失败: ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async sendText(chatId, text, retries = 2) {
|
|
37
|
+
return this._sendPost('chat_id', chatId, text, retries);
|
|
38
|
+
}
|
|
39
|
+
async sendTextToUser(openId, text, retries = 2) {
|
|
40
|
+
return this._sendPost('open_id', openId, text, retries);
|
|
41
|
+
}
|
|
42
|
+
async _sendPost(receiveIdType, receiveId, text, retries) {
|
|
43
|
+
return this._sendWithRetry(async () => {
|
|
44
|
+
this.logger(`[飞书→] _sendPost(${receiveIdType}=${receiveId}): ${text}`);
|
|
45
|
+
try {
|
|
46
|
+
const content = [[{ tag: 'md', text: text }]];
|
|
47
|
+
const resp = await this.client.im.v1.message.create({
|
|
48
|
+
params: { receive_id_type: receiveIdType },
|
|
49
|
+
data: {
|
|
50
|
+
receive_id: receiveId,
|
|
51
|
+
msg_type: 'post',
|
|
52
|
+
content: JSON.stringify({ zh_cn: { content } }),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
return resp.data?.message_id;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
59
|
+
this.logger(`[飞书→] 发送异常: ${msg}`);
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}, retries, `sendPost(${receiveId})`);
|
|
63
|
+
}
|
|
64
|
+
async sendPost(chatId, title, paragraphs, retries = 2) {
|
|
65
|
+
return this._sendWithRetry(async () => {
|
|
66
|
+
this.logger(`[飞书→] sendPost(${chatId}): ${title}`);
|
|
67
|
+
const content = paragraphs.map(p => [{ tag: 'text', text: p }]);
|
|
68
|
+
const resp = await this.client.im.v1.message.create({
|
|
69
|
+
params: { receive_id_type: 'chat_id' },
|
|
70
|
+
data: {
|
|
71
|
+
receive_id: chatId,
|
|
72
|
+
msg_type: 'post',
|
|
73
|
+
content: JSON.stringify({
|
|
74
|
+
post: {
|
|
75
|
+
zh_cn: { title, content },
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
return resp.data?.message_id;
|
|
81
|
+
}, retries, `sendPost(${chatId})`);
|
|
82
|
+
}
|
|
83
|
+
async sendCard(chatId, card, retries = 2) {
|
|
84
|
+
return this._sendWithRetry(async () => {
|
|
85
|
+
this.logger(`[飞书→] sendCard(${chatId}): ${JSON.stringify(card).slice(0, 200)}`);
|
|
86
|
+
const resp = await this.client.im.v1.message.create({
|
|
87
|
+
params: { receive_id_type: 'chat_id' },
|
|
88
|
+
data: {
|
|
89
|
+
receive_id: chatId,
|
|
90
|
+
msg_type: 'interactive',
|
|
91
|
+
content: JSON.stringify(card),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return resp.data?.message_id;
|
|
95
|
+
}, retries, `sendCard(${chatId})`);
|
|
96
|
+
}
|
|
97
|
+
async replyCard(rootMessageId, card, retries = 2) {
|
|
98
|
+
return this._sendWithRetry(async () => {
|
|
99
|
+
this.logger(`[飞书→] replyCard(${rootMessageId}): ${JSON.stringify(card).slice(0, 200)}`);
|
|
100
|
+
const resp = await this.client.im.v1.message.reply({
|
|
101
|
+
path: { message_id: rootMessageId },
|
|
102
|
+
data: {
|
|
103
|
+
msg_type: 'interactive',
|
|
104
|
+
content: JSON.stringify(card),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
return resp.data?.message_id;
|
|
108
|
+
}, retries, `replyCard(${rootMessageId})`);
|
|
109
|
+
}
|
|
110
|
+
async patchCard(messageId, card, retries = 2) {
|
|
111
|
+
return (await this._sendWithRetry(async () => {
|
|
112
|
+
this.logger(`[飞书→] patchCard(${messageId})`);
|
|
113
|
+
await this.client.im.v1.message.patch({
|
|
114
|
+
path: { message_id: messageId },
|
|
115
|
+
data: { content: JSON.stringify(card) },
|
|
116
|
+
});
|
|
117
|
+
return true;
|
|
118
|
+
}, retries, `patchCard(${messageId})`)) ?? false;
|
|
119
|
+
}
|
|
120
|
+
async _sendWithRetry(fn, retries, label) {
|
|
121
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
122
|
+
try {
|
|
123
|
+
return await fn();
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
127
|
+
const isTransient = msg.includes('ECONN') || msg.includes('EOF') || msg.includes('timeout') || msg.includes('RESET');
|
|
128
|
+
if (attempt < retries && isTransient) {
|
|
129
|
+
this.logger(`[飞书↻] ${label} 重试 ${attempt + 1}/${retries}`);
|
|
130
|
+
await delay(300 * 2 ** attempt);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
this.logger(`[飞书] ${label} 失败: ${msg}`);
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.Replier = Replier;
|
|
141
|
+
function delay(ms) {
|
|
142
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
143
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Router = void 0;
|
|
4
|
+
const base_1 = require("./cards/base");
|
|
5
|
+
class Router {
|
|
6
|
+
constructor(replier) {
|
|
7
|
+
this.cmds = new Map();
|
|
8
|
+
this.ordered = [];
|
|
9
|
+
this.replier = replier;
|
|
10
|
+
}
|
|
11
|
+
register(name, fn, aliases = []) {
|
|
12
|
+
const entry = { name, fn };
|
|
13
|
+
this.cmds.set(name, entry);
|
|
14
|
+
for (const a of aliases)
|
|
15
|
+
this.cmds.set(a, entry);
|
|
16
|
+
this.ordered.push(entry);
|
|
17
|
+
}
|
|
18
|
+
async dispatch(text, meta) {
|
|
19
|
+
const trimmed = text.trim();
|
|
20
|
+
if (!trimmed)
|
|
21
|
+
return;
|
|
22
|
+
if (trimmed.startsWith('/')) {
|
|
23
|
+
const rest = trimmed.slice(1);
|
|
24
|
+
const spaceIdx = rest.indexOf(' ');
|
|
25
|
+
const name = (spaceIdx < 0 ? rest : rest.slice(0, spaceIdx)).toLowerCase();
|
|
26
|
+
const args = spaceIdx < 0 ? '' : rest.slice(spaceIdx + 1);
|
|
27
|
+
const cmd = this.cmds.get(name);
|
|
28
|
+
if (!cmd) {
|
|
29
|
+
await this.replyAsCard(meta.messageId, `未知命令: /${name}\n输入 /help 查看可用命令`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const result = await cmd.fn(args, meta, { replier: this.replier, commands: this.cmds });
|
|
33
|
+
if (result)
|
|
34
|
+
await this.replyAsCard(meta.messageId, result);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const sendCmd = this.cmds.get('s');
|
|
38
|
+
if (sendCmd) {
|
|
39
|
+
const result = await sendCmd.fn(trimmed, meta, { replier: this.replier, commands: this.cmds });
|
|
40
|
+
if (result) {
|
|
41
|
+
if (result.includes('没有活跃的会话')) {
|
|
42
|
+
const ask = this.cmds.get('ask');
|
|
43
|
+
if (ask) {
|
|
44
|
+
const r = await ask.fn(trimmed, meta, { replier: this.replier, commands: this.cmds });
|
|
45
|
+
if (r)
|
|
46
|
+
await this.replyAsCard(meta.messageId, r);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await this.replyAsCard(meta.messageId, result);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async replyAsCard(messageId, text) {
|
|
55
|
+
const ok = await this.replier.replyCard(messageId, (0, base_1.textCard)(text));
|
|
56
|
+
if (!ok) {
|
|
57
|
+
await this.replier.replyText(messageId, text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.Router = Router;
|