dsh-router-core 0.4.1 → 0.4.3
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 +18 -15
- package/cordis.patch.yml +10 -0
- package/docs/ext.md +65 -3
- package/lib/client-registry.js +633 -50
- package/lib/client.js +633 -50
- package/lib/contract-D4BWZ7Xe.js +13 -0
- package/lib/ext-prompt/index.js +527 -0
- package/lib/ext-prompt/locale/en.json +6 -0
- package/lib/ext-prompt/locale/zh.json +6 -0
- package/lib/ext-prompt/package.json +5 -0
- package/lib/ext-test/index.js +1 -12
- package/lib/index.js +298 -119
- package/lib/types/ext/contract.d.ts +113 -0
- package/lib/types/ext-prompt/content.d.ts +57 -0
- package/lib/types/ext-prompt/index.d.ts +53 -0
- package/lib/types/ext-prompt/logo.d.ts +17 -0
- package/lib/types/ext-prompt/mount.d.ts +51 -0
- package/lib/types/ext-prompt/plugin.d.ts +32 -0
- package/lib/types/ext-prompt/render.d.ts +105 -0
- package/lib/types/host-base-url.d.ts +31 -0
- package/lib/types/host-version.d.ts +7 -8
- package/lib/types/index.d.ts +1 -1
- package/lib/types/llm/adapter.d.ts +6 -1
- package/lib/types/shared.d.ts +33 -0
- package/package.json +17 -7
- package/src/client/EndpointTab.tsx +15 -18
- package/src/client/ExtDetail.tsx +552 -9
- package/src/client/index.tsx +7 -28
- package/src/client/router.css +132 -62
- package/src/ext/contract.ts +111 -0
- package/src/ext-prompt/assembly.test.ts +79 -0
- package/src/ext-prompt/content.ts +174 -0
- package/src/ext-prompt/ext-prompt.test.ts +432 -0
- package/src/ext-prompt/index.ts +104 -0
- package/src/ext-prompt/locale/en.json +6 -0
- package/src/ext-prompt/locale/zh.json +6 -0
- package/src/ext-prompt/logo.svg +14 -0
- package/src/ext-prompt/logo.ts +18 -0
- package/src/ext-prompt/mount.test.ts +95 -0
- package/src/ext-prompt/mount.ts +79 -0
- package/src/ext-prompt/plugin.ts +223 -0
- package/src/ext-prompt/render.test.ts +256 -0
- package/src/ext-prompt/render.ts +216 -0
- package/src/ext-prompt/row.json +5 -0
- package/src/ext-route.test.ts +78 -0
- package/src/host-base-url.test.ts +129 -0
- package/src/host-base-url.ts +43 -0
- package/src/host-version.test.ts +21 -28
- package/src/host-version.ts +39 -39
- package/src/index.ts +230 -96
- package/src/llm/adapter.test.ts +19 -42
- package/src/llm/adapter.ts +32 -50
- package/src/shared.ts +34 -0
package/src/llm/adapter.ts
CHANGED
|
@@ -123,11 +123,10 @@ export interface RouterAdapterSource {
|
|
|
123
123
|
* base64)。图片字节经 `resolveAttachments` 读取;读不到时回退为稳定的占位文本,
|
|
124
124
|
* **绝不静默丢图**(丢图 = 模型看到一条没有图的消息却毫无提示)。
|
|
125
125
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* 两者在 wire 上统一序列化为 role:'tool',产物一致。见主循环里的 role==='tool' 分支。
|
|
126
|
+
* 工具结果只有一种形态:独立 `role:'tool'` 消息、toolCallId 挂消息级,wire 上原样
|
|
127
|
+
* 发 `role:'tool'`。见主循环里的 `role === 'tool'` 分支。
|
|
129
128
|
*/
|
|
130
|
-
/** tool
|
|
129
|
+
/** tool 消息里的图片挂到 user 消息时用的说明文字(与官方适配器一致)。 */
|
|
131
130
|
const TOOL_RESULT_IMAGE_TEXT = 'Tool result images'
|
|
132
131
|
|
|
133
132
|
/**
|
|
@@ -144,7 +143,7 @@ const STREAM_IDLE_TIMEOUT_MS = 300_000
|
|
|
144
143
|
async function wireMessages(options: GenerateOptions, system: string | undefined, attachments: RouterAttachmentStore | undefined, log?: (msg: string) => void): Promise<Array<Record<string, unknown>>> {
|
|
145
144
|
const out: Array<Record<string, unknown>> = []
|
|
146
145
|
/**
|
|
147
|
-
* 攒着「tool
|
|
146
|
+
* 攒着「tool 消息里带出来的图片」,等这一串 tool 消息**发完**再合并成一条
|
|
148
147
|
* user 消息补在后面。
|
|
149
148
|
*
|
|
150
149
|
* 为什么必须跨消息攒、而不是各消息各发一条:harness 把**每个 tool 结果放
|
|
@@ -188,14 +187,13 @@ async function wireMessages(options: GenerateOptions, system: string | undefined
|
|
|
188
187
|
})
|
|
189
188
|
continue
|
|
190
189
|
}
|
|
191
|
-
// tool
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
if (isToolRoleMessage) {
|
|
190
|
+
// tool 结果:0.2.0 起**只有一种形态** —— 独立 role:'tool' 消息、toolCallId 挂消息级。
|
|
191
|
+
// 旧的「user 消息内嵌 'tool-result' 块」(0.1.5/0.1.6 时代,0.1.7 起退役)**不再
|
|
192
|
+
// 支持**:0.2.0 的 v3→v4 会话迁移主动拒绝 tool-result 包装
|
|
193
|
+
// (`must not contain a released tool-result wrapper`),老会话要么被迁移展开成
|
|
194
|
+
// tool 消息、要么直接拒绝打开 —— 这条形态到不了 adapter(见 host-version.ts 的
|
|
195
|
+
// 版本地板)。
|
|
196
|
+
if ((message.role as string) === 'tool') {
|
|
199
197
|
// tool 消息必须紧跟 assistant(tool_calls):中间插进任何 user 消息,上游都会判定
|
|
200
198
|
// tool_call 与 tool_result 失配 → codebuddy 直接 400 网关码 11148(bad_request
|
|
201
199
|
// 按核心设计不罚账号,面板上看不到异常,易误判成额度/风控)。
|
|
@@ -214,35 +212,11 @@ async function wireMessages(options: GenerateOptions, system: string | undefined
|
|
|
214
212
|
pendingToolImages.push(...imageParts)
|
|
215
213
|
continue
|
|
216
214
|
}
|
|
217
|
-
// user
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
attachments,
|
|
223
|
-
options.signal,
|
|
224
|
-
log,
|
|
225
|
-
)
|
|
226
|
-
// 有实质 user 内容(或压根没有 tool 结果)就先发这条 user 消息;
|
|
227
|
-
// 只有 tool 结果的「纯工具」消息不发空 user,图片留到 pending 里合并。
|
|
228
|
-
if (ownParts.length > 0 || toolResults.length === 0) {
|
|
229
|
-
flushToolImages()
|
|
230
|
-
out.push({ role: 'user', content: compactUserContent(ownParts) })
|
|
231
|
-
}
|
|
232
|
-
for (const result of toolResults) {
|
|
233
|
-
// **tool 消息必须紧跟 assistant(tool_calls)**:中间插进任何 user 消息,
|
|
234
|
-
// 上游都会判定 tool_call 与 tool_result 失配 —— codebuddy 直接 400 网关码
|
|
235
|
-
// 11148「tool calls and tool results do not match」。
|
|
236
|
-
// 图片不能放进 tool 消息(实测模型会把两张不同的图认成同一张),
|
|
237
|
-
// 也不能插在两条 tool 之间(同样 400),所以先攒起来、发完整串再补一条 user。
|
|
238
|
-
const parts = await contentParts(result.content, attachments, options.signal, log)
|
|
239
|
-
const imageParts = parts.filter((p) => p.type !== 'text')
|
|
240
|
-
const text = parts.filter((p) => p.type === 'text').map((p) => (p as { text?: string }).text ?? '').join('')
|
|
241
|
-
// 空结果就发空串,不要替换成 '(no output)' 之类的字面量 —— 模型会
|
|
242
|
-
// 以为工具真的打印了那句话(9router 也是补 content: "")。
|
|
243
|
-
out.push({ role: 'tool', tool_call_id: result.toolCallId, content: text })
|
|
244
|
-
pendingToolImages.push(...imageParts)
|
|
245
|
-
}
|
|
215
|
+
// user:文本 + 图片。**即使内容为空也照发** —— 这条 user 消息在历史里存在过,
|
|
216
|
+
// 悄悄省掉等于让模型看到一段断掉的话轮。
|
|
217
|
+
const userParts = await contentParts(message.content, attachments, options.signal, log)
|
|
218
|
+
flushToolImages()
|
|
219
|
+
out.push({ role: 'user', content: compactUserContent(userParts) })
|
|
246
220
|
}
|
|
247
221
|
flushToolImages()
|
|
248
222
|
return out
|
|
@@ -262,7 +236,7 @@ function compactUserContent(parts: Array<Record<string, unknown>>): string | Arr
|
|
|
262
236
|
}
|
|
263
237
|
|
|
264
238
|
/**
|
|
265
|
-
*
|
|
239
|
+
* 一条 DSH 消息的内容块序列化成 OpenAI content parts(text + image_url)。
|
|
266
240
|
* 纯文本时返回空数组(上层走紧凑字符串路径)。图片转 base64 data URI;
|
|
267
241
|
* attachments 缺失或读取失败时回退稳定占位文本(绝不静默丢图)。
|
|
268
242
|
*/
|
|
@@ -284,10 +258,9 @@ async function contentParts(
|
|
|
284
258
|
parts.push(...(await imageParts(ref, attachments, signal, log)))
|
|
285
259
|
continue
|
|
286
260
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// 其它块(reasoning/tool-call)不属于 user content,忽略
|
|
261
|
+
// 其它块(reasoning/tool-call/tool-addition/tool-removal/file)不属于发给上游的
|
|
262
|
+
// user/tool 内容,忽略。0.1.x 的 'tool-result' 块曾在这里递归展开,随 0.2.0
|
|
263
|
+
// 单面编写一并删除(该块在 0.2.0 的 ContentBlockMap 里已不存在,见主循环注释)。
|
|
291
264
|
}
|
|
292
265
|
return parts
|
|
293
266
|
}
|
|
@@ -634,7 +607,12 @@ const ROUTER_REASONING_EFFORTS: readonly LlmReasoningEffortInfo[] = [
|
|
|
634
607
|
]
|
|
635
608
|
|
|
636
609
|
export class RouterAdapter extends LlmAdapter {
|
|
637
|
-
|
|
610
|
+
/**
|
|
611
|
+
* 网关端点。**函数形态 = 每次请求现算**:端口要等宿主 listen 完才有值的场景
|
|
612
|
+
* 必须传函数(写死 3080 在桌面端等非 3080 宿主上全线 `fetch failed`,
|
|
613
|
+
* issue #9),见 `host-base-url.ts`。
|
|
614
|
+
*/
|
|
615
|
+
private readonly baseURL: string | (() => string)
|
|
638
616
|
private readonly source: RouterAdapterSource
|
|
639
617
|
private readonly resolveAttachments: () => RouterAttachmentStore | undefined
|
|
640
618
|
/** 流空闲超时(ms):上游这么久不吐字节就判 TIMEOUT。 */
|
|
@@ -649,7 +627,7 @@ export class RouterAdapter extends LlmAdapter {
|
|
|
649
627
|
* 曾经那条路径是个空 `catch`,出问题跨版本都没人发现(issue #8)。
|
|
650
628
|
*/
|
|
651
629
|
constructor(
|
|
652
|
-
baseURL: string,
|
|
630
|
+
baseURL: string | (() => string),
|
|
653
631
|
source: RouterAdapterSource,
|
|
654
632
|
resolveAttachments?: () => RouterAttachmentStore | undefined,
|
|
655
633
|
idleTimeoutMs: number = STREAM_IDLE_TIMEOUT_MS,
|
|
@@ -713,7 +691,11 @@ export class RouterAdapter extends LlmAdapter {
|
|
|
713
691
|
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
714
692
|
let resp: Response
|
|
715
693
|
try {
|
|
716
|
-
|
|
694
|
+
// 端点现算:宿主监听端口在 listen 之后才有值、也会随宿主重启变(issue #9)。
|
|
695
|
+
// 解析器自己抛错时也落进下面这个 catch,于是「端口拿不到」是以
|
|
696
|
+
// LlmError 消息报出来的,而不是一句无从查证的 `fetch failed`。
|
|
697
|
+
const base = typeof this.baseURL === 'function' ? this.baseURL() : this.baseURL
|
|
698
|
+
resp = await fetch(`${base}/chat/completions`, {
|
|
717
699
|
method: 'POST',
|
|
718
700
|
// attributionHeaders() 是 dsh-llm 对 adapter 的硬契约:每个 provider
|
|
719
701
|
// 请求都必须带(LlmAdapter 类注释原话)。它给出
|
package/src/shared.ts
CHANGED
|
@@ -164,6 +164,40 @@ export interface RouterExtItem {
|
|
|
164
164
|
* 「包含的组件」里占一行了,两处都列就是同一个东西显示两遍(与内置供应商同理)。
|
|
165
165
|
*/
|
|
166
166
|
source?: 'builtin'
|
|
167
|
+
/**
|
|
168
|
+
* 扩展自带的子开关(如准则的每一条)。**由扩展自报**,核心只搬运。
|
|
169
|
+
*
|
|
170
|
+
* ⚠️ 与 `enabled`(总开关)不同:子开关**不改变扩展是否被加载**,只改它
|
|
171
|
+
* 自己的行为。总开关关时这些子开关一并失效(面板上禁用,不隐藏)。
|
|
172
|
+
*/
|
|
173
|
+
controls?: ExtControlItem[]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** `/router/api/ext` 里的一条子开关(与 `router.ext` 契约的 `ExtControl` 对应)。 */
|
|
177
|
+
export interface ExtControlItem {
|
|
178
|
+
/** 稳定 id(持久化键;改名会丢用户已保存的选择)。 */
|
|
179
|
+
id: string
|
|
180
|
+
title: string
|
|
181
|
+
/** 当前是否开启(扩展自算:默认值 + 用户已存选择)。 */
|
|
182
|
+
on: boolean
|
|
183
|
+
/** 一句话说明(面板副行,可选)。 */
|
|
184
|
+
detail?: string
|
|
185
|
+
/** 原文(面板点开即可读到;不给出就是在盲切,见 `ExtControl.body`)。 */
|
|
186
|
+
body?: string
|
|
187
|
+
/** 文本可编辑(缺省 = 只读;见 `ExtControl.editable`)。 */
|
|
188
|
+
editable?: boolean
|
|
189
|
+
/**
|
|
190
|
+
* 自建条目(可删)还是内置(不可删、只有开关与还原)。
|
|
191
|
+
*
|
|
192
|
+
* ⚠️ **必填,不给可选**(2026-09-30 实测踩过):曾声明成 `custom?: boolean`,
|
|
193
|
+
* 而服务端只在 `custom === true` 时才带这个字段 ⇒ 内置条目拿到的是
|
|
194
|
+
* `undefined`,面板判 `c.custom === true` 为假,**恰好是对的** ——
|
|
195
|
+
* 但那条正确性建立在"缺省恰好等于 false"上,改任何一边的默认值都会翻转。
|
|
196
|
+
* ⇒ 让服务端**总是**给显式布尔,两边不必猜。
|
|
197
|
+
*/
|
|
198
|
+
custom: boolean
|
|
199
|
+
/** 文本被改过(决定「还原」是否可点)。必填,理由同 `custom`。 */
|
|
200
|
+
overridden: boolean
|
|
167
201
|
}
|
|
168
202
|
|
|
169
203
|
/** `/router/api/ext` response(扩展插件列表 + 开关)。 */
|