chainlesschain 0.162.11 → 0.162.13
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 +30 -25
- package/package.json +4 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-ebtJGjAG.js → AIOps-Bq_zxhCr.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CypkRN-G.js → ActionButton-CaevDm9t.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-B2JMlIng.js → Analytics-B0gOmwPw.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-B8QQ4pk7.js → AppLayout-DWhZiV0Q.js} +2 -2
- package/src/assets/web-panel/assets/{Audit-BoYaAyFa.js → Audit-ZuZJBCxo.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BfackGZ5.js → Backup-CX7jhH5l.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C06FUpDz.js → BaseInput-CVLx7HVq.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BWxMkBYZ.js → Chat-C6yL5tRD.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-XJMvS3PV.js → Checkbox-BePhbqVq.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CzR462RK.js → Codegen-B5E4x1Lm.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BQHpLNCA.js → Col-CWhNU6A7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BWRRbJYd.js → Community-mSEAuJhp.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BunoKIy9.js → Compact-DSudHzX3.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CtJfZctm.js → Compliance-CxJLYjyn.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-ER5-_bod.js → Cowork-C-trppQj.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-C80jYBw1.js → Cron-_Ij4v5fY.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-fEMlCNsL.js → Crosschain-eLHXzp5T.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BZpctKmU.js → DID-BP04AUUB.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-RQhZmLi4.js → Dashboard-CzEeQv62.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CrpwS84l.js → Dropdown-C_SGOB22.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BEQyZtdR.js → Federation-BgaP4BOv.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DCwvl6Vh.js → FormItemContext-BxGLLt9r.js} +1 -1
- package/src/assets/web-panel/assets/{Git-6FihOxMK.js → Git-Bt_uM_Gw.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DlBLHSlJ.js → Governance-N2-0RG_o.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DdSokzV0.js → Inference-eS3g-CzP.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-bg8GBHMr.js → KnowledgeGraph-CUuvNVah.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DdFYdLQ-.js → Logs-wPbwEt2r.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DjnlAeYF.js → Marketplace-DH91kTwo.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Czs41YUh.js → McpTools-D-GyyBrI.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-CX0b3c8D.js → Memory-BOtUy-tw.js} +1 -1
- package/src/assets/web-panel/assets/{MobileBridge-BoFGb9Mm.js → MobileBridge-DIP__XQd.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-B8qQ9H-0.js → MobileProjects-1nqr1UsU.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CRF1NLae.js → Mtc-CCE0x7h2.js} +1 -1
- package/src/assets/web-panel/assets/{MtcAudit-CdCm70cJ.js → MtcAudit-BBkz0XUO.js} +1 -1
- package/src/assets/web-panel/assets/{Multisig-yoZlpq2Y.js → Multisig-CIKSJvTY.js} +2 -2
- package/src/assets/web-panel/assets/{NLProgramming-hFCgqDxJ.js → NLProgramming-BDkgeFcq.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DC8pnxs-.js → Notes-ONiUxfN1.js} +1 -1
- package/src/assets/web-panel/assets/{NotificationSettings-DDVg5Nc8.js → NotificationSettings-CGcyKEIe.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-0YCtAFMS.js → Organization-BZtMYBJt.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DhPLoAdz.js → Overflow-C4LfFZAI.js} +1 -1
- package/src/assets/web-panel/assets/{OverrideContext-x9ZzjLwk.js → OverrideContext-C_4H9tGA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BWpuJhkD.js → P2P-D71Cpk-m.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-B4IrizO9.js → Permissions-DRGYF29v.js} +1 -1
- package/src/assets/web-panel/assets/PersonalDataHub-CfRYoIua.js +1 -0
- package/src/assets/web-panel/assets/PersonalDataHub-Dvaa8niQ.css +1 -0
- package/src/assets/web-panel/assets/{Pipeline-M65jR6sq.js → Pipeline-BOyp0_Qo.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BeO8zLup.js → Privacy-a_AcphvF.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Ck_ZjrVZ.js → ProjectInit-CFu1grYt.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectSettings-ijn-97s0.js → ProjectSettings-p54Eivhh.js} +1 -1
- package/src/assets/web-panel/assets/{Projects-BsNBemeh.js → Projects-DkB88XAu.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CI7UxKVO.js → Providers-EK7f8DEd.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-dE2M1KOB.js → QuickAsk-CpO0w3iP.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-B30EgbKS.js → Recommend-BUJVQdv9.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CV6n7wMx.js → Reputation-kU2fOuZt.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DSaoTjlN.js → Row-oGWRbk6g.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-_NgBmHaC.js → RssFeed-saC_46Yo.js} +1 -1
- package/src/assets/web-panel/assets/{Search-B341ooTV.js → Search-CjtRqFUu.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqCQD8hf.js → Security-BX0NBVfQ.js} +1 -1
- package/src/assets/web-panel/assets/{Services-Cju_95rB.js → Services-Bgxsnei_.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-kh_uW22l.js → Skeleton-CwBb3k2a.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BVRPgciI.js → Skills-CZCMYH6Z.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-y-vKFYkI.js → Sla-Djy1uHnZ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BXJm9zyo.js → SpeechSettings-CGUI_Uyh.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BmHZR-Kv.js → SyncSettings-DK2CKHRD.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-C7zmYW9f.js → Tasks-BKiOzeIO.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DVEG7FdA.js → Templates-CnQpleXj.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CpvjzPCo.js → Tenant-DwKz0cjm.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_Wpp2iE.js → Terminal-A7t_wsR8.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-QBrjdNqi.js → Tokens-BqYY9l44.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BhR_VEvQ.js → Trigger-BI4bXFmi.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-C0xhM2lC.js → Trust-yMynKTRG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BnyP-W3-.js → UkeySign-Br4IScM6.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C5Y8MyEK.js → VideoEditing-CWcThGsP.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DzCPCQNF.js → Wallet-CZcAtjxj.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-6X5bLtHU.js → WebAuthn-BnTZFMA0.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-ekS27G9f.js → WorkflowEditor-N7gGz3_n.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BikodUwh.js → chat-D175ZIO0.js} +1 -1
- package/src/assets/web-panel/assets/{collapseMotion-CjFH_Jop.js → collapseMotion-DfnRZex1.js} +1 -1
- package/src/assets/web-panel/assets/{colors-8yIg5K7E.js → colors-LKhZyttv.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-MLWo5-GY.js → compact-item-CsJSebxT.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-nir7ccDv.js → createContext-BJ_CPYFC.js} +1 -1
- package/src/assets/web-panel/assets/{echarts-Bq-n0MtJ.js → echarts-Dj_pBaVI.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-DxUIHW2P.js → hasIn-CzD3IqH8.js} +1 -1
- package/src/assets/web-panel/assets/{icons-CLQTHa5-.js → icons-BOPtEWK4.js} +4 -4
- package/src/assets/web-panel/assets/{index-vVrIg9Jk.js → index-6qPbrYF7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCGf6IJj.js → index-B7UYymse.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSjWvxVr.js → index-BDSZDDb2.js} +4 -4
- package/src/assets/web-panel/assets/{index-BURKtxBq.js → index-BEDFHKO3.js} +1 -1
- package/src/assets/web-panel/assets/{index-q3Lr2UzW.js → index-BMvdoiFr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgQtoOHc.js → index-BNVLVzN5.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bn5VWKW1.js → index-BSNibAqz.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SFekeAy.js → index-BV-__mlC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfeuuE7v.js → index-BXH9ujMW.js} +1 -1
- package/src/assets/web-panel/assets/{index-h05fIj9Q.js → index-BZluCuTH.js} +1 -1
- package/src/assets/web-panel/assets/{index-9Y0IyfeM.js → index-BhiZDGg7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkMtxzcM.js → index-BjOrt4vw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOIryna2.js → index-BmJdof_c.js} +2 -2
- package/src/assets/web-panel/assets/{index-TB5vrA0Z.js → index-BsirlkJ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgHPrMXP.js → index-BvF2tC6C.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSIaRmzU.js → index-C2HBKw07.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkSN2Ki_.js → index-CKjBAdm0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLiexKJ2.js → index-CRGNuUIM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeX-HLIi.js → index-CTIkCKav.js} +1 -1
- package/src/assets/web-panel/assets/index-CY1mQA2I.js +1 -0
- package/src/assets/web-panel/assets/{index-78olN7S9.js → index-CyHdYUeZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcyG-9vV.js → index-D401L3yx.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_W1kVtY.js → index-D5IZCkZG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bpa9senE.js → index-D7ZcBI5s.js} +1 -1
- package/src/assets/web-panel/assets/{index-dgZZAsxo.js → index-D8kltMTW.js} +1 -1
- package/src/assets/web-panel/assets/index-D9nXHfUB.js +1 -0
- package/src/assets/web-panel/assets/{index-DkIon-Gv.js → index-DJVkBmSc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-UB9bYd.js → index-DM3uBEWD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dx5xZmzt.js → index-DOO73rHE.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVLJ1iGu.js → index-DXp1jVsK.js} +1 -1
- package/src/assets/web-panel/assets/{index-S8mYImvf.js → index-Dd7dICwB.js} +1 -1
- package/src/assets/web-panel/assets/{index-SUYLhwZI.js → index-Dm-3kvtD.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_Xi08tu.js → index-DnPt5OdD.js} +1 -1
- package/src/assets/web-panel/assets/{index-hu-wjfWv.js → index-E7t1hAnk.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cp-YnzHN.js → index-JTX9A7w0.js} +1 -1
- package/src/assets/web-panel/assets/{index-8qrwsaKy.js → index-Kn-Of5ew.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dl-O2OkQ.js → index-R1cFADfk.js} +1 -1
- package/src/assets/web-panel/assets/{index-yfNusVbo.js → index-RIO4JKMP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDmc4cig.js → index-uTEVWPYA.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-xUjF_bq0.js → initDefaultProps-CBW0okek.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B019-Q6h.js → motion-DGAffQ0Z.js} +1 -1
- package/src/assets/web-panel/assets/{move-D2XYj_gA.js → move-DFJ0-5IW.js} +1 -1
- package/src/assets/web-panel/assets/{omit-AIzzlguv.js → omit-AvrDghg1.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CRkEQaLs.js → pickAttrs-D7csw9i1.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-s4kAStH6.js → placementArrow-hZ6Lg6kG.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BCsWrTkb.js → responsiveObserve-DLLx5VvS.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B_Hggtvv.js → slide-BaRIT3ev.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DnNf15VW.js → statusUtils-Cdjyuhrz.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CSQdy9SQ.js → styleChecker-CbrNybTt.js} +1 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-B8gAhiRC.js +1 -0
- package/src/assets/web-panel/assets/{useFs-D78PlgeG.js → useFs-BZPy4ICP.js} +1 -1
- package/src/assets/web-panel/assets/{useMergedState-O7QXt4P5.js → useMergedState-WwedrFR0.js} +1 -1
- package/src/assets/web-panel/assets/{useRefs-0J6m8UWN.js → useRefs-Cdq8EWeF.js} +1 -1
- package/src/assets/web-panel/assets/{useState-CSzR8F8O.js → useState-DGS1NOyn.js} +1 -1
- package/src/assets/web-panel/assets/{vendor-M5lGV-wr.js → vendor-DhFY8mDK.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BTMmpsWu.js → vnode-6Y0NDMVv.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CwOTbvKc.js → zoom-DTbMGsSH.js} +1 -1
- package/src/assets/web-panel/index.html +3 -3
- package/src/commands/__tests__/hub-aichat.test.js +277 -0
- package/src/commands/__tests__/hub-wechat.test.js +243 -0
- package/src/commands/hub.js +881 -0
- package/src/commands/sync-providers.js +436 -0
- package/src/gateways/ws/personal-data-hub-protocol.js +68 -0
- package/src/index.js +6 -0
- package/src/lib/__tests__/personal-data-hub-aichat-wizard.test.js +209 -0
- package/src/lib/__tests__/sync-credentials.test.js +265 -0
- package/src/lib/__tests__/sync-engine-cli.test.js +293 -0
- package/src/lib/personal-data-hub-aichat-wizard.js +242 -0
- package/src/lib/personal-data-hub-wiring.js +189 -0
- package/src/lib/sync-cli-db.js +194 -0
- package/src/lib/sync-credentials.js +225 -0
- package/src/lib/sync-engine-cli.js +406 -0
- package/src/lib/sync-oss-client.js +273 -0
- package/src/lib/sync-webdav-client.js +194 -0
- package/src/assets/web-panel/assets/PersonalDataHub-BK7I0Rsb.css +0 -1
- package/src/assets/web-panel/assets/PersonalDataHub-ZbziiUr6.js +0 -1
- package/src/assets/web-panel/assets/index-BeA3spHc.js +0 -1
- package/src/assets/web-panel/assets/index-DoLRjAoc.js +0 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-Bt-T27Pf.js +0 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI sync engine — Phase 3c follow-up Phase 2.
|
|
3
|
+
*
|
|
4
|
+
* Provider-agnostic sync engine consolidating the desktop-side
|
|
5
|
+
* sync-external-store + incremental-walker + markdown-renderer + engine
|
|
6
|
+
* into ONE focused CLI module. The CLI vault has only KNOWLEDGE_ITEM
|
|
7
|
+
* tombstones (no mobile sync ResourceTypes) so we drop the filter
|
|
8
|
+
* parameter and simplify.
|
|
9
|
+
*
|
|
10
|
+
* Flow (mirror desktop runWebDAVSync / runOSSSync):
|
|
11
|
+
* 1. ensureCursor
|
|
12
|
+
* 2. drain tombstones (delete then deleteFile on remote)
|
|
13
|
+
* 3. push loop: fetchBatch → putFile → recordPushed → advance cursor
|
|
14
|
+
* 4. update final cursor state
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const PROGRESS_FLUSH_EVERY = 5;
|
|
20
|
+
const PROGRESS_FLUSH_MS = 500;
|
|
21
|
+
|
|
22
|
+
// ── markdown renderer (deterministic; mirror desktop) ──────────────
|
|
23
|
+
|
|
24
|
+
function _cleanTitle(title) {
|
|
25
|
+
// Note: place `-` AT END of char-class — putting it mid-class triggers
|
|
26
|
+
// JS "invalid range" silent fallthrough and space stops matching.
|
|
27
|
+
return (
|
|
28
|
+
String(title || "untitled")
|
|
29
|
+
.replace(/[\\/:*?"<>|\s-]/g, "_")
|
|
30
|
+
.replace(/_+/g, "_")
|
|
31
|
+
.replace(/^_|_$/g, "")
|
|
32
|
+
.slice(0, 80) || "untitled"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function generateFilename(item) {
|
|
37
|
+
return `${item.id}-${_cleanTitle(item.title)}.md`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generateMarkdown(item) {
|
|
41
|
+
const tags = item.tags ? String(item.tags) : "";
|
|
42
|
+
const frontMatter = [
|
|
43
|
+
"---",
|
|
44
|
+
`id: ${item.id}`,
|
|
45
|
+
`title: ${JSON.stringify(item.title || "untitled")}`,
|
|
46
|
+
`type: ${item.type || "note"}`,
|
|
47
|
+
`created_at: ${item.created_at}`,
|
|
48
|
+
`updated_at: ${item.updated_at}`,
|
|
49
|
+
tags ? `tags: ${tags}` : "",
|
|
50
|
+
"---",
|
|
51
|
+
"",
|
|
52
|
+
]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join("\n");
|
|
55
|
+
return frontMatter + (item.content || "") + "\n";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── store: cursor + tombstone CRUD ─────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function _parseJsonField(v, fallback) {
|
|
61
|
+
if (v == null || v === "") return fallback;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(v);
|
|
64
|
+
} catch (_e) {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getCursor(dbManager, providerId, accountKey = "") {
|
|
70
|
+
const row = dbManager.get(
|
|
71
|
+
`SELECT * FROM sync_external_provider_cursor
|
|
72
|
+
WHERE provider_id = ? AND account_key = ?`,
|
|
73
|
+
[providerId, accountKey],
|
|
74
|
+
);
|
|
75
|
+
if (!row) return undefined;
|
|
76
|
+
return {
|
|
77
|
+
providerId: row.provider_id,
|
|
78
|
+
accountKey: row.account_key,
|
|
79
|
+
lastSyncAt: row.last_sync_at,
|
|
80
|
+
lastItemId: row.last_item_id,
|
|
81
|
+
remoteEtagMap: _parseJsonField(row.remote_etag_map, {}),
|
|
82
|
+
remoteFilenameMap: _parseJsonField(row.remote_filename_map, {}),
|
|
83
|
+
lastRunStatus: row.last_run_status,
|
|
84
|
+
lastRunError: row.last_run_error,
|
|
85
|
+
lastRunDurationMs: row.last_run_duration_ms,
|
|
86
|
+
itemsPushed: row.items_pushed,
|
|
87
|
+
itemsSkipped: row.items_skipped,
|
|
88
|
+
itemsDeleted: row.items_deleted,
|
|
89
|
+
createdAt: row.created_at,
|
|
90
|
+
updatedAt: row.updated_at,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureCursor(dbManager, providerId, accountKey = "") {
|
|
95
|
+
const existing = getCursor(dbManager, providerId, accountKey);
|
|
96
|
+
if (existing) return existing;
|
|
97
|
+
dbManager.run(
|
|
98
|
+
`INSERT OR IGNORE INTO sync_external_provider_cursor
|
|
99
|
+
(provider_id, account_key) VALUES (?, ?)`,
|
|
100
|
+
[providerId, accountKey],
|
|
101
|
+
);
|
|
102
|
+
return getCursor(dbManager, providerId, accountKey);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateCursor(dbManager, providerId, patch, accountKey = "") {
|
|
106
|
+
if (!patch || typeof patch !== "object") return;
|
|
107
|
+
const fields = [];
|
|
108
|
+
const params = [];
|
|
109
|
+
const map = {
|
|
110
|
+
lastSyncAt: "last_sync_at",
|
|
111
|
+
lastItemId: "last_item_id",
|
|
112
|
+
lastRunStatus: "last_run_status",
|
|
113
|
+
lastRunError: "last_run_error",
|
|
114
|
+
lastRunDurationMs: "last_run_duration_ms",
|
|
115
|
+
itemsPushed: "items_pushed",
|
|
116
|
+
itemsSkipped: "items_skipped",
|
|
117
|
+
itemsDeleted: "items_deleted",
|
|
118
|
+
};
|
|
119
|
+
for (const [k, col] of Object.entries(map)) {
|
|
120
|
+
if (k in patch) {
|
|
121
|
+
fields.push(`${col} = ?`);
|
|
122
|
+
params.push(patch[k]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if ("remoteEtagMap" in patch) {
|
|
126
|
+
fields.push("remote_etag_map = ?");
|
|
127
|
+
params.push(JSON.stringify(patch.remoteEtagMap));
|
|
128
|
+
}
|
|
129
|
+
if ("remoteFilenameMap" in patch) {
|
|
130
|
+
fields.push("remote_filename_map = ?");
|
|
131
|
+
params.push(JSON.stringify(patch.remoteFilenameMap));
|
|
132
|
+
}
|
|
133
|
+
fields.push("updated_at = ?");
|
|
134
|
+
params.push(Date.now());
|
|
135
|
+
params.push(providerId, accountKey);
|
|
136
|
+
dbManager.run(
|
|
137
|
+
`UPDATE sync_external_provider_cursor SET ${fields.join(", ")}
|
|
138
|
+
WHERE provider_id = ? AND account_key = ?`,
|
|
139
|
+
params,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function recordPushedItem(
|
|
144
|
+
dbManager,
|
|
145
|
+
providerId,
|
|
146
|
+
itemId,
|
|
147
|
+
etag,
|
|
148
|
+
filename,
|
|
149
|
+
accountKey = "",
|
|
150
|
+
) {
|
|
151
|
+
const cursor = getCursor(dbManager, providerId, accountKey) || {
|
|
152
|
+
remoteEtagMap: {},
|
|
153
|
+
remoteFilenameMap: {},
|
|
154
|
+
};
|
|
155
|
+
const etagMap = { ...cursor.remoteEtagMap, [itemId]: etag };
|
|
156
|
+
const fnMap = { ...cursor.remoteFilenameMap, [itemId]: filename };
|
|
157
|
+
dbManager.run(
|
|
158
|
+
`UPDATE sync_external_provider_cursor
|
|
159
|
+
SET remote_etag_map = ?, remote_filename_map = ?, updated_at = ?
|
|
160
|
+
WHERE provider_id = ? AND account_key = ?`,
|
|
161
|
+
[
|
|
162
|
+
JSON.stringify(etagMap),
|
|
163
|
+
JSON.stringify(fnMap),
|
|
164
|
+
Date.now(),
|
|
165
|
+
providerId,
|
|
166
|
+
accountKey,
|
|
167
|
+
],
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function removeFromMaps(dbManager, providerId, itemId, accountKey = "") {
|
|
172
|
+
const cursor = getCursor(dbManager, providerId, accountKey);
|
|
173
|
+
if (!cursor) return;
|
|
174
|
+
const etagMap = { ...cursor.remoteEtagMap };
|
|
175
|
+
const fnMap = { ...cursor.remoteFilenameMap };
|
|
176
|
+
delete etagMap[itemId];
|
|
177
|
+
delete fnMap[itemId];
|
|
178
|
+
dbManager.run(
|
|
179
|
+
`UPDATE sync_external_provider_cursor
|
|
180
|
+
SET remote_etag_map = ?, remote_filename_map = ?, updated_at = ?
|
|
181
|
+
WHERE provider_id = ? AND account_key = ?`,
|
|
182
|
+
[
|
|
183
|
+
JSON.stringify(etagMap),
|
|
184
|
+
JSON.stringify(fnMap),
|
|
185
|
+
Date.now(),
|
|
186
|
+
providerId,
|
|
187
|
+
accountKey,
|
|
188
|
+
],
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function listTombstones(dbManager, providerId, accountKey = "", limit = 1000) {
|
|
193
|
+
return dbManager.all(
|
|
194
|
+
`SELECT * FROM sync_external_tombstones
|
|
195
|
+
WHERE provider_id = ? AND account_key = ?
|
|
196
|
+
AND resource_type = 'KNOWLEDGE_ITEM'
|
|
197
|
+
ORDER BY deleted_at ASC LIMIT ?`,
|
|
198
|
+
[providerId, accountKey, limit],
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function deleteTombstone(dbManager, id) {
|
|
203
|
+
dbManager.run(`DELETE FROM sync_external_tombstones WHERE id = ?`, [id]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function markTombstoneFailed(dbManager, id, errMsg) {
|
|
207
|
+
dbManager.run(
|
|
208
|
+
`UPDATE sync_external_tombstones
|
|
209
|
+
SET retry_count = retry_count + 1, last_error = ?
|
|
210
|
+
WHERE id = ?`,
|
|
211
|
+
[String(errMsg || "").slice(0, 500), id],
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── walker: incremental batch fetching ─────────────────────────────
|
|
216
|
+
|
|
217
|
+
function fetchBatch(dbManager, cursor, batchSize = 200) {
|
|
218
|
+
const after = cursor?.lastSyncAt || 0;
|
|
219
|
+
const afterId = cursor?.lastItemId || "";
|
|
220
|
+
return dbManager.all(
|
|
221
|
+
`SELECT id, title, type, content, tags, created_at, updated_at
|
|
222
|
+
FROM knowledge_items
|
|
223
|
+
WHERE updated_at > ? OR (updated_at = ? AND id > ?)
|
|
224
|
+
ORDER BY updated_at ASC, id ASC
|
|
225
|
+
LIMIT ?`,
|
|
226
|
+
[after, after, afterId, batchSize],
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function cursorAfterItem(item) {
|
|
231
|
+
return { lastSyncAt: item.updated_at, lastItemId: item.id };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function countPending(dbManager, cursor) {
|
|
235
|
+
const after = cursor?.lastSyncAt || 0;
|
|
236
|
+
const afterId = cursor?.lastItemId || "";
|
|
237
|
+
const row = dbManager.get(
|
|
238
|
+
`SELECT COUNT(*) AS c FROM knowledge_items
|
|
239
|
+
WHERE updated_at > ? OR (updated_at = ? AND id > ?)`,
|
|
240
|
+
[after, after, afterId],
|
|
241
|
+
);
|
|
242
|
+
return row?.c || 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── engine: orchestration ──────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
async function runSync(deps) {
|
|
248
|
+
const { dbManager, client, providerId, accountKey = "", onProgress } = deps;
|
|
249
|
+
|
|
250
|
+
const t0 = Date.now();
|
|
251
|
+
let pushed = 0;
|
|
252
|
+
let skipped = 0;
|
|
253
|
+
let deleted = 0;
|
|
254
|
+
let lastError = null;
|
|
255
|
+
let lastFlushPushed = 0;
|
|
256
|
+
let lastFlushDeleted = 0;
|
|
257
|
+
let lastFlushAt = Date.now();
|
|
258
|
+
let totalPending = 0;
|
|
259
|
+
|
|
260
|
+
function maybeFlush(phase) {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const delta = pushed - lastFlushPushed + (deleted - lastFlushDeleted);
|
|
263
|
+
if (
|
|
264
|
+
delta >= PROGRESS_FLUSH_EVERY ||
|
|
265
|
+
now - lastFlushAt >= PROGRESS_FLUSH_MS
|
|
266
|
+
) {
|
|
267
|
+
onProgress?.({ phase, pushed, skipped, deleted, totalPending });
|
|
268
|
+
lastFlushPushed = pushed;
|
|
269
|
+
lastFlushDeleted = deleted;
|
|
270
|
+
lastFlushAt = now;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function refresh() {
|
|
275
|
+
return getCursor(dbManager, providerId, accountKey) || {};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let cursor = ensureCursor(dbManager, providerId, accountKey);
|
|
279
|
+
totalPending =
|
|
280
|
+
countPending(dbManager, cursor) +
|
|
281
|
+
listTombstones(dbManager, providerId, accountKey, 1000).length;
|
|
282
|
+
onProgress?.({
|
|
283
|
+
phase: "start",
|
|
284
|
+
pushed: 0,
|
|
285
|
+
skipped: 0,
|
|
286
|
+
deleted: 0,
|
|
287
|
+
totalPending,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Drain tombstones
|
|
291
|
+
const tombs = listTombstones(dbManager, providerId, accountKey, 1000);
|
|
292
|
+
for (const t of tombs) {
|
|
293
|
+
const fn = cursor.remoteFilenameMap?.[t.item_id];
|
|
294
|
+
if (!fn) {
|
|
295
|
+
deleteTombstone(dbManager, t.id);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const etag = cursor.remoteEtagMap?.[t.item_id] || null;
|
|
299
|
+
let res;
|
|
300
|
+
try {
|
|
301
|
+
res = await client.deleteFile(fn, etag);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
markTombstoneFailed(dbManager, t.id, err?.message || String(err));
|
|
304
|
+
lastError = err?.message || String(err);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (res.ok) {
|
|
308
|
+
deleteTombstone(dbManager, t.id);
|
|
309
|
+
removeFromMaps(dbManager, providerId, t.item_id, accountKey);
|
|
310
|
+
deleted++;
|
|
311
|
+
cursor = refresh();
|
|
312
|
+
maybeFlush("delete");
|
|
313
|
+
} else if (res.conflict) {
|
|
314
|
+
markTombstoneFailed(dbManager, t.id, "etag mismatch");
|
|
315
|
+
skipped++;
|
|
316
|
+
} else {
|
|
317
|
+
markTombstoneFailed(dbManager, t.id, res.error || "unknown");
|
|
318
|
+
lastError = res.error || lastError;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Push loop
|
|
323
|
+
pushLoop: while (true) {
|
|
324
|
+
const batch = fetchBatch(dbManager, cursor, 200);
|
|
325
|
+
if (batch.length === 0) break;
|
|
326
|
+
for (const item of batch) {
|
|
327
|
+
const filename = generateFilename(item);
|
|
328
|
+
const content = generateMarkdown(item);
|
|
329
|
+
const etag = cursor.remoteEtagMap?.[item.id] || null;
|
|
330
|
+
let res;
|
|
331
|
+
try {
|
|
332
|
+
res = await client.putFile(filename, content, etag);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
lastError = err?.message || String(err);
|
|
335
|
+
break pushLoop;
|
|
336
|
+
}
|
|
337
|
+
if (res.ok) {
|
|
338
|
+
recordPushedItem(
|
|
339
|
+
dbManager,
|
|
340
|
+
providerId,
|
|
341
|
+
item.id,
|
|
342
|
+
res.etag,
|
|
343
|
+
filename,
|
|
344
|
+
accountKey,
|
|
345
|
+
);
|
|
346
|
+
updateCursor(dbManager, providerId, cursorAfterItem(item), accountKey);
|
|
347
|
+
cursor = refresh();
|
|
348
|
+
pushed++;
|
|
349
|
+
maybeFlush("push");
|
|
350
|
+
} else if (res.conflict) {
|
|
351
|
+
skipped++;
|
|
352
|
+
updateCursor(dbManager, providerId, cursorAfterItem(item), accountKey);
|
|
353
|
+
cursor = refresh();
|
|
354
|
+
} else {
|
|
355
|
+
lastError = res.error || `status ${res.status}`;
|
|
356
|
+
break pushLoop;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const durationMs = Date.now() - t0;
|
|
362
|
+
const status = lastError ? "failed" : skipped > 0 ? "conflict" : "success";
|
|
363
|
+
updateCursor(
|
|
364
|
+
dbManager,
|
|
365
|
+
providerId,
|
|
366
|
+
{
|
|
367
|
+
lastRunStatus: status,
|
|
368
|
+
lastRunError: lastError,
|
|
369
|
+
lastRunDurationMs: durationMs,
|
|
370
|
+
itemsPushed: pushed,
|
|
371
|
+
itemsSkipped: skipped,
|
|
372
|
+
itemsDeleted: deleted,
|
|
373
|
+
},
|
|
374
|
+
accountKey,
|
|
375
|
+
);
|
|
376
|
+
onProgress?.({ phase: status, pushed, skipped, deleted, totalPending });
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
success: !lastError,
|
|
380
|
+
status,
|
|
381
|
+
pushed,
|
|
382
|
+
skipped,
|
|
383
|
+
deleted,
|
|
384
|
+
durationMs,
|
|
385
|
+
error: lastError || undefined,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export {
|
|
390
|
+
runSync,
|
|
391
|
+
generateFilename,
|
|
392
|
+
generateMarkdown,
|
|
393
|
+
getCursor,
|
|
394
|
+
ensureCursor,
|
|
395
|
+
updateCursor,
|
|
396
|
+
recordPushedItem,
|
|
397
|
+
removeFromMaps,
|
|
398
|
+
listTombstones,
|
|
399
|
+
deleteTombstone,
|
|
400
|
+
markTombstoneFailed,
|
|
401
|
+
fetchBatch,
|
|
402
|
+
cursorAfterItem,
|
|
403
|
+
countPending,
|
|
404
|
+
PROGRESS_FLUSH_EVERY,
|
|
405
|
+
PROGRESS_FLUSH_MS,
|
|
406
|
+
};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 / OSS client for CLI (Phase 3c follow-up Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* ESM port of desktop-app-vue/src/main/sync/oss-client.js — same API.
|
|
5
|
+
* Lazy-imports @aws-sdk/client-s3 so non-sync commands don't pay the
|
|
6
|
+
* load cost. Test seam _setS3LoaderForTest.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const RETRY_MAX = 3;
|
|
12
|
+
const RETRY_BASE_MS = 500;
|
|
13
|
+
const RETRY_MAX_MS = 8000;
|
|
14
|
+
|
|
15
|
+
let _s3Loader = async () => await import("@aws-sdk/client-s3");
|
|
16
|
+
|
|
17
|
+
function _setS3LoaderForTest(loader) {
|
|
18
|
+
_s3Loader = loader;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _resetS3LoaderForTest() {
|
|
22
|
+
_s3Loader = async () => await import("@aws-sdk/client-s3");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function _isRetriable(status) {
|
|
26
|
+
return status === 429 || (status >= 500 && status < 600);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _backoffMs(attempt) {
|
|
30
|
+
const base = RETRY_BASE_MS * Math.pow(2, attempt - 1);
|
|
31
|
+
const jitter = Math.random() * 0.3 * base;
|
|
32
|
+
return Math.min(RETRY_MAX_MS, Math.floor(base + jitter));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _sleep(ms) {
|
|
36
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _extractStatus(err) {
|
|
40
|
+
return (
|
|
41
|
+
err?.$metadata?.httpStatusCode ??
|
|
42
|
+
err?.statusCode ??
|
|
43
|
+
err?.status ??
|
|
44
|
+
err?.response?.status ??
|
|
45
|
+
null
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _extractEtag(headers) {
|
|
50
|
+
if (!headers) return null;
|
|
51
|
+
const raw = headers.ETag || headers.etag || null;
|
|
52
|
+
if (typeof raw !== "string") return null;
|
|
53
|
+
return raw.replace(/^"|"$/g, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _normalizePrefix(remotePath) {
|
|
57
|
+
return String(remotePath || "")
|
|
58
|
+
.replace(/^\/+/, "")
|
|
59
|
+
.replace(/\/+$/, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class OSSClient {
|
|
63
|
+
constructor(opts = {}) {
|
|
64
|
+
this.endpoint = String(opts.endpoint || "").trim();
|
|
65
|
+
this.region = opts.region || "auto";
|
|
66
|
+
this.bucket = String(opts.bucket || "").trim();
|
|
67
|
+
this.accessKeyId = opts.accessKeyId || "";
|
|
68
|
+
this.secretAccessKey = opts.secretAccessKey || "";
|
|
69
|
+
this.remotePath = _normalizePrefix(opts.remotePath || "");
|
|
70
|
+
this.forcePathStyle = opts.forcePathStyle === true;
|
|
71
|
+
this._client = null;
|
|
72
|
+
this._cmds = null;
|
|
73
|
+
if (!this.endpoint) throw new Error("OSSClient: endpoint 必填");
|
|
74
|
+
if (!this.bucket) throw new Error("OSSClient: bucket 必填");
|
|
75
|
+
if (!this.accessKeyId) throw new Error("OSSClient: accessKeyId 必填");
|
|
76
|
+
if (!this.secretAccessKey)
|
|
77
|
+
throw new Error("OSSClient: secretAccessKey 必填");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async _ensureClient() {
|
|
81
|
+
if (this._client) return this._client;
|
|
82
|
+
const mod = await _s3Loader();
|
|
83
|
+
const S3Client = mod.S3Client || mod.default?.S3Client;
|
|
84
|
+
if (typeof S3Client !== "function") {
|
|
85
|
+
throw new Error("@aws-sdk/client-s3 module missing S3Client");
|
|
86
|
+
}
|
|
87
|
+
this._cmds = {
|
|
88
|
+
HeadBucketCommand:
|
|
89
|
+
mod.HeadBucketCommand || mod.default?.HeadBucketCommand,
|
|
90
|
+
PutObjectCommand: mod.PutObjectCommand || mod.default?.PutObjectCommand,
|
|
91
|
+
DeleteObjectCommand:
|
|
92
|
+
mod.DeleteObjectCommand || mod.default?.DeleteObjectCommand,
|
|
93
|
+
HeadObjectCommand:
|
|
94
|
+
mod.HeadObjectCommand || mod.default?.HeadObjectCommand,
|
|
95
|
+
ListObjectsV2Command:
|
|
96
|
+
mod.ListObjectsV2Command || mod.default?.ListObjectsV2Command,
|
|
97
|
+
};
|
|
98
|
+
for (const [name, ctor] of Object.entries(this._cmds)) {
|
|
99
|
+
if (typeof ctor !== "function") {
|
|
100
|
+
throw new Error(`@aws-sdk/client-s3 missing ${name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this._client = new S3Client({
|
|
104
|
+
endpoint: this.endpoint,
|
|
105
|
+
region: this.region,
|
|
106
|
+
credentials: {
|
|
107
|
+
accessKeyId: this.accessKeyId,
|
|
108
|
+
secretAccessKey: this.secretAccessKey,
|
|
109
|
+
},
|
|
110
|
+
forcePathStyle: this.forcePathStyle,
|
|
111
|
+
});
|
|
112
|
+
return this._client;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_resolveKey(filename) {
|
|
116
|
+
const safe = String(filename || "").replace(/^\/+/, "");
|
|
117
|
+
if (!this.remotePath) return safe;
|
|
118
|
+
return `${this.remotePath}/${safe}`.replace(/\/{2,}/g, "/");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async _withRetry(label, fn) {
|
|
122
|
+
let lastErr;
|
|
123
|
+
for (let attempt = 1; attempt <= RETRY_MAX; attempt++) {
|
|
124
|
+
try {
|
|
125
|
+
return await fn();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
lastErr = err;
|
|
128
|
+
const status = _extractStatus(err);
|
|
129
|
+
if (!_isRetriable(status) || attempt === RETRY_MAX) throw err;
|
|
130
|
+
await _sleep(_backoffMs(attempt));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw lastErr;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async testConnection() {
|
|
137
|
+
try {
|
|
138
|
+
const client = await this._ensureClient();
|
|
139
|
+
const cmd = new this._cmds.HeadBucketCommand({ Bucket: this.bucket });
|
|
140
|
+
await this._withRetry("testConnection", () => client.send(cmd));
|
|
141
|
+
return { ok: true };
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const status = _extractStatus(err);
|
|
144
|
+
if (status === 404) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
status,
|
|
148
|
+
error: `Bucket ${this.bucket} 不存在;请确认名称大小写或先创建`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (status === 403) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
status,
|
|
155
|
+
error:
|
|
156
|
+
"认证失败 (accessKey/secretKey 错误,或账户对该 bucket 无访问权限)",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (status === 301 || status === 400) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
status,
|
|
163
|
+
error: `endpoint / region 不匹配:${err?.message || "请确认 region 与 endpoint 对应"}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { ok: false, status, error: err?.message || String(err) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async putFile(filename, content, etag = null) {
|
|
171
|
+
const key = this._resolveKey(filename);
|
|
172
|
+
try {
|
|
173
|
+
const client = await this._ensureClient();
|
|
174
|
+
const params = {
|
|
175
|
+
Bucket: this.bucket,
|
|
176
|
+
Key: key,
|
|
177
|
+
Body: content,
|
|
178
|
+
ContentType: "text/markdown; charset=utf-8",
|
|
179
|
+
};
|
|
180
|
+
if (etag) params.IfMatch = etag;
|
|
181
|
+
const cmd = new this._cmds.PutObjectCommand(params);
|
|
182
|
+
const res = await this._withRetry("putFile", () => client.send(cmd));
|
|
183
|
+
const newEtag = _extractEtag({ ETag: res?.ETag });
|
|
184
|
+
return { ok: true, etag: newEtag, raw: res };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
const status = _extractStatus(err);
|
|
187
|
+
if (status === 412) return { ok: false, conflict: true, status };
|
|
188
|
+
if (status === 501) return { ok: false, conflict: true, status };
|
|
189
|
+
return { ok: false, error: err?.message || String(err), status };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async deleteFile(filename, _etag = null) {
|
|
194
|
+
const key = this._resolveKey(filename);
|
|
195
|
+
try {
|
|
196
|
+
const client = await this._ensureClient();
|
|
197
|
+
const cmd = new this._cmds.DeleteObjectCommand({
|
|
198
|
+
Bucket: this.bucket,
|
|
199
|
+
Key: key,
|
|
200
|
+
});
|
|
201
|
+
await this._withRetry("deleteFile", () => client.send(cmd));
|
|
202
|
+
return { ok: true };
|
|
203
|
+
} catch (err) {
|
|
204
|
+
const status = _extractStatus(err);
|
|
205
|
+
if (status === 404) return { ok: true, alreadyAbsent: true };
|
|
206
|
+
return { ok: false, error: err?.message || String(err), status };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async getEtag(filename) {
|
|
211
|
+
const key = this._resolveKey(filename);
|
|
212
|
+
try {
|
|
213
|
+
const client = await this._ensureClient();
|
|
214
|
+
const cmd = new this._cmds.HeadObjectCommand({
|
|
215
|
+
Bucket: this.bucket,
|
|
216
|
+
Key: key,
|
|
217
|
+
});
|
|
218
|
+
const res = await this._withRetry("getEtag", () => client.send(cmd));
|
|
219
|
+
return _extractEtag({ ETag: res?.ETag });
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (_extractStatus(err) === 404) return null;
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async listRemote(subPath = "") {
|
|
227
|
+
const prefix = subPath
|
|
228
|
+
? this._resolveKey(subPath).replace(/\/?$/, "/")
|
|
229
|
+
: this.remotePath
|
|
230
|
+
? this.remotePath + "/"
|
|
231
|
+
: "";
|
|
232
|
+
const client = await this._ensureClient();
|
|
233
|
+
const items = [];
|
|
234
|
+
let continuationToken = undefined;
|
|
235
|
+
do {
|
|
236
|
+
const cmd = new this._cmds.ListObjectsV2Command({
|
|
237
|
+
Bucket: this.bucket,
|
|
238
|
+
Prefix: prefix,
|
|
239
|
+
ContinuationToken: continuationToken,
|
|
240
|
+
});
|
|
241
|
+
const res = await this._withRetry("listRemote", () => client.send(cmd));
|
|
242
|
+
const contents = res?.Contents || [];
|
|
243
|
+
for (const obj of contents) {
|
|
244
|
+
const key = obj.Key || "";
|
|
245
|
+
if (!key.endsWith(".md")) continue;
|
|
246
|
+
const basename = key.includes("/")
|
|
247
|
+
? key.slice(key.lastIndexOf("/") + 1)
|
|
248
|
+
: key;
|
|
249
|
+
items.push({
|
|
250
|
+
filename: basename,
|
|
251
|
+
key,
|
|
252
|
+
etag: _extractEtag({ ETag: obj.ETag }),
|
|
253
|
+
size: obj.Size ?? 0,
|
|
254
|
+
lastmod: obj.LastModified
|
|
255
|
+
? new Date(obj.LastModified).toISOString()
|
|
256
|
+
: null,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
continuationToken = res?.IsTruncated
|
|
260
|
+
? res.NextContinuationToken
|
|
261
|
+
: undefined;
|
|
262
|
+
} while (continuationToken);
|
|
263
|
+
return items;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export {
|
|
268
|
+
OSSClient,
|
|
269
|
+
RETRY_MAX,
|
|
270
|
+
RETRY_BASE_MS,
|
|
271
|
+
_setS3LoaderForTest,
|
|
272
|
+
_resetS3LoaderForTest,
|
|
273
|
+
};
|