dsh-plugin-deck-studio 0.2.25 → 0.2.27
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/index.js +30 -1
- package/lib/data-dir.js +9 -0
- package/package.json +1 -1
- package/seeds/docs/install.md +7 -0
- package/ui/workbench.html +46 -3
package/index.js
CHANGED
|
@@ -2458,7 +2458,12 @@ export function createService(config, logger, env = process.env) {
|
|
|
2458
2458
|
appendixCount: adapterOf(deck.deckId)?.available() ? adapterOf(deck.deckId).overview().appendixCount ?? 0 : workspace.manifest(deck.deckId)?.appendix?.count ?? 0,
|
|
2459
2459
|
appendixRows: workspace.manifest(deck.deckId)?.appendix?.rows ?? [],
|
|
2460
2460
|
}));
|
|
2461
|
-
|
|
2461
|
+
// F-205:显式点名要打开某本 deck、但这次快照里还没有它(刚导入 / 刚创建,列表还没跟上)时,
|
|
2462
|
+
// 不要静默退回第一本——前端拿到 requestedMissing 会立刻重试,避免「导入成功却打开别的 deck」。
|
|
2463
|
+
const requestedMissing = Boolean(requestedDeckId) && !decks.some((deck) => deck.deckId === requestedDeckId);
|
|
2464
|
+
const activeId = requestedMissing
|
|
2465
|
+
? decks[0]?.deckId ?? workspace.listDeckIds()[0] ?? null
|
|
2466
|
+
: requestedDeckId ?? decks[0]?.deckId ?? workspace.listDeckIds()[0] ?? null;
|
|
2462
2467
|
// P0:进 deck 时先体检(规格 §2「进入某本 deck 时,每次,读 manifest 之前」)。
|
|
2463
2468
|
// mismatch / partial 时不 ensureDeck:那会凭空建出空的 decks/<id>/,旧图也找不回(禁止项)。
|
|
2464
2469
|
const dataDirInfo = dataDirStateOf({ config, store, workspace, adapters, env }, activeId);
|
|
@@ -2575,6 +2580,9 @@ export function createService(config, logger, env = process.env) {
|
|
|
2575
2580
|
renderSettings: renderSettings(),
|
|
2576
2581
|
decks,
|
|
2577
2582
|
active,
|
|
2583
|
+
// F-205:这次请求点名的 deck 是否存在(true = 列表还没跟上,前端重试即可)。
|
|
2584
|
+
requestedDeckId: requestedDeckId ?? null,
|
|
2585
|
+
requestedMissing,
|
|
2578
2586
|
designProjects: designProjects(),
|
|
2579
2587
|
bindings: listBindings(),
|
|
2580
2588
|
proposals: base.pendingProposals,
|
|
@@ -2677,6 +2685,25 @@ function noteDataDirTransitions(serviceLike, report, trigger = "poll") {
|
|
|
2677
2685
|
/**
|
|
2678
2686
|
* 数据目录载荷:黄条 / 设置页 / 自检共用(不重算解析,只合并最新体检)。
|
|
2679
2687
|
*/
|
|
2688
|
+
/**
|
|
2689
|
+
* 「指针覆盖了 profile 配置」的事实(没有冲突返回 null)。
|
|
2690
|
+
* 这是本次事故的根因:web profile 没配 dataRoot、desktop 配了仓库 .deck,而指针全机共享——
|
|
2691
|
+
* 谁写的指针谁说了算,界面上却只显示「当前 = 指针」,看不出 profile 里还写着另一个目录。
|
|
2692
|
+
*/
|
|
2693
|
+
function pointerConflictOf(resolved = {}) {
|
|
2694
|
+
const profileDataRoot = String(resolved.profileDataRoot ?? "").trim();
|
|
2695
|
+
const fact = resolved.dataRoot ?? {};
|
|
2696
|
+
if (!profileDataRoot || fact.source !== "pointer") return null;
|
|
2697
|
+
if (String(fact.value ?? "") === profileDataRoot) return null;
|
|
2698
|
+
return {
|
|
2699
|
+
profileDataRoot,
|
|
2700
|
+
pointerDataRoot: String(fact.value ?? ""),
|
|
2701
|
+
pointerFile: String(resolved.pointerFile?.path ?? ""),
|
|
2702
|
+
setBy: String(resolved.pointerFile?.setBy ?? ""),
|
|
2703
|
+
updatedAt: String(resolved.pointerFile?.updatedAt ?? ""),
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2680
2707
|
function dataDirStateOf(serviceLike, deckId = null, { refresh = false, countImages = false } = {}) {
|
|
2681
2708
|
const { config } = serviceLike;
|
|
2682
2709
|
const resolved = config.dataDir ?? {};
|
|
@@ -2693,6 +2720,8 @@ function dataDirStateOf(serviceLike, deckId = null, { refresh = false, countImag
|
|
|
2693
2720
|
firstRun: resolved.firstRun === true,
|
|
2694
2721
|
processCwd: resolved.processCwd ?? process.cwd(),
|
|
2695
2722
|
sourceLabel: resolved.sourceLabel ?? "",
|
|
2723
|
+
// 指针赢过 profile 时的「为什么」:profile 配的是哪个目录、指针文件在哪。前端只在有冲突时多画一行。
|
|
2724
|
+
pointerConflict: pointerConflictOf(resolved),
|
|
2696
2725
|
banner: dataDirBannerFacts({ report, deckId, envOverride: resolved.envOverride?.active === true, dataRoot: config.dataRoot }),
|
|
2697
2726
|
decks: report.decks,
|
|
2698
2727
|
candidates: report.candidates,
|
package/lib/data-dir.js
CHANGED
|
@@ -232,6 +232,8 @@ function decksDirEmpty(dataRoot) {
|
|
|
232
232
|
/** 冲突 warn 只打一次:5s 轮询与多次重建服务都不重复刷屏。 */
|
|
233
233
|
let warnedDataRootConflict = false;
|
|
234
234
|
let warnedProjectsRootConflict = false;
|
|
235
|
+
/** 指针 vs profile 的冲突按「指针→profile」这一对去重:同一处冲突不刷屏,换了目录再提醒一次。 */
|
|
236
|
+
const warnedPointerProfilePairs = new Set();
|
|
235
237
|
|
|
236
238
|
export function resolveDataDir({ env = process.env, profileDataRoot = "", profileProjectsRoot = "", logger = null } = {}) {
|
|
237
239
|
const envDataRoot = normalizeDirPath(env[DATA_DIR_ENV] ?? "", env);
|
|
@@ -253,6 +255,13 @@ export function resolveDataDir({ env = process.env, profileDataRoot = "", profil
|
|
|
253
255
|
const pointerDataRoot = pointer.error || !isAbsolute(pointer.dataRoot) ? "" : pointer.dataRoot;
|
|
254
256
|
const pointerProjectsRoot = pointer.error || !isAbsolute(pointer.projectsRoot) ? "" : pointer.projectsRoot;
|
|
255
257
|
|
|
258
|
+
// 指针(用户点「切到这个目录」写下的)优先级高于 profile;两者不一致时以前是**完全静默**的,
|
|
259
|
+
// 表现成「我这本 deck 怎么没了」。留一条 warn,日志里能找到是谁把目录换掉的。
|
|
260
|
+
const pointerProfileKey = pointerDataRoot && profileRoot && pointerDataRoot !== profileRoot ? `${pointerDataRoot}→${profileRoot}` : "";
|
|
261
|
+
if (warn && pointerProfileKey && !warnedPointerProfilePairs.has(pointerProfileKey)) {
|
|
262
|
+
warnedPointerProfilePairs.add(pointerProfileKey);
|
|
263
|
+
warn(`dataRoot: 指针文件(${pointerDataRoot})与 profile 配置(${profileRoot})不一致,已按指针生效;要让 profile 配置生效就删掉指针文件 ${pointer.path}`);
|
|
264
|
+
}
|
|
256
265
|
const dataRoot = envDataRoot || pointerDataRoot || profileRoot || normalizeDirPath(defaultDataRoot(env), env);
|
|
257
266
|
const source = envDataRoot ? "env" : pointerDataRoot ? "pointer" : profileRoot ? "profile" : "default";
|
|
258
267
|
const projectsSource = envProjectsRoot ? "env" : pointerProjectsRoot ? "pointer" : profileProjects ? "profile" : "default";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-deck-studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "Deckline:DSH 的 image deck / PPT 工作台插件——原生 Chat + 整页图片 deck 工作区 + 设计风格库;用对话做 PPT、按页出图、版本可追踪、导出 PDF/PPTX。",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"homepage": "https://github.com/yuna78/dsh-deck-studio#readme",
|
package/seeds/docs/install.md
CHANGED
|
@@ -188,6 +188,13 @@ Deckline 的数据分两处:**你的 deck 源**(`projectsRoot`,纯 md +
|
|
|
188
188
|
`dataRoot` / `DSH_DECK_DATA_ROOT`,两边就各看各的。所以「换到另一个端口,内容变空 / 标题在但页面空白」先别怀疑端口:
|
|
189
189
|
去那个实例的 ⚙ 看「当前数据目录」,再和这边显示的路径比一比。
|
|
190
190
|
**换端口不改数据,换 dataRoot 才改数据。**
|
|
191
|
+
- **指针的优先级高于 profile**(「打开一片空白 + 黄条」最常见的真凶):`$DSH_HOME/deckline/location.json`
|
|
192
|
+
是全机共享的「用户选择」,解析顺序是 `env > 指针 > profile patch > 默认`。在一个实例里点过
|
|
193
|
+
「切到这个目录」,同一个 `$DSH_HOME` 下的其它 profile 也会跟着换目录——**哪怕它在 patch 里写了 `dataRoot`**。
|
|
194
|
+
想让各端稳定一致,二选一:① 每个 profile 的 patch 写**同一个** `dataRoot`,并删掉
|
|
195
|
+
`~/.dsh/deckline/location.json`;② 只用默认目录,不要在界面里切。真有冲突时黄条会多写一行
|
|
196
|
+
「你的 profile 里配的是 A,但指针文件把它改成了 B——指针优先;切换只改指针,不动文件」,
|
|
197
|
+
日志里也有一条 `dataRoot: 指针文件(B)与 profile 配置(A)不一致` 的 warn(每次冲突只打一次)。
|
|
191
198
|
- 报问题不用自己拼路径:⚙ →「当前数据目录与自检」底部 **「复制诊断信息」** 一键复制(内容由插件生成、已脱敏,
|
|
192
199
|
不含 key),贴出来就包含 cwd / dataRoot(含来源)/ projectsRoot / 每本 deck 有没有 `script/current.md`。
|
|
193
200
|
- 开发环境用独立 `dataRoot`,不要把运行数据写进仓库(见 §7「开发数据目录」)。
|
package/ui/workbench.html
CHANGED
|
@@ -830,9 +830,15 @@ function mdToHtml(markdown, options = {}) {
|
|
|
830
830
|
}
|
|
831
831
|
|
|
832
832
|
/* ---------------- 数据加载 ---------------- */
|
|
833
|
+
/* F-205:并发保护。导入 / 切 deck 会连着打好几次 workbench 接口,先发的旧请求如果后回来,
|
|
834
|
+
* 会把刚切到的新 deck 覆盖成旧数据(表现:导入成功却停在上一个 / 第一个 deck)。
|
|
835
|
+
* 只让**最后发起**的那次结果落地。 */
|
|
836
|
+
let LOAD_SEQ = 0;
|
|
833
837
|
async function loadWorkbench(deckId = null) {
|
|
838
|
+
const seq = ++LOAD_SEQ;
|
|
834
839
|
try {
|
|
835
840
|
const next = await route("workbench", deckId ? { deckId } : undefined);
|
|
841
|
+
if (seq !== LOAD_SEQ) return;
|
|
836
842
|
const active = next.active || {};
|
|
837
843
|
const signature = JSON.stringify([
|
|
838
844
|
active.deckId,
|
|
@@ -865,6 +871,7 @@ async function loadWorkbench(deckId = null) {
|
|
|
865
871
|
}
|
|
866
872
|
}
|
|
867
873
|
} catch (error) {
|
|
874
|
+
if (seq !== LOAD_SEQ) return;
|
|
868
875
|
STATE.demo = true;
|
|
869
876
|
STATE.data = demoData();
|
|
870
877
|
renderChrome();
|
|
@@ -1521,11 +1528,16 @@ function renderDataDirBanner(data) {
|
|
|
1521
1528
|
: "⚠ 这本 deck 的正文和图片不在当前数据目录";
|
|
1522
1529
|
const currentRoot = dd?.dataRoot?.value || banner.dataRoot || "—";
|
|
1523
1530
|
const sourceNote = dd?.sourceLabel ? `(${esc(dd.sourceLabel)})` : "";
|
|
1531
|
+
// 指针赢过 profile 时把「为什么」写出来:否则只看到「当前 = 指针」,不知道 profile 里还写着另一个目录。
|
|
1532
|
+
const conflict = dd?.pointerConflict && dd.pointerConflict.profileDataRoot !== dd?.dataRoot?.value ? dd.pointerConflict : null;
|
|
1533
|
+
const conflictLine = conflict
|
|
1534
|
+
? `<span class="data-dir-line data-dir-conflict">你的 profile 里配的是 <code>${esc(conflict.profileDataRoot)}</code>,但指针文件把它改成了上面这个目录${conflict.setBy ? `(${esc(conflict.setBy)})` : ""}——<b>指针优先</b>。点「切到这个目录」只改指针,不会删改任何 deck 文件。</span>`
|
|
1535
|
+
: "";
|
|
1524
1536
|
const candidates = banner.candidates || [];
|
|
1525
1537
|
const rows = candidates.length
|
|
1526
1538
|
? candidates.map((candidate) => `<div class="data-dir-candidate"><span class="data-dir-path">已发现:${esc(candidate.dataRoot)}</span><span class="data-dir-count">(${Number(candidate.deckCount) || 0} 本 deck · ${Number(candidate.imageCount) || 0} 张图)</span><button class="btn ghost" data-data-root="${esc(candidate.dataRoot)}"${envOverride ? " disabled" : ""}>切到这个目录</button></div>`).join("")
|
|
1527
1539
|
: `<div class="data-dir-candidate data-dir-empty"><span>没找到这本 deck 的历史数据。如果你在另一个 DSH 实例里出过图,请在那个实例的『设置』里看它显示的数据目录,然后把它填到下面。</span><input id="dataDirManualInput" placeholder="/绝对路径"${envOverride ? " disabled" : ""}><button class="btn ghost" id="dataDirManualSwitch"${envOverride ? " disabled" : ""}>用这个目录</button></div>`;
|
|
1528
|
-
box.innerHTML = `<div class="data-dir-banner-body"><div class="data-dir-banner-main"><b>${title}</b><span class="data-dir-line" title="换端口不改数据,换 dataRoot 才改数据。">当前:<code>${esc(currentRoot)}</code>${sourceNote}</span>${rows}</div><div class="data-dir-banner-actions"><button class="btn ghost" id="dataDirOpenSettings">打开设置</button><button class="btn ghost" id="dataDirCopyDiag">复制诊断信息</button>${envOverride ? `<span class="data-dir-readonly">由环境变量 ${esc(dd?.envOverride?.name || "DSH_DECK_DATA_ROOT")} 指定(只读)</span>` : ""}</div></div>`;
|
|
1540
|
+
box.innerHTML = `<div class="data-dir-banner-body"><div class="data-dir-banner-main"><b>${title}</b><span class="data-dir-line" title="换端口不改数据,换 dataRoot 才改数据。">当前:<code>${esc(currentRoot)}</code>${sourceNote}</span>${conflictLine}${rows}</div><div class="data-dir-banner-actions"><button class="btn ghost" id="dataDirOpenSettings">打开设置</button><button class="btn ghost" id="dataDirCopyDiag">复制诊断信息</button>${envOverride ? `<span class="data-dir-readonly">由环境变量 ${esc(dd?.envOverride?.name || "DSH_DECK_DATA_ROOT")} 指定(只读)</span>` : ""}</div></div>`;
|
|
1529
1541
|
box.hidden = false;
|
|
1530
1542
|
box.querySelectorAll("[data-data-root]").forEach((node) => node.addEventListener("click", () => switchDataDirTo(node.dataset.dataRoot)));
|
|
1531
1543
|
box.querySelector("#dataDirOpenSettings")?.addEventListener("click", () => openSettingsPanel());
|
|
@@ -5119,6 +5131,32 @@ getEl("impList").addEventListener("click", (event) => {
|
|
|
5119
5131
|
renderImportPicker();
|
|
5120
5132
|
}
|
|
5121
5133
|
});
|
|
5134
|
+
/**
|
|
5135
|
+
* F-205:导入成功后必须停在**刚导入的这本** deck。
|
|
5136
|
+
*
|
|
5137
|
+
* 导入要跑 pdftoppm(几秒到几十秒),期间 5s 轮询、宿主指令、刚创建 deck 的列表延迟都可能
|
|
5138
|
+
* 把视图切到别的 deck(典型:工作台列表还没收录新 deck 时接口退回第一本)。
|
|
5139
|
+
* 这里在成功后的 ~3 秒里以新 deck 为准核对几次;用户自己点了/按了键就立刻停手,不抢手动导航。
|
|
5140
|
+
*/
|
|
5141
|
+
async function openImportedDeck(deckId) {
|
|
5142
|
+
let userTookOver = false;
|
|
5143
|
+
const flag = () => { userTookOver = true; };
|
|
5144
|
+
window.addEventListener("pointerdown", flag, true);
|
|
5145
|
+
window.addEventListener("keydown", flag, true);
|
|
5146
|
+
try {
|
|
5147
|
+
await showDeck(deckId);
|
|
5148
|
+
for (const gap of [500, 900, 1500]) {
|
|
5149
|
+
await new Promise((resolve) => setTimeout(resolve, gap));
|
|
5150
|
+
if (userTookOver) return;
|
|
5151
|
+
if (STATE.view === "deck" && STATE.deckId === deckId) continue;
|
|
5152
|
+
await showDeck(deckId);
|
|
5153
|
+
}
|
|
5154
|
+
} finally {
|
|
5155
|
+
window.removeEventListener("pointerdown", flag, true);
|
|
5156
|
+
window.removeEventListener("keydown", flag, true);
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5122
5160
|
getEl("impSubmit").addEventListener("click", async () => {
|
|
5123
5161
|
if (!IMP.selected || IMP.busy) return;
|
|
5124
5162
|
IMP.busy = true;
|
|
@@ -5143,7 +5181,8 @@ getEl("impSubmit").addEventListener("click", async () => {
|
|
|
5143
5181
|
getEl("impState").textContent = parts.join(" · ");
|
|
5144
5182
|
closeImportPdfModal();
|
|
5145
5183
|
flash(`已导入「${result.deck?.label || ""}」:${result.pages} 页整页图`, "ok");
|
|
5146
|
-
|
|
5184
|
+
// F-205:导入成功后以新 deck 为准(详见 openImportedDeck)。
|
|
5185
|
+
if (result.deck?.deckId) await openImportedDeck(result.deck.deckId);
|
|
5147
5186
|
} catch (error) {
|
|
5148
5187
|
getEl("impState").textContent = "导入失败:" + importErrText(error?.code, error?.message);
|
|
5149
5188
|
} finally {
|
|
@@ -6322,7 +6361,11 @@ function backToManual(box) {
|
|
|
6322
6361
|
STATE.homeSection = match ? match[1] : "decks";
|
|
6323
6362
|
showHome();
|
|
6324
6363
|
}
|
|
6325
|
-
|
|
6364
|
+
// F-205:导入(pdftoppm 可能要几十秒)期间别用旧 deckId 轮询,否则回来晚的那次会把新 deck 顶掉。
|
|
6365
|
+
setInterval(() => {
|
|
6366
|
+
if (IMP.busy) return;
|
|
6367
|
+
loadWorkbench(STATE.deckId);
|
|
6368
|
+
}, 5000);
|
|
6326
6369
|
|
|
6327
6370
|
/**
|
|
6328
6371
|
* 0.2.21:boot 完成 → 把启动期间暂存的「打开某本 deck」执行掉,再向宿主报 ready。
|