dsh-lark-bot 0.6.0 → 0.7.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/dist/plugin.d.ts CHANGED
@@ -1,113 +1,700 @@
1
1
  import { Service, Context } from '@deepseek-ai/cordis';
2
+ import { NormalizedMessage, createLarkChannel, LarkChannel } from '@larksuite/channel';
2
3
 
3
- interface AppPaths {
4
- root: string;
5
- configFile: string;
6
- activeProfileFile: string;
7
- profileDir: (profile: string) => string;
8
- profilePath: (profile: string, ...parts: string[]) => string;
9
- sessionsFile: (profile: string) => string;
10
- sessionCatalogFile: (profile: string) => string;
11
- archivesDir: (profile: string) => string;
12
- workspacesFile: (profile: string) => string;
13
- mediaDir: (profile: string) => string;
14
- logsDir: (profile: string) => string;
15
- serviceDir: string;
16
- serviceEnvFile: string;
17
- serviceMetadataFile: string;
18
- serviceLogFile: (profile: string) => string;
19
- registryFile: string;
20
- locksDir: string;
21
- }
22
-
23
- type ServicePlatform = 'linux-systemd' | 'linux-portable' | 'darwin-launchd' | 'win32-task';
24
- type ServiceRuntimeState = 'running' | 'stopped' | 'error' | 'unknown';
25
- interface ServiceSpec {
26
- serviceName: string;
27
- label: string;
28
- profile: string;
29
- nodePath: string;
30
- cliJsPath: string;
31
- envFile: string;
32
- logFile: string;
33
- env: Record<string, string>;
4
+ type AgentEvent = {
5
+ type: 'system';
6
+ sessionId: string | undefined;
7
+ cwd: string | undefined;
8
+ model: string | undefined;
9
+ } | {
10
+ type: 'text';
11
+ delta: string;
12
+ } | {
13
+ type: 'final_text';
14
+ content: string;
15
+ } | {
16
+ type: 'thinking';
17
+ delta: string;
18
+ } | {
19
+ type: 'tool_use';
20
+ id: string;
21
+ name: string;
22
+ input: unknown;
23
+ } | {
24
+ type: 'tool_result';
25
+ id: string;
26
+ output: string;
27
+ isError: boolean;
28
+ } | {
29
+ type: 'usage';
30
+ inputTokens?: number;
31
+ outputTokens?: number;
32
+ costUsd?: number;
33
+ } | {
34
+ type: 'done';
35
+ sessionId: string | undefined;
36
+ terminationReason: 'normal' | 'interrupted' | 'timeout';
37
+ } | {
38
+ type: 'error';
39
+ message: string;
40
+ terminationReason: 'failed' | 'interrupted' | 'timeout';
41
+ };
42
+ type ApprovalOptionKind = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
43
+ interface ApprovalOption {
44
+ optionId: string;
45
+ name: string;
46
+ kind: ApprovalOptionKind;
47
+ }
48
+ interface ApprovalRequest {
49
+ id: string;
50
+ sessionId: string | undefined;
51
+ toolName: string;
52
+ reason: string | undefined;
53
+ options: readonly ApprovalOption[];
54
+ }
55
+ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled';
56
+ interface AgentRunOptions {
57
+ runId: string;
58
+ prompt: string;
59
+ cwd: string | undefined;
60
+ sessionId: string | undefined;
61
+ model: string | undefined;
62
+ images: readonly string[] | undefined;
63
+ stopGraceMs: number | undefined;
64
+ /** ACP approval channel: invoked when the agent requests a one-shot permission. */
65
+ onApprovalRequest?: (request: ApprovalRequest) => Promise<ApprovalOutcome>;
66
+ }
67
+ interface AgentRun {
68
+ readonly runId: string;
69
+ readonly events: AsyncIterable<AgentEvent>;
70
+ stop(): Promise<void>;
71
+ waitForExit(timeoutMs: number): Promise<boolean>;
72
+ }
73
+ interface AgentAvailability {
74
+ ok: boolean;
75
+ error: string | undefined;
76
+ version: string | undefined;
34
77
  }
35
- interface ServiceStatus {
78
+ interface AgentAdapter {
79
+ readonly id: string;
80
+ readonly displayName: string;
81
+ isAvailable(): Promise<boolean>;
82
+ checkAvailability(): Promise<AgentAvailability>;
83
+ run(options: AgentRunOptions): AgentRun;
84
+ /** Optional teardown hook called on bridge shutdown. */
85
+ dispose?(): Promise<void>;
86
+ }
87
+
88
+ type LarkTenant = 'feishu' | 'lark';
89
+ type AdapterMode = 'sdk' | 'acp' | 'headless';
90
+ interface RuntimeEnv {
91
+ home: string;
92
+ tenant: LarkTenant;
93
+ appId: string | undefined;
94
+ appSecret: string | undefined;
95
+ workspace: string | undefined;
96
+ dshCommand: string;
97
+ dshArgs: string[];
98
+ /** True when DSH_LARK_DSH_COMMAND / DSH_LARK_DSH_ARGS were set explicitly. */
99
+ dshExplicit: boolean;
100
+ adapterMode: AdapterMode;
101
+ provider: string;
102
+ model: string;
103
+ maxTokens: number | undefined;
104
+ runTimeoutMs: number;
105
+ stopGraceMs: number;
106
+ /** Max agent runs allowed concurrently per scope (default 2). */
107
+ scopeConcurrency: number;
108
+ /** Live messages kept per scope before overflow is archived (default 40). */
109
+ retentionMsgs: number;
110
+ /** Max archives retained per scope before pruning (default 50, 0 disables). */
111
+ archiveMax: number;
112
+ /** Archives older than this many days are pruned (default 90, 0 disables). */
113
+ archiveMaxAgeDays: number;
114
+ accessDefaultDeny: boolean;
115
+ eventFreshnessMs: number;
116
+ }
117
+
118
+ interface ActiveRunHandle {
119
+ runId: string;
120
+ stop(): Promise<void>;
121
+ }
122
+ /**
123
+ * Tracks every running agent run, keyed by scope. A scope can hold several
124
+ * concurrent runs (multi-agent collaboration); `interrupt` stops them all,
125
+ * while `interruptRun` targets one run by id.
126
+ */
127
+ declare class ActiveRuns {
128
+ private readonly runs;
129
+ set(scope: string, handle: ActiveRunHandle): void;
130
+ /** First active run for the scope, if any (backward-compatible accessor). */
131
+ get(scope: string): ActiveRunHandle | undefined;
132
+ list(scope: string): ActiveRunHandle[];
133
+ count(scope: string): number;
134
+ has(scope: string): boolean;
135
+ delete(scope: string, runId: string): boolean;
136
+ /** Stop every active run in the scope. Returns how many were interrupted. */
137
+ interrupt(scope: string): Promise<number>;
138
+ /** Stop one run by id. Returns whether the run existed. */
139
+ interruptRun(scope: string, runId: string): Promise<boolean>;
140
+ }
141
+
142
+ /**
143
+ * Pending approval registry. Cards are rendered by the caller; this store
144
+ * correlates card button clicks with the ACP `request_permission` promise and
145
+ * guarantees every request is settled when a run ends or is disposed.
146
+ */
147
+ declare class ApprovalRegistry {
148
+ private readonly pending;
149
+ register(scope: string, request: ApprovalRequest): Promise<ApprovalOutcome>;
150
+ resolve(scope: string, id: string, outcome: ApprovalOutcome): boolean;
151
+ /** Settle every pending approval for a scope (run end / dispose). */
152
+ settleAll(scope: string, outcome: ApprovalOutcome): number;
153
+ pendingCount(scope: string): number;
154
+ }
155
+
156
+ type CardDensity = 'compact' | 'standard' | 'detailed';
157
+
158
+ /** Per-scope card density overrides on top of a default. */
159
+ declare class DensityStore {
160
+ private readonly defaultDensity;
161
+ private readonly overrides;
162
+ constructor(defaultDensity?: CardDensity);
163
+ get(scope: string): CardDensity;
164
+ set(scope: string, density: CardDensity): void;
165
+ clear(scope: string): boolean;
166
+ }
167
+
168
+ /**
169
+ * Per-scope concurrent-run overrides. `undefined` means the profile /
170
+ * environment default applies. Mirrors `RunPolicyStore` semantics.
171
+ */
172
+ declare class ConcurrencyStore {
173
+ private readonly limits;
174
+ get(scope: string): number | undefined;
175
+ set(scope: string, limit: number): void;
176
+ clear(scope: string): boolean;
177
+ }
178
+
179
+ declare class ModelStore {
180
+ private readonly overrides;
181
+ get(scope: string): string | undefined;
182
+ set(scope: string, model: string): void;
183
+ clear(scope: string): boolean;
184
+ }
185
+
186
+ type PendingFlush<T> = (scope: string, batch: T[]) => void | Promise<void>;
187
+ /**
188
+ * Per-scope debounced message queue with bounded concurrency. A scope may run
189
+ * several flushes in parallel up to `concurrencyFor(scope)`; `block` stops
190
+ * NEW flushes from starting while already-running ones finish, and `unblock`
191
+ * resumes scheduling.
192
+ */
193
+ declare class PendingQueue<T> {
194
+ private readonly quietMs;
195
+ private readonly onFlush;
196
+ private readonly concurrencyFor;
197
+ private readonly pending;
198
+ private readonly timers;
199
+ private readonly blocked;
200
+ private readonly flushingCount;
201
+ constructor(quietMs: number, onFlush: PendingFlush<T>, concurrencyFor?: (scope: string) => number);
202
+ push(scope: string, item: T): void;
203
+ block(scope: string): void;
204
+ unblock(scope: string): void;
205
+ flushNow(scope: string): Promise<void>;
206
+ hasPending(scope: string): boolean;
207
+ isBlocked(scope: string): boolean;
208
+ isFlushing(scope: string): boolean;
209
+ activeFlushes(scope: string): number;
210
+ private schedule;
211
+ private clearTimer;
212
+ }
213
+
214
+ type QuestionKind = 'single' | 'multi' | 'text';
215
+ interface QuestionCardInput {
216
+ id: string;
217
+ kind: QuestionKind;
218
+ question: string;
219
+ options?: string[];
220
+ placeholder?: string;
221
+ }
222
+
223
+ /**
224
+ * Pending question-card registry. `/ask` registers a card; the card submit
225
+ * action resolves it, and the answer is recorded back into the session.
226
+ */
227
+ declare class QuestionRegistry {
228
+ private readonly pending;
229
+ register(scope: string, input: Omit<QuestionCardInput, 'id'>): {
230
+ id: string;
231
+ promise: Promise<string | string[] | undefined>;
232
+ };
233
+ resolve(scope: string, id: string, answer: string | string[] | undefined): boolean;
234
+ settleAll(scope: string): number;
235
+ get(scope: string, id: string): QuestionCardInput | undefined;
236
+ }
237
+
238
+ /**
239
+ * Per-scope live-message retention overrides. `undefined` means the profile /
240
+ * environment default applies. Mirrors `RunPolicyStore` semantics.
241
+ */
242
+ declare class RetentionStore {
243
+ private readonly retentions;
244
+ get(scope: string): number | undefined;
245
+ set(scope: string, retention: number): void;
246
+ clear(scope: string): boolean;
247
+ }
248
+
249
+ interface RoleDefinition {
250
+ id: string;
36
251
  name: string;
37
- platform: ServicePlatform;
38
- installed: boolean;
39
- autostartEnabled: boolean;
40
- state: ServiceRuntimeState;
41
- pid: number | undefined;
42
- detail: string;
43
- restarts: number | undefined;
44
- }
45
- interface ServiceController {
46
- readonly platform: ServicePlatform;
47
- installAndStart(spec: ServiceSpec): Promise<void>;
48
- stop(spec: ServiceSpec): Promise<void>;
49
- restart(spec: ServiceSpec): Promise<void>;
50
- status(spec: ServiceSpec): Promise<ServiceStatus>;
51
- }
52
-
53
- interface ServiceMetadata {
252
+ /** System-style persona/instructions applied to every run of this role. */
253
+ persona: string;
254
+ /** Optional model override for this role (below the per-scope /model use). */
255
+ model?: string;
256
+ /** Optional comma-separated tool guidance shown to the agent. */
257
+ tools?: string;
258
+ /** Optional role rules text injected like an AGENTS.md. */
259
+ agentsMd?: string;
260
+ createdAt: string;
261
+ updatedAt: string;
262
+ }
263
+ interface RoleStoreInput {
264
+ id: string;
265
+ name: string;
266
+ persona: string;
267
+ model?: string;
268
+ tools?: string;
269
+ agentsMd?: string;
270
+ }
271
+ /**
272
+ * Persisted role registry: named agent roles (persona / model / tools guidance /
273
+ * role rules) plus per-scope role bindings. Stored per profile at
274
+ * `<profile>/roles.json` (0600). Memory of bindings survives restarts.
275
+ */
276
+ declare class RoleStore {
277
+ private data;
278
+ private saving;
279
+ private readonly path;
280
+ constructor(path: string);
281
+ load(): Promise<void>;
282
+ list(): RoleDefinition[];
283
+ get(id: string): RoleDefinition | undefined;
284
+ upsert(input: RoleStoreInput): RoleDefinition;
285
+ remove(id: string): boolean;
286
+ roleForScope(scope: string): RoleDefinition | undefined;
287
+ setScopeRole(scope: string, roleId: string): boolean;
288
+ clearScopeRole(scope: string): boolean;
289
+ flush(): Promise<void>;
290
+ private schedulePersist;
291
+ }
292
+
293
+ declare class RunPolicyStore {
294
+ private readonly timeouts;
295
+ get(scope: string): number | undefined;
296
+ set(scope: string, runTimeoutMs: number): void;
297
+ clear(scope: string): boolean;
298
+ }
299
+
300
+ interface ProfileConfig {
54
301
  schemaVersion: 1;
55
- serviceName: string;
56
- profile: string;
57
- platform: ServicePlatform;
58
- version: string;
59
- installedAt: string;
302
+ agentKind: 'dsh';
303
+ tenant: LarkTenant;
304
+ accounts: {
305
+ appId: string;
306
+ appSecret: string;
307
+ };
308
+ workspaces: {
309
+ default: string | undefined;
310
+ };
311
+ preferences: {
312
+ model: string | undefined;
313
+ stopGraceMs: number | undefined;
314
+ runTimeoutMs: number | undefined;
315
+ };
316
+ access: {
317
+ allowedUsers: string[];
318
+ allowedChats: string[];
319
+ admins: string[];
320
+ };
60
321
  }
61
- interface ServiceManagerDeps {
62
- profile?: string;
322
+ interface RootConfig {
323
+ schemaVersion: 1;
324
+ activeProfile: string;
325
+ profiles: Record<string, ProfileConfig>;
326
+ }
327
+ declare class ConfigStore {
328
+ private readonly path;
329
+ private data;
330
+ constructor(path: string);
331
+ load(): Promise<RootConfig>;
332
+ getActiveProfile(): ProfileConfig | undefined;
333
+ getProfile(name: string): ProfileConfig | undefined;
334
+ saveProfile(name: string, input: {
335
+ tenant: LarkTenant;
336
+ appId: string;
337
+ appSecret: string;
338
+ workspace?: string;
339
+ model?: string;
340
+ stopGraceMs?: number;
341
+ runTimeoutMs?: number;
342
+ operatorOpenId?: string;
343
+ access?: {
344
+ allowedUsers?: string[];
345
+ allowedChats?: string[];
346
+ admins?: string[];
347
+ };
348
+ }): Promise<void>;
349
+ private getData;
350
+ private persist;
351
+ }
352
+
353
+ interface AccessSnapshot {
354
+ allowedUsers: string[];
355
+ allowedChats: string[];
356
+ admins: string[];
357
+ }
358
+ declare class AccessManager {
359
+ private readonly store;
360
+ private readonly profileName;
361
+ constructor(store: ConfigStore, profileName: string);
362
+ snapshot(): AccessSnapshot;
363
+ isAdmin(openId: string | undefined): boolean;
364
+ addUser(id: string): Promise<void>;
365
+ addAdmin(id: string): Promise<void>;
366
+ addChat(chatId: string): Promise<void>;
367
+ removeUser(id: string): Promise<void>;
368
+ removeChat(chatId: string): Promise<void>;
369
+ private profile;
370
+ private persist;
371
+ }
372
+
373
+ interface DshModelEntry {
374
+ id: string;
375
+ name: string | undefined;
376
+ contextWindow: number | undefined;
377
+ maxTokens: number | undefined;
378
+ }
379
+ interface DshProviderSummary {
380
+ id: string;
381
+ displayName: string;
382
+ namespace: string;
383
+ configured: boolean;
384
+ credentialRef: string | undefined;
385
+ credentialReady: boolean;
386
+ models: DshModelEntry[];
387
+ /** Whether dsh-lark-bot can add/update/remove this provider via chat. */
388
+ managed: boolean;
389
+ }
390
+ interface DshProviderManagerOptions {
391
+ home?: string;
63
392
  env?: NodeJS.ProcessEnv;
64
- paths?: AppPaths;
65
- version?: string;
66
- controller?: ServiceController;
67
- }
68
- declare class ServiceManager {
69
- private readonly profile;
70
- private readonly sourceEnv;
71
- private readonly paths;
72
- private readonly version;
73
- private readonly controllerOverride;
74
- constructor(deps?: ServiceManagerDeps);
75
- private buildSpec;
76
- resolveController(): Promise<ServiceController>;
77
- private saveMetadata;
78
- readMetadata(): Promise<ServiceMetadata | undefined>;
79
- private prepareFiles;
80
- start(): Promise<ServiceStatus>;
81
- stop(): Promise<ServiceStatus>;
82
- restart(): Promise<ServiceStatus>;
83
- status(): Promise<ServiceStatus>;
393
+ settingsFile?: string;
394
+ credentialsFile?: string;
395
+ }
396
+ interface DshPiAiProviderInput {
397
+ id: string;
398
+ displayName?: string;
399
+ apiKeyEnv?: string;
400
+ api?: string;
401
+ baseURL?: string;
402
+ models?: DshModelEntry[];
403
+ }
404
+ declare class DshProviderManager {
405
+ private readonly home;
406
+ private readonly env;
407
+ private readonly settingsFile;
408
+ private readonly credentialsFile;
409
+ constructor(options?: DshProviderManagerOptions);
410
+ readSettings(): Promise<Record<string, unknown>>;
411
+ readCredentials(): Promise<Record<string, string>>;
412
+ hasCredential(ref: string): Promise<boolean>;
413
+ listCredentialRefs(): Promise<string[]>;
414
+ listProviders(): Promise<DshProviderSummary[]>;
415
+ private describeDeepseek;
416
+ private describePiAi;
417
+ defaultModel(): Promise<string | undefined>;
418
+ setDefaultModel(model: string): Promise<void>;
419
+ upsertDeepseekProvider(input: {
420
+ baseURL?: string;
421
+ apiKeyEnv?: string;
422
+ apiKey?: string;
423
+ }): Promise<void>;
424
+ removeDeepseekProvider(): Promise<void>;
425
+ addDeepseekModel(input: DshModelEntry): Promise<void>;
426
+ removeDeepseekModel(id: string): Promise<boolean>;
427
+ upsertPiAiProvider(input: DshPiAiProviderInput): Promise<void>;
428
+ removePiAiProvider(id: string): Promise<boolean>;
429
+ addPiAiModel(providerId: string, input: DshModelEntry): Promise<void>;
430
+ removePiAiModel(providerId: string, modelId: string): Promise<boolean>;
431
+ setCredential(ref: string, value: string): Promise<void>;
432
+ removeCredential(ref: string): Promise<boolean>;
433
+ private writeNamespace;
434
+ private deleteNamespace;
435
+ }
436
+
437
+ interface SessionRecord {
438
+ sessionId: string | undefined;
439
+ cwd: string;
440
+ messages: ChatMessage[];
441
+ }
442
+ interface ChatMessage {
443
+ role: 'user' | 'assistant';
444
+ content: string;
445
+ }
446
+ interface RecordExchangeOptions {
447
+ /** Max live messages kept for the scope (overflow is archived, then trimmed). */
448
+ retention?: number;
449
+ /** Called with the messages that fall outside the retention window. */
450
+ onArchive?: (overflow: ChatMessage[]) => void | Promise<void>;
451
+ }
452
+ declare class SessionStore {
453
+ private data;
454
+ private saving;
455
+ private pendingArchive;
456
+ private readonly path;
457
+ constructor(path: string);
458
+ load(): Promise<void>;
459
+ getRaw(scopeId: string): SessionRecord | undefined;
460
+ set(scopeId: string, sessionId: string | undefined, cwd: string): void;
461
+ historyFor(scopeId: string, cwd: string): ChatMessage[];
462
+ recordExchange(scopeId: string, cwd: string, userMessages: string[], assistantMessage: string | undefined, options?: RecordExchangeOptions): void;
463
+ /** Full live transcript for a scope (subject to the current retention window). */
464
+ fullHistoryFor(scopeId: string, cwd: string): ChatMessage[];
465
+ clear(scopeId: string): boolean;
466
+ fork(scopeId: string, newScopeId: string, cwd: string): boolean;
467
+ resumeFor(scopeId: string, cwd: string): string | undefined;
468
+ flush(): Promise<void>;
469
+ private schedulePersist;
470
+ private snapshot;
84
471
  }
85
472
 
473
+ type ArchiveSource = 'manual' | 'retention';
474
+ interface ArchiveRecord {
475
+ /** Stable archive id: `20260815T020000Z-<short hex>`. */
476
+ archiveId: string;
477
+ scope: string;
478
+ cwd: string | undefined;
479
+ source: ArchiveSource;
480
+ note: string | undefined;
481
+ messageCount: number;
482
+ archivedAt: string;
483
+ jsonlPath: string;
484
+ markdownPath: string;
485
+ gitCommit: string | undefined;
486
+ }
487
+ interface ArchiveInput {
488
+ scope: string;
489
+ cwd: string | undefined;
490
+ messages: readonly ChatMessage[];
491
+ source?: ArchiveSource;
492
+ note?: string;
493
+ }
494
+ interface ArchivePruneOptions {
495
+ /** Keep at most this many archives per scope (oldest removed first). */
496
+ maxArchives?: number;
497
+ /** Remove archives older than this many milliseconds. */
498
+ maxAgeMs?: number;
499
+ }
500
+ /**
501
+ * Durable session/task archival: every archive is written as a human-readable
502
+ * Markdown transcript plus a machine-readable JSONL payload under
503
+ * `<profile>/archives/<scope-slug>/`. When `git` is available, the archive
504
+ * root is lazily initialized as its own Git repository and each archive is
505
+ * committed, giving an auditable, replayable history that survives local
506
+ * retention trimming.
507
+ */
508
+ declare class SessionArchive {
509
+ private readonly archiveDir;
510
+ private readonly runGit;
511
+ private gitInitialized;
512
+ constructor(archiveDir: string, runGit?: (args: string[], cwd: string) => Promise<string>);
513
+ archive(input: ArchiveInput): Promise<ArchiveRecord>;
514
+ list(scope?: string): Promise<ArchiveRecord[]>;
515
+ /** Remove archives beyond per-scope count/age limits. Returns the number removed. */
516
+ prune(options?: ArchivePruneOptions): Promise<number>;
517
+ private ensureGit;
518
+ }
519
+
520
+ declare class WorkspaceStore {
521
+ private data;
522
+ private saving;
523
+ private readonly path;
524
+ constructor(path: string);
525
+ load(): Promise<void>;
526
+ cwdFor(scopeId: string): string | undefined;
527
+ setCwd(scopeId: string, cwd: string): void;
528
+ removeCwd(scopeId: string): boolean;
529
+ listNamed(): Record<string, string>;
530
+ getNamed(name: string): string | undefined;
531
+ saveNamed(name: string, cwd: string): void;
532
+ touchNamed(name: string): void;
533
+ removeNamed(name: string): boolean;
534
+ listIndex(): Array<{
535
+ name: string;
536
+ cwd: string;
537
+ lastUsed: number | undefined;
538
+ }>;
539
+ flush(): Promise<void>;
540
+ private schedulePersist;
541
+ }
542
+
543
+ /**
544
+ * Persistent scope → chat/thread directory. Every inbound message registers
545
+ * its scope, so the bridge can later push outbound notifications to other
546
+ * chats/topics (cross-session messaging). Persisted per profile at
547
+ * `<profile>/scopes.json` so targets survive restarts.
548
+ */
549
+ declare class ScopeDirectory {
550
+ private data;
551
+ private saving;
552
+ private readonly path;
553
+ constructor(path: string);
554
+ load(): Promise<void>;
555
+ register(scope: string, chatId: string, threadId: string | undefined): void;
556
+ /** Resolve a scope key to its chat destination. */
557
+ resolve(scope: string): {
558
+ chatId: string;
559
+ threadId: string | undefined;
560
+ } | undefined;
561
+ /** Direct chat lookup by chatId (also matches topic scopes by prefix). */
562
+ resolveChat(chatId: string): {
563
+ chatId: string;
564
+ threadId: string | undefined;
565
+ } | undefined;
566
+ knownScopes(): string[];
567
+ flush(): Promise<void>;
568
+ private schedulePersist;
569
+ }
570
+
571
+ interface StartChannelDeps {
572
+ appId: string;
573
+ appSecret: string;
574
+ tenant: 'feishu' | 'lark';
575
+ adapter: AgentAdapter;
576
+ sessions: SessionStore;
577
+ workspaces: WorkspaceStore;
578
+ activeRuns: ActiveRuns;
579
+ runPolicies: RunPolicyStore;
580
+ concurrencyStore: ConcurrencyStore;
581
+ defaultScopeConcurrency: number;
582
+ retentionStore: RetentionStore;
583
+ roleStore: RoleStore;
584
+ scopeDirectory?: ScopeDirectory;
585
+ archiver: SessionArchive;
586
+ defaultRetention: number;
587
+ archiveMax: number;
588
+ archiveMaxAgeDays: number;
589
+ defaultRunTimeoutMs: number;
590
+ accessManager: AccessManager;
591
+ pending: PendingQueue<NormalizedMessage>;
592
+ approvals?: ApprovalRegistry;
593
+ questions?: QuestionRegistry;
594
+ densityStore?: DensityStore;
595
+ models: ModelStore;
596
+ dshConfig: DshProviderManager;
597
+ defaultWorkspace: string;
598
+ defaultModel: string;
599
+ allowedUsers?: string[];
600
+ allowedChats?: string[];
601
+ accessDefaultDeny?: boolean;
602
+ eventFreshnessMs?: number;
603
+ stopGraceMs?: number;
604
+ createChannel?: typeof createLarkChannel;
605
+ }
606
+ interface BridgeChannel {
607
+ channel: LarkChannel;
608
+ disconnect(): Promise<void>;
609
+ }
610
+ declare function startChannel(deps: StartChannelDeps): Promise<BridgeChannel>;
611
+
612
+ interface BridgeEngineStatus {
613
+ state: 'running' | 'stopped';
614
+ profile: string;
615
+ home: string;
616
+ adapterId: string;
617
+ startedAt: string | undefined;
618
+ workspace: string | undefined;
619
+ notifyUrl: string | undefined;
620
+ }
621
+ interface BridgeEngine {
622
+ readonly profile: string;
623
+ readonly home: string;
624
+ status(): BridgeEngineStatus;
625
+ stop(): Promise<void>;
626
+ }
627
+ interface BridgeEngineOptions {
628
+ env: RuntimeEnv;
629
+ profileName: string;
630
+ /** Allow first-run QR onboarding when no credentials exist. */
631
+ allowOnboarding: boolean;
632
+ /** Injectable channel factory (tests / host integration). */
633
+ createChannel?: Parameters<typeof startChannel>[0]['createChannel'];
634
+ /** Injectable adapter (tests); otherwise built from env. */
635
+ adapter?: AgentAdapter;
636
+ }
637
+ /**
638
+ * Start the bridge engine. Runs the full Feishu channel pipeline (stores,
639
+ * adapter, card queue, notify server) and returns a handle that can be
640
+ * stopped. No process-level signal handling: the CLI wrapper owns signals,
641
+ * and the dsh bundle plugin owns lifecycle via the cordis context.
642
+ */
643
+ declare function startBridgeEngine(options: BridgeEngineOptions): Promise<BridgeEngine>;
644
+
86
645
  /** Cordis plugin name; stable across releases (referenced by the bundle patch). */
87
646
  declare const name = "dsh-lark-bot";
88
647
  /** No hard service dependency: the bundle must never block a profile boot. */
89
648
  declare const inject: string[];
90
649
  interface Config {
91
- /** Bridge profile managed by this bundle (defaults to `default`). */
650
+ /** Bridge bot profile name inside the bridge state store (default `default`). */
92
651
  profile?: string;
93
- /** Start the standalone bridge when the profile boots (env `DSH_LARK_AUTOSTART=1`). */
94
- autostart?: boolean;
95
652
  /** Explicit `~/.dsh-lark` override (env `DSH_LARK_HOME`). */
96
653
  home?: string;
654
+ /** Feishu/Lark app id (env `DSH_LARK_APP_ID`). */
655
+ appId?: string;
656
+ /** Feishu/Lark app secret (env `DSH_LARK_APP_SECRET`). */
657
+ appSecret?: string;
658
+ /** `feishu` or `lark` (env `DSH_LARK_TENANT`). */
659
+ tenant?: string;
660
+ /** Default workspace for new sessions (env `DSH_LARK_WORKSPACE`). */
661
+ workspace?: string;
662
+ /** Agent backend mode: `sdk` (default) / `acp` / `headless`. */
663
+ adapter?: 'sdk' | 'acp' | 'headless';
664
+ /** Default model (env `DSH_LARK_MODEL`). */
665
+ model?: string;
666
+ /** Set to true (or env `DSH_LARK_DISABLED=1`) to keep the bridge stopped. */
667
+ disabled?: boolean;
668
+ }
669
+ /** Test-only dependency overrides; production rows configure through Config/env. */
670
+ interface PluginDeps {
671
+ env?: NodeJS.ProcessEnv;
672
+ createChannel?: Parameters<typeof startBridgeEngine>[0]['createChannel'];
673
+ adapter?: AgentAdapter;
674
+ }
675
+ interface LarkBridgeStatus {
676
+ state: 'starting' | 'running' | 'stopped';
677
+ profile: string;
678
+ home: string;
679
+ adapterId: string;
680
+ startedAt: string | undefined;
681
+ workspace: string | undefined;
682
+ notifyUrl: string | undefined;
97
683
  }
98
684
  /**
99
- * `larkBridge` service exposed to other in-process plugins: manages the
100
- * standalone dsh-lark-bot background service (start / status / restart /
101
- * stop) through the same controllers the CLI uses.
685
+ * `larkBridge` service exposed to other in-process plugins: starts/stops the
686
+ * bridge engine and reports its status. The engine runs inside the dsh
687
+ * process (same event loop), so the Feishu channel, notify callback and the
688
+ * nested dsh SDK runtime are all owned by the loaded plugin.
102
689
  */
103
690
  declare class LarkBridgeService extends Service {
104
- private readonly manager;
105
- constructor(ctx: Context, manager: ServiceManager);
106
- status(): Promise<ServiceStatus>;
107
- start(): Promise<ServiceStatus>;
108
- restart(): Promise<ServiceStatus>;
109
- stop(): Promise<ServiceStatus>;
691
+ private engine;
692
+ private startPromise;
693
+ constructor(ctx: Context);
694
+ status(): LarkBridgeStatus;
695
+ start(config?: Config, deps?: PluginDeps): Promise<BridgeEngine>;
696
+ stop(): Promise<void>;
110
697
  }
111
- declare function apply(ctx: Context, config?: Config, managerOverride?: ServiceManager): LarkBridgeService;
698
+ declare function apply(ctx: Context, config?: Config, deps?: PluginDeps): () => void;
112
699
 
113
- export { type Config, LarkBridgeService, apply, inject, name };
700
+ export { type Config, LarkBridgeService, type LarkBridgeStatus, type PluginDeps, apply, inject, name };