e10-data 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/bin/e10-data.mjs +121 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ e10-data noun list --skill <skillKey> 词库查询(可带 --s
|
|
|
21
21
|
e10-data field id2name|name2id 关联字段翻译(testRemoteBrowser / complete)
|
|
22
22
|
e10-data conf get|batch|batch-file|resolve|resolve-file|add|add-file|update|delete 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连;confType 默认按 CONN_/DS_/WF_ 前缀推导,可 --type 显式)
|
|
23
23
|
e10-data exp status|manifest|commits|file|pull|init|commit|lock|release|rollback 专家包版本管理
|
|
24
|
+
e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假等,v0.1.5 新增)
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
## 通用约定
|
|
@@ -47,5 +48,35 @@ e10-data conf resolve '{"connId":"#{CONN_EC_EDCAPP}"}' # 占位
|
|
|
47
48
|
e10-data conf add CONN_EC_EDCAPP --value 10 --name "说明" --attrs '{"schema":"ec_edcapp"}'
|
|
48
49
|
e10-data conf delete CONN_EC_EDCAPP
|
|
49
50
|
e10-data exp status --expert data-assistant
|
|
50
|
-
e10-data exp pull --expert data-assistant --dir <本地专家包目录> --dry-run
|
|
51
|
+
e10-data exp pull --expert data-assistant --dir <本地专家包目录> [--dry-run] [--force]
|
|
52
|
+
e10-data attend statis # 查当前登录用户当月假期余额(病假/事假/年假)
|
|
53
|
+
e10-data attend statis --user 6264843388578732287 --begin 2026-09-01 --end 2026-09-30
|
|
51
54
|
```
|
|
55
|
+
|
|
56
|
+
## exp pull 智能拉取(v0.1.4 新增,防本地修改被覆盖)`exp pull` 不再盲目覆盖。CLI 在专家包本地目录维护基线文件 `.sync-state.json`(记录上次同步的 commitId/headRevision 与每文件 sha256),拉取时逐文件三方对比:
|
|
57
|
+
|
|
58
|
+
| 场景 | 判定 | 处理 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| 仅远端改(本地未动) | 本地 sha == 基线 sha ≠ 远端 sha | 直接下载覆盖(安全) |
|
|
61
|
+
| 仅本地改(远端未动) | 本地 sha ≠ 基线 sha == 远端 sha | **保留本地,不覆盖** |
|
|
62
|
+
| 双方都改 / 无基线可判 | 本地、基线、远端互不相同 | **先备份本地副本到 `<专家包目录>.pull-backup-<时间戳>/`,再下载覆盖** |
|
|
63
|
+
| 本地缺失 | manifest 有、本地无 | 直接下载(新增/恢复) |
|
|
64
|
+
|
|
65
|
+
- 拉取成功后将 `.sync-state.json` 刷新到新 head,后续判定持续准确;
|
|
66
|
+
- `--force`:一律以远端覆盖(放弃本地修改,不备份);
|
|
67
|
+
- `--dry-run`:只预览决策,不写盘不下载;
|
|
68
|
+
- `.sync-state.json` 为本地同步元文件,**永不随包提交**(`walkDir`/`collectPackage` 已排除);`exp commit` 成功后也会自动刷新该基线。
|
|
69
|
+
|
|
70
|
+
## attend statis 考勤假期余额(v0.1.5 新增)
|
|
71
|
+
|
|
72
|
+
封装 E10 考勤 REST 接口 `POST /api/attend/web/attendInfoV2/getAttendInfoStatis`(hr-attendance 场景六:病假/事假/年假余额统计,替代原手工 curl)。认证/绕代理由 CLI 进程内自动完成(读 `~/.workbuddy/e10-login-cache.md`)。
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
e10-data attend statis [--user <人员ID>] [--begin 2026-09-01] [--end 2026-09-30]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `--user`:目标人员 ID;**缺省 = 当前登录用户**(查他人前需先 HRM 下属校验,见 hr-attendance Skill);
|
|
79
|
+
- `--begin` / `--end`:统计月份范围,**缺省 = 当前月 1 日 ~ 月末**(接口按整月统计,时间范围不影响假期余额口径);
|
|
80
|
+
- `--json`:原样输出接口 JSON(含 `data.attendStatis` 出勤统计 / `data.attendSummary` 汇总 / `data.leaveInfo` 请假 / `data.outAttend` 外出 / `data.vacationBalanceList` 假期余额);
|
|
81
|
+
- 默认人读输出:仅列出 `data.vacationBalanceList`(按 title 区分假期类型)——带薪病假 `100584850000000003`、带薪事假 `100584850000000010` 含 `lastRemaining`(历年可请)/`remaining`(今年可请)/`total`(总计可请) 三项。
|
|
82
|
+
|
package/bin/e10-data.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* e10-data field id2name|name2id 关联字段翻译 testRemoteBrowser / complete
|
|
12
12
|
* e10-data conf get|batch|batch-file|resolve|resolve-file|add|add-file|update|delete
|
|
13
13
|
* 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连)
|
|
14
|
+
* e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假,2026-09-07)
|
|
14
15
|
*
|
|
15
16
|
* 通用:--json 原样输出;复杂 JSON 参数支持 @文件 或 内联(中文走 @文件 更稳)
|
|
16
17
|
* 认证:进程内读取用户级登录缓存 ~/.workbuddy/e10-login-cache.md;不存在/失效提示先 e10-login
|
|
@@ -679,6 +680,44 @@ async function cmdConf(sub, args, json, pos) {
|
|
|
679
680
|
}
|
|
680
681
|
}
|
|
681
682
|
|
|
683
|
+
// ---------------- attend:考勤 REST 接口 ----------------
|
|
684
|
+
// 2026-09-07:封装 /api/attend/web/attendInfoV2/getAttendInfoStatis(假期余额统计,hr-attendance 场景六病假/事假)。
|
|
685
|
+
// 请求参数 {beginDate, endDate, userId};返回 data.vacationBalanceList[]:{id,title,data:[{name,value,key}]},
|
|
686
|
+
// key 含 lastRemaining(历年可请)/remaining(今年可请)/total(总计可请)。
|
|
687
|
+
function defaultMonthRange() {
|
|
688
|
+
const now = new Date();
|
|
689
|
+
const y = now.getFullYear();
|
|
690
|
+
const m = now.getMonth() + 1;
|
|
691
|
+
const lastDay = new Date(y, m, 0).getDate();
|
|
692
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
693
|
+
return { begin: y + "-" + p(m) + "-01", end: y + "-" + p(m) + "-" + p(lastDay) };
|
|
694
|
+
}
|
|
695
|
+
async function cmdAttendStatis(args, json) {
|
|
696
|
+
const user = args.user || auth.userId || "";
|
|
697
|
+
if (!user) throw new Error("缺少查询人员:--user <人员ID>(查本人可不传,默认当前登录用户)");
|
|
698
|
+
const def = defaultMonthRange();
|
|
699
|
+
const body = { beginDate: args.begin || def.begin, endDate: args.end || def.end, userId: user };
|
|
700
|
+
const r = await postJson("/api/attend/web/attendInfoV2/getAttendInfoStatis", body);
|
|
701
|
+
if (json) return r;
|
|
702
|
+
if (String(r.code) !== "200") {
|
|
703
|
+
console.log("code=" + r.code + " msg=" + (r.msg || ""));
|
|
704
|
+
if (/没有权限|无权限|权限不足|未授权|无权访问|无查询权限/i.test(String(r.msg || ""))) {
|
|
705
|
+
throw new Error("当前用户对该人员考勤数据无查询权限,请检查 ETEAMSID 或联系管理员开通");
|
|
706
|
+
}
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const d = r.data || {};
|
|
710
|
+
const list = Array.isArray(d.vacationBalanceList) ? d.vacationBalanceList : [];
|
|
711
|
+
if (!list.length) { console.log("(空:该人员无假期余额数据) userId=" + user + " " + body.beginDate + "~" + body.endDate); return; }
|
|
712
|
+
console.log("== 考勤假期余额 userId=" + user + "(" + body.beginDate + " ~ " + body.endDate + ")==");
|
|
713
|
+
for (const item of list) {
|
|
714
|
+
console.log((item.title || item.id || "?") + (item.id ? " [" + item.id + "]" : ""));
|
|
715
|
+
if (Array.isArray(item.data) && item.data.length) {
|
|
716
|
+
console.log(" " + item.data.map((x) => (x.name || x.key) + "=" + (x.value ?? "")).join(" "));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
682
721
|
// ---------------- exp:专家包版本管理(expert-repository) ----------------
|
|
683
722
|
async function expGet(url) {
|
|
684
723
|
// 返回 {raw, j}:raw 用于正则提取大整数 id(commitId 19 位超 JS Number 精度,JSON.parse 会丢精度!)
|
|
@@ -705,6 +744,7 @@ function rawNumSeq(raw, key) {
|
|
|
705
744
|
}
|
|
706
745
|
const EXP_ALLOWED_DIRS = [".codebuddy-plugin", "agents", "avatars", "skills"];
|
|
707
746
|
const EXP_SKIP_DIRS = [".git", ".workbuddy", "node_modules", "__pycache__"];
|
|
747
|
+
const EXP_SKIP_FILES = [".sync-state.json"]; // 本地同步状态元文件(含基线 sha),永不随包提交
|
|
708
748
|
function sha256(buf) { return crypto.createHash("sha256").update(buf).digest("hex"); }
|
|
709
749
|
function walkDir(dir, root) {
|
|
710
750
|
const out = [];
|
|
@@ -717,6 +757,7 @@ function walkDir(dir, root) {
|
|
|
717
757
|
if (rel.includes("/") && !EXP_ALLOWED_DIRS.includes(top)) continue; // 仅允许根文件+四大目录
|
|
718
758
|
walkDir(abs, root).forEach((x) => out.push(x));
|
|
719
759
|
} else if (entry.isFile()) {
|
|
760
|
+
if (EXP_SKIP_FILES.includes(entry.name)) continue; // .sync-state.json 不入包
|
|
720
761
|
const top = rel.split("/")[0];
|
|
721
762
|
if (!rel.includes("/") || EXP_ALLOWED_DIRS.includes(top)) out.push({ rel, abs });
|
|
722
763
|
}
|
|
@@ -803,37 +844,96 @@ async function expFile(args, json) {
|
|
|
803
844
|
if (json) { console.log(JSON.stringify({ relativePath: rp, size: buf.length, base64: buf.toString("base64") })); return; }
|
|
804
845
|
process.stdout.write(buf.toString("utf-8"));
|
|
805
846
|
}
|
|
847
|
+
// ---- exp 智能拉取:基线状态文件(三方差异保护,2026-09-04 实现)----
|
|
848
|
+
// .sync-state.json 记录上次成功同步的 commitId/revision 与每文件 sha256(基线)。
|
|
849
|
+
// pull 逐文件三方对比:local(当前本地) vs base(上次同步基线) vs remote(远端 manifest),
|
|
850
|
+
// 决策:仅远端改→覆盖;仅本地改→保留本地;双方都改→备份本地副本后覆盖(备份目录保留本地版);
|
|
851
|
+
// 无基线文件/基线无记录→保守备份后覆盖。拉取成功后把 .sync-state.json 更新为新 head。
|
|
852
|
+
function syncStatePath(dir) { return path.join(dir, ".sync-state.json"); }
|
|
853
|
+
function readSyncState(dir) {
|
|
854
|
+
const p = syncStatePath(dir);
|
|
855
|
+
if (!fs.existsSync(p)) return null;
|
|
856
|
+
try {
|
|
857
|
+
const j = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
858
|
+
const files = {};
|
|
859
|
+
for (const [rel, v] of Object.entries(j.files || {})) files[rel] = (typeof v === "string") ? v : (v.sha256 || "");
|
|
860
|
+
return { commitId: String(j.commitId || ""), headRevision: j.headRevision, files };
|
|
861
|
+
} catch { return null; }
|
|
862
|
+
}
|
|
863
|
+
function writeSyncState(dir, commitId, headRevision, fileShaMap) {
|
|
864
|
+
const files = {};
|
|
865
|
+
for (const [rel, sha] of Object.entries(fileShaMap)) files[rel] = { sha256: sha };
|
|
866
|
+
const obj = { commitId: String(commitId), headRevision, files, updateTime: new Date().toISOString() };
|
|
867
|
+
fs.writeFileSync(syncStatePath(dir), JSON.stringify(obj, null, 1), "utf-8");
|
|
868
|
+
}
|
|
806
869
|
async function expPull(args, json) {
|
|
807
870
|
const expert = need("expert", args), dir = need("dir", args);
|
|
808
871
|
const dry = !!args["dry-run"];
|
|
872
|
+
const force = !!args["force"]; // --force = 一律以远端覆盖(放弃本地修改)
|
|
809
873
|
const st = await expGet("/api/dw/expert-repository/v1/repositories/status?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
|
|
810
874
|
if (!expOk(st.j)) throw new Error(expFailMsg(st.j));
|
|
811
875
|
const headId = rawNum(st.raw, "headCommitId") || String((st.j.data || {}).headCommitId);
|
|
876
|
+
const headRev = (st.j.data || {}).headRevision;
|
|
812
877
|
const mf = await expGet("/api/dw/expert-repository/v1/repositories/manifest?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
|
|
813
878
|
if (!expOk(mf.j)) throw new Error(expFailMsg(mf.j));
|
|
814
879
|
const items = (mf.j.data || []).filter((f) => f.relativePath && f.relativePath !== "DELETE");
|
|
815
880
|
const local = fs.existsSync(dir) ? localShaMap(dir) : {};
|
|
816
|
-
|
|
817
|
-
const
|
|
881
|
+
const base = readSyncState(dir); // 基线:上次同步后的文件 sha(无则 null)
|
|
882
|
+
const baseCommit = (base && base.commitId) || "";
|
|
883
|
+
// 基线有效判定:上次同步 commitId 须在提交历史上(取不到历史就保守按无基线处理)
|
|
884
|
+
let backupRoot = "";
|
|
885
|
+
let downloaded = 0, unchanged = 0, preserved = 0, backed = 0, missing = 0;
|
|
886
|
+
const plan = []; // {rel, action, reason}
|
|
887
|
+
const mkBackupRoot = () => {
|
|
888
|
+
if (backupRoot) return backupRoot;
|
|
889
|
+
const absDir = path.resolve(dir);
|
|
890
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
891
|
+
backupRoot = path.join(path.dirname(absDir), path.basename(absDir) + ".pull-backup-" + ts);
|
|
892
|
+
fs.mkdirSync(backupRoot, { recursive: true });
|
|
893
|
+
return backupRoot;
|
|
894
|
+
};
|
|
818
895
|
for (const it of items) {
|
|
819
896
|
const lp = path.join(dir, it.relativePath.split("/").join(path.sep));
|
|
820
897
|
const ls = local[it.relativePath];
|
|
898
|
+
const baseSha = (base && base.files) ? base.files[it.relativePath] : undefined;
|
|
821
899
|
if (ls === it.sha256) { unchanged++; continue; }
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
900
|
+
if (ls === undefined) { // 本地缺失 → 直接下载(新增/恢复)
|
|
901
|
+
missing++; plan.push({ rel: it.relativePath, action: dry ? "待下载" : "已下载", reason: "本地缺失" });
|
|
902
|
+
} else if (!force && baseSha !== undefined && ls === baseSha && baseSha !== it.sha256) {
|
|
903
|
+
// 仅远端改(本地未动,与基线一致)→ 覆盖
|
|
904
|
+
plan.push({ rel: it.relativePath, action: dry ? "待下载" : "已下载", reason: "远端更新(本地未动)" });
|
|
905
|
+
} else if (!force && baseSha !== undefined && baseSha === it.sha256) {
|
|
906
|
+
// 仅本地改(远端=基线,本地≠基线)→ 保留本地,不覆盖
|
|
907
|
+
preserved++; plan.push({ rel: it.relativePath, action: "保留本地", reason: "本地修改(远端未动)" });
|
|
908
|
+
continue;
|
|
909
|
+
} else { // 双方都改 / 无基线可判 / --force → 先备份本地副本再覆盖
|
|
910
|
+
backed++;
|
|
911
|
+
if (!dry && !force) {
|
|
912
|
+
const bdir = path.join(mkBackupRoot(), it.relativePath.split("/").join(path.sep));
|
|
913
|
+
fs.mkdirSync(path.dirname(bdir), { recursive: true });
|
|
914
|
+
fs.copyFileSync(lp, bdir);
|
|
915
|
+
}
|
|
916
|
+
plan.push({ rel: it.relativePath, action: dry ? "待下载(将备份本地)" : (force ? "已下载(force覆盖)" : "已下载(本地已备份)"), reason: (force ? "force 覆盖" : (baseSha === undefined ? "无基线记录" : "双方都改")) });
|
|
917
|
+
}
|
|
825
918
|
if (dry) continue;
|
|
826
919
|
fs.mkdirSync(path.dirname(lp), { recursive: true });
|
|
827
920
|
const buf = await rawGet("/api/dw/expert-repository/v1/repositories/file?expertCode=" + encodeURIComponent(expert) + "&commitId=" + encodeURIComponent(headId) + "&relativePath=" + encodeURIComponent(it.relativePath));
|
|
828
921
|
fs.writeFileSync(lp, buf);
|
|
829
922
|
downloaded++;
|
|
830
923
|
}
|
|
831
|
-
|
|
924
|
+
// 拉取成功后更新基线状态文件(记录新 head + 本地最终文件 sha)
|
|
925
|
+
if (!dry) {
|
|
926
|
+
const finalLocal = localShaMap(dir);
|
|
927
|
+
writeSyncState(dir, headId, headRev, finalLocal);
|
|
928
|
+
}
|
|
929
|
+
const out = { expert, headCommitId: headId, headRevision: headRev, total: items.length, unchanged, downloaded, preserved, backed, missing, dryRun: !!dry, backupDir: backupRoot || undefined };
|
|
832
930
|
if (json) { console.log(JSON.stringify(out)); return; }
|
|
833
931
|
console.log("== pull " + expert + "@" + headId + (dry ? " (dry-run)" : "") + " ==");
|
|
834
|
-
console.log("清单 " + out.total + " 项 | 未变 " + unchanged + " |
|
|
835
|
-
for (const p of plan.slice(0,
|
|
836
|
-
if (plan.length >
|
|
932
|
+
console.log("清单 " + out.total + " 项 | 未变 " + unchanged + " | 已下载 " + downloaded + " | 保留本地 " + preserved + " | 覆盖前备份 " + backed + " | 缺失 " + missing);
|
|
933
|
+
for (const p of plan.slice(0, 40)) console.log(" [" + p.action + "] " + p.rel + (p.reason ? "(" + p.reason + ")" : ""));
|
|
934
|
+
if (plan.length > 40) console.log(" ... 共 " + plan.length + " 个文件");
|
|
935
|
+
if (backupRoot && !dry) console.log("本地修改副本备份于: " + backupRoot);
|
|
936
|
+
if (preserved > 0 && !dry) console.log("⚠️ " + preserved + " 个文件因含本地修改被保留(远端未改);如需强制覆盖请加 --force");
|
|
837
937
|
}
|
|
838
938
|
async function expInit(args, json) {
|
|
839
939
|
const expert = need("expert", args), dir = need("dir", args);
|
|
@@ -882,6 +982,12 @@ async function expCommit(args, json) {
|
|
|
882
982
|
if (/基线|过期|revision|冲突/i.test(m)) throw new Error("提交冲突: " + m + " —— 请先 exp pull 拉取最新并合并后重试");
|
|
883
983
|
throw new Error("提交失败: " + m);
|
|
884
984
|
}
|
|
985
|
+
// 提交成功后把本地 .sync-state.json 基线刷新到新 head(与本地文件 sha 一致),保证后续 pull 三方判定准确
|
|
986
|
+
const rj = r.data || {};
|
|
987
|
+
const newHead = rawNum(JSON.stringify(r), "headCommitId") || rawNum(JSON.stringify(r), "commitId") || rj.headCommitId || rj.commitId || "";
|
|
988
|
+
if (newHead && fs.existsSync(dir)) {
|
|
989
|
+
try { writeSyncState(dir, newHead, rj.headRevision ?? rj.revision ?? 0, localShaMap(dir)); } catch {}
|
|
990
|
+
}
|
|
885
991
|
console.log("== 提交成功 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
|
|
886
992
|
}
|
|
887
993
|
async function expLock(args, json) {
|
|
@@ -975,12 +1081,14 @@ exp 专家包版本管理(expert-repository/v1,响应以 data.actionMsg.
|
|
|
975
1081
|
exp manifest --expert <code> [--commit <id>] # 文件清单(sha256)
|
|
976
1082
|
exp commits --expert <code> # 提交历史
|
|
977
1083
|
exp file --expert <code> --commit <id> --path <相对路径> [--out 本地文件]
|
|
978
|
-
exp pull --expert <code> --dir <本地专家包目录> [--dry-run]
|
|
1084
|
+
exp pull --expert <code> --dir <本地专家包目录> [--dry-run] [--force] # 智能拉取:基线三方对比(仅远端改→覆盖/仅本地改→保留/双方都改→备份后覆盖;--force 一律覆盖),成功后更新 .sync-state.json 基线
|
|
979
1085
|
exp init --expert <code> --dir <本地目录> [--display-name X] [--message 初始化导入]
|
|
980
1086
|
exp commit --expert <code> --dir <本地目录> --message "..." [--mode FULL_SNAPSHOT|FILE_PATCH]
|
|
981
1087
|
exp lock --expert <code> --action query|acquire(默认)|heartbeat|release --component ROOT --ttl 30 --lock-token <token>
|
|
982
1088
|
exp release --expert <code> --commit <id> --version vX.Y.Z [--note 说明]
|
|
983
1089
|
exp rollback --expert <code> --version vX.Y.Z
|
|
1090
|
+
attend 考勤 REST 接口(2026-09-07 新增,hr-attendance 病假/事假等假期余额)
|
|
1091
|
+
attend statis [--user <人员ID>] [--begin 2026-09-01] [--end 2026-09-30] # getAttendInfoStatis 假期余额统计;--user 默认当前登录用户,--begin/--end 默认当月
|
|
984
1092
|
认证: 读 ~/.workbuddy/e10-login-cache.md;失效请先 e10-login oidc`);
|
|
985
1093
|
}
|
|
986
1094
|
|
|
@@ -1037,6 +1145,9 @@ async function main() {
|
|
|
1037
1145
|
case "rollback": result = await expRollback(args, json); break;
|
|
1038
1146
|
default: usage(); process.exit(1);
|
|
1039
1147
|
}
|
|
1148
|
+
} else if (family === "attend") {
|
|
1149
|
+
if (sub === "statis") result = await cmdAttendStatis(args, json);
|
|
1150
|
+
else { usage(); process.exit(1); }
|
|
1040
1151
|
} else { usage(); process.exit(1); }
|
|
1041
1152
|
if (json && result !== undefined) console.log(JSON.stringify(result, null, 1));
|
|
1042
1153
|
}
|
package/package.json
CHANGED