feat(api-tools): native TS tools for Jira/Confluence/GitLab, drop MCP bridge
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Pure REST helpers for the corporate wiki (Confluence Server/DC, rest/api).
|
||||
* `docsUrl` comes from config/company.json; `CONFLUENCE_TOKEN` from the env.
|
||||
* No pi API — unit-testable.
|
||||
*/
|
||||
import { apiRequest, requireToken, ApiError } from "./atlassian.ts";
|
||||
import { readLocalConfig } from "./company-config.ts";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CONFIG_PATH = path.join(HERE, "..", "..", "config", "company.json");
|
||||
|
||||
/** Resolve the Confluence base URL from company.json (env override for labs). */
|
||||
export function confluenceBaseUrl(): string {
|
||||
const cfg = readLocalConfig(CONFIG_PATH);
|
||||
const fromCfg = cfg.docsUrl && cfg.docsUrl !== "TODO" ? cfg.docsUrl : "";
|
||||
const url = (process.env.CONFLUENCE_URL && process.env.CONFLUENCE_URL !== "TODO" ? process.env.CONFLUENCE_URL : fromCfg).replace(/\/+$/, "");
|
||||
if (!url) throw new ApiError("Не задан docsUrl в config/company.json и нет CONFLUENCE_URL.");
|
||||
return url;
|
||||
}
|
||||
|
||||
function token(): string {
|
||||
return requireToken(
|
||||
"CONFLUENCE_TOKEN",
|
||||
"personal access token Confluence. Где брать: Профиль → Security → API tokens / Personal Access Tokens → создать",
|
||||
);
|
||||
}
|
||||
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
url?: string;
|
||||
body?: string;
|
||||
spaceKey?: string;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
pages: Page[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** CQL search for Confluence pages. `cql` supports the standard text/title/space operators. */
|
||||
export async function search(cql: string, opts: { limit?: number; expandBody?: boolean } = {}): Promise<SearchResult> {
|
||||
const base = confluenceBaseUrl();
|
||||
if (!cql) throw new ApiError("search требует CQL-запрос (например 'text=\"foo\"').");
|
||||
const data: any = await apiRequest(base, token(), "/rest/api/content/search", {
|
||||
query: { cql, limit: opts.limit ?? 25, expand: opts.expandBody ? "body.view" : undefined },
|
||||
});
|
||||
const pages: Page[] = (data?.results ?? []).map((p: any) => ({
|
||||
id: p.id,
|
||||
title: p.title ?? "",
|
||||
spaceKey: p.space?.key,
|
||||
body: opts.expandBody ? (p.body?.view?.value ?? "") : undefined,
|
||||
url: `${base}/spaces/${p.space?.key ?? "~self"}/pages/${p.id}`,
|
||||
}));
|
||||
return { pages, total: data?.size ?? pages.length };
|
||||
}
|
||||
|
||||
export async function pageGet(id: string, opts: { expandBody?: boolean } = {}): Promise<Page> {
|
||||
const base = confluenceBaseUrl();
|
||||
if (!id) throw new ApiError("pageGet требует id страницы.");
|
||||
const data: any = await apiRequest(base, token(), `/rest/api/content/${encodeURIComponent(id)}`, {
|
||||
query: opts.expandBody ? { expand: "body.view,version" } : undefined,
|
||||
});
|
||||
return {
|
||||
id: data?.id ?? id,
|
||||
title: data?.title ?? "",
|
||||
spaceKey: data?.space?.key,
|
||||
body: opts.expandBody ? (data?.body?.view?.value ?? "") : undefined,
|
||||
url: `${base}/spaces/${data?.space?.key ?? "~self"}/pages/${data?.id ?? id}`,
|
||||
};
|
||||
}
|
||||
|
||||
export interface PageCreateInput {
|
||||
spaceKey: string;
|
||||
title: string;
|
||||
body: string; // HTML (Storage format) or text
|
||||
parentId?: string;
|
||||
representation?: "storage" | "editor" | "wiki";
|
||||
}
|
||||
|
||||
/** Create a new Confluence page. `body` is HTML in Storage format by default. */
|
||||
export async function pageCreate(input: PageCreateInput): Promise<Page> {
|
||||
const base = confluenceBaseUrl();
|
||||
if (!input.spaceKey || !input.title || !input.body) {
|
||||
throw new ApiError("pageCreate требует spaceKey, title и body.");
|
||||
}
|
||||
const rep = input.representation ?? "storage";
|
||||
const body: Record<string, unknown> = {
|
||||
type: "page",
|
||||
title: input.title,
|
||||
space: { key: input.spaceKey },
|
||||
body: { [rep]: { value: input.body, representation: rep } },
|
||||
};
|
||||
if (input.parentId) (body as any).ancestors = [{ id: input.parentId }];
|
||||
|
||||
const data: any = await apiRequest(base, token(), "/rest/api/content", { method: "POST", body });
|
||||
return {
|
||||
id: data?.id ?? "",
|
||||
title: data?.title ?? input.title,
|
||||
spaceKey: data?.space?.key ?? input.spaceKey,
|
||||
url: `${base}/spaces/${input.spaceKey}/pages/${data?.id ?? ""}`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user