@ganglion/xacpx 0.9.3 → 0.10.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 +2 -0
- package/dist/bridge/bridge-main.js +170 -78
- package/dist/channels/types.d.ts +11 -0
- package/dist/channels/weixin-channel.d.ts +8 -0
- package/dist/cli.js +2044 -688
- 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 -2
- package/dist/perf/perf-log-writer.d.ts +1 -0
- package/dist/plugin-api.js +24 -6
- 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
package/README.md
CHANGED
|
@@ -258,6 +258,7 @@ xacpx doctor
|
|
|
258
258
|
xacpx doctor --verbose
|
|
259
259
|
xacpx doctor --smoke
|
|
260
260
|
xacpx doctor --smoke --agent codex --workspace backend
|
|
261
|
+
xacpx doctor --fix
|
|
261
262
|
```
|
|
262
263
|
|
|
263
264
|
Notes:
|
|
@@ -266,6 +267,7 @@ Notes:
|
|
|
266
267
|
- `--smoke` additionally runs a minimal real transport-level prompt check
|
|
267
268
|
- `--agent` / `--workspace` only affect `--smoke`
|
|
268
269
|
- Without `--smoke`, the related checks show as `SKIP`
|
|
270
|
+
- `--fix` applies safe local repairs (runtime dir permissions, stale locks, invalid state records) and re-checks; state-mutating repairs are withheld while the daemon runs — see [docs/doctor-command.md](docs/doctor-command.md)
|
|
269
271
|
|
|
270
272
|
### How to use `update`
|
|
271
273
|
|
|
@@ -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
|
};
|
|
@@ -1682,6 +1688,8 @@ var init_plugin_cli = __esm(() => {
|
|
|
1682
1688
|
noPlugins: "No plugins installed yet.",
|
|
1683
1689
|
pluginListHeader: "Plugins:",
|
|
1684
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.`,
|
|
1685
1693
|
pluginInstallFailed: (packageSpec, error) => `Plugin ${packageSpec} install failed: ${error}`,
|
|
1686
1694
|
pluginValidateFailed: (recordedName, error) => `Plugin ${recordedName} validation failed: ${error}`,
|
|
1687
1695
|
pluginInstalled: (recordedName) => `Plugin ${recordedName} installed`,
|
|
@@ -1793,8 +1801,6 @@ var init_weixin = __esm(() => {
|
|
|
1793
1801
|
debugEnabled: "Debug mode enabled",
|
|
1794
1802
|
debugDisabled: "Debug mode disabled",
|
|
1795
1803
|
sessionCleared: "✅ Session cleared. Starting a fresh conversation.",
|
|
1796
|
-
noAccountsLoggedIn: "No accounts are currently logged in.",
|
|
1797
|
-
logoutSuccess: "✅ Logged out. All account credentials cleared.",
|
|
1798
1804
|
commandFailed: (detail) => `❌ Command failed: ${detail}`
|
|
1799
1805
|
};
|
|
1800
1806
|
});
|
|
@@ -1832,6 +1838,7 @@ var init_misc = __esm(() => {
|
|
|
1832
1838
|
quotaOverflowSummary: (count) => `(${count} progress updates omitted due to message limit; see final result below)`,
|
|
1833
1839
|
finalHeadsUp: (total, sentSoFar, remaining) => `—
|
|
1834
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.`,
|
|
1835
1842
|
quotedMessagePrefix: (parts) => `[Quote: ${parts}]`,
|
|
1836
1843
|
scheduledTaskFailed: (message) => `Scheduled task failed: ${message}`,
|
|
1837
1844
|
orchestrationTaskCompleted: (taskId, workerSession, result) => `Delegation task "${taskId}" completed
|
|
@@ -1860,6 +1867,8 @@ var init_misc = __esm(() => {
|
|
|
1860
1867
|
delegateQPackageInstr3: "Do not forward the human's exact words to the worker",
|
|
1861
1868
|
commandAccessDeniedSuffix: " is restricted to group owner only.",
|
|
1862
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.",
|
|
1863
1872
|
commandLabelThisMessage: "This message",
|
|
1864
1873
|
sessionResetNoCurrentSession: "No session is currently selected. Run /session new ... or /use <alias> first.",
|
|
1865
1874
|
sessionResetFailed: (alias) => `Session "${alias}" reset failed. The new backend session was not created, please try again later.`,
|
|
@@ -1930,6 +1939,11 @@ var init_session2 = __esm(() => {
|
|
|
1930
1939
|
currentLabel: "[当前]",
|
|
1931
1940
|
sessionListItem: (alias, agent2, workspace2) => `- ${alias} (${agent2} @ ${workspace2})`,
|
|
1932
1941
|
sessionCreated: (alias) => `会话「${alias}」已创建并切换`,
|
|
1942
|
+
sessionAlreadyExists: (alias, agent2, workspace2) => [
|
|
1943
|
+
`会话「${alias}」已存在(${agent2} @ ${workspace2})。`,
|
|
1944
|
+
`发送 /use ${alias} 切换到它,或先执行 /session rm ${alias} 删除后再创建。`
|
|
1945
|
+
].join(`
|
|
1946
|
+
`),
|
|
1933
1947
|
sessionAttachNotFound: (alias, agent2, workspace2) => [
|
|
1934
1948
|
"没有找到可绑定的已有会话。",
|
|
1935
1949
|
`请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent2} --ws ${workspace2} --name <会话名>`
|
|
@@ -1966,6 +1980,7 @@ var init_session2 = __esm(() => {
|
|
|
1966
1980
|
sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
|
|
1967
1981
|
sessionRemoved: (alias) => `已删除会话「${alias}」。`,
|
|
1968
1982
|
sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
|
|
1983
|
+
sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
|
|
1969
1984
|
sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
|
|
1970
1985
|
sessionOrchestrationPurgeFailed: (warning) => `提示:清理任务编排引用失败(${warning}),请稍后执行 /tasks clean 手动清理。`,
|
|
1971
1986
|
sessionTransportTeardownFailed: (warning) => `提示:后端会话未能自动关闭(${warning}),如有残留请手动执行 acpx sessions close。`,
|
|
@@ -2259,7 +2274,7 @@ var init_later2 = __esm(() => {
|
|
|
2259
2274
|
helpNote2: "时间必须在 10 秒之后、7 天之内",
|
|
2260
2275
|
helpNote3: "默认在为本次任务新建的临时会话里执行,跑完即销毁",
|
|
2261
2276
|
helpNote4: "加 --bind 改为发送到创建时绑定的当前会话(默认模式可用 later.defaultMode 配置)",
|
|
2262
|
-
helpNote5: "/lt list
|
|
2277
|
+
helpNote5: "/lt list 只显示本聊天创建的待执行任务;群聊中只有群主可取消",
|
|
2263
2278
|
helpNote6: "不支持延迟执行 / 开头的 xacpx 命令",
|
|
2264
2279
|
helpNote7: "完整时间格式与说明见 docs/later-command.md"
|
|
2265
2280
|
};
|
|
@@ -2752,6 +2767,8 @@ var init_plugin_cli2 = __esm(() => {
|
|
|
2752
2767
|
noPlugins: "还没有安装插件。",
|
|
2753
2768
|
pluginListHeader: "插件:",
|
|
2754
2769
|
unrecognizedArgs: (args) => `未识别的参数:${args}`,
|
|
2770
|
+
pluginSpecHasDoubleQuote: (spec) => `非法插件 spec ${spec}:npm 包 spec 不允许包含双引号 (")。`,
|
|
2771
|
+
pluginSpecHasPercentOnWindows: (spec) => `非法插件 spec ${spec}:Windows 上 cmd.exe 会展开 %,无法安全传递。请改用 npm 直接安装该包。`,
|
|
2755
2772
|
pluginInstallFailed: (packageSpec, error) => `插件 ${packageSpec} 安装失败:${error}`,
|
|
2756
2773
|
pluginValidateFailed: (recordedName, error) => `插件 ${recordedName} 校验失败:${error}`,
|
|
2757
2774
|
pluginInstalled: (recordedName) => `插件 ${recordedName} 已安装`,
|
|
@@ -2863,8 +2880,6 @@ var init_weixin2 = __esm(() => {
|
|
|
2863
2880
|
debugEnabled: "Debug 模式已开启",
|
|
2864
2881
|
debugDisabled: "Debug 模式已关闭",
|
|
2865
2882
|
sessionCleared: "✅ 会话已清除,重新开始对话",
|
|
2866
|
-
noAccountsLoggedIn: "当前没有已登录的账号",
|
|
2867
|
-
logoutSuccess: "✅ 已退出登录,清除所有账号凭证",
|
|
2868
2883
|
commandFailed: (detail) => `❌ 指令执行失败: ${detail}`
|
|
2869
2884
|
};
|
|
2870
2885
|
});
|
|
@@ -2902,6 +2917,7 @@ var init_misc2 = __esm(() => {
|
|
|
2902
2917
|
quotaOverflowSummary: (count) => `(因消息次数限制省略 ${count} 条进度,请继续查看下方最终结果)`,
|
|
2903
2918
|
finalHeadsUp: (total, sentSoFar, remaining) => `—
|
|
2904
2919
|
\uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`,
|
|
2920
|
+
finalAllParked: (count) => `\uD83D\uDCC4 已达消息上限:结果共 ${count} 段已暂存。回复 /jx 接收。`,
|
|
2905
2921
|
quotedMessagePrefix: (parts) => `[引用: ${parts}]`,
|
|
2906
2922
|
scheduledTaskFailed: (message) => `定时任务执行失败:${message}`,
|
|
2907
2923
|
orchestrationTaskCompleted: (taskId, workerSession, result) => `委派任务「${taskId}」已完成
|
|
@@ -2930,6 +2946,8 @@ var init_misc2 = __esm(() => {
|
|
|
2930
2946
|
delegateQPackageInstr3: "不要直接把 human 原话转发给 worker",
|
|
2931
2947
|
commandAccessDeniedSuffix: " 仅限群创建者/频道 owner 使用。",
|
|
2932
2948
|
commandAccessDeniedHint: "如果需要执行控制类操作,请由 owner 在群内发送,或改用私聊。",
|
|
2949
|
+
commandAccessDeniedChatTypeMissingSuffix: " 已被拦截:该频道未上报会话类型(直聊/群聊),控制类命令在此暂不可用。",
|
|
2950
|
+
commandAccessDeniedChatTypeMissingHint: "只读命令与普通对话不受影响。这是频道元数据问题,请升级或反馈该频道插件。",
|
|
2933
2951
|
commandLabelThisMessage: "该消息",
|
|
2934
2952
|
sessionResetNoCurrentSession: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。",
|
|
2935
2953
|
sessionResetFailed: (alias) => `会话「${alias}」重置失败。
|
|
@@ -3351,6 +3369,19 @@ function normalizeBridgePermissionMode(value) {
|
|
|
3351
3369
|
function normalizeBridgeNonInteractivePermissions(value) {
|
|
3352
3370
|
return value === "deny" || value === "fail" ? value : "deny";
|
|
3353
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
|
+
}
|
|
3354
3385
|
function normalizeBridgeQueueOwnerTtlSeconds(value) {
|
|
3355
3386
|
if (value === undefined) {
|
|
3356
3387
|
return;
|
|
@@ -3396,6 +3427,7 @@ class BridgeRequestScheduler {
|
|
|
3396
3427
|
|
|
3397
3428
|
// src/bridge/bridge-runtime.ts
|
|
3398
3429
|
init_spawn_command();
|
|
3430
|
+
init_terminate_process_tree();
|
|
3399
3431
|
init_prompt_output();
|
|
3400
3432
|
init_prompt_media();
|
|
3401
3433
|
init_streaming_prompt();
|
|
@@ -3431,6 +3463,16 @@ class EnsureSessionFailedError extends Error {
|
|
|
3431
3463
|
this.data = data;
|
|
3432
3464
|
}
|
|
3433
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
|
+
}
|
|
3434
3476
|
|
|
3435
3477
|
class BridgeRuntime {
|
|
3436
3478
|
command;
|
|
@@ -3483,12 +3525,25 @@ class BridgeRuntime {
|
|
|
3483
3525
|
"--resume-session",
|
|
3484
3526
|
input.agentSessionId
|
|
3485
3527
|
], { format: "quiet" }));
|
|
3486
|
-
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
|
+
}
|
|
3487
3538
|
if (result.code !== 0) {
|
|
3488
3539
|
throw new Error(result.stderr || result.stdout || "sessions resume failed");
|
|
3489
3540
|
}
|
|
3490
3541
|
return {};
|
|
3491
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
|
+
}
|
|
3492
3547
|
async hasSession(input) {
|
|
3493
3548
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
3494
3549
|
"sessions",
|
|
@@ -3520,6 +3575,11 @@ class BridgeRuntime {
|
|
|
3520
3575
|
}
|
|
3521
3576
|
async ensureSession(input, onProgress) {
|
|
3522
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");
|
|
3523
3583
|
const onStderrLine = onProgress ? (line) => {
|
|
3524
3584
|
const trimmed = line.replace(/\r$/, "").trimEnd();
|
|
3525
3585
|
if (trimmed.length === 0)
|
|
@@ -3527,41 +3587,58 @@ class BridgeRuntime {
|
|
|
3527
3587
|
onProgress({ kind: "note", text: trimmed });
|
|
3528
3588
|
} : undefined;
|
|
3529
3589
|
const runWithVerboseFallback = async (tailArgs, runner) => {
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
if (
|
|
3535
|
-
|
|
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
|
+
}
|
|
3536
3604
|
return result;
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
if (error instanceof CommandTimeoutError) {
|
|
3607
|
+
throw sessionInitTimedOutError();
|
|
3608
|
+
}
|
|
3609
|
+
throw error;
|
|
3537
3610
|
}
|
|
3538
|
-
if (useVerbose && isUnknownVerboseOption(result.stderr, result.stdout)) {
|
|
3539
|
-
this.acpxVerboseSupported = false;
|
|
3540
|
-
const retrySpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, tailArgs, { verbose: false }));
|
|
3541
|
-
return await runner(retrySpec.command, retrySpec.args);
|
|
3542
|
-
}
|
|
3543
|
-
return result;
|
|
3544
3611
|
};
|
|
3545
|
-
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() }));
|
|
3546
3613
|
if (ensured.code === 0) {
|
|
3547
3614
|
onProgress?.("ready");
|
|
3548
3615
|
return {};
|
|
3549
3616
|
}
|
|
3550
3617
|
const existingSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
|
|
3551
|
-
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();
|
|
3552
3629
|
if (existing.code === 0) {
|
|
3553
3630
|
onProgress?.("ready");
|
|
3554
3631
|
return {};
|
|
3555
3632
|
}
|
|
3556
3633
|
onProgress?.("initializing");
|
|
3557
|
-
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() }));
|
|
3558
3635
|
if (created.code === 0) {
|
|
3559
3636
|
onProgress?.("ready");
|
|
3560
3637
|
return {};
|
|
3561
3638
|
}
|
|
3562
3639
|
const output = created.stderr || created.stdout || "";
|
|
3563
3640
|
if (output.includes("EPERM") && await this.repairSessionIndex()) {
|
|
3564
|
-
const repaired = await
|
|
3641
|
+
const repaired = await runShowProbe();
|
|
3565
3642
|
if (repaired.code === 0) {
|
|
3566
3643
|
onProgress?.("ready");
|
|
3567
3644
|
return {};
|
|
@@ -3762,12 +3839,18 @@ class BridgeRuntime {
|
|
|
3762
3839
|
}
|
|
3763
3840
|
function spawnCapture(command, args, options) {
|
|
3764
3841
|
return new Promise((resolve, reject) => {
|
|
3765
|
-
const
|
|
3842
|
+
const spawnFn = options?.spawnFn ?? spawn4;
|
|
3843
|
+
const child = spawnFn(command, args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
3766
3844
|
child.stdout.setEncoding("utf8");
|
|
3767
3845
|
child.stderr.setEncoding("utf8");
|
|
3768
3846
|
let stdout = "";
|
|
3769
3847
|
let stderr = "";
|
|
3770
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;
|
|
3771
3854
|
child.stdout.on("data", (chunk) => {
|
|
3772
3855
|
stdout += String(chunk);
|
|
3773
3856
|
});
|
|
@@ -3783,8 +3866,14 @@ function spawnCapture(command, args, options) {
|
|
|
3783
3866
|
options.onStderrLine(line);
|
|
3784
3867
|
}
|
|
3785
3868
|
});
|
|
3786
|
-
child.on("error",
|
|
3869
|
+
child.on("error", (error) => {
|
|
3870
|
+
if (timeoutId)
|
|
3871
|
+
clearTimeout(timeoutId);
|
|
3872
|
+
reject(error);
|
|
3873
|
+
});
|
|
3787
3874
|
child.on("close", (code) => {
|
|
3875
|
+
if (timeoutId)
|
|
3876
|
+
clearTimeout(timeoutId);
|
|
3788
3877
|
if (options?.onStderrLine && stderrTail.length > 0) {
|
|
3789
3878
|
options.onStderrLine(stderrTail);
|
|
3790
3879
|
}
|
|
@@ -3857,7 +3946,7 @@ async function defaultPromptRunner(command, args, onEvent, options) {
|
|
|
3857
3946
|
return await runStreamingPrompt(command, args, onEvent, options);
|
|
3858
3947
|
}
|
|
3859
3948
|
async function shellSessionCreateRunner(command, args, cwd, options) {
|
|
3860
|
-
return await spawnCapture(command, args, {
|
|
3949
|
+
return await spawnCapture(command, args, { ...options, cwd });
|
|
3861
3950
|
}
|
|
3862
3951
|
function selectLatestAcpxSessionIndexTmp(files) {
|
|
3863
3952
|
let latestTmp = null;
|
|
@@ -4006,7 +4095,8 @@ class BridgeServer {
|
|
|
4006
4095
|
case "updatePermissionPolicy":
|
|
4007
4096
|
return await this.runtime.updatePermissionPolicy({
|
|
4008
4097
|
permissionMode: requirePermissionMode(params, "permissionMode"),
|
|
4009
|
-
nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions")
|
|
4098
|
+
nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions"),
|
|
4099
|
+
permissionPolicy: asOptionalString(params.permissionPolicy)
|
|
4010
4100
|
});
|
|
4011
4101
|
case "hasSession":
|
|
4012
4102
|
return await this.runtime.hasSession({
|
|
@@ -4315,7 +4405,9 @@ async function runBridgeMain() {
|
|
|
4315
4405
|
const server = new BridgeServer(new BridgeRuntime(coreEnv("BRIDGE_ACPX_COMMAND") ?? "acpx", undefined, undefined, {
|
|
4316
4406
|
permissionMode: normalizeBridgePermissionMode(coreEnv("BRIDGE_PERMISSION_MODE")),
|
|
4317
4407
|
nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(coreEnv("BRIDGE_NON_INTERACTIVE_PERMISSIONS")),
|
|
4318
|
-
|
|
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"))
|
|
4319
4411
|
}));
|
|
4320
4412
|
const input = createInterface({
|
|
4321
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>;
|