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