@mrrisega/dsh-remote 0.7.12-beta.2 → 0.7.12-beta.3
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-bot.mjs +49 -6
- package/clients/dsh-remote/docs/feishu-bot-contract.md +56 -16
- package/clients/dsh-remote/dsh-bridge.mjs +6 -4
- package/clients/dsh-remote/feishu-bot.mjs +197 -13
- package/clients/dsh-remote/test/dingtalk-bot.test.mjs +81 -1
- package/clients/dsh-remote/test/feishu-bot.test.mjs +260 -10
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +237 -69
- package/packages/dsh-remote-web/lib/index.js +215 -7
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dingtalk-bot.mjs +1318 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/docs/feishu-bot-contract.md +177 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +404 -90
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/feishu-bot.mjs +1668 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mirror-diag-script.js +53 -14
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +355 -40
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +1 -1
- package/packages/dsh-remote-web/test/announcements-proxy.test.mjs +14 -1
- package/packages/dsh-remote-web/test/im-bots.test.mjs +157 -5
- package/packages/dsh-remote-web/test/panel-ia.test.mjs +261 -132
|
@@ -6529,6 +6529,15 @@ const TELEMETRY_FAIL_CODES = new Set([
|
|
|
6529
6529
|
"npx_cmd_unavailable", "registry_timeout", "install_script_missing", "npx_exit_nonzero", "npx_output_encoding", "update_stalled",
|
|
6530
6530
|
"unknown",
|
|
6531
6531
|
]);
|
|
6532
|
+
/**
|
|
6533
|
+
* mirror_load 的 outcome 枚举(2026-11-08,与服务端 TELEMETRY_OUTCOME 逐字对齐):
|
|
6534
|
+
* ok = 就绪(真实活跃耗时)
|
|
6535
|
+
* slow = 20s 未就绪(删失样本)
|
|
6536
|
+
* suspended = 页面被系统挂起/冻结(旧客户端不会发,新客户端也暂不发;服务端按 hidden/active 派生)
|
|
6537
|
+
* abandoned = 用户关页/切走时仍未就绪
|
|
6538
|
+
* 缺省/未知一律发 ""(未知),**不再默认谎报 ok** —— 否则旧客户端的挂起样本会被记成正常。
|
|
6539
|
+
*/
|
|
6540
|
+
const TELEMETRY_OUTCOMES = new Set(["ok", "slow", "suspended", "abandoned"]);
|
|
6532
6541
|
/** node 半只发 source=plugin。 */
|
|
6533
6542
|
const TELEMETRY_SOURCE = "plugin";
|
|
6534
6543
|
const TELEMETRY_INSTALL_ID_FILE = ".telemetry-install-id";
|
|
@@ -6849,7 +6858,18 @@ function telemetryEventOf(name, extra = {}) {
|
|
|
6849
6858
|
if (name === "mirror_load") {
|
|
6850
6859
|
if (Number.isFinite(Number(extra.at)) && Number(extra.at) > 0) ev.at = Math.round(Number(extra.at));
|
|
6851
6860
|
if (Number.isFinite(Number(extra.ms))) ev.ms = Math.max(0, Math.min(600000, Math.round(Number(extra.ms))));
|
|
6852
|
-
|
|
6861
|
+
// 2026-11-08:outcome 四枚举透传;active_ms/hidden_ms/frozen_ms 是"活跃加载时间"口径。
|
|
6862
|
+
// 旧客户端不带这些字段 → outcome=""、数值字段缺省;旧服务端忽略未知字段,零破坏。
|
|
6863
|
+
const oc = String(extra.outcome || "");
|
|
6864
|
+
ev.outcome = TELEMETRY_OUTCOMES.has(oc) ? oc : "";
|
|
6865
|
+
for (const k of ["active_ms", "hidden_ms", "frozen_ms"]) {
|
|
6866
|
+
const v = Number(extra[k]);
|
|
6867
|
+
if (Number.isFinite(v) && v >= 0) ev[k] = Math.max(0, Math.min(600000, Math.round(v)));
|
|
6868
|
+
}
|
|
6869
|
+
// 防御:活跃耗时不可能超过墙钟耗时(取整/时钟差可能造出 1ms 级倒挂)
|
|
6870
|
+
if (Number.isFinite(Number(ev.active_ms)) && Number.isFinite(Number(ev.ms))) {
|
|
6871
|
+
ev.active_ms = Math.min(ev.active_ms, ev.ms);
|
|
6872
|
+
}
|
|
6853
6873
|
}
|
|
6854
6874
|
return ev;
|
|
6855
6875
|
}
|
|
@@ -7767,6 +7787,7 @@ function imBotsView(relayDir) {
|
|
|
7767
7787
|
allowFrom: dingtalkCfgOf(bots).allowFrom,
|
|
7768
7788
|
cardTemplateId: dingtalkCfgOf(bots).cardTemplateId,
|
|
7769
7789
|
streamUrl: dingtalkCfgOf(bots).streamUrl,
|
|
7790
|
+
credentialSource: dingtalkCfgOf(bots).credentialSource,
|
|
7770
7791
|
},
|
|
7771
7792
|
feishu: {
|
|
7772
7793
|
enabled: feishuCfgOf(bots).enabled,
|
|
@@ -7904,6 +7925,8 @@ function dingtalkCfgOf(bots) {
|
|
|
7904
7925
|
allowFrom: strListOf(d.allowFrom),
|
|
7905
7926
|
cardTemplateId: strOf(d.cardTemplateId),
|
|
7906
7927
|
streamUrl: strOf(d.streamUrl),
|
|
7928
|
+
// 凭据来源:scan=扫码一键创建;manual=用户手填(2026-11-08 面板显示用,非敏感)
|
|
7929
|
+
credentialSource: strOf(d.credentialSource),
|
|
7907
7930
|
};
|
|
7908
7931
|
}
|
|
7909
7932
|
|
|
@@ -7941,6 +7964,18 @@ function imPlatformStatusOf(status, platform) {
|
|
|
7941
7964
|
authenticated: !!p.authenticated,
|
|
7942
7965
|
lastError: strOf(p.lastError).slice(0, 500),
|
|
7943
7966
|
boundChatId: strOf(p.boundChatId),
|
|
7967
|
+
// 飞书"接收群/私聊自动发现"(2026-11-08 业主:用户不需要填接收人)——
|
|
7968
|
+
// boundChatType:p2p/group;receiveIdSource:auto/manual/config(桥侧口径,只透传字符串)
|
|
7969
|
+
boundChatType: strOf(p.boundChatType).slice(0, 40),
|
|
7970
|
+
receiveIdSource: strOf(p.receiveIdSource).slice(0, 40),
|
|
7971
|
+
// 钉钉能力位(2026-11-08 桥侧新增):凭据来源 / 是否已配置 Robot Code / 能力开关。
|
|
7972
|
+
// 面板据 robotCodeConfigured 把「主动推送/卡片」置灰(缺 Robot Code 时能力不可用)。
|
|
7973
|
+
credentialSource: strOf(p.credentialSource).slice(0, 20),
|
|
7974
|
+
robotCodeConfigured: !!p.robotCodeConfigured,
|
|
7975
|
+
capabilities: (() => {
|
|
7976
|
+
const c = p.capabilities && typeof p.capabilities === "object" ? p.capabilities : null;
|
|
7977
|
+
return c ? { openapi: !!c.openapi, card: !!c.card } : null;
|
|
7978
|
+
})(),
|
|
7944
7979
|
tier: (t === "free" || t === "pro" || t === "pro_max") ? t : "",
|
|
7945
7980
|
caps: strListOf(p.caps).map((x) => x.slice(0, 80)),
|
|
7946
7981
|
capsCount: numOf(p.capsCount),
|
|
@@ -8060,6 +8095,145 @@ function startFeishuInstall(relayDir, opts = {}) {
|
|
|
8060
8095
|
return { ok: true, started: true, taskId, deps: { feishu: feishuDepsInfo(relayDir) } };
|
|
8061
8096
|
}
|
|
8062
8097
|
|
|
8098
|
+
// ── 钉钉「扫码一键创建机器人」(2026-11-08,官方 connector 同款 device-flow)─────────────
|
|
8099
|
+
// 流程(服务端代跑,device_code 只存内存、不下发浏览器):
|
|
8100
|
+
// POST .../app/registration/init {source:"DING_DWS_CLAW"} → {nonce}
|
|
8101
|
+
// POST .../app/registration/begin {nonce} → {device_code, verification_uri_complete, interval, expires_in}
|
|
8102
|
+
// POST .../app/registration/poll {device_code} → {status:"WAITING"|"SUCCESS", client_id, client_secret}
|
|
8103
|
+
// 安全:浏览器只拿 authId + 授权链接;clientSecret 只在 success 时直接写进 0600 的 im-bots.json,绝不回显。
|
|
8104
|
+
const DINGTALK_REG_BASE = "https://oapi.dingtalk.com/app/registration";
|
|
8105
|
+
const DINGTALK_AUTH_TTL_MS = 2 * 60 * 1000; // 面板最长轮询 2 分钟
|
|
8106
|
+
const dingtalkAuthSessions = new Map(); // authId → {relayDir, deviceCode, url, interval, createdAt, expiresAt}
|
|
8107
|
+
const dingtalkAuthSuperseded = new Map(); // 被新授权作废的 authId → at(查询时回 cancelled)
|
|
8108
|
+
|
|
8109
|
+
function jsonPostForAuth(fetchImpl, url, payload) {
|
|
8110
|
+
return fetchImpl(url, {
|
|
8111
|
+
method: "POST",
|
|
8112
|
+
headers: { "content-type": "application/json" },
|
|
8113
|
+
body: JSON.stringify(payload || {}),
|
|
8114
|
+
signal: AbortSignal.timeout(8000),
|
|
8115
|
+
}).then(async (r) => {
|
|
8116
|
+
let body = null;
|
|
8117
|
+
try { body = await r.json(); } catch { body = null; }
|
|
8118
|
+
return { ok: !!r.ok, status: r.status, body: body || {} };
|
|
8119
|
+
});
|
|
8120
|
+
}
|
|
8121
|
+
|
|
8122
|
+
/**
|
|
8123
|
+
* 开始一次钉钉扫码授权(POST /dsh-remote/im-dingtalk-auth/begin)。
|
|
8124
|
+
* 同一 relayDir 同时只保留一个会话:begin 时把旧的作废(旧 authId 后续 poll 只会得到 expired)。
|
|
8125
|
+
*/
|
|
8126
|
+
async function beginDingtalkAuth(relayDir, opts = {}) {
|
|
8127
|
+
const fetchImpl = typeof opts.imAuthFetch === "function" ? opts.imAuthFetch : fetch;
|
|
8128
|
+
// 作废旧会话(同一时间只允许一个进行中的授权;旧 authId 后续查询回 cancelled)
|
|
8129
|
+
for (const [id, sess] of dingtalkAuthSessions) {
|
|
8130
|
+
if (sess.relayDir === relayDir) {
|
|
8131
|
+
dingtalkAuthSessions.delete(id);
|
|
8132
|
+
dingtalkAuthSuperseded.set(id, Date.now());
|
|
8133
|
+
}
|
|
8134
|
+
}
|
|
8135
|
+
// 有界:只留最近 20 个作废标记,避免长期运行内存增长
|
|
8136
|
+
while (dingtalkAuthSuperseded.size > 20) {
|
|
8137
|
+
const first = dingtalkAuthSuperseded.keys().next().value;
|
|
8138
|
+
dingtalkAuthSuperseded.delete(first);
|
|
8139
|
+
}
|
|
8140
|
+
let init;
|
|
8141
|
+
try {
|
|
8142
|
+
init = await jsonPostForAuth(fetchImpl, `${DINGTALK_REG_BASE}/init`, { source: "DING_DWS_CLAW" });
|
|
8143
|
+
} catch (e) {
|
|
8144
|
+
return { ok: false, code: "network_error", message: `网络不通:无法连接钉钉接口(${e?.message || e})` };
|
|
8145
|
+
}
|
|
8146
|
+
const nonce = init.ok && init.body && typeof init.body.nonce === "string" ? init.body.nonce : "";
|
|
8147
|
+
if (!nonce) {
|
|
8148
|
+
return { ok: false, code: "dingtalk_api_rejected", message: "钉钉接口拒绝了初始化请求(请稍后重试)" };
|
|
8149
|
+
}
|
|
8150
|
+
let begin;
|
|
8151
|
+
try {
|
|
8152
|
+
begin = await jsonPostForAuth(fetchImpl, `${DINGTALK_REG_BASE}/begin`, { nonce });
|
|
8153
|
+
} catch (e) {
|
|
8154
|
+
return { ok: false, code: "network_error", message: `网络不通:无法连接钉钉接口(${e?.message || e})` };
|
|
8155
|
+
}
|
|
8156
|
+
const b = begin.body || {};
|
|
8157
|
+
const deviceCode = typeof b.device_code === "string" ? b.device_code : "";
|
|
8158
|
+
const url = typeof b.verification_uri_complete === "string" ? b.verification_uri_complete : "";
|
|
8159
|
+
if (!begin.ok || !deviceCode || !url) {
|
|
8160
|
+
return { ok: false, code: "dingtalk_api_rejected", message: "钉钉接口没有返回授权链接(请稍后重试)" };
|
|
8161
|
+
}
|
|
8162
|
+
const authId = `ding-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8163
|
+
const interval = Math.max(1, Math.min(10, Number(b.interval) || 2));
|
|
8164
|
+
const expiresIn = Math.max(10, Math.min(600, Number(b.expires_in) || 120));
|
|
8165
|
+
const now = Date.now();
|
|
8166
|
+
dingtalkAuthSessions.set(authId, {
|
|
8167
|
+
relayDir, deviceCode, url, interval,
|
|
8168
|
+
createdAt: now, expiresAt: Math.min(now + expiresIn * 1000, now + DINGTALK_AUTH_TTL_MS),
|
|
8169
|
+
});
|
|
8170
|
+
return { ok: true, authId, url, interval, expiresIn };
|
|
8171
|
+
}
|
|
8172
|
+
|
|
8173
|
+
/** 把扫码拿到的凭据写进 im-bots.json 的 dingtalk 段(0600 原子写;保留其它字段)。 */
|
|
8174
|
+
function saveDingtalkScannedCredentials(relayDir, clientId, clientSecret) {
|
|
8175
|
+
const bots = readImBots(relayDir);
|
|
8176
|
+
const cur = dingtalkCfgOf(bots);
|
|
8177
|
+
const next = {
|
|
8178
|
+
...cur,
|
|
8179
|
+
enabled: true,
|
|
8180
|
+
clientId: String(clientId || "").trim(),
|
|
8181
|
+
clientSecret: String(clientSecret || "").trim(),
|
|
8182
|
+
// 扫码流程只给 client_id/client_secret;robotCode 由桥连上后自动补齐/发现
|
|
8183
|
+
credentialSource: "scan",
|
|
8184
|
+
};
|
|
8185
|
+
try {
|
|
8186
|
+
writeImBots(relayDir, { ...bots, version: 1, dingtalk: next });
|
|
8187
|
+
return { ok: true };
|
|
8188
|
+
} catch (e) {
|
|
8189
|
+
return { ok: false, code: "write_failed", message: `保存扫码凭据失败:${e?.message || e}` };
|
|
8190
|
+
}
|
|
8191
|
+
}
|
|
8192
|
+
|
|
8193
|
+
/**
|
|
8194
|
+
* 查询一次扫码授权状态(GET /dsh-remote/im-dingtalk-auth/status?id=...)。
|
|
8195
|
+
* 返回 status: waiting | success | failed | expired;success 时**已**把凭据写盘,只回脱敏信息。
|
|
8196
|
+
*/
|
|
8197
|
+
async function dingtalkAuthStatus(authId, opts = {}) {
|
|
8198
|
+
const key = String(authId || "");
|
|
8199
|
+
const sess = dingtalkAuthSessions.get(key);
|
|
8200
|
+
if (!sess) {
|
|
8201
|
+
if (dingtalkAuthSuperseded.has(key)) {
|
|
8202
|
+
dingtalkAuthSuperseded.delete(key);
|
|
8203
|
+
return { ok: false, status: "failed", code: "cancelled", message: "已开始新的授权,这次已取消" };
|
|
8204
|
+
}
|
|
8205
|
+
return { ok: false, status: "expired", code: "expired", message: "授权会话不存在或已超时,请重新发起" };
|
|
8206
|
+
}
|
|
8207
|
+
const now = Date.now();
|
|
8208
|
+
if (now > sess.expiresAt) {
|
|
8209
|
+
dingtalkAuthSessions.delete(String(authId));
|
|
8210
|
+
return { ok: false, status: "expired", code: "expired", message: "授权已超时(2 分钟内未确认),请重新发起" };
|
|
8211
|
+
}
|
|
8212
|
+
const fetchImpl = typeof opts.imAuthFetch === "function" ? opts.imAuthFetch : fetch;
|
|
8213
|
+
let r;
|
|
8214
|
+
try {
|
|
8215
|
+
r = await jsonPostForAuth(fetchImpl, `${DINGTALK_REG_BASE}/poll`, { device_code: sess.deviceCode });
|
|
8216
|
+
} catch (e) {
|
|
8217
|
+
return { ok: false, status: "failed", code: "network_error", message: `网络不通:查询授权状态失败(${e?.message || e})` };
|
|
8218
|
+
}
|
|
8219
|
+
const b = r.body || {};
|
|
8220
|
+
const st = String(b.status || "").toUpperCase();
|
|
8221
|
+
if (st === "WAITING" || st === "PENDING") return { ok: true, status: "waiting" };
|
|
8222
|
+
if (st === "SUCCESS" || (b.client_id && b.client_secret)) {
|
|
8223
|
+
const saved = saveDingtalkScannedCredentials(sess.relayDir, b.client_id, b.client_secret);
|
|
8224
|
+
dingtalkAuthSessions.delete(String(authId));
|
|
8225
|
+
if (!saved.ok) return { ok: false, status: "failed", code: saved.code, message: saved.message };
|
|
8226
|
+
const cid = String(b.client_id || "");
|
|
8227
|
+
return {
|
|
8228
|
+
ok: true, status: "success", credentialSource: "scan",
|
|
8229
|
+
clientIdTail: cid.length > 4 ? cid.slice(-4) : cid,
|
|
8230
|
+
hasSecret: true
|
|
8231
|
+
};
|
|
8232
|
+
}
|
|
8233
|
+
// 钉钉明确拒绝(创建被取消/参数过期等)
|
|
8234
|
+
return { ok: false, status: "failed", code: "dingtalk_api_rejected", message: String(b.message || b.errmsg || "钉钉授权被拒绝,请重试") };
|
|
8235
|
+
}
|
|
8236
|
+
|
|
8063
8237
|
/** 保存 dingtalk 配置(空 secret = 不修改;凭据完整性在返回里说清)。 */
|
|
8064
8238
|
function saveDingtalkConfig(relayDir, input) {
|
|
8065
8239
|
const src = input && typeof input === "object" ? input : {};
|
|
@@ -8078,6 +8252,10 @@ function saveDingtalkConfig(relayDir, input) {
|
|
|
8078
8252
|
if (Object.prototype.hasOwnProperty.call(src, "allowFrom")) next.allowFrom = strListOf(src.allowFrom);
|
|
8079
8253
|
if (Object.prototype.hasOwnProperty.call(src, "enabled")) next.enabled = !!src.enabled;
|
|
8080
8254
|
else if (!next.enabled && next.clientId && next.clientSecret && next.robotCode) next.enabled = true;
|
|
8255
|
+
// 凭据来源标记(面板显示「扫码创建 / 手动填写」):手填过凭据就记 manual;扫码流程会写 scan。
|
|
8256
|
+
if (Object.prototype.hasOwnProperty.call(src, "clientId") || Object.prototype.hasOwnProperty.call(src, "clientSecret")) {
|
|
8257
|
+
next.credentialSource = "manual";
|
|
8258
|
+
}
|
|
8081
8259
|
if (!next.clientId) return { ok: false, code: "missing_client_id", message: "请填写 Client ID(= 钉钉应用的 AppKey)" };
|
|
8082
8260
|
if (!next.clientSecret) return { ok: false, code: "missing_client_secret", message: "请填写 Client Secret(= 钉钉应用的 AppSecret,只保存在本机)" };
|
|
8083
8261
|
if (!next.robotCode) return { ok: false, code: "missing_robot_code", message: "请填写 Robot Code(机器人编码,在钉钉开放平台机器人页复制)" };
|
|
@@ -8116,7 +8294,8 @@ function saveFeishuConfig(relayDir, input) {
|
|
|
8116
8294
|
else if (!next.enabled && next.appId && next.appSecret && next.receiveId) next.enabled = true;
|
|
8117
8295
|
if (!next.appId) return { ok: false, code: "missing_app_id", message: "请填写 App ID(飞书开放平台应用的 App ID)" };
|
|
8118
8296
|
if (!next.appSecret) return { ok: false, code: "missing_app_secret", message: "请填写 App Secret(只保存在本机,不会回显)" };
|
|
8119
|
-
|
|
8297
|
+
// 接收人**不是必填**(2026-11-08 业主实测:App ID+Secret 连上后桥会自己发现你的群/私聊)。
|
|
8298
|
+
// 保留 receiveId/receiveIdType 字段是为了兼容桥侧手工配置/后续高级用法;面板不再要求填写。
|
|
8120
8299
|
const out = { ...bots, version: 1, feishu: next };
|
|
8121
8300
|
try {
|
|
8122
8301
|
writeImBots(relayDir, out);
|
|
@@ -8367,8 +8546,10 @@ function registerRoutes(ctx, relayDir, opts = {}) {
|
|
|
8367
8546
|
},
|
|
8368
8547
|
},
|
|
8369
8548
|
// 镜像页就绪耗时(D1 在 clients/dsh-remote/mobile-adapter.mjs 里量,经此入队):
|
|
8370
|
-
// body = { ms: 耗时毫秒, at: 事件时刻 epoch ms, outcome: "ok"|"slow"
|
|
8371
|
-
//
|
|
8549
|
+
// body = { ms: 耗时毫秒, at: 事件时刻 epoch ms, outcome: "ok"|"slow"|"suspended"|"abandoned",
|
|
8550
|
+
// active_ms?, hidden_ms?, frozen_ms? }
|
|
8551
|
+
// 契约(926 迭代 §2 + 2026-11-08 活跃时间口径):事件名固定 mirror_load,ms 钳制 [0, 600000],
|
|
8552
|
+
// outcome 只认四枚举(非法/缺省发 "",**不再默认 ok**),三个时长字段可选、越界丢弃。
|
|
8372
8553
|
//
|
|
8373
8554
|
// 🔒 入参校验失败**静默丢弃**但仍回 200 {ok:true}:这条上报是手机镜像页加载路径上的
|
|
8374
8555
|
// fire-and-forget,任何 4xx 都可能被页面当成"功能坏了"而冒泡给用户;统计丢一条无所谓,
|
|
@@ -8386,11 +8567,17 @@ function registerRoutes(ctx, relayDir, opts = {}) {
|
|
|
8386
8567
|
if (body && Number.isFinite(ms) && ms >= 0 && ms <= 600000) {
|
|
8387
8568
|
const atRaw = Number(body.at);
|
|
8388
8569
|
const at = Number.isFinite(atRaw) && atRaw > 0 ? Math.round(atRaw) : 0;
|
|
8389
|
-
|
|
8570
|
+
const oc = String(body.outcome || "");
|
|
8571
|
+
const rec = {
|
|
8390
8572
|
ms: Math.round(ms),
|
|
8391
8573
|
at,
|
|
8392
|
-
outcome:
|
|
8393
|
-
}
|
|
8574
|
+
outcome: TELEMETRY_OUTCOMES.has(oc) ? oc : "",
|
|
8575
|
+
};
|
|
8576
|
+
for (const k of ["active_ms", "hidden_ms", "frozen_ms"]) {
|
|
8577
|
+
const v = Number(body[k]);
|
|
8578
|
+
if (Number.isFinite(v) && v >= 0) rec[k] = Math.max(0, Math.min(600000, Math.round(v)));
|
|
8579
|
+
}
|
|
8580
|
+
telemetryRecord(relayDir, "mirror_load", rec);
|
|
8394
8581
|
}
|
|
8395
8582
|
} catch { /* 体读不到/超限:静默 */ }
|
|
8396
8583
|
sendJson(res, 200, { ok: true });
|
|
@@ -9040,6 +9227,25 @@ function registerRoutes(ctx, relayDir, opts = {}) {
|
|
|
9040
9227
|
sendJson(res, 200, imBotsView(relayDir));
|
|
9041
9228
|
},
|
|
9042
9229
|
},
|
|
9230
|
+
{
|
|
9231
|
+
// 钉钉扫码一键创建机器人:开始授权(返回授权链接;device_code 只存服务端内存)
|
|
9232
|
+
method: "POST",
|
|
9233
|
+
path: "/dsh-remote/im-dingtalk-auth/begin",
|
|
9234
|
+
handler: async (_req, res) => {
|
|
9235
|
+
const r = await beginDingtalkAuth(relayDir, { imAuthFetch: opts.imAuthFetch });
|
|
9236
|
+
sendJson(res, r.ok ? 200 : 502, r);
|
|
9237
|
+
},
|
|
9238
|
+
},
|
|
9239
|
+
{
|
|
9240
|
+
// 轮询授权状态:waiting / success(已写盘,只回脱敏结果)/ failed / expired
|
|
9241
|
+
method: "GET",
|
|
9242
|
+
path: "/dsh-remote/im-dingtalk-auth/status",
|
|
9243
|
+
handler: async (req, res) => {
|
|
9244
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
9245
|
+
const r = await dingtalkAuthStatus(url.searchParams.get("id") || "", { imAuthFetch: opts.imAuthFetch });
|
|
9246
|
+
sendJson(res, 200, r);
|
|
9247
|
+
},
|
|
9248
|
+
},
|
|
9043
9249
|
{
|
|
9044
9250
|
// 飞书 SDK「按需下载」现状(面板据此显示未装/已装/下载中/失败)。
|
|
9045
9251
|
method: "GET",
|
|
@@ -9428,6 +9634,8 @@ export function apply(ctx, config = {}) {
|
|
|
9428
9634
|
imBotsWaitMs: Number(config.imBotsWaitMs) || undefined,
|
|
9429
9635
|
// 测试钩子:飞书 SDK 安装的 npm spawn(用例注入假实现,**绝不真的联网装包**)。
|
|
9430
9636
|
imDepsSpawn: typeof config.imDepsSpawn === "function" ? config.imDepsSpawn : undefined,
|
|
9637
|
+
// 测试钩子:钉钉扫码授权的 fetch(用例注入假实现,**绝不真的打钉钉接口**)。
|
|
9638
|
+
imAuthFetch: typeof config.imAuthFetch === "function" ? config.imAuthFetch : undefined,
|
|
9431
9639
|
}), "dsh-remote-web: /dsh-remote routes");
|
|
9432
9640
|
// 0.1.2-rc.1+ 浏览器会话代持:换取 Harness 会话 Cookie 供 bridge 上游携带(手机点设备不再 401 白页)
|
|
9433
9641
|
ctx.effect(() => scheduleHarnessMint(ctx, relayDir), "dsh-remote-web: harness browser-session mint");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-web",
|
|
3
|
-
"version": "0.7.12-beta.
|
|
3
|
+
"version": "0.7.12-beta.3",
|
|
4
4
|
"description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|