cardbuddy 0.22.0 → 0.23.0
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 +11 -11
- package/dist/cli.js +2 -2
- package/dist/config.js +3 -3
- package/dist/init.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/preview-editor/assets/{index-CU8zVA2P.css → index-7RzNd7tf.css} +1 -1
- package/dist/preview-editor/assets/{index-CPpLZZyi.js → index-C7N5n_qW.js} +80 -80
- package/dist/preview-editor/index.html +2 -2
- package/dist/skills-upgrade.js +1 -1
- package/dist/standalone.js +2 -2
- package/dist/vendored/card-editor/runtime/adapter.js +10 -11
- package/dist/vendored/mcp/client.mjs +50 -13
- package/dist/vendored/mcp/tools.mjs +13 -7
- package/dist/vendored/server/core/ai-chat.js +540 -183
- package/dist/vendored/server/core/assetProxy.js +1 -1
- package/dist/vendored/server/core/browser.js +1 -1
- package/dist/vendored/server/core/cardDigest.js +324 -0
- package/dist/vendored/server/core/cardExport.js +5 -5
- package/dist/vendored/server/core/cardFiles.js +1 -1
- package/dist/vendored/server/core/cardHistoryRemote.js +1 -1
- package/dist/vendored/server/core/cardImportMeta.js +1 -1
- package/dist/vendored/server/core/cardListCache.js +92 -10
- package/dist/vendored/server/core/cardPublish.js +11 -6
- package/dist/vendored/server/core/cardPublishVersions.js +8 -8
- package/dist/vendored/server/core/cardSave.js +14 -10
- package/dist/vendored/server/core/cardSidecar.js +3 -3
- package/dist/vendored/server/core/cardSync.js +35 -23
- package/dist/vendored/server/core/cardVersion.js +1 -1
- package/dist/vendored/server/core/index.js +6 -6
- package/dist/vendored/server/core/logger.js +1 -1
- package/dist/vendored/server/core/remoteCard.js +5 -5
- package/dist/vendored/server/core/skillPaths.js +1 -1
- package/dist/vendored/server/core/sliceUpload.js +4 -4
- package/dist/vendored/skills/README.md +13 -13
- package/dist/vendored/skills/card-doctor/SKILL.md +2 -2
- package/dist/vendored/skills/dsl-card-ai/SKILL.md +4 -4
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +2 -2
- package/dist/vendored/skills/dsl-card-generator/references/amount_formatting.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/build_protocol.md +54 -0
- package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +2 -2
- package/dist/vendored/skills/dsl-card-generator/references/converge_protocol.md +36 -0
- package/dist/vendored/skills/dsl-card-generator/references/event_patterns.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/image_to_card.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +4 -4
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/inventory_schemas.py +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +2 -2
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-CeVX_y-M.js +137 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-DiEiY59H.css +1 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/standalone.html +2 -2
- package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/SKILL.md +26 -26
- package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/api_batch_sync.mjs +184 -162
- package/dist/vendored/skills/{finmall-card-sync → platform-card-sync}/scripts/slice_upload.mjs +48 -29
- package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/SKILL.md +13 -13
- package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/package-lock.json +2 -2
- package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/package.json +2 -2
- package/dist/vendored/skills/{finmall-session-navigator/scripts/finmall_login.mjs → platform-session-navigator/scripts/platform_login.mjs} +41 -31
- package/dist/vendored/skills/{finmall-session-navigator → platform-session-navigator}/scripts/session-manager.mjs +17 -17
- package/dist/vendored/skills/shared/README.md +21 -21
- package/dist/vendored/skills/shared/auth-model.md +8 -10
- package/dist/vendored/skills/shared/{finmall-config.mjs → platform-config.mjs} +10 -10
- package/dist/vendored/tools/{finmall-env.mjs → platform-env.mjs} +13 -13
- package/package.json +1 -1
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-BCLshIzC.css +0 -1
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-pP5kTKiI.js +0 -88
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 大卡 schema 的**读方向折叠**(structure digest)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:`schema.json` 是编辑器持久化格式,单卡紧凑 JSON 实测最大 239KB / 167 节点。
|
|
5
|
+
* 读大卡时旧行为是「JSON 字符串取前 12000 字符」,而 JSON 的键序是
|
|
6
|
+
* `version / pageId / name / desc / platform / dictionary / view` —— `dictionary` 排在 view 前面,
|
|
7
|
+
* 30 条 inputParams 就吃掉 10.5KB。实测 overseaTransfer(239KB / 167 节点)的 12k 切片里
|
|
8
|
+
* **componentId 出现 0 次**:模型花 12k 字符只拿到参数声明 + 根节点的半张样式表,
|
|
9
|
+
* 一个组件都没看到,于是转头用 card_grep 反复试路径,白走 2~5 步(每步 20~40k 输入)。
|
|
10
|
+
*
|
|
11
|
+
* 同一个 12k 预算下,全节点骨架只要 4.7k:167 行 `componentId(ClassName) 「主文本」`。
|
|
12
|
+
* 所以折叠的目标不是「少给」,而是**把预算从「前 N 个字节」换成「N 个节点的地图」**:
|
|
13
|
+
* - 骨架:全部节点的身份 + 主文本 + 层级(模型据此能点到任何一处)
|
|
14
|
+
* - 参数:inputParams 压缩成一行 key:type=default
|
|
15
|
+
* - 不给样式与属性全量 —— 要就 `card_read {id, node:"<componentId>"}` 定向取子树
|
|
16
|
+
* - 同时给出该子树的真实 JSON 路径,`card_patch` 可直接用,省掉 card_grep 摸索
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 骨架里主文本的截断长度:够认人即可,不搬正文 */
|
|
20
|
+
const TEXT_MAX = 24
|
|
21
|
+
/** 层级缩进最多叠到第几层(更深的保持同缩进,防止 30 层缩进把行宽吃光) */
|
|
22
|
+
const DEPTH_INDENT_MAX = 8
|
|
23
|
+
/** 骨架占预算的比例,其余留给参数表 / 元信息 / 下一步指引 */
|
|
24
|
+
const TREE_BUDGET_RATIO = 0.66
|
|
25
|
+
/** 参数表预算上限 */
|
|
26
|
+
const PARAM_BUDGET = 2400
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 取节点主值(等价于渲染出来的那段字)。
|
|
30
|
+
* `itemValueFrom`: 0=常量 / 1=变量 / 2=图片(与 ir-format.md 的 p / v / pi 对应)。
|
|
31
|
+
* @returns {string} 空串表示该节点没有主值
|
|
32
|
+
*/
|
|
33
|
+
function mainValue(node) {
|
|
34
|
+
const props = Array.isArray(node.properties) ? node.properties : []
|
|
35
|
+
for (const p of props) {
|
|
36
|
+
if (!p || p.itemKey !== 'value') continue
|
|
37
|
+
const from = String(p.itemValueFrom ?? '0')
|
|
38
|
+
if (from === '2') return '[图片]'
|
|
39
|
+
const raw = p.itemValue
|
|
40
|
+
if (from === '1') {
|
|
41
|
+
const name = typeof raw === 'string' ? raw : raw && typeof raw === 'object' ? (raw.value ?? raw.key) : ''
|
|
42
|
+
const s = name ? String(name) : ''
|
|
43
|
+
if (!s) return '[变量]'
|
|
44
|
+
// itemValue 本身就形如 `$remark`,不要再补一个 $(曾出现 `$$remark`)
|
|
45
|
+
return s.startsWith('$') ? s : `$${s}`
|
|
46
|
+
}
|
|
47
|
+
if (raw == null || typeof raw === 'object') return ''
|
|
48
|
+
return String(raw)
|
|
49
|
+
}
|
|
50
|
+
return ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 事件摘要:把 `eventactions[]` 压成 `eventKey:actionType(N字)`,多个逗号分隔。
|
|
55
|
+
*
|
|
56
|
+
* 为什么必须进骨架:全库 366 个节点有事件、代码合计 324KB,单卡最多 27.5KB。
|
|
57
|
+
* 事件是卡片里真正的业务逻辑,却藏在 8 层路径下
|
|
58
|
+
* (`eventactions[0].action.properties[0].itemValue.code`)。骨架不给这个标记,
|
|
59
|
+
* 模型拿到 167 行结构图也不知道哪个节点有事件,只能 `jq` 手数路径去扫 ——
|
|
60
|
+
* 历史日志里同型命令跨 3 张卡出现 12 次以上,每次一整步(20~40k 输入)。
|
|
61
|
+
*
|
|
62
|
+
* 只标「有事件 + 什么事件 + 代码多长」,不搬代码本身:读代码走
|
|
63
|
+
* `card_read {id, node:"<componentId>"}`,那一棵子树才几千字符。
|
|
64
|
+
* @returns {string} 空串表示该节点无事件
|
|
65
|
+
*/
|
|
66
|
+
function eventSummary(node) {
|
|
67
|
+
const ea = node && node.eventactions
|
|
68
|
+
if (!Array.isArray(ea) || !ea.length) return ''
|
|
69
|
+
const parts = []
|
|
70
|
+
for (const a of ea) {
|
|
71
|
+
if (!a || typeof a !== 'object') continue
|
|
72
|
+
const key = (a.event && a.event.eventKey) || '?'
|
|
73
|
+
const type = (a.action && a.action.actionType) || '?'
|
|
74
|
+
const props = Array.isArray(a.action && a.action.properties) ? a.action.properties : []
|
|
75
|
+
const fn = props.find((p) => p && p.itemKey === 'function')
|
|
76
|
+
const iv = fn && fn.itemValue
|
|
77
|
+
// 运行时取 `code || compiled`(见 preview 的 actionCode),长度按同口径算
|
|
78
|
+
const code = iv && typeof iv === 'object' ? String(iv.code || iv.compiled || '') : ''
|
|
79
|
+
parts.push(`${key}:${type}${code ? `(${code.length}字)` : ''}`)
|
|
80
|
+
}
|
|
81
|
+
return parts.join(',')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 全卡事件代码总字符数(运行时口径 `code || compiled`)。
|
|
86
|
+
* 只用于 meta 报规模,帮模型判断「要不要一次读完」。
|
|
87
|
+
*/
|
|
88
|
+
function countEventChars(view) {
|
|
89
|
+
let total = 0
|
|
90
|
+
const walk = (n) => {
|
|
91
|
+
if (!n || typeof n !== 'object') return
|
|
92
|
+
const ea = n.eventactions
|
|
93
|
+
if (Array.isArray(ea)) {
|
|
94
|
+
for (const a of ea) {
|
|
95
|
+
const props = Array.isArray(a && a.action && a.action.properties) ? a.action.properties : []
|
|
96
|
+
const fn = props.find((p) => p && p.itemKey === 'function')
|
|
97
|
+
const iv = fn && fn.itemValue
|
|
98
|
+
if (iv && typeof iv === 'object') total += String(iv.code || iv.compiled || '').length
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const c of n.components || []) walk(c)
|
|
102
|
+
for (const sl of n.slots || []) for (const c of (sl && sl.components) || []) walk(c)
|
|
103
|
+
}
|
|
104
|
+
walk(view)
|
|
105
|
+
return total
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 子节点列表,带**可直接用于 card_patch 的真实 JSON 点路径**。
|
|
110
|
+
* 节点树是 `view.components[]` 与 `view.slots[].components[]` 两种容器,不是 `view.children`。
|
|
111
|
+
* @returns {Array<{node: object, path: string}>}
|
|
112
|
+
*/
|
|
113
|
+
function childEntries(node, nodePath) {
|
|
114
|
+
const out = []
|
|
115
|
+
if (Array.isArray(node.components)) {
|
|
116
|
+
node.components.forEach((c, i) => {
|
|
117
|
+
if (c && typeof c === 'object') out.push({ node: c, path: `${nodePath}.components.${i}` })
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(node.slots)) {
|
|
121
|
+
node.slots.forEach((sl, j) => {
|
|
122
|
+
const inner = Array.isArray(sl && sl.components) ? sl.components : []
|
|
123
|
+
inner.forEach((c, k) => {
|
|
124
|
+
if (c && typeof c === 'object') out.push({ node: c, path: `${nodePath}.slots.${j}.components.${k}` })
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 深度优先遍历,产出行列表(带真实路径) */
|
|
132
|
+
function flatten(view) {
|
|
133
|
+
const rows = []
|
|
134
|
+
const walk = (node, nodePath, depth) => {
|
|
135
|
+
const v = mainValue(node)
|
|
136
|
+
rows.push({
|
|
137
|
+
depth,
|
|
138
|
+
cid: typeof node.componentId === 'string' ? node.componentId : '',
|
|
139
|
+
cls: typeof node.className === 'string' ? node.className : '',
|
|
140
|
+
text: v ? v.slice(0, TEXT_MAX) : '',
|
|
141
|
+
events: eventSummary(node),
|
|
142
|
+
path: nodePath,
|
|
143
|
+
})
|
|
144
|
+
for (const e of childEntries(node, nodePath)) walk(e.node, e.path, depth + 1)
|
|
145
|
+
}
|
|
146
|
+
walk(view, 'view', 0)
|
|
147
|
+
return rows
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 骨架单行渲染(缩进=层级;componentId(className) 「主文本」 ⚡事件) */
|
|
151
|
+
function renderRow(r) {
|
|
152
|
+
const indent = ' '.repeat(Math.min(r.depth, DEPTH_INDENT_MAX))
|
|
153
|
+
// 根节点通常没有 componentId,别渲染成 `(无id)(Root)` 这种噪音
|
|
154
|
+
const head = r.cid ? `${r.cid}(${r.cls})` : r.cls || '(无类名)'
|
|
155
|
+
const ev = r.events ? ` ⚡${r.events}` : ''
|
|
156
|
+
return `${indent}${head}${r.text ? ` 「${r.text}」` : ''}${ev}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* inputParams 压成一行 `key:type=default`。
|
|
161
|
+
* 富字段(rule / children / desc)不进 —— 需要结构化细节时读 node 或 fs_read data.json。
|
|
162
|
+
* 只给**标量**默认值:对象/数组默认值是 mock 数据(实测 `myCardList` 一条就 400 字符),
|
|
163
|
+
* 它要的是「存在」而不是「内容」,全搬进来等于把 dictionary 换个地方重新塞满预算。
|
|
164
|
+
*/
|
|
165
|
+
function renderParams(inputParams, budget = PARAM_BUDGET) {
|
|
166
|
+
const list = Array.isArray(inputParams) ? inputParams : []
|
|
167
|
+
const parts = []
|
|
168
|
+
let used = 0
|
|
169
|
+
let listed = 0
|
|
170
|
+
for (const p of list) {
|
|
171
|
+
if (!p || typeof p !== 'object') continue
|
|
172
|
+
const key = String(p.key ?? '')
|
|
173
|
+
if (!key) continue
|
|
174
|
+
const type = String(p.type ?? 'any')
|
|
175
|
+
const def = p.defaultValue
|
|
176
|
+
const isScalar = typeof def === 'string' || typeof def === 'number' || typeof def === 'boolean'
|
|
177
|
+
let suffix = ''
|
|
178
|
+
if (isScalar && def !== '') {
|
|
179
|
+
const shown = typeof def === 'string' && def.length > 24 ? `${def.slice(0, 24)}…` : def
|
|
180
|
+
suffix = `=${JSON.stringify(shown)}`
|
|
181
|
+
}
|
|
182
|
+
const seg = `${key}:${type}${suffix}`
|
|
183
|
+
if (used + seg.length + 2 > budget) break
|
|
184
|
+
parts.push(seg)
|
|
185
|
+
used += seg.length + 2
|
|
186
|
+
listed++
|
|
187
|
+
}
|
|
188
|
+
const more = list.length - listed
|
|
189
|
+
return {
|
|
190
|
+
text: parts.join(', ') + (more > 0 ? `, …(另有 ${more} 个未列出,用 fs_read data.json 看全量)` : ''),
|
|
191
|
+
listed,
|
|
192
|
+
total: list.length,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 生成大卡结构摘要。
|
|
198
|
+
* @param {object} schema 完整 schema 对象
|
|
199
|
+
* @param {object} data 派生 data(用于参数表兜底;通常与 inputParams.defaultValue 一致)
|
|
200
|
+
* @param {{id?: string, budget?: number}} opts budget ≈ 该工具结果的字符档位
|
|
201
|
+
* @returns {{meta: object, tree: string, treeTruncated: boolean, params: string, next: string[]}|null}
|
|
202
|
+
*/
|
|
203
|
+
export function digestCard(schema, data, { id = '', budget = 12000 } = {}) {
|
|
204
|
+
const view = schema && typeof schema.view === 'object' ? schema.view : null
|
|
205
|
+
if (!view) return null
|
|
206
|
+
const rows = flatten(view)
|
|
207
|
+
const compactBytes = JSON.stringify(schema).length
|
|
208
|
+
const treeBudget = Math.max(1200, Math.floor(budget * TREE_BUDGET_RATIO))
|
|
209
|
+
|
|
210
|
+
const lines = []
|
|
211
|
+
let used = 0
|
|
212
|
+
let listed = 0
|
|
213
|
+
for (const r of rows) {
|
|
214
|
+
const line = renderRow(r)
|
|
215
|
+
// 留 1 字节给换行;超预算即停(宁可少列,也不让骨架本身被 clipToolResult 二次腰斩)
|
|
216
|
+
if (used + line.length + 1 > treeBudget) break
|
|
217
|
+
lines.push(line)
|
|
218
|
+
used += line.length + 1
|
|
219
|
+
listed++
|
|
220
|
+
}
|
|
221
|
+
const truncated = listed < rows.length
|
|
222
|
+
|
|
223
|
+
// 截断时补一段「被截掉的区块分布」:不列全文,但让模型知道还有哪些区域、各多少节点,
|
|
224
|
+
// 从而能直接 node 定向读,而不是重新猜。
|
|
225
|
+
let restNote = ''
|
|
226
|
+
if (truncated) {
|
|
227
|
+
const rest = rows.slice(listed)
|
|
228
|
+
const byCls = new Map()
|
|
229
|
+
for (const r of rest) {
|
|
230
|
+
const k = `${r.cls}`
|
|
231
|
+
byCls.set(k, (byCls.get(k) || 0) + 1)
|
|
232
|
+
}
|
|
233
|
+
const dist = [...byCls].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([k, n]) => `${k}×${n}`).join(' ')
|
|
234
|
+
// 事件节点尤其要点名:截断区里的事件是「看不见但一定要改」的那部分
|
|
235
|
+
const restEv = rest.filter((r) => r.events)
|
|
236
|
+
const evNote = restEv.length
|
|
237
|
+
? `\n其中 ${restEv.length} 个节点带事件:${restEv.slice(0, 10).map((r) => `${r.cid || '(根)'}[${r.events}]`).join(' ')}${restEv.length > 10 ? ' …' : ''}`
|
|
238
|
+
: ''
|
|
239
|
+
restNote =
|
|
240
|
+
`\n… 还有 ${rest.length} 个节点未列出(已达骨架预算)。它们的组件分布:${dist}${evNote}` +
|
|
241
|
+
`\n用 card_read {id:"${id}", node:"<componentId>"} 定向读某棵子树;` +
|
|
242
|
+
`未列出的组件 id 可用 card_grep 按文案/类名查,或 fs_read schema.json 按 offset 续读。`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const params = renderParams(schema?.dictionary?.inputParams)
|
|
246
|
+
const evRows = rows.filter((r) => r.events)
|
|
247
|
+
const evChars = countEventChars(view)
|
|
248
|
+
const next = [
|
|
249
|
+
`**带 ⚡ 标记的节点有事件**(click / onReady / onMeasured…),圆括号里是代码字符数;读它 → card_read {id:"${id}", node:"<componentId>"},返回该子树含完整 JS(比整卡小一到两个数量级)`,
|
|
250
|
+
`改值/改样式/改事件 → card_patch {id:"${id}", patches:[{path:"@<componentId>/eventactions.0.action.properties.0.itemValue.code", value:"…"}]};@ 前缀自动解析成真实 JSON 路径,不用先 card_grep;写 code 时 compiled 会自动同步,不必手写第二份`,
|
|
251
|
+
`看某节点的样式/属性 → card_read {id:"${id}", node:"<componentId>"}`,
|
|
252
|
+
`要看原文 → fs_read {"path":"src/cards/${id}/schema.json","offset":<字节偏移>}`,
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
meta: {
|
|
257
|
+
id,
|
|
258
|
+
pageId: schema?.pageId ?? '',
|
|
259
|
+
platform: schema?.platform ?? '',
|
|
260
|
+
root: view.className || '',
|
|
261
|
+
nodes: rows.length,
|
|
262
|
+
nodesListed: listed,
|
|
263
|
+
compactBytes,
|
|
264
|
+
/** 有事件的节点数 / 事件代码总字符(不含在白给的 tree 里,要按 node 定向读) */
|
|
265
|
+
eventNodes: evRows.length,
|
|
266
|
+
eventChars: evChars,
|
|
267
|
+
},
|
|
268
|
+
tree: lines.join('\n') + restNote,
|
|
269
|
+
treeTruncated: truncated,
|
|
270
|
+
params: params.text,
|
|
271
|
+
next,
|
|
272
|
+
hasData: !!(data && typeof data === 'object' && Object.keys(data).length),
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 按 componentId 找节点,返回它的真实 JSON 路径与祖先链。
|
|
278
|
+
* @returns {{path: string, node: object, depth: number, ancestors: Array<{cid: string, cls: string, path: string}>}|null}
|
|
279
|
+
*/
|
|
280
|
+
export function findNodeByComponentId(schema, componentId) {
|
|
281
|
+
const view = schema && typeof schema.view === 'object' ? schema.view : null
|
|
282
|
+
const want = String(componentId || '')
|
|
283
|
+
if (!view || !want) return null
|
|
284
|
+
let hit = null
|
|
285
|
+
const walk = (node, nodePath, depth, ancestors) => {
|
|
286
|
+
if (hit) return
|
|
287
|
+
if (node.componentId === want) {
|
|
288
|
+
hit = { path: nodePath, node, depth, ancestors: ancestors.slice() }
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
const chain = depth === 0 ? [] : ancestors
|
|
292
|
+
for (const e of childEntries(node, nodePath)) {
|
|
293
|
+
walk(e.node, e.path, depth + 1, [...chain, { cid: node.componentId || '', cls: node.className || '', path: nodePath }])
|
|
294
|
+
if (hit) return
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
walk(view, 'view', 0, [])
|
|
298
|
+
return hit
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 把 `@<componentId>` 开头的路径解析成真实 JSON 点路径。
|
|
303
|
+
* 支持三种写法:
|
|
304
|
+
* `@text_dw` → 节点自身路径
|
|
305
|
+
* `@text_dw/s.properties` → 用 `/` 分隔相对段(推荐:componentId 本身可能含 `.`?实际不会,
|
|
306
|
+
* 但相对段里全是数字下标与字段名,用 `/` 更不易歧义)
|
|
307
|
+
* `@text_dw.properties.0...` → 用 `.` 续写相对段
|
|
308
|
+
* 找不到组件时抛错(响亮失败,不静默新建分支)。
|
|
309
|
+
* @returns {string} 真实点路径
|
|
310
|
+
*/
|
|
311
|
+
export function resolveComponentPath(schema, atPath) {
|
|
312
|
+
const s = String(atPath || '')
|
|
313
|
+
if (!s.startsWith('@')) return s
|
|
314
|
+
const body = s.slice(1)
|
|
315
|
+
// componentId 的合法字符集是字母数字 _ -,遇到首个 `/` 或 `.` 就断开
|
|
316
|
+
const m = body.match(/^([A-Za-z0-9_-]+)(.*)$/)
|
|
317
|
+
if (!m) throw new Error(`@ 路径缺少 componentId:${s}`)
|
|
318
|
+
const cid = m[1]
|
|
319
|
+
const rest = m[2].replace(/^\//, '').replace(/^\./, '')
|
|
320
|
+
const found = findNodeByComponentId(schema, cid)
|
|
321
|
+
if (!found) throw new Error(`找不到 componentId=${cid};先用 card_read 看结构图,或 card_grep 按文案定位`)
|
|
322
|
+
if (!rest) return found.path
|
|
323
|
+
return `${found.path}.${rest}`
|
|
324
|
+
}
|
|
@@ -10,7 +10,7 @@ import { resolveCardDir } from './cardOpenFolder.js'
|
|
|
10
10
|
/**
|
|
11
11
|
* 卡片导出端点(编辑器「卡片网格预览 → 勾选 → 导出」的服务端实现)。
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* 产物契约(对桌面两份远端导出样本的逆向结论,详见
|
|
14
14
|
* docs/卡片导出产物逆向分析与导出功能.md):
|
|
15
15
|
* <targetDir>/<yyMMddHHmmss+4位序号>/ ← 临时文件夹(完成后删除)
|
|
16
16
|
* cards.json ← 卡片清单(cardPO 数组,殿后写入)
|
|
@@ -49,7 +49,7 @@ const EXPORT_JOB_TIMEOUT_MS = Number(process.env.CARDBUDDY_EXPORT_JOB_TIMEOUT_MS
|
|
|
49
49
|
/** 图片下载超时(与资源代理回源超时同口径) */
|
|
50
50
|
const IMAGE_FETCH_TIMEOUT_MS = 12000
|
|
51
51
|
|
|
52
|
-
/** 封面候选扩展名(与
|
|
52
|
+
/** 封面候选扩展名(与 platform-card-sync 的 findExistingCover 同集) */
|
|
53
53
|
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'])
|
|
54
54
|
|
|
55
55
|
/**
|
|
@@ -60,7 +60,7 @@ const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.
|
|
|
60
60
|
*/
|
|
61
61
|
const COVER_FILESTORE_PREFIX = '@filestore/dzqd-download'
|
|
62
62
|
|
|
63
|
-
/**
|
|
63
|
+
/** 平台状态常量:两份样本恒为三端全平台(与远端发布接口默认平台集一致) */
|
|
64
64
|
const PLATFORM_STATUS = ['iOS', 'Android', 'HarmonyOS']
|
|
65
65
|
|
|
66
66
|
/** 最小合法骨架屏 SVG:样本为线上编辑器按布局生成,本地不做布局推测,占位保证字段非空 */
|
|
@@ -255,7 +255,7 @@ function stampFolderName(targetDir, now = new Date()) {
|
|
|
255
255
|
return base + Math.random().toString(36).slice(2, 6).padStart(4, '0')
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
/** 找本地封面预览图(<cardId>.<ext>);没有返回 null(与
|
|
258
|
+
/** 找本地封面预览图(<cardId>.<ext>);没有返回 null(与 platform-card-sync 同规则) */
|
|
259
259
|
function findExistingCover(cardDir, id) {
|
|
260
260
|
let entries = []
|
|
261
261
|
try {
|
|
@@ -639,7 +639,7 @@ async function exportOneCard({ root, id, outDir, upstream, buildPreview }) {
|
|
|
639
639
|
fs.copyFileSync(src, dest)
|
|
640
640
|
} else {
|
|
641
641
|
const url = filestoreUrlFor(ref, upstream)
|
|
642
|
-
if (!url) throw new Error('
|
|
642
|
+
if (!url) throw new Error('未配置远端上游地址,无法下载 @filestore 图片')
|
|
643
643
|
const res = await fetch(url, {
|
|
644
644
|
headers: { 'User-Agent': 'Mozilla/5.0', Referer: upstream ? `${upstream}/` : '' },
|
|
645
645
|
signal: AbortSignal.timeout(IMAGE_FETCH_TIMEOUT_MS),
|
|
@@ -14,7 +14,7 @@ import { buildDataFromSchema } from './buildDataFromSchema.js'
|
|
|
14
14
|
* 空白卡必须显式归零压掉注入,否则新卡自带隐性间距且属性面板显示「未设置」。
|
|
15
15
|
* 形状取自存量卡根节点(见 style-meta.json:marginType / paddingType,'0' = 全边统一值)。
|
|
16
16
|
* 同一形状还有两份内联副本(禁跨包 import):skills/dsl-card-generator lib/scaffold.mjs、
|
|
17
|
-
* skills/
|
|
17
|
+
* skills/platform-card-sync api_batch_sync.mjs 的 buildBlankSchema —— 改这里必须同步改那两处。
|
|
18
18
|
*/
|
|
19
19
|
function blankResetStyle() {
|
|
20
20
|
return {
|
|
@@ -5,7 +5,7 @@ import { writeCardFiles } from './cardFiles.js'
|
|
|
5
5
|
import { log } from './logger.js'
|
|
6
6
|
import { resolveSkillPathOrProject } from './skillPaths.js'
|
|
7
7
|
|
|
8
|
-
const SYNC_SCRIPT_REL = '
|
|
8
|
+
const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
|
|
9
9
|
|
|
10
10
|
/** 读取 JSON 请求体(1MB 上限防失控) */
|
|
11
11
|
function readJsonBody(req) {
|
|
@@ -45,7 +45,7 @@ export function buildImportMeta({ baseUrl, appId, workspaceId, cardId, cardCode,
|
|
|
45
45
|
// 推文件名去读卡目录根的封面,picUrl 为空会退化成目录路径、直接抛异常。真值只在同步时可获得。
|
|
46
46
|
remote: remote || null,
|
|
47
47
|
syncedAt: new Date().toISOString(),
|
|
48
|
-
tool: '
|
|
48
|
+
tool: 'platform-card-sync',
|
|
49
49
|
dirty: false, // 本地编辑后偏离导入镜像为 true(重置为导入镜像后回 false)
|
|
50
50
|
lastExport: null, // 远端保存(/__save)后写入校验结果
|
|
51
51
|
editedAt: null, // 最近一次本地编辑时间(每次写盘都更新),供「按修改时间排序」
|
|
@@ -1,26 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 远端卡片列表缓存(dev server 进程内单例)。
|
|
3
3
|
*
|
|
4
|
-
* 用途:推送(保存 /
|
|
4
|
+
* 用途:推送(保存 / 预览 / 发布)前确认目标卡片确实存在于远端卡片列表中,
|
|
5
5
|
* 拦截「推到已删除 / pageId 错位的卡片」这类风险。
|
|
6
6
|
*
|
|
7
7
|
* 数据来源:IDC10001 卡片列表(与同步同一份数据)。刷新时机由同步逻辑触发——
|
|
8
8
|
* 全量同步(无 cardCode 过滤)时整体替换;单卡同步时增量合并,避免清空其他卡片。
|
|
9
9
|
* 列表条目字段:cardId(数字型,等于 schema.pageId)、code(英文卡 id)、cardName。
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* app 上下文:列表内容随 appId / workspaceId 而异(同一 baseUrl 下多个应用各有
|
|
12
|
+
* 独立卡片列表)。开发中途切换 .env.cb 的 appId 后,旧 app 的缓存对当前环境
|
|
13
|
+
* 完全无意义——若不失效,存在性闸门会拿旧 app 的列表误判新 app 的卡片
|
|
14
|
+
* 「不在远端列表中」。因此:
|
|
15
|
+
* - refreshCardList 写入时盖 appId 戳;发现 appId 变化即清空旧缓存(无论 full 与否);
|
|
16
|
+
* - checkPushAllowed 带上当前 appId 比对,缓存属于另一 app 时视为「未同步」放行。
|
|
17
|
+
*
|
|
18
|
+
* 单例性:本模块被 server/* 与 skills/platform-card-sync 以相同绝对路径 import,
|
|
12
19
|
* 在同一 dev server 进程内共享同一份内存,故同步写入、推送读取天然一致。
|
|
13
20
|
*/
|
|
14
21
|
|
|
22
|
+
import fs from 'node:fs'
|
|
23
|
+
import path from 'node:path'
|
|
24
|
+
import { pathToFileURL } from 'node:url'
|
|
25
|
+
import { skillsCandidates } from './skillPaths.js'
|
|
26
|
+
|
|
15
27
|
const cardIndex = new Map(); // cardId(String) -> { cardId, code, cardName }
|
|
16
28
|
|
|
29
|
+
/** 缓存当前所属的远端环境(由最近一次 refreshCardList 盖戳)。 */
|
|
30
|
+
const cacheCtx = { appId: null, workspaceId: null };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 最近一次「全量同步」所属的 appId。存在性闸门只在「当前 appId == 最近全量同步的
|
|
34
|
+
* appId」时才有判离权:增量合并(单卡同步)只会让列表更片面,片面列表永远不能
|
|
35
|
+
* 证明「卡片不在远端」——拿它做存在性判断就是刻舟求剑。
|
|
36
|
+
*/
|
|
37
|
+
let fullSyncedAppId = null;
|
|
38
|
+
|
|
17
39
|
/**
|
|
18
40
|
* 写入/合并卡片列表。
|
|
19
41
|
* @param {Array<{cardId:string|number, code?:string, cardName?:string}>} cards
|
|
20
|
-
* @param {{ full?: boolean }} [opts]
|
|
42
|
+
* @param {{ full?: boolean, appId?: string|number, workspaceId?: string|number }} [opts]
|
|
43
|
+
* full=true 时整体替换(先清空),否则增量合并;
|
|
44
|
+
* appId 与缓存戳不一致(环境切换)时无条件清空——旧 app 的列表条目不可复用;
|
|
45
|
+
* 仅 full=true 才更新 fullSyncedAppId(增量合并不赋予闸门判离权)。
|
|
21
46
|
*/
|
|
22
|
-
export function refreshCardList(cards, { full = false } = {}) {
|
|
23
|
-
|
|
47
|
+
export function refreshCardList(cards, { full = false, appId = null, workspaceId = null } = {}) {
|
|
48
|
+
const ctxChanged =
|
|
49
|
+
appId != null && cacheCtx.appId != null && String(cacheCtx.appId) !== String(appId);
|
|
50
|
+
if (full || ctxChanged) {
|
|
51
|
+
cardIndex.clear();
|
|
52
|
+
if (ctxChanged) fullSyncedAppId = null; // 换了环境,旧的全量结论作废
|
|
53
|
+
}
|
|
54
|
+
if (appId != null) {
|
|
55
|
+
cacheCtx.appId = String(appId);
|
|
56
|
+
cacheCtx.workspaceId = workspaceId == null ? null : String(workspaceId);
|
|
57
|
+
}
|
|
58
|
+
if (full && appId != null) fullSyncedAppId = String(appId);
|
|
24
59
|
for (const c of cards || []) {
|
|
25
60
|
if (c == null || c.cardId == null) continue;
|
|
26
61
|
const id = String(c.cardId);
|
|
@@ -35,17 +70,64 @@ export function hasCardId(cardId) {
|
|
|
35
70
|
|
|
36
71
|
/**
|
|
37
72
|
* 推送前存在性校验:目标卡片是否确实在远端卡片列表中(按 cardId/pageId 查)。
|
|
38
|
-
* - 缓存为空(本次 dev server
|
|
39
|
-
* -
|
|
40
|
-
*
|
|
73
|
+
* - 缓存为空(本次 dev server 尚未同步过):放行,避免误拦截首推。
|
|
74
|
+
* - 当前 appId 从未做过全量同步(或中途切换过环境):缓存片面 / 不可判,放行并标记
|
|
75
|
+
* notFullSynced / staleContext=true(调用方可据此记日志提示)。
|
|
76
|
+
* - 命中:放行。
|
|
77
|
+
* - 未命中(且当前 appId 有全量列表背书):拒绝,reason 给出拦截说明。
|
|
41
78
|
* @param {string|number} pageId
|
|
42
|
-
* @
|
|
79
|
+
* @param {{ appId?: string|number }} [ctx] 当前生效的远端环境(来自 platform-config)
|
|
80
|
+
* @returns {{ allowed: boolean, reason: string, staleContext?: boolean, notFullSynced?: boolean }}
|
|
43
81
|
*/
|
|
44
|
-
export function checkPushAllowed(pageId) {
|
|
82
|
+
export function checkPushAllowed(pageId, { appId = null } = {}) {
|
|
45
83
|
if (cardIndex.size === 0) return { allowed: true, reason: '' };
|
|
84
|
+
if (appId != null && String(appId) !== fullSyncedAppId) {
|
|
85
|
+
const staleContext =
|
|
86
|
+
cacheCtx.appId != null && String(cacheCtx.appId) !== String(appId);
|
|
87
|
+
return {
|
|
88
|
+
allowed: true,
|
|
89
|
+
reason: '',
|
|
90
|
+
staleContext: staleContext || undefined,
|
|
91
|
+
notFullSynced: !staleContext || undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
46
94
|
if (hasCardId(pageId)) return { allowed: true, reason: '' };
|
|
47
95
|
return {
|
|
48
96
|
allowed: false,
|
|
49
97
|
reason: `卡片 pageId=${pageId} 不在远端卡片列表中,疑似已删除或 pageId 错位,请先「远程同步」刷新列表后再推送`,
|
|
50
98
|
};
|
|
51
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 读取当前工程生效的远端环境(appId / workspaceId),供推送闸门做上下文比对。
|
|
103
|
+
* 从项目 skills/shared/platform-config.mjs 动态加载(三态解析,优先项目根下的
|
|
104
|
+
* skills/——其内部按自身位置向上找 .env.cb,读到的正是当前工程生效值)。
|
|
105
|
+
* 进程内按 root 记忆化:配置在 dev server 生命周期内视为不变(改 .env 需重启服务)。
|
|
106
|
+
* @param {string} root 项目根
|
|
107
|
+
* @returns {Promise<{ appId: string|null, workspaceId: string|null }>}
|
|
108
|
+
*/
|
|
109
|
+
const _ctxPromises = new Map();
|
|
110
|
+
export function currentAppContext(root) {
|
|
111
|
+
const key = String(root || '');
|
|
112
|
+
if (!_ctxPromises.has(key)) {
|
|
113
|
+
_ctxPromises.set(
|
|
114
|
+
key,
|
|
115
|
+
(async () => {
|
|
116
|
+
for (const dir of skillsCandidates(root)) {
|
|
117
|
+
const p = path.join(dir, 'shared', 'platform-config.mjs');
|
|
118
|
+
if (!fs.existsSync(p)) continue;
|
|
119
|
+
try {
|
|
120
|
+
const m = await import(pathToFileURL(p).href);
|
|
121
|
+
const c = m.platformConfig || {};
|
|
122
|
+
return {
|
|
123
|
+
appId: c.appId != null ? String(c.appId) : null,
|
|
124
|
+
workspaceId: c.workspaceId != null ? String(c.workspaceId) : null,
|
|
125
|
+
};
|
|
126
|
+
} catch { /* 读下一个候选 */ }
|
|
127
|
+
}
|
|
128
|
+
return { appId: null, workspaceId: null };
|
|
129
|
+
})(),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return _ctxPromises.get(key);
|
|
133
|
+
}
|
|
@@ -2,23 +2,23 @@ import path from 'node:path'
|
|
|
2
2
|
import { pathToFileURL } from 'node:url'
|
|
3
3
|
import { assertRemoteSource, findCardDir } from './cardSidecar.js'
|
|
4
4
|
import { commitCardChanges } from './cardGitCommit.js'
|
|
5
|
-
import { checkPushAllowed } from './cardListCache.js'
|
|
5
|
+
import { checkPushAllowed, currentAppContext } from './cardListCache.js'
|
|
6
6
|
import { log } from './logger.js'
|
|
7
7
|
import { resolveSkillPathOrProject } from './skillPaths.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* platform-card-sync 的批量同步脚本(发布复用),相对 skills/
|
|
11
11
|
* 与 cardSync.js 同款定位:发布只用到其中的 publishCard。
|
|
12
12
|
*/
|
|
13
|
-
const SYNC_SCRIPT_REL = '
|
|
13
|
+
const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* 顶栏「远端 → 卡片发布」的服务端处理:取远端最大版本号 +1 后发布上线(IDC10004 → IDC10016)。
|
|
17
|
-
* 复用
|
|
17
|
+
* 复用 platform-card-sync 的 publishCard(与「同步」「保存远端」「预览」共用同一后台远端会话)。
|
|
18
18
|
*
|
|
19
19
|
* 安全闸(与 /__preview 一致):
|
|
20
20
|
* - assertRemoteSource:本地自建、无 .import-meta.json 侧车的卡一律禁止发布,
|
|
21
|
-
*
|
|
21
|
+
* 避免把测试数据误发布到远端服务器。
|
|
22
22
|
* - checkPushAllowed:卡片须存在于远端列表缓存(全量同步时刷新);缓存为空(尚未同步)则放行。
|
|
23
23
|
*
|
|
24
24
|
* 发布成功后的收尾:把该卡目录(src/cards/<code>/)的未提交变更提交为一条
|
|
@@ -64,8 +64,13 @@ export function createPublishMiddleware({ root }) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
// 存在性闸门:发布前确认目标卡片在远端卡片列表缓存中(为空说明尚未同步,放行)。
|
|
67
|
+
// 缓存带 appId 戳:环境切换后旧 app 的缓存自动失效放行,不再误判「不在远端列表」。
|
|
67
68
|
{
|
|
68
|
-
const
|
|
69
|
+
const ctx = await currentAppContext(root)
|
|
70
|
+
const allow = checkPushAllowed(payload.schema.pageId, ctx)
|
|
71
|
+
if (allow.staleContext || allow.notFullSynced) {
|
|
72
|
+
log.warn('远端列表缓存对当前 appId 不可判(环境已切换或未全量同步),存在性闸门放行;建议先「远程同步」刷新当前环境列表', { ...ctx, staleContext: !!allow.staleContext })
|
|
73
|
+
}
|
|
69
74
|
if (!allow.allowed) {
|
|
70
75
|
res.statusCode = 409
|
|
71
76
|
res.setHeader('Content-Type', 'application/json')
|
|
@@ -4,7 +4,7 @@ import { readImportMeta } from './cardImportMeta.js'
|
|
|
4
4
|
import { log } from './logger.js'
|
|
5
5
|
import { resolveSkillPathOrProject } from './skillPaths.js'
|
|
6
6
|
|
|
7
|
-
const SYNC_SCRIPT_REL = '
|
|
7
|
+
const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* 调试面板「导入信息」tab「发布版本记录」弹框的远端数据源:
|
|
@@ -248,13 +248,13 @@ function handleStopPublish({ root, cardCode, req, res }) {
|
|
|
248
248
|
message: raw,
|
|
249
249
|
})
|
|
250
250
|
}
|
|
251
|
-
//
|
|
251
|
+
// platformPost 抛出的是「远端交易失败 [errorCode=IMVM0005] 状态修改失败 | {整包 JSON}」,
|
|
252
252
|
// 整包塞进 toast 读不过来:这里只取 errorMsg + code,其余留日志。
|
|
253
253
|
log.error('停止远端发布失败', { cardCode, versionId, taskId, error: raw.slice(0, 300) })
|
|
254
254
|
sendJson(res, 500, {
|
|
255
255
|
ok: false,
|
|
256
256
|
error: 'stop_publish_failed',
|
|
257
|
-
message:
|
|
257
|
+
message: platformErrorMessage(raw, '远端拒绝了本次停止操作'),
|
|
258
258
|
})
|
|
259
259
|
}
|
|
260
260
|
})()
|
|
@@ -368,7 +368,7 @@ function handlePublishVersion({ root, cardCode, req, res }) {
|
|
|
368
368
|
sendJson(res, 500, {
|
|
369
369
|
ok: false,
|
|
370
370
|
error: 'publish_failed',
|
|
371
|
-
message:
|
|
371
|
+
message: platformErrorMessage(raw, '远端拒绝了本次发布'),
|
|
372
372
|
})
|
|
373
373
|
}
|
|
374
374
|
})()
|
|
@@ -376,12 +376,12 @@ function handlePublishVersion({ root, cardCode, req, res }) {
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
/**
|
|
379
|
-
* 把
|
|
380
|
-
*
|
|
379
|
+
* 把 platformPost 抛出的原始错误压成一句可读文案。
|
|
380
|
+
* 原始形态:「远端交易失败 [errorCode=IMVM0005] 状态修改失败 | {整包 JSON}」——
|
|
381
381
|
* 整包 JSON 塞进 toast 读不过来,只取 errorMsg + code,其余由调用方写日志。
|
|
382
382
|
*/
|
|
383
|
-
function
|
|
384
|
-
const m = String(raw || '').match(
|
|
383
|
+
function platformErrorMessage(raw, fallback) {
|
|
384
|
+
const m = String(raw || '').match(/远端交易失败 \[errorCode=([^\]]+)\]\s*([^|]*)/)
|
|
385
385
|
if (!m) return raw
|
|
386
386
|
return `${(m[2] || '').trim() || fallback}(${m[1]})`
|
|
387
387
|
}
|