@hubex/mcp 0.9.20261015 → 0.10.20261016
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/.env.example +6 -0
- package/CONNECTING.md +9 -1
- package/README.md +5 -3
- package/dist/auth.js +99 -6
- package/dist/config.js +1 -0
- package/dist/server.js +3 -0
- package/dist/tenant-prompt.js +51 -0
- package/dist/tools/curated.js +55 -6
- package/docs/guides/taskcreate.md +5 -2
- package/generated/index.dev.json +614 -89
- package/package.json +1 -1
- package/swagger/dev/WORK.json +18309 -17872
- package/swagger/dev/WSP.json +563 -180
package/.env.example
CHANGED
|
@@ -10,6 +10,7 @@ HUBEX_ENV=dev
|
|
|
10
10
|
HUBEX_APPLICATION_ID=5
|
|
11
11
|
|
|
12
12
|
# Optional: pin the tenant explicitly. If omitted, it is read from the JWT (TenantID claim).
|
|
13
|
+
# In "login" mode it picks the tenant to authorize in.
|
|
13
14
|
# HUBEX_TENANT_ID=5
|
|
14
15
|
|
|
15
16
|
# Auth mode: token | login | service
|
|
@@ -23,6 +24,11 @@ HUBEX_AUTH_MODE=token
|
|
|
23
24
|
# HUBEX_AUTH_MODE=login
|
|
24
25
|
# HUBEX_USERNAME=poweruser@hubex.ru
|
|
25
26
|
# HUBEX_PASSWORD=your-password
|
|
27
|
+
# Если учётная запись может войти от имени нескольких пользователей (в разных
|
|
28
|
+
# тенантах или в одном), выбери нужного через HUBEX_TENANT_ID и HUBEX_USER_ID
|
|
29
|
+
# (userID пользователя в тенанте). Если они не сужают выбор до одного, сервер
|
|
30
|
+
# спросит при первом обращении (список в клиенте, либо инструмент hubex_select_tenant).
|
|
31
|
+
# HUBEX_USER_ID=868
|
|
26
32
|
|
|
27
33
|
# --- mode "service" ---
|
|
28
34
|
# Долгоживущий интеграционный токен. HUBEX_TENANT_ID в этом режиме не нужен:
|
package/CONNECTING.md
CHANGED
|
@@ -79,7 +79,8 @@ npm run build
|
|
|
79
79
|
| `HUBEX_AUTH_MODE` | да | `token`, `login` или `service` |
|
|
80
80
|
| `HUBEX_USERNAME` / `HUBEX_PASSWORD` | для `login` | логин/пароль (токен обновляется сам) |
|
|
81
81
|
| `HUBEX_SERVICE_TOKEN` | для `service` | долгоживущий интеграционный токен (обновляется сам) |
|
|
82
|
-
| `HUBEX_TENANT_ID` | нет | tenant явно; иначе берётся из JWT |
|
|
82
|
+
| `HUBEX_TENANT_ID` | нет | tenant явно; иначе берётся из JWT. В `login` — в каком тенанте авторизоваться |
|
|
83
|
+
| `HUBEX_USER_ID` | нет | в `login` — от имени какого пользователя (userID) войти, если в тенанте их несколько |
|
|
83
84
|
| `HUBEX_METHODS` | нет | список доступных HTTP-методов через запятую (по умолчанию — все шесть) |
|
|
84
85
|
| `HUBEX_READONLY` | нет | `true` — эквивалент `HUBEX_METHODS=GET,HEAD` |
|
|
85
86
|
| `HUBEX_SERVICES` | нет | ограничить поиск и исполнение списком сервисов (по умолчанию — все 22) |
|
|
@@ -95,6 +96,13 @@ npm run build
|
|
|
95
96
|
> Если HubEx отверг токен (ошибка 401), следующий запрос получит новый — по
|
|
96
97
|
> refresh, а если не выйдет, сервер снова попросит токен.
|
|
97
98
|
|
|
99
|
+
> В режиме `login` учётная запись может входить от имени нескольких
|
|
100
|
+
> пользователей — в разных тенантах или в одном. Если `HUBEX_TENANT_ID` и
|
|
101
|
+
> `HUBEX_USER_ID` не сужают выбор до одного, при первом обращении сервер
|
|
102
|
+
> покажет список (клиенты с elicitation) или попросит вызвать инструмент
|
|
103
|
+
> `hubex_select_tenant` с выбранными `tenantID` и `userID`. Выбор действует до
|
|
104
|
+
> перезапуска сервера.
|
|
105
|
+
|
|
98
106
|
> Сервисный токен выпускается один раз администратором:
|
|
99
107
|
> `POST /fsm/AUTHZ/ServiceTokens` с Bearer и полномочием `ServiceTokenAdd`, тело — массив id
|
|
100
108
|
> членов тенанта. Отзыв — `DELETE /fsm/AUTHZ/ServiceTokens` с полномочием `ServiceTokenRemove`.
|
package/README.md
CHANGED
|
@@ -151,7 +151,8 @@ claude mcp add hubex --scope user \
|
|
|
151
151
|
|---|---|---|
|
|
152
152
|
| `HUBEX_ENV` | `dev` | Окружение: `dev` \| `stg` \| `prod`. От него зависят базовый URL и то, какой кэш swagger-схем берётся (`prod` или `dev`) |
|
|
153
153
|
| `HUBEX_APPLICATION_ID` | — (**обязательный**) | Значение заголовка `X-Application-ID` (`5` — Api/интеграции, `3` — MainWeb) |
|
|
154
|
-
| `HUBEX_TENANT_ID` | из JWT (`TenantID`) | Задаёт тенант явно. В режиме `service` не нужен |
|
|
154
|
+
| `HUBEX_TENANT_ID` | из JWT (`TenantID`) | Задаёт тенант явно. В режиме `login` — в каком тенанте авторизоваться; если не задан и тенантов несколько, сервер спросит. В режиме `service` не нужен |
|
|
155
|
+
| `HUBEX_USER_ID` | — | Только для `login`: userID, от имени которого войти, если пользователей несколько. Если не задан, сервер спросит |
|
|
155
156
|
| `HUBEX_AUTH_MODE` | `token` | Режим авторизации: `token` \| `login` \| `service` |
|
|
156
157
|
| `HUBEX_USERNAME` | — | Логин. Обязателен при `login` |
|
|
157
158
|
| `HUBEX_PASSWORD` | — | Пароль. Обязателен при `login` |
|
|
@@ -177,7 +178,7 @@ claude mcp add hubex --scope user \
|
|
|
177
178
|
| Настройка | Что делает | По умолчанию |
|
|
178
179
|
|---|---|---|
|
|
179
180
|
| `HUBEX_ENV` | Контур: `dev` — тестовый, `stg` — предбоевой, `prod` — боевой | `dev` |
|
|
180
|
-
| `HUBEX_TENANT_ID` | Номер тенанта. Обычно не нужен: сервер сам берёт его из вашего токена.
|
|
181
|
+
| `HUBEX_TENANT_ID` | Номер тенанта. Обычно не нужен: сервер сам берёт его из вашего токена. Если вы входите по логину и паролю и у вас несколько тенантов, сервер спросит, в какой войти, — переменная избавляет от вопроса | берётся из токена |
|
|
181
182
|
|
|
182
183
|
### Как входить
|
|
183
184
|
|
|
@@ -186,6 +187,7 @@ claude mcp add hubex --scope user \
|
|
|
186
187
|
| `HUBEX_AUTH_MODE` | Способ входа: `token` — сервер спросит токен при первом обращении, `login` — вход по логину и паролю, `service` — по долгоживущему служебному токену | `token` |
|
|
187
188
|
| `HUBEX_USERNAME` | Логин. Нужен только при `HUBEX_AUTH_MODE=login` | — |
|
|
188
189
|
| `HUBEX_PASSWORD` | Пароль. Нужен только при `HUBEX_AUTH_MODE=login` | — |
|
|
190
|
+
| `HUBEX_USER_ID` | Номер пользователя, от имени которого входить по логину и паролю, если в тенанте их у вас несколько. Если не задан, сервер спросит | — |
|
|
189
191
|
| `HUBEX_SERVICE_TOKEN` | Служебный токен, который выдаёт администратор HubEx. Нужен только при `HUBEX_AUTH_MODE=service`. Удобен, когда сервер работает сам по себе и спросить токен не у кого | — |
|
|
190
192
|
|
|
191
193
|
### Что помощнику разрешено делать
|
|
@@ -206,7 +208,7 @@ claude mcp add hubex --scope user \
|
|
|
206
208
|
| Настройка | Что делает |
|
|
207
209
|
|---|---|
|
|
208
210
|
| `HUBEX_DEFAULT_COMPANY_ID` | Компания по умолчанию |
|
|
209
|
-
| `HUBEX_DEFAULT_TASK_TYPE_ID` | Тип заявки по
|
|
211
|
+
| `HUBEX_DEFAULT_TASK_TYPE_ID` | Тип заявки по умолчанию. Если не задан, перед созданием заявки ассистент спросит тип у вас |
|
|
210
212
|
| `HUBEX_DEFAULT_REQUEST_METHOD_ID` | Способ обращения по умолчанию (например, «звонок» или «почта») |
|
|
211
213
|
|
|
212
214
|
### Пример: строгая настройка для боевого контура
|
package/dist/auth.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication. One lifecycle, three ways to obtain the first access token:
|
|
3
3
|
* - "token": the user is asked for a ready Bearer JWT.
|
|
4
|
-
* - "login": Basic (username:password)
|
|
4
|
+
* - "login": Basic (username:password) via AUTHN, then a tenant user JWT via
|
|
5
|
+
* AUTHZ/Accounts/authorize.
|
|
5
6
|
* - "service": a long-lived service token -> JWT via AUTHZ/AccessTokens.
|
|
6
7
|
* Afterwards all three refresh the same way.
|
|
7
8
|
*/
|
|
9
|
+
export const SELECT_TENANT_TOOL = "hubex_select_tenant";
|
|
8
10
|
/** Decode a JWT payload without verifying the signature (we only read claims). */
|
|
9
11
|
export function decodeJwt(token) {
|
|
10
12
|
const parts = token.split(".");
|
|
@@ -41,6 +43,16 @@ export function assertUsableJwt(token) {
|
|
|
41
43
|
throw new Error("Срок действия токена уже истёк. Нужен свежий access-токен HubEx.");
|
|
42
44
|
}
|
|
43
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Lists tenant users for the model to relay. Person names stay out: this text
|
|
48
|
+
* reaches the LLM context, which only gets PII through the masking layer.
|
|
49
|
+
*/
|
|
50
|
+
function describeMembers(members) {
|
|
51
|
+
return members
|
|
52
|
+
.map((m) => `тенант ${m.tenantID}${m.name ? ` «${m.name}»` : ""}, пользователь ${m.userID}` +
|
|
53
|
+
(m.tenantMemberDescription ? ` (${m.tenantMemberDescription})` : ""))
|
|
54
|
+
.join("; ");
|
|
55
|
+
}
|
|
44
56
|
/**
|
|
45
57
|
* Obtains a JWT and keeps it fresh. Three ways in — Basic credentials
|
|
46
58
|
* ("login"), a long-lived service token ("service") or a token typed in by the
|
|
@@ -49,6 +61,9 @@ export function assertUsableJwt(token) {
|
|
|
49
61
|
class RefreshingAuth {
|
|
50
62
|
config;
|
|
51
63
|
prompter;
|
|
64
|
+
chooser;
|
|
65
|
+
/** Tenant user picked by the user; survives re-logins so they are asked once. */
|
|
66
|
+
chosen;
|
|
52
67
|
accessToken;
|
|
53
68
|
accessExp; // seconds since epoch
|
|
54
69
|
/**
|
|
@@ -70,6 +85,28 @@ class RefreshingAuth {
|
|
|
70
85
|
setPrompter(prompt) {
|
|
71
86
|
this.prompter = prompt;
|
|
72
87
|
}
|
|
88
|
+
setTenantChooser(choose) {
|
|
89
|
+
this.chooser = choose;
|
|
90
|
+
}
|
|
91
|
+
async selectTenantMember(tenantID, userID) {
|
|
92
|
+
if (this.inflight)
|
|
93
|
+
await this.inflight.catch(() => undefined);
|
|
94
|
+
const previous = this.chosen;
|
|
95
|
+
this.chosen = { tenantID, userID };
|
|
96
|
+
// Tokens of the previous user must not be reused or refreshed.
|
|
97
|
+
this.accessToken = undefined;
|
|
98
|
+
this.accessExp = undefined;
|
|
99
|
+
this.refreshToken = undefined;
|
|
100
|
+
this.refreshExp = undefined;
|
|
101
|
+
try {
|
|
102
|
+
await this.getAccessToken();
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// A wrong pick must not lock out the next login.
|
|
106
|
+
this.chosen = previous;
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
73
110
|
/** Stores a token supplied out of band and immediately buys a refresh token. */
|
|
74
111
|
async acceptToken(token) {
|
|
75
112
|
const trimmed = token.trim();
|
|
@@ -167,13 +204,65 @@ class RefreshingAuth {
|
|
|
167
204
|
assertUsableJwt(token);
|
|
168
205
|
return token;
|
|
169
206
|
}
|
|
170
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Two steps, as in the HubEx web and mobile apps. AUTHN only proves the
|
|
209
|
+
* credentials: its token carries no tenant, so every service answers 401 and
|
|
210
|
+
* AUTHZ/RefreshTokens 403. AUTHZ/Accounts/authorize trades it for a token of
|
|
211
|
+
* one tenant member, picked from the tenants the login reported.
|
|
212
|
+
*/
|
|
213
|
+
async loginByPassword() {
|
|
171
214
|
const basic = Buffer.from(`${encodeURIComponent(this.config.username)}:${encodeURIComponent(this.config.password)}`).toString("base64");
|
|
172
|
-
|
|
215
|
+
const account = (await this.fetchJwt(`${this.config.apiBaseUrl}/AUTHN/Accounts/login`, { method: "POST", headers: this.authHeaders({ Authorization: `Basic ${basic}` }) }, `Проверь HUBEX_USERNAME и HUBEX_PASSWORD: учётная запись должна существовать в окружении ` +
|
|
216
|
+
`HUBEX_ENV=${this.config.env}.`));
|
|
217
|
+
const member = await this.pickTenantMember(account.tenantEntities ?? []);
|
|
218
|
+
// The chosen tenant, even when switching users: absorb() keeps a known tenant.
|
|
219
|
+
this.tenantId = String(member.tenantID);
|
|
220
|
+
return this.fetchJwt(`${this.config.apiBaseUrl}/AUTHZ/Accounts/authorize`, {
|
|
173
221
|
method: "POST",
|
|
174
|
-
headers: this.authHeaders({
|
|
222
|
+
headers: this.authHeaders({
|
|
223
|
+
Authorization: `Bearer ${account.access_token}`,
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
}),
|
|
226
|
+
body: JSON.stringify({ tenantID: member.tenantID, tenantMemberID: member.tenantMemberID }),
|
|
175
227
|
});
|
|
176
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Narrows the account's tenant users by HUBEX_TENANT_ID / HUBEX_USER_ID. One
|
|
231
|
+
* left — sign in as it; several — the user decides, once.
|
|
232
|
+
*/
|
|
233
|
+
async pickTenantMember(members) {
|
|
234
|
+
if (members.length === 0) {
|
|
235
|
+
throw new Error("Учётная запись не состоит ни в одном тенанте — авторизоваться не в чем.");
|
|
236
|
+
}
|
|
237
|
+
const { tenantId, userId } = this.config;
|
|
238
|
+
const candidates = members.filter((m) => (!tenantId || String(m.tenantID) === tenantId) && (!userId || String(m.userID) === userId));
|
|
239
|
+
if (candidates.length === 0) {
|
|
240
|
+
const filter = [tenantId && `HUBEX_TENANT_ID=${tenantId}`, userId && `HUBEX_USER_ID=${userId}`]
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.join(", ");
|
|
243
|
+
throw new Error(`Нет пользователя с ${filter}. Учётной записи доступны: ${describeMembers(members)}.`);
|
|
244
|
+
}
|
|
245
|
+
const chosen = this.chosen;
|
|
246
|
+
if (chosen) {
|
|
247
|
+
const match = candidates.find((m) => m.tenantID === chosen.tenantID && m.userID === chosen.userID);
|
|
248
|
+
if (!match) {
|
|
249
|
+
throw new Error(`Нет пользователя ${chosen.userID} в тенанте ${chosen.tenantID}. ` +
|
|
250
|
+
`Доступны: ${describeMembers(candidates)}.`);
|
|
251
|
+
}
|
|
252
|
+
return match;
|
|
253
|
+
}
|
|
254
|
+
if (candidates.length === 1)
|
|
255
|
+
return candidates[0];
|
|
256
|
+
const picked = this.chooser ? await this.chooser(candidates) : undefined;
|
|
257
|
+
if (!picked) {
|
|
258
|
+
throw new Error(`Учётная запись может войти от имени нескольких пользователей HubEx. Спроси пользователя, ` +
|
|
259
|
+
`под кем работать, и вызови ${SELECT_TENANT_TOOL} с его tenantID и userID ` +
|
|
260
|
+
`(или задай HUBEX_TENANT_ID и HUBEX_USER_ID в настройках сервера). ` +
|
|
261
|
+
`Варианты: ${describeMembers(candidates)}.`);
|
|
262
|
+
}
|
|
263
|
+
this.chosen = { tenantID: picked.tenantID, userID: picked.userID };
|
|
264
|
+
return picked;
|
|
265
|
+
}
|
|
177
266
|
/** AUTHZ/AccessTokens is anonymous — the service token itself identifies the member. */
|
|
178
267
|
loginByServiceToken() {
|
|
179
268
|
return this.fetchJwt(`${this.config.apiBaseUrl}/AUTHZ/AccessTokens`, {
|
|
@@ -213,12 +302,16 @@ class RefreshingAuth {
|
|
|
213
302
|
...extra,
|
|
214
303
|
};
|
|
215
304
|
}
|
|
216
|
-
|
|
305
|
+
/** `rejectHint` replaces the default wrong-environment hint on 401/403/409. */
|
|
306
|
+
async fetchJwt(url, init, rejectHint) {
|
|
217
307
|
const res = await fetch(url, init);
|
|
218
308
|
if (!res.ok) {
|
|
219
309
|
const body = await res.text().catch(() => "");
|
|
220
310
|
let message = `${init.method ?? "GET"} ${url} failed (HTTP ${res.status}): ${body.slice(0, 500)}`;
|
|
221
|
-
if (
|
|
311
|
+
if (rejectHint && [401, 403, 409].includes(res.status)) {
|
|
312
|
+
message += ` ${rejectHint}`;
|
|
313
|
+
}
|
|
314
|
+
else if (res.status === 401 || res.status === 403) {
|
|
222
315
|
message +=
|
|
223
316
|
` Сейчас выбрано окружение HUBEX_ENV=${this.config.env}. Если токен получен в другом ` +
|
|
224
317
|
`окружении (dev/stg/prod), авторизация не пройдёт — проверь, что токен именно из ${this.config.env}.`;
|
package/dist/config.js
CHANGED
|
@@ -93,6 +93,7 @@ export function loadConfig(env = process.env) {
|
|
|
93
93
|
swaggerCatalog: swaggerCatalogFor(hubexEnv),
|
|
94
94
|
applicationId: required("HUBEX_APPLICATION_ID", env.HUBEX_APPLICATION_ID),
|
|
95
95
|
tenantId: env.HUBEX_TENANT_ID?.trim() || undefined,
|
|
96
|
+
userId: env.HUBEX_USER_ID?.trim() || undefined,
|
|
96
97
|
authMode: authMode,
|
|
97
98
|
readonly: parseBool(env.HUBEX_READONLY),
|
|
98
99
|
maskPii: parseBoolDefaultTrue(env.HUBEX_MASK_PII),
|
package/dist/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { createAuthProvider } from "./auth.js";
|
|
9
9
|
import { createElicitPrompter } from "./token-prompt.js";
|
|
10
|
+
import { createElicitTenantChooser } from "./tenant-prompt.js";
|
|
10
11
|
import { HubexClient } from "./http.js";
|
|
11
12
|
import { EndpointIndex, loadIndexFile } from "./index/store.js";
|
|
12
13
|
import { SchemaProvider } from "./schema/describe.js";
|
|
@@ -51,6 +52,8 @@ export function createServer(config) {
|
|
|
51
52
|
// Провайдер создаётся раньше сервера, поэтому способ ввода вносится сюда, а не в конструктор.
|
|
52
53
|
if (config.authMode === "token")
|
|
53
54
|
auth.setPrompter(createElicitPrompter(server));
|
|
55
|
+
if (config.authMode === "login")
|
|
56
|
+
auth.setTenantChooser(createElicitTenantChooser(server));
|
|
54
57
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
55
58
|
tools: [...registry.values()].map((t) => ({
|
|
56
59
|
name: t.definition.name,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking the user which tenant user to sign in as in `HUBEX_AUTH_MODE=login`,
|
|
3
|
+
* when the account has several and HUBEX_TENANT_ID / HUBEX_USER_ID do not
|
|
4
|
+
* narrow them to one.
|
|
5
|
+
*
|
|
6
|
+
* The primary channel is MCP elicitation: the client shows a pick list and the
|
|
7
|
+
* people's names in it never pass through the model's context. Clients without
|
|
8
|
+
* that capability fall back to the `hubex_select_tenant` tool.
|
|
9
|
+
*/
|
|
10
|
+
import { SELECT_TENANT_TOOL } from "./auth.js";
|
|
11
|
+
const PROMPT_MESSAGE = "Учётная запись HubEx может войти от имени нескольких пользователей. Выбери, под кем работать — " +
|
|
12
|
+
"выбор сохранится до перезапуска сервера.";
|
|
13
|
+
function optionValue(m) {
|
|
14
|
+
return `${m.tenantID}:${m.userID}`;
|
|
15
|
+
}
|
|
16
|
+
function optionLabel(m) {
|
|
17
|
+
const person = [m.lastName, m.firstName].filter(Boolean).join(" ");
|
|
18
|
+
const details = [person, m.tenantMemberDescription].filter(Boolean).join(", ");
|
|
19
|
+
return `${m.name ?? `Тенант ${m.tenantID}`}${details ? ` — ${details}` : ""} ` +
|
|
20
|
+
`(тенант ${m.tenantID}, пользователь ${m.userID})`;
|
|
21
|
+
}
|
|
22
|
+
export function createElicitTenantChooser(server) {
|
|
23
|
+
return async (options) => {
|
|
24
|
+
if (!server.getClientCapabilities()?.elicitation)
|
|
25
|
+
return undefined;
|
|
26
|
+
const result = await server.elicitInput({
|
|
27
|
+
message: PROMPT_MESSAGE,
|
|
28
|
+
requestedSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
member: {
|
|
32
|
+
type: "string",
|
|
33
|
+
title: "Пользователь HubEx",
|
|
34
|
+
enum: options.map(optionValue),
|
|
35
|
+
enumNames: options.map(optionLabel),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ["member"],
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (result.action !== "accept") {
|
|
42
|
+
throw new Error("Выбор пользователя HubEx отменён — авторизоваться не удалось. Если клиент заявляет " +
|
|
43
|
+
"поддержку elicitation, но не показывает форму, спроси пользователя, под кем работать, " +
|
|
44
|
+
`и вызови ${SELECT_TENANT_TOOL} с его tenantID и userID.`);
|
|
45
|
+
}
|
|
46
|
+
const picked = options.find((m) => optionValue(m) === result.content?.member);
|
|
47
|
+
if (!picked)
|
|
48
|
+
throw new Error("Пользователь HubEx не выбран.");
|
|
49
|
+
return picked;
|
|
50
|
+
};
|
|
51
|
+
}
|
package/dist/tools/curated.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hand-written convenience tools layered on top of the generated ones.
|
|
3
3
|
*/
|
|
4
|
-
import { decodeJwt } from "../auth.js";
|
|
4
|
+
import { SELECT_TENANT_TOOL, decodeJwt } from "../auth.js";
|
|
5
5
|
import { SET_TOKEN_TOOL } from "../token-prompt.js";
|
|
6
6
|
import { okResult, failResult } from "./registry.js";
|
|
7
7
|
/** Marker prepended to test data so it is easy to find and clean up later. */
|
|
@@ -75,7 +75,49 @@ function setToken(auth) {
|
|
|
75
75
|
},
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Fallback for clients without elicitation support: the model asks the user
|
|
80
|
+
* which tenant user to sign in as and passes the answer here.
|
|
81
|
+
*/
|
|
82
|
+
function selectTenant(auth) {
|
|
83
|
+
return {
|
|
84
|
+
definition: {
|
|
85
|
+
name: SELECT_TENANT_TOOL,
|
|
86
|
+
description: "Выбрать, от имени какого пользователя HubEx работать, если учётная запись может войти от " +
|
|
87
|
+
"нескольких (в разных тенантах или в одном). Нужен, если клиент не умеет показывать выбор " +
|
|
88
|
+
"сам: варианты придут в ошибке авторизации — спроси пользователя и передай его выбор сюда. " +
|
|
89
|
+
"Выбор действует до перезапуска сервера.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
tenantID: { type: "integer", description: "ID тенанта." },
|
|
94
|
+
userID: { type: "integer", description: "ID пользователя в этом тенанте." },
|
|
95
|
+
},
|
|
96
|
+
required: ["tenantID", "userID"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
async execute(args) {
|
|
100
|
+
try {
|
|
101
|
+
await auth.selectTenantMember(Number(args.tenantID), Number(args.userID));
|
|
102
|
+
const claims = decodeJwt(await auth.getAccessToken());
|
|
103
|
+
return okResult({
|
|
104
|
+
ok: true,
|
|
105
|
+
tenantId: auth.getTenantId(),
|
|
106
|
+
tenantMemberId: claims?.TenantMemberID,
|
|
107
|
+
account: claims?.sub,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return failResult(err);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Типы заявок у каждого тенанта свои: без серверного дефолта модель не должна выбирать тип сама.
|
|
117
|
+
const ASK_TASK_TYPE = "Спроси пользователя, какой тип заявки использовать: покажи ему список из " +
|
|
118
|
+
"hubex_request_read с endpointId WORK:GET:/TaskTypes и не выбирай тип сам.";
|
|
78
119
|
function createTestTask(client, config) {
|
|
120
|
+
const defaultTaskTypeID = config.defaults.taskTypeID;
|
|
79
121
|
return {
|
|
80
122
|
definition: {
|
|
81
123
|
name: "hubex_create_test_task",
|
|
@@ -87,7 +129,10 @@ function createTestTask(client, config) {
|
|
|
87
129
|
notes: { type: "string", description: "Текст/описание заявки." },
|
|
88
130
|
taskTypeID: {
|
|
89
131
|
type: "integer",
|
|
90
|
-
description:
|
|
132
|
+
description: defaultTaskTypeID !== undefined
|
|
133
|
+
? "ID типа заявки (список: hubex_request_read с endpointId WORK:GET:/TaskTypes). " +
|
|
134
|
+
`По умолчанию ${defaultTaskTypeID} (HUBEX_DEFAULT_TASK_TYPE_ID).`
|
|
135
|
+
: `ID типа заявки. Обязателен: дефолт HUBEX_DEFAULT_TASK_TYPE_ID не задан. ${ASK_TASK_TYPE}`,
|
|
91
136
|
},
|
|
92
137
|
workTypeID: {
|
|
93
138
|
type: "integer",
|
|
@@ -104,20 +149,22 @@ function createTestTask(client, config) {
|
|
|
104
149
|
"По умолчанию 1 (WEB), если не настроен HUBEX_DEFAULT_REQUEST_METHOD_ID.",
|
|
105
150
|
},
|
|
106
151
|
},
|
|
107
|
-
required: ["notes"],
|
|
152
|
+
required: defaultTaskTypeID !== undefined ? ["notes"] : ["notes", "taskTypeID"],
|
|
108
153
|
},
|
|
109
154
|
},
|
|
110
155
|
async execute(args) {
|
|
111
156
|
try {
|
|
157
|
+
const taskTypeID = args.taskTypeID ?? defaultTaskTypeID;
|
|
158
|
+
if (taskTypeID === undefined) {
|
|
159
|
+
return failResult(new Error(`Не указан taskTypeID, а HUBEX_DEFAULT_TASK_TYPE_ID не задан. ${ASK_TASK_TYPE}`));
|
|
160
|
+
}
|
|
112
161
|
const notes = String(args.notes ?? "").trim();
|
|
113
162
|
const body = {
|
|
114
163
|
notes: `${TEST_MARKER} ${notes}`.trim(),
|
|
115
164
|
// Приоритет: явный аргумент модели > серверный дефолт (HUBEX_DEFAULT_*) > встроенный дефолт.
|
|
116
165
|
requestMethodID: args.requestMethodID ?? config.defaults.requestMethodID ?? 1,
|
|
166
|
+
taskTypeID,
|
|
117
167
|
};
|
|
118
|
-
const taskTypeID = args.taskTypeID ?? config.defaults.taskTypeID;
|
|
119
|
-
if (taskTypeID !== undefined)
|
|
120
|
-
body.taskTypeID = taskTypeID;
|
|
121
168
|
const companyID = args.companyID ?? config.defaults.companyID;
|
|
122
169
|
if (companyID !== undefined)
|
|
123
170
|
body.companyID = companyID;
|
|
@@ -144,6 +191,8 @@ export function buildCuratedTools(client, config, auth) {
|
|
|
144
191
|
const tools = [whoami(config, auth)];
|
|
145
192
|
if (config.authMode === "token")
|
|
146
193
|
tools.push(setToken(auth));
|
|
194
|
+
if (config.authMode === "login")
|
|
195
|
+
tools.push(selectTenant(auth));
|
|
147
196
|
if (config.methods.includes("POST") && (!config.services || config.services.includes("WORK"))) {
|
|
148
197
|
tools.push(createTestTask(client, config));
|
|
149
198
|
}
|
|
@@ -65,8 +65,11 @@ sources:
|
|
|
65
65
|
Набор полей заявки в каждом тенанте свой. Не собирай тело по этому гайду по памяти
|
|
66
66
|
и не придумывай значения за пользователя — действуй так:
|
|
67
67
|
|
|
68
|
-
1. Возьми `taskTypeID
|
|
69
|
-
|
|
68
|
+
1. Возьми `taskTypeID`. Если пользователь тип не назвал и он не задан в настройках
|
|
69
|
+
сервера (тогда `taskTypeID` перечислен среди подставляемых полей в описании
|
|
70
|
+
`hubex_request_write`), покажи список из `WORK:GET:/TaskTypes` и спроси, какой
|
|
71
|
+
нужен. Не выбирай тип сам по похожему названию; молча брать можно, только если
|
|
72
|
+
тип в тенанте один.
|
|
70
73
|
2. Запроси `WORK:GET:/Tasks/new/meta` с query `taskTypeID=<выбранный>` **до** сборки тела.
|
|
71
74
|
3. Разбери ответ на три группы:
|
|
72
75
|
- `capability` = `RWM` в `components` → **обязательное поле**, заявка без него
|