@oomol-lab/open-connector 1.6.3 → 1.6.5
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/assets/open-connector/catalog/apps/aisa.json +568 -0
- package/assets/open-connector/catalog/apps/bilibili.json +2586 -0
- package/assets/open-connector/catalog/apps/bitwarden.json +1423 -0
- package/assets/open-connector/catalog/apps/cronfree.json +303 -0
- package/assets/open-connector/catalog/apps/cufinder.json +368 -0
- package/assets/open-connector/catalog/apps/feishu_app_bot.json +2 -2
- package/assets/open-connector/catalog/apps/gmail.json +73 -0
- package/assets/open-connector/catalog/apps/investoday_mcp.json +22228 -197
- package/assets/open-connector/catalog/apps/ixspy.json +2274 -0
- package/assets/open-connector/catalog/apps/jin10.json +1 -1
- package/assets/open-connector/catalog/apps/osv.json +663 -0
- package/assets/open-connector/catalog/apps/pdf_vector.json +329 -0
- package/assets/open-connector/catalog/apps/pulsetic.json +406 -0
- package/assets/open-connector/catalog/apps/shopee.json +1785 -0
- package/assets/open-connector/catalog/apps/slack.json +81 -0
- package/assets/open-connector/catalog/apps/slackbot.json +81 -0
- package/assets/open-connector/catalog/apps/speak_ai.json +360 -0
- package/assets/open-connector/catalog/apps/tongdaxin.json +11 -440
- package/assets/open-connector/catalog/apps/zyhub.json +1585 -0
- package/assets/open-connector/catalog/apps-index.json +1 -1
- package/package.json +1 -1
- package/src/core/json-schema.d.ts +5 -1
- package/src/core/json-schema.js +14 -2
- package/src/core/types.d.ts +9 -0
- package/src/oauth/oauth-credential-refresh-service.js +3 -1
- package/src/oauth/oauth-flow-service.js +17 -2
- package/src/oauth/oauth-token.d.ts +13 -0
- package/src/providers/action-contracts.generated.d.ts +16 -4
- package/src/providers/aisa/executors.d.ts +4 -0
- package/src/providers/aisa/executors.js +176 -0
- package/src/providers/bilibili/articles.d.ts +3 -0
- package/src/providers/bilibili/articles.js +369 -0
- package/src/providers/bilibili/executors.d.ts +8 -0
- package/src/providers/bilibili/executors.js +262 -0
- package/src/providers/bilibili/runtime.d.ts +95 -0
- package/src/providers/bilibili/runtime.js +238 -0
- package/src/providers/bilibili/upload.d.ts +14 -0
- package/src/providers/bilibili/upload.js +105 -0
- package/src/providers/bitwarden/executors.d.ts +3 -0
- package/src/providers/bitwarden/executors.js +146 -0
- package/src/providers/cronfree/executors.d.ts +4 -0
- package/src/providers/cronfree/executors.js +89 -0
- package/src/providers/cufinder/executors.d.ts +4 -0
- package/src/providers/cufinder/executors.js +105 -0
- package/src/providers/dropbox/executors.js +21 -3
- package/src/providers/elevenreader/runtime.js +6 -5
- package/src/providers/feishu_app_bot/scopes.js +1 -1
- package/src/providers/gmail/attachment-stream.d.ts +2 -0
- package/src/providers/gmail/attachment-stream.js +162 -0
- package/src/providers/gmail/executors.d.ts +3 -1
- package/src/providers/gmail/executors.js +53 -25
- package/src/providers/investoday_mcp/official-actions.js +19 -372
- package/src/providers/investoday_mcp/official-tool-types.d.ts +10 -0
- package/src/providers/investoday_mcp/official-tool-types.js +4 -0
- package/src/providers/investoday_mcp/official-tools/charts.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/charts.js +32 -0
- package/src/providers/investoday_mcp/official-tools/fixed-income.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/fixed-income.js +326 -0
- package/src/providers/investoday_mcp/official-tools/funds.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/funds.js +1085 -0
- package/src/providers/investoday_mcp/official-tools/hong-kong.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/hong-kong.js +275 -0
- package/src/providers/investoday_mcp/official-tools/indices-industries.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/indices-industries.js +642 -0
- package/src/providers/investoday_mcp/official-tools/macro-calendar.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/macro-calendar.js +398 -0
- package/src/providers/investoday_mcp/official-tools/other.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/other.js +131 -0
- package/src/providers/investoday_mcp/official-tools/research-news.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/research-news.js +302 -0
- package/src/providers/investoday_mcp/official-tools/stocks.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/stocks.js +2829 -0
- package/src/providers/ixspy/executors.d.ts +3 -0
- package/src/providers/ixspy/executors.js +220 -0
- package/src/providers/mcp-client.d.ts +1 -0
- package/src/providers/mcp-client.js +41 -27
- package/src/providers/osv/executors.d.ts +2 -0
- package/src/providers/osv/executors.js +11 -0
- package/src/providers/osv/runtime.d.ts +9 -0
- package/src/providers/osv/runtime.js +66 -0
- package/src/providers/pdf_vector/executors.d.ts +4 -0
- package/src/providers/pdf_vector/executors.js +108 -0
- package/src/providers/provider-runtime.d.ts +3 -0
- package/src/providers/provider-runtime.js +4 -1
- package/src/providers/pulsetic/executors.d.ts +4 -0
- package/src/providers/pulsetic/executors.js +126 -0
- package/src/providers/registry.generated.js +12 -0
- package/src/providers/sellersprite_mcp/executors.js +2 -1
- package/src/providers/shopee/actions.d.ts +2 -0
- package/src/providers/shopee/actions.js +474 -0
- package/src/providers/shopee/executors.d.ts +3 -0
- package/src/providers/shopee/executors.js +85 -0
- package/src/providers/shopee/runtime.d.ts +79 -0
- package/src/providers/shopee/runtime.js +693 -0
- package/src/providers/slack/actions.js +17 -0
- package/src/providers/slack/runtime.js +62 -4
- package/src/providers/speak_ai/executors.d.ts +3 -0
- package/src/providers/speak_ai/executors.js +187 -0
- package/src/providers/tongdaxin/executors.js +5 -4
- package/src/providers/tongdaxin/named-action-output.js +3 -9
- package/src/providers/tongdaxin/named-action-routing.js +1 -1
- package/src/providers/tongdaxin/named-actions.js +1 -6
- package/src/providers/tushare/runtime.js +22 -9
- package/src/providers/twenty_crm/runtime.js +4 -2
- package/src/providers/zyhub/executors.d.ts +3 -0
- package/src/providers/zyhub/executors.js +240 -0
- package/src/server/files/transit-files.d.ts +3 -2
- package/src/server/files/transit-files.js +31 -35
|
@@ -537,6 +537,23 @@ export const slackActions = [
|
|
|
537
537
|
inputSchema: s.object({ fileId: fileIdSchema }, { required: ["fileId"], description: "Input parameters for fetching a Slack file." }),
|
|
538
538
|
outputSchema: s.object({ file: fileSchema }, { required: ["file"], description: "The output payload for this action." }),
|
|
539
539
|
}),
|
|
540
|
+
action({
|
|
541
|
+
name: "download_file",
|
|
542
|
+
operationType: "read",
|
|
543
|
+
description: "Download a Slack-hosted file into transit storage using the connected identity's access.",
|
|
544
|
+
requiredScopes: ["files:read"],
|
|
545
|
+
inputSchema: s.requiredObject("The Slack file to download.", { fileId: fileIdSchema }),
|
|
546
|
+
outputSchema: s.requiredObject("A Slack file downloaded into transit storage.", {
|
|
547
|
+
fileId: fileIdSchema,
|
|
548
|
+
file: s.requiredObject("The downloaded file in transit storage.", {
|
|
549
|
+
fileId: s.nonEmptyString("The transit file identifier."),
|
|
550
|
+
downloadUrl: s.url("The transit URL for downloading the stored file."),
|
|
551
|
+
sizeBytes: s.nonNegativeInteger("The stored file size in bytes."),
|
|
552
|
+
name: s.nonEmptyString("The stored file name."),
|
|
553
|
+
mimeType: s.nonEmptyString("The stored file MIME type."),
|
|
554
|
+
}),
|
|
555
|
+
}),
|
|
556
|
+
}),
|
|
540
557
|
action({
|
|
541
558
|
name: "delete_file",
|
|
542
559
|
operationType: "destructive",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { compactObject, optionalBoolean, optionalInteger, optionalRecord, optionalString, requiredString, } from "../../core/cast.js";
|
|
1
|
+
import { compactObject, optionalBoolean, optionalInteger, optionalNumber, optionalRecord, optionalString, requiredString, } from "../../core/cast.js";
|
|
2
2
|
import { assertPublicHttpUrl, readBoundedResponseBytes } from "../../core/request.js";
|
|
3
|
-
import { createProviderTimeout, defineProviderExecutors, isAbortLikeError, ProviderRequestError, providerUserAgent, } from "../provider-runtime.js";
|
|
3
|
+
import { createProviderTimeout, defineProviderExecutors, isAbortLikeError, ProviderRequestError, providerInputError, providerResponseError, providerUserAgent, requiredInputString, requiredResponseRecord, runProviderRequest, } from "../provider-runtime.js";
|
|
4
4
|
import { slackConversationTypes } from "./constants.js";
|
|
5
5
|
export const slackApiBaseUrl = "https://slack.com/api";
|
|
6
6
|
const slackFileUrlMaxBytes = 100 * 1024 * 1024;
|
|
@@ -116,6 +116,9 @@ export const slackActionHandlers = {
|
|
|
116
116
|
get_file(input, context) {
|
|
117
117
|
return slackGetFile(input, context);
|
|
118
118
|
},
|
|
119
|
+
download_file(input, context) {
|
|
120
|
+
return slackDownloadFile(input, context);
|
|
121
|
+
},
|
|
119
122
|
delete_file(input, context) {
|
|
120
123
|
return slackDeleteFile(input, context);
|
|
121
124
|
},
|
|
@@ -545,6 +548,61 @@ async function slackGetFile(input, context) {
|
|
|
545
548
|
file: normalizeFile(payload.file ?? {}),
|
|
546
549
|
};
|
|
547
550
|
}
|
|
551
|
+
async function slackDownloadFile(input, context) {
|
|
552
|
+
const { transitFiles } = context;
|
|
553
|
+
if (!transitFiles) {
|
|
554
|
+
throw providerInputError("Slack download_file requires transit file storage");
|
|
555
|
+
}
|
|
556
|
+
const fileId = requiredInputString(input.fileId, "fileId");
|
|
557
|
+
return runProviderRequest({ signal: context.signal, label: "Slack file download" }, async (signal) => {
|
|
558
|
+
signal.throwIfAborted();
|
|
559
|
+
const infoUrl = slackApiUrl("files.info");
|
|
560
|
+
infoUrl.searchParams.set("file", fileId);
|
|
561
|
+
const payload = await slackGetJson(infoUrl, { ...context, signal });
|
|
562
|
+
const metadata = requiredResponseRecord(payload.file, "Slack file metadata");
|
|
563
|
+
const privateUrl = optionalString(metadata.url_private_download) ?? optionalString(metadata.url_private);
|
|
564
|
+
if (metadata.is_external === true || !privateUrl) {
|
|
565
|
+
throw providerInputError("This Slack file has no downloadable Slack-hosted content");
|
|
566
|
+
}
|
|
567
|
+
const url = assertPublicHttpUrl(privateUrl, { fieldName: "Slack file URL", createError: providerResponseError });
|
|
568
|
+
// Only Slack's file origin may receive the connection's bearer token.
|
|
569
|
+
// The shared fetch guard drops it if a redirect crosses origins.
|
|
570
|
+
if (url.origin !== "https://files.slack.com") {
|
|
571
|
+
throw providerResponseError("Slack file URL must use https://files.slack.com");
|
|
572
|
+
}
|
|
573
|
+
if ((optionalNumber(metadata.size) ?? 0) > transitFiles.maxBytes) {
|
|
574
|
+
throw new ProviderRequestError(413, `Slack file download exceeds ${transitFiles.maxBytes} bytes`);
|
|
575
|
+
}
|
|
576
|
+
const response = await context.fetcher(url, {
|
|
577
|
+
headers: { authorization: `Bearer ${context.accessToken}`, "user-agent": providerUserAgent },
|
|
578
|
+
signal,
|
|
579
|
+
});
|
|
580
|
+
try {
|
|
581
|
+
if (!response.ok) {
|
|
582
|
+
throw new ProviderRequestError(response.status, `Slack file download failed with HTTP ${response.status}`);
|
|
583
|
+
}
|
|
584
|
+
const mimeType = optionalString(response.headers.get("content-type")) ??
|
|
585
|
+
optionalString(metadata.mimetype) ??
|
|
586
|
+
"application/octet-stream";
|
|
587
|
+
// Slack can return a sign-in page instead of the requested bytes.
|
|
588
|
+
if (mimeType.split(";")[0]?.trim().toLowerCase() === "text/html" && metadata.mimetype !== "text/html") {
|
|
589
|
+
throw providerResponseError("Slack returned an HTML page instead of the requested file");
|
|
590
|
+
}
|
|
591
|
+
const bytes = await readBoundedResponseBytes(response, {
|
|
592
|
+
maxBytes: transitFiles.maxBytes,
|
|
593
|
+
fieldName: "Slack file download",
|
|
594
|
+
createError: (message) => new ProviderRequestError(413, message),
|
|
595
|
+
});
|
|
596
|
+
signal.throwIfAborted();
|
|
597
|
+
const name = optionalString(metadata.name) ?? fileId;
|
|
598
|
+
const file = await transitFiles.create(new File([Uint8Array.from(bytes)], name, { type: mimeType }));
|
|
599
|
+
return { fileId, file };
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
await response.body?.cancel().catch(() => undefined);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
548
606
|
async function slackDeleteFile(input, context) {
|
|
549
607
|
await slackRequestJson({
|
|
550
608
|
...context,
|
|
@@ -688,10 +746,10 @@ function readSlackTokenKind(accessToken, metadata) {
|
|
|
688
746
|
if (rawTokenType == "user") {
|
|
689
747
|
return "user";
|
|
690
748
|
}
|
|
691
|
-
if (accessToken.startsWith("xoxb-")) {
|
|
749
|
+
if (accessToken.startsWith("xoxb-") || accessToken.startsWith("xoxe.xoxb-")) {
|
|
692
750
|
return "bot";
|
|
693
751
|
}
|
|
694
|
-
if (accessToken.startsWith("xoxp-")) {
|
|
752
|
+
if (accessToken.startsWith("xoxp-") || accessToken.startsWith("xoxe.xoxp-")) {
|
|
695
753
|
return "user";
|
|
696
754
|
}
|
|
697
755
|
return undefined;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { sha256Hex } from "../../core/aws-sigv4.js";
|
|
2
|
+
import { compactObject, looseArray, optionalInteger, optionalString } from "../../core/cast.js";
|
|
3
|
+
import { defineApiKeyProviderExecutors, providerInputError, ProviderRequestError, providerResponseError, providerUserAgent, readProviderJsonBody, requiredInputString, requiredResponseRecord, runProviderRequest, } from "../provider-runtime.js";
|
|
4
|
+
const service = "speak_ai";
|
|
5
|
+
const apiBaseUrl = "https://api.speakai.co/v1";
|
|
6
|
+
const tokenCache = new Map();
|
|
7
|
+
const handlers = {
|
|
8
|
+
async upload_media(input, context) {
|
|
9
|
+
const data = await request("/media/upload", {
|
|
10
|
+
method: "POST",
|
|
11
|
+
body: compactObject({
|
|
12
|
+
name: requiredInputString(input.name, "name"),
|
|
13
|
+
url: requiredInputString(input.mediaUrl, "mediaUrl"),
|
|
14
|
+
mediaType: optionalString(input.mediaType),
|
|
15
|
+
description: optionalString(input.description),
|
|
16
|
+
sourceLanguage: optionalString(input.sourceLanguage),
|
|
17
|
+
tags: optionalString(input.tags),
|
|
18
|
+
folderId: optionalString(input.folderId),
|
|
19
|
+
}),
|
|
20
|
+
}, context, "execute");
|
|
21
|
+
return { mediaId: requiredResponseString(data.mediaId, "Speak AI upload mediaId") };
|
|
22
|
+
},
|
|
23
|
+
async list_media(input, context) {
|
|
24
|
+
const data = await request(buildUrl("/media", input, {
|
|
25
|
+
mediaType: "mediaType",
|
|
26
|
+
page: "page",
|
|
27
|
+
pageSize: "pageSize",
|
|
28
|
+
sortBy: "sortBy",
|
|
29
|
+
filterMedia: "filterMedia",
|
|
30
|
+
filterName: "filterName",
|
|
31
|
+
folderId: "folderId",
|
|
32
|
+
}), { method: "GET" }, context, "execute");
|
|
33
|
+
return {
|
|
34
|
+
totalCount: requireInteger(data.totalCount, "totalCount"),
|
|
35
|
+
pages: requireInteger(data.pages, "pages"),
|
|
36
|
+
media: looseArray(data.mediaList),
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
async get_media_status(input, context) {
|
|
40
|
+
const data = await media(input, context, "status");
|
|
41
|
+
return {
|
|
42
|
+
mediaId: requiredResponseString(data.mediaId, "mediaId"),
|
|
43
|
+
state: requiredResponseString(data.state, "state"),
|
|
44
|
+
media: data,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
async get_transcript(input, context) {
|
|
48
|
+
const data = await media(input, context, "transcript");
|
|
49
|
+
return { mediaId: requiredResponseString(data.mediaId, "mediaId"), transcript: data };
|
|
50
|
+
},
|
|
51
|
+
async get_media_insights(input, context) {
|
|
52
|
+
const data = await media(input, context, "insight");
|
|
53
|
+
return { mediaId: requiredResponseString(data.mediaId, "mediaId"), insights: data };
|
|
54
|
+
},
|
|
55
|
+
async list_folders(input, context) {
|
|
56
|
+
const data = await request(buildUrl("/folder", input, { page: "page", pageSize: "pageSize", sortBy: "sortBy" }), { method: "GET" }, context, "execute");
|
|
57
|
+
return {
|
|
58
|
+
totalCount: requireInteger(data.totalCount, "totalCount"),
|
|
59
|
+
pages: data.pages == null ? null : requireInteger(data.pages, "pages"),
|
|
60
|
+
folders: looseArray(data.folders),
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
export const executors = defineApiKeyProviderExecutors(service, handlers, {
|
|
65
|
+
skipDnsValidation: true,
|
|
66
|
+
});
|
|
67
|
+
export const credentialValidators = {
|
|
68
|
+
async apiKey(input, { fetcher }) {
|
|
69
|
+
const token = await authenticate(input.apiKey, fetcher, "validate");
|
|
70
|
+
return {
|
|
71
|
+
profile: { accountId: token.email ?? "speak-ai-api-key", displayName: token.email ?? "Speak AI API Key" },
|
|
72
|
+
grantedScopes: [],
|
|
73
|
+
metadata: { apiBaseUrl, validationEndpoint: "/auth/accessToken" },
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
async function media(input, context, family) {
|
|
78
|
+
const mediaId = requiredInputString(input.mediaId, "mediaId");
|
|
79
|
+
const data = await request(`/media/${family}/${encodeURIComponent(mediaId)}`, { method: "GET" }, context, "execute");
|
|
80
|
+
return data.state === "notUploaded" && data.mediaId === undefined ? { ...data, mediaId } : data;
|
|
81
|
+
}
|
|
82
|
+
async function request(path, init, context, phase) {
|
|
83
|
+
let token = await authenticate(context.apiKey, context.fetcher, phase);
|
|
84
|
+
let response = await fetchToken(path, init, context, token.accessToken);
|
|
85
|
+
let payload = await readProviderJsonBody(response, {
|
|
86
|
+
emptyBody: {},
|
|
87
|
+
invalidJsonMessage: "Speak AI returned invalid JSON",
|
|
88
|
+
});
|
|
89
|
+
if (phase === "execute" && response.status === 401) {
|
|
90
|
+
tokenCache.delete(sha256Hex(context.apiKey));
|
|
91
|
+
token = await authenticate(context.apiKey, context.fetcher, phase);
|
|
92
|
+
response = await fetchToken(path, init, context, token.accessToken);
|
|
93
|
+
payload = await readProviderJsonBody(response, {
|
|
94
|
+
emptyBody: {},
|
|
95
|
+
invalidJsonMessage: "Speak AI returned invalid JSON",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (!response.ok)
|
|
99
|
+
throw error(response.status, payload, phase);
|
|
100
|
+
const envelope = requiredResponseRecord(payload, "Speak AI response");
|
|
101
|
+
if (envelope.state === "notUploaded" && envelope.data === undefined)
|
|
102
|
+
return envelope;
|
|
103
|
+
return requiredResponseRecord(envelope.data, "Speak AI response data");
|
|
104
|
+
}
|
|
105
|
+
function requiredResponseString(value, label) {
|
|
106
|
+
const result = optionalString(value);
|
|
107
|
+
if (!result)
|
|
108
|
+
throw providerResponseError(`${label} must be a non-empty string`);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
function fetchToken(path, init, context, accessToken) {
|
|
112
|
+
return runProviderRequest({ signal: context.signal, label: "Speak AI" }, (signal) => context.fetcher(path instanceof URL ? path : `${apiBaseUrl}${path}`, {
|
|
113
|
+
method: init.method,
|
|
114
|
+
headers: {
|
|
115
|
+
accept: "application/json",
|
|
116
|
+
"content-type": "application/json",
|
|
117
|
+
"user-agent": providerUserAgent,
|
|
118
|
+
"x-speakai-key": context.apiKey,
|
|
119
|
+
"x-access-token": accessToken,
|
|
120
|
+
},
|
|
121
|
+
body: init.body ? JSON.stringify(init.body) : undefined,
|
|
122
|
+
signal,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
async function authenticate(apiKey, fetcher, phase) {
|
|
126
|
+
const key = sha256Hex(apiKey);
|
|
127
|
+
if (phase === "validate")
|
|
128
|
+
tokenCache.delete(key);
|
|
129
|
+
const cached = tokenCache.get(key);
|
|
130
|
+
if (cached) {
|
|
131
|
+
const value = await cached;
|
|
132
|
+
if (Date.now() < value.expiresAt)
|
|
133
|
+
return value;
|
|
134
|
+
tokenCache.delete(key);
|
|
135
|
+
}
|
|
136
|
+
const pending = runProviderRequest({ label: "Speak AI authentication" }, async (signal) => {
|
|
137
|
+
const response = await fetcher(`${apiBaseUrl}/auth/accessToken`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { "content-type": "application/json", "user-agent": providerUserAgent, "x-speakai-key": apiKey },
|
|
140
|
+
body: "{}",
|
|
141
|
+
signal,
|
|
142
|
+
});
|
|
143
|
+
const payload = await readProviderJsonBody(response, {
|
|
144
|
+
emptyBody: {},
|
|
145
|
+
invalidJsonMessage: "Speak AI returned invalid JSON",
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok)
|
|
148
|
+
throw error(response.status, payload, phase);
|
|
149
|
+
const data = requiredResponseRecord(requiredResponseRecord(payload, "Speak AI authentication response").data, "Speak AI authentication data");
|
|
150
|
+
return {
|
|
151
|
+
accessToken: requiredInputString(data.accessToken, "accessToken"),
|
|
152
|
+
email: optionalString(data.email),
|
|
153
|
+
expiresAt: Date.now() + 3_000_000,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
tokenCache.set(key, pending);
|
|
157
|
+
try {
|
|
158
|
+
return await pending;
|
|
159
|
+
}
|
|
160
|
+
catch (cause) {
|
|
161
|
+
tokenCache.delete(key);
|
|
162
|
+
throw cause;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function error(status, payload, phase) {
|
|
166
|
+
const body = requiredResponseRecord(payload, "Speak AI error response");
|
|
167
|
+
const message = optionalString(body.message) ?? "Speak AI request failed";
|
|
168
|
+
if (status === 429)
|
|
169
|
+
return new ProviderRequestError(429, message, payload);
|
|
170
|
+
if (phase === "validate" && optionalInteger(body.code) === 401)
|
|
171
|
+
return providerInputError(message);
|
|
172
|
+
if (status === 400 || status === 422)
|
|
173
|
+
return providerInputError(message);
|
|
174
|
+
return new ProviderRequestError(status || 502, message, payload);
|
|
175
|
+
}
|
|
176
|
+
function requireInteger(value, label) {
|
|
177
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
178
|
+
throw new ProviderRequestError(502, `Speak AI ${label} must be an integer`);
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
function buildUrl(path, input, mapping) {
|
|
182
|
+
const url = new URL(`${apiBaseUrl}${path}`);
|
|
183
|
+
for (const [key, query] of Object.entries(mapping))
|
|
184
|
+
if (input[key] != null)
|
|
185
|
+
url.searchParams.set(query, String(input[key]));
|
|
186
|
+
return url;
|
|
187
|
+
}
|
|
@@ -20,7 +20,7 @@ const handlers = {
|
|
|
20
20
|
if (!argumentsValue)
|
|
21
21
|
throw new ProviderRequestError(400, "arguments must be a JSON object");
|
|
22
22
|
assertToolArgumentsSize(argumentsValue);
|
|
23
|
-
const result = await withTongdaxinClient(context, "execute", (client) => client.callTool({ name: toolName, arguments: argumentsValue }, { timeout: requestTimeoutMs, signal: context.signal }));
|
|
23
|
+
const result = await withTongdaxinClient(context, "execute", false, (client) => client.callTool({ name: toolName, arguments: argumentsValue }, { timeout: requestTimeoutMs, signal: context.signal }));
|
|
24
24
|
if (!("toolResult" in result) && result.isError) {
|
|
25
25
|
throw new ProviderRequestError(502, `Tongdaxin MCP tool ${toolName} returned an error`, result);
|
|
26
26
|
}
|
|
@@ -48,7 +48,7 @@ for (const actionName of Object.keys(resolveNamedHandlers())) {
|
|
|
48
48
|
tool.annotations.destructiveHint === true)) {
|
|
49
49
|
throw new ProviderRequestError(403, `Tongdaxin MCP does not currently affirm ${tool.name} as a non-destructive read-only tool`);
|
|
50
50
|
}
|
|
51
|
-
const result = await withTongdaxinClient(context, "execute", (client) => client.callTool({ name: toolCall.toolName, arguments: toolCall.arguments }, { timeout: requestTimeoutMs, signal: context.signal }));
|
|
51
|
+
const result = await withTongdaxinClient(context, "execute", true, (client) => client.callTool({ name: toolCall.toolName, arguments: toolCall.arguments }, { timeout: requestTimeoutMs, signal: context.signal }));
|
|
52
52
|
if (!("toolResult" in result) && result.isError) {
|
|
53
53
|
throw new ProviderRequestError(502, `Tongdaxin MCP tool ${toolCall.toolName} returned an error`, result);
|
|
54
54
|
}
|
|
@@ -78,7 +78,7 @@ export const credentialValidators = {
|
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
80
|
async function discoverSupportedTools(context, phase) {
|
|
81
|
-
const result = await withTongdaxinClient(context, phase, (client) => client.listTools({}, { timeout: requestTimeoutMs, signal: context.signal }));
|
|
81
|
+
const result = await withTongdaxinClient(context, phase, true, (client) => client.listTools({}, { timeout: requestTimeoutMs, signal: context.signal }));
|
|
82
82
|
return result.tools.map((tool) => ({
|
|
83
83
|
name: tool.name,
|
|
84
84
|
description: tool.description,
|
|
@@ -86,7 +86,7 @@ async function discoverSupportedTools(context, phase) {
|
|
|
86
86
|
inputSchema: tool.inputSchema,
|
|
87
87
|
}));
|
|
88
88
|
}
|
|
89
|
-
async function withTongdaxinClient(context, phase, run) {
|
|
89
|
+
async function withTongdaxinClient(context, phase, retryOnSessionNotFound, run) {
|
|
90
90
|
return withMcpClient({
|
|
91
91
|
endpoint,
|
|
92
92
|
transport: "streamable_http",
|
|
@@ -97,6 +97,7 @@ async function withTongdaxinClient(context, phase, run) {
|
|
|
97
97
|
},
|
|
98
98
|
redirect: "error",
|
|
99
99
|
signal: context.signal,
|
|
100
|
+
retryOnSessionNotFound,
|
|
100
101
|
mapError: (error) => mapTongdaxinError(error, phase),
|
|
101
102
|
}, run);
|
|
102
103
|
}
|
|
@@ -40,7 +40,7 @@ export function normalizeTongdaxinNamedActionOutput(actionName, result) {
|
|
|
40
40
|
case "get_board_and_industry_data":
|
|
41
41
|
case "get_hot_topics":
|
|
42
42
|
case "get_hk_financials":
|
|
43
|
-
return normalizeF10(resultObject
|
|
43
|
+
return normalizeF10(resultObject);
|
|
44
44
|
default:
|
|
45
45
|
return assertNever(actionName);
|
|
46
46
|
}
|
|
@@ -103,15 +103,9 @@ function normalizeSearch(resultObject, result) {
|
|
|
103
103
|
result,
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
-
function normalizeF10(resultObject
|
|
106
|
+
function normalizeF10(resultObject) {
|
|
107
107
|
const response = requiredResponseRecord(resultObject.response, "Tongdaxin F10 response");
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
ok: optionalBoolean(resultObject.ok) ?? optionalBoolean(response.ok) ?? null,
|
|
111
|
-
summary: asStringOrNull(transformed.summary ?? resultObject.summary),
|
|
112
|
-
tables: requireResponseObjectArray(transformed.tables, "Tongdaxin F10 tables"),
|
|
113
|
-
result,
|
|
114
|
-
};
|
|
108
|
+
return response.transformed ?? null;
|
|
115
109
|
}
|
|
116
110
|
function assertNever(value) {
|
|
117
111
|
throw providerResponseError(`unhandled Tongdaxin action output: ${value}`);
|
|
@@ -295,7 +295,7 @@ function capitalAndTradingArguments(input) {
|
|
|
295
295
|
extra: input.date,
|
|
296
296
|
};
|
|
297
297
|
case "BLOCK_TRADE_INTENTION":
|
|
298
|
-
return dataSection("TdxSharePCCW.
|
|
298
|
+
return dataSection("TdxSharePCCW.tdxf10_gg_jyds", code, "yxsbxx");
|
|
299
299
|
case "MARGIN_TRADING":
|
|
300
300
|
return dataSection("TdxSharePCCW.tdxf10_gg_jyds", code, "rzrq");
|
|
301
301
|
case "REFINANCING":
|
|
@@ -62,12 +62,7 @@ const searchOutputSchema = s.object("A normalized Tongdaxin current-information
|
|
|
62
62
|
data: s.nullable(s.unknown("The provider-defined search rows returned by Wenda, or null when unavailable.")),
|
|
63
63
|
result: rawResultSchema,
|
|
64
64
|
});
|
|
65
|
-
const f10OutputSchema = s.
|
|
66
|
-
ok: s.nullableBoolean("Whether the underlying F10 request reported success."),
|
|
67
|
-
summary: s.nullableString("The structured result summary, or null when unavailable."),
|
|
68
|
-
tables: s.array("The formatted F10 result tables.", looseRowSchema),
|
|
69
|
-
result: rawResultSchema,
|
|
70
|
-
});
|
|
65
|
+
const f10OutputSchema = s.unknown("The transformed F10 data returned by Tongdaxin, preserved as any JSON value.");
|
|
71
66
|
function defineReadAction(name, description, inputSchema, outputSchema = f10OutputSchema) {
|
|
72
67
|
return defineProviderAction(service, {
|
|
73
68
|
name,
|
|
@@ -234,12 +234,18 @@ export const tushareActionHandlers = {
|
|
|
234
234
|
},
|
|
235
235
|
};
|
|
236
236
|
export async function validateTushareCredential(apiKey, fetcher, signal) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
237
|
+
try {
|
|
238
|
+
const payload = await tushareRequest({
|
|
239
|
+
apiName: "daily",
|
|
240
|
+
params: { ts_code: "000001.SZ", trade_date: "20180903" },
|
|
241
|
+
fields: "ts_code,trade_date",
|
|
242
|
+
}, { apiKey, fetcher, signal });
|
|
243
|
+
normalizeTableData(payload.data);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (!(error instanceof ProviderRequestError) || error.status !== 403)
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
243
249
|
return {
|
|
244
250
|
profile: {
|
|
245
251
|
accountId: "tushare-token",
|
|
@@ -248,7 +254,7 @@ export async function validateTushareCredential(apiKey, fetcher, signal) {
|
|
|
248
254
|
grantedScopes: [],
|
|
249
255
|
metadata: {
|
|
250
256
|
apiBaseUrl: tushareApiBaseUrl,
|
|
251
|
-
validationApiName: "
|
|
257
|
+
validationApiName: "daily",
|
|
252
258
|
credentialHelpUrl: "https://tushare.pro/document/1?doc_id=39",
|
|
253
259
|
},
|
|
254
260
|
};
|
|
@@ -333,8 +339,10 @@ async function readJsonPayload(response) {
|
|
|
333
339
|
}
|
|
334
340
|
function createTushareHttpError(status, payload) {
|
|
335
341
|
const message = extractErrorMessage(payload) ?? `Tushare request failed with ${status || 500}`;
|
|
336
|
-
if (status === 401
|
|
342
|
+
if (status === 401)
|
|
337
343
|
return new ProviderRequestError(400, message, payload);
|
|
344
|
+
if (status === 403)
|
|
345
|
+
return new ProviderRequestError(403, message, payload);
|
|
338
346
|
if (status === 429)
|
|
339
347
|
return new ProviderRequestError(429, message, payload);
|
|
340
348
|
return new ProviderRequestError(status || 502, message, payload);
|
|
@@ -343,12 +351,17 @@ function createTushareApiError(code, payload) {
|
|
|
343
351
|
const message = `Tushare request failed: ${extractErrorMessage(payload) ?? `code ${code}`}`;
|
|
344
352
|
if (code === 40101)
|
|
345
353
|
return new ProviderRequestError(400, message, payload);
|
|
346
|
-
if (code
|
|
354
|
+
if (isTusharePermissionError(code, message))
|
|
347
355
|
return new ProviderRequestError(403, message, payload);
|
|
348
356
|
if (code === 2003 || code === 2004 || message.includes("频率超限"))
|
|
349
357
|
return new ProviderRequestError(429, message, payload);
|
|
350
358
|
return new ProviderRequestError(502, message, payload);
|
|
351
359
|
}
|
|
360
|
+
function isTusharePermissionError(code, message) {
|
|
361
|
+
return (code === 2002 ||
|
|
362
|
+
message.includes("没有权限访问该接口") ||
|
|
363
|
+
(message.includes("没有接口(") && message.includes("访问权限")));
|
|
364
|
+
}
|
|
352
365
|
function normalizeTableData(value) {
|
|
353
366
|
const data = requiredResponseRecord(value, "data");
|
|
354
367
|
return {
|
|
@@ -57,9 +57,10 @@ async function listMetadataObjects(context, phase = "execute") {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
async function listRecords(input, context) {
|
|
60
|
+
const objectNamePlural = readRequiredString(input.objectNamePlural, "objectNamePlural");
|
|
60
61
|
const payload = await requestTwentyCrmJson({
|
|
61
62
|
method: "GET",
|
|
62
|
-
path: `/rest/${encodeObjectName(
|
|
63
|
+
path: `/rest/${encodeObjectName(objectNamePlural)}`,
|
|
63
64
|
query: compactObject({
|
|
64
65
|
limit: optionalInteger(input.limit),
|
|
65
66
|
starting_after: readOptionalString(input.startingAfter),
|
|
@@ -70,8 +71,9 @@ async function listRecords(input, context) {
|
|
|
70
71
|
}),
|
|
71
72
|
}, context);
|
|
72
73
|
const record = requireProviderObject(payload, "Twenty CRM list records response");
|
|
74
|
+
const data = requireProviderObject(record.data, "Twenty CRM data");
|
|
73
75
|
return {
|
|
74
|
-
records: readArray(
|
|
76
|
+
records: readArray(data, objectNamePlural).map((item) => requireProviderObject(item, "Twenty CRM record")),
|
|
75
77
|
pageInfo: optionalRecord(record.pageInfo) ?? {},
|
|
76
78
|
raw: record,
|
|
77
79
|
};
|