From 9e959f58e0c6caba693f9a151217129e63208e83 Mon Sep 17 00:00:00 2001 From: zelong <2895587166@qq.com> Date: Thu, 18 Dec 2025 16:23:16 +0800 Subject: [PATCH] 'phase4 --- TAPD接口文档.md | 1162 +++++++++++++++++++++++++++++++++++++++ config/config.ini | 2 +- config/token_cache.json | 4 + src/main.py | 76 ++- src/smartsheet.py | 16 +- src/token_manager.py | 328 +++++++++++ 开发路线.md | 274 +++++---- 7 files changed, 1708 insertions(+), 154 deletions(-) create mode 100644 config/token_cache.json create mode 100644 src/token_manager.py diff --git a/TAPD接口文档.md b/TAPD接口文档.md index 3753015..e9edc35 100644 --- a/TAPD接口文档.md +++ b/TAPD接口文档.md @@ -416,6 +416,1168 @@ curl -u 'api_user:api_password' -d 'name=story_created_by_api&workspace_id=10158 # 缺陷 +## 获取缺陷所有字段及候选值 + +### 说明 + +返回缺陷所有字段及候选值(枚举值),即通常理解的字段的 "英文Key" 和 "中文值". + +# [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#url)url + +``` +https://tapd-api.bilibili.co/bugs/get_fields_info +``` + +# [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#支持格式)支持格式 + +JSON/XML(默认JSON格式) + +# [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#http请求方式)HTTP请求方式 + +GET + +# [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#请求数限制)请求数限制 + +默认返回所有数据 + +# [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#请求参数)请求参数 + +| 字段名 | 必选 | 类型及范围 | 说明 | 特殊规则 | +| :----------: | :--: | :--------: | :----------------------------------------------------------: | :------: | +| workspace_id | `是` | integer | 项目ID | | +| all_options | 否 | integer | 是否也返回已关闭的选项。all_options=1 则返回。默认是 0,不返回,与TAPD界面对齐 | | + +# [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#调用示例及返回结果)调用示例及返回结果 + +## [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#获取项目下的缺陷字段)获取项目下的缺陷字段 + +### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#curl-使用-basic-auth-鉴权调用示例)curl 使用 Basic Auth 鉴权调用示例 + +``` +curl -u 'api_user:api_password' 'https://tapd-api.bilibili.co/bugs/get_fields_info?workspace_id=10104801' +``` + +### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#返回结果)返回结果 + +```json +{ + "status": 1, + "data": { + "id": { + "name": "id", + "label": "ID", + "options": [], + "pure_options": [], + "html_type": "input", + "memo": "" + }, + "title": { + "name": "title", + "label": "标题", + "options": [], + "pure_options": [], + "html_type": "input", + "memo": "" + }, + "description": { + "name": "description", + "label": "详细描述", + "options": [], + "pure_options": [], + "html_type": "rich_edit", + "memo": "" + }, + "module": { + "name": "module", + "label": "模块", + "options": { + "333": "333", + "创建模块": "创建模块" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "feature": { + "name": "feature", + "label": "特性", + "options": { + "tx1": "tx1" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "reporter": { + "name": "reporter", + "label": "创建人", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "deadline": { + "name": "deadline", + "label": "解决期限", + "options": [], + "pure_options": [], + "html_type": "dateinput", + "memo": "" + }, + "created": { + "name": "created", + "label": "创建时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "reopen_time": { + "name": "reopen_time", + "label": "重新打开时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "closed": { + "name": "closed", + "label": "关闭时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "modified": { + "name": "modified", + "label": "最后修改时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "lastmodify": { + "name": "lastmodify", + "label": "最后修改人", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "de": { + "name": "de", + "label": "开发人员", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "te": { + "name": "te", + "label": "测试人员", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "auditer": { + "name": "auditer", + "label": "审核人", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "confirmer": { + "name": "confirmer", + "label": "验证人", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "current_owner": { + "name": "current_owner", + "label": "处理人", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "status": { + "name": "status", + "label": "状态", + "options": { + "new": "新", + "in_progress": "接受/处理", + "assigned": "已分配", + "verified": "已验证", + "postponed": "延期", + "reopened": "重新打开", + "resolved": "已解决", + "rejected": "已拒绝", + "closed": "已关闭x", + "unconfirmed": "待确认的", + "planning": "计划中", + "feedback": "需要再次说明", + "acknowledged": "已了解", + "suspended": "挂起", + "TM_audited": "TM审核", + "PMM_audited": "PMM审核", + "PM_audited": "PM审核", + "QA_audited": "QA审核" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "resolution": { + "name": "resolution", + "label": "解决方法", + "options": { + "ignore": "无需解决", + "fixed": "已修改", + "fix later": "延期解决", + "failed to recur": "无法重现", + "external reason": "外部原因", + "duplicated": "重复", + "intentional design": "设计如此", + "unclear description ": "问题描述不准确", + "feature change": "需求变更", + "transferred to story": "已转需求", + "hold": "挂起" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "priority": { + "name": "priority", + "label": "优先级", + "color_options": [ + { + "value": "urgent", + "label": "紧急", + "color": "#FF6770" + }, + { + "value": "high", + "label": "高", + "color": "#FF6770" + }, + { + "value": "medium", + "label": "中", + "color": "#28AB80" + }, + { + "value": "low", + "label": "低", + "color": "#7C8597" + }, + { + "value": "insignificant", + "label": "无关紧要", + "color": "#7C8597" + } + ], + "pure_options": [], + "html_type": "select", + "memo": "", + "options": { + "urgent": "紧急", + "high": "高", + "medium": "中", + "low": "低", + "insignificant": "无关紧要" + } + }, + "severity": { + "name": "severity", + "label": "严重程度", + "options": { + "fatal": "致命", + "serious": "严重", + "normal": "一般", + "prompt": "提示", + "advice": "建议" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "platform": { + "name": "platform", + "label": "软件平台", + "options": { + "PC": "PC", + "其他": "其他" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "os": { + "name": "os", + "label": "操作系统", + "options": { + "All": "All", + "Windows XP": "Windows XP", + "Windows 2000": "Windows 2000", + "Windows NT": "Windows NT", + "Linux": "Linux", + "Unix": "Unix" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "testmode": { + "name": "testmode", + "label": "测试方式", + "options": { + "手工测试": "手工测试", + "自动化测试": "自动化测试" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "testtype": { + "name": "testtype", + "label": "测试类型", + "options": { + "功能测试": "功能测试", + "性能测试": "性能测试", + "界面测试": "界面测试" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "testphase": { + "name": "testphase", + "label": "测试阶段", + "options": { + "单元测试": "单元测试", + "集成测试": "集成测试", + "系统测试": "系统测试", + "运营测试": "运营测试" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "source": { + "name": "source", + "label": "缺陷根源", + "options": { + "需求": "需求", + "设计": "设计", + "编码": "编码", + "其它": "其它" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "frequency": { + "name": "frequency", + "label": "重现规律", + "options": { + "可重现": "可重现", + "随机重现": "随机重现", + "不可重现": "不可重现" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "cc": { + "name": "cc", + "label": "抄送人", + "options": [], + "pure_options": [], + "html_type": "mix_chooser", + "memo": "" + }, + "fixer": { + "name": "fixer", + "label": "修复人", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "closer": { + "name": "closer", + "label": "关闭人", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "participator": { + "name": "participator", + "label": "参与人", + "options": [], + "pure_options": [], + "html_type": "user_chooser", + "memo": "" + }, + "version_report": { + "name": "version_report", + "label": "发现版本", + "options": { + "ver1.1": "ver1.1", + "[v1.16.1]计费下单支付-支持渠道物品信息反查": "[v1.16.1]计费下单支付-支持渠道物品信息反查", + "测试": "测试" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "version_test": { + "name": "version_test", + "label": "验证版本", + "options": { + "ver1.1": "ver1.1", + "[v1.16.1]计费下单支付-支持渠道物品信息反查": "[v1.16.1]计费下单支付-支持渠道物品信息反查", + "测试": "测试" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "version_fix": { + "name": "version_fix", + "label": "合入版本", + "options": { + "ver1.1": "ver1.1", + "[v1.16.1]计费下单支付-支持渠道物品信息反查": "[v1.16.1]计费下单支付-支持渠道物品信息反查", + "测试": "测试" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "version_close": { + "name": "version_close", + "label": "关闭版本", + "options": { + "ver1.1": "ver1.1", + "[v1.16.1]计费下单支付-支持渠道物品信息反查": "[v1.16.1]计费下单支付-支持渠道物品信息反查", + "测试": "测试" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "baseline_find": { + "name": "baseline_find", + "label": "发现基线", + "options": [], + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "baseline_join": { + "name": "baseline_join", + "label": "合入基线", + "options": [], + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "baseline_close": { + "name": "baseline_close", + "label": "关闭基线", + "options": [], + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "baseline_test": { + "name": "baseline_test", + "label": "验证基线", + "options": [], + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "originphase": { + "name": "originphase", + "label": "发现阶段", + "options": { + "需求阶段": "需求阶段", + "架构阶段": "架构阶段", + "设计阶段": "设计阶段", + "编码阶段": "编码阶段", + "测试阶段": "测试阶段", + "上线阶段": "上线阶段" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "sourcephase": { + "name": "sourcephase", + "label": "引入阶段", + "options": { + "需求阶段": "需求阶段", + "架构阶段": "架构阶段", + "设计阶段": "设计阶段", + "编码阶段": "编码阶段", + "测试阶段": "测试阶段", + "集成阶段": "集成阶段" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "bugtype": { + "name": "bugtype", + "label": "缺陷类型", + "options": { + "SQL注入": "SQL注入", + "XSS": "XSS", + "CSRF": "CSRF", + "访问控制": "访问控制", + "权限控制": "权限控制", + "其它": "其它" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "reject_time": { + "name": "reject_time", + "label": "拒绝时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "in_progress_time": { + "name": "in_progress_time", + "label": "接受处理时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "resolved": { + "name": "resolved", + "label": "解决时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "verify_time": { + "name": "verify_time", + "label": "验证时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "assigned_time": { + "name": "assigned_time", + "label": "分配时间", + "options": [], + "pure_options": [], + "html_type": "datetime", + "memo": "" + }, + "iteration_id": { + "name": "iteration_id", + "label": "迭代", + "options": { + "1010104801001787437": "测试自定义字段", + "1010104801001662155": "fromAPI", + "1010104801001641399": "create by api", + "1010104801001641397": "页面新迭代", + "1010104801001595935": "锁定的迭代", + "1010104801001179909": "aaaassssss", + "1020419374001202991": "adra", + "1010104801001179887": "cjdd", + "1010104801001103985": "对对对", + "1010104801001112221": "myI", + "1010104801001080913": "【Word】191001常规发布", + "1010104801001171067": "oyctest20190911xxx1111", + "1010104801001169803": "oyctest20190911xxx1111", + "1010104801001105965": "oyctest20190911xxx1111", + "1010104801001105193": "oyctest20190911xxx1111", + "1010104801001082017": "日常发布", + "1010104801001082013": "日常发布", + "1010104801001081899": "oyctest20190917ABC2", + "1010104801001080783": "日常发布", + "1010104801001123207": "oyctest20190911", + "1010104801000507689": "i1", + "1010104801000423181": "迭代1", + "1010104801001733159": "测试创建迭代2", + "1010104801001628193": "API创建测试", + "1010104801001628057": "测试创建迭代2", + "1010104801001628055": "测试创建迭代" + }, + "pure_options": [ + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299600000", + "value": "1010104801001787437", + "label": "测试自定义字段", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299500000", + "value": "1010104801001662155", + "label": "fromAPI", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299400000", + "value": "1010104801001641399", + "label": "create by api", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299300000", + "value": "1010104801001641397", + "label": "页面新迭代", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299200000", + "value": "1010104801001595935", + "label": "锁定的迭代", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "117990900000", + "value": "1010104801001179909", + "label": "aaaassssss", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299100000", + "value": "1020419374001202991", + "label": "adra", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "117988700000", + "value": "1010104801001179887", + "label": "cjdd", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "110398500000", + "value": "1010104801001103985", + "label": "对对对", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "111222100000", + "value": "1010104801001112221", + "label": "myI", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "108091300000", + "value": "1010104801001080913", + "label": "【Word】191001常规发布", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "117106700000", + "value": "1010104801001171067", + "label": "oyctest20190911xxx1111", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "116980300000", + "value": "1010104801001169803", + "label": "oyctest20190911xxx1111", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "110596500000", + "value": "1010104801001105965", + "label": "oyctest20190911xxx1111", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "110519300000", + "value": "1010104801001105193", + "label": "oyctest20190911xxx1111", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "108201700000", + "value": "1010104801001082017", + "label": "日常发布", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "108201300000", + "value": "1010104801001082013", + "label": "日常发布", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "108189900000", + "value": "1010104801001081899", + "label": "oyctest20190917ABC2", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "108078300000", + "value": "1010104801001080783", + "label": "日常发布", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "112320700000", + "value": "1010104801001123207", + "label": "oyctest20190911", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "50768900000", + "value": "1010104801000507689", + "label": "i1", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "42318100000", + "value": "1010104801000423181", + "label": "迭代1", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "0", + "value": "1010104801001733159", + "label": "测试创建迭代2", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "0", + "value": "1010104801001628193", + "label": "API创建测试", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "0", + "value": "1010104801001628057", + "label": "测试创建迭代2", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "0", + "value": "1010104801001628055", + "label": "测试创建迭代", + "panel": 0 + } + ], + "html_type": "select", + "lock_info": [], + "memo": "" + }, + "project_id": { + "name": "project_id", + "label": "所属项目", + "options": { + "10104801": "TAPD 乌云" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "begin": { + "name": "begin", + "label": "预计开始", + "options": [], + "pure_options": [], + "html_type": "dateinput", + "memo": "" + }, + "due": { + "name": "due", + "label": "预计结束", + "options": [], + "pure_options": [], + "html_type": "dateinput", + "memo": "" + }, + "label": { + "name": "label", + "label": "标签", + "options": { + "三方依赖": "三方依赖", + "待讨论": "待讨论", + "提高优先级": "提高优先级", + "管理层关注": "管理层关注", + "紧急需求": "紧急需求", + "非常棒": "非常棒", + "阻塞": "阻塞", + "开发受阻": "开发受阻", + "有风险": "有风险", + "等待设计走查": "等待设计走查", + "方案已沟通": "方案已沟通", + "等待转测": "等待转测" + }, + "pure_options": [], + "html_type": "multi_select", + "memo": "" + }, + "size": { + "name": "size", + "label": "规模", + "options": [], + "pure_options": [], + "html_type": "integer", + "memo": "" + }, + "effort": { + "name": "effort", + "label": "预估工时", + "options": [], + "pure_options": [], + "html_type": "float", + "memo": "" + }, + "effort_completed": { + "name": "effort_completed", + "label": "完成工时", + "options": [], + "pure_options": [], + "html_type": "float", + "memo": "" + }, + "remain": { + "name": "remain", + "label": "剩余工时", + "options": [], + "pure_options": [], + "html_type": "float", + "memo": "" + }, + "exceed": { + "name": "exceed", + "label": "超出工时", + "options": [], + "pure_options": [], + "html_type": "float", + "memo": "" + }, + "progress": { + "name": "progress", + "label": "进度", + "options": [], + "pure_options": [], + "html_type": "input", + "memo": "" + }, + "estimate": { + "name": "estimate", + "label": "预计解决时间", + "options": [], + "pure_options": [], + "html_type": "int", + "memo": "" + }, + "custom_field_four": { + "name": "custom_field_four", + "label": "文本的", + "options": [], + "pure_options": [], + "html_type": "text", + "memo": "" + }, + "custom_field_one": { + "name": "custom_field_one", + "label": "测试下拉", + "options": { + "xxx": "xxx", + "qqq": "qqq" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "custom_field_two": { + "name": "custom_field_two", + "label": "测试字段", + "options": [], + "pure_options": [], + "html_type": "text", + "memo": "" + }, + "custom_field_three": { + "name": "custom_field_three", + "label": "123", + "options": [], + "pure_options": [], + "html_type": "text", + "memo": "" + }, + "custom_plan_field_1": { + "name": "custom_plan_field_1", + "label": "超级迭代", + "options": { + "1010104801001754573": "快速迭代1" + }, + "pure_options": [ + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299600000", + "plan_app_id": "1010104801000000397", + "value": "1010104801001754573", + "label": "快速迭代1", + "panel": 0 + } + ], + "html_type": "select", + "memo": "" + }, + "custom_plan_field_2": { + "name": "custom_plan_field_2", + "label": "发布计划", + "options": { + "1010104801001934461": "v2test", + "1010104801001934459": "test", + "1010104801001934457": "发布", + "1010104801001934455": "4324" + }, + "pure_options": [ + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299700000", + "plan_app_id": "1010104801000085167", + "value": "1010104801001934461", + "label": "v2test", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299700000", + "plan_app_id": "1010104801000085167", + "value": "1010104801001934459", + "label": "test", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299700000", + "plan_app_id": "1010104801000085167", + "value": "1010104801001934457", + "label": "发布", + "panel": 0 + }, + { + "parent_id": "0", + "workspace_id": "10104801", + "sort": "120299700000", + "plan_app_id": "1010104801000085167", + "value": "1010104801001934455", + "label": "4324", + "panel": 0 + } + ], + "html_type": "select", + "memo": "" + }, + "priority_label": { + "name": "priority_label", + "label": "优先级", + "color_options": [ + { + "value": "urgent", + "label": "紧急", + "color": "#FF6770" + }, + { + "value": "high", + "label": "高", + "color": "#FF6770" + }, + { + "value": "medium", + "label": "中", + "color": "#28AB80" + }, + { + "value": "low", + "label": "低", + "color": "#7C8597" + }, + { + "value": "insignificant", + "label": "无关紧要", + "color": "#7C8597" + } + ], + "options": { + "urgent": "紧急", + "high": "高", + "medium": "中", + "low": "低", + "insignificant": "无关紧要" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + }, + "release_id": { + "name": "release_id", + "label": "发布计划", + "options": { + "1010104801000069739": "v2test", + "1010104801000068721": "test", + "1010104801000068405": "发布", + "1010104801000058771": "4324" + }, + "pure_options": [], + "html_type": "select", + "memo": "" + } + }, + "info": "success" +} +``` + +### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#返回格式说明)返回格式说明 + +| 字段 | 说明 | +| :-------: | :------: | +| name | name | +| options | 候选值 | +| html_type | 类型 | +| label | 中文名称 | + +### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#返回候选值内容)返回候选值内容 + +| 缺陷字段 | 说明 | +| :------------: | :----------------------: | +| priority | 优先级 | +| severity | 严重程度 | +| status | 状态 枚举值 (可选值) | +| iteration_id | 迭代 枚举值 (可选值) | +| module | 模块 枚举值 (可选值) | +| release_id | 发布计划 枚举值 (可选值) | +| version_report | 发现版本 枚举值 (可选值) | +| version_test | 验证版本 枚举值 (可选值) | +| version_fix | 合入版本 枚举值 (可选值) | +| version_close | 关闭版本 枚举值 (可选值) | +| baseline_find | 发现基线 枚举值 (可选值) | +| baseline_join | 合入基线 枚举值 (可选值) | +| baseline_test | 验证基线 枚举值 (可选值) | +| baseline_close | 关闭基线 枚举值 (可选值) | +| os | 操作系统 枚举值 (可选值) | +| platform | 软件平台 枚举值 (可选值) | +| testmode | 测试方式 枚举值 (可选值) | +| testphase | 测试阶段 枚举值 (可选值) | +| testtype | 测试类型 枚举值 (可选值) | +| source | 缺陷根源 枚举值 (可选值) | +| bugtype | 缺陷类型 枚举值 (可选值) | +| frequency | 重现规律 枚举值 (可选值) | +| originphase | 发现阶段 枚举值 (可选值) | +| sourcephase | 引入阶段 枚举值 (可选值) | +| resolution | 解决方法 枚举值 (可选值) | +| custom_field_* | 解决方法 枚举值 (可选值) | + +### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#缺陷状态-status-可选值说明)缺陷状态(status)可选值说明 + +status状态是支持每个项目单独配置的,所以状态(status)没有固定的中英文映射, 只能通过该接口获取. + +### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#缺陷优先级-priority-取值字段说明)缺陷优先级(priority)取值字段说明 + +为了兼容自定义优先级,`请使用 priority_label 字段`,详情参考:[如何兼容自定义优先级](https://open.tapd.cn/document/api-doc/API文档/subject/custom_priority/) 。 + +### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#缺陷严重程度-severity-可选值说明)缺陷严重程度(severity)可选值说明 + +| 取值 | 字面值 | +| :-----: | :----: | +| fatal | 致命 | +| serious | 严重 | +| normal | 一般 | +| prompt | 提示 | +| advice | 建议 | + +### [#](https://open.tapd.cn/document/api-doc/API文档/api_reference/bug/get_bug_fields_info.html#缺陷解决方法-resolution-可选值说明)缺陷解决方法(resolution)可选值说明 + +| 取值 | 字面值 | +| :------------------: | :------------: | +| ignore | 无需解决 | +| fix | 延期解决 | +| failed | 无法重现 | +| external | 外部原因 | +| duplicated | 重复 | +| intentional | 设计如此 | +| unclear | 问题描述不准确 | +| hold | 挂起 | +| feature | 需求变更 | +| fixed | 已解决 | +| transferred to story | 已转需求 | + + + ## 缺陷重要字段说明 | 字段 | 说明 | diff --git a/config/config.ini b/config/config.ini index ffe9fc5..1630cc3 100644 --- a/config/config.ini +++ b/config/config.ini @@ -2,7 +2,7 @@ [TAPD] # TAPD项目ID -workspace_id = 58335167 +workspace_id = 5833516 7 # Bug报告人 reporter = G41小助手 diff --git a/config/token_cache.json b/config/token_cache.json new file mode 100644 index 0000000..f528e81 --- /dev/null +++ b/config/token_cache.json @@ -0,0 +1,4 @@ +{ + "access_token": "NWdoeLm7usm42yi-6fXJ3KjEcY_JzON22cG5tH0Ezio1ytQyRbst9D52xjK01Ga0tXEI6q9U9zeCS1fjM51IfSPGRqo1By4yAgbbNQwIOJEsQr0fMXIRaMN3yi_DauifuRgqyv56h2sfInkuFB_3bhUNTMpe-xp0Vn1iSmg-D9Lo4gMAzvuqxXR1_WPyUZi9H9ZDg_KQtx4YsNMHxvxdl7jA6QvZckwveA96CRAb9fU", + "fetch_time": 1766040126.4893737 +} \ No newline at end of file diff --git a/src/main.py b/src/main.py index 1683af6..3366811 100644 --- a/src/main.py +++ b/src/main.py @@ -17,6 +17,7 @@ from src.smartsheet import SmartSheetAPI from src.validator import RecordValidator from src.tapd_api import TAPDApi from src.mapper import FieldMapper, BugCreationResult +from src.token_manager import TokenManager def parse_arguments(): @@ -31,16 +32,23 @@ def parse_arguments(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 示例用法: + # 手动提供access_token python main.py --access-token YOUR_ACCESS_TOKEN - python main.py -t YOUR_ACCESS_TOKEN --config /path/to/config.ini + + # 自动从环境变量获取access_token(需要设置CORPID和CORPSECRET) + python main.py + + # 指定配置文件路径 + python main.py --config /path/to/config.ini """ ) - # 必需参数 + # 可选参数:access_token(如果不提供,将自动从环境变量获取) parser.add_argument( '-t', '--access-token', - required=True, - help='企业微信access_token(必填)' + required=False, + default=None, + help='企业微信access_token(可选,如不提供则自动从环境变量获取)' ) # 可选参数 @@ -65,22 +73,47 @@ def parse_arguments(): return parser.parse_args() -def validate_access_token(access_token): +def get_or_validate_access_token(access_token_arg): """ - 验证access_token是否有效 + 获取或验证access_token + + 如果命令行提供了token,则验证并使用; + 如果未提供,则通过TokenManager自动获取 Args: - access_token: 待验证的token + access_token_arg: 命令行传入的token(可能为None) + + Returns: + str: 有效的access_token Raises: - ValueError: token无效时抛出 + ValueError: token无效或环境变量未设置时抛出 + RuntimeError: 自动获取token失败时抛出 """ - if not access_token or not access_token.strip(): - raise ValueError("access_token不能为空") + if access_token_arg: + # 命令行提供了token,进行验证 + print("使用命令行提供的access_token") - # 基本格式检查(企业微信的access_token通常较长) - if len(access_token.strip()) < 20: - raise ValueError("access_token格式可能不正确(长度过短)") + if not access_token_arg.strip(): + raise ValueError("access_token不能为空") + + + return access_token_arg.strip() + else: + # 命令行未提供token,使用TokenManager自动获取 + print("命令行未提供access_token,将自动从环境变量获取") + print() + + try: + token_manager = TokenManager() + access_token = token_manager.get_token() + return access_token + except ValueError as e: + # 环境变量未设置 + raise ValueError(f"自动获取access_token失败: {e}") + except RuntimeError as e: + # API调用失败 + raise RuntimeError(f"自动获取access_token失败: {e}") def scan_and_validate_records(access_token: str, docid: str, verbose: bool = False, test_mode: bool = False): @@ -437,7 +470,7 @@ def main(): """主函数""" print("=" * 60) print("autoTAPD - Debug阶段自动开单工具") - print("版本: 0.2.0 (第二阶段)") + print("版本: 0.4.0 (第四阶段 - 支持自动获取access_token)") print("=" * 60) print() @@ -452,10 +485,11 @@ def main(): print(f" - verbose: {args.verbose}") print(f" - test: {args.test}") - # 2. 验证access_token - print("[2/3] 验证access_token...") - validate_access_token(args.access_token) - print(" ✓ access_token格式验证通过") + # 2. 获取或验证access_token + print("[2/3] 获取access_token...") + access_token = get_or_validate_access_token(args.access_token) + print(" ✓ access_token准备就绪") + print() # 3. 加载配置文件 print("[3/3] 加载配置文件...") @@ -472,14 +506,14 @@ def main(): print("-" * 60) print(f"TAPD workspace_id: {all_config['tapd']['workspace_id']}") print(f"SmartSheet docid: {all_config['smartsheet']['docid'][:20]}...") - print(f"Access Token: {args.access_token[:10]}...(已隐藏)") + print(f"Access Token: {access_token[:10]}...(已隐藏)") if args.test: print(f"测试模式: 已启用") print("=" * 60) # 4. 扫描智能表格并校验记录 result = scan_and_validate_records( - args.access_token, + access_token, all_config['smartsheet']['docid'], args.verbose, args.test @@ -509,7 +543,7 @@ def main(): sheet_id = result.get('sheet_id') if sheet_id: writeback_result = write_back_results( - args.access_token, + access_token, all_config['smartsheet']['docid'], sheet_id, creation_result['success_results'], diff --git a/src/smartsheet.py b/src/smartsheet.py index 0c1c9cd..2d5b3a5 100644 --- a/src/smartsheet.py +++ b/src/smartsheet.py @@ -70,16 +70,8 @@ class SmartSheetAPI: import json print(f"\n响应状态码: {response.status_code}") print(f"响应数据:") - # 对于records数据,只显示摘要信息,避免输出过多 - if endpoint == "smartsheet/get_records" and 'records' in result: - display_result = result.copy() - records_count = len(result.get('records', [])) - if records_count > 0: - display_result['records'] = f"[{records_count}条记录,已省略详情]" - display_result['_records_sample'] = result['records'][0] if records_count > 0 else None - print(json.dumps(display_result, ensure_ascii=False, indent=2)) - else: - print(json.dumps(result, ensure_ascii=False, indent=2)) + # 测试模式下显示完整的响应数据 + print(json.dumps(result, ensure_ascii=False, indent=2)) print("=" * 80) # 检查企业微信API返回的错误码 @@ -219,11 +211,11 @@ class SmartSheetAPI: # 构造过滤条件:开单状态字段为空 filter_spec = { - "conjunction": "and", + "conjunction": "CONJUNCTION_AND", "conditions": [ { "field_id": status_field_id, - "operator": "is_empty" + "operator": "OPERATOR_IS_EMPTY" } ] } diff --git a/src/token_manager.py b/src/token_manager.py new file mode 100644 index 0000000..fc06617 --- /dev/null +++ b/src/token_manager.py @@ -0,0 +1,328 @@ +""" +企业微信 Access Token 管理模块 +负责自动获取、缓存和刷新 access_token + +该模块完全独立,不依赖其他业务模块 +""" + +import os +import json +import time +import requests +from pathlib import Path +from typing import Optional, Dict + + +class TokenManager: + """企业微信 Access Token 管理器""" + + # 企业微信获取token的API地址 + TOKEN_API_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" + + # Token有效期(秒),企业微信返回的是7200秒 + TOKEN_EXPIRES_IN = 7200 + + # 提前刷新时间(秒),在token过期前5分钟就刷新 + REFRESH_BEFORE_EXPIRE = 300 + + def __init__(self, cache_file_path: Optional[str] = None): + """ + 初始化Token管理器 + + Args: + cache_file_path: token缓存文件路径,如果为None则使用默认路径 + """ + # 确定缓存文件路径 + if cache_file_path is None: + # 默认路径:项目根目录/config/token_cache.json + project_root = Path(__file__).parent.parent + cache_file_path = project_root / "config" / "token_cache.json" + + self.cache_file_path = Path(cache_file_path) + + # 从环境变量读取企业微信配置 + self.corpid = os.environ.get('CORPID') + self.corpsecret = os.environ.get('CORPSECRET') + + def _validate_env_config(self): + """ + 验证环境变量配置是否完整 + + Raises: + ValueError: 环境变量未设置时抛出 + """ + if not self.corpid: + raise ValueError( + "环境变量 CORPID 未设置\n" + "请设置环境变量: export CORPID=your_corpid" + ) + + if not self.corpsecret: + raise ValueError( + "环境变量 CORPSECRET 未设置\n" + "请设置环境变量: export CORPSECRET=your_corpsecret" + ) + + def _fetch_token_from_api(self) -> Dict[str, any]: + """ + 从企业微信API获取新的access_token + + Returns: + Dict: 包含access_token和expires_in的字典 + + Raises: + RuntimeError: API调用失败时抛出 + """ + print("正在从企业微信API获取新的access_token...") + + try: + params = { + 'corpid': self.corpid, + 'corpsecret': self.corpsecret + } + + response = requests.get(self.TOKEN_API_URL, params=params, timeout=10) + response.raise_for_status() + result = response.json() + + # 检查返回的错误码 + errcode = result.get('errcode', 0) + if errcode != 0: + errmsg = result.get('errmsg', '未知错误') + raise RuntimeError(f"获取access_token失败: errcode={errcode}, errmsg={errmsg}") + + access_token = result.get('access_token') + expires_in = result.get('expires_in', self.TOKEN_EXPIRES_IN) + + if not access_token: + raise RuntimeError("API返回的数据中未找到access_token") + + print(f" ✓ 成功获取access_token (有效期: {expires_in}秒)") + + return { + 'access_token': access_token, + 'expires_in': expires_in + } + + except requests.exceptions.Timeout: + raise RuntimeError("获取access_token超时,请检查网络连接") + except requests.exceptions.RequestException as e: + raise RuntimeError(f"获取access_token失败: {e}") + + def _load_cache(self) -> Optional[Dict]: + """ + 从缓存文件加载token + + Returns: + Optional[Dict]: 缓存的token数据,如果文件不存在或格式错误则返回None + """ + if not self.cache_file_path.exists(): + return None + + try: + with open(self.cache_file_path, 'r', encoding='utf-8') as f: + cache_data = json.load(f) + + # 验证缓存数据格式 + if 'access_token' not in cache_data or 'fetch_time' not in cache_data: + print(" ⚠ 缓存文件格式不正确,将重新获取token") + return None + + return cache_data + + except json.JSONDecodeError: + print(" ⚠ 缓存文件JSON格式错误,将重新获取token") + return None + except Exception as e: + print(f" ⚠ 读取缓存文件失败: {e},将重新获取token") + return None + + def _save_cache(self, access_token: str, fetch_time: float): + """ + 保存token到缓存文件 + + Args: + access_token: access_token值 + fetch_time: 获取时间(时间戳) + + Raises: + RuntimeError: 保存失败时抛出 + """ + try: + # 确保缓存目录存在 + self.cache_file_path.parent.mkdir(parents=True, exist_ok=True) + + cache_data = { + 'access_token': access_token, + 'fetch_time': fetch_time + } + + with open(self.cache_file_path, 'w', encoding='utf-8') as f: + json.dump(cache_data, f, ensure_ascii=False, indent=2) + + print(f" ✓ Token已缓存到: {self.cache_file_path}") + + except Exception as e: + raise RuntimeError(f"保存token缓存失败: {e}") + + def _is_token_expired(self, fetch_time: float) -> bool: + """ + 检查token是否已过期或即将过期 + + Args: + fetch_time: token获取时间(时间戳) + + Returns: + bool: 如果已过期或即将过期返回True,否则返回False + """ + current_time = time.time() + elapsed_time = current_time - fetch_time + + # 如果已使用时间超过 (有效期 - 提前刷新时间),则认为需要刷新 + return elapsed_time >= (self.TOKEN_EXPIRES_IN - self.REFRESH_BEFORE_EXPIRE) + + def get_token(self) -> str: + """ + 获取有效的access_token + + 优先从缓存读取,如果缓存不存在或已过期,则从API获取新token + + Returns: + str: 有效的access_token + + Raises: + ValueError: 环境变量未设置时抛出 + RuntimeError: 获取token失败时抛出 + """ + # 1. 验证环境变量配置 + self._validate_env_config() + + # 2. 尝试从缓存加载 + print("检查token缓存...") + cache_data = self._load_cache() + + if cache_data: + access_token = cache_data['access_token'] + fetch_time = cache_data['fetch_time'] + + # 检查是否过期 + if not self._is_token_expired(fetch_time): + elapsed_time = time.time() - fetch_time + remaining_time = self.TOKEN_EXPIRES_IN - elapsed_time + print(f" ✓ 使用缓存的token (剩余有效期: {int(remaining_time)}秒)") + return access_token + else: + print(" ⚠ 缓存的token已过期或即将过期,将重新获取") + else: + print(" ⚠ 未找到有效的token缓存") + + # 3. 从API获取新token + token_data = self._fetch_token_from_api() + access_token = token_data['access_token'] + fetch_time = time.time() + + # 4. 保存到缓存 + self._save_cache(access_token, fetch_time) + + return access_token + + def refresh_token(self) -> str: + """ + 强制刷新token(无论是否过期) + + Returns: + str: 新的access_token + + Raises: + ValueError: 环境变量未设置时抛出 + RuntimeError: 获取token失败时抛出 + """ + print("强制刷新access_token...") + + # 验证环境变量配置 + self._validate_env_config() + + # 从API获取新token + token_data = self._fetch_token_from_api() + access_token = token_data['access_token'] + fetch_time = time.time() + + # 保存到缓存 + self._save_cache(access_token, fetch_time) + + return access_token + + def clear_cache(self): + """ + 清除token缓存文件 + """ + if self.cache_file_path.exists(): + try: + self.cache_file_path.unlink() + print(f" ✓ 已清除token缓存: {self.cache_file_path}") + except Exception as e: + print(f" ✗ 清除缓存失败: {e}") + else: + print(" ℹ 缓存文件不存在,无需清除") + + +def get_access_token(cache_file_path: Optional[str] = None) -> str: + """ + 便捷函数:获取有效的access_token + + 这是一个简化的接口,用于快速获取token而不需要创建TokenManager实例 + + Args: + cache_file_path: token缓存文件路径,如果为None则使用默认路径 + + Returns: + str: 有效的access_token + + Raises: + ValueError: 环境变量未设置时抛出 + RuntimeError: 获取token失败时抛出 + """ + manager = TokenManager(cache_file_path) + return manager.get_token() + + +if __name__ == "__main__": + """测试代码""" + print("=" * 60) + print("Token Manager 测试") + print("=" * 60) + print() + + try: + # 测试获取token + manager = TokenManager() + token = manager.get_token() + + print("\n" + "=" * 60) + print("测试结果") + print("=" * 60) + print(f"✓ 成功获取access_token") + print(f" Token (前20字符): {token[:20]}...") + print(f" Token长度: {len(token)}") + print("=" * 60) + + # 测试再次获取(应该使用缓存) + print("\n再次获取token(测试缓存)...") + token2 = manager.get_token() + + if token == token2: + print(" ✓ 成功使用缓存的token") + else: + print(" ⚠ 获取了新的token(可能缓存失效)") + + except ValueError as e: + print(f"\n✗ 配置错误: {e}") + print("\n请设置以下环境变量:") + print(" export CORPID=your_corpid") + print(" export CORPSECRET=your_corpsecret") + except RuntimeError as e: + print(f"\n✗ 运行错误: {e}") + except Exception as e: + print(f"\n✗ 未预期的错误: {e}") + import traceback + traceback.print_exc() diff --git a/开发路线.md b/开发路线.md index 460c3c3..8d76231 100644 --- a/开发路线.md +++ b/开发路线.md @@ -7,14 +7,14 @@ ## 二、开发说明 ### 2.1 access_token管理策略 -- **第五阶段前:** 所有需要access_token的操作,由开发者手动获取token并传入程序 -- **第五阶段:** 实现access_token的自动获取与缓存机制 -- **第五阶段后:** 程序自动管理access_token的获取、缓存和刷新 +- **第四阶段前:** 所有需要access_token的操作,由开发者手动获取token并传入程序 +- **第四阶段:** 实现access_token的自动获取与缓存机制 +- **第四阶段后:** 程序自动管理access_token的获取、缓存和刷新 ### 2.2 日志记录策略 - **所有api调用记录都要写入log文件夹下的api_log.json** -- **第八阶段前:** 使用简单的print输出关键信息 -- **第八阶段:** 实现完整的日志记录系统 +- **第九阶段前:** 使用简单的print输出关键信息 +- **第九阶段:** 实现完整的日志记录系统 ### 2.3 API频率限制 @@ -269,9 +269,9 @@ - 重试机制 **验收标准:** -- [ ] 开单成功后能正确回写✅到"开单状态"字段 -- [ ] 能正确回写TAPD单号,并生成可点击的链接 -- [ ] 能正确回写bug状态 +- [x] 开单成功后能正确回写✅到"开单状态"字段 +- [x] 能正确回写TAPD单号,并生成可点击的链接 +- [x] 能正确回写bug状态 - [ ] 开单失败后能正确回写❌到"开单状态"字段 - [ ] 回写失败时有重试机制 - [ ] 所有操作都有详细的日志记录 @@ -288,102 +288,152 @@ --- -### 第四阶段:定时任务与服务化 +### 第四阶段:access_token自动获取与缓存 -**目标:** 将工具部署为服务,实现定时扫描和状态同步 +**目标:** 实现access_token的自动获取与缓存机制,不再需要手动传入token,该模块应该与其他功能独立 + +**任务清单:** + +1. 实现企业微信认证模块 + - 从环境变量读取CORPID和CORPSECRET + - 调用企业微信API获取access_token + - 处理认证失败的情况 +2. 设计token缓存文件结构 + - 在项目根目录创建token缓存文件(token_cache.json) + - 存储token值 + - 存储获取时间(时间戳) +3. 实现token缓存逻辑 + - 首次获取时写入缓存文件 + - 每次使用前从文件读取并检查是否过期(当前时间 - 获取时间 >= 7200秒) + - 过期则重新获取并更新缓存文件 +4. 实现token失效处理 + - 检测token失效(API返回42001错误码) + - 自动重新获取并更新缓存 +5. 重构现有代码 + - 保留命令行传入access_token的逻辑,如无传入则自动从缓存获取 + - 所有API调用自动使用缓存的token + - 修改wework_api.py,添加token管理功能 + +**验收标准:** +- [x] 能从环境变量读取corpid和corpsecret +- [x] 能自动获取access_token并写入缓存文件 +- [x] token能正确从文件读取和复用 +- [x] token过期时能自动重新获取 +- [ ] token失效时(API返回42001)能自动重新获取 +- [x] 不再需要手动传入access_token(命令行参数可选) +- [ ] 减少了获取token的API调用次数 +- [ ] 环境变量未设置时有清晰的错误提示 +- [x] 缓存文件格式清晰易读 + +*修复了开单状态不为空的记录也被读取的问题:filter_spec的conjunction应该为CONJUNCTION_AND而不是and + +**技术要点:** + +- token有效期为7200秒(2小时) +- 使用时间戳判断过期(time.time()) +- 使用os.environ读取环境变量 +- JSON文件的读写操作 +- 异常处理:网络错误、认证失败、文件读写错误等 + +**缓存文件格式示例:** +```json +{ + "access_token": "xxx", + "fetch_time": 1702800000 +} +``` + +**潜在问题记录:** +- 多实例部署时的token文件竞争问题(本阶段暂不处理) +- 时钟不同步导致的过期判断错误 +- 环境变量的安全性问题 +- 缓存文件的权限问题 + +--- + +### 第五阶段:定时任务与服务化 + +**目标:** 实现定时任务调度,让工具能够自动定期执行开单扫描 **任务清单:** 1. 实现定时任务调度 - 使用schedule库或APScheduler - 配置开单扫描频率(默认5分钟) - - 配置状态同步频率(默认15分钟) -2. 实现bug状态同步功能 - - 查询已开单的记录 - - 调用TAPD API获取bug最新状态 - - 更新智能表格的"bug状态"字段 -3. 实现服务启动和停止 - - 命令行参数解析 - - 优雅退出机制 -4. 实现配置文件扩展 - - 添加轮询频率配置 - - 添加状态同步频率配置 -5. 完善日志和监控 - - 添加运行统计信息 - - 添加性能监控 + - 创建调度脚本(scheduler.py) +2. 实现服务启动和停止 + - 命令行参数解析(启动、停止) + - 优雅退出机制(捕获SIGINT、SIGTERM信号) +3. 实现配置文件扩展 + - 在config.ini中添加轮询频率配置 +4. 完善执行统计 + - 每次执行后输出统计信息(扫描X条,成功Y条,失败Z条) + - 记录执行时间 **验收标准:** - [ ] 服务能正常启动和停止 - [ ] 能按配置的频率执行开单扫描 -- [ ] 能按配置的频率执行状态同步 -- [ ] 状态同步功能正常工作 -- [ ] 服务异常时能自动恢复 -- [ ] 有完善的运行日志和统计信息 +- [ ] 每次执行都调用main.py的核心逻辑 +- [ ] 服务异常时能自动恢复(捕获异常后继续下次执行) +- [ ] 有清晰的执行统计信息 +- [ ] Ctrl+C能优雅退出 **技术要点:** -- 定时任务的线程安全 -- 信号处理(SIGINT、SIGTERM) +- schedule库的基本使用 +- 信号处理(signal模块) - 异常捕获和恢复 -- 配置热加载(可选) +- 循环执行的设计 **潜在问题记录:** - 长时间运行的内存泄漏问题 -- access_token过期的自动刷新 -- 并发执行的冲突处理 +- 并发执行的冲突处理(如果上次执行未完成) --- -### 第五阶段:access_token自动获取与缓存 +### 第六阶段:bug状态同步功能 -**目标:** 实现access_token的自动获取与缓存机制,不再需要手动传入token +**目标:** 实现定期同步TAPD的bug状态到智能表格 **任务清单:** -1. 实现企业微信认证模块 - - 从环境变量读取corpid和corpsecret - - 调用企业微信API获取access_token - - 处理认证失败的情况 -2. 设计token缓存结构 - - 存储token值 - - 存储过期时间 - - 存储获取时间 -3. 实现token缓存逻辑 - - 首次获取时缓存 - - 使用前检查是否过期 - - 过期前自动刷新(建议提前5分钟) -4. 实现持久化存储(可选) - - 使用文件存储token - - 服务重启后能恢复token -5. 实现token失效处理 - - 检测token失效(API返回42001错误码) - - 自动重新获取 -6. 重构现有代码 - - 保留命令行传入access_token的逻辑,如无传入则自动调用缓存的token - - 所有API调用自动使用缓存的token +1. 实现状态同步功能模块 + - 查询智能表格中已开单的记录("开单状态"为✅且"TAPD单号"不为空) + - 过滤掉状态为"完成"或"取消"的记录 + - 调用TAPD API获取bug最新状态 + - 对比状态是否变化 +2. 实现状态回写逻辑 + - 只有状态发生变化时才更新智能表格 + - 批量更新智能表格的"bug状态"字段 +3. 集成到定时任务 + - 在scheduler.py中添加状态同步任务 + - 配置状态同步频率(默认15分钟) +4. 实现配置文件扩展 + - 在config.ini中添加状态同步频率配置 +5. 完善错误处理 + - TAPD API调用失败的处理 + - 智能表格更新失败的处理 **验收标准:** -- [ ] 能从环境变量读取corpid和corpsecret -- [ ] 能自动获取access_token -- [ ] token能正确缓存和复用 -- [ ] token过期前能自动刷新 -- [ ] token失效时能自动重新获取 -- [ ] 不再需要手动传入access_token -- [ ] 减少了获取token的API调用次数 -- [ ] 环境变量未设置时有清晰的错误提示 +- [ ] 能正确查询已开单的记录 +- [ ] 能正确过滤"完成"和"取消"状态的记录 +- [ ] 能成功调用TAPD API获取bug状态 +- [ ] 状态变化时能正确回写到智能表格 +- [ ] 状态未变化时不进行更新(减少API调用) +- [ ] 能按配置的频率执行状态同步 +- [ ] 有清晰的同步统计信息(检查X条,更新Y条) **技术要点:** -- token有效期为7200秒(2小时) -- 建议在过期前5分钟刷新 -- 使用os.environ读取环境变量 -- 线程安全的缓存实现 -- 异常处理:网络错误、认证失败等 +- TAPD获取bug详情API +- 状态对比逻辑 +- 批量更新优化 +- 定时任务的多任务调度 **潜在问题记录:** -- 多实例部署时的token共享问题 -- 时钟不同步导致的过期判断错误 -- 环境变量的安全性问题 +- 大量bug的状态查询性能问题 +- TAPD API频率限制 +- 状态映射的准确性 --- -### 第六阶段:企业微信推送功能 +### 第七阶段:企业微信推送功能 **目标:** 实现开单失败时的企业微信推送通知 @@ -425,48 +475,27 @@ --- -### 第七阶段:字段合法性校验与优化 +### 第八阶段:测试 -**目标:** 完善字段值的合法性校验,提高数据质量 +**目标:** 进行边界测试等,防止出现意外情况导致服务出错 **任务清单:** -1. 实现优先级字段校验 - - 获取TAPD项目的优先级配置 - - 校验智能表格中的值是否合法 -2. 实现严重程度字段校验 - - 校验可选值:fatal、serious、normal、prompt、advice -3. 实现人员字段校验 - - 校验userid格式 - - 校验用户是否存在 -4. 实现模块字段校验 - - 获取TAPD项目的模块配置 - - 校验模块是否存在 -5. 实现版本字段校验 - - 获取TAPD项目的版本配置 - - 校验版本是否存在 -6. 完善错误提示 - - 明确指出不合法的字段和原因 - - 提供修正建议 +1. **验收标准:** -- [ ] 能正确校验所有字段的合法性 -- [ ] 不合法的值有明确的错误提示 -- [ ] 校验失败时不创建TAPD单 -- [ ] 校验结果能回写到智能表格(可选) +- [ ] +- [ ] **技术要点:** -- TAPD API:获取字段配置接口 -- 智能表格:单选字段的option_id -- 数据缓存:避免频繁查询配置 -- 错误信息的友好展示 + +- **潜在问题记录:** -- TAPD配置变更时的同步问题 -- 自定义字段的校验规则 +- --- -### 第八阶段:日志系统与性能优化 +### 第九阶段:日志系统与性能优化 **目标:** 实现完整的日志记录系统,优化系统性能,添加监控和告警 @@ -534,8 +563,8 @@ - argparse:命令行参数解析 - json:JSON文件处理 - datetime:时间处理 - - logging:日志记录(第八阶段) - - schedule/APScheduler:定时任务(第四阶段) + - logging:日志记录(第九阶段) + - schedule/APScheduler:定时任务(第五阶段) - **API:** - 企业微信文档API - TAPD Open API @@ -551,14 +580,15 @@ workspace_id = 10158231 docid = your_doc_id [Schedule] -# 第四阶段添加 +# 第五阶段添加 scan_interval = 5 +# 第六阶段添加 sync_interval = 15 ``` -### 环境变量(第五阶段开始使用) +### 环境变量(第四阶段开始使用) ```bash -# 企业微信(第五阶段) +# 企业微信(第四阶段) WEWORK_CORPID=your_corpid WEWORK_CORPSECRET=your_corpsecret @@ -577,18 +607,20 @@ autoTAPD/ │ ├── __init__.py │ ├── api_test.py # 前期准备:API测试工具 │ ├── config.py # 配置管理 -│ ├── wework_api.py # 企业微信API(第五阶段完善) +│ ├── wework_api.py # 企业微信API(第四阶段完善) │ ├── smartsheet.py # 智能表格操作 │ ├── tapd_api.py # TAPD API │ ├── validator.py # 数据校验 │ ├── mapper.py # 字段映射 -│ ├── token_cache.py # token缓存(第五阶段) -│ ├── scheduler.py # 定时任务(第四阶段) -│ ├── logger.py # 日志模块(第八阶段) +│ ├── scheduler.py # 定时任务(第五阶段) +│ ├── sync_status.py # bug状态同步(第六阶段) +│ ├── logger.py # 日志模块(第九阶段) │ └── main.py # 主程序 -├── logs/ # 日志目录(第八阶段) +├── logs/ # 日志目录(第九阶段) +│ ├── api_log.json # API调用记录 │ └── api_test_log.json # API测试记录 ├── tests/ # 测试代码 +├── token_cache.json # token缓存文件(第四阶段) ├── requirements.txt # 依赖包 └── README.md # 项目说明 ``` @@ -609,8 +641,8 @@ autoTAPD/ 3. **错误恢复:** 服务异常时要能自动恢复,不影响后续执行 4. **向后兼容:** 配置文件和数据结构的变更要考虑向后兼容 5. **文档更新:** 每个阶段完成后更新相关文档 -6. **access_token管理:** 第五阶段前手动传入,第五阶段后自动管理 -7. **日志策略:** 第八阶段前使用print,第八阶段后使用logging模块 +6. **access_token管理:** 第四阶段前手动传入,第四阶段后自动管理 +7. **日志策略:** 第九阶段前使用print,第九阶段后使用logging模块 ## 十、阶段依赖关系 @@ -623,15 +655,17 @@ autoTAPD/ ↓ 第三阶段(回写结果) ↓ -第四阶段(定时任务) +第四阶段(access_token自动化) ↓ -第五阶段(access_token自动化) +第五阶段(定时任务与服务化) ↓ -第六阶段(企业微信推送) +第六阶段(bug状态同步) ↓ -第七阶段(字段合法性校验) +第七阶段(企业微信推送) ↓ -第八阶段(日志系统 + 性能优化) +第八阶段(测试) + ↓ +第九阶段(日志系统 + 性能优化) ``` ## 十一、后续优化方向