@tokensapi/dsh-plugin-check 0.3.3 → 0.4.0
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 +116 -95
- package/bin/dsh-plugin-check.mjs +26 -12
- package/docs/CHECKS.md +335 -0
- package/lib/client-check.mjs +98 -0
- package/lib/contract.mjs +210 -0
- package/lib/cowork-plugin.mjs +118 -113
- package/lib/manifest-check.mjs +105 -69
- package/lib/patch-rows.mjs +126 -31
- package/lib/registry-check.mjs +19 -146
- package/lib/semver-rules.mjs +129 -0
- package/lib/smoke-check.mjs +13 -1
- package/lib/tarball.mjs +5 -1
- package/package.json +45 -44
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* M12-client:dsh.client 声明
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 为什么这条是 error 而不是"你自己的事":宿主的 client-modules 在
|
|
5
|
+
* **构造期同步**解析所有已加载包的 dsh.client(parseDshClient,
|
|
6
|
+
* packages/client/modules/src/index.ts:200-221),一份声明写坏会抛出
|
|
7
|
+
* 并让整个 client-modules fiber FAIL —— 受害的不止这个插件,而是同
|
|
8
|
+
* 一宿主里所有需要前端模块的插件。
|
|
9
|
+
*
|
|
10
|
+
* 字段全集就是 { platform, inject?, external?, immediately? }
|
|
11
|
+
* (同文件 :51-64),未知键被忽略。
|
|
12
|
+
* ============================================================ */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 上游 exactPackageSpecifier(index.ts:192-198):只接受精确的裸包根,
|
|
16
|
+
* scope 包恰好两段、非 scope 包不含 "/"。
|
|
17
|
+
* @param {string} specifier
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function isExactPackageSpecifier(specifier) {
|
|
21
|
+
if (specifier.startsWith('@')) {
|
|
22
|
+
const parts = specifier.split('/')
|
|
23
|
+
return parts.length === 2 && parts.every(Boolean)
|
|
24
|
+
}
|
|
25
|
+
return specifier.length > 0 && !specifier.includes('/')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 上游 clientExportOf(index.ts:224-234)接受的两种形状。 */
|
|
29
|
+
function clientExportProblem(exportsField) {
|
|
30
|
+
if (typeof exportsField !== 'object' || exportsField === null) return 'missing'
|
|
31
|
+
const client = exportsField['./client']
|
|
32
|
+
if (client === undefined) return 'missing'
|
|
33
|
+
if (typeof client === 'string') return undefined
|
|
34
|
+
if (typeof client === 'object' && client !== null && typeof client.default === 'string') return undefined
|
|
35
|
+
return 'malformed'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 对一份清单执行 M12 判定。
|
|
40
|
+
* 没有 dsh.client 的插件(绝大多数)直接返回空,不产生任何噪音。
|
|
41
|
+
* @param {object} manifest - package.json / registry 版本文档。
|
|
42
|
+
* @returns {Array<{level: 'error'|'warning', rule: string, message: string}>}
|
|
43
|
+
*/
|
|
44
|
+
export function checkClientDeclaration(manifest) {
|
|
45
|
+
const findings = []
|
|
46
|
+
const error = (message) => findings.push({ level: 'error', rule: 'M12-client', message })
|
|
47
|
+
const warning = (message) => findings.push({ level: 'warning', rule: 'M12-client', message })
|
|
48
|
+
|
|
49
|
+
const declaration = manifest.dsh?.client
|
|
50
|
+
if (declaration === undefined) return findings
|
|
51
|
+
|
|
52
|
+
if (typeof declaration !== 'object' || declaration === null || Array.isArray(declaration)) {
|
|
53
|
+
error('dsh.client 不是对象;宿主解析到非对象声明会同步抛错,'
|
|
54
|
+
+ '连带让整个 client-modules 失败(同宿主其他插件的前端模块一起挂)')
|
|
55
|
+
return findings
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof declaration.platform !== 'string') {
|
|
59
|
+
error('dsh.client.platform 缺失或不是字符串;宿主 parseDshClient 会同步抛错')
|
|
60
|
+
} else if (declaration.platform !== 'web') {
|
|
61
|
+
warning(`dsh.client.platform 是 "${declaration.platform}";`
|
|
62
|
+
+ '宿主只把 platform === "web" 的行当作前端模块,其他取值等于这段声明不会生效')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const field of ['inject', 'external']) {
|
|
66
|
+
const value = declaration[field]
|
|
67
|
+
if (value === undefined) continue
|
|
68
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
|
69
|
+
error(`dsh.client.${field} 必须是字符串数组`)
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (field !== 'external') continue
|
|
73
|
+
const packageName = typeof manifest.name === 'string' ? manifest.name : ''
|
|
74
|
+
for (const item of value) {
|
|
75
|
+
if (!isExactPackageSpecifier(item)) {
|
|
76
|
+
error(`dsh.client.external 的 "${item}" 不是精确的裸包名;`
|
|
77
|
+
+ 'scope 包必须恰好两段(@scope/name),非 scope 包不能含 "/"')
|
|
78
|
+
} else if (packageName !== '' && item === packageName) {
|
|
79
|
+
error(`dsh.client.external 不能包含本包自身 "${item}";`
|
|
80
|
+
+ '宿主会判定这一行"请求了自己提供的模块"并抛错')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (declaration.immediately !== undefined && typeof declaration.immediately !== 'boolean') {
|
|
86
|
+
error('dsh.client.immediately 必须是布尔值')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const exportProblem = clientExportProblem(manifest.exports)
|
|
90
|
+
if (exportProblem === 'missing') {
|
|
91
|
+
error('声明了 dsh.client 却没有 exports["./client"];'
|
|
92
|
+
+ '宿主会以 "declares dsh.client but exports no ./client bundle" 抛错')
|
|
93
|
+
} else if (exportProblem === 'malformed') {
|
|
94
|
+
error('exports["./client"] 必须是字符串,或带字符串 default 的对象')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return findings
|
|
98
|
+
}
|
package/lib/contract.mjs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* 上游契约基线
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 这个工具的每条规则都不是写法偏好,而是照着某个上游版本的真实
|
|
5
|
+
* 行为写下的判定。上游一变,规则可能要跟着变,也可能完全不用变
|
|
6
|
+
* —— 区别只有"回去看一眼"才知道。所以把「照着哪个版本核对的」
|
|
7
|
+
* 做成机器可读的一份数据,跟着每次体检结果一起输出;目标运行时
|
|
8
|
+
* 落在核对过的范围之外时明确说"没核对过",而不是假装照样有效。
|
|
9
|
+
*
|
|
10
|
+
* 每条规则锚在上游哪个文件/行:见 docs/CHECKS.md。
|
|
11
|
+
* 上游升级后的逐条复核流程:见同一文档末尾「上游变动后怎么复核」。
|
|
12
|
+
* ============================================================ */
|
|
13
|
+
|
|
14
|
+
/** 上游契约基线:最近一次逐条核对上游代码时的事实快照。 */
|
|
15
|
+
export const CONTRACT = {
|
|
16
|
+
/** 最近一次逐条核对上游代码的日期。 */
|
|
17
|
+
verifiedAt: '2026-09-10',
|
|
18
|
+
/** 核对时上游各包的版本;规则的判定口径照着这些版本的代码写。 */
|
|
19
|
+
upstream: {
|
|
20
|
+
'dsh-plugin-desktop': '2.0.5',
|
|
21
|
+
'dsh-community-market': '0.1.0-dev.0',
|
|
22
|
+
'@deepseek-ai/cordis': '4.0.2',
|
|
23
|
+
'@deepseek-ai/dsh-tools': '0.1.3-alpha.1',
|
|
24
|
+
pnpm: '11.8.0',
|
|
25
|
+
},
|
|
26
|
+
/** 实际核对过的 DSH 运行时版本。 */
|
|
27
|
+
runtimes: ['0.1.0-rc.8', '0.1.3-alpha.1'],
|
|
28
|
+
/** 核对过的运行时区间;超出只代表未核对,不代表不可用。 */
|
|
29
|
+
runtimeRange: '>=0.1.0-rc.8 <0.2.0',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 一行人读的基线摘要,供 CLI 抬头与会话内结果展示。 */
|
|
33
|
+
export function contractLine() {
|
|
34
|
+
const { upstream } = CONTRACT
|
|
35
|
+
return `契约基线 ${CONTRACT.verifiedAt} · desktop ${upstream['dsh-plugin-desktop']}`
|
|
36
|
+
+ ` / market ${upstream['dsh-community-market']}`
|
|
37
|
+
+ ` · 已核对运行时 ${CONTRACT.runtimes.join('、')}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 目标运行时是否在核对过的范围内。
|
|
42
|
+
* 不做区间数学:只认"确实核对过的那几个版本",别的一律如实说
|
|
43
|
+
* 没核对过 —— 这条提示的价值就在于不替上游的未知变动打包票。
|
|
44
|
+
* @param {string | undefined} runtime - 目标 DSH 运行时版本。
|
|
45
|
+
* @returns {Array<{level: 'warning', rule: string, message: string}>}
|
|
46
|
+
*/
|
|
47
|
+
export function checkContractBaseline(runtime) {
|
|
48
|
+
if (runtime === undefined || runtime === '') return []
|
|
49
|
+
if (CONTRACT.runtimes.includes(runtime)) return []
|
|
50
|
+
return [{
|
|
51
|
+
level: 'warning',
|
|
52
|
+
rule: 'C0-contract',
|
|
53
|
+
message: `目标运行时 ${runtime} 不在本工具核对过的版本里`
|
|
54
|
+
+ `(已核对 ${CONTRACT.runtimes.join('、')},核对日期 ${CONTRACT.verifiedAt});`
|
|
55
|
+
+ `规则仍按 ${CONTRACT.runtimeRange} 的上游行为判定,上游若已改动,结论可能过时或过严。`
|
|
56
|
+
+ `复核清单见 docs/CHECKS.md`,
|
|
57
|
+
}]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 安装期生命周期脚本。曾经是市场硬红线,核对下来现在不是了。
|
|
62
|
+
* 上游 dsh-community-market 0.1.0-dev.0 的 npm 验证器已不再看
|
|
63
|
+
* scripts;受控安装是 `pnpm add --save-exact --registry=…`,没有
|
|
64
|
+
* --ignore-scripts,而 Profile 也没有配 onlyBuiltDependencies 允许
|
|
65
|
+
* 名单。两种结局都不好,但都不是"上架被拒",所以降为 warning:
|
|
66
|
+
* - 脚本真执行 → 安装即在用户机器上跑任意代码;
|
|
67
|
+
* - 脚本被包管理器默认策略拦下 → 依赖脚本产物的插件装完即坏。
|
|
68
|
+
*/
|
|
69
|
+
export const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare']
|
|
70
|
+
|
|
71
|
+
/** M2 的措辞:两条安装路径行为不同,分开说清楚。 */
|
|
72
|
+
export function lifecycleMessage(script) {
|
|
73
|
+
if (script === 'prepare') {
|
|
74
|
+
return 'prepare 脚本:从 npm tarball 安装时不执行,从 github: 源安装时执行;'
|
|
75
|
+
+ '同一个包两条安装路径行为不一致,构建请改用 prepack'
|
|
76
|
+
}
|
|
77
|
+
return `${script} 脚本:宿主受控安装是 pnpm add(未加 --ignore-scripts),`
|
|
78
|
+
+ `要么在用户机器上执行任意代码,要么被包管理器默认策略拦下、脚本产物缺失导致插件装完即坏;`
|
|
79
|
+
+ `构建请改用 prepack`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ============================================================
|
|
83
|
+
* 上游谓词的本地副本
|
|
84
|
+
* ============================================================
|
|
85
|
+
* 下面几个判定逐字照抄上游,注释里标出锚点。照抄而不是"按理解重写"
|
|
86
|
+
* 是有意的:上游改了这几行,diff 才看得出来我们跟着漂了没有。
|
|
87
|
+
* ============================================================ */
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 携带身份语义的核心包:cordis 上下文与 dsh 服务在宿主/插件各持一份时
|
|
91
|
+
* Symbol 不等,注册对不上。schemastery、cosmokit 等纯工具库不在此列,
|
|
92
|
+
* 插件可以正常依赖。
|
|
93
|
+
*/
|
|
94
|
+
export const CORE_SCOPE = '@deepseek-ai/'
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 是否为带身份语义的核心包(M3 / M7 共用)。
|
|
98
|
+
* 曾经 manifest-check 与 registry-check 各存一份,这里合并为一处。
|
|
99
|
+
* @param {string} name - npm 包名。
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
export function isIdentityCore(name) {
|
|
103
|
+
return name === `${CORE_SCOPE}cordis`
|
|
104
|
+
|| name.startsWith(`${CORE_SCOPE}cordis-`)
|
|
105
|
+
|| name.startsWith(`${CORE_SCOPE}dsh`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 是否与 DSH 运行时**同一条版本线**。
|
|
110
|
+
*
|
|
111
|
+
* 这条区分不能省:核心包里只有 @deepseek-ai/dsh* 跟着 DSH 运行时版本
|
|
112
|
+
* 走(0.1.3-alpha.1 这类),而 @deepseek-ai/cordis 自成一条 4.x 线 ——
|
|
113
|
+
* 拿 "4.0.1 || 4.0.2" 去比对运行时 0.1.3-alpha.1,得到的"不含目标运行时"
|
|
114
|
+
* 是范畴错误,而不是插件的问题(本包自己的清单就是这个形状)。
|
|
115
|
+
* 所以 M7 的**范围合法性**覆盖全部核心包,**是否含目标运行时**只对这条线判。
|
|
116
|
+
* @param {string} name - npm 包名。
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
export function isRuntimeVersioned(name) {
|
|
120
|
+
return name.startsWith(`${CORE_SCOPE}dsh`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 上游 dsh-community-market/src/install/service.ts:26 的 PACKAGE_NAME_PATTERN。 */
|
|
124
|
+
export const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 上游 desktop-plugins.ts:193 的 safePackageName(比市场侧多一条 214 长度限制)。
|
|
128
|
+
* @param {unknown} value
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
export function safePackageName(value) {
|
|
132
|
+
return typeof value === 'string' && value.length <= 214 && PACKAGE_NAME_PATTERN.test(value)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 永远装不进市场的产品包名。
|
|
137
|
+
* 上游 service.ts:35 BLOCKED_PRODUCT_PACKAGES + desktop-plugins.ts IMMUTABLE_BUNDLES
|
|
138
|
+
* 里对第三方有意义的那几个(宿主自身与市场自身)。
|
|
139
|
+
*/
|
|
140
|
+
export const BLOCKED_PACKAGE_NAMES = new Set([
|
|
141
|
+
'dsh-plugin-desktop',
|
|
142
|
+
'dsh-plugin-desktop-beta',
|
|
143
|
+
'dsh-community-market',
|
|
144
|
+
'@deepseek-ai/dsh-desktop-app',
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 上游 dsh-community-market/src/install/service.ts:268 safeBundlePatch,逐字照抄。
|
|
149
|
+
* @param {unknown} value - dsh.bundle.patch 的值。
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
export function safeBundlePatch(value) {
|
|
153
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 512 || value.includes('\0')) return false
|
|
154
|
+
const path = value.startsWith('./') ? value.slice(2) : value
|
|
155
|
+
return path.length > 0
|
|
156
|
+
&& !path.startsWith('/')
|
|
157
|
+
&& !path.includes('\\')
|
|
158
|
+
&& path.split('/').every(segment => segment.length > 0 && segment !== '.' && segment !== '..' && !segment.includes(':'))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* safeBundlePatch 不通过时,说清楚具体违反了哪一条 —— 只说"不合法"
|
|
163
|
+
* 作者无从下手。
|
|
164
|
+
* @param {string} value - 已确认为非空字符串的 patch 路径。
|
|
165
|
+
* @returns {string | undefined} 违规原因,合法时返回 undefined。
|
|
166
|
+
*/
|
|
167
|
+
export function bundlePatchProblem(value) {
|
|
168
|
+
if (safeBundlePatch(value)) return undefined
|
|
169
|
+
if (value.length > 512) return '超过 512 字符'
|
|
170
|
+
if (value.includes('\0')) return '含 NUL 字节'
|
|
171
|
+
const path = value.startsWith('./') ? value.slice(2) : value
|
|
172
|
+
if (path.length === 0) return '去掉 "./" 之后为空'
|
|
173
|
+
if (path.startsWith('/')) return '是绝对路径(必须相对于包根)'
|
|
174
|
+
if (path.includes('\\')) return '含反斜杠(必须用 "/" 分隔,Windows 风格路径不接受)'
|
|
175
|
+
const segments = path.split('/')
|
|
176
|
+
if (segments.some(segment => segment.length === 0)) return '含空路径段(如重复的 "//")'
|
|
177
|
+
if (segments.some(segment => segment === '.' || segment === '..')) return '含 "." 或 ".." 路径段(不得越出包根)'
|
|
178
|
+
if (segments.some(segment => segment.includes(':'))) return '路径段含 ":"'
|
|
179
|
+
return '不满足上游 safeBundlePatch'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 宿主保留的 Loader 行 id。撞上的后果不是"这个插件不生效":
|
|
184
|
+
* - settings / web-runtime:dsh-plugin-desktop 直接抛错,Desktop 起不来
|
|
185
|
+
* (profile.ts:955-958、977-979);
|
|
186
|
+
* - 市场行 id:该行被静默剥离,并让整个 Market provider 以
|
|
187
|
+
* "conflicting Market provider Loader identity was removed" 失败
|
|
188
|
+
* (profile.ts:704-750、913)。
|
|
189
|
+
*/
|
|
190
|
+
export const RESERVED_ROW_IDS = new Set([
|
|
191
|
+
'settings',
|
|
192
|
+
'web-runtime',
|
|
193
|
+
'desktop-webserver',
|
|
194
|
+
// DESKTOP_MARKET_IDENTITIES.{community,dshMarket}.rowId(desktop-market.ts:31,36)
|
|
195
|
+
'community-market',
|
|
196
|
+
'dsh-market',
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 宿主保留的 Loader 行 name(MARKET_PACKAGE_NAMES,desktop-market.ts:32,37)。
|
|
201
|
+
* 补丁行的 name 撞上同样触发 Market provider 失败;不过这类 name 本来就
|
|
202
|
+
* 会先被 M5 拦在"超出本包命名空间"上,这里保留是为了措辞更准确。
|
|
203
|
+
*/
|
|
204
|
+
export const RESERVED_ROW_NAMES = new Set([
|
|
205
|
+
'dsh-community-market',
|
|
206
|
+
'dshmarket',
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
/** 宿主 dsh-plugin-desktop 2.0.5 的 engines.node。 */
|
|
210
|
+
export const HOST_NODE_ENGINES = '^22.19.0 || >=24.0.0'
|
package/lib/cowork-plugin.mjs
CHANGED
|
@@ -1,113 +1,118 @@
|
|
|
1
|
-
/* ============================================================
|
|
2
|
-
* Cowork 插件入口:会话内插件体检工具
|
|
3
|
-
* ============================================================
|
|
4
|
-
* 装进 TokensCowork 后注册 plugin_check 工具:对本地插件目录或
|
|
5
|
-
* 任意 npm 插件包
|
|
6
|
-
* 跑清单规则 + 包内补丁行检查(与 CLI 的清单阶段同源),模型或
|
|
7
|
-
* 用户在会话里即可判断"这个插件装进来会不会有问题"。补丁行会
|
|
8
|
-
* 从 tarball 里真读出来 —— 只看 registry 清单会漏掉"补丁行 name
|
|
9
|
-
* 写成插件名"这类装上即崩的缺陷。含隔离启动冒烟的完整体检仍走
|
|
10
|
-
* CLI:npx @tokensapi/dsh-plugin-check --package <name@ver>。
|
|
11
|
-
* ============================================================ */
|
|
12
|
-
import { resolve } from 'node:path'
|
|
13
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
14
|
-
import { checkManifest } from './manifest-check.mjs'
|
|
15
|
-
import { checkPublishedPackage } from './registry-check.mjs'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* @param {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
+ '
|
|
53
|
-
+ '
|
|
54
|
-
+ '
|
|
55
|
-
+ '
|
|
56
|
-
+ '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* Cowork 插件入口:会话内插件体检工具
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 装进 TokensCowork 后注册 plugin_check 工具:对本地插件目录或
|
|
5
|
+
* 任意 npm 插件包
|
|
6
|
+
* 跑清单规则 + 包内补丁行检查(与 CLI 的清单阶段同源),模型或
|
|
7
|
+
* 用户在会话里即可判断"这个插件装进来会不会有问题"。补丁行会
|
|
8
|
+
* 从 tarball 里真读出来 —— 只看 registry 清单会漏掉"补丁行 name
|
|
9
|
+
* 写成插件名"这类装上即崩的缺陷。含隔离启动冒烟的完整体检仍走
|
|
10
|
+
* CLI:npx @tokensapi/dsh-plugin-check --package <name@ver>。
|
|
11
|
+
* ============================================================ */
|
|
12
|
+
import { resolve } from 'node:path'
|
|
13
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
14
|
+
import { checkManifest } from './manifest-check.mjs'
|
|
15
|
+
import { checkPublishedPackage } from './registry-check.mjs'
|
|
16
|
+
import { CONTRACT, checkContractBaseline } from './contract.mjs'
|
|
17
|
+
|
|
18
|
+
/** 缺省比对的运行时版本;宿主升级后可通过插件配置覆盖。 */
|
|
19
|
+
const DEFAULT_RUNTIME = '0.1.3-alpha.1'
|
|
20
|
+
const REGISTRY = 'https://registry.npmjs.org'
|
|
21
|
+
const FETCH_TIMEOUT_MS = 15_000
|
|
22
|
+
|
|
23
|
+
export const name = 'tokens-plugin-check'
|
|
24
|
+
export const inject = ['tools']
|
|
25
|
+
|
|
26
|
+
async function fetchManifest(packageName, version) {
|
|
27
|
+
const controller = new AbortController()
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(
|
|
31
|
+
`${REGISTRY}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`,
|
|
32
|
+
{ signal: controller.signal, headers: { accept: 'application/json' } },
|
|
33
|
+
)
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(response.status === 404 ? `npm 上未找到 ${packageName}@${version}` : `npm 查询失败(HTTP ${response.status})`)
|
|
36
|
+
}
|
|
37
|
+
return await response.json()
|
|
38
|
+
} finally {
|
|
39
|
+
clearTimeout(timer)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
45
|
+
* @param {{ runtime?: string }} [config]
|
|
46
|
+
*/
|
|
47
|
+
export function apply(ctx, config = {}) {
|
|
48
|
+
const runtime = typeof config.runtime === 'string' && config.runtime !== '' ? config.runtime : DEFAULT_RUNTIME
|
|
49
|
+
ctx.tools.register(defineTool({
|
|
50
|
+
name: 'plugin_check',
|
|
51
|
+
description: 'Check whether a DSH/TokensCowork plugin is safe to install: forbidden lifecycle '
|
|
52
|
+
+ 'scripts, dual-kernel core dependencies, missing or out-of-scope cordis patch rows, and '
|
|
53
|
+
+ 'runtime/peer compatibility against the current DSH runtime. Returns ok=false with per-rule '
|
|
54
|
+
+ 'findings when the plugin would break the host. '
|
|
55
|
+
+ 'Pass "path" for a plugin the user is developing locally (checks the working tree, no publish '
|
|
56
|
+
+ 'needed) — prefer this whenever the user asks about their own plugin before releasing it. '
|
|
57
|
+
+ 'Pass "package" for an already published npm package. Exactly one of the two is required.',
|
|
58
|
+
parameters: {
|
|
59
|
+
path: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Local plugin directory containing package.json. Use for unpublished/in-development plugins.',
|
|
62
|
+
},
|
|
63
|
+
package: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Published npm package name, e.g. "@tokensapi/dsh-progressive-tools".',
|
|
66
|
+
},
|
|
67
|
+
version: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Exact version to check with "package". Defaults to the latest dist-tag.',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
output: {
|
|
73
|
+
schema: { type: 'json' },
|
|
74
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
75
|
+
},
|
|
76
|
+
isConcurrencySafe: () => true,
|
|
77
|
+
async execute(args) {
|
|
78
|
+
const hasPath = typeof args.path === 'string' && args.path !== ''
|
|
79
|
+
const hasPackage = typeof args.package === 'string' && args.package !== ''
|
|
80
|
+
if (hasPath === hasPackage) {
|
|
81
|
+
throw new Error('plugin_check 需要且只需要一个目标:本地目录传 path,已发布包传 package')
|
|
82
|
+
}
|
|
83
|
+
if (hasPath) {
|
|
84
|
+
// 本地工作区体检:发版前就能判,不需要先 publish。隔离冒烟要装
|
|
85
|
+
// 依赖起子进程,宿主进程里不做,如实告知走 CLI。
|
|
86
|
+
const local = checkManifest(resolve(args.path), { runtime })
|
|
87
|
+
const localFindings = [...checkContractBaseline(runtime), ...local.findings]
|
|
88
|
+
return {
|
|
89
|
+
path: resolve(args.path),
|
|
90
|
+
package: local.manifest?.name,
|
|
91
|
+
version: local.manifest?.version,
|
|
92
|
+
runtime,
|
|
93
|
+
contract: CONTRACT,
|
|
94
|
+
ok: !localFindings.some(finding => finding.level === 'error'),
|
|
95
|
+
inspectedPatch: true,
|
|
96
|
+
findings: localFindings,
|
|
97
|
+
note: '以上为清单规则 + 补丁行检查(读的是本地工作区);隔离启动冒烟请在插件目录运行 '
|
|
98
|
+
+ 'npx @tokensapi/dsh-plugin-check --runtime ' + runtime,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const manifest = await fetchManifest(args.package, args.version ?? 'latest')
|
|
102
|
+
const { findings, inspectedPatch } = await checkPublishedPackage(manifest, runtime)
|
|
103
|
+
const published = [...checkContractBaseline(runtime), ...findings]
|
|
104
|
+
return {
|
|
105
|
+
package: manifest.name,
|
|
106
|
+
version: manifest.version,
|
|
107
|
+
runtime,
|
|
108
|
+
contract: CONTRACT,
|
|
109
|
+
ok: !published.some(finding => finding.level === 'error'),
|
|
110
|
+
inspectedPatch,
|
|
111
|
+
findings: published,
|
|
112
|
+
note: inspectedPatch
|
|
113
|
+
? '以上为清单规则 + 包内补丁行检查;含隔离启动冒烟的完整体检请运行 npx @tokensapi/dsh-plugin-check --package <name@version>'
|
|
114
|
+
: '未读到包内补丁文件,补丁行未检查;请运行 npx @tokensapi/dsh-plugin-check --package <name@version> 做完整体检',
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
}))
|
|
118
|
+
}
|