cardbuddy 0.22.0 → 0.24.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-CPpLZZyi.js → index-BumNBo-n.js} +81 -80
- package/dist/preview-editor/assets/{index-CU8zVA2P.css → index-MWKPP9nd.css} +1 -1
- 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/cardCompile.js +202 -0
- 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/flameCompile/client.js +466 -0
- package/dist/vendored/server/core/flameCompile/clientElements.js +372 -0
- package/dist/vendored/server/core/flameCompile/common.js +253 -0
- package/dist/vendored/server/core/flameCompile/h5.js +326 -0
- package/dist/vendored/server/core/flameCompile/index.js +126 -0
- package/dist/vendored/server/core/flameCompile/model.js +162 -0
- package/dist/vendored/server/core/index.js +12 -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
|
@@ -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
|
}
|
|
@@ -5,15 +5,15 @@ import { fileURLToPath } from 'node:url'
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6
6
|
|
|
7
7
|
import { recordExport, markDirty } from './cardImportMeta.js'
|
|
8
|
-
import { checkPushAllowed } from './cardListCache.js'
|
|
8
|
+
import { checkPushAllowed, currentAppContext } from './cardListCache.js'
|
|
9
9
|
import { assertRemoteSource, findCardDir } from './cardSidecar.js'
|
|
10
10
|
import { log } from './logger.js'
|
|
11
11
|
import { resolveSkillPathOrProject } from './skillPaths.js'
|
|
12
12
|
import { generateCardCover } from './screenshot.js'
|
|
13
13
|
import { serialize } from './browser.js'
|
|
14
14
|
|
|
15
|
-
/** 远端保存脚本相对 skills/ 的路径(与「同步」共用
|
|
16
|
-
const SYNC_SCRIPT_REL = '
|
|
15
|
+
/** 远端保存脚本相对 skills/ 的路径(与「同步」共用 platform-card-sync) */
|
|
16
|
+
const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* 推送成功后「截图 + 上传为远端预览图」的整段预算。
|
|
@@ -47,7 +47,7 @@ function withDeadline(promise, ms, label) {
|
|
|
47
47
|
*
|
|
48
48
|
* 为什么放在服务端而不是前端:①截图走本地无头渲染(screenshot.js 单例 Chrome),
|
|
49
49
|
* 与「预览图」按钮同一条链路,产出透明底、与卡片实际渲染尺寸 1:1 的 PNG;
|
|
50
|
-
*
|
|
50
|
+
* ②上传必须带远端 token,只有已登录的后台会话拿得到,前端不接触凭据。
|
|
51
51
|
*
|
|
52
52
|
* 三条纪律:
|
|
53
53
|
* - **best-effort**:任何失败都返回结构化结果,绝不向上抛——预览图是推送的附加产物,
|
|
@@ -108,14 +108,14 @@ export async function uploadPreviewAfterPush({
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
111
|
+
* 远端保存(写回远端)服务端处理。
|
|
112
112
|
* 前端「保存」按钮把当前预览的 schema(含实时数据绑定)POST 过来,
|
|
113
113
|
* 本中间件校验 pageId 后,经已登录浏览器中转调用 IDC10010.do 写回远端,
|
|
114
|
-
* 与「同步」共用
|
|
114
|
+
* 与「同步」共用 platform-card-sync 的 saveCardToRemote(带会话自愈)。
|
|
115
115
|
*
|
|
116
116
|
* @param {{ root: string, previewAfterPush?: typeof uploadPreviewAfterPush }} opts
|
|
117
117
|
* - previewAfterPush:推送成功后的「截图 + 上传预览图」实现,默认即上面的
|
|
118
|
-
* uploadPreviewAfterPush;单测注入替身以便在无 Chrome /
|
|
118
|
+
* uploadPreviewAfterPush;单测注入替身以便在无 Chrome / 无远端会话的环境断言接线。
|
|
119
119
|
*/
|
|
120
120
|
export function createSaveMiddleware({ root, previewAfterPush = uploadPreviewAfterPush }) {
|
|
121
121
|
return async function saveMiddleware(req, res, next) {
|
|
@@ -150,7 +150,7 @@ export function createSaveMiddleware({ root, previewAfterPush = uploadPreviewAft
|
|
|
150
150
|
return res.end(JSON.stringify({ ok: false, error: 'schema.pageId 缺失,无法确定要保存的卡片' }))
|
|
151
151
|
}
|
|
152
152
|
// 远端来源硬闸:本地自建、无 .import-meta.json 侧车的卡一律禁止推送,
|
|
153
|
-
//
|
|
153
|
+
// 避免把测试数据误提交到远端服务器(比「是否在远端列表」更根本)。
|
|
154
154
|
{
|
|
155
155
|
const src = assertRemoteSource(root, { code, pageId: schema.pageId })
|
|
156
156
|
if (!src.ok) {
|
|
@@ -183,9 +183,13 @@ export function createSaveMiddleware({ root, previewAfterPush = uploadPreviewAft
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
// 存在性闸门:推送前确认目标卡片在远端卡片列表缓存中(全量同步时刷新)。
|
|
186
|
-
//
|
|
186
|
+
// 缓存非空才校验(为空说明尚未同步,放行);缓存属于其他 appId(环境切换)也放行。
|
|
187
187
|
{
|
|
188
|
-
const
|
|
188
|
+
const ctx = await currentAppContext(root)
|
|
189
|
+
const allow = checkPushAllowed(schema.pageId, ctx)
|
|
190
|
+
if (allow.staleContext || allow.notFullSynced) {
|
|
191
|
+
log.warn('远端列表缓存对当前 appId 不可判(环境已切换或未全量同步),存在性闸门放行;建议先「远程同步」刷新当前环境列表', { ...ctx, staleContext: !!allow.staleContext })
|
|
192
|
+
}
|
|
189
193
|
if (!allow.allowed) {
|
|
190
194
|
res.statusCode = 409
|
|
191
195
|
res.setHeader('Content-Type', 'application/json')
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* 卡片「远端来源侧车」判定。
|
|
3
3
|
*
|
|
4
4
|
* 本地卡片目录 src/cards/<code>/ 下的 .import-meta.json 由「远程同步」写入,
|
|
5
|
-
*
|
|
5
|
+
* 记录该卡来自远端的 cardId 等来源信息。只有存在侧车的卡才被视为
|
|
6
6
|
* 「有合法远端来源」,允许推送(保存 / 预览)到服务器;本地自建、从未同步过的
|
|
7
|
-
*
|
|
7
|
+
* 测试卡没有侧车,一律禁止推送,避免把测试数据误提交到远端。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import fs from 'node:fs'
|
|
@@ -66,7 +66,7 @@ export function assertRemoteSource(root, { code, pageId } = {}) {
|
|
|
66
66
|
ok: false,
|
|
67
67
|
reason:
|
|
68
68
|
'该卡片为本地自建、无远端同步来源(缺少 .import-meta.json 侧车)。' +
|
|
69
|
-
'
|
|
69
|
+
'为防止将测试数据误提交到远端服务器,已禁止保存 / 推送。' +
|
|
70
70
|
'请先通过「远程同步」把卡片来源刷新到本地后再操作。',
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { pathToFileURL } from 'node:url'
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
|
-
import { checkPushAllowed, refreshCardList, hasCardId } from './cardListCache.js'
|
|
4
|
+
import { checkPushAllowed, refreshCardList, hasCardId, currentAppContext } from './cardListCache.js'
|
|
5
5
|
import { assertRemoteSource } from './cardSidecar.js'
|
|
6
6
|
import { log, fmtBytes } from './logger.js'
|
|
7
7
|
import { buildImportMeta, writeImportMeta } from './cardImportMeta.js'
|
|
8
8
|
import { resolveSkillPathOrProject, projectSkillsDir } from './skillPaths.js'
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
const SYNC_SCRIPT_REL = '
|
|
10
|
+
/** platform-card-sync 的批量同步脚本(同步 / 远端预览共用),相对 skills/ */
|
|
11
|
+
const SYNC_SCRIPT_REL = 'platform-card-sync/scripts/api_batch_sync.mjs'
|
|
12
12
|
|
|
13
13
|
/** 卡片 id 合法形态(与 normalizeCard / 新建卡同源正则) */
|
|
14
14
|
const CARD_CODE_RE = /^[A-Za-z0-9_-]+$/
|
|
@@ -96,8 +96,8 @@ function logSyncSummary(summary, requestLabel) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
100
|
-
* 复用
|
|
99
|
+
* 预览「同步远程数据」按钮的服务端处理:经已认证的远端浏览器会话拉取最新卡片写回 src/cards。
|
|
100
|
+
* 复用 platform-card-sync 的 syncCards(connectOverCDP 连用户已开的 Chrome,disconnect 不杀进程)。
|
|
101
101
|
*
|
|
102
102
|
* 任务制端点(编辑器「远端 → 查看全部 → 下载选中 / 全部下载」的进度弹窗依赖):
|
|
103
103
|
* - POST /__sync body { cardCode? , cardCodes? } → 立即返回 { ok, jobId, total }
|
|
@@ -282,7 +282,7 @@ export function createSyncMiddleware({ root }) {
|
|
|
282
282
|
job.result = {
|
|
283
283
|
success: false,
|
|
284
284
|
timedOut: true,
|
|
285
|
-
error: `同步任务超过 ${Math.round(SYNC_JOB_TIMEOUT_MS / 1000)}
|
|
285
|
+
error: `同步任务超过 ${Math.round(SYNC_JOB_TIMEOUT_MS / 1000)} 秒未完成,已自动收尾(上游无响应)。请重试,或检查远端登录态 / 网络。`,
|
|
286
286
|
}
|
|
287
287
|
log.error('远端同步看门狗触发:任务超时,已代其收尾并释放同步锁', {
|
|
288
288
|
jobId: job.id,
|
|
@@ -352,9 +352,9 @@ export function createSyncMiddleware({ root }) {
|
|
|
352
352
|
|
|
353
353
|
/**
|
|
354
354
|
* 预览「远端预览」按钮的服务端处理:复刻 ubomp 的 PreviewPopover 全链路——
|
|
355
|
-
*
|
|
355
|
+
* 先把当前卡片保存到远端(IDC10010),再取卡片 code(IDC10015)、
|
|
356
356
|
* 发布预览测试版(IDC10020)、通知真机(IDC10021),最终返回 cardCode 供前端生成二维码。
|
|
357
|
-
* 复用
|
|
357
|
+
* 复用 platform-card-sync 的 previewCardRemote(与「同步」「保存远端」共用同一后台远端会话)。
|
|
358
358
|
*
|
|
359
359
|
* @param {{ root: string }} opts
|
|
360
360
|
* @returns {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: Function) => void}
|
|
@@ -386,7 +386,7 @@ export function createPreviewMiddleware({ root }) {
|
|
|
386
386
|
return res.end(JSON.stringify({ error: 'missing schema.pageId', message: '需传入当前卡片 schema(含 pageId)' }))
|
|
387
387
|
}
|
|
388
388
|
// 远端来源硬闸:本地自建、无 .import-meta.json 侧车的卡一律禁止推送,
|
|
389
|
-
//
|
|
389
|
+
// 避免把测试数据误提交到远端服务器(比「是否在远端列表」更根本)。
|
|
390
390
|
{
|
|
391
391
|
const src = assertRemoteSource(root, { pageId: payload.schema.pageId })
|
|
392
392
|
if (!src.ok) {
|
|
@@ -396,9 +396,13 @@ export function createPreviewMiddleware({ root }) {
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
// 存在性闸门:推送前确认目标卡片在远端卡片列表缓存中(全量同步时刷新)。
|
|
399
|
-
//
|
|
399
|
+
// 缓存非空才校验(为空说明尚未同步,放行);缓存属于其他 appId(环境切换)也放行。
|
|
400
400
|
{
|
|
401
|
-
const
|
|
401
|
+
const ctx = await currentAppContext(root)
|
|
402
|
+
const allow = checkPushAllowed(payload.schema.pageId, ctx)
|
|
403
|
+
if (allow.staleContext || allow.notFullSynced) {
|
|
404
|
+
log.warn('远端列表缓存对当前 appId 不可判(环境已切换或未全量同步),存在性闸门放行;建议先「远程同步」刷新当前环境列表', { ...ctx, staleContext: !!allow.staleContext })
|
|
405
|
+
}
|
|
402
406
|
if (!allow.allowed) {
|
|
403
407
|
res.statusCode = 409
|
|
404
408
|
res.setHeader('Content-Type', 'application/json')
|
|
@@ -428,22 +432,22 @@ export function createPreviewMiddleware({ root }) {
|
|
|
428
432
|
}
|
|
429
433
|
}
|
|
430
434
|
|
|
431
|
-
/**
|
|
432
|
-
const SESSION_LOGIN_REL = '
|
|
433
|
-
/**
|
|
434
|
-
const SESSION_MANAGER_REL = '
|
|
435
|
+
/** platform-session-navigator 登录脚本(凭据读写 + 页面内登录验证),相对 skills/ */
|
|
436
|
+
const SESSION_LOGIN_REL = 'platform-session-navigator/scripts/platform_login.mjs'
|
|
437
|
+
/** platform-session-navigator 后台会话管理(同步 / 登录验证共用同一 headless Chrome) */
|
|
438
|
+
const SESSION_MANAGER_REL = 'platform-session-navigator/scripts/session-manager.mjs'
|
|
435
439
|
|
|
436
|
-
/** 凭据落盘路径:与
|
|
440
|
+
/** 凭据落盘路径:与 platform_login.mjs 默认路径同址,但经 projectSkillsDir 永远写项目根 skills/,不进 node_modules */
|
|
437
441
|
function credentialsFilePath(root) {
|
|
438
442
|
return path.join(
|
|
439
443
|
projectSkillsDir(root),
|
|
440
|
-
'
|
|
444
|
+
'platform-session-navigator',
|
|
441
445
|
'.local',
|
|
442
446
|
'credentials.json',
|
|
443
447
|
)
|
|
444
448
|
}
|
|
445
449
|
|
|
446
|
-
/** 动态加载
|
|
450
|
+
/** 动态加载 platform-session-navigator 登录脚本(仓库内 / 消费者工程 / 包内快照三态通用) */
|
|
447
451
|
async function loadLoginModule(root) {
|
|
448
452
|
const p = resolveSkillPathOrProject(root, SESSION_LOGIN_REL)
|
|
449
453
|
return import(pathToFileURL(p).href)
|
|
@@ -462,14 +466,14 @@ async function readSavedLoginName(root) {
|
|
|
462
466
|
|
|
463
467
|
/**
|
|
464
468
|
* 用给定账号密码做一次真实登录验证(不落盘)。
|
|
465
|
-
* 与「同步」共用同一后台 headless Chrome 会话池:连上 /
|
|
469
|
+
* 与「同步」共用同一后台 headless Chrome 会话池:连上 / 拉起后在远端同源页面内
|
|
466
470
|
* 直调登录接口(performLogin),以是否建立会话(sessionCreated)判定凭据是否正确——
|
|
467
471
|
* 已登录会话不会蒙混过关,因为performLogin 每次都真实提交一遍账号密码。
|
|
468
472
|
* 只 disconnect 不关闭浏览器:后台 Chrome 以 detached 常驻,供后续同步复用。
|
|
469
473
|
*
|
|
470
474
|
* @returns {Promise<{ sessionCreated: boolean, businessFailure: boolean, status?: number }>}
|
|
471
475
|
*/
|
|
472
|
-
async function
|
|
476
|
+
async function verifyPlatformLogin(root, loginName, rawPassword) {
|
|
473
477
|
const {
|
|
474
478
|
performLogin,
|
|
475
479
|
tryConnectBrowser,
|
|
@@ -485,10 +489,18 @@ async function verifyFinmallLogin(root, loginName, rawPassword) {
|
|
|
485
489
|
const md5Hex = createHash('md5')
|
|
486
490
|
.update(String(rawPassword), 'utf8')
|
|
487
491
|
.digest('hex')
|
|
492
|
+
// 对端站点 host:从登录模块给出的目标 URL 派生(不写死域名),用于认出已登录的标签页
|
|
493
|
+
const targetHost = (() => {
|
|
494
|
+
try {
|
|
495
|
+
return new URL(TARGET_URL).host
|
|
496
|
+
} catch {
|
|
497
|
+
return ''
|
|
498
|
+
}
|
|
499
|
+
})()
|
|
488
500
|
|
|
489
501
|
let browser = await tryConnectBrowser(SYNC_PORT)
|
|
490
502
|
if (!browser) {
|
|
491
|
-
// 端口未启动:拉起后台 headless Chrome(与
|
|
503
|
+
// 端口未启动:拉起后台 headless Chrome(与 ensurePlatformSession 同端口同数据目录)
|
|
492
504
|
launchChromeDetached(findChromePath(), SYNC_BROWSER_DATA_DIR, SYNC_PORT, TARGET_URL, {
|
|
493
505
|
headless: true,
|
|
494
506
|
})
|
|
@@ -508,7 +520,7 @@ async function verifyFinmallLogin(root, loginName, rawPassword) {
|
|
|
508
520
|
}
|
|
509
521
|
try {
|
|
510
522
|
const context = browser.contexts()[0]
|
|
511
|
-
let page = context?.pages().find((p) => p.url().includes(
|
|
523
|
+
let page = context?.pages().find((p) => targetHost && p.url().includes(targetHost))
|
|
512
524
|
if (!page) {
|
|
513
525
|
page = context ? await context.newPage() : await browser.newPage()
|
|
514
526
|
await page
|
|
@@ -594,7 +606,7 @@ export function createLoginCredentialsMiddleware({ root }) {
|
|
|
594
606
|
}
|
|
595
607
|
try {
|
|
596
608
|
// 1) 先做一次真实登录验证,通过才落盘
|
|
597
|
-
const result = await
|
|
609
|
+
const result = await verifyPlatformLogin(root, loginName, rawPassword)
|
|
598
610
|
if (result.sessionCreated) {
|
|
599
611
|
const { saveCredentials } = await loadLoginModule(root)
|
|
600
612
|
await saveCredentials(loginName, rawPassword, credentialsFilePath(root))
|
|
@@ -5,7 +5,7 @@ import { readImportMeta, recordRemoteVersion } from './cardImportMeta.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
|
/**
|
|
11
11
|
* 调试面板「卡片来源」tab 的远端最新版本持久化(POST,fetch + persist):POST /__card-version/:id
|