forked from EduCraft/curriculum-project-hub
feat(hub): built-in PBank 题库 capability + role tools (v0.0.42)
Register pbank as an ADR-0027 external capability with org-scoped username/password envelopes, readiness via /login, and in-process cph_hub MCP tools (search/get/get_many) that materialize sources under the run workspace. Extend the capability secret payload for docmind vs pbank kinds, admin capabilities UI, role tool umbrella `pbank`, and the pbank-problem-report skill. Credentials never reach the Agent process.
This commit is contained in:
@@ -17,6 +17,8 @@ import {
|
||||
type PdfToMdBatchItemResult,
|
||||
} from "../capability/pdfToMdBundle.js";
|
||||
import { AliyunDocmindClient } from "../capability/docmindClient.js";
|
||||
import { createPbankService, type PbankToolResult } from "../capability/pbank.js";
|
||||
import { CapabilityConnectionUnavailable } from "../capability/types.js";
|
||||
|
||||
export interface FileDeliveryToolOptions {
|
||||
readonly rt: FeishuRuntime;
|
||||
@@ -321,6 +323,83 @@ export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): M
|
||||
);
|
||||
}
|
||||
|
||||
const pbankEnabled =
|
||||
enabledTools.has("pbank_search_problems") ||
|
||||
enabledTools.has("pbank_get_problem") ||
|
||||
enabledTools.has("pbank_get_many_problems");
|
||||
if (pbankEnabled) {
|
||||
const pbank = createPbankService({
|
||||
prisma: options.prisma,
|
||||
secrets: options.secretEnvelope,
|
||||
});
|
||||
const pbankCtx = {
|
||||
organizationId: options.organizationId,
|
||||
runId: options.runId,
|
||||
workspaceDir: options.workspaceDir,
|
||||
};
|
||||
|
||||
if (enabledTools.has("pbank_search_problems")) {
|
||||
tools.push(
|
||||
tool(
|
||||
"pbank_search_problems",
|
||||
"Search Paradigm PBank (题库) by title/keyword. Returns page metadata, operator-confirmed rights guidance, and matching problem summaries. Requires an ACTIVE org capability connection for `pbank`.",
|
||||
{
|
||||
q: z.string().optional().describe("Search text."),
|
||||
keywords: z.array(z.string()).optional().describe("Exact keywords to filter by."),
|
||||
pageNum: z.number().int().min(1).optional().describe("Page number (default 1)."),
|
||||
pageSize: z.number().int().min(1).max(50).optional().describe("Page size (default 10, max 50)."),
|
||||
},
|
||||
async (args) =>
|
||||
runPbankTool(() =>
|
||||
pbank.searchProblems(pbankCtx, {
|
||||
q: args.q,
|
||||
keywords: args.keywords,
|
||||
pageNum: args.pageNum,
|
||||
pageSize: args.pageSize,
|
||||
}),
|
||||
),
|
||||
{ alwaysLoad: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (enabledTools.has("pbank_get_problem")) {
|
||||
tools.push(
|
||||
tool(
|
||||
"pbank_get_problem",
|
||||
"Fetch one PBank problem by URL or UUID. Returns metadata, rights guidance, text-like source files, local zip/extract paths under .pbank-sources/, and optional image assets. Requires ACTIVE org capability `pbank`.",
|
||||
{
|
||||
urlOrId: z.string().min(1).describe("PBank problem URL or UUID."),
|
||||
includeProjects: z.boolean().optional().describe("Include problem/answer project archives (default true)."),
|
||||
materializeProjects: z.boolean().optional().describe("Write archives under workspace .pbank-sources/ (default true)."),
|
||||
includeAssetImages: z.boolean().optional().describe("Inline small image assets when possible (default true)."),
|
||||
includeOccurrences: z.boolean().optional().describe("Include occurrence list (default false)."),
|
||||
},
|
||||
async (args) => runPbankTool(() => pbank.getProblem(pbankCtx, args)),
|
||||
{ alwaysLoad: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (enabledTools.has("pbank_get_many_problems")) {
|
||||
tools.push(
|
||||
tool(
|
||||
"pbank_get_many_problems",
|
||||
"Fetch several PBank problems by URL or UUID. Use when the teacher pastes multiple example links. Returns rights guidance together with each problem. Requires ACTIVE org capability `pbank`.",
|
||||
{
|
||||
urlsOrIds: z.array(z.string().min(1)).min(1).max(20).describe("PBank problem URLs or UUIDs."),
|
||||
includeProjects: z.boolean().optional().describe("Include problem/answer project archives (default true)."),
|
||||
materializeProjects: z.boolean().optional().describe("Write archives under workspace .pbank-sources/ (default true)."),
|
||||
includeAssetImages: z.boolean().optional().describe("Inline small image assets when possible (default true)."),
|
||||
includeOccurrences: z.boolean().optional().describe("Include occurrence list (default false)."),
|
||||
},
|
||||
async (args) => runPbankTool(() => pbank.getManyProblems(pbankCtx, args)),
|
||||
{ alwaysLoad: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const instructions = mcpInstructions(enabledTools);
|
||||
return createSdkMcpServer({
|
||||
name: "cph_hub",
|
||||
@@ -357,6 +436,36 @@ function formatPdfToMdBatchResult(results: readonly PdfToMdBatchItemResult[]): s
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function runPbankTool(
|
||||
invoke: () => Promise<PbankToolResult>,
|
||||
): Promise<{
|
||||
content: Array<
|
||||
| { type: "text"; text: string }
|
||||
| { type: "image"; data: string; mimeType: string }
|
||||
>;
|
||||
isError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const result = await invoke();
|
||||
const content: Array<
|
||||
| { type: "text"; text: string }
|
||||
| { type: "image"; data: string; mimeType: string }
|
||||
> = [{ type: "text", text: JSON.stringify(result.data, null, 2) }];
|
||||
for (const image of result.inlineImages) {
|
||||
content.push({ type: "image", data: image.data, mimeType: image.mimeType });
|
||||
}
|
||||
return { content };
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof CapabilityConnectionUnavailable
|
||||
? `${error.message}. Ask an org admin to configure the pbank capability connection.`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
return { isError: true, content: [{ type: "text", text: message }] };
|
||||
}
|
||||
}
|
||||
|
||||
function mcpInstructions(enabledTools: ReadonlySet<CphHubMcpToolId>): string {
|
||||
const instructions: string[] = [];
|
||||
if (enabledTools.has("send_file")) {
|
||||
@@ -386,6 +495,18 @@ function mcpInstructions(enabledTools: ReadonlySet<CphHubMcpToolId>): string {
|
||||
"Do NOT attempt to parse PDFs yourself with Read or Bash — always use convert_pdf_to_md.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
enabledTools.has("pbank_search_problems") ||
|
||||
enabledTools.has("pbank_get_problem") ||
|
||||
enabledTools.has("pbank_get_many_problems")
|
||||
) {
|
||||
instructions.push(
|
||||
"Use pbank_search_problems / pbank_get_problem / pbank_get_many_problems for Paradigm PBank (题库) selection.",
|
||||
"Treat the returned rights object as authoritative for derivative use.",
|
||||
"Materialized sources land under workspace-relative .pbank-sources/ — read them; do not invent problem content.",
|
||||
"If tools fail because no ACTIVE pbank capability connection exists, tell the user an org admin must configure 题库 on the admin capabilities page.",
|
||||
);
|
||||
}
|
||||
instructions.push(
|
||||
"Role skill docs (when bound) are readable at .cph/runtime-skills/<skill-name>/SKILL.md or $CPH_RUNTIME_SKILLS_DIR/<skill-name>/SKILL.md. Prefer the Skill tool when available. Workspace .claude/ and .mcp.json are sandbox stubs — not skill or MCP source.",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user