@dingxiang-me/openclaw-wechat 1.7.2 → 2.0.0
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 +151 -0
- package/README.en.md +379 -11
- package/README.md +620 -12
- package/docs/channels/wecom.md +181 -3
- package/openclaw.plugin.json +148 -5
- package/package.json +9 -5
- package/src/core/delivery-router.js +2 -0
- package/src/core/stream-manager.js +13 -2
- package/src/core.js +96 -6
- package/src/wecom/account-config-core.js +2 -0
- package/src/wecom/account-config.js +12 -3
- package/src/wecom/agent-context.js +7 -1
- package/src/wecom/agent-dispatch-executor.js +13 -1
- package/src/wecom/agent-dispatch-fallback.js +23 -0
- package/src/wecom/agent-inbound-dispatch.js +1 -1
- package/src/wecom/agent-inbound-processor.js +33 -2
- package/src/wecom/agent-late-reply-runtime.js +31 -1
- package/src/wecom/agent-runtime-context.js +3 -0
- package/src/wecom/agent-webhook-handler.js +5 -0
- package/src/wecom/api-client-core.js +1 -1
- package/src/wecom/bot-context.js +7 -1
- package/src/wecom/bot-dispatch-fallback.js +34 -3
- package/src/wecom/bot-dispatch-handlers.js +47 -4
- package/src/wecom/bot-inbound-dispatch-runtime.js +10 -0
- package/src/wecom/bot-inbound-executor-helpers.js +11 -4
- package/src/wecom/bot-inbound-executor.js +34 -0
- package/src/wecom/bot-long-connection-manager.js +971 -0
- package/src/wecom/bot-reply-runtime.js +36 -6
- package/src/wecom/bot-runtime-context.js +3 -0
- package/src/wecom/bot-state-store.js +4 -5
- package/src/wecom/bot-webhook-dispatch.js +5 -0
- package/src/wecom/bot-webhook-handler.js +5 -0
- package/src/wecom/callback-health-diagnostics.js +86 -0
- package/src/wecom/channel-config-schema.js +242 -0
- package/src/wecom/channel-plugin.js +162 -4
- package/src/wecom/channel-status-state.js +150 -0
- package/src/wecom/command-handlers.js +6 -0
- package/src/wecom/command-status-text.js +32 -3
- package/src/wecom/doc-client.js +537 -0
- package/src/wecom/doc-schema.js +380 -0
- package/src/wecom/doc-tool.js +833 -0
- package/src/wecom/outbound-active-stream.js +17 -10
- package/src/wecom/outbound-delivery.js +49 -0
- package/src/wecom/plugin-account-policy-services.js +4 -1
- package/src/wecom/plugin-composition.js +2 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/plugin-delivery-inbound-services.js +4 -0
- package/src/wecom/plugin-processing-deps.js +5 -0
- package/src/wecom/plugin-route-runtime-deps.js +2 -0
- package/src/wecom/plugin-services.js +37 -0
- package/src/wecom/register-runtime.js +20 -1
- package/src/wecom/request-parsers.js +1 -0
- package/src/wecom/route-registration.js +4 -1
- package/src/wecom/session-reset.js +168 -0
- package/src/wecom/text-format.js +22 -5
- package/src/wecom/text-inbound-scheduler.js +1 -1
- package/src/wecom/thinking-parser.js +74 -0
- package/src/wecom/voice-transcription-process.js +80 -8
- package/src/wecom/voice-transcription.js +11 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
import { createWecomDocClient } from "./doc-client.js";
|
|
2
|
+
import { wecomDocToolSchema } from "./doc-schema.js";
|
|
3
|
+
|
|
4
|
+
function ensureFunction(name, value) {
|
|
5
|
+
if (typeof value !== "function") {
|
|
6
|
+
throw new Error(`createWecomDocToolRegistrar: ${name} is required`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readString(value) {
|
|
11
|
+
const trimmed = String(value ?? "").trim();
|
|
12
|
+
return trimmed || "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeOptionalAccountId(normalizeAccountIdFn, value) {
|
|
16
|
+
const trimmed = readString(value);
|
|
17
|
+
if (!trimmed) return "";
|
|
18
|
+
return normalizeAccountIdFn(trimmed);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readChannelConfig(api) {
|
|
22
|
+
return api?.config?.channels?.wecom ?? {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readConfiguredDefaultAccountId(api, normalizeAccountIdFn) {
|
|
26
|
+
return normalizeOptionalAccountId(normalizeAccountIdFn, readChannelConfig(api)?.defaultAccount);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readChannelDocEnabled(api) {
|
|
30
|
+
const value = readChannelConfig(api)?.tools?.doc;
|
|
31
|
+
if (typeof value === "boolean") return value;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readChannelDocAutoGrantRequesterCollaborator(api) {
|
|
36
|
+
const value = readChannelConfig(api)?.tools?.docAutoGrantRequesterCollaborator;
|
|
37
|
+
if (typeof value === "boolean") return value;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isDocEnabledForAccount(api, account) {
|
|
42
|
+
if (typeof account?.tools?.doc === "boolean") return account.tools.doc;
|
|
43
|
+
return readChannelDocEnabled(api);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isDocAutoGrantRequesterCollaboratorEnabled(api, account) {
|
|
47
|
+
if (typeof account?.tools?.docAutoGrantRequesterCollaborator === "boolean") {
|
|
48
|
+
return account.tools.docAutoGrantRequesterCollaborator;
|
|
49
|
+
}
|
|
50
|
+
return readChannelDocAutoGrantRequesterCollaborator(api);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasDocCredentials(account) {
|
|
54
|
+
return Boolean(
|
|
55
|
+
readString(account?.corpId) &&
|
|
56
|
+
readString(account?.corpSecret) &&
|
|
57
|
+
Number.isFinite(Number(account?.agentId)),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function listEligibleDocAccounts(api, listEnabledWecomAccounts) {
|
|
62
|
+
return listEnabledWecomAccounts(api).filter(
|
|
63
|
+
(account) => hasDocCredentials(account) && isDocEnabledForAccount(api, account),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildToolResult(payload) {
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
70
|
+
details: payload,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mapDocTypeLabel(docType) {
|
|
75
|
+
if (docType === 5) return "智能表格";
|
|
76
|
+
return docType === 4 ? "表格" : "文档";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function summarizeDocInfo(info = {}) {
|
|
80
|
+
const docName = readString(info.doc_name) || "未命名文档";
|
|
81
|
+
const docType = mapDocTypeLabel(Number(info.doc_type));
|
|
82
|
+
return `${docType}“${docName}”信息已获取`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function summarizeDocAuth(result = {}) {
|
|
86
|
+
return `权限信息已获取:通知成员 ${result.docMembers?.length ?? 0},协作者 ${result.coAuthList?.length ?? 0}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readBooleanFlag(value) {
|
|
90
|
+
return typeof value === "boolean" ? value : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatDocMemberRef(value) {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
95
|
+
const userid = readString(value.userid ?? value.userId);
|
|
96
|
+
if (userid) return `userid:${userid}`;
|
|
97
|
+
const partyid = readString(value.partyid);
|
|
98
|
+
if (partyid) return `partyid:${partyid}`;
|
|
99
|
+
const tagid = readString(value.tagid);
|
|
100
|
+
if (tagid) return `tagid:${tagid}`;
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function mapDocMemberList(values) {
|
|
105
|
+
return Array.isArray(values)
|
|
106
|
+
? values.map((item) => formatDocMemberRef(item)).filter(Boolean)
|
|
107
|
+
: [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function describeFlagState(value, enabledLabel, disabledLabel, unknownLabel = "未知") {
|
|
111
|
+
if (value === true) return enabledLabel;
|
|
112
|
+
if (value === false) return disabledLabel;
|
|
113
|
+
return unknownLabel;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildDocAuthDiagnosis(result = {}, requesterSenderId = "") {
|
|
117
|
+
const accessRule = result.accessRule && typeof result.accessRule === "object" ? result.accessRule : {};
|
|
118
|
+
const viewers = mapDocMemberList(result.docMembers);
|
|
119
|
+
const collaborators = mapDocMemberList(result.coAuthList);
|
|
120
|
+
const requester = readString(requesterSenderId);
|
|
121
|
+
const requesterViewerRef = requester ? `userid:${requester}` : "";
|
|
122
|
+
const requesterIsViewer = requesterViewerRef ? viewers.includes(requesterViewerRef) : false;
|
|
123
|
+
const requesterIsCollaborator = requesterViewerRef ? collaborators.includes(requesterViewerRef) : false;
|
|
124
|
+
const internalAccessEnabled = readBooleanFlag(accessRule.enable_corp_internal);
|
|
125
|
+
const externalAccessEnabled = readBooleanFlag(accessRule.enable_corp_external);
|
|
126
|
+
const externalShareAllowed = typeof accessRule.ban_share_external === "boolean"
|
|
127
|
+
? !accessRule.ban_share_external
|
|
128
|
+
: null;
|
|
129
|
+
const likelyAnonymousLinkFailure = internalAccessEnabled === true && externalAccessEnabled === false;
|
|
130
|
+
const findings = [
|
|
131
|
+
`企业内访问:${describeFlagState(internalAccessEnabled, "开启", "关闭")}`,
|
|
132
|
+
`企业外访问:${describeFlagState(externalAccessEnabled, "开启", "关闭")}`,
|
|
133
|
+
`外部分享:${describeFlagState(externalShareAllowed, "允许", "禁止")}`,
|
|
134
|
+
`查看成员:${viewers.length}`,
|
|
135
|
+
`协作者:${collaborators.length}`,
|
|
136
|
+
];
|
|
137
|
+
const recommendations = [];
|
|
138
|
+
if (likelyAnonymousLinkFailure) {
|
|
139
|
+
recommendations.push("当前更像是仅企业内可访问;匿名浏览器或未登录企业微信环境通常会显示“文档不存在”。");
|
|
140
|
+
}
|
|
141
|
+
if (requester) {
|
|
142
|
+
if (requesterIsCollaborator) {
|
|
143
|
+
recommendations.push(`当前请求人 ${requester} 已在协作者列表中。`);
|
|
144
|
+
} else if (requesterIsViewer) {
|
|
145
|
+
recommendations.push(`当前请求人 ${requester} 已在查看成员列表中,但还不是协作者。`);
|
|
146
|
+
} else {
|
|
147
|
+
recommendations.push(`当前请求人 ${requester} 不在查看成员或协作者列表中。`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
internalAccessEnabled,
|
|
152
|
+
externalAccessEnabled,
|
|
153
|
+
externalShareAllowed,
|
|
154
|
+
viewerCount: viewers.length,
|
|
155
|
+
collaboratorCount: collaborators.length,
|
|
156
|
+
viewers,
|
|
157
|
+
collaborators,
|
|
158
|
+
requesterSenderId: requester || undefined,
|
|
159
|
+
requesterRole: requesterIsCollaborator ? "collaborator" : requesterIsViewer ? "viewer" : requester ? "none" : "unknown",
|
|
160
|
+
likelyAnonymousLinkFailure,
|
|
161
|
+
findings,
|
|
162
|
+
recommendations,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function summarizeDocAuthDiagnosis(diagnosis = {}) {
|
|
167
|
+
const parts = Array.isArray(diagnosis.findings) ? diagnosis.findings : [];
|
|
168
|
+
return parts.length > 0 ? `文档权限诊断:${parts.join(",")}` : "文档权限诊断已完成";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildDocIdUsageHint(docId) {
|
|
172
|
+
const normalizedDocId = readString(docId);
|
|
173
|
+
if (!normalizedDocId) return "";
|
|
174
|
+
return `后续权限、分享和诊断操作请使用真实 docId:${normalizedDocId};不要直接使用分享链接路径中的片段。`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function safeParseJson(text) {
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(text);
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function extractEmbeddedJson(html, variableName) {
|
|
186
|
+
const source = String(html ?? "");
|
|
187
|
+
if (!source) return null;
|
|
188
|
+
const marker = `window.${variableName}=`;
|
|
189
|
+
const start = source.indexOf(marker);
|
|
190
|
+
if (start < 0) return null;
|
|
191
|
+
const valueStart = start + marker.length;
|
|
192
|
+
const end = source.indexOf(";</script>", valueStart);
|
|
193
|
+
if (end < 0) return null;
|
|
194
|
+
return safeParseJson(source.slice(valueStart, end));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildShareLinkDiagnosis({ shareUrl, finalUrl, status, contentType, basicClientVars }) {
|
|
198
|
+
const parsedUrl = new URL(finalUrl || shareUrl);
|
|
199
|
+
const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);
|
|
200
|
+
const pathResourceType = readString(pathSegments[0]);
|
|
201
|
+
const pathResourceId = readString(pathSegments[1]);
|
|
202
|
+
const shareCode = readString(parsedUrl.searchParams.get("scode"));
|
|
203
|
+
const userInfo = basicClientVars?.userInfo && typeof basicClientVars.userInfo === "object"
|
|
204
|
+
? basicClientVars.userInfo
|
|
205
|
+
: {};
|
|
206
|
+
const docInfo = basicClientVars?.docInfo && typeof basicClientVars.docInfo === "object"
|
|
207
|
+
? basicClientVars.docInfo
|
|
208
|
+
: {};
|
|
209
|
+
const padInfo = docInfo?.padInfo && typeof docInfo.padInfo === "object"
|
|
210
|
+
? docInfo.padInfo
|
|
211
|
+
: {};
|
|
212
|
+
const ownerInfo = docInfo?.ownerInfo && typeof docInfo.ownerInfo === "object"
|
|
213
|
+
? docInfo.ownerInfo
|
|
214
|
+
: {};
|
|
215
|
+
const shareInfo = docInfo?.shareInfo && typeof docInfo.shareInfo === "object"
|
|
216
|
+
? docInfo.shareInfo
|
|
217
|
+
: {};
|
|
218
|
+
const aclInfo = docInfo?.aclInfo && typeof docInfo.aclInfo === "object"
|
|
219
|
+
? docInfo.aclInfo
|
|
220
|
+
: {};
|
|
221
|
+
const userType = readString(userInfo.userType);
|
|
222
|
+
const padType = readString(padInfo.padType);
|
|
223
|
+
const padId = readString(padInfo.padId);
|
|
224
|
+
const padTitle = readString(padInfo.padTitle);
|
|
225
|
+
const isGuest = userType === "guest" || Number(userInfo.loginType) === 0;
|
|
226
|
+
const isBlankPage = padType === "blankpage";
|
|
227
|
+
const likelyUnavailableToGuest = isGuest && isBlankPage && !padTitle;
|
|
228
|
+
const findings = [
|
|
229
|
+
`HTTP ${String(status || "")}`.trim(),
|
|
230
|
+
`内容类型:${readString(contentType) || "未知"}`,
|
|
231
|
+
`访问身份:${userType || "未知"}`,
|
|
232
|
+
`页面类型:${padType || "未知"}`,
|
|
233
|
+
`路径资源:${pathResourceType || "未知"} / ${pathResourceId || "未知"}`,
|
|
234
|
+
];
|
|
235
|
+
const recommendations = [];
|
|
236
|
+
if (likelyUnavailableToGuest) {
|
|
237
|
+
recommendations.push("当前链接对 guest/未登录企业微信环境返回 blankpage,外部访问会表现为打不开或像“文档不存在”。");
|
|
238
|
+
}
|
|
239
|
+
if (shareCode) {
|
|
240
|
+
recommendations.push(`当前链接带有分享码 scode=${shareCode}。如分享码过期或未生效,外部访问会失败。`);
|
|
241
|
+
}
|
|
242
|
+
if (pathResourceId && padId && pathResourceId !== padId) {
|
|
243
|
+
recommendations.push(`链接路径中的资源标识与页面 padId 不一致:path=${pathResourceId},padId=${padId}。`);
|
|
244
|
+
}
|
|
245
|
+
if (pathResourceId && padId && pathResourceId === padId) {
|
|
246
|
+
recommendations.push("链接路径资源标识与页面 padId 一致,但这仍不等同于 Wedoc API 可用的真实 docId。");
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
shareUrl,
|
|
250
|
+
finalUrl,
|
|
251
|
+
httpStatus: status,
|
|
252
|
+
contentType: readString(contentType) || undefined,
|
|
253
|
+
pathResourceType: pathResourceType || undefined,
|
|
254
|
+
pathResourceId: pathResourceId || undefined,
|
|
255
|
+
shareCode: shareCode || undefined,
|
|
256
|
+
userType: userType || undefined,
|
|
257
|
+
isGuest,
|
|
258
|
+
padId: padId || undefined,
|
|
259
|
+
padType: padType || undefined,
|
|
260
|
+
padTitle: padTitle || undefined,
|
|
261
|
+
ownerId: readString(ownerInfo.ownerId) || undefined,
|
|
262
|
+
hasShareInfo: Object.keys(shareInfo).length > 0,
|
|
263
|
+
hasAclInfo: Object.keys(aclInfo).length > 0,
|
|
264
|
+
likelyUnavailableToGuest,
|
|
265
|
+
findings,
|
|
266
|
+
recommendations,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function inspectWecomShareLink({ shareUrl, fetchImpl }) {
|
|
271
|
+
const normalizedUrl = readString(shareUrl);
|
|
272
|
+
if (!normalizedUrl) throw new Error("shareUrl required");
|
|
273
|
+
let parsed;
|
|
274
|
+
try {
|
|
275
|
+
parsed = new URL(normalizedUrl);
|
|
276
|
+
} catch {
|
|
277
|
+
throw new Error("shareUrl must be a valid URL");
|
|
278
|
+
}
|
|
279
|
+
const response = await fetchImpl(parsed.toString(), {
|
|
280
|
+
headers: {
|
|
281
|
+
"user-agent": "OpenClaw-Wechat/1.0",
|
|
282
|
+
accept: "text/html,application/xhtml+xml",
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const contentType = response.headers?.get?.("content-type") || "";
|
|
286
|
+
const html = await response.text();
|
|
287
|
+
const basicClientVars = extractEmbeddedJson(html, "basicClientVars");
|
|
288
|
+
const diagnosis = buildShareLinkDiagnosis({
|
|
289
|
+
shareUrl: normalizedUrl,
|
|
290
|
+
finalUrl: response.url || parsed.toString(),
|
|
291
|
+
status: response.status,
|
|
292
|
+
contentType,
|
|
293
|
+
basicClientVars,
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
raw: {
|
|
297
|
+
httpStatus: response.status,
|
|
298
|
+
finalUrl: response.url || parsed.toString(),
|
|
299
|
+
contentType,
|
|
300
|
+
basicClientVars,
|
|
301
|
+
},
|
|
302
|
+
diagnosis,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function summarizeShareLinkDiagnosis(diagnosis = {}) {
|
|
307
|
+
const parts = Array.isArray(diagnosis.findings) ? diagnosis.findings : [];
|
|
308
|
+
return parts.length > 0 ? `分享链接校验:${parts.join(",")}` : "分享链接校验已完成";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function summarizeSheetProperties(result = {}) {
|
|
312
|
+
return `表格属性已获取:工作表 ${result.properties?.length ?? 0}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function summarizeDocAccess(result = {}) {
|
|
316
|
+
const parts = [];
|
|
317
|
+
if (result.addedViewerCount) parts.push(`新增查看成员 ${result.addedViewerCount}`);
|
|
318
|
+
if (result.addedCollaboratorCount) parts.push(`新增协作者 ${result.addedCollaboratorCount}`);
|
|
319
|
+
if (result.removedViewerCount) parts.push(`移除查看成员 ${result.removedViewerCount}`);
|
|
320
|
+
if (result.removedCollaboratorCount) parts.push(`移除协作者 ${result.removedCollaboratorCount}`);
|
|
321
|
+
return parts.length > 0 ? `文档权限已更新:${parts.join(",")}` : "文档权限已更新";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function summarizeFormInfo(result = {}) {
|
|
325
|
+
const title = readString(result.formInfo?.form_title) || "未命名收集表";
|
|
326
|
+
return `收集表“${title}”信息已获取`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function summarizeFormAnswer(result = {}) {
|
|
330
|
+
return `收集表答案已获取:字段 ${result.answerList?.length ?? 0}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function summarizeFormStatistic(result = {}) {
|
|
334
|
+
return `收集表统计已获取:请求 ${result.items?.length ?? 0},成功 ${result.successCount ?? 0}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function readMemberUserId(value) {
|
|
338
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
339
|
+
return readString(value);
|
|
340
|
+
}
|
|
341
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
342
|
+
return readString(value.userid ?? value.userId);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function hasMemberUserId(values, requesterSenderId) {
|
|
346
|
+
const normalizedRequesterSenderId = readString(requesterSenderId);
|
|
347
|
+
if (!normalizedRequesterSenderId) return false;
|
|
348
|
+
return Array.isArray(values) && values.some((item) => readMemberUserId(item) === normalizedRequesterSenderId);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function resolveCreateCollaborators({
|
|
352
|
+
api,
|
|
353
|
+
account,
|
|
354
|
+
toolContext,
|
|
355
|
+
params,
|
|
356
|
+
}) {
|
|
357
|
+
const explicitCollaborators = Array.isArray(params?.collaborators) ? [...params.collaborators] : [];
|
|
358
|
+
const requesterSenderId = readString(toolContext?.requesterSenderId);
|
|
359
|
+
if (!requesterSenderId) return explicitCollaborators;
|
|
360
|
+
if (!isDocAutoGrantRequesterCollaboratorEnabled(api, account)) return explicitCollaborators;
|
|
361
|
+
if (hasMemberUserId(explicitCollaborators, requesterSenderId)) return explicitCollaborators;
|
|
362
|
+
if (hasMemberUserId(params?.viewers, requesterSenderId)) return explicitCollaborators;
|
|
363
|
+
explicitCollaborators.push(requesterSenderId);
|
|
364
|
+
return explicitCollaborators;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function resolveToolAccount({
|
|
368
|
+
api,
|
|
369
|
+
toolContext,
|
|
370
|
+
params,
|
|
371
|
+
listEnabledWecomAccounts,
|
|
372
|
+
normalizeAccountIdFn,
|
|
373
|
+
}) {
|
|
374
|
+
const eligibleAccounts = listEligibleDocAccounts(api, listEnabledWecomAccounts);
|
|
375
|
+
if (eligibleAccounts.length === 0) {
|
|
376
|
+
throw new Error("WeCom Doc tool unavailable: no eligible account with doc access is enabled");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const explicitAccountId = normalizeOptionalAccountId(normalizeAccountIdFn, params?.accountId);
|
|
380
|
+
if (explicitAccountId) {
|
|
381
|
+
const matched = eligibleAccounts.find(
|
|
382
|
+
(account) => normalizeAccountIdFn(account.accountId) === explicitAccountId,
|
|
383
|
+
);
|
|
384
|
+
if (!matched) {
|
|
385
|
+
throw new Error(`WeCom Doc account not found or doc tool disabled: ${explicitAccountId}`);
|
|
386
|
+
}
|
|
387
|
+
return matched;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const agentAccountId = normalizeOptionalAccountId(normalizeAccountIdFn, toolContext?.agentAccountId);
|
|
391
|
+
if (agentAccountId) {
|
|
392
|
+
const matched = eligibleAccounts.find(
|
|
393
|
+
(account) => normalizeAccountIdFn(account.accountId) === agentAccountId,
|
|
394
|
+
);
|
|
395
|
+
if (matched) return matched;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const configuredDefaultAccountId = readConfiguredDefaultAccountId(api, normalizeAccountIdFn);
|
|
399
|
+
if (configuredDefaultAccountId) {
|
|
400
|
+
const matched = eligibleAccounts.find(
|
|
401
|
+
(account) => normalizeAccountIdFn(account.accountId) === configuredDefaultAccountId,
|
|
402
|
+
);
|
|
403
|
+
if (matched) return matched;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return eligibleAccounts[0];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function createWecomDocToolRegistrar({
|
|
410
|
+
listEnabledWecomAccounts,
|
|
411
|
+
normalizeAccountId: normalizeAccountIdFn,
|
|
412
|
+
fetchWithRetry,
|
|
413
|
+
getWecomAccessToken,
|
|
414
|
+
fetchImpl = fetch,
|
|
415
|
+
} = {}) {
|
|
416
|
+
ensureFunction("listEnabledWecomAccounts", listEnabledWecomAccounts);
|
|
417
|
+
ensureFunction("normalizeAccountId", normalizeAccountIdFn);
|
|
418
|
+
ensureFunction("fetchWithRetry", fetchWithRetry);
|
|
419
|
+
ensureFunction("getWecomAccessToken", getWecomAccessToken);
|
|
420
|
+
ensureFunction("fetchImpl", fetchImpl);
|
|
421
|
+
|
|
422
|
+
const docClient = createWecomDocClient({
|
|
423
|
+
fetchWithRetry,
|
|
424
|
+
getWecomAccessToken,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return function registerWecomDocTools(api) {
|
|
428
|
+
if (typeof api?.registerTool !== "function") return;
|
|
429
|
+
if (!readChannelDocEnabled(api)) {
|
|
430
|
+
api.logger?.info?.("wecom_doc: disabled by channels.wecom.tools.doc=false");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const eligibleAccounts = listEligibleDocAccounts(api, listEnabledWecomAccounts);
|
|
435
|
+
if (eligibleAccounts.length === 0) {
|
|
436
|
+
api.logger?.info?.("wecom_doc: no eligible account found; tool not registered");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
api.registerTool((toolContext = {}) => ({
|
|
441
|
+
name: "wecom_doc",
|
|
442
|
+
label: "WeCom Doc",
|
|
443
|
+
description:
|
|
444
|
+
"企业微信文档工具。支持文档创建、查看/协作者权限配置、收集表管理与表格属性查询。",
|
|
445
|
+
parameters: wecomDocToolSchema,
|
|
446
|
+
async execute(_toolCallId, params) {
|
|
447
|
+
try {
|
|
448
|
+
const account = resolveToolAccount({
|
|
449
|
+
api,
|
|
450
|
+
toolContext,
|
|
451
|
+
params,
|
|
452
|
+
listEnabledWecomAccounts,
|
|
453
|
+
normalizeAccountIdFn,
|
|
454
|
+
});
|
|
455
|
+
const accountContext = {
|
|
456
|
+
...account,
|
|
457
|
+
logger: api.logger,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
switch (params?.action) {
|
|
461
|
+
case "create": {
|
|
462
|
+
const collaborators = resolveCreateCollaborators({
|
|
463
|
+
api,
|
|
464
|
+
account,
|
|
465
|
+
toolContext,
|
|
466
|
+
params,
|
|
467
|
+
});
|
|
468
|
+
const result = await docClient.createDoc({
|
|
469
|
+
account: accountContext,
|
|
470
|
+
docName: params.docName,
|
|
471
|
+
docType: params.docType,
|
|
472
|
+
spaceId: params.spaceId,
|
|
473
|
+
fatherId: params.fatherId,
|
|
474
|
+
adminUsers: params.adminUsers,
|
|
475
|
+
});
|
|
476
|
+
let accessResult = null;
|
|
477
|
+
if ((Array.isArray(params.viewers) && params.viewers.length > 0) ||
|
|
478
|
+
collaborators.length > 0) {
|
|
479
|
+
try {
|
|
480
|
+
accessResult = await docClient.grantDocAccess({
|
|
481
|
+
account: accountContext,
|
|
482
|
+
docId: result.docId,
|
|
483
|
+
viewers: params.viewers,
|
|
484
|
+
collaborators,
|
|
485
|
+
});
|
|
486
|
+
} catch (err) {
|
|
487
|
+
return buildToolResult({
|
|
488
|
+
ok: false,
|
|
489
|
+
partial: true,
|
|
490
|
+
action: "create",
|
|
491
|
+
accountId: account.accountId,
|
|
492
|
+
resourceType: result.docTypeLabel,
|
|
493
|
+
canonicalDocId: result.docId,
|
|
494
|
+
docId: result.docId,
|
|
495
|
+
title: readString(params.docName),
|
|
496
|
+
url: result.url || undefined,
|
|
497
|
+
summary: `已创建${mapDocTypeLabel(result.docType)}“${readString(params.docName)}”(docId: ${result.docId}),但权限授予失败`,
|
|
498
|
+
usageHint: buildDocIdUsageHint(result.docId) || undefined,
|
|
499
|
+
error: err instanceof Error ? err.message : String(err),
|
|
500
|
+
raw: {
|
|
501
|
+
create: result.raw,
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return buildToolResult({
|
|
507
|
+
ok: true,
|
|
508
|
+
action: "create",
|
|
509
|
+
accountId: account.accountId,
|
|
510
|
+
resourceType: result.docTypeLabel,
|
|
511
|
+
canonicalDocId: result.docId,
|
|
512
|
+
docId: result.docId,
|
|
513
|
+
title: readString(params.docName),
|
|
514
|
+
url: result.url || undefined,
|
|
515
|
+
summary: accessResult
|
|
516
|
+
? `已创建${mapDocTypeLabel(result.docType)}“${readString(params.docName)}”(docId: ${result.docId});${summarizeDocAccess(accessResult)}`
|
|
517
|
+
: `已创建${mapDocTypeLabel(result.docType)}“${readString(params.docName)}”(docId: ${result.docId})`,
|
|
518
|
+
usageHint: buildDocIdUsageHint(result.docId) || undefined,
|
|
519
|
+
raw: accessResult
|
|
520
|
+
? {
|
|
521
|
+
create: result.raw,
|
|
522
|
+
access: accessResult.raw,
|
|
523
|
+
}
|
|
524
|
+
: result.raw,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
case "rename": {
|
|
528
|
+
const result = await docClient.renameDoc({
|
|
529
|
+
account: accountContext,
|
|
530
|
+
docId: params.docId,
|
|
531
|
+
newName: params.newName,
|
|
532
|
+
});
|
|
533
|
+
return buildToolResult({
|
|
534
|
+
ok: true,
|
|
535
|
+
action: "rename",
|
|
536
|
+
accountId: account.accountId,
|
|
537
|
+
docId: result.docId,
|
|
538
|
+
title: result.newName,
|
|
539
|
+
summary: `文档已重命名为“${result.newName}”`,
|
|
540
|
+
raw: result.raw,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
case "get_info": {
|
|
544
|
+
const result = await docClient.getDocBaseInfo({
|
|
545
|
+
account: accountContext,
|
|
546
|
+
docId: params.docId,
|
|
547
|
+
});
|
|
548
|
+
return buildToolResult({
|
|
549
|
+
ok: true,
|
|
550
|
+
action: "get_info",
|
|
551
|
+
accountId: account.accountId,
|
|
552
|
+
docId: params.docId,
|
|
553
|
+
title: readString(result.info?.doc_name) || undefined,
|
|
554
|
+
resourceType:
|
|
555
|
+
Number(result.info?.doc_type) === 5
|
|
556
|
+
? "smart_table"
|
|
557
|
+
: Number(result.info?.doc_type) === 4
|
|
558
|
+
? "spreadsheet"
|
|
559
|
+
: "doc",
|
|
560
|
+
summary: summarizeDocInfo(result.info),
|
|
561
|
+
raw: result.raw,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
case "share": {
|
|
565
|
+
const result = await docClient.shareDoc({
|
|
566
|
+
account: accountContext,
|
|
567
|
+
docId: params.docId,
|
|
568
|
+
});
|
|
569
|
+
return buildToolResult({
|
|
570
|
+
ok: true,
|
|
571
|
+
action: "share",
|
|
572
|
+
accountId: account.accountId,
|
|
573
|
+
canonicalDocId: params.docId,
|
|
574
|
+
docId: params.docId,
|
|
575
|
+
url: result.shareUrl || undefined,
|
|
576
|
+
summary: result.shareUrl ? `文档分享链接已获取(docId: ${params.docId})` : `文档分享接口调用成功(docId: ${params.docId})`,
|
|
577
|
+
usageHint: buildDocIdUsageHint(params.docId) || undefined,
|
|
578
|
+
raw: result.raw,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
case "get_auth": {
|
|
582
|
+
const result = await docClient.getDocAuth({
|
|
583
|
+
account: accountContext,
|
|
584
|
+
docId: params.docId,
|
|
585
|
+
});
|
|
586
|
+
const diagnosis = buildDocAuthDiagnosis(result, toolContext?.requesterSenderId);
|
|
587
|
+
return buildToolResult({
|
|
588
|
+
ok: true,
|
|
589
|
+
action: "get_auth",
|
|
590
|
+
accountId: account.accountId,
|
|
591
|
+
canonicalDocId: params.docId,
|
|
592
|
+
docId: params.docId,
|
|
593
|
+
summary: summarizeDocAuth(result),
|
|
594
|
+
diagnosis,
|
|
595
|
+
raw: result.raw,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
case "diagnose_auth": {
|
|
599
|
+
const result = await docClient.getDocAuth({
|
|
600
|
+
account: accountContext,
|
|
601
|
+
docId: params.docId,
|
|
602
|
+
});
|
|
603
|
+
const diagnosis = buildDocAuthDiagnosis(result, toolContext?.requesterSenderId);
|
|
604
|
+
return buildToolResult({
|
|
605
|
+
ok: true,
|
|
606
|
+
action: "diagnose_auth",
|
|
607
|
+
accountId: account.accountId,
|
|
608
|
+
canonicalDocId: params.docId,
|
|
609
|
+
docId: params.docId,
|
|
610
|
+
summary: summarizeDocAuthDiagnosis(diagnosis),
|
|
611
|
+
diagnosis,
|
|
612
|
+
raw: result.raw,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
case "validate_share_link": {
|
|
616
|
+
const result = await inspectWecomShareLink({
|
|
617
|
+
shareUrl: params.shareUrl,
|
|
618
|
+
fetchImpl,
|
|
619
|
+
});
|
|
620
|
+
return buildToolResult({
|
|
621
|
+
ok: true,
|
|
622
|
+
action: "validate_share_link",
|
|
623
|
+
accountId: account.accountId,
|
|
624
|
+
url: result.diagnosis.finalUrl || params.shareUrl,
|
|
625
|
+
summary: summarizeShareLinkDiagnosis(result.diagnosis),
|
|
626
|
+
diagnosis: result.diagnosis,
|
|
627
|
+
raw: result.raw,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
case "delete": {
|
|
631
|
+
const result = await docClient.deleteDoc({
|
|
632
|
+
account: accountContext,
|
|
633
|
+
docId: params.docId,
|
|
634
|
+
formId: params.formId,
|
|
635
|
+
});
|
|
636
|
+
return buildToolResult({
|
|
637
|
+
ok: true,
|
|
638
|
+
action: "delete",
|
|
639
|
+
accountId: account.accountId,
|
|
640
|
+
docId: result.docId || undefined,
|
|
641
|
+
formId: result.formId || undefined,
|
|
642
|
+
summary: result.formId ? "收集表已删除" : "文档已删除",
|
|
643
|
+
raw: result.raw,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
case "set_join_rule": {
|
|
647
|
+
const result = await docClient.setDocJoinRule({
|
|
648
|
+
account: accountContext,
|
|
649
|
+
docId: params.docId,
|
|
650
|
+
request: params.request,
|
|
651
|
+
});
|
|
652
|
+
return buildToolResult({
|
|
653
|
+
ok: true,
|
|
654
|
+
action: "set_join_rule",
|
|
655
|
+
accountId: account.accountId,
|
|
656
|
+
docId: result.docId,
|
|
657
|
+
summary: "文档查看规则已更新",
|
|
658
|
+
raw: result.raw,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
case "set_member_auth": {
|
|
662
|
+
const result = await docClient.setDocMemberAuth({
|
|
663
|
+
account: accountContext,
|
|
664
|
+
docId: params.docId,
|
|
665
|
+
request: params.request,
|
|
666
|
+
});
|
|
667
|
+
return buildToolResult({
|
|
668
|
+
ok: true,
|
|
669
|
+
action: "set_member_auth",
|
|
670
|
+
accountId: account.accountId,
|
|
671
|
+
docId: result.docId,
|
|
672
|
+
summary: "文档通知范围及成员权限已更新",
|
|
673
|
+
raw: result.raw,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
case "grant_access": {
|
|
677
|
+
const result = await docClient.grantDocAccess({
|
|
678
|
+
account: accountContext,
|
|
679
|
+
docId: params.docId,
|
|
680
|
+
viewers: params.viewers,
|
|
681
|
+
collaborators: params.collaborators,
|
|
682
|
+
removeViewers: params.removeViewers,
|
|
683
|
+
removeCollaborators: params.removeCollaborators,
|
|
684
|
+
});
|
|
685
|
+
return buildToolResult({
|
|
686
|
+
ok: true,
|
|
687
|
+
action: "grant_access",
|
|
688
|
+
accountId: account.accountId,
|
|
689
|
+
docId: result.docId,
|
|
690
|
+
summary: summarizeDocAccess(result),
|
|
691
|
+
raw: result.raw,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
case "add_collaborators": {
|
|
695
|
+
const result = await docClient.addDocCollaborators({
|
|
696
|
+
account: accountContext,
|
|
697
|
+
docId: params.docId,
|
|
698
|
+
collaborators: params.collaborators,
|
|
699
|
+
});
|
|
700
|
+
return buildToolResult({
|
|
701
|
+
ok: true,
|
|
702
|
+
action: "add_collaborators",
|
|
703
|
+
accountId: account.accountId,
|
|
704
|
+
docId: result.docId,
|
|
705
|
+
summary: `协作者已添加:${result.addedCollaboratorCount ?? 0}`,
|
|
706
|
+
raw: result.raw,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
case "set_safety_setting": {
|
|
710
|
+
const result = await docClient.setDocSafetySetting({
|
|
711
|
+
account: accountContext,
|
|
712
|
+
docId: params.docId,
|
|
713
|
+
request: params.request,
|
|
714
|
+
});
|
|
715
|
+
return buildToolResult({
|
|
716
|
+
ok: true,
|
|
717
|
+
action: "set_safety_setting",
|
|
718
|
+
accountId: account.accountId,
|
|
719
|
+
docId: result.docId,
|
|
720
|
+
summary: "文档安全设置已更新",
|
|
721
|
+
raw: result.raw,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
case "create_collect": {
|
|
725
|
+
const result = await docClient.createCollect({
|
|
726
|
+
account: accountContext,
|
|
727
|
+
formInfo: params.formInfo,
|
|
728
|
+
spaceId: params.spaceId,
|
|
729
|
+
fatherId: params.fatherId,
|
|
730
|
+
});
|
|
731
|
+
const title = readString(result.title);
|
|
732
|
+
return buildToolResult({
|
|
733
|
+
ok: true,
|
|
734
|
+
action: "create_collect",
|
|
735
|
+
accountId: account.accountId,
|
|
736
|
+
formId: result.formId,
|
|
737
|
+
title: title || undefined,
|
|
738
|
+
summary: title ? `已创建收集表“${title}”` : "收集表已创建",
|
|
739
|
+
raw: result.raw,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
case "modify_collect": {
|
|
743
|
+
const result = await docClient.modifyCollect({
|
|
744
|
+
account: accountContext,
|
|
745
|
+
oper: params.oper,
|
|
746
|
+
formId: params.formId,
|
|
747
|
+
formInfo: params.formInfo,
|
|
748
|
+
});
|
|
749
|
+
const title = readString(result.title);
|
|
750
|
+
return buildToolResult({
|
|
751
|
+
ok: true,
|
|
752
|
+
action: "modify_collect",
|
|
753
|
+
accountId: account.accountId,
|
|
754
|
+
formId: result.formId,
|
|
755
|
+
title: title || undefined,
|
|
756
|
+
summary: title
|
|
757
|
+
? `收集表已更新(${result.oper}):“${title}”`
|
|
758
|
+
: `收集表已更新(${result.oper})`,
|
|
759
|
+
raw: result.raw,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
case "get_form_info": {
|
|
763
|
+
const result = await docClient.getFormInfo({
|
|
764
|
+
account: accountContext,
|
|
765
|
+
formId: params.formId,
|
|
766
|
+
});
|
|
767
|
+
return buildToolResult({
|
|
768
|
+
ok: true,
|
|
769
|
+
action: "get_form_info",
|
|
770
|
+
accountId: account.accountId,
|
|
771
|
+
formId: params.formId,
|
|
772
|
+
title: readString(result.formInfo?.form_title) || undefined,
|
|
773
|
+
summary: summarizeFormInfo(result),
|
|
774
|
+
raw: result.raw,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
case "get_form_answer": {
|
|
778
|
+
const result = await docClient.getFormAnswer({
|
|
779
|
+
account: accountContext,
|
|
780
|
+
repeatedId: params.repeatedId,
|
|
781
|
+
answerIds: params.answerIds,
|
|
782
|
+
});
|
|
783
|
+
return buildToolResult({
|
|
784
|
+
ok: true,
|
|
785
|
+
action: "get_form_answer",
|
|
786
|
+
accountId: account.accountId,
|
|
787
|
+
repeatedId: params.repeatedId,
|
|
788
|
+
summary: summarizeFormAnswer(result),
|
|
789
|
+
raw: result.raw,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
case "get_form_statistic": {
|
|
793
|
+
const result = await docClient.getFormStatistic({
|
|
794
|
+
account: accountContext,
|
|
795
|
+
requests: params.requests,
|
|
796
|
+
});
|
|
797
|
+
return buildToolResult({
|
|
798
|
+
ok: true,
|
|
799
|
+
action: "get_form_statistic",
|
|
800
|
+
accountId: account.accountId,
|
|
801
|
+
summary: summarizeFormStatistic(result),
|
|
802
|
+
raw: result.raw,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
case "get_sheet_properties": {
|
|
806
|
+
const result = await docClient.getSheetProperties({
|
|
807
|
+
account: accountContext,
|
|
808
|
+
docId: params.docId,
|
|
809
|
+
});
|
|
810
|
+
return buildToolResult({
|
|
811
|
+
ok: true,
|
|
812
|
+
action: "get_sheet_properties",
|
|
813
|
+
accountId: account.accountId,
|
|
814
|
+
docId: params.docId,
|
|
815
|
+
summary: summarizeSheetProperties(result),
|
|
816
|
+
raw: result.raw,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
default:
|
|
820
|
+
throw new Error(`Unknown wecom_doc action: ${String(params?.action || "")}`);
|
|
821
|
+
}
|
|
822
|
+
} catch (err) {
|
|
823
|
+
return buildToolResult({
|
|
824
|
+
ok: false,
|
|
825
|
+
action: String(params?.action || ""),
|
|
826
|
+
error: err instanceof Error ? err.message : String(err),
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
}));
|
|
831
|
+
api.logger?.info?.(`wecom_doc: registered (accounts=${eligibleAccounts.length})`);
|
|
832
|
+
};
|
|
833
|
+
}
|