@xmanrui/dsh-im 0.6.0 → 0.7.1

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.
Files changed (42) hide show
  1. package/README.md +22 -4
  2. package/lib/client.js +649 -357
  3. package/lib/index.js +120 -111
  4. package/package.json +1 -1
  5. package/plugin-src/client/build.mjs +1 -1
  6. package/plugin-src/client/i18n.js +14 -0
  7. package/plugin-src/client/index.js +11 -3
  8. package/plugin-src/client/styles.js +49 -8
  9. package/plugin-src/client/workspace-directory-picker.js +230 -0
  10. package/plugin-src/client/workspace-editor.js +38 -65
  11. package/scripts/verify-package.mjs +5 -0
  12. package/src/channels/dingtalk/dingtalk-bridge.mjs +363 -9
  13. package/src/channels/dingtalk/harness-client.mjs +16 -310
  14. package/src/channels/discord/discord-api.mjs +1 -1
  15. package/src/channels/discord/discord-runtime.mjs +11 -1
  16. package/src/channels/discord/harness-client.mjs +10 -2
  17. package/src/channels/feishu/bridge.mjs +525 -51
  18. package/src/channels/feishu/feishu-runtime.mjs +41 -1
  19. package/src/channels/feishu/harness-client.mjs +16 -279
  20. package/src/channels/qq/harness-client.mjs +10 -2
  21. package/src/channels/qq/qq-bridge.mjs +383 -29
  22. package/src/channels/qq/qq-runtime.mjs +14 -3
  23. package/src/channels/shared/bot-workspace-store.mjs +185 -4
  24. package/src/channels/shared/harness-client.mjs +825 -0
  25. package/src/channels/shared/harness-question.mjs +85 -0
  26. package/src/channels/shared/harness-session-binding.mjs +110 -0
  27. package/src/channels/shared/text-harness-bridge.mjs +439 -25
  28. package/src/channels/shared/workspace-command.mjs +212 -16
  29. package/src/channels/shared/workspace-session.mjs +22 -9
  30. package/src/channels/slack/harness-client.mjs +10 -2
  31. package/src/channels/slack/slack-runtime.mjs +11 -1
  32. package/src/channels/telegram/harness-client.mjs +10 -2
  33. package/src/channels/telegram/telegram-runtime.mjs +15 -4
  34. package/src/channels/wecom/harness-client.mjs +10 -2
  35. package/src/channels/wecom/wecom-bridge.mjs +389 -15
  36. package/src/channels/wecom/wecom-runtime.mjs +6 -0
  37. package/src/channels/weixin/harness-client.mjs +16 -270
  38. package/src/channels/weixin/weixin-api.mjs +1 -1
  39. package/src/channels/weixin/weixin-bridge.mjs +406 -24
  40. package/src/channels/weixin/weixin-runtime.mjs +56 -7
  41. package/src/channels/whatsapp/harness-client.mjs +10 -2
  42. package/src/channels/whatsapp/whatsapp-runtime.mjs +1 -0
@@ -1,10 +1,37 @@
1
- import { mkdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ realpath,
5
+ rename,
6
+ stat,
7
+ unlink,
8
+ writeFile,
9
+ } from 'node:fs/promises';
2
10
  import { dirname, isAbsolute, resolve } from 'node:path';
3
11
 
4
12
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
5
13
 
6
14
  const EMPTY_DOCUMENT = Object.freeze({ version: 1, workspaces: Object.freeze({}) });
7
15
 
16
+ function workspaceSessionStale(message) {
17
+ const error = new Error(message);
18
+ error.code = WORKSPACE_SESSION_STALE;
19
+ return error;
20
+ }
21
+
22
+ async function canonicalWorkspacePath(value) {
23
+ return resolve(await realpath(value));
24
+ }
25
+
26
+ async function sameWorkspacePath(left, right) {
27
+ if (left === right) return true;
28
+ try {
29
+ return await canonicalWorkspacePath(left) === await canonicalWorkspacePath(right);
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
8
35
  function botIdOf(value) {
9
36
  if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
10
37
  throw new TypeError('Invalid bot id');
@@ -179,6 +206,72 @@ export class BotWorkspaceStore {
179
206
  });
180
207
  }
181
208
 
209
+ async bindWorkspaceSession(botId, value, {
210
+ conversationKey,
211
+ sessionId,
212
+ clearSessions,
213
+ setSession,
214
+ incarnation,
215
+ expectedGeneration,
216
+ } = {}) {
217
+ const id = botIdOf(botId);
218
+ if (typeof conversationKey !== 'string' || !conversationKey
219
+ || typeof sessionId !== 'string' || !sessionId) {
220
+ throw new TypeError('conversationKey and sessionId are required');
221
+ }
222
+ if (typeof clearSessions !== 'function' || typeof setSession !== 'function') {
223
+ throw new TypeError('session state callbacks are required');
224
+ }
225
+ if (!this.has(id)
226
+ || (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
227
+ const error = new Error('找不到要修改的机器人。');
228
+ error.code = 'workspace-bot-not-found';
229
+ throw error;
230
+ }
231
+ const workspace = await canonicalWorkspacePath(await validateWorkspacePath(value));
232
+ return this.#enqueue(id, async () => {
233
+ if (!this.has(id)
234
+ || (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
235
+ const error = new Error('找不到要修改的机器人。');
236
+ error.code = 'workspace-bot-not-found';
237
+ throw error;
238
+ }
239
+ if (expectedGeneration !== undefined
240
+ && expectedGeneration !== this.generationFor(id)) {
241
+ throw workspaceSessionStale(
242
+ 'The bot workspace changed before the session binding could be committed.',
243
+ );
244
+ }
245
+
246
+ if (!(await sameWorkspacePath(workspace, this.workspaceFor(id)))) {
247
+ const previous = this.#workspaces[id];
248
+ // Fence every session resolved before this transition, then remove
249
+ // the old workspace mappings before publishing the new workspace.
250
+ this.#generations.set(id, this.#freshGeneration());
251
+ await clearSessions();
252
+ this.#workspaces[id] = workspace;
253
+ try {
254
+ await this.#persist();
255
+ } catch (error) {
256
+ // Session mappings stay cleared and the advanced generation stays
257
+ // fenced. Restoring either could pair an old session with a state
258
+ // transition whose durable outcome is unknown.
259
+ this.#workspaces[id] = previous;
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ // This write remains inside the same bot transition as the workspace
265
+ // mutation, so another switch or bind cannot interleave between them.
266
+ await setSession(conversationKey, sessionId);
267
+ return {
268
+ workspace,
269
+ sessionId,
270
+ generation: this.#generations.get(id),
271
+ };
272
+ });
273
+ }
274
+
182
275
  async invalidateSessions(botId, { clearSessions } = {}) {
183
276
  const id = botIdOf(botId);
184
277
  return this.#enqueue(id, async () => {
@@ -417,20 +510,21 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
417
510
  }
418
511
  };
419
512
  }
420
- if (property === 'listWorkspaces' && typeof target.listWorkspaces === 'function') {
513
+ if ((property === 'listWorkspaces' || property === 'listWorkspaceSessions')
514
+ && typeof target[property] === 'function') {
421
515
  return async (...args) => {
422
516
  if (!isCurrentScope()) {
423
517
  const error = new Error('找不到要修改的机器人。');
424
518
  error.code = 'workspace-bot-not-found';
425
519
  throw error;
426
520
  }
427
- const paths = await target.listWorkspaces(...args);
521
+ const result = await target[property](...args);
428
522
  if (!isCurrentScope()) {
429
523
  const error = new Error('找不到要修改的机器人。');
430
524
  error.code = 'workspace-bot-not-found';
431
525
  throw error;
432
526
  }
433
- return paths;
527
+ return result;
434
528
  };
435
529
  }
436
530
  if (property === 'switchWorkspace') {
@@ -446,6 +540,62 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
446
540
  });
447
541
  };
448
542
  }
543
+ if (property === 'bindWorkspaceSession') {
544
+ return async (conversationKey, sessionId) => {
545
+ if (typeof conversationKey !== 'string' || !conversationKey
546
+ || typeof sessionId !== 'string' || !sessionId) {
547
+ throw new TypeError('conversationKey and sessionId are required');
548
+ }
549
+ if (!isCurrentScope()) {
550
+ const error = new Error('找不到要修改的机器人。');
551
+ error.code = 'workspace-bot-not-found';
552
+ throw error;
553
+ }
554
+ if (typeof target.adoptWorkspaceSession !== 'function') {
555
+ throw new TypeError('Harness does not support adopting workspace sessions');
556
+ }
557
+ const expectedGeneration = workspaces.generationFor(botId);
558
+ const adopted = await target.adoptWorkspaceSession(sessionId);
559
+ if (!isCurrentScope()) {
560
+ const error = new Error('找不到要修改的机器人。');
561
+ error.code = 'workspace-bot-not-found';
562
+ throw error;
563
+ }
564
+ if (expectedGeneration !== workspaces.generationFor(botId)) {
565
+ throw workspaceSessionStale(
566
+ 'The bot workspace changed while the session was being adopted.',
567
+ );
568
+ }
569
+ if (!adopted || typeof adopted !== 'object'
570
+ || adopted.sessionId !== sessionId || typeof adopted.workspace !== 'string') {
571
+ throw new TypeError('Harness returned an invalid adopted workspace session');
572
+ }
573
+ const bound = await workspaces.bindWorkspaceSession(botId, adopted.workspace, {
574
+ conversationKey,
575
+ sessionId,
576
+ clearSessions: () => state.clearSessions(),
577
+ setSession: (key, selectedSessionId) => state.setSession(key, selectedSessionId),
578
+ incarnation,
579
+ expectedGeneration,
580
+ });
581
+ if (!isCurrentScope()) {
582
+ const error = new Error('找不到要修改的机器人。');
583
+ error.code = 'workspace-bot-not-found';
584
+ throw error;
585
+ }
586
+ if (bound.generation !== workspaces.generationFor(botId)) {
587
+ throw workspaceSessionStale(
588
+ 'The bot workspace changed before the session binding completed.',
589
+ );
590
+ }
591
+ sessionGenerations.set(sessionId, bound.generation);
592
+ return {
593
+ ...adopted,
594
+ workspace: bound.workspace,
595
+ sessionId: bound.sessionId,
596
+ };
597
+ };
598
+ }
449
599
  if (property === 'createSession') {
450
600
  return async (options = {}) => {
451
601
  await workspaces.whenBotIdle(botId);
@@ -463,6 +613,37 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
463
613
  return sessionId;
464
614
  };
465
615
  }
616
+ if (property === 'workspaceSession') {
617
+ return (sessionId) => {
618
+ if (typeof sessionId !== 'string' || !sessionId) {
619
+ throw new TypeError('sessionId is required');
620
+ }
621
+ const generation = sessionGenerations.get(sessionId)
622
+ ?? workspaces.generationFor(botId);
623
+ // Transfer the mutable provenance entry into this immutable handle.
624
+ // A later handle for the same id captures its own generation instead
625
+ // of sharing deletion or rebinding state with this call.
626
+ sessionGenerations.delete(sessionId);
627
+ const isCurrentSession = () => isCurrentScope()
628
+ && generation === workspaces.generationFor(botId);
629
+ return Object.freeze({
630
+ sessionId,
631
+ async sessionExists(...args) {
632
+ if (!isCurrentSession()) return false;
633
+ const exists = await target.sessionExists(sessionId, ...args);
634
+ return isCurrentSession() && exists;
635
+ },
636
+ ask(...args) {
637
+ if (!isCurrentSession()) {
638
+ throw workspaceSessionStale(
639
+ 'The bot workspace changed before this prompt started.',
640
+ );
641
+ }
642
+ return target.ask(sessionId, ...args);
643
+ },
644
+ });
645
+ };
646
+ }
466
647
  if (property === 'sessionExists') {
467
648
  return (sessionId, ...args) => {
468
649
  if (!isCurrentScope()) return false;