liangzimixin 0.3.2 → 0.3.4
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/dist/index.cjs +75 -202
- package/dist/index.d.cts +48 -142
- package/dist/setup-entry.cjs +75 -202
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -17483,10 +17483,7 @@ var DEFAULT_INTERNAL_CONFIG = {
|
|
|
17483
17483
|
}
|
|
17484
17484
|
},
|
|
17485
17485
|
auth: {
|
|
17486
|
-
serverUrl: process.env.LZMX_AUTH_URL || "https://
|
|
17487
|
-
clientName: "liangzimixin",
|
|
17488
|
-
scopes: ["openid", "im_sdk", "data_operate", "offline_access"],
|
|
17489
|
-
autoRegister: true,
|
|
17486
|
+
serverUrl: process.env.LZMX_AUTH_URL || "https://imtwo.zdxlz.com/open-apis/v1",
|
|
17490
17487
|
refreshAheadMs: 3e5
|
|
17491
17488
|
},
|
|
17492
17489
|
crypto: {
|
|
@@ -17974,7 +17971,7 @@ async function resolveAndUploadMedia(params) {
|
|
|
17974
17971
|
};
|
|
17975
17972
|
}
|
|
17976
17973
|
const msgType = fileType;
|
|
17977
|
-
const contentPayload = fileType === "file" ? {
|
|
17974
|
+
const contentPayload = fileType === "file" ? { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize } : { fileId: uploadResult.fileKey };
|
|
17978
17975
|
await messagePipe.sendMessage({
|
|
17979
17976
|
chatId,
|
|
17980
17977
|
senderId: chatId,
|
|
@@ -17984,7 +17981,7 @@ async function resolveAndUploadMedia(params) {
|
|
|
17984
17981
|
});
|
|
17985
17982
|
log3.info("media:sent", {
|
|
17986
17983
|
chatId,
|
|
17987
|
-
|
|
17984
|
+
fileId: uploadResult.fileKey,
|
|
17988
17985
|
msgType
|
|
17989
17986
|
});
|
|
17990
17987
|
return {
|
|
@@ -18111,20 +18108,20 @@ ${body}` : body || raw.content;
|
|
|
18111
18108
|
}
|
|
18112
18109
|
case "image": {
|
|
18113
18110
|
const c = parsed;
|
|
18114
|
-
fileId = c?.
|
|
18111
|
+
fileId = c?.fileId;
|
|
18115
18112
|
text = c?.altText ?? (fileId ? `` : "[image]");
|
|
18116
18113
|
break;
|
|
18117
18114
|
}
|
|
18118
18115
|
case "file": {
|
|
18119
18116
|
const c = parsed;
|
|
18120
|
-
fileId = c?.
|
|
18121
|
-
fileName = c?.
|
|
18117
|
+
fileId = c?.fileId;
|
|
18118
|
+
fileName = c?.fileName;
|
|
18122
18119
|
text = fileName ? `[File: ${fileName}]` : "[file]";
|
|
18123
18120
|
break;
|
|
18124
18121
|
}
|
|
18125
18122
|
case "voice": {
|
|
18126
18123
|
const c = parsed;
|
|
18127
|
-
fileId = c?.
|
|
18124
|
+
fileId = c?.fileId;
|
|
18128
18125
|
const durationMs = c?.duration;
|
|
18129
18126
|
const durationStr = durationMs != null ? ` ${(durationMs / 1e3).toFixed(1)}s` : "";
|
|
18130
18127
|
text = `[Voice${durationStr}]`;
|
|
@@ -18132,7 +18129,7 @@ ${body}` : body || raw.content;
|
|
|
18132
18129
|
}
|
|
18133
18130
|
case "video": {
|
|
18134
18131
|
const c = parsed;
|
|
18135
|
-
fileId = c?.
|
|
18132
|
+
fileId = c?.fileId;
|
|
18136
18133
|
const durationMs = c?.duration;
|
|
18137
18134
|
const durationStr = durationMs != null ? ` ${(durationMs / 1e3).toFixed(1)}s` : "";
|
|
18138
18135
|
text = `[Video${durationStr}]`;
|
|
@@ -19008,13 +19005,13 @@ var CryptoEngine = class _CryptoEngine {
|
|
|
19008
19005
|
|
|
19009
19006
|
// src/auth/oauth-client.ts
|
|
19010
19007
|
var log16 = createLogger("auth/oauth-client");
|
|
19011
|
-
var
|
|
19008
|
+
var ApiError = class extends Error {
|
|
19012
19009
|
constructor(status, body, endpoint) {
|
|
19013
|
-
super(`
|
|
19010
|
+
super(`API error: ${status} on ${endpoint}`);
|
|
19014
19011
|
this.status = status;
|
|
19015
19012
|
this.body = body;
|
|
19016
19013
|
this.endpoint = endpoint;
|
|
19017
|
-
this.name = "
|
|
19014
|
+
this.name = "ApiError";
|
|
19018
19015
|
}
|
|
19019
19016
|
};
|
|
19020
19017
|
var TokenAcquireError = class extends Error {
|
|
@@ -19031,28 +19028,21 @@ function sleep(ms) {
|
|
|
19031
19028
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
19032
19029
|
}
|
|
19033
19030
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
19034
|
-
var
|
|
19031
|
+
var GRANT_TYPE = "client_credentials";
|
|
19032
|
+
var SCOPE = "client_credentials refresh_token";
|
|
19033
|
+
var OAuthClient = class {
|
|
19035
19034
|
baseUrl;
|
|
19036
|
-
|
|
19037
|
-
|
|
19038
|
-
clientName;
|
|
19039
|
-
scopes;
|
|
19035
|
+
appId;
|
|
19036
|
+
appSecret;
|
|
19040
19037
|
constructor(config2) {
|
|
19041
|
-
this.baseUrl = config2.
|
|
19042
|
-
this.
|
|
19043
|
-
this.
|
|
19044
|
-
this.
|
|
19045
|
-
this.scopes = config2.scopes;
|
|
19046
|
-
log16.info("SealClient initialized", { serverUrl: this.baseUrl, clientName: config2.clientName });
|
|
19038
|
+
this.baseUrl = config2.baseUrl.replace(/\/+$/, "");
|
|
19039
|
+
this.appId = config2.appId;
|
|
19040
|
+
this.appSecret = config2.appSecret;
|
|
19041
|
+
log16.info("OAuthClient initialized", { baseUrl: this.baseUrl, appId: sanitize(this.appId) });
|
|
19047
19042
|
}
|
|
19048
19043
|
// ── 通用 HTTP 请求 ──
|
|
19049
19044
|
/**
|
|
19050
19045
|
* 统一 HTTP 请求封装 — 含超时、日志脱敏和错误处理
|
|
19051
|
-
* @param method - HTTP 方法
|
|
19052
|
-
* @param path - 请求路径 (如 '/seal/client/register')
|
|
19053
|
-
* @param options - 请求选项
|
|
19054
|
-
* @returns 解析后的 JSON 响应
|
|
19055
|
-
* @throws SealApiError — HTTP 非 2xx 响应
|
|
19056
19046
|
*/
|
|
19057
19047
|
async request(method, path2, options = {}) {
|
|
19058
19048
|
const url2 = `${this.baseUrl}${path2}`;
|
|
@@ -19084,109 +19074,38 @@ var SealClient = class {
|
|
|
19084
19074
|
parsedBody = responseBody;
|
|
19085
19075
|
}
|
|
19086
19076
|
log16.error("HTTP error", { method, url: url2, status: response.status });
|
|
19087
|
-
throw new
|
|
19077
|
+
throw new ApiError(response.status, parsedBody, path2);
|
|
19088
19078
|
}
|
|
19089
19079
|
log16.debug("HTTP response", { method, url: url2, status: response.status });
|
|
19090
19080
|
try {
|
|
19091
19081
|
return JSON.parse(responseBody);
|
|
19092
19082
|
} catch {
|
|
19093
|
-
throw new
|
|
19083
|
+
throw new ApiError(response.status, responseBody, path2);
|
|
19094
19084
|
}
|
|
19095
19085
|
}
|
|
19096
19086
|
// ── 公共方法 ──
|
|
19097
|
-
/** 判断是否已注册 (有 clientId + clientSecret) */
|
|
19098
|
-
isRegistered() {
|
|
19099
|
-
return !!(this.clientId && this.clientSecret);
|
|
19100
|
-
}
|
|
19101
|
-
/** 更新 clientId / clientSecret (注册成功后或从持久化加载后调用) */
|
|
19102
|
-
setCredentials(clientId, clientSecret) {
|
|
19103
|
-
this.clientId = clientId;
|
|
19104
|
-
this.clientSecret = clientSecret;
|
|
19105
|
-
log16.info("Credentials updated", { clientId: sanitize(clientId) });
|
|
19106
|
-
}
|
|
19107
|
-
/**
|
|
19108
|
-
* 动态注册客户端 → POST /seal/client/register
|
|
19109
|
-
*
|
|
19110
|
-
* 首次运行时自动调用,获取 clientId 和 clientSecret。
|
|
19111
|
-
* 注册成功后自动更新内部凭据。
|
|
19112
|
-
*/
|
|
19113
|
-
async register(params) {
|
|
19114
|
-
const body = {
|
|
19115
|
-
client_name: params.clientName,
|
|
19116
|
-
scopes: params.scopes,
|
|
19117
|
-
authentication_methods: params.authMethods ?? ["client_secret_post"],
|
|
19118
|
-
grant_types: params.grantTypes ?? ["authorization_code", "client_credentials", "refresh_token"],
|
|
19119
|
-
redirect_uris: params.redirectUris ?? ["https://placeholder.local"],
|
|
19120
|
-
logout_redirect_uris: [],
|
|
19121
|
-
client_settings: {
|
|
19122
|
-
"settings.client.require-authorization-consent": true
|
|
19123
|
-
}
|
|
19124
|
-
};
|
|
19125
|
-
if (params.tokenSettings) {
|
|
19126
|
-
const ts = {};
|
|
19127
|
-
if (params.tokenSettings.accessTokenTtl) {
|
|
19128
|
-
ts["settings.token.access-token-time-to-live"] = params.tokenSettings.accessTokenTtl;
|
|
19129
|
-
}
|
|
19130
|
-
if (params.tokenSettings.refreshTokenTtl) {
|
|
19131
|
-
ts["settings.token.refresh-token-time-to-live"] = params.tokenSettings.refreshTokenTtl;
|
|
19132
|
-
}
|
|
19133
|
-
if (params.tokenSettings.accessTokenFormat) {
|
|
19134
|
-
ts["settings.token.access-token-format"] = params.tokenSettings.accessTokenFormat;
|
|
19135
|
-
}
|
|
19136
|
-
if (params.tokenSettings.reuseRefreshTokens !== void 0) {
|
|
19137
|
-
ts["settings.token.reuse-refresh-tokens"] = params.tokenSettings.reuseRefreshTokens;
|
|
19138
|
-
}
|
|
19139
|
-
ts["settings.token.authorization-code-time-to-live"] = "300";
|
|
19140
|
-
body.token_settings = ts;
|
|
19141
|
-
}
|
|
19142
|
-
log16.info("Registering OAuth client", { clientName: params.clientName });
|
|
19143
|
-
const response = await this.request(
|
|
19144
|
-
"POST",
|
|
19145
|
-
"/seal/client/register",
|
|
19146
|
-
{ body, contentType: "json" }
|
|
19147
|
-
);
|
|
19148
|
-
const registration = {
|
|
19149
|
-
clientId: response.client_id,
|
|
19150
|
-
clientSecret: response.client_secret,
|
|
19151
|
-
clientSecretExpiresAt: response.client_secret_expires_at ?? "",
|
|
19152
|
-
clientIdIssuedAt: response.client_id_issued_at ?? "",
|
|
19153
|
-
scopes: response.scopes ?? params.scopes
|
|
19154
|
-
};
|
|
19155
|
-
this.setCredentials(registration.clientId, registration.clientSecret);
|
|
19156
|
-
log16.info("OAuth client registered", {
|
|
19157
|
-
clientId: sanitize(registration.clientId),
|
|
19158
|
-
clientSecret: sanitize(registration.clientSecret),
|
|
19159
|
-
scopes: registration.scopes,
|
|
19160
|
-
expiresAt: registration.clientSecretExpiresAt
|
|
19161
|
-
});
|
|
19162
|
-
return registration;
|
|
19163
|
-
}
|
|
19164
19087
|
/**
|
|
19165
|
-
* 获取访问令牌 → POST /
|
|
19088
|
+
* 获取访问令牌 → POST /auth/token (client_credentials)
|
|
19166
19089
|
*
|
|
19167
|
-
*
|
|
19168
|
-
*
|
|
19090
|
+
* 请求参数 (x-www-form-urlencoded):
|
|
19091
|
+
* - grant_type: client_credentials (写死)
|
|
19092
|
+
* - client_id: appId
|
|
19093
|
+
* - client_secret: appSecret
|
|
19094
|
+
* - scope: client_credentials refresh_token (写死)
|
|
19169
19095
|
*
|
|
19170
|
-
* @param scope - 请求的权限范围 (空格分隔),不传则使用注册时的全部 scope
|
|
19171
19096
|
* @returns TokenData 包含 access_token、过期时间等
|
|
19172
|
-
* @throws
|
|
19173
|
-
* @throws SealApiError — HTTP 错误
|
|
19097
|
+
* @throws ApiError — HTTP 错误
|
|
19174
19098
|
*/
|
|
19175
|
-
async getToken(
|
|
19176
|
-
if (!this.clientId || !this.clientSecret) {
|
|
19177
|
-
throw new Error("Cannot get token: clientId and clientSecret are required. Call register() or setCredentials() first.");
|
|
19178
|
-
}
|
|
19099
|
+
async getToken() {
|
|
19179
19100
|
const params = new URLSearchParams();
|
|
19180
|
-
params.set("grant_type",
|
|
19181
|
-
params.set("client_id", this.
|
|
19182
|
-
params.set("client_secret", this.
|
|
19183
|
-
|
|
19184
|
-
|
|
19185
|
-
}
|
|
19186
|
-
log16.info("Requesting access token", { clientId: sanitize(this.clientId) });
|
|
19101
|
+
params.set("grant_type", GRANT_TYPE);
|
|
19102
|
+
params.set("client_id", this.appId);
|
|
19103
|
+
params.set("client_secret", this.appSecret);
|
|
19104
|
+
params.set("scope", SCOPE);
|
|
19105
|
+
log16.info("Requesting access token", { appId: sanitize(this.appId) });
|
|
19187
19106
|
const response = await this.request(
|
|
19188
19107
|
"POST",
|
|
19189
|
-
"/
|
|
19108
|
+
"/auth/token",
|
|
19190
19109
|
{ body: params, contentType: "form" }
|
|
19191
19110
|
);
|
|
19192
19111
|
const now = Date.now();
|
|
@@ -19197,7 +19116,6 @@ var SealClient = class {
|
|
|
19197
19116
|
expiresIn,
|
|
19198
19117
|
scope: response.scope ?? "",
|
|
19199
19118
|
refreshToken: void 0,
|
|
19200
|
-
// Seal 不返回 refresh_token
|
|
19201
19119
|
grantedAt: now,
|
|
19202
19120
|
expiresAt: now + expiresIn * 1e3
|
|
19203
19121
|
};
|
|
@@ -19208,12 +19126,9 @@ var SealClient = class {
|
|
|
19208
19126
|
});
|
|
19209
19127
|
return tokenData;
|
|
19210
19128
|
}
|
|
19211
|
-
/**
|
|
19212
|
-
|
|
19213
|
-
|
|
19214
|
-
*/
|
|
19215
|
-
async introspect(_token) {
|
|
19216
|
-
throw new Error("introspect() not implemented yet \u2014 Seal introspect API \u6682\u4E0D\u4F7F\u7528");
|
|
19129
|
+
/** 获取 baseUrl (供其他模块复用) */
|
|
19130
|
+
getBaseUrl() {
|
|
19131
|
+
return this.baseUrl;
|
|
19217
19132
|
}
|
|
19218
19133
|
};
|
|
19219
19134
|
|
|
@@ -19347,12 +19262,10 @@ var DEFAULT_REFRESH_AHEAD_MS = 5 * 60 * 1e3;
|
|
|
19347
19262
|
var MAX_RETRIES = 3;
|
|
19348
19263
|
var RETRY_BASE_MS = 1e3;
|
|
19349
19264
|
var TokenManager = class {
|
|
19350
|
-
|
|
19265
|
+
oauthClient;
|
|
19351
19266
|
tokenStore;
|
|
19352
19267
|
/** Token 过期前提前刷新的时间 (ms) */
|
|
19353
19268
|
refreshAheadMs;
|
|
19354
|
-
/** 自动注册的参数 */
|
|
19355
|
-
autoRegisterParams;
|
|
19356
19269
|
// ── 内存缓存 ──
|
|
19357
19270
|
/** 内存中缓存的 access_token */
|
|
19358
19271
|
cachedToken = null;
|
|
@@ -19366,10 +19279,9 @@ var TokenManager = class {
|
|
|
19366
19279
|
/** 定时刷新器句柄 */
|
|
19367
19280
|
refreshTimer = null;
|
|
19368
19281
|
constructor(deps) {
|
|
19369
|
-
this.
|
|
19282
|
+
this.oauthClient = deps.oauthClient;
|
|
19370
19283
|
this.tokenStore = deps.tokenStore;
|
|
19371
19284
|
this.refreshAheadMs = deps.refreshAheadMs ?? DEFAULT_REFRESH_AHEAD_MS;
|
|
19372
|
-
this.autoRegisterParams = deps.autoRegisterParams;
|
|
19373
19285
|
log18.info("TokenManager initialized", { refreshAheadMs: this.refreshAheadMs });
|
|
19374
19286
|
}
|
|
19375
19287
|
// ── 核心方法 ──
|
|
@@ -19398,13 +19310,6 @@ var TokenManager = class {
|
|
|
19398
19310
|
this.refreshLock = null;
|
|
19399
19311
|
}
|
|
19400
19312
|
}
|
|
19401
|
-
/**
|
|
19402
|
-
* 令牌自省 — 暂不实现,后续补充。
|
|
19403
|
-
* 返回的 identity_id 用于 gate.ts 的 anti-loop 检查 (bot 自己的 ID)。
|
|
19404
|
-
*/
|
|
19405
|
-
async introspect() {
|
|
19406
|
-
throw new Error("introspect() not implemented yet \u2014 Seal introspect API \u6682\u4E0D\u4F7F\u7528");
|
|
19407
|
-
}
|
|
19408
19313
|
/** 检查当前令牌是否包含指定的 scope */
|
|
19409
19314
|
hasScope(scope) {
|
|
19410
19315
|
if (!this.cachedScope) return false;
|
|
@@ -19435,14 +19340,14 @@ var TokenManager = class {
|
|
|
19435
19340
|
*
|
|
19436
19341
|
* 重试策略:
|
|
19437
19342
|
* - 网络错误 / 5xx → 重试 (最多 3 次)
|
|
19438
|
-
* - 401/403 → 不重试 (
|
|
19343
|
+
* - 401/403 → 不重试 (凭证可能无效)
|
|
19439
19344
|
*/
|
|
19440
19345
|
async _acquireTokenWithRetry() {
|
|
19441
19346
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
19442
19347
|
try {
|
|
19443
19348
|
return await this._acquireToken();
|
|
19444
19349
|
} catch (err) {
|
|
19445
|
-
if (err instanceof
|
|
19350
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
19446
19351
|
log18.error("Token acquire failed with auth error, not retrying", { status: err.status });
|
|
19447
19352
|
throw err;
|
|
19448
19353
|
}
|
|
@@ -19470,10 +19375,9 @@ var TokenManager = class {
|
|
|
19470
19375
|
*
|
|
19471
19376
|
* 流程:
|
|
19472
19377
|
* 1. 尝试从 TokenStore 加载缓存
|
|
19473
|
-
* 2.
|
|
19474
|
-
* 3.
|
|
19475
|
-
* 4.
|
|
19476
|
-
* 5. 设置定时刷新器
|
|
19378
|
+
* 2. 调用 OAuthClient.getToken() 获取新 Token
|
|
19379
|
+
* 3. 保存到内存缓存 + TokenStore
|
|
19380
|
+
* 4. 设置定时刷新器
|
|
19477
19381
|
*/
|
|
19478
19382
|
async _acquireToken() {
|
|
19479
19383
|
try {
|
|
@@ -19490,43 +19394,8 @@ var TokenManager = class {
|
|
|
19490
19394
|
} catch {
|
|
19491
19395
|
log18.warn("Failed to load token from store, acquiring new one");
|
|
19492
19396
|
}
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
const savedCreds = await this.tokenStore.load("credentials");
|
|
19496
|
-
if (savedCreds?.clientId && savedCreds?.clientSecret) {
|
|
19497
|
-
this.sealClient.setCredentials(
|
|
19498
|
-
savedCreds.clientId,
|
|
19499
|
-
savedCreds.clientSecret
|
|
19500
|
-
);
|
|
19501
|
-
log18.info("Credentials loaded from store");
|
|
19502
|
-
}
|
|
19503
|
-
} catch {
|
|
19504
|
-
log18.debug("No saved credentials found");
|
|
19505
|
-
}
|
|
19506
|
-
}
|
|
19507
|
-
if (!this.sealClient.isRegistered()) {
|
|
19508
|
-
if (!this.autoRegisterParams) {
|
|
19509
|
-
throw new Error("SealClient not registered and autoRegisterParams not provided. Call setCredentials() or enable autoRegister.");
|
|
19510
|
-
}
|
|
19511
|
-
log18.info("Auto-registering OAuth client");
|
|
19512
|
-
const registration = await this.sealClient.register({
|
|
19513
|
-
clientName: this.autoRegisterParams.clientName,
|
|
19514
|
-
scopes: this.autoRegisterParams.scopes,
|
|
19515
|
-
tokenSettings: this.autoRegisterParams.tokenSettings ? {
|
|
19516
|
-
accessTokenTtl: this.autoRegisterParams.tokenSettings.accessTokenTtl,
|
|
19517
|
-
refreshTokenTtl: this.autoRegisterParams.tokenSettings.refreshTokenTtl,
|
|
19518
|
-
accessTokenFormat: this.autoRegisterParams.tokenSettings.accessTokenFormat
|
|
19519
|
-
} : void 0
|
|
19520
|
-
});
|
|
19521
|
-
await this.tokenStore.save("credentials", {
|
|
19522
|
-
clientId: registration.clientId,
|
|
19523
|
-
clientSecret: registration.clientSecret,
|
|
19524
|
-
clientSecretExpiresAt: registration.clientSecretExpiresAt
|
|
19525
|
-
});
|
|
19526
|
-
log18.info("OAuth client registered and credentials saved");
|
|
19527
|
-
}
|
|
19528
|
-
log18.info("Acquiring new access token");
|
|
19529
|
-
const tokenData = await this.sealClient.getToken();
|
|
19397
|
+
log18.info("Acquiring new access token via POST /auth/token");
|
|
19398
|
+
const tokenData = await this.oauthClient.getToken();
|
|
19530
19399
|
this.updateCache(tokenData);
|
|
19531
19400
|
await this.tokenStore.save("token", tokenData);
|
|
19532
19401
|
this.scheduleRefresh(tokenData);
|
|
@@ -19883,11 +19752,18 @@ var MessagePipe = class {
|
|
|
19883
19752
|
log21.warn("inbound:data-parse-error", { error: err.message });
|
|
19884
19753
|
return;
|
|
19885
19754
|
}
|
|
19886
|
-
if (callbackData.eventType !== "
|
|
19755
|
+
if (callbackData.eventType !== "callback:direct") {
|
|
19887
19756
|
log21.debug("inbound:event-ignored", { eventType: callbackData.eventType });
|
|
19888
19757
|
return;
|
|
19889
19758
|
}
|
|
19890
|
-
const msg =
|
|
19759
|
+
const msg = {
|
|
19760
|
+
messageId: callbackData.msgUid,
|
|
19761
|
+
chatId: callbackData.groupId || callbackData.userId,
|
|
19762
|
+
senderId: callbackData.userId,
|
|
19763
|
+
msgType: callbackData.type,
|
|
19764
|
+
content: JSON.stringify(callbackData.content),
|
|
19765
|
+
timestamp: Date.now()
|
|
19766
|
+
};
|
|
19891
19767
|
log21.debug("inbound:frame", { messageId: msg.messageId, eventType: callbackData.eventType });
|
|
19892
19768
|
if (this.dedup.isDuplicate(msg.messageId)) {
|
|
19893
19769
|
log21.debug("inbound:dedup-hit", { messageId: msg.messageId });
|
|
@@ -19948,10 +19824,14 @@ var ConnectionManager = class {
|
|
|
19948
19824
|
this.running = true;
|
|
19949
19825
|
this.reconnectAttempts = 0;
|
|
19950
19826
|
const token = await tokenFn();
|
|
19827
|
+
const wsUrl = url2.replace(/\/+$/, "") + "/events/stream";
|
|
19951
19828
|
await this.client.connect({
|
|
19952
|
-
url:
|
|
19829
|
+
url: wsUrl,
|
|
19953
19830
|
token,
|
|
19954
|
-
headers:
|
|
19831
|
+
headers: {
|
|
19832
|
+
"Authorization": token,
|
|
19833
|
+
...this.appId ? { "X-App-ID": this.appId } : {}
|
|
19834
|
+
}
|
|
19955
19835
|
});
|
|
19956
19836
|
this.client.on("close", () => {
|
|
19957
19837
|
if (this.running) {
|
|
@@ -20004,10 +19884,14 @@ var ConnectionManager = class {
|
|
|
20004
19884
|
this.reconnectAttempts++;
|
|
20005
19885
|
try {
|
|
20006
19886
|
const token = await this.tokenFn();
|
|
19887
|
+
const wsUrl = this.url.replace(/\/+$/, "") + "/events/stream";
|
|
20007
19888
|
await this.client.connect({
|
|
20008
|
-
url:
|
|
19889
|
+
url: wsUrl,
|
|
20009
19890
|
token,
|
|
20010
|
-
headers:
|
|
19891
|
+
headers: {
|
|
19892
|
+
"Authorization": token,
|
|
19893
|
+
...this.appId ? { "X-App-ID": this.appId } : {}
|
|
19894
|
+
}
|
|
20011
19895
|
});
|
|
20012
19896
|
this.reconnectAttempts = 0;
|
|
20013
19897
|
this.startHeartbeat();
|
|
@@ -20432,28 +20316,17 @@ async function startPlugin(accountConfig, internalOverrides) {
|
|
|
20432
20316
|
cryptoEngine = CryptoEngine.createPassthrough();
|
|
20433
20317
|
log25.info("Crypto passthrough mode \u2713 (disabled by config)");
|
|
20434
20318
|
}
|
|
20435
|
-
const
|
|
20436
|
-
|
|
20437
|
-
|
|
20438
|
-
|
|
20439
|
-
clientSecret: config2.auth.clientSecret,
|
|
20440
|
-
scopes: config2.auth.scopes
|
|
20319
|
+
const oauthClient = new OAuthClient({
|
|
20320
|
+
baseUrl: config2.auth.serverUrl,
|
|
20321
|
+
appId: accountConfig.appId,
|
|
20322
|
+
appSecret: accountConfig.appSecret
|
|
20441
20323
|
});
|
|
20442
20324
|
const tokenStorePath = (0, import_node_path2.join)((0, import_node_os.homedir)(), TOKEN_STORE_DIR, "tokens");
|
|
20443
20325
|
const tokenStore = new TokenStore(tokenStorePath, cryptoEngine);
|
|
20444
20326
|
const tokenManager = new TokenManager({
|
|
20445
|
-
|
|
20327
|
+
oauthClient,
|
|
20446
20328
|
tokenStore,
|
|
20447
|
-
refreshAheadMs: config2.auth.refreshAheadMs
|
|
20448
|
-
autoRegisterParams: config2.auth.autoRegister ? {
|
|
20449
|
-
clientName: config2.auth.clientName,
|
|
20450
|
-
scopes: config2.auth.scopes,
|
|
20451
|
-
tokenSettings: config2.auth.tokenSettings ? {
|
|
20452
|
-
accessTokenTtl: config2.auth.tokenSettings.accessTokenTtl,
|
|
20453
|
-
refreshTokenTtl: config2.auth.tokenSettings.refreshTokenTtl,
|
|
20454
|
-
accessTokenFormat: config2.auth.tokenSettings.accessTokenFormat
|
|
20455
|
-
} : void 0
|
|
20456
|
-
} : void 0
|
|
20329
|
+
refreshAheadMs: config2.auth.refreshAheadMs
|
|
20457
20330
|
});
|
|
20458
20331
|
log25.info("Auth initialized \u2713");
|
|
20459
20332
|
let pushQueue = null;
|
package/dist/index.d.cts
CHANGED
|
@@ -64,26 +64,10 @@ interface InternalConfig {
|
|
|
64
64
|
};
|
|
65
65
|
};
|
|
66
66
|
auth: {
|
|
67
|
-
/** 🔴
|
|
67
|
+
/** 🔴 API 基础地址 (如 https://imtwo.zdxlz.com/open-apis/v1) */
|
|
68
68
|
serverUrl: string;
|
|
69
|
-
/** 🔴 OAuth 客户端显示名称 */
|
|
70
|
-
clientName: string;
|
|
71
|
-
/** 🟡 OAuth Client ID (不填则自动注册) */
|
|
72
|
-
clientId?: string;
|
|
73
|
-
/** 🟡 OAuth Client Secret */
|
|
74
|
-
clientSecret?: string;
|
|
75
|
-
/** 🟡 OAuth 授权范围 */
|
|
76
|
-
scopes: string[];
|
|
77
|
-
/** 🟡 是否自动注册 OAuth 客户端 */
|
|
78
|
-
autoRegister: boolean;
|
|
79
69
|
/** 🟡 Token 提前刷新时间 (ms) */
|
|
80
70
|
refreshAheadMs: number;
|
|
81
|
-
/** 🟡 Token 高级设置 */
|
|
82
|
-
tokenSettings?: {
|
|
83
|
-
accessTokenTtl: string;
|
|
84
|
-
refreshTokenTtl: string;
|
|
85
|
-
accessTokenFormat: string;
|
|
86
|
-
};
|
|
87
71
|
};
|
|
88
72
|
crypto: {
|
|
89
73
|
/** 🟡 是否启用量子加密 */
|
|
@@ -162,10 +146,10 @@ interface InboundMessage {
|
|
|
162
146
|
* 根据 msgType 解析为对应的 Content 类型:
|
|
163
147
|
* - text: `{"content": "你好"}`
|
|
164
148
|
* - markdown: `{"title": "标题", "content": "正文", "recommendations": [...]}`
|
|
165
|
-
* - image: `{"
|
|
166
|
-
* - file: `{"
|
|
167
|
-
* - voice: `{"
|
|
168
|
-
* - video: `{"
|
|
149
|
+
* - image: `{"fileId": "xxx", "width": 1920, "height": 1200}`
|
|
150
|
+
* - file: `{"fileId": "xxx", "fileName": "test.xlsx", "size": 9763}`
|
|
151
|
+
* - voice: `{"fileId": "xxx", "duration": 2914.23}`
|
|
152
|
+
* - video: `{"fileId": "xxx", "duration": 5291667.32, "width": 1200}`
|
|
169
153
|
* - system: `{"text": "用户已加入会话"}`
|
|
170
154
|
*/
|
|
171
155
|
content: string;
|
|
@@ -204,31 +188,7 @@ declare enum EncryptionStrategy {
|
|
|
204
188
|
METADATA = "metadata",
|
|
205
189
|
NONE = "none"
|
|
206
190
|
}
|
|
207
|
-
/**
|
|
208
|
-
interface SealConfig {
|
|
209
|
-
/** OAuth 服务器地址 */
|
|
210
|
-
serverUrl: string;
|
|
211
|
-
/** 客户端 ID (动态注册后获取) */
|
|
212
|
-
clientId?: string;
|
|
213
|
-
/** 客户端 Secret (动态注册后获取) */
|
|
214
|
-
clientSecret?: string;
|
|
215
|
-
/** 客户端显示名称 (注册时使用) */
|
|
216
|
-
clientName: string;
|
|
217
|
-
/** 请求的 OAuth scope 列表 */
|
|
218
|
-
scopes: string[];
|
|
219
|
-
}
|
|
220
|
-
/** 客户端动态注册结果 — POST /seal/client/register 响应 */
|
|
221
|
-
interface SealClientRegistration {
|
|
222
|
-
clientId: string;
|
|
223
|
-
clientSecret: string;
|
|
224
|
-
/** Secret 过期时间 (ISO 8601) */
|
|
225
|
-
clientSecretExpiresAt: string;
|
|
226
|
-
/** 客户端 ID 签发时间 (ISO 8601) */
|
|
227
|
-
clientIdIssuedAt: string;
|
|
228
|
-
/** 实际授权的 scope 列表 */
|
|
229
|
-
scopes: string[];
|
|
230
|
-
}
|
|
231
|
-
/** OAuth 令牌数据 — 包含 access_token 和可选的 refresh_token */
|
|
191
|
+
/** OAuth 令牌数据 — POST /auth/token 响应 */
|
|
232
192
|
interface TokenData {
|
|
233
193
|
accessToken: string;
|
|
234
194
|
tokenType: 'Bearer';
|
|
@@ -242,23 +202,12 @@ interface TokenData {
|
|
|
242
202
|
/** 签发时间戳 (Unix ms) */
|
|
243
203
|
grantedAt: number;
|
|
244
204
|
}
|
|
245
|
-
/** Token 自省结果 — GET /seal/oauth2/introspect 响应 */
|
|
246
|
-
interface IntrospectResult {
|
|
247
|
-
/** 令牌类型标识 */
|
|
248
|
-
schema: string;
|
|
249
|
-
/** 客户端 ID */
|
|
250
|
-
clientId: string;
|
|
251
|
-
/** 用户身份 ID (用于 gate.ts anti-loop 检查: 识别 bot 自己的 ID) */
|
|
252
|
-
identityId: string;
|
|
253
|
-
/** 实际授权的 scope */
|
|
254
|
-
scope: string;
|
|
255
|
-
}
|
|
256
205
|
|
|
257
206
|
/**
|
|
258
|
-
* openclaw-liangzimixin —
|
|
207
|
+
* openclaw-liangzimixin — OAuth HTTP 客户端
|
|
259
208
|
*
|
|
260
|
-
* 封装
|
|
261
|
-
*
|
|
209
|
+
* 封装 IM 开放平台授权通信: POST /auth/token (client_credentials)
|
|
210
|
+
* 基于《第三方应用机器人对接 API 文档》
|
|
262
211
|
*
|
|
263
212
|
* 安全要求:
|
|
264
213
|
* - client_secret / access_token 不得出现在日志中 (仅前 8 位 + ***)
|
|
@@ -266,73 +215,48 @@ interface IntrospectResult {
|
|
|
266
215
|
*/
|
|
267
216
|
|
|
268
217
|
/**
|
|
269
|
-
*
|
|
270
|
-
|
|
218
|
+
* OAuth 客户端配置 — 构造 OAuthClient 时传入
|
|
219
|
+
*/
|
|
220
|
+
interface OAuthClientConfig {
|
|
221
|
+
/** API 基础地址 (如 https://imtwo.zdxlz.com/open-apis/v1) */
|
|
222
|
+
baseUrl: string;
|
|
223
|
+
/** 应用 ID — 用作 OAuth client_id */
|
|
224
|
+
appId: string;
|
|
225
|
+
/** 应用密钥 — 用作 OAuth client_secret */
|
|
226
|
+
appSecret: string;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* OAuth HTTP 客户端 — 封装与 IM 开放平台的认证通信。
|
|
271
230
|
*
|
|
272
|
-
*
|
|
231
|
+
* 对接《第三方应用机器人对接 API 文档》中的授权接口:
|
|
232
|
+
* POST {{baseUrl}}/auth/token
|
|
233
|
+
* Content-Type: application/x-www-form-urlencoded
|
|
234
|
+
* grant_type=client_credentials & client_id={appId} & client_secret={appSecret} & scope=...
|
|
273
235
|
*/
|
|
274
|
-
declare class
|
|
236
|
+
declare class OAuthClient {
|
|
275
237
|
private readonly baseUrl;
|
|
276
|
-
private
|
|
277
|
-
private
|
|
278
|
-
|
|
279
|
-
private readonly scopes;
|
|
280
|
-
constructor(config: SealConfig);
|
|
238
|
+
private readonly appId;
|
|
239
|
+
private readonly appSecret;
|
|
240
|
+
constructor(config: OAuthClientConfig);
|
|
281
241
|
/**
|
|
282
242
|
* 统一 HTTP 请求封装 — 含超时、日志脱敏和错误处理
|
|
283
|
-
* @param method - HTTP 方法
|
|
284
|
-
* @param path - 请求路径 (如 '/seal/client/register')
|
|
285
|
-
* @param options - 请求选项
|
|
286
|
-
* @returns 解析后的 JSON 响应
|
|
287
|
-
* @throws SealApiError — HTTP 非 2xx 响应
|
|
288
243
|
*/
|
|
289
244
|
private request;
|
|
290
|
-
/** 判断是否已注册 (有 clientId + clientSecret) */
|
|
291
|
-
isRegistered(): boolean;
|
|
292
|
-
/** 更新 clientId / clientSecret (注册成功后或从持久化加载后调用) */
|
|
293
|
-
setCredentials(clientId: string, clientSecret: string): void;
|
|
294
245
|
/**
|
|
295
|
-
*
|
|
246
|
+
* 获取访问令牌 → POST /auth/token (client_credentials)
|
|
296
247
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
clientName: string;
|
|
303
|
-
/** 请求的 OAuth scope */
|
|
304
|
-
scopes: string[];
|
|
305
|
-
/** 授权类型 (默认包含 authorization_code, client_credentials, refresh_token) */
|
|
306
|
-
grantTypes?: string[];
|
|
307
|
-
/** 认证方法 (默认 ['client_secret_post']) */
|
|
308
|
-
authMethods?: string[];
|
|
309
|
-
/** 重定向 URI (默认 ['https://placeholder.local']) */
|
|
310
|
-
redirectUris?: string[];
|
|
311
|
-
/** Token 高级设置 */
|
|
312
|
-
tokenSettings?: {
|
|
313
|
-
accessTokenTtl?: string;
|
|
314
|
-
refreshTokenTtl?: string;
|
|
315
|
-
accessTokenFormat?: string;
|
|
316
|
-
reuseRefreshTokens?: boolean;
|
|
317
|
-
};
|
|
318
|
-
}): Promise<SealClientRegistration>;
|
|
319
|
-
/**
|
|
320
|
-
* 获取访问令牌 → POST /seal/oauth2/token (client_credentials)
|
|
248
|
+
* 请求参数 (x-www-form-urlencoded):
|
|
249
|
+
* - grant_type: client_credentials (写死)
|
|
250
|
+
* - client_id: appId
|
|
251
|
+
* - client_secret: appSecret
|
|
252
|
+
* - scope: client_credentials refresh_token (写死)
|
|
321
253
|
*
|
|
322
|
-
* 使用客户端凭证模式获取 Access Token。
|
|
323
|
-
* 前置条件: clientId 和 clientSecret 必须存在 (通过 register 或 setCredentials 设置)
|
|
324
|
-
*
|
|
325
|
-
* @param scope - 请求的权限范围 (空格分隔),不传则使用注册时的全部 scope
|
|
326
254
|
* @returns TokenData 包含 access_token、过期时间等
|
|
327
|
-
* @throws
|
|
328
|
-
* @throws SealApiError — HTTP 错误
|
|
255
|
+
* @throws ApiError — HTTP 错误
|
|
329
256
|
*/
|
|
330
|
-
getToken(
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
* 暂不实现 — 保留接口签名,后续需要时再补充
|
|
334
|
-
*/
|
|
335
|
-
introspect(_token: string): Promise<IntrospectResult>;
|
|
257
|
+
getToken(): Promise<TokenData>;
|
|
258
|
+
/** 获取 baseUrl (供其他模块复用) */
|
|
259
|
+
getBaseUrl(): string;
|
|
336
260
|
}
|
|
337
261
|
|
|
338
262
|
/**
|
|
@@ -540,11 +464,11 @@ declare class TokenStore {
|
|
|
540
464
|
/**
|
|
541
465
|
* openclaw-liangzimixin — Token 生命周期管理
|
|
542
466
|
*
|
|
543
|
-
* 自动获取 / 缓存 / 过期重取 / 并发锁 /
|
|
467
|
+
* 自动获取 / 缓存 / 过期重取 / 并发锁 / 定时刷新
|
|
544
468
|
*
|
|
545
469
|
* Token 生命周期策略:
|
|
546
|
-
* - 首次调用: 检查本地缓存 →
|
|
547
|
-
* - 过期处理: 直接重新调用
|
|
470
|
+
* - 首次调用: 检查本地缓存 → 未命中则调用 POST /auth/token → 缓存
|
|
471
|
+
* - 过期处理: 直接重新调用 getToken() 获取新 Token
|
|
548
472
|
* - 提前刷新: Token 过期前 refreshAheadMs 毫秒自动重新获取
|
|
549
473
|
* - 并发锁: 多个并发请求共享同一个 Token 获取 Promise
|
|
550
474
|
*
|
|
@@ -565,12 +489,10 @@ declare class TokenStore {
|
|
|
565
489
|
* shutdown() — 清理定时器和并发锁
|
|
566
490
|
*/
|
|
567
491
|
declare class TokenManager {
|
|
568
|
-
private readonly
|
|
492
|
+
private readonly oauthClient;
|
|
569
493
|
private readonly tokenStore;
|
|
570
494
|
/** Token 过期前提前刷新的时间 (ms) */
|
|
571
495
|
private readonly refreshAheadMs;
|
|
572
|
-
/** 自动注册的参数 */
|
|
573
|
-
private readonly autoRegisterParams?;
|
|
574
496
|
/** 内存中缓存的 access_token */
|
|
575
497
|
private cachedToken;
|
|
576
498
|
/** 内存中缓存的 scope (用于 hasScope 检查) */
|
|
@@ -582,19 +504,9 @@ declare class TokenManager {
|
|
|
582
504
|
/** 定时刷新器句柄 */
|
|
583
505
|
private refreshTimer;
|
|
584
506
|
constructor(deps: {
|
|
585
|
-
|
|
507
|
+
oauthClient: OAuthClient;
|
|
586
508
|
tokenStore: TokenStore;
|
|
587
509
|
refreshAheadMs?: number;
|
|
588
|
-
/** 自动注册参数 — autoRegister=true 时使用 */
|
|
589
|
-
autoRegisterParams?: {
|
|
590
|
-
clientName: string;
|
|
591
|
-
scopes: string[];
|
|
592
|
-
tokenSettings?: {
|
|
593
|
-
accessTokenTtl?: string;
|
|
594
|
-
refreshTokenTtl?: string;
|
|
595
|
-
accessTokenFormat?: string;
|
|
596
|
-
};
|
|
597
|
-
};
|
|
598
510
|
});
|
|
599
511
|
/**
|
|
600
512
|
* 获取有效的 access_token — 自动获取/重取,含并发锁。
|
|
@@ -607,11 +519,6 @@ declare class TokenManager {
|
|
|
607
519
|
* @returns 有效的 access_token 字符串
|
|
608
520
|
*/
|
|
609
521
|
getValidToken(): Promise<string>;
|
|
610
|
-
/**
|
|
611
|
-
* 令牌自省 — 暂不实现,后续补充。
|
|
612
|
-
* 返回的 identity_id 用于 gate.ts 的 anti-loop 检查 (bot 自己的 ID)。
|
|
613
|
-
*/
|
|
614
|
-
introspect(): Promise<IntrospectResult>;
|
|
615
522
|
/** 检查当前令牌是否包含指定的 scope */
|
|
616
523
|
hasScope(scope: string): boolean;
|
|
617
524
|
/** 检查是否已授权 (是否持有有效令牌) */
|
|
@@ -625,7 +532,7 @@ declare class TokenManager {
|
|
|
625
532
|
*
|
|
626
533
|
* 重试策略:
|
|
627
534
|
* - 网络错误 / 5xx → 重试 (最多 3 次)
|
|
628
|
-
* - 401/403 → 不重试 (
|
|
535
|
+
* - 401/403 → 不重试 (凭证可能无效)
|
|
629
536
|
*/
|
|
630
537
|
private _acquireTokenWithRetry;
|
|
631
538
|
/**
|
|
@@ -633,10 +540,9 @@ declare class TokenManager {
|
|
|
633
540
|
*
|
|
634
541
|
* 流程:
|
|
635
542
|
* 1. 尝试从 TokenStore 加载缓存
|
|
636
|
-
* 2.
|
|
637
|
-
* 3.
|
|
638
|
-
* 4.
|
|
639
|
-
* 5. 设置定时刷新器
|
|
543
|
+
* 2. 调用 OAuthClient.getToken() 获取新 Token
|
|
544
|
+
* 3. 保存到内存缓存 + TokenStore
|
|
545
|
+
* 4. 设置定时刷新器
|
|
640
546
|
*/
|
|
641
547
|
private _acquireToken;
|
|
642
548
|
/** 更新内存缓存 */
|
package/dist/setup-entry.cjs
CHANGED
|
@@ -4120,7 +4120,7 @@ async function resolveAndUploadMedia(params) {
|
|
|
4120
4120
|
};
|
|
4121
4121
|
}
|
|
4122
4122
|
const msgType = fileType;
|
|
4123
|
-
const contentPayload = fileType === "file" ? {
|
|
4123
|
+
const contentPayload = fileType === "file" ? { fileId: uploadResult.fileKey, fileName, size: uploadResult.fileSize } : { fileId: uploadResult.fileKey };
|
|
4124
4124
|
await messagePipe.sendMessage({
|
|
4125
4125
|
chatId,
|
|
4126
4126
|
senderId: chatId,
|
|
@@ -4130,7 +4130,7 @@ async function resolveAndUploadMedia(params) {
|
|
|
4130
4130
|
});
|
|
4131
4131
|
log3.info("media:sent", {
|
|
4132
4132
|
chatId,
|
|
4133
|
-
|
|
4133
|
+
fileId: uploadResult.fileKey,
|
|
4134
4134
|
msgType
|
|
4135
4135
|
});
|
|
4136
4136
|
return {
|
|
@@ -18039,10 +18039,7 @@ var DEFAULT_INTERNAL_CONFIG = {
|
|
|
18039
18039
|
}
|
|
18040
18040
|
},
|
|
18041
18041
|
auth: {
|
|
18042
|
-
serverUrl: process.env.LZMX_AUTH_URL || "https://
|
|
18043
|
-
clientName: "liangzimixin",
|
|
18044
|
-
scopes: ["openid", "im_sdk", "data_operate", "offline_access"],
|
|
18045
|
-
autoRegister: true,
|
|
18042
|
+
serverUrl: process.env.LZMX_AUTH_URL || "https://imtwo.zdxlz.com/open-apis/v1",
|
|
18046
18043
|
refreshAheadMs: 3e5
|
|
18047
18044
|
},
|
|
18048
18045
|
crypto: {
|
|
@@ -18266,13 +18263,13 @@ var CryptoEngine = class _CryptoEngine {
|
|
|
18266
18263
|
|
|
18267
18264
|
// src/auth/oauth-client.ts
|
|
18268
18265
|
var log7 = createLogger("auth/oauth-client");
|
|
18269
|
-
var
|
|
18266
|
+
var ApiError = class extends Error {
|
|
18270
18267
|
constructor(status, body, endpoint) {
|
|
18271
|
-
super(`
|
|
18268
|
+
super(`API error: ${status} on ${endpoint}`);
|
|
18272
18269
|
this.status = status;
|
|
18273
18270
|
this.body = body;
|
|
18274
18271
|
this.endpoint = endpoint;
|
|
18275
|
-
this.name = "
|
|
18272
|
+
this.name = "ApiError";
|
|
18276
18273
|
}
|
|
18277
18274
|
};
|
|
18278
18275
|
var TokenAcquireError = class extends Error {
|
|
@@ -18289,28 +18286,21 @@ function sleep(ms) {
|
|
|
18289
18286
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
18290
18287
|
}
|
|
18291
18288
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
18292
|
-
var
|
|
18289
|
+
var GRANT_TYPE = "client_credentials";
|
|
18290
|
+
var SCOPE = "client_credentials refresh_token";
|
|
18291
|
+
var OAuthClient = class {
|
|
18293
18292
|
baseUrl;
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
clientName;
|
|
18297
|
-
scopes;
|
|
18293
|
+
appId;
|
|
18294
|
+
appSecret;
|
|
18298
18295
|
constructor(config2) {
|
|
18299
|
-
this.baseUrl = config2.
|
|
18300
|
-
this.
|
|
18301
|
-
this.
|
|
18302
|
-
this.
|
|
18303
|
-
this.scopes = config2.scopes;
|
|
18304
|
-
log7.info("SealClient initialized", { serverUrl: this.baseUrl, clientName: config2.clientName });
|
|
18296
|
+
this.baseUrl = config2.baseUrl.replace(/\/+$/, "");
|
|
18297
|
+
this.appId = config2.appId;
|
|
18298
|
+
this.appSecret = config2.appSecret;
|
|
18299
|
+
log7.info("OAuthClient initialized", { baseUrl: this.baseUrl, appId: sanitize(this.appId) });
|
|
18305
18300
|
}
|
|
18306
18301
|
// ── 通用 HTTP 请求 ──
|
|
18307
18302
|
/**
|
|
18308
18303
|
* 统一 HTTP 请求封装 — 含超时、日志脱敏和错误处理
|
|
18309
|
-
* @param method - HTTP 方法
|
|
18310
|
-
* @param path - 请求路径 (如 '/seal/client/register')
|
|
18311
|
-
* @param options - 请求选项
|
|
18312
|
-
* @returns 解析后的 JSON 响应
|
|
18313
|
-
* @throws SealApiError — HTTP 非 2xx 响应
|
|
18314
18304
|
*/
|
|
18315
18305
|
async request(method, path2, options = {}) {
|
|
18316
18306
|
const url2 = `${this.baseUrl}${path2}`;
|
|
@@ -18342,109 +18332,38 @@ var SealClient = class {
|
|
|
18342
18332
|
parsedBody = responseBody;
|
|
18343
18333
|
}
|
|
18344
18334
|
log7.error("HTTP error", { method, url: url2, status: response.status });
|
|
18345
|
-
throw new
|
|
18335
|
+
throw new ApiError(response.status, parsedBody, path2);
|
|
18346
18336
|
}
|
|
18347
18337
|
log7.debug("HTTP response", { method, url: url2, status: response.status });
|
|
18348
18338
|
try {
|
|
18349
18339
|
return JSON.parse(responseBody);
|
|
18350
18340
|
} catch {
|
|
18351
|
-
throw new
|
|
18341
|
+
throw new ApiError(response.status, responseBody, path2);
|
|
18352
18342
|
}
|
|
18353
18343
|
}
|
|
18354
18344
|
// ── 公共方法 ──
|
|
18355
|
-
/** 判断是否已注册 (有 clientId + clientSecret) */
|
|
18356
|
-
isRegistered() {
|
|
18357
|
-
return !!(this.clientId && this.clientSecret);
|
|
18358
|
-
}
|
|
18359
|
-
/** 更新 clientId / clientSecret (注册成功后或从持久化加载后调用) */
|
|
18360
|
-
setCredentials(clientId, clientSecret) {
|
|
18361
|
-
this.clientId = clientId;
|
|
18362
|
-
this.clientSecret = clientSecret;
|
|
18363
|
-
log7.info("Credentials updated", { clientId: sanitize(clientId) });
|
|
18364
|
-
}
|
|
18365
|
-
/**
|
|
18366
|
-
* 动态注册客户端 → POST /seal/client/register
|
|
18367
|
-
*
|
|
18368
|
-
* 首次运行时自动调用,获取 clientId 和 clientSecret。
|
|
18369
|
-
* 注册成功后自动更新内部凭据。
|
|
18370
|
-
*/
|
|
18371
|
-
async register(params) {
|
|
18372
|
-
const body = {
|
|
18373
|
-
client_name: params.clientName,
|
|
18374
|
-
scopes: params.scopes,
|
|
18375
|
-
authentication_methods: params.authMethods ?? ["client_secret_post"],
|
|
18376
|
-
grant_types: params.grantTypes ?? ["authorization_code", "client_credentials", "refresh_token"],
|
|
18377
|
-
redirect_uris: params.redirectUris ?? ["https://placeholder.local"],
|
|
18378
|
-
logout_redirect_uris: [],
|
|
18379
|
-
client_settings: {
|
|
18380
|
-
"settings.client.require-authorization-consent": true
|
|
18381
|
-
}
|
|
18382
|
-
};
|
|
18383
|
-
if (params.tokenSettings) {
|
|
18384
|
-
const ts = {};
|
|
18385
|
-
if (params.tokenSettings.accessTokenTtl) {
|
|
18386
|
-
ts["settings.token.access-token-time-to-live"] = params.tokenSettings.accessTokenTtl;
|
|
18387
|
-
}
|
|
18388
|
-
if (params.tokenSettings.refreshTokenTtl) {
|
|
18389
|
-
ts["settings.token.refresh-token-time-to-live"] = params.tokenSettings.refreshTokenTtl;
|
|
18390
|
-
}
|
|
18391
|
-
if (params.tokenSettings.accessTokenFormat) {
|
|
18392
|
-
ts["settings.token.access-token-format"] = params.tokenSettings.accessTokenFormat;
|
|
18393
|
-
}
|
|
18394
|
-
if (params.tokenSettings.reuseRefreshTokens !== void 0) {
|
|
18395
|
-
ts["settings.token.reuse-refresh-tokens"] = params.tokenSettings.reuseRefreshTokens;
|
|
18396
|
-
}
|
|
18397
|
-
ts["settings.token.authorization-code-time-to-live"] = "300";
|
|
18398
|
-
body.token_settings = ts;
|
|
18399
|
-
}
|
|
18400
|
-
log7.info("Registering OAuth client", { clientName: params.clientName });
|
|
18401
|
-
const response = await this.request(
|
|
18402
|
-
"POST",
|
|
18403
|
-
"/seal/client/register",
|
|
18404
|
-
{ body, contentType: "json" }
|
|
18405
|
-
);
|
|
18406
|
-
const registration = {
|
|
18407
|
-
clientId: response.client_id,
|
|
18408
|
-
clientSecret: response.client_secret,
|
|
18409
|
-
clientSecretExpiresAt: response.client_secret_expires_at ?? "",
|
|
18410
|
-
clientIdIssuedAt: response.client_id_issued_at ?? "",
|
|
18411
|
-
scopes: response.scopes ?? params.scopes
|
|
18412
|
-
};
|
|
18413
|
-
this.setCredentials(registration.clientId, registration.clientSecret);
|
|
18414
|
-
log7.info("OAuth client registered", {
|
|
18415
|
-
clientId: sanitize(registration.clientId),
|
|
18416
|
-
clientSecret: sanitize(registration.clientSecret),
|
|
18417
|
-
scopes: registration.scopes,
|
|
18418
|
-
expiresAt: registration.clientSecretExpiresAt
|
|
18419
|
-
});
|
|
18420
|
-
return registration;
|
|
18421
|
-
}
|
|
18422
18345
|
/**
|
|
18423
|
-
* 获取访问令牌 → POST /
|
|
18346
|
+
* 获取访问令牌 → POST /auth/token (client_credentials)
|
|
18424
18347
|
*
|
|
18425
|
-
*
|
|
18426
|
-
*
|
|
18348
|
+
* 请求参数 (x-www-form-urlencoded):
|
|
18349
|
+
* - grant_type: client_credentials (写死)
|
|
18350
|
+
* - client_id: appId
|
|
18351
|
+
* - client_secret: appSecret
|
|
18352
|
+
* - scope: client_credentials refresh_token (写死)
|
|
18427
18353
|
*
|
|
18428
|
-
* @param scope - 请求的权限范围 (空格分隔),不传则使用注册时的全部 scope
|
|
18429
18354
|
* @returns TokenData 包含 access_token、过期时间等
|
|
18430
|
-
* @throws
|
|
18431
|
-
* @throws SealApiError — HTTP 错误
|
|
18355
|
+
* @throws ApiError — HTTP 错误
|
|
18432
18356
|
*/
|
|
18433
|
-
async getToken(
|
|
18434
|
-
if (!this.clientId || !this.clientSecret) {
|
|
18435
|
-
throw new Error("Cannot get token: clientId and clientSecret are required. Call register() or setCredentials() first.");
|
|
18436
|
-
}
|
|
18357
|
+
async getToken() {
|
|
18437
18358
|
const params = new URLSearchParams();
|
|
18438
|
-
params.set("grant_type",
|
|
18439
|
-
params.set("client_id", this.
|
|
18440
|
-
params.set("client_secret", this.
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
}
|
|
18444
|
-
log7.info("Requesting access token", { clientId: sanitize(this.clientId) });
|
|
18359
|
+
params.set("grant_type", GRANT_TYPE);
|
|
18360
|
+
params.set("client_id", this.appId);
|
|
18361
|
+
params.set("client_secret", this.appSecret);
|
|
18362
|
+
params.set("scope", SCOPE);
|
|
18363
|
+
log7.info("Requesting access token", { appId: sanitize(this.appId) });
|
|
18445
18364
|
const response = await this.request(
|
|
18446
18365
|
"POST",
|
|
18447
|
-
"/
|
|
18366
|
+
"/auth/token",
|
|
18448
18367
|
{ body: params, contentType: "form" }
|
|
18449
18368
|
);
|
|
18450
18369
|
const now = Date.now();
|
|
@@ -18455,7 +18374,6 @@ var SealClient = class {
|
|
|
18455
18374
|
expiresIn,
|
|
18456
18375
|
scope: response.scope ?? "",
|
|
18457
18376
|
refreshToken: void 0,
|
|
18458
|
-
// Seal 不返回 refresh_token
|
|
18459
18377
|
grantedAt: now,
|
|
18460
18378
|
expiresAt: now + expiresIn * 1e3
|
|
18461
18379
|
};
|
|
@@ -18466,12 +18384,9 @@ var SealClient = class {
|
|
|
18466
18384
|
});
|
|
18467
18385
|
return tokenData;
|
|
18468
18386
|
}
|
|
18469
|
-
/**
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
*/
|
|
18473
|
-
async introspect(_token) {
|
|
18474
|
-
throw new Error("introspect() not implemented yet \u2014 Seal introspect API \u6682\u4E0D\u4F7F\u7528");
|
|
18387
|
+
/** 获取 baseUrl (供其他模块复用) */
|
|
18388
|
+
getBaseUrl() {
|
|
18389
|
+
return this.baseUrl;
|
|
18475
18390
|
}
|
|
18476
18391
|
};
|
|
18477
18392
|
|
|
@@ -18605,12 +18520,10 @@ var DEFAULT_REFRESH_AHEAD_MS = 5 * 60 * 1e3;
|
|
|
18605
18520
|
var MAX_RETRIES = 3;
|
|
18606
18521
|
var RETRY_BASE_MS = 1e3;
|
|
18607
18522
|
var TokenManager = class {
|
|
18608
|
-
|
|
18523
|
+
oauthClient;
|
|
18609
18524
|
tokenStore;
|
|
18610
18525
|
/** Token 过期前提前刷新的时间 (ms) */
|
|
18611
18526
|
refreshAheadMs;
|
|
18612
|
-
/** 自动注册的参数 */
|
|
18613
|
-
autoRegisterParams;
|
|
18614
18527
|
// ── 内存缓存 ──
|
|
18615
18528
|
/** 内存中缓存的 access_token */
|
|
18616
18529
|
cachedToken = null;
|
|
@@ -18624,10 +18537,9 @@ var TokenManager = class {
|
|
|
18624
18537
|
/** 定时刷新器句柄 */
|
|
18625
18538
|
refreshTimer = null;
|
|
18626
18539
|
constructor(deps) {
|
|
18627
|
-
this.
|
|
18540
|
+
this.oauthClient = deps.oauthClient;
|
|
18628
18541
|
this.tokenStore = deps.tokenStore;
|
|
18629
18542
|
this.refreshAheadMs = deps.refreshAheadMs ?? DEFAULT_REFRESH_AHEAD_MS;
|
|
18630
|
-
this.autoRegisterParams = deps.autoRegisterParams;
|
|
18631
18543
|
log9.info("TokenManager initialized", { refreshAheadMs: this.refreshAheadMs });
|
|
18632
18544
|
}
|
|
18633
18545
|
// ── 核心方法 ──
|
|
@@ -18656,13 +18568,6 @@ var TokenManager = class {
|
|
|
18656
18568
|
this.refreshLock = null;
|
|
18657
18569
|
}
|
|
18658
18570
|
}
|
|
18659
|
-
/**
|
|
18660
|
-
* 令牌自省 — 暂不实现,后续补充。
|
|
18661
|
-
* 返回的 identity_id 用于 gate.ts 的 anti-loop 检查 (bot 自己的 ID)。
|
|
18662
|
-
*/
|
|
18663
|
-
async introspect() {
|
|
18664
|
-
throw new Error("introspect() not implemented yet \u2014 Seal introspect API \u6682\u4E0D\u4F7F\u7528");
|
|
18665
|
-
}
|
|
18666
18571
|
/** 检查当前令牌是否包含指定的 scope */
|
|
18667
18572
|
hasScope(scope) {
|
|
18668
18573
|
if (!this.cachedScope) return false;
|
|
@@ -18693,14 +18598,14 @@ var TokenManager = class {
|
|
|
18693
18598
|
*
|
|
18694
18599
|
* 重试策略:
|
|
18695
18600
|
* - 网络错误 / 5xx → 重试 (最多 3 次)
|
|
18696
|
-
* - 401/403 → 不重试 (
|
|
18601
|
+
* - 401/403 → 不重试 (凭证可能无效)
|
|
18697
18602
|
*/
|
|
18698
18603
|
async _acquireTokenWithRetry() {
|
|
18699
18604
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
18700
18605
|
try {
|
|
18701
18606
|
return await this._acquireToken();
|
|
18702
18607
|
} catch (err) {
|
|
18703
|
-
if (err instanceof
|
|
18608
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
18704
18609
|
log9.error("Token acquire failed with auth error, not retrying", { status: err.status });
|
|
18705
18610
|
throw err;
|
|
18706
18611
|
}
|
|
@@ -18728,10 +18633,9 @@ var TokenManager = class {
|
|
|
18728
18633
|
*
|
|
18729
18634
|
* 流程:
|
|
18730
18635
|
* 1. 尝试从 TokenStore 加载缓存
|
|
18731
|
-
* 2.
|
|
18732
|
-
* 3.
|
|
18733
|
-
* 4.
|
|
18734
|
-
* 5. 设置定时刷新器
|
|
18636
|
+
* 2. 调用 OAuthClient.getToken() 获取新 Token
|
|
18637
|
+
* 3. 保存到内存缓存 + TokenStore
|
|
18638
|
+
* 4. 设置定时刷新器
|
|
18735
18639
|
*/
|
|
18736
18640
|
async _acquireToken() {
|
|
18737
18641
|
try {
|
|
@@ -18748,43 +18652,8 @@ var TokenManager = class {
|
|
|
18748
18652
|
} catch {
|
|
18749
18653
|
log9.warn("Failed to load token from store, acquiring new one");
|
|
18750
18654
|
}
|
|
18751
|
-
|
|
18752
|
-
|
|
18753
|
-
const savedCreds = await this.tokenStore.load("credentials");
|
|
18754
|
-
if (savedCreds?.clientId && savedCreds?.clientSecret) {
|
|
18755
|
-
this.sealClient.setCredentials(
|
|
18756
|
-
savedCreds.clientId,
|
|
18757
|
-
savedCreds.clientSecret
|
|
18758
|
-
);
|
|
18759
|
-
log9.info("Credentials loaded from store");
|
|
18760
|
-
}
|
|
18761
|
-
} catch {
|
|
18762
|
-
log9.debug("No saved credentials found");
|
|
18763
|
-
}
|
|
18764
|
-
}
|
|
18765
|
-
if (!this.sealClient.isRegistered()) {
|
|
18766
|
-
if (!this.autoRegisterParams) {
|
|
18767
|
-
throw new Error("SealClient not registered and autoRegisterParams not provided. Call setCredentials() or enable autoRegister.");
|
|
18768
|
-
}
|
|
18769
|
-
log9.info("Auto-registering OAuth client");
|
|
18770
|
-
const registration = await this.sealClient.register({
|
|
18771
|
-
clientName: this.autoRegisterParams.clientName,
|
|
18772
|
-
scopes: this.autoRegisterParams.scopes,
|
|
18773
|
-
tokenSettings: this.autoRegisterParams.tokenSettings ? {
|
|
18774
|
-
accessTokenTtl: this.autoRegisterParams.tokenSettings.accessTokenTtl,
|
|
18775
|
-
refreshTokenTtl: this.autoRegisterParams.tokenSettings.refreshTokenTtl,
|
|
18776
|
-
accessTokenFormat: this.autoRegisterParams.tokenSettings.accessTokenFormat
|
|
18777
|
-
} : void 0
|
|
18778
|
-
});
|
|
18779
|
-
await this.tokenStore.save("credentials", {
|
|
18780
|
-
clientId: registration.clientId,
|
|
18781
|
-
clientSecret: registration.clientSecret,
|
|
18782
|
-
clientSecretExpiresAt: registration.clientSecretExpiresAt
|
|
18783
|
-
});
|
|
18784
|
-
log9.info("OAuth client registered and credentials saved");
|
|
18785
|
-
}
|
|
18786
|
-
log9.info("Acquiring new access token");
|
|
18787
|
-
const tokenData = await this.sealClient.getToken();
|
|
18655
|
+
log9.info("Acquiring new access token via POST /auth/token");
|
|
18656
|
+
const tokenData = await this.oauthClient.getToken();
|
|
18788
18657
|
this.updateCache(tokenData);
|
|
18789
18658
|
await this.tokenStore.save("token", tokenData);
|
|
18790
18659
|
this.scheduleRefresh(tokenData);
|
|
@@ -19141,11 +19010,18 @@ var MessagePipe = class {
|
|
|
19141
19010
|
log12.warn("inbound:data-parse-error", { error: err.message });
|
|
19142
19011
|
return;
|
|
19143
19012
|
}
|
|
19144
|
-
if (callbackData.eventType !== "
|
|
19013
|
+
if (callbackData.eventType !== "callback:direct") {
|
|
19145
19014
|
log12.debug("inbound:event-ignored", { eventType: callbackData.eventType });
|
|
19146
19015
|
return;
|
|
19147
19016
|
}
|
|
19148
|
-
const msg =
|
|
19017
|
+
const msg = {
|
|
19018
|
+
messageId: callbackData.msgUid,
|
|
19019
|
+
chatId: callbackData.groupId || callbackData.userId,
|
|
19020
|
+
senderId: callbackData.userId,
|
|
19021
|
+
msgType: callbackData.type,
|
|
19022
|
+
content: JSON.stringify(callbackData.content),
|
|
19023
|
+
timestamp: Date.now()
|
|
19024
|
+
};
|
|
19149
19025
|
log12.debug("inbound:frame", { messageId: msg.messageId, eventType: callbackData.eventType });
|
|
19150
19026
|
if (this.dedup.isDuplicate(msg.messageId)) {
|
|
19151
19027
|
log12.debug("inbound:dedup-hit", { messageId: msg.messageId });
|
|
@@ -19206,10 +19082,14 @@ var ConnectionManager = class {
|
|
|
19206
19082
|
this.running = true;
|
|
19207
19083
|
this.reconnectAttempts = 0;
|
|
19208
19084
|
const token = await tokenFn();
|
|
19085
|
+
const wsUrl = url2.replace(/\/+$/, "") + "/events/stream";
|
|
19209
19086
|
await this.client.connect({
|
|
19210
|
-
url:
|
|
19087
|
+
url: wsUrl,
|
|
19211
19088
|
token,
|
|
19212
|
-
headers:
|
|
19089
|
+
headers: {
|
|
19090
|
+
"Authorization": token,
|
|
19091
|
+
...this.appId ? { "X-App-ID": this.appId } : {}
|
|
19092
|
+
}
|
|
19213
19093
|
});
|
|
19214
19094
|
this.client.on("close", () => {
|
|
19215
19095
|
if (this.running) {
|
|
@@ -19262,10 +19142,14 @@ var ConnectionManager = class {
|
|
|
19262
19142
|
this.reconnectAttempts++;
|
|
19263
19143
|
try {
|
|
19264
19144
|
const token = await this.tokenFn();
|
|
19145
|
+
const wsUrl = this.url.replace(/\/+$/, "") + "/events/stream";
|
|
19265
19146
|
await this.client.connect({
|
|
19266
|
-
url:
|
|
19147
|
+
url: wsUrl,
|
|
19267
19148
|
token,
|
|
19268
|
-
headers:
|
|
19149
|
+
headers: {
|
|
19150
|
+
"Authorization": token,
|
|
19151
|
+
...this.appId ? { "X-App-ID": this.appId } : {}
|
|
19152
|
+
}
|
|
19269
19153
|
});
|
|
19270
19154
|
this.reconnectAttempts = 0;
|
|
19271
19155
|
this.startHeartbeat();
|
|
@@ -19738,28 +19622,17 @@ async function startPlugin(accountConfig, internalOverrides) {
|
|
|
19738
19622
|
cryptoEngine = CryptoEngine.createPassthrough();
|
|
19739
19623
|
log16.info("Crypto passthrough mode \u2713 (disabled by config)");
|
|
19740
19624
|
}
|
|
19741
|
-
const
|
|
19742
|
-
|
|
19743
|
-
|
|
19744
|
-
|
|
19745
|
-
clientSecret: config2.auth.clientSecret,
|
|
19746
|
-
scopes: config2.auth.scopes
|
|
19625
|
+
const oauthClient = new OAuthClient({
|
|
19626
|
+
baseUrl: config2.auth.serverUrl,
|
|
19627
|
+
appId: accountConfig.appId,
|
|
19628
|
+
appSecret: accountConfig.appSecret
|
|
19747
19629
|
});
|
|
19748
19630
|
const tokenStorePath = (0, import_node_path2.join)((0, import_node_os.homedir)(), TOKEN_STORE_DIR, "tokens");
|
|
19749
19631
|
const tokenStore = new TokenStore(tokenStorePath, cryptoEngine);
|
|
19750
19632
|
const tokenManager = new TokenManager({
|
|
19751
|
-
|
|
19633
|
+
oauthClient,
|
|
19752
19634
|
tokenStore,
|
|
19753
|
-
refreshAheadMs: config2.auth.refreshAheadMs
|
|
19754
|
-
autoRegisterParams: config2.auth.autoRegister ? {
|
|
19755
|
-
clientName: config2.auth.clientName,
|
|
19756
|
-
scopes: config2.auth.scopes,
|
|
19757
|
-
tokenSettings: config2.auth.tokenSettings ? {
|
|
19758
|
-
accessTokenTtl: config2.auth.tokenSettings.accessTokenTtl,
|
|
19759
|
-
refreshTokenTtl: config2.auth.tokenSettings.refreshTokenTtl,
|
|
19760
|
-
accessTokenFormat: config2.auth.tokenSettings.accessTokenFormat
|
|
19761
|
-
} : void 0
|
|
19762
|
-
} : void 0
|
|
19635
|
+
refreshAheadMs: config2.auth.refreshAheadMs
|
|
19763
19636
|
});
|
|
19764
19637
|
log16.info("Auth initialized \u2713");
|
|
19765
19638
|
let pushQueue = null;
|
|
@@ -19844,20 +19717,20 @@ ${body}` : body || raw.content;
|
|
|
19844
19717
|
}
|
|
19845
19718
|
case "image": {
|
|
19846
19719
|
const c = parsed;
|
|
19847
|
-
fileId = c?.
|
|
19720
|
+
fileId = c?.fileId;
|
|
19848
19721
|
text = c?.altText ?? (fileId ? `` : "[image]");
|
|
19849
19722
|
break;
|
|
19850
19723
|
}
|
|
19851
19724
|
case "file": {
|
|
19852
19725
|
const c = parsed;
|
|
19853
|
-
fileId = c?.
|
|
19854
|
-
fileName = c?.
|
|
19726
|
+
fileId = c?.fileId;
|
|
19727
|
+
fileName = c?.fileName;
|
|
19855
19728
|
text = fileName ? `[File: ${fileName}]` : "[file]";
|
|
19856
19729
|
break;
|
|
19857
19730
|
}
|
|
19858
19731
|
case "voice": {
|
|
19859
19732
|
const c = parsed;
|
|
19860
|
-
fileId = c?.
|
|
19733
|
+
fileId = c?.fileId;
|
|
19861
19734
|
const durationMs = c?.duration;
|
|
19862
19735
|
const durationStr = durationMs != null ? ` ${(durationMs / 1e3).toFixed(1)}s` : "";
|
|
19863
19736
|
text = `[Voice${durationStr}]`;
|
|
@@ -19865,7 +19738,7 @@ ${body}` : body || raw.content;
|
|
|
19865
19738
|
}
|
|
19866
19739
|
case "video": {
|
|
19867
19740
|
const c = parsed;
|
|
19868
|
-
fileId = c?.
|
|
19741
|
+
fileId = c?.fileId;
|
|
19869
19742
|
const durationMs = c?.duration;
|
|
19870
19743
|
const durationStr = durationMs != null ? ` ${(durationMs / 1e3).toFixed(1)}s` : "";
|
|
19871
19744
|
text = `[Video${durationStr}]`;
|