@ai-workbench/remote-agent 0.8.25 → 0.8.27
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 +5 -1
- package/dist/catalog-ssh.d.ts +2 -1
- package/dist/catalog-ssh.js +44 -37
- package/dist/employee-shell.d.ts +1 -1
- package/dist/employee-shell.js +45 -12
- package/dist/index.js +6 -3
- package/dist/model-catalog.d.ts +4 -1
- package/dist/model-catalog.js +60 -19
- package/dist/remote-details.d.ts +2 -0
- package/dist/remote-details.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.8.
|
|
1
|
+
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.8.27 · 插件 → 添加插件
|
|
2
2
|
|
|
3
3
|
通过官方 DSH 插件入口使用 Claude(远程主机)与 Codex(远程主机)。登录和工作区来源由“远程主机连接”组件提供;员工在原生工作区和模型菜单中选择文件夹与型号。
|
|
4
4
|
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
需要远程主机连接组件 0.7.29 或更新的 0.7.x,以及联网助手 0.4.9。主机来源保存于官方 storage.domain 的会话元数据表,每轮仅由连接组件检查一次主机目录,不检查本机占位目录。
|
|
8
8
|
|
|
9
|
+
Claude 本机 Bash 会单独消费员工桥代理的控制诊断,不把它交给模型当作命令输出;真实传输中断仍提示结果未知。命令自身的标准输出、标准错误与退出码照常返回。
|
|
10
|
+
|
|
11
|
+
模型列表读取会随连接状态或连接代际变化取消,连接恢复时通过官方机制刷新。目录失败原因写入当前 profile 的 `ai-workbench-remote-agent/logs/model-catalog.jsonl`(脱敏);未增加单次读取时限或重试。
|
|
12
|
+
|
|
9
13
|
## 会话行为
|
|
10
14
|
|
|
11
15
|
每轮启动一个新助手进程,按原生会话编号恢复上下文;不接回旧进程或重放上一轮输入。主机会话始终使用创建时保存的主机和目录;工作区之后的选择只影响新会话。
|
package/dist/catalog-ssh.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export interface CatalogSshEvent {
|
|
|
14
14
|
export declare function catalogSafeText(value: string): string;
|
|
15
15
|
export declare class CatalogSshError extends ModelConfigurationError {
|
|
16
16
|
readonly event: CatalogSshEvent;
|
|
17
|
-
|
|
17
|
+
private readonly stderr;
|
|
18
|
+
constructor(reason: string, event: CatalogSshEvent, stderr?: string);
|
|
18
19
|
get diagnostic(): string;
|
|
19
20
|
}
|
|
20
21
|
export type CatalogSshObserver = (event: Readonly<CatalogSshEvent>) => void | Promise<void>;
|
package/dist/catalog-ssh.js
CHANGED
|
@@ -49,11 +49,13 @@ export function catalogSafeText(value) {
|
|
|
49
49
|
}
|
|
50
50
|
export class CatalogSshError extends ModelConfigurationError {
|
|
51
51
|
event;
|
|
52
|
-
|
|
52
|
+
stderr;
|
|
53
|
+
constructor(reason, event, stderr = '') {
|
|
53
54
|
super(reason);
|
|
54
55
|
this.event = event;
|
|
56
|
+
this.stderr = stderr;
|
|
55
57
|
}
|
|
56
|
-
get diagnostic() { return '目录连接技术详情:' + JSON.stringify(this.event); }
|
|
58
|
+
get diagnostic() { return '目录连接技术详情:' + JSON.stringify(this.event) + (this.stderr ? '\nSSH诊断:' + this.stderr : ''); }
|
|
57
59
|
}
|
|
58
60
|
/** Each query starts a fresh strict L SSH invocation. No connection pool or retry. */
|
|
59
61
|
export async function runCatalog(target, command, signal, maxBytes, observe) {
|
|
@@ -76,10 +78,12 @@ export async function runCatalog(target, command, signal, maxBytes, observe) {
|
|
|
76
78
|
return new Promise((resolve, reject) => {
|
|
77
79
|
const child = spawn('ssh', argv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
78
80
|
const output = [], diagnostics = [];
|
|
79
|
-
let total = 0, stdoutBytes = 0, stderrBytes = 0, processErrorCode = null, failure;
|
|
80
|
-
const abort = () => { failure ??= { code: 'MODEL_CATALOG_CANCELLED', reason: '
|
|
81
|
+
let settled = false, total = 0, stdoutBytes = 0, stderrBytes = 0, processErrorCode = null, failure;
|
|
82
|
+
const abort = () => { failure ??= { code: 'MODEL_CATALOG_CANCELLED', reason: '连接已变化或读取已取消;连接恢复后请重新打开模型菜单。' }; child.kill('SIGTERM'); complete(null, null); };
|
|
81
83
|
signal.addEventListener('abort', abort, { once: true });
|
|
82
84
|
const receive = (buffer, stdout) => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
83
87
|
if (stdout)
|
|
84
88
|
stdoutBytes += buffer.length;
|
|
85
89
|
else
|
|
@@ -88,46 +92,49 @@ export async function runCatalog(target, command, signal, maxBytes, observe) {
|
|
|
88
92
|
if (failure)
|
|
89
93
|
return;
|
|
90
94
|
if (total > maxBytes) {
|
|
91
|
-
failure = { code: 'MODEL_CATALOG_OUTPUT_LIMIT', reason: '
|
|
95
|
+
failure = { code: 'MODEL_CATALOG_OUTPUT_LIMIT', reason: '主机返回的模型列表过大,暂时无法显示;请联系管理员检查后重新打开模型菜单。' };
|
|
92
96
|
child.kill('SIGTERM');
|
|
97
|
+
complete(null, null);
|
|
93
98
|
return;
|
|
94
99
|
}
|
|
95
100
|
(stdout ? output : diagnostics).push(buffer);
|
|
96
101
|
};
|
|
97
102
|
child.stdout.on('data', (b) => receive(b, true));
|
|
98
103
|
child.stderr.on('data', (b) => receive(b, false));
|
|
99
|
-
child.on('error', (error) => { processErrorCode = typeof error.code === 'string' && /^[A-Z0-9_]+$/.test(error.code) ? error.code : 'SSH_SPAWN_FAILED'; failure ??= { code: processErrorCode, reason: '
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
catch
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
104
|
+
child.on('error', (error) => { processErrorCode = typeof error.code === 'string' && /^[A-Z0-9_]+$/.test(error.code) ? error.code : 'SSH_SPAWN_FAILED'; failure ??= { code: processErrorCode, reason: '连接程序未能启动,暂时读不到模型列表;请从远程主机连接检查组件后重新打开模型菜单。' }; complete(null, null); });
|
|
105
|
+
const complete = (code, closeSignal) => {
|
|
106
|
+
if (settled)
|
|
107
|
+
return;
|
|
108
|
+
settled = true;
|
|
109
|
+
signal.removeEventListener('abort', abort);
|
|
110
|
+
let value = '', typed = false;
|
|
111
|
+
try {
|
|
112
|
+
value = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(output));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
failure ??= { code: 'MODEL_CATALOG_UTF8_INVALID', reason: '主机返回的模型列表内容损坏,暂时无法读取;请联系管理员检查后重新打开模型菜单。' };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
typed = JSON.parse(value)?.ok === false;
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
if (!failure && code !== 0 && !typed)
|
|
122
|
+
failure = { code: 'SSH_EXIT_NONZERO', reason: '模型列表暂时无法读取,请检查远程主机连接后重新打开模型菜单。' };
|
|
123
|
+
const final = { provider, exitCode: code ?? null, signal: closeSignal ?? null, stdoutBytes, stderrBytes, elapsedMs: performance.now() - tick, failureCode: failure?.code ?? null, processErrorCode };
|
|
124
|
+
console.info('[remote-agent] model-list ' + provider + ' exit=' + final.exitCode + ' bytes=' + stdoutBytes + ' elapsedMs=' + final.elapsedMs.toFixed(1));
|
|
125
|
+
try {
|
|
126
|
+
void Promise.resolve(observe?.(final)).catch(() => console.warn('[remote-agent] 模型列表查询日志未保存'));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
console.warn('[remote-agent] 模型列表查询日志未保存');
|
|
130
|
+
}
|
|
131
|
+
if (failure) {
|
|
132
|
+
reject(new CatalogSshError(failure.reason, final, catalogSafeText(Buffer.concat(diagnostics).toString('utf8'))));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
resolve(value);
|
|
136
|
+
};
|
|
137
|
+
child.once('close', (code, closeSignal) => complete(code, closeSignal));
|
|
131
138
|
if (signal.aborted)
|
|
132
139
|
abort();
|
|
133
140
|
});
|
package/dist/employee-shell.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CloseDiagnostics } from './close-diagnostics.ts';
|
|
2
2
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
3
3
|
import type { EmployeeEndpointTarget } from './employee-endpoint.ts';
|
|
4
4
|
export interface EmployeeShellResult {
|
package/dist/employee-shell.js
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
|
-
import { closeCode } from "./close-diagnostics.js";
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { EMPLOYEE_PYTHON_ERRORS } from "./employee-relay.js";
|
|
4
3
|
import { NeedError } from "./remote.js";
|
|
5
4
|
const SHELL_PYTHON = String.raw `
|
|
6
|
-
import sys,json,subprocess,shlex
|
|
5
|
+
import sys,json,subprocess,shlex,tempfile,signal,re
|
|
7
6
|
def fail(code):
|
|
8
7
|
sys.stderr.write(json.dumps({"code":code})+"\n");sys.exit(254)
|
|
9
8
|
try:
|
|
10
|
-
x=json.loads(sys.argv[1]);e=x["endpoint"];args=x["sshArgs"];cwd=x["cwd"]
|
|
9
|
+
x=json.loads(sys.argv[1]);e=x["endpoint"];args=list(x["sshArgs"]);cwd=x["cwd"]
|
|
10
|
+
proxy=[i for i,arg in enumerate(args) if arg.startswith("ProxyCommand=")]
|
|
11
|
+
if len(proxy)!=1:fail("EMPLOYEE_BRIDGE_PROTOCOL_INVALID")
|
|
11
12
|
# User source is stdin, never argv. The sentinel preserves trailing newlines.
|
|
12
13
|
wrapper="cd -- "+shlex.quote(cwd)+" || exit 254; "+"printf '%s\\n' "+shlex.quote('AW_EMPLOYEE_SHELL_STARTED '+x['executionId'])+" >&2; "+'aw_x_source=$(/bin/cat; printf x); aw_x_source='+chr(36)+'{aw_x_source%?}; eval "'+chr(36)+'aw_x_source"'
|
|
13
14
|
command=shlex.join([e["endpointShell"],"-c",wrapper])
|
|
14
15
|
sys.stderr.write("AW_EMPLOYEE_SHELL_READY "+x["executionId"]+"\n");sys.stderr.flush()
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
# OpenSSH inherits ProxyCommand fd2 into its combined stderr. Separate that
|
|
17
|
+
# producer before launch; never inspect/filter the employee command's bytes.
|
|
18
|
+
def interrupted(signum,frame):
|
|
19
|
+
raise SystemExit(255)
|
|
20
|
+
for sig in [signal.SIGTERM,signal.SIGHUP,signal.SIGINT]:signal.signal(sig,interrupted)
|
|
21
|
+
with tempfile.NamedTemporaryFile(prefix="aw-x-bridge-") as diagnostic:
|
|
22
|
+
# S already escaped its own percent tokens. Only escape the added path.
|
|
23
|
+
i=proxy[0];args[i]+=" 2>"+shlex.quote(diagnostic.name.replace("%","%%"))
|
|
24
|
+
p=subprocess.Popen(["/usr/bin/ssh","-T"]+args+[command],stdin=sys.stdin,stdout=sys.stdout,stderr=sys.stderr)
|
|
25
|
+
code=p.wait()
|
|
26
|
+
diagnostic.seek(0)
|
|
27
|
+
# S's raw CLI emits at most one fixed error line, never arbitrary diagnostics.
|
|
28
|
+
raw=diagnostic.read(x["maxBytes"]+1)
|
|
29
|
+
match=re.fullmatch(rb"BRIDGE_ERROR ([A-Z][A-Z0-9_]{0,63})\n",raw)
|
|
30
|
+
bridge_code=match.group(1).decode("ascii") if match else None
|
|
31
|
+
sys.stderr.write("\nAW_EMPLOYEE_BRIDGE_RESULT "+x["executionId"]+" "+json.dumps(bridge_code)+"\n");sys.stderr.flush()
|
|
32
|
+
sys.exit(code if code>=0 else 255)
|
|
17
33
|
` + EMPLOYEE_PYTHON_ERRORS;
|
|
18
34
|
const unknown = (code) => new NeedError(code, '本机命令结果未知;没有重放,也未改在主机执行。', '员工端真实退出回执');
|
|
19
35
|
/** Use only the target verified at this round lease opening. Cancellation cannot prove remote exit. */
|
|
@@ -22,11 +38,10 @@ export function createEmployeeShell(options) {
|
|
|
22
38
|
signal.throwIfAborted();
|
|
23
39
|
const diagnostics = options.closeDiagnostics?.child();
|
|
24
40
|
const executionId = randomUUID(), target = options.target;
|
|
25
|
-
const child = await options.attach({ command: '/usr/bin/python3', args: ['-c', SHELL_PYTHON, JSON.stringify({ endpoint: target.endpoint, sshArgs: options.sshArgs, cwd: target.local.directory.path, executionId })] });
|
|
41
|
+
const child = await options.attach({ command: '/usr/bin/python3', args: ['-c', SHELL_PYTHON, JSON.stringify({ endpoint: target.endpoint, sshArgs: options.sshArgs, cwd: target.local.directory.path, executionId, maxBytes: options.maxBytes })] });
|
|
26
42
|
return new Promise((yes, no) => {
|
|
27
43
|
let settled = false, bytes = 0, stdout = [], stderr = [], reason, grace, timer;
|
|
28
|
-
const finish = (error, code) => {
|
|
29
|
-
diagnostics?.record({ layer: 'executor', initiator: 'shell.finish', reason: closeCode(error, 'SHELL_FINISHED'), action: 'close', exitCode: code ?? null, changes: [{ field: 'shell.settled', before: settled, after: true }] });
|
|
44
|
+
const finish = (error, code, exitSignal) => {
|
|
30
45
|
if (settled)
|
|
31
46
|
return;
|
|
32
47
|
settled = true;
|
|
@@ -39,6 +54,7 @@ export function createEmployeeShell(options) {
|
|
|
39
54
|
child.stdout.destroy();
|
|
40
55
|
child.stderr.destroy();
|
|
41
56
|
if (error) {
|
|
57
|
+
diagnostics?.record({ layer: 'executor', initiator: 'shell.finish', reason: error.code, action: 'failure', exitCode: code ?? null });
|
|
42
58
|
no(error);
|
|
43
59
|
return;
|
|
44
60
|
}
|
|
@@ -51,6 +67,23 @@ export function createEmployeeShell(options) {
|
|
|
51
67
|
no(unknown('EMPLOYEE_OUTPUT_BINARY_UNSUPPORTED'));
|
|
52
68
|
return;
|
|
53
69
|
}
|
|
70
|
+
const trailer = '\nAW_EMPLOYEE_BRIDGE_RESULT ' + executionId + ' ', end = err.lastIndexOf(trailer);
|
|
71
|
+
let bridgeCode;
|
|
72
|
+
if (end >= 0) {
|
|
73
|
+
try {
|
|
74
|
+
const value = JSON.parse(err.slice(end + trailer.length));
|
|
75
|
+
if (typeof value === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(value))
|
|
76
|
+
bridgeCode = value;
|
|
77
|
+
else if (value !== null)
|
|
78
|
+
throw Error();
|
|
79
|
+
err = err.slice(0, end);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
no(unknown('EMPLOYEE_EXIT_UNKNOWN'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
diagnostics?.record({ layer: 'relay', initiator: 'shell.process-close', reason: 'PROCESS_EXIT', action: 'exit', exitCode: code ?? null, signal: exitSignal ?? null, ...bridgeCode ? { remoteReason: bridgeCode } : {} });
|
|
54
87
|
const header = 'AW_EMPLOYEE_SHELL_READY ' + executionId + '\n', started = 'AW_EMPLOYEE_SHELL_STARTED ' + executionId + '\n';
|
|
55
88
|
const start = err.indexOf(started, header.length);
|
|
56
89
|
if (!err.startsWith(header) || start < header.length || start > header.length && err[start - 1] !== '\n') {
|
|
@@ -62,11 +95,11 @@ export function createEmployeeShell(options) {
|
|
|
62
95
|
source = parsed.code;
|
|
63
96
|
}
|
|
64
97
|
catch { /* Never expose arbitrary transport diagnostics. */ }
|
|
65
|
-
no(unknown(source ?? 'EMPLOYEE_EXIT_UNKNOWN'));
|
|
98
|
+
no(unknown(bridgeCode ?? source ?? 'EMPLOYEE_EXIT_UNKNOWN'));
|
|
66
99
|
return;
|
|
67
100
|
}
|
|
68
|
-
if (code === null || code === undefined || code === 255) {
|
|
69
|
-
no(unknown('EMPLOYEE_EXIT_UNKNOWN'));
|
|
101
|
+
if (code === null || code === undefined || code === 255 || end < 0) {
|
|
102
|
+
no(unknown(bridgeCode ?? 'EMPLOYEE_EXIT_UNKNOWN'));
|
|
70
103
|
return;
|
|
71
104
|
}
|
|
72
105
|
yes({ stdout: out, stderr: err.slice(header.length, start) + err.slice(start + started.length), stderrSource: 'combined-ssh-and-command', exitCode: code, signal: null });
|
|
@@ -85,7 +118,7 @@ export function createEmployeeShell(options) {
|
|
|
85
118
|
child.stderr.on('data', (b) => collect(stderr, b));
|
|
86
119
|
child.stdin.on('error', () => stop('EMPLOYEE_INPUT_UNKNOWN'));
|
|
87
120
|
child.once('error', () => { diagnostics?.record({ layer: 'relay', initiator: 'shell.process-error', reason: 'EMPLOYEE_TRANSPORT_UNKNOWN', action: 'failure' }); finish(unknown('EMPLOYEE_TRANSPORT_UNKNOWN')); });
|
|
88
|
-
child.once('close', (code, exitSignal) =>
|
|
121
|
+
child.once('close', (code, exitSignal) => finish(reason, code, exitSignal));
|
|
89
122
|
signal.addEventListener('abort', abort, { once: true });
|
|
90
123
|
if (signal.aborted)
|
|
91
124
|
abort();
|
package/dist/index.js
CHANGED
|
@@ -115,7 +115,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
115
115
|
providerInfo(provider) { return { id: provider, name: this.providerName() }; }
|
|
116
116
|
catalog(signal) { if (!this.directory)
|
|
117
117
|
throw new LlmError('无法获取模型列表:缺少可信主机目录能力', 'MODEL_CATALOG_UNAVAILABLE'); return this.directory.read(this.agent, signal); }
|
|
118
|
-
async listModels(provider) { const catalog = await this.catalog(); try {
|
|
118
|
+
async listModels(provider) { const catalog = await this.catalog(this.lifetime.signal); try {
|
|
119
|
+
this.lifetime.signal.throwIfAborted();
|
|
119
120
|
this.modelPresentation.remember(catalog);
|
|
120
121
|
return catalog.models.map(model => modelInfo(provider, model));
|
|
121
122
|
}
|
|
@@ -621,7 +622,7 @@ export function apply(ctx, live) {
|
|
|
621
622
|
};
|
|
622
623
|
// Public Loader Entry.options.id; optional when mounted without Loader.
|
|
623
624
|
const actualSettingsNamespace = ctx.fiber.entry?.options.id;
|
|
624
|
-
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes);
|
|
625
|
+
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, undefined, (provider, error) => managerForProfile().recordCatalogFailure(provider, error));
|
|
625
626
|
const adapter = new RemoteAgentsAdapter(config, agent => {
|
|
626
627
|
const details = async (request, api, connection, target) => {
|
|
627
628
|
return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
|
|
@@ -689,8 +690,10 @@ export function apply(ctx, live) {
|
|
|
689
690
|
const previous = connection;
|
|
690
691
|
connection = state;
|
|
691
692
|
// Official replacement invalidates the browser's shared catalog and loads it.
|
|
692
|
-
if (
|
|
693
|
+
if (previous?.phase !== state.phase || previous.instanceId !== state.instanceId || previous.epoch !== state.epoch) {
|
|
694
|
+
modelDirectory.cancelPending();
|
|
693
695
|
registration.replace(ids);
|
|
696
|
+
}
|
|
694
697
|
});
|
|
695
698
|
installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnosticForAttempt: position => preflightDiagnostics.forPreparation(position.sessionId, position), preflightDiagnostic: (provider, message) => diagnostic('preflight', provider, message), diagnostic: (provider, message) => diagnostic('input', provider, message) });
|
|
696
699
|
const cleanup = async (id) => { try {
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -44,7 +44,10 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
44
44
|
private get;
|
|
45
45
|
private maxBytes;
|
|
46
46
|
private run;
|
|
47
|
-
|
|
47
|
+
private reportFailure?;
|
|
48
|
+
private pending;
|
|
49
|
+
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, reportFailure?: ((provider: "claude" | "codex", error: unknown) => void) | undefined);
|
|
50
|
+
cancelPending(): void;
|
|
48
51
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
49
52
|
dispose(): void;
|
|
50
53
|
}
|
package/dist/model-catalog.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import { validateTarget } from "./provider.js";
|
|
3
|
+
import { ModelConfigurationError } from "./model-configuration.js";
|
|
3
4
|
import { runCatalog, catalogSafeText } from "./catalog-ssh.js";
|
|
4
5
|
export { runCatalog } from "./catalog-ssh.js";
|
|
5
6
|
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -91,32 +92,72 @@ export class ModelCatalog {
|
|
|
91
92
|
get;
|
|
92
93
|
maxBytes;
|
|
93
94
|
run;
|
|
94
|
-
|
|
95
|
+
reportFailure;
|
|
96
|
+
pending = new Set();
|
|
97
|
+
constructor(get, maxBytes, run = runCatalog, reportFailure) {
|
|
95
98
|
this.get = get;
|
|
96
99
|
this.maxBytes = maxBytes;
|
|
97
100
|
this.run = run;
|
|
101
|
+
this.reportFailure = reportFailure;
|
|
102
|
+
}
|
|
103
|
+
cancelPending() {
|
|
104
|
+
for (const controller of this.pending)
|
|
105
|
+
controller.abort(new CatalogError('连接已变化,本次模型列表读取已停止;连接恢复后请重新打开模型菜单。', 'MODEL_CATALOG_CONNECTION_CHANGED'));
|
|
106
|
+
this.pending.clear();
|
|
98
107
|
}
|
|
99
108
|
async read(agent, signal) {
|
|
100
|
-
signal?.throwIfAborted();
|
|
101
|
-
const api = this.get();
|
|
102
|
-
if (!api || typeof api.getModelCatalogTarget !== 'function')
|
|
103
|
-
fail('远程主机连接组件没有提供模型目录');
|
|
104
|
-
const reply = api.getModelCatalogTarget(agent.loginProvider);
|
|
105
|
-
if (!reply.ok)
|
|
106
|
-
fail(reply.error.message);
|
|
107
|
-
const target = reply.value;
|
|
108
|
-
validateTarget(target.target);
|
|
109
109
|
const controller = new AbortController(), combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
let off;
|
|
111
|
+
this.pending.add(controller);
|
|
112
|
+
try {
|
|
113
|
+
combined.throwIfAborted();
|
|
114
|
+
const api = this.get();
|
|
115
|
+
if (!api || typeof api.getModelCatalogTarget !== 'function')
|
|
116
|
+
fail('远程主机连接组件尚未就绪,请等待连接恢复后重新打开模型菜单。');
|
|
117
|
+
const reply = api.getModelCatalogTarget(agent.loginProvider);
|
|
118
|
+
if (!reply.ok)
|
|
119
|
+
fail(reply.error.message);
|
|
120
|
+
const target = reply.value;
|
|
121
|
+
validateTarget(target.target);
|
|
122
|
+
off = api.onConnectionState?.(() => {
|
|
123
|
+
const state = api.getConnectionState();
|
|
124
|
+
if (state.phase !== 'connected' || state.instanceId !== target.instanceId || state.epoch !== target.epoch)
|
|
125
|
+
controller.abort(new CatalogError('连接已变化,本次模型列表读取已停止;连接恢复后请重新打开模型菜单。', 'MODEL_CATALOG_CONNECTION_CHANGED'));
|
|
126
|
+
});
|
|
127
|
+
const max = this.maxBytes();
|
|
128
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
129
|
+
fail('模型目录容量设置无效,请联系管理员检查插件配置。');
|
|
130
|
+
const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, combined, max);
|
|
131
|
+
combined.throwIfAborted();
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = parseHostCatalog(output, agent.loginProvider, target.target.username);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error instanceof CatalogError)
|
|
138
|
+
throw new CatalogError(error.message.slice('无法获取模型列表:'.length), error.diagnostic + '\n主机响应:' + safe(output));
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
if (parsed.issues.length)
|
|
142
|
+
console.warn('[remote-agent] 模型菜单部分条目无效:' + parsed.issues.map(issue => issue.code).join(','));
|
|
143
|
+
return { target, models: parsed.models, signal: combined, release() { } };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const failure = signal?.aborted ? signal.reason : error instanceof CatalogError || error instanceof ModelConfigurationError ? error : new CatalogError('暂时无法读取模型列表,请检查远程主机连接后重新打开模型菜单。', safe(error instanceof Error ? error.message : String(error)));
|
|
147
|
+
try {
|
|
148
|
+
this.reportFailure?.(agent.loginProvider, failure);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
console.warn('[remote-agent] 模型列表失败日志未保存');
|
|
152
|
+
}
|
|
153
|
+
throw failure;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
off?.();
|
|
157
|
+
this.pending.delete(controller);
|
|
158
|
+
}
|
|
118
159
|
}
|
|
119
|
-
dispose() { }
|
|
160
|
+
dispose() { this.cancelPending(); }
|
|
120
161
|
}
|
|
121
162
|
export function modelInfo(provider, model) {
|
|
122
163
|
// Official rc.2 represents no reasoning levels by absence, and rejects an empty efforts array.
|
package/dist/remote-details.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export declare function createRemoteDetails(options: {
|
|
|
50
50
|
getApi?: () => unknown;
|
|
51
51
|
warn?: (line: string) => void;
|
|
52
52
|
}): {
|
|
53
|
+
/** Menu failures have no Session; keep them in the same private local logs. */
|
|
54
|
+
recordCatalogFailure(provider: "claude" | "codex", error: unknown): void;
|
|
53
55
|
/** Await queued best-effort writes only when releasing the profile, never before dispatch. */
|
|
54
56
|
dispose(): Promise<void>;
|
|
55
57
|
releaseSession(sessionId: string): void;
|
package/dist/remote-details.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { catalogSafeText } from "./catalog-ssh.js";
|
|
1
2
|
import { closeCode } from "./close-diagnostics.js";
|
|
2
3
|
import { randomUUID, createHash } from 'node:crypto';
|
|
3
4
|
import { constants } from 'node:fs';
|
|
@@ -74,6 +75,14 @@ export function createRemoteDetails(options) {
|
|
|
74
75
|
return { eventId };
|
|
75
76
|
}
|
|
76
77
|
return {
|
|
78
|
+
/** Menu failures have no Session; keep them in the same private local logs. */
|
|
79
|
+
recordCatalogFailure(provider, error) {
|
|
80
|
+
const reason = error instanceof Error ? error.message : typeof error === 'string' ? error : '模型目录查询失败';
|
|
81
|
+
const diagnostic = error && typeof error === 'object' && 'diagnostic' in error && typeof error.diagnostic === 'string' ? error.diagnostic : reason;
|
|
82
|
+
const line = { version: 1, at: new Date().toISOString(), provider, code: 'MODEL_CATALOG_UNAVAILABLE', message: catalogSafeText(reason), diagnostic: catalogSafeText(diagnostic) };
|
|
83
|
+
writes = writes.then(async () => { if (!root)
|
|
84
|
+
throw failure('DIAGNOSTIC_CONTEXT_UNAVAILABLE'); await privateDirectory(join(root, '..')); await privateDirectory(root); await appendFile(join(root, 'model-catalog.jsonl'), JSON.stringify(line) + '\n', { mode: 0o600, flag: constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW }); }).catch(warn);
|
|
85
|
+
},
|
|
77
86
|
/** Await queued best-effort writes only when releasing the profile, never before dispatch. */
|
|
78
87
|
async dispose() { await writes; memory.clear(); },
|
|
79
88
|
releaseSession(sessionId) { for (const provider of ['claude', 'codex'])
|