dsh-lark-bot 0.2.8 → 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/README.md +19 -6
- package/SECURITY.md +42 -0
- package/dist/cli.js +2729 -1112
- package/dist/cli.js.map +1 -1
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -1,111 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
85
|
-
|
|
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) =>
|
|
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 =
|
|
73
|
+
const dshHome = resolveDshHome(home, env);
|
|
98
74
|
const candidates = [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 [
|
|
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 =
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
169
|
-
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
created: false,
|
|
236
|
+
error: error instanceof Error ? error.message : String(error)
|
|
237
|
+
};
|
|
224
238
|
}
|
|
225
239
|
}
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
311
|
+
default:
|
|
312
|
+
return [];
|
|
271
313
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
314
|
+
}
|
|
315
|
+
function mapApprovalOutcome(outcome, options) {
|
|
316
|
+
if (outcome === "cancelled") {
|
|
317
|
+
return { outcome: { outcome: "cancelled" } };
|
|
275
318
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
393
|
-
import {
|
|
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((
|
|
846
|
+
return new Promise((resolve5) => {
|
|
602
847
|
const onExit = (code) => {
|
|
603
848
|
if (timer !== void 0) clearTimeout(timer);
|
|
604
|
-
|
|
849
|
+
resolve5({ code, timedOut: false });
|
|
605
850
|
};
|
|
606
851
|
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
607
852
|
child.removeListener("exit", onExit);
|
|
608
|
-
|
|
853
|
+
resolve5({ code: child.exitCode, timedOut: true });
|
|
609
854
|
}, timeoutMs) : void 0;
|
|
610
855
|
child.once("exit", onExit);
|
|
611
856
|
});
|
|
@@ -622,864 +867,2207 @@ async function waitForChildExit(child, timeoutMs) {
|
|
|
622
867
|
return !result.timedOut && result.code !== null;
|
|
623
868
|
}
|
|
624
869
|
|
|
625
|
-
// src/
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
635
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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/
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
653
|
-
|
|
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
|
|
673
|
-
this.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1114
|
+
await client.close();
|
|
683
1115
|
}
|
|
684
1116
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
this.
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
721
|
-
|
|
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/
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
import {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
778
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
795
|
-
const
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
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
|
-
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
973
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
|
1446
|
+
return parsed;
|
|
1014
1447
|
}
|
|
1015
|
-
function
|
|
1016
|
-
|
|
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
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
|
1029
|
-
const
|
|
1030
|
-
|
|
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
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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/
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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;
|
|
1074
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
|
+
};
|
|
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/
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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) }
|
|
1129
2334
|
},
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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" }
|
|
1135
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" }
|
|
1136
2384
|
},
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
},
|
|
1143
|
-
error: (error) => {
|
|
1144
|
-
log.fail("channel", error);
|
|
2385
|
+
body: {
|
|
2386
|
+
elements: [
|
|
2387
|
+
{ tag: "markdown", content: markdown2 },
|
|
2388
|
+
{ tag: "action", actions }
|
|
2389
|
+
]
|
|
1145
2390
|
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
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
|
+
}
|
|
1148
2415
|
return {
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
+
}
|
|
1151
2445
|
};
|
|
1152
2446
|
}
|
|
1153
|
-
function
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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;
|
|
1159
2464
|
}
|
|
1160
2465
|
|
|
1161
2466
|
// src/bridge/run-flow.ts
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
);
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
{ kind: "text", content: event.content, streaming: false }
|
|
1215
|
-
],
|
|
1216
|
-
reasoning: { ...state.reasoning, active: false },
|
|
1217
|
-
footer: null
|
|
1218
|
-
};
|
|
1219
|
-
case "tool_use":
|
|
1220
|
-
return {
|
|
1221
|
-
...state,
|
|
1222
|
-
blocks: [
|
|
1223
|
-
...closeStreamingText(state.blocks),
|
|
1224
|
-
{
|
|
1225
|
-
kind: "tool",
|
|
1226
|
-
tool: {
|
|
1227
|
-
id: event.id,
|
|
1228
|
-
name: event.name,
|
|
1229
|
-
input: event.input,
|
|
1230
|
-
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;
|
|
1231
2519
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
reasoning: { ...state.reasoning, active: false },
|
|
1235
|
-
footer: "tool_running"
|
|
1236
|
-
};
|
|
1237
|
-
case "tool_result":
|
|
1238
|
-
return {
|
|
1239
|
-
...state,
|
|
1240
|
-
blocks: state.blocks.map((block) => {
|
|
1241
|
-
if (block.kind !== "tool" || block.tool.id !== event.id) return block;
|
|
1242
|
-
return {
|
|
1243
|
-
...block,
|
|
1244
|
-
tool: {
|
|
1245
|
-
...block.tool,
|
|
1246
|
-
status: event.isError ? "error" : "done",
|
|
1247
|
-
output: event.output
|
|
2520
|
+
if (event.type === "system" && event.sessionId) {
|
|
2521
|
+
input.sessions.set(input.scope, event.sessionId, event.cwd ?? cwd);
|
|
1248
2522
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
+
}
|
|
1270
2566
|
}
|
|
1271
2567
|
}
|
|
1272
|
-
function
|
|
1273
|
-
return {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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;
|
|
1279
2588
|
};
|
|
1280
2589
|
}
|
|
1281
|
-
function
|
|
1282
|
-
return {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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;
|
|
1289
2607
|
};
|
|
1290
2608
|
}
|
|
1291
|
-
function
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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");
|
|
1300
2660
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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`);
|
|
1305
2672
|
}
|
|
1306
|
-
function
|
|
1307
|
-
|
|
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");
|
|
1308
2734
|
}
|
|
1309
|
-
function
|
|
1310
|
-
const
|
|
1311
|
-
|
|
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
|
+
);
|
|
1312
2749
|
}
|
|
1313
|
-
function
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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"));
|
|
1321
2762
|
}
|
|
1322
|
-
function
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
text: { tag: "plain_text", content: "\u23F9 \u7EC8\u6B62" },
|
|
1326
|
-
type: "danger",
|
|
1327
|
-
value: { cmd: "stop" }
|
|
1328
|
-
};
|
|
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");
|
|
1329
2766
|
}
|
|
1330
|
-
function
|
|
1331
|
-
|
|
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`);
|
|
1332
2795
|
}
|
|
1333
|
-
function
|
|
1334
|
-
const
|
|
1335
|
-
|
|
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`);
|
|
1336
2818
|
}
|
|
1337
|
-
function
|
|
1338
|
-
const
|
|
1339
|
-
if (
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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")
|
|
1344
2860
|
);
|
|
2861
|
+
return;
|
|
1345
2862
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
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;
|
|
1348
2869
|
}
|
|
1349
|
-
if (
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
} else if (state.terminal === "error" && state.errorMsg) {
|
|
1354
|
-
elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
|
|
1355
|
-
} else if (state.terminal === "done" && elements.length === 0) {
|
|
1356
|
-
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;
|
|
1357
2874
|
}
|
|
1358
|
-
if (
|
|
1359
|
-
|
|
1360
|
-
|
|
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;
|
|
1361
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
|
+
};
|
|
1362
2938
|
return {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
summary: { content: summaryText(state) }
|
|
2939
|
+
...base,
|
|
2940
|
+
async sendCard(chatId, card) {
|
|
2941
|
+
await channel.send(chatId, { card }, {});
|
|
1367
2942
|
},
|
|
1368
|
-
|
|
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
|
+
}
|
|
1369
2959
|
};
|
|
1370
2960
|
}
|
|
1371
2961
|
|
|
1372
|
-
// src/bridge/
|
|
1373
|
-
async function
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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
|
|
1396
2987
|
});
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
}) : void 0;
|
|
1431
|
-
try {
|
|
1432
|
-
if (timeoutPromise) {
|
|
1433
|
-
await Promise.race([consume(), timeoutPromise]);
|
|
1434
|
-
} else {
|
|
1435
|
-
await consume();
|
|
1436
|
-
}
|
|
1437
|
-
state = timedOut ? markIdleTimeout(state, timeoutMs / 6e4) : finalizeIfRunning(state);
|
|
1438
|
-
await controller.update(renderCard(state));
|
|
1439
|
-
} finally {
|
|
1440
|
-
if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
|
|
1441
|
-
}
|
|
1442
|
-
},
|
|
1443
|
-
replyOptions
|
|
1444
|
-
);
|
|
1445
|
-
input.sessions.recordExchange(input.scope, cwd, input.messages, assistantOutput);
|
|
1446
|
-
} catch (error) {
|
|
1447
|
-
log.fail("run-flow", error, { scope: input.scope, runId });
|
|
1448
|
-
state = markInterrupted(state);
|
|
1449
|
-
try {
|
|
1450
|
-
await input.channel.sendMarkdown(input.chatId, `\u26A0\uFE0F agent \u8FD0\u884C\u5931\u8D25\uFF1A${errorMessage(error)}`, {
|
|
1451
|
-
...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;
|
|
1452
3021
|
});
|
|
1453
|
-
|
|
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);
|
|
1454
3057
|
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
const transcript = history.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.content}`).join("\n\n");
|
|
1462
|
-
return [
|
|
1463
|
-
"Continue the conversation using the history below.",
|
|
1464
|
-
"",
|
|
1465
|
-
transcript,
|
|
1466
|
-
"",
|
|
1467
|
-
`Current user message:
|
|
1468
|
-
${messages.join("\n\n")}`
|
|
1469
|
-
].join("\n");
|
|
3058
|
+
});
|
|
3059
|
+
await channel.connect();
|
|
3060
|
+
return {
|
|
3061
|
+
channel,
|
|
3062
|
+
disconnect: () => channel.disconnect()
|
|
3063
|
+
};
|
|
1470
3064
|
}
|
|
1471
|
-
function
|
|
1472
|
-
if (
|
|
1473
|
-
|
|
1474
|
-
return markInterrupted(state);
|
|
1475
|
-
}
|
|
1476
|
-
if (event.type === "error" && event.terminationReason === "timeout") {
|
|
1477
|
-
return markIdleTimeout(state, 0);
|
|
1478
|
-
}
|
|
1479
|
-
return reduce(state, event);
|
|
3065
|
+
function scopeForMessage(msg) {
|
|
3066
|
+
if (msg.chatMode === "topic" && msg.threadId) return `${msg.chatId}:${msg.threadId}`;
|
|
3067
|
+
return msg.chatId;
|
|
1480
3068
|
}
|
|
1481
|
-
function
|
|
1482
|
-
return
|
|
3069
|
+
async function resolveCardScope(chatId, raw) {
|
|
3070
|
+
return raw.message?.thread_id ? `${chatId}:${raw.message.thread_id}` : chatId;
|
|
1483
3071
|
}
|
|
1484
3072
|
|
|
1485
3073
|
// src/config/access-manager.ts
|
|
@@ -1588,15 +3176,21 @@ async function onboardPersonalAgent(deps = {}) {
|
|
|
1588
3176
|
}
|
|
1589
3177
|
|
|
1590
3178
|
// src/media/attachments.ts
|
|
1591
|
-
import { mkdir as
|
|
1592
|
-
import { join as
|
|
3179
|
+
import { mkdir as mkdir5, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
3180
|
+
import { join as join7 } from "path";
|
|
1593
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
|
+
}
|
|
1594
3187
|
async function prepareAttachments(channel, message, mediaDir) {
|
|
1595
|
-
await
|
|
3188
|
+
await mkdir5(mediaDir, { recursive: true });
|
|
1596
3189
|
const result = { imagePaths: [], textFileNotes: [] };
|
|
1597
3190
|
for (const resource of message.resources) {
|
|
1598
3191
|
if (!channel || resource.type !== "image" && resource.type !== "file") continue;
|
|
1599
|
-
const destination =
|
|
3192
|
+
const destination = join7(mediaDir, `${message.messageId}-${resource.fileKey}`);
|
|
3193
|
+
assertSafeMediaName(mediaDir, destination);
|
|
1600
3194
|
await channel.downloadResourceToFile(
|
|
1601
3195
|
message.messageId,
|
|
1602
3196
|
resource.fileKey,
|
|
@@ -1613,9 +3207,10 @@ async function prepareAttachments(channel, message, mediaDir) {
|
|
|
1613
3207
|
continue;
|
|
1614
3208
|
}
|
|
1615
3209
|
const content = await readFile2(destination, "utf8");
|
|
3210
|
+
const safeContent = truncateUtf8Safe(content, MAX_TEXT_FILE_BYTES);
|
|
1616
3211
|
result.textFileNotes.push(
|
|
1617
3212
|
`[attachment: ${resource.fileName ?? resource.fileKey}]
|
|
1618
|
-
${
|
|
3213
|
+
${safeContent}`
|
|
1619
3214
|
);
|
|
1620
3215
|
}
|
|
1621
3216
|
return result;
|
|
@@ -1736,8 +3331,8 @@ var SessionStore = class {
|
|
|
1736
3331
|
// src/workspace/git-worktree.ts
|
|
1737
3332
|
import { execFile } from "child_process";
|
|
1738
3333
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1739
|
-
import { access, copyFile, mkdir as
|
|
1740
|
-
import { dirname as dirname3, join as
|
|
3334
|
+
import { access, copyFile, mkdir as mkdir6 } from "fs/promises";
|
|
3335
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
1741
3336
|
import { promisify } from "util";
|
|
1742
3337
|
var execFileAsync = promisify(execFile);
|
|
1743
3338
|
async function defaultRunGit(args, cwd) {
|
|
@@ -1756,11 +3351,11 @@ async function exists(path) {
|
|
|
1756
3351
|
}
|
|
1757
3352
|
}
|
|
1758
3353
|
async function defaultCopyRulesFile(source, target) {
|
|
1759
|
-
await
|
|
3354
|
+
await mkdir6(dirname3(target), { recursive: true });
|
|
1760
3355
|
await copyFile(source, target);
|
|
1761
3356
|
}
|
|
1762
3357
|
function slugify(scope) {
|
|
1763
|
-
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);
|
|
1764
3359
|
return slug || "session";
|
|
1765
3360
|
}
|
|
1766
3361
|
var GitWorktreeManager = class {
|
|
@@ -1776,13 +3371,16 @@ var GitWorktreeManager = class {
|
|
|
1776
3371
|
const isGit = await this.isGitRepository(base);
|
|
1777
3372
|
if (!isGit) return { cwd: base, created: false };
|
|
1778
3373
|
const slug = slugify(scope);
|
|
1779
|
-
const target =
|
|
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
|
+
}
|
|
1780
3378
|
if (await exists(target)) {
|
|
1781
3379
|
await this.ensureProjectRules(base, target);
|
|
1782
3380
|
return { cwd: target, created: false };
|
|
1783
3381
|
}
|
|
1784
3382
|
const branch = `dsh-lark/${slug}-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`;
|
|
1785
|
-
await
|
|
3383
|
+
await mkdir6(this.options.worktreesRoot, { recursive: true });
|
|
1786
3384
|
await this.runGit(["worktree", "add", "-b", branch, target, "HEAD"], base);
|
|
1787
3385
|
await this.ensureProjectRules(base, target);
|
|
1788
3386
|
return { cwd: target, created: true, branch };
|
|
@@ -1796,9 +3394,9 @@ var GitWorktreeManager = class {
|
|
|
1796
3394
|
}
|
|
1797
3395
|
}
|
|
1798
3396
|
async ensureProjectRules(base, target) {
|
|
1799
|
-
const destination =
|
|
3397
|
+
const destination = join8(target, "AGENTS.md");
|
|
1800
3398
|
if (await exists(destination)) return;
|
|
1801
|
-
const sourceCandidates = [
|
|
3399
|
+
const sourceCandidates = [join8(base, ".dsh-lark", "AGENTS.md"), join8(base, "AGENTS.md")];
|
|
1802
3400
|
for (const source of sourceCandidates) {
|
|
1803
3401
|
if (await exists(source)) {
|
|
1804
3402
|
await this.copyRulesFile(source, destination);
|
|
@@ -1948,24 +3546,31 @@ async function runStart(options) {
|
|
|
1948
3546
|
return;
|
|
1949
3547
|
}
|
|
1950
3548
|
const defaultWorkspace = options.workspace ?? activeProfile.workspaces.default ?? env.workspace ?? paths.profilePath(profileName, "workspace");
|
|
1951
|
-
await
|
|
3549
|
+
await mkdir7(defaultWorkspace, { recursive: true });
|
|
1952
3550
|
const sessions = new SessionStore(paths.sessionsFile(profileName));
|
|
1953
3551
|
const workspaces = new WorkspaceStore(paths.workspacesFile(profileName));
|
|
1954
3552
|
const worktreeManager = new GitWorktreeManager({
|
|
1955
3553
|
worktreesRoot: paths.profilePath(profileName, "worktrees")
|
|
1956
3554
|
});
|
|
1957
3555
|
await Promise.all([sessions.load(), workspaces.load()]);
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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;
|
|
1965
3568
|
}
|
|
1966
|
-
const adapter = new DshAdapter(adapterOptions);
|
|
1967
3569
|
const activeRuns = new ActiveRuns();
|
|
1968
3570
|
const runPolicies = new RunPolicyStore();
|
|
3571
|
+
const approvals = new ApprovalRegistry();
|
|
3572
|
+
const questions = new QuestionRegistry();
|
|
3573
|
+
const densityStore = new DensityStore();
|
|
1969
3574
|
let streaming;
|
|
1970
3575
|
let larkChannel;
|
|
1971
3576
|
const pending = new PendingQueue(DEBOUNCE_MS, async (scope, batch) => {
|
|
@@ -1993,6 +3598,9 @@ async function runStart(options) {
|
|
|
1993
3598
|
workspaceManager: worktreeManager,
|
|
1994
3599
|
activeRuns,
|
|
1995
3600
|
runPolicies,
|
|
3601
|
+
approvals,
|
|
3602
|
+
questions,
|
|
3603
|
+
densityStore,
|
|
1996
3604
|
channel: streaming,
|
|
1997
3605
|
defaultWorkspace,
|
|
1998
3606
|
replyTo: first.messageId,
|
|
@@ -2017,10 +3625,15 @@ async function runStart(options) {
|
|
|
2017
3625
|
workspaces,
|
|
2018
3626
|
activeRuns,
|
|
2019
3627
|
runPolicies,
|
|
3628
|
+
approvals,
|
|
3629
|
+
questions,
|
|
3630
|
+
densityStore,
|
|
2020
3631
|
defaultRunTimeoutMs: activeProfile.preferences.runTimeoutMs ?? env.runTimeoutMs,
|
|
2021
3632
|
accessManager,
|
|
2022
3633
|
pending,
|
|
2023
3634
|
defaultWorkspace,
|
|
3635
|
+
accessDefaultDeny: env.accessDefaultDeny,
|
|
3636
|
+
eventFreshnessMs: env.eventFreshnessMs,
|
|
2024
3637
|
allowedUsers: activeProfile.access.allowedUsers,
|
|
2025
3638
|
allowedChats: activeProfile.access.allowedChats
|
|
2026
3639
|
};
|
|
@@ -2041,14 +3654,18 @@ async function runStart(options) {
|
|
|
2041
3654
|
`);
|
|
2042
3655
|
await waitForShutdown();
|
|
2043
3656
|
await bridge.disconnect();
|
|
3657
|
+
await adapter.dispose?.();
|
|
2044
3658
|
await Promise.all([sessions.flush(), workspaces.flush()]);
|
|
2045
3659
|
}
|
|
3660
|
+
function errorMessage2(error) {
|
|
3661
|
+
return error instanceof Error ? error.message : String(error);
|
|
3662
|
+
}
|
|
2046
3663
|
function waitForShutdown() {
|
|
2047
|
-
return new Promise((
|
|
3664
|
+
return new Promise((resolve5) => {
|
|
2048
3665
|
const shutdown = () => {
|
|
2049
3666
|
process.off("SIGINT", shutdown);
|
|
2050
3667
|
process.off("SIGTERM", shutdown);
|
|
2051
|
-
|
|
3668
|
+
resolve5();
|
|
2052
3669
|
};
|
|
2053
3670
|
process.once("SIGINT", shutdown);
|
|
2054
3671
|
process.once("SIGTERM", shutdown);
|