dsh-lark-bot 0.1.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/cli.js ADDED
@@ -0,0 +1,1663 @@
1
+ // src/cli.ts
2
+ import { readFileSync } from "fs";
3
+ import { Command } from "commander";
4
+
5
+ // src/cli/commands/doctor.ts
6
+ import { stat } from "fs/promises";
7
+
8
+ // src/adapters/dsh/availability.ts
9
+ import { spawn } from "cross-spawn";
10
+ async function checkDshAvailability(options) {
11
+ const timeoutMs = options.timeoutMs ?? 5e3;
12
+ return new Promise((resolve4) => {
13
+ const child = spawn(options.command, ["--version"], {
14
+ stdio: ["ignore", "pipe", "pipe"]
15
+ });
16
+ let stdout = "";
17
+ let stderr = "";
18
+ let settled = false;
19
+ const finish = (result) => {
20
+ if (settled) return;
21
+ settled = true;
22
+ resolve4(result);
23
+ };
24
+ const timer = setTimeout(() => {
25
+ child.kill("SIGKILL");
26
+ finish({ ok: false, error: `timed out after ${timeoutMs}ms`, version: void 0 });
27
+ }, timeoutMs);
28
+ child.stdout?.on("data", (chunk) => {
29
+ stdout += chunk.toString("utf8");
30
+ });
31
+ child.stderr?.on("data", (chunk) => {
32
+ stderr += chunk.toString("utf8");
33
+ });
34
+ child.on("error", (error) => {
35
+ clearTimeout(timer);
36
+ finish({ ok: false, error: error.message, version: void 0 });
37
+ });
38
+ child.on("exit", (code) => {
39
+ clearTimeout(timer);
40
+ const version = stdout.trim();
41
+ if (code !== 0 && !version) {
42
+ finish({ ok: false, error: stderr.trim() || `exited with code ${String(code)}`, version: void 0 });
43
+ return;
44
+ }
45
+ finish({ ok: true, error: void 0, version: version || void 0 });
46
+ });
47
+ });
48
+ }
49
+
50
+ // src/config/app-paths.ts
51
+ import { homedir } from "os";
52
+ import { join, resolve } from "path";
53
+ function defaultHome() {
54
+ const override = process.env.DSH_LARK_HOME?.trim();
55
+ return override ? resolve(override) : join(homedir(), ".dsh-lark");
56
+ }
57
+ function resolveAppPaths(root = defaultHome()) {
58
+ const profileDir = (profile) => join(root, "profiles", profile);
59
+ const profilePath = (profile, ...parts) => join(profileDir(profile), ...parts);
60
+ return {
61
+ root,
62
+ configFile: join(root, "config.json"),
63
+ activeProfileFile: join(root, "active-profile"),
64
+ profileDir,
65
+ profilePath,
66
+ sessionsFile: (profile) => profilePath(profile, "sessions.json"),
67
+ sessionCatalogFile: (profile) => profilePath(profile, "sessions.json.catalog.json"),
68
+ workspacesFile: (profile) => profilePath(profile, "workspaces.json"),
69
+ mediaDir: (profile) => profilePath(profile, "media"),
70
+ logsDir: (profile) => profilePath(profile, "logs"),
71
+ registryFile: join(root, "registry", "processes.json"),
72
+ locksDir: join(root, "registry", "locks")
73
+ };
74
+ }
75
+
76
+ // src/config/env.ts
77
+ import { join as join2, resolve as resolve2 } from "path";
78
+ import { homedir as homedir2 } from "os";
79
+ var DEFAULTS = {
80
+ tenant: "feishu",
81
+ dshCommand: "node",
82
+ dshArgs: ["lib/bin.js", "cordis.yml"],
83
+ provider: "deepseek-official",
84
+ model: "deepseek-v4-flash",
85
+ runTimeoutMs: 3e5,
86
+ stopGraceMs: 5e3
87
+ };
88
+ function nonEmpty(value) {
89
+ const trimmed = value?.trim();
90
+ return trimmed ? trimmed : void 0;
91
+ }
92
+ function parseTenant(value) {
93
+ const tenant = nonEmpty(value) ?? DEFAULTS.tenant;
94
+ if (tenant !== "feishu" && tenant !== "lark") {
95
+ throw new Error(`DSH_LARK_TENANT must be "feishu" or "lark", got "${tenant}"`);
96
+ }
97
+ return tenant;
98
+ }
99
+ function parseDshArgs(value) {
100
+ const raw = value?.trim();
101
+ if (!raw) return [...DEFAULTS.dshArgs];
102
+ return raw.split(",").map((item) => item.trim()).filter(Boolean);
103
+ }
104
+ function parseTimeout(value) {
105
+ const raw = value?.trim();
106
+ if (!raw) return DEFAULTS.runTimeoutMs;
107
+ const parsed = Number(raw);
108
+ if (!Number.isInteger(parsed) || parsed < 0) {
109
+ throw new Error(`DSH_LARK_RUN_TIMEOUT_MS must be a non-negative integer, got "${raw}"`);
110
+ }
111
+ return parsed;
112
+ }
113
+ function parseStopGrace(value) {
114
+ const raw = value?.trim();
115
+ if (!raw) return DEFAULTS.stopGraceMs;
116
+ const parsed = Number(raw);
117
+ if (!Number.isInteger(parsed) || parsed < 0) {
118
+ throw new Error(`DSH_LARK_STOP_GRACE_MS must be a non-negative integer, got "${raw}"`);
119
+ }
120
+ return parsed;
121
+ }
122
+ function loadRuntimeEnv(source = process.env) {
123
+ const homeOverride = nonEmpty(source.DSH_LARK_HOME);
124
+ const workspace = nonEmpty(source.DSH_LARK_WORKSPACE);
125
+ return {
126
+ home: homeOverride ? resolve2(homeOverride) : join2(homedir2(), ".dsh-lark"),
127
+ tenant: parseTenant(source.DSH_LARK_TENANT),
128
+ appId: nonEmpty(source.DSH_LARK_APP_ID),
129
+ appSecret: nonEmpty(source.DSH_LARK_APP_SECRET),
130
+ workspace: workspace ? resolve2(workspace) : void 0,
131
+ dshCommand: nonEmpty(source.DSH_LARK_DSH_COMMAND) ?? DEFAULTS.dshCommand,
132
+ dshArgs: parseDshArgs(source.DSH_LARK_DSH_ARGS),
133
+ provider: nonEmpty(source.DSH_LARK_PROVIDER) ?? DEFAULTS.provider,
134
+ model: nonEmpty(source.DSH_LARK_MODEL) ?? DEFAULTS.model,
135
+ runTimeoutMs: parseTimeout(source.DSH_LARK_RUN_TIMEOUT_MS),
136
+ stopGraceMs: parseStopGrace(source.DSH_LARK_STOP_GRACE_MS)
137
+ };
138
+ }
139
+
140
+ // src/config/profile-store.ts
141
+ import { readFile } from "fs/promises";
142
+ import { mkdir as mkdir2 } from "fs/promises";
143
+ import { dirname as dirname2 } from "path";
144
+
145
+ // src/platform/atomic-write.ts
146
+ import { randomBytes } from "crypto";
147
+ import { mkdir, rename, writeFile } from "fs/promises";
148
+ import { dirname, join as join3 } from "path";
149
+ async function writeFileAtomic(target, data, options = {}) {
150
+ const directory = dirname(target);
151
+ await mkdir(directory, { recursive: true });
152
+ const temporary = join3(directory, `.${randomBytes(8).toString("hex")}.tmp`);
153
+ try {
154
+ await writeFile(temporary, data, options.mode === void 0 ? void 0 : { mode: options.mode });
155
+ await rename(temporary, target);
156
+ } catch (error) {
157
+ await rmSilently(temporary);
158
+ throw error;
159
+ }
160
+ }
161
+ async function rmSilently(path) {
162
+ const { rm } = await import("fs/promises");
163
+ try {
164
+ await rm(path, { force: true });
165
+ } catch {
166
+ }
167
+ }
168
+
169
+ // src/config/profile-store.ts
170
+ var ConfigStore = class {
171
+ constructor(path) {
172
+ this.path = path;
173
+ }
174
+ path;
175
+ data;
176
+ async load() {
177
+ try {
178
+ const raw = await readFile(this.path, "utf8");
179
+ const parsed = JSON.parse(raw);
180
+ this.data = {
181
+ schemaVersion: 1,
182
+ activeProfile: parsed.activeProfile ?? "default",
183
+ profiles: parsed.profiles ?? {}
184
+ };
185
+ } catch (error) {
186
+ if (error.code !== "ENOENT") throw error;
187
+ this.data = {
188
+ schemaVersion: 1,
189
+ activeProfile: "default",
190
+ profiles: {}
191
+ };
192
+ }
193
+ return this.data;
194
+ }
195
+ getActiveProfile() {
196
+ const data = this.getData();
197
+ return data.profiles[data.activeProfile];
198
+ }
199
+ getProfile(name) {
200
+ const data = this.getData();
201
+ return data.profiles[name];
202
+ }
203
+ async saveProfile(name, input) {
204
+ const data = this.getData();
205
+ const existing = data.profiles[name];
206
+ const profile = {
207
+ schemaVersion: 1,
208
+ agentKind: "dsh",
209
+ tenant: input.tenant,
210
+ accounts: {
211
+ appId: input.appId,
212
+ appSecret: input.appSecret
213
+ },
214
+ workspaces: {
215
+ default: input.workspace ?? existing?.workspaces.default ?? void 0
216
+ },
217
+ preferences: {
218
+ model: input.model ?? existing?.preferences.model ?? void 0,
219
+ stopGraceMs: input.stopGraceMs ?? existing?.preferences.stopGraceMs ?? void 0,
220
+ runTimeoutMs: input.runTimeoutMs ?? existing?.preferences.runTimeoutMs ?? void 0
221
+ }
222
+ };
223
+ data.profiles[name] = profile;
224
+ data.activeProfile = name;
225
+ await this.persist();
226
+ }
227
+ getData() {
228
+ if (!this.data) {
229
+ throw new Error("ConfigStore must be loaded before use");
230
+ }
231
+ return this.data;
232
+ }
233
+ async persist() {
234
+ const data = this.getData();
235
+ await mkdir2(dirname2(this.path), { recursive: true });
236
+ await writeFileAtomic(this.path, `${JSON.stringify(data, null, 2)}
237
+ `, {
238
+ mode: 384
239
+ });
240
+ }
241
+ };
242
+
243
+ // src/cli/commands/doctor.ts
244
+ async function runDoctor(options) {
245
+ const env = loadRuntimeEnv({
246
+ ...process.env,
247
+ ...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
248
+ ...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
249
+ ...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
250
+ ...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
251
+ });
252
+ const paths = resolveAppPaths(env.home);
253
+ const profileName = options.profile ?? "default";
254
+ const store = new ConfigStore(paths.configFile);
255
+ await store.load();
256
+ const profile = store.getProfile(profileName);
257
+ const lines = [
258
+ "dsh-lark-bot doctor",
259
+ `version: ${options.version ?? "unknown"}`,
260
+ `node: ${process.version}`,
261
+ `profile: ${profileName}`,
262
+ `home: ${paths.root}`
263
+ ];
264
+ let critical = false;
265
+ if (!profile) {
266
+ lines.push("config: missing");
267
+ critical = true;
268
+ } else {
269
+ lines.push(
270
+ [
271
+ "config: ok",
272
+ `tenant=${profile.tenant}`,
273
+ `app_id=${profile.accounts.appId}`,
274
+ `app_secret=${profile.accounts.appSecret ? "present" : "missing"}`
275
+ ].join(" ")
276
+ );
277
+ if (!profile.accounts.appId || !profile.accounts.appSecret) critical = true;
278
+ }
279
+ const workspace = options.workspace ?? profile?.workspaces.default ?? env.workspace ?? paths.profilePath(profileName, "workspace");
280
+ try {
281
+ const info = await stat(workspace);
282
+ lines.push(`workspace: ${workspace} (${info.isDirectory() ? "directory" : "not-directory"})`);
283
+ } catch {
284
+ lines.push(`workspace: ${workspace} (missing)`);
285
+ }
286
+ const availability = await checkDshAvailability({ command: env.dshCommand });
287
+ if (availability.ok) {
288
+ lines.push(`dsh: ok${availability.version ? ` (${availability.version})` : ""}`);
289
+ } else {
290
+ lines.push(`dsh: unavailable (${availability.error ?? "unknown"})`);
291
+ critical = true;
292
+ }
293
+ const output = options.output ?? ((text) => process.stdout.write(text));
294
+ output(`${lines.join("\n")}
295
+ `);
296
+ if (critical) process.exitCode = 1;
297
+ }
298
+
299
+ // src/cli/commands/start.ts
300
+ import { mkdir as mkdir4 } from "fs/promises";
301
+
302
+ // src/adapters/dsh/adapter.ts
303
+ import { spawn as spawn2 } from "cross-spawn";
304
+ import { createInterface } from "readline";
305
+
306
+ // src/adapters/dsh/translate.ts
307
+ function isRecord(value) {
308
+ return typeof value === "object" && value !== null;
309
+ }
310
+ function stringValue(value) {
311
+ return typeof value === "string" ? value : void 0;
312
+ }
313
+ function translateDshLine(line) {
314
+ const trimmed = line.trim();
315
+ if (!trimmed) return [];
316
+ try {
317
+ const parsed = JSON.parse(trimmed);
318
+ if (!isRecord(parsed) || typeof parsed.type !== "string") return [];
319
+ switch (parsed.type) {
320
+ case "system":
321
+ return [
322
+ {
323
+ type: "system",
324
+ sessionId: stringValue(parsed.sessionId),
325
+ cwd: stringValue(parsed.cwd),
326
+ model: stringValue(parsed.model)
327
+ }
328
+ ];
329
+ case "text":
330
+ return [{ type: "text", delta: stringValue(parsed.delta) ?? "" }];
331
+ case "final_text":
332
+ return [{ type: "final_text", content: stringValue(parsed.content) ?? "" }];
333
+ case "thinking":
334
+ return [{ type: "thinking", delta: stringValue(parsed.delta) ?? "" }];
335
+ case "tool_use":
336
+ return [
337
+ {
338
+ type: "tool_use",
339
+ id: stringValue(parsed.id) ?? "unknown",
340
+ name: stringValue(parsed.name) ?? "tool",
341
+ input: parsed.input
342
+ }
343
+ ];
344
+ case "tool_result":
345
+ return [
346
+ {
347
+ type: "tool_result",
348
+ id: stringValue(parsed.id) ?? "unknown",
349
+ output: stringValue(parsed.output) ?? "",
350
+ isError: parsed.isError === true
351
+ }
352
+ ];
353
+ case "done":
354
+ return [
355
+ {
356
+ type: "done",
357
+ sessionId: stringValue(parsed.sessionId),
358
+ terminationReason: "normal"
359
+ }
360
+ ];
361
+ case "error":
362
+ return [
363
+ {
364
+ type: "error",
365
+ message: stringValue(parsed.message) ?? "dsh failed",
366
+ terminationReason: "failed"
367
+ }
368
+ ];
369
+ default:
370
+ return [];
371
+ }
372
+ } catch {
373
+ return [];
374
+ }
375
+ }
376
+ function plainOutputEvent(output) {
377
+ const trimmed = output.trim();
378
+ return trimmed ? [{ type: "final_text", content: trimmed }] : [];
379
+ }
380
+
381
+ // src/adapters/dsh/adapter.ts
382
+ var DshAdapter = class {
383
+ id = "dsh";
384
+ displayName = "DeepSeek Harness";
385
+ command;
386
+ args;
387
+ stopGraceMs;
388
+ constructor(options = {}) {
389
+ this.command = options.command ?? "dsh";
390
+ this.args = options.args ?? ["--profile", "headless"];
391
+ this.stopGraceMs = options.stopGraceMs ?? 5e3;
392
+ }
393
+ async isAvailable() {
394
+ return (await this.checkAvailability()).ok;
395
+ }
396
+ checkAvailability() {
397
+ return checkDshAvailability({ command: this.command });
398
+ }
399
+ run(options) {
400
+ const child = spawn2(
401
+ this.command,
402
+ [...this.args, options.prompt],
403
+ {
404
+ cwd: options.cwd,
405
+ stdio: ["pipe", "pipe", "pipe"]
406
+ }
407
+ );
408
+ child.stdin?.end();
409
+ const stderrChunks = [];
410
+ let runtimeError = null;
411
+ child.stderr?.on("data", (chunk) => {
412
+ stderrChunks.push(chunk);
413
+ });
414
+ child.on("error", (error) => {
415
+ runtimeError = error;
416
+ });
417
+ return {
418
+ runId: options.runId,
419
+ events: createEventStream(child, stderrChunks, () => runtimeError, {
420
+ sessionId: options.sessionId,
421
+ cwd: options.cwd,
422
+ model: options.model
423
+ }),
424
+ stop: () => stopChild(child, this.stopGraceMs),
425
+ waitForExit: (timeoutMs) => waitForChildExit(child, timeoutMs)
426
+ };
427
+ }
428
+ };
429
+ async function* createEventStream(child, stderrChunks, getError, system) {
430
+ if (!child.pid) {
431
+ const error = getError();
432
+ yield {
433
+ type: "error",
434
+ message: error ? `failed to spawn dsh: ${error.message}` : "spawn returned no pid",
435
+ terminationReason: "failed"
436
+ };
437
+ return;
438
+ }
439
+ yield {
440
+ type: "system",
441
+ sessionId: system.sessionId,
442
+ cwd: system.cwd,
443
+ model: system.model
444
+ };
445
+ if (!child.stdout) {
446
+ yield {
447
+ type: "error",
448
+ message: "dsh stdout stream is unavailable",
449
+ terminationReason: "failed"
450
+ };
451
+ return;
452
+ }
453
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
454
+ const plainLines = [];
455
+ let emittedProtocol = false;
456
+ let terminalEmitted = false;
457
+ for await (const line of lines) {
458
+ const trimmed = line.trim();
459
+ if (!trimmed) continue;
460
+ const events = translateDshLine(trimmed);
461
+ if (events.length > 0) {
462
+ emittedProtocol = true;
463
+ for (const event of events) {
464
+ if (event.type === "done" || event.type === "error") terminalEmitted = true;
465
+ yield event;
466
+ }
467
+ } else {
468
+ plainLines.push(trimmed);
469
+ }
470
+ }
471
+ const exitCode = await waitForExitResult(child, 0).then((value) => value.code);
472
+ const runtimeError = getError();
473
+ if (!terminalEmitted) {
474
+ if (exitCode !== null && exitCode !== 0) {
475
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
476
+ const detail = stderr ? `: ${stderr.slice(0, 500)}` : "";
477
+ yield {
478
+ type: "error",
479
+ message: `dsh exited with code ${exitCode}${detail}`,
480
+ terminationReason: "failed"
481
+ };
482
+ return;
483
+ }
484
+ if (runtimeError) {
485
+ yield {
486
+ type: "error",
487
+ message: `dsh runtime error: ${runtimeError.message}`,
488
+ terminationReason: "failed"
489
+ };
490
+ return;
491
+ }
492
+ if (emittedProtocol) {
493
+ yield { type: "done", sessionId: system.sessionId, terminationReason: "normal" };
494
+ } else {
495
+ yield* plainOutputEvent(plainLines.join("\n"));
496
+ yield { type: "done", sessionId: system.sessionId, terminationReason: "normal" };
497
+ }
498
+ }
499
+ }
500
+ async function waitForExitResult(child, timeoutMs) {
501
+ if (child.exitCode !== null || child.signalCode !== null) {
502
+ return { code: child.exitCode, timedOut: false };
503
+ }
504
+ return new Promise((resolve4) => {
505
+ const onExit = (code) => {
506
+ if (timer !== void 0) clearTimeout(timer);
507
+ resolve4({ code, timedOut: false });
508
+ };
509
+ const timer = timeoutMs > 0 ? setTimeout(() => {
510
+ child.removeListener("exit", onExit);
511
+ resolve4({ code: child.exitCode, timedOut: true });
512
+ }, timeoutMs) : void 0;
513
+ child.once("exit", onExit);
514
+ });
515
+ }
516
+ async function stopChild(child, graceMs) {
517
+ if (child.exitCode !== null || child.signalCode !== null) return;
518
+ child.kill("SIGTERM");
519
+ const stopped = await waitForExitResult(child, graceMs);
520
+ if (!stopped.timedOut) return;
521
+ child.kill("SIGKILL");
522
+ }
523
+ async function waitForChildExit(child, timeoutMs) {
524
+ const result = await waitForExitResult(child, timeoutMs);
525
+ return !result.timedOut && result.code !== null;
526
+ }
527
+
528
+ // src/bot/active-runs.ts
529
+ var ActiveRuns = class {
530
+ runs = /* @__PURE__ */ new Map();
531
+ set(scope, handle) {
532
+ this.runs.set(scope, handle);
533
+ }
534
+ get(scope) {
535
+ return this.runs.get(scope);
536
+ }
537
+ delete(scope) {
538
+ return this.runs.delete(scope);
539
+ }
540
+ async interrupt(scope) {
541
+ const handle = this.runs.get(scope);
542
+ if (!handle) return false;
543
+ this.runs.delete(scope);
544
+ await handle.stop();
545
+ return true;
546
+ }
547
+ };
548
+
549
+ // src/bot/pending-queue.ts
550
+ var PendingQueue = class {
551
+ constructor(quietMs, onFlush) {
552
+ this.quietMs = quietMs;
553
+ this.onFlush = onFlush;
554
+ }
555
+ quietMs;
556
+ onFlush;
557
+ pending = /* @__PURE__ */ new Map();
558
+ timers = /* @__PURE__ */ new Map();
559
+ blocked = /* @__PURE__ */ new Set();
560
+ flushing = /* @__PURE__ */ new Set();
561
+ push(scope, item) {
562
+ const batch = this.pending.get(scope) ?? [];
563
+ batch.push(item);
564
+ this.pending.set(scope, batch);
565
+ this.schedule(scope);
566
+ }
567
+ block(scope) {
568
+ this.blocked.add(scope);
569
+ this.clearTimer(scope);
570
+ }
571
+ unblock(scope) {
572
+ this.blocked.delete(scope);
573
+ this.schedule(scope);
574
+ }
575
+ async flushNow(scope) {
576
+ this.clearTimer(scope);
577
+ if (this.flushing.has(scope)) return;
578
+ const batch = this.pending.get(scope) ?? [];
579
+ this.pending.delete(scope);
580
+ if (batch.length === 0) return;
581
+ this.flushing.add(scope);
582
+ try {
583
+ await this.onFlush(scope, batch);
584
+ } finally {
585
+ this.flushing.delete(scope);
586
+ }
587
+ }
588
+ hasPending(scope) {
589
+ return (this.pending.get(scope)?.length ?? 0) > 0;
590
+ }
591
+ isBlocked(scope) {
592
+ return this.blocked.has(scope);
593
+ }
594
+ schedule(scope) {
595
+ if (this.blocked.has(scope) || this.flushing.has(scope)) return;
596
+ if (this.timers.has(scope)) return;
597
+ if (!this.hasPending(scope)) return;
598
+ this.timers.set(
599
+ scope,
600
+ setTimeout(() => {
601
+ this.timers.delete(scope);
602
+ void this.flushNow(scope);
603
+ }, this.quietMs)
604
+ );
605
+ }
606
+ clearTimer(scope) {
607
+ const timer = this.timers.get(scope);
608
+ if (timer === void 0) return;
609
+ clearTimeout(timer);
610
+ this.timers.delete(scope);
611
+ }
612
+ };
613
+
614
+ // src/bot/run-policy.ts
615
+ var RunPolicyStore = class {
616
+ timeouts = /* @__PURE__ */ new Map();
617
+ get(scope) {
618
+ return this.timeouts.get(scope);
619
+ }
620
+ set(scope, runTimeoutMs) {
621
+ this.timeouts.set(scope, runTimeoutMs);
622
+ }
623
+ clear(scope) {
624
+ return this.timeouts.delete(scope);
625
+ }
626
+ };
627
+
628
+ // src/bridge/channel.ts
629
+ import { createLarkChannel } from "@larksuite/channel";
630
+
631
+ // src/commands/index.ts
632
+ import { resolve as resolve3 } from "path";
633
+ var HELP = [
634
+ "**dsh-lark-bot \u547D\u4EE4**",
635
+ "",
636
+ "- `/new` `/reset` \u2014 \u5F00\u59CB\u65B0\u4F1A\u8BDD",
637
+ "- `/cd <path>` \u2014 \u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\u5E76\u91CD\u7F6E\u4F1A\u8BDD",
638
+ "- `/ws list|save <name>|use <name>|remove <name>` \u2014 \u7BA1\u7406\u5DE5\u4F5C\u7A7A\u95F4",
639
+ "- `/status` \u2014 \u67E5\u770B\u5F53\u524D\u72B6\u6001",
640
+ "- `/stop` \u2014 \u7EC8\u6B62\u5F53\u524D\u4EFB\u52A1",
641
+ "- `/timeout [N|off|default]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6",
642
+ "- `/help` \u2014 \u663E\u793A\u672C\u5E2E\u52A9"
643
+ ].join("\n");
644
+ async function reply(ctx, markdown2) {
645
+ await ctx.channel.sendMarkdown(ctx.chatId, markdown2, {
646
+ replyTo: ctx.messageId
647
+ });
648
+ }
649
+ async function handleNew(_args, ctx) {
650
+ const wasRunning = await ctx.activeRuns.interrupt(ctx.scope);
651
+ ctx.sessions.clear(ctx.scope);
652
+ await reply(ctx, wasRunning ? "\u5DF2\u4E2D\u65AD\u5F53\u524D\u4EFB\u52A1\u5E76\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002" : "\u5DF2\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002");
653
+ }
654
+ async function handleCd(args, ctx) {
655
+ const path = args.trim();
656
+ if (!path) {
657
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`");
658
+ return;
659
+ }
660
+ const cwd = resolve3(path);
661
+ await ctx.activeRuns.interrupt(ctx.scope);
662
+ ctx.workspaces.setCwd(ctx.scope, cwd);
663
+ ctx.sessions.clear(ctx.scope);
664
+ await reply(ctx, `\u5DF2\u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\uFF1A\`${cwd}\`\uFF0C\u4F1A\u8BDD\u5DF2\u91CD\u7F6E\u3002`);
665
+ }
666
+ async function handleWs(args, ctx) {
667
+ const [sub, ...rest] = args.trim().split(/\s+/);
668
+ const name = rest.join(" ").trim();
669
+ if (!sub || sub === "list") {
670
+ const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
671
+ const named = ctx.workspaces.listNamed();
672
+ const lines = Object.entries(named).map(
673
+ ([key, value]) => `- **${key}** \u2192 \`${value}\`${value === current ? " \u2190 \u5F53\u524D" : ""}`
674
+ );
675
+ await reply(
676
+ ctx,
677
+ [
678
+ `\u5F53\u524D cwd\uFF1A\`${current}\``,
679
+ "",
680
+ ...lines.length > 0 ? lines : ["\u6682\u65E0\u547D\u540D\u5DE5\u4F5C\u7A7A\u95F4\u3002"]
681
+ ].join("\n")
682
+ );
683
+ return;
684
+ }
685
+ if (sub === "save") {
686
+ if (!name) {
687
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/ws save <name>`");
688
+ return;
689
+ }
690
+ const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
691
+ ctx.workspaces.saveNamed(name, current);
692
+ await reply(ctx, `\u5DF2\u4FDD\u5B58\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${current}\``);
693
+ return;
694
+ }
695
+ if (sub === "use") {
696
+ if (!name) {
697
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/ws use <name>`");
698
+ return;
699
+ }
700
+ const cwd = ctx.workspaces.getNamed(name);
701
+ if (!cwd) {
702
+ await reply(ctx, `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`);
703
+ return;
704
+ }
705
+ await ctx.activeRuns.interrupt(ctx.scope);
706
+ ctx.workspaces.setCwd(ctx.scope, cwd);
707
+ ctx.sessions.clear(ctx.scope);
708
+ await reply(ctx, `\u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${cwd}\``);
709
+ return;
710
+ }
711
+ if (sub === "remove") {
712
+ if (!name) {
713
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/ws remove <name>`");
714
+ return;
715
+ }
716
+ const removed = ctx.workspaces.removeNamed(name);
717
+ await reply(ctx, removed ? `\u5DF2\u5220\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**` : `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`);
718
+ return;
719
+ }
720
+ await reply(ctx, "\u672A\u77E5 `/ws` \u5B50\u547D\u4EE4\uFF0C\u8BF7\u4F7F\u7528 list / save / use / remove\u3002");
721
+ }
722
+ async function handleStatus(_args, ctx) {
723
+ const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
724
+ const session = ctx.sessions.getRaw(ctx.scope)?.sessionId ?? "(\u65E0)";
725
+ const running = Boolean(ctx.activeRuns.get(ctx.scope));
726
+ const scopeLabel = ctx.chatMode === "topic" ? `${ctx.scope}\uFF08\u8BDD\u9898\u72EC\u7ACB session\uFF09` : ctx.scope;
727
+ await reply(
728
+ ctx,
729
+ [
730
+ `\u{1F9ED} **scope**: \`${scopeLabel}\``,
731
+ `\u{1F4C1} **cwd**: \`${cwd}\``,
732
+ `\u{1F517} **session**: \`${session}\``,
733
+ `\u{1F3C3} **active run**: ${running ? "yes" : "no"}`
734
+ ].join("\n")
735
+ );
736
+ }
737
+ async function handleStop(_args, ctx) {
738
+ const stopped = await ctx.activeRuns.interrupt(ctx.scope);
739
+ await reply(ctx, stopped ? "\u5DF2\u8BF7\u6C42\u7EC8\u6B62\u5F53\u524D\u4EFB\u52A1\u3002" : "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u3002");
740
+ }
741
+ async function handleTimeout(args, ctx) {
742
+ const input = args.trim();
743
+ const effectiveMs = ctx.runPolicies.get(ctx.scope) ?? ctx.defaultRunTimeoutMs;
744
+ if (!input) {
745
+ const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
746
+ await reply(
747
+ ctx,
748
+ minutes2 > 0 ? `\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A${minutes2} \u5206\u949F\u3002\u53EF\u7528 \`/timeout <N|off|default>\` \u8C03\u6574\u3002` : "\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A\u5173\u95ED\u3002"
749
+ );
750
+ return;
751
+ }
752
+ if (input === "off") {
753
+ ctx.runPolicies.set(ctx.scope, 0);
754
+ await reply(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\u3002");
755
+ return;
756
+ }
757
+ if (input === "default") {
758
+ ctx.runPolicies.clear(ctx.scope);
759
+ await reply(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u8FD0\u884C\u8D85\u65F6\u3002");
760
+ return;
761
+ }
762
+ const minutes = Number(input);
763
+ if (!Number.isInteger(minutes) || minutes <= 0) {
764
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/timeout <N|off|default>`\uFF0CN \u4E3A\u5927\u4E8E 0 \u7684\u5206\u949F\u6570\u3002");
765
+ return;
766
+ }
767
+ ctx.runPolicies.set(ctx.scope, minutes * 6e4);
768
+ await reply(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A${minutes} \u5206\u949F\u3002`);
769
+ }
770
+ async function handleHelp(_args, ctx) {
771
+ await reply(ctx, HELP);
772
+ }
773
+ var handlers = {
774
+ "/new": handleNew,
775
+ "/reset": handleNew,
776
+ "/cd": handleCd,
777
+ "/ws": handleWs,
778
+ "/status": handleStatus,
779
+ "/stop": handleStop,
780
+ "/timeout": handleTimeout,
781
+ "/help": handleHelp
782
+ };
783
+ async function tryHandleCommand(text, ctx) {
784
+ const trimmed = text.trim();
785
+ if (!trimmed.startsWith("/")) return false;
786
+ const [command, ...rest] = trimmed.split(/\s+/);
787
+ const handler = handlers[command ?? ""];
788
+ if (!handler) return false;
789
+ await handler(rest.join(" "), ctx);
790
+ return true;
791
+ }
792
+
793
+ // src/core/logger.ts
794
+ var REDACTED = "[redacted]";
795
+ var SENSITIVE_KEY = /(secret|token|password|api[_-]?key)/i;
796
+ function redactValue(value, key, depth) {
797
+ if (SENSITIVE_KEY.test(key)) return REDACTED;
798
+ if (depth === 0) return value;
799
+ if (Array.isArray(value)) {
800
+ return value.map((item, index) => redactValue(item, `${key}.${index}`, depth - 1));
801
+ }
802
+ if (value && typeof value === "object") {
803
+ const out = {};
804
+ for (const [childKey, childValue] of Object.entries(value)) {
805
+ out[childKey] = redactValue(childValue, childKey, depth - 1);
806
+ }
807
+ return out;
808
+ }
809
+ return value;
810
+ }
811
+ function redactFields(fields, depth = 4) {
812
+ return redactValue(fields, "", depth);
813
+ }
814
+ function serializeMessage(level, category, event, fields) {
815
+ const entry = {
816
+ time: (/* @__PURE__ */ new Date()).toISOString(),
817
+ level,
818
+ category,
819
+ event,
820
+ fields: redactFields(fields)
821
+ };
822
+ return JSON.stringify(entry);
823
+ }
824
+ function createLogger(output = process.stderr) {
825
+ const write = (level, category, event, fields) => {
826
+ output.write(`${serializeMessage(level, category, event, fields)}
827
+ `);
828
+ };
829
+ return {
830
+ info: (category, event, fields = {}) => write("info", category, event, fields),
831
+ warn: (category, event, fields = {}) => write("warn", category, event, fields),
832
+ error: (category, event, fields = {}) => write("error", category, event, fields),
833
+ fail(category, error, fields = {}) {
834
+ const message = error instanceof Error ? error.message : String(error);
835
+ write("error", category, "fail", { message, ...fields });
836
+ }
837
+ };
838
+ }
839
+ var log = createLogger();
840
+
841
+ // src/bridge/lark-channel.ts
842
+ function adaptLarkChannel(channel) {
843
+ const base = {
844
+ async sendMarkdown(chatId, markdown2, options) {
845
+ const sendOptions = {};
846
+ if (options?.replyTo) sendOptions.replyTo = options.replyTo;
847
+ await channel.send(chatId, { markdown: markdown2 }, sendOptions);
848
+ }
849
+ };
850
+ return {
851
+ ...base,
852
+ async streamCard(chatId, initial, producer, options) {
853
+ const sendOptions = {};
854
+ if (options?.replyTo) sendOptions.replyTo = options.replyTo;
855
+ await channel.stream(
856
+ chatId,
857
+ {
858
+ card: {
859
+ initial,
860
+ producer: async (controller) => {
861
+ await producer(controller);
862
+ }
863
+ }
864
+ },
865
+ sendOptions
866
+ );
867
+ }
868
+ };
869
+ }
870
+
871
+ // src/bridge/channel.ts
872
+ async function startChannel(deps) {
873
+ const channel = (deps.createChannel ?? createLarkChannel)({
874
+ appId: deps.appId,
875
+ appSecret: deps.appSecret,
876
+ domain: deps.tenant === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn",
877
+ source: "dsh-lark-bot",
878
+ policy: {
879
+ dmMode: "open",
880
+ requireMention: true,
881
+ respondToMentionAll: false
882
+ },
883
+ safety: {
884
+ chatQueue: { enabled: false }
885
+ },
886
+ outbound: {
887
+ streamThrottleMs: 400
888
+ },
889
+ includeRawEvent: true,
890
+ handshakeTimeoutMs: 8e3,
891
+ httpTimeoutMs: 3e4,
892
+ respectProxyEnv: true
893
+ });
894
+ const streaming = adaptLarkChannel(channel);
895
+ const commandChannel = streaming;
896
+ channel.on({
897
+ message: async (msg) => {
898
+ const scope = scopeForMessage(msg);
899
+ const context = {
900
+ scope,
901
+ chatId: msg.chatId,
902
+ messageId: msg.messageId,
903
+ threadId: msg.threadId,
904
+ chatMode: msg.chatMode ?? msg.chatType,
905
+ sessions: deps.sessions,
906
+ workspaces: deps.workspaces,
907
+ activeRuns: deps.activeRuns,
908
+ runPolicies: deps.runPolicies,
909
+ defaultRunTimeoutMs: deps.defaultRunTimeoutMs,
910
+ channel: commandChannel,
911
+ defaultWorkspace: deps.defaultWorkspace
912
+ };
913
+ const handled = await tryHandleCommand(msg.content, context).catch((error) => {
914
+ log.fail("channel-command", error, { scope });
915
+ return false;
916
+ });
917
+ if (!handled) deps.pending.push(scope, msg);
918
+ },
919
+ cardAction: async (event) => {
920
+ const value = event.action.value && typeof event.action.value === "object" ? event.action.value : void 0;
921
+ if (value?.cmd === "stop") {
922
+ const scope = event.raw ? await resolveCardScope(event.chatId, event.raw) : event.chatId;
923
+ await deps.activeRuns.interrupt(scope);
924
+ }
925
+ },
926
+ reconnecting: () => {
927
+ log.warn("channel", "reconnecting", {});
928
+ },
929
+ reconnected: () => {
930
+ log.info("channel", "reconnected", {});
931
+ },
932
+ error: (error) => {
933
+ log.fail("channel", error);
934
+ }
935
+ });
936
+ await channel.connect();
937
+ return {
938
+ channel,
939
+ disconnect: () => channel.disconnect()
940
+ };
941
+ }
942
+ function scopeForMessage(msg) {
943
+ if (msg.chatMode === "topic" && msg.threadId) return `${msg.chatId}:${msg.threadId}`;
944
+ return msg.chatId;
945
+ }
946
+ async function resolveCardScope(chatId, raw) {
947
+ return raw.message?.thread_id ? `${chatId}:${raw.message.thread_id}` : chatId;
948
+ }
949
+
950
+ // src/bridge/run-flow.ts
951
+ import { randomUUID } from "crypto";
952
+
953
+ // src/card/run-state.ts
954
+ var initialState = {
955
+ blocks: [],
956
+ reasoning: { content: "", active: false },
957
+ footer: "thinking",
958
+ terminal: "running",
959
+ errorMsg: void 0,
960
+ idleTimeoutMinutes: void 0
961
+ };
962
+ function closeStreamingText(blocks) {
963
+ return blocks.map(
964
+ (block) => block.kind === "text" && block.streaming ? { ...block, streaming: false } : block
965
+ );
966
+ }
967
+ function reduce(state, event) {
968
+ switch (event.type) {
969
+ case "text": {
970
+ const last = state.blocks[state.blocks.length - 1];
971
+ if (last?.kind === "text" && last.streaming) {
972
+ return {
973
+ ...state,
974
+ blocks: [
975
+ ...state.blocks.slice(0, -1),
976
+ { ...last, content: last.content + event.delta }
977
+ ],
978
+ reasoning: { ...state.reasoning, active: false },
979
+ footer: "streaming"
980
+ };
981
+ }
982
+ return {
983
+ ...state,
984
+ blocks: [...state.blocks, { kind: "text", content: event.delta, streaming: true }],
985
+ reasoning: { ...state.reasoning, active: false },
986
+ footer: "streaming"
987
+ };
988
+ }
989
+ case "thinking":
990
+ return {
991
+ ...state,
992
+ reasoning: {
993
+ content: state.reasoning.content + event.delta,
994
+ active: true
995
+ },
996
+ footer: "thinking"
997
+ };
998
+ case "tool_use":
999
+ return {
1000
+ ...state,
1001
+ blocks: [
1002
+ ...closeStreamingText(state.blocks),
1003
+ {
1004
+ kind: "tool",
1005
+ tool: {
1006
+ id: event.id,
1007
+ name: event.name,
1008
+ input: event.input,
1009
+ status: "running"
1010
+ }
1011
+ }
1012
+ ],
1013
+ reasoning: { ...state.reasoning, active: false },
1014
+ footer: "tool_running"
1015
+ };
1016
+ case "tool_result":
1017
+ return {
1018
+ ...state,
1019
+ blocks: state.blocks.map((block) => {
1020
+ if (block.kind !== "tool" || block.tool.id !== event.id) return block;
1021
+ return {
1022
+ ...block,
1023
+ tool: {
1024
+ ...block.tool,
1025
+ status: event.isError ? "error" : "done",
1026
+ output: event.output
1027
+ }
1028
+ };
1029
+ })
1030
+ };
1031
+ case "error":
1032
+ return {
1033
+ ...state,
1034
+ terminal: event.terminationReason === "interrupted" ? "interrupted" : event.terminationReason === "timeout" ? "idle_timeout" : "error",
1035
+ errorMsg: event.terminationReason === "failed" ? event.message : void 0,
1036
+ idleTimeoutMinutes: void 0,
1037
+ footer: null
1038
+ };
1039
+ case "done":
1040
+ return {
1041
+ ...state,
1042
+ blocks: closeStreamingText(state.blocks),
1043
+ reasoning: { ...state.reasoning, active: false },
1044
+ terminal: event.terminationReason === "interrupted" ? "interrupted" : event.terminationReason === "timeout" ? "idle_timeout" : "done",
1045
+ footer: null
1046
+ };
1047
+ default:
1048
+ return state;
1049
+ }
1050
+ }
1051
+ function markInterrupted(state) {
1052
+ return {
1053
+ ...state,
1054
+ blocks: closeStreamingText(state.blocks),
1055
+ reasoning: { ...state.reasoning, active: false },
1056
+ terminal: "interrupted",
1057
+ footer: null
1058
+ };
1059
+ }
1060
+ function markIdleTimeout(state, minutes) {
1061
+ return {
1062
+ ...state,
1063
+ blocks: closeStreamingText(state.blocks),
1064
+ reasoning: { ...state.reasoning, active: false },
1065
+ terminal: "idle_timeout",
1066
+ footer: null,
1067
+ idleTimeoutMinutes: minutes
1068
+ };
1069
+ }
1070
+ function finalizeIfRunning(state) {
1071
+ if (state.terminal !== "running") return state;
1072
+ return {
1073
+ ...state,
1074
+ blocks: closeStreamingText(state.blocks),
1075
+ reasoning: { ...state.reasoning, active: false },
1076
+ terminal: "done",
1077
+ footer: null
1078
+ };
1079
+ }
1080
+
1081
+ // src/card/run-renderer.ts
1082
+ function markdown(content) {
1083
+ return { tag: "markdown", content };
1084
+ }
1085
+ function noteMd(content) {
1086
+ return { tag: "markdown", content, text_size: "notation" };
1087
+ }
1088
+ function footerStatus(status) {
1089
+ const text = status === "thinking" ? "\u{1F9E0} \u6B63\u5728\u601D\u8003" : status === "tool_running" ? "\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177" : "\u270D\uFE0F \u6B63\u5728\u8F93\u51FA";
1090
+ return noteMd(text);
1091
+ }
1092
+ function summaryText(state) {
1093
+ if (state.terminal === "interrupted") return "\u5DF2\u4E2D\u65AD";
1094
+ if (state.terminal === "idle_timeout") return "\u5DF2\u8D85\u65F6";
1095
+ if (state.terminal === "error") return "\u51FA\u9519";
1096
+ if (state.terminal === "done") return "\u5DF2\u5B8C\u6210";
1097
+ if (state.footer === "tool_running") return "\u6B63\u5728\u8C03\u7528\u5DE5\u5177";
1098
+ if (state.footer === "streaming") return "\u6B63\u5728\u8F93\u51FA";
1099
+ return "\u601D\u8003\u4E2D";
1100
+ }
1101
+ function stopButton() {
1102
+ return {
1103
+ tag: "button",
1104
+ text: { tag: "plain_text", content: "\u23F9 \u7EC8\u6B62" },
1105
+ type: "danger",
1106
+ value: { cmd: "stop" }
1107
+ };
1108
+ }
1109
+ function textBlock(block) {
1110
+ return markdown(block.content);
1111
+ }
1112
+ function toolBlock(tool) {
1113
+ const icon = tool.status === "error" ? "\u26A0\uFE0F" : tool.status === "done" ? "\u2705" : "\u23F3";
1114
+ return markdown(`${icon} **${tool.name}**`);
1115
+ }
1116
+ function renderCard(state) {
1117
+ const elements = [];
1118
+ if (state.reasoning.content) {
1119
+ elements.push(
1120
+ noteMd(
1121
+ state.reasoning.active ? "\u{1F9E0} \u6B63\u5728\u601D\u8003\u2026" : `\u{1F9E0} \u601D\u8003\u5B8C\u6210\uFF1A${state.reasoning.content.slice(0, 300)}`
1122
+ )
1123
+ );
1124
+ }
1125
+ for (const block of state.blocks) {
1126
+ elements.push(block.kind === "text" ? textBlock(block) : toolBlock(block.tool));
1127
+ }
1128
+ if (state.terminal === "interrupted") {
1129
+ elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
1130
+ } else if (state.terminal === "idle_timeout") {
1131
+ elements.push(noteMd(`_\u23F1 ${state.idleTimeoutMinutes ?? 0} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
1132
+ } else if (state.terminal === "error" && state.errorMsg) {
1133
+ elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
1134
+ } else if (state.terminal === "done" && elements.length === 0) {
1135
+ elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
1136
+ }
1137
+ if (state.terminal === "running") {
1138
+ if (state.footer) elements.push(footerStatus(state.footer));
1139
+ elements.push(stopButton());
1140
+ }
1141
+ return {
1142
+ schema: "2.0",
1143
+ config: {
1144
+ streaming_mode: state.terminal === "running",
1145
+ summary: { content: summaryText(state) }
1146
+ },
1147
+ body: { elements }
1148
+ };
1149
+ }
1150
+
1151
+ // src/bridge/run-flow.ts
1152
+ async function runAgentBatch(input) {
1153
+ const replyOptions = input.replyTo ? { replyTo: input.replyTo } : {};
1154
+ if (input.activeRuns.get(input.scope)) {
1155
+ await input.channel.sendMarkdown(input.chatId, "\u5F53\u524D\u4F1A\u8BDD\u5DF2\u6709\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148 `/stop` \u6216\u7B49\u5F85\u5B8C\u6210\u3002", {
1156
+ ...replyOptions
1157
+ });
1158
+ return;
1159
+ }
1160
+ const requestedCwd = input.workspaces.cwdFor(input.scope) ?? input.defaultWorkspace;
1161
+ const workspace = input.workspaceManager ? await input.workspaceManager.ensure(input.scope, requestedCwd) : { cwd: requestedCwd };
1162
+ const cwd = workspace.cwd;
1163
+ const sessionId = input.sessions.resumeFor(input.scope, cwd);
1164
+ const prompt = input.messages.join("\n\n");
1165
+ const runId = randomUUID();
1166
+ const run = input.adapter.run({
1167
+ runId,
1168
+ prompt,
1169
+ cwd,
1170
+ sessionId,
1171
+ model: input.model,
1172
+ images: void 0,
1173
+ stopGraceMs: input.stopGraceMs
1174
+ });
1175
+ input.activeRuns.set(input.scope, { runId, stop: run.stop });
1176
+ let state = initialState;
1177
+ const stopRequested = { value: false };
1178
+ const timeoutMs = input.runPolicies?.get(input.scope) ?? input.runTimeoutMs ?? 0;
1179
+ let timedOut = false;
1180
+ try {
1181
+ await input.channel.streamCard(
1182
+ input.chatId,
1183
+ renderCard(state),
1184
+ async (controller) => {
1185
+ const consume = async () => {
1186
+ for await (const event of run.events) {
1187
+ if (timedOut) return;
1188
+ state = applyEvent(state, event, stopRequested);
1189
+ if (event.type === "system" && event.sessionId) {
1190
+ input.sessions.set(input.scope, event.sessionId, event.cwd ?? cwd);
1191
+ }
1192
+ await controller.update(renderCard(state));
1193
+ }
1194
+ };
1195
+ let timeoutTimer;
1196
+ const timeoutPromise = timeoutMs > 0 ? new Promise((resolve4) => {
1197
+ timeoutTimer = setTimeout(() => {
1198
+ timedOut = true;
1199
+ void run.stop();
1200
+ resolve4();
1201
+ }, timeoutMs);
1202
+ }) : void 0;
1203
+ try {
1204
+ if (timeoutPromise) {
1205
+ await Promise.race([consume(), timeoutPromise]);
1206
+ } else {
1207
+ await consume();
1208
+ }
1209
+ state = timedOut ? markIdleTimeout(state, timeoutMs / 6e4) : finalizeIfRunning(state);
1210
+ await controller.update(renderCard(state));
1211
+ } finally {
1212
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
1213
+ }
1214
+ },
1215
+ replyOptions
1216
+ );
1217
+ } catch (error) {
1218
+ log.fail("run-flow", error, { scope: input.scope, runId });
1219
+ state = markInterrupted(state);
1220
+ try {
1221
+ await input.channel.sendMarkdown(input.chatId, `\u26A0\uFE0F agent \u8FD0\u884C\u5931\u8D25\uFF1A${errorMessage(error)}`, {
1222
+ ...replyOptions
1223
+ });
1224
+ } catch {
1225
+ }
1226
+ } finally {
1227
+ input.activeRuns.delete(input.scope);
1228
+ }
1229
+ }
1230
+ function applyEvent(state, event, stopRequested) {
1231
+ if (event.type === "done" && event.terminationReason === "interrupted") {
1232
+ stopRequested.value = true;
1233
+ return markInterrupted(state);
1234
+ }
1235
+ if (event.type === "error" && event.terminationReason === "timeout") {
1236
+ return markIdleTimeout(state, 0);
1237
+ }
1238
+ return reduce(state, event);
1239
+ }
1240
+ function errorMessage(error) {
1241
+ return error instanceof Error ? error.message : String(error);
1242
+ }
1243
+
1244
+ // src/onboard/registration.ts
1245
+ import { registerApp } from "@larksuite/channel";
1246
+ import qrcode from "qrcode-terminal";
1247
+ var DEFAULT_SOURCE = "dsh-lark-bot";
1248
+ async function onboardPersonalAgent(deps = {}) {
1249
+ const register = deps.register ?? registerApp;
1250
+ const renderQr = deps.renderQr ?? ((value) => qrcode.generate(value, { small: true }));
1251
+ const print = deps.print ?? ((line) => process.stdout.write(`${line}
1252
+ `));
1253
+ print("");
1254
+ print("\u672A\u68C0\u6D4B\u5230\u98DE\u4E66 / Lark \u5E94\u7528\u51ED\u636E\uFF0C\u8FDB\u5165\u626B\u7801\u521B\u5EFA\u5411\u5BFC\u3002");
1255
+ print("");
1256
+ const result = await register({
1257
+ source: deps.source ?? DEFAULT_SOURCE,
1258
+ ...deps.signal ? { signal: deps.signal } : {},
1259
+ onQRCodeReady: (info) => {
1260
+ print("\u8BF7\u4F7F\u7528\u98DE\u4E66 / Lark App \u626B\u63CF\u4EE5\u4E0B\u4E8C\u7EF4\u7801\uFF0C\u521B\u5EFA\u6216\u9009\u62E9 PersonalAgent \u5E94\u7528\uFF1A");
1261
+ print("");
1262
+ renderQr(info.url);
1263
+ print("");
1264
+ const minutes = Math.max(1, Math.round(info.expireIn / 60));
1265
+ print(`\u4E8C\u7EF4\u7801\u6709\u6548\u671F\u7EA6 ${minutes} \u5206\u949F\u3002`);
1266
+ print(`\u4E5F\u53EF\u4EE5\u76F4\u63A5\u5728\u6D4F\u89C8\u5668\u6253\u5F00\uFF1A${info.url}`);
1267
+ print("");
1268
+ },
1269
+ onStatusChange: (info) => {
1270
+ if (info.status === "domain_switched") {
1271
+ print("\u5DF2\u8BC6\u522B\u5230\u56FD\u9645\u7248\u79DF\u6237\uFF0C\u81EA\u52A8\u5207\u6362\u5230 larksuite.com \u57DF\u540D\u3002");
1272
+ } else if (info.status === "slow_down") {
1273
+ print("\u8F6E\u8BE2\u901F\u5EA6\u8FC7\u5FEB\uFF0C\u5DF2\u81EA\u52A8\u964D\u901F\u3002");
1274
+ }
1275
+ }
1276
+ });
1277
+ const tenant = result.user_info?.tenant_brand === "lark" ? "lark" : "feishu";
1278
+ print("\u2713 PersonalAgent \u5E94\u7528\u521B\u5EFA / \u7ED1\u5B9A\u6210\u529F\u3002");
1279
+ print(` App ID: ${result.client_id}`);
1280
+ print(` Tenant: ${tenant}`);
1281
+ print("");
1282
+ return {
1283
+ appId: result.client_id,
1284
+ appSecret: result.client_secret,
1285
+ tenant
1286
+ };
1287
+ }
1288
+
1289
+ // src/session/store.ts
1290
+ import { readFile as readFile2 } from "fs/promises";
1291
+ var SessionStore = class {
1292
+ data = { chats: {} };
1293
+ saving = Promise.resolve();
1294
+ path;
1295
+ constructor(path) {
1296
+ this.path = path;
1297
+ }
1298
+ async load() {
1299
+ try {
1300
+ const raw = await readFile2(this.path, "utf8");
1301
+ const parsed = JSON.parse(raw);
1302
+ this.data = {
1303
+ chats: parsed.chats ?? {}
1304
+ };
1305
+ } catch (error) {
1306
+ if (error.code === "ENOENT") return;
1307
+ throw error;
1308
+ }
1309
+ }
1310
+ getRaw(scopeId) {
1311
+ return this.data.chats[scopeId];
1312
+ }
1313
+ set(scopeId, sessionId, cwd) {
1314
+ this.data.chats[scopeId] = { sessionId, cwd };
1315
+ this.schedulePersist();
1316
+ }
1317
+ clear(scopeId) {
1318
+ if (!(scopeId in this.data.chats)) return false;
1319
+ delete this.data.chats[scopeId];
1320
+ this.schedulePersist();
1321
+ return true;
1322
+ }
1323
+ resumeFor(scopeId, cwd) {
1324
+ const record = this.data.chats[scopeId];
1325
+ return record && record.cwd === cwd ? record.sessionId : void 0;
1326
+ }
1327
+ async flush() {
1328
+ await this.saving;
1329
+ }
1330
+ schedulePersist() {
1331
+ const snapshot = this.snapshot();
1332
+ this.saving = this.saving.then(async () => {
1333
+ await writeFileAtomic(this.path, `${JSON.stringify(snapshot, null, 2)}
1334
+ `, {
1335
+ mode: 384
1336
+ });
1337
+ }).catch((error) => {
1338
+ log.fail("session", error, { step: "persist" });
1339
+ });
1340
+ }
1341
+ snapshot() {
1342
+ return {
1343
+ chats: { ...this.data.chats }
1344
+ };
1345
+ }
1346
+ };
1347
+
1348
+ // src/workspace/git-worktree.ts
1349
+ import { execFile } from "child_process";
1350
+ import { randomBytes as randomBytes2 } from "crypto";
1351
+ import { access, copyFile, mkdir as mkdir3 } from "fs/promises";
1352
+ import { dirname as dirname3, join as join4 } from "path";
1353
+ import { promisify } from "util";
1354
+ var execFileAsync = promisify(execFile);
1355
+ async function defaultRunGit(args, cwd) {
1356
+ const result = await execFileAsync("git", args, {
1357
+ cwd,
1358
+ encoding: "utf8"
1359
+ });
1360
+ return result.stdout;
1361
+ }
1362
+ async function exists(path) {
1363
+ try {
1364
+ await access(path);
1365
+ return true;
1366
+ } catch {
1367
+ return false;
1368
+ }
1369
+ }
1370
+ async function defaultCopyRulesFile(source, target) {
1371
+ await mkdir3(dirname3(target), { recursive: true });
1372
+ await copyFile(source, target);
1373
+ }
1374
+ function slugify(scope) {
1375
+ const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
1376
+ return slug || "session";
1377
+ }
1378
+ var GitWorktreeManager = class {
1379
+ constructor(options) {
1380
+ this.options = options;
1381
+ this.runGit = options.runGit ?? defaultRunGit;
1382
+ this.copyRulesFile = options.copyRulesFile ?? defaultCopyRulesFile;
1383
+ }
1384
+ options;
1385
+ runGit;
1386
+ copyRulesFile;
1387
+ async ensure(scope, base) {
1388
+ const isGit = await this.isGitRepository(base);
1389
+ if (!isGit) return { cwd: base, created: false };
1390
+ const slug = slugify(scope);
1391
+ const target = join4(this.options.worktreesRoot, slug);
1392
+ if (await exists(target)) {
1393
+ await this.ensureProjectRules(base, target);
1394
+ return { cwd: target, created: false };
1395
+ }
1396
+ const branch = `dsh-lark/${slug}-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`;
1397
+ await mkdir3(this.options.worktreesRoot, { recursive: true });
1398
+ await this.runGit(["worktree", "add", "-b", branch, target, "HEAD"], base);
1399
+ await this.ensureProjectRules(base, target);
1400
+ return { cwd: target, created: true, branch };
1401
+ }
1402
+ async isGitRepository(cwd) {
1403
+ try {
1404
+ const result = await this.runGit(["rev-parse", "--is-inside-work-tree"], cwd);
1405
+ return result.trim() === "true";
1406
+ } catch {
1407
+ return false;
1408
+ }
1409
+ }
1410
+ async ensureProjectRules(base, target) {
1411
+ const destination = join4(target, "AGENTS.md");
1412
+ if (await exists(destination)) return;
1413
+ const sourceCandidates = [join4(base, ".dsh-lark", "AGENTS.md"), join4(base, "AGENTS.md")];
1414
+ for (const source of sourceCandidates) {
1415
+ if (await exists(source)) {
1416
+ await this.copyRulesFile(source, destination);
1417
+ return;
1418
+ }
1419
+ }
1420
+ }
1421
+ };
1422
+
1423
+ // src/workspace/store.ts
1424
+ import { readFile as readFile3 } from "fs/promises";
1425
+ var WorkspaceStore = class {
1426
+ data = { chats: {}, named: {} };
1427
+ saving = Promise.resolve();
1428
+ path;
1429
+ constructor(path) {
1430
+ this.path = path;
1431
+ }
1432
+ async load() {
1433
+ try {
1434
+ const raw = await readFile3(this.path, "utf8");
1435
+ const parsed = JSON.parse(raw);
1436
+ this.data = {
1437
+ chats: parsed.chats ?? {},
1438
+ named: parsed.named ?? {}
1439
+ };
1440
+ } catch (error) {
1441
+ if (error.code === "ENOENT") return;
1442
+ throw error;
1443
+ }
1444
+ }
1445
+ cwdFor(scopeId) {
1446
+ return this.data.chats[scopeId]?.cwd;
1447
+ }
1448
+ setCwd(scopeId, cwd) {
1449
+ this.data.chats[scopeId] = { cwd };
1450
+ this.schedulePersist();
1451
+ }
1452
+ removeCwd(scopeId) {
1453
+ if (!(scopeId in this.data.chats)) return false;
1454
+ delete this.data.chats[scopeId];
1455
+ this.schedulePersist();
1456
+ return true;
1457
+ }
1458
+ listNamed() {
1459
+ return { ...this.data.named };
1460
+ }
1461
+ getNamed(name) {
1462
+ return this.data.named[name];
1463
+ }
1464
+ saveNamed(name, cwd) {
1465
+ this.data.named[name] = cwd;
1466
+ this.schedulePersist();
1467
+ }
1468
+ removeNamed(name) {
1469
+ if (!(name in this.data.named)) return false;
1470
+ delete this.data.named[name];
1471
+ this.schedulePersist();
1472
+ return true;
1473
+ }
1474
+ async flush() {
1475
+ await this.saving;
1476
+ }
1477
+ schedulePersist() {
1478
+ const snapshot = {
1479
+ chats: { ...this.data.chats },
1480
+ named: { ...this.data.named }
1481
+ };
1482
+ this.saving = this.saving.then(async () => {
1483
+ await writeFileAtomic(this.path, `${JSON.stringify(snapshot, null, 2)}
1484
+ `, {
1485
+ mode: 384
1486
+ });
1487
+ }).catch((error) => {
1488
+ log.fail("workspace", error, { step: "persist" });
1489
+ });
1490
+ }
1491
+ };
1492
+
1493
+ // src/cli/commands/start.ts
1494
+ var DEBOUNCE_MS = 600;
1495
+ async function runStart(options) {
1496
+ const env = loadRuntimeEnv({
1497
+ ...process.env,
1498
+ ...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
1499
+ ...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
1500
+ ...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
1501
+ ...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
1502
+ });
1503
+ const paths = resolveAppPaths(env.home);
1504
+ const profileName = options.profile ?? "default";
1505
+ const configStore = new ConfigStore(paths.configFile);
1506
+ await configStore.load();
1507
+ if (env.appId && env.appSecret) {
1508
+ const profileInput = {
1509
+ tenant: env.tenant,
1510
+ appId: env.appId,
1511
+ appSecret: env.appSecret,
1512
+ model: env.model,
1513
+ stopGraceMs: env.stopGraceMs,
1514
+ runTimeoutMs: env.runTimeoutMs
1515
+ };
1516
+ if (env.workspace !== void 0) profileInput.workspace = env.workspace;
1517
+ await configStore.saveProfile(profileName, profileInput);
1518
+ }
1519
+ const profile = configStore.getProfile(profileName);
1520
+ if (!profile) {
1521
+ try {
1522
+ const created = await onboardPersonalAgent();
1523
+ const onboardingProfile = {
1524
+ tenant: created.tenant,
1525
+ appId: created.appId,
1526
+ appSecret: created.appSecret,
1527
+ model: env.model,
1528
+ stopGraceMs: env.stopGraceMs,
1529
+ runTimeoutMs: env.runTimeoutMs
1530
+ };
1531
+ if (env.workspace !== void 0) onboardingProfile.workspace = env.workspace;
1532
+ await configStore.saveProfile(profileName, onboardingProfile);
1533
+ } catch (error) {
1534
+ log.fail("onboarding", error);
1535
+ process.stderr.write("\u626B\u7801\u521B\u5EFA\u5E94\u7528\u5931\u8D25\uFF0C\u672A\u5199\u5165\u672C\u5730\u914D\u7F6E\u3002\n");
1536
+ process.exitCode = 1;
1537
+ return;
1538
+ }
1539
+ }
1540
+ const activeProfile = configStore.getProfile(profileName);
1541
+ if (!activeProfile) {
1542
+ process.stderr.write("\u672C\u5730\u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5\u3002\n");
1543
+ process.exitCode = 1;
1544
+ return;
1545
+ }
1546
+ const defaultWorkspace = options.workspace ?? activeProfile.workspaces.default ?? env.workspace ?? paths.profilePath(profileName, "workspace");
1547
+ await mkdir4(defaultWorkspace, { recursive: true });
1548
+ const sessions = new SessionStore(paths.sessionsFile(profileName));
1549
+ const workspaces = new WorkspaceStore(paths.workspacesFile(profileName));
1550
+ const worktreeManager = new GitWorktreeManager({
1551
+ worktreesRoot: paths.profilePath(profileName, "worktrees")
1552
+ });
1553
+ await Promise.all([sessions.load(), workspaces.load()]);
1554
+ const adapterOptions = {
1555
+ command: env.dshCommand,
1556
+ args: env.dshArgs,
1557
+ stopGraceMs: env.stopGraceMs
1558
+ };
1559
+ if (activeProfile.preferences.stopGraceMs !== void 0) {
1560
+ Object.assign(adapterOptions, { stopGraceMs: activeProfile.preferences.stopGraceMs });
1561
+ }
1562
+ const adapter = new DshAdapter(adapterOptions);
1563
+ const activeRuns = new ActiveRuns();
1564
+ const runPolicies = new RunPolicyStore();
1565
+ let streaming;
1566
+ const pending = new PendingQueue(DEBOUNCE_MS, async (scope, batch) => {
1567
+ if (!streaming) return;
1568
+ const first = batch[0];
1569
+ if (!first) return;
1570
+ pending.block(scope);
1571
+ try {
1572
+ const runInput = {
1573
+ scope,
1574
+ chatId: first.chatId,
1575
+ messages: batch.map((message) => message.content),
1576
+ adapter,
1577
+ sessions,
1578
+ workspaces,
1579
+ workspaceManager: worktreeManager,
1580
+ activeRuns,
1581
+ runPolicies,
1582
+ channel: streaming,
1583
+ defaultWorkspace,
1584
+ replyTo: first.messageId,
1585
+ runTimeoutMs: activeProfile.preferences.runTimeoutMs ?? env.runTimeoutMs
1586
+ };
1587
+ if (activeProfile.preferences.model !== void 0) runInput.model = activeProfile.preferences.model;
1588
+ if (activeProfile.preferences.stopGraceMs !== void 0) {
1589
+ runInput.stopGraceMs = activeProfile.preferences.stopGraceMs;
1590
+ }
1591
+ await runAgentBatch(runInput);
1592
+ } finally {
1593
+ pending.unblock(scope);
1594
+ }
1595
+ });
1596
+ const channelInput = {
1597
+ appId: activeProfile.accounts.appId,
1598
+ appSecret: activeProfile.accounts.appSecret,
1599
+ tenant: activeProfile.tenant,
1600
+ adapter,
1601
+ sessions,
1602
+ workspaces,
1603
+ activeRuns,
1604
+ runPolicies,
1605
+ defaultRunTimeoutMs: activeProfile.preferences.runTimeoutMs ?? env.runTimeoutMs,
1606
+ pending,
1607
+ defaultWorkspace
1608
+ };
1609
+ if (activeProfile.preferences.model !== void 0) channelInput.model = activeProfile.preferences.model;
1610
+ if (activeProfile.preferences.stopGraceMs !== void 0) {
1611
+ channelInput.stopGraceMs = activeProfile.preferences.stopGraceMs;
1612
+ }
1613
+ const bridge = await startChannel(channelInput);
1614
+ streaming = adaptLarkChannel(bridge.channel);
1615
+ log.info("cli", "started", {
1616
+ profile: profileName,
1617
+ home: paths.root,
1618
+ tenant: activeProfile.tenant,
1619
+ workspace: defaultWorkspace
1620
+ });
1621
+ process.stdout.write(`dsh-lark-bot \u5DF2\u542F\u52A8\uFF0Cprofile=${profileName}
1622
+ `);
1623
+ await waitForShutdown();
1624
+ await bridge.disconnect();
1625
+ await Promise.all([sessions.flush(), workspaces.flush()]);
1626
+ }
1627
+ function waitForShutdown() {
1628
+ return new Promise((resolve4) => {
1629
+ const shutdown = () => {
1630
+ process.off("SIGINT", shutdown);
1631
+ process.off("SIGTERM", shutdown);
1632
+ resolve4();
1633
+ };
1634
+ process.once("SIGINT", shutdown);
1635
+ process.once("SIGTERM", shutdown);
1636
+ });
1637
+ }
1638
+
1639
+ // src/cli.ts
1640
+ function packageVersion() {
1641
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
1642
+ const pkg = JSON.parse(raw);
1643
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
1644
+ }
1645
+ function buildProgram() {
1646
+ const program = new Command();
1647
+ program.name("dsh-lark-bot").description("Bridge DeepSeek Harness into Feishu / Lark").version(packageVersion(), "-v, --version");
1648
+ program.command("start").description("Start the bridge in the foreground").option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark").action(async (opts) => {
1649
+ await runStart(opts);
1650
+ });
1651
+ program.command("doctor").description("Run local diagnostics").option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark").action(async (opts) => {
1652
+ await runDoctor({ ...opts, version: packageVersion() });
1653
+ });
1654
+ return program;
1655
+ }
1656
+ async function main(argv = process.argv) {
1657
+ await buildProgram().parseAsync([...argv]);
1658
+ }
1659
+ export {
1660
+ buildProgram,
1661
+ main
1662
+ };
1663
+ //# sourceMappingURL=cli.js.map