forked from EduCraft/curriculum-project-hub
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 251ad43ca2 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@paradigm/hub",
|
"name": "@paradigm/hub",
|
||||||
"version": "0.0.33",
|
"version": "0.0.32",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
name: pdf-to-md
|
|
||||||
description: >
|
|
||||||
Convert PDF documents to Markdown bundles using the convert_pdf_to_md tool.
|
|
||||||
Handles PDFs from Feishu messages, local workspace files, and produces
|
|
||||||
high-quality Markdown with LaTeX formulas and extracted images.
|
|
||||||
---
|
|
||||||
|
|
||||||
# PDF to Markdown Conversion
|
|
||||||
|
|
||||||
## When to use
|
|
||||||
|
|
||||||
Use this skill when the user asks to convert a PDF to Markdown, extract text
|
|
||||||
from a PDF, or turn a PDF document into an editable format.
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
The `convert_pdf_to_md` tool (provided by the `cph_hub` MCP server) calls
|
|
||||||
Alibaba Cloud Document Mind to parse the PDF. It:
|
|
||||||
|
|
||||||
- Extracts text in reading order (handles multi-column, scanned, and
|
|
||||||
multi-language documents)
|
|
||||||
- Converts mathematical formulas to **LaTeX** (`$...$` inline, `$$...$$` block)
|
|
||||||
- Extracts tables as Markdown tables
|
|
||||||
- Downloads embedded images into the output directory
|
|
||||||
- Writes a single `document.md` file plus image files
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
### PDF from a Feishu message
|
|
||||||
|
|
||||||
1. Use `feishu_read_context` to find the `file_key` of the PDF attachment.
|
|
||||||
2. Use `feishu_download_resource` to download it into the workspace.
|
|
||||||
3. Use `convert_pdf_to_md` with the downloaded file path and an output directory.
|
|
||||||
|
|
||||||
### PDF already in the workspace
|
|
||||||
|
|
||||||
1. Use `convert_pdf_to_md` directly with the file path and an output directory.
|
|
||||||
|
|
||||||
## Important rules
|
|
||||||
|
|
||||||
- **Always** use `convert_pdf_to_md` for PDF→Markdown. Do NOT attempt to parse
|
|
||||||
PDFs yourself with Read, Bash, Python, or any other method. The tool provides
|
|
||||||
accurate formula, table, and image extraction that manual methods cannot
|
|
||||||
match.
|
|
||||||
- If `convert_pdf_to_md` fails because no capability connection is configured,
|
|
||||||
tell the user to ask their organization admin to configure the Aliyun
|
|
||||||
docmind credential in the admin web UI (组织后台 → 能力).
|
|
||||||
- The output directory will be created if it does not exist.
|
|
||||||
- After conversion, use `send_file` to send the generated markdown back to the
|
|
||||||
user if they requested it.
|
|
||||||
|
|
||||||
## Output
|
|
||||||
|
|
||||||
The tool returns a list of generated files:
|
|
||||||
- `document.md` — the main markdown file
|
|
||||||
- `*.jpg` / `*.png` — extracted images, referenced from the markdown
|
|
||||||
|
|
||||||
## Cost
|
|
||||||
|
|
||||||
The conversion is billed per page (0.04 CNY/page ≈ $0.0056/page for the
|
|
||||||
enhanced formula mode). The cost is automatically recorded on the run's
|
|
||||||
usage ledger.
|
|
||||||
@@ -14,7 +14,6 @@ export const CPH_HUB_MCP_TOOL_IDS = [
|
|||||||
"feishu_read_context",
|
"feishu_read_context",
|
||||||
"feishu_download_resource",
|
"feishu_download_resource",
|
||||||
"request_approval",
|
"request_approval",
|
||||||
"convert_pdf_to_md",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type CphHubMcpToolId = (typeof CPH_HUB_MCP_TOOL_IDS)[number];
|
export type CphHubMcpToolId = (typeof CPH_HUB_MCP_TOOL_IDS)[number];
|
||||||
@@ -51,12 +50,10 @@ const ROLE_TOOL_TO_CPH_HUB_MCP_TOOL = new Map<string, CphHubMcpToolId>([
|
|||||||
["feishu_read_context", "feishu_read_context"],
|
["feishu_read_context", "feishu_read_context"],
|
||||||
["feishu_download_resource", "feishu_download_resource"],
|
["feishu_download_resource", "feishu_download_resource"],
|
||||||
["request_approval", "request_approval"],
|
["request_approval", "request_approval"],
|
||||||
["convert_pdf_to_md", "convert_pdf_to_md"],
|
|
||||||
["mcp__cph_hub__send_file", "send_file"],
|
["mcp__cph_hub__send_file", "send_file"],
|
||||||
["mcp__cph_hub__feishu_read_context", "feishu_read_context"],
|
["mcp__cph_hub__feishu_read_context", "feishu_read_context"],
|
||||||
["mcp__cph_hub__feishu_download_resource", "feishu_download_resource"],
|
["mcp__cph_hub__feishu_download_resource", "feishu_download_resource"],
|
||||||
["mcp__cph_hub__request_approval", "request_approval"],
|
["mcp__cph_hub__request_approval", "request_approval"],
|
||||||
["mcp__cph_hub__convert_pdf_to_md", "convert_pdf_to_md"],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SUPPORTED_ROLE_TOOLS = new Set([
|
const SUPPORTED_ROLE_TOOLS = new Set([
|
||||||
|
|||||||
@@ -7,16 +7,11 @@ import { readFeishuContext } from "./read.js";
|
|||||||
import type { ApprovalManager } from "./approval.js";
|
import type { ApprovalManager } from "./approval.js";
|
||||||
import { CPH_HUB_MCP_TOOL_IDS, type CphHubMcpToolId } from "../agent/roleTools.js";
|
import { CPH_HUB_MCP_TOOL_IDS, type CphHubMcpToolId } from "../agent/roleTools.js";
|
||||||
import { WorkspaceFileBoundaryError } from "../security/workspaceFiles.js";
|
import { WorkspaceFileBoundaryError } from "../security/workspaceFiles.js";
|
||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
import type { LocalSecretEnvelope } from "../security/secretEnvelope.js";
|
|
||||||
import { createPdfToMdBundleAdapter } from "../capability/pdfToMdBundle.js";
|
|
||||||
import { AliyunDocmindClient } from "../capability/docmindClient.js";
|
|
||||||
|
|
||||||
export interface FileDeliveryToolOptions {
|
export interface FileDeliveryToolOptions {
|
||||||
readonly rt: FeishuRuntime;
|
readonly rt: FeishuRuntime;
|
||||||
readonly chatId: string;
|
readonly chatId: string;
|
||||||
readonly projectId: string;
|
readonly projectId: string;
|
||||||
readonly organizationId: string;
|
|
||||||
readonly runId: string;
|
readonly runId: string;
|
||||||
readonly workspaceRoot?: string | undefined;
|
readonly workspaceRoot?: string | undefined;
|
||||||
readonly workspaceDir: string;
|
readonly workspaceDir: string;
|
||||||
@@ -25,8 +20,6 @@ export interface FileDeliveryToolOptions {
|
|||||||
readonly approvalManager: ApprovalManager;
|
readonly approvalManager: ApprovalManager;
|
||||||
readonly onDelivered?: (path: string) => void;
|
readonly onDelivered?: (path: string) => void;
|
||||||
readonly tools?: readonly CphHubMcpToolId[] | undefined;
|
readonly tools?: readonly CphHubMcpToolId[] | undefined;
|
||||||
readonly prisma: PrismaClient;
|
|
||||||
readonly secretEnvelope: LocalSecretEnvelope;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): McpSdkServerConfigWithInstance {
|
export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): McpSdkServerConfigWithInstance {
|
||||||
@@ -237,51 +230,6 @@ export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): M
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enabledTools.has("convert_pdf_to_md")) {
|
|
||||||
const adapter = createPdfToMdBundleAdapter({
|
|
||||||
secrets: options.secretEnvelope,
|
|
||||||
client: new AliyunDocmindClient(),
|
|
||||||
prisma: options.prisma,
|
|
||||||
});
|
|
||||||
tools.push(
|
|
||||||
tool(
|
|
||||||
"convert_pdf_to_md",
|
|
||||||
"Convert a PDF file in the workspace to a Markdown bundle (markdown + extracted images) using Alibaba Cloud Document Mind. The PDF must already be in the workspace (use feishu_download_resource first if it came from Feishu). Returns the path to the generated markdown file and the list of extracted image paths. Mathematical formulas are converted to LaTeX.",
|
|
||||||
{
|
|
||||||
input_path: z.string().describe("Relative path to the input PDF within the workspace."),
|
|
||||||
output_dir: z.string().describe("Relative directory within the workspace to write the markdown and images into. Will be created if it does not exist."),
|
|
||||||
},
|
|
||||||
async (args) => {
|
|
||||||
try {
|
|
||||||
const result = await adapter.invoke({
|
|
||||||
runId: options.runId,
|
|
||||||
organizationId: options.organizationId,
|
|
||||||
projectId: options.projectId,
|
|
||||||
workspaceDir: options.workspaceDir,
|
|
||||||
inputPath: args.input_path,
|
|
||||||
outputDir: args.output_dir,
|
|
||||||
prisma: options.prisma,
|
|
||||||
});
|
|
||||||
const lines = [`Converted PDF to markdown. ${result.artifacts.length} files written:`];
|
|
||||||
for (const artifact of result.artifacts) {
|
|
||||||
lines.push(` - ${artifact.path} (${artifact.kind})`);
|
|
||||||
}
|
|
||||||
lines.push(`Pages: ${result.consumption.quantity}, Cost: $${(result.consumption.costUsd ?? 0).toFixed(4)}`);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: lines.join("\n") }],
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ alwaysLoad: true },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const instructions = mcpInstructions(enabledTools);
|
const instructions = mcpInstructions(enabledTools);
|
||||||
return createSdkMcpServer({
|
return createSdkMcpServer({
|
||||||
name: "cph_hub",
|
name: "cph_hub",
|
||||||
@@ -312,12 +260,5 @@ function mcpInstructions(enabledTools: ReadonlySet<CphHubMcpToolId>): string {
|
|||||||
if (enabledTools.has("request_approval")) {
|
if (enabledTools.has("request_approval")) {
|
||||||
instructions.push("Use request_approval when explicit human approval or confirmation is required before continuing.");
|
instructions.push("Use request_approval when explicit human approval or confirmation is required before continuing.");
|
||||||
}
|
}
|
||||||
if (enabledTools.has("convert_pdf_to_md")) {
|
|
||||||
instructions.push(
|
|
||||||
"Use convert_pdf_to_md when the user asks to convert a PDF to Markdown.",
|
|
||||||
"If the PDF came from a Feishu message, first use feishu_download_resource to save it to the workspace, then call convert_pdf_to_md.",
|
|
||||||
"Do NOT attempt to parse PDFs yourself with Read or Bash — always use convert_pdf_to_md for accurate text, formula, and image extraction.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return instructions.join(" ");
|
return instructions.join(" ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { join } from "node:path";
|
|||||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { FastifyBaseLogger } from "fastify";
|
import type { FastifyBaseLogger } from "fastify";
|
||||||
import type { LocalSecretEnvelope } from "../security/secretEnvelope.js";
|
|
||||||
import {
|
import {
|
||||||
sendText,
|
sendText,
|
||||||
sendTextMessage,
|
sendTextMessage,
|
||||||
@@ -94,7 +93,6 @@ interface TriggerDeps {
|
|||||||
readonly prisma: PrismaClient;
|
readonly prisma: PrismaClient;
|
||||||
readonly settings: RuntimeSettings;
|
readonly settings: RuntimeSettings;
|
||||||
readonly logger: FastifyBaseLogger;
|
readonly logger: FastifyBaseLogger;
|
||||||
readonly secretEnvelope: LocalSecretEnvelope;
|
|
||||||
readonly runAgent?: (req: RunRequest) => Promise<RunResult>;
|
readonly runAgent?: (req: RunRequest) => Promise<RunResult>;
|
||||||
readonly authorizer?: PermissionAuthorizer | undefined;
|
readonly authorizer?: PermissionAuthorizer | undefined;
|
||||||
readonly messageBatcherOptions?: MessageBatcherOptions | undefined;
|
readonly messageBatcherOptions?: MessageBatcherOptions | undefined;
|
||||||
@@ -488,7 +486,6 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
// Streaming agent card: single interactive card through the full run
|
// Streaming agent card: single interactive card through the full run
|
||||||
// lifecycle (thinking → tool calls → streaming text → complete).
|
// lifecycle (thinking → tool calls → streaming text → complete).
|
||||||
// Shows tool-use trace panel + reasoning panel + answer text.
|
// Shows tool-use trace panel + reasoning panel + answer text.
|
||||||
const deliveredFiles: string[] = [];
|
|
||||||
const card = new StreamingAgentCard({
|
const card = new StreamingAgentCard({
|
||||||
runId: run.id,
|
runId: run.id,
|
||||||
rt,
|
rt,
|
||||||
@@ -497,11 +494,11 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
patchIntervalMs: undefined,
|
patchIntervalMs: undefined,
|
||||||
maxMessageLength: undefined,
|
maxMessageLength: undefined,
|
||||||
});
|
});
|
||||||
|
const deliveredFiles: string[] = [];
|
||||||
const fileDeliveryMcpServer = createFileDeliveryMcpServer({
|
const fileDeliveryMcpServer = createFileDeliveryMcpServer({
|
||||||
rt,
|
rt,
|
||||||
chatId,
|
chatId,
|
||||||
projectId,
|
projectId,
|
||||||
organizationId: siloOrganizationId,
|
|
||||||
runId: run.id,
|
runId: run.id,
|
||||||
workspaceRoot: projectWorkspaceRoot,
|
workspaceRoot: projectWorkspaceRoot,
|
||||||
workspaceDir: project.workspaceDir,
|
workspaceDir: project.workspaceDir,
|
||||||
@@ -509,8 +506,6 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
sendOptions,
|
sendOptions,
|
||||||
approvalManager,
|
approvalManager,
|
||||||
tools: cphHubMcpToolsForRole(roleTools),
|
tools: cphHubMcpToolsForRole(roleTools),
|
||||||
prisma: deps.prisma,
|
|
||||||
secretEnvelope: deps.secretEnvelope,
|
|
||||||
onDelivered: (path) => {
|
onDelivered: (path) => {
|
||||||
deliveredFiles.push(path);
|
deliveredFiles.push(path);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ export async function startHub(): Promise<void> {
|
|||||||
prisma,
|
prisma,
|
||||||
settings: runtimeSettings,
|
settings: runtimeSettings,
|
||||||
logger: app.log,
|
logger: app.log,
|
||||||
secretEnvelope,
|
|
||||||
projectWorkspaceRoot,
|
projectWorkspaceRoot,
|
||||||
publicBaseUrl,
|
publicBaseUrl,
|
||||||
siloOrganizationId: siloOrganization.id,
|
siloOrganizationId: siloOrganization.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user