dsh-lark-bot 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -6
- package/SECURITY.md +42 -0
- package/dist/cli.js +2729 -1111
- 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,863 +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;
|
|
1971
|
+
}
|
|
1972
|
+
return count;
|
|
1973
|
+
}
|
|
1974
|
+
get(scope, id) {
|
|
1975
|
+
return this.pending.get(`${scope}:${id}`)?.input;
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
|
|
1979
|
+
// src/bot/run-policy.ts
|
|
1980
|
+
var RunPolicyStore = class {
|
|
1981
|
+
timeouts = /* @__PURE__ */ new Map();
|
|
1982
|
+
get(scope) {
|
|
1983
|
+
return this.timeouts.get(scope);
|
|
1984
|
+
}
|
|
1985
|
+
set(scope, runTimeoutMs) {
|
|
1986
|
+
this.timeouts.set(scope, runTimeoutMs);
|
|
1987
|
+
}
|
|
1988
|
+
clear(scope) {
|
|
1989
|
+
return this.timeouts.delete(scope);
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
// src/bridge/channel.ts
|
|
1994
|
+
import { createLarkChannel } from "@larksuite/channel";
|
|
1995
|
+
|
|
1996
|
+
// src/commands/index.ts
|
|
1997
|
+
import { resolve as resolve4 } from "path";
|
|
1998
|
+
|
|
1999
|
+
// src/card/workspace-card.ts
|
|
2000
|
+
function renderWorkspaceCard(input) {
|
|
2001
|
+
const entries = input.index;
|
|
2002
|
+
const markdown2 = [
|
|
2003
|
+
`**\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55**
|
|
2004
|
+
\`${input.current}\``,
|
|
2005
|
+
"",
|
|
2006
|
+
...entries.length > 0 ? entries.map(
|
|
2007
|
+
({ name, cwd, lastUsed }) => `- **${name}** \u2192 \`${cwd}\`${lastUsed ? ` \xB7 ${new Date(lastUsed).toLocaleString()}` : ""}`
|
|
2008
|
+
) : ["\u6682\u65E0\u547D\u540D\u5DE5\u4F5C\u7A7A\u95F4\u3002"]
|
|
2009
|
+
].join("\n");
|
|
2010
|
+
return {
|
|
2011
|
+
schema: "2.0",
|
|
2012
|
+
config: {
|
|
2013
|
+
summary: { content: "\u5DE5\u4F5C\u7A7A\u95F4\u5BFC\u822A" }
|
|
2014
|
+
},
|
|
2015
|
+
body: {
|
|
2016
|
+
elements: [
|
|
2017
|
+
{
|
|
2018
|
+
tag: "markdown",
|
|
2019
|
+
content: markdown2
|
|
2020
|
+
}
|
|
2021
|
+
]
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// src/card/density.ts
|
|
2027
|
+
var CARD_DENSITIES = ["compact", "standard", "detailed"];
|
|
2028
|
+
function parseCardDensity(value) {
|
|
2029
|
+
const normalized = value?.trim().toLowerCase();
|
|
2030
|
+
if (!normalized) return void 0;
|
|
2031
|
+
if (CARD_DENSITIES.includes(normalized)) {
|
|
2032
|
+
return normalized;
|
|
2033
|
+
}
|
|
2034
|
+
return void 0;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// src/bridge/run-flow.ts
|
|
2038
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2039
|
+
|
|
2040
|
+
// src/card/run-state.ts
|
|
2041
|
+
var initialState = {
|
|
2042
|
+
blocks: [],
|
|
2043
|
+
reasoning: { content: "", active: false },
|
|
2044
|
+
usage: void 0,
|
|
2045
|
+
footer: "thinking",
|
|
2046
|
+
terminal: "running",
|
|
2047
|
+
errorMsg: void 0,
|
|
2048
|
+
idleTimeoutMinutes: void 0
|
|
2049
|
+
};
|
|
2050
|
+
function closeStreamingText(blocks) {
|
|
2051
|
+
return blocks.map(
|
|
2052
|
+
(block) => block.kind === "text" && block.streaming ? { ...block, streaming: false } : block
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
function reduce(state, event) {
|
|
2056
|
+
switch (event.type) {
|
|
2057
|
+
case "text": {
|
|
2058
|
+
const last = state.blocks[state.blocks.length - 1];
|
|
2059
|
+
if (last?.kind === "text" && last.streaming) {
|
|
2060
|
+
return {
|
|
2061
|
+
...state,
|
|
2062
|
+
blocks: [
|
|
2063
|
+
...state.blocks.slice(0, -1),
|
|
2064
|
+
{ ...last, content: last.content + event.delta }
|
|
2065
|
+
],
|
|
2066
|
+
reasoning: { ...state.reasoning, active: false },
|
|
2067
|
+
footer: "streaming"
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
return {
|
|
2071
|
+
...state,
|
|
2072
|
+
blocks: [...state.blocks, { kind: "text", content: event.delta, streaming: true }],
|
|
2073
|
+
reasoning: { ...state.reasoning, active: false },
|
|
2074
|
+
footer: "streaming"
|
|
2075
|
+
};
|
|
1074
2076
|
}
|
|
2077
|
+
case "thinking":
|
|
2078
|
+
return {
|
|
2079
|
+
...state,
|
|
2080
|
+
reasoning: {
|
|
2081
|
+
content: state.reasoning.content + event.delta,
|
|
2082
|
+
active: true
|
|
2083
|
+
},
|
|
2084
|
+
footer: "thinking"
|
|
2085
|
+
};
|
|
2086
|
+
case "final_text":
|
|
2087
|
+
return {
|
|
2088
|
+
...state,
|
|
2089
|
+
blocks: [
|
|
2090
|
+
...closeStreamingText(state.blocks),
|
|
2091
|
+
{ kind: "text", content: event.content, streaming: false }
|
|
2092
|
+
],
|
|
2093
|
+
reasoning: { ...state.reasoning, active: false },
|
|
2094
|
+
footer: null
|
|
2095
|
+
};
|
|
2096
|
+
case "usage":
|
|
2097
|
+
return {
|
|
2098
|
+
...state,
|
|
2099
|
+
usage: {
|
|
2100
|
+
inputTokens: event.inputTokens,
|
|
2101
|
+
outputTokens: event.outputTokens
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
case "tool_use":
|
|
2105
|
+
return {
|
|
2106
|
+
...state,
|
|
2107
|
+
blocks: [
|
|
2108
|
+
...closeStreamingText(state.blocks),
|
|
2109
|
+
{
|
|
2110
|
+
kind: "tool",
|
|
2111
|
+
tool: {
|
|
2112
|
+
id: event.id,
|
|
2113
|
+
name: event.name,
|
|
2114
|
+
input: event.input,
|
|
2115
|
+
status: "running"
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
],
|
|
2119
|
+
reasoning: { ...state.reasoning, active: false },
|
|
2120
|
+
footer: "tool_running"
|
|
2121
|
+
};
|
|
2122
|
+
case "tool_result":
|
|
2123
|
+
return {
|
|
2124
|
+
...state,
|
|
2125
|
+
blocks: state.blocks.map((block) => {
|
|
2126
|
+
if (block.kind !== "tool" || block.tool.id !== event.id) return block;
|
|
2127
|
+
return {
|
|
2128
|
+
...block,
|
|
2129
|
+
tool: {
|
|
2130
|
+
...block.tool,
|
|
2131
|
+
status: event.isError ? "error" : "done",
|
|
2132
|
+
output: event.output
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
})
|
|
2136
|
+
};
|
|
2137
|
+
case "error":
|
|
2138
|
+
return {
|
|
2139
|
+
...state,
|
|
2140
|
+
terminal: event.terminationReason === "interrupted" ? "interrupted" : event.terminationReason === "timeout" ? "idle_timeout" : "error",
|
|
2141
|
+
errorMsg: event.terminationReason === "failed" ? event.message : void 0,
|
|
2142
|
+
idleTimeoutMinutes: void 0,
|
|
2143
|
+
footer: null
|
|
2144
|
+
};
|
|
2145
|
+
case "done":
|
|
2146
|
+
return {
|
|
2147
|
+
...state,
|
|
2148
|
+
blocks: closeStreamingText(state.blocks),
|
|
2149
|
+
reasoning: { ...state.reasoning, active: false },
|
|
2150
|
+
terminal: event.terminationReason === "interrupted" ? "interrupted" : event.terminationReason === "timeout" ? "idle_timeout" : "done",
|
|
2151
|
+
footer: null
|
|
2152
|
+
};
|
|
2153
|
+
default:
|
|
2154
|
+
return state;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
function markInterrupted(state) {
|
|
2158
|
+
return {
|
|
2159
|
+
...state,
|
|
2160
|
+
blocks: closeStreamingText(state.blocks),
|
|
2161
|
+
reasoning: { ...state.reasoning, active: false },
|
|
2162
|
+
terminal: "interrupted",
|
|
2163
|
+
footer: null
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function markIdleTimeout(state, minutes) {
|
|
2167
|
+
return {
|
|
2168
|
+
...state,
|
|
2169
|
+
blocks: closeStreamingText(state.blocks),
|
|
2170
|
+
reasoning: { ...state.reasoning, active: false },
|
|
2171
|
+
terminal: "idle_timeout",
|
|
2172
|
+
footer: null,
|
|
2173
|
+
idleTimeoutMinutes: minutes
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
function finalizeIfRunning(state) {
|
|
2177
|
+
if (state.terminal !== "running") return state;
|
|
2178
|
+
return {
|
|
2179
|
+
...state,
|
|
2180
|
+
blocks: closeStreamingText(state.blocks),
|
|
2181
|
+
reasoning: { ...state.reasoning, active: false },
|
|
2182
|
+
terminal: "done",
|
|
2183
|
+
footer: null
|
|
1075
2184
|
};
|
|
1076
2185
|
}
|
|
1077
2186
|
|
|
1078
|
-
// src/
|
|
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
|
-
|
|
2284
|
+
body: { elements }
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
function renderDetailed(state) {
|
|
2288
|
+
const elements = [];
|
|
2289
|
+
if (state.reasoning.content) {
|
|
2290
|
+
elements.push(
|
|
2291
|
+
noteMd(
|
|
2292
|
+
state.reasoning.active ? "\u{1F9E0} \u6B63\u5728\u601D\u8003\u2026" : `\u{1F9E0} **\u601D\u8003\u8FC7\u7A0B**
|
|
2293
|
+
${state.reasoning.content.slice(0, 2e3)}`
|
|
2294
|
+
)
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
for (const block of state.blocks) {
|
|
2298
|
+
if (block.kind === "text") {
|
|
2299
|
+
elements.push(textBlock(block));
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
const tool = block.tool;
|
|
2303
|
+
const icon = tool.status === "error" ? "\u26A0\uFE0F" : tool.status === "done" ? "\u2705" : "\u23F3";
|
|
2304
|
+
const lines = [`${icon} **${tool.name}**`];
|
|
2305
|
+
if (tool.input !== void 0 && tool.input !== "") {
|
|
2306
|
+
lines.push(`\u8F93\u5165\uFF1A\`\`\`
|
|
2307
|
+
${safeJsonPreview(tool.input)}
|
|
2308
|
+
\`\`\``);
|
|
2309
|
+
}
|
|
2310
|
+
if (tool.output) {
|
|
2311
|
+
lines.push(`\u8F93\u51FA\uFF1A${tool.output.slice(0, 500)}`);
|
|
2312
|
+
}
|
|
2313
|
+
elements.push(markdown(lines.join("\n")));
|
|
2314
|
+
}
|
|
2315
|
+
if (state.terminal === "interrupted") {
|
|
2316
|
+
elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
|
|
2317
|
+
} else if (state.terminal === "idle_timeout") {
|
|
2318
|
+
elements.push(noteMd(`_\u23F1 ${state.idleTimeoutMinutes ?? 0} \u5206\u949F\u65E0\u54CD\u5E94\uFF0C\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
|
|
2319
|
+
} else if (state.terminal === "error" && state.errorMsg) {
|
|
2320
|
+
elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
|
|
2321
|
+
} else if (state.terminal === "done") {
|
|
2322
|
+
const usage = usageLine(state);
|
|
2323
|
+
if (usage) elements.push(noteMd(usage));
|
|
2324
|
+
}
|
|
2325
|
+
if (state.terminal === "running") {
|
|
2326
|
+
if (state.footer) elements.push(footerStatus(state.footer));
|
|
2327
|
+
elements.push(stopButton());
|
|
2328
|
+
}
|
|
2329
|
+
return {
|
|
2330
|
+
schema: "2.0",
|
|
2331
|
+
config: {
|
|
2332
|
+
streaming_mode: state.terminal === "running",
|
|
2333
|
+
summary: { content: summaryText(state) }
|
|
1128
2334
|
},
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
2335
|
+
body: { elements }
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
function safeJsonPreview(value) {
|
|
2339
|
+
if (typeof value === "string") return value.slice(0, 300);
|
|
2340
|
+
try {
|
|
2341
|
+
return JSON.stringify(value).slice(0, 300);
|
|
2342
|
+
} catch {
|
|
2343
|
+
return String(value).slice(0, 300);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
function renderCard(state, density = "standard") {
|
|
2347
|
+
if (density === "compact") return renderCompact(state);
|
|
2348
|
+
if (density === "detailed") return renderDetailed(state);
|
|
2349
|
+
return renderStandard(state);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// src/card/approval-card.ts
|
|
2353
|
+
function renderApprovalCard(input) {
|
|
2354
|
+
const allow = input.options.find((option) => option.kind === "allow_once") ?? input.options.find((option) => option.kind === "allow_always");
|
|
2355
|
+
const reject = input.options.find((option) => option.kind === "reject_once") ?? input.options.find((option) => option.kind === "reject_always");
|
|
2356
|
+
const markdown2 = [
|
|
2357
|
+
"\u{1F510} **\u5BA1\u6279\u8BF7\u6C42**",
|
|
2358
|
+
"",
|
|
2359
|
+
`**\u5DE5\u5177** \`${input.toolName}\``,
|
|
2360
|
+
...input.reason ? ["", input.reason] : []
|
|
2361
|
+
].join("\n");
|
|
2362
|
+
const actions = [
|
|
2363
|
+
...allow ? [
|
|
2364
|
+
{
|
|
2365
|
+
tag: "button",
|
|
2366
|
+
text: { tag: "plain_text", content: `\u2705 ${allow.name}` },
|
|
2367
|
+
type: "primary",
|
|
2368
|
+
value: { cmd: "approve", id: input.id, outcome: "allow" }
|
|
1134
2369
|
}
|
|
2370
|
+
] : [],
|
|
2371
|
+
...reject ? [
|
|
2372
|
+
{
|
|
2373
|
+
tag: "button",
|
|
2374
|
+
text: { tag: "plain_text", content: `\u26D4 ${reject.name}` },
|
|
2375
|
+
type: "danger",
|
|
2376
|
+
value: { cmd: "approve", id: input.id, outcome: "reject" }
|
|
2377
|
+
}
|
|
2378
|
+
] : []
|
|
2379
|
+
];
|
|
2380
|
+
return {
|
|
2381
|
+
schema: "2.0",
|
|
2382
|
+
config: {
|
|
2383
|
+
summary: { content: "\u5BA1\u6279\u8BF7\u6C42" }
|
|
1135
2384
|
},
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
},
|
|
1142
|
-
error: (error) => {
|
|
1143
|
-
log.fail("channel", error);
|
|
2385
|
+
body: {
|
|
2386
|
+
elements: [
|
|
2387
|
+
{ tag: "markdown", content: markdown2 },
|
|
2388
|
+
{ tag: "action", actions }
|
|
2389
|
+
]
|
|
1144
2390
|
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// src/card/question-card.ts
|
|
2395
|
+
function formElement(input) {
|
|
2396
|
+
if (input.kind === "text") {
|
|
2397
|
+
return {
|
|
2398
|
+
tag: "input",
|
|
2399
|
+
name: "answer",
|
|
2400
|
+
placeholder: { tag: "plain_text", content: input.placeholder ?? "\u8BF7\u8F93\u5165\u7B54\u6848\u2026" }
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
const options = (input.options ?? []).map((option, index) => ({
|
|
2404
|
+
text: { tag: "plain_text", content: option },
|
|
2405
|
+
value: `option-${index}`
|
|
2406
|
+
}));
|
|
2407
|
+
if (input.kind === "multi") {
|
|
2408
|
+
return {
|
|
2409
|
+
tag: "multi_select_static",
|
|
2410
|
+
name: "answer",
|
|
2411
|
+
placeholder: { tag: "plain_text", content: "\u8BF7\u9009\u62E9\uFF08\u53EF\u591A\u9009\uFF09\u2026" },
|
|
2412
|
+
options
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
1147
2415
|
return {
|
|
1148
|
-
|
|
1149
|
-
|
|
2416
|
+
tag: "select_static",
|
|
2417
|
+
name: "answer",
|
|
2418
|
+
placeholder: { tag: "plain_text", content: "\u8BF7\u9009\u62E9\u2026" },
|
|
2419
|
+
options
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
function renderQuestionCard(input) {
|
|
2423
|
+
return {
|
|
2424
|
+
schema: "2.0",
|
|
2425
|
+
config: {
|
|
2426
|
+
summary: { content: "\u95EE\u9898" }
|
|
2427
|
+
},
|
|
2428
|
+
body: {
|
|
2429
|
+
elements: [
|
|
2430
|
+
{ tag: "markdown", content: `\u2753 ${input.question}` },
|
|
2431
|
+
formElement(input),
|
|
2432
|
+
{
|
|
2433
|
+
tag: "action",
|
|
2434
|
+
actions: [
|
|
2435
|
+
{
|
|
2436
|
+
tag: "button",
|
|
2437
|
+
text: { tag: "plain_text", content: "\u63D0\u4EA4" },
|
|
2438
|
+
type: "primary",
|
|
2439
|
+
value: { cmd: "question-submit", id: input.id }
|
|
2440
|
+
}
|
|
2441
|
+
]
|
|
2442
|
+
}
|
|
2443
|
+
]
|
|
2444
|
+
}
|
|
1150
2445
|
};
|
|
1151
2446
|
}
|
|
1152
|
-
function
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
2447
|
+
function extractQuestionAnswer(kind, value, options) {
|
|
2448
|
+
const answer = value;
|
|
2449
|
+
if (kind === "text") {
|
|
2450
|
+
return typeof answer === "string" && answer.trim() ? answer.trim() : void 0;
|
|
2451
|
+
}
|
|
2452
|
+
const selected = Array.isArray(answer) ? answer.map((item) => String(item)) : typeof answer === "string" && answer ? [answer] : [];
|
|
2453
|
+
if (selected.length === 0) return void 0;
|
|
2454
|
+
const labels = options ?? [];
|
|
2455
|
+
const resolved = selected.map((item) => {
|
|
2456
|
+
const match = /^option-(\d+)$/.exec(item);
|
|
2457
|
+
if (match) {
|
|
2458
|
+
const index = Number(match[1]);
|
|
2459
|
+
return labels[index] ?? item;
|
|
2460
|
+
}
|
|
2461
|
+
return item;
|
|
2462
|
+
});
|
|
2463
|
+
return kind === "single" ? resolved[0] : resolved;
|
|
1158
2464
|
}
|
|
1159
2465
|
|
|
1160
2466
|
// src/bridge/run-flow.ts
|
|
1161
|
-
|
|
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
|
-
{ kind: "text", content: event.content, streaming: false }
|
|
1214
|
-
],
|
|
1215
|
-
reasoning: { ...state.reasoning, active: false },
|
|
1216
|
-
footer: null
|
|
1217
|
-
};
|
|
1218
|
-
case "tool_use":
|
|
1219
|
-
return {
|
|
1220
|
-
...state,
|
|
1221
|
-
blocks: [
|
|
1222
|
-
...closeStreamingText(state.blocks),
|
|
1223
|
-
{
|
|
1224
|
-
kind: "tool",
|
|
1225
|
-
tool: {
|
|
1226
|
-
id: event.id,
|
|
1227
|
-
name: event.name,
|
|
1228
|
-
input: event.input,
|
|
1229
|
-
status: "running"
|
|
2467
|
+
async function runAgentBatch(input) {
|
|
2468
|
+
const replyOptions = input.replyTo ? { replyTo: input.replyTo } : {};
|
|
2469
|
+
if (input.activeRuns.get(input.scope)) {
|
|
2470
|
+
await input.channel.sendMarkdown(input.chatId, "\u5F53\u524D\u4F1A\u8BDD\u5DF2\u6709\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148 `/stop` \u6216\u7B49\u5F85\u5B8C\u6210\u3002", {
|
|
2471
|
+
...replyOptions
|
|
2472
|
+
});
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
const requestedCwd = input.workspaces.cwdFor(input.scope) ?? input.defaultWorkspace;
|
|
2476
|
+
const workspace = input.workspaceManager ? await input.workspaceManager.ensure(input.scope, requestedCwd) : { cwd: requestedCwd };
|
|
2477
|
+
const cwd = workspace.cwd;
|
|
2478
|
+
const sessionId = input.sessions.resumeFor(input.scope, cwd);
|
|
2479
|
+
const history = input.sessions.historyFor(input.scope, cwd);
|
|
2480
|
+
const prompt = buildPrompt(history, input.messages);
|
|
2481
|
+
const runId = randomUUID4();
|
|
2482
|
+
const run = input.adapter.run({
|
|
2483
|
+
runId,
|
|
2484
|
+
prompt,
|
|
2485
|
+
cwd,
|
|
2486
|
+
sessionId,
|
|
2487
|
+
model: input.model,
|
|
2488
|
+
images: input.images,
|
|
2489
|
+
stopGraceMs: input.stopGraceMs,
|
|
2490
|
+
...input.approvals ? {
|
|
2491
|
+
onApprovalRequest: approvalHandlerFor({
|
|
2492
|
+
approvals: input.approvals,
|
|
2493
|
+
channel: input.channel,
|
|
2494
|
+
chatId: input.chatId,
|
|
2495
|
+
scope: input.scope
|
|
2496
|
+
})
|
|
2497
|
+
} : {}
|
|
2498
|
+
});
|
|
2499
|
+
input.activeRuns.set(input.scope, { runId, stop: run.stop });
|
|
2500
|
+
let state = initialState;
|
|
2501
|
+
const stopRequested = { value: false };
|
|
2502
|
+
const timeoutMs = input.runPolicies?.get(input.scope) ?? input.runTimeoutMs ?? 0;
|
|
2503
|
+
let timedOut = false;
|
|
2504
|
+
let assistantOutput = "";
|
|
2505
|
+
const density = input.densityStore?.get(input.scope) ?? "standard";
|
|
2506
|
+
try {
|
|
2507
|
+
await input.channel.streamCard(
|
|
2508
|
+
input.chatId,
|
|
2509
|
+
renderCard(state, density),
|
|
2510
|
+
async (controller) => {
|
|
2511
|
+
const consume = async () => {
|
|
2512
|
+
for await (const event of run.events) {
|
|
2513
|
+
if (timedOut) return;
|
|
2514
|
+
state = applyEvent(state, event, stopRequested);
|
|
2515
|
+
if (event.type === "final_text") {
|
|
2516
|
+
assistantOutput = event.content;
|
|
2517
|
+
} else if (event.type === "text") {
|
|
2518
|
+
assistantOutput += event.delta;
|
|
1230
2519
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
reasoning: { ...state.reasoning, active: false },
|
|
1234
|
-
footer: "tool_running"
|
|
1235
|
-
};
|
|
1236
|
-
case "tool_result":
|
|
1237
|
-
return {
|
|
1238
|
-
...state,
|
|
1239
|
-
blocks: state.blocks.map((block) => {
|
|
1240
|
-
if (block.kind !== "tool" || block.tool.id !== event.id) return block;
|
|
1241
|
-
return {
|
|
1242
|
-
...block,
|
|
1243
|
-
tool: {
|
|
1244
|
-
...block.tool,
|
|
1245
|
-
status: event.isError ? "error" : "done",
|
|
1246
|
-
output: event.output
|
|
2520
|
+
if (event.type === "system" && event.sessionId) {
|
|
2521
|
+
input.sessions.set(input.scope, event.sessionId, event.cwd ?? cwd);
|
|
1247
2522
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
2523
|
+
await controller.update(renderCard(state, density));
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
let timeoutTimer;
|
|
2527
|
+
const timeoutPromise = timeoutMs > 0 ? new Promise((resolve5) => {
|
|
2528
|
+
timeoutTimer = setTimeout(() => {
|
|
2529
|
+
timedOut = true;
|
|
2530
|
+
void run.stop();
|
|
2531
|
+
resolve5();
|
|
2532
|
+
}, timeoutMs);
|
|
2533
|
+
}) : void 0;
|
|
2534
|
+
try {
|
|
2535
|
+
if (timeoutPromise) {
|
|
2536
|
+
await Promise.race([consume(), timeoutPromise]);
|
|
2537
|
+
} else {
|
|
2538
|
+
await consume();
|
|
2539
|
+
}
|
|
2540
|
+
state = timedOut ? markIdleTimeout(state, timeoutMs / 6e4) : finalizeIfRunning(state);
|
|
2541
|
+
await controller.update(renderCard(state, density));
|
|
2542
|
+
} finally {
|
|
2543
|
+
if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
|
|
2544
|
+
}
|
|
2545
|
+
},
|
|
2546
|
+
replyOptions
|
|
2547
|
+
);
|
|
2548
|
+
input.sessions.recordExchange(input.scope, cwd, input.messages, assistantOutput);
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
log.fail("run-flow", error, { scope: input.scope, runId });
|
|
2551
|
+
state = markInterrupted(state);
|
|
2552
|
+
try {
|
|
2553
|
+
await input.channel.sendMarkdown(input.chatId, `\u26A0\uFE0F agent \u8FD0\u884C\u5931\u8D25\uFF1A${errorMessage(error)}`, {
|
|
2554
|
+
...replyOptions
|
|
2555
|
+
});
|
|
2556
|
+
} catch {
|
|
2557
|
+
}
|
|
2558
|
+
} finally {
|
|
2559
|
+
input.activeRuns.delete(input.scope);
|
|
2560
|
+
if (input.approvals) {
|
|
2561
|
+
input.approvals.settleAll(input.scope, "cancelled");
|
|
2562
|
+
}
|
|
2563
|
+
if (input.questions) {
|
|
2564
|
+
input.questions.settleAll(input.scope);
|
|
2565
|
+
}
|
|
1269
2566
|
}
|
|
1270
2567
|
}
|
|
1271
|
-
function
|
|
1272
|
-
return {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
2568
|
+
function approvalHandlerFor(input) {
|
|
2569
|
+
return async (request) => {
|
|
2570
|
+
if (!input.approvals || !input.channel.sendCard) return "cancelled";
|
|
2571
|
+
const promise = input.approvals.register(input.scope, request);
|
|
2572
|
+
try {
|
|
2573
|
+
await input.channel.sendCard(
|
|
2574
|
+
input.chatId,
|
|
2575
|
+
renderApprovalCard({
|
|
2576
|
+
id: request.id,
|
|
2577
|
+
toolName: request.toolName,
|
|
2578
|
+
reason: request.reason,
|
|
2579
|
+
options: request.options
|
|
2580
|
+
})
|
|
2581
|
+
);
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
log.fail("approval-card", error, { scope: input.scope });
|
|
2584
|
+
input.approvals.settleAll(input.scope, "cancelled");
|
|
2585
|
+
return "cancelled";
|
|
2586
|
+
}
|
|
2587
|
+
return promise;
|
|
1278
2588
|
};
|
|
1279
2589
|
}
|
|
1280
|
-
function
|
|
1281
|
-
return {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
2590
|
+
function questionHandlerFor(input) {
|
|
2591
|
+
return async (question) => {
|
|
2592
|
+
if (!input.questions || !input.channel.sendCard) return void 0;
|
|
2593
|
+
const { id, promise } = input.questions.register(input.scope, {
|
|
2594
|
+
kind: question.kind,
|
|
2595
|
+
question: question.question,
|
|
2596
|
+
...question.options === void 0 ? {} : { options: question.options },
|
|
2597
|
+
...question.placeholder === void 0 ? {} : { placeholder: question.placeholder }
|
|
2598
|
+
});
|
|
2599
|
+
try {
|
|
2600
|
+
await input.channel.sendCard(input.chatId, renderQuestionCard({ ...question, id }));
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
log.fail("question-card", error, { scope: input.scope });
|
|
2603
|
+
input.questions.settleAll(input.scope);
|
|
2604
|
+
return void 0;
|
|
2605
|
+
}
|
|
2606
|
+
return promise;
|
|
1288
2607
|
};
|
|
1289
2608
|
}
|
|
1290
|
-
function
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
2609
|
+
function buildPrompt(history, messages) {
|
|
2610
|
+
if (history.length === 0) return messages.join("\n\n");
|
|
2611
|
+
const transcript = history.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.content}`).join("\n\n");
|
|
2612
|
+
return [
|
|
2613
|
+
"Continue the conversation using the history below.",
|
|
2614
|
+
"",
|
|
2615
|
+
transcript,
|
|
2616
|
+
"",
|
|
2617
|
+
`Current user message:
|
|
2618
|
+
${messages.join("\n\n")}`
|
|
2619
|
+
].join("\n");
|
|
2620
|
+
}
|
|
2621
|
+
function applyEvent(state, event, stopRequested) {
|
|
2622
|
+
if (event.type === "done" && event.terminationReason === "interrupted") {
|
|
2623
|
+
stopRequested.value = true;
|
|
2624
|
+
return markInterrupted(state);
|
|
2625
|
+
}
|
|
2626
|
+
if (event.type === "error" && event.terminationReason === "timeout") {
|
|
2627
|
+
return markIdleTimeout(state, 0);
|
|
2628
|
+
}
|
|
2629
|
+
return reduce(state, event);
|
|
2630
|
+
}
|
|
2631
|
+
function errorMessage(error) {
|
|
2632
|
+
return error instanceof Error ? error.message : String(error);
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/commands/index.ts
|
|
2636
|
+
var HELP = [
|
|
2637
|
+
"**dsh-lark-bot \u547D\u4EE4**",
|
|
2638
|
+
"",
|
|
2639
|
+
"- `/new` `/reset` \u2014 \u5F00\u59CB\u65B0\u4F1A\u8BDD",
|
|
2640
|
+
"- `/cd <path>` \u2014 \u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\u5E76\u91CD\u7F6E\u4F1A\u8BDD",
|
|
2641
|
+
"- `/ws list|save <name>|use <name>|remove <name>` \u2014 \u7BA1\u7406\u5DE5\u4F5C\u7A7A\u95F4",
|
|
2642
|
+
"- `/status` \u2014 \u67E5\u770B\u5F53\u524D\u72B6\u6001",
|
|
2643
|
+
"- `/resume` \u2014 \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u6700\u8FD1\u4E0A\u4E0B\u6587",
|
|
2644
|
+
"- `/stop` \u2014 \u7EC8\u6B62\u5F53\u524D\u4EFB\u52A1",
|
|
2645
|
+
"- `/timeout [N|off|default]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6",
|
|
2646
|
+
"- `/density [compact|standard|detailed]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5361\u7247\u5BC6\u5EA6",
|
|
2647
|
+
"- `/ask <\u95EE\u9898>` \u2014 \u53D1\u9001\u7ED3\u6784\u5316\u95EE\u7B54\u5361\uFF08\u56DE\u7B54\u5C06\u8BB0\u5165\u4F1A\u8BDD\uFF09",
|
|
2648
|
+
"- `/invite user|admin|group <id>` \u2014 \u7BA1\u7406\u8BBF\u95EE\u767D\u540D\u5355",
|
|
2649
|
+
"- `/help` \u2014 \u663E\u793A\u672C\u5E2E\u52A9"
|
|
2650
|
+
].join("\n");
|
|
2651
|
+
async function reply(ctx, markdown2) {
|
|
2652
|
+
await ctx.channel.sendMarkdown(ctx.chatId, markdown2, {
|
|
2653
|
+
replyTo: ctx.messageId
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
async function handleNew(_args, ctx) {
|
|
2657
|
+
const wasRunning = await ctx.activeRuns.interrupt(ctx.scope);
|
|
2658
|
+
ctx.sessions.clear(ctx.scope);
|
|
2659
|
+
await reply(ctx, wasRunning ? "\u5DF2\u4E2D\u65AD\u5F53\u524D\u4EFB\u52A1\u5E76\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002" : "\u5DF2\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002");
|
|
1299
2660
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
2661
|
+
async function handleCd(args, ctx) {
|
|
2662
|
+
const path = args.trim();
|
|
2663
|
+
if (!path) {
|
|
2664
|
+
await reply(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`");
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
const cwd = resolve4(path);
|
|
2668
|
+
await ctx.activeRuns.interrupt(ctx.scope);
|
|
2669
|
+
ctx.workspaces.setCwd(ctx.scope, cwd);
|
|
2670
|
+
ctx.sessions.clear(ctx.scope);
|
|
2671
|
+
await reply(ctx, `\u5DF2\u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\uFF1A\`${cwd}\`\uFF0C\u4F1A\u8BDD\u5DF2\u91CD\u7F6E\u3002`);
|
|
1304
2672
|
}
|
|
1305
|
-
function
|
|
1306
|
-
|
|
2673
|
+
async function handleWs(args, ctx) {
|
|
2674
|
+
const [sub, ...rest] = args.trim().split(/\s+/);
|
|
2675
|
+
const name = rest.join(" ").trim();
|
|
2676
|
+
if (!sub || sub === "list") {
|
|
2677
|
+
const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
|
|
2678
|
+
const named = ctx.workspaces.listNamed();
|
|
2679
|
+
const index = ctx.workspaces.listIndex();
|
|
2680
|
+
if (ctx.channel.sendCard) {
|
|
2681
|
+
await ctx.channel.sendCard(ctx.chatId, renderWorkspaceCard({ current, index }));
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const lines = Object.entries(named).map(
|
|
2685
|
+
([key, value]) => `- **${key}** \u2192 \`${value}\`${value === current ? " \u2190 \u5F53\u524D" : ""}`
|
|
2686
|
+
);
|
|
2687
|
+
await reply(
|
|
2688
|
+
ctx,
|
|
2689
|
+
[
|
|
2690
|
+
`\u5F53\u524D cwd\uFF1A\`${current}\``,
|
|
2691
|
+
"",
|
|
2692
|
+
...lines.length > 0 ? lines : ["\u6682\u65E0\u547D\u540D\u5DE5\u4F5C\u7A7A\u95F4\u3002"]
|
|
2693
|
+
].join("\n")
|
|
2694
|
+
);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (sub === "save") {
|
|
2698
|
+
if (!name) {
|
|
2699
|
+
await reply(ctx, "\u7528\u6CD5\uFF1A`/ws save <name>`");
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
|
|
2703
|
+
ctx.workspaces.saveNamed(name, current);
|
|
2704
|
+
await reply(ctx, `\u5DF2\u4FDD\u5B58\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${current}\``);
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
if (sub === "use") {
|
|
2708
|
+
if (!name) {
|
|
2709
|
+
await reply(ctx, "\u7528\u6CD5\uFF1A`/ws use <name>`");
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
const cwd = ctx.workspaces.getNamed(name);
|
|
2713
|
+
if (!cwd) {
|
|
2714
|
+
await reply(ctx, `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`);
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
await ctx.activeRuns.interrupt(ctx.scope);
|
|
2718
|
+
ctx.workspaces.setCwd(ctx.scope, cwd);
|
|
2719
|
+
ctx.workspaces.touchNamed(name);
|
|
2720
|
+
ctx.sessions.clear(ctx.scope);
|
|
2721
|
+
await reply(ctx, `\u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${cwd}\``);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
if (sub === "remove") {
|
|
2725
|
+
if (!name) {
|
|
2726
|
+
await reply(ctx, "\u7528\u6CD5\uFF1A`/ws remove <name>`");
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
const removed = ctx.workspaces.removeNamed(name);
|
|
2730
|
+
await reply(ctx, removed ? `\u5DF2\u5220\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**` : `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
await reply(ctx, "\u672A\u77E5 `/ws` \u5B50\u547D\u4EE4\uFF0C\u8BF7\u4F7F\u7528 list / save / use / remove\u3002");
|
|
1307
2734
|
}
|
|
1308
|
-
function
|
|
1309
|
-
const
|
|
1310
|
-
|
|
2735
|
+
async function handleStatus(_args, ctx) {
|
|
2736
|
+
const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
|
|
2737
|
+
const session = ctx.sessions.getRaw(ctx.scope)?.sessionId ?? "(\u65E0)";
|
|
2738
|
+
const running = Boolean(ctx.activeRuns.get(ctx.scope));
|
|
2739
|
+
const scopeLabel = ctx.chatMode === "topic" ? `${ctx.scope}\uFF08\u8BDD\u9898\u72EC\u7ACB session\uFF09` : ctx.scope;
|
|
2740
|
+
await reply(
|
|
2741
|
+
ctx,
|
|
2742
|
+
[
|
|
2743
|
+
`\u{1F9ED} **scope**: \`${scopeLabel}\``,
|
|
2744
|
+
`\u{1F4C1} **cwd**: \`${cwd}\``,
|
|
2745
|
+
`\u{1F517} **session**: \`${session}\``,
|
|
2746
|
+
`\u{1F3C3} **active run**: ${running ? "yes" : "no"}`
|
|
2747
|
+
].join("\n")
|
|
2748
|
+
);
|
|
1311
2749
|
}
|
|
1312
|
-
function
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
if (
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
2750
|
+
async function handleResume(_args, ctx) {
|
|
2751
|
+
const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
|
|
2752
|
+
const history = ctx.sessions.historyFor(ctx.scope, cwd);
|
|
2753
|
+
if (history.length === 0) {
|
|
2754
|
+
await reply(ctx, "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u5386\u53F2\u4E0A\u4E0B\u6587\u3002");
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
const recent = history.slice(-6).map((message) => {
|
|
2758
|
+
const speaker = message.role === "user" ? "\u{1F464}" : "\u{1F916}";
|
|
2759
|
+
return `${speaker} ${message.content.slice(0, 300)}`;
|
|
2760
|
+
});
|
|
2761
|
+
await reply(ctx, [`\u5F53\u524D scope\uFF1A\`${ctx.scope}\``, "", ...recent].join("\n"));
|
|
1320
2762
|
}
|
|
1321
|
-
function
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
text: { tag: "plain_text", content: "\u23F9 \u7EC8\u6B62" },
|
|
1325
|
-
type: "danger",
|
|
1326
|
-
value: { cmd: "stop" }
|
|
1327
|
-
};
|
|
2763
|
+
async function handleStop(_args, ctx) {
|
|
2764
|
+
const stopped = await ctx.activeRuns.interrupt(ctx.scope);
|
|
2765
|
+
await reply(ctx, stopped ? "\u5DF2\u8BF7\u6C42\u7EC8\u6B62\u5F53\u524D\u4EFB\u52A1\u3002" : "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u3002");
|
|
1328
2766
|
}
|
|
1329
|
-
function
|
|
1330
|
-
|
|
2767
|
+
async function handleTimeout(args, ctx) {
|
|
2768
|
+
const input = args.trim();
|
|
2769
|
+
const effectiveMs = ctx.runPolicies.get(ctx.scope) ?? ctx.defaultRunTimeoutMs;
|
|
2770
|
+
if (!input) {
|
|
2771
|
+
const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
|
|
2772
|
+
await reply(
|
|
2773
|
+
ctx,
|
|
2774
|
+
minutes2 > 0 ? `\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A${minutes2} \u5206\u949F\u3002\u53EF\u7528 \`/timeout <N|off|default>\` \u8C03\u6574\u3002` : "\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A\u5173\u95ED\u3002"
|
|
2775
|
+
);
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
if (input === "off") {
|
|
2779
|
+
ctx.runPolicies.set(ctx.scope, 0);
|
|
2780
|
+
await reply(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\u3002");
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
if (input === "default") {
|
|
2784
|
+
ctx.runPolicies.clear(ctx.scope);
|
|
2785
|
+
await reply(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u8FD0\u884C\u8D85\u65F6\u3002");
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
const minutes = Number(input);
|
|
2789
|
+
if (!Number.isInteger(minutes) || minutes <= 0) {
|
|
2790
|
+
await reply(ctx, "\u7528\u6CD5\uFF1A`/timeout <N|off|default>`\uFF0CN \u4E3A\u5927\u4E8E 0 \u7684\u5206\u949F\u6570\u3002");
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
ctx.runPolicies.set(ctx.scope, minutes * 6e4);
|
|
2794
|
+
await reply(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u8FD0\u884C\u8D85\u65F6\uFF1A${minutes} \u5206\u949F\u3002`);
|
|
1331
2795
|
}
|
|
1332
|
-
function
|
|
1333
|
-
const
|
|
1334
|
-
|
|
2796
|
+
async function handleDensity(args, ctx) {
|
|
2797
|
+
const input = args.trim().toLowerCase();
|
|
2798
|
+
if (!input) {
|
|
2799
|
+
const current = ctx.densityStore?.get(ctx.scope) ?? "standard";
|
|
2800
|
+
await reply(
|
|
2801
|
+
ctx,
|
|
2802
|
+
`\u5F53\u524D\u5361\u7247\u5BC6\u5EA6\uFF1A**${current}**\u3002\u53EF\u7528 \`/density compact|standard|detailed\` \u8C03\u6574\u3002`
|
|
2803
|
+
);
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
if (input === "default") {
|
|
2807
|
+
ctx.densityStore?.clear(ctx.scope);
|
|
2808
|
+
await reply(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u5361\u7247\u5BC6\u5EA6\u3002");
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const density = parseCardDensity(input);
|
|
2812
|
+
if (!density) {
|
|
2813
|
+
await reply(ctx, "\u7528\u6CD5\uFF1A`/density [compact|standard|detailed|default]`");
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
ctx.densityStore?.set(ctx.scope, density);
|
|
2817
|
+
await reply(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u5361\u7247\u5BC6\u5EA6\uFF1A**${density}**\u3002`);
|
|
1335
2818
|
}
|
|
1336
|
-
function
|
|
1337
|
-
const
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
2819
|
+
async function handleAsk(args, ctx) {
|
|
2820
|
+
const question = args.trim();
|
|
2821
|
+
if (!question) {
|
|
2822
|
+
await reply(ctx, "\u7528\u6CD5\uFF1A`/ask <\u95EE\u9898>`");
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
if (!ctx.questions) {
|
|
2826
|
+
await reply(ctx, "\u95EE\u7B54\u5361\u672A\u542F\u7528\uFF08\u8BF7\u786E\u8BA4 questions \u5DF2\u63A5\u7EBF\uFF09\u3002");
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
const answer = await questionHandlerFor({
|
|
2830
|
+
questions: ctx.questions,
|
|
2831
|
+
channel: ctx.channel,
|
|
2832
|
+
chatId: ctx.chatId,
|
|
2833
|
+
scope: ctx.scope
|
|
2834
|
+
})({
|
|
2835
|
+
kind: "text",
|
|
2836
|
+
question,
|
|
2837
|
+
id: ""
|
|
2838
|
+
});
|
|
2839
|
+
if (answer !== void 0) {
|
|
2840
|
+
const text = Array.isArray(answer) ? answer.join("\u3001") : answer;
|
|
2841
|
+
ctx.sessions.recordExchange(ctx.scope, ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace, [text], void 0);
|
|
2842
|
+
await reply(ctx, `\u5DF2\u8BB0\u5F55\u4F60\u7684\u56DE\u7B54\uFF0C\u5E76\u5199\u5165\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002`);
|
|
2843
|
+
} else {
|
|
2844
|
+
await reply(ctx, "\u672A\u6536\u5230\u56DE\u7B54\uFF08\u5361\u7247\u53EF\u80FD\u5DF2\u8D85\u65F6\u6216\u88AB\u5FFD\u7565\uFF09\u3002");
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
async function handleInvite(args, ctx) {
|
|
2848
|
+
const [kind, ...rest] = args.trim().split(/\s+/);
|
|
2849
|
+
const id = rest.join(" ").trim();
|
|
2850
|
+
if (kind === "list") {
|
|
2851
|
+
const snapshot = ctx.accessManager.snapshot();
|
|
2852
|
+
await reply(
|
|
2853
|
+
ctx,
|
|
2854
|
+
[
|
|
2855
|
+
"**\u8BBF\u95EE\u767D\u540D\u5355**",
|
|
2856
|
+
`users: ${snapshot.allowedUsers.join(", ") || "(\u7A7A)"}`,
|
|
2857
|
+
`chats: ${snapshot.allowedChats.join(", ") || "(\u7A7A)"}`,
|
|
2858
|
+
`admins: ${snapshot.admins.join(", ") || "(\u7A7A)"}`
|
|
2859
|
+
].join("\n")
|
|
1343
2860
|
);
|
|
2861
|
+
return;
|
|
1344
2862
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
2863
|
+
if (!kind || !id) {
|
|
2864
|
+
await reply(
|
|
2865
|
+
ctx,
|
|
2866
|
+
"\u7528\u6CD5\uFF1A`/invite user|admin|group <id>`\u3001`/invite list`\u3001`/invite remove user|group <id>`"
|
|
2867
|
+
);
|
|
2868
|
+
return;
|
|
1347
2869
|
}
|
|
1348
|
-
if (
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
} else if (state.terminal === "error" && state.errorMsg) {
|
|
1353
|
-
elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
|
|
1354
|
-
} else if (state.terminal === "done" && elements.length === 0) {
|
|
1355
|
-
elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
|
|
2870
|
+
if (kind === "user") {
|
|
2871
|
+
await ctx.accessManager.addUser(id);
|
|
2872
|
+
await reply(ctx, `\u5DF2\u5141\u8BB8\u7528\u6237\uFF1A\`${id}\``);
|
|
2873
|
+
return;
|
|
1356
2874
|
}
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1359
|
-
|
|
2875
|
+
if (kind === "admin") {
|
|
2876
|
+
await ctx.accessManager.addAdmin(id);
|
|
2877
|
+
await reply(ctx, `\u5DF2\u8BBE\u4E3A\u7BA1\u7406\u5458\uFF1A\`${id}\``);
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
if (kind === "group") {
|
|
2881
|
+
await ctx.accessManager.addChat(id);
|
|
2882
|
+
await reply(ctx, `\u5DF2\u5141\u8BB8\u7FA4\u804A\uFF1A\`${id}\``);
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
if (kind === "remove") {
|
|
2886
|
+
const [sub, target] = rest;
|
|
2887
|
+
if (sub === "user" && target) {
|
|
2888
|
+
await ctx.accessManager.removeUser(target);
|
|
2889
|
+
await reply(ctx, `\u5DF2\u79FB\u9664\u7528\u6237\uFF1A\`${target}\``);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (sub === "group" && target) {
|
|
2893
|
+
await ctx.accessManager.removeChat(target);
|
|
2894
|
+
await reply(ctx, `\u5DF2\u79FB\u9664\u7FA4\u804A\uFF1A\`${target}\``);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
await reply(ctx, "\u7528\u6CD5\uFF1A`/invite remove user <id>` \u6216 `/invite remove group <chatId>`");
|
|
2898
|
+
return;
|
|
1360
2899
|
}
|
|
2900
|
+
await reply(ctx, "\u672A\u77E5 `/invite` \u7C7B\u578B\uFF0C\u8BF7\u4F7F\u7528 user / admin / group / list / remove\u3002");
|
|
2901
|
+
}
|
|
2902
|
+
async function handleHelp(_args, ctx) {
|
|
2903
|
+
await reply(ctx, HELP);
|
|
2904
|
+
}
|
|
2905
|
+
var handlers = {
|
|
2906
|
+
"/new": handleNew,
|
|
2907
|
+
"/reset": handleNew,
|
|
2908
|
+
"/cd": handleCd,
|
|
2909
|
+
"/ws": handleWs,
|
|
2910
|
+
"/status": handleStatus,
|
|
2911
|
+
"/resume": handleResume,
|
|
2912
|
+
"/stop": handleStop,
|
|
2913
|
+
"/timeout": handleTimeout,
|
|
2914
|
+
"/density": handleDensity,
|
|
2915
|
+
"/ask": handleAsk,
|
|
2916
|
+
"/invite": handleInvite,
|
|
2917
|
+
"/help": handleHelp
|
|
2918
|
+
};
|
|
2919
|
+
async function tryHandleCommand(text, ctx) {
|
|
2920
|
+
const trimmed = text.trim();
|
|
2921
|
+
if (!trimmed.startsWith("/")) return false;
|
|
2922
|
+
const [command, ...rest] = trimmed.split(/\s+/);
|
|
2923
|
+
const handler = handlers[command ?? ""];
|
|
2924
|
+
if (!handler) return false;
|
|
2925
|
+
await handler(rest.join(" "), ctx);
|
|
2926
|
+
return true;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
// src/bridge/lark-channel.ts
|
|
2930
|
+
function adaptLarkChannel(channel) {
|
|
2931
|
+
const base = {
|
|
2932
|
+
async sendMarkdown(chatId, markdown2, options) {
|
|
2933
|
+
const sendOptions = {};
|
|
2934
|
+
if (options?.replyTo) sendOptions.replyTo = options.replyTo;
|
|
2935
|
+
await channel.send(chatId, { markdown: markdown2 }, sendOptions);
|
|
2936
|
+
}
|
|
2937
|
+
};
|
|
1361
2938
|
return {
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
summary: { content: summaryText(state) }
|
|
2939
|
+
...base,
|
|
2940
|
+
async sendCard(chatId, card) {
|
|
2941
|
+
await channel.send(chatId, { card }, {});
|
|
1366
2942
|
},
|
|
1367
|
-
|
|
2943
|
+
async streamCard(chatId, initial, producer, options) {
|
|
2944
|
+
const sendOptions = {};
|
|
2945
|
+
if (options?.replyTo) sendOptions.replyTo = options.replyTo;
|
|
2946
|
+
await channel.stream(
|
|
2947
|
+
chatId,
|
|
2948
|
+
{
|
|
2949
|
+
card: {
|
|
2950
|
+
initial,
|
|
2951
|
+
producer: async (controller) => {
|
|
2952
|
+
await producer(controller);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
},
|
|
2956
|
+
sendOptions
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
1368
2959
|
};
|
|
1369
2960
|
}
|
|
1370
2961
|
|
|
1371
|
-
// src/bridge/
|
|
1372
|
-
async function
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
2962
|
+
// src/bridge/channel.ts
|
|
2963
|
+
async function startChannel(deps) {
|
|
2964
|
+
const channel = (deps.createChannel ?? createLarkChannel)({
|
|
2965
|
+
appId: deps.appId,
|
|
2966
|
+
appSecret: deps.appSecret,
|
|
2967
|
+
domain: deps.tenant === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn",
|
|
2968
|
+
source: "dsh-lark-bot",
|
|
2969
|
+
policy: {
|
|
2970
|
+
dmMode: deps.allowedUsers?.length ? "allowlist" : deps.accessDefaultDeny === true ? "disabled" : "open",
|
|
2971
|
+
requireMention: true,
|
|
2972
|
+
respondToMentionAll: false,
|
|
2973
|
+
...deps.allowedUsers ? { dmAllowlist: deps.allowedUsers } : {},
|
|
2974
|
+
...deps.allowedChats ? { groupAllowlist: deps.allowedChats } : {}
|
|
2975
|
+
},
|
|
2976
|
+
safety: {
|
|
2977
|
+
chatQueue: { enabled: false }
|
|
2978
|
+
},
|
|
2979
|
+
outbound: {
|
|
2980
|
+
streamThrottleMs: 400
|
|
2981
|
+
},
|
|
2982
|
+
includeRawEvent: true,
|
|
2983
|
+
resolveChatMode: true,
|
|
2984
|
+
handshakeTimeoutMs: 8e3,
|
|
2985
|
+
httpTimeoutMs: 3e4,
|
|
2986
|
+
respectProxyEnv: true
|
|
1395
2987
|
});
|
|
1396
|
-
|
|
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
|
-
}) : void 0;
|
|
1430
|
-
try {
|
|
1431
|
-
if (timeoutPromise) {
|
|
1432
|
-
await Promise.race([consume(), timeoutPromise]);
|
|
1433
|
-
} else {
|
|
1434
|
-
await consume();
|
|
1435
|
-
}
|
|
1436
|
-
state = timedOut ? markIdleTimeout(state, timeoutMs / 6e4) : finalizeIfRunning(state);
|
|
1437
|
-
await controller.update(renderCard(state));
|
|
1438
|
-
} finally {
|
|
1439
|
-
if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
|
|
1440
|
-
}
|
|
1441
|
-
},
|
|
1442
|
-
replyOptions
|
|
1443
|
-
);
|
|
1444
|
-
input.sessions.recordExchange(input.scope, cwd, input.messages, assistantOutput);
|
|
1445
|
-
} catch (error) {
|
|
1446
|
-
log.fail("run-flow", error, { scope: input.scope, runId });
|
|
1447
|
-
state = markInterrupted(state);
|
|
1448
|
-
try {
|
|
1449
|
-
await input.channel.sendMarkdown(input.chatId, `\u26A0\uFE0F agent \u8FD0\u884C\u5931\u8D25\uFF1A${errorMessage(error)}`, {
|
|
1450
|
-
...replyOptions
|
|
2988
|
+
const streaming = adaptLarkChannel(channel);
|
|
2989
|
+
const commandChannel = streaming;
|
|
2990
|
+
channel.on({
|
|
2991
|
+
message: async (msg) => {
|
|
2992
|
+
const scope = scopeForMessage(msg);
|
|
2993
|
+
if (deps.eventFreshnessMs !== void 0 && deps.eventFreshnessMs > 0 && !isEventFresh(msg.createTime, deps.eventFreshnessMs)) {
|
|
2994
|
+
log.warn("channel", "stale-message-dropped", {
|
|
2995
|
+
scope,
|
|
2996
|
+
ageMs: Date.now() - msg.createTime
|
|
2997
|
+
});
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
const context = {
|
|
3001
|
+
scope,
|
|
3002
|
+
chatId: msg.chatId,
|
|
3003
|
+
messageId: msg.messageId,
|
|
3004
|
+
threadId: msg.threadId,
|
|
3005
|
+
chatMode: msg.chatMode ?? msg.chatType,
|
|
3006
|
+
sessions: deps.sessions,
|
|
3007
|
+
workspaces: deps.workspaces,
|
|
3008
|
+
activeRuns: deps.activeRuns,
|
|
3009
|
+
runPolicies: deps.runPolicies,
|
|
3010
|
+
defaultRunTimeoutMs: deps.defaultRunTimeoutMs,
|
|
3011
|
+
accessManager: deps.accessManager,
|
|
3012
|
+
approvals: deps.approvals,
|
|
3013
|
+
questions: deps.questions,
|
|
3014
|
+
densityStore: deps.densityStore,
|
|
3015
|
+
channel: commandChannel,
|
|
3016
|
+
defaultWorkspace: deps.defaultWorkspace
|
|
3017
|
+
};
|
|
3018
|
+
const handled = await tryHandleCommand(msg.content, context).catch((error) => {
|
|
3019
|
+
log.fail("channel-command", error, { scope });
|
|
3020
|
+
return false;
|
|
1451
3021
|
});
|
|
1452
|
-
|
|
3022
|
+
if (!handled) deps.pending.push(scope, msg);
|
|
3023
|
+
},
|
|
3024
|
+
cardAction: async (event) => {
|
|
3025
|
+
const value = event.action.value && typeof event.action.value === "object" ? event.action.value : void 0;
|
|
3026
|
+
const scope = event.raw ? await resolveCardScope(event.chatId, event.raw) : event.chatId;
|
|
3027
|
+
if (value?.cmd === "stop") {
|
|
3028
|
+
await deps.activeRuns.interrupt(scope);
|
|
3029
|
+
return;
|
|
3030
|
+
}
|
|
3031
|
+
if (value?.cmd === "approve" && typeof value.id === "string" && deps.approvals) {
|
|
3032
|
+
const outcome = value.outcome === "allow" ? "allowed-once" : "rejected";
|
|
3033
|
+
deps.approvals.resolve(scope, value.id, outcome);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
if (value?.cmd === "question-submit" && typeof value.id === "string" && deps.questions) {
|
|
3037
|
+
const question = deps.questions.get(scope, value.id);
|
|
3038
|
+
const form = event.action.formValue;
|
|
3039
|
+
if (question) {
|
|
3040
|
+
const answer = extractQuestionAnswer(
|
|
3041
|
+
question.kind,
|
|
3042
|
+
form?.answer,
|
|
3043
|
+
question.options
|
|
3044
|
+
);
|
|
3045
|
+
deps.questions.resolve(scope, value.id, answer);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
},
|
|
3049
|
+
reconnecting: () => {
|
|
3050
|
+
log.warn("channel", "reconnecting", {});
|
|
3051
|
+
},
|
|
3052
|
+
reconnected: () => {
|
|
3053
|
+
log.info("channel", "reconnected", {});
|
|
3054
|
+
},
|
|
3055
|
+
error: (error) => {
|
|
3056
|
+
log.fail("channel", error);
|
|
1453
3057
|
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
const transcript = history.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.content}`).join("\n\n");
|
|
1461
|
-
return [
|
|
1462
|
-
"Continue the conversation using the history below.",
|
|
1463
|
-
"",
|
|
1464
|
-
transcript,
|
|
1465
|
-
"",
|
|
1466
|
-
`Current user message:
|
|
1467
|
-
${messages.join("\n\n")}`
|
|
1468
|
-
].join("\n");
|
|
3058
|
+
});
|
|
3059
|
+
await channel.connect();
|
|
3060
|
+
return {
|
|
3061
|
+
channel,
|
|
3062
|
+
disconnect: () => channel.disconnect()
|
|
3063
|
+
};
|
|
1469
3064
|
}
|
|
1470
|
-
function
|
|
1471
|
-
if (
|
|
1472
|
-
|
|
1473
|
-
return markInterrupted(state);
|
|
1474
|
-
}
|
|
1475
|
-
if (event.type === "error" && event.terminationReason === "timeout") {
|
|
1476
|
-
return markIdleTimeout(state, 0);
|
|
1477
|
-
}
|
|
1478
|
-
return reduce(state, event);
|
|
3065
|
+
function scopeForMessage(msg) {
|
|
3066
|
+
if (msg.chatMode === "topic" && msg.threadId) return `${msg.chatId}:${msg.threadId}`;
|
|
3067
|
+
return msg.chatId;
|
|
1479
3068
|
}
|
|
1480
|
-
function
|
|
1481
|
-
return
|
|
3069
|
+
async function resolveCardScope(chatId, raw) {
|
|
3070
|
+
return raw.message?.thread_id ? `${chatId}:${raw.message.thread_id}` : chatId;
|
|
1482
3071
|
}
|
|
1483
3072
|
|
|
1484
3073
|
// src/config/access-manager.ts
|
|
@@ -1587,15 +3176,21 @@ async function onboardPersonalAgent(deps = {}) {
|
|
|
1587
3176
|
}
|
|
1588
3177
|
|
|
1589
3178
|
// src/media/attachments.ts
|
|
1590
|
-
import { mkdir as
|
|
1591
|
-
import { join as
|
|
3179
|
+
import { mkdir as mkdir5, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
3180
|
+
import { join as join7 } from "path";
|
|
1592
3181
|
var MAX_TEXT_FILE_BYTES = 256e3;
|
|
3182
|
+
function assertSafeMediaName(mediaDir, destination) {
|
|
3183
|
+
if (!isPathWithin(mediaDir, destination)) {
|
|
3184
|
+
throw new Error(`unsafe attachment destination rejected: ${destination}`);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
1593
3187
|
async function prepareAttachments(channel, message, mediaDir) {
|
|
1594
|
-
await
|
|
3188
|
+
await mkdir5(mediaDir, { recursive: true });
|
|
1595
3189
|
const result = { imagePaths: [], textFileNotes: [] };
|
|
1596
3190
|
for (const resource of message.resources) {
|
|
1597
3191
|
if (!channel || resource.type !== "image" && resource.type !== "file") continue;
|
|
1598
|
-
const destination =
|
|
3192
|
+
const destination = join7(mediaDir, `${message.messageId}-${resource.fileKey}`);
|
|
3193
|
+
assertSafeMediaName(mediaDir, destination);
|
|
1599
3194
|
await channel.downloadResourceToFile(
|
|
1600
3195
|
message.messageId,
|
|
1601
3196
|
resource.fileKey,
|
|
@@ -1612,9 +3207,10 @@ async function prepareAttachments(channel, message, mediaDir) {
|
|
|
1612
3207
|
continue;
|
|
1613
3208
|
}
|
|
1614
3209
|
const content = await readFile2(destination, "utf8");
|
|
3210
|
+
const safeContent = truncateUtf8Safe(content, MAX_TEXT_FILE_BYTES);
|
|
1615
3211
|
result.textFileNotes.push(
|
|
1616
3212
|
`[attachment: ${resource.fileName ?? resource.fileKey}]
|
|
1617
|
-
${
|
|
3213
|
+
${safeContent}`
|
|
1618
3214
|
);
|
|
1619
3215
|
}
|
|
1620
3216
|
return result;
|
|
@@ -1735,8 +3331,8 @@ var SessionStore = class {
|
|
|
1735
3331
|
// src/workspace/git-worktree.ts
|
|
1736
3332
|
import { execFile } from "child_process";
|
|
1737
3333
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1738
|
-
import { access, copyFile, mkdir as
|
|
1739
|
-
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";
|
|
1740
3336
|
import { promisify } from "util";
|
|
1741
3337
|
var execFileAsync = promisify(execFile);
|
|
1742
3338
|
async function defaultRunGit(args, cwd) {
|
|
@@ -1755,11 +3351,11 @@ async function exists(path) {
|
|
|
1755
3351
|
}
|
|
1756
3352
|
}
|
|
1757
3353
|
async function defaultCopyRulesFile(source, target) {
|
|
1758
|
-
await
|
|
3354
|
+
await mkdir6(dirname3(target), { recursive: true });
|
|
1759
3355
|
await copyFile(source, target);
|
|
1760
3356
|
}
|
|
1761
3357
|
function slugify(scope) {
|
|
1762
|
-
const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
3358
|
+
const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[.]+/, "").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
1763
3359
|
return slug || "session";
|
|
1764
3360
|
}
|
|
1765
3361
|
var GitWorktreeManager = class {
|
|
@@ -1775,13 +3371,16 @@ var GitWorktreeManager = class {
|
|
|
1775
3371
|
const isGit = await this.isGitRepository(base);
|
|
1776
3372
|
if (!isGit) return { cwd: base, created: false };
|
|
1777
3373
|
const slug = slugify(scope);
|
|
1778
|
-
const target =
|
|
3374
|
+
const target = join8(this.options.worktreesRoot, slug);
|
|
3375
|
+
if (!isPathWithin(this.options.worktreesRoot, target)) {
|
|
3376
|
+
throw new Error(`unsafe worktree target rejected: ${target}`);
|
|
3377
|
+
}
|
|
1779
3378
|
if (await exists(target)) {
|
|
1780
3379
|
await this.ensureProjectRules(base, target);
|
|
1781
3380
|
return { cwd: target, created: false };
|
|
1782
3381
|
}
|
|
1783
3382
|
const branch = `dsh-lark/${slug}-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`;
|
|
1784
|
-
await
|
|
3383
|
+
await mkdir6(this.options.worktreesRoot, { recursive: true });
|
|
1785
3384
|
await this.runGit(["worktree", "add", "-b", branch, target, "HEAD"], base);
|
|
1786
3385
|
await this.ensureProjectRules(base, target);
|
|
1787
3386
|
return { cwd: target, created: true, branch };
|
|
@@ -1795,9 +3394,9 @@ var GitWorktreeManager = class {
|
|
|
1795
3394
|
}
|
|
1796
3395
|
}
|
|
1797
3396
|
async ensureProjectRules(base, target) {
|
|
1798
|
-
const destination =
|
|
3397
|
+
const destination = join8(target, "AGENTS.md");
|
|
1799
3398
|
if (await exists(destination)) return;
|
|
1800
|
-
const sourceCandidates = [
|
|
3399
|
+
const sourceCandidates = [join8(base, ".dsh-lark", "AGENTS.md"), join8(base, "AGENTS.md")];
|
|
1801
3400
|
for (const source of sourceCandidates) {
|
|
1802
3401
|
if (await exists(source)) {
|
|
1803
3402
|
await this.copyRulesFile(source, destination);
|
|
@@ -1947,24 +3546,31 @@ async function runStart(options) {
|
|
|
1947
3546
|
return;
|
|
1948
3547
|
}
|
|
1949
3548
|
const defaultWorkspace = options.workspace ?? activeProfile.workspaces.default ?? env.workspace ?? paths.profilePath(profileName, "workspace");
|
|
1950
|
-
await
|
|
3549
|
+
await mkdir7(defaultWorkspace, { recursive: true });
|
|
1951
3550
|
const sessions = new SessionStore(paths.sessionsFile(profileName));
|
|
1952
3551
|
const workspaces = new WorkspaceStore(paths.workspacesFile(profileName));
|
|
1953
3552
|
const worktreeManager = new GitWorktreeManager({
|
|
1954
3553
|
worktreesRoot: paths.profilePath(profileName, "worktrees")
|
|
1955
3554
|
});
|
|
1956
3555
|
await Promise.all([sessions.load(), workspaces.load()]);
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
3556
|
+
let adapter;
|
|
3557
|
+
try {
|
|
3558
|
+
adapter = await buildAgentAdapter(env, {
|
|
3559
|
+
stopGraceMs: activeProfile.preferences.stopGraceMs ?? env.stopGraceMs,
|
|
3560
|
+
model: activeProfile.preferences.model
|
|
3561
|
+
});
|
|
3562
|
+
} catch (error) {
|
|
3563
|
+
log.fail("adapter", error);
|
|
3564
|
+
process.stderr.write(`agent adapter \u521D\u59CB\u5316\u5931\u8D25\uFF1A${errorMessage2(error)}
|
|
3565
|
+
`);
|
|
3566
|
+
process.exitCode = 1;
|
|
3567
|
+
return;
|
|
1964
3568
|
}
|
|
1965
|
-
const adapter = new DshAdapter(adapterOptions);
|
|
1966
3569
|
const activeRuns = new ActiveRuns();
|
|
1967
3570
|
const runPolicies = new RunPolicyStore();
|
|
3571
|
+
const approvals = new ApprovalRegistry();
|
|
3572
|
+
const questions = new QuestionRegistry();
|
|
3573
|
+
const densityStore = new DensityStore();
|
|
1968
3574
|
let streaming;
|
|
1969
3575
|
let larkChannel;
|
|
1970
3576
|
const pending = new PendingQueue(DEBOUNCE_MS, async (scope, batch) => {
|
|
@@ -1992,6 +3598,9 @@ async function runStart(options) {
|
|
|
1992
3598
|
workspaceManager: worktreeManager,
|
|
1993
3599
|
activeRuns,
|
|
1994
3600
|
runPolicies,
|
|
3601
|
+
approvals,
|
|
3602
|
+
questions,
|
|
3603
|
+
densityStore,
|
|
1995
3604
|
channel: streaming,
|
|
1996
3605
|
defaultWorkspace,
|
|
1997
3606
|
replyTo: first.messageId,
|
|
@@ -2016,10 +3625,15 @@ async function runStart(options) {
|
|
|
2016
3625
|
workspaces,
|
|
2017
3626
|
activeRuns,
|
|
2018
3627
|
runPolicies,
|
|
3628
|
+
approvals,
|
|
3629
|
+
questions,
|
|
3630
|
+
densityStore,
|
|
2019
3631
|
defaultRunTimeoutMs: activeProfile.preferences.runTimeoutMs ?? env.runTimeoutMs,
|
|
2020
3632
|
accessManager,
|
|
2021
3633
|
pending,
|
|
2022
3634
|
defaultWorkspace,
|
|
3635
|
+
accessDefaultDeny: env.accessDefaultDeny,
|
|
3636
|
+
eventFreshnessMs: env.eventFreshnessMs,
|
|
2023
3637
|
allowedUsers: activeProfile.access.allowedUsers,
|
|
2024
3638
|
allowedChats: activeProfile.access.allowedChats
|
|
2025
3639
|
};
|
|
@@ -2040,14 +3654,18 @@ async function runStart(options) {
|
|
|
2040
3654
|
`);
|
|
2041
3655
|
await waitForShutdown();
|
|
2042
3656
|
await bridge.disconnect();
|
|
3657
|
+
await adapter.dispose?.();
|
|
2043
3658
|
await Promise.all([sessions.flush(), workspaces.flush()]);
|
|
2044
3659
|
}
|
|
3660
|
+
function errorMessage2(error) {
|
|
3661
|
+
return error instanceof Error ? error.message : String(error);
|
|
3662
|
+
}
|
|
2045
3663
|
function waitForShutdown() {
|
|
2046
|
-
return new Promise((
|
|
3664
|
+
return new Promise((resolve5) => {
|
|
2047
3665
|
const shutdown = () => {
|
|
2048
3666
|
process.off("SIGINT", shutdown);
|
|
2049
3667
|
process.off("SIGTERM", shutdown);
|
|
2050
|
-
|
|
3668
|
+
resolve5();
|
|
2051
3669
|
};
|
|
2052
3670
|
process.once("SIGINT", shutdown);
|
|
2053
3671
|
process.once("SIGTERM", shutdown);
|