@jeik/dingtalk-connector 0.8.21
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/CHANGELOG.md +684 -0
- package/LICENSE +21 -0
- package/README.en.md +179 -0
- package/README.md +219 -0
- package/bin/dingtalk-connector.js +838 -0
- package/bin/wizard-config.mjs +94 -0
- package/dist/accounts-BAzdqkAV.mjs +268 -0
- package/dist/accounts-BQptOmgB.mjs +2 -0
- package/dist/chunk-upload-BBQgGtcZ.mjs +193 -0
- package/dist/chunk-upload-DaLXXZH3.mjs +2 -0
- package/dist/common-C8pYKU_y.mjs +2 -0
- package/dist/common-Dt9n6fQN.mjs +101 -0
- package/dist/connection-DHHFFNQJ.mjs +423 -0
- package/dist/entry-bundled.d.mts +16 -0
- package/dist/entry-bundled.mjs +31 -0
- package/dist/game-xiyou-CqHt-6Q1.mjs +4271 -0
- package/dist/gateway-methods-C4tcgI7P.mjs +771 -0
- package/dist/gateway-methods-Ci31A3vg.mjs +2 -0
- package/dist/http-client-CpnJHB89.mjs +2 -0
- package/dist/http-client-DFWZgO1n.mjs +33 -0
- package/dist/index.d.mts +193 -0
- package/dist/index.mjs +45 -0
- package/dist/logger-BmJkQkm1.mjs +2 -0
- package/dist/logger-mZ9OSbmD.mjs +58 -0
- package/dist/media-C_SVin7s.mjs +2 -0
- package/dist/media-cz72EVS3.mjs +509 -0
- package/dist/message-handler-DESzFFDc.mjs +1971 -0
- package/dist/messaging-B6l1sRvX.mjs +1044 -0
- package/dist/runtime-DUgpo5zC.mjs +1422 -0
- package/dist/session-DJ4jYqPv.mjs +114 -0
- package/dist/utils-Bjh4r_qS.mjs +4 -0
- package/dist/utils-CIfI_3Jh.mjs +63 -0
- package/dist/utils-legacy-CALCPP1t.mjs +230 -0
- package/dist/utils-legacy-CFYDBM4r.mjs +3 -0
- package/docs/DEAP_AGENT_GUIDE.en.md +115 -0
- package/docs/DEAP_AGENT_GUIDE.md +115 -0
- package/docs/DINGTALK_MANUAL_SETUP.md +50 -0
- package/docs/MULTI_AGENT_SETUP.md +306 -0
- package/docs/RELEASE_NOTES_V0.7.10.md +40 -0
- package/docs/RELEASE_NOTES_V0.7.2.md +143 -0
- package/docs/RELEASE_NOTES_V0.7.3.md +149 -0
- package/docs/RELEASE_NOTES_V0.7.4.md +206 -0
- package/docs/RELEASE_NOTES_V0.7.5.md +267 -0
- package/docs/RELEASE_NOTES_V0.7.6.md +219 -0
- package/docs/RELEASE_NOTES_V0.7.7.md +122 -0
- package/docs/RELEASE_NOTES_V0.7.8.md +101 -0
- package/docs/RELEASE_NOTES_V0.7.9.md +65 -0
- package/docs/RELEASE_NOTES_V0.8.0.md +53 -0
- package/docs/RELEASE_NOTES_V0.8.1.md +47 -0
- package/docs/RELEASE_NOTES_V0.8.10.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.11.md +51 -0
- package/docs/RELEASE_NOTES_V0.8.12.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.13-beta.0.md +69 -0
- package/docs/RELEASE_NOTES_V0.8.13.md +62 -0
- package/docs/RELEASE_NOTES_V0.8.14.md +86 -0
- package/docs/RELEASE_NOTES_V0.8.16.md +40 -0
- package/docs/RELEASE_NOTES_V0.8.17.md +87 -0
- package/docs/RELEASE_NOTES_V0.8.18.md +64 -0
- package/docs/RELEASE_NOTES_V0.8.19.md +62 -0
- package/docs/RELEASE_NOTES_V0.8.2.md +55 -0
- package/docs/RELEASE_NOTES_V0.8.20.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.3.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.4.md +45 -0
- package/docs/RELEASE_NOTES_V0.8.7.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.8.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.9.md +81 -0
- package/docs/RELEASE_NOTES_v0.7.0.md +142 -0
- package/docs/RELEASE_NOTES_v0.7.1.md +74 -0
- package/docs/TROUBLESHOOTING.md +122 -0
- package/index.ts +77 -0
- package/openclaw.plugin.json +551 -0
- package/package.json +147 -0
- package/skills/dingtalk-channel-rules/SKILL.md +91 -0
- package/skills/dingtalk-troubleshoot/SKILL.md +93 -0
- package/skills/dws-cli/SKILL.md +129 -0
- package/skills/dws-cli/references/error-codes.md +95 -0
- package/skills/dws-cli/references/field-rules.md +105 -0
- package/skills/dws-cli/references/global-reference.md +104 -0
- package/skills/dws-cli/references/intent-guide.md +114 -0
- package/skills/dws-cli/references/products/aitable.md +452 -0
- package/skills/dws-cli/references/products/attendance.md +93 -0
- package/skills/dws-cli/references/products/calendar.md +217 -0
- package/skills/dws-cli/references/products/chat.md +292 -0
- package/skills/dws-cli/references/products/contact.md +108 -0
- package/skills/dws-cli/references/products/ding.md +57 -0
- package/skills/dws-cli/references/products/report.md +162 -0
- package/skills/dws-cli/references/products/simple.md +128 -0
- package/skills/dws-cli/references/products/todo.md +138 -0
- package/skills/dws-cli/references/products/workbench.md +39 -0
- package/skills/dws-cli/references/recovery-guide.md +94 -0
- package/src/channel.ts +588 -0
- package/src/config/accounts.ts +242 -0
- package/src/config/schema.ts +180 -0
- package/src/core/connection.ts +741 -0
- package/src/core/message-handler.ts +1788 -0
- package/src/core/provider.ts +111 -0
- package/src/core/state.ts +54 -0
- package/src/device-auth-config.ts +14 -0
- package/src/device-auth.ts +197 -0
- package/src/directory.ts +95 -0
- package/src/docs.ts +293 -0
- package/src/game-xiyou/achievement-engine.ts +252 -0
- package/src/game-xiyou/bounty-system.ts +315 -0
- package/src/game-xiyou/commands.ts +223 -0
- package/src/game-xiyou/drop-engine.ts +241 -0
- package/src/game-xiyou/encounter-system.ts +135 -0
- package/src/game-xiyou/escape-engine.ts +164 -0
- package/src/game-xiyou/exp-calculator.ts +139 -0
- package/src/game-xiyou/index.ts +479 -0
- package/src/game-xiyou/level-system.ts +91 -0
- package/src/game-xiyou/monster-pool.ts +180 -0
- package/src/game-xiyou/pity-counter.ts +114 -0
- package/src/game-xiyou/random-event-engine.ts +648 -0
- package/src/game-xiyou/renderer.ts +679 -0
- package/src/game-xiyou/storage.ts +218 -0
- package/src/game-xiyou/treasure-system.ts +105 -0
- package/src/game-xiyou/types.ts +582 -0
- package/src/game-xiyou/uid-resolver.ts +49 -0
- package/src/gateway-methods.ts +740 -0
- package/src/onboarding.ts +553 -0
- package/src/policy.ts +32 -0
- package/src/probe.ts +210 -0
- package/src/reply-dispatcher.ts +874 -0
- package/src/runtime.ts +32 -0
- package/src/sdk/helpers.ts +322 -0
- package/src/sdk/types.ts +519 -0
- package/src/secret-input.ts +19 -0
- package/src/services/media/audio.ts +54 -0
- package/src/services/media/chunk-upload.ts +296 -0
- package/src/services/media/common.ts +155 -0
- package/src/services/media/file.ts +75 -0
- package/src/services/media/image.ts +81 -0
- package/src/services/media/index.ts +10 -0
- package/src/services/media/video.ts +162 -0
- package/src/services/media.ts +1143 -0
- package/src/services/messaging/card.ts +604 -0
- package/src/services/messaging/index.ts +18 -0
- package/src/services/messaging/mentions.ts +267 -0
- package/src/services/messaging/send.ts +141 -0
- package/src/services/messaging.ts +1191 -0
- package/src/services/reply-markers.ts +55 -0
- package/src/targets.ts +45 -0
- package/src/types/index.ts +59 -0
- package/src/types/pdf-parse.d.ts +3 -0
- package/src/utils/agent.ts +63 -0
- package/src/utils/async.ts +51 -0
- package/src/utils/constants.ts +27 -0
- package/src/utils/http-client.ts +38 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +78 -0
- package/src/utils/session.ts +147 -0
- package/src/utils/token.ts +93 -0
- package/src/utils/utils-legacy.ts +454 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// 钉钉安装向导的配置操作 —— 纯函数(不碰 IO/process),便于单测。
|
|
2
|
+
// channelId 由调用方传入(= "dingtalk-connector")。
|
|
3
|
+
//
|
|
4
|
+
// 配置结构(openclaw.json):
|
|
5
|
+
// channels.<id>.accounts.<accountId> = { enabled, name, clientId, clientSecret, cardTemplateId?, cardContentVar? }
|
|
6
|
+
// bindings[] = { agentId, match: { channel, accountId } }
|
|
7
|
+
// 早期向导写过“扁平单机器人”结构(channels.<id>.clientId 直接挂在渠道上),这里会按需迁成 accounts 结构。
|
|
8
|
+
|
|
9
|
+
/** 列出现有非空的钉钉机器人账号(accounts 结构 + 兼容扁平结构)。返回 [{id, clientId, flat?}]。 */
|
|
10
|
+
export function dingtalkAccountSummaries(cfg, channelId) {
|
|
11
|
+
const ch = cfg?.channels?.[channelId];
|
|
12
|
+
const out = [];
|
|
13
|
+
if (!ch || typeof ch !== "object") return out;
|
|
14
|
+
const accounts = ch.accounts && typeof ch.accounts === "object" ? ch.accounts : {};
|
|
15
|
+
for (const [id, a] of Object.entries(accounts)) {
|
|
16
|
+
if (a && String(a.clientId || "").trim()) out.push({ id, clientId: String(a.clientId) });
|
|
17
|
+
}
|
|
18
|
+
if (String(ch.clientId || "").trim()) {
|
|
19
|
+
out.push({ id: ch.name || "apibot", clientId: String(ch.clientId), flat: true });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 确保插件条目启用。 */
|
|
25
|
+
export function ensurePluginEnabled(cfg, channelId) {
|
|
26
|
+
cfg.plugins ??= {};
|
|
27
|
+
cfg.plugins.entries ??= {};
|
|
28
|
+
cfg.plugins.entries[channelId] ??= {};
|
|
29
|
+
cfg.plugins.entries[channelId].enabled = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function uniqueAccountId(ch, base = "apibot") {
|
|
33
|
+
const used = new Set(Object.keys(ch.accounts || {}));
|
|
34
|
+
if (ch.name) used.add(ch.name);
|
|
35
|
+
if (!used.has(base)) return base;
|
|
36
|
+
let i = 2;
|
|
37
|
+
while (used.has(base + i)) i++;
|
|
38
|
+
return base + i;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 把旧的“扁平单机器人”结构迁成 accounts 结构(保留凭证,并给它补一条 binding 默认 main,避免新增后路由丢失)。 */
|
|
42
|
+
export function migrateFlatToAccounts(cfg, channelId) {
|
|
43
|
+
const ch = cfg?.channels?.[channelId];
|
|
44
|
+
if (!ch) return;
|
|
45
|
+
ch.accounts ??= {};
|
|
46
|
+
if (String(ch.clientId || "").trim()) {
|
|
47
|
+
const id = ch.name || "apibot";
|
|
48
|
+
ch.accounts[id] ??= {
|
|
49
|
+
enabled: true,
|
|
50
|
+
name: id,
|
|
51
|
+
clientId: ch.clientId,
|
|
52
|
+
clientSecret: ch.clientSecret,
|
|
53
|
+
...(ch.cardTemplateId ? { cardTemplateId: ch.cardTemplateId } : {}),
|
|
54
|
+
...(ch.cardContentVar ? { cardContentVar: ch.cardContentVar } : {}),
|
|
55
|
+
};
|
|
56
|
+
cfg.bindings ??= [];
|
|
57
|
+
const bound = cfg.bindings.some((b) => String(b?.match?.channel) === channelId);
|
|
58
|
+
if (!bound) cfg.bindings.push({ agentId: "main", match: { channel: channelId, accountId: id } });
|
|
59
|
+
delete ch.clientId;
|
|
60
|
+
delete ch.clientSecret;
|
|
61
|
+
delete ch.cardTemplateId;
|
|
62
|
+
delete ch.cardContentVar;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 新增一个机器人账号(不动现有账号),自动补一条 binding 到指定 agent。返回新账号 id。 */
|
|
67
|
+
export function addBotAccount(cfg, channelId, { clientId, clientSecret, agentId }) {
|
|
68
|
+
cfg.channels ??= {};
|
|
69
|
+
cfg.channels[channelId] ??= {};
|
|
70
|
+
const ch = cfg.channels[channelId];
|
|
71
|
+
ch.enabled = true;
|
|
72
|
+
migrateFlatToAccounts(cfg, channelId);
|
|
73
|
+
ch.accounts ??= {};
|
|
74
|
+
const id = uniqueAccountId(ch);
|
|
75
|
+
ch.accounts[id] = { enabled: true, name: id, clientId, clientSecret };
|
|
76
|
+
cfg.bindings ??= [];
|
|
77
|
+
cfg.bindings.push({ agentId: agentId || "main", match: { channel: channelId, accountId: id } });
|
|
78
|
+
ensurePluginEnabled(cfg, channelId);
|
|
79
|
+
return id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 覆盖:把钉钉渠道重置为单个机器人 + 单条 binding(清掉本渠道其它账号/绑定,其它渠道的 binding 保留)。 */
|
|
83
|
+
export function overwriteWithSingleBot(cfg, channelId, { clientId, clientSecret, agentId }) {
|
|
84
|
+
cfg.channels ??= {};
|
|
85
|
+
cfg.channels[channelId] = {
|
|
86
|
+
enabled: true,
|
|
87
|
+
accounts: { apibot: { enabled: true, name: "apibot", clientId, clientSecret } },
|
|
88
|
+
};
|
|
89
|
+
const others = (Array.isArray(cfg.bindings) ? cfg.bindings : []).filter(
|
|
90
|
+
(b) => String(b?.match?.channel) !== channelId,
|
|
91
|
+
);
|
|
92
|
+
cfg.bindings = [...others, { agentId: agentId || "main", match: { channel: channelId, accountId: "apibot" } }];
|
|
93
|
+
ensurePluginEnabled(cfg, channelId);
|
|
94
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
//#region src/sdk/helpers.ts
|
|
2
|
+
/**
|
|
3
|
+
* 默认账号 ID
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_ACCOUNT_ID = "__default__";
|
|
6
|
+
/**
|
|
7
|
+
* 规范化账号 ID
|
|
8
|
+
*
|
|
9
|
+
* 注意:账号 ID 保留原始大小写,仅做 trim 处理。
|
|
10
|
+
* 不做 toLowerCase,因为配置文件中的 accounts key 是大小写敏感的,
|
|
11
|
+
* 如 "zhizaoDashuIP" 与 "zhizaodashuip" 是不同的账号。
|
|
12
|
+
* 特殊值 "default"(不区分大小写)和空字符串映射到 DEFAULT_ACCOUNT_ID。
|
|
13
|
+
*/
|
|
14
|
+
function normalizeAccountId(accountId) {
|
|
15
|
+
const trimmed = accountId.trim();
|
|
16
|
+
if (trimmed.toLowerCase() === "default" || trimmed === "") return DEFAULT_ACCOUNT_ID;
|
|
17
|
+
return trimmed;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 判断是否为 SecretInput 引用
|
|
21
|
+
*/
|
|
22
|
+
function isSecretInputRef(value) {
|
|
23
|
+
if (!value || typeof value !== "object") return false;
|
|
24
|
+
const ref = value;
|
|
25
|
+
return typeof ref.source === "string" && [
|
|
26
|
+
"env",
|
|
27
|
+
"file",
|
|
28
|
+
"exec"
|
|
29
|
+
].includes(ref.source) && typeof ref.provider === "string" && ref.provider.length > 0 && typeof ref.id === "string" && ref.id.length > 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 规范化 SecretInput 字符串
|
|
33
|
+
* 用于显示和日志,会隐藏敏感信息
|
|
34
|
+
*/
|
|
35
|
+
function normalizeSecretInputString(value) {
|
|
36
|
+
if (typeof value === "string") return value.trim() || void 0;
|
|
37
|
+
if (isSecretInputRef(value)) {
|
|
38
|
+
const ref = value;
|
|
39
|
+
return `<${ref.source}:${ref.provider}:${ref.id}>`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 检查 SecretInput 是否已配置
|
|
44
|
+
*/
|
|
45
|
+
function hasConfiguredSecretInput(value) {
|
|
46
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
47
|
+
if (isSecretInputRef(value)) {
|
|
48
|
+
const ref = value;
|
|
49
|
+
if (ref.source === "env") return typeof process.env[ref.id] === "string" && process.env[ref.id].trim().length > 0;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 规范化已解析的 SecretInput 字符串
|
|
56
|
+
* 用于配置验证和错误提示
|
|
57
|
+
*/
|
|
58
|
+
function normalizeResolvedSecretInputString(params) {
|
|
59
|
+
const { value, path } = params;
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
if (trimmed) return trimmed;
|
|
63
|
+
throw new Error(`${path} must be a non-empty string`);
|
|
64
|
+
}
|
|
65
|
+
if (isSecretInputRef(value)) {
|
|
66
|
+
const ref = value;
|
|
67
|
+
if (![
|
|
68
|
+
"env",
|
|
69
|
+
"file",
|
|
70
|
+
"exec"
|
|
71
|
+
].includes(ref.source)) throw new Error(`${path}.source must be one of: env, file, exec`);
|
|
72
|
+
if (typeof ref.provider !== "string" || !ref.provider.trim()) throw new Error(`${path}.provider must be a non-empty string`);
|
|
73
|
+
if (typeof ref.id !== "string" || !ref.id.trim()) throw new Error(`${path}.id must be a non-empty string`);
|
|
74
|
+
if (ref.source === "env") {
|
|
75
|
+
const envValue = process.env[ref.id];
|
|
76
|
+
if (!envValue || !envValue.trim()) throw new Error(`${path}: environment variable ${ref.id} is not set`);
|
|
77
|
+
return envValue.trim();
|
|
78
|
+
}
|
|
79
|
+
return `<${ref.source}:${ref.provider}:${ref.id}>`;
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`${path} must be a string or SecretInput object`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 解析默认群组策略
|
|
85
|
+
*/
|
|
86
|
+
function resolveDefaultGroupPolicy(cfg) {
|
|
87
|
+
return (cfg.channels?.["dingtalk-connector"])?.groupPolicy ?? "open";
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 解析允许列表提供者运行时群组策略
|
|
91
|
+
*/
|
|
92
|
+
function resolveAllowlistProviderRuntimeGroupPolicy(params) {
|
|
93
|
+
const { providerConfigPresent, groupPolicy, defaultGroupPolicy } = params;
|
|
94
|
+
if (groupPolicy) return { groupPolicy };
|
|
95
|
+
if (providerConfigPresent) return { groupPolicy: defaultGroupPolicy };
|
|
96
|
+
return { groupPolicy: "disabled" };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 创建默认通道运行时状态
|
|
100
|
+
*/
|
|
101
|
+
function createDefaultChannelRuntimeState(accountId, extras) {
|
|
102
|
+
return {
|
|
103
|
+
running: false,
|
|
104
|
+
lastStartAt: null,
|
|
105
|
+
lastStopAt: null,
|
|
106
|
+
lastError: null,
|
|
107
|
+
port: null,
|
|
108
|
+
accountId,
|
|
109
|
+
...extras
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 添加通配符到 allowFrom
|
|
114
|
+
*/
|
|
115
|
+
function addWildcardAllowFrom(existing) {
|
|
116
|
+
if (!existing || existing.length === 0) return ["*"];
|
|
117
|
+
if (existing.includes("*")) return existing;
|
|
118
|
+
return [...existing, "*"];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 格式化文档链接
|
|
122
|
+
*/
|
|
123
|
+
function formatDocsLink(path, label) {
|
|
124
|
+
return `https://docs.openclaw.ai${path}`;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/config/accounts.ts
|
|
128
|
+
/**
|
|
129
|
+
* List all configured account IDs from the accounts field.
|
|
130
|
+
*/
|
|
131
|
+
function listConfiguredAccountIds(cfg) {
|
|
132
|
+
const accounts = (cfg.channels?.["dingtalk-connector"])?.accounts;
|
|
133
|
+
if (!accounts || typeof accounts !== "object") return [];
|
|
134
|
+
return Object.keys(accounts).filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* List all DingTalk account IDs.
|
|
138
|
+
* If no accounts are configured, returns [DEFAULT_ACCOUNT_ID] for backward compatibility.
|
|
139
|
+
*/
|
|
140
|
+
function listDingtalkAccountIds(cfg) {
|
|
141
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
142
|
+
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
|
143
|
+
return [...ids].sort((a, b) => a.localeCompare(b));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the default account selection and its source.
|
|
147
|
+
*/
|
|
148
|
+
function resolveDefaultDingtalkAccountSelection(cfg) {
|
|
149
|
+
const preferredRaw = (cfg.channels?.["dingtalk-connector"])?.defaultAccount?.trim();
|
|
150
|
+
const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : void 0;
|
|
151
|
+
if (preferred) return {
|
|
152
|
+
accountId: preferred,
|
|
153
|
+
source: "explicit-default"
|
|
154
|
+
};
|
|
155
|
+
const ids = listDingtalkAccountIds(cfg);
|
|
156
|
+
if (ids.includes("__default__")) return {
|
|
157
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
158
|
+
source: "mapped-default"
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
accountId: ids[0] ?? "__default__",
|
|
162
|
+
source: "fallback"
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the default account ID.
|
|
167
|
+
*/
|
|
168
|
+
function resolveDefaultDingtalkAccountId(cfg) {
|
|
169
|
+
return resolveDefaultDingtalkAccountSelection(cfg).accountId;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the raw account-specific config.
|
|
173
|
+
*/
|
|
174
|
+
function resolveAccountConfig(cfg, accountId) {
|
|
175
|
+
const accounts = (cfg.channels?.["dingtalk-connector"])?.accounts;
|
|
176
|
+
if (!accounts || typeof accounts !== "object") return;
|
|
177
|
+
return accounts[accountId];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Merge top-level config with account-specific config.
|
|
181
|
+
* Account-specific fields override top-level fields.
|
|
182
|
+
*/
|
|
183
|
+
function mergeDingtalkAccountConfig(cfg, accountId) {
|
|
184
|
+
const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...base } = cfg.channels?.["dingtalk-connector"] ?? {};
|
|
185
|
+
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
186
|
+
return {
|
|
187
|
+
...base,
|
|
188
|
+
...account
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function resolveDingtalkCredentials(cfg, options) {
|
|
192
|
+
const normalizeString = (value) => {
|
|
193
|
+
if (typeof value === "number") return String(value);
|
|
194
|
+
if (typeof value !== "string") return;
|
|
195
|
+
const trimmed = value.trim();
|
|
196
|
+
return trimmed ? trimmed : void 0;
|
|
197
|
+
};
|
|
198
|
+
const resolveSecretLike = (value, path) => {
|
|
199
|
+
if (value === void 0 || value === null) return;
|
|
200
|
+
const asString = normalizeString(value);
|
|
201
|
+
if (asString) return asString;
|
|
202
|
+
if (options?.allowUnresolvedSecretRef && typeof value === "object" && value !== null) {
|
|
203
|
+
const rec = value;
|
|
204
|
+
const source = normalizeString(rec.source)?.toLowerCase();
|
|
205
|
+
const id = normalizeString(rec.id);
|
|
206
|
+
if (source === "env" && id) {
|
|
207
|
+
const envValue = normalizeString(process.env[id]);
|
|
208
|
+
if (envValue) return envValue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (options?.allowUnresolvedSecretRef) return normalizeSecretInputString(value);
|
|
212
|
+
return normalizeResolvedSecretInputString({
|
|
213
|
+
value,
|
|
214
|
+
path
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
const clientId = resolveSecretLike(cfg?.clientId, "channels.dingtalk-connector.clientId");
|
|
218
|
+
const clientSecret = resolveSecretLike(cfg?.clientSecret, "channels.dingtalk-connector.clientSecret");
|
|
219
|
+
if (!clientId || !clientSecret) return null;
|
|
220
|
+
return {
|
|
221
|
+
clientId,
|
|
222
|
+
clientSecret
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Resolve a complete DingTalk account with merged config.
|
|
227
|
+
*/
|
|
228
|
+
function resolveDingtalkAccount(params) {
|
|
229
|
+
const hasExplicitAccountId = typeof params.accountId === "string" && params.accountId.trim() !== "";
|
|
230
|
+
const defaultSelection = hasExplicitAccountId ? null : resolveDefaultDingtalkAccountSelection(params.cfg);
|
|
231
|
+
const accountId = hasExplicitAccountId ? normalizeAccountId(params.accountId ?? "") : defaultSelection?.accountId ?? "__default__";
|
|
232
|
+
const selectionSource = hasExplicitAccountId ? "explicit" : defaultSelection?.source ?? "fallback";
|
|
233
|
+
const baseEnabled = (params.cfg.channels?.["dingtalk-connector"])?.enabled !== false;
|
|
234
|
+
const merged = mergeDingtalkAccountConfig(params.cfg, accountId);
|
|
235
|
+
const accountEnabled = merged.enabled !== false;
|
|
236
|
+
const enabled = baseEnabled && accountEnabled;
|
|
237
|
+
const creds = resolveDingtalkCredentials(merged);
|
|
238
|
+
const accountName = merged.name;
|
|
239
|
+
return {
|
|
240
|
+
accountId,
|
|
241
|
+
selectionSource,
|
|
242
|
+
enabled,
|
|
243
|
+
configured: Boolean(creds),
|
|
244
|
+
name: typeof accountName === "string" ? accountName.trim() || void 0 : void 0,
|
|
245
|
+
clientId: creds?.clientId,
|
|
246
|
+
clientSecret: creds?.clientSecret,
|
|
247
|
+
config: merged
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* List all enabled and configured accounts.
|
|
252
|
+
* Deduplicates by clientId to avoid creating multiple connections with the same credentials.
|
|
253
|
+
*/
|
|
254
|
+
function listEnabledDingtalkAccounts(cfg) {
|
|
255
|
+
const accounts = listDingtalkAccountIds(cfg).map((accountId) => resolveDingtalkAccount({
|
|
256
|
+
cfg,
|
|
257
|
+
accountId
|
|
258
|
+
})).filter((account) => account.enabled && account.configured);
|
|
259
|
+
const seen = /* @__PURE__ */ new Set();
|
|
260
|
+
return accounts.filter((account) => {
|
|
261
|
+
if (!account.clientId) return true;
|
|
262
|
+
if (seen.has(account.clientId)) return false;
|
|
263
|
+
seen.add(account.clientId);
|
|
264
|
+
return true;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
export { resolveDingtalkAccount as a, addWildcardAllowFrom as c, hasConfiguredSecretInput as d, normalizeAccountId as f, resolveDefaultDingtalkAccountSelection as i, createDefaultChannelRuntimeState as l, resolveDefaultGroupPolicy as m, listEnabledDingtalkAccounts as n, resolveDingtalkCredentials as o, resolveAllowlistProviderRuntimeGroupPolicy as p, resolveDefaultDingtalkAccountId as r, DEFAULT_ACCOUNT_ID as s, listDingtalkAccountIds as t, formatDocsLink as u };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as resolveDingtalkAccount, i as resolveDefaultDingtalkAccountSelection, n as listEnabledDingtalkAccounts, o as resolveDingtalkCredentials, r as resolveDefaultDingtalkAccountId, t as listDingtalkAccountIds } from "./accounts-BAzdqkAV.mjs";
|
|
2
|
+
export { listDingtalkAccountIds, listEnabledDingtalkAccounts, resolveDefaultDingtalkAccountId, resolveDefaultDingtalkAccountSelection, resolveDingtalkAccount, resolveDingtalkCredentials };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
|
|
2
|
+
import { n as dingtalkOapiHttp } from "./http-client-DFWZgO1n.mjs";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import FormData from "form-data";
|
|
6
|
+
//#region src/services/media/chunk-upload.ts
|
|
7
|
+
/**
|
|
8
|
+
* 钉钉文件分块上传模块
|
|
9
|
+
* 支持大文件(>20MB)的分块上传
|
|
10
|
+
*
|
|
11
|
+
* API 文档:
|
|
12
|
+
* - 开启事务:https://open.dingtalk.com/document/development/enable-upload-transaction
|
|
13
|
+
* - 上传块:https://open.dingtalk.com/document/development/upload-file-blocks
|
|
14
|
+
* - 提交事务:https://open.dingtalk.com/document/development/submit-a-file-upload-transaction
|
|
15
|
+
*/
|
|
16
|
+
const DINGTALK_OAPI = "https://oapi.dingtalk.com";
|
|
17
|
+
/** 分块上传配置 */
|
|
18
|
+
const CHUNK_CONFIG = {
|
|
19
|
+
MIN_CHUNK_SIZE: 100 * 1024,
|
|
20
|
+
MAX_CHUNK_SIZE: 8 * 1024 * 1024,
|
|
21
|
+
DEFAULT_CHUNK_SIZE: 5 * 1024 * 1024,
|
|
22
|
+
SIZE_THRESHOLD: 20 * 1024 * 1024
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 步骤一:开启分块上传事务
|
|
26
|
+
* @param oapiToken 钉钉 access_token
|
|
27
|
+
* @param fileName 文件名
|
|
28
|
+
* @param fileSize 文件大小(字节)
|
|
29
|
+
* @param log 日志对象
|
|
30
|
+
*/
|
|
31
|
+
async function enableUploadTransaction(oapiToken, fileName, fileSize, debug = false) {
|
|
32
|
+
const log = createLogger(debug, "DingTalk][ChunkUpload");
|
|
33
|
+
try {
|
|
34
|
+
log.info(`开启上传事务:${fileName}, 大小:${(fileSize / 1024 / 1024).toFixed(2)}MB`);
|
|
35
|
+
const form = new FormData();
|
|
36
|
+
form.append("file_name", fileName);
|
|
37
|
+
form.append("file_size", fileSize.toString());
|
|
38
|
+
const resp = await dingtalkOapiHttp.post(`${DINGTALK_OAPI}/file/upload/transaction/enable`, form, {
|
|
39
|
+
params: { access_token: oapiToken },
|
|
40
|
+
headers: form.getHeaders(),
|
|
41
|
+
timeout: 6e4
|
|
42
|
+
});
|
|
43
|
+
if (resp.data.errcode === 0) {
|
|
44
|
+
log.info(`事务开启成功,upload_id: ${resp.data.upload_id}`);
|
|
45
|
+
return resp.data.upload_id;
|
|
46
|
+
} else {
|
|
47
|
+
log.error(`开启事务失败:${resp.data.errmsg}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.error(`开启事务异常:${err.message}`);
|
|
52
|
+
console.error(`开启事务异常详情:`, err.response?.data || err);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 步骤二:上传文件块
|
|
58
|
+
* @param oapiToken 钉钉 access_token
|
|
59
|
+
* @param uploadId 上传事务 ID
|
|
60
|
+
* @param chunkData 文件块数据
|
|
61
|
+
* @param chunkNumber 块编号(从 1 开始)
|
|
62
|
+
* @param totalChunks 总块数
|
|
63
|
+
* @param log 日志对象
|
|
64
|
+
*/
|
|
65
|
+
async function uploadFileBlock(oapiToken, uploadId, chunkData, chunkNumber, totalChunks, debug = false) {
|
|
66
|
+
const log = createLogger(debug, "DingTalk][ChunkUpload");
|
|
67
|
+
try {
|
|
68
|
+
log.info(`上传块 ${chunkNumber}/${totalChunks}, 大小:${(chunkData.length / 1024).toFixed(2)}KB`);
|
|
69
|
+
const form = new FormData();
|
|
70
|
+
form.append("upload_id", uploadId);
|
|
71
|
+
form.append("chunk_number", chunkNumber.toString());
|
|
72
|
+
form.append("total_chunks", totalChunks.toString());
|
|
73
|
+
form.append("file", chunkData, {
|
|
74
|
+
filename: `chunk_${chunkNumber}`,
|
|
75
|
+
contentType: "application/octet-stream"
|
|
76
|
+
});
|
|
77
|
+
const resp = await dingtalkOapiHttp.post(`${DINGTALK_OAPI}/file/upload/chunk`, form, {
|
|
78
|
+
params: { access_token: oapiToken },
|
|
79
|
+
headers: form.getHeaders(),
|
|
80
|
+
timeout: 6e4
|
|
81
|
+
});
|
|
82
|
+
if (resp.data.errcode === 0) {
|
|
83
|
+
log.info(`块 ${chunkNumber} 上传成功`);
|
|
84
|
+
return true;
|
|
85
|
+
} else {
|
|
86
|
+
log.error(`块 ${chunkNumber} 上传失败:${resp.data.errmsg}`);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log.error(`块 ${chunkNumber} 上传异常:${err.message}`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 步骤三:提交分块上传事务
|
|
96
|
+
* @param oapiToken 钉钉 access_token
|
|
97
|
+
* @param uploadId 上传事务 ID
|
|
98
|
+
* @param fileName 文件名
|
|
99
|
+
* @param log 日志对象
|
|
100
|
+
*/
|
|
101
|
+
async function submitUploadTransaction(oapiToken, uploadId, fileName, debug = false) {
|
|
102
|
+
const log = createLogger(debug, "DingTalk][ChunkUpload");
|
|
103
|
+
try {
|
|
104
|
+
log.info(`提交上传事务:${uploadId}`);
|
|
105
|
+
const resp = await dingtalkOapiHttp.get(`${DINGTALK_OAPI}/file/upload/transaction/submit`, {
|
|
106
|
+
params: {
|
|
107
|
+
access_token: oapiToken,
|
|
108
|
+
upload_id: uploadId,
|
|
109
|
+
file_name: fileName
|
|
110
|
+
},
|
|
111
|
+
timeout: 6e4
|
|
112
|
+
});
|
|
113
|
+
if (resp.data.errcode === 0) {
|
|
114
|
+
log.info(`事务提交成功,file_id: ${resp.data.file_id}, download_code: ${resp.data.download_code}`);
|
|
115
|
+
return {
|
|
116
|
+
fileId: resp.data.file_id,
|
|
117
|
+
downloadCode: resp.data.download_code
|
|
118
|
+
};
|
|
119
|
+
} else {
|
|
120
|
+
log.error(`事务提交失败:${resp.data.errmsg}`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
log.error(`事务提交异常:${err.message}`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 计算分块参数
|
|
130
|
+
*/
|
|
131
|
+
function calculateChunkParams(fileSize) {
|
|
132
|
+
let chunkSize = CHUNK_CONFIG.DEFAULT_CHUNK_SIZE;
|
|
133
|
+
if (fileSize > 100 * 1024 * 1024) chunkSize = CHUNK_CONFIG.MAX_CHUNK_SIZE;
|
|
134
|
+
else if (fileSize > 50 * 1024 * 1024) chunkSize = 6 * 1024 * 1024;
|
|
135
|
+
const totalChunks = Math.ceil(fileSize / chunkSize);
|
|
136
|
+
return {
|
|
137
|
+
chunkSize,
|
|
138
|
+
totalChunks
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 分块上传大文件(>20MB)
|
|
143
|
+
* @param filePath 文件路径
|
|
144
|
+
* @param mediaType 媒体类型:video, file
|
|
145
|
+
* @param oapiToken 钉钉 access_token
|
|
146
|
+
* @param log 日志对象
|
|
147
|
+
* @returns download_code 或 null
|
|
148
|
+
*/
|
|
149
|
+
async function uploadLargeFileByChunks(filePath, mediaType, oapiToken, debug = false) {
|
|
150
|
+
const log = createLogger(debug, "DingTalk][ChunkUpload");
|
|
151
|
+
try {
|
|
152
|
+
const absPath = path.resolve(filePath);
|
|
153
|
+
if (!fs.existsSync(absPath)) {
|
|
154
|
+
log.warn(`文件不存在:${absPath}`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const fileSize = fs.statSync(absPath).size;
|
|
158
|
+
const fileName = path.basename(absPath);
|
|
159
|
+
const fileSizeMB = (fileSize / 1024 / 1024).toFixed(2);
|
|
160
|
+
log.info(`开始分块上传:${fileName}, 大小:${fileSizeMB}MB, 类型:${mediaType}`);
|
|
161
|
+
const uploadId = await enableUploadTransaction(oapiToken, fileName, fileSize, debug);
|
|
162
|
+
if (!uploadId) {
|
|
163
|
+
log.error(`开启事务失败,终止上传`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const { chunkSize, totalChunks } = calculateChunkParams(fileSize);
|
|
167
|
+
log.info(`分块参数:chunkSize=${(chunkSize / 1024 / 1024).toFixed(2)}MB, totalChunks=${totalChunks}`);
|
|
168
|
+
const fileBuffer = fs.readFileSync(absPath);
|
|
169
|
+
let successCount = 0;
|
|
170
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
171
|
+
const start = i * chunkSize;
|
|
172
|
+
const end = Math.min(start + chunkSize, fileSize);
|
|
173
|
+
if (!await uploadFileBlock(oapiToken, uploadId, fileBuffer.slice(start, end), i + 1, totalChunks, debug)) {
|
|
174
|
+
log.error(`块 ${i + 1} 上传失败,终止上传`);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
successCount++;
|
|
178
|
+
log.info(`进度:${successCount}/${totalChunks} (${(successCount / totalChunks * 100).toFixed(1)}%)`);
|
|
179
|
+
}
|
|
180
|
+
const result = await submitUploadTransaction(oapiToken, uploadId, fileName, debug);
|
|
181
|
+
if (!result || !result.downloadCode) {
|
|
182
|
+
log.error(`提交事务失败`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
log.info(`分块上传完成:${fileName}, download_code: ${result.downloadCode}`);
|
|
186
|
+
return result.downloadCode;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log.error(`分块上传异常:${err.message}`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
export { uploadLargeFileByChunks as a, uploadFileBlock as i, enableUploadTransaction as n, submitUploadTransaction as r, CHUNK_CONFIG as t };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
|
|
2
|
+
import { r as dingtalkUploadHttp } from "./http-client-DFWZgO1n.mjs";
|
|
3
|
+
import { t as CHUNK_CONFIG } from "./chunk-upload-BBQgGtcZ.mjs";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import FormData from "form-data";
|
|
7
|
+
//#region src/services/media/common.ts
|
|
8
|
+
/**
|
|
9
|
+
* 媒体处理公共工具和常量
|
|
10
|
+
*/
|
|
11
|
+
/** 本地图片路径正则表达式(跨平台) */
|
|
12
|
+
const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(((?:file:\/\/|MEDIA:|attachment:\/\/)[^)]+|\/(?:tmp|var|private|Users|home|root)[^)]+|[A-Za-z]:[\\/][^)]+)\)/g;
|
|
13
|
+
/** 视频标记正则表达式 */
|
|
14
|
+
const VIDEO_MARKER_PATTERN = /\[DINGTALK_VIDEO\](.*?)\[\/DINGTALK_VIDEO\]/gs;
|
|
15
|
+
/** 音频标记正则表达式 */
|
|
16
|
+
const AUDIO_MARKER_PATTERN = /\[DINGTALK_AUDIO\](.*?)\[\/DINGTALK_AUDIO\]/gs;
|
|
17
|
+
/** 文件标记正则表达式 */
|
|
18
|
+
const FILE_MARKER_PATTERN = /\[DINGTALK_FILE\](.*?)\[\/DINGTALK_FILE\]/gs;
|
|
19
|
+
/**
|
|
20
|
+
* 去掉 file:// / MEDIA: / attachment:// 前缀,得到实际的绝对路径
|
|
21
|
+
*/
|
|
22
|
+
function toLocalPath(raw) {
|
|
23
|
+
let filePath = raw;
|
|
24
|
+
if (filePath.startsWith("file://")) filePath = filePath.replace("file://", "");
|
|
25
|
+
else if (filePath.startsWith("MEDIA:")) filePath = filePath.replace("MEDIA:", "");
|
|
26
|
+
else if (filePath.startsWith("attachment://")) filePath = filePath.replace("attachment://", "");
|
|
27
|
+
try {
|
|
28
|
+
filePath = decodeURIComponent(filePath);
|
|
29
|
+
} catch {}
|
|
30
|
+
return filePath;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 谨慎使用,返回的是cleanMediaId
|
|
34
|
+
* 后续逐步删除,可用 media.ts
|
|
35
|
+
*/
|
|
36
|
+
async function uploadMediaToDingTalk(filePath, mediaType, oapiToken, maxSize = 20 * 1024 * 1024, logOrDebug, debug) {
|
|
37
|
+
const debugEnabled = typeof logOrDebug === "boolean" ? logOrDebug === true : debug === true;
|
|
38
|
+
const log = (typeof logOrDebug === "boolean" ? void 0 : logOrDebug) ?? createLogger(debugEnabled, `DingTalk][${mediaType}`);
|
|
39
|
+
log?.info?.(`[uploadMediaToDingTalk] 开始上传,filePath: ${filePath}, mediaType: ${mediaType}, debug: ${debugEnabled}`);
|
|
40
|
+
try {
|
|
41
|
+
const absPath = toLocalPath(filePath);
|
|
42
|
+
log?.info?.(`检查文件是否存在:${absPath}`);
|
|
43
|
+
if (!fs.existsSync(absPath)) {
|
|
44
|
+
log?.warn?.(`文件不存在:${absPath}`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const stats = fs.statSync(absPath);
|
|
48
|
+
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
49
|
+
const fileSize = stats.size;
|
|
50
|
+
if ((mediaType === "video" || mediaType === "file") && fileSize > CHUNK_CONFIG.SIZE_THRESHOLD) {
|
|
51
|
+
log?.info?.(`文件超过 20MB,使用分块上传:${absPath} (${fileSizeMB}MB)`);
|
|
52
|
+
try {
|
|
53
|
+
const { uploadLargeFileByChunks } = await import("./chunk-upload-DaLXXZH3.mjs");
|
|
54
|
+
const downloadCode = await uploadLargeFileByChunks(absPath, mediaType, oapiToken, debugEnabled);
|
|
55
|
+
if (downloadCode) {
|
|
56
|
+
log?.info?.(`分块上传成功:${absPath}, download_code: ${downloadCode}`);
|
|
57
|
+
return downloadCode;
|
|
58
|
+
}
|
|
59
|
+
log?.error?.(`分块上传失败:${absPath}`);
|
|
60
|
+
} catch (chunkErr) {
|
|
61
|
+
log?.error?.(`分块上传异常:${chunkErr.message}`);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (stats.size > maxSize) {
|
|
66
|
+
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(0);
|
|
67
|
+
log?.warn?.(`文件过大:${absPath}, 大小:${fileSizeMB}MB, 超过限制 ${maxSizeMB}MB`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const form = new FormData();
|
|
71
|
+
form.append("media", fs.createReadStream(absPath), {
|
|
72
|
+
filename: path.basename(absPath),
|
|
73
|
+
contentType: mediaType === "image" ? "image/jpeg" : "application/octet-stream"
|
|
74
|
+
});
|
|
75
|
+
const uploadType = mediaType;
|
|
76
|
+
log?.info?.(`上传文件:${absPath} (${fileSizeMB}MB), uploadType=${uploadType}`);
|
|
77
|
+
const mediaId = (await dingtalkUploadHttp.post(`${DINGTALK_OAPI}/media/upload`, form, {
|
|
78
|
+
params: {
|
|
79
|
+
access_token: oapiToken,
|
|
80
|
+
type: mediaType
|
|
81
|
+
},
|
|
82
|
+
headers: form.getHeaders(),
|
|
83
|
+
timeout: 6e4,
|
|
84
|
+
maxBodyLength: Infinity
|
|
85
|
+
})).data?.media_id;
|
|
86
|
+
if (mediaId) {
|
|
87
|
+
const cleanMediaId = mediaId.startsWith("@") ? mediaId.substring(1) : mediaId;
|
|
88
|
+
log?.info?.(`上传成功:mediaId=${cleanMediaId}`);
|
|
89
|
+
return cleanMediaId;
|
|
90
|
+
}
|
|
91
|
+
log?.warn?.(`上传返回无 media_id`);
|
|
92
|
+
return null;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
log?.error?.(`上传失败:${err.message}`);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** 钉钉 OAPI 常量 */
|
|
99
|
+
const DINGTALK_OAPI = "https://oapi.dingtalk.com";
|
|
100
|
+
//#endregion
|
|
101
|
+
export { VIDEO_MARKER_PATTERN as a, LOCAL_IMAGE_RE as i, DINGTALK_OAPI as n, toLocalPath as o, FILE_MARKER_PATTERN as r, uploadMediaToDingTalk as s, AUDIO_MARKER_PATTERN as t };
|