dsh-plc-decode 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.en.md +85 -0
- package/README.md +128 -2
- package/assets/codesys-tools/check_project.py +390 -0
- package/assets/codesys-tools/dvp_decode.py +588 -0
- package/assets/codesys-tools/precompile_extract.py +146 -0
- package/assets/codesys-tools/read_project.py +506 -0
- package/assets/decode-pack.mjs +206 -0
- package/assets/icon-180.png +0 -0
- package/assets/smart-lad.mjs +299 -0
- package/assets/smart-net-trace.mjs +199 -0
- package/cordis.patch.yml +4 -0
- package/index.js +3435 -0
- package/invariant.js +10 -0
- package/package.json +57 -4
- package/scripts/smoke.mjs +157 -0
package/invariant.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const PACKAGE_NAME = 'dsh-plc-decode'
|
|
2
|
+
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export const name = 'dsh-plc-decode-invariant'
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export const inject = ['invariants']
|
|
7
|
+
|
|
8
|
+
const install = () => {}
|
|
9
|
+
|
|
10
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plc-decode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "在 DSH 里读 PLC 工程文件:CoDeSys .project、台达 .dvp、西门子 S7-200 SMART .smart/.smartlib 统一解码成可读报告与结构化数据,另带新旧工程对比、地址体检与连线还原。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./invariant": "./invariant.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"invariant.js",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"assets",
|
|
17
|
+
"scripts",
|
|
18
|
+
"README.md",
|
|
19
|
+
"README.en.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
24
|
+
"dsh": ">=0.2.1-rc.5 <0.3.0-0"
|
|
25
|
+
},
|
|
26
|
+
"dsh": {
|
|
27
|
+
"bundle": {
|
|
28
|
+
"patch": "./cordis.patch.yml"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"deepseek-harness",
|
|
33
|
+
"dsh",
|
|
34
|
+
"dsh-plugin",
|
|
35
|
+
"plc",
|
|
36
|
+
"siemens",
|
|
37
|
+
"s7-200-smart",
|
|
38
|
+
"codesys",
|
|
39
|
+
"delta-dvp",
|
|
40
|
+
"ladder",
|
|
41
|
+
"industrial-automation",
|
|
42
|
+
"leadshine",
|
|
43
|
+
"leadsysstudio",
|
|
44
|
+
"雷赛"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"smoke": "node scripts/smoke.mjs",
|
|
48
|
+
"check": "node --check index.js && node --check invariant.js && node --check assets/decode-pack.mjs && node --check assets/smart-lad.mjs && node --check assets/smart-net-trace.mjs"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/taoziliyouchong/dsh-plc-decode"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/taoziliyouchong/dsh-plc-decode#readme",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/taoziliyouchong/dsh-plc-decode/issues"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 冒烟测试:装载插件、核对注册面,可选对一个真实工程跑一次完整解码。
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node scripts/smoke.mjs # 只测装载与注册面
|
|
7
|
+
* node scripts/smoke.mjs <PLC 工程文件路径> # 额外解码一次,核对产物落盘
|
|
8
|
+
*
|
|
9
|
+
* 退出码 0 表示全部通过;任一断言失败时退出码 1。
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from 'node:child_process'
|
|
12
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
import { apply, inject, name as pluginName } from '../index.js'
|
|
17
|
+
|
|
18
|
+
const EXPECTED_TOOLS = 12
|
|
19
|
+
const EXPECTED_COMMANDS = 12
|
|
20
|
+
|
|
21
|
+
let failed = 0
|
|
22
|
+
const pass = (m) => console.log(` ok ${m}`)
|
|
23
|
+
const fail = (m) => { failed++; console.log(` FAIL ${m}`) }
|
|
24
|
+
|
|
25
|
+
/** 解码产物落盘检查:四格式各自的入口文件 */
|
|
26
|
+
const INDEX_FILES = {
|
|
27
|
+
project: '00-索引.md',
|
|
28
|
+
dvp: '00-索引.md',
|
|
29
|
+
smart: '00-目录.md',
|
|
30
|
+
smartlib: '00-目录.md',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 用子进程代替 DSH 的 shell 服务:插件只依赖 resolve/execute/result 三个方法,
|
|
35
|
+
* 这里把命令交给 PowerShell 执行。插件在 runPy 里钉了 PYTHONIOENCODING=utf-8,
|
|
36
|
+
* python 输出是 UTF-8,所以这里也按 UTF-8 解码(2026-11-05 前是 GBK)。
|
|
37
|
+
*/
|
|
38
|
+
function createShell() {
|
|
39
|
+
return {
|
|
40
|
+
resolve: (spec) => spec,
|
|
41
|
+
execute: async (spec) => {
|
|
42
|
+
const r = spawnSync(
|
|
43
|
+
'powershell.exe',
|
|
44
|
+
['-NoProfile', '-NonInteractive', '-Command', spec.command],
|
|
45
|
+
{ cwd: spec.workdir || process.cwd(), timeout: spec.timeoutMs, maxBuffer: 64 * 1024 * 1024, encoding: 'buffer' },
|
|
46
|
+
)
|
|
47
|
+
const utf8 = new TextDecoder('utf-8')
|
|
48
|
+
return {
|
|
49
|
+
result: async () => ({
|
|
50
|
+
exitCode: r.status ?? 1,
|
|
51
|
+
timedOut: r.error?.code === 'ETIMEDOUT',
|
|
52
|
+
aborted: false,
|
|
53
|
+
stdout: { text: utf8.decode(r.stdout ?? Buffer.alloc(0)) },
|
|
54
|
+
stderr: { text: utf8.decode(r.stderr ?? Buffer.alloc(0)) },
|
|
55
|
+
}),
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tools = new Map()
|
|
62
|
+
const commands = new Map()
|
|
63
|
+
const ctx = {
|
|
64
|
+
tools: { register: (t) => tools.set(t.name, t) },
|
|
65
|
+
commands: { register: (c) => commands.set(c.name, c) },
|
|
66
|
+
shell: createShell(),
|
|
67
|
+
sandboxPolicy: { resolve: () => undefined },
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkPlugin() {
|
|
71
|
+
console.log(`插件:${pluginName}`)
|
|
72
|
+
if (!pluginName || pluginName !== 'dsh-plc-decode') fail(`插件名应为 dsh-plc-decode,实际 ${pluginName}`)
|
|
73
|
+
else pass('插件名')
|
|
74
|
+
if (!inject.includes('tools') || !inject.includes('commands')) fail(`inject 缺项:${inject.join(', ')}`)
|
|
75
|
+
else pass(`inject:${inject.join(', ')}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function checkPython() {
|
|
79
|
+
const r = spawnSync('python', ['--version'], { encoding: 'utf8' })
|
|
80
|
+
if (r.status === 0) pass(`python 可用:${(r.stdout || r.stderr || '').trim()}`)
|
|
81
|
+
else console.log(' note 未找到 python:.project / .dvp 解码需要 Python 3.9+(.smart 不受影响)')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function checkRegistrations() {
|
|
85
|
+
if (tools.size !== EXPECTED_TOOLS) fail(`工具数应为 ${EXPECTED_TOOLS},实际 ${tools.size}`)
|
|
86
|
+
else pass(`工具数 ${tools.size}`)
|
|
87
|
+
if (commands.size !== EXPECTED_COMMANDS) fail(`命令数应为 ${EXPECTED_COMMANDS},实际 ${commands.size}`)
|
|
88
|
+
else pass(`命令数 ${commands.size}`)
|
|
89
|
+
|
|
90
|
+
for (const [toolName, tool] of tools) {
|
|
91
|
+
if (typeof tool.execute !== 'function') fail(`工具 ${toolName} 缺 execute`)
|
|
92
|
+
if (!tool.description || tool.description.length < 10) fail(`工具 ${toolName} 缺描述`)
|
|
93
|
+
const schema = tool.parameters
|
|
94
|
+
if (!schema || schema.type !== 'object' || typeof schema.properties !== 'object') fail(`工具 ${toolName} 参数 schema 不合法`)
|
|
95
|
+
}
|
|
96
|
+
if (!failed) pass('各工具 execute / description / 参数 schema')
|
|
97
|
+
|
|
98
|
+
for (const [cmdName, cmd] of commands) {
|
|
99
|
+
if (typeof cmd.handler !== 'function') fail(`命令 /${cmdName} 缺 handler`)
|
|
100
|
+
if (!cmd.description) fail(`命令 /${cmdName} 缺描述`)
|
|
101
|
+
}
|
|
102
|
+
pass('各命令 handler / description')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function checkBadFile() {
|
|
106
|
+
const dir = mkdtempSync(join(tmpdir(), 'plc-decode-smoke-'))
|
|
107
|
+
const bad = join(dir, 'not-a-project.smart')
|
|
108
|
+
writeFileSync(bad, Buffer.from('NOPE12345678'), 'utf8')
|
|
109
|
+
try {
|
|
110
|
+
await tools.get('plc-decode').execute({ file: bad }, {})
|
|
111
|
+
fail('坏文件应报"不是可识别的 PLC 工程文件",实际未抛错')
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (/不是可识别的 PLC 工程文件/.test(String(error.message))) pass('坏文件报错路径')
|
|
114
|
+
else fail(`坏文件报错内容异常:${error.message}`)
|
|
115
|
+
} finally {
|
|
116
|
+
rmSync(dir, { recursive: true, force: true })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function checkDecode(fileArg) {
|
|
121
|
+
if (!existsSync(fileArg)) { fail(`工程文件不存在:${fileArg}`); return }
|
|
122
|
+
const outDir = mkdtempSync(join(tmpdir(), 'plc-decode-out-'))
|
|
123
|
+
const started = Date.now()
|
|
124
|
+
try {
|
|
125
|
+
const value = await tools.get('plc-decode').execute({ file: fileArg, outDir }, {})
|
|
126
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1)
|
|
127
|
+
const kind = /类型: (\w+)/.exec(value?.text ?? '')?.[1]
|
|
128
|
+
?? (fileArg.toLowerCase().endsWith('.smartlib') ? 'smartlib' : fileArg.toLowerCase().endsWith('.smart') ? 'smart' : fileArg.toLowerCase().endsWith('.dvp') ? 'dvp' : 'project')
|
|
129
|
+
const index = join(outDir, INDEX_FILES[kind] ?? '00-索引.md')
|
|
130
|
+
if (existsSync(index)) pass(`解码 ${fileArg}(${kind},${seconds}s),产物入口 ${INDEX_FILES[kind]}`)
|
|
131
|
+
else fail(`解码完成但缺产物入口:${index}\n${value?.text ?? ''}`)
|
|
132
|
+
} catch (error) {
|
|
133
|
+
fail(`解码失败:${error.message}`)
|
|
134
|
+
} finally {
|
|
135
|
+
rmSync(outDir, { recursive: true, force: true })
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('== 装载 ==')
|
|
140
|
+
await apply(ctx)
|
|
141
|
+
checkPlugin()
|
|
142
|
+
checkRegistrations()
|
|
143
|
+
console.log('== 依赖 ==')
|
|
144
|
+
checkPython()
|
|
145
|
+
console.log('== 错误路径 ==')
|
|
146
|
+
await checkBadFile()
|
|
147
|
+
|
|
148
|
+
const fileArg = process.argv[2]
|
|
149
|
+
if (fileArg) {
|
|
150
|
+
console.log('== 端到端解码 ==')
|
|
151
|
+
await checkDecode(fileArg)
|
|
152
|
+
} else {
|
|
153
|
+
console.log('== 端到端解码:跳过(未给工程文件路径)==')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(failed ? `\n失败 ${failed} 项` : '\n全部通过')
|
|
157
|
+
process.exit(failed ? 1 : 0)
|