Compare commits

...
68 Commits
Author SHA1 Message Date
cheney 8ff4917d0c feat(kit): 实现 tcpc tcp 报文发收命令
TDevOpsCICD / build-kit (push) Successful in 6m11s
- tcpc 支持 hex/文本发送、--headlen 大端长度头自动填充与收包校验、超时控制

refactor(killport): 支持 --dry 模式,Linux 下通过 docker-proxy 定位容器内真实进程(拆分至 linux/portpid.js)
2026-10-16 17:29:40 +08:00
cheney 40d5dea9cc feat(glance): --report 传端口号时导出报告到临时目录并开启 http 服务在线查看
TDevOpsCICD / build-kit (push) Successful in 5m50s
2026-09-18 22:32:46 +08:00
cheney 00aaf282bb fix(glance): 端口监控透过 docker-proxy 定位容器内真实进程
TDevOpsCICD / build-kit (push) Successful in 6m3s
2026-09-18 12:00:16 +08:00
cheney 072c9bfa50 新增 glance 进程监控命令:采集 cpu/内存/句柄数并生成报告
TDevOpsCICD / build-kit (push) Successful in 5m54s
2026-09-18 10:00:15 +08:00
jif2.zhang 593ac96c8c feat: kit 增加 registry 命令对接配置中心
TDevOpsCICD / build-kit (push) Successful in 5m26s
2026-09-11 14:14:18 +08:00
jif2.zhang 31b50f9f43 docs: registry 模块设计文档 + 网站使用介绍
TDevOpsCICD / build-kit (push) Successful in 5m25s
2026-09-11 11:26:57 +08:00
jif2.zhang e5e38a16eb website: 首页增加下载卡片,整理文档内容
TDevOpsCICD / build-kit (push) Successful in 5m33s
2026-09-11 11:06:39 +08:00
jif2.zhang 297c2366f2 ci: 修复 GitHub 仓库为空时 clone 失败的问题
TDevOpsCICD / build-kit (push) Successful in 5m27s
2026-09-11 10:00:14 +08:00
jif2.zhang 97c3b439fc ci: 移除 Gitea packages 上传步骤,仅保留 R2 存档
TDevOpsCICD / build-kit (push) Failing after 5m29s
2026-09-11 09:39:03 +08:00
jif2.zhang 348486fd38 fix(build): Linux bun 构建同样排除 ssh2 的可选原生模块
TDevOpsCICD / build-kit (push) Failing after 3m1s
2026-09-11 09:32:33 +08:00
jif2.zhang b6bd70611d fix(build): 交叉编译 Windows 时排除 ssh2 的可选原生模块
TDevOpsCICD / build-kit (push) Failing after 1m19s
2026-09-11 09:25:09 +08:00
cheney bfc29ea262 更新 cicd:构建后发布 R2 + website 到 GitHub,密钥走配置中心
TDevOpsCICD / build-kit (push) Failing after 55s
2026-09-08 10:58:16 +08:00
cheney 1a929761d5 执行成功,但是目标代码没有保存下来。
TDevOPsCICD / build-image (push) Successful in 4m25s
2026-07-21 14:49:59 +08:00
cheney 52b8e57fc7 修改 cicd 运行环境
TDevOPsCICD / build-image (push) Successful in 4m58s
2026-07-21 14:41:27 +08:00
cheney f01eb56097 save
TDevOPsCICD / build-image (push) Waiting to run
2026-07-21 14:24:13 +08:00
cheney d93316be15 publish
TDevOPsCICD / build-image (push) Has been cancelled
2026-06-17 11:13:02 +08:00
cheney 97614a9079 remoteshell 2026-06-17 10:52:30 +08:00
cheney 37fa4cf020 save 2026-06-17 10:46:31 +08:00
cheney 4207d6e467 更新 cli 命令
TDevOPsCICD / build-image (push) Waiting to run
2026-06-17 10:31:26 +08:00
cheney a7f596aa0a 调试通过 2026-06-17 10:25:27 +08:00
cheney 364efbc1c1 修改 meta.json 规则, 涉及version 、group 字段. 2026-06-17 10:03:51 +08:00
cheney fc81584878 上传成功,准备修改路径 2026-06-17 09:30:22 +08:00
cheney 0132be7586 添加 docker runsh 命令 2026-06-17 09:13:30 +08:00
cheney e05ce19eac 整理远程模块
TDevOPsCICD / build-image (push) Waiting to run
2026-06-16 16:40:34 +08:00
cheney b46cddc7cf 完善 cicd 命令
TDevOPsCICD / build-image (push) Waiting to run
2026-06-16 09:59:46 +08:00
cheney 98de345c5f 打包过程不顺利
TDevOPsCICD / build-image (push) Has been cancelled
2026-04-04 20:57:28 +08:00
cheney b46258d014 集成到 cli 2026-04-04 20:40:52 +08:00
cheney 05e8de9686 完善验签逻辑 2026-04-04 20:34:09 +08:00
cheney b73d9b0aac 测试通过
TDevOPsCICD / build-image (push) Waiting to run
2026-04-04 18:24:03 +08:00
cheney 03169b5165 验证失败 2026-04-04 18:06:09 +08:00
cheney 959f058def sm2 验签不通过 2026-04-04 17:54:13 +08:00
cheney f9d9203349 sm2 验签不通过 2026-04-04 17:45:53 +08:00
cheney 6e95e8a1b1 sm2 验签通过 2026-04-04 17:29:00 +08:00
cheney 9d826fb985 rsa p7 验签通过 2026-04-04 08:32:06 +08:00
cheney 5a033318c0 rsa p7 info 通过 2026-04-04 08:24:27 +08:00
cheney e97d7d7fd0 添加快速发布
TDevOPsCICD / build-image (push) Has been cancelled
2026-01-27 17:10:20 +08:00
cheney 24961dc734 优化显示
TDevOPsCICD / build-image (push) Waiting to run
2026-01-27 16:59:57 +08:00
cheney 6ddcff5275 build info
TDevOPsCICD / build-image (push) Waiting to run
2026-01-27 16:12:31 +08:00
cheney d5e12945e1 修正 platform 命令 2026-01-27 15:59:24 +08:00
cheney 714572994a linux 打包方式回到最老的方式
TDevOPsCICD / build-image (push) Waiting to run
2026-01-27 14:06:16 +08:00
cheney a226bc65dd net scan
TDevOPsCICD / build-image (push) Waiting to run
2026-01-27 10:13:12 +08:00
cheney 5a5bb80927 :7 testci
TDevOPsCICD / build-image (push) Failing after 4m49s
2025-10-22 16:01:12 +08:00
cheney e6c264f226 :6 testci
TDevOPsCICD / build-image (push) Failing after 6m42s
2025-10-22 15:46:15 +08:00
cheney 94709a25e6 :5 testci
TDevOPsCICD / build-image (push) Failing after 1m21s
2025-10-22 15:41:15 +08:00
cheney 38cf2d53d8 :4 testci
TDevOPsCICD / build-image (push) Successful in 1m28s
2025-10-22 15:05:02 +08:00
cheney 9cef28b135 :3 ci
TDevOPsCICD / build-image (push) Failing after 6m41s
2025-10-22 14:51:56 +08:00
cheney 0e15380aa3 :2 testci
TDevOPsCICD / build-image (push) Failing after 52s
2025-10-22 14:42:00 +08:00
cheney 9c62274108 :1 test ci
TDevOPsCICD / build-image (push) Failing after 1m22s
2025-10-22 14:39:24 +08:00
Cheney 0ca130f8e8 测试 CICD
TDevOPsCICD / build-image (push) Successful in 56s
2025-04-23 15:55:54 +08:00
Cheney 25f48f8ede 测试 CICD
TDevOPsCICD / build-image (push) Waiting to run
2025-04-23 15:54:37 +08:00
Cheney a849c280f7 测试 CICD
TDevOPsCICD / build-image (push) Failing after 30s
2025-04-23 15:39:57 +08:00
Cheney b99b0b8299 测试 CICD
TDevOPsCICD / build-image (push) Failing after 30s
2025-04-23 15:10:18 +08:00
Cheney 60c1e61f73 测试 CICD
TDevOPsCICD / lint-and-test (18.x) (push) Has been cancelled
TDevOPsCICD / lint-and-test (20.x) (push) Has been cancelled
TDevOPsCICD / build-image (push) Has been cancelled
2025-04-16 15:34:09 +08:00
Cheney c2119665fd remote 命令 2025-03-13 16:02:37 +08:00
Cheney 658a7031b6 remote 命令 2025-03-13 15:48:22 +08:00
Cheney 7eccf1bf15 remote 命令 2025-03-13 15:37:01 +08:00
Cheney ecff0496cb remote 命令 2025-03-13 15:31:16 +08:00
Cheney d05bb4cfe6 remote 命令 2025-03-13 15:27:23 +08:00
Cheney 37af0bd7a6 修改配置路径 2025-03-13 15:16:30 +08:00
Cheney 5ff621c05b publish 功能 2025-03-13 11:27:11 +08:00
Cheney 5f5e73e1c9 更新 platform 打印格式 2025-03-12 11:23:44 +08:00
Cheney 77e610b38b 添加脚本和 meta 2025-03-12 10:27:24 +08:00
Cheney 97a76195c2 add os release 2025-03-11 17:29:48 +08:00
Cheney 8149846f16 git problem 2025-03-11 16:50:44 +08:00
Cheney 5713e9e663 update 2025-03-11 14:59:31 +08:00
Cheney 4b6508a606 run 命令试用 2024-09-03 15:40:58 +08:00
Cheney 4275120cba 添加 freem 命令 2024-09-02 17:37:31 +08:00
Cheney 2acd92086c 恢复一个误修改 2024-09-02 17:36:34 +08:00
101 changed files with 10029 additions and 1845 deletions
+146
View File
@@ -0,0 +1,146 @@
name: TDevOpsCICD
on: [push]
jobs:
build-kit:
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: 从配置中心加载密钥
env:
CONFIG_CENTER_URL: ${{ secrets.CONFIG_CENTER_URL }}
CONFIG_CENTER_TOKEN: ${{ secrets.CONFIG_CENTER_TOKEN }}
run: |
set -e
BASE_URL="${CONFIG_CENTER_URL:-http://bh.vps.honor3.com:8008}"
AUTH_ARGS=()
if [ -n "$CONFIG_CENTER_TOKEN" ]; then
AUTH_ARGS=(-H "Authorization: Bearer ${CONFIG_CENTER_TOKEN}")
fi
curl -fsSL "${AUTH_ARGS[@]}" "$BASE_URL/api/projects" -o /tmp/cc-projects.json
node -e '
const fs = require("fs");
const projects = JSON.parse(fs.readFileSync("/tmp/cc-projects.json", "utf8")).data;
const proj = projects.find(p => p.name === "CommonExternalService");
if (!proj) { console.error("project CommonExternalService not found"); process.exit(1); }
fs.writeFileSync("/tmp/cc-project-id", proj.id);
'
PROJECT_ID=$(cat /tmp/cc-project-id)
curl -fsSL "${AUTH_ARGS[@]}" "$BASE_URL/api/projects/$PROJECT_ID/configs" -o /tmp/cc-configs.json
node -e '
const fs = require("fs");
const data = JSON.parse(fs.readFileSync("/tmp/cc-configs.json", "utf8")).data;
const allow = ["CF_API_TOKEN","CF_R2_PUBLIC_URL","CF_R2_BUCKET","GH_TOKEN","GH_OWNER","GH_REPO","GH_BRANCH"];
const loaded = [];
for (const c of data) {
const key = String(c.key).split("/").pop();
if (allow.includes(key) && c.value) {
fs.appendFileSync(process.env.GITHUB_ENV, `${key}=${c.value}\n`);
loaded.push(key);
}
}
console.log("loaded config keys:", loaded.join(", "));
'
- name: Setup Bun
run: |
curl -fsSL https://bun.sh/install | bash
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
echo "BUN_INSTALL=$HOME/.bun" >> $GITHUB_ENV
echo "$HOME/.bun/bin" >> $GITHUB_PATH
- name: Install pkg
run: npm i -g pkg
- name: Install deps
run: |
cd kit
bun i
- name: Build Windows (bun 交叉编译)
run: |
cd kit
bun run windows
ls -la ./dist/windows/
- name: Build Linux (bun, 现代 glibc)
run: |
cd kit
bun run linux
ls -la ./dist/linux/
- name: Build Linux (pkg, 低版本 glibc / Kylin V10) + 打包 zip
run: |
set -e
cd kit
# pkg 首次构建需要从 GitHub 下载 node14 基础运行时,网络不稳时重试
for i in 1 2 3; do npm run pkg-linux && break || sleep 5; done
test -f ./dist/kylin/kit || { echo "pkg build failed"; exit 1; }
if ! command -v zip >/dev/null 2>&1; then
(apt-get update -qq && apt-get install -y -qq zip) >/dev/null 2>&1 || true
fi
command -v zip >/dev/null 2>&1 || { echo "zip not available"; exit 1; }
cd ./dist/kylin
zip -9 -j kit.zip kit
rm -f kit
ls -la .
- name: 上传二进制到 Cloudflare R2(公开桶)
run: |
set -e
test -n "$CF_API_TOKEN" || { echo "CF_API_TOKEN 未从配置中心加载"; exit 1; }
npm i -g wrangler
export CLOUDFLARE_API_TOKEN="$CF_API_TOKEN"
# 仅需 token,wrangler 会自动从 token 解析账号 ID,无需 CF_ACCOUNT_ID
BUCKET="${CF_R2_BUCKET:-store}"
wrangler r2 object put "$BUCKET/data/windows/kit.exe" --file ./kit/dist/windows/kit.exe --content-type application/octet-stream --remote
wrangler r2 object put "$BUCKET/data/linux/kit" --file ./kit/dist/linux/kit --content-type application/octet-stream --remote
wrangler r2 object put "$BUCKET/data/kylin/kit.zip" --file ./kit/dist/kylin/kit.zip --content-type application/zip --remote
- name: 发布 website 到 GitHub(触发 Cloudflare Pages 自动部署)
env:
REF_NAME: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
set -e
test -n "$GH_TOKEN" || { echo "GH_TOKEN 未从配置中心加载"; exit 1; }
mkdir -p website/static/data
cat > website/static/data/kit.json <<EOF
{
"version": "${REF_NAME}",
"commit": "${SHA}",
"updatedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"binaries": {
"windows": { "url": "${CF_R2_PUBLIC_URL}/data/windows/kit.exe" },
"linux": { "url": "${CF_R2_PUBLIC_URL}/data/linux/kit" },
"kylin": { "url": "${CF_R2_PUBLIC_URL}/data/kylin/kit.zip" }
}
}
EOF
cat website/static/data/kit.json
URL="https://x-access-token:${GH_TOKEN}@github.com/${GH_OWNER}/${GH_REPO}.git"
BRANCH="${GH_BRANCH:-main}"
rm -rf /tmp/website-pub
if git clone --depth 1 --branch "$BRANCH" "${URL}" /tmp/website-pub 2>/dev/null; then
cp -a website/. /tmp/website-pub/
else
rm -rf /tmp/website-pub
mkdir -p /tmp/website-pub
cd /tmp/website-pub
git init -b "$BRANCH"
git remote add origin "${URL}"
cp -a "$GITHUB_WORKSPACE/website/." .
fi
cd /tmp/website-pub
git config user.name "kit-ci"
git config user.email "kit-ci@honor3.com"
git add -A
if git diff --cached --quiet; then
echo "No website changes to publish"
else
git commit -m "release(website): publish kit ${REF_NAME}"
git push origin "$BRANCH"
fi
+5 -1
View File
@@ -1,2 +1,6 @@
node_modules
dist
dist
build.json
kit/.kit/modules/*
kit/publish/*
.wrangler
+68
View File
@@ -0,0 +1,68 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AliAccessStaticViaInstance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliArrayNamingShouldHaveBracket" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliDeprecation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliEqualsAvoidNull" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliLongLiteralsEndingWithLowercaseL" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliMissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliWrapperTypeEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAbstractClassShouldStartWithAbstractNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAbstractMethodOrInterfaceMethodMustUseJavadoc" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidApacheBeanUtilsCopy" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidCallStaticSimpleDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidCommentBehindStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidComplexCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidConcurrentCompetitionRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidDoubleOrFloatEqualCompare" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidManuallyCreateThread" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidMissUseOfMathRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidNegationOperator" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidNewDateGetTime" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidPatternCompileInMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidReturnInFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidStartWithDollarAndUnderLineNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidUseTimer" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaBigDecimalAvoidDoubleConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaBooleanPropertyShouldNotStartWithIs" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassCastExceptionWithSubListToArrayList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassCastExceptionWithToArray" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassMustHaveAuthor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassNamingShouldBeCamel" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCollectionInitShouldAssignCapacity" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCommentsMustBeJavadocFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaConcurrentExceptionWithModifyOriginSubList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaConstantFieldShouldBeUpperCase" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCountDownShouldInFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaDontModifyInForeachCircle" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaEnumConstantsMustHaveComment" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaExceptionClassShouldEndWithException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaIbatisMethodQueryForList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaLockShouldWithTryFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaLowerCamelCaseVariableNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaMethodReturnWrapperType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaMethodTooLong" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPackageNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoMustOverrideToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoMustUsePrimitiveField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoNoDefaultValue" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaRemoveCommentedCode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaServiceOrDaoClassShouldEndWithImpl" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSneakyThrowsWithoutExceptionType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaStringConcat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSwitchExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaTestClassShouldEndWithTestNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadLocalShouldRemove" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadPoolCreation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadShouldSetName" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaTransactionMustHaveRollback" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUndefineMagicConstant" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUnsupportedExceptionWithModifyAsList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUseQuietReferenceNotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUseRightCaseForDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
+9
View File
@@ -0,0 +1,9 @@
# Readme
+1 -1
View File
@@ -19,7 +19,7 @@ module.exports = class CliCmdLine {
let param = cmdLine.params[pname];
if ( param.isFlag ) {
if ( param.needValue ){
if ( params[pname] ) {
if ( params[pname] !== undefined ) {
this.params[pname] = params[pname]
} else {
if ( ! param.isOptional ) {
+117 -109
View File
@@ -1,110 +1,118 @@
const CmdLine = require("./CmdLine")
const CliCmdLine = require("./CliCmdLine")
const Config = require("./config")
const gConfig = new Config();
module.exports = class CliCmdParser {
constructor(name) {
// 软件名称
this.name = name
this.cmdLines = {}
this.funcs = {}
}
/**
* 声明一条命令
* @param cmdLine 命令模板
* @param info 命令说明
* @param func 响应函数
*/
addCmdLine(cmdLine, info, func) {
let cl = CmdLine.parse(cmdLine, info);
if( this.cmdLines[ cl.cmd ] ) {
throw "重复注册的命令 " + cl.cmd
}
this.cmdLines[ cl.cmd ] = cl;
if ( func ) {
this.funcs[ cl.cmd ] = func;
}
return cl;
}
/**
* 打印命令帮助
*/
help(){
let bs = ""
if( this.name ) {
bs += this.name + gConfig.crlf
}
for ( let cmd in this.cmdLines ) {
bs = bs.concat("\t")
.concat( this.cmdLines[ cmd ].line )
.concat("\t")
.concat( this.cmdLines[ cmd ].info )
.concat( gConfig.crlf );
}
return bs;
}
canEval( cliCmdLine ){
return !! this.funcs[ cliCmdLine.cmdline.cmd ];
}
async eval( cliCmdLine ){
let func = this.funcs[ cliCmdLine.cmdline.cmd ];
if ( null == func ) {
throw "没找到命令相应函数";
}
return await func( cliCmdLine );
}
parse(argv){
if ( ! argv || argv.length < 1 ) {
return null;
}
if ( argv[0].startsWith("-") ) {
throw "入参命令格式错误 : 没有入参 cmd"
}
if ( ! this.cmdLines[ argv[0] ] ) {
return null;
}
// 标记: 去除 - 之后的值
const flags = new Set([])
// 命名参数: 去除 -- 之后的值为 key,之后值为 value
const params = {}
// 位置参数:值
const position = []
for (let i = 1; i < argv.length; i++ ) {
if ( argv[i].startsWith("--") ) {
if ( i + 1 == argv.length || argv[ i + 1 ].startsWith("-") ) {
throw "flag 格式错误" + argv[i];
}
params[ argv[i].substring(2) ] = argv[++i];
} else if ( argv[i].startsWith("-") ) {
flags.add( argv[i].substring(1) );
} else {
position.push( argv[i] );
}
}
let line = this.cmdLines[ argv[0] ];
// 整理
// line 是藐视
// flags, params, position 是值
return new CliCmdLine( this, line, flags, params, position);
}
const CmdLine = require("./CmdLine")
const CliCmdLine = require("./CliCmdLine")
const Config = require("./Config")
const gConfig = new Config();
module.exports = class CliCmdParser {
constructor(name) {
// 软件名称
this.name = name
this.cmdLines = {}
this.funcs = {}
}
/**
* 声明一条命令
* @param cmdLine 命令模板
* @param info 命令说明
* @param func 响应函数
*/
addCmdLine(cmdLine, info, func) {
let cl = CmdLine.parse(cmdLine, info);
if( this.cmdLines[ cl.cmd ] ) {
throw "重复注册的命令 " + cl.cmd
}
this.cmdLines[ cl.cmd ] = cl;
if ( func ) {
this.funcs[ cl.cmd ] = func;
}
return cl;
}
/**
* 打印命令帮助
*/
help(){
let bs = ""
if( this.name ) {
bs += this.name + gConfig.crlf
}
for ( let cmd in this.cmdLines ) {
bs = bs.concat("\t")
.concat( this.cmdLines[ cmd ].line )
.concat("\t")
.concat( this.cmdLines[ cmd ].info )
.concat( gConfig.crlf );
}
return bs;
}
canEval( cliCmdLine ){
return !! this.funcs[ cliCmdLine.cmdline.cmd ];
}
async eval( cliCmdLine ){
let func = this.funcs[ cliCmdLine.cmdline.cmd ];
if ( null == func ) {
throw "没找到命令相应函数";
}
return await func( cliCmdLine );
}
parse(argv){
if ( ! argv || argv.length < 1 ) {
return null;
}
if ( argv[0].startsWith("-") ) {
throw "入参命令格式错误 : 没有入参 cmd"
}
if ( ! this.cmdLines[ argv[0] ] ) {
return null;
}
// 标记: 去除 - 之后的值
const flags = new Set([])
// 命名参数: 去除 -- 之后的值为 key,之后值为 value
const params = {}
// 位置参数:值
const position = []
let line = this.cmdLines[ argv[0] ];
for (let i = 1; i < argv.length; i++ ) {
if ( argv[i].startsWith("--") ) {
const name = argv[i].substring(2);
if ( i + 1 == argv.length || argv[ i + 1 ].startsWith("-") ) {
const param = line.params[name];
if ( ! param || ! param.isOptional ) {
throw "flag 格式错误" + argv[i];
}
// 可选命名参数允许不带值,值为空串,由命令自行做默认值
params[ name ] = "";
} else {
params[ name ] = argv[++i];
}
} else if ( argv[i].startsWith("-") ) {
flags.add( argv[i].substring(1) );
} else {
position.push( argv[i] );
}
}
// 整理
// line 是藐视
// flags, params, position 是值
return new CliCmdLine( this, line, flags, params, position);
}
}
+10 -7
View File
@@ -1,14 +1,17 @@
FROM node:latest
FROM node:20-bookworm
LABEL authors="hq@gpio.me"
WORKDIR /app
ADD index.js /app
RUN npm config set registry http://registry.npmmirror.com
RUN apt-get update \
&& apt-get install -y --no-install-recommends git curl zip python3 \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://bun.sh/install | bash \
&& ln -s /root/.bun/bin/bun /usr/local/bin/bun
RUN npm i pkg -g
RUN pkg -t node14-alpine-arm64 index.js
RUN pkg -t node14-linux-arm64 index.js
ENV PATH="/root/.bun/bin:${PATH}"
+175
View File
@@ -0,0 +1,175 @@
# Registry 模块设计文档
## 概述
kit 的 registry 模块用于对接 Registry 配置管理中心(`D:\workbench\registry`),提供命令行方式管理微服务配置。
Registry 是一个基于 REST API 的配置管理系统,支持项目隔离、三维配置(机房/环境/分组)、规划/编辑/只读三种模式。
## 对接方式
kit 通过 HTTP 请求调用 Registry REST API,使用 Node 内置 `http`/`https` 模块,不引入新依赖。
## 配置存储
registry 的连接信息复用 kit 已有的 `kit config` 机制:
```shell
kit config registryUrl http://bh.vps.honor3.com:8008
kit config registryToken abc123
```
## 命令设计
### 配置管理
复用 `kit config`,无需新增命令。
### 项目与配置操作
| 命令 | 说明 | 示例 |
|------|------|------|
| `kit registry list` | 列出所有项目(含详情) | `kit registry list` |
| `kit registry config <project>[:path]` | 查看/设置配置项 | 见下方 |
#### `kit registry list`
列出 Registry 上所有项目,输出项目名称、ID、维度启用状态、配置数量。
```shell
$ kit registry list
ID 名称 维度
proj-abc-123 CommonExternalService IDC=✓ Env=✓ Group=✓
proj-def-456 OrderService IDC=✗ Env=✓ Group=✓
共 2 个项目
```
#### `kit registry config <project>[:path]`
三种用法:
**1. 查看配置列表**(不指定 key)
```shell
# 查看整个项目
kit registry config CommonExternalService
# 查看路径下的配置
kit registry config CommonExternalService:data/windows
```
输出:
```
路径 值 类型
CF_API_TOKEN **** string
CF_R2_BUCKET store string
CF_R2_PUBLIC_URL https://pub-xxx.r2.dev string
```
**2. 获取单个值**(指定 key)
```shell
kit registry config CommonExternalService:CF_R2_BUCKET
# 输出: store
```
**3. 设置值**(指定 key + value)
```shell
kit registry config CommonExternalService:CF_R2_BUCKET my-new-bucket
```
### 导入导出
| 命令 | 说明 | 示例 |
|------|------|------|
| `kit registry export <project>[:path] [file]` | 导出为 JSON | `kit registry export CommonExternalService:data ./backup.json` |
| `kit registry import <project>[:path] <file>` | 从 JSON 导入 | `kit registry import CommonExternalService:data ./backup.json` |
#### `project:path` 语法
`project:path` 中的 `path` 对应 Registry 中配置的路径前缀(由维度映射 + key 构成)。
| 语法 | 含义 |
|------|------|
| `proj` | 整个项目的所有配置 |
| `proj:data` | 路径以 `data` 开头的配置 |
| `proj:data/windows` | 路径以 `data/windows` 开头的配置 |
| `proj:data/windows/kit.exe` | 精确匹配单个配置 |
#### export 示例
```shell
# 导出整个项目
kit registry export CommonExternalService ./backup.json
# 仅导出 data/windows 下的配置
kit registry export CommonExternalService:data/windows ./backup-windows.json
```
导出文件格式:
```json
{
"project": "CommonExternalService",
"path": "data/windows",
"exportedAt": "2026-09-11T02:00:00Z",
"configs": [
{
"key": "kit.exe",
"value": "...",
"type": "string",
"description": "Windows 二进制下载地址",
"idc": "",
"environment": "",
"group": ""
}
]
}
```
#### import 示例
```shell
# 从备份文件导入整个项目
kit registry import CommonExternalService ./backup.json
# 将备份导入到指定路径下
kit registry import CommonExternalService:data/windows ./backup-windows.json
```
import 行为:
- 已存在的 key(按路径匹配)→ 更新 value
- 不存在的 key → 新增
- 远程有但本地没有的 → 保留不动(不删除)
## API 映射
| kit 命令 | Registry API |
|----------|-------------|
| `kit registry list` | `GET /api/projects` |
| `kit registry config <project>` | `GET /api/projects/:id/configs` |
| `kit registry config <project>:<key>` | `GET /api/projects/:id/configs` → 按 path 匹配 |
| `kit registry config <project>:<key> <value>` | `GET` → 匹配后 `PUT` 或 `POST` |
| `kit registry export` | `GET /api/projects/:id/configs` → 写文件 |
| `kit registry import` | 读文件 → 逐项 `PUT`/`POST` |
## 文件结构
```
kit/src/registry/
├── index.js # 命令注册入口
├── client.js # HTTP 客户端(调用 Registry API)
└── sync.js # export/import 逻辑
```
## 错误处理
- 未配置 `registryUrl` → 提示 `请先运行 kit config registryUrl <url>`
- 连接失败 → 输出 `Registry 不可达: <url>`
- 认证失败 → 输出 `认证失败,请检查 registryToken`
- 项目不存在 → 输出 `项目不存在: <name>`
- 只读模式下写入 → 输出 `Registry 当前为只读模式`
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
bun /app/kit.program/kit/src/index.js $@
+47
View File
@@ -0,0 +1,47 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "remote_test",
"runtimeExecutable": "bun",
"runtimeArgs": ["test", "./test/remote_test.js", "-t", "exposes documented public methods", "--inspect-brk"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/**/*.js"]
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Debug File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"noDebug": true,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "attach",
"name": "Attach Bun",
"url": "ws://localhost:6499/e4xu5fgl1a",
"stopOnEntry": false
}
]
}
+22
View File
@@ -0,0 +1,22 @@
// build-time.js
const fs = require('fs');
const path = require('path');
// 生成当前时间的 yyyyMMdd 格式
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const buildTime = `${year}${month}${day}`;
// 创建 JSON 对象
const buildData = {
buildTime: buildTime,
timestamp: now.toISOString()
};
// 写入文件
const filePath = path.join(__dirname, 'build.json');
fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
console.log(`Build time ${buildTime} written to build.json`);
+366
View File
@@ -0,0 +1,366 @@
- 基于 webdav 协议访问。包括**远程仓库存储方案**(基于 WebDAV 协议)与**本地客户端存储方案**。
- 支持存储多种操作系统(Linux、Windows)、多种架构(X86、aarch64)
- 支持记录官网地址,方便定期从官网拉取新版本存储。
---
## Part A:远程仓库存储方案(基于 WebDAV)
### 1. 概述
本方案定义了一种基于 WebDAV 协议访问的远程仓库存储格式,用于存储开源项目的目标代码(二进制分发文件)。仓库支持:
- 多种操作系统(Linux、Windows、macOS、FreeBSD 等)
- 多种硬件架构(x86_64、aarch64、armv7、i386 等)
- 平台无关的二进制文件(如 Java JAR、Python 轮包、纯 JS 文件等)
- 单文件可执行程序与多文件压缩包(zip、tar.gz 等)
- 版本管理(语义化版本)与元数据记录
仓库通过 WebDAV 的目录和文件操作进行访问,支持枚举、下载、上传、删除等操作。
### 2. 仓库根目录结构
```
/repo/ # WebDAV 服务暴露的根路径
└─ {group}/ # 分组目录,默认 /modules;空值等同 /
└─ {project-id}/ # 项目目录(唯一标识),实际路径为 group/id
├─ metadata.json # 项目级元数据文件
└─ {version}/ # 版本目录(如 1.0、1.1、2.0)
├─ version.json # 版本级元数据文件(推荐)
├─ any/any/ # 平台无关文件目录(保留字)
│ └─ ... # 单文件或压缩包
├─ {os}/ # 操作系统(如 linux, windows, darwin)
│ └─ {arch}/ # 架构(如 x86_64, aarch64)
│ └─ ... # 具体二进制文件或压缩包
└─ ...
```
#### 2.1 命名规则
- `project-id`:来自项目 `meta.json` 中的唯一 `id`,小写字母、数字、连字符(-),长度 ≤ 64。
- `group`:来自项目 `meta.json`,默认 `/modules`;空字符串等同 `/`;支持 `/docker`、`/tmpls`、`/docker/net` 等子路径。
- `version`:来自项目 `meta.json` 的 `version`;若为空,发布时根据远程 `metadata.json.version` 尾数 +1,远程不存在时初始为 `1.0`。
- `os` 与 `arch` 使用标准化名称(见下表),`any` 为保留字,表示任意平台。
| 操作系统 | 标准名称 | 架构 | 标准名称 |
| ------- | --------- | ------- | --------- |
| Linux | `linux` | x86_64 | `x86_64` |
| Windows | `windows` | aarch64 | `aarch64` |
| macOS | `darwin` | armv7 | `armv7` |
| FreeBSD | `freebsd` | i386 | `i386` |
| 任意平台 | `any` | 任意架构 | `any` |
#### 2.2 目录说明
- **`any/any/`**:存放与操作系统和架构均无关的文件(如 JAR、纯 Python 脚本、WASM 模块)。客户端在任何平台均可直接使用此目录下的文件。
- **`{os}/{arch}/`**:存放特定平台、特定架构的二进制文件或压缩包。
- 同一版本下可同时包含 `any/any/` 和具体平台目录。
### 3. 元数据文件规范
#### 3.1 项目级元数据:`metadata.json`
位于每个项目根目录,UTF-8 编码,JSON 格式。
**字段说明**:
| 字段名 | 类型 | 必选 | 说明 |
| `project_id` | string | 是 | 项目唯一标识,与目录名相同 |
| `group` | string | 否 | 仓库分组路径,默认 `/modules`,空值等同 `/` |
| `official_url` | string | 是 | 项目官网地址(用于定期拉取新版本) |
| `description` | string | 否 | 项目简述 |
| `version` | string | 是 | 当前远程最新版本号;不再使用 `versions` 数组 |
| `last_fetch_check` | string (ISO 8601) | 否 | 上次检查官网更新的时间戳 |
| `fetch_rules` | array of objects | 否 | 自动拉取时的匹配规则(见 3.3) |
| `extra` | object | 否 | 扩展字段 |
**示例**:
```json
{
"project_id": "myapp",
"group": "/modules",
"official_url": "",
"description": "An example tool",
"version": "1.2",
"last_fetch_check": "2026-06-15T12:00:00Z",
"fetch_rules": [...],
"extra": { "license": "Apache-2.0" }
}
```
#### 3.2 版本级元数据:`version.json`
位于每个版本目录下,包含该版本的详细信息和文件清单。
**字段说明**:
| 字段名 | 类型 | 必选 | 说明 |
| ----------------------- | ------------------ | ---- | ---------------------------------------------- |
| `version` | string | 是 | 版本号,与目录名一致 |
| `release_date` | string (ISO 8601) | 否 | 官方发布日期 |
| `official_download_url` | string | 否 | 官网的直接下载页或 API 地址 |
| `artifacts` | array of objects | 是 | 该版本包含的所有文件条目(见 3.2.1) |
| `checksums` | object | 否 | 文件路径到哈希值的映射(可选,与 artifacts 可共存) |
| `signatures` | object | 否 | 文件路径到数字签名的映射 |
##### 3.2.1 `artifacts` 数组中每个对象的字段
| 字段名 | 类型 | 必选 | 说明 |
| ----------------- | --------- | ---- | ------------------------------------------------------------ |
| `path` | string | 是 | 相对于版本目录的文件路径,如 `linux/x86_64/myapp` 或 `any/any/app.zip` |
| `type` | string | 是 | 文件类型:`executable`(单文件可执行)、`archive`(压缩包)、`library`(库文件)、`data`(资源数据) |
| `archive_format` | string | 否 | 当 `type="archive"` 时,取值为 `zip`、`tar.gz`、`tar.xz` 等 |
| `entry_point` | string | 否 | 当 `type="archive"` 时,指定压缩包内的可执行文件相对路径(如 `bin/app`) |
| `unpack` | boolean | 否 | 是否建议客户端自动解压,默认 `false`。若为 `true`,客户端解压后应运行 `entry_point` |
| `checksum` | string | 否 | 该文件的哈希值(格式 `算法:值`,如 `sha256:abc...`) |
**示例**:
- **单文件可执行(Linux)**
```json
{
"version": "1.0.0",
"artifacts": [
{
"path": "linux/x86_64/myapp",
"type": "executable"
}
],
"checksums": {
"linux/x86_64/myapp": "sha256:e3b0c442..."
}
}
```
- **多文件压缩包(Windows)**
```json
{
"version": "2.0.0",
"artifacts": [
{
"path": "windows/x86_64/myapp.zip",
"type": "archive",
"archive_format": "zip",
"entry_point": "myapp/bin/myapp.exe",
"unpack": true
}
]
}
```
- **平台无关 JAR 文件**
```json
{
"version": "3.0.0",
"artifacts": [
{
"path": "any/any/myapp.jar",
"type": "executable"
}
]
}
```
#### 3.3 自动拉取规则(`fetch_rules`)
在 `metadata.json` 中可定义 `fetch_rules` 数组,用于指导自动化工具从官网下载并分类存储。
每个规则对象包含:
| 字段名 | 类型 | 说明 |
| ----------------- | --------- | ------------------------------------------------------------ |
| `url_pattern` | string | 正则表达式,匹配下载 URL |
| `os` | string | 目标操作系统(`linux`、`windows`、`darwin`、`any` 等) |
| `arch` | string | 目标架构(`x86_64`、`aarch64`、`any` 等) |
| `type` | string | 文件类型(`executable` 或 `archive`) |
| `archive_format` | string | 可选,当 `type="archive"` 时指定 |
| `entry_point` | string | 可选,压缩包内的入口点 |
| `unpack` | boolean | 可选,是否自动解压 |
**示例**:
```json
"fetch_rules": [
{
"url_pattern": ".*-linux-amd64\\.tar\\.gz$",
"os": "linux",
"arch": "x86_64",
"type": "archive",
"archive_format": "tar.gz",
"entry_point": "myapp/bin/myapp",
"unpack": true
},
{
"url_pattern": ".*\\.jar$",
"os": "any",
"arch": "any",
"type": "executable"
}
]
```
### 4. 客户端访问方式
基于 WebDAV 协议,客户端可通过以下 HTTP 方法操作:
- `PROPFIND`:列举项目、版本、os/arch 目录。
- `GET`:下载二进制文件或元数据文件。
- `PUT` / `MKCOL`:上传新版本或新文件(用于手动或自动填充)。
- `DELETE`:删除过期版本。
推荐 WebDAV 服务器配置:启用目录列表、支持 CORS、支持 HTTPS。
### 5. 自动化拉取新版本流程
外部工具(如 cron 作业)按以下步骤更新仓库:
1. 遍历仓库根目录下每个项目的 `metadata.json`。
2. 访问 `official_url`,获取所有可用版本列表及最新版本。
3. 对比已存储版本,找出缺失的新版本。
4. 对于每个新版本,根据 `fetch_rules`(或内置逻辑)匹配下载 URL,下载对应的二进制/压缩包。
5. 按 `os` / `arch` / `any` 规则存放到 `{group}/{project-id}/{version}/{os}/{arch}/` 下。
6. 计算文件哈希,生成或更新该版本的 `version.json`。
7. 更新项目级 `metadata.json`(`version`、`group`、`last_fetch_check`),不再写入 `versions`。
---
## Part B:本地客户端存储方案(用于版本管理与回退)
### 1. 概述
客户端从远程 WebDAV 仓库下载项目的目标代码后,需要在本地持久化存储,并支持:
- 多版本共存
- 当前激活版本的快速切换(符号链接)
- 版本回退(无需重新下载)
- 压缩包自动解压与入口点管理
- 元数据记录(安装历史、校验信息)
- 可配置的旧版本清理策略
### 2. 本地根目录结构
客户端约定一个根目录(如 `~/.cache/artifact-repo/` 或 `~/.local/share/artifact-repo/`),其下按项目组织:
```
~/.local/share/artifact-repo/
└─ {project-id}/ # 项目目录(与远程 project-id 一致)
├─ .meta/ # 元数据目录(隐藏)
│ ├─ install.json # 本地安装记录
│ └─ history/ # 可选,操作历史 JSON 文件
├─ versions/ # 所有已下载的版本
│ ├─ {semver}/ # 版本目录(如 1.2.0)
│ │ ├─ .version.json # 从远程复制的 version.json,附加本地信息
│ │ └─ {os}/{arch}/ # 与远程结构一致的实际文件
│ │ └─ ...
│ └─ {semver}/...
└─ current -> versions/{semver} # 符号链接,指向当前激活的版本
```
> **注意**:符号链接可放在项目根目录下,也可放在 `.meta/current`。推荐项目根目录下的 `current`,便于外部脚本直接引用(如 `$REPO_HOME/myapp/current/bin/myapp`)。
### 3. 元数据文件 `install.json`
位于 `.meta/install.json`,记录项目的本地安装状态。
**字段说明**:
| 字段名 | 类型 | 说明 |
| -------------------- | ------------------ | ------------------------------------------------------------ |
| `project_id` | string | 项目标识 |
| `installed_versions` | array of strings | 已下载到本地的所有版本号 |
| `current_version` | string | 当前激活的版本号(应与 `current` 符号链接一致) |
| `last_updated` | string (ISO 8601) | 最后一次变更时间(安装、切换、删除) |
| `history` | array of objects | 操作历史记录,每个元素包含 `timestamp`, `action`, `version`, `from`(可选) |
| `options` | object | 客户端配置,如 `auto_cleanup`, `max_versions` 等 |
**示例**:
```json
{
"project_id": "myapp",
"installed_versions": ["1.0.0", "1.2.0", "1.2.1"],
"current_version": "1.2.1",
"last_updated": "2026-06-15T14:30:00Z",
"history": [
{
"timestamp": "2026-06-10T09:00:00Z",
"action": "install",
"version": "1.0.0"
},
{
"timestamp": "2026-06-15T14:30:00Z",
"action": "switch",
"version": "1.2.1",
"from": "1.2.0"
}
],
"options": {
"auto_cleanup": true,
"max_versions": 5
}
}
```
### 4. 版本目录中的 `.version.json`
从远程仓库的 `version.json` 复制而来,并增加本地特有字段:
```json
{
"version": "1.2.1",
"installed_at": "2026-06-15T14:25:00Z",
"local_checksums": {
"linux/x86_64/myapp": "sha256:..."
},
... // 原始远程字段(artifacts, release_date 等)
}
```
### 5. 核心操作流程
#### 5.1 安装新版本(更新)
1. 从远程 WebDAV 仓库下载指定版本的所有相关文件到 `versions/{new_version}/`,保持远程目录结构(包括 `any/any/` 和具体平台目录)。
2. 同时下载 `version.json` 并保存为 `.version.json`,添加 `installed_at` 时间戳。
3. 更新 `.meta/install.json`:
- 将 `new_version` 加入 `installed_versions`(若未存在)。
- 设置 `current_version` 为 `new_version`(若需要立即激活)。
- 添加历史记录(`action: "install"` 或 `"switch"`)。
4. 更新项目根目录下的 `current` 符号链接指向 `versions/{new_version}`。
5. 如果 `artifacts` 中有 `type="archive"` 且 `unpack=true` 的条目,则解压到版本目录下的特定子目录(如 `extracted/`),并在 `.version.json` 中记录解压后的入口点路径。
6. 根据 `options` 中的清理策略删除旧版本(保留最近 N 个版本或保留指定天数)。
#### 5.2 回退到已安装的旧版本
1. 确认目标旧版本存在于 `installed_versions` 中。
2. 修改 `current` 符号链接指向 `versions/{old_version}`。
3. 更新 `.meta/install.json`:
- 将 `current_version` 改为 `old_version`。
- 添加历史记录(`action: "rollback"` 或 `"switch"`,可记录 `from` 原版本)。
4. 无需重新下载任何文件。
#### 5.3 卸载特定版本
1. 如果卸载的是当前激活版本,先切换到另一个已安装版本(或提示用户)。
2. 删除 `versions/{version}` 整个目录。
3. 从 `install.json` 的 `installed_versions` 数组中移除该版本。
4. 添加历史记录(`action: "uninstall"`)。
#### 5.4 清理旧版本(自动或手动)
清理策略示例:
- 保留最近 `max_versions` 个版本(不计当前版本)。
- 或删除 `keep_recent_days` 天之前安装且不是当前版本的版本。
- 每次安装新版本后自动触发清理。
### 6. 处理压缩包(archive)的特别说明
当远程 `artifact` 的 `unpack=true` 时:
- 客户端下载压缩包到 `versions/{version}/{path}`。
- 立即解压到同目录下的 `_extracted/` 子目录(例如 `versions/1.2.1/linux/x86_64/_extracted/`)。
- 根据 `entry_point` 在 `.version.json` 中记录解压后的可执行文件绝对路径(或相对于版本目录的路径)。
- 符号链接 `current` 可以直接指向解压后的入口点文件,而不是整个版本目录。实现方式:
```
current/bin/myapp -> ../versions/1.2.1/_extracted/bin/myapp
```
这样切换版本时只需更新符号链接,无需重新解压。
### 7. 多平台兼容性
客户端在下载时,应根据当前运行的操作系统和架构,仅从远程仓库获取匹配的文件:
- 优先尝试 `{os}/{arch}/`
- 若不存在,回退到 `any/any/`
- 下载后保持远程的相对路径结构,但客户端实际运行时只使用与当前平台相关的文件。
在本地存储中,所有版本目录仍保留完整的 `{os}/{arch}/` 和 `any/any/` 结构,便于在不同平台间迁移或共享缓存。
+36
View File
@@ -0,0 +1,36 @@
- ## 编码前思考
- 明确假设,不确定时询问而非猜测。
- 存在歧义时,列出多种解释,不默默选定单一方案。
- 如果任务有明显更简单的做法,直接指出优化思路。
- 发现代码矛盾、逻辑不一致时及时暂停,请求信息澄清。
## 简洁优先
- 用最少的代码解决问题,拒绝冗余实现。
- 不为一次性需求创建抽象层、复杂架构。
- 不盲目增加扩展性、可配置性,应对“未来可能用到”的场景。
- 若代码可大幅精简,主动重写优化。
- 校验标准:以资深工程师视角判断,代码若过于复杂,立即简化。
## 精准修改
- 仅修改与当前任务直接相关的代码内容。
- 不顺手优化相邻代码、注释、排版格式。
- 不重构原本可以正常运行的代码模块。
- 严格匹配项目现有代码风格,保留原有编码习惯。
- 因本次修改产生的无效导入、废弃变量,可直接删除。
- 发现项目中原有的死代码、冗余内容,仅做文字提醒,不擅自删除。
## 目标驱动执行
- 执行任务前,定义清晰、可落地的成功标准。
- 将“修复Bug”转化为:编写用例复现问题,再调试至用例正常通过。
- 将“新增校验功能”转化为:针对异常输入编写测试用例,保证全部通过。
- 将“代码重构”转化为:完成重构后,确保原有所有测试用例正常运行。
- 多步骤复杂任务,先输出简短执行计划,同时标注每一步的验证方式。
## 编程规范
- 每个函数都要有中文注释, 包括功能说明、参数说明、返回值说明等,如果有注意事项,也要在注释中特别说明。
- 每次需求修改都要同时更新文档、代码、测试代码。
- 每条测试用例都要有一个唯一编号。例如 1-1、2-20 等。
+42
View File
@@ -0,0 +1,42 @@
```bash
# 验证远程仓库可达
check
# 发行某个模块 eg. myapp
publish myapp
# 列出已安装版本 eg. myapp
list myapp
# 输出:
# 1.0.0
# * 1.2.0 (current)
# 1.2.1
# 安装某个模块 eg. myapp
install myapp
# 切换到版本 1.2.0
use myapp@1.2.0
# 回滚到上一个版本
rollback myapp
# 删除模块
remove myapp
```
## 发布规则
项目 `meta.json` 支持 `group` 字段:
- 默认值为 `/modules`。
- 空字符串等同于 `/`。
- 可配置为 `/docker`、`/tmpls` 等一级路径,也可配置为 `/docker/net` 等子路径。
- 远程仓库整体上传路径为 `group/id`,其中 `id` 是项目唯一标识。
项目 `meta.json` 只使用 `version` 字段,不再使用 `versions`:
- `version` 有值时,按该版本上传;上传前检查远程 `group/id/version` 是否存在,已存在则报错。
- `version` 为空时,先读取远程 `group/id/metadata.json` 中的 `version`,将尾数 +1 后上传。
- 远程没有版本信息时,初始版本号为 `1.0`。
+5
View File
@@ -0,0 +1,5 @@
{
"linux" : "./dist/linux/kit",
"windows" : "./dist/windows/kit.exe",
"dir" : "C:\\Users\\occul\\SynologyDrive\\bin"
}
+747 -555
View File
File diff suppressed because it is too large Load Diff
+16 -3
View File
@@ -3,17 +3,30 @@
"version": "0.1",
"bin": "src/index.js",
"scripts": {
"linux": "pkg --compress GZip . -t linux -o ./dist/linux/kit",
"windows": "pkg --compress GZip . -t win -o ./dist/windows/kit.exe",
"alpine" : "pkg --compress GZip . -t node14-alpine-arm64 -o ./dist/linux/kit"
"test": "bun src/index.js",
"publish": "node publish.js",
"linux": "bun build.js && bun build ./src/index.js --compile --bytecode --minify --target=bun-linux-x64-baseline --external cpu-features --external \"*.node\" --outfile=./dist/linux/kit",
"windows": "bun build.js && bun build ./src/index.js --compile --bytecode --minify --target=bun-windows-x64-baseline --external cpu-features --external \"*.node\" --outfile=./dist/windows/kit.exe",
"pkg-alpine": "bun build.js && pkg --compress GZip . -t node14-alpine-arm64 -o ./dist/alpine/kit",
"pkg-windows": "bun build.js && pkg --compress GZip . -t node14-windows-x64 -o ./dist/windows/kit.exe",
"pkg-arm64": "bun build.js && pkg --compress GZip . -t node14-linux-arm64 -o ./dist/linux/kit",
"pkg-linux": "bun build.js && pkg --compress GZip . -t node14-linux-x64 -o ./dist/kylin/kit",
"kylin": "npm run pkg-linux"
},
"dependencies": {
"auto-launch": "^5.0.6",
"cross-port-killer": "^1.4.0",
"extract-zip": "^2.0.1",
"get-port": "^7.0.0",
"json5": "^2.2.3",
"jsrsasign": "^11.1.1",
"log4js": "^6.9.1",
"node-forge": "^1.3.1",
"physical-cpu-count": "^2.0.0",
"sm-crypto": "^0.3.13",
"ssh2": "^1.16.0",
"systeminformation": "^5.30.6",
"webdav": "^5.3.1"
}
}
+116
View File
@@ -0,0 +1,116 @@
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const JSON5 = require('json5');
const root = __dirname;
const publishDir = path.resolve(root, 'publish');
const artifacts = [
{
script: 'pkg-windows',
source: path.resolve(root, 'dist/windows/kit.exe'),
target: path.resolve(publishDir, 'windows/x86_64/kit.exe'),
localPath: 'publish/windows/x86_64/kit.exe',
os: 'windows',
arch: 'x86_64',
filename: 'kit.exe'
},
{
script: 'pkg-linux',
source: path.resolve(root, 'dist/linux/kit'),
target: path.resolve(publishDir, 'linux/x86_64/kit'),
localPath: 'publish/linux/x86_64/kit',
os: 'linux',
arch: 'x86_64',
filename: 'kit'
}
];
function run(command, args) {
console.log(`> ${command} ${args.join(' ')}`);
const result = spawnSync(command, args, {
cwd: root,
stdio: 'inherit',
shell: process.platform === 'win32'
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
process.exit(result.status || 1);
}
}
function readJson(file) {
return JSON5.parse(fs.readFileSync(file, 'utf-8'));
}
function readPackage() {
return readJson(path.resolve(root, 'package.json'));
}
function readSourceMeta() {
const candidates = [
path.resolve(root, 'meta.json'),
path.resolve(root, '..', 'meta.json')
];
for (const file of candidates) {
if (fs.existsSync(file)) {
return readJson(file);
}
}
return {};
}
function copyFile(source, target) {
if (!fs.existsSync(source)) {
throw new Error(`Build artifact not found: ${source}`);
}
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(source, target);
}
function writeMeta() {
const pkg = readPackage();
const sourceMeta = readSourceMeta();
const meta = {
id: sourceMeta.id || pkg.name || 'kit',
name: sourceMeta.name || pkg.name || 'kit',
desc: sourceMeta.desc || sourceMeta.description || '',
group: sourceMeta.group || '/modules',
dist: artifacts.map((artifact) => ({
path: artifact.localPath,
os: artifact.os,
arch: artifact.arch,
filename: artifact.filename,
type: 'executable'
}))
};
fs.mkdirSync(publishDir, { recursive: true });
fs.writeFileSync(path.resolve(publishDir, 'meta.json'), JSON.stringify(meta, null, 4), 'utf-8');
}
function preparePublishDir() {
fs.rmSync(publishDir, { recursive: true, force: true });
for (const artifact of artifacts) {
copyFile(artifact.source, artifact.target);
}
writeMeta();
}
for (const artifact of artifacts) {
run('npm', ['run', artifact.script]);
}
preparePublishDir();
run('kit3', ['publish', './publish']);
console.log('Publish completed.');
+1
View File
@@ -0,0 +1 @@
node push.js windows D:\\SynologyDrive\\bin
+58
View File
@@ -0,0 +1,58 @@
// push.js
const fs = require('fs');
const path = require('path');
// 读取命令行参数
const args = process.argv.slice(2);
const platform = args[0]; // 'windows' 或 'linux'
const target = args[1]; // 目标配置键,如 'dir'
if (!platform || !target) {
console.error('用法: node push.js <平台> <目标配置键>');
console.error('示例: node push.js windows dir');
process.exit(1);
}
try {
// 读取配置文件
const configPath = path.join(__dirname, 'local.json');
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
// 验证参数
if (!configData[platform]) {
console.error(`错误: 配置文件中找不到平台 "${platform}"`);
process.exit(1);
}
if (!configData[target]) {
console.error(`错误: 配置文件中找不到目标 "${target}"`);
process.exit(1);
}
// 获取源文件和目标目录
const sourceFile = path.resolve(__dirname, configData[platform]);
const targetDir = configData[target];
// 获取源文件名
const sourceFileName = path.basename(sourceFile);
// 创建目标路径
const targetPath = path.join(targetDir, sourceFileName);
// 确保目标目录存在
if (!fs.existsSync(targetDir)) {
console.log(`创建目录: ${targetDir}`);
fs.mkdirSync(targetDir, { recursive: true });
}
// 复制文件
fs.copyFileSync(sourceFile, targetPath);
console.log(`成功复制文件:`);
console.log(` 从: ${sourceFile}`);
console.log(` 到: ${targetPath}`);
} catch (error) {
console.error(`错误: ${error.message}`);
process.exit(1);
}
+12 -8
View File
@@ -30,9 +30,13 @@ module.exports = class SystemConfig {
static getLocalKitPath(){
let path = "./"
if ( this.isWindows() ) {
path = require('path').dirname(require.main.filename)
if( require && require.main && require.main.filename ){
path = require('path').dirname(require.main.filename)
} else {
path = process.cwd()
}
} else {
path = process.execPath;
path = require('path').dirname(process.execPath);
}
this.affirmDir(path)
$logger.debug(`LocalKitPath=${path}`)
@@ -47,11 +51,11 @@ module.exports = class SystemConfig {
}
static getUserPath() {
let userDirectory = '~';
if ( this.isWindows() ) {
userDirectory = process.env.USERPROFILE;
}
return userDirectory;
// let userDirectory = '~';
// if ( this.isWindows() ) {
// userDirectory = process.env.USERPROFILE;
// }
return os.homedir();
}
static getUserConfigPath() {
@@ -87,7 +91,7 @@ module.exports = class SystemConfig {
fs.mkdirSync(path, { recursive: true });
} catch (err) {
// 处理错误
log.error(`affirmDir ${path} error ${ err }`);
$logger.error(`affirmDir ${path} error ${ err }`);
}
}
}
+446 -1
View File
@@ -1,5 +1,9 @@
const sm4 = require('sm-crypto').sm4
const sm3 = require('sm-crypto').sm3
const sm2 = require('sm-crypto').sm2
const crypto = require('crypto')
const forge = require('node-forge')
const rs = require('jsrsasign')
module.exports.sm4en3 = function (key, data){
@@ -15,4 +19,445 @@ module.exports.sm4de3 = function (key, data){
sm3key = sm3key.substring(0, 32)
let decryptData = sm4.decrypt(data, sm3key)
return decryptData
}
}
/**
* 从 PKCS7 签名中获取信息(包含证书、公钥和签名值)
* 支持 RSA 和 SM2 算法
* @param {Buffer|string} p7sign - PKCS7 签名数据
* @returns {Object} 包含证书、公钥和签名值的对象
*/
module.exports.getInfoFromPKCS7 = function (p7sign) {
// 处理输入数据
let base64Data
if (Buffer.isBuffer(p7sign)) {
base64Data = p7sign.toString('base64')
} else if (typeof p7sign === 'string') {
// 判断是否是 PEM 格式
if (p7sign.includes('-----BEGIN PKCS7-----')) {
// 提取 base64 部分
base64Data = p7sign
.replace(/-----BEGIN PKCS7-----\n?/, '')
.replace(/-----END PKCS7-----\n?/, '')
.replace(/\n/g, '')
} else {
// 如果不是 PEM 格式,假设是 base64
base64Data = p7sign
}
} else {
throw new Error('Invalid input type for p7sign')
}
// 尝试使用 node-forge 解析(支持 RSA)
try {
const pemData = `-----BEGIN PKCS7-----\n${base64Data}\n-----END PKCS7-----`
const p7 = forge.pkcs7.messageFromPem(pemData)
return extractInfoFromForgeP7(p7)
} catch (forgeError) {
// 如果 node-forge 解析失败,尝试使用 jsrsasign(支持 SM2)
try {
return extractInfoFromJsrsasignP7(base64Data)
} catch (jsrsasignError) {
throw new Error(`Failed to parse PKCS7: ${forgeError.message}, ${jsrsasignError.message}`)
}
}
}
/**
* 从 node-forge 解析的 PKCS7 中提取信息(RSA)
*/
function extractInfoFromForgeP7(p7) {
// 辅助函数:将属性数组转换为 DN 字符串
function attrsToDN(attrs) {
if (!attrs || attrs.length === 0) return ''
return attrs.map(attr => `${attr.name}=${attr.value}`).join(', ')
}
// 提取证书
let cert = null
if (p7.certificates && p7.certificates.length > 0) {
const certObj = p7.certificates[0]
cert = {
subject: certObj.subject.attributes.map(attr => ({
name: attr.name,
value: attr.value
})),
subjectDN: attrsToDN(certObj.subject.attributes),
issuer: certObj.issuer.attributes.map(attr => ({
name: attr.name,
value: attr.value
})),
issuerDN: attrsToDN(certObj.issuer.attributes),
serialNumber: certObj.serialNumber,
notBefore: certObj.validity.notBefore,
notAfter: certObj.validity.notAfter,
pem: forge.pki.certificateToPem(certObj)
}
}
// 提取公钥 - 从证书中提取公钥对象并转换为 PEM 和 Hex
let publicKey = null
let publicKeyHex = null
if (p7.certificates && p7.certificates.length > 0) {
const certObj = p7.certificates[0]
// 提取公钥对象并转换为 PEM 格式
if (certObj.publicKey) {
publicKey = forge.pki.publicKeyToPem(certObj.publicKey)
// 将公钥转换为 hex 格式
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(certObj.publicKey)
const publicKeyDer = forge.asn1.toDer(publicKeyAsn1)
publicKeyHex = forge.util.bytesToHex(publicKeyDer.getBytes())
}
}
// 提取签名值 - 从 rawCapture 中获取(已经是原始签名值)
let signature = null
let signatureHex = null
if (p7.rawCapture && p7.rawCapture.signature) {
// rawCapture.signature 已经是原始签名值字符串
signature = forge.util.encode64(p7.rawCapture.signature)
// 同时提供 hex 格式
signatureHex = forge.util.bytesToHex(p7.rawCapture.signature)
}
return {
cert,
publicKey,
publicKeyHex,
signature,
signatureHex
}
}
/**
* 从 jsrsasign 解析的 PKCS7 中提取信息(支持 SM2)
*/
function extractInfoFromJsrsasignP7(base64Data) {
const parser = new rs.KJUR.asn1.cms.CMSParser()
const p7 = parser.getCMSSignedData(rs.b64tohex(base64Data))
// 提取证书
let cert = null
let publicKey = null
let publicKeyHex = null
// p7.certs 是一个对象,包含 array 属性
const certs = p7.certs && p7.certs.array ? p7.certs.array : []
if (certs.length > 0) {
const certPem = certs[0]
// 解析证书获取详细信息
const x509 = new rs.X509()
x509.readCertPEM(certPem)
// 获取主题和颁发者
const subjectStr = x509.getSubjectString()
const issuerStr = x509.getIssuerString()
// 将字符串转换为数组格式
function parseDNString(dnStr) {
if (!dnStr || dnStr === '/') return []
const parts = dnStr.substring(1).split('/')
return parts.map(part => {
const [name, value] = part.split('=')
return { name, value }
})
}
const subjectAttrs = parseDNString(subjectStr)
const issuerAttrs = parseDNString(issuerStr)
// 转换为 DN 字符串
function attrsToDN(attrs) {
if (!attrs || attrs.length === 0) return ''
return attrs.map(attr => `${attr.name}=${attr.value}`).join(', ')
}
cert = {
subject: subjectAttrs,
subjectDN: attrsToDN(subjectAttrs),
issuer: issuerAttrs,
issuerDN: attrsToDN(issuerAttrs),
serialNumber: x509.getSerialNumberHex(),
notBefore: x509.getNotBefore(),
notAfter: x509.getNotAfter(),
pem: certPem
}
// 提取公钥 - 使用 ASN.1 解析提取原始公钥字节
try {
// 将 PEM 转换为 hex
const certHex = rs.pemtohex(certPem)
// 获取 tbsCertificate
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0])
// 获取 subjectPublicKeyInfo (在 tbsCertificate 中的索引是 6)
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6])
// 提取公钥值 (BIT STRING)
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1])
// BIT STRING 的内容:跳过 tag 03 (2 hex), 跳过 length (2 hex), 跳过 unused bits (2 hex)
// 所以从第 6 个字符开始
const publicKeyHexContent = publicKeyBitString.substring(6)
// 提取算法 OID 来检测是否是 SM2
const algorithmSeq = rs.ASN1HEX.getTLVbyList(spki, 0, [0])
const oidTlv = rs.ASN1HEX.getTLVbyList(algorithmSeq, 0, [0])
const oidHex = oidTlv.substring(4)
// SM2 OID: 1.2.156.10197.1.301 -> hex 2A811CCF550101301
const isSM2 = oidHex.includes('2A811CCF55') || oidHex.includes('2a811ccf55')
// 创建完整的 subjectPublicKeyInfo PEM
publicKey = rs.KJUR.asn1.ASN1Util.getPEMStringFromHex(spki, 'PUBLIC KEY')
// publicKeyHex 就是 subjectPublicKeyInfo 的完整 hex
publicKeyHex = spki
} catch (e) {
// SM2 公钥可能无法通过标准方式获取
console.error('提取 SM2 公钥错误:', e.message)
publicKey = null
publicKeyHex = null
}
}
// 提取签名值
let signature = null
let signatureHex = null
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0]
if (signer.sighex) {
signatureHex = signer.sighex
signature = rs.hextob64(signer.sighex)
}
}
return {
cert,
publicKey,
publicKeyHex,
signature,
signatureHex
}
}
/**
* 验签 PKCS7 签名(自动判断 RSA 或 SM2 算法)
* @param {Buffer|string} p7sign - PKCS7 签名数据
* @param {Buffer|string} [originalData] - 原始数据,当签名是 detech 时必须,当签名是 attach 时不传
* @returns {boolean} 验签结果
*/
module.exports.verifyPKCS7 = function (p7sign, originalData) {
// 处理输入数据
let base64Data
if (Buffer.isBuffer(p7sign)) {
base64Data = p7sign.toString('base64')
} else if (typeof p7sign === 'string') {
// 判断是否是 PEM 格式
if (p7sign.includes('-----BEGIN PKCS7-----')) {
// 提取 base64 部分
base64Data = p7sign
.replace(/-----BEGIN PKCS7-----\n?/, '')
.replace(/-----END PKCS7-----\n?/, '')
.replace(/\n/g, '')
} else {
// 如果不是 PEM 格式,假设是 base64
base64Data = p7sign
}
} else {
throw new Error('Invalid input type for p7sign')
}
// 准备验证所需的数据
let verifyData
if (originalData) {
// 当提供了原始数据时,使用原始数据进行验证(detch 模式)
if (Buffer.isBuffer(originalData)) {
verifyData = originalData
} else if (typeof originalData === 'string') {
verifyData = Buffer.from(originalData)
} else {
throw new Error('Invalid input type for originalData')
}
}
// 尝试使用 node-forge 解析和验证(支持 RSA)
try {
const pemData = `-----BEGIN PKCS7-----\n${base64Data}\n-----END PKCS7-----`
const p7 = forge.pkcs7.messageFromPem(pemData)
// 如果没有提供原始数据,从 PKCS7 中提取(attach 模式)
if (!verifyData) {
if (p7.content) {
verifyData = Buffer.from(p7.content.getBytes(), 'binary')
} else {
throw new Error('No content found in PKCS7 signature for attach mode')
}
}
// 对于 RSA 签名,使用公钥手动验证
if (p7.certificates && p7.certificates.length > 0) {
const publicKey = p7.certificates[0].publicKey
if (publicKey && p7.rawCapture && p7.rawCapture.signature) {
// 计算数据的摘要
const md = forge.md.sha256.create()
md.update(verifyData.toString(), 'utf8')
const digest = md.digest().getBytes()
// 使用公钥验证签名
return publicKey.verify(digest, p7.rawCapture.signature)
}
}
// 如果 RSA 验证失败或不适用,尝试使用 node-forge 的 verify(如果支持)
return p7.verify()
} catch (forgeError) {
// 如果 node-forge 解析失败,尝试使用 jsrsasign 解析和验证(支持 SM2)
try {
return verifyPKCS7WithJsrsasign(base64Data, verifyData)
} catch (jsrsasignError) {
// 静默处理错误,直接返回 false
return false
}
}
}
/**
* 使用 jsrsasign 验证 PKCS7 签名(支持 SM2)
*/
function verifyPKCS7WithJsrsasign(base64Data, verifyData) {
const parser = new rs.KJUR.asn1.cms.CMSParser()
const p7 = parser.getCMSSignedData(rs.b64tohex(base64Data))
// 如果没有提供原始数据,从 PKCS7 中提取(attach 模式)
if (!verifyData) {
if (p7.econtent && p7.econtent.hex) {
// econtent.hex 是原始数据的 hex 编码
verifyData = Buffer.from(p7.econtent.hex, 'hex')
} else {
throw new Error('No content found in PKCS7 signature for attach mode')
}
}
// 获取签名信息
if (!p7.sinfos || p7.sinfos.length === 0) {
throw new Error('No signer info found in PKCS7')
}
const signer = p7.sinfos[0]
// 获取证书和公钥
if (!p7.certs || !p7.certs.array || p7.certs.array.length === 0) {
throw new Error('No certificate found in PKCS7')
}
const certPem = p7.certs.array[0]
const x509 = new rs.X509()
x509.readCertPEM(certPem)
// 判断签名算法
const sigAlg = signer.sigalg
// SM2 签名算法 OID: 1.2.156.10197.1.301.1 或 1.2.156.10197.1.501
if (sigAlg === '1.2.156.10197.1.301.1' || sigAlg === '1.2.156.10197.1.501' || sigAlg.includes('sm2')) {
// SM2 验证
// 提取公钥 hex
const certHex = rs.pemtohex(certPem)
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0])
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6])
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1])
let publicKeyHex = publicKeyBitString.substring(6) // 跳过 tag, length, unused bits
// 确保公钥有 04 前缀(sm-crypto 0.3.13 需要)
if (!publicKeyHex.startsWith('04')) {
publicKeyHex = '04' + publicKeyHex
}
// 计算 SM3 哈希
const hash = sm3(verifyData.toString())
// 签名格式:用户提供的是 ASN.1 DER 格式,需要使用 der: true 选项
let signatureHex = signer.sighex
// 使用 SM2 验证签名 (注意:使用 doVerifySignature,不是 doVerify)
// 尝试不同的选项组合,包括 userId
const userId = '1234567812345678' // 默认 userId
try {
// 尝试 1: 使用原始数据 + DER + SM3 + userId
const result1 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true, hash: true, userId })
if (result1) return true
// 尝试 2: 使用原始数据 + DER + SM3
const result2 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true, hash: true })
if (result2) return true
// 尝试 3: 使用原始数据 + DER
const result3 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true })
if (result3) return true
// 尝试 4: 使用哈希 + DER + SM3 + userId
const result4 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true, hash: true, userId })
if (result4) return true
// 尝试 5: 使用哈希 + DER + SM3
const result5 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true, hash: true })
if (result5) return true
// 尝试 6: 使用哈希 + DER
const result6 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true })
if (result6) return true
// 尝试 7: 不使用 der 选项(直接使用签名)
const result7 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex)
if (result7) return true
// 尝试 8: 不使用 der 选项 + SM3 + userId
const result8 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { hash: true, userId })
if (result8) return true
// 尝试 9: 转换签名格式为原始 r|s 格式
if (signatureHex.startsWith('30')) {
try {
// 解析 ASN.1 DER 格式
// 格式: 30 len 02 len r 02 len s
const rStart = 8 // 30440220
const r = signatureHex.substring(rStart, rStart + 64) // 32字节 = 64 hex 字符
const sStart = rStart + 64 + 4 // 0220
const s = signatureHex.substring(sStart, sStart + 64) // 32字节 = 64 hex 字符
const rawSignature = r + s
// 尝试使用原始 r|s 格式
const result9 = sm2.doVerifySignature(verifyData.toString(), rawSignature, publicKeyHex)
if (result9) return true
// 尝试使用原始 r|s 格式 + SM3 + userId
const result10 = sm2.doVerifySignature(verifyData.toString(), rawSignature, publicKeyHex, { hash: true, userId })
if (result10) return true
} catch (e) {
// 忽略错误,继续尝试
}
}
} catch (e) {
// 忽略错误,继续尝试其他选项
}
return false
} else {
// RSA 验证(通过 jsrsasign)
try {
const pubKey = x509.getPublicKey()
const sig = new rs.KJUR.crypto.Signature({ alg: 'SHA256withRSA' })
sig.init(pubKey)
sig.updateString(verifyData.toString())
return sig.verify(signer.sighex)
} catch (e) {
throw new Error('RSA verification failed: ' + e.message)
}
}
}
+261
View File
@@ -0,0 +1,261 @@
#!/usr/bin/env node
const { execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");
function exitError(message, code = 1) {
console.error("ERROR: " + message);
process.exit(code);
}
function run(command, args) {
return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
}
function ensureDocker() {
try {
run("docker", ["version", "--format", "{{.Server.Version}}"]);
} catch (e) {
exitError("docker command is not executable, or Docker daemon is unavailable");
}
}
function inspectContainer(container) {
try {
const text = run("docker", ["inspect", container]);
const data = JSON.parse(text);
if (!Array.isArray(data) || data.length === 0) {
exitError("container not found: " + container);
}
return data[0];
} catch (e) {
if (e instanceof SyntaxError) {
exitError("docker inspect output is not valid JSON");
}
exitError("docker inspect failed: " + container);
}
}
function quoteSh(value) {
if (value === undefined || value === null) {
return "''";
}
const text = String(value);
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) {
return text;
}
return "'" + text.replace(/'/g, "'\\''") + "'";
}
function quoteMountSpec(value) {
const text = String(value);
if (text.startsWith("$(pwd)")) {
const rest = text.slice("$(pwd)".length).replace(/(["\\`$])/g, "\\$1");
return '"$(pwd)' + rest + '"';
}
return quoteSh(text);
}
function normalizeContainerName(name) {
return (name || "container").replace(/^\/+/, "") || "container";
}
function isLikelyProjectPath(source) {
if (!source) return false;
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
if (normalized === "/") return false;
if (/^[A-Za-z]:$/i.test(normalized)) return false;
const specialPrefixes = [
"/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/proc", "/run", "/sbin", "/sys", "/tmp", "/usr", "/var",
"/opt/docker", "/var/lib/docker", "/var/run/docker.sock",
"C:/Program Files", "C:/Program Files (x86)", "C:/ProgramData", "C:/Windows"
];
return !specialPrefixes.some(prefix => normalized === prefix || normalized.startsWith(prefix + "/"));
}
function relativeToPwdIfProject(source) {
if (!isLikelyProjectPath(source)) {
return source;
}
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
const base = path.posix.basename(normalized);
if (!base || base === "." || base === "/") {
return source;
}
return "$(pwd)/" + base;
}
function collectEnv(config) {
const env = Array.isArray(config.Env) ? config.Env : [];
return env
.filter(item => typeof item === "string" && item.length > 0)
.map(item => " -e " + quoteSh(item) + " \\");
}
function collectPorts(networkSettings, hostConfig) {
const lines = [];
const seen = new Set();
const ports = Object.assign({}, networkSettings && networkSettings.Ports ? networkSettings.Ports : {});
const bindings = hostConfig && hostConfig.PortBindings ? hostConfig.PortBindings : {};
for (const containerPort of Object.keys(bindings)) {
if (!ports[containerPort]) ports[containerPort] = bindings[containerPort];
}
for (const containerPort of Object.keys(ports).sort()) {
const entries = ports[containerPort];
if (!Array.isArray(entries)) continue;
for (const entry of entries) {
if (!entry || !entry.HostPort) continue;
const hostIp = entry.HostIp && entry.HostIp !== "0.0.0.0" && entry.HostIp !== "::" ? entry.HostIp + ":" : "";
const spec = hostIp + entry.HostPort + ":" + containerPort;
if (seen.has(spec)) continue;
seen.add(spec);
lines.push(" -p " + quoteSh(spec) + " \\");
}
}
return lines;
}
function collectMounts(info) {
const mounts = Array.isArray(info.Mounts) ? info.Mounts : [];
const lines = [];
for (const mount of mounts) {
if (!mount || !mount.Destination) continue;
if (mount.Type === "volume" && mount.Name) {
let spec = mount.Name + ":" + mount.Destination;
if (mount.RW === false) spec += ":ro";
lines.push(" -v " + quoteMountSpec(spec) + " \\");
continue;
}
if (mount.Source) {
let spec = relativeToPwdIfProject(mount.Source) + ":" + mount.Destination;
if (mount.RW === false) spec += ":ro";
lines.push(" -v " + quoteMountSpec(spec) + " \\");
}
}
return lines;
}
function collectNetworks(info) {
const hostConfig = info.HostConfig || {};
const networkMode = hostConfig.NetworkMode;
const lines = [];
if (networkMode && networkMode !== "default" && networkMode !== "bridge") {
lines.push(" --network " + quoteSh(networkMode) + " \\");
return lines;
}
const networkSettings = info.NetworkSettings || {};
const networks = networkSettings.Networks ? Object.keys(networkSettings.Networks) : [];
if (networks.length === 1 && networks[0] !== "bridge") {
lines.push(" --network " + quoteSh(networks[0]) + " \\");
}
return lines;
}
function collectSimpleOptions(info) {
const config = info.Config || {};
const hostConfig = info.HostConfig || {};
const lines = [];
if (hostConfig.RestartPolicy && hostConfig.RestartPolicy.Name && hostConfig.RestartPolicy.Name !== "no") {
let restart = hostConfig.RestartPolicy.Name;
if (restart === "on-failure" && hostConfig.RestartPolicy.MaximumRetryCount) {
restart += ":" + hostConfig.RestartPolicy.MaximumRetryCount;
}
lines.push(" --restart " + quoteSh(restart) + " \\");
}
if (config.WorkingDir) lines.push(" -w " + quoteSh(config.WorkingDir) + " \\");
if (config.User) lines.push(" -u " + quoteSh(config.User) + " \\");
if (hostConfig.Privileged) lines.push(" --privileged \\");
if (config.Tty) lines.push(" -t \\");
if (config.OpenStdin) lines.push(" -i \\");
return lines;
}
function commandTail(config) {
let entrypoint = [];
if (Array.isArray(config.Entrypoint)) {
entrypoint = config.Entrypoint.filter(item => item !== null && item !== undefined).map(String);
} else if (typeof config.Entrypoint === "string" && config.Entrypoint) {
entrypoint = [config.Entrypoint];
}
const cmd = Array.isArray(config.Cmd) ? config.Cmd : (typeof config.Cmd === "string" && config.Cmd ? [config.Cmd] : []);
return {
entrypoint: entrypoint.length > 0 ? entrypoint[0] : null,
args: entrypoint.slice(1).concat(cmd)
};
}
function buildScript(info) {
const config = info.Config || {};
const hostConfig = info.HostConfig || {};
const name = normalizeContainerName(info.Name);
const image = config.Image || info.Image || "";
const tail = commandTail(config);
const lines = [
"#!/usr/bin/env bash",
"set -euo pipefail",
"",
"name=" + quoteSh(name),
"img=" + quoteSh(image),
"",
"docker run -d \\",
" --name \"$name\" \\",
];
lines.push(...collectSimpleOptions(info));
lines.push(...collectEnv(config));
lines.push(...collectPorts(info.NetworkSettings || {}, hostConfig));
lines.push(...collectMounts(info));
lines.push(...collectNetworks(info));
if (tail.entrypoint) {
lines.push(" --entrypoint " + quoteSh(tail.entrypoint) + " \\");
}
const cmd = tail.args.map(quoteSh).join(" ");
if (cmd) {
lines.push(" \"$img\" " + cmd);
} else {
lines.push(" \"$img\"");
}
lines.push("");
return lines.join("\n");
}
function outputFile(container, explicitOutput) {
if (explicitOutput) return explicitOutput;
const safe = container.replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "container";
return path.resolve(process.cwd(), "docker-run-" + safe + ".sh");
}
function main() {
const args = process.argv.slice(2);
if (args.length < 1 || args[0] === "-h" || args[0] === "--help") {
console.log("Usage: node restore-run.js <container-id-or-name> [output-file]");
process.exit(args.length < 1 ? 1 : 0);
}
ensureDocker();
const container = args[0];
const info = inspectContainer(container);
const script = buildScript(info);
const file = outputFile(container, args[1]);
fs.writeFileSync(file, script, { mode: 0o755 });
console.log(file);
}
if (require.main === module) {
main();
}
module.exports = {
buildScript,
main
};
+217
View File
@@ -0,0 +1,217 @@
/**
* kit glance 命令
* - 记录模式:--pid <pid> 或 --port <port>,采集一次写入 sqlite(~/.kit/glance.db)
* - 报告文件:--report [文件](默认 report.html),需配合 --pid/--port
* - 报告服务:--report <端口号>,报告导出到系统临时目录 kit-glance/<端口>/index.html,
* 并在该端口开启简易 http 服务在线查看
* - --startTime/--endTime 格式 yyyyMMddHHmmss,指定报告时间范围
*/
const fs = require("fs");
const path = require("path");
const os = require("os");
const http = require("http");
const store = require("./store");
const report = require("./report");
const monitor = require("./monitor");
const HOUR = 60 * 60 * 1000;
const HOURS_24 = 24 * HOUR;
function parseTime(text) {
const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(text);
if (!m) {
return null;
}
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6]));
return d.getTime();
}
function resolveRange(startText, endText) {
if (startText && endText) {
const s = parseTime(startText);
const e = parseTime(endText);
if (s == null || e == null) {
return { error: "--startTime/--endTime 格式应为 yyyyMMddHHmmss" };
}
if (e <= s) {
return { error: "--endTime 必须晚于 --startTime" };
}
return { start: s, end: e };
}
const now = Date.now();
if (startText) {
const s = parseTime(startText);
if (s == null) {
return { error: "--startTime 格式应为 yyyyMMddHHmmss" };
}
return { start: s, end: s + HOURS_24 };
}
if (endText) {
const e = parseTime(endText);
if (e == null) {
return { error: "--endTime 格式应为 yyyyMMddHHmmss" };
}
return { start: e - HOURS_24, end: e };
}
return { start: now - HOURS_24, end: now };
}
async function record(cli) {
const db = store.open();
let record = null;
const pidText = cli.getParamValue("pid");
if (pidText && pidText !== "") {
const pid = Number(pidText);
if (!Number.isInteger(pid) || pid <= 0) {
$logger.error(`pid 参数无效: ${pidText}`);
process.exit(-1);
}
const data = await monitor.collectPid(pid);
if (!data) {
$logger.warn(`进程 ${pid} 不存在或已退出,本次不记录`);
return;
}
record = { ts: Date.now(), target: "pid", pid, port: null, cpu: data.cpu, mem: data.mem, handle: data.handle };
} else {
const portText = cli.getParamValue("port");
const port = Number(portText);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
$logger.error(`port 参数无效: ${portText}`);
process.exit(-1);
}
const found = monitor.findPortPid(port);
if (!found) {
$logger.warn(`端口 ${port} 无监听进程,本次不记录`);
return;
}
const data = await monitor.collectPid(found);
if (!data) {
$logger.warn(`进程 ${found} 已退出,本次不记录`);
return;
}
record = { ts: Date.now(), target: "port", pid: found, port, cpu: data.cpu, mem: data.mem, handle: data.handle };
}
store.insert(db, record);
$logger.info(`已记录 target=${record.target} pid=${record.pid} cpu=${record.cpu}% mem=${record.mem}MB handle=${record.handle}`);
}
function buildReport(cli, reportFile) {
const isPid = cli.getParamValue("pid") != null;
const { start, end, error } = resolveRange(cli.getParamValue("startTime"), cli.getParamValue("endTime"));
if (error) {
$logger.error(error);
process.exit(-1);
}
const db = store.open();
const rows = isPid
? store.queryRange(db, { pid: Number(cli.getParamValue("pid")), start, end })
: store.queryRange(db, { port: Number(cli.getParamValue("port")), start, end });
const html = report.render({
mode: isPid ? "pid" : "port",
value: isPid ? Number(cli.getParamValue("pid")) : Number(cli.getParamValue("port")),
rows,
start,
end
});
fs.writeFileSync(reportFile, html, "utf8");
$logger.info(`报告已生成: ${path.resolve(reportFile)}`);
}
/**
* 报告在线服务:把报告导出为系统临时目录 kit-glance/<端口>/index.html,
* 并在该端口启动一个简易 http 服务供在线查看(监听 0.0.0.0)。
* 报告数据按 --pid/--port 与 --startTime/--endTime 从当前库生成。
* @param {object} cli 命令行
* @param {number} serverPort 报告服务端口
*/
function serveOnline(cli, serverPort) {
const serveDir = path.join(os.tmpdir(), "kit-glance", String(serverPort));
fs.mkdirSync(serveDir, { recursive: true });
const indexPath = path.join(serveDir, "index.html");
buildReport(cli, indexPath);
const server = http.createServer((req, res) => {
const urlPath = (req.url || "/").split("?")[0];
if (urlPath === "/" || urlPath === "/index.html") {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
fs.createReadStream(indexPath).pipe(res);
return;
}
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not Found");
});
server.on("error", (e) => {
if (e.code === "EADDRINUSE") {
$logger.warn(`端口 ${serverPort} 已被占用:报告已更新到 ${indexPath},已有报告服务运行中可直接刷新查看`);
process.exit(0);
} else {
$logger.error(`启动 http 服务失败: ${e.message}`);
process.exit(-1);
}
});
server.listen(serverPort, "0.0.0.0", () => {
$logger.info(`在线查看: http://localhost:${serverPort}/ (监听 0.0.0.0:${serverPort},可通过本机任一 IP 访问)`);
});
}
async function main(cli) {
const pidText = cli.getParamValue("pid");
const portText = cli.getParamValue("port");
const reportRaw = cli.getParamValue("report");
const isPid = pidText != null && pidText !== "";
const isPort = portText != null && portText !== "";
const isReport = reportRaw !== undefined;
if (isPid && isPort) {
$logger.error("--pid 和 --port 只能传递一个");
process.exit(-1);
}
if (!isPid && !isPort) {
$logger.error("--pid 和 --port 必须传递一个");
process.exit(-1);
}
if (isPid) {
const pid = Number(pidText);
if (!Number.isInteger(pid) || pid <= 0) {
$logger.error(`pid 参数无效: ${pidText}`);
process.exit(-1);
}
} else if (isPort) {
const port = Number(portText);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
$logger.error(`port 参数无效: ${portText}`);
process.exit(-1);
}
}
if (isReport) {
const reportValue = reportRaw || "";
const servePort = /^\d+$/.test(reportValue) ? Number(reportValue) : 0;
if (servePort >= 1 && servePort <= 65535) {
serveOnline(cli, servePort);
return;
}
buildReport(cli, reportValue || "report.html");
return;
}
if (!monitor.isLinux()) {
$logger.error("glance 仅支持 Linux 操作系统");
process.exit(-1);
}
await record(cli);
}
module.exports = { main };
+80
View File
@@ -0,0 +1,80 @@
/**
* 进程监控指标采集(仅支持 Linux)
* - CPU 占用率:两次采样 /proc/<pid>/stat 与 /proc/stat 差值计算,内部休眠约 1 秒
* - 内存占用:/proc/<pid>/status 的 VmRSS,单位 MB
* - 句柄数:/proc/<pid>/fd 目录条目数
* - 端口 -> pid:端口解析逻辑见 ../linux/portpid.js(含 docker 端口发布)
*/
const fs = require("fs");
const portpid = require("../linux/portpid");
const os = require("os");
function isLinux() {
return os.type() === "Linux";
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function readPidTicks(pid) {
const txt = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
// 字段 14=utime 15=stime,comm 可能含空格,按最后一个 ) 截断
const rest = txt.slice(txt.lastIndexOf(")") + 1).trim().split(/\s+/);
return Number(rest[11]) + Number(rest[12]);
}
function readCpuTotal() {
const line = fs.readFileSync("/proc/stat", "utf8").split("\n")[0];
// user nice system idle iowait irq softirq steal(guest 已计入 user)
const parts = line.trim().split(/\s+/).slice(1, 9).map(Number);
let total = 0;
for (const v of parts) {
total += Number.isFinite(v) ? v : 0;
}
return total;
}
function readMemMB(pid) {
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
const m = status.match(/^VmRSS:\s+(\d+)\s+kB/m);
if (!m) {
return null;
}
return Math.round(Number(m[1]) / 1024 * 100) / 100;
}
function readHandleCount(pid) {
return fs.readdirSync(`/proc/${pid}/fd`, { withFileTypes: true }).length;
}
/**
* 采集单个进程的监控数据
* @param {number} pid
* @returns {Promise<{pid:number,cpu:number,mem:number,handle:number}|null>} 进程不存在/不可读返回 null
*/
async function collectPid(pid) {
try {
const beforePid = readPidTicks(pid);
const beforeTotal = readCpuTotal();
await sleep(1000);
const afterPid = readPidTicks(pid);
const afterTotal = readCpuTotal();
const totalDelta = afterTotal - beforeTotal;
const pidDelta = afterPid - beforePid;
const cpu = totalDelta > 0 ? Math.round(pidDelta / totalDelta * 10000) / 100 : 0;
const mem = readMemMB(pid);
const handle = readHandleCount(pid);
return { pid, cpu, mem, handle };
} catch (e) {
return null;
}
}
module.exports = {
isLinux,
collectPid,
findPortPid: portpid.findPortPid
};
+312
View File
@@ -0,0 +1,312 @@
/**
* glance 报告生成:生成一个自包含 HTML 文件(内联 SVG 图表,无外部依赖)。
* 三张图表:CPU 占用率、内存占用、句柄数。
* - 纵轴/横轴根据数据量自动缩放
* - 时间段内无数据或进程中停(时间缺口)时,图表会显现
* - pid 变化(进程重启)以橙色虚线 + 标注标出
*/
const HOUR = 60 * 60 * 1000;
function esc(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function pad2(n) {
return String(n).padStart(2, "0");
}
function fmtTime(ts) {
const d = new Date(ts);
return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
}
function fmtFull(ts) {
const d = new Date(ts);
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
}
function niceStep(raw) {
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
const norm = raw / mag;
let step;
if (norm <= 1) step = 1;
else if (norm <= 2) step = 2;
else if (norm <= 5) step = 5;
else step = 10;
return step * mag;
}
function computeYAxis(values) {
const vs = values.filter(v => Number.isFinite(v));
if (!vs.length) {
return { min: 0, max: 1, ticks: [0, 0.2, 0.4, 0.6, 0.8, 1] };
}
let min = Math.min(...vs);
let max = Math.max(...vs);
if (min === max) {
min -= 1;
max += 1;
}
const step = niceStep((max - min) / 5);
const lo = Math.floor(min / step) * step;
const hi = Math.ceil(max / step) * step;
const ticks = [];
for (let v = lo; v <= hi + step / 2; v += step) {
ticks.push(Math.round(v * 1e6) / 1e6);
}
return { min: lo, max: hi, ticks };
}
/**
* 生成折线内容:按时间缺口切成多段 polyline,缺口处绘制淡色竖带
*/
function buildLine(entries, X, Y, gapThreshold, color) {
let svg = "";
let segment = [];
const gaps = [];
function flush() {
if (segment.length > 0) {
const points = segment.map(p => `${X(p.ts).toFixed(2)},${Y(p.value).toFixed(2)}`).join(" ");
svg += `\n <polyline fill="none" stroke="${color}" stroke-width="2" stroke-linejoin="round" points="${points}"/>`;
for (const p of segment) {
svg += `\n <circle cx="${X(p.ts).toFixed(2)}" cy="${Y(p.value).toFixed(2)}" r="2.5" fill="${color}"/>`;
}
segment = [];
}
}
for (let i = 0; i < entries.length; i++) {
if (i > 0 && gapThreshold > 0) {
const dt = entries[i].ts - entries[i - 1].ts;
if (dt > gapThreshold) {
gaps.push({ x0: X(entries[i - 1].ts), x1: X(entries[i].ts) });
flush();
}
}
segment.push(entries[i]);
}
flush();
return { svg, gaps };
}
function buildChart(title, unit, color, rows, start, end, gapThreshold, pidChanges) {
const W = 960, H = 300, mL = 80, mR = 40, mT = 36, mB = 52;
const pw = W - mL - mR;
const ph = H - mT - mB;
const span = (end - start) || 1;
const entries = rows
.filter(r => r.value != null && Number.isFinite(r.value))
.map(r => ({ ts: r.ts, value: r.value, pid: r.pid }));
const X = ts => mL + Math.max(0, Math.min(1, (ts - start) / span)) * pw;
const axis = computeYAxis(entries.map(e => e.value));
const Y = v => mT + ph - ((v - axis.min) / ((axis.max - axis.min) || 1)) * ph;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="100%" height="${H}" role="img">`;
svg += `\n <text x="${mL}" y="22" font-size="13" font-weight="600" fill="#333">${esc(title)}${unit ? `(${esc(unit)})` : ""}</text>`;
// 网格与纵轴
for (const t of axis.ticks) {
const y = Y(t);
svg += `\n <line x1="${mL}" y1="${y.toFixed(2)}" x2="${W - mR}" y2="${y.toFixed(2)}" stroke="#e5e7eb" stroke-width="1"/>`;
svg += `\n <text x="${mL - 8}" y="${(y + 4).toFixed(2)}" font-size="11" fill="#6b7280" text-anchor="end">${t}</text>`;
}
// 横轴(6 个刻度)
for (let k = 0; k <= 5; k++) {
const ts = start + (end - start) * k / 5;
const x = X(ts);
svg += `\n <line x1="${x.toFixed(2)}" y1="${mT + ph}" x2="${x.toFixed(2)}" y2="${mT + ph + 4}" stroke="#9ca3af" stroke-width="1"/>`;
svg += `\n <text x="${x.toFixed(2)}" y="${mT + ph + 18}" font-size="11" fill="#6b7280" text-anchor="middle">${fmtTime(ts)}</text>`;
}
// 坐标轴
svg += `\n <line x1="${mL}" y1="${mT}" x2="${mL}" y2="${mT + ph}" stroke="#9ca3af" stroke-width="1"/>`;
svg += `\n <line x1="${mL}" y1="${mT + ph}" x2="${W - mR}" y2="${mT + ph}" stroke="#9ca3af" stroke-width="1"/>`;
if (entries.length === 0) {
svg += `\n <text x="${mL + pw / 2}" y="${mT + ph / 2}" font-size="14" fill="#9ca3af" text-anchor="middle">该时间范围无数据</text>`;
} else {
const { svg: lineSvg, gaps } = buildLine(entries, X, Y, gapThreshold, color);
svg += lineSvg;
// 时间缺口高亮(一段时间没数据)
for (const g of gaps) {
const x = Math.min(g.x0, g.x1);
const w = Math.max(1, Math.abs(g.x1 - g.x0));
svg += `\n <rect x="${x.toFixed(2)}" y="${(mT + 6).toFixed(2)}" width="${w.toFixed(2)}" height="${(ph - 12).toFixed(2)}" fill="#f3f4f6" stroke="#d1d5db" stroke-width="0.5"/>`;
}
// pid 变化(进程重启)竖线
if (pidChanges && pidChanges.length) {
for (const ch of pidChanges) {
const x = X(ch.ts);
svg += `\n <line x1="${x.toFixed(2)}" y1="${mT}" x2="${x.toFixed(2)}" y2="${mT + ph}" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4,3"/>`;
const label = `pid ${ch.from}→${ch.to}`;
const lx = Math.min(x + 5, W - mR - 110);
const ly = mT + 14;
svg += `\n <text x="${lx.toFixed(2)}" y="${ly}" font-size="11" fill="#b45309" stroke="#ffffff" stroke-width="3" paint-order="stroke">↻ ${esc(label)}</text>`;
}
}
}
svg += "\n</svg>";
return svg;
}
function computeGapThreshold(rows) {
const tsList = rows
.map(r => r.ts)
.filter(ts => Number.isFinite(ts))
.sort((a, b) => a - b);
if (tsList.length < 2) {
return 0;
}
const deltas = [];
for (let i = 1; i < tsList.length; i++) {
const d = tsList[i] - tsList[i - 1];
if (d > 0) {
deltas.push(d);
}
}
if (!deltas.length) {
return 0;
}
deltas.sort((a, b) => a - b);
const typical = deltas[Math.floor(deltas.length / 2)];
return Math.max(typical * 3, 30 * 1000);
}
function collectPidChanges(rows) {
const changes = [];
for (let i = 1; i < rows.length; i++) {
const prev = rows[i - 1].pid;
const cur = rows[i].pid;
if (prev != null && cur != null && prev !== cur) {
changes.push({ ts: rows[i].ts, from: prev, to: cur });
}
}
return changes;
}
function summary(rows, key) {
const vs = rows.map(r => r[key]).filter(v => v != null && Number.isFinite(v));
if (!vs.length) {
return null;
}
const min = Math.min(...vs);
const max = Math.max(...vs);
const avg = vs.reduce((a, b) => a + b, 0) / vs.length;
return { min, max, avg };
}
function fmtNum(v, digits) {
if (v == null || !Number.isFinite(v)) {
return "-";
}
return Number(v.toFixed(digits));
}
/**
* 生成报告 HTML
* @param opts {mode:'pid'|'port', value, rows, start, end}
*/
function render(opts) {
const { mode, value, rows, start, end } = opts;
const spanHours = Math.round((end - start) / HOUR * 10) / 10;
const targetText = mode === "pid" ? `--pid ${value}` : `--port ${value}`;
const gapThreshold = computeGapThreshold(rows);
const pidChanges = collectPidChanges(rows);
const restarts = pidChanges.length;
const generatedAt = fmtFull(Date.now());
const series = [
{ key: "cpu", title: "CPU 占用率", unit: "%", color: "#2563eb" },
{ key: "mem", title: "内存占用", unit: "MB", color: "#16a34a" },
{ key: "handle", title: "句柄数", unit: "", color: "#dc2626" }
];
const summaryRows = series.map(s => {
const st = summary(rows, s.key);
return {
title: s.title,
unit: s.unit,
min: st ? fmtNum(st.min, 2) : "-",
max: st ? fmtNum(st.max, 2) : "-",
avg: st ? fmtNum(st.avg, 2) : "-"
};
});
let chartsHtml = "";
for (const s of series) {
chartsHtml += `
<section class="chart">
<h2>${esc(s.title)}</h2>
${buildChart(s.title, s.unit, s.color, rows.map(r => ({ ts: r.ts, value: r[s.key], pid: r.pid })), start, end, gapThreshold, pidChanges)}
</section>`;
}
const summaryHtml = summaryRows.map(r => `
<tr>
<td>${esc(r.title)}${r.unit ? `(${esc(r.unit)})` : ""}</td>
<td>${r.min}</td>
<td>${r.max}</td>
<td>${r.avg}</td>
</tr>`).join("");
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>进程监控报告 ${esc(targetText)}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: #f9fafb; margin: 0; padding: 24px; color: #1f2937; }
.report { max-width: 1040px; margin: 0 auto; }
header { background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px 24px; margin-bottom: 16px; }
header h1 { margin: 0 0 8px; font-size: 22px; }
.meta { display: flex; flex-wrap: wrap; gap: 8px 32px; font-size: 13px; color: #4b5563; }
.meta b { color: #111827; }
.charts { display: flex; flex-direction: column; gap: 16px; }
.chart { background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px 8px; }
.chart h2 { margin: 0 0 4px; font-size: 15px; color: #374151; }
.legend { color: #6b7280; font-size: 12px; margin-top: 4px; }
table { border-collapse: collapse; margin-top: 12px; font-size: 13px; width: 100%; }
th, td { text-align: right; padding: 6px 12px; border-bottom: 1px solid #e5e7eb; }
th:first-child, td:first-child { text-align: left; }
th { color: #6b7280; font-weight: 500; }
footer { margin-top: 16px; font-size: 12px; color: #9ca3af; text-align: center; }
</style>
</head>
<body>
<div class="report">
<header>
<h1>进程监控报告</h1>
<div class="meta">
<span>监控目标:<b>${esc(targetText)}</b></span>
<span>时间范围:<b>${fmtFull(start)} ~ ${fmtFull(end)}</b>(约 ${spanHours} 小时)</span>
<span>样本数:<b>${rows.length}</b></span>
<span>重启次数:<b>${restarts}</b></span>
<span>生成时间:<b>${generatedAt}</b></span>
</div>
<table>
<thead><tr><th>指标</th><th>最小值</th><th>最大值</th><th>平均值</th></tr></thead>
<tbody>${summaryHtml}</tbody>
</table>
<div class="legend">说明:时间轴覆盖请求范围;灰色竖带表示其间无采样数据(进程中断/采集缺失);橙色虚线表示 pid 变化(进程重启)。</div>
</header>
<div class="charts">
${chartsHtml}
</div>
<footer>generated by kit glance</footer>
</div>
</body>
</html>`;
}
module.exports = { render };
+68
View File
@@ -0,0 +1,68 @@
/**
* glance 数据存储:默认 sqlite 位于 ~/.kit/glance.db(不存在则创建)
* 依赖 bun:sqlite,仅在调用时加载,避免影响 node 环境下的其他命令。
*/
const fs = require("fs");
const os = require("os");
const path = require("path");
function defaultDbPath() {
return path.join(os.homedir(), ".kit", "glance.db");
}
function open(dbPath) {
dbPath = dbPath || defaultDbPath();
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const { Database } = require("bun:sqlite");
const db = new Database(dbPath);
db.exec(`CREATE TABLE IF NOT EXISTS glance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
target TEXT NOT NULL,
pid INTEGER,
port INTEGER,
cpu REAL,
mem REAL,
handle INTEGER
)`);
db.exec("CREATE INDEX IF NOT EXISTS idx_glance_lookup ON glance(pid, port, ts)");
return db;
}
/**
* 插入一条监控记录
* @param db database
* @param record {ts, target, pid, port, cpu, mem, handle}
*/
function insert(db, record) {
db.query("INSERT INTO glance (ts, target, pid, port, cpu, mem, handle) VALUES (?, ?, ?, ?, ?, ?, ?)")
.run(record.ts, record.target, record.pid ?? null, record.port ?? null, record.cpu ?? null, record.mem ?? null, record.handle ?? null);
}
/**
* 按时间范围查询监控数据(忽略 target,pid/port 二者取其一)
* @param db database
* @param opts {pid?, port?, start, end}
* @returns rows
*/
function queryRange(db, opts) {
let where;
const params = [];
if (opts.pid != null) {
where = "pid = ?";
params.push(opts.pid);
} else {
where = "port = ?";
params.push(opts.port);
}
params.push(opts.start, opts.end);
return db.query(`SELECT ts, target, pid, port, cpu, mem, handle FROM glance WHERE ${where} AND ts >= ? AND ts < ? ORDER BY ts ASC`).all(...params);
}
module.exports = {
defaultDbPath,
open,
insert,
queryRange
};
+469 -320
View File
@@ -1,320 +1,469 @@
/**
* kit cli 注册和调用执行
*/
require("./init")
const { CliCmdParser } = require("../../argv")
const parser = new CliCmdParser("kit V" + $version);
const net = require("./net")
const shell = require("./shell")
const {sm4en3, sm4de3} = require("./crypto");
const convutil = require("./util/convutil")
const path = require("path");
const fs = require("fs");
const {sm3} = require("sm-crypto");
const zlib = require("zlib");
parser.addCmdLine("man", "打开在线帮助;", async function (){
const uConfig = await $context.get("uConfig")
shell.execSync("start " + uConfig.get("man"))
});
parser.addCmdLine("manu", "人工维护配置;", function (){
shell.execSync("start " + $sConfig.getUserConfigPath())
});
parser.addCmdLine("run [--file] [func]", "运行项目脚本;", function (cli) {
$logger.info( "kit 当前执行目录 " + process.cwd());
const fs = require("fs");
let step = cli.getParamValue("func");
if ( ! step ) {
step = "main"
}
let file = cli.getParamValue("file");
if ( ! file ) {
file = "kit"
}
const stat = fs.existsSync(`./${file}.js`)
if ( ! stat ) {
if ( ! cli.getParamValue("file") ) {
fs.writeFileSync("./kit.js",
`
async function main(){
console.log("hello kit!")
}
`);
} else {
$logger.info( `文件 ${file} 不存在` );
return
}
}
const projectScript = fs.readFileSync("./kit.js", "utf-8");
const vm = require('node:vm');
const script = new vm.Script(projectScript + ";" + step + "()");
const context = {
animal: 'cat',
count: 2,
$logger
};
script.runInNewContext(context, {
timeout : 10 * 60 * 1000,
breakOnSigint : true,
});
$logger.info( "暂未实现" );
});
parser.addCmdLine("config <key> [value]", "查询/设置配置;", async function (cli){
let key = cli.getParamValue("key");
let value = cli.getParamValue("value");
const uConfig = await $context.get("uConfig")
if (null != key && null == value) {
let v = uConfig.get(key);
$logger.info("{} : {}", key, v);
} else if (null != key && null != value) {
uConfig.set(key, value);
$logger.info("set {} : {}", key, value);
} else {
$logger.error("参数异常");
}
});
parser.addCmdLine("remote-check", "验证远程仓库;", async function (){
let remote = await $context.get("remote")
if (! ( await remote.check()) ) {
$logger.error("远程仓库不可用")
return
} else {
$logger.info("远程仓库可用")
}
});
parser.addCmdLine("publish", "发布到远程仓库;", async function (){
let remote = await $context.get("remote")
if (! ( await remote.check()) ) {
$logger.error("远程仓库不可用")
return
} else {
$logger.info("远程仓库可用")
}
remote.publish()
});
parser.addCmdLine("install [module]", "安装/更新模块;", async function (cli){
let remote = await $context.get("remote")
if (! ( await remote.check()) ) {
$logger.error("远程仓库不可用")
return
} else {
$logger.info("远程仓库可用")
}
remote.install(cli.getParamValue("module"))
});
parser.addCmdLine("killport <port>", "释放端口", function (cli) {
let port = cli.getParamValue("port")
net.killport( parseInt(port, 10) );
});
parser.addCmdLine("getport [--port]", "获取一个可用端口;或查询指定端口状态;", async function (cli) {
const getPort = await import('get-port').then(psl => psl.default);
let port = cli.getParamValue("port")
if( ! port ) {
let ret = await getPort()
$logger.info(`端口 ${ret} 未被使用`)
} else {
await net.getport( port )
}
});
parser.addCmdLine("checkip", "获取自己的公网 ip;", async function (cli) {
let ret = await net.checkip()
if( "ok" === ret.ret ) {
$logger.info( "ip : " + ret.data.ip )
$logger.info( "location : " + ret.data.location.join(" ") )
$logger.info("\n" + ret.data.ip)
} else {
$logger.error("获取失败")
}
});
parser.addCmdLine("telnet [--timeout] <ip-port> [port]", "验证远程 TCP 端口是否可用;", async function (cli) {
let ip = cli.getParamValue("ip-port")
let port = 80;
// 允许冒号形式指定端口
const re = /^(?<ip>.*):(?<port>[0-9]+)$/
const match = ip.match( re );
if ( match ) {
ip = match.groups.ip;
port = match.groups.port;
} else {
// 如果指定了 port 参数,那么使用 port 参数
let dport = cli.getParamValue("port")
if ( dport ) {
port = dport;
}
}
// 支持不填写 ip,默认访问本地
if( "" === ip.trim() ) {
ip = "127.0.0.1";
}
try {
await net.telnet(port, ip, cli.getParamValue("timeout"))
$logger.info("连接成功")
}catch (e) {
$logger.error( e )
}
});
parser.addCmdLine("checklist <checkListFile>", "生成/验证文件 hash", async function (cli) {
let checkListFile = cli.getParamValue("checkListFile")
if ( !fs.existsSync( checkListFile ) ) {
$logger.error(`file ${checkListFile} 不存在`)
process.exit(-2)
}
const sm3 = require('sm-crypto').sm3
const list = fs.readFileSync(checkListFile, "UTF-8")
const files = list.split('\n').filter(line => line.trim() !== '');
for ( let file of files ) {
const infos = file.split(/\s+/)
const info = {}
if ( infos.length > 2 ) {
info.file = infos[0]
info.hash = infos[1]
info.alg = infos[2]
} else {
info.file = infos[0]
info.hash = infos[1]
info.alg = "md5"
}
if ( "sm3" === info.alg ) {
if ( !fs.existsSync( info.file ) ) {
$logger.error(`file ${info.file} 不存在`)
process.exit(-2)
}
const data = fs.readFileSync(info.file);
const arrayBuffer = Buffer.from(data);
const uint8Array = new Uint8Array(arrayBuffer);
let hashData = sm3( uint8Array ) // 杂凑
if ( hashData === info.hash ) {
$logger.info(`file ${file} 验证通过`)
} else {
$logger.error(`file ${file} 验证不通过`)
process.exit(-400)
}
} else {
$logger.error(`不支持 file ${file} `)
process.exit(-2)
}
}
$logger.info(`${checkListFile} 中的文件全部验证通过`)
})
parser.addCmdLine("sm3 [--file] [--text] [--hash]", "生成/验证 SM3 值;", async function (cli) {
let filePath = cli.getParamValue("file")
let text = cli.getParamValue("text")
if( !filePath && !text ) {
$logger.error(`file 和 text 入参不能同时有`)
process.exit(-1)
}
const sm3 = require('sm-crypto').sm3
if ( text ) {
let hashData = sm3( text ) // 杂凑
$logger.info( hashData )
}
if ( filePath ) {
if ( !fs.existsSync( filePath ) ) {
$logger.error(`file ${filePath} 不存在`)
process.exit(-2)
}
const data = fs.readFileSync(filePath);
const arrayBuffer = Buffer.from(data);
const uint8Array = new Uint8Array(arrayBuffer);
let hashData = sm3( uint8Array ) // 杂凑
$logger.info( hashData )
}
});
parser.addCmdLine("sm4en3 <key> <data>", "sm4 加密,密钥通过 sm3 制备;", function (cli) {
const {sm4en3} = require("./crypto");
let key = cli.getParamValue("key")
let data = cli.getParamValue("data")
let out = sm4en3( key, data )
$logger.info( out );
});
parser.addCmdLine("sm4de3 <key> <data>", "sm4 解密,密钥通过 sm3 制备;", function (cli) {
const {sm4de3} = require("./crypto");
let key = cli.getParamValue("key")
let data = cli.getParamValue("data")
let out = sm4de3( key, data )
$logger.info( out );
});
// parser.addCmdLine("base64 <-en/-de> <data>", "base64 编解码", function (cli) {
parser.addCmdLine("base64 [-en] [-de] [-hex] <data>", "base64 编解码;", function (cli) {
let data = cli.getParamValue("data")
if ( cli.hasFlag("de") ) {
if ( cli.hasFlag("hex") ) {
$logger.info( "执行 base64 decode to hex: " );
$logger.info( convutil.base64ToHex( data ) );
} else {
$logger.info( "执行 base64 decode to string: " );
$logger.info( convutil.base64ToString( data ) );
}
} else {
if ( cli.hasFlag("hex") ) {
$logger.info( "执行 hex encode to base64: " );
$logger.info( convutil.hexToBase64( data ) );
} else {
$logger.info( "执行 string encode to base64: " );
$logger.info( convutil.stringToBase64( data ) );
}
}
});
parser.addCmdLine("replace [-d] <file_name> <re1> <re2>", "文件内容正则替换;", function (cli) {
$logger.info( "暂未实现" );
});
parser.addCmdLine("crlf [-d] [-r] [--path] [--filter-name] [--filter-path] <crlf:{CRLF,CR,LF,\\r\\n,\\r,\\n}>", "回车一致化", function (cli) {
$logger.info( "暂未实现" );
});
parser.addCmdLine("tcpc [-d] [-x] [--host] [--ip] [--port] [--headlen] [--timeout] [--filepath]", "tcp 报文发收", function (cli) {
$logger.info( "暂未实现" );
});
async function main(){
try {
let cmd = parser.parse(process.argv.slice(2))
if ( ! cmd ) {
console.log( parser.help() )
} else {
$logger.info(`执行命令 ${cmd.cmdline.cmd}`)
if ( cmd.canEval() ){
await cmd.eval()
} else {
console.log( parser.help() )
}
}
} catch ( e ) {
console.error( e )
}
}
main()
/**
* kit cli 注册和调用执行
*/
require("./init")
const { CliCmdParser } = require("../../argv")
const parser = new CliCmdParser("kit V" + $version + " build " + $build);
const net = require("./net")
const shell = require("./shell")
const {sm4en3, sm4de3} = require("./crypto");
const convutil = require("./util/convutil")
const path = require("path");
const fs = require("fs");
const {sm3} = require("sm-crypto");
const zlib = require("zlib");
const mem = require("./mem")
const { getAllDeviceInfo } = require("./platform")
const kitcommand = require("./kitcommand")
const glance = require("./glance")
kitcommand.init()
kitcommand.regTo( parser )
const registry = require("./registry")
registry.init()
registry.regTo( parser )
// parser.addCmdLine("man", "打开在线帮助;", async function (){
// const uConfig = await $context.get("uConfig")
// shell.execSync("start " + uConfig.get("man"))
// });
// parser.addCmdLine("manu", "人工维护配置;", function (){
// shell.execSync("start " + $sConfig.getUserConfigPath())
// });
//
// parser.addCmdLine("run [--file] [func]", "运行项目脚本;", function (cli) {
// $logger.info( "kit 当前执行目录 " + process.cwd());
// const fs = require("fs");
//
// let step = cli.getParamValue("func");
// if ( ! step ) {
// step = "main"
// }
//
// let file = cli.getParamValue("file");
// if ( ! file ) {
// file = "kit"
// }
//
// const stat = fs.existsSync(`./${file}.js`)
// if ( ! stat ) {
// if ( ! cli.getParamValue("file") ) {
// fs.writeFileSync("./kit.js",
// `
// async function main(){
// console.log("hello kit!")
// }
// `);
// } else {
// $logger.info( `文件 ${file} 不存在` );
// return
// }
// }
//
//
// const projectScript = fs.readFileSync("./kit.js", "utf-8");
// const vm = require('node:vm');
// const script = new vm.Script(projectScript + ";" + step + "()");
// const context = {
// animal: 'cat',
// count: 2,
// $logger,
// shell
// };
//
// script.runInNewContext(context, {
// timeout : 10 * 60 * 1000,
// breakOnSigint : true,
// });
// $logger.info( "暂未实现" );
// });
//
// parser.addCmdLine("config <key> [value]", "查询/设置配置;", async function (cli){
// let key = cli.getParamValue("key");
// let value = cli.getParamValue("value");
// const uConfig = await $context.get("uConfig")
// if (null != key && null == value) {
// let v = uConfig.get(key);
// $logger.info("{} : {}", key, v);
// } else if (null != key && null != value) {
// uConfig.set(key, value);
// $logger.info("set {} : {}", key, value);
// } else {
// $logger.error("参数异常");
// }
// });
//
// parser.addCmdLine("remote-check", "验证远程仓库;", async function (){
// let remote = await $context.get("remote")
// if (! ( await remote.check()) ) {
// $logger.error("远程仓库不可用")
// return
// } else {
// $logger.info("远程仓库可用")
// }
// });
//
// parser.addCmdLine("publish", "发布到远程仓库;", async function (){
// let remote = await $context.get("remote")
// if (! ( await remote.check()) ) {
// $logger.error("远程仓库不可用")
// return
// } else {
// $logger.info("远程仓库可用")
// }
// remote.publish()
// });
// parser.addCmdLine("install [module]", "安装/更新模块;", async function (cli){
// let remote = await $context.get("remote")
// if (! ( await remote.check()) ) {
// $logger.error("远程仓库不可用")
// return
// } else {
// $logger.info("远程仓库可用")
// }
// remote.install(cli.getParamValue("module"))
// });
parser.addCmdLine("platform [-json]", "平台信息", function (cli) {
// 调用主函数并打印结果
getAllDeviceInfo().then(info => {
console.log('Device Information:');
let table = []
for ( let k in info.os ){
table.push({ '项' : 'OS ' + k, '值' : info.os[k]})
}
for ( let k in info.cpu ){
table.push({ '项' : 'CPU ' + k, '值' : info.cpu[k]})
}
for ( let k in info.memory ){
if ( k.endsWith("Size") ) {
continue;
}
table.push({ '项' : 'Mem ' + k, '值' : info.memory[k]})
}
if( cli.hasFlag("json") ) {
console.log( JSON.stringify(info, null, 4));
} else {
console.table(table);
const disks = info.disk;
const diskTable = []
disks.forEach(disk => {
diskTable.push({
'盘符' : disk.mount
,'总空间' : `${(disk.size / 1024 / 1024 / 1024).toFixed(2)} GB`
,'已用空间' : `${(disk.used / 1024 / 1024 / 1024).toFixed(2)} GB`
,'可用空间' : `${(disk.available / 1024 / 1024 / 1024).toFixed(2)} GB`
,'使用率' : `${disk.use}%`
})
});
console.table(diskTable);
}
}).catch(error => {
console.error('Error getting device information:', error);
});
});
parser.addCmdLine("freem [level]", "释放内存", function (cli) {
let level = cli.getParamValue("level")
mem.free( level )
});
parser.addCmdLine("dockersh <container> [--file]", "反向获取 docker 容器启动脚本", function (cli) {
let container = cli.getParamValue("container")
let file = cli.getParamValue("file")
import("./docker/index.js").then(psl => psl.main(container, file))
});
parser.addCmdLine("killport <port> [-dry]", "释放端口;-dry 仅显示端口与进程信息,不执行清理", async function (cli) {
let port = cli.getParamValue("port")
let dry = cli.hasFlag("dry")
await net.killport( parseInt(port, 10), dry );
});
parser.addCmdLine("glance [--pid] [--port] [--report] [--startTime] [--endTime]", "进程监控:--pid/--port 采集监控数据(仅 Linux);--report 生成报告 HTML,传端口号时导出到临时目录并开启 http 服务在线查看;", async function (cli) {
await glance.main(cli);
});
parser.addCmdLine("getport [--port]", "获取一个可用端口;或查询指定端口状态;", async function (cli) {
const getPort = await import('get-port').then(psl => psl.default);
let port = cli.getParamValue("port")
if( ! port ) {
let ret = await getPort()
$logger.info(`端口 ${ret} 未被使用`)
} else {
await net.getport( port )
}
});
parser.addCmdLine("checkip", "获取自己的公网 ip;", async function (cli) {
let ret = await net.checkip()
if( "ok" === ret.ret ) {
$logger.info( "ip : " + ret.data.ip )
$logger.info( "location : " + ret.data.location.join(" ") )
} else {
$logger.error("获取失败")
}
});
parser.addCmdLine("scan <ip-and-mask> [ports]", "扫描局域网;", async function (cli) {
let ipAndMask = cli.getParamValue("ip-and-mask")
let ports = cli.getParamValue("ports")
let ret = await net.scan(ipAndMask, ports)
$logger.info( JSON.stringify(ret, null, 4) )
});
parser.addCmdLine("telnet [--timeout] <ip-port> [port]", "验证远程 TCP 端口是否可用;", async function (cli) {
let ip = cli.getParamValue("ip-port")
let port = 80;
// 允许冒号形式指定端口
const re = /^(?<ip>.*):(?<port>[0-9]+)$/
const match = ip.match( re );
if ( match ) {
ip = match.groups.ip;
port = match.groups.port;
} else {
// 如果指定了 port 参数,那么使用 port 参数
let dport = cli.getParamValue("port")
if ( dport ) {
port = dport;
}
}
// 支持不填写 ip,默认访问本地
if( "" === ip.trim() ) {
ip = "127.0.0.1";
}
try {
await net.telnet(port, ip, cli.getParamValue("timeout"))
$logger.info("连接成功")
process.exit(0)
}catch (e) {
$logger.error( e )
process.exit(-1)
}
});
parser.addCmdLine("checklist <checkListFile>", "生成/验证文件 hash", async function (cli) {
let checkListFile = cli.getParamValue("checkListFile")
if ( !fs.existsSync( checkListFile ) ) {
$logger.error(`file ${checkListFile} 不存在`)
process.exit(-2)
}
const sm3 = require('sm-crypto').sm3
const list = fs.readFileSync(checkListFile, "UTF-8")
const files = list.split('\n').filter(line => line.trim() !== '');
for ( let file of files ) {
const infos = file.split(/\s+/)
const info = {}
if ( infos.length > 2 ) {
info.file = infos[0]
info.hash = infos[1]
info.alg = infos[2]
} else {
info.file = infos[0]
info.hash = infos[1]
info.alg = "md5"
}
if ( "sm3" === info.alg ) {
if ( !fs.existsSync( info.file ) ) {
$logger.error(`file ${info.file} 不存在`)
process.exit(-2)
}
const data = fs.readFileSync(info.file);
const arrayBuffer = Buffer.from(data);
const uint8Array = new Uint8Array(arrayBuffer);
let hashData = sm3( uint8Array ) // 杂凑
if ( hashData === info.hash ) {
$logger.info(`file ${file} 验证通过`)
} else {
$logger.error(`file ${file} 验证不通过`)
process.exit(-400)
}
} else {
$logger.error(`不支持 file ${file} `)
process.exit(-2)
}
}
$logger.info(`${checkListFile} 中的文件全部验证通过`)
})
parser.addCmdLine("sm3 [--file] [--text] [--hash]", "生成/验证 SM3 值;", async function (cli) {
let filePath = cli.getParamValue("file")
let text = cli.getParamValue("text")
if( !filePath && !text ) {
$logger.error(`file 和 text 入参不能同时有`)
process.exit(-1)
}
const sm3 = require('sm-crypto').sm3
if ( text ) {
let hashData = sm3( text ) // 杂凑
$logger.info( hashData )
}
if ( filePath ) {
if ( !fs.existsSync( filePath ) ) {
$logger.error(`file ${filePath} 不存在`)
process.exit(-2)
}
const data = fs.readFileSync(filePath);
const arrayBuffer = Buffer.from(data);
const uint8Array = new Uint8Array(arrayBuffer);
let hashData = sm3( uint8Array ) // 杂凑
$logger.info( hashData )
}
});
parser.addCmdLine("sm4en3 <key> <data>", "sm4 加密,密钥通过 sm3 制备;", function (cli) {
const {sm4en3} = require("./crypto");
let key = cli.getParamValue("key")
let data = cli.getParamValue("data")
let out = sm4en3( key, data )
$logger.info( out );
});
parser.addCmdLine("sm4de3 <key> <data>", "sm4 解密,密钥通过 sm3 制备;", function (cli) {
const {sm4de3} = require("./crypto");
let key = cli.getParamValue("key")
let data = cli.getParamValue("data")
let out = sm4de3( key, data )
$logger.info( out );
});
parser.addCmdLine("pkcs7verify <sign> <data>", "验证 pkCS7 签名;", function (cli) {
const {verifyPKCS7} = require("./crypto");
let sign = cli.getParamValue("sign")
let data = cli.getParamValue("data")
let out = verifyPKCS7( sign, data )
if ( out ) {
$logger.info("验证通过")
} else {
$logger.error("验证不通过")
}
});
parser.addCmdLine("pkcs7info <sign>", "从 pkCS7 签名中提取信息(证书、公钥、签名);", function (cli) {
const {getInfoFromPKCS7} = require("./crypto");
let sign = cli.getParamValue("sign")
let out = getInfoFromPKCS7( sign )
$logger.info(' cert:');
$logger.info(' subject:', result.cert?.subjectDN);
$logger.info(' issuer:', result.cert?.issuerDN);
$logger.info(' serialNumber:', result.cert?.serialNumber);
$logger.info(' notBefore:', result.cert?.notBefore);
$logger.info(' notAfter:', result.cert?.notAfter);
$logger.info(' pem:');
$logger.info(result.cert?.pem);
$logger.info(' publicKey:');
$logger.info(result.publicKey);
$logger.info(' publicKey (hex):');
$logger.info(result.publicKeyHex);
$logger.info(' signature (hex):');
$logger.info(result.signatureHex);
});
// parser.addCmdLine("base64 <-en/-de> <data>", "base64 编解码", function (cli) {
parser.addCmdLine("base64 [-en] [-de] [-hex] <data>", "base64 编解码;", function (cli) {
let data = cli.getParamValue("data")
if ( cli.hasFlag("de") ) {
if ( cli.hasFlag("hex") ) {
$logger.info( "执行 base64 decode to hex: " );
$logger.info( convutil.base64ToHex( data ) );
} else {
$logger.info( "执行 base64 decode to string: " );
$logger.info( convutil.base64ToString( data ) );
}
} else {
if ( cli.hasFlag("hex") ) {
$logger.info( "执行 hex encode to base64: " );
$logger.info( convutil.hexToBase64( data ) );
} else {
$logger.info( "执行 string encode to base64: " );
$logger.info( convutil.stringToBase64( data ) );
}
}
});
parser.addCmdLine("touch [-d] <file_name>", "文件时间修改为当前时间;", function (cli) {
const fs = require('fs');
const path = cli.getParamValue("file_name")
// 获取当前日期时间
const now = new Date();
// 修改文件的访问时间和修改时间为当前时间
fs.utimes(path, now, now, (err) => {
if (err) {
$logger.error('修改文件时间出错:', err);
} else {
$logger.info('文件时间修改成功');
}
});
});
parser.addCmdLine("replace [-d] <file_name> <re1> <re2>", "文件内容正则替换;", function (cli) {
$logger.info( "暂未实现" );
});
parser.addCmdLine("crlf [-d] [-r] [--path] [--filter-name] [--filter-path] <crlf:{CRLF,CR,LF,\\r\\n,\\r,\\n}>", "回车一致化", function (cli) {
$logger.info( "暂未实现" );
});
parser.addCmdLine("tcpc [-d] [-x] [--ip] [--port] [--headlen] [--timeout] [--data]", "tcp 报文发收", async function (cli) {
const hex = cli.hasFlag("x");
try {
const recv = await net.tcpc({
ip: cli.getParamValue("ip"),
port: cli.getParamValue("port"),
data: cli.getParamValue("data"),
hex,
headlen: cli.getParamValue("headlen"),
timeout: cli.getParamValue("timeout"),
debug: cli.hasFlag("d")
});
$logger.info( hex ? recv.toString("hex") : recv.toString("utf8") );
process.exit(0)
} catch ( e ) {
$logger.error( e )
process.exit(-1)
}
});
async function main(){
try {
let cmd = parser.parse(process.argv.slice(2))
if ( ! cmd ) {
console.log( parser.help() )
} else {
$logger.info(`执行命令 ${cmd.cmdline.cmd}`)
if ( cmd.canEval() ){
await cmd.eval()
} else {
console.log( parser.help() )
}
}
} catch ( e ) {
console.error( e )
}
}
main()
+3 -1
View File
@@ -1,6 +1,5 @@
const config = require('./config')
const Context = require("./context")
const log4js = require("log4js");
// const layout = log4js.layouts.patternLayout('%m')
if ( ! global.$inited ) {
init()
@@ -15,6 +14,9 @@ function init(){
const package = require("../package.json")
global.$version = package.version
const builder = require("../build.json")
global.$build = builder.buildTime
// 上下文模块
global.$context = new Context();
+186
View File
@@ -0,0 +1,186 @@
const shell = require("./shell");
const os = require("os");
const kitfunction = require("./kitfunction");
async function getAvailableRemote() {
const remote = await $context.get("remote");
if (!(await remote.check())) {
$logger.error("远程仓库不可用");
return null;
}
return remote;
}
module.exports = {
init : function () {
return this;
},
regTo: function (parser) {
parser.addCmdLine("help", "打开在线帮助;", async function () {
const uConfig = await $context.get("uConfig")
if (os.type() === 'Windows_NT') {
shell.execSync("start " + uConfig.get("man"))
} else {
console.log("请用浏览器访问 " + uConfig.get("man"))
}
});
parser.addCmdLine("manu", "人工维护配置;", function () {
if (os.type() === 'Windows_NT') {
shell.execSync("start " + $sConfig.getUserConfigPath())
} else {
console.log("cd " + $sConfig.getUserConfigPath())
}
});
parser.addCmdLine("run [--file] [func]", "运行项目脚本;", function (cli) {
$logger.info("kit 当前执行目录 " + process.cwd());
const fs = require("fs");
let step = cli.getParamValue("func");
if (!step) {
step = "main"
}
let file = cli.getParamValue("file");
if (!file) {
file = "kitx"
}
const scriptFile = `./${file}.js`;
const stat = fs.existsSync(scriptFile)
if (!stat) {
if (!cli.getParamValue("file")) {
fs.writeFileSync("./kitx.js", `
async function main(){
console.log("hello kit!");
}
`);
} else {
$logger.info(`文件 ${file} 不存在`);
return
}
}
const projectScript = fs.readFileSync(scriptFile, "utf-8");
const vm = require('vm');
const script = new vm.Script(projectScript + ";" + step + "()");
const context = kitfunction;
script.runInNewContext(context, {
timeout: 10 * 60 * 1000, breakOnSigint: true,
});
$logger.info("执行结束");
});
parser.addCmdLine("config <key> [value]", "查询/设置配置;", async function (cli) {
let key = cli.getParamValue("key");
let value = cli.getParamValue("value");
const uConfig = await $context.get("uConfig")
if (null != key && null == value) {
let v = uConfig.get(key);
$logger.info("{} : {}", key, v);
} else if (null != key && null != value) {
uConfig.set(key, value);
$logger.info("set {} : {}", key, value);
} else {
$logger.error("参数异常");
}
});
parser.addCmdLine("remote", "验证远程仓库可达;", async function () {
const remote = await getAvailableRemote();
if (remote) {
$logger.info("远程仓库可用");
}
});
parser.addCmdLine("check [module]", "验证远程仓库可达;指定 module 时查看远程 meta 信息;", async function (cli) {
const remote = await getAvailableRemote();
if (!remote) {
return;
}
const module = cli.getParamValue("module");
if (!module) {
$logger.info("远程仓库可用");
return;
}
try {
const parsed = remote.parseProjectVersion(module);
const meta = await remote.getProjectMetadata(parsed.projectId, parsed.group);
$logger.info(JSON.stringify(meta, null, 4));
} catch (e) {
$logger.info(`远程 module 不存在: ${module}`);
}
});
parser.addCmdLine("publish [localFile] [remotePath]", "发布到远程仓库; localFile 为空时使用当前目录 meta.json", async function (cli) {
const fs = require("fs");
let localFile = cli.getParamValue("localFile");
let remotePath = cli.getParamValue("remotePath");
if (localFile && !fs.existsSync(localFile)) {
$logger.error("本地文件不存在");
return;
}
if (!localFile && !fs.existsSync("meta.json")) {
$logger.error("没有找到本地配置 meta.json");
return;
}
const remote = await getAvailableRemote();
if (!remote) {
return;
}
if (remotePath) {
$logger.info("发布到远程路径 " + remotePath);
} else {
$logger.info("使用 meta.json 中的 group/id/version 规则发布");
}
await remote.publish(localFile, remotePath);
$logger.info("发布成功");
process.exit(0);
});
parser.addCmdLine("install [module]", "安装/更新模块; 支持 myapp、myapp@1.0、/docker/net/myapp", async function (cli) {
const remote = await getAvailableRemote();
if (!remote) {
return;
}
await remote.install(cli.getParamValue("module"));
});
parser.addCmdLine("list [module]", "列出本地已安装版本;", async function (cli) {
let module = cli.getParamValue("module");
if (!module) {
module = "kit";
}
const remote = await $context.get("remote");
await remote.list(module);
});
parser.addCmdLine("use <module-version>", "切换到已安装版本,例如 myapp@1.0;", async function (cli) {
const remote = await $context.get("remote");
const version = await remote.use(cli.getParamValue("module-version"));
$logger.info("当前版本 " + version);
});
parser.addCmdLine("rollback <module>", "回滚到上一个已安装版本;", async function (cli) {
const remote = await $context.get("remote");
const version = await remote.rollback(cli.getParamValue("module"));
$logger.info("当前版本 " + version);
});
parser.addCmdLine("remove <module-version>", "删除本地已安装版本,例如 myapp@1.0;", async function (cli) {
const remote = await $context.get("remote");
await remote.remove(cli.getParamValue("module-version"));
$logger.info("删除成功");
});
}
}
+557
View File
@@ -0,0 +1,557 @@
const crypto = require("crypto");
const fs = require("fs");
const os = require("os");
const path = require("path");
const platform = os.type() === "Windows_NT" ? require("./windows/kitfunction") : require("./linux/kitfunction");
const vars = Object.create(null);
function setVar(name, value) {
if (name) {
vars[name] = value;
}
return value;
}
function getVar(name) {
return vars[name];
}
function resolveValue(value) {
if (typeof value !== "string") {
return value;
}
const varMatch = value.match(/^%([A-Za-z_][A-Za-z0-9_]*)%$/);
if (varMatch) {
return getVar(varMatch[1]);
}
const randomMatch = value.match(/^%random\((number),(\d+)\)%$/);
if (randomMatch) {
const length = Number(randomMatch[2]);
let result = "";
while (result.length < length) {
result += crypto.randomInt(0, 10).toString();
}
return result;
}
return value;
}
function info(msg) {
const text = String(resolveValue(msg) ?? "");
const line = "=".repeat(Math.max(24, text.length + 8));
console.log(`\n\x1b[36m${line}\x1b[0m`);
console.log(`\x1b[1m\x1b[33m>>> ${text}\x1b[0m`);
console.log(`\x1b[36m${line}\x1b[0m\n`);
}
function mkdir(dirPath) {
if (!dirPath) {
throw new Error("mkdir path is required");
}
fs.mkdirSync(path.resolve(dirPath), { recursive: true });
}
function rm(targetPath) {
if (!targetPath) {
throw new Error("rm path is required");
}
fs.rmSync(path.resolve(targetPath), { recursive: true, force: true });
}
function copyDir(from, to) {
fs.mkdirSync(to, { recursive: true });
for (const item of fs.readdirSync(from, { withFileTypes: true })) {
const source = path.join(from, item.name);
const destination = path.join(to, item.name);
if (item.isDirectory()) {
copyDir(source, destination);
} else if (item.isSymbolicLink()) {
fs.rmSync(destination, { recursive: true, force: true });
fs.symlinkSync(fs.readlinkSync(source), destination);
} else {
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(source, destination);
}
}
}
function cp(from, to) {
if (!from || !to) {
throw new Error("cp from and to are required");
}
const source = path.resolve(from);
if (!fs.existsSync(source)) {
return;
}
let destination = path.resolve(to);
const stat = fs.statSync(source);
if (!stat.isDirectory() && fs.existsSync(destination) && fs.statSync(destination).isDirectory()) {
destination = path.join(destination, path.basename(source));
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
if (stat.isDirectory()) {
rm(destination);
copyDir(source, destination);
} else {
fs.copyFileSync(source, destination);
}
}
function mv(from, to) {
if (!from || !to) {
throw new Error("mv from and to are required");
}
const source = path.resolve(from);
if (!fs.existsSync(source)) {
return;
}
let destination = path.resolve(to);
const stat = fs.statSync(source);
if (!stat.isDirectory() && fs.existsSync(destination) && fs.statSync(destination).isDirectory()) {
destination = path.join(destination, path.basename(source));
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
rm(destination);
try {
fs.renameSync(source, destination);
} catch (e) {
cp(source, destination);
rm(source);
}
}
function normalizeList(value) {
if (!value) {
return [];
}
return Array.isArray(value) ? value : [value];
}
function normalizeSlash(value) {
return value.replace(/\\/g, "/");
}
function matchRule(relativePath, rule) {
if (typeof rule === "function") {
return rule(relativePath);
}
if (rule instanceof RegExp) {
return rule.test(relativePath);
}
if (typeof rule === "string") {
const text = normalizeSlash(rule);
return relativePath === text || relativePath.startsWith(text.endsWith("/") ? text : `${text}/`) || relativePath.includes(text);
}
return false;
}
function shouldInclude(relativePath, params) {
const normalized = normalizeSlash(relativePath);
const filters = normalizeList(params.filter);
const excludes = normalizeList(params.execute || params.exclude);
if (filters.length > 0 && !filters.some((rule) => matchRule(normalized, rule))) {
return false;
}
if (excludes.some((rule) => matchRule(normalized, rule))) {
return false;
}
return true;
}
function copyFiltered(source, tempRoot, params) {
const sourceStat = fs.statSync(source);
const baseName = path.basename(source);
const root = sourceStat.isDirectory() ? source : path.dirname(source);
function walk(current) {
const relative = path.relative(root, current) || baseName;
const target = path.join(tempRoot, relative);
const stat = fs.statSync(current);
if (!shouldInclude(relative, params)) {
return;
}
if (stat.isDirectory()) {
fs.mkdirSync(target, { recursive: true });
for (const name of fs.readdirSync(current)) {
walk(path.join(current, name));
}
} else {
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(current, target);
}
}
walk(source);
}
function zip(from, to, params = {}) {
if (!from || !to) {
throw new Error("zip from and to are required");
}
const source = path.resolve(from);
if (!fs.existsSync(source)) {
return;
}
const options = Object.assign({}, params, {
type: params.type || "zip",
password: resolveValue(params.password)
});
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "kit-zip-"));
try {
copyFiltered(source, tempRoot, options);
platform.zip(tempRoot, to, options);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}
function md5(filePath, params = {}) {
const target = filePath ? path.resolve(filePath) : "";
let value = "0000000000000000";
if (target && fs.existsSync(target) && fs.statSync(target).isFile()) {
value = crypto.createHash("md5").update(fs.readFileSync(target)).digest("hex");
}
return setVar(params.var, value);
}
function parseKey(key) {
if (!key) {
return [];
}
return String(key).split(".").filter((item) => item.length > 0).map((item) => /^\d+$/.test(item) ? Number(item) : item);
}
function readJsonValue(data, key) {
let current = data;
for (const item of parseKey(key)) {
if (current == null) {
return undefined;
}
current = current[item];
}
return current;
}
function ensureContainer(parent, key, nextKey) {
if (parent[key] == null || typeof parent[key] !== "object") {
parent[key] = typeof nextKey === "number" ? [] : {};
}
}
function writeJsonValue(data, key, value) {
const keys = parseKey(key);
if (keys.length === 0) {
return value;
}
let current = data;
for (let index = 0; index < keys.length - 1; index++) {
ensureContainer(current, keys[index], keys[index + 1]);
current = current[keys[index]];
}
current[keys[keys.length - 1]] = value;
return data;
}
function readJson(filePath, key, params = {}) {
if (!filePath) {
throw new Error("readJson filePath is required");
}
const data = JSON.parse(fs.readFileSync(path.resolve(filePath), "utf-8"));
return setVar(params.var, readJsonValue(data, key));
}
function writeJson(filePath, key, value) {
if (!filePath) {
throw new Error("writeJson filePath is required");
}
const target = path.resolve(filePath);
let data = {};
if (fs.existsSync(target)) {
data = JSON.parse(fs.readFileSync(target, "utf-8"));
}
const nextData = writeJsonValue(data, key, resolveValue(value));
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, JSON.stringify(nextData, null, 4), "utf-8");
return nextData;
}
function resolvePlainPassword(params) {
if (params.plainPwd !== undefined) {
return params.plainPwd;
}
if (params.plainPassword !== undefined) {
return params.plainPassword;
}
if (params.plaintextPwd !== undefined) {
return params.plaintextPwd;
}
if (params.plaintextPassword !== undefined) {
return params.plaintextPassword;
}
return undefined;
}
function resolveScpPassword(params) {
const plainPassword = resolvePlainPassword(params);
if (plainPassword !== undefined) {
return plainPassword;
}
return resolveValue(params.pwd || params.password);
}
function scp(from, to, params = {}) {
if (!from || !to) {
throw new Error("scp from and to are required");
}
const pwd = resolveScpPassword(params);
platform.scp(from, to, Object.assign({}, params, {
pwd,
password: pwd,
key: resolveValue(params.key || params.identityFile),
identityFile: resolveValue(params.identityFile || params.key)
}));
}
function loadSsh2Client() {
try {
return require("ssh2").Client;
} catch (e) {
throw new Error("remoteShell requires pure JS dependency ssh2. Please run npm install ssh2 in kit project.");
}
}
function normalizeRemoteShellConfig(config = {}) {
const host = config.host || config.hostname;
if (!host) {
throw new Error("remoteShell host is required");
}
const username = config.username || config.user;
if (!username) {
throw new Error("remoteShell username is required");
}
const password = resolveScpPassword(config);
const privateKey = config.privateKey || config.keyText || (config.keyFile ? fs.readFileSync(path.resolve(config.keyFile), "utf-8") : undefined);
const connectConfig = Object.assign({}, config, {
host,
username,
port: config.port || 22,
password,
privateKey,
passphrase: resolveValue(config.passphrase)
});
delete connectConfig.user;
delete connectConfig.hostname;
delete connectConfig.pwd;
delete connectConfig.plainPwd;
delete connectConfig.plainPassword;
delete connectConfig.plaintextPwd;
delete connectConfig.plaintextPassword;
delete connectConfig.keyText;
delete connectConfig.keyFile;
return connectConfig;
}
function remoteShell(config = {}) {
const Client = loadSsh2Client();
const connectConfig = normalizeRemoteShellConfig(config);
let client = null;
let connectPromise = null;
function connect() {
if (client) {
return Promise.resolve(client);
}
if (connectPromise) {
return connectPromise;
}
connectPromise = new Promise((resolve, reject) => {
const conn = new Client();
conn.once("ready", () => {
client = conn;
resolve(conn);
});
conn.once("error", (e) => {
connectPromise = null;
reject(e);
});
conn.once("close", () => {
client = null;
connectPromise = null;
});
conn.connect(connectConfig);
});
return connectPromise;
}
async function exec(cmd, options = {}) {
if (!cmd) {
throw new Error("remoteShell exec cmd is required");
}
const conn = await connect();
return new Promise((resolve, reject) => {
const execOptions = Object.assign({}, options);
delete execOptions.stdio;
delete execOptions.rejectOnError;
conn.exec(cmd, execOptions, (err, stream) => {
if (err) {
reject(err);
return;
}
let stdout = "";
let stderr = "";
stream.on("error", reject);
stream.on("close", (code, signal) => {
const result = { code, signal, stdout, stderr };
if (options.rejectOnError === false || code === 0) {
resolve(result);
} else {
const error = new Error(`Remote command failed with code ${code}: ${cmd}`);
error.result = result;
reject(error);
}
});
stream.on("data", (data) => {
const text = data.toString();
stdout += text;
if (options.stdio === "inherit") {
process.stdout.write(text);
}
});
stream.stderr.on("data", (data) => {
const text = data.toString();
stderr += text;
if (options.stdio === "inherit") {
process.stderr.write(text);
}
});
});
});
}
async function execText(cmd, options = {}) {
const result = await exec(cmd, options);
return result.stdout;
}
async function call(cmd, options = {}) {
return exec(cmd, Object.assign({ stdio: "inherit" }, options));
}
function close() {
if (client) {
client.end();
}
client = null;
connectPromise = null;
}
return { connect, exec, execText, call, close };
}
function exit(code = 0) {
process.exit(code);
}
function isSecretLogKey(key) {
return /^(pwd|password|plainPwd|plainPassword|plaintextPwd|plaintextPassword|privateKey|passphrase|keyText)$/i.test(key);
}
function redactLogValue(value) {
if (Array.isArray(value)) {
return value.map(redactLogValue);
}
if (value && typeof value === "object") {
const result = {};
for (const key of Object.keys(value)) {
result[key] = isSecretLogKey(key) ? "******" : redactLogValue(value[key]);
}
return result;
}
return value;
}
function stringifyLogValue(value) {
if (typeof value === "string") {
return value;
}
if (value === undefined) {
return "undefined";
}
if (typeof value === "function") {
return `[Function ${value.name || "anonymous"}]`;
}
try {
return JSON.stringify(redactLogValue(value));
} catch (e) {
return String(value);
}
}
function printCallLog(name, args, result) {
console.log(`调用 ${name} 功能`);
console.log(`入参 ${args.map(stringifyLogValue).join(" ")}`);
if (result === undefined) {
console.log("执行结束");
} else {
console.log(`执行结果 ${stringifyLogValue(result)}`);
}
}
function withCallLog(name, fn) {
return function (...args) {
const result = fn.apply(this, args);
printCallLog(name, args, result);
return result;
};
}
module.exports = {
info: withCallLog("info", info),
mkdir: withCallLog("mkdir", mkdir),
rm: withCallLog("rm", rm),
cp: withCallLog("cp", cp),
mv: withCallLog("mv", mv),
zip: withCallLog("zip", zip),
md5: withCallLog("md5", md5),
readJson: withCallLog("readJson", readJson),
writeJson: withCallLog("writeJson", writeJson),
scp: withCallLog("scp", scp),
remoteShell: withCallLog("remoteShell", remoteShell),
exit: withCallLog("exit", exit),
setVar: withCallLog("setVar", setVar),
getVar: withCallLog("getVar", getVar),
vars
};
+21
View File
@@ -0,0 +1,21 @@
/**
* 将 kit 或其他文件注册为开机启动
* kit 启动通过 auto-launch 模块实现,其他文件通过 kit 调用系统命令启动。
*/
const AutoLaunch = require('auto-launch');
function reg(){
var launch = new AutoLaunch({
name: 'Kit',
path: 'node D:\\fullstack\\TDevOps\\kit\\src\\index.js launch',
});
launch.enable();
}
reg()
+117
View File
@@ -0,0 +1,117 @@
const { execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");
function commandExists(command) {
try {
execFileSync("which", [command], { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}
function run(command, args, options = {}) {
return execFileSync(command, args, Object.assign({ stdio: "inherit" }, options));
}
function addToZip(sourceDir, to, password) {
fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true });
if (commandExists("zip")) {
const args = ["-r"];
if (password) {
args.push("-P", password);
}
args.push(path.resolve(to), ".");
run("zip", args, { cwd: sourceDir, stdio: "inherit" });
return;
}
if (password) {
throw new Error("Linux password zip requires zip in PATH");
}
const script = `import zipfile, pathlib\nroot = pathlib.Path(${JSON.stringify(path.resolve(sourceDir))})\nout = pathlib.Path(${JSON.stringify(path.resolve(to))})\nout.parent.mkdir(parents=True, exist_ok=True)\nmode = 'a' if out.exists() else 'w'\nwith zipfile.ZipFile(out, mode, zipfile.ZIP_DEFLATED) as z:\n for p in root.rglob('*'):\n if p.is_file():\n z.write(p, p.relative_to(root).as_posix())`;
run("python3", ["-c", script]);
}
function addToTar(sourceDir, to) {
fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true });
const args = fs.existsSync(to) ? ["-rf", path.resolve(to), "."] : ["-cf", path.resolve(to), "."];
run("tar", args, { cwd: sourceDir, stdio: "inherit" });
}
function zip(sourceDir, to, params = {}) {
if (params.type === "tar") {
addToTar(sourceDir, to);
} else {
addToZip(sourceDir, to, params.password);
}
}
function isRemotePath(value) {
if (typeof value !== "string") {
return false;
}
if (/^[A-Za-z]:[\\/]/.test(value)) {
return false;
}
return /^[^\s:]+:.+/.test(value);
}
function resolveScpPath(value) {
return isRemotePath(value) ? value : path.resolve(value);
}
function appendScpParams(args, params) {
if (params.recursive !== false) {
args.push("-r");
}
if (params.port) {
args.push("-P", String(params.port));
}
if (params.key) {
args.push("-i", params.key);
}
if (params.preserve) {
args.push("-p");
}
if (params.compress) {
args.push("-C");
}
if (params.strictHostKeyChecking !== undefined) {
args.push("-o", `StrictHostKeyChecking=${params.strictHostKeyChecking ? "yes" : "no"}`);
}
if (params.options) {
for (const name of Object.keys(params.options)) {
args.push("-o", `${name}=${params.options[name]}`);
}
}
if (params.args) {
args.push(...(Array.isArray(params.args) ? params.args : [params.args]));
}
}
function scp(from, to, params = {}) {
const source = resolveScpPath(from);
if (!isRemotePath(from) && !fs.existsSync(source)) {
return;
}
const args = [];
appendScpParams(args, params);
args.push(source, resolveScpPath(to));
if (params.pwd) {
if (!commandExists("sshpass")) {
throw new Error("Linux scp with pwd requires sshpass in PATH");
}
run("sshpass", ["-p", params.pwd, "scp", ...args]);
return;
}
run("scp", args);
}
module.exports = { zip, scp };
+29 -19
View File
@@ -1,29 +1,39 @@
const { findPortPid, cmdlineOf } = require("./portpid");
async function killport(port) {
async function killport(port, dry) {
$logger.info(`kill process on port ${port}`);
const util = require('util');
const exec = util.promisify(require('child_process').exec);
// 解析监听进程:若由 docker-proxy 发布端口,会优先返回容器内真实应用进程
const pid = findPortPid(port);
if (pid == null) {
$logger.error("此端口未被使用,或清理进程失败。");
return;
}
const cmd = cmdlineOf(pid);
const program = cmd.length > 0 && cmd[0] ? cmd[0] : `pid=${pid}`;
if (program.includes("docker-proxy")) {
// 容器内应用未就绪时 findPortPid 会退回 docker-proxy 自身;
// 杀掉它既停不掉容器内的服务,也会被 docker 重新拉起,只清理真实进程。
$logger.error(`端口=${port} 由 docker-proxy 发布,但未定位到容器内的真实应用进程,已跳过清理(避免误杀 docker 进程)。`);
return;
}
$logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${program};`);
if (dry) {
$logger.info(`dry 模式,未执行清理: pid=${pid}`);
return;
}
try {
// 查 PID
let { stdout : stdout1 } = exec(`lsof -t -sTCP:LISTEN -i:${port}`)
let pid = stdout1.trim();
$logger.debug(`绑定端口号 ${port} 的进程 PID=${pid}`)
// 查进程信息
let { stdout : stdout2 } = exec(`ps -p ${pid} -o comm=`)
let processName = stdout2.trim();
$logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${processName};`);
// Kill 进程
exec(`kill -9 ${pid}`)
await exec(`kill -9 ${pid}`)
$logger.info(`已结束进程 PID=${pid}`);
} catch (e) {
if ( 1 === e.code ) {
$logger.error("此端口未被使用,或清理进程失败。")
return
}
$logger.error("此端口未被使用,或清理进程失败。")
}
}
+278
View File
@@ -0,0 +1,278 @@
/**
* 端口 -> 进程 pid 解析(仅支持 Linux,基于 /proc)
*
* 优先命中容器内真实应用进程而非 docker-proxy;端口映射不一致
* (如 0.0.0.0:6380->6379)时按 docker-proxy 解析出的容器 IP/端口定位:
* 1. 宿主网络命名空间中监听该端口的 fd 属主(普通端口服务,如 sshd)
* 2. 若属主是 docker-proxy,则解析其 -container-ip/-container-port,
* 在容器网络命名空间内按容器内端口查找持有监听 fd 的真实应用进程
* (处理 0.0.0.0:6380->6379 这类端口映射不一致,并用 fib_trie 的 /32
* 本地地址匹配容器 IP 以区分布局在网段相同、内网端口相同的多个容器)
* 3. 宿主无监听时(userland-proxy 关闭),直接扫描非宿主 netns 的监听进程
*/
const fs = require("fs");
const path = require("path");
/**
* 读取进程所在网络命名空间标识
* @param {number} pid
* @returns {string|null}
*/
function readNetNs(pid) {
try {
return fs.readlinkSync(`/proc/${pid}/ns/net`);
} catch (e) {
return null;
}
}
/**
* 读取一个网络命名空间内监听指定端口的 socket inode 集合
* @param {string} netNs
* @param {number} port
* @returns {Set<string>}
*/
function listenInodesInNetNs(netNs, port) {
const out = new Set();
for (const entry of fs.readdirSync("/proc")) {
if (!/^\d+$/.test(entry)) {
continue;
}
if (readNetNs(Number(entry)) !== netNs) {
continue;
}
for (const file of ["tcp", "tcp6"]) {
let text;
try {
text = fs.readFileSync(`/proc/${entry}/net/${file}`, "utf8");
} catch (e) {
continue;
}
for (const line of text.split("\n").slice(1)) {
const parts = line.trim().split(/\s+/);
if (parts.length < 10 || parts[3] !== "0A") {
continue;
}
const local = parts[1];
const idx = local.lastIndexOf(":");
if (idx < 0) {
continue;
}
if (parseInt(local.slice(idx + 1), 16) === Number(port)) {
out.add(parts[9]);
}
}
}
}
return out;
}
/**
* 在指定网络命名空间内查找持有这些 socket inode 的进程 pid(归一化为 Tgid)
* @param {string} netNs
* @param {Set<string>} inodes
* @returns {number|null}
*/
function ownerInNetNs(netNs, inodes) {
for (const entry of fs.readdirSync("/proc")) {
if (!/^\d+$/.test(entry)) {
continue;
}
if (readNetNs(Number(entry)) !== netNs) {
continue;
}
const fdDir = `/proc/${entry}/fd`;
let names;
try {
names = fs.readdirSync(fdDir);
} catch (e) {
continue;
}
for (const fd of names) {
try {
const target = fs.readlinkSync(path.join(fdDir, fd));
const m = target.match(/^socket:\[(\d+)\]$/);
if (m && inodes.has(m[1])) {
return tgidOf(Number(entry));
}
} catch (e) {
// 忽略无法读取的 fd
}
}
}
return null;
}
/**
* 归一化线程 pid 为进程 Tgid
* @param {number} pid
* @returns {number}
*/
function tgidOf(pid) {
try {
const m = fs.readFileSync(`/proc/${pid}/status`, "utf8").match(/^Tgid:\s+(\d+)/m);
return m ? Number(m[1]) : pid;
} catch (e) {
return pid;
}
}
/**
* 读取进程启动参数
* @param {number} pid
* @returns {string[]}
*/
function cmdlineOf(pid) {
try {
return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0");
} catch (e) {
return [];
}
}
/**
* 读取网络命名空间内声明的本地 IPv4 地址集合(/proc/<pid>/net/fib_trie)
* @param {string} netNs
* @returns {Set<string>}
*/
function netNsIpv4s(netNs) {
const ips = new Set();
for (const entry of fs.readdirSync("/proc")) {
if (!/^\d+$/.test(entry)) {
continue;
}
if (readNetNs(Number(entry)) !== netNs) {
continue;
}
try {
const text = fs.readFileSync(`/proc/${entry}/net/fib_trie`, "utf8");
const re = /\b(\d+\.\d+\.\d+\.\d+)\b/g;
let m;
while ((m = re.exec(text)) !== null) {
ips.add(m[1]);
}
break;
} catch (e) {
// 继续尝试下一个候选
}
}
return ips;
}
/**
* 根据端口查找监听进程 pid
*
* 优先命中容器内真实应用进程而非 docker-proxy;端口映射不一致
* (如 0.0.0.0:6380->6379)时按 docker-proxy 解析出的容器 IP/端口定位。
* 容器应用未就绪时仍可能退回 docker-proxy 自身的 pid,调用方需自行判断。
* @param {number} port
* @returns {number|null}
*/
function findPortPid(port) {
const hostNs = readNetNs(1);
if (hostNs === null) {
return null;
}
const hostInodes = listenInodesInNetNs(hostNs, port);
if (hostInodes.size > 0) {
const owner = ownerInNetNs(hostNs, hostInodes);
if (owner !== null) {
const cmd = cmdlineOf(owner);
if (cmd.length > 0 && cmd[0].includes("docker-proxy")) {
const args = {};
for (let i = 1; i < cmd.length - 1; i++) {
if (cmd[i].startsWith("-")) {
args[cmd[i]] = cmd[i + 1];
}
}
const cip = args["-container-ip"];
const cport = Number(args["-container-port"]);
if (Number.isFinite(cport) && cport > 0) {
// 所有可能持有该容器内端口的网络命名空间
const groups = new Map(); // netNs -> Set<inode>
for (const entry of fs.readdirSync("/proc")) {
if (!/^\d+$/.test(entry)) {
continue;
}
const ns = readNetNs(Number(entry));
if (ns === null || ns === hostNs) {
continue;
}
const inodes = new Set();
for (const file of ["tcp", "tcp6"]) {
let text;
try {
text = fs.readFileSync(`/proc/${entry}/net/${file}`, "utf8");
} catch (e) {
continue;
}
for (const line of text.split("\n").slice(1)) {
const parts = line.trim().split(/\s+/);
if (parts.length < 10 || parts[3] !== "0A") {
continue;
}
const idx = parts[1].lastIndexOf(":");
if (idx >= 0 && parseInt(parts[1].slice(idx + 1), 16) === cport) {
inodes.add(parts[9]);
}
}
}
if (inodes.size > 0) {
groups.set(ns, inodes);
}
}
// 优先选 fib_trie 本地地址与容器 IP 一致的命名空间
if (cip) {
for (const [ns, inodes] of groups) {
if (netNsIpv4s(ns).has(cip)) {
const pid = ownerInNetNs(ns, inodes);
if (pid !== null) {
return pid;
}
}
}
}
// 退而求其次:任意持有该容器内端口监听的命名空间的属主
for (const [ns, inodes] of groups) {
const pid = ownerInNetNs(ns, inodes);
if (pid !== null) {
return pid;
}
}
}
// 容器应用未就绪:退回 docker-proxy,保留可见性
return owner;
}
return owner;
}
}
// 宿主无监听(userland-proxy 关闭):扫描非宿主命名空间内监听该端口的进程
const groups = new Map();
for (const entry of fs.readdirSync("/proc")) {
if (!/^\d+$/.test(entry)) {
continue;
}
const ns = readNetNs(Number(entry));
if (ns === null || ns === hostNs) {
continue;
}
const inodes = listenInodesInNetNs(ns, port);
if (inodes.size > 0) {
groups.set(ns, inodes);
}
}
for (const [ns, inodes] of groups) {
const pid = ownerInNetNs(ns, inodes);
if (pid !== null) {
return pid;
}
}
return null;
}
module.exports = {
findPortPid,
cmdlineOf
};
+22
View File
@@ -0,0 +1,22 @@
const osutil = require("./util/osutil")
const shell = require("./shell")
function free(level) {
osutil.choise({
[osutil.Windows] : ( level ) => {
throw Error("暂不支持")
},
[osutil.Linux] : ( level ) => {
if ( [1,2,3].indexOf( parseInt(level,10)) === -1 ) {
level = 1
}
shell.execSync("sync")
shell.execSync(`echo ${level} > /proc/sys/vm/drop_caches`)
},
})
}
module.exports = {
free
}
+476 -83
View File
@@ -1,84 +1,477 @@
require("./init")
const osutil = require("./util/osutil")
const killer = require('cross-port-killer');
const net = require('node:net');
/**
* 模拟 telnet ,检测端口是否开放
* @param port
* @param host
* @param timeout
* @returns {Promise<unknown>}
*/
async function telnet(port, host, timeout=3000){
$logger.debug(`port=${port} host=${host} timeout=${timeout}`)
return new Promise(function (resolve, reject){
const socket = new net.Socket();
socket.setTimeout(timeout, function() {
socket.end();
reject("链接超时")
});
socket.connect(port, host);
socket.on('connect', function() {
resolve('连接成功');
socket.end();
});
socket.on('error', function(err) {
reject('连接失败');
});
})
}
async function killport(port) {
return killer.kill( port );
// let func = osutil.choise([
// require("./windows/net").killport,
// require("./linux/net").killport,
// ])
// return func && func(port);
}
/**
* 获取端口信息
* 因为命令输出缓存问题,有缺陷。
* @param port
* @returns {Promise<void>}
*/
async function getport(port) {
$logger.info( "暂未实现" );
}
async function checkip(){
const url = "https://myip.ipip.net/json"
const http = url.startsWith("https:") ? require("node:https") : require('node:http');
return new Promise((resolve, reject) => {
http.get(url, (res) => {
let data = '';
// A chunk of data has been recieved.
res.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received. Resolve the promise with result
res.on('end', () => {
resolve(JSON.parse(data));
});
}).on("error", (err) => {
// In case of any error reject the promise
reject(err);
});
});
}
module.exports = {
killport, getport, telnet, checkip
require("./init")
const killer = require('cross-port-killer');
const net = require('net');
const dns = require('dns').promises;
/**
* 模拟 telnet ,检测端口是否开放
* @param port
* @param host
* @param timeout
* @returns {Promise<unknown>}
*/
async function telnet(port, host, timeout=3000){
$logger.debug(`port=${port} host=${host} timeout=${timeout}`)
return new Promise(function (resolve, reject){
const socket = new net.Socket();
socket.setTimeout(timeout, function() {
socket.end();
reject("链接超时")
});
socket.connect(port, host);
socket.on('connect', function() {
resolve('连接成功');
socket.end();
});
socket.on('error', function(err) {
reject('连接失败');
});
})
}
async function killport(port, dry) {
// Linux:先解析监听进程。若端口由 docker-proxy 发布,会定位容器内
// 真实应用进程再清理,避免误杀 docker 进程(见 ./linux/net.js)。
if (process.platform === "linux") {
return require("./linux/net").killport(port, dry);
}
// dry 模式:Windows 走 netstat 解析 pid,仅展示不清理
if (dry) {
return require("./windows/net").killport(port, dry);
}
return killer.kill( port );
}
/**
* 获取端口信息
* 因为命令输出缓存问题,有缺陷。
* @param port
* @returns {Promise<void>}
*/
async function getport(port) {
$logger.info( "暂未实现" );
}
async function checkip(){
const url = "https://myip.ipip.net/json"
const http = url.startsWith("https:") ? require("node:https") : require('node:http');
return new Promise((resolve, reject) => {
http.get(url, (res) => {
let data = '';
// A chunk of data has been recieved.
res.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received. Resolve the promise with result
res.on('end', () => {
resolve(JSON.parse(data));
});
}).on("error", (err) => {
// In case of any error reject the promise
reject(err);
});
});
}
/**
* 内网IP端口扫描函数
* @param {string} ipAndMask - IP地址或IP段,如:"192.168.1.1" 或 "192.168.1.0/24"
* @param {string|null} ports - 端口,可以是:null(使用默认端口)、单个端口、多个端口逗号分隔
* @returns {Promise<Array>} - 返回扫描结果数组
*/
async function scan(ipAndMask, ports) {
const defaultPorts = [22, 80, 443, 3389, 8889];
let targetIps = [];
let targetPorts = [];
// 1. 解析端口参数
if (!ports) {
targetPorts = defaultPorts;
} else if (ports.includes(',')) {
targetPorts = ports.split(',').map(p => parseInt(p.trim())).filter(p => p > 0 && p <= 65535);
} else {
targetPorts = [parseInt(ports)];
}
// 2. 解析IP地址/网段
if (ipAndMask.includes('/')) {
// CIDR表示法,如 192.168.1.0/24
targetIps = await parseCIDR(ipAndMask);
} else if (net.isIP(ipAndMask)) {
// 单个IP地址
targetIps = [ipAndMask];
} else {
// 可能是主机名,尝试解析
try {
const addresses = await dns.resolve4(ipAndMask);
targetIps = addresses;
} catch (error) {
throw new Error(`无法解析主机名: ${ipAndMask}`);
}
}
// 3. 扫描所有IP和端口
const scanPromises = targetIps.map(ip =>
scanPortsForIP(ip, targetPorts)
);
const results = await Promise.all(scanPromises);
// 4. 过滤出有开放端口的结果
return results.filter(result => result.openPorts.length > 0);
}
/**
* 解析CIDR格式的IP段
* @param {string} cidr - CIDR格式,如 "192.168.1.0/24"
* @returns {Array} - IP地址数组
*/
function parseCIDR(cidr) {
const [ipStr, maskStr] = cidr.split('/');
const mask = parseInt(maskStr);
if (mask < 0 || mask > 32) {
throw new Error('子网掩码必须在0-32之间');
}
// 将IP转换为32位整数
const ipToInt = (ip) => {
const parts = ip.split('.').map(Number);
return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
};
// 将32位整数转换为IP字符串
const intToIp = (int) => {
return [
(int >>> 24) & 255,
(int >>> 16) & 255,
(int >>> 8) & 255,
int & 255
].join('.');
};
const ipInt = ipToInt(ipStr);
const hostBits = 32 - mask;
const networkMask = (0xFFFFFFFF << hostBits) >>> 0; // 无符号右移
const networkAddress = ipInt & networkMask;
const broadcastAddress = networkAddress | ((1 << hostBits) - 1);
// 排除网络地址和广播地址
const start = networkAddress + 1;
const end = broadcastAddress - 1;
const ips = [];
for (let i = start; i <= end; i++) {
ips.push(intToIp(i));
}
return ips;
}
/**
* 扫描单个IP的所有指定端口
* @param {string} ip - IP地址
* @param {Array} ports - 端口数组
* @param {number} timeout - 连接超时时间(毫秒)
* @returns {Promise<Object>} - 扫描结果
*/
async function scanPortsForIP(ip, ports, timeout = 2000) {
const results = {
ip: ip,
openPorts: [],
scannedPorts: ports.length,
startTime: Date.now()
};
// 并行扫描所有端口
const portPromises = ports.map(port =>
scanPort(ip, port, timeout)
);
const portResults = await Promise.allSettled(portPromises);
// 收集开放的端口
portResults.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value.open) {
results.openPorts.push({
port: ports[index],
service: result.value.service
});
}
});
results.endTime = Date.now();
results.scanDuration = results.endTime - results.startTime;
return results;
}
/**
* 扫描单个端口
* @param {string} ip - IP地址
* @param {number} port - 端口号
* @param {number} timeout - 超时时间
* @returns {Promise<Object>} - 端口扫描结果
*/
function scanPort(ip, port, timeout) {
return new Promise((resolve) => {
const socket = new net.Socket();
const result = {
ip: ip,
port: port,
open: false,
service: getServiceName(port)
};
// 设置超时
socket.setTimeout(timeout);
socket.on('connect', () => {
result.open = true;
socket.destroy();
resolve(result);
});
socket.on('timeout', () => {
socket.destroy();
resolve(result);
});
socket.on('error', () => {
socket.destroy();
resolve(result);
});
socket.on('close', () => {
// 连接已关闭
});
// 开始连接
socket.connect(port, ip);
});
}
/**
* 根据端口号获取常见服务名称
* @param {number} port - 端口号
* @returns {string} - 服务名称
*/
function getServiceName(port) {
const commonServices = {
20: 'FTP Data',
21: 'FTP Control',
22: 'SSH',
23: 'Telnet',
25: 'SMTP',
53: 'DNS',
67: 'DHCP Server',
68: 'DHCP Client',
69: 'TFTP',
80: 'HTTP',
88: 'Kerberos',
110: 'POP3',
123: 'NTP',
135: 'MSRPC',
137: 'NetBIOS Name',
138: 'NetBIOS Datagram',
139: 'NetBIOS Session',
143: 'IMAP',
161: 'SNMP',
162: 'SNMP Trap',
389: 'LDAP',
443: 'HTTPS',
445: 'SMB',
465: 'SMTPS',
514: 'Syslog',
515: 'LPD/LPR',
587: 'SMTP Submission',
636: 'LDAPS',
993: 'IMAPS',
995: 'POP3S',
1433: 'MSSQL',
1521: 'Oracle',
1723: 'PPTP',
1883: 'MQTT',
1900: 'UPnP',
2082: 'cPanel',
2083: 'cPanel SSL',
2086: 'WHM',
2087: 'WHM SSL',
2095: 'Webmail',
2096: 'Webmail SSL',
2181: 'ZooKeeper',
2375: 'Docker',
2376: 'Docker TLS',
3000: 'Node.js',
3306: 'MySQL',
3389: 'RDP',
4333: 'mSQL',
4443: 'HTTPS Alt',
4567: 'Sinatra',
4848: 'GlassFish',
5432: 'PostgreSQL',
5500: 'VNC',
5601: 'Kibana',
5672: 'RabbitMQ',
5900: 'VNC',
5984: 'CouchDB',
6379: 'Redis',
6443: 'Kubernetes API',
6666: 'IRC',
6667: 'IRC',
7000: 'Cassandra',
7001: 'WebLogic',
7077: 'Spark',
7200: 'Cassandra Native',
7474: 'Neo4j',
7687: 'Neo4j Bolt',
7777: 'Game Server',
8000: 'HTTP Alt',
8001: 'Tor',
8080: 'HTTP Proxy',
8081: 'HTTP Proxy Alt',
8088: 'HTTP Alt',
8089: 'HTTP Alt',
8090: 'HTTP Alt',
8091: 'HTTP Alt',
8181: 'HTTP Alt',
8443: 'HTTPS',
8880: 'HTTP Alt',
8888: 'HTTP Alt',
8889: 'Sunyard HSM',
9000: 'PHP-FPM',
9001: 'Tor',
9090: 'HTTP Alt',
9091: 'HTTP Alt',
9100: 'Print Server',
9200: 'Elasticsearch',
9300: 'Elasticsearch',
9418: 'Git',
9999: 'HTTP Alt',
10000: 'Webmin',
11211: 'Memcached',
15672: 'RabbitMQ Management',
27017: 'MongoDB',
27018: 'MongoDB',
28017: 'MongoDB HTTP',
50000: 'SAP',
50030: 'Hadoop JobTracker',
50070: 'Hadoop NameNode',
61616: 'ActiveMQ',
// 添加更多端口...
};
return commonServices[port] || `Unknown (${port})`;
}
/**
* tcp 报文发收:以 hex 或文本发送一包数据,再接收一包并返回。
*
* @param {Object} opts
* @param {string} opts.ip 目标 ip
* @param {string|number} opts.port 目标端口,必填
* @param {string} opts.data 待发送数据
* @param {boolean} [opts.hex=false] true 时 data 为 hex 格式,发送前转回 bytes
* @param {number|string} [opts.headlen] 长度头字节数(1-6):发送前按大端填入 data 长度,接收时按其声明长度读满一包
* @param {number|string} [opts.timeout=3000] 超时毫秒
* @param {boolean} [opts.debug=false] true 时打印过程日志
* @returns {Promise<Buffer>} 接收到的一包数据
*/
async function tcpc({ ip, port, data, hex=false, headlen=null, timeout=3000, debug=false } = {}) {
if ( null == ip || "" === String(ip).trim() ) throw "缺少 --ip";
if ( null == port || "" === String(port).trim() ) throw "缺少 --port";
if ( null == data ) throw "缺少 --data";
const log = function(){ if ( debug ) $logger.info( ...arguments ) };
const dport = parseInt(port, 10);
if ( ! Number.isInteger(dport) || dport < 1 || dport > 65535 ) throw "非法端口: " + port;
const tmo = timeout ? parseInt(timeout, 10) : 3000;
if ( ! Number.isInteger(tmo) || tmo <= 0 ) throw "非法 timeout: " + timeout;
// 组装发送载荷
let payload;
if ( hex ) {
const h = String(data).replace(/\s+/g, "");
if ( h.length % 2 !== 0 ) throw "hex 数据长度必须为偶数";
if ( h.length > 0 && ! /^[0-9a-fA-F]+$/.test(h) ) throw "hex 数据包含非法字符";
payload = Buffer.from(h, "hex");
} else {
payload = Buffer.from(String(data), "utf8");
}
// 长度头:大端填入 data 字节数(不含头自身)
let hl = null;
if ( null != headlen && "" !== String(headlen) ) {
hl = parseInt(headlen, 10);
if ( ! Number.isInteger(hl) || hl < 1 || hl > 6 ) throw "--headlen 必须为 1-6 的整数";
if ( payload.length >= Math.pow(2, hl * 8) ) throw `数据长度 ${payload.length} 超出 ${hl} 字节长度头表示范围`;
}
let packet = payload;
if ( hl ) {
const hdr = Buffer.alloc(hl);
hdr.writeUIntBE(payload.length, 0, hl);
packet = Buffer.concat([hdr, payload]);
}
log(`连接 ${ip}:${dport},超时 ${tmo}ms`);
log(`发送 ${packet.length} 字节: ${packet.toString("hex")}`);
return new Promise(function (resolve, reject){
const socket = new net.Socket();
let timer = null;
let buf = Buffer.alloc(0);
let got = Buffer.alloc(0);
let done = false;
const finish = function(err, val){
if ( done ) return;
done = true;
if ( timer ) clearTimeout(timer);
socket.destroy();
if ( err ) reject(err); else resolve(val);
};
timer = setTimeout(() => finish("收发超时"), tmo);
socket.setTimeout(tmo, () => finish("链接超时"));
socket.on("error", e => finish(e));
socket.connect(dport, ip, () => socket.write(packet));
socket.on("data", chunk => {
if ( hl ) {
buf = Buffer.concat([buf, chunk]);
if ( buf.length < hl ) return;
const declared = buf.readUIntBE(0, hl);
const total = hl + declared;
if ( buf.length < total ) return;
got = buf.slice(0, total);
log(`收到 ${got.length} 字节(头声明 data ${declared} 字节)`);
finish(null, got);
} else {
got = Buffer.concat([got, chunk]);
log(`收到 ${got.length} 字节`);
finish(null, got);
}
});
socket.on("end", () => finish("连接关闭,收包不完整"));
});
}
module.exports = {
killport, getport, telnet, checkip, scan, tcpc
}
+111
View File
@@ -0,0 +1,111 @@
const os = require('os');
const si = require('systeminformation');
const { formatBytes } = require("./util/convutil")
const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs');
const path = require('path');
// 将 exec 函数转换为返回 Promise 的函数
const execPromise = promisify(exec);
// 将 readFile 函数转换为返回 Promise 的函数
const readFilePromise = promisify(fs.readFile);
// 获取操作系统信息
async function getOSInfo() {
return {
type: os.type(),
platform: os.platform(),
release: os.release(),
arch: os.arch(),
os: await getOSDescription()
};
}
// 获取 CPU 信息
function getCPUInfo() {
const cpus = os.cpus();
const physicalCpuCount = require('physical-cpu-count')
return {
model: cpus[0].model,
speed: cpus[0].speed,
cores: cpus.length,
physicalCores: physicalCpuCount
};
}
// 获取内存信息
function getMemoryInfo() {
let info = {
totalSize: os.totalmem(),
freeSize: os.freemem()
}
return Object.assign(info, {
total : formatBytes(info.totalSize),
free : formatBytes(info.freeSize),
});
}
// 主函数,调用上述函数获取所有信息
async function getAllDeviceInfo() {
const osInfo = await getOSInfo();
const cpuInfo = getCPUInfo();
const memoryInfo = getMemoryInfo();
const diskInfo = await si.fsSize();
return {
os: osInfo,
cpu: cpuInfo,
memory: memoryInfo,
disk: diskInfo
};
}
async function getOSDescription() {
if (os.platform() === 'win32') {
return undefined;
}
try {
// 尝试执行 lsb_release 命令
const { stdout } = await execPromise('lsb_release -d');
// 提取描述信息
const distroDescription = stdout.split(':')[1].trim();
return distroDescription;
} catch (lsbError) {
try {
// lsb_release 命令不可用,尝试读取 /etc/os-release 文件
const osReleasePath = path.join('/', 'etc', 'os-release');
const data = await readFilePromise(osReleasePath, 'utf8');
// 解析文件内容
const lines = data.split('\n');
const info = {};
lines.forEach(line => {
const [key, value] = line.split('=');
if (key && value) {
info[key] = value.replace(/^"|"$/g, '');
}
});
// 获取 PRETTY_NAME
const prettyName = info['PRETTY_NAME'];
if (prettyName) {
return prettyName;
}
} catch (fileError) {
console.error('读取 /etc/os-release 文件时出错:', fileError);
}
}
return undefined;
}
module.exports = {
getOSInfo,
getCPUInfo,
getMemoryInfo,
getAllDeviceInfo
};
+98
View File
@@ -0,0 +1,98 @@
/**
* Registry HTTP 客户端
* 调用 Registry 配置中心 REST API
*/
const http = require("http");
const https = require("https");
class RegistryClient {
constructor(baseUrl, token) {
this.baseUrl = String(baseUrl || "").replace(/\/+$/, "");
this.token = token || "";
}
/**
* 发起 HTTP 请求
* @param {string} method - GET/POST/PUT/DELETE
* @param {string} path - API 路径
* @param {object} body - 请求体
* @returns {Promise<object>} 响应 data
*/
request(method, path, body) {
return new Promise((resolve, reject) => {
let url;
try {
url = new URL(this.baseUrl + path);
} catch (e) {
reject(new Error("Registry 地址无效: " + this.baseUrl));
return;
}
const lib = url.protocol === "https:" ? https : http;
const options = {
method,
hostname: url.hostname,
port: url.port || (url.protocol === "https:" ? 443 : 80),
path: url.pathname + url.search,
headers: { "Content-Type": "application/json" },
};
if (this.token) {
options.headers.Authorization = "Bearer " + this.token;
}
const req = lib.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
let parsed = null;
try {
parsed = JSON.parse(data);
} catch (e) {
/* 非 JSON 响应 */
}
if (parsed && parsed.success === true) {
resolve(parsed.data);
} else {
const msg = parsed && parsed.message
? parsed.message
: "HTTP " + res.statusCode;
const err = new Error(msg);
err.status = res.statusCode;
reject(err);
}
});
});
req.on("error", (e) => {
reject(new Error("Registry 不可达: " + e.message));
});
req.setTimeout(10000, () => {
req.destroy(new Error("请求超时"));
});
if (body !== undefined) {
req.write(JSON.stringify(body));
}
req.end();
});
}
listProjects() {
return this.request("GET", "/api/projects");
}
listConfigs(projectId) {
return this.request("GET", `/api/projects/${encodeURIComponent(projectId)}/configs`);
}
createConfig(projectId, data) {
return this.request("POST", `/api/projects/${encodeURIComponent(projectId)}/configs`, data);
}
updateConfig(projectId, configId, data) {
return this.request("PUT", `/api/projects/${encodeURIComponent(projectId)}/configs/${encodeURIComponent(configId)}`, data);
}
}
module.exports = RegistryClient;
+404
View File
@@ -0,0 +1,404 @@
/**
* Registry 模块
* kit 命令行对接 Registry 配置管理中心
*
* 配置项(通过 kit config 设置):
* registryUrl Registry 服务地址
* registryToken Registry 认证 Token
*
* 命令:
* kit registry 查看用法
* kit registry list 列出项目
* kit registry config <project[:path]> 查看配置
* kit registry config <project[:path]> <value> 设置配置
* kit registry export <project[:path]> [file] 导出配置
* kit registry import <project[:path]> <file> 导入配置
*/
const RegistryClient = require("./client");
const USAGE = [
"registry 用法:",
" kit registry list 列出 Registry 项目",
" kit registry config <project[:path]> 查看配置",
" kit registry config <project[:path]> <value> 设置配置",
" kit registry export <project[:path]> [file] 导出配置为 JSON",
" kit registry import <project[:path]> <file> 从 JSON 导入配置",
"配置项: kit config registryUrl <url>, kit config registryToken <token>",
].join("\n");
/**
* 解析 project:path 语法
* @param {string} spec - 例如 CommonExternalService 或 CommonExternalService:data/windows
*/
function parseProjectPath(spec) {
spec = String(spec || "");
const idx = spec.indexOf(":");
if (idx === -1) {
return { project: spec, path: "" };
}
return {
project: spec.substring(0, idx),
path: spec.substring(idx + 1).replace(/^\/+|\/+$/g, ""),
};
}
/**
* 规整 key:去空白、压缩连续斜杠、去首尾斜杠
*/
function normalizeKey(key) {
return String(key || "")
.trim()
.split("/")
.map((s) => s.trim())
.filter((s) => s !== "")
.join("/");
}
/**
* 获取配置并用 path 过滤
* @returns {Promise<{all: Array, matched: Array}>}
*/
async function getConfigsFiltered(client, projectId, prefix) {
const configs = await client.listConfigs(projectId);
if (!prefix) {
return { all: configs, matched: configs };
}
const matched = configs.filter((c) => {
const p = c.path || "";
return p === prefix || p.startsWith(prefix + "/");
});
return { all: configs, matched };
}
/**
* 格式化配置值用于展示
*/
function formatValue(value, type) {
if (value === undefined || value === null) {
return "";
}
if (type === "json" && typeof value !== "string") {
return JSON.stringify(value);
}
return String(value);
}
/**
* 解析项目(支持名称或 ID)
*/
async function resolveProject(client, name) {
const projects = await client.listProjects();
const project = projects.find((p) => p.id === name || p.name === name);
if (!project) {
throw new Error("项目不存在: " + name);
}
return project;
}
/**
* 打印配置表格
*/
function printConfigTable(configs) {
if (!configs || configs.length === 0) {
$logger.info("无匹配的配置");
return;
}
const table = configs.map((c) => ({
路径: c.path,
值: formatValue(c.value, c.type),
类型: c.type,
必填: c.required ? "✓" : "",
存在: c.exists === false ? "✗" : "✓",
}));
console.table(table);
$logger.info("共 " + configs.length + " 条配置");
}
/**
* 构建导出文件内容
*/
function buildExport(project, prefix, configs) {
return {
project: project.name,
path: prefix,
exportedAt: new Date().toISOString(),
configs: configs.map((c) => {
const fullPath = c.path || "";
let key = c.key || "";
if (prefix && fullPath === prefix) {
key = "";
} else if (prefix && fullPath.startsWith(prefix + "/")) {
key = fullPath.substring(prefix.length + 1);
}
return {
key,
path: fullPath,
value: c.value,
type: c.type,
description: c.description,
required: c.required === true,
exists: c.exists !== false,
defaultValue: c.defaultValue,
purpose: c.purpose,
valueRange: c.valueRange,
idc: c.idc || "",
environment: c.environment || "",
group: c.group || "",
};
}),
};
}
/**
* 按完整路径查找已存在的配置
*/
function findExistingByPath(configs, fullPath) {
const target = normalizeKey(fullPath);
return configs.find((c) => normalizeKey(c.path || "") === target);
}
/**
* 将完整路径分解为 key + 已启用 path 维度的值
* 通过前端剥离维度前缀还原存储 key
*/
function decomposeByDims(project, fullPath, dims) {
let key = normalizeKey(fullPath);
let idc = project.enableIdc ? String(dims.idc || "") : "";
let environment = project.enableEnvironment ? String(dims.environment || "") : "";
let group = project.enableGroup ? String(dims.group || "") : "";
if (idc && key.startsWith(idc + "/")) {
key = key.substring(idc.length + 1);
}
if (environment && key.startsWith(environment + "/")) {
key = key.substring(environment.length + 1);
}
if (group && key.startsWith(group + "/")) {
key = key.substring(group.length + 1);
}
return { key: normalizeKey(key), idc, environment, group };
}
/**
* 尝试解析 JSON 字符串
* 对于 json 类型值自动从字符串转为对象
*/
function maybeParseJson(value) {
if (typeof value !== "string") {
return value;
}
const trimmed = value.trim();
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
try {
return JSON.parse(trimmed);
} catch (e) {
// 解析失败,保持原字符串
}
}
return value;
}
/**
* 获取 Registry 客户端
*/
async function getClient() {
const uConfig = await $context.get("uConfig");
const baseUrl = uConfig.get("registryUrl");
if (!baseUrl) {
throw new Error("未配置 Registry 地址,请先运行: kit config registryUrl <url>");
}
const token = uConfig.get("registryToken") || "";
return new RegistryClient(baseUrl, token);
}
/**
* 默认导出文件名
*/
function defaultExportFile(projectName, prefix) {
const safe = String(projectName).replace(/[^a-zA-Z0-9_-]/g, "_");
const suffix = prefix ? "-" + String(prefix).replace(/[^a-zA-Z0-9_-]/g, "_") : "";
return safe + suffix + "-" + new Date().toISOString().slice(0, 10) + ".json";
}
async function cmdList(client) {
const projects = await client.listProjects();
if (!projects || projects.length === 0) {
$logger.info("Registry 上没有项目");
return;
}
const table = [];
for (const p of projects) {
let configs = [];
try {
configs = await client.listConfigs(p.id);
} catch (e) {
/* 忽略单个项目配置数获取失败 */
}
table.push({
ID: p.id,
名称: p.name,
说明: p.description,
机房: p.enableIdc ? "✓" : "✗",
环境: p.enableEnvironment ? "✓" : "✗",
分组: p.enableGroup ? "✓" : "✗",
配置数: configs.length,
});
}
console.table(table);
$logger.info("共 " + projects.length + " 个项目");
}
async function cmdConfig(client, spec, value) {
const { project: projectSpec, path: prefix } = parseProjectPath(spec);
if (!projectSpec) {
throw new Error("缺少 project,用法: kit registry config <project[:path]> [value]");
}
const project = await resolveProject(client, projectSpec);
const { matched } = await getConfigsFiltered(client, project.id, prefix);
if (value === undefined || value === null) {
if (matched.length === 1 && matched[0].path === prefix) {
const c = matched[0];
$logger.info(formatValue(c.value, c.type));
} else {
printConfigTable(matched);
}
return;
}
if (matched.length === 0) {
throw new Error("配置不存在: " + prefix + ",请先用 kit registry import 导入或在 Registry 界面创建");
}
if (matched.length > 1) {
throw new Error("路径匹配到多条配置,请指定完整 key: " + prefix);
}
const target = matched[0];
const newValue = maybeParseJson(value);
const updated = await client.updateConfig(project.id, target.id, { value: newValue });
$logger.info("设置成功: " + updated.path + " = " + formatValue(updated.value, updated.type));
}
async function cmdExport(client, spec, file) {
const { project: projectSpec, path: prefix } = parseProjectPath(spec);
if (!projectSpec) {
throw new Error("缺少 project,用法: kit registry export <project[:path]> [file]");
}
const project = await resolveProject(client, projectSpec);
const { matched } = await getConfigsFiltered(client, project.id, prefix);
const data = buildExport(project, prefix, matched);
const outFile = file || defaultExportFile(project.name, prefix);
const fs = require("fs");
fs.writeFileSync(outFile, JSON.stringify(data, null, 4), "utf-8");
$logger.info("已导出 " + matched.length + " 条配置到 " + outFile);
}
async function cmdImport(client, spec, file) {
const fs = require("fs");
if (!file) {
throw new Error("缺少 file,用法: kit registry import <project[:path]> <file>");
}
if (!fs.existsSync(file)) {
throw new Error("文件不存在: " + file);
}
let data;
try {
const raw = fs.readFileSync(file, "utf-8").replace(/^\uFEFF/, "");
data = JSON.parse(raw);
} catch (e) {
throw new Error("JSON 解析失败: " + e.message);
}
if (!data || !Array.isArray(data.configs)) {
throw new Error("文件格式不正确,缺少 configs 数组");
}
const { project: projectSpec, path: prefix } = parseProjectPath(spec);
if (!projectSpec) {
throw new Error("缺少 project,用法: kit registry import <project[:path]> <file>");
}
const project = await resolveProject(client, projectSpec);
const { all } = await getConfigsFiltered(client, project.id, "");
let created = 0;
let updated = 0;
let skipped = 0;
for (const item of data.configs) {
const baseKey = normalizeKey(item.key);
// 指定前缀时按前缀+相对key 重定位;未指定前缀时沿用导出时的完整路径,保证幂等
const targetPath = normalizeKey(prefix ? prefix + "/" + baseKey : item.path || baseKey);
if (!targetPath) {
skipped++;
continue;
}
const existing = findExistingByPath(all, targetPath);
if (existing) {
const patch = { value: item.value };
if (item.exists !== undefined) {
patch.exists = item.exists !== false;
}
await client.updateConfig(project.id, existing.id, patch);
updated++;
} else {
const { key, idc, environment, group } = decomposeByDims(project, targetPath, item);
await client.createConfig(project.id, {
key,
value: item.value,
type: item.type || "string",
description: item.description || "",
required: item.required === true,
defaultValue: item.defaultValue,
purpose: item.purpose,
valueRange: item.valueRange,
idc,
environment,
group,
});
created++;
}
}
$logger.info("导入完成: 新增 " + created + ",更新 " + updated + (skipped ? ",跳过 " + skipped : "") + " 条");
}
module.exports = {
init: function () {
return this;
},
regTo: function (parser) {
parser.addCmdLine("registry [sub] [project-path] [value]", "Registry 配置管理; sub: list/config/export/import", async function (cli) {
const sub = cli.getParamValue("sub");
const spec = cli.getParamValue("project-path");
const value = cli.getParamValue("value");
switch (sub) {
case "list": {
const client = await getClient();
await cmdList(client);
break;
}
case "config": {
const client = await getClient();
await cmdConfig(client, spec, value);
break;
}
case "export": {
const client = await getClient();
await cmdExport(client, spec, value);
break;
}
case "import": {
const client = await getClient();
await cmdImport(client, spec, value);
break;
}
default:
$logger.info(USAGE);
}
});
},
};
+341 -208
View File
@@ -1,228 +1,361 @@
/**
* remote 模块用来处理远程仓库相关事务
*/
const path = require("path")
const fs = require("fs")
const JSON5 = require('json5')
const { pipeline, Transform} = require("node:stream/promises");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { pipeline } = require("stream/promises");
const JSON5 = require("json5");
function now() {
return new Date().toISOString();
}
function normalizeRemotePath(value) {
if (!value || value === "/") {
return "/";
}
return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$/g, "") || "/";
}
function normalizeGroup(group) {
return normalizeRemotePath(group == null ? "/modules" : group);
}
function joinRemote(...items) {
return normalizeRemotePath(items.filter((item) => item !== undefined && item !== null && item !== "").join("/"));
}
function normalizeOs(value) {
const name = String(value || os.platform()).toLowerCase();
if (["windows_nt", "win32", "windows"].includes(name)) {
return "windows";
}
if (["macos", "mac", "darwin"].includes(name)) {
return "darwin";
}
if (["linux", "alpine"].includes(name)) {
return "linux";
}
return name;
}
function normalizeArch(value) {
const name = String(value || os.arch()).toLowerCase();
if (["x64", "x86-64", "x86_64", "amd64"].includes(name)) {
return "x86_64";
}
if (["arm64", "aarch64"].includes(name)) {
return "aarch64";
}
if (["ia32", "i386", "x86"].includes(name)) {
return "i386";
}
return name;
}
function localModuleRoot(projectId) {
return path.resolve($sConfig.getLocalStorePath(), "modules", projectId);
}
function readJsonFile(filePath, fallback) {
try {
if (fs.existsSync(filePath)) {
return JSON5.parse(fs.readFileSync(filePath, "utf-8"));
}
} catch (e) {
$logger.warn(`读取 JSON 失败 ${filePath}: ${e.message}`);
}
return fallback;
}
function writeJsonFile(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 4), "utf-8");
}
function safeRemove(target) {
fs.rmSync(target, { recursive: true, force: true });
}
function copyRecursive(from, to) {
const stat = fs.statSync(from);
if (stat.isDirectory()) {
fs.mkdirSync(to, { recursive: true });
for (const item of fs.readdirSync(from)) {
copyRecursive(path.join(from, item), path.join(to, item));
}
} else {
fs.mkdirSync(path.dirname(to), { recursive: true });
fs.copyFileSync(from, to);
}
}
module.exports = class Remote {
constructor() {
this.init()
this.func_publish = require("./publish")
this.func_install = require("./install")
this.func_publish = require("./publish");
this.func_install = require("./install");
this.root = "/";
}
async init() {
this.webdav = await import("webdav");
const webdav = await import("webdav");
this.webdav = webdav.createClient ? webdav : webdav.default;
const uConfig = await $context.get("uConfig");
if ( uConfig.get("remoteServer") ){
let server = uConfig.get("remoteServer");
if ( server.startsWith("webdav") ) {
server = "http" + server.substring(6)
}
if ( uConfig.get("remoteAuth") ) {
let up = uConfig.get("remoteAuth");
up = up.split(":", 2)
this.client = this.webdav.createClient(
server,
{
username: up[0],
password: up[1]
}
);
} else {
this.client = this.webdav.createClient(
server
);
}
let server = uConfig.get("remoteServer");
if (!server) {
return;
}
if (server.startsWith("webdav")) {
server = "http" + server.substring(6);
}
const options = {};
const auth = uConfig.get("remoteAuth");
if (auth) {
const [username, password] = auth.split(":", 2);
options.username = username;
options.password = password;
}
this.client = this.webdav.createClient(server, options);
}
async check() {
if ( ! this.webdav ) {
return false
}
if ( ! this.client ) {
return false
if (!this.client) {
$logger.info("remote client unavailable");
return false;
}
try {
// Get directory contents
const directoryItems = await this.client.getDirectoryContents("/");
// 是否包含 index.json ?
// 不包含尝试创建
// 包含了尝试下载
const indexJsonFile = this.getIndexJson( directoryItems );
if ( indexJsonFile ) {
// 包含了尝试下载
// 下载有缓存
this.download(indexJsonFile)
} else {
// 不包含尝试创建
await this.affireRemoteStore()
}
return true
}catch ( e ) {
$logger.error("remote check 过程中出错", e)
return false
}
}
/**
* 先对比时间和长度,有不同才会真正下载,否则使用本地缓存。
* 本地缓存路径即本地仓库路径
* @param rFile
*/
download(rFile){
const lFilePath = path.resolve($sConfig.getLocalStorePath(), rFile.filename);
if ( fs.existsSync( lFilePath ) ) {
const lFileInfo = fs.statSync(lFilePath);
if ( lFileInfo.size === rFile.length
&& lFileInfo.mtime === rFile.lastmod
) {
// 使用缓存
$logger.debug("use cache")
return true
}
}
// 下载并覆盖
// 不知道是同步还是异步
this.wDownload( rFile.filename, lFilePath );
}
async affireRemoteStore(){
// Get directory contents
const directoryItems = await this.client.getDirectoryContents("/");
// 是否包含 index.json ?
// 不包含尝试创建
// 包含了尝试下载
const indexJsonFile = this.getIndexJson( directoryItems );
if ( ! indexJsonFile ) {
// 创建 index
await this.client.putFileContents("/index.json", JSON5.stringify({
modules : [],
tmpls : [],
lists : []
}, null, 4) , { overwrite: false });
}
const modulesFolder = this.getFileInfoByName(directoryItems, "modules")
if ( ! modulesFolder ) {
// 创建文件夹
await this.client.createDirectory("/modules");
}
const tmplsFolder = this.getFileInfoByName(directoryItems, "tmpls")
if ( ! tmplsFolder ) {
// 创建文件夹
await this.client.createDirectory("/tmpls");
}
const listsFolder = this.getFileInfoByName(directoryItems, "lists")
if ( ! listsFolder ) {
// 创建文件夹
await this.client.createDirectory("/lists");
}
}
getIndexJson(items){
return this.getFileInfoByName( items, "index.json" );
}
getFileInfoByName(items, name){
if ( ! items || items.length < 1 ) {
await this.ensureDir("/");
await this.client.getDirectoryContents("/");
return true;
} catch (e) {
$logger.error("remote check 过程中出错 " + e.message);
return false;
}
for ( let item of items ) {
if ( item.basename === name ) {
return item;
}
async publish(localFile, remotePath) {
return await this.func_publish(this, localFile, remotePath);
}
async install(id) {
return await this.func_install(this, id);
}
async list(projectId) {
const parsed = this.parseProjectVersion(projectId);
const state = this.getInstallState(parsed.projectId);
for (const version of state.installed_versions) {
const current = version === state.current_version ? "*" : " ";
$logger.info(`${current} ${version}${current === "*" ? " (current)" : ""}`);
}
return state.installed_versions;
}
async use(projectVersion) {
const { projectId, version } = this.parseProjectVersion(projectVersion);
return this.activate(projectId, version, "switch");
}
async rollback(projectId) {
const parsed = this.parseProjectVersion(projectId);
const state = this.getInstallState(parsed.projectId);
const history = [...state.history].reverse();
const current = state.current_version;
const previous = history.find((item) => item.version && item.version !== current && state.installed_versions.includes(item.version));
if (!previous) {
throw new Error(`没有可回滚版本 ${parsed.projectId}`);
}
return this.activate(parsed.projectId, previous.version, "rollback");
}
async remove(projectVersion) {
const { projectId, version } = this.parseProjectVersion(projectVersion);
const root = localModuleRoot(projectId);
const state = this.getInstallState(projectId);
const targetVersion = version || state.current_version;
if (!targetVersion) {
return false;
}
if (state.current_version === targetVersion) {
throw new Error(`不能删除当前激活版本 ${projectId}@${targetVersion}`);
}
safeRemove(path.resolve(root, "versions", targetVersion));
state.installed_versions = state.installed_versions.filter((item) => item !== targetVersion);
this.pushHistory(state, "uninstall", targetVersion);
this.saveInstallState(projectId, state);
return true;
}
parseProjectVersion(value) {
const text = String(value || "kit");
const index = text.lastIndexOf("@");
const target = index <= 0 ? text : text.substring(0, index);
const version = index <= 0 ? undefined : text.substring(index + 1);
if (target.startsWith("/")) {
const normalized = normalizeRemotePath(target);
const projectId = path.posix.basename(normalized);
const group = normalizeGroup(path.posix.dirname(normalized));
return { projectId, version, group };
}
return { projectId: target, version, group: "/modules" };
}
getLocalModuleRoot(projectId) {
const root = localModuleRoot(projectId);
fs.mkdirSync(root, { recursive: true });
return root;
}
getInstallState(projectId) {
const statePath = path.resolve(this.getLocalModuleRoot(projectId), ".meta", "install.json");
const state = readJsonFile(statePath, {
project_id: projectId,
installed_versions: [],
current_version: undefined,
last_updated: undefined,
history: [],
options: {}
});
state.project_id = state.project_id || projectId;
state.installed_versions = state.installed_versions || [];
state.history = state.history || [];
state.options = state.options || {};
return state;
}
saveInstallState(projectId, state) {
state.last_updated = now();
writeJsonFile(path.resolve(this.getLocalModuleRoot(projectId), ".meta", "install.json"), state);
}
pushHistory(state, action, version, from) {
state.history.push({ timestamp: now(), action, version, from });
}
async activate(projectId, version, action = "switch") {
const root = this.getLocalModuleRoot(projectId);
const versionDir = path.resolve(root, "versions", version);
if (!fs.existsSync(versionDir)) {
throw new Error(`本地版本不存在 ${projectId}@${version}`);
}
const current = path.resolve(root, "current");
safeRemove(current);
try {
fs.symlinkSync(path.relative(root, versionDir), current, "junction");
} catch (e) {
copyRecursive(versionDir, current);
}
const state = this.getInstallState(projectId);
const from = state.current_version;
state.current_version = version;
if (!state.installed_versions.includes(version)) {
state.installed_versions.push(version);
}
this.pushHistory(state, action, version, from);
this.saveInstallState(projectId, state);
fs.writeFileSync(path.resolve(root, "version"), version, "utf-8");
return version;
}
getPlatformCandidates() {
return [
{ os: normalizeOs(os.platform()), arch: normalizeArch(os.arch()) },
{ os: "any", arch: "any" }
];
}
getRemoteProjectRoot(projectId, group = "/modules") {
return joinRemote(normalizeGroup(group), projectId);
}
async ensureDir(remotePath) {
const target = normalizeRemotePath(remotePath);
if (target === "/") {
return;
}
if (await this.client.exists(target)) {
return;
}
await this.ensureDir(path.posix.dirname(target));
await this.client.createDirectory(target);
}
async putJson(remotePath, data) {
await this.ensureDir(path.posix.dirname(remotePath));
await this.client.putFileContents(normalizeRemotePath(remotePath), JSON.stringify(data, null, 4), { overwrite: true });
}
async getJson(remotePath, fallback) {
try {
const text = await this.client.getFileContents(normalizeRemotePath(remotePath), { format: "text" });
return JSON5.parse(text);
} catch (e) {
return fallback;
}
}
async wUpload(from, to) {
await this.ensureDir(path.posix.dirname(to));
const rs = fs.createReadStream(from);
const ws = this.client.createWriteStream(normalizeRemotePath(to));
await pipeline(rs, ws);
}
async wDownload(from, to) {
fs.mkdirSync(path.dirname(to), { recursive: true });
const rs = this.client.createReadStream(normalizeRemotePath(from));
const ws = fs.createWriteStream(to);
await pipeline(rs, ws);
}
async wMkdir(folder) {
return this.ensureDir(folder);
}
async wGetModuleInfo(projectId, group = "/modules") {
return this.getProjectMetadata(projectId, group);
}
async getProjectMetadata(projectId, group = "/modules") {
const metadata = await this.getJson(joinRemote(this.getRemoteProjectRoot(projectId, group), "metadata.json"), undefined);
if (metadata) {
metadata.group = normalizeGroup(metadata.group == null ? group : metadata.group);
return metadata;
}
throw new Error(`远程模块不存在 ${normalizeGroup(group)}/${projectId}`);
}
async getVersionMetadata(projectId, version, group = "/modules") {
const metadata = await this.getJson(joinRemote(this.getRemoteProjectRoot(projectId, group), version, "version.json"), undefined);
if (metadata) {
return metadata;
}
throw new Error(`远程版本不存在 ${normalizeGroup(group)}/${projectId}@${version}`);
}
async downloadArtifact(projectId, version, artifact, versionDir, group = "/modules") {
const remotePath = joinRemote(this.getRemoteProjectRoot(projectId, group), version, artifact.path);
const localPath = path.resolve(versionDir, artifact.path);
await this.wDownload(remotePath, localPath);
return localPath;
}
getMatchingArtifacts(versionMetadata) {
const artifacts = versionMetadata.artifacts || [];
const candidates = this.getPlatformCandidates();
for (const candidate of candidates) {
const prefix = `${candidate.os}/${candidate.arch}/`;
const matched = artifacts.filter((item) => String(item.path).replace(/\\/g, "/").startsWith(prefix));
if (matched.length > 0) {
return matched;
}
}
return false;
return [];
}
async publish(){
return this.func_publish( this )
}
async install(id){
return this.func_install( this, id )
}
async wUpload(from, to){
const totalSize = fs.statSync(from).size;
const rs = fs.createReadStream( from );
let wSize = 0;
let rSize = 0;
rs.on("data", (r)=>{
rSize += r.length;
// console.log("r", rSize * 100 / totalSize)
})
const ws = this.client.createWriteStream(to)
ws.on("data", (r)=>{
wSize += r.length;
// console.log("w", wSize * 100 / totalSize)
})
return pipeline( rs, ws);
}
async wDownload(from, to){
$logger.info("download", from, to )
const stat = await this.client.stat(from);
const totalSize = stat.size;
let wSize = 0;
let rSize = 0;
const rs = this.client.createReadStream(from);
rs.on("data", (r)=>{
rSize += r.length;
// console.log("r", rSize * 100 / totalSize)
})
const ws = fs.createWriteStream(to);
ws.on("data", (r)=>{
wSize += r.length;
// console.log("w", wSize * 100 / totalSize)
})
return pipeline(
rs, ws
)
}
async wMkdir(folder){
const folderfather = path.dirname( folder );
if ( ! await this.client.exists( folderfather ) ) {
// await remote.client.createDirectory( folderfather )
await this.wMkdir( folderfather )
}
if ( await this.client.exists( folder ) ) {
return
} else {
await this.client.createDirectory( folder )
}
}
async wGetModuleInfo(id){
let meta = await this.client.getFileContents("/modules/" + id + "/meta.json", { format: "text" });
let metaJson = JSON5.parse( meta );
return metaJson
}
}
+71 -40
View File
@@ -1,53 +1,84 @@
const os = require('os');
const fs = require("fs");
const osutil = require("../util/osutil")
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
function matchDistName(cur, dists){
const list = []
for (let dist of dists) {
if ( cur.curOS === dist.os && cur.curPlatform === dist.platform) {
list.push( dist )
}
else if ( cur.curOS === dist.os && "cross" === dist.platform) {
list.unshift( dist )
}
else if ( "cross" === dist.os && cur.curPlatform === dist.platform) {
list.unshift( dist )
}
}
return list
function now() {
return new Date().toISOString();
}
function verifyChecksum(filePath, checksum) {
if (!checksum) {
return;
}
const [alg, expected] = checksum.split(":", 2);
if (!alg || !expected) {
return;
}
const crypto = require("crypto");
const actual = crypto.createHash(alg).update(fs.readFileSync(filePath)).digest("hex");
if (actual !== expected) {
throw new Error(`文件校验失败 ${filePath}`);
}
}
module.exports = async function install(remote, id){
const cur = osutil.cur();
if ( ! id ) {
id = "kit"
function unpackArtifact(artifact, localPath, versionDir) {
if (artifact.type !== "archive" || !artifact.unpack) {
return;
}
// 获取本地版本
const localInfo = osutil.choise([
require("../windows/local").getModuleInfo,
require("../linux/local").getModuleInfo,
])( id )
const extractDir = path.resolve(path.dirname(localPath), "_extracted");
fs.mkdirSync(extractDir, { recursive: true });
const format = artifact.archive_format || "zip";
if (format === "zip") {
if (process.platform === "win32") {
const source = localPath.replace(/'/g, "''");
const target = extractDir.replace(/'/g, "''");
execFileSync("powershell.exe", ["-NoProfile", "-Command", `Expand-Archive -Path '${source}' -DestinationPath '${target}' -Force`], { stdio: "inherit" });
} else {
execFileSync("unzip", ["-o", localPath, "-d", extractDir], { stdio: "inherit" });
}
} else if (["tar", "tar.gz", "tgz", "tar.xz"].includes(format)) {
execFileSync("tar", ["-xf", localPath, "-C", extractDir], { stdio: "inherit" });
}
$logger.info("local version", localInfo.version)
artifact.extracted_path = path.relative(versionDir, extractDir).replace(/\\/g, "/");
if (artifact.entry_point) {
artifact.extracted_entry_point = path.posix.join(artifact.extracted_path, artifact.entry_point.replace(/\\/g, "/"));
}
}
// 确认远程版本文件是否存在
const remoteInfo = await remote.wGetModuleInfo(id)
$logger.info("remote latest version", remoteInfo.latest.version)
module.exports = async function install(remote, id) {
const parsed = remote.parseProjectVersion(id || "kit");
const projectId = parsed.projectId;
const group = parsed.group || "/modules";
const project = await remote.getProjectMetadata(projectId, group);
const version = parsed.version || project.version;
if (!version) {
throw new Error(`远程模块没有可安装版本 ${projectId}`);
}
const dists = matchDistName(cur, remoteInfo.latest.dist)
const dist = dists.pop()
const versionMeta = await remote.getVersionMetadata(projectId, version, project.group || group);
const artifacts = remote.getMatchingArtifacts(versionMeta);
if (artifacts.length < 1) {
throw new Error(`没有匹配当前平台的文件 ${projectId}@${version}`);
}
await remote.wDownload(
"/modules/" + id + "/" + remoteInfo.latest.version + "/" + cur.curOS + "/" + cur.curPlatform + "/" + dist.filename,
localInfo.homeDir + "/" + dist.filename,
)
const root = remote.getLocalModuleRoot(projectId);
const versionDir = path.resolve(root, "versions", version);
fs.mkdirSync(versionDir, { recursive: true });
fs.writeFileSync( localInfo.homeDir + "/version", remoteInfo.latest.version );
for (const artifact of artifacts) {
const localPath = await remote.downloadArtifact(projectId, version, artifact, versionDir, project.group || group);
verifyChecksum(localPath, artifact.checksum || (versionMeta.checksums && versionMeta.checksums[artifact.path]));
unpackArtifact(artifact, localPath, versionDir);
}
$logger.info("当前版本", remoteInfo.latest.version )
const localVersionMeta = Object.assign({}, versionMeta, { installed_at: now(), artifacts });
fs.writeFileSync(path.resolve(versionDir, ".version.json"), JSON.stringify(localVersionMeta, null, 4), "utf-8");
}
const state = remote.getInstallState(projectId);
const action = state.current_version ? "switch" : "install";
await remote.activate(projectId, version, action);
$logger.info(`当前版本 ${projectId}@${version}`);
return version;
};
+200 -65
View File
@@ -1,79 +1,214 @@
const path = require("path")
const fs = require("fs")
const JSON5 = require('json5')
const { pipeline } = require('node:stream/promises');
const objutil = require("../util/objutil")
module.exports = async function publish(remote, meta){
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const JSON5 = require("json5");
let curPath = process.cwd();
const metaFilePath = path.resolve( curPath, "meta.json" );
if ( ! fs.existsSync( metaFilePath ) ) {
$logger.error("当前路径不包含 meta.json")
return
function now() {
return new Date().toISOString();
}
function normalizeRemotePath(value) {
if (!value || value === "/") {
return "/";
}
return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$/g, "") || "/";
}
function normalizeGroup(group) {
return normalizeRemotePath(group == null ? "/modules" : group);
}
function joinRemote(...items) {
return normalizeRemotePath(items.filter((item) => item !== undefined && item !== null && item !== "").join("/"));
}
function normalizeOs(value) {
const name = String(value || "any").toLowerCase();
if (["windows_nt", "win32", "windows"].includes(name)) {
return "windows";
}
if (["macos", "mac", "darwin"].includes(name)) {
return "darwin";
}
if (["linux", "alpine"].includes(name)) {
return "linux";
}
if (["cross", "all", "any"].includes(name)) {
return "any";
}
return name;
}
function normalizeArch(value) {
const name = String(value || "any").toLowerCase();
if (["x64", "x86-64", "x86_64", "amd64"].includes(name)) {
return "x86_64";
}
if (["arm64", "aarch64"].includes(name)) {
return "aarch64";
}
if (["ia32", "i386", "x86"].includes(name)) {
return "i386";
}
if (["cross", "all", "any"].includes(name)) {
return "any";
}
return name;
}
function sha256(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
function guessArtifactType(filePath) {
const ext = path.extname(filePath).toLowerCase();
if ([".zip", ".tgz", ".gz", ".xz", ".tar"].includes(ext)) {
return "archive";
}
return "executable";
}
function archiveFormat(filePath) {
const name = path.basename(filePath).toLowerCase();
if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
return "tar.gz";
}
if (name.endsWith(".tar.xz")) {
return "tar.xz";
}
if (name.endsWith(".zip")) {
return "zip";
}
if (name.endsWith(".tar")) {
return "tar";
}
return undefined;
}
function readLocalMeta(localFile) {
const base = localFile ? path.resolve(localFile) : process.cwd();
const metaFile = fs.statSync(base).isDirectory() ? path.resolve(base, "meta.json") : path.resolve(process.cwd(), "meta.json");
if (fs.existsSync(metaFile)) {
return JSON5.parse(fs.readFileSync(metaFile, "utf-8"));
}
if (!localFile) {
throw new Error("当前路径不包含 meta.json");
}
return {
id: path.basename(localFile, path.extname(localFile)).toLowerCase(),
group: "/modules",
version: "",
desc: "",
dist: [{ path: localFile, os: "any", platform: "any", filename: path.basename(localFile) }]
};
}
function buildArtifacts(localMeta) {
const dists = localMeta.dist || localMeta.artifacts || [];
return dists.map((dist) => {
const localPath = path.resolve(dist.path || dist.localPath || dist.file || dist.filename);
const osName = normalizeOs(dist.os);
const archName = normalizeArch(dist.arch || dist.platform);
const filename = dist.filename || path.basename(localPath);
const artifactPath = dist.remotePath || `${osName}/${archName}/${filename}`;
const type = dist.type || guessArtifactType(localPath);
const artifact = {
path: artifactPath.replace(/\\/g, "/"),
type,
checksum: `sha256:${sha256(localPath)}`,
localPath
};
if (type === "archive") {
artifact.archive_format = dist.archive_format || archiveFormat(localPath);
artifact.entry_point = dist.entry_point;
artifact.unpack = Boolean(dist.unpack);
}
return artifact;
});
}
const metaFile = fs.readFileSync(metaFilePath);
function bumpTailVersion(version) {
const text = String(version || "").trim();
if (!text) {
return "1.0";
}
const match = text.match(/^(.*?)(\d+)$/);
if (!match) {
return `${text}.1`;
}
return `${match[1]}${Number(match[2]) + 1}`;
}
const localMeta = JSON5.parse( metaFile );
// 更新版本
const date = new Date();
localMeta.version = `${localMeta.version}.${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, '0')}${date.getDate().toString().padStart(2, '0')}${date.getHours().toString().padStart(2, '0')}${date.getMinutes().toString().padStart(2, '0')}${date.getSeconds().toString().padStart(2, '0')}`;
// 确认目录
// const directoryItems = await remote.getDirectoryContents("/modules");
// const modulesFolder = remote.getFileInfoByName(directoryItems, localMeta.id)
if ( ! await remote.client.exists("/modules/" + localMeta.id) ) {
// 创建文件夹
await remote.client.createDirectory("/modules/" + localMeta.id);
async function resolveVersion(remote, projectRoot, localMeta) {
const explicitVersion = String(localMeta.version || "").trim();
if (explicitVersion) {
if (await remote.client.exists(joinRemote(projectRoot, explicitVersion))) {
throw new Error(`远程版本已存在 ${projectRoot}@${explicitVersion}`);
}
return explicitVersion;
}
// 先更新文件
for (let dist of localMeta.dist ) {
const metadata = await remote.getJson(joinRemote(projectRoot, "metadata.json"), undefined);
if (metadata && metadata.version) {
return bumpTailVersion(metadata.version);
}
return "1.0";
}
const folder = "/modules/" + localMeta.id + "/" + localMeta.version + "/" + dist.os + "/" + dist.platform
await remote.wMkdir( folder );
module.exports = async function publish(remote, localFile, remotePath) {
if (remotePath) {
const target = path.posix.resolve("/", remotePath.replace(/\\/g, "/"));
await remote.wUpload(localFile, path.posix.join(target, path.basename(localFile)));
$logger.info("文件上传成功");
return;
}
$logger.info("upload dist", dist.path, fs.existsSync(dist.path))
const localMeta = readLocalMeta(localFile);
const projectId = localMeta.project_id || localMeta.id || localMeta.name;
if (!projectId) {
throw new Error("meta.json 缺少 project_id/id/name");
}
await remote.wUpload( dist.path , folder + "/" + dist.filename );
const group = normalizeGroup(localMeta.group);
const projectRoot = remote.getRemoteProjectRoot ? remote.getRemoteProjectRoot(projectId, group) : joinRemote(group, projectId);
const version = await resolveVersion(remote, projectRoot, localMeta);
const artifacts = buildArtifacts(localMeta);
await remote.ensureDir(joinRemote(projectRoot, version));
}
const versionMeta = {
version,
release_date: localMeta.release_date || now(),
official_download_url: localMeta.official_download_url || "",
artifacts: artifacts.map(({ localPath, ...artifact }) => artifact),
checksums: Object.fromEntries(artifacts.map((artifact) => [artifact.path, artifact.checksum]))
};
// remote meta
let remoteMeta = {
"versions" : []
}
try {
const remoteMetaStr = await remote.client.getFileContents("/modules/" + localMeta.id + "/meta.json", { format: "text" });
remoteMeta = JSON5.parse( remoteMetaStr )
} catch (e) {
for (const artifact of artifacts) {
await remote.wUpload(artifact.localPath, joinRemote(projectRoot, version, artifact.path));
}
await remote.putJson(joinRemote(projectRoot, version, "version.json"), versionMeta);
}
const metadata = await remote.getJson(joinRemote(projectRoot, "metadata.json"), {
project_id: projectId,
group,
official_url: localMeta.official_url || localMeta.vcs || "",
description: localMeta.description || localMeta.desc || "",
version,
fetch_rules: localMeta.fetch_rules || [],
extra: {}
});
remoteMeta.id = localMeta.id;
remoteMeta.name = localMeta.name;
remoteMeta.desc = localMeta.desc;
remoteMeta.type = localMeta.type;
remoteMeta.vcs = localMeta.vcs;
// 废弃
// remoteMeta.platform = "cross" === localMeta.platform ? ["X86_64", "ARM64"] : localMeta.platform;
// remoteMeta.os = "cross" === localMeta.os ? ["Windows", "Linux", "Alpine"] : localMeta.os;
const dist = objutil.copy(localMeta.dist)
for (let d of dist) {
delete dist.path
}
remoteMeta.latest = {
version : localMeta.version,
dist
}
remoteMeta.versions.push(remoteMeta.latest)
if ( remoteMeta.versions.length >= 20 ) {
remoteMeta.versions = remoteMeta.versions.slice(0, 20);
}
metadata.project_id = projectId;
metadata.group = group;
metadata.official_url = metadata.official_url || localMeta.official_url || localMeta.vcs || "";
metadata.description = localMeta.description || localMeta.desc || metadata.description || "";
metadata.version = version;
delete metadata.versions;
delete metadata.latest_version;
metadata.extra = Object.assign({}, metadata.extra || {}, localMeta.extra || {});
await remote.client.putFileContents("/modules/" + localMeta.id + "/meta.json", JSON5.stringify(remoteMeta, null, 4) , { overwrite: true });
$logger.info("发布成功")
}
await remote.putJson(joinRemote(projectRoot, "metadata.json"), metadata);
$logger.info(`文件上传成功 ${group}/${projectId}@${version}`);
};
+6
View File
@@ -23,5 +23,11 @@ module.exports = {
$logger.debug(`execSync ${cmd}`)
const output = execSync(cmd);
return output.toString()
},
callSync(cmd){
$logger.debug(`callSync ${cmd}`)
const output = execSync(cmd);
console.log(output.toString())
}
}
+155 -130
View File
@@ -1,3 +1,21 @@
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return "0 B"; // 处理 0 字节的情况
const units = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
let unitIndex = 0;
// 循环除以 1024,直到字节数小于 1024 或单位用完
while (bytes >= 1024 && unitIndex < units.length - 1) {
bytes /= 1024;
unitIndex++;
}
// 保留小数位数,并移除末尾多余的零(如 2.0 → 2)
const formattedNumber = bytes.toFixed(decimals).replace(/\.0+$/, "");
return `${formattedNumber} ${units[unitIndex]}`;
}
var codePoint2utf8 = function (code) {
var bin = parseInt(code).toString(2)
var ln = 0;
@@ -209,135 +227,6 @@ function base64ArrayBuffer(arrayBuffer) {
return base64
}
(function() {
var base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
base64DecodeChars = new Array(( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 62, ( - 1), ( - 1), ( - 1), 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1));
this.base64encode = function(e) {
var r, a, c, h, o, t;
for (c = e.length, a = 0, r = ''; a < c;) {
if (h = 255 & e.charCodeAt(a++), a == c) {
r += base64EncodeChars.charAt(h >> 2),
r += base64EncodeChars.charAt((3 & h) << 4),
r += '==';
break
}
if (o = e.charCodeAt(a++), a == c) {
r += base64EncodeChars.charAt(h >> 2),
r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
r += base64EncodeChars.charAt((15 & o) << 2),
r += '=';
break
}
t = e.charCodeAt(a++),
r += base64EncodeChars.charAt(h >> 2),
r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
r += base64EncodeChars.charAt(63 & t)
}
return r
}
this.base64decode = function(e) {
var r, a, c, h, o, t, d;
for (t = e.length, o = 0, d = ''; o < t;) {
do r = base64DecodeChars[255 & e.charCodeAt(o++)];
while (o < t && r == -1);
if (r == -1) break;
do a = base64DecodeChars[255 & e.charCodeAt(o++)];
while (o < t && a == -1);
if (a == -1) break;
d += String.fromCharCode(r << 2 | (48 & a) >> 4);
do {
if (c = 255 & e.charCodeAt(o++), 61 == c) return d;
c = base64DecodeChars[c]
} while ( o < t && c == - 1 );
if (c == -1) break;
d += String.fromCharCode((15 & a) << 4 | (60 & c) >> 2);
do {
if (h = 255 & e.charCodeAt(o++), 61 == h) return d;
h = base64DecodeChars[h]
} while ( o < t && h == - 1 );
if (h == -1) break;
d += String.fromCharCode((3 & c) << 6 | h)
}
return d
}
this.HexToBase64 = function(str) {
return base64encode(String.fromCharCode.apply(null, str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" ")));
}
this.Base64Tohex = function(str) {
for (var i = 0,
bin = base64decode(str.replace(/[ \r\n]+$/, "")), hex = []; i < bin.length; ++i) {
var tmp = bin.charCodeAt(i).toString(16);
if (tmp.length === 1) tmp = "0" + tmp;
hex[hex.length] = tmp;
}
return hex.join("");
},
this.toBytes = function(str) {
for (var bytes = [], c = 0; c < str.length; c += 2)
bytes.push(parseInt(str.substr(c, 2), 16));
return bytes;
}
}) ();
//hexToBase64 Base64Tohex base64decode base64encode
var bytesToString = function(bytes){
return hexToString(bytesToHex(bytes));
}
var bytesToBase64 = function(bytes){
return base64ArrayBuffer(bytes);
}
// Convert a byte array to a hex string
var bytesToHex = function(bytes) {
for (var hex = [], i = 0; i < bytes.length; i++) {
hex.push((bytes[i] >>> 4).toString(16));
hex.push((bytes[i] & 0xF).toString(16));
}
return hex.join("");
}
var stringToBase64 = function(str){
return base64encode(str);
}
var stringToBytes = function(str){
return hexToBytes(stringToHex(str));
}
// Convert a ASCII string to a hex string
var stringToHex = function(str) {
return str.split("").map(function(c) {
return ("0" + c.charCodeAt(0).toString(16)).slice(-2);
}).join("");
}
var hexToBytes= function(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
// Convert a hex string to a ASCII string
var hexToString = function(hexStr) {
var hex = hexStr.toString();//force conversion
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}
var hexToBase64 = function(hexStr){
return HexToBase64(hexStr);
}
var base64ToString = function(base64str){
return base64decode(base64str);
}
const Base64 = {
@@ -434,6 +323,138 @@ const Base64 = {
}
}
// (function() {
// var base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
// base64DecodeChars = new Array(( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 62, ( - 1), ( - 1), ( - 1), 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1));
// this.base64encode = function(e) {
// var r, a, c, h, o, t;
// for (c = e.length, a = 0, r = ''; a < c;) {
// if (h = 255 & e.charCodeAt(a++), a == c) {
// r += base64EncodeChars.charAt(h >> 2),
// r += base64EncodeChars.charAt((3 & h) << 4),
// r += '==';
// break
// }
// if (o = e.charCodeAt(a++), a == c) {
// r += base64EncodeChars.charAt(h >> 2),
// r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
// r += base64EncodeChars.charAt((15 & o) << 2),
// r += '=';
// break
// }
// t = e.charCodeAt(a++),
// r += base64EncodeChars.charAt(h >> 2),
// r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
// r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
// r += base64EncodeChars.charAt(63 & t)
// }
// return r
// }
// this.base64decode = function(e) {
// var r, a, c, h, o, t, d;
// for (t = e.length, o = 0, d = ''; o < t;) {
// do r = base64DecodeChars[255 & e.charCodeAt(o++)];
// while (o < t && r == -1);
// if (r == -1) break;
// do a = base64DecodeChars[255 & e.charCodeAt(o++)];
// while (o < t && a == -1);
// if (a == -1) break;
// d += String.fromCharCode(r << 2 | (48 & a) >> 4);
// do {
// if (c = 255 & e.charCodeAt(o++), 61 == c) return d;
// c = base64DecodeChars[c]
// } while ( o < t && c == - 1 );
// if (c == -1) break;
// d += String.fromCharCode((15 & a) << 4 | (60 & c) >> 2);
// do {
// if (h = 255 & e.charCodeAt(o++), 61 == h) return d;
// h = base64DecodeChars[h]
// } while ( o < t && h == - 1 );
// if (h == -1) break;
// d += String.fromCharCode((3 & c) << 6 | h)
// }
// return d
// }
// this.HexToBase64 = function(str) {
// return base64encode(String.fromCharCode.apply(null, str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" ")));
// }
// this.Base64Tohex = function(str) {
// for (var i = 0,
// bin = base64decode(str.replace(/[ \r\n]+$/, "")), hex = []; i < bin.length; ++i) {
// var tmp = bin.charCodeAt(i).toString(16);
// if (tmp.length === 1) tmp = "0" + tmp;
// hex[hex.length] = tmp;
// }
// return hex.join("");
// },
// this.toBytes = function(str) {
// for (var bytes = [], c = 0; c < str.length; c += 2)
// bytes.push(parseInt(str.substr(c, 2), 16));
// return bytes;
// }
//
// }) ();
//hexToBase64 Base64Tohex base64decode base64encode
var bytesToString = function(bytes){
return hexToString(bytesToHex(bytes));
}
var bytesToBase64 = function(bytes){
return base64ArrayBuffer(bytes);
}
// Convert a byte array to a hex string
var bytesToHex = function(bytes) {
for (var hex = [], i = 0; i < bytes.length; i++) {
hex.push((bytes[i] >>> 4).toString(16));
hex.push((bytes[i] & 0xF).toString(16));
}
return hex.join("");
}
var stringToBase64 = function(str){
return base64encode(str);
}
var stringToBytes = function(str){
return hexToBytes(stringToHex(str));
}
// Convert a ASCII string to a hex string
var stringToHex = function(str) {
return str.split("").map(function(c) {
return ("0" + c.charCodeAt(0).toString(16)).slice(-2);
}).join("");
}
var hexToBytes= function(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
// Convert a hex string to a ASCII string
var hexToString = function(hexStr) {
var hex = hexStr.toString();//force conversion
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}
var hexToBase64 = function(hexStr){
return HexToBase64(hexStr);
}
var base64ToString = function(base64str){
return base64decode(base64str);
}
function encodeUtf8(text) {
const code = encodeURIComponent(text);
const bytes = [];
@@ -546,6 +567,10 @@ if ( module ) {
utf8ToBase64,
base64ToUtf8,
utf8ToHex,
hexToUtf8
hexToUtf8,
formatBytes
}
}
+27
View File
@@ -0,0 +1,27 @@
module.exports = {
// 定义格式化日期的函数
formatDate : function (date){
// 获取年份
const year = date.getFullYear();
// 获取月份,注意月份是从 0 开始的,所以要加 1
const month = String(date.getMonth() + 1).padStart(2, '0');
// 获取日期
const day = String(date.getDate()).padStart(2, '0');
// 获取小时
const hours = String(date.getHours()).padStart(2, '0');
// 获取分钟
const minutes = String(date.getMinutes()).padStart(2, '0');
// 获取秒数
const seconds = String(date.getSeconds()).padStart(2, '0');
// 按照 yyyyMMddHHmmss 的格式拼接字符串
return `${year}${month}${day}${hours}${minutes}${seconds}`;
},
now : function (){
// 获取当前日期和时间
const currentDate = new Date();
// 调用格式化函数
const formattedDate = this.formatDate(currentDate);
return formattedDate
}
}
+50 -50
View File
@@ -1,51 +1,51 @@
const os = require('os');
const fs = require("fs");
const Windows = 0;
const Linux = 1;
module.exports = {
Windows, Linux,
/**
* 根据 os 不同,载入不同的方法
*/
choise : function ( cbs ) {
if (os.type() == 'Windows_NT') {
// windows平台
return cbs && cbs[ Windows ]
}
else if (os.type() == 'Linux') {
// Linux平台
return cbs && cbs[ Linux ]
}
else {
throw { message : "不支持的 OS" }
}
},
cur : function () {
let isLinux = false;
// 操作系统不同,安装方式不同
let curOS = os.type()
if ( curOS === "Windows_NT" ) {
curOS = "Windows"
}
else if ( curOS === "Linux" ) {
isLinux = true;
const fs = require('fs');
if( fs.existsSync('/etc/alpine-release') ){
curOS = "Alpine"
}
}
let curPlatform = os.arch()
if ( curPlatform === "x64" ) {
curPlatform = "X86_64"
} else if ( curPlatform.startsWith("arm") ) {
curPlatform = "ARM64"
}
return { isLinux, curOS, curPlatform }
}
const os = require('os');
const fs = require("fs");
const Windows = "Windows";
const Linux = "Linux";
module.exports = {
Windows, Linux,
/**
* 根据 os 不同,载入不同的方法
*/
choise : function ( cbs ) {
if (os.type() === 'Windows_NT') {
// windows平台
return cbs && cbs[ Windows ]
}
else if (os.type() === 'Linux') {
// Linux平台
return cbs && cbs[ Linux ]
}
else {
throw { message : "不支持的 OS" }
}
},
cur : function () {
let isLinux = false;
// 操作系统不同,安装方式不同
let curOS = os.type()
if ( curOS === "Windows_NT" ) {
curOS = "Windows"
}
else if ( curOS === "Linux" ) {
isLinux = true;
const fs = require('fs');
if( fs.existsSync('/etc/alpine-release') ){
curOS = "Alpine"
}
}
let curPlatform = os.arch()
if ( curPlatform === "x64" ) {
curPlatform = "X86_64"
} else if ( curPlatform.startsWith("arm") ) {
curPlatform = "ARM64"
}
return { isLinux, curOS, curPlatform }
}
}
+121
View File
@@ -0,0 +1,121 @@
const { execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");
function commandExists(command) {
try {
execFileSync("where.exe", [command], { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}
function run(command, args, options = {}) {
return execFileSync(command, args, Object.assign({ stdio: "inherit" }, options));
}
function addToZip(sourceDir, to, password) {
fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true });
if (password) {
if (!commandExists("7z.exe") && !commandExists("7z")) {
throw new Error("Windows password zip requires 7z in PATH");
}
const bin = commandExists("7z.exe") ? "7z.exe" : "7z";
run(bin, ["a", "-tzip", `-p${password}`, "-y", path.resolve(to), "."], { cwd: sourceDir, stdio: "inherit" });
return;
}
const destination = path.resolve(to).replace(/'/g, "''");
const source = path.resolve(sourceDir, "*").replace(/'/g, "''");
const update = fs.existsSync(to) ? " -Update" : "";
const script = `Compress-Archive -Path '${source}' -DestinationPath '${destination}'${update} -Force`;
run("powershell.exe", ["-NoProfile", "-Command", script]);
}
function addToTar(sourceDir, to) {
fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true });
const args = fs.existsSync(to) ? ["-rf", path.resolve(to), "."] : ["-cf", path.resolve(to), "."];
run("tar.exe", args, { cwd: sourceDir, stdio: "inherit" });
}
function zip(sourceDir, to, params = {}) {
if (params.type === "tar") {
addToTar(sourceDir, to);
} else {
addToZip(sourceDir, to, params.password);
}
}
function isRemotePath(value) {
if (typeof value !== "string") {
return false;
}
if (/^[A-Za-z]:[\\/]/.test(value)) {
return false;
}
return /^[^\s:]+:.+/.test(value);
}
function resolveScpPath(value) {
return isRemotePath(value) ? value : path.resolve(value);
}
function appendScpParams(args, params, supportOpenSshOptions) {
if (params.recursive !== false) {
args.push("-r");
}
if (params.port) {
args.push("-P", String(params.port));
}
if (params.key) {
args.push("-i", params.key);
}
if (params.preserve) {
args.push("-p");
}
if (params.compress) {
args.push("-C");
}
if (supportOpenSshOptions && params.strictHostKeyChecking !== undefined) {
args.push("-o", `StrictHostKeyChecking=${params.strictHostKeyChecking ? "yes" : "no"}`);
}
if (supportOpenSshOptions && params.options) {
for (const name of Object.keys(params.options)) {
args.push("-o", `${name}=${params.options[name]}`);
}
}
if (params.args) {
args.push(...(Array.isArray(params.args) ? params.args : [params.args]));
}
}
function scp(from, to, params = {}) {
const source = resolveScpPath(from);
if (!isRemotePath(from) && !fs.existsSync(source)) {
return;
}
if (params.pwd) {
if (!commandExists("pscp.exe") && !commandExists("pscp")) {
throw new Error("Windows scp with pwd requires pscp in PATH");
}
const bin = commandExists("pscp.exe") ? "pscp.exe" : "pscp";
const args = ["-pw", params.pwd];
appendScpParams(args, params, false);
args.push(source, resolveScpPath(to));
run(bin, args);
return;
}
if (!commandExists("scp.exe")) {
throw new Error("Windows scp requires scp.exe in PATH");
}
const args = [];
appendScpParams(args, params, true);
args.push(source, resolveScpPath(to));
run("scp.exe", args);
}
module.exports = { zip, scp };
+6 -1
View File
@@ -1,6 +1,6 @@
async function killport(port) {
async function killport(port, dry) {
$logger.info(`kill process on port ${port}`);
const util = require('util');
const exec = util.promisify(require('child_process').exec);
@@ -23,6 +23,11 @@ async function killport(port) {
$logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${processName};`);
if (dry) {
$logger.info(`dry 模式,未执行清理: pid=${pid}`);
return
}
// Kill 进程
await exec(`taskkill /PID ${pid} /F`)
+138
View File
@@ -0,0 +1,138 @@
const crypto = require('../src/crypto');
const fs = require('fs');
const path = require('path');
// 测试用例
console.log('=== 开始测试 crypto 模块 ===');
// 测试 1: getInfoFromPKCS7 接口(RSA)
try {
console.log('\n1. 测试 getInfoFromPKCS7 接口(RSA)');
// 使用真实的 PKCS7 签名数据进行测试
const mockP7Sign = `-----BEGIN PKCS7-----
MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExDzANBglghkgBZQMEAgEFADA8Bgkq
hkiG9w0BBwGgLwQtVGhpcyBpcyB0aGUgdGVzdCBjb250ZW50IGZvciBQS0NTNyBz
aWduYXR1cmUuoIIB5jCCAeIwggFLoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwNzEZ
MBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTEaMBgGA1UEChMRVGVzdCBPcmdhbml6
YXRpb24wHhcNMjYwNDA0MDAxMDIyWhcNMjcwNDA0MDAxMDIyWjA3MRkwFwYDVQQD
ExB0ZXN0LmV4YW1wbGUuY29tMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1e+05xb8T3Lo2v9MZQen5HYffpnc
yY9mnT/ygvfxZNM9TeYkq4fACiuS99FONnlHFQdlXEPJluIk1NBFUoLpehAbzOSY
Fu0WEjBOVJIg9FIWm7TUojU8yvPsfPGFYTEGEOGAse8AfHw4pl+w5SvxQUF8f2jB
XsRY0MrDyb07xC8CAwEAATANBgkqhkiG9w0BAQsFAAOBgQAOEttx/p3xosIm/8sM
kIaxR2c/fbYoMbBsX+geaiRu+xNOItbtQpGOhW6Ql981c3EwIjyd34KavplofZIr
Slj4jHQFySZaJkegHY+LCAQz9GEg2TMke3MsPJ7nSZS7MUqB2AQt1tLuiXUuRh3V
jF4yiqi6lGVmvPJf+y1GXk47KjGB5TCB4gIBATA8MDcxGTAXBgNVBAMTEHRlc3Qu
ZXhhbXBsZS5jb20xGjAYBgNVBAoTEVRlc3QgT3JnYW5pemF0aW9uAgEBMA0GCWCG
SAFlAwQCAQUAMA0GCSqGSIb3DQEBAQUABIGAMxSC81vzjSiZcJTcSicgCG9879mu
p/bikqdHZPyF8UgABjikXXoVJ9IClFRd5/3wRf8ez+hwnxKR8j1vAsDI7U6dXJtr
wp0aO7Ua6ecKhI5V0F8nUg5CjEK9FR8vNg0Il8U5AXDuan0iYKvguhWkiuJlvQlV
X9ncPP4VRERPw8U=
-----END PKCS7-----`;
const result = crypto.getInfoFromPKCS7(mockP7Sign);
console.log('测试结果:');
console.log(' cert:');
console.log(' subject:', result.cert?.subjectDN);
console.log(' issuer:', result.cert?.issuerDN);
console.log(' serialNumber:', result.cert?.serialNumber);
console.log(' notBefore:', result.cert?.notBefore);
console.log(' notAfter:', result.cert?.notAfter);
console.log(' pem:');
console.log(result.cert?.pem);
console.log(' publicKey:');
console.log(result.publicKey);
console.log(' publicKey (hex):');
console.log(result.publicKeyHex);
console.log(' signature (hex):');
console.log(result.signatureHex);
console.log('✓ getInfoFromPKCS7 接口(RSA)测试通过');
} catch (error) {
console.error('✗ getInfoFromPKCS7 接口(RSA)测试失败:', error.message);
console.error(error.stack);
}
// 测试 2: getInfoFromPKCS7 接口(SM2)
try {
console.log('\n2. 测试 getInfoFromPKCS7 接口(SM2)');
// 使用用户提供的真实 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const result = crypto.getInfoFromPKCS7(sm2P7Base64);
console.log('测试结果:');
console.log(' cert:');
console.log(' subject:', result.cert?.subjectDN);
console.log(' issuer:', result.cert?.issuerDN);
console.log(' serialNumber:', result.cert?.serialNumber);
console.log(' notBefore:', result.cert?.notBefore);
console.log(' notAfter:', result.cert?.notAfter);
console.log(' pem:');
console.log(result.cert?.pem);
console.log(' publicKey:');
console.log(result.publicKey);
console.log(' publicKey (hex):');
console.log(result.publicKeyHex);
console.log(' signature (hex):');
console.log(result.signatureHex);
console.log('✓ getInfoFromPKCS7 接口(SM2)测试通过');
} catch (error) {
console.error('✗ getInfoFromPKCS7 接口(SM2)测试失败:', error.message);
console.error(error.stack);
}
// 测试 3: verifyPKCS7 接口(RSA)
try {
console.log('\n3. 测试 verifyPKCS7 接口(RSA)');
// 使用真实的 PKCS7 签名数据和原始数据进行测试
const mockP7Sign = `-----BEGIN PKCS7-----
MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExDzANBglghkgBZQMEAgEFADA8Bgkq
hkiG9w0BBwGgLwQtVGhpcyBpcyB0aGUgdGVzdCBjb250ZW50IGZvciBQS0NTNyBz
aWduYXR1cmUuoIIB5jCCAeIwggFLoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwNzEZ
MBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTEaMBgGA1UEChMRVGVzdCBPcmdhbml6
YXRpb24wHhcNMjYwNDA0MDAxMDIyWhcNMjcwNDA0MDAxMDIyWjA3MRkwFwYDVQQD
ExB0ZXN0LmV4YW1wbGUuY29tMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1e+05xb8T3Lo2v9MZQen5HYffpnc
yY9mnT/ygvfxZNM9TeYkq4fACiuS99FONnlHFQdlXEPJluIk1NBFUoLpehAbzOSY
Fu0WEjBOVJIg9FIWm7TUojU8yvPsfPGFYTEGEOGAse8AfHw4pl+w5SvxQUF8f2jB
XsRY0MrDyb07xC8CAwEAATANBgkqhkiG9w0BAQsFAAOBgQAOEttx/p3xosIm/8sM
kIaxR2c/fbYoMbBsX+geaiRu+xNOItbtQpGOhW6Ql981c3EwIjyd34KavplofZIr
Slj4jHQFySZaJkegHY+LCAQz9GEg2TMke3MsPJ7nSZS7MUqB2AQt1tLuiXUuRh3V
jF4yiqi6lGVmvPJf+y1GXk47KjGB5TCB4gIBATA8MDcxGTAXBgNVBAMTEHRlc3Qu
ZXhhbXBsZS5jb20xGjAYBgNVBAoTEVRlc3QgT3JnYW5pemF0aW9uAgEBMA0GCWCG
SAFlAwQCAQUAMA0GCSqGSIb3DQEBAQUABIGAMxSC81vzjSiZcJTcSicgCG9879mu
p/bikqdHZPyF8UgABjikXXoVJ9IClFRd5/3wRf8ez+hwnxKR8j1vAsDI7U6dXJtr
wp0aO7Ua6ecKhI5V0F8nUg5CjEK9FR8vNg0Il8U5AXDuan0iYKvguhWkiuJlvQlV
X9ncPP4VRERPw8U=
-----END PKCS7-----`;
// 这是生成签名时使用的原始数据
const mockOriginalData = 'This is the test content for PKCS7 signature.';
const result = crypto.verifyPKCS7(mockP7Sign, mockOriginalData);
console.log('测试结果:', result);
if( !result ) {
throw new Error("验签不通过")
}
console.log('✓ verifyPKCS7 接口(RSA)测试通过');
} catch (error) {
console.error('✗ verifyPKCS7 接口(RSA)测试失败:', error.message);
console.error(error.stack);
}
// 测试 4: verifyPKCS7 接口(SM2)
try {
console.log('\n4. 测试 verifyPKCS7 接口(SM2)');
// 这里应该使用实际的 PKCS7 签名数据和原始数据进行测试
// 由于没有实际数据,这里使用模拟数据
const mockP7Sign = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const mockOriginalData = 'QDJNHHVB_FWQ';
const result = crypto.verifyPKCS7(mockP7Sign, mockOriginalData);
console.log('测试结果:', result);
if( !result ) {
throw new Error("验签不通过")
}
console.log('✓ verifyPKCS7 接口(SM2)测试通过');
} catch (error) {
console.error('✗ verifyPKCS7 接口(SM2)测试失败:', error.message);
}
console.log('\n=== 测试完成 ===');
+60
View File
@@ -0,0 +1,60 @@
const forge = require('node-forge');
const mockP7Sign = `-----BEGIN PKCS7-----
MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExDzANBglghkgBZQMEAgEFADA8Bgkq
hkiG9w0BBwGgLwQtVGhpcyBpcyB0aGUgdGVzdCBjb250ZW50IGZvciBQS0NTNyBz
aWduYXR1cmUuoIIB5jCCAeIwggFLoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwNzEZ
MBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTEaMBgGA1UEChMRVGVzdCBPcmdhbml6
YXRpb24wHhcNMjYwNDA0MDAxMDIyWhcNMjcwNDA0MDAxMDIyWjA3MRkwFwYDVQQD
ExB0ZXN0LmV4YW1wbGUuY29tMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1e+05xb8T3Lo2v9MZQen5HYffpnc
yY9mnT/ygvfxZNM9TeYkq4fACiuS99FONnlHFQdlXEPJluIk1NBFUoLpehAbzOSY
Fu0WEjBOVJIg9FIWm7TUojU8yvPsfPGFYTEGEOGAse8AfHw4pl+w5SvxQUF8f2jB
XsRY0MrDyb07xC8CAwEAATANBgkqhkiG9w0BAQsFAAOBgQAOEttx/p3xosIm/8sM
kIaxR2c/fbYoMbBsX+geaiRu+xNOItbtQpGOhW6Ql981c3EwIjyd34KavplofZIr
Slj4jHQFySZaJkegHY+LCAQz9GEg2TMke3MsPJ7nSZS7MUqB2AQt1tLuiXUuRh3V
jF4yiqi6lGVmvPJf+y1GXk47KjGB5TCB4gIBATA8MDcxGTAXBgNVBAMTEHRlc3Qu
ZXhhbXBsZS5jb20xGjAYBgNVBAoTEVRlc3QgT3JnYW5pemF0aW9uAgEBMA0GCWCG
SAFlAwQCAQUAMA0GCSqGSIb3DQEBAQUABIGAMxSC81vzjSiZcJTcSicgCG9879mu
p/bikqdHZPyF8UgABjikXXoVJ9IClFRd5/3wRf8ez+hwnxKR8j1vAsDI7U6dXJtr
wp0aO7Ua6ecKhI5V0F8nUg5CjEK9FR8vNg0Il8U5AXDuan0iYKvguhWkiuJlvQlV
X9ncPP4VRERPw8U=
-----END PKCS7-----`;
const p7 = forge.pkcs7.messageFromPem(mockP7Sign);
console.log('=== p7.rawCapture ===');
console.log('rawCapture keys:', Object.keys(p7.rawCapture || {}));
if (p7.rawCapture) {
for (const key of Object.keys(p7.rawCapture)) {
const value = p7.rawCapture[key];
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
console.log(`${key}: Buffer/Uint8Array, length=${value.length}`);
console.log(`${key} hex:`, Buffer.from(value).toString('hex').substring(0, 100) + '...');
} else if (typeof value === 'string') {
console.log(`${key}: string, length=${value.length}`);
console.log(`${key} content:`, value.substring(0, 100) + '...');
} else if (value && typeof value === 'object') {
console.log(`${key}: object, keys=${Object.keys(value).join(',')}`);
} else {
console.log(`${key}:`, value);
}
}
}
console.log('\n=== p7.signerInfos ===');
console.log('signerInfos:', p7.signerInfos);
console.log('\n=== p7.signers ===');
console.log('signers:', p7.signers);
console.log('\n=== p7.certificates ===');
if (p7.certificates && p7.certificates.length > 0) {
const cert = p7.certificates[0];
console.log('cert publicKey:', cert.publicKey ? 'exists' : 'null');
if (cert.publicKey) {
console.log('publicKey type:', typeof cert.publicKey);
console.log('publicKey keys:', Object.keys(cert.publicKey));
}
}
+24
View File
@@ -0,0 +1,24 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
console.log('=== 调试签名格式 ===');
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
console.log('签名算法:', signer.sigalg);
console.log('签名值 (signer.sighex):', signer.sighex);
console.log('签名值长度:', signer.sighex.length / 2, '字节');
console.log('签名值开头:', signer.sighex.substring(0, 10));
// 检查是否是 ASN.1 DER 格式
if (signer.sighex.startsWith('30')) {
console.log('签名格式: ASN.1 DER');
} else {
console.log('签名格式: 原始 r|s');
}
}
+25
View File
@@ -0,0 +1,25 @@
const smCrypto = require('sm-crypto');
console.log('=== 调试 sm-crypto API ===');
console.log('sm-crypto 模块:', Object.keys(smCrypto));
console.log('sm2 对象:', Object.keys(smCrypto.sm2));
console.log('sm3 对象:', typeof smCrypto.sm3);
// 测试 SM3
const testData = 'test';
const hash = smCrypto.sm3(testData);
console.log('\nSM3 哈希:', hash);
// 生成密钥对
const keypair = smCrypto.sm2.generateKeyPairHex();
console.log('\n生成的密钥对:');
console.log(' 私钥:', keypair.privateKey.substring(0, 20) + '...');
console.log(' 公钥:', keypair.publicKey.substring(0, 20) + '...');
// 签名
const sig = smCrypto.sm2.doSignature(hash, keypair.privateKey);
console.log('\n签名:', sig);
// 验证签名
const verifyResult = smCrypto.sm2.doVerifySignature(hash, sig, keypair.publicKey);
console.log('验证结果:', verifyResult);
+22
View File
@@ -0,0 +1,22 @@
const sm2 = require('sm-crypto').sm2;
console.log('=== 检查 sm-crypto 0.3.13 API ===');
// 检查 sm2 对象的属性
console.log('sm2 对象属性:', Object.keys(sm2));
// 检查 doVerifySignature 函数
console.log('\ndoVerifySignature 函数:', sm2.doVerifySignature.toString());
// 测试生成的签名和公钥
const testData = 'test';
const keypair = sm2.generateKeyPairHex();
const sig = sm2.doSignature(testData, keypair.privateKey);
console.log('\n测试数据:', testData);
console.log('生成的公钥:', keypair.publicKey);
console.log('生成的签名:', sig);
// 验证生成的签名
const verifyResult = sm2.doVerifySignature(testData, sig, keypair.publicKey);
console.log('验证结果:', verifyResult);
+55
View File
@@ -0,0 +1,55 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 调试 sm-crypto 0.3.13 API ===');
// 测试基本功能
const testData = 'test';
const hash = sm3(testData);
console.log('测试数据:', testData);
console.log('SM3 哈希:', hash);
// 生成密钥对
const keypair = sm2.generateKeyPairHex();
console.log('\n生成的密钥对:');
console.log(' 私钥:', keypair.privateKey);
console.log(' 公钥:', keypair.publicKey);
// 签名
const sig = sm2.doSignature(testData, keypair.privateKey);
console.log('\n签名:', sig);
console.log('签名长度:', sig.length);
// 验证签名
const verifyResult = sm2.doVerifySignature(testData, sig, keypair.publicKey);
console.log('验证结果:', verifyResult);
// 测试用户提供的数据
console.log('\n=== 测试用户数据 ===');
const data = 'QDJNHHVB_FWQ';
const signature = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFA CB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50'.replace(/\s/g, '');
const publicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
console.log('数据:', data);
console.log('签名:', signature);
console.log('公钥:', publicKey);
// 尝试验证
console.log('\n尝试验证:');
try {
// 注意:sm-crypto 0.3.13 的 API 可能不同
// 尝试不带选项
const result1 = sm2.doVerifySignature(data, signature, publicKey);
console.log(' 验证结果 (无选项):', result1);
} catch (e) {
console.error(' 错误 (无选项):', e.message);
}
try {
// 尝试带哈希选项
const result2 = sm2.doVerifySignature(hash, signature, publicKey, true); // 第二个参数是 hash 选项
console.log(' 验证结果 (hash=true):', result2);
} catch (e) {
console.error(' 错误 (hash=true):', e.message);
}
+37
View File
@@ -0,0 +1,37 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据(没有头尾)
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('=== 调试 SM2 证书提取 ===');
console.log('p7 keys:', Object.keys(p7));
console.log('p7.certs:', p7.certs);
console.log('p7.sinfos:', p7.sinfos);
// 尝试直接解析证书
console.log('\n=== 直接解析证书 ===');
try {
// 从 hex 中提取证书
const hex = rs.b64tohex(sm2P7Base64);
console.log('hex length:', hex.length);
// 使用 ASN1HEX 解析
const hCert = rs.ASN1HEX.getTLVbyList(hex, 0, [0, 3, 0, 0]);
console.log('hCert:', hCert ? hCert.substring(0, 50) + '...' : 'null');
if (hCert) {
const certPem = rs.KJUR.asn1.ASN1Util.getPEMStringFromHex(hCert, 'CERTIFICATE');
console.log('certPem:', certPem.substring(0, 200));
const x509 = new rs.X509();
x509.readCertPEM(certPem);
console.log('\n主题:', x509.getSubjectString());
console.log('颁发者:', x509.getIssuerString());
}
} catch (e) {
console.error('错误:', e.message);
}
+29
View File
@@ -0,0 +1,29 @@
const sm2 = require('sm-crypto').sm2;
console.log('=== 调试 SM2 DER 格式 ===');
// 测试数据
const data = 'QDJNHHVB_FWQ';
const userId = '1234567812345678';
// 生成密钥对
const keypair = sm2.generateKeyPairHex();
console.log('公钥:', keypair.publicKey);
// 生成签名(带 der 选项)
const sigWithDer = sm2.doSignature(data, keypair.privateKey, { der: true });
console.log('\n带 DER 的签名:', sigWithDer);
console.log('带 DER 的签名长度:', sigWithDer.length);
// 生成签名(不带 der 选项)
const sigWithoutDer = sm2.doSignature(data, keypair.privateKey);
console.log('\n不带 DER 的签名:', sigWithoutDer);
console.log('不带 DER 的签名长度:', sigWithoutDer.length);
// 验证带 DER 的签名
const verifyWithDer = sm2.doVerifySignature(data, sigWithDer, keypair.publicKey, { der: true });
console.log('\n验证带 DER 的签名:', verifyWithDer);
// 验证不带 DER 的签名
const verifyWithoutDer = sm2.doVerifySignature(data, sigWithoutDer, keypair.publicKey);
console.log('验证不带 DER 的签名:', verifyWithoutDer);
+42
View File
@@ -0,0 +1,42 @@
const crypto = require('../src/crypto.js');
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
console.log('=== 测试不同的原始数据 ===');
// 测试不同的原始数据
const testDataList = [
'QDJNHHVB_FWQ',
'QDJNHHVB_FWQ\n',
'QDJNHHVB_FWQ\r\n',
'QDJNHHVB FWQ', // 空格替换下划线
'QDJNHHVB_FW', // 少一个字符
'QDJNHHVB_FWQQ', // 多一个字符
'qDJNHHVB_FWQ', // 小写
'QDJnhhVB_FWQ', // 混合大小写
'1234567890', // 数字
'', // 空字符串
];
testDataList.forEach((data, index) => {
console.log(`\n测试数据 ${index + 1}: "${data}"`);
console.log(` 长度: ${data.length}`);
console.log(` UTF-8 编码: ${Buffer.from(data, 'utf8').toString('hex')}`);
const result = crypto.verifyPKCS7(sm2P7Base64, data);
console.log(` 验证结果: ${result}`);
if (result) {
console.log(' ✅ 验证成功!');
}
});
// 也测试一下 attach 模式
console.log('\n=== 测试 attach 模式 ===');
try {
const result = crypto.verifyPKCS7(sm2P7Base64);
console.log('attach 模式验证结果:', result);
} catch (e) {
console.error('attach 模式错误:', e.message);
}
+67
View File
@@ -0,0 +1,67 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据(没有头尾)
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('=== 调试 SM2 公钥提取 ===');
if (p7.certs && p7.certs.array && p7.certs.array.length > 0) {
const certPem = p7.certs.array[0];
console.log('证书 PEM 前200字符:', certPem.substring(0, 200));
const x509 = new rs.X509();
x509.readCertPEM(certPem);
console.log('\n证书信息:');
console.log('主题:', x509.getSubjectString());
console.log('颁发者:', x509.getIssuerString());
console.log('序列号:', x509.getSerialNumberHex());
// 尝试获取公钥
console.log('\n尝试获取公钥:');
try {
const pubKey = x509.getPublicKey();
console.log('getPublicKey() 结果:', pubKey);
if (pubKey) {
console.log('公钥类型:', typeof pubKey);
console.log('公钥 keys:', Object.keys(pubKey));
}
} catch (e) {
console.error('getPublicKey() 错误:', e.message);
}
// 尝试直接从 ASN.1 解析
console.log('\n尝试从 ASN.1 解析公钥:');
try {
const hex = rs.pemtohex(certPem);
// 证书结构: SEQUENCE -> tbsCertificate -> subjectPublicKeyInfo
const hTBS = rs.ASN1HEX.getTLVbyList(hex, 0, [0]);
const hSPKI = rs.ASN1HEX.getTLVbyList(hTBS, 0, [6]); // subjectPublicKeyInfo 通常在第6个位置
if (hSPKI) {
console.log('subjectPublicKeyInfo hex:', hSPKI.substring(0, 100) + '...');
// 提取公钥值
const hAlgorithm = rs.ASN1HEX.getTLVbyList(hSPKI, 0, [0]);
const hPublicKey = rs.ASN1HEX.getTLVbyList(hSPKI, 0, [1]);
if (hAlgorithm) {
console.log('算法 OID:', rs.ASN1HEX.getOIDbyList(hAlgorithm, 0, [0]));
}
if (hPublicKey) {
console.log('公钥值 hex:', hPublicKey.substring(4) + '...'); // 跳过 tag 和 length
// 尝试创建 PEM
const pubKeyPem = rs.KJUR.asn1.ASN1Util.getPEMStringFromHex(hSPKI, 'PUBLIC KEY');
console.log('公钥 PEM:', pubKeyPem.substring(0, 200) + '...');
}
}
} catch (e) {
console.error('ASN.1 解析错误:', e.message);
}
}
+95
View File
@@ -0,0 +1,95 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('=== 调试 SM2 公钥提取 ===');
if (p7.certs && p7.certs.array && p7.certs.array.length > 0) {
const certPem = p7.certs.array[0];
// 将 PEM 转换为 hex
const certHex = rs.pemtohex(certPem);
console.log('证书 hex 长度:', certHex.length);
console.log('证书 hex 前100字符:', certHex.substring(0, 100));
// 证书结构: SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
// tbsCertificate 结构: SEQUENCE { version, serialNumber, signature, issuer, validity, subject, subjectPublicKeyInfo, ... }
try {
// 获取 tbsCertificate
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0]);
console.log('\ntbsCertificate 长度:', tbsCert.length);
// 获取 subjectPublicKeyInfo (在 tbsCertificate 中的索引通常是 6)
// 但我们需要先确定正确的索引
let spkiIndex = 6;
let spki = null;
// 尝试不同的索引
for (let i = 3; i <= 8; i++) {
try {
const tlv = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [i]);
if (tlv && tlv.startsWith('30')) { // SEQUENCE
// 检查是否是 subjectPublicKeyInfo
// subjectPublicKeyInfo 应该包含 algorithm 和 subjectPublicKey
const inner = rs.ASN1HEX.getTLVbyList(tlv, 0, [0]);
if (inner && inner.startsWith('30')) {
console.log(`索引 ${i} 可能是 subjectPublicKeyInfo`);
spki = tlv;
spkiIndex = i;
break;
}
}
} catch (e) {
// 忽略错误
}
}
if (spki) {
console.log('\nsubjectPublicKeyInfo hex:', spki.substring(0, 100));
// 提取算法 OID
try {
const algorithmSeq = rs.ASN1HEX.getTLVbyList(spki, 0, [0]);
console.log('algorithmSeq:', algorithmSeq);
// 提取 OID
const oidTlv = rs.ASN1HEX.getTLVbyList(algorithmSeq, 0, [0]);
console.log('OID TLV:', oidTlv);
// 解析 OID
if (oidTlv && oidTlv.startsWith('06')) {
const oidHex = oidTlv.substring(4);
console.log('OID hex:', oidHex);
}
} catch (e) {
console.error('提取算法 OID 错误:', e.message);
}
// 提取公钥值 (BIT STRING)
try {
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1]);
console.log('\n公钥 BIT STRING:', publicKeyBitString.substring(0, 100));
// BIT STRING 的内容从第4个字符开始 (跳过 tag 03, length, 和 unused bits 00)
const publicKeyContent = publicKeyBitString.substring(4);
console.log('公钥内容:', publicKeyContent.substring(0, 100));
// 对于 SM2,公钥通常是 65 字节 (0x04 + 32字节 X + 32字节 Y)
console.log('公钥内容长度:', publicKeyContent.length / 2, '字节');
// 创建公钥 hex
console.log('\n公钥 hex (完整):', publicKeyContent);
} catch (e) {
console.error('提取公钥值错误:', e.message);
}
}
} catch (e) {
console.error('ASN.1 解析错误:', e.message);
console.error(e.stack);
}
}
+45
View File
@@ -0,0 +1,45 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 调试 SM2 公钥格式 ===');
// 测试数据
const testData = 'QDJNHHVB_FWQ';
const hash = sm3(testData);
console.log('测试数据:', testData);
console.log('SM3 哈希:', hash);
// 用户提供的签名(ASN.1 DER 格式)
const userSignature = '3044022002d3816303ee2a352477f3227070cc34297bd6e89dc86e2c54840fffc92c3b67022066114b6bfacb00408cc51b8c18d02023f50671844372094d9460bd5f85acac50';
// 解析签名
const rStart = 8;
const r = userSignature.substring(rStart, rStart + 64);
const sStart = rStart + 64 + 4;
const s = userSignature.substring(sStart, sStart + 64);
const rawSignature = r + s;
console.log('\n原始签名格式:', rawSignature);
// 用户公钥
const userPublicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
console.log('\n完整公钥:', userPublicKey);
// 去掉 04 前缀
const pubKeyWithoutPrefix = userPublicKey.substring(2);
console.log('去掉前缀的公钥:', pubKeyWithoutPrefix);
// 尝试验证
console.log('\n验证结果:');
try {
const result1 = sm2.doVerifySignature(hash, rawSignature, userPublicKey);
console.log(' 使用完整公钥:', result1);
} catch (e) {
console.error(' 完整公钥错误:', e.message);
}
try {
const result2 = sm2.doVerifySignature(hash, rawSignature, pubKeyWithoutPrefix);
console.log(' 使用去掉前缀的公钥:', result2);
} catch (e) {
console.error(' 去掉前缀错误:', e.message);
}
+43
View File
@@ -0,0 +1,43 @@
const sm2 = require('sm-crypto').sm2;
console.log('=== 测试公钥格式 ===');
// 测试数据
const data = 'QDJNHHVB_FWQ';
const signature = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFACB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50';
// 完整公钥(带 04 前缀)
const fullPublicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
// 去掉 04 前缀的公钥
const publicKeyWithoutPrefix = fullPublicKey.substring(2);
console.log('完整公钥:', fullPublicKey);
console.log('去掉前缀的公钥:', publicKeyWithoutPrefix);
// 测试完整公钥
console.log('\n使用完整公钥:');
try {
const result1 = sm2.doVerifySignature(data, signature, fullPublicKey);
console.log(' 验证结果:', result1);
} catch (e) {
console.error(' 错误:', e.message);
}
// 测试去掉前缀的公钥
console.log('\n使用去掉前缀的公钥:');
try {
const result2 = sm2.doVerifySignature(data, signature, publicKeyWithoutPrefix);
console.log(' 验证结果:', result2);
} catch (e) {
console.error(' 错误:', e.message);
}
// 测试带哈希选项
console.log('\n使用去掉前缀的公钥 + 哈希:');
try {
const result3 = sm2.doVerifySignature(data, signature, publicKeyWithoutPrefix, true);
console.log(' 验证结果:', result3);
} catch (e) {
console.error(' 错误:', e.message);
}
+67
View File
@@ -0,0 +1,67 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 调试 SM2 签名格式 ===');
// 生成测试数据
const testData = 'QDJNHHVB_FWQ';
const hash = sm3(testData);
console.log('测试数据:', testData);
console.log('SM3 哈希:', hash);
// 生成密钥对
const keypair = sm2.generateKeyPairHex();
console.log('\n生成的密钥对:');
console.log(' 私钥:', keypair.privateKey);
console.log(' 公钥:', keypair.publicKey);
// 生成签名
const sig = sm2.doSignature(hash, keypair.privateKey);
console.log('\nsm-crypto 生成的签名:', sig);
console.log('签名长度:', sig.length);
// 验证签名
const verifyResult = sm2.doVerifySignature(hash, sig, keypair.publicKey);
console.log('验证结果:', verifyResult);
// 对比用户提供的签名
const userSignature = '3044022002d3816303ee2a352477f3227070cc34297bd6e89dc86e2c54840fffc92c3b67022066114b6bfacb00408cc51b8c18d02023f50671844372094d9460bd5f85acac50';
console.log('\n用户提供的签名:', userSignature);
console.log('用户签名长度:', userSignature.length);
// 分析签名格式
console.log('\n签名格式分析:');
console.log('sm-crypto 签名格式:', sig.length === 128 ? '原始 r|s 格式' : '其他格式');
console.log('用户签名格式:', userSignature.startsWith('30') ? 'ASN.1 DER 格式' : '其他格式');
// 尝试转换用户签名格式
if (userSignature.startsWith('30')) {
console.log('\n尝试将 ASN.1 DER 格式转换为原始格式:');
try {
// 解析 ASN.1 DER 格式
// 格式: 30 len 02 len r 02 len s
const rStart = 6; // 30 44 02 20
const rLen = parseInt(userSignature.substring(4, 6), 16) * 2;
const r = userSignature.substring(rStart, rStart + rLen);
const sStart = rStart + rLen + 4; // 02 20
const sLen = parseInt(userSignature.substring(rStart + rLen + 2, rStart + rLen + 4), 16) * 2;
const s = userSignature.substring(sStart, sStart + sLen);
console.log(' 提取的 r:', r);
console.log(' 提取的 s:', s);
// 组合成原始格式
const rawSignature = r + s;
console.log(' 转换后的原始格式:', rawSignature);
console.log(' 原始格式长度:', rawSignature.length);
// 尝试验证
const userPublicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
const result = sm2.doVerifySignature(hash, rawSignature, userPublicKey);
console.log(' 验证结果:', result);
} catch (e) {
console.error(' 转换错误:', e.message);
}
}
+50
View File
@@ -0,0 +1,50 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 调试 SM2 签名格式(正确解析)===');
// 测试数据
const testData = 'QDJNHHVB_FWQ';
const hash = sm3(testData);
console.log('测试数据:', testData);
console.log('SM3 哈希:', hash);
// 用户提供的签名(ASN.1 DER 格式)
const userSignature = '3044022002d3816303ee2a352477f3227070cc34297bd6e89dc86e2c54840fffc92c3b67022066114b6bfacb00408cc51b8c18d02023f50671844372094d9460bd5f85acac50';
console.log('\n用户提供的签名:', userSignature);
// 正确解析 ASN.1 DER 格式
console.log('\n正确解析 ASN.1 DER 格式:');
try {
// 30 44 02 20 [r] 02 20 [s]
// 30 = 序列
// 44 = 长度 68 字节
// 02 = 整数
// 20 = 长度 32 字节
// 提取 r 值
const rStart = 8; // 30440220
const r = userSignature.substring(rStart, rStart + 64); // 32字节 = 64 hex 字符
console.log(' r 值:', r);
// 提取 s 值
const sStart = rStart + 64 + 4; // 0220
const s = userSignature.substring(sStart, sStart + 64); // 32字节 = 64 hex 字符
console.log(' s 值:', s);
// 组合成原始格式 (r + s)
const rawSignature = r + s;
console.log(' 转换后的原始格式:', rawSignature);
console.log(' 原始格式长度:', rawSignature.length);
// 用户公钥
const userPublicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
console.log('\n用户公钥:', userPublicKey);
// 尝试验证
const result = sm2.doVerifySignature(hash, rawSignature, userPublicKey);
console.log('\n验证结果:', result);
} catch (e) {
console.error('解析错误:', e.message);
}
+80
View File
@@ -0,0 +1,80 @@
const rs = require('jsrsasign');
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
// 测试用的原始数据
const mockOriginalData = 'QDJNHHVB_FWQ';
console.log('=== 调试 SM2 PKCS7 验证 ===');
console.log('原始数据:', mockOriginalData);
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('\nPKCS7 信息:');
console.log('版本:', p7.version);
console.log('哈希算法:', p7.hashalgs);
console.log('签名者数量:', p7.sinfos ? p7.sinfos.length : 0);
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
console.log('\n签名者信息:');
console.log(' 签名算法:', signer.sigalg);
console.log(' 签名值 (hex):', signer.sighex);
console.log(' 签名值长度:', signer.sighex ? signer.sighex.length / 2 : 0, '字节');
}
// 提取原始数据(econtent)
if (p7.econtent && p7.econtent.hex) {
const extractedData = Buffer.from(p7.econtent.hex, 'hex').toString();
console.log('\n从 PKCS7 提取的原始数据:', extractedData);
console.log('与提供的原始数据匹配:', extractedData === mockOriginalData);
}
// 提取公钥
if (p7.certs && p7.certs.array && p7.certs.array.length > 0) {
const certPem = p7.certs.array[0];
const certHex = rs.pemtohex(certPem);
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0]);
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6]);
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1]);
const publicKeyHex = publicKeyBitString.substring(6);
console.log('\n公钥信息:');
console.log(' 公钥 (hex):', publicKeyHex.substring(0, 50) + '...');
console.log(' 公钥长度:', publicKeyHex.length / 2, '字节');
// 尝试验证签名
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
// 计算 SM3 哈希
const hash = sm3(mockOriginalData);
console.log('\nSM3 哈希:', hash);
// 尝试验证
console.log('\n尝试使用 sm-crypto 验证:');
try {
// sm-crypto 的 doVerify 需要 hash 和公钥
// 注意:sm-crypto 的 API 可能需要不同的格式
const result = sm2.doVerify(hash, signer.sighex, publicKeyHex);
console.log('验证结果:', result);
} catch (e) {
console.error('验证错误:', e.message);
// 尝试其他格式
console.log('\n尝试其他格式...');
try {
// 可能需要去掉 04 前缀
const pubKeyWithoutPrefix = publicKeyHex.substring(2);
const result = sm2.doVerify(hash, signer.sighex, pubKeyWithoutPrefix);
console.log('验证结果 (去掉前缀):', result);
} catch (e2) {
console.error('验证错误 (去掉前缀):', e2.message);
}
}
}
}
+113
View File
@@ -0,0 +1,113 @@
const rs = require('jsrsasign');
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
// 测试用的原始数据
const mockOriginalData = 'QDJNHHVB_FWQ';
console.log('=== 详细调试 SM2 PKCS7 验证 ===');
console.log('原始数据:', mockOriginalData);
console.log('原始数据长度:', mockOriginalData.length);
console.log('原始数据 UTF-8 编码:', Buffer.from(mockOriginalData, 'utf8').toString('hex'));
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('\nPKCS7 信息:');
console.log('版本:', p7.version);
console.log('哈希算法:', p7.hashalgs);
console.log('签名者数量:', p7.sinfos ? p7.sinfos.length : 0);
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
console.log('\n签名者信息:');
console.log(' 签名算法:', signer.sigalg);
console.log(' 签名值 (hex):', signer.sighex);
console.log(' 签名值长度:', signer.sighex ? signer.sighex.length / 2 : 0, '字节');
// 解析签名值(ASN.1 DER 格式)
console.log('\n解析签名值:');
try {
const sigAsn1 = rs.ASN1HEX.getTLVbyList(signer.sighex, 0, []);
console.log(' 签名 ASN.1 结构:', sigAsn1);
// 提取 r 和 s 值
const r = rs.ASN1HEX.getTLVbyList(sigAsn1, 0, [0]);
const s = rs.ASN1HEX.getTLVbyList(sigAsn1, 0, [1]);
console.log(' r 值:', r.substring(4)); // 跳过 tag 和 length
console.log(' s 值:', s.substring(4));
} catch (e) {
console.error(' 解析签名值错误:', e.message);
}
}
// 提取公钥
if (p7.certs && p7.certs.array && p7.certs.array.length > 0) {
const certPem = p7.certs.array[0];
const certHex = rs.pemtohex(certPem);
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0]);
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6]);
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1]);
const publicKeyHex = publicKeyBitString.substring(6);
console.log('\n公钥信息:');
console.log(' 公钥 (hex):', publicKeyHex);
console.log(' 公钥长度:', publicKeyHex.length / 2, '字节');
console.log(' 公钥格式:', publicKeyHex.startsWith('04') ? '未压缩格式' : '压缩格式');
// 尝试验证签名
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
// 计算 SM3 哈希
console.log('\n计算 SM3 哈希:');
const hash = sm3(mockOriginalData);
console.log(' 原始数据哈希:', hash);
// 测试不同的公钥格式
console.log('\n测试不同的公钥格式:');
// 1. 使用完整公钥(带 04 前缀)
try {
const result1 = sm2.doVerifySignature(hash, signer.sighex, publicKeyHex);
console.log(' 验证结果(完整公钥):', result1);
} catch (e) {
console.error(' 验证错误(完整公钥):', e.message);
}
// 2. 使用不带 04 前缀的公钥
try {
const pubKeyWithoutPrefix = publicKeyHex.substring(2);
const result2 = sm2.doVerifySignature(hash, signer.sighex, pubKeyWithoutPrefix);
console.log(' 验证结果(不带前缀):', result2);
} catch (e) {
console.error(' 验证错误(不带前缀):', e.message);
}
// 3. 尝试使用签名值的不同格式
console.log('\n测试不同的签名值格式:');
try {
// 直接使用 sighex
const result3 = sm2.doVerifySignature(hash, signer.sighex, publicKeyHex);
console.log(' 验证结果(直接使用 sighex):', result3);
} catch (e) {
console.error(' 验证错误(直接使用 sighex):', e.message);
}
// 4. 测试不同的编码
console.log('\n测试不同的编码:');
try {
// 使用 Buffer 转换
const hashBuffer = Buffer.from(mockOriginalData, 'utf8');
const hash2 = sm3(hashBuffer);
console.log(' Buffer 哈希:', hash2);
const result4 = sm2.doVerifySignature(hash2, signer.sighex, publicKeyHex);
console.log(' 验证结果(Buffer 编码):', result4);
} catch (e) {
console.error(' 验证错误(Buffer 编码):', e.message);
}
}
}
+35
View File
@@ -0,0 +1,35 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 检查 sm-crypto 版本 ===');
console.log('sm-crypto 版本:', require('sm-crypto/package.json').version);
// 测试公钥格式
console.log('\n=== 测试公钥格式 ===');
// 生成一个有效的 SM2 密钥对
const keypair = sm2.generateKeyPairHex();
console.log('生成的公钥:', keypair.publicKey);
console.log('生成的公钥长度:', keypair.publicKey.length);
// 用户提供的公钥
const userPublicKey = 'bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
console.log('\n用户提供的公钥:', userPublicKey);
console.log('用户提供的公钥长度:', userPublicKey.length);
// 测试用户公钥
console.log('\n=== 测试用户公钥 ===');
try {
const data = 'test';
const sig = sm2.doSignature(data, keypair.privateKey);
// 尝试使用生成的公钥验证(应该成功)
const verify1 = sm2.doVerifySignature(data, sig, keypair.publicKey);
console.log('使用生成的公钥验证:', verify1);
// 尝试使用用户公钥验证(应该失败)
const verify2 = sm2.doVerifySignature(data, sig, userPublicKey);
console.log('使用用户公钥验证:', verify2);
} catch (e) {
console.error('错误:', e.message);
}
+69
View File
@@ -0,0 +1,69 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 验证用户提供的具体数据 ===');
// 用户提供的数据
const dataHex = '51444A4E484856425F465751';
const signatureHex = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFA CB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50'.replace(/\s/g, '');
const userId = '1234567812345678';
const hashHex = '0F76D50E476E24507187885DF1BC197B48F368F3C03DDF0A5C0C067D2EEF63EE';
// 从用户的 PKCS7 签名中提取的公钥
const publicKeyHex = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
// 转换数据
const data = Buffer.from(dataHex, 'hex').toString('utf8');
console.log('数据:', data);
console.log('数据长度:', data.length);
console.log('签名:', signatureHex);
console.log('签名长度:', signatureHex.length / 2, '字节');
console.log('公钥:', publicKeyHex);
// 测试不同的验证选项
console.log('\n测试不同的验证选项:');
// 尝试 1: 使用原始数据 + DER + SM3 + userId
console.log('\n1. 使用原始数据 + DER + SM3 + userId:');
try {
const result1 = sm2.doVerifySignature(data, signatureHex, publicKeyHex, { der: true, hash: true, userId });
console.log(' 验证结果:', result1);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 2: 使用哈希 + DER
console.log('\n2. 使用哈希 + DER:');
try {
const result2 = sm2.doVerifySignature(hashHex, signatureHex, publicKeyHex, { der: true });
console.log(' 验证结果:', result2);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 3: 使用原始数据 + DER
console.log('\n3. 使用原始数据 + DER:');
try {
const result3 = sm2.doVerifySignature(data, signatureHex, publicKeyHex, { der: true });
console.log(' 验证结果:', result3);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 4: 直接使用哈希
console.log('\n4. 直接使用哈希:');
try {
const result4 = sm2.doVerifySignature(hashHex, signatureHex, publicKeyHex);
console.log(' 验证结果:', result4);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 5: 直接使用原始数据
console.log('\n5. 直接使用原始数据:');
try {
const result5 = sm2.doVerifySignature(data, signatureHex, publicKeyHex);
console.log(' 验证结果:', result5);
} catch (e) {
console.error(' 错误:', e.message);
}
+80
View File
@@ -0,0 +1,80 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 验证用户提供的 SM2 数据 ===');
// 用户提供的数据
const dataHex = '51444A4E484856425F465751';
const signatureHex = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFA CB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50'.replace(/\s/g, '');
const userId = '1234567812345678';
const hashHex = '0F76D50E476E24507187885DF1BC197B48F368F3C03DDF0A5C0C067D2EEF63EE';
// 从用户的 PKCS7 签名中提取的公钥
const publicKeyHex = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
// 转换数据
const data = Buffer.from(dataHex, 'hex').toString('utf8');
console.log('数据:', data);
console.log('数据长度:', data.length);
console.log('签名长度:', signatureHex.length / 2, '字节');
// 去掉公钥的 04 前缀
const pubKeyWithoutPrefix = publicKeyHex.substring(2);
console.log('公钥(去掉前缀):', pubKeyWithoutPrefix);
// 测试不同的验证选项
console.log('\n测试不同的验证选项:');
// 尝试 1: 使用哈希 + userId
console.log('\n1. 使用哈希 + userId:');
try {
const result1 = sm2.doVerifySignature(hashHex, signatureHex, pubKeyWithoutPrefix, { hash: true, userId });
console.log(' 验证结果:', result1);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 2: 使用原始数据 + userId
console.log('\n2. 使用原始数据 + userId:');
try {
const result2 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix, { hash: true, userId });
console.log(' 验证结果:', result2);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 3: 使用原始数据 + userId + 公钥
console.log('\n3. 使用原始数据 + userId + 公钥:');
try {
const result3 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix, { hash: true, userId, publicKey: pubKeyWithoutPrefix });
console.log(' 验证结果:', result3);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 4: 直接使用原始数据
console.log('\n4. 直接使用原始数据:');
try {
const result4 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix);
console.log(' 验证结果:', result4);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 5: 使用 DER 格式
console.log('\n5. 使用 DER 格式:');
try {
const result5 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix, { der: true });
console.log(' 验证结果:', result5);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 6: 使用 DER + 哈希 + userId
console.log('\n6. 使用 DER + 哈希 + userId:');
try {
const result6 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix, { der: true, hash: true, userId });
console.log(' 验证结果:', result6);
} catch (e) {
console.error(' 错误:', e.message);
}
+91
View File
@@ -0,0 +1,91 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 验证用户提供的 SM2 数据 ===');
// 用户提供的数据
const dataHex = '51444A4E484856425F465751';
const signatureHex = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFA CB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50'.replace(/\s/g, '');
const userId = '1234567812345678';
const hashHex = '0F76D50E476E24507187885DF1BC197B48F368F3C03DDF0A5C0C067D2EEF63EE';
// 从用户的 PKCS7 签名中提取的公钥(去掉 04 前缀)
const publicKeyHex = 'bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
// 转换数据
const data = Buffer.from(dataHex, 'hex').toString('utf8');
console.log('数据:', data);
console.log('数据长度:', data.length);
console.log('签名:', signatureHex);
console.log('签名长度:', signatureHex.length / 2, '字节');
console.log('公钥:', publicKeyHex);
console.log('userId:', userId);
// 计算哈希
const calculatedHash = sm3(data);
console.log('\n计算的哈希:', calculatedHash);
console.log('用户提供的哈希:', hashHex);
console.log('哈希是否匹配:', calculatedHash === hashHex);
// 测试不同的验证选项
console.log('\n测试不同的验证选项:');
// 尝试 1: 直接验证(原始 r|s 格式)
console.log('\n1. 直接验证(原始 r|s 格式):');
try {
const result1 = sm2.doVerifySignature(data, signatureHex, publicKeyHex);
console.log(' 验证结果:', result1);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 2: 带 SM3 哈希
console.log('\n2. 带 SM3 哈希:');
try {
const result2 = sm2.doVerifySignature(data, signatureHex, publicKeyHex, { hash: true });
console.log(' 验证结果:', result2);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 3: 带 SM3 哈希 + userId
console.log('\n3. 带 SM3 哈希 + userId:');
try {
const result3 = sm2.doVerifySignature(data, signatureHex, publicKeyHex, { hash: true, userId });
console.log(' 验证结果:', result3);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 4: 使用哈希值
console.log('\n4. 使用哈希值:');
try {
const result4 = sm2.doVerifySignature(hashHex, signatureHex, publicKeyHex, { hash: true });
console.log(' 验证结果:', result4);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 5: 使用哈希值 + userId
console.log('\n5. 使用哈希值 + userId:');
try {
const result5 = sm2.doVerifySignature(hashHex, signatureHex, publicKeyHex, { hash: true, userId });
console.log(' 验证结果:', result5);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 6: 转换为 DER 格式
console.log('\n6. 转换为 DER 格式:');
try {
// 构造 DER 格式: 30 44 02 20 [r] 02 20 [s]
const r = signatureHex.substring(0, 64);
const s = signatureHex.substring(64);
const derSignature = '30440220' + r + '0220' + s;
console.log(' DER 签名:', derSignature);
const result6 = sm2.doVerifySignature(data, derSignature, publicKeyHex, { der: true, hash: true, userId });
console.log(' 验证结果:', result6);
} catch (e) {
console.error(' 错误:', e.message);
}
+79
View File
@@ -0,0 +1,79 @@
const forge = require('node-forge');
const mockP7Sign = `-----BEGIN PKCS7-----
MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExDzANBglghkgBZQMEAgEFADA8Bgkq
hkiG9w0BBwGgLwQtVGhpcyBpcyB0aGUgdGVzdCBjb250ZW50IGZvciBQS0NTNyBz
aWduYXR1cmUuoIIB5jCCAeIwggFLoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwNzEZ
MBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTEaMBgGA1UEChMRVGVzdCBPcmdhbml6
YXRpb24wHhcNMjYwNDA0MDAxMDIyWhcNMjcwNDA0MDAxMDIyWjA3MRkwFwYDVQQD
ExB0ZXN0LmV4YW1wbGUuY29tMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1e+05xb8T3Lo2v9MZQen5HYffpnc
yY9mnT/ygvfxZNM9TeYkq4fACiuS99FONnlHFQdlXEPJluIk1NBFUoLpehAbzOSY
Fu0WEjBOVJIg9FIWm7TUojU8yvPsfPGFYTEGEOGAse8AfHw4pl+w5SvxQUF8f2jB
XsRY0MrDyb07xC8CAwEAATANBgkqhkiG9w0BAQsFAAOBgQAOEttx/p3xosIm/8sM
kIaxR2c/fbYoMbBsX+geaiRu+xNOItbtQpGOhW6Ql981c3EwIjyd34KavplofZIr
Slj4jHQFySZaJkegHY+LCAQz9GEg2TMke3MsPJ7nSZS7MUqB2AQt1tLuiXUuRh3V
jF4yiqi6lGVmvPJf+y1GXk47KjGB5TCB4gIBATA8MDcxGTAXBgNVBAMTEHRlc3Qu
ZXhhbXBsZS5jb20xGjAYBgNVBAoTEVRlc3QgT3JnYW5pemF0aW9uAgEBMA0GCWCG
SAFlAwQCAQUAMA0GCSqGSIb3DQEBAQUABIGAMxSC81vzjSiZcJTcSicgCG9879mu
p/bikqdHZPyF8UgABjikXXoVJ9IClFRd5/3wRf8ez+hwnxKR8j1vAsDI7U6dXJtr
wp0aO7Ua6ecKhI5V0F8nUg5CjEK9FR8vNg0Il8U5AXDuan0iYKvguhWkiuJlvQlV
X9ncPP4VRERPw8U=
-----END PKCS7-----`;
const originalData = 'This is the test content for PKCS7 signature.';
try {
const p7 = forge.pkcs7.messageFromPem(mockP7Sign);
console.log('=== PKCS7 信息 ===');
console.log('type:', p7.type);
console.log('content:', p7.content ? p7.content.getBytes() : 'null');
console.log('certificates count:', p7.certificates ? p7.certificates.length : 0);
if (p7.certificates && p7.certificates.length > 0) {
const cert = p7.certificates[0];
console.log('cert subject:', cert.subject.attributes.map(a => `${a.name}=${a.value}`).join(', '));
}
console.log('\n=== 尝试验证 ===');
try {
// 尝试验证
const result = p7.verify();
console.log('verify() result:', result);
} catch (e) {
console.log('verify() error:', e.message);
}
// 手动验证
console.log('\n=== 手动验证 ===');
const md = forge.md.sha256.create();
md.update(originalData, 'utf8');
const digest = md.digest().getBytes();
console.log('calculated digest (hex):', forge.util.bytesToHex(digest));
// 获取签名
if (p7.rawCapture && p7.rawCapture.signature) {
console.log('signature length:', p7.rawCapture.signature.length);
console.log('signature (hex):', forge.util.bytesToHex(p7.rawCapture.signature).substring(0, 100) + '...');
}
// 获取公钥
if (p7.certificates && p7.certificates.length > 0) {
const publicKey = p7.certificates[0].publicKey;
if (publicKey) {
console.log('publicKey exists:', true);
// 尝试用公钥验证
try {
const verified = publicKey.verify(digest, p7.rawCapture.signature);
console.log('publicKey.verify() result:', verified);
} catch (e) {
console.log('publicKey.verify() error:', e.message);
}
}
}
} catch (error) {
console.error('Error:', error.message);
console.error(error.stack);
}
+21
View File
@@ -0,0 +1,21 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const { buildScript } = require("./restore-run");
const inspectPath = path.join(__dirname, "testcase-inspect.json");
const expectedPath = path.join(__dirname, "testcase-run.sh");
const inspectData = JSON.parse(fs.readFileSync(inspectPath, "utf8"));
const info = Array.isArray(inspectData) ? inspectData[0] : inspectData;
const actual = buildScript(info);
const expected = fs.readFileSync(expectedPath, "utf8").replace(/\r\n/g, "\n");
assert.strictEqual(actual, expected);
assert.ok(actual.includes("name=freshrss"));
assert.ok(actual.includes("img=freshrss/freshrss:latest"));
assert.ok(actual.includes('-v "$(pwd)/data:/var/www/FreshRSS/data"'));
assert.ok(actual.includes('-v "$(pwd)/extensions:/var/www/FreshRSS/extensions"'));
assert.strictEqual((actual.match(/-p 8082:80\/tcp/g) || []).length, 1);
console.log("restore-run testcase passed");
+272
View File
@@ -0,0 +1,272 @@
[
{
"Id": "71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338",
"Created": "2026-06-09T08:40:16.386743045Z",
"Path": "./Docker/entrypoint.sh",
"Args": [
"/bin/bash",
"-o",
"pipefail",
"-c",
"([ -z \"$CRON_MIN\" ] || cron) && \t. /etc/apache2/envvars && \texec apache2 -D FOREGROUND $([ -n \"$OIDC_ENABLED\" ] && [ \"$OIDC_ENABLED\" -ne 0 ] && echo \"-D OIDC_ENABLED\")"
],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 2178,
"ExitCode": 0,
"Error": "",
"StartedAt": "2026-06-11T03:36:17.818299057Z",
"FinishedAt": "2026-06-11T03:36:11.0220238Z"
},
"Image": "sha256:fc6e4cf934a2d62c5da897c9fd011ef29dc3c83948ef3581d2a526fc90789622",
"ResolvConfPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/hostname",
"HostsPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/hosts",
"LogPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338-json.log",
"Name": "/freshrss",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "docker-default",
"ExecIDs": null,
"HostConfig": {
"Binds": [
"/app/freshrss/extensions:/var/www/FreshRSS/extensions",
"/app/freshrss/data:/var/www/FreshRSS/data"
],
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {
"max-file": "3",
"max-size": "10m"
}
},
"NetworkMode": "bridge",
"PortBindings": {
"80/tcp": [
{
"HostIp": "",
"HostPort": "8082"
}
]
},
"RestartPolicy": {
"Name": "unless-stopped",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"ConsoleSize": [
30,
118
],
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "private",
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": [],
"BlkioDeviceReadBps": [],
"BlkioDeviceWriteBps": [],
"BlkioDeviceReadIOps": [],
"BlkioDeviceWriteIOps": [],
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DeviceCgroupRules": null,
"DeviceRequests": null,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": null,
"PidsLimit": null,
"Ulimits": [],
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9-init/diff:/var/lib/docker/overlay2/e16b5110bbde48cd9070a8bea52ca1b49a6c7a6ae17abc23bc2f2152aa851dbb/diff:/var/lib/docker/overlay2/6d104177f8014ee6b291525597ac93c48cce1d6e3f45f8390330dbee8f536d89/diff:/var/lib/docker/overlay2/640758ad6ad59ac4f5825afc463879a4da93264dcbe96cfd06653d494689b1c4/diff:/var/lib/docker/overlay2/51e3461fd7fed56b0595c7c83e8c0edd6a67ae72ef9a59d2a29d6bb808847204/diff:/var/lib/docker/overlay2/b7de361f0da3dea53c4bcc25a9bbde40efaaf7933147da3b95ed8e7d4d718ad4/diff:/var/lib/docker/overlay2/1ee479d5e4ed919312b4792bec9fd9a2425ee199b4b7ff911572f272b2ba2449/diff:/var/lib/docker/overlay2/4d45f37889ffe243f232f86582a4c3b8125b13b397cacfc0193a79af39158190/diff",
"MergedDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/merged",
"UpperDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/diff",
"WorkDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/work"
},
"Name": "overlay2"
},
"Mounts": [
{
"Type": "bind",
"Source": "/app/freshrss/data",
"Destination": "/var/www/FreshRSS/data",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/app/freshrss/extensions",
"Destination": "/var/www/FreshRSS/extensions",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
"Config": {
"Hostname": "71ba6f6111c8",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"80/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"TZ=Asia/Shanghai",
"CRON_MIN=*/30",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"COPY_LOG_TO_SYSLOG=On",
"COPY_SYSLOG_TO_STDERR=On",
"DATA_PATH=",
"FRESHRSS_ENV=",
"LISTEN=",
"OIDC_ENABLED=",
"TRUSTED_PROXY="
],
"Cmd": [
"/bin/bash",
"-o",
"pipefail",
"-c",
"([ -z \"$CRON_MIN\" ] || cron) && \t. /etc/apache2/envvars && \texec apache2 -D FOREGROUND $([ -n \"$OIDC_ENABLED\" ] && [ \"$OIDC_ENABLED\" -ne 0 ] && echo \"-D OIDC_ENABLED\")"
],
"Image": "freshrss/freshrss:latest",
"Volumes": null,
"WorkingDir": "/var/www/FreshRSS",
"Entrypoint": [
"./Docker/entrypoint.sh"
],
"OnBuild": null,
"Labels": {
"org.opencontainers.image.created": "2026-05-20T17:58:29.110Z",
"org.opencontainers.image.description": "A free, self-hostable news aggregator…",
"org.opencontainers.image.documentation": "https://freshrss.github.io/FreshRSS/",
"org.opencontainers.image.licenses": "AGPL-3.0",
"org.opencontainers.image.revision": "b2c50115baa36c217e939ee3ea8ecfae52f91abd",
"org.opencontainers.image.source": "https://github.com/FreshRSS/FreshRSS",
"org.opencontainers.image.title": "FreshRSS",
"org.opencontainers.image.url": "https://freshrss.org/",
"org.opencontainers.image.vendor": "FreshRSS",
"org.opencontainers.image.version": "1.29.1"
}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "78f7f8cccc0810fcdadb3d0e7bff0fe16156a46ab4b0feb7a4ebd6e80e5e330d",
"SandboxKey": "/var/run/docker/netns/78f7f8cccc08",
"Ports": {
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "8082"
},
{
"HostIp": "::",
"HostPort": "8082"
}
]
},
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "14c4ae2de08b7c68dfe8a757933088247f4c4ffd8c3d1236ff919da0422a655c",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:03",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"MacAddress": "02:42:ac:11:00:03",
"DriverOpts": null,
"NetworkID": "2e5dc7af2729566328476edc3fe4594e9f6b39adabbdcda5b7cc26fcbe073224",
"EndpointID": "14c4ae2de08b7c68dfe8a757933088247f4c4ffd8c3d1236ff919da0422a655c",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"DNSNames": null
}
}
}
}
]
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
name=freshrss
img=freshrss/freshrss:latest
docker run -d \
--name "$name" \
--restart unless-stopped \
-w /var/www/FreshRSS \
-e TZ=Asia/Shanghai \
-e 'CRON_MIN=*/30' \
-e PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
-e COPY_LOG_TO_SYSLOG=On \
-e COPY_SYSLOG_TO_STDERR=On \
-e DATA_PATH= \
-e FRESHRSS_ENV= \
-e LISTEN= \
-e OIDC_ENABLED= \
-e TRUSTED_PROXY= \
-p 8082:80/tcp \
-v "$(pwd)/data:/var/www/FreshRSS/data" \
-v "$(pwd)/extensions:/var/www/FreshRSS/extensions" \
--entrypoint ./Docker/entrypoint.sh \
"$img" /bin/bash -o pipefail -c '([ -z "$CRON_MIN" ] || cron) && . /etc/apache2/envvars && exec apache2 -D FOREGROUND $([ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ] && echo "-D OIDC_ENABLED")'
+31
View File
@@ -0,0 +1,31 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
const forge = require('node-forge');
console.log('=== 生成 SM2 测试数据 ===');
// 生成 SM2 密钥对
const keypair = sm2.generateKeyPairHex();
console.log('私钥:', keypair.privateKey);
console.log('公钥:', keypair.publicKey);
// 测试数据
const originalData = 'This is test data for SM2 signature.';
console.log('\n原始数据:', originalData);
// 计算 SM3 哈希
const hash = sm3(originalData);
console.log('SM3 哈希:', hash);
// SM2 签名
const sig = sm2.doSignature(hash, keypair.privateKey);
console.log('SM2 签名:', sig);
// 验证签名
const verifyResult = sm2.doVerify(hash, sig, keypair.publicKey);
console.log('签名验证结果:', verifyResult);
console.log('\n=== 注意 ===');
console.log('由于 node-forge 不支持 SM2 算法,无法生成标准的 SM2 PKCS7 签名。');
console.log('SM2 PKCS7 签名需要特殊的 OID 和格式,目前仅支持 RSA PKCS7。');
console.log('建议:测试2可以使用 RSA 的 PKCS7 数据,或者跳过 SM2 测试。');
+57
View File
@@ -0,0 +1,57 @@
const forge = require('node-forge');
console.log('=== 生成测试用的 PKCS7 签名 ===');
try {
// 1. 生成 RSA 密钥对
console.log('生成 RSA 密钥对...');
const keys = forge.pki.rsa.generateKeyPair(1024);
const privateKey = keys.privateKey;
const publicKey = keys.publicKey;
// 2. 创建自签名证书
console.log('创建自签名证书...');
const cert = forge.pki.createCertificate();
cert.publicKey = publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [
{ name: 'commonName', value: 'test.example.com' },
{ name: 'organizationName', value: 'Test Organization' }
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(privateKey, forge.md.sha256.create());
// 3. 创建 PKCS7 签名
console.log('创建 PKCS7 签名...');
const p7 = forge.pkcs7.createSignedData();
p7.content = forge.util.createBuffer('This is the test content for PKCS7 signature.');
p7.addCertificate(cert);
p7.addSigner({
key: privateKey,
certificate: cert,
digestAlgorithm: forge.pki.oids.sha256
});
p7.sign();
// 4. 转换为 PEM 格式
const pem = forge.pkcs7.messageToPem(p7);
console.log('\n=== 生成的 PKCS7 签名 PEM 格式 ===');
console.log(pem);
console.log('\n=== 提取 base64 部分 ===');
const base64 = pem.replace(/-----BEGIN PKCS7-----\n?/, '')
.replace(/-----END PKCS7-----\n?/, '')
.replace(/\n/g, '');
console.log(base64);
console.log('\n=== 完成 ===');
} catch (error) {
console.error('生成失败:', error);
console.error(error.stack);
}
+4 -6
View File
@@ -1,6 +1,4 @@
const net = require(__dirname + "/../src/net")
net.killport(6667);
const net = require(__dirname + "/../src/net")
net.killport(6667);
+13
View File
@@ -0,0 +1,13 @@
require("../src/init")
const Remote = require("../src/remote")
async function main(){
const remote = new Remote();
await remote.init()
console.log(await remote.check())
}
main()
+260
View File
@@ -0,0 +1,260 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "fs";
import os from "os";
import path from "path";
const RUN_MANUAL = process.env.KIT_REMOTE_MANU_TEST === "1";
const manualDescribe = RUN_MANUAL ? describe : describe.skip;
const manualTest = RUN_MANUAL ? test : test.skip;
let Remote;
if (RUN_MANUAL) {
require("../src/init");
Remote = require("../src/remote");
}
async function createRemote() {
const remote = new Remote();
await remote.init();
return remote;
}
function writeFixtureProject(tempDir, version, content, options = {}) {
const projectId = options.id || "myapp";
const group = options.group === undefined ? "/modules" : options.group;
const projectDir = path.resolve(tempDir, `fixture-${projectId}-${version || "auto"}-${Math.random().toString(16).slice(2)}`);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.resolve(projectDir, "myapp.txt");
fs.writeFileSync(filePath, content, "utf-8");
fs.writeFileSync(path.resolve(projectDir, "meta.json"), JSON.stringify({
id: projectId,
group,
version,
desc: "test app",
official_url: "https://example.test/myapp",
dist: [
{
path: filePath,
os: "any",
platform: "any",
filename: "myapp.txt",
type: "data"
}
]
}, null, 4), "utf-8");
return projectDir;
}
function normalizeRemotePath(value) {
if (!value || value === "/") {
return "/";
}
return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$/g, "") || "/";
}
function remoteProjectPath(group, projectId) {
const normalizedGroup = group === undefined ? "/modules" : normalizeRemotePath(group);
return normalizeRemotePath(path.posix.join(normalizedGroup, projectId));
}
async function deleteRemotePath(remote, remotePath) {
try {
if (remote.client && remote.client.deleteFile && await remote.client.exists(remotePath)) {
await remote.client.deleteFile(remotePath);
}
} catch (e) {
console.warn(`cleanup remote failed ${remotePath}: ${e.message}`);
}
}
let tempDir;
let localStore;
let remote;
let runId;
let counter;
let projectIds;
let remoteRoots;
beforeEach(async () => {
if (!RUN_MANUAL) {
return;
}
tempDir = fs.mkdtempSync(path.resolve(os.tmpdir(), "kit-remote-manu-test-"));
localStore = $sConfig.getLocalStorePath();
remote = await createRemote();
runId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
counter = 0;
projectIds = [];
remoteRoots = [];
});
afterEach(async () => {
if (!RUN_MANUAL) {
return;
}
for (const remoteRoot of remoteRoots.reverse()) {
await deleteRemotePath(remote, remoteRoot);
}
for (const projectId of projectIds) {
fs.rmSync(path.resolve(localStore, "modules", projectId), { recursive: true, force: true });
}
fs.rmSync(tempDir, { recursive: true, force: true });
});
function nextProjectId() {
const projectId = `myapp-${runId}-${++counter}`.toLowerCase();
projectIds.push(projectId);
return projectId;
}
function trackRemote(group, projectId) {
const root = remoteProjectPath(group, projectId);
remoteRoots.push(root);
return root;
}
manualDescribe("remote module public API manual webdav", () => {
manualTest("1-1 exposes documented public methods", async () => {
expect(typeof remote.check).toBe("function");
expect(typeof remote.publish).toBe("function");
expect(typeof remote.install).toBe("function");
expect(typeof remote.list).toBe("function");
expect(typeof remote.use).toBe("function");
expect(typeof remote.rollback).toBe("function");
expect(typeof remote.remove).toBe("function");
});
manualTest("1-2 check validates an available webdav client", async () => {
expect(await remote.check()).toBe(true);
});
manualTest("1-3 publish stores files under default group /modules/id", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
const projectDir = writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId });
await remote.publish(projectDir);
const metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" }));
const version = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/1.0/version.json`, { format: "text" }));
const artifact = await remote.client.getFileContents(`/modules/${projectId}/1.0/any/any/myapp.txt`, { format: "text" });
expect(metadata.project_id).toBe(projectId);
expect(metadata.group).toBe("/modules");
expect(metadata.version).toBe("1.0");
expect(metadata.versions).toBeUndefined();
expect(metadata.latest_version).toBeUndefined();
expect(version.artifacts[0].path).toBe("any/any/myapp.txt");
expect(artifact).toBe("hello v1");
}, 30000);
manualTest("1-4 publish supports custom nested group", async () => {
const projectId = nextProjectId();
const group = `/kit-manual-test/${runId}/docker/net`;
trackRemote(group, projectId);
await remote.publish(writeFixtureProject(tempDir, "2.0", "docker net", { id: projectId, group }));
const metadata = JSON.parse(await remote.client.getFileContents(`${group}/${projectId}/metadata.json`, { format: "text" }));
const artifact = await remote.client.getFileContents(`${group}/${projectId}/2.0/any/any/myapp.txt`, { format: "text" });
expect(metadata.group).toBe(group);
expect(metadata.version).toBe("2.0");
expect(artifact).toBe("docker net");
}, 30000);
manualTest("1-5 empty group stores project at repository root", async () => {
const projectId = nextProjectId();
trackRemote("", projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "root group", { id: projectId, group: "" }));
const metadata = JSON.parse(await remote.client.getFileContents(`/${projectId}/metadata.json`, { format: "text" }));
expect(metadata.group).toBe("/");
expect(await remote.client.getFileContents(`/${projectId}/1.0/any/any/myapp.txt`, { format: "text" })).toBe("root group");
}, 30000);
manualTest("1-6 explicit version fails when remote version already exists", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId }));
await expect(remote.publish(writeFixtureProject(tempDir, "1.0", "hello again", { id: projectId }))).rejects.toThrow("远程版本已存在");
}, 30000);
manualTest("1-7 blank version increments remote version tail or starts at 1.0", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
await remote.publish(writeFixtureProject(tempDir, "", "first auto", { id: projectId }));
let metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" }));
expect(metadata.version).toBe("1.0");
await remote.publish(writeFixtureProject(tempDir, "", "second auto", { id: projectId }));
metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" }));
expect(metadata.version).toBe("1.1");
expect(await remote.client.getFileContents(`/modules/${projectId}/1.1/any/any/myapp.txt`, { format: "text" })).toBe("second auto");
}, 30000);
manualTest("1-8 install downloads latest version from default group", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId }));
const installed = await remote.install(projectId);
const moduleRoot = path.resolve(localStore, "modules", projectId);
const installJson = JSON.parse(fs.readFileSync(path.resolve(moduleRoot, ".meta", "install.json"), "utf-8"));
const installedFile = fs.readFileSync(path.resolve(moduleRoot, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("hello v1");
expect(installJson.current_version).toBe("1.0");
expect(installJson.installed_versions).toContain("1.0");
expect(fs.existsSync(path.resolve(moduleRoot, "current"))).toBe(true);
}, 30000);
manualTest("1-9 install can target a custom group path", async () => {
const projectId = nextProjectId();
const group = `/kit-manual-test/${runId}/docker/net`;
trackRemote(group, projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "docker install", { id: projectId, group }));
const installed = await remote.install(`${group}/${projectId}`);
const installedFile = fs.readFileSync(path.resolve(localStore, "modules", projectId, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("docker install");
}, 30000);
manualTest("1-10 list use rollback and remove manage installed versions", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId }));
await remote.install(projectId);
await remote.publish(writeFixtureProject(tempDir, "2.0", "hello v2", { id: projectId }));
await remote.install(projectId);
expect(await remote.list(projectId)).toEqual(["1.0", "2.0"]);
await remote.rollback(projectId);
let installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("1.0");
await remote.use(`${projectId}@2.0`);
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("2.0");
await remote.remove(`${projectId}@1.0`);
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8"));
expect(installJson.installed_versions).toEqual(["2.0"]);
expect(fs.existsSync(path.resolve(localStore, "modules", projectId, "versions", "1.0"))).toBe(false);
}, 30000);
});
if (!RUN_MANUAL) {
describe("remote manual webdav tests", () => {
test.skip("set KIT_REMOTE_MANU_TEST=1 and run bun test test/remote_manu_test.js", () => {});
});
}
+278
View File
@@ -0,0 +1,278 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "fs";
import os from "os";
import path from "path";
import { Readable, Writable } from "stream";
import Remote from "../src/remote";
class MemoryWebdavClient {
constructor() {
this.files = new Map();
this.dirs = new Set(["/"]);
}
normalize(remotePath) {
const normalized = path.posix.normalize("/" + String(remotePath || "").replace(/\\/g, "/"));
return normalized === "/" ? "/" : normalized.replace(/\/$/, "");
}
async exists(remotePath) {
const key = this.normalize(remotePath);
return this.dirs.has(key) || this.files.has(key);
}
async createDirectory(remotePath) {
const key = this.normalize(remotePath);
const parts = key.split("/").filter(Boolean);
let current = "";
for (const part of parts) {
current += "/" + part;
this.dirs.add(current);
}
}
async getDirectoryContents(remotePath) {
const dir = this.normalize(remotePath);
const prefix = dir === "/" ? "/" : dir + "/";
const names = new Map();
for (const item of [...this.dirs, ...this.files.keys()]) {
if (item === dir || !item.startsWith(prefix)) {
continue;
}
const rest = item.slice(prefix.length);
const name = rest.split("/")[0];
names.set(name, prefix + name);
}
return [...names].map(([basename, filename]) => ({ basename, filename, type: this.dirs.has(this.normalize(filename)) ? "directory" : "file" }));
}
async putFileContents(remotePath, content) {
const key = this.normalize(remotePath);
await this.createDirectory(path.posix.dirname(key));
this.files.set(key, Buffer.isBuffer(content) ? content : Buffer.from(String(content)));
}
async getFileContents(remotePath, options = {}) {
const key = this.normalize(remotePath);
if (!this.files.has(key)) {
throw new Error(`missing ${key}`);
}
const data = this.files.get(key);
return options.format === "text" ? data.toString("utf-8") : data;
}
createWriteStream(remotePath) {
const key = this.normalize(remotePath);
const chunks = [];
const stream = new Writable({
write(chunk, encoding, callback) {
chunks.push(Buffer.from(chunk));
callback();
}
});
stream.on("finish", () => {
this.createDirectory(path.posix.dirname(key));
this.files.set(key, Buffer.concat(chunks));
});
return stream;
}
createReadStream(remotePath) {
const key = this.normalize(remotePath);
if (!this.files.has(key)) {
throw new Error(`missing ${key}`);
}
return Readable.from(this.files.get(key));
}
async stat(remotePath) {
const key = this.normalize(remotePath);
const data = this.files.get(key);
if (!data) {
throw new Error(`missing ${key}`);
}
return { size: data.length };
}
}
function writeFixtureProject(tempDir, version, content, group = "/modules") {
const projectDir = path.resolve(tempDir, `fixture-${version || "auto"}-${Math.random().toString(16).slice(2)}`);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.resolve(projectDir, "myapp.txt");
fs.writeFileSync(filePath, content, "utf-8");
fs.writeFileSync(path.resolve(projectDir, "meta.json"), JSON.stringify({
id: "myapp",
group,
version,
desc: "test app",
official_url: "https://example.test/myapp",
dist: [
{
path: filePath,
os: "any",
platform: "any",
filename: "myapp.txt",
type: "data"
}
]
}, null, 4), "utf-8");
return projectDir;
}
function createRemote() {
const remote = new Remote();
remote.client = new MemoryWebdavClient();
return remote;
}
let tempDir;
let localStore;
let oldLogger;
let oldConfig;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.resolve(os.tmpdir(), "kit-remote-test-"));
localStore = path.resolve(tempDir, "local-store");
oldLogger = globalThis.$logger;
oldConfig = globalThis.$sConfig;
globalThis.$logger = { info() {}, warn() {}, error() {}, debug() {} };
globalThis.$sConfig = { getLocalStorePath: () => localStore };
});
afterEach(() => {
globalThis.$logger = oldLogger;
globalThis.$sConfig = oldConfig;
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe("remote module public API", () => {
test("exposes documented public methods", () => {
const remote = createRemote();
expect(typeof remote.check).toBe("function");
expect(typeof remote.publish).toBe("function");
expect(typeof remote.install).toBe("function");
expect(typeof remote.list).toBe("function");
expect(typeof remote.use).toBe("function");
expect(typeof remote.rollback).toBe("function");
expect(typeof remote.remove).toBe("function");
});
test("check validates an available webdav client", async () => {
const remote = createRemote();
expect(await remote.check()).toBe(true);
});
test("publish stores files under default group /modules/id", async () => {
const remote = createRemote();
const projectDir = writeFixtureProject(tempDir, "1.0", "hello v1");
await remote.publish(projectDir);
const metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
const version = JSON.parse(await remote.client.getFileContents("/modules/myapp/1.0/version.json", { format: "text" }));
const artifact = await remote.client.getFileContents("/modules/myapp/1.0/any/any/myapp.txt", { format: "text" });
expect(metadata.project_id).toBe("myapp");
expect(metadata.group).toBe("/modules");
expect(metadata.version).toBe("1.0");
expect(metadata.versions).toBeUndefined();
expect(metadata.latest_version).toBeUndefined();
expect(version.artifacts[0].path).toBe("any/any/myapp.txt");
expect(artifact).toBe("hello v1");
});
test("publish supports custom nested group", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "2.0", "docker net", "/docker/net"));
const metadata = JSON.parse(await remote.client.getFileContents("/docker/net/myapp/metadata.json", { format: "text" }));
const artifact = await remote.client.getFileContents("/docker/net/myapp/2.0/any/any/myapp.txt", { format: "text" });
expect(metadata.group).toBe("/docker/net");
expect(metadata.version).toBe("2.0");
expect(artifact).toBe("docker net");
});
test("empty group stores project at repository root", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "root group", ""));
const metadata = JSON.parse(await remote.client.getFileContents("/myapp/metadata.json", { format: "text" }));
expect(metadata.group).toBe("/");
expect(await remote.client.getFileContents("/myapp/1.0/any/any/myapp.txt", { format: "text" })).toBe("root group");
});
test("explicit version fails when remote version already exists", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
await expect(remote.publish(writeFixtureProject(tempDir, "1.0", "hello again"))).rejects.toThrow("远程版本已存在");
});
test("blank version increments remote version tail or starts at 1.0", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "", "first auto"));
let metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
expect(metadata.version).toBe("1.0");
await remote.publish(writeFixtureProject(tempDir, "", "second auto"));
metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
expect(metadata.version).toBe("1.1");
expect(await remote.client.getFileContents("/modules/myapp/1.1/any/any/myapp.txt", { format: "text" })).toBe("second auto");
});
test("install downloads latest version from default group", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
const installed = await remote.install("myapp");
const moduleRoot = path.resolve(localStore, "modules", "myapp");
const installJson = JSON.parse(fs.readFileSync(path.resolve(moduleRoot, ".meta", "install.json"), "utf-8"));
const installedFile = fs.readFileSync(path.resolve(moduleRoot, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("hello v1");
expect(installJson.current_version).toBe("1.0");
expect(installJson.installed_versions).toContain("1.0");
expect(fs.existsSync(path.resolve(moduleRoot, "current"))).toBe(true);
});
test("install can target a custom group path", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "docker install", "/docker/net"));
const installed = await remote.install("/docker/net/myapp");
const installedFile = fs.readFileSync(path.resolve(localStore, "modules", "myapp", "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("docker install");
});
test("list use rollback and remove manage installed versions", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
await remote.install("myapp");
await remote.publish(writeFixtureProject(tempDir, "2.0", "hello v2"));
await remote.install("myapp");
expect(await remote.list("myapp")).toEqual(["1.0", "2.0"]);
await remote.rollback("myapp");
let installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("1.0");
await remote.use("myapp@2.0");
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("2.0");
await remote.remove("myapp@1.0");
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.installed_versions).toEqual(["2.0"]);
expect(fs.existsSync(path.resolve(localStore, "modules", "myapp", "versions", "1.0"))).toBe(false);
});
});
+42
View File
@@ -0,0 +1,42 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据(没有头尾)
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
// 转换为 hex
const sm2P7Hex = rs.hextorstr(rs.b64tohex(sm2P7Base64));
console.log('=== 使用 jsrsasign 测试 SM2 PKCS7 ===');
console.log('数据长度:', sm2P7Base64.length);
try {
// 使用 CMSParser 解析
const parser = new rs.KJUR.asn1.cms.CMSParser();
const result = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('\n解析成功!');
console.log('版本:', result.version);
console.log('哈希算法:', result.hashalgs);
console.log('内容类型:', result.econtent?.type);
console.log('证书数量:', result.certs?.length);
console.log('签名者数量:', result.sinfos?.length);
if (result.sinfos && result.sinfos.length > 0) {
const signer = result.sinfos[0];
console.log('\n签名者信息:');
console.log(' 版本:', signer.version);
console.log(' 哈希算法:', signer.hashalg);
console.log(' 签名算法:', signer.sigalg);
console.log(' 签名值 (hex):', signer.sighex?.substring(0, 50) + '...');
}
if (result.certs && result.certs.length > 0) {
console.log('\n证书信息:');
const cert = result.certs[0];
console.log(' 证书 PEM:', cert.substring(0, 100) + '...');
}
} catch (error) {
console.error('\n解析失败:', error.message);
console.error(error.stack);
}
+26
View File
@@ -0,0 +1,26 @@
const forge = require('node-forge');
// 用户提供的 SM2 PKCS7 签名数据(没有头尾)
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
// 包装成 PEM 格式
const pemData = `-----BEGIN PKCS7-----\n${sm2P7Base64}\n-----END PKCS7-----`;
console.log('=== 测试 SM2 PKCS7 数据 ===');
console.log('数据长度:', sm2P7Base64.length);
console.log('PEM 格式前200字符:', pemData.substring(0, 200));
try {
const p7 = forge.pkcs7.messageFromPem(pemData);
console.log('\n解析成功!');
console.log('类型:', p7.type);
console.log('证书数量:', p7.certificates ? p7.certificates.length : 0);
if (p7.certificates && p7.certificates.length > 0) {
const cert = p7.certificates[0];
console.log('证书主题:', cert.subject ? cert.subject.attributes.map(a => `${a.name}=${a.value}`).join(', ') : 'N/A');
console.log('证书颁发者:', cert.issuer ? cert.issuer.attributes.map(a => `${a.name}=${a.value}`).join(', ') : 'N/A');
}
} catch (error) {
console.error('\n解析失败:', error.message);
}
+32
View File
@@ -0,0 +1,32 @@
const crypto = require('../src/crypto.js');
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const rs = require('jsrsasign');
console.log('=== 使用真实原始数据验证 SM2 PKCS7 ===');
// 从 PKCS7 中提取原始数据
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
let realOriginalData = null;
if (p7.econtent && p7.econtent.hex) {
realOriginalData = Buffer.from(p7.econtent.hex, 'hex').toString();
console.log('从 PKCS7 提取的原始数据:', realOriginalData);
}
// 使用正确的原始数据进行验证
if (realOriginalData) {
console.log('\n使用提取的原始数据进行验证:');
const result = crypto.verifyPKCS7(sm2P7Base64, realOriginalData);
console.log('验证结果:', result);
} else {
console.log('无法从 PKCS7 中提取原始数据');
}
// 也测试一下不提供原始数据的情况(attach 模式)
console.log('\n使用 attach 模式(不提供原始数据):');
const result2 = crypto.verifyPKCS7(sm2P7Base64);
console.log('验证结果:', result2);
+91 -19
View File
@@ -1,7 +1,56 @@
const fs = require("fs");
const { pipeline } = require('node:stream/promises');
const { Transform } = require('stream');
const { pipeline } = require('node:stream/promises');
const paths = require('path');
function convertBytes(bytes) {
if (bytes >= 1024 * 1024 * 1024) {
const gigabytes = bytes / (1024 * 1024 * 1024);
return `${gigabytes.toFixed(2)}GB`;
} else if (bytes >= 1024 * 1024) {
const megabytes = bytes / (1024 * 1024);
return `${megabytes.toFixed(2)}MB`;
} else if (bytes >= 1024 ) {
const megabytes = bytes / (1024 );
return `${megabytes.toFixed(2)}KB`;
} else {
return `${bytes} B`;
}
}
// 创建一个用于统计字节数的Transform流
class ByteCounter extends Transform {
constructor(totalSize) {
super();
this.bytesCount = 0;
this.totalSize = totalSize;
this.timestamp = (new Date()).getTime()
this.recordBytesCount = 0;
this.speed = 0;
this.intervalId = setInterval(()=>{
let now = (new Date()).getTime()
let timecost = now - this.timestamp;
this.speed = ((this.bytesCount - this.recordBytesCount ) * 1000 / timecost).toFixed(2)
this.timestamp = now;
this.recordBytesCount = this.bytesCount;
}, 2000);
}
_transform(chunk, encoding, callback) {
this.bytesCount += chunk.length;
let process = (this.bytesCount * 100 / this.totalSize).toFixed(2)
console.log(`进度 ${process}%\t速度${ convertBytes(this.speed) }/s\t${convertBytes(this.bytesCount)} / ${convertBytes(this.totalSize)}`);
callback(null, chunk);
}
_flush(callback) {
if (this.intervalId) {
clearInterval(this.intervalId);
}
callback();
}
}
async function wUpload(client , from, to){
const totalSize = fs.statSync(from).size;
const rs = fs.createReadStream( from );
@@ -22,33 +71,53 @@ async function wUpload(client , from, to){
return pipeline( rs, ws );
}
/**
* 文件不存在会抛出异常
* @param client
* @param file
* @param to
* @returns {Promise<void>}
*/
async function wDownload(client, file, to){
// 确认文件存在以及大小
let stat = await client.stat(file);
let totalSize = stat.size;
if ( ! to ) {
to = paths.basename(file);
}
const byteCounter = new ByteCounter(totalSize);
await pipeline(
client.createReadStream(file),
byteCounter,
fs.createWriteStream(to)
);
await pipeline(rs , ws );
}
async function main(){
const webdav = await import("webdav");
const client = webdav.createClient(
"http://baishe.fullstack.club:5244/dav",
{
username: "cheney",
password: "722V587"
}
);
// const client = webdav.createClient(
// "http://172.16.17.230:5244/dav",
// "http://baishe.fullstack.club:5244/dav",
// {
// username: "kit",
// password: "kitkit"
// username: "cheney",
// password: "722V587"
// }
// );
const client = webdav.createClient(
"http://openlist.honor3.com/dav",
{
username: "sunyard",
password: "sunyard"
}
);
// Get directory contents
const directoryItems = await client.getDirectoryContents("/");
console.log(directoryItems)
// await pipeline(
// client.createReadStream(
// "/index.json"
// ),
// fs.createWriteStream("./xxx.json")
// );
// 下载测试
// await wDownload(client, "/kit")
// let rs = fs.createReadStream("D:\\fullstack\\TDevOps\\kit\\dist\\windows\\kit.exe")
// rs.on("data", (r)=>{
@@ -64,12 +133,15 @@ async function main(){
// ws.on("data", (r)=>{
// console.log("w", r.length)
// })
// await pipeline(rs , ws );
// console.log("end")
await wUpload( client, "./kit", "/kit" )
// await wUpload( client, "D:\\fullstack\\TDevOps\\Readme.md", "/test/case1/data/20250313111818/Readme.md2" )
// await wUpload( client, "../dist/windows/kit.exe", "/modules/kit/0.1.20240227094429/Windows/X86_64/kit.exe" )
console.log("over")
}
//kit3 config remoteAuth sunyard:sunyard
//kit3 config remoteServer http://openlist.honor3.com/dav
main()
+26 -20
View File
@@ -1,21 +1,27 @@
{
"id" : "kit", // 项目 ID
"name" : "kit 辅助工具", // 项目显示名称
"desc" : "手边的开发运维小工具", // 项目描述
"type" : "app", // 类型参考类型表;
"vcs" : "folder", // 版本管理方式
"version": "0.1", // 当前版本号,只包含两位,其他部分自动生成
// "platform" : "cross", // cross=跨平台;[X86_64, ARM64]
// "os" : "cross", // cross=跨操作系统;[Windows, Linux, Alpine]
"dist": [{
platform : "X86_64",
os : "Windows",
path: "kit/dist/windows/kit.exe", // 本地存储路径
"filename" : "kit.exe"
}, {
platform : "X86_64",
os : "Linux",
path: "kit/dist/linux/kit", // 本地存储路径
"filename" : "kit"
}]
{
"id" : "kit", // 项目 ID
"name" : "kit 辅助工具", // 项目显示名称
"desc" : "手边的开发运维小工具", // 项目描述
"type" : "app", // 类型参考类型表;
"vcs" : "folder", // 版本管理方式
"version": "0.1", // 当前版本号,只包含两位,其他部分自动生成
// "platform" : "cross", // cross=跨平台;[X86_64, ARM64]
// "os" : "cross", // cross=跨操作系统;[Windows, Linux, Alpine]
"versions": [{
"version" : "0.1",
"match" : {
"x64|win32" : {
"path": "data/windows/kit.exe",
"filename" : "kit.exe"
},
"x64|linux" : {
"path": "data/linux/kit",
"filename" : "kit"
},
"x64|linux|kylin V10" : {
"path": "data/kylin/kit.zip",
"filename" : "kit"
}
}
}]
}
@@ -1,12 +0,0 @@
---
slug: first-blog-post
title: First Blog Post
authors:
name: Gao Wei
title: Docusaurus Core Team
url: https://github.com/wgao19
image_url: https://github.com/wgao19.png
tags: [hola, docusaurus]
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
-44
View File
@@ -1,44 +0,0 @@
---
slug: long-blog-post
title: Long Blog Post
authors: endi
tags: [hello, docusaurus]
---
This is the summary of a very long blog post,
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
<!--truncate-->
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
-20
View File
@@ -1,20 +0,0 @@
---
slug: mdx-blog-post
title: MDX Blog Post
authors: [slorber]
tags: [docusaurus]
---
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
:::tip
Use the power of React to create interactive blog posts.
```js
<button onClick={() => alert('button clicked!')}>Click me!</button>
```
<button onClick={() => alert('button clicked!')}>Click me!</button>
:::
Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

-25
View File
@@ -1,25 +0,0 @@
---
slug: welcome
title: Welcome
authors: [slorber, yangshun]
tags: [facebook, hello, docusaurus]
---
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
Simply add Markdown files (or folders) to the `blog` directory.
Regular blog authors can be added to `authors.yml`.
The blog post date can be extracted from filenames, such as:
- `2019-05-30-welcome.md`
- `2019-05-30-welcome/index.md`
A blog post folder can be convenient to co-locate blog post images:
![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg)
The blog supports tags as well!
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
-17
View File
@@ -1,17 +0,0 @@
endi:
name: Endilie Yacop Sucipto
title: Maintainer of Docusaurus
url: https://github.com/endiliey
image_url: https://github.com/endiliey.png
yangshun:
name: Yangshun Tay
title: Front End Engineer @ Facebook
url: https://github.com/yangshun
image_url: https://github.com/yangshun.png
slorber:
name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
+75
View File
@@ -0,0 +1,75 @@
---
sidebar_position: 2
---
# 功能概览
kit 提供命令行工具、脚本组装、运维管理三大核心能力。
## 命令行工具
kit 内置跨平台的常用命令,解决 Windows/Linux 脚本兼容性问题:
| 命令 | 说明 |
|------|------|
| `info` | 查看系统信息 |
| `mkdir` | 创建目录 |
| `rm` | 删除文件/目录 |
| `cp` | 复制文件 |
| `mv` | 移动文件 |
| `zip` | 压缩/解压 |
| `md5` | 计算 MD5 |
| `readJson` | 读取 JSON 文件 |
| `writeJson` | 写入 JSON 文件 |
## 远程操作
| 命令 | 说明 |
|------|------|
| `scp` | 跨主机文件传输 |
| `remoteShell` | SSH 远程执行命令 |
## 模块管理
kit 可作为模块管理器,从远程仓库安装、升级、卸载工具:
```shell
# 安装模块
kit install myapp
# 指定版本安装
kit install myapp@1.0
# 查看已安装版本
kit list myapp
# 切换版本
kit use myapp@1.0
# 回滚到上一个版本
kit rollback myapp
# 删除版本
kit remove myapp@1.0
```
## 脚本组装
kit 支持通过脚本文件(`kitx.js`)组合各种能力,实现工作流自动化:
```js
// kitx.js
async function main() {
console.log("开始部署...");
mkdir("./dist");
cp("./src/*", "./dist/");
scp("./dist/", "server:/app/");
remoteShell({host: "server"}, "cd /app && restart.sh");
}
```
运行脚本:
```shell
kit run
```
+29 -4
View File
@@ -4,19 +4,44 @@ sidebar_position: 1
# 安装
kit 支持 Windows(x86_64)、Linux(x86_64)、 Linux(arm64)、 Alpine 等多个执行平台。
kit 是一个跨平台命令行工具,支持以下平台:
下载地址:http://baishe.fullstack.club:5244/
| 平台 | 架构 | 说明 |
|------|------|------|
| Windows | x86_64 | `kit.exe` |
| Linux | x86_64 | 现代 glibc(Ubuntu 20.04+) |
| Linux | x86_64 | Kylin V10 低版本 glibc |
## 下载
前往首页下载对应平台的二进制文件:
- **Windows**: [kit.exe](https://pub-98ee77ce79984c2cad0dc1c300901158.r2.dev/data/windows/kit.exe)
- **Linux (现代 glibc)**: [kit](https://pub-98ee77ce79984c2cad0dc1c300901158.r2.dev/data/linux/kit)
- **Linux (Kylin V10)**: [kit.zip](https://pub-98ee77ce79984c2cad0dc1c300901158.r2.dev/data/kylin/kit.zip)
下载后将可执行文件添加到系统 `PATH` 中即可使用。
## 远程仓库
kit 支持从远程仓库安装和管理模块。配置仓库地址:
```shell
kit config remoteServer http://baishe.fullstack.club:5244/dav
kit config remoteAuth cheney@722V587
```
## 信雅达内部仓库
### 信雅达内部仓库
```shell
kit config remoteServer http://172.16.17.230:5244/dav
kit config remoteAuth kit@kitkit
```
```
## 验证
配置完成后,运行以下命令验证仓库连接:
```shell
kit remote
```
+7 -3
View File
@@ -1,6 +1,10 @@
---
sidebar_position: 1
sidebar_position: 3
---
# 更多
等多文档请移步 -> **[Kit 辅助开发工具](https://flowus.cn/fullstack/share/9e90b5b7-cea7-44d7-9231-e5d8a6c807f9)**
# 更多文档
详细的 API 文档和使用示例,请参考:
- **在线文档**: [Kit 辅助开发工具](https://flowus.cn/fullstack/share/9e90b5b7-cea7-44d7-9231-e5d8a6c807f9)
- **代码仓库**: [git.honor3.com/Fullstack/kit.program](http://git.honor3.com/Fullstack/kit.program)
+116
View File
@@ -0,0 +1,116 @@
---
sidebar_position: 3
---
# Registry 配置管理
kit 可对接 [Registry](https://git.honor3.com/Fullstack/registry) 配置管理中心,通过命令行管理微服务配置。
## 配置连接
使用 `kit config` 设置 Registry 地址和认证信息:
```shell
# 设置 Registry 服务地址
kit config registryUrl http://your-registry-host:8008
# 设置认证 Token(Registry 设置中获取)
kit config registryToken your-token-here
```
验证连接:
```shell
kit registry list
```
## 查看项目
```shell
# 列出所有项目
kit registry list
# 输出示例:
# ID 名称 维度
# proj-abc-123 CommonExternalService IDC=✓ Env=✓ Group=✓
# proj-def-456 OrderService IDC=✗ Env=✓ Group=✓
```
## 管理配置
`kit registry config` 支持查看和修改配置项。
### 查看配置
```shell
# 查看整个项目的所有配置
kit registry config CommonExternalService
# 查看指定路径下的配置
kit registry config CommonExternalService:data/windows
# 获取单个配置值
kit registry config CommonExternalService:CF_R2_BUCKET
```
### 设置配置值
```shell
kit registry config CommonExternalService:CF_R2_BUCKET new-value
```
### 路径语法
`project:path` 中的 `path` 对应配置的路径前缀:
| 语法 | 含义 |
|------|------|
| `project` | 整个项目 |
| `project:data` | 路径以 `data` 开头的配置 |
| `project:data/windows` | 路径以 `data/windows` 开头的配置 |
| `project:data/windows/kit.exe` | 精确匹配单个配置 |
## 导入导出
### 导出
```shell
# 导出整个项目到文件
kit registry export CommonExternalService ./backup.json
# 仅导出指定路径下的配置
kit registry export CommonExternalService:data/windows ./backup-windows.json
```
导出文件包含项目名、路径、时间戳和配置列表:
```json
{
"project": "CommonExternalService",
"path": "data/windows",
"exportedAt": "2026-09-11T02:00:00Z",
"configs": [
{
"key": "kit.exe",
"value": "https://pub-xxx.r2.dev/data/windows/kit.exe",
"type": "string",
"description": "Windows 二进制下载地址"
}
]
}
```
### 导入
```shell
# 从备份文件导入
kit registry import CommonExternalService ./backup.json
# 导入到指定路径下
kit registry import CommonExternalService:data ./backup.json
```
导入规则:
- 已存在的 key → 更新值
- 不存在的 key → 新增
- 远程有但本地没有的 → 保留不动
+31 -40
View File
@@ -1,8 +1,4 @@
// @ts-check
// `@type` JSDoc annotations allow editor autocompletion and type checking
// (when paired with `@ts-check`).
// There are various equivalent ways to declare your Docusaurus config.
// See: https://docusaurus.io/docs/api/docusaurus-config
import {themes as prismThemes} from 'prism-react-renderer';
@@ -12,23 +8,15 @@ const config = {
tagline: '手边的开发运维工具',
favicon: 'img/favicon.ico',
// Set the production url of your site here
url: 'http://kit.fullstack.club',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name.
organizationName: 'Fullstack',
projectName: 'kit.program',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'zh',
locales: ['zh'],
@@ -41,18 +29,8 @@ const config = {
({
docs: {
sidebarPath: './sidebars.js',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
blog: {
showReadingTime: true,
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
blog: false,
theme: {
customCss: './src/css/custom.css',
},
@@ -63,45 +41,57 @@ const config = {
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
navbar: {
title: 'Kit 项目主页',
title: 'Kit',
logo: {
alt: 'kit logo is still in production',
alt: 'Kit logo',
src: 'img/logo.svg',
},
items: [
{ href: 'https://flowus.cn/fullstack/share/9e90b5b7-cea7-44d7-9231-e5d8a6c807f9', label: '文档', position: 'right'}
{
type: 'docSidebar',
sidebarId: 'tutorialSidebar',
position: 'left',
label: '文档',
},
{
href: 'http://git.honor3.com/Fullstack/kit.program',
label: '仓库',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'More',
title: 'Kit',
items: [
{
label: '代码仓库',
href: 'https://gitee.com/fullstackclub/kit.git',
label: '下载',
to: '/',
},
{
label: '开发者博客',
to: 'http://gpio.me',
label: '文档',
to: '/docs/intro',
},
{
label: '反馈',
to: 'mailto:hq@gpio.me',
label: '功能概览',
to: '/docs/features',
},
],
},
{
title: 'Docs',
title: '社区',
items: [
{
label: '文档',
to: 'https://flowus.cn/fullstack/share/9e90b5b7-cea7-44d7-9231-e5d8a6c807f9',
label: '代码仓库',
href: 'http://git.honor3.com/Fullstack/kit.program',
},
{
label: '反馈',
href: 'mailto:hq@gpio.me',
},
],
},
@@ -111,6 +101,7 @@ const config = {
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
additionalLanguages: ['bash'],
},
}),
};
+3 -25
View File
@@ -1,33 +1,11 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
'features',
'registry',
'more',
],
*/
};
export default sidebars;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
.downloads {
display: flex;
align-items: center;
padding: 2rem 0 4rem;
width: 100%;
}
.downloadCard {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
transition: box-shadow 0.2s;
}
.downloadCard:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.iconWrap {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.5rem;
color: var(--ifm-color-primary);
}
.filename {
font-family: var(--ifm-font-family-monospace);
font-size: 0.85rem;
color: var(--ifm-color-emphasis-600);
margin: 0;
}
+3 -1
View File
@@ -3,6 +3,7 @@ import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures';
import DownloadSection from '@site/src/components/DownloadSection';
import Heading from '@theme/Heading';
import styles from './index.module.css';
@@ -33,10 +34,11 @@ export default function Home() {
return (
<Layout
title={`${siteConfig.title}`}
description="Description will go into a meta tag in <head />">
description="手边的开发运维工具">
<HomepageHeader />
<main>
<HomepageFeatures />
<DownloadSection />
</main>
</Layout>
);

Some files were not shown because too many files have changed in this diff Show More