dsh-lark-bot 0.2.7 → 0.3.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 CHANGED
@@ -1,111 +1,87 @@
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";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
7
10
 
8
- // src/adapters/dsh/availability.ts
9
- import { spawn } from "cross-spawn";
10
- import { existsSync } from "fs";
11
- async function checkDshAvailability(options) {
12
- const timeoutMs = options.timeoutMs ?? 5e3;
13
- const versionArgs = options.command === "node" && options.args?.[0] && existsSync(options.args[0]) ? [options.args[0], "--version"] : ["--version"];
14
- return new Promise((resolve4) => {
15
- const child = spawn(options.command, versionArgs, {
16
- stdio: ["ignore", "pipe", "pipe"]
17
- });
18
- let stdout = "";
19
- let stderr = "";
20
- let settled = false;
21
- const finish = (result) => {
22
- if (settled) return;
23
- settled = true;
24
- resolve4(result);
25
- };
26
- const timer = setTimeout(() => {
27
- child.kill("SIGKILL");
28
- finish({ ok: false, error: `timed out after ${timeoutMs}ms`, version: void 0 });
29
- }, timeoutMs);
30
- child.stdout?.on("data", (chunk) => {
31
- stdout += chunk.toString("utf8");
32
- });
33
- child.stderr?.on("data", (chunk) => {
34
- stderr += chunk.toString("utf8");
35
- });
36
- child.on("error", (error) => {
37
- clearTimeout(timer);
38
- finish({ ok: false, error: error.message, version: void 0 });
39
- });
40
- child.on("exit", (code) => {
41
- clearTimeout(timer);
42
- const version = stdout.trim();
43
- if (code !== 0 && !version) {
44
- finish({ ok: false, error: stderr.trim() || `exited with code ${String(code)}`, version: void 0 });
45
- return;
11
+ // src/adapters/dsh/event-channel.ts
12
+ var EventChannel;
13
+ var init_event_channel = __esm({
14
+ "src/adapters/dsh/event-channel.ts"() {
15
+ "use strict";
16
+ EventChannel = class _EventChannel {
17
+ static END = /* @__PURE__ */ Symbol("event-channel-end");
18
+ queue = [];
19
+ waiters = [];
20
+ closed = false;
21
+ push(event) {
22
+ if (this.closed) return;
23
+ const waiter = this.waiters.shift();
24
+ if (waiter) {
25
+ waiter(event);
26
+ } else {
27
+ this.queue.push(event);
28
+ }
46
29
  }
47
- finish({ ok: true, error: void 0, version: version || void 0 });
48
- });
49
- });
50
- }
51
-
52
- // src/config/app-paths.ts
53
- import { homedir } from "os";
54
- import { join, resolve } from "path";
55
- function defaultHome() {
56
- const override = process.env.DSH_LARK_HOME?.trim();
57
- return override ? resolve(override) : join(homedir(), ".dsh-lark");
58
- }
59
- function resolveAppPaths(root = defaultHome()) {
60
- const profileDir = (profile) => join(root, "profiles", profile);
61
- const profilePath = (profile, ...parts) => join(profileDir(profile), ...parts);
62
- return {
63
- root,
64
- configFile: join(root, "config.json"),
65
- activeProfileFile: join(root, "active-profile"),
66
- profileDir,
67
- profilePath,
68
- sessionsFile: (profile) => profilePath(profile, "sessions.json"),
69
- sessionCatalogFile: (profile) => profilePath(profile, "sessions.json.catalog.json"),
70
- workspacesFile: (profile) => profilePath(profile, "workspaces.json"),
71
- mediaDir: (profile) => profilePath(profile, "media"),
72
- logsDir: (profile) => profilePath(profile, "logs"),
73
- registryFile: join(root, "registry", "processes.json"),
74
- locksDir: join(root, "registry", "locks")
75
- };
76
- }
77
-
78
- // src/config/env.ts
79
- import { join as join3, resolve as resolve2 } from "path";
80
- import { homedir as homedir2 } from "os";
30
+ close() {
31
+ if (this.closed) return;
32
+ this.closed = true;
33
+ for (const waiter of this.waiters.splice(0)) {
34
+ waiter(_EventChannel.END);
35
+ }
36
+ }
37
+ async *[Symbol.asyncIterator]() {
38
+ for (; ; ) {
39
+ if (this.closed && this.queue.length === 0) return;
40
+ const queued = this.queue.shift();
41
+ if (queued !== void 0) {
42
+ yield queued;
43
+ continue;
44
+ }
45
+ const value = await new Promise((resolve5) => {
46
+ this.waiters.push(resolve5);
47
+ });
48
+ if (value === _EventChannel.END) return;
49
+ yield value;
50
+ }
51
+ }
52
+ };
53
+ }
54
+ });
81
55
 
82
56
  // src/config/dsh-runtime.ts
83
57
  import { existsSync as existsSync2, readdirSync } from "fs";
84
- import { join as join2 } from "path";
85
- var DS_HARNESS_RELATIVE = join2("@deepseek-ai", "dsh", "lib", "bin.js");
58
+ import { join } from "path";
59
+ function resolveDshHome(home, env = process.env) {
60
+ return env.DSH_HOME?.trim() || join(home, ".dsh");
61
+ }
86
62
  function firstExisting(...paths) {
87
63
  return paths.find((path) => existsSync2(path));
88
64
  }
89
65
  function children(root) {
90
66
  try {
91
- return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => join2(root, entry.name));
67
+ return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => join(root, entry.name));
92
68
  } catch {
93
69
  return [];
94
70
  }
95
71
  }
96
72
  function discoverDshBin(home, env = process.env) {
97
- const dshHome = env.DSH_HOME?.trim() || join2(home, ".dsh");
73
+ const dshHome = resolveDshHome(home, env);
98
74
  const candidates = [
99
- join2(dshHome, "profiles", "node_modules", DS_HARNESS_RELATIVE),
100
- join2(home, ".npm", "_npx", "node_modules", DS_HARNESS_RELATIVE),
101
- join2(home, ".cache", "pnpm", "dlx", "node_modules", DS_HARNESS_RELATIVE),
102
- join2(home, ".local", "share", "pnpm", "node_modules", DS_HARNESS_RELATIVE)
75
+ join(dshHome, "profiles", "node_modules", DS_HARNESS_RELATIVE),
76
+ join(home, ".npm", "_npx", "node_modules", DS_HARNESS_RELATIVE),
77
+ join(home, ".cache", "pnpm", "dlx", "node_modules", DS_HARNESS_RELATIVE),
78
+ join(home, ".local", "share", "pnpm", "node_modules", DS_HARNESS_RELATIVE)
103
79
  ];
104
80
  const direct = firstExisting(...candidates);
105
81
  if (direct) return direct;
106
- for (const root of [join2(home, ".npm", "_npx"), join2(home, ".cache", "pnpm", "dlx")]) {
82
+ for (const root of [join(home, ".npm", "_npx"), join(home, ".cache", "pnpm", "dlx")]) {
107
83
  for (const child of children(root)) {
108
- const path = join2(child, "node_modules", DS_HARNESS_RELATIVE);
84
+ const path = join(child, "node_modules", DS_HARNESS_RELATIVE);
109
85
  if (existsSync2(path)) return path;
110
86
  }
111
87
  }
@@ -131,271 +107,540 @@ function resolveDshRuntime(input) {
131
107
  args: ["--profile", "headless"]
132
108
  };
133
109
  }
134
-
135
- // src/config/env.ts
136
- var DEFAULTS = {
137
- tenant: "feishu",
138
- provider: "deepseek-official",
139
- model: "deepseek-v4-flash",
140
- runTimeoutMs: 3e5,
141
- stopGraceMs: 5e3
142
- };
143
- function nonEmpty(value) {
144
- const trimmed = value?.trim();
145
- return trimmed ? trimmed : void 0;
146
- }
147
- function parseTenant(value) {
148
- const tenant = nonEmpty(value) ?? DEFAULTS.tenant;
149
- if (tenant !== "feishu" && tenant !== "lark") {
150
- throw new Error(`DSH_LARK_TENANT must be "feishu" or "lark", got "${tenant}"`);
110
+ var DS_HARNESS_RELATIVE;
111
+ var init_dsh_runtime = __esm({
112
+ "src/config/dsh-runtime.ts"() {
113
+ "use strict";
114
+ DS_HARNESS_RELATIVE = join("@deepseek-ai", "dsh", "lib", "bin.js");
151
115
  }
152
- return tenant;
116
+ });
117
+
118
+ // src/adapters/dsh/acp-runtime.ts
119
+ import { spawn as spawn4 } from "child_process";
120
+ import { existsSync as existsSync4 } from "fs";
121
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
122
+ import { join as join3 } from "path";
123
+ function acpProfileRoot(home, profile, env) {
124
+ return join3(resolveDshHome(home, env), "profiles", profile);
125
+ }
126
+ function packageJsonFor2(profile) {
127
+ return `${JSON.stringify(
128
+ {
129
+ name: `dsh-profile-${profile}`,
130
+ private: true,
131
+ dependencies: {
132
+ [ACP_PACKAGE]: ACP_VERSION
133
+ },
134
+ dsh: {
135
+ profile: {
136
+ bundles: [ACP_BASE_BUNDLE]
137
+ }
138
+ }
139
+ },
140
+ null,
141
+ 2
142
+ )}
143
+ `;
153
144
  }
154
- function parseDshArgs(value) {
155
- const raw = value?.trim();
156
- if (!raw) return [];
157
- return raw.split(",").map((item) => item.trim()).filter(Boolean);
145
+ function acpPatchYaml(provider, model) {
146
+ return [
147
+ "# dsh-lark ACP JSON-RPC runtime overlay (managed by dsh-lark-bot).",
148
+ "# stdout is reserved for ACP JSON-RPC frames; no console logger may load.",
149
+ "- insert:",
150
+ " - id: acp",
151
+ ` name: '${ACP_PACKAGE}'`,
152
+ " config:",
153
+ ` provider: ${provider}`,
154
+ ` model: ${model}`,
155
+ "",
156
+ "# Unattended IM runtime: interactive user questions cannot be answered",
157
+ "# through Feishu, so the tool is disabled (default-deny).",
158
+ "- id: user-questions",
159
+ " disabled: true",
160
+ "",
161
+ "- id: system-prompt",
162
+ " config:",
163
+ " persona: >-",
164
+ " You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.",
165
+ "",
166
+ "- id: hmr",
167
+ " disabled: true",
168
+ ""
169
+ ].join("\n");
158
170
  }
159
- function parseTimeout(value) {
160
- const raw = value?.trim();
161
- if (!raw) return DEFAULTS.runTimeoutMs;
162
- const parsed = Number(raw);
163
- if (!Number.isInteger(parsed) || parsed < 0) {
164
- throw new Error(`DSH_LARK_RUN_TIMEOUT_MS must be a non-negative integer, got "${raw}"`);
165
- }
166
- return parsed;
171
+ function acpPluginInstalled(profileRoot) {
172
+ const candidates = [
173
+ join3(profileRoot, "node_modules", ACP_PACKAGE),
174
+ join3(profileRoot, "..", "node_modules", ACP_PACKAGE)
175
+ ];
176
+ return candidates.some((path) => existsSync4(path));
167
177
  }
168
- function parseStopGrace(value) {
169
- const raw = value?.trim();
170
- if (!raw) return DEFAULTS.stopGraceMs;
171
- const parsed = Number(raw);
172
- if (!Number.isInteger(parsed) || parsed < 0) {
173
- throw new Error(`DSH_LARK_STOP_GRACE_MS must be a non-negative integer, got "${raw}"`);
174
- }
175
- return parsed;
178
+ function isAcpProfileReady(profileRoot) {
179
+ return existsSync4(join3(profileRoot, "package.json")) && existsSync4(join3(profileRoot, "cordis.yml")) && existsSync4(join3(profileRoot, "cordis.patch.yml")) && acpPluginInstalled(profileRoot);
176
180
  }
177
- function loadRuntimeEnv(source = process.env) {
178
- const homeOverride = nonEmpty(source.DSH_LARK_HOME);
179
- const workspace = nonEmpty(source.DSH_LARK_WORKSPACE);
180
- const home = homeOverride ? resolve2(homeOverride) : join3(homedir2(), ".dsh-lark");
181
- const osHome = homedir2();
182
- const dshCommand = nonEmpty(source.DSH_LARK_DSH_COMMAND);
183
- const rawDshArgs = nonEmpty(source.DSH_LARK_DSH_ARGS);
184
- const dshRuntime = resolveDshRuntime({
185
- home: osHome,
186
- env: source,
187
- ...dshCommand ? { command: dshCommand } : {},
188
- ...rawDshArgs ? { args: parseDshArgs(rawDshArgs) } : {}
181
+ function runPnpmInstall2(profileRoot) {
182
+ return new Promise((resolve5, reject) => {
183
+ const child = spawn4("pnpm", ["install"], {
184
+ cwd: profileRoot,
185
+ stdio: ["ignore", "pipe", "pipe"]
186
+ });
187
+ let output = "";
188
+ child.stdout.on("data", (chunk) => {
189
+ output += chunk.toString("utf8");
190
+ });
191
+ child.stderr.on("data", (chunk) => {
192
+ output += chunk.toString("utf8");
193
+ });
194
+ child.on("error", (error) => {
195
+ reject(error);
196
+ });
197
+ child.on("close", (code) => {
198
+ if (code === 0) {
199
+ resolve5();
200
+ return;
201
+ }
202
+ const tail = output.trim().split("\n").slice(-8).join("\n");
203
+ reject(new Error(`pnpm install exited with code ${String(code)}
204
+ ${tail}`));
205
+ });
189
206
  });
190
- return {
191
- home,
192
- tenant: parseTenant(source.DSH_LARK_TENANT),
193
- appId: nonEmpty(source.DSH_LARK_APP_ID),
194
- appSecret: nonEmpty(source.DSH_LARK_APP_SECRET),
195
- workspace: workspace ? resolve2(workspace) : void 0,
196
- dshCommand: dshRuntime.command,
197
- dshArgs: dshRuntime.args,
198
- provider: nonEmpty(source.DSH_LARK_PROVIDER) ?? DEFAULTS.provider,
199
- model: nonEmpty(source.DSH_LARK_MODEL) ?? DEFAULTS.model,
200
- runTimeoutMs: parseTimeout(source.DSH_LARK_RUN_TIMEOUT_MS),
201
- stopGraceMs: parseStopGrace(source.DSH_LARK_STOP_GRACE_MS)
202
- };
203
207
  }
204
-
205
- // src/config/profile-store.ts
206
- import { readFile } from "fs/promises";
207
- import { mkdir as mkdir2 } from "fs/promises";
208
- import { dirname as dirname2 } from "path";
209
-
210
- // src/platform/atomic-write.ts
211
- import { randomBytes } from "crypto";
212
- import { mkdir, rename, writeFile } from "fs/promises";
213
- import { dirname, join as join4 } from "path";
214
- async function writeFileAtomic(target, data, options = {}) {
215
- const directory = dirname(target);
216
- await mkdir(directory, { recursive: true });
217
- const temporary = join4(directory, `.${randomBytes(8).toString("hex")}.tmp`);
208
+ async function ensureAcpProfile(options) {
209
+ const profile = options.profile ?? DEFAULT_ACP_PROFILE;
210
+ const root = acpProfileRoot(options.home, profile, options.env);
211
+ const provider = options.provider ?? "deepseek-official";
212
+ const model = options.model ?? "deepseek-v4-flash";
213
+ const ready = isAcpProfileReady(root);
218
214
  try {
219
- await writeFile(temporary, data, options.mode === void 0 ? void 0 : { mode: options.mode });
220
- await rename(temporary, target);
215
+ if (!ready) {
216
+ await mkdir2(root, { recursive: true });
217
+ await writeFile2(join3(root, "package.json"), packageJsonFor2(profile), "utf8");
218
+ await writeFile2(join3(root, "cordis.yml"), "[]\n", "utf8");
219
+ await writeFile2(join3(root, "cordis.patch.yml"), acpPatchYaml(provider, model), "utf8");
220
+ const install = options.install ?? runPnpmInstall2;
221
+ await install(root);
222
+ if (!isAcpProfileReady(root)) {
223
+ return {
224
+ ok: false,
225
+ created: true,
226
+ error: `${ACP_PACKAGE}@${ACP_VERSION} was not found after install`
227
+ };
228
+ }
229
+ }
230
+ await writeFile2(join3(root, "cordis.patch.yml"), acpPatchYaml(provider, model), "utf8");
231
+ return { ok: true, created: !ready };
221
232
  } catch (error) {
222
- await rmSilently(temporary);
223
- throw error;
233
+ return {
234
+ ok: false,
235
+ created: false,
236
+ error: error instanceof Error ? error.message : String(error)
237
+ };
224
238
  }
225
239
  }
226
- async function rmSilently(path) {
227
- const { rm } = await import("fs/promises");
228
- try {
229
- await rm(path, { force: true });
230
- } catch {
240
+ function resolveAcpLaunch(options) {
241
+ const profile = options.profile ?? DEFAULT_ACP_PROFILE;
242
+ if (options.command || options.args) {
243
+ return {
244
+ command: options.command ?? "node",
245
+ args: options.args ?? ["--profile", profile],
246
+ profile
247
+ };
248
+ }
249
+ const bin = options.bin ?? discoverDshBin(options.home, options.env);
250
+ if (bin) {
251
+ return { command: "node", args: [bin, "--profile", profile], profile };
231
252
  }
253
+ return { command: "dsh", args: ["--profile", profile], profile };
232
254
  }
233
-
234
- // src/config/profile-store.ts
235
- var ConfigStore = class {
236
- constructor(path) {
237
- this.path = path;
255
+ var ACP_PACKAGE, ACP_VERSION, ACP_BASE_BUNDLE, DEFAULT_ACP_PROFILE;
256
+ var init_acp_runtime = __esm({
257
+ "src/adapters/dsh/acp-runtime.ts"() {
258
+ "use strict";
259
+ init_dsh_runtime();
260
+ ACP_PACKAGE = "@deepseek-ai/dsh-acp";
261
+ ACP_VERSION = "0.1.0-rc.6";
262
+ ACP_BASE_BUNDLE = "@deepseek-ai/dsh-base";
263
+ DEFAULT_ACP_PROFILE = "dsh-lark-acp";
238
264
  }
239
- path;
240
- data;
241
- async load() {
242
- try {
243
- const raw = await readFile(this.path, "utf8");
244
- const parsed = JSON.parse(raw);
245
- this.data = {
246
- schemaVersion: 1,
247
- activeProfile: parsed.activeProfile ?? "default",
248
- profiles: Object.fromEntries(
249
- Object.entries(parsed.profiles ?? {}).map(([name, profile]) => [
250
- name,
251
- {
252
- ...profile,
253
- access: {
254
- allowedUsers: profile.access?.allowedUsers ?? [],
255
- allowedChats: profile.access?.allowedChats ?? [],
256
- admins: profile.access?.admins ?? []
257
- }
258
- }
259
- ])
260
- )
261
- };
262
- } catch (error) {
263
- if (error.code !== "ENOENT") throw error;
264
- this.data = {
265
- schemaVersion: 1,
266
- activeProfile: "default",
267
- profiles: {}
268
- };
265
+ });
266
+
267
+ // src/adapters/dsh/acp-adapter.ts
268
+ var acp_adapter_exports = {};
269
+ __export(acp_adapter_exports, {
270
+ AcpDshAdapter: () => AcpDshAdapter,
271
+ buildAcpAgentAdapter: () => buildAcpAgentAdapter,
272
+ translateAcpUpdate: () => translateAcpUpdate
273
+ });
274
+ import { randomUUID as randomUUID2 } from "crypto";
275
+ import {
276
+ spawn as defaultSpawn
277
+ } from "child_process";
278
+ import { homedir } from "os";
279
+ import { Readable as NodeReadable, Writable as NodeWritable } from "stream";
280
+ import {
281
+ ClientSideConnection,
282
+ ndJsonStream,
283
+ PROTOCOL_VERSION
284
+ } from "@agentclientprotocol/sdk";
285
+ function isRecord3(value) {
286
+ return typeof value === "object" && value !== null && !Array.isArray(value);
287
+ }
288
+ function textOfContent(content) {
289
+ if (!isRecord3(content) || typeof content.text !== "string") return "";
290
+ return content.text;
291
+ }
292
+ function translateAcpUpdate(notification) {
293
+ const update = notification.update;
294
+ if (!isRecord3(update)) return [];
295
+ switch (update.sessionUpdate) {
296
+ case "agent_message_chunk":
297
+ return textOfContent(update.content) ? [{ type: "text", delta: textOfContent(update.content) }] : [];
298
+ case "agent_thought_chunk":
299
+ return textOfContent(update.content) ? [{ type: "thinking", delta: textOfContent(update.content) }] : [];
300
+ case "tool_call": {
301
+ if (typeof update.toolCallId !== "string") return [];
302
+ return [
303
+ {
304
+ type: "tool_use",
305
+ id: update.toolCallId,
306
+ name: typeof update.title === "string" ? update.title : "tool",
307
+ input: update.rawInput ?? {}
308
+ }
309
+ ];
269
310
  }
270
- return this.data;
311
+ default:
312
+ return [];
271
313
  }
272
- getActiveProfile() {
273
- const data = this.getData();
274
- return data.profiles[data.activeProfile];
314
+ }
315
+ function mapApprovalOutcome(outcome, options) {
316
+ if (outcome === "cancelled") {
317
+ return { outcome: { outcome: "cancelled" } };
275
318
  }
276
- getProfile(name) {
277
- const data = this.getData();
278
- return data.profiles[name];
319
+ const wanted = outcome === "allowed-once" ? "allow_once" : "reject_once";
320
+ const option = options.find((item) => item.kind === wanted);
321
+ if (!option) return { outcome: { outcome: "cancelled" } };
322
+ return { outcome: { outcome: "selected", optionId: option.optionId } };
323
+ }
324
+ function buildPromptText2(prompt, images) {
325
+ return images?.length ? `${prompt}
326
+
327
+ Image files attached to this message:
328
+ ${images.join("\n")}` : prompt;
329
+ }
330
+ function waitForChildExit2(child, timeoutMs) {
331
+ if (child.exitCode !== null || child.signalCode !== null) {
332
+ return Promise.resolve({ code: child.exitCode, timedOut: false });
279
333
  }
280
- async saveProfile(name, input) {
281
- const data = this.getData();
282
- const existing = data.profiles[name];
283
- const profile = {
284
- schemaVersion: 1,
285
- agentKind: "dsh",
286
- tenant: input.tenant,
287
- accounts: {
288
- appId: input.appId,
289
- appSecret: input.appSecret
290
- },
291
- workspaces: {
292
- default: input.workspace ?? existing?.workspaces.default ?? void 0
293
- },
294
- preferences: {
295
- model: input.model ?? existing?.preferences.model ?? void 0,
296
- stopGraceMs: input.stopGraceMs ?? existing?.preferences.stopGraceMs ?? void 0,
297
- runTimeoutMs: input.runTimeoutMs ?? existing?.preferences.runTimeoutMs ?? void 0
334
+ return new Promise((resolve5) => {
335
+ const timer = timeoutMs > 0 ? setTimeout(() => {
336
+ child.removeListener("exit", onExit);
337
+ resolve5({ code: child.exitCode, timedOut: true });
338
+ }, timeoutMs) : void 0;
339
+ const onExit = (code) => {
340
+ if (timer !== void 0) clearTimeout(timer);
341
+ resolve5({ code, timedOut: false });
342
+ };
343
+ child.once("exit", onExit);
344
+ });
345
+ }
346
+ async function disposeChild(child, graceMs) {
347
+ if (child.exitCode !== null || child.signalCode !== null) return;
348
+ child.stdin?.end();
349
+ if (!(await waitForChildExit2(child, graceMs)).timedOut) return;
350
+ child.kill("SIGTERM");
351
+ if (!(await waitForChildExit2(child, graceMs)).timedOut) return;
352
+ child.kill("SIGKILL");
353
+ }
354
+ function withTimeout(promise, timeoutMs) {
355
+ return new Promise((resolve5, reject) => {
356
+ const timer = setTimeout(() => {
357
+ reject(new Error(`ACP probe timed out after ${timeoutMs}ms`));
358
+ }, timeoutMs);
359
+ promise.then(
360
+ (value) => {
361
+ clearTimeout(timer);
362
+ resolve5(value);
298
363
  },
299
- access: {
300
- allowedUsers: input.access?.allowedUsers ?? existing?.access?.allowedUsers ?? [],
301
- allowedChats: input.access?.allowedChats ?? existing?.access?.allowedChats ?? [],
302
- admins: input.access?.admins ?? existing?.access?.admins ?? []
364
+ (error) => {
365
+ clearTimeout(timer);
366
+ reject(error);
303
367
  }
304
- };
305
- if (input.operatorOpenId && !profile.access.allowedUsers.includes(input.operatorOpenId)) {
306
- profile.access.allowedUsers.push(input.operatorOpenId);
307
- profile.access.admins.push(input.operatorOpenId);
308
- }
309
- data.profiles[name] = profile;
310
- data.activeProfile = name;
311
- await this.persist();
312
- }
313
- getData() {
314
- if (!this.data) {
315
- throw new Error("ConfigStore must be loaded before use");
316
- }
317
- return this.data;
318
- }
319
- async persist() {
320
- const data = this.getData();
321
- await mkdir2(dirname2(this.path), { recursive: true });
322
- await writeFileAtomic(this.path, `${JSON.stringify(data, null, 2)}
323
- `, {
324
- mode: 384
325
- });
326
- }
327
- };
328
-
329
- // src/cli/commands/doctor.ts
330
- async function runDoctor(options) {
331
- const env = loadRuntimeEnv({
332
- ...process.env,
333
- ...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
334
- ...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
335
- ...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
336
- ...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
368
+ );
337
369
  });
338
- const paths = resolveAppPaths(env.home);
339
- const profileName = options.profile ?? "default";
340
- const store = new ConfigStore(paths.configFile);
341
- await store.load();
342
- const profile = store.getProfile(profileName);
343
- const lines = [
344
- "dsh-lark-bot doctor",
345
- `version: ${options.version ?? "unknown"}`,
346
- `node: ${process.version}`,
347
- `profile: ${profileName}`,
348
- `home: ${paths.root}`,
349
- `dsh_command: ${env.dshCommand}`,
350
- `dsh_args: ${env.dshArgs.join(",")}`
351
- ];
352
- let critical = false;
353
- if (!profile) {
354
- lines.push("config: missing");
355
- critical = true;
356
- } else {
357
- lines.push(
358
- [
359
- "config: ok",
360
- `tenant=${profile.tenant}`,
361
- `app_id=${profile.accounts.appId}`,
362
- `app_secret=${profile.accounts.appSecret ? "present" : "missing"}`,
363
- `allowed_users=${profile.access.allowedUsers.length}`,
364
- `allowed_chats=${profile.access.allowedChats.length}`
365
- ].join(" ")
370
+ }
371
+ async function buildAcpAgentAdapter(env, preferences) {
372
+ const runtimeOptions = {
373
+ home: homedir(),
374
+ env: process.env,
375
+ provider: env.provider,
376
+ model: preferences.model ?? env.model,
377
+ ...env.dshExplicit ? { command: env.dshCommand, args: env.dshArgs } : {}
378
+ };
379
+ const ensure = await ensureAcpProfile(runtimeOptions);
380
+ if (!ensure.ok) {
381
+ throw new Error(
382
+ `ACP runtime profile setup failed: ${ensure.error ?? "unknown error"} (install pnpm, or set DSH_LARK_ADAPTER=headless for the legacy adapter)`
366
383
  );
367
- if (!profile.accounts.appId || !profile.accounts.appSecret) critical = true;
368
- }
369
- const workspace = options.workspace ?? profile?.workspaces.default ?? env.workspace ?? paths.profilePath(profileName, "workspace");
370
- try {
371
- const info = await stat(workspace);
372
- lines.push(`workspace: ${workspace} (${info.isDirectory() ? "directory" : "not-directory"})`);
373
- } catch {
374
- lines.push(`workspace: ${workspace} (missing)`);
375
384
  }
376
- const availability = await checkDshAvailability({
377
- command: env.dshCommand,
378
- args: env.dshArgs
385
+ const launch = resolveAcpLaunch(runtimeOptions);
386
+ return new AcpDshAdapter({
387
+ launch,
388
+ provider: env.provider,
389
+ model: preferences.model ?? env.model
379
390
  });
380
- if (availability.ok) {
381
- lines.push(`dsh: ok${availability.version ? ` (${availability.version})` : ""}`);
382
- } else {
383
- lines.push(`dsh: unavailable (${availability.error ?? "unknown"})`);
384
- critical = true;
385
- }
386
- const output = options.output ?? ((text) => process.stdout.write(text));
387
- output(`${lines.join("\n")}
388
- `);
389
- if (critical) process.exitCode = 1;
390
391
  }
392
+ var AcpDshAdapter;
393
+ var init_acp_adapter = __esm({
394
+ "src/adapters/dsh/acp-adapter.ts"() {
395
+ "use strict";
396
+ init_acp_runtime();
397
+ init_event_channel();
398
+ AcpDshAdapter = class {
399
+ id = "dsh-acp";
400
+ displayName = "DeepSeek Harness (ACP)";
401
+ launch;
402
+ model;
403
+ probeTimeoutMs;
404
+ spawn;
405
+ stopGraceMs;
406
+ constructor(options) {
407
+ this.launch = options.launch;
408
+ this.model = options.model;
409
+ this.probeTimeoutMs = options.probeTimeoutMs ?? 15e3;
410
+ this.spawn = options.spawn;
411
+ this.stopGraceMs = 5e3;
412
+ }
413
+ async isAvailable() {
414
+ return (await this.checkAvailability()).ok;
415
+ }
416
+ async checkAvailability() {
417
+ const child = this.spawnChild(process.cwd());
418
+ if (!child.stdin || !child.stdout) {
419
+ await disposeChild(child, this.stopGraceMs);
420
+ return { ok: false, error: "ACP child did not expose protocol streams", version: void 0 };
421
+ }
422
+ const conn = new ClientSideConnection(
423
+ () => ({
424
+ requestPermission: () => Promise.resolve({ outcome: { outcome: "cancelled" } }),
425
+ sessionUpdate: () => Promise.resolve()
426
+ }),
427
+ ndJsonStream(
428
+ NodeWritable.toWeb(child.stdin),
429
+ NodeReadable.toWeb(child.stdout)
430
+ )
431
+ );
432
+ try {
433
+ const info = await withTimeout(
434
+ conn.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }),
435
+ this.probeTimeoutMs
436
+ );
437
+ const name = info.agentInfo?.name ?? "deepseek-harness-acp";
438
+ const version = info.agentInfo?.version ?? "unknown";
439
+ return { ok: true, error: void 0, version: `${name}@${version}` };
440
+ } catch (error) {
441
+ return {
442
+ ok: false,
443
+ error: error instanceof Error ? error.message : String(error),
444
+ version: void 0
445
+ };
446
+ } finally {
447
+ await disposeChild(child, this.stopGraceMs);
448
+ }
449
+ }
450
+ run(options) {
451
+ const cwd = options.cwd ?? process.cwd();
452
+ const model = options.model ?? this.model;
453
+ const stopRequested = { value: false };
454
+ const channel = new EventChannel();
455
+ const child = this.spawnChild(cwd);
456
+ let serverSessionId;
457
+ let conn;
458
+ let hadError = false;
459
+ const clientSessionId = `acp-${randomUUID2().replaceAll("-", "")}`;
460
+ const makeClient = (_agent) => ({
461
+ sessionUpdate: async (notification) => {
462
+ for (const event of translateAcpUpdate(notification)) {
463
+ if (event.type === "error") hadError = true;
464
+ channel.push(event);
465
+ }
466
+ },
467
+ requestPermission: async (request) => {
468
+ if (!options.onApprovalRequest) {
469
+ return { outcome: { outcome: "cancelled" } };
470
+ }
471
+ const approval = {
472
+ id: request.toolCall.toolCallId,
473
+ sessionId: request.sessionId,
474
+ toolName: typeof request.toolCall.title === "string" ? request.toolCall.title : "tool",
475
+ reason: void 0,
476
+ options: request.options.map((item) => ({
477
+ optionId: item.optionId,
478
+ name: item.name,
479
+ kind: item.kind
480
+ }))
481
+ };
482
+ const outcome = await options.onApprovalRequest(approval);
483
+ return mapApprovalOutcome(outcome, request.options);
484
+ }
485
+ });
486
+ const task = (async () => {
487
+ if (!child.stdin || !child.stdout) {
488
+ throw new Error("ACP child did not expose protocol streams");
489
+ }
490
+ conn = new ClientSideConnection(
491
+ makeClient,
492
+ ndJsonStream(
493
+ NodeWritable.toWeb(child.stdin),
494
+ NodeReadable.toWeb(child.stdout)
495
+ )
496
+ );
497
+ await conn.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
498
+ const created = await conn.newSession({ cwd, mcpServers: [] });
499
+ serverSessionId = created.sessionId;
500
+ const blocks = [
501
+ { type: "text", text: buildPromptText2(options.prompt, options.images) }
502
+ ];
503
+ const response = await conn.prompt({ sessionId: serverSessionId, prompt: blocks });
504
+ if (response.stopReason === "cancelled") {
505
+ if (!stopRequested.value) {
506
+ channel.push({ type: "error", message: "ACP turn cancelled", terminationReason: "interrupted" });
507
+ hadError = true;
508
+ }
509
+ return;
510
+ }
511
+ if (response.stopReason === "refusal" || response.stopReason === "max_turn_requests") {
512
+ channel.push({
513
+ type: "error",
514
+ message: `ACP turn stopped with reason: ${response.stopReason}`,
515
+ terminationReason: "failed"
516
+ });
517
+ hadError = true;
518
+ return;
519
+ }
520
+ if (response.usage) {
521
+ const usage = {
522
+ type: "usage",
523
+ ...typeof response.usage.inputTokens === "number" ? { inputTokens: response.usage.inputTokens } : {},
524
+ ...typeof response.usage.outputTokens === "number" ? { outputTokens: response.usage.outputTokens } : {}
525
+ };
526
+ channel.push(usage);
527
+ }
528
+ })().catch((error) => {
529
+ if (!stopRequested.value) {
530
+ channel.push({
531
+ type: "error",
532
+ message: error instanceof Error ? error.message : String(error),
533
+ terminationReason: "failed"
534
+ });
535
+ hadError = true;
536
+ }
537
+ }).finally(() => {
538
+ channel.close();
539
+ });
540
+ async function* events() {
541
+ yield {
542
+ type: "system",
543
+ sessionId: clientSessionId,
544
+ cwd,
545
+ model
546
+ };
547
+ for await (const event of channel) {
548
+ yield event;
549
+ }
550
+ await task;
551
+ if (!hadError) {
552
+ yield {
553
+ type: "done",
554
+ sessionId: clientSessionId,
555
+ terminationReason: stopRequested.value ? "interrupted" : "normal"
556
+ };
557
+ }
558
+ }
559
+ return {
560
+ runId: options.runId,
561
+ events: events(),
562
+ stop: async () => {
563
+ stopRequested.value = true;
564
+ if (conn && serverSessionId !== void 0) {
565
+ try {
566
+ await conn.cancel({ sessionId: serverSessionId });
567
+ } catch {
568
+ }
569
+ }
570
+ await disposeChild(child, this.stopGraceMs);
571
+ },
572
+ waitForExit: (timeoutMs) => waitForChildExit2(child, timeoutMs).then((result) => !result.timedOut && result.code !== null)
573
+ };
574
+ }
575
+ spawnChild(cwd) {
576
+ const spawnFn = this.spawn ?? defaultSpawn;
577
+ return spawnFn(this.launch.command, this.launch.args, {
578
+ cwd,
579
+ stdio: ["pipe", "pipe", "pipe"]
580
+ });
581
+ }
582
+ };
583
+ }
584
+ });
391
585
 
392
- // src/cli/commands/start.ts
393
- import { mkdir as mkdir5 } from "fs/promises";
586
+ // src/cli.ts
587
+ import { readFileSync } from "fs";
588
+ import { Command } from "commander";
589
+
590
+ // src/cli/commands/doctor.ts
591
+ import { stat } from "fs/promises";
592
+
593
+ // src/adapters/index.ts
594
+ import { homedir as homedir2 } from "os";
394
595
 
395
596
  // src/adapters/dsh/adapter.ts
396
597
  import { spawn as spawn2 } from "cross-spawn";
397
598
  import { createInterface } from "readline";
398
599
 
600
+ // src/adapters/dsh/availability.ts
601
+ import { spawn } from "cross-spawn";
602
+ import { existsSync } from "fs";
603
+ async function checkDshAvailability(options) {
604
+ const timeoutMs = options.timeoutMs ?? 5e3;
605
+ const versionArgs = options.command === "node" && options.args?.[0] && existsSync(options.args[0]) ? [options.args[0], "--version"] : ["--version"];
606
+ return new Promise((resolve5) => {
607
+ const child = spawn(options.command, versionArgs, {
608
+ stdio: ["ignore", "pipe", "pipe"]
609
+ });
610
+ let stdout = "";
611
+ let stderr = "";
612
+ let settled = false;
613
+ const finish = (result) => {
614
+ if (settled) return;
615
+ settled = true;
616
+ resolve5(result);
617
+ };
618
+ const timer = setTimeout(() => {
619
+ child.kill("SIGKILL");
620
+ finish({ ok: false, error: `timed out after ${timeoutMs}ms`, version: void 0 });
621
+ }, timeoutMs);
622
+ child.stdout?.on("data", (chunk) => {
623
+ stdout += chunk.toString("utf8");
624
+ });
625
+ child.stderr?.on("data", (chunk) => {
626
+ stderr += chunk.toString("utf8");
627
+ });
628
+ child.on("error", (error) => {
629
+ clearTimeout(timer);
630
+ finish({ ok: false, error: error.message, version: void 0 });
631
+ });
632
+ child.on("exit", (code) => {
633
+ clearTimeout(timer);
634
+ const version = stdout.trim();
635
+ if (code !== 0 && !version) {
636
+ finish({ ok: false, error: stderr.trim() || `exited with code ${String(code)}`, version: void 0 });
637
+ return;
638
+ }
639
+ finish({ ok: true, error: void 0, version: version || void 0 });
640
+ });
641
+ });
642
+ }
643
+
399
644
  // src/adapters/dsh/translate.ts
400
645
  function isRecord(value) {
401
646
  return typeof value === "object" && value !== null;
@@ -598,14 +843,14 @@ async function waitForExitResult(child, timeoutMs) {
598
843
  if (child.exitCode !== null || child.signalCode !== null) {
599
844
  return { code: child.exitCode, timedOut: false };
600
845
  }
601
- return new Promise((resolve4) => {
846
+ return new Promise((resolve5) => {
602
847
  const onExit = (code) => {
603
848
  if (timer !== void 0) clearTimeout(timer);
604
- resolve4({ code, timedOut: false });
849
+ resolve5({ code, timedOut: false });
605
850
  };
606
851
  const timer = timeoutMs > 0 ? setTimeout(() => {
607
852
  child.removeListener("exit", onExit);
608
- resolve4({ code: child.exitCode, timedOut: true });
853
+ resolve5({ code: child.exitCode, timedOut: true });
609
854
  }, timeoutMs) : void 0;
610
855
  child.once("exit", onExit);
611
856
  });
@@ -622,863 +867,2207 @@ async function waitForChildExit(child, timeoutMs) {
622
867
  return !result.timedOut && result.code !== null;
623
868
  }
624
869
 
625
- // src/bot/active-runs.ts
626
- var ActiveRuns = class {
627
- runs = /* @__PURE__ */ new Map();
628
- set(scope, handle) {
629
- this.runs.set(scope, handle);
630
- }
631
- get(scope) {
632
- return this.runs.get(scope);
870
+ // src/adapters/dsh/sdk-adapter.ts
871
+ import { randomUUID } from "crypto";
872
+ import {
873
+ DeepSeekHarness,
874
+ HarnessClient
875
+ } from "@deepseek-ai/dsh-sdk-client";
876
+
877
+ // src/adapters/dsh/sdk-translate.ts
878
+ init_event_channel();
879
+ function isRecord2(value) {
880
+ return typeof value === "object" && value !== null && !Array.isArray(value);
881
+ }
882
+ function stringValue2(value) {
883
+ return typeof value === "string" ? value : void 0;
884
+ }
885
+ function safeParseJson(value) {
886
+ if (typeof value !== "string") return value;
887
+ try {
888
+ return JSON.parse(value);
889
+ } catch {
890
+ return value;
891
+ }
892
+ }
893
+ function textOfBlocks(blocks) {
894
+ if (!Array.isArray(blocks)) return "";
895
+ return blocks.filter(isRecord2).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
896
+ }
897
+ function translateChunk(chunk, tracker) {
898
+ if (!isRecord2(chunk)) return [];
899
+ switch (chunk.type) {
900
+ case "reasoning-delta":
901
+ return typeof chunk.text === "string" && chunk.text ? [{ type: "thinking", delta: chunk.text }] : [];
902
+ case "text-delta":
903
+ return typeof chunk.text === "string" && chunk.text ? [{ type: "text", delta: chunk.text }] : [];
904
+ case "tool-call-delta": {
905
+ const id = stringValue2(chunk.id);
906
+ if (!id || tracker.emitted.has(id)) return [];
907
+ tracker.emitted.add(id);
908
+ return [
909
+ {
910
+ type: "tool_use",
911
+ id,
912
+ name: stringValue2(chunk.name) ?? "tool",
913
+ input: {}
914
+ }
915
+ ];
916
+ }
917
+ default:
918
+ return [];
633
919
  }
634
- delete(scope) {
635
- return this.runs.delete(scope);
920
+ }
921
+ function translateToolResult(data) {
922
+ if (!isRecord2(data)) return [];
923
+ const message = isRecord2(data.message) ? data.message : void 0;
924
+ const block = Array.isArray(message?.content) ? message.content[0] : void 0;
925
+ const toolCallId = stringValue2(
926
+ isRecord2(block) ? block.toolCallId : void 0
927
+ );
928
+ if (!toolCallId) return [];
929
+ const output = textOfBlocks(isRecord2(block) ? block.content : void 0);
930
+ const isError = data.error !== void 0 || (isRecord2(block) ? block.isError === true : false);
931
+ return [
932
+ {
933
+ type: "tool_result",
934
+ id: toolCallId,
935
+ output,
936
+ isError
937
+ }
938
+ ];
939
+ }
940
+ function translateAssistantMessage(data) {
941
+ if (!isRecord2(data) || !isRecord2(data.usage)) return [];
942
+ const usage = data.usage;
943
+ const inputTokens = typeof usage.inputTokens === "number" ? usage.inputTokens : void 0;
944
+ const outputTokens = typeof usage.outputTokens === "number" ? usage.outputTokens : void 0;
945
+ if (inputTokens === void 0 && outputTokens === void 0) return [];
946
+ return [
947
+ {
948
+ type: "usage",
949
+ ...inputTokens === void 0 ? {} : { inputTokens },
950
+ ...outputTokens === void 0 ? {} : { outputTokens }
951
+ }
952
+ ];
953
+ }
954
+ function translateTurnEnd(data) {
955
+ if (!isRecord2(data) || !isRecord2(data.reason) || data.reason.kind !== "error") return [];
956
+ const failure = isRecord2(data.reason.error) ? data.reason.error : void 0;
957
+ const message = stringValue2(failure?.message) ?? "dsh turn failed";
958
+ return [{ type: "error", message, terminationReason: "failed" }];
959
+ }
960
+ function translateSessionEvent(event, tracker) {
961
+ if (!isRecord2(event) || typeof event.type !== "string") return [];
962
+ switch (event.type) {
963
+ case "assistant/chunk":
964
+ return translateChunk(isRecord2(event.data) ? event.data.chunk : void 0, tracker);
965
+ case "tool/call": {
966
+ const data = isRecord2(event.data) ? event.data : void 0;
967
+ const id = stringValue2(data?.callId);
968
+ if (!id) return [];
969
+ return [
970
+ {
971
+ type: "tool_use",
972
+ id,
973
+ name: stringValue2(data?.name) ?? "tool",
974
+ input: safeParseJson(data?.arguments)
975
+ }
976
+ ];
977
+ }
978
+ case "tool/result":
979
+ return translateToolResult(event.data);
980
+ case "assistant/message":
981
+ return translateAssistantMessage(event.data);
982
+ case "turn/end":
983
+ return translateTurnEnd(event.data);
984
+ default:
985
+ return [];
636
986
  }
637
- async interrupt(scope) {
638
- const handle = this.runs.get(scope);
639
- if (!handle) return false;
640
- this.runs.delete(scope);
641
- await handle.stop();
642
- return true;
987
+ }
988
+ function translateNotification(notification, sessionId, tracker) {
989
+ if (notification.method !== "session.event") return [];
990
+ const params = isRecord2(notification.params) ? notification.params : void 0;
991
+ if (params?.sessionId !== sessionId) return [];
992
+ return translateSessionEvent(params.event, tracker);
993
+ }
994
+ function buildPromptText(prompt, images) {
995
+ return images?.length ? `${prompt}
996
+
997
+ Image files attached to this message:
998
+ ${images.join("\n")}` : prompt;
999
+ }
1000
+ function createSdkRun(harness, prompt, options) {
1001
+ const channel = new EventChannel();
1002
+ const tracker = { emitted: /* @__PURE__ */ new Set() };
1003
+ let hadError = false;
1004
+ const task = harness.run(buildPromptText(prompt, options.images), {
1005
+ sessionId: options.sessionId,
1006
+ onNotification: (notification) => {
1007
+ for (const event of translateNotification(notification, options.sessionId, tracker)) {
1008
+ if (event.type === "error") hadError = true;
1009
+ channel.push(event);
1010
+ }
1011
+ }
1012
+ }).catch((error) => {
1013
+ if (!options.stopRequested.value) {
1014
+ channel.push({
1015
+ type: "error",
1016
+ message: error instanceof Error ? error.message : String(error),
1017
+ terminationReason: "failed"
1018
+ });
1019
+ hadError = true;
1020
+ }
1021
+ }).finally(() => {
1022
+ channel.close();
1023
+ });
1024
+ async function* events() {
1025
+ yield {
1026
+ type: "system",
1027
+ sessionId: options.sessionId,
1028
+ cwd: options.cwd,
1029
+ model: options.model
1030
+ };
1031
+ for await (const event of channel) {
1032
+ yield event;
1033
+ }
1034
+ await task;
1035
+ if (!hadError) {
1036
+ yield {
1037
+ type: "done",
1038
+ sessionId: options.sessionId,
1039
+ terminationReason: options.stopRequested.value ? "interrupted" : "normal"
1040
+ };
1041
+ }
643
1042
  }
644
- };
1043
+ return { events: events(), settled: task.then(() => void 0) };
1044
+ }
645
1045
 
646
- // src/bot/pending-queue.ts
647
- var PendingQueue = class {
648
- constructor(quietMs, onFlush) {
649
- this.quietMs = quietMs;
650
- this.onFlush = onFlush;
1046
+ // src/adapters/dsh/sdk-adapter.ts
1047
+ function waitWithTimeout(promise, timeoutMs) {
1048
+ return new Promise((resolve5) => {
1049
+ const timer = setTimeout(() => resolve5(false), timeoutMs > 0 ? timeoutMs : 5e3);
1050
+ promise.then(
1051
+ () => {
1052
+ clearTimeout(timer);
1053
+ resolve5(true);
1054
+ },
1055
+ () => {
1056
+ clearTimeout(timer);
1057
+ resolve5(true);
1058
+ }
1059
+ );
1060
+ });
1061
+ }
1062
+ var SdkDshAdapter = class {
1063
+ id = "dsh-sdk";
1064
+ displayName = "DeepSeek Harness (SDK)";
1065
+ launch;
1066
+ provider;
1067
+ model;
1068
+ maxTokens;
1069
+ probeTimeoutMs;
1070
+ harnessFactory;
1071
+ runtimes = /* @__PURE__ */ new Map();
1072
+ disposed = false;
1073
+ constructor(options) {
1074
+ this.launch = options.launch;
1075
+ this.provider = options.provider;
1076
+ this.model = options.model;
1077
+ this.maxTokens = options.maxTokens;
1078
+ this.probeTimeoutMs = options.probeTimeoutMs ?? 15e3;
1079
+ this.harnessFactory = options.harnessFactory ?? ((cwd) => this.createHarness(cwd));
651
1080
  }
652
- quietMs;
653
- onFlush;
654
- pending = /* @__PURE__ */ new Map();
655
- timers = /* @__PURE__ */ new Map();
656
- blocked = /* @__PURE__ */ new Set();
657
- flushing = /* @__PURE__ */ new Set();
658
- push(scope, item) {
659
- const batch = this.pending.get(scope) ?? [];
660
- batch.push(item);
661
- this.pending.set(scope, batch);
662
- this.schedule(scope);
663
- }
664
- block(scope) {
665
- this.blocked.add(scope);
666
- this.clearTimer(scope);
667
- }
668
- unblock(scope) {
669
- this.blocked.delete(scope);
670
- this.schedule(scope);
1081
+ async isAvailable() {
1082
+ return (await this.checkAvailability()).ok;
671
1083
  }
672
- async flushNow(scope) {
673
- this.clearTimer(scope);
674
- if (this.flushing.has(scope)) return;
675
- const batch = this.pending.get(scope) ?? [];
676
- this.pending.delete(scope);
677
- if (batch.length === 0) return;
678
- this.flushing.add(scope);
1084
+ async checkAvailability() {
1085
+ if (this.disposed) {
1086
+ return { ok: false, error: "adapter disposed", version: void 0 };
1087
+ }
1088
+ const client = new HarnessClient({
1089
+ command: this.launch.command,
1090
+ args: this.launch.args,
1091
+ cwd: process.cwd(),
1092
+ requestTimeoutMs: this.probeTimeoutMs
1093
+ });
679
1094
  try {
680
- await this.onFlush(scope, batch);
1095
+ client.start();
1096
+ const info = await client.initialize({
1097
+ cwd: process.cwd(),
1098
+ provider: this.provider,
1099
+ model: this.model,
1100
+ ...this.maxTokens === void 0 ? {} : { maxTokens: this.maxTokens }
1101
+ });
1102
+ return {
1103
+ ok: true,
1104
+ error: void 0,
1105
+ version: `${info.serverInfo.name}@${info.serverInfo.version}`
1106
+ };
1107
+ } catch (error) {
1108
+ return {
1109
+ ok: false,
1110
+ error: error instanceof Error ? error.message : String(error),
1111
+ version: void 0
1112
+ };
681
1113
  } finally {
682
- this.flushing.delete(scope);
1114
+ await client.close();
683
1115
  }
684
1116
  }
685
- hasPending(scope) {
686
- return (this.pending.get(scope)?.length ?? 0) > 0;
687
- }
688
- isBlocked(scope) {
689
- return this.blocked.has(scope);
1117
+ run(options) {
1118
+ const cwd = options.cwd ?? process.cwd();
1119
+ const entry = this.runtimeFor(cwd);
1120
+ const sessionId = options.sessionId ?? `session-${randomUUID().replaceAll("-", "")}`;
1121
+ const stopRequested = { value: false };
1122
+ const handle = createSdkRun(entry.harness, options.prompt, {
1123
+ sessionId,
1124
+ cwd,
1125
+ model: options.model ?? this.model,
1126
+ images: options.images,
1127
+ stopRequested
1128
+ });
1129
+ return {
1130
+ runId: options.runId,
1131
+ events: handle.events,
1132
+ stop: async () => {
1133
+ stopRequested.value = true;
1134
+ await this.closeRuntime(cwd);
1135
+ },
1136
+ waitForExit: (timeoutMs) => waitWithTimeout(handle.settled, timeoutMs)
1137
+ };
690
1138
  }
691
- schedule(scope) {
692
- if (this.blocked.has(scope) || this.flushing.has(scope)) return;
693
- if (this.timers.has(scope)) return;
694
- if (!this.hasPending(scope)) return;
695
- this.timers.set(
696
- scope,
697
- setTimeout(() => {
698
- this.timers.delete(scope);
699
- void this.flushNow(scope);
700
- }, this.quietMs)
1139
+ /** Close every runtime subprocess (used on bridge shutdown). */
1140
+ async dispose() {
1141
+ this.disposed = true;
1142
+ const entries = [...this.runtimes.values()];
1143
+ this.runtimes.clear();
1144
+ await Promise.allSettled(
1145
+ entries.map((entry) => entry.harness.close())
701
1146
  );
702
1147
  }
703
- clearTimer(scope) {
704
- const timer = this.timers.get(scope);
705
- if (timer === void 0) return;
706
- clearTimeout(timer);
707
- this.timers.delete(scope);
708
- }
709
- };
710
-
711
- // src/bot/run-policy.ts
712
- var RunPolicyStore = class {
713
- timeouts = /* @__PURE__ */ new Map();
714
- get(scope) {
715
- return this.timeouts.get(scope);
716
- }
717
- set(scope, runTimeoutMs) {
718
- this.timeouts.set(scope, runTimeoutMs);
1148
+ createHarness(cwd) {
1149
+ return new DeepSeekHarness({
1150
+ launch: {
1151
+ command: this.launch.command,
1152
+ args: this.launch.args,
1153
+ cwd
1154
+ },
1155
+ cwd,
1156
+ provider: this.provider,
1157
+ model: this.model,
1158
+ ...this.maxTokens === void 0 ? {} : { maxTokens: this.maxTokens }
1159
+ });
719
1160
  }
720
- clear(scope) {
721
- return this.timeouts.delete(scope);
1161
+ runtimeFor(cwd) {
1162
+ if (this.disposed) {
1163
+ throw new Error("SdkDshAdapter is disposed");
1164
+ }
1165
+ const existing = this.runtimes.get(cwd);
1166
+ if (existing) return existing;
1167
+ const entry = { harness: this.harnessFactory(cwd) };
1168
+ this.runtimes.set(cwd, entry);
1169
+ return entry;
1170
+ }
1171
+ async closeRuntime(cwd) {
1172
+ const entry = this.runtimes.get(cwd);
1173
+ if (!entry) return;
1174
+ this.runtimes.delete(cwd);
1175
+ try {
1176
+ await entry.harness.close();
1177
+ } catch (error) {
1178
+ void error;
1179
+ }
722
1180
  }
723
1181
  };
724
1182
 
725
- // src/bridge/channel.ts
726
- import { createLarkChannel } from "@larksuite/channel";
727
-
728
- // src/commands/index.ts
729
- import { resolve as resolve3 } from "path";
730
-
731
- // src/card/workspace-card.ts
732
- function renderWorkspaceCard(input) {
733
- const entries = input.index;
734
- const markdown2 = [
735
- `**\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55**
736
- \`${input.current}\``,
1183
+ // src/adapters/dsh/sdk-runtime.ts
1184
+ init_dsh_runtime();
1185
+ import { spawn as spawn3 } from "child_process";
1186
+ import { existsSync as existsSync3 } from "fs";
1187
+ import { mkdir, writeFile } from "fs/promises";
1188
+ import { join as join2 } from "path";
1189
+ var SDK_SERVER_PACKAGE = "@deepseek-ai/dsh-sdk-jsonrpc-server";
1190
+ var SDK_SERVER_VERSION = "0.1.0-rc.6";
1191
+ var SDK_BASE_BUNDLE = "@deepseek-ai/dsh-base";
1192
+ var DEFAULT_SDK_PROFILE = "dsh-lark";
1193
+ function sdkProfileRoot(home, profile, env) {
1194
+ return join2(resolveDshHome(home, env), "profiles", profile);
1195
+ }
1196
+ function packageJsonFor(profile) {
1197
+ return `${JSON.stringify(
1198
+ {
1199
+ name: `dsh-profile-${profile}`,
1200
+ private: true,
1201
+ dependencies: {
1202
+ [SDK_SERVER_PACKAGE]: SDK_SERVER_VERSION
1203
+ },
1204
+ dsh: {
1205
+ profile: {
1206
+ bundles: [SDK_BASE_BUNDLE]
1207
+ }
1208
+ }
1209
+ },
1210
+ null,
1211
+ 2
1212
+ )}
1213
+ `;
1214
+ }
1215
+ function patchYamlFor() {
1216
+ return [
1217
+ "# dsh-lark SDK JSON-RPC runtime overlay (managed by dsh-lark-bot).",
1218
+ "# stdout is reserved for SDK JSON-RPC frames; no console logger may load.",
1219
+ "- insert:",
1220
+ " - id: sdk-jsonrpc-server",
1221
+ ` name: '${SDK_SERVER_PACKAGE}'`,
1222
+ " config:",
1223
+ " maxTokensAsSuccess: true",
737
1224
  "",
738
- ...entries.length > 0 ? entries.map(
739
- ({ name, cwd, lastUsed }) => `- **${name}** \u2192 \`${cwd}\`${lastUsed ? ` \xB7 ${new Date(lastUsed).toLocaleString()}` : ""}`
740
- ) : ["\u6682\u65E0\u547D\u540D\u5DE5\u4F5C\u7A7A\u95F4\u3002"]
1225
+ "# Unattended IM runtime: interactive user questions cannot be answered",
1226
+ "# through Feishu, so the tool is disabled (default-deny).",
1227
+ "- id: user-questions",
1228
+ " disabled: true",
1229
+ "",
1230
+ "- id: system-prompt",
1231
+ " config:",
1232
+ " persona: >-",
1233
+ " You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.",
1234
+ "",
1235
+ "- id: hmr",
1236
+ " disabled: true",
1237
+ ""
741
1238
  ].join("\n");
742
- return {
743
- schema: "2.0",
744
- config: {
745
- summary: { content: "\u5DE5\u4F5C\u7A7A\u95F4\u5BFC\u822A" }
746
- },
747
- body: {
748
- elements: [
749
- {
750
- tag: "markdown",
751
- content: markdown2
752
- }
753
- ]
754
- }
755
- };
756
1239
  }
757
-
758
- // src/commands/index.ts
759
- var HELP = [
760
- "**dsh-lark-bot \u547D\u4EE4**",
761
- "",
762
- "- `/new` `/reset` \u2014 \u5F00\u59CB\u65B0\u4F1A\u8BDD",
763
- "- `/cd <path>` \u2014 \u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\u5E76\u91CD\u7F6E\u4F1A\u8BDD",
764
- "- `/ws list|save <name>|use <name>|remove <name>` \u2014 \u7BA1\u7406\u5DE5\u4F5C\u7A7A\u95F4",
765
- "- `/status` \u2014 \u67E5\u770B\u5F53\u524D\u72B6\u6001",
766
- "- `/resume` \u2014 \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u6700\u8FD1\u4E0A\u4E0B\u6587",
767
- "- `/stop` \u2014 \u7EC8\u6B62\u5F53\u524D\u4EFB\u52A1",
768
- "- `/timeout [N|off|default]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6",
769
- "- `/invite user|admin|group <id>` \u2014 \u7BA1\u7406\u8BBF\u95EE\u767D\u540D\u5355",
770
- "- `/help` \u2014 \u663E\u793A\u672C\u5E2E\u52A9"
771
- ].join("\n");
772
- async function reply(ctx, markdown2) {
773
- await ctx.channel.sendMarkdown(ctx.chatId, markdown2, {
774
- replyTo: ctx.messageId
775
- });
1240
+ function sdkServerInstalled(profileRoot) {
1241
+ const candidates = [
1242
+ join2(profileRoot, "node_modules", SDK_SERVER_PACKAGE),
1243
+ join2(profileRoot, "..", "node_modules", SDK_SERVER_PACKAGE)
1244
+ ];
1245
+ return candidates.some((path) => existsSync3(path));
776
1246
  }
777
- async function handleNew(_args, ctx) {
778
- const wasRunning = await ctx.activeRuns.interrupt(ctx.scope);
779
- ctx.sessions.clear(ctx.scope);
780
- 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");
1247
+ function isSdkProfileReady(profileRoot) {
1248
+ return existsSync3(join2(profileRoot, "package.json")) && existsSync3(join2(profileRoot, "cordis.yml")) && existsSync3(join2(profileRoot, "cordis.patch.yml")) && sdkServerInstalled(profileRoot);
781
1249
  }
782
- async function handleCd(args, ctx) {
783
- const path = args.trim();
784
- if (!path) {
785
- await reply(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`");
786
- return;
787
- }
788
- const cwd = resolve3(path);
789
- await ctx.activeRuns.interrupt(ctx.scope);
790
- ctx.workspaces.setCwd(ctx.scope, cwd);
791
- ctx.sessions.clear(ctx.scope);
792
- await reply(ctx, `\u5DF2\u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\uFF1A\`${cwd}\`\uFF0C\u4F1A\u8BDD\u5DF2\u91CD\u7F6E\u3002`);
1250
+ function runPnpmInstall(profileRoot) {
1251
+ return new Promise((resolve5, reject) => {
1252
+ const child = spawn3("pnpm", ["install"], {
1253
+ cwd: profileRoot,
1254
+ stdio: ["ignore", "pipe", "pipe"]
1255
+ });
1256
+ let output = "";
1257
+ child.stdout.on("data", (chunk) => {
1258
+ output += chunk.toString("utf8");
1259
+ });
1260
+ child.stderr.on("data", (chunk) => {
1261
+ output += chunk.toString("utf8");
1262
+ });
1263
+ child.on("error", (error) => {
1264
+ reject(error);
1265
+ });
1266
+ child.on("close", (code) => {
1267
+ if (code === 0) {
1268
+ resolve5();
1269
+ return;
1270
+ }
1271
+ const tail = output.trim().split("\n").slice(-8).join("\n");
1272
+ reject(new Error(`pnpm install exited with code ${String(code)}
1273
+ ${tail}`));
1274
+ });
1275
+ });
793
1276
  }
794
- async function handleWs(args, ctx) {
795
- const [sub, ...rest] = args.trim().split(/\s+/);
796
- const name = rest.join(" ").trim();
797
- if (!sub || sub === "list") {
798
- const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
799
- const named = ctx.workspaces.listNamed();
800
- const index = ctx.workspaces.listIndex();
801
- if (ctx.channel.sendCard) {
802
- await ctx.channel.sendCard(ctx.chatId, renderWorkspaceCard({ current, index }));
803
- return;
1277
+ async function ensureSdkProfile(options) {
1278
+ const profile = options.profile ?? DEFAULT_SDK_PROFILE;
1279
+ const root = sdkProfileRoot(options.home, profile, options.env);
1280
+ if (isSdkProfileReady(root)) return { ok: true, created: false };
1281
+ try {
1282
+ await mkdir(root, { recursive: true });
1283
+ await writeFile(join2(root, "package.json"), packageJsonFor(profile), "utf8");
1284
+ await writeFile(join2(root, "cordis.yml"), "[]\n", "utf8");
1285
+ await writeFile(join2(root, "cordis.patch.yml"), patchYamlFor(), "utf8");
1286
+ const install = options.install ?? runPnpmInstall;
1287
+ await install(root);
1288
+ if (!isSdkProfileReady(root)) {
1289
+ return {
1290
+ ok: false,
1291
+ created: true,
1292
+ error: `${SDK_SERVER_PACKAGE}@${SDK_SERVER_VERSION} was not found after install`
1293
+ };
804
1294
  }
805
- const lines = Object.entries(named).map(
806
- ([key, value]) => `- **${key}** \u2192 \`${value}\`${value === current ? " \u2190 \u5F53\u524D" : ""}`
807
- );
808
- await reply(
809
- ctx,
810
- [
811
- `\u5F53\u524D cwd\uFF1A\`${current}\``,
812
- "",
813
- ...lines.length > 0 ? lines : ["\u6682\u65E0\u547D\u540D\u5DE5\u4F5C\u7A7A\u95F4\u3002"]
814
- ].join("\n")
815
- );
816
- return;
1295
+ return { ok: true, created: true };
1296
+ } catch (error) {
1297
+ return {
1298
+ ok: false,
1299
+ created: false,
1300
+ error: error instanceof Error ? error.message : String(error)
1301
+ };
817
1302
  }
818
- if (sub === "save") {
819
- if (!name) {
820
- await reply(ctx, "\u7528\u6CD5\uFF1A`/ws save <name>`");
821
- return;
822
- }
823
- const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
824
- ctx.workspaces.saveNamed(name, current);
825
- await reply(ctx, `\u5DF2\u4FDD\u5B58\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${current}\``);
826
- return;
1303
+ }
1304
+ function resolveSdkLaunch(options) {
1305
+ const profile = options.profile ?? DEFAULT_SDK_PROFILE;
1306
+ if (options.command || options.args) {
1307
+ return {
1308
+ command: options.command ?? "node",
1309
+ args: options.args ?? ["--profile", profile],
1310
+ profile
1311
+ };
827
1312
  }
828
- if (sub === "use") {
829
- if (!name) {
830
- await reply(ctx, "\u7528\u6CD5\uFF1A`/ws use <name>`");
831
- return;
1313
+ const bin = options.bin ?? discoverDshBin(options.home, options.env);
1314
+ if (bin) {
1315
+ return { command: "node", args: [bin, "--profile", profile], profile };
1316
+ }
1317
+ return { command: "dsh", args: ["--profile", profile], profile };
1318
+ }
1319
+
1320
+ // src/adapters/index.ts
1321
+ async function buildAgentAdapter(env, preferences = { stopGraceMs: void 0, model: void 0 }) {
1322
+ switch (env.adapterMode) {
1323
+ case "headless": {
1324
+ const options = {
1325
+ command: env.dshCommand,
1326
+ args: env.dshArgs
1327
+ };
1328
+ if (preferences.stopGraceMs !== void 0) {
1329
+ options.stopGraceMs = preferences.stopGraceMs;
1330
+ }
1331
+ return new DshAdapter(options);
832
1332
  }
833
- const cwd = ctx.workspaces.getNamed(name);
834
- if (!cwd) {
835
- await reply(ctx, `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`);
836
- return;
1333
+ case "acp": {
1334
+ const { buildAcpAgentAdapter: buildAcpAgentAdapter2 } = await Promise.resolve().then(() => (init_acp_adapter(), acp_adapter_exports));
1335
+ return buildAcpAgentAdapter2(env, preferences);
837
1336
  }
838
- await ctx.activeRuns.interrupt(ctx.scope);
839
- ctx.workspaces.setCwd(ctx.scope, cwd);
840
- ctx.workspaces.touchNamed(name);
841
- ctx.sessions.clear(ctx.scope);
842
- await reply(ctx, `\u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${cwd}\``);
843
- return;
844
- }
845
- if (sub === "remove") {
846
- if (!name) {
847
- await reply(ctx, "\u7528\u6CD5\uFF1A`/ws remove <name>`");
848
- return;
1337
+ case "sdk":
1338
+ default: {
1339
+ const runtimeOptions = {
1340
+ home: homedir2(),
1341
+ env: process.env,
1342
+ ...env.dshExplicit ? { command: env.dshCommand, args: env.dshArgs } : {}
1343
+ };
1344
+ const ensure = await ensureSdkProfile(runtimeOptions);
1345
+ if (!ensure.ok) {
1346
+ throw new Error(
1347
+ `SDK runtime profile setup failed: ${ensure.error ?? "unknown error"} (install pnpm, or set DSH_LARK_ADAPTER=headless for the legacy adapter)`
1348
+ );
1349
+ }
1350
+ const launch = resolveSdkLaunch(runtimeOptions);
1351
+ return new SdkDshAdapter({
1352
+ launch,
1353
+ provider: env.provider,
1354
+ model: preferences.model ?? env.model,
1355
+ ...env.maxTokens === void 0 ? {} : { maxTokens: env.maxTokens }
1356
+ });
849
1357
  }
850
- const removed = ctx.workspaces.removeNamed(name);
851
- await reply(ctx, removed ? `\u5DF2\u5220\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**` : `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`);
852
- return;
853
1358
  }
854
- await reply(ctx, "\u672A\u77E5 `/ws` \u5B50\u547D\u4EE4\uFF0C\u8BF7\u4F7F\u7528 list / save / use / remove\u3002");
855
1359
  }
856
- async function handleStatus(_args, ctx) {
857
- const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
858
- const session = ctx.sessions.getRaw(ctx.scope)?.sessionId ?? "(\u65E0)";
859
- const running = Boolean(ctx.activeRuns.get(ctx.scope));
860
- const scopeLabel = ctx.chatMode === "topic" ? `${ctx.scope}\uFF08\u8BDD\u9898\u72EC\u7ACB session\uFF09` : ctx.scope;
861
- await reply(
862
- ctx,
863
- [
864
- `\u{1F9ED} **scope**: \`${scopeLabel}\``,
865
- `\u{1F4C1} **cwd**: \`${cwd}\``,
866
- `\u{1F517} **session**: \`${session}\``,
867
- `\u{1F3C3} **active run**: ${running ? "yes" : "no"}`
868
- ].join("\n")
869
- );
1360
+
1361
+ // src/config/app-paths.ts
1362
+ import { homedir as homedir3 } from "os";
1363
+ import { join as join4, resolve } from "path";
1364
+ function defaultHome() {
1365
+ const override = process.env.DSH_LARK_HOME?.trim();
1366
+ return override ? resolve(override) : join4(homedir3(), ".dsh-lark");
870
1367
  }
871
- async function handleResume(_args, ctx) {
872
- const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
873
- const history = ctx.sessions.historyFor(ctx.scope, cwd);
874
- if (history.length === 0) {
875
- await reply(ctx, "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u5386\u53F2\u4E0A\u4E0B\u6587\u3002");
876
- return;
877
- }
878
- const recent = history.slice(-6).map((message) => {
879
- const speaker = message.role === "user" ? "\u{1F464}" : "\u{1F916}";
880
- return `${speaker} ${message.content.slice(0, 300)}`;
881
- });
882
- await reply(ctx, [`\u5F53\u524D scope\uFF1A\`${ctx.scope}\``, "", ...recent].join("\n"));
1368
+ function resolveAppPaths(root = defaultHome()) {
1369
+ const profileDir = (profile) => join4(root, "profiles", profile);
1370
+ const profilePath = (profile, ...parts) => join4(profileDir(profile), ...parts);
1371
+ return {
1372
+ root,
1373
+ configFile: join4(root, "config.json"),
1374
+ activeProfileFile: join4(root, "active-profile"),
1375
+ profileDir,
1376
+ profilePath,
1377
+ sessionsFile: (profile) => profilePath(profile, "sessions.json"),
1378
+ sessionCatalogFile: (profile) => profilePath(profile, "sessions.json.catalog.json"),
1379
+ workspacesFile: (profile) => profilePath(profile, "workspaces.json"),
1380
+ mediaDir: (profile) => profilePath(profile, "media"),
1381
+ logsDir: (profile) => profilePath(profile, "logs"),
1382
+ registryFile: join4(root, "registry", "processes.json"),
1383
+ locksDir: join4(root, "registry", "locks")
1384
+ };
883
1385
  }
884
- async function handleStop(_args, ctx) {
885
- const stopped = await ctx.activeRuns.interrupt(ctx.scope);
886
- 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");
1386
+
1387
+ // src/config/env.ts
1388
+ init_dsh_runtime();
1389
+ import { join as join5, resolve as resolve2 } from "path";
1390
+ import { homedir as homedir4 } from "os";
1391
+ var DEFAULTS = {
1392
+ tenant: "feishu",
1393
+ provider: "deepseek-official",
1394
+ model: "deepseek-v4-flash",
1395
+ runTimeoutMs: 3e5,
1396
+ stopGraceMs: 5e3
1397
+ };
1398
+ function nonEmpty(value) {
1399
+ const trimmed = value?.trim();
1400
+ return trimmed ? trimmed : void 0;
887
1401
  }
888
- async function handleTimeout(args, ctx) {
889
- const input = args.trim();
890
- const effectiveMs = ctx.runPolicies.get(ctx.scope) ?? ctx.defaultRunTimeoutMs;
891
- if (!input) {
892
- const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
893
- await reply(
894
- ctx,
895
- 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"
896
- );
897
- return;
898
- }
899
- if (input === "off") {
900
- ctx.runPolicies.set(ctx.scope, 0);
901
- await reply(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\u3002");
902
- return;
903
- }
904
- if (input === "default") {
905
- ctx.runPolicies.clear(ctx.scope);
906
- await reply(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u8FD0\u884C\u8D85\u65F6\u3002");
907
- return;
908
- }
909
- const minutes = Number(input);
910
- if (!Number.isInteger(minutes) || minutes <= 0) {
911
- await reply(ctx, "\u7528\u6CD5\uFF1A`/timeout <N|off|default>`\uFF0CN \u4E3A\u5927\u4E8E 0 \u7684\u5206\u949F\u6570\u3002");
912
- return;
1402
+ function parseTenant(value) {
1403
+ const tenant = nonEmpty(value) ?? DEFAULTS.tenant;
1404
+ if (tenant !== "feishu" && tenant !== "lark") {
1405
+ throw new Error(`DSH_LARK_TENANT must be "feishu" or "lark", got "${tenant}"`);
913
1406
  }
914
- ctx.runPolicies.set(ctx.scope, minutes * 6e4);
915
- await reply(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A${minutes} \u5206\u949F\u3002`);
1407
+ return tenant;
916
1408
  }
917
- async function handleInvite(args, ctx) {
918
- const [kind, ...rest] = args.trim().split(/\s+/);
919
- const id = rest.join(" ").trim();
920
- if (kind === "list") {
921
- const snapshot = ctx.accessManager.snapshot();
922
- await reply(
923
- ctx,
924
- [
925
- "**\u8BBF\u95EE\u767D\u540D\u5355**",
926
- `users: ${snapshot.allowedUsers.join(", ") || "(\u7A7A)"}`,
927
- `chats: ${snapshot.allowedChats.join(", ") || "(\u7A7A)"}`,
928
- `admins: ${snapshot.admins.join(", ") || "(\u7A7A)"}`
929
- ].join("\n")
930
- );
931
- return;
932
- }
933
- if (!kind || !id) {
934
- await reply(
935
- ctx,
936
- "\u7528\u6CD5\uFF1A`/invite user|admin|group <id>`\u3001`/invite list`\u3001`/invite remove user|group <id>`"
937
- );
938
- return;
939
- }
940
- if (kind === "user") {
941
- await ctx.accessManager.addUser(id);
942
- await reply(ctx, `\u5DF2\u5141\u8BB8\u7528\u6237\uFF1A\`${id}\``);
943
- return;
944
- }
945
- if (kind === "admin") {
946
- await ctx.accessManager.addAdmin(id);
947
- await reply(ctx, `\u5DF2\u8BBE\u4E3A\u7BA1\u7406\u5458\uFF1A\`${id}\``);
948
- return;
949
- }
950
- if (kind === "group") {
951
- await ctx.accessManager.addChat(id);
952
- await reply(ctx, `\u5DF2\u5141\u8BB8\u7FA4\u804A\uFF1A\`${id}\``);
953
- return;
954
- }
955
- if (kind === "remove") {
956
- const [sub, target] = rest;
957
- if (sub === "user" && target) {
958
- await ctx.accessManager.removeUser(target);
959
- await reply(ctx, `\u5DF2\u79FB\u9664\u7528\u6237\uFF1A\`${target}\``);
960
- return;
961
- }
962
- if (sub === "group" && target) {
963
- await ctx.accessManager.removeChat(target);
964
- await reply(ctx, `\u5DF2\u79FB\u9664\u7FA4\u804A\uFF1A\`${target}\``);
965
- return;
966
- }
967
- await reply(ctx, "\u7528\u6CD5\uFF1A`/invite remove user <id>` \u6216 `/invite remove group <chatId>`");
968
- return;
969
- }
970
- await reply(ctx, "\u672A\u77E5 `/invite` \u7C7B\u578B\uFF0C\u8BF7\u4F7F\u7528 user / admin / group / list / remove\u3002");
1409
+ function parseDshArgs(value) {
1410
+ const raw = value?.trim();
1411
+ if (!raw) return [];
1412
+ return raw.split(",").map((item) => item.trim()).filter(Boolean);
971
1413
  }
972
- async function handleHelp(_args, ctx) {
973
- await reply(ctx, HELP);
1414
+ function parseTimeout(value) {
1415
+ const raw = value?.trim();
1416
+ if (!raw) return DEFAULTS.runTimeoutMs;
1417
+ const parsed = Number(raw);
1418
+ if (!Number.isInteger(parsed) || parsed < 0) {
1419
+ throw new Error(`DSH_LARK_RUN_TIMEOUT_MS must be a non-negative integer, got "${raw}"`);
1420
+ }
1421
+ return parsed;
974
1422
  }
975
- var handlers = {
976
- "/new": handleNew,
977
- "/reset": handleNew,
978
- "/cd": handleCd,
979
- "/ws": handleWs,
980
- "/status": handleStatus,
981
- "/resume": handleResume,
982
- "/stop": handleStop,
983
- "/timeout": handleTimeout,
984
- "/invite": handleInvite,
985
- "/help": handleHelp
986
- };
987
- async function tryHandleCommand(text, ctx) {
988
- const trimmed = text.trim();
989
- if (!trimmed.startsWith("/")) return false;
990
- const [command, ...rest] = trimmed.split(/\s+/);
991
- const handler = handlers[command ?? ""];
992
- if (!handler) return false;
993
- await handler(rest.join(" "), ctx);
994
- return true;
1423
+ function parseStopGrace(value) {
1424
+ const raw = value?.trim();
1425
+ if (!raw) return DEFAULTS.stopGraceMs;
1426
+ const parsed = Number(raw);
1427
+ if (!Number.isInteger(parsed) || parsed < 0) {
1428
+ throw new Error(`DSH_LARK_STOP_GRACE_MS must be a non-negative integer, got "${raw}"`);
1429
+ }
1430
+ return parsed;
995
1431
  }
996
-
997
- // src/core/logger.ts
998
- var REDACTED = "[redacted]";
999
- var SENSITIVE_KEY = /(secret|token|password|api[_-]?key)/i;
1000
- function redactValue(value, key, depth) {
1001
- if (SENSITIVE_KEY.test(key)) return REDACTED;
1002
- if (depth === 0) return value;
1003
- if (Array.isArray(value)) {
1004
- return value.map((item, index) => redactValue(item, `${key}.${index}`, depth - 1));
1432
+ function parseAdapterMode(value) {
1433
+ const mode = nonEmpty(value) ?? "sdk";
1434
+ if (mode !== "sdk" && mode !== "acp" && mode !== "headless") {
1435
+ throw new Error(`DSH_LARK_ADAPTER must be "sdk", "acp" or "headless", got "${mode}"`);
1005
1436
  }
1006
- if (value && typeof value === "object") {
1007
- const out = {};
1008
- for (const [childKey, childValue] of Object.entries(value)) {
1009
- out[childKey] = redactValue(childValue, childKey, depth - 1);
1010
- }
1011
- return out;
1437
+ return mode;
1438
+ }
1439
+ function parseMaxTokens(value) {
1440
+ const raw = value?.trim();
1441
+ if (!raw) return void 0;
1442
+ const parsed = Number(raw);
1443
+ if (!Number.isInteger(parsed) || parsed <= 0) {
1444
+ throw new Error(`DSH_LARK_MAX_TOKENS must be a positive integer, got "${raw}"`);
1012
1445
  }
1013
- return value;
1446
+ return parsed;
1014
1447
  }
1015
- function redactFields(fields, depth = 4) {
1016
- return redactValue(fields, "", depth);
1448
+ function parseBoolean(value, fallback) {
1449
+ const raw = value?.trim().toLowerCase();
1450
+ if (!raw) return fallback;
1451
+ if (raw === "1" || raw === "true" || raw === "yes") return true;
1452
+ if (raw === "0" || raw === "false" || raw === "no") return false;
1453
+ return fallback;
1017
1454
  }
1018
- function serializeMessage(level, category, event, fields) {
1019
- const entry = {
1020
- time: (/* @__PURE__ */ new Date()).toISOString(),
1021
- level,
1022
- category,
1023
- event,
1024
- fields: redactFields(fields)
1025
- };
1026
- return JSON.stringify(entry);
1455
+ function parseFreshness(value) {
1456
+ const raw = value?.trim();
1457
+ if (!raw) return 6e5;
1458
+ const parsed = Number(raw);
1459
+ if (!Number.isInteger(parsed) || parsed < 0) {
1460
+ throw new Error(`DSH_LARK_EVENT_FRESHNESS_MS must be a non-negative integer, got "${raw}"`);
1461
+ }
1462
+ return parsed;
1027
1463
  }
1028
- function createLogger(output = process.stderr) {
1029
- const write = (level, category, event, fields) => {
1030
- output.write(`${serializeMessage(level, category, event, fields)}
1031
- `);
1032
- };
1464
+ function loadRuntimeEnv(source = process.env) {
1465
+ const homeOverride = nonEmpty(source.DSH_LARK_HOME);
1466
+ const workspace = nonEmpty(source.DSH_LARK_WORKSPACE);
1467
+ const home = homeOverride ? resolve2(homeOverride) : join5(homedir4(), ".dsh-lark");
1468
+ const osHome = homedir4();
1469
+ const explicitCommand = nonEmpty(source.DSH_LARK_DSH_COMMAND);
1470
+ const rawDshArgs = nonEmpty(source.DSH_LARK_DSH_ARGS);
1471
+ const dshExplicit = explicitCommand !== void 0 || rawDshArgs !== void 0;
1472
+ const dshRuntime = resolveDshRuntime({
1473
+ home: osHome,
1474
+ env: source,
1475
+ ...explicitCommand ? { command: explicitCommand } : {},
1476
+ ...rawDshArgs ? { args: parseDshArgs(rawDshArgs) } : {}
1477
+ });
1033
1478
  return {
1034
- info: (category, event, fields = {}) => write("info", category, event, fields),
1035
- warn: (category, event, fields = {}) => write("warn", category, event, fields),
1036
- error: (category, event, fields = {}) => write("error", category, event, fields),
1037
- fail(category, error, fields = {}) {
1038
- const message = error instanceof Error ? error.message : String(error);
1039
- write("error", category, "fail", { message, ...fields });
1040
- }
1479
+ home,
1480
+ tenant: parseTenant(source.DSH_LARK_TENANT),
1481
+ appId: nonEmpty(source.DSH_LARK_APP_ID),
1482
+ appSecret: nonEmpty(source.DSH_LARK_APP_SECRET),
1483
+ workspace: workspace ? resolve2(workspace) : void 0,
1484
+ dshCommand: dshRuntime.command,
1485
+ dshArgs: dshRuntime.args,
1486
+ dshExplicit,
1487
+ adapterMode: parseAdapterMode(source.DSH_LARK_ADAPTER),
1488
+ provider: nonEmpty(source.DSH_LARK_PROVIDER) ?? DEFAULTS.provider,
1489
+ model: nonEmpty(source.DSH_LARK_MODEL) ?? DEFAULTS.model,
1490
+ maxTokens: parseMaxTokens(source.DSH_LARK_MAX_TOKENS),
1491
+ runTimeoutMs: parseTimeout(source.DSH_LARK_RUN_TIMEOUT_MS),
1492
+ stopGraceMs: parseStopGrace(source.DSH_LARK_STOP_GRACE_MS),
1493
+ accessDefaultDeny: parseBoolean(source.DSH_LARK_ACCESS_DEFAULT_DENY, false),
1494
+ eventFreshnessMs: parseFreshness(source.DSH_LARK_EVENT_FRESHNESS_MS)
1041
1495
  };
1042
1496
  }
1043
- var log = createLogger();
1044
1497
 
1045
- // src/bridge/lark-channel.ts
1046
- function adaptLarkChannel(channel) {
1047
- const base = {
1048
- async sendMarkdown(chatId, markdown2, options) {
1049
- const sendOptions = {};
1050
- if (options?.replyTo) sendOptions.replyTo = options.replyTo;
1051
- await channel.send(chatId, { markdown: markdown2 }, sendOptions);
1052
- }
1053
- };
1054
- return {
1055
- ...base,
1056
- async sendCard(chatId, card) {
1057
- await channel.send(chatId, { card }, {});
1058
- },
1059
- async streamCard(chatId, initial, producer, options) {
1060
- const sendOptions = {};
1061
- if (options?.replyTo) sendOptions.replyTo = options.replyTo;
1062
- await channel.stream(
1063
- chatId,
1064
- {
1065
- card: {
1066
- initial,
1067
- producer: async (controller) => {
1068
- await producer(controller);
1069
- }
1070
- }
1071
- },
1072
- sendOptions
1073
- );
1498
+ // src/config/profile-store.ts
1499
+ import { readFile } from "fs/promises";
1500
+ import { mkdir as mkdir4 } from "fs/promises";
1501
+ import { dirname as dirname2 } from "path";
1502
+
1503
+ // src/platform/atomic-write.ts
1504
+ import { randomBytes } from "crypto";
1505
+ import { mkdir as mkdir3, rename, writeFile as writeFile3 } from "fs/promises";
1506
+ import { dirname, join as join6 } from "path";
1507
+ async function writeFileAtomic(target, data, options = {}) {
1508
+ const directory = dirname(target);
1509
+ await mkdir3(directory, { recursive: true });
1510
+ const temporary = join6(directory, `.${randomBytes(8).toString("hex")}.tmp`);
1511
+ try {
1512
+ await writeFile3(temporary, data, options.mode === void 0 ? void 0 : { mode: options.mode });
1513
+ await rename(temporary, target);
1514
+ } catch (error) {
1515
+ await rmSilently(temporary);
1516
+ throw error;
1517
+ }
1518
+ }
1519
+ async function rmSilently(path) {
1520
+ const { rm } = await import("fs/promises");
1521
+ try {
1522
+ await rm(path, { force: true });
1523
+ } catch {
1524
+ }
1525
+ }
1526
+
1527
+ // src/config/profile-store.ts
1528
+ var ConfigStore = class {
1529
+ constructor(path) {
1530
+ this.path = path;
1531
+ }
1532
+ path;
1533
+ data;
1534
+ async load() {
1535
+ try {
1536
+ const raw = await readFile(this.path, "utf8");
1537
+ const parsed = JSON.parse(raw);
1538
+ this.data = {
1539
+ schemaVersion: 1,
1540
+ activeProfile: parsed.activeProfile ?? "default",
1541
+ profiles: Object.fromEntries(
1542
+ Object.entries(parsed.profiles ?? {}).map(([name, profile]) => [
1543
+ name,
1544
+ {
1545
+ ...profile,
1546
+ access: {
1547
+ allowedUsers: profile.access?.allowedUsers ?? [],
1548
+ allowedChats: profile.access?.allowedChats ?? [],
1549
+ admins: profile.access?.admins ?? []
1550
+ }
1551
+ }
1552
+ ])
1553
+ )
1554
+ };
1555
+ } catch (error) {
1556
+ if (error.code !== "ENOENT") throw error;
1557
+ this.data = {
1558
+ schemaVersion: 1,
1559
+ activeProfile: "default",
1560
+ profiles: {}
1561
+ };
1562
+ }
1563
+ return this.data;
1564
+ }
1565
+ getActiveProfile() {
1566
+ const data = this.getData();
1567
+ return data.profiles[data.activeProfile];
1568
+ }
1569
+ getProfile(name) {
1570
+ const data = this.getData();
1571
+ return data.profiles[name];
1572
+ }
1573
+ async saveProfile(name, input) {
1574
+ const data = this.getData();
1575
+ const existing = data.profiles[name];
1576
+ const profile = {
1577
+ schemaVersion: 1,
1578
+ agentKind: "dsh",
1579
+ tenant: input.tenant,
1580
+ accounts: {
1581
+ appId: input.appId,
1582
+ appSecret: input.appSecret
1583
+ },
1584
+ workspaces: {
1585
+ default: input.workspace ?? existing?.workspaces.default ?? void 0
1586
+ },
1587
+ preferences: {
1588
+ model: input.model ?? existing?.preferences.model ?? void 0,
1589
+ stopGraceMs: input.stopGraceMs ?? existing?.preferences.stopGraceMs ?? void 0,
1590
+ runTimeoutMs: input.runTimeoutMs ?? existing?.preferences.runTimeoutMs ?? void 0
1591
+ },
1592
+ access: {
1593
+ allowedUsers: input.access?.allowedUsers ?? existing?.access?.allowedUsers ?? [],
1594
+ allowedChats: input.access?.allowedChats ?? existing?.access?.allowedChats ?? [],
1595
+ admins: input.access?.admins ?? existing?.access?.admins ?? []
1596
+ }
1597
+ };
1598
+ if (input.operatorOpenId && !profile.access.allowedUsers.includes(input.operatorOpenId)) {
1599
+ profile.access.allowedUsers.push(input.operatorOpenId);
1600
+ profile.access.admins.push(input.operatorOpenId);
1601
+ }
1602
+ data.profiles[name] = profile;
1603
+ data.activeProfile = name;
1604
+ await this.persist();
1605
+ }
1606
+ getData() {
1607
+ if (!this.data) {
1608
+ throw new Error("ConfigStore must be loaded before use");
1609
+ }
1610
+ return this.data;
1611
+ }
1612
+ async persist() {
1613
+ const data = this.getData();
1614
+ await mkdir4(dirname2(this.path), { recursive: true });
1615
+ await writeFileAtomic(this.path, `${JSON.stringify(data, null, 2)}
1616
+ `, {
1617
+ mode: 384
1618
+ });
1619
+ }
1620
+ };
1621
+
1622
+ // src/cli/commands/doctor.ts
1623
+ async function runDoctor(options) {
1624
+ const env = loadRuntimeEnv({
1625
+ ...process.env,
1626
+ ...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
1627
+ ...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
1628
+ ...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
1629
+ ...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
1630
+ });
1631
+ const paths = resolveAppPaths(env.home);
1632
+ const profileName = options.profile ?? "default";
1633
+ const store = new ConfigStore(paths.configFile);
1634
+ await store.load();
1635
+ const profile = store.getProfile(profileName);
1636
+ const lines = [
1637
+ "dsh-lark-bot doctor",
1638
+ `version: ${options.version ?? "unknown"}`,
1639
+ `node: ${process.version}`,
1640
+ `profile: ${profileName}`,
1641
+ `home: ${paths.root}`,
1642
+ `adapter: ${env.adapterMode}`,
1643
+ `dsh_command: ${env.dshCommand}`,
1644
+ `dsh_args: ${env.dshArgs.join(",")}`
1645
+ ];
1646
+ let critical = false;
1647
+ if (!profile) {
1648
+ lines.push("config: missing");
1649
+ critical = true;
1650
+ } else {
1651
+ lines.push(
1652
+ [
1653
+ "config: ok",
1654
+ `tenant=${profile.tenant}`,
1655
+ `app_id=${profile.accounts.appId}`,
1656
+ `app_secret=${profile.accounts.appSecret ? "present" : "missing"}`,
1657
+ `allowed_users=${profile.access.allowedUsers.length}`,
1658
+ `allowed_chats=${profile.access.allowedChats.length}`
1659
+ ].join(" ")
1660
+ );
1661
+ if (!profile.accounts.appId || !profile.accounts.appSecret) critical = true;
1662
+ }
1663
+ const workspace = options.workspace ?? profile?.workspaces.default ?? env.workspace ?? paths.profilePath(profileName, "workspace");
1664
+ try {
1665
+ const info = await stat(workspace);
1666
+ lines.push(`workspace: ${workspace} (${info.isDirectory() ? "directory" : "not-directory"})`);
1667
+ } catch {
1668
+ lines.push(`workspace: ${workspace} (missing)`);
1669
+ }
1670
+ try {
1671
+ const adapter = await buildAgentAdapter(env, {
1672
+ stopGraceMs: profile?.preferences.stopGraceMs,
1673
+ model: profile?.preferences.model
1674
+ });
1675
+ const availability = await adapter.checkAvailability();
1676
+ if (availability.ok) {
1677
+ lines.push(`dsh: ok${availability.version ? ` (${availability.version})` : ""}`);
1678
+ } else {
1679
+ lines.push(`dsh: unavailable (${availability.error ?? "unknown"})`);
1680
+ critical = true;
1681
+ }
1682
+ await adapter.dispose?.();
1683
+ } catch (error) {
1684
+ lines.push(`dsh: unavailable (${error instanceof Error ? error.message : String(error)})`);
1685
+ critical = true;
1686
+ }
1687
+ const output = options.output ?? ((text) => process.stdout.write(text));
1688
+ output(`${lines.join("\n")}
1689
+ `);
1690
+ if (critical) process.exitCode = 1;
1691
+ }
1692
+
1693
+ // src/cli/commands/start.ts
1694
+ import { mkdir as mkdir7 } from "fs/promises";
1695
+
1696
+ // src/bot/active-runs.ts
1697
+ var ActiveRuns = class {
1698
+ runs = /* @__PURE__ */ new Map();
1699
+ set(scope, handle) {
1700
+ this.runs.set(scope, handle);
1701
+ }
1702
+ get(scope) {
1703
+ return this.runs.get(scope);
1704
+ }
1705
+ delete(scope) {
1706
+ return this.runs.delete(scope);
1707
+ }
1708
+ async interrupt(scope) {
1709
+ const handle = this.runs.get(scope);
1710
+ if (!handle) return false;
1711
+ this.runs.delete(scope);
1712
+ await handle.stop();
1713
+ return true;
1714
+ }
1715
+ };
1716
+
1717
+ // src/config/security.ts
1718
+ import { realpathSync } from "fs";
1719
+ import { isIP } from "net";
1720
+ import { isAbsolute, relative, resolve as resolve3, sep } from "path";
1721
+ var REDACTED = "[redacted]";
1722
+ var SECRET_PATTERNS = [
1723
+ { label: "bearer", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/g, keyed: false },
1724
+ { label: "deepseek-key", pattern: /\bsk-[A-Za-z0-9_-]{8,}/g, keyed: false },
1725
+ { label: "api-key", pattern: /\b(api[_-]?key)\s*[=:]\s*["']?[A-Za-z0-9._~+/=-]{8,}/gi, keyed: true }
1726
+ ];
1727
+ function redactSecrets(text) {
1728
+ let result = text;
1729
+ for (const { pattern, keyed } of SECRET_PATTERNS) {
1730
+ result = keyed ? result.replace(pattern, (_match, keyName) => `${keyName}=${REDACTED}`) : result.replace(pattern, REDACTED);
1731
+ }
1732
+ return result;
1733
+ }
1734
+ function resolveReal(path) {
1735
+ try {
1736
+ return realpathSync(path);
1737
+ } catch {
1738
+ return resolve3(path);
1739
+ }
1740
+ }
1741
+ function isPathWithin(root, candidate) {
1742
+ const rootReal = resolveReal(root);
1743
+ const candidateReal = resolveReal(candidate);
1744
+ const rel = relative(rootReal, candidateReal);
1745
+ return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
1746
+ }
1747
+ function truncateUtf8Safe(text, maxBytes) {
1748
+ if (maxBytes <= 0) return "";
1749
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
1750
+ let slice = text.slice(0, maxBytes);
1751
+ while (Buffer.byteLength(slice, "utf8") > maxBytes && slice.length > 0) {
1752
+ slice = slice.slice(0, -1);
1753
+ }
1754
+ const code = slice.charCodeAt(slice.length - 1);
1755
+ if (code >= 55296 && code <= 56319) {
1756
+ slice = slice.slice(0, -1);
1757
+ }
1758
+ return slice;
1759
+ }
1760
+ function isEventFresh(timestampMs, windowMs, now = Date.now()) {
1761
+ if (timestampMs === void 0) return true;
1762
+ return now - timestampMs <= windowMs;
1763
+ }
1764
+
1765
+ // src/core/logger.ts
1766
+ var REDACTED2 = "[redacted]";
1767
+ var SENSITIVE_KEY = /(secret|token|password|api[_-]?key)/i;
1768
+ function redactValue(value, key, depth) {
1769
+ if (SENSITIVE_KEY.test(key)) return REDACTED2;
1770
+ if (typeof value === "string") return redactSecrets(value);
1771
+ if (depth === 0) return value;
1772
+ if (Array.isArray(value)) {
1773
+ return value.map((item, index) => redactValue(item, `${key}.${index}`, depth - 1));
1774
+ }
1775
+ if (value && typeof value === "object") {
1776
+ const out = {};
1777
+ for (const [childKey, childValue] of Object.entries(value)) {
1778
+ out[childKey] = redactValue(childValue, childKey, depth - 1);
1779
+ }
1780
+ return out;
1781
+ }
1782
+ return value;
1783
+ }
1784
+ function redactFields(fields, depth = 4) {
1785
+ return redactValue(fields, "", depth);
1786
+ }
1787
+ function serializeMessage(level, category, event, fields) {
1788
+ const entry = {
1789
+ time: (/* @__PURE__ */ new Date()).toISOString(),
1790
+ level,
1791
+ category,
1792
+ event,
1793
+ fields: redactFields(fields)
1794
+ };
1795
+ return JSON.stringify(entry);
1796
+ }
1797
+ function createLogger(output = process.stderr) {
1798
+ const write = (level, category, event, fields) => {
1799
+ output.write(`${serializeMessage(level, category, event, fields)}
1800
+ `);
1801
+ };
1802
+ return {
1803
+ info: (category, event, fields = {}) => write("info", category, event, fields),
1804
+ warn: (category, event, fields = {}) => write("warn", category, event, fields),
1805
+ error: (category, event, fields = {}) => write("error", category, event, fields),
1806
+ fail(category, error, fields = {}) {
1807
+ const message = error instanceof Error ? error.message : String(error);
1808
+ write("error", category, "fail", { message, ...fields });
1809
+ }
1810
+ };
1811
+ }
1812
+ var log = createLogger();
1813
+
1814
+ // src/bot/approvals.ts
1815
+ var ApprovalRegistry = class {
1816
+ pending = /* @__PURE__ */ new Map();
1817
+ register(scope, request) {
1818
+ const key = `${scope}:${request.id}`;
1819
+ const existing = this.pending.get(key);
1820
+ if (existing) return Promise.resolve("cancelled");
1821
+ return new Promise((resolve5) => {
1822
+ this.pending.set(key, { request, resolve: resolve5, settled: false });
1823
+ });
1824
+ }
1825
+ resolve(scope, id, outcome) {
1826
+ const key = `${scope}:${id}`;
1827
+ const pending = this.pending.get(key);
1828
+ if (!pending || pending.settled) return false;
1829
+ pending.settled = true;
1830
+ this.pending.delete(key);
1831
+ pending.resolve(outcome);
1832
+ return true;
1833
+ }
1834
+ /** Settle every pending approval for a scope (run end / dispose). */
1835
+ settleAll(scope, outcome) {
1836
+ let count = 0;
1837
+ for (const [key, pending] of this.pending) {
1838
+ if (!key.startsWith(`${scope}:`)) continue;
1839
+ if (pending.settled) continue;
1840
+ pending.settled = true;
1841
+ this.pending.delete(key);
1842
+ pending.resolve(outcome);
1843
+ count += 1;
1844
+ }
1845
+ if (count > 0) {
1846
+ log.warn("approvals", "settled-pending", { scope, outcome, count });
1847
+ }
1848
+ return count;
1849
+ }
1850
+ pendingCount(scope) {
1851
+ let count = 0;
1852
+ for (const key of this.pending.keys()) {
1853
+ if (key.startsWith(`${scope}:`)) count += 1;
1854
+ }
1855
+ return count;
1856
+ }
1857
+ };
1858
+
1859
+ // src/bot/density-store.ts
1860
+ var DensityStore = class {
1861
+ constructor(defaultDensity = "standard") {
1862
+ this.defaultDensity = defaultDensity;
1863
+ }
1864
+ defaultDensity;
1865
+ overrides = /* @__PURE__ */ new Map();
1866
+ get(scope) {
1867
+ return this.overrides.get(scope) ?? this.defaultDensity;
1868
+ }
1869
+ set(scope, density) {
1870
+ this.overrides.set(scope, density);
1871
+ }
1872
+ clear(scope) {
1873
+ return this.overrides.delete(scope);
1874
+ }
1875
+ };
1876
+
1877
+ // src/bot/pending-queue.ts
1878
+ var PendingQueue = class {
1879
+ constructor(quietMs, onFlush) {
1880
+ this.quietMs = quietMs;
1881
+ this.onFlush = onFlush;
1882
+ }
1883
+ quietMs;
1884
+ onFlush;
1885
+ pending = /* @__PURE__ */ new Map();
1886
+ timers = /* @__PURE__ */ new Map();
1887
+ blocked = /* @__PURE__ */ new Set();
1888
+ flushing = /* @__PURE__ */ new Set();
1889
+ push(scope, item) {
1890
+ const batch = this.pending.get(scope) ?? [];
1891
+ batch.push(item);
1892
+ this.pending.set(scope, batch);
1893
+ this.schedule(scope);
1894
+ }
1895
+ block(scope) {
1896
+ this.blocked.add(scope);
1897
+ this.clearTimer(scope);
1898
+ }
1899
+ unblock(scope) {
1900
+ this.blocked.delete(scope);
1901
+ this.schedule(scope);
1902
+ }
1903
+ async flushNow(scope) {
1904
+ this.clearTimer(scope);
1905
+ if (this.flushing.has(scope)) return;
1906
+ const batch = this.pending.get(scope) ?? [];
1907
+ this.pending.delete(scope);
1908
+ if (batch.length === 0) return;
1909
+ this.flushing.add(scope);
1910
+ try {
1911
+ await this.onFlush(scope, batch);
1912
+ } finally {
1913
+ this.flushing.delete(scope);
1914
+ }
1915
+ }
1916
+ hasPending(scope) {
1917
+ return (this.pending.get(scope)?.length ?? 0) > 0;
1918
+ }
1919
+ isBlocked(scope) {
1920
+ return this.blocked.has(scope);
1921
+ }
1922
+ schedule(scope) {
1923
+ if (this.blocked.has(scope) || this.flushing.has(scope)) return;
1924
+ if (this.timers.has(scope)) return;
1925
+ if (!this.hasPending(scope)) return;
1926
+ this.timers.set(
1927
+ scope,
1928
+ setTimeout(() => {
1929
+ this.timers.delete(scope);
1930
+ void this.flushNow(scope);
1931
+ }, this.quietMs)
1932
+ );
1933
+ }
1934
+ clearTimer(scope) {
1935
+ const timer = this.timers.get(scope);
1936
+ if (timer === void 0) return;
1937
+ clearTimeout(timer);
1938
+ this.timers.delete(scope);
1939
+ }
1940
+ };
1941
+
1942
+ // src/bot/questions.ts
1943
+ import { randomUUID as randomUUID3 } from "crypto";
1944
+ var QuestionRegistry = class {
1945
+ pending = /* @__PURE__ */ new Map();
1946
+ register(scope, input) {
1947
+ const id = `question-${randomUUID3().replaceAll("-", "")}`;
1948
+ const full = { ...input, id };
1949
+ const promise = new Promise((resolve5) => {
1950
+ this.pending.set(`${scope}:${id}`, { input: full, resolve: resolve5, settled: false });
1951
+ });
1952
+ return { id, promise };
1953
+ }
1954
+ resolve(scope, id, answer) {
1955
+ const pending = this.pending.get(`${scope}:${id}`);
1956
+ if (!pending || pending.settled) return false;
1957
+ pending.settled = true;
1958
+ this.pending.delete(`${scope}:${id}`);
1959
+ pending.resolve(answer);
1960
+ return true;
1961
+ }
1962
+ settleAll(scope) {
1963
+ let count = 0;
1964
+ for (const [key, pending] of this.pending) {
1965
+ if (!key.startsWith(`${scope}:`)) continue;
1966
+ if (pending.settled) continue;
1967
+ pending.settled = true;
1968
+ this.pending.delete(key);
1969
+ pending.resolve(void 0);
1970
+ count += 1;
1971
+ }
1972
+ return count;
1973
+ }
1974
+ get(scope, id) {
1975
+ return this.pending.get(`${scope}:${id}`)?.input;
1976
+ }
1977
+ };
1978
+
1979
+ // src/bot/run-policy.ts
1980
+ var RunPolicyStore = class {
1981
+ timeouts = /* @__PURE__ */ new Map();
1982
+ get(scope) {
1983
+ return this.timeouts.get(scope);
1984
+ }
1985
+ set(scope, runTimeoutMs) {
1986
+ this.timeouts.set(scope, runTimeoutMs);
1987
+ }
1988
+ clear(scope) {
1989
+ return this.timeouts.delete(scope);
1990
+ }
1991
+ };
1992
+
1993
+ // src/bridge/channel.ts
1994
+ import { createLarkChannel } from "@larksuite/channel";
1995
+
1996
+ // src/commands/index.ts
1997
+ import { resolve as resolve4 } from "path";
1998
+
1999
+ // src/card/workspace-card.ts
2000
+ function renderWorkspaceCard(input) {
2001
+ const entries = input.index;
2002
+ const markdown2 = [
2003
+ `**\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55**
2004
+ \`${input.current}\``,
2005
+ "",
2006
+ ...entries.length > 0 ? entries.map(
2007
+ ({ name, cwd, lastUsed }) => `- **${name}** \u2192 \`${cwd}\`${lastUsed ? ` \xB7 ${new Date(lastUsed).toLocaleString()}` : ""}`
2008
+ ) : ["\u6682\u65E0\u547D\u540D\u5DE5\u4F5C\u7A7A\u95F4\u3002"]
2009
+ ].join("\n");
2010
+ return {
2011
+ schema: "2.0",
2012
+ config: {
2013
+ summary: { content: "\u5DE5\u4F5C\u7A7A\u95F4\u5BFC\u822A" }
2014
+ },
2015
+ body: {
2016
+ elements: [
2017
+ {
2018
+ tag: "markdown",
2019
+ content: markdown2
2020
+ }
2021
+ ]
2022
+ }
2023
+ };
2024
+ }
2025
+
2026
+ // src/card/density.ts
2027
+ var CARD_DENSITIES = ["compact", "standard", "detailed"];
2028
+ function parseCardDensity(value) {
2029
+ const normalized = value?.trim().toLowerCase();
2030
+ if (!normalized) return void 0;
2031
+ if (CARD_DENSITIES.includes(normalized)) {
2032
+ return normalized;
2033
+ }
2034
+ return void 0;
2035
+ }
2036
+
2037
+ // src/bridge/run-flow.ts
2038
+ import { randomUUID as randomUUID4 } from "crypto";
2039
+
2040
+ // src/card/run-state.ts
2041
+ var initialState = {
2042
+ blocks: [],
2043
+ reasoning: { content: "", active: false },
2044
+ usage: void 0,
2045
+ footer: "thinking",
2046
+ terminal: "running",
2047
+ errorMsg: void 0,
2048
+ idleTimeoutMinutes: void 0
2049
+ };
2050
+ function closeStreamingText(blocks) {
2051
+ return blocks.map(
2052
+ (block) => block.kind === "text" && block.streaming ? { ...block, streaming: false } : block
2053
+ );
2054
+ }
2055
+ function reduce(state, event) {
2056
+ switch (event.type) {
2057
+ case "text": {
2058
+ const last = state.blocks[state.blocks.length - 1];
2059
+ if (last?.kind === "text" && last.streaming) {
2060
+ return {
2061
+ ...state,
2062
+ blocks: [
2063
+ ...state.blocks.slice(0, -1),
2064
+ { ...last, content: last.content + event.delta }
2065
+ ],
2066
+ reasoning: { ...state.reasoning, active: false },
2067
+ footer: "streaming"
2068
+ };
2069
+ }
2070
+ return {
2071
+ ...state,
2072
+ blocks: [...state.blocks, { kind: "text", content: event.delta, streaming: true }],
2073
+ reasoning: { ...state.reasoning, active: false },
2074
+ footer: "streaming"
2075
+ };
1074
2076
  }
2077
+ case "thinking":
2078
+ return {
2079
+ ...state,
2080
+ reasoning: {
2081
+ content: state.reasoning.content + event.delta,
2082
+ active: true
2083
+ },
2084
+ footer: "thinking"
2085
+ };
2086
+ case "final_text":
2087
+ return {
2088
+ ...state,
2089
+ blocks: [
2090
+ ...closeStreamingText(state.blocks),
2091
+ { kind: "text", content: event.content, streaming: false }
2092
+ ],
2093
+ reasoning: { ...state.reasoning, active: false },
2094
+ footer: null
2095
+ };
2096
+ case "usage":
2097
+ return {
2098
+ ...state,
2099
+ usage: {
2100
+ inputTokens: event.inputTokens,
2101
+ outputTokens: event.outputTokens
2102
+ }
2103
+ };
2104
+ case "tool_use":
2105
+ return {
2106
+ ...state,
2107
+ blocks: [
2108
+ ...closeStreamingText(state.blocks),
2109
+ {
2110
+ kind: "tool",
2111
+ tool: {
2112
+ id: event.id,
2113
+ name: event.name,
2114
+ input: event.input,
2115
+ status: "running"
2116
+ }
2117
+ }
2118
+ ],
2119
+ reasoning: { ...state.reasoning, active: false },
2120
+ footer: "tool_running"
2121
+ };
2122
+ case "tool_result":
2123
+ return {
2124
+ ...state,
2125
+ blocks: state.blocks.map((block) => {
2126
+ if (block.kind !== "tool" || block.tool.id !== event.id) return block;
2127
+ return {
2128
+ ...block,
2129
+ tool: {
2130
+ ...block.tool,
2131
+ status: event.isError ? "error" : "done",
2132
+ output: event.output
2133
+ }
2134
+ };
2135
+ })
2136
+ };
2137
+ case "error":
2138
+ return {
2139
+ ...state,
2140
+ terminal: event.terminationReason === "interrupted" ? "interrupted" : event.terminationReason === "timeout" ? "idle_timeout" : "error",
2141
+ errorMsg: event.terminationReason === "failed" ? event.message : void 0,
2142
+ idleTimeoutMinutes: void 0,
2143
+ footer: null
2144
+ };
2145
+ case "done":
2146
+ return {
2147
+ ...state,
2148
+ blocks: closeStreamingText(state.blocks),
2149
+ reasoning: { ...state.reasoning, active: false },
2150
+ terminal: event.terminationReason === "interrupted" ? "interrupted" : event.terminationReason === "timeout" ? "idle_timeout" : "done",
2151
+ footer: null
2152
+ };
2153
+ default:
2154
+ return state;
2155
+ }
2156
+ }
2157
+ function markInterrupted(state) {
2158
+ return {
2159
+ ...state,
2160
+ blocks: closeStreamingText(state.blocks),
2161
+ reasoning: { ...state.reasoning, active: false },
2162
+ terminal: "interrupted",
2163
+ footer: null
2164
+ };
2165
+ }
2166
+ function markIdleTimeout(state, minutes) {
2167
+ return {
2168
+ ...state,
2169
+ blocks: closeStreamingText(state.blocks),
2170
+ reasoning: { ...state.reasoning, active: false },
2171
+ terminal: "idle_timeout",
2172
+ footer: null,
2173
+ idleTimeoutMinutes: minutes
2174
+ };
2175
+ }
2176
+ function finalizeIfRunning(state) {
2177
+ if (state.terminal !== "running") return state;
2178
+ return {
2179
+ ...state,
2180
+ blocks: closeStreamingText(state.blocks),
2181
+ reasoning: { ...state.reasoning, active: false },
2182
+ terminal: "done",
2183
+ footer: null
1075
2184
  };
1076
2185
  }
1077
2186
 
1078
- // src/bridge/channel.ts
1079
- async function startChannel(deps) {
1080
- const channel = (deps.createChannel ?? createLarkChannel)({
1081
- appId: deps.appId,
1082
- appSecret: deps.appSecret,
1083
- domain: deps.tenant === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn",
1084
- source: "dsh-lark-bot",
1085
- policy: {
1086
- dmMode: deps.allowedUsers?.length ? "allowlist" : "open",
1087
- requireMention: true,
1088
- respondToMentionAll: false,
1089
- ...deps.allowedUsers ? { dmAllowlist: deps.allowedUsers } : {},
1090
- ...deps.allowedChats ? { groupAllowlist: deps.allowedChats } : {}
1091
- },
1092
- safety: {
1093
- chatQueue: { enabled: false }
2187
+ // src/card/run-renderer.ts
2188
+ function markdown(content) {
2189
+ return { tag: "markdown", content };
2190
+ }
2191
+ function noteMd(content) {
2192
+ return { tag: "markdown", content, text_size: "notation" };
2193
+ }
2194
+ function footerStatus(status) {
2195
+ 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";
2196
+ return noteMd(text);
2197
+ }
2198
+ function summaryText(state) {
2199
+ if (state.terminal === "interrupted") return "\u5DF2\u4E2D\u65AD";
2200
+ if (state.terminal === "idle_timeout") return "\u5DF2\u8D85\u65F6";
2201
+ if (state.terminal === "error") return "\u51FA\u9519";
2202
+ if (state.terminal === "done") return "\u5DF2\u5B8C\u6210";
2203
+ if (state.footer === "tool_running") return "\u6B63\u5728\u8C03\u7528\u5DE5\u5177";
2204
+ if (state.footer === "streaming") return "\u6B63\u5728\u8F93\u51FA";
2205
+ return "\u601D\u8003\u4E2D";
2206
+ }
2207
+ function stopButton() {
2208
+ return {
2209
+ tag: "button",
2210
+ text: { tag: "plain_text", content: "\u23F9 \u7EC8\u6B62" },
2211
+ type: "danger",
2212
+ value: { cmd: "stop" }
2213
+ };
2214
+ }
2215
+ function textBlock(block) {
2216
+ return markdown(block.content);
2217
+ }
2218
+ function toolBlock(tool) {
2219
+ const icon = tool.status === "error" ? "\u26A0\uFE0F" : tool.status === "done" ? "\u2705" : "\u23F3";
2220
+ return markdown(`${icon} **${tool.name}**`);
2221
+ }
2222
+ function usageLine(state) {
2223
+ if (!state.usage) return "";
2224
+ const parts = [];
2225
+ if (state.usage.inputTokens !== void 0) parts.push(`in ${state.usage.inputTokens}`);
2226
+ if (state.usage.outputTokens !== void 0) parts.push(`out ${state.usage.outputTokens}`);
2227
+ return parts.length ? `\uFF08tokens ${parts.join(" \xB7 ")}\uFF09` : "";
2228
+ }
2229
+ function renderStandard(state) {
2230
+ const elements = [];
2231
+ if (state.reasoning.content) {
2232
+ elements.push(
2233
+ noteMd(
2234
+ state.reasoning.active ? "\u{1F9E0} \u6B63\u5728\u601D\u8003\u2026" : `\u{1F9E0} \u601D\u8003\u5B8C\u6210\uFF1A${state.reasoning.content.slice(0, 300)}`
2235
+ )
2236
+ );
2237
+ }
2238
+ for (const block of state.blocks) {
2239
+ if (block.kind === "text") {
2240
+ elements.push(textBlock(block));
2241
+ } else if (block.tool.status !== "done") {
2242
+ elements.push(toolBlock(block.tool));
2243
+ }
2244
+ }
2245
+ if (state.terminal === "interrupted") {
2246
+ elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
2247
+ } else if (state.terminal === "idle_timeout") {
2248
+ elements.push(noteMd(`_\u23F1 ${state.idleTimeoutMinutes ?? 0} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
2249
+ } else if (state.terminal === "error" && state.errorMsg) {
2250
+ elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
2251
+ } else if (state.terminal === "done" && elements.length === 0) {
2252
+ elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
2253
+ } else if (state.terminal === "done") {
2254
+ const usage = usageLine(state);
2255
+ if (usage) elements.push(noteMd(usage));
2256
+ }
2257
+ if (state.terminal === "running") {
2258
+ if (state.footer) elements.push(footerStatus(state.footer));
2259
+ elements.push(stopButton());
2260
+ }
2261
+ return {
2262
+ schema: "2.0",
2263
+ config: {
2264
+ streaming_mode: state.terminal === "running",
2265
+ summary: { content: summaryText(state) }
1094
2266
  },
1095
- outbound: {
1096
- streamThrottleMs: 400
2267
+ body: { elements }
2268
+ };
2269
+ }
2270
+ function renderCompact(state) {
2271
+ const elements = [noteMd(summaryText(state))];
2272
+ if (state.terminal === "running" && state.footer) {
2273
+ elements.push(footerStatus(state.footer));
2274
+ }
2275
+ if (state.terminal === "running") {
2276
+ elements.push(stopButton());
2277
+ }
2278
+ return {
2279
+ schema: "2.0",
2280
+ config: {
2281
+ streaming_mode: state.terminal === "running",
2282
+ summary: { content: summaryText(state) }
1097
2283
  },
1098
- includeRawEvent: true,
1099
- handshakeTimeoutMs: 8e3,
1100
- httpTimeoutMs: 3e4,
1101
- respectProxyEnv: true
1102
- });
1103
- const streaming = adaptLarkChannel(channel);
1104
- const commandChannel = streaming;
1105
- channel.on({
1106
- message: async (msg) => {
1107
- const scope = scopeForMessage(msg);
1108
- const context = {
1109
- scope,
1110
- chatId: msg.chatId,
1111
- messageId: msg.messageId,
1112
- threadId: msg.threadId,
1113
- chatMode: msg.chatMode ?? msg.chatType,
1114
- sessions: deps.sessions,
1115
- workspaces: deps.workspaces,
1116
- activeRuns: deps.activeRuns,
1117
- runPolicies: deps.runPolicies,
1118
- defaultRunTimeoutMs: deps.defaultRunTimeoutMs,
1119
- accessManager: deps.accessManager,
1120
- channel: commandChannel,
1121
- defaultWorkspace: deps.defaultWorkspace
1122
- };
1123
- const handled = await tryHandleCommand(msg.content, context).catch((error) => {
1124
- log.fail("channel-command", error, { scope });
1125
- return false;
1126
- });
1127
- if (!handled) deps.pending.push(scope, msg);
2284
+ body: { elements }
2285
+ };
2286
+ }
2287
+ function renderDetailed(state) {
2288
+ const elements = [];
2289
+ if (state.reasoning.content) {
2290
+ elements.push(
2291
+ noteMd(
2292
+ state.reasoning.active ? "\u{1F9E0} \u6B63\u5728\u601D\u8003\u2026" : `\u{1F9E0} **\u601D\u8003\u8FC7\u7A0B**
2293
+ ${state.reasoning.content.slice(0, 2e3)}`
2294
+ )
2295
+ );
2296
+ }
2297
+ for (const block of state.blocks) {
2298
+ if (block.kind === "text") {
2299
+ elements.push(textBlock(block));
2300
+ continue;
2301
+ }
2302
+ const tool = block.tool;
2303
+ const icon = tool.status === "error" ? "\u26A0\uFE0F" : tool.status === "done" ? "\u2705" : "\u23F3";
2304
+ const lines = [`${icon} **${tool.name}**`];
2305
+ if (tool.input !== void 0 && tool.input !== "") {
2306
+ lines.push(`\u8F93\u5165\uFF1A\`\`\`
2307
+ ${safeJsonPreview(tool.input)}
2308
+ \`\`\``);
2309
+ }
2310
+ if (tool.output) {
2311
+ lines.push(`\u8F93\u51FA\uFF1A${tool.output.slice(0, 500)}`);
2312
+ }
2313
+ elements.push(markdown(lines.join("\n")));
2314
+ }
2315
+ if (state.terminal === "interrupted") {
2316
+ elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
2317
+ } else if (state.terminal === "idle_timeout") {
2318
+ elements.push(noteMd(`_\u23F1 ${state.idleTimeoutMinutes ?? 0} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
2319
+ } else if (state.terminal === "error" && state.errorMsg) {
2320
+ elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
2321
+ } else if (state.terminal === "done") {
2322
+ const usage = usageLine(state);
2323
+ if (usage) elements.push(noteMd(usage));
2324
+ }
2325
+ if (state.terminal === "running") {
2326
+ if (state.footer) elements.push(footerStatus(state.footer));
2327
+ elements.push(stopButton());
2328
+ }
2329
+ return {
2330
+ schema: "2.0",
2331
+ config: {
2332
+ streaming_mode: state.terminal === "running",
2333
+ summary: { content: summaryText(state) }
1128
2334
  },
1129
- cardAction: async (event) => {
1130
- const value = event.action.value && typeof event.action.value === "object" ? event.action.value : void 0;
1131
- if (value?.cmd === "stop") {
1132
- const scope = event.raw ? await resolveCardScope(event.chatId, event.raw) : event.chatId;
1133
- await deps.activeRuns.interrupt(scope);
2335
+ body: { elements }
2336
+ };
2337
+ }
2338
+ function safeJsonPreview(value) {
2339
+ if (typeof value === "string") return value.slice(0, 300);
2340
+ try {
2341
+ return JSON.stringify(value).slice(0, 300);
2342
+ } catch {
2343
+ return String(value).slice(0, 300);
2344
+ }
2345
+ }
2346
+ function renderCard(state, density = "standard") {
2347
+ if (density === "compact") return renderCompact(state);
2348
+ if (density === "detailed") return renderDetailed(state);
2349
+ return renderStandard(state);
2350
+ }
2351
+
2352
+ // src/card/approval-card.ts
2353
+ function renderApprovalCard(input) {
2354
+ const allow = input.options.find((option) => option.kind === "allow_once") ?? input.options.find((option) => option.kind === "allow_always");
2355
+ const reject = input.options.find((option) => option.kind === "reject_once") ?? input.options.find((option) => option.kind === "reject_always");
2356
+ const markdown2 = [
2357
+ "\u{1F510} **\u5BA1\u6279\u8BF7\u6C42**",
2358
+ "",
2359
+ `**\u5DE5\u5177** \`${input.toolName}\``,
2360
+ ...input.reason ? ["", input.reason] : []
2361
+ ].join("\n");
2362
+ const actions = [
2363
+ ...allow ? [
2364
+ {
2365
+ tag: "button",
2366
+ text: { tag: "plain_text", content: `\u2705 ${allow.name}` },
2367
+ type: "primary",
2368
+ value: { cmd: "approve", id: input.id, outcome: "allow" }
1134
2369
  }
2370
+ ] : [],
2371
+ ...reject ? [
2372
+ {
2373
+ tag: "button",
2374
+ text: { tag: "plain_text", content: `\u26D4 ${reject.name}` },
2375
+ type: "danger",
2376
+ value: { cmd: "approve", id: input.id, outcome: "reject" }
2377
+ }
2378
+ ] : []
2379
+ ];
2380
+ return {
2381
+ schema: "2.0",
2382
+ config: {
2383
+ summary: { content: "\u5BA1\u6279\u8BF7\u6C42" }
1135
2384
  },
1136
- reconnecting: () => {
1137
- log.warn("channel", "reconnecting", {});
1138
- },
1139
- reconnected: () => {
1140
- log.info("channel", "reconnected", {});
1141
- },
1142
- error: (error) => {
1143
- log.fail("channel", error);
2385
+ body: {
2386
+ elements: [
2387
+ { tag: "markdown", content: markdown2 },
2388
+ { tag: "action", actions }
2389
+ ]
1144
2390
  }
1145
- });
1146
- await channel.connect();
2391
+ };
2392
+ }
2393
+
2394
+ // src/card/question-card.ts
2395
+ function formElement(input) {
2396
+ if (input.kind === "text") {
2397
+ return {
2398
+ tag: "input",
2399
+ name: "answer",
2400
+ placeholder: { tag: "plain_text", content: input.placeholder ?? "\u8BF7\u8F93\u5165\u7B54\u6848\u2026" }
2401
+ };
2402
+ }
2403
+ const options = (input.options ?? []).map((option, index) => ({
2404
+ text: { tag: "plain_text", content: option },
2405
+ value: `option-${index}`
2406
+ }));
2407
+ if (input.kind === "multi") {
2408
+ return {
2409
+ tag: "multi_select_static",
2410
+ name: "answer",
2411
+ placeholder: { tag: "plain_text", content: "\u8BF7\u9009\u62E9\uFF08\u53EF\u591A\u9009\uFF09\u2026" },
2412
+ options
2413
+ };
2414
+ }
1147
2415
  return {
1148
- channel,
1149
- disconnect: () => channel.disconnect()
2416
+ tag: "select_static",
2417
+ name: "answer",
2418
+ placeholder: { tag: "plain_text", content: "\u8BF7\u9009\u62E9\u2026" },
2419
+ options
2420
+ };
2421
+ }
2422
+ function renderQuestionCard(input) {
2423
+ return {
2424
+ schema: "2.0",
2425
+ config: {
2426
+ summary: { content: "\u95EE\u9898" }
2427
+ },
2428
+ body: {
2429
+ elements: [
2430
+ { tag: "markdown", content: `\u2753 ${input.question}` },
2431
+ formElement(input),
2432
+ {
2433
+ tag: "action",
2434
+ actions: [
2435
+ {
2436
+ tag: "button",
2437
+ text: { tag: "plain_text", content: "\u63D0\u4EA4" },
2438
+ type: "primary",
2439
+ value: { cmd: "question-submit", id: input.id }
2440
+ }
2441
+ ]
2442
+ }
2443
+ ]
2444
+ }
1150
2445
  };
1151
2446
  }
1152
- function scopeForMessage(msg) {
1153
- if (msg.chatMode === "topic" && msg.threadId) return `${msg.chatId}:${msg.threadId}`;
1154
- return msg.chatId;
1155
- }
1156
- async function resolveCardScope(chatId, raw) {
1157
- return raw.message?.thread_id ? `${chatId}:${raw.message.thread_id}` : chatId;
2447
+ function extractQuestionAnswer(kind, value, options) {
2448
+ const answer = value;
2449
+ if (kind === "text") {
2450
+ return typeof answer === "string" && answer.trim() ? answer.trim() : void 0;
2451
+ }
2452
+ const selected = Array.isArray(answer) ? answer.map((item) => String(item)) : typeof answer === "string" && answer ? [answer] : [];
2453
+ if (selected.length === 0) return void 0;
2454
+ const labels = options ?? [];
2455
+ const resolved = selected.map((item) => {
2456
+ const match = /^option-(\d+)$/.exec(item);
2457
+ if (match) {
2458
+ const index = Number(match[1]);
2459
+ return labels[index] ?? item;
2460
+ }
2461
+ return item;
2462
+ });
2463
+ return kind === "single" ? resolved[0] : resolved;
1158
2464
  }
1159
2465
 
1160
2466
  // src/bridge/run-flow.ts
1161
- import { randomUUID } from "crypto";
1162
-
1163
- // src/card/run-state.ts
1164
- var initialState = {
1165
- blocks: [],
1166
- reasoning: { content: "", active: false },
1167
- footer: "thinking",
1168
- terminal: "running",
1169
- errorMsg: void 0,
1170
- idleTimeoutMinutes: void 0
1171
- };
1172
- function closeStreamingText(blocks) {
1173
- return blocks.map(
1174
- (block) => block.kind === "text" && block.streaming ? { ...block, streaming: false } : block
1175
- );
1176
- }
1177
- function reduce(state, event) {
1178
- switch (event.type) {
1179
- case "text": {
1180
- const last = state.blocks[state.blocks.length - 1];
1181
- if (last?.kind === "text" && last.streaming) {
1182
- return {
1183
- ...state,
1184
- blocks: [
1185
- ...state.blocks.slice(0, -1),
1186
- { ...last, content: last.content + event.delta }
1187
- ],
1188
- reasoning: { ...state.reasoning, active: false },
1189
- footer: "streaming"
1190
- };
1191
- }
1192
- return {
1193
- ...state,
1194
- blocks: [...state.blocks, { kind: "text", content: event.delta, streaming: true }],
1195
- reasoning: { ...state.reasoning, active: false },
1196
- footer: "streaming"
1197
- };
1198
- }
1199
- case "thinking":
1200
- return {
1201
- ...state,
1202
- reasoning: {
1203
- content: state.reasoning.content + event.delta,
1204
- active: true
1205
- },
1206
- footer: "thinking"
1207
- };
1208
- case "final_text":
1209
- return {
1210
- ...state,
1211
- blocks: [
1212
- ...closeStreamingText(state.blocks),
1213
- { kind: "text", content: event.content, streaming: false }
1214
- ],
1215
- reasoning: { ...state.reasoning, active: false },
1216
- footer: null
1217
- };
1218
- case "tool_use":
1219
- return {
1220
- ...state,
1221
- blocks: [
1222
- ...closeStreamingText(state.blocks),
1223
- {
1224
- kind: "tool",
1225
- tool: {
1226
- id: event.id,
1227
- name: event.name,
1228
- input: event.input,
1229
- status: "running"
2467
+ async function runAgentBatch(input) {
2468
+ const replyOptions = input.replyTo ? { replyTo: input.replyTo } : {};
2469
+ if (input.activeRuns.get(input.scope)) {
2470
+ 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", {
2471
+ ...replyOptions
2472
+ });
2473
+ return;
2474
+ }
2475
+ const requestedCwd = input.workspaces.cwdFor(input.scope) ?? input.defaultWorkspace;
2476
+ const workspace = input.workspaceManager ? await input.workspaceManager.ensure(input.scope, requestedCwd) : { cwd: requestedCwd };
2477
+ const cwd = workspace.cwd;
2478
+ const sessionId = input.sessions.resumeFor(input.scope, cwd);
2479
+ const history = input.sessions.historyFor(input.scope, cwd);
2480
+ const prompt = buildPrompt(history, input.messages);
2481
+ const runId = randomUUID4();
2482
+ const run = input.adapter.run({
2483
+ runId,
2484
+ prompt,
2485
+ cwd,
2486
+ sessionId,
2487
+ model: input.model,
2488
+ images: input.images,
2489
+ stopGraceMs: input.stopGraceMs,
2490
+ ...input.approvals ? {
2491
+ onApprovalRequest: approvalHandlerFor({
2492
+ approvals: input.approvals,
2493
+ channel: input.channel,
2494
+ chatId: input.chatId,
2495
+ scope: input.scope
2496
+ })
2497
+ } : {}
2498
+ });
2499
+ input.activeRuns.set(input.scope, { runId, stop: run.stop });
2500
+ let state = initialState;
2501
+ const stopRequested = { value: false };
2502
+ const timeoutMs = input.runPolicies?.get(input.scope) ?? input.runTimeoutMs ?? 0;
2503
+ let timedOut = false;
2504
+ let assistantOutput = "";
2505
+ const density = input.densityStore?.get(input.scope) ?? "standard";
2506
+ try {
2507
+ await input.channel.streamCard(
2508
+ input.chatId,
2509
+ renderCard(state, density),
2510
+ async (controller) => {
2511
+ const consume = async () => {
2512
+ for await (const event of run.events) {
2513
+ if (timedOut) return;
2514
+ state = applyEvent(state, event, stopRequested);
2515
+ if (event.type === "final_text") {
2516
+ assistantOutput = event.content;
2517
+ } else if (event.type === "text") {
2518
+ assistantOutput += event.delta;
1230
2519
  }
1231
- }
1232
- ],
1233
- reasoning: { ...state.reasoning, active: false },
1234
- footer: "tool_running"
1235
- };
1236
- case "tool_result":
1237
- return {
1238
- ...state,
1239
- blocks: state.blocks.map((block) => {
1240
- if (block.kind !== "tool" || block.tool.id !== event.id) return block;
1241
- return {
1242
- ...block,
1243
- tool: {
1244
- ...block.tool,
1245
- status: event.isError ? "error" : "done",
1246
- output: event.output
2520
+ if (event.type === "system" && event.sessionId) {
2521
+ input.sessions.set(input.scope, event.sessionId, event.cwd ?? cwd);
1247
2522
  }
1248
- };
1249
- })
1250
- };
1251
- case "error":
1252
- return {
1253
- ...state,
1254
- terminal: event.terminationReason === "interrupted" ? "interrupted" : event.terminationReason === "timeout" ? "idle_timeout" : "error",
1255
- errorMsg: event.terminationReason === "failed" ? event.message : void 0,
1256
- idleTimeoutMinutes: void 0,
1257
- footer: null
1258
- };
1259
- case "done":
1260
- return {
1261
- ...state,
1262
- blocks: closeStreamingText(state.blocks),
1263
- reasoning: { ...state.reasoning, active: false },
1264
- terminal: event.terminationReason === "interrupted" ? "interrupted" : event.terminationReason === "timeout" ? "idle_timeout" : "done",
1265
- footer: null
1266
- };
1267
- default:
1268
- return state;
2523
+ await controller.update(renderCard(state, density));
2524
+ }
2525
+ };
2526
+ let timeoutTimer;
2527
+ const timeoutPromise = timeoutMs > 0 ? new Promise((resolve5) => {
2528
+ timeoutTimer = setTimeout(() => {
2529
+ timedOut = true;
2530
+ void run.stop();
2531
+ resolve5();
2532
+ }, timeoutMs);
2533
+ }) : void 0;
2534
+ try {
2535
+ if (timeoutPromise) {
2536
+ await Promise.race([consume(), timeoutPromise]);
2537
+ } else {
2538
+ await consume();
2539
+ }
2540
+ state = timedOut ? markIdleTimeout(state, timeoutMs / 6e4) : finalizeIfRunning(state);
2541
+ await controller.update(renderCard(state, density));
2542
+ } finally {
2543
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
2544
+ }
2545
+ },
2546
+ replyOptions
2547
+ );
2548
+ input.sessions.recordExchange(input.scope, cwd, input.messages, assistantOutput);
2549
+ } catch (error) {
2550
+ log.fail("run-flow", error, { scope: input.scope, runId });
2551
+ state = markInterrupted(state);
2552
+ try {
2553
+ await input.channel.sendMarkdown(input.chatId, `\u26A0\uFE0F agent \u8FD0\u884C\u5931\u8D25\uFF1A${errorMessage(error)}`, {
2554
+ ...replyOptions
2555
+ });
2556
+ } catch {
2557
+ }
2558
+ } finally {
2559
+ input.activeRuns.delete(input.scope);
2560
+ if (input.approvals) {
2561
+ input.approvals.settleAll(input.scope, "cancelled");
2562
+ }
2563
+ if (input.questions) {
2564
+ input.questions.settleAll(input.scope);
2565
+ }
1269
2566
  }
1270
2567
  }
1271
- function markInterrupted(state) {
1272
- return {
1273
- ...state,
1274
- blocks: closeStreamingText(state.blocks),
1275
- reasoning: { ...state.reasoning, active: false },
1276
- terminal: "interrupted",
1277
- footer: null
2568
+ function approvalHandlerFor(input) {
2569
+ return async (request) => {
2570
+ if (!input.approvals || !input.channel.sendCard) return "cancelled";
2571
+ const promise = input.approvals.register(input.scope, request);
2572
+ try {
2573
+ await input.channel.sendCard(
2574
+ input.chatId,
2575
+ renderApprovalCard({
2576
+ id: request.id,
2577
+ toolName: request.toolName,
2578
+ reason: request.reason,
2579
+ options: request.options
2580
+ })
2581
+ );
2582
+ } catch (error) {
2583
+ log.fail("approval-card", error, { scope: input.scope });
2584
+ input.approvals.settleAll(input.scope, "cancelled");
2585
+ return "cancelled";
2586
+ }
2587
+ return promise;
1278
2588
  };
1279
2589
  }
1280
- function markIdleTimeout(state, minutes) {
1281
- return {
1282
- ...state,
1283
- blocks: closeStreamingText(state.blocks),
1284
- reasoning: { ...state.reasoning, active: false },
1285
- terminal: "idle_timeout",
1286
- footer: null,
1287
- idleTimeoutMinutes: minutes
2590
+ function questionHandlerFor(input) {
2591
+ return async (question) => {
2592
+ if (!input.questions || !input.channel.sendCard) return void 0;
2593
+ const { id, promise } = input.questions.register(input.scope, {
2594
+ kind: question.kind,
2595
+ question: question.question,
2596
+ ...question.options === void 0 ? {} : { options: question.options },
2597
+ ...question.placeholder === void 0 ? {} : { placeholder: question.placeholder }
2598
+ });
2599
+ try {
2600
+ await input.channel.sendCard(input.chatId, renderQuestionCard({ ...question, id }));
2601
+ } catch (error) {
2602
+ log.fail("question-card", error, { scope: input.scope });
2603
+ input.questions.settleAll(input.scope);
2604
+ return void 0;
2605
+ }
2606
+ return promise;
1288
2607
  };
1289
2608
  }
1290
- function finalizeIfRunning(state) {
1291
- if (state.terminal !== "running") return state;
1292
- return {
1293
- ...state,
1294
- blocks: closeStreamingText(state.blocks),
1295
- reasoning: { ...state.reasoning, active: false },
1296
- terminal: "done",
1297
- footer: null
1298
- };
2609
+ function buildPrompt(history, messages) {
2610
+ if (history.length === 0) return messages.join("\n\n");
2611
+ const transcript = history.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.content}`).join("\n\n");
2612
+ return [
2613
+ "Continue the conversation using the history below.",
2614
+ "",
2615
+ transcript,
2616
+ "",
2617
+ `Current user message:
2618
+ ${messages.join("\n\n")}`
2619
+ ].join("\n");
2620
+ }
2621
+ function applyEvent(state, event, stopRequested) {
2622
+ if (event.type === "done" && event.terminationReason === "interrupted") {
2623
+ stopRequested.value = true;
2624
+ return markInterrupted(state);
2625
+ }
2626
+ if (event.type === "error" && event.terminationReason === "timeout") {
2627
+ return markIdleTimeout(state, 0);
2628
+ }
2629
+ return reduce(state, event);
2630
+ }
2631
+ function errorMessage(error) {
2632
+ return error instanceof Error ? error.message : String(error);
2633
+ }
2634
+
2635
+ // src/commands/index.ts
2636
+ var HELP = [
2637
+ "**dsh-lark-bot \u547D\u4EE4**",
2638
+ "",
2639
+ "- `/new` `/reset` \u2014 \u5F00\u59CB\u65B0\u4F1A\u8BDD",
2640
+ "- `/cd <path>` \u2014 \u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\u5E76\u91CD\u7F6E\u4F1A\u8BDD",
2641
+ "- `/ws list|save <name>|use <name>|remove <name>` \u2014 \u7BA1\u7406\u5DE5\u4F5C\u7A7A\u95F4",
2642
+ "- `/status` \u2014 \u67E5\u770B\u5F53\u524D\u72B6\u6001",
2643
+ "- `/resume` \u2014 \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u6700\u8FD1\u4E0A\u4E0B\u6587",
2644
+ "- `/stop` \u2014 \u7EC8\u6B62\u5F53\u524D\u4EFB\u52A1",
2645
+ "- `/timeout [N|off|default]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6",
2646
+ "- `/density [compact|standard|detailed]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5361\u7247\u5BC6\u5EA6",
2647
+ "- `/ask <\u95EE\u9898>` \u2014 \u53D1\u9001\u7ED3\u6784\u5316\u95EE\u7B54\u5361\uFF08\u56DE\u7B54\u5C06\u8BB0\u5165\u4F1A\u8BDD\uFF09",
2648
+ "- `/invite user|admin|group <id>` \u2014 \u7BA1\u7406\u8BBF\u95EE\u767D\u540D\u5355",
2649
+ "- `/help` \u2014 \u663E\u793A\u672C\u5E2E\u52A9"
2650
+ ].join("\n");
2651
+ async function reply(ctx, markdown2) {
2652
+ await ctx.channel.sendMarkdown(ctx.chatId, markdown2, {
2653
+ replyTo: ctx.messageId
2654
+ });
2655
+ }
2656
+ async function handleNew(_args, ctx) {
2657
+ const wasRunning = await ctx.activeRuns.interrupt(ctx.scope);
2658
+ ctx.sessions.clear(ctx.scope);
2659
+ 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");
1299
2660
  }
1300
-
1301
- // src/card/run-renderer.ts
1302
- function markdown(content) {
1303
- return { tag: "markdown", content };
2661
+ async function handleCd(args, ctx) {
2662
+ const path = args.trim();
2663
+ if (!path) {
2664
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`");
2665
+ return;
2666
+ }
2667
+ const cwd = resolve4(path);
2668
+ await ctx.activeRuns.interrupt(ctx.scope);
2669
+ ctx.workspaces.setCwd(ctx.scope, cwd);
2670
+ ctx.sessions.clear(ctx.scope);
2671
+ await reply(ctx, `\u5DF2\u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\uFF1A\`${cwd}\`\uFF0C\u4F1A\u8BDD\u5DF2\u91CD\u7F6E\u3002`);
1304
2672
  }
1305
- function noteMd(content) {
1306
- return { tag: "markdown", content, text_size: "notation" };
2673
+ async function handleWs(args, ctx) {
2674
+ const [sub, ...rest] = args.trim().split(/\s+/);
2675
+ const name = rest.join(" ").trim();
2676
+ if (!sub || sub === "list") {
2677
+ const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
2678
+ const named = ctx.workspaces.listNamed();
2679
+ const index = ctx.workspaces.listIndex();
2680
+ if (ctx.channel.sendCard) {
2681
+ await ctx.channel.sendCard(ctx.chatId, renderWorkspaceCard({ current, index }));
2682
+ return;
2683
+ }
2684
+ const lines = Object.entries(named).map(
2685
+ ([key, value]) => `- **${key}** \u2192 \`${value}\`${value === current ? " \u2190 \u5F53\u524D" : ""}`
2686
+ );
2687
+ await reply(
2688
+ ctx,
2689
+ [
2690
+ `\u5F53\u524D cwd\uFF1A\`${current}\``,
2691
+ "",
2692
+ ...lines.length > 0 ? lines : ["\u6682\u65E0\u547D\u540D\u5DE5\u4F5C\u7A7A\u95F4\u3002"]
2693
+ ].join("\n")
2694
+ );
2695
+ return;
2696
+ }
2697
+ if (sub === "save") {
2698
+ if (!name) {
2699
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/ws save <name>`");
2700
+ return;
2701
+ }
2702
+ const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
2703
+ ctx.workspaces.saveNamed(name, current);
2704
+ await reply(ctx, `\u5DF2\u4FDD\u5B58\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${current}\``);
2705
+ return;
2706
+ }
2707
+ if (sub === "use") {
2708
+ if (!name) {
2709
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/ws use <name>`");
2710
+ return;
2711
+ }
2712
+ const cwd = ctx.workspaces.getNamed(name);
2713
+ if (!cwd) {
2714
+ await reply(ctx, `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`);
2715
+ return;
2716
+ }
2717
+ await ctx.activeRuns.interrupt(ctx.scope);
2718
+ ctx.workspaces.setCwd(ctx.scope, cwd);
2719
+ ctx.workspaces.touchNamed(name);
2720
+ ctx.sessions.clear(ctx.scope);
2721
+ await reply(ctx, `\u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${cwd}\``);
2722
+ return;
2723
+ }
2724
+ if (sub === "remove") {
2725
+ if (!name) {
2726
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/ws remove <name>`");
2727
+ return;
2728
+ }
2729
+ const removed = ctx.workspaces.removeNamed(name);
2730
+ await reply(ctx, removed ? `\u5DF2\u5220\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**` : `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`);
2731
+ return;
2732
+ }
2733
+ await reply(ctx, "\u672A\u77E5 `/ws` \u5B50\u547D\u4EE4\uFF0C\u8BF7\u4F7F\u7528 list / save / use / remove\u3002");
1307
2734
  }
1308
- function footerStatus(status) {
1309
- 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";
1310
- return noteMd(text);
2735
+ async function handleStatus(_args, ctx) {
2736
+ const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
2737
+ const session = ctx.sessions.getRaw(ctx.scope)?.sessionId ?? "(\u65E0)";
2738
+ const running = Boolean(ctx.activeRuns.get(ctx.scope));
2739
+ const scopeLabel = ctx.chatMode === "topic" ? `${ctx.scope}\uFF08\u8BDD\u9898\u72EC\u7ACB session\uFF09` : ctx.scope;
2740
+ await reply(
2741
+ ctx,
2742
+ [
2743
+ `\u{1F9ED} **scope**: \`${scopeLabel}\``,
2744
+ `\u{1F4C1} **cwd**: \`${cwd}\``,
2745
+ `\u{1F517} **session**: \`${session}\``,
2746
+ `\u{1F3C3} **active run**: ${running ? "yes" : "no"}`
2747
+ ].join("\n")
2748
+ );
1311
2749
  }
1312
- function summaryText(state) {
1313
- if (state.terminal === "interrupted") return "\u5DF2\u4E2D\u65AD";
1314
- if (state.terminal === "idle_timeout") return "\u5DF2\u8D85\u65F6";
1315
- if (state.terminal === "error") return "\u51FA\u9519";
1316
- if (state.terminal === "done") return "\u5DF2\u5B8C\u6210";
1317
- if (state.footer === "tool_running") return "\u6B63\u5728\u8C03\u7528\u5DE5\u5177";
1318
- if (state.footer === "streaming") return "\u6B63\u5728\u8F93\u51FA";
1319
- return "\u601D\u8003\u4E2D";
2750
+ async function handleResume(_args, ctx) {
2751
+ const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
2752
+ const history = ctx.sessions.historyFor(ctx.scope, cwd);
2753
+ if (history.length === 0) {
2754
+ await reply(ctx, "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u5386\u53F2\u4E0A\u4E0B\u6587\u3002");
2755
+ return;
2756
+ }
2757
+ const recent = history.slice(-6).map((message) => {
2758
+ const speaker = message.role === "user" ? "\u{1F464}" : "\u{1F916}";
2759
+ return `${speaker} ${message.content.slice(0, 300)}`;
2760
+ });
2761
+ await reply(ctx, [`\u5F53\u524D scope\uFF1A\`${ctx.scope}\``, "", ...recent].join("\n"));
1320
2762
  }
1321
- function stopButton() {
1322
- return {
1323
- tag: "button",
1324
- text: { tag: "plain_text", content: "\u23F9 \u7EC8\u6B62" },
1325
- type: "danger",
1326
- value: { cmd: "stop" }
1327
- };
2763
+ async function handleStop(_args, ctx) {
2764
+ const stopped = await ctx.activeRuns.interrupt(ctx.scope);
2765
+ 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");
1328
2766
  }
1329
- function textBlock(block) {
1330
- return markdown(block.content);
2767
+ async function handleTimeout(args, ctx) {
2768
+ const input = args.trim();
2769
+ const effectiveMs = ctx.runPolicies.get(ctx.scope) ?? ctx.defaultRunTimeoutMs;
2770
+ if (!input) {
2771
+ const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
2772
+ await reply(
2773
+ ctx,
2774
+ 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"
2775
+ );
2776
+ return;
2777
+ }
2778
+ if (input === "off") {
2779
+ ctx.runPolicies.set(ctx.scope, 0);
2780
+ await reply(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\u3002");
2781
+ return;
2782
+ }
2783
+ if (input === "default") {
2784
+ ctx.runPolicies.clear(ctx.scope);
2785
+ await reply(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u8FD0\u884C\u8D85\u65F6\u3002");
2786
+ return;
2787
+ }
2788
+ const minutes = Number(input);
2789
+ if (!Number.isInteger(minutes) || minutes <= 0) {
2790
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/timeout <N|off|default>`\uFF0CN \u4E3A\u5927\u4E8E 0 \u7684\u5206\u949F\u6570\u3002");
2791
+ return;
2792
+ }
2793
+ ctx.runPolicies.set(ctx.scope, minutes * 6e4);
2794
+ await reply(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A${minutes} \u5206\u949F\u3002`);
1331
2795
  }
1332
- function toolBlock(tool) {
1333
- const icon = tool.status === "error" ? "\u26A0\uFE0F" : tool.status === "done" ? "\u2705" : "\u23F3";
1334
- return markdown(`${icon} **${tool.name}**`);
2796
+ async function handleDensity(args, ctx) {
2797
+ const input = args.trim().toLowerCase();
2798
+ if (!input) {
2799
+ const current = ctx.densityStore?.get(ctx.scope) ?? "standard";
2800
+ await reply(
2801
+ ctx,
2802
+ `\u5F53\u524D\u5361\u7247\u5BC6\u5EA6\uFF1A**${current}**\u3002\u53EF\u7528 \`/density compact|standard|detailed\` \u8C03\u6574\u3002`
2803
+ );
2804
+ return;
2805
+ }
2806
+ if (input === "default") {
2807
+ ctx.densityStore?.clear(ctx.scope);
2808
+ await reply(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u5361\u7247\u5BC6\u5EA6\u3002");
2809
+ return;
2810
+ }
2811
+ const density = parseCardDensity(input);
2812
+ if (!density) {
2813
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/density [compact|standard|detailed|default]`");
2814
+ return;
2815
+ }
2816
+ ctx.densityStore?.set(ctx.scope, density);
2817
+ await reply(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u5361\u7247\u5BC6\u5EA6\uFF1A**${density}**\u3002`);
1335
2818
  }
1336
- function renderCard(state) {
1337
- const elements = [];
1338
- if (state.reasoning.content) {
1339
- elements.push(
1340
- noteMd(
1341
- state.reasoning.active ? "\u{1F9E0} \u6B63\u5728\u601D\u8003\u2026" : `\u{1F9E0} \u601D\u8003\u5B8C\u6210\uFF1A${state.reasoning.content.slice(0, 300)}`
1342
- )
2819
+ async function handleAsk(args, ctx) {
2820
+ const question = args.trim();
2821
+ if (!question) {
2822
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/ask <\u95EE\u9898>`");
2823
+ return;
2824
+ }
2825
+ if (!ctx.questions) {
2826
+ await reply(ctx, "\u95EE\u7B54\u5361\u672A\u542F\u7528\uFF08\u8BF7\u786E\u8BA4 questions \u5DF2\u63A5\u7EBF\uFF09\u3002");
2827
+ return;
2828
+ }
2829
+ const answer = await questionHandlerFor({
2830
+ questions: ctx.questions,
2831
+ channel: ctx.channel,
2832
+ chatId: ctx.chatId,
2833
+ scope: ctx.scope
2834
+ })({
2835
+ kind: "text",
2836
+ question,
2837
+ id: ""
2838
+ });
2839
+ if (answer !== void 0) {
2840
+ const text = Array.isArray(answer) ? answer.join("\u3001") : answer;
2841
+ ctx.sessions.recordExchange(ctx.scope, ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace, [text], void 0);
2842
+ await reply(ctx, `\u5DF2\u8BB0\u5F55\u4F60\u7684\u56DE\u7B54\uFF0C\u5E76\u5199\u5165\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002`);
2843
+ } else {
2844
+ await reply(ctx, "\u672A\u6536\u5230\u56DE\u7B54\uFF08\u5361\u7247\u53EF\u80FD\u5DF2\u8D85\u65F6\u6216\u88AB\u5FFD\u7565\uFF09\u3002");
2845
+ }
2846
+ }
2847
+ async function handleInvite(args, ctx) {
2848
+ const [kind, ...rest] = args.trim().split(/\s+/);
2849
+ const id = rest.join(" ").trim();
2850
+ if (kind === "list") {
2851
+ const snapshot = ctx.accessManager.snapshot();
2852
+ await reply(
2853
+ ctx,
2854
+ [
2855
+ "**\u8BBF\u95EE\u767D\u540D\u5355**",
2856
+ `users: ${snapshot.allowedUsers.join(", ") || "(\u7A7A)"}`,
2857
+ `chats: ${snapshot.allowedChats.join(", ") || "(\u7A7A)"}`,
2858
+ `admins: ${snapshot.admins.join(", ") || "(\u7A7A)"}`
2859
+ ].join("\n")
1343
2860
  );
2861
+ return;
1344
2862
  }
1345
- for (const block of state.blocks) {
1346
- elements.push(block.kind === "text" ? textBlock(block) : toolBlock(block.tool));
2863
+ if (!kind || !id) {
2864
+ await reply(
2865
+ ctx,
2866
+ "\u7528\u6CD5\uFF1A`/invite user|admin|group <id>`\u3001`/invite list`\u3001`/invite remove user|group <id>`"
2867
+ );
2868
+ return;
1347
2869
  }
1348
- if (state.terminal === "interrupted") {
1349
- elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
1350
- } else if (state.terminal === "idle_timeout") {
1351
- elements.push(noteMd(`_\u23F1 ${state.idleTimeoutMinutes ?? 0} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
1352
- } else if (state.terminal === "error" && state.errorMsg) {
1353
- elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
1354
- } else if (state.terminal === "done" && elements.length === 0) {
1355
- elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
2870
+ if (kind === "user") {
2871
+ await ctx.accessManager.addUser(id);
2872
+ await reply(ctx, `\u5DF2\u5141\u8BB8\u7528\u6237\uFF1A\`${id}\``);
2873
+ return;
1356
2874
  }
1357
- if (state.terminal === "running") {
1358
- if (state.footer) elements.push(footerStatus(state.footer));
1359
- elements.push(stopButton());
2875
+ if (kind === "admin") {
2876
+ await ctx.accessManager.addAdmin(id);
2877
+ await reply(ctx, `\u5DF2\u8BBE\u4E3A\u7BA1\u7406\u5458\uFF1A\`${id}\``);
2878
+ return;
2879
+ }
2880
+ if (kind === "group") {
2881
+ await ctx.accessManager.addChat(id);
2882
+ await reply(ctx, `\u5DF2\u5141\u8BB8\u7FA4\u804A\uFF1A\`${id}\``);
2883
+ return;
2884
+ }
2885
+ if (kind === "remove") {
2886
+ const [sub, target] = rest;
2887
+ if (sub === "user" && target) {
2888
+ await ctx.accessManager.removeUser(target);
2889
+ await reply(ctx, `\u5DF2\u79FB\u9664\u7528\u6237\uFF1A\`${target}\``);
2890
+ return;
2891
+ }
2892
+ if (sub === "group" && target) {
2893
+ await ctx.accessManager.removeChat(target);
2894
+ await reply(ctx, `\u5DF2\u79FB\u9664\u7FA4\u804A\uFF1A\`${target}\``);
2895
+ return;
2896
+ }
2897
+ await reply(ctx, "\u7528\u6CD5\uFF1A`/invite remove user <id>` \u6216 `/invite remove group <chatId>`");
2898
+ return;
1360
2899
  }
2900
+ await reply(ctx, "\u672A\u77E5 `/invite` \u7C7B\u578B\uFF0C\u8BF7\u4F7F\u7528 user / admin / group / list / remove\u3002");
2901
+ }
2902
+ async function handleHelp(_args, ctx) {
2903
+ await reply(ctx, HELP);
2904
+ }
2905
+ var handlers = {
2906
+ "/new": handleNew,
2907
+ "/reset": handleNew,
2908
+ "/cd": handleCd,
2909
+ "/ws": handleWs,
2910
+ "/status": handleStatus,
2911
+ "/resume": handleResume,
2912
+ "/stop": handleStop,
2913
+ "/timeout": handleTimeout,
2914
+ "/density": handleDensity,
2915
+ "/ask": handleAsk,
2916
+ "/invite": handleInvite,
2917
+ "/help": handleHelp
2918
+ };
2919
+ async function tryHandleCommand(text, ctx) {
2920
+ const trimmed = text.trim();
2921
+ if (!trimmed.startsWith("/")) return false;
2922
+ const [command, ...rest] = trimmed.split(/\s+/);
2923
+ const handler = handlers[command ?? ""];
2924
+ if (!handler) return false;
2925
+ await handler(rest.join(" "), ctx);
2926
+ return true;
2927
+ }
2928
+
2929
+ // src/bridge/lark-channel.ts
2930
+ function adaptLarkChannel(channel) {
2931
+ const base = {
2932
+ async sendMarkdown(chatId, markdown2, options) {
2933
+ const sendOptions = {};
2934
+ if (options?.replyTo) sendOptions.replyTo = options.replyTo;
2935
+ await channel.send(chatId, { markdown: markdown2 }, sendOptions);
2936
+ }
2937
+ };
1361
2938
  return {
1362
- schema: "2.0",
1363
- config: {
1364
- streaming_mode: state.terminal === "running",
1365
- summary: { content: summaryText(state) }
2939
+ ...base,
2940
+ async sendCard(chatId, card) {
2941
+ await channel.send(chatId, { card }, {});
1366
2942
  },
1367
- body: { elements }
2943
+ async streamCard(chatId, initial, producer, options) {
2944
+ const sendOptions = {};
2945
+ if (options?.replyTo) sendOptions.replyTo = options.replyTo;
2946
+ await channel.stream(
2947
+ chatId,
2948
+ {
2949
+ card: {
2950
+ initial,
2951
+ producer: async (controller) => {
2952
+ await producer(controller);
2953
+ }
2954
+ }
2955
+ },
2956
+ sendOptions
2957
+ );
2958
+ }
1368
2959
  };
1369
2960
  }
1370
2961
 
1371
- // src/bridge/run-flow.ts
1372
- async function runAgentBatch(input) {
1373
- const replyOptions = input.replyTo ? { replyTo: input.replyTo } : {};
1374
- if (input.activeRuns.get(input.scope)) {
1375
- 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", {
1376
- ...replyOptions
1377
- });
1378
- return;
1379
- }
1380
- const requestedCwd = input.workspaces.cwdFor(input.scope) ?? input.defaultWorkspace;
1381
- const workspace = input.workspaceManager ? await input.workspaceManager.ensure(input.scope, requestedCwd) : { cwd: requestedCwd };
1382
- const cwd = workspace.cwd;
1383
- const sessionId = input.sessions.resumeFor(input.scope, cwd);
1384
- const history = input.sessions.historyFor(input.scope, cwd);
1385
- const prompt = buildPrompt(history, input.messages);
1386
- const runId = randomUUID();
1387
- const run = input.adapter.run({
1388
- runId,
1389
- prompt,
1390
- cwd,
1391
- sessionId,
1392
- model: input.model,
1393
- images: input.images,
1394
- stopGraceMs: input.stopGraceMs
2962
+ // src/bridge/channel.ts
2963
+ async function startChannel(deps) {
2964
+ const channel = (deps.createChannel ?? createLarkChannel)({
2965
+ appId: deps.appId,
2966
+ appSecret: deps.appSecret,
2967
+ domain: deps.tenant === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn",
2968
+ source: "dsh-lark-bot",
2969
+ policy: {
2970
+ dmMode: deps.allowedUsers?.length ? "allowlist" : deps.accessDefaultDeny === true ? "disabled" : "open",
2971
+ requireMention: true,
2972
+ respondToMentionAll: false,
2973
+ ...deps.allowedUsers ? { dmAllowlist: deps.allowedUsers } : {},
2974
+ ...deps.allowedChats ? { groupAllowlist: deps.allowedChats } : {}
2975
+ },
2976
+ safety: {
2977
+ chatQueue: { enabled: false }
2978
+ },
2979
+ outbound: {
2980
+ streamThrottleMs: 400
2981
+ },
2982
+ includeRawEvent: true,
2983
+ resolveChatMode: true,
2984
+ handshakeTimeoutMs: 8e3,
2985
+ httpTimeoutMs: 3e4,
2986
+ respectProxyEnv: true
1395
2987
  });
1396
- input.activeRuns.set(input.scope, { runId, stop: run.stop });
1397
- let state = initialState;
1398
- const stopRequested = { value: false };
1399
- const timeoutMs = input.runPolicies?.get(input.scope) ?? input.runTimeoutMs ?? 0;
1400
- let timedOut = false;
1401
- let assistantOutput = "";
1402
- try {
1403
- await input.channel.streamCard(
1404
- input.chatId,
1405
- renderCard(state),
1406
- async (controller) => {
1407
- const consume = async () => {
1408
- for await (const event of run.events) {
1409
- if (timedOut) return;
1410
- state = applyEvent(state, event, stopRequested);
1411
- if (event.type === "final_text") {
1412
- assistantOutput = event.content;
1413
- } else if (event.type === "text") {
1414
- assistantOutput += event.delta;
1415
- }
1416
- if (event.type === "system" && event.sessionId) {
1417
- input.sessions.set(input.scope, event.sessionId, event.cwd ?? cwd);
1418
- }
1419
- await controller.update(renderCard(state));
1420
- }
1421
- };
1422
- let timeoutTimer;
1423
- const timeoutPromise = timeoutMs > 0 ? new Promise((resolve4) => {
1424
- timeoutTimer = setTimeout(() => {
1425
- timedOut = true;
1426
- void run.stop();
1427
- resolve4();
1428
- }, timeoutMs);
1429
- }) : void 0;
1430
- try {
1431
- if (timeoutPromise) {
1432
- await Promise.race([consume(), timeoutPromise]);
1433
- } else {
1434
- await consume();
1435
- }
1436
- state = timedOut ? markIdleTimeout(state, timeoutMs / 6e4) : finalizeIfRunning(state);
1437
- await controller.update(renderCard(state));
1438
- } finally {
1439
- if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
1440
- }
1441
- },
1442
- replyOptions
1443
- );
1444
- input.sessions.recordExchange(input.scope, cwd, input.messages, assistantOutput);
1445
- } catch (error) {
1446
- log.fail("run-flow", error, { scope: input.scope, runId });
1447
- state = markInterrupted(state);
1448
- try {
1449
- await input.channel.sendMarkdown(input.chatId, `\u26A0\uFE0F agent \u8FD0\u884C\u5931\u8D25\uFF1A${errorMessage(error)}`, {
1450
- ...replyOptions
2988
+ const streaming = adaptLarkChannel(channel);
2989
+ const commandChannel = streaming;
2990
+ channel.on({
2991
+ message: async (msg) => {
2992
+ const scope = scopeForMessage(msg);
2993
+ if (deps.eventFreshnessMs !== void 0 && deps.eventFreshnessMs > 0 && !isEventFresh(msg.createTime, deps.eventFreshnessMs)) {
2994
+ log.warn("channel", "stale-message-dropped", {
2995
+ scope,
2996
+ ageMs: Date.now() - msg.createTime
2997
+ });
2998
+ return;
2999
+ }
3000
+ const context = {
3001
+ scope,
3002
+ chatId: msg.chatId,
3003
+ messageId: msg.messageId,
3004
+ threadId: msg.threadId,
3005
+ chatMode: msg.chatMode ?? msg.chatType,
3006
+ sessions: deps.sessions,
3007
+ workspaces: deps.workspaces,
3008
+ activeRuns: deps.activeRuns,
3009
+ runPolicies: deps.runPolicies,
3010
+ defaultRunTimeoutMs: deps.defaultRunTimeoutMs,
3011
+ accessManager: deps.accessManager,
3012
+ approvals: deps.approvals,
3013
+ questions: deps.questions,
3014
+ densityStore: deps.densityStore,
3015
+ channel: commandChannel,
3016
+ defaultWorkspace: deps.defaultWorkspace
3017
+ };
3018
+ const handled = await tryHandleCommand(msg.content, context).catch((error) => {
3019
+ log.fail("channel-command", error, { scope });
3020
+ return false;
1451
3021
  });
1452
- } catch {
3022
+ if (!handled) deps.pending.push(scope, msg);
3023
+ },
3024
+ cardAction: async (event) => {
3025
+ const value = event.action.value && typeof event.action.value === "object" ? event.action.value : void 0;
3026
+ const scope = event.raw ? await resolveCardScope(event.chatId, event.raw) : event.chatId;
3027
+ if (value?.cmd === "stop") {
3028
+ await deps.activeRuns.interrupt(scope);
3029
+ return;
3030
+ }
3031
+ if (value?.cmd === "approve" && typeof value.id === "string" && deps.approvals) {
3032
+ const outcome = value.outcome === "allow" ? "allowed-once" : "rejected";
3033
+ deps.approvals.resolve(scope, value.id, outcome);
3034
+ return;
3035
+ }
3036
+ if (value?.cmd === "question-submit" && typeof value.id === "string" && deps.questions) {
3037
+ const question = deps.questions.get(scope, value.id);
3038
+ const form = event.action.formValue;
3039
+ if (question) {
3040
+ const answer = extractQuestionAnswer(
3041
+ question.kind,
3042
+ form?.answer,
3043
+ question.options
3044
+ );
3045
+ deps.questions.resolve(scope, value.id, answer);
3046
+ }
3047
+ }
3048
+ },
3049
+ reconnecting: () => {
3050
+ log.warn("channel", "reconnecting", {});
3051
+ },
3052
+ reconnected: () => {
3053
+ log.info("channel", "reconnected", {});
3054
+ },
3055
+ error: (error) => {
3056
+ log.fail("channel", error);
1453
3057
  }
1454
- } finally {
1455
- input.activeRuns.delete(input.scope);
1456
- }
1457
- }
1458
- function buildPrompt(history, messages) {
1459
- if (history.length === 0) return messages.join("\n\n");
1460
- const transcript = history.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.content}`).join("\n\n");
1461
- return [
1462
- "Continue the conversation using the history below.",
1463
- "",
1464
- transcript,
1465
- "",
1466
- `Current user message:
1467
- ${messages.join("\n\n")}`
1468
- ].join("\n");
3058
+ });
3059
+ await channel.connect();
3060
+ return {
3061
+ channel,
3062
+ disconnect: () => channel.disconnect()
3063
+ };
1469
3064
  }
1470
- function applyEvent(state, event, stopRequested) {
1471
- if (event.type === "done" && event.terminationReason === "interrupted") {
1472
- stopRequested.value = true;
1473
- return markInterrupted(state);
1474
- }
1475
- if (event.type === "error" && event.terminationReason === "timeout") {
1476
- return markIdleTimeout(state, 0);
1477
- }
1478
- return reduce(state, event);
3065
+ function scopeForMessage(msg) {
3066
+ if (msg.chatMode === "topic" && msg.threadId) return `${msg.chatId}:${msg.threadId}`;
3067
+ return msg.chatId;
1479
3068
  }
1480
- function errorMessage(error) {
1481
- return error instanceof Error ? error.message : String(error);
3069
+ async function resolveCardScope(chatId, raw) {
3070
+ return raw.message?.thread_id ? `${chatId}:${raw.message.thread_id}` : chatId;
1482
3071
  }
1483
3072
 
1484
3073
  // src/config/access-manager.ts
@@ -1587,15 +3176,21 @@ async function onboardPersonalAgent(deps = {}) {
1587
3176
  }
1588
3177
 
1589
3178
  // src/media/attachments.ts
1590
- import { mkdir as mkdir3, readFile as readFile2, stat as stat2 } from "fs/promises";
1591
- import { join as join5 } from "path";
3179
+ import { mkdir as mkdir5, readFile as readFile2, stat as stat2 } from "fs/promises";
3180
+ import { join as join7 } from "path";
1592
3181
  var MAX_TEXT_FILE_BYTES = 256e3;
3182
+ function assertSafeMediaName(mediaDir, destination) {
3183
+ if (!isPathWithin(mediaDir, destination)) {
3184
+ throw new Error(`unsafe attachment destination rejected: ${destination}`);
3185
+ }
3186
+ }
1593
3187
  async function prepareAttachments(channel, message, mediaDir) {
1594
- await mkdir3(mediaDir, { recursive: true });
3188
+ await mkdir5(mediaDir, { recursive: true });
1595
3189
  const result = { imagePaths: [], textFileNotes: [] };
1596
3190
  for (const resource of message.resources) {
1597
3191
  if (!channel || resource.type !== "image" && resource.type !== "file") continue;
1598
- const destination = join5(mediaDir, `${message.messageId}-${resource.fileKey}`);
3192
+ const destination = join7(mediaDir, `${message.messageId}-${resource.fileKey}`);
3193
+ assertSafeMediaName(mediaDir, destination);
1599
3194
  await channel.downloadResourceToFile(
1600
3195
  message.messageId,
1601
3196
  resource.fileKey,
@@ -1612,9 +3207,10 @@ async function prepareAttachments(channel, message, mediaDir) {
1612
3207
  continue;
1613
3208
  }
1614
3209
  const content = await readFile2(destination, "utf8");
3210
+ const safeContent = truncateUtf8Safe(content, MAX_TEXT_FILE_BYTES);
1615
3211
  result.textFileNotes.push(
1616
3212
  `[attachment: ${resource.fileName ?? resource.fileKey}]
1617
- ${content}`
3213
+ ${safeContent}`
1618
3214
  );
1619
3215
  }
1620
3216
  return result;
@@ -1735,8 +3331,8 @@ var SessionStore = class {
1735
3331
  // src/workspace/git-worktree.ts
1736
3332
  import { execFile } from "child_process";
1737
3333
  import { randomBytes as randomBytes2 } from "crypto";
1738
- import { access, copyFile, mkdir as mkdir4 } from "fs/promises";
1739
- import { dirname as dirname3, join as join6 } from "path";
3334
+ import { access, copyFile, mkdir as mkdir6 } from "fs/promises";
3335
+ import { dirname as dirname3, join as join8 } from "path";
1740
3336
  import { promisify } from "util";
1741
3337
  var execFileAsync = promisify(execFile);
1742
3338
  async function defaultRunGit(args, cwd) {
@@ -1755,11 +3351,11 @@ async function exists(path) {
1755
3351
  }
1756
3352
  }
1757
3353
  async function defaultCopyRulesFile(source, target) {
1758
- await mkdir4(dirname3(target), { recursive: true });
3354
+ await mkdir6(dirname3(target), { recursive: true });
1759
3355
  await copyFile(source, target);
1760
3356
  }
1761
3357
  function slugify(scope) {
1762
- const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
3358
+ const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[.]+/, "").replace(/^-+|-+$/g, "").slice(0, 64);
1763
3359
  return slug || "session";
1764
3360
  }
1765
3361
  var GitWorktreeManager = class {
@@ -1775,13 +3371,16 @@ var GitWorktreeManager = class {
1775
3371
  const isGit = await this.isGitRepository(base);
1776
3372
  if (!isGit) return { cwd: base, created: false };
1777
3373
  const slug = slugify(scope);
1778
- const target = join6(this.options.worktreesRoot, slug);
3374
+ const target = join8(this.options.worktreesRoot, slug);
3375
+ if (!isPathWithin(this.options.worktreesRoot, target)) {
3376
+ throw new Error(`unsafe worktree target rejected: ${target}`);
3377
+ }
1779
3378
  if (await exists(target)) {
1780
3379
  await this.ensureProjectRules(base, target);
1781
3380
  return { cwd: target, created: false };
1782
3381
  }
1783
3382
  const branch = `dsh-lark/${slug}-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`;
1784
- await mkdir4(this.options.worktreesRoot, { recursive: true });
3383
+ await mkdir6(this.options.worktreesRoot, { recursive: true });
1785
3384
  await this.runGit(["worktree", "add", "-b", branch, target, "HEAD"], base);
1786
3385
  await this.ensureProjectRules(base, target);
1787
3386
  return { cwd: target, created: true, branch };
@@ -1795,9 +3394,9 @@ var GitWorktreeManager = class {
1795
3394
  }
1796
3395
  }
1797
3396
  async ensureProjectRules(base, target) {
1798
- const destination = join6(target, "AGENTS.md");
3397
+ const destination = join8(target, "AGENTS.md");
1799
3398
  if (await exists(destination)) return;
1800
- const sourceCandidates = [join6(base, ".dsh-lark", "AGENTS.md"), join6(base, "AGENTS.md")];
3399
+ const sourceCandidates = [join8(base, ".dsh-lark", "AGENTS.md"), join8(base, "AGENTS.md")];
1801
3400
  for (const source of sourceCandidates) {
1802
3401
  if (await exists(source)) {
1803
3402
  await this.copyRulesFile(source, destination);
@@ -1947,24 +3546,31 @@ async function runStart(options) {
1947
3546
  return;
1948
3547
  }
1949
3548
  const defaultWorkspace = options.workspace ?? activeProfile.workspaces.default ?? env.workspace ?? paths.profilePath(profileName, "workspace");
1950
- await mkdir5(defaultWorkspace, { recursive: true });
3549
+ await mkdir7(defaultWorkspace, { recursive: true });
1951
3550
  const sessions = new SessionStore(paths.sessionsFile(profileName));
1952
3551
  const workspaces = new WorkspaceStore(paths.workspacesFile(profileName));
1953
3552
  const worktreeManager = new GitWorktreeManager({
1954
3553
  worktreesRoot: paths.profilePath(profileName, "worktrees")
1955
3554
  });
1956
3555
  await Promise.all([sessions.load(), workspaces.load()]);
1957
- const adapterOptions = {
1958
- command: env.dshCommand,
1959
- args: env.dshArgs,
1960
- stopGraceMs: env.stopGraceMs
1961
- };
1962
- if (activeProfile.preferences.stopGraceMs !== void 0) {
1963
- Object.assign(adapterOptions, { stopGraceMs: activeProfile.preferences.stopGraceMs });
3556
+ let adapter;
3557
+ try {
3558
+ adapter = await buildAgentAdapter(env, {
3559
+ stopGraceMs: activeProfile.preferences.stopGraceMs ?? env.stopGraceMs,
3560
+ model: activeProfile.preferences.model
3561
+ });
3562
+ } catch (error) {
3563
+ log.fail("adapter", error);
3564
+ process.stderr.write(`agent adapter \u521D\u59CB\u5316\u5931\u8D25\uFF1A${errorMessage2(error)}
3565
+ `);
3566
+ process.exitCode = 1;
3567
+ return;
1964
3568
  }
1965
- const adapter = new DshAdapter(adapterOptions);
1966
3569
  const activeRuns = new ActiveRuns();
1967
3570
  const runPolicies = new RunPolicyStore();
3571
+ const approvals = new ApprovalRegistry();
3572
+ const questions = new QuestionRegistry();
3573
+ const densityStore = new DensityStore();
1968
3574
  let streaming;
1969
3575
  let larkChannel;
1970
3576
  const pending = new PendingQueue(DEBOUNCE_MS, async (scope, batch) => {
@@ -1992,6 +3598,9 @@ async function runStart(options) {
1992
3598
  workspaceManager: worktreeManager,
1993
3599
  activeRuns,
1994
3600
  runPolicies,
3601
+ approvals,
3602
+ questions,
3603
+ densityStore,
1995
3604
  channel: streaming,
1996
3605
  defaultWorkspace,
1997
3606
  replyTo: first.messageId,
@@ -2016,10 +3625,15 @@ async function runStart(options) {
2016
3625
  workspaces,
2017
3626
  activeRuns,
2018
3627
  runPolicies,
3628
+ approvals,
3629
+ questions,
3630
+ densityStore,
2019
3631
  defaultRunTimeoutMs: activeProfile.preferences.runTimeoutMs ?? env.runTimeoutMs,
2020
3632
  accessManager,
2021
3633
  pending,
2022
3634
  defaultWorkspace,
3635
+ accessDefaultDeny: env.accessDefaultDeny,
3636
+ eventFreshnessMs: env.eventFreshnessMs,
2023
3637
  allowedUsers: activeProfile.access.allowedUsers,
2024
3638
  allowedChats: activeProfile.access.allowedChats
2025
3639
  };
@@ -2040,14 +3654,18 @@ async function runStart(options) {
2040
3654
  `);
2041
3655
  await waitForShutdown();
2042
3656
  await bridge.disconnect();
3657
+ await adapter.dispose?.();
2043
3658
  await Promise.all([sessions.flush(), workspaces.flush()]);
2044
3659
  }
3660
+ function errorMessage2(error) {
3661
+ return error instanceof Error ? error.message : String(error);
3662
+ }
2045
3663
  function waitForShutdown() {
2046
- return new Promise((resolve4) => {
3664
+ return new Promise((resolve5) => {
2047
3665
  const shutdown = () => {
2048
3666
  process.off("SIGINT", shutdown);
2049
3667
  process.off("SIGTERM", shutdown);
2050
- resolve4();
3668
+ resolve5();
2051
3669
  };
2052
3670
  process.once("SIGINT", shutdown);
2053
3671
  process.once("SIGTERM", shutdown);