@xmanrui/dsh-im 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +246 -46
- package/lib/index.js +137 -131
- package/package.json +1 -1
- package/plugin-src/client/channels/feishu/api.js +23 -1
- package/plugin-src/client/channels/feishu/index.js +235 -46
- package/plugin-src/client/channels/feishu/styles.js +2 -0
- package/plugin-src/client/i18n.js +39 -0
- package/plugin-src/client/index.js +2 -1
- package/plugin-src/client/styles.js +2 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +146 -12
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +723 -1
- package/src/channels/feishu/feishu-cards.mjs +155 -0
- package/src/channels/feishu/feishu-runtime.mjs +198 -0
- package/src/channels/feishu/multi-bot-controller.mjs +357 -2
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/feishu/repair-manager.mjs +109 -0
- package/src/channels/shared/workspace-command.mjs +2 -2
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-controller.mjs +56 -12
- package/src/channels/weixin/weixin-runtime.mjs +12 -1
|
@@ -58,7 +58,7 @@ const CHANNELS = Object.freeze([
|
|
|
58
58
|
{ id: 'telegram', label: 'Telegram' },
|
|
59
59
|
{ id: 'discord', label: 'Discord' },
|
|
60
60
|
{ id: 'whatsapp', label: 'WhatsApp' },
|
|
61
|
-
{ id: 'office', label: 'AI Office' },
|
|
61
|
+
{ id: 'office', label: 'AI Office', note: '(实验功能)' },
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
64
|
function WeixinLogo() {
|
|
@@ -176,6 +176,7 @@ export function IMSettingsTab({
|
|
|
176
176
|
h(ChannelLogo, { channel: channel.id }),
|
|
177
177
|
h('span', { className: 'dim-channelCopy' },
|
|
178
178
|
h('strong', null, channel.label),
|
|
179
|
+
channel.note ? h('small', { className: 'dim-channelNote' }, channel.note) : null,
|
|
179
180
|
)))),
|
|
180
181
|
h('div', { className: 'dim-divider', 'aria-hidden': 'true' }),
|
|
181
182
|
h('main', {
|
|
@@ -52,8 +52,9 @@ const CSS = String.raw`
|
|
|
52
52
|
.dim-logoSlack svg { width: 21px; height: 21px; }
|
|
53
53
|
.dim-logoWhatsapp { color: white; background: #25d366; }
|
|
54
54
|
.dim-logoWhatsapp svg { width: 21px; height: 21px; }
|
|
55
|
-
.dim-channelCopy { min-width: 0; display:
|
|
55
|
+
.dim-channelCopy { min-width: 0; display: grid; }
|
|
56
56
|
.dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
|
+
.dim-channelNote { overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 10px; line-height: 13px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
58
|
.dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-border-l1, #eef0f3); }
|
|
58
59
|
.dim-panel { min-width: 0; container-type: inline-size; }
|
|
59
60
|
.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page, .dim-panel .dsl-page, .dim-panel .dwa-page { width: 100%; max-width: none; padding: 0 0 24px; }
|
|
@@ -130,13 +130,15 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
130
130
|
verifyApp,
|
|
131
131
|
credentials: ctx.credentials,
|
|
132
132
|
configStore: observedConfigStore,
|
|
133
|
-
createRuntime: async ({ botId, config: botConfig, appSecret }) => {
|
|
133
|
+
createRuntime: async ({ botId, config: botConfig, appSecret, repair }) => {
|
|
134
134
|
const state = await stateFor(botConfig);
|
|
135
135
|
const id = botId ?? botConfig.id ?? botConfig.appId;
|
|
136
136
|
await workspaces.ensure(id);
|
|
137
137
|
const workspaceScope = createBotWorkspaceScope(harness, { botId: id, workspaces, state });
|
|
138
138
|
return new Runtime({
|
|
139
139
|
lark,
|
|
140
|
+
botId: id,
|
|
141
|
+
repair,
|
|
140
142
|
appId: botConfig.appId,
|
|
141
143
|
appSecret,
|
|
142
144
|
domain: botConfig.domain,
|
|
@@ -21,7 +21,16 @@ const REGISTRATION_STATES = new Set([
|
|
|
21
21
|
'idle', 'starting', 'qr_ready', 'polling', 'slow_down',
|
|
22
22
|
'domain_switched', 'saving', 'succeeded', 'expired', 'cancelled', 'error',
|
|
23
23
|
]);
|
|
24
|
+
const REGISTRATION_OPERATIONS = new Set(['provision', 'callback_repair']);
|
|
25
|
+
const CALLBACK_REPAIR_OPERATION = 'callback_repair';
|
|
26
|
+
const OFFICIAL_REGISTRATION_HOSTS = new Set([
|
|
27
|
+
'accounts.feishu.cn',
|
|
28
|
+
'accounts.larksuite.com',
|
|
29
|
+
'open.feishu.cn',
|
|
30
|
+
'open.larksuite.com',
|
|
31
|
+
]);
|
|
24
32
|
const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
33
|
+
const SAFE_FEISHU_APP_ID = /^cli_[A-Za-z0-9_-]+$/;
|
|
25
34
|
|
|
26
35
|
const PUBLIC_ERROR_MESSAGES = Object.freeze({
|
|
27
36
|
abort: 'Registration was cancelled.',
|
|
@@ -35,6 +44,20 @@ const PUBLIC_ERROR_MESSAGES = Object.freeze({
|
|
|
35
44
|
state_cleanup_failed: 'Unable to remove the bot session data. Please retry.',
|
|
36
45
|
deletion_pending: 'Bot deletion is incomplete. Retry removal to finish cleanup.',
|
|
37
46
|
missing_credentials: 'The bot credentials are missing. Delete it and scan again.',
|
|
47
|
+
repair_app_mismatch: 'The authorized Feishu app does not match the selected bot.',
|
|
48
|
+
repair_owner_missing: 'Feishu did not return the authorizing account identity.',
|
|
49
|
+
repair_domain_mismatch: 'The authorized Feishu tenant does not match the selected bot.',
|
|
50
|
+
repair_owner_mismatch: 'The authorizing Feishu account is not an owner of the selected bot.',
|
|
51
|
+
repair_credentials_invalid: 'Feishu returned credentials that could not be verified for the selected bot.',
|
|
52
|
+
repair_bot_mismatch: 'The verified Feishu bot does not match the selected bot.',
|
|
53
|
+
repair_target_changed: 'The selected bot changed while repair was in progress. Start the repair again.',
|
|
54
|
+
credential_update_failed: 'Unable to store the repaired Feishu credentials.',
|
|
55
|
+
credential_state_unknown: 'The repaired Feishu credentials could not be confirmed after saving.',
|
|
56
|
+
repair_connection_failed: 'The callback update was accepted, but the selected bot could not reconnect.',
|
|
57
|
+
card_action_probe_unavailable: 'The selected bot is not connected, so its card button cannot be verified.',
|
|
58
|
+
card_action_probe_send_failed: 'The callback update was accepted, but the verification card could not be sent.',
|
|
59
|
+
card_action_probe_timeout: 'Feishu accepted the update, but the card button was not verified in time. Start the repair again and click the test button within two minutes.',
|
|
60
|
+
card_action_probe_failed: 'Feishu accepted the update, but the card button verification failed.',
|
|
38
61
|
});
|
|
39
62
|
|
|
40
63
|
const POLL_STATUS_BY_REGISTRATION = Object.freeze({
|
|
@@ -81,6 +104,9 @@ function publicRegistration(registration) {
|
|
|
81
104
|
? registration.attempt
|
|
82
105
|
: (finiteNumber(registration.attempt) ?? 0);
|
|
83
106
|
const result = { state, attempt };
|
|
107
|
+
result.operation = REGISTRATION_OPERATIONS.has(registration.operation)
|
|
108
|
+
? registration.operation
|
|
109
|
+
: 'provision';
|
|
84
110
|
const updatedAt = finiteNumber(registration.updatedAt);
|
|
85
111
|
const expiresAt = finiteNumber(registration.expiresAt);
|
|
86
112
|
const remainingSeconds = finiteNumber(registration.remainingSeconds);
|
|
@@ -98,6 +124,40 @@ function publicRegistration(registration) {
|
|
|
98
124
|
return result;
|
|
99
125
|
}
|
|
100
126
|
|
|
127
|
+
function safeRegistrationUrl(value, operation, expectedHost) {
|
|
128
|
+
if (typeof value !== 'string' || value.length === 0) return undefined;
|
|
129
|
+
try {
|
|
130
|
+
const url = new URL(value);
|
|
131
|
+
if (url.protocol !== 'https:'
|
|
132
|
+
|| !OFFICIAL_REGISTRATION_HOSTS.has(url.hostname)
|
|
133
|
+
|| url.port
|
|
134
|
+
|| url.username
|
|
135
|
+
|| url.password) return undefined;
|
|
136
|
+
if (operation === CALLBACK_REPAIR_OPERATION) {
|
|
137
|
+
const clientIds = url.searchParams.getAll('clientID');
|
|
138
|
+
const transportKinds = url.searchParams.getAll('tp');
|
|
139
|
+
const addons = url.searchParams.getAll('addons');
|
|
140
|
+
const hasPlaceholder = [...url.searchParams.values()].some((item) => (
|
|
141
|
+
item.includes('{{') || item.includes('}}')
|
|
142
|
+
));
|
|
143
|
+
if (clientIds.length !== 1
|
|
144
|
+
|| transportKinds.length !== 1
|
|
145
|
+
|| transportKinds[0] !== 'sdk'
|
|
146
|
+
|| !SAFE_FEISHU_APP_ID.test(clientIds[0] ?? '')
|
|
147
|
+
|| url.hostname !== expectedHost
|
|
148
|
+
|| url.searchParams.has('createOnly')
|
|
149
|
+
|| addons.length !== 1
|
|
150
|
+
|| !addons[0]?.trim()
|
|
151
|
+
|| hasPlaceholder) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return url.toString();
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
101
161
|
function connectionFacts(connection) {
|
|
102
162
|
const source = connection && typeof connection === 'object' ? connection : {};
|
|
103
163
|
const connected = source.connected === true
|
|
@@ -151,15 +211,35 @@ async function qrCodeDataUrl(verificationUrl) {
|
|
|
151
211
|
});
|
|
152
212
|
}
|
|
153
213
|
|
|
154
|
-
async function publicProvisioning(registration, encodeQr) {
|
|
155
|
-
|
|
156
|
-
|
|
214
|
+
async function publicProvisioning(registration, encodeQr, expectedHost) {
|
|
215
|
+
const verificationUrl = safeRegistrationUrl(
|
|
216
|
+
registration.qrCodeUrl,
|
|
217
|
+
registration.operation,
|
|
218
|
+
expectedHost,
|
|
219
|
+
);
|
|
220
|
+
const projection = {
|
|
157
221
|
attemptId: String(registration.attempt),
|
|
158
|
-
|
|
159
|
-
|
|
222
|
+
operation: registration.operation,
|
|
223
|
+
...(registration.botId ? { botId: registration.botId } : {}),
|
|
160
224
|
expiresAt: registration.expiresAt ?? Date.now() + (5 * 60_000),
|
|
161
225
|
pollIntervalMs: Math.max(800, Math.min(10_000, (registration.pollIntervalSeconds ?? 1.8) * 1000)),
|
|
162
226
|
};
|
|
227
|
+
if (verificationUrl) {
|
|
228
|
+
return {
|
|
229
|
+
...projection,
|
|
230
|
+
verificationUrl,
|
|
231
|
+
qrCodeDataUrl: await encodeQr(verificationUrl),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// RegistrationManager deliberately removes the device URL as soon as the
|
|
235
|
+
// remote update is committed. Keep only the opaque attempt identity so a
|
|
236
|
+
// browser reload can resume polling the non-cancellable verification phase.
|
|
237
|
+
if (registration.state === 'saving'
|
|
238
|
+
&& registration.operation === CALLBACK_REPAIR_OPERATION
|
|
239
|
+
&& registration.botId) {
|
|
240
|
+
return { ...projection, submitted: true };
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
163
243
|
}
|
|
164
244
|
|
|
165
245
|
function publicBotEntry(entry) {
|
|
@@ -188,7 +268,19 @@ export async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl }
|
|
|
188
268
|
const registration = publicRegistration(source.registration);
|
|
189
269
|
const facts = connectionFacts(source.connection);
|
|
190
270
|
const connected = source.connected === true || facts.connected;
|
|
191
|
-
const
|
|
271
|
+
const repairTarget = registration.operation === CALLBACK_REPAIR_OPERATION
|
|
272
|
+
&& registration.botId
|
|
273
|
+
&& Array.isArray(source.bots)
|
|
274
|
+
? source.bots.find((entry) => entry?.botId === registration.botId)
|
|
275
|
+
: undefined;
|
|
276
|
+
const expectedRegistrationHost = repairTarget?.bot?.domain === 'lark'
|
|
277
|
+
? 'open.larksuite.com'
|
|
278
|
+
: 'open.feishu.cn';
|
|
279
|
+
const provisioning = await publicProvisioning(
|
|
280
|
+
registration,
|
|
281
|
+
encodeQr,
|
|
282
|
+
expectedRegistrationHost,
|
|
283
|
+
);
|
|
192
284
|
const error = publicError(source.error) ?? registration.error ?? null;
|
|
193
285
|
const bots = Array.isArray(source.bots)
|
|
194
286
|
? source.bots.map(publicBotEntry).filter(Boolean)
|
|
@@ -241,6 +333,11 @@ function validPayload(endpoint, payload) {
|
|
|
241
333
|
}
|
|
242
334
|
return null;
|
|
243
335
|
}
|
|
336
|
+
if (endpoint === FEISHU_ENDPOINTS.beginCallbackRepair) {
|
|
337
|
+
return hasOnlyKeys(payload, new Set(['botId'])) && safeOpaqueId(payload.botId)
|
|
338
|
+
? null
|
|
339
|
+
: 'Callback repair requires a single valid botId.';
|
|
340
|
+
}
|
|
244
341
|
if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
|
|
245
342
|
return hasOnlyKeys(payload, new Set(['appId', 'appSecret']))
|
|
246
343
|
&& validCredential(payload.appId, 256)
|
|
@@ -386,6 +483,20 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|
|
386
483
|
value = (await toPublicFeishuStatus(ready, { encodeQr: cachedEncodeQr })).provisioning;
|
|
387
484
|
if (!value) throw new Error('Provisioning did not produce a QR code.');
|
|
388
485
|
attemptQr.set(attemptId, value.verificationUrl);
|
|
486
|
+
} else if (endpoint === FEISHU_ENDPOINTS.beginCallbackRepair) {
|
|
487
|
+
if (typeof controller.startCallbackRepair !== 'function') {
|
|
488
|
+
throw new Error('Feishu callback repair is unavailable.');
|
|
489
|
+
}
|
|
490
|
+
const started = await controller.startCallbackRepair(payload.botId);
|
|
491
|
+
const attemptId = String(publicRegistration(started?.registration).attempt);
|
|
492
|
+
const ready = await waitForQr(controller, started, attemptId, signal);
|
|
493
|
+
value = (await toPublicFeishuStatus(ready, { encodeQr: cachedEncodeQr })).provisioning;
|
|
494
|
+
if (!value
|
|
495
|
+
|| value.operation !== CALLBACK_REPAIR_OPERATION
|
|
496
|
+
|| value.botId !== payload.botId) {
|
|
497
|
+
throw new Error('Callback repair did not produce a safe QR code.');
|
|
498
|
+
}
|
|
499
|
+
attemptQr.set(attemptId, value.verificationUrl);
|
|
389
500
|
} else if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) {
|
|
390
501
|
const current = await statusForRegistration(controller, payload.attemptId);
|
|
391
502
|
if (!current || !sameAttempt(current, payload.attemptId)) {
|
|
@@ -395,6 +506,7 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|
|
395
506
|
const connection = await toPublicFeishuStatus(current, { encodeQr: cachedEncodeQr });
|
|
396
507
|
value = {
|
|
397
508
|
status: pollStatus(current),
|
|
509
|
+
operation: registration.operation,
|
|
398
510
|
...(registration.botId ? { botId: registration.botId } : {}),
|
|
399
511
|
...(connection.provisioning ? { provisioning: connection.provisioning } : {}),
|
|
400
512
|
...(registration.botId && connection.connected ? { connection } : {}),
|
|
@@ -412,12 +524,34 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|
|
412
524
|
}
|
|
413
525
|
const multi = typeof controller.registrationStatus === 'function';
|
|
414
526
|
const registration = publicRegistration(current.registration);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
527
|
+
let after;
|
|
528
|
+
if (!multi && registration.state === 'saving') {
|
|
529
|
+
after = await controller.disconnect();
|
|
530
|
+
} else {
|
|
531
|
+
after = await controller.cancelRegistration(payload.attemptId);
|
|
532
|
+
}
|
|
533
|
+
const afterRegistration = publicRegistration(after?.registration);
|
|
534
|
+
if (registration.operation === CALLBACK_REPAIR_OPERATION
|
|
535
|
+
&& registration.state === 'saving'
|
|
536
|
+
&& ['saving', 'succeeded'].includes(afterRegistration.state)) {
|
|
537
|
+
value = {
|
|
538
|
+
status: pollStatus(after),
|
|
539
|
+
operation: afterRegistration.operation,
|
|
540
|
+
...(afterRegistration.botId ? { botId: afterRegistration.botId } : {}),
|
|
541
|
+
message: 'Callback repair was already submitted and is still being verified.',
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (value?.status !== 'connecting') {
|
|
545
|
+
const url = attemptQr.get(payload.attemptId);
|
|
546
|
+
if (url) qrCache.delete(url);
|
|
547
|
+
attemptQr.delete(payload.attemptId);
|
|
548
|
+
value ??= {
|
|
549
|
+
status: 'failed',
|
|
550
|
+
operation: registration.operation,
|
|
551
|
+
...(registration.botId ? { botId: registration.botId } : {}),
|
|
552
|
+
message: 'Registration was cancelled.',
|
|
553
|
+
};
|
|
554
|
+
}
|
|
421
555
|
} else if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
|
|
422
556
|
if (typeof controller.bindCredentials !== 'function') {
|
|
423
557
|
throw new Error('Credential binding is unavailable');
|
|
@@ -108,7 +108,7 @@ export class DiscordApi {
|
|
|
108
108
|
headers: {
|
|
109
109
|
authorization: `Bot ${this.#token}`,
|
|
110
110
|
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.16.0)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|