@syrkos/filaxis 0.1.0-alpha.12 → 0.1.0-alpha.13
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.
Potentially problematic release.
This version of @syrkos/filaxis might be problematic. Click here for more details.
- package/dist/agent-toolkit.d.ts +46 -5
- package/dist/file-edit.d.ts +14 -6
- package/dist/index.js +322 -43
- package/dist/runtime-stack.d.ts +83 -1
- package/package.json +1 -1
- package/src/agent-toolkit.ts +143 -18
- package/src/file-edit.ts +33 -48
- package/src/runtime-stack.ts +290 -2
- package/src/ssh-pty-driver.ts +7 -4
package/dist/agent-toolkit.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent-toolkit.ts — 原生 Agent 工具集(Gemini 试用反馈 #3
|
|
2
|
+
* agent-toolkit.ts — 原生 Agent 工具集(Gemini 试用反馈 #3 + 第十季层族)
|
|
3
3
|
*
|
|
4
|
-
* 一行 `createAgentToolkit(stack)` 生成 LLM
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 一行 `createAgentToolkit(stack)` 生成 LLM 工具面——OpenAI/Anthropic/Vercel AI SDK
|
|
5
|
+
* 通吃的 {tools, handlers} 双层结构:定义层喂模型,执行层留宿主(桥到任意框架)。
|
|
6
|
+
* 语义契约(退出码是数据/终态三态/就绪握手)由内核承载,宿主零胶水。
|
|
7
|
+
*
|
|
8
|
+
* 第十季层族:执行类工具带可选 layer 参数(缺省=默认层——最近 push 的命名层;
|
|
9
|
+
* 未用层族时与旧行为一致,零破坏);push_ssh/push_into/list_layers/remove_layer
|
|
10
|
+
* 操作 RuntimeStack 命名层注册表(Paramiko 式句柄寻址——栈/焦点弱化归消费方)。
|
|
8
11
|
*/
|
|
9
12
|
import type { RuntimeStack } from "./runtime-stack.js";
|
|
10
13
|
import type { RuntimeDriver } from "./driver.js";
|
|
@@ -53,6 +56,7 @@ export interface AgentToolHandlers {
|
|
|
53
56
|
duration_ms?: number;
|
|
54
57
|
stream?: "stdout" | "stderr" | "both";
|
|
55
58
|
name?: string;
|
|
59
|
+
layer?: string;
|
|
56
60
|
}): Promise<{
|
|
57
61
|
session_id: string;
|
|
58
62
|
lines: Array<{
|
|
@@ -65,6 +69,7 @@ export interface AgentToolHandlers {
|
|
|
65
69
|
ready_marker?: string;
|
|
66
70
|
error_pattern?: string;
|
|
67
71
|
name?: string;
|
|
72
|
+
layer?: string;
|
|
68
73
|
}): Promise<{
|
|
69
74
|
session_id: string;
|
|
70
75
|
log_base: string;
|
|
@@ -77,6 +82,42 @@ export interface AgentToolHandlers {
|
|
|
77
82
|
exit_code?: number | null;
|
|
78
83
|
note?: string;
|
|
79
84
|
}>;
|
|
85
|
+
push_ssh(args: {
|
|
86
|
+
host: string;
|
|
87
|
+
port?: number;
|
|
88
|
+
user?: string;
|
|
89
|
+
key?: string;
|
|
90
|
+
jump?: string;
|
|
91
|
+
name?: string;
|
|
92
|
+
}): Promise<{
|
|
93
|
+
layer: string;
|
|
94
|
+
reused: boolean;
|
|
95
|
+
reused_from?: string;
|
|
96
|
+
note?: string;
|
|
97
|
+
}>;
|
|
98
|
+
push_into(args: {
|
|
99
|
+
enter_cmd: string;
|
|
100
|
+
label?: string;
|
|
101
|
+
from?: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
}): Promise<{
|
|
104
|
+
layer: string;
|
|
105
|
+
target?: string;
|
|
106
|
+
}>;
|
|
107
|
+
list_layers(args: Record<string, never>): Promise<{
|
|
108
|
+
text: string;
|
|
109
|
+
layers: Array<{
|
|
110
|
+
name: string;
|
|
111
|
+
driver: string;
|
|
112
|
+
target?: string;
|
|
113
|
+
}>;
|
|
114
|
+
}>;
|
|
115
|
+
remove_layer(args: {
|
|
116
|
+
name: string;
|
|
117
|
+
destroy?: boolean;
|
|
118
|
+
}): Promise<{
|
|
119
|
+
removed: string;
|
|
120
|
+
}>;
|
|
80
121
|
}
|
|
81
122
|
export interface AgentToolkit {
|
|
82
123
|
tools: AgentToolDef[];
|
package/dist/file-edit.d.ts
CHANGED
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 第七季盲测反馈(B2 逐秒拆解):旧实现一次 editFile = 5 次串行 SSH 往返
|
|
8
8
|
* (stat/read/stat/write/stat),Windows 无 ControlMaster 下放大到 15s+。
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* 第七季版合成「读+戳 1 次 + 远端 CAS 原子写回 1 次」——但方言探测(base64
|
|
10
|
+
* -w 0/-d/-D)是独立往返,SDK 常驻内存下有 dialectCache 掩护,**CLI 每次冷启动
|
|
11
|
+
* 实为 3 次物理往返**(第十季 B5 轨迹实锤:3×SSH 握手 ≈11.4s 突破宿主 10s 后台
|
|
12
|
+
* 阈值,多耗 3 轮轮询)。
|
|
13
|
+
*
|
|
14
|
+
* 本版(第十季):**真 2 次往返**——方言探测合成进读往返(① stat/sha 戳 +
|
|
15
|
+
* base64 -d 探测 + 文件读取一次带回;② CAS 写回用已确定 argv)。编码侧无需
|
|
16
|
+
* 探测:折行输出由解析侧 \s+ 归一消化(base64 < file 全方言通吃);解码侧
|
|
17
|
+
* 不用 `base64 -d || base64 -D` 赌 fallback(GPT 否决:双解码器共用 stdin,
|
|
18
|
+
* 首个消费后第二个拿不到完整载荷)。大文件(base64>30KB)自动回落 fs 层路径。
|
|
13
19
|
*
|
|
14
20
|
* **仅适用 UTF-8 文本**(readFile 返回 UTF-8 字符串;二进制文件的编辑走
|
|
15
21
|
* readBase64 + 消费方自行改写 + writeFile)。
|
|
@@ -31,8 +37,10 @@ export declare class EditConflictError extends Error {
|
|
|
31
37
|
constructor(message: string);
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
|
-
*
|
|
35
|
-
* ①
|
|
40
|
+
* 受控编辑(真 2 次往返 + 远端 CAS 原子写回):
|
|
41
|
+
* ① 读+戳+方言探测(1 次往返):`stat 戳; sha 戳; __FXS__; base64 -d 探测;
|
|
42
|
+
* __FXD__; base64 < file` —— (mtime,size)+全文+解码 argv 一次带回
|
|
43
|
+
* (__FXD__ 含下划线不在 base64 字母表——与 __FXS__ 同款防撞)
|
|
36
44
|
* ② 精确替换(本地):0 命中/多处命中(未开 all)拒绝
|
|
37
45
|
* ③ CAS 原子写回(1 次往返):远端一条命令内完成「戳比对 + tmp 写 + mv 原子替换 + 新戳回执」
|
|
38
46
|
* —— 戳不符(他人先写)→ EditConflictError;写后回执新戳
|
package/dist/index.js
CHANGED
|
@@ -1182,7 +1182,7 @@ class PtyLink {
|
|
|
1182
1182
|
registerFallbackStep() {
|
|
1183
1183
|
const step = String(this.fallbackSteps.size);
|
|
1184
1184
|
this.fallbackSteps.add(step);
|
|
1185
|
-
return `; echo __FILAXIS_FB_${this.fallbackNonce}_${step}_$?_
|
|
1185
|
+
return `; echo __FILAXIS_FB_${this.fallbackNonce}_${step}_$?_$$`;
|
|
1186
1186
|
}
|
|
1187
1187
|
attach(args) {
|
|
1188
1188
|
this.fallbackNonce = crypto4.randomBytes(16).toString("hex");
|
|
@@ -1232,7 +1232,7 @@ ${raw.slice(-1500)}` : `${reason}
|
|
|
1232
1232
|
this.fallbackSeen = undefined;
|
|
1233
1233
|
if (!this.pendingExitFailure)
|
|
1234
1234
|
await this.waitQuiet();
|
|
1235
|
-
this.send(`stty -echo -ixon cols 500 rows 50; export PS1= PS2=; echo PTYRDY_$
|
|
1235
|
+
this.send(`stty -echo -ixon cols 500 rows 50; export PS1= PS2=; echo PTYRDY_$$_$(id -un 2>/dev/null)
|
|
1236
1236
|
`);
|
|
1237
1237
|
try {
|
|
1238
1238
|
await new Promise((resolve, reject) => {
|
|
@@ -2612,6 +2612,8 @@ class RuntimeFactory {
|
|
|
2612
2612
|
}
|
|
2613
2613
|
// src/runtime-stack.ts
|
|
2614
2614
|
import { spawn as spawn5 } from "node:child_process";
|
|
2615
|
+
import * as os from "node:os";
|
|
2616
|
+
init_ssh_helpers();
|
|
2615
2617
|
class RuntimeStack {
|
|
2616
2618
|
static driverCache = new Map;
|
|
2617
2619
|
events = new EventBus;
|
|
@@ -2631,12 +2633,15 @@ class RuntimeStack {
|
|
|
2631
2633
|
cwd: d.cwd,
|
|
2632
2634
|
capabilities: d.capabilities,
|
|
2633
2635
|
cfg: {
|
|
2636
|
+
pty: c.pty,
|
|
2637
|
+
connParams: c.connParams,
|
|
2638
|
+
sshHops: c.sshHops,
|
|
2639
|
+
enters: enterStepsOf(c).map((s) => s.cmd),
|
|
2634
2640
|
launchCmd: c.launchCmd,
|
|
2635
2641
|
runtime: c.runtime,
|
|
2636
2642
|
shell: c.shell,
|
|
2637
2643
|
namespace: c.namespace,
|
|
2638
|
-
sshOptions: c.sshOptions
|
|
2639
|
-
connParams: c.connParams
|
|
2644
|
+
sshOptions: c.sshOptions
|
|
2640
2645
|
}
|
|
2641
2646
|
});
|
|
2642
2647
|
}
|
|
@@ -2663,6 +2668,185 @@ class RuntimeStack {
|
|
|
2663
2668
|
this.factory.registerDriver(type, f);
|
|
2664
2669
|
this.featuresCache = null;
|
|
2665
2670
|
}
|
|
2671
|
+
namedLayers = new Map;
|
|
2672
|
+
activeName;
|
|
2673
|
+
get layers() {
|
|
2674
|
+
return this.namedLayers;
|
|
2675
|
+
}
|
|
2676
|
+
get active() {
|
|
2677
|
+
return this.activeName ? this.namedLayers.get(this.activeName)?.driver : undefined;
|
|
2678
|
+
}
|
|
2679
|
+
get activeLayer() {
|
|
2680
|
+
return this.activeName ? this.namedLayers.get(this.activeName) : undefined;
|
|
2681
|
+
}
|
|
2682
|
+
async addLayer(name, descriptorOrDriver) {
|
|
2683
|
+
this.assertNotDupName(name);
|
|
2684
|
+
if (this.isDriver(descriptorOrDriver)) {
|
|
2685
|
+
const driver = descriptorOrDriver;
|
|
2686
|
+
const handle = { name, descriptor: driver.descriptor, driver };
|
|
2687
|
+
this.namedLayers.set(name, handle);
|
|
2688
|
+
this.activeName = name;
|
|
2689
|
+
return { handle, reused: false };
|
|
2690
|
+
}
|
|
2691
|
+
const descriptor = descriptorOrDriver;
|
|
2692
|
+
const hit = await this.findIdentityHit(descriptor);
|
|
2693
|
+
if (hit) {
|
|
2694
|
+
const reused = hit.terminal || await this.verifyLanding(hit.handle, hopHost(layerPathIdentities(descriptor).at(-1) ?? ""));
|
|
2695
|
+
if (reused) {
|
|
2696
|
+
this.log.info(`[Filaxis] 层 '${name}' 落点已由 '${hit.handle.name}' 持有——幂等复用(不重复建连)`);
|
|
2697
|
+
return { handle: hit.handle, reused: true, reusedFrom: hit.handle.name };
|
|
2698
|
+
}
|
|
2699
|
+
throw new Error(`层名 '${name}' 的落点 identity 命中既有层 '${hit.handle.name}' 的途经节点,且落点校验不符` + `(期望 ${hopHost(layerPathIdentities(descriptor).at(-1) ?? "?")},实测 ${hit.handle.actualHostname ?? "?"})` + `(宁重连不错主机):请检查连接参数后重试`);
|
|
2700
|
+
}
|
|
2701
|
+
const driver = await this.connect(descriptor);
|
|
2702
|
+
const handle = { name, descriptor: driver.descriptor, driver };
|
|
2703
|
+
this.namedLayers.set(name, handle);
|
|
2704
|
+
this.activeName = name;
|
|
2705
|
+
return { handle, reused: false };
|
|
2706
|
+
}
|
|
2707
|
+
async layerInto(from, name, enterCmd, opts = {}) {
|
|
2708
|
+
this.assertNotDupName(name);
|
|
2709
|
+
const src = this.requireLayer(from);
|
|
2710
|
+
const pushInto = src.driver.pushInto;
|
|
2711
|
+
if (typeof pushInto !== "function") {
|
|
2712
|
+
throw new Error(`层 '${from}'(${src.descriptor.driver})不支持派生进入——需 ssh PTY 形态(pushInto 缺席)`);
|
|
2713
|
+
}
|
|
2714
|
+
const driver = await pushInto.call(src.driver, enterCmd, opts.label ?? enterCmd.slice(0, 30));
|
|
2715
|
+
const handle = { name, descriptor: driver.descriptor, driver };
|
|
2716
|
+
this.namedLayers.set(name, handle);
|
|
2717
|
+
this.activeName = name;
|
|
2718
|
+
return handle;
|
|
2719
|
+
}
|
|
2720
|
+
async sshInto(from, name, target, connParams) {
|
|
2721
|
+
this.assertNotDupName(name);
|
|
2722
|
+
const src = this.requireLayer(from);
|
|
2723
|
+
const hop = connParams ? { host: target, conn: connParams } : target;
|
|
2724
|
+
const hit = await this.findIdentityHopHit(hop);
|
|
2725
|
+
if (hit) {
|
|
2726
|
+
const reused = hit.terminal || await this.verifyLanding(hit.handle, hopHost(hop));
|
|
2727
|
+
if (reused) {
|
|
2728
|
+
this.log.info(`[Filaxis] 层 '${name}' 落点已由 '${hit.handle.name}' 持有——幂等复用(不重复建连)`);
|
|
2729
|
+
return { handle: hit.handle, reused: true, reusedFrom: hit.handle.name };
|
|
2730
|
+
}
|
|
2731
|
+
throw new Error(`层名 '${name}' 的落点 identity 命中既有层 '${hit.handle.name}' 的途经节点,且落点校验不符` + `(期望 ${hopHost(hop)},实测 ${hit.handle.actualHostname ?? "?"})——宁重连不错主机:请检查连接参数后重试`);
|
|
2732
|
+
}
|
|
2733
|
+
const pushChild = src.driver.pushChild;
|
|
2734
|
+
if (typeof pushChild !== "function") {
|
|
2735
|
+
throw new Error(`层 '${from}'(${src.descriptor.driver})不支持链式 ssh 派生——需 ssh PTY 形态(pushChild 缺席)`);
|
|
2736
|
+
}
|
|
2737
|
+
const driver = await pushChild.call(src.driver, hop);
|
|
2738
|
+
const handle = { name, descriptor: driver.descriptor, driver };
|
|
2739
|
+
this.namedLayers.set(name, handle);
|
|
2740
|
+
this.activeName = name;
|
|
2741
|
+
return { handle, reused: false };
|
|
2742
|
+
}
|
|
2743
|
+
layer(name) {
|
|
2744
|
+
return this.requireLayer(name);
|
|
2745
|
+
}
|
|
2746
|
+
setActive(name) {
|
|
2747
|
+
const h = this.requireLayer(name);
|
|
2748
|
+
this.activeName = name;
|
|
2749
|
+
return h;
|
|
2750
|
+
}
|
|
2751
|
+
async removeLayer(name, opts = {}) {
|
|
2752
|
+
const h = this.requireLayer(name);
|
|
2753
|
+
this.namedLayers.delete(name);
|
|
2754
|
+
if (opts.destroy) {
|
|
2755
|
+
RuntimeStack.evictDriver(h.descriptor);
|
|
2756
|
+
await h.driver.destroy().catch(() => {
|
|
2757
|
+
return;
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
if (this.activeName === name) {
|
|
2761
|
+
this.activeName = [...this.namedLayers.keys()].at(-1);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
layerText() {
|
|
2765
|
+
if (this.namedLayers.size === 0)
|
|
2766
|
+
return "Filaxis 命名层:(空——addLayer/layerInto 注册)";
|
|
2767
|
+
const lines = [];
|
|
2768
|
+
for (const h of this.namedLayers.values()) {
|
|
2769
|
+
const d = h.driver;
|
|
2770
|
+
const base = h.descriptor.driver === "local" ? `${os.hostname()}[local]` : layerDisplayName(h.descriptor);
|
|
2771
|
+
const state = d.reconnecting === true ? " ↻连中" : d.layerShellGone ? " ⚠退化(内层 shell 已退出,勿在此层执行)" : d.alive === false ? " ⚠死(removeLayer 后按原参数重建)" : "";
|
|
2772
|
+
const user = d.effectiveUser ? ` · ${d.effectiveUser}$` : "";
|
|
2773
|
+
const mark = h.name === this.activeName ? " ◀ 默认" : "";
|
|
2774
|
+
lines.push(` ${h.name}: ${base}${user}${state}${mark}`);
|
|
2775
|
+
}
|
|
2776
|
+
return ["Filaxis 命名层(工具缺省落【默认层】;显式按名寻址任意层):", ...lines].join(`
|
|
2777
|
+
`);
|
|
2778
|
+
}
|
|
2779
|
+
snapshot() {
|
|
2780
|
+
return {
|
|
2781
|
+
layers: [...this.namedLayers.values()].map((h) => ({ name: h.name, descriptor: h.descriptor })),
|
|
2782
|
+
active: this.activeName
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
async restore(snap) {
|
|
2786
|
+
for (const { name, descriptor } of snap.layers) {
|
|
2787
|
+
if (this.namedLayers.has(name))
|
|
2788
|
+
continue;
|
|
2789
|
+
const driver = await this.connect(descriptor);
|
|
2790
|
+
this.namedLayers.set(name, { name, descriptor: driver.descriptor, driver });
|
|
2791
|
+
}
|
|
2792
|
+
if (snap.active && this.namedLayers.has(snap.active))
|
|
2793
|
+
this.activeName = snap.active;
|
|
2794
|
+
}
|
|
2795
|
+
assertNotDupName(name) {
|
|
2796
|
+
if (this.namedLayers.has(name)) {
|
|
2797
|
+
throw new Error(`层名 '${name}' 已占用(既有:${[...this.namedLayers.keys()].join(", ")})——换名或先 removeLayer`);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
requireLayer(name) {
|
|
2801
|
+
const h = this.namedLayers.get(name);
|
|
2802
|
+
if (!h) {
|
|
2803
|
+
throw new Error(`层 '${name}' 不存在(既有:${[...this.namedLayers.keys()].join(", ") || "空"})`);
|
|
2804
|
+
}
|
|
2805
|
+
return h;
|
|
2806
|
+
}
|
|
2807
|
+
isDriver(x) {
|
|
2808
|
+
return typeof x.exec === "function";
|
|
2809
|
+
}
|
|
2810
|
+
async findIdentityHit(descriptor) {
|
|
2811
|
+
if (enterStepsOf(descriptor.config ?? {}).length > 0)
|
|
2812
|
+
return;
|
|
2813
|
+
const dest = layerPathIdentities(descriptor).at(-1);
|
|
2814
|
+
if (!dest)
|
|
2815
|
+
return;
|
|
2816
|
+
return this.findIdentityHopHit(dest);
|
|
2817
|
+
}
|
|
2818
|
+
async findIdentityHopHit(dest) {
|
|
2819
|
+
const destId = await resolveSshIdentity(dest);
|
|
2820
|
+
if (!destId || destId.startsWith("unresolved:")) {
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
for (const h of this.namedLayers.values()) {
|
|
2824
|
+
if (enterStepsOf(h.descriptor.config ?? {}).length > 0)
|
|
2825
|
+
continue;
|
|
2826
|
+
const path = layerPathIdentities(h.descriptor);
|
|
2827
|
+
for (let i = 0;i < path.length; i++) {
|
|
2828
|
+
if (await resolveSshIdentity(path[i]) === destId) {
|
|
2829
|
+
return { handle: h, terminal: i === path.length - 1 };
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
async verifyLanding(hit, expectedHost) {
|
|
2836
|
+
if (!expectedHost)
|
|
2837
|
+
return true;
|
|
2838
|
+
const d = hit.driver;
|
|
2839
|
+
if (typeof d.exec !== "function")
|
|
2840
|
+
return true;
|
|
2841
|
+
try {
|
|
2842
|
+
hit.actualHostname ??= ((await d.exec("hostname")).stdout || "").trim().split(`
|
|
2843
|
+
`).pop() || "";
|
|
2844
|
+
} catch {
|
|
2845
|
+
return false;
|
|
2846
|
+
}
|
|
2847
|
+
const actual = hit.actualHostname;
|
|
2848
|
+
return Boolean(actual) && (actual.includes(expectedHost) || expectedHost.includes(actual));
|
|
2849
|
+
}
|
|
2666
2850
|
async features() {
|
|
2667
2851
|
if (this.featuresCache)
|
|
2668
2852
|
return this.featuresCache;
|
|
@@ -3573,31 +3757,12 @@ var q5 = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
|
3573
3757
|
var shellSafe = (s) => !s.includes("'") && !s.includes(`
|
|
3574
3758
|
`);
|
|
3575
3759
|
var statCmdOf = (f) => `stat -c '%Y %s' ${f} 2>/dev/null || stat -f '%m %z' ${f} 2>/dev/null`;
|
|
3576
|
-
var
|
|
3577
|
-
async function dialectOf(drv) {
|
|
3578
|
-
const key = JSON.stringify(drv.descriptor);
|
|
3579
|
-
const hit = dialectCache.get(key);
|
|
3580
|
-
if (hit)
|
|
3581
|
-
return hit;
|
|
3582
|
-
const probe = await drv.exec("echo FX2=$(printf filaxis | base64 -w 0 2>/dev/null | head -c 8);" + "echo FX3=$(printf ZmlsYXhpcw== | base64 -d 2>/dev/null | head -c 7);" + "echo FX4=$(printf ZmlsYXhpcw== | base64 -D 2>/dev/null | head -c 7)", { timeoutMs: 15000 });
|
|
3583
|
-
const out = probe.stdout || "";
|
|
3584
|
-
const b64Gnu = /FX2=ZmlsYXhp/.test(out);
|
|
3585
|
-
const decD = /FX4=filaxis/.test(out);
|
|
3586
|
-
const d = {
|
|
3587
|
-
statCmd: "",
|
|
3588
|
-
enc: b64Gnu ? "base64 -w 0" : "base64",
|
|
3589
|
-
dec: decD ? "base64 -D" : "base64 -d",
|
|
3590
|
-
shaCmd: `{ sha256sum ${"{f}"} 2>/dev/null || sha256 -q ${"{f}"} 2>/dev/null || openssl dgst -sha256 ${"{f}"} 2>/dev/null | tail -1; } | head -c 12`
|
|
3591
|
-
};
|
|
3592
|
-
dialectCache.set(key, d);
|
|
3593
|
-
return d;
|
|
3594
|
-
}
|
|
3760
|
+
var shaCmdOf = (f) => `{ sha256sum ${f} 2>/dev/null || sha256 -q ${f} 2>/dev/null || openssl dgst -sha256 ${f} 2>/dev/null | tail -1; } | head -c 12`;
|
|
3595
3761
|
async function editFile(drv, path, oldStr, newStr, opts = {}) {
|
|
3596
|
-
const dialect = await dialectOf(drv);
|
|
3597
3762
|
const qp = q5(path);
|
|
3598
|
-
const readCmd = `${statCmdOf(qp)}; ${
|
|
3763
|
+
const readCmd = `${statCmdOf(qp)}; ${shaCmdOf(qp)}; echo __FXS__; ` + `printf ZmlsYXhpcw== | base64 -d 2>/dev/null; echo __FXD__; base64 < ${qp} 2>/dev/null`;
|
|
3599
3764
|
const r = await drv.exec(readCmd, { timeoutMs: 30000 });
|
|
3600
|
-
const [stampPart,
|
|
3765
|
+
const [stampPart, restPart] = (r.stdout || "").split("__FXS__");
|
|
3601
3766
|
const originStampRaw = (stampPart ?? "").trim();
|
|
3602
3767
|
const originLines = originStampRaw.split(/\n/);
|
|
3603
3768
|
const statStamp = (originLines[0] ?? "").trim();
|
|
@@ -3612,7 +3777,9 @@ async function editFile(drv, path, oldStr, newStr, opts = {}) {
|
|
|
3612
3777
|
};
|
|
3613
3778
|
if (!origin.size || !shaStamp)
|
|
3614
3779
|
throw new Error(`文件不存在或指纹异常: ${path}(${originStampRaw})`);
|
|
3615
|
-
const
|
|
3780
|
+
const [decPart, b64Part = ""] = (restPart ?? "").split("__FXD__");
|
|
3781
|
+
const dec = (decPart ?? "").trim() === "filaxis" ? "base64 -d" : "base64 -D";
|
|
3782
|
+
const content0 = Buffer.from(b64Part.replace(/\s+/g, ""), "base64").toString("utf-8");
|
|
3616
3783
|
const occurrences = content0.split(oldStr).length - 1;
|
|
3617
3784
|
if (occurrences === 0) {
|
|
3618
3785
|
throw new Error(`未命中待替换片段(0 处): ${path}(oldStr 长度 ${oldStr.length},文件 ${content0.length} 字符)`);
|
|
@@ -3625,7 +3792,7 @@ async function editFile(drv, path, oldStr, newStr, opts = {}) {
|
|
|
3625
3792
|
if (newB64.length > 30000) {
|
|
3626
3793
|
return editFileViaFs(drv, path, edited, occurrences);
|
|
3627
3794
|
}
|
|
3628
|
-
const cmd = `if [ "$(${statCmdOf(qp)})" = "${statStamp}" ] && ` + `[ "$(${
|
|
3795
|
+
const cmd = `if [ "$(${statCmdOf(qp)})" = "${statStamp}" ] && ` + `[ "$(${shaCmdOf(qp)})" = "${shaStamp}" ]; then ` + `printf %s ${q5(newB64)} | ${dec} > ${qp}.fx-tmp && mv ${qp}.fx-tmp ${qp} && ` + `echo "FX_OK $(stat -c '%Y %s' ${qp} 2>/dev/null || stat -f '%m %z' ${qp} 2>/dev/null)"; ` + `else echo FX_CAS_FAIL; fi`;
|
|
3629
3796
|
const cas = await drv.exec(cmd, { timeoutMs: 30000 });
|
|
3630
3797
|
const out = (cas.stdout || "").trim();
|
|
3631
3798
|
if (out.startsWith("FX_CAS_FAIL")) {
|
|
@@ -3680,7 +3847,8 @@ var TOOL_DEFS = [
|
|
|
3680
3847
|
type: "object",
|
|
3681
3848
|
properties: {
|
|
3682
3849
|
command: { type: "string", description: "shell 命令" },
|
|
3683
|
-
timeout_ms: { type: "number", description: "超时毫秒(缺省 120000)" }
|
|
3850
|
+
timeout_ms: { type: "number", description: "超时毫秒(缺省 120000)" },
|
|
3851
|
+
layer: { type: "string", description: "目标层名(缺省=默认层——最近 push_ssh/push_into 的层)" }
|
|
3684
3852
|
},
|
|
3685
3853
|
required: ["command"]
|
|
3686
3854
|
}
|
|
@@ -3697,7 +3865,8 @@ var TOOL_DEFS = [
|
|
|
3697
3865
|
command: { type: "string", description: "长跑命令" },
|
|
3698
3866
|
ready_marker: { type: "string", description: "就绪标记(如 'listening on')" },
|
|
3699
3867
|
error_pattern: { type: "string", description: "错误模式(命中即收摊)" },
|
|
3700
|
-
name: { type: "string", description: "会话名(人读前缀)" }
|
|
3868
|
+
name: { type: "string", description: "会话名(人读前缀)" },
|
|
3869
|
+
layer: { type: "string", description: "目标层名(缺省=默认层)" }
|
|
3701
3870
|
},
|
|
3702
3871
|
required: ["command"]
|
|
3703
3872
|
}
|
|
@@ -3725,7 +3894,8 @@ var TOOL_DEFS = [
|
|
|
3725
3894
|
parameters: {
|
|
3726
3895
|
type: "object",
|
|
3727
3896
|
properties: {
|
|
3728
|
-
path: { type: "string" }
|
|
3897
|
+
path: { type: "string" },
|
|
3898
|
+
layer: { type: "string", description: "目标层名(缺省=默认层)" }
|
|
3729
3899
|
},
|
|
3730
3900
|
required: ["path"]
|
|
3731
3901
|
}
|
|
@@ -3742,7 +3912,8 @@ var TOOL_DEFS = [
|
|
|
3742
3912
|
path: { type: "string" },
|
|
3743
3913
|
old: { type: "string", description: "待替换片段(含足够上下文唯一)" },
|
|
3744
3914
|
new: { type: "string" },
|
|
3745
|
-
all: { type: "boolean", description: "多处命中全替换(缺省报错)" }
|
|
3915
|
+
all: { type: "boolean", description: "多处命中全替换(缺省报错)" },
|
|
3916
|
+
layer: { type: "string", description: "目标层名(缺省=默认层)" }
|
|
3746
3917
|
},
|
|
3747
3918
|
required: ["path", "old", "new"]
|
|
3748
3919
|
}
|
|
@@ -3759,22 +3930,87 @@ var TOOL_DEFS = [
|
|
|
3759
3930
|
command: { type: "string" },
|
|
3760
3931
|
duration_ms: { type: "number", description: "观察时长(缺省 8000)" },
|
|
3761
3932
|
stream: { type: "string", enum: ["stdout", "stderr", "both"], description: "流过滤(缺省 both)" },
|
|
3762
|
-
name: { type: "string" }
|
|
3933
|
+
name: { type: "string" },
|
|
3934
|
+
layer: { type: "string", description: "目标层名(缺省=默认层)" }
|
|
3763
3935
|
},
|
|
3764
3936
|
required: ["command"]
|
|
3765
3937
|
}
|
|
3766
3938
|
}
|
|
3939
|
+
},
|
|
3940
|
+
{
|
|
3941
|
+
type: "function",
|
|
3942
|
+
function: {
|
|
3943
|
+
name: "push_ssh",
|
|
3944
|
+
description: "建立命名 ssh 层(成为默认层——后续工具缺省落它)。同落点幂等复用(reused=true 返回既有层," + "不重复建连)+ 落点校验(宁重连不错主机)。跳板传 jump(-J 不占层)。",
|
|
3945
|
+
parameters: {
|
|
3946
|
+
type: "object",
|
|
3947
|
+
properties: {
|
|
3948
|
+
host: { type: "string", description: "目标主机(user@host 或 ssh config 别名)" },
|
|
3949
|
+
port: { type: "number", description: "SSH 端口(非 22 时指定)" },
|
|
3950
|
+
user: { type: "string", description: "登录用户名" },
|
|
3951
|
+
key: { type: "string", description: "私钥文件路径(-i)" },
|
|
3952
|
+
jump: { type: "string", description: "跳板(-J:途径机不占层)" },
|
|
3953
|
+
name: { type: "string", description: "层名(缺省=host;注册表内唯一,显式寻址用)" }
|
|
3954
|
+
},
|
|
3955
|
+
required: ["host"]
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
},
|
|
3959
|
+
{
|
|
3960
|
+
type: "function",
|
|
3961
|
+
function: {
|
|
3962
|
+
name: "push_into",
|
|
3963
|
+
description: "从默认层(或 from 指定层)进入内层 shell(docker exec -i <ctr> sh / kubectl exec 等)——" + "enter 管线全量继承追加(父层 docker→再 su 不丢)。新层成为默认层。需 ssh 层为源。",
|
|
3964
|
+
parameters: {
|
|
3965
|
+
type: "object",
|
|
3966
|
+
properties: {
|
|
3967
|
+
enter_cmd: { type: "string", description: '进入命令,如 "docker exec -i web sh"' },
|
|
3968
|
+
label: { type: "string", description: "层显示标签" },
|
|
3969
|
+
from: { type: "string", description: "源层名(缺省=默认层)" },
|
|
3970
|
+
name: { type: "string", description: "层名(缺省=label)" }
|
|
3971
|
+
},
|
|
3972
|
+
required: ["enter_cmd"]
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
},
|
|
3976
|
+
{
|
|
3977
|
+
type: "function",
|
|
3978
|
+
function: {
|
|
3979
|
+
name: "list_layers",
|
|
3980
|
+
description: "列出全部命名层(链路/状态/有效用户/默认层标记)。死层/退化层有警示标注(removeLayer 后按原参数重建)。",
|
|
3981
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
3982
|
+
}
|
|
3983
|
+
},
|
|
3984
|
+
{
|
|
3985
|
+
type: "function",
|
|
3986
|
+
function: {
|
|
3987
|
+
name: "remove_layer",
|
|
3988
|
+
description: "移出命名层(缺省连接保留在指纹缓存——再建同层零成本;destroy=true 才真销毁连接)。",
|
|
3989
|
+
parameters: {
|
|
3990
|
+
type: "object",
|
|
3991
|
+
properties: {
|
|
3992
|
+
name: { type: "string", description: "层名" },
|
|
3993
|
+
destroy: { type: "boolean", description: "真销毁连接(缺省 false=保留可复用)" }
|
|
3994
|
+
},
|
|
3995
|
+
required: ["name"]
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3767
3998
|
}
|
|
3768
3999
|
];
|
|
3769
4000
|
function createAgentToolkit(stack, opts = {}) {
|
|
3770
4001
|
const layer = opts.driver ? Promise.resolve(opts.driver) : stack.connect(opts.descriptor ?? { driver: "local" });
|
|
3771
4002
|
const readyTimeoutMs = opts.readyTimeoutMs ?? 15000;
|
|
4003
|
+
const resolveDrv = async (layerName) => {
|
|
4004
|
+
if (layerName)
|
|
4005
|
+
return stack.layer(layerName).driver;
|
|
4006
|
+
return stack.active ?? await layer;
|
|
4007
|
+
};
|
|
3772
4008
|
return {
|
|
3773
4009
|
tools: TOOL_DEFS,
|
|
3774
4010
|
layer,
|
|
3775
4011
|
handlers: {
|
|
3776
|
-
async run_command({ command, timeout_ms }) {
|
|
3777
|
-
const drv = await
|
|
4012
|
+
async run_command({ command, timeout_ms, layer: layerName }) {
|
|
4013
|
+
const drv = await resolveDrv(layerName);
|
|
3778
4014
|
const r = await drv.exec(command, { timeoutMs: timeout_ms });
|
|
3779
4015
|
return {
|
|
3780
4016
|
stdout: r.stdout,
|
|
@@ -3783,8 +4019,8 @@ function createAgentToolkit(stack, opts = {}) {
|
|
|
3783
4019
|
effective_user: r.effectiveUser
|
|
3784
4020
|
};
|
|
3785
4021
|
},
|
|
3786
|
-
async spawn_and_observe({ command, ready_marker, error_pattern, name }) {
|
|
3787
|
-
const drv = await
|
|
4022
|
+
async spawn_and_observe({ command, ready_marker, error_pattern, name, layer: layerName }) {
|
|
4023
|
+
const drv = await resolveDrv(layerName);
|
|
3788
4024
|
const h = await drv.execSession(command, name ? { name } : undefined);
|
|
3789
4025
|
if (error_pattern) {
|
|
3790
4026
|
drv.watchSession(h.sessionId, { pattern: error_pattern, once: false, thenStop: true });
|
|
@@ -3809,8 +4045,8 @@ function createAgentToolkit(stack, opts = {}) {
|
|
|
3809
4045
|
}
|
|
3810
4046
|
return { session_id: h.sessionId, log_base: h.logBase, note };
|
|
3811
4047
|
},
|
|
3812
|
-
async read_file({ path }) {
|
|
3813
|
-
const drv = await
|
|
4048
|
+
async read_file({ path, layer: layerName }) {
|
|
4049
|
+
const drv = await resolveDrv(layerName);
|
|
3814
4050
|
if (!drv.fs.stat)
|
|
3815
4051
|
throw new Error("该层不支持 read_file 的元数据(fs.stat 缺席)");
|
|
3816
4052
|
const st = await drv.fs.stat(path);
|
|
@@ -3819,13 +4055,13 @@ function createAgentToolkit(stack, opts = {}) {
|
|
|
3819
4055
|
const content = await drv.fs.readFile(path);
|
|
3820
4056
|
return { path, content, size: st.size, mtime_ms: st.mtimeMs };
|
|
3821
4057
|
},
|
|
3822
|
-
async edit_file({ path, old, new: newStr, all }) {
|
|
3823
|
-
const drv = await
|
|
4058
|
+
async edit_file({ path, old, new: newStr, all, layer: layerName }) {
|
|
4059
|
+
const drv = await resolveDrv(layerName);
|
|
3824
4060
|
const r = await editFile(drv, path, old, newStr, { all });
|
|
3825
4061
|
return { path: r.path, occurrences: r.occurrences, bytes_before: r.bytesBefore, bytes_after: r.bytesAfter };
|
|
3826
4062
|
},
|
|
3827
|
-
async observe_stream({ command, duration_ms, stream, name }) {
|
|
3828
|
-
const drv = await
|
|
4063
|
+
async observe_stream({ command, duration_ms, stream, name, layer: layerName }) {
|
|
4064
|
+
const drv = await resolveDrv(layerName);
|
|
3829
4065
|
const h = await drv.execSession(command, name ? { name } : undefined);
|
|
3830
4066
|
const dur = duration_ms ?? 8000;
|
|
3831
4067
|
await new Promise((r) => setTimeout(r, dur));
|
|
@@ -3837,6 +4073,49 @@ function createAgentToolkit(stack, opts = {}) {
|
|
|
3837
4073
|
});
|
|
3838
4074
|
return { session_id: h.sessionId, lines: filtered };
|
|
3839
4075
|
},
|
|
4076
|
+
async push_ssh({ host, port, user, key, jump, name }) {
|
|
4077
|
+
const connParams = {};
|
|
4078
|
+
if (port != null)
|
|
4079
|
+
connParams.port = Number(port);
|
|
4080
|
+
if (user)
|
|
4081
|
+
connParams.user = String(user);
|
|
4082
|
+
if (key)
|
|
4083
|
+
connParams.keyFile = String(key);
|
|
4084
|
+
if (jump)
|
|
4085
|
+
connParams.jump = String(jump);
|
|
4086
|
+
const r = await stack.addLayer(name ?? String(host), {
|
|
4087
|
+
driver: "ssh",
|
|
4088
|
+
target: String(host),
|
|
4089
|
+
config: Object.keys(connParams).length ? { connParams } : undefined
|
|
4090
|
+
});
|
|
4091
|
+
return {
|
|
4092
|
+
layer: r.handle.name,
|
|
4093
|
+
reused: r.reused,
|
|
4094
|
+
reused_from: r.reusedFrom,
|
|
4095
|
+
note: r.reused ? "落点已由既有层持有——幂等复用(未重复建连)" : "新层已建并成为默认层"
|
|
4096
|
+
};
|
|
4097
|
+
},
|
|
4098
|
+
async push_into({ enter_cmd, label, from, name }) {
|
|
4099
|
+
const srcName = from ?? stack.activeLayer?.name;
|
|
4100
|
+
if (!srcName)
|
|
4101
|
+
throw new Error("无源层——先 push_ssh 建层,或显式传 from");
|
|
4102
|
+
const h = await stack.layerInto(srcName, name ?? label ?? enter_cmd.slice(0, 30), enter_cmd, { label });
|
|
4103
|
+
return { layer: h.name, target: h.descriptor.target };
|
|
4104
|
+
},
|
|
4105
|
+
async list_layers(_args) {
|
|
4106
|
+
return {
|
|
4107
|
+
text: stack.layerText(),
|
|
4108
|
+
layers: [...stack.layers.values()].map((h) => ({
|
|
4109
|
+
name: h.name,
|
|
4110
|
+
driver: h.descriptor.driver,
|
|
4111
|
+
target: h.descriptor.target
|
|
4112
|
+
}))
|
|
4113
|
+
};
|
|
4114
|
+
},
|
|
4115
|
+
async remove_layer({ name, destroy }) {
|
|
4116
|
+
await stack.removeLayer(name, { destroy });
|
|
4117
|
+
return { removed: name };
|
|
4118
|
+
},
|
|
3840
4119
|
async stop_and_verify({ session_id }) {
|
|
3841
4120
|
const drv = await layer;
|
|
3842
4121
|
const r = await drv.stopSession(session_id);
|