# -*- coding: utf-8 -*- """生成《禅道 AI 通道接口文档》Word 对外交付版(3 接口 + token,不含 login)""" from docx import Document from docx.shared import Pt, RGBColor, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn TOKEN = ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." "eyJhY2NvdW50IjoiYWkiLCJwYXNzd29yZCI6ImUxMGFkYzM5NDliYTU5YWJiZTU2ZTA1N2YyMGY4ODNlIiwidXNlclR5cGUiOjN9." "d43vA9pN_dhwwfeO0zg3_AaX76vEMXRYFZSc6AkYF7c") doc = Document() # 全局字体:正文宋体/Calibri,标题黑体 style = doc.styles['Normal'] style.font.name = 'Calibri' style.font.size = Pt(10.5) style.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') def set_cn_font(run, name='宋体'): run.font.name = name run._element.rPr.rFonts.set(qn('w:eastAsia'), name) def heading(text, level): h = doc.add_heading(text, level=level) for r in h.runs: set_cn_font(r, '黑体') r.font.color.rgb = RGBColor(0, 0, 0) return h def para(text, bold=False, size=10.5, color=None): p = doc.add_paragraph() r = p.add_run(text) r.bold = bold r.font.size = Pt(size) if color: r.font.color.rgb = color set_cn_font(r) return p def code_block(text): p = doc.add_paragraph() p.paragraph_format.left_indent = Cm(0.5) p.paragraph_format.space_before = Pt(4) p.paragraph_format.space_after = Pt(4) r = p.add_run(text) r.font.name = 'Consolas' r.font.size = Pt(9) r._element.rPr.rFonts.set(qn('w:eastAsia'), 'Consolas') return p def table(headers, rows, widths=None): t = doc.add_table(rows=1 + len(rows), cols=len(headers)) t.style = 'Table Grid' for i, h in enumerate(headers): cell = t.rows[0].cells[i] cell.text = '' r = cell.paragraphs[0].add_run(h) r.bold = True r.font.size = Pt(9.5) set_cn_font(r) for ri, row in enumerate(rows): for ci, val in enumerate(row): cell = t.rows[ri + 1].cells[ci] cell.text = '' r = cell.paragraphs[0].add_run(str(val)) r.font.size = Pt(9.5) set_cn_font(r) if widths: for ci, w in enumerate(widths): for row in t.rows: row.cells[ci].width = Cm(w) doc.add_paragraph() return t # ================= 封面信息 ================= title = doc.add_paragraph() title.alignment = WD_ALIGN_PARAGRAPH.CENTER r = title.add_run('禅道 AI 通道接口文档') r.bold = True r.font.size = Pt(22) set_cn_font(r, '黑体') sub = doc.add_paragraph() sub.alignment = WD_ALIGN_PARAGRAPH.CENTER r = sub.add_run('(AI 框架 × 禅道系统 对接接口 · 对外提供版)') r.font.size = Pt(12) set_cn_font(r) info = doc.add_paragraph() info.alignment = WD_ALIGN_PARAGRAPH.CENTER r = info.add_run('版本:v1.0 日期:2026-08-07 密级:内部(含永久令牌,注意保管)') r.font.size = Pt(10) set_cn_font(r) doc.add_paragraph() # ================= 1 接入信息 ================= heading('1. 接入信息', 1) heading('1.1 服务地址', 2) table(['环境', 'Base URL', '说明'], [['测试环境', 'http://127.0.0.1:8085/zentao', '本地/联调使用'], ['生产环境', 'https://itsm.sino-assist.com/zentao', '正式环境']], widths=[3, 8, 5]) para('所有接口路径均以 Base URL 为前缀拼接;请求与响应均为 UTF-8 编码。') heading('1.2 鉴权方式(重要)', 2) para('所有接口必须在请求头中携带访问令牌(token):') code_block('Authorization: ') para('注意:直接放 token 原文,不要加 "Bearer " 前缀。', bold=True) para('本令牌为 AI 专用账号(账号名:ai)的永久令牌,不会过期,请妥善保管、勿外泄、勿提交到代码仓库:', bold=True) code_block(TOKEN) para('未携带或令牌无效时,接口返回:{"code":-1,"message":"请登录"}') heading('1.3 统一响应格式', 2) para('所有接口返回统一 JSON 结构:') code_block('{\n "code": 0, // 0=成功;-1=失败(message 为失败原因)\n "message": "成功",\n "data": { ... } // 各接口不同,可能为 null\n}') # ================= 2 接口一览 ================= heading('2. 接口一览', 1) table(['#', '接口名称', '方法', '路径', '提交方式', '用途'], [['1', '评估结果提交', 'POST', '/zentao/zt-story-expand/saveOrUpdate', 'JSON', '提交需求的工作量评估指标(单元数量/复杂度/系数/工时)与验收标准'], ['2', 'AI 批量创建任务', 'POST', '/zentao/zt-task/aiBatchAdd', 'JSON', '为指定需求批量创建研发/测试任务,重复任务自动跳过'], ['3', '文件上传并绑定', 'POST', '/zentao/common/uploadBind', 'multipart/form-data', '上传文档并一步绑定到需求/会议,页面即时可见']], widths=[1, 3.2, 1.6, 5.5, 3, 5]) # ================= 3 saveOrUpdate ================= heading('3. 接口一:评估结果提交 saveOrUpdate', 1) code_block('POST {BaseURL}/zt-story-expand/saveOrUpdate\n' 'Content-Type: application/json\n' 'Authorization: ') para('用途:需求评估完成后,将工作量评估指标与验收标准写入需求扩展信息。同一需求重复提交为覆盖更新(幂等)。') heading('3.1 请求参数', 2) table(['参数', '类型', '必填', '说明'], [['storyId', 'Integer', '是', '需求 ID'], ['numberUnits', 'Integer', '评估时必填', '单元数量 S'], ['unitBusinessComplexity', 'String', '评估时必填', '单元业务复杂度 B,如 "2.2"'], ['technicalComplexityCoefficient', 'String', '评估时必填', '技术复杂度系数 F(T),如 "1.4"'], ['aiEfficiencyCoefficient', 'String', '评估时必填', 'AI 效率系数 G(A),如 "0.55"'], ['evaluationTime', 'Number', '评估时必填', '评估工时 W(人日),如 5.1'], ['workloadIndex', 'String', '否', '工作量指数'], ['requirementStatus', 'String', '否', 'inProgress(默认)/ finished;传 finished 表示需求完成,将锁定记录并结算当月工作量'], ['requirementCompletionDegree', 'String', '否', '完成度 "0"~"100";finished 时自动置 100'], ['acceptanceCriteria', 'String', '否', '验收标准(Markdown 文本)'], ['productPerson', 'String', '否', '产品人员(中文名)'], ['developPerson', 'String', '否', '开发人员(中文名)'], ['testPerson', 'String', '否', '测试人员(中文名)']], widths=[4.5, 2, 2.2, 8]) heading('3.2 请求示例', 2) code_block('{\n' ' "storyId": 9130,\n' ' "numberUnits": 3,\n' ' "unitBusinessComplexity": "2.2",\n' ' "technicalComplexityCoefficient": "1.4",\n' ' "aiEfficiencyCoefficient": "0.55",\n' ' "evaluationTime": 5.1,\n' ' "workloadIndex": "5.1",\n' ' "developPerson": "张三",\n' ' "testPerson": "李四"\n' '}') heading('3.3 响应示例', 2) code_block('{ "code": 0, "message": "成功", "data": null }') heading('3.4 注意事项', 2) for t in ['storyId 为空时接口返回成功但不做任何处理(请务必确认已传 storyId);', '需求一旦置为 finished,记录即锁定,后续提交将被拒绝(提示"该需求已完成,不可再修改");', '同一 storyId 重复提交 = 覆盖更新,不会产生重复记录。']: p = doc.add_paragraph(t, style='List Bullet') for r in p.runs: set_cn_font(r) r.font.size = Pt(10) # ================= 4 aiBatchAdd ================= heading('4. 接口二:AI 批量创建任务 aiBatchAdd', 1) code_block('POST {BaseURL}/zt-task/aiBatchAdd\n' 'Content-Type: application/json\n' 'Authorization: ') para('用途:为指定需求批量创建研发/测试任务。已存在的同名同类型任务自动跳过(防重),并在需求与任务的操作记录中留痕。') heading('4.1 请求参数', 2) table(['参数', '类型', '必填', '说明'], [['storyId', 'Integer', '是', '需求 ID;需求不存在则整批拒绝'], ['tasks', 'Array', '是', '任务列表,不能为空'], ['tasks[].name', 'String', '是', '任务名称'], ['tasks[].type', 'String', '是', '任务类型:devel=开发 / test=测试,其他值整批拒绝'], ['tasks[].assignedTo', 'String', '否', '指派人账号(登录账号,非中文名)'], ['tasks[].aiEvaluationTime', 'Float', '否', 'AI 评估工时(小时),写入任务预计工时'], ['tasks[].planStartDate', 'String', '否', '预计开始日期,格式 yyyy-MM-dd'], ['tasks[].deadline', 'String', '否', '预计完成日期,格式 yyyy-MM-dd']], widths=[4.5, 2, 2.2, 8]) heading('4.2 请求示例', 2) code_block('{\n' ' "storyId": 9130,\n' ' "tasks": [\n' ' {"name": "后端接口开发", "type": "devel", "assignedTo": "zhangsan",\n' ' "aiEvaluationTime": 8, "planStartDate": "2026-08-10", "deadline": "2026-08-11"},\n' ' {"name": "接口测试", "type": "test", "assignedTo": "lisi",\n' ' "aiEvaluationTime": 4, "planStartDate": "2026-08-12", "deadline": "2026-08-12"}\n' ' ]\n' '}') heading('4.3 响应示例', 2) code_block('{\n' ' "code": 0,\n' ' "message": "成功",\n' ' "data": {\n' ' "created": 2, // 实际新建任务数\n' ' "taskIds": [18565, 18566], // 新建任务 ID 列表\n' ' "skipped": ["接口测试"] // 因重名同类型被跳过的任务名\n' ' }\n' '}') heading('4.4 注意事项', 2) for t in ['校验规则为"整批拒绝":任一任务不合法(类型错误/名称为空/日期格式错误等),本批全部不创建;', '防重规则:同一需求下已存在同名且同类型(未删除)的任务 → 跳过并记入 skipped,不算失败;', '全部命中防重时返回 code:0、created:0,skipped 列出全部任务名;', '新任务初始状态为"未开始",创建人显示为 ai,操作记录可在需求/任务历史中查看。']: p = doc.add_paragraph(t, style='List Bullet') for r in p.runs: set_cn_font(r) r.font.size = Pt(10) # ================= 5 uploadBind ================= heading('5. 接口三:文件上传并绑定 uploadBind', 1) code_block('POST {BaseURL}/common/uploadBind\n' 'Content-Type: multipart/form-data\n' 'Authorization: ') para('用途:上传文件并一步绑定到业务对象(需求/会议):文件入库的同时,自动刷新业务对象的文档链接字段,' '并在其操作记录中留痕。需求详情页、会议纪要页等界面即时可见。') heading('5.1 请求参数(表单字段)', 2) table(['参数', '类型', '必填', '说明'], [['file', 'File', '是', '上传的文件(支持 .md 等常见格式)'], ['objectType', 'String', '是', '业务对象类型,取值见 5.2 对照表'], ['objectId', 'Integer', '是', '业务对象 ID(需求 ID 或会议 ID)'], ['title', 'String', '否', '文件标题;不传默认取原始文件名(支持中文)'], ['reviewResult', 'String', '否', '仅 objectType=aiCodeReview 时有效:pass=审查通过 / reject=审查不通过']], widths=[3, 2, 2.2, 9.3]) heading('5.2 objectType 取值对照表', 2) table(['objectType', '含义', '绑定后效果'], [['story', '需求文档(PRD 等)', '需求文档链接更新'], ['aiCodeReview', '代码审查报告', '审查报告链接更新;带 reviewResult 时同步审查状态'], ['aiWorkLog', '工作日志', '工作日志链接更新'], ['aiDocUpdate', 'AI 项目文档更新记录', '更新记录链接更新'], ['testCase', '测试用例', '测试用例链接更新'], ['testReport', '测试报告模版', '报告模版链接更新'], ['testReportSubmit', '测试报告提交', '报告提交链接更新(SOP 流程卡点依据)'], ['testOther', '其他测试文档', '其他文档链接更新'], ['meeting', '会议纪要', '会议纪要链接更新(objectId 填会议 ID)']], widths=[3.5, 4.5, 8.5]) heading('5.3 请求示例', 2) code_block('curl -X POST "http://127.0.0.1:8085/zentao/common/uploadBind" \\\n' ' -H "Authorization: " \\\n' ' -F "file=@代码审查报告.md" \\\n' ' -F "objectType=aiCodeReview" \\\n' ' -F "objectId=9130" \\\n' ' -F "title=代码审查报告-v1" \\\n' ' -F "reviewResult=pass"') heading('5.4 响应示例', 2) code_block('{\n' ' "code": 0,\n' ' "message": "成功",\n' ' "data": {\n' ' "id": 1234,\n' ' "title": "代码审查报告-v1",\n' ' "extension": ".md",\n' ' "size": 5321,\n' ' "url": "/zentao/img/20260806170215xxxx.md", // 文件访问地址(相对路径)\n' ' "objecttype": "aiCodeReview",\n' ' "objectid": 9130,\n' ' "addedby": "ai",\n' ' "addeddate": "2026-08-06 17:02:15"\n' ' }\n' '}') heading('5.5 注意事项', 2) for t in ['同一对象同一类型可多次上传形成文件列表,业务对象上的链接字段始终指向最新一份;', 'objectType/objectId 错误、对象不存在、类型不在对照表内,均会拒绝且不落盘(无残留);', '返回的 url 为相对路径,访问时拼在系统域名后即可打开/下载。']: p = doc.add_paragraph(t, style='List Bullet') for r in p.runs: set_cn_font(r) r.font.size = Pt(10) # ================= 6 错误码 ================= heading('6. 常见返回码速查', 1) table(['code', 'message 示例', '含义与处理'], [['0', '成功', '调用成功'], ['-1', '请登录', '未带 token 或 token 无效 → 检查 Authorization 头'], ['-1', '该接口仅AI框架通道可用(需ai账户token)', 'token 不是 ai 账号令牌 → 换用本文档 1.2 节令牌'], ['-1', '该需求已完成,不可再修改', '需求已 finished 锁定(接口一的锁定机制)'], ['-1', '需求不存在:xxx / 会议不存在:xxx', 'objectId 或 storyId 对应记录不存在 → 核对 ID'], ['-1', 'uploadBind不支持的objectType:xxx', 'objectType 取值错误 → 对照 5.2 表'], ['-1', '任务类型仅支持devel/test:xxx', '任务类型取值错误(接口二)'], ['-1', '日期格式错误,应为yyyy-MM-dd:xxx', '日期格式错误(接口二)']], widths=[1.5, 7, 8]) doc.add_paragraph() tail = doc.add_paragraph() r = tail.add_run('—— 本文档含永久访问令牌,仅限授权对接人员持有,请勿外传 ——') r.font.size = Pt(9) r.font.color.rgb = RGBColor(0x99, 0x99, 0x99) set_cn_font(r) OUT = r'F:\zentao\1\PM\prds\ai-sop-20260723-1024\outputs\禅道AI通道接口文档_v1.0.docx' doc.save(OUT) print('saved:', OUT)