@ganglion/xacpx 0.9.0 → 0.9.2
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 +6 -4
- package/dist/bridge/bridge-main.js +50 -16
- package/dist/cli.js +207 -79
- package/dist/commands/command-hints.d.ts +1 -1
- package/dist/commands/command-list.d.ts +3 -3
- package/dist/commands/handlers/resolve-reply-mode.d.ts +13 -0
- package/dist/config/types.d.ts +1 -0
- package/dist/i18n/resolve-locale.d.ts +1 -0
- package/dist/i18n/types.d.ts +6 -1
- package/dist/plugin-api.d.ts +1 -1
- package/dist/plugin-api.js +38 -12
- package/dist/plugins/compatibility.d.ts +4 -1
- package/dist/plugins/types.d.ts +2 -2
- package/dist/state/types.d.ts +1 -1
- package/dist/transport/types.d.ts +9 -1
- package/dist/version.d.ts +2 -2
- package/dist/weixin/agent/interface.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@ganglion/xacpx)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
|
-
[](https://zread.ai/gadzan/
|
|
7
|
+
[](https://zread.ai/gadzan/xacpx)
|
|
8
8
|
[](./LICENSE)
|
|
9
9
|
|
|
10
10
|
English · **[中文](./docs/zh/README_zh.md)**
|
|
@@ -13,6 +13,8 @@ English · **[中文](./docs/zh/README_zh.md)**
|
|
|
13
13
|
|
|
14
14
|
`xacpx` is a tool that lets you control ACP agents such as Codex / Claude Code / Gemini / OpenCode directly from WeChat, Feishu, or Yuanbao. It connects chat messages to your agent CLI sessions through `acpx`, so you can, right from your phone:
|
|
15
15
|
|
|
16
|
+
[](https://imgchr.com/i/pmZXIv6)
|
|
17
|
+
|
|
16
18
|
- Create and switch between sessions
|
|
17
19
|
- Have the agent keep working in a specific project directory
|
|
18
20
|
- View streaming replies, final results, and tool-call summaries
|
|
@@ -281,7 +283,7 @@ Notes:
|
|
|
281
283
|
- In a non-interactive environment, updating the core or plugins needs explicit confirmation: use `xacpx update --all`, or name the target with `xacpx update <name>`.
|
|
282
284
|
- `update` covers the core package and channel plugins; to manage a single plugin's version directly, see `xacpx plugin update <name>` ([docs/plugin-development.md](./docs/plugin-development.md)).
|
|
283
285
|
- After updating, run `xacpx restart` so a running daemon loads the new version.
|
|
284
|
-
- Cross-package rename migration:
|
|
286
|
+
- Cross-package rename migration: this project was renamed `weacpx` → `xacpx`. If you still have the legacy `weacpx` package installed, running `weacpx update` will offer to migrate you across to `xacpx` automatically (you confirm the switch). Already on `xacpx`? Just use `xacpx update` as a normal self-update.
|
|
285
287
|
|
|
286
288
|
## Common chat commands
|
|
287
289
|
|
|
@@ -427,7 +429,7 @@ Notes:
|
|
|
427
429
|
|
|
428
430
|
If you want to first understand when to delegate and when to dispatch multiple subtasks in parallel, see:
|
|
429
431
|
|
|
430
|
-
- [docs/
|
|
432
|
+
- [docs/xacpx-group-usage-guide.md](./docs/xacpx-group-usage-guide.md)
|
|
431
433
|
|
|
432
434
|
|
|
433
435
|
### MCP integration: external coordinator
|
|
@@ -579,7 +581,7 @@ If what you're about to do is one of the following, you can continue from here:
|
|
|
579
581
|
|
|
580
582
|
- Want the full chat-command reference: [docs/commands.md](./docs/commands.md)
|
|
581
583
|
- Want to schedule a one-time future message with scheduled tasks (`/later`): [docs/later-command.md](./docs/later-command.md)
|
|
582
|
-
- Want to understand when to delegate and when to open a group: [docs/
|
|
584
|
+
- Want to understand when to delegate and when to open a group: [docs/xacpx-group-usage-guide.md](./docs/xacpx-group-usage-guide.md)
|
|
583
585
|
|
|
584
586
|
### Troubleshooting and verification
|
|
585
587
|
|
|
@@ -880,10 +880,11 @@ var init_session = __esm(() => {
|
|
|
880
880
|
replyModeHeader: "Current reply mode:",
|
|
881
881
|
replyModeSessionLabel: (alias) => `- Session: ${alias}`,
|
|
882
882
|
replyModeGlobalDefault: (value) => `- Global default: ${value}`,
|
|
883
|
+
replyModeChannelDefault: (value) => `- Channel default: ${value}`,
|
|
883
884
|
replyModeSessionOverride: (value) => `- Session override: ${value}`,
|
|
884
885
|
replyModeEffective: (value) => `- Effective: ${value}`,
|
|
885
886
|
replyModeSet: (replyMode) => `Current session reply mode set to: ${replyMode}`,
|
|
886
|
-
replyModeReset: (
|
|
887
|
+
replyModeReset: (effective) => `Session reply mode reset. Now effective: ${effective}`,
|
|
887
888
|
statusHeader: "Current session:",
|
|
888
889
|
statusNameLabel: (alias) => `- Name: ${alias}`,
|
|
889
890
|
statusAgentLabel: (agent) => `- Agent: ${agent}`,
|
|
@@ -1426,6 +1427,8 @@ var init_config = __esm(() => {
|
|
|
1426
1427
|
mustBePositiveNumber: (path2) => `${path2} must be a positive number.`,
|
|
1427
1428
|
channelTypeDisabled: "channel.type is a legacy single-channel field; /config set writes are disabled. Use `xacpx channel ...` to manage channels[], then restart xacpx.",
|
|
1428
1429
|
channelReplyModeInvalid: "channel.replyMode only supports: stream, final, verbose",
|
|
1430
|
+
channelRuntimeNotFound: (id) => `Channel "${id}" does not exist; add it first with \`xacpx channel add ${id}\`.`,
|
|
1431
|
+
channelRuntimeReplyModeInvalid: (id) => `channels.${id}.replyMode only supports: stream, final, verbose`,
|
|
1429
1432
|
wechatReplyModeInvalid: "wechat.replyMode only supports: stream, final, verbose",
|
|
1430
1433
|
wechatReplyModeMapped: (value) => `${value} (mapped to channel.replyMode)`,
|
|
1431
1434
|
agentNotFound: (name) => `Agent "${name}" does not exist. Create it first.`,
|
|
@@ -1646,6 +1649,8 @@ var init_channel_cli = __esm(() => {
|
|
|
1646
1649
|
channelRemoved: (id) => `Channel ${id} removed`,
|
|
1647
1650
|
cannotDisableLastEnabled: "Cannot disable the last enabled channel.",
|
|
1648
1651
|
channelEnabledToggled: (id, enabled) => `Channel ${id} ${enabled ? "enabled" : "disabled"}`,
|
|
1652
|
+
channelReplyModeSet: (id, mode) => `Channel ${id} default reply mode set to: ${mode}`,
|
|
1653
|
+
channelReplyModeInvalid: (mode) => `reply mode must be stream / final / verbose, got: ${mode}`,
|
|
1649
1654
|
channelAccountAlreadyExists: (type, accountId) => `Account ${accountId} already exists on channel ${type}; run xacpx channel rm ${type} --account ${accountId} first`,
|
|
1650
1655
|
channelAccountAdded: (type, accountId) => `Channel ${type} account ${accountId} added`,
|
|
1651
1656
|
channelReEnabled: (type) => `Channel ${type} was disabled; it has been automatically re-enabled.`,
|
|
@@ -1946,10 +1951,11 @@ var init_session2 = __esm(() => {
|
|
|
1946
1951
|
replyModeHeader: "当前 reply mode:",
|
|
1947
1952
|
replyModeSessionLabel: (alias) => `- 会话:${alias}`,
|
|
1948
1953
|
replyModeGlobalDefault: (value) => `- 全局默认:${value}`,
|
|
1954
|
+
replyModeChannelDefault: (value) => `- 频道默认:${value}`,
|
|
1949
1955
|
replyModeSessionOverride: (value) => `- 当前会话覆盖:${value}`,
|
|
1950
1956
|
replyModeEffective: (value) => `- 当前生效:${value}`,
|
|
1951
1957
|
replyModeSet: (replyMode) => `已设置当前会话 reply mode:${replyMode}`,
|
|
1952
|
-
replyModeReset: (
|
|
1958
|
+
replyModeReset: (effective) => `已重置当前会话 reply mode,当前生效:${effective}`,
|
|
1953
1959
|
statusHeader: "当前会话:",
|
|
1954
1960
|
statusNameLabel: (alias) => `- 名称:${alias}`,
|
|
1955
1961
|
statusAgentLabel: (agent2) => `- Agent:${agent2}`,
|
|
@@ -2492,6 +2498,8 @@ var init_config2 = __esm(() => {
|
|
|
2492
2498
|
mustBePositiveNumber: (path2) => `${path2} 必须是正数。`,
|
|
2493
2499
|
channelTypeDisabled: "channel.type 是旧单频道字段,/config set 已禁用写入;请使用 `xacpx channel ...` 管理 channels[],然后重启 xacpx。",
|
|
2494
2500
|
channelReplyModeInvalid: "channel.replyMode 只支持:stream、final、verbose",
|
|
2501
|
+
channelRuntimeNotFound: (id) => `频道「${id}」不存在;请先用 \`xacpx channel add ${id}\` 添加。`,
|
|
2502
|
+
channelRuntimeReplyModeInvalid: (id) => `channels.${id}.replyMode 只支持:stream、final、verbose`,
|
|
2495
2503
|
wechatReplyModeInvalid: "wechat.replyMode 只支持:stream、final、verbose",
|
|
2496
2504
|
wechatReplyModeMapped: (value) => `${value}(已映射到 channel.replyMode)`,
|
|
2497
2505
|
agentNotFound: (name) => `Agent「${name}」不存在,请先创建。`,
|
|
@@ -2712,6 +2720,8 @@ var init_channel_cli2 = __esm(() => {
|
|
|
2712
2720
|
channelRemoved: (id) => `频道 ${id} 已删除`,
|
|
2713
2721
|
cannotDisableLastEnabled: "不能禁用最后一个启用的频道。",
|
|
2714
2722
|
channelEnabledToggled: (id, enabled) => `频道 ${id} 已${enabled ? "启用" : "禁用"}`,
|
|
2723
|
+
channelReplyModeSet: (id, mode) => `频道 ${id} 的默认 reply mode 已设置为:${mode}`,
|
|
2724
|
+
channelReplyModeInvalid: (mode) => `reply mode 只支持 stream / final / verbose,收到:${mode}`,
|
|
2715
2725
|
channelAccountAlreadyExists: (type, accountId) => `频道 ${type} 的账号 ${accountId} 已存在;先 xacpx channel rm ${type} --account ${accountId}`,
|
|
2716
2726
|
channelAccountAdded: (type, accountId) => `频道 ${type} 账号 ${accountId} 已添加`,
|
|
2717
2727
|
channelReEnabled: (type) => `频道 ${type} 此前是 disabled 状态,已自动启用。`,
|
|
@@ -2976,12 +2986,22 @@ var init_zh = __esm(() => {
|
|
|
2976
2986
|
function isLocale(value) {
|
|
2977
2987
|
return typeof value === "string" && VALID.includes(value);
|
|
2978
2988
|
}
|
|
2989
|
+
function detectSystemLocale() {
|
|
2990
|
+
try {
|
|
2991
|
+
return Intl.DateTimeFormat().resolvedOptions().locale || "";
|
|
2992
|
+
} catch {
|
|
2993
|
+
return "";
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2979
2996
|
function resolveLocale(input = {}) {
|
|
2980
2997
|
const { configLanguage, env = process.env } = input;
|
|
2981
2998
|
if (isLocale(configLanguage))
|
|
2982
2999
|
return configLanguage;
|
|
2983
3000
|
const raw = env.LC_ALL || env.LC_MESSAGES || env.LANG || "";
|
|
2984
|
-
|
|
3001
|
+
if (raw)
|
|
3002
|
+
return /^zh/i.test(raw) ? "zh" : "en";
|
|
3003
|
+
const systemLocale = input.systemLocale ?? detectSystemLocale();
|
|
3004
|
+
return /^zh/i.test(systemLocale) ? "zh" : "en";
|
|
2985
3005
|
}
|
|
2986
3006
|
var VALID;
|
|
2987
3007
|
var init_resolve_locale = __esm(() => {
|
|
@@ -3020,10 +3040,10 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
3020
3040
|
import { readFile, unlink } from "node:fs/promises";
|
|
3021
3041
|
import { homedir as homedir2 } from "node:os";
|
|
3022
3042
|
import { join as join2 } from "node:path";
|
|
3023
|
-
function
|
|
3024
|
-
const { command, args } = splitCommandLine(input.
|
|
3043
|
+
function buildXacpxMcpServerSpec(input) {
|
|
3044
|
+
const { command, args } = splitCommandLine(input.xacpxCommand);
|
|
3025
3045
|
return {
|
|
3026
|
-
name: "
|
|
3046
|
+
name: "xacpx",
|
|
3027
3047
|
type: "stdio",
|
|
3028
3048
|
command,
|
|
3029
3049
|
args: [
|
|
@@ -3050,7 +3070,7 @@ function buildQueueOwnerPayload(input) {
|
|
|
3050
3070
|
|
|
3051
3071
|
class AcpxQueueOwnerLauncher {
|
|
3052
3072
|
acpxCommand;
|
|
3053
|
-
|
|
3073
|
+
xacpxCommand;
|
|
3054
3074
|
spawnOwner;
|
|
3055
3075
|
terminateOwner;
|
|
3056
3076
|
baseEnv;
|
|
@@ -3059,7 +3079,7 @@ class AcpxQueueOwnerLauncher {
|
|
|
3059
3079
|
launchLocks = new Map;
|
|
3060
3080
|
constructor(options) {
|
|
3061
3081
|
this.acpxCommand = options.acpxCommand;
|
|
3062
|
-
this.
|
|
3082
|
+
this.xacpxCommand = options.xacpxCommand ?? resolveDefaultXacpxCommand(options.baseEnv ?? process.env);
|
|
3063
3083
|
this.spawnOwner = options.spawnOwner ?? defaultQueueOwnerSpawner;
|
|
3064
3084
|
this.terminateOwner = options.terminateOwner ?? createDefaultQueueOwnerTerminator(options.acpxCommand);
|
|
3065
3085
|
this.baseEnv = options.baseEnv ?? process.env;
|
|
@@ -3087,8 +3107,8 @@ class AcpxQueueOwnerLauncher {
|
|
|
3087
3107
|
nonInteractivePermissions: input.nonInteractivePermissions,
|
|
3088
3108
|
ttlMs: this.ttlMs,
|
|
3089
3109
|
maxQueueDepth: this.maxQueueDepth,
|
|
3090
|
-
mcpServers: [
|
|
3091
|
-
|
|
3110
|
+
mcpServers: [buildXacpxMcpServerSpec({
|
|
3111
|
+
xacpxCommand: this.xacpxCommand,
|
|
3092
3112
|
coordinatorSession: input.coordinatorSession,
|
|
3093
3113
|
...input.sourceHandle ? { sourceHandle: input.sourceHandle } : {}
|
|
3094
3114
|
})]
|
|
@@ -3143,13 +3163,13 @@ function splitCommandLine(value) {
|
|
|
3143
3163
|
current += "\\";
|
|
3144
3164
|
}
|
|
3145
3165
|
if (quote) {
|
|
3146
|
-
throw new Error("
|
|
3166
|
+
throw new Error("xacpx MCP command has an unterminated quote");
|
|
3147
3167
|
}
|
|
3148
3168
|
if (current.length > 0) {
|
|
3149
3169
|
parts.push(current);
|
|
3150
3170
|
}
|
|
3151
3171
|
if (parts.length === 0) {
|
|
3152
|
-
throw new Error("
|
|
3172
|
+
throw new Error("xacpx MCP command must not be empty");
|
|
3153
3173
|
}
|
|
3154
3174
|
return { command: parts[0], args: parts.slice(1) };
|
|
3155
3175
|
}
|
|
@@ -3195,7 +3215,7 @@ function queueLockFilePath(sessionId) {
|
|
|
3195
3215
|
function shortHash(value, length) {
|
|
3196
3216
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
3197
3217
|
}
|
|
3198
|
-
function
|
|
3218
|
+
function resolveDefaultXacpxCommand(env) {
|
|
3199
3219
|
const cliCommand = coreEnv("CLI_COMMAND", env);
|
|
3200
3220
|
if (cliCommand?.trim()) {
|
|
3201
3221
|
return cliCommand.trim();
|
|
@@ -3631,8 +3651,9 @@ class BridgeRuntime {
|
|
|
3631
3651
|
} else if (typeof parsed.id === "string") {
|
|
3632
3652
|
acpxRecordId = parsed.id;
|
|
3633
3653
|
}
|
|
3654
|
+
const agentSessionId = typeof parsed.agentSessionId === "string" ? parsed.agentSessionId : undefined;
|
|
3634
3655
|
if (acpxRecordId) {
|
|
3635
|
-
return { acpxRecordId };
|
|
3656
|
+
return { acpxRecordId, agentSessionId };
|
|
3636
3657
|
}
|
|
3637
3658
|
} catch {
|
|
3638
3659
|
const firstLine = result.stdout.trim().split(/\r?\n/, 1)[0];
|
|
@@ -3642,6 +3663,10 @@ class BridgeRuntime {
|
|
|
3642
3663
|
}
|
|
3643
3664
|
throw new Error("failed to resolve acpx session record id");
|
|
3644
3665
|
}
|
|
3666
|
+
async getAgentSessionId(input) {
|
|
3667
|
+
const record = await this.readSessionRecord(input);
|
|
3668
|
+
return { agentSessionId: record.agentSessionId };
|
|
3669
|
+
}
|
|
3645
3670
|
async setMode(input) {
|
|
3646
3671
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
3647
3672
|
"set-mode",
|
|
@@ -3909,7 +3934,8 @@ var BRIDGE_METHODS = new Set([
|
|
|
3909
3934
|
"prompt",
|
|
3910
3935
|
"setMode",
|
|
3911
3936
|
"cancel",
|
|
3912
|
-
"removeSession"
|
|
3937
|
+
"removeSession",
|
|
3938
|
+
"getAgentSessionId"
|
|
3913
3939
|
]);
|
|
3914
3940
|
var SESSION_SCOPED_METHODS = new Set([
|
|
3915
3941
|
"hasSession",
|
|
@@ -3919,7 +3945,8 @@ var SESSION_SCOPED_METHODS = new Set([
|
|
|
3919
3945
|
"prompt",
|
|
3920
3946
|
"setMode",
|
|
3921
3947
|
"cancel",
|
|
3922
|
-
"removeSession"
|
|
3948
|
+
"removeSession",
|
|
3949
|
+
"getAgentSessionId"
|
|
3923
3950
|
]);
|
|
3924
3951
|
|
|
3925
3952
|
class BridgeServer {
|
|
@@ -4095,6 +4122,13 @@ class BridgeServer {
|
|
|
4095
4122
|
cwd: requireString(params, "cwd"),
|
|
4096
4123
|
name: requireString(params, "name")
|
|
4097
4124
|
});
|
|
4125
|
+
case "getAgentSessionId":
|
|
4126
|
+
return await this.runtime.getAgentSessionId({
|
|
4127
|
+
agent: requireString(params, "agent"),
|
|
4128
|
+
agentCommand: asOptionalString(params.agentCommand),
|
|
4129
|
+
cwd: requireString(params, "cwd"),
|
|
4130
|
+
name: requireString(params, "name")
|
|
4131
|
+
});
|
|
4098
4132
|
default:
|
|
4099
4133
|
throw new Error(`unsupported bridge method: ${method}`);
|
|
4100
4134
|
}
|