@xmanrui/dsh-im 4.28.0 → 4.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +9 -7
- package/README.md +9 -7
- package/lib/client.js +370 -115
- package/lib/index.js +298 -298
- package/package.json +5 -1
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/voice-editor.js +115 -0
- package/plugin-src/client/channels/feishu/voice-settings.js +71 -0
- package/plugin-src/client/delivery-settings.js +8 -0
- package/plugin-src/client/i18n.js +16 -0
- package/plugin-src/client/styles.js +11 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +12 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +21 -0
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/shared/production.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +1 -1
- package/plugin-src/host/channels/wecom-app/production.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +22 -5
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +165 -12
- package/src/channels/feishu/feishu-cards.mjs +4 -2
- package/src/channels/feishu/feishu-runtime.mjs +16 -0
- package/src/channels/feishu/multi-bot-controller.mjs +22 -0
- package/src/channels/feishu/plugin-config-store.mjs +2 -0
- package/src/channels/feishu/voice-config.mjs +34 -0
- package/src/channels/feishu/voice.mjs +134 -0
- package/src/channels/shared/bot-workspace-store.mjs +30 -10
- package/src/channels/shared/default-workspace.mjs +6 -3
- package/src/channels/shared/harness-approval.mjs +24 -17
- package/src/channels/shared/harness-session-binding.mjs +4 -0
- package/src/channels/weixin/weixin-api.mjs +3 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t } from './i18n.mjs';
|
|
2
2
|
import { atConnectionStage, connectionStageError, extractConnectionEvidence } from './connection-error.mjs';
|
|
3
3
|
import { validateBotAlias, withBotAlias } from './bot-alias.mjs';
|
|
4
|
-
import { defaultImWorkspace, sameWorkspacePath } from './default-workspace.mjs';
|
|
4
|
+
import { defaultImWorkspace, ensureImWorkspaceDirectory, sameWorkspacePath } from './default-workspace.mjs';
|
|
5
5
|
import {
|
|
6
6
|
mkdir,
|
|
7
7
|
readFile,
|
|
@@ -367,13 +367,14 @@ async function writeStoredDocument(path, document) {
|
|
|
367
367
|
await rename(temporary, path);
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
export async function validateWorkspacePath(value) {
|
|
370
|
+
export async function validateWorkspacePath(value, { ungroupedWorkspace } = {}) {
|
|
371
371
|
if (typeof value !== 'string' || !value.trim() || !isAbsolute(value.trim())) {
|
|
372
372
|
const error = new Error('工作区必须是绝对路径。');
|
|
373
373
|
error.code = 'workspace-not-absolute';
|
|
374
374
|
throw error;
|
|
375
375
|
}
|
|
376
376
|
const workspace = resolve(value.trim());
|
|
377
|
+
await ensureImWorkspaceDirectory(workspace, ungroupedWorkspace);
|
|
377
378
|
let info;
|
|
378
379
|
try {
|
|
379
380
|
info = await stat(workspace);
|
|
@@ -393,6 +394,7 @@ export async function validateWorkspacePath(value) {
|
|
|
393
394
|
export class BotWorkspaceStore {
|
|
394
395
|
#path;
|
|
395
396
|
#defaultWorkspace;
|
|
397
|
+
#ungroupedWorkspace;
|
|
396
398
|
#version = 1;
|
|
397
399
|
#workspaces = {};
|
|
398
400
|
#agentPresets = {};
|
|
@@ -414,10 +416,11 @@ export class BotWorkspaceStore {
|
|
|
414
416
|
#writeQueue = Promise.resolve();
|
|
415
417
|
#botQueues = new Map();
|
|
416
418
|
|
|
417
|
-
constructor(path, { defaultWorkspace = defaultImWorkspace() } = {}) {
|
|
419
|
+
constructor(path, { defaultWorkspace = defaultImWorkspace(), ungroupedWorkspace } = {}) {
|
|
418
420
|
if (typeof path !== 'string' || !path) throw new TypeError('workspace store path is required');
|
|
419
421
|
this.#path = path;
|
|
420
422
|
this.#defaultWorkspace = resolve(defaultWorkspace);
|
|
423
|
+
this.#ungroupedWorkspace = ungroupedWorkspace;
|
|
421
424
|
}
|
|
422
425
|
|
|
423
426
|
async load() {
|
|
@@ -691,11 +694,20 @@ export class BotWorkspaceStore {
|
|
|
691
694
|
const createsBot = !this.#workspaces[id];
|
|
692
695
|
const initializesAccessPolicy = initialAccessPolicy !== undefined
|
|
693
696
|
&& !Object.hasOwn(this.#accessPolicies, id);
|
|
697
|
+
const accessPolicy = initializesAccessPolicy
|
|
698
|
+
? validateAccessPolicy(initialAccessPolicy)
|
|
699
|
+
: undefined;
|
|
700
|
+
const agentPreset = createsBot ? validateAgentPresetId(defaultAgentPreset) : null;
|
|
701
|
+
// Resolve saved bot/conversation choices before preparing the shared IM
|
|
702
|
+
// directory. Merely loading a channel must not create an unused fallback.
|
|
703
|
+
const selectedWorkspaces = new Set([
|
|
704
|
+
this.#workspaces[id] ?? initialWorkspace,
|
|
705
|
+
...Object.values(this.#conversationWorkspaces[id] ?? {}),
|
|
706
|
+
]);
|
|
707
|
+
for (const selected of selectedWorkspaces) {
|
|
708
|
+
await ensureImWorkspaceDirectory(selected, this.#ungroupedWorkspace);
|
|
709
|
+
}
|
|
694
710
|
if (createsBot || initializesAccessPolicy) {
|
|
695
|
-
const accessPolicy = initializesAccessPolicy
|
|
696
|
-
? validateAccessPolicy(initialAccessPolicy)
|
|
697
|
-
: undefined;
|
|
698
|
-
const agentPreset = createsBot ? validateAgentPresetId(defaultAgentPreset) : null;
|
|
699
711
|
const hadAgentPreset = Object.hasOwn(this.#agentPresets, id);
|
|
700
712
|
const previousAgentPreset = this.#agentPresets[id];
|
|
701
713
|
const nextAccessPolicies = initializesAccessPolicy
|
|
@@ -744,7 +756,7 @@ export class BotWorkspaceStore {
|
|
|
744
756
|
error.code = 'workspace-bot-not-found';
|
|
745
757
|
throw error;
|
|
746
758
|
}
|
|
747
|
-
const workspace = await validateWorkspacePath(value);
|
|
759
|
+
const workspace = await validateWorkspacePath(value, { ungroupedWorkspace: this.#ungroupedWorkspace });
|
|
748
760
|
return this.#enqueue(id, async () => {
|
|
749
761
|
if (!this.has(id)
|
|
750
762
|
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
|
@@ -965,7 +977,9 @@ export class BotWorkspaceStore {
|
|
|
965
977
|
}
|
|
966
978
|
// Validate before queueing so an invalid path fails without disturbing the
|
|
967
979
|
// conversation's current workspace or its session.
|
|
968
|
-
const workspace = value === null ? null : await validateWorkspacePath(value
|
|
980
|
+
const workspace = value === null ? null : await validateWorkspacePath(value, {
|
|
981
|
+
ungroupedWorkspace: this.#ungroupedWorkspace,
|
|
982
|
+
});
|
|
969
983
|
return this.#enqueue(id, async () => {
|
|
970
984
|
const assertCurrentSwitch = () => {
|
|
971
985
|
if (!this.has(id)
|
|
@@ -981,6 +995,10 @@ export class BotWorkspaceStore {
|
|
|
981
995
|
}
|
|
982
996
|
};
|
|
983
997
|
assertCurrentSwitch();
|
|
998
|
+
if (workspace === null) {
|
|
999
|
+
await ensureImWorkspaceDirectory(this.workspaceFor(id), this.#ungroupedWorkspace);
|
|
1000
|
+
assertCurrentSwitch();
|
|
1001
|
+
}
|
|
984
1002
|
const previousOverrides = this.#conversationWorkspaces[id];
|
|
985
1003
|
const previous = previousOverrides?.[key];
|
|
986
1004
|
const hadOverride = Boolean(previousOverrides)
|
|
@@ -1065,7 +1083,9 @@ export class BotWorkspaceStore {
|
|
|
1065
1083
|
);
|
|
1066
1084
|
}
|
|
1067
1085
|
};
|
|
1068
|
-
const workspace = await canonicalWorkspacePath(await validateWorkspacePath(value
|
|
1086
|
+
const workspace = await canonicalWorkspacePath(await validateWorkspacePath(value, {
|
|
1087
|
+
ungroupedWorkspace: this.#ungroupedWorkspace,
|
|
1088
|
+
}));
|
|
1069
1089
|
return this.#enqueue(id, async () => {
|
|
1070
1090
|
if (!this.has(id)
|
|
1071
1091
|
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
|
@@ -21,8 +21,11 @@ export async function sameWorkspacePath(left, right) {
|
|
|
21
21
|
export async function prepareBotWorkspace(config = {}) {
|
|
22
22
|
const ungroupedWorkspace = defaultImWorkspace(config);
|
|
23
23
|
const defaultWorkspace = resolve(config.workspace ?? ungroupedWorkspace);
|
|
24
|
-
if (await sameWorkspacePath(defaultWorkspace, ungroupedWorkspace)) {
|
|
25
|
-
await mkdir(defaultWorkspace, { recursive: true });
|
|
26
|
-
}
|
|
27
24
|
return { defaultWorkspace, ungroupedWorkspace };
|
|
28
25
|
}
|
|
26
|
+
|
|
27
|
+
export async function ensureImWorkspaceDirectory(workspace, ungroupedWorkspace) {
|
|
28
|
+
if (await sameWorkspacePath(workspace, ungroupedWorkspace)) {
|
|
29
|
+
await mkdir(workspace, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -272,6 +272,7 @@ export class HarnessApprovalQueue {
|
|
|
272
272
|
requiresMention: context.requiresMention === true,
|
|
273
273
|
send,
|
|
274
274
|
render,
|
|
275
|
+
onResolved: typeof context.onResolved === 'function' ? context.onResolved : null,
|
|
275
276
|
text,
|
|
276
277
|
presented: false,
|
|
277
278
|
presentationTask: null,
|
|
@@ -328,8 +329,7 @@ export class HarnessApprovalQueue {
|
|
|
328
329
|
delivered = await presentationTask.then(() => true, () => false);
|
|
329
330
|
}
|
|
330
331
|
if (shouldNotify && delivered) {
|
|
331
|
-
pending.
|
|
332
|
-
await send(approvalOutcomeText(resolution.outcome)).catch(() => undefined);
|
|
332
|
+
await this.#notifyResolved(pending, resolution.outcome, send);
|
|
333
333
|
}
|
|
334
334
|
});
|
|
335
335
|
return true;
|
|
@@ -347,16 +347,14 @@ export class HarnessApprovalQueue {
|
|
|
347
347
|
{ signal: AbortSignal.timeout(5_000) },
|
|
348
348
|
);
|
|
349
349
|
pending.closedOutcome = 'rejected';
|
|
350
|
-
if (
|
|
351
|
-
pending
|
|
352
|
-
await pending.send(approvalOutcomeText('rejected')).catch(() => undefined);
|
|
350
|
+
if (pending.presented || pending.deliveryCompleted) {
|
|
351
|
+
await this.#notifyResolved(pending, 'rejected');
|
|
353
352
|
}
|
|
354
353
|
} catch (error) {
|
|
355
354
|
if (error?.code === 'interaction-not-pending') {
|
|
356
355
|
pending.closedOutcome = 'resolved';
|
|
357
|
-
if (
|
|
358
|
-
pending
|
|
359
|
-
await pending.send(t(APPROVAL_RESOLVED_TEXT)).catch(() => undefined);
|
|
356
|
+
if (pending.presented || pending.deliveryCompleted) {
|
|
357
|
+
await this.#notifyResolved(pending, 'resolved');
|
|
360
358
|
}
|
|
361
359
|
} else {
|
|
362
360
|
this.#logger.warn?.(`[dsh-im:${this.#label}] failed to reject a closing approval:`, error);
|
|
@@ -383,9 +381,8 @@ export class HarnessApprovalQueue {
|
|
|
383
381
|
pending.deliveryCompleted = true;
|
|
384
382
|
if (!pending.inactive) {
|
|
385
383
|
pending.presented = true;
|
|
386
|
-
} else if (pending.closedOutcome
|
|
387
|
-
pending.
|
|
388
|
-
await pending.send(approvalOutcomeText(pending.closedOutcome)).catch(() => undefined);
|
|
384
|
+
} else if (pending.closedOutcome) {
|
|
385
|
+
await this.#notifyResolved(pending, pending.closedOutcome);
|
|
389
386
|
}
|
|
390
387
|
} finally {
|
|
391
388
|
if (pending.presentationTask === task) pending.presentationTask = null;
|
|
@@ -401,9 +398,7 @@ export class HarnessApprovalQueue {
|
|
|
401
398
|
const send = pending.send;
|
|
402
399
|
const next = this.#remove(pending);
|
|
403
400
|
await this.#transition(next, async () => {
|
|
404
|
-
|
|
405
|
-
await send(t(APPROVAL_RESOLVED_TEXT)).catch(() => undefined);
|
|
406
|
-
}
|
|
401
|
+
await this.#notifyResolved(pending, 'resolved', send);
|
|
407
402
|
});
|
|
408
403
|
return;
|
|
409
404
|
}
|
|
@@ -417,12 +412,24 @@ export class HarnessApprovalQueue {
|
|
|
417
412
|
const send = pending.send;
|
|
418
413
|
const next = this.#remove(pending);
|
|
419
414
|
await this.#transition(next, async () => {
|
|
420
|
-
|
|
421
|
-
await send(approvalOutcomeText(outcome)).catch(() => undefined);
|
|
422
|
-
}
|
|
415
|
+
await this.#notifyResolved(pending, outcome, send);
|
|
423
416
|
});
|
|
424
417
|
}
|
|
425
418
|
|
|
419
|
+
async #notifyResolved(pending, outcome, send = pending.send) {
|
|
420
|
+
if (pending.resolutionNotified) return;
|
|
421
|
+
// Claim notification before awaiting the channel, so a concurrent close or
|
|
422
|
+
// resolved event cannot replace the confirmed result or notify twice.
|
|
423
|
+
pending.resolutionNotified = true;
|
|
424
|
+
const text = approvalOutcomeText(outcome);
|
|
425
|
+
try {
|
|
426
|
+
await pending.onResolved?.(text);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] failed to update a resolved approval:`, error);
|
|
429
|
+
}
|
|
430
|
+
await send(text).catch(() => undefined);
|
|
431
|
+
}
|
|
432
|
+
|
|
426
433
|
async #transition(next, work) {
|
|
427
434
|
let release;
|
|
428
435
|
const barrier = new Promise((resolve) => { release = resolve; });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
1
2
|
import { isAbsolute } from 'node:path';
|
|
2
3
|
|
|
3
4
|
const MAX_SESSION_ID_LENGTH = 256;
|
|
@@ -99,6 +100,9 @@ export async function adoptRegisteredWorkspaceSession(client, value, options = {
|
|
|
99
100
|
}
|
|
100
101
|
workspace = { path: summary.cwd };
|
|
101
102
|
}
|
|
103
|
+
if (await client.isUngroupedWorkspace?.(workspace.path)) {
|
|
104
|
+
await mkdir(workspace.path, { recursive: true });
|
|
105
|
+
}
|
|
102
106
|
const adopted = await client.rpc('session.create', {
|
|
103
107
|
...(workspace.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: workspace.path }),
|
|
104
108
|
sessionId,
|
|
@@ -504,8 +504,9 @@ function validateLoginResponse(value) {
|
|
|
504
504
|
return value;
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
507
|
+
export function createWeixinApi({ fetchImpl = fetch, uploadFetchImpl = fetchImpl } = {}) {
|
|
508
508
|
if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
|
|
509
|
+
if (typeof uploadFetchImpl !== 'function') throw new TypeError('uploadFetchImpl must be a function');
|
|
509
510
|
|
|
510
511
|
async function sendArtifact({
|
|
511
512
|
baseUrl,
|
|
@@ -566,7 +567,7 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
566
567
|
const ciphertextSize = aesEcbPaddedSize(file.bytes.byteLength);
|
|
567
568
|
let downloadParam;
|
|
568
569
|
try {
|
|
569
|
-
downloadParam = await uploadWeixinCdn(
|
|
570
|
+
downloadParam = await uploadWeixinCdn(uploadFetchImpl, uploadUrl, file.bytes, aesKey, { signal });
|
|
570
571
|
} catch (error) {
|
|
571
572
|
if (signal?.aborted) throw abortError(signal);
|
|
572
573
|
const status = Number(error?.status);
|