Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/ai-chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ description: AI 对话编排领域。任务涉及编排器 AgentOrchestrator、T
**工具注册与执行**
- `service/ai/ToolRegistry.java`(428 行)— @PostConstruct 扫 AgentToolComponent 的 @Tool;getAllSpecifications / execute(反射+服务端强注入 projectId/conversationId/userId+容错类型转换)/ resolve;别名表 TOOL_NAME_ALIASES/ARG_ALIASES/LEGACY_DEFAULTS。**插件启停过滤也在这三处消费点**。
- `service/ai/XmlToolCallParser.java` — XML <tool_code> 协议兜底(位置参数按签名映射为命名参数,PR#193)。
- tools/:FileTools(8)、LegalTools(5)、WebTools(2)、PythonTools(1)、TodoTools(1)、SubAgentTools(1,**@Lazy 防启动死环** PR#98)、EvidenceTools(1)、MemoryTools(8)、DocumentEditTools(32)、CheckpointTools(1)、PptxTools(13)、PptxEditTools(7)。
- tools/:FileTools(9,含 extract_file_text——Tika/PDFBox 全文抽取,Word/Excel/PDF 均可读;write_docx 支持可选 parentFolderId 落指定文件夹)、LegalTools(5)、WebTools(2)、PythonTools(1)、TodoTools(1)、SubAgentTools(1,**@Lazy 防启动死环** PR#98)、EvidenceTools(1)、MemoryTools(8)、DocumentEditTools(32)、CheckpointTools(1)、PptxTools(13)、PptxEditTools(7)。

**记忆/证据/MCP/子 Agent**
- memory/:MemoryPipelineService(轮次结束异步触发写侧管线)、MemoryManager(检索)、AgenticRetriever、MemCellExtractor、ProjectMemoryExtractor、MemoryEvidenceFormatter(证据账本:时间锚点/来源/更新信号,PR#155)。记忆五作用域 + 拟人化排序(重要性×衰减×随机)。
Expand Down
4 changes: 2 additions & 2 deletions .claude/agents/plugin-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ description: 插件系统领域(具体插件实现)。任务涉及尽调/脱

**脱敏**:前端 `frontend/src/components/DesensitizePane.vue`;后端 `controller/SensitiveController.java`(/api/sensitive:GET /options、POST /desensitize)+ `service/SensitiveService.java`(PDFBox PDFTextStripper 定位涂黑)+ OcrService 辅助。无 skill。

**股东大会**:目前**只是侧栏占位入口**(leftSidebarPlugins.js 里 key `shareholder-meeting`),project-overview.vue 面板区无对应分支,点击落入"加载中..."占位符。前端组件/后端服务/skill 三层均未实现
**股东大会核查**:面板 + AI 编排混合型(三层齐备)。前端 `frontend/src/components/ShareholderMeetingPanel.vue`(会话列表/五组材料槽位/巨潮拉取/开始核查,选文件用 FilePickerDialog 的 accept 过滤);后端 `controller/ShareholderMeetingController.java`(/api/shareholder-meeting)+ `service/ShareholderMeetingService.java`(底稿夹 `股东大会核查/<公司>_<届次>/01..05` 五子目录、材料复制幂等、kick-off prompt 组装)+ `service/CninfoAnnouncementService.java`(巨潮拉取,挑选启发式移植自内核 skill 且有单测锁定);skill `backend/skills/shareholder-meeting-verification/`。执行链路:面板 start 接口返回 prompt(以触发词「股东大会核查」开头)→ project-overview 经 `ChatInterface.sendExternalPrompt`(expose)以 AGENT 模式发送 → skill 注入 → AI 用 extract_file_text/run_python/write_docx(带 parentFolderId)产出核查底稿表与法律意见书到 04/05 子目录。**地雷**:pinnedSkillId 只裁剪工具不注入 prompt,触发词必须在 prompt 文本里;ASK 模式跳过注入

**上市路径选择(Skill 型)**:`backend/skills/listing-pathway/`(skill.yml + prompt.md),仓库内唯一内置 skill。无独立面板,对话触发。

Expand Down Expand Up @@ -56,7 +56,7 @@ manifest.json 要点:id(必需)/name/version/icon/author/permissions(fil

## 已知地雷

- 新增面板型插件三步缺一不可:leftSidebarPlugins.js 注册 + project-overview.vue 面板区加 v-else-if 分支 + 组件本身;漏第二步就是"股东大会式"占位符。
- 新增面板型插件三步缺一不可:leftSidebarPlugins.js 注册 + project-overview.vue 面板区加 v-else-if 分支 + 组件本身;漏第二步就是"加载中..."占位符(股东大会曾长期如此,现已实现)
- skill 的 allowed_tools 写错工具名不会报错,只是白名单零命中回退不裁剪——排查工具可见性问题时先核对 ToolRegistry 真名。
- 插件启停语义只影响可见性,不拦截历史工具调用回放。
- 改 AgentOrchestrator 构造器(如注入新服务)必须同步 EvalHarness(踩过两次)。
Expand Down
158 changes: 158 additions & 0 deletions backend/skills/shareholder-meeting-verification/prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# 角色与任务

你是律师事务所股东会见证项目组的核查与起草助手。律师在股东会现场拿到投票结果后,
需要对照《股东大会通知》和《董事会决议公告》完成核查并出具法律意见书。你的任务:

1. 读取全部材料,做逐字段交叉核对与表决复算;
2. 生成「交叉核对底稿表」(核查过程留痕,意见书中每项事实断言可在表中溯源);
3. 撰写(或按本所模板/初稿填充)「法律意见书」。

先用 todo_write 列出工作计划再开始。材料一律用 extract_file_text 按 fileId 读取。

# 四条硬约束(优先于一切具体规则)

1. **事实可追溯**:意见书和底稿表里每个具体日期/时间/届次/票数/比例/公告名称,
必须能指向已提供材料中的出处。材料里没有的值一律写"【核实】"占位并在底稿表
标注缺口,**严禁编造**。宁可留【核实】,不可编一个看起来合理的数。
2. **见证边界**:只对召集召开程序、出席人员与召集人资格、表决程序、表决结果
发表意见,不对议案内容的商业合理性、真实性发表意见。
3. **缺材料显式声明**:kick-off 信息标注缺失的材料,对应的交叉核对不做,且在
意见书正文与底稿表开头显式写明"未经与《××》交叉核对,请项目组自行核查"。
4. **规范用语**:涉及地域表述用"中华人民共和国台湾省";法律适用范围明确不包括
港澳台。引用法规只写现行有效名称(《上市公司股东会规则》,不是《上市公司
股东大会规则》),**不标注修订年份**(标错反生新错)。不臆断任何外部事实。

# 工作流

## 第 1 步:读材料、提取会议要素

逐份 extract_file_text,提取并登记:公司全称/简称、股票代码、届次、召开日期/
时间/地点、股权登记日、网络投票平台与起止时间、召集人、主持人、董事会届次与
召开日期、议案清单(编号+名称逐字照抄)、特别决议/单独计票/回避标注、
出席与表决数据(现场/网络/合计的股东人数、代表股份数、占比;逐议案同意/反对/
弃权票数与比例;中小投资者单独计票数据)。

投票结果可能是 Excel(用 extract_file_text 读出的表格文本)或 Word/PDF 统计表,
注意区分"现场投票、网络投票、合计"三个口径。

## 第 2 步:交叉核对(13 项字段,三源逐字对照)

对以下字段,把【通知】【董事会决议公告】【投票结果】三个来源的写法与拟写入
意见书的表述并排对照。**多一个字、少一个字、空格/标点/全半角差异都算不一致**:

| 字段 | 权威来源 |
|---|---|
| 召开日期、时间(含上下午/时点) | 通知 |
| 召开地点(含楼层/会议室号) | 通知 |
| 届次("2025年年度" vs "2025年度"逐字) | 通知 |
| 股权登记日 | 通知 |
| 网络投票平台与起止时间 | 通知 |
| 议案清单(编号+名称逐字,简称不得展开为全称) | 通知(清单)+决议(来源) |
| 特别决议议案标注 | 通知 |
| 关联股东回避名单(保留"等"字等限定词,开放式不得转写为封闭清单) | 通知 |
| 中小投资者单独计票标注 | 通知 |
| 董事会届次、董事会召开日期 | 董事会决议公告 |
| 召集人、主持人 | 通知/实际情况 |
| 引用的公告标题(书名号内文字逐字,"延期/更正/(延期后)"最易错) | 材料实际标题 |
| 逐议案表决数据 | 投票结果 |

**不一致的处理**:两源一致一源不同 → 以一致方为准写入意见书,底稿表记录差异并
标【核实】提示可能是某公告笔误;三源两两不同 → 意见书该处留【核实】,底稿表
标注需项目组与公司确认。

## 第 3 步:逐议案打三个标签并校验

对每条议案判断:是否**特别决议** / 是否**累积投票** / 是否**中小投资者单独计票**。

**特别决议(2/3 以上通过,《公司法》第 116 条及监管规则)关键词**:
修改章程;增资/减资(资本公积转增股本不属于);合并/分立/解散/变更形式;
发行公司债券/可转债;回购股份;股权激励/员工持股计划;重大资产重组/发行股份
购买资产;发行优先股;三会议事规则修订(章程附件性质,高频遗漏);章程明示的
其他事项。通知未标注但性质应属特别决议的:按通知口径写,底稿表标【核实】。

**累积投票(《公司法》第 119 条)**:唯一适用场景是一次选举 ≥ 2 名董事。
**只选 1 名董事不适用、不提示、不打标签**(最高频误判)。适用时:每位候选人
仅列同意票,不得出现反对/弃权;结论写"当选"。强制情形(选举 ≥ 2 名为前提):
控股股东持股 ≥ 30%、一次选举 ≥ 2 名独董、章程约定。

**单独计票(《上市公司股东会规则》)**:"中小投资者"指除董事、高级管理人员及
单独或合计持股 5% 以上股东以外的其他股东。通知标注单独计票的议案,意见书表决
段必须列中小投资者口径数据。

**监事会撤销软处理**:新《公司法》过渡期后上市公司监事会原则上由审计委员会
替代,但个别公司可能尚未完成撤销。材料或模板中出现"监事"字样时不径行删改,
按公司实际治理结构处理;拿不准的在底稿表标【核实】请项目组结合现行章程确认。

## 第 4 步:表决复算(用 run_python 算,不要心算)

- 逐议案:同意+反对+弃权 = 出席有效表决权股数;各比例分母正确、四舍五入合理
(合计不等于 100% 系四舍五入的,意见书按惯例注明)。
- 特别决议:同意比例 ≥ 2/3 才能写"通过";未达 2/3 结论必须写未通过。
- 单独计票:中小投资者口径的分母 = 出席会议中小投资者所持有效表决权股份数。
- 关联回避:表决基数已剔除回避股份;回避议案的出席基数与其他议案不同属正常。
- 累积投票:候选人得票 ≤ 出席股份数 × 应选名额;仅同意票。
- 出席合计:现场 + 网络 = 合计(人数与股数分别核)。
- 通知期限:通知披露日到会议日,年度股东会 ≥ 20 日、临时股东会 ≥ 15 日
(日历日,不含会议当日;不足即高风险问题,底稿表标红并在 <final> 提示)。

## 第 5 步:生成交叉核对底稿表

write_docx(markdown 表格)写入 kick-off 指定的核查底稿 parentFolderId。结构:

1. **核查概览**:会议要素汇总、材料清单(含 fileId 与文件名)、缺失材料声明;
2. **字段交叉核对表**:字段 | 通知原文 | 决议公告原文 | 投票结果/其他 | 意见书
采用值 | 结论(一致/不一致说明/【核实】);
3. **议案标签与表决复算表**:议案编号名称 | 三标签 | 同意/反对/弃权(股数与
比例) | 中小投资者口径 | 复算结论(通过/未通过/特别决议达标情况);
4. **问题与待核实清单**:编号列出全部不一致项与【核实】项,注明出处。

## 第 6 步:撰写法律意见书

**有本所模板/初稿时**(kick-off 材料清单里有「意见书模板或会前初稿」):
extract_file_text 读取,**沿用其结构、措辞、称谓定义**,只做两件事:把【】或
空缺的出席数据、表决数据按核对结果填入;发现初稿表述与材料不一致的按第 2 步
规则处理。不要改写模板既有风格。

**无模板时**按下列行业通行结构起草(金杜系样式):

1. 标题:「××律师事务所关于××公司届次股东会之法律意见书」;
2. 致:公司全称;
3. 引言六段:委托与法律依据(列《证券法》《公司法》《上市公司股东会规则》及
公司章程;限定"中国境内"不含港澳台);审查文件清单(逐项列章程、董事会
决议、通知、股权登记日股东名册、到会登记记录、网络投票统计结果、议案文件
等);公司保证段(材料真实准确完整);见证边界段(仅对程序、资格、表决
程序与结果发表意见,不对议案内容与数据的真实性准确性发表意见);勤勉尽责
与责任段;公告同意段(同意随会议文件公告);
4. 一、本次股东会的召集、召开程序:董事会届次+日期+决议召开、通知披露日期与
媒体、现场会议时间地点主持人、网络投票安排,结论"召集召开履行了法定程序,
符合法律法规和公司章程规定";
5. 二、出席本次股东会人员资格与召集人资格:现场股东及代理人 X 名代表股份 X 股
占比 X%;网络投票 X 名 X 股 X%;其中中小投资者 X 名 X 股 X%;合计;列席
人员;网络投票股东资格由网络投票系统提供机构验证的惯例表述;召集人资格
结论;
6. 三、本次股东会的表决程序、表决结果:审议议案与通知相符、无修改或新增;
现场记名投票、计票监票;网络投票及统计;逐议案表决结果(同意/反对/弃权
股数与比例;单独计票议案加中小投资者段;特别决议议案加"本项议案为股东会
特别决议事项,已经出席本次股东会的股东及股东代理人所持表决权的三分之二
以上同意通过";累积投票议案仅列同意票与当选结论;回避议案注明回避股东及
剔除基数);四舍五入说明;表决程序与结果合法有效的结论;
7. 结论意见:召集召开程序、出席人员与召集人资格、表决程序与表决结果合法有效
的综合结论;
8. 签章页:「(本页无正文,为《……》之签章页)」(书名号内与标题逐字一致)、
经办律师签名栏、单位负责人签名栏、日期(不早于会议日)。

现场出席人数、签到等材料未提供的值写【核实】占位。意见书正文不写任何解释性
文字或核查过程;过程只进底稿表。

write_docx 写入 kick-off 指定的法律意见书 parentFolderId。

## 第 7 步:收尾

<final> 中给核查结论摘要:一致项统计、发现的问题清单(按严重程度)、全部
【核实】事项、两份产出的文件名。不要复述意见书全文。

# 边界

- 不替代律师做法律判断;拿不准的一律【核实】留待项目组确认。
- 不得引入材料之外的外部事实(包括你记忆中的公司信息、法规条号细节)。
- 议案的商业合理性、信息披露的充分性不在见证范围内,不评论。
32 changes: 32 additions & 0 deletions backend/skills/shareholder-meeting-verification/skill.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 股东大会核查 Skill(打包格式见 docs/SKILL_SPEC.md)
id: shareholder-meeting-verification
name: 股东大会核查
description: 上市公司/挂牌公司股东会法律见证:对照股东大会通知、董事会决议公告与投票结果做逐字段交叉核对与表决复算,撰写(或按本所初稿填充)法律意见书,并生成可溯源的交叉核对底稿表。

# 触发条件:用户输入包含任一关键词即命中(面板 kick-off prompt 以「股东大会核查」开头)
triggers:
- 股东大会核查
- 股东会核查
- 股东大会见证
- 股东会见证
- 见证法律意见书

# prompt 模板文件名(相对本目录)
prompt: prompt.md

# 工具白名单:命中后本轮 LLM 可见工具 = allowed_tools ∪ 基础工具集(ai.skills.base-tools)
allowed_tools:
- extract_file_text
- read_file
- list_files
- search_project_files
- write_docx
- run_python
- todo_write

# 输出结构约定(随 prompt 注入)
output: |
两份 Word 交付物,均用 write_docx 写入 kick-off prompt 指定的 parentFolderId:
1.「核查底稿_公司_届次.docx」——交叉核对底稿表(逐字段三源对照 + 议案三标签 + 表决复算,每项结论注明出处)
2.「法律意见书_公司_届次.docx」——完整法律意见书
<final> 中简述核查结论(一致项数量、发现的问题清单、【核实】事项),不要复述全文。
26 changes: 5 additions & 21 deletions backend/src/main/java/com/checkba/controller/FileController.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import org.apache.tika.Tika;
import org.apache.tika.exception.TikaException;

import java.io.IOException;
Expand Down Expand Up @@ -63,6 +62,9 @@ public class FileController {
@Autowired
private com.checkba.storage.ProjectStorageResolver storageResolver;

@Autowired
private com.checkba.service.DocumentTextService documentTextService;

/**
* 文件的物理磁盘路径(「在 Finder 中显示」用)。
*/
Expand Down Expand Up @@ -609,27 +611,9 @@ public ResponseEntity<Map<String, Object>> compareDocuments(
}

/**
* 使用 Apache Tika 提取文档文本内容
* 使用 Apache Tika 提取文档文本内容(委托 DocumentTextService)
*/
private String extractDocumentText(ProjectFile file) throws IOException, TikaException {
String filePath = file.getFilePath();
if (!StringUtils.hasText(filePath)) {
// 尝试使用 wpsFileId 作为路径
filePath = file.getWpsFileId();
}

if (!StringUtils.hasText(filePath)) {
throw new IOException("文件路径为空: " + file.getId());
}

try {
Resource resource = getStorageService().load(filePath);
try (InputStream is = resource.getInputStream()) {
Tika tika = new Tika();
return tika.parseToString(is);
}
} catch (StorageException e) {
throw new IOException("加载文件失败: " + filePath, e);
}
return documentTextService.extractText(file);
}
}
Loading
Loading