# -*- coding: utf-8 -*- """ Generate Bruno .bru files for crm-opportunity module into D:/code/crm-api-docs. All files are page-tag-based (config grouping = tag). Strictly follows the bruno-sync SKILL template. """ import io, os, sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') DOCS = 'D:/code/crm-api-docs' # ---------- utility ---------- def ensure_dir(p): os.makedirs(p, exist_ok=True) def write_bru(folder_rel, filename, content): d = os.path.join(DOCS, folder_rel) ensure_dir(d) p = os.path.join(d, filename) with open(p, 'w', encoding='utf-8', newline='\n') as f: f.write(content) print(f' wrote {folder_rel}/{filename}') def write_folder_bru(folder_rel, name, seq): d = os.path.join(DOCS, folder_rel) ensure_dir(d) p = os.path.join(d, 'folder.bru') if os.path.exists(p): return with open(p, 'w', encoding='utf-8', newline='\n') as f: f.write(f'meta {{\n name: {name}\n seq: {seq}\n}}\n') print(f' wrote {folder_rel}/folder.bru') # ---------- template pieces ---------- def bru_form(seq, name, method, url, params_rows, docs_body): """ method: 'post' / 'get' params_rows: list of (key, example) — for form / query block docs_body: full docs { ... } inner (without wrapping braces) """ body_kw = 'formUrlEncoded' if method == 'post' else 'none' lines = [] lines.append('meta {') lines.append(f' name: {name}') lines.append(' type: http') lines.append(f' seq: {seq}') lines.append(' tags: [') lines.append(' generated') lines.append(' ]') lines.append('}') lines.append('') lines.append(f'{method} {{') lines.append(f' url: {{{{baseUrl}}}}{url}') lines.append(f' body: {body_kw}') lines.append(' auth: inherit') lines.append('}') lines.append('') if method == 'post' and params_rows: lines.append('body:form-urlencoded {') for k, v in params_rows: lines.append(f' {k}: {v}') lines.append('}') lines.append('') elif method == 'get' and params_rows: lines.append('params:query {') for k, v in params_rows: lines.append(f' {k}: {v}') lines.append('}') lines.append('') lines.append('docs {') for line in docs_body.rstrip().split('\n'): lines.append(' ' + line if line else '') lines.append('}') return '\n'.join(lines) + '\n' def build_docs(title, method, path, desc, params_table, req_example, resp_data_table, resp_example_json_or_text, resp_is_text=False): """ params_table: list of (name, type, required, default, desc) resp_data_table: list of (field, type, desc); or None for null; or a special marker resp_example_json_or_text: str (either JSON block content without fences, or a plain sentence when resp_is_text=True) """ lines = [] lines.append(f'# {title}') lines.append(f'`{method} {path}`') lines.append(desc) lines.append('') lines.append('## 请求参数') lines.append('') if params_table: lines.append('| 参数 | 类型 | 必填 | 默认 | 说明 |') lines.append('| --- | --- | --- | --- | --- |') for name, typ, req, dflt, d in params_table: lines.append(f'| {name} | {typ} | {req} | {dflt} | {d} |') else: lines.append('无参数。') lines.append('') lines.append('## 请求示例') lines.append('') for line in req_example.rstrip().split('\n'): lines.append(line) lines.append('') lines.append('## 响应 data 结构') lines.append('') if resp_data_table is None: lines.append('null') elif isinstance(resp_data_table, str): lines.append(resp_data_table) else: lines.append('| 字段 | 类型 | 说明 |') lines.append('| --- | --- | --- |') for f, t, d in resp_data_table: lines.append(f'| {f} | {t} | {d} |') lines.append('') lines.append('## 响应示例') lines.append('') if resp_is_text: lines.append(resp_example_json_or_text) else: lines.append('// 示例数据,字段结构以上表为准,非真实返回') lines.append('```json') for line in resp_example_json_or_text.rstrip().split('\n'): lines.append(line) lines.append('```') return '\n'.join(lines) def env_json(data_literal): """Wrap data literal (already indented as body) in the standard success envelope.""" return ( '{\n' ' "code": 0,\n' ' "success": true,\n' ' "message": "success",\n' f' "data": {data_literal}\n' '}' ) def req_form_example(method, path, kv_pairs, public=False): header = '请求头:无需鉴权' if public else '请求头:Authorization: Bearer {{token}}' body = '&'.join(f'{k}={v}' for k, v in kv_pairs) return ( f'{header}\n' f'Content-Type: application/x-www-form-urlencoded\n\n' f'{method} {path}\n' f'{body}' ) def req_query_example(method, path, kv_pairs): header = '请求头:Authorization: Bearer {{token}}' if kv_pairs: qs = '&'.join(f'{k}={v}' for k, v in kv_pairs) return f'{header}\n\n{method} {path}?{qs}' return f'{header}\n\n{method} {path}' # ---------- top-level folder scaffolds ---------- print('== folders ==') write_folder_bru('商机管理', '商机管理', 20) write_folder_bru('销售机会', '销售机会', 21) write_folder_bru('商机公海', '商机公海', 22) # sub-folders under 商机管理 for name in ['商机新建', '商机详情', '商机流转', '协作', '方案卡', '商机阶段', '详情子域']: write_folder_bru(f'商机管理/{name}', name, 10) # sub-folders under 销售机会 for i, name in enumerate(['最近访问', '我负责的', '我参与的', '我关注的'], start=10): write_folder_bru(f'销售机会/{name}', name, i) # ================================================================ # Reusable pieces — big response tables # ================================================================ # OpportunityDTO fields (used for detail & inside page/PageResult content) OPP_DTO_ROWS = [ ('id', 'string', '商机ID(Long,序列化为 string)'), ('createTime', 'string', '创建时间 yyyy-MM-dd HH:mm:ss'), ('updateTime', 'string', '更新时间 yyyy-MM-dd HH:mm:ss'), ('oppName', 'string', '商机名称'), ('oppSource', 'string', '商机来源字典 code'), ('oppType', 'string', '商机类型字典 code'), ('industryCode', 'string', '行业字典 code'), ('bidForm', 'string', '招标形式字典 code'), ('localityType', 'string', '项目属地字典 code'), ('partyAClear', 'number', '甲方是否明确 0/1'), ('partyA', 'string', '甲方名称'), ('provinceCode', 'string', '省份国标 code'), ('cityCode', 'string', '市/区国标 code'), ('remark', 'string', '备注'), ('projectAmount', 'number', '项目金额(列表展示为「预计金额」)'), ('ownerUserId', 'string', '负责人ID(Long,序列化为 string)'), ('ownerNameSnapshot', 'string', '负责人姓名快照'), ('ownerDeptId', 'string', '负责人部门ID快照'), ('poolReason', 'string', '进入公海原因字典 code'), ('oppStatus', 'number', '商机状态(轴1:1待领取/2推进中/3暂缓中/4已关闭/5已转项目)'), ('currentStageId', 'string', '当前阶段节点ID(轴2)'), ('stageTemplateVersion', 'string', '阶段模板版本号冗余'), ('sourceLeadId', 'string', '来源线索ID'), ('primaryCustomerId', 'string', '主要意向客户ID'), ('primaryCustomerNameSnapshot', 'string', '主客户名快照(列表「意向客户」列)'), ('claimTime', 'string', '领取时刻'), ('lastValidFollowTime', 'string', '最近有效跟进时间'), ('oppSourceName', 'string', '商机来源名称(出参回显,字典快照)'), ('industryName', 'string', '行业名称(出参回显,字典快照)'), ('provinceName', 'string', '省份名称(出参回显)'), ('cityName', 'string', '市/区名称(出参回显)'), ('ownerDeptName', 'string', '负责人部门名称(出参回显)'), ('currentStageName', 'string', '当前工作节点名称(出参回显)'), ('stageStayDays', 'number', '节点停留时间(天,出参回显)'), ('nextStageName', 'string', '下一节点名称(出参回显,展示用建议下一步)'), ('lastFollowTime', 'string', '最近跟进时间(出参回显)'), ('schemeCardStatus', 'number', '方案卡状态(出参回显;无主客户卡=未创建0)'), ('schemeBudget', 'number', '方案预算(出参回显)'), ('followed', 'boolean', '当前用户是否已关注(出参回显)'), ('poolStageNameSnapshot', 'string', '入池前工作节点名称(出参回显,公海视图专属)'), ] OPP_DTO_JSON = ( '{\n' ' "id": "1",\n' ' "createTime": "2024-01-01 12:00:00",\n' ' "updateTime": "2024-01-01 12:00:00",\n' ' "oppName": "示例商机",\n' ' "oppSource": "opp_source_01",\n' ' "oppType": "opp_type_01",\n' ' "industryCode": "industry_01",\n' ' "bidForm": "bid_01",\n' ' "localityType": "local_01",\n' ' "partyAClear": 1,\n' ' "partyA": "示例甲方",\n' ' "provinceCode": "110000",\n' ' "cityCode": "110100",\n' ' "remark": "备注示例",\n' ' "projectAmount": 1000000,\n' ' "ownerUserId": "1",\n' ' "ownerNameSnapshot": "张三",\n' ' "ownerDeptId": "1",\n' ' "poolReason": null,\n' ' "oppStatus": 2,\n' ' "currentStageId": "1",\n' ' "stageTemplateVersion": "v1",\n' ' "sourceLeadId": "1",\n' ' "primaryCustomerId": "1",\n' ' "primaryCustomerNameSnapshot": "示例客户",\n' ' "claimTime": "2024-01-01 12:00:00",\n' ' "lastValidFollowTime": "2024-01-02 10:00:00",\n' ' "oppSourceName": "线索转入",\n' ' "industryName": "示例行业",\n' ' "provinceName": "北京市",\n' ' "cityName": "北京市",\n' ' "ownerDeptName": "华北大区",\n' ' "currentStageName": "需求确认",\n' ' "stageStayDays": 3,\n' ' "nextStageName": "方案报价",\n' ' "lastFollowTime": "2024-01-02 10:00:00",\n' ' "schemeCardStatus": 1,\n' ' "schemeBudget": 800000,\n' ' "followed": true,\n' ' "poolStageNameSnapshot": null\n' ' }' ) print('done setup, will emit in next files')