From c736d73541ca5d2e43d3968a7930a1e4fb30ee52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sun, 20 Sep 2026 11:04:53 +0800 Subject: [PATCH] =?UTF-8?q?=E8=84=91=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_brain/__init__.py | 2 + yuthon_brain/__manifest__.py | 18 + yuthon_brain/controllers/__init__.py | 1 + yuthon_brain/controllers/main.py | 66 ++ yuthon_brain/data/brain_data.xml | 66 ++ yuthon_brain/models/__init__.py | 3 + yuthon_brain/models/brain_edge.py | 24 + yuthon_brain/models/brain_node.py | 39 + yuthon_brain/models/brain_source.py | 297 +++++++ yuthon_brain/security/ir.model.access.csv | 7 + yuthon_brain/static/src/brain_view.html | 898 ++++++++++++++++++++++ yuthon_brain/static/src/img/the_brain.png | Bin 0 -> 7127 bytes yuthon_brain/views/brain_views.xml | 158 ++++ yuthon_brain/views/menu.xml | 43 ++ 14 files changed, 1622 insertions(+) create mode 100644 yuthon_brain/__init__.py create mode 100644 yuthon_brain/__manifest__.py create mode 100644 yuthon_brain/controllers/__init__.py create mode 100644 yuthon_brain/controllers/main.py create mode 100644 yuthon_brain/data/brain_data.xml create mode 100644 yuthon_brain/models/__init__.py create mode 100644 yuthon_brain/models/brain_edge.py create mode 100644 yuthon_brain/models/brain_node.py create mode 100644 yuthon_brain/models/brain_source.py create mode 100644 yuthon_brain/security/ir.model.access.csv create mode 100644 yuthon_brain/static/src/brain_view.html create mode 100644 yuthon_brain/static/src/img/the_brain.png create mode 100644 yuthon_brain/views/brain_views.xml create mode 100644 yuthon_brain/views/menu.xml diff --git a/yuthon_brain/__init__.py b/yuthon_brain/__init__.py new file mode 100644 index 0000000..f7209b1 --- /dev/null +++ b/yuthon_brain/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/yuthon_brain/__manifest__.py b/yuthon_brain/__manifest__.py new file mode 100644 index 0000000..53b53d4 --- /dev/null +++ b/yuthon_brain/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': 'Yuthon Brain', + 'summary': 'TheBrain 风格脑图化内容展示', + 'version': '18.0.1.0.0', + 'category': 'Tools', + 'license': 'OPL-1', + 'depends': ['base', 'web'], + 'data': [ + 'security/ir.model.access.csv', + # 注意顺序:menu.xml 里的 会在解析时立即 + # env.ref() 解析动作的 xmlid,因此定义动作的 brain_views.xml 必须在前。 + 'views/brain_views.xml', + 'data/brain_data.xml', + 'views/menu.xml', + ], + 'installable': True, + 'application': True, +} diff --git a/yuthon_brain/controllers/__init__.py b/yuthon_brain/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/yuthon_brain/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/yuthon_brain/controllers/main.py b/yuthon_brain/controllers/main.py new file mode 100644 index 0000000..b23c255 --- /dev/null +++ b/yuthon_brain/controllers/main.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +import json +from odoo import http +from odoo.http import request, Response + + +class BrainController(http.Controller): + """只读控制器:前端脑图页面与查询接口,无任何写操作。""" + + # ---------------- 只读查询接口(JSON)---------------- + + @http.route('/yuthon_brain/api//graph', type='http', + auth='user', methods=['GET'], csrf=False) + def api_graph(self, key, **kw): + """返回某数据源的脑图结构:{root, nodes, edges, theme, layout}。""" + source = request.env['yuthon.brain.source'].search( + [('key', '=', key), ('active', '=', True)], limit=1) + if not source: + return Response(json.dumps({'error': 'not_found'}, ensure_ascii=False), + mimetype='application/json', status=404) + + nodes = [] + for n in source.node_ids: + try: + extra = json.loads(n.extra or '{}') if n.extra else {} + except (ValueError, TypeError): + extra = {} + nodes.append({ + 'id': n.id, + 'label': n.label, + 'parent_id': n.parent_id.id if n.parent_id else None, + 'rel_type': n.rel_type, + 'color': n.color or '#1E6FFF', + 'icon': n.icon or '', + 'level': n.level, + 'extra': extra, + }) + + edges = [{ + 'id': e.id, + 'from': e.from_id.id, + 'to': e.to_id.id, + 'rel_type': e.rel_type, + 'label': e.label or '', + } for e in source.edge_ids] + + payload = { + 'root': source.root_id.id if source.root_id else None, + 'theme': source.theme, + 'layout': source.layout, + 'source_name': source.name, + 'nodes': nodes, + 'edges': edges, + } + return Response(json.dumps(payload, ensure_ascii=False), + mimetype='application/json') + + @http.route('/yuthon_brain/api/sources', type='http', + auth='user', methods=['GET'], csrf=False) + def api_sources(self, **kw): + """已启用数据源列表。""" + sources = request.env['yuthon.brain.source'].search( + [('active', '=', True)], order='sequence, id') + data = [{'key': s.key, 'name': s.name} for s in sources] + return Response(json.dumps(data, ensure_ascii=False), + mimetype='application/json') diff --git a/yuthon_brain/data/brain_data.xml b/yuthon_brain/data/brain_data.xml new file mode 100644 index 0000000..9152927 --- /dev/null +++ b/yuthon_brain/data/brain_data.xml @@ -0,0 +1,66 @@ + + + + + shcool 文档脑图 + shcool_doc + light + radial + 3 + 10 + yuthon.about.page + name + category + [('active', '=', True)] + + + + + + shcool 文档 + child + #0ea5e9 + + + + + + + + + + shcool 功能与应用 + shcool_module + light + radial + 3 + 20 + feature + ir.module.module + shortdesc + features + [('features', '!=', False)] + + + + + shcool 功能与应用 + child + #0ea5e9 + + + + + + + + + diff --git a/yuthon_brain/models/__init__.py b/yuthon_brain/models/__init__.py new file mode 100644 index 0000000..48290d6 --- /dev/null +++ b/yuthon_brain/models/__init__.py @@ -0,0 +1,3 @@ +from . import brain_source +from . import brain_node +from . import brain_edge diff --git a/yuthon_brain/models/brain_edge.py b/yuthon_brain/models/brain_edge.py new file mode 100644 index 0000000..d20b66b --- /dev/null +++ b/yuthon_brain/models/brain_edge.py @@ -0,0 +1,24 @@ +from odoo import models, fields + + +class BrainEdge(models.Model): + """脑图连线:表达非树状的 jump / 跨模块引用。普通父子关系由 node.parent_id 承载,无需 edge。""" + _name = 'yuthon.brain.edge' + _description = '脑图连线' + _order = 'sequence, id' + + source_id = fields.Many2one( + 'yuthon.brain.source', string='数据源', required=True, ondelete='cascade', + ) + from_id = fields.Many2one( + 'yuthon.brain.node', string='起点', required=True, ondelete='cascade', + ) + to_id = fields.Many2one( + 'yuthon.brain.node', string='终点', required=True, ondelete='cascade', + ) + rel_type = fields.Selection( + [('jump', '跳转'), ('ref', '引用')], + string='关系', default='jump', + ) + label = fields.Char(string='连线说明') + sequence = fields.Integer(default=10) diff --git a/yuthon_brain/models/brain_node.py b/yuthon_brain/models/brain_node.py new file mode 100644 index 0000000..ef36149 --- /dev/null +++ b/yuthon_brain/models/brain_node.py @@ -0,0 +1,39 @@ +from odoo import models, fields + + +class BrainNode(models.Model): + """脑图节点。父子关系通过 parent_id 自引用表达(等价于 menu.parent_id)。""" + _name = 'yuthon.brain.node' + _description = '脑图节点' + _rec_name = 'label' + _order = 'sequence, id' + + source_id = fields.Many2one( + 'yuthon.brain.source', string='数据源', required=True, ondelete='cascade', + ) + parent_id = fields.Many2one( + 'yuthon.brain.node', string='父节点', ondelete='cascade', + ) + rel_type = fields.Selection( + [('parent', '父级'), ('child', '子级'), ('jump', '跳转')], + string='关系类型', default='child', + ) + label = fields.Char(string='显示文字', required=True) + color = fields.Char(string='颜色', default='#1E6FFF', help='节点主题色,十六进制') + icon = fields.Char(string='图标', help='可选,前端展示用') + extra = fields.Text(string='扩展数据(JSON)', help='前端自由展示的额外字段,JSON 字符串') + auto = fields.Boolean( + string='自动同步', default=False, + help='由 source.action_sync() 生成;重同步时会被清理,手动维护的节点不会被清。', + ) + sequence = fields.Integer(default=10) + level = fields.Integer(string='层级', default=0, help='可选;前端按 parent_id 链推导真实深度') + + child_ids = fields.One2many('yuthon.brain.node', 'parent_id', string='子节点') + edge_from_ids = fields.One2many('yuthon.brain.edge', 'from_id', string='出边') + edge_to_ids = fields.One2many('yuthon.brain.edge', 'to_id', string='入边') + + _sql_constraints = [ + ('src_parent_label_uniq', 'unique(source_id, parent_id, label)', + '同一数据源内、同一父节点下显示文字不能重复'), + ] diff --git a/yuthon_brain/models/brain_source.py b/yuthon_brain/models/brain_source.py new file mode 100644 index 0000000..16c8f19 --- /dev/null +++ b/yuthon_brain/models/brain_source.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +import json +import logging +import re + +from odoo import _, models, fields, api +from odoo.exceptions import UserError +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + +_LI_RE = re.compile(r']*>(.*?)', re.S | re.I) +_STRONG_RE = re.compile(r']*>(.*?)', re.S | re.I) +_TAG_RE = re.compile(r'<[^>]+>') +_ENTITIES = ( + (' ', ' '), ('&', '&'), ('<', '<'), + ('>', '>'), ('"', '"'), (''', "'"), +) + + +def _plain(text): + """去掉 HTML 标签与常见实体,压平空白并剥掉首尾标点。""" + s = _TAG_RE.sub('', text or '') + for ent, ch in _ENTITIES: + s = s.replace(ent, ch) + return ' '.join(s.split()).strip(' \u3000::;;,,。') + + +def parse_feature_html(html): + """把 features 这类 HTML 列表解析成 [(标题, 说明), ...]。 + + 兼容三种写法(shcool 用的是第 1、3 种): +
  • 师生管理:说明文字
  • → ('师生管理', '说明文字') +
  • 预置三组权限
  • → ('预置三组权限', '') strong 不在行首 +
  • 整句说明
  • → ('整句说明', '') + """ + items = [] + seen = set() + for raw in _LI_RE.findall(html or ''): + m = _STRONG_RE.search(raw) + if m and not _plain(raw[:m.start()]): + # strong 在行首:标题取 strong 内容,其余作为说明 + title = _plain(m.group(1)) + desc = _plain(raw[m.end():]) + else: + # 无 strong,或 strong 在句中(此时整句才是语义完整的标题) + title, desc = _plain(raw), '' + if not title or title in seen: + continue + seen.add(title) + items.append((title, desc)) + return items + + +class BrainSource(models.Model): + """脑图数据源:一个 source 对应一张可查看的脑图。 + + 节点数据有两种来源,互不冲突: + 1) 数据继承:子模块在自己的 data/*.xml 里用 parent_id ref 挂接(推荐,零代码); + 2) ORM 同步:本模型的 action_sync() 在运行时读取某个业务模型(如 shcool 的 + yuthon.about.page)记录,按分组字段生成节点,用于「不改动对方模块」的测试/导入。 + """ + + _name = 'yuthon.brain.source' + _description = '脑图数据源' + _order = 'sequence, id' + + name = fields.Char(string='名称', required=True) + key = fields.Char( + string='路由标识', + required=True, + help='前端接口路由用,如 product / org。需唯一且只含字母数字下划线。', + ) + root_id = fields.Many2one('yuthon.brain.node', string='根节点', ondelete='set null') + layout = fields.Selection( + [('radial', '放射状'), ('tree', '树状')], + string='布局', default='radial', + ) + theme = fields.Selection( + [('dark', '深色'), ('light', '浅色')], + string='主题', default='light', + ) + max_levels = fields.Integer(string='最大展开层级', default=3) + active = fields.Boolean(string='启用', default=True) + sequence = fields.Integer(default=10) + + # ---- ORM 同步配置(可选;用于从外部模块读取数据,不改对方代码)---- + sync_mode = fields.Selection( + [('record', '按记录分组'), ('feature', '解析功能亮点')], + string='同步方式', default='record', required=True, + help='按记录分组:每条记录生成一个节点;' + '解析功能亮点:把每条记录的 HTML 详情字段(如模块的「功能亮点」)' + '拆成若干功能点,作为该记录节点的子节点。', + ) + sync_model = fields.Char( + string='同步模型', + help='目标业务模型技术名,如 yuthon.about.page、ir.module.module。留空则不使用 ORM 同步。', + ) + sync_name_field = fields.Char( + string='名称字段', default='name', + help='作为节点显示文字的字段,默认 name;读模块时可用 shortdesc(中文显示名)。', + ) + sync_group_field = fields.Char( + string='分组字段', + help='仅在「按记录分组」模式生效:按该字段值分组生成父节点(如 category)。留空则全部直接挂在根下。', + ) + sync_detail_field = fields.Char( + string='详情字段', + help='仅在「解析功能亮点」模式生效:要拆解的 HTML 字段技术名,如 features。', + ) + sync_domain = fields.Char( + string='过滤条件', default='[]', + help='Odoo domain 字符串,如 [(\'active\',\'=\',True)]。', + ) + + node_ids = fields.One2many('yuthon.brain.node', 'source_id', string='节点') + edge_ids = fields.One2many('yuthon.brain.edge', 'source_id', string='连线') + + _sql_constraints = [ + ('uniq_key', 'unique(key)', '路由标识(key)必须唯一'), + ] + + def action_sync(self): + """从 sync_model 读取记录生成脑图节点(标记 auto,便于重同步清理)。 + + 不改任何外部模块代码,仅在 yuthon_brain 内创建节点。 + """ + self.ensure_one() + if not self.sync_model: + raise UserError(_('请先设置「同步模型」')) + if self.sync_model not in self.env: + raise UserError(_( + '目标模型 %s 未安装,无法同步(请确认对应模块已安装)' + ) % self.sync_model) + + Model = self.env[self.sync_model] + try: + domain = safe_eval(self.sync_domain or '[]') + except Exception: + domain = [] + recs = Model.search(domain) + + # 清掉上次自动同步的节点,保留手动维护的 + self.node_ids.filtered(lambda n: n.auto).unlink() + + root = self.root_id + if not root: + root = self.env['yuthon.brain.node'].create({ + 'source_id': self.id, + 'label': self.name, + 'rel_type': 'child', + 'color': '#0ea5e9', + 'auto': True, + }) + self.root_id = root + + name_field = self.sync_name_field or 'name' + group_field = self.sync_group_field + + if self.sync_mode == 'feature': + created, skipped = self._sync_features(Model, recs, root, name_field) + return self._done(created, recs, skipped) + + created = 0 + if group_field and group_field in Model._fields: + f = Model._fields[group_field] + selection = dict(f.selection) if f.type == 'selection' else {} + groups = {} + for r in recs: + gv = r[group_field] + groups.setdefault(gv, []).append(r) + for gv, grp in groups.items(): + glabel = selection.get(gv, gv) if gv else '未分类' + gnode = self.env['yuthon.brain.node'].create({ + 'source_id': self.id, + 'parent_id': root.id, + 'label': glabel, + 'rel_type': 'child', + 'color': '#2563eb', + 'auto': True, + 'extra': json.dumps({'group': True, 'value': gv}, + ensure_ascii=False), + }) + for r in grp: + self.env['yuthon.brain.node'].create({ + 'source_id': self.id, + 'parent_id': gnode.id, + 'label': r[name_field] or ('#%d' % r.id), + 'rel_type': 'child', + 'color': '#1E6FFF', + 'auto': True, + 'extra': json.dumps( + {'model': self.sync_model, 'res_id': r.id}, + ensure_ascii=False), + }) + created += 1 + else: + for r in recs: + self.env['yuthon.brain.node'].create({ + 'source_id': self.id, + 'parent_id': root.id, + 'label': r[name_field] or ('#%d' % r.id), + 'rel_type': 'child', + 'color': '#1E6FFF', + 'auto': True, + 'extra': json.dumps( + {'model': self.sync_model, 'res_id': r.id}, + ensure_ascii=False), + }) + created += 1 + + return self._done(created, recs) + + @api.model + def action_sync_all(self): + """安装/升级时自动同步所有已配置 ORM 的数据源,让脑图开箱即有内容。 + + 单个数据源失败(例如目标模块未安装)只记日志、不阻断安装。 + """ + for src in self.search([('sync_model', '!=', False)]): + try: + src.action_sync() + _logger.info('yuthon_brain: 数据源 %s 自动同步完成', src.key) + except Exception: + _logger.warning( + 'yuthon_brain: 数据源 %s 自动同步失败(已跳过)', src.key, + exc_info=True) + return True + + def _done(self, created, recs, skipped=None): + """统一的同步结果提示。""" + msg = _('已同步 %(rec)d 条记录、生成 %(n)d 个节点') % { + 'rec': len(recs), 'n': created, + } + if skipped: + msg += _(';%d 条记录无功能亮点已跳过') % len(skipped) + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'message': msg, + 'type': 'success', + 'sticky': False, + }, + } + + def _sync_features(self, Model, recs, root, name_field): + """解析功能亮点模式:每条记录 → 一个父节点,其详情 HTML 的每个
  • → 一个子节点。 + + 用于把 ir.module.module 的「功能亮点」这类富文本清单摊平成脑图分支, + 全程只读外部模型,不修改对方任何代码或数据。 + """ + detail_field = self.sync_detail_field + if not detail_field: + raise UserError(_('「解析功能亮点」模式需要先设置「详情字段」,例如 features')) + if detail_field not in Model._fields: + raise UserError(_( + '模型 %s 上不存在字段 %s,请检查「详情字段」设置' + ) % (self.sync_model, detail_field)) + + Node = self.env['yuthon.brain.node'] + created = 0 + skipped = [] + for r in recs: + items = parse_feature_html(r[detail_field]) + if not items: + skipped.append(r[name_field] or r.display_name) + continue + label = (r[name_field] if name_field in Model._fields else '') \ + or r.display_name or ('#%d' % r.id) + rec_node = Node.create({ + 'source_id': self.id, + 'parent_id': root.id, + 'label': label, + 'rel_type': 'child', + 'color': '#2563eb', + 'auto': True, + 'extra': json.dumps( + {'model': self.sync_model, 'res_id': r.id, + 'count': len(items)}, + ensure_ascii=False), + }) + created += 1 + for title, desc in items: + Node.create({ + 'source_id': self.id, + 'parent_id': rec_node.id, + 'label': title, + 'rel_type': 'child', + 'color': '#1E6FFF', + 'auto': True, + 'extra': json.dumps( + {'model': self.sync_model, 'res_id': r.id, 'desc': desc}, + ensure_ascii=False), + }) + created += 1 + return created, skipped diff --git a/yuthon_brain/security/ir.model.access.csv b/yuthon_brain/security/ir.model.access.csv new file mode 100644 index 0000000..d05e00c --- /dev/null +++ b/yuthon_brain/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_yuthon_brain_source_admin,yuthon.brain.source.admin,model_yuthon_brain_source,base.group_system,1,1,1,1 +access_yuthon_brain_source_user,yuthon.brain.source.user,model_yuthon_brain_source,base.group_user,1,0,0,0 +access_yuthon_brain_node_admin,yuthon.brain.node.admin,model_yuthon_brain_node,base.group_system,1,1,1,1 +access_yuthon_brain_node_user,yuthon.brain.node.user,model_yuthon_brain_node,base.group_user,1,0,0,0 +access_yuthon_brain_edge_admin,yuthon.brain.edge.admin,model_yuthon_brain_edge,base.group_system,1,1,1,1 +access_yuthon_brain_edge_user,yuthon.brain.edge.user,model_yuthon_brain_edge,base.group_user,1,0,0,0 \ No newline at end of file diff --git a/yuthon_brain/static/src/brain_view.html b/yuthon_brain/static/src/brain_view.html new file mode 100644 index 0000000..5315ab5 --- /dev/null +++ b/yuthon_brain/static/src/brain_view.html @@ -0,0 +1,898 @@ + + + + + + 脑图 + + + +
    +
    +
    + + 脑图 +
    + +
    + + + + +
    +
    +
    + + +
    +
    滚轮缩放 · 拖拽平移 · 点击节点聚焦
    +
    + + + + diff --git a/yuthon_brain/static/src/img/the_brain.png b/yuthon_brain/static/src/img/the_brain.png new file mode 100644 index 0000000000000000000000000000000000000000..ff48c136193ec6286e4ceba3977ef3b599b8f2e1 GIT binary patch literal 7127 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 + + + + yuthon.brain.source.list + yuthon.brain.source + + + + + + + + + + + + + + + yuthon.brain.source.form + yuthon.brain.source + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + + 脑图数据源 + yuthon.brain.source + list,form + + + + + yuthon.brain.node.list + yuthon.brain.node + + + + + + + + + + + + + + yuthon.brain.node.form + yuthon.brain.node + +
    + + + + + + + + + + + + + + + +
    +
    +
    + + + + yuthon.brain.edge.list + yuthon.brain.edge + + + + + + + + + + + + + yuthon.brain.edge.form + yuthon.brain.edge + +
    + + + + + + + + + +
    +
    +
    +
    diff --git a/yuthon_brain/views/menu.xml b/yuthon_brain/views/menu.xml new file mode 100644 index 0000000..b0ac57e --- /dev/null +++ b/yuthon_brain/views/menu.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + shcool 文档脑图 + /yuthon_brain/static/src/brain_view.html?key=shcool_doc + new + + + + + + shcool 功能与应用 + /yuthon_brain/static/src/brain_view.html?key=shcool_module + new + + + -- 2.34.1