dsh-lark-agent 0.3.2 → 0.4.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.md CHANGED
@@ -30,8 +30,6 @@ Before you start:
30
30
 
31
31
  In the sidebar, open **插件 → 添加插件** (Plugins → Add plugin), enter `dsh-lark-agent`, click **安装** (Install), then **立即启用** (Enable now). Restart if the app asks.
32
32
 
33
- <img src="https://raw.githubusercontent.com/Nirvana-Jie/dsh-lark-agent/main/assets/readme/desktop-install-02.jpg" width="720" alt="The Add plugin dialog in the DeepSeek Harness desktop app">
34
-
35
33
  ### Install with the `dsh` CLI
36
34
 
37
35
  [`dsh plugin`](https://www.npmjs.com/package/@deepseek-ai/dsh) runs pnpm, so pnpm must be on your PATH:
@@ -68,7 +66,7 @@ Button labels are quoted in Chinese, as they appear, with an English gloss.
68
66
  3. Click **打开飞书私聊** (open private chat) and, from the account that scanned, send the pairing word (`DSH-…`, valid for 10 minutes) as a plain-text message on its own, exactly as shown. If asked, confirm with **这是我,完成连接** (this is me).
69
67
  4. When the page shows **已连接** (connected), send the bot a message.
70
68
 
71
- The Feishu entry works only while the host is online: not when the app is closed, the web host is stopped, or the computer sleeps.
69
+ The connection's model check does not create a “飞书连接测试” conversation in Harness. The Feishu entry works only while the host is online: not when the app is closed, the web host is stopped, or the computer sleeps.
72
70
 
73
71
  ## What it can do
74
72
 
@@ -80,7 +78,7 @@ The Feishu entry works only while the host is online: not when the app is closed
80
78
  | Use `/permission`, or **设置 → 权限模式** (Settings → Permission mode) in the bottom menu | Switches between read-only, workspace edits and full access, like the app's permission selector; full access stops asking. The card you tap updates in place and turns green when done |
81
79
  | Ask for the result file | Sends files from its workspace straight to the chat |
82
80
  | Say "lead with the conclusion from now on" | Remembers the preference |
83
- | Keep chatting, or use `/sessions`, `/new` | Continues the topic, returns to an earlier one, or starts fresh |
81
+ | Continue inside a topic, or use `/sessions`, `/new` | Keeps each conversation in its own native Feishu topic; reply beneath a new topic's entry card, or use the searchable, paged dropdown to reopen an earlier topic |
84
82
  | Ask for a bar chart of some numbers | Replies with a native Feishu chart card |
85
83
  | Ask it to let you pick between options first | Sends a form card and continues once you submit |
86
84
  | Ask for an architecture diagram or a report | Creates a Feishu doc (with a whiteboard for diagrams) shared read-only with you alone; in App `0.2.1-rc.2` / `rc.3`, opening that Feishu session in the app also shows a local preview |
@@ -88,6 +86,8 @@ The Feishu entry works only while the host is online: not when the app is closed
88
86
 
89
87
  By default it answers only your own private chat. In that chat the assistant is your Harness itself: the tools it can use and the actions that need your confirmation follow the permission settings on your computer. Sessions and memory stay on the computer running Harness; messages pass through Feishu and your model provider.
90
88
 
89
+ A new request in the main chat opens a topic; continue inside that topic to keep its context. Authorized group chats work the same way and still require an @mention on each message. `/new` opens another topic and keeps the earlier ones available.
90
+
91
91
  ## Good to know
92
92
 
93
93
  - **More docs**: commands, privacy, upgrades, the chat bottom menu, team deployment and troubleshooting are in [DEVELOPMENT.md](https://github.com/Nirvana-Jie/dsh-lark-agent/blob/main/DEVELOPMENT.md) (Chinese).
@@ -10,6 +10,12 @@ export declare const BOT_MENU_COMMANDS: Readonly<{
10
10
  }>;
11
11
  export type BotMenuCommand = (typeof BOT_MENU_COMMANDS)[keyof typeof BOT_MENU_COMMANDS];
12
12
  export declare function botMenuCommand(eventKey: unknown): BotMenuCommand | undefined;
13
+ /** A menu entry as the owner adds it in the developer console. */
14
+ export interface BotMenuEntry {
15
+ label: string;
16
+ eventKey: string;
17
+ parent?: string;
18
+ }
13
19
  export interface BotMenuNode {
14
20
  menu_id: string;
15
21
  parent_menu_id?: string;
@@ -32,6 +38,12 @@ export declare function buildBotMenuAbility(): {
32
38
  bot_menus: BotMenuNode[];
33
39
  };
34
40
  };
41
+ /**
42
+ * The entries a published menu lacks, by label. A menu configured by hand
43
+ * before a release added an entry stays without it; none is reported when
44
+ * no dsh entry was configured at all.
45
+ */
46
+ export declare function missingBotMenuEntries(eventKeys: readonly string[]): BotMenuEntry[];
35
47
  /** Pure review/export plan: this module never invokes an API or changes an app. */
36
48
  export declare function buildBotMenuPlan(appId: string): {
37
49
  scope: string;
package/dist/bot-menu.js CHANGED
@@ -29,6 +29,20 @@ export function buildBotMenuAbility() {
29
29
  { menu_id: 'dsh_permission', parent_menu_id: 'dsh_settings', sort: 2, default_name: '权限模式', menu_content_type: 2, event_key: 'dsh.permission' },
30
30
  ] } };
31
31
  }
32
+ /**
33
+ * The entries a published menu lacks, by label. A menu configured by hand
34
+ * before a release added an entry stays without it; none is reported when
35
+ * no dsh entry was configured at all.
36
+ */
37
+ export function missingBotMenuEntries(eventKeys) {
38
+ const nodes = buildBotMenuAbility().bot.bot_menus, entries = nodes.filter(node => node.event_key);
39
+ if (!entries.some(node => eventKeys.includes(node.event_key)))
40
+ return [];
41
+ return entries.filter(node => !eventKeys.includes(node.event_key)).map(node => {
42
+ const parent = nodes.find(candidate => candidate.menu_id === node.parent_menu_id)?.default_name;
43
+ return { label: node.default_name, eventKey: node.event_key, ...(parent ? { parent } : {}) };
44
+ });
45
+ }
32
46
  /** Pure review/export plan: this module never invokes an API or changes an app. */
33
47
  export function buildBotMenuPlan(appId) {
34
48
  if (typeof appId !== 'string' || !/^cli_[A-Za-z0-9_-]{1,128}$/.test(appId))
@@ -14,6 +14,7 @@ import { ModelPreferencesStore } from './model-preferences.js';
14
14
  import { ArtifactStore } from './artifacts.js';
15
15
  import { LarkArtifactPublisher } from './artifact-publisher.js';
16
16
  import { PermissionPreferencesStore } from './permission-preferences.js';
17
+ import { TopicStore } from './topic-store.js';
17
18
  /** A failed close is still an owned service, never a disposable startup error. */
18
19
  export class BotServiceStartError extends Error {
19
20
  service;
@@ -38,6 +39,7 @@ export async function createBotService(ctx, config, onHealth = () => undefined,
38
39
  config.cardInteractions ? CardActions.open(config.stateDir, JSON.stringify([config.profile, config.botOpenId])) : undefined,
39
40
  DirectChatsStore.open(config.stateDir), ModelPreferencesStore.open(config.stateDir), ArtifactStore.open(config.stateDir),
40
41
  PermissionPreferencesStore.open(config.stateDir),
42
+ TopicStore.open(config.stateDir),
41
43
  ];
42
44
  // An opener may create signing state: finish all openers before releasing
43
45
  // ownership after a sibling fails, so no old writer outlives the lock.
@@ -45,7 +47,7 @@ export async function createBotService(ctx, config, onHealth = () => undefined,
45
47
  const failed = openedStores.find(result => result.status === 'rejected');
46
48
  if (failed?.status === 'rejected')
47
49
  throw failed.reason;
48
- const [sessions, memories, actions, directChats, modelPreferences, artifacts, permissionPreferences] = await Promise.all(openingStores);
50
+ const [sessions, memories, actions, directChats, modelPreferences, artifacts, permissionPreferences, topics] = await Promise.all(openingStores);
49
51
  if (startup?.directChat) {
50
52
  const { openId, chatId } = startup.directChat;
51
53
  if (!config.users.some(user => user.openId === openId && user.direct))
@@ -61,7 +63,7 @@ export async function createBotService(ctx, config, onHealth = () => undefined,
61
63
  ? new LarkCli({ profile: config.profile, executable: config.executable, eventKey: 'application.bot.menu_v6' }) : undefined;
62
64
  const progressTransport = config.loadingReaction
63
65
  ? new LarkCli({ profile: config.profile, executable: config.executable, commandTimeoutMs: 5_000 }) : undefined;
64
- const bridge = new BotBridge(config, new DshRuntime(ctx), transport, ledger, { sessions, memories, actions, directChats, modelPreferences, artifacts, permissionPreferences }, event => console.info(`[im-larkbot] ${JSON.stringify(event)}`), progressTransport, new LarkArtifactPublisher(config.profile, config.botOpenId, { executable: config.executable }));
66
+ const bridge = new BotBridge(config, new DshRuntime(ctx), transport, ledger, { sessions, memories, actions, directChats, modelPreferences, artifacts, permissionPreferences, topics }, event => console.info(`[im-larkbot] ${JSON.stringify(event)}`), progressTransport, new LarkArtifactPublisher(config.profile, config.botOpenId, { executable: config.executable }));
65
67
  let stopped = false, started = false, healthy = false;
66
68
  const readiness = { messages: false, cards: !callbacks, menu: !menu };
67
69
  let stopping;
package/dist/bridge.d.ts CHANGED
@@ -10,6 +10,7 @@ import { type ModelPreferencesStore } from './model-preferences.js';
10
10
  import type { PermissionPreferencesStore } from './permission-preferences.js';
11
11
  import type { ArtifactStore } from './artifacts.js';
12
12
  import type { ArtifactPublisher } from './artifact-publisher.js';
13
+ import { type TopicStore, type TopicReceipt } from './topic-store.js';
13
14
  export interface Runtime {
14
15
  create(policy: AgentPolicy): Promise<RuntimeSession>;
15
16
  resume(policy: AgentPolicy, id: string): Promise<RuntimeSession>;
@@ -32,6 +33,7 @@ export interface BotState {
32
33
  };
33
34
  artifacts?: Pick<ArtifactStore, 'add' | 'get' | 'list' | 'claimPublish' | 'publication' | 'submit'>;
34
35
  permissionPreferences?: Pick<PermissionPreferencesStore, 'get' | 'set'>;
36
+ topics?: Pick<TopicStore, 'get' | 'list' | 'record' | 'occupied'>;
35
37
  }
36
38
  export interface Transport {
37
39
  reply(messageId: string, text: string, options: {
@@ -69,6 +71,15 @@ export interface Transport {
69
71
  inThread: boolean;
70
72
  image: boolean;
71
73
  }): Promise<void>;
74
+ openTopic?(target: {
75
+ chatId: string;
76
+ messageId?: string;
77
+ }, title: string, options: {
78
+ idempotencyKey: string;
79
+ chatType?: 'p2p' | 'group';
80
+ textOnly?: boolean;
81
+ }): Promise<TopicReceipt>;
82
+ topicLink?(messageId: string): Promise<string | undefined>;
72
83
  }
73
84
  export type Audit = (event: {
74
85
  stage: string;
@@ -97,6 +108,8 @@ export declare class BotBridge {
97
108
  private readonly progress;
98
109
  private readonly interactions;
99
110
  private liveUnavailableUntil;
111
+ private readonly topicCreations;
112
+ private readonly topicResumes;
100
113
  constructor(config: BotConfig, runtime: Runtime, transport: Transport, ledger: MessageLedger, state: BotState, audit?: Audit, progressTransport?: ProgressTransport, artifactPublisher?: ArtifactPublisher | undefined);
101
114
  setCardActionsReady(ready: boolean): void;
102
115
  private parseIncoming;
@@ -131,8 +144,19 @@ export declare class BotBridge {
131
144
  /** Redraw a stale card with the current state, without running its command. */
132
145
  private refreshPanel;
133
146
  private commandFeedback;
147
+ private topicsEnabled;
148
+ private topicScope;
149
+ private newTopicKey;
150
+ private entryKey;
151
+ private reserveTopic;
152
+ private createTopic;
134
153
  private context;
135
154
  private sessionContext;
155
+ private catalogContext;
156
+ /** Only this user's chat and current disclosure epoch participate in the picker. */
157
+ private topicSessions;
158
+ private continueTopic;
159
+ private topicNavigation;
136
160
  private policy;
137
161
  private ensureSession;
138
162
  private bindRestoredSession;
@@ -154,6 +178,7 @@ export declare class BotBridge {
154
178
  private permissionCommand;
155
179
  /** Bring a newly opened harness session to the chat's choice, or a new one without a choice to the App's default. */
156
180
  private applyPermission;
181
+ private permissionContext;
157
182
  /** A preset changed in the App on this chat's live session becomes the chat's choice, so new sessions keep it. */
158
183
  private adoptPermission;
159
184
  private modelState;