Compare commits

...
293 Commits
Author SHA1 Message Date
qing 81ba942b8e feat(libmsg): extract pymsg-compatible messaging library as shared lib
- Add libmsg: C++20 header-only pymsg protocol implementation
  - Msg.hpp: pmsg_send/pmsg_recv (blocking + deadline modes) + MsgClient
  - MsgServer.hpp: multi-client TCP server with select event loop + thread pool
  - AsyncQueue<T>: generic async batch processing queue
  - RpcClient/RpcServer: JSON RPC convention layer (requires boost::json)
  - Msg_Basic.hpp/cpp: exception hierarchy, constants, version
- Fix thread safety: MsgClient _sockfd/_running use std::atomic with exchange()
- Fix deadline recv: client call() uses shared deadline for header+payload
- Fix graceful shutdown: thread pool drains queued tasks before exit
- Fix pmsg_send size check: enforce 64MiB limit (was 4GiB no-op check)
- Align AsyncQueue batch semantics with Python: batch_size is threshold
- Refactor gateway/LogC.hpp to use libmsg AsyncQueue<T>
- Update gateway Makefile to link -lus_msg, DbC.hpp to use <us/Msg.hpp>
- Add libmsg to _inst.sh build order
2026-08-06 23:14:13 +08:00
qing bbb88a96f8 fix(gateway): implement dual-track token revocation mechanism (P0-1)
Add thread-safe in-memory token revocation to fix the critical issue
where logged-out or password-reset tokens remain valid until expiry.

- Add jti (uuid v4) to all issued tokens for single-token revocation
- Add per-user version number for bulk revocation on password reset
- Add jti blacklist with TTL-based auto-cleanup to prevent memory leaks
- Introduce unified verify_token() replacing 4 copies of inline logic
- Refactor all handlers (login/logout/refresh/reset_password/proxy/whoami/
  admin_verify/ws_admin) to use the new verification pipeline
- Refresh token now revokes old jti (prevents replay after refresh)
- Bump version to 0.8.0
2026-08-06 19:01:34 +08:00
qing 7b0b49fde1 Merge pull request 'fix: 更新 gateway 管理员白名单并修复 fix_orphan_notes 脚本' (#21) from dev_qing into main
Reviewed-on: #21
2026-08-05 16:40:30 +08:00
qing 5a8ac61502 fix: 更新 gateway 管理员白名单并修复 fix_orphan_notes 脚本
- confs/configs/gateway.ini: 管理员账号从 qing@qsont.me;shufeng@qsont.me 更新为 1776177662@qq.com;277665039@qq.com
- db/fix_orphan_notes.py: 修复 _sys.path 笔误为 sys.path,补充文件末尾换行
2026-08-05 16:38:39 +08:00
qing 8b3863f3ae Merge pull request 'Dev qing' (#20) from dev_qing into main
Reviewed-on: #20
2026-08-05 16:36:10 +08:00
qing 6483719551 docs(dnfs): 同步 README 至审计 08-03 验证后的真实状态
- 主干: Token 改为委托 gateway /whoami 验签描述
- 已知问题: 标注 P0 #1/#3、P1 #5/#10 已修复,P0 #2 基本修复,其余未修复项保留
2026-08-05 16:34:34 +08:00
qing 9c73d70582 docs(gateway): 标注旧哈希自动迁移已被注释禁用
README 安全特性中"旧哈希自动迁移"实际在 main.cpp L1102-1109 被注释,标注其当前未生效。
2026-08-05 16:29:29 +08:00
qing 0348b959bb docs: 同步 static 与 waf README 至审计后真实状态
- static: 移除/标注已禁用的 reset_password 与 yhdet 系列路由,更新 check_user 为经 db 服务查询,补充代理路由 CORS 通配例外说明
- waf: 明确 CRS 需先 _fetch_crs.sh 才生效,未拉取时 WAF 开箱无拦截能力
2026-08-05 16:26:23 +08:00
qing 3ebf402efd Merge pull request 'Dev qing' (#19) from dev_qing into main
Reviewed-on: #19
2026-08-05 12:31:42 +08:00
qing 63b5243faf style(index): 首页移动端响应式优化
触屏加大可点目标,平板/手机收敛光晕与留白,超小屏进一步紧凑排版。
2026-08-05 10:48:09 +08:00
qing 1b6a767f62 feat(dnfs): 节点卡片仅标题可点击进入子节点
将进入子节点的点击事件从整张卡片收窄到标题 h3,内容/统计/操作区不再触发
跳转;同步 theme.css 将手型光标与 hover 高亮集中在标题上。
2026-08-05 10:48:09 +08:00
qing 7f6f077469 feat(dnfs): support in-node rendering for frontend code nodes 2026-08-05 10:30:29 +08:00
qing 60db2c40d8 特效保持全屏视觉,但 iframe 元素挂载到节点容器内 2026-08-05 10:15:21 +08:00
qing 08aff2656d 前端特效节点改为在节点容器内展示,不再全屏铺底 2026-08-05 10:12:47 +08:00
qing c059589e94 style(dnfs): 首页进一步美化,光晕氛围与卡片细节 2026-08-05 10:01:17 +08:00
qing d1e4b5b01a style(dnfs): 打磨首页 index 视觉细节,保持朴素风格 2026-08-05 09:25:43 +08:00
qing 897e76fd8f style(dnfs): 移除左侧用户面板按钮的 emoji 表情,保持文字简洁 2026-08-05 02:02:00 +08:00
qing ac2677abc7 feat(dnfs): 顶部搜索框卡片化美化,整体视觉和谐统一
- dnfs.html:去掉内联样式,重构为「放大镜图标 + 输入框 + 搜索按钮」结构;按钮改 <button> 避免意外提交
- theme.css:新增 .searchbox 胶囊卡片样式,与 .card/.userpanel 统一设计语言(圆角 20px、
  半透明卡片底色、浅阴影、1px 细边、橙色主按钮)
- focus-within 态:边框变橙 + 图标变橙 + 发光阴影,焦点反馈清晰
- 输入框无边框全透明,视觉焦点集中在胶囊容器
- 480px 以下移动端缩紧内边距与字号
2026-08-05 01:58:55 +08:00
qing d680dcd058 fix: 修复 WebSocket 长连接空闲断线问题
三处协同修复:
1. 前端 aibox.js 添加 25s 心跳(ping),防止 gateway 30s 空闲超时
2. gateway libhttp doWsRead 对 WS 代理连接跳过空闲超时
3. aibox.py _handle_tool_calls 启动 drain 线程实时排空输出队列,
   避免 SubAgent 等长时间工具执行期间前端收不到数据
2026-08-05 01:58:19 +08:00
qing 84a1a5e3be feat(dnfs): 顶部搜索框实时过滤当前层节点卡片
- 抽取 renderCards() 复用卡片渲染,load_notes 保存 current_children
- 卡片绑定 __item 引用,搜索时直接切换 .style.display(不重建 DOM、不清特效)
- 按 title/owner/content 子串匹配;信封节点链接/代码/文件名纳入搜索,排除图片/文件的 base64 数据
- 支持实时 input 过滤、回车触发、ESC 清空;无匹配时显示提示
- 切换/返回节点时自动清空搜索关键词
2026-08-05 01:52:52 +08:00
qing 058eb11f5f docs(aibox): 更新 README 补充 express_emotion 工具与 /search 命令(移除过时说明、命令表新增 /search、工具表新增 express_emotion、消息类型表补充 task_list/task_completed、更新日志新增 v2.8) 2026-08-05 01:51:10 +08:00
qing 85c9699758 feat(dnfs): 用户面板折叠时不再显示用户名,仅显示展开按钮 2026-08-05 01:42:51 +08:00
qing 9dd16e0eef feat(dnfs): 进入新节点时先清除上一节点特效再加载当前节点特效 2026-08-05 01:29:49 +08:00
qing 28894e48dc feat(dnfs): 用户面板下移并新增无特效模式开关;管理员增加运维机器人/进程管理器入口 2026-08-05 00:49:30 +08:00
qing 924c718005 fix(waf): WebSocket 劫持后清除 http.Server 读写 deadline,避免长连接被强制断开 2026-08-05 00:37:11 +08:00
qing d71f526af4 feat(aibox): 子代理思考过程实时输出到主对话侧栏
- run_full_subagent 新增 output_callback 参数,转发子代理 reasoning 事件
- SubAgentTool 新增 _subagent_output 回调,将子代理思考过程以 line 流式输出到前端
2026-08-05 00:16:34 +08:00
qing 09556c9ef4 feat(aibox): run_command 强制流式输出;子代理升级为完整工具代理;补充情绪应对策略
- run_command 移除 stream 参数,删除非流式 subprocess.run 执行路径,统一走流式输出
- 子代理 (create_subagent) 从轻量无工具升级为拥有全部工具的完整代理,支持多轮工具调用与 max_turns 限制,结束后自动总结
- 系统提示词补充子代理使用建议与情绪应对策略
2026-08-05 00:09:25 +08:00
qing cc671a9fce feat(dnfs): 含前端内容的节点渲染到当前页面时自动执行(隔离iframe沙箱启动特效) 2026-08-04 23:57:39 +08:00
qing f7ed1f785b fix(dnfs): 前端代码节点改为隔离iframe沙箱运行,特效不再污染父页面导致运行后无法进入节点 2026-08-04 23:51:21 +08:00
qing 2ff7908bc7 feat(db): _view_db -n 改为取最后 N 行
按 rowid 倒序取末尾 N 行,外层再正序展示,便于查看最新日志;
-w 条件放入内层子查询,语义为"最后 N 条匹配记录"。
2026-08-04 23:44:59 +08:00
qing 745ce06f85 fix(dnfs): 前端代码节点运行改为局部输出缓冲+捕获窗口自动恢复console,避免应用自身日志(pid)刷进输出框干扰导航 2026-08-04 22:47:48 +08:00
qing f17a6a8233 fix(dnfs): 前端代码节点注入脚本包IIFE隔离作用域,避免覆盖页面全局导致运行后无法进入节点 2026-08-04 22:29:10 +08:00
qing 30360cb106 feat(dnfs): 前端代码节点支持完整HTML/CSS/JS并注入当前页面
将内联 JS 节点扩展为内联前端代码节点:内容存完整 HTML 文档,
运行后解析注入当前页面——style 注入 head、script 在全局作用域执行
可操作整页加特效、body 渲染到节点下方容器,console 输出捕获到下方。
2026-08-04 22:24:33 +08:00
qing ec3df34b43 feat(db): 新增修复 Notes 表 pid 悬空节点的脚本 2026-08-04 22:19:05 +08:00
qing c7441c1c6f fix(dnfs): JS代码节点改用<script>注入执行以兼容CSP
new Function/eval 被 CSP(script-src self unsafe-inline) 拦截,改用注入内联
<script> 执行,避免引入 unsafe-eval。代码在全局作用域运行可操作页面加特效,
console 输出经 hook 捕获到输出区(保留原 console 行为)。
2026-08-04 22:11:19 +08:00
qing fed2d42472 refactor(dnfs): JS代码节点改为在当前页面执行以支持页面特效
移除 iframe 沙箱方案,改为 new Function 在当前页面上下文执行,
代码可直接操作 document/window 对页面施加特效;console 输出捕获到下方区域。
2026-08-04 22:08:15 +08:00
qing 2918f2c83c feat(dnfs): 特殊节点类型由网页代码(html)改为JS代码(js)
将 _sp 标识从 html 改为 js,运行方式由新窗口 document.write 改为
插入页面 iframe 沙箱执行,捕获 console 输出,postMessage 自适应高度。
后端 dnfs.py 白名单、校验、审核文本同步更新。
2026-08-04 22:05:01 +08:00
qing 87538e7729 fix(waf): 豁免扩展到所有笔记写接口 (add/update/edit/delete_note)
修正正则匹配 query 前缀问题,统一豁免笔记接口的 CRS 误报规则。
2026-08-04 21:52:15 +08:00
qing fd088f5959 fix(waf): 对 /add_note 接口豁免 CRS 误报规则
嵌入式/技术笔记含 C 代码(if()/select/sbit)被 SQLi/命令注入规则误判拦截。
仅移除误报规则(942170/942230/932xxx/934xxx),其余 CRS 规则及接口仍生效。
2026-08-04 21:32:14 +08:00
qing 15f8c9552d fix(core,mgmt): list 返回 running+command 字段,命令列不显示为空
原 list 把 check()(命令字符串)放进 status 字段,前端误当状态显示,
命令列取不存在的 command 字段导致显示为空。

修复:list 返回 running(bool) + command(string),前端对应显示状态与命令。
2026-08-04 21:31:21 +08:00
qing 43c5821d3d fix(gateway): 修复代理 URL 双斜杠导致下游 404
handleProxy 拼接转发 URL 时,target_path 以 '/' 开头,
url = t_url + '/' + target_path 会产生双斜杠(如 //list)。
core 等使用精确路径匹配的下游服务返回 404。

修复:仅当 target_path 不以 '/' 开头时才补斜杠。
2026-08-04 21:20:24 +08:00
qing 1d7641cc89 fix(core): 纯 HTTP 启动 HTTP 服务(不需要 TLS,符合项目约束)
core 的 HTTP 服务仅供 gateway 内部代理,且 WAF 已在前端终止 TLS。
原代码强制加载 server.crt/server.key,但证书不存在,导致 core 无法启动,
所有请求被 gateway 返回 500 'Internal Server Error'。

修复:使用无 TLS 的 3 参数 HttpServer 构造函数,纯 HTTP 启动。
2026-08-04 21:11:35 +08:00
qing 6a68b3393d docs(waf): _fetch_crs.sh 生成的 coraza.conf 模板指向 tuning.conf
避免重跑脚本覆盖调优配置,模板不再包含重复的阈值/偏执规则
2026-08-04 21:11:26 +08:00
qing 0700491300 取消注释 2026-08-04 21:02:08 +08:00
qing d036af935f feat(waf): 启用 OWASP CRS 异常评分制自动拦截
- SecRuleEngine On 自动拦截异常评分达阈值的请求(规则 949110)
- 新增 rules/local/tuning.conf 调优文件(偏执等级 1 + 阈值 5/4)
- tuning.conf 独立于自动生成的 crs/coraza.conf,重跑 fetch 不覆盖
2026-08-04 20:40:25 +08:00
qing 34cd295005 feat(core): mgmt 接入 core 进程管理 + 管理员认证
- gateway 新增 POST /admin/verify 验签接口,供内部服务校验管理员 Token
- core 的 /list /exec /kill 接入管理员认证(调用 gateway 验签,fail-closed)
- gateway 代理 opened 白名单加入 core:5200
- mgmt 管理页显示当前用户与角色,进程页接入 core 进程管理接口
2026-08-04 20:00:24 +08:00
qing c8812dd27c static: 迁移 mgmt 前端到 static/public 并移除静态列表
将 mgmt Jinja2 模板原封不动迁移为纯静态 HTML/CSS,
删除用户列表与模块列表,改为静态提示文字。
2026-08-04 19:23:57 +08:00
qing 3460a157d0 docs: 新增 libth 安全审计与 aibox 开放版开发计划 2026-08-04 19:02:22 +08:00
qing fe618be1fa fix(captcha): 验证码存储容量上限防 OOM (P1-1)
新增 MAX_STORE_SIZE(默认 10000,可配置 max_store/CAPTCHA_MAX_STORE),
_handle_image 在清理过期项后检查容量,超限拒绝生成并返回错误,
防止攻击者高频生成验证码填满内存。
2026-08-04 18:40:36 +08:00
qing 4d9bb5d940 fix(libhttp): WebSocket 发送/代理队列上限防 OOM (P1-6)
- 新增 setMaxWsQueueSize()(default 1000),每个入队点检查
- wsSend / onProxyReadClient / onProxyReadBackend 超限关闭连接
- 更新审计文档 P1-6 状态、README v3.4.3
2026-08-04 18:27:50 +08:00
qing 37c6d401d6 fix(libhttp): 服务器读超时防 Slowloris (P1-4)
- 新增 setHeaderTimeout()(default 10s) / setReadTimeout()(default 30s),对齐 WAF
- doSSLHandshake 设置头超时覆盖握手,doRead 拆分为 async_read_header (头超时) + async_read (体超时)
- doWsRead 每次读取设置空闲超时,自动清理长期空闲连接
- 更新审计文档 P1-4 状态、README v3.4.2
2026-08-04 17:39:27 +08:00
qing 507054c4b2 fix(libhttp): WebSocket 单条消息大小限制防 OOM (P1-3)
- 新增 setMaxWsMessageSize/getMaxWsMessageSize(默认 16MB,与 Beast 默认一致)
- 服务端 doWsUpgrade/doWssUpgrade 与代理流 onProxyConnect/onProxySslHandshake 创建后调用 read_message_max
- 超限消息由 Beast 在分配内存前拒绝并关闭连接
- 更新审计文档 P1-3 状态、README v3.4.1
2026-08-04 16:03:06 +08:00
qing 85e0d0740b fix(libhttp): HttpClient 响应大小限制防 OOM
- request_stream_sync 改为真流式,按块回调不二次缓冲
- 新增 set_max_response_size/get_max_response_size(默认 10MB,对齐 WAF)
- write_callback 超限返回 0 中止传输并抛异常
- 更新审计文档 P1-2 状态、README v3.4.0、INDEX 移除不存在的文档
2026-08-04 15:12:06 +08:00
qing dc52ec13ad fix(aibox): 工具结果超长仅返回提示,隔离各会话命令执行器
- 工具结果超过 max_result_length 时不再保留截断内容,仅返回提示信息,避免超大结果污染 AI 上下文

- 每个任务/WebSocket 连接使用独立 SubprocessCommandExecutor,避免 cancel() 误杀其他任务/用户正在运行的子进程
2026-08-04 13:11:37 +08:00
qing 5ff1dfa4d2 系统状态技能 2026-08-04 12:57:27 +08:00
qing 2eb6d3c310 生成技术手册技能 2026-08-04 12:46:50 +08:00
qing d921f84b83 feat(aibox): WebSocket连接用户独立AI实例,支持多用户并发对话
- 每个连接创建独立DeepSeekChat实例,拥有独立messages/工具注册表/消息锁
- 对话状态与并发控制从全局改为会话级,多用户可同时与AI对话互不干扰
- 中断/重置/重载提示词等命令改为针对当前连接的会话AI
- 技能管理器懒加载共享,避免每个连接重复扫描技能
2026-08-04 11:44:57 +08:00
qing 69a8eee192 feat(aibox): 调度器任务间上下文隔离
- 每个定时任务创建独立DeepSeekChat实例,拥有独立messages/工具注册表/消息锁
- 任务与WebSocket主对话互不干扰,消除并发冲突与上下文污染根因
- 共享技能管理器懒加载一次供所有任务复用,避免重复扫描技能
- DeepSeekChat支持传入skill_manager复用,任务实例history_manager=None不污染历史
2026-08-04 09:23:51 +08:00
qing fa9679b7a6 fix(aibox): 修复并发线程导致对话历史污染引发HTTP 400错误
- 添加线程锁(RLock)保护 self.messages,防止WebSocket与调度器线程并发修改
- 重写 _repair_tool_pairs 方法,支持穿透system消息查找tool响应、去重、清理孤立tool消息
- 新增 _validate_enhanced_messages API调用前最终校验,就地修复消息结构
- 调度器执行定时任务前先cancel+wait锁释放,避免与WebSocket对话冲突
- /new命令和global_exit_handler改用锁保护的reset_conversation,不再裸改messages
- 新增 is_busy() 非阻塞方法供外部判断AI是否正在处理中
2026-08-04 09:11:31 +08:00
qing 53ddcb364f feat(aibox): 创建/安装技能工具统一生成 .md 扩展名 + 下线旧版 smtp.py
- CreateSkillTool/InstallSkillTool 改为生成带 .md 扩展名的技能文件,与人格技能文件规范一致
- 同步更新 7_extension_skills.md 模板与工具描述
- 删除旧版 smtp.py(P0-1)并同步审计文档与 README
2026-08-04 08:26:57 +08:00
qing fb485ebac4 fix(libhttp): 修复P1-5静态文件路径遍历 + 同步审计文档
- 新增 isSafeStaticPath() 校验静态文件路径,拒绝 ..、空字节、非/开头及URL编码遍历(%2e/%2f/%5c)
- handleRequest 静态文件服务前校验 target_path
- 审计文档标记 P1-5 已修复
2026-08-04 00:04:55 +08:00
qing da4957c52d fix(libhttp): 请求体大小限制可配置并超限返回 413 (P1-1) 2026-08-03 23:57:00 +08:00
qing b37bf255d9 fix(libhttp): 删除遗留 WebSocketServer/WebSocketClient 并同步 CMakeLists 与 Makefile 2026-08-03 23:46:10 +08:00
qing 2f5fe8baaa docs(security): 新增 libhttp 模块安全审计报告 2026-08-03 23:42:32 +08:00
qing 2e3f07bf4f docs(gateway): 审计文档行号纠偏并补充 CORS 白名单死代码根因
- 修正 gateway 审计文档中因代码演进漂移的行号引用(handleProxy/handleLogout/wrapperHttp/限流/TLS/路径防护等)

- 补充 P2-3 CORS 根因:_CORS_ORIGINS 白名单已读取但 OPTIONS 预检由 libhttp 处理,白名单未生效

- 修正 README 中 OPTIONS 预检描述(反射请求 Origin 而非通配 *)及 CORS 配置失效提示
2026-08-03 23:37:47 +08:00
qing 028e0e0806 docs(security): 删除全局审计文档并将问题并入各模块文档
- 各模块专属 P0/P1/P2 问题已分发到 docs/security/ 下各模块审计文档

- gateway 补充 P1-6(反向代理响应无大小限制)、P2-3(CORS 通配)、P2-4(HTTP 无超时)

- 删除全局 SECURITY_AUDIT_20260801.md,避免重复
2026-08-03 23:32:28 +08:00
qing 37c50ed517 fix(static): 获取邮箱验证码后保留已输入的图形验证码
refreshCaptcha 增加 preserveInput 参数,获取验证码后的自动刷新不再清空图形验证码输入框,避免误清空用户输入
2026-08-03 23:11:10 +08:00
qing 02904f711e fix(aibox): 修复工具调用配对缺失导致的 insufficient tool messages 400
当 assistant 携带 tool_calls 但对应 tool 响应缺失(工具被中断/压缩丢弃)时,
下一次请求报 400。新增 _repair_tool_pairs 在构建请求前补齐缺失的 tool 响应。
2026-08-03 23:05:28 +08:00
qing d24066369b refactor(aibox): 解耦大模型调用,新增 llm_client 抽象层以支持多厂商切换
将 think_and_respond 与 _compress_via_llm 中的直接 DeepSeek API 调用
收敛到 LLMClient 抽象层(DeepSeekClient 实现 + create_llm_client 工厂),
统一流式事件格式,为子代理/多模型切换预留扩展点;配置新增 api.provider。
2026-08-03 23:03:03 +08:00
qing 417fda5479 refactor(aibox): 文件/网络操作工具改为返回命令行指引并移除beautifulsoup4依赖 2026-08-03 22:52:12 +08:00
qing fb5920d89f feat(gateway): 新增重置密码接口并改造前端 forgot 页对接
- gateway 新增 POST /reset_password:复用注册校验链(限流/密码强度/邮箱格式/CAPTCHA/邮箱验证码),最后一步由创建用户改为 UPDATE Users SET password_hash,用户不存在时统一返回成功防枚举
- gateway 新增 POST /reset/send_code:CAPTCHA 强制 + 邮箱双窗口限流,定制重置密码文案
- 前端 forgot.html 弃用 static 被禁用的 /api/proxy_* 接口,对接 gateway 新接口,并改为中文 register.html 风格
2026-08-03 22:40:43 +08:00
qing dd4fef6dee fix(pymsg): 异常信息脱敏与JSON深度/大数字限制,防信息泄露与DoS 2026-08-03 22:30:17 +08:00
qing 87f209af73 fix(pymsg): 增加连接数与线程池队列上限,防资源耗尽
- P1-1: MsgServer 新增 max_connections(默认1000),_accept_client 超限
  直接拒绝并关闭新连接,防连接耗尽攻击
- P1-2: 新增 _BoundedThreadPoolExecutor + max_queue_size(默认1000),
  慢 handler 时任务排队超限即拒绝并断开连接,防内存无限增长
同步更新审计报告 P1-1/P1-2 状态,修复率 18.75% -> 25%
2026-08-03 22:25:32 +08:00
qing 47ab9f2fe6 fix(pymsg): 使用 deadline 模式修复 recv 总超时绕过(Slowloris DoS)
_recv_exact 从单次 sock.settimeout() 改为整体 deadline 模式:
header 与 payload 共享同一 total deadline,每次 recv_into 前按剩余
时间收紧超时,逐字节发送无法刷新 deadline,半开连接被及时关闭。
同步更新 pymsg 安全审计报告 P0-1 状态,修复率 6.25% -> 12.5%。
2026-08-03 22:15:44 +08:00
qing ac16d89150 docs(aibox): 添加文件/网络操作命令行化开发计划 2026-08-03 22:05:52 +08:00
qing f53085b2a6 fix(static): 禁用 yhdet 系列功能并迁移数据库访问到 db 服务
- 注释禁用 yhdet OAuth 相关常量、辅助函数和视图函数(yhdet_callback/yhdet_login/yhdet_verify/proxy_send_code),消除 P0-2/P0-3/P0-4/NEW-1/NEW-2 风险

- YHDET_SECRET 置空并标注已失效;PRIVATE_SECRET 与 YHDET_PRIVATE_API 一并注释

- 新增 _db_call() 辅助函数,api_check_user 改经 pymsg 调 db 服务,不再直连 SQLite

- 修复注册页验证码:点击获取邮箱验证码时图形验证码输入框被清空的问题

- 更新 static 安全审计报告,修复率 25% → 65%
2026-08-03 22:05:02 +08:00
qing 4749e58eeb fix(static): 禁用无认证的 /api/reset_password 接口 2026-08-03 14:26:06 +08:00
qing 5fbe3ed5e4 docs(security): 更新全部模块审计报告,添加代码验证记录
逐模块对照实际代码验证审计报告中的问题修复状态,添加代码验证记录、逐条验证结果、修复率统计。合并aibox两份审计报告,删除重复文件。修复率:confs 61% > dnfs 60% > ai_review 50% > smtp 37.5% > db 33% > core 12.5% > gateway 12.5% > pymsg 6.25% > static 5% > captcha/aibox/BotOps 0%
2026-08-03 14:12:45 +08:00
qing d3158c2420 docs: add date suffix to remaining documentation files
- Rename SECURITY_HARDENING_PLAN.md -> SECURITY_HARDENING_PLAN_20260803.md
- Rename aibox_SECURITY_AUDIT.md -> aibox_SECURITY_AUDIT_full_20260801.md
- Update INDEX.md links accordingly
2026-08-03 13:26:38 +08:00
qing a7a78d1424 docs: aggregate project documentation under docs/ with unified index
- Move all per-module security audit reports to docs/security/
- Move code review logs to docs/reviews/, analysis reports to docs/reports/
- Add docs/INDEX.md as single entry point for all project documentation
- Add documentation management conventions section to README.md
- Clean up empty per-module docs/ directories
2026-08-03 13:21:48 +08:00
qing 9dbe395484 docs: 添加内网通信安全加固开发计划 2026-08-03 13:13:52 +08:00
qing 9f440ee884 fix(db): 监听地址改为 127.0.0.1 修复内网暴露 2026-08-03 13:10:17 +08:00
qing 4449a23ba1 docs(lci): 完善 LCI 图形系统架构文档 2026-08-02 21:37:02 +08:00
qing 40b9b9c92f Merge pull request 'Dev qing' (#18) from dev_qing into main
Reviewed-on: #18
2026-08-02 21:33:21 +08:00
qing 0493383ee8 遗漏的部分 2026-08-02 21:21:03 +08:00
qing 84fb370da8 docs(lci): 添加 LCI 帧缓冲图形界面能力介绍到 README 2026-08-02 21:18:19 +08:00
qing 8499631091 add old lci module 2026-08-02 20:15:07 +08:00
qing 38a6e815f0 fix(static): __view.html 改为绝对路径,修复子目录下 CSS/JS 404 2026-08-02 16:41:08 +08:00
qing c58ef2b873 fix(static): 修复令牌刷新死循环 + 补全 index/aibox 令牌管理
- expired estimate 改用 created_at 替代 iat,与网关校验逻辑一致

- index.html 和 aibox.html 补全 token_manager.js 引用
2026-08-02 16:11:23 +08:00
qing 52052e51a8 feat(aibox): 在pre_prompt中新增情绪感知提醒
每轮首轮对话的用户输入前,新增【情绪感知提醒】段落,引导AI:
- 识别用户情绪状态并共情回应
- 按情绪状态调整回应策略(安抚/分享/道歉)
- 用户要求特定情绪模式时搜索对应技能
2026-08-02 14:30:20 +08:00
qing ffbe076a32 feat: add execute method to ExpressEmotionTool 2026-08-02 14:17:33 +08:00
qing 92820799fa 补全情绪化工具 2026-08-02 14:02:51 +08:00
qing 054f65f858 增加智能体生气的模板 2026-08-02 13:54:58 +08:00
qing 918f3ea32a fix(aibox): reasoning展示严格遵循配置文件reasoning_enabled开关
之前即使reasoning_enabled=false,模型自发返回的reasoning_content
仍会被动态展示——配置只管了API请求参数,不管展示逻辑。
现在添加constants.REASONING_ENABLED门控,关着时静默忽略。
2026-08-02 13:26:34 +08:00
qing b216535757 comment 2026-08-02 13:21:45 +08:00
qing 45db2f780b refix core initialize list 2026-08-02 12:56:14 +08:00
qing c1724ccea9 fix(redirect80): extract .content from confs RPC response dict
RpcClient.call() returns the data dict, not a raw string.
configparser.read_string() expects str, hence TypeError.
Now uses .get("content") to unwrap the ini text.
2026-08-02 12:52:51 +08:00
qing f9c02cd6ad feat(redirect80): new HTTP→HTTPS redirect service on port 80
Pure Python stdlib http.server + ThreadingMixIn, zero web framework deps.
- GET/HEAD → 301, POST/PUT/DELETE/PATCH → 308
- Reads Host header, strips port, redirects to https://{host}{path}
- Config via confs (retry_on_refused, hard dependency) + env var override
- Logs via log_client → db Logs table (fail-safe async batch)
- Port 80 privilege via setcap cap_net_bind_service in _run.sh
2026-08-02 12:50:23 +08:00
qing c7364286b7 fix(static): getNode仅401跳登录,其他异常提示原因并上抛 2026-08-02 12:19:02 +08:00
qing c3c07ab06d Merge pull request 'Main' (#17) from main into dev_qing
Reviewed-on: #17
2026-08-02 11:51:31 +08:00
qing 2e210e4729 chore(db): gitignore 忽略 SQLite WAL/SHM 运行时临时文件 2026-08-02 11:49:42 +08:00
qing 5a3dff9026 fix(static): 修复dnfs导航栈污染与返回键栈空越界
- 点卡片/返回上一层前先经 confirmLeaveIfDirty 守卫确认,用户选择
  不放弃输入时不再执行 push/pop,stack 保持原样
- 返回键目标 id 判断由 length>=0(恒真,空栈取到 undefined)改为
  length>0,空栈正确回根节点 0
2026-08-02 11:46:51 +08:00
shufeng2012 7e4fd7e54a Add search input border. 2026-08-02 09:04:53 +08:00
qing 8200cbfba7 refix 2026-08-02 01:19:56 +08:00
qing 192610615e feat(captcha): 接入 confs 配置中心并迁移字体路径
- confs/configs/captcha.ini: 新增 captcha 配置 (server + captcha 段, 含 font_path)
- captcha/log_client.py: 新增日志客户端副本 (service_name=captcha)
- captcha/captcha_service.py: 接入统一的 confs 配置加载模式
  (confs 强依赖 retry_on_refused -> 环境变量覆盖 -> 完整性校验)
  - CAPTCHA_LEN/CAPTCHA_TTL 改为可配置
  - 字体路径迁移到 confs, 新增 CAPTCHA_FONT_PATH 环境变量覆盖与 PIL 回退兜底
  - 关键路径 (启动/生成/校验失败) 接入 log_client 日志上报
2026-08-02 01:09:44 +08:00
qing 2bb1d69d8a feat(static/aibox.js): 同步BotOps修复到Web前端
同步以下修复:
1. endReasoning()/abortReasoning() 结束后清除引用(DOM保留显示),
   多轮工具调用时每轮创建独立思考区域,按时间顺序分布
2. 新增 breakAi() 分段函数,处理 ai_break 信号:工具调用前分段
   AI文本,后续chunk创建新气泡,避免思考区域被夹在AI文本中间
3. line消息过滤前缀扩展:增加分析工具结果/需要调用工具等
4. 未知消息类型fallback从appendAi改为addTaskLine,防止非AI内容污染聊天区
2026-08-01 22:19:54 +08:00
qing 392a734707 fix(ai_review): 添加内容大小限制和频率限制
/review: 10KB内容上限(413)、30次/分钟/IP(429);/rag/feedback: 5次/分钟/IP。使用滑动窗口限流器,优先取X-Forwarded-For作为客户端IP。
2026-08-01 22:12:58 +08:00
qing 89b374c21d fix(pymsg): 捕获 UnicodeDecodeError 防止同步模式服务崩溃
recv() payload.decode("utf-8") 抛出 UnicodeDecodeError(ValueError子类)不在 _handle_client 异常捕获列表中,同步模式(workers=0)下收到无效UTF-8字节会导致整个服务进程崩溃。修复:recv()中将UnicodeDecodeError包装为ConnectionError;_handle_client()异常列表增加防御性兜底。
2026-08-01 22:11:38 +08:00
qing af5cd02663 fix(aibox/botops): 思考区域按时间顺序分布,不再全部堆在顶部
问题根因:
1. ChatView.end_reasoning() 不清空 _reasoning_widget 引用,导致
   多轮工具调用时 start_reasoning() 直接 return,所有轮次的思考
   内容都被追加到第一个思考区域(在聊天区最顶部)。
2. AI widget 在工具调用期间不重置,导致后续轮次的思考区域被
   挂载在AI widget中间,顺序混乱。

修复:
1. end_reasoning()/abort_reasoning() 更新标签后清除引用(widget
   保留在DOM显示),下一轮 reasoning_start 创建新的思考区域。
2. 新增 ai_break 协议信号:服务端在工具调用执行前发送,前端
   break_ai() 清除AI widget引用(保留已显示内容),后续chunk
   创建新的AI widget,确保思考区域按时间顺序插入在两段AI文本之间。
3. ai_break 前flush chunk_buffer,防止工具调用前的最后文本片段丢失。
2026-08-01 21:10:42 +08:00
qing 763faf9c83 Merge branch 'dev_qing' of https://git.cancerai.top/qing/micro_servers into dev_qing 2026-08-01 21:02:49 +08:00
qing f3d06252bb fix(aibox/botops): 修复两处重复输出根因
1. 侧栏重复:_handle_tool_calls 中 msg_type in ("line", "complete")
   将流式工具末尾的 complete 信号也当作 line 转发,导致所有支持
   流式输出的工具(get_current_time、run_command、read_file等20+个)
   结果在侧栏显示两次。改为仅转发 "line" 类型。

2. 聊天区Markdown列表重复:Textual 的 Markdown.append() 在 chunk
   边界切断 Markdown 语法(如列表项 "- "被拆成两个chunk)时,增量
   解析可能导致内容重复渲染。改为累积完整文本后调用 update() 整体
   重新解析,避免增量解析的边界问题。
2026-08-01 21:01:31 +08:00
qing b60ac06d55 Merge branch 'dev_qing' of https://git.cancerai.top/qing/micro_servers into dev_qing 2026-08-01 20:57:02 +08:00
qing 66963092a3 feat(static): 优化社区首页 — Hero首屏+特性卡片+悬浮毛玻璃导航,CTA随登录态切换 2026-08-01 20:56:39 +08:00
qing 27ab50ae68 fix(aibox/botops): 修复重复输出问题
1. 服务端:is_continuation(工具调用后递归继续)时不再重复发送
   "思考中"提示和reasoning_start信号,避免多轮工具调用时侧栏
   出现多条冗余"思考中"消息
2. BotOps:line消息过滤前缀从5个扩展到9个,过滤掉"分析工具结果"
   "需要调用工具"等被后续具体信息覆盖的过渡提示
3. BotOps:移除危险的fallback分支(未知消息类型直接append到AI
   聊天区),改为写入侧栏日志,防止非AI内容污染回复
2026-08-01 20:53:40 +08:00
shufeng2012 627950394d ✨ 添加代码块语法高亮功能 2026-08-01 20:08:51 +08:00
qing 16b059253b Merge pull request 'Dev qing' (#16) from dev_qing into main
Reviewed-on: #16
2026-08-01 16:18:56 +08:00
qing f701730016 fix(aibox): 修复中断信号延迟生效问题,确保Ctrl+C能立即中断AI响应
根因:WebSocket消息接收循环使用await串行处理,导致interrupt消息在
AI整段对话完成后才被读取和处理。

修复内容:
1. 消息接收与处理解耦:用create_task异步处理消息,不再await阻塞接收循环
2. cancel()直接同步调用而非通过run_in_executor,消除线程池调度延迟
3. 强制关闭response.raw底层socket连接,中断阻塞的iter_lines() recv()
4. 添加_is_processing_message标志防止多消息并发污染对话历史
5. 连接关闭时自动取消正在进行的处理任务
2026-08-01 14:55:49 +08:00
qing f319686571 feat(static): aibox Web客户端重写为移动端专版
- 移除桌面端媒体查询与限宽居中,任务面板固定为抽屉形态
- 新增 enterkeyhint=send、overscroll-behavior、tap-highlight 等移动端触控优化
- 配合已提交的中断机制改动,协议与 BotOps 保持兼容
2026-08-01 14:17:36 +08:00
qing 08f737ba2c fix(static): aibox Web客户端适配中断机制(Ctrl+C可靠停止)
问题(与BotOps v1.3修复一致):
- interrupt_ack只显示系统消息,没有立即收尾前端状态
- 思考区域一直停留在"💭 思考中..."状态(服务端中断不发reasoning_end)
- AI状态未重置,残留reasoning片段可能重新创建思考区域

修复:
- 新增abortReasoning()函数:中断时更新思考区域标签为"🛑 思考已中断"(保留已展示内容)
- appendReasoning()增加aiActive===false检查,中断后丢弃残留片段
- interrupt_ack处理改为:先abortReasoning()更新思考区域,再finishAi()收尾AI状态,最后显示通知
- 中断后残留chunk/reasoning/complete均被安全忽略(appendAi已有aiActive检查,finishAi幂等)
2026-08-01 14:14:53 +08:00
qing c7ef7e715c fix(botops): v1.3 - 适配服务端中断机制,Ctrl+C立即可靠停止
问题:收到interrupt_ack时仅显示系统通知,没有立即收尾前端状态:
- 思考区域一直停留在"💭 思考中..."状态,不会更新
- AI回复状态未重置,后续残留chunk可能继续追加
- 服务端不发送reasoning_end,思考区域无法正常结束

修复:
- ChatView新增abort_reasoning()方法:中断时将思考区域标签更新为"🛑 思考已中断"(保留已展示内容)
- append_reasoning()增加_ai_finalized检查,中断后丢弃残留思考片段,防止重新创建思考区域
- app.py收到interrupt_ack时:先abort_reasoning()更新思考区域,再finish_ai()收尾AI状态,最后显示系统通知
- 中断后服务端发来的残留chunk/reasoning/complete均被安全忽略(append_ai已有_finalized检查,finish_ai幂等)
- 更新README v1.3日志
2026-08-01 14:12:27 +08:00
qing 9d737897df fix(aibox): v2.6 - 修复Ctrl+C中断无法停止服务器的问题
问题根因:
1. WebSocketHandler.handle_message 完全没有处理 interrupt 消息类型(前端Ctrl+C发送的消息被丢弃)
2. DeepSeekChat 缺少取消机制,阻塞的 API 请求(iter_lines)和子进程无法被中断

修复方案 - 三层取消机制:
1. threading.Event 取消标志:流式响应循环每轮检查 _is_cancelled(),工具调用前检查
2. HTTP连接关闭:保存 _current_response 引用,cancel() 时调用 response.close() 中断 iter_lines()
3. 子进程终止:SubprocessCommandExecutor 添加 cancel() 方法,保存 _current_process,cancel时kill

修改内容:
- DeepSeekChat: 添加 _cancel_event/_current_response/_command_executor 字段,cancel()/_is_cancelled()/_check_cancelled() 方法
- SubprocessCommandExecutor: 添加 __init__/_process_lock/cancel(),execute()/execute_streaming() 中设置和清除进程引用
- WebSocketHandler: 添加 handle_interrupt() 处理 interrupt 消息,回复 interrupt_ack 后调用 ai_manager.cancel()
- EnhancedAIManager: 添加 cancel() 统一入口,同时取消AI和命令执行器
- think_and_respond: 首轮清除cancel标志,流式循环检查取消,API/JSON/未知异常中均检查取消标志
- _handle_tool_calls: 每个工具调用前检查取消信号
- 更新README v2.6日志和API文档
2026-08-01 14:09:24 +08:00
qing b935fefb82 refactor(gateway): remove local ini fallback, configs only via confs
Follow the same convention as the static module: remove local config.ini
fallback and --config/-i command-line argument entirely. All configuration
is now managed exclusively through confs service.

- Delete gateway/example.ini (moved to confs/configs/gateway.ini).
- Remove -i/--input option from boost::program_options; help (-h) and
  version (-v) are the only CLI flags remaining.
- Simplify main.cpp config loading: remove local-file branch, always fetch
  gateway.ini from confs with retry_on_refused=true waiting for confs ready.
- Update _run.sh: no longer passes any arguments; config comes from confs.
- Update README: remove example.ini references, -i flag docs, local file
  mode description, and example.ini from directory structure tree.
- Clarify changelog entry for v0.7.0.
2026-08-01 14:00:26 +08:00
qing 9ccb9cbdca feat(gateway): migrate configuration to confs service (v0.7.0)
Migrate gateway from local -i ini file to the unified confs configuration
service, following the same two-layer pattern as Python services (dnfs/db/static):

- Add confs/configs/gateway.ini with all existing settings plus new
  [Downstream] section for db/captcha/smtp host:port (previously hardcoded).
- Conf.hpp: add fromString() to parse ini from text (confs response),
  env()/env_int() static helpers for environment variables, public default
  constructor, and empty-filename guard in file constructor.
- DbC.hpp: add init(host, port) static method and static _host/_port members,
  replacing hardcoded "127.0.0.1":8005. Defaults preserved for backward compat.
- main.cpp:
  * Rewrite config loading: bootstrap CONFS_HOST/CONFS_PORT from env
    (default 127.0.0.1:8010), fetch gateway.ini via pymsg from confs
    (retry_on_refused=true), parse with ConfIni::fromString().
  * -i flag is now optional; when provided, reads local ini (dev/debug mode).
  * Add downstream host/port globals (_DB_HOST/_DB_PORT, _CAPTCHA_HOST/_CAPTCHA_PORT,
    _SMTP_HOST/_SMTP_PORT, _CONFS_HOST/_CONFS_PORT), populated from [Downstream].
  * Environment variable overrides (highest priority): LISTEN_ADDR/LISTEN_PORT,
    DB_HOST/DB_PORT, CAPTCHA_HOST/CAPTCHA_PORT, SMTP_HOST/SMTP_PORT.
  * Config integrity validation: required fields (Server.address/port,
    Proxy.default, Downstream.db_host/db_port) cause startup failure if missing.
  * Initialize DbC::init() and logs().set_endpoint() after config load.
  * Replace all hardcoded "127.0.0.1":8005/8008/8009 in captcha/smtp/db calls
    with configurable globals.
  * Add downstream service info to startup log output.
  * Fix: -v/--version flag now actually prints version and exits.
  * Bump version to 0.7.0.
- _run.sh: remove -i example.ini default; now starts without args (confs mode),
  passes "$@" through for optional -i override.
- Update README with new config loading docs, Downstream config section,
  updated directory structure, and v0.7.0 changelog.
2026-08-01 13:55:18 +08:00
qing de58c0487a fix(dnfs): validate token via gateway /whoami with RSA signature verification
- Replace unsafe base64-only decode_token() with HTTP POST to gateway /whoami,
  which performs RSA signature verification and expiry checks internally
- Fail-closed on gateway unreachable: return 503 instead of allowing access
- Add [gateway] config section (url/timeout) with GATEWAY_URL env override
- Add missing @_handle_exp decorator to delete_note and update_note views
- Fix Flask endpoint name collision using @functools.wraps on _handle_exp
- Remove unused base64 import
- Update dnfs.ini with gateway config (http://127.0.0.1:8443/whoami, timeout=5)
- Update docker-compose.yml with GATEWAY_URL and host.docker.internal for containers
2026-08-01 13:49:28 +08:00
qing e7a2877422 feat(dnfs): delegate Token verification to gateway /whoami
Replace the insecure local base64 decode (with FIXME comment about
missing signature verification) with proper remote RSA signature
verification through gateway's /whoami endpoint:

- Add [gateway] config section (url, timeout) to dnfs.ini with defaults.
- Support GATEWAY_URL environment variable override for Docker.
- decode_token() now POSTs to gateway /whoami with Authorization: Bearer
  <token>; gateway verifies RSA signature and expiry, returns login/user/
  is_admin. Network/gateway errors fail-closed with 503.
- Add @_handle_exp decorators to delete_note() and update_note() which
  were missing exception handling wrappers (consistent with other routes).
- docker-compose.yml: add GATEWAY_URL, CONFS_HOST, CONFS_PORT env vars
  plus host.docker.internal extra_hosts so dnfs container can reach
  host-side gateway and confs services.
2026-08-01 13:42:59 +08:00
qing 7d6ba6de93 feat(gateway): add retry_on_refused for pymsg MsgClient (v0.6.1)
- Add MsgConnectionRefusedException to distinguish ECONNREFUSED from other
  connection errors; openConnection() now throws it when connect() returns
  ECONNREFUSED.
- MsgClient::call() adds retry_on_refused/retry_interval parameters (defaults
  false/1.0s), matching Python pymsg.MsgClient.call(retry_on_refused=True).
- Retry loop: only retries on ECONNREFUSED; other exceptions (timeout,
  connection reset, business errors) propagate immediately. Throttled logging:
  first failure prints immediately, subsequent logs every 5 seconds.
- DbC::call_msg() enables retry_on_refused=true (db is a hard dependency;
  gateway blocks waiting for db on startup instead of crashing).
- LogC::_send() explicitly passes retry_on_refused=false (fail-safe; never
  block the background log thread when db is unavailable).
- captcha/smtp auxiliary services keep default retry_on_refused=false
  (fail-fast; blocking in request threads would exhaust the thread pool).
- Update README with pymsg protocol documentation, downstream retry strategy
  table, and v0.6.1 changelog entry.
2026-08-01 13:42:24 +08:00
qing b30ce1424b docs(dnfs): update README with security audit findings and architecture corrections
- Fix architecture diagram to reflect pymsg TCP RPC (not HTTP), add confs dependency
- Update security headers to match actual security_middleware.py values
- Add complete P0/P1/P2 security audit findings (16 issues total):
  - P0: Token without RSA signature verification (forgery risk)
  - P0: review_content() fail-closed broken due to exception handling gaps
  - P0: delete_note/update_note missing @_handle_exp decorator
  - P0: Parent nodes with children can be deleted (no DB foreign key + TOCTOU race)
  - P1: No token expiry check, no request body size limit, log_client config mismatch
  - P2: Error message info leakage, overly permissive CORS, legacy commented code
- Correct file paths for frontend (static/public/), dependencies (pymsg, not nuitka)
- Fix outdated code line references
2026-08-01 13:28:41 +08:00
qing 7ef6a0b65f Merge branch 'dev_qing' of https://git.cancerai.top/qing/micro_servers into dev_qing 2026-08-01 13:19:58 +08:00
qing 531358919c fix conf 2026-08-01 13:16:45 +08:00
qing bd1d7ca4d0 refactor(libhttp): split HttpServer into modular TUs for faster incremental builds
Split the 1400-line HttpServer.cpp into 6 focused translation units by
functional area:
- HttpServer.cpp: core lifecycle, SSL init, session management, debug
- HttpServer_Listener.cpp: TCP connection acceptor
- HttpServer_Routes.cpp: route registration, matching, wildcard/query parsing
- HttpServer_Request.cpp: request dispatch, static files, CORS, HTTP I/O
- HttpServer_Ws.cpp: WS route registry, client management, broadcast APIs
- HttpServer_WsSession.cpp: per-session WS frames + bidirectional WS proxy

Add inc/us/detail/HttpServerImpl.hpp for internal shared definitions
(exception classes, full Session/Listener declarations) installed under
/usr/include/us/detail/. Update Makefile with proper dependency tracking.
Update README with Source Layout section and v3.3.0 changelog entry.

Modifying one module now only requires recompiling its corresponding .o
instead of the entire monolithic translation unit.
2026-08-01 13:15:52 +08:00
qing d883dc620e refactor(pymsg): 新增AsyncRpcQueue通用异步队列,log_client基于此重构
- pymsg新增AsyncRpcQueue:提供内存队列+后台线程+攒批处理的通用异步基础设施,
  支持两种模式:简单RPC模式(client+method+serialize)和自定义handler模式
- confs/dnfs/static的log_client.py重构为基于AsyncRpcQueue的薄封装,
  保持API完全向后兼容(push/info/error/warning/debug/critical + 全局logs单例)
- 消除了三份log_client副本中重复的队列/线程/攒批逻辑(约150行/份),
  代码量净减少约50行
- LogClient的行为与旧版一致:逐条INSERT到db、fail-fast网络错误、队列满丢最旧
2026-08-01 12:59:33 +08:00
qing 101aed48be feat(pymsg): RpcClient 新增 retry_on_refused 阻塞等待模式,解决微服务启动顺序依赖
pymsg 客户端:
- MsgClient.call() 新增 retry_on_refused/retry_interval 参数
  - retry_on_refused=True 时,ConnectionRefusedError 不抛出,按 retry_interval(默认1s)阻塞重试
  - 重试期间每5秒向 stderr 打印等待提示,避免日志刷屏
  - 仅对 ConnectionRefusedError 重试;TimeoutError/ConnectionError/RpcError 仍立即抛出
  - 默认 retry_on_refused=False,完全向后兼容
- RpcClient 构造函数新增同名关键字参数,call() 支持单次调用覆盖
- 新增 time 模块导入
- 更新 README 文档(新增"启动等待模式"章节和参数表)

各服务(db/dnfs/static):
- _fetch_from_confs() 改用 retry_on_refused=True,移除手动 ConnectionRefusedError 捕获和 sys.exit
- 保留 RpcError(如配置文件不存在)的友好错误处理和 sys.exit(1)
- 启动时不再因 confs 未就绪而退出:pymsg 自动阻塞等待 confs 启动后自动连接成功
- 启动顺序从"刚性要求 confs 先启动"变为"不严格要求启动顺序"
- log_client 保持默认 retry_on_refused=False,后台线程不阻塞(fail-safe 丢弃日志)
- 更新各服务 README 和 confs README 的启动顺序描述

测试验证:
- 基本 call() 正常工作
- retry_on_refused=False 连接拒绝立即抛 ConnectionRefusedError
- retry_on_refused=True 阻塞等待(延迟1.5s启动服务后成功连接,重试日志正常输出)
2026-08-01 12:47:08 +08:00
qing 0ae107f953 refactor(static): 删除默认配置与本地回退逻辑,confs 改为强依赖
- 配置加载从三层(内置默认值→confs→本地config.ini回退)改为两层(confs 强依赖→环境变量覆盖)
- confs 连接失败直接 sys.exit(1),不再静默使用内置默认值或回退本地 config.ini
- file_config 初始值改为空字符串白名单模板,移除所有内置默认值
- 新增配置完整性校验:server.host/port、directory.root、cors.allowed_origins 必须非空
- ssl 段保留合理默认(enabled=false、cert/key 默认文件名),不强制配置
- 区分错误类型:连接拒绝/RpcError/通用异常分别打印明确错误信息到 stderr
- confs 连接地址改为引导配置(环境变量 CONFS_HOST/CONFS_PORT 优先,默认 127.0.0.1:8010)
- 移除 --config 命令行参数(本地 config.ini 回退已删除)
- 删除 static/config.ini 本地回退文件
- 更新 _run.sh 移除 --config 参数
- 更新 static/README.md 与 confs/README.md 文档
- confs 集成关系描述更新为"confs 最先启动"
2026-08-01 12:38:51 +08:00
qing de0e4168b2 refactor(static): 首页内联样式解耦至 css/index.css
- 新增页面级样式文件 css/index.css(与 view.css/aibox.css 惯例一致)
- index.html 移除内联 <style> 块与头像内联样式,改为外链
- 头像圆角 100px 优化为 border-radius: 50%
2026-08-01 12:37:19 +08:00
qing 1a5e9f7064 feat(static): 首页添加登录态检测与进入笔记入口
- index.html: lang 改为 zh-CN,新增"关于本站"介绍段落,按钮改为有 id 的登录/注册/进入/退出四按钮(默认隐藏进入/退出)
- 新增 index.js: POST /whoami 检测登录态(HttpOnly Cookie 自动携带),已登录则显示用户名+进入笔记/退出按钮,未登录显示登录/注册
- 退出按钮调用 POST /logout 后刷新页面
2026-08-01 12:34:53 +08:00
qing 84adec4722 refactor(dnfs): 删除默认配置逻辑,confs 改为强依赖
- 配置加载从三层(默认值→confs→环境变量)改为两层(confs 强依赖→环境变量覆盖)
- confs 连接失败直接 sys.exit(1),不再静默使用内置默认值
- _dnfs_config 初始值改为空字符串白名单模板,移除内置默认值
- 新增配置完整性校验:confs 返回后检查所有必需 section/key 非空
- 区分错误类型:连接拒绝/RpcError/通用异常分别打印明确错误信息到 stderr
- confs 连接地址改为引导配置(环境变量优先,默认 127.0.0.1:8010),移除 dnfs.ini 中 [confs] 段
- 启动顺序调整为 confs → db → dnfs → ai_review/...(dnfs 同时依赖 confs 和 db)
- 更新 dnfs/README.md 与 confs/README.md 文档
2026-08-01 12:33:51 +08:00
qing 2cbb93b3b7 refactor(db): 删除 confs 连接失败时使用默认配置的 fail-safe 逻辑
- confs 改为 db 启动的强依赖:连接失败直接 sys.exit(1),不再静默使用默认值
- 配置加载从三层(默认值→confs→环境变量)改为两层(confs→环境变量覆盖)
- _db_config 初始值改为空字符串白名单模板,移除内置默认值
- 新增配置完整性校验:confs 返回后检查所有必需 section/key 非空,缺失则报错退出
- 区分错误类型:连接拒绝/RpcError/通用异常分别打印明确的错误信息到 stderr
- confs 连接地址改为引导配置(环境变量优先,默认 127.0.0.1:8010),不再从 db.ini 读取
- 更新 db/README.md 与 confs/README.md,说明启动顺序调整为 confs → db → ...
2026-08-01 12:29:55 +08:00
qing 6ebd544619 feat(db): 迁移 db 配置到 confs 统一配置服务
- 新增 confs/configs/db.ini 集中托管 db 服务配置(server/database/logs/confs)
- dbv2.py 采用三层 fail-safe 加载模式:内置默认值 → confs(db.ini,优先)→ 环境变量覆盖(最高优先级,Docker 兼容)
- _merge_ini() 白名单合并,仅覆盖已知 section/key,ConfigParser(interpolation=None) 禁用插值
- db 先于 confs 启动时连接失败静默 fail-safe,启动时打印配置来源
- db_tools.py 运维工具保持独立,直接读 DATABASE_URL 环境变量,不依赖 confs
- 更新 confs/README.md 与 db/README.md 文档
2026-08-01 12:23:21 +08:00
qing c06eb19f64 refix 2026-08-01 11:47:14 +08:00
qing 18387586a4 Merge pull request 'Dev qing' (#15) from dev_qing into main
Reviewed-on: #15
2026-08-01 06:12:13 +08:00
qing 46b66f8bd8 fix(gateway,static): 配置 /ws/aibox WS 反代路由;aibox 桌面端对话左对齐 2026-08-01 06:08:20 +08:00
qing e25c132722 解耦libhttp/HttpClient 2026-08-01 06:04:36 +08:00
qing 373f6af352 ^-^ 2026-08-01 06:04:27 +08:00
qing 65580e7e50 refactor(static): aibox.html 拆分内联 CSS/JS 为独立文件 2026-08-01 05:27:02 +08:00
qing 227fbb17f4 build(libhttp): lower optimization to -O2 to avoid cc1plus OOM on low-memory machines 2026-08-01 05:26:52 +08:00
qing cebef9fe70 docs: 各模块安全审计报告 + 补充 .gitignore 忽略规则 2026-08-01 05:21:23 +08:00
qing 12ece9020c feat(libhttp,gateway): add admin-only WebSocket reverse proxy
libhttp v3.1:
- add ws_proxy(path, handshake) API with WsProxyHandshakeHandler callback
- implement full-duplex async bidirectional bridging (text+binary frames)
- support ws:// and wss:// backends with SNI, write queues for ordering
- proxy routes take priority over regular ws() routes
- 403 response on handshake rejection, graceful close propagation

gateway v0.6.0:
- add [WsProxy] config section (routes = /path=ws://backend;...)
- handshake callback verifies Token (Authorization/Cookie) is_admin=true
- non-admin / expired / missing token → 403, admin → transparent bridge
- fix LocalTime.hpp C++20 tzdb incompatibility on Apple clang (use strftime/get_time/mktime)
- update example.ini and README docs for both modules
2026-08-01 05:20:14 +08:00
qing 6ed88feaa4 feat(static): 新增 aibox 智能体 Web 客户端(移动端适配)
- aibox.html:自包含移动端聊天页,经 gateway [WsProxy] 反向代理与 aibox 通信
- 协议兼容 BotOps client.py:chunk 流式渲染 / reasoning 思考区 / task 任务面板 / 指数退避重连
- 移动端适配:100dvh 动态视口 + safe-area 刘海屏 + 44px 触控目标 + 任务面板抽屉式
- 安全:握手由 gateway 强制 is_admin 验签,Token 全程 HttpOnly Cookie,CSP 合规(本地 markdown-it)
- README 同步页面清单与功能说明
2026-08-01 05:18:30 +08:00
qing bf2b5d31f7 fix(botops): v1.2 - 彻底修复思考区域与正式回答顺序问题
问题根因:之前预创建空 AI widget,思考 widget 挂载在其后,即使动态重排也可能因 Textual 组件树更新时序导致顺序不稳定。

修复方案:
- 移除 prefab 空 AI widget 预创建逻辑
- start_ai() 仅标记状态,不创建任何 widget
- _ensure_ai_widget() 延迟到第一次 chunk 到来时才创建 AI widget
- 按消息到达顺序自然挂载:reasoning_start → reasoning → chunk → end_reasoning/finish_ai
- 思考 widget 必然先于 AI widget 挂载,顺序天然正确

同时:
- 简化 app.py 发送失败处理,不再需要移除空 widget
- 更新 README v1.2 说明
2026-08-01 05:00:56 +08:00
qing 9815ff44af feat(libhttp): integrate WebSocket client into HttpClient
Merge WebSocket client functionality into HttpClient using Boost.Beast,
enabling a single client instance to handle both HTTP (libcurl) and
WebSocket (ws:// and wss://) protocols. Backward-compatible type aliases
(WebSocketClient, WebSocketClientBase) preserve existing code.

Changes:
- Add synchronous WebSocket client: ws_connect/ws_send/ws_recv/ws_close
- Support ws:// (plain TCP) and wss:// (TLS with SNI) connections
- Layered stream architecture with beast::tcp_stream for timeout support
- Binary message support via ws_send_binary()
- Update Makefile to link libcurl (-lcurl)
- Add README.md documenting unified HttpServer + HttpClient API
2026-08-01 04:59:47 +08:00
qing 5edb67b46d docs(botops): update README with reasoning mode ordering fix and v1.1 changelog
- Update reasoning display limit from 2000 to 3000 chars in protocol docs
- Add widget ordering guarantee note (reasoning always before answer)
- Document info message filtering for conversation start notifications
- Add v1.1 changelog section listing reasoning mode features and fixes
2026-08-01 04:56:25 +08:00
qing 039d6acf37 fix(botops): fix reasoning panel appearing after AI response and filter conversation start notification
- Add _ai_prefab flag to track pre-created empty AI widget
- start_reasoning() now reorders widgets: remove empty AI widget, mount reasoning first, then remount AI widget
- This ensures reasoning always appears BEFORE the actual answer
- Filter out 'Conversation started'/'对话开始' info notifications (implicit event, no need to show)
- Increase reasoning display limit from 2000 to 3000 chars
2026-08-01 04:55:12 +08:00
qing ba9a77fe66 feat(libhttp): integrate WebSocket support into HttpServer
Merge WebSocketServer functionality into HttpServer so a single port can
serve both HTTP(S) and WebSocket(S) endpoints while preserving full backward
compatibility for existing HTTP interfaces.

Key changes:
- Add WebSocket route registration: ws(), onWsConnect(), onWsDisconnect()
- Add client management: sendToWsClient(), broadcastWs(), disconnectWsClient()
- Rewrite Session class using beast::tcp_stream for unified HTTP/WS handling
- Support automatic ws:// and wss:// upgrades on HTTP/HTTPS ports
- Thread-safe message queue with asio::post for cross-thread sends
- Update Makefile to link OpenSSL and pthread for TLS/WebSocket support
- Add test_server.cpp demonstrating HTTP + WebSocket mixed usage
2026-08-01 04:43:33 +08:00
qing e007990594 feat(dnfs,static): 新增管理员特殊节点(链接/网页代码/图片/文件)
- 信封格式:content 为 {"_sp":...} JSON,普通 Markdown 节点不受影响
- dnfs:add_note/update_note 按 is_admin 强制特殊节点发布权(非管理员 403),
  字段与大小校验(url≤2048、code≤512KB、base64≤3.5M 字符),AI 审核剔除 base64 负载
- dnfs:修复存量 bug —— 模块级 _sys.path.insert 未定义导致 import 即 NameError
- static:新增 GET /api/favicon 服务端代理(SSRF 防护:内网地址拦截 + 重定向校验 + 512KB 上限)
- 前端:管理员可见节点类型选择器;链接 favicon 卡片 / 网页可审计可运行 / 图片直显 / 文件下载
- theme.css 补充 select 与特殊节点样式;两个 README 同步更新
2026-08-01 04:34:23 +08:00
qing b79044d631 feat(aibox): add DeepSeek reasoning mode support with tool calling on deepseek-v4-flash
- Add api.reasoning_enabled config (default false) to enable thinking mode
- Uses deepseek-v4-flash native thinking capability (thinking param), supports tool calls while reasoning
- Real-time reasoning content streaming via reasoning_start/reasoning/reasoning_end WS messages
- Frontend shows reasoning in gray italic area, auto-folds long reasoning (>2000 chars)
- Dynamic reasoning detection: shows reasoning even if model returns it without explicit config
- Increase default API timeout to 120s for longer reasoning responses
- Remove duplicate _add_assistant_message_with_reasoning method
- Update docs: config example, message types table, v2.5 changelog
2026-08-01 04:33:55 +08:00
qing 81ae84851d feat(waf): add WebSocket proxy support and fix HTTP/2 proto version
- Implement http.Hijacker on responseRecorder to enable WebSocket upgrade
  (delegates Hijack() to underlying ResponseWriter, marks hijacked state)
- After WebSocket upgrade (101 Switching Protocols), skip HTTP response-phase
  WAF checks since connection becomes a binary frame stream
- Log WebSocket upgrades at Info level for observability
- Fix P2-3: use r.Proto instead of hardcoded "HTTP/1.1" in ProcessResponseHeaders
  for correct HTTP/2 protocol version reporting
- Add Write() method to responseRecorder to ensure WriteHeader is called
  before first Write (complies with http.ResponseWriter contract)
- Add bufio/net imports required by Hijacker interface
- Update README with WebSocket support section and v1.2 changelog
- Update SECURITY_AUDIT to mark P2-3 and P2-4 as fixed
2026-08-01 04:18:30 +08:00
qing 4e190953c0 feat(confs): integrate dnfs service with centralized config management
- Add dnfs.ini to confs/configs/ with server, database, ai_review, confs sections
- Refactor dnfs.py to use three-layer fail-safe config loading (defaults → confs → env vars)
- Unify timeouts for DB RPC and AI review calls via configuration
- Add _merge_ini() whitelist merging to prevent unexpected config injection
- Environment variables have highest priority for Docker networking (db/ai_review hostnames)
- Document config loading mechanism in both confs/README.md and dnfs/README.md

Follows the same pattern as static service integration; confs failure falls back
gracefully to built-in defaults + environment variables.
2026-08-01 04:13:39 +08:00
qing b4436b192a fix(waf): harden against OOM and Slowloris DoS attacks
- P0: add 10MB hard request body size limit with io.LimitReader,
  return 413 Request Entity Too Large when exceeded, prevents OOM
- P0: configure http.Server timeouts: ReadHeaderTimeout=10s (Slowloris),
  ReadTimeout=15s, WriteTimeout=30s, IdleTimeout=120s, MaxHeaderBytes=1MB
- P1: properly handle body read errors, log and return on client disconnect
- P2: drain and close r.Body in defer to ensure TCP connection reuse
- Log errors from ReadRequestBodyFrom and ProcessRequestBody
- Inject security headers on 413 responses
- Add security audit report (docs/SECURITY_AUDIT_20260801.md)
- Update README with timeout/body-limit docs and changelog
2026-08-01 04:13:39 +08:00
qing bd544a4181 docs(aibox): update file line count to ~8450 2026-08-01 03:51:17 +08:00
qing 164bc65b95 feat(aibox): add dedicated workspace directory to prevent file pollution
- Add WORKSPACE_DIR constant pointing to ~/.aibox/workspace by default
- Update file operations prompt to require writing temp files to workspace
- Update command execution prompt to use workspace as default work_dir
- Add command output size limit note in command execution prompt
- Update README with workspace directory structure and specification
- Add system.workspace_dir config item documentation
- Add v2.4 changelog entry
2026-08-01 03:49:25 +08:00
qing 53ce69c8dd docs(confs): update README for P1 fixes and clarify RpcClient usage
- Update title/description: pymsg RpcServer/TCP instead of plain pymsg/TCP
- Add new feature entries: cache copy return, list symlink guard, strict
  param validation, ConfigParser interpolation=None, Slowloris protection
- Fix Python call examples: RpcClient.call() returns data directly, not
  wrapped in {'success':..., 'data':...}; add RpcError try/except example
- Clarify reload semantics: empty string/non-string name returns error;
  only omitted name or explicit null triggers full cache clear
- Add parameter validation rules for get (name/key type checks) and reload
- Document INI % literal behavior (interpolation disabled)
- Add '返回值隔离' to cache mechanism section
- Expand security mechanism table: cache anti-tamper, list symlink guard,
  strict param validation, INI safe parsing
- Add docs/ directory and audit report reference to project structure
- Add notes about correct reload usage, INI % handling, and return copy
- Add audit report cross-reference in 注意事项
2026-08-01 03:46:39 +08:00
qing 2f3e63e499 fix(aibox): prevent OOM from large tool outputs with multi-layer safeguards
- ReadFileTool: replace readlines() with on-demand range reading; add reverse block reading for large file tail; limit single line to 10KB; hard-cap max_lines to 500
- SubprocessCommandExecutor: replace capture_output with line-by-line Popen reading; enforce 1MB output limit with immediate process termination; merge stdout/stderr counting for both sync and streaming modes
- Raise TOOL_MAX_RESULT_LENGTH default from 10KB to 1MB as final safety net
- Add SECURITY_AUDIT.md documenting vulnerabilities and remediation roadmap
- Update README with memory safety features and v2.3 changelog
2026-08-01 03:46:02 +08:00
qing 90761a723d docs(confs): fill in commit hashes for fixed issues in audit report 2026-08-01 03:43:01 +08:00
qing b53d4999ce fix(confs): harden parameter validation and prevent cache/info leaks
P1 fixes:
- P1-1 _handle_reload: empty string/0/false/falsy name no longer clears all
  caches; only explicit null/omitted name triggers full cache clear; non-string
  name raises ConfigError instead of being silently str()-converted
- P1-2 ConfigStore.get(): return dict(entry) shallow copy instead of direct
  cache reference to prevent callers from mutating the internal cache
- P1-3 list(): resolve symlinks via os.path.realpath() and skip entries whose
  real path escapes configs/ root; prevents leaking external file metadata
  (size, mtime) through directory-traversing symlinks
- P1-4 _handle_get key: validate key is a string (isinstance check); reject
  non-string/empty key with clear ConfigError instead of misleading str()
  conversion leading to "key not found" errors

P2 fixes:
- P2-1 ConfigParser: use interpolation=None to disable %(key)s interpolation,
  preventing InterpolationError on config values containing bare % characters
- P2-2 argparse: update description from "pymsg/TCP" to "pymsg RpcServer/TCP"
  to match module docstring

Also update SECURITY_AUDIT_20260801.md to mark issues 9-14 as fixed and
refresh the security mechanisms summary table; all P0/P1 issues are now resolved.
2026-08-01 03:42:46 +08:00
qing e4e9968d14 docs(confs): add security audit report 20260801
- Audit covers confs.py, log_client.py, startup scripts, and pymsg/rpc.py dependency
- Documents 8 issues fixed during this audit cycle (P0-P2) including Slowloris DoS,
  OOM, thread death, TOCTOU race, and log_client hardening
- Lists 4 remaining P1 issues (reload empty string clears all cache, mutable cache
  reference leak, list() symlink metadata leak, key type validation), 4 P2 issues,
  and 6 P3 issues with root cause, risk analysis, and fix suggestions
- Security mechanisms summary table for quick reference
- Follows project convention: gateway/docs/SECURITY_AUDIT_YYYYMMDD.md
2026-08-01 03:37:22 +08:00
qing 9efb96ea66 fix(confs): eliminate TOCTOU race in ConfigStore path resolution
- Split _resolve() into _validate_name() (pure string checks, lock-free
  fast-fail) and _resolve_locked() (filesystem checks, must be called
  under lock)
- Perform all filesystem operations (realpath → isfile → stat → open)
  inside self._lock in get(), eliminating the window between path
  validation and file open where a symlink swap could escape the
  configs directory
- Apply same fix to reload(): validate name outside lock, resolve path
  inside lock to prevent TOCTOU during targeted reload
- Hold lock during list() directory traversal so the `cached` flag
  (which reads self._cache) is consistent with directory state; wrap
  os.stat() in try/except to tolerate files disappearing mid-traversal
- Update docstring and README:
  - Protocol description corrected to pymsg RpcServer/TCP
  - Python client examples updated from legacy MsgClient to RpcClient
  - Security section now documents TOCTOU mitigation and Slowloris
    protection (added by earlier pymsg fix)
2026-08-01 03:32:19 +08:00
qing 6c587a22d8 docs(db): update README for log_client SDK template and hardening features
- Document that example_log_client.py now uses RpcClient (high-level RPC)
  instead of legacy MsgClient (low-level TCP frames)
- Add comprehensive client feature list reflecting all hardening fixes:
  - Thread exception protection (outer try/except prevents thread death)
  - Accurate enqueue-time timestamps
  - Queue maxsize with drop-oldest policy
  - Safe JSON serialization fallback
  - Event-based flush synchronization
  - Non-blocking push() guarantee
- Clarify length truncation: server-side _log_internal() truncates
  internal logs to 8KB/4KB, external clients should control length
- Add db_tools.py to file table (was previously undocumented)
- Update service description to mention RpcServer
2026-08-01 03:28:40 +08:00
qing bdbea6ac40 fix(log_client): update example_log_client template to use RpcClient
- Replace legacy MsgClient (low-level TCP frames) with RpcClient (high-level
  RPC) to match dbv2's RpcServer interface; the old MsgClient-based version
  was incompatible with the current RPC routing protocol
- Apply all hardening fixes from the service copies:
  - Outer try/except in _flush_loop to prevent any exception from killing
    the background thread
  - _safe_json() helper for unserializable extra fields (fallback to str/repr)
  - Timestamp generated at push() (enqueue time) instead of _send() time
  - Event-based flush() synchronization instead of unreliable sleep(0.6)
  - Queue maxsize=10000 with drop-oldest policy to prevent unbounded memory
  - put_nowait with Full handling ensures push() never blocks business threads
  - RpcClient construction wrapped in try/except
- Add explicit service_name parameter example for all 4 services in comments
- This is the template file referenced by db/README.md and copied by new
  services; keeping it in sync prevents propagating old bugs
2026-08-01 03:26:19 +08:00
qing 3be9e78335 docs(aibox): update README for v2.2 prompt system fixes
- Expand prompts directory listing to show all 10 skill files explicitly
- Update prompt system table: '1_*.md' → 'N_*.md (N=1~10)' with numeric ordering
- Document load order guarantee (identity → skills 1-10 → soul → status)
- Document is_continuation behavior (no repeated pre_prompt on recursive calls)
- Add v2.2 changelog: prompt ordering fix, token waste reduction,
  recursion pre_prompt dedup, large-file threshold, memory search tuning
2026-08-01 03:24:39 +08:00
qing 1b9c727e80 fix(log_client): harden background flush thread against exceptions
- Wrap _flush_loop main body in outer try/except to prevent any
  unexpected exception (network errors, serialization errors, etc.)
  from killing the background thread (which would cause unbounded
  queue growth and eventual OOM)
- Wrap _send() call in inner try/except as final safety net
- Wrap RpcClient() construction in try/except (connection failures
  during client creation were previously unhandled)
- Add _safe_json() helper: unserializable extra fields fall back to
  repr()/str() instead of raising TypeError that could kill thread
- Generate timestamp at push() (enqueue time) instead of _send() time,
  so timestamps reflect actual event time even when queue is backed up
- Replace unreliable time.sleep(0.6) in flush() with _flush_done Event
  for proper synchronization between caller and background thread
- Add _QUEUE_MAXSIZE=10000 with drop-oldest policy to prevent unbounded
  memory growth even in pathological cases
- Use put_nowait with Full handling to ensure push() never blocks
  business threads
- Sync fix to all 4 copies: confs/static/smtp/dnfs
2026-08-01 03:23:19 +08:00
qing e8fa26d79f fix(aibox): fix prompt ordering, reduce per-turn token waste, and improve defaults
Prompt system bugs and improvements:

1. **SKILL_PREFIX bug fixed**: SKILL_PREFIX = "1_" only matched files starting
   with "1_", causing 2_ through 10_ skill prompt files to be misclassified
   as "other" files and sorted in wrong order (e.g., 10_task_handling.md
   sorted before 2_memory_save.md due to lexicographic ordering). Replaced
   with regex _SKILL_FILE_RE = re.compile(r'^(\d+)_') and _skill_sort_key()
   that extracts the numeric prefix for proper numeric sorting.

2. **_init_system_prompts used unsorted list**: get_prompts_list() returned
   prompts in os.listdir() lexicographic order, which put 100_STATUS.md
   before 1_memory_search.md and 10_task_handling.md before 1_memory_search.md.
   Now uses get_sorted_prompts_list() for correct ordering:
   identity → skill prompts (1-10 by number) → soul → status.

3. **Verbose pre_prompt injected every turn (~30 lines)**: The pre_prompt
   contained detailed workflow instructions (search_skills first, memory
   search, ask_user, etc.) that duplicated what system skill prompts
   already say, wasting ~300+ tokens per user message. Worse, the same
   verbose prompt was injected on recursive think_and_respond calls after
   tool execution, confusing the model with repeated "第一步搜索技能"
   instructions when it was already mid-conversation with tool results.
   - Reduced pre_prompt to 5 concise bullet points as a reminder
   - Added is_continuation parameter: recursive calls after tool execution
     skip pre_prompt entirely and just pass "请基于以上工具执行结果继续回答。"
   - Removed post_prompt (save_memory/update_soul instructions already in
     2_memory_save.md and 8_identity_management.md)

4. **Unreasonable large-file thresholds**: 3_file_operations.md default
   said >50KB or >200 lines is "too large" — too conservative for code
   files. Raised to 500KB/2000 lines; added tail=true and binary file
   guidance.

5. **Overly rigid memory search instruction**: 1_memory_search.md said
   "每次都要做!" (must do every time), causing unnecessary memory searches
   for simple greetings/acknowledgments. Softened to "根据话题相关性判断",
   with explicit examples of when to skip (greetings, confirmations).

New/changed methods:
- PromptManager._skill_sort_key(cls, filename) - extract numeric prefix
- PromptManager._prompt_sort_key(self, p) - tuple-based sort key
- PromptManager.get_sorted_prompts_list() - returns correctly ordered list
- PromptManager.get_skill_prompts() - uses regex instead of startswith("1_")
- DeepSeekChat.think_and_respond() - added is_continuation parameter
2026-08-01 03:19:58 +08:00
qing da7f11d352 fix(confs): add MAX_CONFIG_SIZE (1 MiB) to prevent OOM on oversized configs
- Add MAX_CONFIG_SIZE constant (1 MiB) to limit single config file size
- Check file size via os.stat() before reading into memory
- Oversized files raise ConfigError with descriptive message instead of
  being fully read (which could cause OOM with multi-GB files)
- Use single os.stat() call to get both mtime and size atomically
- Update README: document size limit in security mechanisms and notes
- Existing small configs (ini/json/yaml, typically <10KB) are unaffected
2026-08-01 03:15:06 +08:00
qing 9708fbc87e fix(pymsg): add recv_timeout to MsgServer to prevent Slowloris DoS
- Add configurable recv_timeout (default 10s) to MsgServer
- _handle_client now uses recv(s, timeout=...) instead of infinite blocking recv(s)
- Catch TimeoutError and close the slow client connection
- This prevents a single stalled connection from blocking the entire main select loop
- Update README with recv_timeout parameter and Slowloris mitigation docs
- Backward compatible: keyword-only arg with sensible default
2026-08-01 03:12:49 +08:00
qing ae42ab8a91 docs(aibox): update README for v2.1 robustness fixes and correct entry filename
- Fix all references from main.py to aibox.py (actual entry file)
- Update installation to use pip install -r requirements.txt
- Expand WebSocket message types table: add line/task_error/task_added/
  notification/connection_ack/interrupt_ack, correct chunk/complete/error semantics
- Fix message flow example: tool calls are reported via 'line' messages,
  not a non-existent 'tool_call' type
- Document _sanitize_tool_result() and error recovery (_rollback_messages)
  in development guide
- Add FAQ entry for tool-call-induced API failures
- Add v2.1 changelog covering all robustness fixes (reasoning_content
  filtering, tool_call id fallback, control character sanitization,
  message rollback on API failure)
2026-08-01 03:09:34 +08:00
qing a80bd582da fix(aibox): prevent tool results from corrupting conversation and breaking API requests
Root causes of API request failures after tool calls:
1. reasoning_content (an API response field) was echoed back in assistant
   messages, which could cause API format errors
2. tool_call id could be empty string when model didn't return one in a
   streaming chunk, producing invalid tool_calls that caused 400 errors
3. Tool execute() could return None or non-string types; direct string
   operations (len(), + "\n", [:200]) raised TypeError and produced
   non-string content in tool messages, breaking JSON serialization
4. Tool output (e.g. from run_command) could contain control characters
   (NULL/BEL/BS) that corrupted JSON payloads
5. When API request failed (network/4xx/5xx/parse error), messages added
   in that turn (user msg, assistant msg with tool_calls, tool responses)
   remained in self.messages, permanently poisoning conversation history
   so all subsequent requests would also fail

Fixes:
- Stop sending reasoning_content back to API in message construction;
  normalize assistant message content: null when tool_calls present, ""
  when neither content nor tool_calls exist, str otherwise
- Sanitize tool messages: ensure tool_call_id is non-empty (fallback id)
  and content is always a string
- Generate fallback tool_call id when model omits it; handle streaming id
  delivery where id arrives in a later chunk
- Add _sanitize_tool_result(): coerce None/non-string to str, strip
  control characters (preserve \n \t \r) to prevent JSON/API corruption
- Wrap execute_tool call in try/except in _handle_tool_calls for double
  safety; ensure history logging never crashes
- Truncate oversized results with an explicit note instead of replacing
  with a generic error that lost all useful output
- Add _rollback_messages(to_length) and record msgs_before_turn at start
  of think_and_respond; on any exception (RequestException, JSONDecodeError,
  or unexpected Exception), rollback self.messages to the pre-turn state
  so the conversation is not left in a broken state
- In _handle_tool_calls, wrap execute_tool in try/except and always
  produce a safe string result
2026-08-01 03:06:34 +08:00
qing 1b9e6f6b3a fix(botops): resolve duplicate AI response rendering in ChatView
Root cause: aibox.py think_and_respond recursively calls itself after tool
execution, and each recursive call's complete callback carries only the
final-round full_response. The old finish_ai logic treated complete as a
full-text alignment signal — when full_text != self._ai_text (which always
happened after recursive tool calls because _ai_text accumulated all chunks
while complete only held the last round), it triggered remove+remount,
causing visible duplicate/flash of AI responses. A secondary issue was
dialogue_ended arriving before complete, which made a second finish_ai call
remount the message a second time.

Changes:
- Remove the full-text replacement branch in finish_ai; chunk streaming is
  now the authoritative content source, complete is a finalization signal only
- Add _ai_finalized flag: start_ai resets it, finish_ai sets it; repeated
  finish_ai calls (dialogue_ended + complete) return early to prevent remount
- append_ai drops late chunks after finalization to avoid dirty appends
- Empty pre-built widget without chunks still gets filled via w.update(full_text)
- Full lost-frame fallback (no widget + full_text) is preserved
- Update BotOps/README.md protocol section to reflect new complete semantics
2026-08-01 02:56:29 +08:00
qing f003b4dfaa style(static): 注册页图形验证码统一为 code-box 布局
- register.html 图形验证码改用 .code-box 行内布局,与邮箱验证码对齐
- theme.css 新增可复用 .captcha-img 类,替代内联样式
- README 功能列表补充表单组件说明
2026-08-01 02:51:55 +08:00
qing eb506e5cbf docs(confs): 更新 README 反映 static 已接入现状
新增"已接入的服务"表格,记录 static 的三层 fail-safe 接入方式
及实现细节;注意事项改为指向集成章节,避免重复描述。
2026-08-01 02:46:58 +08:00
qing ec1eb1e805 docs(gateway): 补充前端接入图形验证码说明
- README.md v0.5.4 变更记录补充前端改动(register.html/register.js 接入图形验证码)
- SECURITY_AUDIT_20260801.md P0-1 修复说明补充第 5 项前端同步,改动文件清单补全
2026-08-01 02:43:06 +08:00
qing 6ee1eca789 feat(static): 注册页获取验证码接入图形验证码
配合 gateway v0.5.4 邮件轰炸防护(/register/send_code 强制 CAPTCHA):

- register.html: 邮箱与邮箱验证码之间新增图形验证码区块(图片+输入框,复用 login 页结构)
- register.js:
  - 新增 refreshCaptcha() 复用 /captcha/image 代理路由(与 login.js 一致)
  - 新增 checkCaptcha() 校验图形验证码非空且 Token 已加载
  - 获取验证码按钮点击时先校验图形验证码,再携带 captcha_token + captcha_answer 发送
  - 无论发送成败都刷新图形验证码(Token 一次性消费)
  - 重置表单时清空图形验证码并刷新

方案 A:仅在「获取验证码」时要求 CAPTCHA,注册提交不加(邮箱验证码已是二次验证)
2026-08-01 02:42:15 +08:00
qing 0ef96c28f6 docs(confs): 新增服务 README
含功能特性、协议路由、缓存/安全机制、集成关系与项目结构
2026-08-01 02:38:20 +08:00
qing 49f0698b9d fix(gateway,smtp): 修复邮件轰炸漏洞(审计 P0-1)
gateway v0.5.3 → v0.5.4:

1. gateway/src/main.cpp
   - /register/send_code 强制 CAPTCHA 校验(经 captcha:8009,Token 一次性消费,fail-closed)
   - 新增邮箱级双窗口限流(3 次/小时 + 10 次/天,CAPTCHA 通过后才计入配额)
   - 新增 _EMAIL_ADDR_RATE_HOUR / _EMAIL_ADDR_RATE_DAY 限流器实例

2. smtp/smtp_v2.py
   - 新增邮箱级长期限流兜底(EMAIL_HOURLY_LIMIT / EMAIL_DAILY_LIMIT,默认 3/10)
   - 新增全局熔断(SMTP_HOURLY_LIMIT,默认 100 封/小时,防 smtp 账号被封)
   - 预占/回滚与 60s 冷却同模式,发送失败不计入配额

3. gateway/docs/SECURITY_AUDIT_20260801.md
   - 新增带日期的安全审计报告
   - P0-1 标记为已修复 v0.5.4

4. gateway/README.md
   - 更新 /register/send_code 接口文档(CAPTCHA 必填 + 5 层防护)
   - 更新版本变更日志

防护层次:IP 限流 → CAPTCHA → 邮箱级限流(gateway)→ 60s 冷却 → 邮箱级限流(smtp)→ 全局熔断
2026-08-01 02:37:15 +08:00
qing 2774c3471f docs(readme): 补充 confs 统一配置服务说明
- 架构总览/服务表格/端口表新增 confs(8010),日志链路加入 confs,新增配置链路
- 新增 confs 小节:路由说明、mtime 自动重载、消费方接入约定
- static 配置说明更新为 confs 分发 + 本地回退
- 修正安装序列(补 captcha/confs)、init.txt 启动顺序与实际一致
2026-08-01 02:34:51 +08:00
qing 287cab189d feat(static): 配置接入 confs 统一配置服务
- fsys.py 启动时优先从 confs(127.0.0.1:8010)拉取 static.ini,
  支持 CONFS_HOST/CONFS_PORT 环境变量覆盖
- confs 不可用时 fail-safe 回退本地 config.ini
- static.ini 纳入 confs/configs/ 统一分发
2026-08-01 02:30:14 +08:00
qing 7d7e3dc10a fix(confs): 删除错放的 .env 文件
confs.py 不读取 .env(无 dotenv 依赖),该文件实为 SMTP 凭据,
被误放入 confs 目录且会导致 python-dotenv 解析报错。
2026-08-01 02:20:46 +08:00
qing 55fbe2f1f1 feat(confs): 统一配置服务(pymsg/TCP :8010)
- confs.py 重写为统一配置服务:get/list/reload/health 路由,
  内存缓存 + mtime 自动重载,路径遍历防护,ini/json 按键提取
- 复制 log_client.py,新增 configs/example.ini 示例配置
- 注册到 core/init.txt(db 之后启动)与 _inst.sh 安装序列
2026-08-01 02:15:04 +08:00
qing ae92a6edde perf(pymsg): send/recv 零拷贝优化
send: sendmsg 散射-聚集 I/O 替代 header+payload 拼接,内核直接
从两块内存读取,省掉大消息的用户态拷贝。

recv: recv_into 写入预分配 bytearray 替代 bytearray 逐步 extend,
省掉 realloc 和 bytes() 转换。
2026-08-01 02:08:03 +08:00
qing 267d572906 fix(pymsg): 修复消息长度无上限导致 OOM DoS 漏洞
Python/Go 端 recv 缺失消息大小上限检查,恶意客户端发送 4 字节
超大长度头即可触发 OOM。新增 64 MiB 上限(与 C++ 端 Msg.hpp
对齐),在分配内存前拒绝并断开连接。新增 5 个单元测试覆盖。
2026-08-01 01:55:44 +08:00
qing e248290860 统一配置模块 2026-08-01 01:54:25 +08:00
qing 48b45cb44a 修复网关bug 2026-07-31 22:19:56 +08:00
qing e7f435dc4e refix 2026-07-31 21:07:41 +08:00
qing ada2af47af 增加handle_exp包装器 2026-07-31 19:47:27 +08:00
qing ea670adde2 feat(static): dnfs 内容输入框随内容自动调整高度
- textarea 输入时自动增长,最小 120px,最大 400px
- 超过最大高度后出现滚动条,避免页面无限撑开
- 编辑已有节点时自动适配内容高度
- 提交/重置/退出编辑后恢复初始高度
- 禁用手动拖拽 resize,由自动增长统一控制
2026-07-30 22:21:24 +08:00
qing 5839172dae 迁移/whoami视图 2026-07-30 22:05:12 +08:00
qing 3f97dd92ef update 2026-07-30 21:57:04 +08:00
qing f935448389 refix 2026-07-30 21:33:03 +08:00
qing 32d3a5a19f 重构db 2026-07-30 20:15:02 +08:00
qing edb53d079c Merge pull request 'Dev qing' (#14) from dev_qing into main
Reviewed-on: #14
2026-07-30 19:47:21 +08:00
qing 74610c32c8 微调 2026-07-30 19:03:34 +08:00
qing eec869a28d style(dnfs): 增大节点表单输入框尺寸 2026-07-30 15:56:06 +08:00
qing d309f41e47 refix 2026-07-30 15:52:18 +08:00
qing 7589b77916 docs: 更新 README.md - 补充 bot4cj、libtry、tests 等缺失模块说明 2026-07-30 12:44:29 +08:00
qing e4831522a4 docs: 新增 libhttp/libtry/pymsg/README.robot.md 技术手册,更新 BotOps/README.md 2026-07-30 12:38:31 +08:00
qing 8a14247aed docs(BotOps): 安装指引改用脚本 + 补全项目结构树 + 新增 Nuitka 编译说明
- 安装章节改用 bash _set.sh / _run.sh 一键脚本
- 新增 _inst.sh Nuitka 编译为 botops.bin 单文件的说明
- 项目结构树补全 _set.sh / _run.sh / _inst.sh / requirements.txt / __init__.py
2026-07-30 12:38:14 +08:00
qing 803d176066 docs(db): 修正 log_client 描述并补充 message/extra 截断说明
- "纯标准库实现" → "无第三方依赖(仅依赖 pymsg 项目内置库)",更准确
- 新增长度保护说明:message 上限 8KB、extra 上限 4KB,超长自动截断

fix(db): _set.sh Nuitka 注释 main.py → dbv2.py
2026-07-30 12:34:37 +08:00
qing ac8f3da9df docs(libcrypt): 完整 README,覆盖 5 个类 API、构建、示例与注意事项
- 5 个类的完整 API 文档(Crypto_Basic/AES_CBC/Base64/SHA256/MyRSA)
- 架构层级图与 header-only 设计说明
- 构建/安装/链接指引(含 OpenSSL 3.x 依赖说明)
- 可运行的使用示例
- Private_Key 私有继承、链接 -lssl -lcrypto 等注意事项

fix(libcrypt): SHA256.hpp 删除重复 bytes_to_hex,改用基类版本

- 私有 static bytes_to_hex 与 Crypto_Basic::bytes_to_hex 重复,已删除
- include 路径统一为 "Crypto_Basic.hpp"(与其他三文件一致)
- 移除随之无用的 <iomanip> <sstream> 头文件

fix(libcrypt): AES_CBC.hpp 补充缺失的 #include <iomanip>
2026-07-30 12:26:15 +08:00
qing 6649b31e17 feat(static): add frontend token auto-refresh for dnfs page
- New token_manager.js: silent refresh timer (80% lifetime) + global
  fetch 401 interceptor with one-shot retry.
- dnfs.py /whoami now returns iat, created_at, keep_time for expiry
  estimation.
- Dnfs.html loads token_manager.js before dnfs.js (zero change to dnfs.js).
- Graceful fallback: stale tokens without new fields, refresh failures,
  and /refresh_token 401 itself all degrade to the existing 401→login flow.
2026-07-30 12:25:11 +08:00
qing 254fbf4628 docs(aibox): 更新 README.md 并新增 README.robot.md 技术手册
README.md 修复:
- 新增 ask_user 工具到工具列表
- 补充配置表中缺失的 17 个字段(system/scheduler/api/context/memo/files/commands/web/memory/conversation/tools/compressor/proxy)
- 补充对话压缩完整配置项

README.robot.md 新增:
- 17 章节完整技术手册(~500 行)
- 架构图、数据流、协议详情
- 19 个工具的完整实现信息
- 配置参考(含所有字段说明)
- 已知限制与安全建议
- 部署与故障排查指南
2026-07-30 12:08:17 +08:00
qing 931e134796 docs(smtp): 新建完整 README,覆盖协议、路由、配置与安全机制
- 3 个路由的完整请求/响应文档(send_email / verify_email / health)
- .env 配置项说明(4 种格式支持、必填/可选字段)
- 验证码生命周期:生成→冷却→消费→过期→作废→失败回滚
- 安全机制:发送冷却、失败次数上限防枚举、SMTP 失败回滚
- 集成关系:gateway 调用流程、core 守护、log_client 日志推送
- 旧版 smtp.py 的安全风险标注

fix(smtp): _set.sh 中 nuitka 编译目标 smtp.py → smtp_v2.py
2026-07-30 12:02:25 +08:00
qing 63a80694ae docs(core): 新增代码审查日志 CODE_REVIEW_LOG_20260730.md
- 审查日期:2026-07-30 14:30:00 (Asia/Shanghai)
- 发现 16 个问题(4 Critical / 6 Medium / 6 Low)
- 核心风险:命令注入、无鉴权、空指针解引用
- 包含分阶段修复计划与优先级建议
2026-07-30 12:01:04 +08:00
qing 4506601773 docs(libth): 完整的 README 文档,覆盖 API、架构、构建与使用示例
- 新增 4 个类的完整 API 说明(Thread/Fsm/ITask/Cv_wait)
- "回字形"执行循环和两阶段构造的设计说明
- 构建、安装、链接、卸载的完整指引
- 可运行的使用示例代码
- 设计要点与注意事项(死锁风险、单次使用限制等)
- 修正 C++ 标准版本为 C++20(与 Makefile 一致)

fix(libth): Cv_wait.hpp 中 lcok 拼写错误修正为 lock
2026-07-30 11:58:26 +08:00
qing f214c862d3 docs(bot4cj): 新增 README.robot.md 完整技术手册
- 补充 5 层架构分层图(脚本→解析器→结构构建→硬件实现→系统库)
- 详解 .mt 脚本语法、块类型、占位符规则
- 补充 PCA9685/Servo/MPU6050/Speaker 各硬件寄存器与校准参数
- 说明执行流程:stdin→FSM 解析→链表→build/play/free
- 列出 I2C 总线保护、GC 析构、pthread_cancel 等安全限制
- 补充 11 条已知缺陷清单(D1-D11)
- 新增 NN/CNN 测试命令与 DRL 预留接口说明
2026-07-30 11:54:51 +08:00
qing a8a190571c feat(gateway): add /refresh_token API with grace window + refresh limit
- POST /refresh_token: accept current Bearer token (or Cookie fallback),
  verify RSA signature, check grace window (iat + keep_time * multiplier)
  and refresh count limit, then issue a new token.
- Token payload extended with iat, refresh_count, max_refresh fields.
- iat preserves original issue time; created_at updates on each refresh.
- Rate limited: 5 req / 5 min / IP via RateLimiter.
- Config: Token.refresh_grace_multiplier (default 2), Token.max_refresh (default 10).
- Backward compatible: old tokens without new fields still pass proxy checks.
2026-07-30 11:54:31 +08:00
qing aa3a99825c docs(captcha): 项目结构树和安装指引补全遗漏项
- 安装章节新增 bash _set.sh 初始化步骤
- 项目结构树补全 _set.sh 和 requirements.txt
2026-07-30 11:53:30 +08:00
qing dcfdbcb9f2 docs(waf): 更新 README 文档与代码实现对齐
- 新增「安全响应头」章节,列出 7 个 WAF 注入的响应头及说明
- 修正「已知限制」中响应体检测描述,明确响应头检测已启用
- 澄清拦截日志等级区分(WARNING 命中拦截 / ERROR 加载错误)及 extra 字段类型
- 补充规则 ID 分段表的 CRS 可选说明
- 更新目录结构列表补充 logclient 子包
- 请求处理流程新增 ModifyResponse 步骤
2026-07-30 11:49:10 +08:00
qing 62007e7848 docs(static): README 路由表、架构图、配置、依赖与实际代码同步
- 修正根路径 / 描述:index.html → login.html
- 补全全部 19 个路由(原仅 6 个),分组为核心/内部工具/OAuth/代理
- 架构图补全 waf:443 → gateway:8443 → static:9200 完整链路
- 配置示例新增 [cors] 段
- 依赖列表补全 waitress 和 pymsg
- 页面和功能列表补全遗漏项
2026-07-30 11:48:54 +08:00
qing 91e6659dac docs(waf): 拦截日志章节更新为 logclient 推送 db Logs 表的说明 2026-07-30 11:01:34 +08:00
qing 9765edac9b fix(static): 前端脚本识别 WAF 拦截响应并正确提示
- validation.js 新增 WafTip 公共检测:403 + 响应体含 blocked by WAF 双重特征,
  与 gateway 白名单拒绝的 403(JSON 错误消息)精确区分
- login/register/dnfs/view 各请求点在 JSON 解析前识别拦截,统一提示
  「请求被 WAF 安全规则拦截,请检查输入内容」
- 修复 dnfs getNode 被 WAF 拦截时落入 catch 误跳登录页的问题
- 验证码图片加载识别 403 拦截,alt 提示点击重试
- __view.html 补充引入 validation.js
2026-07-30 11:01:34 +08:00
qing 40175eddb5 Revert "style(dnfs): 用户信息面板移到左边中间,与右侧 minimap 镜像对称"
This reverts commit 5c33dbe027.
2026-07-30 09:52:08 +08:00
qing 5c33dbe027 style(dnfs): 用户信息面板移到左边中间,与右侧 minimap 镜像对称 2026-07-30 09:44:15 +08:00
qing a58a0f97e3 feat(dnfs): minimap 与用户面板按视口宽度决定初始折叠态
新增 PANEL_EXPAND_MIN_WIDTH=768 断点:宽屏默认展开、窄屏默认折叠避免
遮挡内容;仅决定初始状态,用户手动折叠/展开不被 resize 覆盖。
2026-07-30 09:40:58 +08:00
qing 55b127747a fix(dnfs): 用户面板用户名/角色改为纯展示,去除手型光标与 hover 变色
新增 .tag.no-hover 修饰类还原交互态;折叠按钮与退出登录保持可点样式。
2026-07-30 09:34:53 +08:00
qing 8c1956cdeb feat(dnfs): 左上角用户信息面板(默认折叠),新增 /whoami 身份接口
dnfs 新增 POST /whoami:解 Token payload 返回 user/is_admin,无或畸形
Token 返回 login=false(HttpOnly Cookie 模式下 JS 无法读 Token,身份
必须由后端下发);static 加 /whoami 代理(Cookie 转 Bearer);前端
dnfs.html 左上新增 userpanel,镜像 minimap 样式默认折叠,展开显示
用户名/角色/退出登录(POST /logout 清 Cookie 后跳登录页)。
2026-07-30 09:31:28 +08:00
qing 1388fcb72b feat(dnfs): 卡片移动按钮一并纳入 can_operate 权限显隐
编辑/移动/删除三个按钮合并为单次权限判断(移动本质也是 update_note),
未登录或无权时不渲染整个按钮组。
2026-07-30 09:21:15 +08:00
qing b4e1cf462a feat(dnfs): get_notes 按当前用户注入 can_operate,前端按权显隐编辑/删除按钮
dnfs get_notes 可选解析 Authorization(畸形 Token try 兜底),为每个节点
附带 can_operate 标记(is_admin 或 owner,口径与 delete/update 一致);
dnfs.js 卡片编辑/删除按钮仅 can_operate === true 时渲染,未登录或无权隐藏。
2026-07-30 09:17:28 +08:00
qing 46c3a197be refactor(auth): 管理员判定上移至 gateway,经 Token payload 下发 is_admin
gateway 登录签发 Token 时,账号命中 [Admin] users 白名单即在 payload
写入 is_admin=true(随 RSA 签名保护);dnfs 移除本地 ADMIN_USERS 硬编码,
delete_note/update_note 豁免判断改读 payload 的 is_admin 字段。
旧 Token 无此字段按非管理员处理(安全方向)。
2026-07-30 09:08:33 +08:00
qing e9f9860fe1 fix(dnfs): review_content 兼容布尔/字符串审核结果,修复安全内容误拦
仅明确判定安全(布尔 false 或字符串 "false")才放行,其余一律拦截,
保持 fail-closed;同时消除 invalid 字段缺失时的 KeyError。
2026-07-30 08:38:14 +08:00
qing 949d274c22 获取节点失败后跳转 2026-07-30 08:22:53 +08:00
qing bcd6c84e3b Merge branch 'dev_qing' of https://git.cancerai.top/qing/micro_servers into dev_qing 2026-07-29 21:57:47 +08:00
qing 5275b8bf75 feat(smtp): 邮箱验证码连续校验失败 5 次自动作废,防枚举 6 位数字码 2026-07-29 21:57:16 +08:00
qing e6e5dab4cd 智能体提问 2026-07-29 21:54:22 +08:00
qing 3c02ba43ad AskUserTool 内置技能 2026-07-29 21:23:04 +08:00
qing c5dd8d198d feat(smtp): 同一邮箱 60 秒发送冷却,gateway 429 透传 + 前端重发倒计时(gateway v0.5.3) 2026-07-29 20:54:56 +08:00
qing f4a7aca2fa update: smtp 验证码有效期 60s 调整为 10 分钟,邮件正文从 CODE_TTL 派生
60 秒对邮件送达延迟过于紧张;正文有效期文案改为按 TTL 自动生成避免漂移,前端提示与 README 同步。同时移除 .env 排查用的凭证明文调试打印。
2026-07-29 19:32:44 +08:00
qing f5b69d0b4c fix: smtp .env 解析兼容 KEY: VALUE 冒号格式,MAIL_FROM 默认跟随 SMTP_USER
服务器 .env 误用 YAML 冒号格式导致凭证全部加载为空;163 等服务商强制 From 头与登录账号一致,写死的 MAIL_FROM 会触发 553,现默认取 SMTP_USER、可用 .env 显式覆盖。
2026-07-29 19:08:13 +08:00
qing 53c742c26e refactor: smtp.py.remote 更名为 smtp_v2.py,恢复 .py 后缀以便工具链识别
_run.sh 同步指向新文件名;start() 新增启动时凭证明文打印(仅 stdout 不落日志库),用于排查 .env 加载问题,调试完成后需移除。
2026-07-29 18:36:21 +08:00
qing 11993e178b smtp dev 2026-07-29 18:22:38 +08:00
qing 2a6d68386d Merge pull request 'Dev qing' (#13) from dev_qing into main
Reviewed-on: #13
2026-07-29 14:39:15 +08:00
qing 3bc4f9720f fix(dnfs): 移动请求期间禁用按钮防止重复提交
后端 update_note 走 AI 审核耗时较长,confirmMove 期间禁用移动条
与表单按钮,finally 统一恢复
2026-07-29 14:37:27 +08:00
qing 751866d9c1 refix 2026-07-29 11:52:46 +08:00
qing 3f9bffb9ea feat(dnfs): 节点移动功能
- dnfs.js: 卡片新增移动按钮,startMove 暂存节点,底部悬浮移动条
  提供移动到当前层/取消;confirmMove 经 update_note 改 pid 完成迁移;
  拦截移动到自身与已在当前层的情况
- dnfs.html: 新增 #movebar 容器
- theme.css: act-move 描边按钮与 movebar 底部悬浮条样式
2026-07-29 11:40:35 +08:00
qing ace98caff6 feat: 卡片编辑/删除按钮 + dnfs 管理员豁免
- dnfs.js: 卡片底部新增编辑/删除按钮,复用下方表单支持编辑模式
  (startEdit/cancelEdit/updateNode/deleteChild),stopPropagation 防误触
- theme.css: 新增 card-actions/act-btn 按钮样式,编辑橙色/删除红色软底
- dnfs.py: ADMIN_USERS 统一管理豁免名单(admin, admin@qsont.me),
  update_note 新增管理员豁免,delete_note 同步改用名单
- dnfs/README.md: 同步更新所有权豁免说明
2026-07-29 11:22:30 +08:00
qing a0f6a14b5d style(dnfs): 笔记卡片尺寸随内容自适应
- recommend-list 改为 flex 纵向布局,gap 统一间距,隐藏 JS 遗留 <br> 分隔符
- 卡片宽度 fit-content 包裹内容,min-width 240px 保底、max-width 100% 封顶
- 内容图片限宽防撑破自适应布局
2026-07-29 11:05:20 +08:00
qing 2fc12e7b00 feat(dnfs): 卡片增加观看/点赞/分享互动统计图标
- dnfs.js: 新增眼睛/爱心/转发 SVG 图标常量与 makeStatItem 构造函数,
  统计行挂载在 owner 胶囊下方,计数缺省显示 0(后端暂无该字段)
- theme.css: 新增 card-stats/stat-item/stat-num 样式
2026-07-29 10:59:40 +08:00
qing dae81ba4a1 style(dnfs): 美化卡片发帖用户显示为头像+昵称胶囊
- dnfs.js: owner 纯文本改为首字符圆形头像 + 昵称,空值兜底匿名
- theme.css: 新增 Owner Chip 样式段,复用主题变量自动适配深色模式
2026-07-29 10:53:29 +08:00
qing a905dac00d display note ownerx 2026-07-29 10:47:20 +08:00
qing b8ec752a12 docs(pymsg): 添加中文 README 文档,涵盖协议、API 参考和实际用例 2026-07-29 09:39:49 +08:00
qing b86b089634 fix(BotOps): 修正 aibox 协议语义错配导致的输出丢失
- 服务端 chunk 为增量片段(chunk_buffer 发送后即清零),原 _delta
  按累计全文假设计算,相邻 chunk 前缀撞字时静默丢字符;改为直接拼接
- complete 携带完整全文,finish_ai 改为全文对齐(不一致时整体替换,
  可自愈丢帧/乱序),修复结尾全文重复 append
- 新增 line/reasoning/task_added/dialogue_reset/pong 消息路由:
  line(🔧 工具调用提示、工具输出)不再落入兜底污染聊天区,改投任务侧栏
- 发送失败时移除预建的空 AI 消息框并提示,避免空等回复
- 断线提示补充重连窗口期服务端输出丢失说明
- 同步修正 ws_client/README 中的协议描述
2026-07-29 09:32:30 +08:00
qing f9cf408c82 static: 切换 fsys.py 到 Waitress WSGI 服务器
- HTTP 场景使用 waitress.serve(threads=8) 替代 Flask 开发服务器
- SSL 场景回退 Flask 内置服务器(Waitress 不支持 TLS)
- requirements.txt 新增 waitress 依赖
2026-07-29 09:10:09 +08:00
qing f0cdaacd85 feat(gateway): 运行日志接入 db 集中式日志(新增 C++ 版 LogC,pymsg 推送 Logs 表)
- 新增 gateway/inc/LogC.hpp:异步批量日志客户端(内存队列+后台线程,50条/2秒 flush),
  对齐 Python 版 log_client.py,fail-safe 设计(db 不可用静默丢弃)
- main.cpp 全部日志输出迁移:wrapperHttp 双异常分支(>=500 ERROR / 其余 WARNING)、
  启动/关闭、登录成功、注册成功、验证码邮件发送成功、代理白名单拒绝
- Makefile 增加 -pthread(LogC 使用 std::thread)
2026-07-29 08:34:14 +08:00
qing 6a0551ab34 fix(gateway): 限流错误码统一429、登录用户不存在改401并拉平时序、pymsg超时常量化(v0.5.2)
- 三处限流错误码统一为 429(login 400→429 并修 attampts 拼写;register/send_code 403→429)
- 登录用户不存在不再兜底 500:空结果统一走 401,dummy PBKDF2 校验拉平时序防用户枚举
- db 查询失败消息误名修正:Login (Update Step) Failed → (Query Step)
- 超时常量化:DbC::DEFAULT_TIMEOUT=5.0;_TIMEOUT_CAPTCHA/_SMTP_VERIFY=5.0、_SMTP_SEND=20.0
- 文档:gateway/README 数据库节 email→username 重写、建表归 db/create_sqlite3.sql;根 README 版本号与自动建表描述同步
2026-07-29 00:21:13 +08:00
qing c2e9cce6a7 fix(BotOps): _inst.sh 补 --include-package=websockets/textual,修复打包后找不到 websockets.asyncio 2026-07-28 23:38:16 +08:00
qing b07dbf2da5 feat(gateway): 登录成功后更新 Users.last_login(v0.5.1)
启用 handleLogin 中被注释的 last_login 更新:经 DbC::call_msg 以
"$now" 哨兵(db 日期适配层转为服务端 UTC 时间)写入,失败按惯例
THROW_EX_CUSTOM 抛 500 由 wrapper 统一处理。README 版本节同步
(原仍停留在 0.3.0-security)。
2026-07-28 23:31:20 +08:00
qing 19d5b65625 fix(db): 新增日期列类型适配层,修复无法经 db 处理 DATETIME 列的问题
JSON 无日期类型,而 SQLite DATETIME 只接受 Python datetime 对象,
导致各服务对日期列的写入与 where 比较全部失败。_coerce_value 按反射
列类型把 ISO 8601 字符串转为 datetime/date/time(带时区偏移统一转
UTC),并支持 "$now" 哨兵取服务端当前时间;响应中日期时间改为一律
序列化为 ISO 8601(T 分隔)。黑盒测试补 3 个日期列场景。
2026-07-28 23:16:17 +08:00
qing 12e736d264 chore: BotOps .gitignore 忽略 nuitka 编译产物 2026-07-28 22:54:06 +08:00
qing 8be62683dc chore: BotOps 添加 _inst.sh 单文件编译脚本(nuitka --onefile) 2026-07-28 22:53:35 +08:00
qing c93023bd5d fix(BotOps): 聊天区改用 anchor 锚定吸底,修复流式时滚动条不跟随 2026-07-28 22:48:45 +08:00
qing 422ca798da chore: BotOps 添加 _set.sh/_run.sh 启动脚本(与 aibox 同款) 2026-07-28 22:44:24 +08:00
qing c064caa3ec chore: BotOps 添加 .gitignore 并移除误提交的 __pycache__ 2026-07-28 22:41:34 +08:00
qing f91d4166d6 feat: 新增 BotOps 子模块——aibox 的 Textual TUI 前端(流式 Markdown 渲染+自动重连+任务侧栏) 2026-07-28 22:41:15 +08:00
qing afb55494e5 interpreter project 2026-07-28 22:25:01 +08:00
qing 6692c9c4ee refactor(db): 清理遗留死代码并对齐文档与测试
删除 dbv2 从未挂载的 log_module.py(Flask /log/* 遗留)与纯 TCP 服务
用不到的 security_middleware.py 副本;db/README.md 按 pymsg 实现重写,
requirements 去掉 flask/waitress;黑盒测试对 pymsg 服务改用 TCP 探测,
修复 check_service HTTP 探测导致的 db 测试整体假 skip。
2026-07-28 22:21:36 +08:00
qing dabd892601 fix(aibox/client): 放弃 rich.Live 改为 complete 时一次性静态渲染,修复内容一闪而过 2026-07-28 22:01:21 +08:00
qing 2982bc30a4 feat(aibox/client): AI 回复流式渲染 Markdown(rich.Live,无 rich 时降级纯文本) 2026-07-28 21:51:53 +08:00
qing aaeac343d3 fix(aibox/client): 修4处——stdin单线程防泄漏、LRU去重上限、自动重连、弃用get_event_loop 2026-07-28 21:44:23 +08:00
shufeng2012 9e2667d604 🦺 扩大验证码颜色生成范围 2026-07-28 20:38:43 +08:00
shufeng2012 c592b02636 🦺 更新验证码混淆程度(我自己都过不了) 2026-07-28 20:17:28 +08:00
qing d5a67ce77e fix: smtp .env 兼容 export 前缀并在启动时报告缺失的配置项名称 2026-07-28 17:42:37 +08:00
qing 6330e04474 fix: smtp .env 手动解析替代 dotenv,同时支持 KEY=VALUE 和 KEY VALUE 格式 2026-07-28 17:34:55 +08:00
qing df518d1adb feat: 注册强制邮箱验证码(gateway v0.5.0 新增 /register/send_code,pymsg 调 smtp) 2026-07-28 17:29:52 +08:00
qing 9078c4d051 refactor: smtp 改造为 pymsg TCP 服务并修复 NameError/finally/KeyError 等已知 bug 2026-07-28 17:12:45 +08:00
qing bcd49f0e81 Merge pull request 'Dev qing' (#12) from dev_qing into main
Reviewed-on: #12
2026-07-28 12:46:26 +08:00
qing 78ad3e6d57 移植bot4cj 2026-07-28 12:44:07 +08:00
qing 7803843155 ^-^ 2026-07-28 12:37:39 +08:00
qing ec755a30f2 3d打印件 2026-07-28 12:34:02 +08:00
qing 483c5f227f fix: 发布节点期间同步禁用重置/删除按钮,防止误触 2026-07-28 12:17:27 +08:00
qing bfd2d334fe fix: 前端按钮防重复点击,避免重复请求(dnfs提交/删除、view刷新、验证码刷新) 2026-07-28 12:10:48 +08:00
qing 50f209e84a fix: X-Frame-Options/frame-ancestors 改为同源以修复条例弹窗空白 2026-07-28 11:53:35 +08:00
qing 680d28b616 ui: 用户使用条例改为弹窗显示并移除 LICENSE.html 返回按钮 2026-07-28 11:46:25 +08:00
qing 4d16bc32fc security: CORS 白名单移除 localhost 并改为 config.ini 驱动
扫描发现生产 CORS 含 http://localhost:9200(硬编码于 fsys.py),且 config.ini 的 cors 段(通配符 *)从未被读取。白名单改为从 config.ini 读取,生产值仅 qsont.me/yhdet.top,localhost 仅限本地开发显式配置。
2026-07-28 11:40:19 +08:00
qing 7726857d6b feat: 新增用户使用条例独立页面并在登录页引用
新增 LICENSE.html(六章条例,开源社区定位,复用主题样式);登录页勾选框文案改为链接新标签页打开,不影响勾选交互与表单状态。
2026-07-28 10:36:06 +08:00
qing 3ef7d1b780 fix: 修复验证码图片被输入框挤换行,改为右侧同一行
theme.css 全局设定文本输入框 display:block;width:100%,导致验证码图片无法与之同行;改用 flex 容器包裹(输入框 flex:1、图片 flex:none 居右),布局与主题风格一致。
2026-07-28 10:29:22 +08:00
qing 62804d242c ui: 验证码改为 4 位,登录页图片移到输入框左侧
captcha 服务 CAPTCHA_LEN 调整为 4 并适配字符间距;登录页验证码图片与输入框换序,同一行展示更紧凑。
2026-07-28 10:24:48 +08:00
qing a5e581ef24 fix: CSP img-src 放行 blob:,修复验证码图片被拦截
登录页验证码用 URL.createObjectURL 生成的 blob: URL 显示,被 img-src 'self' data: 拦截导致图片无法加载;五处 CSP 定义(waf 边缘 + 四个 Flask 中间件副本)同步放宽。
2026-07-28 10:19:47 +08:00
qing 6fcb70d235 feat: 登录页接入图形验证码,登录接口强制校验
登录页展示 captcha 图形码(点击刷新),登录请求携带 captcha_token/captcha_answer;gateway 的 /login 启用验证码校验,经 pymsg 调 captcha verify 路由(fail-closed),补齐防爆破短板。
2026-07-28 10:15:01 +08:00
qing 323dd3061b rewrite captcha 2026-07-28 10:07:32 +08:00
qing 44c1e5ef72 feat: captcha 改造为 pymsg/TCP 服务,gateway 新增图形码代理路由
captcha 参照 dbv2 统一为 pymsg 协议(image/verify/health 路由),与主干 RPC 栈对齐;gateway 新增 GET /captcha/image 做 HTTP→pymsg 转换,解码 base64 PNG 并以原契约(image/png + X-Captcha-Token)返回浏览器。
2026-07-28 09:54:08 +08:00
qing d06546f6bf 测试模块初步完成 2026-07-28 09:39:37 +08:00
qing 4ad3fbfca8 fix: 修复公开页面因旧 Cookie 验签失败误返回 401
浏览器会自动给每个请求携带 q4_token,gateway 重启轮换内存 RSA 密钥后旧凭证验签失败,导致无需登录的公开页面也 401。代理层改为仅对需鉴权目标校验凭证,公开目标跳过;前端遇 401 自动跳转登录页自愈。
2026-07-28 09:31:10 +08:00
qing b2ddd9e7e3 security: waf 边缘统一注入安全响应头
外部扫描发现站点缺少安全响应头。在 waf 反代/403/502 三个出站路径统一注入 XFO/nosniff/Referrer-Policy/Permissions-Policy/HSTS/CSP,下游同名头一律覆盖避免重复;static 补上漏调用的 apply_security_headers 做纵深防御。
2026-07-28 09:19:43 +08:00
qing e89578fa82 security: Token 改为 HttpOnly Cookie 下发,消除 XSS 窃取面
登录时网关签发 HttpOnly+Secure+SameSite=Lax 的 q4_token Cookie,前端不再经手 Token;网关代理时支持从 Cookie 验签并向下游注入 Authorization(dnfs 零改动),新增 /logout 清除 Cookie,static 代理路由兼容 Cookie 转 Bearer。
2026-07-28 09:07:12 +08:00
qing 65cfaf97a1 docs: 按代码实况修正根 README 与实现不符的描述
db 实为 pymsg/TCP(非 HTTP),ai_review 实为 fail-closed,captcha 路由与实现方式、static 的 dnfs 代理与账号旁路、gateway 用户存储与 core 守护机制等均按代码核实结果更正。
2026-07-28 08:33:30 +08:00
qing e923b32b70 Merge pull request 'Dev qing' (#9) from dev_qing into main
Reviewed-on: #9
2026-07-27 21:47:16 +08:00
559 changed files with 126521 additions and 5039 deletions

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
# Git
.git
.gitignore
.gitattributes
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
.env
.env/
.venv/
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
.DS_Store
# Build artifacts
*.bin
*.o
*.a
*.out
# Documentation (exclude READMEs)
*.md
!README.md
docs/
# Logs
*.log
logs/
# Data files (handled via volumes)
*.db
*.sqlite
*.sqlite3
data/
chroma/
# Other modules not needed for build
bot4cj/
core/
gateway/
lib*/
waf/
captcha/
smtp/
static/
tests/
3d/
BotOps/
pymsg/tests/
+11
View File
@@ -0,0 +1,11 @@
# AI Review 服务配置
# 复制此文件为 ai_review/.env 并填入你的 API Key
# DeepSeek/OpenAI API 密钥
OPENAI_API_KEY=your_api_key_here
# API Base URL(默认使用 DeepSeek)
OPENAI_BASE_URL=https://api.deepseek.com/v1
# 使用的模型名称
MODEL_NAME=deepseek-chat
+5
View File
@@ -1,3 +1,8 @@
gateway/shufeng.ini
core/Nidus.service.shufeng
ai_review/.env
confs/__pycache__/
.workbuddy/
libhttp/test_server
*.pyc
__pycache__/
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+13452
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
Binary file not shown.
+1026
View File
File diff suppressed because it is too large. Load diff
+2476
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
+2698
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
+2809
View File
File diff suppressed because it is too large. Load diff
+2834
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -0,0 +1,9 @@
__pycache__/
*.pyc
venv/
# nuitka 编译产物
botops.bin
app.build/
app.dist/
app.onefile-build/
+91
View File
@@ -0,0 +1,91 @@
# BotOps — aibox 智能体 TUI 前端
复刻并增强 [aibox/client.py](../aibox/client.py),基于 [Textual](https://textual.textualize.io/) 的现代化终端 UI,连接 aibox 微服务(WebSocket,默认 `ws://localhost:8765`)。
## 相比 client.py 的增强
| 能力 | client.py | BotOps |
|------|-----------|--------|
| AI 回复渲染 | 纯文本流式 / 一次性静态渲染 | **Markdown 增量流式渲染**(无闪烁) |
| 终端管理 | 手搓光标/提示符 | Textual 部件框架(多面板布局) |
| 任务事件 | 混在对话流里 | **独立侧栏**(task_process / task_status / task_error) |
| 自动重连 | ✅(已移植) | ✅ 指数退避 2s→30s,`/disconnect` 抑制 |
| 快捷键 | Ctrl+C 中断 | Ctrl+C 中断 / Ctrl+Q 退出 / Ctrl+L 清屏 |
## 安装与运行
```bash
cd BotOps
# 一键初始化(创建 venv + 安装依赖)
bash _set.sh
# 启动
bash _run.sh # 默认 ws://localhost:8765
bash _run.sh ws://your-host:8765 # 指定 aibox 地址
```
### Nuitka 编译为独立二进制
```bash
bash _inst.sh # 产出 ./botops.bin(单文件,无需 Python 环境)
./botops.bin [ws://host:port]
```
## 命令
- 本地命令(不发给服务器):`/connect` `/disconnect` `/interrupt` `/clear` `/status` `/help` `/quit`
- 其余 `/` 命令透传给 aibox 服务器(与 client.py 行为一致)
## 结构
```
BotOps/
├── app.py # Textual App:布局 + 消息路由 + 快捷键
├── ws_client.py # WebSocket 协议层(自动重连)
├── commands.py # 斜杠命令注册表
├── _run.sh # 启动脚本
├── _set.sh # 初始化脚本(venv + 依赖安装)
├── _inst.sh # Nuitka 编译脚本(产出 botops.bin)
├── requirements.txt # Python 依赖
└── widgets/
├── __init__.py
├── chat_view.py # 聊天区:AI 回复 Markdown 增量流式渲染
└── status_bar.py # 底部状态栏(连接状态)
```
## 协议说明
与 aibox 服务端协议完全兼容(详见 aibox/client.py 注释):
- 发送:`{"type":"message","content":...}` / `{"type":"command","command":"/..."}` / `{"type":"interrupt",...}`
- 接收:
- `chunk`(**增量片段**):服务端 `chunk_buffer` 发送后即清零,前端直接 `append_ai` 拼接,为内容权威来源
- `reasoning_start`(**思考开始**):创建灰色斜体思考区域,实时展示 AI 思考过程
- `reasoning`(**思考流**):思考内容增量片段,追加到思考区域(超长自动折叠显示最后 3000 字符)
- `reasoning_end`(**思考结束**):思考结束,切换到正式回答
- **顺序保证**:不预创建空 AI widget,按消息到达顺序自然挂载(`reasoning_start`→`reasoning`→`chunk`),思考区域必然在 AI 正式回答**之前**
- `complete`(**收尾信号**):chunk 流式已完整渲染,complete 仅用于收尾(重置状态)
- `line`(过程行):🔧 工具调用提示、工具输出,路由到任务侧栏;助手前缀/思考提示在聊天区已由对应区域承载,不重复展示
- `info`(系统信息):过滤"Conversation started"/"对话开始"等隐式通知,避免污染聊天流
- `dialogue_ended` / `dialogue_reset` / `task_added` / `task_process` / `task_status` / `task_error` / `interrupt_ack` / `error` / `command_result` / `connection_ack` / `notification` / `pong`
## 更新日志
### v1.3
- ✅ **适配服务端中断机制(Ctrl+C可靠停止)**:
- 收到 `interrupt_ack` 时立即收尾前端状态:调用 `abort_reasoning()` 将思考区域更新为"🛑 思考已中断"标签,调用 `finish_ai()` 结束AI回复状态
- 新增 `ChatView.abort_reasoning()` 方法:中断时更新思考区域标签(保留已展示的思考内容)
- `append_reasoning()` 增加 `_ai_finalized` 检查,中断后丢弃残留的思考片段防止重新创建思考区域
- 中断后服务端发来的残留 `chunk`/`reasoning`/`complete` 消息均被安全忽略,不会污染UI
### v1.2
- ✅ **彻底修复思考区域顺序问题**:移除预创建空 AI widget 的逻辑,改为第一次收到 `chunk` 时才创建 widget。按消息到达顺序自然挂载(`reasoning_start`→`reasoning`→`chunk`),从根本上保证思考区域始终在正式回答之前
- ✅ 简化 `complete` 处理逻辑:仅作为收尾重置状态信号,不再进行补建逻辑
### v1.1
- ✅ 支持 DeepSeek 思考模式展示:新增 `reasoning_start`/`reasoning`/`reasoning_end` 消息处理,思考内容以灰色斜体区域展示
- ✅ 超长思考内容自动折叠(显示最后 3000 字符)
- ✅ 过滤"Conversation started"/"对话开始"等隐式系统通知,避免在回答末尾显示无用信息
### v1.0
- 初始版本:Markdown 增量流式渲染、任务独立侧栏、自动重连、快捷键支持
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# 编译 BotOps 为独立单文件(不运行):产出 ./botops.bin
# 用法: ./_inst.sh
cd "$(dirname "$0")" || exit
source venv/bin/activate
python3 -m nuitka --standalone --onefile \
--assume-yes-for-downloads \
--include-package=websockets \
--include-package=textual \
--include-package-data=textual \
--output-filename=botops.bin \
app.py
echo "编译完成: ./botops.bin(运行: ./botops.bin [ws://host:port])"
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
source venv/bin/activate
exec python3 app.py $@
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env python3
"""BotOps — aibox 智能体的现代化 TUI 前端(Textual)。
复刻并增强 aibox/client.py:
- AI 回复流式 Markdown 渲染(Markdown 部件增量追加,无闪烁)
- 断线指数退避自动重连(/disconnect 抑制)
- 任务事件独立侧栏(task_process / task_status / task_error)
- Ctrl+C 中断生成、Ctrl+Q 退出、Ctrl+L 清屏
用法:
python app.py [ws://host:port] # 默认 ws://localhost:8765
"""
import sys
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal
from textual.widgets import Footer, Header, Input, RichLog
from commands import CommandRegistry
from widgets.chat_view import ChatView
from widgets.status_bar import StatusBar
from ws_client import BotWSClient
DEFAULT_URI = "ws://localhost:8765"
class BotOpsApp(App):
TITLE = "BotOps"
SUB_TITLE = "aibox TUI"
CSS = """
#chat {
width: 3fr;
border: solid $primary;
}
#tasks {
width: 1fr;
border: solid $secondary;
}
.msg-user {
color: cyan;
padding: 0 1;
margin-top: 1;
}
.msg-ai {
padding: 0 1;
}
.msg-system {
color: $text-muted;
padding: 0 1;
}
.msg-error {
color: red;
padding: 0 1;
}
.msg-task {
color: yellow;
padding: 0 1;
}
.msg-reasoning {
color: $text-muted;
background: $surface;
padding: 0 1;
margin: 0 1;
text-style: italic;
}
#status {
height: 1;
}
"""
BINDINGS = [
Binding("ctrl+c", "interrupt", "中断", show=True),
Binding("ctrl+q", "quit", "退出", show=True),
Binding("ctrl+l", "clear", "清屏", show=True),
]
def __init__(self, uri: str = DEFAULT_URI):
super().__init__()
self.ws = BotWSClient(uri)
self.ws.on_message = self._on_ws_message
self.ws.on_state = self._on_ws_state
self.registry = CommandRegistry(self)
# ── 布局 ─────────────────────────────────────────────────────────────
def compose(self) -> ComposeResult:
yield Header()
with Horizontal():
yield ChatView(id="chat")
yield RichLog(id="tasks", max_lines=1000)
yield Input(placeholder="输入消息,/help 查看命令,Enter 发送", id="input")
yield StatusBar(id="status")
yield Footer()
async def on_mount(self):
self.query_one("#input", Input).focus()
self._refresh_status("")
self.run_worker(self.connect_to(self.ws.uri))
@property
def chat(self) -> ChatView:
return self.query_one("#chat", ChatView)
@property
def tasks(self) -> RichLog:
return self.query_one("#tasks", RichLog)
# ── 连接管理 ──────────────────────────────────────────────────────────
async def connect_to(self, uri: str):
if self.ws.connected:
await self.ws.disconnect()
self.ws.uri = uri
await self.notify_system(f"正在连接 {uri} …")
await self.ws.connect()
async def disconnect(self):
await self.ws.disconnect()
async def send_interrupt(self):
if await self.ws.send_interrupt():
await self.notify_system("🛑 已发送中断信号")
else:
await self.notify_system("未连接到服务器", level="error")
# ── UI 操作 ───────────────────────────────────────────────────────────
async def notify_system(self, text: str, level: str = "info"):
css = {"info": "msg-system", "error": "msg-error", "task": "msg-task"}.get(level, "msg-system")
await self.chat.add_system(text, css)
async def clear_chat(self):
await self.chat.reset()
def _refresh_status(self, extra: str):
self.query_one("#status", StatusBar).set_state(
self.ws.connected, self.ws.uri if self.ws.connected else "", extra)
# ── 输入处理 ──────────────────────────────────────────────────────────
async def on_input_submitted(self, event: Input.Submitted):
text = event.value.strip()
event.input.value = ""
if not text:
return
if text.startswith("/") and await self.registry.execute(text):
return
if not self.ws.connected:
await self.notify_system("未连接到服务器,先 /connect", level="error")
return
await self.chat.add_user(text)
await self.chat.start_ai() # 标记AI回复开始状态(不预创建空widget)
if not await self.ws.send_message(text):
await self.chat.finish_ai("") # 发送失败:重置AI状态
await self.notify_system("❌ 发送失败:连接已断开,请 /connect 重连", level="error")
# ── 快捷键 ────────────────────────────────────────────────────────────
async def action_interrupt(self):
await self.send_interrupt()
async def action_clear(self):
await self.clear_chat()
def action_quit(self):
self.exit()
# ── aibox 协议消息路由(对应 client.py 的 _handle_message)─────────────
async def _on_ws_message(self, data: dict):
t = data.get("type", "")
content = data.get("content", "")
if t == "chunk":
# 服务端 chunk 为增量片段(发送后即清零),直接拼接
await self.chat.append_ai(content)
elif t == "reasoning_start":
# 开始深度思考,创建思考区域
await self.chat.start_reasoning(content or "🤔 深度思考中...")
elif t == "reasoning":
# 思考内容增量
stripped = content
if stripped:
await self.chat.append_reasoning(stripped)
elif t == "reasoning_end":
# 思考结束
await self.chat.end_reasoning()
elif t == "line":
# 过程行(服务端 output_callback 原样透传):工具调用提示 / 工具输出 / 前缀
stripped = content.strip()
if not stripped:
pass
elif stripped.startswith((
"💬 助手", "💬 Assistant", # 助手前缀(聊天区已有对应区域)
"🤔 思考中", "🤔 Thinking", # 思考提示(聊天区已有思考区域)
"🤔 深度思考中",
"🤔 分析工具结果", "🤔 Analyzing", # 工具分析提示(后续有具体工具输出,此为冗余)
"🔧 需要调用工具", "🔧 Need to call",# 工具调用前置提示(后续有"调用工具: xxx"具体提示)
)):
pass # 冗余/有替代展示的提示行,过滤避免重复
else:
# 🔧 调用工具: xxx / 工具输出 / 用户中断提示等 → 任务侧栏
self.tasks.write(stripped)
elif t == "ai_break":
# 工具调用前分段:结束当前AI文本段(保留已显示内容),后续chunk创建新widget
await self.chat.break_ai()
elif t == "complete":
# complete 携带完整全文,用于对齐已显示内容
await self.chat.finish_ai(content)
elif t == "dialogue_ended":
await self.chat.finish_ai(self.chat.ai_text)
if content:
await self.notify_system(f"✅ {content}")
elif t == "task_added":
await self.notify_system(f"➕ {content}")
elif t == "dialogue_reset":
if content:
await self.notify_system(f"🔄 {content}")
elif t == "pong":
pass
elif t == "task_process":
sub = data.get("msg_type", "")
title = data.get("task_title", "未知任务")
if sub == "chunk":
tail = str(data.get("content", ""))[-200:]
if tail:
self.tasks.write(f"⚡ {title}: {tail}")
elif sub == "complete":
self.tasks.write(f"✅ 任务完成: {title}")
elif t == "task_status":
icon = {"executing": "⚙️", "complete": "✅", "failed": "❌",
"added": "➕", "batch_start": "🚀", "batch_complete": "🏁"
}.get(data.get("status", ""), "📋")
self.tasks.write(
f"{icon} [{data.get('task_id', '?')}] {data.get('title', '')}: "
f"{data.get('message', content)}")
elif t == "task_error":
self.tasks.write(
f"❌ [{data.get('task_id', '?')}] {data.get('task_title', '?')}: "
f"{data.get('error', '未知错误')}")
elif t == "interrupt_ack":
# 服务端确认中断:立即收尾前端状态(更新思考区域为已中断、结束AI回复)
await self.chat.abort_reasoning()
await self.chat.finish_ai("")
await self.notify_system(f"🛑 {content}")
elif t == "error":
await self.notify_system(f"❌ {content}", level="error")
elif t == "info":
# 过滤掉对话开始这类隐式通知(用户发消息自然知道对话开始了)
skip_keywords = ("Conversation started", "对话开始", "🎭")
if not any(k in content for k in skip_keywords):
await self.notify_system(f"ℹ️ {content}")
elif t == "command_result":
await self.notify_system(content)
elif t == "connection_ack":
await self.notify_system(f"✅ {content}")
elif t == "notification":
await self.notify_system(f"🔔 {content}")
elif content:
# 未知消息类型不追加到AI聊天区(避免非AI内容污染回复),仅记入侧栏日志
self.tasks.write(f"[未知消息 type={t}] {str(content)[:200]}")
# ── 连接状态回调 ──────────────────────────────────────────────────────
async def _on_ws_state(self, state: str, detail: str):
if state == "connected":
self._refresh_status("")
await self.notify_system(f"✅ 已连接到 {self.ws.uri}")
elif state == "disconnected":
self._refresh_status("")
await self.notify_system(
f"❌ {detail}(中断期间的服务端输出已丢失,如回复不完整请重发)")
elif state == "reconnecting":
self._refresh_status(detail)
elif state == "error":
self._refresh_status("")
await self.notify_system(detail, level="error")
def main():
uri = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URI
BotOpsApp(uri).run()
if __name__ == "__main__":
main()
+62
View File
@@ -0,0 +1,62 @@
"""斜杠命令注册表:/connect /disconnect /interrupt /clear /status /help /quit。
本地命令在此拦截处理;未注册的 / 命令透传给 aibox 服务器(与 client.py 行为一致)。
"""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app import BotOpsApp
class CommandRegistry:
def __init__(self, app: "BotOpsApp"):
self.app = app
self._commands = {
"/connect": ("连接服务器:/connect ws://host:port", self._connect),
"/disconnect": ("断开连接(不自动重连)", self._disconnect),
"/interrupt": ("中断当前生成(或按 Ctrl+C)", self._interrupt),
"/clear": ("清空聊天区", self._clear),
"/status": ("显示连接状态", self._status),
"/help": ("显示帮助", self._help),
"/quit": ("退出 BotOps", self._quit),
}
async def execute(self, line: str) -> bool:
"""是本地命令则执行并返回 True;否则返回 False(调用方应透传服务器)。"""
cmd = line.split()[0].lower()
entry = self._commands.get(cmd)
if entry is None:
return False
await entry[1](line)
return True
async def _connect(self, line: str):
parts = line.split()
if len(parts) < 2:
await self.app.notify_system("用法: /connect ws://host:port", level="error")
return
await self.app.connect_to(parts[1])
async def _disconnect(self, _line: str):
await self.app.disconnect()
async def _interrupt(self, _line: str):
await self.app.send_interrupt()
async def _clear(self, _line: str):
await self.app.clear_chat()
async def _status(self, _line: str):
if self.app.ws.connected:
await self.app.notify_system(f"✅ 已连接: {self.app.ws.uri}")
else:
await self.app.notify_system("○ 未连接")
async def _help(self, _line: str):
lines = ["可用命令:"] + [f" {k:<14} {v[0]}" for k, v in self._commands.items()]
lines.append(" 其余 / 命令将透传给 aibox 服务器")
await self.app.notify_system("\n".join(lines))
async def _quit(self, _line: str):
await self.app.ws.disconnect()
self.app.exit()
+3
View File
@@ -0,0 +1,3 @@
textual>=0.60.0
websockets>=11.0
nuitka
View File
Whitespace-only changes.
+205
View File
@@ -0,0 +1,205 @@
"""聊天视图:用户/AI/系统消息 + AI 回复流式 Markdown 渲染。
服务端协议(aibox/aibox.py output_callback 原样透传 msg_type):
- chunk 增量片段(正式回答内容),直接 append 到 AI 消息
- reasoning_start 思考开始信号,在当前末尾创建新的思考区域
- reasoning 思考内容增量片段,追加到当前活跃的思考区域
- reasoning_end 思考结束信号:更新标签后清除引用(widget保留显示),
后续 reasoning_start 会创建新的思考区域
- complete 收尾信号:chunk 流式是内容权威来源,complete 仅用于收尾;
仅当流式完全丢帧(未建 widget 且无内容)时,用 full_text 补建
顺序保证:不预创建空AI widget,按消息到达顺序自然挂载。
多轮工具调用场景:每轮思考(reasoning_start→reasoning_end)创建独立的思考区域,
按时间顺序分布在对话流中各自的AI回答之前,而非全部堆在顶部。
"""
from textual.containers import VerticalScroll
from textual.widgets import Markdown, Static
class ChatView(VerticalScroll):
"""可滚动聊天区。AI 回复用单个 Markdown 部件增量 append,实现流式渲染。
不预创建空widget:避免重排复杂性,消息按接收顺序自然挂载,
天然保证思考过程在正式回答之前。
滚动策略:每条新消息挂载后调用 anchor() 锚定底部——之后内容增长
(流式 append)容器会自动保持吸底;用户上翻时 Textual 自动脱锚,
下一条新消息再重新锚定。
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._ai_widget: Markdown | None = None
self._reasoning_widget: Static | None = None
self._ai_text = ""
self._reasoning_text = ""
self._ai_finalized = True # 初始无活跃 AI 消息
@property
def ai_text(self) -> str:
return self._ai_text
def _anchor_bottom(self):
"""锚定吸底(需在布局刷新后执行,否则滚到的是旧位置)。"""
self.call_after_refresh(self.anchor)
async def add_user(self, text: str):
await self.mount(Static(f"👤 你: {text}", classes="msg-user", markup=False))
self._anchor_bottom()
async def add_system(self, text: str, css_class: str = "msg-system"):
await self.mount(Static(text, classes=css_class, markup=False))
self._anchor_bottom()
async def start_ai(self):
"""开始AI回复(标记状态,不预创建空widget)。
为保持顺序正确,空widget不在此处创建。第一个chunk到来时才创建AI widget,
此时思考widget(如果有)已经先挂载,顺序自然正确。
"""
if not self._ai_finalized:
return
self._ai_text = ""
self._ai_finalized = False
async def start_reasoning(self, hint: str = "🤔 思考中..."):
"""开始展示思考过程区域。直接挂载在当前末尾位置。"""
if self._reasoning_widget is not None:
return
self._reasoning_text = ""
self._reasoning_widget = Static(hint + "\n", classes="msg-reasoning", markup=False)
await self.mount(self._reasoning_widget)
self._anchor_bottom()
async def append_reasoning(self, delta: str):
"""追加思考内容增量。"""
if not delta:
return
if self._ai_finalized:
# 已收尾(中断/完成),丢弃残留的思考片段
return
if self._reasoning_widget is None:
await self.start_reasoning()
self._reasoning_text += delta
# 限制思考内容显示长度(最多显示最后 3000 字符,避免思考过长占满屏幕)
display = self._reasoning_text
if len(display) > 3000:
display = "...(前略)\n" + display[-3000:]
self._reasoning_widget.update(f"💭 思考中...\n{display}")
self.call_after_refresh(self.scroll_end, animate=False)
async def end_reasoning(self):
"""结束当前思考区域,更新标签后清除引用(widget保留在界面上显示)。
清除引用后,后续 reasoning_start 会在当前末尾位置创建新的思考区域,
实现多轮工具调用时思考区域按时间顺序分布,而非全部堆在顶部。
"""
if self._reasoning_widget is not None and self._reasoning_text:
display = self._reasoning_text
if len(display) > 3000:
display = "...(前略)\n" + display[-3000:]
self._reasoning_widget.update(f"💭 思考过程({len(self._reasoning_text)}字符)\n{display}\n{'─' * 30}")
# 清除引用但保留widget在DOM中显示——下一轮reasoning_start会创建新widget
self._reasoning_widget = None
self._reasoning_text = ""
async def abort_reasoning(self):
"""中断当前思考区域,更新提示文字后清除引用。"""
if self._reasoning_widget is not None:
if self._reasoning_text:
display = self._reasoning_text
if len(display) > 3000:
display = "...(前略)\n" + display[-3000:]
self._reasoning_widget.update(f"🛑 思考已中断({len(self._reasoning_text)}字符)\n{display}")
else:
self._reasoning_widget.update("🛑 思考已中断")
# 清除引用,已显示的widget保留
self._reasoning_widget = None
self._reasoning_text = ""
async def _ensure_ai_widget(self):
"""确保AI widget存在(第一次chunk到来时创建)。"""
if self._ai_widget is None:
self._ai_widget = Markdown("", classes="msg-ai")
await self.mount(self._ai_widget)
self._anchor_bottom()
async def break_ai(self):
"""分段当前AI回答:清除引用但保留widget在DOM中显示。
用于工具调用场景:工具调用前的AI文字(如"好的让我查一下")保留显示,
后续chunk会创建新的AI widget,保证思考区域按时间顺序插入在两段AI文字之间。
与finish_ai不同:不设置_ai_finalized,对话继续中。
"""
if self._ai_widget is not None:
self._ai_widget = None
self._ai_text = ""
async def append_ai(self, delta: str):
"""追加增量片段(服务端 chunk 为增量语义,累积文本后 update 整个 Markdown)。
使用 update() 而非 Markdown.append():Textual 的 Markdown.append() 在
chunk 边界切断 Markdown 语法(如列表项、代码块)时可能导致重复解析渲染。
累积完整文本后 update() 确保每次都从完整文本重新解析,避免重复。
"""
if not delta:
return
if self._ai_finalized:
# 收尾后收到的延迟 chunk,丢弃以避免在上一条回复后追加脏数据
return
# 第一次收到chunk时创建widget(此时思考widget如果存在已经在前面了)
await self._ensure_ai_widget()
self._ai_text += delta
# 累积文本后整体update,而非增量append,避免Markdown增量解析导致的重复渲染
self._ai_widget.update(self._ai_text)
self.call_after_refresh(self.scroll_end, animate=False)
async def finish_ai(self, full_text: str = ""):
"""收尾一条 AI 消息。
chunk 流式追加是内容权威来源,complete 只做收尾:
- 已收尾则直接返回(防止 dialogue_ended + complete 双发导致重复挂载)
- widget 存在且有流式内容:保持已渲染内容不变,仅清空引用
- widget 存在但无内容(异常场景)且有 full_text:用 full_text 填充
- widget 不存在(未收到任何 chunk)且有 full_text:补建
- widget 不存在且无内容:正常无内容场景
"""
if self._ai_finalized:
return
w = self._ai_widget
self._ai_widget = None
if w is not None:
if self._ai_text:
# 有流式内容,chunk 已完整渲染,直接保留,不做替换
pass
elif full_text:
# widget存在但没收到任何chunk(异常):用full_text填充
w.update(full_text)
self._ai_text = full_text
else:
# widget存在但没有任何输出:移除空框(正常不会走到这里,因为不预创建)
await w.remove()
else:
# widget不存在(start_ai后没收到任何chunk),有full_text则补建
if full_text:
self._ai_text = full_text
self._ai_widget = Markdown(full_text, classes="msg-ai")
await self.mount(self._ai_widget)
self._ai_widget = None
self._anchor_bottom()
self._ai_text = ""
self._reasoning_widget = None
self._reasoning_text = ""
self._ai_finalized = True
self.call_after_refresh(self.scroll_end, animate=False)
async def reset(self):
"""清空聊天区(/clear)。"""
self._ai_widget = None
self._reasoning_widget = None
self._ai_text = ""
self._reasoning_text = ""
self._ai_finalized = True
await self.remove_children()
+14
View File
@@ -0,0 +1,14 @@
"""底部状态栏:连接状态 + 服务器地址 + 附加信息。"""
from textual.widgets import Static
class StatusBar(Static):
def set_state(self, connected: bool, uri: str = "", extra: str = ""):
dot = "●" if connected else "○"
color = "green" if connected else "red"
text = f"[{color}]{dot}[/] {'已连接' if connected else '未连接'}"
if uri:
text += f" {uri}"
if extra:
text += f" | {extra}"
self.update(text)
+148
View File
@@ -0,0 +1,148 @@
"""BotOps WebSocket 协议层(提炼自 aibox/client.py,增加状态回调与自动重连)。
协议:JSON 消息。
发送:{"type": "message", "content": ...}
{"type": "command", "command": "/..."}
{"type": "interrupt", "content": ...}
接收:chunk / line / reasoning / complete / dialogue_ended / dialogue_reset /
task_process / task_status / task_error / task_added / interrupt_ack /
error / info / command_result / connection_ack / notification / pong
说明:服务端 chunk 为**增量片段**(output_callback 发送 chunk_buffer 后即清零),
complete 为完整全文(可用于对齐);line 为过程行(工具提示/输出/前缀)。
"""
import asyncio
import json
from typing import Awaitable, Callable, Optional
import websockets
MessageHandler = Callable[[dict], Awaitable[None]]
StateHandler = Callable[[str, str], Awaitable[None]] # (state, detail)
class BotWSClient:
"""aibox 协议 WebSocket 客户端:断线指数退避自动重连,手动断开则抑制。"""
def __init__(self, uri: str):
self.uri = uri
self.ws = None
self.connected = False
self._manual_disconnect = False
self._listen_task: Optional[asyncio.Task] = None
self._reconnect_task: Optional[asyncio.Task] = None
self.on_message: Optional[MessageHandler] = None
self.on_state: Optional[StateHandler] = None
async def _emit_state(self, state: str, detail: str = ""):
if self.on_state:
await self.on_state(state, detail)
async def connect(self) -> bool:
"""连接并开始监听;成功返回 True。"""
await self._close_ws()
try:
self.ws = await websockets.connect(self.uri)
except Exception as e:
await self._emit_state("error", f"连接失败: {e}")
return False
self.connected = True
self._manual_disconnect = False
self._listen_task = asyncio.create_task(self._listen())
await self._emit_state("connected", self.uri)
return True
async def disconnect(self):
"""手动断开(抑制自动重连)。"""
self._manual_disconnect = True
if self._reconnect_task and not self._reconnect_task.done():
self._reconnect_task.cancel()
self.connected = False
await self._close_ws()
if self._listen_task and not self._listen_task.done():
self._listen_task.cancel()
await self._emit_state("disconnected", "已手动断开")
async def _close_ws(self):
if self.ws is not None:
try:
await self.ws.close()
except Exception:
pass
self.ws = None
async def send_message(self, text: str) -> bool:
"""发送用户消息(/ 开头按 command 发送,与 aibox 协议一致)。"""
if not self.connected or not self.ws:
return False
data = ({"type": "command", "command": text} if text.startswith("/")
else {"type": "message", "content": text})
try:
await self.ws.send(json.dumps(data, ensure_ascii=False))
return True
except Exception as e:
await self._emit_state("error", f"发送失败: {e}")
return False
async def send_interrupt(self) -> bool:
"""发送中断信号(对应 client.py 的 Ctrl+C 行为)。"""
if not self.connected or not self.ws:
return False
try:
await self.ws.send(json.dumps(
{"type": "interrupt", "content": "用户按下了 Ctrl+C 中断当前操作"},
ensure_ascii=False))
return True
except Exception:
return False
async def _listen(self):
"""接收循环:断线时在 finally 中触发自动重连。"""
try:
async for raw in self.ws:
try:
data = json.loads(raw)
except json.JSONDecodeError:
data = {"type": "chunk", "content": raw}
if self.on_message:
await self.on_message(data)
except websockets.exceptions.ConnectionClosed:
pass
except asyncio.CancelledError:
raise
except Exception as e:
await self._emit_state("error", f"接收错误: {e}")
finally:
was_connected = self.connected
self.connected = False
if was_connected:
await self._emit_state("disconnected", "连接已断开")
self._schedule_reconnect()
def _schedule_reconnect(self):
if self._manual_disconnect:
return
if self._reconnect_task and not self._reconnect_task.done():
return
self._reconnect_task = asyncio.create_task(self._reconnect_loop())
async def _reconnect_loop(self):
"""指数退避 2s→30s 封顶,直到成功或手动断开。"""
delay = 2
while not self._manual_disconnect and not self.connected:
await self._emit_state("reconnecting", f"{delay}s 后自动重连 {self.uri} …")
try:
await asyncio.sleep(delay)
except asyncio.CancelledError:
return
if self._manual_disconnect:
return
try:
self.ws = await websockets.connect(self.uri)
except Exception:
delay = min(delay * 2, 30)
continue
self.connected = True
self._listen_task = asyncio.create_task(self._listen())
await self._emit_state("connected", self.uri)
return
+110 -53
View File
@@ -1,7 +1,7 @@
# micro_servers
一套 DIY 的轻量级微服务架构(UniServe Framework),由 C++、Python 与 Go 混合实现。
`core` 作为进程托管服务负责拉起并守护各业务子进程,`waf` 作为前置 WAF 网关终止 TLS 并做规则匹配,`gateway` 作为应用层网关统一鉴权与反向代理,`db` 提供通用数据库访问与集中式日志收集,`ai_review` 提供 AI 内容审核,`static` 与 `dnfs` 作为业务服务对外提供能力。
一套 DIY 的轻量级微服务架构(UniServe Framework),由 C++、Python、Go 与仓颉(Cangjie) 混合实现。
`core` 作为进程托管服务负责拉起并守护各业务子进程,`waf` 作为前置 WAF 网关终止 TLS 并做规则匹配,`gateway` 作为应用层网关统一鉴权与反向代理,`db` 提供通用数据库访问与集中式日志收集,`confs` 提供统一配置分发,`ai_review` 提供 AI 内容审核,`static` 与 `dnfs` 作为业务服务对外提供能力。此外包含独立的机器人硬件控制模块 `bot4cj` 和嵌入式脚本引擎 `libtry`。
## 架构总览
@@ -18,25 +18,25 @@
│ waf │ │ gateway │ │ static │ │ dnfs │ │ db │ │ ai_review │
│ WAF 网关 │ │ 应用层网关 │ │前端托管│ │ 树状菜单 │ │ 通用数据库 │ │ AI内容审核 │
│ :443 (入口) │ │ :8443 (内部) │ │ :9200 │ │ :8006 │ │ :8005 │ │ :8007 │
│ coraza+反代 │ │ 鉴权 + 反代 │ │静态/媒体│ │ 节点CRUD │ │ SQL+日志收集 │ │ DeepSeek+RAG │
└──────┬───────┘ └──────┬───────┘ └────────┘ └──────┬───────┘ └──────────────┘ └──────────────┘
│ HTTPS终止 │ 反向代理 │ 调用
└──────────────────►│ ├──────────► db:8005
├────────────────► static:9200
└────────────────► dnfs:8006 ──► ai_review:8007
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox ──(log_client)──► db:8005/log/ingest
│ coraza+反代 │ │ 鉴权 + 反代 │ │静态/媒体│ │ 节点CRUD │ │ pymsg/TCP │ │ DeepSeek+RAG │
└──────┬───────┘ └──────┬───────┘ └───┬────┘ └──────┬───────┘ └──────▲───────┘ └──────▲───────┘
│ HTTPS终止 │ 反向代理 │ 代理笔记请求 │ pymsg │ pymsg │ HTTP
└──────────────────►│ ├─────────────►│─────────────────┘ │
├────────────►│ └─────── HTTP /review ──────────────┘
└────────────►│
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox / confs ──(log_client, pymsg)──► db:8005 (Logs 表)
配置链路: static 等业务服务 ──(pymsg get)──► confs:8010(配置统一存放于 confs/configs/,mtime 变化自动重载)
```
### 请求链路
1. 客户端所有请求先打到 `waf:443`(HTTPS),WAF 终止 TLS、运行 coraza 规则匹配,命中则直接返回 403。
2. 未命中规则的请求由 WAF 以明文 HTTP 转发到 `gateway:8443`,并设置 `X-Forwarded-For` / `X-Forwarded-Proto` 头。
3. `gateway` 对 `/login`、`/register` 做账号鉴权(SQLite + PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制)。
3. `gateway` 对 `/login`、`/register` 做账号鉴权(PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制;注册需邮箱验证码,经 pymsg/TCP 调 `smtp:8008` 发送与校验),用户数据通过 pymsg/TCP 调用 `db:8005` 存取。
4. 其余请求携带 Token,经 `gateway` 校验后反向代理到 `Proxy.opened` 中放开的内部服务(`static:9200` / `dnfs:8006`)。
5. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部服务间调用,不对外暴露。
6. `dnfs` 处理节点增删改查时,通过 HTTP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
7. 各服务通过 `log_client.py` SDK 异步批量推送运行日志到 `db:8005/log/ingest`,实现集中式日志收集(fail-safe,db 不可用时静默丢弃)。
6. `dnfs` 处理节点增删改查时,通过 pymsg/TCP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
7. 各服务通过 `log_client.py` SDK 异步批量推送运行日志(pymsg/TCP 写入 db 的 `Logs` 表),实现集中式日志收集(fail-safe,db 不可用时静默丢弃)。
## 各服务说明
@@ -47,15 +47,17 @@
| [gateway](gateway/) | C++20 | 8443 | 应用层网关(内部 HTTP),用户注册/登录鉴权、Token 签发与校验、反向代理到内部服务 |
| [static](static/) | Python(Flask) | 9200 | 前端托管服务,社区首页、静态页面与文件浏览、视频/音频流媒体播放、路径遍历防护 |
| [dnfs](dnfs/) | Python(Flask) | 8006 | 树状网络文件系统(Dendrachy Network File-System),基于父子 pid 的节点树增删改查,集成 AI 内容审核 |
| [db](db/) | Python(Flask) | 8005 | 通用数据库服务,基于 SQLAlchemy Core 的 SQL-over-HTTP,支持 select/insert/update/delete,参数化查询防注入;内置集中式日志收集(/log/* 路由) |
| [db](db/) | Python(pymsg) | 8005 | 通用数据库服务,基于 pymsg 协议(TCP 长度前缀帧 + JSON)的 SQL-over-TCP,SQLAlchemy Core 参数化查询防注入;内置集中式日志收集(Logs 表 + 自动清理) |
| [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 |
| [captcha](captcha/) | Python(Flask) | 8009 | 验证码服务,生成图形验证码(5 分钟过期、一次性),供 gateway 登录/注册可选启用 CAPTCHA 校验 |
| [smtp](smtp/) | Python(Flask) | 8008 | 邮件服务,发送/校验邮箱验证码(/send_email、/verify_email),注册流程的邮件验证码后端,SMTP 账号配置在 .env |
| [captcha](captcha/) | Python(pymsg) | 8009 | 验证码服务(pymsg/TCP),生成图形验证码(5 分钟过期、一次性),经 gateway 的 `GET /captcha/image` 代理给浏览器 |
| [smtp](smtp/) | Python(pymsg) | 8008 | 邮件服务,发送/校验邮箱验证码(pymsg 路由 send_email/verify_email/health),注册流程的邮件验证码后端,SMTP 账号配置在 .env |
| [confs](confs/) | Python(pymsg) | 8010 | 统一配置服务,集中存放并分发各服务配置文件(pymsg 路由 get/list/reload/health),内存缓存 + mtime 自动重载,路径遍历防护 |
| [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 |
| [bot4cj](bot4cj/) | 仓颉(Cangjie) | — | 机器人硬件脚本解析器,解析 .mt 矩阵脚本通过 I2C 驱动 PCA9685 舵机 / MPU6050 陀螺仪,面向树莓派 Dashun 蜘蛛机器人平台(独立硬件项目) |
### core(核心模块 · 进程托管服务)
- 可执行文件 `nidus.out`,作为 systemd 服务(`Nidus.service`)常驻运行。
- 可执行文件 `nidus.out`,作为 systemd 服务(`Nidus.service`)常驻运行(崩溃守护由 systemd 负责,core 自身不含自动重启逻辑)。
- 支持两种工作模式:命令行子系统调用(`--send/--exec/--list/--kill`)与 HTTP 伺服器模式(`--serve <port>`)。
- 通过命名管道(Pipe)与消息队列(Mq)实现进程间通信。
- 读取 `init.txt` 自启动列表,按行拉起各业务服务(ai_review / db / dnfs / smtp / static / waf / gateway / captcha / aibox)。
@@ -69,71 +71,83 @@
- 极客操作:`kill -HUP <pid>` 热加载规则;`waf -list` 列规则;`waf -test <file>` 校验语法;`waf -check "<input>"` 测试单条输入。
- 配置见 [waf/config.yaml](waf/config.yaml),详见 [waf/README.md](waf/README.md)。
### gateway(应用层网关 · v0.3.0-security)
### gateway(应用层网关 · v0.5.3)
- 内部 HTTP 服务,默认监听 127.0.0.1:8443,不再承载 TLS(TLS 已由 WAF 终止)。
- 通过 `Server.tls = on/off` 配置切换 HTTPS/HTTP 模式([example.ini](gateway/example.ini)),独立部署时仍可启用 TLS。
- **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),登录时自动迁移旧 SHA256 哈希,常数时间比较防 timing attack。
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行。
- **CAPTCHA**:`/captcha` 接口生成验证码(5 分钟过期、一次性使用),登录与注册均可选启用。
- **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),常数时间比较防 timing attack。
- **用户存储**:用户数据通过 pymsg/TCP 调用 `db:8005`(`DbC::call_msg`)存取,不再直连本地 SQLite;登录成功即更新 `Users.last_login`(`"$now"` 哨兵)。
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行;RSA 密钥对启动时内存生成,重启后旧 Token 全部失效。
- **Cookie 认证**:登录成功同时签发 `HttpOnly + Secure + SameSite=Lax` 的 `q4_token` Cookie;代理时无 `Authorization` 头则回退读 Cookie 验签,并向下游注入 `Authorization: Bearer`(下游零改动);`POST /logout` 清除 Cookie。公开目标(`Proxy.default`)不做凭证校验,避免旧 Cookie 影响公开页面。
- **CAPTCHA**:`/captcha` 接口生成验证码(5 分钟过期、一次性使用),登录与注册均可选启用;`GET /captcha/image` 为 captcha 服务(pymsg/TCP :8009)的图形验证码代理路由。
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟,按 `X-Forwarded-For`(WAF 注入)客户端 IP 计数。
- **反向代理**:`Target-Url` 必须在 `Proxy.opened` 白名单中(`closed` 仅记录用途,实际隔离靠"不在 opened 中即拒绝"),缺省走 `Proxy.default`(不需 Token,服务前端静态资源)。
- **自动建表**:启动时若 `Users` 表不存在则自动创建,首位注册者即为管理员。
- **用户表**:`Users` 表由 [db/create_sqlite3.sql](db/create_sqlite3.sql) 建立(gateway 不再自动建表),无默认管理员账号,首位注册者即为管理员。
- 详见 [gateway/README.md](gateway/README.md)。
### static(前端托管服务)
- 托管 `public/` 下的前端页面:社区首页(`index.html`)、登录/注册、树状笔记(`dnfs.html`)、文件浏览(`__view.html`)等。
- 支持文件浏览、视频/音频在线播放(HTTP Range 分段请求)、一键下载。
- 路径遍历防护,仅允许访问 `directory.root` 之内。
- 支持文件浏览、视频/音频在线播放(HTTP Range 分段请求,返回 206/416)、一键下载。
- 路径遍历防护(`safe_path_join`:realpath 前缀校验),仅允许访问 `directory.root` 之内。
- **dnfs 代理**:`/get_notes`、`/add_note`、`/delete_note`、`/update_note` 透传 Authorization 转发到 `dnfs:8006`(解决浏览器跨域)。
- **账号旁路**:`/api/check_user`、`/api/reset_password` 等接口直接读写 `gateway/sqlite3.db` 的 Users 表;`/yhdet_callback`、`/api/yhdet_*` 对接 yhdet.top OAuth 登录。
- 统一品牌"树状菜单",支持深色模式(`css/theme.css`)。
- 可使用 Nuitka 编译为独立二进制(`fsys.bin`)。
- 配置见 [static/config.ini](static/config.ini)。
- 配置由 `confs` 统一配置服务分发(`confs/configs/static.ini`,启动时经 pymsg 拉取,`CONFS_HOST`/`CONFS_PORT` 环境变量可覆盖);本地 [static/config.ini](static/config.ini) 作为 confs 不可用时的 fail-safe 回退。
### dnfs(树状菜单服务)
- 基于 `pid`(父节点 id)组织的树状节点,节点含 `title` 与 `content`。
- 提供 `/get_notes`、`/add_note`、`/delete_note`、`/update_note` 四个接口。
- 数据持久化通过 HTTP 调用 `db` 服务(`http://127.0.0.1:8005/execute`),不再直接操作数据库。
- 数据持久化通过 pymsg/TCP 调用 `db` 服务(`127.0.0.1:8005`,execute 路由),不再直接操作数据库。
- 新增节点前调用 `ai_review` 服务(`http://127.0.0.1:8007/review`)进行内容审核,审核不通过则拒绝写入。
### db(通用数据库服务)
- 单一接口 `POST /execute`,接收结构化 JSON(action + table + where + values),翻译为 SQL 执行。
- 基于自研 [pymsg](pymsg/) 协议(TCP 长度前缀帧 + JSON),单一路由 `_route: "execute"`,接收结构化 JSON(action + table + where + values),翻译为 SQL 执行。
- 支持 select / insert / update / delete,运算符包括 eq、ne、gt、lt、ge、le、like、in,支持排序与分页。
- 基于 SQLAlchemy Core 表反射,参数化查询,零 SQL 注入风险。
- 仅监听 127.0.0.1,单线程,自动回滚,不暴露 DDL 操作。
- 仅监听 127.0.0.1,自动提交/回滚,不暴露 DDL 操作。
- 默认使用 SQLite(`database.db`),可通过 `DATABASE_URL` 环境变量切换至 PostgreSQL / MySQL。
- **内置日志子模块**([log_module.py](db/log_module.py)):
- `log_internal()` — db 进程内部直接写 Logs 表,无 HTTP 往返,防递归设计。
- `/log/ingest` — 接收外部服务批量推送日志(单次上限 100 条,校验 service / level / message)。
- `/log/query` — 结构化查询(service / level / trace_id / 时间区间 / 关键字模糊搜索 / 排序分页)。
- `/log/stats` — 按 service / level 分组统计。
- `/log/purge` — 手动清理指定时间之前的日志。
- `/log/health` — 健康检查。
- **日期/时间列适配**:values 与 where 中的 DATETIME/DATE/TIME 列接受 ISO 8601 字符串(带时区偏移自动转 UTC)或哨兵值 `"$now"`(服务端当前 UTC);响应中的日期时间一律序列化为 ISO 8601(T 分隔)。
- **内置日志子模块**:
- 启动时自动建 `Logs` 表(service / level / message / trace_id / extra + 4 个索引)。
- 外部服务通过 `log_client.py` 以 pymsg 调 execute 路由逐条 INSERT 写入日志。
- `_log_internal()` — db 进程内部直接写 Logs 表,无网络往返,防递归设计。
- 后台守护线程每小时自动清理:保留 30 天 / 上限 100 万条。
- **统一日志客户端 SDK**([log_client.py](db/log_client.py)):各服务复制即用,纯标准库实现(urllib),异步非阻塞(内存队列 + 后台线程,50 条/2 秒批量 flush),fail-safe 设计(db 不可用时静默丢弃,不影响业务)。
- **统一日志客户端 SDK**(各服务目录下的 `log_client.py`,如 [dnfs/log_client.py](dnfs/log_client.py)):各服务复制即用,纯标准库实现,异步非阻塞(内存队列 + 后台线程,50 条/2 秒批量 flush),fail-safe 设计(db 不可用时静默丢弃,不影响业务)。
### ai_review(AI 内容审核服务)
- 基于 DeepSeek LLM + ChromaDB 向量检索的 RAG 审核架构。
- 审核类别:色情、违禁药品、赌博、诈骗、仇恨/暴力/恐怖主义,灰色地带内容从严判定。
- 接口:`POST /review`(提交审核)、`POST /rag/feedback`(人工纠偏)、`GET /rag/list`(知识库列表)、`GET /health`(健康检查)。
- RAG 检索 top-3 相似已标注样本注入 prompt,MD5 去重,API 异常时 fail-open。
- RAG 检索 top-3 相似已标注样本注入 prompt,MD5 去重。
- **fail-closed 设计**:API 异常或响应解析失败时一律返回 `{"invalid": "true"}`(拦截),宁可误拦不可漏放。
- 配置通过 `.env` 文件(已 gitignore),含 DeepSeek API 密钥、模型参数、监听地址。
### captcha(验证码服务)
- 生成图形验证码,Token 一次性使用、5 分钟过期。
- 接口:`GET /captcha`(生成)、`POST /verify`(校验)、`POST /new`(刷新);答案错误返回 `200 {valid:false}`(Token 照常消费),仅参数 / Token 错误返回 `400`。
- 生成图形验证码,Token 一次性使用、5 分钟过期;基于 [pymsg](pymsg/) 协议(TCP 长度前缀帧 + JSON),仅依赖 PIL。
- pymsg 路由:`image`(生成,返回 token + base64 PNG)、`verify`(校验,一次性消费)、`health`。
- 答案比较采用 XOR 防时序攻击;错误答案与无效 Token 分别返回 `{valid:false}` 与 `{valid:false, error:...}`。
- **gateway 代理路由**:`GET /captcha/image` 经 pymsg 调 captcha 后解码 PNG,以 `image/png` + `X-Captcha-Token` 响应头返回浏览器(`Cache-Control: no-store`)。
- 由 `gateway` 在 `/login`、`/register` 中可选启用 CAPTCHA 校验(字段 `captcha_token` / `captcha_answer`)。
- 内存存储,不支持多实例水平扩展(需换 Redis);详见 [captcha/README.md](captcha/README.md)。
### smtp(邮件服务)
- 注册流程的邮箱验证码后端,基于 Flask + waitress,监听 127.0.0.1:8008。
- 接口:`POST /send_email`(生成 6 位验证码并经 SMTP_SSL 发送至收件人,60 秒有效)、`POST /verify_email`(校验)。
- 验证码存于进程内存,SMTP 账号通过 `.env` 配置(`SMTP_SERVER` / `SMTP_PORT` / `SMTP_USER` / `__PASSWORD`)。
- 注册流程的邮箱验证码后端,基于 pymsg(MsgServer,TCP 长度前缀帧 + JSON),监听 127.0.0.1:8008。
- pymsg 路由:`send_email`(生成 6 位验证码并经 SMTP_SSL 发送,10 分钟有效,发送失败自动作废;同一邮箱 60 秒发送冷却,冷却期拒绝并返回 `retry_after`,不覆盖已发出的有效验证码,发送失败不计入冷却)、`verify_email`(校验,通过即一次性消费防重放,连续失败 5 次自动作废并返回 `invalidated`,兼容旧字段 `input_captcha`)、`health`。
- 验证码存于进程内存(锁保护 + 惰性过期清理),SMTP 账号通过 `.env` 配置(`SMTP_SERVER` / `SMTP_PORT` / `SMTP_USER` / `__PASSWORD`,`__PASSWORD` 兼容别名 `SMTP_PASSWORD`)。
### confs(统一配置服务)
- 集中存放各微服务的配置文件(`confs/configs/`),基于 pymsg(RpcServer,TCP 长度前缀帧 + JSON)分发,监听 127.0.0.1:8010。
- pymsg 路由:`get`(取配置文件全文;带 `key` 时按键提取,ini 用 `Section.option`、json 用点号路径)、`list`(列出全部配置及元信息)、`reload`(重载指定配置,缺省清空全部缓存)、`health`。
- 内存缓存 + 文件 mtime 变化自动重载,改配置无需重启 confs;配置名校验(basename + realpath 前缀 + 扩展名白名单)防路径遍历。
- 消费方约定:服务启动时用 `RpcClient(8010).call("get", {"name": "<服务名>.ini"})` 拉取配置,confs 不可用时回退本地配置文件(fail-safe)。目前 `static` 已接入。
### aibox(独立 AI 智能体系统)
@@ -141,21 +155,31 @@
- 自带技能系统(`skills/`)、任务调度器、记忆管理、前端页面与语音客户端(`voice_client.py`)。
- 默认模型 `deepseek-v4-flash`,配置在 `config.json`;详见 [aibox/README.md](aibox/README.md)。
### bot4cj(机器人硬件脚本解析器)
- 基于仓颉(Cangjie)编程语言和 Linux I2C 接口的机器人动作脚本解析器,面向树莓派 Dashun 蜘蛛机器人平台(10 舵机 + 2 云台)。
- 解析 `.mt` 矩阵脚本(行列式表格语法,支持 `@Servo` / `@Action` 等块),编译为舵机角度序列执行。
- 硬件层通过 I2C 驱动 PCA9685(PWM 舵机控制,50Hz,脉宽 0.5–2.5ms)、MPU6050(陀螺仪/加速度计,10ms 采样 + 卡尔曼滤波)、Speaker(正弦波发声)。
- 附带数学库(矩阵运算、NN/CNN、Adam 优化器)、CUDA GPU 加速 kernel 和 Lua 硬件模块(ADS1115 / ADS1256 / SPI)。
- 与主干微服务完全独立,需部署在树莓派上独立运行。
## 共享基础库
| 库 | 说明 |
|----|------|
| [libth](libth/) | 线程库:`Thread`、`Cv_wait`(条件变量等待)、`Fsm`、`ITask` 任务接口 |
| [libhttp](libhttp/) | HTTP/WebSocket 库:`HttpServer`、`HttpClient`、`WebSocketServer/Client`(基于 Boost.Beast) |
| [libhttp](libhttp/) | HTTP/WebSocket 库:`HttpServer`(含 WebSocket 服务/代理)、`HttpClient`(含 WebSocket 客户端,基于 Boost.Beast) |
| [libcrypt](libcrypt/) | 加密库:`AES_CBC`、`MyRSA`、`SHA256`、`Base64`、`Crypto_Basic` |
| [libsqlite3](libsqlite3/) | SQLite3 C++ 封装(Header-only) |
| [libtry](libtry/) | 嵌入式脚本解释器(~741 行 C),手写递归下降词法/语法/执行引擎,支持变量/数组/函数/控制流,宽字符(中文标识符),用于嵌入式操作界面 shell |
### Python 共享组件
| 文件 | 说明 |
|------|------|
| [log_client.py](db/log_client.py) | 统一日志客户端 SDK,各服务复制即用(纯标准库,异步批量推送) |
| [security_middleware.py](db/security_middleware.py) | Flask 安全响应头中间件(CSP / X-Frame-Options / nosniff 等),各服务各持一份副本 |
| [pymsg](pymsg/) | 自研 RPC 基础库(setuptools 包),协议为 `[4字节大端长度][UTF-8 JSON]`,提供 `send/recv/MsgClient/MsgServer`;db 服务端与各 log_client 的基础依赖,requirements 以 `../pymsg` 本地路径引用 |
| log_client.py | 统一日志客户端 SDK,各服务目录各持一份副本(如 [dnfs/log_client.py](dnfs/log_client.py)),纯标准库 + pymsg,异步批量推送 |
| security_middleware.py | Flask 安全响应头中间件(CSP / X-Frame-Options / nosniff 等),各服务各持一份副本 |
## 端口一览
@@ -166,11 +190,27 @@
| 5200 | core | — | PMC HTTP 管理接口 |
| 9200 | static | 127.0.0.1 | 前端托管(经 gateway 代理) |
| 8006 | dnfs | 127.0.0.1 | 树状菜单(经 gateway 代理) |
| 8005 | db | 127.0.0.1 | 数据库服务(仅内部调用) |
| 8005 | db | 127.0.0.1 | 数据库服务(pymsg/TCP,仅内部调用) |
| 8007 | ai_review | 127.0.0.1 | AI 审核(仅内部调用) |
| 8009 | captcha | 127.0.0.1 | 验证码服务(独立组件) |
| 8009 | captcha | 127.0.0.1 | 验证码服务(pymsg/TCP,经 gateway 代理) |
| 8008 | smtp | 127.0.0.1 | 邮件服务(仅内部调用,注册验证码邮件后端) |
| 8010 | confs | 127.0.0.1 | 统一配置服务(pymsg/TCP,仅内部调用) |
| 8765 | aibox | 127.0.0.1 | AI 智能体(WebSocket,独立产品) |
| — | bot4cj | — (树莓派 I2C) | 机器人硬件脚本解析器(独立硬件项目,不联网) |
## 测试
[tests/](tests/) 包含一套黑盒测试套件,基于子进程调用与 HTTP 请求验证各微服务功能正确性:
| 测试文件 | 被测模块 | 说明 |
|----------|----------|------|
| `test_db_*.py` | db | SQL-over-TCP 的 select/insert/update/delete、日期哨兵、边界条件 |
| `test_gateway_*.py` | gateway | 用户注册/登录/Token 校验、速率限制、CAPTCHA 集成 |
| `test_waf_*.py` | waf | coraza 规则匹配与拦截、白名单放行 |
| `test_dnfs_*.py` | dnfs | 节点树 CRUD、AI 审核集成 |
- 默认使用 `config.py` 中的生产地址(127.0.0.1),每个测试函数前后自动备份/恢复 db 数据,保证不污染线上数据。
- 测试依赖 `pymsg`、`Flask`、`flask-cors`、`requests`、`Pillow`,详见 [tests/requirements-test.txt](tests/requirements-test.txt)。
## 依赖
@@ -188,7 +228,7 @@ bash _setup.deb.sh # Debian/Ubuntu
# 或
bash _setup.rhel.sh # RHEL/CentOS/Rocky/Fedora
# 2. 依次安装各模块(libth -> libhttp -> libcrypt -> libsqlite3 -> gateway -> waf -> static -> dnfs -> db -> core)
# 2. 依次安装各模块(libth -> libhttp -> libcrypt -> libsqlite3 -> gateway -> waf -> static -> dnfs -> db -> smtp -> captcha -> confs -> core)
bash _inst.sh
```
@@ -201,7 +241,7 @@ bash _run.sh # 运行
部署路径约定为 `/opt/micro_servers/`,`core` 的 `init.txt` 即从该路径拉起其余服务。
> **注意**:`ai_review` 未包含在 `_inst.sh` 中,需单独进入 `ai_review/` 目录执行 `_set.sh` 安装,并配置 `.env` 文件(DeepSeek API 密钥等)。
> **注意**:`ai_review` 未包含在 `_inst.sh` 中,需进入 `ai_review/` 目录执行 `_set.sh` 安装,并额外配置 `.env` 文件(DeepSeek API 密钥等)。
> **注意**:waf 首次编译需联网下载 Go 依赖,[waf/_set.sh](waf/_set.sh) 会自动调用 [waf/_goproxy.sh](waf/_goproxy.sh) 设置 GOPROXY 国内镜像。如需手动设置:`go env -w GOPROXY=https://goproxy.cn,direct`。
@@ -210,8 +250,9 @@ bash _run.sh # 运行
由 `core` 守护的子进程通过 `init.txt` 自启动(顺序很重要,waf 必须先于 gateway):
```
bash /opt/micro_servers/ai_review/_run.sh
bash /opt/micro_servers/db/_run.sh
bash /opt/micro_servers/confs/_run.sh
bash /opt/micro_servers/ai_review/_run.sh
bash /opt/micro_servers/dnfs/_run.sh
bash /opt/micro_servers/smtp/_run.sh
bash /opt/micro_servers/static/_run.sh
@@ -249,14 +290,30 @@ gateway 改为内部 HTTP 模式([example.ini](gateway/example.ini) 中 `tls =
- **服务按业务能力拆分**:WAF 网关、应用网关、前端托管、节点树、数据库、内容审核各自独立、独立端口、独立数据存储。
- **API 网关模式**:`waf` 作为边缘网关统一入口,`gateway` 承担鉴权与反向代理,内部服务通过 `closed` 列表隔离。
- **进程编排/守护**:`core` 作为服务管理器拉起并守护子进程,类似简易版 supervisor。
- **多语言异构**:C++(core / gateway)、Python(static / dnfs / db / ai_review)、Go(waf)混合,通过 HTTP 通信。
- **数据访问层解耦**:`db` 服务将数据库操作收敛为统一的 HTTP 接口,业务服务无需直连数据库。
- **多语言异构**:C++(core / gateway)、Python(static / dnfs / db / ai_review)、Go(waf)、仓颉/Cangjie(bot4cj)混合,对外 HTTP、内部数据访问走 pymsg/TCP。
- **数据访问层解耦**:`db` 服务将数据库操作收敛为统一的 pymsg/TCP 接口,业务服务无需直连数据库。
- **AI 能力集成**:`ai_review` 以 RAG 增强的 LLM 审核为业务层提供内容安全能力,支持人工反馈闭环。
- **集中式可观测性**:`db` 内置日志收集服务(/log/* 路由),各服务通过 `log_client.py` SDK 异步推送日志,实现全链路汇聚、查询与统计,无需引入额外中间件。
- **集中式可观测性**:`db` 内置日志表与自动清理,各服务通过 `log_client.py` SDK 异步推送日志(pymsg),实现全链路汇聚,无需引入额外中间件。
- **安全分层**:`waf` 在边缘做规则匹配(OWASP CRS / 自定义 SecLang 规则),`gateway` 在应用层做鉴权,`db` 仅监听本机,多层防护。
与云原生微服务的差异:未引入服务注册发现、容器编排、消息总线,服务地址在 `waf/config.yaml` 与 `gateway/example.ini` 中静态配置。对于个人/中小规模的 DIY 场景,这种取舍在简洁性与可控性上是合理的。
## 文档管理约定
所有项目文档统一通过根目录 [docs/](docs/) 聚合管理,入口索引见 [docs/INDEX.md](docs/INDEX.md)。文档分类与存放规则如下:
| 文档类型 | 存放位置 | 说明 |
|----------|----------|------|
| **模块 README** | 各模块根目录(如 `waf/README.md`) | 遵循行业惯例,方便 GitHub/IDE 直接识别,不移动 |
| **安全审计报告** | `docs/security/` | 各模块的安全审计、部署审计、依赖审计、加固计划统一存放,文件命名为 `<模块名>_<文档类型>_<日期>.md` |
| **代码审查日志** | `docs/reviews/` | 代码审查、评审记录统一存放 |
| **分析报告** | `docs/reports/` | 各类分析报告(HTML/MD 等)统一存放 |
| **全局文档** | `docs/` 根目录 | 全局审计总览、跨模块文档直接放在 `docs/` 下 |
| **aibox 技能提示词** | `aibox/skills/`、`aibox/tasks/` | 属于运行时数据,保留在原目录 |
| **虚拟环境/第三方文档** | `.venv/` 等 | 不纳入文档管理,由 `.gitignore` 排除 |
新增模块文档时遵循以上规则:模块说明写本目录 `README.md`,审计/审查/报告类文档放入 `docs/` 对应子目录。
## 许可证
各模块均采用 MIT License,详见各目录下的 `LICENSE`。
+3
View File
@@ -28,6 +28,7 @@ inst_module() {
inst_module "libth"
inst_module "libhttp"
inst_module "libcrypt"
inst_module "libmsg"
inst_module "libsqlite3"
inst_module "gateway"
inst_module "waf" # waf 在 gateway 之前编译,部署时由 core 先拉起 waf 再拉起 gateway
@@ -35,4 +36,6 @@ inst_module "static"
inst_module "dnfs"
inst_module "db"
inst_module "smtp"
inst_module "captcha"
inst_module "confs"
inst_module "core" # core last
+18
View File
@@ -0,0 +1,18 @@
.env
.env/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
.git
.gitignore
*.md
!README.md
.envrc
.venv/
venv/
ENV/
chroma/
.DS_Store
+43
View File
@@ -0,0 +1,43 @@
FROM python:3.11-slim
LABEL maintainer="qingsong"
LABEL service="ai_review"
LABEL description="AI内容审核服务 - DeepSeek + RAG"
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# 安装依赖(ai_review不依赖pymsg)
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 复制应用代码
COPY . /app/
# 创建ChromaDB持久化目录
RUN mkdir -p /app/chroma
VOLUME ["/app/chroma"]
# 创建非root用户
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8007
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8007/health', timeout=5)" || exit 1
# 默认环境变量
ENV AI_REVIEW_HOST=0.0.0.0 \
AI_REVIEW_PORT=8007 \
CHROMA_PERSIST_DIR=/app/chroma
CMD ["python", "ai_review.py"]
+64
View File
@@ -14,6 +14,9 @@ from dotenv import load_dotenv
import hashlib
import os
import sys
import threading
import time
from collections import defaultdict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env"))
@@ -27,6 +30,53 @@ CORS(app)
review_agent = ReviewAgent()
# ── 内容大小限制(10KB,审核文本不应超过此值)──
MAX_CONTENT_LENGTH = 10 * 1024
# ── 简易滑动窗口限流器 ──────────────────────────────────────────────
# /review: 30次/分钟/IP(正常审核请求)
# /rag/feedback: 5次/分钟/IP(人工纠偏低频操作)
_RATE_LIMIT_WINDOW = 60 # 窗口秒数
_RATE_LIMIT_REVIEW = 30 # /review 每分钟上限
_RATE_LIMIT_FEEDBACK = 5 # /rag/feedback 每分钟上限
_RATE_LIMIT_LOCK = threading.Lock()
_RATE_LIMIT_BUCKETS: "dict[str, list[float]]" = defaultdict(list)
def _cleanup_buckets():
"""惰性清理过期时间戳,在线程锁内完成。"""
now = time.monotonic()
cutoff = now - _RATE_LIMIT_WINDOW
stale_keys = []
for key, timestamps in _RATE_LIMIT_BUCKETS.items():
timestamps[:] = [t for t in timestamps if t > cutoff]
if not timestamps:
stale_keys.append(key)
for key in stale_keys:
del _RATE_LIMIT_BUCKETS[key]
def _check_rate_limit(key: str, limit: int) -> bool:
"""检查是否超出限流,返回 True 表示放行,False 表示拒绝。"""
now = time.monotonic()
cutoff = now - _RATE_LIMIT_WINDOW
with _RATE_LIMIT_LOCK:
bucket = _RATE_LIMIT_BUCKETS[key]
bucket[:] = [t for t in bucket if t > cutoff]
if len(bucket) >= limit:
return False
bucket.append(now)
return True
def _client_ip() -> str:
"""获取客户端真实 IP(优先 X-Forwarded-For)。"""
xff = request.headers.get("X-Forwarded-For", "")
if xff:
return xff.split(",")[0].strip()
return request.remote_addr or "127.0.0.1"
@app.route('/review', methods=['POST'])
def review():
@@ -35,6 +85,11 @@ def review():
Request: {"content": "待审核的字符串"}
Response: {"invalid": "true"} 或 {"invalid": "false"}
"""
# 频率限制
ip = _client_ip()
if not _check_rate_limit(f"review:{ip}", _RATE_LIMIT_REVIEW):
return jsonify({"error": "rate limit exceeded", "retry_after": _RATE_LIMIT_WINDOW}), 429
data = request.get_json()
if not data or 'content' not in data:
@@ -45,6 +100,10 @@ def review():
if not content:
return jsonify({"error": "content cannot be empty"}), 400
# 内容大小限制
if len(content) > MAX_CONTENT_LENGTH:
return jsonify({"error": f"content too large, max {MAX_CONTENT_LENGTH} bytes"}), 413
result = review_agent.review(content)
return jsonify(result), 200
@@ -61,6 +120,11 @@ def rag_feedback():
Response: {"added": true, "id": "v_xxx", "md5": "xxx"}
{"added": false, "reason": "duplicate"}
"""
# 频率限制(人工纠偏低频操作,严格限制)
ip = _client_ip()
if not _check_rate_limit(f"feedback:{ip}", _RATE_LIMIT_FEEDBACK):
return jsonify({"error": "rate limit exceeded", "retry_after": _RATE_LIMIT_WINDOW}), 429
data = request.get_json()
if not data or 'content' not in data:
+3 -3
View File
@@ -12,7 +12,7 @@ def apply_security_headers(app, csp_policy=None):
@app.after_request
def set_security_headers(response):
# Prevent clickjacking
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
# XSS protection (legacy browsers)
response.headers['X-XSS-Protection'] = '1; mode=block'
# Prevent MIME type sniffing
@@ -30,10 +30,10 @@ def apply_security_headers(app, csp_policy=None):
"default-src 'self'; "
"script-src 'self' 'unsafe-inline'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
"img-src 'self' data: blob:; "
"font-src 'self'; "
"connect-src 'self'; "
"frame-ancestors 'none'"
"frame-ancestors 'self'"
)
# Remove server header info
+238 -49
View File
@@ -15,10 +15,14 @@
- 🔧 **扩展技能**:动态加载 `~/.aibox/skills/` 下的可执行脚本或 Markdown 技能文件
- 🌍 **多语言支持**:自动检测系统语言,支持中英文
- 🧬 **智能体自我进化**:通过 `update_soul` / `update_identity` 工具动态更新角色定位与用户画像
- 🤖 **子代理**:通过 `create_subagent` 工具派生出拥有独立上下文/角色设定的轻量子代理,主代理可分解长任务、隔离上下文,子代理可选用不同模型
- 🔄 **技能热重载**:无需重启即可添加、修改或删除扩展技能
- 🔐 **SOCKS5 代理**:通过 `set_proxy` / `get_proxy` 工具为网页访问与 API 调用配置代理,可持久化
- 💾 **对话管理**:历史对话归档、开始新对话(重置 AI 记忆并重新加载提示词)
- 📦 **对话压缩**:消息数或 Token 数达到阈值时自动压缩,保持上下文质量
- 🛡️ **内存安全防护**:文件读取按需加载不全载、命令输出 1MB 限制、单行超长截断、工具结果多层防护防止 OOM
- 🔒 **错误自动恢复**:API 请求失败自动回滚消息,工具结果安全清洗(控制字符过滤、非字符串转换)
- 🛑 **可靠中断机制**:支持 Ctrl+C 实时中断,通过 `threading.Event` + HTTP 连接关闭 + 子进程 kill 三层取消,流式响应和长时间运行命令均可立即终止
## 🚀 快速开始
@@ -32,7 +36,7 @@
```bash
# 克隆或下载代码后,安装依赖
pip install websockets requests beautifulsoup4
pip install -r requirements.txt
```
### 配置
@@ -46,18 +50,18 @@ export DEEPSEEK_API_KEY="your-api-key"
3. **启动服务器**:
```bash
python main.py
python aibox.py
```
启动后,WebSocket 服务器将运行在 `ws://localhost:8765`(地址与端口可在 `config.json` 的 `system.websocket_host` / `system.websocket_port` 中修改)。
> 注意:本模块入口文件为 `main.py`(不是 `agent.py`)。`client.py` 为示例 WebSocket 客户端,`voice_client.py` 为语音客户端,`_run.sh` 为被 `core` 拉起的启动脚本。
> 注意:本模块入口文件为 `aibox.py`(单文件约 8450 行,集成 WebSocket 服务器、AI 引擎、工具/技能系统、任务调度器于一体)。`client.py` 为示例 WebSocket 客户端,`voice_client.py` 为语音客户端,`_run.sh` 为被 `core` 拉起的启动脚本。
## 📁 目录结构
```
aibox/
├── main.py # 服务主程序(WebSocket 服务器 + AI 引擎 + 工具/技能系统)
├── aibox.py # 服务主程序(WebSocket 服务器 + AI 引擎 + 工具/技能系统 + 任务调度器)
├── client.py # 示例 WebSocket 客户端
├── voice_client.py # 语音客户端
├── entry.html # 前端入口页
@@ -85,12 +89,22 @@ aibox/
```
~/.aibox/
├── config.json # 配置文件
├── prompts/ # 系统提示词目录
│ ├── 00_IDENTITY.md # 用户身份信息
│ ├── 1_*.md # 基础技能提示词(文件操作、网页访问等)
├── prompts/ # 系统提示词目录(按文件名数字前缀排序加载)
│ ├── 00_IDENTITY.md # 用户身份信息(最先加载)
│ ├── 1_memory_search.md # 基础技能提示词(1-10,按数字升序加载)
│ ├── 2_memory_save.md
│ ├── 3_file_operations.md
│ ├── 4_web_access.md
│ ├── 5_memo_management.md
│ ├── 6_command_execution.md
│ ├── 7_extension_skills.md
│ ├── 8_identity_management.md
│ ├── 9_language_instruction.md
│ ├── 10_task_handling.md
│ ├── 99_SOUL.md # AI 灵魂定义(角色、性格、能力边界)
│ └── 100_STATUS.md # AI 状态记录
│ └── 100_STATUS.md # AI 状态记录(最后加载)
├── skills/ # 扩展技能目录(运行时从仓库 skills/ 复制或用户自建)
├── workspace/ # AI 专用工作目录(临时文件、脚本、数据文件默认存放位置)
├── memories.db # 记忆和任务数据库(SQLite)
├── current_conversation.json # 当前对话
├── conversation_history.json # 对话索引
@@ -98,16 +112,26 @@ aibox/
└── tool_results/ # 工具结果缓存
```
## 📂 工作目录规范
为避免 AI 产生的临时文件污染系统其他目录,所有 AI 主动创建的临时文件、脚本、数据文件默认写入 `~/.aibox/workspace/` 目录:
- **文件操作**:`write_file` 工具默认路径为 `~/.aibox/workspace/`(用户明确指定路径时除外)
- **命令执行**:`run_command` 工具默认 `work_dir` 为 `~/.aibox/workspace/`,命令产生的文件落在该专用工作区
- **用户指定路径优先**:当用户明确要求在特定路径操作时,按用户指定路径执行
- **读取不受限**:读取系统文件、项目文件时使用实际路径,不受工作目录限制
- **技能调用**:`~/.aibox/skills/` 下的技能执行不受此限制
## 🔧 可用工具
AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生,在 `DeepSeekChat` 中注册):
| 工具名称 | 功能描述 |
|---------|---------|
| `read_file` | 读取文件内容(支持分页、head/tail) |
| `write_file` | 写入或追加文件 |
| `list_files` | 列出目录内容 |
| `web` | 访问网页、获取标题/文本/链接 |
| `read_file` | 读取文件内容(返回 markdown 指引,改用 `head`/`tail`/`sed`/`wc` 等命令行读取) |
| `write_file` | 写入或追加文件(返回 markdown 指引,改用 `mkdir -p` + `cat` 重定向写入) |
| `list_files` | 列出目录内容(返回 markdown 指引,改用 `ls`/`du`/`find` 命令行列出) |
| `web` | 访问网页、获取标题/文本/链接(返回 markdown 指引,改用 `curl` 命令行访问) |
| `memory` | 保存、搜索(支持分页)、删除记忆 |
| `memo` | 创建、查看、完成、搜索任务 |
| `run_command` | 执行系统命令(实时输出,支持超时控制) |
@@ -115,6 +139,7 @@ AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生,
| `reload_skills` | 热重载扩展技能 |
| `create_skill` | 创建新技能(AI 自主编写 Python 脚本) |
| `install_skill` | 技能安装指引 |
| `ask_user` | 向用户提问(AI 主动交互) |
| `update_identity` | 更新用户身份信息(`00_IDENTITY.md`) |
| `update_soul` | 更新 AI 自我定义(`99_SOUL.md`,智能体进化) |
| `save_memory_and_end_conversation` | 保存记忆并结束对话 |
@@ -122,6 +147,8 @@ AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生,
| `calculator` | 数学计算 |
| `set_proxy` | 设置 SOCKS5 代理(host/port/可选账号密码,可持久化,即时生效) |
| `get_proxy` | 查看当前 SOCKS5 代理配置 |
| `express_emotion` | 表达情绪并记录当前情绪状态(anger/joy/sadness/surprise),可指定 `persona` 从 `~/.aibox/skills/` 加载人格技能(如 angry/coquettish/tsundere)派生携带该人格的情绪子代理,保证人格独立、不污染主对话上下文 |
| `create_subagent` | 派生子代理:独立上下文/角色设定,轻量无工具、可选独立模型,用于长任务分解、上下文隔离、独立角色专注处理 |
## 💬 WebSocket API
@@ -169,9 +196,16 @@ const ws = new WebSocket('ws://localhost:8765');
}
```
### 支持的命令
#### 5. 中断(Ctrl+C)
```json
{
"type": "interrupt",
"content": "用户按下了 Ctrl+C 中断当前操作"
}
```
服务端收到后立即回复 `interrupt_ack`,并通过三层取消机制(Event标志 + HTTP连接关闭 + 子进程kill)终止正在进行的AI思考和工具执行。
> 注意:记忆搜索通过 `memory` 工具(在对话中由 AI 调用)完成,本系统**不存在** `/search` 斜杠命令。
### 支持的命令
| 命令 | 功能 |
|-----|------|
@@ -179,6 +213,7 @@ const ws = new WebSocket('ws://localhost:8765');
| `/new` | 开始新对话(保存当前,重置 AI 记忆,重新加载提示词) |
| `/list` | 列出所有历史对话 |
| `/memories` | 查看最近记忆(支持分页) |
| `/search <关键词>` | 按关键词搜索记忆 |
| `/memos` | 查看待办任务 |
| `/config` | 查看配置 |
| `/reload` | 重载配置 |
@@ -191,15 +226,25 @@ const ws = new WebSocket('ws://localhost:8765');
| 类型 | 说明 |
|-----|------|
| `chunk` | AI 响应的片段(流式输出) |
| `complete` | AI 响应完成 |
| `reasoning` | AI 思考过程(思考模式) |
| `error` | 错误信息 |
| `chunk` | AI 响应的**增量片段**(流式输出,chunk_buffer 发送后即清零,为内容权威来源) |
| `reasoning_start` | AI 开始思考信号(思考模式,创建思考区域展示) |
| `reasoning` | AI 思考过程增量片段(流式输出,灰色/斜体样式) |
| `reasoning_end` | AI 思考结束信号(切换到正式回答) |
| `complete` | AI 响应**收尾信号**(chunk 流式是内容权威来源,complete 仅用于收尾;完全丢帧时可用 full_text 补建) |
| `line` | 过程行(🔧 工具调用提示、工具输出、💬 助手前缀等,供任务侧栏/终端显示) |
| `error` | 错误信息(API 请求失败、解析失败等;服务端自动回滚本轮消息防止对话历史被污染) |
| `command_result` | 命令执行结果 |
| `task_status` | 任务状态更新(executing/complete/failed) |
| `task_process` | 任务执行过程中的 AI 输出 |
| `task_error` | 任务执行错误 |
| `task_added` | 任务添加成功 |
| `task_list` | 待办任务列表(`task` action=list 的返回) |
| `task_completed` | 任务完成通知 |
| `dialogue_ended` | 对话结束通知 |
| `dialogue_reset` | 对话重置完成 |
| `notification` | 通知消息 |
| `connection_ack` | 连接确认 |
| `interrupt_ack` | 中断确认 |
| `info` | 系统信息 |
| `pong` | Ping 响应 |
@@ -357,15 +402,17 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i
## 🧩 提示词系统
提示词文件位于 `~/.aibox/prompts/`:
提示词文件位于 `~/.aibox/prompts/`,按文件名**数字前缀升序**加载(identity → 技能 1~10 → soul → status):
| 文件 | 用途 | 更新方式 |
|-----|------|---------|
| `00_IDENTITY.md` | 用户身份信息 | AI 通过 `update_identity` 工具更新 |
| `1_*.md` | 基础技能提示词 | 手动编辑,AI 通过 `reload_prompts` 重载 |
| `99_SOUL.md` | AI 灵魂定义(角色、性格) | AI 通过 `update_soul` 工具更新 |
| `N_*.md`(N=1~10) | 基础技能提示词(记忆搜索/保存、文件/网页/命令/任务操作、技能扩展、身份管理、语言、任务规则) | 手动编辑,AI 通过 `/reload_prompts` 重载 |
| `99_SOUL.md` | AI 灵魂定义(角色、性格、能力边界、行为准则) | AI 通过 `update_soul` 工具更新 |
| `100_STATUS.md` | AI 状态记录 | AI 通过 `save_memory_and_end_conversation` 更新 |
> **加载顺序保证**:`00_IDENTITY.md` 最先加载(AI 先知道用户是谁),然后是 1~10 号技能提示词(按数字升序),接着是 `99_SOUL.md`(AI 的角色定位),最后是 `100_STATUS.md`(当前状态)。每轮用户输入前会附加一段简洁的工作流程提醒(不重复 system prompt 中的详细说明),工具调用后递归继续时不再重复注入。
## ⚙️ 配置说明
主要配置项(`~/.aibox/config.json`,首次启动自动生成):
@@ -374,46 +421,105 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i
{
"system": {
"save_dir": "~/.aibox",
"datetime_format": "%Y-%m-%d %H:%M:%S",
"date_format": "%Y%m%d_%H%M%S",
"debug_level": 1,
"load_history_by_default": true,
"log_file": "~/.aibox/ai.log",
"log_max_size": 10485760,
"log_backup_count": 5,
"websocket_host": "localhost",
"websocket_port": 8765,
"debug_level": 1,
"log_file": "~/.aibox/ai.log",
"skills_dir": "~/.aibox/skills",
"prompts_dir": "~/.aibox/prompts"
"prompts_dir": "~/.aibox/prompts",
"workspace_dir": "~/.aibox/workspace"
},
"scheduler": {
"enabled": true,
"check_interval": 1.0,
"max_tasks_per_run": 10,
"broadcast_enabled": true
},
"api": {
"provider": "deepseek",
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
"deepseek_model": "deepseek-v4-flash",
"reasoning_enabled": false,
"api_key_env": "DEEPSEEK_API_KEY",
"timeout": 120,
"temperature": 0.7
},
"context": {
"max_history_messages": 500,
"max_context_messages": 120,
"max_tokens": 8192,
"max_history_conversations": 5,
"preserve_system_prompts": true,
"compress_enabled": true,
"compress_message_threshold": 200,
"compress_token_threshold": 120000,
"compress_ratio": 0.2,
"compress_keep_recent": 8
},
"scheduler": {
"enabled": true,
"check_interval": 1.0,
"max_tasks_per_run": 10
"memo": {
"default_reminder_minutes": 60,
"max_memos_per_check": 10,
"allow_user_tasks": true
},
"api": {
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
"deepseek_model": "deepseek-v4-flash",
"api_key_env": "DEEPSEEK_API_KEY"
"files": {
"max_file_size": 10485710
},
"commands": {
"enabled": true
},
"web": {
"timeout": 10,
"delay": 0,
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"respect_robots": false,
"max_download_size": 104857600
},
"memory": {
"max_search_results": 100
"db_name": "memories.db",
"max_search_results": 100,
"default_importance": 2
},
"conversation": {
"current_file": "current_conversation.json",
"history_file": "conversation_history.json",
"archive_dir": "archive"
},
"tools": {
"max_result_length": 10000
"max_result_length": 1048576,
"save_result_to_file": false,
"tool_results_dir": "tool_results"
},
"compressor": {
"min_messages_to_compress": 5,
"weight_decay_factor": 0.01,
"max_input_chars": 6000,
"llm_max_tokens": 800
},
"proxy": {
"type": "",
"host": "",
"port": 0,
"username": "",
"password": ""
}
}
```
> 默认模型为 `deepseek-v4-flash`(通过 `api.deepseek_model` 修改)。API Key 从环境变量 `DEEPSEEK_API_KEY` 读取(亦支持 `~/.cert/deepseek.key` 文件)。
> 默认模型为 `deepseek-v4-flash`(通过 `api.deepseek_model` 修改)。设置 `api.reasoning_enabled: true` 可启用思考模式,此时会在请求中添加 `thinking: {type: "enabled"}` 参数,模型会先进行思考再回答,实时展示思考过程(灰色斜体区域),回答质量更高且**支持工具调用**(文件操作、命令执行、记忆搜索等工具均可在思考模式下正常使用)。
>
> API Key 从环境变量 `DEEPSEEK_API_KEY` 读取(亦支持 `~/.cert/deepseek.key` 文件)。
>
> **多厂商切换**:大模型调用已通过 `aibox/llm_client.py` 抽象层解耦,业务层不再直接依赖具体厂商。配置 `api.provider`(默认 `deepseek`)即可切换厂商,当前内置 `DeepSeekClient`(兼容 OpenAI Chat Completions 协议,Kimi / Qwen / GLM / Doubao 等绝大多数厂商均可复用)。新增厂商只需在此文件新增一个 `LLMClient` 子类并在 `create_llm_client` 工厂中注册,无需改动业务代码,为后续子代理/多模型共发预留了扩展点。
## 🛠️ 命令行参数
```bash
python main.py [选项]
python aibox.py [选项]
选项:
--verbose, -v 显示详细日志
@@ -434,19 +540,19 @@ python main.py [选项]
```bash
# 启动服务器(详细日志)
python main.py --verbose
python aibox.py --verbose
# 指定端口
python main.py --port 9000
python aibox.py --port 9000
# 检查任务
python main.py --check-memo
python aibox.py --check-memo
# 添加任务(命令行)
python main.py --memo-add "提醒开会|下午3点会议室A|+1h"
python aibox.py --memo-add "提醒开会|下午3点会议室A|+1h"
# 查看所有任务
python main.py --memo-list
python aibox.py --memo-list
```
## 🔄 架构流程
@@ -486,9 +592,8 @@ python main.py --memo-list
### 普通对话流程
```
Client -> Server: {type: "message", content: "帮我查天气"}
Server -> Client: {type: "chunk", content: "正在查"}
Server -> Client: {type: "chunk", content: "询天气..."}
Server -> Client: {type: "tool_call", tool: "search_skills", args: {keyword: "天气"}}
Server -> Client: {type: "line", content: "🔧 调用工具: search_skills\n"}
Server -> Client: {type: "line", content: "🔧 调用工具: bing_search\n"}
Server -> Client: {type: "chunk", content: "今日天气晴朗..."}
Server -> Client: {type: "complete", content: "今日天气晴朗,温度25°C"}
```
@@ -516,10 +621,10 @@ Server -> Client: {type: "dialogue_reset", content: "✅ 对话状态已完全
### 添加新工具
1. 继承 `Tool` 基类(位于 `main.py`)
1. 继承 `Tool` 基类(位于 `aibox.py`)
2. 实现 `get_name()`、`get_description()`、`get_parameters()`、`execute()`
3. 可选实现 `execute_streaming()` 支持流式输出
4. 在 `DeepSeekChat` 的工具注册处注册
3. 可选实现 `execute_streaming()` 支持流式输出(通过 `output_queue` 发送 `("line", content)` 和 `("complete", final_result)`)
4. 在 `DeepSeekChat._register_tools()` 中注册
```python
class MyTool(Tool):
@@ -540,6 +645,11 @@ class MyTool(Tool):
return f"执行结果: {param}"
```
> **注意**:`execute()` 必须返回字符串。工具结果会经过多层安全防护:
> 1. **工具层源头限制**:文件/网页工具返回 markdown 指引,实际 I/O 由 `run_command` 完成(`run_command` 输出≤1MB,超出立即终止进程;`read_file` 指引命令限制 500 行、单行≤10KB)
> 2. **`_sanitize_tool_result()`**:None/非字符串转为安全字符串,过滤控制字符
> 3. **最终兜底截断**:超过 `tools.max_result_length`(默认1MB字符)时截断并提示,避免破坏 API 请求格式
### 添加新命令
在 `WebSocketHandler` 的命令处理分支(`handle_command`)中添加:
@@ -550,6 +660,10 @@ elif command == "/mycommand":
await self.send_message(websocket, {"type": "command_result", "content": result})
```
### 错误恢复机制
`think_and_respond()` 在每轮对话开始前记录消息列表长度 `msgs_before_turn`。若 API 请求失败(网络错误、HTTP 错误、JSON 解析错误或其他异常),会自动调用 `_rollback_messages(msgs_before_turn)` 回滚本轮添加的所有消息(包括 user 消息、可能的 assistant/tool_calls 消息、tool 响应消息),防止格式错误的消息污染对话历史导致后续请求持续失败。客户端收到 `error` 消息后可安全重试。
## ❓ 常见问题
### Q: API Key 无效怎么办?
@@ -595,15 +709,90 @@ elif command == "/mycommand":
### Q: WebSocket 连接断开?
**A:**
1. 检查服务器是否运行:`ps aux | grep main.py`
1. 检查服务器是否运行:`ps aux | grep aibox.py`
2. 确认端口未被占用:`netstat -tlnp | grep 8765`
3. 检查防火墙设置
4. 查看服务器日志:`tail -f ~/.aibox/ai.log`
5. 考虑使用 `--host 0.0.0.0` 允许外部连接
### Q: 工具调用后 API 报错无法继续?
**A:** 此问题已在 v2.1 修复:工具结果会经过安全清洗(控制字符过滤、非字符串转换、超长截断),API 请求失败时自动回滚本轮消息。如果仍出现问题:
1. 查看日志:`tail -f ~/.aibox/ai.log`
2. 使用 `/new` 命令重置对话
3. 确认工具返回值为字符串且不含二进制/控制字符
## 📄 更新日志
### v2.0 (当前版本)
### v2.8
- ✅ **新增 `express_emotion` 工具**:表达情绪并记录当前情绪状态(anger/joy/sadness/surprise),可指定 `persona` 从 `~/.aibox/skills/` 加载人格技能文件(如 angry/coquettish/tsundere),自动派生携带该人格的情绪子代理生成回应,保证人格独立、不污染主对话上下文
- ✅ **新增 `/search <关键词>` 命令**:在 WebSocket 会话中按关键词搜索记忆,返回匹配的记忆条目
### v2.7
- ✅ **新增子代理功能(`create_subagent` 工具)**:
- 主代理可派生出拥有**独立上下文窗口**和**独立角色设定**的轻量子代理,不污染主对话上下文
- 轻量子代理:无工具,通过一次 LLM 补全返回结果,适合长任务分解、上下文隔离、独立角色专注处理(如代码审查、文本总结、资料研究)
- 子代理可选用**不同模型**(复用 `llm_client` 抽象层,`create_llm_client` 新增 `model` 参数)
- 参数:`role`(角色/系统提示词,必填)、`task`(任务,必填)、`model`/`temperature`/`max_tokens`(可选)
- `max_tokens` 已钳制到 `[1, 主配置 DEFAULT_MAX_TOKENS]`,防止意外超限消耗
### v2.6
- ✅ **修复 Ctrl+C 中断无法停止服务器的问题**:
- 新增 `interrupt` WebSocket 消息类型处理(BotOps 前端 Ctrl+C 发送此消息),立即回复 `interrupt_ack` 确认
- `DeepSeekChat` 新增 `cancel()` 方法和 `_cancel_event`(threading.Event),线程安全可跨线程调用
- **三层取消机制**:
1. 设置取消标志,流式响应循环每轮检查,发现取消立即 break
2. 关闭当前 HTTP response 对象,中断 `iter_lines()` 阻塞读取
3. 终止正在运行的子进程(`SubprocessCommandExecutor.cancel()` → `process.kill()`)
- 工具调用循环中每个工具执行前检查取消信号,中断后不再继续后续工具
- API 请求异常/JSON解析异常/未知异常中均检查取消标志,若因取消导致异常则按中断处理而非报错
- 中断后自动回滚本轮消息,防止污染对话历史
- 新增 `EnhancedAIManager.cancel()` 统一入口,同时终止 AI 和命令执行器
### v2.5
- ✅ **新增 DeepSeek 思考模式支持**(Reasoning Mode):
- 新增 `api.reasoning_enabled` 配置项(默认 `false`),启用后在请求中添加 `thinking: {type: "enabled"}` 参数
- 使用 `deepseek-v4-flash` 模型原生思考能力,**思考模式下完整支持工具调用**(文件操作、命令执行、记忆搜索等均可正常使用)
- 实时展示 AI 思考过程:新增 `reasoning_start`/`reasoning`/`reasoning_end` WebSocket 消息类型
- 思考内容在前端以灰色斜体区域独立展示,超长思考自动折叠显示最后 2000 字符
- 即使未启用思考模式,若模型返回 `reasoning_content` 也会自动实时展示
- API 超时默认从 30s 提高到 120s,适配思考模式更长的响应时间
- ✅ 删除重复定义的 `_add_assistant_message_with_reasoning` 方法
### v2.4
- ✅ **新增专用工作目录规范**:
- AI 主动创建的临时文件、脚本、数据文件默认写入 `~/.aibox/workspace/`,避免污染系统其他目录
- `write_file` 工具默认路径设为工作目录(用户明确指定路径时除外)
- `run_command` 工具默认 `work_dir` 设为工作目录,命令产生的文件落在专用工作区
- 新增 `system.workspace_dir` 配置项(默认 `~/.aibox/workspace`),可自定义工作目录位置
- 在文件操作和命令执行提示词中明确工作目录规范
- ✅ 命令执行提示词补充 1MB 输出大小限制说明
### v2.3
- ✅ **修复工具输出OOM崩溃问题**:
- `read_file`:不再一次性 `readlines()` 加载整个文件,改为按行范围按需读取;大文件 `tail=true` 使用反向块读取(>1MB文件不全文遍历);新增单行长度限制(10KB/行,超长截断);`max_lines` 硬上限500行防止请求超大范围
- `run_command`:非流式执行从 `capture_output=True` 改为逐行读取并实时计数,输出达到1MB立即终止进程;流式执行同样增加1MB输出大小限制,超出终止并提示截断;统一stdout/stderr合并计数
- `tools.max_result_length` 默认值从10KB提高到1MB字符,作为最终兜底截断
- 形成"工具层源头限制→结果清洗→最终兜底"三层防护,彻底防止大文件/大输出导致内存溢出
### v2.2
- ✅ 修复提示词系统排序 bug:`SKILL_PREFIX = "1_"` 只匹配 `1_` 开头文件,导致 `2_`~`10_` 技能提示词排序错乱(`10_task_handling.md` 排在 `1_memory_search.md` 前面);改用正则按数字前缀排序
- ✅ 修复 `_init_system_prompts` 使用未排序列表的问题,确保 system prompt 按 identity → 技能 → soul → status 正确顺序加载
- ✅ 精简每轮用户输入的 pre_prompt:原来约 30 行重复指令(与 system prompt 内容重复),每轮浪费 ~300 token;精简为 5 条简洁提醒
- ✅ 修复工具调用后递归继续时重复注入工作流指令的问题:新增 `is_continuation` 参数,递归时跳过 pre_prompt
- ✅ 优化默认大文件阈值:50KB/200行 → 500KB/2000行,增加 `tail=true` 和二进制文件处理指导
- ✅ 优化记忆搜索习惯:"每次都要做"→"根据话题相关性判断",避免简单问候也触发无意义搜索
### v2.1
- ✅ 修复工具调用结果破坏对话导致 API 持续请求失败的问题
- 不再回传 `reasoning_content`(API 响应字段)到请求中
- `tool_call.id` 为空时自动生成 fallback id,防止 API 400
- 新增 `_sanitize_tool_result()`:None/非字符串安全转换、控制字符过滤、超长结果截断
- `execute_tool` 异常捕获,避免工具执行异常中断整个对话
- API 请求失败时自动回滚本轮消息(`_rollback_messages`),防止坏消息污染历史
- ✅ 修复 `complete` 消息语义:chunk 流式为内容权威来源,complete 仅用于收尾
- ✅ 修正文档:入口文件为 `aibox.py`(非 `main.py`),补充缺失的消息类型(`line`/`task_error`/`notification` 等)
### v2.0
- ✅ 新增 `update_soul` 工具(智能体自我进化)
- ✅ 新增 `create_skill` 工具(AI 自主创建技能)
- ✅ 新增 `install_skill` 工具(技能安装指引)
+824
View File
@@ -0,0 +1,824 @@
# AIBOX 模块技术手册
> 文档版本:2026-07-30
> 代码版本:v2.0
> 语言:Python 3.8+
> 定位:单 AI 智能体系统(独立于微服务体系)
---
## 一、模块概述
### 1.1 定位
**AIBOX** 是一个基于 DeepSeek API 的单 AI 智能体系统,通过 WebSocket 与客户端双向通信。所有任务(文件操作、网页访问、记忆管理、任务调度等)都由 AI 对话驱动执行。
### 1.2 与微服务体系的关系
| 属性 | 说明 |
|------|------|
| 启动方式 | `core`(PMC) 通过 `init.txt` 拉起 |
| 监听端口 | 8765(WebSocket) |
| 与主干关系 | **完全独立**,不调用 waf/gateway/dnfs 等服务 |
| 数据存储 | 本地 `~/.aibox/`(不使用 db:8005) |
| 前端 | 独立 HTML 页面 |
### 1.3 核心特性
| 特性 | 说明 |
|------|------|
| AI 对话 | DeepSeek API + 思考模式(reasoning 流式输出) |
| 实时通信 | WebSocket 双向通信 + 流式输出 |
| 工具系统 | 19 个内置工具(文件/网页/记忆/任务/命令等) |
| 技能扩展 | 热重载 `.py` 脚本和 `.md` 技能 |
| 记忆管理 | 自动保存/检索对话记忆 |
| 任务调度 | 一次性/重复/即时任务 |
| 自我进化 | AI 动态更新角色与用户画像 |
| 对话压缩 | 自动压缩长对话历史 |
| 多语言 | 中英自动检测 |
---
## 二、技术架构
### 2.1 技术栈
| 层 | 技术 | 用途 |
|----|------|------|
| 语言 | Python 3.8+ | 主程序 |
| 通信 | websockets | WebSocket 服务器 |
| AI | DeepSeek API | 智能对话、工具调用、流式输出 |
| 存储 | SQLite | 记忆数据库、备忘录任务 |
| HTTP | requests | 网页访问、API 调用 |
| HTML 解析 | beautifulsoup4 | 网页内容提取 |
| SSH | paramiko | SSH 远程执行(技能) |
| 加密 | cryptography | AES 加密(技能) |
### 2.2 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ WebSocket 客户端 │
│ (entry.html / frontend.html / client.py / voice_client.py) │
└────────────────────────┬────────────────────────────────────┘
│ ws://localhost:8765
┌────────────────────────▼────────────────────────────────────┐
│ WebSocket 服务器 (main.py) │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 消息处理器 │ │ 命令处理器 │ │ 任务调度器 │ │
│ │ (handle_msg) │ │ (handle_cmd) │ │ (MemoScheduler) │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
└─────────┼─────────────────┼───────────────────┼─────────────┘
│ │ │
┌─────────▼─────────────────▼───────────────────▼─────────────┐
│ AI 对话引擎 (DeepSeekChat) │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 提示词管理器 │ │ 工具注册器 │ │ 对话压缩器 │ │
│ │ (PromptMgr) │ │ (ToolRegistry)│ │ (ConversationComp)│ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
└─────────┼─────────────────┼───────────────────┼─────────────┘
│ │ │
┌─────────▼─────────────────▼───────────────────▼─────────────┐
│ 数据持久层 │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 记忆数据库 │ │ 备忘录 DB │ │ 对话历史管理器 │ │
│ │ (memories.db) │ │ (memos.db) │ │ (JSON files) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 2.3 核心数据流
```
客户端请求 → WebSocketHandler.handle_message()
↓
┌─── 消息类型判断 ───┬─── 命令类型判断 ───┬─── 任务操作判断 ───┐
│ type="message" │ type="command" │ type="task" │
└─────────┬─────────┴─────────┬─────────┴─────────┬─────────┘
│ │ │
▼ ▼ ▼
AI 对话引擎 命令处理器 任务管理器
(DeepSeekChat) (handle_command) (handle_task)
│ │ │
▼ ▼ ▼
工具调用循环 系统命令执行 任务 CRUD 操作
(ToolRegistry) (/new, /list...) (add, complete...)
│ │ │
▼ ▼ ▼
流式响应生成 命令结果返回 任务状态广播
(chunk → complete) (command_result) (task_status)
```
---
## 三、代码结构
### 3.1 文件结构
```
aibox/
├── main.py # 核心(~8000 行)
│ ├── WebSocketHandler # WebSocket 消息处理
│ ├── DeepSeekChat # AI 对话引擎
│ ├── PromptManager # 提示词管理
│ ├── Tool + 19 个工具类 # 工具系统
│ ├── MemoScheduler # 任务调度器
│ ├── MemoryManager # 记忆管理
│ └── ConfigManager # 配置管理
├── client.py # CLI WebSocket 客户端
├── voice_client.py # 语音客户端(语音输入/输出)
├── entry.html # Web 前端入口
├── frontend.html # Web 主页面
├── skills/ # 内置扩展技能
│ ├── hello-world.py # 示例技能
│ ├── bing_search.py # 必应搜索
│ ├── ssh-exec.py # SSH 远程执行
│ ├── sftp-transfer.py # SFTP 传输
│ ├── facerec.py # 人脸识别
│ ├── aes-crypto.py # AES 加密
│ ├── gitclient.py # Git 操作
│ └── *.md # Markdown 技能(prompt)
├── tasks/ITIL.md # ITIL 任务模板
├── SKILLS_FORMAT.md # 技能格式规范
├── requirements.txt
├── _run.sh # core 拉起的启动脚本
├── _runcli.sh # CLI 启动
├── _set.sh # 依赖安装
├── _setenv.sh # 环境变量设置
└── README.md # 用户文档
```
### 3.2 main.py 核心类
| 类名 | 行号 | 功能 |
|------|------|------|
| `Tool` | L3642 | 工具基类(抽象方法:get_name/execute) |
| `WebSocketHandler` | ~L1900 | WebSocket 消息处理 |
| `DeepSeekChat` | ~L2400 | AI 对话引擎 |
| `PromptManager` | ~L1050 | 提示词文件管理 |
| `ConfigManager` | ~L1080 | 配置加载/合并 |
| `MemoryManager` | ~L5200 | 记忆 CRUD |
| `MemoScheduler` | ~L4700 | 任务调度器 |
| `ConversationCompressor` | ~L6480 | 对话压缩 |
### 3.3 运行期数据
```
~/.aibox/
├── config.json # 配置(自动生成默认值)
├── prompts/
│ ├── 00_IDENTITY.md # 用户身份(AI 可更新)
│ ├── 1_*.md # 基础技能提示词
│ ├── 99_SOUL.md # AI 灵魂定义(AI 可更新)
│ └── 100_STATUS.md # AI 状态记录
├── skills/ # 用户扩展技能
├── memories.db # 记忆库(SQLite)
├── memos.db # 备忘录任务(SQLite)
├── current_conversation.json # 当前对话上下文
├── conversation_history.json # 对话索引
├── archive/ # 历史对话归档
├── tool_results/ # 工具执行结果缓存
└── ai.log # 日志(自动轮转)
```
---
## 四、工具系统
### 4.1 工具基类
```python
class Tool:
"""工具基类,所有工具必须继承此类"""
def get_name(self) -> str:
"""返回工具名称(抽象方法,必须实现)"""
pass
def get_description(self) -> str:
"""返回工具描述"""
return ""
def get_parameters(self) -> Dict:
"""返回工具参数(JSON Schema 格式)"""
return {"type": "object", "properties": {}}
def execute(self, **kwargs) -> str:
"""执行工具(抽象方法,必须实现)"""
pass
def execute_streaming(self, **kwargs):
"""流式执行(可选实现)"""
return None
```
### 4.2 内置工具一览
| 工具名 | 类名 | 行号 | 功能 |
|--------|------|------|------|
| `read_file` | `ReadFileTool` | L5762 | 读取文件(支持 head/tail/paging) |
| `write_file` | `WriteFileTool` | L5884 | 写入/追加文件 |
| `list_files` | `ListFilesTool` | L5961 | 目录列表 |
| `web` | `SimpleWebCrawlerTool` | L5637 | 网页抓取(标题/正文/链接) |
| `memory` | `MemoryTool` | L5280 | 记忆 CRUD(search/save/delete) |
| `memo` | `MemoTool` | L4753 | 任务 CRUD(add/complete/list/search) |
| `run_command` | `CommandLineTool` | L6150 | 系统命令执行(支持超时) |
| `search_skills` | `SearchSkillsTool` | L4076 | 搜索扩展技能 |
| `reload_skills` | `ReloadSkillsTool` | L4199 | 热重载技能 |
| `create_skill` | `CreateSkillTool` | L4409 | AI 自主创建技能 |
| `install_skill` | `InstallSkillTool` | L4306 | 技能安装指引 |
| `ask_user` | `AskUserTool` | L4275 | 向用户提问 |
| `update_identity` | `UpdateIdentityTool` | L4518 | 更新用户身份 |
| `update_soul` | `UpdateSoulTool` | L4582 | 更新 AI 灵魂 |
| `save_memory_and_end_conversation` | `SaveMemoryAndEndConversationTool` | L4646 | 保存记忆并结束对话 |
| `get_current_time` | `GetCurrentTimeTool` | L6055 | 获取当前时间 |
| `calculator` | `CalculatorTool` | L6095 | 数学计算 |
| `set_proxy` | `SetProxyTool` | L6415 | 设置 SOCKS5 代理 |
| `get_proxy` | `GetProxyTool` | L6453 | 查看代理配置 |
### 4.3 工具执行流程
```
AI 决定调用工具 → 生成 JSON 格式工具调用
↓
DeepSeekChat.process_tool_calls()
↓
┌─── 解析工具名 ───┬─── 解析参数 ───┬─── 注册工具 ───┐
└─────────────────┴───────────────┴─────────────────┘
↓
ToolRegistry.get_tool(name)
↓
tool.execute(**params)
↓
┌─── 结果处理 ───┬─── 错误处理 ───┬─── 长结果截断 ───┐
└─────────────────┴───────────────┴─────────────────┘
↓
返回结果给 AI → AI 继续生成响应
```
---
## 五、WebSocket 协议
### 5.1 客户端 → 服务器
| 类型 | 字段 | 说明 |
|------|------|------|
| `message` | `content` | 普通对话文本 |
| `command` | `command` | 斜杠命令(如 `/new`, `/help`) |
| `task` | `action`, `title`, ... | 任务操作(add/complete/list) |
| `ping` | — | 心跳检测 |
### 5.2 服务器 → 客户端
| 类型 | 字段 | 说明 |
|------|------|------|
| `chunk` | `content` | AI 响应片段(流式) |
| `complete` | `content` | AI 响应完成 |
| `reasoning` | `content` | AI 思考过程 |
| `error` | `content` | 错误信息 |
| `command_result` | `content` | 命令执行结果 |
| `task_status` | `task_id`, `status` | 任务状态更新 |
| `task_process` | `task_id`, `content` | 任务执行过程 |
| `dialogue_ended` | — | 对话结束 |
| `dialogue_reset` | — | 对话重置完成 |
| `info` | `content` | 系统信息 |
| `pong` | — | Ping 响应 |
### 5.3 消息流示例
```
// 普通对话
Client: {type: "message", content: "你好"}
Server: {type: "chunk", content: "你好!"}
Server: {type: "complete", content: "你好!有什么可以帮你的吗?"}
// 工具调用
Server: {type: "tool_call", tool: "web", args: {url: "..."}}
Server: {type: "chunk", content: "网页内容:..."}
Server: {type: "complete", content: "网页已获取"}
// 任务执行
Client: {type: "task", action: "add", title: "提醒", delay: 60}
Server: {type: "command_result", content: "任务已添加"}
// 60 秒后
Server: {type: "task_status", task_id: 1, status: "executing"}
Server: {type: "task_process", task_id: "1", content: "处理中..."}
Server: {type: "task_status", task_id: 1, status: "complete"}
```
---
## 六、任务系统
### 6.1 任务类型
| 类型 | 字段 | 说明 |
|------|------|------|
| 一次性 | `reminder_time` | 指定执行时间 |
| 重复(daily) | `repeat_type: "daily"` | 每天执行 |
| 重复(weekly) | `repeat_type: "weekly"`, `weekday` | 每周指定日 |
| 重复(monthly) | `repeat_type: "monthly"`, `day` | 每月指定日 |
| 重复(custom) | `repeat_type: "custom"`, `interval`, `unit` | 自定义间隔 |
| 即时 | `is_immediate: true` | 立即执行 |
### 6.2 任务调度流程
```
MemoScheduler._run() # 后台线程
↓
检查所有未完成任务
↓
┌─── 到达时间? ───┬─── 未完成? ───┬─── 允许用户任务? ───┐
└─────────────────┴───────────────┴─────────────────┘
↓
创建新的 AI 对话(将任务内容作为用户输入)
↓
AI 处理任务(可能调用多个工具)
↓
标记任务已触发
↓
广播结果到所有客户端
↓
计算下一次执行时间(重复任务)
```
### 6.3 数据库表结构
```sql
-- memories.db
CREATE TABLE memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
content TEXT,
importance INTEGER DEFAULT 2,
created_at TEXT,
updated_at TEXT,
embedding BLOB -- 预留向量嵌入
);
-- memos.db
CREATE TABLE memos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
reminder_time TEXT,
repeat_type TEXT,
repeat_interval INTEGER,
repeat_interval_unit TEXT,
is_immediate INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending',
is_triggered INTEGER DEFAULT 0,
created_at TEXT,
user_id TEXT
);
```
---
## 七、记忆系统
### 7.1 记忆操作
| 操作 | AI 调用 | 说明 |
|------|---------|------|
| 保存 | `memory(action="save", content=...)` | AI 主动保存重要信息 |
| 搜索 | `memory(action="search", query=...)` | 按关键词搜索记忆 |
| 列表 | `memory(action="list", page=...)` | 分页浏览所有记忆 |
| 删除 | `memory(action="delete", id=...)` | 删除指定记忆 |
### 7.2 自动记忆
- AI 在对话结束时(`/exit`)自动保存关键信息
- 可配置 `system.auto_save_memory` 开关
- 重要度(importance)1-5 级
---
## 八、对话压缩
### 8.1 压缩触发条件
| 条件 | 阈值(默认) | 配置项 |
|------|-------------|--------|
| 消息数超过 | 200 条 | `compress_message_threshold` |
| Token 数超过 | 120000 | `compress_token_threshold` |
### 8.2 压缩流程
```
检测到阈值触发
↓
保留最近 8 条消息(compress_keep_recent)
↓
对早期消息按时间衰减加权(weight_decay_factor=0.01)
↓
调用 AI 生成摘要(max_input_chars=6000)
↓
将摘要作为系统消息插入
↓
压缩后继续对话
```
### 8.3 压缩配置
```json
{
"compressor": {
"min_messages_to_compress": 5,
"weight_decay_factor": 0.01,
"max_input_chars": 6000,
"llm_max_tokens": 800
}
}
```
---
## 九、扩展技能系统
### 9.1 技能类型
| 类型 | 格式 | 存储位置 |
|------|------|---------|
| 可执行脚本 | Python/Shell,支持 `--help` | `~/.aibox/skills/` |
| Markdown 技能 | `.md` 文件,首行为简介 | `~/.aibox/skills/` |
### 9.2 内置技能
| 技能名 | 文件 | 功能 |
|--------|------|------|
| hello-world | `skills/hello-world.py` | 示例技能 |
| bing_search | `skills/bing_search.py` | 必应搜索 |
| ssh-exec | `skills/ssh-exec.py` | SSH 远程执行 |
| sftp-transfer | `skills/sftp-transfer.py` | SFTP 文件传输 |
| facerec | `skills/facerec.py` | 人脸识别 |
| aes-crypto | `skills/aes-crypto.py` | AES 加密/解密 |
| gitclient | `skills/gitclient.py` | Git 操作封装 |
### 9.3 技能热重载
```python
# 新技能放置到 ~/.aibox/skills/
# 调用 reload_skills 工具
# AI 下次对话时即可使用
```
### 9.4 技能格式规范
详见 [SKILLS_FORMAT.md](file:///Users/qingsong/micro_servers/aibox/SKILLS_FORMAT.md)
---
## 十、配置参考
### 10.1 完整配置项
#### system 段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `save_dir` | str | `~/.aibox` | 数据存储目录 |
| `datetime_format` | str | `%Y-%m-%d %H:%M:%S` | 日期时间格式 |
| `date_format` | str | `%Y%m%d_%H%M%S` | 文件名日期格式 |
| `debug_level` | int | `1` | 调试级别 0-3 |
| `load_history_by_default` | bool | `true` | 启动时加载历史对话 |
| `log_file` | str | `~/.aibox/ai.log` | 日志文件路径 |
| `log_max_size` | int | `10485760` | 日志最大字节数(10MB) |
| `log_backup_count` | int | `5` | 日志备份数量 |
| `websocket_host` | str | `localhost` | WebSocket 监听地址 |
| `websocket_port` | int | `8765` | WebSocket 监听端口 |
| `skills_dir` | str | `~/.aibox/skills` | 技能目录 |
| `prompts_dir` | str | `~/.aibox/prompts` | 提示词目录 |
#### scheduler 段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `enabled` | bool | `true` | 启用任务调度器 |
| `check_interval` | float | `1.0` | 检查间隔(秒) |
| `max_tasks_per_run` | int | `10` | 单次最大触发任务数 |
| `broadcast_enabled` | bool | `true` | 广播任务执行结果 |
#### api 段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `provider` | str | `deepseek` | 模型厂商,用于 `llm_client` 工厂路由 |
| `deepseek_api_url` | str | `https://api.deepseek.com/chat/completions` | API URL |
| `deepseek_model` | str | `deepseek-v4-flash` | 模型名 |
| `api_key_env` | str | `DEEPSEEK_API_KEY` | API Key 环境变量名 |
| `timeout` | int | `30` | 请求超时(秒) |
| `temperature` | float | `0.7` | 生成温度 0-1 |
#### context 段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `max_history_messages` | int | `500` | 历史消息上限 |
| `max_context_messages` | int | `120` | 上下文窗口 |
| `max_tokens` | int | `8192` | 最大生成 Token |
| `max_history_conversations` | int | `5` | 保留历史对话数 |
| `preserve_system_prompts` | bool | `true` | 保留系统提示词 |
| `compress_enabled` | bool | `true` | 启用对话压缩 |
| `compress_message_threshold` | int | `200` | 压缩消息阈值 |
| `compress_token_threshold` | int | `120000` | 压缩 Token 阈值 |
| `compress_ratio` | float | `0.2` | 压缩保留比例 |
| `compress_keep_recent` | int | `8` | 保留最近消息数 |
#### memo 段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `default_reminder_minutes` | int | `60` | 默认提醒分钟数 |
| `max_memos_per_check` | int | `10` | 单次最大触发任务数 |
| `allow_user_tasks` | bool | `true` | 允许用户创建任务 |
#### proxy 段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `type` | str | `""` | 代理类型(socks5/http) |
| `host` | str | `""` | 代理主机 |
| `port` | int | `0` | 代理端口 |
| `username` | str | `""` | 代理用户名 |
| `password` | str | `""` | 代理密码 |
### 10.2 环境变量
| 变量 | 说明 |
|------|------|
| `DEEPSEEK_API_KEY` | DeepSeek API Key(必填) |
| `AI_CONFIG_PATH` | 自定义配置文件路径 |
---
## 十一、命令行参数
```bash
python main.py [选项]
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `--verbose`, `-v` | flag | `false` | 显示详细日志 |
| `--debug` | int | `None` | 调试级别 0-3 |
| `--lang` | str | `None` | 强制语言 zh/en |
| `--host` | str | `None` | 覆盖 WebSocket 主机 |
| `--port` | int | `None` | 覆盖 WebSocket 端口 |
| `--no-scheduler` | flag | `false` | 禁用任务调度器 |
| `--scheduler-interval` | float | `1.0` | 调度器检查间隔 |
| `--config`, `-c` | str | `None` | 指定配置文件 |
| `--check-memo` | flag | `false` | 检查到期任务并退出 |
| `--memo-add` | str | `None` | 添加任务(CLI) |
| `--memo-list` | flag | `false` | 列出任务 |
| `--memo-complete` | int | `None` | 完成指定任务 |
### 使用示例
```bash
# 启动服务器
python main.py
# 详细日志 + 指定端口
python main.py --verbose --port 9000
# 检查并执行到期任务(cron 场景)
python main.py --check-memo
# 添加任务
python main.py --memo-add "每日备份|执行数据库备份|+1h"
# 列出所有任务
python main.py --memo-list
```
---
## 十二、前端集成
### 12.1 前端文件
| 文件 | 说明 |
|------|------|
| `entry.html` | 简单入口页(重定向到 frontend.html) |
| `frontend.html` | 完整前端界面(对话/任务/设置) |
### 12.2 WebSocket 连接
```javascript
const ws = new WebSocket('ws://localhost:8765');
// 发送消息
ws.send(JSON.stringify({
type: 'message',
content: '你好'
}));
// 接收消息
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'chunk':
appendToChat(data.content);
break;
case 'complete':
finishResponse();
break;
case 'error':
showError(data.content);
break;
}
};
```
---
## 十三、部署
### 13.1 目录结构
```
/opt/micro_servers/aibox/
├── main.py
├── client.py
├── skills/
├── _run.sh # core 拉起入口
├── _set.sh # 首次安装
└── requirements.txt
```
### 13.2 core init.txt 配置
```bash
# aibox 由 core 拉起
bash /opt/micro_servers/aibox/_run.sh
```
### 13.3 systemd 单元(可选)
```ini
[Unit]
Description=AIBOX AI Agent
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/opt/micro_servers/aibox
ExecStart=/usr/bin/python3 main.py
Restart=always
RestartSec=5
Environment=DEEPSEEK_API_KEY=xxx
[Install]
WantedBy=multi-user.target
```
---
## 十四、已知限制
| ID | 描述 | 严重等级 |
|----|------|---------|
| L1 | 单实例运行,不支持多进程 | 🟡 Medium |
| L2 | 记忆库无向量搜索,仅关键词匹配 | 🟡 Medium |
| L3 | 对话压缩可能丢失早期重要信息 | 🟢 Low |
| L4 | 工具执行无超时控制(部分工具) | 🟡 Medium |
| L5 | 日志无远程推送,仅本地文件 | 🟢 Low |
| L6 | 无用户认证,WebSocket 连接可被滥用 | 🔴 High |
| L7 | API Key 以明文存储在环境变量或文件中 | 🟡 Medium |
### L6 详细说明
WebSocket 服务器**无任何认证**,任何能连接到 `ws://localhost:8765` 的客户端都可以:
- 与 AI 对话(消耗 API 额度)
- 执行系统命令(`run_command`)
- 操作文件系统(`read_file`/`write_file`)
- 创建/删除任务
**建议**:
1. 仅绑定 `localhost`(默认配置已这样做)
2. 添加 API Token 认证
3. 限制 `run_command` 可执行的命令范围
---
## 十五、开发指南
### 15.1 添加新工具
1. 继承 `Tool` 基类
2. 实现 `get_name()`、`get_description()`、`get_parameters()`、`execute()`
3. 在 `DeepSeekChat._register_tools()` 中注册
4. 更新 README 文档
```python
class MyTool(Tool):
def get_name(self) -> str:
return "my_tool"
def get_description(self) -> str:
return "我的自定义工具"
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"param": {"type": "string", "description": "参数说明"}
},
"required": ["param"]
}
def execute(self, param: str, **kwargs) -> str:
return f"执行结果: {param}"
```
### 15.2 添加新命令
在 `WebSocketHandler.handle_command()` 中添加:
```python
elif command == "/mycommand":
result = "命令执行结果"
await self.send_message(websocket, {
"type": "command_result",
"content": result
})
```
### 15.3 添加配置项
1. 在 `ConfigManager._get_default_config()` 中添加默认值
2. 在 README 文档中添加说明
3. 确保 `_merge_configs()` 能正确处理新增字段
---
## 十六、故障排查
### 16.1 常见问题
| 问题 | 排查步骤 |
|------|---------|
| API Key 无效 | 检查 `echo $DEEPSEEK_API_KEY` 或 `~/.cert/deepseek.key` |
| 技能不生效 | 确认技能文件在 `~/.aibox/skills/` 且有执行权限 |
| 任务不执行 | 检查调度器状态:`/tasks`;检查时间设置 |
| 对话压缩丢失信息 | 调整 `compress_message_threshold` 和 `compress_keep_recent` |
| WebSocket 断开 | 检查服务器进程:`ps aux | grep main.py`;查看日志 |
### 16.2 日志位置
```bash
# 查看实时日志
tail -f ~/.aibox/ai.log
# 查看最近错误
grep "ERROR" ~/.aibox/ai.log | tail -20
```
### 16.3 数据库检查
```bash
# 检查记忆库
sqlite3 ~/.aibox/memories.db "SELECT * FROM memories LIMIT 10;"
# 检查任务库
sqlite3 ~/.aibox/memos.db "SELECT * FROM memos WHERE status='pending';"
```
---
## 十七、版本历史
| 版本 | 日期 | 主要变更 |
|------|------|---------|
| v2.0 | 当前 | 新增 `ask_user`、`update_soul`、`create_skill`、`install_skill`、`set_proxy`/`get_proxy` 等工具;支持 Markdown 技能;对话压缩支持 Token 阈值 |
| v1.0 | — | 初始版本:WebSocket 服务器、文件/网页/记忆操作、任务调度、扩展技能、多语言 |
---
## 附录
### A. 配置文件示例
完整默认配置见 [main.py:1100-1180](file:///Users/qingsong/micro_servers/aibox/main.py#L1100-L1180)
### B. 数据库 Schema
完整建表语句见:
- 记忆库:[main.py](file:///Users/qingsong/micro_servers/aibox/main.py) 中 `MemoryManager._init_db()`
- 任务库:[main.py](file:///Users/qingsong/micro_servers/aibox/main.py) 中 `MemoScheduler._init_db()`
### C. 参考文档
- [DeepSeek API 文档](https://platform.deepseek.com/docs)
- [websockets 库文档](https://websockets.readthedocs.io/)
- [SKILLS_FORMAT.md](file:///Users/qingsong/micro_servers/aibox/SKILLS_FORMAT.md)
---
*本文档由代码审查工具生成,供开发团队参考。*
+1 -1
View File
@@ -3,6 +3,6 @@ cd "$(dirname "$0")" || exit
source ./_setenv.sh
source ./venv/bin/activate
export PATH=${PATH}:~/.aibox/skills
exec python main.py $@
exec python aibox.py $@
File diff suppressed because it is too large. Load diff
+179 -21
View File
@@ -9,11 +9,20 @@ import sys
import os
import threading
import hashlib
from collections import OrderedDict
from datetime import datetime
from typing import Optional, Dict, Any, List
import websockets
from colorama import init, Fore, Style as ColoramaStyle
# rich 为可选依赖:有则 AI 回复渲染为 Markdown,无则回退纯文本流式输出
try:
from rich.console import Console
from rich.markdown import Markdown
_RICH = True
except ImportError:
_RICH = False
init(autoreset=True)
class Colors:
@@ -75,10 +84,15 @@ class WebSocketClient:
except json.JSONDecodeError:
self.callback("chunk", message)
except websockets.exceptions.ConnectionClosed:
self.connected = False
self.callback("system", "❌ 连接已断开")
pass
except Exception as e:
self.callback("error", f"接收消息错误: {e}")
finally:
# 统一在此通知断线;手动 disconnect() 已把 connected 置 False,不会误触发自动重连
was_connected = self.connected
self.connected = False
if was_connected:
self.callback("disconnected", "❌ 连接已断开")
async def _handle_message(self, data: dict):
msg_type = data.get('type', '')
@@ -134,12 +148,25 @@ class SimpleCLI:
self.ai_full = ""
self.ai_prefix_shown = False
# 已显示消息的去重
self.displayed_messages: set = set()
# 已显示消息的去重(LRU 上限,防长对话内存无限增长)
self.DISPLAYED_MAX = 500
self.displayed_messages: OrderedDict = OrderedDict()
# 锁和状态
self.lock = threading.Lock()
self.current_input = ""
# 标准输入:单守护线程阻塞读 + asyncio.Queue 中转(避免 Ctrl+C 泄漏线程)
self._input_queue: asyncio.Queue = asyncio.Queue()
self._stdin_thread: Optional[threading.Thread] = None
# 自动重连状态
self._manual_disconnect = False # 用户主动 /disconnect、/quit、/connect 后置 True,抑制自动重连
self._reconnect_task: Optional[asyncio.Task] = None
# Markdown 渲染(rich 可用时启用)
self._console = Console() if _RICH else None
self._md_generating_shown = False # "生成中…"状态行是否已显示
# 流式输出状态
self.streaming_line = False # 是否正在流式输出行
@@ -232,16 +259,23 @@ class SimpleCLI:
"""流式显示增量内容"""
if not content:
return
# 清理内容
clean = self._clean_content(content)
if not clean:
return
key = clean[:200]
if key in self.displayed_messages:
return
# rich 可用:流式期间只显示状态行并累计全文,complete 时一次性渲染
if _RICH:
self._md_show_generating()
self.ai_previous = clean
self.ai_full = clean
return
# 检测增量
if self.ai_previous and clean.startswith(self.ai_previous):
new_part = clean[len(self.ai_previous):]
@@ -275,6 +309,39 @@ class SimpleCLI:
self.ai_previous = clean
self.ai_full = clean
# ── Markdown 渲染(rich)─────────────────────────────────────────────
def _md_show_generating(self):
"""流式期间显示一次性状态行(不实时渲染,避免 Live 闪烁/被卷走)。"""
if not _RICH:
return
with self.lock:
if not self._md_generating_shown:
sys.stdout.write('\n')
sys.stdout.write(f"{Colors.AI}🤖 AI助手: 生成中…{Colors.RESET}")
sys.stdout.flush()
self._md_generating_shown = True
self.streaming_line = True
self.prompt_shown = False
def _md_render_final(self, text: str):
"""complete 时一次性静态渲染最终 Markdown(先擦掉"生成中…"行)。"""
if not _RICH:
return
with self.lock:
if self._md_generating_shown:
sys.stdout.write('\r' + ' ' * 40 + '\r')
self._md_generating_shown = False
self.streaming_line = False
if text:
self._console.print(Markdown(text))
def _remember_displayed(self, key: str):
"""记录已显示消息(LRU:超 DISPLAYED_MAX 淘汰最旧,防长对话内存膨胀)。"""
self.displayed_messages[key] = None
self.displayed_messages.move_to_end(key)
if len(self.displayed_messages) > self.DISPLAYED_MAX:
self.displayed_messages.popitem(last=False)
def display(self, msg_type: str, content: str, extra: Any = None):
"""显示消息"""
if msg_type == 'chunk':
@@ -302,6 +369,11 @@ class SimpleCLI:
elif msg_type == 'system':
self._finish_stream()
self._print_line(f"{Colors.SYSTEM}{content}{Colors.RESET}")
elif msg_type == 'disconnected':
self.connected = False
self._finish_stream()
self._print_line(f"{Colors.SYSTEM}{content}{Colors.RESET}")
self._schedule_reconnect()
elif msg_type == 'info':
self._finish_stream()
self._print_line(f"{Colors.INFO}ℹ️ {content}{Colors.RESET}")
@@ -323,7 +395,7 @@ class SimpleCLI:
sys.stdout.write(f"{Colors.TASK}{content}{Colors.RESET}\n")
sys.stdout.flush()
return
# 检查是否是工具结果
if content.startswith(('✅ 对话记忆已保存', '💾 保存记忆', '❌', '⚠️')):
with self.lock:
@@ -350,6 +422,25 @@ class SimpleCLI:
def _handle_complete(self, content: str):
"""处理完成事件 - 只负责结束流式输出,不重复显示内容"""
# rich 渲染路径:擦掉"生成中…",一次性渲染最终 Markdown,恢复提示符
if _RICH:
clean = self._clean_content(content) if content else ""
self._md_render_final(clean)
if clean:
self._remember_displayed(clean[:200])
with self.lock:
self.in_tool_phase = False
self.waiting_for_response = False
self.streaming_line = False
self.prompt_shown = True
current = self.current_input
sys.stdout.write(self._get_prompt() + current)
sys.stdout.flush()
self.ai_previous = ""
self.ai_full = ""
self.ai_prefix_shown = False
return
# 如果之前在工具阶段,现在退出
if self.in_tool_phase:
with self.lock:
@@ -406,7 +497,7 @@ class SimpleCLI:
return
# 只有当内容没有被流式显示时才显示完整消息
self.displayed_messages.add(key)
self._remember_displayed(key)
# 如果之前没有流式输出,现在显示完整消息
self._print_line(f"{Colors.AI}🤖 AI助手: {clean}")
@@ -466,7 +557,7 @@ class SimpleCLI:
clean = content.strip()
key = clean[:200]
if key not in self.displayed_messages:
self.displayed_messages.add(key)
self._remember_displayed(key)
self._print_line(f"{Colors.TASK}⚡ 任务 - {task_title}: {clean}")
del self._task_buffers[task_title]
self.streaming_line = False
@@ -519,10 +610,15 @@ class SimpleCLI:
self.display("error", "用法: /connect ws://host:port")
return
uri = parts[1]
# 切换服务器:抑制旧连接的自动重连
self._manual_disconnect = True
if self._reconnect_task and not self._reconnect_task.done():
self._reconnect_task.cancel()
if self.client and self.client.connected:
await self.client.disconnect()
self.client = WebSocketClient(uri, self.display)
if await self.client.connect():
self._manual_disconnect = False
self.connected = True
self.display("system", f"✅ 已连接到 {uri}")
asyncio.create_task(self.client.receive_messages())
@@ -530,6 +626,9 @@ class SimpleCLI:
self.client = None
self.connected = False
elif cmd == '/disconnect':
self._manual_disconnect = True # 主动断开,不自动重连
if self._reconnect_task and not self._reconnect_task.done():
self._reconnect_task.cancel()
if self.client:
await self.client.disconnect()
self.client = None
@@ -556,6 +655,9 @@ class SimpleCLI:
elif cmd == '/quit':
self.display("system", "👋 再见!")
self.running = False
self._manual_disconnect = True
if self._reconnect_task and not self._reconnect_task.done():
self._reconnect_task.cancel()
if self.client:
await self.client.disconnect()
return
@@ -569,6 +671,35 @@ class SimpleCLI:
self.display("error", f"未知命令: {cmd}")
self._print_prompt(clear_input=True)
def _schedule_reconnect(self):
"""启动自动重连(若未手动断开且当前无进行中的重连任务)。"""
if self._manual_disconnect or not self.running or not self.client:
return
if self._reconnect_task and not self._reconnect_task.done():
return
try:
self._reconnect_task = asyncio.create_task(self._auto_reconnect_loop(self.client))
except RuntimeError:
pass # 事件循环正在关闭(程序退出中),放弃重连
async def _auto_reconnect_loop(self, client: WebSocketClient):
"""断线自动重连:指数退避 2s→30s 封顶,直到成功、/disconnect 或退出。"""
delay = 2
while self.running and not self._manual_disconnect and self.client is client:
self.display("info", f"🔄 {delay}s 后自动重连 {client.uri} …(/disconnect 取消)")
try:
await asyncio.sleep(delay)
except asyncio.CancelledError:
return
if not self.running or self._manual_disconnect or self.client is not client:
return
if await client.connect():
self.connected = True
self.display("system", f"✅ 已自动重连到 {client.uri}")
asyncio.create_task(client.receive_messages())
return
delay = min(delay * 2, 30)
async def _send_interrupt(self):
if self.client and self.client.connected:
interrupt_msg = {"type": "interrupt", "content": "用户按下了 Ctrl+C 中断当前操作"}
@@ -581,7 +712,7 @@ class SimpleCLI:
if not self.client or not self.client.connected:
self.display("error", "未连接到服务器")
return
# 重置流式状态
with self.lock:
if self.streaming_line:
@@ -601,30 +732,57 @@ class SimpleCLI:
await self.client.send_message(message)
# 不在这里显示提示符,等待AI响应结束后再显示
def _stdin_reader(self, loop: asyncio.AbstractEventLoop):
"""独立守护线程:阻塞读 stdin,经 call_soon_threadsafe 投入队列。
进程生命周期只启动一次——替代原 run_in_executor 每行一个线程的方案,
避免 Ctrl+C 取消 await 后旧线程仍阻塞在 readline 上无限泄漏。
"""
def _put(item):
try:
loop.call_soon_threadsafe(self._input_queue.put_nowait, item)
except RuntimeError:
pass # 事件循环已关闭(程序退出中)
while self.running:
try:
line = sys.stdin.readline()
except Exception:
break
if not line: # EOF (Ctrl+D)
_put(None)
break
_put(line)
async def run(self):
self.print_banner()
self._task_buffers = {}
loop = asyncio.get_running_loop()
if self._stdin_thread is None:
self._stdin_thread = threading.Thread(
target=self._stdin_reader, args=(loop,),
daemon=True, name="stdin-reader")
self._stdin_thread.start()
while self.running:
try:
line = await asyncio.get_event_loop().run_in_executor(
None, lambda: sys.stdin.readline()
)
if not line:
line = await self._input_queue.get()
if line is None: # EOF (Ctrl+D)
break
self.current_input = line.rstrip('\n')
line = line.strip()
if not line:
self._print_prompt(clear_input=True)
continue
if line.startswith('/'):
await self.handle_command(line)
else:
await self.send_user_message(line)
except KeyboardInterrupt:
if self.client and self.client.connected:
await self._send_interrupt()
+273
View File
@@ -0,0 +1,273 @@
# -*- coding: utf-8 -*-
"""
大模型客户端抽象层
为 aibox 提供统一的 LLM 调用接口,将「调用哪个厂商 / 哪个模型」与业务逻辑解耦。
业务层只依赖 `LLMClient` 抽象方法与统一的流式事件格式,切换厂商只需:
1. 新增一个 `LLMClient` 子类实现;
2. 在 `create_llm_client` 工厂里按 `api.provider` 进行路由。
当前内置实现:
- `DeepSeekClient`:兼容 OpenAI Chat Completions 协议(DeepSeek / Kimi / Qwen / GLM / Doubao 等绝大多数厂商均可复用)。
统一流式事件格式(`stream_chat` 逐条 yield 的 dict):
{"type": "content", "text": str} # 增量正文
{"type": "reasoning", "text": str} # 增量思考内容(可选,厂商支持时)
{"type": "tool_call", "index": int, "id": str, "name": str, "arguments": str} # 工具调用增量
{"type": "done"} # 流结束
"""
from abc import ABC, abstractmethod
from typing import Dict, Iterator, List, Optional
import json
import os
import requests
class LLMClient(ABC):
"""大模型客户端抽象基类。子类需实现流式/非流式两种调用,并归一化为统一事件格式。"""
def __init__(self, api_key: str, model: str, base_url: str,
timeout: int, config=None, logger=None):
self.api_key = api_key
self.model = model
self.base_url = base_url
self.timeout = timeout
self.config = config # 可选,用于读取代理等动态配置
self.logger = logger # 可选,用于日志输出
# 当前活动的 HTTP 响应引用,供 cancel() 强制关闭底层连接以中断阻塞读取
self._current_response = None
@property
def name(self) -> str:
"""厂商/模型展示名,业务层可用于上下文(如 history 里的 assistant 名)。"""
return "LLM"
# ---------- 工具方法 ----------
def _resolve_proxies(self) -> Optional[Dict[str, str]]:
"""从 config 解析 requests 可用的 proxies 字典(动态代理)。"""
if self.config is not None and hasattr(self.config, "get_proxy"):
return self.config.get_proxy()
return None
def _log(self, level: str, msg: str):
if self.logger is not None and hasattr(self.logger, level):
try:
getattr(self.logger, level)(msg)
except Exception:
pass
def cancel(self):
"""中断当前流式请求。线程安全,可从任意线程调用。"""
resp = self._current_response
self._current_response = None
if resp is not None:
try:
# 先关闭底层 raw socket,强制阻塞的 recv() 立即抛出异常
if getattr(resp, "raw", None) is not None:
resp.raw.close()
except Exception:
pass
try:
resp.close()
except Exception:
pass
# ---------- 抽象接口 ----------
@abstractmethod
def chat_completion(self, messages: List[Dict], temperature: float = 0.3,
max_tokens: int = 1024, **kwargs) -> str:
"""非流式对话,返回完整文本。用于对话压缩等离线摘要场景。"""
@abstractmethod
def stream_chat(self, messages: List[Dict], temperature: float = 0.7,
max_tokens: int = 8192, tools: Optional[List[Dict]] = None,
tool_choice: Optional[str] = None,
reasoning_enabled: bool = False, **extra) -> Iterator[Dict]:
"""流式对话,逐条 yield 统一事件格式的 dict。业务层据此驱动输出/工具调用。"""
class DeepSeekClient(LLMClient):
"""DeepSeek 客户端(OpenAI Chat Completions 协议兼容)。
思考模式通过 `thinking: {"type": "enabled"}` 参数控制,思考内容字段为
`delta.reasoning_content`。二者均为 DeepSeek 特色,其他 OpenAI 兼容厂商
若字段不同,可在子类中覆写 `_parse_delta` 归一化。
"""
def __init__(self, api_key: str, model: str, base_url: str,
timeout: int = 30, config=None, logger=None):
super().__init__(api_key, model, base_url, timeout, config, logger)
@property
def name(self) -> str:
return "DeepSeek"
def _headers(self) -> Dict[str, str]:
return {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
def chat_completion(self, messages: List[Dict], temperature: float = 0.3,
max_tokens: int = 1024, **kwargs) -> str:
payload = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
payload.update(kwargs)
try:
response = requests.post(
self.base_url,
headers=self._headers(),
json=payload,
timeout=30,
proxies=self._resolve_proxies(),
)
if response.status_code == 200:
result = response.json()
return result["choices"][0]["message"]["content"].strip()
self._log("error", f"LLM 压缩请求失败: HTTP {response.status_code}")
return None
except Exception as e:
self._log("error", f"LLM 压缩请求异常: {e}")
return None
def stream_chat(self, messages: List[Dict], temperature: float = 0.7,
max_tokens: int = 8192, tools: Optional[List[Dict]] = None,
tool_choice: Optional[str] = None,
reasoning_enabled: bool = False, **extra) -> Iterator[Dict]:
payload = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": True,
}
# 思考模式
if reasoning_enabled:
payload["thinking"] = {"type": "enabled"}
# 工具调用
if tools:
payload["tools"] = tools
payload["tool_choice"] = tool_choice or "auto"
payload.update(extra)
proxies = self._resolve_proxies()
response = requests.post(
self.base_url,
headers=self._headers(),
json=payload,
timeout=self.timeout,
stream=True,
proxies=proxies,
)
self._current_response = response
if response.status_code != 200:
raise RuntimeError(
f"HTTP {response.status_code}: {response.text}")
try:
for line in response.iter_lines():
if not line:
continue
line_text = line.decode("utf-8")
if not line_text.startswith("data: "):
continue
data = line_text[6:]
if data == "[DONE]":
yield {"type": "done"}
break
try:
chunk = json.loads(data)
except json.JSONDecodeError as e:
self._log("error", f"JSON 解析错误: {e}, 数据: {data}")
continue
choices = chunk.get("choices", [])
if not choices:
continue
delta = choices[0].get("delta", {})
for event in self._parse_delta(delta, reasoning_enabled):
yield event
finally:
self._current_response = None
def _parse_delta(self, delta: Dict, reasoning_enabled: bool) -> Iterator[Dict]:
"""将单条 delta 归一化为统一事件。子类覆写以适配不同厂商字段。"""
# 思考内容
if reasoning_enabled and delta.get("reasoning_content"):
yield {"type": "reasoning", "text": delta["reasoning_content"]}
# 正文
if delta.get("content"):
yield {"type": "content", "text": delta["content"]}
# 工具调用(增量)
for tc in delta.get("tool_calls", []):
index = tc.get("index", 0)
function = tc.get("function", {})
yield {
"type": "tool_call",
"index": index,
"id": tc.get("id", ""),
"name": function.get("name", ""),
"arguments": function.get("arguments", ""),
}
def resolve_api_key(api_key: Optional[str], config=None) -> str:
"""解析 API Key:优先显式传入,其次环境变量,最后 ~/.cert/deepseek.key。"""
if api_key:
return api_key
env_name = None
if config is not None and hasattr(config, "get"):
env_name = config.get("api.api_key_env")
env_name = env_name or "DEEPSEEK_API_KEY"
env_key = os.environ.get(env_name)
if env_key:
return env_key
try:
home = os.environ.get("HOME", os.environ["USERPROFILE"])
key_path = os.path.join(home, ".cert", "deepseek.key")
with open(key_path, "r") as f:
return f.read().strip()
except Exception:
raise RuntimeError(
f"请设置环境变量 {env_name} 或在配置文件中设置 API Key")
def create_llm_client(config, api_key: Optional[str] = None,
logger=None, model: Optional[str] = None) -> LLMClient:
"""根据配置创建对应厂商的 LLM 客户端。可通过 `api.provider` 切换厂商。
Args:
config: 配置对象(需提供 get 方法)。
api_key: 显式传入的 API Key;为 None 时按 resolve_api_key 解析。
logger: 可选日志对象。
model: 可选模型名覆盖;为 None 时使用配置中的 `api.deepseek_model`。
子代理等场景可通过此参数显式指定与主代理不同的模型。
"""
provider = (config.get("api.provider", "deepseek") or "deepseek").lower()
base_url = config.get("api.deepseek_api_url")
model = model or config.get("api.deepseek_model", "deepseek-v4-flash")
timeout = config.get("api.timeout", 30)
resolved_key = resolve_api_key(api_key, config)
if provider in ("deepseek", "deepseek-chat"):
return DeepSeekClient(
api_key=resolved_key,
model=model,
base_url=base_url,
timeout=timeout,
config=config,
logger=logger,
)
# 其他 OpenAI 兼容厂商:可直接复用 DeepSeekClient(继承其协议解析),
# 或在此接入专有实现。新增厂商时在此扩展即可。
raise ValueError(f"未知的模型提供商: {provider}")
-1
View File
@@ -1,7 +1,6 @@
# backend
websockets
requests
beautifulsoup4
# frontend
websockets>=11.0
+113
View File
@@ -0,0 +1,113 @@
# sysinfo - 获取系统运行时间、CPU、内存、磁盘、网络、负载、温度、电池等状态信息的技能
## 功能概述
本技能用于查询 Linux 服务器/本机的系统状态信息。它通过 `psutil` 库和系统命令采集数据,支持按需获取单项或组合信息,支持文本和 JSON 两种输出格式。
**依赖要求**:需要 Python3 环境,并安装 `psutil` 库(`pip install psutil`)。若未安装,可改用 `/proc`、`free`、`df`、`uptime`、`top` 等系统命令替代(见下文备选方案)。
## 信息类型及对应获取方法
### 1. 系统基本信息 (system)
- 系统名称、主机名、内核版本、硬件架构、处理器型号、Python 版本
- 命令:`uname -a`、`hostname`、`python3 --version`
### 2. 运行时间 (uptime)
- 开机时间、已运行时长(天/时/分/秒)
- 命令:`uptime -s`(开机时间)、`uptime`(运行时长)
### 3. CPU 信息 (cpu)
- 物理核心数、逻辑核心数、实时使用率、当前频率
- 命令:`lscpu`、`nproc`、`top -bn1 | head -5`、`cat /proc/cpuinfo`
### 4. 内存信息 (memory)
- 总内存、可用内存、已用内存、内存使用率、交换分区大小及使用率
- 命令:`free -h`
### 5. 指定磁盘信息 (disk)
- 指定路径(默认 `/`)的总空间、已用、可用、使用率
- 命令:`df -h /路径`
### 6. 所有磁盘分区 (disks)
- 所有挂载点的文件系统、总空间、已用、可用、使用率
- 命令:`df -hT`
### 7. 进程信息 (process)
- 进程总数、运行中进程数、睡眠进程数
- 命令:`ps -e | wc -l`、`ps -eo stat | grep -c '^R'`、`ps -eo stat | grep -c '^S'`
### 8. 网络信息 (network)
- 累计发送/接收的数据量(MB)、发送/接收包数
- 命令:`cat /proc/net/dev`、`ip -s link`
### 9. 系统负载 (load)
- 1/5/15 分钟负载及每核心负载
- 命令:`cat /proc/loadavg`、`uptime`
### 10. 温度信息 (temperature)
- CPU/主板等传感器温度(仅支持部分硬件)
- 命令:`sensors`(需安装 lm-sensors)、`cat /sys/class/thermal/thermal_zone*/temp`
### 11. 电池信息 (battery)
- 电量百分比、是否充电中、剩余时间(仅笔记本)
- 命令:`cat /sys/class/power_supply/BAT0/capacity`、`cat /sys/class/power_supply/BAT0/status`
## 使用方式
### 方式一:直接运行原版 Python 脚本(若旧版技能仍可用)
```bash
# 获取系统基本信息
python3 /opt/micro_servers/aibox/skills/info.py --info system
# 获取 CPU + 内存 + 磁盘信息
python3 /opt/micro_servers/aibox/skills/info.py --info cpu --info memory --info disk
# 指定磁盘路径
python3 /opt/micro_servers/aibox/skills/info.py --info disk --disk-path /home
# 获取所有信息(JSON 格式,便于程序解析)
python3 /opt/micro_servers/aibox/skills/info.py --info all --format json
# 获取负载 + 温度 + 电池
python3 /opt/micro_servers/aibox/skills/info.py --info load --info temperature --info battery
```
### 方式二:使用系统命令(无 psutil 时的备选方案)
```bash
# 综合系统信息
uname -a && hostname && uptime && free -h && df -hT && nproc
```
## 参数说明(原版脚本)
| 参数 | 简写 | 说明 |
|------|------|------|
| `--info` / `-i` | 可多次 | 信息类型:system/uptime/cpu/memory/disk/disks/process/network/load/temperature/battery/all |
| `--disk-path` / `-d` | 默认 `/` | 磁盘检查路径 |
| `--format` / `-f` | 默认 text | 输出格式:text 或 json |
| `--help` / `-h` | - | 显示帮助与示例 |
## 输出示例(文本格式)
```
系统信息:
系统: Linux
主机名: my-server
架构: x86_64
CPU信息:
物理核心数: 4
逻辑核心数: 8
使用率: 12.5%
内存信息:
总内存: 15.58 GB
可用内存: 8.21 GB
内存使用率: 45.2%
```
## 典型使用场景
1. **运维巡检**:配合定时任务,每日采集系统资源快照,监控负载与内存趋势
2. **故障排查**:磁盘空间告警时,快速定位使用率最高的挂载点
3. **性能评估**:获取 CPU 核心数、内存大小、网络吞吐等硬件基线数据
4. **上报集成**:使用 `--format json` 输出结构化数据,便于脚本解析和上报
## 注意事项
- 温度/电池信息仅在支持对应硬件的系统上可用,服务器通常无电池
- CPU 使用率为瞬时采样(间隔 1 秒),如需平均值建议多次采样
- 网络数据为累计值,对比时需记录时间差
+97
View File
@@ -0,0 +1,97 @@
# tech-manual - 生成Linux命令中文技术手册的技能(输入命令名,输出完整Markdown手册)
## 用途
当用户需要了解某个 Linux 命令的用法时,本技能指导智能体生成该命令的**完整中文技术手册**(Markdown 格式),涵盖语法、全部常用选项、环境变量、退出状态、实战示例和相关命令,内容基于 GNU coreutils / man pages 官方文档,准确实用。
## 适用场景
- 用户问:"帮我看看 xx 命令怎么用"
- 用户说:"生成 xx 命令的技术手册"
- 用户问:"xx 命令有哪些选项/参数"
- 用户想深入学习某个命令(ls、find、grep、tar、awk、sed、du、df、ps、top、netstat、ss 等)
## 执行步骤
1. **确认命令名**:从用户输入中提取目标命令名(如 `ls`、`find`)。若用户未指定,可用 ask_user 询问。
2. **收集资料**(可选):若对命令不熟悉,可运行 `man <命令> | col -b | head -200` 或 `info <命令>` 获取权威信息;也可用 `run_command` 执行 `命令 --help` 查看选项。
3. **生成手册内容**:按以下固定结构编写 Markdown 手册:
### 手册结构(8 个章节)
```
# <命令> 命令技术手册
## 1. 名称(NAME)
<命令> — 一句话功能说明
## 2. 语法(SYNOPSIS)
```bash
<命令> [选项]... [参数]...
```
给出标准调用形式,说明缺省参数时的行为。
## 3. 描述(DESCRIPTION)
命令的功能、工作原理、关键特性(3-6 条要点,可用列表)。
## 4. 选项(OPTIONS)
分类列出所有常用与关键选项(至少 20 个),每个选项格式:
#### `-x, --long-option`
作用说明(一句话)。
```bash
<命令> -x
```
可选:典型输出示例。
分类建议(按命令特点调整):
- 基础选项
- 排序/过滤选项
- 输出格式选项
- 时间/状态选项
- 长格式字段说明(如 ls -l、ps -ef 的列含义)
- 权限/特殊位解析(如适用)
## 5. 环境变量(ENVIRONMENT)
影响该命令行为的环境变量,如 LS_COLORS、COLUMNS、TZ、LANG/LC_* 等。
## 6. 退出状态(EXIT STATUS)
| 状态码 | 含义 |
|--------|------|
| 0 | 成功 |
| 1 | 轻微错误(如文件不存在) |
| 2 | 严重错误(如参数错误) |
## 7. 示例(EXAMPLES)
至少 15 个真实场景示例,按类别分组(日常使用/排序查找/递归/时间/格式/管道脚本/实用技巧),每个示例用 ```bash 代码块并附注释。
## 8. 相关命令(SEE ALSO)
以表格列出相关命令及其用途。
---
*本文档基于 GNU coreutils/man pages 编写,适用于大多数 Linux 发行版。BSD/macOS 部分选项有差异,以 man 手册为准。*
```
4. **写入文件**:将手册写入工作区文件 `~/.aibox/workspace/<命令名>_manual.md`。
- 内容较长时**分多次追加写入**(每次 200-400 行),避免 heredoc 超限。
- 写入后校验:`wc -l <文件>` 和 `grep -E "^#{1,3} " <文件>` 检查行数和章节完整性。
5. **汇报结果**:告知用户文件路径、总行数、章节概览,并询问是否需要进一步操作(如安装为技能、补充其他命令)。
## 注意事项
- 内容必须**准确**,基于 GNU coreutils / 官方 man pages,不要臆造选项。
- 代码示例统一用 ` ```bash ` 代码块。
- 若命令是 shell 内建(如 cd、echo)或非 coreutils 命令,结构可适当调整,但保持 8 章节框架。
- 写入工作区使用 `~/.aibox/workspace/` 目录,遵守工作目录规范。
## 典型示例
用户:"帮我生成 find 命令的技术手册"
→ 执行本技能,输出 `~/.aibox/workspace/find_manual.md`
用户:"grep 有哪些常用选项?"
→ 执行本技能,生成 grep 手册并重点展示选项章节
用户:"看看 tar 命令怎么打包解压"
→ 执行本技能,生成 tar 手册并重点展示示例章节
+2
View File
@@ -0,0 +1,2 @@
# 数据库备份输出目录
# 容器内备份会输出到此目录,可直接复制走
+38
View File
@@ -0,0 +1,38 @@
# prjs
nn/
bot
bot?
test_*
.git/
.vscode/
.idea/
.cache/
#
*.so
*.o
*.cjo
*.dll
*.exe
*.lib
*.exp
main
# cangjie
build/
target/
output/
module-resolve.json
module-lock.json
cjpm.lock
.cjpm-history
*.macrocall
*.gcda
*.gcno
# 其他
log
report
tmp
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 卿松
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+60
View File
@@ -0,0 +1,60 @@
MODE ?= cpu
VERSION := 0.0.2
APP_NAME := bot
CC:= gcc
CXX:= g++
NVCC:= nvcc
LIBS := -lpthread
FLAGS := -O3 -pipe #-Wall -Werror
#-----------
all: libi2c4cj.so libsnd4cj.so libmt4cj.so libfft4cj.so #libcnn4cj.so
# I2C
libi2c4cj.so: src/cpp/I2C.c
$(CC) $(FLAGS) -shared -fPIC $^ -o $@
# SOUND
libsnd4cj.so: src/cpp/snd.cpp
$(CC) $(FLAGS) -shared -fPIC $^ -o $@
# cpu nn
ifeq ($(MODE), cpu)
libmt4cj.so: src/cpp/Matrix.cpp
$(CC) $(FLAGS) -shared -fPIC $^ -o $@
endif
ifeq ($(MODE), cuda)# GPU矩阵
libmt4cj.so: src/cuda/matrix.cu
#$(NVCC) -arch=compute_61 -code=sm_61 -O3 -Xcompiler -fPIC -shared src/cuda/matrix.cu -o libmt4cj.so
$(NVCC) -fPIC -shared src/cuda/matrix.cu -o libmt4cj.so
endif
# 快速傅里叶变换fft
libfft4cj.so: src/cpp/fft1.c src/cpp/fft.cpp
$(CC) $(FLAGS) -shared -fPIC $^ -lm -o $@
# 卷积神经网络cnn
libcnn4cj.so: src/cpp/cnn/cnn_i.cpp conv.o pooling.o ga_pooling.o flatten.o imgl.o
$(CC) $(FLAGS) -std=c++17 -shared -fPIC $^ -lm -o $@
conv.o: src/cpp/cnn/conv.cpp
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
pooling.o: src/cpp/cnn/pooling.cpp
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
ga_pooling.o: src/cpp/cnn/ga_pooling.cpp
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
flatten.o: src/cpp/cnn/flatten.cpp
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
imgl.o: src/cpp/cnn/imgl.cpp
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
clear:
rm -f *.o
rm -f *.so
+134
View File
@@ -0,0 +1,134 @@
# bot4cj
bot4cj是基于仓颉和 Linux I2C 接口的机器人硬件脚本解析器。
## 依赖库
libc、libpthread、liblua-5.4
You may need this
``` bash
sudo ln -s /usr/lib/aarch64-linux-gnu/liblua5.4.so /usr/lib/aarch64-linux-gnu/liblua.so
sudo ln -s /usr/lib/x86_64-linux-gnu/liblua5.4.so /usr/lib/x86_64-linux-gnu/liblua.so
```
## 项目介绍
想要得到一个机器人软件通常比一个普通的软件更为困难,这是因为这个世界上的机器人五花八门,所以它们的软件往往都是独特的,所以我想写一种用于描述机器人硬件的通用的脚本。
`bot4cj` 基于仓颉编程语言和 Linux I2C 接口,提供了便捷的硬件控制能力,支持pca9685、mpu6050,我通常使用树莓派作为宿主机。
## 解析器
树莓派通过I2C接口连接pca9685后使用如下方法使用:
```bash
cat ss/test.mt | cjpm run # 蜘蛛机器人脚本
```
## 动作脚本
项目中包含的动作脚本在ss文件夹下面,这些动作脚本的适配机器人型号如下
```textplain
# PCA9685
1号舵机(s1) => 左下水平关节
2号舵机(s2) => 左下垂直关节
3号舵机(s3) => 右下水平关节
4号舵机(s4) => 右下垂直关节
5号舵机(s5) => 右上水平关节
6号舵机(s6) => 右上垂直关节
7号舵机(s7) => 左上水平关节
8号舵机(s8) => 左上垂直关节
9号舵机(s9) => 云台1
10号舵机(s10) => 云台2
扬声器(spk) => 仅负责延时
```
适用的机器人建模文件[仓库链接](https://github.com/gopsc/Dashun)
### 机器人脚本语法(.mt)
**.bot文件是旧版动作脚本**
机器人脚本是行列式表格,每列之间以一个或多个空格、制表符分隔,每行以分号(;)结尾。
每个矩阵对象以@name 行开头,@ 行结束。
示例:
```mt
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
# 定义I2C硬件
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
# 定义pca9685硬件
@PCA9685
name I2C path;
pca9685-0 i2c-1 0x40;
@
# 定义mpu6050硬件
@MPU6050
name I2C path;
mpu6050-0 i2c-1 0x69;
mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
# 定义扬声器硬件
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
# 定义舵机硬件
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵为硬件变量输入参数
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
90 - - - - - - - - - - 20; # left-behind
45 - - - - - - - - - 0 500; # stand
- - 90 - - - 45 - - - - 20; # right-behind
- - 45 - - - - - - - 200 500; # stand
- - - - 90 - - - - - 0 20; # right-front
- - - - 45 - - - - - - 500; # stand
- - - - - - 90 - - - - 20; # left-front
- - - - - - 45 - - - - 500; # stand
- - - - - - - - - - - 20; # stand
@
```
+384
View File
@@ -0,0 +1,384 @@
# bot4cj — 机器人硬件脚本解析器
基于**仓颉(Cangjie)** 编程语言和 Linux I2C 接口的机器人动作脚本解析器。用一种矩阵式脚本来描述机器人的硬件构成与动作序列,解析后驱动真实硬件执行。
## 架构分层
```
┌──────────────────────────────────────────────────────────────┐
│ .mt 矩阵脚本(硬件定义 + 动作序列) │
├──────────────────────────────────────────────────────────────┤
│ MatrixParser(P.cj) — FSM 逐字符解析 → MatrixObject 链表 │
├──────────────────────────────────────────────────────────────┤
│ RobotStruct(r.cj) — 从矩阵对象构建硬件变量(createHardVars) │
│ MatrixVars_Hardware 基类 + 闭包回调: │
│ · build() → rise 回调(打开硬件、获取资源) │
│ · play() → things 回调(执行动作,参数/返回值均为 JsonValue) │
│ · free() → free 回调(释放资源) │
├──────────────────────────────────────────────────────────────┤
│ 硬件实现:hd/*.cj │
│ PCA9685(I2C 0x40, 50Hz PWM, 角度→脉宽 0.5~2.5ms) │
│ Servo(舵机:max/min/offset 校准 + 角度翻转) │
│ MPU6050(陀螺+加速度,±500°/s ±8g,后台 10ms 采样线程+卡尔曼) │
│ Speaker(正弦波发声,5ms 淡入淡出) │
│ fb.cj(外部硬件扩展口) │
├──────────────────────────────────────────────────────────────┤
│ 系统库:cpp/ + cuda/ │
│ libi2c4cj.so — I2C glibc 封装 + 20 槽锁数组 │
│ libmt4cj.so — FP32 矩阵运算(CPU / CUDA 二选一) │
│ libfft4cj.so — 基-2 Cooley-Tukey FFT │
│ libcnn4cj.so — Conv/Pool/GA-Pool/Flatten + Adam/Momentum │
│ libsnd4cj.so — WaveAPI / ALSA 音频播放 │
├──────────────────────────────────────────────────────────────┤
│ 数学/NN 库:math/*.cj │
│ Matrix1(二维矩阵)+ Tensor + 6 种激活函数 + Xavier 初始化 │
│ Nn1 全连接层(前向/反向/权重更新) │
│ Af(Sigmoid/Tanh/ReLU/Leaky_ReLU/Swish/Softmax) │
└──────────────────────────────────────────────────────────────┘
```
## 技术栈
| 层 | 技术 | 说明 |
|----|------|------|
| 应用层 | **仓颉 Cangjie** | 主程序、解析器、机器人结构、数学库、HTTP 服务 |
| 系统层 | C / C++ | I2C 驱动、矩阵运算、FFT、CNN、音频 |
| 加速层 | CUDA(可选) | `MODE=cpu/cuda` 切换 |
| 脚本层 | `.mt` 矩阵脚本 | 硬件定义 + 动作序列 |
| 依赖包 | ctype4cj / pthread4cj / spi4cj / lua4cj | 仓颉官方扩展包 |
| 构建 | Makefile(C/C++ 共享库)+ cjpm.toml(仓颉包管理) | — |
## 目录结构
```
bot4cj/
├── src/
│ ├── main.cj # 入口:CLI 参数解析(-i/-o/--test)+ stdin 管道执行
│ ├── P.cj # 矩阵脚本解析器(FSM 逐字符解析)
│ ├── r.cj # RobotStruct + MatrixVars_Hardware 基类
│ ├── testCNN.cj # CNN 测试入口
│ ├── testCNN1.cj # 几何 CNN 测试
│ ├── hd/
│ │ ├── I2C.cj # I2C 封装 + 20 槽锁数组
│ │ ├── PCA9685.cj # PCA9685 舵机控制板(已标记由 Lua 替代)
│ │ ├── Servo.cj # 舵机:校准 + 角度翻转
│ │ ├── MPU6050.cj # MPU6050 陀螺+加速度+卡尔曼滤波
│ │ ├── snd.cj # 扬声器
│ │ ├── fb.cj # 外部硬件扩展
│ │ └── extern/
│ │ └── macro_ExternHardware.cj
│ ├── math/
│ │ ├── Matrix.cj # 二维矩阵(+运算符重载)
│ │ ├── Tensor.cj # 张量
│ │ ├── Af.cj # 6 种激活函数
│ │ ├── Ini.cj # Xavier/Kaiming 初始化
│ │ ├── nn.cj # 全连接层 Nn1
│ │ ├── nns.cj # 旧版神经网络(XNOR 门测试)
│ │ ├── cnn.cj # CNN 仓颉绑定
│ │ ├── pooling.cj # 池化
│ │ ├── flatten.cj # Flatten
│ │ ├── fft.cj # FFT 仓颉绑定
│ │ ├── kalman.cj # 卡尔曼滤波
│ │ └── Adam.cj # Adam 优化器
│ ├── cpp/ # C/C++ 系统库源
│ │ ├── I2C.c # I2C glibc 封装
│ │ ├── Matrix.cpp # CPU 矩阵运算
│ │ ├── fft1.c # FFT 单实序列
│ │ ├── fft.cpp # FFT 双实序列 + 基-2 Cooley-Tukey
│ │ ├── kalman.c # 卡尔曼滤波
│ │ ├── snd.cpp # 音频播放(WaveAPI/ALSA)
│ │ └── cnn/ # 纯 C++ CNN(Conv/Pool/GA-Pool/Flatten)
│ └── cuda/
│ ├── matrix.cu # GPU 矩阵运算 kernel
│ └── error.cuh # CUDA 错误检查宏
├── modules/ # Lua 硬件模块(ADS1115/ADS1256/PCA9685/SPI)
├── scripts/ # .mt 动作脚本(std/jump/lie-down/tilt-* 等)
├── test/ # C/Lua 测试脚本
├── docs/Reports/ # 深度分析报告
├── cjpm.toml # 仓颉包配置
├── Makefile # C/C++ 共享库构建
├── _set.sh # 编译(cjpm update + build)
├── _run.sh # 运行(stdin 管道)
├── compile.ps1 # Windows 编译脚本
├── run.ps1 # Windows 运行脚本
├── README.md # 旧版说明
└── README.robot.md # 本文件
```
## 快速开始
```bash
# 1. 编译 C/C++ 共享库(一次性)
make MODE=cpu # CPU 模式(默认)
# 或
make MODE=cuda # GPU 模式(需要 nvcc)
# 2. 编译仓颉程序
bash _set.sh
# 3. 运行动作脚本(stdin 管道)
cat scripts/std.mt | bash _run.sh
```
> **硬件要求**:树莓派(aarch64)或其他 Linux I2C 设备。x86_64 开发板需自行验证 I2C 参数。Windows 暂不支持(依赖 glibc)。
## CLI 参数
```
./bot # 从 stdin 读取 .mt 脚本执行
./bot --help # 打印帮助
./bot --test nn # 运行神经网络与门测试
./bot --test cnn # 运行 CNN 测试
./bot --test cnn1 # 运行几何 CNN 测试
./bot --test matrix_o # 解析 ss/test.mt 并保存为 ss/output.mt
./bot --test matrix_2 # 解析 ss/output.mt → 执行 → 再次保存
./bot -i model.json -o out.json # 加载/保存神经网络模型(DRL 预留)
```
## .mt 脚本语法
行列式表格,每列以空格或制表符分隔,每行以分号 `;` 结尾。
### 块结构
```
@BlockName
column1 column2 column3;
value1 value2 value3;
@
```
每个 `@` 块由 `@Name` 行开始,`@` 行结束。支持的块类型:
| 块 | 说明 |
|----|------|
| `@Alias` | 列名别名(如 `A` → `angle`、`F` → `frequency`) |
| `@I2C` | 定义 I2C 设备(name + path) |
| `@PCA9685` | 定义 PCA9685 舵机控制板(name + I2C 引用 + 地址) |
| `@MPU6050` | 定义 MPU6050 传感器(name + I2C 引用 + 地址) |
| `@Speaker` | 定义扬声器(channels + sample_rate + volume + max_f + min_f) |
| `@Servo` | 定义舵机(name + max + min + offset + controller + channel) |
| `@Action` | **动作矩阵**:列头为硬件变量的属性,行为每帧的参数 |
### 占位符
`-` 表示"沿用上一行同列的值",当前行该列不产生调用。
### 完整示例
```mt
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0;
s2 180 - - - 1;
s3 -90 - - - 2;
s4 -180 - - - 3;
@
@Action
s1.A s2.A s3.A s4.A spk.F spk.delay;
45 - 45 - 10 200; # 站立
90 - - - - 20; # 左转
@
```
## 执行流程
```
stdin.read()
↓
P.MatrixParser (FSM 逐字符解析)
↓
LinkedList<MatrixObject> 链表
↓
RobotStruct.createHardVars() # 按 @I2C/@PCA9685/@Servo/@Speaker/@MPU6050 构建硬件变量
↓
robot.active() # build():逐个硬件调用 rise 回调(打开 I2C、初始化 PWM 等)
↓
robot.run() # 逐行遍历 @Action 矩阵,每列调用对应硬件的 play() 回调
↓
robot.deactive() # free():倒序释放资源
```
`play()` 的输入输出均为 `JsonValue`,实现了硬件与脚本之间的统一通信协议。
## 已支持的硬件
### PCA9685(I2C 舵机控制板)
- 地址:默认 `0x40`
- PWM 频率:50Hz(舵机标准 20ms 周期)
- 舵机脉宽映射:0°→0.5ms,180°→2.5ms
- 角度映射到 12 位 PWM 值(0-4095)
- **注意**:PCA9685.cj 代码已标记"由 Lua 脚本取代",保留作备用
### Servo(舵机)
- 支持 `max` / `min` 角度限制与 `offset` 漂移校准
- `max < 0` 时自动翻转角度(反向安装的舵机)
- 通过闭包回调与实际硬件(PCA9685/Lua 模块)解耦
### MPU6050(6 轴传感器)
- 陀螺仪:±500°/s 量程
- 加速度计:±8g 量程
- 后台 10ms 采样线程(pthread_create + pthread_cancel)
- 卡尔曼滤波融合数据
- **已知问题**:`ADDR_0` 与 `ADDR_1` 常量均为 `0x68`([MPU6050.cj:14-15](src/hd/MPU6050.cj#L14-L15)),第二个地址应改为 `0x69`
### Speaker(扬声器)
- 支持正弦波发声(指定频率 `F` + 延时 `delay`)
- 5ms 淡入淡出,避免"咔哒"声
- 通道数/采样率/音量可在 `.mt` 中配置
- **已知问题**:`play()` 仅 sleep 指定延时,真实发声依赖底层 C++ 实现
## 机器人型号适配
### Dashun 蜘蛛机器人(默认)
10 舵机 + 2 云台 + 扬声器,建模文件见 [gopsc/Dashun](https://github.com/gopsc/Dashun):
| 舵机 | 关节 | 舵机 | 关节 |
|------|------|------|------|
| s1 | 左下水平 (rb0) | s6 | 右上垂直 (lf1) |
| s2 | 左下垂直 (rb1) | s7 | 左上水平 (rf0) |
| s3 | 右下水平 (lb0) | s8 | 左上垂直 (rf1) |
| s4 | 右下垂直 (lb1) | s9 | 云台1 (cam0) |
| s5 | 右上水平 (lf0) | s10 | 云台2 (cam1) |
### 可用脚本
`scripts/` 目录下预写了多种动作脚本:
- `std.mt` — 站立姿态
- `jump.mt` — 跳跃
- `lie-down.mt` — 趴下
- `left-hand.mt` / `rght-hand.mt` — 单手动作
- `left-turn.mt` / `tilt-f.mt` / `tilt-b.mt` / `tilt-l.mt` / `tilt-r.mt` — 转向与倾斜
## 构建
### 依赖
```bash
# 仓颉 1.1.3
# 仓颉官方扩展包:ctype4cj / pthread4cj / spi4cj / lua4cj
# libc / libpthread
# liblua-5.4(可选,Lua 硬件模块支持)
sudo ln -s /usr/lib/aarch64-linux-gnu/liblua5.4.so /usr/lib/aarch64-linux-gnu/liblua.so
sudo ln -s /usr/lib/x86_64-linux-gnu/liblua5.4.so /usr/lib/x86_64-linux-gnu/liblua.so
```
### 构建步骤
```bash
# 1. 构建 C/C++ 共享库
make MODE=cpu
# 产出:libi2c4cj.so libmt4cj.so libfft4cj.so libsnd4cj.so
# 2. (可选)构建 CNN
make libcnn4cj.so
# 3. (可选)CUDA 模式需 NVIDIA CUDA Toolkit
make MODE=cuda
# 4. 仓颉主程序
bash _set.sh
```
[_set.sh](_set.sh) 执行 `cjpm update` + `cjpm build`,自动解析 cjpm.toml 依赖。
### 运行
```bash
bash _run.sh scripts/std.mt
# 或
cat scripts/std.mt | bash _run.sh
```
[_run.sh](_run.sh) 以 `cjStackSize=64MB` / `cjHeapSize=64MB` 启动。
## 神经网络支持
项目内置了全连接层 Nn1([math/nn.cj](src/math/nn.cj))与 CNN([math/cnn.cj](src/math/cnn.cj) + [cpp/cnn/](src/cpp/cnn/)):
- **Nn1**:多层感知机,支持前向传播、反向传播、权重更新
- **CNN**:Conv → ReLU → Pool → GA-Pool → Flatten → FC
- **优化器**:Adam / Momentum
- **激活函数**:Sigmoid / Tanh / ReLU / Leaky_ReLU / Swish / Softmax
- **初始化**:Xavier / Kaiming
### 测试
```bash
./bot --test nn # 与门训练(2 输入 → 1 输出)
./bot --test cnn # CNN 前向/反向测试
./bot --test cnn1 # 几何分类 CNN 测试
```
### DRL 预留(未完成)
main.cj 中预留了 DRL 流程(注释状态):加载预训练权重 → 执行动作 → 保存微调模型。通过 `-i` / `-o` 参数指定输入/输出路径。
## 安全与限制
### I2C 总线保护
- [hd/I2C.cj](src/hd/I2C.cj) 内置 **20 槽锁数组**,多硬件共享 I2C 总线时自动互斥
- 每个 I2C fd 独立加锁,避免并发写入冲突
### 硬件关闭时机
- `MatrixVars_Hardware` 析构依赖仓颉 GC,**不保证**在确定性时机调用 `free()`
- 机器人使用 `try/finally` 在 [main.cj:80-87](src/main.cj#L80) 中显式调用 `deactive()`,确保硬件资源正确释放
### 线程安全
- MPU6050 采样通过 `pthread_create` 启动后台线程
- `pthread_cancel` 依赖系统定义的取消点(sleep / 系统调用),可能在非安全点中断
### 平台支持
- ✅ Linux aarch64(树莓派)/ x86_64
- ❌ Windows(依赖 glibc)
## 已知问题
| ID | 描述 | 影响 |
|----|------|------|
| D1 | 仓颉 GC 不保证析构器调用,I2C fd 可能泄漏 | 长时间运行后 I2C 资源耗尽 |
| D2 | Speaker `play()` 仅 sleep,真实发声未接入 C++ | 扬声器无实际声音 |
| D3 | MPU6050 `pthread_cancel` 可能在非安全点中断 | 传感器数据不一致 |
| D4 | PCA9685 初始化被标记为"由 Lua 取代",但仍保留主路径 | 代码存在双实现 |
| D5 | `_set.sh` 会删除 `a.out` | 误删用户产物 |
| D6 | Softmax 雅可比矩阵实现错误(math/Af.cj) | NN 训练收敛异常 |
| D7 | Tensor 乘法仅支持 2D,3D+ 会 panic | 限制模型复杂度 |
| D8 | Tanh 导数实现错误(使用 `1 - y^2` 而非 `1 - tanh^2(x)`) | NN 训练收敛异常 |
| D9 | CNN `testGeometryCNN` 使用随机数据 | 测试无实际意义 |
| D10 | MPU6050 `ADDR_0` 与 `ADDR_1` 均为 `0x68` | 无法同时使用两个 MPU6050 |
| D11 | `@Action` 与 `@Action_Auto` 并存未合并 | 脚本语法冗余 |
## 相关资源
- [Dashun 蜘蛛机器人建模](https://github.com/gopsc/Dashun)
- [深度分析报告](docs/Reports/analysis_20260525_00.md)(28 章节,1280 行)
- [仓颉编程语言](https://cangjie-lang.org/)
- [PCA9685 数据手册](https://www.nxp.com/docs/en/data-sheet/PCA9685.pdf)
- [MPU6050 数据手册](https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf)
## 许可证
MIT License
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
export cjStackSize=64MB
export cjHeapSize=64MB
exec cjpm run --run-args="$@"
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
export cjHeapSize=16MB
cjpm update
exec cjpm build
+39
View File
@@ -0,0 +1,39 @@
[package]
cjc-version = "1.1.3"
name = "bot4cj"
description = "nothing here"
version = "0.0.3"
target-dir = ""
src-dir = ""
output-type = "executable"
compile-option = "-l c -l pthread -l lua" # Linux
override-compile-option = ""
link-option = ""
package-configuration = {}
[dependencies]
#ctype4cj = { git = "https://gitcode.com/qingss0/ctype4cj.git", branch="main" }
ctype4cj = "1.0.3"
#pthread4cj = { git = "https://gitcode.com/qingss0/pthread4cj.git", branch="main"}
pthread4cj = "1.0.0"
#spi4cj = { git = "https://gitcode.com/qingss0/spi4cj.git", branch="main"}
spi4cj = "1.0.0"
#lua4cj = { git = "https://gitcode.com/qingss0/lua4cj.git", branch="main" }
lua4cj = "1.0.0"
# 这种x86机器人开发板感觉不经常使用,我没用过,不知道里面的i2c调用是不是参数是一样的
[target.x86_64-unknown-linux-gnu]
[target.x86_64-unknown-linux-gnu.dependencies]
[target.x86_64-unknown-linux-gnu.bin-dependencies]
path-option = ["${CANGJIE_STDX_PATH}/linux_x86_64_cjnative/static/stdx"]
[target.aarch64-unknown-linux-gnu]
[target.aarch64-unknown-linux-gnu.dependencies]
[target.aarch64-unknown-linux-gnu.bin-dependencies]
path-option = ["${CANGJIE_STDX_PATH}/linux_aarch64_cjnative/static/stdx"]
# 为什么不支持Windows了?
# 因为新版我们加载了Linux的glibc,通常Windows中没有这个
#[target.x86_64-w64-mingw32]
# [target.x86_64-w64-mingw32.bin-dependencies]
# path-option = ["${CANGJIE_STDX_PATH}\\windows_x86_64_cjnative\\dynamic\\stdx"]
+11
View File
@@ -0,0 +1,11 @@
nvcc -shared -o libmt4cj.dll src/cuda/matrix.cu -allow-unsupported-compiler
#nvcc -shared -o libmt4cj.dll src/cpp/Matrix.cpp -allow-unsupported-compiler
nvcc -shared -o libi2c4cj.dll src/cpp/I2C.c -allow-unsupported-compiler
nvcc -shared -o libfft4cj.dll src/cpp/fft.cpp src/cpp/Matrix.cpp src/cpp/fft1.c -allow-unsupported-compiler
nvcc -shared -o conv.lib -c src/cpp/cnn/conv.cpp -allow-unsupported-compiler
nvcc -shared -o imgl.lib -c src/cpp/cnn/imgl.cpp -allow-unsupported-compiler
nvcc -shared -o pooling.lib -c src/cpp/cnn/pooling.cpp -allow-unsupported-compiler
nvcc -shared -o ga_pooling.lib -c src/cpp/cnn/ga_pooling.cpp -allow-unsupported-compiler
nvcc -shared -o flatten.lib -c src/cpp/cnn/flatten.cpp -allow-unsupported-compiler
nvcc -shared --std=c++17 -o libcnn4cj.dll src/cpp/cnn/cnn_i.cpp conv.lib imgl.lib pooling.lib ga_pooling.lib flatten.lib -allow-unsupported-compiler
nvcc -shared -o libsnd4cj.dll src/cpp/snd.cpp -allow-unsupported-compiler
+325
View File
@@ -0,0 +1,325 @@
-- ADS1115模数转换器 硬件脚本(测试)
-- 这个函数是辅助用的,它能够打印一张表出来
function print_table(t, indent)
indent = indent or 0
for k, v in pairs(t) do
if type(v) == 'table' then
print(string.rep(' ', indent) .. k .. ":")
print_table(v, indent + 2)
else
print(string.rep(' ', indent) .. k .. ": " .. tostring(v))
end
end
end
ADS1115_ADDR = 0x48 -- ADS1115的I2C地址
ADS1115_REG_CONVERSION = 0x00 -- 转换结果(只读)
ADS1115_REG_CONFIG = 0x01 -- 配置寄存器(读/写)
ADS1115_REG_LO_THRESH = 0x02 -- 低阈值比较
ADS1115_REG_HI_THRESH = 0x03 -- 高阈值比较
-- ====================================
-- ADS1115 配置寄存器 bit 定义(16位)
-- ====================================
-- BIT15: OS[0] => 单次转换启动位(1=开始)
-- BIT14-12: MUX[2:0] => 输入多路选择
-- BIT11-9: PGA[2:0] => 可编程增益放大器
-- BIT8: MODE => 0=连续转换,1=单次(省电)
-- BIT7-5: DR[2:0] => 数据速率
-- BIT4: COMP_MODE => 比较模式(0=传统)
-- BIT3: COMP_POL => 比较极性(0=低有效)
-- BIT2: COMP_LAT => 比较锁存(0=不锁存)
-- BIT1-0: COMP_QUE => 比较器队列(11=禁用)
--
-- ==== MUX 输入通道 ====
ADS1115_MUX_AIN0_AIN1 = 0x0000 -- 差分:P=AIN0,N=AIN1(默认)
ADS1115_MUX_AIN0_AIN3 = 0x1000 -- 差分:P=AIN0,N=AIN3
ADS1115_MUX_AIN1_AIN3 = 0x2000 -- 差分:P=AIN1,N=AIN3
ADS1115_MUX_AIN2_AIN3 = 0x3000 -- 差分:P=AIN2,N=AIN3
ADS1115_MUX_AIN0_GND = 0x4000 -- 单端:AIN0
ADS1115_MUX_AIN1_GND = 0x5000 -- 单端:AIN1
ADS1115_MUX_AIN2_GND = 0x6000 -- 单端:AIN2
ADS1115_MUX_AIN3_GND = 0x7000 -- 单端:AIN3
-- ==== PGA 增益(满量程范围)====
ADS1115_PGA_6_144V = 0x0000 -- ±6.144V (1LSB=0.1875mV)
ADS1115_PGA_4_096V = 0x0200 -- ±4.096V (1LSB=0.125mV)
ADS1115_PGA_2_048V = 0x0400 -- ±2.048V (1LSB=0.0625mV)(默认)
ADS1115_PGA_1_024V = 0x0600 -- ±1.024V (1LSB=0.03125mV)
ADS1115_PGA_0_512V = 0x0800 -- ±0.512V (1LSB=0.015625mV)
ADS1115_PGA_0_256V = 0x0A00 -- ±0.256V (1LSB=0.0078125mV)
-- ==== 数据速率 ====
ADS1115_DR_8SPS = 0x0000 -- 8 SPS
ADS1115_DR_16SPS = 0x0020 -- 16 SPS
ADS1115_DR_32SPS = 0x0040 -- 32 SPS
ADS1115_DR_64SPS = 0x0060 -- 64 SPS
ADS1115_DR_128SPS = 0x0080 -- 128 SPS (默认)
ADS1115_DR_250SPS = 0x00A0 -- 250 SPS
ADS1115_DR_475SPS = 0x00C0 -- 475 SPS
ADS1115_DR_860SPS = 0x00E0 -- 860 SPS
-- ==== 比较器队列(COMP_QUE)====
ADS1115_COMP_QUE_1 = 0x0000 -- 1次后触发
ADS1115_COMP_QUE_2 = 0x0001 -- 2次后触发
ADS1115_COMP_QUE_4 = 0x0002 -- 4次后触发
ADS1115_COMP_QUE_DISABLE = 0x0003 -- 禁用比较器(默认)
-- ==== 操作模式 ====
ADS1115_MODE_CONTINUOUS = 0x0000 -- 连续转换
ADS1115_MODE_SINGLE = 0x0100 -- 单次转换(默认,省电)
-- ==== 启动转换 ====
ADS1115_OS_START = 0x8000 -- 启动单次转换
-- ADS1115 配置上下文
ads1115_t = {
addr = 0x00,
config = 0x00, -- 16bits
fs_valtage = 0.0 -- 满量程电压(取决于PGA)
}
-- PGA对应的满量程电压(单位:V)
pga_fs = {
6.144, -- ADS1115_PGA_6_144V [0]
4.096, -- ADS1115_PGA_4_096V [1]
2.048, -- ADS1115_PGA_2_048V [2]
1.024, -- ADS1115_PGA_1_024V [3]
0.512, -- ADS1115_PGA_0_512V [4]
0.256 -- ADS1115_PGA_0_256V [5]
}
-- PGA 增益对应的满量程电压名
pga_names = {
"±6.144V", -- [0]
"±4.096V", -- [1]
"±2.048V", -- [2]
"±1.024V", -- [3]
"±0.512V", -- [4]
"±0.256V" -- [5]
}
-- 数据速率字符串
dr_names = {
"8 SPS", -- [0]
"16 SPS", -- [1]
"32 SPS", -- [2]
"64 SPS", -- [3]
"128 SPS", -- [4]
"250 SPS", -- [5]
"475 SPS", -- [6]
"860 SPS" -- [7]
}
-- I2C写2字节(大端)
function i2c_write_word(fd, reg, val)
i2c_write_reg(fd, reg + 0, (val >> 8) | 0xFF)
i2c_write_reg(fd, reg + 1, val & 0xFF)
end
--
-- 【配置脚本】
-- FIXME(20260602-qing): 异常处理
function build(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua builder func get a table --------")
print_table(data)
print("----------------------------------------------")
local fd = data.fd
-- 检查文件句柄参数是否存在
if fd == nil then
return {
result = "ERR",
message = "Invalid Arguments: fd"
}
end
local addr = data.address
-- 检查地址参数是否存在
if addr == nil then
return {
result = "ERR",
message = "Invalid Arguments: addr"
}
end
addr = tonumber(addr)
-- 写进配置表
ads1115.addr = addr
-- 开始配置PCA9685
i2c_set_addr(fd, addr)
-- 重启PCA9685
i2c_write_reg(fd, 0x00, 0x80)
-- 等待振荡器启动
milli_sleep(5)
i2c_write_reg(fd, 0x00, 0x10) -- 进入SLEEP模式(允许改频率)
i2c_write_reg(fd, 0xFE, math.floor(prescale))
i2c_write_reg(fd, 0x00, 0x20) -- 退出SLEEP模式
-- 等待振荡器启动
milli_sleep(5)
-- 返回成功信号
return {
result = "OK"
}
end
-- 【销毁PCA9685的脚本】
-- 重置PCA9685
-- NOTE(20260612-qing): 其实可以不需要,因为我们只是清空设置
function destroy(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua ender func get a table --------")
print_table(data)
print("--------------------------------------------")
local fd = data.fd
-- 检查fd参数是否存在
if fd == nil then
return {
result = "ERR",
message = "Invalid Arguments: fd"
}
end
local addr = data.address
-- 检查address参数是否存在
if addr == nil then
return {
result = "ERR",
message = "Invalid Arguments: addr"
}
end
addr = tonumber(addr)
-- 准备配置
i2c_set_addr(fd, addr)
-- 重启PCA9685
i2c_write_reg(fd, 0x00, 0x80)
-- 返回成功信号
return {
result = "OK"
}
end
-- 【调用PCA9685】
-- 将一个舵机通道转动到一个角度
-- NOTE(20260612-qing): 接收一个参数(表),返回一个结果(表)
function call(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua caller func get a table --------")
print_table(data)
print("---------------------------------------------")
local fd = data.fd
-- 检查fd参数是否存在
if fd == nil then
return {
result = "ERR",
message = "Invalid Argument: fd"
}
end
local addr = data.address
-- 检查address参数是否存在
if addr == nil then
return {
result = "ERR",
message = "Invalid Argument: addr"
}
end
addr = tonumber(addr)
local channel = data.channel
-- 检查channel参数是否存在
if channel == nil then
return {
result = "ERR",
message = "Invalid Argument: channel"
}
end
local angle = data.angle
-- 检查angle参数是否存在
if angle == nil then
return {
result = "ERR",
message = "Invalid Argument: angle"
}
end
print(string.format("Lua hardware drive set angle: %d", angle))
-- 设置I2C地址
i2c_set_addr(fd, addr)
-- 计算需要的参数
local min = 0.5 -- 0度对应0.5ms
local max = 2.5 -- 180度对应2.5ms
local width = min + (max - min) * (angle / 180.0)
local offval = math.floor(width * 4096.0 / 20.0)
local reg = 0x06 + 4 * channel
-- 写I2C寄存器
i2c_write_reg(fd, reg + 0, (0 >> 0) & 0xFF) -- 1 * 8
i2c_write_reg(fd, reg + 1, (0 >> 8) & 0xFF) -- 2 * 8
i2c_write_reg(fd, reg + 2, (offval >> 0) & 0xFF) -- 3 * 8
i2c_write_reg(fd, reg + 3, (offval >> 8) & 0xFF) -- 4 * 8
-- 返回字典
return {
result = "OK"
}
end
+10
View File
@@ -0,0 +1,10 @@
-- ADS1256 模数转换器 - SPI接口
function build(data)
end
function destroy(data)
end
function call(data)
end
+223
View File
@@ -0,0 +1,223 @@
-- PCA9685 PWM控制板 硬件脚本(测试)
-- 这个函数是辅助用的,它能够打印一张表出来
function print_table(t, indent)
indent = indent or 0
for k, v in pairs(t) do
if type(v) == 'table' then
print(string.rep(' ', indent) .. k .. ":")
print_table(v, indent + 2)
else
print(string.rep(' ', indent) .. k .. ": " .. tostring(v))
end
end
end
-- 【配置脚本】
-- 该脚本负责初始化PCA9685
-- FIXME(20260602-qing): 异常处理
function build(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua builder func get a table --------")
print_table(data)
print("----------------------------------------------")
local fd = data.fd
-- 检查文件句柄参数是否存在
if fd == nil then
return {
result = "ERR",
message = "Invalid Arguments: fd"
}
end
local addr = data.address
-- 检查地址参数是否存在
if addr == nil then
return {
result = "ERR",
message = "Invalid Arguments: addr"
}
end
addr = tonumber(addr)
-- 设置PWM频率
local freq = 50.0
local prescale_val = 25000000.0 / (4096.0 * freq) - 1.0 -- 25MHz主频
local prescale = prescale_val + 0.5
-- 开始配置PCA9685
i2c_set_addr(fd, addr)
-- 重启PCA9685
i2c_write_reg(fd, 0x00, 0x80)
-- 等待振荡器启动
milli_sleep(5)
i2c_write_reg(fd, 0x00, 0x10) -- 进入SLEEP模式(允许改频率)
i2c_write_reg(fd, 0xFE, math.floor(prescale))
i2c_write_reg(fd, 0x00, 0x20) -- 退出SLEEP模式
-- 等待振荡器启动
milli_sleep(5)
-- 返回成功信号
return {
result = "OK"
}
end
-- 【销毁PCA9685的脚本】
-- 重置PCA9685
-- NOTE(20260612-qing): 其实可以不需要,因为我们只是清空设置
function destroy(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua ender func get a table --------")
print_table(data)
print("--------------------------------------------")
local fd = data.fd
-- 检查fd参数是否存在
if fd == nil then
return {
result = "ERR",
message = "Invalid Arguments: fd"
}
end
local addr = data.address
-- 检查address参数是否存在
if addr == nil then
return {
result = "ERR",
message = "Invalid Arguments: addr"
}
end
addr = tonumber(addr)
-- 准备配置
i2c_set_addr(fd, addr)
-- 重启PCA9685
i2c_write_reg(fd, 0x00, 0x80)
-- 返回成功信号
return {
result = "OK"
}
end
-- 【调用PCA9685】
-- 将一个舵机通道转动到一个角度
-- NOTE(20260612-qing): 接收一个参数(表),返回一个结果(表)
function call(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua caller func get a table --------")
print_table(data)
print("---------------------------------------------")
local fd = data.fd
-- 检查fd参数是否存在
if fd == nil then
return {
result = "ERR",
message = "Invalid Argument: fd"
}
end
local addr = data.address
-- 检查address参数是否存在
if addr == nil then
return {
result = "ERR",
message = "Invalid Argument: addr"
}
end
addr = tonumber(addr)
local channel = data.channel
-- 检查channel参数是否存在
if channel == nil then
return {
result = "ERR",
message = "Invalid Argument: channel"
}
end
local angle = data.angle
-- 检查angle参数是否存在
if angle == nil then
return {
result = "ERR",
message = "Invalid Argument: angle"
}
end
print(string.format("Lua hardware drive set angle: %d", angle))
-- 设置I2C地址
i2c_set_addr(fd, addr)
-- 计算需要的参数
local min = 0.5 -- 0度对应0.5ms
local max = 2.5 -- 180度对应2.5ms
local width = min + (max - min) * (angle / 180.0)
local offval = math.floor(width * 4096.0 / 20.0)
local reg = 0x06 + 4 * channel
-- 写I2C寄存器
i2c_write_reg(fd, reg + 0, (0 >> 0) & 0xFF) -- 1 * 8
i2c_write_reg(fd, reg + 1, (0 >> 8) & 0xFF) -- 2 * 8
i2c_write_reg(fd, reg + 2, (offval >> 0) & 0xFF) -- 3 * 8
i2c_write_reg(fd, reg + 3, (offval >> 8) & 0xFF) -- 4 * 8
-- 返回字典
return {
result = "OK"
}
end
+163
View File
@@ -0,0 +1,163 @@
-- SPI 硬件
--
SPI_SUCCESS = 0
SPI_FAILED = 1
-- 全局文件句柄
fd = 0
-- 这个函数是辅助用的,它能够打印一张表出来
function print_table(t, indent)
indent = indent or 0
for k, v in pairs(t) do
if type(v) == 'table' then
print(string.rep(' ', indent) .. k .. ":")
print_table(v, indent + 2)
else
print(string.rep(' ', indent) .. k .. ": " .. tostring(v))
end
end
end
function build(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- SPI.Lua builder func get a table --------")
print_table(data)
print("--------------------------------------------------")
local path = data.path
-- 检查文件句柄参数是否存在
if path == nil then
return {
result = "ERR",
message = "Invalid Arguments: path"
}
end
-- 模式
local spi_mode = data.spi_mode
if spi_mode == nil then
return {
result = "ERR",
message = "Invalid Arguments: spi_mode"
}
end
print(spi_mode)
spi_mode = tonumber(spi_mode)
-- 比特
local bits = data.bits
if bits == nil then
return {
result = "ERR",
message = "Invalid Arguments: bits"
}
end
bits = tonumber(bits)
-- 速率
local speed = data.speed
if speed == nil then
return {
result = "ERR",
message = "Invalid Arguments: speed"
}
end
speed = tonumber(speed)
--
local delay = data.delay
if delay == nil then
return {
result = "ERR",
message = "Invalid Arguments: delay"
}
end
delay = tonumber(delay)
-- 开始执行
fd = spi_open(path)
if fd <= 0 then
return {
result = "ERR",
message = "Failed to open SPI file"
}
end
local res = spi_setup(fd, spi_mode, bits, speed, delay)
if res ~= 0 then
return {
result = "ERR",
message = "Failed to setup SPI device"
}
end
return {
result = "OK",
fd = fd
}
end
function destroy(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- SPI.Lua ender func get a table --------")
print_table(data)
print(string.format("now fd: %i", fd))
print("------------------------------------------------")
spi_close(fd)
fd = -1
return {
result = "OK"
}
end
function call(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- SPI.Lua caller func get a table --------")
print_table(data)
print("-------------------------------------------------")
return {
fd = fd
}
end
+86
View File
@@ -0,0 +1,86 @@
-- PCA9685 PWM控制板 硬件脚本(测试)
-- 这个函数是辅助用的,它能够打印一张表出来
function print_table(t, indent)
indent = indent or 0
for k, v in pairs(t) do
if type(v) == 'table' then
print(string.rep(' ', indent) .. k .. ":")
print_table(v, indent + 2)
else
print(string.rep(' ', indent) .. k .. ": " .. tostring(v))
end
end
end
-- 【配置脚本】
function build(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua builder func get a table --------")
print_table(data)
print("----------------------------------------------")
-- 返回成功信号
return {
result = "OK"
}
end
-- 【销毁用的脚本】
function destroy(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua ender func get a table --------")
print_table(data)
print("--------------------------------------------")
-- 返回成功信号
return {
result = "OK"
}
end
-- 【调用的脚本】
function call(data)
-- 检查该参数是否是一张表
if type(data) ~= "table" then
return {
result = "ERR",
message = "Invalid Arguments: table"
}
end
print("-------- Lua caller func get a table --------")
print_table(data)
print("---------------------------------------------")
-- 返回字典
return {
result = "OK"
}
end
+1
View File
@@ -0,0 +1 @@
cjpm run --run-args="$($args -join ' ')"
+55
View File
@@ -0,0 +1,55 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- 10 - 10 - 10 - 10 - - - 20;
- 20 - 20 - 20 - 20 - - - 20;
- 30 - 30 - 30 - 30 - - - 20;
45 0 45 0 45 0 45 0 90 90 10 200; # stand
@
+54
View File
@@ -0,0 +1,54 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- - - - - - - - - - 400 200;
0 - - - - - - 150 - - 1000 1000;
45 - - - - - - 0 - - 10 20;
@
+63
View File
@@ -0,0 +1,63 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- 45 - - - 45 - - - - - 100;
90 - - - 90 - - - - - - 50; # stand
#
- 0 - - - 0 - - - - - 50;
- - - 45 - - - 45 - - - 100;
45 - - - 45 - - - - - - 10;
- - 0 - - - 0 - - - - 50;
#
- - - 0 - - - 0 - - - 50;
- 45 - - - 45 - - - - - 100;
- - 45 - - - 45 - - - - 10;
- 0 - - - 0 - - - - - 50;
@
+53
View File
@@ -0,0 +1,53 @@
# ONE STEP
#
# legs | neck | free | speaker | delay
~s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 - - - - - - spk delay; # column name
!90 180 -90 -180 90 180 -90 -180 180 180 - - - - - - 5000 1000; # 4-legs spider robot
!- - - - - - - - - 90 - - - - - - - -; # 4-legs spider robot
!- - - - - - - - - - - - - - - - - -; # offset
45 - 45 - 45 - 45 - 90 90 - - - - - - 1000 100; # stand
- - - - - - - - - - - - - - - - 1000 -; # stand
- - - 30 - - - 30 - - - - - - - - 0 50; # right behind up & left front up
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
0 - 90 - 90 - 0 - - - - - - - - - - -; # right front forward & left behind forward
- 0 - - - 0 - - - - - - - - - - - -; # right front down & left behind down
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
0 - 90 - 90 - 0 - - - - - - - - - - -; #
- 0 - - - 0 - - - - - - - - - - - -; #
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
0 - 90 - 90 - 0 - - - - - - - - - - -; #
- 0 - - - 0 - - - - - - - - - - - -; #
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
0 - 90 - 90 - 0 - - - - - - - - - - -; #
- 0 - - - 0 - - - - - - - - - - - -; #
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
0 - 90 - 90 - 0 - - - - - - - - - - -; #
- 0 - - - 0 - - - - - - - - - - - -; #
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
45 - 45 - 45 - 45 - - - - - - - - - - -; # right front forward & left behind forward
- 0 - - - 0 - - - - - - - - - - - -; # right front down & left behind down
+56
View File
@@ -0,0 +1,56 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- 10 - 10 - 10 - 10 - - - 20;
- 15 - 15 - 15 - 15 - - - 20;
- 20 - 20 - 20 - 20 - - - 20;
- 25 - 25 - 25 - 25 - - - 20;
- 30 - 30 - 30 - 30 - - - 20;
@
+26
View File
@@ -0,0 +1,26 @@
# SNNEAK
# legs | neck | free | delay
~s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 - - - - - - spk delay; # column name
!90 180 -90 -180 90 180 -90 -180 180 180 - - - - - - 5000 1000; # 4-legs spider robot
! - - - - - - - - - - - - - - - - - -; # 4-legs spider robot
! - - - - - - - - - - - - - - - - - -10; # offset
# 俯卧撑
#
90 0 90 0 90 0 90 0 90 90 - - - - - - 1000 200;
- 5 - 5 - 5 - 5 - - - - - - - - 0 50;
- 10 - 10 - 10 - 10 - - - - - - - - - -;
- 15 - 15 - 15 - 15 - - - - - - - - - -;
- 20 - 20 - 20 - 20 - - - - - - - - - -;
- 25 - 25 - 25 - 25 - - - - - - - - - -;
- 30 - 30 - 30 - 30 - - - - - - - - - -;
- 35 - 35 - 35 - 35 - - - - - - - - - -;
#r
- 35 - 35 - 35 - 35 - - - - - - - - 1000 200;
- 30 - 30 - 30 - 30 - - - - - - - - 0 50;
- 25 - 25 - 25 - 25 - - - - - - - - - -;
- 20 - 20 - 20 - 20 - - - - - - - - - -;
- 15 - 15 - 15 - 15 - - - - - - - - - -;
- 10 - 10 - 10 - 10 - - - - - - - - - -;
- 5 - 5 - 5 - 5 - - - - - - - - - -;
- 0 - 0 - 0 - 0 - - - - - - - - - -;
+54
View File
@@ -0,0 +1,54 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- - - - - 0 - - - - 400 200;
- - 0 - - 150 - - - - 1000 1000;
- - 45 - - 0 - - - - 10 20;
@
+56
View File
@@ -0,0 +1,56 @@
# ONE STEP
# legs | neck | free | delay
~s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 - - - - - - spk delay; # column name
!90 180 -90 -180 90 180 -90 -180 180 180 - - - - - - 5000 1000; # 4-legs spider robot
! - - - - - - - - - - - - - - - - - -; # 4-legs spider robot
! - - - - - - - - - - - - - - - - - -; # offset
45 - 45 - 45 - 45 - 90 90 - - - - - - 1000 100; # stand
- - - - - - - - - - - - - - - - - -; # stand
#----------------------------------------------------------------
- 30 - - - 25 40 - - - - - - - - - 0 50;
0 - - - 90 - - - - - - - - - - - - -;
- 0 - - - 0 - - - - - - - - - - - -;
- - - 25 - - - 30 - - - - - - - - - -;
50 - 0 - 0 - 90 - - - - - - - - - - -;
- - - 0 - - - 0 - - - - - - - - - -;
#----------------------------------------------------------------
- 30 90 - - 25 40 - - - - - - - - - - -;
0 - - - 90 - - - - - - - - - - - - -;
- 0 - - - 0 - - - - - - - - - - - -;
- - - 25 - - - 30 - - - - - - - - - -;
50 - 0 - 0 - 90 - - - - - - - - - - -;
- - - 0 - - - 0 - - - - - - - - - -;
- 30 90 - - 25 40 - - - - - - - - - - -;
0 - - - 90 - - - - - - - - - - - - -;
- 0 - - - 0 - - - - - - - - - - - -;
- - - 25 - - - 30 - - - - - - - - - -;
50 - 0 - 0 - 90 - - - - - - - - - - -;
- - - 0 - - - 0 - - - - - - - - - -;
- 30 90 - - 25 40 - - - - - - - - - - -;
0 - - - 90 - - - - - - - - - - - - -;
- 0 - - - 0 - - - - - - - - - - - -;
- - - 25 - - - 30 - - - - - - - - - -;
50 - 0 - 0 - 90 - - - - - - - - - - -;
- - - 0 - - - 0 - - - - - - - - - -;
- 30 90 - - 25 40 - - - - - - - - - - -;
0 - - - 90 - - - - - - - - - - - - -;
- 0 - - - 0 - - - - - - - - - - - -;
- - - 25 - - - 30 - - - - - - - - - -;
50 - 0 - 0 - 90 - - - - - - - - - - -;
- - - 0 - - - 0 - - - - - - - - - -;
#----------------------------------------------------------------
- 30 90 - - 25 40 - - - - - - - - - - -;
0 - - - 90 - - - - - - - - - - - - -;
- 0 - - - 0 - - - - - - - - - - - -;
- - - 25 - - - 30 - - - - - - - - - -;
45 - 45 - 45 - 45 - - - - - - - - - - -;
- - - 0 - - - 0 - - - - - - - - - -;
+51
View File
@@ -0,0 +1,51 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
@
+57
View File
@@ -0,0 +1,57 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- 10 - 10 - 10 - 10 - - - 50;
- 20 - 20 - 20 - 20 - - - -;
- 30 - 30 - 30 - 30 - - - -;
- 20 - 20 - 20 - 20 - - - -;
- 10 - 10 - 10 - 10 - - - -;
- 0 - 0 - 0 - 0 - - - 200; # stand
@
+56
View File
@@ -0,0 +1,56 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
#i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
90 - - - - - - - - - - 20; # left-behind
45 - - - - - - - - - 0 500; # stand
- - 90 - - - 45 - - - - 20; # right-behind
- - 45 - - - - - - 45 200 500; # stand
- - - - 90 - - - - - 0 20; # right-front
- - - - 45 - - - - - - 500; # stand
- - - - - - 90 - - - - 20; # left-front
- - - - - - 45 - - - - 500; # stand
- - - - - - - - - 90 - 20; # stand
@
+21
View File
@@ -0,0 +1,21 @@
# 无硬件情况下的测试脚本
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@TEST
name parameter0 patameter1;
t0 p0 p1;
t1 p2 p3;
@
@Action
t0.A t1.F;
10 20;
@
+57
View File
@@ -0,0 +1,57 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- 10 - 10 - - - - - - - 50;
- 20 - 20 - - - - - - - -;
- 30 - 30 - - - - - - - -;
- 20 - 20 - - - - - - - -;
- 10 - 10 - - - - - - - -;
- 0 - 0 - - - - - - - 200; # stand
@
+57
View File
@@ -0,0 +1,57 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- - - - - 10 - 10 - - - 50;
- - - - - 20 - 20 - - - -;
- - - - - 30 - 30 - - - -;
- - - - - 20 - 20 - - - -;
- - - - - 10 - 10 - - - -;
- - - - - 0 - 0 - - - 200; # stand
@
+57
View File
@@ -0,0 +1,57 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- 10 - - - - - 10 - - - 50;
- 20 - - - - - 20 - - - -;
- 30 - - - - - 30 - - - -;
- 20 - - - - - 20 - - - -;
- 10 - - - - - 10 - - - -;
- 0 - - - - - 0 - - - 200; # stand
@
+57
View File
@@ -0,0 +1,57 @@
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@MPU6050_BIND
name I2C addr;
#mpu6050-0 i2c-1 0x69;
#mpu6050-1 i2c-1 0x68;
#mpu6050-2 i2c-0 0x68;
#mpu6050-3 i2c-0 0x69;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0; # rb0
s2 180 - - - 1; # rb1
s3 -90 - - - 2; # lb0
s4 -180 - - - 3; # lb1
s5 90 - - - 4; # lf0
s6 180 - - - 5; # lf1
s7 -90 - - - 6; # rf0
s8 -180 - - - 7; # rf1
s9 180 - - - 8; # cam0
s10 180 - - - 9; # cam1
@
# 行动矩阵
@Action
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
45 - 45 - 45 - 45 - 90 90 10 200; # stand
- - - 10 - 10 - - - - - 50;
- - - 20 - 20 - - - - - -;
- - - 30 - 30 - - - - - -;
- - - 20 - 20 - - - - - -;
- - - 10 - 10 - - - - - -;
- - - 0 - 0 - - - - - 200; # stand
@
+29
View File
@@ -0,0 +1,29 @@
# SNNEAK
# legs | neck | free | delay
#lb rb rf lf
~s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 - - - - - - spk delay; # column name
!90 180 -90 -180 90 180 -90 -180 180 180 - - - - - - 5000 1000; # 4-legs spider robot
! - - - - - - - - - - - - - - - - - -; # 4-legs spider robot
! - - - - - - - - - - - - - - - - - -; # 4-legs spider robot
# 俯卧撑
#
- 0 - 0 - 0 - 0 90 90 - - - - - - 1000 200;
- 5 - 5 - 5 - 5 - - - - - - - - 0 50;
- 10 - 10 - 10 - 10 - - - - - - - - - -;
- 15 - 15 - 15 - 15 - - - - - - - - - -;
- 20 - 20 - 20 - 20 - - - - - - - - - -;
- 25 - 25 - 25 - 25 - - - - - - - - - -;
- 30 - 30 - 30 - 30 - - - - - - - - - -;
- 35 - 35 - 35 - 35 - - - - - - - - - -;
#r
- 35 - 35 - 35 - 35 - - - - - - - - 1000 200;
- 30 - 30 - 30 - 30 - - - - - - - - 0 50;
- 25 - 25 - 25 - 25 - - - - - - - - - -;
- 20 - 20 - 20 - 20 - - - - - - - - - -;
- 15 - 15 - 15 - 15 - - - - - - - - - -;
- 10 - 10 - 10 - 10 - - - - - - - - - -;
- 5 - 5 - 5 - 5 - - - - - - - - - -;
- 0 - 0 - 0 - 0 - - - - - - - - - -;
+263
View File
@@ -0,0 +1,263 @@
package bot4cj
import std.io.*
import std.math.*
import std.convert.*
import std.collection.*
import bot4cj.math.*
/* -------- 矩阵编程语言 - 基本对象 --------
* TODO:增加储存到文件系统的功能
* TODO:神经网络层转换为矩阵对象
-------- */
class MatrixObject {
private var name: String = "" /* -------- 名字 -------- */
private var tmp = ArrayList<Rune>() /* ---- rune buf 字符缓冲区 ---- */
private var grp = ArrayList<String>() /* ---- one line 整行缓冲区 ---- */
private var values = ArrayList<ArrayList<String>>() /* ---- 原始矩阵 ---- */
/* -------- 是否已命名 -------- */
public func hasName(): Bool { !(name == "") }
/* --------- 获得名字 --------- */
public func getName(): String { return name }
/* ------- 取动作的长度 ------- */
public mut prop size: Int64 {
get() { values.size }
set(_) {}
}
/* -------- 取动作的列数 -------- */
public mut prop col: Int64 {
get() {
if (this.size > 0) {
return values[0].size }
else { return 0 }
}
set(_) {}
}
/* ------- 获取矩阵元素,只能取,不能修改 ------- */
public operator func [](row: Int64, col: Int64): String {
return values[row][col]
}
/** -- 在某一行中寻找某一个字符串的列号 -- */
public func indexOfCol(row: Int64, key: String): Int64 {
for (col in 0..values[row].size) {
if (values[row][col] == key) {
return col }
}
/* ---- 没找到 ---- */
throw IllegalArgumentException("The key does not found.")
}
/* ----- 储存对象 ----- */
public func save(output: StringWriter<OutputStream>) {
output.writeln("@${getName()}")
for (vec in values) {
for (item in vec) {
output.write(item)
output.write(" ")
}
output.writeln(";")
}
output.writeln("@")
}
/* -------- 添加字符 -------- */
public func addRune(c: Rune) { tmp.add(c) }
/* -------- 提交名字 -------- */
public func submitName() {
if (tmp.size == 0) { return }
let s = String(tmp)
tmp.clear()
name = s
}
/* -------- 提交元素 -------- */
public func submitItem() {
if (tmp.size == 0) { return }
let s = String(tmp)
tmp = ArrayList<Rune>()
grp.add(s)
}
/* -------- 提交行 -------- */
public func submitLine(count: Int64) {
if (grp.size == 0) { return }
if (!values.size == 0 && !grp.size == values[values.size - 1].size)
{ throw DoesNotSync(count) }
values.add(grp)
grp = ArrayList<String>()
}
}
/* ---------------- 对Nn1进行扩展 ---------------- */
extend Nn1 {
/* -------- 转换为MatrixObject对象 -------- */
public func toMto(): MatrixObject {
let mto = MatrixObject()
return mto
}
}
/* -------- 矩阵式解析器 --------
* TODO(20260514-qing): 这个类输入流数据,输出MatrixObject对象
* 它是与其深度绑定的,我倾向于将其与MatrixObject融合
* 即持有HashMap<String, ArrayList<ArrayList<String>>> */
class MatrixParser {
/* ---- 静态判断Option<Rune>是否有值 ----
- 该方法用于文本文件遍历时判空 */
private static func hasValue(co: Option<Rune>): Bool {
match(co) {
case Some(_) => true
case None => false
}
}
/* -------- 主初始化器(构造函数) --------
- 设置目标输入流 */
private var input: StringReader<InputStream>
public init(input: StringReader<InputStream>) {
this.input = input }
/* -------- 切换目标输入流 -------- */
public func reset(input: StringReader<InputStream>) {
this.input = input }
/* -------- 解析器 - 循环不变式 ----------------------
* -------- 初始化: ss是一个动作脚本的输入流 --------
* -------- 保持: ss是动作脚本剩余部分的输入流 ------
* -------- 终止: 当输入流为空时,循环终止 ---------- */
private var obj = MatrixObject()
public func parse(): Option<MatrixObject> {
var count = 1i64 /* 行计数器始于1 */
flag = false /* 重置结束标志 */
this.obj = MatrixObject() /* 创建一个新的对象,一次只解析一个对象 */
var so = input.read() /* 获取初始状态 */
while (!flag && hasValue(so)) {
try {
let s = so.getOrThrow()
print(s) /* debug */
count = _next(s, count) /* 进行解析解析 */
so = input.read() /* 为下一次循环提供条件 */
}
catch (exp: BufOverTheLine) { throw exp }
catch (exp: IllegalArgumentException) { throw IllegalArgumentException("Parse failed at line ${count}") }
catch (exp: Exception) { throw UnknowExcept(count) } /* FIXME: 还是暴露出原来的错误位置来更好? */
}
/* 如果没有新的矩阵式对象(意外终止)则返回空 */
if (!obj.hasName()) { return None }
return obj /* 否则为解析成功正常退出,返回解析到的矩阵式对象 */
}
private var flag = false /* 结束标志 */
private var nl: Bool = true /* new line */
private var cm: Bool = false /* comment */
private var nm: Bool = false /* name-line */
private func _next(c: Rune, ct: Int64): Int64 {
var count = ct
/* ------------ COMMENTS ------------ */
if (!cm && c == r'#') { cm = true } /* 注释开始时 */
else if (cm && c == r'\n') { /* 注释结束时 */
nl = true
cm = false
count += 1
}
else if (cm && c != r'\n') { ; } /* 注释进行时 */
/* FIXME: EOF情况 */
/* ------------ new line ------------ */
else if (nl && (c == r' ' || c == r'\t' || c == r'\r')) { ; } /* 新行空字符,维持现状 */
else if (nl && c == r'\n') { /* 新行空行 */
count += 1
}
else if (nl && c == r'@' && !obj.hasName()) {
nl = false
nm = true
}
else if (nl && c == r'@' && obj.hasName()) {
flag =true
nl = false
nm = true
}
else if (nl && c != r'@') { /* 新行内容 */
nl = false
obj.addRune(c)
}
/* ------------- not nl ------------- */
else if (!nl && nm && (c == r' ' || c == r'\t' || c == r'\n')) { /* 添加名字 */
obj.submitName()
nl = true
nm = false
}
else if (!nl && !nm && (c == r' ' || c == r'\t' || c == r"\n")) { /* 添加命名项 */
obj.submitItem()
if (c == r"\n") { count += 1 }
}
else if (!nl && c == r';') { /* 普通行结束 */
obj.submitItem()
obj.submitLine(count)
nl = true
}
else if (!nl) { // 非空格、非制表符、非分号
obj.addRune(c)
}
/* -------- 返回行号 -------- */
return count
}
}
/* -------- 矩阵式储存器 --------
* - 接收矩阵式的哈希表(HashMap<String, MatrixObject>)
* - 以及一个输出流(在构造函数)。 */
class MatrixSaver {
private let stringWriter: StringWriter<OutputStream>
public init(output: OutputStream) {
stringWriter = StringWriter(output) }
public func save(C: LinkedList<MatrixObject>) {
for (item in C) {
item.save(stringWriter)
stringWriter.writeln() /* 对象之间空一行 */
stringWriter.flush() }
}
}
/* --------------------------------------------------------- */
/* --------------------------------------------------------- */
/* TODO(20260516-qing): 继续完善异常种类 */
/* -------- 解析异常 -------- */
open class ParseExcept <: Exception {
public ParseExcept(msg: String, line: Int64) {
super('${msg}. we got line: ${line}') }
}
/* -------- 缓冲区过长 -------- */
class BufOverTheLine <: ParseExcept {
BufOverTheLine(count: Int64) {
super('Buffer length over the line', count) }
}
/* -------- 行没对齐 -------- */
class DoesNotSync <: ParseExcept {
DoesNotSync(count: Int64) {
super("Matrix Line Does Not Sync", count) }
}
/* -------- 未知异常 -------- */
class UnknowExcept <: ParseExcept {
UnknowExcept(count: Int64) {
super("I don't understand what happened", count) }
}
+152
View File
@@ -0,0 +1,152 @@
/*
* i2c - 用户界面
*
* 这里已经提供了I2C通用接口,可以将上层代码移动到仓颉下面
*/
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#ifdef __linux__
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h> /* 其中宏的取值可能具有平台特异性 */
#include <i2c/smbus.h>
#endif
#include "bot4cj.h" /* 有没有可能在仓颉代码中解析C头文件取得宏 */
/*
* 初始化i2c设备
*
* 动态指定设备文件
*
* 返回打开的文件句柄或者错误码
*/
#ifdef __linux__
int16_t I2C_init(const char* filepath) {
int16_t fd;
if ((fd = open(filepath, O_RDWR)) < 0) {
perror("Failed to open I2C bus");
return BOT4CJ_I2C_OPENED_FAILED;
}
else {
return fd;
}
}
#endif
#ifdef _WIN64
int16_t I2C_init(const char* filepath) {
return BOT4CJ_I2C_OPENED_FAILED;
}
#endif
/* 释放I2C设备文件 */
#ifdef __linux__
void I2C_free(int16_t fd) {
close(fd);
}
#endif
#ifdef _WIN64
void I2C_free(int16_t fd) {
/* Windows平台下无需操作 */
}
#endif
/* 设置I2C地址 */
#ifdef __linux__
int I2C_setAddr(int16_t fd, uint8_t addr) {
if (ioctl(fd, I2C_SLAVE, addr) < 0) {
perror("Failed to set I2C address");
return BOT4CJ_I2C_ERR;
}
else {
return BOT4CJ_I2C_OK;
}
}
#endif
#ifdef _WIN64
int I2C_setAddr(int16_t fd, uint8_t addr) {
return BOT4CJ_I2C_ERR;
}
#endif
/* 写寄存器 */
#ifdef __linux__
int I2C_writeReg(int16_t fd, uint8_t reg, uint8_t val) {
/*uint8_t buf[] = {reg, val};
if (write(fd, buf, 2) != 2) {
perror("I2C write error");
return BOT4CJ_I2C_ERR;
}
else {
return BOT4CJ_I2C_OK;
}*/
return i2c_smbus_write_byte_data(fd, reg, val);
}
#endif
#ifdef _WIN64
int I2C_writeReg(int16_t fd, uint8_t reg, uint8_t val) {
return BOT4CJ_I2C_ERR;
}
#endif
/* 读寄存器 失败返回0 */
#ifdef __linux__
uint8_t I2C_readReg(int16_t fd, uint8_t reg) {
uint8_t value;
/*if (write(fd, &reg, 1) != 1) {
perror("I2C read error");
return 0;
}
if (read(fd, &value, 1)!= 1) {
perror("I2C read error");
return 0;
}*/
//return value;
int8_t a = i2c_smbus_read_byte_data(fd, reg);
return *(uint8_t *)& a;
}
#endif
#ifdef _WIN64
uint8_t I2C_readReg(int16_t fd, uint8_t reg) {
return 0;
}
#endif
/* 读寄存器 - 有符号
* 失败返回0 */
#ifdef __linux__
int8_t I2C_readRegFlag(int16_t fd, uint8_t reg) {
int8_t value;
//if (write(fd, &reg, 1) != 1) {
//perror("I2C read error");
//return 0;
//}
//if (read(fd, &value, 1)!= 1) {
//perror("I2C read error");
//return 0;
//}
//return value;
return i2c_smbus_read_byte_data(fd, reg);
}
#endif
#ifdef _WIN64
int8_t I2C_readRegFlag(int16_t fd, uint8_t reg) {
return 0;
}
#endif
+516
View File
@@ -0,0 +1,516 @@
#include <random>
#include <algorithm>
#include "Matrix.hh"
namespace qing {
template <typename T>
Matrx<T>::Matrx(): row(0), col(0) {}
template <typename T>
Matrx<T>::Matrx(const uint64_t row, const uint64_t col)
: row(row), col(col) { /* 该构造函数用于从头开始构造 */
arr = std::vector<T>(col * row);
}
template<typename T>
Matrx<T>::Matrx(const uint64_t row, const uint64_t col, const std::vector<T>& src)
: row(row), col(col) { /* 该构造函数用于通过已有的vector进行构造 */
arr = src;
}
template<typename T>
const T& Matrx<T>::get(uint64_t row, uint64_t col) const { /* 访问器 - 访问矩阵元素 */
return arr[col * this->row + row];
}
template<typename T>
T& Matrx<T>::witch(uint64_t row, uint64_t col) { /* 访问器 - 访问矩阵元素 */
return arr[col * this->row + row];
}
template<typename T>
void Matrx<T>::set(uint64_t row, uint64_t col, T v) { /* 设置矩阵元素 */
arr[col * this->row + row] = v;
}
template<typename T>
const std::vector<T>& Matrx<T>::get_data() const{ /* 访问器 - 用于获取原始数据 */
return arr;
}
template<typename T>
const uint64_t Matrx<T>::get_col() const { /* 获取列数 */
return col;
}
template<typename T>
const uint64_t Matrx<T>::get_row() const { /* 获取行数 */
return row;
}
template<typename T>
bool Matrx<T>::has_same_shape(const Matrx<T>& other) { /* 两个矩阵是否具有相同形状 */
return other.get_col() == this->get_col() && other.get_row() == this->get_row();
}
template<typename T>
bool Matrx<T>::has_dot_shape(const Matrx<T>& other) { /* 具有矩阵乘法的形状 */
return this->get_col() == other.get_row();
}
template<typename T>
void Matrx<T>::assert_same_shape(const Matrx<T>& other) { /* 断言两个矩阵具有相同形状 */
if (!has_same_shape(other))
throw std::invalid_argument("The shapes of matrix are not same.");
}
template<typename T>
void Matrx<T>::assert_dot_shape(const Matrx<T>& other) { /* 断言两个矩阵具有点积前置形状 */
if (!has_dot_shape(other)) {
throw std::invalid_argument("The shapes of matrix can not be dot.");
}
}
template<typename T>
Matrx<T> Matrx<T>::operator+(const Matrx<T>& other) { /* 加法操作符重定义 - 矩阵加法 */
//assert_same_shape(other); /* 检查两个矩阵形状是否一致 */
std::vector<T> v0 = this->get_data();
std::vector<T> vec = other.get_data();
for (auto it = v0.begin(), it_or = vec.begin(); it != v0.end(); ++it, ++it_or )
*it += *it_or;
return Matrx<T>{this->get_row(), this->get_col(), v0};
}
template<typename T>
Matrx<T> Matrx<T>::operator+(T& item) { /* 矩阵加法广播 */
auto len = this->get_data().size();
auto v0 = std::vector<T> (len, item);
auto matrix = (*this) + Matrx(get_row(), get_col(), v0);
//auto matrix = *this;
//for (auto it = arr.begin(); it != arr.end(); ++it)
// *it += item;
return matrix;
}
template<typename T>
Matrx<T> Matrx<T>::operator-(const Matrx<T>& other) { /* 操作符重载 - 矩阵减法 */
//assert_same_shape(other); /* 检查两个矩阵形状是否一致 */
std::vector<T> v0 = this->get_data();
std::vector <T> vec = other.get_data();
for (auto it = v0.begin(), it_or = vec.begin(); it != v0.end(); ++it, ++it_or )
*it -= *it_or;
return Matrx{this->get_row(), this->get_col(), v0};
}
template<typename T>
Matrx<T> Matrx<T>::operator-(T& item) { /* 操作符重载 - 矩阵减法广播 */
auto len = this->get_data().size();
auto v0 = std::vector<T> (len, item);
auto matrix = (*this) - Matrx(get_row(), get_col(), v0);
//Matrx<T> matrix = *this;
//for (auto it = arr.begin(); it != arr.end(); ++it)
// *it -= item;
return matrix;
}
template<typename T>
Matrx<T> Matrx<T>::dot(const Matrx<T>& other) { /* 操作符重载 - 矩阵乘法 */
//assert_dot_shape(other); /* 它们的形状是否能够点积 */
auto matrix = Matrx<T>(this->get_row(), other.get_col());
for (uint64_t i=0; i<this->get_row(); ++i) /* 点积运算 */
for (uint64_t j=0; j<other.get_col(); ++j) {
T sum{0};
for (uint64_t k=0; k<this->get_col(); ++k)
sum += this->get(i, k) * other.get(k, j);
matrix.witch(i, j) = sum;
}
return matrix;
}
template<typename T>
Matrx<T> Matrx<T>::operator*(T& item) { /* 操作符重载 - 矩阵乘法广播 */
std::vector<T> vec = this->get_data();
for (auto it = vec.begin(); it != vec.end(); ++it)
*it *= item;
return Matrx<T>{this->get_row(), this->get_col(), vec};
}
template<typename T>
Matrx<T> Matrx<T>::operator*(const Matrx<T>& other) { /* 加法操作符重定义 - 矩阵加法 */
//assert_same_shape(other); /* 检查两个矩阵形状是否一致 */
std::vector<T> v0 = this->get_data();
std::vector<T> vec = other.get_data();
for (auto it = v0.begin(), it_or = vec.begin(); it != v0.end(); ++it, ++it_or )
*it *= *it_or;
return Matrx<T>{this->get_row(), this->get_col(), v0};
}
template<typename T>
Matrx<T> Matrx<T>::operator/(T& item) { /* 操作符重载 - 矩阵除法广播 */
std::vector<T> vec = this->get_data();
for (auto it = vec.begin(); it != vec.end(); ++it)
*it /= item;
return Matrx<T>(this->get_row(), this->get_col(), vec);
}
template<typename T>
Matrx<T> Matrx<T>::operator/(const Matrx<T>& other) { /* 加法操作符重定义 - 矩阵加法 */
//assert_same_shape(other); /* 检查两个矩阵形状是否一致 */
std::vector<T> v0 = this->get_data();
std::vector<T> vec = other.get_data();
for (auto it = v0.begin(), it_or = vec.begin(); it != v0.end(); ++it, ++it_or )
*it /= *it_or;
return Matrx<T>{this->get_row(), this->get_col(), v0};
}
template class Matrx<float>; /* 显式实例化 */
template class Matrx<double>;
}
/* FIXME: 形状不合法时返回空指针 */
using namespace qing;
extern "C" {
#ifdef _WIN64
_declspec(dllexport)
#endif
void* createMatrix(uint64_t row, uint64_t col) {
return (void *) new Matrx<float>(row, col);
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void freeMatrix(void* M) {
delete (Matrx<float>*) M;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
float* witchMatrix(void *m, uint64_t row, uint64_t col) {
Matrx<float> *M = (Matrx<float>*) m;
return &(M->witch(row, col));
}
#ifdef _WIN64
_declspec(dllexport)
#endif
float getMatrix(void *m, uint64_t row, uint64_t col) {
Matrx<float> *M = (Matrx<float>*) m;
return M->get(row, col);
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void setMatrix(void *m, uint64_t row, uint64_t col, float v) {
Matrx<float> *M = (Matrx<float>*) m;
M->set(row, col, v);
}
#ifdef _WIN64
_declspec(dllexport)
#endif
uint64_t getrowMatrix(void *m) {
Matrx<float> *M = (Matrx<float>*) m;
return M->get_row();
}
#ifdef _WIN64
_declspec(dllexport)
#endif
uint64_t getcolMatrix(void *m) {
Matrx<float> *M = (Matrx<float>*) m;
return M->get_col();
}
#ifdef _WIN64
_declspec(dllexport)
#endif
bool sameShapeMatrix(void *m, void *s) {
Matrx<float> *M = (Matrx<float>*) m;
Matrx<float> *S = (Matrx<float>*) s;
return M->has_same_shape(*S);
}
#ifdef _WIN64
_declspec(dllexport)
#endif
bool dotShapeMatrix(void *m, void *s) {
Matrx<float> *M = (Matrx<float>*) m;
Matrx<float> *S = (Matrx<float>*) s;
return M->has_dot_shape(*S);
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* plusMatrix(void *m, void *s) {
Matrx<float> *M = (Matrx<float>*) m;
Matrx<float> *S = (Matrx<float>*) s;
auto a = new Matrx<float>((*M) + (*S));
return (void *) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* plusBroadcastMatrix(void *m, float t) {
Matrx<float> *M = (Matrx<float>*) m;
auto a = new Matrx<float> ((*M) + t);
return (void*) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* subMatrix(void *m, void *s) {
Matrx<float> *M = (Matrx<float>*) m;
Matrx<float> *S = (Matrx<float>*) s;
auto a = new Matrx<float>((*M) - (*S));
return (void *) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void * subBroadcastMatrix(void *m, float t) {
Matrx<float> *M = (Matrx<float> *) m;
auto a = new Matrx<float> ((*M) - t);
return (void *) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* multiMatrix(void *m, void *s) {
Matrx<float> *M = (Matrx<float>*) m;
Matrx<float> *S = (Matrx<float>*) s;
auto a = new Matrx<float>((*M) * (*S));
return (void *) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* multiBroadcastMatrix(void *m, float t) {
Matrx<float> *M = (Matrx<float> *) m;
auto a = new Matrx<float>((*M) * t);
return (void*) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* divMatrix(void *m, void *s) {
Matrx<float> *M = (Matrx<float>*) m;
Matrx<float> *S = (Matrx<float>*) s;
auto a = new Matrx<float>((*M) / (*S));
return (void *) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* divBroadcastMatrix(void *m, float t) {
Matrx<float> *M = (Matrx<float> *) m;
auto a = new Matrx<float>((*M) / t);
return (void*) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* dotMatrix(void *m, void *s) {
Matrx<float> *M = (Matrx<float>*) m;
Matrx<float> *S = (Matrx<float>*) s;
auto a = new Matrx<float>(M->dot(*S));
return (void *) a;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* transposeMatrix(void *m) {
Matrx<float> *M = (Matrx<float> *)m;
auto N= new Matrx<float>(M->get_col(), M->get_row());
for (uint64_t row = 0; row< N->get_row(); row++) {
for (uint64_t col = 0; col < N->get_col(); col++) {
N->witch(row, col) = M->get(col, row);
}
}
return (void *)N;
}
/* 正态分布Xavier初始化 */
#ifdef _WIN64
_declspec(dllexport)
#endif
void* xavierNormalInit(uint64_t fan_in, uint64_t fan_out)
{
/* 构建空数组 */
std::vector<float> weights(fan_in * fan_out);
/* 高质量的随机数生成器 */
static std::mt19937 generator(std::random_device{}());
/* 计算标准差 */
float sigma = std::sqrt(2.0f / (fan_in + fan_out));
/* 创建正态分布对象,均值为0,标准差为sigma */
std::normal_distribution<float> distribution(0.0f, sigma);
/* 遍历权重矩阵,进行赋值 */
uint64_t k=0;
for (uint64_t i=0; i<fan_in; ++i)
for (uint64_t j=0; j<fan_out; ++j)
weights[k++] = distribution(generator);
/* 构建矩阵 */
auto matrix = new Matrx<float>(fan_in, fan_out, weights);
return (void *)matrix;
}
/* 均匀分布Xavier初始化函数 */
#ifdef _WIN64
_declspec(dllexport)
#endif
void* xavierUniformInit(uint64_t fan_in, uint64_t fan_out)
{
/* 构建空的数组 */
auto weights = std::vector<float>(fan_in * fan_out);
/* 使用高质量的随机数生成器 */
static std::mt19937 generator(std::random_device{}());
/* 计算区间范围:sqrt(2 / (fan_in + fan_out)) */
float scale = std::sqrt(2.0f / (fan_in + fan_out));
/* 创建均匀分布对象,范围为[-scale, scale] */
std::uniform_real_distribution<float> distribution(-scale, scale);
/* 遍历权重矩阵,进行赋值 */
uint64_t k=0;
for (uint64_t i=0; i<fan_in; ++i)
for (uint64_t j=0; j<fan_out; ++j)
weights[k++] = distribution(generator);
/* 构建矩阵对象 */
auto matrix = new Matrx<float> (fan_in, fan_out, weights);
return (void *)matrix;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* Sigmoid_do(void *m) {
Matrx<float> *M = (Matrx<float>*) m;
std::vector<float> v = M->get_data();
for (float& x: v) {
x = 1.0 / (1.0 + std::exp(-x));
}
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
return (void *)N;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* Sigmoid_dot(void *m) {
Matrx<float> *M = (Matrx<float>*) m;
std::vector<float> v = M->get_data();
for (float& o: v) {
o = o * (1.0 - o);
}
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
return (void *)N;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* ReLU_do(void *m) {
Matrx<float> *M = (Matrx<float>*) m;
std::vector<float> v = M->get_data();
for (float& x: v) {
x = x > 0 ? x : 0;
}
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
return (void *)N;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* ReLU_dot(void *m) {
Matrx<float> *M = (Matrx<float>*) m;
std::vector<float> v = M->get_data();
for (float& x: v) {
x = x > 0 ? 1.0f : 0.0f;
}
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
return (void *)N;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* Leaky_ReLU_do(void *m, float alpha) {
Matrx<float> *M = (Matrx<float>*) m;
std::vector<float> v = M->get_data();
for (float& x: v) {
x = x > 0 ? x : alpha * x;
}
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
return (void *)N;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* Leaky_ReLU_dot(void *m, float alpha) {
Matrx<float> *M = (Matrx<float>*) m;
std::vector<float> v = M->get_data();
for (float& x: v) {
x = x > 0 ? 1.0 : alpha;
}
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
return (void *)N;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* Tanh_do(void *m) {
Matrx<float> *M = (Matrx<float>*) m;
std::vector<float> v = M->get_data();
for (float& x: v) {
x = std::tanh(x);
}
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
return (void *)N;
}
#ifdef _WIN64
_declspec(dllexport)
#endif
void* Tanh_dot(void *m) {
Matrx<float> *M = (Matrx<float>*) m;
std::vector<float> v = M->get_data();
for (float& x: v) {
if (x > 20.0) x = 0.0; // tanh(x) ≈ 1, 导数≈0
else if (x < -20.0) x = 0.0; // tanh(x) ≈ -1, 导数≈0
else {
float t = tanh(x);
x = 1.0 - t * t;
}
}
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
return (void *)N;
}
}
+60
View File
@@ -0,0 +1,60 @@
/*
___col___
/ \
+---------+ \
| + + + + | | row - 第一维度
| + + + + | |
+---------+ /
*/
#pragma once
#include <cmath>
#include <vector>
#include <iostream>
#include <stdexcept>
#include <cstdint>
namespace qing {
template <class T>
class Matrx {
public:
Matrx();
Matrx(const uint64_t row, const uint64_t col);
Matrx(const uint64_t row, const uint64_t col, const std::vector<T>& src);
const T& get(uint64_t row, uint64_t col) const;
//-------------旧接口----------------
T& witch(uint64_t row, uint64_t col);
//-------------新接口----------------
void set(uint64_t row, uint64_t col, T v);
//-----------------------------------
const std::vector<T>& get_data() const;
const uint64_t get_col() const;
const uint64_t get_row() const;
bool has_same_shape(const Matrx<T>& other);
bool has_dot_shape(const Matrx<T>& other);
void assert_same_shape(const Matrx<T>& other);
void assert_dot_shape(const Matrx<T>& other);
Matrx<T> operator+(const Matrx<T>& other);
Matrx<T> operator+(T& item);
Matrx<T> operator-(const Matrx<T>& other);
Matrx<T> operator-(T& item);
Matrx<T> operator*(const Matrx<T>& other);
Matrx<T> operator*(T& item);
Matrx<T> operator/(T& item);
Matrx<T> operator/(const Matrx<T>& other);
Matrx<T> dot(const Matrx<T>& other);
void save(std::ostream& out) {
out << row << " " << col;
for (const auto& item : arr) {
out << " ";
out << item;
}
}
private:
uint64_t row, col;
std::vector<T> arr;
};
}
+21
View File
@@ -0,0 +1,21 @@
/* I2C操作库 - PCA9685 - MPU6050 */
#ifndef QING_I2C
#define QING_I2C
/* I2C */
#define BOT4CJ_I2C_OK 0
#define BOT4CJ_I2C_ERR 1
#define BOT4CJ_I2C_OPENED_FAILED -1
#ifdef _WIN64
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif
DLLEXPORT int16_t I2C_init(const char* filepath);
DLLEXPORT void I2C_free(int16_t fd);
DLLEXPORT int I2C_setAddr(int16_t fd, uint8_t addr);
DLLEXPORT int I2C_writeReg(int16_t fd, uint8_t reg, uint8_t val);
DLLEXPORT uint8_t I2C_readReg(int16_t fd, uint8_t reg);
DLLEXPORT int8_t I2C_readRegFlag(int16_t fd, uint8_t reg);
#endif
+80
View File
@@ -0,0 +1,80 @@
#define _USE_MATH_DEFINES
#include <cmath>
#include <vector>
#include "fft.h"
#include "Matrix.hh"
using namespace qing;
void functf1d(float A[DN])
{
unsigned long int i;
float dt, tt;
float p = 0.1;
float freq = 10.0; // 10Hz
float fs = DN; // 采样率65536Hz
dt = 1.0 / fs;
for(i = 0; i < DN; i++)
{
tt = (float)i * dt;
A[i] = sin(2.0 * M_PI * freq * tt) * p; // 正弦波
//A[1][i] = 0.0; // 虚部为0
}
}
// 初始化FFT数组
static void fft_init(float A[2][DN], Matrx<float>& M)
{
for (int i = 0; i < DN; i++) {
float a = M.witch(i, 0);
A[0][i] = a;
A[1][i] = 0;
}
}
// 计算FFT结果
static void fft_result(std::vector<float>& R, float A[2][DN])
{
for (int i = 0; i < DN/2; i++) {
float a = pow(A[0][i], 2) + pow(A[1][i], 2);
R[i] = sqrt(a) / (DN/2);
//std::cout << i << ":\t" << R[i] << std::endl;
}
}
extern "C" {
// 生成正弦波
#ifdef _WIN64
__declspec(dllexport) void* gen_wave()
#else
void* gen_wave()
#endif
{
std::vector<float> A(DN);
functf1d(A.data());
return (void *)new Matrx<float>(DN, 1, A);
}
// 开始FFT
#ifdef _WIN64
__declspec(dllexport) void* start_fft(void* M)
#else
void* start_fft(void* M)
#endif
{
float A[2][DN];
std::vector<float> R(N);
fft_init(A, *(Matrx<float> *)M);
//fft1(A, 1); // 复序列基2 FFT
fft1r1(A, 1); // 单实序列
fft_result(R, A);
return (void*) new Matrx<float>(N/2, 1, R);
}
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef FFT_H
#define FFT_H
#ifndef N
#define N 32768
#endif
#ifndef DN
#define DN (N*2)
#endif
#define eps 1.0e-9
#ifdef __cplusplus
extern "C" {
#endif
void initial(void);
void fft1(float A[2][N], int ifft);
unsigned long int inverseq(unsigned long int i1,
unsigned long int imax);
void fft1r2(float A[2][N], float B[2][N], int ifft);
void fft1r1(float A[2][DN], int ifft);
#ifdef __cplusplus
}
#endif
#endif
+231
View File
@@ -0,0 +1,231 @@
/* fft1.c: fast Fourier transform base on 2 for one dimensional
complex array, direct and inverse,
This program is contributed by Jiang Yong, 2003. 1. */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "fft.h"
static int ifirst=0, sign=-1;
static unsigned M, jup[64], kup[64];
static float pi, wr[N], wi[N];
/********* initial **************/
void initial(void)
{
unsigned long int i, j, ni;
float unit, tt;
ni = N;
M = 0;
while (ni > 1) { ni=ni/2; M=M+1; }
ni = 1;
for(j=0; j<M; j++) ni = ni * 2;
if ((ni-N) != 0)
{
printf("N isn't compatible! (N => %i, ni -> %lu)\n", N, ni);
exit(1);
}
kup[0] = N/2; /* kup[l] = 2 ^ (M - l - 1) */
jup[0] =1;
for (j=1;j<M;j++)
{
kup[j]=kup[j-1]/2;
jup[j]=jup[j-1]*2;
}
pi=4.0*atan((float)1.0);
unit=2.0*pi/((float)N);
tt=0.0;
for(i=0;i<N;i++)
{
wr[i] = cos(tt);
wi[i] = sign * sin(tt);
tt = tt + unit;
}
}
/** ifft=1<-->FFT ** ifft=-1<-->IFFT **/
void fft1(float A[2][N], int ifft)
{
unsigned long int l, k, i, j, jl0, jl0k, jl1k, reseq, ij;
float wrc, wis, ar, ai, x[2][N];
if (ifirst == 0) { initial(), ifirst=1; }
if (ifft == -1)
for(i=0;i<N;i++) A[1][i] = -A[1][i];
for (l=0; l<M; l++)
{
for (j=0; j<jup[l]; j++)
{
reseq=inverseq(j, l);
ij=kup[l]*reseq;
jl0=2*kup[l]*j;
wrc = wr[ij];
wis = wi[ij];
for (k=0; k<kup[l]; k++)
{
jl0k = jl0 + k;
jl1k = jl0 + kup[l] + k;
ar = A[0][jl1k] * wrc - A[1][jl1k] * wis;
ai = A[0][jl1k] * wis + A[1][jl1k] * wrc;
A[0][jl1k] = A[0][jl0k] - ar;
A[1][jl1k] = A[1][jl0k] - ai;
A[0][jl0k] = A[0][jl0k] + ar;
A[1][jl0k] = A[1][jl0k] + ai;
}
}
}
for (i=0; i<N; i++)
{
j=inverseq(i, M);
x[0][i] = A[0][j];
x[1][i] = A[1][j];
}
for (i=0; i<N; i++)
{
A[0][i] = x[0][i];
A[1][i] = x[1][i];
}
if (ifft == -1)
for (i=0; i<N; i++)
{
A[0][i] = A[0][i]/((float)N);
A[1][i] = -A[1][i]/((float)N);
}
}
/*********************************/
unsigned long int inverseq(unsigned long int i1,
unsigned long int imax)
{
unsigned long int j, ii, i0, inv;
i0 = i1;
inv = 0;
for (j=0; j < imax; j++)
{
ii=i0%2;
inv=2*inv+ii;
i0=(i0-ii)/2;
}
return (inv);
}
/* ifft=1 <--> FFT ** ifft=-1 <--> IFFT */
void fft1r2(float A[2][N], float B[2][N], int ifft)
{
unsigned long int k, j;
float norm, c[2][N];
if (ifft != -1)
{
for (k=0; k<N; k++)
{
c[0][k]=A[0][k];
c[1][k]=B[0][k];
}
fft1(c, 1);
A[0][0] = c[0][0];
A[1][0] = 0.0;
B[0][0] = c[1][0];
B[1][0] = 0.0;
for (j=1; j<N; j++)
{
A[0][j] = 0.5 * (c[0][j] + c[0][N-j]);
A[1][j] = 0.5 * (c[1][j] - c[1][N-j]);
B[0][j] = 0.5 * (c[1][j] + c[1][N-j]);
B[1][j] = -0.5 * (c[0][j] - c[0][N-j]);
}
} /* end if*/
else
{
for (j=0; j<N/2; j++)
/* check {xj}, {yj} is to real {Ak}, {Bk} */
{
if (j == 0) norm = 2.0 * fabs(A[1][0])
+ 2.0 * fabs(B[1][0]);
else norm = fabs(A[1][j]+A[1][N-j])
+ fabs(A[0][j]-A[0][N-j])
+ fabs(B[1][j]+B[1][N-j])
+ fabs(B[0][j]-B[0][N-j]);
if (norm > eps)
{
printf("The IDFT of the series isn't real!\n");
exit(0);
}
}
for (j=0; j<N; j++)
{
c[0][j]=A[0][j] -B[1][j];
c[1][j]=A[1][j] +B[0][j];
}
fft1(c, -1);
for (k=0; k<N; k++)
{
A[0][k] = c[0][k];
B[0][k] = c[1][k];
A[1][k] = 0.0;
B[1][k] = 0.0;
}
} /* end else */
}
/* ifft = 1 <--> FFT ** ifft = -1 <--> IFFT */
void fft1r1(float A[2][DN], int ifft)
{
unsigned long int j, k, dk;
float norm, c[2][N], d[2][N], tt, hunit, cosj, sinj;
hunit = 4.0 * atan(1.0) / (float)N;
if ( ifft != -1)
{
for (k=0; k<N; k++)
{
dk = k + k;
c[0][k] = A[0][dk];
d[0][k] = A[0][dk+1];
c[1][k] = 0.0;
d[1][k] = 0.0;
}
fft1r2(c, d, 1);
for (j = 0; j < N; j++)
{
tt = (float)j * hunit;
cosj = cos(tt);
sinj = sign * sin(tt);
A[0][j] = c[0][j] + cosj * d[0][j] - sinj * d[1][j];
A[1][j] = c[1][j] + cosj * d[1][j] + sinj * d[0][j];
}
A[0][N] = c[0][0] - d[0][0];
A[1][N] = c[1][0] - d[1][0];
for(j=1; j<N; j++)
{
A[0][DN-j] = A[0][j];
A[1][DN-j] = -A[1][j];
}
} // end if
else
{
for (j = 0; j <N; j++) // check {xj} is to real {Ak}
{
if (j ==0) norm = 2.0 * fabs(A[1][0]);
else norm = fabs(A[1][j]+A[1][DN-j])+
fabs(A[0][j]-A[0][DN-j]);
if (norm > eps)
{
printf("The IDFT of the series {xj} isn't real!\n");
exit(0);
}
}
for (j = 0; j <N; j++)
{
c[0][j]= 0.5 * (A[0][j]+A[0][N+j]);
c[1][j]= 0.5 * (A[1][j]+A[1][N+j]);
tt = (float) j * hunit;
cosj = cos(tt);
sinj = -sign*sin(tt);
d[0][j]= 0.5 * (cosj * (A[0][j]-A[0][N+j])
-sinj*(A[1][j] - A[1][N+j]));
}
fft1r2(c, d, -1);
for (k=0; k<N; k++)
{
dk = k+k;
A[0][dk]=c[0][k];
A[0][dk+1]=d[0][k];
A[1][dk]=0.0;
A[1][dk+1]=0.0;
}
} // end else
}
+63
View File
@@ -0,0 +1,63 @@
/**
* 一维卡尔曼滤波器
* 使用时先定义一个kalman指针,然后调用kalmanCreate()创建一个滤波器
* 每次读取到传感器数据后即可调用KalmanFilter()来对数据进行滤波
* 使用示例
* extKalman_t p; //定义一个卡尔曼滤波器结构体
* float SersorData; //需要进行滤波的数据
* KalmanCreate(&p,20,200); //初始化该滤波器的Q=20 R=200参数
* while(1)
* {
* SersorData = sersor(); //获取数据
* SersorData = KalmanFilter(&p,SersorData); //对数据进行滤波
* }
*/
#include "kalman.h"
/**
* @name kalmanCreate
* @brief 创建一个卡尔曼滤波器
* @param p: 滤波器
* T_Q:系统噪声协方差
* T_R:测量噪声协方差
*
* @retval none
* @attention R固定,Q越大,代表越信任侧量值,Q无穷代表只用测量值
* 反之,Q越小代表越信任模型预测值,Q为零则是只用模型预测
*/
void KalmanCreate(extKalman_t* p, float T_Q, float T_R)
{
p->X_last = (float)0;
p->P_last = 0;
p->Q = T_Q;
p->R = T_R;
p->A = 1;
p->B = 0;
p->H = 1;
p->X_mid = p->X_last;
}
/**
* @name KalmanFilter
* @brief 卡尔曼滤波器
* @param p: 滤波器
* dat:待滤波数据
* @retval 滤波后的数据
* @attention Z(k)是系统输入,即测量值 X(k|k)是卡尔曼滤波后的值,即最终输出
* A=1 B=0 H=1 I=1 W(K) V(k)是高斯白噪声,叠加在测量值上了,可以不用管
* 以下是卡尔曼的5个核心公式
* 一阶H'即为它本身,否则为转置矩阵
*/
float KalmanFilter(extKalman_t* p, float dat)
{
p->X_mid = p->A * p->X_last; //百度对应公式(1) x(k|k-1) = A*X(k-1|k-1)+B*U(k)+W(K)
p->P_mid = p->A * p->P_last + p->Q; //百度对应公式(2) p(k|k-1) = A*p(k-1|k-1)*A'+Q
p->kg = p->P_mid / (p->P_mid + p->R); //百度对应公式(4) kg(k) = p(k|k-1)*H'/(H*p(k|k-1)*H'+R)
p->X_now = p->X_mid + p->kg * (dat - p->X_mid); //百度对应公式(3) x(k|k) = X(k|k-1)+kg(k)*(Z(k)-H*X(k|k-1))
p->P_now = (1 - p->kg) * p->P_mid; //百度对应公式(5) p(k|k) = (I-kg(k)*H)*P(k|k-1)
p->P_last = p->P_now; //状态更新
p->X_last = p->X_now;
return p->X_now; //输出预测结果x(k|k)
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef _KALMAN_H
#define _KALMAN_H
typedef struct
{
float X_last; //上一时刻的最优结果 X(k-|k-1)
float X_mid; //当前时刻的预测结果 X(k|k-1)
float X_now; //当前时刻的最优结果 X(k|k)
float P_mid; //当前时刻预测结果的协方差 P(k|k-1)
float P_now; //当前时刻最优结果的协方差 P(k|k)
float P_last; //上一时刻最优结果的协方差 P(k-1|k-1)
float kg; //kalman增益
float A; //系统参数
float B;
float Q;
float R;
float H;
} extKalman_t;
void KalmanCreate(extKalman_t* p, float T_Q, float T_R);
float KalmanFilter(extKalman_t* p, float dat);
#endif
+354
View File
@@ -0,0 +1,354 @@
#ifdef _WIN32
#include <windows.h>
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "user32.lib")
#define _USE_MATH_DEFINES
#include <cmath>
#else
#include <alsa/asoundlib.h>
#include <cmath>
#endif
#include <iostream>
#include <cstdint>
#include <vector>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#ifndef M_LN2
#define M_LN2 0.693147180559945309417
#endif
#ifdef _WIN32
// Windows 回调结构
struct PlaybackContext {
volatile bool done;
WAVEHDR* header;
};
// Windows 播放完成回调函数
void CALLBACK waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance,
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
if (uMsg == WOM_DONE) {
PlaybackContext* ctx = reinterpret_cast<PlaybackContext*>(dwInstance);
ctx->done = true;
}
}
#endif
extern "C" {
#ifdef _WIN32
#define SND_EXPORT __declspec(dllexport)
#else
#define SND_EXPORT
#endif
SND_EXPORT bool play_frequency(float frequency, int32_t duration_ms,
double volume, int32_t channels,
int32_t sample_rate) {
// 参数验证
if (frequency <= 0.0f || duration_ms <= 0 ||
volume <= 0.0 || volume > 1.0 ||
channels <= 0 || channels > 8 ||
sample_rate <= 0) {
std::cerr << "Invalid parameters" << std::endl;
return false;
}
// 精确计算帧数:使用 round 而非截断
// duration_ms / 1000.0 得到秒数,乘以采样率得到帧数
int64_t num_frames = static_cast<int64_t>(
std::round(static_cast<double>(sample_rate) * duration_ms / 1000.0)
);
if (num_frames <= 0) {
std::cerr << "Duration too short to generate any frames" << std::endl;
return false;
}
// 计算实际播放时长(用于调试)
double actual_duration_ms = static_cast<double>(num_frames) / sample_rate * 1000.0;
std::cout << "Requested duration: " << duration_ms << " ms, "
<< "Actual duration: " << actual_duration_ms << " ms, "
<< "Frames: " << num_frames << std::endl;
// 计算总采样点数
int32_t total_samples = static_cast<int32_t>(num_frames * channels);
std::vector<int16_t> buffer(total_samples);
// 计算振幅(应用对数音量缩放以获得更自然的音量控制)
int32_t amplitude = static_cast<int32_t>(32767.0 * volume);
// 生成正弦波数据
for (int64_t i = 0; i < num_frames; i++) {
// 使用 double 计算以提高精度
double t = static_cast<double>(i) / sample_rate;
double value = sin(2.0 * M_PI * frequency * t);
// 应用淡入淡出以避免咔嗒声
int64_t fade_samples = static_cast<int64_t>(sample_rate * 0.005); // 5ms 淡入淡出
double fade_factor = 1.0;
if (i < fade_samples) {
// 淡入:从 0 到 1
fade_factor = static_cast<double>(i) / fade_samples;
} else if (i >= num_frames - fade_samples) {
// 淡出:从 1 到 0
fade_factor = static_cast<double>(num_frames - 1 - i) / fade_samples;
}
int16_t sample = static_cast<int16_t>(
amplitude * value * fade_factor
);
// 填充所有声道
for (int32_t ch = 0; ch < channels; ch++) {
buffer[i * channels + ch] = sample;
}
}
#ifdef _WIN32
// ===== Windows 实现(带回调,精确计时) =====
WAVEFORMATEX wfx;
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nChannels = channels;
wfx.nSamplesPerSec = sample_rate;
wfx.wBitsPerSample = 16;
wfx.nBlockAlign = channels * 2; // 16-bit = 2 bytes per sample
wfx.nAvgBytesPerSec = sample_rate * wfx.nBlockAlign;
wfx.cbSize = 0;
PlaybackContext context;
context.done = false;
context.header = new WAVEHDR();
// 打开波形设备,使用回调函数
HWAVEOUT hWaveOut;
MMRESULT result = waveOutOpen(
&hWaveOut,
WAVE_MAPPER,
&wfx,
reinterpret_cast<DWORD_PTR>(waveOutProc), // 回调函数
reinterpret_cast<DWORD_PTR>(&context), // 传递给回调的数据
CALLBACK_FUNCTION
);
if (result != MMSYSERR_NOERROR) {
std::cerr << "Cannot open audio device. Error code: " << result << std::endl;
delete context.header;
return false;
}
// 准备 WAVEHDR
WAVEHDR& header = *context.header;
ZeroMemory(&header, sizeof(WAVEHDR));
header.lpData = reinterpret_cast<char*>(buffer.data());
header.dwBufferLength = static_cast<DWORD>(total_samples * sizeof(int16_t));
header.dwFlags = 0;
header.dwLoops = 0;
result = waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
if (result != MMSYSERR_NOERROR) {
std::cerr << "Failed to prepare audio header. Error code: " << result << std::endl;
waveOutClose(hWaveOut);
delete context.header;
return false;
}
// 写入音频数据
result = waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR));
if (result != MMSYSERR_NOERROR) {
std::cerr << "Failed to write audio data. Error code: " << result << std::endl;
waveOutUnprepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
waveOutClose(hWaveOut);
delete context.header;
return false;
}
// 等待播放完成(使用事件等待而非轮询)
auto start_time = timeGetTime();
while (!context.done) {
// 使用 MsgWaitForMultipleObjects 等待消息或超时
// 这样可以在等待的同时处理 Windows 消息
DWORD wait_result = MsgWaitForMultipleObjects(0, NULL, FALSE, 100, QS_ALLINPUT);
if (wait_result == WAIT_OBJECT_0) {
// 处理 Windows 消息
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// 超时检查(防止无限等待)
DWORD elapsed = timeGetTime() - start_time;
if (elapsed > static_cast<DWORD>(duration_ms + 1000)) {
std::cerr << "Playback timeout" << std::endl;
waveOutReset(hWaveOut);
break;
}
}
auto end_time = timeGetTime();
DWORD actual_play_time = end_time - start_time;
std::cout << "Actual playback time: " << actual_play_time << " ms" << std::endl;
// 清理
waveOutUnprepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
waveOutClose(hWaveOut);
delete context.header;
#else
// ===== Linux ALSA 实现 =====
snd_pcm_t *pcm_handle = nullptr;
const char* device = "default";
int rc = snd_pcm_open(&pcm_handle, device, SND_PCM_STREAM_PLAYBACK, 0);
if (rc < 0) {
std::cerr << "Cannot open device '" << device << "': "
<< snd_strerror(rc) << std::endl;
return false;
}
// 设置硬件参数
snd_pcm_hw_params_t *hw_params;
snd_pcm_hw_params_alloca(&hw_params);
rc = snd_pcm_hw_params_any(pcm_handle, hw_params);
if (rc < 0) {
std::cerr << "Cannot get hardware parameters: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
rc = snd_pcm_hw_params_set_access(pcm_handle, hw_params,
SND_PCM_ACCESS_RW_INTERLEAVED);
if (rc < 0) {
std::cerr << "Cannot set access type: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
rc = snd_pcm_hw_params_set_format(pcm_handle, hw_params,
SND_PCM_FORMAT_S16_LE);
if (rc < 0) {
std::cerr << "Cannot set sample format: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
rc = snd_pcm_hw_params_set_channels(pcm_handle, hw_params, channels);
if (rc < 0) {
std::cerr << "Cannot set channel count: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
unsigned int hw_sample_rate = static_cast<unsigned int>(sample_rate);
rc = snd_pcm_hw_params_set_rate_near(pcm_handle, hw_params,
&hw_sample_rate, 0);
if (rc < 0) {
std::cerr << "Cannot set sample rate: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
if (hw_sample_rate != static_cast<unsigned int>(sample_rate)) {
std::cout << "Requested sample rate " << sample_rate
<< " Hz not supported, using " << hw_sample_rate << " Hz" << std::endl;
}
// 设置缓冲区大小和周期
snd_pcm_uframes_t buffer_size;
rc = snd_pcm_hw_params_get_buffer_size_max(hw_params, &buffer_size);
if (buffer_size > static_cast<snd_pcm_uframes_t>(num_frames)) {
snd_pcm_hw_params_set_buffer_size_near(pcm_handle, hw_params,
&reinterpret_cast<snd_pcm_uframes_t&>(num_frames));
}
rc = snd_pcm_hw_params(pcm_handle, hw_params);
if (rc < 0) {
std::cerr << "Cannot set hardware parameters: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
// 设置软件参数
snd_pcm_sw_params_t *sw_params;
snd_pcm_sw_params_alloca(&sw_params);
rc = snd_pcm_sw_params_current(pcm_handle, sw_params);
if (rc < 0) {
std::cerr << "Cannot get software parameters: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
rc = snd_pcm_sw_params_set_start_threshold(pcm_handle, sw_params,
static_cast<snd_pcm_uframes_t>(num_frames));
if (rc < 0) {
std::cerr << "Cannot set start threshold: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
rc = snd_pcm_sw_params(pcm_handle, sw_params);
if (rc < 0) {
std::cerr << "Cannot set software parameters: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
// 播放音频(写入数据)
snd_pcm_sframes_t frames_written = 0;
snd_pcm_uframes_t frames_to_write = static_cast<snd_pcm_uframes_t>(num_frames);
// 循环写入直到所有数据都播放完毕
while (frames_written < static_cast<snd_pcm_sframes_t>(num_frames)) {
snd_pcm_uframes_t remaining = static_cast<snd_pcm_uframes_t>(num_frames - frames_written);
rc = snd_pcm_writei(pcm_handle,
buffer.data() + (frames_written * channels),
remaining);
if (rc == -EPIPE) {
// 缓冲区下溢,需要恢复
std::cerr << "Buffer underrun occurred" << std::endl;
snd_pcm_prepare(pcm_handle);
rc = snd_pcm_writei(pcm_handle,
buffer.data() + (frames_written * channels),
remaining);
}
if (rc < 0) {
std::cerr << "Error writing to PCM device: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
if (rc > 0) {
frames_written += rc;
}
}
// 等待所有数据播放完成
rc = snd_pcm_drain(pcm_handle);
if (rc < 0) {
std::cerr << "Error draining PCM device: " << snd_strerror(rc) << std::endl;
snd_pcm_close(pcm_handle);
return false;
}
snd_pcm_close(pcm_handle);
#endif
return true;
}
} // extern "C"
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <cuda_runtime.h>
#include <stdio.h>
#define CHECK(call) do {\
cudaError_t err = call;\
if (err != cudaSuccess) {\
fprintf(stderr, "CUDA error at %s:%d\n", __FILE__, __LINE__);\
fprintf(stderr, "%s\n", cudaGetErrorString(err));\
exit(EXIT_FAILURE);\
}\
} while (0)
+872
View File
@@ -0,0 +1,872 @@
#include <cstdint>
#include <stdint.h>
#include <time.h>
#include <random>
#include "error.cuh"
extern "C" {
/* 创建一个设备数组 */
static void* _crtDevArr(const size_t m)
{
void *d;
CHECK(cudaMalloc(&d, m));
return d;
}
/* 释放一个设备数组 */
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_Free(void* d)
#else
void Cu4cj_Free(void* d)
#endif
{
CHECK(cudaFree(d));
}
/* 创建一个单精度浮点数组 输入数组的长度(不初始化) */
#ifdef _WIN64
__declspec(dllexport) void* Cu4cj_Orig(uint64_t n)
#else
void* Cu4cj_Orig(int64_t n)
#endif
{
return _crtDevArr(n * sizeof(float));
}
/*
* 从主机内存拷贝数据到设备内存
* FIXME: 是否需要做成输入目的地址
*/
#ifdef _WIN64
__declspec(dllexport) float* Cu4cj_ToDev(float* h, uint64_t n)
#else
float* Cu4cj_ToDev(float* h, int64_t n)
#endif
{
float* d = (float*)Cu4cj_Orig(n);
CHECK(cudaMemcpy(d, h, n * sizeof(float), cudaMemcpyHostToDevice));
return d;
}
/* 从设备内存拷贝数据到主机内存 */
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_ToHost(float* d, float* h, const uint64_t n)
#else
void Cu4cj_ToHost(float* d, float* h, int64_t n)
#endif
{
CHECK(cudaMemcpy(h, d, n * sizeof(float), cudaMemcpyDeviceToHost));
}
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_Copy(float* d, float* h, const uint64_t n)
#else
void Cu4cj_Copy(float* d, float* h, int64_t n)
#endif
{
CHECK(cudaMemcpy(h, d, n * sizeof(float), cudaMemcpyDeviceToHost));
}
/* 创建一个单精度浮点数组并将其初始化为零
* FIXME: 当n == 10000 时进程被杀死 */
#ifdef _WIN64
__declspec(dllexport) float* Cu4cj_Zero(const uint64_t n)
#else
float* Cu4cj_Zero(int64_t n)
#endif
{
float* d = (float*)Cu4cj_Orig(n);
CHECK(cudaMemset(d, 0, n*sizeof(float)));
return d;
}
/* 正态分布初始化(适用于神经网络权重) */
#ifdef _WIN64
__declspec(dllexport) float* Cu4cj_Normal(uint64_t n, uint64_t fan_in, uint64_t fan_out)
#else
float* Cu4cj_Normal(int64_t n, int64_t fan_in, int64_t fan_out)
#endif
{
// 计算 Xavier/Glorot 初始化的标准差
float std = sqrt(2.0f / (fan_in + fan_out));
// 先在主机上分配内存并生成正态分布随机数
float* h = (float*)malloc(n * sizeof(float));
/* 初始化随机数种子 */
srand(time(NULL));
for (size_t i = 0; i < n; i++) {
// 使用 Box-Muller 变换生成正态分布随机数
float u1 = (float)rand() / RAND_MAX;
float u2 = (float)rand() / RAND_MAX;
float z = sqrt(-2.0f * log(u1)) * cos(2.0f * 3.1415926535f * u2);
h[i] = z * std;
}
// 在设备上分配内存
float* d = (float*)Cu4cj_Orig(n);
// 将主机内存中的数据拷贝到设备内存
CHECK(cudaMemcpy(d, h, n * sizeof(float), cudaMemcpyHostToDevice));
// 释放主机内存
free(h);
return d;
}
// 向量加法
static void __global__ _devAdd(float* d1, float* d2, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] + d2[i];
}
}
// 向量加法
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_Add(float* d1, float* d2, float* d_r, uint64_t n)
#else
void Cu4cj_Add(float* d1, float* d2, float* d_r, int64_t n)
#endif
{
_devAdd<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 向量减法
static void __global__ _devSub(float* d1, float* d2, float* r, int64_t n)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] - d2[i];
}
}
// 向量减法
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_Sub(float* d1, float* d2, float* d_r, uint64_t n)
#else
void Cu4cj_Sub(float* d1, float* d2, float* d_r, int64_t n)
#endif
{
_devSub<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 向量依次相乘
static void __global__ _devMul(float* d1, float* d2, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] * d2[i];
}
}
// 向量依次相乘
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_Mul(float* d1, float* d2, float* d_r, uint64_t n)
#else
void Cu4cj_Mul(float* d1, float* d2, float* d_r, int64_t n)
#endif
{
_devMul<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 向量依次相除
static void __global__ _devDiv(float* d1, float* d2, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] / d2[i];
}
}
// 向量依次相除
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_Div(float* d1, float* d2, float* d_r, uint64_t n)
#else
void Cu4cj_Div(float* d1, float* d2, float* d_r, int64_t n)
#endif
{
_devDiv<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 向量加法广播
static void __global__ _devAddBroadcast(float* d1, float a, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] + a;
}
}
// 加法广播
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_AddBroadcast(float* d1, float a, float* d_r, uint64_t n)
#else
void Cu4cj_AddBroadcast(float* d1, float a, float* d_r, int64_t n)
#endif
{
_devAddBroadcast<<<(n + 255) / 256, 255>>>(d1, a, d_r, n);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 减法广播
static void __global__ _devSubBroadcast(float* d1, float a, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] - a;
}
}
// 减法广播
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_SubBroadcast(float* d1, float a, float* d_r, uint64_t n)
#else
void Cu4cj_SubBroadcast(float* d1, float a, float* d_r, int64_t n)
#endif
{
_devSubBroadcast<<<(n + 255) / 256, 256>>>(d1, a, d_r, n);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 乘法广播
static void __global__ _devMulBroadcast(float* d1, float a, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] * a;
}
}
// 乘法广播
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_MulBroadcast(float* d1, float a, float* d_r, uint64_t n)
#else
void Cu4cj_MulBroadcast(float* d1, float a, float* d_r, int64_t n)
#endif
{
_devMulBroadcast<<<(n + 255) / 256, 256>>>(d1, a, d_r, n);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 除法广播
static void __global__ _devDivBroadcast(float* d1, float a, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] / a;
}
}
// 除法广播
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_DivBroadcast(float* d1, float a, float* d_r, uint64_t n)
#else
void Cu4cj_DivBroadcast(float* d1, float a, float* d_r, int64_t n)
#endif
{
_devDivBroadcast<<<(n + 255) / 256, 256>>>(d1, a, d_r, n);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 矩阵乘法
static void __global__ _devMatDot(float* d1, float* d2, float* r, int64_t m, int64_t n, int64_t k) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
int j = blockIdx.y * blockDim.y + threadIdx.y;
if (i < m && j < k) {
float sum = 0.0f;
for (int p = 0; p < n; p++) {
sum += d1[i * n + p] * d2[p * k + j];
}
r[i * k + j] = sum;
}
}
// 矩阵乘法
// m、n、k 分别为矩阵 A、B、C 的行数、列数
// 其中 n 为矩阵 A 的列数,也为矩阵 B 的行数
#ifdef _WIN64
__declspec(dllexport) void Cu4cj_MatDot(float* d1, float* d2, float* d_r, uint64_t m, uint64_t n, uint64_t k)
#else
void Cu4cj_MatDot(float* d1, float* d2, float* d_r, int64_t m, int64_t n, int64_t k)
#endif
{
dim3 blockDim(16, 16);
dim3 gridDim((m + blockDim.x - 1) / blockDim.x, (k + blockDim.y - 1) / blockDim.y);
_devMatDot<<<gridDim, blockDim>>>(d1, d2, d_r, m, n, k);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
}
// 矩阵转置
static void __global__ _devTransport(float* d1, float* d2, int64_t m, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
int j = blockIdx.y * blockDim.y + threadIdx.y;
if (i < m && j < n) {
d2[j * m + i] = d1[i * n + j];
}
}
#ifdef _WIN64
__declspec(dllexport) float* Cu4cj_Transport(float *m, uint64_t r, uint64_t c)
#else
float* Cu4cj_Transport(float *m, uint64_t r, uint64_t c)
#endif
{
float *d_r = (float*)Cu4cj_Zero(c * r * sizeof(float));
_devTransport<<<1, c * r>>>(m, d_r, c, r);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return d_r;
}
// Sigmoid 激活函数,输入为矩阵,输出为矩阵
static void __global__ _devSigmoid(float* d1, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = 1.0f / (1.0f + exp(-d1[i]));
}
}
// Sigmoid 导数
static void __global__ _devSigmoidDerivative(float* d1, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] * (1.0f - d1[i]);
}
}
// ReLU
static void __global__ _devReLU(float* d1, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] > 0.0f ? d1[i] : 0.0f;
}
}
// ReLU 导数
static void __global__ _devReLUDerivative(float* d1, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] > 0.0f ? 1.0f : 0.0f;
}
}
// Leaky_ReLU
static void __global__ _devLeakyReLU(float* d1, float* r, int64_t n, float a) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] > 0.0f ? d1[i] : a * d1[i];
}
}
// Leaky_ReLU 导数
static void __global__ _devLeakyReLU_Derivative(float* d1, float* r, int64_t n, float a) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = d1[i] > 0.0f ? 1.0f : a;
}
}
// Tanh 激活函数,输入为矩阵,输出为矩阵
static void __global__ _devTanh(float* d1, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = (exp(d1[i]) - exp(-d1[i])) / (exp(d1[i]) + exp(-d1[i]));
}
}
// Tanh 导数
static void __global__ _devTanhDerivative(float* d1, float* r, int64_t n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
r[i] = 1.0f - d1[i] * d1[i];
}
}
}
namespace qing {
class Matrix {
public:
Matrix(uint64_t r, uint64_t c) {
this->r = r;
this->c = c;
this->d = (float*)Cu4cj_Zero(r * c * sizeof(float));
}
Matrix(uint64_t r, uint64_t c, float* d) {
this->r = r;
this->c = c;
this->d = d;
}
Matrix(const Matrix& m) {
this->r = m.getR();
this->c = m.getC();
this->d = (float*)Cu4cj_Zero(m.getR() * m.getC() * sizeof(float));
Cu4cj_Copy(m.getD(), d, m.getR() * m.getC() * sizeof(float));
}
~Matrix() {
Cu4cj_Free(d);
}
uint64_t getR() const {
return r;
}
uint64_t getC() const {
return c;
}
float* getD() const {
return d;
}
float get(uint64_t r, uint64_t c) const {
float h;
CHECK(cudaMemcpy(&h, &d[c * this->r + r], sizeof(float), cudaMemcpyDeviceToHost));
return h;
}
void set(uint64_t r, uint64_t c, float v) {
CHECK(cudaMemcpy(&d[c * this->r + r], &v, sizeof(float), cudaMemcpyHostToDevice));
}
bool hasSameShape(const Matrix& m) {
return r == m.getR() && c == m.getC();
}
bool hasDotShape(const Matrix& m) {
return c == m.getR();
}
Matrix operator+(const Matrix& m) {
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
Cu4cj_Add(d, m.getD(), d_r, r * c);
return Matrix(r, c, d_r);
}
Matrix operator+(float a) {
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
Cu4cj_AddBroadcast(d, a, d_r, r * c);
return Matrix(r, c, d_r);
}
Matrix operator-(const Matrix& m) {
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
Cu4cj_Sub(d, m.getD(), d_r, r * c);
return Matrix(r, c, d_r);
}
Matrix operator-(float a) {
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
Cu4cj_SubBroadcast(d, a, d_r, r * c);
return Matrix(r, c, d_r);
}
Matrix operator*(const Matrix& m) {
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
Cu4cj_Mul(d, m.getD(), d_r, r * c);
return Matrix(r, c, d_r);
}
Matrix operator*(float a) {
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
Cu4cj_MulBroadcast(d, a, d_r, r * c);
return Matrix(r, c, d_r);
}
Matrix operator/(const Matrix& m) {
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
Cu4cj_Div(d, m.getD(), d_r, r * c);
return Matrix(r, c, d_r);
}
Matrix operator/(float a) {
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
Cu4cj_DivBroadcast(d, a, d_r, r * c);
return Matrix(r, c, d_r);
}
Matrix dot(const Matrix& m) {
//if (!hasDotShape(m)) {
// throw std::invalid_argument("Dot product: matrices must have compatible shapes");
//}
float *d_r = (float*)Cu4cj_Zero(r * m.getC() * sizeof(float));
Cu4cj_MatDot(d, m.getD(), d_r, r, c, m.getC());
return Matrix(r, m.getC(), d_r);
}
Matrix T() {
float *d_r = (float*)Cu4cj_Transport(d, r, c);
return Matrix(c, r, d_r);
}
private:
uint64_t r, c;
float* d;
};
}
using namespace qing;
extern "C" {
#ifdef _WIN64
__declspec(dllexport) void* createMatrix(const uint64_t r, const uint64_t c)
#else
void* createMatrix(int64_t r, int64_t c)
#endif
{
return(void*) new Matrix(r, c);
}
#ifdef _WIN64
__declspec(dllexport) void freeMatrix(void* m)
#else
void freeMatrix(void* m)
#endif
{
delete (Matrix*)m;
}
#ifdef _WIN64
__declspec(dllexport) void* xavierNormalInit(uint64_t fan_in, uint64_t fan_out)
#else
void* xavierNormalInit(uint64_t fan_in, uint64_t fan_out)
#endif
{
float* d = Cu4cj_Normal(fan_in * fan_out, fan_in, fan_out);
return (void *)new Matrix(fan_in, fan_out, d);
}
#ifdef _WIN64
__declspec(dllexport) void* xavierUniformInit(uint64_t fan_in, uint64_t fan_out)
#else
void* xavierUniformInit(uint64_t fan_in, uint64_t fan_out)
#endif
{
uint64_t n = fan_in * fan_out;
// 先在主机上分配内存并生成正态分布随机数
float* h = (float*)malloc(n * sizeof(float));
/* 使用高质量的随机数生成器 */
static std::mt19937 generator(std::random_device{}());
/* 计算区间范围:sqrt(2 / (fan_in + fan_out)) */
float scale = std::sqrt(2.0f / (fan_in + fan_out));
/* 创建均匀分布对象,范围为[-scale, scale] */
std::uniform_real_distribution<float> distribution(-scale, scale);
/* 遍历权重矩阵,进行赋值 */
uint64_t k=0;
for (uint64_t i=0; i<fan_in; ++i)
for (uint64_t j=0; j<fan_out; ++j)
h[k++] = distribution(generator);
// 在设备上分配内存
float* d = (float*)Cu4cj_Orig(n*sizeof(float));
// 将主机内存中的数据拷贝到设备内存
CHECK(cudaMemcpy(d, h, n * sizeof(float), cudaMemcpyHostToDevice));
// 释放主机内存
free(h);
return (void *)new Matrix(fan_in, fan_out, d);
}
#ifdef _WIN64
__declspec(dllexport) uint64_t getrowMatrix(void *m)
#else
uint64_t getrowMatrix(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
return m1->getR();
}
#ifdef _WIN64
__declspec(dllexport) uint64_t getcolMatrix(void *m)
#else
uint64_t getcolMatrix(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
return m1->getC();
}
#ifdef _WIN64
__declspec(dllexport) float getMatrix(void *m, uint64_t r, uint64_t c)
#else
float getMatrix(void *m, uint64_t r, uint64_t c)
#endif
{
Matrix* m1 = (Matrix*)m;
return m1->get(r, c);
}
#ifdef _WIN64
__declspec(dllexport) void setMatrix(void *m, uint64_t r, uint64_t c, float v)
#else
void setMatrix(void *m, uint64_t r, uint64_t c, float v)
#endif
{
Matrix* m1 = (Matrix*)m;
m1->set(r, c, v);
}
#ifdef _WIN64
__declspec(dllexport) bool sameShapeMatrix(void *m, void *s)
#else
bool sameShapeMatrix(void *m, void *s)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix* s1 = (Matrix*)s;
return (*m1).hasSameShape(*s1);
}
#ifdef _WIN64
__declspec(dllexport) bool dotShapeMatrix(void *m, void *s)
#else
bool dotShapeMatrix(void *m, void *s)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix* s1 = (Matrix*)s;
return (*m1).hasDotShape(*s1);
}
#ifdef _WIN64
__declspec(dllexport) void* plusMatrix(void *m, void *s)
#endif
#ifdef __linux__
void* plusMatrix(void *m, void *s)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix* s1 = (Matrix*)s;
Matrix r = *m1 + *s1;
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* plusBroadcastMatrix(void *m, float a)
#else
void* plusBroadcastMatrix(void *m, float a)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix r = *m1 + a;
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* subMatrix(void *m, void *s)
#else
void* subMatrix(void *m, void *s)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix* s1 = (Matrix*)s;
Matrix r = *m1 - *s1;
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* subBroadcastMatrix(void *m, float a)
#else
void* subBroadcastMatrix(void *m, float a)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix r = *m1 - a;
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* multiMatrix(void *m, void *s)
#else
void* multiMatrix(void *m, void *s)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix* s1 = (Matrix*)s;
Matrix r = (*m1) * (*s1);
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* multiBroadcastMatrix(void *m, float a)
#else
void* multiBroadcastMatrix(void *m, float a)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix r = (*m1) * a;
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* divMatrix(void *m, void *s)
#else
void* divMatrix(void *m, void *s)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix* s1 = (Matrix*)s;
Matrix r = (*m1) / (*s1);
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* divBroadcastMatrix(void *m, float a)
#else
void* divBroadcastMatrix(void *m, float a)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix r = (*m1) / a;
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* dotMatrix(void *m, void *s)
#else
void* dotMatrix(void *m, void *s)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix* s1 = (Matrix*)s;
Matrix r = (*m1).dot(*s1);
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* transposeMatrix(void *m)
#endif
#ifdef __linux__
void* transposeMatrix(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix r = m1->T();
return (void *)new Matrix(r);
}
#ifdef _WIN64
__declspec(dllexport) void* Sigmoid_do(void *m)
#else
void* Sigmoid_do(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix s = Matrix(*m1);
_devSigmoid<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return (void *)new Matrix(s);
}
#ifdef _WIN64
__declspec(dllexport) void* Sigmoid_dot(void *m)
#else
void* Sigmoid_dot(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix s = Matrix(*m1);
_devSigmoidDerivative<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return (void *)new Matrix(s);
}
#ifdef _WIN64
__declspec(dllexport) void* ReLU_do(void *m)
#else
void* ReLU_do(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix s = Matrix(*m1);
_devReLU<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return (void *)new Matrix(s);
}
#ifdef _WIN64
__declspec(dllexport) void* ReLU_dot(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix s = Matrix(*m1);
_devReLUDerivative<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return (void *)new Matrix(s);
}
#ifdef _WIN64
__declspec(dllexport) void* Leaky_ReLU_do(void *m, float a)
#else
void* LeakyReLU_do(void *m, float a)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix s = Matrix(*m1);
_devLeakyReLU<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC(), a);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return (void *)new Matrix(s);
}
#ifdef _WIN64
__declspec(dllexport) void* Leaky_ReLU_dot(void *m, float a)
#else
void* LeakyReLU_dot(void *m, float a)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix s = Matrix(*m1);
_devLeakyReLU_Derivative<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC(), a);
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return (void *)new Matrix(s);
}
// Tanh
#ifdef _WIN64
__declspec(dllexport) void* Tanh_do(void *m)
#else
void* Tanh_do(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix s = Matrix(*m1);
_devTanh<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return (void *)new Matrix(s);
}
#ifdef _WIN64
__declspec(dllexport) void* Tanh_dot(void *m)
#else
void* Tanh_dot(void *m)
#endif
{
Matrix* m1 = (Matrix*)m;
Matrix s = Matrix(*m1);
_devTanhDerivative<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize());
return (void *)new Matrix(s);
}
}
+339
View File
@@ -0,0 +1,339 @@
/* TODO(20260518-qing): 稍后我们专门做一个i2c代码仓库,这样就能够区分不同架构上面的int的大小
* TODO(20260615-qing): 有没有可能只做一个简单的封装不引入异常机制 */
package bot4cj.hd
import pthread4cj.*
import ctype4cj.*
/* -------- 我们将直接调用glibc中的一些函数 -------- */
foreign func ioctl(fd: C_Int, addr: C_ULong, ...): C_Int /* -------- 成功返回非负整数,失败返回-1 -------- */
const I2C_FAILED = -1i32 /* -------- I2C关闭文件失败 -------- */
const I2C_SUCCESS = 0i32 /* -------- I2C关闭文件成功 -------- */
const I2C_SLAVE = 0x0703u64 /* -------- at least on arm64 -------- */
/* -------- OPEN I2C DEVICE 打开I2C设备 -------- */
@C
public func openI2cDev(path: CString): Int32 {
unsafe {
let fd = libc_open(path, O_RDWR)
if (fd > 0) {
I2cLock_create(fd) /* -------- 为I2C设备创建锁 -------- */
return fd
}
else {
throw I2cOpenedFailed("?")
}
}
}
/* ---- 简化版,判断在外部实现 ---- */
@C
public func openI2cDev1(path: CString): C_Int {
unsafe {
let fd = libc_open(path, O_RDWR)
I2cLock_create(fd)
return fd
}
}
/* -------- CLOSE I2C DEVICE 关闭I2C设备 -------- */
@C
public func closeI2cDev(fd: Int32): Unit {
unsafe {
I2cLock_destroy(fd) /* -------- 销毁I2C设备的锁 -------- */
let ret = libc_close(fd)
if (ret == I2C_FAILED) {
throw I2cClosedFailed("?") }
else if (ret == I2C_SUCCESS) { ; }
else {
throw I2cUnknowExp(
"?", "$function returned: {ret.toString()}")
}
}
}
@C
public func closeI2cDev1(fd: C_Int): Unit {
unsafe {
I2cLock_destroy(fd)
libc_close(fd)
}
}
/* -------- SET I2C ADDR 设置I2C地址 -------- */
@C
public func setI2cAddr(fd: Int32, addr: UInt8): Unit {
unsafe {
let ret = ioctl(fd, I2C_SLAVE, addr)
if (ret < 0) {
throw I2cSetAddressFailed("?") }
}
}
@C
public func setI2cAddr1(fd: C_Int, addr: UInt8): C_Int {
unsafe {
return ioctl(fd, I2C_SLAVE, addr)
}
}
/* -------- WRITE I2C REG 写I2C寄存器 -------- */
@C
public func writeI2cReg(fd: Int32, reg: UInt8, val:UInt8): Unit {
var a: VArray<UInt8, $2> = [reg, val]
var p: CPointer<Unit> = unsafe { CPointer<Unit>(inout a) }
if (2 != unsafe { libc_write(fd, p, 2) }) {
throw I2cWriteRegFailed("?")
}
}
/* -------- READ I2C REG 读I2C寄存器 --------
* INFO(20260522-qing): 读取寄存器之前要先写寄存器地址
* INFO(20260616-qing): 这个函数只读取一个字节的数据
* -------- */
@C
public func readI2cReg(fd: Int32, reg: UInt8): Int8 {
var ptr = reg
if (1 != unsafe { libc_write(fd, CPointer<Unit>(inout ptr), 1) }) {
throw I2cReadRegFailed("?")
}
var value: Int8 = 0
var p = unsafe { CPointer<Unit>(inout value) }
let ret = unsafe { libc_read(fd, p, 1) }
if (ret != 1) { throw I2cReadRegFailed("?") }
return value
}
/* -------- I2C锁具 --------
* INFO(20260521-qing): 因为每个I2C接口在通信时具有原子化的需求
* - 需要一个互斥锁集合,在打开第一个I2C设备时被初始化,
* - 释放最后一个I2C设备时,将锁具释放
* - **每次进行I2C通信时,进行上锁操作** */
@C
struct I2C_Lock {
var i2c_fd: Int32 = 0 /* -------- 这个锁绑定的I2C设备文件句柄 -------- */
var lock = CPointer<UInt8>() /* -------- phread_mutex_t 锁对象 -------- */
var is_active = false /* -------- 该锁具是否被激活 --------*/
}
var MAIN_LOCK = CPointer<UInt8>() /* -------- 主锁,操作锁具时使用 -------- */
var LOCKS = CPointer<I2C_Lock>() /* -------- 指向锁具内存的指针 -------- */
const LOCKS_SIZE: Int64 = 20 /* -------- 锁具的容量 --------*/
/* -------- 为一个I2C设备注册锁具 -------- */
@C
func I2cLock_create(fd: Int32): Unit {
unsafe {
/* -------- 初始化主要的锁 -------- */
if (MAIN_LOCK.isNull()) {
MAIN_LOCK = _mutex_create()
LOCKS = LibC.malloc<I2C_Lock>(count: LOCKS_SIZE)
for (i in 0..20) {
let tmp = I2C_Lock()
(LOCKS+i).write(tmp)
}
}
/* -------- 获取主锁 --------*/
_mutex_lock(MAIN_LOCK)
var flag = false
for (idx in 0..LOCKS_SIZE) {
var lock = (LOCKS+idx).read()
if (lock.is_active == false) {
lock.is_active = true
lock.i2c_fd = fd
lock.lock = _mutex_create()
_mutex_init(lock.lock)
(LOCKS+idx).write(lock)
flag = true
break
}
}
_mutex_unlock(MAIN_LOCK)
if (flag ) { return } /* -------- 出口 -------- */
throw OverflowException("LOCKS is over.")
}
}
/* -------- 为一个I2C设备注销锁具 -------- */
@C
func I2cLock_destroy(fd: Int32): Unit {
unsafe {
/* -------- 检查锁具有没有被初始化 --------*/
if (MAIN_LOCK.isNull()) {
throw NoneValueException("no i2c lock")
}
_mutex_lock(MAIN_LOCK)
var flag_any = false
var flag_target = false
for (idx in 0..LOCKS_SIZE) {
var lock = (LOCKS+idx).read()
if (lock.is_active == true && lock.i2c_fd == fd) {
lock.is_active = false
_mutex_destroy(lock.lock)
_mutex_free(lock.lock)
(LOCKS+idx).write(lock)
flag_target = true
/* -------- NOTE: 扫描整个锁具不终止 -------- */
}
else if (lock.is_active == true && lock.i2c_fd != fd) {
flag_any = true
}
}
_mutex_unlock(MAIN_LOCK)
/* -------- 如果锁具里已经没有I2C锁, 就释放整个锁具 -------- */
if (!flag_any) {
LibC.free(LOCKS)
_mutex_free(MAIN_LOCK)
MAIN_LOCK = CPointer<UInt8>()
}
/* -------- 出口 -------- */
if (flag_target) {
return }
throw NoneValueException("i2c lock nor found")
}
}
/* -------- 为一个I2C锁具加锁 -------- */
@C
func I2cLock_lock(fd: Int32): Unit {
unsafe {
if (MAIN_LOCK.isNull()) {
throw NoneValueException("no i2c lock")
}
_mutex_lock(MAIN_LOCK)
var flag = false
for (idx in 0..LOCKS_SIZE) {
var lock = (LOCKS+idx).read()
if (lock.is_active == true && lock.i2c_fd == fd) {
_mutex_lock(lock.lock)
(LOCKS+idx).write(lock)
flag = true
break
}
}
_mutex_unlock(MAIN_LOCK)
if (flag) { return }
throw NoneValueException("i2c lock nor found")
}
}
/* -------- 为一个I2C锁具解锁 -------- */
@C
func I2cLock_unlock(fd: Int32): Unit {
unsafe {
if (MAIN_LOCK.isNull()) {
throw NoneValueException("no i2c lock")
}
_mutex_lock(MAIN_LOCK)
var flag = false
for (idx in 0..LOCKS_SIZE) {
var lock = (LOCKS+idx).read()
if (lock.is_active == true && lock.i2c_fd == fd) {
_mutex_unlock(lock.lock)
(LOCKS+idx).write(lock)
flag = true
break
}
}
_mutex_unlock(MAIN_LOCK)
if (flag) {return}
throw NoneValueException("i2c lock nor found")
}
}
/* -------- 原生I2C通信类--------
* INFO(20260521-qing): 直接调用glibc的接口 */
public class I2C_Orig {
private var fd: Int32 = -1
private var path: String
public init (path: String) {
this.path = path
let s1 = unsafe {
LibC.mallocCString(path) }
fd = unsafe { openI2cDev(s1) }
}
public func _close(): Unit {
unsafe { closeI2cDev(fd) }
}
public func setAddr(addr: UInt8) {
unsafe { setI2cAddr(fd, addr) }
}
public func writeReg(reg: UInt8, val:UInt8): Unit {
unsafe { writeI2cReg(fd, reg, val) }
}
public func readReg(reg: UInt8): Int8 {
var value = unsafe { readI2cReg(fd, reg) }
return value
}
/*--------获取文件句柄--------*/
public func getFd(): Int32 {
return fd
}
}
/* -------- I2C设备异常 -------- */
open private class I2cExcept <: Exception {
public I2cExcept(dev: String, msg: String) {
super('${dev}: ${msg}') }
}
/* -------- 打开I2C设备失败 -------- */
private class I2cOpenedFailed <: I2cExcept {
public I2cOpenedFailed(dev: String) {
super(dev, "failed to open i2c device") }
}
/* -------- 关闭I2C设备失败 -------- */
private class I2cClosedFailed <: I2cExcept {
public I2cClosedFailed(dev: String) {
super(dev, "failed to close i2c device") }
}
/* -------- I2C设置地址失败 -------- */
private class I2cSetAddressFailed <: I2cExcept {
public I2cSetAddressFailed(dev: String) {
super(dev, "failed to open i2c device") }
}
/* -------- I2C写从机I2C失败 -------- */
private class I2cWriteRegFailed <: I2cExcept {
public I2cWriteRegFailed(dev: String) {
super(dev, "failed to write i2c reg") }
}
/* -------- I2C读从机I2C失败 -------- */
private class I2cReadRegFailed <: I2cExcept {
public I2cReadRegFailed(dev: String) {
super(dev, "failed to read i2c reg") }
}
/* -------- 未知I2C异常 -------- */
private class I2cUnknowExp <: I2cExcept {
public I2cUnknowExp(dev: String, msg: String) {
super(dev, msg) }
}
+243
View File
@@ -0,0 +1,243 @@
/* ----------------
* MPU6050陀螺仪与加速度计模块
* -
* TODO(20260516-qing): 陀螺仪需要卡尔曼滤波、通过四元数等方式获取姿态角
* - 所以应该使用一个C/C++线程来持续地采样,并且留一个接口进行获取
*/
package bot4cj.hd
import std.time.*
import std.math.*
import pthread4cj.*
import bot4cj.math.*
const ADDR_0: UInt8 = 0x68 /* 默认地址 */
const ADDR_1: UInt8 = 0x68 /* 地址2 */
const PWR_MGMT_1: UInt8 = 0x6B /* 电源管理寄存器1 */
const PWR_NOSLEEP: UInt8 = 0x00 /* 电源管理 - 非睡眠模式 */
const GYRO_CONF_REG: UInt8 = 0x1B /* 陀螺仪配置寄存器 */
const GYRO_FS_500DPS: UInt8 = 0x08 /* 陀螺仪量程 - 正负500度/秒 */
const GYRO_REG_X_HIGH: UInt8 = 0x43 /* x - 陀螺仪横轴 高字节 */
const GYRO_REG_X_LOW: UInt8 = 0x44
const GYRO_REG_Y_HIGH: UInt8 = 0x45 /* y */
const GYRO_REG_Y_LOW: UInt8 = 0x46
const GYRO_REG_Z_HIGH: UInt8 = 0x47 /* z */
const GYRO_REG_Z_LOW: UInt8 = 0x48
const ACCEL_CONF_REG: UInt8 = 0x1C /* 加速度计配置寄存器 */
const ACCEL_FS_8g: UInt8 = 0x10 /* 加速度计量程 正负8g */
const ACCEL_REG_X_HIGH: UInt8 = 0x3B
const ACCEL_REG_X_LOW: UInt8 = 0x3C
const ACCEL_REG_Y_HIGH: UInt8 = 0x3D
const ACCEL_REG_Y_LOW: UInt8 = 0x3E
const ACCEL_REG_Z_HIGH: UInt8 = 0x3F
const ACCEL_REG_Z_LOW: UInt8 = 0x40
/* -------- MPU6050传感器数据 -------- */
@C
public struct MPU6050_sensorData {
public var accel_x: Int16 = 0 /* 加速度计 */
public var accel_y: Int16 = 0
public var accel_z: Int16 = 0
public var gyro_x: Int16 = 0 /* 陀螺仪 */
public var gyro_y: Int16 = 0
public var gyro_z: Int16 = 0
public func print() { /* 格式化打印 */
println("[accel x, y, z] ${accel_x}, ${accel_y}, ${accel_z}")
println("[gyro x, y, z] ${gyro_x}, ${gyro_y}, ${gyro_z}")
println("--------------------")
}
}
/* -------- 设置MPU6050 -------- */
@C
func mpu6050_setup(i2c_fd: Int32, addr: UInt8) {
unsafe {
I2cLock_lock(i2c_fd)
setI2cAddr(i2c_fd, addr);
sleep(Duration.millisecond * 1)
writeI2cReg(i2c_fd, PWR_MGMT_1, PWR_NOSLEEP); /* 退出睡眠 */
writeI2cReg(i2c_fd, GYRO_CONF_REG, GYRO_FS_500DPS); /* ±500°/s */
writeI2cReg(i2c_fd, ACCEL_CONF_REG, ACCEL_FS_8g); /* ±8g */
I2cLock_unlock(i2c_fd)
sleep(Duration.millisecond * 100); /* 等待稳定 */
}
}
@C
/* -------- 读取 16 位传感器数据(带符号)--------
* FIXME: 是否能够正确处理符号位 */
func read16BitReg(i2c_fd: Int32, high: UInt8, low: UInt8): Int16 {
unsafe {
let hh: Int16 = Int16(readI2cReg(i2c_fd, high))
let ll: Int16 = Int16(readI2cReg(i2c_fd, low))
return (hh << 8) | ll
}
}
/* -------- 读取传感器数据 -------- */
@C
func mpu6050_readSensor(i2c_fd: Int32, addr: UInt8) {
var data = MPU6050_sensorData()
unsafe {
I2cLock_lock(i2c_fd)
setI2cAddr(i2c_fd, addr)
data.accel_x = read16BitReg(i2c_fd, ACCEL_REG_X_HIGH, ACCEL_REG_X_LOW);
data.accel_y = read16BitReg(i2c_fd, ACCEL_REG_Y_HIGH, ACCEL_REG_Y_LOW);
data.accel_z = read16BitReg(i2c_fd, ACCEL_REG_Z_HIGH, ACCEL_REG_Z_LOW);
data.gyro_x = read16BitReg(i2c_fd, GYRO_REG_X_HIGH, GYRO_REG_X_LOW);
data.gyro_y = read16BitReg(i2c_fd, GYRO_REG_Y_HIGH, GYRO_REG_Y_LOW);
data.gyro_z = read16BitReg(i2c_fd, GYRO_REG_Z_HIGH, GYRO_REG_Z_LOW);
I2cLock_unlock(i2c_fd)
}
return data
}
/* -------- 计算PITCH角 -------- */
//@C
//func mpu6050_calcPitch()
/* -------- mpu6050主线程 --------
* TODO(20260525-qing): 使用互补滤波就够用了 */
const RAD_TO_DEG = 57.2956f32 /* ---- 弧度转换为角度的系数(180/π ≈ 57.2956) ---- */
//foreign func pthread_testcancel(): Unit
@C
func mpu6050_main(args: CPointer<Unit>): CPointer<Unit> {
/* -------- 获取传入线程的参数 -------- */
let param = unsafe{ CPointer<mpu6050_th_param>(args).read() }
/* -------- 为加速度计准备滤波器组 -------- */
var kalman_accel_x = extKalman(20.0f32, 200.0f32)
var kalman_accel_y = extKalman(20.0f32, 200.0f32)
var kalman_accel_z = extKalman(20.0f32, 200.0f32)
/* -------- 为陀螺仪准备滤波器组 -------- */
var kalman_gyro_x = extKalman(20.0f32, 200.0f32)
var kalman_gyro_y = extKalman(20.0f32, 200.0f32)
var kalman_gyro_z = extKalman(20.0f32, 200.0f32)
while(true) {
println("I am running. i2c_fd: ${param.i2c_fd}, addr: ${param.addr}")
//unsafe { _mutex_lock(mutex) }
/* -------- 获取传感器数据 -------- */
var data = unsafe { mpu6050_readSensor(param.i2c_fd, param.addr) }
/* -------- 为加速度计执行滤波操作 -------- */
let filtered_accel_x = kalman_accel_x.filter(Float32(data.accel_x))
let filtered_accel_y = kalman_accel_y.filter(Float32(data.accel_y))
let filtered_accel_z = kalman_accel_z.filter(Float32(data.accel_z))
/* -------- 为陀螺仪执行滤波操作 -------- */
let filtered_gyro_x = kalman_gyro_x.filter(Float32(data.gyro_x))
let filtered_gyro_y = kalman_gyro_y.filter(Float32(data.gyro_y))
let filtered_gyro_z = kalman_gyro_z.filter(Float32(data.gyro_z))
/* -------- 计算Pitch(俯仰角)和Roll(翻滚角) -------- */
let roll = atan2(filtered_accel_y, filtered_accel_z) * RAD_TO_DEG
let pitch = atan(-filtered_accel_x / sqrt(filtered_accel_y * filtered_accel_y + filtered_accel_z * filtered_accel_z)) * RAD_TO_DEG
let roll1 = atan(filtered_accel_y / sqrt(filtered_accel_x * filtered_accel_x + filtered_accel_z * filtered_accel_z)) * RAD_TO_DEG
let pitch1 = atan2(-filtered_accel_x, filtered_accel_z) * RAD_TO_DEG
print("roll: ${roll}\t")
println("pitch: ${pitch}")
print("roll1: ${roll1}\t")
println("pitch1: ${pitch1}")
//unsafe { _mutex_unlock(mutex) }
sleep(Duration.millisecond * 10)
}
/* -------- 返回NULL指针以占位 -------- */
return CPointer<Unit>()
}
/* -------- mpu6050线程参数 --------
* INFO(20260522-qing): mutex字段是一个锁的指针,需要在MPU6050的构造函数中获取资源
* - 用于辅助别的线程获取值。 */
@C
struct mpu6050_th_param {
public var i2c_fd: Int32 = 0 /* i2c文件句柄 */
public var addr: UInt8 = 0 /* i2c设备地址 */
public var mutex = CPointer<UInt8>() /* 用于辅助别的线程获取值的锁 */
}
/* -------- MPU6050陀螺仪、加速度计 --------
* - 输入并持有一个I2C文件句柄 */
public class MPU6050 {
/* -------- 主初始化器 --------
* FIXME(20260517-qing): 由于gc的原因,我们可能会手动构造RAII,
* - 将一部分初始化移动到别的函数 */
public init(i2c_fd: Int32, addr: UInt8) {
this.i2c_fd = i2c_fd
this.addr = addr
unsafe {
param = LibC.malloc<mpu6050_th_param>()
var tmp = mpu6050_th_param()
tmp.i2c_fd = i2c_fd
tmp.addr = addr
tmp.mutex = LibC.malloc<UInt8>()
param.write(tmp)
}
}
/* -------- 终结器 -------- */
~init() {
/* -------- 清除构造时申请的线程参数和锁 -------- */
unsafe {
var tmp = param.read()
LibC.free(tmp.mutex)
LibC.free(param)
}
}
/* -------- 初始化该模块 -------- */
public func setup() {
unsafe { mpu6050_setup(i2c_fd, addr) }
}
/*----------------*/
private var i2c_fd: Int32 /* -------- I2C设备文件句柄 -------- */
private var addr: UInt8 /* -------- I2C设备地址 -------- */
/* -------- 获取原始MPU6050传感器数据 -------- */
public func readSensor(): MPU6050_sensorData {
return unsafe { mpu6050_readSensor(i2c_fd, addr) }
}
/* -------- mpu6050启动守护 --------
* TODO(20260520-qing): 放进类中
* --------------------------------- */
var param: CPointer<mpu6050_th_param> /* -------- 线程参数必须能够持久保存 -------- */
var pth: Option<Pth> = None /* -------- 线程对象 -------- */
public func startDaemon() { /* -------- 启动线程 -------- */
pth = Pth(mpu6050_main, CPointer<Unit>(param))
}
/* -------- 停止线程 -------- */
public func stopDaemon() {
if (let Some(P) <- pth) {
P.close() }
}
/* -------- 等待线程终止 -------- */
public func waitDaemon() {
if (let Some(P) <- pth) {
var ptr: CPointer<Unit> = CPointer<Unit>()
P.join(CPointer<CPointer<Unit>>(CPointer<CPointer<Unit>>(ptr))) }
}
}
+69
View File
@@ -0,0 +1,69 @@
package bot4cj.hd
import pthread4cj.*
/* pca9685舵机控制版 - bot4cj
* TODO(20260515-qing): 由于对统一接口的需求,设计基于回调的虚拟化硬件
* TODO(20260516-qing): 获取pca9685的中文或者英文参考资料
* NOTE(20260618-qing): 该代码已经被Lua脚本所取代,留着备用说不定以后会删除。 */
public class PCA9685 {
private static const ADDR_0: UInt8 = 0x40 /* PCA9685的i2c地址 */
private let i2c_fd: Int32
private let addr: UInt8
public init( i2c_fd: Int32,
addr!: UInt8 = ADDR_0) { /* PCA9685硬件地址 */
this.i2c_fd = i2c_fd
this.addr = addr
/* -------- 初始化PCA9685 --------
* FIXME(20260517-qing): 在旧版机器人代码移除以后,移除这一段 */
}
/* -------- 设置PWM频率为50Hz(舵机标准周期20ms0) --------
* TODO(20260516-qing): 不将数值写死 */
public func setup() {
unsafe {
I2cLock_lock(i2c_fd)
setI2cAddr(i2c_fd, addr);
writeI2cReg(i2c_fd, 0x00, 0x20); /* 开启AI(地址自增)和关闭SLEEP */
sleep(5000 * Duration.nanosecond); /* 等待振荡器启动 */
/* FIXME: 这一条是不是重复了 */
var freq: Float32 = 50.0;
var prescale_val: Float32 = 25000000.0 / (4096.0 * freq) - 1.0; /* 25MHz主频 */
var prescale: UInt8 = UInt8(prescale_val + 0.5);
writeI2cReg(i2c_fd, 0x00, 0x10); /* 进入SLEEP模式(允许改频率) */
writeI2cReg(i2c_fd, 0xFE, prescale); /* 写入预分频值 */
writeI2cReg(i2c_fd, 0x00, 0x20); /* 退出SLEEP */
I2cLock_unlock(i2c_fd)
}
sleep(5000 * Duration.nanosecond); /* 等待振荡器启动 */
}
/* -------- 设置指定通道的PWM信号(0-4095)-------- */
public func setPWM(channel: UInt8, on: UInt16, off: UInt16) {
var reg: UInt8 = 0x06 + 4 * channel; /* LED0 寄存器起始地址 */
unsafe {
writeI2cReg(i2c_fd, reg, UInt8(on & 0xFF));
writeI2cReg(i2c_fd, reg + 1, UInt8(on >> 8));
writeI2cReg(i2c_fd, reg + 2, UInt8(off & 0xFF));
writeI2cReg(i2c_fd, reg + 3, UInt8(off >> 8));
}
return
}
/* -------- 角度转PWM值(0°~180° → 0.5ms~2.5ms脉宽) -------- */
public func setAngle(channel: UInt8, angle: Float32) {
const min: Float32 = 0.5; /* 0°对应0.5ms */
const max: Float32 = 2.5; /* 180°对应2.5ms */
let width: Float32 = min + (max - min) * (angle / 180.0);
let off_val: UInt16 = UInt16(width * 4096.0 / 20.0); /* 20ms 周期 */
unsafe {
I2cLock_lock(i2c_fd)
setI2cAddr(i2c_fd, this.addr);
this.setPWM(channel, 0, off_val); /* ON=0, OFF=计算值 */
I2cLock_unlock(i2c_fd)
}
}
}
+30
View File
@@ -0,0 +1,30 @@
package bot4cj.hd
import std.math.*
/* -------- 舵机类 --------
* 含最大值、最小值、漂移值三个调整项
* 以及一个设置角度回调函数
*
* FIXME(20260618-qing): 在构造函数中设置角度最大值 */
public class Servo {
private var max: Float32
private var min: Float32
private var offset: Float32
private var callback_setAngle: (Float32) -> Unit
private static let MAX_ANGLE: Float32 = 180.0f32
public init(max!: Float32, min!: Float32, offset!: Float32,
callback_setAngle!: (Float32) -> Unit) {
this.max =max
this.min = min
this.offset = offset
this.callback_setAngle = callback_setAngle
}
public func setAngle(angle: Float32) {
var a = angle
a += offset /* ---- 加上漂移值 ---- */
if (a > abs(max)) { a = abs(max)}
if (a < abs(min)) { a = abs(min)}
if (max < 0.0f32) { a = MAX_ANGLE - a }
callback_setAngle(a)
}
}
+16
View File
@@ -0,0 +1,16 @@
macro package bot4cj.hd.extern
import std.fs.*
import std.io.*
import std.ast.*
public macro ExternHardware(attrTokens: Tokens, inputTokens: Tokens): Tokens {
let expr = LitConstExpr(attrTokens)
let file = File(expr.literal.value, OpenMode.Read)
let bytes = readToEnd(file)
file.close()
let sb = StringBuilder()
sb.appendFromUtf8(bytes)
let code = sb.toString()
return cangjieLex(code)
}
+138
View File
@@ -0,0 +1,138 @@
package bot4cj.hd
import ctype4cj.*
const FBIOGET_VSCREENINFO = 0x4602u64
const FBIOGET_FSCREENINFO = 0x4600u64
const PROT_WRITE: C_Int = 0x2
const MAP_SHARED: C_Int = 0x01
/* --- 定义在I2C.cj里了 ---*/
//foreign func ioctl(fd: C_Int, request: C_ULong, ...): C_Int
/* ---- 内存映射 ---- */
public type off_t = C_LongLong
foreign func mmap(addr: CPointer<Unit>, length: UIntNative, prot: C_Int, flags: C_Int, fd: C_Int, offset: off_t): CPointer<Unit>
/* -------- 打开framebuffer帧缓冲设备 -------- */
@C
public func fb_open(fb_dev: CString): C_Int {
return unsafe { libc_open(fb_dev, O_RDWR) }
}
/* -------- 帧缓冲区固定参数 -------- */
@C
public struct fb_fix_screeninfo {
var id: VArray<C_Char, $16> /* 设备驱动名称字符串,比如"TT Builtin" */
var smem_start: C_ULong = 0 /* 帧缓冲内存(显存)的物理起始地址 */
/* (physical address) */
var smem_len: UInt32 = 0 /* 帧缓冲内存的总长度,单位字节 */
var type_: UInt32 = 0 /* 显存类型(fb存储布局)see FB_TYPE_* */
var type_aux: UInt32 = 0 /* 显存附加类型 */
var visual: UInt32 = 0 /* 色彩显示模式 see FB_VISUAL_* */
var xpanstep: UInt16 = 0 /* zero if no hardware panning */
var ypanstep: UInt16 = 0 /* zero if no hardware panning */
var ywrapstep: UInt16 = 0 /* zero if no hardware ywrap */
var line_length: UInt32 = 0 /* 显示屏上一行像素数据所占用的字节数 */
var mmio_start: C_ULong = 0 /* 内存映射I/O的物理起始地址 */
/* (physical address) */
var mmio_len: UInt32 = 0 /* 内存映射的区域长度,单位字节 */
var accel: UInt32 = 0 /* 硬件加速类型 */
/* specific chip/card we have */
var capabilities: UInt16 = 0 /* see FB_CAP_* */
var reserved: VArray<UInt16, $2> /* 保留字段,用于未来扩展,确保兼容 */
public init() {
id = VArray<C_Char, $16> (repeat: 0)
reserved = VArray<UInt16, $2> (repeat: 0)
}
}
/* 颜色字段偏移量的解释:所有偏移量均从右侧开始计算,
* 位于一个“像素”值内部,该像素值的宽度正好等于 'bits_per_pixel'
* (也就是说:你可以将偏移量作为 << 的右操作数使用)。
* 之后的像素就是一个比特流,会原封不动地写入显存。
*
* 对于伪彩色:所有颜色分量的偏移量和长度应该相同。
* 偏移量指定了调色板索引的最低有效位在像素值中的位置。
* 长度表示可用的调色板条目数量(即条目数 = 1 << 长度)。
*/
@C
public struct fb_bitfield {
var offset: UInt32 = 0 /* 位域的起始位置(从右侧算起)*/
var length: UInt32 = 0 /* 位域的长度(比特数) */
var msb_right: UInt32 = 0 /* != 0 : 最高有效位在右侧 */
/* (即低位在右) */
}
/* -------- 帧缓冲区可变参数 -------- */
@C
public struct fb_var_screeninfo {
var xres: UInt32 = 0 /* visible resolution */
var yres: UInt32 = 0
var xres_virtual: UInt32 = 0 /* virtual resolution */
var yres_virtual: UInt32 = 0
var xoffset: UInt32 = 0 /* offset from virtual to visible */
var yoffset: UInt32 = 0 /* resolution */
var bits_per_pixel: UInt32 = 0 /* guess what */
var grayscale: UInt32 = 0 /* 0 = color, 1 = grayscale, */
/* >1 = FOURCC */
var red: fb_bitfield /* bitfield in fb mem if true color, */
var green: fb_bitfield /* else only length is significant */
var blue: fb_bitfield
var transp: fb_bitfield /* transparency */
var nonstd: UInt32 = 0 /* != 0 Non standard pixel format */
var activate: UInt32 = 0 /* see FB_ACTIVATE_* */
var height: UInt32 = 0 /* height of picture in mm */
var width: UInt32 = 0 /* width of picture in mm */
var accel_flags: UInt32 = 0 /* (OBSOLETE) see fb_info.flags */
/* Timing: All values in pixclocks, except pixclock (of course) */
var pixclock: UInt32 = 0 /* pixel clock in ps (pico seconds) */
var left_margin: UInt32 = 0 /* time from sync to picture */
var right_margin: UInt32 = 0 /* time from picture to sync */
var upper_margin: UInt32 = 0 /* time from sync to picture */
var lower_margin: UInt32 = 0
var hsync_len: UInt32 = 0 /* length of horizontal sync */
var vsync_len: UInt32 = 0 /* length of vertical sync */
var sync: UInt32 =0 /* see FB_SYNC_* */
var vmode: UInt32 = 0 /* see FB_VMODE_* */
var rotate: UInt32 = 0 /* angle we rotate counter clockwise */
var colorspace: UInt32 = 0 /* colorspace for FOURCC-based modes */
var reserved: VArray<UInt32, $4>/* Reserved for future compatibility */
public init() {
red = fb_bitfield()
green = fb_bitfield()
blue = fb_bitfield()
transp = fb_bitfield()
reserved = VArray<UInt32, $4> (repeat: 0)
}
}
/* ---- 获取帧缓冲区可变参数 ---- */
@C
public func fb_get_var_info(fd: C_Int) {
var info = fb_var_screeninfo()
unsafe { ioctl(fd, FBIOGET_VSCREENINFO, inout info) }
return info
}
/* ---- 获取帧缓冲区固定参数 ---- */
@C
public func fb_get_fix_info(fd: C_Int) {
var info = fb_fix_screeninfo()
unsafe { ioctl(fd, FBIOGET_FSCREENINFO, inout info) }
return info
}
/* ---- 内存映射 ---- */
@C
public func fb_mmap(addr: CPointer<Unit>, length: UIntNative, fd: C_Int, offset: off_t): CPointer<Unit>
{
let buf = unsafe { mmap(addr, length, PROT_WRITE, MAP_SHARED, fd, offset) }
return buf
}
+22
View File
@@ -0,0 +1,22 @@
package bot4cj.hd
import std.time.*
//foreign func play_frequency(frequency: Int32, duration_ms: Int32, sample_rate: Int32): Bool
//foreign func play_frequency(frequency: Float32, duration_ms: Int32, volume: Float64, channels: Int32, sample_rate: Int32): Bool
/* -------- 扬声器类 --------
* TODO: 一次打开设备,一段时间后再关闭 */
public class Spk {
private var channels: Int32
private var sample_rate: Int32
public init(channels!: Int32, sample_rate!: Int32) {
this.channels = channels
this.sample_rate = sample_rate
}
/* -------- 控制扬声器发声 -------- */
public func play(frequency: Float32, volume: Float64, durations_ms: Int32): Bool {
//unsafe { play_frequency(frequency, durations_ms, volume, channels, sample_rate) }
sleep(Duration.millisecond * Int64(durations_ms))
return true
}
}
+177
View File
@@ -0,0 +1,177 @@
package bot4cj
import std.io.*
import std.fs.*
import std.env.*
import std.time.*
import std.argopt.*
import std.convert.*
import std.collection.*
import stdx.encoding.json.*
import bot4cj.hd.*
import bot4cj.math.*
import ctype4cj.*
/* ---- 神经网络输出路径 ---- */
var NNS_OUT: Option<String> = None
main(args: Array<String>): Int64 {
/* ---- 解析命令行参数 ---- */
let specs = [
ArgumentSpec.Long("help", ArgumentMode.NoValue), /* ---- 拉起帮助菜单 ---- */
ArgumentSpec.Short(r'i', ArgumentMode.RequiredValue), /* ---- 神经网络模型 - 输入 ---- */
ArgumentSpec.Short(r'o', ArgumentMode.RequiredValue), /* ---- 神经网络模型 - 输出 ---- */
ArgumentSpec.Long("test", ArgumentMode.RequiredValue) /* ---- 运行一个测试分支 - 需要输入测试类型 ---- */
]
/* ---- 执行解析 ---- */
let parsed = parseArguments(args, specs)
let options = parsed.options
//---------------------------
/* 读取设置项 */
/*---------------------------*/
/*---------------------------*/
/* 初始化神经网络 (init nn)
* TODO: 可以去掉内部初始化参数,使用矩阵式模板进行初始化 */
//var nns: ArrayList<Nn> =
//if (options.keys().contains("i")) {
// let filename = options['i']
// load_nns(filename) }
//else {
// let nn_in = ArrayList<Nn>()
// nn_in.add(Nn(41, 80, ActivationFuncs.Leaky_ReLU, 0.2f32))
// nn_in.add(Nn(80, 80, ActivationFuncs.Leaky_ReLU, 0.2f32))
// nn_in.add(Nn(80, 40, ActivationFuncs.Leaky_ReLU, 0.2f32))
// nn_in.add(Nn(40, 17, ActivationFuncs.Sigmoid, 0.2f32)) // 16个舵机加1个时延
// nn_in }
/* ---- 使用-o选项进行输出路径的选择 ---- */
if (options.contains("o")) {
NNS_OUT = Option<String>.Some(options['o'])
}
/*---------------------------*/
/*---------------------------*/
/* 分支选择 */
/* -------- 拉起帮助手册
* -------- */
if (options.contains("help")) {
print_help()
return 0
}
/* -------- 启动一个测试分支
* -------- */
else if (options.contains("test")) {
run_test(options["test"])
return 0
}
/* -------- 缺省行为:从标准输入获取动作脚本
* --------
* -------- TODO(20260519-qing): 进行脚本引入(相对路径也可,从**环境变量**的路径中搜寻也可) */
else {
let consoleReader = getStdIn()
let robot = RobotStruct(consoleReader)
try {
robot.active()
robot.run()
}
finally {
robot.deactive()
}
/* ---- NOTE(20260523-qing): 储存微调网络模型
* ---- */
// let consoleReader = getStdIn()
// run_action_drl(consoleReader, nns)
// if (options.contains("o")) {
// save_nns(nns, options['o']) }
}
return 0
}
/* -------- 打印帮助手册
* -------- */
func print_help()
{
println("这是一个机器人动作脚本解析器。")
println("使用管道输入动作脚本,示例:")
println("cat std.mt | ./bot")
println()
println("--i2c=/path/to/device\t选择i2c设备")
println()
println("-i 选择输入的模型的路径")
println("-o 设置输出的模型的路径")
}
/* -------- 集成多种常规测试、临时测试
* -------- */
func run_test(key: String) {
/* ----------- 神经网络与门测试
* -----------
* ----------- INFO: 我们使用随机生成的动态数据集,这样会导致一些后果 */
if (key == "nn") {
let nn_t = ArrayList<Nn1>()
nn_t.add(Nn1(2, 4, 0.05f32, Af.Leaky_ReLU(FP32(0.01))))
nn_t.add(Nn1(4, 1, 0.05f32, Af.Sigmoid))
let batch = 1
let num =2000
let prt_num = 100
for (i in 0..(num*batch)) {
let m = Matrix1(1, 2)
let t = Matrix1(1, 1)
prepare_andDoor(m, t)
let l = train1(nn_t, m, t, 1.0f32) // 这个1.0是学习率的折扣,而非学习率本身
if (i % prt_num == prt_num-1) {
print("${i}\t:")
print("${l}")
println()
}
}
}
else if (key == "cnn") {
testCNN()
}
else if (key == "cnn1") {
testGeometryCNN()
}
/* -------- 测试解析矩阵,并且输出
* -------- */
else if (key == "matrix_o") {
let file = File("ss/test.mt", OpenMode.Read)
let robot = RobotStruct(file)
file.close()
let outf = File("ss/output.mt", OpenMode.Write)
robot.save(outf)
outf.close()
}
/* -------- 解析测试输出的矩阵
* -------- */
else if (key == "matrix_2") {
let file = File("ss/output.mt", OpenMode.Read)
let robot = RobotStruct(file)
file.close()
robot.active()
robot.run()
robot.deactive()
let outf = File("ss/output_1.mt", OpenMode.Write)
robot.save(outf)
outf.close()
}
}
+204
View File
@@ -0,0 +1,204 @@
// Adam.cj - Adam 优化器实现(添加全连接层支持)
package bot4cj.math
import std.math.*
import std.collection.*
public class Adam {
let learningRate: FP32
let beta1: FP32
let beta2: FP32
let epsilon: FP32
var m: HashMap<String, Matrix1>
var v: HashMap<String, Matrix1>
var t: Int64
// 完整参数构造函数
public init(
lr: FP32,
beta1: FP32,
beta2: FP32,
epsilon: FP32
) {
learningRate = lr
this.beta1 = beta1
this.beta2 = beta2
this.epsilon = epsilon
m = HashMap<String, Matrix1>()
v = HashMap<String, Matrix1>()
t = 0
}
// 默认参数构造函数
public init() {
learningRate = FP32(0.001)
beta1 = FP32(0.9)
beta2 = FP32(0.999)
epsilon = FP32(0.00000001)
m = HashMap<String, Matrix1>()
v = HashMap<String, Matrix1>()
t = 0
}
// 仅自定义学习率的构造函数
public init(lr: FP32) {
learningRate = lr
beta1 = FP32(0.9)
beta2 = FP32(0.999)
epsilon = FP32(0.00000001)
m = HashMap<String, Matrix1>()
v = HashMap<String, Matrix1>()
t = 0
}
/**
* 计算幂运算的辅助函数
*/
private func power(base: FP32, exp: Int64): FP32 {
var result = FP32(1.0)
for (i in 0..exp) {
result = result * base
}
return result
}
/**
* 计算平方根的辅助函数 - 使用牛顿迭代法
*/
private func sqrt(val: FP32): FP32 {
let x = val.getValue()
if (x <= 0.0) {
return FP32(0.0)
}
var guess = x
for (i in 0..10) {
guess = (guess + x / guess) / 2.0
}
return FP32(guess)
}
/**
* 更新矩阵参数(通用方法)
* @param param 要更新的参数矩阵
* @param grad 梯度矩阵
* @param key 参数在HashMap中的键名
*/
private func updateMatrix(
param: Matrix1,
grad: Matrix1,
key: String
): Unit {
// 初始化动量
let hasState = m.contains(key)
if (!hasState) {
let rows = param.get_row()
let cols = param.get_col()
m.add(key, Matrix1(rows, cols))
v.add(key, Matrix1(rows, cols))
}
let mMat = m.get(key).getOrThrow()
let vMat = v.get(key).getOrThrow()
let rows = param.get_row()
let cols = param.get_col()
for (r in 0..rows) {
for (c in 0..cols) {
let g = grad.get(r, c).getOrThrow()
// 更新一阶矩
let oldM = mMat.get(r, c).getOrThrow()
let newM = beta1 * oldM + (FP32(1.0) - beta1) * g
mMat.set(r, c, newM)
// 更新二阶矩
let oldV = vMat.get(r, c).getOrThrow()
let newV = beta2 * oldV + (FP32(1.0) - beta2) * g * g
vMat.set(r, c, newV)
// 偏差修正
let one = FP32(1.0)
let beta1Pow = power(beta1, t)
let beta2Pow = power(beta2, t)
let mHat = newM / (one - beta1Pow)
let vHat = newV / (one - beta2Pow)
// 更新参数
let oldParam = param.get(r, c).getOrThrow()
let update = learningRate * mHat / (sqrt(vHat) + epsilon)
param.set(r, c, oldParam - update)
}
}
}
/**
* 更新卷积层参数
*/
public func updateConvLayer(layer: ConvLayer): Unit {
t += 1
// 更新卷积核
var kGrad = match (layer.kernelsGrad) {
case Some(val) => val
case _ => throw Exception("No kernel gradients")
}
for (oc in 0..layer.nOutputChannels) {
let key = "conv_kernel_" + oc.toString()
updateMatrix(layer.kernels[oc], kGrad[oc], key)
}
// 更新偏置
var bGrad = match (layer.biasGrad) {
case Some(val) => val
case _ => throw Exception("No bias gradient")
}
let biasKey = "conv_bias"
updateMatrix(layer.biases, bGrad, biasKey)
}
/**
* 更新全连接层参数(Nn1)
*/
public func updateFCLayer(layer: Nn1): Unit {
t += 1
// 更新权重
var wGrad = match (layer.weights_grad) {
case Some(val) => val
case _ => throw Exception("No weights gradient")
}
let weightKey = "fc_weights"
updateMatrix(layer.weights, wGrad, weightKey)
// 更新偏置
var bGrad = match (layer.bias_grad) {
case Some(val) => val
case _ => throw Exception("No bias gradient")
}
let biasKey = "fc_bias"
updateMatrix(layer.bias, bGrad, biasKey)
}
/**
* 重置优化器状态
*/
public func reset(): Unit {
m.clear()
v.clear()
t = 0
}
/**
* 获取当前迭代次数
*/
public func getStep(): Int64 {
t
}
}
+273
View File
@@ -0,0 +1,273 @@
package bot4cj.math
import std.math.*
/* -------- Activation Functions 激活函数族 --------
* TODO(20260516-qing): 增加序列化、反序列化工厂函数 */
public enum Af {
None | Sigmoid | ReLU | Leaky_ReLU(FP32) | Tanh | Softmax
/* -------- 执行激活函数 -------- */
public func forward(x: Matrix1) {
match(this) {
case None => none(x)
case Sigmoid => sigmoid(x)
case ReLU => relu(x)
case Leaky_ReLU(alpha) => leaky_relu(x, alpha)
case Tanh => tanh(x)
case Softmax => softmax(x)
}
}
/* -------- 激活函数的导数 -------- */
public func backward(y: Matrix1) {
match(this) {
case None => none_dot(y)
case Sigmoid => sigmoid_dot(y)
case ReLU => relu_dot(y)
case Leaky_ReLU(alpha) => leaky_relu_dot(y, alpha)
case Tanh => tanh_dot(y)
case Softmax => softmax_jacobian(y)
}
}
/* -------- 序列化与反序列化 -------- */
public func get_type(): String {
return match(this) {
case None => "None"
case Sigmoid => "Sigmoid"
case ReLU => "ReLU"
case Leaky_ReLU(alpha) => "Leaky_ReLU"
case Tanh => "Tanh"
case Softmax => "Softmax"
}
}
/* */
public func get_value(): FP32 {
return match(this) {
case None => FP32(0.0f32)
case Sigmoid => FP32(0.0f32)
case ReLU => FP32(0.0f32)
case Leaky_ReLU(alpha) => alpha
case Tanh => FP32(0.0f32)
case Softmax => FP32(0.0f32)
}
}
/* -------- 空的激活函数 -------- */
private static func none(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
N.set(r, c, FP32(x1)) }
}
return N
}
/* -------- 空激活函数的导数 -------- */
private static func none_dot(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
N.set(r, c, FP32(1.0f32)) }
}
return N
}
/* -------- S型生长曲线 -------- */
private static func sigmoid(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 = 1.0f32 / (1.0f32 + exp(-x1))
N.set(r, c, FP32(x2)) }
}
return N
}
/* -------- S型生长曲线的导数 -------- */
private static func sigmoid_dot(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 = 1.0f32 / (1.0f32 + exp(-x1))
let x3 = x2 * (1.0f32 - x2)
N.set(r, c, FP32(x3)) }
}
return N
}
/* -------- 线性整流单元 -------- */
private static func relu(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 =if (x1 > 0.0f32) {x1} else{0.0f32}
N.set(r, c, FP32(x2))
}
}
return N
}
/* -------- 线性整流单元的导数 -------- */
private static func relu_dot(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 =if (x1 > 0.0f32) {1.0f32} else{0.0f32}
N.set(r, c, FP32(x2))
}
}
return N
}
/* -------- 带泄露的线性整流单元 -------- */
private static func leaky_relu(M: Matrix1, alpha: FP32) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 =if (x1 > 0.0f32) {x1} else{x1 * alpha.getValue()}
N.set(r, c, FP32(x2))
}
}
return N
}
/* -------- 带泄露的整流单元导数 -------- */
private static func leaky_relu_dot(M: Matrix1, alpha: FP32) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 =if (x1 > 0.0f32) { 1.0f32 } else{alpha.getValue()}
N.set(r, c, FP32(x2)) }
}
return N
}
/* -------- 双曲正切函数 --------*/
private static func tanh(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 = tanh(x1)
N.set(r, c, FP32(x2)) }
}
return N
}
/* -------- 双曲正切函数的导数 -------- */
private static func tanh_dot(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
for (r in 0..row) {
for (c in 0..col) {
let y = M.get(r, c).getOrThrow()
let y_val= y.getValue() /*---- NOTE(20260525-qing): 修复命名(x => y) ----*/
let rea = 1.0f32 - y_val * y_val
N.set(r, c, FP32(rea)) }
}
return N
}
/* -------- 归一化指数函数 -------- */
private static func softmax(M: Matrix1) {
let row = M.get_row()
let col = M.get_col()
let N = Matrix1(row, col)
var max_val = 0.0f32
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
if (x1 > max_val) {
max_val = x1 }
}
}
var sum = 0.0f32
for (r in 0..row) {
for (c in 0..col) {
let x = M.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 = exp(x1 - max_val)
N.set(r, c, FP32(x2))
sum += x2
}
}
for (r in 0..row) {
for (c in 0..col) {
let x = N.get(r, c).getOrThrow()
let x1= x.getValue()
let x2 = x1 / sum
N.set(r, c, FP32(x2))
}
}
return N
}
/* -------- 计算Softmax的雅可比矩阵 --------
* 输入:已计算好的softmax输出(概率分布)
* 输出:雅可比矩阵 J[i][j] = ∂y_i/∂x_j */
private func softmax_jacobian(softmax_output: Matrix1) {
let row = softmax_output.get_row()
let col = softmax_output.get_col()
let n = row * col
var jacobian = Matrix1(n, n)
for (i in 0..n) {
for (j in 0..n) {
let row_i = i / col /* 需要将一维索引转换为二维索引来访问Matrix1 */
let col_i = i % col
let row_j = j / col
let col_j = j % col
let val_i = softmax_output.get(row_i, col_i).getOrThrow().getValue()
let val_j = softmax_output.get(row_j, col_j).getOrThrow().getValue()
if (i == j) { /* 对角线元素: y_i * (1 - y_i) */
jacobian.set(i, j, FP32(val_i * (1.0f32 - val_i))) }
else { /* 非对角线元素: -y_i * y_j */
jacobian.set(i, j, FP32(-val_i * val_j)) }
}
}
return jacobian
}
}
+37
View File
@@ -0,0 +1,37 @@
package bot4cj.math
import std.random.*
import std.time.*
import std.math.*
/* -------- 神经网络使用的权重矩阵初始化类 --------
* TODO(20260517-qing): 将该函数改为枚举类,统一一个接口,根据模式匹配执行初始化
* - 这样在矩阵式中可以定义初始化方式 */
public class Ini {
/* -------- xavier正态初始化 -------- */
public static func xavierNormalInit(target: Matrix1, fan_in: Int64, fan_out: Int64) {
let seed = DateTime.now().nanosecond
let random = Random(UInt64(seed))
let sigma = sqrt(2.0f32 / Float32(fan_in + fan_out)) /* 计算标准差 */
for (r in 0..fan_in) {
for (c in 0..fan_out) {
let v = random.nextGaussianFloat32(mean: 0.0f32, sigma:sigma)
target.set(r, c, FP32(v))
}
}
}
/* -------- Glorot均匀初始化 -------- */
public static func xavierUniformInit(target: Matrix1, fan_in: Int64, fan_out: Int64) {
let limit = sqrt(6.0f32 / Float32(fan_in + fan_out))
let seed = DateTime.now().nanosecond
let random = Random(UInt64(seed))
for (r in 0..fan_in) {
for (c in 0..fan_out) {
let v = (random.nextFloat32() * 2.0f32 - 1.0f32) * limit /* 均匀分布 [-limit, limit] */
target.set(r, c, FP32(v)) }
}
}
}
Loaded 100 of 559 files, more files were not shown because too many files have changed in this diff. Show more