@mrrisega/dsh-remote 0.6.0-beta.6 → 0.6.0-beta.7
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/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +148 -53
- package/packages/dsh-remote-web/lib/index.js +1 -1
- package/packages/dsh-remote-web/test/account-switch.test.mjs +1 -1
- package/packages/dsh-remote-web/test/feedback-account-mine.test.mjs +70 -0
- package/packages/dsh-remote-web/test/uninstall-runtime.test.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.7",
|
|
4
4
|
"description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -335,6 +335,8 @@ window.__ModuleLoader__.load({
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
// ── 用户反馈模块:本地状态(thread 令牌 / 弹窗节流) ───────────────────
|
|
338
|
+
// 「我的反馈」双数据源:本地 dsh-feedback-threads(thread_token 凭据,匿名/本机未同步行)
|
|
339
|
+
// + 服务端 /api/feedback/mine(登录态账号历史,见 FeedbackCard loadMine/fbRows,合并去重展示)。
|
|
338
340
|
var FB_THREADS_KEY = "dsh-feedback-threads";
|
|
339
341
|
var FB_POPUP_KEY = "dsh-feedback-popup";
|
|
340
342
|
var FB_POPUP_DELAY_MS = 60 * 60 * 1000; // 安装/体验后至少 1 小时才弹窗(只弹一次)
|
|
@@ -400,9 +402,17 @@ window.__ModuleLoader__.load({
|
|
|
400
402
|
var threadDataArr = useState(null); var threadData = threadDataArr[0]; var setThreadData = threadDataArr[1];
|
|
401
403
|
var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
|
|
402
404
|
var msgArr = useState(null); var fbMsg = msgArr[0]; var setFbMsg = msgArr[1];
|
|
405
|
+
// 账号历史(服务端 /feedback/mine 聚合列表):null=未加载;登录态(SaaS 账号已配)打开「我的反馈」时拉取
|
|
406
|
+
var mineListArr = useState(null); var mineList = mineListArr[0]; var setMineList = mineListArr[1];
|
|
407
|
+
var mineErrArr = useState(false); var mineErr = mineErrArr[0]; var setMineErr = mineErrArr[1];
|
|
408
|
+
var mineBusyArr = useState(false); var mineBusy = mineBusyArr[0]; var setMineBusy = mineBusyArr[1];
|
|
403
409
|
|
|
404
410
|
function setMsg(kind, text) { setFbMsg({ kind: kind, text: text }); }
|
|
405
411
|
|
|
412
|
+
// 登录态(SaaS 且账号已配)→ 打开「我的反馈」拉账号历史(/mine 需要 JWT,节点半在登录态自动附加);
|
|
413
|
+
// 自建/未登录(cfg.phone 为空)→ 仍走本地 thread_token,不发 /mine。
|
|
414
|
+
var fbAccount = !!(cfg && cfg.phone && fbAuth === "account");
|
|
415
|
+
|
|
406
416
|
var loadCfg = useCallback(function () {
|
|
407
417
|
api("/dsh-remote/feedback-config").then(function (b) {
|
|
408
418
|
setCfg(b);
|
|
@@ -413,18 +423,68 @@ window.__ModuleLoader__.load({
|
|
|
413
423
|
}).catch(function (e) { setCfg({ reachable: false }); setMsg("warn", "反馈服务未连接:" + e.message); });
|
|
414
424
|
}, []);
|
|
415
425
|
|
|
426
|
+
/** 服务端账号历史行(mine 列表含该 id)→ 单条打开/回复走服务端(节点半自动附 JWT),无需本地 thread_token。 */
|
|
427
|
+
function fbRowIsAccount(id) {
|
|
428
|
+
return !!(mineList && mineList.some(function (x) { return x.id === id; }));
|
|
429
|
+
}
|
|
430
|
+
|
|
416
431
|
function loadThread(id) {
|
|
417
432
|
var t = threads.filter(function (x) { return x.id === id; })[0];
|
|
418
|
-
|
|
433
|
+
// 账号历史行:不带 thread_token(登录态由节点半自动附账号 JWT,服务端按账号归属放行);
|
|
434
|
+
// 匿名/纯本地行:仍按原 thread_token 逻辑(带本地凭据访问)。
|
|
435
|
+
var accountRow = fbRowIsAccount(id);
|
|
436
|
+
if (!accountRow && !t) return;
|
|
419
437
|
setBusy("thread:" + id);
|
|
420
|
-
fbApi("/feedback/" + id, { token: t.token }).then(function (b) {
|
|
438
|
+
fbApi("/feedback/" + id, accountRow ? {} : { token: t.token }).then(function (b) {
|
|
421
439
|
setThreadData(b.feedback);
|
|
422
440
|
}).catch(function (e) {
|
|
423
|
-
setMsg("err", "
|
|
441
|
+
if (e && e.status === 401) setMsg("err", "登录已过期,请退出后重新登录后再查看该反馈");
|
|
442
|
+
else setMsg("err", "加载反馈详情失败:" + (e && e.message));
|
|
424
443
|
}).finally(function () { setBusy(""); });
|
|
425
444
|
}
|
|
426
445
|
|
|
446
|
+
/** 登录态拉取账号历史(/feedback/mine,翻页参数可用;节点半在登录态自动附 JWT)。 */
|
|
447
|
+
function loadMine() {
|
|
448
|
+
if (!fbAccount) return;
|
|
449
|
+
setMineBusy(true); setMineErr(false);
|
|
450
|
+
fbApi("/feedback/mine?page=1&page_size=20").then(function (b) {
|
|
451
|
+
setMineList((b && b.items) || []);
|
|
452
|
+
}).catch(function (e) {
|
|
453
|
+
setMineErr(true);
|
|
454
|
+
var eb = e && e.body && e.body.error;
|
|
455
|
+
setMsg("err", e && e.status === 401
|
|
456
|
+
? "账号登录已过期,请退出后重新登录,再查看账号全部历史"
|
|
457
|
+
: "账号历史拉取失败:" + ((eb && eb.message) || (e && e.message)) + "(本机记录仍可查看)");
|
|
458
|
+
}).finally(function () { setMineBusy(false); });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** 合并展示列表:账号历史(服务端 mine,新→旧)在前;本机 thread_token 行去重后保留
|
|
462
|
+
* (匿名/本机新增但服务端未聚合的行仍可见,打开继续走本地凭据)。 */
|
|
463
|
+
function fbRows() {
|
|
464
|
+
var out = [];
|
|
465
|
+
var seen = {};
|
|
466
|
+
(mineList || []).forEach(function (it) {
|
|
467
|
+
seen[it.id] = true;
|
|
468
|
+
out.push({
|
|
469
|
+
id: it.id, acct: true,
|
|
470
|
+
status: it.status || "", category: it.category || "", title: it.title || "",
|
|
471
|
+
created_at: it.created_at || 0, reply_count: it.reply_count || 0,
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
threads.slice().reverse().forEach(function (t) {
|
|
475
|
+
if (seen[t.id]) return;
|
|
476
|
+
seen[t.id] = true;
|
|
477
|
+
out.push({ id: t.id, acct: false, status: "", category: "", title: "", created_at: 0, reply_count: 0 });
|
|
478
|
+
});
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
|
|
427
482
|
useEffect(function () { loadCfg(); }, [loadCfg]);
|
|
483
|
+
// 登录态打开「我的反馈」→ 拉账号历史;仅在未加载且非错误态自动触发(失败后点 tab 重试,不无限重试)
|
|
484
|
+
useEffect(function () {
|
|
485
|
+
if (tab === "mine" && fbAccount && mineList === null && !mineErr && !mineBusy) loadMine();
|
|
486
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
487
|
+
}, [tab, fbAccount, mineList === null, mineErr, mineBusy]);
|
|
428
488
|
|
|
429
489
|
var doSubmit = function () {
|
|
430
490
|
if (!fbContent.trim()) { setMsg("err", "请填写反馈内容"); return; }
|
|
@@ -441,6 +501,7 @@ window.__ModuleLoader__.load({
|
|
|
441
501
|
.then(function (b) {
|
|
442
502
|
fbRememberThread(b.feedback.id, b.thread_token);
|
|
443
503
|
setThreads(fbLoadThreads());
|
|
504
|
+
if (fbAccount) setMineList(null); // 账号态提交成功 → 回「我的反馈」时重新拉取(含本条)
|
|
444
505
|
setFbTitle(""); setFbContent(""); setContact("");
|
|
445
506
|
setMsg("ok", "✅ 反馈已提交,可在「我的反馈」查看回复");
|
|
446
507
|
setTab("mine");
|
|
@@ -458,9 +519,15 @@ window.__ModuleLoader__.load({
|
|
|
458
519
|
var text = input ? input.value.trim() : "";
|
|
459
520
|
if (!text) { setMsg("err", "请填写回复内容"); return; }
|
|
460
521
|
var t = threads.filter(function (x) { return x.id === id; })[0];
|
|
461
|
-
|
|
522
|
+
var accountRow = fbRowIsAccount(id);
|
|
523
|
+
// 账号历史行:服务端回复(节点半自动附 JWT,无需本地 thread_token);本地/匿名行:带 thread_token 回复
|
|
524
|
+
var replyOpts = { method: "POST", body: JSON.stringify({ content: text }) };
|
|
525
|
+
if (!accountRow) {
|
|
526
|
+
if (!t) return;
|
|
527
|
+
replyOpts.token = t.token;
|
|
528
|
+
}
|
|
462
529
|
setBusy("reply:" + id);
|
|
463
|
-
fbApi("/feedback/" + id + "/replies",
|
|
530
|
+
fbApi("/feedback/" + id + "/replies", replyOpts)
|
|
464
531
|
.then(function () {
|
|
465
532
|
if (input) input.value = "";
|
|
466
533
|
loadThread(id);
|
|
@@ -468,7 +535,8 @@ window.__ModuleLoader__.load({
|
|
|
468
535
|
})
|
|
469
536
|
.catch(function (e) {
|
|
470
537
|
var errBody = e.body && e.body.error;
|
|
471
|
-
if (
|
|
538
|
+
if (e && e.status === 401) setMsg("err", "登录已过期,请重新登录后再回复");
|
|
539
|
+
else if (errBody && errBody.code === "rate_limited") setMsg("err", "今天回复次数已达上限,请明天再试");
|
|
472
540
|
else setMsg("err", "回复失败:" + (errBody && errBody.message ? errBody.message : e.message));
|
|
473
541
|
})
|
|
474
542
|
.finally(function () { setBusy(""); });
|
|
@@ -480,11 +548,83 @@ window.__ModuleLoader__.load({
|
|
|
480
548
|
if (id !== openId) loadThread(id);
|
|
481
549
|
};
|
|
482
550
|
|
|
551
|
+
/** 「我的反馈」tab 内容:账号历史行直接用服务端字段渲染(状态/类别/标题/时间/回复数),
|
|
552
|
+
* 并保留本机未同步的 thread_token 行;单条展开/回复由 loadThread/doReply 分流。 */
|
|
553
|
+
function renderMine() {
|
|
554
|
+
var rows = fbRows();
|
|
555
|
+
if (rows.length === 0) {
|
|
556
|
+
var emptyText = fbAccount
|
|
557
|
+
? (mineBusy ? "正在加载账号历史…" : (mineErr ? "账号历史加载失败,点上方「我的反馈」tab 重试" : "暂无反馈,提交第一条?"))
|
|
558
|
+
: "还没有提交过反馈。" + (cfg && cfg.reachable === false ? "(反馈服务未连接)" : "登录手机号账号后,可在任意设备查看账号全部历史。");
|
|
559
|
+
return h("div", null,
|
|
560
|
+
h("div", { className: "dru-fb-empty" }, emptyText),
|
|
561
|
+
fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return h("div", null,
|
|
565
|
+
rows.map(function (r) {
|
|
566
|
+
var open = openId === r.id;
|
|
567
|
+
var td = open ? threadData : null;
|
|
568
|
+
var st = td ? td.status : r.status;
|
|
569
|
+
var cat = td ? td.category : r.category;
|
|
570
|
+
var titleTxt = td ? (td.title || "") : r.title;
|
|
571
|
+
var ts = td ? td.created_at : r.created_at;
|
|
572
|
+
var catLbl = (function () {
|
|
573
|
+
var c = FB_CATEGORIES.filter(function (x) { return x.value === cat; })[0];
|
|
574
|
+
return c ? c.label : (cat || "反馈");
|
|
575
|
+
})();
|
|
576
|
+
return h("div", { key: r.id, className: "dru-fb-item" },
|
|
577
|
+
h("div", { className: "dru-fb-item-head" },
|
|
578
|
+
fbStatusBadge(st || "open"),
|
|
579
|
+
h("span", { className: "dru-fb-cat" }, catLbl),
|
|
580
|
+
h("span", { className: "dru-fb-item-title" }, titleTxt || ("反馈 #" + r.id.slice(-6))),
|
|
581
|
+
h("span", { className: "dru-fb-item-time" },
|
|
582
|
+
(ts ? new Date(ts).toLocaleString() : "") +
|
|
583
|
+
(r.acct && !open && r.reply_count > 0 ? " · " + r.reply_count + " 条回复" : ""))
|
|
584
|
+
),
|
|
585
|
+
open && td
|
|
586
|
+
? h("div", null,
|
|
587
|
+
h("div", { className: "dru-fb-item-content" }, td.content),
|
|
588
|
+
td.replies.map(function (reply) {
|
|
589
|
+
return h("div", { key: reply.id, className: "dru-fb-reply" },
|
|
590
|
+
h("div", { className: "dru-fb-reply-row" },
|
|
591
|
+
h("span", { className: "dru-fb-reply-who" + (reply.author === "user" ? " user" : "") }, reply.author === "admin" ? "管理员回复" : "我"),
|
|
592
|
+
h("span", { className: "dru-fb-reply-text" }, reply.content)
|
|
593
|
+
),
|
|
594
|
+
h("div", { className: "dru-fb-item-time" }, new Date(reply.created_at).toLocaleString())
|
|
595
|
+
);
|
|
596
|
+
}),
|
|
597
|
+
h("div", { className: "dru-fb-reply" },
|
|
598
|
+
h("textarea", { id: "dru-fb-reply-" + r.id, className: "dru-fb-reply-input", placeholder: "回复管理员…", maxLength: 2000 }),
|
|
599
|
+
h("div", { style: { textAlign: "right", marginTop: 6 } },
|
|
600
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { doReply(r.id); } }, busy === "reply:" + r.id ? "发送中…" : "回复")
|
|
601
|
+
)
|
|
602
|
+
),
|
|
603
|
+
h("div", { className: "dru-fb-item-time", style: { textAlign: "right", marginTop: 8 } },
|
|
604
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { loadThread(r.id); } }, "↻ 刷新"),
|
|
605
|
+
" ",
|
|
606
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, onClick: function () { setOpenId(""); setThreadData(null); } }, "收起")
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
: h("div", { className: "dru-fb-item-time", style: { textAlign: "right" } },
|
|
610
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { openThread(r.id); } }, open ? "收起" : "查看 / 回复"))
|
|
611
|
+
);
|
|
612
|
+
}),
|
|
613
|
+
mineBusy ? h("div", { className: "dru-hint", style: { marginTop: 6 } }, "账号历史同步中…") : null,
|
|
614
|
+
fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
var mineRows = fbRows(); // 「我的反馈」展示行数:账号历史 + 本机未同步线程(合并去重后)
|
|
483
619
|
return h("div", { className: "dru-card" },
|
|
484
620
|
h("h3", null, "💬 用户反馈"),
|
|
485
621
|
h("div", { className: "dru-fb-tabs" },
|
|
486
622
|
h("div", { className: "dru-fb-tab" + (tab === "submit" ? " active" : ""), onClick: function () { setTab("submit"); setFbMsg(null); } }, "提交反馈"),
|
|
487
|
-
h("div", { className: "dru-fb-tab" + (tab === "mine" ? " active" : ""), onClick: function () {
|
|
623
|
+
h("div", { className: "dru-fb-tab" + (tab === "mine" ? " active" : ""), onClick: function () {
|
|
624
|
+
setTab("mine"); setFbMsg(null); setThreads(fbLoadThreads());
|
|
625
|
+
// 登录态(账号已配)打开「我的反馈」→ 拉取账号历史(合并展示,换设备/重装也能看到)
|
|
626
|
+
if (fbAccount && mineList === null) loadMine();
|
|
627
|
+
} }, "我的反馈" + (mineRows.length ? "(" + mineRows.length + ")" : ""))
|
|
488
628
|
),
|
|
489
629
|
tab === "submit"
|
|
490
630
|
? h("div", null,
|
|
@@ -512,52 +652,7 @@ window.__ModuleLoader__.load({
|
|
|
512
652
|
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doSubmit }, busy === "submit" ? "提交中…" : "提交反馈"),
|
|
513
653
|
fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
|
|
514
654
|
)
|
|
515
|
-
:
|
|
516
|
-
threads.length === 0
|
|
517
|
-
? h("div", { className: "dru-fb-empty" }, "还没有提交过反馈。" + (cfg && cfg.reachable === false ? "(反馈服务未连接)" : ""))
|
|
518
|
-
: threads.slice().reverse().map(function (t) {
|
|
519
|
-
var open = openId === t.id;
|
|
520
|
-
var td = open ? threadData : null;
|
|
521
|
-
return h("div", { key: t.id, className: "dru-fb-item" },
|
|
522
|
-
h("div", { className: "dru-fb-item-head" },
|
|
523
|
-
fbStatusBadge(td ? td.status : "open"),
|
|
524
|
-
h("span", { className: "dru-fb-cat" }, (function () {
|
|
525
|
-
var c = FB_CATEGORIES.filter(function (x) { return x.value === (td ? td.category : ""); })[0];
|
|
526
|
-
return c ? c.label : (td ? td.category : "反馈");
|
|
527
|
-
})()),
|
|
528
|
-
h("span", { className: "dru-fb-item-title" }, td ? (td.title || "反馈") : "反馈 #" + t.id.slice(-6)),
|
|
529
|
-
h("span", { className: "dru-fb-item-time" }, td ? new Date(td.created_at).toLocaleString() : "")
|
|
530
|
-
),
|
|
531
|
-
open && td
|
|
532
|
-
? h("div", null,
|
|
533
|
-
h("div", { className: "dru-fb-item-content" }, td.content),
|
|
534
|
-
td.replies.map(function (r) {
|
|
535
|
-
return h("div", { key: r.id, className: "dru-fb-reply" },
|
|
536
|
-
h("div", { className: "dru-fb-reply-row" },
|
|
537
|
-
h("span", { className: "dru-fb-reply-who" + (r.author === "user" ? " user" : "") }, r.author === "admin" ? "管理员回复" : "我"),
|
|
538
|
-
h("span", { className: "dru-fb-reply-text" }, r.content)
|
|
539
|
-
),
|
|
540
|
-
h("div", { className: "dru-fb-item-time" }, new Date(r.created_at).toLocaleString())
|
|
541
|
-
);
|
|
542
|
-
}),
|
|
543
|
-
h("div", { className: "dru-fb-reply" },
|
|
544
|
-
h("textarea", { id: "dru-fb-reply-" + t.id, className: "dru-fb-reply-input", placeholder: "回复管理员…", maxLength: 2000 }),
|
|
545
|
-
h("div", { style: { textAlign: "right", marginTop: 6 } },
|
|
546
|
-
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { doReply(t.id); } }, busy === "reply:" + t.id ? "发送中…" : "回复")
|
|
547
|
-
)
|
|
548
|
-
),
|
|
549
|
-
h("div", { className: "dru-fb-item-time", style: { textAlign: "right", marginTop: 8 } },
|
|
550
|
-
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { loadThread(t.id); } }, "↻ 刷新"),
|
|
551
|
-
" ",
|
|
552
|
-
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, onClick: function () { setOpenId(""); setThreadData(null); } }, "收起")
|
|
553
|
-
)
|
|
554
|
-
)
|
|
555
|
-
: h("div", { className: "dru-fb-item-time", style: { textAlign: "right" } },
|
|
556
|
-
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { openThread(t.id); } }, open ? "收起" : "查看 / 回复"))
|
|
557
|
-
);
|
|
558
|
-
}),
|
|
559
|
-
fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
|
|
560
|
-
)
|
|
655
|
+
: renderMine()
|
|
561
656
|
);
|
|
562
657
|
}
|
|
563
658
|
|
|
@@ -946,7 +946,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
946
946
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
947
947
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
948
948
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
949
|
-
const PLUGIN_VERSION = "0.6.0-beta.
|
|
949
|
+
const PLUGIN_VERSION = "0.6.0-beta.7";
|
|
950
950
|
const UPDATE_LOG = ".dsh-update.log";
|
|
951
951
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
952
952
|
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import { apply } from "../lib/index.js";
|
|
8
8
|
|
|
9
|
-
test("切换账号时轮换设备身份并重启 bridge", async () => {
|
|
9
|
+
test("切换账号时轮换设备身份并重启 bridge", { skip: process.platform !== "darwin" }, async () => {
|
|
10
10
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-account-switch-"));
|
|
11
11
|
const fakeHome = path.join(tempDir, "home");
|
|
12
12
|
const fakeBin = path.join(tempDir, "bin");
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 「用户反馈」账号历史(浏览器半)回归:登录态(SaaS 且账号已配)打开「我的反馈」时
|
|
2
|
+
// 拉服务端 /api/feedback/mine 并与其本地 thread_token 列表去重合并;账号历史行单条
|
|
3
|
+
// 打开/回复走服务端(节点半自动附 JWT,无需本地 thread_token),匿名/本地行仍走原
|
|
4
|
+
// thread_token 路径。这些是 client.js 的源码级/结构断言(沿用 settings-entry.test.mjs 手法)。
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
const SOURCE = readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
|
|
10
|
+
|
|
11
|
+
test("登录态打开「我的反馈」拉账号历史:fbAccount 门槛 + /feedback/mine 请求(带翻页)", () => {
|
|
12
|
+
// /mine 需要 JWT:仅 SaaS 且账号已配(cfg.phone)才发请求;自建/未登录不发(本地 thread_token 路径不变)
|
|
13
|
+
assert.match(SOURCE, /var fbAccount = !!\s*\(cfg && cfg\.phone && fbAuth === "account"\);/);
|
|
14
|
+
// 经 /dsh-remote/feedback/api 前缀代理 → 上游 {base}/api/feedback/mine?page&page_size
|
|
15
|
+
assert.match(SOURCE, /fbApi\("\/feedback\/mine\?page=1&page_size=20"\)/);
|
|
16
|
+
// 仅在账号态拉取:loadMine 第一行即兜底
|
|
17
|
+
assert.match(SOURCE, /function loadMine\(\) \{\s*if \(!fbAccount\) return;/);
|
|
18
|
+
// 打开「我的反馈」tab 时触发;useEffect 自动补拉但带错误/忙碌护栏(失败后点 tab 重试,不无限重试)
|
|
19
|
+
assert.match(SOURCE, /if \(fbAccount && mineList === null\) loadMine\(\);/);
|
|
20
|
+
assert.match(SOURCE, /if \(tab === "mine" && fbAccount && mineList === null && !mineErr && !mineBusy\) loadMine\(\);/);
|
|
21
|
+
// 账号态提交成功后清空列表缓存 → 下次进「我的反馈」重新拉取(含本条)
|
|
22
|
+
assert.match(SOURCE, /if \(fbAccount\) setMineList\(null\);\s*\/\/ 账号态提交成功 → 回「我的反馈」时重新拉取\(含本条\)/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("合并展示:服务端 mine 行在前、本机未同步 thread_token 行去重保留;字段映射到卡片渲染", () => {
|
|
26
|
+
// mine 列表项(服务端 toPublic + reply_count)映射为展示行:status/category/title/created_at/reply_count
|
|
27
|
+
assert.match(SOURCE, /function fbRows\(\) \{/);
|
|
28
|
+
assert.match(SOURCE, /status: it\.status \|\| "", category: it\.category \|\| "", title: it\.title \|\| "",/);
|
|
29
|
+
assert.match(SOURCE, /created_at: it\.created_at \|\| 0, reply_count: it\.reply_count \|\| 0,/);
|
|
30
|
+
// 结构顺序:账号历史行先 push,本机行(去重)后 push
|
|
31
|
+
const minePush = SOURCE.indexOf("it.reply_count || 0,");
|
|
32
|
+
const localPush = SOURCE.indexOf("out.push({ id: t.id, acct: false");
|
|
33
|
+
assert.ok(minePush !== -1 && localPush !== -1 && minePush < localPush, "账号历史行应先于本机行进入展示列表");
|
|
34
|
+
// 去重:mine 已含的本地行跳过
|
|
35
|
+
assert.match(SOURCE, /if \(seen\[t\.id\]\) return;/);
|
|
36
|
+
// 卡片渲染路径:折叠态直接用服务端行字段(状态徽章/时间/回复数),展开仍复用详情+回复结构
|
|
37
|
+
assert.match(SOURCE, /fbStatusBadge\(st \|\| "open"\)/);
|
|
38
|
+
assert.match(SOURCE, /new Date\(ts\)\.toLocaleString\(\)/);
|
|
39
|
+
assert.match(SOURCE, /r\.acct && !open && r\.reply_count > 0 \? " · " \+ r\.reply_count \+ " 条回复"/);
|
|
40
|
+
// 管理员回复/回复输入框保留
|
|
41
|
+
assert.match(SOURCE, /reply\.author === "admin" \? "管理员回复" : "我"/);
|
|
42
|
+
assert.match(SOURCE, /id: "dru-fb-reply-" \+ r\.id/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("账号历史行打开/回复走服务端(自动附 JWT),本地/匿名行仍带 thread_token", () => {
|
|
46
|
+
// 归属判断:mine 列表含该 id → 账号行
|
|
47
|
+
assert.match(SOURCE, /function fbRowIsAccount\(id\)/);
|
|
48
|
+
assert.match(SOURCE, /mineList && mineList\.some\(function \(x\) \{ return x\.id === id; \}\)/);
|
|
49
|
+
// 单条加载:账号行不带 token(节点半登录态自动附 JWT,服务端按账号归属放行);本地行带 token
|
|
50
|
+
assert.match(SOURCE, /fbApi\("\/feedback\/" \+ id, accountRow \? \{\} : \{ token: t\.token \}\)/);
|
|
51
|
+
// 回复同理:仅非账号行补充 thread_token
|
|
52
|
+
assert.match(SOURCE, /var replyOpts = \{ method: "POST", body: JSON\.stringify\(\{ content: text \}\) \};/);
|
|
53
|
+
assert.match(SOURCE, /if \(!accountRow\) \{\s*if \(!t\) return;\s*replyOpts\.token = t\.token;/);
|
|
54
|
+
// 匿名/本地提交路径原样保留(thread 凭据仍只存本地 localStorage)
|
|
55
|
+
assert.match(SOURCE, /fbRememberThread\(b\.feedback\.id, b\.thread_token\);/);
|
|
56
|
+
assert.match(SOURCE, /var FB_THREADS_KEY = "dsh-feedback-threads";/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("错误与空态:401 → 提示重新登录;网络失败 → 可读;空态区分 账号/未登录", () => {
|
|
60
|
+
// 拉列表 401(如 token 失效) → 重新登录提示
|
|
61
|
+
assert.match(SOURCE, /账号登录已过期,请退出后重新登录,再查看账号全部历史/);
|
|
62
|
+
// 单条 401 同样给重新登录提示
|
|
63
|
+
assert.match(SOURCE, /if \(e && e\.status === 401\) setMsg\("err", "登录已过期,请退出后重新登录后再查看该反馈"\);/);
|
|
64
|
+
// 网络/服务端错误 → 可读错误并保留本机记录说明
|
|
65
|
+
assert.match(SOURCE, /账号历史拉取失败:" \+ \(\(eb && eb\.message\) \|\| \(e && e\.message\)\) \+ "(本机记录仍可查看)"/);
|
|
66
|
+
// 账号态空列表 / 未登录空列表 两种文案
|
|
67
|
+
assert.match(SOURCE, /暂无反馈,提交第一条?/);
|
|
68
|
+
assert.match(SOURCE, /还没有提交过反馈。/);
|
|
69
|
+
assert.match(SOURCE, /登录手机号账号后,可在任意设备查看账号全部历史。/);
|
|
70
|
+
});
|
|
@@ -77,7 +77,7 @@ async function plantInstallation(tempHome, opts = {}) {
|
|
|
77
77
|
return { relayDir, profile };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
test("彻底卸载:配置目录整目录清空(含账号/密钥/固化运行时),不误伤 profile 其它条目与家目录其它数据", async () => {
|
|
80
|
+
test("彻底卸载:配置目录整目录清空(含账号/密钥/固化运行时),不误伤 profile 其它条目与家目录其它数据", { skip: process.platform !== "darwin" }, async () => {
|
|
81
81
|
const tempHome = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-rt-"));
|
|
82
82
|
const prevHome = process.env.HOME;
|
|
83
83
|
process.env.HOME = tempHome;
|
|
@@ -220,7 +220,7 @@ test("DSH_RELAY_SKIP_SERVICE 开关:置位时即便存在自启动 plist 也
|
|
|
220
220
|
}
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
test("自然跳过(无开关):HOME 有 plist 但服务未运行 → 删除 plist 但不发 launchctl bootout(PATH 假命令接管)", async () => {
|
|
223
|
+
test("自然跳过(无开关):HOME 有 plist 但服务未运行 → 删除 plist 但不发 launchctl bootout(PATH 假命令接管)", { skip: process.platform !== "darwin" }, async () => {
|
|
224
224
|
const tempHome = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-rt-natural-"));
|
|
225
225
|
const prevHome = process.env.HOME;
|
|
226
226
|
const prevPath = process.env.PATH;
|