@mrrisega/dsh-remote 0.7.12-beta.4 → 0.7.12-beta.6
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/clients/dsh-remote/dingtalk-auth.mjs +631 -0
- package/clients/dsh-remote/dingtalk-bot.mjs +249 -32
- package/clients/dsh-remote/dsh-bridge.mjs +35 -0
- package/clients/dsh-remote/feishu-bot.mjs +101 -0
- package/clients/dsh-remote/im-commands.mjs +302 -0
- package/clients/dsh-remote/test/dingtalk-auth-e2e.test.mjs +138 -0
- package/clients/dsh-remote/test/dingtalk-auth.test.mjs +465 -0
- package/clients/dsh-remote/test/dingtalk-bot.test.mjs +218 -1
- package/clients/dsh-remote/test/feishu-bot.test.mjs +34 -2
- package/clients/dsh-remote/test/im-caps-fixture.mjs +8 -0
- package/clients/dsh-remote/test/im-commands.test.mjs +283 -0
- package/clients/dsh-remote/test/wecom-bot.test.mjs +37 -0
- package/clients/dsh-remote/wecom-bot.mjs +84 -2
- package/dsh-setup.mjs +1 -1
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +85 -48
- package/packages/dsh-remote-web/lib/index.js +62 -202
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dingtalk-auth.mjs +631 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +35 -0
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +1 -1
- package/packages/dsh-remote-web/test/im-bots.test.mjs +76 -166
- package/packages/dsh-remote-web/test/panel-ia.test.mjs +82 -29
|
@@ -53,6 +53,9 @@ import WebSocket from "ws";
|
|
|
53
53
|
import { createEventSubscriber, NODE_KINDS } from "./dsh-events.mjs";
|
|
54
54
|
// 通道感知格式化(2026-11-08:企微/钉钉 markdown 不支持表格,发前统一适配;飞书/微信各有分支)
|
|
55
55
|
import { formatForChannel } from "./im-format.mjs";
|
|
56
|
+
// 命令层(2026-11-08):/help /status /stop /ls /use /unbind 在**进模型前**拦下执行;
|
|
57
|
+
// 解析/文案/能力门禁三家共用(与会员升级文案同口径)。
|
|
58
|
+
import { executeImCommand, writeImBotsSession } from "./im-commands.mjs";
|
|
56
59
|
// 会员能力表 = **单一真相源**:直接复用微信通道那套(WECHAT_CAPABILITY_TABLE / limitsFor / grantsCap),
|
|
57
60
|
// 不另造一套。企微/飞书/钉钉将来都套同一套判定(见 research/.../im-channel-membership-design.md)。
|
|
58
61
|
import {
|
|
@@ -716,8 +719,8 @@ export function createWecomBot(options = {}) {
|
|
|
716
719
|
const approvalCardTtlMs = positiveNumber(options.approvalCardTtlMs, APPROVAL_CARD_TTL_MS);
|
|
717
720
|
const onApprovalDecision = typeof options.onApprovalDecision === "function" ? options.onApprovalDecision : null;
|
|
718
721
|
// 会话可选绑定(task-24):配置固定的会话(空 = 跟随活跃会话)
|
|
719
|
-
|
|
720
|
-
|
|
722
|
+
let configuredSessionId = String(options.configuredSessionId || "").trim();
|
|
723
|
+
let configuredSessionTitle = truncateText(options.configuredSessionTitle || "", 60);
|
|
721
724
|
// ── 会员判定(2026-11-07 业主拍板:通知免费、审批免费、互动半阉割)──
|
|
722
725
|
// capsProvider 由 bridge 注入(复用微信通道的 resolveWechatTier:字符串档位或 {plan,caps,limits,rev});
|
|
723
726
|
// 拿不到 / 未注入 → **按 free fail-closed**。
|
|
@@ -1367,8 +1370,87 @@ export function createWecomBot(options = {}) {
|
|
|
1367
1370
|
}
|
|
1368
1371
|
|
|
1369
1372
|
/** 把入站文本转给 DSH 会话(尽力,失败给用户一句人话)。 */
|
|
1373
|
+
/**
|
|
1374
|
+
* 命令层(三家共用 `im-commands.mjs`):命中返回 true(已回复,调用方直接 return)。
|
|
1375
|
+
* 回复走被动回复/主动推送(replyText),不经过模型、不计额度。
|
|
1376
|
+
*/
|
|
1377
|
+
async function handleCommand(frame, info) {
|
|
1378
|
+
const text = String((info && info.text) || "");
|
|
1379
|
+
if (!/^\s*\//.test(text)) return false; // 快路径:非命令不构造 ctx
|
|
1380
|
+
const cmd = await executeImCommand({
|
|
1381
|
+
text,
|
|
1382
|
+
ctx: {
|
|
1383
|
+
caps: capsNow(),
|
|
1384
|
+
tier: tierNow(),
|
|
1385
|
+
appUrl,
|
|
1386
|
+
channelLabel: "企业微信",
|
|
1387
|
+
sessions: async () => {
|
|
1388
|
+
try { await refreshSessions({ force: true }); } catch { /* 忽略 */ }
|
|
1389
|
+
return sessionList.map((x) => ({ sessionId: x.id, title: x.title, updatedAt: x.updatedAt, running: false }));
|
|
1390
|
+
},
|
|
1391
|
+
currentSessionId: () => {
|
|
1392
|
+
const known = boundSessionId || activeSession.id || "";
|
|
1393
|
+
if (known) return known;
|
|
1394
|
+
if (configuredSessionId && canCap("sessions")) return configuredSessionId;
|
|
1395
|
+
if (sessionList.length) return sessionList[0].id;
|
|
1396
|
+
const sub = subscriber;
|
|
1397
|
+
try {
|
|
1398
|
+
if (sub && typeof sub.sessions === "function") {
|
|
1399
|
+
const ids = sub.sessions();
|
|
1400
|
+
if (Array.isArray(ids) && ids.length) return String(ids[0]);
|
|
1401
|
+
}
|
|
1402
|
+
} catch { /* 忽略 */ }
|
|
1403
|
+
return "";
|
|
1404
|
+
},
|
|
1405
|
+
currentSessionTitle: () => titleOfSession(boundSessionId || activeSession.id, activeSession.title || ""),
|
|
1406
|
+
cancelSession: async ({ sessionId }) => {
|
|
1407
|
+
const sub = subscriber;
|
|
1408
|
+
if (!sub || typeof sub.cancelSession !== "function") return { ok: false, message: "订阅器未就绪" };
|
|
1409
|
+
try {
|
|
1410
|
+
const r = await sub.cancelSession({ sessionId });
|
|
1411
|
+
return r && typeof r === "object" ? r : { ok: Boolean(r) };
|
|
1412
|
+
} catch (e) { return { ok: false, message: e && e.message ? e.message : String(e) }; }
|
|
1413
|
+
},
|
|
1414
|
+
switchSession: async ({ sessionId, title }) => {
|
|
1415
|
+
const r = writeImBotsSession(relayDir, "wecom", { sessionId, sessionTitle: title });
|
|
1416
|
+
if (r.ok) {
|
|
1417
|
+
configuredSessionId = sessionId;
|
|
1418
|
+
configuredSessionTitle = truncateText(title || "", 60);
|
|
1419
|
+
writeStatus();
|
|
1420
|
+
}
|
|
1421
|
+
return r;
|
|
1422
|
+
},
|
|
1423
|
+
unbind: async () => {
|
|
1424
|
+
state.boundChatId = "";
|
|
1425
|
+
writeStatus();
|
|
1426
|
+
return { ok: true };
|
|
1427
|
+
},
|
|
1428
|
+
statusSnapshot: async () => ({
|
|
1429
|
+
connected: state.connected === true,
|
|
1430
|
+
authenticated: state.authenticated === true,
|
|
1431
|
+
state: state.connected ? "connected" : "idle",
|
|
1432
|
+
lastError: state.lastError || "",
|
|
1433
|
+
tier: tierNow(),
|
|
1434
|
+
usage: { month: monthKey(), messages: usageThisMonth() },
|
|
1435
|
+
messagesPerMonth: messageLimitNow(),
|
|
1436
|
+
remainingMessages: messageLimitNow() ? Math.max(0, messageLimitNow() - usageThisMonth()) : null,
|
|
1437
|
+
currentSessionId: boundSessionId || activeSession.id || "",
|
|
1438
|
+
currentSessionTitle: titleOfSession(boundSessionId || activeSession.id, activeSession.title || "")
|
|
1439
|
+
})
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
if (!cmd || !cmd.handled) return false;
|
|
1443
|
+
replyText(frame, cmd.text);
|
|
1444
|
+
emitter.emit("command", { ...info, command: cmd.command, ok: cmd.ok === true, gated: cmd.gated === true });
|
|
1445
|
+
log.info(`命令 ${cmd.command}${cmd.gated ? "(会员闸拦截)" : ""} 已回复(不转模型)`);
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1370
1449
|
async function dispatchInbound(frame, info) {
|
|
1371
1450
|
const sub = subscriber;
|
|
1451
|
+
// ⓪ 命令层:命中即执行并回复,**绝不进模型**,也不计入额度(/stop、/unbind 永远免费)
|
|
1452
|
+
const cmd = await handleCommand(frame, info);
|
|
1453
|
+
if (cmd) return;
|
|
1372
1454
|
// ① 会员闸(通知/审批免费;派活受能力与月度额度约束,fail-closed)
|
|
1373
1455
|
const gated = await membershipGate(frame, info);
|
|
1374
1456
|
if (gated) return;
|
package/dsh-setup.mjs
CHANGED
|
@@ -149,7 +149,7 @@ function findDepWs(startDir) {
|
|
|
149
149
|
* 漏同步就会出现「入口脚本在、依赖不在」的半装运行时(插件半据此判定「运行环境已就绪」,
|
|
150
150
|
* 却永远起不来 bridge —— 2026-09-23 用户反馈的现场)。`src/channel-ref.mjs` 同理(本安装器也 import)。
|
|
151
151
|
*/
|
|
152
|
-
const RUNTIME_CLIENT_FILES = ["mobile-adapter.mjs", "dsh-bridge.mjs", "e2ee-shim.mjs", "e2ee-client.mjs", "e2ee-shim-script.js", "mobile-adapter.test.mjs", "wechat-channel.mjs", "dsh-events.mjs", "wechat-runtime.mjs", "wecom-bot.mjs", "im-format.mjs", "dingtalk-bot.mjs", "feishu-bot.mjs", "src/lifecycle.mjs", "src/channel-ref.mjs"];
|
|
152
|
+
const RUNTIME_CLIENT_FILES = ["mobile-adapter.mjs", "dsh-bridge.mjs", "e2ee-shim.mjs", "e2ee-client.mjs", "e2ee-shim-script.js", "mobile-adapter.test.mjs", "wechat-channel.mjs", "dsh-events.mjs", "wechat-runtime.mjs", "wecom-bot.mjs", "im-format.mjs", "dingtalk-bot.mjs", "dingtalk-auth.mjs", "im-commands.mjs", "feishu-bot.mjs", "src/lifecycle.mjs", "src/channel-ref.mjs"];
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
155
|
* 把"客户端脚本"补齐到配置目录。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.7.12-beta.
|
|
3
|
+
"version": "0.7.12-beta.6",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"test:plugin": "DSH_PROFILE_DIR= DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 DSH_RELAY_DEFAULT_API=http://127.0.0.1:1 DSH_RELAY_BUNDLED_RUNTIME=/nonexistent-dsh-bundled NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
|
|
57
57
|
"_comment:test:bridge": "⚠️ 必须 --test-concurrency=1(与 test:plugin 对齐):tunnel-auth 有一条「bridge 5 秒内必须重连」的硬期限,并行跑时新增用例抢 CPU 会把它撞破 → 偶发假红(实测单独跑 2/2 通过、并发跑偶发 0/2)。假红比慢更贵。",
|
|
58
58
|
"test:bridge": "DSH_PROFILE_DIR= DSH_RELAY_DEFAULT_API=http://127.0.0.1:1 NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 clients/dsh-remote/test/*.test.mjs",
|
|
59
|
-
"check": "node --check scripts/check-profile-activation.mjs && node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/wechat-channel.mjs && node --check clients/dsh-remote/wechat-runtime.mjs && node --check clients/dsh-remote/dsh-events.mjs && node --check clients/dsh-remote/agent/index.mjs && node --check clients/dsh-remote/agent/version.mjs && node --check clients/dsh-remote/agent/detect.mjs && node --check clients/dsh-remote/agent/permission-bridge.mjs && node --check clients/dsh-remote/agent/proc.mjs && node --check clients/dsh-remote/agent/events.mjs && node --check clients/dsh-remote/agent/codex.mjs && node --check clients/dsh-remote/agent/gemini.mjs && node --check clients/dsh-remote/agent/opencode.mjs && node --check clients/dsh-remote/agent/agent-e2e-codex.mjs && node --check clients/dsh-remote/agent/agent-e2e-real.mjs && node --check clients/dsh-remote/push-reporter.mjs && node --check clients/dsh-remote/decision-poller.mjs && node --check clients/dsh-remote/approval-coordinator.mjs && node --check clients/dsh-remote/wecom-bot.mjs && node --check clients/dsh-remote/im-format.mjs && node --check clients/dsh-remote/dingtalk-bot.mjs && node --check clients/dsh-remote/feishu-bot.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && node --check packages/relay-router/src/index.mjs && node --check packages/relay-router/src/logger.mjs && bash -n deploy/install-open.sh && grep -q 'command -v setsid' deploy/install-open.sh && grep -q 'command -v ss' deploy/install-open.sh",
|
|
59
|
+
"check": "node --check scripts/check-profile-activation.mjs && node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/wechat-channel.mjs && node --check clients/dsh-remote/wechat-runtime.mjs && node --check clients/dsh-remote/dsh-events.mjs && node --check clients/dsh-remote/agent/index.mjs && node --check clients/dsh-remote/agent/version.mjs && node --check clients/dsh-remote/agent/detect.mjs && node --check clients/dsh-remote/agent/permission-bridge.mjs && node --check clients/dsh-remote/agent/proc.mjs && node --check clients/dsh-remote/agent/events.mjs && node --check clients/dsh-remote/agent/codex.mjs && node --check clients/dsh-remote/agent/gemini.mjs && node --check clients/dsh-remote/agent/opencode.mjs && node --check clients/dsh-remote/agent/agent-e2e-codex.mjs && node --check clients/dsh-remote/agent/agent-e2e-real.mjs && node --check clients/dsh-remote/push-reporter.mjs && node --check clients/dsh-remote/decision-poller.mjs && node --check clients/dsh-remote/approval-coordinator.mjs && node --check clients/dsh-remote/wecom-bot.mjs && node --check clients/dsh-remote/im-format.mjs && node --check clients/dsh-remote/dingtalk-bot.mjs && node --check clients/dsh-remote/feishu-bot.mjs && node --check clients/dsh-remote/dingtalk-auth.mjs && node --check clients/dsh-remote/im-commands.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && node --check packages/relay-router/src/index.mjs && node --check packages/relay-router/src/logger.mjs && bash -n deploy/install-open.sh && grep -q 'command -v setsid' deploy/install-open.sh && grep -q 'command -v ss' deploy/install-open.sh",
|
|
60
60
|
"release:tarballs": "bash scripts/release-tarballs.sh",
|
|
61
61
|
"sync:local": "node scripts/sync-local.mjs"
|
|
62
62
|
},
|
|
@@ -3593,7 +3593,7 @@ window.__ModuleLoader__.load({
|
|
|
3593
3593
|
var imDingIdArr = useState(""); var imDingId = imDingIdArr[0]; var setImDingId = imDingIdArr[1];
|
|
3594
3594
|
var imDingSecretArr = useState(""); var imDingSecret = imDingSecretArr[0]; var setImDingSecret = imDingSecretArr[1];
|
|
3595
3595
|
var imDingRobotArr = useState(""); var imDingRobot = imDingRobotArr[0]; var setImDingRobot = imDingRobotArr[1];
|
|
3596
|
-
// 钉钉扫码一键创建(2026-11-08
|
|
3596
|
+
// 钉钉扫码一键创建(2026-11-08,委托桥侧持久化授权):本地只留等待态/链接/剩余时间;手动配置默认折叠
|
|
3597
3597
|
var imDingAuthArr = useState(null); var imDingAuth = imDingAuthArr[0]; var setImDingAuth = imDingAuthArr[1];
|
|
3598
3598
|
var imDingManualArr = useState(false); var imDingManual = imDingManualArr[0]; var setImDingManual = imDingManualArr[1];
|
|
3599
3599
|
var imFeishuAppIdArr = useState(""); var imFeishuAppId = imFeishuAppIdArr[0]; var setImFeishuAppId = imFeishuAppIdArr[1];
|
|
@@ -6027,6 +6027,16 @@ window.__ModuleLoader__.load({
|
|
|
6027
6027
|
var deps = b && b.status && b.status.deps && b.status.deps.feishu;
|
|
6028
6028
|
if (deps) setImDeps(deps);
|
|
6029
6029
|
loadImDeps();
|
|
6030
|
+
// 授权会话在桥里持久化:重开面板时若仍在等待,恢复展示(URL/剩余时间从状态文件读)
|
|
6031
|
+
api("/dsh-remote/im-dingtalk-auth/status").then(function (a) {
|
|
6032
|
+
if (a && a.state === "waiting") {
|
|
6033
|
+
setImDingAuth({
|
|
6034
|
+
phase: "waiting", url: String(a.url || ""), startedAt: Date.now(),
|
|
6035
|
+
expiresAt: Number(a.expiresAt) || (Date.now() + 15 * 60 * 1000),
|
|
6036
|
+
remainingSec: Number(a.remainingSec) || 0, error: ""
|
|
6037
|
+
});
|
|
6038
|
+
}
|
|
6039
|
+
}).catch(function () { /* 忽略 */ });
|
|
6030
6040
|
});
|
|
6031
6041
|
}
|
|
6032
6042
|
// 数据加载交给下面的 effect(按"弹层是否打开"触发):
|
|
@@ -6745,28 +6755,24 @@ window.__ModuleLoader__.load({
|
|
|
6745
6755
|
return IM_HUMAN_ERR[c] || "操作失败,请稍后重试";
|
|
6746
6756
|
}
|
|
6747
6757
|
|
|
6748
|
-
/**
|
|
6749
|
-
*
|
|
6758
|
+
/** 钉钉扫码授权(2026-11-08 架构修正:委托桥侧持久化授权)。
|
|
6759
|
+
* 面板只做三件事:写 begin 命令 → 轮询状态文件 → 展示。授权会话在桥那边落盘续轮询,
|
|
6760
|
+
* 关面板/宿主重启都不会丢(用户"钉钉说成功、我们没接住"的根因)。 */
|
|
6750
6761
|
function startDingAuth() {
|
|
6751
6762
|
if (imPlatBusy) return;
|
|
6752
6763
|
setImPlatBusy("auth");
|
|
6753
6764
|
setImPlatMsg(null);
|
|
6754
6765
|
setImPlatErr({});
|
|
6755
6766
|
post("/dsh-remote/im-dingtalk-auth/begin", {}).then(function (b) {
|
|
6756
|
-
if (!b || b.ok !== true
|
|
6767
|
+
if (!b || b.ok !== true) {
|
|
6757
6768
|
setImDingAuth(null);
|
|
6758
6769
|
setImPlatMsg({ kind: "err", text: "发起授权失败:" + imHumanErr(b && b.code, b && b.message) });
|
|
6759
6770
|
return;
|
|
6760
6771
|
}
|
|
6761
|
-
|
|
6772
|
+
// begin 只写命令文件;URL/剩余时间要等桥跑完写进状态文件 → 由轮询拿
|
|
6762
6773
|
setImDingAuth({
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
interval: Math.max(1, Math.min(10, Number(b.interval) || 3)),
|
|
6766
|
-
expiresAt: Date.now() + expiresIn * 1000,
|
|
6767
|
-
startedAt: Date.now(),
|
|
6768
|
-
phase: "waiting",
|
|
6769
|
-
error: ""
|
|
6774
|
+
phase: "waiting", url: "", startedAt: Date.now(),
|
|
6775
|
+
expiresAt: Date.now() + 15 * 60 * 1000, remainingSec: 0, error: ""
|
|
6770
6776
|
});
|
|
6771
6777
|
}).catch(function (e) {
|
|
6772
6778
|
setImDingAuth(null);
|
|
@@ -6774,13 +6780,13 @@ window.__ModuleLoader__.load({
|
|
|
6774
6780
|
setImPlatMsg({ kind: "err", text: "发起授权失败:" + imHumanErr(body.code || (e && e.code), body.message || (e && e.message)) });
|
|
6775
6781
|
}).finally(function () { setImPlatBusy(""); });
|
|
6776
6782
|
}
|
|
6777
|
-
/**
|
|
6783
|
+
/** 读一次桥写的授权进度(GET status → 状态文件;不自己 poll 钉钉)。 */
|
|
6778
6784
|
function pollDingAuth(manual) {
|
|
6779
6785
|
var a = imDingAuth;
|
|
6780
|
-
if (!a ||
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
if (
|
|
6786
|
+
if (!a || a.phase !== "waiting") return;
|
|
6787
|
+
api("/dsh-remote/im-dingtalk-auth/status").then(function (b) {
|
|
6788
|
+
var st2 = String((b && b.state) || "idle");
|
|
6789
|
+
if (st2 === "success") {
|
|
6784
6790
|
setImDingAuth(null); // 成功后自动收起授权块
|
|
6785
6791
|
setImPlatMsg({
|
|
6786
6792
|
kind: "ok",
|
|
@@ -6789,31 +6795,49 @@ window.__ModuleLoader__.load({
|
|
|
6789
6795
|
loadImBots();
|
|
6790
6796
|
return;
|
|
6791
6797
|
}
|
|
6792
|
-
if (
|
|
6793
|
-
setImDingAuth({
|
|
6798
|
+
if (st2 === "failed" || st2 === "expired") {
|
|
6799
|
+
setImDingAuth({
|
|
6800
|
+
phase: "failed", url: a.url, startedAt: a.startedAt, expiresAt: a.expiresAt,
|
|
6801
|
+
remainingSec: 0, error: imHumanErr(st2, b && b.message)
|
|
6802
|
+
});
|
|
6794
6803
|
return;
|
|
6795
6804
|
}
|
|
6796
|
-
// waiting
|
|
6797
|
-
setImDingAuth({
|
|
6805
|
+
// waiting / idle(桥还没开始):刷新 url 与剩余时间,继续等
|
|
6806
|
+
setImDingAuth({
|
|
6807
|
+
phase: "waiting",
|
|
6808
|
+
url: String((b && b.url) || a.url || ""),
|
|
6809
|
+
startedAt: a.startedAt || Date.now(),
|
|
6810
|
+
expiresAt: Number(b && b.expiresAt) || a.expiresAt || (Date.now() + 15 * 60 * 1000),
|
|
6811
|
+
remainingSec: Number(b && b.remainingSec) || 0,
|
|
6812
|
+
error: "", polledAt: Date.now()
|
|
6813
|
+
});
|
|
6798
6814
|
}).catch(function (e) {
|
|
6799
|
-
// 网络抖动:不打断等待(手动按钮仍在),仅在自动轮询里静默
|
|
6800
6815
|
if (manual) {
|
|
6801
6816
|
var body = (e && e.body) || {};
|
|
6802
6817
|
setImPlatMsg({ kind: "warn", text: "检查失败:" + imHumanErr(body.code, body.message || (e && e.message)) + "(可再试)" });
|
|
6803
6818
|
}
|
|
6804
6819
|
});
|
|
6805
6820
|
}
|
|
6806
|
-
/**
|
|
6821
|
+
/** 等待确认中:每 2s 读一次状态(桥后台在推进);**最多自动看 15 分钟后停止**,手动按钮仍在。 */
|
|
6807
6822
|
useEffect(function () {
|
|
6808
6823
|
if (iaModal !== "channel" || iaChStep !== "dingtalk") return undefined;
|
|
6809
6824
|
if (!imDingAuth || imDingAuth.phase !== "waiting") return undefined;
|
|
6810
6825
|
var waited = Date.now() - (imDingAuth.startedAt || Date.now());
|
|
6811
|
-
if (waited >
|
|
6826
|
+
if (waited > 15 * 60 * 1000) return undefined;
|
|
6812
6827
|
var t = null;
|
|
6813
|
-
try { t = setTimeout(function () { pollDingAuth(false); },
|
|
6828
|
+
try { t = setTimeout(function () { pollDingAuth(false); }, 2000); } catch (e) { /* 忽略 */ }
|
|
6814
6829
|
return function () { try { if (t) clearTimeout(t); } catch (e) { /* 忽略 */ } };
|
|
6815
6830
|
}, [iaModal, iaChStep, imDingAuth]);
|
|
6816
6831
|
|
|
6832
|
+
/** 取消授权:写 cancel 命令(桥停轮询 + 删持久化会话)。 */
|
|
6833
|
+
function cancelDingAuth() {
|
|
6834
|
+
post("/dsh-remote/im-dingtalk-auth/cancel", {}).catch(function () { /* 桥不在也不影响面板收起 */ })
|
|
6835
|
+
.finally(function () {
|
|
6836
|
+
setImDingAuth(null);
|
|
6837
|
+
setImPlatMsg({ kind: "ok", text: "已取消本次扫码授权" });
|
|
6838
|
+
});
|
|
6839
|
+
}
|
|
6840
|
+
|
|
6817
6841
|
/** 钉钉手动创建 7 步(2026-11-08 业主给的官方链接;每步一句 + 链接)。 */
|
|
6818
6842
|
var DINGTALK_STEPS = [
|
|
6819
6843
|
{ t: "用钉钉账号登录开发者后台", url: "https://open-dev.dingtalk.com/" },
|
|
@@ -6849,6 +6873,7 @@ window.__ModuleLoader__.load({
|
|
|
6849
6873
|
var st = (imBots && imBots.status && imBots.status.dingtalk) || {};
|
|
6850
6874
|
var err = imPlatErr || {};
|
|
6851
6875
|
var auth = imDingAuth;
|
|
6876
|
+
var authStatus = st.auth || {};
|
|
6852
6877
|
var sourceText = (st.credentialSource || d.credentialSource) === "scan" ? "扫码创建"
|
|
6853
6878
|
: (st.credentialSource || d.credentialSource) === "manual" ? "手动填写" : "";
|
|
6854
6879
|
var clientTail = String(d.clientId || "").length > 4 ? String(d.clientId).slice(-4) : String(d.clientId || "");
|
|
@@ -6881,6 +6906,13 @@ window.__ModuleLoader__.load({
|
|
|
6881
6906
|
// · 有凭据/已连接 → 「已连接 · Client ID 尾号 · 来源:…」
|
|
6882
6907
|
(function () {
|
|
6883
6908
|
var configured = !!d.enabled || !!d.hasSecret || !!String(d.credentialSource || "").trim();
|
|
6909
|
+
// 授权会话状态优先:failed/expired 是"授权"失败,不是"连接"失败(措辞不能混)
|
|
6910
|
+
if (authStatus.state === "failed") {
|
|
6911
|
+
return h("div", { className: "dru-ia-state is-err", role: "status" }, "授权失败:" + imHumanErr("", String(authStatus.message || "")));
|
|
6912
|
+
}
|
|
6913
|
+
if (authStatus.state === "expired") {
|
|
6914
|
+
return h("div", { className: "dru-ia-state is-warn", role: "status" }, "授权已过期:" + imHumanErr("expired", String(authStatus.message || "")) + "(点下方按钮可重新获取链接)");
|
|
6915
|
+
}
|
|
6884
6916
|
var failState = st.state === "failed" || st.state === "error";
|
|
6885
6917
|
if (!configured) return h("div", { className: "dru-ia-state" }, druIcon("chat"), "未配置:点下方按钮扫码创建机器人");
|
|
6886
6918
|
if (failState) {
|
|
@@ -6892,38 +6924,43 @@ window.__ModuleLoader__.load({
|
|
|
6892
6924
|
if (st.connected) return h("div", { className: "dru-ia-state is-ok" }, druIcon("check"), "已连接" + tailBit + srcBit);
|
|
6893
6925
|
return h("div", { className: "dru-ia-state" }, druIcon("chat"), "已保存凭据,等待连接…" + tailBit + srcBit);
|
|
6894
6926
|
})(),
|
|
6895
|
-
//
|
|
6927
|
+
// 扫码授权中(桥侧持久化:begin 只写命令,URL 等桥跑完写进状态 → 轮询拿)
|
|
6896
6928
|
(auth && auth.phase === "waiting")
|
|
6897
6929
|
? h("div", { className: "dru-ia-field" },
|
|
6898
6930
|
h("div", { className: "dru-ia-state", role: "status" }, h("span", { className: "dru-spin" }),
|
|
6899
|
-
|
|
6931
|
+
!auth.url
|
|
6932
|
+
? "正在生成授权链接…(桥正在创建,约需几秒)"
|
|
6933
|
+
: (auth.remainingSec > 0
|
|
6934
|
+
? "等待你在钉钉里确认…(剩余约 " + Math.max(1, Math.round(auth.remainingSec / 60)) + " 分钟;用手机钉钉扫码或打开链接)"
|
|
6935
|
+
: "等待你在钉钉里确认…(用手机钉钉扫码,或复制链接到手机打开)")),
|
|
6936
|
+
h("div", { className: "dru-ia-hint" }, "关掉面板也不影响:桥会继续等你在钉钉里确认,回来点「我已完成授权,检查一下」即可。"),
|
|
6900
6937
|
(function () {
|
|
6901
6938
|
var waitedMin = Math.floor((Date.now() - (auth.startedAt || Date.now())) / 60000);
|
|
6902
|
-
return
|
|
6903
|
-
waitedMin
|
|
6904
|
-
|
|
6905
|
-
: "授权链接 2 小时内有效;关掉面板也不影响,回来点「我已完成授权,检查一下」即可。");
|
|
6939
|
+
return waitedMin >= 15
|
|
6940
|
+
? h("div", { className: "dru-ia-hint" }, "已等待 " + waitedMin + " 分钟:自动刷新先停下(桥仍在继续),完成确认后点「我已完成授权,检查一下」。")
|
|
6941
|
+
: null;
|
|
6906
6942
|
})(),
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6943
|
+
auth.url
|
|
6944
|
+
? h("div", { className: "dru-copy-row" },
|
|
6945
|
+
h("div", { className: "dru-copy-box" },
|
|
6946
|
+
h("span", { className: "dru-link-val" }, auth.url),
|
|
6947
|
+
h("button", {
|
|
6948
|
+
type: "button", className: "dru-copy-btn", "aria-label": "复制授权链接",
|
|
6949
|
+
onClick: function () {
|
|
6950
|
+
copyText(auth.url).then(function (ok) { if (!ok) setImPlatMsg({ kind: "err", text: "复制失败:请手动选中链接复制" }); });
|
|
6951
|
+
}
|
|
6952
|
+
}, "复制授权链接")),
|
|
6953
|
+
h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
6954
|
+
h("a", { className: "dru-ia-btn is-primary", href: auth.url, target: "_blank", rel: "noopener" }, "在浏览器打开授权页"),
|
|
6955
|
+
h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: function () { pollDingAuth(true); } }, "我已完成授权,检查一下"),
|
|
6956
|
+
h("button", { type: "button", className: "dru-ia-btn", onClick: cancelDingAuth }, "取消授权")))
|
|
6957
|
+
: h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
6958
|
+
h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: function () { pollDingAuth(true); } }, "我已完成授权,检查一下"),
|
|
6959
|
+
h("button", { type: "button", className: "dru-ia-btn", onClick: cancelDingAuth }, "取消授权")))
|
|
6923
6960
|
: null,
|
|
6924
6961
|
(auth && auth.phase === "failed")
|
|
6925
6962
|
? h("div", { className: "dru-ia-field" },
|
|
6926
|
-
h("div", { className: "dru-ia-state is-err", role: "alert" }, auth.error || "
|
|
6963
|
+
h("div", { className: "dru-ia-state is-err", role: "alert" }, "授权失败:" + (imHumanErr("", auth.error) || "请重试")),
|
|
6927
6964
|
h("button", { type: "button", className: "dru-ia-btn", onClick: function () { startDingAuth(); } }, "重试扫码授权"))
|
|
6928
6965
|
: null,
|
|
6929
6966
|
// 能力:主动推送 / 卡片(缺 Robot Code 时置灰,提示补 Robot Code)
|