@cjhyy/code-shell-server 0.9.43 → 0.9.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attachment-service.d.ts +2 -0
- package/dist/attachment-service.js +40 -27
- package/dist/bin/code-shell-link-cleanup.d.ts +2 -0
- package/dist/bin/code-shell-link-cleanup.js +3 -0
- package/dist/desktop-web/http-api.d.ts +3 -0
- package/dist/desktop-web/http-api.js +16 -1
- package/dist/hub/configuration.d.ts +6 -3
- package/dist/hub/configuration.js +19 -6
- package/dist/hub/mcp-probe.d.ts +4 -24
- package/dist/hub/mcp-probe.js +18 -389
- package/dist/hub/model-probe.d.ts +2 -20
- package/dist/hub/model-probe.js +2 -193
- package/dist/hub/run-replay.d.ts +7 -1
- package/dist/hub/run-replay.js +10 -2
- package/dist/hub/session-management.js +18 -53
- package/dist/hub/skills-management.js +6 -8
- package/dist/hub/uploads.d.ts +10 -0
- package/dist/hub/uploads.js +107 -15
- package/dist/index.desktop-web.d.ts +1 -0
- package/dist/index.desktop-web.js +1 -0
- package/dist/index.links.d.ts +3 -0
- package/dist/index.links.js +2 -0
- package/dist/index.panels.d.ts +1 -0
- package/dist/index.panels.js +1 -0
- package/dist/index.serve.d.ts +1 -0
- package/dist/index.serve.js +1 -0
- package/dist/index.storage.d.ts +1 -0
- package/dist/index.storage.js +1 -0
- package/dist/links/catalog.js +2 -0
- package/dist/links/diagnostics.d.ts +60 -0
- package/dist/links/diagnostics.js +184 -0
- package/dist/links/http.d.ts +10 -8
- package/dist/links/http.js +144 -125
- package/dist/links/legacy-cleanup-cli.d.ts +3 -0
- package/dist/links/legacy-cleanup-cli.js +64 -0
- package/dist/links/providers/defaults.d.ts +5 -0
- package/dist/links/providers/defaults.js +782 -0
- package/dist/links/providers/index.d.ts +7 -0
- package/dist/links/providers/index.js +22 -0
- package/dist/links/remote-catalog.d.ts +10 -3
- package/dist/links/remote-catalog.js +76 -29
- package/dist/links/runtime/operation.d.ts +9 -0
- package/dist/links/runtime/operation.js +16 -0
- package/dist/links/service.d.ts +18 -2
- package/dist/links/service.js +593 -62
- package/dist/links/types.d.ts +21 -1
- package/dist/mobile-remote/codex-parse.d.ts +3 -1
- package/dist/mobile-remote/codex-parse.js +45 -11
- package/dist/mobile-remote/mobile-chat-turn.d.ts +1 -0
- package/dist/mobile-remote/mobile-chat-turn.js +3 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +13 -0
- package/dist/mobile-remote/mobile-history.d.ts +10 -1
- package/dist/mobile-remote/mobile-history.js +24 -16
- package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
- package/dist/mobile-remote/remote-host-manager.js +23 -8
- package/dist/mobile-remote/resident-agent.js +17 -4
- package/dist/mobile-remote/room-manager.d.ts +23 -13
- package/dist/mobile-remote/room-manager.js +102 -20
- package/dist/mobile-remote/room-model-tracing.d.ts +18 -0
- package/dist/mobile-remote/room-model-tracing.js +196 -0
- package/dist/model-trace-history.d.ts +29 -0
- package/dist/model-trace-history.js +198 -0
- package/dist/model-trace-http.d.ts +14 -0
- package/dist/model-trace-http.js +105 -0
- package/dist/panels/agent-task-host.js +1 -0
- package/dist/panels/directory-bookmarks.js +62 -29
- package/dist/panels/http.d.ts +1 -0
- package/dist/panels/hub-binding.js +5 -5
- package/dist/panels/model-trace-channel.d.ts +21 -0
- package/dist/panels/model-trace-channel.js +110 -0
- package/dist/panels/model-traces.d.ts +39 -0
- package/dist/panels/model-traces.js +222 -0
- package/dist/panels/process-service.d.ts +12 -2
- package/dist/panels/process-service.js +49 -6
- package/dist/panels/runtime.d.ts +1 -0
- package/dist/panels/runtime.js +54 -6
- package/dist/panels/tool-jobs.d.ts +2 -0
- package/dist/panels/tool-jobs.js +21 -1
- package/dist/serve/cli.js +8 -1
- package/dist/serve/headless-server.js +68 -14
- package/dist/sessions-service.js +17 -27
- package/dist/worker-bridge-core.js +11 -0
- package/package.json +8 -7
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import { executeLinkOperation } from "../runtime/operation.js";
|
|
2
|
+
import { GITHUB_SOURCE_ACTIONS, githubSourceEndpoint, normalizeGithubSourceResult, slackSourceParams, normalizeSlackSourceResult, } from "@cjhyy/code-shell-core/extension";
|
|
3
|
+
import { githubIssueStateParameters } from "@cjhyy/code-shell-core/extension";
|
|
4
|
+
import { githubRepositoryParameters, githubSetStarredParameters, } from "@cjhyy/code-shell-core/extension";
|
|
5
|
+
import { FIGMA_RESPONSE_BYTES, FIGMA_RESPONSE_TOO_LARGE, figmaActionEndpoint, figmaActionParameters, normalizeFigmaActionResult, } from "@cjhyy/code-shell-core/extension";
|
|
6
|
+
import { asArray, asRecord, firstString, intParam, linkRequestJson, linkRequestStatus, pathParam, pathSegmentParam, pick, stringParam, } from "@cjhyy/code-shell-core/extension";
|
|
7
|
+
import { FIGMA_MCP_ACTION_IDS } from "@cjhyy/code-shell-link";
|
|
8
|
+
const defaultRuntime = {
|
|
9
|
+
GITHUB_SOURCE_ACTIONS,
|
|
10
|
+
githubSourceEndpoint,
|
|
11
|
+
normalizeGithubSourceResult,
|
|
12
|
+
slackSourceParams,
|
|
13
|
+
normalizeSlackSourceResult,
|
|
14
|
+
githubIssueStateParameters,
|
|
15
|
+
githubRepositoryParameters,
|
|
16
|
+
githubSetStarredParameters,
|
|
17
|
+
FIGMA_RESPONSE_BYTES,
|
|
18
|
+
FIGMA_RESPONSE_TOO_LARGE,
|
|
19
|
+
figmaActionEndpoint,
|
|
20
|
+
figmaActionParameters,
|
|
21
|
+
normalizeFigmaActionResult,
|
|
22
|
+
asArray,
|
|
23
|
+
asRecord,
|
|
24
|
+
firstString,
|
|
25
|
+
intParam,
|
|
26
|
+
linkRequestJson,
|
|
27
|
+
linkRequestStatus,
|
|
28
|
+
pathParam,
|
|
29
|
+
pathSegmentParam,
|
|
30
|
+
pick,
|
|
31
|
+
stringParam,
|
|
32
|
+
};
|
|
33
|
+
/** Product-owned HTTP implementations. Importing the factory does not install providers. */
|
|
34
|
+
export function createDefaultLinkProviders(runtime = defaultRuntime) {
|
|
35
|
+
const { GITHUB_SOURCE_ACTIONS, githubSourceEndpoint, normalizeGithubSourceResult, slackSourceParams, normalizeSlackSourceResult, githubIssueStateParameters, githubRepositoryParameters, githubSetStarredParameters, FIGMA_RESPONSE_BYTES, FIGMA_RESPONSE_TOO_LARGE, figmaActionEndpoint, figmaActionParameters, normalizeFigmaActionResult, asArray, asRecord, firstString, intParam, linkRequestJson, linkRequestStatus, pathParam, pathSegmentParam, pick, stringParam, } = runtime;
|
|
36
|
+
const bearer = (token) => ({ Authorization: `Bearer ${token}` });
|
|
37
|
+
const gitlabHeaders = (ctx) => ctx.authKind === "oauth" ? bearer(ctx.token) : { "PRIVATE-TOKEN": ctx.token };
|
|
38
|
+
const figmaHeaders = (ctx) => ctx.authKind === "oauth" ? bearer(ctx.token) : { "X-Figma-Token": ctx.token };
|
|
39
|
+
const GITHUB_API_VERSION = "2022-11-28";
|
|
40
|
+
const NOTION_API_VERSION = "2022-06-28";
|
|
41
|
+
const githubHeaders = (token) => ({
|
|
42
|
+
...bearer(token),
|
|
43
|
+
Accept: "application/vnd.github+json",
|
|
44
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION,
|
|
45
|
+
"User-Agent": "CodeShell-Link",
|
|
46
|
+
});
|
|
47
|
+
function action(id, title, description, execute, risk = "read") {
|
|
48
|
+
return { id, title, description, risk, execute };
|
|
49
|
+
}
|
|
50
|
+
async function request(context, url, options = {}) {
|
|
51
|
+
return linkRequestJson({
|
|
52
|
+
url: typeof url === "string" ? new URL(url) : url,
|
|
53
|
+
method: options.method,
|
|
54
|
+
headers: options.headers ?? bearer(context.token),
|
|
55
|
+
body: options.body,
|
|
56
|
+
signal: context.signal,
|
|
57
|
+
fetchImpl: context.fetchImpl,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function identity(data, idKeys, labelKeys, detailKeys = []) {
|
|
61
|
+
const record = asRecord(data) ?? {};
|
|
62
|
+
const externalAccountId = firstString(...idKeys.map((key) => record[key])) ?? String(record.id ?? "account");
|
|
63
|
+
const label = firstString(...labelKeys.map((key) => record[key])) ?? externalAccountId;
|
|
64
|
+
const detail = firstString(...detailKeys.map((key) => record[key]));
|
|
65
|
+
return { externalAccountId, label, ...(detail ? { detail } : {}) };
|
|
66
|
+
}
|
|
67
|
+
function compactList(data, keys, limit = 50) {
|
|
68
|
+
return asArray(data)
|
|
69
|
+
.slice(0, limit)
|
|
70
|
+
.map((value) => pick(value, keys));
|
|
71
|
+
}
|
|
72
|
+
const github = {
|
|
73
|
+
id: "github",
|
|
74
|
+
displayName: "GitHub",
|
|
75
|
+
tokenLabel: "Fine-grained PAT",
|
|
76
|
+
tokenPlaceholder: "github_pat_…",
|
|
77
|
+
async validate(context) {
|
|
78
|
+
const data = await request(context, "https://api.github.com/user", {
|
|
79
|
+
headers: githubHeaders(context.token),
|
|
80
|
+
});
|
|
81
|
+
const repositoriesUrl = new URL("https://api.github.com/user/repos");
|
|
82
|
+
repositoriesUrl.searchParams.set("per_page", "12");
|
|
83
|
+
repositoriesUrl.searchParams.set("sort", "updated");
|
|
84
|
+
const repositories = await request(context, repositoriesUrl, {
|
|
85
|
+
headers: githubHeaders(context.token),
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
...identity(data, ["id"], ["login", "name"], ["name", "email"]),
|
|
89
|
+
resourceLabels: asArray(repositories)
|
|
90
|
+
.map((value) => firstString(asRecord(value)?.full_name))
|
|
91
|
+
.filter((value) => Boolean(value))
|
|
92
|
+
.slice(0, 12),
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
actions: [
|
|
96
|
+
...GITHUB_SOURCE_ACTIONS.map((id) => action(id, id, "Read GitHub contribution evidence; params: owner, repo, pull_number (or full sha for get_commit), optional page/limit. Returns bounded patches and authors, never inferred responsibility.", async (ctx) => normalizeGithubSourceResult(id, ctx.params, await request(ctx, `https://api.github.com/${githubSourceEndpoint(id, ctx.params)}`, {
|
|
97
|
+
headers: githubHeaders(ctx.token),
|
|
98
|
+
})))),
|
|
99
|
+
action("list_repositories", "列出仓库", "列出当前 Token 可访问的 GitHub 仓库。", async (ctx) => {
|
|
100
|
+
const url = new URL("https://api.github.com/user/repos");
|
|
101
|
+
url.searchParams.set("per_page", String(intParam(ctx.params, "limit", 30, 100)));
|
|
102
|
+
url.searchParams.set("sort", "updated");
|
|
103
|
+
const data = await request(ctx, url, {
|
|
104
|
+
headers: githubHeaders(ctx.token),
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
repositories: compactList(data, [
|
|
108
|
+
"id",
|
|
109
|
+
"full_name",
|
|
110
|
+
"description",
|
|
111
|
+
"private",
|
|
112
|
+
"archived",
|
|
113
|
+
"default_branch",
|
|
114
|
+
"html_url",
|
|
115
|
+
"updated_at",
|
|
116
|
+
], 100),
|
|
117
|
+
};
|
|
118
|
+
}),
|
|
119
|
+
action("get_repository", "读取仓库", "读取固定 GitHub 仓库身份;params: owner, repo。", async (ctx) => {
|
|
120
|
+
const { owner, repo } = githubRepositoryParameters(ctx.params);
|
|
121
|
+
return pick(await request(ctx, `https://api.github.com/repos/${owner}/${repo}`, {
|
|
122
|
+
headers: githubHeaders(ctx.token),
|
|
123
|
+
}), ["id", "full_name", "private", "archived", "html_url"]);
|
|
124
|
+
}),
|
|
125
|
+
action("get_starred", "读取 Star 状态", "读取当前连接账号是否 Star 固定仓库;params: owner, repo。", async (ctx) => {
|
|
126
|
+
const { owner, repo } = githubRepositoryParameters(ctx.params);
|
|
127
|
+
const status = await linkRequestStatus({
|
|
128
|
+
url: new URL(`https://api.github.com/user/starred/${owner}/${repo}`),
|
|
129
|
+
headers: githubHeaders(ctx.token),
|
|
130
|
+
signal: ctx.signal,
|
|
131
|
+
fetchImpl: ctx.fetchImpl,
|
|
132
|
+
}, [204, 404]);
|
|
133
|
+
return { starred: status === 204 };
|
|
134
|
+
}),
|
|
135
|
+
action("set_starred", "设置 Star 状态", "设置固定仓库的 Star 状态并独立验证;params: owner, repo, starred(布尔)。", async (ctx) => {
|
|
136
|
+
const { owner, repo, starred } = githubSetStarredParameters(ctx.params);
|
|
137
|
+
await linkRequestStatus({
|
|
138
|
+
url: new URL(`https://api.github.com/user/starred/${owner}/${repo}`),
|
|
139
|
+
method: starred ? "PUT" : "DELETE",
|
|
140
|
+
headers: { ...githubHeaders(ctx.token), "Content-Length": "0" },
|
|
141
|
+
signal: ctx.signal,
|
|
142
|
+
fetchImpl: ctx.fetchImpl,
|
|
143
|
+
}, [204]);
|
|
144
|
+
return { acknowledged: true };
|
|
145
|
+
}, "write"),
|
|
146
|
+
action("get_readme", "读取 README", "读取 GitHub README;params: owner, repo。", async (ctx) => {
|
|
147
|
+
const owner = pathSegmentParam(ctx.params, "owner", { required: true, maxLength: 100 });
|
|
148
|
+
const repo = pathSegmentParam(ctx.params, "repo", { required: true, maxLength: 100 });
|
|
149
|
+
const data = asRecord(await request(ctx, `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/readme`, { headers: githubHeaders(ctx.token) })) ?? {};
|
|
150
|
+
const encoded = typeof data.content === "string" ? data.content.replace(/\s/g, "") : "";
|
|
151
|
+
const content = encoded
|
|
152
|
+
? Buffer.from(encoded, "base64").toString("utf8").slice(0, 262_144)
|
|
153
|
+
: "";
|
|
154
|
+
return {
|
|
155
|
+
owner,
|
|
156
|
+
repo,
|
|
157
|
+
path: data.path,
|
|
158
|
+
sha: data.sha,
|
|
159
|
+
content,
|
|
160
|
+
truncated: content.length >= 262_144,
|
|
161
|
+
html_url: data.html_url,
|
|
162
|
+
};
|
|
163
|
+
}),
|
|
164
|
+
action("list_issues", "列出 Issue", "列出 GitHub Issue(排除 Pull Request);params: owner, repo,可选 limit、page(1..1000)、state(open/closed/all,默认open)。", async (ctx) => {
|
|
165
|
+
const owner = pathSegmentParam(ctx.params, "owner", { required: true, maxLength: 100 });
|
|
166
|
+
const repo = pathSegmentParam(ctx.params, "repo", { required: true, maxLength: 100 });
|
|
167
|
+
const url = new URL(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`);
|
|
168
|
+
url.searchParams.set("per_page", String(intParam(ctx.params, "limit", 30, 100)));
|
|
169
|
+
url.searchParams.set("page", String(intParam(ctx.params, "page", 1, 1_000)));
|
|
170
|
+
const state = stringParam(ctx.params, "state") ?? "open";
|
|
171
|
+
if (!["open", "closed", "all"].includes(state))
|
|
172
|
+
throw new Error("Invalid issue state");
|
|
173
|
+
url.searchParams.set("state", state);
|
|
174
|
+
const data = asArray(await request(ctx, url, {
|
|
175
|
+
headers: githubHeaders(ctx.token),
|
|
176
|
+
}));
|
|
177
|
+
return {
|
|
178
|
+
issues: data
|
|
179
|
+
.filter((value) => !asRecord(value)?.pull_request)
|
|
180
|
+
.slice(0, 100)
|
|
181
|
+
.map((value) => pick(value, [
|
|
182
|
+
"number",
|
|
183
|
+
"title",
|
|
184
|
+
"state",
|
|
185
|
+
"html_url",
|
|
186
|
+
"created_at",
|
|
187
|
+
"updated_at",
|
|
188
|
+
"user",
|
|
189
|
+
"labels",
|
|
190
|
+
])),
|
|
191
|
+
};
|
|
192
|
+
}),
|
|
193
|
+
action("get_file", "读取指定文件", "读取 GitHub 仓库文件;params: owner, repo, path, 可选 ref。", async (ctx) => {
|
|
194
|
+
const owner = pathSegmentParam(ctx.params, "owner", { required: true, maxLength: 100 });
|
|
195
|
+
const repo = pathSegmentParam(ctx.params, "repo", { required: true, maxLength: 100 });
|
|
196
|
+
// Exact: LinkAction trims agent free text; saved references replay byte-exact.
|
|
197
|
+
const path = pathParam(ctx.params, "path", {
|
|
198
|
+
required: true,
|
|
199
|
+
maxLength: 2_000,
|
|
200
|
+
exact: true,
|
|
201
|
+
});
|
|
202
|
+
const ref = stringParam(ctx.params, "ref", { maxLength: 300 });
|
|
203
|
+
const url = new URL(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path}`);
|
|
204
|
+
if (ref)
|
|
205
|
+
url.searchParams.set("ref", ref);
|
|
206
|
+
const data = asRecord(await request(ctx, url, { headers: githubHeaders(ctx.token) })) ?? {};
|
|
207
|
+
if (data.type !== "file")
|
|
208
|
+
throw new Error("GitHub path is not a file");
|
|
209
|
+
const encoded = typeof data.content === "string" ? data.content.replace(/\s/g, "") : "";
|
|
210
|
+
const content = encoded
|
|
211
|
+
? Buffer.from(encoded, "base64").toString("utf8").slice(0, 262_144)
|
|
212
|
+
: "";
|
|
213
|
+
return {
|
|
214
|
+
owner,
|
|
215
|
+
repo,
|
|
216
|
+
path: data.path,
|
|
217
|
+
ref,
|
|
218
|
+
sha: data.sha,
|
|
219
|
+
size: data.size,
|
|
220
|
+
content,
|
|
221
|
+
truncated: typeof data.size === "number" ? data.size > Buffer.byteLength(content) : false,
|
|
222
|
+
html_url: data.html_url,
|
|
223
|
+
};
|
|
224
|
+
}),
|
|
225
|
+
action("list_pull_requests", "列出 Pull Request", "列出 GitHub Pull Request;params: owner, repo,可选 limit、page(1..1000)。", async (ctx) => {
|
|
226
|
+
const owner = pathSegmentParam(ctx.params, "owner", { required: true, maxLength: 100 });
|
|
227
|
+
const repo = pathSegmentParam(ctx.params, "repo", { required: true, maxLength: 100 });
|
|
228
|
+
const url = new URL(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`);
|
|
229
|
+
url.searchParams.set("per_page", String(intParam(ctx.params, "limit", 30, 100)));
|
|
230
|
+
url.searchParams.set("page", String(intParam(ctx.params, "page", 1, 1_000)));
|
|
231
|
+
const data = await request(ctx, url, {
|
|
232
|
+
headers: githubHeaders(ctx.token),
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
pull_requests: compactList(data, [
|
|
236
|
+
"number",
|
|
237
|
+
"title",
|
|
238
|
+
"state",
|
|
239
|
+
"draft",
|
|
240
|
+
"html_url",
|
|
241
|
+
"created_at",
|
|
242
|
+
"updated_at",
|
|
243
|
+
"user",
|
|
244
|
+
"head",
|
|
245
|
+
"base",
|
|
246
|
+
], 100),
|
|
247
|
+
};
|
|
248
|
+
}),
|
|
249
|
+
action("get_issue", "查看 Issue", "查看单个 GitHub Issue;params: owner, repo, issue_number。", async (ctx) => {
|
|
250
|
+
const owner = pathSegmentParam(ctx.params, "owner", { required: true, maxLength: 100 });
|
|
251
|
+
const repo = pathSegmentParam(ctx.params, "repo", { required: true, maxLength: 100 });
|
|
252
|
+
const number = intParam(ctx.params, "issue_number", 0, Number.MAX_SAFE_INTEGER);
|
|
253
|
+
return pick(await request(ctx, `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}`, { headers: githubHeaders(ctx.token) }), [
|
|
254
|
+
"id",
|
|
255
|
+
"url",
|
|
256
|
+
"repository_url",
|
|
257
|
+
"pull_request",
|
|
258
|
+
"number",
|
|
259
|
+
"title",
|
|
260
|
+
"body",
|
|
261
|
+
"state",
|
|
262
|
+
"state_reason",
|
|
263
|
+
"html_url",
|
|
264
|
+
"created_at",
|
|
265
|
+
"updated_at",
|
|
266
|
+
"closed_at",
|
|
267
|
+
"user",
|
|
268
|
+
"assignees",
|
|
269
|
+
"labels",
|
|
270
|
+
"milestone",
|
|
271
|
+
"comments",
|
|
272
|
+
]);
|
|
273
|
+
}),
|
|
274
|
+
action("get_pull_request", "查看 Pull Request", "查看单个 GitHub Pull Request;params: owner, repo, pull_number。", async (ctx) => {
|
|
275
|
+
const owner = pathSegmentParam(ctx.params, "owner", { required: true, maxLength: 100 });
|
|
276
|
+
const repo = pathSegmentParam(ctx.params, "repo", { required: true, maxLength: 100 });
|
|
277
|
+
const number = intParam(ctx.params, "pull_number", 0, Number.MAX_SAFE_INTEGER);
|
|
278
|
+
return pick(await request(ctx, `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}`, { headers: githubHeaders(ctx.token) }), [
|
|
279
|
+
"number",
|
|
280
|
+
"title",
|
|
281
|
+
"body",
|
|
282
|
+
"state",
|
|
283
|
+
"draft",
|
|
284
|
+
"merged",
|
|
285
|
+
"mergeable",
|
|
286
|
+
"html_url",
|
|
287
|
+
"created_at",
|
|
288
|
+
"updated_at",
|
|
289
|
+
"closed_at",
|
|
290
|
+
"merged_at",
|
|
291
|
+
"user",
|
|
292
|
+
"assignees",
|
|
293
|
+
"requested_reviewers",
|
|
294
|
+
"labels",
|
|
295
|
+
"head",
|
|
296
|
+
"base",
|
|
297
|
+
"commits",
|
|
298
|
+
"additions",
|
|
299
|
+
"deletions",
|
|
300
|
+
"changed_files",
|
|
301
|
+
]);
|
|
302
|
+
}),
|
|
303
|
+
action("update_issue", "关闭或重开 Issue", "设置已有 Issue 的状态并独立验证;params: owner, repo, issue_number(整数), state(open 或 closed)。不支持 PR 或其它字段。", async (ctx) => {
|
|
304
|
+
const { owner, repo, issue_number, state } = githubIssueStateParameters(ctx.params);
|
|
305
|
+
// The response is only an acknowledgement; the owning controller reads independently.
|
|
306
|
+
await request(ctx, `https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`, {
|
|
307
|
+
method: "PATCH",
|
|
308
|
+
headers: githubHeaders(ctx.token),
|
|
309
|
+
body: { state },
|
|
310
|
+
});
|
|
311
|
+
return { acknowledged: true };
|
|
312
|
+
}, "write"),
|
|
313
|
+
action("create_issue", "创建 Issue", "创建 GitHub Issue;params: owner, repo, title, 可选 body;执行前始终审批。", async (ctx) => {
|
|
314
|
+
const owner = pathSegmentParam(ctx.params, "owner", { required: true, maxLength: 100 });
|
|
315
|
+
const repo = pathSegmentParam(ctx.params, "repo", { required: true, maxLength: 100 });
|
|
316
|
+
const title = stringParam(ctx.params, "title", { required: true, maxLength: 256 });
|
|
317
|
+
const body = stringParam(ctx.params, "body", { maxLength: 20_000 });
|
|
318
|
+
const data = await request(ctx, `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: githubHeaders(ctx.token),
|
|
321
|
+
body: { title, ...(body ? { body } : {}) },
|
|
322
|
+
});
|
|
323
|
+
return pick(data, ["number", "title", "state", "html_url", "created_at"]);
|
|
324
|
+
}, "write"),
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
const gitlab = {
|
|
328
|
+
id: "gitlab",
|
|
329
|
+
displayName: "GitLab",
|
|
330
|
+
tokenLabel: "Personal access token",
|
|
331
|
+
tokenPlaceholder: "glpat-…",
|
|
332
|
+
async validate(context) {
|
|
333
|
+
const data = await request(context, "https://gitlab.com/api/v4/user", {
|
|
334
|
+
headers: gitlabHeaders(context),
|
|
335
|
+
});
|
|
336
|
+
return identity(data, ["id"], ["username", "name"], ["name", "email"]);
|
|
337
|
+
},
|
|
338
|
+
actions: [
|
|
339
|
+
action("list_projects", "列出项目", "列出当前 Token 可访问的 GitLab 项目。", async (ctx) => {
|
|
340
|
+
const url = new URL("https://gitlab.com/api/v4/projects");
|
|
341
|
+
url.searchParams.set("membership", "true");
|
|
342
|
+
url.searchParams.set("simple", "true");
|
|
343
|
+
url.searchParams.set("per_page", String(intParam(ctx.params, "limit", 30, 100)));
|
|
344
|
+
const data = await request(ctx, url, { headers: gitlabHeaders(ctx) });
|
|
345
|
+
return {
|
|
346
|
+
projects: compactList(data, [
|
|
347
|
+
"id",
|
|
348
|
+
"name",
|
|
349
|
+
"path_with_namespace",
|
|
350
|
+
"description",
|
|
351
|
+
"visibility",
|
|
352
|
+
"default_branch",
|
|
353
|
+
"web_url",
|
|
354
|
+
"last_activity_at",
|
|
355
|
+
], 100),
|
|
356
|
+
};
|
|
357
|
+
}),
|
|
358
|
+
action("list_issues", "列出 Issue", "列出当前用户可见的 GitLab Issue。", async (ctx) => {
|
|
359
|
+
const url = new URL("https://gitlab.com/api/v4/issues");
|
|
360
|
+
url.searchParams.set("scope", "assigned_to_me");
|
|
361
|
+
url.searchParams.set("per_page", String(intParam(ctx.params, "limit", 30, 100)));
|
|
362
|
+
const data = await request(ctx, url, { headers: gitlabHeaders(ctx) });
|
|
363
|
+
return {
|
|
364
|
+
issues: compactList(data, [
|
|
365
|
+
"id",
|
|
366
|
+
"iid",
|
|
367
|
+
"project_id",
|
|
368
|
+
"title",
|
|
369
|
+
"state",
|
|
370
|
+
"web_url",
|
|
371
|
+
"created_at",
|
|
372
|
+
"updated_at",
|
|
373
|
+
"labels",
|
|
374
|
+
], 100),
|
|
375
|
+
};
|
|
376
|
+
}),
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
const figma = {
|
|
380
|
+
id: "figma",
|
|
381
|
+
credentialActionIds: [
|
|
382
|
+
"get_file",
|
|
383
|
+
"read_design_content",
|
|
384
|
+
"get_nodes",
|
|
385
|
+
"get_images",
|
|
386
|
+
"get_image_fills",
|
|
387
|
+
"get_comments",
|
|
388
|
+
],
|
|
389
|
+
displayName: "Figma",
|
|
390
|
+
tokenLabel: "Personal access token",
|
|
391
|
+
tokenPlaceholder: "figd_…",
|
|
392
|
+
async validate(context) {
|
|
393
|
+
const data = await request(context, "https://api.figma.com/v1/me", {
|
|
394
|
+
headers: figmaHeaders(context),
|
|
395
|
+
});
|
|
396
|
+
return identity(data, ["id"], ["handle", "email"], ["email"]);
|
|
397
|
+
},
|
|
398
|
+
actions: [
|
|
399
|
+
action("get_file", "读取页面摘要(旧版)", "只接受 file_url_or_key 或 file_key,返回最多100个页面的名称、ID和类型;不返回 document、节点内容或设计全文。需要实际设计内容时使用 read_design_content;旧服务未提供该动作时需升级服务,不能把 pages 摘要视为设计内容。", (ctx) => executeFigmaAction("get_file", ctx)),
|
|
400
|
+
action("read_design_content", "读取设计内容", "用 file_url_or_key 读取实际设计 document,默认 depth=2;有需要再用 get_nodes 按 ids 分批读取指定子树,只有明确需要完整文件时设置 full:true(不能同时指定depth)。可选 depth (1..100)、ids/node_ids (逗号字符串或数组,最多50)、version、geometry=paths、plugin_data、branch_data;节点URL的node-id会限制ids。设计字段完整保留在请求范围内,requestedDepth标明深度限制。复刻时用 get_images 渲染图和 get_image_fills 素材。远端token不由UseCredential导出,继续用LinkAction;缺token值不代表没权限。没有document的响应是读取失败,不可把pages当成设计全文。", (ctx) => executeFigmaAction("read_design_content", ctx)),
|
|
401
|
+
action("get_nodes", "读取节点内容", "用 file_url_or_key 与 ids/node_ids (最多50,或节点 URL 的 node-id) 读取完整节点子树,包括文字、布局、样式。可选 depth (1..100)、version、geometry=paths、plugin_data;depth 相对所选节点。", (ctx) => executeFigmaAction("get_nodes", ctx)),
|
|
402
|
+
action("get_images", "渲染节点图片", "用 file_url_or_key 与 ids/node_ids (最多50,或节点 URL 的 node-id) 获取渲染图片 URL。可选 scale (0.01..4)、format (png/jpg/svg/pdf)、version、svg_outline_text、svg_include_id、svg_include_node_id、svg_simplify_stroke、contents_only、use_absolute_bounds (布尔)。图片 URL 有效期有限,null 表示对应节点未能渲染。", (ctx) => executeFigmaAction("get_images", ctx)),
|
|
403
|
+
action("get_image_fills", "读取填充图片", "用 file_url_or_key 获取当前文件 imageRef 到图片 URL 的映射,用于还原 IMAGE 填充;URL 有效期有限。", (ctx) => executeFigmaAction("get_image_fills", ctx)),
|
|
404
|
+
action("get_comments", "读取评论", "用 file_url_or_key 读取 Figma 文件评论。", (ctx) => executeFigmaAction("get_comments", ctx)),
|
|
405
|
+
...FIGMA_MCP_ACTION_IDS.map((id) => action(id, {
|
|
406
|
+
get_metadata: "读取 MCP 节点目录",
|
|
407
|
+
get_design_context: "读取设计上下文或 Make 文件清单",
|
|
408
|
+
get_screenshot: "读取节点截图",
|
|
409
|
+
get_variable_defs: "读取选中节点变量",
|
|
410
|
+
get_motion_context: "读取动画时间线",
|
|
411
|
+
get_figjam: "读取 FigJam 上下文",
|
|
412
|
+
get_code_connect_map: "读取组件代码映射",
|
|
413
|
+
get_libraries: "读取设计库",
|
|
414
|
+
search_design_system: "搜索设计系统",
|
|
415
|
+
list_resources: "列出 Make 源码资源",
|
|
416
|
+
read_resource: "读取所选 Make 源文件",
|
|
417
|
+
}[id], "Official Figma MCP, only when API reads cannot supply the needed context. " +
|
|
418
|
+
"Requires the separate Figma MCP connection; API/PAT tokens cannot execute this action. " +
|
|
419
|
+
"params: file_url_or_key (preferred, Design/FigJam/Make URL) or file_key, optional file_type; " +
|
|
420
|
+
"node_id or URL node-id selects detailed Design content; do not read whole files automatically. " +
|
|
421
|
+
(id === "get_design_context"
|
|
422
|
+
? "For Make, this first discovers the available source resources; read selected files with read_resource afterwards. "
|
|
423
|
+
: "") +
|
|
424
|
+
(id === "read_resource"
|
|
425
|
+
? "Pass uri exactly as returned by get_design_context/list_resources on this connection and Make file. "
|
|
426
|
+
: "") +
|
|
427
|
+
(id === "list_resources"
|
|
428
|
+
? "Optional cursor requests one resource page. If resource listing is unsupported, use get_design_context to obtain Make resource links. "
|
|
429
|
+
: "") +
|
|
430
|
+
(id === "search_design_system" ? "queries: 1 to 20 search strings. " : "") +
|
|
431
|
+
"Optional client_languages/client_frameworks describe the target implementation; recursive is for motion context. " +
|
|
432
|
+
"Raw node JSON, image exports, and comments should use the API get_nodes/get_images/get_comments actions.", async () => {
|
|
433
|
+
throw new Error("Figma MCP actions require the Host-owned Figma Link executor.");
|
|
434
|
+
})),
|
|
435
|
+
],
|
|
436
|
+
};
|
|
437
|
+
async function executeFigmaAction(actionId, context) {
|
|
438
|
+
const params = figmaActionParameters(actionId, context.params);
|
|
439
|
+
const data = await linkRequestJson({
|
|
440
|
+
url: figmaActionEndpoint(actionId, params),
|
|
441
|
+
headers: figmaHeaders(context),
|
|
442
|
+
signal: context.signal,
|
|
443
|
+
fetchImpl: context.fetchImpl,
|
|
444
|
+
maxResponseBytes: FIGMA_RESPONSE_BYTES,
|
|
445
|
+
responseTooLargeMessage: FIGMA_RESPONSE_TOO_LARGE,
|
|
446
|
+
});
|
|
447
|
+
return normalizeFigmaActionResult(actionId, data, params);
|
|
448
|
+
}
|
|
449
|
+
const notionHeaders = (token) => ({
|
|
450
|
+
...bearer(token),
|
|
451
|
+
"Notion-Version": NOTION_API_VERSION,
|
|
452
|
+
});
|
|
453
|
+
const notion = {
|
|
454
|
+
id: "notion",
|
|
455
|
+
displayName: "Notion",
|
|
456
|
+
tokenLabel: "Internal integration token",
|
|
457
|
+
tokenPlaceholder: "ntn_… / secret_…",
|
|
458
|
+
async validate(context) {
|
|
459
|
+
const data = await request(context, "https://api.notion.com/v1/users/me", {
|
|
460
|
+
headers: notionHeaders(context.token),
|
|
461
|
+
});
|
|
462
|
+
const record = asRecord(data) ?? {};
|
|
463
|
+
const bot = asRecord(record.bot);
|
|
464
|
+
return {
|
|
465
|
+
externalAccountId: String(record.id ?? "bot"),
|
|
466
|
+
label: firstString(record.name, bot?.owner && "Notion integration") ?? "Notion integration",
|
|
467
|
+
detail: firstString(record.type),
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
actions: [
|
|
471
|
+
action("search", "搜索页面和数据库", "搜索已授权给该 Integration 的 Notion 页面和数据库。", async (ctx) => {
|
|
472
|
+
const query = stringParam(ctx.params, "query", { maxLength: 200 });
|
|
473
|
+
const data = asRecord(await request(ctx, "https://api.notion.com/v1/search", {
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers: notionHeaders(ctx.token),
|
|
476
|
+
body: {
|
|
477
|
+
page_size: intParam(ctx.params, "limit", 30, 100),
|
|
478
|
+
...(query ? { query } : {}),
|
|
479
|
+
},
|
|
480
|
+
})) ?? {};
|
|
481
|
+
return {
|
|
482
|
+
results: asArray(data.results)
|
|
483
|
+
.slice(0, 100)
|
|
484
|
+
.map((value) => pick(value, [
|
|
485
|
+
"object",
|
|
486
|
+
"id",
|
|
487
|
+
"url",
|
|
488
|
+
"created_time",
|
|
489
|
+
"last_edited_time",
|
|
490
|
+
"archived",
|
|
491
|
+
"parent",
|
|
492
|
+
"properties",
|
|
493
|
+
"title",
|
|
494
|
+
])),
|
|
495
|
+
has_more: data.has_more,
|
|
496
|
+
next_cursor: data.next_cursor,
|
|
497
|
+
};
|
|
498
|
+
}),
|
|
499
|
+
action("get_page", "读取页面属性", "读取 Notion 页面属性;params: page_id。", async (ctx) => {
|
|
500
|
+
const pageId = pathSegmentParam(ctx.params, "page_id", { required: true, maxLength: 100 });
|
|
501
|
+
return pick(await request(ctx, `https://api.notion.com/v1/pages/${encodeURIComponent(pageId)}`, {
|
|
502
|
+
headers: notionHeaders(ctx.token),
|
|
503
|
+
}), [
|
|
504
|
+
"object",
|
|
505
|
+
"id",
|
|
506
|
+
"url",
|
|
507
|
+
"created_time",
|
|
508
|
+
"last_edited_time",
|
|
509
|
+
"archived",
|
|
510
|
+
"parent",
|
|
511
|
+
"properties",
|
|
512
|
+
]);
|
|
513
|
+
}),
|
|
514
|
+
],
|
|
515
|
+
};
|
|
516
|
+
async function linearGraphql(context, query, variables = {}) {
|
|
517
|
+
const response = asRecord(await request(context, "https://api.linear.app/graphql", {
|
|
518
|
+
method: "POST",
|
|
519
|
+
headers: context.authKind === "oauth" ? bearer(context.token) : { Authorization: context.token },
|
|
520
|
+
body: { query, variables },
|
|
521
|
+
})) ?? {};
|
|
522
|
+
if (asArray(response.errors).length)
|
|
523
|
+
throw new Error(`Linear GraphQL request failed: ${JSON.stringify(response.errors).slice(0, 300)}`);
|
|
524
|
+
return asRecord(response.data) ?? {};
|
|
525
|
+
}
|
|
526
|
+
const linear = {
|
|
527
|
+
id: "linear",
|
|
528
|
+
displayName: "Linear",
|
|
529
|
+
tokenLabel: "Personal API key",
|
|
530
|
+
tokenPlaceholder: "lin_api_…",
|
|
531
|
+
async validate(context) {
|
|
532
|
+
const data = await linearGraphql(context, "query { viewer { id name displayName email } }");
|
|
533
|
+
const viewer = asRecord(data.viewer) ?? {};
|
|
534
|
+
return identity(viewer, ["id"], ["displayName", "name", "email"], ["email"]);
|
|
535
|
+
},
|
|
536
|
+
actions: [
|
|
537
|
+
action("list_issues", "列出 Issue", "列出分配给当前 Linear 用户的 Issue。", async (ctx) => {
|
|
538
|
+
const first = intParam(ctx.params, "limit", 30, 50);
|
|
539
|
+
const data = await linearGraphql(ctx, "query Issues($first: Int!) { viewer { assignedIssues(first: $first) { nodes { id identifier title description priority url createdAt updatedAt state { name type } team { id key name } } } } }", { first });
|
|
540
|
+
const viewer = asRecord(data.viewer);
|
|
541
|
+
const assigned = asRecord(viewer?.assignedIssues);
|
|
542
|
+
return { issues: asArray(assigned?.nodes).slice(0, 50) };
|
|
543
|
+
}),
|
|
544
|
+
action("list_teams", "列出团队", "列出当前 Linear 工作区中的团队。", async (ctx) => {
|
|
545
|
+
const data = await linearGraphql(ctx, "query { teams(first: 50) { nodes { id key name description } } }");
|
|
546
|
+
return { teams: asArray(asRecord(data.teams)?.nodes).slice(0, 50) };
|
|
547
|
+
}),
|
|
548
|
+
],
|
|
549
|
+
};
|
|
550
|
+
async function slackRequest(context, endpoint, body) {
|
|
551
|
+
const data = asRecord(await request(context, `https://slack.com/api/${endpoint}`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
body: body ?? {},
|
|
554
|
+
})) ?? {};
|
|
555
|
+
if (data.ok !== true)
|
|
556
|
+
throw new Error(`Slack API request failed: ${firstString(data.error) ?? "unknown_error"}`);
|
|
557
|
+
return data;
|
|
558
|
+
}
|
|
559
|
+
const slack = {
|
|
560
|
+
id: "slack",
|
|
561
|
+
displayName: "Slack",
|
|
562
|
+
tokenLabel: "Bot or user token",
|
|
563
|
+
tokenPlaceholder: "xoxb-… / xoxp-…",
|
|
564
|
+
async validate(context) {
|
|
565
|
+
const data = await slackRequest(context, "auth.test");
|
|
566
|
+
return {
|
|
567
|
+
externalAccountId: firstString(data.user_id, data.bot_id, data.team_id) ?? "slack",
|
|
568
|
+
label: firstString(data.user, data.team) ?? "Slack",
|
|
569
|
+
detail: firstString(data.team, data.url),
|
|
570
|
+
};
|
|
571
|
+
},
|
|
572
|
+
actions: [
|
|
573
|
+
...["get_message", "get_thread"].map((id) => action(id, id, id === "get_message"
|
|
574
|
+
? "Read exact Slack message; params: permalink (a reply URL must include thread_ts)."
|
|
575
|
+
: "Read one Slack thread page; params: channel, ts (parent), optional permalink/cursor/limit (max 15). Channel threads may require a user token.", async (ctx) => {
|
|
576
|
+
const p = slackSourceParams(id, ctx.params);
|
|
577
|
+
const isReply = id === "get_message" && p.thread_ts && p.thread_ts !== p.ts;
|
|
578
|
+
const data = await slackRequest(ctx, id === "get_thread" || isReply ? "conversations.replies" : "conversations.history", {
|
|
579
|
+
channel: p.channel,
|
|
580
|
+
limit: String(p.limit),
|
|
581
|
+
...(id === "get_thread"
|
|
582
|
+
? { ts: p.ts, ...(p.cursor ? { cursor: p.cursor } : {}) }
|
|
583
|
+
: {
|
|
584
|
+
oldest: p.ts,
|
|
585
|
+
latest: p.ts,
|
|
586
|
+
inclusive: "true",
|
|
587
|
+
...(isReply ? { ts: p.thread_ts } : {}),
|
|
588
|
+
}),
|
|
589
|
+
});
|
|
590
|
+
// Resolve through the selected account; a supplied URL never controls the API origin.
|
|
591
|
+
const link = await slackRequest(ctx, "chat.getPermalink", {
|
|
592
|
+
channel: p.channel,
|
|
593
|
+
message_ts: p.ts,
|
|
594
|
+
});
|
|
595
|
+
return normalizeSlackSourceResult(id, ctx.params, {
|
|
596
|
+
...data,
|
|
597
|
+
permalink: link.permalink,
|
|
598
|
+
});
|
|
599
|
+
})),
|
|
600
|
+
action("list_channels", "列出频道", "列出 Token 可访问的 Slack 频道。", async (ctx) => {
|
|
601
|
+
const data = await slackRequest(ctx, "conversations.list", {
|
|
602
|
+
limit: String(intParam(ctx.params, "limit", 50, 200)),
|
|
603
|
+
exclude_archived: "true",
|
|
604
|
+
});
|
|
605
|
+
return {
|
|
606
|
+
channels: compactList(data.channels, ["id", "name", "is_private", "is_member", "topic", "purpose", "num_members"], 200),
|
|
607
|
+
next_cursor: asRecord(data.response_metadata)?.next_cursor,
|
|
608
|
+
};
|
|
609
|
+
}),
|
|
610
|
+
action("get_channel_history", "读取频道消息", "读取 Slack 频道最近消息;params: channel, 可选 limit(最多 15)。", async (ctx) => {
|
|
611
|
+
const channel = stringParam(ctx.params, "channel", { required: true, maxLength: 100 });
|
|
612
|
+
const data = await slackRequest(ctx, "conversations.history", {
|
|
613
|
+
channel,
|
|
614
|
+
limit: String(intParam(ctx.params, "limit", 15, 15)),
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
messages: compactList(data.messages, ["type", "user", "text", "ts", "thread_ts", "reply_count", "files"], 100),
|
|
618
|
+
has_more: data.has_more,
|
|
619
|
+
};
|
|
620
|
+
}),
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
const sentry = {
|
|
624
|
+
id: "sentry",
|
|
625
|
+
displayName: "Sentry",
|
|
626
|
+
tokenLabel: "User auth token",
|
|
627
|
+
tokenPlaceholder: "sntrys_…",
|
|
628
|
+
async validate(context) {
|
|
629
|
+
const data = await request(context, "https://sentry.io/api/0/organizations/");
|
|
630
|
+
const organizations = asArray(data);
|
|
631
|
+
const first = asRecord(organizations[0]);
|
|
632
|
+
return {
|
|
633
|
+
externalAccountId: firstString(first?.id, first?.slug) ?? "sentry-token",
|
|
634
|
+
label: organizations.length > 0
|
|
635
|
+
? `${firstString(first?.name, first?.slug) ?? "Sentry"} · ${organizations.length} org`
|
|
636
|
+
: "Sentry token",
|
|
637
|
+
};
|
|
638
|
+
},
|
|
639
|
+
actions: [
|
|
640
|
+
action("list_organizations", "列出组织", "列出当前 Sentry Token 可访问的组织。", async (ctx) => ({
|
|
641
|
+
organizations: compactList(await request(ctx, "https://sentry.io/api/0/organizations/"), ["id", "slug", "name", "status", "dateCreated", "isEarlyAdopter"], 100),
|
|
642
|
+
})),
|
|
643
|
+
action("list_projects", "列出项目", "列出 Sentry 组织项目;params: organization。", async (ctx) => {
|
|
644
|
+
const org = pathSegmentParam(ctx.params, "organization", {
|
|
645
|
+
required: true,
|
|
646
|
+
maxLength: 100,
|
|
647
|
+
});
|
|
648
|
+
return {
|
|
649
|
+
projects: compactList(await request(ctx, `https://sentry.io/api/0/organizations/${encodeURIComponent(org)}/projects/`), ["id", "slug", "name", "platform", "dateCreated", "isBookmarked"], 100),
|
|
650
|
+
};
|
|
651
|
+
}),
|
|
652
|
+
],
|
|
653
|
+
};
|
|
654
|
+
const airtable = {
|
|
655
|
+
id: "airtable",
|
|
656
|
+
displayName: "Airtable",
|
|
657
|
+
tokenLabel: "Personal access token",
|
|
658
|
+
tokenPlaceholder: "pat…",
|
|
659
|
+
async validate(context) {
|
|
660
|
+
const root = asRecord(await request(context, "https://api.airtable.com/v0/meta/bases")) ?? {};
|
|
661
|
+
const bases = asArray(root.bases);
|
|
662
|
+
const first = asRecord(bases[0]);
|
|
663
|
+
return {
|
|
664
|
+
externalAccountId: firstString(first?.id) ?? "airtable-token",
|
|
665
|
+
label: bases.length > 0
|
|
666
|
+
? `Airtable · ${bases.length} base${bases.length === 1 ? "" : "s"}`
|
|
667
|
+
: "Airtable token",
|
|
668
|
+
};
|
|
669
|
+
},
|
|
670
|
+
actions: [
|
|
671
|
+
action("list_bases", "列出 Base", "列出当前 Airtable PAT 可访问的 Base。", async (ctx) => {
|
|
672
|
+
const data = asRecord(await request(ctx, "https://api.airtable.com/v0/meta/bases")) ?? {};
|
|
673
|
+
return {
|
|
674
|
+
bases: compactList(data.bases, ["id", "name", "permissionLevel"], 100),
|
|
675
|
+
offset: data.offset,
|
|
676
|
+
};
|
|
677
|
+
}),
|
|
678
|
+
action("list_tables", "列出数据表", "列出 Airtable 表与字段;params: base_id。", async (ctx) => {
|
|
679
|
+
const baseId = pathSegmentParam(ctx.params, "base_id", {
|
|
680
|
+
required: true,
|
|
681
|
+
maxLength: 100,
|
|
682
|
+
});
|
|
683
|
+
const data = asRecord(await request(ctx, `https://api.airtable.com/v0/meta/bases/${encodeURIComponent(baseId)}/tables`)) ?? {};
|
|
684
|
+
return {
|
|
685
|
+
tables: asArray(data.tables)
|
|
686
|
+
.slice(0, 100)
|
|
687
|
+
.map((value) => pick(value, ["id", "name", "primaryFieldId", "fields", "views"])),
|
|
688
|
+
};
|
|
689
|
+
}),
|
|
690
|
+
],
|
|
691
|
+
};
|
|
692
|
+
const todoist = {
|
|
693
|
+
id: "todoist",
|
|
694
|
+
displayName: "Todoist",
|
|
695
|
+
tokenLabel: "Developer API token",
|
|
696
|
+
tokenPlaceholder: "Todoist API token",
|
|
697
|
+
async validate(context) {
|
|
698
|
+
const data = await request(context, "https://api.todoist.com/api/v1/user");
|
|
699
|
+
return identity(data, ["id"], ["full_name", "email"], ["email"]);
|
|
700
|
+
},
|
|
701
|
+
actions: [
|
|
702
|
+
action("list_projects", "列出项目", "列出当前 Todoist Token 可访问的项目。", async (ctx) => {
|
|
703
|
+
const root = asRecord(await request(ctx, "https://api.todoist.com/api/v1/projects"));
|
|
704
|
+
const data = root?.results ?? root ?? [];
|
|
705
|
+
return {
|
|
706
|
+
projects: compactList(data, ["id", "name", "color", "is_shared", "is_favorite", "is_archived", "url"], 100),
|
|
707
|
+
};
|
|
708
|
+
}),
|
|
709
|
+
action("list_tasks", "列出任务", "列出当前 Todoist 用户的未完成任务。", async (ctx) => {
|
|
710
|
+
const url = new URL("https://api.todoist.com/api/v1/tasks");
|
|
711
|
+
url.searchParams.set("limit", String(intParam(ctx.params, "limit", 50, 100)));
|
|
712
|
+
const root = asRecord(await request(ctx, url));
|
|
713
|
+
const data = root?.results ?? root ?? [];
|
|
714
|
+
return {
|
|
715
|
+
tasks: compactList(data, ["id", "content", "description", "project_id", "priority", "due", "labels", "url"], 100),
|
|
716
|
+
next_cursor: root?.next_cursor,
|
|
717
|
+
};
|
|
718
|
+
}),
|
|
719
|
+
],
|
|
720
|
+
};
|
|
721
|
+
const vercel = {
|
|
722
|
+
id: "vercel",
|
|
723
|
+
displayName: "Vercel",
|
|
724
|
+
tokenLabel: "Access token",
|
|
725
|
+
tokenPlaceholder: "Vercel access token",
|
|
726
|
+
async validate(context) {
|
|
727
|
+
const root = asRecord(await request(context, "https://api.vercel.com/v2/user")) ?? {};
|
|
728
|
+
return identity(root.user, ["id", "uid"], ["name", "username", "email"], ["email"]);
|
|
729
|
+
},
|
|
730
|
+
actions: [
|
|
731
|
+
action("list_projects", "列出项目", "列出当前 Vercel Token 可访问的项目。", async (ctx) => {
|
|
732
|
+
const url = new URL("https://api.vercel.com/v10/projects");
|
|
733
|
+
url.searchParams.set("limit", String(intParam(ctx.params, "limit", 50, 100)));
|
|
734
|
+
const teamId = stringParam(ctx.params, "team_id", { maxLength: 100 });
|
|
735
|
+
if (teamId)
|
|
736
|
+
url.searchParams.set("teamId", teamId);
|
|
737
|
+
const root = asRecord(await request(ctx, url)) ?? {};
|
|
738
|
+
return {
|
|
739
|
+
projects: compactList(root.projects, ["id", "name", "framework", "updatedAt", "createdAt", "latestDeployments"], 100),
|
|
740
|
+
pagination: root.pagination,
|
|
741
|
+
};
|
|
742
|
+
}),
|
|
743
|
+
action("list_deployments", "列出部署", "列出当前 Vercel Token 可访问的部署。", async (ctx) => {
|
|
744
|
+
const url = new URL("https://api.vercel.com/v7/deployments");
|
|
745
|
+
url.searchParams.set("limit", String(intParam(ctx.params, "limit", 50, 100)));
|
|
746
|
+
const teamId = stringParam(ctx.params, "team_id", { maxLength: 100 });
|
|
747
|
+
if (teamId)
|
|
748
|
+
url.searchParams.set("teamId", teamId);
|
|
749
|
+
const root = asRecord(await request(ctx, url)) ?? {};
|
|
750
|
+
return {
|
|
751
|
+
deployments: compactList(root.deployments, ["uid", "projectId", "name", "url", "state", "target", "created", "ready", "meta"], 100),
|
|
752
|
+
pagination: root.pagination,
|
|
753
|
+
};
|
|
754
|
+
}),
|
|
755
|
+
],
|
|
756
|
+
};
|
|
757
|
+
const DEFAULT_LINK_PROVIDERS = [
|
|
758
|
+
github,
|
|
759
|
+
gitlab,
|
|
760
|
+
figma,
|
|
761
|
+
notion,
|
|
762
|
+
linear,
|
|
763
|
+
slack,
|
|
764
|
+
sentry,
|
|
765
|
+
airtable,
|
|
766
|
+
todoist,
|
|
767
|
+
vercel,
|
|
768
|
+
];
|
|
769
|
+
return DEFAULT_LINK_PROVIDERS.map((provider) => ({
|
|
770
|
+
...provider,
|
|
771
|
+
actions: provider.actions.map((action) => ({
|
|
772
|
+
...action,
|
|
773
|
+
execute: (context) => executeLinkOperation({
|
|
774
|
+
providerId: provider.id,
|
|
775
|
+
action: action.id,
|
|
776
|
+
params: context.params,
|
|
777
|
+
signal: context.signal,
|
|
778
|
+
execute: (params) => action.execute({ ...context, params }),
|
|
779
|
+
}),
|
|
780
|
+
})),
|
|
781
|
+
}));
|
|
782
|
+
}
|