139 lines
12 KiB
Python
139 lines
12 KiB
Python
from pathlib import Path
|
|
root=Path(r'C:\kefu')
|
|
p=root/'admin-web/apps/web-antd/src/api/knowledge.ts'
|
|
s=p.read_text(encoding='utf-8').replace(' options: { engine: string; max_messages: number };',''' total_messages: null | number;
|
|
progress_percent: null | number;
|
|
completion_reason: string;
|
|
options: { engine: string; max_messages: number; source_name?: string; incremental?: boolean };
|
|
}
|
|
|
|
export interface KnowledgePreview {
|
|
available_messages: number;
|
|
eligible_text_messages: number;
|
|
conversations: number;
|
|
total_messages: number;
|
|
limited: boolean;
|
|
cutoff_at: string;
|
|
source_name: string;''')
|
|
s += '''
|
|
export const previewKnowledgeJob = (id: number, body: Record<string, unknown>) =>
|
|
requestClient.post<KnowledgePreview>(path('jobs/preview', id), body, { timeout: 60000 });
|
|
'''
|
|
p.write_text(s,encoding='utf-8',newline='\n')
|
|
p=root/'admin-web/apps/web-antd/src/components/archive-account-selector.vue'
|
|
s=p.read_text(encoding='utf-8').replace('defineProps<{ modelValue: number }>()','defineProps<{ modelValue: number; disabled?: boolean }>()')
|
|
s=s.replace('const loading = ref(false);','''const loading = ref(false);
|
|
const dropdownOpen = ref(false);
|
|
defineExpose({ open: () => { dropdownOpen.value = true; } });''')
|
|
s=s.replace('function choose(value: number) {','function choose(value: number) {\n dropdownOpen.value = false;')
|
|
s=s.replace('saved === ALL_ACCOUNTS || accounts.value.some','saved === ALL_ACCOUNTS || (saved === 0 && canViewLegacy.value) || accounts.value.some')
|
|
s=s.replace(' :loading="loading"',''' :loading="loading"
|
|
:disabled="disabled"
|
|
:open="dropdownOpen"
|
|
@dropdown-visible-change="(visible) => { dropdownOpen = visible; }"''')
|
|
p.write_text(s,encoding='utf-8',newline='\n')
|
|
p=root/'admin-web/apps/web-antd/src/views/knowledge/center.vue'
|
|
s=p.read_text(encoding='utf-8').replace('onUnmounted, reactive, ref','onUnmounted, ref')
|
|
s=s.replace('InputNumber, message, Modal, Select, Space, Switch, Table, Tabs, Tag','message, Modal, Progress, Select, Space, Steps, Switch, Table, Tabs, Tag')
|
|
s=s.replace('createKnowledgeJob, ', '')
|
|
s=s.replace("import ArchiveAccountSelector from '#/components/archive-account-selector.vue';", """import ArchiveAccountSelector from '#/components/archive-account-selector.vue';
|
|
import KnowledgeJobWizard from './job-wizard.vue';
|
|
import KnowledgeBatchDialog from './batch-dialog.vue';
|
|
import type { BatchAction } from './batch';
|
|
import { actionProblem } from './batch';""")
|
|
s=s.replace("const createOpen = ref(false);", """const createOpen = ref(false);
|
|
const accountSelector = ref<InstanceType<typeof ArchiveAccountSelector>>();
|
|
let pendingCreate = false;
|
|
const selectedKeys = ref<string[]>([]);
|
|
const batchOpen = ref(false);
|
|
const batchAction = ref<BatchAction>('approve');
|
|
const batchEntries = ref<KnowledgeItem[]>([]);
|
|
const selectedRows = computed(() => items.value.filter((item) => selectedKeys.value.includes(item.id)));
|
|
const rowSelection = computed(() => single.value && (canReview.value || canPublish.value) ? {
|
|
selectedRowKeys: selectedKeys.value,
|
|
onChange: (keys: (number | string)[]) => { selectedKeys.value = keys.map(String); },
|
|
} : undefined);
|
|
const guideStep = computed(() => !single.value ? 0 : (overview.value?.counts.published || 0) > 0 ? 3
|
|
: (overview.value?.counts.draft || 0) + (overview.value?.counts.approved || 0) > 0 ? 2 : 1);
|
|
const guideText = computed(() => !single.value ? '先选择一个客服账号,系统会预览该账号可整理的聊天范围。'
|
|
: !overview.value?.source_accounts.length ? '当前账号尚无聊天归档,请先上传归档或更换账号。'
|
|
: guideStep.value === 1 ? '先预览范围并生成草稿,再逐条核对来源和适用条件。'
|
|
: guideStep.value === 2 ? '草稿补全适用条件后审核,通过后发布;也可以勾选列表中的多条知识集中处理。'
|
|
: '已发布知识可在检索测试中验证;确认结果后开启客服使用知识。');
|
|
function batchAllowed(action: BatchAction) {
|
|
return selectedRows.value.length > 0 && selectedRows.value.every((item) => !actionProblem(item, action));
|
|
}
|
|
function openBatch(action: BatchAction) {
|
|
batchAction.value = action;
|
|
batchEntries.value = selectedRows.value.map((item) => ({ ...item }));
|
|
batchOpen.value = true;
|
|
}
|
|
function batchFinished() { selectedKeys.value = []; void load(); }
|
|
async function startCreate() {
|
|
if (!single.value) { pendingCreate = true; accountSelector.value?.open(); return; }
|
|
if (!overview.value) await load();
|
|
createOpen.value = true;
|
|
}
|
|
function created() { tab.value = 'jobs'; void load(); }
|
|
function listChanged() { selectedKeys.value = []; void load(); }
|
|
function cancelJob(jobId: string) {
|
|
Modal.confirm({ title: '取消此整理任务?', content: '已生成的草稿会保留。取消后不再继续处理;已经开始的模型请求可能仍会计费。持续整理开关需要单独关闭。',
|
|
okText: '取消任务', cancelText: '保留任务', onOk: () => jobAction(jobId, 'cancel') });
|
|
}""")
|
|
start=s.index('const form = reactive(')
|
|
end=s.index('\n',start)
|
|
s=s[:start]+s[end:]
|
|
s=s.replace("failed: '失败'", "failed: '失败', cancelled: '已取消'")
|
|
s=s.replace('function accountChanged() {','async function accountChanged() {')
|
|
s=s.replace(" form.source_account_id = ''; form.staff_confirmed = false; page.value = 1;\n void load();", """ selectedKeys.value = []; batchOpen.value = false; batchEntries.value = []; page.value = 1;
|
|
await load();
|
|
if (pendingCreate && single.value) { pendingCreate = false; createOpen.value = true; }
|
|
else pendingCreate = false;""")
|
|
start=s.index('async function create() {')
|
|
end=s.index('async function jobAction(',start)
|
|
s=s[:start]+s[end:]
|
|
s=s.replace('<ArchiveAccountSelector v-model="accountId" @change="accountChanged" />','<ArchiveAccountSelector ref="accountSelector" v-model="accountId" :disabled="busy || createOpen || batchOpen" @change="accountChanged" />')
|
|
s=s.replace('<p class="mb-4 text-gray-500">把历史聊天整理成可追溯的知识,审核发布后供客服回答时参考。</p>', '''<p class="mb-4 text-gray-500">把历史聊天整理成可追溯的知识,审核发布后供客服回答时参考。</p>
|
|
<div class="mb-5 rounded-lg border p-4">
|
|
<Steps size="small" :current="guideStep" :items="[{ title: '选择账号' }, { title: '预览并整理聊天' }, { title: '审核知识' }, { title: '发布并测试使用' }]" />
|
|
<p class="mb-0 mt-3 text-gray-500">{{ guideText }}</p>
|
|
</div>''')
|
|
s=s.replace(':disabled="!single || !canPublish || busy"', ':disabled="!single || !canPublish || busy || (!overview?.settings?.enabled && !overview?.counts.published)"')
|
|
s=s.replace('''<Button v-if="canWrite" type="primary" :disabled="!single" @click="createOpen = true">从聊天整理知识</Button>''','''<Button v-if="canWrite" type="primary" :disabled="busy || loading" @click="startCreate">{{ single ? '从聊天整理知识' : '选择账号开始整理' }}</Button>''')
|
|
s=s.replace('page = 1; load();', 'page = 1; listChanged();')
|
|
s=s.replace(''' <Table :data-source="items"''',''' <Space v-if="single && (canReview || canPublish)" class="mb-4 flex" wrap>
|
|
<span>已选 {{ selectedKeys.length }} 条(当前页)</span>
|
|
<Button v-if="canReview" :disabled="!selectedKeys.length || busy" @click="openBatch('approve')">批量审核</Button>
|
|
<Button v-if="canPublish" type="primary" :disabled="!batchAllowed('publish') || busy" @click="openBatch('publish')">发布选中</Button>
|
|
<Button v-if="canReview" :disabled="!batchAllowed('reject') || busy" @click="openBatch('reject')">驳回选中</Button>
|
|
<Button v-if="canPublish" danger :disabled="!batchAllowed('disable') || busy" @click="openBatch('disable')">停用选中</Button>
|
|
<Button v-if="selectedKeys.length" type="link" @click="selectedKeys = []">清空选择</Button>
|
|
</Space>
|
|
<Table :row-selection="rowSelection" :data-source="items"''')
|
|
s=s.replace('page = p.current || 1; load();','page = p.current || 1; listChanged();')
|
|
s=s.replace(''' <template #bodyCell="{ column, record }">
|
|
<Button v-if="column.key === 'title'"''',''' <template #emptyText><Empty :description="query || filter ? '没有符合筛选条件的知识,可调整筛选后重试。' : single ? '还没有知识,先从该账号的历史聊天生成待审草稿。' : '选择具体账号,开始整理历史聊天。'"><Button v-if="canWrite && !query && !filter" type="primary" @click="startCreate">{{ single ? '预览并整理聊天' : '选择账号' }}</Button></Empty></template>
|
|
<template #bodyCell="{ column, record }">
|
|
<Button v-if="column.key === 'title'"''')
|
|
s=s.replace("[{ title: '创建时间', dataIndex: 'created_at' },", "[{ title: '客服来源', key: 'source' }, { title: '创建时间', dataIndex: 'created_at' },")
|
|
s=s.replace(''' <template v-if="column.key === 'status'"><Tag''',''' <template v-if="column.key === 'source'">{{ record.options.source_name || '历史任务' }}<div class="text-xs text-gray-500">{{ record.options.engine === 'model' ? '模型整理' : '规则整理' }}{{ record.options.incremental ? ' · 增量任务' : '' }}</div></template>
|
|
<template v-else-if="column.key === 'status'"><Tag''')
|
|
s=s.replace('''<template v-else-if="column.key === 'progress'">已读 {{ record.processed }} / 上限 {{ record.options.max_messages }} 条<br />草稿 {{ record.created_items }} · 重复 {{ record.duplicates }} · 无效消息 {{ record.skipped }}</template>''','''<template v-else-if="column.key === 'progress'">
|
|
<div class="min-w-[240px]">已读 {{ record.processed }}<span v-if="record.total_messages !== null && record.total_messages !== undefined"> / {{ record.total_messages }}</span> 条
|
|
<Progress v-if="record.progress_percent !== null && record.progress_percent !== undefined" :percent="record.progress_percent" size="small" :status="record.status === 'failed' ? 'exception' : record.status === 'running' ? 'active' : undefined" />
|
|
<div>草稿 {{ record.created_items }} · 重复 {{ record.duplicates }} · 无效消息 {{ record.skipped }}</div>
|
|
<div v-if="record.completion_reason" class="text-xs text-gray-500">{{ record.completion_reason === 'message_limit' ? '达到消息上限;末尾不完整问答已丢弃' : '所选范围已读取完成' }}</div>
|
|
<div v-if="record.status === 'completed' && !record.created_items" class="text-xs text-gray-500">未生成新知识:请检查有效文本、完整问答或是否已被其他任务整理。</div>
|
|
</div>
|
|
</template>''')
|
|
s=s.replace(''' <Button v-if="['paused', 'failed'].includes(record.status)" :disabled="busy" @click="jobAction(record.id, 'resume')">继续 / 重试</Button>''',''' <Button v-if="['paused', 'failed'].includes(record.status)" :disabled="busy" @click="jobAction(record.id, 'resume')">继续 / 重试</Button>
|
|
<Button v-if="['queued', 'running', 'paused', 'failed'].includes(record.status)" danger type="link" :disabled="busy" @click="cancelJob(record.id)">取消任务</Button>''')
|
|
start=s.index(' <Modal v-model:open="createOpen"')
|
|
end=s.index(' <Modal v-model:open="editorOpen"',start)
|
|
s=s[:start]+''' <KnowledgeJobWizard v-model:open="createOpen" :account-id="accountId" :sources="overview?.source_accounts || []" @created="created" />
|
|
<KnowledgeBatchDialog v-model:open="batchOpen" :account-id="accountId" :entries="batchEntries" :action="batchAction" @finished="batchFinished" @edit="(id) => { batchOpen = false; inspect(id); }" />
|
|
|
|
'''+s[end:]
|
|
p.write_text(s,encoding='utf-8',newline='\n')
|
|
print('Updated workflow UI and API types.')
|