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:
@@ -480,7 +480,18 @@ export const api = {
|
||||
rotateCapabilityConnection: (
|
||||
slug: string,
|
||||
capabilityId: string,
|
||||
body: { accessKeyId: string; accessKeySecret: string; endpoint: string },
|
||||
body:
|
||||
| { kind?: 'docmind'; accessKeyId: string; accessKeySecret: string; endpoint: string }
|
||||
| {
|
||||
kind: 'pbank';
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
password: string;
|
||||
rightsStatus?: string;
|
||||
rightsHolder?: string;
|
||||
rightsScope?: string;
|
||||
rightsNote?: string;
|
||||
},
|
||||
) =>
|
||||
put(`${orgBase(slug)}/capability-connections/${encodeURIComponent(capabilityId)}`, body) as Promise<CapabilityConnection>,
|
||||
disableCapabilityConnection: (slug: string, capabilityId: string) =>
|
||||
|
||||
@@ -18,6 +18,8 @@ export const TOOL_OPTIONS: ToolOption[] = [
|
||||
{ id: 'feishu_read_context', label: '读飞书上下文', group: '飞书' },
|
||||
{ id: 'feishu_download_resource', label: '下载飞书资源', group: '飞书' },
|
||||
{ id: 'request_approval', label: '请求审批', group: '飞书' },
|
||||
{ id: 'convert_pdf_to_md', label: 'PDF→Markdown', group: '能力' },
|
||||
{ id: 'pbank', label: '题库 (PBank)', group: '能力' },
|
||||
];
|
||||
|
||||
/** 组织成员角色(接口枚举保持英文,界面用 orgRoleLabel) */
|
||||
|
||||
@@ -13,9 +13,26 @@
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
type CapKind = 'docmind' | 'pbank';
|
||||
const KNOWN_CAPABILITIES = [
|
||||
{ id: 'pdf_to_md_bundle', label: 'PDF → Markdown', description: '将 PDF 转换为带图片的 Markdown bundle(阿里云文档智能,含公式 LaTeX 识别)' },
|
||||
{ id: 'audio_video_to_text', label: '音视频 → 文本', description: '将音频/视频转写为文本(阿里云文档智能,按秒计费)' },
|
||||
{
|
||||
id: 'pdf_to_md_bundle',
|
||||
kind: 'docmind' as const,
|
||||
label: 'PDF → Markdown',
|
||||
description: '将 PDF 转换为带图片的 Markdown bundle(阿里云文档智能,含公式 LaTeX 识别)'
|
||||
},
|
||||
{
|
||||
id: 'audio_video_to_text',
|
||||
kind: 'docmind' as const,
|
||||
label: '音视频 → 文本',
|
||||
description: '将音频/视频转写为文本(阿里云文档智能,按秒计费)'
|
||||
},
|
||||
{
|
||||
id: 'pbank',
|
||||
kind: 'pbank' as const,
|
||||
label: '题库 (PBank)',
|
||||
description: '搜索/拉取 Paradigm 题库题目与源工程;Agent 通过 pbank_* 工具访问,凭据不下发到 Agent 进程'
|
||||
}
|
||||
] as const;
|
||||
|
||||
let connections = $state<Map<string, CapabilityConnection>>(new Map());
|
||||
@@ -23,9 +40,17 @@
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let editingCap = $state<string | null>(null);
|
||||
let editingKind = $state<CapKind>('docmind');
|
||||
let accessKeyId = $state('');
|
||||
let accessKeySecret = $state('');
|
||||
let endpoint = $state('docmind-api.cn-hangzhou.aliyuncs.com');
|
||||
let baseUrl = $state('https://pbank.paradigm-edu.net/api');
|
||||
let username = $state('');
|
||||
let password = $state('');
|
||||
let rightsStatus = $state('owned');
|
||||
let rightsHolder = $state('Paradigm Education');
|
||||
let rightsScope = $state('internal teaching-material production');
|
||||
let rightsNote = $state('');
|
||||
let saving = $state(false);
|
||||
let disabling = $state<string | null>(null);
|
||||
|
||||
@@ -42,11 +67,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(capId: string) {
|
||||
function startEdit(capId: string, kind: CapKind) {
|
||||
editingCap = capId;
|
||||
editingKind = kind;
|
||||
accessKeyId = '';
|
||||
accessKeySecret = '';
|
||||
endpoint = 'docmind-api.cn-hangzhou.aliyuncs.com';
|
||||
baseUrl = 'https://pbank.paradigm-edu.net/api';
|
||||
username = '';
|
||||
password = '';
|
||||
rightsStatus = 'owned';
|
||||
rightsHolder = 'Paradigm Education';
|
||||
rightsScope = 'internal teaching-material production';
|
||||
rightsNote = '';
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
@@ -54,17 +87,36 @@
|
||||
}
|
||||
|
||||
async function save(capId: string) {
|
||||
if (accessKeyId.trim() === '' || accessKeySecret.trim() === '' || endpoint.trim() === '') {
|
||||
toastError('AccessKey ID、AccessKey Secret、Endpoint 均为必填');
|
||||
return;
|
||||
}
|
||||
saving = true;
|
||||
try {
|
||||
const result = await api.rotateCapabilityConnection(slug, capId, {
|
||||
accessKeyId: accessKeyId.trim(),
|
||||
accessKeySecret: accessKeySecret.trim(),
|
||||
endpoint: endpoint.trim(),
|
||||
});
|
||||
let result: CapabilityConnection;
|
||||
if (editingKind === 'docmind') {
|
||||
if (accessKeyId.trim() === '' || accessKeySecret.trim() === '' || endpoint.trim() === '') {
|
||||
toastError('AccessKey ID、AccessKey Secret、Endpoint 均为必填');
|
||||
return;
|
||||
}
|
||||
result = await api.rotateCapabilityConnection(slug, capId, {
|
||||
kind: 'docmind',
|
||||
accessKeyId: accessKeyId.trim(),
|
||||
accessKeySecret: accessKeySecret.trim(),
|
||||
endpoint: endpoint.trim()
|
||||
});
|
||||
} else {
|
||||
if (baseUrl.trim() === '' || username.trim() === '' || password.trim() === '') {
|
||||
toastError('Base URL、用户名、密码均为必填');
|
||||
return;
|
||||
}
|
||||
result = await api.rotateCapabilityConnection(slug, capId, {
|
||||
kind: 'pbank',
|
||||
baseUrl: baseUrl.trim(),
|
||||
username: username.trim(),
|
||||
password: password.trim(),
|
||||
...(rightsStatus.trim() !== '' ? { rightsStatus: rightsStatus.trim() } : {}),
|
||||
...(rightsHolder.trim() !== '' ? { rightsHolder: rightsHolder.trim() } : {}),
|
||||
...(rightsScope.trim() !== '' ? { rightsScope: rightsScope.trim() } : {}),
|
||||
...(rightsNote.trim() !== '' ? { rightsNote: rightsNote.trim() } : {})
|
||||
});
|
||||
}
|
||||
connections.set(capId, result);
|
||||
connections = new Map(connections);
|
||||
editingCap = null;
|
||||
@@ -110,7 +162,7 @@
|
||||
|
||||
<PageHeader
|
||||
title="外部能力"
|
||||
description="管理文档/媒体转换服务的组织级凭据(ADR-0027)。凭据按组织隔离、版本化信封存储,缺失或校验失败即 fail-closed。"
|
||||
description="管理文档/媒体转换与题库等外部服务的组织级凭据(ADR-0027)。凭据按组织隔离、版本化信封存储,缺失或校验失败即 fail-closed。Agent 永不接收能力凭据。"
|
||||
/>
|
||||
|
||||
{#if loading}
|
||||
@@ -147,7 +199,7 @@
|
||||
{/if}
|
||||
<button
|
||||
class="saas-btn-primary text-sm"
|
||||
onclick={() => startEdit(cap.id)}
|
||||
onclick={() => startEdit(cap.id, cap.kind)}
|
||||
disabled={editingCap === cap.id}
|
||||
>
|
||||
{conn ? '轮换凭据' : '配置凭据'}
|
||||
@@ -174,23 +226,77 @@
|
||||
|
||||
{#if editingCap === cap.id}
|
||||
<div class="mt-4 border-t border-surface-100 pt-4">
|
||||
<p class="saas-muted mb-3 text-sm">
|
||||
阿里云 RAM 用户的 AccessKey。密钥仅写入新版本,旧版本归档。
|
||||
</p>
|
||||
<div class="grid gap-4">
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="ak-id-{cap.id}">AccessKey ID</Label.Root>
|
||||
<input id="ak-id-{cap.id}" class="saas-input font-mono text-sm" bind:value={accessKeyId} />
|
||||
{#if cap.kind === 'docmind'}
|
||||
<p class="saas-muted mb-3 text-sm">
|
||||
阿里云 RAM 用户的 AccessKey。密钥仅写入新版本,旧版本归档。
|
||||
</p>
|
||||
<div class="grid gap-4">
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="ak-id-{cap.id}">AccessKey ID</Label.Root>
|
||||
<input id="ak-id-{cap.id}" class="saas-input font-mono text-sm" bind:value={accessKeyId} />
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="ak-secret-{cap.id}">AccessKey Secret</Label.Root>
|
||||
<input
|
||||
id="ak-secret-{cap.id}"
|
||||
class="saas-input"
|
||||
type="password"
|
||||
bind:value={accessKeySecret}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="endpoint-{cap.id}">Endpoint</Label.Root>
|
||||
<input id="endpoint-{cap.id}" class="saas-input font-mono text-sm" bind:value={endpoint} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="ak-secret-{cap.id}">AccessKey Secret</Label.Root>
|
||||
<input id="ak-secret-{cap.id}" class="saas-input" type="password" bind:value={accessKeySecret} />
|
||||
{:else}
|
||||
<p class="saas-muted mb-3 text-sm">
|
||||
Paradigm 题库登录凭据。激活前会探测 /login;凭据仅写入信封新版本。
|
||||
</p>
|
||||
<div class="grid gap-4">
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="pbank-base-{cap.id}">API Base URL</Label.Root>
|
||||
<input id="pbank-base-{cap.id}" class="saas-input font-mono text-sm" bind:value={baseUrl} />
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="pbank-user-{cap.id}">用户名</Label.Root>
|
||||
<input id="pbank-user-{cap.id}" class="saas-input font-mono text-sm" bind:value={username} />
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="pbank-pass-{cap.id}">密码</Label.Root>
|
||||
<input id="pbank-pass-{cap.id}" class="saas-input" type="password" bind:value={password} />
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="pbank-rights-status-{cap.id}">权利状态</Label.Root>
|
||||
<input
|
||||
id="pbank-rights-status-{cap.id}"
|
||||
class="saas-input font-mono text-sm"
|
||||
bind:value={rightsStatus}
|
||||
placeholder="owned | exclusive_license | licensed_adapt | unknown"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="pbank-rights-holder-{cap.id}">权利主体</Label.Root>
|
||||
<input
|
||||
id="pbank-rights-holder-{cap.id}"
|
||||
class="saas-input text-sm"
|
||||
bind:value={rightsHolder}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="pbank-rights-scope-{cap.id}">使用范围</Label.Root>
|
||||
<input id="pbank-rights-scope-{cap.id}" class="saas-input text-sm" bind:value={rightsScope} />
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="pbank-rights-note-{cap.id}">权利说明(可选)</Label.Root>
|
||||
<textarea
|
||||
id="pbank-rights-note-{cap.id}"
|
||||
class="saas-input min-h-20 text-sm"
|
||||
bind:value={rightsNote}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label.Root class="saas-label" for="endpoint-{cap.id}">Endpoint</Label.Root>
|
||||
<input id="endpoint-{cap.id}" class="saas-input font-mono text-sm" bind:value={endpoint} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 flex items-center justify-end gap-3">
|
||||
<button class="saas-btn-ghost" onclick={cancelEdit} disabled={saving}>取消</button>
|
||||
<button class="saas-btn-primary" onclick={() => save(cap.id)} disabled={saving}>
|
||||
|
||||
Reference in New Issue
Block a user