Files
kefu/deploy/knowledge-v2-ui-edit.py
T
2026-09-21 10:34:06 +08:00

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.')