e10-data 0.1.2 → 0.1.4
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 +23 -1
- package/bin/e10-data.mjs +318 -11
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ e10-data ds tables|fields|list|chart|stat|count 数据源(业务模块
|
|
|
19
19
|
e10-data doc count|list 文档查询(documentListCount + documentList)
|
|
20
20
|
e10-data noun list --skill <skillKey> 词库查询(可带 --source-type/--source-group/--source-table 三参数)
|
|
21
21
|
e10-data field id2name|name2id 关联字段翻译(testRemoteBrowser / complete)
|
|
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 显式)
|
|
22
23
|
e10-data exp status|manifest|commits|file|pull|init|commit|lock|release|rollback 专家包版本管理
|
|
23
24
|
```
|
|
24
25
|
|
|
@@ -41,6 +42,27 @@ e10-data doc count --name 智能问数
|
|
|
41
42
|
e10-data noun list --skill rd-department
|
|
42
43
|
e10-data field id2name --module ebuilder/form --type ebuilder --ids 3194494277170470571 \
|
|
43
44
|
--form-param '{"fieldId":"966849910772850766","module":"ebuilderform"}'
|
|
45
|
+
e10-data conf get CONN_EC_EDCAPP # 读配置中心单条
|
|
46
|
+
e10-data conf resolve '{"connId":"#{CONN_EC_EDCAPP}"}' # 占位符替换为真实值
|
|
47
|
+
e10-data conf add CONN_EC_EDCAPP --value 10 --name "说明" --attrs '{"schema":"ec_edcapp"}'
|
|
48
|
+
e10-data conf delete CONN_EC_EDCAPP
|
|
44
49
|
e10-data exp status --expert data-assistant
|
|
45
|
-
e10-data exp pull --expert data-assistant --dir <本地专家包目录> --dry-run
|
|
50
|
+
e10-data exp pull --expert data-assistant --dir <本地专家包目录> [--dry-run] [--force]
|
|
46
51
|
```
|
|
52
|
+
|
|
53
|
+
## exp pull 智能拉取(v0.1.4 新增,防本地修改被覆盖)
|
|
54
|
+
|
|
55
|
+
`exp pull` 不再盲目覆盖。CLI 在专家包本地目录维护基线文件 `.sync-state.json`(记录上次同步的 commitId/headRevision 与每文件 sha256),拉取时逐文件三方对比:
|
|
56
|
+
|
|
57
|
+
| 场景 | 判定 | 处理 |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| 仅远端改(本地未动) | 本地 sha == 基线 sha ≠ 远端 sha | 直接下载覆盖(安全) |
|
|
60
|
+
| 仅本地改(远端未动) | 本地 sha ≠ 基线 sha == 远端 sha | **保留本地,不覆盖** |
|
|
61
|
+
| 双方都改 / 无基线可判 | 本地、基线、远端互不相同 | **先备份本地副本到 `<专家包目录>.pull-backup-<时间戳>/`,再下载覆盖** |
|
|
62
|
+
| 本地缺失 | manifest 有、本地无 | 直接下载(新增/恢复) |
|
|
63
|
+
|
|
64
|
+
- 拉取成功后将 `.sync-state.json` 刷新到新 head,后续判定持续准确;
|
|
65
|
+
- `--force`:一律以远端覆盖(放弃本地修改,不备份);
|
|
66
|
+
- `--dry-run`:只预览决策,不写盘不下载;
|
|
67
|
+
- `.sync-state.json` 为本地同步元文件,**永不随包提交**(`walkDir`/`collectPackage` 已排除);`exp commit` 成功后也会自动刷新该基线。
|
|
68
|
+
|
package/bin/e10-data.mjs
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* e10-data doc count|list 文档查询 documentListCount / documentList
|
|
10
10
|
* e10-data noun list 词库 getChatDocsBySkillKey
|
|
11
11
|
* e10-data field id2name|name2id 关联字段翻译 testRemoteBrowser / complete
|
|
12
|
+
* e10-data conf get|batch|batch-file|resolve|resolve-file|add|add-file|update|delete
|
|
13
|
+
* 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连)
|
|
12
14
|
*
|
|
13
15
|
* 通用:--json 原样输出;复杂 JSON 参数支持 @文件 或 内联(中文走 @文件 更稳)
|
|
14
16
|
* 认证:进程内读取用户级登录缓存 ~/.workbuddy/e10-login-cache.md;不存在/失效提示先 e10-login
|
|
@@ -453,6 +455,230 @@ async function cmdFieldName2id(args, json) {
|
|
|
453
455
|
printHuman("name→id 候选 " + rows.length + " 条", rows.map((x) => ({ id: x.id, name: x.name ?? x.title ?? "" })), ["id", "name"]);
|
|
454
456
|
}
|
|
455
457
|
|
|
458
|
+
// ---------------- conf:环境参数配置中心(chatBi config map) ----------------
|
|
459
|
+
// 覆盖 config-resolve 原 resolve_config.py 的 URL 直连调用:get/batch/batch-file/resolve/resolve-file
|
|
460
|
+
// 与管理 add/add-file/update/delete。认证复用进程内登录缓存(完整 Cookie + eteamsid,同 query/ds/doc),
|
|
461
|
+
// 无内置令牌;baseUrl 取登录缓存(与 executeQueryDsl 同域)。confType 默认由 key 前缀推导
|
|
462
|
+
// (CONN_→conn / DS_→ds / WF_→wf),可用 --type 显式指定任意大类(如 data-source-query / system-config)。
|
|
463
|
+
const CONF_PREFIX_TYPE = [["CONN_", "conn"], ["DS_", "ds"], ["WF_", "wf"]];
|
|
464
|
+
const CONF_GET_PATH = "/api/dw/newetl/chatBi/getChatBiConfigMap";
|
|
465
|
+
const CONF_BATCH_PATH = "/api/dw/newetl/chatBi/batchGetChatBiConfigMap";
|
|
466
|
+
const CONF_ADD_PATH = "/api/dw/newetl/chatBi/batchAddChatBiConfigMap";
|
|
467
|
+
const CONF_UPDATE_PATH = "/api/dw/newetl/chatBi/updateChatBiConfigMap";
|
|
468
|
+
const CONF_DELETE_PATH = "/api/dw/newetl/chatBi/deleteChatBiConfigMap";
|
|
469
|
+
const CONF_TOKEN_RE = /#\{([A-Z][A-Z0-9_]{2,})\}/g;
|
|
470
|
+
function confKeyType(key, explicitType) {
|
|
471
|
+
if (explicitType) return explicitType;
|
|
472
|
+
for (const [p, t] of CONF_PREFIX_TYPE) if (key.startsWith(p)) return t;
|
|
473
|
+
throw new Error("无法由 confKey 推导 confType(需 CONN_/DS_/WF_ 前缀或显式 --type): " + key);
|
|
474
|
+
}
|
|
475
|
+
function confFail(j, url) {
|
|
476
|
+
const m = (j && (j.msg || (j.actionMsg && j.actionMsg.message))) || "";
|
|
477
|
+
throw new Error("配置服务请求失败 [" + url + "]" + (m ? ": " + m : ""));
|
|
478
|
+
}
|
|
479
|
+
async function confRequest(method, url, bodyObj) {
|
|
480
|
+
const resp = await fetch(BASE + url, {
|
|
481
|
+
method,
|
|
482
|
+
headers: {
|
|
483
|
+
"accept": "application/json, text/plain, */*",
|
|
484
|
+
"langType": "zh_CN",
|
|
485
|
+
"cookie": auth.cookie,
|
|
486
|
+
"eteamsid": auth.eteamsid,
|
|
487
|
+
"ebbusinessid": EB_BIZ_ID,
|
|
488
|
+
"user-agent": UA,
|
|
489
|
+
...(bodyObj !== undefined ? { "Content-Type": "application/json;charset=UTF-8" } : {}),
|
|
490
|
+
},
|
|
491
|
+
...(bodyObj !== undefined ? { body: JSON.stringify(bodyObj) } : {}),
|
|
492
|
+
});
|
|
493
|
+
const text = await resp.text();
|
|
494
|
+
let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
|
|
495
|
+
checkAuthFail(resp, text, j);
|
|
496
|
+
return j;
|
|
497
|
+
}
|
|
498
|
+
async function confGetData(ctype, key, silent) {
|
|
499
|
+
const url = CONF_GET_PATH + "?confType=" + encodeURIComponent(ctype) + "&confKey=" + encodeURIComponent(key);
|
|
500
|
+
let r;
|
|
501
|
+
try { r = await confRequest("GET", url); }
|
|
502
|
+
catch (e) { if (silent) return null; throw e; }
|
|
503
|
+
const data = (r && r.data) || null;
|
|
504
|
+
if (!silent && (!data || data.confValue === undefined || data.confValue === null || data.confValue === "")) {
|
|
505
|
+
throw new Error("配置不存在或为空: confType=" + ctype + " confKey=" + key);
|
|
506
|
+
}
|
|
507
|
+
return data;
|
|
508
|
+
}
|
|
509
|
+
function confNormalizeBatch(payload) {
|
|
510
|
+
const data = (payload && payload.data) || null;
|
|
511
|
+
const out = {};
|
|
512
|
+
if (Array.isArray(data)) {
|
|
513
|
+
for (const row of data) {
|
|
514
|
+
if (!row || typeof row !== "object") continue;
|
|
515
|
+
const k = row.confKey || row.conf_key;
|
|
516
|
+
if (k) out[k] = row;
|
|
517
|
+
}
|
|
518
|
+
} else if (data && typeof data === "object") {
|
|
519
|
+
for (const [k, v] of Object.entries(data)) {
|
|
520
|
+
if (v && typeof v === "object" && !v.confKey) v.confKey = k;
|
|
521
|
+
out[k] = v;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
async function cmdConfGet(args, json, keys) {
|
|
527
|
+
const key = keys[0];
|
|
528
|
+
if (!key) throw new Error("conf get 缺少 confKey(用法: conf get <key> [--type t])");
|
|
529
|
+
const ctype = confKeyType(key, args.type);
|
|
530
|
+
const data = await confGetData(ctype, key);
|
|
531
|
+
if (json) return data;
|
|
532
|
+
console.log(JSON.stringify(data, null, 1));
|
|
533
|
+
}
|
|
534
|
+
async function cmdConfBatch(args, json, keys) {
|
|
535
|
+
if (!keys.length) throw new Error("conf batch 缺少 confKey 列表(用法: conf batch <key1> [key2 ...])");
|
|
536
|
+
const items = keys.map((k) => ({ confType: confKeyType(k, args.type), confKey: k }));
|
|
537
|
+
let map = {};
|
|
538
|
+
try {
|
|
539
|
+
const r = await confRequest("POST", CONF_BATCH_PATH, items);
|
|
540
|
+
map = confNormalizeBatch(r);
|
|
541
|
+
for (const it of items) {
|
|
542
|
+
const c = map[it.confKey];
|
|
543
|
+
if (!c || c.confValue === undefined || c.confValue === null || c.confValue === "") {
|
|
544
|
+
throw new Error("批量响应缺失 confKey=" + it.confKey);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} catch (e) {
|
|
548
|
+
if (String(e.message).indexOf("批量响应缺失") === 0) throw e;
|
|
549
|
+
console.error("[config-resolve] 批量接口不可用,降级逐条单查: " + e.message);
|
|
550
|
+
map = {};
|
|
551
|
+
for (const it of items) map[it.confKey] = await confGetData(it.confType, it.confKey);
|
|
552
|
+
}
|
|
553
|
+
if (json) return map;
|
|
554
|
+
printHuman("配置 " + items.length + " 条", keys.map((k) => ({ key: k, confType: items.find((x) => x.confKey === k).confType, confValue: (map[k] || {}).confValue ?? "", confName: (map[k] || {}).confName ?? "" })), ["key", "confType", "confValue", "confName"]);
|
|
555
|
+
}
|
|
556
|
+
async function cmdConfBatchFile(args, json) {
|
|
557
|
+
if (!args["batch-file"]) throw new Error("conf batch-file 缺少参数(用法: conf batch-file @items.json)");
|
|
558
|
+
const items = readJsonArg(args["batch-file"], "--batch-file");
|
|
559
|
+
const list = Array.isArray(items) ? items : [items];
|
|
560
|
+
const map = {};
|
|
561
|
+
for (const it of list) {
|
|
562
|
+
if (!it.confKey || !it.confType) continue;
|
|
563
|
+
map[it.confKey] = await confGetData(it.confType, it.confKey);
|
|
564
|
+
}
|
|
565
|
+
if (json) return map;
|
|
566
|
+
printHuman("配置 " + Object.keys(map).length + " 条", Object.keys(map).map((k) => ({ key: k, confValue: (map[k] || {}).confValue ?? "", confName: (map[k] || {}).confName ?? "" })), ["key", "confValue", "confName"]);
|
|
567
|
+
}
|
|
568
|
+
async function confResolveAll(keys) {
|
|
569
|
+
// 多占位符:先批量预取一轮(失败降级逐条单查),返回 {key: data}
|
|
570
|
+
const items = keys.map((k) => ({ confType: confKeyType(k, ""), confKey: k }));
|
|
571
|
+
const map = {};
|
|
572
|
+
try {
|
|
573
|
+
const r = await confRequest("POST", CONF_BATCH_PATH, items);
|
|
574
|
+
const got = confNormalizeBatch(r);
|
|
575
|
+
for (const it of items) {
|
|
576
|
+
const c = got[it.confKey];
|
|
577
|
+
if (!c || c.confValue === undefined || c.confValue === null || c.confValue === "") throw new Error("缺失 " + it.confKey);
|
|
578
|
+
map[it.confKey] = c;
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
for (const it of items) map[it.confKey] = await confGetData(it.confType, it.confKey);
|
|
582
|
+
}
|
|
583
|
+
return map;
|
|
584
|
+
}
|
|
585
|
+
async function confResolveText(text, json) {
|
|
586
|
+
const found = [...new Set([...text.matchAll(CONF_TOKEN_RE)].map((m) => m[1]))];
|
|
587
|
+
const map = found.length ? await confResolveAll(found) : {};
|
|
588
|
+
const out = text.replace(CONF_TOKEN_RE, (m0, k) => {
|
|
589
|
+
const cfg = map[k];
|
|
590
|
+
if (!cfg) throw new Error("配置不存在或为空: " + k);
|
|
591
|
+
return cfg.confValue ?? "";
|
|
592
|
+
});
|
|
593
|
+
if (json) return { input: text, output: out, resolved: Object.fromEntries(Object.entries(map).map(([k, v]) => [k, v.confValue ?? ""])) };
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
async function cmdConfResolve(args, json, keys) {
|
|
597
|
+
let text = keys[0];
|
|
598
|
+
if (!text) throw new Error("conf resolve 缺少文本(用法: conf resolve '<含#{KEY}文本>' | @file.txt)");
|
|
599
|
+
if (text.startsWith("@")) text = fs.readFileSync(text.slice(1), "utf-8");
|
|
600
|
+
const out = await confResolveText(text, json);
|
|
601
|
+
if (json) { console.log(JSON.stringify(out, null, 1)); return out; }
|
|
602
|
+
process.stdout.write(out + "\n");
|
|
603
|
+
}
|
|
604
|
+
async function cmdConfResolveFile(args, json) {
|
|
605
|
+
const p = args["resolve-file"];
|
|
606
|
+
if (!p) throw new Error("conf resolve-file 缺少文件路径(用法: conf resolve-file <文件路径>)");
|
|
607
|
+
if (!fs.existsSync(p)) throw new Error("文件不存在: " + p);
|
|
608
|
+
const text = fs.readFileSync(p, "utf-8");
|
|
609
|
+
const out = await confResolveText(text, json);
|
|
610
|
+
if (json) { console.log(JSON.stringify(out, null, 1)); return out; }
|
|
611
|
+
process.stdout.write(out + "\n");
|
|
612
|
+
}
|
|
613
|
+
async function cmdConfAdd(args, json, keys) {
|
|
614
|
+
const key = keys[0];
|
|
615
|
+
if (!key || args.value === undefined) throw new Error("conf add 缺少 confKey/confValue(用法: conf add <key> --value <真实值> [--name] [--attrs] [--type] [--force])");
|
|
616
|
+
const ctype = confKeyType(key, args.type);
|
|
617
|
+
if (!args.force && await confGetData(ctype, key, true)) {
|
|
618
|
+
console.error("[config-resolve] 配置已存在,跳过新增(如需修改请用 conf update;确需重建可先 conf delete): " + key);
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
const item = { confType: ctype, confKey: key, confValue: String(args.value), confAttrs: args.attrs || "", confName: args.name || "" };
|
|
622
|
+
const r = await confRequest("POST", CONF_ADD_PATH, [item]);
|
|
623
|
+
if (json) return r;
|
|
624
|
+
console.log("新增成功: " + key + " = " + item.confValue);
|
|
625
|
+
}
|
|
626
|
+
async function cmdConfAddFile(args, json) {
|
|
627
|
+
if (args["add-file"] === undefined) throw new Error("conf add-file 缺少参数(用法: conf add-file @items.json)");
|
|
628
|
+
const items = readJsonArg(args["add-file"], "--add-file");
|
|
629
|
+
const list = Array.isArray(items) ? items : [items];
|
|
630
|
+
const toAdd = [];
|
|
631
|
+
for (const it of list) {
|
|
632
|
+
if (!it.confKey || it.confValue === undefined) continue;
|
|
633
|
+
const ctype = it.confType || confKeyType(it.confKey, args.type);
|
|
634
|
+
const dup = await confGetData(ctype, it.confKey, true);
|
|
635
|
+
if (dup) { console.error("[config-resolve] 已存在跳过: " + it.confKey); continue; }
|
|
636
|
+
toAdd.push({ confType: ctype, confKey: it.confKey, confValue: String(it.confValue), confAttrs: it.confAttrs || "", confName: it.confName || "" });
|
|
637
|
+
}
|
|
638
|
+
if (!toAdd.length) { console.log("全部已存在,无新增"); return null; }
|
|
639
|
+
const r = await confRequest("POST", CONF_ADD_PATH, toAdd);
|
|
640
|
+
if (json) return r;
|
|
641
|
+
console.log("批量新增 " + toAdd.length + " 条成功");
|
|
642
|
+
}
|
|
643
|
+
async function cmdConfUpdate(args, json, keys) {
|
|
644
|
+
const key = keys[0];
|
|
645
|
+
if (!key) throw new Error("conf update 缺少 confKey(用法: conf update <key> [--value v] [--name n] [--attrs a] [--type t])");
|
|
646
|
+
if (args.value === undefined && args.name === undefined && args.attrs === undefined) {
|
|
647
|
+
throw new Error("conf update 至少需提供 --value / --name / --attrs 之一");
|
|
648
|
+
}
|
|
649
|
+
const body = { confType: confKeyType(key, args.type), confKey: key };
|
|
650
|
+
if (args.value !== undefined) body.confValue = String(args.value);
|
|
651
|
+
if (args.name !== undefined) body.confName = args.name;
|
|
652
|
+
if (args.attrs !== undefined) body.confAttrs = args.attrs;
|
|
653
|
+
const r = await confRequest("POST", CONF_UPDATE_PATH, body);
|
|
654
|
+
if (json) return r;
|
|
655
|
+
console.log("更新成功: " + key);
|
|
656
|
+
}
|
|
657
|
+
async function cmdConfDelete(args, json, keys) {
|
|
658
|
+
const key = keys[0];
|
|
659
|
+
if (!key) throw new Error("conf delete 缺少 confKey(用法: conf delete <key> [--type t])");
|
|
660
|
+
const ctype = confKeyType(key, args.type);
|
|
661
|
+
const url = CONF_DELETE_PATH + "?confType=" + encodeURIComponent(ctype) + "&confKey=" + encodeURIComponent(key);
|
|
662
|
+
const r = await confRequest("DELETE", url);
|
|
663
|
+
if (json) return r;
|
|
664
|
+
console.log("删除成功: " + key);
|
|
665
|
+
}
|
|
666
|
+
async function cmdConf(sub, args, json, pos) {
|
|
667
|
+
const keys = pos.slice(2);
|
|
668
|
+
switch (sub) {
|
|
669
|
+
case "get": return cmdConfGet(args, json, keys);
|
|
670
|
+
case "batch": return cmdConfBatch(args, json, keys);
|
|
671
|
+
case "batch-file": return cmdConfBatchFile(args, json);
|
|
672
|
+
case "resolve": return cmdConfResolve(args, json, keys);
|
|
673
|
+
case "resolve-file": return cmdConfResolveFile(args, json);
|
|
674
|
+
case "add": return cmdConfAdd(args, json, keys);
|
|
675
|
+
case "add-file": return cmdConfAddFile(args, json);
|
|
676
|
+
case "update": return cmdConfUpdate(args, json, keys);
|
|
677
|
+
case "delete": return cmdConfDelete(args, json, keys);
|
|
678
|
+
default: usage(); process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
456
682
|
// ---------------- exp:专家包版本管理(expert-repository) ----------------
|
|
457
683
|
async function expGet(url) {
|
|
458
684
|
// 返回 {raw, j}:raw 用于正则提取大整数 id(commitId 19 位超 JS Number 精度,JSON.parse 会丢精度!)
|
|
@@ -479,6 +705,7 @@ function rawNumSeq(raw, key) {
|
|
|
479
705
|
}
|
|
480
706
|
const EXP_ALLOWED_DIRS = [".codebuddy-plugin", "agents", "avatars", "skills"];
|
|
481
707
|
const EXP_SKIP_DIRS = [".git", ".workbuddy", "node_modules", "__pycache__"];
|
|
708
|
+
const EXP_SKIP_FILES = [".sync-state.json"]; // 本地同步状态元文件(含基线 sha),永不随包提交
|
|
482
709
|
function sha256(buf) { return crypto.createHash("sha256").update(buf).digest("hex"); }
|
|
483
710
|
function walkDir(dir, root) {
|
|
484
711
|
const out = [];
|
|
@@ -491,6 +718,7 @@ function walkDir(dir, root) {
|
|
|
491
718
|
if (rel.includes("/") && !EXP_ALLOWED_DIRS.includes(top)) continue; // 仅允许根文件+四大目录
|
|
492
719
|
walkDir(abs, root).forEach((x) => out.push(x));
|
|
493
720
|
} else if (entry.isFile()) {
|
|
721
|
+
if (EXP_SKIP_FILES.includes(entry.name)) continue; // .sync-state.json 不入包
|
|
494
722
|
const top = rel.split("/")[0];
|
|
495
723
|
if (!rel.includes("/") || EXP_ALLOWED_DIRS.includes(top)) out.push({ rel, abs });
|
|
496
724
|
}
|
|
@@ -577,37 +805,96 @@ async function expFile(args, json) {
|
|
|
577
805
|
if (json) { console.log(JSON.stringify({ relativePath: rp, size: buf.length, base64: buf.toString("base64") })); return; }
|
|
578
806
|
process.stdout.write(buf.toString("utf-8"));
|
|
579
807
|
}
|
|
808
|
+
// ---- exp 智能拉取:基线状态文件(三方差异保护,2026-09-04 实现)----
|
|
809
|
+
// .sync-state.json 记录上次成功同步的 commitId/revision 与每文件 sha256(基线)。
|
|
810
|
+
// pull 逐文件三方对比:local(当前本地) vs base(上次同步基线) vs remote(远端 manifest),
|
|
811
|
+
// 决策:仅远端改→覆盖;仅本地改→保留本地;双方都改→备份本地副本后覆盖(备份目录保留本地版);
|
|
812
|
+
// 无基线文件/基线无记录→保守备份后覆盖。拉取成功后把 .sync-state.json 更新为新 head。
|
|
813
|
+
function syncStatePath(dir) { return path.join(dir, ".sync-state.json"); }
|
|
814
|
+
function readSyncState(dir) {
|
|
815
|
+
const p = syncStatePath(dir);
|
|
816
|
+
if (!fs.existsSync(p)) return null;
|
|
817
|
+
try {
|
|
818
|
+
const j = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
819
|
+
const files = {};
|
|
820
|
+
for (const [rel, v] of Object.entries(j.files || {})) files[rel] = (typeof v === "string") ? v : (v.sha256 || "");
|
|
821
|
+
return { commitId: String(j.commitId || ""), headRevision: j.headRevision, files };
|
|
822
|
+
} catch { return null; }
|
|
823
|
+
}
|
|
824
|
+
function writeSyncState(dir, commitId, headRevision, fileShaMap) {
|
|
825
|
+
const files = {};
|
|
826
|
+
for (const [rel, sha] of Object.entries(fileShaMap)) files[rel] = { sha256: sha };
|
|
827
|
+
const obj = { commitId: String(commitId), headRevision, files, updateTime: new Date().toISOString() };
|
|
828
|
+
fs.writeFileSync(syncStatePath(dir), JSON.stringify(obj, null, 1), "utf-8");
|
|
829
|
+
}
|
|
580
830
|
async function expPull(args, json) {
|
|
581
831
|
const expert = need("expert", args), dir = need("dir", args);
|
|
582
832
|
const dry = !!args["dry-run"];
|
|
833
|
+
const force = !!args["force"]; // --force = 一律以远端覆盖(放弃本地修改)
|
|
583
834
|
const st = await expGet("/api/dw/expert-repository/v1/repositories/status?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
|
|
584
835
|
if (!expOk(st.j)) throw new Error(expFailMsg(st.j));
|
|
585
836
|
const headId = rawNum(st.raw, "headCommitId") || String((st.j.data || {}).headCommitId);
|
|
837
|
+
const headRev = (st.j.data || {}).headRevision;
|
|
586
838
|
const mf = await expGet("/api/dw/expert-repository/v1/repositories/manifest?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
|
|
587
839
|
if (!expOk(mf.j)) throw new Error(expFailMsg(mf.j));
|
|
588
840
|
const items = (mf.j.data || []).filter((f) => f.relativePath && f.relativePath !== "DELETE");
|
|
589
841
|
const local = fs.existsSync(dir) ? localShaMap(dir) : {};
|
|
590
|
-
|
|
591
|
-
const
|
|
842
|
+
const base = readSyncState(dir); // 基线:上次同步后的文件 sha(无则 null)
|
|
843
|
+
const baseCommit = (base && base.commitId) || "";
|
|
844
|
+
// 基线有效判定:上次同步 commitId 须在提交历史上(取不到历史就保守按无基线处理)
|
|
845
|
+
let backupRoot = "";
|
|
846
|
+
let downloaded = 0, unchanged = 0, preserved = 0, backed = 0, missing = 0;
|
|
847
|
+
const plan = []; // {rel, action, reason}
|
|
848
|
+
const mkBackupRoot = () => {
|
|
849
|
+
if (backupRoot) return backupRoot;
|
|
850
|
+
const absDir = path.resolve(dir);
|
|
851
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
852
|
+
backupRoot = path.join(path.dirname(absDir), path.basename(absDir) + ".pull-backup-" + ts);
|
|
853
|
+
fs.mkdirSync(backupRoot, { recursive: true });
|
|
854
|
+
return backupRoot;
|
|
855
|
+
};
|
|
592
856
|
for (const it of items) {
|
|
593
857
|
const lp = path.join(dir, it.relativePath.split("/").join(path.sep));
|
|
594
858
|
const ls = local[it.relativePath];
|
|
859
|
+
const baseSha = (base && base.files) ? base.files[it.relativePath] : undefined;
|
|
595
860
|
if (ls === it.sha256) { unchanged++; continue; }
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
861
|
+
if (ls === undefined) { // 本地缺失 → 直接下载(新增/恢复)
|
|
862
|
+
missing++; plan.push({ rel: it.relativePath, action: dry ? "待下载" : "已下载", reason: "本地缺失" });
|
|
863
|
+
} else if (!force && baseSha !== undefined && ls === baseSha && baseSha !== it.sha256) {
|
|
864
|
+
// 仅远端改(本地未动,与基线一致)→ 覆盖
|
|
865
|
+
plan.push({ rel: it.relativePath, action: dry ? "待下载" : "已下载", reason: "远端更新(本地未动)" });
|
|
866
|
+
} else if (!force && baseSha !== undefined && baseSha === it.sha256) {
|
|
867
|
+
// 仅本地改(远端=基线,本地≠基线)→ 保留本地,不覆盖
|
|
868
|
+
preserved++; plan.push({ rel: it.relativePath, action: "保留本地", reason: "本地修改(远端未动)" });
|
|
869
|
+
continue;
|
|
870
|
+
} else { // 双方都改 / 无基线可判 / --force → 先备份本地副本再覆盖
|
|
871
|
+
backed++;
|
|
872
|
+
if (!dry && !force) {
|
|
873
|
+
const bdir = path.join(mkBackupRoot(), it.relativePath.split("/").join(path.sep));
|
|
874
|
+
fs.mkdirSync(path.dirname(bdir), { recursive: true });
|
|
875
|
+
fs.copyFileSync(lp, bdir);
|
|
876
|
+
}
|
|
877
|
+
plan.push({ rel: it.relativePath, action: dry ? "待下载(将备份本地)" : (force ? "已下载(force覆盖)" : "已下载(本地已备份)"), reason: (force ? "force 覆盖" : (baseSha === undefined ? "无基线记录" : "双方都改")) });
|
|
878
|
+
}
|
|
599
879
|
if (dry) continue;
|
|
600
880
|
fs.mkdirSync(path.dirname(lp), { recursive: true });
|
|
601
881
|
const buf = await rawGet("/api/dw/expert-repository/v1/repositories/file?expertCode=" + encodeURIComponent(expert) + "&commitId=" + encodeURIComponent(headId) + "&relativePath=" + encodeURIComponent(it.relativePath));
|
|
602
882
|
fs.writeFileSync(lp, buf);
|
|
603
883
|
downloaded++;
|
|
604
884
|
}
|
|
605
|
-
|
|
885
|
+
// 拉取成功后更新基线状态文件(记录新 head + 本地最终文件 sha)
|
|
886
|
+
if (!dry) {
|
|
887
|
+
const finalLocal = localShaMap(dir);
|
|
888
|
+
writeSyncState(dir, headId, headRev, finalLocal);
|
|
889
|
+
}
|
|
890
|
+
const out = { expert, headCommitId: headId, headRevision: headRev, total: items.length, unchanged, downloaded, preserved, backed, missing, dryRun: !!dry, backupDir: backupRoot || undefined };
|
|
606
891
|
if (json) { console.log(JSON.stringify(out)); return; }
|
|
607
892
|
console.log("== pull " + expert + "@" + headId + (dry ? " (dry-run)" : "") + " ==");
|
|
608
|
-
console.log("清单 " + out.total + " 项 | 未变 " + unchanged + " |
|
|
609
|
-
for (const p of plan.slice(0,
|
|
610
|
-
if (plan.length >
|
|
893
|
+
console.log("清单 " + out.total + " 项 | 未变 " + unchanged + " | 已下载 " + downloaded + " | 保留本地 " + preserved + " | 覆盖前备份 " + backed + " | 缺失 " + missing);
|
|
894
|
+
for (const p of plan.slice(0, 40)) console.log(" [" + p.action + "] " + p.rel + (p.reason ? "(" + p.reason + ")" : ""));
|
|
895
|
+
if (plan.length > 40) console.log(" ... 共 " + plan.length + " 个文件");
|
|
896
|
+
if (backupRoot && !dry) console.log("本地修改副本备份于: " + backupRoot);
|
|
897
|
+
if (preserved > 0 && !dry) console.log("⚠️ " + preserved + " 个文件因含本地修改被保留(远端未改);如需强制覆盖请加 --force");
|
|
611
898
|
}
|
|
612
899
|
async function expInit(args, json) {
|
|
613
900
|
const expert = need("expert", args), dir = need("dir", args);
|
|
@@ -656,6 +943,12 @@ async function expCommit(args, json) {
|
|
|
656
943
|
if (/基线|过期|revision|冲突/i.test(m)) throw new Error("提交冲突: " + m + " —— 请先 exp pull 拉取最新并合并后重试");
|
|
657
944
|
throw new Error("提交失败: " + m);
|
|
658
945
|
}
|
|
946
|
+
// 提交成功后把本地 .sync-state.json 基线刷新到新 head(与本地文件 sha 一致),保证后续 pull 三方判定准确
|
|
947
|
+
const rj = r.data || {};
|
|
948
|
+
const newHead = rawNum(JSON.stringify(r), "headCommitId") || rawNum(JSON.stringify(r), "commitId") || rj.headCommitId || rj.commitId || "";
|
|
949
|
+
if (newHead && fs.existsSync(dir)) {
|
|
950
|
+
try { writeSyncState(dir, newHead, rj.headRevision ?? rj.revision ?? 0, localShaMap(dir)); } catch {}
|
|
951
|
+
}
|
|
659
952
|
console.log("== 提交成功 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
|
|
660
953
|
}
|
|
661
954
|
async function expLock(args, json) {
|
|
@@ -734,12 +1027,22 @@ noun 词库查询
|
|
|
734
1027
|
field 关联字段翻译
|
|
735
1028
|
field id2name --module <m> --type <t> --ids id1,id2 [--form-param '{"fieldId":"..","module":"ebuilderform"}'|--eb-browser-param '{..}'|--property-type x]
|
|
736
1029
|
field name2id --module <m> --type <t> --name 名称 [同上附加参数]
|
|
1030
|
+
conf 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连;confType 默认由 CONN_/DS_/WF_ 前缀推导,可 --type 显式)
|
|
1031
|
+
conf get <key> [--type t] # 单查(confValue/confAttrs/confName 全量 JSON)
|
|
1032
|
+
conf batch <key1> [key2 ...] # 批量查(批量接口异常自动降级逐条单查)
|
|
1033
|
+
conf batch-file @items.json # 按 [{confType,confKey}..] 显式批量查(支持任意 confType)
|
|
1034
|
+
conf resolve '<含#{KEY}文本>'|@file.txt # 文本占位符替换为真实值(含中文建议 @file)
|
|
1035
|
+
conf resolve-file <文件路径> # 读文件替换全部占位符并打印(不改写原文件)
|
|
1036
|
+
conf add <key> --value <真实值> [--name 说明] [--attrs '{"schema":..}'] [--type t] [--force] # 新增(默认判重,已存在跳过)
|
|
1037
|
+
conf add-file @items.json # JSON 数组批量新增(逐条判重)
|
|
1038
|
+
conf update <key> [--value v] [--name n] [--attrs a] [--type t] # 更新(confType+confKey 定位,传哪个字段更哪个)
|
|
1039
|
+
conf delete <key> [--type t] # 删除
|
|
737
1040
|
exp 专家包版本管理(expert-repository/v1,响应以 data.actionMsg.code=0 判成功)
|
|
738
1041
|
exp status --expert <code> [--branch main] # 仓库状态(headCommitId/headRevision=提交基线)
|
|
739
1042
|
exp manifest --expert <code> [--commit <id>] # 文件清单(sha256)
|
|
740
1043
|
exp commits --expert <code> # 提交历史
|
|
741
1044
|
exp file --expert <code> --commit <id> --path <相对路径> [--out 本地文件]
|
|
742
|
-
exp pull --expert <code> --dir <本地专家包目录> [--dry-run]
|
|
1045
|
+
exp pull --expert <code> --dir <本地专家包目录> [--dry-run] [--force] # 智能拉取:基线三方对比(仅远端改→覆盖/仅本地改→保留/双方都改→备份后覆盖;--force 一律覆盖),成功后更新 .sync-state.json 基线
|
|
743
1046
|
exp init --expert <code> --dir <本地目录> [--display-name X] [--message 初始化导入]
|
|
744
1047
|
exp commit --expert <code> --dir <本地目录> --message "..." [--mode FULL_SNAPSHOT|FILE_PATCH]
|
|
745
1048
|
exp lock --expert <code> --action query|acquire(默认)|heartbeat|release --component ROOT --ttl 30 --lock-token <token>
|
|
@@ -750,7 +1053,8 @@ exp 专家包版本管理(expert-repository/v1,响应以 data.actionMsg.
|
|
|
750
1053
|
|
|
751
1054
|
async function main() {
|
|
752
1055
|
const rawArgv = process.argv.slice(2);
|
|
753
|
-
|
|
1056
|
+
// 仅当顶层显式 -v/--version(无子命令)时打印版本;exp release/rollback 的 --version 是版本号参数、conf 文本可能含 version 字样,一律放行
|
|
1057
|
+
if (rawArgv.length === 1 && (rawArgv[0] === "--version" || rawArgv[0] === "-v")) {
|
|
754
1058
|
console.log("e10-data " + cliVersion());
|
|
755
1059
|
process.exit(0);
|
|
756
1060
|
}
|
|
@@ -782,6 +1086,9 @@ async function main() {
|
|
|
782
1086
|
if (sub === "id2name") result = await cmdFieldId2name(args, json);
|
|
783
1087
|
else if (sub === "name2id") result = await cmdFieldName2id(args, json);
|
|
784
1088
|
else { usage(); process.exit(1); }
|
|
1089
|
+
} else if (family === "conf") {
|
|
1090
|
+
if (!sub) { usage(); process.exit(1); }
|
|
1091
|
+
result = await cmdConf(sub, args, json, pos);
|
|
785
1092
|
} else if (family === "exp") {
|
|
786
1093
|
if (!sub) { usage(); process.exit(1); }
|
|
787
1094
|
switch (sub) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "e10-data",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "泛微 E10 数据接口统一 CLI(data-assistant 专家配套):DSL
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "泛微 E10 数据接口统一 CLI(data-assistant 专家配套):DSL 查询/数据源/文档/词库/关联字段翻译/环境参数配置中心(conf)/专家包版本管理,认证复用 weaver-e10-login 登录态",
|
|
5
5
|
"author": "e10-dw",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"pack": "npm pack"
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|