cardbuddy 0.26.0 → 0.26.2
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/dist/preview-editor/assets/{index-CeoWrec1.css → index-BGqgV1Dw.css} +1 -1
- package/dist/preview-editor/assets/{index-Bn5gP68-.js → index-DD2MA6i_.js} +105 -103
- package/dist/preview-editor/index.html +2 -2
- package/dist/vendored/server/core/cardSync.js +2 -5
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +27 -12
- package/dist/vendored/skills/platform-card-sync/scripts/api_batch_sync.mjs +90 -95
- package/package.json +1 -1
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16-Cg6Nrosk.png" />
|
|
9
9
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32-B_1744sp.png" />
|
|
10
10
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-BQcES9QK.png" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DD2MA6i_.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BGqgV1Dw.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="app"></div>
|
|
@@ -71,11 +71,8 @@ function logSyncSummary(summary, requestLabel) {
|
|
|
71
71
|
for (const s of summary.synced) {
|
|
72
72
|
const f = s.files || {}
|
|
73
73
|
// 空白卡(远端无 schemaValue):标注落盘策略,避免误读成「抓到了完整 schema」。
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
? ' [空白卡·保留本地]'
|
|
77
|
-
: ' [空白卡·新建空白 schema]'
|
|
78
|
-
: ''
|
|
74
|
+
// 全量覆盖语义下本地旧内容一律丢弃,不再有「保留本地」分支。
|
|
75
|
+
const tag = s.blank ? ' [空白卡·按远端重建]' : ''
|
|
79
76
|
lines.push(
|
|
80
77
|
`• ${s.id}${s.title ? `(${s.title})` : ''}${tag} schema=${fmtBytes(f.schema)} data=${fmtBytes(f.data)} cover=${fmtBytes(f.cover)}`
|
|
81
78
|
)
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
- **Input 回写只写 rt.state**:Input 的 value 绑定 $key 时,渲染层兜底 `rt.state[key] = v`(**只写 rt.state、绝不写 rt.rootScope**,故打字不触发"保存到本地"脏标记)。
|
|
46
46
|
- **读写约定**:运行态变量(含用户输入/绑定派生值)一律用 this.state 读、写走 this.setData;this.data 仅用于读 Input 绑定键的实时输入值;持久卡片态存 this.state;任何应重渲染的改动后调 this.setData(...)。**调试交互绝不调 setRootData**——否则会污染"保存到本地"基线。
|
|
47
47
|
- **事件代码必须 `var self = this` 起手(指针安全铁律)**:每个 customAction 的 `function.code` / `compiled` **第一行**写 `var self = this;`。**任何嵌套函数(addEventListener 回调、`Array.map/forEach/find` 回调、setTimeout、ynet.callJSAPI 回调等)里访问组件实例一律用 `self`,禁止用 `this`**——`this` 在回调里会被运行时重绑(指向全局 / 事件源 / undefined),直接 `this.setData` 会指向错误对象,导致静默失效或 `this is undefined` 报错。顶层(非嵌套)直接调用可用 `this`,但统一用 `self` 最稳、零歧义。**生成事件代码默认就按此写,不要等出错再改。** export-proven:holdFinancialProducts 根 `onReady` 写 `var self = this; ... this.addEventListener('globalSet', (e) => self.$set(e.data.key, e.data.value))`;recipes.md §4 / §7 同构。
|
|
48
|
-
- **List/Grid item 事件改绑定数据必须 globalSet 透传(铁律)**:ListLayout/GridLayout 的 item(slotScope)事件里禁止直接改绑定数据(this.setData/setRootData/直接 state 赋值)——必须 `this.postMessage('globalSet', patch)`(patch 与 setData 入参逐字一致),并先在根节点 onReady `addEventListener('globalSet')` 注册监听、监听里用闭包 self 代 `setData(patch)`;其他节点事件可直接 setData。lint `r54` ERROR 拦截「item 直改」与「用 globalSet
|
|
48
|
+
- **List/Grid item 事件改绑定数据必须 globalSet 透传(铁律)**:ListLayout/GridLayout 的 item(slotScope)事件里禁止直接改绑定数据(this.setData/setRootData/直接 state 赋值)——必须 `this.postMessage('globalSet', patch)`(patch 与 setData 入参逐字一致),并先在根节点 onReady `addEventListener('globalSet')` 注册监听、监听里用闭包 self 代 `setData(patch)`;其他节点事件可直接 setData。lint `r54` ERROR 拦截「item 直改」与「用 globalSet 但根无监听」;判定覆盖 item 模板(`slotScope`)子树内的**所有节点**(不止 item 根),`that` / `self` / `_this` 别名与 `this` 等价计入。固定范式见 recipes.md §4 / recipe_snippets/globalSet.json。
|
|
49
49
|
- **slotScope item 字段在事件脚本里直读(铁律 r55)**:slotScope:true 的 List/Grid item 事件脚本里,当前 item 字段**直接 `this.state.<field>` 读取**(如 `var q = this.state.query;`)——作用域展开是平台保证,**不是**需要兜底判断的不确定行为;禁止写 `if (x === undefined && this.state.$$)` 之类的「$$ 兜底」。事件脚本(customAction)里**禁止出现任何 `$$`**(含 `this.state.$$`、`$$.field`);`$$` 只在**表达式**里引用整个 item 时才允许(R50 另管 `$$.field` 冗余)。lint `r55-event-no-dollar-dollar` ERROR 拦截。
|
|
50
50
|
- **调 JSAPI 直接裸调 ynet(铁律 r56)**:事件脚本里调 JSAPI 一律 `ynet.callJSAPI(name, params, cb)`——ynet 由平台注入为全局;**禁止 `window.ynet` 前缀**,也**禁止 `typeof window !== 'undefined' && window.ynet && window.ynet.callJSAPI` 之类存在性守卫**(把平台保证误当不确定行为的冗余错写;49 张导出卡全量裸调为证)。lint `r56-jsapi-no-window-guard` ERROR 拦截。
|
|
51
51
|
- **生成代码必须严格带 `;`(语句结束符不省略)**:customAction 的 `function.code` / `compiled` 里**每条 JS 语句结尾都要写分号,禁止依赖 ASI(自动分号插入)省略**。`return` / 换行 / 对象字面量 / 数组字面量等场景省略分号会触发 ASI 陷阱(如 `return\n{...}` 被解析成 `return;` 直接返回 undefined);且导出卡(49+ 卡 `compiled` 全量带分号)风格不一致会让 `diff_card.mjs` 更难比对。**生成时默认每条语句结尾补 `;`,不要"省着写"。** export-proven:holdFinancialProducts / creditCardBill 等所有导出卡 `compiled` 均全量带分号。
|
|
@@ -101,11 +101,25 @@ Options:
|
|
|
101
101
|
/* ------------------------------------------------------------------ */
|
|
102
102
|
/* 节点遍历 */
|
|
103
103
|
/* ------------------------------------------------------------------ */
|
|
104
|
-
|
|
104
|
+
/**
|
|
105
|
+
* slot.slotScope 判定:正常是布尔 true,但存量卡里有 2 处写成字符串 'true',一并兼容
|
|
106
|
+
*/
|
|
107
|
+
function isSlotScope(slot) {
|
|
108
|
+
const v = (slot || {}).slotScope
|
|
109
|
+
return v === true || v === 'true'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {object} node
|
|
114
|
+
* @param {(node:object, parent:object|null, inSlotScope:boolean)=>void} visitor
|
|
115
|
+
* @param {object|null} parent 父节点(实参传递,不往 schema 上挂字段,避免污染 JSON.stringify)
|
|
116
|
+
* @param {boolean} inSlotScope 是否位于 ListLayout/GridLayout 的 item 模板(slot.slotScope === true)子树内
|
|
117
|
+
*/
|
|
118
|
+
function walk(node, visitor, parent = null, inSlotScope = false) {
|
|
105
119
|
if (!node || typeof node !== 'object') return
|
|
106
|
-
visitor(node, parent)
|
|
107
|
-
if (Array.isArray(node.components)) for (const c of node.components) walk(c, visitor, node)
|
|
108
|
-
for (const s of node.slots || []) for (const c of s.components || []) walk(c, visitor, node)
|
|
120
|
+
visitor(node, parent, inSlotScope)
|
|
121
|
+
if (Array.isArray(node.components)) for (const c of node.components) walk(c, visitor, node, inSlotScope)
|
|
122
|
+
for (const s of node.slots || []) for (const c of s.components || []) walk(c, visitor, node, inSlotScope || isSlotScope(s))
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
/** 获取默认插槽直接子节点(兼容 Root.components 与普通节点 slots[0].components) */
|
|
@@ -1329,13 +1343,14 @@ export function lint(schema, data) {
|
|
|
1329
1343
|
const f = fn.find((x) => x.itemKey === 'function') || {}
|
|
1330
1344
|
return String((f.itemValue || {}).code || (f.itemValue || {}).compiled || '')
|
|
1331
1345
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1346
|
+
// 别名与 collectBindings 保持一致:脚本里常用 that/self/_this 承接 this(部分由低代码编译器生成)
|
|
1347
|
+
const ALIAS = '(?:this|self|that|_this)'
|
|
1348
|
+
const MUTATE_RE = new RegExp(
|
|
1349
|
+
`\\b${ALIAS}\\s*\\.\\s*(?:setData|setRootData)\\s*\\(`
|
|
1350
|
+
+ `|\\b${ALIAS}\\s*\\.\\s*(?:state|data)(?:\\.[\\w$]+)+\\s*=[^=]`,
|
|
1351
|
+
)
|
|
1352
|
+
walk(view, (n, parent, inSlotScope) => {
|
|
1353
|
+
if (!inSlotScope) return
|
|
1339
1354
|
if (n.className === 'ListLayout' || n.className === 'GridLayout') return
|
|
1340
1355
|
const cid = n.componentId || '(root)'
|
|
1341
1356
|
for (const ea of n.eventactions || []) {
|
|
@@ -1350,7 +1365,7 @@ export function lint(schema, data) {
|
|
|
1350
1365
|
})
|
|
1351
1366
|
}
|
|
1352
1367
|
}
|
|
1353
|
-
}
|
|
1368
|
+
})
|
|
1354
1369
|
// R54-b: 用了 globalSet 透传但根节点 onReady 没注册监听 → 消息石沉大海
|
|
1355
1370
|
const allCode = []
|
|
1356
1371
|
let rootOnReadyCode = ''
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, stat, writeFile,
|
|
1
|
+
import { mkdir, stat, writeFile, readdir, rm } from 'node:fs/promises';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -208,8 +208,9 @@ export async function syncCards(opts = {}) {
|
|
|
208
208
|
const session = await ensurePlatformSession();
|
|
209
209
|
const browser = session.browser;
|
|
210
210
|
const summary = { success: true, synced: [], failed: [], duplicates: [] };
|
|
211
|
-
//
|
|
212
|
-
|
|
211
|
+
// duplicates 保留为空数组仅为兼容既有结果结构(前端 / CLI 都读这个字段)。
|
|
212
|
+
// 不再按「不同卡拿到字节相同的 schema」判串卡:同模板创建、内容清空、空白骨架
|
|
213
|
+
// 都会产生相同 schema,属合法状态;内容相似度推不出「抓错了卡」。
|
|
213
214
|
|
|
214
215
|
try {
|
|
215
216
|
const context = browser.contexts()[0];
|
|
@@ -247,12 +248,11 @@ export async function syncCards(opts = {}) {
|
|
|
247
248
|
if (cardCodeSet && !cardCodeSet.has(card.id)) continue;
|
|
248
249
|
log.info('同步中', { page: `${pageNo}/${totalPages}`, index, id: card.id, title: card.title });
|
|
249
250
|
try {
|
|
250
|
-
const files = await syncCard(page, card, { outRoot, appId, workspaceId,
|
|
251
|
+
const files = await syncCard(page, card, { outRoot, appId, workspaceId, importMeta });
|
|
251
252
|
// 空白卡不算失败,但要在结果里显式标记,避免与「抓到完整 schema」混为一谈。
|
|
252
253
|
summary.synced.push({
|
|
253
254
|
page: pageNo, index, id: card.id, title: card.title, files,
|
|
254
255
|
blank: !!files.blank,
|
|
255
|
-
keptLocal: !!files.keptLocal,
|
|
256
256
|
});
|
|
257
257
|
} catch (error) {
|
|
258
258
|
summary.success = false;
|
|
@@ -1691,9 +1691,10 @@ async function fetchCardSchema(page, card, { appId, workspaceId }) {
|
|
|
1691
1691
|
log.warn('远端响应体为空,按空白卡处理', { id: card.id, cardId: card.cardId });
|
|
1692
1692
|
return null;
|
|
1693
1693
|
}
|
|
1694
|
-
//
|
|
1694
|
+
// 抓错卡的唯一直接证据:服务端回显的主键必须等于本次请求主键。
|
|
1695
|
+
// 这是主键级校验,保留为硬闸;内容相似、pageId 缺失等都不构成「串卡」证据。
|
|
1695
1696
|
if (body.cardId && String(body.cardId) !== String(card.cardId)) {
|
|
1696
|
-
throw new Error(`${card.id}
|
|
1697
|
+
throw new Error(`${card.id} 响应主键不匹配:请求 cardId=${card.cardId},响应 cardId=${body.cardId},已中止本卡落盘`);
|
|
1697
1698
|
}
|
|
1698
1699
|
if (!body.schemaValue) {
|
|
1699
1700
|
log.warn('远端无 schemaValue,判定为空白卡', { id: card.id, cardId: card.cardId });
|
|
@@ -1827,92 +1828,117 @@ function filestoreToUrl(value) {
|
|
|
1827
1828
|
return platformConfig.filestoreUrl(value);
|
|
1828
1829
|
}
|
|
1829
1830
|
|
|
1830
|
-
|
|
1831
|
+
/**
|
|
1832
|
+
* 全量覆盖:同步 = 用远端状态重建整张卡,先清空卡目录内的既有产物再写入。
|
|
1833
|
+
*
|
|
1834
|
+
* 旧实现会「保留本地 schema/data、只改 pageId 一个字段」、本地已有封面就跳过下载,
|
|
1835
|
+
* 目录里的旧封面、旧侧车、多余文件也从不清理 —— 那不是覆盖,是打补丁。
|
|
1836
|
+
* 现在除 .history(本地编辑历史,唯一恢复点)外的所有条目一律删除:
|
|
1837
|
+
* schema.json / data.json / 所有封面 / .import-meta.json / 其它残留
|
|
1838
|
+
* 都会由本次远端数据重新生成,不继承任何本地状态。
|
|
1839
|
+
*
|
|
1840
|
+
* 调用时机必须在 fetchCardSchema 成功之后:远端数据没到手就清目录,
|
|
1841
|
+
* 一次网络失败会把本地卡毁成空目录。
|
|
1842
|
+
*
|
|
1843
|
+
* @returns {Promise<number>} 被清除的条目数
|
|
1844
|
+
*/
|
|
1845
|
+
export async function resetCardDir(cardDir) {
|
|
1846
|
+
const entries = await readdir(cardDir, { withFileTypes: true }).catch(() => []);
|
|
1847
|
+
let removed = 0;
|
|
1848
|
+
for (const entry of entries) {
|
|
1849
|
+
if (entry.name === '.history') continue;
|
|
1850
|
+
await rm(path.join(cardDir, entry.name), { recursive: true, force: true });
|
|
1851
|
+
removed++;
|
|
1852
|
+
}
|
|
1853
|
+
return removed;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* 同步落盘:把 schema.pageId 规范为本次列表给出的 cardId。
|
|
1858
|
+
*
|
|
1859
|
+
* 这不是「只改一个字段」的补丁:卡片目录已由 resetCardDir 全量重建,
|
|
1860
|
+
* 此处处理的是远端数据自身的问题 —— IDC10012 返回的 schemaValue 里可能带着
|
|
1861
|
+
* 模板或旧实例的 pageId(实测 creditCardPayment:schema.pageId=10000818,
|
|
1862
|
+
* 而 IDC10001 列表 cardId=10000874)。保存链路 saveCardToRemote 用 schema.pageId
|
|
1863
|
+
* 作为 IDC10010 的 body.cardId,推送闸门 server/core/cardSave.js 又要求它等于
|
|
1864
|
+
* 侧车 cardId;留着旧值会让这张卡永远推不上去(409 pageId_mismatch)。
|
|
1865
|
+
* 因此以「这张卡本次的身份」即列表 cardId 为准,与 buildBlankSchema 一致。
|
|
1866
|
+
*
|
|
1867
|
+
* 抓错卡的判定不使用本函数:唯一主键级证据是 fetchCardSchema 里
|
|
1868
|
+
*「IDC10012 回显 cardId ≠ 请求 cardId」,内容相似与 pageId 差异都不构成串卡证据。
|
|
1869
|
+
*
|
|
1870
|
+
* @returns {{rewritten:boolean, from:string, to:string}}
|
|
1871
|
+
*/
|
|
1872
|
+
export function alignSchemaPageId(schema, card) {
|
|
1873
|
+
const from = schema.pageId == null ? '' : String(schema.pageId).trim();
|
|
1874
|
+
const cardId = card.cardId == null ? '' : String(card.cardId).trim();
|
|
1875
|
+
if (!cardId || from === cardId) return { rewritten: false, from, to: from };
|
|
1876
|
+
schema.pageId = cardId;
|
|
1877
|
+
return { rewritten: true, from, to: cardId };
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
async function syncCard(page, card, { outRoot, appId, workspaceId, importMeta }) {
|
|
1831
1881
|
const cardDir = path.join(outRoot, card.id);
|
|
1832
1882
|
await mkdir(cardDir, { recursive: true });
|
|
1833
1883
|
const schema = await fetchCardSchema(page, card, { appId, workspaceId });
|
|
1834
|
-
//
|
|
1884
|
+
// 全量覆盖:远端数据到手后才重建目录(拉取失败时本地卡片保持原样)。
|
|
1885
|
+
await resetCardDir(cardDir);
|
|
1886
|
+
// 空白卡(远端从未保存过 schema):合法状态,同样按远端状态全量重建。
|
|
1835
1887
|
if (!schema) {
|
|
1836
1888
|
return syncBlankCard(page, card, { cardDir, appId, workspaceId, importMeta });
|
|
1837
1889
|
}
|
|
1838
|
-
//
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
throw new Error(`${card.id} pageId 错位:列表 cardId=${card.cardId} ≠ schema.pageId=${schema.pageId},疑似串卡,已拒绝落盘`);
|
|
1843
|
-
}
|
|
1844
|
-
const coverPath = await saveCover(page, card, cardDir);
|
|
1845
|
-
// 注意:schema 内部的 pageId 一律以「远端拉取」的为准,原样落盘,不做覆盖。
|
|
1846
|
-
// 远端返回的 pageId 是远端编辑器维护的权威页面/模板 ID,落盘后随 schema 一并保留,
|
|
1847
|
-
// 供保存(IDC10010)/预览(IDC10015/10020/10021)链路按此主键打到线上。
|
|
1848
|
-
// 重复 schema 检测:若不同 code 抓到了字节完全相同的 schema,说明仍串卡,
|
|
1849
|
-
// 记录到 duplicates 并把整体结果标为失败,便于重抓时立即发现而非事后排查。
|
|
1850
|
-
const schemaHash = createHash('md5').update(JSON.stringify(schema)).digest('hex');
|
|
1851
|
-
const dupOf = schemaHashes.get(schemaHash);
|
|
1852
|
-
if (dupOf && dupOf !== card.id) {
|
|
1853
|
-
summary.duplicates.push({ id: card.id, dupOf, hash: schemaHash.slice(0, 8) });
|
|
1854
|
-
summary.success = false;
|
|
1855
|
-
log.warn('检测到重复 schema(疑似串卡)', { id: card.id, dupOf, hash: schemaHash.slice(0, 8) });
|
|
1890
|
+
// 目录已重建,这里处理的是远端数据自带的旧实例 pageId(详见函数注释)。
|
|
1891
|
+
const { rewritten, from, to } = alignSchemaPageId(schema, card);
|
|
1892
|
+
if (rewritten) {
|
|
1893
|
+
log.info('schema.pageId 已按列表 cardId 规范', { id: card.id, from: from || '(空)', to });
|
|
1856
1894
|
}
|
|
1857
|
-
|
|
1895
|
+
// 先写核心文件,再取封面:封面下载失败不至于把卡目录留成空壳。
|
|
1858
1896
|
const schemaPath = path.join(cardDir, 'schema.json');
|
|
1859
1897
|
await writeFile(schemaPath, JSON.stringify(schema, null, 2), 'utf8');
|
|
1860
1898
|
const dataPath = path.join(cardDir, 'data.json');
|
|
1861
1899
|
await writeFile(dataPath, JSON.stringify(buildDataFromSchema(schema), null, 2), 'utf8');
|
|
1862
|
-
|
|
1863
|
-
|
|
1900
|
+
let coverPath = null;
|
|
1901
|
+
if (card.coverUrl) {
|
|
1902
|
+
try {
|
|
1903
|
+
coverPath = await saveCover(page, card, cardDir);
|
|
1904
|
+
} catch (e) {
|
|
1905
|
+
log.warn('预览图下载失败(不影响卡片全量覆盖落盘)', { id: card.id, error: String(e && e.message) });
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const [schemaInfo, data, cover] = await Promise.all([
|
|
1909
|
+
stat(schemaPath), stat(dataPath), coverPath ? stat(coverPath).catch(() => null) : Promise.resolve(null),
|
|
1864
1910
|
]);
|
|
1865
1911
|
await writeImportSidecar(cardDir, {
|
|
1866
1912
|
appId,
|
|
1867
1913
|
workspaceId,
|
|
1868
1914
|
card,
|
|
1869
1915
|
importMeta,
|
|
1870
|
-
files: { schema: schemaInfo.size, data: data.size, cover: cover.size },
|
|
1916
|
+
files: { schema: schemaInfo.size, data: data.size, cover: cover ? cover.size : 0 },
|
|
1871
1917
|
});
|
|
1872
|
-
return { cover: cover.size, schema: schemaInfo.size, data: data.size };
|
|
1918
|
+
return { cover: cover ? cover.size : 0, schema: schemaInfo.size, data: data.size };
|
|
1873
1919
|
}
|
|
1874
1920
|
|
|
1875
1921
|
/**
|
|
1876
1922
|
* 空白卡落盘:远端从未保存过 schema(IDC10012 无 schemaValue),但卡片实例真实存在。
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1879
|
-
*
|
|
1880
|
-
*
|
|
1881
|
-
* 全链路按这个数字主键定位线上卡片,错位会打到别的卡上)。
|
|
1882
|
-
* 本地无任何卡片时,落一份最小合法空白 schema,保证目录可被编辑器/预览正常加载。
|
|
1923
|
+
* 全量覆盖语义:目录已由 resetCardDir 清空,这里不读本地、不保留任何旧内容,
|
|
1924
|
+
* 直接按远端状态落一份最小合法空白 schema + 空 data,pageId 取列表 cardId
|
|
1925
|
+
* (保存 IDC10010 / 预览 10015·10020·10021 全链路按这个数字主键定位线上卡片)。
|
|
1926
|
+
* 远端「空白」就是本地「空白」,不再拿本地旧卡去填。
|
|
1883
1927
|
*/
|
|
1884
1928
|
async function syncBlankCard(page, card, { cardDir, appId, workspaceId, importMeta }) {
|
|
1885
1929
|
const schemaPath = path.join(cardDir, 'schema.json');
|
|
1886
1930
|
const dataPath = path.join(cardDir, 'data.json');
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
// 本地有实体卡片:保留本地内容;否则建最小空白骨架。
|
|
1892
|
-
const schema = localSchema || buildBlankSchema(card);
|
|
1893
|
-
const data = localData || (keptLocal ? buildDataFromSchema(schema) : {});
|
|
1894
|
-
|
|
1895
|
-
// pageId 对齐远端 cardId:只改这一个字段,其余本地内容原样保留。
|
|
1896
|
-
const pageIdRewritten = String(schema.pageId ?? '') !== String(card.cardId);
|
|
1897
|
-
if (pageIdRewritten) schema.pageId = String(card.cardId);
|
|
1898
|
-
|
|
1899
|
-
if (!keptLocal || pageIdRewritten) {
|
|
1900
|
-
await writeFile(schemaPath, JSON.stringify(schema, null, 2), 'utf8');
|
|
1901
|
-
}
|
|
1902
|
-
if (!localData) {
|
|
1903
|
-
await writeFile(dataPath, JSON.stringify(data, null, 2), 'utf8');
|
|
1904
|
-
}
|
|
1931
|
+
const schema = buildBlankSchema(card);
|
|
1932
|
+
await writeFile(schemaPath, JSON.stringify(schema, null, 2), 'utf8');
|
|
1933
|
+
await writeFile(dataPath, JSON.stringify({}, null, 2), 'utf8');
|
|
1905
1934
|
|
|
1906
|
-
//
|
|
1907
|
-
let coverPath =
|
|
1908
|
-
|
|
1909
|
-
let coverError = '';
|
|
1910
|
-
if (!coverPath && card.coverUrl) {
|
|
1935
|
+
// 预览图一律按远端重新下载;无图源或下载失败不阻断卡片落盘。
|
|
1936
|
+
let coverPath = null;
|
|
1937
|
+
if (card.coverUrl) {
|
|
1911
1938
|
try {
|
|
1912
1939
|
coverPath = await saveCover(page, card, cardDir);
|
|
1913
1940
|
} catch (e) {
|
|
1914
|
-
|
|
1915
|
-
log.warn('空白卡预览图同步失败(不影响卡片落盘)', { id: card.id, error: coverError });
|
|
1941
|
+
log.warn('空白卡预览图同步失败(不影响卡片落盘)', { id: card.id, error: String(e && e.message) });
|
|
1916
1942
|
}
|
|
1917
1943
|
}
|
|
1918
1944
|
|
|
@@ -1931,10 +1957,6 @@ async function syncBlankCard(page, card, { cardDir, appId, workspaceId, importMe
|
|
|
1931
1957
|
schema: schemaInfo.size,
|
|
1932
1958
|
data: dataInfo.size,
|
|
1933
1959
|
blank: true,
|
|
1934
|
-
keptLocal,
|
|
1935
|
-
pageIdRewritten,
|
|
1936
|
-
coverSkipped,
|
|
1937
|
-
coverError,
|
|
1938
1960
|
};
|
|
1939
1961
|
}
|
|
1940
1962
|
|
|
@@ -2002,32 +2024,6 @@ function buildBlankSchema(card) {
|
|
|
2002
2024
|
};
|
|
2003
2025
|
}
|
|
2004
2026
|
|
|
2005
|
-
/** 读本地 JSON;文件不存在或解析失败返回 null(不抛,交给调用方决定降级策略)。 */
|
|
2006
|
-
async function readJsonIfValid(filePath) {
|
|
2007
|
-
try {
|
|
2008
|
-
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
2009
|
-
} catch {
|
|
2010
|
-
return null;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']);
|
|
2015
|
-
|
|
2016
|
-
/** 找本地已有的同名预览图(<cardId>.<ext>);没有返回 null。 */
|
|
2017
|
-
async function findExistingCover(cardDir, id) {
|
|
2018
|
-
let entries = [];
|
|
2019
|
-
try {
|
|
2020
|
-
entries = await readdir(cardDir);
|
|
2021
|
-
} catch {
|
|
2022
|
-
return null;
|
|
2023
|
-
}
|
|
2024
|
-
for (const name of entries) {
|
|
2025
|
-
const ext = path.extname(name).toLowerCase();
|
|
2026
|
-
if (IMAGE_EXTS.has(ext) && path.basename(name, ext) === id) return path.join(cardDir, name);
|
|
2027
|
-
}
|
|
2028
|
-
return null;
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
2027
|
/**
|
|
2032
2028
|
* 导入来源可追溯(Phase E):把这张卡从哪来 / 何时导入 / 落盘在哪写进侧车 .import-meta.json。
|
|
2033
2029
|
* 侧车独立于 schema.json/data.json,绝不污染 DSL 产品数据;找不到 server 模块时静默跳过。
|
|
@@ -2137,9 +2133,8 @@ if (isMain) {
|
|
|
2137
2133
|
`已同步:${summary.synced.length} 张`,
|
|
2138
2134
|
`失败:${summary.failed.length} 张`,
|
|
2139
2135
|
`空白卡片:${blanks.length} 张`,
|
|
2140
|
-
`重复 schema:${summary.duplicates.length} 项`,
|
|
2141
2136
|
...(blanks.length
|
|
2142
|
-
? [`空白明细:${blanks.map((b) => `${b.id}
|
|
2137
|
+
? [`空白明细:${blanks.map((b) => `${b.id}(按远端重建空白)`).join('、')}`]
|
|
2143
2138
|
: []),
|
|
2144
2139
|
...(summary.failed.length
|
|
2145
2140
|
? [`失败明细:${summary.failed.map((f) => f.id || '(unknown)').join('、')}`]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cardbuddy",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
4
4
|
"description": "Local dev service for the CardBuddy card editor — decouples card CRUD (sync/save/validate/render-status/screenshot) from the static public editor, so the editor can be deployed to any static host while each developer runs a local service against their own card source.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|