@ganglion/xacpx 0.9.2 → 0.10.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/dist/bridge/bridge-main.js +170 -80
- package/dist/channels/types.d.ts +11 -0
- package/dist/channels/weixin-channel.d.ts +8 -0
- package/dist/cli.js +1456 -833
- package/dist/commands/handlers/later-handler.d.ts +3 -3
- package/dist/commands/parse-command.d.ts +1 -0
- package/dist/commands/router-types.d.ts +1 -1
- package/dist/config/config-store.d.ts +44 -5
- package/dist/config/types.d.ts +8 -0
- package/dist/i18n/types.d.ts +7 -3
- package/dist/orchestration/coordinator-identity.d.ts +27 -0
- package/dist/orchestration/orchestration-service.d.ts +0 -23
- package/dist/perf/perf-log-writer.d.ts +1 -0
- package/dist/plugin-api.js +24 -8
- package/dist/scheduled/scheduled-service.d.ts +5 -2
- package/dist/sessions/session-service.d.ts +7 -3
- package/dist/state/state-store.d.ts +69 -2
- package/dist/util/private-file.d.ts +12 -0
- package/package.json +1 -1
|
@@ -210,6 +210,54 @@ var init_spawn_command = __esm(() => {
|
|
|
210
210
|
SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
// src/process/terminate-process-tree.ts
|
|
214
|
+
import { spawn } from "node:child_process";
|
|
215
|
+
async function terminateProcessTree(pid, options = {}, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
|
|
216
|
+
process.kill(targetPid, signal);
|
|
217
|
+
}, isProcessRunning = defaultIsProcessRunning) {
|
|
218
|
+
if (pid <= 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (platform === "win32") {
|
|
222
|
+
try {
|
|
223
|
+
await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
|
|
224
|
+
} catch {}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const targetPid = options.detachedProcessGroup ? -pid : pid;
|
|
228
|
+
try {
|
|
229
|
+
killProcess(targetPid, "SIGTERM");
|
|
230
|
+
} catch {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const deadline = Date.now() + 5000;
|
|
234
|
+
while (Date.now() < deadline) {
|
|
235
|
+
if (!isProcessRunning(targetPid)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
killProcess(targetPid, "SIGKILL");
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
function defaultIsProcessRunning(pid) {
|
|
245
|
+
try {
|
|
246
|
+
process.kill(pid, 0);
|
|
247
|
+
return true;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function defaultRunProcessCommand(command, args) {
|
|
253
|
+
return await new Promise((resolve, reject) => {
|
|
254
|
+
const child = spawn(command, args, { stdio: "ignore" });
|
|
255
|
+
child.on("error", reject);
|
|
256
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
var init_terminate_process_tree = () => {};
|
|
260
|
+
|
|
213
261
|
// src/transport/prompt-media.ts
|
|
214
262
|
import { mkdtemp, open, rm, writeFile } from "node:fs/promises";
|
|
215
263
|
import { tmpdir as defaultTmpdir } from "node:os";
|
|
@@ -645,7 +693,7 @@ var init_streaming_prompt = __esm(() => {
|
|
|
645
693
|
});
|
|
646
694
|
|
|
647
695
|
// src/recovery/discover-parent-package-paths.ts
|
|
648
|
-
import { spawn } from "node:child_process";
|
|
696
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
649
697
|
import { createRequire as createRequire2 } from "node:module";
|
|
650
698
|
import { access } from "node:fs/promises";
|
|
651
699
|
import { homedir } from "node:os";
|
|
@@ -739,7 +787,7 @@ async function defaultQueryPackageManagerRoot(tool) {
|
|
|
739
787
|
};
|
|
740
788
|
let child;
|
|
741
789
|
try {
|
|
742
|
-
child =
|
|
790
|
+
child = spawn2(spec.cmd, spec.args, {
|
|
743
791
|
stdio: ["ignore", "pipe", "pipe"],
|
|
744
792
|
shell: process.platform === "win32"
|
|
745
793
|
});
|
|
@@ -778,54 +826,6 @@ var init_discover_parent_package_paths = __esm(() => {
|
|
|
778
826
|
require2 = createRequire2(import.meta.url);
|
|
779
827
|
});
|
|
780
828
|
|
|
781
|
-
// src/process/terminate-process-tree.ts
|
|
782
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
783
|
-
async function terminateProcessTree(pid, options = {}, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
|
|
784
|
-
process.kill(targetPid, signal);
|
|
785
|
-
}, isProcessRunning = defaultIsProcessRunning) {
|
|
786
|
-
if (pid <= 0) {
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
if (platform === "win32") {
|
|
790
|
-
try {
|
|
791
|
-
await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
|
|
792
|
-
} catch {}
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
const targetPid = options.detachedProcessGroup ? -pid : pid;
|
|
796
|
-
try {
|
|
797
|
-
killProcess(targetPid, "SIGTERM");
|
|
798
|
-
} catch {
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
const deadline = Date.now() + 5000;
|
|
802
|
-
while (Date.now() < deadline) {
|
|
803
|
-
if (!isProcessRunning(targetPid)) {
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
807
|
-
}
|
|
808
|
-
try {
|
|
809
|
-
killProcess(targetPid, "SIGKILL");
|
|
810
|
-
} catch {}
|
|
811
|
-
}
|
|
812
|
-
function defaultIsProcessRunning(pid) {
|
|
813
|
-
try {
|
|
814
|
-
process.kill(pid, 0);
|
|
815
|
-
return true;
|
|
816
|
-
} catch {
|
|
817
|
-
return false;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
async function defaultRunProcessCommand(command, args) {
|
|
821
|
-
return await new Promise((resolve, reject) => {
|
|
822
|
-
const child = spawn2(command, args, { stdio: "ignore" });
|
|
823
|
-
child.on("error", reject);
|
|
824
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
var init_terminate_process_tree = () => {};
|
|
828
|
-
|
|
829
829
|
// src/util/text.ts
|
|
830
830
|
function truncateText(text, maxLength, ellipsis = "…") {
|
|
831
831
|
if (text.length <= maxLength)
|
|
@@ -860,6 +860,11 @@ var init_session = __esm(() => {
|
|
|
860
860
|
currentLabel: "[current]",
|
|
861
861
|
sessionListItem: (alias, agent, workspace) => `- ${alias} (${agent} @ ${workspace})`,
|
|
862
862
|
sessionCreated: (alias) => `Session "${alias}" created and switched.`,
|
|
863
|
+
sessionAlreadyExists: (alias, agent, workspace) => [
|
|
864
|
+
`Session "${alias}" already exists (${agent} @ ${workspace}).`,
|
|
865
|
+
`Switch to it with /use ${alias}, or remove it first with /session rm ${alias}.`
|
|
866
|
+
].join(`
|
|
867
|
+
`),
|
|
863
868
|
sessionAttachNotFound: (alias, agent, workspace) => [
|
|
864
869
|
"No existing session found to attach.",
|
|
865
870
|
`Check the session name and retry: /session attach ${alias} --agent ${agent} --ws ${workspace} --name <session-name>`
|
|
@@ -896,6 +901,7 @@ var init_session = __esm(() => {
|
|
|
896
901
|
sessionBlockedByTasksHint: "Use /tasks to list tasks, or /task cancel <id> to cancel one.",
|
|
897
902
|
sessionRemoved: (alias) => `Session "${alias}" removed.`,
|
|
898
903
|
sessionRemovedWasActive: "This was the active session. Its chat context has been cleared.",
|
|
904
|
+
sessionRemovedWasActivePromoted: (alias) => `This was the active session. Switched back to the previous session "${alias}".`,
|
|
899
905
|
sessionTransportShared: (transportSession, count) => `Note: backend session "${transportSession}" is still referenced by ${count} other session(s) and was not closed.`,
|
|
900
906
|
sessionOrchestrationPurgeFailed: (warning) => `Note: failed to purge orchestration references (${warning}). Run /tasks clean manually to clean up.`,
|
|
901
907
|
sessionTransportTeardownFailed: (warning) => `Note: backend session could not be closed automatically (${warning}). Run acpx sessions close manually if needed.`,
|
|
@@ -1189,7 +1195,7 @@ var init_later = __esm(() => {
|
|
|
1189
1195
|
helpNote2: "Time must be at least 10 seconds and at most 7 days away",
|
|
1190
1196
|
helpNote3: "By default runs in a new temporary session that is destroyed after completion",
|
|
1191
1197
|
helpNote4: "Use --bind to send to the session that was current when the task was created (configurable via later.defaultMode)",
|
|
1192
|
-
helpNote5: "/lt list shows
|
|
1198
|
+
helpNote5: "/lt list shows only this chat's pending tasks; in group chats only the owner can cancel",
|
|
1193
1199
|
helpNote6: "Scheduling slash-prefixed xacpx commands is not supported",
|
|
1194
1200
|
helpNote7: "Full time format reference: docs/later-command.md"
|
|
1195
1201
|
};
|
|
@@ -1618,7 +1624,6 @@ var init_cli_update = __esm(() => {
|
|
|
1618
1624
|
updateFailed: (name, error) => `${name} update failed: ${error}`,
|
|
1619
1625
|
targetNotFound: (name) => `Update target not found: ${name}`,
|
|
1620
1626
|
targetVersionUnknown: (name) => `${name}: cannot check latest version; skipped.`,
|
|
1621
|
-
targetNotPinned: (name) => `${name} has no recorded version; use \`xacpx plugin update ${name}\` or specify a version explicitly.`,
|
|
1622
1627
|
multiTargetNonInteractive: "Installed plugins detected; in non-interactive mode use `xacpx update --all` or `xacpx update <name>`.",
|
|
1623
1628
|
selectionPrompt: "Select items to update (numbers, comma-separated; a=all; Enter to cancel): ",
|
|
1624
1629
|
selectionInvalid: (part) => `Invalid selection: ${part}`,
|
|
@@ -1683,6 +1688,8 @@ var init_plugin_cli = __esm(() => {
|
|
|
1683
1688
|
noPlugins: "No plugins installed yet.",
|
|
1684
1689
|
pluginListHeader: "Plugins:",
|
|
1685
1690
|
unrecognizedArgs: (args) => `Unrecognized arguments: ${args}`,
|
|
1691
|
+
pluginSpecHasDoubleQuote: (spec) => `Invalid plugin spec ${spec}: double quotes (") are never valid in an npm package spec.`,
|
|
1692
|
+
pluginSpecHasPercentOnWindows: (spec) => `Invalid plugin spec ${spec}: "%" would be mangled by cmd.exe on Windows. Install the package with npm directly instead.`,
|
|
1686
1693
|
pluginInstallFailed: (packageSpec, error) => `Plugin ${packageSpec} install failed: ${error}`,
|
|
1687
1694
|
pluginValidateFailed: (recordedName, error) => `Plugin ${recordedName} validation failed: ${error}`,
|
|
1688
1695
|
pluginInstalled: (recordedName) => `Plugin ${recordedName} installed`,
|
|
@@ -1794,8 +1801,6 @@ var init_weixin = __esm(() => {
|
|
|
1794
1801
|
debugEnabled: "Debug mode enabled",
|
|
1795
1802
|
debugDisabled: "Debug mode disabled",
|
|
1796
1803
|
sessionCleared: "✅ Session cleared. Starting a fresh conversation.",
|
|
1797
|
-
noAccountsLoggedIn: "No accounts are currently logged in.",
|
|
1798
|
-
logoutSuccess: "✅ Logged out. All account credentials cleared.",
|
|
1799
1804
|
commandFailed: (detail) => `❌ Command failed: ${detail}`
|
|
1800
1805
|
};
|
|
1801
1806
|
});
|
|
@@ -1833,6 +1838,7 @@ var init_misc = __esm(() => {
|
|
|
1833
1838
|
quotaOverflowSummary: (count) => `(${count} progress updates omitted due to message limit; see final result below)`,
|
|
1834
1839
|
finalHeadsUp: (total, sentSoFar, remaining) => `—
|
|
1835
1840
|
\uD83D\uDCC4 Result: ${total} parts total, ${sentSoFar} sent. Reply /jx to see the next ${remaining} parts.`,
|
|
1841
|
+
finalAllParked: (count) => `\uD83D\uDCC4 Message limit reached: the result (${count} parts) is parked. Reply /jx to receive it.`,
|
|
1836
1842
|
quotedMessagePrefix: (parts) => `[Quote: ${parts}]`,
|
|
1837
1843
|
scheduledTaskFailed: (message) => `Scheduled task failed: ${message}`,
|
|
1838
1844
|
orchestrationTaskCompleted: (taskId, workerSession, result) => `Delegation task "${taskId}" completed
|
|
@@ -1861,6 +1867,8 @@ var init_misc = __esm(() => {
|
|
|
1861
1867
|
delegateQPackageInstr3: "Do not forward the human's exact words to the worker",
|
|
1862
1868
|
commandAccessDeniedSuffix: " is restricted to group owner only.",
|
|
1863
1869
|
commandAccessDeniedHint: "To perform control operations, have the owner send them in the group, or use a private chat.",
|
|
1870
|
+
commandAccessDeniedChatTypeMissingSuffix: " was blocked: this channel did not report the chat type (direct or group), so control commands are disabled here.",
|
|
1871
|
+
commandAccessDeniedChatTypeMissingHint: "Read-only commands and prompts still work. This is a channel metadata issue — update or report the channel plugin.",
|
|
1864
1872
|
commandLabelThisMessage: "This message",
|
|
1865
1873
|
sessionResetNoCurrentSession: "No session is currently selected. Run /session new ... or /use <alias> first.",
|
|
1866
1874
|
sessionResetFailed: (alias) => `Session "${alias}" reset failed. The new backend session was not created, please try again later.`,
|
|
@@ -1931,6 +1939,11 @@ var init_session2 = __esm(() => {
|
|
|
1931
1939
|
currentLabel: "[当前]",
|
|
1932
1940
|
sessionListItem: (alias, agent2, workspace2) => `- ${alias} (${agent2} @ ${workspace2})`,
|
|
1933
1941
|
sessionCreated: (alias) => `会话「${alias}」已创建并切换`,
|
|
1942
|
+
sessionAlreadyExists: (alias, agent2, workspace2) => [
|
|
1943
|
+
`会话「${alias}」已存在(${agent2} @ ${workspace2})。`,
|
|
1944
|
+
`发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
|
|
1945
|
+
].join(`
|
|
1946
|
+
`),
|
|
1934
1947
|
sessionAttachNotFound: (alias, agent2, workspace2) => [
|
|
1935
1948
|
"没有找到可绑定的已有会话。",
|
|
1936
1949
|
`请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent2} --ws ${workspace2} --name <会话名>`
|
|
@@ -1967,6 +1980,7 @@ var init_session2 = __esm(() => {
|
|
|
1967
1980
|
sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
|
|
1968
1981
|
sessionRemoved: (alias) => `已删除会话「${alias}」。`,
|
|
1969
1982
|
sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
|
|
1983
|
+
sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
|
|
1970
1984
|
sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
|
|
1971
1985
|
sessionOrchestrationPurgeFailed: (warning) => `提示:清理任务编排引用失败(${warning}),请稍后执行 /tasks clean 手动清理。`,
|
|
1972
1986
|
sessionTransportTeardownFailed: (warning) => `提示:后端会话未能自动关闭(${warning}),如有残留请手动执行 acpx sessions close。`,
|
|
@@ -2260,7 +2274,7 @@ var init_later2 = __esm(() => {
|
|
|
2260
2274
|
helpNote2: "时间必须在 10 秒之后、7 天之内",
|
|
2261
2275
|
helpNote3: "默认在为本次任务新建的临时会话里执行,跑完即销毁",
|
|
2262
2276
|
helpNote4: "加 --bind 改为发送到创建时绑定的当前会话(默认模式可用 later.defaultMode 配置)",
|
|
2263
|
-
helpNote5: "/lt list
|
|
2277
|
+
helpNote5: "/lt list 只显示本聊天创建的待执行任务;群聊中只有群主可取消",
|
|
2264
2278
|
helpNote6: "不支持延迟执行 / 开头的 xacpx 命令",
|
|
2265
2279
|
helpNote7: "完整时间格式与说明见 docs/later-command.md"
|
|
2266
2280
|
};
|
|
@@ -2689,7 +2703,6 @@ var init_cli_update2 = __esm(() => {
|
|
|
2689
2703
|
updateFailed: (name, error) => `${name} 更新失败:${error}`,
|
|
2690
2704
|
targetNotFound: (name) => `没有找到更新项:${name}`,
|
|
2691
2705
|
targetVersionUnknown: (name) => `${name} 无法检查最新版本,已跳过。`,
|
|
2692
|
-
targetNotPinned: (name) => `${name} 未记录当前版本;请先使用 \`xacpx plugin update ${name}\` 或显式选择版本。`,
|
|
2693
2706
|
multiTargetNonInteractive: "检测到已安装插件;非交互模式请使用 `xacpx update --all` 或 `xacpx update <name>`。",
|
|
2694
2707
|
selectionPrompt: "请选择要更新的项目(数字,逗号分隔,a=全部,回车取消):",
|
|
2695
2708
|
selectionInvalid: (part) => `无效选择:${part}`,
|
|
@@ -2754,6 +2767,8 @@ var init_plugin_cli2 = __esm(() => {
|
|
|
2754
2767
|
noPlugins: "还没有安装插件。",
|
|
2755
2768
|
pluginListHeader: "插件:",
|
|
2756
2769
|
unrecognizedArgs: (args) => `未识别的参数:${args}`,
|
|
2770
|
+
pluginSpecHasDoubleQuote: (spec) => `非法插件 spec ${spec}:npm 包 spec 不允许包含双引号 (")。`,
|
|
2771
|
+
pluginSpecHasPercentOnWindows: (spec) => `非法插件 spec ${spec}:Windows 上 cmd.exe 会展开 %,无法安全传递。请改用 npm 直接安装该包。`,
|
|
2757
2772
|
pluginInstallFailed: (packageSpec, error) => `插件 ${packageSpec} 安装失败:${error}`,
|
|
2758
2773
|
pluginValidateFailed: (recordedName, error) => `插件 ${recordedName} 校验失败:${error}`,
|
|
2759
2774
|
pluginInstalled: (recordedName) => `插件 ${recordedName} 已安装`,
|
|
@@ -2865,8 +2880,6 @@ var init_weixin2 = __esm(() => {
|
|
|
2865
2880
|
debugEnabled: "Debug 模式已开启",
|
|
2866
2881
|
debugDisabled: "Debug 模式已关闭",
|
|
2867
2882
|
sessionCleared: "✅ 会话已清除,重新开始对话",
|
|
2868
|
-
noAccountsLoggedIn: "当前没有已登录的账号",
|
|
2869
|
-
logoutSuccess: "✅ 已退出登录,清除所有账号凭证",
|
|
2870
2883
|
commandFailed: (detail) => `❌ 指令执行失败: ${detail}`
|
|
2871
2884
|
};
|
|
2872
2885
|
});
|
|
@@ -2904,6 +2917,7 @@ var init_misc2 = __esm(() => {
|
|
|
2904
2917
|
quotaOverflowSummary: (count) => `(因消息次数限制省略 ${count} 条进度,请继续查看下方最终结果)`,
|
|
2905
2918
|
finalHeadsUp: (total, sentSoFar, remaining) => `—
|
|
2906
2919
|
\uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`,
|
|
2920
|
+
finalAllParked: (count) => `\uD83D\uDCC4 已达消息上限:结果共 ${count} 段已暂存。回复 /jx 接收。`,
|
|
2907
2921
|
quotedMessagePrefix: (parts) => `[引用: ${parts}]`,
|
|
2908
2922
|
scheduledTaskFailed: (message) => `定时任务执行失败:${message}`,
|
|
2909
2923
|
orchestrationTaskCompleted: (taskId, workerSession, result) => `委派任务「${taskId}」已完成
|
|
@@ -2932,6 +2946,8 @@ var init_misc2 = __esm(() => {
|
|
|
2932
2946
|
delegateQPackageInstr3: "不要直接把 human 原话转发给 worker",
|
|
2933
2947
|
commandAccessDeniedSuffix: " 仅限群创建者/频道 owner 使用。",
|
|
2934
2948
|
commandAccessDeniedHint: "如果需要执行控制类操作,请由 owner 在群内发送,或改用私聊。",
|
|
2949
|
+
commandAccessDeniedChatTypeMissingSuffix: " 已被拦截:该频道未上报会话类型(直聊/群聊),控制类命令在此暂不可用。",
|
|
2950
|
+
commandAccessDeniedChatTypeMissingHint: "只读命令与普通对话不受影响。这是频道元数据问题,请升级或反馈该频道插件。",
|
|
2935
2951
|
commandLabelThisMessage: "该消息",
|
|
2936
2952
|
sessionResetNoCurrentSession: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。",
|
|
2937
2953
|
sessionResetFailed: (alias) => `会话「${alias}」重置失败。
|
|
@@ -3353,6 +3369,19 @@ function normalizeBridgePermissionMode(value) {
|
|
|
3353
3369
|
function normalizeBridgeNonInteractivePermissions(value) {
|
|
3354
3370
|
return value === "deny" || value === "fail" ? value : "deny";
|
|
3355
3371
|
}
|
|
3372
|
+
function normalizeBridgePermissionPolicy(value) {
|
|
3373
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
return value;
|
|
3377
|
+
}
|
|
3378
|
+
function normalizeBridgeSessionInitTimeoutMs(value) {
|
|
3379
|
+
if (value === undefined || value.trim().length === 0) {
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
const parsed = Number(value);
|
|
3383
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
3384
|
+
}
|
|
3356
3385
|
function normalizeBridgeQueueOwnerTtlSeconds(value) {
|
|
3357
3386
|
if (value === undefined) {
|
|
3358
3387
|
return;
|
|
@@ -3398,6 +3427,7 @@ class BridgeRequestScheduler {
|
|
|
3398
3427
|
|
|
3399
3428
|
// src/bridge/bridge-runtime.ts
|
|
3400
3429
|
init_spawn_command();
|
|
3430
|
+
init_terminate_process_tree();
|
|
3401
3431
|
init_prompt_output();
|
|
3402
3432
|
init_prompt_media();
|
|
3403
3433
|
init_streaming_prompt();
|
|
@@ -3433,6 +3463,16 @@ class EnsureSessionFailedError extends Error {
|
|
|
3433
3463
|
this.data = data;
|
|
3434
3464
|
}
|
|
3435
3465
|
}
|
|
3466
|
+
var DEFAULT_SESSION_INIT_TIMEOUT_MS = 120000;
|
|
3467
|
+
|
|
3468
|
+
class CommandTimeoutError extends Error {
|
|
3469
|
+
timeoutMs;
|
|
3470
|
+
constructor(timeoutMs, command) {
|
|
3471
|
+
super(`acpx command timed out after ${timeoutMs / 1000}s: ${command}`);
|
|
3472
|
+
this.timeoutMs = timeoutMs;
|
|
3473
|
+
this.name = "CommandTimeoutError";
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3436
3476
|
|
|
3437
3477
|
class BridgeRuntime {
|
|
3438
3478
|
command;
|
|
@@ -3485,12 +3525,25 @@ class BridgeRuntime {
|
|
|
3485
3525
|
"--resume-session",
|
|
3486
3526
|
input.agentSessionId
|
|
3487
3527
|
], { format: "quiet" }));
|
|
3488
|
-
const
|
|
3528
|
+
const timeoutMs = this.sessionInitTimeoutMs();
|
|
3529
|
+
let result;
|
|
3530
|
+
try {
|
|
3531
|
+
result = await this.runSessionCreate(spawnSpec.command, spawnSpec.args, input.cwd, { timeoutMs });
|
|
3532
|
+
} catch (error) {
|
|
3533
|
+
if (error instanceof CommandTimeoutError) {
|
|
3534
|
+
throw new Error(`session initialization timed out after ${timeoutMs / 1000}s`);
|
|
3535
|
+
}
|
|
3536
|
+
throw error;
|
|
3537
|
+
}
|
|
3489
3538
|
if (result.code !== 0) {
|
|
3490
3539
|
throw new Error(result.stderr || result.stdout || "sessions resume failed");
|
|
3491
3540
|
}
|
|
3492
3541
|
return {};
|
|
3493
3542
|
}
|
|
3543
|
+
sessionInitTimeoutMs() {
|
|
3544
|
+
const value = this.options.sessionInitTimeoutMs;
|
|
3545
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_SESSION_INIT_TIMEOUT_MS;
|
|
3546
|
+
}
|
|
3494
3547
|
async hasSession(input) {
|
|
3495
3548
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
3496
3549
|
"sessions",
|
|
@@ -3522,6 +3575,11 @@ class BridgeRuntime {
|
|
|
3522
3575
|
}
|
|
3523
3576
|
async ensureSession(input, onProgress) {
|
|
3524
3577
|
onProgress?.("spawn");
|
|
3578
|
+
const timeoutMs = this.sessionInitTimeoutMs();
|
|
3579
|
+
const now = this.options.now ?? Date.now;
|
|
3580
|
+
const deadline = now() + timeoutMs;
|
|
3581
|
+
const remainingTimeoutMs = () => Math.max(deadline - now(), 1);
|
|
3582
|
+
const sessionInitTimedOutError = () => new EnsureSessionFailedError(`session initialization timed out after ${timeoutMs / 1000}s`, "generic");
|
|
3525
3583
|
const onStderrLine = onProgress ? (line) => {
|
|
3526
3584
|
const trimmed = line.replace(/\r$/, "").trimEnd();
|
|
3527
3585
|
if (trimmed.length === 0)
|
|
@@ -3529,41 +3587,58 @@ class BridgeRuntime {
|
|
|
3529
3587
|
onProgress({ kind: "note", text: trimmed });
|
|
3530
3588
|
} : undefined;
|
|
3531
3589
|
const runWithVerboseFallback = async (tailArgs, runner) => {
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
if (
|
|
3537
|
-
|
|
3590
|
+
try {
|
|
3591
|
+
const useVerbose = this.acpxVerboseSupported !== false;
|
|
3592
|
+
const spec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, tailArgs, { verbose: useVerbose }));
|
|
3593
|
+
const result = await runner(spec.command, spec.args);
|
|
3594
|
+
if (result.code === 0) {
|
|
3595
|
+
if (useVerbose)
|
|
3596
|
+
this.acpxVerboseSupported = true;
|
|
3597
|
+
return result;
|
|
3598
|
+
}
|
|
3599
|
+
if (useVerbose && isUnknownVerboseOption(result.stderr, result.stdout)) {
|
|
3600
|
+
this.acpxVerboseSupported = false;
|
|
3601
|
+
const retrySpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, tailArgs, { verbose: false }));
|
|
3602
|
+
return await runner(retrySpec.command, retrySpec.args);
|
|
3603
|
+
}
|
|
3538
3604
|
return result;
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
if (error instanceof CommandTimeoutError) {
|
|
3607
|
+
throw sessionInitTimedOutError();
|
|
3608
|
+
}
|
|
3609
|
+
throw error;
|
|
3539
3610
|
}
|
|
3540
|
-
if (useVerbose && isUnknownVerboseOption(result.stderr, result.stdout)) {
|
|
3541
|
-
this.acpxVerboseSupported = false;
|
|
3542
|
-
const retrySpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, tailArgs, { verbose: false }));
|
|
3543
|
-
return await runner(retrySpec.command, retrySpec.args);
|
|
3544
|
-
}
|
|
3545
|
-
return result;
|
|
3546
3611
|
};
|
|
3547
|
-
const ensured = await runWithVerboseFallback(["sessions", "ensure", "--name", input.name], (command, args) => this.run(command, args, { onStderrLine }));
|
|
3612
|
+
const ensured = await runWithVerboseFallback(["sessions", "ensure", "--name", input.name], (command, args) => this.run(command, args, { onStderrLine, timeoutMs: remainingTimeoutMs() }));
|
|
3548
3613
|
if (ensured.code === 0) {
|
|
3549
3614
|
onProgress?.("ready");
|
|
3550
3615
|
return {};
|
|
3551
3616
|
}
|
|
3552
3617
|
const existingSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
|
|
3553
|
-
const
|
|
3618
|
+
const runShowProbe = async () => {
|
|
3619
|
+
try {
|
|
3620
|
+
return await this.run(existingSpec.command, existingSpec.args, { timeoutMs: remainingTimeoutMs() });
|
|
3621
|
+
} catch (error) {
|
|
3622
|
+
if (error instanceof CommandTimeoutError) {
|
|
3623
|
+
throw sessionInitTimedOutError();
|
|
3624
|
+
}
|
|
3625
|
+
throw error;
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
const existing = await runShowProbe();
|
|
3554
3629
|
if (existing.code === 0) {
|
|
3555
3630
|
onProgress?.("ready");
|
|
3556
3631
|
return {};
|
|
3557
3632
|
}
|
|
3558
3633
|
onProgress?.("initializing");
|
|
3559
|
-
const created = await runWithVerboseFallback(["sessions", "new", "--name", input.name], (command, args) => this.runSessionCreate(command, args, input.cwd, { onStderrLine }));
|
|
3634
|
+
const created = await runWithVerboseFallback(["sessions", "new", "--name", input.name], (command, args) => this.runSessionCreate(command, args, input.cwd, { onStderrLine, timeoutMs: remainingTimeoutMs() }));
|
|
3560
3635
|
if (created.code === 0) {
|
|
3561
3636
|
onProgress?.("ready");
|
|
3562
3637
|
return {};
|
|
3563
3638
|
}
|
|
3564
3639
|
const output = created.stderr || created.stdout || "";
|
|
3565
3640
|
if (output.includes("EPERM") && await this.repairSessionIndex()) {
|
|
3566
|
-
const repaired = await
|
|
3641
|
+
const repaired = await runShowProbe();
|
|
3567
3642
|
if (repaired.code === 0) {
|
|
3568
3643
|
onProgress?.("ready");
|
|
3569
3644
|
return {};
|
|
@@ -3764,12 +3839,18 @@ class BridgeRuntime {
|
|
|
3764
3839
|
}
|
|
3765
3840
|
function spawnCapture(command, args, options) {
|
|
3766
3841
|
return new Promise((resolve, reject) => {
|
|
3767
|
-
const
|
|
3842
|
+
const spawnFn = options?.spawnFn ?? spawn4;
|
|
3843
|
+
const child = spawnFn(command, args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
3768
3844
|
child.stdout.setEncoding("utf8");
|
|
3769
3845
|
child.stderr.setEncoding("utf8");
|
|
3770
3846
|
let stdout = "";
|
|
3771
3847
|
let stderr = "";
|
|
3772
3848
|
let stderrTail = "";
|
|
3849
|
+
const timeoutId = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? setTimeout(() => {
|
|
3850
|
+
const kill = options.killProcessTreeFn ?? ((pid) => terminateProcessTree(pid, { detachedProcessGroup: false }));
|
|
3851
|
+
kill(child.pid ?? 0);
|
|
3852
|
+
reject(new CommandTimeoutError(options.timeoutMs, [command, ...args].join(" ")));
|
|
3853
|
+
}, options.timeoutMs) : undefined;
|
|
3773
3854
|
child.stdout.on("data", (chunk) => {
|
|
3774
3855
|
stdout += String(chunk);
|
|
3775
3856
|
});
|
|
@@ -3785,8 +3866,14 @@ function spawnCapture(command, args, options) {
|
|
|
3785
3866
|
options.onStderrLine(line);
|
|
3786
3867
|
}
|
|
3787
3868
|
});
|
|
3788
|
-
child.on("error",
|
|
3869
|
+
child.on("error", (error) => {
|
|
3870
|
+
if (timeoutId)
|
|
3871
|
+
clearTimeout(timeoutId);
|
|
3872
|
+
reject(error);
|
|
3873
|
+
});
|
|
3789
3874
|
child.on("close", (code) => {
|
|
3875
|
+
if (timeoutId)
|
|
3876
|
+
clearTimeout(timeoutId);
|
|
3790
3877
|
if (options?.onStderrLine && stderrTail.length > 0) {
|
|
3791
3878
|
options.onStderrLine(stderrTail);
|
|
3792
3879
|
}
|
|
@@ -3859,7 +3946,7 @@ async function defaultPromptRunner(command, args, onEvent, options) {
|
|
|
3859
3946
|
return await runStreamingPrompt(command, args, onEvent, options);
|
|
3860
3947
|
}
|
|
3861
3948
|
async function shellSessionCreateRunner(command, args, cwd, options) {
|
|
3862
|
-
return await spawnCapture(command, args, {
|
|
3949
|
+
return await spawnCapture(command, args, { ...options, cwd });
|
|
3863
3950
|
}
|
|
3864
3951
|
function selectLatestAcpxSessionIndexTmp(files) {
|
|
3865
3952
|
let latestTmp = null;
|
|
@@ -4008,7 +4095,8 @@ class BridgeServer {
|
|
|
4008
4095
|
case "updatePermissionPolicy":
|
|
4009
4096
|
return await this.runtime.updatePermissionPolicy({
|
|
4010
4097
|
permissionMode: requirePermissionMode(params, "permissionMode"),
|
|
4011
|
-
nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions")
|
|
4098
|
+
nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions"),
|
|
4099
|
+
permissionPolicy: asOptionalString(params.permissionPolicy)
|
|
4012
4100
|
});
|
|
4013
4101
|
case "hasSession":
|
|
4014
4102
|
return await this.runtime.hasSession({
|
|
@@ -4317,7 +4405,9 @@ async function runBridgeMain() {
|
|
|
4317
4405
|
const server = new BridgeServer(new BridgeRuntime(coreEnv("BRIDGE_ACPX_COMMAND") ?? "acpx", undefined, undefined, {
|
|
4318
4406
|
permissionMode: normalizeBridgePermissionMode(coreEnv("BRIDGE_PERMISSION_MODE")),
|
|
4319
4407
|
nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(coreEnv("BRIDGE_NON_INTERACTIVE_PERMISSIONS")),
|
|
4320
|
-
|
|
4408
|
+
permissionPolicy: normalizeBridgePermissionPolicy(coreEnv("BRIDGE_PERMISSION_POLICY")),
|
|
4409
|
+
queueOwnerTtlSeconds: normalizeBridgeQueueOwnerTtlSeconds(coreEnv("BRIDGE_QUEUE_OWNER_TTL_SECONDS")),
|
|
4410
|
+
sessionInitTimeoutMs: normalizeBridgeSessionInitTimeoutMs(coreEnv("BRIDGE_SESSION_INIT_TIMEOUT_MS"))
|
|
4321
4411
|
}));
|
|
4322
4412
|
const input = createInterface({
|
|
4323
4413
|
input: process.stdin,
|
package/dist/channels/types.d.ts
CHANGED
|
@@ -96,8 +96,19 @@ export interface MessageChannelRuntime {
|
|
|
96
96
|
id: string;
|
|
97
97
|
isLoggedIn(): boolean;
|
|
98
98
|
login(): Promise<string>;
|
|
99
|
+
/**
|
|
100
|
+
* Destructive credential removal. Reached only via the explicit
|
|
101
|
+
* `xacpx logout` CLI path — never as part of a normal shutdown.
|
|
102
|
+
*/
|
|
99
103
|
logout(): void;
|
|
100
104
|
start(input: ChannelStartInput): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Non-destructive shutdown: release runtime resources without touching
|
|
107
|
+
* stored credentials. Optional for compatibility with already-published
|
|
108
|
+
* plugin channels; when absent, the registry falls back to `logout()`
|
|
109
|
+
* (which for those plugins is a benign client stop).
|
|
110
|
+
*/
|
|
111
|
+
stop?(): void | Promise<void>;
|
|
101
112
|
createConsumerLock?(options?: ConsumerLockOptions): ConsumerLock;
|
|
102
113
|
configureOrchestration?(callbacks: OrchestrationDeliveryCallbacks): void;
|
|
103
114
|
notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
|
|
@@ -15,6 +15,14 @@ export declare class WeixinChannel implements MessageChannelRuntime {
|
|
|
15
15
|
isLoggedIn(): boolean;
|
|
16
16
|
login(): Promise<string>;
|
|
17
17
|
logout(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Non-destructive shutdown. The monitor loop is already stopped via the
|
|
20
|
+
* abort signal passed to start(); this only drops runtime references and
|
|
21
|
+
* MUST NOT touch the credential files on disk (a graceful daemon stop or
|
|
22
|
+
* restart must not force a QR re-login). Destructive credential removal
|
|
23
|
+
* happens only through logout() (the explicit `xacpx logout` CLI path).
|
|
24
|
+
*/
|
|
25
|
+
stop(): void;
|
|
18
26
|
createConsumerLock(options?: ConsumerLockOptions): ConsumerLock;
|
|
19
27
|
configureOrchestration(callbacks: OrchestrationDeliveryCallbacks): void;
|
|
20
28
|
start(input: ChannelStartInput): Promise<void>;
|