vibe-coding-master 0.4.30 → 0.4.31
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 +25 -14
- package/dist/backend/adapters/git-adapter.js +22 -0
- package/dist/backend/api/gateway-routes.js +3 -0
- package/dist/backend/api/harness-routes.js +7 -0
- package/dist/backend/gateway/channels/lark-channel.js +254 -0
- package/dist/backend/gateway/channels/weixin-ilink-channel.js +10 -3
- package/dist/backend/gateway/gateway-channel.js +16 -0
- package/dist/backend/gateway/gateway-service.js +164 -39
- package/dist/backend/gateway/gateway-settings-service.js +60 -14
- package/dist/backend/server.js +12 -2
- package/dist/backend/services/harness-service.js +32 -0
- package/dist-frontend/assets/index-CKX3RbCz.js +96 -0
- package/dist-frontend/assets/index-CuNHDIFw.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/gateway-design.md +48 -32
- package/docs/product-design.md +20 -15
- package/package.json +2 -1
- package/dist-frontend/assets/index-BV_K-1WH.css +0 -32
- package/dist-frontend/assets/index-DNh_JE6R.js +0 -96
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ When Gate Review Gates are enabled for a task, or when a Gate Reviewer session a
|
|
|
33
33
|
- VCM-managed root rules, role agents, repo-local VCM skills, Claude Code hooks, generated-context tools, and PR template.
|
|
34
34
|
- Generated context for Rust and npm workspace module indexing plus public surface indexing.
|
|
35
35
|
- Translation panel powered by the long-lived Translator session.
|
|
36
|
-
- Mobile Gateway through Tencent iLink
|
|
36
|
+
- Mobile Gateway through Tencent iLink / Weixin DM or Lark, for talking to PM and managing tasks from a phone.
|
|
37
37
|
- Durable task state, role session state, handoff artifacts, and message history.
|
|
38
38
|
|
|
39
39
|
## Requirements
|
|
@@ -221,7 +221,7 @@ The left sidebar is intentionally compact and collapsible:
|
|
|
221
221
|
- `Settings`: `Theme`, `Flow pause alert`, `Try alert`, `Messages`, and `Events`.
|
|
222
222
|
- `Translation`: global conversation translation, auto-send, target language, output scope, file translation, bootstrap, memory update, session status, and Translator session access.
|
|
223
223
|
- `Gate Review Gates`: global gate switches for architecture plan, validation adequacy, and final diff.
|
|
224
|
-
- `Gateway`: Weixin iLink binding, Gateway on/off, Gateway translation, and
|
|
224
|
+
- `Gateway`: Weixin iLink or Lark binding, Gateway on/off, Gateway translation, QR login, and Lark pairing.
|
|
225
225
|
- `VCM Harness`: fixed-install status, bootstrap completion checks, and the bootstrap terminal when one is running.
|
|
226
226
|
- `New Task`: one `task name` input.
|
|
227
227
|
- `Tasks`: task list and task status.
|
|
@@ -238,11 +238,11 @@ When VCM is connected to an active task, the bottom of the sidebar shows a task
|
|
|
238
238
|
|
|
239
239
|
## Mobile Gateway
|
|
240
240
|
|
|
241
|
-
VCM Gateway lets one
|
|
241
|
+
VCM Gateway lets one mobile chat identity bind to one desktop VCM instance. It supports Weixin iLink and Lark. It is a mobile control surface for the current desktop VCM, not a remote terminal and not a multi-user collaboration surface.
|
|
242
242
|
|
|
243
243
|
Gateway rules:
|
|
244
244
|
|
|
245
|
-
- DM only; group
|
|
245
|
+
- Weixin is DM only; Lark can receive group messages only when the bot is mentioned.
|
|
246
246
|
- One phone identity binds to one desktop VCM instance.
|
|
247
247
|
- The phone can manage projects and tasks available to that desktop VCM instance.
|
|
248
248
|
- When the desktop UI has an active task selected, Gateway uses that task automatically.
|
|
@@ -273,7 +273,18 @@ Gateway state is stored locally under:
|
|
|
273
273
|
|
|
274
274
|
The `Gateway` toggle is disabled until a QR login has produced a usable iLink token. After binding, VCM keeps receiving `/help`, `/start`, `/status`, `/projects`, and `/tasks` even when the toggle is off. Turning `Gateway` on, either from desktop or by sending `/start`, enables PM messages, task-changing commands, and PM reply push. `Reset Binding` clears the stored token and bound Weixin identity so the desktop VCM can bind again.
|
|
275
275
|
|
|
276
|
-
|
|
276
|
+
### Bind Lark
|
|
277
|
+
|
|
278
|
+
1. Create a Lark app with bot capability, message receive events, and WebSocket event delivery.
|
|
279
|
+
2. Open the sidebar `Gateway` section and select `Lark`.
|
|
280
|
+
3. Save the Lark App ID and App Secret.
|
|
281
|
+
4. Click `Create Pairing Code`.
|
|
282
|
+
5. Send `/bind CODE` to the Lark bot before the code expires.
|
|
283
|
+
6. After binding succeeds, turn `Gateway` on in the sidebar.
|
|
284
|
+
|
|
285
|
+
Lark credentials stay in local VCM state. The App Secret is not shown again in the UI. `Reset Binding` clears the bound Lark user/chat state while keeping the saved Lark app credentials.
|
|
286
|
+
|
|
287
|
+
When Gateway is turned on, VCM automatically turns off the browser `Flow pause alert` and disables `Try alert`. Gateway becomes the notification path, so the browser should not show blocking flow-pause dialogs while the user is managing the task from the phone.
|
|
277
288
|
|
|
278
289
|
### Translation
|
|
279
290
|
|
|
@@ -281,17 +292,17 @@ The Gateway section has its own `Translation` toggle.
|
|
|
281
292
|
|
|
282
293
|
When Gateway translation is on:
|
|
283
294
|
|
|
284
|
-
-
|
|
295
|
+
- Mobile input is translated to English before being submitted to PM.
|
|
285
296
|
- The prompt sent to PM includes only the translated English text with a `[VCM Gateway]` marker.
|
|
286
297
|
- The original Chinese text is not included in the PM prompt.
|
|
287
|
-
- PM replies are translated
|
|
298
|
+
- PM replies are translated before VCM sends them to the bound mobile chat.
|
|
288
299
|
- If PM reply translation fails or times out, VCM sends a translation failure notice instead of the English source. The user can send `/retry` to retry the latest failed Gateway output translation.
|
|
289
300
|
|
|
290
|
-
When Gateway translation is off, plain
|
|
301
|
+
When Gateway translation is off, plain mobile text is sent to PM as-is.
|
|
291
302
|
|
|
292
303
|
### Commands
|
|
293
304
|
|
|
294
|
-
After Gateway is bound, send commands in the bound
|
|
305
|
+
After Gateway is bound, send commands in the bound mobile conversation.
|
|
295
306
|
|
|
296
307
|
When `Gateway` is off, only these commands are accepted:
|
|
297
308
|
|
|
@@ -343,7 +354,7 @@ Typical mobile flow:
|
|
|
343
354
|
|
|
344
355
|
- `/status`: shows Gateway, binding, translation, current project, current task, and last poll status.
|
|
345
356
|
- `/status` also adopts the current desktop project/task when one is selected.
|
|
346
|
-
- `/start`: turns Gateway on from the bound
|
|
357
|
+
- `/start`: turns Gateway on from the bound mobile conversation so full mobile task operations and PM messages are allowed. If the current task has a cached latest PM reply, `/start` includes it in the response.
|
|
347
358
|
- `/retry`: retries the latest failed Gateway output translation in the current VCM process.
|
|
348
359
|
- `/projects`: lists the current/recent repositories known by the desktop VCM.
|
|
349
360
|
- `/use-project <index-or-path>`: selects the Gateway's current project context.
|
|
@@ -366,12 +377,12 @@ Typical mobile flow:
|
|
|
366
377
|
- If the QR dialog does not appear, refresh the page and click `Start QR Login` again.
|
|
367
378
|
- If the QR status stays `wait`, confirm the login on the phone and click `Confirm` again.
|
|
368
379
|
- If the QR code expires, start a new QR login.
|
|
369
|
-
- If `Gateway` cannot be enabled, bind Weixin first.
|
|
370
|
-
- If `/start` or read-only commands do not receive replies, check that the
|
|
380
|
+
- If `Gateway` cannot be enabled, bind Weixin or pair Lark first.
|
|
381
|
+
- If `/start` or read-only commands do not receive replies, check that the selected channel is connected and the message comes from the bound identity.
|
|
371
382
|
- If PM messages or task-changing commands are rejected, check that Gateway is on.
|
|
372
383
|
- If plain text cannot be sent to PM, select a project and task first, and make sure the task's PM session is running and idle.
|
|
373
384
|
- If PM replies are not pushed, check that Gateway is on and the PM session is producing normal Claude transcript output.
|
|
374
|
-
- If PM reply translation fails, send `/retry` from the bound
|
|
385
|
+
- If PM reply translation fails, send `/retry` from the bound mobile conversation. Retry state is memory-only and is cleared when VCM restarts.
|
|
375
386
|
|
|
376
387
|
## Translation
|
|
377
388
|
|
|
@@ -393,7 +404,7 @@ The sidebar `Settings` section also stores the UI theme preference in this file.
|
|
|
393
404
|
|
|
394
405
|
The same sidebar also has a `Flow pause alert` toggle. It is on by default and controls the local alert that fires when VCM detects that the current role flow has stopped advancing. Short flows use a weak reminder: the soft two-note chime plays 3 times, 1.4 seconds apart. Flows lasting 2 minutes or longer use a strong reminder: VCM shows an alert dialog and repeats the chime until the user confirms it. The alert sound reuses one browser audio context so repeated reminders remain reliable in stricter browsers such as Safari. Safari users may still need to manually set `Safari > Website Settings > Auto-Play > Allow All Auto-Play`; Chrome is recommended for the most reliable alert sound behavior. The `Try alert` button always triggers the strong reminder for testing.
|
|
395
406
|
|
|
396
|
-
When Gateway is on, `Flow pause alert` is forced off because mobile notifications are delivered through
|
|
407
|
+
When Gateway is on, `Flow pause alert` is forced off because mobile notifications are delivered through Gateway and browser alerts can block normal workflow progress.
|
|
397
408
|
|
|
398
409
|
Translation behavior:
|
|
399
410
|
|
|
@@ -135,6 +135,28 @@ export function createGitAdapter(runner) {
|
|
|
135
135
|
}
|
|
136
136
|
return result.stdout;
|
|
137
137
|
},
|
|
138
|
+
async getDiff(repoRoot, baseRef, headRef = null, paths = []) {
|
|
139
|
+
const result = await runGit(runner, repoRoot, [
|
|
140
|
+
"diff",
|
|
141
|
+
"--no-ext-diff",
|
|
142
|
+
"--binary",
|
|
143
|
+
"--src-prefix=a/",
|
|
144
|
+
"--dst-prefix=b/",
|
|
145
|
+
baseRef,
|
|
146
|
+
...(headRef ? [headRef] : []),
|
|
147
|
+
"--",
|
|
148
|
+
...paths
|
|
149
|
+
]);
|
|
150
|
+
if (result.exitCode !== 0) {
|
|
151
|
+
throw new VcmError({
|
|
152
|
+
code: "GIT_ERROR",
|
|
153
|
+
message: "Unable to read Git diff.",
|
|
154
|
+
statusCode: 400,
|
|
155
|
+
hint: result.stderr
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return result.stdout;
|
|
159
|
+
},
|
|
138
160
|
async getCommitList(repoRoot, range) {
|
|
139
161
|
const result = await runGit(runner, repoRoot, ["log", "--format=%H%x00%s%x00%cI%x1e", range]);
|
|
140
162
|
if (result.exitCode !== 0) {
|
|
@@ -8,6 +8,9 @@ export function registerGatewayRoutes(app, deps) {
|
|
|
8
8
|
app.post("/api/gateway/qr/start", async () => {
|
|
9
9
|
return deps.gatewayService.startQrLogin();
|
|
10
10
|
});
|
|
11
|
+
app.post("/api/gateway/pairing-code", async () => {
|
|
12
|
+
return deps.gatewayService.createPairingCode();
|
|
13
|
+
});
|
|
11
14
|
app.post("/api/gateway/qr/check", async (request) => {
|
|
12
15
|
return deps.gatewayService.checkQrLogin(request.body);
|
|
13
16
|
});
|
|
@@ -38,6 +38,13 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
38
38
|
commitSha: request.query.commit
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
+
app.get("/api/projects/harness/repository-diff/file", async (request) => {
|
|
42
|
+
const { project, task } = await requireHarnessTaskContext(deps, request.query.taskSlug);
|
|
43
|
+
return deps.harnessService.getRepositoryFileDiff(task.worktreePath, {
|
|
44
|
+
baseRepoRoot: project.repoRoot,
|
|
45
|
+
path: request.query.path ?? ""
|
|
46
|
+
});
|
|
47
|
+
});
|
|
41
48
|
app.post("/api/projects/harness/repository-diff/merge-to-current-branch", async (request) => {
|
|
42
49
|
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
43
50
|
return deps.harnessService.mergeRepositoryDiffToCurrentBranch(project.repoRoot, {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const DEFAULT_BASE_URL = "lark://open-platform";
|
|
3
|
+
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
4
|
+
export function createLarkChannel(options = {}) {
|
|
5
|
+
let connection = null;
|
|
6
|
+
async function ensureConnection(input) {
|
|
7
|
+
const key = `${input.appId}:${input.appSecret}`;
|
|
8
|
+
if (connection?.key === key) {
|
|
9
|
+
return connection;
|
|
10
|
+
}
|
|
11
|
+
connection?.wsClient?.close({ force: true });
|
|
12
|
+
const Lark = await import("@larksuiteoapi/node-sdk");
|
|
13
|
+
const queue = [];
|
|
14
|
+
const waiters = [];
|
|
15
|
+
const state = {
|
|
16
|
+
key,
|
|
17
|
+
wsClient: null,
|
|
18
|
+
client: null,
|
|
19
|
+
queue,
|
|
20
|
+
waiters,
|
|
21
|
+
ready: Promise.resolve(),
|
|
22
|
+
error: null
|
|
23
|
+
};
|
|
24
|
+
const client = new Lark.Client({
|
|
25
|
+
appId: input.appId,
|
|
26
|
+
appSecret: input.appSecret,
|
|
27
|
+
domain: options.domain ?? "lark"
|
|
28
|
+
});
|
|
29
|
+
const dispatcher = new Lark.EventDispatcher({}).register({
|
|
30
|
+
"im.message.receive_v1": async (data) => {
|
|
31
|
+
for (const update of parseLarkMessageEvent(data)) {
|
|
32
|
+
queue.push(update);
|
|
33
|
+
}
|
|
34
|
+
while (waiters.length > 0) {
|
|
35
|
+
waiters.shift()?.();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const ready = new Promise((resolve, reject) => {
|
|
40
|
+
const wsClient = new Lark.WSClient({
|
|
41
|
+
appId: input.appId,
|
|
42
|
+
appSecret: input.appSecret,
|
|
43
|
+
domain: options.domain ?? "lark",
|
|
44
|
+
source: "vcm-gateway",
|
|
45
|
+
autoReconnect: true,
|
|
46
|
+
loggerLevel: options.loggerLevel,
|
|
47
|
+
onReady: () => resolve(),
|
|
48
|
+
onError: (error) => {
|
|
49
|
+
state.error = error;
|
|
50
|
+
reject(error);
|
|
51
|
+
while (waiters.length > 0) {
|
|
52
|
+
waiters.shift()?.();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
state.wsClient = wsClient;
|
|
57
|
+
state.client = client;
|
|
58
|
+
void wsClient.start({ eventDispatcher: dispatcher }).catch((error) => {
|
|
59
|
+
state.error = error;
|
|
60
|
+
reject(error);
|
|
61
|
+
while (waiters.length > 0) {
|
|
62
|
+
waiters.shift()?.();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
state.ready = ready;
|
|
67
|
+
connection = state;
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
id: "lark",
|
|
72
|
+
label: "Lark",
|
|
73
|
+
defaultBaseUrl: DEFAULT_BASE_URL,
|
|
74
|
+
supportsQrLogin: false,
|
|
75
|
+
async getUpdates(input) {
|
|
76
|
+
const appId = input.account.appId?.trim();
|
|
77
|
+
const appSecret = input.account.appSecret?.trim();
|
|
78
|
+
if (!appId || !appSecret) {
|
|
79
|
+
throw new Error("Lark App ID and App Secret are required.");
|
|
80
|
+
}
|
|
81
|
+
const state = await ensureConnection({ appId, appSecret });
|
|
82
|
+
await state.ready;
|
|
83
|
+
if (state.error) {
|
|
84
|
+
throw state.error;
|
|
85
|
+
}
|
|
86
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
87
|
+
const updates = await waitForUpdates(state, timeoutMs, input.signal);
|
|
88
|
+
if (input.signal?.aborted && connection === state) {
|
|
89
|
+
connection = null;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
cursor: new Date().toISOString(),
|
|
93
|
+
timeoutMs,
|
|
94
|
+
updates
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
async sendText(input) {
|
|
98
|
+
const appId = input.account.appId?.trim();
|
|
99
|
+
const appSecret = input.account.appSecret?.trim();
|
|
100
|
+
if (!appId || !appSecret) {
|
|
101
|
+
throw new Error("Lark App ID and App Secret are required.");
|
|
102
|
+
}
|
|
103
|
+
const state = await ensureConnection({ appId, appSecret });
|
|
104
|
+
await state.ready;
|
|
105
|
+
if (!state.client) {
|
|
106
|
+
throw new Error("Lark client is not initialized.");
|
|
107
|
+
}
|
|
108
|
+
const receiveId = input.chatId || input.account.homeChatId || input.toUserId;
|
|
109
|
+
const receiveIdType = input.chatId || input.account.homeChatId ? "chat_id" : "open_id";
|
|
110
|
+
if (!receiveId) {
|
|
111
|
+
throw new Error("Lark send target is missing.");
|
|
112
|
+
}
|
|
113
|
+
const result = await state.client.im.v1.message.create({
|
|
114
|
+
params: {
|
|
115
|
+
receive_id_type: receiveIdType
|
|
116
|
+
},
|
|
117
|
+
data: {
|
|
118
|
+
receive_id: receiveId,
|
|
119
|
+
msg_type: "text",
|
|
120
|
+
content: JSON.stringify({ text: input.text }),
|
|
121
|
+
uuid: `vcm-gateway-${randomUUID()}`
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return result?.data?.message_id ?? `vcm-gateway-${randomUUID()}`;
|
|
125
|
+
},
|
|
126
|
+
isSessionExpiredError() {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function waitForUpdates(state, timeoutMs, signal) {
|
|
132
|
+
if (state.queue.length > 0) {
|
|
133
|
+
return state.queue.splice(0);
|
|
134
|
+
}
|
|
135
|
+
if (signal?.aborted) {
|
|
136
|
+
state.wsClient?.close({ force: true });
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
await new Promise((resolve) => {
|
|
140
|
+
let settled = false;
|
|
141
|
+
let timeout;
|
|
142
|
+
const finish = () => {
|
|
143
|
+
if (settled) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
settled = true;
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
const index = state.waiters.indexOf(waiter);
|
|
149
|
+
if (index >= 0) {
|
|
150
|
+
state.waiters.splice(index, 1);
|
|
151
|
+
}
|
|
152
|
+
signal?.removeEventListener("abort", finish);
|
|
153
|
+
resolve();
|
|
154
|
+
};
|
|
155
|
+
const waiter = finish;
|
|
156
|
+
timeout = setTimeout(finish, timeoutMs);
|
|
157
|
+
state.waiters.push(waiter);
|
|
158
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
159
|
+
});
|
|
160
|
+
if (state.error) {
|
|
161
|
+
throw state.error;
|
|
162
|
+
}
|
|
163
|
+
if (signal?.aborted) {
|
|
164
|
+
state.wsClient?.close({ force: true });
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
return state.queue.splice(0);
|
|
168
|
+
}
|
|
169
|
+
function parseLarkMessageEvent(data) {
|
|
170
|
+
if (!isObject(data)) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const event = isObject(data.event) ? data.event : data;
|
|
174
|
+
if (!isObject(event)) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const message = isObject(event.message) ? event.message : {};
|
|
178
|
+
const sender = isObject(event.sender) ? event.sender : {};
|
|
179
|
+
const senderId = isObject(sender.sender_id) ? sender.sender_id : {};
|
|
180
|
+
const fromUserId = stringOrUndefined(senderId.open_id)
|
|
181
|
+
?? stringOrUndefined(senderId.user_id)
|
|
182
|
+
?? stringOrUndefined(senderId.union_id);
|
|
183
|
+
const messageId = stringOrUndefined(message.message_id);
|
|
184
|
+
const chatId = stringOrUndefined(message.chat_id);
|
|
185
|
+
const chatType = stringOrUndefined(message.chat_type);
|
|
186
|
+
const rawText = extractTextContent(message.content);
|
|
187
|
+
if (!fromUserId || !messageId || !rawText.trim()) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
const mentions = Array.isArray(message.mentions) ? message.mentions : [];
|
|
191
|
+
const isDirect = chatType === "p2p";
|
|
192
|
+
if (!isDirect && mentions.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const text = stripMentionTokens(rawText, mentions);
|
|
196
|
+
if (!text.trim()) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
const createdAt = normalizeLarkTimestamp(message.create_time);
|
|
200
|
+
return [{
|
|
201
|
+
messageId,
|
|
202
|
+
fromUserId,
|
|
203
|
+
chatId,
|
|
204
|
+
chatType: isDirect ? "dm" : "group",
|
|
205
|
+
text,
|
|
206
|
+
createdAt,
|
|
207
|
+
raw: data
|
|
208
|
+
}];
|
|
209
|
+
}
|
|
210
|
+
function extractTextContent(input) {
|
|
211
|
+
if (typeof input !== "string") {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const parsed = JSON.parse(input);
|
|
216
|
+
if (isObject(parsed) && typeof parsed.text === "string") {
|
|
217
|
+
return parsed.text;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return input;
|
|
222
|
+
}
|
|
223
|
+
return input;
|
|
224
|
+
}
|
|
225
|
+
function stripMentionTokens(text, mentions) {
|
|
226
|
+
let out = text;
|
|
227
|
+
for (const mention of mentions) {
|
|
228
|
+
if (!isObject(mention)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const key = stringOrUndefined(mention.key);
|
|
232
|
+
if (key) {
|
|
233
|
+
out = out.replaceAll(key, "");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return out.replace(/<at\b[^>]*>.*?<\/at>/g, "").trim();
|
|
237
|
+
}
|
|
238
|
+
function normalizeLarkTimestamp(input) {
|
|
239
|
+
const raw = stringOrUndefined(input);
|
|
240
|
+
if (!raw) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
const timestamp = Number.parseInt(raw, 10);
|
|
244
|
+
if (!Number.isFinite(timestamp)) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
return new Date(timestamp).toISOString();
|
|
248
|
+
}
|
|
249
|
+
function stringOrUndefined(input) {
|
|
250
|
+
return typeof input === "string" && input.trim() ? input.trim() : undefined;
|
|
251
|
+
}
|
|
252
|
+
function isObject(value) {
|
|
253
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
254
|
+
}
|
|
@@ -92,10 +92,14 @@ export function createWeixinIlinkChannel(options = {}) {
|
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
return {
|
|
95
|
+
id: "weixin-ilink",
|
|
96
|
+
label: "Weixin iLink",
|
|
97
|
+
defaultBaseUrl: DEFAULT_BASE_URL,
|
|
98
|
+
supportsQrLogin: true,
|
|
95
99
|
async startQrLogin(input) {
|
|
96
100
|
const payload = await post({
|
|
97
101
|
requestBaseUrl: baseUrl,
|
|
98
|
-
endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(
|
|
102
|
+
endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(DEFAULT_BOT_TYPE)}`,
|
|
99
103
|
body: {
|
|
100
104
|
local_token_list: input.localTokenList ?? []
|
|
101
105
|
},
|
|
@@ -134,7 +138,7 @@ export function createWeixinIlinkChannel(options = {}) {
|
|
|
134
138
|
const payload = await post({
|
|
135
139
|
requestBaseUrl: input.account.baseUrl,
|
|
136
140
|
endpoint: "ilink/bot/getupdates",
|
|
137
|
-
token: input.account.token,
|
|
141
|
+
token: input.account.token ?? undefined,
|
|
138
142
|
timeoutMs: input.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS,
|
|
139
143
|
label: "getupdates",
|
|
140
144
|
signal: input.signal,
|
|
@@ -160,7 +164,7 @@ export function createWeixinIlinkChannel(options = {}) {
|
|
|
160
164
|
const payload = await post({
|
|
161
165
|
requestBaseUrl: input.account.baseUrl,
|
|
162
166
|
endpoint: "ilink/bot/sendmessage",
|
|
163
|
-
token: input.account.token,
|
|
167
|
+
token: input.account.token ?? undefined,
|
|
164
168
|
label: "sendmessage",
|
|
165
169
|
body: {
|
|
166
170
|
msg: {
|
|
@@ -184,6 +188,9 @@ export function createWeixinIlinkChannel(options = {}) {
|
|
|
184
188
|
});
|
|
185
189
|
assertIlinkOk(payload, "sendmessage");
|
|
186
190
|
return clientId;
|
|
191
|
+
},
|
|
192
|
+
isSessionExpiredError(error) {
|
|
193
|
+
return error instanceof Error && error.message.toLowerCase().includes("expired");
|
|
187
194
|
}
|
|
188
195
|
};
|
|
189
196
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createGatewayChannelRegistry(channels) {
|
|
2
|
+
if (channels.length === 0) {
|
|
3
|
+
throw new Error("At least one gateway channel must be registered.");
|
|
4
|
+
}
|
|
5
|
+
const byId = new Map();
|
|
6
|
+
for (const channel of channels) {
|
|
7
|
+
byId.set(channel.id, channel);
|
|
8
|
+
}
|
|
9
|
+
const defaultChannel = channels[0];
|
|
10
|
+
return {
|
|
11
|
+
defaultChannel,
|
|
12
|
+
get(channel) {
|
|
13
|
+
return byId.get(channel) ?? defaultChannel;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|