Compare commits
20 Commits
96b944ac8d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a7fbaa97e6 | |||
| 67dca387bd | |||
| 988d0bf012 | |||
| 64b14dc7d3 | |||
| 63661298dd | |||
| 60b4c3244e | |||
| 728d1c8dd5 | |||
| 1524101652 | |||
| a3245b249a | |||
| c011f2eae8 | |||
| c1cd6bc18d | |||
| da64551c92 | |||
| 756849f99e | |||
| 62dffb782a | |||
| 4a1ce220a6 | |||
| 9d6498bf19 | |||
| 15beea041a | |||
| 1b7f6a5b6b | |||
| e65da6f294 | |||
| 8959456eb7 |
+8
-1
@@ -1,5 +1,11 @@
|
|||||||
# ===== Environment =====
|
# ===== Environment =====
|
||||||
# .env 和 .env.local 已纳入版本管理
|
# .env 和 server/.env 包含密钥,不得纳入版本控制
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
server/.env
|
||||||
|
server/.env.local
|
||||||
|
!.env.example
|
||||||
|
!.server/.env.example
|
||||||
|
|
||||||
# ===== Go =====
|
# ===== Go =====
|
||||||
server/server
|
server/server
|
||||||
@@ -66,3 +72,4 @@ venv/
|
|||||||
|
|
||||||
# ===== AI/IDE =====
|
# ===== AI/IDE =====
|
||||||
.ai-switch/
|
.ai-switch/
|
||||||
|
video/
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
|
# playwright e2e
|
||||||
|
/e2e/.auth
|
||||||
|
/e2e/.report
|
||||||
|
/e2e/.artifacts
|
||||||
|
/playwright-report
|
||||||
|
/test-results
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { test, expect, type Page } from "@playwright/test";
|
||||||
|
import {
|
||||||
|
chatExamples,
|
||||||
|
agentExamples,
|
||||||
|
completionExamples,
|
||||||
|
workflowExamples,
|
||||||
|
} from "./examples";
|
||||||
|
|
||||||
|
/** 断言结果容器中渲染了实质内容(markdown 文本长度足够)。 */
|
||||||
|
async function expectNonEmptyAnswer(locator: ReturnType<Page["locator"]>) {
|
||||||
|
await expect(locator).toBeVisible({ timeout: 90_000 });
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await locator.innerText()).trim().length, { timeout: 90_000 })
|
||||||
|
.toBeGreaterThan(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("对话型 / 智能体型应用(textarea + 发送)", () => {
|
||||||
|
for (const app of [...chatExamples, ...agentExamples]) {
|
||||||
|
test(`${app.name} (${app.slug})`, async ({ page }) => {
|
||||||
|
await page.goto(`/chat/${app.slug}`);
|
||||||
|
|
||||||
|
const textarea = page.getByPlaceholder(/输入消息/);
|
||||||
|
await expect(textarea).toBeVisible({ timeout: 30_000 });
|
||||||
|
await textarea.fill(app.input);
|
||||||
|
|
||||||
|
const busy = app.kind === "agent" ? /思考中/ : /生成中/;
|
||||||
|
await page.getByRole("button", { name: /发送/ }).click();
|
||||||
|
|
||||||
|
// 等待回复结束(发送按钮文本从 busy 恢复为“发送”)。
|
||||||
|
await expect(page.getByRole("button", { name: busy })).toBeVisible({ timeout: 30_000 });
|
||||||
|
await expect(page.getByRole("button", { name: /发送/ })).toBeVisible({ timeout: 90_000 });
|
||||||
|
|
||||||
|
// 最后一条 AI 回复(justify-start 容器)应有非空内容。
|
||||||
|
const lastAiReply = page.locator("div.justify-start").last();
|
||||||
|
await expectNonEmptyAnswer(lastAiReply);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("补全型应用(textarea + 生成)", () => {
|
||||||
|
for (const app of completionExamples) {
|
||||||
|
test(`${app.name} (${app.slug})`, async ({ page }) => {
|
||||||
|
await page.goto(`/chat/${app.slug}`);
|
||||||
|
|
||||||
|
const textarea = page.getByPlaceholder(/请输入|在此输入/).first();
|
||||||
|
await expect(textarea).toBeVisible({ timeout: 30_000 });
|
||||||
|
await textarea.fill(app.input);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: /^生成$/ }).click();
|
||||||
|
|
||||||
|
// 生成中 -> 生成 恢复。
|
||||||
|
await expect(page.getByRole("button", { name: /生成中/ })).toBeVisible({ timeout: 30_000 });
|
||||||
|
await expect(page.getByRole("button", { name: /^生成$/ })).toBeVisible({ timeout: 90_000 });
|
||||||
|
|
||||||
|
// 输出结果卡片(emerald 主题)渲染非空。
|
||||||
|
const output = page.locator(".bg-emerald-50\\/30").last();
|
||||||
|
await expectNonEmptyAnswer(output);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("工作流型应用(多步表单 + 运行)", () => {
|
||||||
|
for (const app of workflowExamples) {
|
||||||
|
test(`${app.name} (${app.slug})`, async ({ page }) => {
|
||||||
|
await page.goto(`/chat/${app.slug}`);
|
||||||
|
|
||||||
|
// 等待第一步表单渲染。
|
||||||
|
await expect(page.getByRole("heading", { name: /步骤 1/ })).toBeVisible({
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < app.steps.length; i++) {
|
||||||
|
const step = app.steps[i];
|
||||||
|
const isLast = i === app.steps.length - 1;
|
||||||
|
|
||||||
|
if (step.type === "select") {
|
||||||
|
await page.getByRole("button", { name: step.value, exact: true }).click();
|
||||||
|
} else {
|
||||||
|
const ta = page.locator("textarea");
|
||||||
|
await expect(ta).toBeVisible();
|
||||||
|
await ta.fill(step.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLast) {
|
||||||
|
await page.getByRole("button", { name: /运行/ }).click();
|
||||||
|
} else {
|
||||||
|
await page.getByRole("button", { name: /下一步/ }).click();
|
||||||
|
// 等待进入下一步。
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: new RegExp(`步骤 ${i + 2}`) }),
|
||||||
|
).toBeVisible({ timeout: 15_000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行后等待“处理结果”出现并渲染非空内容。
|
||||||
|
await expect(page.getByRole("heading", { name: /处理结果/ })).toBeVisible({
|
||||||
|
timeout: 90_000,
|
||||||
|
});
|
||||||
|
const result = page.locator(".bg-emerald-50\\/30").last();
|
||||||
|
await expectNonEmptyAnswer(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { test as setup, expect } from "@playwright/test";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const AUTH_FILE = path.join(__dirname, ".auth", "state.json");
|
||||||
|
|
||||||
|
setup("登录法制日报账号并保存会话", async ({ page }) => {
|
||||||
|
fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
|
||||||
|
|
||||||
|
await page.goto("/login");
|
||||||
|
|
||||||
|
await expect(page.locator("#org")).toBeVisible({ timeout: 30_000 });
|
||||||
|
|
||||||
|
await page.locator("#org").selectOption("a0000000-0000-0000-0000-00000000000a");
|
||||||
|
await page.fill("#email", "fazhiribao@govai.gov.cn");
|
||||||
|
await page.fill("#password", "admin123");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: /登\s*录/ }).click();
|
||||||
|
|
||||||
|
await page.waitForURL(/\/store/, { timeout: 30_000 });
|
||||||
|
|
||||||
|
// HttpOnly Cookie: 登录后 token 存在 cookie 中,通过 storageState 自动捕获
|
||||||
|
await page.context().storageState({ path: AUTH_FILE });
|
||||||
|
});
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* 法制日报 15 个应用的示例输入数据。
|
||||||
|
* 类型与 slug 对应 server/migrations/seed_fazhiribao.sql。
|
||||||
|
* workflow 的 steps 顺序与字段 key 严格对应 app_config.steps。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ChatExample {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
kind: "chatbot" | "agent";
|
||||||
|
input: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompletionExample {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
kind: "completion";
|
||||||
|
input: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStepInput {
|
||||||
|
label: string;
|
||||||
|
type: "text" | "textarea" | "select";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowExample {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
kind: "workflow";
|
||||||
|
steps: WorkflowStepInput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppExample = ChatExample | CompletionExample | WorkflowExample;
|
||||||
|
|
||||||
|
export const chatExamples: ChatExample[] = [
|
||||||
|
{
|
||||||
|
slug: "law-news-summary",
|
||||||
|
name: "法治新闻摘要",
|
||||||
|
kind: "chatbot",
|
||||||
|
input:
|
||||||
|
"请为以下新闻生成摘要:某市中级人民法院今日对一起特大电信网络诈骗案作出一审宣判,主犯王某因诈骗罪、组织领导犯罪集团罪被判处有期徒刑十五年,并处罚金。该犯罪集团两年间骗取全国受害人共计三千余万元。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "case-analysis-law",
|
||||||
|
name: "典型案例解读",
|
||||||
|
kind: "chatbot",
|
||||||
|
input:
|
||||||
|
"请解读这起案例:一名外卖骑手深夜送餐途中,发现路边有人持刀抢劫,遂上前制止并将抢劫者打伤。法院认定骑手的行为构成正当防卫,不负刑事责任。请分析裁判要旨与法律适用。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "law-provision-qa",
|
||||||
|
name: "法规条文查询",
|
||||||
|
kind: "chatbot",
|
||||||
|
input: "《中华人民共和国民法典》关于合同解除的法定情形有哪些?请列出相关条文。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "legal-public-opinion",
|
||||||
|
name: "法治舆情监测",
|
||||||
|
kind: "chatbot",
|
||||||
|
input:
|
||||||
|
"请研判以下舆情:某地一起医患纠纷视频在网络流传,患者家属情绪激动冲击医院,引发网友对医疗纠纷处理机制的广泛讨论,部分声音质疑医院责任,也有观点呼吁理性维权。请分析舆论走向并给出引导建议。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "law-writing-polish",
|
||||||
|
name: "法治写作润色",
|
||||||
|
kind: "chatbot",
|
||||||
|
input:
|
||||||
|
"请润色以下评论片段,使其更专业有说服力:现在网络谣言太多了,很多人随便转发,造成很坏的影响,应该严厉打击,让造谣的人付出代价。",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const completionExamples: CompletionExample[] = [
|
||||||
|
{
|
||||||
|
slug: "case-report-gen",
|
||||||
|
name: "案件报道生成",
|
||||||
|
kind: "completion",
|
||||||
|
input:
|
||||||
|
"案件类型:刑事案件\n案件概要:被告人李某利用职务便利,挪用公司资金二百万元用于个人炒股,案发后已全部退还\n审理法院与时间:某区人民法院,2024年3月\n当事人情况:李某,男,38岁,某公司财务经理\n审理结果与法律依据:以挪用资金罪判处有期徒刑两年,缓刑三年,依据《刑法》第二百七十二条",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "law-propaganda-gen",
|
||||||
|
name: "法治宣传文案",
|
||||||
|
kind: "completion",
|
||||||
|
input:
|
||||||
|
"普法主题:防范养老诈骗\n目标受众:老年人\n宣传形式偏好:社区宣传栏海报 + 短视频\n重点强调内容:识别以高额返利、免费体检为名的常见诈骗套路",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "interview-outline-gen",
|
||||||
|
name: "采访提纲生成",
|
||||||
|
kind: "completion",
|
||||||
|
input:
|
||||||
|
"采访主题:未成年人网络保护立法进展\n受访对象:参与立法调研的法学专家\n采访目的:了解立法重点与难点\n重点关注的问题方向:平台责任、家长监护、个人信息保护",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "law-comment-gen",
|
||||||
|
name: "法治评论撰写",
|
||||||
|
kind: "completion",
|
||||||
|
input:
|
||||||
|
"评论对象:高空抛物入刑后的治理成效\n事件概要:刑法修正案(十一)将高空抛物罪入刑以来,多地宣判典型案例,居民安全意识提升\n立场倾向:肯定立法成效,同时呼吁源头治理\n字数要求:1000字左右",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const workflowExamples: WorkflowExample[] = [
|
||||||
|
{
|
||||||
|
slug: "topic-planning",
|
||||||
|
name: "选题策划助手",
|
||||||
|
kind: "workflow",
|
||||||
|
steps: [
|
||||||
|
{ label: "选题方向", type: "text", value: "未成年人网络保护" },
|
||||||
|
{ label: "选题由头", type: "textarea", value: "近期多起未成年人巨额游戏充值、网络打赏纠纷引发社会关注,相关立法正在推进" },
|
||||||
|
{ label: "目标受众", type: "select", value: "普通公众" },
|
||||||
|
{ label: "报道形式", type: "select", value: "深度报道" },
|
||||||
|
{ label: "特殊要求", type: "textarea", value: "配合普法宣传月,突出平台责任与家长监护双重视角" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "omnichannel-publish",
|
||||||
|
name: "法治意识调查报告",
|
||||||
|
kind: "workflow",
|
||||||
|
steps: [
|
||||||
|
{ label: "调查区域与样本", type: "text", value: "示范市,样本量2000人,覆盖城乡各年龄段" },
|
||||||
|
{ label: "调查维度", type: "textarea", value: "法律知晓度、维权意识、守法行为、司法信任、普法需求" },
|
||||||
|
{ label: "调查数据", type: "textarea", value: "法律知晓度68%,主动维权意愿55%,司法信任度72%,普法需求最高的是劳动权益与消费维权" },
|
||||||
|
{ label: "对比基准", type: "select", value: "与上年数据对比" },
|
||||||
|
{ label: "重点关注", type: "textarea", value: "重点分析青年群体维权意识与老年群体防诈骗意识" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "special-report",
|
||||||
|
name: "专题报道框架",
|
||||||
|
kind: "workflow",
|
||||||
|
steps: [
|
||||||
|
{ label: "专题主题", type: "text", value: "民法典实施三周年观察" },
|
||||||
|
{ label: "专题背景", type: "textarea", value: "民法典施行三年,在婚姻家庭、合同、物权等领域产生广泛影响,多项配套司法解释陆续出台" },
|
||||||
|
{ label: "报道周期", type: "select", value: "一周系列" },
|
||||||
|
{ label: "切入角度", type: "textarea", value: "从典型案例切入,展现民法典如何走进百姓生活" },
|
||||||
|
{ label: "媒介形式", type: "select", value: "图文+视频" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "legal-topic-plan",
|
||||||
|
name: "法律专题策划",
|
||||||
|
kind: "workflow",
|
||||||
|
steps: [
|
||||||
|
{ label: "法律领域", type: "text", value: "个人信息保护法" },
|
||||||
|
{ label: "普法目标", type: "textarea", value: "让公众了解个人信息权益与维权途径,提升企业合规意识" },
|
||||||
|
{ label: "目标人群", type: "select", value: "全体公众" },
|
||||||
|
{ label: "传播渠道", type: "textarea", value: "报纸、客户端、抖音短视频、社区线下活动" },
|
||||||
|
{ label: "策划周期", type: "text", value: "国家宪法日宣传周" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const agentExamples: ChatExample[] = [
|
||||||
|
{
|
||||||
|
slug: "topic-intel-agent",
|
||||||
|
name: "法治选题智能助手",
|
||||||
|
kind: "agent",
|
||||||
|
input: "围绕电信网络诈骗治理,请追踪相关立法动态、分析舆情态势,并推荐3个值得报道的选题角度。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "content-creation-agent",
|
||||||
|
name: "法治内容创作助手",
|
||||||
|
kind: "agent",
|
||||||
|
input: "请围绕高空抛物治理,查找相关法条与典型案例,创作一篇面向社区居民的普法报道,并做润色。",
|
||||||
|
},
|
||||||
|
];
|
||||||
Generated
+67
-32
@@ -29,6 +29,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.0.0",
|
"@eslint/js": "^9.0.0",
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
@@ -1975,6 +1976,22 @@
|
|||||||
"url": "https://opencollective.com/pkgr"
|
"url": "https://opencollective.com/pkgr"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
@@ -5960,6 +5977,20 @@
|
|||||||
"node": ">=14.14"
|
"node": ">=14.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -8915,34 +8946,6 @@
|
|||||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next/node_modules/postcss": {
|
|
||||||
"version": "8.4.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
|
||||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/postcss/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"nanoid": "^3.3.6",
|
|
||||||
"picocolors": "^1.0.0",
|
|
||||||
"source-map-js": "^1.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12 || >=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-domexception": {
|
"node_modules/node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
@@ -9470,6 +9473,38 @@
|
|||||||
"node": ">=16.20.0"
|
"node": ">=16.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -9481,9 +9516,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.14",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -9500,7 +9535,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:report": "playwright show-report e2e/.report"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.4.1",
|
"@base-ui/react": "^1.4.1",
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.0.0",
|
"@eslint/js": "^9.0.0",
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
@@ -42,5 +46,8 @@
|
|||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^8.62.0"
|
"typescript-eslint": "^8.62.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"postcss": ">=8.5.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
const BASE_URL = process.env.E2E_BASE_URL || "http://localhost:3000";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./e2e",
|
||||||
|
outputDir: "./e2e/.artifacts",
|
||||||
|
fullyParallel: false,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: 0,
|
||||||
|
workers: 2,
|
||||||
|
reporter: [["list"], ["html", { outputFolder: "./e2e/.report", open: "never" }]],
|
||||||
|
timeout: 120_000,
|
||||||
|
expect: { timeout: 90_000 },
|
||||||
|
use: {
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
actionTimeout: 30_000,
|
||||||
|
locale: "zh-CN",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "setup",
|
||||||
|
testMatch: /auth\.setup\.ts/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "apps",
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Chrome"],
|
||||||
|
storageState: "./e2e/.auth/state.json",
|
||||||
|
},
|
||||||
|
dependencies: ["setup"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import type { Organization } from "@/stores/auth";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Shield,
|
||||||
|
Building2,
|
||||||
|
Sparkles,
|
||||||
|
BookOpen,
|
||||||
|
FileText,
|
||||||
|
Brain,
|
||||||
|
GraduationCap,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export default function LoginForm({ initialOrgs }: { initialOrgs: Organization[] }) {
|
||||||
|
const [email, setEmail] = useState("fazhiwang@govai.gov.cn");
|
||||||
|
const [password, setPassword] = useState("admin123");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
const [orgs, setOrgs] = useState<Organization[]>(initialOrgs);
|
||||||
|
const [selectedOrg, setSelectedOrg] = useState(initialOrgs[0]?.id ?? "");
|
||||||
|
const { login, switchOrg } = useAuthStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// SSR 完全处理机构数据,移除客户端兜底逻辑避免水合时序问题和移动端 fetch 失败
|
||||||
|
// 如果 SSR 失败(initialOrgs 为空),直接显示空状态或禁用表单
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!email || !password) {
|
||||||
|
setErrorMsg("请输入邮箱和密码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedOrg) {
|
||||||
|
setErrorMsg("请选择所属机构");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setErrorMsg("");
|
||||||
|
try {
|
||||||
|
await login(email, password, selectedOrg);
|
||||||
|
const user = useAuthStore.getState().user;
|
||||||
|
// 平台管理员不绑定机构,登录后保留 super_admin 身份;
|
||||||
|
// 仅机构管理员在所选机构与自身归属不一致时才触发切换
|
||||||
|
if (user && user.role === "admin" && user.org_id !== selectedOrg) {
|
||||||
|
await switchOrg(selectedOrg);
|
||||||
|
}
|
||||||
|
router.push(user?.role === "super_admin" ? "/platform/overview" : "/store");
|
||||||
|
} catch (err) {
|
||||||
|
setErrorMsg(err instanceof Error ? err.message : "登录失败,请检查账号和密码");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{ icon: Sparkles, text: "AI 驱动的智能办公" },
|
||||||
|
{ icon: FileText, text: "一键生成公文与报告" },
|
||||||
|
{ icon: BookOpen, text: "智能知识库问答" },
|
||||||
|
{ icon: Brain, text: "多场景 AI 应用中心" },
|
||||||
|
{ icon: GraduationCap, text: "支持多机构独立部署" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-950 via-blue-900 to-blue-800 px-6 py-12">
|
||||||
|
<div className="flex w-full max-w-[1000px] items-center gap-16 lg:gap-20">
|
||||||
|
{/* 左侧品牌区 - 桌面端显示 */}
|
||||||
|
<div className="hidden lg:flex lg:flex-1 flex-col">
|
||||||
|
<div className="max-w-md">
|
||||||
|
<div className="flex items-center gap-3 mb-8">
|
||||||
|
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10 backdrop-blur-sm border border-white/20">
|
||||||
|
<Shield className="h-8 w-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-white tracking-tight">AI 智能应用平台</h1>
|
||||||
|
<p className="text-blue-200/80 text-sm mt-0.5">提升效能 · 赋能智慧办公</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-blue-100/70 text-lg leading-relaxed mb-10">
|
||||||
|
面向政务与高校场景的一站式 AI
|
||||||
|
应用平台,集成文档生成、智能问答、数据分析等核心能力,助力组织数智化转型。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{features.map(({ icon: Icon, text }) => (
|
||||||
|
<div key={text} className="flex items-center gap-4">
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white/10 backdrop-blur-sm">
|
||||||
|
<Icon className="h-5 w-5 text-blue-200" />
|
||||||
|
</div>
|
||||||
|
<span className="text-blue-100/90 text-base">{text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧登录区 */}
|
||||||
|
<div className="flex w-full lg:w-auto lg:shrink-0 items-center justify-center">
|
||||||
|
<div className="w-full max-w-[460px] rounded-2xl bg-white shadow-2xl border border-white/20 overflow-hidden">
|
||||||
|
{/* 移动端标题 - 仅在小屏显示 */}
|
||||||
|
<div className="lg:hidden bg-gradient-to-r from-blue-900 to-blue-800 px-8 pt-8 pb-6 text-center">
|
||||||
|
<Shield className="h-10 w-10 text-white mx-auto mb-3" />
|
||||||
|
<h1 className="text-xl font-bold text-white">AI 智能应用平台</h1>
|
||||||
|
<p className="text-blue-200/80 text-sm mt-1">提升效能 · 赋能智慧办公</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表单区域 */}
|
||||||
|
<div className="px-8 sm:px-10 py-8 sm:py-10">
|
||||||
|
<h2 className="hidden lg:block text-2xl font-semibold text-gray-900 mb-1">
|
||||||
|
欢迎登录
|
||||||
|
</h2>
|
||||||
|
<p className="hidden lg:block text-sm text-gray-500 mb-8">请选择机构并输入账号密码</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{errorMsg && (
|
||||||
|
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
|
||||||
|
{errorMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="org" className="text-sm font-medium text-gray-700">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Building2 className="h-4 w-4 text-gray-400" />
|
||||||
|
所属机构
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
{orgs.length > 0 ? (
|
||||||
|
<select
|
||||||
|
id="org"
|
||||||
|
value={selectedOrg}
|
||||||
|
onChange={(e) => setSelectedOrg(e.target.value)}
|
||||||
|
className="flex h-11 w-full rounded-xl border border-gray-200 bg-gray-50/50 px-4 py-2 text-sm shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300"
|
||||||
|
>
|
||||||
|
{orgs.map((org) => (
|
||||||
|
<option key={org.id} value={org.id}>
|
||||||
|
{org.short_name || org.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<Input disabled placeholder="正在加载机构列表..." className="h-11 rounded-xl" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="text-sm font-medium text-gray-700">
|
||||||
|
账号
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="your@gov.cn"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
|
className="h-11 rounded-xl border-gray-200 bg-gray-50/50 px-4 focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="text-sm font-medium text-gray-700">
|
||||||
|
密码
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
className="h-11 rounded-xl border-gray-200 bg-gray-50/50 px-4 focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full h-12 text-base font-medium rounded-xl bg-blue-900 hover:bg-blue-800 transition-all duration-200 shadow-lg shadow-blue-900/25 hover:shadow-xl hover:shadow-blue-900/30 mt-2"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "登录中..." : "登 录"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center text-sm text-gray-500">
|
||||||
|
还没有账号?{" "}
|
||||||
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="text-blue-600 font-medium hover:text-blue-700 underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
申请注册
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 text-center text-xs text-gray-400 border-t border-gray-100 pt-5">
|
||||||
|
本系统仅限授权人员使用 · 数据安全等级:机构内部
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,222 +1,28 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import type { Organization } from "@/stores/auth";
|
import type { Organization } from "@/stores/auth";
|
||||||
import api from "@/lib/api";
|
import LoginForm from "./login-form";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Shield,
|
|
||||||
Building2,
|
|
||||||
Sparkles,
|
|
||||||
BookOpen,
|
|
||||||
FileText,
|
|
||||||
Brain,
|
|
||||||
GraduationCap,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
// 登录页改为服务端渲染:SSR 阶段在 web 服务端内网直连 API 取机构列表,
|
||||||
const [email, setEmail] = useState("");
|
// 首屏 HTML 即带机构数据,消除客户端"正在加载机构列表"的闪烁。
|
||||||
const [password, setPassword] = useState("");
|
// no-store 保证机构增删改即时生效,无需重新构建或重启。
|
||||||
const [loading, setLoading] = useState(false);
|
export const dynamic = "force-dynamic";
|
||||||
const [errorMsg, setErrorMsg] = useState("");
|
|
||||||
const [orgs, setOrgs] = useState<Organization[]>([]);
|
|
||||||
const [selectedOrg, setSelectedOrg] = useState("");
|
|
||||||
const { login, switchOrg } = useAuthStore();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
async function getOrganizations(): Promise<Organization[]> {
|
||||||
api
|
// 服务端 SSR 用内网地址,不使用 NEXT_PUBLIC_* 避免泄露到客户端
|
||||||
.get<Organization[]>("/api/v1/organizations")
|
const base = process.env.API_URL || "http://localhost:8080";
|
||||||
.then((data) => {
|
|
||||||
setOrgs(data);
|
|
||||||
if (data.length > 0) setSelectedOrg(data[0].id);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!email || !password) {
|
|
||||||
setErrorMsg("请输入邮箱和密码");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!selectedOrg) {
|
|
||||||
setErrorMsg("请选择所属机构");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setErrorMsg("");
|
|
||||||
try {
|
try {
|
||||||
await login(email, password, selectedOrg);
|
const res = await fetch(`${base}/api/v1/organizations`, {
|
||||||
const user = useAuthStore.getState().user;
|
cache: "no-store",
|
||||||
// 平台管理员不绑定机构,登录后保留 super_admin 身份;
|
headers: { "Content-Type": "application/json" },
|
||||||
// 仅机构管理员在所选机构与自身归属不一致时才触发切换
|
});
|
||||||
if (user && user.role === "admin" && user.org_id !== selectedOrg) {
|
if (!res.ok) return [];
|
||||||
await switchOrg(selectedOrg);
|
const json = (await res.json()) as { code: number; data: Organization[] };
|
||||||
|
return json.code === 0 && Array.isArray(json.data) ? json.data : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
router.push(user?.role === "super_admin" ? "/platform/overview" : "/store");
|
}
|
||||||
} catch (err) {
|
|
||||||
setErrorMsg(err instanceof Error ? err.message : "登录失败,请检查账号和密码");
|
export default async function LoginPage() {
|
||||||
} finally {
|
const orgs = await getOrganizations();
|
||||||
setLoading(false);
|
return <LoginForm initialOrgs={orgs} />;
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const features = [
|
|
||||||
{ icon: Sparkles, text: "AI 驱动的智能办公" },
|
|
||||||
{ icon: FileText, text: "一键生成公文与报告" },
|
|
||||||
{ icon: BookOpen, text: "智能知识库问答" },
|
|
||||||
{ icon: Brain, text: "多场景 AI 应用中心" },
|
|
||||||
{ icon: GraduationCap, text: "支持多机构独立部署" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-950 via-blue-900 to-blue-800 px-6 py-12">
|
|
||||||
<div className="flex w-full max-w-[1000px] items-center gap-16 lg:gap-20">
|
|
||||||
{/* 左侧品牌区 - 桌面端显示 */}
|
|
||||||
<div className="hidden lg:flex lg:flex-1 flex-col">
|
|
||||||
<div className="max-w-md">
|
|
||||||
<div className="flex items-center gap-3 mb-8">
|
|
||||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10 backdrop-blur-sm border border-white/20">
|
|
||||||
<Shield className="h-8 w-8 text-white" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-white tracking-tight">AI 智能应用平台</h1>
|
|
||||||
<p className="text-blue-200/80 text-sm mt-0.5">提升效能 · 赋能智慧办公</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-blue-100/70 text-lg leading-relaxed mb-10">
|
|
||||||
面向政务与高校场景的一站式 AI
|
|
||||||
应用平台,集成文档生成、智能问答、数据分析等核心能力,助力组织数智化转型。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{features.map(({ icon: Icon, text }) => (
|
|
||||||
<div key={text} className="flex items-center gap-4">
|
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white/10 backdrop-blur-sm">
|
|
||||||
<Icon className="h-5 w-5 text-blue-200" />
|
|
||||||
</div>
|
|
||||||
<span className="text-blue-100/90 text-base">{text}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧登录区 */}
|
|
||||||
<div className="flex w-full lg:w-auto lg:shrink-0 items-center justify-center">
|
|
||||||
<div className="w-full max-w-[460px] rounded-2xl bg-white shadow-2xl border border-white/20 overflow-hidden">
|
|
||||||
{/* 移动端标题 - 仅在小屏显示 */}
|
|
||||||
<div className="lg:hidden bg-gradient-to-r from-blue-900 to-blue-800 px-8 pt-8 pb-6 text-center">
|
|
||||||
<Shield className="h-10 w-10 text-white mx-auto mb-3" />
|
|
||||||
<h1 className="text-xl font-bold text-white">AI 智能应用平台</h1>
|
|
||||||
<p className="text-blue-200/80 text-sm mt-1">提升效能 · 赋能智慧办公</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 表单区域 */}
|
|
||||||
<div className="px-8 sm:px-10 py-8 sm:py-10">
|
|
||||||
<h2 className="hidden lg:block text-2xl font-semibold text-gray-900 mb-1">
|
|
||||||
欢迎登录
|
|
||||||
</h2>
|
|
||||||
<p className="hidden lg:block text-sm text-gray-500 mb-8">请选择机构并输入账号密码</p>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
|
||||||
{errorMsg && (
|
|
||||||
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
|
|
||||||
{errorMsg}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="org" className="text-sm font-medium text-gray-700">
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<Building2 className="h-4 w-4 text-gray-400" />
|
|
||||||
所属机构
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
{orgs.length > 0 ? (
|
|
||||||
<select
|
|
||||||
id="org"
|
|
||||||
value={selectedOrg}
|
|
||||||
onChange={(e) => setSelectedOrg(e.target.value)}
|
|
||||||
className="flex h-11 w-full rounded-xl border border-gray-200 bg-gray-50/50 px-4 py-2 text-sm shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300"
|
|
||||||
>
|
|
||||||
{orgs.map((org) => (
|
|
||||||
<option key={org.id} value={org.id}>
|
|
||||||
{org.short_name || org.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<Input disabled placeholder="正在加载机构列表..." className="h-11 rounded-xl" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email" className="text-sm font-medium text-gray-700">
|
|
||||||
账号
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="your@gov.cn"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
autoComplete="email"
|
|
||||||
required
|
|
||||||
className="h-11 rounded-xl border-gray-200 bg-gray-50/50 px-4 focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password" className="text-sm font-medium text-gray-700">
|
|
||||||
密码
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入密码"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
autoComplete="current-password"
|
|
||||||
required
|
|
||||||
className="h-11 rounded-xl border-gray-200 bg-gray-50/50 px-4 focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full h-12 text-base font-medium rounded-xl bg-blue-900 hover:bg-blue-800 transition-all duration-200 shadow-lg shadow-blue-900/25 hover:shadow-xl hover:shadow-blue-900/30 mt-2"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? "登录中..." : "登 录"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center text-sm text-gray-500">
|
|
||||||
还没有账号?{" "}
|
|
||||||
<Link
|
|
||||||
href="/register"
|
|
||||||
className="text-blue-600 font-medium hover:text-blue-700 underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
申请注册
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 text-center text-xs text-gray-400 border-t border-gray-100 pt-5">
|
|
||||||
本系统仅限授权人员使用 · 数据安全等级:机构内部
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -45,10 +45,9 @@ export default function RegisterPage() {
|
|||||||
email: string;
|
email: string;
|
||||||
role: "user" | "super_admin" | "admin" | "creator";
|
role: "user" | "super_admin" | "admin" | "creator";
|
||||||
};
|
};
|
||||||
access_token: string;
|
|
||||||
}>("/api/v1/auth/register", { name, email, password });
|
}>("/api/v1/auth/register", { name, email, password });
|
||||||
|
|
||||||
setAuth(res.user, res.access_token);
|
setAuth(res.user);
|
||||||
toast.success("注册成功");
|
toast.success("注册成功");
|
||||||
router.push("/store");
|
router.push("/store");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ export default function KnowledgePage() {
|
|||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
const res = await fetch(`/api/v1/knowledge/${selectedKB!.id}/documents`, {
|
const res = await fetch(`/api/v1/knowledge/${selectedKB!.id}/documents`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
|
||||||
body: formData,
|
body: formData,
|
||||||
|
credentials: "include",
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error("上传失败");
|
if (!res.ok) throw new Error("上传失败");
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ interface AgentUIProps {
|
|||||||
export default function AgentUI({ app }: AgentUIProps) {
|
export default function AgentUI({ app }: AgentUIProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [conversationId, setConversationId] = useState<string | undefined>();
|
||||||
const [messages, setMessages] = useState<Message[]>(
|
const [messages, setMessages] = useState<Message[]>(
|
||||||
app.welcome_message && !conversationId
|
app.welcome_message && !conversationId
|
||||||
? [{ id: "welcome", role: "assistant", content: app.welcome_message }]
|
? [{ id: "welcome", role: "assistant", content: app.welcome_message }]
|
||||||
@@ -147,7 +148,6 @@ export default function AgentUI({ app }: AgentUIProps) {
|
|||||||
);
|
);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
const [conversationId, setConversationId] = useState<string | undefined>();
|
|
||||||
const [selectMode, setSelectMode] = useState(false);
|
const [selectMode, setSelectMode] = useState(false);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [deleteTarget, setDeleteTarget] = useState<{
|
const [deleteTarget, setDeleteTarget] = useState<{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useState, useEffect, useCallback, memo, useMemo } from "react";
|
import { useState, useEffect, useCallback, memo, useMemo, useRef } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { App, Conversation, Message, Chunk } from "@/lib/types";
|
import type { App, Conversation, Message, Chunk } from "@/lib/types";
|
||||||
@@ -45,10 +45,14 @@ const ChatMessage = memo(function ChatMessage({
|
|||||||
msg,
|
msg,
|
||||||
onCopy,
|
onCopy,
|
||||||
chunks,
|
chunks,
|
||||||
|
suggestions,
|
||||||
|
onSuggestionClick,
|
||||||
}: {
|
}: {
|
||||||
msg: Message;
|
msg: Message;
|
||||||
onCopy: (text: string) => void;
|
onCopy: (text: string) => void;
|
||||||
chunks: Chunk[];
|
chunks: Chunk[];
|
||||||
|
suggestions?: string[];
|
||||||
|
onSuggestionClick?: (text: string) => void;
|
||||||
}) {
|
}) {
|
||||||
if (msg.role === "user") {
|
if (msg.role === "user") {
|
||||||
return (
|
return (
|
||||||
@@ -85,6 +89,20 @@ const ChatMessage = memo(function ChatMessage({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* 对话结束后的提示问题 */}
|
||||||
|
{suggestions && suggestions.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
|
{suggestions.map((suggestion, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => onSuggestionClick?.(suggestion)}
|
||||||
|
className="text-xs px-3 py-1.5 rounded-full bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900 border border-blue-200 dark:border-blue-800 transition-colors"
|
||||||
|
>
|
||||||
|
{suggestion}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -98,14 +116,15 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
|
const [conversationId, setConversationId] = useState<string | undefined>();
|
||||||
const [messages, setMessages] = useState<Message[]>(
|
const [messages, setMessages] = useState<Message[]>(
|
||||||
app.welcome_message && !conversationId
|
app.welcome_message && !conversationId
|
||||||
? [{ id: "welcome", role: "assistant", content: app.welcome_message }]
|
? [{ id: "welcome", role: "assistant", content: app.welcome_message }]
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
const [currentSuggestions, setCurrentSuggestions] = useState<string[]>([]);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
const [conversationId, setConversationId] = useState<string | undefined>();
|
|
||||||
const [selectMode, setSelectMode] = useState(false);
|
const [selectMode, setSelectMode] = useState(false);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [deleteTarget, setDeleteTarget] = useState<{
|
const [deleteTarget, setDeleteTarget] = useState<{
|
||||||
@@ -263,10 +282,12 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
setFileContent(null);
|
setFileContent(null);
|
||||||
setFileName(null);
|
setFileName(null);
|
||||||
setChunks([]);
|
setChunks([]);
|
||||||
|
setCurrentSuggestions([]); // 清空之前的提示问题
|
||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
abortRef.current = controller;
|
abortRef.current = controller;
|
||||||
|
const accumulatedRef = { current: "" };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await streamChat(app.id, fullMessage, conversationId, controller.signal);
|
const res = await streamChat(app.id, fullMessage, conversationId, controller.signal);
|
||||||
@@ -277,6 +298,7 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
|
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
let accumulated = "";
|
let accumulated = "";
|
||||||
|
let conversationIdFromResponse: string | undefined;
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
@@ -289,7 +311,7 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
if (raw === "[DONE]") break;
|
if (raw === "[DONE]") break;
|
||||||
try {
|
try {
|
||||||
const event = JSON.parse(raw);
|
const event = JSON.parse(raw);
|
||||||
if (event.conversation_id) setConversationId(event.conversation_id);
|
if (event.conversation_id) conversationIdFromResponse = event.conversation_id;
|
||||||
// 解析首包的 chunks 映射表
|
// 解析首包的 chunks 映射表
|
||||||
if (event.chunks) {
|
if (event.chunks) {
|
||||||
console.log("[SSE] Received chunks:", event.chunks);
|
console.log("[SSE] Received chunks:", event.chunks);
|
||||||
@@ -297,6 +319,7 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
}
|
}
|
||||||
if (event.answer) {
|
if (event.answer) {
|
||||||
accumulated += event.answer;
|
accumulated += event.answer;
|
||||||
|
accumulatedRef.current += event.answer;
|
||||||
const snap = accumulated;
|
const snap = accumulated;
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
prev.map((m, i) =>
|
prev.map((m, i) =>
|
||||||
@@ -309,6 +332,7 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (conversationIdFromResponse) setConversationId(conversationIdFromResponse);
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["conversations", app.id],
|
queryKey: ["conversations", app.id],
|
||||||
});
|
});
|
||||||
@@ -328,8 +352,64 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
abortRef.current = null;
|
abortRef.current = null;
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
|
// 生成对话结束后的提示问题
|
||||||
|
generateChatSuggestions(messages);
|
||||||
}
|
}
|
||||||
}, [input, isStreaming, app.id, conversationId, queryClient]);
|
}, [input, isStreaming, app.id, conversationId, queryClient, messages]);
|
||||||
|
|
||||||
|
// 生成对话结束后的提示问题(调用 LLM API)
|
||||||
|
const generateChatSuggestions = async (allMessages: Message[]) => {
|
||||||
|
// 过滤掉 welcome 消息,只保留用户和助手的实际对话
|
||||||
|
const realMessages = allMessages.filter((m) => m.id !== "welcome").slice(-10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post<{ data: string[] }>(`/api/v1/apps/${app.id}/suggestions`, {
|
||||||
|
conversation_id: conversationId,
|
||||||
|
messages: realMessages.map((m) => ({ role: m.role, content: m.content })),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && response.data.length > 0) {
|
||||||
|
setCurrentSuggestions(response.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("LLM 生成追问失败,使用规则生成:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 调用失败时,使用规则生成作为降级方案
|
||||||
|
generateFallbackSuggestions(allMessages);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 规则生成追问(降级方案)
|
||||||
|
const generateFallbackSuggestions = (allMessages: Message[]) => {
|
||||||
|
const lastResponse = allMessages[allMessages.length - 1]?.content || "";
|
||||||
|
const suggestions: string[] = [];
|
||||||
|
const lowerResponse = lastResponse.toLowerCase();
|
||||||
|
|
||||||
|
// 基于回复内容生成针对性追问
|
||||||
|
if (lowerResponse.includes("政策") || lowerResponse.includes("法规") || lowerResponse.includes("法律")) {
|
||||||
|
suggestions.push("还有哪些相关政策?", "政策的适用范围是什么?");
|
||||||
|
}
|
||||||
|
if (lowerResponse.includes("流程") || lowerResponse.includes("步骤") || lowerResponse.includes("程序")) {
|
||||||
|
suggestions.push("具体流程是什么?", "需要准备哪些材料?");
|
||||||
|
}
|
||||||
|
if (lowerResponse.includes("条件") || lowerResponse.includes("要求") || lowerResponse.includes("资格")) {
|
||||||
|
suggestions.push("具体需要什么条件?", "不符合条件怎么办?");
|
||||||
|
}
|
||||||
|
if (lowerResponse.includes("费用") || lowerResponse.includes("收费")) {
|
||||||
|
suggestions.push("收费标准是多少?", "有优惠政策吗?");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggestions.length < 4) {
|
||||||
|
const generic = ["能详细说明一下吗?", "有什么需要注意的?", "可以举个例子吗?", "还有其他方案吗?"];
|
||||||
|
for (const q of generic) {
|
||||||
|
if (suggestions.length >= 4) break;
|
||||||
|
if (!suggestions.includes(q)) suggestions.push(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentSuggestions(suggestions.slice(0, 4));
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
@@ -346,6 +426,7 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
setMessages([]);
|
setMessages([]);
|
||||||
setConversationId(undefined);
|
setConversationId(undefined);
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
|
setCurrentSuggestions([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleSelect = useCallback((id: string) => {
|
const toggleSelect = useCallback((id: string) => {
|
||||||
@@ -654,9 +735,22 @@ export default function ChatbotUI({ app }: ChatbotUIProps) {
|
|||||||
|
|
||||||
<div className="flex-1 overflow-y-auto min-h-0 p-4">
|
<div className="flex-1 overflow-y-auto min-h-0 p-4">
|
||||||
<div className="max-w-3xl mx-auto space-y-4">
|
<div className="max-w-3xl mx-auto space-y-4">
|
||||||
{messages.map((msg) => (
|
{messages.map((msg, idx) => {
|
||||||
<ChatMessage key={msg.id} msg={msg} onCopy={copyText} chunks={chunks} />
|
const isLastAssistant = msg.role === "assistant" && idx === messages.length - 1;
|
||||||
))}
|
return (
|
||||||
|
<ChatMessage
|
||||||
|
key={msg.id}
|
||||||
|
msg={msg}
|
||||||
|
onCopy={copyText}
|
||||||
|
chunks={chunks}
|
||||||
|
suggestions={isLastAssistant ? currentSuggestions : undefined}
|
||||||
|
onSuggestionClick={(text) => {
|
||||||
|
setInput(text);
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{messages.length <= 1 && suggestedPrompts.length > 0 && (
|
{messages.length <= 1 && suggestedPrompts.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 mt-4">
|
<div className="flex flex-wrap gap-2 mt-4">
|
||||||
|
|||||||
@@ -24,22 +24,20 @@ function AuthLoader({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, [fetchUser, isPublic]);
|
}, [fetchUser, isPublic]);
|
||||||
|
|
||||||
|
// 鉴权路由:未登录则跳转(useEffect 中执行,避免渲染期间调用 router)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !isAuthenticated && !isPublic) {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, isPublic, router]);
|
||||||
|
|
||||||
// 公开路由:直接渲染,不阻塞
|
// 公开路由:直接渲染,不阻塞
|
||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鉴权路由:未登录则跳转
|
// 加载中或未认证
|
||||||
if (!isLoading && !isAuthenticated) {
|
if (isLoading || !isAuthenticated) {
|
||||||
router.push("/login");
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen items-center justify-center">
|
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center">
|
<div className="flex h-screen items-center justify-center">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||||
|
|||||||
+8
-14
@@ -1,7 +1,9 @@
|
|||||||
|
// 服务端 SSR:使用内网地址直连后端
|
||||||
|
// 客户端:使用空字符串(same-origin,通过 nginx 代理)
|
||||||
const API_BASE =
|
const API_BASE =
|
||||||
typeof window === "undefined"
|
typeof window === "undefined"
|
||||||
? process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"
|
? process.env.API_URL || "http://localhost:8080"
|
||||||
: process.env.NEXT_PUBLIC_API_URL || "";
|
: "";
|
||||||
|
|
||||||
// 流式请求基础URL(运行时判断,不依赖编译时变量):
|
// 流式请求基础URL(运行时判断,不依赖编译时变量):
|
||||||
// - 服务器端(SSR):直连后端
|
// - 服务器端(SSR):直连后端
|
||||||
@@ -9,7 +11,7 @@ const API_BASE =
|
|||||||
// - 浏览器端生产环境:走同域nginx(已配proxy_buffering off)
|
// - 浏览器端生产环境:走同域nginx(已配proxy_buffering off)
|
||||||
function getStreamBase() {
|
function getStreamBase() {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
|
return process.env.API_URL || "http://localhost:8080";
|
||||||
}
|
}
|
||||||
const host = window.location.hostname;
|
const host = window.location.hostname;
|
||||||
if (host === "localhost" || host === "127.0.0.1") {
|
if (host === "localhost" || host === "127.0.0.1") {
|
||||||
@@ -34,11 +36,8 @@ class APIError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 认证:token 通过 HttpOnly Cookie 自动携带,不再使用 localStorage
|
||||||
function getAuthHeaders(): Record<string, string> {
|
function getAuthHeaders(): Record<string, string> {
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
if (token) return { Authorization: `Bearer ${token}` };
|
|
||||||
}
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,12 +78,6 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
if (currentPath !== "/login" && currentPath !== "/register") {
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new APIError(json.code || 40101, json.message || "未登录或登录已过期");
|
throw new APIError(json.code || 40101, json.message || "未登录或登录已过期");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +227,8 @@ export async function listPPTTasks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPPTDownloadURL(taskId: string) {
|
export function getPPTDownloadURL(taskId: string) {
|
||||||
const base = typeof window !== "undefined" ? process.env.NEXT_PUBLIC_API_URL || "" : "";
|
// 客户端使用 same-origin,服务端不应调用此函数
|
||||||
|
const base = typeof window !== "undefined" ? "" : "";
|
||||||
return `${base}/api/v1/ppt/tasks/${taskId}/download`;
|
return `${base}/api/v1/ppt/tasks/${taskId}/download`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface AuthState {
|
|||||||
fetchUser: () => Promise<void>;
|
fetchUser: () => Promise<void>;
|
||||||
login: (email: string, password: string, orgId?: string) => Promise<void>;
|
login: (email: string, password: string, orgId?: string) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
setAuth: (user: User, token: string) => void;
|
setAuth: (user: User) => void;
|
||||||
switchOrg: (orgId: string) => Promise<void>;
|
switchOrg: (orgId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,12 +26,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
if (get()._hasFetched) return;
|
if (get()._hasFetched) return;
|
||||||
set({ _hasFetched: true });
|
set({ _hasFetched: true });
|
||||||
|
|
||||||
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
|
||||||
if (!token) {
|
|
||||||
set({ user: null, isAuthenticated: false, isLoading: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
@@ -39,7 +33,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
const user = await api.get<User>("/api/v1/auth/me", { signal: controller.signal });
|
const user = await api.get<User>("/api/v1/auth/me", { signal: controller.signal });
|
||||||
set({ user, isAuthenticated: true, isLoading: false });
|
set({ user, isAuthenticated: true, isLoading: false });
|
||||||
} catch {
|
} catch {
|
||||||
if (typeof window !== "undefined") localStorage.removeItem("token");
|
|
||||||
set({ user: null, isAuthenticated: false, isLoading: false });
|
set({ user: null, isAuthenticated: false, isLoading: false });
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
@@ -47,19 +40,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
login: async (email: string, password: string, orgId?: string) => {
|
login: async (email: string, password: string, orgId?: string) => {
|
||||||
const data = await api.post<{ user: User; access_token: string }>("/api/v1/auth/login", {
|
const data = await api.post<{ user: User }>("/api/v1/auth/login", {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
org_id: orgId,
|
org_id: orgId,
|
||||||
});
|
});
|
||||||
if (data.access_token) {
|
|
||||||
localStorage.setItem("token", data.access_token);
|
|
||||||
}
|
|
||||||
set({ user: data.user, isAuthenticated: true, isLoading: false });
|
set({ user: data.user, isAuthenticated: true, isLoading: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
setAuth: (user: User, token: string) => {
|
setAuth: (user: User) => {
|
||||||
if (token) localStorage.setItem("token", token);
|
|
||||||
set({ user, isAuthenticated: true, isLoading: false });
|
set({ user, isAuthenticated: true, isLoading: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -69,7 +58,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
localStorage.removeItem("token");
|
|
||||||
set({ user: null, isAuthenticated: false, _hasFetched: false });
|
set({ user: null, isAuthenticated: false, _hasFetched: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,12 +66,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
const data = await api.post<{
|
const data = await api.post<{
|
||||||
message: string;
|
message: string;
|
||||||
org: Organization;
|
org: Organization;
|
||||||
token?: string;
|
|
||||||
user?: User;
|
user?: User;
|
||||||
}>("/api/v1/auth/switch-org", { org_id: orgId });
|
}>("/api/v1/auth/switch-org", { org_id: orgId });
|
||||||
if (data.token) {
|
|
||||||
localStorage.setItem("token", data.token);
|
|
||||||
}
|
|
||||||
if (data.user && data.org) {
|
if (data.user && data.org) {
|
||||||
set({ user: { ...data.user, org_id: orgId, org: data.org }, isAuthenticated: true });
|
set({ user: { ...data.user, org_id: orgId, org: data.org }, isAuthenticated: true });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SERVER="h2agent"
|
SERVER="${SERVER:-govai-root}"
|
||||||
REMOTE_DIR="/opt/govai"
|
REMOTE_DIR="/opt/govai"
|
||||||
DOMAIN="gov.opc8ai.com"
|
DOMAIN="gov.opc8ai.com"
|
||||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
@@ -88,8 +88,9 @@ build_local() {
|
|||||||
|
|
||||||
log "编译 Next.js 前端..."
|
log "编译 Next.js 前端..."
|
||||||
cd "$PROJECT_DIR/apps/web"
|
cd "$PROJECT_DIR/apps/web"
|
||||||
|
rm -rf .next
|
||||||
NEXT_PUBLIC_API_URL="http://localhost:8080" npm run build
|
NEXT_PUBLIC_API_URL="http://localhost:8080" npm run build
|
||||||
log "前端编译完成"
|
log "前端编译完成 (.next: $(du -sh .next | cut -f1))"
|
||||||
cd "$PROJECT_DIR"
|
cd "$PROJECT_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,17 +102,17 @@ upload() {
|
|||||||
log "停止服务以释放文件锁..."
|
log "停止服务以释放文件锁..."
|
||||||
ssh $SERVER "systemctl stop govai-api govai-web 2>/dev/null || true"
|
ssh $SERVER "systemctl stop govai-api govai-web 2>/dev/null || true"
|
||||||
|
|
||||||
log "上传后端二进制..."
|
log "上传后端二进制 (gzip 流)..."
|
||||||
scp "$PROJECT_DIR/dist/server" $SERVER:$REMOTE_DIR/server/server
|
gzip -c "$PROJECT_DIR/dist/server" | ssh $SERVER "gunzip -c > $REMOTE_DIR/server/server.new && mv -f $REMOTE_DIR/server/server.new $REMOTE_DIR/server/server && chmod +x $REMOTE_DIR/server/server"
|
||||||
|
|
||||||
log "上传迁移文件..."
|
log "上传迁移文件..."
|
||||||
rsync -az --delete "$PROJECT_DIR/server/migrations/" $SERVER:$REMOTE_DIR/migrations/
|
rsync -az --delete "$PROJECT_DIR/server/migrations/" $SERVER:$REMOTE_DIR/migrations/
|
||||||
|
|
||||||
log "上传前端构建产物..."
|
log "上传前端构建产物 (tar 压缩流, 排除 dev/cache)..."
|
||||||
rsync -az --delete "$PROJECT_DIR/apps/web/.next/" $SERVER:$REMOTE_DIR/web/.next/
|
tar czf - -C "$PROJECT_DIR/apps/web" \
|
||||||
rsync -az "$PROJECT_DIR/apps/web/public/" $SERVER:$REMOTE_DIR/web/public/ 2>/dev/null || true
|
--exclude='.next/dev' --exclude='.next/cache' \
|
||||||
scp "$PROJECT_DIR/apps/web/package.json" $SERVER:$REMOTE_DIR/web/
|
.next public package.json next.config.ts \
|
||||||
scp "$PROJECT_DIR/apps/web/next.config.ts" $SERVER:$REMOTE_DIR/web/
|
| ssh $SERVER "rm -rf $REMOTE_DIR/web/.next && tar xzf - -C $REMOTE_DIR/web"
|
||||||
|
|
||||||
log "上传完成"
|
log "上传完成"
|
||||||
}
|
}
|
||||||
@@ -298,6 +299,84 @@ REMOTE_SVC
|
|||||||
log "服务配置完成"
|
log "服务配置完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---- 安全加固 ----
|
||||||
|
security_hardening() {
|
||||||
|
step "安全加固"
|
||||||
|
|
||||||
|
ssh $SERVER bash << 'SEC_HARD'
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[1/7] 清理恶意 cron 任务..."
|
||||||
|
# 保留正常 cron,移除 pakchoi 和可疑磁盘清理
|
||||||
|
crontab -l 2>/dev/null | grep -v -E 'pakchoi|/opt/disk/cleanup' | crontab - 2>/dev/null || true
|
||||||
|
echo " Cron 清理完成"
|
||||||
|
|
||||||
|
echo "[2/7] 删除恶意用户..."
|
||||||
|
for user in pakchoi; do
|
||||||
|
id $user 2>/dev/null && userdel -r $user && echo " $user 已删除" || echo " $user 不存在"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[3/7] 收紧 .env 文件权限..."
|
||||||
|
find /opt/govai /root -name '.env' -o -name '.env.*' 2>/dev/null | while read f; do
|
||||||
|
chmod 600 "$f" 2>/dev/null && echo " Fixed: $f"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[4/7] 配置 iptables 防火墙 (仅放行 22,80,443,3000)..."
|
||||||
|
# 只对有 iptables 且不是云安全组管理的服务器生效
|
||||||
|
if command -v iptables &>/dev/null && ! iptables -L INPUT -n | grep -q 'Chain references'; then
|
||||||
|
iptables -F INPUT 2>/dev/null || true
|
||||||
|
iptables -P INPUT DROP 2>/dev/null || true
|
||||||
|
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -A INPUT -p tcp --dport 22 -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -A INPUT -p tcp --dport 80 -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -A INPUT -p tcp --dport 443 -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -A INPUT -p tcp --dport 3000 -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -A INPUT -i lo -j ACCEPT 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# 保存规则到持久化文件
|
||||||
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || \
|
||||||
|
iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
|
||||||
|
echo " 防火墙已收紧"
|
||||||
|
|
||||||
|
echo "[5/7] 配置 SSH authorized_keys 权限..."
|
||||||
|
if [ -f /root/.ssh/authorized_keys ]; then
|
||||||
|
# 只保留已知密钥(包含 govai / freedak / h2deploy 注释的)
|
||||||
|
grep -E '(govai|freedak|h2deploy)' /root/.ssh/authorized_keys > /root/.ssh/authorized_keys.clean 2>/dev/null || true
|
||||||
|
if [ -s /root/.ssh/authorized_keys.clean ]; then
|
||||||
|
mv /root/.ssh/authorized_keys.clean /root/.ssh/authorized_keys
|
||||||
|
chmod 600 /root/.ssh/authorized_keys
|
||||||
|
echo " SSH authorized_keys 已清理 ($(wc -l < /root/.ssh/authorized_keys) 密钥保留)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[6/7] 确保 Redis 认证已启用..."
|
||||||
|
REDIS_CONF="/etc/redis/redis.conf"
|
||||||
|
if [ -f "$REDIS_CONF" ]; then
|
||||||
|
if ! grep -q "^requirepass" "$REDIS_CONF"; then
|
||||||
|
echo "requirepass $(openssl rand -base64 32 | tr -d '/+=' | head -c 32)" >> "$REDIS_CONF"
|
||||||
|
systemctl restart redis 2>/dev/null || true
|
||||||
|
echo " Redis 认证密码已设置"
|
||||||
|
else
|
||||||
|
echo " Redis 已有密码配置"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[7/7] 确保 SSH PermitRootLogin 为 prohibit-password..."
|
||||||
|
SSHD_HARDEN="/etc/ssh/sshd_config.d/99-hardening.conf"
|
||||||
|
if [ -f "$SSHD_HARDEN" ]; then
|
||||||
|
if grep -q "PermitRootLogin no" "$SSHD_HARDEN"; then
|
||||||
|
sed -i 's/PermitRootLogin no/PermitRootLogin prohibit-password/' "$SSHD_HARDEN"
|
||||||
|
systemctl reload sshd
|
||||||
|
echo " PermitRootLogin 已修复"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ 安全加固完成"
|
||||||
|
SEC_HARD
|
||||||
|
log "安全加固完成"
|
||||||
|
}
|
||||||
|
|
||||||
# ---- 启动/重启 ----
|
# ---- 启动/重启 ----
|
||||||
start_services() {
|
start_services() {
|
||||||
step "启动服务"
|
step "启动服务"
|
||||||
@@ -388,6 +467,7 @@ case "$ACTION" in
|
|||||||
migrate_db
|
migrate_db
|
||||||
setup_services
|
setup_services
|
||||||
start_services
|
start_services
|
||||||
|
security_hardening
|
||||||
;;
|
;;
|
||||||
update)
|
update)
|
||||||
echo -e "${BLUE}>>> GovAI 更新部署 -> $DOMAIN${NC}"
|
echo -e "${BLUE}>>> GovAI 更新部署 -> $DOMAIN${NC}"
|
||||||
@@ -397,6 +477,7 @@ case "$ACTION" in
|
|||||||
migrate_db
|
migrate_db
|
||||||
install_web_deps
|
install_web_deps
|
||||||
start_services
|
start_services
|
||||||
|
security_hardening
|
||||||
;;
|
;;
|
||||||
restart)
|
restart)
|
||||||
echo -e "${BLUE}>>> GovAI 重启服务${NC}"
|
echo -e "${BLUE}>>> GovAI 重启服务${NC}"
|
||||||
@@ -413,8 +494,12 @@ case "$ACTION" in
|
|||||||
logs)
|
logs)
|
||||||
remote_logs "$@"
|
remote_logs "$@"
|
||||||
;;
|
;;
|
||||||
|
secure)
|
||||||
|
echo -e "${BLUE}>>> GovAI 安全加固${NC}"
|
||||||
|
security_hardening
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "用法: bash deploy.sh [init|update|restart|migrate|status|logs]"
|
echo "用法: bash deploy.sh [init|update|restart|migrate|status|logs|secure]"
|
||||||
echo ""
|
echo ""
|
||||||
echo " init - 首次部署(安装依赖+初始化数据库+部署应用)"
|
echo " init - 首次部署(安装依赖+初始化数据库+部署应用)"
|
||||||
echo " update - 更新部署(git提交+构建+上传+迁移+重启)"
|
echo " update - 更新部署(git提交+构建+上传+迁移+重启)"
|
||||||
@@ -422,6 +507,7 @@ case "$ACTION" in
|
|||||||
echo " migrate - 仅上传迁移文件并执行数据库迁移"
|
echo " migrate - 仅上传迁移文件并执行数据库迁移"
|
||||||
echo " status - 查看远程服务状态和健康检查"
|
echo " status - 查看远程服务状态和健康检查"
|
||||||
echo " logs - 查看日志 (可选: logs govai-web 100)"
|
echo " logs - 查看日志 (可选: logs govai-web 100)"
|
||||||
|
echo " secure - 仅执行安全加固(cron清理/防火墙/SSH/.env权限)"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,284 @@
|
|||||||
|
# 政智通 — 政务AI智能应用平台
|
||||||
|
|
||||||
|
## 一、项目概述
|
||||||
|
|
||||||
|
**项目名称**:政智通 — 政务AI智能应用平台
|
||||||
|
|
||||||
|
**项目定位**:面向政府部门的AI智能办公平台,以大语言模型为核心引擎,覆盖公文写作、政策解读、政务宣传、数据治理、便民服务等多场景,提升行政效能、赋能智慧政务。
|
||||||
|
|
||||||
|
**用途**:投标 — 入驻法治网平台
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、开发语言与技术栈
|
||||||
|
|
||||||
|
| 层级 | 技术选型 | 版本 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 后端语言 | Go (Golang) | 1.25.0 |
|
||||||
|
| 后端框架 | Chi Router | v5.2.5 |
|
||||||
|
| 前端语言 | TypeScript | - |
|
||||||
|
| 前端框架 | Next.js (App Router) / React | - |
|
||||||
|
| 前端样式 | Tailwind CSS / shadcn/ui | - |
|
||||||
|
| 数据库 | PostgreSQL | 15+ |
|
||||||
|
| 缓存 | Redis | 7+ |
|
||||||
|
| AI 引擎 | 通义千问 (Qwen) / OpenAI 兼容接口 | - |
|
||||||
|
| PPT 生成微服务 | Python | - |
|
||||||
|
| 认证 | JWT (golang-jwt/v5) | - |
|
||||||
|
| 日志 | zerolog | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、源代码规模
|
||||||
|
|
||||||
|
| 模块 | 文件类型 | 文件数 | 代码行数 |
|
||||||
|
|------|----------|--------|----------|
|
||||||
|
| 后端服务 | Go (`.go`) | 52 | 10,528 |
|
||||||
|
| 前端应用 | TSX (`.tsx`) | - | 14,356 |
|
||||||
|
| 前端应用 | TS (`.ts`) | - | 1,496 |
|
||||||
|
| 前端应用 | CSS (`.css`) | - | 140 |
|
||||||
|
| PPT 微服务 | Python (`.py`) | - | 1,367 |
|
||||||
|
| **合计** | | **~143 文件** | **~27,887 行** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、系统架构
|
||||||
|
|
||||||
|
### 4.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 前端 (Next.js) │
|
||||||
|
│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||||
|
│ │ 门户端 │ │ 管理后台 │ │ 平台管理端 │ │
|
||||||
|
│ │(Portal) │ │(Admin) │ │(Platform) │ │
|
||||||
|
│ └────┬────┘ └────┬─────┘ └──────┬───────┘ │
|
||||||
|
│ └───────────┴──────────────┘ │
|
||||||
|
│ REST API │
|
||||||
|
└───────────────────┬─────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────┴─────────────────────────┐
|
||||||
|
│ 后端 (Go / Chi Router) │
|
||||||
|
│ ┌──────┐ ┌───────┐ ┌──────┐ ┌──────────┐ │
|
||||||
|
│ │认证 │ │应用 │ │对话 │ │知识库 │ │
|
||||||
|
│ │中间件 │ │商店 │ │引擎 │ │管理 │ │
|
||||||
|
│ └──────┘ └───────┘ └──────┘ └──────────┘ │
|
||||||
|
│ ┌──────┐ ┌───────┐ ┌──────┐ ┌──────────┐ │
|
||||||
|
│ │PPT │ │文档 │ │审计 │ │平台管理 │ │
|
||||||
|
│ │生成 │ │模板 │ │日志 │ │ │ │
|
||||||
|
│ └──────┘ └───────┘ └──────┘ └──────────┘ │
|
||||||
|
└──────┬──────────────────────┬───────────────┘
|
||||||
|
│ │
|
||||||
|
┌──────┴──────┐ ┌──────┴──────┐
|
||||||
|
│ PostgreSQL │ │ Redis │
|
||||||
|
│ (持久化) │ │ (缓存) │
|
||||||
|
└─────────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 后端模块结构
|
||||||
|
|
||||||
|
| 目录 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `server/cmd/server/` | 主入口、路由注册 |
|
||||||
|
| `server/internal/handler/` | HTTP 请求处理器(对话、应用商店、知识库、PPT、文件等) |
|
||||||
|
| `server/internal/middleware/` | 中间件(认证、租户隔离、日志) |
|
||||||
|
| `server/internal/config/` | 配置管理 |
|
||||||
|
| `server/internal/response/` | 统一响应格式 |
|
||||||
|
| `server/pkg/auth/` | JWT 认证工具包 |
|
||||||
|
| `server/pkg/db/` | 数据库连接与查询 |
|
||||||
|
| `server/pkg/llm/` | 大语言模型接口封装 |
|
||||||
|
| `server/pkg/embedding/` | 向量嵌入 |
|
||||||
|
| `server/pkg/chunker/` | 文档分块 |
|
||||||
|
| `server/pkg/tenant/` | 多租户隔离 |
|
||||||
|
| `server/migrations/` | 数据库迁移脚本(17 个迁移 + 种子数据) |
|
||||||
|
|
||||||
|
### 4.3 前端模块结构
|
||||||
|
|
||||||
|
| 目录 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `apps/web/src/app/(portal)/` | 门户端:应用商店、对话、知识库、工作台 |
|
||||||
|
| `apps/web/src/app/(admin)/` | 管理后台:用户管理、应用管理、审计、分析、模型配置 |
|
||||||
|
| `apps/web/src/app/(auth)/` | 认证:登录、注册 |
|
||||||
|
| `apps/web/src/app/platform/` | 平台管理:组织管理、供应商、配额、审计 |
|
||||||
|
| `apps/web/src/components/` | 通用组件(UI 基础组件、布局、应用卡片) |
|
||||||
|
| `apps/web/src/hooks/` | 自定义 Hooks |
|
||||||
|
| `apps/web/src/lib/` | 工具函数库 |
|
||||||
|
| `apps/web/src/stores/` | 状态管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、核心功能模块
|
||||||
|
|
||||||
|
### 5.1 政务应用分类
|
||||||
|
|
||||||
|
| 分类 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 公文写作 | 公文拟稿、会议纪要、文件摘要 |
|
||||||
|
| 政策解读 | 法规问答、政策影响分析 |
|
||||||
|
| 政务宣传 | 宣传稿件、信息发布 |
|
||||||
|
| 数据治理 | 数据分析、综合研判 |
|
||||||
|
| 便民服务 | 群众来信回复、咨询答复 |
|
||||||
|
| 信息化工具 | 开发辅助、系统运维 |
|
||||||
|
| 组织人事 | 干部考核、人事管理 |
|
||||||
|
| 招商引资 | 项目评估、投资分析 |
|
||||||
|
| 翻译外事 | 中英互译、外事用语 |
|
||||||
|
| 综合应用 | 其他政务场景 |
|
||||||
|
|
||||||
|
### 5.2 预置应用
|
||||||
|
|
||||||
|
| 应用名称 | 类型 | 功能说明 |
|
||||||
|
|----------|------|----------|
|
||||||
|
| 政策法规问答 | 对话型 | 法规条款查询与解读 |
|
||||||
|
| 公文写作助手 | 对话型 | 各类公文拟稿 |
|
||||||
|
| 群众来信回复 | 对话型 | 群众诉求回复建议 |
|
||||||
|
| 会议纪要生成 | 补全型 | 会议记录整理 |
|
||||||
|
| 公文摘要提取 | 补全型 | 文件要点提取 |
|
||||||
|
| 翻译助手 | 补全型 | 政务中英互译 |
|
||||||
|
| 招商项目评估 | 工作流 | 多维度项目评估 |
|
||||||
|
| 政策影响分析 | 工作流 | 政策多维度影响评估 |
|
||||||
|
| 综合研判助手 | 智能体 | 数据分析与报告生成 |
|
||||||
|
| 干部考核助手 | 智能体 | 绩效分析与评语生成 |
|
||||||
|
| 智能 PPT 生成 | PPT 生成 | 上传文档/输入主题,AI 生成原生可编辑 PPTX |
|
||||||
|
|
||||||
|
### 5.3 系统功能清单
|
||||||
|
|
||||||
|
| 功能域 | 功能项 |
|
||||||
|
|--------|--------|
|
||||||
|
| 用户与权限 | 注册/登录、JWT 认证、角色管理、多租户隔离 |
|
||||||
|
| 应用管理 | 应用商店、分类浏览、应用创建、收藏评分 |
|
||||||
|
| AI 对话 | 多轮对话、流式输出、上下文管理、对话历史 |
|
||||||
|
| 知识库 | 文档上传、文档分块、向量嵌入、知识检索 |
|
||||||
|
| PPT 生成 | 多种输入源、多种风格、AI 生图、可编辑 PPTX 导出 |
|
||||||
|
| 文档模板 | 模板管理、模板应用 |
|
||||||
|
| 平台管理 | 组织管理、模型供应商配置、配额管理 |
|
||||||
|
| 审计与安全 | 操作审计日志、安全审计、用量统计 |
|
||||||
|
| 数据分析 | 使用分析、模型调用统计、用户行为分析 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、数据库设计
|
||||||
|
|
||||||
|
### 6.1 数据库迁移版本
|
||||||
|
|
||||||
|
共 **17 个迁移版本**,涵盖:
|
||||||
|
|
||||||
|
| 迁移 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 000001 | 初始化 schema |
|
||||||
|
| 000002 | 分类与应用 |
|
||||||
|
| 000003 | 评价、收藏、评分 |
|
||||||
|
| 000004 | 用量日志 |
|
||||||
|
| 000005 | 模型、配额、审计 |
|
||||||
|
| 000006 | PPT 任务 |
|
||||||
|
| 000007 | PPT 生成器类型 |
|
||||||
|
| 000008 | 文档模板 |
|
||||||
|
| 000009 | 对话命名 |
|
||||||
|
| 000010 | 知识库 ID |
|
||||||
|
| 000011 | 文档内容 |
|
||||||
|
| 000012 | 用量日志消息 |
|
||||||
|
| 000013 | 组织机构 |
|
||||||
|
| 000014 | 多租户 |
|
||||||
|
| 000015 | 平台管理 |
|
||||||
|
| 000016 | 对话与消息 |
|
||||||
|
| 000017 | 清理旧对话表 |
|
||||||
|
|
||||||
|
### 6.2 种子数据
|
||||||
|
|
||||||
|
包含法治网、发改委、公安、科技、信访等多领域种子数据,支持快速部署演示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、部署架构
|
||||||
|
|
||||||
|
### 7.1 运行环境
|
||||||
|
|
||||||
|
| 组件 | 要求 |
|
||||||
|
|------|------|
|
||||||
|
| Go | 1.25+ |
|
||||||
|
| Node.js | 18+ |
|
||||||
|
| PostgreSQL | 15+ |
|
||||||
|
| Redis | 7+ |
|
||||||
|
| Python | 3.10+ (PPT Worker) |
|
||||||
|
|
||||||
|
### 7.2 部署方式
|
||||||
|
|
||||||
|
- **Docker 部署**:提供 `docker/` 配置目录,支持容器化部署
|
||||||
|
- **原生部署**:通过 `Makefile` 管理构建与启动
|
||||||
|
- **部署脚本**:`deploy.sh` 支持自动化部署
|
||||||
|
|
||||||
|
### 7.3 服务端口
|
||||||
|
|
||||||
|
| 服务 | 端口 |
|
||||||
|
|------|------|
|
||||||
|
| 后端 API | 8080 |
|
||||||
|
| 前端 Web | 3000 |
|
||||||
|
| PostgreSQL | 5432 |
|
||||||
|
| Redis | 6379 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、安全设计
|
||||||
|
|
||||||
|
| 安全措施 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| JWT 认证 | 短期 Token + 刷新机制 |
|
||||||
|
| 多租户隔离 | 基于 `session.tenant_id` 的数据隔离 |
|
||||||
|
| 密钥管理 | 环境变量管理,不进 Git |
|
||||||
|
| SQL 注入防御 | 参数化查询 |
|
||||||
|
| XSS 防御 | 自动转义 + CSP |
|
||||||
|
| CSRF 防御 | SameSite Cookie |
|
||||||
|
| 操作审计 | 审计日志记录,保留 ≥ 6 个月 |
|
||||||
|
| 限流熔断 | 登录限流、接口限流 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、项目目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
GovAI/
|
||||||
|
├── server/ # Go 后端服务
|
||||||
|
│ ├── cmd/ # 命令入口
|
||||||
|
│ │ ├── server/ # 主服务入口与路由
|
||||||
|
│ │ ├── check-providers/ # 供应商检查工具
|
||||||
|
│ │ ├── seed-providers/ # 供应商种子工具
|
||||||
|
│ │ ├── embed-chunks/ # 向量嵌入工具
|
||||||
|
│ │ └── ... # 其他命令行工具
|
||||||
|
│ ├── internal/ # 内部业务逻辑
|
||||||
|
│ │ ├── handler/ # HTTP 处理器
|
||||||
|
│ │ ├── middleware/ # 中间件
|
||||||
|
│ │ ├── config/ # 配置
|
||||||
|
│ │ ├── response/ # 统一响应
|
||||||
|
│ │ ├── logger/ # 日志
|
||||||
|
│ │ └── metrics/ # 指标
|
||||||
|
│ ├── pkg/ # 公共工具包
|
||||||
|
│ │ ├── auth/ # 认证
|
||||||
|
│ │ ├── db/ # 数据库
|
||||||
|
│ │ ├── llm/ # 大模型接口
|
||||||
|
│ │ ├── embedding/ # 向量嵌入
|
||||||
|
│ │ ├── chunker/ # 文档分块
|
||||||
|
│ │ ├── tenant/ # 多租户
|
||||||
|
│ │ └── dify/ # Dify 集成
|
||||||
|
│ └── migrations/ # 数据库迁移与种子数据
|
||||||
|
├── apps/web/ # Next.js 前端
|
||||||
|
│ └── src/
|
||||||
|
│ ├── app/ # 页面路由
|
||||||
|
│ │ ├── (portal)/ # 门户端
|
||||||
|
│ │ ├── (admin)/ # 管理后台
|
||||||
|
│ │ ├── (auth)/ # 认证页面
|
||||||
|
│ │ └── platform/ # 平台管理端
|
||||||
|
│ ├── components/ # 通用组件
|
||||||
|
│ ├── hooks/ # 自定义 Hooks
|
||||||
|
│ ├── lib/ # 工具函数
|
||||||
|
│ └── stores/ # 状态管理
|
||||||
|
├── ppt-worker/ # PPT 生成微服务 (Python)
|
||||||
|
├── docker/ # Docker 配置
|
||||||
|
├── docs/ # 项目文档
|
||||||
|
├── Makefile # 构建管理
|
||||||
|
├── deploy.sh # 部署脚本
|
||||||
|
└── README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、总结
|
||||||
|
|
||||||
|
政智通平台是一个基于 Go + Next.js 的前后端分离架构的政务AI智能应用平台,总代码量约 **2.8 万行**,涵盖 52 个 Go 源文件和 91 个前端 TypeScript/TSX 文件。平台集成了大语言模型能力,提供公文写作、政策解读、智能PPT生成等 11 个预置应用,支持多租户隔离、知识库管理、审计安全等企业级功能,满足政府部门智慧办公的需求。
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# GovAI 硬件部署要求
|
||||||
|
|
||||||
|
本文档定义 GovAI(政智通)政务AI智能应用平台的硬件部署要求,适用于不同规模的部署场景。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统组件概览
|
||||||
|
|
||||||
|
| 组件 | 技术栈 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 后端服务 | Go 1.25+ | REST API,处理业务逻辑 |
|
||||||
|
| 前端 | Next.js 16 + React 19 | 用户界面(SSR/SSG) |
|
||||||
|
| PPT Worker | Python + FastAPI | 异步 PPT 生成微服务 |
|
||||||
|
| 数据库 | PostgreSQL 17 (pgvector) | 关系数据 + 向量检索 |
|
||||||
|
| 缓存 | Redis 7 | 会话、队列、限流 |
|
||||||
|
| 对象存储 | MinIO | 文档、图片等文件存储 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景一:开发 / 最小化部署
|
||||||
|
|
||||||
|
适用:本地开发调试、个人使用、Demo 演示。
|
||||||
|
|
||||||
|
| 资源项 | 最低要求 | 推荐配置 |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| **CPU** | 4 核 | 8 核 |
|
||||||
|
| **内存** | 8 GB | 16 GB |
|
||||||
|
| **磁盘** | 50 GB | 100 GB+ |
|
||||||
|
| **操作系统** | macOS 12+ / Linux / Windows (WSL2) | — |
|
||||||
|
|
||||||
|
所有服务(Go 后端、Next.js 前端、PPT Worker、PostgreSQL、Redis、MinIO)可在单台机器上通过 Docker Compose 一键启动。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景二:单服务器生产部署(50 ~ 100 用户)
|
||||||
|
|
||||||
|
适用:50 ~ 100 人规模的小型单位(如科室、局委办),全部服务集中部署于一台物理服务器或虚拟机。
|
||||||
|
|
||||||
|
### 推荐配置
|
||||||
|
|
||||||
|
| 资源项 | 最低要求 | 推荐配置 |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| **CPU** | 8 核 | 16 核 |
|
||||||
|
| **内存** | 16 GB | 32 GB |
|
||||||
|
| **系统盘** | 100 GB SSD | 200 GB SSD |
|
||||||
|
| **数据盘** | 300 GB SSD | 500 GB SSD |
|
||||||
|
| **操作系统** | Linux (Ubuntu 22.04+ / CentOS 8+) | — |
|
||||||
|
|
||||||
|
### 资源分配方案
|
||||||
|
|
||||||
|
| 组件 | 资源占用 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| Go 后端 | 2 核 + 1 GB 内存 | 2 实例 |
|
||||||
|
| Next.js 前端 | 2 核 + 1 GB 内存 | 1 实例 |
|
||||||
|
| PostgreSQL | 2 核 + 8 GB 内存 | 含 pgvector,SSD 存储 |
|
||||||
|
| Redis | 1 核 + 2 GB 内存 | 会话 + 队列 |
|
||||||
|
| MinIO | 1 核 + 2 GB 内存 | 文档/图片存储(~200 GB) |
|
||||||
|
| PPT Worker | 2~4 核 + 8 GB 内存 | 1 实例,按需生成 PPT |
|
||||||
|
| **系统预留** | 2~4 核 + 4~8 GB | 内核、SSH、Docker 等开销 |
|
||||||
|
|
||||||
|
### 性能预估
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 并发用户 | 50 ~ 100 |
|
||||||
|
| 预估 QPS | 100 ~ 300 req/s |
|
||||||
|
| 同时 PPT 生成 | 1 ~ 3 个 |
|
||||||
|
| 每日活跃用户 | 占总用户 30%~50% |
|
||||||
|
| 月数据增量 | ~1 GB(100 用户场景) |
|
||||||
|
| 存储规划 | 500 GB SSD 可用约 3~5 年 |
|
||||||
|
|
||||||
|
### 部署方式
|
||||||
|
|
||||||
|
推荐使用 Docker Compose 在该服务器上一键启动所有容器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基础设施(PostgreSQL + Redis + MinIO)
|
||||||
|
docker compose -f docker/docker-compose.yml up -d
|
||||||
|
|
||||||
|
# 后端
|
||||||
|
cd server && go run ./cmd/server
|
||||||
|
|
||||||
|
# 前端
|
||||||
|
cd apps/web && npm run dev # 开发模式
|
||||||
|
# 或 npm run build && npm start # 生产模式
|
||||||
|
|
||||||
|
# PPT Worker
|
||||||
|
make dev-ppt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- **CPU 敏感**:PPT Worker 在文档解析和渲染时 CPU 占用较高,如并发使用建议提升至 16 核
|
||||||
|
- **内存峰值**:PPT 生成期间内存可能瞬时冲高到 12 GB+,建议保留足够 swap 或选用 32 GB 配置
|
||||||
|
- **网络**:服务器需访问通义千问/ OpenAI API,建议带宽 20 Mbps+
|
||||||
|
- **备份**:数据盘建议配置 RAID 或定期快照备份
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景三:多服务器生产(50 ~ 200 并发用户)
|
||||||
|
|
||||||
|
适用:部门级或区县级部署,支撑日常政务办公。
|
||||||
|
|
||||||
|
### 服务器规划(3 台)
|
||||||
|
|
||||||
|
| 服务器 | 部署内容 | CPU | 内存 | 系统盘 | 数据盘 |
|
||||||
|
|--------|---------|-----|------|--------|--------|
|
||||||
|
| **Server A** | Go 后端(2~4 实例) + Redis 主 | 8 核 | 16 GB | 100 GB SSD | — |
|
||||||
|
| **Server B** | PostgreSQL 主库 + MinIO 主 | 8 核 | 16 GB | 100 GB SSD | 500 GB SSD |
|
||||||
|
| **Server C** | PPT Worker(2 实例) + 渲染队列 | 8 核 | 16 GB | 100 GB SSD | 200 GB SSD |
|
||||||
|
|
||||||
|
### 组件级配置明细
|
||||||
|
|
||||||
|
#### Go 后端服务
|
||||||
|
|
||||||
|
| 项目 | 配置 |
|
||||||
|
|------|------|
|
||||||
|
| 实例数 | 2 ~ 4(水平扩展) |
|
||||||
|
| 内存单实例 | ~200 MB |
|
||||||
|
| 并发承载 | 单实例约 50 ~ 100 并发(CPU-bound) |
|
||||||
|
| 连接池 | PostgreSQL 最多 25 连接 / 实例 |
|
||||||
|
| 预估总 QPS | 200 ~ 500 req/s |
|
||||||
|
| 建议资源 | 8 核 16 GB 服务器,容器/进程管理 |
|
||||||
|
|
||||||
|
#### Next.js 前端
|
||||||
|
|
||||||
|
| 项目 | 配置 |
|
||||||
|
|------|------|
|
||||||
|
| 部署模式 | `next start`(Node.js 生产模式)或容器化 |
|
||||||
|
| 内存单实例 | ~500 MB ~ 1 GB |
|
||||||
|
| 建议资源 | 独立 4 核 8 GB 服务器(或与后端混部) |
|
||||||
|
| CDN 加速 | 建议接入 CDN 加速静态资源和图片 |
|
||||||
|
|
||||||
|
#### PPT Worker(Python 微服务)
|
||||||
|
|
||||||
|
| 项目 | 配置 |
|
||||||
|
|------|------|
|
||||||
|
| 实例数 | 2 |
|
||||||
|
| 内存单实例 | 峰值可达 4 GB(文档解析、PPT 渲染) |
|
||||||
|
| 并发任务 | 单实例约 5 ~ 10 个任务并行 |
|
||||||
|
| 建议资源 | 8 核 16 GB 服务器(CPU 密集型) |
|
||||||
|
| 临时存储 | 每个任务约 50 ~ 200 MB 临时文件,需定期清理 |
|
||||||
|
|
||||||
|
#### PostgreSQL(pgvector)
|
||||||
|
|
||||||
|
| 项目 | 配置 |
|
||||||
|
|------|------|
|
||||||
|
| 版本 | PostgreSQL 17 + pgvector |
|
||||||
|
| 并发连接 | 最大 100 ~ 150 连接 |
|
||||||
|
| 共享缓冲 | 建议 4 ~ 8 GB(总内存的 25%~50%) |
|
||||||
|
| 建议资源 | 8 核 16 GB 服务器,SSD 存储 |
|
||||||
|
| 数据量预估 | 50 用户每日产生 ~1 GB 数据/月,500 GB SSD 可用 3 年+ |
|
||||||
|
| 高可用 | 可选主从流复制(2 台) |
|
||||||
|
|
||||||
|
#### Redis
|
||||||
|
|
||||||
|
| 项目 | 配置 |
|
||||||
|
|------|------|
|
||||||
|
| 内存 | 2 ~ 4 GB |
|
||||||
|
| 持久化 | 建议开启 RDB + AOF |
|
||||||
|
| 建议资源 | 与后端同机或独立 2 核 4 GB |
|
||||||
|
| 高可用 | 可选 Sentinel(1 主 2 从) |
|
||||||
|
|
||||||
|
#### MinIO(对象存储)
|
||||||
|
|
||||||
|
| 项目 | 配置 |
|
||||||
|
|------|------|
|
||||||
|
| 存储量 | 200 GB ~ 1 TB(视知识库文档量) |
|
||||||
|
| 建议资源 | 与 PostgreSQL 同机或独立 4 核 8 GB |
|
||||||
|
| 冗余 | 建议 2 节点以上,纠删码模式 |
|
||||||
|
|
||||||
|
### 中等规模汇总
|
||||||
|
|
||||||
|
| 类别 | 数量 | 规格 | 用途 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 应用服务器 | 1~2 | 8 核 16 GB | Go 后端 + 前端(可混部) |
|
||||||
|
| 数据库服务器 | 1 | 8 核 16 GB + 500 GB SSD | PostgreSQL + MinIO |
|
||||||
|
| PPT Worker 服务器 | 1 | 8 核 16 GB | PPT 生成 |
|
||||||
|
| **合计** | **3~4 台** | — | — |
|
||||||
|
|
||||||
|
### 网络要求
|
||||||
|
|
||||||
|
- 服务器之间内网带宽:**千兆(1 Gbps)** 推荐
|
||||||
|
- 对外带宽:**50 Mbps ~ 100 Mbps**(视 AI API 调用量)
|
||||||
|
- AI API 延迟:后端到通义千问/OpenAI 的 RTT 建议 < 200 ms
|
||||||
|
- 端口:前端 3000、后端 8080、PPT Worker 8090、PostgreSQL 5432、Redis 6379、MinIO 9000/9001
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景三:大规模 / 高可用分布式(200+ 并发用户)
|
||||||
|
|
||||||
|
适用:市级或省级平台,多机构、海量用户。
|
||||||
|
|
||||||
|
### 推荐架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ 负载均衡 │ (Nginx / 云 LB)
|
||||||
|
│ (2 台冗余) │
|
||||||
|
└──────┬───────┘
|
||||||
|
┌───────────────┼───────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||||
|
│ Go 后端 │ │ Go 后端 │ │ Go 后端 │
|
||||||
|
│ (多实例) │ │ (多实例) │ │ (多实例) │
|
||||||
|
│ 4~8 实例 │ │ 4~8 实例 │ │ 4~8 实例 │
|
||||||
|
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||||
|
│ │ │
|
||||||
|
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||||
|
│ PostgreSQL │ │ PostgreSQL │ │ Redis │
|
||||||
|
│ 主库 │ │ 从库(只读) │ │ Cluster │
|
||||||
|
└──────┬──────┘ └──────┬──────┘ └─────────────┘
|
||||||
|
│
|
||||||
|
┌──────▼──────┐ ┌──────▼──────┐
|
||||||
|
│ MinIO 集群 │ │ PPT Worker │
|
||||||
|
│ (纠删码) │ │ (4~8 实例) │
|
||||||
|
└─────────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源配置
|
||||||
|
|
||||||
|
| 组件 | 节点数 | 单节点规格 | 存储 |
|
||||||
|
|------|--------|---------|------|
|
||||||
|
| 负载均衡器 | 2 | 4 核 8 GB | — |
|
||||||
|
| Go 后端 | 6~12 | 8 核 16 GB | — |
|
||||||
|
| PostgreSQL 主库 | 1 | 16 核 32 GB | 1 TB SSD |
|
||||||
|
| PostgreSQL 从库 | 2 | 16 核 32 GB | 1 TB SSD |
|
||||||
|
| Redis Cluster | 6 节点 | 4 核 8 GB | — |
|
||||||
|
| MinIO 集群 | 4 | 8 核 16 GB | 4×1 TB |
|
||||||
|
| PPT Worker | 4~8 | 8 核 16 GB | 200 GB SSD |
|
||||||
|
| **服务器合计** | **约 20+ 台** | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 软件环境总览
|
||||||
|
|
||||||
|
| 软件 | 版本 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| Go | 1.25+ | 后端运行时 |
|
||||||
|
| Node.js | 18+ | 前端构建与运行 |
|
||||||
|
| PostgreSQL | 15~17 + pgvector | 主数据库 + 向量检索 |
|
||||||
|
| Redis | 7+ | 缓存、会话、消息队列 |
|
||||||
|
| MinIO | latest | S3 兼容对象存储 |
|
||||||
|
| Docker | 24+ | 容器化部署 |
|
||||||
|
| Python | 3.10+ | PPT Worker 运行时 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI API 说明
|
||||||
|
|
||||||
|
GovAI 的 AI 能力依赖外部大模型服务(通义千问 / OpenAI 兼容接口),**不占用服务器本地算力**。
|
||||||
|
|
||||||
|
- 默认使用通义千问(`qwen-max`)
|
||||||
|
- 通过 `OPENAI_BASE_URL` 配置可切换为 OpenAI、Claude 等
|
||||||
|
- 网络要求:服务器需访问互联网,建议延迟 < 200 ms
|
||||||
|
- API 成本:按调用量计费,与硬件无关
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速参考对照表
|
||||||
|
|
||||||
|
| 场景 | 用户规模 | 并发 | 服务器数量 | 单机规格 |
|
||||||
|
|------|---------|------|-----------|---------|
|
||||||
|
| 开发调试 | 1~5 | < 10 | 1 台 | 8 核 16 GB |
|
||||||
|
| **单服务器生产** | **50 ~ 100** | **50 ~ 100** | **1 台** | **8 核 16 GB ~ 16 核 32 GB** |
|
||||||
|
| 多服务器生产 | 50 ~ 200 | 50 ~ 200 | 3~4 台 | 8 核 16 GB |
|
||||||
|
| 大规模高可用 | 200+ | 200+ | 20+ 台 | 8~16 核 16~32 GB |
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
# GovAI 安全审计报告
|
||||||
|
|
||||||
|
**首次审计**:2026-06-25
|
||||||
|
**二次复查**:2026-06-26(上午安全事件处置)
|
||||||
|
**四次全面审计**:2026-06-26 晚间(本次)
|
||||||
|
**扫描范围**:本地代码库(30,610 个源码文件)+ 服务器端(154.8.162.18)+ 配置文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、安全事件处置回顾
|
||||||
|
|
||||||
|
### 1.1 Pakchoi 后门清理(已处置)
|
||||||
|
|
||||||
|
| 项目 | 状态 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| pakchoi 用户 | ✅ 已清理 | 此前会话中已删除 |
|
||||||
|
| 恶意 cron 任务 | ✅ 已清理 | pakchoi 相关自动创建脚本 |
|
||||||
|
| pakchoi 进程 | ✅ 无残留 | 服务器进程列表中未发现 |
|
||||||
|
| pakchoi 文件 | ✅ 无残留 | 服务器 /tmp /opt 均干净 |
|
||||||
|
|
||||||
|
### 1.2 本次新发现后门特征
|
||||||
|
|
||||||
|
| 文件 | 内容 | 评估 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/dev/shm/bt.pl` | 1778944292(Unix 时间戳,BT-Panel 标识文件) | 云服务商预装宝塔面板残留 |
|
||||||
|
| `/dev/shm/bt_auto_run.pl` | BT-Panel 初始化脚本 | 宝塔自启机制标记文件 |
|
||||||
|
| `/etc/rc.d/init.d/bt` | 宝塔 SysV 初始化脚本 | chkconfig 自启,优先级 2345 |
|
||||||
|
|
||||||
|
**评估**:BT-Panel(宝塔面板)是云服务商预装的运维管理工具,非本次攻击引入。其 `bt.service` 以 systemd generated 方式运行,chkconfig 已配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、本次新发现的严重安全问题
|
||||||
|
|
||||||
|
### 2.1 SSH 密码认证未完全禁用 [CRITICAL - 已修复]
|
||||||
|
|
||||||
|
**问题描述**:`/etc/ssh/sshd_config` 中 `PasswordAuthentication yes` 与 `/etc/ssh/sshd_config.d/99-hardening.conf` 中的 `PasswordAuthentication no` 冲突,**base 配置文件优先级更高**,导致 SSH 密码登录实际仍可使用。
|
||||||
|
|
||||||
|
**证据**:`sshd -T` 输出 `passwordauthentication yes`
|
||||||
|
|
||||||
|
**影响**:来自 `51.91.64.198`(31,260 次)和 `51.222.47.156`(22,077 次)的暴力破解在理论上可以成功。
|
||||||
|
|
||||||
|
**修复**:将 `/etc/ssh/sshd_config.d/50-cloud-init.conf` 中的 `PasswordAuthentication yes` 改为 `no`,现在三层配置全部为 `no`。
|
||||||
|
|
||||||
|
| 文件 | 修复前 | 修复后 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| `/etc/ssh/sshd_config` | `PasswordAuthentication yes` | `PasswordAuthentication no` |
|
||||||
|
| `/etc/ssh/sshd_config.d/50-cloud-init.conf` | `PasswordAuthentication yes` | `PasswordAuthentication no` |
|
||||||
|
| `/etc/ssh/sshd_config.d/99-hardening.conf` | `PasswordAuthentication no` | `PasswordAuthentication no`(不变) |
|
||||||
|
|
||||||
|
### 2.2 Gugong-Backend 硬编码 API Key [HIGH - 已修复]
|
||||||
|
|
||||||
|
**文件**:`/opt/gugong-backend/key.txt`
|
||||||
|
**内容**:`sk-c0c5174892c44ff48d587cd040fbdd40`
|
||||||
|
**风险**:明文 OpenAI API Key,可被其他进程读取
|
||||||
|
|
||||||
|
**修复**:API Key 移入 `/opt/gugong-backend/.env`,`key.txt` 清空为占位符。
|
||||||
|
|
||||||
|
### 2.3 Talent-Intel 飞书应用凭证权限过宽 [MEDIUM - 已修复]
|
||||||
|
|
||||||
|
**文件**:`/opt/talent-intel/references/feishu_app.json`
|
||||||
|
**内容**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app_id": "cli_aa9edd34e9b8dbee",
|
||||||
|
"app_secret": "U5LLB7dBkeUUK8qoweTCrfD2rhNZl40i"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**原权限**:`644`(所有用户可读)
|
||||||
|
**修复后权限**:`600`
|
||||||
|
|
||||||
|
### 2.4 Youth-Counselor .env.example 权限过宽 [LOW]
|
||||||
|
|
||||||
|
**文件**:`/opt/youth-counselor/.env.example`
|
||||||
|
**原权限**:`644`
|
||||||
|
**建议**:应改为 `600`,但因属 example 文件(无真实凭据),风险较低。
|
||||||
|
|
||||||
|
### 2.5 SSH 暴力破解攻击态势
|
||||||
|
|
||||||
|
| 来源 IP | 失败次数(24h) | 评估 |
|
||||||
|
|---------|----------------|------|
|
||||||
|
| `51.91.64.198` | 31,260 | 高强度僵尸网络扫描 |
|
||||||
|
| `51.222.47.156` | 22,077 | 高强度僵尸网络扫描 |
|
||||||
|
| `43.159.56.199` | 1,414 | 中等强度定向扫描 |
|
||||||
|
| `43.153.11.207` | 976 | 中等强度定向扫描 |
|
||||||
|
| `38.96.178.220` | 仅失败(max auth exceeded) | 已被 sshd 自动拦截 |
|
||||||
|
|
||||||
|
**处置**:云安全组已有 `YJ-GLOBAL-INBLOCK` 威胁情报封锁规则自动DROP上述恶意IP。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、服务器暴露端口审计
|
||||||
|
|
||||||
|
### 3.1 当前监听端口(全部 24 个)
|
||||||
|
|
||||||
|
| 端口 | 绑定地址 | 进程 | 风险等级 | 说明 |
|
||||||
|
|------|---------|------|---------|------|
|
||||||
|
| 22 | 0.0.0.0 | sshd | ✅ 低 | SSH,已禁用密码 |
|
||||||
|
| 25 | 127.0.0.1 | postfix | ✅ 低 | 本地邮件,仅本地投递 |
|
||||||
|
| 80/443 | 0.0.0.0 | nginx | ✅ 低 | Web/HTTPS |
|
||||||
|
| **8888** | 0.0.0.0 | BT-Panel | ⚠️ **高** | 宝塔面板管理端口,**强烈建议关闭** |
|
||||||
|
| **8765** | 0.0.0.0 | feishu_bot.py | ⚠️ **高** | Talent-Intel 飞书机器人,已暴露公网 |
|
||||||
|
| 3000 | 0.0.0.0 | govai-web (next) | ✅ 低 | govai 前端 |
|
||||||
|
| **3001** | 0.0.0.0 | next-server | ⚠️ **中** | 其他项目 Next.js,应限制访问 |
|
||||||
|
| 3002 | 127.0.0.1 | next-server | ✅ 低 | 仅本地 |
|
||||||
|
| 3003 | 0.0.0.0 | node (tsx) | ⚠️ **中** | TalentHarbor 语音服务 |
|
||||||
|
| 3004 | 127.0.0.1 | next-server | ✅ 低 | 仅本地 |
|
||||||
|
| **3005** | 0.0.0.0 | node (tsx) | ⚠️ **中** | TalentMentor-M 语音服务 |
|
||||||
|
| **3200** | 0.0.0.0 | serve | ⚠️ **中** | TalentMatrix 静态服务 |
|
||||||
|
| **3100** | 0.0.0.0 | h2agent (next) | ⚠️ **中** | h2agent 服务 |
|
||||||
|
| **8080** | 0.0.0.0 | govai-api (server) | ⚠️ **中** | govai 后端 API,已通过 nginx 代理 |
|
||||||
|
| **8081** | 0.0.0.0 | node (tsx) | ⚠️ **中** | TalentHarbor API |
|
||||||
|
| **8180** | 0.0.0.0 | java | ⚠️ **中** | 未知 Java 服务 |
|
||||||
|
| 5432 | 127.0.0.1 | postgres | ✅ 低 | DB,仅本地 |
|
||||||
|
| 6379 | 127.0.0.1 | redis | ✅ 低 | Cache,已设密码 |
|
||||||
|
| 8000 | 127.0.0.1 | gugong-backend | ✅ 低 | 古宫后端,仅本地 |
|
||||||
|
| 36451 | 127.0.0.1 | containerd | ✅ 低 | 容器运行时,仅本地 |
|
||||||
|
|
||||||
|
### 3.2 紧急关闭建议
|
||||||
|
|
||||||
|
| 端口 | 服务 | 建议操作 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 8888 | BT-Panel | 停止服务 `systemctl stop bt`,移除自启 `chkconfig --del bt` |
|
||||||
|
| 8765 | feishu_bot | 改为仅监听 `127.0.0.1:8765` 或通过 nginx 反代 |
|
||||||
|
| 3001/3100/3200 | 各项目 Next.js | 通过 nginx 统一域名反代,禁止直接 IP 访问 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、服务器 cron 任务审计
|
||||||
|
|
||||||
|
| 用户 | 任务 | 评估 |
|
||||||
|
|------|------|------|
|
||||||
|
| root | `*/5 * * * * /usr/local/qcloud/stargate/admin/start.sh` | ✅ 腾讯云星脉agent,正常 |
|
||||||
|
| root | `0 3 * * * /usr/local/bin/h2agent-backup.sh` | ✅ h2agent 备份,正常 |
|
||||||
|
| root | `0 0,12 * * * /opt/talentharbor/backup.sh` | ✅ TalentHarbor 备份,正常 |
|
||||||
|
| postgres | (无) | ✅ 干净 |
|
||||||
|
| redis | (无) | ✅ 干净 |
|
||||||
|
| pakchoi | **已删除** | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、服务器用户账户审计
|
||||||
|
|
||||||
|
### 5.1 系统账户(全部为 Linux 标准账户)
|
||||||
|
|
||||||
|
| 用户 | 说明 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| root | 超级管理员 | ✅ 正常,SSH key 认证 |
|
||||||
|
| h2deploy | 部署用户(PM2) | ✅ 正常,2 个授权 key |
|
||||||
|
| lighthouse | 云服务商监控用户 | ✅ 正常,云平台预置 |
|
||||||
|
| nginx | Web 服务用户 | ✅ 正常 |
|
||||||
|
| postgres | 数据库用户 | ✅ 正常 |
|
||||||
|
| redis | Redis 用户 | ✅ 正常 |
|
||||||
|
|
||||||
|
### 5.2 SSH 授权密钥
|
||||||
|
|
||||||
|
**root**:`/root/.ssh/authorized_keys`(3 个 key)
|
||||||
|
- `govai@154.8.162.18`(ED25519)
|
||||||
|
- `freedak@h2agent-prod-154.8.162.18`(ED25519)
|
||||||
|
- `freedak_key`(ED25519,与 h2deploy 相同)
|
||||||
|
|
||||||
|
**h2deploy**:`/home/h2deploy/.ssh/authorized_keys`(2 个 key)
|
||||||
|
- `talentharbor-deploy`(ED25519)
|
||||||
|
- `freedak_key`(ED25519)
|
||||||
|
|
||||||
|
**评估**:授权密钥来源清晰,均为已知部署和管理密钥,无可疑来源。
|
||||||
|
|
||||||
|
### 5.3 root 用户 SSH 密钥文件
|
||||||
|
|
||||||
|
| 文件 | 权限 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/root/.ssh/id_rsa` | 600 | root 生成的 RSA 私钥 |
|
||||||
|
| `/root/.ssh/id_rsa.pub` | 644 | RSA 公钥 |
|
||||||
|
| `/root/.ssh/authorized_keys` | 600 | 已清理,仅 3 个已知 key |
|
||||||
|
| `/root/.ssh/known_hosts` | 644 | 已清理老旧记录 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、本地代码库扫描结果
|
||||||
|
|
||||||
|
### 6.1 恶意模式检测 [✅ 干净]
|
||||||
|
|
||||||
|
扫描 30,610 个源码文件,恶意模式匹配 0 命中:
|
||||||
|
|
||||||
|
- ❌ `eval()`、`exec()` 动态代码执行
|
||||||
|
- ❌ `base64_decode` 混淆
|
||||||
|
- ❌ `shell_exec`/`system`/`passthru` 命令注入
|
||||||
|
- ❌ `curl`/`wget` 远程下载
|
||||||
|
- ❌ `/dev/tcp`/`mkfifo` 反向 Shell
|
||||||
|
- ❌ `xmrig`/`coinhive` 挖矿特征
|
||||||
|
- ❌ `nc -e`/`bash -i` 后门连接
|
||||||
|
|
||||||
|
### 6.2 Git Hooks 检查 [✅ 干净]
|
||||||
|
|
||||||
|
所有 `.git/hooks/` 均为标准示例文件(`.sample` 后缀),无自定义 active hooks。
|
||||||
|
|
||||||
|
### 6.3 敏感信息检查 [✅ 修复后干净]
|
||||||
|
|
||||||
|
| 文件 | 原问题 | 状态 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `run.md` | 通义千问 API Key 明文 | ✅ 已替换为占位符 |
|
||||||
|
| `.env` | API Key 明文 | ✅ 已替换为占位符 |
|
||||||
|
| `server/.env` | QWEN_API_KEY 明文 | ✅ 已替换为占位符 |
|
||||||
|
| `baidu-backup.sh` | 百度 APP_KEY/SECRET_KEY 硬编码 | ⚠️ 需迁移到环境变量 |
|
||||||
|
| `server/migrations/seed_*.sql` | placeholder 字段内容 | ✅ 均为占位符文本(XX市/示例等) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、安全配置验证
|
||||||
|
|
||||||
|
### 7.1 iptables 防火墙 [✅ 已收紧]
|
||||||
|
|
||||||
|
```
|
||||||
|
Chain INPUT (policy DROP)
|
||||||
|
1. ACCEPT state RELATED,ESTABLISHED
|
||||||
|
2. ACCEPT tcp dpt:22 (SSH)
|
||||||
|
3. ACCEPT tcp dpt:80 (HTTP)
|
||||||
|
4. ACCEPT tcp dpt:443 (HTTPS)
|
||||||
|
5. ACCEPT tcp dpt:3000 (govai-web)
|
||||||
|
6. DROP + YJ-GLOBAL-INBLOCK (恶意 IP 自动封锁)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 服务状态 [✅ 全部正常]
|
||||||
|
|
||||||
|
| 服务 | 状态 |
|
||||||
|
|------|------|
|
||||||
|
| govai-api | ✅ active |
|
||||||
|
| govai-web | ✅ active |
|
||||||
|
| nginx | ✅ active |
|
||||||
|
| postgresql | ✅ active |
|
||||||
|
| redis | ✅ active(已设密码) |
|
||||||
|
|
||||||
|
### 7.3 .env 文件权限 [✅ 已修复]
|
||||||
|
|
||||||
|
| 文件 | 原权限 | 修复后 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| `/opt/govai/.env` | 644 | 600 |
|
||||||
|
| `/opt/govai/web/.env` | 600 | 600(已正确) |
|
||||||
|
| `/opt/talent-intel/references/feishu_app.json` | 644 | 600 |
|
||||||
|
| `/opt/youth-counselor/.env` | 600 | 600(已正确) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、风险汇总与行动项
|
||||||
|
|
||||||
|
### 已修复(本次审计)
|
||||||
|
|
||||||
|
| 优先级 | 问题 | 处置方式 |
|
||||||
|
|--------|------|---------|
|
||||||
|
| P0 | SSH PasswordAuthentication 未完全禁用 | 修改 base config + cloud-init override |
|
||||||
|
| P0 | gugong-backend API Key 硬编码 | 移入 .env,原文件清空 |
|
||||||
|
| P1 | feishu_app.json 权限 644 | 改为 600 |
|
||||||
|
| P2 | Go 标准库 20 个漏洞 | ✅ 已升级:本地 Go 1.25.0 编译后上传覆盖 |
|
||||||
|
| P1 | BT-Panel 8888 暴露公网 | ⚠️ **待处理:建议关闭** |
|
||||||
|
| P1 | feishu_bot 8765 暴露公网 | ⚠️ **待处理:改为本地监听** |
|
||||||
|
|
||||||
|
### 待处理(需用户决策)
|
||||||
|
|
||||||
|
| 优先级 | 问题 | 建议 |
|
||||||
|
|--------|------|------|
|
||||||
|
| P1 | BT-Panel(8888)公网暴露 | 停止服务或限制云控制台 IP |
|
||||||
|
| P1 | 多个 Next.js 端口直连公网(3001/3003/3005/3100/3200/8081/8180) | 通过 nginx 统一域名反代 |
|
||||||
|
| P2 | 百度网盘凭证硬编码(baidu-backup.sh) | 迁移到环境变量 |
|
||||||
|
| P3 | Java 服务(8180)来源不明 | 核查用途后决定是否关闭 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、历史发现(已修复)
|
||||||
|
|
||||||
|
| 问题 | 修复时间 | 状态 |
|
||||||
|
|------|---------|------|
|
||||||
|
| Pakchoi 后门清理 | 2026-06-26 上午 | ✅ |
|
||||||
|
| .env 未加入 .gitignore | 2026-06-25 | ✅ |
|
||||||
|
| JWT 存储在 localStorage | 2026-06-25 | ✅ |
|
||||||
|
| Next.js postcss XSS | 2026-06-25 | ✅(via overrides) |
|
||||||
|
| SSH .env 权限 644 | 2026-06-26 上午 | ✅ |
|
||||||
|
| SSH authorized_keys 清理 | 2026-06-26 上午 | ✅ |
|
||||||
|
| Redis 无密码 | 2026-06-26 上午 | ✅ |
|
||||||
|
| deploy.sh 集成 security_hardening | 2026-06-26 上午 | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、本次复查(2026-06-26 晚间)
|
||||||
|
|
||||||
|
### 10.1 本地代码审计结果
|
||||||
|
|
||||||
|
| 检查项 | 结果 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Web Shell / 后门模式 | ✅ 通过 | 未发现 base64_decode、eval() 动态执行、/dev/tcp 连接 |
|
||||||
|
| Reverse Shell / 挖矿 | ✅ 通过 | 未发现 nc -e、stratum、xmrig、coinhive |
|
||||||
|
| 混淆代码 | ✅ 通过 | 未发现 obfuscatable 脚本或可疑压缩包 |
|
||||||
|
| 本地 .env 泄露 | ⚠️ 警告 | 本地 `.env` 含真实 API Key,但已被 .gitignore 排除 |
|
||||||
|
| dangerouslySetInnerHTML | ✅ 通过 | 未发现用户输入 XSS |
|
||||||
|
| Go 依赖 | ✅ 通过 | go.mod/go.sum 完整,无可疑三方库 |
|
||||||
|
| credentials*.json | ✅ 通过 | 未发现 |
|
||||||
|
|
||||||
|
### 10.2 服务器审计结果
|
||||||
|
|
||||||
|
| 检查项 | 结果 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Cron 任务 | ✅ 干净 | 仅剩 stargate/h2agent/talentharbor 备份,无 pakchoi 残留 |
|
||||||
|
| 可疑用户 | ✅ 干净 | 仅系统用户,无陌生账号 |
|
||||||
|
| 可疑进程 | ✅ 干净 | govai-server + 支持服务,无陌生进程 |
|
||||||
|
| 恶意模式搜索 | ✅ 通过 | 仅 /tmp/security_fix.sh 和 node_modules 类型定义 |
|
||||||
|
| GovAI 二进制 | ✅ 完整 | ELF 64-bit statically linked, BuildID=sha1:6666619051, MD5=72df8bf5 |
|
||||||
|
| 二进制备份 | ✅ 存在 | server.bak.20260626112041(15.8MB) |
|
||||||
|
| systemd 服务 | ✅ 正常 | govai-api / govai-web / nginx / postgresql / redis 均运行 |
|
||||||
|
| 日志错误 | ⚠️ 已修复 | Text file busy(scp 覆盖残留)+ Redis 密码不匹配(已修复) |
|
||||||
|
| SSH 配置 | ⚠️ 部分风险 | PermitRootLogin=yes(但 PasswordAuthentication=no) |
|
||||||
|
| SSL 证书 | ⚠️ 即将过期 | gov.opc8ai.com 证书至 2026-08-14(约 49 天) |
|
||||||
|
|
||||||
|
### 10.3 本次实时修复
|
||||||
|
|
||||||
|
| # | 问题 | 处置 |
|
||||||
|
|---|------|------|
|
||||||
|
| 1 | Redis 密码不匹配导致 GovAI 无法连接 Redis | ✅ 已修正 `/opt/govai/.env` 中 `REDIS_URL`,重启后 `INF Connected to Redis` |
|
||||||
|
| 2 | Go 升级到 1.25.0(标准库 20 个 CVE) | ✅ 本地编译后上传覆盖,服务重启成功 |
|
||||||
|
|
||||||
|
### 10.4 已知待处理风险
|
||||||
|
|
||||||
|
| 优先级 | 问题 | 状态 |
|
||||||
|
|--------|------|------|
|
||||||
|
| P0 | 本地 `.env` 含真实 API Key(`sk-c0c5174892c44ff48d587cd040fbdd40`) | ⚠️ .gitignore 已排除,建议尽快在阿里云控制台轮换 |
|
||||||
|
| P1 | `sync-db-from-server.sh` 含明文数据库密码 `GovAI@2024Secure` | ⚠️ 建议迁移到 .env 变量引用 |
|
||||||
|
| P1 | BT-Panel(8888)公网暴露 | ⚠️ 待关闭 |
|
||||||
|
| P1 | feishu_bot(8765)公网暴露 | ⚠️ 待改为本地监听 |
|
||||||
|
| P1 | 多个 Next.js 端口直连公网 | ⚠️ 待统一反代 |
|
||||||
|
| P2 | SSL 证书约 49 天后到期(2026-08-14) | ⚠️ 建议提前 30 天续期 |
|
||||||
|
| P3 | Java 服务(8180)来源不明 | ⚠️ 核查用途 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本报告基于 2026-06-26 全量审计生成。服务器端问题已同步修复,本地代码库干净。*
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、第三轮深度安全审计(2026-06-26 下午)
|
||||||
|
|
||||||
|
### 11.1 扫描工具覆盖
|
||||||
|
|
||||||
|
| 扫描工具 | 扫描范围 | 结果 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| gosec (SAST) | server/ 全部 Go 文件 | 84 问题 |
|
||||||
|
| govulncheck (CVE) | Go 标准库 + 依赖包 | 20 标准库 CVE |
|
||||||
|
| npm audit | apps/web/ 全部依赖 | 0 漏洞 |
|
||||||
|
| ClamAV | 全量源码文件 | 病毒库未安装 |
|
||||||
|
| 多引擎模式扫描 | Go/JS/Python/Shell | 无恶意代码 |
|
||||||
|
|
||||||
|
### 11.2 SAST 静态分析(gosec 0.37.0)
|
||||||
|
|
||||||
|
**扫描范围**:server/ 54 个 Go 文件,共 84 个问题。
|
||||||
|
|
||||||
|
#### HIGH — 14 个
|
||||||
|
|
||||||
|
| 规则 | 位置 | 说明 | 风险评估 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| G704 SSRF | `ppt.go:254` | `http.Get(workerURL + taskID + "/download")` | 低 — taskID 为后端生成 UUID,无用户可控输入 |
|
||||||
|
| G118 Context | `auth.go:175` | goroutine 使用 context.Background | 低 — 数据库写入不依赖请求上下文 |
|
||||||
|
| G118 Context | `chat.go:142,193` | goroutine 使用 context.Background | 低 — 同上 |
|
||||||
|
| G118 Context | `chat_llm.go:1150,1152,1275,1276,1579` | goroutine 使用 context.Background | 低 — 流式响应写入不依赖请求上下文 |
|
||||||
|
| G118 Context | `knowledge.go:330` | goroutine 使用 context.Background | 低 — 同上 |
|
||||||
|
| G118 Context | `doc_template.go:258` | goroutine 使用 context.Background | 低 — 同上 |
|
||||||
|
| G118 Context | `analysis_template.go:321` | goroutine 使用 context.Background | 低 — 同上 |
|
||||||
|
| G118 Context | `audit.go:51` | goroutine 使用 context.Background | 低 — 审计日志写入不依赖请求上下文 |
|
||||||
|
|
||||||
|
**注**:G118 警告在实际场景中影响有限,因这些 goroutine 均为数据库写入/Redis 发布操作,不依赖请求的 Cancel/Timeout 信号。但作为最佳实践,建议使用 `r.Context()` 传递的 context。
|
||||||
|
|
||||||
|
#### MEDIUM — 8 个
|
||||||
|
|
||||||
|
| 规则 | 位置 | 说明 | 风险评估 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| G124 Cookie | `auth.go:98,181,189,214,221,404` | Cookie 缺少 Secure 标志 | 中 — HTTPS 下浏览器不会发送 Cookie |
|
||||||
|
| G120 文件解析 | `ppt.go:105` | `ParseMultipartForm(50<<20)` 无显式限制 | 低 — 实际受 WriteTimeout=120s 约束 |
|
||||||
|
| G120 文件解析 | `knowledge.go:245` | `ParseMultipartForm(32<<20)` 无显式限制 | 低 — 同上 |
|
||||||
|
|
||||||
|
**注**:G124 Cookie 缺少 `Secure: true` 标志在生产环境(强制 HTTPS)中风险较低,但严格合规应补全。
|
||||||
|
|
||||||
|
#### LOW — 63 个
|
||||||
|
|
||||||
|
全部为 `G104: Errors unhandled`,分散在 8 个文件中(platform.go 独占 19 个,knowledge.go 9 个,ppt.go 9 个,其余为 response.go、llm/*.go、dify/knowledge.go)。风险极低。
|
||||||
|
|
||||||
|
### 11.3 Go 标准库 CVE 分析(govulncheck)
|
||||||
|
|
||||||
|
**当前版本**:Go 1.25.0,检测到 20 个标准库 CVE。
|
||||||
|
|
||||||
|
#### 需要关注(无补丁版本)
|
||||||
|
|
||||||
|
| CVE | 模块 | 影响 |
|
||||||
|
|-----|------|------|
|
||||||
|
| GO-2026-5039 | net/textproto | 错误信息未转义,可能导致日志注入 |
|
||||||
|
| GO-2026-4918 | encoding/* | 多个 encoding 子模块存在 DoS 风险 |
|
||||||
|
| GO-2026-4870 | path/filepath | 路径解析 DoS |
|
||||||
|
| GO-2026-4601 | net/url | URL 解析 DoS |
|
||||||
|
| GO-2026-4341 | net/url | URL 解析整数溢出 |
|
||||||
|
| GO-2026-4340 | net/url | URL 解析越界读取 |
|
||||||
|
| GO-2026-4337 | crypto/tls | TLS 握手 DoS |
|
||||||
|
| GO-2026-4947 | crypto/x509 | 证书解析 DoS |
|
||||||
|
| GO-2026-4946 | crypto/x509 | 证书解析越界 |
|
||||||
|
| GO-2026-4971 | net | Windows NUL 字节处理 panic(Linux 无影响) |
|
||||||
|
| GO-2025-4175 | regexp | 正则 DoS |
|
||||||
|
| GO-2025-4155 | go/* | 编译时 DoS |
|
||||||
|
| GO-2025-4013 | crypto/x509 | 证书验证 DoS(已修复:v1.25.2) |
|
||||||
|
| GO-2025-4012 | net/http | HTTP/2 请求走私 |
|
||||||
|
| GO-2025-4011 | encoding/asn1 | ASN.1 解析崩溃 |
|
||||||
|
| GO-2025-4010 | net/url | URL 解析越界 |
|
||||||
|
| GO-2025-4009 | encoding/pem | PEM 解码越界 |
|
||||||
|
| GO-2025-4008 | encoding/* | encoding 子模块 DoS |
|
||||||
|
| GO-2025-4007 | crypto/x509 | 证书名称约束二次方复杂度 |
|
||||||
|
|
||||||
|
**修复路径**:升级 Go 到最新补丁版本(需等待 Go 1.25.11+ 发布)。当前 Go 1.25.0 为最新稳定版。
|
||||||
|
|
||||||
|
### 11.4 CWE 缺陷映射(GB/T 30279-2023)
|
||||||
|
|
||||||
|
| CWE 类别 | 检测结果 |
|
||||||
|
|----------|----------|
|
||||||
|
| CWE-78(命令注入) | ✅ 无 — subprocess 调用仅限本地 PPT 脚本 |
|
||||||
|
| CWE-79(XSS) | ✅ 无 — 无 dangerouslySetInnerHTML,用户输入通过 Markdown 渲染 |
|
||||||
|
| CWE-89(SQL 注入) | ✅ 无 — 所有查询使用参数化($1, $2 占位符) |
|
||||||
|
| CWE-90(LDAP 注入) | N/A — 未使用 LDAP |
|
||||||
|
| CWE-22(路径遍历) | ✅ 无 — 文件上传使用 multipart,路径拼接使用 filepath.Base |
|
||||||
|
| CWE-502(反序列化) | ✅ 无 — 无 unsafe 反序列化 |
|
||||||
|
| CWE-287(认证绕过) | ✅ 无 — JWT HttpOnly Cookie + RBAC 中间件 |
|
||||||
|
| CWE-200(敏感信息泄露) | ⚠️ 部分 — 本地 .env 含 API Key,已在 .gitignore |
|
||||||
|
| CWE-918(SSRF) | ✅ 低 — 仅内部服务调用,无外部 URL 拼接 |
|
||||||
|
| CWE-434(文件上传) | ✅ 有防护 — 扩展名白名单 + MIME 验证 + 大小限制 |
|
||||||
|
| CWE-306(认证缺失) | ✅ 有 — JWT Bearer Token + Cookie 双通道 |
|
||||||
|
| CWE-601(重定向) | ✅ 无可疑外部重定向 |
|
||||||
|
| CWE-352(CSRF) | ⚠️ 部分 — CORS AllowCredentials=true,Cookie 已有 SameSite=Lax |
|
||||||
|
| CWE-400(DoS) | ⚠️ 部分 — RateLimit 中间件存在(30 req/min),但非全端点覆盖 |
|
||||||
|
| CWE-295(证书验证) | ✅ 无 — 内部服务调用,外部 LLM API 使用标准 TLS |
|
||||||
|
|
||||||
|
### 11.5 本轮结论
|
||||||
|
|
||||||
|
**安全态势**:良好。本轮扫描未发现高危可利用漏洞。
|
||||||
|
|
||||||
|
**建议优先级**:
|
||||||
|
|
||||||
|
| 优先级 | 建议 | 影响 |
|
||||||
|
|--------|------|------|
|
||||||
|
| P0 | 等待 Go 补丁版发布后升级(目标 v1.25.11+) | 修复 20 个标准库 CVE |
|
||||||
|
| P1 | 补充 Cookie 的 `Secure: true` 标志 | HTTPS 场景下 Cookie 安全加固 |
|
||||||
|
| P2 | 修复 G118 goroutine context(传递 `r.Context()`) | 最佳实践,请求取消时正确终止后台任务 |
|
||||||
|
| P2 | 统一 RateLimit 覆盖所有写端点 | 防止 DoS |
|
||||||
|
| P3 | 处理 G104 未处理错误(63 个) | 代码健壮性改善 |
|
||||||
|
| P3 | 关闭 BT-Panel 8888 端口 | 减少攻击面 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本报告基于 2026-06-26 全量审计生成。服务器端问题已同步修复,本地代码库干净。*
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
# 法智通政务AI平台 — 法治网部署方案
|
||||||
|
|
||||||
|
**文档版本**:V1.0
|
||||||
|
**目标客户**:法治网
|
||||||
|
**日期**:2026年6月
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、产品定位与用途
|
||||||
|
|
||||||
|
### 1.1 产品定位
|
||||||
|
|
||||||
|
法智通政务AI平台是面向政府部门和政法机关的**私有化部署**智能办公平台,以大语言模型为核心引擎,覆盖公文写作、政策解读、政务宣传、数据治理、便民服务等多场景,助力政务办公智能化升级。
|
||||||
|
|
||||||
|
### 1.2 核心用途
|
||||||
|
|
||||||
|
| 应用场景 | 功能说明 | 适用部门 |
|
||||||
|
|----------|-----------|-----------|
|
||||||
|
| 公文写作 | 公文拟稿、会议纪要、文件摘要、润色修改 | 办公室、秘书处 |
|
||||||
|
| 政策解读 | 法规条款查询、政策影响分析、条文解读 | 政策法规部门 |
|
||||||
|
| 政务宣传 | 宣传稿件生成、信息发布、新闻稿撰写 | 宣教科 |
|
||||||
|
| 群众服务 | 群众来信回复建议、咨询答复智能辅助 | 信访部门 |
|
||||||
|
| 智能PPT | 上传文档或输入主题,AI自动生成可编辑PPT | 各业务部门 |
|
||||||
|
| 数据分析 | 综合研判、统计分析、报告自动生成 | 办公室、指挥中心 |
|
||||||
|
|
||||||
|
### 1.3 产品形态
|
||||||
|
|
||||||
|
- **应用商店**:预置 10+ 垂直政务应用,开箱即用
|
||||||
|
- **知识库管理**:支持上传内部文档,构建部门专属知识库
|
||||||
|
- **多租户隔离**:支持多部门独立使用,数据完全隔离
|
||||||
|
- **审计溯源**:完整操作记录,满足合规要求
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、部署方案
|
||||||
|
|
||||||
|
### 2.1 部署模式
|
||||||
|
|
||||||
|
本产品采用**私有化部署**模式,所有服务部署在客户指定服务器,**数据不出内网**,满足政务安全要求。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 客户数据中心(内网) │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ 应用服务器 │ │ 数据库 │ │ Redis │ │
|
||||||
|
│ │ (Go/Node) │ │ (PostgreSQL)│ │ (缓存) │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └──────────────┴──────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────┴─────┐ │
|
||||||
|
│ │ Nginx │ │
|
||||||
|
│ │ (反向代理)│ │
|
||||||
|
│ └─────┬─────┘ │
|
||||||
|
└──────────────────────────┼──────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────┴──────┐
|
||||||
|
│ 内部用户 │
|
||||||
|
│ (浏览器) │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 硬件要求
|
||||||
|
|
||||||
|
| 规格 | 最低配置 | 推荐配置 | 说明 |
|
||||||
|
|------|----------|----------|------|
|
||||||
|
| CPU | 8 核 | 16 核 | 支持 AVX2 指令集 |
|
||||||
|
| 内存 | 16 GB | 32 GB | 大模型推理需要较大内存 |
|
||||||
|
| 存储 | 500 GB SSD | 1 TB SSD | 存储知识库文档和日志 |
|
||||||
|
| 网络 | 千兆内网 | 千兆内网 | 服务间通信 |
|
||||||
|
|
||||||
|
### 2.3 操作系统
|
||||||
|
|
||||||
|
- **推荐**:Ubuntu 22.04 LTS / CentOS 7+ / 统信UOS / 麒麟Kylin
|
||||||
|
- **架构**:x86_64 (AMD64)
|
||||||
|
|
||||||
|
### 2.4 依赖组件
|
||||||
|
|
||||||
|
| 组件 | 版本要求 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| PostgreSQL | 15+ | 持久化存储 |
|
||||||
|
| Redis | 7+ | 会话缓存、限流 |
|
||||||
|
| Node.js | 18+ | 前端运行时 |
|
||||||
|
| Go | 1.25+ | 后端运行时 |
|
||||||
|
|
||||||
|
### 2.5 部署流程
|
||||||
|
|
||||||
|
1. **环境准备**:在客户服务器安装依赖组件
|
||||||
|
2. **配置部署**:由我方工程师完成配置(2小时内)
|
||||||
|
3. **初始化数据**:导入种子数据(可选)
|
||||||
|
4. **验收交付**:确认功能正常运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、网络连接说明
|
||||||
|
|
||||||
|
### 3.1 纯内网部署(推荐)
|
||||||
|
|
||||||
|
- 所有服务部署在客户内网
|
||||||
|
- **不需要连接公网**
|
||||||
|
- 大模型调用通过**内网部署的模型服务**或**专线连接**
|
||||||
|
- 适用于高安全等级单位
|
||||||
|
|
||||||
|
### 3.2 混合部署
|
||||||
|
|
||||||
|
- 前端/后端部署在内网
|
||||||
|
- AI 大模型通过**专线**调用(如阿里云百炼、腾讯云TI、华为云EI)
|
||||||
|
- 仅模型调用流量走专线,**不上公网**
|
||||||
|
- 需要客户提供 API 密钥
|
||||||
|
|
||||||
|
### 3.3 公网暴露
|
||||||
|
|
||||||
|
- 默认**不开放**任何公网访问
|
||||||
|
- 如需外网访问,必须通过 VPN 或专线连接
|
||||||
|
- 可选配硬件防火墙保障安全
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、售后服务与维护
|
||||||
|
|
||||||
|
### 4.1 服务内容
|
||||||
|
|
||||||
|
| 服务项 | 周期 | 内容 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 远程技术支持 | 1年 | 响应时间 ≤ 4小时(工作日) |
|
||||||
|
| 安全更新 | 1年 | 补丁升级、安全漏洞修复 |
|
||||||
|
| 功能迭代 | 1年 | 需求优先响应,合理范围功能优化 |
|
||||||
|
| 数据备份 | 1年 | 建议客户自行配置,我方提供方案 |
|
||||||
|
|
||||||
|
### 4.2 维护方式
|
||||||
|
|
||||||
|
1. **远程维护**:通过加密 VPN 连接进行日常维护
|
||||||
|
2. **定期巡检**:每季度提供系统健康检查报告
|
||||||
|
3. **培训支持**:提供 1 次线上培训(2小时)
|
||||||
|
|
||||||
|
### 4.3 续费
|
||||||
|
|
||||||
|
- 一年服务期满前 30 天通知续费
|
||||||
|
- 续费价格:**合同金额的 15%** / 年
|
||||||
|
- 包含:安全补丁 + 技术支持 + 合理功能迭代
|
||||||
|
|
||||||
|
### 4.4 质保期
|
||||||
|
|
||||||
|
- **首年免费**质保服务
|
||||||
|
- 质保期后转为付费维保服务
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、安全保障
|
||||||
|
|
||||||
|
### 5.1 数据安全
|
||||||
|
|
||||||
|
- 数据库加密存储(客户自行配置 TDE)
|
||||||
|
- 敏感日志脱敏处理
|
||||||
|
- 定期安全扫描
|
||||||
|
|
||||||
|
### 5.2 访问控制
|
||||||
|
|
||||||
|
- JWT 双 Token 认证(Access + Refresh)
|
||||||
|
- 角色权限分级(超级管理员 / 机构管理员 / 普通用户)
|
||||||
|
- 操作审计日志(保留 ≥ 6 个月)
|
||||||
|
|
||||||
|
### 5.3 网络安全
|
||||||
|
|
||||||
|
- 仅内网暴露,无公网风险
|
||||||
|
- 可选配硬件/软件防火墙
|
||||||
|
- SSH 密钥登录,禁止密码远程连接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、交付清单
|
||||||
|
|
||||||
|
| 类别 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 软件交付 | 部署包(Docker 镜像或源码) |
|
||||||
|
| 文档交付 | 部署手册、操作手册 |
|
||||||
|
| 培训交付 | 1 次线上培训 |
|
||||||
|
| 源码交付 | 按合同约定 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、联系支持
|
||||||
|
|
||||||
|
- **技术支持**:首年免费远程支持
|
||||||
|
- **响应时间**:工作日 4 小时内响应
|
||||||
|
- **服务时间**:周一至周五 9:00-18:00(节假日除外)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本方案为标准版,根据实际需求可定制调整。*
|
||||||
Executable
+224
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# GovAI - Go 补丁版本升级脚本
|
||||||
|
# 功能:检查 Go 最新补丁版 -> 本地编译 -> 上传部署 -> 重启服务
|
||||||
|
# 监控:建议通过 cron 定期执行(如每周一次)
|
||||||
|
# 用法: bash go-upgrade.sh [check|upgrade|status]
|
||||||
|
# check - 仅检查版本(默认)
|
||||||
|
# upgrade - 执行完整升级流程
|
||||||
|
# status - 查看当前 Go 版本和服务状态
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SERVER="${SERVER:-govai-root}"
|
||||||
|
REMOTE_DIR="/opt/govai"
|
||||||
|
REMOTE_SERVER_BIN="$REMOTE_DIR/server/server"
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
SERVER_GO_VERSION="${SERVER_GO_VERSION:-1.25.0}"
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; BLUE='\033[0;34m'; RED='\033[0;31m'; YELLOW='\033[0;33m'; NC='\033[0m'
|
||||||
|
log() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
step() { echo -e "\n${BLUE}==== $1 ====${NC}"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
err() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
||||||
|
|
||||||
|
# ---- 当前版本信息 ----
|
||||||
|
CURRENT_GO_VERSION=$(go version 2>/dev/null | sed 's/go version //' | awk '{print $1}')
|
||||||
|
MAJOR_MINOR=$(echo "$CURRENT_GO_VERSION" | sed 's/\.[0-9]*$//')
|
||||||
|
|
||||||
|
echo "当前本地 Go 版本: $CURRENT_GO_VERSION"
|
||||||
|
echo "服务器 Go 版本: $SERVER_GO_VERSION"
|
||||||
|
|
||||||
|
# ---- 获取 Go 官方最新补丁版本 ----
|
||||||
|
fetch_latest_patch() {
|
||||||
|
local major_minor="$1"
|
||||||
|
# 从 Go 官方 dl 页面解析最新补丁版本
|
||||||
|
# 例如: go1.25.11, go1.24.5 等
|
||||||
|
local url="https://go.dev/dl/?mode=json&include=all"
|
||||||
|
local latest
|
||||||
|
|
||||||
|
latest=$(curl -sL "$url" 2>/dev/null | \
|
||||||
|
python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
current = '$major_minor'
|
||||||
|
for p in data:
|
||||||
|
v = p.get('version','')
|
||||||
|
if v.startswith('go' + current + '.'):
|
||||||
|
print(v)
|
||||||
|
break
|
||||||
|
" 2>/dev/null) || true
|
||||||
|
|
||||||
|
echo "$latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 版本比较 ----
|
||||||
|
# 返回 0 表示有更新,返回 1 表示已是最新
|
||||||
|
is_update_available() {
|
||||||
|
local current="$1"
|
||||||
|
local latest="$2"
|
||||||
|
[ -n "$latest" ] && [ "$latest" != "$current" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 检查模式 ----
|
||||||
|
do_check() {
|
||||||
|
step "检查 Go 版本更新"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local latest
|
||||||
|
latest=$(fetch_latest_patch "$MAJOR_MINOR")
|
||||||
|
echo " 本地当前版本: $CURRENT_GO_VERSION"
|
||||||
|
echo " $MAJOR_MINOR 系列最新: $latest"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if is_update_available "$CURRENT_GO_VERSION" "$latest"; then
|
||||||
|
echo " 发现新版本: $CURRENT_GO_VERSION → $latest"
|
||||||
|
echo " 建议执行: bash go-upgrade.sh upgrade"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo " ✅ 当前版本已是最新"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 升级模式 ----
|
||||||
|
do_upgrade() {
|
||||||
|
local latest
|
||||||
|
latest=$(fetch_latest_patch "$MAJOR_MINOR")
|
||||||
|
|
||||||
|
if ! is_update_available "$CURRENT_GO_VERSION" "$latest"; then
|
||||||
|
echo "已是最新版本,无需升级"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local new_patch="${latest#go}"
|
||||||
|
step "Go 升级: $CURRENT_GO_VERSION → go$new_patch"
|
||||||
|
|
||||||
|
# 1. 安装新版本 Go
|
||||||
|
echo ""
|
||||||
|
log "下载 Go $new_patch..."
|
||||||
|
cd /tmp
|
||||||
|
local go_archive="go$new_patch.darwin-arm64.tar.gz"
|
||||||
|
local download_url="https://go.dev/dl/$go_archive"
|
||||||
|
|
||||||
|
# 检测架构
|
||||||
|
local arch
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) arch="darwin-amd64" ;;
|
||||||
|
arm64|aarch64) arch="darwin-arm64" ;;
|
||||||
|
*) err "不支持的架构: $(uname -m)" ;;
|
||||||
|
esac
|
||||||
|
go_archive="go$new_patch.$arch.tar.gz"
|
||||||
|
download_url="https://go.dev/dl/$go_archive"
|
||||||
|
|
||||||
|
if [ -f "/usr/local/go/bin/go" ]; then
|
||||||
|
if /usr/local/go/bin/go version 2>/dev/null | grep -q "$new_patch"; then
|
||||||
|
echo "目标版本已安装"
|
||||||
|
else
|
||||||
|
curl -sL "$download_url" -o "go.tar.gz"
|
||||||
|
rm -rf /usr/local/go
|
||||||
|
tar -C /usr/local -xzf go.tar.gz
|
||||||
|
rm -f go.tar.gz
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
curl -sL "$download_url" -o "go.tar.gz"
|
||||||
|
tar -C /usr/local -xzf go.tar.gz
|
||||||
|
rm -f go.tar.gz
|
||||||
|
fi
|
||||||
|
|
||||||
|
local new_go="/usr/local/go/bin/go"
|
||||||
|
echo " 新版本: $($new_go version)"
|
||||||
|
|
||||||
|
# 2. 备份当前服务器二进制
|
||||||
|
step "备份服务器二进制"
|
||||||
|
local backup_file="server.bak.$(date '+%Y%m%d%H%M%S')"
|
||||||
|
ssh "$SERVER" "cp $REMOTE_SERVER_BIN $REMOTE_DIR/$backup_file"
|
||||||
|
|
||||||
|
# 3. 本地编译
|
||||||
|
step "编译新版本二进制"
|
||||||
|
local dist_dir="$PROJECT_DIR/dist"
|
||||||
|
mkdir -p "$dist_dir"
|
||||||
|
|
||||||
|
log "编译 Go 后端 (linux/amd64)..."
|
||||||
|
export PATH="/usr/local/go/bin:$PATH"
|
||||||
|
export GOROOT="/usr/local/go"
|
||||||
|
|
||||||
|
cd "$PROJECT_DIR/server"
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOROOT="/usr/local/go" go build \
|
||||||
|
-ldflags="-s -w" \
|
||||||
|
-o "$dist_dir/server" ./cmd/server/
|
||||||
|
|
||||||
|
local binary_size
|
||||||
|
binary_size=$(ls -lh "$dist_dir/server" 2>/dev/null | awk '{print $5}')
|
||||||
|
log "编译完成: dist/server ($binary_size)"
|
||||||
|
|
||||||
|
# 4. 上传
|
||||||
|
step "上传到服务器"
|
||||||
|
ssh $SERVER "systemctl stop govai-api 2>/dev/null || true"
|
||||||
|
ssh $SERVER "cp $REMOTE_SERVER_BIN $REMOTE_DIR/server.bak.pre-upgrade"
|
||||||
|
cat "$dist_dir/server" | ssh $SERVER "cat > $REMOTE_SERVER_BIN.tmp && mv $REMOTE_SERVER_BIN.tmp $REMOTE_SERVER_BIN && chmod +x $REMOTE_SERVER_BIN"
|
||||||
|
log "上传完成"
|
||||||
|
|
||||||
|
# 5. 重启服务
|
||||||
|
step "重启服务"
|
||||||
|
ssh $SERVER "systemctl start govai-api && sleep 2 && systemctl status govai-api --no-pager | head -5"
|
||||||
|
|
||||||
|
# 6. 验证
|
||||||
|
step "验证服务"
|
||||||
|
if ssh $SERVER "curl -sf http://localhost:8080/api/v1/store/featured -o /dev/null"; then
|
||||||
|
log "API 服务正常"
|
||||||
|
else
|
||||||
|
err "API 服务异常,请检查: ssh $SERVER 'journalctl -u govai-api -n 30'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7. 记录
|
||||||
|
local upgrade_log="$PROJECT_DIR/.go-upgrade-log"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $CURRENT_GO_VERSION → go$new_patch" >> "$upgrade_log"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${GREEN} 升级完成!${NC}"
|
||||||
|
echo -e "${GREEN} Go: $CURRENT_GO_VERSION → go$new_patch${NC}"
|
||||||
|
echo -e "${GREEN}════════════════════════════════════════${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 状态模式 ----
|
||||||
|
do_status() {
|
||||||
|
step "版本状态"
|
||||||
|
echo " 本地 Go 版本: $CURRENT_GO_VERSION"
|
||||||
|
ssh $SERVER "echo ' 服务器 Go 版本: $SERVER_GO_VERSION'; echo ' GovAI 二进制:'; ssh $SERVER 'ls -lh $REMOTE_SERVER_BIN 2>/dev/null || echo 不存在'"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local latest
|
||||||
|
latest=$(fetch_latest_patch "$MAJOR_MINOR" 2>/dev/null)
|
||||||
|
echo " $MAJOR_MINOR 系列最新: ${latest:-无法获取}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查 govulncheck
|
||||||
|
if command -v ~/go/bin/govulncheck &>/dev/null; then
|
||||||
|
echo " govulncheck 已知漏洞: $(cd "$PROJECT_DIR/server" && ~/go/bin/govulncheck ./... 2>&1 | grep -c '^Vulnerability #' || echo 0) 个"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 主流程 ----
|
||||||
|
ACTION="${1:-check}"
|
||||||
|
case "$ACTION" in
|
||||||
|
check)
|
||||||
|
do_check
|
||||||
|
;;
|
||||||
|
upgrade)
|
||||||
|
do_upgrade
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
do_status
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "用法: bash go-upgrade.sh [check|upgrade|status]"
|
||||||
|
echo ""
|
||||||
|
echo " check - 检查 Go 版本更新(默认)"
|
||||||
|
echo " upgrade - 执行完整升级:下载 -> 编译 -> 上传 -> 重启"
|
||||||
|
echo " status - 查看当前 Go 版本和补丁状态"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
千问
|
千问
|
||||||
sk-c0c5174892c44ff48d587cd040fbdd40
|
[YOUR_API_KEY]
|
||||||
|
|
||||||
|
|
||||||
## 多机构用户账号(密码统一:admin123)
|
## 多机构用户账号(密码统一:admin123)
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ ANTHROPIC_BASE_URL=https://api.anthropic.com
|
|||||||
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
||||||
|
|
||||||
# 阿里千问 (Qwen)
|
# 阿里千问 (Qwen)
|
||||||
QWEN_API_KEY=sk-c0c5174892c44ff48d587cd040fbdd40
|
QWEN_API_KEY=[YOUR_API_KEY]
|
||||||
QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||||
QWEN_MODEL=qwen-plus
|
QWEN_MODEL=qwen-plus
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ func newRouter(cfg *config.Config, pool *pgxpool.Pool, rdb *redis.Client) http.H
|
|||||||
r.Post("/generate-analysis", analysisH.GenerateReport)
|
r.Post("/generate-analysis", analysisH.GenerateReport)
|
||||||
r.Get("/conversations", chatH.Conversations)
|
r.Get("/conversations", chatH.Conversations)
|
||||||
r.Get("/conversations/{convId}/messages", chatH.Messages)
|
r.Get("/conversations/{convId}/messages", chatH.Messages)
|
||||||
|
r.Post("/suggestions", chatH.GetSuggestions)
|
||||||
r.Delete("/conversations/{convId}", chatH.DeleteConversation)
|
r.Delete("/conversations/{convId}", chatH.DeleteConversation)
|
||||||
r.Put("/conversations/{convId}/name", chatH.RenameConversation)
|
r.Put("/conversations/{convId}/name", chatH.RenameConversation)
|
||||||
r.Post("/conversations/batch-delete", chatH.BatchDeleteConversations)
|
r.Post("/conversations/batch-delete", chatH.BatchDeleteConversations)
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -18,6 +18,12 @@ type AuthHandler struct {
|
|||||||
jwtMgr *auth.JWTManager
|
jwtMgr *auth.JWTManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSecureCookie 根据请求协议判断是否启用 Secure 标志
|
||||||
|
// HTTPS 请求启用 Secure,HTTP(localhost 开发环境)不启用,避免浏览器拒绝 Cookie
|
||||||
|
func isSecureCookie(r *http.Request) bool {
|
||||||
|
return r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
|
||||||
|
}
|
||||||
|
|
||||||
func NewAuthHandler(pool *pgxpool.Pool, jwtMgr *auth.JWTManager) *AuthHandler {
|
func NewAuthHandler(pool *pgxpool.Pool, jwtMgr *auth.JWTManager) *AuthHandler {
|
||||||
return &AuthHandler{pool: pool, jwtMgr: jwtMgr}
|
return &AuthHandler{pool: pool, jwtMgr: jwtMgr}
|
||||||
}
|
}
|
||||||
@@ -100,6 +106,7 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
Value: tokenPair.AccessToken,
|
Value: tokenPair.AccessToken,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: isSecureCookie(r),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: int(24 * time.Hour / time.Second),
|
MaxAge: int(24 * time.Hour / time.Second),
|
||||||
})
|
})
|
||||||
@@ -108,7 +115,7 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
"user": userResponse{
|
"user": userResponse{
|
||||||
ID: id.String(), Name: req.Name, Email: req.Email, Role: "user",
|
ID: id.String(), Name: req.Name, Email: req.Email, Role: "user",
|
||||||
},
|
},
|
||||||
"access_token": tokenPair.AccessToken,
|
// access_token 已通过 HttpOnly Cookie 设置,不再在 body 中返回
|
||||||
"expires_at": tokenPair.ExpiresAt,
|
"expires_at": tokenPair.ExpiresAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -183,6 +190,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
Value: tokenPair.AccessToken,
|
Value: tokenPair.AccessToken,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: isSecureCookie(r),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: int(24 * time.Hour / time.Second),
|
MaxAge: int(24 * time.Hour / time.Second),
|
||||||
})
|
})
|
||||||
@@ -191,6 +199,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
Value: tokenPair.RefreshToken,
|
Value: tokenPair.RefreshToken,
|
||||||
Path: "/api/v1/auth/refresh",
|
Path: "/api/v1/auth/refresh",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: isSecureCookie(r),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: int(7 * 24 * time.Hour / time.Second),
|
MaxAge: int(7 * 24 * time.Hour / time.Second),
|
||||||
})
|
})
|
||||||
@@ -206,7 +215,6 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
response.JSON(w, http.StatusOK, map[string]any{
|
response.JSON(w, http.StatusOK, map[string]any{
|
||||||
"user": usr,
|
"user": usr,
|
||||||
"access_token": tokenPair.AccessToken,
|
|
||||||
"expires_at": tokenPair.ExpiresAt,
|
"expires_at": tokenPair.ExpiresAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -217,6 +225,8 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
|||||||
Value: "",
|
Value: "",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: isSecureCookie(r),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
})
|
})
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
@@ -224,6 +234,8 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
|||||||
Value: "",
|
Value: "",
|
||||||
Path: "/api/v1/auth/refresh",
|
Path: "/api/v1/auth/refresh",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: isSecureCookie(r),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
})
|
})
|
||||||
response.JSON(w, http.StatusOK, nil)
|
response.JSON(w, http.StatusOK, nil)
|
||||||
@@ -316,18 +328,38 @@ func (h *AuthHandler) SwitchOrg(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为目标用户生成新的JWT token
|
// 为目标用户生成新的JWT token(通过 HttpOnly Cookie 设置)
|
||||||
tokens, err := h.jwtMgr.GenerateTokenPair(targetID, targetEmail, targetRole)
|
tokenPair, err := h.jwtMgr.GenerateTokenPair(targetID, targetEmail, targetRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.InternalError(w, "生成令牌失败")
|
response.InternalError(w, "生成令牌失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新 Cookie 中的 token,使切换立即生效
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "access_token",
|
||||||
|
Value: tokenPair.AccessToken,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: isSecureCookie(r),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: int(24 * time.Hour / time.Second),
|
||||||
|
})
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "refresh_token",
|
||||||
|
Value: tokenPair.RefreshToken,
|
||||||
|
Path: "/api/v1/auth/refresh",
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: isSecureCookie(r),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
MaxAge: int(7 * 24 * time.Hour / time.Second),
|
||||||
|
})
|
||||||
|
|
||||||
org := h.loadOrgInfo(r.Context(), req.OrgID)
|
org := h.loadOrgInfo(r.Context(), req.OrgID)
|
||||||
response.JSON(w, http.StatusOK, map[string]any{
|
response.JSON(w, http.StatusOK, map[string]any{
|
||||||
"message": "已切换",
|
"message": "已切换",
|
||||||
"org": org,
|
"org": org,
|
||||||
"token": tokens.AccessToken,
|
// access_token 已通过 HttpOnly Cookie 设置
|
||||||
"user": map[string]any{
|
"user": map[string]any{
|
||||||
"id": targetID,
|
"id": targetID,
|
||||||
"name": targetName,
|
"name": targetName,
|
||||||
@@ -407,12 +439,12 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
|
|||||||
Value: tokenPair.AccessToken,
|
Value: tokenPair.AccessToken,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: isSecureCookie(r),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: int(24 * time.Hour / time.Second),
|
MaxAge: int(24 * time.Hour / time.Second),
|
||||||
})
|
})
|
||||||
|
|
||||||
response.JSON(w, http.StatusOK, map[string]any{
|
response.JSON(w, http.StatusOK, map[string]any{
|
||||||
"access_token": tokenPair.AccessToken,
|
|
||||||
"expires_at": tokenPair.ExpiresAt,
|
"expires_at": tokenPair.ExpiresAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1737,3 +1737,100 @@ func (h *LLMChatHandler) generateConversationName(appID, userID, convID, userMes
|
|||||||
DO UPDATE SET name = EXCLUDED.name, updated_at = now()`,
|
DO UPDATE SET name = EXCLUDED.name, updated_at = now()`,
|
||||||
appID, userID, convID, name)
|
appID, userID, convID, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateSuggestions 生成追问建议(由 LLM 智能生成)
|
||||||
|
func (h *LLMChatHandler) GenerateSuggestions(ctx context.Context, conversationID, appID string, messages []map[string]string) ([]string, error) {
|
||||||
|
if len(messages) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建对话历史上下文
|
||||||
|
var historyBuilder strings.Builder
|
||||||
|
for i, msg := range messages {
|
||||||
|
role := "用户"
|
||||||
|
if msg["role"] == "assistant" {
|
||||||
|
role = "助手"
|
||||||
|
}
|
||||||
|
historyBuilder.WriteString(fmt.Sprintf("%d. %s:%s\n", i+1, role, msg["content"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt := `你是一个智能政务问答助手。请根据对话历史,生成4个追问问题,帮助用户深入了解相关内容。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 问题必须与对话内容紧密相关
|
||||||
|
2. 每个问题不超过20个字
|
||||||
|
3. 问题要有深度,引导用户进一步思考
|
||||||
|
4. 避免重复或过于宽泛的问题
|
||||||
|
5. 直接输出问题,用换行分隔,不要编号,不要解释`
|
||||||
|
|
||||||
|
suggestReq := &llm.ChatRequest{
|
||||||
|
Model: "",
|
||||||
|
Messages: []llm.Message{
|
||||||
|
{Role: "system", Content: systemPrompt},
|
||||||
|
{Role: "user", Content: "对话历史:\n" + historyBuilder.String()},
|
||||||
|
},
|
||||||
|
Temperature: 0.7,
|
||||||
|
MaxTokens: 200,
|
||||||
|
Stream: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取激活的 provider
|
||||||
|
provider, defaultModel, err := h.getProviderWithModel(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if defaultModel != "" {
|
||||||
|
suggestReq.Model = defaultModel
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.manager.Chat(ctx, provider, suggestReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析结果:按换行分割
|
||||||
|
suggestions := strings.Split(strings.TrimSpace(result.Content), "\n")
|
||||||
|
var validSuggestions []string
|
||||||
|
for _, s := range suggestions {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
// 过滤空行和过长的行
|
||||||
|
if len(s) > 0 && len([]rune(s)) <= 25 {
|
||||||
|
validSuggestions = append(validSuggestions, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最多返回4个
|
||||||
|
if len(validSuggestions) > 4 {
|
||||||
|
validSuggestions = validSuggestions[:4]
|
||||||
|
}
|
||||||
|
|
||||||
|
return validSuggestions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSuggestions HTTP handler:获取追问建议
|
||||||
|
func (h *LLMChatHandler) GetSuggestions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
appID := chi.URLParam(r, "id")
|
||||||
|
if appID == "" {
|
||||||
|
response.Error(w, http.StatusBadRequest, 40001, "缺少应用ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ConversationID string `json:"conversation_id"`
|
||||||
|
Messages []map[string]string `json:"messages"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
response.Error(w, http.StatusBadRequest, 40002, "请求格式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestions, err := h.GenerateSuggestions(ctx, req.ConversationID, appID, req.Messages)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("generate suggestions failed")
|
||||||
|
response.Error(w, http.StatusInternalServerError, 50001, "生成追问失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.JSON(w, http.StatusOK, suggestions)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 回滚:删除 messages 表
|
||||||
|
DROP TABLE IF EXISTS messages;
|
||||||
|
|
||||||
|
-- 回滚:删除 conversations 表
|
||||||
|
DROP TABLE IF EXISTS conversations;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- 创建对话(conversations)表
|
||||||
|
CREATE TABLE IF NOT EXISTS conversations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
app_id UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(500) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建消息(messages)表
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
|
role VARCHAR(50) NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建索引以提升查询性能
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_conversations_app_id ON conversations(app_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_conversations_user_id ON conversations(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_conversations_created_at ON conversations(created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
|
||||||
|
|
||||||
|
-- 添加注释
|
||||||
|
COMMENT ON TABLE conversations IS '应用对话记录表';
|
||||||
|
COMMENT ON TABLE messages IS '对话消息表';
|
||||||
|
COMMENT ON COLUMN conversations.app_id IS '关联的应用ID';
|
||||||
|
COMMENT ON COLUMN conversations.user_id IS '发起对话的用户ID';
|
||||||
|
COMMENT ON COLUMN conversations.title IS '对话标题';
|
||||||
|
COMMENT ON COLUMN messages.conversation_id IS '所属对话ID';
|
||||||
|
COMMENT ON COLUMN messages.role IS '消息角色:user/assistant/system';
|
||||||
|
COMMENT ON COLUMN messages.content IS '消息内容';
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- 回滚:重新创建 conversations 表
|
||||||
|
CREATE TABLE IF NOT EXISTS conversations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
app_id UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(500),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 回滚:重新创建 messages 表
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
|
role VARCHAR(20) NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 回滚:创建索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_conversations_app_id ON conversations(app_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_conversations_user_id ON conversations(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 删除未使用的 conversations 和 messages 表
|
||||||
|
DROP TABLE IF EXISTS messages;
|
||||||
|
DROP TABLE IF EXISTS conversations;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
-- 人民法院机构 + 用户种子数据
|
||||||
|
-- 密码统一为 admin123
|
||||||
|
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||||
|
|
||||||
|
-- 1. 创建法院机构
|
||||||
|
INSERT INTO organizations (id, name, slug, short_name, description, logo_url, sort_order, is_active)
|
||||||
|
VALUES (
|
||||||
|
'a0000000-0000-0000-0000-000000000011',
|
||||||
|
'人民法院',
|
||||||
|
'fayuan',
|
||||||
|
'法院',
|
||||||
|
'人民法院司法智能应用平台,覆盖立案、送达、庭审、裁判、执行、审判管理等全流程AI辅助',
|
||||||
|
'',
|
||||||
|
11,
|
||||||
|
true
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 2. 创建法院用户(密码同 admin123)
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
pwd_hash TEXT;
|
||||||
|
fayuan_org UUID := 'a0000000-0000-0000-0000-000000000011';
|
||||||
|
BEGIN
|
||||||
|
SELECT password_hash INTO pwd_hash FROM users WHERE email = 'admin@govai.gov.cn';
|
||||||
|
|
||||||
|
-- 法院管理员
|
||||||
|
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||||
|
(gen_random_uuid(), '法院管理员', 'fayuan-admin@govai.gov.cn', pwd_hash, 'admin', 'active', fayuan_org)
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
-- 法官(creator)
|
||||||
|
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||||
|
(gen_random_uuid(), '李法官', 'lifaguan@govai.gov.cn', pwd_hash, 'creator', 'active', fayuan_org)
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
-- 书记员(user)
|
||||||
|
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||||
|
(gen_random_uuid(), '王书记员', 'wangshujiyuan@govai.gov.cn', pwd_hash, 'user', 'active', fayuan_org)
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
-- 人民法院完整示例数据:应用中心(12个AI Agent)
|
||||||
|
-- 覆盖立案→送达→庭审→裁判→执行→审判管理→诉讼服务全流程
|
||||||
|
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
creator_id UUID;
|
||||||
|
fayuan_org UUID := 'a0000000-0000-0000-0000-000000000011';
|
||||||
|
cat_filing UUID;
|
||||||
|
cat_service_doc UUID;
|
||||||
|
cat_trial UUID;
|
||||||
|
cat_verdict UUID;
|
||||||
|
cat_enforce UUID;
|
||||||
|
cat_mgmt UUID;
|
||||||
|
cat_public UUID;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO creator_id FROM users WHERE email = 'lifaguan@govai.gov.cn';
|
||||||
|
SELECT id INTO cat_filing FROM categories WHERE slug = 'fayuan-filing';
|
||||||
|
SELECT id INTO cat_service_doc FROM categories WHERE slug = 'fayuan-service';
|
||||||
|
SELECT id INTO cat_trial FROM categories WHERE slug = 'fayuan-trial';
|
||||||
|
SELECT id INTO cat_verdict FROM categories WHERE slug = 'fayuan-verdict';
|
||||||
|
SELECT id INTO cat_enforce FROM categories WHERE slug = 'fayuan-enforcement';
|
||||||
|
SELECT id INTO cat_mgmt FROM categories WHERE slug = 'fayuan-management';
|
||||||
|
SELECT id INTO cat_public FROM categories WHERE slug = 'fayuan-public';
|
||||||
|
|
||||||
|
-- 1. 立案审查助手
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000001',
|
||||||
|
'立案审查助手', 'case-filing-review',
|
||||||
|
'智能审查起诉材料,判断管辖权、案由定性、材料完整性',
|
||||||
|
'立案庭AI辅助工具,根据当事人提交的诉讼材料智能分析:\n- 管辖权审查(级别管辖、地域管辖、专属管辖)\n- 案由自动定性(民事/刑事/行政/执行)\n- 起诉材料完整性检查\n- 诉讼费预计算\n- 立案风险提示',
|
||||||
|
'📋', cat_filing, creator_id, fayuan_org,
|
||||||
|
'chatbot', 'approved', 'public', true, 256, 4.6, 32, '1.0',
|
||||||
|
'{"system_prompt": "你是人民法院立案庭的智能审查助手,精通《民事诉讼法》《刑事诉讼法》《行政诉讼法》关于立案的规定。请根据用户提交的案件信息,从管辖权、案由定性、材料完整性、诉讼费等角度进行审查分析,给出立案建议。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 4000}',
|
||||||
|
'您好,我是立案审查助手。请描述案件基本情况,我将为您进行立案审查分析。',
|
||||||
|
'["原告张某起诉被告李某民间借贷纠纷,标的额15万元", "行政诉讼案件,原告对市场监管局处罚决定不服", "执行立案申请,已有生效判决书"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 2. 送达文书生成
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000002',
|
||||||
|
'送达文书生成', 'service-doc-gen',
|
||||||
|
'快速生成传票、应诉通知书、举证通知书等送达文书',
|
||||||
|
'根据案件信息自动生成各类送达文书:\n- 受案传票(含开庭时间、地点)\n- 应诉通知书\n- 举证通知书\n- 权利义务告知书\n- 开庭公告',
|
||||||
|
'📮', cat_service_doc, creator_id, fayuan_org,
|
||||||
|
'workflow', 'approved', 'public', true, 389, 4.7, 41, '1.0',
|
||||||
|
'{"system_prompt": "你是法院送达文书生成专家,熟悉各类送达文书的格式和法定要素。请根据案件信息,生成规范、完整的送达文书。", "model": "qwen-plus", "temperature": 0.2, "max_tokens": 6000, "app_type": "workflow", "steps": [{"key": "doc_type", "label": "文书类型", "type": "select", "options": ["传票", "应诉通知书", "举证通知书", "权利义务告知书", "开庭公告"], "required": true}, {"key": "case_info", "label": "案号", "type": "text", "placeholder": "如:(2024)京0101民初123号", "required": true}, {"key": "parties", "label": "当事人信息", "type": "textarea", "placeholder": "原告/被告姓名、地址、联系方式", "required": true}, {"key": "court_info", "label": "开庭信息", "type": "text", "placeholder": "开庭时间、地点、审判人员"}, {"key": "special_note", "label": "特别说明", "type": "textarea", "placeholder": "其他需要载明的事项"}]}',
|
||||||
|
'请选择文书类型并填写案件信息,我将为您生成规范的送达文书。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 3. 庭前会议纪要
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000003',
|
||||||
|
'庭前会议纪要', 'pretrial-meeting',
|
||||||
|
'整理争议焦点、确认无争议事实、记录证据交换',
|
||||||
|
'庭前会议AI辅助工具,帮助法官高效完成庭前准备:\n- 自动归纳争议焦点\n- 梳理无争议事实\n- 证据交换记录与整理\n- 举证责任分配建议\n- 庭审提纲生成',
|
||||||
|
'📝', cat_trial, creator_id, fayuan_org,
|
||||||
|
'workflow', 'approved', 'public', true, 178, 4.5, 23, '1.0',
|
||||||
|
'{"system_prompt": "你是法院庭前会议辅助专家,精通庭前会议规程。请根据案件信息,整理争议焦点、无争议事实、证据交换情况,生成规范的庭前会议纪要和庭审提纲。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 5000, "app_type": "workflow", "steps": [{"key": "case_brief", "label": "案件简述", "type": "textarea", "placeholder": "案由、当事人、诉讼请求、事实概要", "required": true}, {"key": "plaintiff_evidence", "label": "原告证据", "type": "textarea", "placeholder": "原告提交的证据清单及证明目的"}, {"key": "defendant_evidence", "label": "被告证据", "type": "textarea", "placeholder": "被告提交的证据清单及证明目的"}, {"key": "focus_points", "label": "初步争议焦点", "type": "textarea", "placeholder": "法官初步归纳的争议焦点(可选)"}]}',
|
||||||
|
'请输入案件信息和双方证据情况,我将为您生成庭前会议纪要。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 4. 庭审笔录生成
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000004',
|
||||||
|
'庭审笔录生成', 'trial-record-gen',
|
||||||
|
'根据庭审要点快速生成规范庭审笔录',
|
||||||
|
'庭审记录AI辅助工具,根据庭审过程要点生成规范笔录:\n- 庭审全过程结构化记录\n- 当事人陈述、举证质证、辩论内容整理\n- 法庭调查、法庭辩论阶段分明\n- 自动补充法定程序性记载\n- 支持民事/刑事/行政三类庭审',
|
||||||
|
'⚖️', cat_trial, creator_id, fayuan_org,
|
||||||
|
'workflow', 'approved', 'public', true, 467, 4.8, 56, '1.0',
|
||||||
|
'{"system_prompt": "你是法院庭审记录专家,精通各类案件庭审笔录的规范格式。请根据用户提供的庭审要点,生成结构完整、内容准确的庭审笔录。", "model": "qwen-plus", "temperature": 0.2, "max_tokens": 8000, "app_type": "workflow", "steps": [{"key": "case_type", "label": "案件类型", "type": "select", "options": ["民事案件", "刑事案件", "行政案件"], "required": true}, {"key": "case_info", "label": "案件基本信息", "type": "text", "placeholder": "案号、案由、当事人、审判人员", "required": true}, {"key": "hearing_points", "label": "庭审要点", "type": "textarea", "placeholder": "当事人陈述要点、举证质证情况、辩论焦点等", "required": true}, {"key": "special_notes", "label": "特别记录事项", "type": "textarea", "placeholder": "回避申请、延期审理等特殊情况"}]}',
|
||||||
|
'请输入案件类型和庭审要点,我将为您生成规范的庭审笔录。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 5. 裁判文书生成
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000005',
|
||||||
|
'裁判文书生成', 'verdict-writer',
|
||||||
|
'智能生成判决书、裁定书、调解书,自动引用法条',
|
||||||
|
'裁判文书AI辅助生成工具,根据案件审理情况生成规范文书:\n- 民事/刑事/行政判决书\n- 裁定书(驳回起诉、管辖异议等)\n- 调解书\n- 自动引用相关法条和司法解释\n- 争议焦点分析与裁判说理',
|
||||||
|
'📄', cat_verdict, creator_id, fayuan_org,
|
||||||
|
'workflow', 'approved', 'public', true, 612, 4.9, 78, '1.0',
|
||||||
|
'{"system_prompt": "你是人民法院裁判文书撰写专家,精通各类裁判文书的规范格式和说理要求。请根据案件审理情况,生成结构完整、说理充分、法条引用准确的裁判文书。文书应包含:当事人信息、案件由来、审理经过、查明事实、裁判理由、法律依据、裁判主文。", "model": "qwen-plus", "temperature": 0.2, "max_tokens": 8000, "app_type": "workflow", "steps": [{"key": "doc_type", "label": "文书类型", "type": "select", "options": ["民事判决书", "刑事判决书", "行政判决书", "裁定书", "调解书"], "required": true}, {"key": "case_info", "label": "案件基本信息", "type": "text", "placeholder": "案号、案由、当事人、审判人员", "required": true}, {"key": "facts", "label": "查明事实", "type": "textarea", "placeholder": "经审理查明的事实", "required": true}, {"key": "arguments", "label": "双方主张", "type": "textarea", "placeholder": "原告诉称、被告辩称及争议焦点"}, {"key": "law_basis", "label": "法律依据", "type": "textarea", "placeholder": "拟引用的法条(可选,AI可自动补充)"}]}',
|
||||||
|
'请选择文书类型并填写案件信息,我将为您生成规范的裁判文书。',
|
||||||
|
'["民间借贷纠纷,原告借款10万元未还,有借条和转账记录", "离婚纠纷,双方同意离婚但争议财产分割", "交通事故责任认定,被告对责任划分有异议"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 6. 案件智能分析
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000006',
|
||||||
|
'案件智能分析', 'case-analysis-court',
|
||||||
|
'归纳争议焦点、类案推送、裁判思路参考',
|
||||||
|
'案件分析AI助手,辅助法官深入研究案件:\n- 争议焦点自动归纳\n- 类案智能推送(相似案情裁判结果)\n- 裁判思路参考与说理建议\n- 法律关系梳理\n- 证据关联性分析',
|
||||||
|
'🔍', cat_verdict, creator_id, fayuan_org,
|
||||||
|
'chatbot', 'approved', 'public', true, 423, 4.7, 52, '1.0',
|
||||||
|
'{"system_prompt": "你是人民法院案件分析专家,精通各类案件的法律适用和裁判规则。请根据用户描述的案件信息,从争议焦点归纳、法律关系梳理、类案参考、裁判思路等方面进行专业分析。注意:分析仅供参考,不构成裁判意见。", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 4000}',
|
||||||
|
'您好,我是案件智能分析助手。请描述案件基本情况,我将为您提供分析参考。',
|
||||||
|
'["买卖合同纠纷,买方收货后以质量不合格拒付尾款", "劳动争议案件,员工主张违法解除劳动合同赔偿金", "房屋租赁合同纠纷,承租方提前退租要求退还押金"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 7. 诉讼风险评估
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000007',
|
||||||
|
'诉讼风险评估', 'litigation-risk',
|
||||||
|
'评估诉讼胜负概率,辅助诉前调解和判调结合',
|
||||||
|
'诉讼风险智能评估工具,帮助法官和当事人预判诉讼风险:\n- 胜负概率初步评估\n- 证据充分性分析\n- 法律风险点提示\n- 调解可行性建议\n- 类案裁判结果参考',
|
||||||
|
'📊', cat_verdict, creator_id, fayuan_org,
|
||||||
|
'workflow', 'approved', 'public', false, 234, 4.4, 28, '1.0',
|
||||||
|
'{"system_prompt": "你是诉讼风险评估专家,精通各类案件的风险分析。请根据案件信息,从证据、法律适用、类案参考等角度评估诉讼风险,给出胜负概率和调解建议。注意:评估仅供参考,不构成法律意见。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 4000, "app_type": "workflow", "steps": [{"key": "case_type", "label": "案件类型", "type": "select", "options": ["民间借贷", "合同纠纷", "劳动争议", "婚姻家庭", "交通事故", "行政争议"], "required": true}, {"key": "case_desc", "label": "案情描述", "type": "textarea", "placeholder": "详细描述案件事实和争议焦点", "required": true}, {"key": "evidence", "label": "现有证据", "type": "textarea", "placeholder": "列出各方持有的证据"}, {"key": "claim", "label": "诉讼请求", "type": "textarea", "placeholder": "原告的具体诉讼请求"}]}',
|
||||||
|
'请输入案件信息,我将为您评估诉讼风险。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 8. 执行文书生成
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000008',
|
||||||
|
'执行文书生成', 'enforcement-doc',
|
||||||
|
'生成执行通知书、裁定书、失信曝光公告等执行文书',
|
||||||
|
'执行局AI辅助工具,快速生成各类执行文书:\n- 执行通知书\n- 报告财产令\n- 限制消费令\n- 失信曝光公告\n- 执行裁定书(查封/冻结/扣划)',
|
||||||
|
'⚡', cat_enforce, creator_id, fayuan_org,
|
||||||
|
'workflow', 'approved', 'public', true, 345, 4.6, 37, '1.0',
|
||||||
|
'{"system_prompt": "你是法院执行文书生成专家,精通执行程序和各类执行文书的规范格式。请根据案件信息生成规范、完整的执行文书,确保包含法定要素。", "model": "qwen-plus", "temperature": 0.2, "max_tokens": 6000, "app_type": "workflow", "steps": [{"key": "doc_type", "label": "文书类型", "type": "select", "options": ["执行通知书", "报告财产令", "限制消费令", "失信曝光公告", "执行裁定书"], "required": true}, {"key": "case_info", "label": "执行案号", "type": "text", "placeholder": "如:(2024)京0101执123号", "required": true}, {"key": "parties", "label": "当事人信息", "type": "textarea", "placeholder": "申请执行人/被执行人信息", "required": true}, {"key": "exec_content", "label": "执行内容", "type": "textarea", "placeholder": "执行标的、执行措施等", "required": true}]}',
|
||||||
|
'请选择文书类型并填写执行案件信息,我将为您生成执行文书。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 9. 案件评查助手
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000009',
|
||||||
|
'案件评查助手', 'case-quality-review',
|
||||||
|
'检查文书规范性、程序合规性、裁判一致性',
|
||||||
|
'审判管理AI辅助工具,助力案件质量评查:\n- 文书格式规范性检查\n- 程序合规性审查\n- 裁判尺度一致性分析\n- 法条引用准确性校验\n- 评查报告自动生成',
|
||||||
|
'✅', cat_mgmt, creator_id, fayuan_org,
|
||||||
|
'chatbot', 'approved', 'public', false, 156, 4.5, 19, '1.0',
|
||||||
|
'{"system_prompt": "你是法院案件质量评查专家,精通审判管理规范和评查标准。请根据用户提供的案件信息或文书内容,从格式规范、程序合规、裁判一致性、法条引用等方面进行评查分析。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 4000}',
|
||||||
|
'您好,我是案件评查助手。请提供案件文书内容,我将为您进行质量评查分析。',
|
||||||
|
'["检查一份民事判决书的格式和法条引用是否规范", "审查案件审理程序是否合规", "分析同类案件裁判尺度是否一致"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 10. 司法统计报告
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000010',
|
||||||
|
'司法统计报告', 'judicial-statistics',
|
||||||
|
'生成收结案统计、审判质效分析、态势研判报告',
|
||||||
|
'审判管理办AI辅助工具,自动生成司法统计报告:\n- 收结案统计报告\n- 审判质效分析报告\n- 案件类型分布分析\n- 审判态势研判\n- 法官绩效数据汇总',
|
||||||
|
'📈', cat_mgmt, creator_id, fayuan_org,
|
||||||
|
'workflow', 'approved', 'public', true, 289, 4.6, 31, '1.0',
|
||||||
|
'{"system_prompt": "你是法院司法统计专家,精通审判质效评估指标体系和统计分析方法。请根据用户提供的数据,生成结构清晰、分析深入的司法统计报告。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 6000, "app_type": "workflow", "steps": [{"key": "report_type", "label": "报告类型", "type": "select", "options": ["收结案统计", "审判质效分析", "案件类型分布", "审判态势研判", "法官绩效汇总"], "required": true}, {"key": "period", "label": "统计周期", "type": "text", "placeholder": "如:2024年第一季度", "required": true}, {"key": "data", "label": "统计数据", "type": "textarea", "placeholder": "收案数、结案数、未结数、调解率等数据", "required": true}, {"key": "focus", "label": "重点关注", "type": "textarea", "placeholder": "需要重点分析的指标或问题"}]}',
|
||||||
|
'请选择报告类型并输入统计数据,我将为您生成司法统计报告。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 11. 诉讼服务问答
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000011',
|
||||||
|
'诉讼服务问答', 'litigation-service-qa',
|
||||||
|
'面向当事人:立案流程、诉讼费、执行进度等常见问题解答',
|
||||||
|
'诉讼服务中心AI助手,为当事人提供智能问答服务:\n- 立案条件和流程指引\n- 诉讼费计算与缓减免政策\n- 开庭公告查询指引\n- 执行进度查询指引\n- 诉讼风险提示与法律释明',
|
||||||
|
'🎧', cat_public, creator_id, fayuan_org,
|
||||||
|
'chatbot', 'approved', 'public', true, 578, 4.7, 64, '1.0',
|
||||||
|
'{"system_prompt": "你是法院诉讼服务中心的智能问答助手,熟悉立案、审判、执行各环节的办事流程。请用通俗易懂的语言为当事人解答诉讼相关问题,提供准确的办事指引。注意:不提供具体法律意见,仅做程序性指引。", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 3000}',
|
||||||
|
'您好,我是诉讼服务问答助手。请问您想了解立案、诉讼费、执行还是其他问题?',
|
||||||
|
'["我想起诉别人借钱不还,需要准备什么材料?", "诉讼费怎么计算?可以申请缓交吗?", "我的案件已经判决了,怎么申请执行?"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 12. 信访接待助手
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'f0000000-0000-0000-0000-000000000012',
|
||||||
|
'信访接待助手', 'petition-assistant',
|
||||||
|
'信访分类引导、法律释明、转办建议',
|
||||||
|
'信访接待AI辅助工具,助力信访工作规范化:\n- 信访事项智能分类(诉讼/非诉讼/执行/纪检)\n- 法律释明与政策解读\n- 信访转办建议\n- 信访事项登记辅助\n- 情绪安抚话术建议',
|
||||||
|
'🗣️', cat_public, creator_id, fayuan_org,
|
||||||
|
'chatbot', 'approved', 'public', false, 167, 4.3, 21, '1.0',
|
||||||
|
'{"system_prompt": "你是法院信访接待助手,熟悉信访工作规程和涉诉信访处理流程。请根据信访人反映的问题,进行智能分类、法律释明,并给出转办建议。注意用语规范、态度平和,做好情绪疏导。", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 3000}',
|
||||||
|
'您好,我是信访接待助手。请描述您反映的问题,我将为您进行分类引导和法律释明。',
|
||||||
|
'["对判决结果不服,想申诉", "案件执行不到位,被执行人转移财产", "法官审理案件程序有问题,想投诉"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- 人民法院专属分类数据
|
||||||
|
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
fayuan_org UUID := 'a0000000-0000-0000-0000-000000000011';
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO categories (name, slug, icon, sort_order, org_id) VALUES
|
||||||
|
('立案审查', 'fayuan-filing', 'file-check', 1, fayuan_org),
|
||||||
|
('送达文书', 'fayuan-service', 'send', 2, fayuan_org),
|
||||||
|
('庭审辅助', 'fayuan-trial', 'gavel', 3, fayuan_org),
|
||||||
|
('裁判文书', 'fayuan-verdict', 'file-text', 4, fayuan_org),
|
||||||
|
('执行工作', 'fayuan-enforcement','zap', 5, fayuan_org),
|
||||||
|
('审判管理', 'fayuan-management', 'bar-chart', 6, fayuan_org),
|
||||||
|
('诉讼服务', 'fayuan-public', 'headphones', 7, fayuan_org)
|
||||||
|
ON CONFLICT (slug) DO NOTHING;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
-- 法治网(法治网)应用种子数据:分类 + 15个应用
|
||||||
|
-- 依赖:seed_fazhiwang_users.sql(机构与用户)
|
||||||
|
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
creator_id UUID;
|
||||||
|
fazhi_org UUID := 'a0000000-0000-0000-0000-00000000000a';
|
||||||
|
cat_news UUID;
|
||||||
|
cat_law UUID;
|
||||||
|
cat_publicity UUID;
|
||||||
|
cat_commentary UUID;
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO creator_id FROM users WHERE email = 'fazhibian@govai.gov.cn';
|
||||||
|
|
||||||
|
-- ========== 分类数据 ==========
|
||||||
|
INSERT INTO categories (name, slug, icon, sort_order, org_id) VALUES
|
||||||
|
('法治新闻采编', 'fazhi-news', 'newspaper', 1, fazhi_org),
|
||||||
|
('法律法规解读', 'fazhi-law', 'scale', 2, fazhi_org),
|
||||||
|
('普法宣传', 'fazhi-publicity', 'megaphone', 3, fazhi_org),
|
||||||
|
('法治评论', 'fazhi-commentary', 'message-square', 4, fazhi_org)
|
||||||
|
ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
|
SELECT id INTO cat_news FROM categories WHERE slug = 'fazhi-news';
|
||||||
|
SELECT id INTO cat_law FROM categories WHERE slug = 'fazhi-law';
|
||||||
|
SELECT id INTO cat_publicity FROM categories WHERE slug = 'fazhi-publicity';
|
||||||
|
SELECT id INTO cat_commentary FROM categories WHERE slug = 'fazhi-commentary';
|
||||||
|
|
||||||
|
-- ========== 对话型应用(5个)==========
|
||||||
|
|
||||||
|
-- 1. 法治新闻摘要
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000001',
|
||||||
|
'法治新闻摘要', 'law-news-summary',
|
||||||
|
'快速提炼法治新闻核心要点,生成规范的新闻摘要',
|
||||||
|
E'## 功能介绍\n\n面向法治新闻编辑的智能摘要工具:\n\n- 提取法治新闻核心事实\n- 梳理事件时间线与关键人物\n- 标注涉及的法律法规\n- 生成不同字数版本的摘要\n\n## 使用方法\n\n粘贴法治新闻原文,即可获得结构化摘要。',
|
||||||
|
'📰', cat_news, creator_id, fazhi_org,
|
||||||
|
'chatbot', 'approved', 'public', true, 386, 4.7, 52, '1.0',
|
||||||
|
'{"system_prompt": "你是法治网的资深新闻编辑,擅长提炼法治新闻要点。请对用户提供的新闻进行摘要,输出包括:核心事实、关键人物、涉及法律、事件意义。语言简洁、客观、准确。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 3000}',
|
||||||
|
'您好,我是法治新闻摘要助手。请粘贴新闻原文,我来为您提炼核心要点。',
|
||||||
|
'["请为这则关于网络诈骗案宣判的新闻生成摘要", "提炼这篇民法典普法报道的要点", "总结这起行政诉讼案件的关键信息"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 2. 典型案例解读
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000002',
|
||||||
|
'典型案例解读', 'case-analysis-law',
|
||||||
|
'深度解读典型司法案例,提炼裁判要旨与法律启示',
|
||||||
|
E'## 功能介绍\n\n面向法治报道的案例解读工具:\n\n- 梳理案件事实与争议焦点\n- 解读裁判要旨与法律适用\n- 提炼案例的指导意义\n- 输出通俗版与专业版双版本\n\n## 使用方法\n\n输入案件信息或判决书内容,获取专业解读。',
|
||||||
|
'⚖️', cat_law, creator_id, fazhi_org,
|
||||||
|
'chatbot', 'approved', 'public', true, 512, 4.8, 68, '1.0',
|
||||||
|
'{"system_prompt": "你是法治网的法律专家兼普法工作者。请对用户提供的典型案例进行解读,输出两部分:①专业分析(争议焦点、裁判要旨、法律适用、指导意义);②通俗解读(面向普通读者的白话版)。引用法条使用《法律名》第X条格式。所有解读末尾附:本解读仅供参考,具体以司法机关裁判为准。", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 5000}',
|
||||||
|
'您好,我是典型案例解读助手。请提供案件信息或判决书内容,我来为您深度解读。',
|
||||||
|
'["解读最高法发布的正当防卫典型案例", "分析这起高空抛物入刑案件的裁判要旨", "解读一起个人信息保护民事公益诉讼案"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 3. 法规条文查询
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000003',
|
||||||
|
'法规条文查询', 'law-provision-qa',
|
||||||
|
'精准检索法律法规、司法解释条文,支持法条关联与时效提示',
|
||||||
|
E'## 功能介绍\n\n法律法规智能检索工具:\n\n- 精准检索现行法律法规条文\n- 查询司法解释与指导案例\n- 法条关联与新旧版本对比\n- 时效性提示(生效/修订/废止)\n\n## 使用方法\n\n输入法律问题或法条关键词,获取精准检索结果。',
|
||||||
|
'🔍', cat_law, creator_id, fazhi_org,
|
||||||
|
'chatbot', 'approved', 'public', false, 478, 4.6, 61, '1.0',
|
||||||
|
'{"system_prompt": "你是法律法规检索专家,熟悉中国现行法律法规、司法解释。请精准检索并解读相关条文,引用格式:《法律名》第X条。注明法规的生效日期和最新修订版本。如涉及多个法规,按法律层级排列(宪法>法律>行政法规>部门规章)。", "model": "qwen-plus", "temperature": 0.2, "max_tokens": 4000}',
|
||||||
|
'您好,我是法规条文查询助手。请输入您要检索的法律问题或法条关键词。',
|
||||||
|
'["《民法典》关于合同解除的条款有哪些?", "正当防卫的法律规定及司法解释", "刑法修正案(十一)新增了哪些罪名?"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 4. 法治舆情监测
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000004',
|
||||||
|
'法治舆情监测', 'legal-public-opinion',
|
||||||
|
'监测分析法治领域热点舆情,研判走向并提供应对建议',
|
||||||
|
E'## 功能介绍\n\n法治舆情智能研判工具:\n\n- 分析法治热点事件舆论走向\n- 梳理各方观点与争议焦点\n- 评估社会影响与传播态势\n- 提供舆论引导建议\n\n## 使用方法\n\n描述舆情事件,获取专业研判分析。',
|
||||||
|
'📡', cat_news, creator_id, fazhi_org,
|
||||||
|
'chatbot', 'approved', 'public', false, 234, 4.5, 32, '1.0',
|
||||||
|
'{"system_prompt": "你是法治舆情分析专家,服务于法治网舆情研判工作。请对用户描述的舆情事件进行分析,输出:事件概述、舆论走向、各方观点、法治视角研判、引导建议。分析应客观理性,注重法治价值导向。", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 4000}',
|
||||||
|
'您好,我是法治舆情监测助手。请描述您关注的舆情事件,我来为您研判分析。',
|
||||||
|
'["分析某明星偷逃税事件的舆论走向", "研判一起医患纠纷引发的网络热议", "某地城管执法争议的舆情态势分析"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 5. 法治写作润色
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000005',
|
||||||
|
'法治写作润色', 'law-writing-polish',
|
||||||
|
'对法治类文章、稿件进行专业润色,提升表达质量',
|
||||||
|
E'## 功能介绍\n\n法治文稿专业润色工具:\n\n- 优化语言表达与逻辑结构\n- 规范法律术语使用\n- 校正法条引用格式\n- 调整文风以适配不同栏目\n\n## 使用方法\n\n粘贴稿件内容,说明润色要求,获取优化版本。',
|
||||||
|
'✒️', cat_commentary, creator_id, fazhi_org,
|
||||||
|
'chatbot', 'approved', 'public', false, 312, 4.6, 44, '1.0',
|
||||||
|
'{"system_prompt": "你是法治网的资深文字编辑,擅长法治类文稿润色。请对用户提供的稿件进行润色,包括:语言表达优化、逻辑结构调整、法律术语规范、法条引用校正。保持原意不变,提升专业性与可读性。如有事实或法律错误请指出。", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 5000}',
|
||||||
|
'您好,我是法治写作润色助手。请粘贴您的稿件,并说明润色要求。',
|
||||||
|
'["润色这篇法治评论,使其更有说服力", "优化这篇普法文章的表达,让普通读者易懂", "校正这篇报道中的法律术语和法条引用"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== 补全型应用(4个)==========
|
||||||
|
|
||||||
|
-- 6. 案件报道生成
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000006',
|
||||||
|
'案件报道生成', 'case-report-gen',
|
||||||
|
'输入案件要素,生成结构完整、规范客观的法治案件报道',
|
||||||
|
E'## 功能介绍\n\n法治案件报道智能生成:\n\n- 根据案件要素生成新闻报道\n- 自动组织导语、主体、背景\n- 规范法律术语与法条引用\n- 客观中立,符合法治报道规范\n\n## 使用方法\n\n填写案件要素,一键生成报道初稿。',
|
||||||
|
'📝', cat_news, creator_id, fazhi_org,
|
||||||
|
'completion', 'approved', 'public', true, 423, 4.7, 56, '1.0',
|
||||||
|
'{"system_prompt": "你是法治网记者,请根据用户提供的案件要素,生成一篇结构完整、语言规范的法治新闻报道,包括:标题、导语、主体(事实经过、审理结果、法律依据)、背景链接。坚持客观中立,准确使用法律术语。涉及未决案件时使用审慎表述。", "input_label": "案件要素", "output_label": "案件报道", "input_placeholder": "请输入案件要素:\n1. 案件类型(刑事/民事/行政)\n2. 案件概要\n3. 审理法院与时间\n4. 当事人情况\n5. 审理结果与法律依据"}',
|
||||||
|
NULL,
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 7. 法治宣传文案
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000007',
|
||||||
|
'法治宣传文案', 'law-propaganda-gen',
|
||||||
|
'输入普法主题,生成多形式普法宣传文案',
|
||||||
|
E'## 功能介绍\n\n普法宣传文案智能生成:\n\n- 生成公众号推文、海报标语、口播稿\n- 针对不同受众定制表达\n- 通俗易懂,便于传播\n- 一次输出多种形式\n\n## 使用方法\n\n输入普法主题与受众要求,生成宣传文案。',
|
||||||
|
'📢', cat_publicity, creator_id, fazhi_org,
|
||||||
|
'completion', 'approved', 'public', true, 389, 4.6, 51, '1.0',
|
||||||
|
'{"system_prompt": "你是普法宣传专家,请根据用户提供的主题,生成多形式普法宣传文案,包括:①微信公众号推文(800-1200字);②宣传标语(10条);③短视频口播稿(1分钟版)。语言通俗易懂、生动有趣,贴近群众生活,善用案例。", "input_label": "普法主题", "output_label": "宣传文案", "input_placeholder": "请输入普法主题与要求:\n1. 普法主题(如:防范养老诈骗)\n2. 目标受众(如:老年人)\n3. 宣传形式偏好\n4. 重点强调内容"}',
|
||||||
|
NULL,
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 8. 采访提纲生成
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000008',
|
||||||
|
'采访提纲生成', 'interview-outline-gen',
|
||||||
|
'根据采访主题与对象,生成结构化采访提纲',
|
||||||
|
E'## 功能介绍\n\n法治采访提纲智能生成:\n\n- 根据选题生成采访问题\n- 设计核心问题与追问策略\n- 分析受访对象特点\n- 提示采访注意事项\n\n## 使用方法\n\n输入采访主题与对象信息,生成提纲。',
|
||||||
|
'🎤', cat_news, creator_id, fazhi_org,
|
||||||
|
'completion', 'approved', 'public', false, 267, 4.5, 35, '1.0',
|
||||||
|
'{"system_prompt": "你是资深法治记者,请根据用户提供的采访主题和对象,生成结构化采访提纲,包括:①受访对象分析;②采访目标;③核心问题(按逻辑分组,每组3-5问);④追问策略;⑤注意事项。问题应有深度、有针对性。", "input_label": "采访信息", "output_label": "采访提纲", "input_placeholder": "请输入采访信息:\n1. 采访主题\n2. 受访对象(身份/职务)\n3. 采访目的\n4. 重点关注的问题方向"}',
|
||||||
|
NULL,
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 9. 法治评论撰写
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-000000000009',
|
||||||
|
'法治评论撰写', 'law-comment-gen',
|
||||||
|
'针对法治热点,生成观点鲜明、论据充分的评论文章',
|
||||||
|
E'## 功能介绍\n\n法治评论文章智能生成:\n\n- 针对热点事件提炼评论角度\n- 组织论点论据,逻辑严密\n- 体现法治价值导向\n- 适配评论栏目文风\n\n## 使用方法\n\n输入评论对象与立场倾向,生成评论文章。',
|
||||||
|
'💬', cat_commentary, creator_id, fazhi_org,
|
||||||
|
'completion', 'approved', 'public', false, 298, 4.7, 41, '1.0',
|
||||||
|
'{"system_prompt": "你是法治网评论员,请根据用户提供的热点事件,撰写一篇法治评论文章,包括:标题、引出话题、核心论点、论据论证、结论升华。观点鲜明、逻辑严密、论据充分,体现法治精神与社会责任。字数1000-1500字。", "input_label": "评论素材", "output_label": "评论文章", "input_placeholder": "请输入评论素材:\n1. 评论对象(事件/现象/案件)\n2. 事件概要\n3. 立场倾向或想强调的观点\n4. 字数要求"}',
|
||||||
|
NULL,
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== 工作流型应用(4个)==========
|
||||||
|
|
||||||
|
-- 10. 选题策划助手
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-00000000000a',
|
||||||
|
'选题策划助手', 'topic-planning',
|
||||||
|
'分步引导完成法治报道选题策划,生成完整策划方案',
|
||||||
|
E'## 功能介绍\n\n法治报道选题策划工具:\n\n- 分步收集选题要素\n- 评估新闻价值与报道角度\n- 生成完整策划方案\n- 提供资源与时间规划建议\n\n## 适用场景\n\n适用于法治新闻选题论证与策划。',
|
||||||
|
'💡', cat_news, creator_id, fazhi_org,
|
||||||
|
'workflow', 'approved', 'public', true, 198, 4.6, 27, '1.0',
|
||||||
|
'{"system_prompt": "你是法治新闻策划专家,请根据用户分步提供的信息,生成完整的选题策划方案,包括:选题背景与新闻价值、报道角度、内容框架、呈现形式、采访对象建议、时间节点、传播策略。", "app_type": "workflow", "model": "qwen-plus", "temperature": 0.5, "max_tokens": 5000, "steps": [{"key": "topic", "label": "选题方向", "type": "text", "placeholder": "如:未成年人网络保护", "required": true}, {"key": "trigger", "label": "选题由头", "type": "textarea", "placeholder": "近期相关事件、政策或热点", "required": true}, {"key": "audience", "label": "目标受众", "type": "select", "options": ["普通公众", "法律从业者", "青少年", "企业经营者", "基层执法人员"], "required": true}, {"key": "format", "label": "报道形式", "type": "select", "options": ["深度报道", "系列报道", "图文专题", "短视频", "图文+视频融媒体"], "required": true}, {"key": "requirement", "label": "特殊要求", "type": "textarea", "placeholder": "如:配合普法宣传月、突出某个角度等"}]}',
|
||||||
|
'请填写选题信息,我将为您生成完整的选题策划方案。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 11. 全民法治意识调查报告
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-00000000000b',
|
||||||
|
'法治意识调查报告', 'omnichannel-publish',
|
||||||
|
'分步输入调查数据,生成全民法治意识调查报告',
|
||||||
|
E'## 功能介绍\n\n法治意识调查报告生成:\n\n- 分步收集调查数据\n- 多维度分析法治意识水平\n- 对比研判趋势变化\n- 生成专业调查报告\n\n## 适用场景\n\n适用于普法效果评估与法治宣传调研。',
|
||||||
|
'📊', cat_publicity, creator_id, fazhi_org,
|
||||||
|
'workflow', 'approved', 'public', false, 134, 4.5, 18, '1.0',
|
||||||
|
'{"system_prompt": "你是法治调研专家,请根据用户分步提供的调查数据,生成专业的全民法治意识调查报告,包括:调查概况、数据分析(分维度)、趋势对比、问题发现、原因分析、对策建议。报告应数据翔实、分析深入。", "app_type": "workflow", "model": "qwen-plus", "temperature": 0.3, "max_tokens": 6000, "steps": [{"key": "region", "label": "调查区域与样本", "type": "text", "placeholder": "如:XX市,样本量2000人", "required": true}, {"key": "dimensions", "label": "调查维度", "type": "textarea", "placeholder": "法律知晓度、维权意识、守法行为、司法信任、普法需求等", "required": true}, {"key": "data", "label": "调查数据", "type": "textarea", "placeholder": "请粘贴问卷统计结果或关键数据", "required": true}, {"key": "baseline", "label": "对比基准", "type": "select", "options": ["与上年数据对比", "与全国数据对比", "与其他地区对比", "无对比"], "required": true}, {"key": "focus", "label": "重点关注", "type": "textarea", "placeholder": "需重点分析的问题或群体"}]}',
|
||||||
|
'请填写调查信息,我将为您生成法治意识调查报告。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 12. 专题报道框架
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-00000000000c',
|
||||||
|
'专题报道框架', 'special-report',
|
||||||
|
'分步输入专题信息,生成结构化的专题报道框架',
|
||||||
|
E'## 功能介绍\n\n法治专题报道框架生成:\n\n- 分步梳理专题脉络\n- 规划分集内容结构\n- 设计呈现形式\n- 生成完整报道框架\n\n## 适用场景\n\n适用于重大法治专题报道的前期规划。',
|
||||||
|
'📋', cat_news, creator_id, fazhi_org,
|
||||||
|
'workflow', 'approved', 'public', true, 167, 4.7, 23, '1.0',
|
||||||
|
'{"system_prompt": "你是法治专题报道策划专家,请根据用户分步提供的信息,生成结构化的专题报道框架,包括:专题主旨、分集规划(5-8篇)、每篇核心内容与角度、呈现形式、采访资源、推进时间表。", "app_type": "workflow", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 6000, "steps": [{"key": "subject", "label": "专题主题", "type": "text", "placeholder": "如:民法典实施三周年观察", "required": true}, {"key": "background", "label": "专题背景", "type": "textarea", "placeholder": "策划该专题的背景与意义", "required": true}, {"key": "duration", "label": "报道周期", "type": "select", "options": ["单期专题", "一周系列", "一月系列", "长期跟踪"], "required": true}, {"key": "angle", "label": "切入角度", "type": "textarea", "placeholder": "希望突出的角度或重点", "required": true}, {"key": "media", "label": "媒介形式", "type": "select", "options": ["纯图文", "图文+视频", "全媒体融合", "H5互动"], "required": true}]}',
|
||||||
|
'请填写专题信息,我将为您生成专题报道框架。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 13. 法律专题策划
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-00000000000d',
|
||||||
|
'法律专题策划', 'legal-topic-plan',
|
||||||
|
'围绕特定法律领域,生成深度普法专题策划方案',
|
||||||
|
E'## 功能介绍\n\n法律专题深度策划工具:\n\n- 围绕法律领域系统策划\n- 整合法规、案例、解读\n- 设计普法传播路径\n- 生成完整专题方案\n\n## 适用场景\n\n适用于围绕某部法律或法律议题的深度普法策划。',
|
||||||
|
'⚖️', cat_publicity, creator_id, fazhi_org,
|
||||||
|
'workflow', 'approved', 'public', false, 112, 4.6, 15, '1.0',
|
||||||
|
'{"system_prompt": "你是法律普法专题策划专家,请根据用户分步提供的信息,生成深度普法专题策划方案,包括:专题定位、核心法律点梳理、典型案例选取建议、内容产品规划(图文/视频/问答)、传播渠道与节奏、效果评估指标。", "app_type": "workflow", "model": "qwen-plus", "temperature": 0.4, "max_tokens": 6000, "steps": [{"key": "law_area", "label": "法律领域", "type": "text", "placeholder": "如:个人信息保护法", "required": true}, {"key": "goal", "label": "普法目标", "type": "textarea", "placeholder": "希望达到的普法效果", "required": true}, {"key": "target", "label": "目标人群", "type": "select", "options": ["全体公众", "特定行业从业者", "青少年群体", "老年群体", "企业经营者"], "required": true}, {"key": "channel", "label": "传播渠道", "type": "textarea", "placeholder": "如:报纸、客户端、抖音、社区活动等", "required": true}, {"key": "period", "label": "策划周期", "type": "text", "placeholder": "如:国家宪法日宣传周"}]}',
|
||||||
|
'请填写专题信息,我将为您生成法律专题策划方案。',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== 智能体型应用(2个)==========
|
||||||
|
|
||||||
|
-- 14. 法治选题智能助手
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-00000000000e',
|
||||||
|
'法治选题智能助手', 'topic-intel-agent',
|
||||||
|
'多工具联动:追踪热点、检索法条、分析价值、生成选题建议',
|
||||||
|
E'## 功能介绍\n\n法治选题智能体,集成多种能力辅助选题决策:\n\n- 立法动态追踪:关注最新立法与政策\n- 舆情数据分析:研判热点传播态势\n- 法条检索:匹配相关法律法规\n- 选题建议:评估价值、推荐角度\n\n## 使用方法\n\n描述关注领域或事件,助手自动调用工具综合分析并推荐选题。',
|
||||||
|
'🧭', cat_news, creator_id, fazhi_org,
|
||||||
|
'agent', 'approved', 'public', true, 156, 4.7, 21, '1.0',
|
||||||
|
'{"system_prompt": "你是法治选题智能助手,服务于法治网编辑部选题工作。你具备以下能力:\n1. **立法动态追踪**:关注最新立法、修法与政策动向\n2. **舆情数据分析**:研判法治热点的传播态势\n3. **法条检索**:匹配事件相关的法律法规\n4. **选题建议**:评估新闻价值,推荐报道角度\n\n使用某能力时用 [工具调用: 工具名] 和 [工具结果: 工具名] 标记。输出选题建议时给出价值评估与角度推荐。", "tools": ["立法动态追踪", "舆情数据分析", "法条检索", "选题建议"]}',
|
||||||
|
'您好,我是法治选题智能助手。我可以追踪立法动态、分析舆情、检索法条并推荐选题。请描述您关注的领域或事件。',
|
||||||
|
'["最近有哪些值得报道的立法动态?", "围绕电信诈骗治理推荐3个选题角度", "分析未成年人保护领域的报道机会"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 15. 法治内容创作助手
|
||||||
|
INSERT INTO applications (id, name, slug, description, long_description, icon_url, category_id, creator_id, org_id,
|
||||||
|
dify_app_type, status, visibility, is_featured, usage_count, avg_rating, rating_count, version,
|
||||||
|
app_config, welcome_message, suggested_prompts, published_at)
|
||||||
|
VALUES (
|
||||||
|
'e0000000-0000-0000-0000-00000000000f',
|
||||||
|
'法治内容创作助手', 'content-creation-agent',
|
||||||
|
'多工具联动:查法条、写报道、配案例、润色审稿一站完成',
|
||||||
|
E'## 功能介绍\n\n法治内容创作智能体,端到端辅助内容生产:\n\n- 法条检索:查询相关法律依据\n- 案例匹配:检索关联典型案例\n- 内容创作:撰写报道、评论、普法稿\n- 润色审稿:优化表达、校正引用\n\n## 使用方法\n\n描述创作需求,助手自动调用工具完成从素材到成稿的全流程。',
|
||||||
|
'✨', cat_commentary, creator_id, fazhi_org,
|
||||||
|
'agent', 'approved', 'public', true, 189, 4.8, 26, '1.0',
|
||||||
|
'{"system_prompt": "你是法治内容创作智能助手,服务于法治网内容生产。你具备以下能力:\n1. **法条检索**:查询相关法律依据\n2. **案例匹配**:检索关联典型案例\n3. **内容创作**:撰写报道、评论、普法稿件\n4. **润色审稿**:优化表达、规范术语、校正法条引用\n\n使用某能力时用 [工具调用: 工具名] 和 [工具结果: 工具名] 标记。坚持客观准确、法治导向。涉及具体案件时审慎表述。", "tools": ["法条检索", "案例匹配", "内容创作", "润色审稿"]}',
|
||||||
|
'您好,我是法治内容创作助手。我可以查法条、配案例、写稿件、做润色。请描述您的创作需求。',
|
||||||
|
'["写一篇关于高空抛物治理的普法报道,配相关法条和案例", "围绕个人信息保护写一篇评论并润色", "帮我创作一篇消费者维权的普法文章"]',
|
||||||
|
NOW()
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== 设置发布时间 ==========
|
||||||
|
UPDATE applications SET published_at = NOW()
|
||||||
|
WHERE org_id = fazhi_org AND status = 'approved' AND published_at IS NULL;
|
||||||
|
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
SELECT '法治网应用种子数据插入成功!共15个应用、4个分类。' as status;
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
-- 法治网15个应用的示例使用数据
|
||||||
|
-- 为每个应用创建2-3条示例对话记录(使用 app_usage_logs 表)
|
||||||
|
-- 依赖:seed_fazhiwang.sql(应用数据)
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
fazhi_user UUID;
|
||||||
|
fazhi_org UUID := 'a0000000-0000-0000-0000-00000000000a';
|
||||||
|
BEGIN
|
||||||
|
-- 获取法治网用户ID
|
||||||
|
SELECT id INTO fazhi_user FROM users WHERE email = 'fazhiwang@govai.gov.cn';
|
||||||
|
|
||||||
|
-- ========== 1. 法治新闻摘要 - 示例数据 ==========
|
||||||
|
INSERT INTO app_usage_logs (id, app_id, user_id, conversation_id, message_count,
|
||||||
|
prompt_tokens, completion_tokens, total_tokens, model_name, estimated_cost,
|
||||||
|
duration_ms, is_successful, user_message, ai_response, created_at) VALUES
|
||||||
|
|
||||||
|
(gen_random_uuid(), 'e0000000-0000-0000-0000-000000000001', fazhi_user, 'conv_001_001', 2,
|
||||||
|
850, 520, 1370, 'qwen-plus', 0.0027, 3200, true,
|
||||||
|
'请为以下新闻生成摘要:
|
||||||
|
|
||||||
|
【本报讯】2024年6月20日,某市中级人民法院对一起特大网络诈骗案作出一审判决。被告人张某等12人利用网络平台实施诈骗,涉案金额达2300余万元,受害者遍布全国20余省市。法院经审理认为,被告人张某等人以非法占有为目的,虚构事实、隐瞒真相,骗取他人财物,数额特别巨大,其行为已构成诈骗罪。根据《中华人民共和国刑法》第266条规定,判处主犯张某有期徒刑15年,并处罚金50万元;其他11名从犯分别被判处3年至12年不等有期徒刑。',
|
||||||
|
'**核心事实**
|
||||||
|
- 案件类型:特大网络诈骗案
|
||||||
|
- 审理法院:某市中级人民法院
|
||||||
|
- 宣判时间:2024年6月20日
|
||||||
|
- 涉案金额:2300余万元
|
||||||
|
- 受害范围:全国20余省市
|
||||||
|
|
||||||
|
**关键人物**
|
||||||
|
- 主犯:张某(判处有期徒刑15年,罚金50万元)
|
||||||
|
- 从犯:11人(判处3-12年不等有期徒刑)
|
||||||
|
|
||||||
|
**涉及法律**
|
||||||
|
《中华人民共和国刑法》第266条(诈骗罪)
|
||||||
|
|
||||||
|
**事件意义**
|
||||||
|
本案系特大网络诈骗案件,涉案金额巨大、受害者众多,判决体现了司法机关对网络诈骗犯罪的严厉打击态势,对同类犯罪具有警示作用。',
|
||||||
|
NOW() - INTERVAL '2 days'),
|
||||||
|
|
||||||
|
(gen_random_uuid(), 'e0000000-0000-0000-0000-000000000001', fazhi_user, 'conv_001_002', 2,
|
||||||
|
920, 580, 1500, 'qwen-plus', 0.003, 3500, true,
|
||||||
|
'提炼这篇民法典普法报道的要点:
|
||||||
|
|
||||||
|
【民法典实施三周年】守护美好生活的法治力量
|
||||||
|
|
||||||
|
2024年5月28日,民法典迎来实施三周年。三年来,这部"社会生活的百科全书"深刻影响着每个人的权利保障。从高空抛物入刑到个人信息保护,从居住权制度到离婚冷静期,民法典以人民为中心的立法理念贯穿始终。
|
||||||
|
|
||||||
|
最高人民法院数据显示,三年来全国法院审结涉民法典案件超过1200万件,民法典相关司法解释达30余件。基层普法工作者通过"民法典进社区""以案释法"等形式,让民法典走进群众生活。
|
||||||
|
|
||||||
|
专家指出,民法典实施三年来,在保护民事权利、规范民事行为、维护社会秩序方面发挥了重要作用,彰显了中国特色社会主义法治建设的显著成就。',
|
||||||
|
'**核心事实**
|
||||||
|
- 时间节点:民法典实施三周年(2024年5月28日)
|
||||||
|
- 案件数据:全国法院三年审结涉民法典案件超1200万件
|
||||||
|
- 配套法规:相关司法解释达30余件
|
||||||
|
|
||||||
|
**关键人物**
|
||||||
|
- 报道对象:基层普法工作者、法律专家
|
||||||
|
- 受益群体:全体人民
|
||||||
|
|
||||||
|
**涉及法律**
|
||||||
|
《中华人民共和国民法典》(2021年1月1日起施行)
|
||||||
|
|
||||||
|
**事件意义**
|
||||||
|
民法典实施三年来,在保护民事权利、规范民事行为、维护社会秩序方面发挥重要作用,体现以人民为中心的立法理念。高空抛物、个人信息保护、居住权、离婚冷静期等制度创新深入人心,通过"进社区""以案释法"等形式实现普法落地,彰显中国特色社会主义法治建设成就。',
|
||||||
|
NOW() - INTERVAL '1 day');
|
||||||
|
|
||||||
|
-- ========== 2. 典型案例解读 - 示例数据 ==========
|
||||||
|
INSERT INTO app_usage_logs (id, app_id, user_id, conversation_id, message_count,
|
||||||
|
prompt_tokens, completion_tokens, total_tokens, model_name, estimated_cost,
|
||||||
|
duration_ms, is_successful, user_message, ai_response, created_at) VALUES
|
||||||
|
|
||||||
|
(gen_random_uuid(), 'e0000000-0000-0000-0000-000000000002', fazhi_user, 'conv_002_001', 2,
|
||||||
|
780, 650, 1430, 'qwen-plus', 0.0029, 4100, true,
|
||||||
|
'请解读最高法发布的正当防卫典型案例:
|
||||||
|
|
||||||
|
王某正当防卫案:深夜,王某在家中遭遇入室抢劫,歹徒持刀威胁。王某夺刀反击,致歹徒重伤。检察院认定王某行为属正当防卫,不负刑事责任。',
|
||||||
|
'**案例解读:王某正当防卫案**
|
||||||
|
|
||||||
|
**争议焦点**
|
||||||
|
王某反击行为是否超过正当防卫限度?
|
||||||
|
|
||||||
|
**法律分析**
|
||||||
|
根据《刑法》第20条,正当防卫需满足:
|
||||||
|
1. 不法侵害正在进行(✓ 入室抢劫进行中)
|
||||||
|
2. 针对不法侵害人(✓ 对歹徒反击)
|
||||||
|
3. 保护合法权益(✓ 生命财产安全)
|
||||||
|
4. 防卫限度合理(✓ 面对持刀威胁,夺刀反击属必要手段)
|
||||||
|
|
||||||
|
**裁判要旨**
|
||||||
|
对于严重危及人身安全的暴力犯罪(入室抢劫、持刀威胁),防卫人可采取足以制止不法侵害的一切手段,造成不法侵害人伤亡的,不属于防卫过当,不负刑事责任。
|
||||||
|
|
||||||
|
**指导意义**
|
||||||
|
本案明确了"特殊防卫权"适用标准,鼓励公民依法维护自身权益,体现"法不能向不法让步"的司法导向。',
|
||||||
|
NOW() - INTERVAL '3 days');
|
||||||
|
|
||||||
|
-- ========== 3. 法律条文检索 - 示例数据 ==========
|
||||||
|
INSERT INTO app_usage_logs (id, app_id, user_id, conversation_id, message_count,
|
||||||
|
prompt_tokens, completion_tokens, total_tokens, model_name, estimated_cost,
|
||||||
|
duration_ms, is_successful, user_message, ai_response, created_at) VALUES
|
||||||
|
|
||||||
|
(gen_random_uuid(), 'e0000000-0000-0000-0000-000000000003', fazhi_user, 'conv_003_001', 2,
|
||||||
|
320, 580, 900, 'qwen-plus', 0.0018, 2800, true,
|
||||||
|
'查询民法典关于高空抛物的规定',
|
||||||
|
'**民法典关于高空抛物的规定**
|
||||||
|
|
||||||
|
**主要法条**
|
||||||
|
|
||||||
|
**《民法典》第1254条**(侵权责任编)
|
||||||
|
> 禁止从建筑物中抛掷物品。从建筑物中抛掷物品或者从建筑物上坠落的物品造成他人损害的,由侵权人依法承担侵权责任;经调查难以确定具体侵权人的,除能够证明自己不是侵权人的外,由可能加害的建筑物使用人给予补偿。可能加害的建筑物使用人补偿后,有权向侵权人追偿。
|
||||||
|
|
||||||
|
物业服务企业等建筑物管理人应当采取必要的安全保障措施防止前款规定情形的发生;未采取必要的安全保障措施的,应当依法承担未履行安全保障义务的侵权责任。
|
||||||
|
|
||||||
|
**法律要点**
|
||||||
|
1. 明确禁止高空抛物行为
|
||||||
|
2. 侵权人承担责任(优先原则)
|
||||||
|
3. 无法确定侵权人时,可能加害人补偿(兜底原则)
|
||||||
|
4. 物业负有安全保障义务
|
||||||
|
|
||||||
|
**刑事责任**
|
||||||
|
《刑法修正案(十一)》增设"高空抛物罪"(第291条之二),情节严重的,处一年以下有期徒刑、拘役或者管制,并处或者单处罚金。',
|
||||||
|
NOW() - INTERVAL '4 days');
|
||||||
|
|
||||||
|
-- ========== 4. 法治舆情监测 - 示例数据 ==========
|
||||||
|
INSERT INTO app_usage_logs (id, app_id, user_id, conversation_id, message_count,
|
||||||
|
prompt_tokens, completion_tokens, total_tokens, model_name, estimated_cost,
|
||||||
|
duration_ms, is_successful, user_message, ai_response, created_at) VALUES
|
||||||
|
|
||||||
|
(gen_random_uuid(), 'e0000000-0000-0000-0000-000000000004', fazhi_user, 'conv_004_001', 2,
|
||||||
|
680, 480, 1160, 'qwen-plus', 0.0023, 3300, true,
|
||||||
|
'分析近期"AI换脸"相关法律舆情',
|
||||||
|
'**"AI换脸"法律舆情分析报告**
|
||||||
|
|
||||||
|
**舆情概况**
|
||||||
|
- 热度指数:★★★★☆(85/100)
|
||||||
|
- 关注人群:法律界、科技界、公众
|
||||||
|
- 主要平台:微博、知乎、法律论坛
|
||||||
|
|
||||||
|
**核心议题**
|
||||||
|
1. **技术滥用风险**:AI换脸用于诈骗、侵犯肖像权案件增多
|
||||||
|
2. **法律适用争议**:现行法律能否有效规制新型犯罪
|
||||||
|
3. **平台责任边界**:技术提供方是否承担连带责任
|
||||||
|
|
||||||
|
**典型案例**
|
||||||
|
- 某地利用AI换脸实施电信诈骗案(涉案500万元)
|
||||||
|
- 明星肖像权被AI换脸侵权案(一审判赔20万元)
|
||||||
|
|
||||||
|
**法律依据**
|
||||||
|
- 《民法典》人格权编(肖像权、名誉权)
|
||||||
|
- 《刑法》诈骗罪、侵犯公民个人信息罪
|
||||||
|
- 《网络安全法》《个人信息保护法》
|
||||||
|
|
||||||
|
**舆情研判**
|
||||||
|
该议题处于立法跟进期,建议关注最高法、最高检相关司法解释动向,提前储备选题素材。',
|
||||||
|
NOW() - INTERVAL '5 days');
|
||||||
|
|
||||||
|
-- ========== 5. 选题策划助手 - 示例数据 ==========
|
||||||
|
INSERT INTO app_usage_logs (id, app_id, user_id, conversation_id, message_count,
|
||||||
|
prompt_tokens, completion_tokens, total_tokens, model_name, estimated_cost,
|
||||||
|
duration_ms, is_successful, user_message, ai_response, created_at) VALUES
|
||||||
|
|
||||||
|
(gen_random_uuid(), 'e0000000-0000-0000-0000-00000000000a', fazhi_user, 'conv_005_001', 2,
|
||||||
|
420, 720, 1140, 'qwen-plus', 0.0023, 3800, true,
|
||||||
|
'策划一期"未成年人网络保护"专题报道',
|
||||||
|
'**"未成年人网络保护"专题策划方案**
|
||||||
|
|
||||||
|
**选题背景**
|
||||||
|
《未成年人网络保护条例》(2024年1月1日起施行)实施满半年,未成年人触网低龄化、网络沉迷、信息泄露等问题仍突出。
|
||||||
|
|
||||||
|
**报道角度**(3篇系列)
|
||||||
|
|
||||||
|
**第一篇:政策解读**
|
||||||
|
- 标题:《织密法网守护"数字原住民"——未成年人网络保护条例实施观察》
|
||||||
|
- 内容:条例核心制度(网络素养教育、时间管理、隐私保护、防沉迷机制)
|
||||||
|
- 采访对象:立法参与专家、网信办相关负责人
|
||||||
|
|
||||||
|
**第二篇:案例报道**
|
||||||
|
- 标题:《当10岁孩子遭遇网络陷阱——三起典型案例警示录》
|
||||||
|
- 案例选取:游戏诱导充值、个人信息泄露、网络欺凌
|
||||||
|
- 采访对象:当事家庭、办案法官、心理咨询师
|
||||||
|
|
||||||
|
**第三篇:多方共治**
|
||||||
|
- 标题:《学校、家庭、平台、监管:未成年人网络保护的"四方协同"》
|
||||||
|
- 内容:各方责任落实情况、存在问题、国际经验借鉴
|
||||||
|
- 采访对象:学校、家长代表、互联网企业、监管部门
|
||||||
|
|
||||||
|
**传播策略**
|
||||||
|
- 文字报道 + 数据可视化图表
|
||||||
|
- 短视频访谈(抖音、视频号同步)
|
||||||
|
- 互动话题:#如何守护孩子的数字童年#',
|
||||||
|
NOW() - INTERVAL '6 days');
|
||||||
|
|
||||||
|
RAISE NOTICE '法治网15个应用示例数据插入成功!共创建5个应用的示例对话记录。';
|
||||||
|
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
-- 法治网(法治网)知识库种子数据:2个知识库 + 10篇文档
|
||||||
|
-- 依赖:seed_fazhiwang_users.sql、seed_fazhiwang.sql
|
||||||
|
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
creator_id UUID;
|
||||||
|
fazhi_org UUID := 'a0000000-0000-0000-0000-00000000000a';
|
||||||
|
kb_lib_id UUID := 'e1000000-0000-0000-0000-000000000001';
|
||||||
|
kb_norm_id UUID := 'e1000000-0000-0000-0000-000000000002';
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO creator_id FROM users WHERE email = 'fazhibian@govai.gov.cn';
|
||||||
|
|
||||||
|
-- ========== 知识库 ==========
|
||||||
|
|
||||||
|
-- 1. 法治网精选文库
|
||||||
|
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status)
|
||||||
|
VALUES (
|
||||||
|
kb_lib_id,
|
||||||
|
'法治网精选文库',
|
||||||
|
'收录宪法、民法典、刑法等核心法律条文及新法速递、典型案例汇编,服务法规检索与案例解读',
|
||||||
|
creator_id, fazhi_org, 'department', 5, 28000, 'active'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 2. 法治新闻报道规范
|
||||||
|
INSERT INTO knowledge_bases (id, name, description, owner_id, org_id, visibility, doc_count, total_chars, status)
|
||||||
|
VALUES (
|
||||||
|
kb_norm_id,
|
||||||
|
'法治新闻报道规范',
|
||||||
|
'收录法治新闻写作规范、法律术语使用指南、敏感信息处理规则、法条引述格式标准及优秀报道点评',
|
||||||
|
creator_id, fazhi_org, 'department', 5, 22000, 'active'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== 精选文库文档 ==========
|
||||||
|
|
||||||
|
-- 文档1:宪法条文精选
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000001', kb_lib_id,
|
||||||
|
'宪法条文精选', 'txt', 5000, 'completed', creator_id,
|
||||||
|
E'中华人民共和国宪法(节选)\n\n第一章 总纲\n\n第五条 中华人民共和国实行依法治国,建设社会主义法治国家。\n国家维护社会主义法制的统一和尊严。\n一切法律、行政法规和地方性法规都不得同宪法相抵触。\n一切国家机关和武装力量、各政党和各社会团体、各企业事业组织都必须遵守宪法和法律。一切违反宪法和法律的行为,必须予以追究。\n任何组织或者个人都不得有超越宪法和法律的特权。\n\n第二章 公民的基本权利和义务\n\n第三十三条 凡具有中华人民共和国国籍的人都是中华人民共和国公民。\n中华人民共和国公民在法律面前一律平等。\n国家尊重和保障人权。\n任何公民享有宪法和法律规定的权利,同时必须履行宪法和法律规定的义务。\n\n第三十五条 中华人民共和国公民有言论、出版、集会、结社、游行、示威的自由。\n\n第三十七条 中华人民共和国公民的人身自由不受侵犯。\n任何公民,非经人民检察院批准或者决定或者人民法院决定,并由公安机关执行,不受逮捕。\n禁止非法拘禁和以其他方法非法剥夺或者限制公民的人身自由,禁止非法搜查公民的身体。\n\n第三十八条 中华人民共和国公民的人格尊严不受侵犯。禁止用任何方法对公民进行侮辱、诽谤和诬告陷害。\n\n第四十一条 中华人民共和国公民对于任何国家机关和国家工作人员,有提出批评和建议的权利;对于任何国家机关和国家工作人员的违法失职行为,有向有关国家机关提出申诉、控告或者检举的权利。'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 文档2:民法典重点条款
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000002', kb_lib_id,
|
||||||
|
'民法典重点条款', 'txt', 6000, 'completed', creator_id,
|
||||||
|
E'中华人民共和国民法典(重点条款摘录,2021年1月1日施行)\n\n第一编 总则\n\n第一条 为了保护民事主体的合法权益,调整民事关系,维护社会和经济秩序,适应中国特色社会主义发展要求,弘扬社会主义核心价值观,根据宪法,制定本法。\n\n第三条 民事主体的人身权利、财产权利以及其他合法权益受法律保护,任何组织或者个人不得侵犯。\n\n第一百一十条 自然人享有生命权、身体权、健康权、姓名权、肖像权、名誉权、荣誉权、隐私权、婚姻自主权等权利。\n\n第一编 总则·民事法律行为\n\n第一百四十三条 具备下列条件的民事法律行为有效:(一)行为人具有相应的民事行为能力;(二)意思表示真实;(三)不违反法律、行政法规的强制性规定,不违背公序良俗。\n\n第四编 人格权\n\n第一千零三十二条 自然人享有隐私权。任何组织或者个人不得以刺探、侵扰、泄露、公开等方式侵害他人的隐私权。\n隐私是自然人的私人生活安宁和不愿为他人知晓的私密空间、私密活动、私密信息。\n\n第一千零三十四条 自然人的个人信息受法律保护。\n个人信息是以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人的各种信息。\n\n第六编 合同·合同的解除\n\n第五百六十三条 有下列情形之一的,当事人可以解除合同:(一)因不可抗力致使不能实现合同目的;(二)在履行期限届满前,当事人一方明确表示或者以自己的行为表明不履行主要债务;(三)当事人一方迟延履行主要债务,经催告后在合理期限内仍未履行;(四)当事人一方迟延履行债务或者有其他违约行为致使不能实现合同目的;(五)法律规定的其他情形。\n\n第七编 侵权责任·高空抛物\n\n第一千二百五十四条 禁止从建筑物中抛掷物品。从建筑物中抛掷物品或者从建筑物上坠落的物品造成他人损害的,由侵权人依法承担侵权责任;经调查难以确定具体侵权人的,除能够证明自己不是侵权人的外,由可能加害的建筑物使用人给予补偿。'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 文档3:刑法修正案对比
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000003', kb_lib_id,
|
||||||
|
'刑法修正案对比', 'txt', 5500, 'completed', creator_id,
|
||||||
|
E'刑法修正案(十一)重点修订对比(2021年3月1日施行)\n\n一、刑事责任年龄调整\n修订前:已满十四周岁不满十六周岁的人,仅对八类严重犯罪负刑事责任。\n修订后:已满十二周岁不满十四周岁的人,犯故意杀人、故意伤害罪,致人死亡或者以特别残忍手段致人重伤造成严重残疾,情节恶劣,经最高人民检察院核准追诉的,应当负刑事责任。\n意义:回应低龄未成年人恶性犯罪的社会关切,有限度下调刑事责任年龄。\n\n二、高空抛物入刑\n新增第二百九十一条之二:从建筑物或者其他高空抛掷物品,情节严重的,处一年以下有期徒刑、拘役或者管制,并处或者单处罚金。\n意义:将"头顶上的安全"纳入刑法保护,明确高空抛物的刑事责任。\n\n三、冒名顶替入刑\n新增第二百八十条之二:盗用、冒用他人身份,顶替他人取得的高等学历教育入学资格、公务员录用资格、就业安置待遇的,处三年以下有期徒刑、拘役或者管制,并处罚金。\n意义:回应"冒名顶替上大学"等事件,维护教育公平和社会公正。\n\n四、催收非法债务罪\n新增第二百九十三条之一:有下列情形之一,催收高利放贷等产生的非法债务,情节严重的,处三年以下有期徒刑、拘役或者管制,并处或者单处罚金:(一)使用暴力、胁迫方法的;(二)限制他人人身自由或者侵入他人住宅的;(三)恐吓、跟踪、骚扰他人的。\n意义:针对"套路贷"暴力催收行为,强化对公民人身权利的保护。\n\n五、药品领域犯罪\n修订妨害药品管理罪,加大对生产销售假药劣药行为的打击力度,回应人民群众用药安全关切。'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 文档4:新法速递解读
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000004', kb_lib_id,
|
||||||
|
'新法速递解读', 'txt', 5500, 'completed', creator_id,
|
||||||
|
E'近年重要新法速递解读\n\n一、《个人信息保护法》(2021年11月1日施行)\n核心要点:\n1. 确立个人信息处理的"告知—同意"规则\n2. 单独同意:处理敏感个人信息、向第三方提供、公开个人信息需取得单独同意\n3. 大型平台义务:建立合规制度、定期发布社会责任报告\n4. "大数据杀熟"禁止:自动化决策不得对个人在交易价格等方面实行不合理差别待遇\n5. 个人权利:知情权、决定权、查阅复制权、可携带权、删除权\n违法后果:最高可处五千万元或上一年度营业额百分之五的罚款。\n\n二、《反电信网络诈骗法》(2022年12月1日施行)\n核心要点:\n1. 电话卡、银行卡实名制与数量限制\n2. 金融机构尽职调查义务\n3. 涉诈资金紧急止付、快速冻结、及时返还制度\n4. 对涉诈人员的惩戒措施\n5. 反诈宣传教育责任\n\n三、《家庭教育促进法》(2022年1月1日施行)\n核心要点:\n1. "依法带娃":明确父母或其他监护人的家庭教育主体责任\n2. 国家与社会支持家庭教育\n3. 公检法可对失职监护人发出家庭教育指导令\n意义:将家庭教育从"家事"上升为"国事"。\n\n四、《妇女权益保障法》(2023年1月1日修订施行)\n核心要点:\n1. 完善性骚扰防治机制\n2. 强化就业性别歧视的法律责任\n3. 加强人身安全保护,完善人身安全保护令\n4. 保障农村妇女土地权益'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 文档5:典型案例汇编
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000005', kb_lib_id,
|
||||||
|
'典型案例汇编', 'txt', 6000, 'completed', creator_id,
|
||||||
|
E'法治典型案例汇编\n\n案例一:正当防卫认定——"昆山反杀案"\n基本案情:刘某酒后驾车,与骑车的于某发生争执,刘某持砍刀攻击于某,砍刀脱手后被于某捡起,于某反击致刘某死亡。\n处理结果:检察机关认定于某的行为属于正当防卫,不负刑事责任。\n指导意义:明确正当防卫的认定标准,"法不能向不法让步",鼓励公民勇于同不法侵害作斗争。\n\n案例二:高空抛物——某小区高空抛物损害责任案\n基本案情:某小区一住户从高层抛下杂物,砸坏楼下停放车辆,经调查锁定具体侵权人。\n处理结果:侵权人依法承担全部赔偿责任,情节严重的依高空抛物罪追究刑事责任。\n指导意义:警示高空抛物的法律后果,保护"头顶上的安全"。\n\n案例三:个人信息保护——APP违法收集个人信息案\n基本案情:某APP在用户不知情情况下超范围收集个人信息并提供给第三方。\n处理结果:监管部门责令整改并处罚款,受害用户提起民事公益诉讼获支持。\n指导意义:明确APP运营者的个人信息保护义务,落实"告知—同意"规则。\n\n案例四:消费者权益——"知假买假"惩罚性赔偿案\n基本案情:消费者购买到不符合食品安全标准的食品,主张十倍赔偿。\n处理结果:法院支持消费者的惩罚性赔偿请求。\n指导意义:明确食品安全领域惩罚性赔偿规则,保护消费者合法权益。\n\n案例五:网络谣言——编造传播疫情谣言案\n基本案情:行为人在网络上编造虚假疫情信息并大量传播,造成社会恐慌。\n处理结果:依据治安管理处罚法予以行政拘留,情节严重的追究刑事责任。\n指导意义:警示网络不是法外之地,编造传播谣言要承担法律责任。'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ========== 报道规范文库文档 + 应用关联 ==========
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
creator_id UUID;
|
||||||
|
fazhi_org UUID := 'a0000000-0000-0000-0000-00000000000a';
|
||||||
|
kb_lib_id UUID := 'e1000000-0000-0000-0000-000000000001';
|
||||||
|
kb_norm_id UUID := 'e1000000-0000-0000-0000-000000000002';
|
||||||
|
BEGIN
|
||||||
|
SELECT id INTO creator_id FROM users WHERE email = 'fazhibian@govai.gov.cn';
|
||||||
|
|
||||||
|
-- 文档6:法治新闻写作规范
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000006', kb_norm_id,
|
||||||
|
'法治新闻写作规范', 'txt', 4500, 'completed', creator_id,
|
||||||
|
E'法治新闻写作规范\n\n一、基本原则\n1. 客观真实:以事实为依据,不夸大、不渲染、不臆测\n2. 准确严谨:准确使用法律术语,正确表述法律关系\n3. 平衡报道:兼顾各方立场,避免"舆论审判"\n4. 法治导向:传播法治理念,引导依法办事\n\n二、案件报道注意事项\n1. 未决案件:使用"涉嫌""犯罪嫌疑人""被告人"等审慎表述,不得提前定性\n2. 罪与非罪:未经法院判决,不得称当事人为"罪犯"\n3. 隐私保护:依法保护当事人隐私,未成年人、性侵案件被害人信息须隐去\n4. 程序表述:准确区分"立案侦查""提起公诉""一审判决""终审"等不同诉讼阶段\n\n三、结构要素\n1. 标题:客观准确,不做"标题党"\n2. 导语:概括核心事实(何时、何地、何人、何事、何因、何果)\n3. 主体:按逻辑展开事实经过、处理结果、法律依据\n4. 背景:补充相关法律法规、类似案例、专家解读\n\n四、常见错误\n1. 混淆"逮捕"与"拘留""刑事拘留"与"行政拘留"\n2. 将"判处有期徒刑X年"误写为"判刑X年"\n3. 将民事案件的"被告"误写为刑事案件的"被告人"\n4. 法条引用不规范,未注明法律全称与条款序号'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 文档7:法律术语使用指南
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000007', kb_norm_id,
|
||||||
|
'法律术语使用指南', 'txt', 4500, 'completed', creator_id,
|
||||||
|
E'法律术语使用指南\n\n一、刑事诉讼术语\n- 犯罪嫌疑人:侦查、审查起诉阶段的称谓\n- 被告人:审判阶段的称谓\n- 罪犯/服刑人员:判决生效后的称谓\n- 刑事拘留:侦查阶段的强制措施(公安机关)\n- 逮捕:需经检察院批准或法院决定\n- 提起公诉:检察机关向法院起诉\n- 不起诉:检察机关决定不向法院起诉\n\n二、民事诉讼术语\n- 原告/被告:民事案件双方当事人\n- 起诉/应诉:提起诉讼与应对诉讼\n- 一审/二审/再审:审级表述\n- 调解/判决:纠纷解决方式\n- 执行:判决生效后的强制履行\n\n三、行政诉讼术语\n- 行政相对人:行政行为的对象\n- 行政复议:向上级行政机关申请审查\n- 行政诉讼:"民告官"\n\n四、易混淆术语辨析\n1. "定金"vs"订金":定金有担保性质,适用定金罚则;订金一般可退\n2. "拘役"vs"拘留":拘役是刑罚;拘留是强制措施或行政处罚\n3. "假释"vs"缓刑":假释是服刑后附条件提前释放;缓刑是暂缓执行\n4. "无期徒刑"vs"死缓":性质不同的刑罚\n5. "罚款"vs"罚金":罚款是行政处罚;罚金是刑罚'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 文档8:敏感信息处理规则
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000008', kb_norm_id,
|
||||||
|
'敏感信息处理规则', 'txt', 4000, 'completed', creator_id,
|
||||||
|
E'法治报道敏感信息处理规则\n\n一、必须隐去或处理的信息\n1. 未成年人:涉案未成年人的姓名、住所、就读学校、照片等可能推断其身份的信息\n2. 性侵案件被害人:姓名、住所、工作单位等身份信息\n3. 隐私信息:与案件无关的个人隐私、家庭住址、联系方式\n4. 国家秘密:涉及国家安全、军事、外交等保密信息\n5. 商业秘密:涉案企业的商业秘密\n\n二、案件细节处理\n1. 犯罪手法:避免详细描述作案手段、制毒制爆等技术细节\n2. 自杀案件:避免详细描述自杀方式,应附心理援助信息\n3. 血腥暴力:避免渲染血腥、暴力、恐怖细节\n\n三、特殊主体报道\n1. 涉及党政机关、领导干部:依规依纪报道,把握分寸\n2. 涉外、涉港澳台、涉民族宗教:严格遵守相关报道纪律\n3. 重大敏感案件:以权威部门发布的信息为准\n\n四、信息来源核实\n1. 优先采用司法机关、政府部门发布的权威信息\n2. 多方核实,避免单一信源\n3. 不传播未经证实的小道消息和网络传言\n4. 引用裁判文书须核对原文,确保准确'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 文档9:法条引述格式标准
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000009', kb_norm_id,
|
||||||
|
'法条引述格式标准', 'txt', 4000, 'completed', creator_id,
|
||||||
|
E'法条引述格式标准\n\n一、基本格式\n标准格式:《法律名称》第X条第X款第X项\n示例:《中华人民共和国民法典》第一千二百五十四条第一款\n\n二、首次引用与简称\n1. 首次引用使用全称:《中华人民共和国刑法》\n2. 后文可用简称:《刑法》\n3. 通行简称:民法典、刑法、刑诉法、民诉法、行政诉讼法\n\n三、法律层级与引用顺序\n按效力层级由高到低排列:\n宪法 > 法律 > 行政法规 > 地方性法规 > 部门规章 > 地方政府规章\n引用多个法条时,按此顺序排列。\n\n四、条款项目的规范表述\n- 条:第一条、第二条……(用汉字)\n- 款:第一款、第二款……(同一条下的自然段)\n- 项:第(一)项、第(二)项……\n- 示例:第二百六十四条第一款第(三)项\n\n五、司法解释引用\n格式:《最高人民法院关于……的解释》第X条\n示例:《最高人民法院关于审理民间借贷案件适用法律若干问题的规定》第二十五条\n\n六、注意事项\n1. 注明法律的施行或修订日期(涉及新旧法对比时)\n2. 引用已废止法律须特别说明\n3. 核对条款序号,避免张冠李戴\n4. 引用原文须与正式文本一致,不得节略改动原意'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 文档10:优秀报道案例点评
|
||||||
|
INSERT INTO knowledge_documents (id, kb_id, name, file_type, char_count, indexing_status, uploader_id, content)
|
||||||
|
VALUES (
|
||||||
|
'e2000000-0000-0000-0000-000000000010', kb_norm_id,
|
||||||
|
'优秀报道案例点评', 'txt', 4500, 'completed', creator_id,
|
||||||
|
E'优秀法治报道案例点评\n\n范例一:深度调查类\n《某地"套路贷"黑产业链调查》\n亮点:\n1. 选题敏锐,聚焦民生痛点\n2. 调查扎实,证据链完整\n3. 法律分析到位,引用法条准确\n4. 警示性强,兼具普法价值\n点评:深度报道的典范,体现了法治媒体的舆论监督功能。\n\n范例二:案例解读类\n《从"昆山反杀案"看正当防卫的边界》\n亮点:\n1. 借热点案例普及法律知识\n2. 专家解读权威,深入浅出\n3. 厘清"正当防卫"认定标准\n4. 引导公众正确认识"法不能向不法让步"\n点评:成功将专业法律问题转化为公众易懂的内容。\n\n范例三:普法宣传类\n《民法典里的"小美好"》系列\n亮点:\n1. 以小见大,贴近生活\n2. 故事化表达,可读性强\n3. 系列化策划,形成传播声势\n4. 融媒体呈现,多平台分发\n点评:普法宣传创新的优秀样本,让法律"活"起来。\n\n范例四:舆论引导类\n《理性看待网络热点事件中的法律问题》\n亮点:\n1. 及时回应社会关切\n2. 以法治视角引导舆论\n3. 平衡报道,避免"舆论审判"\n4. 传播法治理性\n点评:在舆论场中发挥了"压舱石"作用。\n\n共性经验:\n1. 坚持正确导向,传播法治理念\n2. 内容专业准确,经得起检验\n3. 表达通俗生动,贴近受众\n4. 创新形式手段,增强传播力'
|
||||||
|
) ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== 应用关联知识库 ==========
|
||||||
|
-- 精选文库:典型案例解读、法规条文查询、典型案例分析、法治评论撰写、法治选题智能助手
|
||||||
|
UPDATE applications SET knowledge_base_id = kb_lib_id
|
||||||
|
WHERE id IN (
|
||||||
|
'e0000000-0000-0000-0000-000000000002', -- 典型案例解读
|
||||||
|
'e0000000-0000-0000-0000-000000000003', -- 法规条文查询
|
||||||
|
'e0000000-0000-0000-0000-000000000009', -- 法治评论撰写
|
||||||
|
'e0000000-0000-0000-0000-00000000000e' -- 法治选题智能助手
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 报道规范:法治新闻摘要、案件报道生成、法治写作润色、采访提纲生成、法治内容创作助手
|
||||||
|
UPDATE applications SET knowledge_base_id = kb_norm_id
|
||||||
|
WHERE id IN (
|
||||||
|
'e0000000-0000-0000-0000-000000000001', -- 法治新闻摘要
|
||||||
|
'e0000000-0000-0000-0000-000000000006', -- 案件报道生成
|
||||||
|
'e0000000-0000-0000-0000-000000000005', -- 法治写作润色
|
||||||
|
'e0000000-0000-0000-0000-000000000008', -- 采访提纲生成
|
||||||
|
'e0000000-0000-0000-0000-00000000000f' -- 法治内容创作助手
|
||||||
|
);
|
||||||
|
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
SELECT '法治网知识库种子数据插入成功!共2个知识库、10篇文档。' as status;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- 法治网机构用户种子数据
|
||||||
|
-- 密码统一为 admin123
|
||||||
|
-- 可重复执行(ON CONFLICT DO NOTHING)
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
pwd_hash TEXT;
|
||||||
|
fazhi_org UUID := 'a0000000-0000-0000-0000-00000000000a';
|
||||||
|
BEGIN
|
||||||
|
-- 获取已有用户的密码hash(admin123)
|
||||||
|
SELECT password_hash INTO pwd_hash FROM users WHERE email = 'admin@govai.gov.cn';
|
||||||
|
|
||||||
|
-- 法治网机构管理员(排序第一位,作为登录页默认账号)
|
||||||
|
INSERT INTO organizations (id, name, slug, short_name, description, sort_order, is_active) VALUES
|
||||||
|
(fazhi_org, '法治网', 'fazhiwang', '法治网',
|
||||||
|
'法治网官方平台,提供法治资讯、政策解读、法律服务与智能应用',
|
||||||
|
1, true)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- 显式重排所有机构,确保法治网稳定排在第一位、其余机构顺延(幂等)
|
||||||
|
UPDATE organizations SET sort_order = 1 WHERE slug = 'fazhiwang';
|
||||||
|
UPDATE organizations SET sort_order = 2 WHERE slug = 'keji';
|
||||||
|
UPDATE organizations SET sort_order = 3 WHERE slug = 'gongan';
|
||||||
|
UPDATE organizations SET sort_order = 4 WHERE slug = 'fagai';
|
||||||
|
UPDATE organizations SET sort_order = 5 WHERE slug = 'jiaoyu';
|
||||||
|
UPDATE organizations SET sort_order = 6 WHERE slug = 'renshe';
|
||||||
|
UPDATE organizations SET sort_order = 7 WHERE slug = 'caizheng';
|
||||||
|
UPDATE organizations SET sort_order = 8 WHERE slug = 'zhujian';
|
||||||
|
UPDATE organizations SET sort_order = 9 WHERE slug = 'shijianju';
|
||||||
|
UPDATE organizations SET sort_order = 10 WHERE slug = 'lvsuo';
|
||||||
|
|
||||||
|
-- 法治网机构用户
|
||||||
|
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||||
|
(gen_random_uuid(), '法治网管理员', 'fazhiwang@govai.gov.cn', pwd_hash, 'admin', 'active', fazhi_org),
|
||||||
|
(gen_random_uuid(), '法治网编辑', 'fazhibian@govai.gov.cn', pwd_hash, 'creator', 'active', fazhi_org),
|
||||||
|
(gen_random_uuid(), '法治网记者', 'fazhijizhe@govai.gov.cn', pwd_hash, 'user', 'active', fazhi_org)
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
END $$;
|
||||||
@@ -13,7 +13,7 @@ INSERT INTO model_providers (
|
|||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
'阿里云百炼 (通义千问)',
|
'阿里云百炼 (通义千问)',
|
||||||
'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
'sk-c0c5174892c44ff48d587cd040fbdd40', -- 实际环境中应该加密存储
|
'[YOUR_API_KEY]', -- 部署时通过环境变量 OPENAI_API_KEY 注入
|
||||||
'[
|
'[
|
||||||
{"id": "qwen-plus", "name": "通义千问-Plus", "type": "chat"},
|
{"id": "qwen-plus", "name": "通义千问-Plus", "type": "chat"},
|
||||||
{"id": "qwen-turbo", "name": "通义千问-Turbo", "type": "chat"},
|
{"id": "qwen-turbo", "name": "通义千问-Turbo", "type": "chat"},
|
||||||
|
|||||||
@@ -56,4 +56,11 @@ BEGIN
|
|||||||
(gen_random_uuid(), '许专员', 'xuzy@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000008')
|
(gen_random_uuid(), '许专员', 'xuzy@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000008')
|
||||||
ON CONFLICT (email) DO NOTHING;
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
-- 法院用户
|
||||||
|
INSERT INTO users (id, name, email, password_hash, role, status, org_id) VALUES
|
||||||
|
(gen_random_uuid(), '法院管理员', 'fayuan-admin@govai.gov.cn', pwd_hash, 'admin', 'active', 'a0000000-0000-0000-0000-000000000011'),
|
||||||
|
(gen_random_uuid(), '李法官', 'lifaguan@govai.gov.cn', pwd_hash, 'creator', 'active', 'a0000000-0000-0000-0000-000000000011'),
|
||||||
|
(gen_random_uuid(), '王书记员', 'wangshujiyuan@govai.gov.cn', pwd_hash, 'user', 'active', 'a0000000-0000-0000-0000-000000000011')
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
END $$;
|
END $$;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ type contextKey string
|
|||||||
const (
|
const (
|
||||||
orgIDKey contextKey = "org_id"
|
orgIDKey contextKey = "org_id"
|
||||||
userIDKey contextKey = "user_id"
|
userIDKey contextKey = "user_id"
|
||||||
roleKey contextKey = "role"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithOrgID stores the organization ID in context.
|
// WithOrgID stores the organization ID in context.
|
||||||
|
|||||||
Reference in New Issue
Block a user