public/index.html 智衡 · 基层法院智能辅助系统

精准辅助 · 人机协同 · 全程留痕

工作总览

小程序已连接

基层法院智能辅助系统

把时间留给判断,
把重复工作交给系统。

聚焦证据处理与文书生成,让每条建议有依据、每次修改有记录、每个结论由法官确认。

AI辅助建议
证据提取 · 校验
文书框架 · 说理
法官审核 · 定稿

核心指标

N=54 调研洞察

有效问卷

54覆盖审判一线核心岗位

核心环节耗时

83.33%集中在证据处理与文书生成

边界认同 × 帮助效果

0.715呈较强正相关

最高制度需求

3.83权责划分与全流程留痕

今日工作

双核心任务

4 个案件

制度安全

边界清楚,才会真正好用

3.83全流程留痕制度
认同程度均值 / 5分

一线人员首先关心“出错谁担责、过程能否倒查、机器建议是否经人工确认”。

数据来源:项目组 N=54 有效问卷

制度需求与应用效能

所有图表均来自项目原始调研结果,可按指标查询、切换视图并导出。

制度建设

制度需求重要性排序

权责划分与流程留痕并列首位,说明制度安全是技术落地的前提。

研究结论

不是“什么都做”,而是先做准关键环节

制度越清楚,顾虑越少;顾虑越少,系统的减负效果越容易被感知。
r ≈ 0.715裁判权边界认同与系统帮助效果呈较强正相关
  • 1证据链核验、文书说理与逻辑核验是优先短板。
  • 2机器生成结构与建议,法官审核、修改并定稿。
  • 3来源标注、人工确认和版本留痕必须嵌入流程。

数据查询

制度指标明细

指标分组均值(1-5分)优先级说明

与小程序共享数据

案件工作台

从证据集上传到文书定稿,跨端同步、统一编号、全程可追溯。

在办案件跨端同步
待复核证据4需人工确认
文书草稿3AI 辅助生成
风险提示2矛盾 / 缺失

案件查询

案卷列表

案件案由承办法官证据状态更新时间

提取 · 分类 · 校验 · 梳理 · 留痕

证据链分析

把小程序上传的材料转化为可核查的事实链,并保留每次人工确认。

事实时间轴

请选择案件

AI 初筛

Coze 智能体

司法智能辅助

围绕证据梳理、法条匹配和文书框架提供可追溯建议。

检测中
智能审判助手

你好。我可以协助梳理证据链、识别材料矛盾、整理争议焦点和生成待审核的文书框架。所有输出均为辅助建议,不替代法官判断。

AI 输出将自动标记并写入操作日志

统一数据 · 安全调用 · 全程追溯

小程序与 Coze 对接

配置一次,网页、小程序和智能体在同一案件上下文中协作。

系统架构

四层协同结构

小程序上传证据 · 案卷管理
网页服务查询 · 图表 · 导出
Coze 智能体提取 · 梳理 · 生成
法官确认审核 · 修改 · 定稿

运行配置

连接状态

网页 API检测中
小程序鉴权检测中
Coze 智能体检测中
密钥仅存服务端环境变量,前端源码不包含 Token。

责任追溯

最近操作日志

html2.link ·粘贴 HTML,一键生成链接
举报 public/styles.css :root { --ink: #14213d; --muted: #62708b; --line: #dfe5ee; --paper: #ffffff; --canvas: #f3f6fa; --navy: #11294d; --blue: #1e63e9; --blue-2: #5b8cff; --cyan: #18a4b9; --gold: #d99a2b; --red: #d84d56; --green: #1b8f68; --shadow: 0 14px 34px rgba(18, 39, 71, 0.08); } * { box-sizing: border-box; } html { scroll-behavior: smooth; } body { margin: 0; font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", Arial, sans-serif; color: var(--ink); background: var(--canvas); } button, input, select, textarea { font: inherit; } button { cursor: pointer; } .app-shell { min-height: 100vh; display: grid; grid-template-columns: 252px 1fr; } .sidebar { position: fixed; inset: 0 auto 0 0; width: 252px; padding: 30px 20px 20px; color: #dce8fa; background: linear-gradient(160deg, #10284b 0%, #0b1d37 72%, #09172b 100%); display: flex; flex-direction: column; z-index: 30; } .brand { display: flex; gap: 12px; align-items: center; color: white; text-decoration: none; padding: 0 10px 28px; } .brand-mark { width: 40px; height: 40px; display: grid; place-items: center; border-radius: 12px 12px 12px 4px; background: linear-gradient(135deg, #6fa1ff, #2466eb); font-family: serif; font-weight: 700; box-shadow: 0 8px 20px rgba(38,102,235,.35); } .brand strong { display: block; font-size: 21px; letter-spacing: 4px; } .brand small { display: block; margin-top: 3px; color: #8fa6c6; font-size: 10px; letter-spacing: 1px; } .nav-list { display: grid; gap: 7px; } .nav-item { border: 0; color: #9eb2ce; background: transparent; border-radius: 11px; padding: 13px 14px; text-align: left; display: flex; gap: 13px; align-items: center; transition: .2s; } .nav-item span { width: 24px; text-align: center; font-size: 17px; } .nav-item:hover { color: white; background: rgba(255,255,255,.06); } .nav-item.active { color: white; background: linear-gradient(90deg, rgba(62,118,235,.32), rgba(62,118,235,.12)); box-shadow: inset 3px 0 #75a3ff; } .boundary-card { margin-top: auto; border: 1px solid rgba(153,184,228,.18); background: rgba(255,255,255,.055); padding: 14px; border-radius: 14px; display: flex; gap: 11px; } .boundary-icon { flex: 0 0 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: rgba(108,154,255,.18); color: #9fc0ff; font-family: serif; } .boundary-card strong { font-size: 12px; color: #e7f0ff; } .boundary-card p { margin: 5px 0 0; color: #8298b8; font-size: 10px; line-height: 1.55; } .sidebar-footer { margin-top: 14px; border-top: 1px solid rgba(255,255,255,.08); padding: 15px 8px 0; display: flex; align-items: center; gap: 7px; color: #8095b4; font-size: 10px; } .sidebar-footer small { margin-left: auto; } .status-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; background: #38c48a; box-shadow: 0 0 0 4px rgba(56,196,138,.12); } .main { grid-column: 2; min-width: 0; } .topbar { height: 92px; background: rgba(255,255,255,.92); backdrop-filter: blur(15px); border-bottom: 1px solid var(--line); display: flex; align-items: center; padding: 0 38px; position: sticky; top: 0; z-index: 20; } .eyebrow { margin: 0 0 6px; color: #6f7d94; font-size: 10px; font-weight: 700; letter-spacing: 1.4px; text-transform: uppercase; } .eyebrow.light { color: #a8c7ff; } .topbar h1 { font-size: 21px; margin: 0; letter-spacing: .5px; } .top-actions { margin-left: auto; display: flex; align-items: center; gap: 12px; } .sync-state { border: 1px solid #dce5f1; background: #f8fafc; padding: 9px 13px; border-radius: 10px; color: var(--muted); font-size: 11px; display: flex; align-items: center; gap: 9px; } .avatar-button { width: 38px; height: 38px; border: 0; border-radius: 10px; background: var(--navy); color: white; } .ghost-button, .primary-button, .text-button { border-radius: 9px; padding: 10px 15px; font-weight: 700; font-size: 12px; } .ghost-button { border: 1px solid #d8e0ec; color: #34445e; background: white; } .ghost-button:hover { border-color: #8db0ef; color: var(--blue); } .primary-button { border: 1px solid var(--blue); background: var(--blue); color: white; box-shadow: 0 8px 16px rgba(30,99,233,.16); } .primary-button:hover { background: #1557d5; transform: translateY(-1px); } .primary-button.full { width: 100%; } .text-button { border: 0; background: transparent; color: var(--blue); padding-inline: 0; } .text-button.light { color: white; } .content { padding: 30px 38px 64px; max-width: 1540px; margin: auto; } .view { display: none; animation: viewIn .25s ease; } .view.active { display: block; } @keyframes viewIn { from { opacity: 0; transform: translateY(5px); } } .hero { min-height: 302px; border-radius: 20px; padding: 42px 50px; overflow: hidden; color: white; background: radial-gradient(circle at 76% 20%, rgba(84,142,255,.45), transparent 27%), linear-gradient(135deg, #183b70 0%, #10284e 55%, #0b1c34 100%); display: grid; grid-template-columns: 1.15fr .85fr; box-shadow: 0 18px 42px rgba(12,32,63,.18); } .hero h2 { font-family: "STSong", "SimSun", serif; margin: 12px 0 14px; font-size: 36px; line-height: 1.25; letter-spacing: 2px; } .hero-copy > p:not(.eyebrow) { max-width: 610px; color: #b8c9e1; line-height: 1.8; font-size: 13px; } .hero-actions { display: flex; gap: 24px; align-items: center; margin-top: 25px; } .hero-visual { position: relative; min-height: 220px; } .orbit { position: absolute; border: 1px solid rgba(138,179,250,.25); border-radius: 50%; left: 50%; top: 50%; transform: translate(-50%,-50%); } .orbit-one { width: 245px; height: 165px; } .orbit-two { width: 330px; height: 220px; border-style: dashed; } .core-node { width: 112px; height: 112px; border-radius: 50%; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); display: grid; place-content: center; text-align: center; background: linear-gradient(145deg, #6297f9, #245ed0); box-shadow: 0 0 0 14px rgba(83,137,235,.12), 0 15px 28px rgba(0,0,0,.2); } .core-node span { font-size: 26px; font-weight: 800; } .core-node small { color: #dce8ff; font-size: 9px; } .satellite { position: absolute; min-width: 108px; padding: 10px 13px; border: 1px solid rgba(165,195,242,.25); background: rgba(11,28,52,.77); border-radius: 12px; box-shadow: 0 10px 26px rgba(0,0,0,.18); } .satellite b, .satellite small { display: block; } .satellite b { font-size: 12px; }.satellite small { margin-top: 3px; color: #91a9c9; font-size: 9px; } .satellite-a { left: 5%; top: 21%; }.satellite-b { right: 0; top: 28%; }.satellite-c { left: 35%; bottom: 1%; } .section-heading, .page-lead { display: flex; justify-content: space-between; align-items: end; gap: 20px; } .section-heading { margin: 34px 0 16px; } .section-heading h2, .page-lead h2 { margin: 0; font-size: 22px; } .page-lead { margin: 3px 0 24px; } .page-lead.compact { margin-top: 0; } .page-lead > div > p:not(.eyebrow) { color: var(--muted); font-size: 12px; margin: 8px 0 0; } .metric-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 15px; } .metric-card, .panel { border: 1px solid #e2e7ef; background: var(--paper); border-radius: 15px; box-shadow: var(--shadow); } .metric-card { position: relative; overflow: hidden; padding: 20px; min-height: 148px; } .metric-card::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--accent); } .metric-card p { margin: 1px 0 12px; color: #596880; font-size: 11px; font-weight: 700; } .metric-card strong { font-size: 28px; letter-spacing: -1px; } .metric-card em { font-size: 12px; margin-left: 3px; font-style: normal; } .metric-card small { display: block; margin-top: 10px; color: #8a95a6; font-size: 9px; } .metric-icon { float: right; width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, white); font-family: serif; font-weight: 700; } .accent-blue { --accent: var(--blue); }.accent-cyan { --accent: var(--cyan); }.accent-gold { --accent: var(--gold); }.accent-red { --accent: var(--red); } .overview-grid { display: grid; grid-template-columns: 1.25fr .75fr; gap: 16px; margin-top: 16px; } .panel { padding: 22px; } .panel-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .panel h3 { margin: 0; font-size: 16px; } .tag { padding: 6px 9px; border-radius: 999px; background: #eef4ff; color: var(--blue); font-size: 9px; font-weight: 700; } .tag.warn { color: #a96f09; background: #fff5dc; } .workflow-list { margin-top: 17px; display: grid; gap: 8px; } .workflow-row { border: 1px solid #e6eaf0; background: #fbfcfe; border-radius: 11px; padding: 14px; display: grid; grid-template-columns: 40px 1fr auto 20px; gap: 12px; align-items: center; text-align: left; color: var(--ink); } .workflow-row:hover { border-color: #a9c3ef; background: #f7faff; } .workflow-number { color: #9aabc1; font-size: 10px; } .workflow-row b,.workflow-row small { display: block; }.workflow-row b { font-size: 12px; }.workflow-row small { color: #8a96a9; font-size: 9px; margin-top: 4px; }.workflow-row strong { font-size: 12px; }.workflow-row i { font-style: normal; color: var(--blue); } .trust-panel { background: linear-gradient(150deg,#fff,#f8fbff); } .trust-line { height: 7px; margin: 24px 0 16px; border-radius: 99px; background: #e9eef5; overflow: hidden; }.trust-line span { display:block; height:100%; width:var(--score); border-radius:inherit; background: linear-gradient(90deg,#1f63e9,#60a0ff); } .trust-score { display: flex; gap: 14px; align-items: center; }.trust-score strong { font-size: 34px; color: var(--blue); }.trust-score span { font-size: 11px; font-weight: 700; line-height: 1.5; }.trust-score small { color: #9aa5b6; font-size: 8px; } .trust-panel > p { border-top: 1px solid #e6eaf1; padding-top: 14px; color: #67758a; font-size: 10px; line-height: 1.6; } .segmented { border: 1px solid #dbe2ed; background: white; padding: 4px; border-radius: 10px; display: flex; } .segmented button { border: 0; padding: 8px 12px; border-radius: 7px; color: #6b778a; background: transparent; font-size: 11px; } .segmented button.active { color: white; background: var(--navy); } .research-grid { display: grid; grid-template-columns: 1.45fr .55fr; gap: 16px; } .chart-panel { min-height: 440px; } .icon-button { width: 36px; height: 36px; border-radius: 9px; border: 1px solid #dfe5ee; background: white; color: var(--ink); } .mobile-menu { display: none; margin-right: 14px; } .chart-stage { height: 305px; margin-top: 20px; display: grid; place-items: center; overflow: hidden; } .chart-stage svg { width: 100%; height: 100%; overflow: visible; } .chart-note { margin: 14px 0 0; padding: 11px 13px; color: #5f6d82; background: #f5f8fc; border-left: 3px solid var(--blue); font-size: 10px; line-height: 1.55; } .insight-panel { color: white; background: linear-gradient(155deg,#173761,#102847); border-color: #173761; } .insight-panel .eyebrow { color: #85a9df; }.insight-panel h3 { font-family: "STSong","SimSun",serif; font-size: 23px; line-height: 1.5; } .insight-panel blockquote { margin: 19px 0; padding: 15px 0 15px 18px; border-left: 2px solid #5d8fe4; color: #bcd0ec; font-size: 11px; line-height: 1.7; } .insight-stat { border: 1px solid rgba(255,255,255,.1); border-radius: 10px; padding: 13px; display: flex; gap: 12px; align-items: center; }.insight-stat strong { color: #79a9ff; font-size: 18px; }.insight-stat span { color: #a7bad5; font-size: 9px; line-height: 1.5; } .insight-panel ul { list-style: none; margin: 16px 0 0; padding: 0; display: grid; gap: 12px; }.insight-panel li { display: flex; gap: 10px; color: #aabbd3; font-size: 10px; line-height: 1.5; }.insight-panel li span { flex:0 0 20px; height:20px; display:grid; place-items:center; border-radius:50%; background:rgba(100,153,245,.18); color:#8eb5fb; } .data-panel { margin-top: 16px; padding-bottom: 10px; } .filter-row { display: flex; gap: 8px; } select,input,textarea { border: 1px solid #dbe2eb; border-radius: 8px; background: white; color: var(--ink); outline: none; } select:focus,input:focus,textarea:focus { border-color: #7ba4ea; box-shadow: 0 0 0 3px rgba(30,99,233,.08); } select,input { height: 36px; padding: 0 11px; font-size: 10px; } input[type=search] { min-width: 190px; } .table-wrap { overflow-x: auto; margin-top: 15px; } table { width: 100%; border-collapse: collapse; font-size: 10px; } th { padding: 11px 12px; color: #7c889b; background: #f6f8fb; text-align: left; font-weight: 700; white-space: nowrap; } td { padding: 13px 12px; border-bottom: 1px solid #edf0f4; color: #435068; vertical-align: middle; } tbody tr:hover { background: #fafcff; } .score-bar { display:flex; align-items:center; gap:8px; }.score-bar span { width:80px; height:5px; border-radius:99px; background:#e7ebf2; overflow:hidden; }.score-bar i { display:block; height:100%; background:var(--blue); } .priority { padding: 4px 7px; border-radius: 99px; background:#eef5ff; color:#2360c7; font-weight:700; } .case-stats { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; margin-bottom: 16px; } .case-stats div { border: 1px solid #e1e6ee; background: white; border-radius: 13px; padding: 17px 20px; box-shadow: 0 8px 24px rgba(18,39,71,.05); }.case-stats span,.case-stats small { display:block; }.case-stats span { color:#6f7c8f; font-size:10px; }.case-stats strong { display:block; font-size:26px; margin:8px 0 3px; }.case-stats small { color:#9aa5b4; font-size:8px; }.case-stats .danger { color: var(--red); } .case-title { font-weight:700; color:var(--ink); }.case-title small { display:block; color:#929eaf; font-weight:400; margin-top:4px; } .status-pill,.risk-pill { padding:5px 8px; border-radius:99px; display:inline-block; font-size:9px; font-weight:700; }.status-pill { color:#2767cf; background:#edf4ff; }.risk-pill.high { color:#b93e48;background:#fff0f1; }.risk-pill.medium { color:#9c6a0e;background:#fff6e3; }.risk-pill.low { color:#17815e;background:#eaf8f2; } .row-button { border:0;background:transparent;color:var(--blue);font-size:10px;font-weight:700; } .large-select { min-width:250px; } .evidence-layout { display:grid;grid-template-columns:1fr 310px;gap:16px; } .chain-panel { min-height:520px; } .chain-stage { margin-top:24px; position:relative; } .chain-stage::before { content:""; position:absolute; left:25px; top:18px; bottom:18px; width:1px; background:#cfdae8; } .chain-item { position:relative; display:grid;grid-template-columns:52px 1fr auto;gap:12px;align-items:start;padding:0 0 22px; } .chain-dot { position:relative;z-index:1;width:14px;height:14px;border:3px solid white;border-radius:50%;margin:4px 0 0 19px;background:var(--blue);box-shadow:0 0 0 2px #9ebced; } .chain-item.conflict .chain-dot { background:var(--red);box-shadow:0 0 0 2px #f0a7ac; } .chain-card { border:1px solid #e0e6ef;border-radius:10px;padding:13px;background:#fbfcfe; }.chain-card b{font-size:11px}.chain-card p{margin:6px 0 0;color:#7d899b;font-size:9px}.chain-date{font-size:9px;color:#8895a8;padding-top:4px}.chain-warning{display:block;margin-top:8px;color:var(--red);font-size:9px;font-weight:700} .review-panel label { display:flex;gap:9px;padding:11px 0;border-bottom:1px solid #edf0f4;color:#4d5a70;font-size:10px; }.review-panel input { min-width:15px;height:15px;accent-color:var(--blue); }.warning-box { margin:18px 0;padding:13px;border-radius:10px;background:#fff8e8;border:1px solid #f1dfb4;color:#7c5a18; }.warning-box b{font-size:10px}.warning-box p{font-size:9px;line-height:1.6;margin:5px 0 0} .assistant-layout { display:grid;grid-template-columns:1fr 290px;gap:16px; }.assistant-main{min-width:0}.mode-badge{padding:7px 10px;border-radius:99px;background:#eaf8f2;color:#177b5b;font-size:9px;font-weight:700}.mode-badge i{width:6px;height:6px;border-radius:50%;background:#20ad7e;display:inline-block;margin-right:6px} .chat-panel { border:1px solid #e1e6ee;background:white;border-radius:15px;overflow:hidden;box-shadow:var(--shadow); } .messages { height:420px;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:18px;background:linear-gradient(#fff,#fbfcfe); } .message { display:flex;gap:11px;max-width:82%; }.message-avatar{flex:0 0 32px;height:32px;border-radius:9px;background:var(--navy);color:white;display:grid;place-items:center;font-family:serif}.message b{font-size:10px}.message p{margin:6px 0 0;border-radius:4px 12px 12px 12px;background:#f0f4f9;padding:12px;color:#46546a;font-size:11px;line-height:1.75;white-space:pre-wrap}.user-message{align-self:flex-end;flex-direction:row-reverse}.user-message .message-avatar{background:var(--blue)}.user-message p{background:#eaf2ff;border-radius:12px 4px 12px 12px}.typing{opacity:.6} .suggestions{padding:10px 18px;border-top:1px solid #edf0f4;display:flex;gap:7px;overflow-x:auto}.suggestions button{white-space:nowrap;border:1px solid #dfe6f0;background:white;color:#58677e;border-radius:99px;padding:7px 10px;font-size:9px}.suggestions button:hover{border-color:#91afe5;color:var(--blue)} .chat-form{border-top:1px solid #e7ebf1;padding:14px 18px}.chat-form textarea{width:100%;resize:none;border:0;padding:4px;line-height:1.5}.chat-form textarea:focus{box-shadow:none}.chat-form>div{display:flex;align-items:center;justify-content:space-between;margin-top:8px}.chat-form span{color:#9ba6b6;font-size:8px} .assistant-context{display:grid;gap:14px;align-content:start}.assistant-context .large-select{width:100%;min-width:0;margin:14px 0}.assistant-context h4{margin:12px 0 5px;font-size:11px}.assistant-context p{color:#718096;font-size:9px;line-height:1.5}.boundary-detail p{margin:10px 0;padding:9px 10px;border-left:2px solid var(--red);background:#fff7f7;color:#8c4b51} .integration-grid{display:grid;grid-template-columns:1.35fr .65fr;gap:16px}.architecture{display:flex;align-items:center;gap:10px;margin-top:25px}.architecture>span{color:#9eabbd}.arch-node{flex:1;border:1px solid #dce3ed;background:#fafcff;border-radius:11px;padding:14px;text-align:center}.arch-node b,.arch-node small{display:block}.arch-node b{font-size:11px}.arch-node small{color:#8a96a8;font-size:8px;margin-top:5px}.arch-node.active{border-color:#92b1e8;background:#eef5ff}.arch-node.judge{border-color:#dac48e;background:#fff9e9} .config-row{display:flex;justify-content:space-between;padding:13px 0;border-bottom:1px solid #edf0f4;font-size:10px}.config-row span{color:#738094}.config-row strong{color:var(--green)}.config-note{margin-top:14px;padding:11px;background:#f5f8fc;border-radius:8px;color:#788598;font-size:9px;line-height:1.55} .log-panel{margin-top:16px}.log-list{margin-top:15px}.log-row{display:grid;grid-template-columns:130px 100px 1fr auto;gap:12px;align-items:center;padding:12px;border-top:1px solid #edf0f4;font-size:10px}.log-row time{color:#8793a5}.log-row strong{font-size:9px;color:#536176}.log-row p{margin:0;color:#536176}.trace{font-family:monospace;color:#8b97a8;font-size:8px} .drawer-backdrop{position:fixed;inset:0;background:rgba(9,23,43,.34);opacity:0;pointer-events:none;transition:.25s;z-index:49}.drawer-backdrop.open{opacity:1;pointer-events:auto}.case-drawer{position:fixed;z-index:50;right:0;top:0;bottom:0;width:min(480px,92vw);background:white;box-shadow:-20px 0 50px rgba(7,23,45,.18);transform:translateX(105%);transition:.3s;padding:30px;overflow-y:auto}.case-drawer.open{transform:translateX(0)}.drawer-close{position:absolute;right:20px;top:18px;border:0;background:#f1f4f8;width:32px;height:32px;border-radius:50%;font-size:20px}.drawer-kicker{color:var(--blue);font-size:9px;font-weight:700;letter-spacing:1px}.drawer-title{font-size:21px;line-height:1.5;margin:9px 40px 8px 0}.drawer-meta{color:#8190a4;font-size:10px}.drawer-section{margin-top:24px}.drawer-section h4{font-size:12px;border-bottom:1px solid #e8ecf2;padding-bottom:9px}.drawer-section p{font-size:10px;color:#5e6b7e;line-height:1.7}.drawer-evidence{padding:11px;border:1px solid #e4e9f0;border-radius:9px;margin:8px 0;display:flex;justify-content:space-between;gap:10px}.drawer-evidence b{font-size:10px}.drawer-evidence small{display:block;color:#929daf;font-size:8px;margin-top:4px} .toast{position:fixed;left:50%;bottom:28px;transform:translate(-50%,30px);padding:11px 16px;border-radius:9px;background:#102847;color:white;font-size:10px;opacity:0;pointer-events:none;transition:.25s;z-index:70;box-shadow:0 10px 30px rgba(0,0,0,.2)}.toast.show{opacity:1;transform:translate(-50%,0)} @media(max-width:1100px){.metric-grid{grid-template-columns:repeat(2,1fr)}.research-grid,.assistant-layout,.integration-grid{grid-template-columns:1fr}.assistant-context{grid-template-columns:1fr 1fr}.evidence-layout{grid-template-columns:1fr}.case-stats{grid-template-columns:repeat(2,1fr)}} @media(max-width:780px){.app-shell{display:block}.sidebar{transform:translateX(-105%);transition:.25s}.sidebar.open{transform:none}.main{grid-column:auto}.topbar{height:76px;padding:0 18px}.mobile-menu{display:block}.sync-state,.top-actions .ghost-button{display:none}.content{padding:20px 16px 50px}.hero{grid-template-columns:1fr;padding:30px 25px}.hero h2{font-size:29px}.hero-visual{display:none}.overview-grid,.assistant-context{grid-template-columns:1fr}.page-lead{align-items:flex-start;flex-direction:column}.metric-grid{grid-template-columns:1fr 1fr}.filter-row{flex-wrap:wrap}.architecture{display:grid;grid-template-columns:1fr}.architecture>span{transform:rotate(90deg);text-align:center}.log-row{grid-template-columns:1fr 1fr}.log-row p{grid-column:1/-1}.large-select{width:100%}.topbar .eyebrow{display:none}} @media(max-width:500px){.metric-grid,.case-stats{grid-template-columns:1fr}.metric-card{min-height:125px}.section-heading{align-items:flex-start}.segmented{width:100%;overflow-x:auto}.research-grid{display:block}.insight-panel{margin-top:14px}.messages{height:360px}.message{max-width:95%}} public/data.js export const surveyData = { sampleSize: 54, frontlineShare: 55.55, coreWorkShare: 83.33, boundaryCorrelation: 0.715, institution: [ { name: "全流程留痕制度", value: 3.83, group: "制度保障" }, { name: "明确权责划分", value: 3.83, group: "制度保障" }, { name: "实质性审核确认", value: 3.78, group: "人工审核" }, { name: "价值判断归法官", value: 3.78, group: "人工审核" }, { name: "算法可解释说明", value: 3.72, group: "透明可控" }, { name: "事务性工作辅助", value: 3.72, group: "功能定位" }, { name: "严格辅助定位", value: 3.72, group: "功能定位" }, { name: "功能负面清单", value: 3.67, group: "风险边界" } ], roles: [ { name: "法官", count: 12, percent: 22.22 }, { name: "法官助理", count: 18, percent: 33.33 }, { name: "书记员", count: 9, percent: 16.67 }, { name: "司法行政人员", count: 9, percent: 16.67 }, { name: "其他", count: 6, percent: 11.11 } ], familiarity: [ { name: "完全不了解", count: 3, percent: 5.56 }, { name: "仅听说过", count: 6, percent: 11.11 }, { name: "了解基础功能", count: 21, percent: 38.89 }, { name: "比较了解", count: 24, percent: 44.44 } ], ipa: [ { name: "信息填充", importance: 3.52, performance: 3.21 }, { name: "形式校验", importance: 3.88, performance: 3.95 }, { name: "法条匹配", importance: 4.05, performance: 3.14 }, { name: "要素提取", importance: 4.15, performance: 3.42 }, { name: "证据分类", importance: 4.22, performance: 3.81 }, { name: "辅助定位", importance: 4.12, performance: 3.55 }, { name: "逻辑核验", importance: 4.35, performance: 2.75 }, { name: "文书说理", importance: 4.41, performance: 2.68 } ], correlations: [ { x: 2.0, y: 2.1 }, { x: 2.2, y: 2.7 }, { x: 2.5, y: 2.4 }, { x: 2.8, y: 3.0 }, { x: 3.0, y: 3.4 }, { x: 3.1, y: 3.0 }, { x: 3.3, y: 3.5 }, { x: 3.5, y: 3.9 }, { x: 3.6, y: 3.6 }, { x: 3.8, y: 4.1 }, { x: 4.0, y: 3.9 }, { x: 4.1, y: 4.4 }, { x: 4.3, y: 4.2 }, { x: 4.5, y: 4.7 }, { x: 4.8, y: 4.6 } ] }; export const glossary = { "全流程留痕制度": "记录上传、分类、引用、AI建议、人工修改和最终确认的完整轨迹。", "明确权责划分": "系统提供辅助建议,法官承担实质审核与最终裁判责任。", "算法可解释说明": "展示建议来源、引用证据、规则依据及不确定性提示。", "文书说理": "围绕争议焦点、证据采信和法律适用形成结构化说理建议。" }; public/app.js import { surveyData, glossary } from "./data.js"; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; const state = { cases: [], chart: "bar", logs: [ { time: "2026-06-29 09:42", user: "李诺", action: "更新网页功能架构与小程序接口", trace: "ARCH-0629-01" }, { time: "2026-06-29 09:18", user: "系统", action: "同步 4 个演示案件与 12 条证据索引", trace: "SYNC-0629-04" }, { time: "2026-06-28 16:42", user: "张法官", action: "人工确认电子合同来源信息", trace: "REV-018-07" }, { time: "2026-06-28 16:31", user: "AI助手", action: "标记转账记录与聊天记录存在时间矛盾", trace: "AI-018-19" } ] }; const escapeHtml = (value = "") => String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]); function toast(message) { const element = $("#toast"); element.textContent = message; element.classList.add("show"); clearTimeout(toast.timer); toast.timer = setTimeout(() => element.classList.remove("show"), 2600); } function navigate(view) { $$(".view").forEach((element) => element.classList.toggle("active", element.id === `view-${view}`)); $$(".nav-item").forEach((element) => element.classList.toggle("active", element.dataset.view === view)); const active = $(`#view-${view}`); $("#pageTitle").textContent = active?.dataset.title || "智衡"; location.hash = view; $("#sidebar").classList.remove("open"); window.scrollTo({ top: 0, behavior: "smooth" }); } $$(".nav-item").forEach((button) => button.addEventListener("click", () => navigate(button.dataset.view))); $$("[data-jump]").forEach((button) => button.addEventListener("click", () => navigate(button.dataset.jump))); $("#mobileMenu").addEventListener("click", () => $("#sidebar").classList.toggle("open")); window.addEventListener("hashchange", () => { const view = location.hash.replace("#", ""); if (["overview", "research", "cases", "evidence", "assistant", "integration"].includes(view)) { navigate(view); } }); function svgWrap(content, viewBox = "0 0 760 300") { return `${content}`; } function barChart() { const data = [...surveyData.institution].sort((a, b) => b.value - a.value); const left = 142, top = 14, row = 33, maxWidth = 520; const content = data.map((item, index) => { const width = ((item.value - 3.4) / 0.5) * maxWidth; const y = top + index * row; return ` ${item.name} ${item.value.toFixed(2)}`; }).join(""); return svgWrap(content, "0 0 760 282"); } function radarChart() { const data = surveyData.institution; const cx = 375, cy = 150, radius = 112; const pointsAt = (scale) => data.map((_, index) => { const angle = -Math.PI / 2 + index * (Math.PI * 2 / data.length); return `${cx + Math.cos(angle) * radius * scale},${cy + Math.sin(angle) * radius * scale}`; }).join(" "); const grid = [0.25, 0.5, 0.75, 1].map((scale) => `` ).join(""); const axes = data.map((_, index) => { const angle = -Math.PI / 2 + index * (Math.PI * 2 / data.length); return ``; }).join(""); const values = data.map((item, index) => { const angle = -Math.PI / 2 + index * (Math.PI * 2 / data.length); const scale = item.value / 5; return `${cx + Math.cos(angle) * radius * scale},${cy + Math.sin(angle) * radius * scale}`; }).join(" "); const labels = data.map((item, index) => { const angle = -Math.PI / 2 + index * (Math.PI * 2 / data.length); const x = cx + Math.cos(angle) * (radius + 29); const y = cy + Math.sin(angle) * (radius + 22); const anchor = x < cx - 5 ? "end" : x > cx + 5 ? "start" : "middle"; return `${item.name}`; }).join(""); return svgWrap(`${grid}${axes}${labels}`, "0 0 760 300"); } function scatterChart() { const data = surveyData.ipa; const left = 76, top = 20, width = 620, height = 225; const xScale = (value) => left + ((value - 3.4) / 1.2) * width; const yScale = (value) => top + height - ((value - 2.4) / 1.8) * height; const meanX = data.reduce((sum, item) => sum + item.importance, 0) / data.length; const meanY = data.reduce((sum, item) => sum + item.performance, 0) / data.length; const ticks = [2.5, 3, 3.5, 4].map((value) => ` ${value.toFixed(1)}` ).join(""); const points = data.map((item) => { const urgent = item.importance > meanX && item.performance < meanY; return ` ${item.name}`; }).join(""); return svgWrap(` ${ticks} ${points} 重要性(Importance) 表现值(Performance) 优先改进区`, "0 0 760 290"); } const chartMeta = { bar: ["制度建设", "制度需求重要性排序", "权责划分与流程留痕并列首位,说明制度安全是技术落地的前提。"], radar: ["制度结构", "制度需求优先级雷达图", "制度需求围绕“权责划分—流程留痕—实质审核”形成稳定核心。"], scatter: ["功能诊断", "功能 IPA 象限分析", "文书说理与逻辑核验重要性高、表现偏低,是最需要优先突破的短板。"] }; function renderChart(type = state.chart) { state.chart = type; $("#chartStage").innerHTML = type === "bar" ? barChart() : type === "radar" ? radarChart() : scatterChart(); const [kicker, title, note] = chartMeta[type]; $("#chartKicker").textContent = kicker; $("#chartTitle").textContent = title; $("#chartNote").textContent = note; $$("#chartSwitcher button").forEach((button) => button.classList.toggle("active", button.dataset.chart === type)); } $("#chartSwitcher").addEventListener("click", (event) => { const button = event.target.closest("button[data-chart]"); if (button) renderChart(button.dataset.chart); }); function download(filename, content, type = "text/plain;charset=utf-8") { const blob = new Blob(["\ufeff", content], { type }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; anchor.click(); URL.revokeObjectURL(url); } $("#downloadChart").addEventListener("click", () => { const svg = $("#chartStage svg"); download(`${chartMeta[state.chart][1]}.svg`, new XMLSerializer().serializeToString(svg), "image/svg+xml"); addLog("李诺", `导出图表:${chartMeta[state.chart][1]}`, `EXP-${Date.now()}`); }); function renderMetrics() { const group = $("#metricGroup").value; const keyword = $("#metricSearch").value.trim(); const rows = surveyData.institution .filter((item) => (!group || item.group === group) && (!keyword || item.name.includes(keyword))) .sort((a, b) => b.value - a.value); $("#metricsTable").innerHTML = rows.map((item, index) => ` ${escapeHtml(item.name)} ${escapeHtml(item.group)}
${item.value.toFixed(2)}
${index < 2 ? "最高" : index < 5 ? "较高" : "基础"} ${escapeHtml(glossary[item.name] || "构成司法智能辅助系统的边界与保障机制。")} `).join("") || `未找到匹配指标`; } function setupMetricFilters() { const groups = [...new Set(surveyData.institution.map((item) => item.group))]; $("#metricGroup").insertAdjacentHTML("beforeend", groups.map((group) => ``).join("")); $("#metricGroup").addEventListener("change", renderMetrics); $("#metricSearch").addEventListener("input", renderMetrics); $("#exportMetrics").addEventListener("click", () => { const csv = ["指标,分组,均值", ...surveyData.institution.map((item) => `${item.name},${item.group},${item.value}`)].join("\n"); download("基层法院智能辅助系统_制度指标.csv", csv, "text/csv;charset=utf-8"); addLog("李诺", "导出制度指标 CSV", `EXP-${Date.now()}`); }); } async function loadCases() { try { const response = await fetch("/api/cases"); const result = await response.json(); state.cases = result.data || []; } catch { state.cases = []; } renderCases(); populateCaseSelects(); } function renderCases() { const keyword = $("#caseSearch")?.value.trim().toLowerCase() || ""; const status = $("#caseStatus")?.value || ""; const cases = state.cases.filter((item) => { const text = `${item.caseNo}${item.title}${item.cause}${item.judge}`.toLowerCase(); return (!keyword || text.includes(keyword)) && (!status || item.status === status); }); $("#caseTotal").textContent = state.cases.length; $("#casesTable").innerHTML = cases.map((item) => ` ${escapeHtml(item.title)}${escapeHtml(item.caseNo)} ${escapeHtml(item.cause)}${escapeHtml(item.judge)} ${item.evidenceCount} 条${escapeHtml(item.status)} ${escapeHtml(item.updatedAt)} `).join("") || `未找到匹配案件`; } $("#runCaseSearch").addEventListener("click", renderCases); $("#caseSearch").addEventListener("keydown", (event) => { if (event.key === "Enter") renderCases(); }); $("#caseStatus").addEventListener("change", renderCases); $("#casesTable").addEventListener("click", (event) => { const button = event.target.closest("[data-case]"); if (button) openCase(button.dataset.case); }); function openCase(id) { const item = state.cases.find((candidate) => candidate.id === id); if (!item) return; $("#drawerContent").innerHTML = `

${escapeHtml(item.caseNo)}

${escapeHtml(item.title)}

${escapeHtml(item.court)} · ${escapeHtml(item.judge)} · ${escapeHtml(item.status)}

当事人

${item.parties.map((party) => `

${escapeHtml(party)}

`).join("")}

证据索引(${item.evidence.length})

${item.evidence.map((evidence) => `
${escapeHtml(evidence.name)}${escapeHtml(evidence.type)} · ${escapeHtml(evidence.source)} · ${escapeHtml(evidence.date)} ${escapeHtml(evidence.status)}
`).join("")}
`; $("#caseDrawer").classList.add("open"); $("#drawerBackdrop").classList.add("open"); } function closeDrawer() { $("#caseDrawer").classList.remove("open"); $("#drawerBackdrop").classList.remove("open"); } $("#drawerClose").addEventListener("click", closeDrawer); $("#drawerBackdrop").addEventListener("click", closeDrawer); $("#caseDrawer").addEventListener("click", (event) => { const button = event.target.closest("[data-open-evidence]"); if (!button) return; $("#evidenceCaseSelect").value = button.dataset.openEvidence; renderEvidence(button.dataset.openEvidence); closeDrawer(); navigate("evidence"); }); function populateCaseSelects() { const options = state.cases.map((item) => ``).join(""); ["evidenceCaseSelect", "assistantCaseSelect"].forEach((id) => { $(`#${id}`).innerHTML = options; }); if (state.cases[0]) { renderEvidence(state.cases[0].id); renderAssistantContext(state.cases[0].id); } } function renderEvidence(id) { const item = state.cases.find((candidate) => candidate.id === id); if (!item) return; $("#evidenceCaseTitle").textContent = item.title; $("#chainStage").innerHTML = item.evidence.map((evidence) => `
${escapeHtml(evidence.name)}

${escapeHtml(evidence.type)} · ${escapeHtml(evidence.source)}

${evidence.status === "存在矛盾" ? `发现时间或内容矛盾,须人工复核` : ""}
${escapeHtml(evidence.date)}
`).join(""); } $("#evidenceCaseSelect").addEventListener("change", (event) => renderEvidence(event.target.value)); $("#confirmReview").addEventListener("click", () => { const checked = $$(".review-panel input:checked").length; addLog("李诺", `记录证据链人工复核(${checked}/4 项)`, `REV-${Date.now()}`); toast(`已记录 ${checked} 项人工复核,操作可追溯`); }); function renderAssistantContext(id) { const item = state.cases.find((candidate) => candidate.id === id); if (!item) return; $("#assistantContext").innerHTML = `

${escapeHtml(item.title)}

${escapeHtml(item.caseNo)}

${item.evidenceCount} 条证据 · ${escapeHtml(item.status)}

${item.risk === "high" ? "高风险" : item.risk === "medium" ? "中风险" : "低风险"}`; } $("#assistantCaseSelect").addEventListener("change", (event) => renderAssistantContext(event.target.value)); function appendMessage(role, text, temporary = false) { const wrapper = document.createElement("div"); wrapper.className = `message ${role === "user" ? "user-message" : "assistant-message"}${temporary ? " typing" : ""}`; wrapper.innerHTML = `${role === "user" ? "李" : "衡"}
${role === "user" ? "你" : "智能审判助手"}

${escapeHtml(text)}

`; $("#messages").appendChild(wrapper); $("#messages").scrollTop = $("#messages").scrollHeight; return wrapper; } async function sendChat(question) { const caseId = $("#assistantCaseSelect").value; const item = state.cases.find((candidate) => candidate.id === caseId); appendMessage("user", question); const typing = appendMessage("assistant", "正在结合案件上下文分析…", true); try { const response = await fetch("/api/coze/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ question, userId: "li-nuo-web", context: item ? `${item.caseNo};${item.title};现有证据 ${item.evidence.map((evidence) => evidence.name).join("、")}` : "" }) }); const result = await response.json(); typing.remove(); if (!response.ok) throw new Error(result.error || "智能体调用失败"); appendMessage("assistant", result.answer); $("#cozeMode").innerHTML = `${result.mode === "coze" ? "Coze 在线" : "演示模式"}`; addLog("李诺", `智能辅助提问:${question.slice(0, 28)}`, result.chatId || `AI-${Date.now()}`); } catch (error) { typing.remove(); appendMessage("assistant", `调用失败:${error.message}\n请检查服务端 Coze 配置。`); } } $("#chatForm").addEventListener("submit", (event) => { event.preventDefault(); const input = $("#chatInput"); const question = input.value.trim(); if (!question) return; input.value = ""; sendChat(question); }); $$(".suggestions button").forEach((button) => button.addEventListener("click", () => sendChat(button.textContent))); function addLog(user, action, trace) { state.logs.unshift({ time: new Date().toLocaleString("zh-CN", { hour12: false }).replaceAll("/", "-"), user, action, trace }); renderLogs(); } function renderLogs() { $("#logList").innerHTML = state.logs.map((item) => `
${escapeHtml(item.user)}

${escapeHtml(item.action)}

${escapeHtml(item.trace)}
`).join(""); } async function healthCheck(showToast = false) { try { const response = await fetch("/api/health"); const result = await response.json(); $("#apiStatus").textContent = result.ok ? "已连接" : "异常"; $("#miniStatus").textContent = result.miniProgramAuth === "configured" ? "生产密钥已配置" : "演示密钥"; $("#cozeStatus").textContent = result.cozeConfigured ? "已配置" : "演示模式"; $("#cozeMode").innerHTML = `${result.cozeConfigured ? "Coze 在线" : "演示模式"}`; if (showToast) toast(result.cozeConfigured ? "网页、小程序接口和 Coze 配置均可用" : "网页接口正常;Coze 当前为演示模式"); } catch { ["apiStatus", "miniStatus", "cozeStatus"].forEach((id) => $(`#${id}`).textContent = "连接失败"); if (showToast) toast("连接检测失败"); } } $("#testConnection").addEventListener("click", () => healthCheck(true)); $("#syncCases").addEventListener("click", async () => { const button = $("#syncCases"); button.disabled = true; button.textContent = "同步中…"; await loadCases(); button.disabled = false; button.textContent = "同步小程序数据"; addLog("李诺", `从小程序接口同步 ${state.cases.length} 个案件`, `SYNC-${Date.now()}`); toast(`同步完成:${state.cases.length} 个案件`); }); $("#exportLogs").addEventListener("click", () => { const csv = ["时间,操作者,动作,留痕编号", ...state.logs.map((item) => `${item.time},${item.user},"${item.action}",${item.trace}`)].join("\n"); download("司法智能辅助系统_操作日志.csv", csv, "text/csv;charset=utf-8"); }); $("#globalExport").addEventListener("click", () => { const exportData = { exportedAt: new Date().toISOString(), survey: surveyData, cases: state.cases, logs: state.logs, disclaimer: "AI 输出仅作辅助建议,事实认定与裁判结论须由法官审核确认。" }; download("基层法院智能辅助系统_数据导出.json", JSON.stringify(exportData, null, 2), "application/json;charset=utf-8"); addLog("李诺", "导出网页全部数据 JSON", `EXP-${Date.now()}`); }); function init() { const hash = location.hash.replace("#", ""); setupMetricFilters(); renderMetrics(); renderChart(); renderLogs(); loadCases(); healthCheck(); if (["overview", "research", "cases", "evidence", "assistant", "integration"].includes(hash)) navigate(hash); } init(); 附录B Coze服务端调用与小程序接口代码 .env.example(调用配置) # Coze 中国区开放平台配置(请勿将真实密钥提交到公开仓库) COZE_API_TOKEN=pat_xxxxxxxxxxxxxxxxx COZE_BOT_ID=xxxxxxxxxxxxxxxxx COZE_API_BASE=https://api.coze.cn # 小程序调用网页服务时使用;部署后应替换为高强度随机值 MINI_PROGRAM_API_KEY=replace-with-a-strong-random-key # 服务监听配置 PORT=4173 HOST=0.0.0.0 server.mjs(完整服务端代码) import { createServer } from "node:http"; import { readFile, writeFile, stat } from "node:fs/promises"; import { extname, join, normalize } from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = fileURLToPath(new URL(".", import.meta.url)); const PUBLIC = join(ROOT, "public"); const CASES_FILE = join(ROOT, "data", "cases.json"); const PORT = Number(process.env.PORT || 4173); const HOST = process.env.HOST || "0.0.0.0"; const API_KEY = process.env.MINI_PROGRAM_API_KEY || "demo-mini-program-key"; const mimeTypes = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".ico": "image/x-icon" }; const json = (response, status, body) => { response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type, X-API-Key", "Access-Control-Allow-Methods": "GET, POST, OPTIONS" }); response.end(JSON.stringify(body)); }; const bodyJson = async (request) => { let raw = ""; for await (const chunk of request) { raw += chunk; if (raw.length > 2_000_000) throw new Error("请求内容超过 2MB 限制"); } return raw ? JSON.parse(raw) : {}; }; const loadCases = async () => JSON.parse(await readFile(CASES_FILE, "utf8")); const saveCases = async (cases) => writeFile(CASES_FILE, JSON.stringify(cases, null, 2), "utf8"); const isAuthorized = (request) => request.headers["x-api-key"] === API_KEY || API_KEY === "demo-mini-program-key"; async function cozeChat(question, userId, context) { const token = process.env.COZE_API_TOKEN; const botId = process.env.COZE_BOT_ID; const base = process.env.COZE_API_BASE || "https://api.coze.cn"; if (!token || !botId) { return { mode: "demo", answer: `【辅助分析|演示模式】\n\n围绕“${question}”,建议先核对证据来源、形成时间、真实性与关联性,再将争议焦点拆分为可验证的事实命题。` + `\n\n系统边界:AI 仅提供结构化整理和提示,不作事实认定或裁判结论。当前案件上下文:${context || "未选择具体案件"}。` + "\n\n下一步可执行:①比对矛盾证据;②标记缺失材料;③生成待人工确认的证据链摘要。" }; } const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; const createResponse = await fetch(`${base}/v3/chat`, { method: "POST", headers, body: JSON.stringify({ bot_id: botId, user_id: userId || "web-reviewer", stream: false, auto_save_history: true, additional_messages: [ { role: "user", content: `${context ? `案件上下文:${context}\n` : ""}${question}`, content_type: "text" } ] }) }); const created = await createResponse.json(); if (!createResponse.ok || created.code) { throw new Error(created.msg || created.message || "Coze 对话创建失败"); } const chatId = created.data?.id; const conversationId = created.data?.conversation_id; if (!chatId || !conversationId) throw new Error("Coze 未返回有效会话标识"); let status = created.data.status; for (let attempt = 0; attempt < 20 && status !== "completed"; attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 650)); const polling = await fetch( `${base}/v3/chat/retrieve?conversation_id=${encodeURIComponent(conversationId)}&chat_id=${encodeURIComponent(chatId)}`, { headers } ); const result = await polling.json(); if (!polling.ok || result.code) throw new Error(result.msg || "Coze 状态查询失败"); status = result.data?.status; if (["failed", "requires_action", "canceled"].includes(status)) { throw new Error(`Coze 对话状态异常:${status}`); } } if (status !== "completed") throw new Error("Coze 响应超时,请稍后重试"); const messageResponse = await fetch( `${base}/v3/chat/message/list?conversation_id=${encodeURIComponent(conversationId)}&chat_id=${encodeURIComponent(chatId)}`, { headers } ); const messageResult = await messageResponse.json(); const answer = messageResult.data ?.filter((item) => item.role === "assistant" && item.type === "answer") .map((item) => item.content) .join("\n"); if (!answer) throw new Error("Coze 未返回回答内容"); return { mode: "coze", answer, conversationId, chatId }; } async function routeApi(request, response, url) { if (request.method === "OPTIONS") return json(response, 204, {}); if (url.pathname === "/api/health" && request.method === "GET") { return json(response, 200, { ok: true, service: "智衡司法智能辅助系统", cozeConfigured: Boolean(process.env.COZE_API_TOKEN && process.env.COZE_BOT_ID), miniProgramAuth: API_KEY === "demo-mini-program-key" ? "demo" : "configured", timestamp: new Date().toISOString() }); } if (url.pathname === "/api/cases" && request.method === "GET") { const cases = await loadCases(); const keyword = (url.searchParams.get("keyword") || "").trim().toLowerCase(); const status = url.searchParams.get("status") || ""; const filtered = cases.filter((item) => { const text = `${item.caseNo}${item.title}${item.cause}${item.judge}`.toLowerCase(); return (!keyword || text.includes(keyword)) && (!status || item.status === status); }); return json(response, 200, { total: filtered.length, data: filtered }); } if (url.pathname.startsWith("/api/cases/") && request.method === "GET") { const id = decodeURIComponent(url.pathname.split("/").pop()); const item = (await loadCases()).find((candidate) => candidate.id === id); return item ? json(response, 200, { data: item }) : json(response, 404, { error: "未找到案件" }); } if (url.pathname === "/api/mini-program/sync" && request.method === "POST") { if (!isAuthorized(request)) return json(response, 401, { error: "接口密钥无效" }); const payload = await bodyJson(request); const incoming = Array.isArray(payload.cases) ? payload.cases : []; const current = await loadCases(); const byId = new Map(current.map((item) => [item.id, item])); incoming.forEach((item) => { if (!item.id || !item.caseNo || !item.title) return; byId.set(item.id, { ...byId.get(item.id), ...item, updatedAt: new Date().toISOString() }); }); const merged = [...byId.values()]; await saveCases(merged); return json(response, 200, { ok: true, accepted: incoming.length, total: merged.length, traceId: `SYNC-${Date.now()}` }); } if (url.pathname === "/api/evidence" && request.method === "POST") { if (!isAuthorized(request)) return json(response, 401, { error: "接口密钥无效" }); const payload = await bodyJson(request); const cases = await loadCases(); const target = cases.find((item) => item.id === payload.caseId); if (!target) return json(response, 404, { error: "案件不存在" }); const evidence = { id: payload.id || `E-${Date.now()}`, name: payload.name || "未命名证据", type: payload.type || "其他", source: payload.source || "小程序同步", date: payload.date || new Date().toISOString().slice(0, 10), status: payload.status || "待复核" }; target.evidence = [...(target.evidence || []), evidence]; target.evidenceCount = target.evidence.length; target.updatedAt = new Date().toISOString(); await saveCases(cases); return json(response, 201, { ok: true, data: evidence, traceId: `EV-${Date.now()}` }); } if (url.pathname === "/api/coze/chat" && request.method === "POST") { const payload = await bodyJson(request); if (!payload.question?.trim()) return json(response, 400, { error: "请输入问题" }); try { const result = await cozeChat( payload.question.trim(), payload.userId, payload.context ); return json(response, 200, result); } catch (error) { return json(response, 502, { error: error.message, hint: "请检查 COZE_API_TOKEN、COZE_BOT_ID、智能体发布状态及网络连接" }); } } return json(response, 404, { error: "接口不存在" }); } async function serveStatic(request, response, url) { const relative = url.pathname === "/" ? "index.html" : url.pathname.slice(1); const safePath = normalize(relative).replace(/^(\.\.[\\/])+/, ""); const filePath = join(PUBLIC, safePath); if (!filePath.startsWith(PUBLIC)) { response.writeHead(403); return response.end("Forbidden"); } try { const info = await stat(filePath); const resolvedPath = info.isDirectory() ? join(filePath, "index.html") : filePath; const body = await readFile(resolvedPath); response.writeHead(200, { "Content-Type": mimeTypes[extname(resolvedPath)] || "application/octet-stream", "Cache-Control": extname(resolvedPath) === ".html" ? "no-cache" : "public, max-age=3600" }); response.end(body); } catch { const index = await readFile(join(PUBLIC, "index.html")); response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); response.end(index); } } const server = createServer(async (request, response) => { const url = new URL(request.url, `http://${request.headers.host || "localhost"}`); try { if (url.pathname.startsWith("/api/")) { await routeApi(request, response, url); } else { await serveStatic(request, response, url); } } catch (error) { json(response, 500, { error: error.message || "服务器内部错误" }); } }); server.listen(PORT, HOST, () => { console.log(`智衡司法智能辅助系统已启动:http://localhost:${PORT}`); }); 小程序调用示例 # 小程序对接说明 ## 1. 对接目标 现有 UniApp 小程序继续承担登录、证据上传、案卷管理和文书生成入口;本网页承担跨案件数据查询、可视化分析、证据链复核、智能问答和结果导出。两端使用同一案件编号和证据编号,避免形成两套孤立数据。 ## 2. 同步请求 ```http POST /api/mini-program/sync Content-Type: application/json X-API-Key: <部署时配置的 MINI_PROGRAM_API_KEY> ``` ```json { "cases": [ { "id": "CASE-2026-018", "caseNo": "(2026) 川0107民初018号", "title": "王某诉成都某科技公司合同纠纷案", "cause": "合同纠纷", "judge": "张法官", "status": "证据整理", "evidenceCount": 12 } ] } ``` ## 3. UniApp 调用示例 ```js uni.request({ url: `${WEB_API_BASE}/api/mini-program/sync`, method: "POST", header: { "Content-Type": "application/json", "X-API-Key": MINI_PROGRAM_API_KEY }, data: { cases: [currentCase] }, success: ({ data }) => { console.log("同步完成", data.traceId); } }); ``` ## 4. 证据同步字段 证据记录至少包含 `caseId`、`name`、`type`、`source`、`date`;服务端会补充证据编号、复核状态和留痕编号。图片、音频和原始文档应先上传到对象存储,再把受控访问地址写入业务数据库,避免把大文件直接塞入同步接口。 ## 5. 安全与责任边界 - 生产环境必须更换默认 API Key,并启用 HTTPS、用户身份校验和请求签名。 - 小程序不得保存 Coze Personal Access Token;所有智能体请求由网页服务端转发。 - AI 输出必须显示“辅助建议”标识,文书定稿前设置法官人工确认节点。 - 上传、修改、引用、AI 建议、人工确认和导出动作均应记录操作者、时间、版本和 traceId。