@ai-workbench/remote-agent 0.8.10 → 0.8.12
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 +29 -6
- package/dist/acp.js +1 -1
- package/dist/catalog-diagnostics.d.ts +16 -0
- package/dist/catalog-diagnostics.js +108 -0
- package/dist/catalog-ssh.d.ts +51 -0
- package/dist/catalog-ssh.js +165 -0
- package/dist/client.js +41 -1
- package/dist/employee-mcp.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +133 -27
- package/dist/model-catalog.d.ts +24 -5
- package/dist/model-catalog.js +174 -132
- package/dist/model-defaults.d.ts +29 -0
- package/dist/model-defaults.js +62 -0
- package/dist/model-input-retention.d.ts +22 -0
- package/dist/model-input-retention.js +137 -0
- package/dist/preflight-diagnostic.d.ts +22 -0
- package/dist/preflight-diagnostic.js +112 -0
- package/dist/preflight-payload.d.ts +11 -0
- package/dist/preflight-payload.js +11 -0
- package/dist/presentation.js +6 -0
- package/dist/provider.d.ts +5 -1
- package/dist/provider.js +48 -21
- package/dist/remote-details.d.ts +18 -0
- package/dist/remote-details.js +140 -125
- package/dist/workspace-source-confirmation.d.ts +4 -0
- package/dist/workspace-source-confirmation.js +12 -0
- package/dist/workspace-source.d.ts +25 -0
- package/dist/workspace-source.js +136 -0
- package/package.json +5 -5
package/dist/model-catalog.js
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
10
1
|
import { LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
11
2
|
import { sameTarget, validateTarget } from "./provider.js";
|
|
12
3
|
import { ModelConfigurationError } from "./model-configuration.js";
|
|
13
|
-
import {
|
|
4
|
+
import { runCatalog, CatalogSshError, catalogSafeText } from "./catalog-ssh.js";
|
|
5
|
+
export { runCatalog } from "./catalog-ssh.js";
|
|
14
6
|
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
15
7
|
const text = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
16
|
-
|
|
8
|
+
class CatalogError extends LlmError {
|
|
9
|
+
diagnostic;
|
|
10
|
+
constructor(reason, diagnostic) { super('无法获取模型列表:' + reason, 'MODEL_CATALOG_UNAVAILABLE'); this.diagnostic = diagnostic + '(诊断代码:MODEL_CATALOG_UNAVAILABLE)'; }
|
|
11
|
+
}
|
|
12
|
+
const safe = catalogSafeText;
|
|
13
|
+
function fail(reason, diagnostic = reason) { throw new CatalogError(reason, diagnostic); }
|
|
17
14
|
function freeze(value) { if (value && typeof value === 'object') {
|
|
18
15
|
for (const item of Object.values(value))
|
|
19
16
|
freeze(item);
|
|
20
17
|
Object.freeze(value);
|
|
21
18
|
} return value; }
|
|
19
|
+
export function catalogDiagnostic(issues) {
|
|
20
|
+
return issues.length ? '以下模型未加入可选列表:\n' + issues.map(issue => `模型「${issue.modelName}」:${issue.reason}(诊断代码:${issue.code})`).join('\n') : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Defaults are provider declarations, never catalog order or display names. */
|
|
23
|
+
export function declaredDefault(agent, models) {
|
|
24
|
+
const selected = models.filter(model => agent.loginProvider === 'claude' ? model.value === 'default' : object(model.source) && model.source.isDefault === true);
|
|
25
|
+
return selected.length === 1 ? selected[0] : undefined;
|
|
26
|
+
}
|
|
22
27
|
export function parseHostCatalog(input, provider, username) {
|
|
23
28
|
let wire;
|
|
24
29
|
try {
|
|
@@ -32,115 +37,69 @@ export function parseHostCatalog(input, provider, username) {
|
|
|
32
37
|
if (wire.ok === false) {
|
|
33
38
|
const error = object(wire.error) ? wire.error : undefined;
|
|
34
39
|
const code = typeof error?.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : 'PROTOCOL_INVALID';
|
|
35
|
-
const reason = text(error?.message) ?
|
|
36
|
-
fail('
|
|
40
|
+
const reason = text(error?.message) ? safe(error.message) : '主机未提供失败说明';
|
|
41
|
+
fail('主机目录查询失败,请到设置 → 模型查看诊断。', `主机目录查询失败(诊断代码:${code}):${reason}`);
|
|
37
42
|
}
|
|
38
43
|
if (wire.ok !== true || wire.permissionProbe !== false || !object(wire.subject) || wire.subject.username !== username || !Number.isSafeInteger(wire.subject.uid) || !text(wire.observedAt) || !object(wire.source) || !object(wire.settings) || !Array.isArray(wire.models))
|
|
39
44
|
fail('主机目录缺少完整元数据');
|
|
40
|
-
const ids = new
|
|
41
|
-
for (const model of wire.models)
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const ids = new Map(), values = new Map();
|
|
46
|
+
for (const model of wire.models)
|
|
47
|
+
if (object(model)) {
|
|
48
|
+
if (text(model.id))
|
|
49
|
+
ids.set(model.id, (ids.get(model.id) ?? 0) + 1);
|
|
50
|
+
if (text(model.value))
|
|
51
|
+
values.set(model.value, (values.get(model.value) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
const models = [], issues = [];
|
|
54
|
+
for (const [index, model] of wire.models.entries()) {
|
|
55
|
+
const modelName = object(model) && text(model.name) ? safe(model.name) : object(model) && text(model.id) ? safe(model.id) : `第 ${index + 1} 项(缺少模型名称)`;
|
|
56
|
+
const reject = (reason, code = 'MODEL_CATALOG_ITEM_INVALID') => issues.push({ modelName, reason, code });
|
|
57
|
+
if (!object(model) || !text(model.id) || !text(model.value) || !text(model.name) || (model.resolvedModel !== null && !text(model.resolvedModel)) || !object(model.source) || !object(model.reasoning)) {
|
|
58
|
+
reject('模型标识、名称或能力资料不完整');
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if ((ids.get(model.id) ?? 0) > 1 || (values.get(model.value) ?? 0) > 1) {
|
|
62
|
+
reject('模型标识或发送值重复,无法确定对应关系', 'MODEL_CATALOG_DUPLICATE');
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (provider === 'codex' && model.resolvedModel !== model.value) {
|
|
66
|
+
reject('具体型号与发送模型值不一致');
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
50
69
|
const r = model.reasoning;
|
|
51
|
-
if (!Array.isArray(r.options) || typeof r.defaultKnown !== 'boolean' || (r.defaultValue !== null && !text(r.defaultValue)) || (!r.defaultKnown && r.defaultValue !== null))
|
|
52
|
-
|
|
70
|
+
if (!Array.isArray(r.options) || typeof r.defaultKnown !== 'boolean' || (r.defaultValue !== null && !text(r.defaultValue)) || (!r.defaultKnown && r.defaultValue !== null)) {
|
|
71
|
+
reject('推理档位或默认档位资料无效', 'MODEL_REASONING_INVALID');
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
53
74
|
const efforts = new Set();
|
|
75
|
+
let invalid = false;
|
|
54
76
|
for (const effort of r.options) {
|
|
55
|
-
if (!object(effort) || !text(effort.value) || !text(effort.name) || efforts.has(effort.value))
|
|
56
|
-
|
|
77
|
+
if (!object(effort) || !text(effort.value) || !text(effort.name) || efforts.has(effort.value)) {
|
|
78
|
+
invalid = true;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
57
81
|
efforts.add(effort.value);
|
|
58
82
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
83
|
+
if (invalid) {
|
|
84
|
+
reject('推理档位缺少名称、值无效或重复', 'MODEL_REASONING_INVALID');
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (r.defaultKnown && (r.defaultValue === null || !efforts.has(r.defaultValue))) {
|
|
88
|
+
reject('默认推理档位不在该模型提供的选项中', 'MODEL_REASONING_DEFAULT_INVALID');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
models.push(model);
|
|
61
92
|
}
|
|
62
|
-
|
|
63
|
-
fail('主机返回的完整模型目录为空');
|
|
64
|
-
return freeze(structuredClone(wire.models));
|
|
93
|
+
return freeze(structuredClone({ models, issues }));
|
|
65
94
|
}
|
|
66
|
-
/**
|
|
67
|
-
export const runCatalog = async (target, command, signal, maxBytes) => {
|
|
68
|
-
let argv;
|
|
69
|
-
try {
|
|
70
|
-
const moduleName = '@ai-workbench/connect';
|
|
71
|
-
const l = await import(__rewriteRelativeImportExtension(moduleName));
|
|
72
|
-
if (typeof l.sshArgs !== 'function')
|
|
73
|
-
fail('远程主机连接组件缺少可信 SSH 能力');
|
|
74
|
-
argv = l.sshArgs(target);
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
if (error instanceof LlmError)
|
|
78
|
-
throw error;
|
|
79
|
-
fail('远程主机连接组件缺少可信 SSH 能力');
|
|
80
|
-
}
|
|
81
|
-
signal.throwIfAborted();
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
const child = spawn('ssh', ['-n', ...argv, command], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
84
|
-
const parts = [];
|
|
85
|
-
let total = 0, stderr = 0;
|
|
86
|
-
let failure;
|
|
87
|
-
const abort = () => { failure = new ModelConfigurationError('连接已改变或目录查询已取消'); child.kill('SIGTERM'); };
|
|
88
|
-
signal.addEventListener('abort', abort, { once: true });
|
|
89
|
-
const receive = (buffer, output) => { if (failure)
|
|
90
|
-
return; total += buffer.length; if (total > maxBytes) {
|
|
91
|
-
failure = new ModelConfigurationError('目录响应超过 acpMaxMessageBytes,未截断返回');
|
|
92
|
-
child.kill('SIGTERM');
|
|
93
|
-
return;
|
|
94
|
-
} if (output)
|
|
95
|
-
parts.push(buffer);
|
|
96
|
-
else
|
|
97
|
-
stderr += buffer.length; };
|
|
98
|
-
child.stdout.on('data', (b) => receive(b, true));
|
|
99
|
-
child.stderr.on('data', (b) => receive(b, false));
|
|
100
|
-
child.on('error', () => { failure = new ModelConfigurationError('可信 SSH 进程启动失败'); });
|
|
101
|
-
child.on('close', code => {
|
|
102
|
-
signal.removeEventListener('abort', abort);
|
|
103
|
-
if (failure) {
|
|
104
|
-
reject(failure);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
let output;
|
|
108
|
-
try {
|
|
109
|
-
output = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(parts));
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
reject(new ModelConfigurationError('主机目录响应不是有效 UTF-8,未替换模型值'));
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
// S publishes its typed failure JSON on stdout even on nonzero exit.
|
|
116
|
-
if (code !== 0) {
|
|
117
|
-
try {
|
|
118
|
-
const wire = JSON.parse(output);
|
|
119
|
-
if (wire?.ok === false) {
|
|
120
|
-
resolve(output);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch { }
|
|
125
|
-
reject(new ModelConfigurationError('可信 SSH 或主机目录命令失败(退出码 ' + String(code) + ')'));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (stderr) {
|
|
129
|
-
reject(new ModelConfigurationError('目录命令返回了非预期诊断输出'));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
resolve(output);
|
|
133
|
-
});
|
|
134
|
-
if (signal.aborted)
|
|
135
|
-
abort();
|
|
136
|
-
});
|
|
137
|
-
};
|
|
138
|
-
/** No cache: every query binds to L's current object, host-user and generation. */
|
|
95
|
+
/** Share only pending work; settled results are never cached. */
|
|
139
96
|
export class ModelCatalog {
|
|
140
97
|
get;
|
|
141
98
|
maxBytes;
|
|
142
99
|
run;
|
|
143
100
|
changed;
|
|
101
|
+
diagnostic;
|
|
102
|
+
loaded;
|
|
144
103
|
api;
|
|
145
104
|
unsubscribe;
|
|
146
105
|
signature;
|
|
@@ -148,11 +107,16 @@ export class ModelCatalog {
|
|
|
148
107
|
disposed = false;
|
|
149
108
|
initialized = false;
|
|
150
109
|
active = new Set();
|
|
151
|
-
|
|
110
|
+
flights = new Map();
|
|
111
|
+
serial = 0;
|
|
112
|
+
latest = new Map();
|
|
113
|
+
constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }) {
|
|
152
114
|
this.get = get;
|
|
153
115
|
this.maxBytes = maxBytes;
|
|
154
116
|
this.run = run;
|
|
155
117
|
this.changed = changed;
|
|
118
|
+
this.diagnostic = diagnostic;
|
|
119
|
+
this.loaded = loaded;
|
|
156
120
|
this.refresh();
|
|
157
121
|
}
|
|
158
122
|
refresh() {
|
|
@@ -160,11 +124,10 @@ export class ModelCatalog {
|
|
|
160
124
|
const state = api?.getConnectionState?.();
|
|
161
125
|
const info = api?.getContractInfo?.();
|
|
162
126
|
const targets = info?.modelCatalogTarget === 1 && api?.getModelCatalogTarget ? ['codex', 'claude'].map(brand => api.getModelCatalogTarget(brand)) : undefined;
|
|
163
|
-
const login = api?.getLoginPromptState?.();
|
|
164
127
|
// L's same-epoch recovery wording is presentation metadata, not an execution change.
|
|
165
128
|
const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase, needCode: state.need?.code };
|
|
166
129
|
const executionTargets = targets?.map(result => result.ok ? result : { ok: false, code: result.error.code });
|
|
167
|
-
const signature = JSON.stringify({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets
|
|
130
|
+
const signature = JSON.stringify({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets });
|
|
168
131
|
const switched = api !== this.api;
|
|
169
132
|
const changed = this.initialized && (switched || signature !== this.signature);
|
|
170
133
|
this.initialized = true;
|
|
@@ -183,9 +146,12 @@ export class ModelCatalog {
|
|
|
183
146
|
this.signature = signature;
|
|
184
147
|
if (changed) {
|
|
185
148
|
this.revision++;
|
|
149
|
+
this.latest.clear();
|
|
186
150
|
for (const controller of this.active)
|
|
187
151
|
controller.abort();
|
|
188
152
|
this.active.clear();
|
|
153
|
+
this.diagnostic('claude', undefined);
|
|
154
|
+
this.diagnostic('codex', undefined);
|
|
189
155
|
this.changed();
|
|
190
156
|
}
|
|
191
157
|
}
|
|
@@ -198,7 +164,7 @@ export class ModelCatalog {
|
|
|
198
164
|
fail('当前没有已验证的远程主机连接');
|
|
199
165
|
const result = api.getModelCatalogTarget(agent.loginProvider);
|
|
200
166
|
if (!result.ok)
|
|
201
|
-
fail(result.error.message);
|
|
167
|
+
fail('远程主机连接暂未提供可信的模型目录目标,请到设置 → 模型查看诊断。', `模型目录目标不可用(诊断代码:${safe(result.error.code)}):${safe(result.error.message)}`);
|
|
202
168
|
const target = result.value;
|
|
203
169
|
if (target.version !== 1 || target.purpose !== 'model-catalog' || target.scope !== 'host-user' || target.provider !== agent.loginProvider || target.instanceId !== state.instanceId || target.epoch !== state.epoch)
|
|
204
170
|
fail('模型目录目标与当前连接或品牌不匹配');
|
|
@@ -210,45 +176,121 @@ export class ModelCatalog {
|
|
|
210
176
|
}
|
|
211
177
|
return structuredClone(target);
|
|
212
178
|
}
|
|
179
|
+
assertFlight(flight) {
|
|
180
|
+
if (flight.controller.signal.aborted || this.disposed || this.get() !== flight.api || flight.revision !== this.revision)
|
|
181
|
+
fail('连接已改变,原模型目录已失效');
|
|
182
|
+
const agent = { loginProvider: flight.target.provider }, current = this.trusted(agent, flight.api);
|
|
183
|
+
if (current.instanceId !== flight.target.instanceId || current.epoch !== flight.target.epoch || current.provider !== flight.target.provider || !sameTarget(current.target, flight.target.target))
|
|
184
|
+
fail('可信目标或连接代次已改变,原模型目录已失效');
|
|
185
|
+
}
|
|
186
|
+
failure(error) {
|
|
187
|
+
// Each consumer needs its own Error identity for its original input/signal proof.
|
|
188
|
+
return new CatalogError(error instanceof CatalogError || error instanceof ModelConfigurationError ? error.message.replace(/^无法获取模型列表:/, '') : '目录查询失败,请到设置 → 模型查看诊断。', error instanceof CatalogError ? error.diagnostic : error instanceof CatalogSshError ? error.diagnostic : error instanceof Error ? `${error instanceof LlmError ? error.code : 'MODEL_CATALOG_QUERY_FAILED'}:${safe(error.message)}` : 'MODEL_CATALOG_QUERY_FAILED:目录查询失败,根因未查明');
|
|
189
|
+
}
|
|
190
|
+
start(agent, api, target, max, key) {
|
|
191
|
+
const deferred = Promise.withResolvers(), controller = new AbortController();
|
|
192
|
+
const flight = { key, api, target, revision: this.revision, serial: ++this.serial, controller, pending: true, users: 0, promise: deferred.promise };
|
|
193
|
+
this.latest.set(agent.loginProvider, flight.serial);
|
|
194
|
+
this.flights.set(key, flight);
|
|
195
|
+
this.active.add(controller);
|
|
196
|
+
const current = () => !this.disposed && flight.revision === this.revision && api === this.get() && this.latest.get(agent.loginProvider) === flight.serial && !controller.signal.aborted;
|
|
197
|
+
const query = async () => {
|
|
198
|
+
try {
|
|
199
|
+
this.assertFlight(flight);
|
|
200
|
+
const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, controller.signal, max);
|
|
201
|
+
this.assertFlight(flight);
|
|
202
|
+
const parsed = parseHostCatalog(output, agent.loginProvider, target.target.username), diagnostic = catalogDiagnostic(parsed.issues);
|
|
203
|
+
if (!parsed.models.length)
|
|
204
|
+
fail('当前没有可选模型,请到设置 → 模型查看诊断。', diagnostic ?? '主机返回的完整模型目录为空(诊断代码:MODEL_CATALOG_EMPTY)');
|
|
205
|
+
if (current())
|
|
206
|
+
this.diagnostic(agent.loginProvider, diagnostic);
|
|
207
|
+
if (current())
|
|
208
|
+
await this.loaded(agent, parsed.models, () => this.assertFlight(flight));
|
|
209
|
+
this.assertFlight(flight);
|
|
210
|
+
return parsed.models;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
const failure = this.failure(error);
|
|
214
|
+
if (current())
|
|
215
|
+
this.diagnostic(agent.loginProvider, failure.diagnostic);
|
|
216
|
+
throw failure;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const settled = () => { flight.pending = false; if (this.flights.get(key) === flight)
|
|
220
|
+
this.flights.delete(key); if (!flight.users)
|
|
221
|
+
this.active.delete(controller); };
|
|
222
|
+
void query().then(models => { settled(); deferred.resolve(models); }, error => { settled(); deferred.reject(error); });
|
|
223
|
+
return flight;
|
|
224
|
+
}
|
|
213
225
|
async read(agent, signal) {
|
|
214
|
-
let
|
|
226
|
+
let flight, released = false;
|
|
227
|
+
const release = () => { if (released || !flight)
|
|
228
|
+
return; released = true; flight.users--; if (!flight.users) {
|
|
229
|
+
if (flight.pending)
|
|
230
|
+
flight.controller.abort();
|
|
231
|
+
else
|
|
232
|
+
this.active.delete(flight.controller);
|
|
233
|
+
} };
|
|
215
234
|
try {
|
|
216
235
|
if (this.disposed)
|
|
217
236
|
fail('远程模型插件已关闭');
|
|
237
|
+
signal?.throwIfAborted();
|
|
218
238
|
this.refresh();
|
|
219
239
|
const api = this.get();
|
|
220
240
|
if (!api)
|
|
221
241
|
fail('远程主机连接组件没有装好');
|
|
222
|
-
const target = freeze(this.trusted(agent, api)),
|
|
223
|
-
const max = this.maxBytes();
|
|
242
|
+
const target = freeze(this.trusted(agent, api)), max = this.maxBytes();
|
|
224
243
|
if (!Number.isSafeInteger(max) || max < 1)
|
|
225
244
|
fail('acpMaxMessageBytes 设置无效');
|
|
226
|
-
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
245
|
+
const key = JSON.stringify([agent.loginProvider, target.instanceId, target.epoch]);
|
|
246
|
+
flight = this.flights.get(key);
|
|
247
|
+
if (flight && (flight.api !== api || flight.revision !== this.revision || !sameTarget(flight.target.target, target.target) || flight.controller.signal.aborted)) {
|
|
248
|
+
const closing = flight;
|
|
249
|
+
closing.controller.abort();
|
|
250
|
+
flight = undefined;
|
|
251
|
+
// Closing work owns the tuple until SSH/runner actually settles. This new
|
|
252
|
+
// caller waits for cleanup, then begins its own query; it never uses old data.
|
|
253
|
+
const waiting = new AbortController(), revision = this.revision;
|
|
254
|
+
this.active.add(waiting);
|
|
255
|
+
const waitSignal = signal ? AbortSignal.any([signal, waiting.signal]) : waiting.signal;
|
|
256
|
+
try {
|
|
257
|
+
await new Promise((resolve, reject) => { const abort = () => { waitSignal.removeEventListener('abort', abort); reject(new ModelConfigurationError('目录查询已取消')); }; waitSignal.addEventListener('abort', abort, { once: true }); closing.promise.then(() => { }, () => { }).then(() => { waitSignal.removeEventListener('abort', abort); resolve(); }); if (waitSignal.aborted)
|
|
258
|
+
abort(); });
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
this.active.delete(waiting);
|
|
262
|
+
}
|
|
263
|
+
if (revision !== this.revision || this.get() !== api)
|
|
264
|
+
fail('连接已改变,原模型目录已失效');
|
|
265
|
+
return this.read(agent, signal);
|
|
266
|
+
}
|
|
267
|
+
flight ??= this.start(agent, api, target, max, key);
|
|
268
|
+
flight.users++;
|
|
269
|
+
const own = flight, combined = signal ? AbortSignal.any([signal, own.controller.signal]) : own.controller.signal;
|
|
270
|
+
const assertCurrent = () => { if (combined.aborted)
|
|
271
|
+
fail('目录查询已取消'); this.assertFlight(own); };
|
|
272
|
+
const models = await new Promise((resolve, reject) => {
|
|
273
|
+
const abort = () => { combined.removeEventListener('abort', abort); reject(new ModelConfigurationError('目录查询已取消')); };
|
|
274
|
+
combined.addEventListener('abort', abort, { once: true });
|
|
275
|
+
own.promise.then(value => { combined.removeEventListener('abort', abort); resolve(value); }, error => { combined.removeEventListener('abort', abort); reject(error); });
|
|
276
|
+
if (combined.aborted)
|
|
277
|
+
abort();
|
|
278
|
+
});
|
|
234
279
|
assertCurrent();
|
|
235
|
-
|
|
236
|
-
const own = controller;
|
|
237
|
-
return { target, models, signal: combined, assertCurrent, release: () => this.active.delete(own) };
|
|
280
|
+
return { target: own.target, models, signal: combined, assertCurrent, release };
|
|
238
281
|
}
|
|
239
282
|
catch (error) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
fail(error.message.replace(/^无法获取模型列表:/, ''));
|
|
246
|
-
fail('目录查询失败,根因未查明');
|
|
283
|
+
release();
|
|
284
|
+
const failure = this.failure(error);
|
|
285
|
+
if (!flight && !this.disposed)
|
|
286
|
+
this.diagnostic(agent.loginProvider, failure.diagnostic);
|
|
287
|
+
throw failure;
|
|
247
288
|
}
|
|
248
289
|
}
|
|
249
|
-
dispose() { this.disposed = true; this.unsubscribe?.(); for (const controller of this.active)
|
|
290
|
+
dispose() { this.disposed = true; this.unsubscribe?.(); this.flights.clear(); for (const controller of this.active)
|
|
250
291
|
controller.abort(); this.active.clear(); }
|
|
251
292
|
}
|
|
252
293
|
export function modelInfo(provider, model) {
|
|
253
|
-
|
|
294
|
+
// Official rc.2 represents no reasoning levels by absence, and rejects an empty efforts array.
|
|
295
|
+
return { provider, id: model.id, name: model.name, inputModalities: ['text', 'image'], ...(model.reasoning.options.length ? { reasoning: { efforts: model.reasoning.options.map(option => ({ id: ReasoningEffortId(option.value), name: option.name })), ...(model.reasoning.defaultKnown && model.reasoning.defaultValue !== null ? { defaultEffort: ReasoningEffortId(model.reasoning.defaultValue) } : {}) } } : {}), catalog: model };
|
|
254
296
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentDefinition } from './agents.ts';
|
|
2
|
+
import { type HostModel } from './model-catalog.ts';
|
|
3
|
+
export interface DefaultSelection {
|
|
4
|
+
provider: string;
|
|
5
|
+
model: string;
|
|
6
|
+
reasoningEffort?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Public agentDefaultModel service; writes below use the atomic config editor. */
|
|
9
|
+
export interface DefaultModelService {
|
|
10
|
+
currentSelection(): DefaultSelection;
|
|
11
|
+
}
|
|
12
|
+
interface DefaultEntry {
|
|
13
|
+
options: {
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Public rc.2 configEditor.entries/edit subset; no ownerContext/private config access. */
|
|
18
|
+
export interface DefaultConfigEditor {
|
|
19
|
+
entries(): readonly DefaultEntry[];
|
|
20
|
+
edit(entry: DefaultEntry, change: (current: Record<string, unknown>, inherited: Record<string, unknown>) => Record<string, unknown>): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export type DefaultSelectionWriter = (expected: DefaultSelection, next: DefaultSelection, assertCurrent: () => void) => Promise<void>;
|
|
23
|
+
/** The editor invokes the guard synchronously under its profile lock after reconciliation. */
|
|
24
|
+
export declare function createDefaultSelectionWriter(service: DefaultModelService, getEditor: () => DefaultConfigEditor | undefined): DefaultSelectionWriter;
|
|
25
|
+
export declare function createModelDefaults(service: DefaultModelService, report: (provider: string, message: string | undefined) => void, write?: DefaultSelectionWriter): {
|
|
26
|
+
observe(agent: AgentDefinition, models: readonly HostModel[], assertCurrent?: () => void): Promise<void>;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { declaredDefault } from "./model-catalog.js";
|
|
2
|
+
const same = (a, b) => a.provider === b.provider && a.model === b.model && a.reasoningEffort === b.reasoningEffort;
|
|
3
|
+
/** The editor invokes the guard synchronously under its profile lock after reconciliation. */
|
|
4
|
+
export function createDefaultSelectionWriter(service, getEditor) {
|
|
5
|
+
return async (expected, next, assertCurrent) => {
|
|
6
|
+
const editor = getEditor();
|
|
7
|
+
const entries = editor?.entries().filter(entry => entry.options.name === '@deepseek-ai/dsh-agent-default-model');
|
|
8
|
+
if (!editor || entries?.length !== 1)
|
|
9
|
+
throw Error('缺少唯一可原子编辑的官方默认模型设置');
|
|
10
|
+
await editor.edit(entries[0], current => {
|
|
11
|
+
assertCurrent();
|
|
12
|
+
if (!same(expected, service.currentSelection()))
|
|
13
|
+
throw Error('员工已改变模型选择,本次默认更新已撤回');
|
|
14
|
+
const updated = { ...current, provider: next.provider, model: next.model };
|
|
15
|
+
Reflect.deleteProperty(updated, 'reasoningEffort');
|
|
16
|
+
if (next.reasoningEffort !== undefined)
|
|
17
|
+
Object.assign(updated, { reasoningEffort: next.reasoningEffort });
|
|
18
|
+
return updated;
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createModelDefaults(service, report, write) {
|
|
23
|
+
let disposed = false, queue = Promise.resolve();
|
|
24
|
+
return {
|
|
25
|
+
observe(agent, models, assertCurrent = () => { }) {
|
|
26
|
+
const snapshot = { ...service.currentSelection() };
|
|
27
|
+
const job = queue.then(async () => {
|
|
28
|
+
if (disposed || snapshot.provider !== agent.id || !same(snapshot, service.currentSelection()))
|
|
29
|
+
return;
|
|
30
|
+
if (snapshot.model !== 'remote' && models.some(model => model.id === snapshot.model)) {
|
|
31
|
+
report(agent.id, undefined);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const model = declaredDefault(agent, models);
|
|
35
|
+
const notice = model ? undefined : '提供方没有声明唯一的默认模型,请先选择模型。(诊断代码:MODEL_DEFAULT_UNAVAILABLE)';
|
|
36
|
+
const next = { provider: agent.id, model: model?.id ?? '', ...(model?.reasoning.defaultKnown && model.reasoning.defaultValue !== null ? { reasoningEffort: model.reasoning.defaultValue } : {}) };
|
|
37
|
+
try {
|
|
38
|
+
const guard = () => { assertCurrent(); if (disposed)
|
|
39
|
+
throw Error('默认设置消费已撤回'); };
|
|
40
|
+
guard();
|
|
41
|
+
if (!same(snapshot, next)) {
|
|
42
|
+
if (!write)
|
|
43
|
+
throw Error('缺少原子配置能力');
|
|
44
|
+
await write(snapshot, next, guard);
|
|
45
|
+
}
|
|
46
|
+
if (disposed)
|
|
47
|
+
return;
|
|
48
|
+
if (!same(next, service.currentSelection()))
|
|
49
|
+
throw Error('未确认官方默认模型设置已保存');
|
|
50
|
+
report(agent.id, notice);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
if (!disposed)
|
|
54
|
+
report(agent.id, (notice ? notice + '\n' : '') + '未能确认默认模型设置已更新,员工新选择不会被迟到的自动更新覆盖;请在模型菜单选择当前目录中的模型。(诊断代码:MODEL_DEFAULT_SAVE_FAILED)');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
queue = job;
|
|
58
|
+
return job;
|
|
59
|
+
},
|
|
60
|
+
dispose() { disposed = true; },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
export interface ModelInputRetentionOptions {
|
|
3
|
+
isOurProvider: (provider: string) => boolean;
|
|
4
|
+
diagnostic: (provider: string, message: string | undefined) => void;
|
|
5
|
+
diagnosticForAttempt?: (position: {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
turn: number;
|
|
8
|
+
step: number;
|
|
9
|
+
}) => ((provider: string, code: string) => Promise<void>);
|
|
10
|
+
preflightDiagnostic?: (provider: string, message: string | undefined) => void;
|
|
11
|
+
}
|
|
12
|
+
/** Mark only at the actual X adapter's preparation catch, then rethrow the same
|
|
13
|
+
* error. One error object can prove only its first preparation, for that signal.
|
|
14
|
+
* Public request config is an intermediate proposal and is not route evidence. */
|
|
15
|
+
export declare function markModelPreparationFailure(provider: string, error: unknown, signal?: AbortSignal): void;
|
|
16
|
+
/**
|
|
17
|
+
* Retain a claimed human batch only after a known, undispatched metadata prepare
|
|
18
|
+
* failure. Restoration uses the public durable inbox without sending or waking
|
|
19
|
+
* the agent. This cannot cover errors before pre-step/request, or unknown core
|
|
20
|
+
* admission failures; it never manufactures transcript messages or model routes.
|
|
21
|
+
*/
|
|
22
|
+
export declare function installModelInputRetention(ctx: Context, options: ModelInputRetentionOptions): void;
|