@ganglion/xacpx 0.12.1 → 0.14.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/README.md +16 -8
- package/dist/bridge/bridge-main.js +139 -11
- package/dist/cli.js +528 -130
- package/dist/commands/handlers/session-handler.d.ts +4 -9
- package/dist/commands/parse-command.d.ts +3 -0
- package/dist/commands/router-types.d.ts +2 -5
- package/dist/control/control-event-bus.d.ts +8 -0
- package/dist/control/control-service.d.ts +23 -0
- package/dist/control/upload-store.d.ts +28 -0
- package/dist/i18n/types.d.ts +1 -0
- package/dist/plugin-api.js +2 -0
- package/dist/sessions/session-service.d.ts +1 -0
- package/dist/state/types.d.ts +4 -0
- package/dist/transport/types.d.ts +49 -4
- package/dist/weixin/agent/interface.d.ts +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -519,19 +519,27 @@ For more filtering, aliases, and troubleshooting, see [docs/native-sessions.md](
|
|
|
519
519
|
|
|
520
520
|
If you run several xacpx instances and want to drive them all from one browser dashboard, you can self-host the **relay hub**. Each instance dials out to the hub over WebSocket and registers; you log in to a multi-tenant web dashboard and manage every instance's sessions — chat, scheduled tasks, and orchestration — from one place. Streaming agent replies render as markdown, and the layout works on mobile.
|
|
521
521
|
|
|
522
|
-
|
|
522
|
+
The hub ships as an npm package (`@ganglion/xacpx-relay`) with the dashboard **bundled in** — no separate build. It serves everything on a single port (HTTP API + dashboard + the instance WebSocket gateway), and authentication is a single **access token** used for both web login and connector pairing.
|
|
523
523
|
|
|
524
524
|
```bash
|
|
525
|
-
#
|
|
526
|
-
|
|
527
|
-
bun run build:relay && bun run build:relay-web
|
|
525
|
+
# 1. On the hub host: install (dashboard is bundled — nothing else to build)
|
|
526
|
+
npm i -g @ganglion/xacpx-relay
|
|
528
527
|
|
|
529
|
-
#
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
528
|
+
# 2. Mint an access token (DB auto-created at ~/.xacpx-relay/relay.db)
|
|
529
|
+
xacpx-relay add token
|
|
530
|
+
# → prints the token once; use it to log into the dashboard AND to pair connectors
|
|
531
|
+
|
|
532
|
+
# 3. Start the hub (defaults: --host 0.0.0.0 --http-port 8787, dashboard auto-detected)
|
|
533
|
+
xacpx-relay start
|
|
534
|
+
|
|
535
|
+
# 4. On each instance host: add the connector channel and point it at the hub
|
|
536
|
+
xacpx plugin add @ganglion/xacpx-channel-relay # requires xacpx >= 0.11.0
|
|
537
|
+
xacpx channel add relay --url wss://relay.example.com --token <access-token> --name my-box
|
|
538
|
+
xacpx restart
|
|
533
539
|
```
|
|
534
540
|
|
|
541
|
+
In production, terminate TLS at a reverse proxy in front of the single port and have instances dial `wss://`. There's no `stop`/`status` subcommand — manage the process with systemd/pm2/Docker (`Ctrl-C`/`SIGTERM` to stop); update with `xacpx-relay update`.
|
|
542
|
+
|
|
535
543
|
Full walkthrough — pairing instances, TLS/reverse-proxy, systemd, backups, troubleshooting: **[Self-Hosting the Relay Hub](https://gadzan.github.io/xacpx/guide/relay-self-hosting)** (or [docs/relay-deployment.md](./docs/relay-deployment.md) for the terse runbook).
|
|
536
544
|
|
|
537
545
|
## Config and runtime files
|
|
@@ -72,6 +72,10 @@ function encodeBridgePromptUsageEvent(event) {
|
|
|
72
72
|
return `${JSON.stringify(event)}
|
|
73
73
|
`;
|
|
74
74
|
}
|
|
75
|
+
function encodeBridgePromptCommandsEvent(event) {
|
|
76
|
+
return `${JSON.stringify(event)}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
75
79
|
function encodeBridgeSessionProgressEvent(event) {
|
|
76
80
|
return `${JSON.stringify(event)}
|
|
77
81
|
`;
|
|
@@ -429,6 +433,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
|
|
|
429
433
|
let onThought;
|
|
430
434
|
let onPlan;
|
|
431
435
|
let onUsage;
|
|
436
|
+
let onCommands;
|
|
432
437
|
let rawStream = false;
|
|
433
438
|
if (options === undefined) {
|
|
434
439
|
toolEventMode = "text";
|
|
@@ -441,6 +446,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
|
|
|
441
446
|
onThought = options.onThought;
|
|
442
447
|
onPlan = options.onPlan;
|
|
443
448
|
onUsage = options.onUsage;
|
|
449
|
+
onCommands = options.onCommands;
|
|
444
450
|
rawStream = options.rawStream ?? false;
|
|
445
451
|
toolEventMode = resolveToolEventMode({
|
|
446
452
|
toolEventMode: options.mode,
|
|
@@ -460,6 +466,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
|
|
|
460
466
|
onThought,
|
|
461
467
|
onPlan,
|
|
462
468
|
onUsage,
|
|
469
|
+
onCommands,
|
|
463
470
|
finalize() {
|
|
464
471
|
if (this.pendingLine.trim().length > 0) {
|
|
465
472
|
parseStreamingChunks(this, this.pendingLine);
|
|
@@ -527,8 +534,17 @@ function parseStreamingChunks(state, line) {
|
|
|
527
534
|
if (update.sessionUpdate === "usage_update") {
|
|
528
535
|
const used = typeof update.used === "number" && Number.isFinite(update.used) ? update.used : undefined;
|
|
529
536
|
const size = typeof update.size === "number" && Number.isFinite(update.size) ? update.size : undefined;
|
|
530
|
-
if (used !== undefined && size !== undefined && size > 0)
|
|
531
|
-
|
|
537
|
+
if (used !== undefined && size !== undefined && size > 0) {
|
|
538
|
+
const cost = normalizeUsageCost(update.cost);
|
|
539
|
+
const breakdown = normalizeUsageBreakdown(update._meta?.usage);
|
|
540
|
+
state.onUsage?.({ used, size, ...cost ? { cost } : {}, ...breakdown ? { breakdown } : {} });
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (update.sessionUpdate === "available_commands_update") {
|
|
545
|
+
if (Array.isArray(update.availableCommands)) {
|
|
546
|
+
state.onCommands?.(normalizeAgentCommands(update.availableCommands));
|
|
547
|
+
}
|
|
532
548
|
return;
|
|
533
549
|
}
|
|
534
550
|
const isThoughtChunk = update.sessionUpdate === "agent_thought_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
|
|
@@ -695,6 +711,52 @@ function readFirstStringArray(record, keys) {
|
|
|
695
711
|
}
|
|
696
712
|
return;
|
|
697
713
|
}
|
|
714
|
+
function asFiniteNumber(value) {
|
|
715
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
716
|
+
}
|
|
717
|
+
function firstFiniteNumber(record, keys) {
|
|
718
|
+
for (const key of keys) {
|
|
719
|
+
const n = asFiniteNumber(record[key]);
|
|
720
|
+
if (n !== undefined)
|
|
721
|
+
return n;
|
|
722
|
+
}
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
function normalizeUsageBreakdown(value) {
|
|
726
|
+
if (!isRecord(value))
|
|
727
|
+
return;
|
|
728
|
+
const out = {};
|
|
729
|
+
for (const [key, aliases] of USAGE_BREAKDOWN_FIELDS) {
|
|
730
|
+
const n = firstFiniteNumber(value, aliases);
|
|
731
|
+
if (n !== undefined)
|
|
732
|
+
out[key] = n;
|
|
733
|
+
}
|
|
734
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
735
|
+
}
|
|
736
|
+
function normalizeUsageCost(value) {
|
|
737
|
+
if (!isRecord(value))
|
|
738
|
+
return;
|
|
739
|
+
const amount = asFiniteNumber(value.amount);
|
|
740
|
+
const currency = readString(value, "currency");
|
|
741
|
+
if (amount === undefined && !currency)
|
|
742
|
+
return;
|
|
743
|
+
return { ...amount !== undefined ? { amount } : {}, ...currency ? { currency } : {} };
|
|
744
|
+
}
|
|
745
|
+
function normalizeAgentCommands(value) {
|
|
746
|
+
if (!Array.isArray(value))
|
|
747
|
+
return [];
|
|
748
|
+
const out = [];
|
|
749
|
+
for (const entry of value) {
|
|
750
|
+
if (!isRecord(entry))
|
|
751
|
+
continue;
|
|
752
|
+
const name = readString(entry, "name");
|
|
753
|
+
if (!name)
|
|
754
|
+
continue;
|
|
755
|
+
const description = readString(entry, "description");
|
|
756
|
+
out.push({ name, ...description ? { description } : {}, hasInput: entry.input != null });
|
|
757
|
+
}
|
|
758
|
+
return out;
|
|
759
|
+
}
|
|
698
760
|
function isRecord(value) {
|
|
699
761
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
700
762
|
}
|
|
@@ -720,8 +782,17 @@ function isGenericToolTitle(kind, title) {
|
|
|
720
782
|
}
|
|
721
783
|
return false;
|
|
722
784
|
}
|
|
785
|
+
var USAGE_BREAKDOWN_FIELDS;
|
|
723
786
|
var init_streaming_prompt = __esm(() => {
|
|
724
787
|
init_tool_kind_emoji();
|
|
788
|
+
USAGE_BREAKDOWN_FIELDS = [
|
|
789
|
+
["inputTokens", ["inputTokens", "input_tokens"]],
|
|
790
|
+
["outputTokens", ["outputTokens", "output_tokens"]],
|
|
791
|
+
["cachedReadTokens", ["cachedReadTokens", "cacheReadInputTokens", "cache_read_input_tokens"]],
|
|
792
|
+
["cachedWriteTokens", ["cachedWriteTokens", "cacheCreationInputTokens", "cache_creation_input_tokens"]],
|
|
793
|
+
["thoughtTokens", ["thoughtTokens", "thought_tokens"]],
|
|
794
|
+
["totalTokens", ["totalTokens", "total_tokens"]]
|
|
795
|
+
];
|
|
725
796
|
});
|
|
726
797
|
|
|
727
798
|
// src/recovery/discover-parent-package-paths.ts
|
|
@@ -940,6 +1011,7 @@ ${detail}`,
|
|
|
940
1011
|
sessionBlockedByTasks: (alias, count) => `Session "${alias}" has ${count} unfinished task(s). Cancel or wait for them to complete first.`,
|
|
941
1012
|
sessionBlockedByTasksHint: "Use /tasks to list tasks, or /task cancel <id> to cancel one.",
|
|
942
1013
|
sessionRemoved: (alias) => `Session "${alias}" removed.`,
|
|
1014
|
+
sessionArchived: (alias) => `Archived session "${alias}". Send a message to restore it.`,
|
|
943
1015
|
sessionRemovedWasActive: "This was the active session. Its chat context has been cleared.",
|
|
944
1016
|
sessionRemovedWasActivePromoted: (alias) => `This was the active session. Switched back to the previous session "${alias}".`,
|
|
945
1017
|
sessionTransportShared: (transportSession, count) => `Note: backend session "${transportSession}" is still referenced by ${count} other session(s) and was not closed.`,
|
|
@@ -2035,6 +2107,7 @@ ${detail}`,
|
|
|
2035
2107
|
sessionBlockedByTasks: (alias, count) => `会话「${alias}」下还有 ${count} 个未结束的任务,请先取消或等待完成。`,
|
|
2036
2108
|
sessionBlockedByTasksHint: "使用 /tasks 查看任务列表,或 /task cancel <id> 取消任务。",
|
|
2037
2109
|
sessionRemoved: (alias) => `已删除会话「${alias}」。`,
|
|
2110
|
+
sessionArchived: (alias) => `已归档会话「${alias}」。发送消息即可恢复。`,
|
|
2038
2111
|
sessionRemovedWasActive: "该会话是当前活跃会话,已自动清除相关聊天上下文。",
|
|
2039
2112
|
sessionRemovedWasActivePromoted: (alias) => `该会话是当前活跃会话,已切换回上一个会话「${alias}」。`,
|
|
2040
2113
|
sessionTransportShared: (transportSession, count) => `提示:后端会话「${transportSession}」仍被其他 ${count} 个会话引用,未关闭。`,
|
|
@@ -3424,6 +3497,31 @@ var init_agent_session_list = __esm(() => {
|
|
|
3424
3497
|
init_path();
|
|
3425
3498
|
});
|
|
3426
3499
|
|
|
3500
|
+
// src/transport/acpx-session-files.ts
|
|
3501
|
+
import { readdir, unlink as unlink2 } from "node:fs/promises";
|
|
3502
|
+
import { homedir as homedir4 } from "node:os";
|
|
3503
|
+
import { join as join3 } from "node:path";
|
|
3504
|
+
async function deleteAcpxSessionFiles(options) {
|
|
3505
|
+
const dir = options.sessionsDir ?? join3(homedir4(), ".acpx", "sessions");
|
|
3506
|
+
const safeId = encodeURIComponent(options.acpxRecordId);
|
|
3507
|
+
await unlink2(join3(dir, `${safeId}.json`)).catch(() => {
|
|
3508
|
+
return;
|
|
3509
|
+
});
|
|
3510
|
+
let entries;
|
|
3511
|
+
try {
|
|
3512
|
+
entries = await readdir(dir);
|
|
3513
|
+
} catch {
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
const streamFiles = entries.filter((name) => name.startsWith(`${safeId}.stream.`));
|
|
3517
|
+
for (const name of streamFiles) {
|
|
3518
|
+
await unlink2(join3(dir, name)).catch(() => {
|
|
3519
|
+
return;
|
|
3520
|
+
});
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
var init_acpx_session_files = () => {};
|
|
3524
|
+
|
|
3427
3525
|
// src/bridge/bridge-main.ts
|
|
3428
3526
|
import { createInterface } from "node:readline";
|
|
3429
3527
|
|
|
@@ -3496,9 +3594,9 @@ init_terminate_process_tree();
|
|
|
3496
3594
|
init_prompt_output();
|
|
3497
3595
|
init_prompt_media();
|
|
3498
3596
|
init_streaming_prompt();
|
|
3499
|
-
import { copyFile, readdir } from "node:fs/promises";
|
|
3500
|
-
import { homedir as
|
|
3501
|
-
import { dirname as dirname2, join as
|
|
3597
|
+
import { copyFile, readdir as readdir2 } from "node:fs/promises";
|
|
3598
|
+
import { homedir as homedir5 } from "node:os";
|
|
3599
|
+
import { dirname as dirname2, join as join4, win32 } from "node:path";
|
|
3502
3600
|
import { spawn as spawn4 } from "node:child_process";
|
|
3503
3601
|
|
|
3504
3602
|
// src/bridge/parse-missing-optional-dep.ts
|
|
@@ -3518,6 +3616,7 @@ function parseMissingOptionalDep(text) {
|
|
|
3518
3616
|
init_discover_parent_package_paths();
|
|
3519
3617
|
init_acpx_queue_owner_launcher();
|
|
3520
3618
|
init_agent_session_list();
|
|
3619
|
+
init_acpx_session_files();
|
|
3521
3620
|
class EnsureSessionFailedError extends Error {
|
|
3522
3621
|
kind;
|
|
3523
3622
|
data;
|
|
@@ -3798,7 +3897,7 @@ class BridgeRuntime {
|
|
|
3798
3897
|
acpxRecordId = parsed.id;
|
|
3799
3898
|
}
|
|
3800
3899
|
const agentSessionId = typeof parsed.agentSessionId === "string" ? parsed.agentSessionId : undefined;
|
|
3801
|
-
if (acpxRecordId) {
|
|
3900
|
+
if (acpxRecordId && /^[\w.:-]+$/.test(acpxRecordId) && acpxRecordId.length >= 8) {
|
|
3802
3901
|
return { acpxRecordId, agentSessionId };
|
|
3803
3902
|
}
|
|
3804
3903
|
} catch {
|
|
@@ -3890,6 +3989,17 @@ class BridgeRuntime {
|
|
|
3890
3989
|
}
|
|
3891
3990
|
throw new Error(result.stderr || result.stdout || "sessions close failed");
|
|
3892
3991
|
}
|
|
3992
|
+
async deleteSession(input) {
|
|
3993
|
+
let acpxRecordId;
|
|
3994
|
+
try {
|
|
3995
|
+
({ acpxRecordId } = await this.readSessionRecord(input));
|
|
3996
|
+
} catch {
|
|
3997
|
+
return {};
|
|
3998
|
+
}
|
|
3999
|
+
await this.removeSession(input);
|
|
4000
|
+
await deleteAcpxSessionFiles({ acpxRecordId });
|
|
4001
|
+
return {};
|
|
4002
|
+
}
|
|
3893
4003
|
async shutdown() {
|
|
3894
4004
|
return {};
|
|
3895
4005
|
}
|
|
@@ -4010,7 +4120,8 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
|
|
|
4010
4120
|
...onEvent && (toolEventMode === "structured" || toolEventMode === "both") ? { onToolEvent: (toolEvent) => onEvent({ type: "prompt.tool_event", event: toolEvent }) } : {},
|
|
4011
4121
|
...onEvent ? { onThought: (chunk) => onEvent({ type: "prompt.thought", text: chunk }) } : {},
|
|
4012
4122
|
...onEvent ? { onPlan: (entries) => onEvent({ type: "prompt.plan", entries }) } : {},
|
|
4013
|
-
...onEvent ? { onUsage: (usage) => onEvent({ type: "prompt.usage", used: usage.used, size: usage.size }) } : {}
|
|
4123
|
+
...onEvent ? { onUsage: (usage) => onEvent({ type: "prompt.usage", used: usage.used, size: usage.size, ...usage.cost ? { cost: usage.cost } : {}, ...usage.breakdown ? { breakdown: usage.breakdown } : {} }) } : {},
|
|
4124
|
+
...onEvent ? { onCommands: (commands) => onEvent({ type: "prompt.commands", commands }) } : {}
|
|
4014
4125
|
});
|
|
4015
4126
|
let lastReplyAt = now();
|
|
4016
4127
|
const flushBuffer = () => {
|
|
@@ -4080,14 +4191,14 @@ async function tryRepairAcpxSessionIndex(deps = {}) {
|
|
|
4080
4191
|
if (platform !== "win32") {
|
|
4081
4192
|
return false;
|
|
4082
4193
|
}
|
|
4083
|
-
const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ??
|
|
4194
|
+
const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir5();
|
|
4084
4195
|
if (!home) {
|
|
4085
4196
|
return false;
|
|
4086
4197
|
}
|
|
4087
|
-
const pathJoin = platform === "win32" ? win32.join :
|
|
4198
|
+
const pathJoin = platform === "win32" ? win32.join : join4;
|
|
4088
4199
|
const sessionsDir = pathJoin(home, ".acpx", "sessions");
|
|
4089
4200
|
const indexPath = pathJoin(sessionsDir, "index.json");
|
|
4090
|
-
const readdirFn = deps.readdirFn ??
|
|
4201
|
+
const readdirFn = deps.readdirFn ?? readdir2;
|
|
4091
4202
|
const copyFileFn = deps.copyFileFn ?? copyFile;
|
|
4092
4203
|
let files;
|
|
4093
4204
|
try {
|
|
@@ -4135,6 +4246,7 @@ var BRIDGE_METHODS = new Set([
|
|
|
4135
4246
|
"getSessionModel",
|
|
4136
4247
|
"cancel",
|
|
4137
4248
|
"removeSession",
|
|
4249
|
+
"deleteSession",
|
|
4138
4250
|
"getAgentSessionId"
|
|
4139
4251
|
]);
|
|
4140
4252
|
var SESSION_SCOPED_METHODS = new Set([
|
|
@@ -4148,6 +4260,7 @@ var SESSION_SCOPED_METHODS = new Set([
|
|
|
4148
4260
|
"getSessionModel",
|
|
4149
4261
|
"cancel",
|
|
4150
4262
|
"removeSession",
|
|
4263
|
+
"deleteSession",
|
|
4151
4264
|
"getAgentSessionId"
|
|
4152
4265
|
]);
|
|
4153
4266
|
|
|
@@ -4306,7 +4419,15 @@ class BridgeServer {
|
|
|
4306
4419
|
id: requestId,
|
|
4307
4420
|
event: "prompt.usage",
|
|
4308
4421
|
used: event.used,
|
|
4309
|
-
size: event.size
|
|
4422
|
+
size: event.size,
|
|
4423
|
+
...event.cost ? { cost: event.cost } : {},
|
|
4424
|
+
...event.breakdown ? { breakdown: event.breakdown } : {}
|
|
4425
|
+
}));
|
|
4426
|
+
} else if (event.type === "prompt.commands") {
|
|
4427
|
+
writeLine?.(encodeBridgePromptCommandsEvent({
|
|
4428
|
+
id: requestId,
|
|
4429
|
+
event: "prompt.commands",
|
|
4430
|
+
commands: event.commands
|
|
4310
4431
|
}));
|
|
4311
4432
|
}
|
|
4312
4433
|
});
|
|
@@ -4355,6 +4476,13 @@ class BridgeServer {
|
|
|
4355
4476
|
cwd: requireString(params, "cwd"),
|
|
4356
4477
|
name: requireString(params, "name")
|
|
4357
4478
|
});
|
|
4479
|
+
case "deleteSession":
|
|
4480
|
+
return await this.runtime.deleteSession({
|
|
4481
|
+
agent: requireString(params, "agent"),
|
|
4482
|
+
agentCommand: asOptionalString(params.agentCommand),
|
|
4483
|
+
cwd: requireString(params, "cwd"),
|
|
4484
|
+
name: requireString(params, "name")
|
|
4485
|
+
});
|
|
4358
4486
|
case "getAgentSessionId":
|
|
4359
4487
|
return await this.runtime.getAgentSessionId({
|
|
4360
4488
|
agent: requireString(params, "agent"),
|