chainlesschain 0.162.12 → 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-C3TDNq29.js → AIOps-Bq_zxhCr.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C9fE18pE.js → ActionButton-CaevDm9t.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-wnZF602C.js → Analytics-B0gOmwPw.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-BjgTMK7O.js → AppLayout-DWhZiV0Q.js} +2 -2
- package/src/assets/web-panel/assets/{Audit-BBL0BW5_.js → Audit-ZuZJBCxo.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BKLqYCWU.js → Backup-CX7jhH5l.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BGSzMCZs.js → BaseInput-CVLx7HVq.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CQWzZWEY.js → Chat-C6yL5tRD.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BkTri12Q.js → Checkbox-BePhbqVq.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BH1m09EO.js → Codegen-B5E4x1Lm.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BXnBuqIa.js → Col-CWhNU6A7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-C_Nr4XCx.js → Community-mSEAuJhp.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Du6GwLrj.js → Compact-DSudHzX3.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-66M0oi1Q.js → Compliance-CxJLYjyn.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DQrkZRNd.js → Cowork-C-trppQj.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-CwdIFH_v.js → Cron-_Ij4v5fY.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-DqlcrQ9L.js → Crosschain-eLHXzp5T.js} +1 -1
- package/src/assets/web-panel/assets/{DID-OmPLKf7L.js → DID-BP04AUUB.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-D_dampTL.js → Dashboard-CzEeQv62.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CA1W7jAn.js → Dropdown-C_SGOB22.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Chlk9a7s.js → Federation-BgaP4BOv.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-t0UqYFLq.js → FormItemContext-BxGLLt9r.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CEq0raYm.js → Git-Bt_uM_Gw.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-C06CX7Ge.js → Governance-N2-0RG_o.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-6VIFHxIP.js → Inference-eS3g-CzP.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BCJPjMBQ.js → KnowledgeGraph-CUuvNVah.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BBpOYFct.js → Logs-wPbwEt2r.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BFH6jMWt.js → Marketplace-DH91kTwo.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-uCFvRqGs.js → McpTools-D-GyyBrI.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-B0Kux_KT.js → Memory-BOtUy-tw.js} +1 -1
- package/src/assets/web-panel/assets/{MobileBridge-DHow2jiK.js → MobileBridge-DIP__XQd.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BFo9YQZp.js → MobileProjects-1nqr1UsU.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-riOh1G_F.js → Mtc-CCE0x7h2.js} +1 -1
- package/src/assets/web-panel/assets/{MtcAudit-Bm-hE2SP.js → MtcAudit-BBkz0XUO.js} +1 -1
- package/src/assets/web-panel/assets/{Multisig-DfUQxh5a.js → Multisig-CIKSJvTY.js} +2 -2
- package/src/assets/web-panel/assets/{NLProgramming-DuNvLBEq.js → NLProgramming-BDkgeFcq.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DB20wd3c.js → Notes-ONiUxfN1.js} +1 -1
- package/src/assets/web-panel/assets/{NotificationSettings-CB-GkOWR.js → NotificationSettings-CGcyKEIe.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-3bU7PZuG.js → Organization-BZtMYBJt.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BGCPP_0Y.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-BHgAe1oC.js → P2P-D71Cpk-m.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-BuOD4xwc.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-DBS5U4LB.js → Pipeline-BOyp0_Qo.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-UNjIc5El.js → Privacy-a_AcphvF.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CicqCJGy.js → ProjectInit-CFu1grYt.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectSettings-CIxAbt4Y.js → ProjectSettings-p54Eivhh.js} +1 -1
- package/src/assets/web-panel/assets/{Projects-BJycZScO.js → Projects-DkB88XAu.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DxXvprme.js → Providers-EK7f8DEd.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-rrqjU8_Y.js → QuickAsk-CpO0w3iP.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BEwHMhI7.js → Recommend-BUJVQdv9.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-DoVKCCMn.js → Reputation-kU2fOuZt.js} +1 -1
- package/src/assets/web-panel/assets/{Row-F5XcDhHr.js → Row-oGWRbk6g.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-cZrRG7k8.js → RssFeed-saC_46Yo.js} +1 -1
- package/src/assets/web-panel/assets/{Search-B9ctZjqx.js → Search-CjtRqFUu.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Z62hl1mc.js → Security-BX0NBVfQ.js} +1 -1
- package/src/assets/web-panel/assets/{Services-CQf5XqgZ.js → Services-Bgxsnei_.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-DuCKw2Eh.js → Skeleton-CwBb3k2a.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-qVkhva0s.js → Skills-CZCMYH6Z.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BQbatr7s.js → Sla-Djy1uHnZ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DLFBzAgD.js → SpeechSettings-CGUI_Uyh.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CrzETZMW.js → SyncSettings-DK2CKHRD.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-D_EQ1nJ7.js → Tasks-BKiOzeIO.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-D4y-dGRc.js → Templates-CnQpleXj.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-2XI0jkPn.js → Tenant-DwKz0cjm.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-fUi5V2Z9.js → Terminal-A7t_wsR8.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BuUNB2mg.js → Tokens-BqYY9l44.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-7DqLLuej.js → Trigger-BI4bXFmi.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CeACvTYx.js → Trust-yMynKTRG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-mDP9EXHq.js → UkeySign-Br4IScM6.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-veWlKclv.js → VideoEditing-CWcThGsP.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Cd2Hheb8.js → Wallet-CZcAtjxj.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-DyL7ZiHX.js → WebAuthn-BnTZFMA0.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-C7-7LJH9.js → WorkflowEditor-N7gGz3_n.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DXomZMuo.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-DlU92QNs.js → colors-LKhZyttv.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-sBiTL8mX.js → compact-item-CsJSebxT.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DZXEnzum.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-CpCHBZ2M.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-ClN_JuFa.js → index-6qPbrYF7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDyzZ8_O.js → index-B7UYymse.js} +1 -1
- package/src/assets/web-panel/assets/{index-C52udT0_.js → index-BDSZDDb2.js} +4 -4
- package/src/assets/web-panel/assets/{index-s8tvk-fF.js → index-BEDFHKO3.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_IgY63-.js → index-BMvdoiFr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dyg6ikIL.js → index-BNVLVzN5.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0Qbxr57.js → index-BSNibAqz.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeGnHcp5.js → index-BV-__mlC.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_-RETt0.js → index-BXH9ujMW.js} +1 -1
- package/src/assets/web-panel/assets/{index-XI6772AD.js → index-BZluCuTH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqnhEJls.js → index-BhiZDGg7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BK2AFy44.js → index-BjOrt4vw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn8m1d1f.js → index-BmJdof_c.js} +2 -2
- package/src/assets/web-panel/assets/{index-C6SDf50u.js → index-BsirlkJ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CD3iljXs.js → index-BvF2tC6C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJ7XYa5K.js → index-C2HBKw07.js} +1 -1
- package/src/assets/web-panel/assets/{index-_zyXBoS7.js → index-CKjBAdm0.js} +1 -1
- package/src/assets/web-panel/assets/{index-X48zYgZ6.js → index-CRGNuUIM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dj9Nvz6S.js → index-CTIkCKav.js} +1 -1
- package/src/assets/web-panel/assets/index-CY1mQA2I.js +1 -0
- package/src/assets/web-panel/assets/{index-C59FgSkU.js → index-CyHdYUeZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B23tuoo9.js → index-D401L3yx.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKe9jmKG.js → index-D5IZCkZG.js} +1 -1
- package/src/assets/web-panel/assets/{index-F9cBucYf.js → index-D7ZcBI5s.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjXSvceY.js → index-D8kltMTW.js} +1 -1
- package/src/assets/web-panel/assets/index-D9nXHfUB.js +1 -0
- package/src/assets/web-panel/assets/{index-Dq5Rn5VS.js → index-DJVkBmSc.js} +1 -1
- package/src/assets/web-panel/assets/{index-ibFHnqHz.js → index-DM3uBEWD.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChahjdYE.js → index-DOO73rHE.js} +1 -1
- package/src/assets/web-panel/assets/{index-CivbS-57.js → index-DXp1jVsK.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3k9UPHc.js → index-Dd7dICwB.js} +1 -1
- package/src/assets/web-panel/assets/{index-Di1_EQ-X.js → index-Dm-3kvtD.js} +1 -1
- package/src/assets/web-panel/assets/{index-DygNvCeR.js → index-DnPt5OdD.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_MzScPM.js → index-E7t1hAnk.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUe5t5Aa.js → index-JTX9A7w0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwbWZubA.js → index-Kn-Of5ew.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUlPMzoM.js → index-R1cFADfk.js} +1 -1
- package/src/assets/web-panel/assets/{index-TwQZkVGh.js → index-RIO4JKMP.js} +1 -1
- package/src/assets/web-panel/assets/{index-ThrAiEF9.js → index-uTEVWPYA.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DEi92ZnZ.js → initDefaultProps-CBW0okek.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BtYKzpOc.js → motion-DGAffQ0Z.js} +1 -1
- package/src/assets/web-panel/assets/{move-Cb3A1-v-.js → move-DFJ0-5IW.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B6qPDdOf.js → omit-AvrDghg1.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DDyeQMUc.js → pickAttrs-D7csw9i1.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BPV6VO47.js → placementArrow-hZ6Lg6kG.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DJ1ra4dT.js → responsiveObserve-DLLx5VvS.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D6v8tHvB.js → slide-BaRIT3ev.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DulKcQLZ.js → statusUtils-Cdjyuhrz.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Bne7zwMt.js → styleChecker-CbrNybTt.js} +1 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-B8gAhiRC.js +1 -0
- package/src/assets/web-panel/assets/{useFs-CR-iLa4Z.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-yL9axxBy.js → vnode-6Y0NDMVv.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B-VCMXSD.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 +417 -0
- package/src/commands/sync-providers.js +436 -0
- package/src/gateways/ws/personal-data-hub-protocol.js +68 -0
- package/src/index.js +4 -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--WA-aZAJ.js +0 -1
- package/src/assets/web-panel/assets/PersonalDataHub-BK7I0Rsb.css +0 -1
- package/src/assets/web-panel/assets/index-CcRX6BlT.js +0 -1
- package/src/assets/web-panel/assets/index-z6h6tqP3.js +0 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-C1miTomM.js +0 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync-engine-cli 单元测试 — Phase 3c follow-up Phase 2.
|
|
3
|
+
*
|
|
4
|
+
* 用 sql.js 内存 SQLite 模拟 dbManager.run/all/get;fake client;验证
|
|
5
|
+
* runSync 完整管线(push / conflict / hard error / tombstone drain /
|
|
6
|
+
* cursor 推进)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeAll, beforeEach, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
runSync,
|
|
13
|
+
generateFilename,
|
|
14
|
+
generateMarkdown,
|
|
15
|
+
getCursor,
|
|
16
|
+
ensureCursor,
|
|
17
|
+
listTombstones,
|
|
18
|
+
} from "../sync-engine-cli.js";
|
|
19
|
+
|
|
20
|
+
class SqlJsDbManager {
|
|
21
|
+
constructor(db) {
|
|
22
|
+
this.db = db;
|
|
23
|
+
}
|
|
24
|
+
run(sql, params = []) {
|
|
25
|
+
this.db.run(sql, params);
|
|
26
|
+
}
|
|
27
|
+
get(sql, params = []) {
|
|
28
|
+
const stmt = this.db.prepare(sql);
|
|
29
|
+
stmt.bind(params);
|
|
30
|
+
let row;
|
|
31
|
+
if (stmt.step()) row = stmt.getAsObject();
|
|
32
|
+
stmt.free();
|
|
33
|
+
return row;
|
|
34
|
+
}
|
|
35
|
+
all(sql, params = []) {
|
|
36
|
+
const stmt = this.db.prepare(sql);
|
|
37
|
+
stmt.bind(params);
|
|
38
|
+
const rows = [];
|
|
39
|
+
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
40
|
+
stmt.free();
|
|
41
|
+
return rows;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let SQL;
|
|
46
|
+
let dbManager;
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
const initSqlJs = (await import("sql.js")).default;
|
|
50
|
+
SQL = await initSqlJs();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
const db = new SQL.Database();
|
|
55
|
+
db.exec(`
|
|
56
|
+
CREATE TABLE knowledge_items (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
title TEXT NOT NULL,
|
|
59
|
+
type TEXT NOT NULL DEFAULT 'note',
|
|
60
|
+
content TEXT,
|
|
61
|
+
tags TEXT,
|
|
62
|
+
created_at INTEGER NOT NULL,
|
|
63
|
+
updated_at INTEGER NOT NULL
|
|
64
|
+
);
|
|
65
|
+
CREATE TABLE sync_external_provider_cursor (
|
|
66
|
+
provider_id TEXT NOT NULL,
|
|
67
|
+
account_key TEXT NOT NULL DEFAULT '',
|
|
68
|
+
last_sync_at INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
last_item_id TEXT,
|
|
70
|
+
remote_etag_map TEXT NOT NULL DEFAULT '{}',
|
|
71
|
+
remote_filename_map TEXT NOT NULL DEFAULT '{}',
|
|
72
|
+
last_run_status TEXT,
|
|
73
|
+
last_run_error TEXT,
|
|
74
|
+
last_run_duration_ms INTEGER,
|
|
75
|
+
items_pushed INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
items_skipped INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
items_deleted INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),
|
|
79
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),
|
|
80
|
+
PRIMARY KEY (provider_id, account_key)
|
|
81
|
+
);
|
|
82
|
+
CREATE TABLE sync_external_tombstones (
|
|
83
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
+
provider_id TEXT NOT NULL,
|
|
85
|
+
account_key TEXT NOT NULL DEFAULT '',
|
|
86
|
+
item_id TEXT NOT NULL,
|
|
87
|
+
resource_type TEXT,
|
|
88
|
+
deleted_at INTEGER NOT NULL,
|
|
89
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
last_error TEXT,
|
|
91
|
+
UNIQUE(provider_id, account_key, item_id)
|
|
92
|
+
);
|
|
93
|
+
CREATE TRIGGER trg_sync_ext_tombstone_on_delete
|
|
94
|
+
AFTER DELETE ON knowledge_items
|
|
95
|
+
FOR EACH ROW
|
|
96
|
+
BEGIN
|
|
97
|
+
INSERT OR IGNORE INTO sync_external_tombstones
|
|
98
|
+
(provider_id, account_key, item_id, resource_type, deleted_at)
|
|
99
|
+
SELECT c.provider_id, c.account_key, OLD.id, 'KNOWLEDGE_ITEM',
|
|
100
|
+
(strftime('%s','now') * 1000)
|
|
101
|
+
FROM sync_external_provider_cursor c;
|
|
102
|
+
END;
|
|
103
|
+
`);
|
|
104
|
+
dbManager = new SqlJsDbManager(db);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function seedItems(items) {
|
|
108
|
+
for (const it of items) {
|
|
109
|
+
dbManager.run(
|
|
110
|
+
`INSERT INTO knowledge_items (id, title, type, content, created_at, updated_at)
|
|
111
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
112
|
+
[
|
|
113
|
+
it.id,
|
|
114
|
+
it.title,
|
|
115
|
+
it.type ?? "note",
|
|
116
|
+
it.content ?? "body",
|
|
117
|
+
it.created_at ?? 1,
|
|
118
|
+
it.updated_at ?? 100,
|
|
119
|
+
],
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function makeFakeClient(overrides = {}) {
|
|
125
|
+
return {
|
|
126
|
+
putFile: vi.fn(
|
|
127
|
+
overrides.putFile ?? (async () => ({ ok: true, etag: "new-etag" })),
|
|
128
|
+
),
|
|
129
|
+
deleteFile: vi.fn(overrides.deleteFile ?? (async () => ({ ok: true }))),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const baseDeps = () => ({ dbManager, providerId: "webdav", accountKey: "" });
|
|
134
|
+
|
|
135
|
+
describe("sync-engine-cli · renderer helpers", () => {
|
|
136
|
+
it("generateFilename cleans unsafe chars + collapses runs", () => {
|
|
137
|
+
const fn = generateFilename({ id: "abc", title: "My / cool: note?" });
|
|
138
|
+
expect(fn).toBe("abc-My_cool_note.md");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("generateMarkdown emits front-matter", () => {
|
|
142
|
+
const md = generateMarkdown({
|
|
143
|
+
id: "x",
|
|
144
|
+
title: "T",
|
|
145
|
+
type: "note",
|
|
146
|
+
content: "body",
|
|
147
|
+
created_at: 1,
|
|
148
|
+
updated_at: 2,
|
|
149
|
+
tags: "a,b",
|
|
150
|
+
});
|
|
151
|
+
expect(md).toContain("---");
|
|
152
|
+
expect(md).toContain('title: "T"');
|
|
153
|
+
expect(md).toContain("tags: a,b");
|
|
154
|
+
expect(md.endsWith("body\n")).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("sync-engine-cli · ensureCursor + getCursor", () => {
|
|
159
|
+
it("ensureCursor creates row + returns it", () => {
|
|
160
|
+
const cursor = ensureCursor(dbManager, "webdav");
|
|
161
|
+
expect(cursor).toBeDefined();
|
|
162
|
+
expect(cursor.providerId).toBe("webdav");
|
|
163
|
+
expect(cursor.itemsPushed).toBe(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("getCursor returns undefined when missing", () => {
|
|
167
|
+
expect(getCursor(dbManager, "oss")).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("sync-engine-cli · runSync happy path", () => {
|
|
172
|
+
it("pushes all items + advances cursor", async () => {
|
|
173
|
+
seedItems([
|
|
174
|
+
{ id: "a", title: "A", updated_at: 100 },
|
|
175
|
+
{ id: "b", title: "B", updated_at: 200 },
|
|
176
|
+
]);
|
|
177
|
+
const client = makeFakeClient();
|
|
178
|
+
const res = await runSync({ ...baseDeps(), client });
|
|
179
|
+
expect(res.success).toBe(true);
|
|
180
|
+
expect(res.status).toBe("success");
|
|
181
|
+
expect(res.pushed).toBe(2);
|
|
182
|
+
expect(client.putFile).toHaveBeenCalledTimes(2);
|
|
183
|
+
const cursor = getCursor(dbManager, "webdav");
|
|
184
|
+
expect(cursor.lastItemId).toBe("b");
|
|
185
|
+
expect(cursor.itemsPushed).toBe(2);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("re-run from advanced cursor pushes nothing new", async () => {
|
|
189
|
+
seedItems([{ id: "a", title: "A", updated_at: 100 }]);
|
|
190
|
+
const client = makeFakeClient();
|
|
191
|
+
await runSync({ ...baseDeps(), client });
|
|
192
|
+
client.putFile.mockClear();
|
|
193
|
+
const r2 = await runSync({ ...baseDeps(), client });
|
|
194
|
+
expect(r2.pushed).toBe(0);
|
|
195
|
+
expect(client.putFile).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("sends If-Match etag on modified item", async () => {
|
|
199
|
+
seedItems([{ id: "a", title: "A", updated_at: 100 }]);
|
|
200
|
+
const client = makeFakeClient();
|
|
201
|
+
await runSync({ ...baseDeps(), client });
|
|
202
|
+
dbManager.run(`UPDATE knowledge_items SET updated_at = 999 WHERE id = 'a'`);
|
|
203
|
+
client.putFile.mockClear();
|
|
204
|
+
await runSync({ ...baseDeps(), client });
|
|
205
|
+
expect(client.putFile).toHaveBeenCalledWith(
|
|
206
|
+
"a-A.md",
|
|
207
|
+
expect.any(String),
|
|
208
|
+
"new-etag",
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("sync-engine-cli · conflict path (412)", () => {
|
|
214
|
+
it("412 → skipped + cursor advances + status=conflict", async () => {
|
|
215
|
+
seedItems([
|
|
216
|
+
{ id: "a", title: "A", updated_at: 100 },
|
|
217
|
+
{ id: "b", title: "B", updated_at: 200 },
|
|
218
|
+
]);
|
|
219
|
+
const client = makeFakeClient({
|
|
220
|
+
putFile: vi.fn(async (fn) =>
|
|
221
|
+
fn === "a-A.md"
|
|
222
|
+
? { ok: false, conflict: true, status: 412 }
|
|
223
|
+
: { ok: true, etag: "e" },
|
|
224
|
+
),
|
|
225
|
+
});
|
|
226
|
+
const res = await runSync({ ...baseDeps(), client });
|
|
227
|
+
expect(res.status).toBe("conflict");
|
|
228
|
+
expect(res.pushed).toBe(1);
|
|
229
|
+
expect(res.skipped).toBe(1);
|
|
230
|
+
expect(getCursor(dbManager, "webdav").lastItemId).toBe("b");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("sync-engine-cli · hard error stops loop", () => {
|
|
235
|
+
it("4xx non-conflict → no advance past failure point", async () => {
|
|
236
|
+
seedItems([
|
|
237
|
+
{ id: "a", title: "A", updated_at: 100 },
|
|
238
|
+
{ id: "b", title: "B", updated_at: 200 },
|
|
239
|
+
]);
|
|
240
|
+
const client = makeFakeClient({
|
|
241
|
+
putFile: vi.fn(async (fn) =>
|
|
242
|
+
fn === "b-B.md"
|
|
243
|
+
? { ok: false, status: 403, error: "forbidden" }
|
|
244
|
+
: { ok: true, etag: "e" },
|
|
245
|
+
),
|
|
246
|
+
});
|
|
247
|
+
const res = await runSync({ ...baseDeps(), client });
|
|
248
|
+
expect(res.success).toBe(false);
|
|
249
|
+
expect(res.status).toBe("failed");
|
|
250
|
+
expect(res.pushed).toBe(1);
|
|
251
|
+
expect(res.error).toMatch(/forbidden/);
|
|
252
|
+
expect(getCursor(dbManager, "webdav").lastItemId).toBe("a");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("sync-engine-cli · tombstone drain", () => {
|
|
257
|
+
it("drains tombstones via deleteFile + cleans maps", async () => {
|
|
258
|
+
seedItems([{ id: "a", title: "A", updated_at: 100 }]);
|
|
259
|
+
const client = makeFakeClient();
|
|
260
|
+
await runSync({ ...baseDeps(), client });
|
|
261
|
+
dbManager.run(`DELETE FROM knowledge_items WHERE id = 'a'`);
|
|
262
|
+
const res = await runSync({ ...baseDeps(), client });
|
|
263
|
+
expect(res.deleted).toBe(1);
|
|
264
|
+
expect(client.deleteFile).toHaveBeenCalledWith("a-A.md", "new-etag");
|
|
265
|
+
expect(listTombstones(dbManager, "webdav")).toHaveLength(0);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("sync-engine-cli · provider isolation", () => {
|
|
270
|
+
it("oss cursor independent of webdav cursor", async () => {
|
|
271
|
+
seedItems([{ id: "a", title: "A", updated_at: 100 }]);
|
|
272
|
+
const client = makeFakeClient();
|
|
273
|
+
await runSync({ dbManager, client, providerId: "webdav", accountKey: "" });
|
|
274
|
+
const webdav = getCursor(dbManager, "webdav");
|
|
275
|
+
const oss = getCursor(dbManager, "oss");
|
|
276
|
+
expect(webdav.itemsPushed).toBe(1);
|
|
277
|
+
expect(oss).toBeUndefined();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("sync-engine-cli · progress callback", () => {
|
|
282
|
+
it("emits start + final events", async () => {
|
|
283
|
+
seedItems([{ id: "a", title: "A", updated_at: 100 }]);
|
|
284
|
+
const events = [];
|
|
285
|
+
await runSync({
|
|
286
|
+
...baseDeps(),
|
|
287
|
+
client: makeFakeClient(),
|
|
288
|
+
onProgress: (e) => events.push(e),
|
|
289
|
+
});
|
|
290
|
+
expect(events.find((e) => e.phase === "start")).toBeDefined();
|
|
291
|
+
expect(events[events.length - 1].phase).toBe("success");
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIChat WebView 鉴权向导 — cli / web-shell wiring (Phase 10.3.4).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `desktop-app-vue/src/main/personal-data-hub/aichat-wizard-factory.js`
|
|
5
|
+
* for the cli + web-shell side. Two structural differences from desktop:
|
|
6
|
+
*
|
|
7
|
+
* 1) `fallbackMode: "paste"` is hard-wired — cc ui does NOT have a
|
|
8
|
+
* BrowserView, so the wizard always returns the paste fallback shape.
|
|
9
|
+
*
|
|
10
|
+
* 2) Accounts store path lives under `getHub().hubDir`, same JSON file
|
|
11
|
+
* shape as desktop (`aichat-accounts.json`). Same hub directory on
|
|
12
|
+
* the same machine means desktop / cc ui share registered vendors
|
|
13
|
+
* (per the memo about WAL sharing — same caveat applies).
|
|
14
|
+
*
|
|
15
|
+
* Reference: docs/design/Personal_Data_Hub_Phase_10_3_AIChat_WebView_Wizard.md §2 §11
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_VENDOR_SPECS,
|
|
24
|
+
HttpClient,
|
|
25
|
+
CookieAuthSession,
|
|
26
|
+
} from "@chainlesschain/personal-data-hub/adapters/ai-chat-history";
|
|
27
|
+
|
|
28
|
+
const _require = createRequire(import.meta.url);
|
|
29
|
+
|
|
30
|
+
export const ACCOUNTS_FILE = "aichat-accounts.json";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* JSON file accountsStore (same on-disk shape as desktop). Async API but
|
|
34
|
+
* implemented sync underneath — concurrent put() ops are chained.
|
|
35
|
+
*/
|
|
36
|
+
export function createAccountsStore({ hubDir }) {
|
|
37
|
+
if (!hubDir || typeof hubDir !== "string") {
|
|
38
|
+
throw new Error("aichat-wizard-cli: hubDir required");
|
|
39
|
+
}
|
|
40
|
+
const filePath = join(hubDir, ACCOUNTS_FILE);
|
|
41
|
+
let writeChain = Promise.resolve();
|
|
42
|
+
|
|
43
|
+
function _readAll() {
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err && err.code === "ENOENT") return {};
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function get(vendor) {
|
|
55
|
+
return _readAll()[vendor] || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function put(vendor, entry) {
|
|
59
|
+
writeChain = writeChain.then(async () => {
|
|
60
|
+
const all = _readAll();
|
|
61
|
+
all[vendor] = entry;
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(hubDir, { recursive: true, mode: 0o700 });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (!err || err.code !== "EEXIST") throw err;
|
|
66
|
+
}
|
|
67
|
+
writeFileSync(filePath, JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
68
|
+
});
|
|
69
|
+
return writeChain;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function del(vendor) {
|
|
73
|
+
writeChain = writeChain.then(async () => {
|
|
74
|
+
const all = _readAll();
|
|
75
|
+
if (!(vendor in all)) return;
|
|
76
|
+
delete all[vendor];
|
|
77
|
+
if (Object.keys(all).length === 0) {
|
|
78
|
+
try {
|
|
79
|
+
unlinkSync(filePath);
|
|
80
|
+
} catch (_e) {
|
|
81
|
+
/* missing is fine */
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
writeFileSync(filePath, JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return writeChain;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function list() {
|
|
91
|
+
return Object.values(_readAll());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { get, put, delete: del, list, _filePath: filePath };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Same bridge as desktop — translates wizard.registerVendor() into
|
|
99
|
+
* spec.validateCookie(). Kept duplicated so the cli build stays ESM-pure
|
|
100
|
+
* without reaching into desktop-app-vue.
|
|
101
|
+
*/
|
|
102
|
+
export function createVendorAdapterBridge({
|
|
103
|
+
specs = DEFAULT_VENDOR_SPECS,
|
|
104
|
+
_httpClientFactory,
|
|
105
|
+
} = {}) {
|
|
106
|
+
// DEFAULT_VENDOR_SPECS is shipped as a vendor-keyed object; tests pass an
|
|
107
|
+
// array. Normalize to an array first so byVendor lookup works either way.
|
|
108
|
+
const arr = Array.isArray(specs)
|
|
109
|
+
? specs
|
|
110
|
+
: specs && typeof specs === "object"
|
|
111
|
+
? Object.values(specs)
|
|
112
|
+
: null;
|
|
113
|
+
if (!arr || arr.length === 0) {
|
|
114
|
+
throw new Error("aichat-wizard-cli: specs required");
|
|
115
|
+
}
|
|
116
|
+
const byVendor = new Map();
|
|
117
|
+
for (const s of arr) {
|
|
118
|
+
if (s && typeof s.name === "string") byVendor.set(s.name, s);
|
|
119
|
+
}
|
|
120
|
+
const buildClient =
|
|
121
|
+
_httpClientFactory ||
|
|
122
|
+
((vendor, spec) => new HttpClient({ vendor, rateLimits: spec.rateLimits }));
|
|
123
|
+
|
|
124
|
+
async function registerVendor(vendor, cookies, _opts = {}) {
|
|
125
|
+
const spec = byVendor.get(vendor);
|
|
126
|
+
if (!spec) return { ok: false, reason: "UNKNOWN_VENDOR" };
|
|
127
|
+
if (typeof spec.validateCookie !== "function") {
|
|
128
|
+
return { ok: false, reason: "SPEC_MISSING_VALIDATE_COOKIE" };
|
|
129
|
+
}
|
|
130
|
+
let client;
|
|
131
|
+
try {
|
|
132
|
+
client = buildClient(vendor, spec);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: "HTTP_CLIENT_INIT_FAILED",
|
|
137
|
+
error: err.message,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const session = new CookieAuthSession({
|
|
141
|
+
vendor,
|
|
142
|
+
cookies: _jarToArray(cookies),
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
const r = await spec.validateCookie({ httpClient: client, session });
|
|
146
|
+
return r || { ok: false, reason: "VALIDATE_RETURNED_NULL" };
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return { ok: false, reason: "VALIDATE_THREW", error: err.message };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { registerVendor };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function _jarToArray(input) {
|
|
156
|
+
if (Array.isArray(input)) {
|
|
157
|
+
return input.filter(
|
|
158
|
+
(c) => c && typeof c.name === "string" && typeof c.value === "string",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (typeof input === "string") {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const pair of input.split(/;\s*/)) {
|
|
164
|
+
const idx = pair.indexOf("=");
|
|
165
|
+
if (idx <= 0) continue;
|
|
166
|
+
const name = pair.slice(0, idx).trim();
|
|
167
|
+
const value = pair.slice(idx + 1).trim();
|
|
168
|
+
if (name && value) out.push({ name, value });
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
if (input && typeof input === "object") {
|
|
173
|
+
const out = [];
|
|
174
|
+
for (const [name, value] of Object.entries(input)) {
|
|
175
|
+
if (typeof value === "string" && value.length > 0)
|
|
176
|
+
out.push({ name, value });
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const _wizardsByHubDir = new Map();
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* cli-side wizard singleton. Always builds in `fallbackMode: "paste"` —
|
|
187
|
+
* cc ui never opens a BrowserView. Tests substitute `_accountsStore` and
|
|
188
|
+
* `_vendorAdapter` for hermetic runs.
|
|
189
|
+
*/
|
|
190
|
+
export function getAIChatWizard({
|
|
191
|
+
hubDir,
|
|
192
|
+
_accountsStore,
|
|
193
|
+
_vendorAdapter,
|
|
194
|
+
_deps,
|
|
195
|
+
} = {}) {
|
|
196
|
+
if (!hubDir) throw new Error("aichat-wizard-cli: hubDir required");
|
|
197
|
+
const isTest = !!(_accountsStore || _vendorAdapter || _deps);
|
|
198
|
+
if (!isTest && _wizardsByHubDir.has(hubDir))
|
|
199
|
+
return _wizardsByHubDir.get(hubDir);
|
|
200
|
+
|
|
201
|
+
const accountsStore = _accountsStore || createAccountsStore({ hubDir });
|
|
202
|
+
const vendorAdapter = _vendorAdapter || createVendorAdapterBridge();
|
|
203
|
+
// Paste-mode is the default for cli. Tests can override via _deps.
|
|
204
|
+
const deps = _deps || {
|
|
205
|
+
sessionFactory: () => ({}),
|
|
206
|
+
clock: () => Date.now(),
|
|
207
|
+
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
|
208
|
+
fallbackMode: "paste",
|
|
209
|
+
};
|
|
210
|
+
if (deps && !deps.fallbackMode) deps.fallbackMode = "paste";
|
|
211
|
+
// The controller resolves classifier / specLookup / knownVendors itself
|
|
212
|
+
// when _deps doesn't supply them — but since we ARE supplying _deps, we
|
|
213
|
+
// must fill those slots too. Re-import the spec module to avoid coupling
|
|
214
|
+
// tests to those exact defaults.
|
|
215
|
+
if (!deps.classifier || !deps.specLookup) {
|
|
216
|
+
// Lazy require to keep top-of-file clean.
|
|
217
|
+
const spec = _require(
|
|
218
|
+
"@chainlesschain/personal-data-hub/adapters/ai-chat-history/cookie-capture-spec",
|
|
219
|
+
);
|
|
220
|
+
deps.classifier = deps.classifier || spec.classifyProbedCookies;
|
|
221
|
+
deps.specLookup = deps.specLookup || spec.getSpec;
|
|
222
|
+
deps.knownVendors = deps.knownVendors || spec.listVendors();
|
|
223
|
+
deps.cookieSpecVersion = deps.cookieSpecVersion || spec.COOKIE_SPEC_VERSION;
|
|
224
|
+
}
|
|
225
|
+
// Lazy require (same pattern as cookie-capture-spec above) — keeps
|
|
226
|
+
// module-load tolerant of older @chainlesschain/personal-data-hub
|
|
227
|
+
// versions that don't yet export this subpath.
|
|
228
|
+
const { createAIChatWizardController } = _require(
|
|
229
|
+
"@chainlesschain/personal-data-hub/adapters/ai-chat-history/wizard-controller",
|
|
230
|
+
);
|
|
231
|
+
const wiz = createAIChatWizardController({
|
|
232
|
+
accountsStore,
|
|
233
|
+
vendorAdapter,
|
|
234
|
+
_deps: deps,
|
|
235
|
+
});
|
|
236
|
+
if (!isTest) _wizardsByHubDir.set(hubDir, wiz);
|
|
237
|
+
return wiz;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function _resetForTests() {
|
|
241
|
+
_wizardsByHubDir.clear();
|
|
242
|
+
}
|