dsh-lark-agent 0.1.2 → 0.2.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
@@ -72,7 +72,11 @@ The Feishu entry works only while the host is online: not when the app is closed
72
72
 
73
73
  | In Feishu, you… | The assistant… |
74
74
  | --- | --- |
75
- | Send a request | Shows a `Typing` reaction while it works (on by default for new bots), then replies in segments |
75
+ | Send a request | Works on it with Harness's own tools (web search, reading pages, workspace files and more), streaming the reply live into one card, with its thinking and tool steps folded inside |
76
+ | Ask for something that needs your OK | Sends a confirmation card: tap **允许一次** (allow once) or **拒绝** (deny); no answer within 5 minutes means it won't run |
77
+ | Get asked to choose by Harness | Sends a choice card: tap an option, or fill it in and submit |
78
+ | Use `/permission`, or 权限模式 in the model settings card | Switches between read-only, workspace edits and full access, like the app's permission selector; full access stops asking |
79
+ | Ask for the result file | Sends files from its workspace straight to the chat |
76
80
  | Say "lead with the conclusion from now on" | Remembers the preference |
77
81
  | Keep chatting, or use `/sessions`, `/new` | Continues the topic, returns to an earlier one, or starts fresh |
78
82
  | Ask for a bar chart of some numbers | Replies with a native Feishu chart card |
@@ -80,7 +84,7 @@ The Feishu entry works only while the host is online: not when the app is closed
80
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`, opening that Feishu session in the app also shows a local preview |
81
85
  | Use `/model`, `/thinking` | Switches between the host's configured models and thinking depth |
82
86
 
83
- By default it answers only your own private chat. Sessions and memory stay on the computer running Harness; messages pass through Feishu and your model provider.
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.
84
88
 
85
89
  ## Good to know
86
90
 
@@ -6,6 +6,7 @@ export declare const BOT_MENU_COMMANDS: Readonly<{
6
6
  readonly 'dsh.sessions': "/sessions";
7
7
  readonly 'dsh.model': "/model";
8
8
  readonly 'dsh.thinking': "/thinking";
9
+ readonly 'dsh.permission': "/permission";
9
10
  }>;
10
11
  export type BotMenuCommand = (typeof BOT_MENU_COMMANDS)[keyof typeof BOT_MENU_COMMANDS];
11
12
  export declare function botMenuCommand(eventKey: unknown): BotMenuCommand | undefined;
package/dist/bot-menu.js CHANGED
@@ -6,6 +6,8 @@ export const BOT_MENU_COMMANDS = Object.freeze({
6
6
  'dsh.sessions': '/sessions',
7
7
  'dsh.model': '/model',
8
8
  'dsh.thinking': '/thinking',
9
+ // Optional: not in the default five-node menu; an owner may add it under 设置.
10
+ 'dsh.permission': '/permission',
9
11
  });
10
12
  export function botMenuCommand(eventKey) {
11
13
  if (typeof eventKey !== 'string' || !Object.hasOwn(BOT_MENU_COMMANDS, eventKey))
@@ -13,6 +13,7 @@ import { DirectChatsStore } from './direct-chats.js';
13
13
  import { ModelPreferencesStore } from './model-preferences.js';
14
14
  import { ArtifactStore } from './artifacts.js';
15
15
  import { LarkArtifactPublisher } from './artifact-publisher.js';
16
+ import { PermissionPreferencesStore } from './permission-preferences.js';
16
17
  /** A failed close is still an owned service, never a disposable startup error. */
17
18
  export class BotServiceStartError extends Error {
18
19
  service;
@@ -36,6 +37,7 @@ export async function createBotService(ctx, config, onHealth = () => undefined,
36
37
  SessionStore.open(config.stateDir), MemoryStore.open(config.stateDir),
37
38
  config.cardInteractions ? CardActions.open(config.stateDir, JSON.stringify([config.profile, config.botOpenId])) : undefined,
38
39
  DirectChatsStore.open(config.stateDir), ModelPreferencesStore.open(config.stateDir), ArtifactStore.open(config.stateDir),
40
+ PermissionPreferencesStore.open(config.stateDir),
39
41
  ];
40
42
  // An opener may create signing state: finish all openers before releasing
41
43
  // ownership after a sibling fails, so no old writer outlives the lock.
@@ -43,7 +45,7 @@ export async function createBotService(ctx, config, onHealth = () => undefined,
43
45
  const failed = openedStores.find(result => result.status === 'rejected');
44
46
  if (failed?.status === 'rejected')
45
47
  throw failed.reason;
46
- const [sessions, memories, actions, directChats, modelPreferences, artifacts] = await Promise.all(openingStores);
48
+ const [sessions, memories, actions, directChats, modelPreferences, artifacts, permissionPreferences] = await Promise.all(openingStores);
47
49
  if (startup?.directChat) {
48
50
  const { openId, chatId } = startup.directChat;
49
51
  if (!config.users.some(user => user.openId === openId && user.direct))
@@ -59,7 +61,7 @@ export async function createBotService(ctx, config, onHealth = () => undefined,
59
61
  ? new LarkCli({ profile: config.profile, executable: config.executable, eventKey: 'application.bot.menu_v6' }) : undefined;
60
62
  const progressTransport = config.loadingReaction
61
63
  ? new LarkCli({ profile: config.profile, executable: config.executable, commandTimeoutMs: 5_000 }) : undefined;
62
- const bridge = new BotBridge(config, new DshRuntime(ctx), transport, ledger, { sessions, memories, actions, directChats, modelPreferences, artifacts }, event => console.info(`[im-larkbot] ${JSON.stringify(event)}`), progressTransport, new LarkArtifactPublisher(config.profile, config.botOpenId, { executable: config.executable }));
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 }));
63
65
  let stopped = false, started = false, healthy = false;
64
66
  const readiness = { messages: false, cards: !callbacks, menu: !menu };
65
67
  let stopping;
package/dist/bridge.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { BotConfig } from './config.js';
2
- import type { AgentPolicy, RuntimeSession, ModelSelection, ModelCatalog } from './dsh-runtime.js';
2
+ import type { AgentPolicy, RuntimeSession, ModelSelection, ModelCatalog, PermissionOption } from './dsh-runtime.js';
3
3
  import type { MessageLedger } from './ledger.js';
4
4
  import { type MemoryStore } from './memory-store.js';
5
5
  import { type SessionStore } from './session-store.js';
@@ -7,6 +7,7 @@ import type { CardActions } from './card-actions.js';
7
7
  import { type ReplyPayload } from './replies.js';
8
8
  import { type ProgressTransport } from './progress.js';
9
9
  import { type ModelPreferencesStore } from './model-preferences.js';
10
+ import type { PermissionPreferencesStore } from './permission-preferences.js';
10
11
  import type { ArtifactStore } from './artifacts.js';
11
12
  import type { ArtifactPublisher } from './artifact-publisher.js';
12
13
  export interface Runtime {
@@ -14,6 +15,7 @@ export interface Runtime {
14
15
  resume(policy: AgentPolicy, id: string): Promise<RuntimeSession>;
15
16
  catalog?(): Promise<ModelCatalog>;
16
17
  resolveModelSelection?(selection?: ModelSelection): Promise<ModelSelection>;
18
+ permissionOptions?(): PermissionOption[];
17
19
  }
18
20
  export interface BotState {
19
21
  sessions: Pick<SessionStore, 'current' | 'list' | 'add' | 'activate' | 'archive' | 'rename' | 'touch'> & Partial<Pick<SessionStore, 'rebindNative'>>;
@@ -25,6 +27,7 @@ export interface BotState {
25
27
  record(openId: string, chatId: string): Promise<void>;
26
28
  };
27
29
  artifacts?: Pick<ArtifactStore, 'add' | 'get' | 'list' | 'claimPublish' | 'publication' | 'submit'>;
30
+ permissionPreferences?: Pick<PermissionPreferencesStore, 'get' | 'set'>;
28
31
  }
29
32
  export interface Transport {
30
33
  reply(messageId: string, text: string, options: {
@@ -50,6 +53,18 @@ export interface Transport {
50
53
  idempotencyKey: string;
51
54
  }): Promise<string>;
52
55
  updateFeedback?(messageId: string, payload: ReplyPayload): Promise<void>;
56
+ createCardEntity?(card: Record<string, unknown>): Promise<string>;
57
+ replyCardEntity?(messageId: string, cardId: string, options: {
58
+ idempotencyKey: string;
59
+ inThread: boolean;
60
+ }): Promise<void>;
61
+ streamCardText?(cardId: string, elementId: string, content: string, sequence: number): Promise<void>;
62
+ updateCardEntity?(cardId: string, card: Record<string, unknown>, sequence: number): Promise<void>;
63
+ replyFile?(messageId: string, path: string, options: {
64
+ idempotencyKey: string;
65
+ inThread: boolean;
66
+ image: boolean;
67
+ }): Promise<void>;
53
68
  }
54
69
  export type Audit = (event: {
55
70
  stage: string;
@@ -76,12 +91,29 @@ export declare class BotBridge {
76
91
  private readonly failedDisposals;
77
92
  private cardActionsReady;
78
93
  private readonly progress;
94
+ private readonly interactions;
95
+ private liveUnavailableUntil;
79
96
  constructor(config: BotConfig, runtime: Runtime, transport: Transport, ledger: MessageLedger, state: BotState, audit?: Audit, progressTransport?: ProgressTransport, artifactPublisher?: ArtifactPublisher | undefined);
80
97
  setCardActionsReady(ready: boolean): void;
81
98
  private parseIncoming;
82
99
  accept(raw: unknown): Promise<void>;
83
100
  private acceptMessage;
84
101
  private process;
102
+ /** Whether this message's reply streams into a live card, which makes the typing reaction redundant. */
103
+ private streams;
104
+ private liveTransport;
105
+ private canInteract;
106
+ private issueAction;
107
+ /** Ask the owner about one Harness approval. Anything but an explicit allow denies the call. */
108
+ private approve;
109
+ /** Ask the owner a Harness question. Without an answer the host gets an error and continues. */
110
+ private ask;
111
+ private waitForAnswer;
112
+ private openInteraction;
113
+ private closeInteraction;
114
+ private answerInteraction;
115
+ /** Send files the host's delivery tool presented, from this user's workspace only. */
116
+ private deliverFiles;
85
117
  private commandFeedback;
86
118
  private context;
87
119
  private sessionContext;
@@ -90,6 +122,15 @@ export declare class BotBridge {
90
122
  private bindRestoredSession;
91
123
  private unload;
92
124
  private command;
125
+ private harness;
126
+ /** The host's presets, from the live session when there is one. Undefined when the host offers none. */
127
+ private permissionView;
128
+ /** What this chat runs: the live session's preset, else the chat's saved choice. */
129
+ private permissionChoice;
130
+ /** Owner-only: switch this private chat's Harness permission preset, like the App's selector. */
131
+ private permissionCommand;
132
+ /** Bring a newly opened harness session to the chat's saved permission preset. */
133
+ private applyPermission;
93
134
  private modelState;
94
135
  private modelCommand;
95
136
  private commandPanel;