@tokensapi/dsh-plugin-check 0.3.1 → 0.3.2

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.
@@ -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
- /** 子进程执行的行级冒烟脚本;结果写文件,避开插件对 stdout 的污染。 */
25
- const RUNNER_SOURCE = `
26
- import { pathToFileURL } from 'node:url'
27
- import { writeFileSync } from 'node:fs'
28
- const [workspace, resultPath, rowsJson] = process.argv.slice(2)
29
- const rows = JSON.parse(rowsJson)
30
- const results = []
31
- let current = null
32
- process.on('uncaughtException', (cause) => finish(cause))
33
- process.on('unhandledRejection', (cause) => finish(cause))
34
- function finish(cause) {
35
- if (current !== null) {
36
- results.push({ ...current, status: 'failed', stage: current.stage,
37
- error: cause instanceof Error ? cause.message : String(cause) })
38
- current = null
39
- }
40
- writeFileSync(resultPath, JSON.stringify(results))
41
- process.exit(0)
42
- }
43
- const { createRequire } = await import('node:module')
44
- const workspaceRequire = createRequire(pathToFileURL(workspace + '/x.js'))
45
- let Context
46
- try {
47
- const cordis = await import(pathToFileURL(workspaceRequire.resolve('@deepseek-ai/cordis')))
48
- Context = cordis.Context
49
- } catch (cause) {
50
- writeFileSync(resultPath, JSON.stringify([{ name: '@deepseek-ai/cordis', stage: 'import',
51
- status: 'failed', error: '无法载入 cordis 运行时: ' + (cause instanceof Error ? cause.message : String(cause)) }]))
52
- process.exit(0)
53
- }
54
- for (const row of rows) {
55
- current = { name: row.name, stage: 'resolve' }
56
- try {
57
- const entryPath = workspaceRequire.resolve(row.name)
58
- current.stage = 'import'
59
- const mod = await import(pathToFileURL(entryPath))
60
- const plugin = mod.default ?? mod
61
- current.stage = 'apply'
62
- const ctx = new Context()
63
- if (row.dynamicConfig) ctx.plugin(plugin)
64
- else if (row.config === undefined) ctx.plugin(plugin)
65
- else ctx.plugin(plugin, row.config)
66
- await new Promise((r) => setTimeout(r, 750))
67
- results.push({ name: row.name, status: 'ok',
68
- note: row.dynamicConfig ? '配置含动态表达式,以空配置应用' : undefined })
69
- current = null
70
- } catch (cause) {
71
- results.push({ name: row.name, status: 'failed', stage: current.stage,
72
- error: cause instanceof Error ? cause.message : String(cause) })
73
- current = null
74
- }
75
- }
76
- writeFileSync(resultPath, JSON.stringify(results))
77
- process.exit(0)
78
- `
79
-
80
-
81
- /**
82
- * 对插件目录执行隔离冒烟。
83
- * @param {string} pluginDir - 插件根目录。
84
- * @param {object} manifest - 已解析的 package.json。
85
- * @param {Array<{name: string, config?: unknown, disabled?: boolean, dynamicConfig: boolean}>} rows - 补丁行。
86
- * @param {{ keepWorkspace?: boolean }} [options]
87
- * @returns {{ findings: Array<{level: 'error'|'warning', rule: string, message: string}>, workspace?: string }}
88
- */
89
- export function checkSmoke(pluginDir, manifest, rows, options = {}) {
90
- const findings = []
91
- const error = (rule, message) => findings.push({ level: 'error', rule, message })
92
- const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
93
-
94
- const workspace = mkdtempSync(join(tmpdir(), 'dsh-plugin-check-'))
95
- const cleanup = () => {
96
- if (options.keepWorkspace) return
97
- try { rmSync(workspace, { recursive: true, force: true }) } catch { /* 临时目录残留不影响判定 */ }
98
- }
99
-
100
- try {
101
- // S1 打包:--ignore-scripts 跳过 prepack 等脚本,只验证发布内容本身。
102
- const pack = runNpm(['pack', resolve(pluginDir), '--ignore-scripts', '--pack-destination', workspace], workspace, INSTALL_TIMEOUT_MS)
103
- if (pack.status !== 0) {
104
- error('S1-pack', `npm pack 失败: ${(pack.stderr || pack.stdout || '').trim().split('\n').slice(-3).join(' ')}`)
105
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
106
- }
107
- const tarball = readdirSync(workspace).find(name => name.endsWith('.tgz'))
108
- if (tarball === undefined) {
109
- error('S1-pack', 'npm pack 未产出 tgz')
110
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
111
- }
112
-
113
- // S2 安装:插件走 tgz(等价用户侧的受控安装),运行时按插件自己
114
- // 声明的 peer 范围解析;cordis 是冒烟宿主的最低要求,未声明则补上。
115
- const dependencies = { [manifest.name]: `file:./${tarball}` }
116
- const peers = manifest.peerDependencies ?? {}
117
- for (const [name, range] of Object.entries(peers)) dependencies[name] = range
118
- if (dependencies['@deepseek-ai/cordis'] === undefined) {
119
- dependencies['@deepseek-ai/cordis'] = '*'
120
- warning('S2-install', 'peerDependencies 未声明 @deepseek-ai/cordis,冒烟以最新版补齐;建议显式声明')
121
- }
122
- writeFileSync(join(workspace, 'package.json'), JSON.stringify({
123
- name: 'dsh-plugin-check-workspace',
124
- private: true,
125
- dependencies,
126
- }, undefined, 2))
127
- const install = runNpm(
128
- ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel=error'],
129
- workspace,
130
- INSTALL_TIMEOUT_MS,
131
- )
132
- if (install.status !== 0) {
133
- error('S2-install', `依赖装不出来(用户侧受控安装会同样失败): ${(install.stderr || '').trim().split('\n').slice(-4).join(' ')}`)
134
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
135
- }
136
-
137
- // S3+S4 行级冒烟:解析/import/apply 三段判定在子进程完成。
138
- const activeRows = rows.filter(row => !row.disabled)
139
- if (activeRows.length === 0) {
140
- warning('S3-rows', '补丁没有启用状态的行,冒烟无事可做')
141
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
142
- }
143
- const runnerPath = join(workspace, '__runner.mjs')
144
- const resultPath = join(workspace, '__result.json')
145
- writeFileSync(runnerPath, RUNNER_SOURCE)
146
- const run = spawnSync(process.execPath, [
147
- runnerPath,
148
- workspace,
149
- resultPath,
150
- JSON.stringify(activeRows.map(row => ({ name: row.name, config: row.config, dynamicConfig: row.dynamicConfig }))),
151
- ], { cwd: workspace, encoding: 'utf8', timeout: RUNNER_TIMEOUT_MS })
152
- if (!existsSync(resultPath)) {
153
- const reason = run.signal === 'SIGTERM'
154
- ? `超时(${RUNNER_TIMEOUT_MS / 1000}s):入口疑似在顶层做阻塞等待`
155
- : (run.stderr || '子进程未产出结果').trim().split('\n').slice(-3).join(' ')
156
- error('S4-apply', `冒烟子进程异常: ${reason}`)
157
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
158
- }
159
- const results = JSON.parse(readFileSync(resultPath, 'utf8'))
160
- const passed = results.filter(result => result.status === 'ok').length
161
- findings.push({ level: 'info', rule: 'S4-apply', message: `隔离冒烟: ${passed}/${results.length} 行通过(解析→import→应用)` })
162
- for (const result of results) {
163
- if (result.status === 'ok') {
164
- if (result.note !== undefined) warning('S4-apply', `${result.name}: ${result.note}`)
165
- continue
166
- }
167
- const stageLabel = { resolve: '入口解析', import: 'import 载入', apply: '插件应用' }[result.stage] ?? result.stage
168
- error('S4-apply', `${result.name} ${stageLabel} 阶段失败: ${result.error}(装入用户 Cowork 会在启动链同点崩溃)`)
169
- }
170
- return { findings, workspace: options.keepWorkspace ? workspace : undefined }
171
- } finally {
172
- cleanup()
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
+ }
@@ -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.1",
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.2",
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
+ }