larkway 0.3.46 → 0.3.48
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 +1 -1
- package/README.zh.md +1 -1
- package/dist/cli/index.js +288 -3
- package/dist/main.js +83 -12
- package/dist/web/public/app.js +839 -128
- package/dist/web/public/index.html +1 -1
- package/dist/web/public/style.css +437 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
|
|
10
10
|
|
|
11
|
-
**Current release: v0.3.
|
|
11
|
+
**Current release: v0.3.48**
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/README.zh.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -131387,7 +131387,11 @@ var execFileAsync7 = promisify8(execFileCallback2);
|
|
|
131387
131387
|
var CHAT_NAME_CACHE_MS = 5 * 60 * 1e3;
|
|
131388
131388
|
var chatNameCache = /* @__PURE__ */ new Map();
|
|
131389
131389
|
var eventExecFile = execFileAsync7;
|
|
131390
|
+
var updateExecFile = execFileAsync7;
|
|
131390
131391
|
var LARKWAY_VERSION = resolveLarkwayVersion(import.meta.url, "0.0.0");
|
|
131392
|
+
var UPDATE_CHECK_TIMEOUT_MS = 5e3;
|
|
131393
|
+
var UPDATE_RUN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
131394
|
+
var UPDATE_MAX_BUFFER = 2 * 1024 * 1024;
|
|
131391
131395
|
function createManagementContext(opts = {}) {
|
|
131392
131396
|
const stores = {
|
|
131393
131397
|
botsStore: opts.stores?.botsStore ?? botsStore_exports,
|
|
@@ -131449,6 +131453,174 @@ var BOT_ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
131449
131453
|
function badBotId(id) {
|
|
131450
131454
|
return BOT_ID_RE.test(id) ? null : { status: 400, json: { error: `\u975E\u6CD5\u7684\u52A9\u624B id "${id}"` } };
|
|
131451
131455
|
}
|
|
131456
|
+
function firstVersion(text) {
|
|
131457
|
+
return text.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/)?.[0] ?? null;
|
|
131458
|
+
}
|
|
131459
|
+
function compareVersion(a, b) {
|
|
131460
|
+
const aa = a.split(/[+-]/)[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
131461
|
+
const bb = b.split(/[+-]/)[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
131462
|
+
for (let i = 0; i < 3; i++) {
|
|
131463
|
+
if ((aa[i] ?? 0) !== (bb[i] ?? 0)) return (aa[i] ?? 0) - (bb[i] ?? 0);
|
|
131464
|
+
}
|
|
131465
|
+
return 0;
|
|
131466
|
+
}
|
|
131467
|
+
async function readGlobalLarkwayVersion() {
|
|
131468
|
+
try {
|
|
131469
|
+
const { stdout: stdout2, stderr } = await updateExecFile("larkway", ["--version"], {
|
|
131470
|
+
timeout: UPDATE_CHECK_TIMEOUT_MS,
|
|
131471
|
+
maxBuffer: UPDATE_MAX_BUFFER
|
|
131472
|
+
});
|
|
131473
|
+
return firstVersion(`${stdout2}
|
|
131474
|
+
${stderr}`) ?? LARKWAY_VERSION;
|
|
131475
|
+
} catch {
|
|
131476
|
+
return LARKWAY_VERSION;
|
|
131477
|
+
}
|
|
131478
|
+
}
|
|
131479
|
+
async function readLatestLarkwayVersion() {
|
|
131480
|
+
try {
|
|
131481
|
+
const { stdout: stdout2, stderr } = await updateExecFile("npm", ["view", "larkway", "version"], {
|
|
131482
|
+
timeout: UPDATE_CHECK_TIMEOUT_MS,
|
|
131483
|
+
maxBuffer: UPDATE_MAX_BUFFER
|
|
131484
|
+
});
|
|
131485
|
+
return firstVersion(`${stdout2}
|
|
131486
|
+
${stderr}`);
|
|
131487
|
+
} catch {
|
|
131488
|
+
return null;
|
|
131489
|
+
}
|
|
131490
|
+
}
|
|
131491
|
+
var healthScanExecFile = execFileAsync7;
|
|
131492
|
+
var healthScanBackendLoginProbe = defaultBackendLoginProbe;
|
|
131493
|
+
async function defaultBackendLoginProbe(backend) {
|
|
131494
|
+
try {
|
|
131495
|
+
if (backend === "claude") return await detectClaudeLogin();
|
|
131496
|
+
if (backend === "codex") return await isCodexReady();
|
|
131497
|
+
return null;
|
|
131498
|
+
} catch {
|
|
131499
|
+
return null;
|
|
131500
|
+
}
|
|
131501
|
+
}
|
|
131502
|
+
var HEALTH_SCAN_TTL_MS = 10 * 60 * 1e3;
|
|
131503
|
+
var HEALTH_SCAN_PROBE_TIMEOUT_MS = 1e4;
|
|
131504
|
+
var healthScanCache = /* @__PURE__ */ new Map();
|
|
131505
|
+
var BOT_PERMISSION_BASELINE = [
|
|
131506
|
+
{ scope: "im:message", reason: "\u6536\u53D1\u6D88\u606F \u2014\u2014 @ \u89E6\u53D1\u4E0E\u8BDD\u9898\u56DE\u590D\u7684\u6839\u672C" },
|
|
131507
|
+
{ scope: "im:message:send_as_bot", reason: "\u4EE5\u5E94\u7528\u8EAB\u4EFD\u53D1\u6D88\u606F \u2014\u2014 \u7B54\u6848\u6295\u9012" },
|
|
131508
|
+
{ scope: "im:resource", reason: "\u6D88\u606F\u56FE\u7247/\u9644\u4EF6\u4E0B\u8F7D \u2014\u2014 agent \u8BFB\u7528\u6237\u53D1\u7684\u622A\u56FE\u548C\u6587\u4EF6" },
|
|
131509
|
+
{ scope: "im:chat:readonly", reason: "\u7FA4\u4FE1\u606F\u8BFB\u53D6 \u2014\u2014 \u6F0F\u6D88\u606F\u8865\u6293(gap-fill)\u4E0E\u7FA4\u540D\u89E3\u6790" },
|
|
131510
|
+
{ scope: "im:message.reactions:write_only", reason: "\u8868\u60C5\u56DE\u5E94 \u2014\u2014 \u300C\u6536\u5230\u300D\u5373\u65F6\u53CD\u9988" },
|
|
131511
|
+
{ scope: "cardkit:card:read", reason: "\u5361\u7247\u8BFB\u53D6 \u2014\u2014 \u6D41\u5F0F\u7B54\u6848\u5361\u72B6\u6001\u56DE\u8BFB" },
|
|
131512
|
+
{ scope: "cardkit:card:write", reason: "\u5361\u7247\u521B\u5EFA\u4E0E\u66F4\u65B0 \u2014\u2014 \u6D41\u5F0F\u7B54\u6848\u5361\u672C\u4F53" },
|
|
131513
|
+
{ scope: "task:task", reason: "\u4EFB\u52A1\u8BFB\u5199 \u2014\u2014 \u8BDD\u9898\u2194\u4EFB\u52A1\u7ED1\u5B9A(\u4E0D\u7528\u4EFB\u52A1\u6D3E\u5355\u53EF\u4E0D\u5F00)" },
|
|
131514
|
+
{ scope: "task:comment", reason: "\u4EFB\u52A1\u8BC4\u8BBA \u2014\u2014 \u4EFB\u52A1\u8FDB\u5C55\u6C47\u62A5(\u4E0D\u7528\u4EFB\u52A1\u6D3E\u5355\u53EF\u4E0D\u5F00)" }
|
|
131515
|
+
];
|
|
131516
|
+
async function runHealthScan(bot, hostConfigStore) {
|
|
131517
|
+
const credentials = [];
|
|
131518
|
+
const backend = bot.backend || "claude";
|
|
131519
|
+
const login = await healthScanBackendLoginProbe(backend);
|
|
131520
|
+
credentials.push({
|
|
131521
|
+
id: "backend-login",
|
|
131522
|
+
label: backend === "claude" ? "Claude \u767B\u5F55" : backend === "codex" ? "Codex \u767B\u5F55" : `${backend} \u767B\u5F55`,
|
|
131523
|
+
status: login === null ? "unknown" : login ? "ok" : "fail",
|
|
131524
|
+
severity: "required",
|
|
131525
|
+
global: true,
|
|
131526
|
+
...login === false ? { hint: `\u7EC8\u7AEF\u8FD0\u884C \`${backend === "claude" ? "claude" : "codex login"}\` \u5B8C\u6210\u767B\u5F55` } : {}
|
|
131527
|
+
});
|
|
131528
|
+
const profile = bot.lark_cli_profile ?? bot.app_id;
|
|
131529
|
+
try {
|
|
131530
|
+
const { stdout: stdout2 } = await healthScanExecFile(
|
|
131531
|
+
process.env.LARK_CLI_PATH || "lark-cli",
|
|
131532
|
+
["--profile", profile, "auth", "status", "--json"],
|
|
131533
|
+
{ timeout: HEALTH_SCAN_PROBE_TIMEOUT_MS, maxBuffer: 1024 * 1024 }
|
|
131534
|
+
);
|
|
131535
|
+
const payload = healthScanParseJson(stdout2);
|
|
131536
|
+
const botIdentity = payload?.identities?.bot;
|
|
131537
|
+
credentials.push({
|
|
131538
|
+
id: "lark-profile",
|
|
131539
|
+
label: "lark-cli profile",
|
|
131540
|
+
status: botIdentity?.available === true ? "ok" : botIdentity ? "fail" : "unknown",
|
|
131541
|
+
severity: "required",
|
|
131542
|
+
global: false,
|
|
131543
|
+
...botIdentity?.available !== true ? { hint: "\u91CD\u542F Larkway \u4F1A\u81EA\u52A8\u91CD\u65B0\u914D\u7F6E profile;\u82E5\u4ECD\u5931\u8D25,\u68C0\u67E5 keychain \u662F\u5426\u9501\u5B9A" } : {}
|
|
131544
|
+
});
|
|
131545
|
+
} catch {
|
|
131546
|
+
credentials.push({
|
|
131547
|
+
id: "lark-profile",
|
|
131548
|
+
label: "lark-cli profile",
|
|
131549
|
+
status: "unknown",
|
|
131550
|
+
severity: "required",
|
|
131551
|
+
global: false,
|
|
131552
|
+
hint: "lark-cli \u63A2\u6D4B\u5931\u8D25(\u672A\u5B89\u88C5\u6216\u8D85\u65F6)\u2014\u2014 \u88C5\u597D\u5DE5\u5177\u540E\u91CD\u65B0\u4F53\u68C0,\u8FD9\u9879\u4F1A\u81EA\u52A8\u8865\u4E0A"
|
|
131553
|
+
});
|
|
131554
|
+
}
|
|
131555
|
+
const tokenEnv = bot.git_token_env ?? bot.gitlab_token_env;
|
|
131556
|
+
if (Array.isArray(bot.repos) && bot.repos.length > 0 && tokenEnv) {
|
|
131557
|
+
const secret = await hostConfigStore.readSecret(tokenEnv).catch(() => null);
|
|
131558
|
+
credentials.push({
|
|
131559
|
+
id: "git-token",
|
|
131560
|
+
label: "Git \u8BBF\u95EE\u4EE4\u724C",
|
|
131561
|
+
status: secret ? "ok" : "fail",
|
|
131562
|
+
severity: "recommended",
|
|
131563
|
+
global: false,
|
|
131564
|
+
...secret ? {} : { hint: `\u73AF\u5883\u91CC\u6CA1\u8BFB\u5230 ${tokenEnv} \u2014\u2014 \u914D\u597D\u540E clone / \u63A8\u9001 / \u5F00 MR \u624D\u8D70\u8FD9\u4E2A bot \u81EA\u5DF1\u7684\u8D26\u53F7` }
|
|
131565
|
+
});
|
|
131566
|
+
}
|
|
131567
|
+
return {
|
|
131568
|
+
credentials,
|
|
131569
|
+
permissions: {
|
|
131570
|
+
status: "unknown",
|
|
131571
|
+
unknownReason: "lark-cli \u6682\u65F6\u67E5\u4E0D\u5230\u5E94\u7528\u5DF2\u5F00\u901A\u54EA\u4E9B\u6743\u9650(auth scopes \u53EA\u62A5\u7528\u6237\u6388\u6743)\u2014\u2014 \u8BF7\u5BF9\u7167\u57FA\u7EBF\u6E05\u5355\u5728\u5F00\u653E\u5E73\u53F0\u6838\u5BF9",
|
|
131572
|
+
baseline: BOT_PERMISSION_BASELINE,
|
|
131573
|
+
consoleUrl: `https://open.feishu.cn/app/${encodeURIComponent(bot.app_id)}/auth`
|
|
131574
|
+
},
|
|
131575
|
+
checkedAt: Date.now(),
|
|
131576
|
+
fromCache: false
|
|
131577
|
+
};
|
|
131578
|
+
}
|
|
131579
|
+
function healthScanParseJson(text) {
|
|
131580
|
+
const raw = String(text || "").trim();
|
|
131581
|
+
if (!raw) return null;
|
|
131582
|
+
try {
|
|
131583
|
+
const direct = JSON.parse(raw);
|
|
131584
|
+
if (direct && typeof direct === "object" && !Array.isArray(direct)) return direct;
|
|
131585
|
+
} catch {
|
|
131586
|
+
}
|
|
131587
|
+
const first = raw.indexOf("{");
|
|
131588
|
+
const last = raw.lastIndexOf("}");
|
|
131589
|
+
if (first >= 0 && last > first) {
|
|
131590
|
+
try {
|
|
131591
|
+
const parsed = JSON.parse(raw.slice(first, last + 1));
|
|
131592
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
131593
|
+
} catch {
|
|
131594
|
+
}
|
|
131595
|
+
}
|
|
131596
|
+
return null;
|
|
131597
|
+
}
|
|
131598
|
+
var getBotHealthScan = async (req) => {
|
|
131599
|
+
const { ctx, params } = req;
|
|
131600
|
+
const id = params.id;
|
|
131601
|
+
{
|
|
131602
|
+
const e = badBotId(id);
|
|
131603
|
+
if (e) return e;
|
|
131604
|
+
}
|
|
131605
|
+
if (ctx.mode !== "local") {
|
|
131606
|
+
return { status: 403, json: { error: "\u53EA\u652F\u6301\u5728\u672C\u673A\u52A9\u624B\u4E0A\u4E0B\u6587\u4E2D\u4F53\u68C0\u3002" } };
|
|
131607
|
+
}
|
|
131608
|
+
const force = req.method === "POST";
|
|
131609
|
+
const cached = healthScanCache.get(id);
|
|
131610
|
+
if (!force && cached && Date.now() - cached.at < HEALTH_SCAN_TTL_MS) {
|
|
131611
|
+
return { status: 200, json: { ...cached.json, fromCache: true } };
|
|
131612
|
+
}
|
|
131613
|
+
let bot;
|
|
131614
|
+
try {
|
|
131615
|
+
bot = await ctx.stores.botsStore.readBot(id);
|
|
131616
|
+
} catch (e) {
|
|
131617
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
131618
|
+
return { status: msg.includes("not found") ? 404 : 500, json: { error: msg } };
|
|
131619
|
+
}
|
|
131620
|
+
const json2 = await runHealthScan(bot, ctx.stores.hostConfig);
|
|
131621
|
+
healthScanCache.set(id, { json: json2, at: Date.now() });
|
|
131622
|
+
return { status: 200, json: json2 };
|
|
131623
|
+
};
|
|
131452
131624
|
var getContext = async (req) => {
|
|
131453
131625
|
const { ctx } = req;
|
|
131454
131626
|
return {
|
|
@@ -131456,6 +131628,114 @@ var getContext = async (req) => {
|
|
|
131456
131628
|
json: { mode: ctx.mode, centralAvailable: false, version: LARKWAY_VERSION }
|
|
131457
131629
|
};
|
|
131458
131630
|
};
|
|
131631
|
+
var getUpdateVersion = async () => {
|
|
131632
|
+
const [currentVersion, latestVersion] = await Promise.all([
|
|
131633
|
+
readGlobalLarkwayVersion(),
|
|
131634
|
+
readLatestLarkwayVersion()
|
|
131635
|
+
]);
|
|
131636
|
+
return {
|
|
131637
|
+
status: 200,
|
|
131638
|
+
json: {
|
|
131639
|
+
currentVersion,
|
|
131640
|
+
latestVersion,
|
|
131641
|
+
updateAvailable: latestVersion ? compareVersion(latestVersion, currentVersion) > 0 : false
|
|
131642
|
+
}
|
|
131643
|
+
};
|
|
131644
|
+
};
|
|
131645
|
+
var updateJob = { status: "idle", log: [] };
|
|
131646
|
+
var updateJobRunner = defaultUpdateJobRunner;
|
|
131647
|
+
async function defaultUpdateJobRunner(onLine) {
|
|
131648
|
+
const { spawn: spawn6 } = await import("node:child_process");
|
|
131649
|
+
return new Promise((resolve2, reject) => {
|
|
131650
|
+
const child = spawn6("larkway", ["update", "--latest", "--json"], {
|
|
131651
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
131652
|
+
});
|
|
131653
|
+
let buf = "";
|
|
131654
|
+
const feed = (chunk) => {
|
|
131655
|
+
buf += String(chunk);
|
|
131656
|
+
let idx;
|
|
131657
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
131658
|
+
const line = buf.slice(0, idx).trim();
|
|
131659
|
+
buf = buf.slice(idx + 1);
|
|
131660
|
+
if (line) onLine(line);
|
|
131661
|
+
}
|
|
131662
|
+
};
|
|
131663
|
+
child.stdout.on("data", feed);
|
|
131664
|
+
child.stderr.on("data", feed);
|
|
131665
|
+
child.on("error", reject);
|
|
131666
|
+
child.on("close", (code) => {
|
|
131667
|
+
if (buf.trim()) onLine(buf.trim());
|
|
131668
|
+
resolve2({ code: code ?? 1 });
|
|
131669
|
+
});
|
|
131670
|
+
});
|
|
131671
|
+
}
|
|
131672
|
+
function updatePhaseOf(step2) {
|
|
131673
|
+
if (/npm i/i.test(step2)) return { phase: "\u4E0B\u8F7D\u5B89\u88C5", phaseIndex: 1 };
|
|
131674
|
+
if (/stop/i.test(step2)) return { phase: "\u91CD\u542F\u670D\u52A1", phaseIndex: 2 };
|
|
131675
|
+
if (/start/i.test(step2)) return { phase: "\u6536\u5C3E", phaseIndex: 3 };
|
|
131676
|
+
return { phase: step2, phaseIndex: updateJob.phaseIndex ?? 1 };
|
|
131677
|
+
}
|
|
131678
|
+
async function runUpdateJob() {
|
|
131679
|
+
updateJob = {
|
|
131680
|
+
status: "running",
|
|
131681
|
+
phase: "\u51C6\u5907",
|
|
131682
|
+
phaseIndex: 1,
|
|
131683
|
+
phaseCount: 3,
|
|
131684
|
+
log: [],
|
|
131685
|
+
startedAt: Date.now()
|
|
131686
|
+
};
|
|
131687
|
+
const pushLog = (line) => {
|
|
131688
|
+
updateJob.log.push(line);
|
|
131689
|
+
if (updateJob.log.length > 60) updateJob.log.splice(0, updateJob.log.length - 60);
|
|
131690
|
+
};
|
|
131691
|
+
try {
|
|
131692
|
+
const { code } = await updateJobRunner((line) => {
|
|
131693
|
+
pushLog(line);
|
|
131694
|
+
try {
|
|
131695
|
+
const ev = JSON.parse(line);
|
|
131696
|
+
if (typeof ev.step === "string") {
|
|
131697
|
+
const { phase, phaseIndex } = updatePhaseOf(ev.step);
|
|
131698
|
+
updateJob.phase = phase;
|
|
131699
|
+
updateJob.phaseIndex = phaseIndex;
|
|
131700
|
+
}
|
|
131701
|
+
if (ev.ok === false && typeof ev.error === "string") updateJob.error = ev.error;
|
|
131702
|
+
} catch {
|
|
131703
|
+
}
|
|
131704
|
+
});
|
|
131705
|
+
if (code === 0) {
|
|
131706
|
+
updateJob.status = "done";
|
|
131707
|
+
updateJob.phase = "\u5B8C\u6210";
|
|
131708
|
+
updateJob.phaseIndex = updateJob.phaseCount;
|
|
131709
|
+
} else {
|
|
131710
|
+
updateJob.status = "failed";
|
|
131711
|
+
updateJob.error = updateJob.error ?? `updater \u9000\u51FA\u7801 ${code}`;
|
|
131712
|
+
}
|
|
131713
|
+
} catch (e) {
|
|
131714
|
+
updateJob.status = "failed";
|
|
131715
|
+
updateJob.error = e instanceof Error ? e.message : String(e);
|
|
131716
|
+
}
|
|
131717
|
+
updateJob.finishedAt = Date.now();
|
|
131718
|
+
}
|
|
131719
|
+
var postUpdateVersion = async () => {
|
|
131720
|
+
if (updateJob.status === "running") {
|
|
131721
|
+
return { status: 409, json: { ok: false, error: "\u66F4\u65B0\u5DF2\u5728\u8FDB\u884C\u4E2D\u3002" } };
|
|
131722
|
+
}
|
|
131723
|
+
void runUpdateJob();
|
|
131724
|
+
return { status: 200, json: { ok: true, started: true } };
|
|
131725
|
+
};
|
|
131726
|
+
var getUpdateVersionStatus = async () => {
|
|
131727
|
+
return {
|
|
131728
|
+
status: 200,
|
|
131729
|
+
json: {
|
|
131730
|
+
status: updateJob.status,
|
|
131731
|
+
phase: updateJob.phase,
|
|
131732
|
+
phaseIndex: updateJob.phaseIndex,
|
|
131733
|
+
phaseCount: updateJob.phaseCount,
|
|
131734
|
+
error: updateJob.error,
|
|
131735
|
+
log: updateJob.log
|
|
131736
|
+
}
|
|
131737
|
+
};
|
|
131738
|
+
};
|
|
131459
131739
|
var getBots = async (req) => {
|
|
131460
131740
|
const { ctx } = req;
|
|
131461
131741
|
const dir = await ctx.activeBotsDir();
|
|
@@ -132124,8 +132404,13 @@ var getBackends = async (_req) => {
|
|
|
132124
132404
|
};
|
|
132125
132405
|
var ROUTES = {
|
|
132126
132406
|
"GET /api/context": getContext,
|
|
132407
|
+
"GET /api/update_version": getUpdateVersion,
|
|
132408
|
+
"POST /api/update_version": postUpdateVersion,
|
|
132409
|
+
"GET /api/update_version/status": getUpdateVersionStatus,
|
|
132127
132410
|
"GET /api/bots": getBots,
|
|
132128
132411
|
"GET /api/bot/:id": getBot,
|
|
132412
|
+
"GET /api/bot/:id/health-scan": getBotHealthScan,
|
|
132413
|
+
"POST /api/bot/:id/health-scan": getBotHealthScan,
|
|
132129
132414
|
"GET /api/bot/:id/events": getBotEvents,
|
|
132130
132415
|
"PUT /api/bot/:id": putBot,
|
|
132131
132416
|
"DELETE /api/bot/:id": deleteBot2,
|
|
@@ -132366,9 +132651,9 @@ function writeBody(res, status, contentType, buf) {
|
|
|
132366
132651
|
}
|
|
132367
132652
|
async function openBrowser(url) {
|
|
132368
132653
|
try {
|
|
132369
|
-
const { spawn:
|
|
132654
|
+
const { spawn: spawn6 } = await import("node:child_process");
|
|
132370
132655
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
132371
|
-
|
|
132656
|
+
spawn6(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
132372
132657
|
} catch {
|
|
132373
132658
|
}
|
|
132374
132659
|
}
|
|
@@ -133059,7 +133344,7 @@ function resolveOwnerOpenId(profile, _spawnSync = spawnSync) {
|
|
|
133059
133344
|
}
|
|
133060
133345
|
|
|
133061
133346
|
// src/lark/profileBootstrap.ts
|
|
133062
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
133347
|
+
import { spawn as spawn5, spawnSync as spawnSync2 } from "node:child_process";
|
|
133063
133348
|
function deriveLarkCliProfile(explicitProfile, appId) {
|
|
133064
133349
|
return explicitProfile ?? appId;
|
|
133065
133350
|
}
|
package/dist/main.js
CHANGED
|
@@ -118871,6 +118871,9 @@ function extractFeishuDocLinks(text) {
|
|
|
118871
118871
|
if (!matches) return [];
|
|
118872
118872
|
return [...new Set(matches)];
|
|
118873
118873
|
}
|
|
118874
|
+
function isStopCommand(event) {
|
|
118875
|
+
return /^\/stop$/i.test(extractText(event));
|
|
118876
|
+
}
|
|
118874
118877
|
function parseMessage(event) {
|
|
118875
118878
|
let parsedContent = null;
|
|
118876
118879
|
try {
|
|
@@ -122445,6 +122448,16 @@ var BridgeHandler = class {
|
|
|
122445
122448
|
* awaits these — the real turn boundary tests must sync assertions/cleanup on.
|
|
122446
122449
|
*/
|
|
122447
122450
|
inFlightTurns = /* @__PURE__ */ new Set();
|
|
122451
|
+
/**
|
|
122452
|
+
* BL-42 /stop: per-queue-key kill hook for the CURRENTLY RUNNING turn.
|
|
122453
|
+
* run()'s dispatch loop intercepts a bare `/stop` message and invokes the
|
|
122454
|
+
* hook for its queue key instead of enqueueing the message (queueing would
|
|
122455
|
+
* make it wait behind the very turn it is trying to stop). Registered by
|
|
122456
|
+
* handleOne right after the agent subprocess spawns; deleted on every
|
|
122457
|
+
* teardown path. Serial-per-key dispatch guarantees at most one live entry
|
|
122458
|
+
* per key, and register/delete never interleave across turns.
|
|
122459
|
+
*/
|
|
122460
|
+
activeTurnStops = /* @__PURE__ */ new Map();
|
|
122448
122461
|
/**
|
|
122449
122462
|
* v3.2 交接断链检测 (docs/task-handle.md §13, revision 2): per-thread "most
|
|
122450
122463
|
* recently RECEIVED" timestamp, stamped in run()'s for-await loop body —
|
|
@@ -122572,6 +122585,10 @@ var BridgeHandler = class {
|
|
|
122572
122585
|
if (this.closed) break;
|
|
122573
122586
|
if (signal?.aborted) break;
|
|
122574
122587
|
const key = typeof event.root_id === "string" && event.root_id ? event.root_id : typeof event.parent_id === "string" && event.parent_id ? event.parent_id : event.message_id;
|
|
122588
|
+
if (isStopCommand(event)) {
|
|
122589
|
+
this.handleStopCommand(key, event, pendingByKey);
|
|
122590
|
+
continue;
|
|
122591
|
+
}
|
|
122575
122592
|
this.threadReceivedAt.set(key, Date.now());
|
|
122576
122593
|
const legacySessionKey = typeof event.root_id === "string" && event.root_id ? event.root_id : event.message_id;
|
|
122577
122594
|
if (legacySessionKey !== key) this.threadReceivedAt.set(legacySessionKey, Date.now());
|
|
@@ -122595,7 +122612,7 @@ var BridgeHandler = class {
|
|
|
122595
122612
|
`[bridge.handler] coalescing ${followups.length} queued follow-up message(s) into turn ${primary.message_id} on thread ${key}`
|
|
122596
122613
|
);
|
|
122597
122614
|
}
|
|
122598
|
-
return this.handleOne(primary, followups);
|
|
122615
|
+
return this.handleOne(primary, followups, key);
|
|
122599
122616
|
}).catch((err) => {
|
|
122600
122617
|
console.error(`[bridge.handler] unhandled error on thread ${key}:`, err);
|
|
122601
122618
|
}).finally(() => {
|
|
@@ -122615,6 +122632,38 @@ var BridgeHandler = class {
|
|
|
122615
122632
|
async close() {
|
|
122616
122633
|
this.closed = true;
|
|
122617
122634
|
}
|
|
122635
|
+
/**
|
|
122636
|
+
* BL-42 /stop (see run()'s intercept site). Every consumed message —
|
|
122637
|
+
* the /stop itself AND the queued messages it drops — is acknowledged so
|
|
122638
|
+
* neither live-WS dedup nor any gap-fill window (this process or
|
|
122639
|
+
* post-restart) ever re-dispatches them: replaying a dropped message after
|
|
122640
|
+
* a /stop would resurrect exactly the work the user just cancelled.
|
|
122641
|
+
*
|
|
122642
|
+
* Known benign race: between "drain took the primary" and "handleOne
|
|
122643
|
+
* registered the kill hook" (subprocess starting) a /stop finds no hook and
|
|
122644
|
+
* only clears the queue — the user can simply /stop again once the card
|
|
122645
|
+
* shows activity.
|
|
122646
|
+
*/
|
|
122647
|
+
handleStopCommand(key, event, pendingByKey) {
|
|
122648
|
+
this.deps.client.acknowledgeMessage(event.message_id);
|
|
122649
|
+
const pending = pendingByKey.get(key);
|
|
122650
|
+
const droppedCount = pending?.length ?? 0;
|
|
122651
|
+
if (pending) {
|
|
122652
|
+
for (const dropped of pending) this.deps.client.acknowledgeMessage(dropped.message_id);
|
|
122653
|
+
pendingByKey.delete(key);
|
|
122654
|
+
}
|
|
122655
|
+
const stop = this.activeTurnStops.get(key);
|
|
122656
|
+
if (stop) {
|
|
122657
|
+
console.log(
|
|
122658
|
+
`[bridge.handler] /stop: killing in-flight turn on thread ${key}` + (droppedCount > 0 ? ` and dropping ${droppedCount} queued message(s)` : "")
|
|
122659
|
+
);
|
|
122660
|
+
stop();
|
|
122661
|
+
} else {
|
|
122662
|
+
console.log(
|
|
122663
|
+
`[bridge.handler] /stop on thread ${key}: no in-flight turn` + (droppedCount > 0 ? `; dropped ${droppedCount} queued message(s)` : " (nothing to do)")
|
|
122664
|
+
);
|
|
122665
|
+
}
|
|
122666
|
+
}
|
|
122618
122667
|
// ---------------------------------------------------------------------------
|
|
122619
122668
|
// Private: single-event lifecycle
|
|
122620
122669
|
// ---------------------------------------------------------------------------
|
|
@@ -122625,7 +122674,7 @@ var BridgeHandler = class {
|
|
|
122625
122674
|
* prompt and settle together with the primary; everything else about the
|
|
122626
122675
|
* turn (card, session, task probes) is driven by `event` alone.
|
|
122627
122676
|
*/
|
|
122628
|
-
async handleOne(event, followups = []) {
|
|
122677
|
+
async handleOne(event, followups = [], queueKey) {
|
|
122629
122678
|
const settleMessageId = event.message_id;
|
|
122630
122679
|
let settled = false;
|
|
122631
122680
|
let agentRunCompleted = false;
|
|
@@ -122885,7 +122934,8 @@ var BridgeHandler = class {
|
|
|
122885
122934
|
});
|
|
122886
122935
|
}
|
|
122887
122936
|
};
|
|
122888
|
-
|
|
122937
|
+
const triggerInTopic = realTopicThreadId(parsed.raw.thread_id) !== void 0;
|
|
122938
|
+
if (!isNewThread && (triggerInTopic || !taskCardAnchorId)) await createCotBubble(messageId);
|
|
122889
122939
|
const createCardKitPlaceholder = async () => {
|
|
122890
122940
|
if (!card && cardKitAvailable && this.deps.cardKitClient) {
|
|
122891
122941
|
try {
|
|
@@ -123187,7 +123237,7 @@ var BridgeHandler = class {
|
|
|
123187
123237
|
console.warn("[bridge.handler] mtime fact computation failed (continuing):", err);
|
|
123188
123238
|
}
|
|
123189
123239
|
}
|
|
123190
|
-
|
|
123240
|
+
{
|
|
123191
123241
|
const anchorMessageId = cardKitProgress?.messageId ?? card?.messageId ?? messageId;
|
|
123192
123242
|
await createCotBubble(anchorMessageId);
|
|
123193
123243
|
}
|
|
@@ -123317,6 +123367,7 @@ var BridgeHandler = class {
|
|
|
123317
123367
|
});
|
|
123318
123368
|
let lastActivityAt = Date.now();
|
|
123319
123369
|
let interruptedByIdle = false;
|
|
123370
|
+
let stoppedByUser = false;
|
|
123320
123371
|
let toolsInFlight = 0;
|
|
123321
123372
|
let toolUseTotalCount = 0;
|
|
123322
123373
|
let idleWatchdog;
|
|
@@ -123336,6 +123387,15 @@ var BridgeHandler = class {
|
|
|
123336
123387
|
}, cadenceMs);
|
|
123337
123388
|
idleWatchdog.unref?.();
|
|
123338
123389
|
}
|
|
123390
|
+
if (queueKey) {
|
|
123391
|
+
this.activeTurnStops.set(queueKey, () => {
|
|
123392
|
+
stoppedByUser = true;
|
|
123393
|
+
try {
|
|
123394
|
+
handle.kill();
|
|
123395
|
+
} catch {
|
|
123396
|
+
}
|
|
123397
|
+
});
|
|
123398
|
+
}
|
|
123339
123399
|
let pidFileWriteSettled = Promise.resolve();
|
|
123340
123400
|
if (isAgentWorkspace && handle.pid != null) {
|
|
123341
123401
|
const sessionPidFile = path10.join(worktreePath, ".larkway", "runner.pid");
|
|
@@ -123373,11 +123433,12 @@ var BridgeHandler = class {
|
|
|
123373
123433
|
clearInterval(idleWatchdog);
|
|
123374
123434
|
idleWatchdog = void 0;
|
|
123375
123435
|
}
|
|
123376
|
-
|
|
123436
|
+
if (queueKey) this.activeTurnStops.delete(queueKey);
|
|
123437
|
+
cotTurnOutcome = interruptedByIdle || stoppedByUser ? "error" : "done";
|
|
123377
123438
|
if (cotPublisher) {
|
|
123378
123439
|
void cotPublisher.finalize(
|
|
123379
|
-
|
|
123380
|
-
interruptedByIdle ? { message: "idle timeout" } : void 0
|
|
123440
|
+
cotTurnOutcome,
|
|
123441
|
+
stoppedByUser ? { message: "stopped by user" } : interruptedByIdle ? { message: "idle timeout" } : void 0
|
|
123381
123442
|
).catch(() => {
|
|
123382
123443
|
});
|
|
123383
123444
|
}
|
|
@@ -123443,7 +123504,7 @@ var BridgeHandler = class {
|
|
|
123443
123504
|
}
|
|
123444
123505
|
const reportedStatus = reportedState?.status;
|
|
123445
123506
|
const reportedError = reportedState?.error;
|
|
123446
|
-
const cardKitTimeoutFailure = interruptedByIdle && reportedStatus !== "ready" && reportedStatus !== "failed";
|
|
123507
|
+
const cardKitTimeoutFailure = !stoppedByUser && interruptedByIdle && reportedStatus !== "ready" && reportedStatus !== "failed";
|
|
123447
123508
|
let success;
|
|
123448
123509
|
let failureReason;
|
|
123449
123510
|
if (reportedStatus === "failed") {
|
|
@@ -123451,6 +123512,14 @@ var BridgeHandler = class {
|
|
|
123451
123512
|
failureReason = reportedError ?? "bot \u62A5\u544A failed (\u65E0 error \u5B57\u6BB5)";
|
|
123452
123513
|
} else if (reportedStatus === "ready") {
|
|
123453
123514
|
success = true;
|
|
123515
|
+
} else if (stoppedByUser) {
|
|
123516
|
+
success = false;
|
|
123517
|
+
failureReason = "\u5DF2\u88AB\u7528\u6237 /stop \u505C\u6B62";
|
|
123518
|
+
await recordEvent({
|
|
123519
|
+
status: "running",
|
|
123520
|
+
appendPath: "\u7528\u6237\u505C\u6B62",
|
|
123521
|
+
reason: failureReason
|
|
123522
|
+
});
|
|
123454
123523
|
} else if (cardKitTimeoutFailure) {
|
|
123455
123524
|
success = false;
|
|
123456
123525
|
failureReason = `agent turn idle for ${idleTimeoutMs}ms with no activity (treated as stuck); run interrupted`;
|
|
@@ -123517,15 +123586,15 @@ var BridgeHandler = class {
|
|
|
123517
123586
|
const fallbackAnswer = trustedAnswerText.trim() || cardKitProgress?.answerText.trim() || "";
|
|
123518
123587
|
const noOutputFallback = "\u26A0\uFE0F \u672C\u8F6E agent \u6CA1\u6709\u4EA7\u51FA\u6B63\u6587\uFF0C\u4E5F\u6CA1\u6709\u5199\u5165\u6709\u6548\u72B6\u6001(state.json)\u3002\n" + (result.exitCode !== 0 ? `agent \u8FDB\u7A0B\u5F02\u5E38\u9000\u51FA(exit code ${result.exitCode})\u3002
|
|
123519
123588
|
` : "") + "\u4E0B\u4E00\u6B65\uFF1A\u6362\u4E2A\u8BF4\u6CD5\u91CD\u8BD5\uFF0C\u6216\u65B0\u5F00\u4E00\u4E2A\u8BDD\u9898\u7EE7\u7EED\uFF1B\u82E5\u53CD\u590D\u5982\u6B64\uFF0C\u8BF7\u8BA9\u7EF4\u62A4\u8005\u67E5\u770B\u8BE5 session \u65E5\u5FD7\u3002";
|
|
123520
|
-
const cardBody = stuckResetTriggered ? "\u26A0\uFE0F \u672C\u8F6E\u88AB\u4E2D\u65AD\uFF08\u957F\u65F6\u95F4\u65E0\u6D3B\u6027\uFF0C\u5224\u5B9A\u5361\u6B7B\uFF09\u3002\u8FDE\u7EED\u591A\u6B21\u5361\u6B7B\uFF0C\u5DF2\u91CD\u7F6E\u672C\u8BDD\u9898\u4E0A\u4E0B\u6587 \u2014\u2014 \u4E0B\u6B21 @ \u6211\u5C06\u5168\u65B0\u5F00\u59CB\uFF0C\u8BF7\u628A\u9700\u6C42\u91CD\u65B0\u8BF4\u4E00\u904D\u3002" : cardKitTimeoutFailure ? "\u26A0\uFE0F \u672C\u8F6E\u88AB\u4E2D\u65AD\uFF08\u957F\u65F6\u95F4\u65E0\u6D3B\u6027\uFF0C\u5224\u5B9A\u5361\u6B7B\uFF09\uFF0C\u672A\u5B8C\u6210\u3002\u8BF7\u91CD\u8BD5\u3002" : reportedState?.last_message ?? (fallbackAnswer ? fallbackAnswer : noOutputFallback);
|
|
123589
|
+
const cardBody = stoppedByUser ? "\u23F9 \u5DF2\u6309 /stop \u505C\u6B62\u672C\u8F6E\u3002\u672C\u8BDD\u9898 session \u4FDD\u7559\uFF0C\u9700\u8981\u7EE7\u7EED\u65F6\u518D @ \u6211\u3002" : stuckResetTriggered ? "\u26A0\uFE0F \u672C\u8F6E\u88AB\u4E2D\u65AD\uFF08\u957F\u65F6\u95F4\u65E0\u6D3B\u6027\uFF0C\u5224\u5B9A\u5361\u6B7B\uFF09\u3002\u8FDE\u7EED\u591A\u6B21\u5361\u6B7B\uFF0C\u5DF2\u91CD\u7F6E\u672C\u8BDD\u9898\u4E0A\u4E0B\u6587 \u2014\u2014 \u4E0B\u6B21 @ \u6211\u5C06\u5168\u65B0\u5F00\u59CB\uFF0C\u8BF7\u628A\u9700\u6C42\u91CD\u65B0\u8BF4\u4E00\u904D\u3002" : cardKitTimeoutFailure ? "\u26A0\uFE0F \u672C\u8F6E\u88AB\u4E2D\u65AD\uFF08\u957F\u65F6\u95F4\u65E0\u6D3B\u6027\uFF0C\u5224\u5B9A\u5361\u6B7B\uFF09\uFF0C\u672A\u5B8C\u6210\u3002\u8BF7\u91CD\u8BD5\u3002" : reportedState?.last_message ?? (fallbackAnswer ? fallbackAnswer : noOutputFallback);
|
|
123521
123590
|
const noReportThisTurn = reportedState === null;
|
|
123522
123591
|
const neutralTitle = noReportThisTurn && success && !failureReason ? "\u{1F4AC} \u5DF2\u56DE\u590D" : void 0;
|
|
123523
123592
|
const baseCardPayload = {
|
|
123524
123593
|
finalText: cardBody,
|
|
123525
123594
|
success,
|
|
123526
123595
|
failureReason,
|
|
123527
|
-
titleOverride: reportedState?.card_title ?? (cardKitTimeoutFailure ? "\u5DF2\u4E2D\u65AD" : neutralTitle),
|
|
123528
|
-
colorOverride: reportedState?.card_color ?? (neutralTitle ? "neutral" : void 0),
|
|
123596
|
+
titleOverride: reportedState?.card_title ?? (stoppedByUser ? "\u5DF2\u505C\u6B62" : cardKitTimeoutFailure ? "\u5DF2\u4E2D\u65AD" : neutralTitle),
|
|
123597
|
+
colorOverride: reportedState?.card_color ?? (stoppedByUser || neutralTitle ? "neutral" : void 0),
|
|
123529
123598
|
// V2 dynamic-choice buttons — agent-declared, rendered verbatim.
|
|
123530
123599
|
// reportedState is null when state.json wasn't freshly written
|
|
123531
123600
|
// (stale-guard), so stale leftover choices never reappear.
|
|
@@ -123700,6 +123769,7 @@ var BridgeHandler = class {
|
|
|
123700
123769
|
clearInterval(idleWatchdog);
|
|
123701
123770
|
idleWatchdog = void 0;
|
|
123702
123771
|
}
|
|
123772
|
+
if (queueKey) this.activeTurnStops.delete(queueKey);
|
|
123703
123773
|
const errMsg = String(spawnErr.message ?? spawnErr);
|
|
123704
123774
|
const isStaleSessionErr = errMsg.includes("No conversation found") || errMsg.includes("thread/resume failed") && errMsg.includes("no rollout found");
|
|
123705
123775
|
if (attempt === 1 && currentExisting != null && isStaleSessionErr) {
|
|
@@ -123802,6 +123872,7 @@ var BridgeHandler = class {
|
|
|
123802
123872
|
void bubbleCreate.then((handle) => handle.finalize(cotTurnOutcome).catch(() => {
|
|
123803
123873
|
}));
|
|
123804
123874
|
}
|
|
123875
|
+
if (queueKey) this.activeTurnStops.delete(queueKey);
|
|
123805
123876
|
settle(false);
|
|
123806
123877
|
}
|
|
123807
123878
|
}
|
|
@@ -130237,7 +130308,7 @@ async function reapOrphanedWarmProcess(pidFilePath) {
|
|
|
130237
130308
|
}
|
|
130238
130309
|
|
|
130239
130310
|
// src/lark/profileBootstrap.ts
|
|
130240
|
-
import { spawnSync } from "node:child_process";
|
|
130311
|
+
import { spawn as spawn6, spawnSync } from "node:child_process";
|
|
130241
130312
|
function ensureLarkCliProfile(botId, profileName, appId, appSecret, _spawnSync = spawnSync, _console = console) {
|
|
130242
130313
|
_console.log(
|
|
130243
130314
|
`[larkway] bot "${botId}": provisioning lark-cli profile "${profileName}" for app ${appId.slice(0, 8)}\u2026`
|