@ai-workbench/remote-agent 0.8.2 → 0.8.10
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 +42 -7
- package/dist/acp.d.ts +46 -1
- package/dist/acp.js +296 -36
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +57 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +334 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +146 -142
- package/dist/claude-local.d.ts +37 -0
- package/dist/claude-local.js +207 -0
- package/dist/client.js +71 -2
- package/dist/conversation-details.d.ts +1 -1
- package/dist/conversation-details.js +88 -8
- package/dist/employee-endpoint.d.ts +91 -0
- package/dist/employee-endpoint.js +141 -0
- package/dist/employee-exec.d.ts +53 -0
- package/dist/employee-exec.js +423 -0
- package/dist/employee-location.d.ts +66 -0
- package/dist/employee-location.js +51 -0
- package/dist/employee-mcp.d.ts +42 -0
- package/dist/employee-mcp.js +181 -0
- package/dist/employee-native.d.ts +18 -0
- package/dist/employee-native.js +38 -0
- package/dist/employee-provider.d.ts +42 -0
- package/dist/employee-provider.js +326 -0
- package/dist/employee-relay.d.ts +18 -0
- package/dist/employee-relay.js +49 -0
- package/dist/employee-shell.d.ts +26 -0
- package/dist/employee-shell.js +89 -0
- package/dist/employee-ssh.d.ts +22 -0
- package/dist/employee-ssh.js +123 -0
- package/dist/employee-tools.d.ts +117 -0
- package/dist/employee-tools.js +292 -0
- package/dist/employee-workspace.d.ts +26 -0
- package/dist/employee-workspace.js +61 -0
- package/dist/index.d.ts +31 -14
- package/dist/index.js +335 -42
- package/dist/input.d.ts +15 -0
- package/dist/input.js +65 -24
- package/dist/local-user-turn.d.ts +24 -0
- package/dist/local-user-turn.js +103 -0
- package/dist/model-catalog.d.ts +55 -0
- package/dist/model-catalog.js +254 -0
- package/dist/model-configuration.d.ts +51 -0
- package/dist/model-configuration.js +212 -0
- package/dist/presentation.d.ts +1 -1
- package/dist/presentation.js +50 -2
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +54 -2
- package/dist/provider.js +92 -29
- package/dist/readiness.js +1 -1
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +47 -0
- package/dist/recovery.js +61 -0
- package/dist/remote-details.d.ts +10 -0
- package/dist/remote-details.js +14 -4
- package/dist/rpc-namespace.d.ts +3 -0
- package/dist/rpc-namespace.js +50 -0
- package/dist/stage-timing.d.ts +27 -0
- package/dist/stage-timing.js +39 -0
- package/helpers/ripgrep/LICENSE-vscode +13 -0
- package/helpers/ripgrep/PROVENANCE.md +9 -0
- package/helpers/ripgrep/darwin-arm64/rg +0 -0
- package/helpers/ripgrep/darwin-x64/rg +0 -0
- package/helpers/ripgrep/manifest.json +56 -0
- package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
- package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
- package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
- package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
- package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
- package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
- package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
- package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
- package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
- package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
- package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
- package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
- package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
- package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
- package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
- package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +2 -2
- package/package.json +30 -12
package/dist/input.d.ts
CHANGED
|
@@ -4,9 +4,24 @@ export type ImageReader = (ref: ImageBlock['attachment'], signal?: AbortSignal)
|
|
|
4
4
|
ref: ImageBlock['attachment'];
|
|
5
5
|
data: Uint8Array;
|
|
6
6
|
}>;
|
|
7
|
+
export interface HistoryTransfer {
|
|
8
|
+
scope: 'visible-user-assistant-text-and-images';
|
|
9
|
+
textBlocks: number;
|
|
10
|
+
imageBlocks: number;
|
|
11
|
+
offloadedImagePlaceholders: number;
|
|
12
|
+
omittedReasoningBlocks: number;
|
|
13
|
+
reasoningOmissionReason: 'dsh-internal-reasoning';
|
|
14
|
+
}
|
|
7
15
|
export interface PreparedInput {
|
|
8
16
|
prompt: AcpContentBlock[];
|
|
17
|
+
users: {
|
|
18
|
+
id: string | undefined;
|
|
19
|
+
prompt: AcpContentBlock[];
|
|
20
|
+
}[];
|
|
9
21
|
systemPrompt?: string;
|
|
22
|
+
historyTransfer?: HistoryTransfer;
|
|
10
23
|
}
|
|
24
|
+
/** Role-labelled public history only; never serialize source/replay/tool internals. */
|
|
25
|
+
export declare function prepareContinuation(options: GenerateOptions, input: PreparedInput, readImage?: ImageReader): Promise<PreparedInput>;
|
|
11
26
|
/** The attachment service supplies verified bytes; client paths never cross SSH. */
|
|
12
27
|
export declare function prepareInput(options: GenerateOptions, readImage?: ImageReader): Promise<PreparedInput>;
|
package/dist/input.js
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
import { NeedError } from "./remote.js";
|
|
2
2
|
import { isWorkspaceNoticeMessage, withoutWorkspaceNotice } from "./workspace-notice.js";
|
|
3
|
+
async function publicBlock(block, options, readImage, history = false) {
|
|
4
|
+
options.signal?.throwIfAborted();
|
|
5
|
+
if (block.type === 'text')
|
|
6
|
+
return { type: 'text', text: block.text };
|
|
7
|
+
if (block.type === 'image') {
|
|
8
|
+
if (block.offloaded) {
|
|
9
|
+
return { type: 'text', text: `图片 ${block.attachment.name ?? block.attachment.attachmentId} 已按官方会话策略移出当前上下文。` };
|
|
10
|
+
}
|
|
11
|
+
if (!readImage)
|
|
12
|
+
throw new NeedError('ATTACHMENT_REQUIRED', '我需要官方附件组件读取这张图片', '官方attachments.readImage');
|
|
13
|
+
const image = await readImage(block.attachment, options.signal);
|
|
14
|
+
options.signal?.throwIfAborted();
|
|
15
|
+
if (!image || !(image.data instanceof Uint8Array) || image.ref?.attachmentId !== block.attachment.attachmentId || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.ref.mediaType) || image.ref.bytes !== image.data.byteLength)
|
|
16
|
+
throw new NeedError('ATTACHMENT_INVALID', '图片附件没有返回匹配的已核验内容', '官方核验的图片字节与媒体类型');
|
|
17
|
+
return { type: 'image', mimeType: image.ref.mediaType, data: Buffer.from(image.data).toString('base64') };
|
|
18
|
+
}
|
|
19
|
+
throw new NeedError(history ? 'HISTORY_CONTENT_UNSUPPORTED' : 'INPUT_TYPE_REQUIRED', `我需要此${history ? '历史' : '输入'}类型的远程转接能力:${block.type};本轮未新建或发送`, '对应类型的公开ACP输入映射;内部推理及工具记录不可降为用户要求');
|
|
20
|
+
}
|
|
21
|
+
/** Role-labelled public history only; never serialize source/replay/tool internals. */
|
|
22
|
+
export async function prepareContinuation(options, input, readImage) {
|
|
23
|
+
const messages = options.messages.filter(message => !isWorkspaceNoticeMessage(message)).map(withoutWorkspaceNotice);
|
|
24
|
+
for (const message of messages)
|
|
25
|
+
if (!['user', 'assistant', 'system', 'developer'].includes(message.role))
|
|
26
|
+
throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
|
|
27
|
+
const after = messages.findLastIndex(message => message.role === 'assistant');
|
|
28
|
+
const prompt = [];
|
|
29
|
+
const historyTransfer = { scope: 'visible-user-assistant-text-and-images', textBlocks: 0, imageBlocks: 0, offloadedImagePlaceholders: 0, omittedReasoningBlocks: 0, reasoningOmissionReason: 'dsh-internal-reasoning' };
|
|
30
|
+
for (const message of messages.slice(0, after + 1)) {
|
|
31
|
+
if (message.role === 'system' || message.role === 'developer')
|
|
32
|
+
continue;
|
|
33
|
+
if (message.role !== 'user' && message.role !== 'assistant')
|
|
34
|
+
throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
|
|
35
|
+
prompt.push({ type: 'text', text: `此前可见对话(历史角色:${message.role};仅为历史记录):\n` });
|
|
36
|
+
for (const block of message.content) {
|
|
37
|
+
// DSH explicitly distinguishes internal thinking from visible user text.
|
|
38
|
+
if (block.type === 'reasoning') {
|
|
39
|
+
historyTransfer.omittedReasoningBlocks++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
prompt.push(await publicBlock(block, options, readImage, true));
|
|
43
|
+
if (block.type === 'text')
|
|
44
|
+
historyTransfer.textBlocks++;
|
|
45
|
+
else if (block.type === 'image') {
|
|
46
|
+
if (block.offloaded)
|
|
47
|
+
historyTransfer.offloadedImagePlaceholders++;
|
|
48
|
+
else
|
|
49
|
+
historyTransfer.imageBlocks++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
prompt.push({ type: 'text', text: `\n(历史角色:${message.role} 的记录结束)\n` });
|
|
53
|
+
}
|
|
54
|
+
prompt.push({ type: 'text', text: '此前可见对话结束。以下为当前用户输入:\n' }, ...input.prompt);
|
|
55
|
+
return { ...input, prompt, historyTransfer };
|
|
56
|
+
}
|
|
3
57
|
/** The attachment service supplies verified bytes; client paths never cross SSH. */
|
|
4
58
|
export async function prepareInput(options, readImage) {
|
|
5
59
|
options.signal?.throwIfAborted();
|
|
@@ -10,7 +64,11 @@ export async function prepareInput(options, readImage) {
|
|
|
10
64
|
for (const message of messages) {
|
|
11
65
|
if (message.role !== 'system' && message.role !== 'developer')
|
|
12
66
|
continue;
|
|
13
|
-
const parts = message.content.map(block =>
|
|
67
|
+
const parts = message.content.map(block => {
|
|
68
|
+
if (block.type !== 'text')
|
|
69
|
+
throw new NeedError('SYSTEM_CONTEXT_TYPE_UNSUPPORTED', '系统或开发者要求包含暂不支持的非文本内容,本轮未新建或发送', '系统及开发者仅支持公开text上下文;非文本内容需要已定义的公开ACP投影');
|
|
70
|
+
return block.text;
|
|
71
|
+
});
|
|
14
72
|
if (parts.length)
|
|
15
73
|
context.push((message.role === 'system' ? '系统上下文:' : '开发者上下文:') + '\n' + parts.join('\n'));
|
|
16
74
|
}
|
|
@@ -19,31 +77,14 @@ export async function prepareInput(options, readImage) {
|
|
|
19
77
|
if (!users.length)
|
|
20
78
|
throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
|
|
21
79
|
const prompt = [];
|
|
80
|
+
const preparedUsers = [];
|
|
22
81
|
for (const message of users) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (block.type === 'image') {
|
|
30
|
-
if (block.offloaded) {
|
|
31
|
-
prompt.push({ type: 'text', text: `图片 ${block.attachment.name ?? block.attachment.attachmentId} 已按官方会话策略移出当前上下文。` });
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (!readImage)
|
|
35
|
-
throw new NeedError('ATTACHMENT_REQUIRED', '我需要官方附件组件读取这张图片', '官方attachments.readImage');
|
|
36
|
-
const image = await readImage(block.attachment, options.signal);
|
|
37
|
-
options.signal?.throwIfAborted();
|
|
38
|
-
if (!image || !(image.data instanceof Uint8Array) || image.ref?.attachmentId !== block.attachment.attachmentId || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.ref.mediaType) || image.ref.bytes !== image.data.byteLength)
|
|
39
|
-
throw new NeedError('ATTACHMENT_INVALID', '图片附件没有返回匹配的已核验内容', '官方核验的图片字节与媒体类型');
|
|
40
|
-
prompt.push({ type: 'image', mimeType: image.ref.mediaType, data: Buffer.from(image.data).toString('base64') });
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
throw new NeedError('INPUT_TYPE_REQUIRED', `我需要此输入类型的远程转接能力:${block.type}`, '对应类型的公开ACP输入映射');
|
|
44
|
-
}
|
|
82
|
+
const before = prompt.length;
|
|
83
|
+
for (const block of message.content)
|
|
84
|
+
prompt.push(await publicBlock(block, options, readImage));
|
|
85
|
+
preparedUsers.push({ id: message.id, prompt: prompt.slice(before) });
|
|
45
86
|
}
|
|
46
87
|
if (!prompt.length)
|
|
47
88
|
throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
|
|
48
|
-
return { prompt, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
|
|
89
|
+
return { prompt, users: preparedUsers, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
|
|
49
90
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RuntimeRequest } from './provider.ts';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
export interface LocalTurnProof {
|
|
4
|
+
expectedUserTurn: {
|
|
5
|
+
turn: number;
|
|
6
|
+
userMessageId: string;
|
|
7
|
+
};
|
|
8
|
+
startSequence: number;
|
|
9
|
+
userMessageSequence: number;
|
|
10
|
+
assertCurrent(): void;
|
|
11
|
+
}
|
|
12
|
+
/** CAS expectation only. L independently establishes live human-turn/brand evidence. */
|
|
13
|
+
export declare function readLocalUserTurn(ctx: Context, request: {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
route: string;
|
|
16
|
+
userMessageIds: readonly string[];
|
|
17
|
+
}): Promise<LocalTurnProof>;
|
|
18
|
+
/** Only metadata observed live since this plugin mounted; never seed or persisted tails. */
|
|
19
|
+
export declare function createLocalUserTurnObserver(ctx: Pick<Context, 'get' | 'on'>): {
|
|
20
|
+
current(request: Pick<RuntimeRequest, "sessionId" | "userInputIds">): {
|
|
21
|
+
turn: number;
|
|
22
|
+
userMessageId: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { NeedError } from "./remote.js";
|
|
4
|
+
const failure = (code = 'REMOTE_DETAIL_SCOPE_INVALID') => new NeedError(code, '本轮输入无法核对,已停止本次处理。', '官方当前员工输入与所选远程品牌');
|
|
5
|
+
/** CAS expectation only. L independently establishes live human-turn/brand evidence. */
|
|
6
|
+
export async function readLocalUserTurn(ctx, request) {
|
|
7
|
+
request = structuredClone(request);
|
|
8
|
+
const id = SessionId(request.sessionId), sessions = ctx.get('sessions'), session = sessions?.get(id);
|
|
9
|
+
const persistence = ctx.get('sessionPersistence');
|
|
10
|
+
if (!session || typeof sessions?.flush !== 'function' || !persistence?.identity || typeof persistence.open !== 'function' || !request.userMessageIds.length)
|
|
11
|
+
throw failure();
|
|
12
|
+
const watermark = session.seq, header = structuredClone(session.header), persistenceIdentity = persistence.identity;
|
|
13
|
+
const assertCurrent = () => { if (ctx.get('sessions')?.get(id) !== session || ctx.get('sessionPersistence')?.identity !== persistenceIdentity || session.seq !== watermark || !isDeepStrictEqual(session.header, header))
|
|
14
|
+
throw failure('LOCAL_USER_TURN_CHANGED'); };
|
|
15
|
+
assertCurrent();
|
|
16
|
+
if (!await sessions.flush(session))
|
|
17
|
+
throw failure();
|
|
18
|
+
assertCurrent();
|
|
19
|
+
const handle = await persistence.open(id, 'read');
|
|
20
|
+
let proof, firstFailure;
|
|
21
|
+
try {
|
|
22
|
+
assertCurrent();
|
|
23
|
+
if (handle.id !== id || handle.access !== 'read' || !isDeepStrictEqual(handle.header, header) || handle.inheritedEventCount !== session.inheritedEventCount)
|
|
24
|
+
throw failure();
|
|
25
|
+
const { events } = await handle.read();
|
|
26
|
+
assertCurrent();
|
|
27
|
+
if (!Array.isArray(events) || events.length !== watermark)
|
|
28
|
+
throw failure();
|
|
29
|
+
let turn, step, input, route, routeContext;
|
|
30
|
+
for (let i = 0; i < events.length; i++) {
|
|
31
|
+
const e = events[i];
|
|
32
|
+
if (e.seq !== i)
|
|
33
|
+
throw failure();
|
|
34
|
+
if (e.type === 'turn/start') {
|
|
35
|
+
turn = e;
|
|
36
|
+
step = undefined;
|
|
37
|
+
input = undefined;
|
|
38
|
+
}
|
|
39
|
+
else if (e.type === 'turn/end') {
|
|
40
|
+
turn = undefined;
|
|
41
|
+
step = undefined;
|
|
42
|
+
input = undefined;
|
|
43
|
+
}
|
|
44
|
+
else if (e.type === 'step/start') {
|
|
45
|
+
step = e;
|
|
46
|
+
input = undefined;
|
|
47
|
+
}
|
|
48
|
+
else if (e.type === 'step/end') {
|
|
49
|
+
step = undefined;
|
|
50
|
+
input = undefined;
|
|
51
|
+
}
|
|
52
|
+
else if (e.type === 'user/message' && step && e.data.source.kind === 'user')
|
|
53
|
+
input = e;
|
|
54
|
+
else if (e.type === 'request/header')
|
|
55
|
+
route = e.data.header.config.provider;
|
|
56
|
+
else if (e.type === 'request/context')
|
|
57
|
+
routeContext = e.data.provider;
|
|
58
|
+
}
|
|
59
|
+
if (!turn || !step || step.data.turn !== turn.data.turn || !input || route !== request.route || routeContext !== undefined && routeContext !== request.route || !session.isOwnSeq(turn.seq) || !session.isOwnSeq(step.seq) || !session.isOwnSeq(input.seq) || !request.userMessageIds.includes(input.data.id))
|
|
60
|
+
throw failure();
|
|
61
|
+
proof = { expectedUserTurn: { turn: turn.data.turn, userMessageId: input.data.id }, startSequence: turn.seq, userMessageSequence: input.seq };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
firstFailure = error;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
await handle.close();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (firstFailure instanceof NeedError)
|
|
71
|
+
throw Object.assign(new NeedError(firstFailure.code, firstFailure.message, firstFailure.requires + ';持久句柄关闭失败'), { cause: new AggregateError([firstFailure, error], 'LOCAL_TURN_READ_AND_CLOSE_FAILED') });
|
|
72
|
+
if (firstFailure !== undefined)
|
|
73
|
+
throw new AggregateError([firstFailure, error], 'LOCAL_TURN_READ_AND_CLOSE_FAILED');
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (firstFailure !== undefined)
|
|
77
|
+
throw firstFailure;
|
|
78
|
+
assertCurrent();
|
|
79
|
+
return { ...proof, assertCurrent };
|
|
80
|
+
}
|
|
81
|
+
/** Only metadata observed live since this plugin mounted; never seed or persisted tails. */
|
|
82
|
+
export function createLocalUserTurnObserver(ctx) {
|
|
83
|
+
const current = new WeakMap();
|
|
84
|
+
ctx.on('session/event', (session, event) => {
|
|
85
|
+
if (event.type === 'turn/start')
|
|
86
|
+
current.set(session, { turn: event.data.turn });
|
|
87
|
+
else if (event.type === 'turn/end')
|
|
88
|
+
current.delete(session);
|
|
89
|
+
else if (event.type === 'user/message' && event.data.source.kind === 'user') {
|
|
90
|
+
const turn = current.get(session);
|
|
91
|
+
if (turn)
|
|
92
|
+
turn.userMessageId = event.data.id;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
ctx.on('session/disposed', session => current.delete(session));
|
|
96
|
+
return { current(request) {
|
|
97
|
+
const session = request.sessionId ? ctx.get('sessions')?.get(SessionId(request.sessionId)) : undefined;
|
|
98
|
+
const metadata = session ? current.get(session) : undefined;
|
|
99
|
+
if (!metadata || !Number.isSafeInteger(metadata.turn) || metadata.turn < 0 || !metadata.userMessageId || !request.userInputIds?.includes(metadata.userMessageId))
|
|
100
|
+
throw new NeedError('LOCAL_USER_TURN_UNCONFIRMED', '无法核对本次员工输入,本轮已停止。', '本插件启用后观察到的官方真实员工输入');
|
|
101
|
+
return { turn: metadata.turn, userMessageId: metadata.userMessageId };
|
|
102
|
+
} };
|
|
103
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import type { ModelCatalogTarget } from '@ai-workbench/connect/api';
|
|
3
|
+
import type { AgentDefinition } from './agents.ts';
|
|
4
|
+
import { type ConnectApi, type ConnectTarget } from './provider.ts';
|
|
5
|
+
export interface HostModel {
|
|
6
|
+
id: string;
|
|
7
|
+
value: string;
|
|
8
|
+
name: string;
|
|
9
|
+
resolvedModel: string | null;
|
|
10
|
+
reasoning: {
|
|
11
|
+
options: {
|
|
12
|
+
value: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}[];
|
|
15
|
+
defaultValue: string | null;
|
|
16
|
+
defaultKnown: boolean;
|
|
17
|
+
};
|
|
18
|
+
source: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface CatalogLease {
|
|
21
|
+
readonly target: ModelCatalogTarget;
|
|
22
|
+
readonly models: readonly HostModel[];
|
|
23
|
+
readonly signal: AbortSignal;
|
|
24
|
+
assertCurrent(): void;
|
|
25
|
+
release(): void;
|
|
26
|
+
}
|
|
27
|
+
export interface ModelDirectory {
|
|
28
|
+
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
29
|
+
}
|
|
30
|
+
export type CatalogRunner = (target: ConnectTarget, command: string, signal: AbortSignal, maxBytes: number) => Promise<string>;
|
|
31
|
+
export declare function parseHostCatalog(input: string, provider: string, username: string): HostModel[];
|
|
32
|
+
/** Only the public L sshArgs function chooses SSH trust and transport arguments. */
|
|
33
|
+
export declare const runCatalog: CatalogRunner;
|
|
34
|
+
/** No cache: every query binds to L's current object, host-user and generation. */
|
|
35
|
+
export declare class ModelCatalog implements ModelDirectory {
|
|
36
|
+
private get;
|
|
37
|
+
private maxBytes;
|
|
38
|
+
private run;
|
|
39
|
+
private changed;
|
|
40
|
+
private api?;
|
|
41
|
+
private unsubscribe?;
|
|
42
|
+
private signature?;
|
|
43
|
+
private revision;
|
|
44
|
+
private disposed;
|
|
45
|
+
private initialized;
|
|
46
|
+
private active;
|
|
47
|
+
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, changed?: () => void);
|
|
48
|
+
refresh(): void;
|
|
49
|
+
private trusted;
|
|
50
|
+
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
51
|
+
dispose(): void;
|
|
52
|
+
}
|
|
53
|
+
export declare function modelInfo(provider: string, model: HostModel): LlmResolvedModelInfo & {
|
|
54
|
+
catalog: HostModel;
|
|
55
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
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
|
+
import { LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
11
|
+
import { sameTarget, validateTarget } from "./provider.js";
|
|
12
|
+
import { ModelConfigurationError } from "./model-configuration.js";
|
|
13
|
+
import { sanitizeObservation } from "./conversation-details.js";
|
|
14
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
const text = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
16
|
+
function fail(reason) { throw new LlmError('无法获取模型列表:' + reason, 'MODEL_CATALOG_UNAVAILABLE'); }
|
|
17
|
+
function freeze(value) { if (value && typeof value === 'object') {
|
|
18
|
+
for (const item of Object.values(value))
|
|
19
|
+
freeze(item);
|
|
20
|
+
Object.freeze(value);
|
|
21
|
+
} return value; }
|
|
22
|
+
export function parseHostCatalog(input, provider, username) {
|
|
23
|
+
let wire;
|
|
24
|
+
try {
|
|
25
|
+
wire = JSON.parse(input);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
fail('主机目录响应不是有效 JSON');
|
|
29
|
+
}
|
|
30
|
+
if (!object(wire) || wire.version !== 1 || wire.purpose !== 'model-catalog' || wire.scope !== 'host-user' || wire.provider !== provider)
|
|
31
|
+
fail('主机目录版本、品牌或作用域不匹配');
|
|
32
|
+
if (wire.ok === false) {
|
|
33
|
+
const error = object(wire.error) ? wire.error : undefined;
|
|
34
|
+
const code = typeof error?.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : 'PROTOCOL_INVALID';
|
|
35
|
+
const reason = text(error?.message) ? sanitizeObservation({ kind: 'session', severity: 'warning', code, summary: error.message, payload: {} }).summary : '主机未提供失败说明';
|
|
36
|
+
fail('主机目录查询失败(' + code + '):' + reason);
|
|
37
|
+
}
|
|
38
|
+
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
|
+
fail('主机目录缺少完整元数据');
|
|
40
|
+
const ids = new Set(), values = new Set();
|
|
41
|
+
for (const model of wire.models) {
|
|
42
|
+
if (!object(model) || !text(model.id) || !text(model.value) || !text(model.name) || (model.resolvedModel !== null && !text(model.resolvedModel)) || !object(model.source) || !object(model.reasoning))
|
|
43
|
+
fail('主机模型条目不完整');
|
|
44
|
+
if (provider === 'codex' && model.resolvedModel !== model.value)
|
|
45
|
+
fail('Codex目录具体型号与模型值不一致');
|
|
46
|
+
if (ids.has(model.id) || values.has(model.value))
|
|
47
|
+
fail('主机目录含重复模型标识或值');
|
|
48
|
+
ids.add(model.id);
|
|
49
|
+
values.add(model.value);
|
|
50
|
+
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
|
+
fail('主机推理等级或默认值无效');
|
|
53
|
+
const efforts = new Set();
|
|
54
|
+
for (const effort of r.options) {
|
|
55
|
+
if (!object(effort) || !text(effort.value) || !text(effort.name) || efforts.has(effort.value))
|
|
56
|
+
fail('主机推理等级条目无效或重复');
|
|
57
|
+
efforts.add(effort.value);
|
|
58
|
+
}
|
|
59
|
+
if (r.defaultKnown && (r.defaultValue === null || !efforts.has(r.defaultValue)))
|
|
60
|
+
fail('主机默认推理等级缺少对应选项');
|
|
61
|
+
}
|
|
62
|
+
if (!wire.models.length)
|
|
63
|
+
fail('主机返回的完整模型目录为空');
|
|
64
|
+
return freeze(structuredClone(wire.models));
|
|
65
|
+
}
|
|
66
|
+
/** Only the public L sshArgs function chooses SSH trust and transport arguments. */
|
|
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. */
|
|
139
|
+
export class ModelCatalog {
|
|
140
|
+
get;
|
|
141
|
+
maxBytes;
|
|
142
|
+
run;
|
|
143
|
+
changed;
|
|
144
|
+
api;
|
|
145
|
+
unsubscribe;
|
|
146
|
+
signature;
|
|
147
|
+
revision = 0;
|
|
148
|
+
disposed = false;
|
|
149
|
+
initialized = false;
|
|
150
|
+
active = new Set();
|
|
151
|
+
constructor(get, maxBytes, run = runCatalog, changed = () => { }) {
|
|
152
|
+
this.get = get;
|
|
153
|
+
this.maxBytes = maxBytes;
|
|
154
|
+
this.run = run;
|
|
155
|
+
this.changed = changed;
|
|
156
|
+
this.refresh();
|
|
157
|
+
}
|
|
158
|
+
refresh() {
|
|
159
|
+
const api = this.get();
|
|
160
|
+
const state = api?.getConnectionState?.();
|
|
161
|
+
const info = api?.getContractInfo?.();
|
|
162
|
+
const targets = info?.modelCatalogTarget === 1 && api?.getModelCatalogTarget ? ['codex', 'claude'].map(brand => api.getModelCatalogTarget(brand)) : undefined;
|
|
163
|
+
const login = api?.getLoginPromptState?.();
|
|
164
|
+
// L's same-epoch recovery wording is presentation metadata, not an execution change.
|
|
165
|
+
const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase, needCode: state.need?.code };
|
|
166
|
+
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, logins: login?.logins, authorizing: login?.phase === 'authorizing' });
|
|
168
|
+
const switched = api !== this.api;
|
|
169
|
+
const changed = this.initialized && (switched || signature !== this.signature);
|
|
170
|
+
this.initialized = true;
|
|
171
|
+
if (switched) {
|
|
172
|
+
this.unsubscribe?.();
|
|
173
|
+
this.unsubscribe = undefined;
|
|
174
|
+
this.api = api;
|
|
175
|
+
const off = [];
|
|
176
|
+
if (typeof api?.onConnectionState === 'function')
|
|
177
|
+
off.push(api.onConnectionState(() => this.refresh()));
|
|
178
|
+
if (typeof api?.onLoginPromptState === 'function')
|
|
179
|
+
off.push(api.onLoginPromptState(() => this.refresh()));
|
|
180
|
+
this.unsubscribe = () => { for (const stop of off)
|
|
181
|
+
stop(); };
|
|
182
|
+
}
|
|
183
|
+
this.signature = signature;
|
|
184
|
+
if (changed) {
|
|
185
|
+
this.revision++;
|
|
186
|
+
for (const controller of this.active)
|
|
187
|
+
controller.abort();
|
|
188
|
+
this.active.clear();
|
|
189
|
+
this.changed();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
trusted(agent, api) {
|
|
193
|
+
const info = api.getContractInfo?.();
|
|
194
|
+
if (info?.modelCatalogTarget !== 1 || typeof api.getModelCatalogTarget !== 'function')
|
|
195
|
+
fail('远程主机连接组件尚未提供模型目录目标能力');
|
|
196
|
+
const state = api.getConnectionState();
|
|
197
|
+
if (state.phase !== 'connected' || !text(state.instanceId) || !Number.isSafeInteger(state.epoch) || state.epoch < 0)
|
|
198
|
+
fail('当前没有已验证的远程主机连接');
|
|
199
|
+
const result = api.getModelCatalogTarget(agent.loginProvider);
|
|
200
|
+
if (!result.ok)
|
|
201
|
+
fail(result.error.message);
|
|
202
|
+
const target = result.value;
|
|
203
|
+
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
|
+
fail('模型目录目标与当前连接或品牌不匹配');
|
|
205
|
+
try {
|
|
206
|
+
validateTarget(target.target);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
fail('模型目录的可信 SSH 目标不完整');
|
|
210
|
+
}
|
|
211
|
+
return structuredClone(target);
|
|
212
|
+
}
|
|
213
|
+
async read(agent, signal) {
|
|
214
|
+
let controller;
|
|
215
|
+
try {
|
|
216
|
+
if (this.disposed)
|
|
217
|
+
fail('远程模型插件已关闭');
|
|
218
|
+
this.refresh();
|
|
219
|
+
const api = this.get();
|
|
220
|
+
if (!api)
|
|
221
|
+
fail('远程主机连接组件没有装好');
|
|
222
|
+
const target = freeze(this.trusted(agent, api)), revision = this.revision;
|
|
223
|
+
const max = this.maxBytes();
|
|
224
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
225
|
+
fail('acpMaxMessageBytes 设置无效');
|
|
226
|
+
controller = new AbortController();
|
|
227
|
+
this.active.add(controller);
|
|
228
|
+
const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
229
|
+
const assertCurrent = () => { if (combined.aborted || this.disposed || this.get() !== api || revision !== this.revision)
|
|
230
|
+
fail('连接已改变,原模型目录已失效'); const current = this.trusted(agent, api); if (current.instanceId !== target.instanceId || current.epoch !== target.epoch || current.provider !== target.provider || !sameTarget(current.target, target.target))
|
|
231
|
+
fail('可信目标或连接代次已改变,原模型目录已失效'); };
|
|
232
|
+
assertCurrent();
|
|
233
|
+
const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, combined, max);
|
|
234
|
+
assertCurrent();
|
|
235
|
+
const models = parseHostCatalog(output, agent.loginProvider, target.target.username);
|
|
236
|
+
const own = controller;
|
|
237
|
+
return { target, models, signal: combined, assertCurrent, release: () => this.active.delete(own) };
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (controller)
|
|
241
|
+
this.active.delete(controller);
|
|
242
|
+
if (error instanceof LlmError)
|
|
243
|
+
throw error;
|
|
244
|
+
if (error instanceof ModelConfigurationError)
|
|
245
|
+
fail(error.message.replace(/^无法获取模型列表:/, ''));
|
|
246
|
+
fail('目录查询失败,根因未查明');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
dispose() { this.disposed = true; this.unsubscribe?.(); for (const controller of this.active)
|
|
250
|
+
controller.abort(); this.active.clear(); }
|
|
251
|
+
}
|
|
252
|
+
export function modelInfo(provider, model) {
|
|
253
|
+
return { provider, id: model.id, name: model.name, inputModalities: ['text', 'image'], 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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SessionConfigOption } from '@agentclientprotocol/sdk/unstable';
|
|
2
|
+
export type DeepReadonly<T> = T extends readonly unknown[] ? {
|
|
3
|
+
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
4
|
+
} : T extends object ? {
|
|
5
|
+
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
6
|
+
} : T;
|
|
7
|
+
export interface ModelCatalogValue {
|
|
8
|
+
readonly value: string;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly description?: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface ModelConfigurationSnapshot {
|
|
13
|
+
readonly configOptions: DeepReadonly<SessionConfigOption[]>;
|
|
14
|
+
readonly models: readonly ModelCatalogValue[];
|
|
15
|
+
readonly modelConfigId?: string;
|
|
16
|
+
readonly effortConfigId?: string;
|
|
17
|
+
readonly model?: string;
|
|
18
|
+
readonly effort?: string;
|
|
19
|
+
readonly effortAvailable: boolean;
|
|
20
|
+
/** Acknowledges the selected values, not the availability of every catalog entry. */
|
|
21
|
+
readonly confirmed: boolean;
|
|
22
|
+
readonly failure?: Readonly<{
|
|
23
|
+
code: 'MODEL_CATALOG_UNAVAILABLE';
|
|
24
|
+
message: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export type ModelConfigurationSetter = (configId: string, value: string) => Promise<{
|
|
28
|
+
configOptions: SessionConfigOption[];
|
|
29
|
+
}>;
|
|
30
|
+
export declare class ModelConfigurationError extends Error {
|
|
31
|
+
readonly code = "MODEL_CATALOG_UNAVAILABLE";
|
|
32
|
+
constructor(reason: string, options?: ErrorOptions);
|
|
33
|
+
}
|
|
34
|
+
/** Pure ACP configuration state. All IDs and values come from the given peer data. */
|
|
35
|
+
export declare class ModelConfiguration {
|
|
36
|
+
private readonly rpcSet;
|
|
37
|
+
private state;
|
|
38
|
+
private parsed?;
|
|
39
|
+
private revision;
|
|
40
|
+
private busy;
|
|
41
|
+
constructor(options: SessionConfigOption[], rpcSet: ModelConfigurationSetter);
|
|
42
|
+
get models(): readonly ModelCatalogValue[];
|
|
43
|
+
get snapshot(): ModelConfigurationSnapshot;
|
|
44
|
+
update(options: SessionConfigOption[]): void;
|
|
45
|
+
private fail;
|
|
46
|
+
private checkAckRevision;
|
|
47
|
+
private set;
|
|
48
|
+
select(model: string, effort?: string): Promise<ModelConfigurationSnapshot>;
|
|
49
|
+
/** Call immediately before prompt; an idle/new/load/resume update withdraws the ACK. */
|
|
50
|
+
assertSelection(model: string, effort?: string): void;
|
|
51
|
+
}
|