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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 网关端点按**宿主实际监听端口**解析 —— issue #9 的闸门。
|
|
3
|
+
*
|
|
4
|
+
* ## 这条闸门拦的是什么
|
|
5
|
+
*
|
|
6
|
+
* `/v1/*` 是注册在**宿主** webServer 上的路由,端口由启动通道给:`dsh web` 默认
|
|
7
|
+
* 3080,桌面端官方通道、以及 `port: 0`(OS 分配)都不是。adapter 曾把
|
|
8
|
+
* `http://localhost:3080/v1` 写死,于是那些宿主上 Router 每一轮对话都是
|
|
9
|
+
* `dsh-router upstream call failed: fetch failed`,而 `/v1` 明明就在本机跑着。
|
|
10
|
+
*
|
|
11
|
+
* 判据刻意用**真服务器 + OS 分配端口**而不是打桩 fetch:打桩只能证明字符串拼对了,
|
|
12
|
+
* 证明不了「非 3080 端口上请求真的落得到」(这正是当初漏掉这条 bug 的原因)。
|
|
13
|
+
* `listen(0)` 拿到的端口几乎不可能是 3080,于是「用 3080 也能过」的假修法在这条
|
|
14
|
+
* 闸门下会当场变红。
|
|
15
|
+
*/
|
|
16
|
+
import test from 'node:test'
|
|
17
|
+
import assert from 'node:assert/strict'
|
|
18
|
+
import { createServer } from 'node:http'
|
|
19
|
+
import type { AddressInfo } from 'node:net'
|
|
20
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
import { RouterAdapter } from './llm/adapter.ts'
|
|
23
|
+
import { loopbackBaseURL } from './host-base-url.ts'
|
|
24
|
+
|
|
25
|
+
/** 起一个只回一条最小 SSE 的真服务器,回报 OS 分配的端口与收到的路径。 */
|
|
26
|
+
async function serveSse(): Promise<{ port: number; hits: string[]; close: () => Promise<void> }> {
|
|
27
|
+
const hits: string[] = []
|
|
28
|
+
const server = createServer((req, res) => {
|
|
29
|
+
hits.push(req.url ?? '')
|
|
30
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
31
|
+
res.end(`data: ${JSON.stringify({ choices: [{ delta: { content: 'ok' } }] })}\n\ndata: [DONE]\n\n`)
|
|
32
|
+
})
|
|
33
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
34
|
+
const { port } = server.address() as AddressInfo
|
|
35
|
+
return { port, hits, close: () => new Promise<void>((resolve) => server.close(() => resolve())) }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 跑完一次 adapter 流,返回收到的块类型。 */
|
|
39
|
+
async function drain(adapter: RouterAdapter): Promise<string[]> {
|
|
40
|
+
const types: string[] = []
|
|
41
|
+
for await (const c of adapter.stream({ model: 'm', messages: [], signal: AbortSignal.timeout(5000) } as never)) {
|
|
42
|
+
types.push(c.type)
|
|
43
|
+
}
|
|
44
|
+
return types
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('端点:宿主端口不是 3080 时,请求照样落得到(issue #9 的原始现场)', async () => {
|
|
48
|
+
const up = await serveSse()
|
|
49
|
+
try {
|
|
50
|
+
assert.notEqual(up.port, 3080, '这条用例的判据就是「端口不是 3080」')
|
|
51
|
+
const adapter = new RouterAdapter(loopbackBaseURL(() => up.port), { comboModels: async () => [] })
|
|
52
|
+
const types = await drain(adapter)
|
|
53
|
+
assert.deepEqual(up.hits, ['/v1/chat/completions'], `请求应打到宿主 /v1,实际打到:${JSON.stringify(up.hits)}`)
|
|
54
|
+
assert.ok(types.includes('text-delta'), `应真的收到上游内容,实际块类型:${JSON.stringify(types)}`)
|
|
55
|
+
} finally {
|
|
56
|
+
await up.close()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('端点:端口每次请求现算(listen 前读不到、宿主重启换端口都不缓存)', async () => {
|
|
61
|
+
const first = await serveSse()
|
|
62
|
+
const second = await serveSse()
|
|
63
|
+
try {
|
|
64
|
+
let port: number | undefined = first.port
|
|
65
|
+
let reads = 0
|
|
66
|
+
const adapter = new RouterAdapter(
|
|
67
|
+
loopbackBaseURL(() => {
|
|
68
|
+
reads += 1
|
|
69
|
+
return port
|
|
70
|
+
}),
|
|
71
|
+
{ comboModels: async () => [] },
|
|
72
|
+
)
|
|
73
|
+
await drain(adapter)
|
|
74
|
+
port = second.port // 宿主换了端口(重启 / 重新 listen)
|
|
75
|
+
await drain(adapter)
|
|
76
|
+
assert.deepEqual(first.hits, ['/v1/chat/completions'], '第一次应打在第一个端口')
|
|
77
|
+
assert.deepEqual(second.hits, ['/v1/chat/completions'], '端口变了之后应改打新端口(不能缓存第一次的值)')
|
|
78
|
+
assert.ok(reads >= 2, `端口应每请求现读,实际只读了 ${reads} 次`)
|
|
79
|
+
} finally {
|
|
80
|
+
await first.close()
|
|
81
|
+
await second.close()
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('端点:端口读不到时报出原因,不回退 3080 硬猜', async () => {
|
|
86
|
+
const adapter = new RouterAdapter(loopbackBaseURL(() => undefined), { comboModels: async () => [] })
|
|
87
|
+
await assert.rejects(
|
|
88
|
+
() => drain(adapter),
|
|
89
|
+
(err: Error) => {
|
|
90
|
+
assert.match(err.message, /webServer/, `报错必须带原因,实际:${err.message}`)
|
|
91
|
+
assert.equal((err as { code?: string }).code, 'TRANSPORT', '应是可重试的传输类错误')
|
|
92
|
+
return true
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('端点:常量形态的 baseURL 仍然可用(测试与显式传入的老调用方)', () => {
|
|
98
|
+
assert.equal(loopbackBaseURL(() => 19387)(), 'http://127.0.0.1:19387/v1')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('源码:非测试代码里不得再出现写死的 3080 端点', () => {
|
|
102
|
+
// 防复发闸门:这次的根因就是一行写死的字符串,只靠「端口现算」的实现测试拦不住
|
|
103
|
+
// 将来有人在别处再写一遍。判据只看**代码**(先剥掉注释)——注释里可以、也应该
|
|
104
|
+
// 写明「当年就是这里写死了 3080」。
|
|
105
|
+
const root = fileURLToPath(new URL('.', import.meta.url))
|
|
106
|
+
const offenders: string[] = []
|
|
107
|
+
for (const file of walk(root)) {
|
|
108
|
+
if (/(?:localhost|127\.0\.0\.1):3080/.test(stripComments(readFileSync(file, 'utf8')))) {
|
|
109
|
+
offenders.push(file.slice(root.length))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
assert.deepEqual(offenders, [], `这些文件里还有写死的 3080 端点(宿主端口随启动通道变):${offenders.join(', ')}`)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/** 去掉块注释与行注释,只留可执行代码。 */
|
|
116
|
+
function stripComments(text: string): string {
|
|
117
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 递归列出 src 下的 .ts 文件(跳过测试自身:测试里出现端口字面量是合法的)。 */
|
|
121
|
+
function walk(dir: string): string[] {
|
|
122
|
+
const out: string[] = []
|
|
123
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
124
|
+
const path = `${dir}${entry.name}`
|
|
125
|
+
if (entry.isDirectory()) out.push(...walk(`${path}/`))
|
|
126
|
+
else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) out.push(path)
|
|
127
|
+
}
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 网关端点地址(`http://127.0.0.1:<宿主端口>/v1`)的解析。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么端口不能写死
|
|
5
|
+
*
|
|
6
|
+
* dsh-router **自己不开端口**:`/v1/*` 是注册在**宿主** web server 上的路由
|
|
7
|
+
* (见 `index.ts` 的 `ctx.webServer.register`)。所以端点地址里的端口由启动通道
|
|
8
|
+
* 决定,而不是插件决定 —— `dsh web` 默认 3080,桌面端官方通道、以及配置
|
|
9
|
+
* `port: 0`(让 OS 分配)时都**不是** 3080。之前 adapter 把
|
|
10
|
+
* `http://localhost:3080/v1` 写死在构造参数里,于是非 3080 的宿主上 Router
|
|
11
|
+
* 的每一轮对话都是 `dsh-router upstream call failed: fetch failed`(issue #9)。
|
|
12
|
+
*
|
|
13
|
+
* ## 为什么返回函数而不是字符串
|
|
14
|
+
*
|
|
15
|
+
* `webServer.port` 是**监听之后**才有值的 getter(listen 前是 `undefined`),
|
|
16
|
+
* 且宿主重启/换端口后它会变。所以这里返回函数、**每次请求现算**,不在注册时
|
|
17
|
+
* 算一次 —— 注册发生在启动早期,那时算只会把「拿不到端口」固化成常量。
|
|
18
|
+
*
|
|
19
|
+
* ## 拿不到端口时为什么抛,而不是回退 3080
|
|
20
|
+
*
|
|
21
|
+
* 回退等于把同一个 bug 换种写法:端口读不到时宿主的路由也没在服务,回退到 3080
|
|
22
|
+
* 只会重新得到一句无从查证的 `fetch failed`。这里抛明确的话,让它出现在
|
|
23
|
+
* `LlmError` 消息里(adapter 在 fetch 同一个 try 里调它),报错自带原因。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** 回环地址:宿主即使监听 `0.0.0.0`(LAN 模式),走 127.0.0.1 也通。 */
|
|
27
|
+
const LOOPBACK_HOST = '127.0.0.1'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 造一个「每次调用现读宿主端口」的端点解析器。
|
|
31
|
+
*
|
|
32
|
+
* @param readPort 读宿主监听端口(`webServer.port`)。取不到时抛错而不是猜。
|
|
33
|
+
* @param path 端点路径前缀,默认 `/v1`。
|
|
34
|
+
*/
|
|
35
|
+
export function loopbackBaseURL(readPort: () => number | undefined, path: string = '/v1'): () => string {
|
|
36
|
+
return () => {
|
|
37
|
+
const port = readPort()
|
|
38
|
+
if (port === undefined) {
|
|
39
|
+
throw new Error('dsh-router: 宿主 webServer 端口未就绪(webServer.port 读不到),无法定位 /v1 端点')
|
|
40
|
+
}
|
|
41
|
+
return `http://${LOOPBACK_HOST}:${String(port)}${path}`
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/host-version.test.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 宿主版本探测测试 ——
|
|
2
|
+
* 宿主版本探测测试 —— 锁死那条**版本地板**(>= 0.2.0)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 这条判据现在只有一个用途:**在老宿主上出声**。兼容分叉(三处 `>= 0.1.7`)随
|
|
5
|
+
* 0.2.0 单面编写一起删掉了,所以本测试守的是「低于地板会被判为不支持 → apply 时
|
|
6
|
+
* 打警告」这条报警链,而不是某个功能开关。
|
|
7
|
+
*
|
|
8
|
+
* 地板抬到 0.2.0 的那一刻,0.1.7 从「支持」翻成「不支持」——这条断言就是那次
|
|
9
|
+
* 翻面的记录:谁想再加回 0.1.x 兼容,本测试会先红。
|
|
7
10
|
*/
|
|
8
11
|
import test from 'node:test'
|
|
9
12
|
import assert from 'node:assert/strict'
|
|
@@ -11,7 +14,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
|
11
14
|
import { tmpdir } from 'node:os'
|
|
12
15
|
import { join } from 'node:path'
|
|
13
16
|
import { pathToFileURL } from 'node:url'
|
|
14
|
-
import { detectHostVersion,
|
|
17
|
+
import { detectHostVersion, isHostSupported } from './host-version.ts'
|
|
15
18
|
|
|
16
19
|
/** 造一个「装了指定版本 dsh」的临时 profile,返回它的 file: URL。 */
|
|
17
20
|
function profileWithDsh(version: string | null): string {
|
|
@@ -25,37 +28,27 @@ function profileWithDsh(version: string | null): string {
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
test('读得到版本:detectHostVersion 返回 profile 里装的 dsh 版本', () => {
|
|
28
|
-
assert.equal(detectHostVersion(profileWithDsh('0.1.
|
|
29
|
-
assert.equal(detectHostVersion(profileWithDsh('0.
|
|
31
|
+
assert.equal(detectHostVersion(profileWithDsh('0.1.7-rc.2')), '0.1.7-rc.2')
|
|
32
|
+
assert.equal(detectHostVersion(profileWithDsh('0.2.0-rc.1')), '0.2.0-rc.1')
|
|
30
33
|
})
|
|
31
34
|
|
|
32
|
-
test('
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
assert.equal(supportsLastHitDock(profileWithDsh('1.0.0')), true)
|
|
37
|
-
// 低于 0.1.7 → 不挂(0.1.5 位置不对,宁可不显示)
|
|
38
|
-
assert.equal(supportsLastHitDock(profileWithDsh('0.1.5-rc.3')), false)
|
|
39
|
-
assert.equal(supportsLastHitDock(profileWithDsh('0.1.6-alpha.2')), false)
|
|
40
|
-
assert.equal(supportsLastHitDock(profileWithDsh('0.1.4')), false)
|
|
35
|
+
test('地板:>= 0.2.0 才算支持(预发布标识忽略,按前导数字比)', () => {
|
|
36
|
+
for (const v of ['0.2.0-rc.1', '0.2.0', '0.2.1', '1.0.0']) {
|
|
37
|
+
assert.equal(isHostSupported(profileWithDsh(v)), true, v)
|
|
38
|
+
}
|
|
41
39
|
})
|
|
42
40
|
|
|
43
|
-
test('
|
|
44
|
-
for (const v of ['0.1.
|
|
45
|
-
assert.equal(
|
|
46
|
-
assert.equal(supportsLastHitDock(profileWithDsh(v)), true, v)
|
|
47
|
-
}
|
|
48
|
-
for (const v of ['0.1.5-rc.3', '0.1.6-alpha.2', '0.1.4']) {
|
|
49
|
-
assert.equal(isHost017Plus(profileWithDsh(v)), false, v)
|
|
50
|
-
assert.equal(supportsLastHitDock(profileWithDsh(v)), false, v)
|
|
41
|
+
test('地板之下(0.1.5/0.1.6/0.1.7)判为不支持 —— 会打警告而不是静默走错路径', () => {
|
|
42
|
+
for (const v of ['0.1.5-rc.3', '0.1.6-alpha.2', '0.1.7-rc.2', '0.1.4']) {
|
|
43
|
+
assert.equal(isHostSupported(profileWithDsh(v)), false, v)
|
|
51
44
|
}
|
|
52
45
|
})
|
|
53
46
|
|
|
54
|
-
test('读不到版本 →
|
|
47
|
+
test('读不到版本 → 判为不支持(无法确认也值得说一声),且不抛', () => {
|
|
55
48
|
assert.equal(detectHostVersion(profileWithDsh(null)), undefined)
|
|
56
|
-
assert.equal(
|
|
49
|
+
assert.equal(isHostSupported(profileWithDsh(null)), false)
|
|
57
50
|
// 垃圾版本号
|
|
58
|
-
assert.equal(
|
|
51
|
+
assert.equal(isHostSupported(profileWithDsh('not-a-version')), false)
|
|
59
52
|
// baseUrl 缺失(冷启动/单测)→ 回落 ~/.dsh/profiles/web,不抛
|
|
60
|
-
assert.doesNotThrow(() =>
|
|
53
|
+
assert.doesNotThrow(() => isHostSupported(undefined))
|
|
61
54
|
})
|
package/src/host-version.ts
CHANGED
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 宿主 DSH 版本探测 ——
|
|
2
|
+
* 宿主 DSH 版本探测 —— 现在只剩两件事:**报版本**(日志 + `/router/api/health`)与
|
|
3
|
+
* **守版本地板**。
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* ——「这是 0.1.7 还是更老」——却各检各的、还容易误判(插件自己 node_modules
|
|
6
|
-
* 里的依赖版本与宿主实际加载的版本并不一致:实测插件解析到 schemastery 3.18.4,
|
|
7
|
-
* profile 里却是 3.18.3、全局是 3.18.2)。所以统一改成**读宿主版本号**判定:
|
|
5
|
+
* ## 为什么不再有兼容分叉
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* 原来这里有三处 `>= 0.1.7` 的分叉(设置卡片注册路径、「最近命中」徽章座位、adapter
|
|
8
|
+
* 的工具结果消息形态),彼此独立判断、还容易误判。本插件现已按 **0.2.0 单面**编写,
|
|
9
|
+
* 三处的旧分支全部删除:
|
|
10
|
+
*
|
|
11
|
+
* 1. 设置卡片:0.1.5 走 `settings.installSection`,0.1.7+ 走导出的 volatile Config。
|
|
12
|
+
* 0.2.0 只有后者 → installSection 分支删除。
|
|
13
|
+
* 2. 「最近命中」徽章:0.1.5 渲染在 InputBar 之后的独立块(位置不对),0.1.7+ 才在
|
|
14
|
+
* composer 底部操作行。0.2.0 起无条件挂载,不再问支持位。
|
|
15
|
+
* 3. adapter 工具结果形态:旧的「user 内嵌 tool-result 块」0.1.7 起退役;0.2.0 的
|
|
16
|
+
* v3→v4 会话迁移还主动拒绝该包装,所以它到不了 adapter → 分支删除。
|
|
17
|
+
*
|
|
18
|
+
* ## 为什么仍要读版本
|
|
19
|
+
*
|
|
20
|
+
* 因为**跑在老宿主上必须出声**。删掉分叉不等于「老宿主也能跑」:0.1.x 上本插件会
|
|
21
|
+
* 静默走错路径(卡片不出、徽章错位、工具结果丢失),而用户看到的现象与「插件坏了」
|
|
22
|
+
* 无法区分。所以这里只保留一个判据 `isHostSupported`(>= 0.2.0),仅用于 apply 时
|
|
23
|
+
* 打一条醒目的警告 —— 不是兼容,是可诊断。
|
|
14
24
|
*
|
|
15
25
|
* 读法:宿主把 profile 目录挂在 `ctx.baseUrl`(见 data-dir.ts)。本插件跑在宿主
|
|
16
26
|
* 进程内,`require.resolve('@deepseek-ai/dsh/package.json', { paths: [profile] })`
|
|
17
|
-
* 会沿 profile 的 node_modules
|
|
18
|
-
*
|
|
27
|
+
* 会沿 profile 的 node_modules 向上解析到全局安装根。拿不到就回落直接拼路径,再不行
|
|
28
|
+
* 返回 undefined(按「无法确认」处理,不抛)。
|
|
19
29
|
*
|
|
20
|
-
* 注意:**只能在 `apply(ctx)` 里用**(那时才有 baseUrl
|
|
21
|
-
*
|
|
30
|
+
* 注意:**只能在 `apply(ctx)` 里用**(那时才有 baseUrl)。模块顶层解析到的是插件自己
|
|
31
|
+
* 的依赖,不能代表宿主。
|
|
22
32
|
*/
|
|
23
33
|
import { readFileSync } from 'node:fs'
|
|
24
34
|
import { createRequire } from 'node:module'
|
|
25
35
|
import { join } from 'node:path'
|
|
26
36
|
import { profileDirOf } from './data-dir.ts'
|
|
27
37
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
const V0_1_7: readonly number[] = [0, 1, 7]
|
|
38
|
+
/** 版本地板:低于此版本的宿主本插件不再兼容(0.1.5/0.1.6/0.1.7 全部落此下)。 */
|
|
39
|
+
const V0_2_0: readonly number[] = [0, 2, 0]
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
42
|
* 解析并比较 semver(只取前导数字段,预发布标识忽略)。
|
|
34
|
-
* `0.
|
|
43
|
+
* `0.2.0-rc.1` → [0,2,0],`0.1.7-alpha.2` → [0,1,7]。
|
|
35
44
|
* 返回负数/0/正数,语义同 `<`/`==`/`>`。
|
|
36
45
|
*/
|
|
37
46
|
function compareVersion(a: readonly number[], b: readonly number[]): number {
|
|
@@ -43,7 +52,7 @@ function compareVersion(a: readonly number[], b: readonly number[]): number {
|
|
|
43
52
|
return 0
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
/** `0.
|
|
55
|
+
/** `0.2.0-rc.1` → [0,2,0];解析失败返回 null。 */
|
|
47
56
|
function parseVersion(raw: string): number[] | null {
|
|
48
57
|
const m = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(raw.trim())
|
|
49
58
|
if (m === null) return null
|
|
@@ -53,11 +62,11 @@ function parseVersion(raw: string): number[] | null {
|
|
|
53
62
|
/**
|
|
54
63
|
* 探测 profile 里安装的 DSH 宿主版本。
|
|
55
64
|
* @param baseUrl 宿主注入的 profile 锚点(`ctx.baseUrl`)
|
|
56
|
-
* @returns 形如 `0.
|
|
65
|
+
* @returns 形如 `0.2.0-rc.1`;读不到返回 undefined
|
|
57
66
|
*/
|
|
58
67
|
export function detectHostVersion(baseUrl?: string): string | undefined {
|
|
59
68
|
const profile = profileDirOf(baseUrl)
|
|
60
|
-
// 先走模块解析(能沿 node_modules
|
|
69
|
+
// 先走模块解析(能沿 node_modules 向上找到全局安装根),再回落直接路径。
|
|
61
70
|
const candidates: string[] = []
|
|
62
71
|
try {
|
|
63
72
|
candidates.push(createRequire(join(profile, 'noop.js')).resolve('@deepseek-ai/dsh/package.json'))
|
|
@@ -76,26 +85,17 @@ export function detectHostVersion(baseUrl?: string): string | undefined {
|
|
|
76
85
|
return undefined
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
88
|
+
/**
|
|
89
|
+
* 宿主是否在本插件支持的地板之上(>= 0.2.0)。
|
|
90
|
+
*
|
|
91
|
+
* **只用于报警,不用于分流** —— 低于地板时插件照常加载,但要在日志里说清「这台宿主
|
|
92
|
+
* 不再兼容、会出现什么症状」,把静默的错变成可查的错。读不到版本 → false(当作
|
|
93
|
+
* 「无法确认」,同样值得说一句)。
|
|
94
|
+
*/
|
|
95
|
+
export function isHostSupported(baseUrl?: string): boolean {
|
|
81
96
|
const v = detectHostVersion(baseUrl)
|
|
82
97
|
if (v === undefined) return false
|
|
83
98
|
const parsed = parseVersion(v)
|
|
84
99
|
if (parsed === null) return false
|
|
85
|
-
return compareVersion(parsed,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 宿主是否 >= 0.1.7。三处兼容分叉共用这一个判据:
|
|
90
|
-
* - 「最近命中」徽章(composer.dock 位置正确)
|
|
91
|
-
* - 设置命名空间(Config 驱动而非 installSection)
|
|
92
|
-
* - adapter 的独立 role:'tool' 消息形态
|
|
93
|
-
*/
|
|
94
|
-
export function isHost017Plus(baseUrl?: string): boolean {
|
|
95
|
-
return atLeast(baseUrl, V0_1_7)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** 「最近命中」徽章是否可用(= 宿主 >= 0.1.7)。 */
|
|
99
|
-
export function supportsLastHitDock(baseUrl?: string): boolean {
|
|
100
|
-
return isHost017Plus(baseUrl)
|
|
100
|
+
return compareVersion(parsed, V0_2_0) >= 0
|
|
101
101
|
}
|