@tokensapi/dsh-plugin-check 0.3.1 → 0.3.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 +95 -79
- package/lib/cowork-plugin.mjs +113 -78
- package/lib/manifest-check.mjs +139 -144
- package/lib/patch-rows.mjs +22 -0
- package/lib/registry-check.mjs +205 -158
- package/lib/smoke-check.mjs +211 -174
- package/lib/tarball.mjs +98 -0
- package/package.json +44 -44
package/lib/smoke-check.mjs
CHANGED
|
@@ -1,174 +1,211 @@
|
|
|
1
|
-
/* ============================================================
|
|
2
|
-
* 阶段②:隔离启动冒烟(联网,分钟级)
|
|
3
|
-
* ============================================================
|
|
4
|
-
* 在临时工作区里按插件自己声明的 peer 范围装出一套真实运行时,
|
|
5
|
-
* 然后在子进程里逐行做真实网关会做的事:import 补丁行入口、
|
|
6
|
-
* new Context()、ctx.plugin() 应用。这里能过,插件就不会以
|
|
7
|
-
* "装上即崩"的方式杀死用户的 Cowork:
|
|
8
|
-
* - 装不上(依赖解析失败)在 install 步暴露;
|
|
9
|
-
* - import 期崩溃(顶层异常/缺模块/ESM 形状)在 import 步暴露;
|
|
10
|
-
* - 应用期崩溃(apply 抛出/插件形状不合法)在 plugin 步暴露;
|
|
11
|
-
* - 声明了 inject 而服务未注入属正常停车,不算失败。
|
|
12
|
-
* 全程 --ignore-scripts,被测代码没有机会在检查器进程里执行
|
|
13
|
-
* 安装钩子;插件代码只在一次性子进程里运行,超时即杀。
|
|
14
|
-
* ============================================================ */
|
|
15
|
-
import { spawnSync } from 'node:child_process'
|
|
16
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync, readdirSync } from 'node:fs'
|
|
17
|
-
import { tmpdir } from 'node:os'
|
|
18
|
-
import { join, resolve } from 'node:path'
|
|
19
|
-
import { runNpm } from './npm.mjs'
|
|
20
|
-
|
|
21
|
-
const RUNNER_TIMEOUT_MS = 90_000
|
|
22
|
-
const INSTALL_TIMEOUT_MS = 300_000
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
process.on('
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
else ctx.plugin(plugin
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*
|
|
87
|
-
* @
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* 阶段②:隔离启动冒烟(联网,分钟级)
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 在临时工作区里按插件自己声明的 peer 范围装出一套真实运行时,
|
|
5
|
+
* 然后在子进程里逐行做真实网关会做的事:import 补丁行入口、
|
|
6
|
+
* new Context()、ctx.plugin() 应用。这里能过,插件就不会以
|
|
7
|
+
* "装上即崩"的方式杀死用户的 Cowork:
|
|
8
|
+
* - 装不上(依赖解析失败)在 install 步暴露;
|
|
9
|
+
* - import 期崩溃(顶层异常/缺模块/ESM 形状)在 import 步暴露;
|
|
10
|
+
* - 应用期崩溃(apply 抛出/插件形状不合法)在 plugin 步暴露;
|
|
11
|
+
* - 声明了 inject 而服务未注入属正常停车,不算失败。
|
|
12
|
+
* 全程 --ignore-scripts,被测代码没有机会在检查器进程里执行
|
|
13
|
+
* 安装钩子;插件代码只在一次性子进程里运行,超时即杀。
|
|
14
|
+
* ============================================================ */
|
|
15
|
+
import { spawnSync } from 'node:child_process'
|
|
16
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync, readdirSync } from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import { join, resolve } from 'node:path'
|
|
19
|
+
import { runNpm } from './npm.mjs'
|
|
20
|
+
|
|
21
|
+
const RUNNER_TIMEOUT_MS = 90_000
|
|
22
|
+
const INSTALL_TIMEOUT_MS = 300_000
|
|
23
|
+
const PROBE_TIMEOUT_MS = 60_000
|
|
24
|
+
|
|
25
|
+
/** 子进程执行的行级冒烟脚本;结果写文件,避开插件对 stdout 的污染。 */
|
|
26
|
+
const RUNNER_SOURCE = `
|
|
27
|
+
import { pathToFileURL } from 'node:url'
|
|
28
|
+
import { writeFileSync } from 'node:fs'
|
|
29
|
+
const [workspace, resultPath, rowsJson] = process.argv.slice(2)
|
|
30
|
+
const rows = JSON.parse(rowsJson)
|
|
31
|
+
const results = []
|
|
32
|
+
let current = null
|
|
33
|
+
process.on('uncaughtException', (cause) => finish(cause))
|
|
34
|
+
process.on('unhandledRejection', (cause) => finish(cause))
|
|
35
|
+
function finish(cause) {
|
|
36
|
+
if (current !== null) {
|
|
37
|
+
results.push({ ...current, status: 'failed', stage: current.stage,
|
|
38
|
+
error: cause instanceof Error ? cause.message : String(cause) })
|
|
39
|
+
current = null
|
|
40
|
+
}
|
|
41
|
+
writeFileSync(resultPath, JSON.stringify(results))
|
|
42
|
+
process.exit(0)
|
|
43
|
+
}
|
|
44
|
+
const { createRequire } = await import('node:module')
|
|
45
|
+
const workspaceRequire = createRequire(pathToFileURL(workspace + '/x.js'))
|
|
46
|
+
let Context
|
|
47
|
+
try {
|
|
48
|
+
const cordis = await import(pathToFileURL(workspaceRequire.resolve('@deepseek-ai/cordis')))
|
|
49
|
+
Context = cordis.Context
|
|
50
|
+
} catch (cause) {
|
|
51
|
+
writeFileSync(resultPath, JSON.stringify([{ name: '@deepseek-ai/cordis', stage: 'import',
|
|
52
|
+
status: 'failed', error: '无法载入 cordis 运行时: ' + (cause instanceof Error ? cause.message : String(cause)) }]))
|
|
53
|
+
process.exit(0)
|
|
54
|
+
}
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
current = { name: row.name, stage: 'resolve' }
|
|
57
|
+
try {
|
|
58
|
+
const entryPath = workspaceRequire.resolve(row.name)
|
|
59
|
+
current.stage = 'import'
|
|
60
|
+
const mod = await import(pathToFileURL(entryPath))
|
|
61
|
+
const plugin = mod.default ?? mod
|
|
62
|
+
current.stage = 'apply'
|
|
63
|
+
const ctx = new Context()
|
|
64
|
+
if (row.dynamicConfig) ctx.plugin(plugin)
|
|
65
|
+
else if (row.config === undefined) ctx.plugin(plugin)
|
|
66
|
+
else ctx.plugin(plugin, row.config)
|
|
67
|
+
await new Promise((r) => setTimeout(r, 750))
|
|
68
|
+
results.push({ name: row.name, status: 'ok',
|
|
69
|
+
note: row.dynamicConfig ? '配置含动态表达式,以空配置应用' : undefined })
|
|
70
|
+
current = null
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
results.push({ name: row.name, status: 'failed', stage: current.stage,
|
|
73
|
+
error: cause instanceof Error ? cause.message : String(cause) })
|
|
74
|
+
current = null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
writeFileSync(resultPath, JSON.stringify(results))
|
|
78
|
+
process.exit(0)
|
|
79
|
+
`
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
/** 宿主内核作用域:这些包由宿主提供,私有版本本就不在公开源上。 */
|
|
83
|
+
const CORE_SCOPE = '@deepseek-ai/'
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 找出「公开源上取不到、但不该算插件问题」的依赖。
|
|
87
|
+
* 宿主内核包(@deepseek-ai/*)的内部版本只发在私有源,检查器在
|
|
88
|
+
* 公网环境里必然解析不到 —— 据此判插件不合格是冤枉它。其它包
|
|
89
|
+
* 解析不到就是插件自己声明错了范围,仍按 error 处理。
|
|
90
|
+
* @param {Record<string, string>} dependencies - 工作区依赖表。
|
|
91
|
+
* @param {string} workspace - 探测执行目录。
|
|
92
|
+
* @returns {string[]} 取不到的内核包(带版本范围)。
|
|
93
|
+
*/
|
|
94
|
+
function findUnreachableCoreDependencies(dependencies, workspace) {
|
|
95
|
+
const missing = []
|
|
96
|
+
for (const [name, range] of Object.entries(dependencies)) {
|
|
97
|
+
if (!name.startsWith(CORE_SCOPE)) continue
|
|
98
|
+
if (typeof range !== 'string' || range.startsWith('file:')) continue
|
|
99
|
+
const view = runNpm(['view', `${name}@${range}`, 'version', '--json'], workspace, PROBE_TIMEOUT_MS)
|
|
100
|
+
const output = `${view.stderr}${view.stdout}`
|
|
101
|
+
// 版本存在时 npm view 会打印版本号;解析不到才算取不到。
|
|
102
|
+
if (view.status !== 0 || output.trim() === '' || output.trim() === '[]') {
|
|
103
|
+
missing.push(`${name}@${range}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return missing
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 对插件目录执行隔离冒烟。
|
|
111
|
+
* @param {string} pluginDir - 插件根目录。
|
|
112
|
+
* @param {object} manifest - 已解析的 package.json。
|
|
113
|
+
* @param {Array<{name: string, config?: unknown, disabled?: boolean, dynamicConfig: boolean}>} rows - 补丁行。
|
|
114
|
+
* @param {{ keepWorkspace?: boolean }} [options]
|
|
115
|
+
* @returns {{ findings: Array<{level: 'error'|'warning', rule: string, message: string}>, workspace?: string }}
|
|
116
|
+
*/
|
|
117
|
+
export function checkSmoke(pluginDir, manifest, rows, options = {}) {
|
|
118
|
+
const findings = []
|
|
119
|
+
const error = (rule, message) => findings.push({ level: 'error', rule, message })
|
|
120
|
+
const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
|
|
121
|
+
|
|
122
|
+
const workspace = mkdtempSync(join(tmpdir(), 'dsh-plugin-check-'))
|
|
123
|
+
const cleanup = () => {
|
|
124
|
+
if (options.keepWorkspace) return
|
|
125
|
+
try { rmSync(workspace, { recursive: true, force: true }) } catch { /* 临时目录残留不影响判定 */ }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// S1 打包:--ignore-scripts 跳过 prepack 等脚本,只验证发布内容本身。
|
|
130
|
+
const pack = runNpm(['pack', resolve(pluginDir), '--ignore-scripts', '--pack-destination', workspace], workspace, INSTALL_TIMEOUT_MS)
|
|
131
|
+
if (pack.status !== 0) {
|
|
132
|
+
error('S1-pack', `npm pack 失败: ${(pack.stderr || pack.stdout || '').trim().split('\n').slice(-3).join(' ')}`)
|
|
133
|
+
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
134
|
+
}
|
|
135
|
+
const tarball = readdirSync(workspace).find(name => name.endsWith('.tgz'))
|
|
136
|
+
if (tarball === undefined) {
|
|
137
|
+
error('S1-pack', 'npm pack 未产出 tgz')
|
|
138
|
+
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// S2 安装:插件走 tgz(等价用户侧的受控安装),运行时按插件自己
|
|
142
|
+
// 声明的 peer 范围解析;cordis 是冒烟宿主的最低要求,未声明则补上。
|
|
143
|
+
const dependencies = { [manifest.name]: `file:./${tarball}` }
|
|
144
|
+
const peers = manifest.peerDependencies ?? {}
|
|
145
|
+
for (const [name, range] of Object.entries(peers)) dependencies[name] = range
|
|
146
|
+
if (dependencies['@deepseek-ai/cordis'] === undefined) {
|
|
147
|
+
dependencies['@deepseek-ai/cordis'] = '*'
|
|
148
|
+
warning('S2-install', 'peerDependencies 未声明 @deepseek-ai/cordis,冒烟以最新版补齐;建议显式声明')
|
|
149
|
+
}
|
|
150
|
+
writeFileSync(join(workspace, 'package.json'), JSON.stringify({
|
|
151
|
+
name: 'dsh-plugin-check-workspace',
|
|
152
|
+
private: true,
|
|
153
|
+
dependencies,
|
|
154
|
+
}, undefined, 2))
|
|
155
|
+
const install = runNpm(
|
|
156
|
+
['install', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel=error'],
|
|
157
|
+
workspace,
|
|
158
|
+
INSTALL_TIMEOUT_MS,
|
|
159
|
+
)
|
|
160
|
+
if (install.status !== 0) {
|
|
161
|
+
const detail = (install.stderr || '').trim().split('\n').slice(-4).join(' ')
|
|
162
|
+
// 区分「真装不出来」和「内核内部版本在公开源取不到」:后者是
|
|
163
|
+
// 检查环境的限制,判成不合格会冤枉插件,只如实标注冒烟未执行。
|
|
164
|
+
const unreachable = findUnreachableCoreDependencies(dependencies, workspace)
|
|
165
|
+
if (unreachable.length > 0) {
|
|
166
|
+
warning('S2-install', `宿主内核依赖 ${unreachable.join('、')} 在公开 npm 上取不到(内部版本),隔离冒烟无法执行,已跳过;`
|
|
167
|
+
+ `清单规则仍已判定。如需完整冒烟,请在能访问这些包的环境里重跑。原始错误: ${detail}`)
|
|
168
|
+
} else {
|
|
169
|
+
error('S2-install', `依赖装不出来(用户侧受控安装会同样失败): ${detail}`)
|
|
170
|
+
}
|
|
171
|
+
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// S3+S4 行级冒烟:解析/import/apply 三段判定在子进程完成。
|
|
175
|
+
const activeRows = rows.filter(row => !row.disabled)
|
|
176
|
+
if (activeRows.length === 0) {
|
|
177
|
+
warning('S3-rows', '补丁没有启用状态的行,冒烟无事可做')
|
|
178
|
+
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
179
|
+
}
|
|
180
|
+
const runnerPath = join(workspace, '__runner.mjs')
|
|
181
|
+
const resultPath = join(workspace, '__result.json')
|
|
182
|
+
writeFileSync(runnerPath, RUNNER_SOURCE)
|
|
183
|
+
const run = spawnSync(process.execPath, [
|
|
184
|
+
runnerPath,
|
|
185
|
+
workspace,
|
|
186
|
+
resultPath,
|
|
187
|
+
JSON.stringify(activeRows.map(row => ({ name: row.name, config: row.config, dynamicConfig: row.dynamicConfig }))),
|
|
188
|
+
], { cwd: workspace, encoding: 'utf8', timeout: RUNNER_TIMEOUT_MS })
|
|
189
|
+
if (!existsSync(resultPath)) {
|
|
190
|
+
const reason = run.signal === 'SIGTERM'
|
|
191
|
+
? `超时(${RUNNER_TIMEOUT_MS / 1000}s):入口疑似在顶层做阻塞等待`
|
|
192
|
+
: (run.stderr || '子进程未产出结果').trim().split('\n').slice(-3).join(' ')
|
|
193
|
+
error('S4-apply', `冒烟子进程异常: ${reason}`)
|
|
194
|
+
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
195
|
+
}
|
|
196
|
+
const results = JSON.parse(readFileSync(resultPath, 'utf8'))
|
|
197
|
+
const passed = results.filter(result => result.status === 'ok').length
|
|
198
|
+
findings.push({ level: 'info', rule: 'S4-apply', message: `隔离冒烟: ${passed}/${results.length} 行通过(解析→import→应用)` })
|
|
199
|
+
for (const result of results) {
|
|
200
|
+
if (result.status === 'ok') {
|
|
201
|
+
if (result.note !== undefined) warning('S4-apply', `${result.name}: ${result.note}`)
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
const stageLabel = { resolve: '入口解析', import: 'import 载入', apply: '插件应用' }[result.stage] ?? result.stage
|
|
205
|
+
error('S4-apply', `${result.name} 在 ${stageLabel} 阶段失败: ${result.error}(装入用户 Cowork 会在启动链同点崩溃)`)
|
|
206
|
+
}
|
|
207
|
+
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
208
|
+
} finally {
|
|
209
|
+
cleanup()
|
|
210
|
+
}
|
|
211
|
+
}
|
package/lib/tarball.mjs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* npm tarball 单文件读取(零依赖)
|
|
3
|
+
* ============================================================
|
|
4
|
+
* registry 的版本清单里没有包内文件内容,而 M4/M5 这类规则必须
|
|
5
|
+
* 读到 cordis.patch.yml 本身 —— 曾有插件把补丁行的 name 写成
|
|
6
|
+
* cordis 插件名而非包名,清单看不出任何异常,装上却让整棵插件树
|
|
7
|
+
* 加载失败。会话内体检没有 npm 子进程可用,因此这里直接取
|
|
8
|
+
* dist.tarball,用 node:zlib 解 gzip 后自行走 tar 头,只取需要的
|
|
9
|
+
* 那一个文件。
|
|
10
|
+
* ============================================================ */
|
|
11
|
+
import { gunzipSync } from 'node:zlib'
|
|
12
|
+
|
|
13
|
+
const BLOCK = 512
|
|
14
|
+
const DEFAULT_MAX_BYTES = 25 * 1024 * 1024
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 20_000
|
|
16
|
+
|
|
17
|
+
const readString = (buffer, offset, length) => {
|
|
18
|
+
const slice = buffer.subarray(offset, offset + length)
|
|
19
|
+
const end = slice.indexOf(0)
|
|
20
|
+
return slice.subarray(0, end === -1 ? slice.length : end).toString('utf8')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** tar 头里的数值字段是补零的八进制字符串;空字段按 0 处理。 */
|
|
24
|
+
const readOctal = (buffer, offset, length) => {
|
|
25
|
+
const raw = readString(buffer, offset, length).trim()
|
|
26
|
+
if (raw === '') return 0
|
|
27
|
+
const value = Number.parseInt(raw, 8)
|
|
28
|
+
return Number.isFinite(value) ? value : 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 遍历 tar,返回首个匹配的条目内容。
|
|
33
|
+
* 处理 ustar 的 prefix 拼接与 pax(typeflag 'x')的路径覆盖,
|
|
34
|
+
* 因此长路径与非 ASCII 路径同样可取。
|
|
35
|
+
* @param {Buffer} tar - 解压后的 tar 字节。
|
|
36
|
+
* @param {(path: string) => boolean} match - 路径判定(已剥掉顶层 package/)。
|
|
37
|
+
* @returns {string | undefined}
|
|
38
|
+
*/
|
|
39
|
+
export function readTarEntry(tar, match) {
|
|
40
|
+
let offset = 0
|
|
41
|
+
let paxPath
|
|
42
|
+
while (offset + BLOCK <= tar.length) {
|
|
43
|
+
const header = tar.subarray(offset, offset + BLOCK)
|
|
44
|
+
if (header.every(byte => byte === 0)) break
|
|
45
|
+
const name = readString(header, 0, 100)
|
|
46
|
+
const size = readOctal(header, 124, 12)
|
|
47
|
+
const typeflag = readString(header, 156, 1)
|
|
48
|
+
const prefix = readString(header, 345, 155)
|
|
49
|
+
const dataOffset = offset + BLOCK
|
|
50
|
+
const padded = Math.ceil(size / BLOCK) * BLOCK
|
|
51
|
+
if (typeflag === 'x' || typeflag === 'X') {
|
|
52
|
+
// pax 扩展头:形如 "<len> path=<value>\n",覆盖紧随其后的条目路径。
|
|
53
|
+
const record = tar.subarray(dataOffset, dataOffset + size).toString('utf8')
|
|
54
|
+
const found = /(?:^|\n)\d+ path=([^\n]*)\n/u.exec(record)
|
|
55
|
+
paxPath = found === null ? undefined : found[1]
|
|
56
|
+
offset = dataOffset + padded
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
const full = paxPath ?? (prefix === '' ? name : `${prefix}/${name}`)
|
|
60
|
+
paxPath = undefined
|
|
61
|
+
if (typeflag === '' || typeflag === '0') {
|
|
62
|
+
// npm 打包约定顶层统一为 package/。
|
|
63
|
+
const relative = full.replace(/^[^/]+\//u, '')
|
|
64
|
+
if (match(relative)) return tar.subarray(dataOffset, dataOffset + size).toString('utf8')
|
|
65
|
+
}
|
|
66
|
+
offset = dataOffset + padded
|
|
67
|
+
}
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 下载并解出 tarball 中的一个文件。
|
|
73
|
+
* 任何取不到的情形都以 { error } 返回,由调用方降级成提示而不是判定
|
|
74
|
+
* 插件不合格 —— 网络问题不是插件的过错。
|
|
75
|
+
* @param {string} url - registry 的 dist.tarball。
|
|
76
|
+
* @param {(path: string) => boolean} match
|
|
77
|
+
* @param {{ maxBytes?: number, timeoutMs?: number }} [options]
|
|
78
|
+
* @returns {Promise<{ content?: string } | { error: string }>}
|
|
79
|
+
*/
|
|
80
|
+
export async function fetchTarEntry(url, match, options = {}) {
|
|
81
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES
|
|
82
|
+
const controller = new AbortController()
|
|
83
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
86
|
+
if (!response.ok) return { error: `下载 tarball 失败(HTTP ${response.status})` }
|
|
87
|
+
const declared = Number(response.headers.get('content-length') ?? '0')
|
|
88
|
+
if (declared > maxBytes) return { error: `tarball 超过 ${Math.round(maxBytes / 1024 / 1024)}MB,跳过包内检查` }
|
|
89
|
+
const raw = Buffer.from(await response.arrayBuffer())
|
|
90
|
+
if (raw.length > maxBytes) return { error: `tarball 超过 ${Math.round(maxBytes / 1024 / 1024)}MB,跳过包内检查` }
|
|
91
|
+
return { content: readTarEntry(gunzipSync(raw), match) }
|
|
92
|
+
} catch (cause) {
|
|
93
|
+
const reason = cause instanceof Error ? cause.message : String(cause)
|
|
94
|
+
return { error: cause?.name === 'AbortError' ? '下载 tarball 超时' : `读取 tarball 失败: ${reason}` }
|
|
95
|
+
} finally {
|
|
96
|
+
clearTimeout(timer)
|
|
97
|
+
}
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@tokensapi/dsh-plugin-check",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "TokensCowork/DSH \u63d2\u4ef6\u5408\u683c\u6027\u4f53\u68c0:CLI \u4e00\u6761\u547d\u4ee4\u5224\u5b9a\u63d2\u4ef6\u80fd\u5426\u5b89\u5168\u88c5\u5165\u5bbf\u4e3b;\u88c5\u5165 Cowork \u540e\u63d0\u4f9b\u4f1a\u8bdd\u5185 plugin_check \u5de5\u5177\u3002",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"dsh-plugin-check": "bin/dsh-plugin-check.mjs"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin",
|
|
11
|
-
"lib",
|
|
12
|
-
"cordis.patch.yml",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"test": "node --test \"tests/*.test.mjs\""
|
|
17
|
-
},
|
|
18
|
-
"engines": {
|
|
19
|
-
"node": ">=22.0.0"
|
|
20
|
-
},
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"js-yaml": "^4.1.0",
|
|
23
|
-
"semver": "^7.6.0"
|
|
24
|
-
},
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "https://github.com/TokensAPI/tokens_DshPluginCheck_code.git"
|
|
29
|
-
},
|
|
30
|
-
"main": "lib/cowork-plugin.mjs",
|
|
31
|
-
"exports": {
|
|
32
|
-
".": "./lib/cowork-plugin.mjs"
|
|
33
|
-
},
|
|
34
|
-
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/cordis": "4.0.1 || 4.0.2",
|
|
36
|
-
"@deepseek-ai/dsh-tools": "0.1.0-rc.8 || 0.1.3-alpha.1"
|
|
37
|
-
},
|
|
38
|
-
"dsh": {
|
|
39
|
-
"engine": ">=0.1.0-rc.8 <0.2.0",
|
|
40
|
-
"bundle": {
|
|
41
|
-
"patch": "./cordis.patch.yml"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokensapi/dsh-plugin-check",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "TokensCowork/DSH \u63d2\u4ef6\u5408\u683c\u6027\u4f53\u68c0:CLI \u4e00\u6761\u547d\u4ee4\u5224\u5b9a\u63d2\u4ef6\u80fd\u5426\u5b89\u5168\u88c5\u5165\u5bbf\u4e3b;\u88c5\u5165 Cowork \u540e\u63d0\u4f9b\u4f1a\u8bdd\u5185 plugin_check \u5de5\u5177\u3002",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dsh-plugin-check": "bin/dsh-plugin-check.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"cordis.patch.yml",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test \"tests/*.test.mjs\""
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"js-yaml": "^4.1.0",
|
|
23
|
+
"semver": "^7.6.0"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/TokensAPI/tokens_DshPluginCheck_code.git"
|
|
29
|
+
},
|
|
30
|
+
"main": "lib/cowork-plugin.mjs",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./lib/cowork-plugin.mjs"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/cordis": "4.0.1 || 4.0.2",
|
|
36
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.8 || 0.1.3-alpha.1"
|
|
37
|
+
},
|
|
38
|
+
"dsh": {
|
|
39
|
+
"engine": ">=0.1.0-rc.8 <0.2.0",
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|