dsh-lark-bot 0.3.0 → 0.4.1
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 +34 -13
- package/SECURITY.md +4 -0
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +1018 -44
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -584,7 +584,8 @@ var init_acp_adapter = __esm({
|
|
|
584
584
|
});
|
|
585
585
|
|
|
586
586
|
// src/cli.ts
|
|
587
|
-
import { readFileSync } from "fs";
|
|
587
|
+
import { readFileSync, realpathSync as realpathSync2 } from "fs";
|
|
588
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
588
589
|
import { Command } from "commander";
|
|
589
590
|
|
|
590
591
|
// src/cli/commands/doctor.ts
|
|
@@ -1379,6 +1380,10 @@ function resolveAppPaths(root = defaultHome()) {
|
|
|
1379
1380
|
workspacesFile: (profile) => profilePath(profile, "workspaces.json"),
|
|
1380
1381
|
mediaDir: (profile) => profilePath(profile, "media"),
|
|
1381
1382
|
logsDir: (profile) => profilePath(profile, "logs"),
|
|
1383
|
+
serviceDir: join4(root, "service"),
|
|
1384
|
+
serviceEnvFile: join4(root, "service", "service.env"),
|
|
1385
|
+
serviceMetadataFile: join4(root, "service", "service.json"),
|
|
1386
|
+
serviceLogFile: (profile) => profilePath(profile, "logs", "bot.log"),
|
|
1382
1387
|
registryFile: join4(root, "registry", "processes.json"),
|
|
1383
1388
|
locksDir: join4(root, "registry", "locks")
|
|
1384
1389
|
};
|
|
@@ -1517,9 +1522,9 @@ async function writeFileAtomic(target, data, options = {}) {
|
|
|
1517
1522
|
}
|
|
1518
1523
|
}
|
|
1519
1524
|
async function rmSilently(path) {
|
|
1520
|
-
const { rm } = await import("fs/promises");
|
|
1525
|
+
const { rm: rm3 } = await import("fs/promises");
|
|
1521
1526
|
try {
|
|
1522
|
-
await
|
|
1527
|
+
await rm3(path, { force: true });
|
|
1523
1528
|
} catch {
|
|
1524
1529
|
}
|
|
1525
1530
|
}
|
|
@@ -1690,7 +1695,7 @@ async function runDoctor(options) {
|
|
|
1690
1695
|
if (critical) process.exitCode = 1;
|
|
1691
1696
|
}
|
|
1692
1697
|
|
|
1693
|
-
// src/cli/commands/
|
|
1698
|
+
// src/cli/commands/run.ts
|
|
1694
1699
|
import { mkdir as mkdir7 } from "fs/promises";
|
|
1695
1700
|
|
|
1696
1701
|
// src/bot/active-runs.ts
|
|
@@ -3488,21 +3493,10 @@ var WorkspaceStore = class {
|
|
|
3488
3493
|
}
|
|
3489
3494
|
};
|
|
3490
3495
|
|
|
3491
|
-
// src/cli/commands/
|
|
3496
|
+
// src/cli/commands/run.ts
|
|
3492
3497
|
var DEBOUNCE_MS = 600;
|
|
3493
|
-
async function
|
|
3494
|
-
const env =
|
|
3495
|
-
...process.env,
|
|
3496
|
-
...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
|
|
3497
|
-
...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
|
|
3498
|
-
...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
|
|
3499
|
-
...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
|
|
3500
|
-
});
|
|
3501
|
-
const paths = resolveAppPaths(env.home);
|
|
3502
|
-
const profileName = options.profile ?? "default";
|
|
3503
|
-
const configStore = new ConfigStore(paths.configFile);
|
|
3504
|
-
await configStore.load();
|
|
3505
|
-
const accessManager = new AccessManager(configStore, profileName);
|
|
3498
|
+
async function ensureBotProfile(store, options) {
|
|
3499
|
+
const { env, profileName } = options;
|
|
3506
3500
|
if (env.appId && env.appSecret) {
|
|
3507
3501
|
const profileInput = {
|
|
3508
3502
|
tenant: env.tenant,
|
|
@@ -3513,31 +3507,61 @@ async function runStart(options) {
|
|
|
3513
3507
|
runTimeoutMs: env.runTimeoutMs
|
|
3514
3508
|
};
|
|
3515
3509
|
if (env.workspace !== void 0) profileInput.workspace = env.workspace;
|
|
3516
|
-
await
|
|
3510
|
+
await store.saveProfile(profileName, profileInput);
|
|
3517
3511
|
}
|
|
3518
|
-
|
|
3519
|
-
if (!
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
process.stderr.write("\u626B\u7801\u521B\u5EFA\u5E94\u7528\u5931\u8D25\uFF0C\u672A\u5199\u5165\u672C\u5730\u914D\u7F6E\u3002\n");
|
|
3538
|
-
process.exitCode = 1;
|
|
3539
|
-
return;
|
|
3512
|
+
if (store.getProfile(profileName)) return true;
|
|
3513
|
+
if (!options.allowOnboarding) {
|
|
3514
|
+
process.stderr.write(
|
|
3515
|
+
"\u672A\u68C0\u6D4B\u5230\u98DE\u4E66 / Lark \u5E94\u7528\u51ED\u636E\u3002\u8BF7\u5148\u5728\u7EC8\u7AEF\u8FD0\u884C `dsh-lark-bot start` \u5B8C\u6210\u626B\u7801\u7ED1\u5B9A\u3002\n"
|
|
3516
|
+
);
|
|
3517
|
+
return false;
|
|
3518
|
+
}
|
|
3519
|
+
try {
|
|
3520
|
+
const created = await onboardPersonalAgent();
|
|
3521
|
+
const onboardingProfile = {
|
|
3522
|
+
tenant: created.tenant,
|
|
3523
|
+
appId: created.appId,
|
|
3524
|
+
appSecret: created.appSecret,
|
|
3525
|
+
model: env.model,
|
|
3526
|
+
stopGraceMs: env.stopGraceMs,
|
|
3527
|
+
runTimeoutMs: env.runTimeoutMs
|
|
3528
|
+
};
|
|
3529
|
+
if (created.operatorOpenId !== void 0) {
|
|
3530
|
+
onboardingProfile.operatorOpenId = created.operatorOpenId;
|
|
3540
3531
|
}
|
|
3532
|
+
if (env.workspace !== void 0) onboardingProfile.workspace = env.workspace;
|
|
3533
|
+
await store.saveProfile(profileName, onboardingProfile);
|
|
3534
|
+
return true;
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
log.fail("onboarding", error);
|
|
3537
|
+
process.stderr.write("\u626B\u7801\u521B\u5EFA\u5E94\u7528\u5931\u8D25\uFF0C\u672A\u5199\u5165\u672C\u5730\u914D\u7F6E\u3002\n");
|
|
3538
|
+
process.exitCode = 1;
|
|
3539
|
+
return false;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
function mergeStartEnv(options) {
|
|
3543
|
+
return {
|
|
3544
|
+
...process.env,
|
|
3545
|
+
...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
|
|
3546
|
+
...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
|
|
3547
|
+
...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
|
|
3548
|
+
...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
async function runBot(options) {
|
|
3552
|
+
const env = loadRuntimeEnv(mergeStartEnv(options));
|
|
3553
|
+
const paths = resolveAppPaths(env.home);
|
|
3554
|
+
const profileName = options.profile ?? "default";
|
|
3555
|
+
const configStore = new ConfigStore(paths.configFile);
|
|
3556
|
+
await configStore.load();
|
|
3557
|
+
const ready = await ensureBotProfile(configStore, {
|
|
3558
|
+
env,
|
|
3559
|
+
profileName,
|
|
3560
|
+
allowOnboarding: false
|
|
3561
|
+
});
|
|
3562
|
+
if (!ready) {
|
|
3563
|
+
process.exitCode = 1;
|
|
3564
|
+
return;
|
|
3541
3565
|
}
|
|
3542
3566
|
const activeProfile = configStore.getProfile(profileName);
|
|
3543
3567
|
if (!activeProfile) {
|
|
@@ -3629,7 +3653,7 @@ async function runStart(options) {
|
|
|
3629
3653
|
questions,
|
|
3630
3654
|
densityStore,
|
|
3631
3655
|
defaultRunTimeoutMs: activeProfile.preferences.runTimeoutMs ?? env.runTimeoutMs,
|
|
3632
|
-
accessManager,
|
|
3656
|
+
accessManager: new AccessManager(configStore, profileName),
|
|
3633
3657
|
pending,
|
|
3634
3658
|
defaultWorkspace,
|
|
3635
3659
|
accessDefaultDeny: env.accessDefaultDeny,
|
|
@@ -3672,28 +3696,978 @@ function waitForShutdown() {
|
|
|
3672
3696
|
});
|
|
3673
3697
|
}
|
|
3674
3698
|
|
|
3699
|
+
// src/service/manager.ts
|
|
3700
|
+
import { mkdir as mkdir12, readFile as readFile7 } from "fs/promises";
|
|
3701
|
+
import { dirname as dirname6 } from "path";
|
|
3702
|
+
|
|
3703
|
+
// src/service/command.ts
|
|
3704
|
+
import { execFile as execFile2 } from "child_process";
|
|
3705
|
+
import { basename, dirname as dirname4, join as join9 } from "path";
|
|
3706
|
+
import { fileURLToPath } from "url";
|
|
3707
|
+
var runCommand = (command, args, options) => new Promise((resolve5) => {
|
|
3708
|
+
execFile2(
|
|
3709
|
+
command,
|
|
3710
|
+
[...args],
|
|
3711
|
+
{
|
|
3712
|
+
env: options?.env,
|
|
3713
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
3714
|
+
windowsHide: true
|
|
3715
|
+
},
|
|
3716
|
+
(error, stdout, stderr) => {
|
|
3717
|
+
let code = 0;
|
|
3718
|
+
if (error) {
|
|
3719
|
+
const errorCode = error.code;
|
|
3720
|
+
code = typeof errorCode === "number" ? errorCode : 1;
|
|
3721
|
+
}
|
|
3722
|
+
resolve5({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
3723
|
+
}
|
|
3724
|
+
);
|
|
3725
|
+
});
|
|
3726
|
+
function resolveCliJsPath(metaUrl = import.meta.url) {
|
|
3727
|
+
const current = fileURLToPath(metaUrl);
|
|
3728
|
+
const name = basename(current);
|
|
3729
|
+
if (name === "cli.js" || name === "cli.mjs" || name === "cli.cjs") return current;
|
|
3730
|
+
return join9(dirname4(current), "..", "dist", "cli.js");
|
|
3731
|
+
}
|
|
3732
|
+
function slugify2(value) {
|
|
3733
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3734
|
+
return slug || "default";
|
|
3735
|
+
}
|
|
3736
|
+
function serviceNameFor(profile) {
|
|
3737
|
+
return profile === "default" ? "dsh-lark-bot" : `dsh-lark-bot-${slugify2(profile)}`;
|
|
3738
|
+
}
|
|
3739
|
+
function launchdLabelFor(profile) {
|
|
3740
|
+
return profile === "default" ? "io.dsh-lark-bot" : `io.dsh-lark-bot.${slugify2(profile)}`;
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
// src/service/env-snapshot.ts
|
|
3744
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
3745
|
+
var EXTRA_KEYS = ["PATH", "HOME", "DEEPSEEK_API_KEY", "DSH_HOME"];
|
|
3746
|
+
function snapshotServiceEnv(source = process.env) {
|
|
3747
|
+
const env = {};
|
|
3748
|
+
for (const [key, value] of Object.entries(source)) {
|
|
3749
|
+
if (value === void 0) continue;
|
|
3750
|
+
if (key.startsWith("DSH_LARK_") || EXTRA_KEYS.includes(key)) {
|
|
3751
|
+
env[key] = value;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
return env;
|
|
3755
|
+
}
|
|
3756
|
+
function formatEnvFile(env) {
|
|
3757
|
+
const lines = [];
|
|
3758
|
+
for (const [key, value] of Object.entries(env)) {
|
|
3759
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
3760
|
+
throw new Error(`invalid service env key: ${key}`);
|
|
3761
|
+
}
|
|
3762
|
+
if (/[\r\n]/.test(value)) {
|
|
3763
|
+
throw new Error(`service env value for ${key} contains a newline`);
|
|
3764
|
+
}
|
|
3765
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
3766
|
+
lines.push(`${key}="${escaped}"`);
|
|
3767
|
+
}
|
|
3768
|
+
return `${lines.join("\n")}
|
|
3769
|
+
`;
|
|
3770
|
+
}
|
|
3771
|
+
function parseEnvFile(text) {
|
|
3772
|
+
const env = {};
|
|
3773
|
+
for (const rawLine of text.split("\n")) {
|
|
3774
|
+
const line = rawLine.trim();
|
|
3775
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
3776
|
+
const eq = line.indexOf("=");
|
|
3777
|
+
if (eq <= 0) continue;
|
|
3778
|
+
const key = line.slice(0, eq).trim();
|
|
3779
|
+
let value = line.slice(eq + 1).trim();
|
|
3780
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
3781
|
+
value = value.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
3782
|
+
} else if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
3783
|
+
value = value.slice(1, -1);
|
|
3784
|
+
}
|
|
3785
|
+
env[key] = value;
|
|
3786
|
+
}
|
|
3787
|
+
return env;
|
|
3788
|
+
}
|
|
3789
|
+
async function writeServiceEnv(file, env) {
|
|
3790
|
+
await writeFileAtomic(file, formatEnvFile(env), { mode: 384 });
|
|
3791
|
+
}
|
|
3792
|
+
async function readServiceEnv(file) {
|
|
3793
|
+
try {
|
|
3794
|
+
return parseEnvFile(await readFile5(file, "utf8"));
|
|
3795
|
+
} catch (error) {
|
|
3796
|
+
if (error.code === "ENOENT") return {};
|
|
3797
|
+
throw error;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
// src/service/linux-systemd.ts
|
|
3802
|
+
import { mkdir as mkdir8, stat as stat3 } from "fs/promises";
|
|
3803
|
+
import { homedir as homedir5 } from "os";
|
|
3804
|
+
import { join as join10 } from "path";
|
|
3805
|
+
function defaultSystemdUnitDir() {
|
|
3806
|
+
return join10(homedir5(), ".config", "systemd", "user");
|
|
3807
|
+
}
|
|
3808
|
+
function quoteSystemdArg(arg) {
|
|
3809
|
+
if (!/[\s"\\]/.test(arg)) return arg;
|
|
3810
|
+
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
3811
|
+
}
|
|
3812
|
+
function buildSystemdUnit(spec) {
|
|
3813
|
+
const exec = [spec.nodePath, spec.cliJsPath, "run", "--profile", spec.profile].map(quoteSystemdArg).join(" ");
|
|
3814
|
+
return [
|
|
3815
|
+
"[Unit]",
|
|
3816
|
+
"Description=dsh-lark-bot bridge service",
|
|
3817
|
+
"After=network-online.target",
|
|
3818
|
+
"Wants=network-online.target",
|
|
3819
|
+
"",
|
|
3820
|
+
"[Service]",
|
|
3821
|
+
"Type=simple",
|
|
3822
|
+
`EnvironmentFile=${quoteSystemdArg(spec.envFile)}`,
|
|
3823
|
+
`ExecStart=${exec}`,
|
|
3824
|
+
"Restart=always",
|
|
3825
|
+
"RestartSec=5",
|
|
3826
|
+
"KillMode=mixed",
|
|
3827
|
+
"TimeoutStopSec=30",
|
|
3828
|
+
`StandardOutput=append:${spec.logFile}`,
|
|
3829
|
+
`StandardError=append:${spec.logFile}`,
|
|
3830
|
+
"",
|
|
3831
|
+
"[Install]",
|
|
3832
|
+
"WantedBy=default.target",
|
|
3833
|
+
""
|
|
3834
|
+
].join("\n");
|
|
3835
|
+
}
|
|
3836
|
+
async function isSystemdUserAvailable(runner = runCommand) {
|
|
3837
|
+
const result = await runner("systemctl", ["--user", "show-environment"]);
|
|
3838
|
+
return result.code === 0;
|
|
3839
|
+
}
|
|
3840
|
+
async function fileExists(path) {
|
|
3841
|
+
try {
|
|
3842
|
+
return (await stat3(path)).isFile();
|
|
3843
|
+
} catch {
|
|
3844
|
+
return false;
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
function parseMainPid(value) {
|
|
3848
|
+
const raw = value?.trim();
|
|
3849
|
+
if (!raw || raw === "0" || raw === "4294967295") return void 0;
|
|
3850
|
+
const pid = Number(raw);
|
|
3851
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
3852
|
+
}
|
|
3853
|
+
function parseNumber(value) {
|
|
3854
|
+
const raw = value?.trim();
|
|
3855
|
+
if (!raw) return void 0;
|
|
3856
|
+
const parsed = Number(raw);
|
|
3857
|
+
return Number.isInteger(parsed) ? parsed : void 0;
|
|
3858
|
+
}
|
|
3859
|
+
var SystemdServiceController = class {
|
|
3860
|
+
platform = "linux-systemd";
|
|
3861
|
+
run;
|
|
3862
|
+
unitDir;
|
|
3863
|
+
constructor(options = {}) {
|
|
3864
|
+
this.run = options.runCommand ?? runCommand;
|
|
3865
|
+
this.unitDir = options.userUnitDir ?? defaultSystemdUnitDir();
|
|
3866
|
+
}
|
|
3867
|
+
unitPath(spec) {
|
|
3868
|
+
return join10(this.unitDir, `${spec.serviceName}.service`);
|
|
3869
|
+
}
|
|
3870
|
+
unitName(spec) {
|
|
3871
|
+
return `${spec.serviceName}.service`;
|
|
3872
|
+
}
|
|
3873
|
+
async isActive(name) {
|
|
3874
|
+
const result = await this.run("systemctl", ["--user", "is-active", name]);
|
|
3875
|
+
return result.code === 0;
|
|
3876
|
+
}
|
|
3877
|
+
async installAndStart(spec) {
|
|
3878
|
+
await mkdir8(this.unitDir, { recursive: true });
|
|
3879
|
+
await writeFileAtomic(this.unitPath(spec), buildSystemdUnit(spec));
|
|
3880
|
+
await this.run("systemctl", ["--user", "daemon-reload"]);
|
|
3881
|
+
const name = this.unitName(spec);
|
|
3882
|
+
if (await this.isActive(name)) {
|
|
3883
|
+
await this.run("systemctl", ["--user", "restart", name]);
|
|
3884
|
+
} else {
|
|
3885
|
+
await this.run("systemctl", ["--user", "enable", "--now", name]);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
async stop(spec) {
|
|
3889
|
+
await this.run("systemctl", ["--user", "disable", "--now", this.unitName(spec)]);
|
|
3890
|
+
}
|
|
3891
|
+
async restart(spec) {
|
|
3892
|
+
await this.run("systemctl", ["--user", "daemon-reload"]);
|
|
3893
|
+
await this.run("systemctl", ["--user", "restart", this.unitName(spec)]);
|
|
3894
|
+
}
|
|
3895
|
+
async status(spec) {
|
|
3896
|
+
const name = this.unitName(spec);
|
|
3897
|
+
const unitFile = this.unitPath(spec);
|
|
3898
|
+
const installed = await fileExists(unitFile);
|
|
3899
|
+
let autostartEnabled = false;
|
|
3900
|
+
if (installed) {
|
|
3901
|
+
const enabled = await this.run("systemctl", ["--user", "is-enabled", name]);
|
|
3902
|
+
autostartEnabled = enabled.code === 0 && enabled.stdout.trim() === "enabled";
|
|
3903
|
+
}
|
|
3904
|
+
const active = await this.run("systemctl", ["--user", "is-active", name]);
|
|
3905
|
+
const isActive = active.code === 0;
|
|
3906
|
+
let detail = active.stdout.trim() || (isActive ? "active" : "inactive");
|
|
3907
|
+
let pid;
|
|
3908
|
+
let restarts;
|
|
3909
|
+
if (isActive || installed) {
|
|
3910
|
+
const show = await this.run(
|
|
3911
|
+
"systemctl",
|
|
3912
|
+
["--user", "show", name, "-p", "MainPID", "-p", "ActiveState", "-p", "NRestarts", "--value"]
|
|
3913
|
+
);
|
|
3914
|
+
const [activeState, mainPid, nRestarts] = show.stdout.trim().split("\n");
|
|
3915
|
+
if (activeState?.trim()) detail = activeState.trim();
|
|
3916
|
+
pid = parseMainPid(mainPid);
|
|
3917
|
+
restarts = parseNumber(nRestarts);
|
|
3918
|
+
}
|
|
3919
|
+
let state;
|
|
3920
|
+
if (!installed) {
|
|
3921
|
+
state = "stopped";
|
|
3922
|
+
} else if (detail === "failed") {
|
|
3923
|
+
state = "error";
|
|
3924
|
+
} else if (isActive || detail === "activating" || detail === "deactivating" || detail === "reloading") {
|
|
3925
|
+
state = "running";
|
|
3926
|
+
} else {
|
|
3927
|
+
state = "stopped";
|
|
3928
|
+
}
|
|
3929
|
+
return {
|
|
3930
|
+
name: spec.serviceName,
|
|
3931
|
+
platform: this.platform,
|
|
3932
|
+
installed,
|
|
3933
|
+
autostartEnabled,
|
|
3934
|
+
state,
|
|
3935
|
+
pid,
|
|
3936
|
+
detail,
|
|
3937
|
+
restarts
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
};
|
|
3941
|
+
|
|
3942
|
+
// src/service/macos-launchd.ts
|
|
3943
|
+
import { mkdir as mkdir9, rm, stat as stat4 } from "fs/promises";
|
|
3944
|
+
import { homedir as homedir6 } from "os";
|
|
3945
|
+
import { join as join11 } from "path";
|
|
3946
|
+
function defaultLaunchAgentDir() {
|
|
3947
|
+
return join11(homedir6(), "Library", "LaunchAgents");
|
|
3948
|
+
}
|
|
3949
|
+
function xmlEscape(value) {
|
|
3950
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3951
|
+
}
|
|
3952
|
+
function buildLaunchdPlist(spec) {
|
|
3953
|
+
const envLines = Object.entries(spec.env).filter(([, value]) => value !== void 0).map(
|
|
3954
|
+
([key, value]) => ` <key>${xmlEscape(key)}</key>
|
|
3955
|
+
<string>${xmlEscape(value)}</string>`
|
|
3956
|
+
).join("\n");
|
|
3957
|
+
return [
|
|
3958
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
3959
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
3960
|
+
'<plist version="1.0">',
|
|
3961
|
+
"<dict>",
|
|
3962
|
+
" <key>Label</key>",
|
|
3963
|
+
` <string>${xmlEscape(spec.label)}</string>`,
|
|
3964
|
+
" <key>ProgramArguments</key>",
|
|
3965
|
+
" <array>",
|
|
3966
|
+
` <string>${xmlEscape(spec.nodePath)}</string>`,
|
|
3967
|
+
` <string>${xmlEscape(spec.cliJsPath)}</string>`,
|
|
3968
|
+
" <string>run</string>",
|
|
3969
|
+
" <string>--profile</string>",
|
|
3970
|
+
` <string>${xmlEscape(spec.profile)}</string>`,
|
|
3971
|
+
" </array>",
|
|
3972
|
+
" <key>RunAtLoad</key>",
|
|
3973
|
+
" <true/>",
|
|
3974
|
+
" <key>KeepAlive</key>",
|
|
3975
|
+
" <true/>",
|
|
3976
|
+
" <key>EnvironmentVariables</key>",
|
|
3977
|
+
" <dict>",
|
|
3978
|
+
envLines,
|
|
3979
|
+
" </dict>",
|
|
3980
|
+
" <key>StandardOutPath</key>",
|
|
3981
|
+
` <string>${xmlEscape(spec.logFile)}</string>`,
|
|
3982
|
+
" <key>StandardErrorPath</key>",
|
|
3983
|
+
` <string>${xmlEscape(spec.logFile)}</string>`,
|
|
3984
|
+
" <key>ProcessType</key>",
|
|
3985
|
+
" <string>Background</string>",
|
|
3986
|
+
"</dict>",
|
|
3987
|
+
"</plist>",
|
|
3988
|
+
""
|
|
3989
|
+
].join("\n");
|
|
3990
|
+
}
|
|
3991
|
+
async function fileExists2(path) {
|
|
3992
|
+
try {
|
|
3993
|
+
return (await stat4(path)).isFile();
|
|
3994
|
+
} catch {
|
|
3995
|
+
return false;
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
var LaunchdServiceController = class {
|
|
3999
|
+
platform = "darwin-launchd";
|
|
4000
|
+
run;
|
|
4001
|
+
agentDir;
|
|
4002
|
+
uid;
|
|
4003
|
+
constructor(options = {}) {
|
|
4004
|
+
this.run = options.runCommand ?? runCommand;
|
|
4005
|
+
this.agentDir = options.launchAgentDir ?? defaultLaunchAgentDir();
|
|
4006
|
+
this.uid = options.uid ?? process.getuid?.() ?? -1;
|
|
4007
|
+
}
|
|
4008
|
+
domain() {
|
|
4009
|
+
return `gui/${this.uid}`;
|
|
4010
|
+
}
|
|
4011
|
+
plistPath(spec) {
|
|
4012
|
+
return join11(this.agentDir, `${spec.label}.plist`);
|
|
4013
|
+
}
|
|
4014
|
+
serviceRef(spec) {
|
|
4015
|
+
return `${this.domain()}/${spec.label}`;
|
|
4016
|
+
}
|
|
4017
|
+
async installAndStart(spec) {
|
|
4018
|
+
await mkdir9(this.agentDir, { recursive: true });
|
|
4019
|
+
await writeFileAtomic(this.plistPath(spec), buildLaunchdPlist(spec));
|
|
4020
|
+
const bootstrap = await this.run("launchctl", ["bootstrap", this.domain(), this.plistPath(spec)]);
|
|
4021
|
+
if (bootstrap.code !== 0) {
|
|
4022
|
+
await this.run("launchctl", ["kickstart", "-k", this.serviceRef(spec)]);
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
async stop(spec) {
|
|
4026
|
+
await this.run("launchctl", ["bootout", this.serviceRef(spec)]);
|
|
4027
|
+
await rm(this.plistPath(spec), { force: true });
|
|
4028
|
+
}
|
|
4029
|
+
async restart(spec) {
|
|
4030
|
+
const kickstart = await this.run("launchctl", ["kickstart", "-k", this.serviceRef(spec)]);
|
|
4031
|
+
if (kickstart.code !== 0) {
|
|
4032
|
+
await this.installAndStart(spec);
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
async status(spec) {
|
|
4036
|
+
const plistExists = await fileExists2(this.plistPath(spec));
|
|
4037
|
+
const printed = await this.run("launchctl", ["print", this.serviceRef(spec)]);
|
|
4038
|
+
const loaded = printed.code === 0;
|
|
4039
|
+
let pid;
|
|
4040
|
+
let state = "stopped";
|
|
4041
|
+
let detail = loaded ? "loaded" : "not loaded";
|
|
4042
|
+
if (loaded) {
|
|
4043
|
+
const pidMatch = /pid = (\d+)/.exec(printed.stdout);
|
|
4044
|
+
if (pidMatch?.[1]) pid = Number(pidMatch[1]);
|
|
4045
|
+
if (/state = running/.test(printed.stdout)) {
|
|
4046
|
+
state = "running";
|
|
4047
|
+
detail = "running";
|
|
4048
|
+
} else if (/state = exited|state = waiting/.test(printed.stdout)) {
|
|
4049
|
+
state = "stopped";
|
|
4050
|
+
detail = /state = exited/.test(printed.stdout) ? "exited" : "waiting";
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
return {
|
|
4054
|
+
name: spec.serviceName,
|
|
4055
|
+
platform: this.platform,
|
|
4056
|
+
installed: plistExists,
|
|
4057
|
+
autostartEnabled: loaded,
|
|
4058
|
+
state,
|
|
4059
|
+
pid,
|
|
4060
|
+
detail,
|
|
4061
|
+
restarts: void 0
|
|
4062
|
+
};
|
|
4063
|
+
}
|
|
4064
|
+
};
|
|
4065
|
+
|
|
4066
|
+
// src/service/portable.ts
|
|
4067
|
+
import { spawn as spawn5 } from "child_process";
|
|
4068
|
+
import { mkdir as mkdir10, readFile as readFile6, rm as rm2, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
4069
|
+
import { homedir as homedir7 } from "os";
|
|
4070
|
+
import { join as join12 } from "path";
|
|
4071
|
+
function defaultAutostartDir() {
|
|
4072
|
+
return join12(homedir7(), ".config", "autostart");
|
|
4073
|
+
}
|
|
4074
|
+
function quoteDesktopArg(arg) {
|
|
4075
|
+
const escaped = arg.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
4076
|
+
return /[\s"\\]/.test(arg) ? `"${escaped}"` : escaped;
|
|
4077
|
+
}
|
|
4078
|
+
function buildDesktopEntry(spec) {
|
|
4079
|
+
const exec = [spec.nodePath, spec.cliJsPath, "supervise", "--profile", spec.profile].map(quoteDesktopArg).join(" ");
|
|
4080
|
+
return [
|
|
4081
|
+
"[Desktop Entry]",
|
|
4082
|
+
"Type=Application",
|
|
4083
|
+
`Name=${spec.serviceName}`,
|
|
4084
|
+
"Comment=Keep dsh-lark-bot running in the background",
|
|
4085
|
+
`Exec=${exec}`,
|
|
4086
|
+
"Terminal=false",
|
|
4087
|
+
"X-GNOME-Autostart-enabled=true",
|
|
4088
|
+
"Hidden=false",
|
|
4089
|
+
""
|
|
4090
|
+
].join("\n");
|
|
4091
|
+
}
|
|
4092
|
+
function supervisorStatusFile(serviceDir, serviceName) {
|
|
4093
|
+
return join12(serviceDir, `supervisor-${serviceName}.json`);
|
|
4094
|
+
}
|
|
4095
|
+
function isProcessAlive(pid) {
|
|
4096
|
+
try {
|
|
4097
|
+
process.kill(pid, 0);
|
|
4098
|
+
return true;
|
|
4099
|
+
} catch (error) {
|
|
4100
|
+
return error.code === "EPERM";
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
async function readSupervisorStatus(file) {
|
|
4104
|
+
try {
|
|
4105
|
+
return JSON.parse(await readFile6(file, "utf8"));
|
|
4106
|
+
} catch {
|
|
4107
|
+
return void 0;
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
function defaultSpawnDetached(nodePath, args) {
|
|
4111
|
+
const child = spawn5(nodePath, [...args], {
|
|
4112
|
+
detached: true,
|
|
4113
|
+
stdio: "ignore",
|
|
4114
|
+
env: process.env
|
|
4115
|
+
});
|
|
4116
|
+
child.unref();
|
|
4117
|
+
return child;
|
|
4118
|
+
}
|
|
4119
|
+
var PortableServiceController = class {
|
|
4120
|
+
platform = "linux-portable";
|
|
4121
|
+
autostartDir;
|
|
4122
|
+
serviceDir;
|
|
4123
|
+
spawnDetached;
|
|
4124
|
+
constructor(options = {}) {
|
|
4125
|
+
this.autostartDir = options.autostartDir ?? defaultAutostartDir();
|
|
4126
|
+
this.serviceDir = options.serviceDir ?? "";
|
|
4127
|
+
this.spawnDetached = options.spawnDetached ?? defaultSpawnDetached;
|
|
4128
|
+
}
|
|
4129
|
+
desktopPath(spec) {
|
|
4130
|
+
return join12(this.autostartDir, `${spec.serviceName}.desktop`);
|
|
4131
|
+
}
|
|
4132
|
+
statusFile(spec) {
|
|
4133
|
+
return supervisorStatusFile(this.serviceDir, spec.serviceName);
|
|
4134
|
+
}
|
|
4135
|
+
async sendStopSignal(spec) {
|
|
4136
|
+
const status = await readSupervisorStatus(this.statusFile(spec));
|
|
4137
|
+
if (!status) return;
|
|
4138
|
+
try {
|
|
4139
|
+
process.kill(status.pid, "SIGTERM");
|
|
4140
|
+
} catch {
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
4144
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 250));
|
|
4145
|
+
if (!isProcessAlive(status.pid)) return;
|
|
4146
|
+
}
|
|
4147
|
+
try {
|
|
4148
|
+
process.kill(status.pid, "SIGKILL");
|
|
4149
|
+
} catch {
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
async installAndStart(spec) {
|
|
4153
|
+
await this.sendStopSignal(spec);
|
|
4154
|
+
await mkdir10(this.autostartDir, { recursive: true });
|
|
4155
|
+
await writeFile4(this.desktopPath(spec), buildDesktopEntry(spec), { encoding: "utf8", mode: 420 });
|
|
4156
|
+
if (!this.serviceDir) throw new Error("serviceDir is required for the portable supervisor");
|
|
4157
|
+
await mkdir10(this.serviceDir, { recursive: true });
|
|
4158
|
+
this.spawnDetached(spec.nodePath, [spec.cliJsPath, "supervise", "--profile", spec.profile]);
|
|
4159
|
+
}
|
|
4160
|
+
async stop(spec) {
|
|
4161
|
+
await this.sendStopSignal(spec);
|
|
4162
|
+
await rm2(this.desktopPath(spec), { force: true });
|
|
4163
|
+
}
|
|
4164
|
+
async restart(spec) {
|
|
4165
|
+
await this.stop(spec);
|
|
4166
|
+
await this.installAndStart(spec);
|
|
4167
|
+
}
|
|
4168
|
+
async status(spec) {
|
|
4169
|
+
let desktopExists = false;
|
|
4170
|
+
try {
|
|
4171
|
+
desktopExists = (await stat5(this.desktopPath(spec))).isFile();
|
|
4172
|
+
} catch {
|
|
4173
|
+
desktopExists = false;
|
|
4174
|
+
}
|
|
4175
|
+
const status = await readSupervisorStatus(this.statusFile(spec));
|
|
4176
|
+
const alive = status !== void 0 && isProcessAlive(status.pid);
|
|
4177
|
+
return {
|
|
4178
|
+
name: spec.serviceName,
|
|
4179
|
+
platform: this.platform,
|
|
4180
|
+
installed: desktopExists,
|
|
4181
|
+
autostartEnabled: desktopExists,
|
|
4182
|
+
state: alive ? "running" : "stopped",
|
|
4183
|
+
pid: status?.childPid,
|
|
4184
|
+
detail: alive ? `supervisor ${status?.pid ?? "?"}` : "stopped",
|
|
4185
|
+
restarts: status?.restarts
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
4188
|
+
};
|
|
4189
|
+
|
|
4190
|
+
// src/service/windows-task.ts
|
|
4191
|
+
import { mkdir as mkdir11, writeFile as writeFile5 } from "fs/promises";
|
|
4192
|
+
import { dirname as dirname5, join as join13 } from "path";
|
|
4193
|
+
function psQuote(value) {
|
|
4194
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
4195
|
+
}
|
|
4196
|
+
function buildScheduledTaskScript(action, spec) {
|
|
4197
|
+
const vars = [
|
|
4198
|
+
`$taskName = ${psQuote(spec.serviceName)}`,
|
|
4199
|
+
`$node = ${psQuote(spec.nodePath)}`,
|
|
4200
|
+
`$cli = ${psQuote(spec.cliJsPath)}`,
|
|
4201
|
+
`$log = ${psQuote(spec.logFile)}`,
|
|
4202
|
+
`$profile = ${psQuote(spec.profile)}`,
|
|
4203
|
+
`$workDir = ${psQuote(dirname5(spec.cliJsPath))}`
|
|
4204
|
+
].join("\n");
|
|
4205
|
+
const install = [
|
|
4206
|
+
`$inner = '"' + $node + '" "' + $cli + '" run --profile "' + $profile + '" >> "' + $log + '" 2>&1'`,
|
|
4207
|
+
`$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument ('/c ' + $inner) -WorkingDirectory $workDir`,
|
|
4208
|
+
`$trigger = New-ScheduledTaskTrigger -AtLogOn`,
|
|
4209
|
+
`$settings = New-ScheduledTaskSettingsSet -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries`,
|
|
4210
|
+
`Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue`,
|
|
4211
|
+
`Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -Force`,
|
|
4212
|
+
`Enable-ScheduledTask -TaskName $taskName`,
|
|
4213
|
+
`Start-ScheduledTask -TaskName $taskName`
|
|
4214
|
+
].join("\n");
|
|
4215
|
+
const stop = [
|
|
4216
|
+
`Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`,
|
|
4217
|
+
`Disable-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`
|
|
4218
|
+
].join("\n");
|
|
4219
|
+
const restart = [
|
|
4220
|
+
`Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`,
|
|
4221
|
+
`Start-ScheduledTask -TaskName $taskName`
|
|
4222
|
+
].join("\n");
|
|
4223
|
+
const status = [
|
|
4224
|
+
`$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`,
|
|
4225
|
+
`if ($null -eq $task) { Write-Output 'STATE:missing'; Write-Output 'ENABLED:false'; exit 2 }`,
|
|
4226
|
+
`$info = Get-ScheduledTaskInfo -TaskName $taskName`,
|
|
4227
|
+
`Write-Output ('STATE:' + $task.State)`,
|
|
4228
|
+
`Write-Output ('ENABLED:' + $task.Settings.Enabled)`,
|
|
4229
|
+
`Write-Output ('LAST_RESULT:' + $info.LastTaskResult)`,
|
|
4230
|
+
`Write-Output ('LAST_RUN:' + $info.LastRunTime)`
|
|
4231
|
+
].join("\n");
|
|
4232
|
+
const body = {
|
|
4233
|
+
install,
|
|
4234
|
+
stop,
|
|
4235
|
+
restart,
|
|
4236
|
+
status
|
|
4237
|
+
};
|
|
4238
|
+
return [
|
|
4239
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
4240
|
+
vars,
|
|
4241
|
+
body[action],
|
|
4242
|
+
""
|
|
4243
|
+
].join("\n");
|
|
4244
|
+
}
|
|
4245
|
+
function parseTaskStatusOutput(output) {
|
|
4246
|
+
const fields = /* @__PURE__ */ new Map();
|
|
4247
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
4248
|
+
const line = rawLine.trim();
|
|
4249
|
+
const eq = line.indexOf(":");
|
|
4250
|
+
if (eq <= 0) continue;
|
|
4251
|
+
fields.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
|
|
4252
|
+
}
|
|
4253
|
+
const stateValue = fields.get("STATE") ?? "missing";
|
|
4254
|
+
const enabled = fields.get("ENABLED") === "True";
|
|
4255
|
+
const installed = stateValue !== "missing";
|
|
4256
|
+
let state = "unknown";
|
|
4257
|
+
if (stateValue === "Running") state = "running";
|
|
4258
|
+
else if (stateValue === "Ready" || stateValue === "Disabled") state = "stopped";
|
|
4259
|
+
else if (stateValue === "Unknown") state = "unknown";
|
|
4260
|
+
else if (stateValue === "missing") state = "stopped";
|
|
4261
|
+
const lastResultRaw = fields.get("LAST_RESULT");
|
|
4262
|
+
const lastResult = lastResultRaw !== void 0 ? Number.parseInt(lastResultRaw, 10) : void 0;
|
|
4263
|
+
const lastResultValue = lastResult !== void 0 && Number.isFinite(lastResult) ? lastResult : void 0;
|
|
4264
|
+
if (installed && lastResultValue !== void 0 && lastResultValue !== 0 && stateValue === "Ready") {
|
|
4265
|
+
state = "error";
|
|
4266
|
+
}
|
|
4267
|
+
return {
|
|
4268
|
+
installed,
|
|
4269
|
+
autostartEnabled: enabled,
|
|
4270
|
+
state,
|
|
4271
|
+
detail: stateValue,
|
|
4272
|
+
lastResult: lastResultValue
|
|
4273
|
+
};
|
|
4274
|
+
}
|
|
4275
|
+
var WindowsTaskServiceController = class {
|
|
4276
|
+
platform = "win32-task";
|
|
4277
|
+
run;
|
|
4278
|
+
scriptDir;
|
|
4279
|
+
constructor(options = {}) {
|
|
4280
|
+
this.run = options.runCommand ?? runCommand;
|
|
4281
|
+
this.scriptDir = options.scriptDir ?? "";
|
|
4282
|
+
}
|
|
4283
|
+
scriptPath(spec, action) {
|
|
4284
|
+
return join13(this.scriptDir, `${action}-${spec.serviceName}.ps1`);
|
|
4285
|
+
}
|
|
4286
|
+
async runScript(spec, action) {
|
|
4287
|
+
const script = this.scriptPath(spec, action);
|
|
4288
|
+
if (this.scriptDir) await mkdir11(this.scriptDir, { recursive: true });
|
|
4289
|
+
await writeFile5(script, buildScheduledTaskScript(action, spec), { encoding: "utf8" });
|
|
4290
|
+
const result = await this.run("powershell.exe", [
|
|
4291
|
+
"-NoProfile",
|
|
4292
|
+
"-NonInteractive",
|
|
4293
|
+
"-ExecutionPolicy",
|
|
4294
|
+
"Bypass",
|
|
4295
|
+
"-File",
|
|
4296
|
+
script
|
|
4297
|
+
]);
|
|
4298
|
+
return { result, script };
|
|
4299
|
+
}
|
|
4300
|
+
async installAndStart(spec) {
|
|
4301
|
+
const { result } = await this.runScript(spec, "install");
|
|
4302
|
+
if (result.code !== 0) {
|
|
4303
|
+
throw new Error(
|
|
4304
|
+
`\u65E0\u6CD5\u6CE8\u518C Windows \u8BA1\u5212\u4EFB\u52A1\uFF08exit ${result.code}\uFF09\uFF1A${result.stderr.trim() || result.stdout.trim() || "\u672A\u77E5\u9519\u8BEF"}`
|
|
4305
|
+
);
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
async stop(spec) {
|
|
4309
|
+
const { result } = await this.runScript(spec, "stop");
|
|
4310
|
+
if (result.code !== 0) {
|
|
4311
|
+
throw new Error(
|
|
4312
|
+
`\u65E0\u6CD5\u505C\u6B62 Windows \u8BA1\u5212\u4EFB\u52A1\uFF08exit ${result.code}\uFF09\uFF1A${result.stderr.trim() || result.stdout.trim() || "\u672A\u77E5\u9519\u8BEF"}`
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
async restart(spec) {
|
|
4317
|
+
const { result } = await this.runScript(spec, "restart");
|
|
4318
|
+
if (result.code !== 0) {
|
|
4319
|
+
throw new Error(
|
|
4320
|
+
`\u65E0\u6CD5\u91CD\u542F Windows \u8BA1\u5212\u4EFB\u52A1\uFF08exit ${result.code}\uFF09\uFF1A${result.stderr.trim() || result.stdout.trim() || "\u672A\u77E5\u9519\u8BEF"}`
|
|
4321
|
+
);
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
async status(spec) {
|
|
4325
|
+
const { result } = await this.runScript(spec, "status");
|
|
4326
|
+
const parsed = parseTaskStatusOutput(result.stdout);
|
|
4327
|
+
if (result.code !== 0 && result.code !== 2) {
|
|
4328
|
+
throw new Error(
|
|
4329
|
+
`\u65E0\u6CD5\u67E5\u8BE2 Windows \u8BA1\u5212\u4EFB\u52A1\uFF08exit ${result.code}\uFF09\uFF1A${result.stderr.trim() || result.stdout.trim() || "\u672A\u77E5\u9519\u8BEF"}`
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
return {
|
|
4333
|
+
name: spec.serviceName,
|
|
4334
|
+
platform: this.platform,
|
|
4335
|
+
installed: parsed.installed,
|
|
4336
|
+
autostartEnabled: parsed.autostartEnabled,
|
|
4337
|
+
state: parsed.state,
|
|
4338
|
+
pid: void 0,
|
|
4339
|
+
detail: parsed.detail,
|
|
4340
|
+
restarts: void 0
|
|
4341
|
+
};
|
|
4342
|
+
}
|
|
4343
|
+
};
|
|
4344
|
+
|
|
4345
|
+
// src/service/manager.ts
|
|
4346
|
+
var ServiceManager = class {
|
|
4347
|
+
profile;
|
|
4348
|
+
sourceEnv;
|
|
4349
|
+
paths;
|
|
4350
|
+
version;
|
|
4351
|
+
controllerOverride;
|
|
4352
|
+
constructor(deps = {}) {
|
|
4353
|
+
this.profile = deps.profile ?? "default";
|
|
4354
|
+
this.sourceEnv = deps.env ?? process.env;
|
|
4355
|
+
this.paths = deps.paths ?? resolveAppPaths();
|
|
4356
|
+
this.version = deps.version ?? "0.0.0";
|
|
4357
|
+
this.controllerOverride = deps.controller;
|
|
4358
|
+
}
|
|
4359
|
+
buildSpec() {
|
|
4360
|
+
const serviceName = serviceNameFor(this.profile);
|
|
4361
|
+
return {
|
|
4362
|
+
serviceName,
|
|
4363
|
+
label: launchdLabelFor(this.profile),
|
|
4364
|
+
profile: this.profile,
|
|
4365
|
+
nodePath: process.execPath,
|
|
4366
|
+
cliJsPath: resolveCliJsPath(),
|
|
4367
|
+
envFile: this.paths.serviceEnvFile,
|
|
4368
|
+
logFile: this.paths.serviceLogFile(this.profile),
|
|
4369
|
+
env: snapshotServiceEnv(this.sourceEnv)
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
async resolveController() {
|
|
4373
|
+
if (this.controllerOverride) return this.controllerOverride;
|
|
4374
|
+
if (process.platform === "darwin") return new LaunchdServiceController();
|
|
4375
|
+
if (process.platform === "win32") return new WindowsTaskServiceController({
|
|
4376
|
+
scriptDir: this.paths.serviceDir
|
|
4377
|
+
});
|
|
4378
|
+
const systemd = new SystemdServiceController();
|
|
4379
|
+
if (await isSystemdUserAvailable()) return systemd;
|
|
4380
|
+
return new PortableServiceController({ serviceDir: this.paths.serviceDir });
|
|
4381
|
+
}
|
|
4382
|
+
async saveMetadata(platform) {
|
|
4383
|
+
const metadata = {
|
|
4384
|
+
schemaVersion: 1,
|
|
4385
|
+
serviceName: this.buildSpec().serviceName,
|
|
4386
|
+
profile: this.profile,
|
|
4387
|
+
platform,
|
|
4388
|
+
version: this.version,
|
|
4389
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4390
|
+
};
|
|
4391
|
+
await writeFileAtomic(this.paths.serviceMetadataFile, `${JSON.stringify(metadata, null, 2)}
|
|
4392
|
+
`);
|
|
4393
|
+
}
|
|
4394
|
+
async readMetadata() {
|
|
4395
|
+
try {
|
|
4396
|
+
return JSON.parse(await readFile7(this.paths.serviceMetadataFile, "utf8"));
|
|
4397
|
+
} catch {
|
|
4398
|
+
return void 0;
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
async prepareFiles(spec) {
|
|
4402
|
+
await mkdir12(dirname6(spec.logFile), { recursive: true });
|
|
4403
|
+
await mkdir12(dirname6(spec.envFile), { recursive: true });
|
|
4404
|
+
await writeServiceEnv(spec.envFile, spec.env);
|
|
4405
|
+
}
|
|
4406
|
+
async start() {
|
|
4407
|
+
const controller = await this.resolveController();
|
|
4408
|
+
const spec = this.buildSpec();
|
|
4409
|
+
await this.prepareFiles(spec);
|
|
4410
|
+
await controller.installAndStart(spec);
|
|
4411
|
+
await this.saveMetadata(controller.platform);
|
|
4412
|
+
return controller.status(spec);
|
|
4413
|
+
}
|
|
4414
|
+
async stop() {
|
|
4415
|
+
const controller = await this.resolveController();
|
|
4416
|
+
const spec = this.buildSpec();
|
|
4417
|
+
await controller.stop(spec);
|
|
4418
|
+
return controller.status(spec);
|
|
4419
|
+
}
|
|
4420
|
+
async restart() {
|
|
4421
|
+
const controller = await this.resolveController();
|
|
4422
|
+
const spec = this.buildSpec();
|
|
4423
|
+
const before = await controller.status(spec);
|
|
4424
|
+
if (!before.installed) {
|
|
4425
|
+
throw new Error("\u540E\u53F0\u670D\u52A1\u5C1A\u672A\u5B89\u88C5\uFF0C\u8BF7\u5148\u8FD0\u884C dsh-lark-bot start");
|
|
4426
|
+
}
|
|
4427
|
+
await this.prepareFiles(spec);
|
|
4428
|
+
await controller.restart(spec);
|
|
4429
|
+
await this.saveMetadata(controller.platform);
|
|
4430
|
+
return controller.status(spec);
|
|
4431
|
+
}
|
|
4432
|
+
async status() {
|
|
4433
|
+
const controller = await this.resolveController();
|
|
4434
|
+
const spec = this.buildSpec();
|
|
4435
|
+
return controller.status(spec);
|
|
4436
|
+
}
|
|
4437
|
+
};
|
|
4438
|
+
|
|
4439
|
+
// src/cli/commands/service.ts
|
|
4440
|
+
function mergeStartEnv2(options) {
|
|
4441
|
+
return {
|
|
4442
|
+
...process.env,
|
|
4443
|
+
...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
|
|
4444
|
+
...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
|
|
4445
|
+
...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
|
|
4446
|
+
...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
function createManager(options, version) {
|
|
4450
|
+
const env = loadRuntimeEnv(mergeStartEnv2(options));
|
|
4451
|
+
const paths = resolveAppPaths(env.home);
|
|
4452
|
+
return new ServiceManager({
|
|
4453
|
+
profile: options.profile ?? "default",
|
|
4454
|
+
env: mergeStartEnv2(options),
|
|
4455
|
+
paths,
|
|
4456
|
+
version
|
|
4457
|
+
});
|
|
4458
|
+
}
|
|
4459
|
+
function printServiceStatus(status, output) {
|
|
4460
|
+
const lines = [
|
|
4461
|
+
"dsh-lark-bot \u670D\u52A1\u72B6\u6001",
|
|
4462
|
+
` name: ${status.name}`,
|
|
4463
|
+
` platform: ${status.platform}`,
|
|
4464
|
+
` installed: ${status.installed ? "\u662F" : "\u5426"}`,
|
|
4465
|
+
` autostart: ${status.autostartEnabled ? "\u5DF2\u542F\u7528\uFF08\u5F00\u673A\u81EA\u542F\uFF09" : "\u672A\u542F\u7528"}`,
|
|
4466
|
+
` state: ${status.state}`,
|
|
4467
|
+
` detail: ${status.detail}`,
|
|
4468
|
+
` pid: ${status.pid ?? "-"}`,
|
|
4469
|
+
` restarts: ${status.restarts ?? "-"}`
|
|
4470
|
+
];
|
|
4471
|
+
output(`${lines.join("\n")}
|
|
4472
|
+
`);
|
|
4473
|
+
}
|
|
4474
|
+
async function runServiceStart(options, deps = {}) {
|
|
4475
|
+
const version = deps.version ?? "0.0.0";
|
|
4476
|
+
const output = deps.output ?? ((text) => process.stdout.write(text));
|
|
4477
|
+
if (!deps.manager) {
|
|
4478
|
+
const env = loadRuntimeEnv(mergeStartEnv2(options));
|
|
4479
|
+
const paths = resolveAppPaths(env.home);
|
|
4480
|
+
const profileName = options.profile ?? "default";
|
|
4481
|
+
const store = new ConfigStore(paths.configFile);
|
|
4482
|
+
await store.load();
|
|
4483
|
+
const ready = await ensureBotProfile(store, {
|
|
4484
|
+
env,
|
|
4485
|
+
profileName,
|
|
4486
|
+
allowOnboarding: true
|
|
4487
|
+
});
|
|
4488
|
+
if (!ready) {
|
|
4489
|
+
process.exitCode = 1;
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
const manager = deps.manager ?? createManager(options, version);
|
|
4494
|
+
try {
|
|
4495
|
+
const status = await manager.start();
|
|
4496
|
+
printServiceStatus(status, output);
|
|
4497
|
+
if (status.state !== "running") process.exitCode = 1;
|
|
4498
|
+
} catch (error) {
|
|
4499
|
+
output(`${error instanceof Error ? error.message : String(error)}
|
|
4500
|
+
`);
|
|
4501
|
+
process.exitCode = 1;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
async function runServiceCommand(action, options, deps = {}) {
|
|
4505
|
+
const output = deps.output ?? ((text) => process.stdout.write(text));
|
|
4506
|
+
const manager = deps.manager ?? createManager(options, deps.version ?? "0.0.0");
|
|
4507
|
+
try {
|
|
4508
|
+
let status;
|
|
4509
|
+
if (action === "status") {
|
|
4510
|
+
status = await manager.status();
|
|
4511
|
+
} else if (action === "restart") {
|
|
4512
|
+
status = await manager.restart();
|
|
4513
|
+
} else {
|
|
4514
|
+
status = await manager.stop();
|
|
4515
|
+
}
|
|
4516
|
+
printServiceStatus(status, output);
|
|
4517
|
+
if (action === "status") {
|
|
4518
|
+
if (status.state !== "running") process.exitCode = 1;
|
|
4519
|
+
} else if (action === "restart") {
|
|
4520
|
+
if (status.state !== "running") process.exitCode = 1;
|
|
4521
|
+
} else if (status.state === "running") {
|
|
4522
|
+
process.exitCode = 1;
|
|
4523
|
+
}
|
|
4524
|
+
} catch (error) {
|
|
4525
|
+
output(`${error instanceof Error ? error.message : String(error)}
|
|
4526
|
+
`);
|
|
4527
|
+
process.exitCode = 1;
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
// src/cli/commands/supervise.ts
|
|
4532
|
+
import { spawn as spawn6 } from "child_process";
|
|
4533
|
+
import { closeSync, mkdirSync, openSync } from "fs";
|
|
4534
|
+
import { dirname as dirname7 } from "path";
|
|
4535
|
+
var BASE_BACKOFF_MS = 5e3;
|
|
4536
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
4537
|
+
function computeBackoffMs(restarts) {
|
|
4538
|
+
return Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** Math.min(restarts, 3));
|
|
4539
|
+
}
|
|
4540
|
+
function waitForExit(child) {
|
|
4541
|
+
return new Promise((resolvePromise) => {
|
|
4542
|
+
const done = () => resolvePromise();
|
|
4543
|
+
child.once("exit", done);
|
|
4544
|
+
child.once("error", done);
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
function sleep(ms) {
|
|
4548
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
4549
|
+
}
|
|
4550
|
+
async function runSupervise(options) {
|
|
4551
|
+
const env = loadRuntimeEnv(process.env);
|
|
4552
|
+
const paths = resolveAppPaths(env.home);
|
|
4553
|
+
const profile = options.profile ?? "default";
|
|
4554
|
+
const serviceName = serviceNameFor(profile);
|
|
4555
|
+
const statusFile = supervisorStatusFile(paths.serviceDir, serviceName);
|
|
4556
|
+
const logFile = paths.serviceLogFile(profile);
|
|
4557
|
+
const serviceEnv = await readServiceEnv(paths.serviceEnvFile);
|
|
4558
|
+
const childEnv = { ...process.env, ...serviceEnv };
|
|
4559
|
+
const cliJsPath = resolveCliJsPath();
|
|
4560
|
+
mkdirSync(dirname7(statusFile), { recursive: true });
|
|
4561
|
+
mkdirSync(dirname7(logFile), { recursive: true });
|
|
4562
|
+
const logFd = openSync(logFile, "a");
|
|
4563
|
+
const writeStatus = async (state, childPid, restarts2) => {
|
|
4564
|
+
const status = {
|
|
4565
|
+
pid: process.pid,
|
|
4566
|
+
childPid,
|
|
4567
|
+
state,
|
|
4568
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4569
|
+
restarts: restarts2,
|
|
4570
|
+
profile
|
|
4571
|
+
};
|
|
4572
|
+
await writeFileAtomic(statusFile, `${JSON.stringify(status)}
|
|
4573
|
+
`);
|
|
4574
|
+
};
|
|
4575
|
+
let stopping = false;
|
|
4576
|
+
let child;
|
|
4577
|
+
const stop = () => {
|
|
4578
|
+
stopping = true;
|
|
4579
|
+
child?.kill("SIGTERM");
|
|
4580
|
+
};
|
|
4581
|
+
process.once("SIGTERM", stop);
|
|
4582
|
+
process.once("SIGINT", stop);
|
|
4583
|
+
let restarts = 0;
|
|
4584
|
+
try {
|
|
4585
|
+
while (!stopping) {
|
|
4586
|
+
await writeStatus("running", void 0, restarts);
|
|
4587
|
+
const spawned = spawn6(process.execPath, [cliJsPath, "run", "--profile", profile], {
|
|
4588
|
+
env: childEnv,
|
|
4589
|
+
stdio: ["ignore", logFd, logFd]
|
|
4590
|
+
});
|
|
4591
|
+
child = spawned;
|
|
4592
|
+
await writeStatus("running", spawned.pid, restarts);
|
|
4593
|
+
await waitForExit(spawned);
|
|
4594
|
+
child = void 0;
|
|
4595
|
+
if (stopping) break;
|
|
4596
|
+
restarts += 1;
|
|
4597
|
+
await writeStatus("restarting", void 0, restarts);
|
|
4598
|
+
const backoff = computeBackoffMs(restarts);
|
|
4599
|
+
await sleep(backoff);
|
|
4600
|
+
}
|
|
4601
|
+
} finally {
|
|
4602
|
+
await writeStatus("stopped", void 0, restarts);
|
|
4603
|
+
closeSync(logFd);
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
|
|
3675
4607
|
// src/cli.ts
|
|
3676
4608
|
function packageVersion() {
|
|
3677
4609
|
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
3678
4610
|
const pkg = JSON.parse(raw);
|
|
3679
4611
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
3680
4612
|
}
|
|
4613
|
+
function addBotOptions(command) {
|
|
4614
|
+
return command.option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark");
|
|
4615
|
+
}
|
|
3681
4616
|
function buildProgram() {
|
|
3682
4617
|
const program = new Command();
|
|
3683
4618
|
program.name("dsh-lark-bot").description("Bridge DeepSeek Harness into Feishu / Lark").version(packageVersion(), "-v, --version");
|
|
3684
|
-
|
|
3685
|
-
|
|
4619
|
+
addBotOptions(
|
|
4620
|
+
program.command("start").description("Install and start the background service (autostart + auto-restart)")
|
|
4621
|
+
).action(async (opts) => {
|
|
4622
|
+
await runServiceStart(opts, { version: packageVersion() });
|
|
3686
4623
|
});
|
|
4624
|
+
addBotOptions(program.command("status").description("Show background service status")).action(
|
|
4625
|
+
async (opts) => {
|
|
4626
|
+
await runServiceCommand("status", opts, { version: packageVersion() });
|
|
4627
|
+
}
|
|
4628
|
+
);
|
|
4629
|
+
addBotOptions(program.command("restart").description("Restart the background service")).action(
|
|
4630
|
+
async (opts) => {
|
|
4631
|
+
await runServiceCommand("restart", opts, { version: packageVersion() });
|
|
4632
|
+
}
|
|
4633
|
+
);
|
|
4634
|
+
addBotOptions(program.command("stop").description("Stop the background service and remove autostart")).action(
|
|
4635
|
+
async (opts) => {
|
|
4636
|
+
await runServiceCommand("stop", opts, { version: packageVersion() });
|
|
4637
|
+
}
|
|
4638
|
+
);
|
|
3687
4639
|
program.command("doctor").description("Run local diagnostics").option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark").action(async (opts) => {
|
|
3688
4640
|
await runDoctor({ ...opts, version: packageVersion() });
|
|
3689
4641
|
});
|
|
4642
|
+
addBotOptions(
|
|
4643
|
+
program.command("run", { hidden: true }).description("Run the bridge process (managed by the background service; not for interactive use)")
|
|
4644
|
+
).action(async (opts) => {
|
|
4645
|
+
await runBot(opts);
|
|
4646
|
+
});
|
|
4647
|
+
addBotOptions(
|
|
4648
|
+
program.command("supervise", { hidden: true }).description("Supervisor loop for the portable background service; not for interactive use")
|
|
4649
|
+
).action(async (opts) => {
|
|
4650
|
+
await runSupervise(opts);
|
|
4651
|
+
});
|
|
3690
4652
|
return program;
|
|
3691
4653
|
}
|
|
3692
4654
|
async function main(argv = process.argv) {
|
|
3693
4655
|
await buildProgram().parseAsync([...argv]);
|
|
3694
4656
|
}
|
|
4657
|
+
function isDirectInvocation(entry = process.argv[1], metaUrl = import.meta.url) {
|
|
4658
|
+
if (!entry) return false;
|
|
4659
|
+
try {
|
|
4660
|
+
return realpathSync2(entry) === fileURLToPath2(metaUrl);
|
|
4661
|
+
} catch {
|
|
4662
|
+
return false;
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
if (isDirectInvocation()) {
|
|
4666
|
+
await main();
|
|
4667
|
+
}
|
|
3695
4668
|
export {
|
|
3696
4669
|
buildProgram,
|
|
4670
|
+
isDirectInvocation,
|
|
3697
4671
|
main
|
|
3698
4672
|
};
|
|
3699
4673
|
//# sourceMappingURL=cli.js.map
|