@kafca/agentdock 0.1.7
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 +70 -0
- package/bin/agentdock.mjs +275 -0
- package/build/icon.png +0 -0
- package/dist/renderer/assets/index-BaNpzC6d.js +482 -0
- package/dist/renderer/assets/index-CLWJyO0E.css +11 -0
- package/dist/renderer/favicon.svg +19 -0
- package/dist/renderer/index.html +14 -0
- package/dist/renderer/logo-options.svg +153 -0
- package/dist-electron/electron/knowledge-skill-script.test.js +125 -0
- package/dist-electron/electron/lac-cli.test.js +221 -0
- package/dist-electron/electron/local-core-refactor.test.js +1075 -0
- package/dist-electron/electron/main.js +163 -0
- package/dist-electron/electron/plugin-kernel.test.js +534 -0
- package/dist-electron/electron/preload.js +2 -0
- package/dist-electron/package.json +3 -0
- package/dist-electron/packages/contracts/src/index.js +18 -0
- package/dist-electron/packages/contracts/src/local-core.js +2 -0
- package/dist-electron/packages/core-sdk/src/index.js +406 -0
- package/dist-electron/packages/knowledge-api/src/ai-vector-provider.js +388 -0
- package/dist-electron/packages/knowledge-api/src/index.js +146 -0
- package/dist-electron/packages/knowledge-api/src/sqlite-store.js +467 -0
- package/dist-electron/packages/knowledge-api/src/thread-knowledge-store.js +21 -0
- package/dist-electron/packages/knowledge-api/test/ai-vector-provider.test.js +412 -0
- package/dist-electron/packages/plugin-sdk/src/index.js +2 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +369 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-response-processor.js +110 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +171 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +635 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-transport.js +190 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +244 -0
- package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +52 -0
- package/dist-electron/services/local-ai-core/src/cli/lac.js +290 -0
- package/dist-electron/services/local-ai-core/src/gateway/local-core-lark-gateway.js +976 -0
- package/dist-electron/services/local-ai-core/src/gateway/local-core-weixin-gateway.js +1143 -0
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +291 -0
- package/dist-electron/services/local-ai-core/src/kernel/capability-registry.js +67 -0
- package/dist-electron/services/local-ai-core/src/kernel/diagnostics.js +27 -0
- package/dist-electron/services/local-ai-core/src/kernel/event-bus.js +27 -0
- package/dist-electron/services/local-ai-core/src/kernel/lifecycle-manager.js +85 -0
- package/dist-electron/services/local-ai-core/src/kernel/plugin-registry.js +60 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js +114 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-lark-plugin.js +59 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +56 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/knowledge-ai-vector-plugin.js +7 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/knowledge-noop-plugin.js +7 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/runtime-capabilities-plugin.js +62 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/scheduler-cron-plugin.js +49 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/scheduler-lark-plugin.js +38 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/scheduler-weixin-plugin.js +38 -0
- package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +230 -0
- package/dist-electron/services/local-ai-core/src/router/workspace-router-types.js +2 -0
- package/dist-electron/services/local-ai-core/src/router/workspace-router.js +389 -0
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +315 -0
- package/dist-electron/services/local-ai-core/src/runtime/local-core-runtime-state.js +282 -0
- package/dist-electron/services/local-ai-core/src/runtime/server.js +465 -0
- package/dist-electron/services/local-ai-core/src/runtime/standalone.js +38 -0
- package/dist-electron/services/local-ai-core/src/scheduler/adapters.js +2 -0
- package/dist-electron/services/local-ai-core/src/scheduler/cron-command-detector.js +70 -0
- package/dist-electron/services/local-ai-core/src/scheduler/cron.js +53 -0
- package/dist-electron/services/local-ai-core/src/scheduler/execution-policy.js +2 -0
- package/dist-electron/services/local-ai-core/src/scheduler/lark-execution-policies.js +66 -0
- package/dist-electron/services/local-ai-core/src/scheduler/lark-schedule-adapter.js +77 -0
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +46 -0
- package/dist-electron/services/local-ai-core/src/scheduler/scheduler-run-lifecycle.js +64 -0
- package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +133 -0
- package/dist-electron/services/local-ai-core/src/scheduler/weixin-execution-policies.js +66 -0
- package/dist-electron/services/local-ai-core/src/scheduler/weixin-schedule-adapter.js +78 -0
- package/dist-electron/services/local-ai-core/src/thread/workspace-thread-id.js +17 -0
- package/dist-electron/services/local-ai-core/src/thread/workspace-thread-mappers.js +43 -0
- package/dist-electron/shared/desktop.js +195 -0
- package/dist-electron/src/api/client.js +73 -0
- package/dist-electron/src/api/sessions.js +21 -0
- package/dist-electron/src/lib/session-utils.js +51 -0
- package/dist-electron/src/pages/Threads/thread-chat-model.js +246 -0
- package/dist-electron/src/pages/Threads/thread-chat-model.test.js +76 -0
- package/dist-electron/src/pages/Threads/thread-chat-permission.js +26 -0
- package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +102 -0
- package/dist-electron/src/pages/Threads/thread-chat-task-state.js +65 -0
- package/package.json +146 -0
|
@@ -0,0 +1,976 @@
|
|
|
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.LocalCoreLarkGateway = void 0;
|
|
37
|
+
const node_events_1 = require("node:events");
|
|
38
|
+
const node_crypto_1 = require("node:crypto");
|
|
39
|
+
const desktop_js_1 = require("../../../../shared/desktop.js");
|
|
40
|
+
const PAIRING_EXPIRY_MS = 10 * 60 * 1000;
|
|
41
|
+
class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
42
|
+
options;
|
|
43
|
+
// Lark card action callbacks are unreliable in current WS-only setup (code 200340).
|
|
44
|
+
// Keep permission approval on explicit text commands: allow all / allow / deny.
|
|
45
|
+
enableCardActions = false;
|
|
46
|
+
// Keep permission state in a dedicated card to avoid mixing order in the main reply card.
|
|
47
|
+
mirrorPermissionStateInMainCard = false;
|
|
48
|
+
runtime = new Map();
|
|
49
|
+
threadRouting = new Map();
|
|
50
|
+
outboundEventChains = new Map();
|
|
51
|
+
outboundTurns = new Map();
|
|
52
|
+
mutedThreadBridgeCounts = new Map();
|
|
53
|
+
larkModulePromise = null;
|
|
54
|
+
platform = 'lark';
|
|
55
|
+
routeType = 'channel.chat';
|
|
56
|
+
constructor(options) {
|
|
57
|
+
super();
|
|
58
|
+
this.options = options;
|
|
59
|
+
}
|
|
60
|
+
async refreshBindings() {
|
|
61
|
+
const config = await this.options.readConfig();
|
|
62
|
+
const bindings = this.collectBindings(config);
|
|
63
|
+
const nextWorkspaceIds = new Set(bindings.map((binding) => binding.workspaceId));
|
|
64
|
+
for (const workspaceId of [...this.runtime.keys()]) {
|
|
65
|
+
if (!nextWorkspaceIds.has(workspaceId)) {
|
|
66
|
+
await this.stopWorkspace(workspaceId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const binding of bindings) {
|
|
70
|
+
const current = this.runtime.get(binding.workspaceId);
|
|
71
|
+
if (!binding.enabled) {
|
|
72
|
+
if (current) {
|
|
73
|
+
await this.stopWorkspace(binding.workspaceId);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this.runtime.set(binding.workspaceId, {
|
|
77
|
+
workspaceId: binding.workspaceId,
|
|
78
|
+
enabled: false,
|
|
79
|
+
status: 'disabled',
|
|
80
|
+
connected: false,
|
|
81
|
+
appId: binding.appId,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (current?.status === 'running' && current.appId === binding.appId) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
await this.startWorkspace(binding);
|
|
90
|
+
}
|
|
91
|
+
this.notifyRuntimeStateChanged();
|
|
92
|
+
}
|
|
93
|
+
async testConnection(workspaceId) {
|
|
94
|
+
const binding = await this.getBinding(workspaceId);
|
|
95
|
+
const result = await this.createSdkClientResult(binding);
|
|
96
|
+
return {
|
|
97
|
+
...result,
|
|
98
|
+
platform: 'lark',
|
|
99
|
+
workspaceId,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async enable(workspaceId) {
|
|
103
|
+
const binding = await this.getBinding(workspaceId);
|
|
104
|
+
await this.startWorkspace(binding);
|
|
105
|
+
return this.getStatus(workspaceId);
|
|
106
|
+
}
|
|
107
|
+
async disable(workspaceId) {
|
|
108
|
+
await this.stopWorkspace(workspaceId);
|
|
109
|
+
return this.getStatus(workspaceId);
|
|
110
|
+
}
|
|
111
|
+
async sendScheduledCard(workspaceId, chatId, text) {
|
|
112
|
+
return this.sendImmediateCard(workspaceId, chatId, text);
|
|
113
|
+
}
|
|
114
|
+
async sendScheduledMessage(workspaceId, route, text) {
|
|
115
|
+
return this.sendScheduledCard(workspaceId, route.channelId, text);
|
|
116
|
+
}
|
|
117
|
+
muteThreadBridge(threadId) {
|
|
118
|
+
const current = this.mutedThreadBridgeCounts.get(threadId) || 0;
|
|
119
|
+
this.mutedThreadBridgeCounts.set(threadId, current + 1);
|
|
120
|
+
}
|
|
121
|
+
unmuteThreadBridge(threadId) {
|
|
122
|
+
const current = this.mutedThreadBridgeCounts.get(threadId) || 0;
|
|
123
|
+
if (current <= 1) {
|
|
124
|
+
this.mutedThreadBridgeCounts.delete(threadId);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.mutedThreadBridgeCounts.set(threadId, current - 1);
|
|
128
|
+
}
|
|
129
|
+
getStatus(workspaceId) {
|
|
130
|
+
this.options.store.expirePendingPairings();
|
|
131
|
+
const binding = this.runtime.get(workspaceId);
|
|
132
|
+
const pairings = this.options.store.listPendingPairings(workspaceId)
|
|
133
|
+
.filter((row) => row.platform === 'lark' && row.expires_at >= new Date().toISOString());
|
|
134
|
+
const users = this.options.store.listAuthorizedUsers(workspaceId, 'lark');
|
|
135
|
+
return {
|
|
136
|
+
workspaceId,
|
|
137
|
+
platform: 'lark',
|
|
138
|
+
enabled: Boolean(binding?.enabled),
|
|
139
|
+
connected: Boolean(binding?.connected),
|
|
140
|
+
status: binding?.status || 'disabled',
|
|
141
|
+
appId: binding?.appId || '',
|
|
142
|
+
lastError: binding?.lastError,
|
|
143
|
+
connectedAt: binding?.connectedAt,
|
|
144
|
+
pendingPairings: pairings.length,
|
|
145
|
+
authorizedUsers: users.length,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
listStatuses() {
|
|
149
|
+
return [...this.runtime.keys()].sort().map((workspaceId) => this.getStatus(workspaceId));
|
|
150
|
+
}
|
|
151
|
+
listPendingPairings(workspaceId) {
|
|
152
|
+
this.options.store.expirePendingPairings();
|
|
153
|
+
return this.options.store
|
|
154
|
+
.listPairingRequests(workspaceId, 'lark')
|
|
155
|
+
.filter((item) => item.status === 'pending' && item.expiresAt >= new Date().toISOString());
|
|
156
|
+
}
|
|
157
|
+
listAuthorizedUsers(workspaceId) {
|
|
158
|
+
return this.options.store.listAuthorizedUsers(workspaceId, 'lark');
|
|
159
|
+
}
|
|
160
|
+
async start() {
|
|
161
|
+
await this.refreshBindings();
|
|
162
|
+
}
|
|
163
|
+
async stop() {
|
|
164
|
+
this.close();
|
|
165
|
+
}
|
|
166
|
+
approvePairing(code) {
|
|
167
|
+
this.options.store.expirePendingPairings();
|
|
168
|
+
const pairing = this.options.store.getPairingRequest(code);
|
|
169
|
+
if (!pairing) {
|
|
170
|
+
throw new Error(`Pairing code not found: ${code}`);
|
|
171
|
+
}
|
|
172
|
+
if (pairing.platform !== 'lark') {
|
|
173
|
+
throw new Error(`Pairing code ${code} is not a Lark pairing`);
|
|
174
|
+
}
|
|
175
|
+
if (pairing.status !== 'pending') {
|
|
176
|
+
throw new Error(`Pairing code ${code} is already ${pairing.status}`);
|
|
177
|
+
}
|
|
178
|
+
if (pairing.expires_at < new Date().toISOString()) {
|
|
179
|
+
this.options.store.updatePairingStatus(code, 'expired');
|
|
180
|
+
throw new Error(`Pairing code ${code} has expired`);
|
|
181
|
+
}
|
|
182
|
+
const existing = this.options.store.getAuthorizedUser(pairing.workspace_id, pairing.platform_user_id);
|
|
183
|
+
const userId = existing?.id || `lark-user-${(0, node_crypto_1.randomUUID)()}`;
|
|
184
|
+
const authorizedAt = new Date().toISOString();
|
|
185
|
+
this.options.store.createAuthorizedUser({
|
|
186
|
+
id: userId,
|
|
187
|
+
workspace_id: pairing.workspace_id,
|
|
188
|
+
platform_user_id: pairing.platform_user_id,
|
|
189
|
+
chat_id: pairing.chat_id,
|
|
190
|
+
display_name: pairing.display_name,
|
|
191
|
+
thread_id: existing?.thread_id || null,
|
|
192
|
+
authorized_at: authorizedAt,
|
|
193
|
+
});
|
|
194
|
+
this.options.store.updatePairingStatus(code, 'approved');
|
|
195
|
+
this.notifyRuntimeStateChanged();
|
|
196
|
+
const user = this.options.store.listAuthorizedUsers(pairing.workspace_id, 'lark').find((entry) => entry.id === userId);
|
|
197
|
+
if (!user) {
|
|
198
|
+
throw new Error('Authorized user lookup failed after approval');
|
|
199
|
+
}
|
|
200
|
+
return user;
|
|
201
|
+
}
|
|
202
|
+
rejectPairing(code) {
|
|
203
|
+
const pairing = this.options.store.getPairingRequest(code);
|
|
204
|
+
if (!pairing) {
|
|
205
|
+
throw new Error(`Pairing code not found: ${code}`);
|
|
206
|
+
}
|
|
207
|
+
this.options.store.updatePairingStatus(code, 'rejected');
|
|
208
|
+
this.notifyRuntimeStateChanged();
|
|
209
|
+
return { rejected: true };
|
|
210
|
+
}
|
|
211
|
+
async onBridgeEvent(event) {
|
|
212
|
+
if (!event.sessionKey) {
|
|
213
|
+
this.options.log?.(`localcore-lark bridge event ignored without sessionKey: ${event.type}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const sessionKey = event.sessionKey;
|
|
217
|
+
const route = this.threadRouting.get(sessionKey);
|
|
218
|
+
if (!route) {
|
|
219
|
+
this.options.log?.(`localcore-lark bridge route miss for sessionKey=${sessionKey} type=${event.type}`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const state = this.runtime.get(route.workspaceId);
|
|
223
|
+
if (!state?.client || !state.connected) {
|
|
224
|
+
this.options.log?.(`localcore-lark bridge event ignored because workspace is not connected: ${route.workspaceId}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const initialBinding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId);
|
|
228
|
+
if (!initialBinding) {
|
|
229
|
+
this.options.log?.(`localcore-lark bridge binding miss for workspace=${route.workspaceId} chat=${route.chatId} user=${route.platformUserId}`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (event.type !== 'preview_start'
|
|
233
|
+
&& event.type !== 'update_message'
|
|
234
|
+
&& event.type !== 'reply'
|
|
235
|
+
&& event.type !== 'buttons'
|
|
236
|
+
&& event.type !== 'typing_start'
|
|
237
|
+
&& event.type !== 'typing_stop'
|
|
238
|
+
&& event.type !== 'status') {
|
|
239
|
+
this.options.log?.(`localcore-lark bridge event ignored type=${event.type}`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const previous = this.outboundEventChains.get(sessionKey) || Promise.resolve();
|
|
243
|
+
const current = previous
|
|
244
|
+
.catch(() => undefined)
|
|
245
|
+
.then(async () => {
|
|
246
|
+
const binding = this.options.store.getPlatformThreadBinding(route.workspaceId, route.chatId, route.platformUserId);
|
|
247
|
+
if (!binding) {
|
|
248
|
+
this.options.log?.(`localcore-lark bridge binding disappeared for sessionKey=${sessionKey}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (this.mutedThreadBridgeCounts.has(binding.thread_id)) {
|
|
252
|
+
this.options.log?.(`localcore-lark bridge muted for thread=${binding.thread_id} type=${event.type}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const turn = this.getOrCreateTurnState(sessionKey, binding.last_platform_message_id || undefined);
|
|
256
|
+
if (event.replyCtx) {
|
|
257
|
+
turn.replyCtx = event.replyCtx;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
if (event.type === 'typing_start') {
|
|
261
|
+
turn.messageId = undefined;
|
|
262
|
+
}
|
|
263
|
+
if (event.type === 'typing_start' && (turn.permissionMessageId || turn.awaitingPermission)) {
|
|
264
|
+
if (turn.permissionMessageId) {
|
|
265
|
+
await this.patchTextCard(state, turn.permissionMessageId, '**工具确认已处理**\n\n继续生成中...', [], sessionKey, binding.thread_id);
|
|
266
|
+
}
|
|
267
|
+
turn.permissionMessageId = undefined;
|
|
268
|
+
turn.awaitingPermission = false;
|
|
269
|
+
// Start a fresh assistant message after permission approval so the
|
|
270
|
+
// final answer appears after the confirmation flow in chat order.
|
|
271
|
+
turn.messageId = undefined;
|
|
272
|
+
this.options.store.clearPlatformThreadMessageId(route.workspaceId, route.chatId, route.platformUserId);
|
|
273
|
+
}
|
|
274
|
+
if (event.type === 'buttons') {
|
|
275
|
+
const permissionCard = this.renderPermissionCard(turn, event);
|
|
276
|
+
if (permissionCard.text || permissionCard.buttonRows.length > 0) {
|
|
277
|
+
if (!turn.permissionMessageId) {
|
|
278
|
+
const createdId = await this.sendTextAsCard(state, route.chatId, permissionCard.text, permissionCard.buttonRows, sessionKey, binding.thread_id);
|
|
279
|
+
if (createdId) {
|
|
280
|
+
turn.permissionMessageId = createdId;
|
|
281
|
+
this.options.log?.(`localcore-lark sent permission card ${createdId} for sessionKey=${sessionKey}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
await this.patchTextCard(state, turn.permissionMessageId, permissionCard.text, permissionCard.buttonRows, sessionKey, binding.thread_id);
|
|
286
|
+
this.options.log?.(`localcore-lark patched permission card ${turn.permissionMessageId} for sessionKey=${sessionKey}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.consumeBridgeEvent(turn, event);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.consumeBridgeEvent(turn, event);
|
|
293
|
+
const rendered = this.renderTurnCard(turn);
|
|
294
|
+
if (!rendered.text && rendered.buttonRows.length === 0) {
|
|
295
|
+
this.options.log?.(`localcore-lark bridge event produced empty render for sessionKey=${sessionKey} type=${event.type}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const shouldThrottle = event.type === 'update_message' &&
|
|
299
|
+
turn.messageId &&
|
|
300
|
+
Date.now() - turn.lastPatchedAt < 900;
|
|
301
|
+
this.options.log?.(`localcore-lark bridge event type=${event.type} sessionKey=${sessionKey} hasMessageId=${Boolean(turn.messageId)} throttle=${shouldThrottle}`);
|
|
302
|
+
if (shouldThrottle) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!turn.messageId) {
|
|
306
|
+
const createdId = await this.sendTextAsCard(state, route.chatId, rendered.text, rendered.buttonRows, sessionKey, binding.thread_id);
|
|
307
|
+
if (createdId) {
|
|
308
|
+
turn.messageId = createdId;
|
|
309
|
+
this.options.store.updatePlatformThreadMessageId(route.workspaceId, route.chatId, route.platformUserId, createdId);
|
|
310
|
+
this.options.log?.(`localcore-lark sent new card message ${createdId} for sessionKey=${sessionKey}`);
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
await this.patchTextCard(state, turn.messageId, rendered.text, rendered.buttonRows, sessionKey, binding.thread_id);
|
|
315
|
+
turn.lastPatchedAt = Date.now();
|
|
316
|
+
this.options.log?.(`localcore-lark patched card message ${turn.messageId} for sessionKey=${sessionKey}`);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
this.options.log?.(`localcore-lark bridge send failed for sessionKey=${sessionKey}: ${error instanceof Error ? error.message : String(error)}`);
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
.finally(() => {
|
|
323
|
+
if (this.outboundEventChains.get(sessionKey) === current) {
|
|
324
|
+
this.outboundEventChains.delete(sessionKey);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
this.outboundEventChains.set(sessionKey, current);
|
|
328
|
+
await current;
|
|
329
|
+
}
|
|
330
|
+
async handleInboundMessage(input) {
|
|
331
|
+
this.options.eventBus.emit({
|
|
332
|
+
type: 'platform.message.received',
|
|
333
|
+
payload: {
|
|
334
|
+
platform: this.platform,
|
|
335
|
+
workspaceId: input.workspaceId,
|
|
336
|
+
participantId: input.platformUserId,
|
|
337
|
+
channelId: input.chatId,
|
|
338
|
+
displayName: input.displayName,
|
|
339
|
+
text: input.text,
|
|
340
|
+
messageId: input.messageId,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
this.options.store.expirePendingPairings();
|
|
344
|
+
const binding = await this.getBinding(input.workspaceId);
|
|
345
|
+
let authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId);
|
|
346
|
+
if (!authorized) {
|
|
347
|
+
if (binding.autoApprove) {
|
|
348
|
+
const authorizedAt = new Date().toISOString();
|
|
349
|
+
this.options.store.createAuthorizedUser({
|
|
350
|
+
id: `lark-user-${(0, node_crypto_1.randomUUID)()}`,
|
|
351
|
+
workspace_id: input.workspaceId,
|
|
352
|
+
platform_user_id: input.platformUserId,
|
|
353
|
+
chat_id: input.chatId,
|
|
354
|
+
display_name: input.displayName,
|
|
355
|
+
thread_id: null,
|
|
356
|
+
authorized_at: authorizedAt,
|
|
357
|
+
});
|
|
358
|
+
authorized = this.options.store.getAuthorizedUser(input.workspaceId, input.platformUserId);
|
|
359
|
+
this.options.log?.(`localcore-lark auto-approved user for ${input.workspaceId}: ${input.platformUserId}`);
|
|
360
|
+
this.notifyRuntimeStateChanged();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!authorized) {
|
|
364
|
+
const existingPending = this.options.store.listPendingPairings(input.workspaceId).find((item) => item.platform === 'lark' && item.platform_user_id === input.platformUserId && item.chat_id === input.chatId && item.status === 'pending');
|
|
365
|
+
let pairingCode = existingPending?.code || '';
|
|
366
|
+
if (!existingPending) {
|
|
367
|
+
const now = new Date();
|
|
368
|
+
pairingCode = this.generatePairingCode();
|
|
369
|
+
this.options.store.createPairingRequest({
|
|
370
|
+
code: pairingCode,
|
|
371
|
+
workspace_id: input.workspaceId,
|
|
372
|
+
platform_user_id: input.platformUserId,
|
|
373
|
+
chat_id: input.chatId,
|
|
374
|
+
display_name: input.displayName,
|
|
375
|
+
requested_at: now.toISOString(),
|
|
376
|
+
expires_at: new Date(now.getTime() + PAIRING_EXPIRY_MS).toISOString(),
|
|
377
|
+
status: 'pending',
|
|
378
|
+
});
|
|
379
|
+
this.notifyRuntimeStateChanged();
|
|
380
|
+
}
|
|
381
|
+
await this.sendImmediateCard(input.workspaceId, input.chatId, this.renderPendingPairingCard(pairingCode));
|
|
382
|
+
return { paired: false };
|
|
383
|
+
}
|
|
384
|
+
const router = this.options.getWorkspaceRouter();
|
|
385
|
+
const threadBinding = this.options.store.getPlatformThreadBinding(input.workspaceId, input.chatId, input.platformUserId);
|
|
386
|
+
let threadId = threadBinding?.thread_id || authorized.thread_id || '';
|
|
387
|
+
if (!threadId) {
|
|
388
|
+
const thread = await router.createThread(input.workspaceId, input.displayName || `Lark ${input.chatId}`);
|
|
389
|
+
threadId = thread.id;
|
|
390
|
+
this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, threadId);
|
|
391
|
+
const now = new Date().toISOString();
|
|
392
|
+
this.options.store.upsertPlatformThreadBinding({
|
|
393
|
+
workspace_id: input.workspaceId,
|
|
394
|
+
chat_id: input.chatId,
|
|
395
|
+
platform_user_id: input.platformUserId,
|
|
396
|
+
thread_id: threadId,
|
|
397
|
+
last_platform_message_id: null,
|
|
398
|
+
created_at: now,
|
|
399
|
+
updated_at: now,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const sessionKey = this.options.getWorkspaceRouter().getThreadSessionKey(threadId);
|
|
403
|
+
const normalizedText = String(input.text || '').trim().toLowerCase();
|
|
404
|
+
const permissionThreadId = (normalizedText === 'allow' || normalizedText === 'allow all' || normalizedText === 'deny')
|
|
405
|
+
? this.findAwaitingPermissionThreadId(input.workspaceId, input.chatId, input.platformUserId)
|
|
406
|
+
: '';
|
|
407
|
+
if (permissionThreadId && permissionThreadId !== threadId) {
|
|
408
|
+
threadId = permissionThreadId;
|
|
409
|
+
}
|
|
410
|
+
const effectiveSessionKey = this.options.getWorkspaceRouter().getThreadSessionKey(threadId);
|
|
411
|
+
this.threadRouting.set(effectiveSessionKey, {
|
|
412
|
+
workspaceId: input.workspaceId,
|
|
413
|
+
platformUserId: input.platformUserId,
|
|
414
|
+
chatId: input.chatId,
|
|
415
|
+
threadId,
|
|
416
|
+
});
|
|
417
|
+
const acknowledgement = this.createTurnState(effectiveSessionKey, input.messageId);
|
|
418
|
+
await this.addAcknowledgementReaction(input.workspaceId, input.messageId, acknowledgement);
|
|
419
|
+
const slashCommand = this.parseSlashCommand(input.text);
|
|
420
|
+
if (slashCommand?.name === 'new') {
|
|
421
|
+
const title = slashCommand.args.join(' ').trim() || `${input.displayName || 'Lark'} ${new Date().toLocaleTimeString()}`;
|
|
422
|
+
const nextThread = await router.createThread(input.workspaceId, title);
|
|
423
|
+
const now = new Date().toISOString();
|
|
424
|
+
this.options.store.updateAuthorizedUserThread(input.workspaceId, input.platformUserId, nextThread.id);
|
|
425
|
+
this.options.store.upsertPlatformThreadBinding({
|
|
426
|
+
workspace_id: input.workspaceId,
|
|
427
|
+
chat_id: input.chatId,
|
|
428
|
+
platform_user_id: input.platformUserId,
|
|
429
|
+
thread_id: nextThread.id,
|
|
430
|
+
last_platform_message_id: null,
|
|
431
|
+
created_at: now,
|
|
432
|
+
updated_at: now,
|
|
433
|
+
});
|
|
434
|
+
this.threadRouting.set(this.options.getWorkspaceRouter().getThreadSessionKey(nextThread.id), {
|
|
435
|
+
workspaceId: input.workspaceId,
|
|
436
|
+
platformUserId: input.platformUserId,
|
|
437
|
+
chatId: input.chatId,
|
|
438
|
+
threadId: nextThread.id,
|
|
439
|
+
});
|
|
440
|
+
await this.sendImmediateCard(input.workspaceId, input.chatId, '**已开始新会话**');
|
|
441
|
+
return { paired: true, threadId: nextThread.id };
|
|
442
|
+
}
|
|
443
|
+
const latestRun = this.options.store.getLatestRunForThread(threadId);
|
|
444
|
+
if ((normalizedText === 'allow' || normalizedText === 'allow all' || normalizedText === 'deny')
|
|
445
|
+
&& latestRun?.status === 'awaiting_input') {
|
|
446
|
+
await router.sendThreadAction(threadId, input.text);
|
|
447
|
+
return { paired: true, threadId };
|
|
448
|
+
}
|
|
449
|
+
this.options.store.clearPlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId);
|
|
450
|
+
await router.sendThreadMessage(threadId, (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text));
|
|
451
|
+
return { paired: true, threadId };
|
|
452
|
+
}
|
|
453
|
+
close() {
|
|
454
|
+
return Promise.all([...this.runtime.keys()].map((workspaceId) => this.stopWorkspace(workspaceId))).then(() => undefined);
|
|
455
|
+
}
|
|
456
|
+
async getBinding(workspaceId) {
|
|
457
|
+
const config = await this.options.readConfig();
|
|
458
|
+
const binding = this.collectBindings(config).find((entry) => entry.workspaceId === workspaceId);
|
|
459
|
+
if (!binding) {
|
|
460
|
+
throw new Error(`No Lark binding configured for workspace "${workspaceId}"`);
|
|
461
|
+
}
|
|
462
|
+
return binding;
|
|
463
|
+
}
|
|
464
|
+
async createSdkClientResult(binding) {
|
|
465
|
+
try {
|
|
466
|
+
const mod = await this.getLarkModule();
|
|
467
|
+
const client = new mod.Client({
|
|
468
|
+
appId: binding.appId,
|
|
469
|
+
appSecret: binding.appSecret,
|
|
470
|
+
appType: mod.AppType.SelfBuild,
|
|
471
|
+
domain: mod.Domain.Feishu,
|
|
472
|
+
});
|
|
473
|
+
await client.auth.v3.appAccessToken.internal({ data: { app_id: binding.appId, app_secret: binding.appSecret } });
|
|
474
|
+
return {
|
|
475
|
+
success: true,
|
|
476
|
+
platform: 'lark',
|
|
477
|
+
workspaceId: binding.workspaceId,
|
|
478
|
+
appId: binding.appId,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
platform: 'lark',
|
|
485
|
+
workspaceId: binding.workspaceId,
|
|
486
|
+
appId: binding.appId,
|
|
487
|
+
error: error instanceof Error ? error.message : String(error),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async startWorkspace(binding) {
|
|
492
|
+
await this.stopWorkspace(binding.workspaceId);
|
|
493
|
+
const status = {
|
|
494
|
+
workspaceId: binding.workspaceId,
|
|
495
|
+
enabled: true,
|
|
496
|
+
status: 'starting',
|
|
497
|
+
connected: false,
|
|
498
|
+
appId: binding.appId,
|
|
499
|
+
};
|
|
500
|
+
this.runtime.set(binding.workspaceId, status);
|
|
501
|
+
this.notifyRuntimeStateChanged();
|
|
502
|
+
try {
|
|
503
|
+
const mod = await this.getLarkModule();
|
|
504
|
+
status.client = new mod.Client({
|
|
505
|
+
appId: binding.appId,
|
|
506
|
+
appSecret: binding.appSecret,
|
|
507
|
+
appType: mod.AppType.SelfBuild,
|
|
508
|
+
domain: mod.Domain.Feishu,
|
|
509
|
+
});
|
|
510
|
+
status.eventDispatcher = new mod.EventDispatcher({
|
|
511
|
+
encryptKey: binding.encryptKey || '',
|
|
512
|
+
verificationToken: binding.verificationToken || '',
|
|
513
|
+
loggerLevel: mod.LoggerLevel.info,
|
|
514
|
+
});
|
|
515
|
+
status.eventDispatcher.register({
|
|
516
|
+
'im.message.receive_v1': async (data) => {
|
|
517
|
+
this.options.log?.(`localcore-lark received im.message.receive_v1 for ${binding.workspaceId}`);
|
|
518
|
+
await this.handleMessageEvent(binding.workspaceId, data);
|
|
519
|
+
},
|
|
520
|
+
'card.action.trigger': async (data) => {
|
|
521
|
+
this.options.log?.(`localcore-lark received card.action.trigger for ${binding.workspaceId}`);
|
|
522
|
+
void this.handleCardActionEvent(binding.workspaceId, data);
|
|
523
|
+
return {};
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
status.wsClient = new mod.WSClient({
|
|
527
|
+
appId: binding.appId,
|
|
528
|
+
appSecret: binding.appSecret,
|
|
529
|
+
domain: mod.Domain.Feishu,
|
|
530
|
+
loggerLevel: mod.LoggerLevel.info,
|
|
531
|
+
});
|
|
532
|
+
await status.wsClient.start({
|
|
533
|
+
eventDispatcher: status.eventDispatcher,
|
|
534
|
+
});
|
|
535
|
+
status.status = 'running';
|
|
536
|
+
status.connected = true;
|
|
537
|
+
status.connectedAt = new Date().toISOString();
|
|
538
|
+
status.lastError = undefined;
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
status.status = 'error';
|
|
542
|
+
status.connected = false;
|
|
543
|
+
status.lastError = error instanceof Error ? error.message : String(error);
|
|
544
|
+
this.options.log?.(`localcore-lark start failed for ${binding.workspaceId}: ${status.lastError}`);
|
|
545
|
+
}
|
|
546
|
+
this.notifyRuntimeStateChanged();
|
|
547
|
+
}
|
|
548
|
+
async stopWorkspace(workspaceId) {
|
|
549
|
+
const state = this.runtime.get(workspaceId);
|
|
550
|
+
if (!state) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
await state.wsClient?.stop?.();
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// Best effort: current SDK versions may not implement stop().
|
|
558
|
+
}
|
|
559
|
+
this.runtime.set(workspaceId, {
|
|
560
|
+
workspaceId,
|
|
561
|
+
enabled: false,
|
|
562
|
+
status: 'stopped',
|
|
563
|
+
connected: false,
|
|
564
|
+
appId: state.appId,
|
|
565
|
+
});
|
|
566
|
+
this.notifyRuntimeStateChanged();
|
|
567
|
+
}
|
|
568
|
+
notifyRuntimeStateChanged() {
|
|
569
|
+
this.options.eventBus.emit({
|
|
570
|
+
type: 'runtime.state.changed',
|
|
571
|
+
payload: {
|
|
572
|
+
reason: 'channel-bindings',
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
async handleMessageEvent(workspaceId, data) {
|
|
577
|
+
const payload = (data?.event && typeof data.event === 'object')
|
|
578
|
+
? data.event
|
|
579
|
+
: data;
|
|
580
|
+
const message = payload?.message;
|
|
581
|
+
const sender = payload?.sender;
|
|
582
|
+
if (!message || !sender) {
|
|
583
|
+
this.options.log?.(`localcore-lark ignored event without message/sender for ${workspaceId}: ${JSON.stringify(Object.keys(data || {}))}`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
let parsedContent = {};
|
|
587
|
+
try {
|
|
588
|
+
parsedContent = JSON.parse(String(message.content || '{}'));
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
parsedContent = {};
|
|
592
|
+
}
|
|
593
|
+
const text = String(parsedContent.text || '').trim();
|
|
594
|
+
if (!text) {
|
|
595
|
+
this.options.log?.(`localcore-lark ignored non-text message for ${workspaceId}: ${String(message.message_type || 'unknown')}`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const platformUserId = String(sender.sender_id?.user_id || sender.sender_id?.open_id || '').trim();
|
|
599
|
+
const chatId = String(message.chat_id || platformUserId).trim();
|
|
600
|
+
if (!platformUserId || !chatId) {
|
|
601
|
+
this.options.log?.(`localcore-lark ignored message without sender/chat for ${workspaceId}`);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
this.options.log?.(`localcore-lark inbound message for ${workspaceId}: chat=${chatId} user=${platformUserId} text=${JSON.stringify(text.slice(0, 120))}`);
|
|
605
|
+
await this.handleInboundMessage({
|
|
606
|
+
workspaceId,
|
|
607
|
+
platformUserId,
|
|
608
|
+
chatId,
|
|
609
|
+
displayName: String(payload?.sender?.sender_id?.user_id ||
|
|
610
|
+
payload?.sender?.sender_id?.open_id ||
|
|
611
|
+
`Lark ${platformUserId.slice(-6)}`),
|
|
612
|
+
text,
|
|
613
|
+
messageId: String(message.message_id || (0, node_crypto_1.randomUUID)()),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
collectBindings(config) {
|
|
617
|
+
const projects = Array.isArray(config?.projects) ? config.projects : [];
|
|
618
|
+
return projects.flatMap((project) => {
|
|
619
|
+
const platforms = Array.isArray(project.platforms) ? project.platforms : [];
|
|
620
|
+
return platforms
|
|
621
|
+
.map((platform) => ({
|
|
622
|
+
platformType: (0, desktop_js_1.normalizeDesktopPlatformType)(platform?.type),
|
|
623
|
+
options: platform?.options && typeof platform.options === 'object'
|
|
624
|
+
? platform.options
|
|
625
|
+
: {},
|
|
626
|
+
}))
|
|
627
|
+
.filter((platform) => platform.platformType === 'lark')
|
|
628
|
+
.map((platform) => ({
|
|
629
|
+
workspaceId: project.name,
|
|
630
|
+
appId: String(platform.options.app_id || '').trim(),
|
|
631
|
+
appSecret: String(platform.options.app_secret || '').trim(),
|
|
632
|
+
encryptKey: String(platform.options.encrypt_key || '').trim(),
|
|
633
|
+
verificationToken: String(platform.options.verification_token || '').trim(),
|
|
634
|
+
autoApprove: String(platform.options.auto_approve || '').trim().toLowerCase() === 'true'
|
|
635
|
+
|| platform.options.auto_approve === true,
|
|
636
|
+
enabled: Boolean(String(platform.options.app_id || '').trim() && String(platform.options.app_secret || '').trim()),
|
|
637
|
+
project,
|
|
638
|
+
}));
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
async getLarkModule() {
|
|
642
|
+
if (!this.larkModulePromise) {
|
|
643
|
+
this.larkModulePromise = Promise.resolve().then(() => __importStar(require('@larksuiteoapi/node-sdk')));
|
|
644
|
+
}
|
|
645
|
+
return this.larkModulePromise;
|
|
646
|
+
}
|
|
647
|
+
buildInteractiveCard(text, buttonRows = [], sessionKey, threadId) {
|
|
648
|
+
const elements = [];
|
|
649
|
+
if (text) {
|
|
650
|
+
elements.push({ tag: 'markdown', content: text });
|
|
651
|
+
}
|
|
652
|
+
for (const row of buttonRows) {
|
|
653
|
+
const actions = row
|
|
654
|
+
.filter((button) => button.text && button.data)
|
|
655
|
+
.map((button, index) => ({
|
|
656
|
+
tag: 'button',
|
|
657
|
+
text: {
|
|
658
|
+
tag: 'plain_text',
|
|
659
|
+
content: button.text,
|
|
660
|
+
},
|
|
661
|
+
type: index === 0 ? 'primary' : 'default',
|
|
662
|
+
value: {
|
|
663
|
+
action: 'permission_response',
|
|
664
|
+
response: button.data,
|
|
665
|
+
session_key: sessionKey || '',
|
|
666
|
+
thread_id: threadId || '',
|
|
667
|
+
},
|
|
668
|
+
}));
|
|
669
|
+
if (actions.length) {
|
|
670
|
+
elements.push({
|
|
671
|
+
tag: 'action',
|
|
672
|
+
actions,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
config: { wide_screen_mode: true },
|
|
678
|
+
elements,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
async sendTextAsCard(state, chatId, text, buttonRows = [], sessionKey, threadId) {
|
|
682
|
+
const receiveIdType = chatId.startsWith('oc_') ? 'chat_id' : chatId.startsWith('ou_') ? 'open_id' : 'user_id';
|
|
683
|
+
const response = await state.client.im.message.create({
|
|
684
|
+
params: {
|
|
685
|
+
receive_id_type: receiveIdType,
|
|
686
|
+
},
|
|
687
|
+
data: {
|
|
688
|
+
receive_id: chatId,
|
|
689
|
+
msg_type: 'interactive',
|
|
690
|
+
content: JSON.stringify(this.buildInteractiveCard(text, buttonRows, sessionKey, threadId)),
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
return String(response?.data?.message_id || '').trim();
|
|
694
|
+
}
|
|
695
|
+
async patchTextCard(state, messageId, text, buttonRows = [], sessionKey, threadId) {
|
|
696
|
+
await state.client.im.message.patch({
|
|
697
|
+
path: {
|
|
698
|
+
message_id: messageId,
|
|
699
|
+
},
|
|
700
|
+
data: {
|
|
701
|
+
content: JSON.stringify(this.buildInteractiveCard(text, buttonRows, sessionKey, threadId)),
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
generatePairingCode() {
|
|
706
|
+
return String((0, node_crypto_1.randomInt)(100000, 1000000));
|
|
707
|
+
}
|
|
708
|
+
findAwaitingPermissionThreadId(workspaceId, chatId, platformUserId) {
|
|
709
|
+
for (const [sessionKey, route] of this.threadRouting.entries()) {
|
|
710
|
+
if (route.workspaceId !== workspaceId
|
|
711
|
+
|| route.chatId !== chatId
|
|
712
|
+
|| route.platformUserId !== platformUserId) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const turn = this.outboundTurns.get(sessionKey);
|
|
716
|
+
if (turn?.awaitingPermission && route.threadId) {
|
|
717
|
+
return route.threadId;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return '';
|
|
721
|
+
}
|
|
722
|
+
parseSlashCommand(text) {
|
|
723
|
+
const normalized = String(text || '').trim();
|
|
724
|
+
if (!normalized.startsWith('/')) {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
const [name = '', ...args] = normalized.slice(1).split(/\s+/);
|
|
728
|
+
if (!name) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
name: name.trim().toLowerCase(),
|
|
733
|
+
args,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
createTurnState(sessionKey, sourceMessageId) {
|
|
737
|
+
const turn = {
|
|
738
|
+
sessionKey,
|
|
739
|
+
sourceMessageId,
|
|
740
|
+
awaitingPermission: false,
|
|
741
|
+
processing: false,
|
|
742
|
+
previewText: '',
|
|
743
|
+
finalText: '',
|
|
744
|
+
thinkingSteps: [],
|
|
745
|
+
toolCalls: [],
|
|
746
|
+
statusLines: [],
|
|
747
|
+
buttonRows: [],
|
|
748
|
+
lastPatchedAt: 0,
|
|
749
|
+
};
|
|
750
|
+
this.outboundTurns.set(sessionKey, turn);
|
|
751
|
+
return turn;
|
|
752
|
+
}
|
|
753
|
+
getOrCreateTurnState(sessionKey, messageId) {
|
|
754
|
+
const existing = this.outboundTurns.get(sessionKey);
|
|
755
|
+
if (existing) {
|
|
756
|
+
if (messageId && !existing.messageId) {
|
|
757
|
+
existing.messageId = messageId;
|
|
758
|
+
}
|
|
759
|
+
return existing;
|
|
760
|
+
}
|
|
761
|
+
const turn = this.createTurnState(sessionKey);
|
|
762
|
+
if (messageId) {
|
|
763
|
+
turn.messageId = messageId;
|
|
764
|
+
}
|
|
765
|
+
return turn;
|
|
766
|
+
}
|
|
767
|
+
consumeBridgeEvent(turn, event) {
|
|
768
|
+
const content = String(event.content || '').trim();
|
|
769
|
+
if (event.type === 'typing_start') {
|
|
770
|
+
// Always start a fresh card for each generation phase.
|
|
771
|
+
// This keeps Feishu message order aligned with user-visible timeline.
|
|
772
|
+
turn.messageId = undefined;
|
|
773
|
+
turn.processing = true;
|
|
774
|
+
turn.permissionMessageId = undefined;
|
|
775
|
+
turn.previewText = '';
|
|
776
|
+
turn.finalText = '';
|
|
777
|
+
turn.thinkingSteps = [];
|
|
778
|
+
turn.toolCalls = [];
|
|
779
|
+
turn.buttonRows = [];
|
|
780
|
+
turn.statusLines = [];
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (event.type === 'typing_stop') {
|
|
784
|
+
turn.processing = false;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (event.type === 'preview_start' || event.type === 'update_message') {
|
|
788
|
+
turn.previewText = content;
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (event.type === 'status') {
|
|
792
|
+
if (content) {
|
|
793
|
+
this.pushUnique(turn.statusLines, content);
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (event.type === 'buttons') {
|
|
798
|
+
turn.awaitingPermission = true;
|
|
799
|
+
turn.buttonRows = this.enableCardActions && Array.isArray(event.buttonRows)
|
|
800
|
+
? event.buttonRows
|
|
801
|
+
.map((row) => Array.isArray(row)
|
|
802
|
+
? row
|
|
803
|
+
.filter((button) => Boolean(button?.text && button?.data))
|
|
804
|
+
.map((button) => ({ text: button.text, data: button.data }))
|
|
805
|
+
: [])
|
|
806
|
+
.filter((row) => row.length > 0)
|
|
807
|
+
: [];
|
|
808
|
+
if (this.mirrorPermissionStateInMainCard && content) {
|
|
809
|
+
this.pushUnique(turn.statusLines, `等待确认: ${content}`);
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (!content) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (content.startsWith('💭 ')) {
|
|
817
|
+
this.pushUnique(turn.thinkingSteps, content.slice(3).trim());
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (content.startsWith('🔧 ')) {
|
|
821
|
+
this.pushUnique(turn.toolCalls, content.slice(3).trim());
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (content.startsWith('⏳ ') || content.startsWith('📤 ')) {
|
|
825
|
+
this.pushUnique(turn.statusLines, content.slice(3).trim());
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
turn.finalText = content;
|
|
829
|
+
turn.previewText = content;
|
|
830
|
+
}
|
|
831
|
+
renderTurnCard(turn) {
|
|
832
|
+
const sections = [];
|
|
833
|
+
if (turn.finalText) {
|
|
834
|
+
sections.push(turn.finalText);
|
|
835
|
+
}
|
|
836
|
+
else if (turn.previewText) {
|
|
837
|
+
sections.push(turn.previewText);
|
|
838
|
+
}
|
|
839
|
+
else if (turn.processing && turn.toolCalls.length > 0) {
|
|
840
|
+
sections.push(`**处理中**\n${this.formatCompactToolLines(turn.toolCalls).join('\n')}`);
|
|
841
|
+
}
|
|
842
|
+
else if (turn.processing) {
|
|
843
|
+
sections.push('**处理中**\n正在思考...');
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
text: sections.join('\n\n').trim(),
|
|
847
|
+
buttonRows: turn.buttonRows,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
renderPermissionCard(turn, event) {
|
|
851
|
+
const summary = this.buildPermissionSummary(turn, String(event.content || '').trim());
|
|
852
|
+
const sections = [
|
|
853
|
+
'**需要工具确认**',
|
|
854
|
+
summary.command ? `\`${summary.command}\`` : '',
|
|
855
|
+
summary.reason || '',
|
|
856
|
+
'回复:`allow` / `allow all` / `deny`',
|
|
857
|
+
].filter(Boolean);
|
|
858
|
+
const buttonRows = this.enableCardActions && Array.isArray(event.buttonRows)
|
|
859
|
+
? event.buttonRows
|
|
860
|
+
.map((row) => Array.isArray(row)
|
|
861
|
+
? row
|
|
862
|
+
.filter((button) => Boolean(button?.text && button?.data))
|
|
863
|
+
.map((button) => ({ text: button.text, data: button.data }))
|
|
864
|
+
: [])
|
|
865
|
+
.filter((row) => row.length > 0)
|
|
866
|
+
: [];
|
|
867
|
+
return {
|
|
868
|
+
text: sections.join('\n\n').trim(),
|
|
869
|
+
buttonRows,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
renderPendingPairingCard(code) {
|
|
873
|
+
const lines = [
|
|
874
|
+
'**已收到消息**',
|
|
875
|
+
'当前账号还未授权接入这个工作区。',
|
|
876
|
+
'请在桌面端完成审批后再次发送消息。',
|
|
877
|
+
];
|
|
878
|
+
if (code) {
|
|
879
|
+
lines.push(`配对码:\`${code}\``);
|
|
880
|
+
}
|
|
881
|
+
return lines.join('\n\n');
|
|
882
|
+
}
|
|
883
|
+
async handleCardActionEvent(workspaceId, data) {
|
|
884
|
+
try {
|
|
885
|
+
const payload = (data?.event && typeof data.event === 'object')
|
|
886
|
+
? data.event
|
|
887
|
+
: data;
|
|
888
|
+
const value = payload?.action?.value;
|
|
889
|
+
if (!value || value.action !== 'permission_response') {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const response = String(value.response || '').trim();
|
|
893
|
+
const threadId = String(value.thread_id || '').trim();
|
|
894
|
+
if (!response || !threadId) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const router = this.options.getWorkspaceRouter();
|
|
898
|
+
await router.sendThreadAction(threadId, response);
|
|
899
|
+
this.options.log?.(`localcore-lark processed card action for ${workspaceId}: ${response}`);
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
this.options.log?.(`localcore-lark card action failed for ${workspaceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
pushUnique(target, value) {
|
|
906
|
+
const normalized = value.trim();
|
|
907
|
+
if (!normalized) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (target[target.length - 1] === normalized) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
target.push(normalized);
|
|
914
|
+
if (target.length > 6) {
|
|
915
|
+
target.splice(0, target.length - 6);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
buildPermissionSummary(turn, rawContent) {
|
|
919
|
+
const lastTool = turn.toolCalls[turn.toolCalls.length - 1] || '';
|
|
920
|
+
const [commandPart = '', reasonPart = ''] = lastTool.split(/\s+-\s+/, 2);
|
|
921
|
+
const compactContent = rawContent
|
|
922
|
+
.replace(/\s+/g, ' ')
|
|
923
|
+
.replace(/请选择一个选项继续执行。?/g, '')
|
|
924
|
+
.replace(/若按钮没有显示,请直接回复:?\s*allow all \/ allow \/ deny/gi, '')
|
|
925
|
+
.replace(/等待工具确认/gi, '')
|
|
926
|
+
.trim();
|
|
927
|
+
return {
|
|
928
|
+
command: commandPart.trim(),
|
|
929
|
+
reason: reasonPart.trim() || compactContent,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
formatCompactToolLines(toolCalls) {
|
|
933
|
+
return toolCalls.slice(-2).map((line) => {
|
|
934
|
+
const compact = line.replace(/\s+/g, ' ').trim();
|
|
935
|
+
return compact.startsWith('Terminal') ? '• Terminal' : `• ${compact}`;
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
async sendImmediateCard(workspaceId, chatId, text) {
|
|
939
|
+
const state = this.runtime.get(workspaceId);
|
|
940
|
+
if (!state?.client || !state.connected) {
|
|
941
|
+
this.options.log?.(`localcore-lark immediate card skipped because workspace is not connected: ${workspaceId}`);
|
|
942
|
+
return '';
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
return await this.sendTextAsCard(state, chatId, text);
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
this.options.log?.(`localcore-lark immediate card failed for ${workspaceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
949
|
+
return '';
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async addAcknowledgementReaction(workspaceId, messageId, turn) {
|
|
953
|
+
const state = this.runtime.get(workspaceId);
|
|
954
|
+
if (!state?.client || !state.connected || !messageId) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
const response = await state.client.im.messageReaction.create({
|
|
959
|
+
path: {
|
|
960
|
+
message_id: messageId,
|
|
961
|
+
},
|
|
962
|
+
data: {
|
|
963
|
+
reaction_type: {
|
|
964
|
+
emoji_type: 'DONE',
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
});
|
|
968
|
+
turn.acknowledgementReactionId = String(response?.data?.reaction_id || '').trim() || undefined;
|
|
969
|
+
this.options.log?.(`localcore-lark acknowledgement reaction added for message=${messageId}`);
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
this.options.log?.(`localcore-lark acknowledgement reaction failed for message=${messageId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
exports.LocalCoreLarkGateway = LocalCoreLarkGateway;
|